/**
* IdentityFacade — IdentityRegistry-compatible facade over unified MetadataBase
*
* Implements the exact same public API as IdentityRegistry but delegates
* all operations to MetadataBase. Zero changes needed in route files.
*
* Key prefixes: unchanged (user:, identity:, pubkey:, sshkey:, noisekey:, username:, revoked:)
* These don't collide with any other namespace.
*/
import crypto from 'bare-crypto'
export class IdentityFacade {
constructor ({ metadataBase }) {
this._meta = metadataBase
}
// ── Properties (delegate to MetadataBase) ────────────────────
get bootstrapKey () { return this._meta.key }
get bootstrapKeyHex () { return this._meta.bootstrapKeyHex }
get discoveryKey () { return this._meta.discoveryKey }
get localKey () { return this._meta.localKey }
get writable () { return this._meta.writable }
get view () { return this._meta.view }
get db () { return this._meta.view } // backward compat alias
// Expose _base for wakeup protocol compatibility
get _base () { return this._meta._base }
async ready () {}
// ── User Management (Write) ──────────────────────────────────
async createUser (data = {}) {
const userId = data.userId || crypto.randomBytes(16).toString('hex')
await this._meta.append({
v: 2,
ns: 'identity',
type: 'create-user',
userId,
username: data.username || null,
name: data.name || data.username || 'Anonymous',
email: data.email || null,
profilePicture: data.profilePicture || null,
globalPermissions: data.permissions || data.globalPermissions || ['read'],
repos: data.repos || [{ name: '*', permissions: ['read'] }],
timestamp: Date.now()
})
await this._meta.update()
return this._meta.getByKey(`user:${userId}`)
}
async deactivateUser (userId, options = {}) {
await this._meta.update()
const user = await this._meta.getByKey(`user:${userId}`)
if (!user) throw new Error(`User not found: ${userId}`)
if (user.deactivatedAt) throw new Error(`User already deactivated: ${userId}`)
await this._meta.append({
v: 2,
ns: 'identity',
type: 'deactivate-user',
userId,
deactivatedBy: options.deactivatedBy || 'system',
reason: options.reason || null,
timestamp: Date.now()
})
await this._meta.update()
return this._meta.getByKey(`user:${userId}`)
}
async reactivateUser (userId) {
await this._meta.update()
const user = await this._meta.getByKey(`user:${userId}`)
if (!user) throw new Error(`User not found: ${userId}`)
if (!user.deactivatedAt) throw new Error(`User not deactivated: ${userId}`)
await this._meta.append({
v: 2,
ns: 'identity',
type: 'reactivate-user',
userId,
timestamp: Date.now()
})
await this._meta.update()
return this._meta.getByKey(`user:${userId}`)
}
async updateUserProfile (userId, updates) {
await this._meta.update()
const user = await this._meta.getByKey(`user:${userId}`)
if (!user) throw new Error(`User not found: ${userId}`)
// Separate arbitrary flags from known fields
const knownFields = ['username', 'name', 'profilePicture', 'email', 'globalPermissions', 'repos']
const flags = {}
const op = {
v: 2,
ns: 'identity',
type: 'update-user',
userId,
timestamp: Date.now()
}
for (const [key, value] of Object.entries(updates)) {
if (knownFields.includes(key)) {
op[key] = value
} else {
flags[key] = value
}
}
if (Object.keys(flags).length > 0) {
op.flags = flags
}
await this._meta.append(op)
await this._meta.update()
return this._meta.getByKey(`user:${userId}`)
}
async upgradePermissions (userId, repoName, newPermissions) {
await this._meta.append({
v: 2,
ns: 'identity',
type: 'upgrade-permissions',
userId,
repoName,
permissions: newPermissions,
timestamp: Date.now()
})
await this._meta.update()
return this._meta.getByKey(`user:${userId}`)
}
async mergeAccounts (primaryUserId, secondaryUserId, options = {}) {
await this._meta.append({
v: 2,
ns: 'identity',
type: 'merge-accounts',
primaryUserId,
secondaryUserId,
timestamp: Date.now()
})
await this._meta.update()
const primary = await this._meta.getByKey(`user:${primaryUserId}`)
const merged = await this._meta.getByKey(`user:${secondaryUserId}`)
return { primary, merged, stats: { identitiesTransferred: 0, permissionsMerged: 0 } }
}
// ── Identity Management (Write) ──────────────────────────────
async createIdentity (data) {
await this._meta.append({
v: 2,
ns: 'identity',
type: 'create-identity',
identityId: data.identityId,
method: data.method,
userId: data.userId || null,
credentials: data.credentials || {},
timestamp: Date.now()
})
await this._meta.update()
return this._meta.getByKey(`identity:${data.identityId}`)
}
async linkIdentity (identityId, userId) {
await this._meta.append({
v: 2,
ns: 'identity',
type: 'link-identity',
identityId,
userId,
timestamp: Date.now()
})
await this._meta.update()
const user = await this._meta.getByKey(`user:${userId}`)
const identity = await this._meta.getByKey(`identity:${identityId}`)
return { user, identity }
}
async unlinkIdentity (identityId) {
await this._meta.update()
const identity = await this._meta.getByKey(`identity:${identityId}`)
if (!identity) throw new Error(`Identity not found: ${identityId}`)
await this._meta.append({
v: 2,
ns: 'identity',
type: 'unlink-identity',
identityId,
timestamp: Date.now()
})
await this._meta.update()
if (identity.userId) {
return this._meta.getByKey(`user:${identity.userId}`)
}
return null
}
async revokeIdentity (identityId, options = {}) {
await this._meta.append({
v: 2,
ns: 'identity',
type: 'revoke-identity',
identityId,
revokedBy: options.revokedBy || 'system',
reason: options.reason || 'No reason provided',
timestamp: Date.now()
})
await this._meta.update()
return this._meta.getByKey(`identity:${identityId}`)
}
async restoreIdentity (identityId) {
await this._meta.append({
v: 2,
ns: 'identity',
type: 'restore-identity',
identityId,
timestamp: Date.now()
})
await this._meta.update()
return this._meta.getByKey(`identity:${identityId}`)
}
async repairIdentity (data) {
await this._meta.append({
v: 2,
ns: 'identity',
type: 'repair-identity',
identityId: data.identityId,
method: data.method,
userId: data.userId || null,
credentials: data.credentials || {},
timestamp: Date.now()
})
await this._meta.update()
return this._meta.getByKey(`identity:${data.identityId}`)
}
async autoRegister (identityData) {
const userId = crypto.randomBytes(16).toString('hex')
const identityId = identityData.identityId
// Create user
await this._meta.append({
v: 2,
ns: 'identity',
type: 'create-user',
userId,
username: identityData.username || null,
name: identityData.name || identityData.username || 'Anonymous',
email: null,
profilePicture: null,
globalPermissions: ['read'],
repos: [{ name: '*', permissions: ['read'] }],
timestamp: Date.now()
})
// Create identity linked to user
await this._meta.append({
v: 2,
ns: 'identity',
type: 'create-identity',
identityId,
method: identityData.method,
userId,
credentials: identityData.credentials || {},
timestamp: Date.now()
})
await this._meta.update()
const user = await this._meta.getByKey(`user:${userId}`)
const identity = await this._meta.getByKey(`identity:${identityId}`)
return { user, identity }
}
// ── Federation (Multi-Writer) ────────────────────────────────
async addWriter (writerKey) {
const keyHex = Buffer.isBuffer(writerKey) ? writerKey.toString('hex') : writerKey
await this._meta.append({
v: 2,
ns: 'identity',
type: 'add-writer',
writerKey: keyHex,
timestamp: Date.now()
})
}
// ── Lookup/Query (Read) ──────────────────────────────────────
async getUser (userId) {
await this._meta.update()
return this._meta.getByKey(`user:${userId}`)
}
async getIdentity (identityId) {
await this._meta.update()
return this._meta.getByKey(`identity:${identityId}`)
}
async getUserByIdentity (identityId) {
await this._meta.update()
const identity = await this._meta.getByKey(`identity:${identityId}`)
if (!identity || !identity.userId) return null
return this._meta.getByKey(`user:${identity.userId}`)
}
async getIdentityByPublicKey (publicKey) {
await this._meta.update()
const entry = await this._meta.getByKey(`pubkey:${publicKey}`)
if (!entry) return null
return this._meta.getByKey(`identity:${entry.identityId}`)
}
async getIdentityBySSHKey (sshPublicKey) {
const fingerprint = this.calculateSSHFingerprint(sshPublicKey)
await this._meta.update()
const entry = await this._meta.getByKey(`sshkey:${fingerprint}`)
if (!entry) return null
return this._meta.getByKey(`identity:${entry.identityId}`)
}
async getIdentityByNoiseKey (noiseKeyHex) {
await this._meta.update()
const entry = await this._meta.getByKey(`noisekey:${noiseKeyHex}`)
if (!entry) return null
return this._meta.getByKey(`identity:${entry.identityId}`)
}
async listUserIdentities (userId) {
await this._meta.update()
const user = await this._meta.getByKey(`user:${userId}`)
if (!user || !user.identities) return []
const results = []
for (const identityId of user.identities) {
const identity = await this._meta.getByKey(`identity:${identityId}`)
if (identity) results.push(identity)
}
return results
}
async isRevoked (identityId) {
await this._meta.update()
const revoked = await this._meta.getByKey(`revoked:${identityId}`)
if (revoked) return true
// Fallback: check identity record
const identity = await this._meta.getByKey(`identity:${identityId}`)
return identity ? !!identity.revoked : false
}
async checkUsernameAvailable (username) {
if (!username) return false
const normalized = username.toLowerCase().trim()
await this._meta.update()
const entry = await this._meta.getByKey(`username:${normalized}`)
return !entry
}
async listRevokedIdentities () {
return this._meta.list('revoked')
}
async listAllUsers () {
return this._meta.list('user')
}
async listAllIdentities () {
return this._meta.list('identity')
}
async getMergePreview (primaryUserId, secondaryUserId) {
await this._meta.update()
const primary = await this._meta.getByKey(`user:${primaryUserId}`)
const secondary = await this._meta.getByKey(`user:${secondaryUserId}`)
if (!primary || !secondary) return null
const secondaryIdentities = []
for (const identityId of (secondary.identities || [])) {
const identity = await this._meta.getByKey(`identity:${identityId}`)
if (identity && !identity.revoked) {
secondaryIdentities.push({ identityId, method: identity.method })
}
}
return {
primary: {
userId: primaryUserId,
name: primary.name,
identityCount: (primary.identities || []).length,
repoCount: (primary.repos || []).length
},
secondary: {
userId: secondaryUserId,
name: secondary.name,
identityCount: (secondary.identities || []).length,
repoCount: (secondary.repos || []).length,
identities: secondaryIdentities
},
willTransfer: {
identities: secondaryIdentities.length,
repoPermissions: (secondary.repos || []).length
}
}
}
// ── Utilities ────────────────────────────────────────────────
calculateSSHFingerprint (sshPublicKey) {
const parts = sshPublicKey.trim().split(/\s+/)
const keyData = parts.length >= 2 ? parts[1] : parts[0]
return crypto.createHash('sha256').update(Buffer.from(keyData, 'base64')).digest('hex').slice(0, 32)
}
// ── Lifecycle ────────────────────────────────────────────────
async replicate (stream) {
return stream
}
async sync (options = {}) {
await this._meta.update()
}
async close () {
// MetadataBase owns the Autobase lifecycle
}
}