OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-identity / lib / auth / providers / atproto.js

64 lines · javascript ref:
import { BaseProvider } from '../base-provider.js'

/**
 * AtprotoProvider — AT-Protocol (did:plc) authentication + binding.
 *
 * Binds a user's atproto identity (did:plc + secp256k1 signing key + handle) to
 * the registry. The binding is seed-anchored: one BIP39 mnemonic yields both the
 * keet Ed25519 identity (our P2P world) and the atproto secp256k1 key (bsky world)
 * — see atproto-bridge/identity.
 *
 * Verification of did:plc control is delegated to an injected `verify` fn (mirrors
 * how KeetIdentityProvider injects IdentityKey), so this package stays dep-light and
 * the atproto crypto lives in one place. `verify(proof)` must return a verified
 * binding `{ did, handle, didKey, ed25519PublicKey }` (or throw/return false).
 */
export class AtprotoProvider extends BaseProvider {
  constructor (identityRegistry, opts = {}) {
    super('atproto')
    this.registry = identityRegistry
    this._verify = opts.verify || null
  }

  async extractCredentials (req) {
    const raw = req.headers['x-atproto-proof']
    if (!raw) return null
    try {
      return { proof: JSON.parse(raw) }
    } catch {
      return null
    }
  }

  async authenticate (credentials) {
    if (!this._verify) throw new Error('atproto verifier not configured')

    const { proof } = credentials
    const binding = await this._verify(proof)
    if (!binding || !binding.did) throw new Error('Invalid atproto proof')

    const { did, handle, didKey, ed25519PublicKey } = binding

    let identity = await this.registry.getIdentityByDid(did)
    if (!identity) {
      // Auto-register. identityId = the did:plc (the atproto identity's own ID);
      // the ed25519 binding link + handle live in credentials.
      await this.registry.registerIdentity({
        identityId: did,
        method: 'atproto',
        credentials: { did, handle, didKey, ed25519PublicKey },
        createdAt: Date.now()
      })
      identity = { identityId: did, method: 'atproto' }
    }

    return {
      identityId: identity.identityId,
      method: 'atproto',
      credentials: { did, handle, didKey }
    }
  }
}

export default AtprotoProvider