import { BaseProvider } from '../base-provider.js'
import { ANProto } from '../../anproto.js'
/**
* WebIdentityProvider — Browser ANProto authentication
*
* Handles Ed25519 challenge-response auth for web clients.
*
* Options:
* autoRegister: true — auto-create user on first auth (default, pear-git/hypernote behavior)
* autoRegister: false — reject unknown keys, require explicit registration
*/
export class WebIdentityProvider extends BaseProvider {
constructor (identityRegistry, challengeManager, opts = {}) {
super('web-identity')
this.registry = identityRegistry
this.challengeManager = challengeManager
this.autoRegister = opts.autoRegister !== false // default: true
}
async extractCredentials (req) {
const publicKey = req.headers['x-wirecore-publickey'] || req.headers['x-identity-publickey']
if (!publicKey) return null
const signature = req.headers['x-wirecore-signature'] || req.headers['x-identity-signature']
const challengeId = req.headers['x-wirecore-challenge'] || req.headers['x-identity-challenge']
return { publicKey, signature, challengeId }
}
async authenticate (credentials) {
const { publicKey, signature, challengeId } = credentials
// Look up identity — existing users may have already proven key ownership
let identity = await this.registry.getIdentityByPublicKey(publicKey)
// Verify signature if provided (challenge-response proves key ownership)
let signatureVerified = false
if (signature && challengeId) {
const challenge = await this.challengeManager.get(challengeId)
if (!challenge) throw new Error('Invalid or expired challenge')
const result = ANProto.verify(challenge.challenge, signature)
if (!result.valid) throw new Error('Invalid signature — ' + (result.error || 'verification failed'))
// Canonical format embeds the public key — verify it matches the claimed key
if (result.publicKey !== publicKey) throw new Error('Signature public key does not match claimed key')
await this.challengeManager.consume(challengeId)
signatureVerified = true
}
// Check revocation
if (identity) {
const revoked = await this.registry.isRevoked(identity.identityId)
if (revoked) throw new Error('Identity has been revoked')
}
if (!identity) {
// SECURITY: New identities MUST prove key ownership via signature.
// Without this check, anyone could register any public key they don't own.
if (!signatureVerified) {
throw new Error('Signature required for new identity registration')
}
if (!this.autoRegister) {
throw new Error('Unknown identity. Register first.')
}
const result = await this.registry.autoRegister({
identityId: publicKey,
method: 'web-identity',
credentials: { publicKey }
})
identity = result.identity
}
return {
identityId: identity.identityId || publicKey,
method: 'web-identity',
credentials: { publicKey }
}
}
}
export default WebIdentityProvider