/**
* Deterministic Apply Function for Identity Registry Autobase
*
* Processes linearized operations and updates the Hyperbee view.
* This function MUST be fully deterministic — it may be called
* multiple times as Autobase reorders causal forks.
*
* View schema (Hyperbee keys):
* user:{userId} → { userId, name, email, profilePicture, globalPermissions, repos, identities, createdAt, updatedAt }
* identity:{identityId} → { identityId, method, userId, credentials, createdAt, updatedAt }
* sshkey:{fingerprint} → { identityId } (reverse lookup for ssh-key)
* noisekey:{publicKey} → { identityId } (reverse lookup for noise-key)
* pubkey:{publicKey} → { identityId } (reverse lookup for web-identity / keet-identity)
* username:{username} → { userId } (reverse lookup for uniqueness)
* revoked:{identityId} → { identityId, revokedAt, revokedBy, reason }
*/
import { decodeOp } from './identity-operations.js'
/**
* Apply function for Autobase
*
* @param {Array} batch - Linearized operation nodes
* @param {Hyperbee} view - Hyperbee view to update
* @param {Object} base - Autobase host (unused for identity ops)
*/
export async function apply (batch, view, base) {
const b = view.batch()
for (const node of batch) {
const op = decodeOp(node.value)
// Handle writer addition (Autobase side-effect, not a view update)
if (op.type === 'add-writer') {
const key = typeof op.writerKey === 'string' ? Buffer.from(op.writerKey, 'hex') : op.writerKey
await base.addWriter(key, { indexer: true })
continue
}
switch (op.type) {
case 'create-user': {
const existing = await b.get(`user:${op.userId}`)
if (existing) break
const username = op.username || null
await b.put(`user:${op.userId}`, {
userId: op.userId,
username,
name: op.name || op.username || 'Anonymous',
email: op.email || null,
profilePicture: op.profilePicture || null,
globalPermissions: op.globalPermissions || ['read'],
repos: op.repos || [{ name: '*', permissions: ['read'] }],
identities: [],
createdAt: op.timestamp,
updatedAt: op.timestamp
})
// Username uniqueness index
if (username) {
await b.put(`username:${username}`, { userId: op.userId })
}
break
}
case 'update-user': {
const entry = await b.get(`user:${op.userId}`)
if (!entry) break
const user = entry.value
if (op.username !== undefined && op.username !== user.username) {
// Remove old username index
if (user.username) await b.del(`username:${user.username}`)
// Add new username index
if (op.username) await b.put(`username:${op.username}`, { userId: op.userId })
user.username = op.username
}
if (op.name !== undefined) user.name = op.name
if (op.profilePicture !== undefined) user.profilePicture = op.profilePicture
if (op.email !== undefined) user.email = op.email
if (op.globalPermissions !== undefined) user.globalPermissions = op.globalPermissions
if (op.repos !== undefined) user.repos = op.repos
// Arbitrary flags (restoredFromAttestation, etc.)
if (op.flags) {
for (const [key, value] of Object.entries(op.flags)) {
user[key] = value
}
}
user.updatedAt = op.timestamp
await b.put(`user:${op.userId}`, user)
break
}
case 'deactivate-user': {
const entry = await b.get(`user:${op.userId}`)
if (!entry) break
const user = entry.value
if (user.deactivatedAt) break // already deactivated
user.deactivatedAt = op.timestamp
user.deactivatedBy = op.deactivatedBy || 'system'
user.deactivationReason = op.reason || null
user.updatedAt = op.timestamp
await b.put(`user:${op.userId}`, user)
break
}
case 'reactivate-user': {
const entry = await b.get(`user:${op.userId}`)
if (!entry) break
const user = entry.value
if (!user.deactivatedAt) break // not deactivated
delete user.deactivatedAt
delete user.deactivatedBy
delete user.deactivationReason
user.reactivatedAt = op.timestamp
user.updatedAt = op.timestamp
await b.put(`user:${op.userId}`, user)
break
}
case 'create-identity': {
const existing = await b.get(`identity:${op.identityId}`)
if (existing) break
const identity = {
identityId: op.identityId,
method: op.method,
userId: op.userId || null,
credentials: op.credentials || {},
createdAt: op.timestamp,
updatedAt: op.timestamp
}
await b.put(`identity:${op.identityId}`, identity)
// Create reverse lookups
if (op.method === 'ssh-key') {
const fingerprint = (op.credentials && op.credentials.fingerprint) || null
if (fingerprint) {
await b.put(`sshkey:${fingerprint}`, { identityId: op.identityId })
}
} else if (op.method === 'noise-key') {
const publicKey = (op.credentials && op.credentials.publicKey) || null
if (publicKey) {
await b.put(`noisekey:${publicKey}`, { identityId: op.identityId })
}
} else if (op.method === 'web-identity' || op.method === 'keet-identity') {
const publicKey = (op.credentials && op.credentials.publicKey) || op.identityId
await b.put(`pubkey:${publicKey}`, { identityId: op.identityId })
}
// If userId provided, add to user's identities array
if (op.userId) {
const userEntry = await b.get(`user:${op.userId}`)
if (userEntry) {
const user = userEntry.value
if (!user.identities.includes(op.identityId)) {
user.identities.push(op.identityId)
user.updatedAt = op.timestamp
await b.put(`user:${op.userId}`, user)
}
}
}
break
}
case 'link-identity': {
const identityEntry = await b.get(`identity:${op.identityId}`)
if (!identityEntry) break
const userEntry = await b.get(`user:${op.userId}`)
if (!userEntry) break
const identity = identityEntry.value
identity.userId = op.userId
identity.updatedAt = op.timestamp
await b.put(`identity:${op.identityId}`, identity)
const user = userEntry.value
if (!user.identities.includes(op.identityId)) {
user.identities.push(op.identityId)
user.updatedAt = op.timestamp
await b.put(`user:${op.userId}`, user)
}
break
}
case 'unlink-identity': {
const identityEntry = await b.get(`identity:${op.identityId}`)
if (!identityEntry) break
const identity = identityEntry.value
if (!identity.userId) break
const userEntry = await b.get(`user:${identity.userId}`)
if (userEntry) {
const user = userEntry.value
user.identities = user.identities.filter(id => id !== op.identityId)
user.updatedAt = op.timestamp
await b.put(`user:${identity.userId}`, user)
}
identity.userId = null
identity.updatedAt = op.timestamp
await b.put(`identity:${op.identityId}`, identity)
break
}
case 'revoke-identity': {
const identityEntry = await b.get(`identity:${op.identityId}`)
if (!identityEntry) break
const identity = identityEntry.value
if (identity.revoked) break
identity.revoked = true
identity.revokedAt = op.timestamp
identity.revokedBy = op.revokedBy || 'system'
identity.revocationReason = op.reason || 'No reason provided'
identity.updatedAt = op.timestamp
await b.put(`identity:${op.identityId}`, identity)
await b.put(`revoked:${op.identityId}`, {
identityId: op.identityId,
revokedAt: op.timestamp,
revokedBy: identity.revokedBy,
reason: identity.revocationReason
})
break
}
case 'restore-identity': {
const identityEntry = await b.get(`identity:${op.identityId}`)
if (!identityEntry) break
const identity = identityEntry.value
if (!identity.revoked) break
delete identity.revoked
delete identity.revokedAt
delete identity.revokedBy
delete identity.revocationReason
identity.restoredAt = op.timestamp
identity.updatedAt = op.timestamp
await b.put(`identity:${op.identityId}`, identity)
await b.del(`revoked:${op.identityId}`)
// Restore reverse lookup (pubkey → identity)
if ((identity.method === 'web-identity' || identity.method === 'keet-identity')) {
const pk = identity.credentials?.publicKey || op.identityId
await b.put(`pubkey:${pk}`, { identityId: op.identityId })
} else if (identity.method === 'ssh-key' && identity.credentials?.fingerprint) {
await b.put(`sshkey:${identity.credentials.fingerprint}`, { identityId: op.identityId })
} else if (identity.method === 'noise-key' && identity.credentials?.publicKey) {
await b.put(`noisekey:${identity.credentials.publicKey}`, { identityId: op.identityId })
}
// Reactivate the parent user if it was deactivated
if (identity.userId) {
const userEntry = await b.get(`user:${identity.userId}`)
if (userEntry && userEntry.value.deactivatedAt) {
const user = userEntry.value
delete user.deactivatedAt
delete user.deactivatedBy
delete user.deactivationReason
user.reactivatedAt = op.timestamp
user.updatedAt = op.timestamp
await b.put(`user:${identity.userId}`, user)
}
}
break
}
case 'repair-identity': {
// Force-create or overwrite an identity record + reverse lookup + user link.
// Used to recover from orphaned/revoked identity state.
const identity = {
identityId: op.identityId,
method: op.method,
userId: op.userId || null,
credentials: op.credentials || {},
createdAt: op.timestamp,
updatedAt: op.timestamp
}
await b.put(`identity:${op.identityId}`, identity)
// Recreate reverse lookup
if (op.method === 'web-identity' || op.method === 'keet-identity') {
const pk = (op.credentials && op.credentials.publicKey) || op.identityId
await b.put(`pubkey:${pk}`, { identityId: op.identityId })
} else if (op.method === 'ssh-key' && op.credentials?.fingerprint) {
await b.put(`sshkey:${op.credentials.fingerprint}`, { identityId: op.identityId })
} else if (op.method === 'noise-key' && op.credentials?.publicKey) {
await b.put(`noisekey:${op.credentials.publicKey}`, { identityId: op.identityId })
}
// Remove any revocation record
await b.del(`revoked:${op.identityId}`)
// Add to user's identities array if userId provided
if (op.userId) {
const userEntry = await b.get(`user:${op.userId}`)
if (userEntry) {
const user = userEntry.value
if (!user.identities.includes(op.identityId)) {
user.identities.push(op.identityId)
user.updatedAt = op.timestamp
await b.put(`user:${op.userId}`, user)
}
}
}
break
}
case 'upgrade-permissions': {
const userEntry = await b.get(`user:${op.userId}`)
if (!userEntry) break
const user = userEntry.value
let repo = user.repos.find(r => r.name === op.repoName)
if (!repo) {
repo = { name: op.repoName, permissions: [] }
user.repos.push(repo)
}
// Set union of permissions
repo.permissions = [...new Set([...repo.permissions, ...op.permissions])]
user.updatedAt = op.timestamp
await b.put(`user:${op.userId}`, user)
break
}
case 'merge-accounts': {
const primaryEntry = await b.get(`user:${op.primaryUserId}`)
const secondaryEntry = await b.get(`user:${op.secondaryUserId}`)
if (!primaryEntry || !secondaryEntry) break
const primary = primaryEntry.value
const secondary = secondaryEntry.value
// Skip if already merged
if (secondary.mergedInto) break
// Transfer identities from secondary to primary (skip revoked)
for (const identityId of secondary.identities) {
const idEntry = await b.get(`identity:${identityId}`)
if (!idEntry) continue
const identity = idEntry.value
if (identity.revoked) continue
identity.userId = op.primaryUserId
identity.updatedAt = op.timestamp
await b.put(`identity:${identityId}`, identity)
if (!primary.identities.includes(identityId)) {
primary.identities.push(identityId)
}
}
// Merge repo permissions (Set union)
for (const secondaryRepo of secondary.repos) {
const existingRepo = primary.repos.find(r => r.name === secondaryRepo.name)
if (existingRepo) {
existingRepo.permissions = [...new Set([...existingRepo.permissions, ...secondaryRepo.permissions])]
} else {
primary.repos.push({ ...secondaryRepo })
}
}
// Merge global permissions
primary.globalPermissions = [...new Set([...primary.globalPermissions, ...secondary.globalPermissions])]
primary.updatedAt = op.timestamp
await b.put(`user:${op.primaryUserId}`, primary)
// Mark secondary as merged
secondary.mergedInto = op.primaryUserId
secondary.mergedAt = op.timestamp
secondary.identities = []
secondary.repos = []
secondary.globalPermissions = []
secondary.updatedAt = op.timestamp
await b.put(`user:${op.secondaryUserId}`, secondary)
break
}
}
}
await b.flush()
}