/**
 * T3 · The SINGLE resolver mapping a verified capability token → its authoritative `accountId`.
 * Per the frozen T1 convention (CAP_TOKEN_SUB_IS_ACCOUNT_ID), `sub` IS the accountId. Kept as a
 * named, greppable function so the cross-tenant gate (INV1) compares a value derived only from
 * authenticated material — never a client field (INV3).
 */
import type { CapabilityToken, AuthenticatedPrincipal } from '../types.js'

export class AuthzInputError extends Error {
  constructor(message: string) {
    super(message)
    this.name = 'AuthzInputError'
  }
}

/** Returns `token.sub` (the accountId); throws on empty/malformed sub (deny-by-default input guard). */
export function accountIdFromToken(token: CapabilityToken): string {
  const sub = token.sub
  if (typeof sub !== 'string' || sub.length === 0) {
    throw new AuthzInputError('capability token has empty/malformed sub')
  }
  return sub
}

/** Synthesize the token-derived principal for an allow outcome (account-scoped; INV3). */
export function principalFromToken(token: CapabilityToken): AuthenticatedPrincipal {
  return {
    kind: 'human',
    accountId: accountIdFromToken(token),
    principalId: accountIdFromToken(token),
    amr: [],
    authAt: token.iat,
    stepUpAt: null,
  }
}
