OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / packages / services / builtin / identity-service.js

179 lines · javascript ref:
/**
 * Identity Service
 *
 * Manages keypair identities and peer verification.
 * Apps use this to verify peer identities, manage local
 * keypairs, and resolve pubkeys to names.
 *
 * Integrates with the Identity Protocol Layer (IPL) when available
 * to provide developer identity resolution via attestations and
 * Nostr profile lookup.
 *
 * Capabilities:
 *   - whoami: Get local node identity (+ developer info if attested)
 *   - verify: Verify a signed message
 *   - sign: Sign a message with the node's key
 *   - resolve: Resolve a pubkey to a name (IPL attestation → device allowlist)
 *   - peers: List connected peers and their identity info
 *   - developer: Look up a developer profile and their app keys
 */

import { ServiceProvider } from 'p2p-hiverelay/core/services/provider.js'
import sodium from 'sodium-universal'
import b4a from 'b4a'

export class IdentityService extends ServiceProvider {
  constructor () {
    super()
    this.node = null
  }

  /** Lazy access to Identity Protocol Layer (initialized after services start) */
  get _ipl () {
    return this.node?.identity || null
  }

  manifest () {
    return {
      name: 'identity',
      version: '1.1.0',
      description: 'Keypair identity management, peer verification, and developer resolution',
      capabilities: ['whoami', 'verify', 'sign', 'resolve', 'peers', 'developer']
    }
  }

  async start (context) {
    this.node = context.node
  }

  async stop () {
    this.node = null
  }

  async whoami () {
    const result = {
      pubkey: this.node.publicKey ? b4a.toString(this.node.publicKey, 'hex') : null,
      name: this.node.config.name || null,
      mode: this.node.mode
    }

    // Enrich with developer identity if IPL has an attestation for this node's key
    if (this._ipl && result.pubkey) {
      try {
        const dev = await this._ipl.resolveIdentity(result.pubkey)
        if (dev && dev.developerKey) {
          result.developer = {
            key: dev.developerKey,
            profile: dev.profile || null,
            attestedAt: dev.attestation?.timestamp || null
          }
        }
      } catch {}
    }

    return result
  }

  async verify (params) {
    const { message, signature, pubkey } = params
    const msgBuf = b4a.from(message)
    const sigBuf = b4a.from(signature, 'hex')
    const pkBuf = b4a.from(pubkey, 'hex')

    if (sigBuf.length !== sodium.crypto_sign_BYTES) {
      return { valid: false, reason: 'invalid signature length' }
    }
    if (pkBuf.length !== sodium.crypto_sign_PUBLICKEYBYTES) {
      return { valid: false, reason: 'invalid pubkey length' }
    }

    const valid = sodium.crypto_sign_verify_detached(sigBuf, msgBuf, pkBuf)
    return { valid }
  }

  async sign (params, context) {
    if (!this.node.keyPair) {
      throw new Error('NO_KEYPAIR: node has no signing keypair')
    }
    // Remote callers must not be able to sign arbitrary data with the node's key
    if (context?.caller === 'remote') {
      throw new Error('UNAUTHORIZED: sign is not available to remote peers')
    }

    const msgBuf = b4a.from(params.message)
    const sigBuf = b4a.alloc(sodium.crypto_sign_BYTES)
    sodium.crypto_sign_detached(sigBuf, msgBuf, this.node.keyPair.secretKey)

    return {
      message: params.message,
      signature: b4a.toString(sigBuf, 'hex'),
      pubkey: b4a.toString(this.node.keyPair.publicKey, 'hex')
    }
  }

  async resolve (params) {
    const { pubkey } = params

    // Try IPL attestation resolution first
    if (this._ipl) {
      try {
        const dev = await this._ipl.resolveIdentity(pubkey)
        if (dev && dev.developerKey) {
          return {
            pubkey,
            name: dev.profile?.displayName || dev.profile?.name || null,
            developerKey: dev.developerKey,
            source: 'attestation',
            profile: dev.profile || null
          }
        }
      } catch {}
    }

    // Fall back to device allowlist
    if (this.node.accessControl) {
      const devices = this.node.listDevices()
      const device = devices.find(d => d.pubkey === pubkey)
      if (device) {
        return { pubkey, name: device.name, source: 'device-allowlist' }
      }
    }

    return { pubkey, name: null, source: 'not-found' }
  }

  async peers () {
    const connections = this.node.connections
    if (!connections) return []
    // Handle both Map (production) and Array (test) formats
    const entries = connections instanceof Map
      ? Array.from(connections.values())
      : connections
    if (entries.length === 0) return []
    return entries.map(c => ({
      pubkey: c.remotePubkey || c.remotePubKey || null,
      type: c.type || 'hyperswarm'
    }))
  }

  async developer (params) {
    if (!this._ipl) {
      return { error: 'Identity protocol not available' }
    }

    const { key } = params
    if (!key) {
      throw new Error('MISSING_PARAM: developer key is required')
    }

    const profile = await this._ipl.developers.getProfile(key)
    const apps = this._ipl.attestation.getDeveloperApps(key)

    return {
      developerKey: key,
      profile,
      apps
    }
  }
}