/** * Core types for the HoloScript Secrets Broker. * * A secrets broker issues short-lived, scope-bounded capability receipts * ("handles") instead of exposing secret material. Any AI surface — mobile, * desktop, headless — receives a handle valid for one session or task. * * Design principles: * 1. Handles-only: the broker never returns plaintext in-band. * 2. Scope-bounded: each grant lists exact secret refs the agent may resolve. * 3. Time-bounded: TTL enforces session-scoped, not long-lived, credentials. * 4. Policy-gated: HoloDoor (or equivalent) checks every issuance. * 5. Audit-heavy: every grant, resolve, and revocation emits a signed receipt. * * @module secrets-broker/types */ /** A canonical reference to a secret. Format: `:` or URI-style infra refs. * Surface may be `env`, `x402`, `custodial`, `gold`, `vault`, or `infra`. * The string is the audit-safe label — NEVER the value. */ type SecretRef = string; /** A canonical capability reference. Format: `cap:///`. * Only `cap://daemon/secrets/*` capabilities are accepted for brokered grants. */ type CapabilityRef = string; /** Policy outcome from the gatekeeper (HoloDoor or adapter). */ type PolicyOutcome = 'allow' | 'warn' | 'block'; /** Issuance parameters for a brokered secret grant. */ interface SecretGrantInput { /** Namespace that scopes the secretRef (workspace, team, project, etc.). */ namespaceId: string; /** Registered agent identity (x402 seat, HoloMesh agentId, etc.). */ agentId: string; /** Canonical secret reference the agent needs access to. */ secretRef: SecretRef; /** Capability the agent claims it needs (must be `cap://daemon/secrets/*`). */ capabilityRef: CapabilityRef; /** Human-readable purpose for audit and compliance. */ purpose: string; /** TTL in seconds (default 15 min, max 1 h). */ ttlSeconds?: number; /** Optional fixed clock for deterministic tests. */ now?: Date; /** If the grant was pre-checked by a policy gate, record the decision id. */ policyDecisionId?: string; /** If the grant was pre-checked, record the outcome. */ policyOutcome?: PolicyOutcome; } /** Immutable receipt issued when a secret grant succeeds. * Contains ZERO secret material — only handles, hashes, and audit metadata. */ interface SecretGrantReceipt { version: 1; event: 'secret.granted'; /** Deterministic grant id. */ grantId: string; namespaceId: string; agentId: string; /** Convenience alias matching agentId. */ agent: string; /** The canonical secret handle. */ secretRef: SecretRef; /** Convenience alias matching secretRef. */ ref: SecretRef; capabilityRef: CapabilityRef; purpose: string; issuedAt: string; expiresAt: string; /** Always `brokered-handle` — plaintext is NEVER returned in-band. */ accessMode: 'brokered-handle'; plaintextReturned: false; /** HoloDoor decision id that gated this grant, if any. */ policyDecisionId: string | null; /** HoloDoor outcome, if any. */ policyOutcome: Exclude | null; /** SHA-256 over the canonical JSON of this receipt (minus receiptHash). */ receiptHash: string; auditTags: string[]; } /** Policy configuration enforced before a grant is issued. */ interface SecretGrantPolicyConfig { allowedSecretRefPrefixes?: string[]; blockedSecretRefPrefixes?: string[]; allowedCapabilityRefs?: string[]; blockedCapabilityRefs?: string[]; allowedAgentIds?: string[]; blockedAgentIds?: string[]; maxTtlSeconds?: number; requirePurpose?: boolean; } /** Structured policy gate definition consumed by the broker. */ interface SecretBrokerPolicy { secretGrants?: SecretGrantPolicyConfig; enforcement?: { onViolation?: 'warn' | 'block'; }; } /** Result of a policy check before issuance. */ interface PolicyDecision { version: 1; event: 'holodoor.policy.checked'; decisionId: string; outcome: PolicyOutcome; reasons: string[]; namespaceId: string; agentId: string; secretRef: SecretRef; capabilityRef: CapabilityRef; requestedTtlSeconds: number; effectiveTtlSeconds: number; checkedAt: string; plaintextReturned: false; receiptHash: string; auditTags: string[]; } /** Combined result when a grant is gated by policy. */ interface PolicyGatedGrant { policyDecision: PolicyDecision; grant: SecretGrantReceipt; } /** Error thrown when policy blocks a grant. */ declare class SecretGrantPolicyError extends Error { readonly decision: PolicyDecision; constructor(decision: PolicyDecision); } /** A handle entry in the broker manifest. */ interface BrokerSecretHandle { name: string; ref: SecretRef; usedBy: string[]; access: 'broker-only'; } /** Manifest that maps human-readable names to scoped secret refs. */ interface BrokerManifest { version: 1; namespaceId: string; storage: 'server-side' | 'github-actions-secret' | 'env-file' | 'vault'; plaintextInNamespace: false; handlesOnly: true; handles: BrokerSecretHandle[]; grantEndpoint: string; brokerCapabilities: CapabilityRef[]; } /** Lease adapter interface — the broker never holds leases itself; * it delegates to a vault-lease registry (e.g. HoloMesh vault-lease-registry). */ interface LeaseAdapter { /** Issue a lease scoped to the given task and agent. */ issueLease(params: { taskId: string; agentId: string; scope: SecretRef[]; durationMs?: number; }): Promise<{ leaseId: string; expiresAt: string; }>; /** Resolve whether the lease permits reading `secretRef`. Returns boolean; * the actual value is fetched by a separate secret-store adapter. */ resolveLease(params: { leaseId: string; agentId: string; secretRef: SecretRef; }): Promise<{ ok: boolean; reason?: string; }>; /** Revoke a lease early (task done, agent compromise, rotation, etc.). */ revokeLease(params: { leaseId: string; reason: string; by: string; }): Promise<{ ok: boolean; }>; } /** Device-flow provisioning result for a new AI surface. */ interface DeviceFlowProvisionResult { status: 'executed' | 'reused'; handle: string; surface: string; seatId: string; walletAddress: string; bearer?: string; agentId?: string; envVarLines: string[]; } /** * Secret grant issuance — handles-only, deterministic, audit-heavy. * * Extracted from `@holoscript/studio/src/lib/workspace/secretBroker.ts` * and generalized into a sovereign primitive so any package or service * can issue brokered grants without depending on Studio internals. * * @module secrets-broker/grant */ /** * Check a grant request against a policy gate without issuing the grant. * Returns a `PolicyDecision` that can be stored, audited, and later passed * to `createSecretGrant` via `policyDecisionId` / `policyOutcome`. */ declare function checkSecretGrantPolicy(input: SecretGrantInput, policy?: SecretBrokerPolicy): PolicyDecision; /** * Issue a brokered secret grant receipt. NEVER returns the plaintext value. * The receipt is deterministic and self-hashing — any party can verify it * was issued by a broker that checked the namespace scope and capability. */ declare function createSecretGrant(input: SecretGrantInput): SecretGrantReceipt; /** * Policy-gated convenience wrapper: check policy, then issue grant. * Throws `SecretGrantPolicyError` when the policy outcome is `block`. */ declare function createPolicyGatedSecretGrant(input: SecretGrantInput, policy?: SecretBrokerPolicy): PolicyGatedGrant; /** * Policy gate helpers for the secrets broker. * * The canonical policy check lives in `grant.ts` (`checkSecretGrantPolicy`). * This module exports convenience builders for common policy shapes. * * @module secrets-broker/policy */ /** Build a policy that only allows a specific set of secret refs. */ declare function allowOnly(refs: string[]): SecretBrokerPolicy; /** Build a policy that blocks everything (deny-by-default). */ declare function denyAll(): SecretBrokerPolicy; /** Build a policy that allows a single agent and a single secret ref. */ declare function allowAgentForRef(agentId: string, ref: string): SecretBrokerPolicy; /** Build a policy from a workspace-like HoloDoor policy JSON shape. */ declare function fromHoloDoorPolicy(shape: { secretGrants?: SecretGrantPolicyConfig; enforcement?: { onViolation?: 'warn' | 'block'; }; }): SecretBrokerPolicy; /** * Lease adapter — bridge between the secrets broker and a vault-lease registry. * * The broker itself never stores leases. It delegates to an adapter that * wraps a vault implementation (e.g. HoloMesh vault-lease-registry, * HashiCorp Vault, AWS Secrets Manager, or an in-memory mock for tests). * * @module secrets-broker/lease-adapter */ /** * In-memory lease adapter for tests and local development. * NOT for production — secrets are not persisted and leases evaporate * on process exit. */ declare function createMemoryLeaseAdapter(): LeaseAdapter; /** * No-op lease adapter that always denies. Useful as a safe default * when no vault is configured. */ declare function createNoOpLeaseAdapter(): LeaseAdapter; /** * Postgres-backed lease adapter — production persistence for the secrets broker. * * Like {@link createMemoryLeaseAdapter}, this NEVER returns secret material: * `resolveLease` answers only the boolean question "may this agent read this * ref under this lease?". The plaintext is fetched by a separate secret-store * adapter after the lease check passes. * * ── Decoupling ────────────────────────────────────────────────────────────── * The adapter takes an INJECTED `query` runner rather than a hardcoded `pg.Pool`, * so it is testable with an in-memory fake and reusable across any driver. The * established in-repo pattern is `pg.Pool#query` — e.g. * `packages/mcp-server/src/auth/postgres-token-store.ts` does * `import type { Pool } from 'pg'` then `this.pool.query(sql, params)`, and the * holomesh stores (`team-store.ts`, `state-store.ts`) use the same shape. To * wire this adapter against a real database, pass that pool's bound method: * * ```ts * import { Pool } from 'pg'; * import { createPostgresLeaseAdapter, SECRET_LEASES_DDL } from './postgres-lease-adapter'; * * const pool = new Pool({ connectionString: process.env.DATABASE_URL }); * await pool.query(SECRET_LEASES_DDL); // or run the migration * const adapter = createPostgresLeaseAdapter({ * query: (sql, params) => pool.query(sql, params as unknown[]), * }); * ``` * * The adapter itself imports no driver — only `node:crypto` — so this package * adds no runtime dependency. * * @module secrets-broker/postgres-lease-adapter */ /** * Minimal query-runner contract the adapter depends on. Structurally compatible * with `pg.Pool#query` / `pg.PoolClient#query` (those return additional fields * such as `rowCount`, which this contract simply ignores). */ interface LeaseQueryRunner { query(sql: string, params: readonly unknown[]): Promise<{ rows: Array>; }>; } /** Dependencies for {@link createPostgresLeaseAdapter}. */ interface PostgresLeaseAdapterDeps { /** Injected query runner (e.g. a bound `pg.Pool#query`). */ query: LeaseQueryRunner['query']; /** Injectable clock for deterministic time math. Defaults to `() => new Date()`. */ now?: () => Date; } /** * DDL for the `secret_leases` table. Idempotent (`IF NOT EXISTS`) so it can be * run on boot or applied as a migration. Exported so callers can ensure the * schema without depending on a migration runner. */ declare const SECRET_LEASES_DDL = "\nCREATE TABLE IF NOT EXISTS secret_leases (\n lease_id TEXT PRIMARY KEY,\n task_id TEXT NOT NULL,\n agent_id TEXT NOT NULL,\n scope JSONB NOT NULL,\n expires_at TIMESTAMPTZ NOT NULL,\n revoked BOOLEAN NOT NULL DEFAULT FALSE,\n revoked_reason TEXT,\n revoked_by TEXT,\n created_at TIMESTAMPTZ NOT NULL DEFAULT now()\n);\nCREATE INDEX IF NOT EXISTS idx_secret_leases_agent ON secret_leases (agent_id);\nCREATE INDEX IF NOT EXISTS idx_secret_leases_task ON secret_leases (task_id);\nCREATE INDEX IF NOT EXISTS idx_secret_leases_expires ON secret_leases (expires_at);\n"; /** * Create a production Postgres-backed {@link LeaseAdapter}. * * `resolveLease` applies the EXACT same checks, in the same order, as * {@link createMemoryLeaseAdapter}: not-found → revoked → expired → * agent-mismatch → scope-violation. It returns a boolean verdict and NEVER a * secret value. * * The adapter performs no schema management itself — run {@link SECRET_LEASES_DDL} * (or the migration) before first use. */ declare function createPostgresLeaseAdapter(deps: PostgresLeaseAdapterDeps): LeaseAdapter; /** * Encrypted per-owner SecretStore — the value-holding half of the secrets broker. * * The lease adapter (`lease-adapter.ts`) only answers "may this agent read this * ref?" — it never stores or decrypts the secret VALUE. This module is that * missing half: it holds the encrypted value behind a `vault:` SecretRef * (`types.ts`), keyed per OWNER, and only decrypts for the authenticated owner. * * ── Crypto: envelope encryption (matches the house style in * `packages/mcp-server/src/holomesh/identity/custodial-wallet.ts`) ────────── * * value ──AES-256-GCM(DEK, iv12)──▶ { ciphertext, iv, authTag } * DEK ──AES-256-GCM(KEK, dekIv12)─▶ { wrappedDek, dekIv, dekAuthTag } * * - One fresh 32-byte DEK per secret (`crypto.randomBytes(32)`). * - One master KEK, sourced by an INJECTED `kekProvider` — this module NEVER * reads `process.env`; the provider owns env-now / KMS-later key sourcing. * - Every row records the `kekId` of the KEK that wrapped its DEK, so * `rotateKek` can find and re-wrap exactly the rows under an old KEK. * * ── Security invariant ────────────────────────────────────────────────────── * `get()` enforces owner isolation INSIDE the function: it fetches the row by * ref, and if `row.owner_id !== ownerId` it throws `OwnerMismatchError` * WITHOUT decrypting. Isolation is never assumed to live in an upstream gate. * * GCM's authentication tag is the integrity guarantee: any tamper of the * ciphertext, or an attempt to unwrap a DEK with the wrong KEK, fails the tag * check and surfaces as `DecryptError` — the store never returns a wrong/garbled * value, it refuses. * * Errors carry ZERO secret material. Storage is behind an injected `backend` * interface so the crypto is testable without Postgres; an in-memory backend * and a Postgres DDL (`SECRET_STORE_DDL`) are exported. * * @module secrets-broker/secret-store */ /** * Key-encryption-key provider. The store calls this to obtain the master KEK * used to wrap/unwrap per-secret DEKs. The provider owns key sourcing — env in * dev, a KMS in production — so this module stays free of `process.env`. * * `getKek(kekId?)` MUST return a 32-byte Buffer. When `kekId` is given the * provider returns THAT specific KEK (needed to unwrap historical rows and to * rotate); when omitted it returns the current KEK. */ interface KekProvider { /** Resolve a 32-byte KEK. With `kekId`, resolve that exact KEK version. */ getKek(kekId?: string): Promise; /** The id of the KEK that `getKek()` (no arg) currently returns. */ currentKekId(): string; /** * Whether this provider sources the KEK from a production-grade store (KMS / HSM / * service-scoped secret) rather than a shared-surface env var. The env provider sets * `false`; the KMS provider sets `true`. Read by the SecretStore's * `requireProductionGradeKek` gate — `undefined` is treated as NOT production-grade. */ readonly productionGrade?: boolean; } /** * A row as persisted by the backend. Holds ONLY ciphertext + crypto metadata; * the plaintext value is never stored or logged. Buffers map to Postgres * `bytea` columns (see {@link SECRET_STORE_DDL}). */ interface SecretRow { /** Row id (uuid). */ id: string; /** Authenticated owner identity that put this secret. */ ownerId: string; /** Human-readable name, unique per owner. */ name: string; /** Canonical ref — `vault:${name}`. */ ref: SecretRef; /** AES-256-GCM ciphertext of the value (under the DEK). */ ciphertext: Buffer; /** 12-byte IV used to encrypt the value. */ iv: Buffer; /** 16-byte GCM auth tag over the value ciphertext. */ authTag: Buffer; /** The DEK, itself AES-256-GCM-encrypted under the KEK. */ wrappedDek: Buffer; /** 12-byte IV used to wrap the DEK. */ dekIv: Buffer; /** 16-byte GCM auth tag over the wrapped DEK. */ dekAuthTag: Buffer; /** Id of the KEK that wrapped `wrappedDek` (tracks rotation). */ kekId: string; /** Monotonic version, bumped on overwrite of an existing name. */ version: number; /** ISO 8601 creation timestamp. */ createdAt: string; /** ISO 8601 timestamp of the last successful owner-authorized get, or null. */ lastUsedAt: string | null; } /** * Storage backend. Crypto lives in the store; persistence lives here, so the * envelope encryption is testable without Postgres. All lookups are scoped by * `ownerId` at the call site; the backend must additionally treat * `(ownerId, name)` as unique. */ interface SecretStoreBackend { /** Insert a fully-encrypted row. */ insert(row: SecretRow): Promise; /** Fetch a single row by `(ownerId, ref)`, or null. */ getByRef(params: { ownerId: string; ref: SecretRef; }): Promise; /** Fetch a single row by `(ownerId, name)`, or null (used to bump version). */ getByName(params: { ownerId: string; name: string; }): Promise; /** All rows for an owner — WITHOUT decrypting. Used by metadata `list()`. */ listByOwner(ownerId: string): Promise; /** All rows wrapped under `kekId`, across owners — used only by rotation. */ listByKekId(kekId: string): Promise; /** Delete a single row by `(ownerId, ref)`. Returns whether a row was removed. */ deleteByRef(params: { ownerId: string; ref: SecretRef; }): Promise; /** Update last-used timestamp after a successful owner-authorized get. */ touchLastUsed(params: { id: string; lastUsedAt: string; }): Promise; /** Re-wrap a row's DEK under a new KEK (rotation). Ciphertext is untouched. */ updateWrappedDek(params: { id: string; wrappedDek: Buffer; dekIv: Buffer; dekAuthTag: Buffer; kekId: string; }): Promise; } /** * Thrown by `get()`/`delete()` when the authenticated caller does not own the * row. Carries only the ref (an audit-safe label) — never the value. */ declare class OwnerMismatchError extends Error { readonly ref: SecretRef; constructor(ref: SecretRef); } /** Thrown when no secret exists for the given owner+ref. */ declare class SecretNotFoundError extends Error { readonly ref: SecretRef; constructor(ref: SecretRef); } /** * Thrown when decryption fails: a tampered value ciphertext (GCM tag mismatch), * or a DEK that cannot be unwrapped because the wrong KEK was supplied. Carries * only the ref — never plaintext, key bytes, or the underlying crypto error * detail (which can leak oracle signal). */ declare class DecryptError extends Error { readonly ref: SecretRef; constructor(ref: SecretRef); } /** * Thrown at construction when `requireProductionGradeKek` is set but the KEK provider is * not production-grade (e.g. the env provider). The Phase-3 gate: no real user secret may * be backed by a shared-surface env KEK in production. */ declare class InsecureKekError extends Error { constructor(); } interface PutInput { /** Authenticated owner identity. */ ownerId: string; /** Human-readable secret name (unique per owner). `ref` is `vault:${name}`. */ name: string; /** The plaintext secret value to encrypt at rest. */ value: string; } interface PutResult { ref: SecretRef; version: number; } interface GetInput { /** Authenticated caller identity — REQUIRED; owner isolation enforced here. */ ownerId: string; /** Canonical ref of the secret to read. */ ref: SecretRef; } interface GetResult { value: string; } /** Metadata-only view of a secret — NEVER includes the value or ciphertext. */ interface SecretMetadata { name: string; ref: SecretRef; version: number; createdAt: string; lastUsedAt: string | null; } interface RotateKekInput { /** KEK id whose rows should be re-wrapped. */ fromKekId: string; /** KEK id to re-wrap them under. */ toKekId: string; } interface RotateKekResult { /** Number of rows re-wrapped from `fromKekId` to `toKekId`. */ rotated: number; } /** The store's dependencies — both injected, nothing read from the ambient env. */ interface SecretStoreDeps { backend: SecretStoreBackend; kekProvider: KekProvider; /** * Phase-3 safety gate: when true, the store REFUSES to construct unless * `kekProvider.productionGrade` is true — so a dev/env KEK can never back real user * secrets in production. The app sets this from NODE_ENV (or an explicit flag). */ requireProductionGradeKek?: boolean; /** Optional fixed clock for deterministic tests. */ now?: () => Date; } interface SecretStore { /** Encrypt + store a value. Bumps version if `name` already exists for owner. */ put(input: PutInput): Promise; /** Owner-isolated decrypt. Throws `OwnerMismatchError` for a non-owner. */ get(input: GetInput): Promise; /** Metadata for all of the owner's secrets — never values. */ list(input: { ownerId: string; }): Promise; /** Owner-scoped delete. */ delete(input: { ownerId: string; ref: SecretRef; }): Promise<{ deleted: boolean; }>; /** Re-wrap every DEK under `fromKekId` with `toKekId`. Values unchanged. */ rotateKek(input: RotateKekInput): Promise; } /** * Create an encrypted per-owner SecretStore over an injected backend + KEK * provider. The store performs envelope encryption; the backend persists * ciphertext; the provider sources KEKs. Nothing is read from `process.env`. */ declare function createSecretStore(deps: SecretStoreDeps): SecretStore; /** * In-memory {@link SecretStoreBackend} for tests and local development. Holds * encrypted rows only — it never sees plaintext (the store seals before insert). * NOT for production: rows evaporate on process exit and enforce uniqueness in * a single process only. */ declare function createInMemorySecretBackend(): SecretStoreBackend; /** * Postgres schema for a production {@link SecretStoreBackend}. Ciphertext and * crypto metadata are `bytea`; plaintext is NEVER a column. `UNIQUE(owner_id, * name)` enforces one current secret per name per owner (re-put bumps version), * and `owner_id` is indexed for owner-scoped lookups and `list()`. * * Map this module's camelCase fields to the snake_case columns in the adapter. */ declare const SECRET_STORE_DDL: "\nCREATE TABLE IF NOT EXISTS secret_store (\n id uuid PRIMARY KEY,\n owner_id text NOT NULL,\n name text NOT NULL,\n ref text NOT NULL,\n ciphertext bytea NOT NULL,\n iv bytea NOT NULL,\n auth_tag bytea NOT NULL,\n wrapped_dek bytea NOT NULL,\n dek_iv bytea NOT NULL,\n dek_auth_tag bytea NOT NULL,\n kek_id text NOT NULL,\n version int NOT NULL DEFAULT 1,\n created_at timestamptz NOT NULL DEFAULT now(),\n last_used_at timestamptz,\n UNIQUE (owner_id, name)\n);\n\nCREATE INDEX IF NOT EXISTS secret_store_owner_id_idx ON secret_store (owner_id);\nCREATE INDEX IF NOT EXISTS secret_store_kek_id_idx ON secret_store (kek_id);\n"; /** * Postgres-backed SecretStore backend — production persistence for HoloKey. * * This is the storage half of the encrypted per-owner SecretStore * (`secret-store.ts`). It holds ONLY ciphertext + crypto metadata; the * plaintext value is never a column, never a param, never logged. Envelope * encryption lives in the store; this module just persists the sealed * {@link SecretRow} and reads it back, byte-for-byte, into and out of the * `secret_store` table ({@link SECRET_STORE_DDL}). * * ── Decoupling (mirrors {@link createPostgresLeaseAdapter}) ────────────────── * The backend takes an INJECTED `query` runner rather than a hardcoded * `pg.Pool`, so it is testable with an in-memory fake and reusable across any * driver. The established in-repo pattern is `pg.Pool#query` — e.g. * `packages/mcp-server/src/auth/postgres-token-store.ts` does * `import type { Pool } from 'pg'` then `this.pool.query(sql, params)`. To wire * this backend against a real database, pass that pool's bound method: * * ```ts * import { Pool } from 'pg'; * import { createPostgresSecretBackend } from './postgres-secret-backend'; * import { SECRET_STORE_DDL, createSecretStore } from './secret-store'; * * const pool = new Pool({ connectionString: process.env.DATABASE_URL }); * await pool.query(SECRET_STORE_DDL); // or run the migration * const backend = createPostgresSecretBackend({ * query: (sql, params) => pool.query(sql, params as unknown[]), * }); * const store = createSecretStore({ backend, kekProvider }); * ``` * * The backend itself imports NO driver, only `SecretStoreBackend`/`SecretRow` * types — this package adds no runtime dependency. * * ── bytea ⇄ Buffer ────────────────────────────────────────────────────────── * The `pg` driver returns a Node `Buffer` for `bytea` columns and accepts a * `Buffer` param for them. So the seven ciphertext/crypto columns round-trip as * Buffers with no encoding step. Every read column is narrowed with a strict * helper (no `any`); a malformed/missing required column throws rather than * silently coercing — a corrupt secret row must fail loud, never return garbage. * * ── timestamptz ⇄ ISO string ──────────────────────────────────────────────── * `SecretRow.createdAt` is an ISO string and `lastUsedAt` is `string | null`. * The `pg` driver hands back a JS `Date` for `timestamptz` (a fake may hand back * an ISO string), so reads normalize via `asIsoString` / `asNullableIsoString`. * Writes pass ISO strings as params; Postgres parses them into `timestamptz`. * * @module secrets-broker/postgres-secret-backend */ /** * Minimal query-runner contract the backend depends on. Structurally compatible * with `pg.Pool#query` / `pg.PoolClient#query` (those return additional fields * such as `rowCount`, which this contract simply ignores). Identical in shape to * the lease adapter's `LeaseQueryRunner`. */ interface SecretQueryRunner { query(sql: string, params: readonly unknown[]): Promise<{ rows: Array>; }>; } /** Dependencies for {@link createPostgresSecretBackend}. */ interface PostgresSecretBackendDeps { /** Injected query runner (e.g. a bound `pg.Pool#query`). */ query: SecretQueryRunner['query']; } /** * Create a production Postgres-backed {@link SecretStoreBackend}. * * Implements every method of the interface against the `secret_store` table: * - `insert` UPSERTs on `UNIQUE(owner_id, name)` so a re-put supersedes the * prior row and carries the bumped version — matching the in-memory backend. * - `getByRef` / `getByName` are owner-scoped single-row reads. * - `listByOwner` is owner-scoped; `listByKekId` spans owners (rotation only). * - `deleteByRef` returns whether a row was removed (RETURNING). * - `touchLastUsed` / `updateWrappedDek` are id-keyed updates. * * The backend performs no schema management — run {@link SECRET_STORE_DDL} * (or the migration) before first use. */ declare function createPostgresSecretBackend(deps: PostgresSecretBackendDeps): SecretStoreBackend; /** * Device-flow provisioning + broker integration. * * Generalises the per-brain `HOLOMESH_API_KEY__X402` pattern into * a public service: pair once, server holds wallets/bearers, any surface * gets short-lived scoped capabilities per session. * * This module defines the **interface** for x402+broker provisioning. * A concrete adapter (e.g. `@holoscript/holoscript-agent/provision` or * a cloud HSM wrapper) implements the async `provisionAgent` call. * * @module secrets-broker/provision */ /** * Parameters for provisioning a new AI surface (mobile, desktop, headless). */ interface ProvisionSurfaceInput { handle: string; surface: 'mobile' | 'desktop' | 'headless' | 'web' | string; meshApiBase?: string; founderBearer: string; autoJoinTeamId?: string; } /** * Provisioning adapter interface. Implementations may use: * - `@holoscript/holoscript-agent` (local file-based wallets) * - Cloud HSM (AWS KMS, GCP Cloud KMS) * - Hardware wallet (Trezor, Ledger) * * The broker primitive does NOT mandate the storage backend. */ interface ProvisionAdapter { provisionAgent(input: ProvisionSurfaceInput, opts: { execute: boolean; force?: boolean; }): Promise; } /** * Create a brokered session after provisioning. * * 1. Provisions the surface (wallet + x402 bearer) via the adapter. * 2. Issues a brokered secret grant scoped to the surface's namespace. * 3. Returns both the provision result and the grant receipt. * * The secret material (private key, bearer token) NEVER leaves the * provision adapter. Only handles and receipts surface here. */ declare function provisionBrokeredSession(input: ProvisionSurfaceInput, opts: { execute: boolean; force?: boolean; }, adapter: ProvisionAdapter): Promise<{ provision: DeviceFlowProvisionResult; }>; /** * Convenience builder for a local-file-based provision adapter. * Wraps the same shape as `@holoscript/holoscript-agent/src/provision.ts` * without creating a runtime dependency on that package. */ declare function localFileProvisionAdapter(impl: (input: ProvisionSurfaceInput, opts: { execute: boolean; force?: boolean; }) => Promise): ProvisionAdapter; /** * Environment-backed KEK provider — DEV / BOOTSTRAP ONLY. * * Implements {@link KekProvider} by reading the master key-encryption-key(s) from * environment variables. This is the ONLY module in the secrets-broker that reads * `process.env` — the SecretStore itself never does; it depends on this provider * so the key source can be swapped without touching the crypto. * * ┌─ ⚠ PHASE-3 GATE (vault premortem, 2026-06-08) ──────────────────────────────┐ * │ An env-var KEK is a SINGLE POINT OF TOTAL COMPROMISE: one leaked variable │ * │ unwraps EVERY user's stored secret. On this monorepo's shared deploy surface │ * │ — where committed keys were found TWICE in the last 60 days — that risk is │ * │ unacceptable for real user secrets. Therefore: │ * │ • This provider is for local dev, tests, and pre-GA bring-up ONLY. │ * │ • Before ANY real user secret is stored (GA), replace it with a │ * │ KMS/HSM-backed provider implementing the SAME `KekProvider` interface — │ * │ no SecretStore change required. (premortem Phase 3.) │ * └─────────────────────────────────────────────────────────────────────────────┘ * * Env contract: * SECRETS_VAULT_KEK_CURRENT = # e.g. "v1" — the active KEK id * SECRETS_VAULT_KEK_ = # e.g. SECRETS_VAULT_KEK_V1=... * Multiple `SECRETS_VAULT_KEK_` vars may coexist so {@link SecretStore.rotateKek} * can unwrap historical rows under their original KEK while re-wrapping under the new one. * * @module secrets-broker/env-kek-provider */ /** Thrown when the env KEK material is missing or malformed. Carries no key bytes. */ declare class EnvKekConfigError extends Error { constructor(message: string); } interface EnvKekProviderDeps { /** Environment to read from. Defaults to `process.env`. Injectable for tests. */ env?: Record; } /** The env var that names the current KEK id. */ declare const KEK_CURRENT_ENV = "SECRETS_VAULT_KEK_CURRENT"; /** Build the env var name that holds the KEK bytes for a given id. */ declare function kekEnvVar(kekId: string): string; /** * Generate a fresh 32-byte KEK, base64-encoded for placing in an env var. * Setup helper — print once, store in the secret manager, never log thereafter. */ declare function generateKekBase64(): string; /** * Create an environment-backed {@link KekProvider}. DEV/BOOTSTRAP ONLY — see the * Phase-3 gate banner above before using with real user secrets. */ declare function createEnvKekProvider(deps?: EnvKekProviderDeps): KekProvider; /** * KMS-backed KEK provider — the PRODUCTION KEK source for HoloKey (Phase-3 gate). * * The env provider (`env-kek-provider.ts`) keeps the master KEK in an environment * variable on the shared deploy surface — one leaked var unwraps the entire vault, and * committed-key incidents have hit that exact surface. This provider instead sources the * KEK from a vendor-agnostic **KMS / secret-manager keyring** — AWS KMS, GCP Secret * Manager, HashiCorp Vault, or a Railway secret scoped to ONLY the resolving service — * so the KEK is never on the shared env surface and key access is audited by the manager. * * Vendor-agnostic by construction: inject a {@link KmsKeyring} adapter; this module has * NO vendor SDK dependency. A vendor adapter is ~10 lines (call the SDK's get-secret / * decrypt and return 32 bytes). Marked `productionGrade: true`, so the SecretStore's * production gate (`requireProductionGradeKek`) accepts it where it rejects the env provider. * * NOTE on strength: this resolves the KEK BYTES into app memory at use-time (the * "scoped secret manager" pattern — the minimum the premortem requires). A stronger * future variant has the KMS/HSM wrap+unwrap the per-secret DEK directly so the root key * never leaves the HSM; that is a separate `KmsDekWrapper` seam, not this provider. * * @module secrets-broker/kms-kek-provider */ /** Thrown when the KMS returns malformed key material. Carries no key bytes. */ declare class KmsKekError extends Error { constructor(message: string); } /** * Vendor-agnostic keyring the provider delegates to. A vendor adapter (AWS/GCP/Vault/ * Railway-scoped) implements these two methods over its SDK — no other contract. */ interface KmsKeyring { /** Resolve the raw 32-byte KEK for `kekId` from the KMS / scoped secret store. */ resolveKekBytes(kekId: string): Promise; /** The id of the KEK that `resolveKekBytes` returns for the current epoch. */ currentKekId(): string; } interface KmsKekProviderDeps { keyring: KmsKeyring; } /** * Create the production {@link KekProvider} backed by a {@link KmsKeyring}. Validates the * returned material is exactly 32 bytes (never echoing it) and is marked production-grade. */ declare function createKmsKekProvider(deps: KmsKekProviderDeps): KekProvider; /** * Scoped-secret KMS keyring — the production KEK source for a Railway-style deploy. * * Implements {@link KmsKeyring} (consumed by `createKmsKekProvider`) by reading the KEK * from a DEDICATED, service-scoped secret namespace — distinct from the shared-surface * env the dev provider uses. The premortem's accepted minimum: "a Railway secret scoped * to ONLY the one service that decrypts, never the shared root." * * Deployment contract (load-bearing — this is what makes it production-grade): * - Set `HOLOKEY_PROD_KEK_CURRENT` + `HOLOKEY_PROD_KEK_` ONLY on the single * service that resolves secrets, NEVER on the shared monorepo deploy root. * - These vars must NOT appear in any `.env`, Dockerfile, or `railway.toml` committed to * the repo — provision them in the platform secret UI for that one service. * * The distinct `HOLOKEY_PROD_*` prefix (vs the dev provider's `SECRETS_VAULT_KEK_*`) is the * signal that these are production, service-scoped material. Wrapping this in * `createKmsKekProvider` yields a `productionGrade: true` provider the SecretStore gate accepts. * * For a true cloud KMS / HSM (AWS KMS, GCP Secret Manager, Vault), write a ~10-line adapter * implementing the same {@link KmsKeyring} over its SDK instead of this env reader. * * @module secrets-broker/scoped-secret-keyring */ /** Thrown when the scoped secret material is missing or malformed. Carries no key bytes. */ declare class ScopedSecretKeyringError extends Error { constructor(message: string); } interface ScopedSecretKeyringDeps { /** Environment to read from. Defaults to `process.env`. Injectable for tests. */ env?: Record; /** Override the var prefix (default `HOLOKEY_PROD_KEK`). The `_CURRENT` / `_` suffixes apply. */ prefix?: string; } /** * Create a {@link KmsKeyring} backed by service-scoped secret env vars. Pass the result to * `createKmsKekProvider` to get the production KEK provider. */ declare function createScopedSecretKeyring(deps?: ScopedSecretKeyringDeps): KmsKeyring; /** * Secret access policy — the HoloGate `scope` axis for value resolution. * * HoloGate admits an entity through `identify → authorize → scope → admit → log`. * The {@link import('./secret-resolver').SecretResolver} already does identify/authorize * (fail-closed auth), admit (owner-bound `SecretStore.get`), and log (audit). This module * supplies the missing **scope** step: a least-authority constraint over WHICH refs a given * execution context may resolve — even for secrets the authenticated owner genuinely owns. * * Why ownership is not enough: a Brittney chat session and a Fleet deploy job can run under * the SAME authenticated owner, yet a chat turn has no business resolving `vault:FLEET_DEPLOY_KEY`. * Ownership answers "is this yours?"; scope answers "may THIS context touch it?". Defense in * depth — a mis-scoped consumer is contained to its allowlist instead of the owner's whole vault. * * Shape mirrors HoloDoor's allow/block lists (cf. `holodoor-routes.ts`): glob patterns over the * canonical `:` ref. Semantics, chosen to fail in the SAFE direction: * - `block` wins: a ref matching ANY block glob is denied, even if `allow` also matches it. * - `allow` present (the key exists) ⇒ allowlist mode: the ref MUST match one entry. * `allow: []` is therefore deny-all (an empty allowlist admits nothing) — programmatic * callers whose `allow` collapses to empty fail CLOSED, never open. * - `allow` absent (undefined) ⇒ no allowlist constraint (block-only mode). * - `{}` (neither key) ⇒ no constraint; the policy is a no-op and ownership alone governs. * * This module is pure (no I/O, no secret material) and carries only ref labels — never values. * * @module secrets-broker/secret-access-policy */ /** * A scope policy over secret refs. Glob patterns (`*` = any run incl. empty, `?` = one char) * match against the whole canonical ref (e.g. `vault:OPENAI_API_KEY`). See module docs for the * block-wins / allowlist-presence semantics. */ interface SecretAccessPolicy { /** Globs a ref MUST match (when the key is present). `[]` denies all; absent = unconstrained. */ readonly allow?: readonly string[]; /** Globs that, when matched, deny regardless of {@link allow}. */ readonly block?: readonly string[]; } /** Outcome of {@link checkSecretAccess}. `reason` is the denial cause, or null when allowed. */ interface SecretAccessDecision { readonly allowed: boolean; /** `'blocked'` | `'not-in-allowlist'` when denied; `null` when allowed. */ readonly reason: 'blocked' | 'not-in-allowlist' | null; } /** Thrown by the resolver when a scope policy denies a ref. Carries the ref + cause, no patterns. */ declare class PolicyDeniedError extends Error { readonly ref: SecretRef; /** Why it was denied: `'blocked'` or `'not-in-allowlist'`. */ readonly reason: 'blocked' | 'not-in-allowlist'; constructor(ref: SecretRef, reason: 'blocked' | 'not-in-allowlist'); } /** * Decide whether `ref` may be resolved under `policy`. Pure; evaluates block-first, then the * allowlist (see module docs). Returns a decision — it does NOT throw; the resolver turns a * `{ allowed: false }` into a {@link PolicyDeniedError} so the value boundary stays single-sourced. */ declare function checkSecretAccess(policy: SecretAccessPolicy, ref: SecretRef): SecretAccessDecision; /** * Secret resolver — the FAIL-CLOSED, audited value-resolution entry point. * * This is the one blessed path through which server-side consumers (Studio / * Brittney, the Fleet job runner) turn an authenticated user identity + a * `vault:` ref into a plaintext secret. Secret VALUES never cross the MCP * wire — the broker tools stay handle/lease-only; value resolution happens here, * inside the trusted server, with the caller's OWN established auth. * * ── Gate-first invariant (vault premortem, 2026-06-08) ─────────────────────── * 1. FAIL CLOSED: a resolve with no authenticated owner is DENIED — it never * reaches the store and never returns a value. There is no admin/default * fallback. (This is the "gate is live and tested to DENY before it is * taught to return a value" requirement, enforced at the value boundary.) * 2. SCOPED (optional): when an access policy is supplied — at the resolver * level and/or per call — the ref is checked against it BEFORE the store is * touched, so an out-of-scope ref never decrypts (no value, no timing * oracle). This is HoloGate's `scope` axis: least-authority per execution * context, layered on top of ownership. Either layer can deny; neither widens. * 3. OWNER-BOUND: the authenticated owner is passed straight to * `SecretStore.get`, which re-checks ownership inside itself — so isolation * holds even if a future caller is mis-wired. * 4. AUDITED: every attempt (allowed OR denied) emits an audit event carrying * only owner + ref + outcome — never the value. * * Consumers MUST derive `authenticatedOwnerId` from verified auth (Studio * session subject, Fleet seat owner) — NEVER from untrusted request input. * * @module secrets-broker/secret-resolver */ /** Thrown when a resolve is attempted without an authenticated owner identity. */ declare class AuthRequiredError extends Error { readonly ref: SecretRef; constructor(ref: SecretRef); } /** Audit record emitted on EVERY resolve attempt. Carries no secret material. */ interface SecretResolveAudit { readonly event: 'secret.resolve'; /** Authenticated owner that attempted the resolve (or '' when unauthenticated). */ readonly ownerId: string; /** The ref that was requested. */ readonly ref: SecretRef; /** Optional human-readable purpose for compliance. */ readonly purpose: string | null; /** Whether a value was returned. */ readonly outcome: 'allowed' | 'denied'; /** Denial reason (error name) when `outcome === 'denied'`. */ readonly reason: string | null; /** ISO 8601 timestamp. */ readonly at: string; } interface SecretResolverDeps { store: SecretStore; /** Audit sink — called for every attempt (allowed + denied). Defaults to no-op. */ audit?: (event: SecretResolveAudit) => void; /** * Optional resolver-level scope policy (HoloGate `scope`). When set it is enforced on * EVERY resolve as a backstop — an out-of-scope ref is denied BEFORE the store is touched * (no decrypt, no value, no timing oracle). A per-call {@link ResolveInput.scope} narrows * it further; neither layer can widen what ownership already permits. */ policy?: SecretAccessPolicy; /** Optional fixed clock for deterministic tests. */ now?: () => Date; } interface ResolveInput { /** * Authenticated caller identity. MUST come from verified server-side auth * (Studio session / Fleet seat) — never from untrusted request input. An empty * or missing value is treated as unauthenticated and DENIED. */ authenticatedOwnerId: string | undefined | null; /** Canonical ref of the secret to resolve. */ ref: SecretRef; /** Optional purpose recorded in the audit trail. */ purpose?: string; /** * Optional per-call scope policy (HoloGate `scope`). Narrows the resolver-level * {@link SecretResolverDeps.policy} for THIS execution context (e.g. a Brittney chat * turn vs a Fleet deploy job sharing one owner). Both must allow; a scope can only * restrict, never widen. */ scope?: SecretAccessPolicy; } interface SecretResolver { /** * Resolve a secret value for an authenticated owner. Throws — never returns a * value — when unauthenticated ({@link AuthRequiredError}), when a scope policy * denies the ref ({@link PolicyDeniedError}, before the store is touched), when * the owner does not own the secret ({@link OwnerMismatchError}), when it is * absent ({@link SecretNotFoundError}), or on a decrypt failure ({@link DecryptError}). */ resolve(input: ResolveInput): Promise<{ value: string; }>; } /** * Create a fail-closed, audited {@link SecretResolver} over a {@link SecretStore}. */ declare function createSecretResolver(deps: SecretResolverDeps): SecretResolver; /** * HoloKey resolve receipts — tamper-evident provenance for the custody "log" step. * * The resolver emits a {@link SecretResolveAudit} for every key handout (allowed or denied). * This module seals each audit into a hash-chained RECEIPT — a SHA-256 over the audit content * plus the previous receipt's hash — so the resolve log becomes append-only and tamper-evident: * any edit, deletion, or reorder breaks the chain and {@link verifyResolveReceiptChain} pinpoints * where. This is HoloKey's contribution to HoloGate's audit-receipt-chain (cf. `verify_cael_trace`), * the `log` in `identify → authorize → scope → admit → log`. * * Receipts carry ZERO secret material — only owner, ref, outcome, reason, time, and hashes. * Additive + side-effect-free: the resolver is untouched; an audit sink seals + persists. * * @module secrets-broker/resolve-receipt */ /** A sealed, hash-chained resolve receipt. Extends the audit with chain hashes. */ interface SecretResolveReceipt extends SecretResolveAudit { /** Hash of the previous receipt in the chain, or null at genesis. */ readonly prevHash: string | null; /** `sha256:` over this receipt's content + prevHash. */ readonly receiptHash: string; } /** * Seal a resolve audit into a chained receipt: stamps `prevHash` (the prior receipt's * `receiptHash`, or null for the first) and a content hash over the whole. Pure. */ declare function sealResolveReceipt(audit: SecretResolveAudit, prevHash: string | null): SecretResolveReceipt; /** * Verify a receipt chain end-to-end. Returns `{ ok: true }` only when every receipt's * `receiptHash` matches its recomputed content hash AND its `prevHash` links to the prior * receipt's `receiptHash` (the first's `prevHash` must be null). On failure, `brokenAt` is * the index of the first receipt that fails — any tampered field, deletion, or reorder. */ declare function verifyResolveReceiptChain(receipts: readonly SecretResolveReceipt[]): { ok: boolean; brokenAt: number | null; }; /** * HoloKey secrets manifest — declare an app's secret NEEDS once, compile to many backends. * * This is the "secrets-as-a-compile-target" innovation: the `BrokerManifest.storage` enum * already anticipated `vault | github-actions-secret | env-file`, and this turns that into a * real emitter. A single {@link SecretsManifest} (names + descriptions, NEVER values) * compiles to: * - `env-template` — a `.env.example`-style scaffold (names only) for local onboarding. * - `github-actions` — `gh secret set …` commands + a workflow `env:` block that references them. * - `holokey-vault` — the `vault:` refs + how to store (SecretStore.put) and consume * (`@needs_key`) them in HoloKey natively. * * So "native vault vs GitHub secrets vs env" stops being a fork: you declare once and emit the * backend your deployment needs. The manifest carries ZERO secret material — only names + metadata. * * @module secrets-broker/secrets-manifest */ /** One declared secret an app needs. Carries NO value — only the name + metadata. */ interface SecretDecl { /** Env-var-style secret name, e.g. `OPENAI_API_KEY`. Becomes the `vault:` key. */ name: string; /** Human-readable description for templates / docs. */ description?: string; /** Whether the app requires it. Defaults to true. */ required?: boolean; } /** An app's full secret-needs declaration. */ interface SecretsManifest { /** App / namespace name (used in headers). */ app: string; secrets: readonly SecretDecl[]; } /** Supported compile targets — mirrors `BrokerManifest.storage`. */ type SecretsCompileTarget = 'env-template' | 'github-actions' | 'holokey-vault' | 'infra-namespace'; /** Thrown when a manifest is malformed (e.g. a non-env-var-style name). */ declare class SecretsManifestError extends Error { constructor(message: string); } /** * Compile a {@link SecretsManifest} to a backend artifact. Pure; emits text only and never * includes secret values (the manifest has none). */ declare function compileSecretsManifest(manifest: SecretsManifest, target: SecretsCompileTarget): string; /** * `@needs_key` — HoloKey's HoloScript-native trait: secrets as composable capabilities. * * This is the differentiating piece of HoloKey (the custody axis of HoloGate). A * HoloScript object declares the keys it needs as a TRAIT — the source carries only * the audit-safe REF, never the value: * * object "BrittneyCall" @needs_key { ref: "vault:OPENAI_API_KEY", purpose: "llm-call" } * * At runtime the trait resolves the secret AT USE-TIME through HoloKey's fail-closed, * owner-bound resolver (`secret-resolver.ts`) and stashes the plaintext TRANSIENTLY on * the in-memory node carrier (`node.__resolvedSecrets[ref]`) for sibling traits on the * same node to consume — it is NEVER written to durable runtime state and NEVER placed * in an emitted event payload. * * Wiring is the same `registerTrait(name, handler)` seam the domain-plugin traits use, * so a runtime that has registered `@needs_key` dispatches it like any other trait. The * app binds the resolver + the AUTHENTICATED OWNER (from a Studio session / Fleet seat) * when it registers the trait — so a runtime with no authenticated owner FAILS CLOSED: * the trait emits `needs_key_denied`, no secret is resolved. * * Events (none carry the value): * - `needs_key_ready` { nodeId, ref, purpose } — secret resolved + stashed for siblings. * - `needs_key_denied` { nodeId, ref, reason } — fail-closed (unauthenticated / not_owner / not_found / decrypt_failed). * - `needs_key_error` { nodeId, error } — malformed config (missing ref). * * @module secrets-broker/needs-key-trait */ /** Config carried by an orb's `@needs_key` directive. `ref` is required (`vault:`). */ interface NeedsKeyConfig { ref?: SecretRef; /** Optional purpose recorded in the HoloKey resolve audit. */ purpose?: string; } /** * The slice of the runtime trait-dispatch context `@needs_key` uses. `emit` is the * standard trait event sink; `provideSecret` (optional) lets the runtime route the * resolved value to sibling traits through a channel of its choosing — the trait also * always stashes it on `node.__resolvedSecrets` regardless. */ interface NeedsKeyDispatchContext { emit(event: string, payload?: unknown): void; /** Optional: hand the resolved value to the runtime for same-node sibling use. */ provideSecret?(ref: SecretRef, value: string): void; } /** * Binding the app supplies when registering the trait: the HoloKey resolver plus the * AUTHENTICATED owner for this runtime. `authenticatedOwnerId` MUST come from verified * server-side auth (Studio session subject / Fleet seat owner) — never user input. An * absent/empty owner makes every resolve fail closed. */ interface NeedsKeyResolution { resolver: SecretResolver; authenticatedOwnerId?: string | null; } /** Structural trait handler shape (matches the domain-plugin trait handlers). */ interface NeedsKeyTraitHandler { name: 'needs_key'; onAttach(node: unknown, config: NeedsKeyConfig | undefined, context: NeedsKeyDispatchContext): Promise; onUpdate(node: unknown, config: NeedsKeyConfig | undefined, context: NeedsKeyDispatchContext): Promise; } /** * Create the `@needs_key` trait handler bound to a {@link NeedsKeyResolution}. The * handler resolves the declared ref through HoloKey at attach/update and stashes the * value transiently for sibling traits — fail-closed and value-free in all events. */ declare function createNeedsKeyHandler(resolution: NeedsKeyResolution): NeedsKeyTraitHandler; /** A runtime that can register behavioral trait handlers (e.g. HoloScriptRuntime). */ interface TraitRegistrarTarget { registerTrait(name: string, handler: unknown): void; } /** * Register the `@needs_key` trait into a runtime, bound to a resolver + authenticated * owner. After this, the runtime's directive dispatch resolves `@needs_key` orbs through * HoloKey at use-time. Mirrors the domain-plugin `register*TraitHandlers` shape. */ declare function registerNeedsKeyTrait(registrar: TraitRegistrarTarget, resolution: NeedsKeyResolution): void; /** * HoloKey vault bootstrap — the single place that turns the encrypted value-store ON. * * Phase 0 of the operational-secret migration (research/2026-06-16_holokey-operational- * secret-migration.md). The secrets-broker package ships every piece — `SecretStore`, * the Postgres backend, KEK providers, the fail-closed resolver — but nothing instantiates * them in a live server: only an in-memory *lease* adapter runs (W.705, built-but-dead- * wired). This factory assembles them from config so an agent/service can finally * `put`/`get`/`resolve` a secret at runtime. * * FLAG-GATED so it can never break a boot: with no KEK configured it returns `null` and * the caller falls back to its prior behavior. A misconfigured prod KEK (a dev KEK under * `NODE_ENV=production`) is logged and also returns `null` rather than throwing. So wiring * `createHoloKeyVault()` into a server bootstrap is purely additive — absent config = the * exact prior behavior. * * The bootstrap secret model (research §bootstrap): a service is configured with ONE * KEK (a Railway managed/sealed var) and a DB URL; with those it decrypts every other * secret from the vault at runtime. N plaintext keys per service → 1 rotatable KEK. * * @module secrets-broker/vault-bootstrap */ type Env$2 = Record; /** Env var naming the current PRODUCTION, service-scoped KEK id (vs the dev `SECRETS_VAULT_KEK_*`). */ declare const PROD_KEK_CURRENT_ENV = "HOLOKEY_PROD_KEK_CURRENT"; interface HoloKeyVault { /** Encrypt + store / metadata / delete / rotate. Owner-isolated. */ readonly store: SecretStore; /** Fail-closed, owner-bound, audited value resolution for trusted server-side consumers. */ readonly resolver: SecretResolver; /** Which KEK backed the store — `production` (KMS/scoped-keyring) or `dev` (env KEK). */ readonly kekGrade: 'production' | 'dev'; /** Which persistence backend — `postgres` (durable) or `in-memory` (non-persistent / tests). */ readonly backend: 'postgres' | 'in-memory'; } interface CreateHoloKeyVaultOpts { /** Environment to read KEK material + NODE_ENV from. Defaults to `process.env`. */ env?: Env$2; /** Injected pg query runner (a bound `pool.query`). Absent → in-memory backend (non-persistent). */ query?: SecretQueryRunner['query']; /** Audit sink for every resolve attempt (allowed + denied). Never carries the value. */ audit?: (e: SecretResolveAudit) => void; } /** * Assemble the HoloKey vault from config, or return `null` (vault OFF) when no KEK is * configured or the prod gate rejects a dev KEK. Never throws on config — a boot wiring * this in keeps its prior behavior when unconfigured. */ declare function createHoloKeyVault(opts?: CreateHoloKeyVaultOpts): HoloKeyVault | null; /** * Service/fleet HoloKey identity. * * Studio users already arrive with a human owner id. Operational services do * not: they boot from Railway, Jetson seats, fleet workers, or an x402 bearer. * This module turns those runtime facts into an audit-safe owner id in the * `infra://` namespace, then normalizes `infra://` refs to the existing * encrypted `vault:` store key for that owner. * * The x402 path hashes the bearer. A bearer may prove custody, but it must not * become an owner id or log line in plaintext. */ type Env$1 = Record; type ServiceIdentitySource = 'explicit' | 'holomesh-agent' | 'fleet-seat' | 'railway-service' | 'x402-bearer' | 'fallback'; interface ServiceIdentity { /** Owner id used against SecretStore/SecretResolver. */ readonly ownerId: string; /** Where the owner id came from. */ readonly source: ServiceIdentitySource; /** Operational secrets live outside human workspace refs. */ readonly namespace: 'infra'; /** Human-readable audit label. Carries no secret material. */ readonly label: string; } interface ResolveServiceIdentityOpts { /** Environment to inspect. Defaults to process.env at the call site. */ env?: Env$1; /** Explicit owner override. Preserves compatibility with HOLOKEY_OWNER. */ owner?: string; /** Final fallback when no operational identity signal is present. */ fallbackOwner?: string; } interface NormalizedServiceSecretRef { /** Store-level ref. HoloKey value storage remains vault-backed. */ readonly ref: SecretRef; /** Environment variable name used for fallback reads. */ readonly envName: string; /** Ref namespace the caller used. */ readonly namespace: 'infra' | 'vault' | 'env-name'; } /** * Resolve the current service/fleet owner identity from explicit config or * operational runtime facts, ordered from strongest stable identity to weakest. */ declare function resolveServiceIdentity(opts?: ResolveServiceIdentityOpts): ServiceIdentity; /** * Normalize service secret refs. * * Accepted inputs: * - `OPENAI_API_KEY` -> env fallback + `vault:OPENAI_API_KEY` * - `vault:OPENAI_API_KEY` -> explicit vault ref * - `infra://OPENAI_API_KEY` -> operational namespace for this service * - `infra://mcp/OPENAI_API_KEY` -> same, with an audit-friendly grouping path */ declare function normalizeServiceSecretRef(input: string): NormalizedServiceSecretRef; /** Build the operational namespace ref for a service secret name. */ declare function infraSecretRef(name: string): SecretRef; /** * Service-side secret resolution — the Phase 1 "resolve from the vault, else process.env" bridge. * * A long-running service (or fleet agent) resolves its OWN config secrets through one helper: if the * HoloKey vault is ON and holds the secret for this service's owner, return it (decrypted at * use-time); otherwise FALL BACK to `process.env[name]` — the exact prior behavior. So a consumer * can swap `process.env.OPENAI_API_KEY` for `resolve('OPENAI_API_KEY')` with ZERO risk: until the * key is put in the vault nothing changes; once it is, the consumer transparently picks it up. This * is the incremental migration off per-service plaintext env — no flag day, no boot coupling. * * Service identity (Phase 1): the owner is derived from `HOLOKEY_OWNER`, HoloMesh agent id, * fleet seat, Railway service id/name, or an x402 bearer fingerprint. Operational secret refs may * use the `infra://` namespace; they resolve for the service owner without going through * Studio's human workspace namespace. * * Fail-safe by construction: the vault is built lazily on first resolve and cached; ANY failure * (no KEK, DDL/pool error, decrypt error, not-found, denied) falls through to `process.env`. The * helper logs ONE affirmation line (vault ON / OFF) so a silently-off vault is observable — the * premortem's explicit-affirmation requirement. * * @module secrets-broker/service-secret-resolver */ type Env = Record; interface ServiceSecretResolverOpts { /** Environment to read KEK material + fall-back values from. Defaults to `process.env`. */ env?: Env; /** Injected pg query runner (bound `pool.query`). Absent → in-memory backend (non-persistent / dev). */ query?: SecretQueryRunner['query']; /** * Owner identity this service resolves as. When absent, derived from HoloKey/HoloMesh/fleet/ * Railway/x402 env signals, with legacy fallback `infra`. */ owner?: string; /** Audit sink for every resolve attempt. */ audit?: (e: SecretResolveAudit) => void; /** One-time affirmation logger (default `console.log`). Pass a no-op to silence (tests). */ log?: (msg: string) => void; /** * Inject a pre-built vault instead of building one from env. `undefined` → build via * createHoloKeyVault; an explicit `HoloKeyVault | null` is used as-is (advanced wiring + tests). */ vault?: HoloKeyVault | null; } interface ServiceSecretResolver { /** * Vault value for this owner if present, else `process.env[name]`, else `undefined`. * Accepts `NAME`, `vault:NAME`, or `infra://NAME`. Never throws for vault failures. */ resolve(nameOrRef: string): Promise; /** Whether the vault is ON for this resolver (lazily determined on first call). */ vaultEnabled(): boolean; /** Service/fleet owner identity used for HoloKey owner isolation and audit. */ identity(): ServiceIdentity; } declare function createServiceSecretResolver(opts?: ServiceSecretResolverOpts): ServiceSecretResolver; /** * @holoscript/secrets-broker — Sovereign primitive for AI-surface capability tokens * * Generalizes the per-brain `HOLOMESH_API_KEY__X402` + x402 pattern (S.IDENT, * docs/headless-agents.md) into a typed, framework-agnostic contract usable by any * AI surface (mobile, desktop, headless, hardware). The broker server holds wallets * and long-lived bearers; surfaces present a short-lived, scoped capability token * per session. * * Companion to: * - `/protocol` (HoloScript Protocol economic layer) for commercialization (D.013) * - HoloMesh signing-middleware for signed-attribution coverage (S.IDENT triangle) * - `packages/studio/src/lib/workspace/secretBroker.ts` (workspace-scoped grants; * this package generalizes that pattern to surface-scoped agent bearers) * * Scope of this scaffold (P5 FOUNDATION first iteration): * - Typed contract for surfaces / handles / capabilities / capability tokens * - Pure (no I/O) capability-token mint + validate + revoke logic * - Device-flow pairing contract (interface only; transport in follow-up task) * - Audit-receipt shape compatible with existing HoloDoor policy emission * * Out of scope (filed as follow-up tasks): * - HTTP transport / HoloMesh server routes * - Wallet storage / x402 bearer minting against real Anthropic / GitHub * - /protocol on-chain commercialization wiring * - Per-surface UX (mobile paste flow, desktop OAuth) * * @module @holoscript/secrets-broker */ /** * Surface kind — drives auto-numbering of handles and capability defaults. * Mirrors the per-window handle revamp from `research/2026-04-27_identity-revamp-per-window.md`. */ type SurfaceKind = 'claude' | 'cursor' | 'copilot' | 'gemini' | 'codex' | 'mobile' | 'headless'; /** * Surface trust tier — gates which capabilities a surface can request. * Mobile defaults to a reduced tier (S-3 / S-4 from mobile-as-seat memo). */ type SurfaceTrust = 'full' | 'reduced' | 'read-only'; /** * Auto-numbered per-window handle (e.g. `claude1`, `cursor2`, `mobile1`). * Naming convention: surface name + small-int slot, NOT editor name. */ type Handle = `${SurfaceKind}${number}`; /** * Capability strings — what a capability token can do. * Closed set so the broker can enforce policy without inspecting payloads. */ type Capability = 'mesh:read' | 'mesh:message' | 'mesh:claim' | 'mesh:done' | 'mesh:knowledge.write' | 'mesh:suggestion.write' | 'mesh:suggestion.vote' | 'mesh:sign' | 'protocol:lookup' | 'protocol:publish' | 'protocol:collect' | 'github:read' | 'github:pr.comment'; /** * Capability set returned for a given surface kind under a trust tier. * Pure data table; consumers may override via {@link CapabilityPolicy}. */ declare const DEFAULT_CAPABILITY_BY_TRUST: Record; /** * Per-surface trust defaults. Mobile + headless start at `reduced` per S-3 * (mobile-as-seat memo) and headless-agents cost discipline. */ declare const DEFAULT_TRUST_BY_SURFACE: Record; /** * Minted, opaque-from-client capability token. * Server holds the underlying bearer / wallet keys; client only ever sees this token. * * Shape is JSON-stable so it can be serialised to HTTP headers, mobile push payloads, * or x402 challenges without renegotiation. */ interface CapabilityToken { readonly version: 1; readonly event: 'capability.minted'; readonly tokenId: string; readonly handle: Handle; readonly surface: SurfaceKind; readonly trust: SurfaceTrust; readonly capabilities: readonly Capability[]; readonly issuedAt: string; readonly expiresAt: string; /** Random opaque secret. Server stores a hash; never log this plaintext. */ readonly tokenSecret: string; /** Hash of the canonical token record. */ readonly receiptHash: string; } /** * Server-side stored shape: same as {@link CapabilityToken} minus the plaintext * `tokenSecret`. Stored for revocation + lookup. */ type StoredCapabilityToken = Omit & { readonly tokenSecretHash: string; revokedAt?: string; revokeReason?: string; }; /** * Input to {@link mintCapabilityToken}. */ interface MintInput { handle: Handle; surface: SurfaceKind; /** Override the surface default trust. Cannot exceed `full`. */ trust?: SurfaceTrust; /** Subset of capabilities to grant. Must be a subset of the trust tier's defaults. */ capabilities?: readonly Capability[]; /** TTL in seconds. Clamped to [{@link MIN_TTL_SECONDS}, {@link MAX_TTL_SECONDS}]. */ ttlSeconds?: number; /** Inject a deterministic clock + RNG for testing. */ now?: Date; randomBytes?: (size: number) => Buffer; } declare const MIN_TTL_SECONDS = 60; /** 1 hour upper bound — short-lived per S-7 memo. */ declare const MAX_TTL_SECONDS: number; /** Default TTL when caller doesn't specify one. */ declare const DEFAULT_TTL_SECONDS: number; type CapabilityTokenErrorCode = 'INVALID_HANDLE' | 'TRUST_NOT_ALLOWED' | 'CAPABILITY_NOT_IN_TRUST_TIER' | 'TTL_OUT_OF_RANGE' | 'TOKEN_REVOKED' | 'TOKEN_EXPIRED' | 'TOKEN_INVALID_SECRET'; declare class CapabilityTokenError extends Error { readonly code: CapabilityTokenErrorCode; constructor(message: string, code: CapabilityTokenErrorCode); } /** * Parse a handle string into {surface, slot}. Returns null on malformed input. */ declare function parseHandle(handle: string): { surface: SurfaceKind; slot: number; } | null; /** * Assert a handle is well-formed and matches the claimed surface. Throws otherwise. */ declare function assertHandle(handle: string, surface: SurfaceKind): asserts handle is Handle; /** * Mint a fresh capability token for a surface handle. * * Pure: caller injects {@link MintInput.now} and {@link MintInput.randomBytes} for * determinism. No I/O. Throws {@link CapabilityTokenError} on policy violations. */ declare function mintCapabilityToken(input: MintInput): CapabilityToken; /** * Convert a minted {@link CapabilityToken} into the server-side {@link StoredCapabilityToken} * shape. Strips plaintext `tokenSecret`, hashes it for later verification. */ declare function storeCapabilityToken(token: CapabilityToken): StoredCapabilityToken; interface ValidateInput { presentedSecret: string; stored: StoredCapabilityToken; /** Capability the caller wants to exercise. Must be in stored.capabilities. */ needsCapability: Capability; /** Inject a deterministic clock for testing. */ now?: Date; } /** * Validate a presented capability token against its stored record. * * Returns `true` only when ALL of: * - not revoked * - not expired (vs `now`) * - presented secret hashes to the stored hash * - `needsCapability` is in the token's granted capability set * * Throws {@link CapabilityTokenError} on first failure; never returns false (G.GOLD.013: * computed-truth assertions need the false-case test, which lives in `index.test.ts`). */ declare function validateCapabilityToken(input: ValidateInput): true; /** * Mark a stored token as revoked. Returns a new frozen object; does not mutate input. */ declare function revokeCapabilityToken(stored: StoredCapabilityToken, reason: string, now?: Date): StoredCapabilityToken; /** * Stage-1 of device-flow: server issues a short user-code + opaque device-code. * Surface shows the user-code to the operator; operator visits the verification URL * on a paired desktop, confirms identity, and the server resolves the device-code * to a minted capability token. * * This package defines the CONTRACT only — transport + UI are deferred to follow-up * tasks (`packages/mcp-server/holomesh/routes/secrets-broker-routes.ts` plus a Studio * verify page). */ interface DeviceFlowChallenge { readonly version: 1; readonly event: 'device-flow.challenge'; readonly deviceCode: string; readonly userCode: string; readonly verificationUri: string; readonly expiresAt: string; readonly intervalSeconds: number; readonly receiptHash: string; } interface CreateDeviceFlowChallengeInput { verificationUri: string; /** TTL of the device-code itself; user-code expires together. */ ttlSeconds?: number; /** Polling interval the surface should respect. Defaults to 5s. */ intervalSeconds?: number; now?: Date; randomBytes?: (size: number) => Buffer; } /** * Mint a device-flow challenge. Pure; transport-agnostic. */ declare function createDeviceFlowChallenge(input: CreateDeviceFlowChallengeInput): DeviceFlowChallenge; /** * In-memory store for {@link StoredCapabilityToken}, keyed on `tokenId`. * * Composes the existing pure mint / validate / revoke functions into a usable * server-side surface: a route handler can `put` a stored token after minting, * later `get` it by id from a presented capability-token header, and `revoke` * it when the owning seat retires or a compromise is detected. * * Phase 1 storage is in-memory only — the registry is rebuilt on server boot * from a persistence layer when that ships (mirrors the AttestationRegistry * Phase-1 pattern at `packages/mcp-server/src/holomesh/identity/attestation-registry.ts`). * * No automatic expiry sweep: callers either let {@link validateCapabilityToken} * reject expired tokens at validate-time (cheap, lazy), or call * {@link CapabilityTokenRegistry.pruneExpired} on a timer. * * @see mintCapabilityToken * @see storeCapabilityToken * @see validateCapabilityToken * @see revokeCapabilityToken */ declare class CapabilityTokenRegistry { private readonly byId; /** * Add or replace a stored token. Replacement is idempotent on identical * input; callers re-storing a revoked token (e.g. on resurrection during * key rotation) should explicitly mint a fresh token instead. */ put(stored: StoredCapabilityToken): void; /** Look up a stored token by id. Returns `undefined` when not found. */ get(tokenId: string): StoredCapabilityToken | undefined; /** True iff the registry holds a token under this id (regardless of revoked/expired state). */ has(tokenId: string): boolean; /** * Revoke the stored token under `tokenId`. Returns the new (revoked) record * or `null` if no token exists under that id. Idempotent on already-revoked * tokens — re-revoking returns the existing revoked record without overwriting * the original `revokedAt` / `revokeReason`. */ revoke(tokenId: string, reason: string, now?: Date): StoredCapabilityToken | null; /** Number of stored tokens (active + revoked + expired). */ size(): number; /** Snapshot of all stored tokens. Returned array is independent of internal state. */ list(): readonly StoredCapabilityToken[]; /** * Drop expired tokens from the registry. Returns the count removed. * Callers can run this on a timer to bound memory; not running it is fine — * {@link validateCapabilityToken} rejects expired tokens lazily. */ pruneExpired(now?: Date): number; /** * Convenience: combine {@link get} + {@link validateCapabilityToken} into a * single call. The caller presents a token id + plaintext secret + the * capability they want to exercise; returns `true` on full success or * throws {@link CapabilityTokenError} otherwise. * * Returns `true` only — never `false` — matching the existing validator * contract. Use this in HTTP route handlers wrapping mutating operations. */ validateById(tokenId: string, presentedSecret: string, needsCapability: Capability, now?: Date): true; /** Test-only: drop all entries. */ clear(): void; } export { AuthRequiredError, type BrokerManifest, type BrokerSecretHandle, type Capability, type CapabilityRef, type CapabilityToken, CapabilityTokenError, type CapabilityTokenErrorCode, CapabilityTokenRegistry, type CreateDeviceFlowChallengeInput, type CreateHoloKeyVaultOpts, DEFAULT_CAPABILITY_BY_TRUST, DEFAULT_TRUST_BY_SURFACE, DEFAULT_TTL_SECONDS, DecryptError, type DeviceFlowChallenge, type DeviceFlowProvisionResult, EnvKekConfigError, type EnvKekProviderDeps, type GetInput, type GetResult, type Handle, type HoloKeyVault, InsecureKekError, KEK_CURRENT_ENV, type KekProvider, KmsKekError, type KmsKekProviderDeps, type KmsKeyring, type LeaseAdapter, type LeaseQueryRunner, MAX_TTL_SECONDS, MIN_TTL_SECONDS, type MintInput, type NeedsKeyConfig, type NeedsKeyDispatchContext, type NeedsKeyResolution, type NeedsKeyTraitHandler, type NormalizedServiceSecretRef, OwnerMismatchError, PROD_KEK_CURRENT_ENV, type PolicyDecision, PolicyDeniedError, type PolicyGatedGrant, type PolicyOutcome, type PostgresLeaseAdapterDeps, type PostgresSecretBackendDeps, type PutInput, type PutResult, type ResolveInput, type ResolveServiceIdentityOpts, type RotateKekInput, type RotateKekResult, SECRET_LEASES_DDL, SECRET_STORE_DDL, type ScopedSecretKeyringDeps, ScopedSecretKeyringError, type SecretAccessDecision, type SecretAccessPolicy, type SecretBrokerPolicy, type SecretDecl, type SecretGrantInput, type SecretGrantPolicyConfig, SecretGrantPolicyError, type SecretGrantReceipt, type SecretMetadata, SecretNotFoundError, type SecretQueryRunner, type SecretRef, type SecretResolveAudit, type SecretResolveReceipt, type SecretResolver, type SecretResolverDeps, type SecretRow, type SecretStore, type SecretStoreBackend, type SecretStoreDeps, type SecretsCompileTarget, type SecretsManifest, SecretsManifestError, type ServiceIdentity, type ServiceIdentitySource, type ServiceSecretResolver, type ServiceSecretResolverOpts, type StoredCapabilityToken, type SurfaceKind, type SurfaceTrust, type ValidateInput, allowAgentForRef, allowOnly, assertHandle, checkSecretAccess, checkSecretGrantPolicy, compileSecretsManifest, createDeviceFlowChallenge, createEnvKekProvider, createHoloKeyVault, createInMemorySecretBackend, createKmsKekProvider, createMemoryLeaseAdapter, createNeedsKeyHandler, createNoOpLeaseAdapter, createPolicyGatedSecretGrant, createPostgresLeaseAdapter, createPostgresSecretBackend, createScopedSecretKeyring, createSecretGrant, createSecretResolver, createSecretStore, createServiceSecretResolver, denyAll, fromHoloDoorPolicy, generateKekBase64, infraSecretRef, kekEnvVar, localFileProvisionAdapter, mintCapabilityToken, normalizeServiceSecretRef, parseHandle, provisionBrokeredSession, registerNeedsKeyTrait, resolveServiceIdentity, revokeCapabilityToken, sealResolveReceipt, storeCapabilityToken, validateCapabilityToken, verifyResolveReceiptChain };