OverviewFilesCommitsIssuesDocsReleasesPeers

pgh / lib / commands / identity.js

88 lines · javascript ref:
/**
 * pgh identity <subcommand>
 *
 * Subcommands:
 *   show   — print root pubkey, device pubkey, chain depth, source
 *
 * Identity lives at ~/.hyperident/ and is loaded via hyper-identity. The
 * `chain` field is the base64-encoded keet attestation proof; when
 * present, the envelope verifier resolves signatures back to the root.
 * When absent (legacy / raw fallback), pgh operates as a self-rooted
 * anproto identity.
 */

import path from '#node:path'
import { homedir } from '#node:os'
import b4a from 'b4a'
import cenc from 'compact-encoding'

import { loadOrCreateIdentity } from '../identity.js'

const HYPERIDENT_HOME = path.join(homedir(), '.hyperident')

export const usage = `pgh identity — show this machine's identity

  pgh identity show                        Root/device pubkey, chain depth, source

Identity lives at ~/.hyperident/ (loaded via hyper-identity).`

export async function run (_client, args, globals = {}) {
  const sub = args[0] || 'show'
  if (sub === 'show') return showIdentity(globals)
  throw new Error('unknown identity subcommand: ' + sub + '\n' + usage)
}

async function showIdentity (globals) {
  const identity = await loadOrCreateIdentity(HYPERIDENT_HOME)

  // Decode chain depth directly via keet-identity-key's ProofEncoding.
  // Kept out of identity.js so that module stays free of keet-identity-key.
  let chainDepth = null
  let chainValid = null
  if (identity.chain) {
    try {
      const [{ ProofEncoding }, { default: IdentityKey }] = await Promise.all([
        import('keet-identity-key/lib/encoding.js'),
        import('keet-identity-key')
      ])
      const proof = b4a.from(identity.chain, 'base64')
      const decoded = cenc.decode(ProofEncoding, proof)
      chainDepth = decoded.chain.length
      chainValid = !!IdentityKey.verify(proof)
    } catch {
      // Leave chainDepth/chainValid at null — chain is present but we
      // couldn't inspect it (old version skew, etc.)
    }
  }

  const result = {
    source: identity.source,
    rootPubkey: identity.rootPubkeyHex || identity.publicKeyHex,
    devicePubkey: identity.publicKeyHex,
    chainPresent: !!identity.chain,
    chainDepth,
    chainValid,
    createdAt: identity.createdAt
      ? new Date(identity.createdAt).toISOString()
      : null
  }

  if (globals.format === 'json') return result

  const self = result.rootPubkey === result.devicePubkey
  const chainLine = result.chainPresent
    ? (result.chainDepth != null
        ? result.chainDepth + ' link(s)' + (result.chainValid === false ? ' (INVALID)' : '')
        : 'present (unparseable)')
    : 'none'

  const lines = [
    'identity (source: ' + result.source + ')',
    '  root pubkey:   ' + (result.rootPubkey || '-'),
    '  device pubkey: ' + (result.devicePubkey || '-') + (self ? '  (self-rooted)' : ''),
    '  chain:         ' + chainLine,
    '  created:       ' + (result.createdAt || '-')
  ]
  return lines.join('\n')
}