/** * T7 — pairing-code issuance (INDEX §4.5 step 1). Mints a single-use short-TTL code; stores * `{ code_hash, account_id, expires_at, redeem_attempts:0 }` — the RAW code is never stored * (INV5). `accountId` comes from the authenticated principal (INV3), never a request field. */ import type { PairingCodeRecord } from '../model/records.js' import type { PairingStore } from '../store/ports.js' import type { AuditWriter } from '../audit/log.js' import { noopAuditWriter } from '../audit/log.js' import { generatePairingCode, formatPairingCode } from './code.js' import { sha256Hex } from '../util/hash.js' import { nowIso } from '../util/ids.js' export interface IssuedPairing { readonly code: string // display-grouped, returned ONCE readonly expiresAt: string } export interface PairingIssuer { issuePairingCode(accountId: string): Promise } export interface PairingIssuerDeps { readonly pairing: PairingStore readonly pairingTtlSec: number readonly audit?: AuditWriter readonly actor?: string } export function createPairingIssuer(deps: PairingIssuerDeps): PairingIssuer { const audit = deps.audit ?? noopAuditWriter() const actor = deps.actor ?? 'system' return { async issuePairingCode(accountId) { const canonical = generatePairingCode() const codeHash = sha256Hex(canonical) // deterministic hash of a 130-bit input (INV5) const expiresAt = new Date(Date.now() + deps.pairingTtlSec * 1000).toISOString() const record: PairingCodeRecord = { codeHash, accountId, expiresAt, redeemedAt: null } await deps.pairing.insert(record) await audit.writeAuditEvent({ action: 'pairing.issue', principalId: actor, accountId, hostId: null, ts: nowIso(), meta: { expiresAt }, }) // Raw code returned ONCE, display-grouped; never persisted. return { code: formatPairingCode(canonical), expiresAt } }, } }