OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git / scripts / old-apply / identity-apply.js

399 lines · javascript ref:
/**
 * 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()
}