@hediet/linkrpc-hub 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +101 -0
- package/dist/chunks/config-BCvkg7jv.d.ts +566 -0
- package/dist/chunks/configFile-y6Phntqu.d.ts +9 -0
- package/dist/chunks/connectionTokenBinder.interfaces-B-beCg06.js +40 -0
- package/dist/chunks/connectionTokenBinder.interfaces-B-beCg06.js.map +1 -0
- package/dist/chunks/connectionTokenBinder.interfaces-BhzQ1DTS.d.ts +36 -0
- package/dist/chunks/hubConnectionAcceptor-B5-8JsFY.js +2768 -0
- package/dist/chunks/hubConnectionAcceptor-B5-8JsFY.js.map +1 -0
- package/dist/chunks/hubConnectionAcceptor-BwydvFa4.d.ts +1560 -0
- package/dist/chunks/index-CLIUrV88.d.ts +481 -0
- package/dist/chunks/node-CTXsQ6oa.js +460 -0
- package/dist/chunks/node-CTXsQ6oa.js.map +1 -0
- package/dist/chunks/nodeTransit-CWeFnbwt.js +444 -0
- package/dist/chunks/nodeTransit-CWeFnbwt.js.map +1 -0
- package/dist/chunks/nodeTransit-cmtZgdpW.d.ts +226 -0
- package/dist/chunks/runHub-P3YUwwdv.js +1797 -0
- package/dist/chunks/runHub-P3YUwwdv.js.map +1 -0
- package/dist/chunks/server-BAxchQhy.js +1368 -0
- package/dist/chunks/server-BAxchQhy.js.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +38 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js +305 -0
- package/dist/config.js.map +1 -0
- package/dist/configFile.d.ts +2 -0
- package/dist/configFile.js +27 -0
- package/dist/configFile.js.map +1 -0
- package/dist/engine/runHub.d.ts +42 -0
- package/dist/engine/runHub.js +2 -0
- package/dist/hub/server/client.d.ts +2 -0
- package/dist/hub/server/client.js +2 -0
- package/dist/hub/server/connectionTokenBinder.d.ts +2 -0
- package/dist/hub/server/connectionTokenBinder.js +2 -0
- package/dist/hub/server/index.d.ts +5 -0
- package/dist/hub/server/index.js +5 -0
- package/dist/hub/server/node/index.d.ts +218 -0
- package/dist/hub/server/node/index.js +2 -0
- package/dist/hub/server/transit.d.ts +2 -0
- package/dist/hub/server/transit.js +2 -0
- package/dist/index.d.ts +512 -0
- package/dist/index.js +309 -0
- package/dist/index.js.map +1 -0
- package/dist/serve.d.ts +14 -0
- package/dist/serve.js +31 -0
- package/dist/serve.js.map +1 -0
- package/dist/spawn.d.ts +12 -0
- package/dist/spawn.js +25 -0
- package/dist/spawn.js.map +1 -0
- package/package.json +83 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server-BAxchQhy.js","names":["_canonicalKey","err"],"sources":["../../src/hub/server/hubRegisterServiceId.ts","../../src/hub/server/accessCandidates.ts","../../src/hub/server/mintCapability.ts","../../src/hub/server/capabilityProposal.ts","../../src/hub/server/hubAccessService.ts","../../src/hub/server/tokenIdentityStore.ts","../../src/hub/server/provenance.ts","../../src/hub/server/prefixPolicy.ts","../../src/hub/server/sqliteIdentityKeystore.ts","../../src/hub/server/identityKeystore.ts"],"sourcesContent":["import { LinkRpcConnection } from '@hediet/linkrpc';\nimport { type AttachedLink, type Hub, type IDisposable } from './routing/routingHub';\nimport type { ServiceId } from '@hediet/linkrpc/hub/common';\n\n/**\n * Register an **in-process** service on `hub` under `serviceId`, returning a\n * {@link RegisteredServiceId} whose `connection` serves its interfaces.\n *\n * This is the hubv2 replacement for the v1 `hub.attachParticipant(pair.a)` +\n * `LinkRpcConnection.fromTransport(pair.b)` + `enableReflection({ serviceId })`\n * dance that every built-in extension service performed. It:\n *\n * 1. attaches an in-memory link to the hub and claims `serviceId` on it, so\n * the hub routes every fully-qualified `serviceId::…` call to this service;\n * 2. exposes a {@link LinkRpcConnection} whose registered interfaces answer\n * those calls (register them with `{ serviceId }`); and\n * 3. enables reflection under `serviceId` so the hub's aggregating\n * `directory::list` surfaces this service.\n *\n * Disposing the returned handle (`svc.dispose()`) closes the connection and\n * detaches the link, releasing the claimed prefix.\n *\n * Unlike an accepted *participant* (which gets a {@link RootOverlay} and only\n * reaches the hub through its uplink), an in-process service is attached\n * directly to the hub as a prefix owner. It is fully trusted — no provenance,\n * identity, or forwarded-call gating applies to traffic it receives.\n */\nexport function hubRegisterServiceId(hub: Hub, serviceId: ServiceId): RegisteredServiceId {\n const link = hub.attachOut();\n link.addPrefixRoute(serviceId);\n\n const connection = LinkRpcConnection.fromTransport(link.transport);\n connection.enableReflection({ serviceId });\n return new RegisteredServiceId(connection, link);\n}\n\n/**\n * Handle to an in-process service registered on a {@link Hub} via\n * {@link hubRegisterServiceId}. Owns both the {@link LinkRpcConnection} the\n * service serves its interfaces on and the hub {@link AttachedLink} that routes\n * traffic to it. {@link dispose} closes the connection and detaches the link\n * (releasing the claimed prefix) — wire it into the owning feature's disposal.\n */\nexport class RegisteredServiceId implements IDisposable {\n public constructor(\n /** Serve the service's interfaces here (register them with `{ serviceId }`). */\n public readonly connection: LinkRpcConnection,\n private readonly _link: AttachedLink,\n ) { }\n\n public dispose(): void {\n this.connection.close();\n this._link.dispose();\n }\n}\n","/**\n * Portable access-candidate resolution for the v2 consent engine.\n *\n * In v1 the hub resolved slot candidates internally inside\n * `_handleAccessRequest` by snapshotting the full interface directory and\n * matching each requested slot against it (`hub/hub.ts:_candidatesForSlot`).\n *\n * In v2 the hub is identity-free and has no consent surface. The consent\n * engine lives outside the hub (in the extension host) and resolves\n * candidates against the **directory interface object**. The reusable walker\n * follows only the referrals explicitly exposed by each directory and returns\n * the resulting interface inventory, so the candidate resolver here remains a\n * pure function over that snapshot.\n *\n * The wire shapes (`AccessSlotRequest`, `AccessSlotCandidate`, ...) match the\n * v1 `hubAccess` protocol so existing consumers (web-editors) keep working.\n */\n\nimport { type Pattern, type LinkRpcConnection, type RootPrincipalSet } from '@hediet/linkrpc';\nimport { walkHubDetailed } from '@hediet/linkrpc/hub/common';\n\n/** A single entry of the aggregated interface directory. */\nexport interface DirectoryEntry {\n readonly serviceId: string;\n readonly interfaceId: string;\n readonly hash: string;\n readonly serviceDescription?: string;\n readonly rootPrincipalSets?: readonly RootPrincipalSet[];\n}\n\nexport interface AccessInterfaceRequirement {\n readonly id: string;\n /** Optional schema-version pin; when set the candidate's hash must match. */\n readonly hash?: string;\n /** Defaults to true. */\n readonly required: boolean;\n}\n\nexport interface AccessMemberRequirement {\n readonly interfaceId: string;\n /** Member name matcher (`{ exact }` or `{ prefix }`). */\n readonly member: Pattern;\n /** Defaults to true. */\n readonly required: boolean;\n}\n\nexport interface AccessSlotRequest {\n readonly interfaces: readonly AccessInterfaceRequirement[];\n readonly members: readonly AccessMemberRequirement[];\n}\n\nexport interface AccessSlotCandidate {\n readonly serviceId: string;\n readonly serviceDescription?: string;\n /** Subset of the slot's requested interfaces actually present on this candidate. */\n readonly satisfiedInterfaces: readonly AccessInterfaceRequirement[];\n /**\n * Requested interfaces the candidate does NOT satisfy. By construction\n * every entry here is `required: false` — a required-but-missing\n * interface filters the candidate out before it is emitted.\n */\n readonly unsatisfiedInterfaces: readonly AccessInterfaceRequirement[];\n}\n\nexport interface ResolvedAccessSlot {\n readonly request: AccessSlotRequest;\n /** Empty = no service satisfies every required interface. */\n readonly candidates: readonly AccessSlotCandidate[];\n}\n\nexport interface ResolvedCandidates {\n readonly dependencies: Readonly<Record<string, ResolvedAccessSlot>>;\n /** Slot ids for which no candidate satisfied every required interface. */\n readonly noCandidateSlots: readonly string[];\n}\n\n/**\n * Fetch the full interface directory by recursively walking the hub's\n * **referral** directory graph. {@link walkHubDetailed} follows explicit\n * `hubrpc.directory` listings to collect leaf interfaces (folding transitive\n * root-node-id requirements onto descendants along the way). Routing claims do\n * not create referrals. This is the v2 replacement for the hub-internal\n * `_fullDirectory()`.\n */\nexport async function fetchFullDirectory(\n connection: LinkRpcConnection,\n hubServiceId = 'hub',\n): Promise<DirectoryEntry[]> {\n const { listings } = await walkHubDetailed(connection.channel, { rootTarget: hubServiceId });\n return listings.map((it) => ({\n serviceId: it.serviceId,\n interfaceId: it.interfaceId,\n hash: it.hash,\n ...(it.serviceDescription !== undefined ? { serviceDescription: it.serviceDescription } : {}),\n ...(it.rootPrincipalSets !== undefined ? { rootPrincipalSets: it.rootPrincipalSets } : {}),\n }));\n}\n\n/**\n * Resolve every dependency slot against a directory snapshot. Pure port of\n * the v1 hub's per-request candidate resolution loop + `_candidatesForSlot`.\n */\nexport function resolveAccessCandidates(\n slots: Readonly<Record<string, AccessSlotRequest>>,\n directory: readonly DirectoryEntry[],\n): ResolvedCandidates {\n const dependencies: Record<string, ResolvedAccessSlot> = {};\n const noCandidateSlots: string[] = [];\n\n for (const [slotId, slot] of Object.entries(slots)) {\n const candidates = candidatesForSlot(slot, directory);\n dependencies[slotId] = { request: slot, candidates };\n if (candidates.length === 0) {\n noCandidateSlots.push(slotId);\n }\n }\n\n return { dependencies, noCandidateSlots };\n}\n\n/**\n * Match one slot against the directory snapshot. A service is a candidate iff\n * it satisfies every `required` interface (by id, and by hash when pinned).\n */\nexport function candidatesForSlot(\n slot: AccessSlotRequest,\n directory: readonly DirectoryEntry[],\n): AccessSlotCandidate[] {\n // Group directory entries by prefix (serviceId).\n const byPrefix = new Map<\n string,\n { interfaces: { id: string; hash: string }[]; serviceDescription?: string }\n >();\n for (const it of directory) {\n let entry = byPrefix.get(it.serviceId);\n if (!entry) {\n entry = { interfaces: [] };\n byPrefix.set(it.serviceId, entry);\n }\n entry.interfaces.push({ id: it.interfaceId, hash: it.hash });\n if (entry.serviceDescription === undefined && it.serviceDescription !== undefined) {\n entry.serviceDescription = it.serviceDescription;\n }\n }\n\n const out: AccessSlotCandidate[] = [];\n for (const [prefix, entry] of byPrefix) {\n const ifaces = entry.interfaces;\n const satisfied: AccessInterfaceRequirement[] = [];\n const unsatisfied: AccessInterfaceRequirement[] = [];\n let missingRequired = false;\n for (const req of slot.interfaces) {\n const present = ifaces.some(\n (i) => i.id === req.id && (req.hash === undefined || i.hash === req.hash),\n );\n if (present) {\n satisfied.push(req);\n } else {\n unsatisfied.push(req);\n if (req.required) {\n missingRequired = true;\n }\n }\n }\n if (missingRequired) {\n continue;\n }\n out.push({\n serviceId: prefix,\n ...(entry.serviceDescription !== undefined\n ? { serviceDescription: entry.serviceDescription }\n : {}),\n satisfiedInterfaces: satisfied,\n unsatisfiedInterfaces: unsatisfied,\n });\n }\n return out;\n}\n","/**\n * Admin-issued capability minting for the v2 consent engine.\n *\n * v1 issued capabilities through the hub (`proposeCapability` +\n * `signProposedCapability`). In v2 the hub no longer signs anything — the\n * consent engine owns an admin {@link SigningIdentity} and mints capabilities\n * directly. A {@link SigningIdentity} signs over arbitrary bytes with its\n * Ed25519 key, which is exactly what {@link signCapability} needs, so this is\n * a thin wrapper that assembles a {@link Capability} and signs it.\n */\n\nimport { bytesToBase64Url, type PrincipalId } from '@hediet/linkrpc';\nimport type {\n Capability,\n Permission,\n SignedCapability,\n} from '@hediet/linkrpc';\nimport type { Base64Sha256 } from '@hediet/linkrpc';\nimport { signCapability } from '@hediet/linkrpc';\nimport type { SigningIdentity } from '@hediet/linkrpc';\n\nexport interface MintCapabilityOptions {\n /** The admin identity that issues (signs) the capability. */\n readonly issuer: SigningIdentity;\n /** The holder allowed to wield the capability. */\n readonly audience: PrincipalId;\n /** What the holder may do. A call matches iff it matches any permission. */\n readonly permissions: readonly Permission[];\n /** Unix milliseconds. Omit for a capability that never expires. */\n readonly expiresAtMs?: number;\n /**\n * Per-cap distinguisher (base64url). Defaults to a fresh random 128-bit\n * value.\n */\n readonly nonce?: string;\n /** Delegation parent: the content hash (`signedHash(\"capability\", parent)`) of the parent capability. */\n readonly parentHash?: Base64Sha256<Capability>;\n}\n\n/**\n * Assemble and sign a {@link Capability} with the admin identity.\n *\n * This replaces the v1 `hub.proposeCapability` / `signProposedCapability`\n * pair: the consent engine decides the permissions (e.g. from\n * {@link resolveAccessCandidates} + the user's consent selection) and mints\n * the capability bound to the consumer's PrincipalId as `audience`.\n */\nexport async function mintCapability(options: MintCapabilityOptions): Promise<SignedCapability> {\n const capability: Capability = {\n issuer: options.issuer.publicSigningIdentity.principal,\n audience: options.audience,\n permissions: [...options.permissions],\n nonce: options.nonce ?? randomNonce(),\n ...(options.expiresAtMs !== undefined ? { expiresAtMs: options.expiresAtMs } : {}),\n ...(options.parentHash !== undefined ? { parentHash: options.parentHash } : {}),\n };\n return signCapability(capability, options.issuer);\n}\n\nfunction randomNonce(): string {\n const bytes = new Uint8Array(16);\n globalThis.crypto.getRandomValues(bytes);\n return bytesToBase64Url(bytes);\n}\n","/**\n * Capability proposal issuer — the byte-equality preview mechanism for the v2\n * consent engine, backed by an admin {@link Identity}.\n *\n * In v1 these were hub-core methods (`Hub.proposeCapability` / `signProposal` /\n * `redeemProposal`). In v2 the hub signs nothing, so the consent engine owns an\n * admin identity and runs the proposal protocol itself. The mechanism lets a\n * separate consent UI (a webview shell, reached over RPC) render the *exact*\n * `Capability` that will be signed, then return it verbatim for redemption —\n * with cryptographic guarantees that the displayed bytes equal the signed\n * bytes and that a proposal cannot be replayed or substituted across prompts.\n *\n * Protocol (all host-side, using the admin key):\n * 1. `propose({ audience, permissions, expiresAtMs? })` → an unsigned\n * `Capability` tagged with an internal marker.\n * 2. `signProposal(cap, { bind? })` → a `CapabilityProposal`: the readable\n * `capability` plus an opaque `$hubData` blob (a domain-separated\n * signature over `canonicalJson({ preview: cap, bind? })`). The UI renders\n * `capability` and, on approval, returns the whole proposal verbatim.\n * 3. `redeemProposal(proposal, { expectedBind? })` → a real\n * `SignedCapability`. Verifies the `$hubData` signature, that the preview\n * deep-equals `proposal.capability`, the optional `bind`, and a one-shot\n * nonce; then signs the bare canonical cap so the result is an ordinary\n * bearer token (verifiable by {@link verifyChain}).\n */\n\nimport {\n base64UrlToBytes,\n bytesToBase64Url,\n jcsCanonicalize,\n jcsCanonicalizeBytes,\n keyIdForPrincipal,\n type JsonValue,\n type PrincipalId,\n publicKeyForKeyId,\n signCapability,\n type SignedCapability,\n} from '@hediet/linkrpc';\nimport { crypto } from '@hediet/linkrpc';\nimport type { Capability, Permission } from '@hediet/linkrpc';\nimport type { SigningIdentity } from '@hediet/linkrpc';\n\n/**\n * Hub-minted \"proposal\" of a capability the user is about to approve.\n * `capability` is the readable JSON the consent UI renders; `$hubData` is an\n * opaque blob the UI must round-trip verbatim — only the issuer inspects it.\n */\nexport interface CapabilityProposal {\n readonly capability: Capability;\n /** Opaque issuer-private signed blob; round-trip verbatim. */\n readonly $hubData: unknown;\n}\n\ninterface HubProposalData {\n readonly payload: string;\n readonly signature: string;\n}\n\nfunction isHubProposalData(v: unknown): v is HubProposalData {\n return (\n v !== null &&\n typeof v === 'object' &&\n typeof (v as HubProposalData).payload === 'string' &&\n typeof (v as HubProposalData).signature === 'string'\n );\n}\n\n/**\n * Access-grant duration vocabulary.\n *\n * `once` and `shortLived` share the same 5-minute TTL — they differ only on\n * the *use* axis: `once` is the single-use \"Allow once\" choice (the consent\n * host additionally pins a `callBind`, making it intrinsically single-use),\n * while `shortLived` is an ordinary multi-use grant that simply expires soon.\n * `persistent` never expires (no `expiresAtMs`); revocation is the only way to\n * end it.\n */\nexport type AccessDurationName = 'once' | 'shortLived' | 'longLived' | 'persistent';\n\nconst _DURATION_TO_MS: Record<Exclude<AccessDurationName, 'persistent'>, number> = {\n once: 5 * 60 * 1000,\n shortLived: 5 * 60 * 1000,\n longLived: 24 * 60 * 60 * 1000,\n};\n\n/**\n * Convert an access duration into a Unix-milliseconds expiration timestamp, so\n * handlers compute the same `expiresAtMs` the issuer mints with. Returns\n * `undefined` for `persistent` (and any future never-expiring duration), which\n * callers pass straight to {@link CapabilityProposalIssuer.propose}/`mint` —\n * both omit `expiresAtMs` when it is `undefined`, yielding a non-expiring cap.\n * Defaults to the safe short TTL (`shortLived`) when no duration is supplied.\n */\nexport function durationToExp(duration: AccessDurationName | undefined): number | undefined {\n const d = duration ?? 'shortLived';\n if (d === 'persistent') {\n return undefined;\n }\n return Date.now() + _DURATION_TO_MS[d];\n}\n\nfunction canonicalEqual(a: unknown, b: unknown): boolean {\n try {\n return jcsCanonicalize(a as JsonValue) === jcsCanonicalize(b as JsonValue);\n } catch {\n return false;\n }\n}\n\n/** Marker proving a `Capability` came from {@link CapabilityProposalIssuer.propose}. */\nconst _PROPOSED = Symbol('linkrpc.proposedCapability');\n\nexport interface ProposeArgs {\n readonly audience: PrincipalId;\n readonly permissions: readonly Permission[];\n /** Unix milliseconds. Absent = never expires. */\n readonly expiresAtMs?: number;\n}\n\n/**\n * Issues capabilities and capability proposals signed by an admin\n * {@link SigningIdentity}. One instance per hub host; tracks redeemed nonces\n * for one-shot proposal redemption.\n */\nexport class CapabilityProposalIssuer {\n private readonly _redeemedNonces = new Set<string>();\n private _nonceCounter = 0;\n\n constructor(\n private readonly _issuer: SigningIdentity,\n ) { }\n\n public get issuerPrincipalId(): PrincipalId {\n return this._issuer.publicSigningIdentity.principal;\n }\n\n /**\n * Build an unsigned `Capability` the issuer is willing to sign, tagged with\n * an internal marker. {@link signProposal} refuses to sign anything lacking\n * the marker — a guard against a handler fabricating arbitrary caps. The\n * marker is non-enumerable, so it never affects serialization or signing.\n */\n public propose(args: ProposeArgs): Capability {\n const cap: Capability = {\n issuer: this._issuer.publicSigningIdentity.principal,\n audience: args.audience,\n permissions: [...args.permissions],\n nonce: this._nextNonce(),\n ...(args.expiresAtMs !== undefined ? { expiresAtMs: args.expiresAtMs } : {}),\n };\n Object.defineProperty(cap, _PROPOSED, {\n value: true,\n enumerable: false,\n configurable: false,\n writable: false,\n });\n return cap;\n }\n\n /**\n * Mint a final `SignedCapability` directly from a proposed cap, bypassing\n * the preview round-trip. Use when no UI needs to render the unsigned form.\n */\n public async mint(args: ProposeArgs): Promise<SignedCapability> {\n const cap = this.propose(args);\n return this._sign(cap);\n }\n\n /**\n * Pair a proposed `Capability` with a domain-separated signature over\n * `canonicalJson({ preview: cap, bind? })`. The result is **not** a bearer\n * token — only this issuer can redeem it.\n */\n public async signProposal(\n cap: Capability,\n opts?: { readonly bind?: JsonValue },\n ): Promise<CapabilityProposal> {\n if (!(cap as unknown as Record<symbol, unknown>)[_PROPOSED]) {\n throw new Error('signProposal: capability was not produced by propose()');\n }\n const payload: { preview: Capability; bind?: JsonValue } = { preview: cap };\n if (opts?.bind !== undefined) {\n payload.bind = opts.bind;\n }\n const previewBytes = jcsCanonicalizeBytes(payload);\n const sig = await this._issuer.sign(previewBytes);\n const hubData: HubProposalData = {\n payload: bytesToBase64Url(previewBytes),\n signature: bytesToBase64Url(sig),\n };\n return { capability: cap, $hubData: hubData };\n }\n\n /**\n * Verify and redeem a {@link CapabilityProposal}, returning a real\n * {@link SignedCapability}. Checks: `$hubData` signature, preview deep-equal\n * to `proposal.capability`, optional `bind` match, and one-shot nonce.\n */\n public async redeemProposal(\n proposal: CapabilityProposal,\n opts?: { readonly expectedBind?: JsonValue },\n ): Promise<SignedCapability> {\n if (!isHubProposalData(proposal.$hubData)) {\n throw new Error('redeemProposal: malformed $hubData');\n }\n const payloadBytes = base64UrlToBytes(proposal.$hubData.payload);\n const sigBytes = base64UrlToBytes(proposal.$hubData.signature);\n const pubKey = publicKeyForKeyId(keyIdForPrincipal(this._issuer.publicSigningIdentity.principal));\n const sigOk = await crypto.verify(pubKey, payloadBytes, sigBytes);\n if (!sigOk) {\n throw new Error('redeemProposal: $hubData.signature does not verify');\n }\n let parsed: { preview?: unknown; bind?: unknown };\n try {\n parsed = JSON.parse(new TextDecoder().decode(payloadBytes)) as {\n preview?: unknown;\n bind?: unknown;\n };\n } catch (e) {\n throw new Error(`redeemProposal: malformed payload (${(e as Error).message})`);\n }\n if (!canonicalEqual(parsed.preview, proposal.capability)) {\n throw new Error('redeemProposal: proposal.capability does not match the signed preview');\n }\n const hasBind = parsed.bind !== undefined;\n const wantBind = opts?.expectedBind !== undefined;\n if (hasBind !== wantBind) {\n throw new Error(\n hasBind\n ? 'redeemProposal: proposal is bound but no expectedBind supplied'\n : 'redeemProposal: expectedBind supplied but proposal has no bind',\n );\n }\n if (hasBind && !canonicalEqual(parsed.bind, opts!.expectedBind)) {\n throw new Error('redeemProposal: bind does not match expectedBind');\n }\n const cap = parsed.preview as Capability;\n if (this._redeemedNonces.has(cap.nonce)) {\n throw new Error('redeemProposal: proposal already redeemed');\n }\n this._redeemedNonces.add(cap.nonce);\n return this._sign(cap);\n }\n\n private _sign(cap: Capability): Promise<SignedCapability> {\n // Sign the bare canonical cap so the result is an ordinary bearer\n // token, indistinguishable from one minted via mintCapability.\n return signCapability(cap, this._issuer);\n }\n\n private _nextNonce(): string {\n const bytes = new Uint8Array(16);\n globalThis.crypto.getRandomValues(bytes);\n this._nonceCounter += 1;\n return `${bytesToBase64Url(bytes)}-${this._nonceCounter}`;\n }\n}\n","/**\n * The v2 consent host: serves `hubAccess::{request,extend,requestAccess}` at the\n * **connection root** of a participant's overlay (root form, no serviceId\n * prefix). The root is never forwarded, so the consent front door is reached\n * directly and needs no capability of its own.\n *\n * In v1 this lived inside the hub core (`hub.ts:_handleAccessRequest` &c.) with\n * the hub signing capabilities itself. In v2 the hub is identity-free, so the\n * consent engine is layered on top:\n *\n * - **identity**: the consumer's PrincipalId is taken **directly from\n * `params.consumer.principal`** (no longer derived from a verified call signer).\n * That PrincipalId becomes the capability `audience`. A cap minted for a nodeId is\n * only usable by the holder of that node's key (enforced at call time by\n * `permits`, which checks `audience === call.signer`).\n * - **candidate resolution**: done outside the hub against the directory\n * interface object via {@link resolveAccessCandidates} + an injected\n * {@link RegisterHubAccessOptions.fetchDirectory}.\n * - **consent + issuance**: delegated to injected {@link HubAccessHandlers}.\n * The host owns the wire shape and candidate plumbing; the handler owns the\n * UI, the policy and the {@link mintCapability} calls.\n *\n * The result shapes match {@link hubAccessInterface} byte-for-byte so existing\n * consumers (web-editors) keep working.\n */\n\nimport {\n type PrincipalId,\n type Pattern,\n type Permission,\n type SignedCapability,\n type LinkRpcConnection,\n} from '@hediet/linkrpc';\nimport { hubAccessInterface } from '@hediet/linkrpc/hub/common';\nimport {\n resolveAccessCandidates,\n type AccessSlotRequest,\n type DirectoryEntry,\n type ResolvedAccessSlot,\n} from './accessCandidates';\nimport type { AccessDurationName } from './capabilityProposal';\n\n// ---- handler-facing types -----------------------------------------------\n\nexport interface AccessConsumer {\n readonly name: string;\n readonly origin?: string;\n readonly purpose?: string;\n}\n\nexport type AccessDuration = AccessDurationName;\n\nexport interface AccessSlotBinding {\n readonly serviceId: string;\n /** Interface ids (of the slot's requested interfaces) the grant covers. */\n readonly interfaces: readonly string[];\n}\n\nexport interface AccessRequestArgs {\n readonly consumer: AccessConsumer;\n /** Verified signer of the `hubAccess::request` call; the capability audience. */\n readonly consumerPrincipalId: PrincipalId;\n readonly dependencies: Readonly<Record<string, ResolvedAccessSlot>>;\n readonly duration: AccessDuration | undefined;\n}\n\nexport type AccessDecision =\n | {\n readonly granted: true;\n readonly resolvedSlots: Readonly<Record<string, AccessSlotBinding>>;\n readonly capabilities: readonly SignedCapability[];\n }\n | { readonly granted: false; readonly reason?: string };\n\nexport interface AccessExtendMember {\n readonly interfaceId: string;\n readonly member: Pattern;\n /** Defaults to true. */\n readonly required: boolean;\n}\n\nexport interface AccessExtendArgs {\n readonly consumer: AccessConsumer;\n readonly consumerPrincipalId: PrincipalId;\n readonly serviceId: string;\n readonly added: readonly AccessExtendMember[];\n readonly duration: AccessDuration | undefined;\n}\n\nexport type AccessExtendDecision =\n | {\n readonly granted: true;\n readonly serviceId: string;\n readonly grantedMembers: readonly { interfaceId: string; member: Pattern }[];\n readonly capabilities?: readonly SignedCapability[];\n }\n | { readonly granted: false; readonly reason?: string };\n\n/**\n * Optional per-permission invocation preview the consumer attaches to a\n * direct ({@link HubAccessHandlers.onAccessRequestDirect}) request. It is\n * **not** a signed authority constraint — it is the data the consent host\n * needs to render an honest \"Allow once\" prompt and to pre-compute the\n * `callBind.payloadHash` that binds a one-shot capability to exactly this\n * call. `nonce`/`signedAtMs`/`interfaceHash` are the values the consumer\n * will sign with when it actually issues the call.\n */\nexport interface AccessCallIntent {\n /** Fully-qualified method name the consumer intends to call. */\n readonly method: string;\n /** Params the consumer intends to send (shown to the user, hashed into callBind). */\n readonly params?: unknown;\n /** Schema-version assertion the call will carry. */\n readonly interfaceHash?: string;\n /** base64url nonce bytes the consumer will sign with. */\n readonly nonce: string;\n /** Unix milliseconds the consumer will sign with. */\n readonly signedAtMs: number;\n /** One-line summary for the prompt. */\n readonly summary?: string;\n /** Consumer's suggested default consent action. */\n readonly suggestion?: AccessDuration;\n}\n\n/**\n * An {@link AccessDirectArgs} permission, carrying the verbatim signed\n * {@link Permission} the consumer asked for plus the optional\n * {@link AccessCallIntent} that enables \"Allow once\" byte-binding.\n */\nexport interface AccessDirectPermission extends Permission {\n readonly callIntent?: AccessCallIntent;\n}\n\nexport interface AccessDirectArgs {\n readonly consumer: AccessConsumer;\n readonly consumerPrincipalId: PrincipalId;\n readonly permissions: readonly AccessDirectPermission[];\n readonly duration: AccessDuration | undefined;\n}\n\nexport type AccessDirectDecision =\n | { readonly granted: true; readonly capabilities: readonly SignedCapability[] }\n | { readonly granted: false; readonly reason?: string };\n\nexport interface HubAccessHandlers {\n /** Service-discovery grant: pick a service per slot and mint scoped caps. */\n onAccessRequest(args: AccessRequestArgs): Promise<AccessDecision>;\n /** Service-pinned widening of an existing grant. */\n onAccessExtend(args: AccessExtendArgs): Promise<AccessExtendDecision>;\n /** Verbatim attenuation grant (no discovery). */\n onAccessRequestDirect(args: AccessDirectArgs): Promise<AccessDirectDecision>;\n}\n\nexport interface RegisterHubAccessOptions {\n /** Consent + capability-issuance callbacks. */\n readonly handlers: HubAccessHandlers;\n /** Provides the directory snapshot for candidate resolution. */\n fetchDirectory(): Promise<readonly DirectoryEntry[]>;\n}\n\n/**\n * Install `hubAccess::{request,extend,requestAccess}` at the **connection\n * root** of a participant's overlay (root form, no serviceId prefix). The root\n * is never forwarded, so the consent front door is reached directly and needs\n * no capability of its own.\n *\n * The capability `audience` is taken **directly from `params.consumer.principal`**\n * — no longer derived from a verified call signer. A cap minted for a nodeId is\n * only usable by the holder of that node's key (enforced at call time by\n * `permits`, which checks `audience === call.signer`), so a self-asserted\n * audience grants no usable authority to a caller who does not hold the key.\n */\nexport function registerHubAccessService(\n connection: LinkRpcConnection<unknown>,\n options: RegisterHubAccessOptions,\n): void {\n const { handlers } = options;\n\n connection.register(hubAccessInterface, {\n request: async (params) => {\n const consumerPrincipalId = params.consumer.principal as PrincipalId;\n\n const slots: Record<string, AccessSlotRequest> = {};\n for (const [slotId, raw] of Object.entries(params.dependencies)) {\n slots[slotId] = normalizeSlot(raw);\n }\n\n const directory = await options.fetchDirectory();\n const { dependencies, noCandidateSlots } = resolveAccessCandidates(slots, [...directory]);\n if (noCandidateSlots.length > 0) {\n return { status: 'noCandidates', slots: [...noCandidateSlots] };\n }\n\n const decision = await handlers.onAccessRequest({\n consumer: params.consumer,\n consumerPrincipalId,\n dependencies,\n duration: params.duration,\n });\n\n if (!decision.granted) {\n return decision.reason !== undefined\n ? { status: 'denied', reason: decision.reason }\n : { status: 'denied' };\n }\n\n // Defense-in-depth: refuse bindings to services the candidate\n // resolver didn't surface for that slot.\n const responseSlots: Record<string, { serviceId: string; satisfiedInterfaces: string[] }> = {};\n for (const [slotId, binding] of Object.entries(decision.resolvedSlots)) {\n const resolved = dependencies[slotId];\n if (!resolved) {\n continue;\n }\n const candidate = resolved.candidates.find((c) => c.serviceId === binding.serviceId);\n if (!candidate) {\n continue;\n }\n responseSlots[slotId] = {\n serviceId: binding.serviceId,\n satisfiedInterfaces: [...binding.interfaces],\n };\n }\n\n return {\n status: 'granted',\n slots: responseSlots,\n capabilities: [...decision.capabilities] as SignedCapability[],\n };\n },\n\n extend: async (params) => {\n const consumerPrincipalId = params.consumer.principal as PrincipalId;\n\n const added: AccessExtendMember[] = params.added.map((a) => ({\n interfaceId: a.interfaceId,\n member: a.member,\n required: a.required !== false,\n }));\n\n const decision = await handlers.onAccessExtend({\n consumer: params.consumer,\n consumerPrincipalId,\n serviceId: params.serviceId,\n added,\n duration: params.duration,\n });\n\n if (!decision.granted) {\n return decision.reason !== undefined\n ? { status: 'denied', reason: decision.reason }\n : { status: 'denied' };\n }\n\n const grantedMembers = decision.grantedMembers.map((g) => ({\n interfaceId: g.interfaceId,\n member: g.member,\n }));\n return decision.capabilities !== undefined\n ? {\n status: 'granted',\n serviceId: decision.serviceId,\n granted: grantedMembers,\n capabilities: [...decision.capabilities] as SignedCapability[],\n }\n : {\n status: 'granted',\n serviceId: decision.serviceId,\n granted: grantedMembers,\n };\n },\n\n requestAccess: async (params) => {\n const consumerPrincipalId = params.consumer.principal as PrincipalId;\n\n const decision = await handlers.onAccessRequestDirect({\n consumer: params.consumer,\n consumerPrincipalId,\n permissions: params.permissions as AccessDirectPermission[],\n duration: params.duration,\n });\n\n if (!decision.granted) {\n return decision.reason !== undefined\n ? { status: 'denied', reason: decision.reason }\n : { status: 'denied' };\n }\n return {\n status: 'granted',\n capabilities: [...decision.capabilities] as SignedCapability[],\n };\n },\n });\n}\n\nfunction normalizeSlot(raw: {\n interfaces: { id: string; hash?: string; required?: boolean }[];\n members?: { interfaceId: string; member: Pattern; required?: boolean }[];\n}): AccessSlotRequest {\n return {\n interfaces: raw.interfaces.map((i) => ({\n id: i.id,\n required: i.required !== false,\n ...(i.hash !== undefined ? { hash: i.hash } : {}),\n })),\n members: (raw.members ?? []).map((m) => ({\n interfaceId: m.interfaceId,\n member: m.member,\n required: m.required !== false,\n })),\n };\n}\n","import { randomBytes } from 'node:crypto';\n\n/**\n * What a connection token resolves to once redeemed. The token is a hub-minted,\n * single-use bearer credential; its presence on a connection attests that the\n * minting authority (a `connectionTokenBinder`-granted endpoint) vouched for the\n * bound slot(s).\n *\n * The two bindings are **independent axes**, each optional:\n *\n * - {@link identitySlot} drives the redeemer's *managed identity* (`identity::*`).\n * - {@link grantedServiceIdNamespace} drives the redeemer's *freely-claimable\n * serviceId namespace* (`hubGrantedServiceId::get`).\n *\n * A binder typically sets both to the same value (the historical convention),\n * but they no longer have to agree — and either may be omitted. A token with\n * neither is still admitted, but the connection routes anonymously (no\n * `identity::*`, no freely-claimable namespace).\n */\nexport interface TokenIdentityBinding {\n /**\n * Managed-identity slot the redeeming connection inherits. Omit to admit the\n * connection without a managed identity.\n */\n readonly identitySlot?: string;\n /**\n * ServiceId namespace the redeeming connection may claim freely (reported\n * via `hubGrantedServiceId::get`). Omit to grant no freely-claimable\n * namespace.\n */\n readonly grantedServiceIdNamespace?: string;\n}\n\n/** A freshly minted token plus its absolute expiry (epoch ms). */\nexport interface MintedToken {\n readonly token: string;\n readonly expiresAt: number;\n}\n\ninterface Entry {\n readonly identitySlot: string | undefined;\n readonly grantedServiceIdNamespace: string | undefined;\n readonly expiresAt: number;\n}\n\nexport interface TokenIdentityStoreOptions {\n /** Default lifetime applied to {@link TokenIdentityStore.mint}. Default 30s. */\n readonly defaultTtlMs?: number;\n /** Clock seam (tests). Default `Date.now`. */\n readonly now?: () => number;\n /** Token generator seam (tests). Default 32 random bytes, base64url. */\n readonly generateToken?: () => string;\n}\n\n/**\n * The hub's connection-token mint + redemption store, shared between the\n * `connectionTokenBinder::bindConnectionToken` front door (mint) and a\n * `managedIdentity: { mode: \"fromToken\" }` / `grantedServiceId: { mode:\n * \"fromToken\" }` listener (redeem).\n *\n * Tokens are random, time-boxed and **single-use**: {@link redeem} consumes the\n * entry, so a token can bind exactly one connection and cannot be replayed once\n * used or expired. Expired entries are pruned lazily on access.\n */\nexport class TokenIdentityStore {\n private readonly _byToken = new Map<string, Entry>();\n private readonly _defaultTtlMs: number;\n private readonly _now: () => number;\n private readonly _generate: () => string;\n\n constructor(options: TokenIdentityStoreOptions = {}) {\n this._defaultTtlMs = options.defaultTtlMs ?? 30_000;\n this._now = options.now ?? ((): number => Date.now());\n this._generate = options.generateToken ?? ((): string => randomBytes(32).toString('base64url'));\n }\n\n /** Mint a fresh single-use token bound to `binding`, valid for `ttlMs`. */\n public mint(binding: TokenIdentityBinding = {}, ttlMs?: number): MintedToken {\n this._prune();\n const token = this._generate();\n const expiresAt = this._now() + (ttlMs ?? this._defaultTtlMs);\n this._byToken.set(token, {\n identitySlot: binding.identitySlot,\n grantedServiceIdNamespace: binding.grantedServiceIdNamespace,\n expiresAt,\n });\n return { token, expiresAt };\n }\n\n /**\n * Non-consuming existence check for the `hubrpc::initialize` token gate.\n * Returns `true` iff the token is live (known and unexpired). Single-use is\n * enforced separately by {@link redeem}.\n */\n public peek(token: string | undefined): boolean {\n if (token === undefined) return false;\n const entry = this._byToken.get(token);\n if (entry === undefined) return false;\n if (entry.expiresAt <= this._now()) {\n this._byToken.delete(token);\n return false;\n }\n return true;\n }\n\n /**\n * Consume `token`, returning its binding exactly once. Returns `undefined`\n * for an unknown, expired or already-redeemed token (the caller should drop\n * the connection in that case).\n */\n public redeem(token: string | undefined): TokenIdentityBinding | undefined {\n if (token === undefined) return undefined;\n const entry = this._byToken.get(token);\n if (entry === undefined) return undefined;\n this._byToken.delete(token);\n if (entry.expiresAt <= this._now()) return undefined;\n const binding: { identitySlot?: string; grantedServiceIdNamespace?: string; } = {};\n if (entry.identitySlot !== undefined) binding.identitySlot = entry.identitySlot;\n if (entry.grantedServiceIdNamespace !== undefined) {\n binding.grantedServiceIdNamespace = entry.grantedServiceIdNamespace;\n }\n return binding;\n }\n\n /** Live (unexpired, unredeemed) token count — primarily for tests/metrics. */\n public get size(): number {\n this._prune();\n return this._byToken.size;\n }\n\n private _prune(): void {\n const now = this._now();\n for (const [token, entry] of this._byToken) {\n if (entry.expiresAt <= now) this._byToken.delete(token);\n }\n }\n}\n","import type { ITransportServer, Transport } from '@hediet/linkrpc/hub/common';\nimport { mapTransport } from '@hediet/linkrpc/hub/common';\n\n/**\n * Attested origin of an incoming connection. Produced by a\n * {@link ConnectionProvenanceProvider} from a freshly accepted transport,\n * *before* any RPC flows. Everything here is, by contract, already verified:\n * there is no \"unverified\" provenance — a provider that cannot fully attest\n * the peer returns `{ error }` instead of a partial result.\n */\nexport interface ConnectionProvenance {\n /**\n * Stable identity key for the peer. INVARIANT: must begin with\n * `` `${provider.identityNamespace}/` `` (e.g. `\"docker/echo-provider\"`),\n * which {@link withProvenance} enforces — a value that doesn't is treated\n * as un-attested. This keeps keys from different provenance sources from\n * colliding and makes the namespace self-describing.\n */\n readonly identityKey: string;\n /**\n * Free-form, already-verified attestation detail for logging and policy\n * (container id, image, pid, uid, …). Never used for routing or trust.\n */\n readonly attributes: Readonly<Record<string, string | number>>;\n}\n\n/**\n * Resolves the {@link ConnectionProvenance} of an accepted transport.\n *\n * Generic over the transport type so the provider can read whatever the\n * concrete transport exposes (e.g. a `NodeSocketTransport`'s `socket` for\n * peercred) — the hub core stays free of those types. Implementations live\n * outside the hub (e.g. `@hediet/linkrpc-extra`).\n */\nexport interface ConnectionProvenanceProvider<TTransport extends Transport> {\n /**\n * Namespace every {@link ConnectionProvenance.identityKey} this provider\n * emits is prefixed with (e.g. `\"docker\"`).\n */\n readonly identityNamespace: string;\n\n /**\n * Resolve provenance for `transport` before any RPC is exchanged. Honour\n * `signal` to abort if the transport closes or a deadline elapses.\n *\n * @returns verified provenance, or `{ error }` to refuse attestation.\n */\n resolve(transport: TTransport, signal: AbortSignal): Promise<ConnectionProvenance | { error: string }>;\n}\n\n/**\n * A {@link Transport} annotated with its (possibly absent) attestation —\n * the narrow shape provenance-aware consumers (e.g. {@link PrefixPolicy})\n * depend on, without naming a concrete backend transport type.\n */\nexport interface ITransportWithProvenance extends Transport {\n readonly provenance: ConnectionProvenance | undefined;\n}\n\n/** A concrete transport `T` annotated with its (possibly absent) attestation. */\nexport type WithProvenance<T extends Transport> = T & {\n readonly provenance: ConnectionProvenance | undefined;\n};\n\nexport interface WithProvenanceOptions {\n /**\n * Abort the provider's `resolve` after this many ms (counts as\n * un-attested). Omit for no deadline.\n */\n readonly resolveTimeoutMs?: number;\n /**\n * Drop connections that could not be attested instead of forwarding them\n * with `provenance: undefined`. Default `false`.\n */\n readonly requireProvenance?: boolean;\n}\n\n/**\n * Annotate every transport from `source` with the provenance `provider`\n * resolves for it. On provider error/timeout — or when the returned\n * `identityKey` does not start with `` `${provider.identityNamespace}/` `` —\n * the transport is forwarded with `provenance: undefined`, unless\n * {@link WithProvenanceOptions.requireProvenance} is set, in which case it is\n * disposed and dropped.\n *\n * This is the one concrete {@link mapTransport} the hub ships: it is where\n * `net.Socket` (or any backend handle) is read for attestation and nowhere\n * else.\n */\nexport function withProvenance<T extends Transport>(\n source: ITransportServer<T>,\n provider: ConnectionProvenanceProvider<T>,\n options: WithProvenanceOptions = {},\n): ITransportServer<WithProvenance<T>> {\n const namespacePrefix = `${provider.identityNamespace}/`;\n return mapTransport(source, async (t) => {\n const controller = new AbortController();\n t.onDidClose(() => controller.abort());\n let timer: ReturnType<typeof setTimeout> | undefined;\n if (options.resolveTimeoutMs !== undefined) {\n timer = setTimeout(() => controller.abort(), options.resolveTimeoutMs);\n }\n\n let provenance: ConnectionProvenance | undefined;\n try {\n const result = await provider.resolve(t, controller.signal);\n if (!('error' in result) && result.identityKey.startsWith(namespacePrefix)) {\n provenance = result;\n }\n } catch {\n provenance = undefined;\n } finally {\n if (timer !== undefined) {\n clearTimeout(timer);\n }\n }\n\n if (provenance === undefined && options.requireProvenance) {\n t.dispose();\n return undefined;\n }\n return Object.assign(t, { provenance }) as WithProvenance<T>;\n });\n}\n","import type { PrincipalId } from '@hediet/linkrpc';\nimport type { ITransportWithProvenance } from './provenance';\nimport { isValidServiceId, ROOT_SERVICE_ID, type ServiceId } from '@hediet/linkrpc/hub/common';\nimport type { Transport } from '@hediet/linkrpc/hub/common';\n\n/** What a prefix-claim is judged against. */\nexport interface ClaimContext<TTransport extends Transport> {\n /**\n * Node id of the participant's hub-minted managed identity, or `undefined`\n * if the connection has no identity (anonymous routing).\n */\n readonly principal: PrincipalId | undefined;\n /** The connecting transport (may carry provenance, depending on `T`). */\n readonly transport: TTransport;\n /** ServiceId prefix the participant is trying to claim. */\n readonly requestedPrefix: ServiceId;\n}\n\n/**\n * Decides whether a participant may own a routing prefix. This is the policy\n * wrapped around the hub's single privileged write seam (`claimPrefix`) — the\n * only place identity/provenance gates routing.\n */\nexport interface PrefixPolicy<TTransport extends Transport> {\n authorizeClaim(\n ctx: ClaimContext<TTransport>,\n ): { ok: true; } | { ok: false; reason: string; };\n}\n\nexport interface PrincipalIdPrefixPolicyOptions<TTransport extends ITransportWithProvenance> {\n /**\n * Prefixes a participant is allowed to claim. Default: the transport's\n * attested {@link ConnectionProvenance.identityKey} used **verbatim** as\n * the sole claimable prefix (e.g. `\"docker/echo-provider\"`). The full,\n * namespace-scoped key is the prefix — it is never split or rewritten. A\n * connection with no provenance may claim nothing.\n */\n derivePrefixes?(ctx: ClaimContext<TTransport>): readonly ServiceId[];\n}\n\n/**\n * Default {@link PrefixPolicy}: a participant may claim only the prefix(es)\n * {@link PrincipalIdPrefixPolicyOptions.derivePrefixes} grants it. The out-of-box\n * derivation ties the claimable prefix to the peer's attested provenance, so\n * `docker/echo-provider` can serve `docker/echo-provider/*` and nothing can\n * impersonate it.\n */\nexport class PrincipalIdPrefixPolicy<TTransport extends ITransportWithProvenance>\n implements PrefixPolicy<TTransport>\n{\n private readonly _derive: (ctx: ClaimContext<TTransport>) => readonly ServiceId[];\n\n constructor(options: PrincipalIdPrefixPolicyOptions<TTransport> = {}) {\n this._derive = options.derivePrefixes ?? defaultDerivePrefixes;\n }\n\n public authorizeClaim(\n ctx: ClaimContext<TTransport>,\n ): { ok: true; } | { ok: false; reason: string; } {\n const allowed = this._derive(ctx);\n if (allowed.includes(ctx.requestedPrefix)) {\n return { ok: true };\n }\n return {\n ok: false,\n reason: `not authorized to claim '${ctx.requestedPrefix}'`,\n };\n }\n}\n\nfunction defaultDerivePrefixes<TTransport extends ITransportWithProvenance>(\n ctx: ClaimContext<TTransport>,\n): readonly ServiceId[] {\n const provenance = ctx.transport.provenance;\n if (!provenance) {\n return [];\n }\n // SECURITY: the attested `identityKey` is itself the claimable prefix and\n // is used verbatim. It is a fully-qualified, namespace-scoped ServiceId\n // (e.g. `docker/echo-provider`). The namespace segment is load-bearing —\n // stripping it would let `docker/echo` and `k8s/echo` derive the same\n // prefix and impersonate one another. We only accept it if it is a\n // well-formed, non-root ServiceId; anything else grants no claim.\n const prefix = provenance.identityKey;\n if (prefix === ROOT_SERVICE_ID || !isValidServiceId(prefix)) {\n return [];\n }\n return [prefix];\n}\n","import * as fs from \"node:fs/promises\";\nimport { createRequire } from \"node:module\";\nimport * as path from \"node:path\";\nimport {\n InMemoryManagedIdentity,\n base64UrlToBytes,\n bytesToBase64Url,\n crypto,\n type Identity,\n type Keypair,\n type ManagedIdentityStorageBackend,\n type X25519Keypair,\n} from \"@hediet/linkrpc\";\nimport type {\n IdentityKeystore,\n IdentitySlot,\n IdentitySnooze,\n SlotAccessKey,\n SlotByIdAndKeyResult,\n SlotByIdAndTimeResult,\n SlotId,\n} from \"./identityKeystore\";\n\ntype DatabaseSync = import(\"node:sqlite\").DatabaseSync;\ntype DatabaseSyncConstructor = typeof import(\"node:sqlite\").DatabaseSync;\n\nlet _DatabaseSync: DatabaseSyncConstructor | undefined;\n\nfunction _getDatabaseSync(): DatabaseSyncConstructor {\n if (_DatabaseSync === undefined) {\n const sqlite = createRequire(import.meta.url)(\"node:sqlite\") as typeof import(\"node:sqlite\");\n _DatabaseSync = sqlite.DatabaseSync;\n }\n return _DatabaseSync;\n}\n\n/**\n * SQLite-backed {@link IdentityKeystore} — a drop-in alternative to the\n * file-per-slot {@link import('./identityKeystore').createIdentityKeystore}.\n * All slot state (identity keypair, per-slot KV storage, the access-key gate,\n * the consent snooze, and the recorded app-files folder) lives in **one**\n * database file across three tables, so the whole keystore is a single\n * `DatabaseSync` connection instead of four files per slot.\n *\n * The model is identical to the file keystore — `id` (the slot), `keys` (the\n * gate), and `time` (the snooze) — even though config/CLI hubs only exercise\n * the `id` + storage axes today. Keeping the full surface means the extension\n * and node-runner can adopt this backend unchanged later.\n *\n * ## Encryption seam\n *\n * This backend currently runs in **unencrypted mode**: secret-bearing columns\n * (identity private keys, storage values) are stored plaintext via the\n * identity {@link _codec}. When at-rest encryption is added, swap {@link _codec}\n * for an AEAD codec keyed by a `keystoreSecret` with `slotId` bound into the\n * AAD (mirroring the file keystore's per-file AAD). The columns stay `TEXT`\n * (ciphertext is base64url), so enabling encryption is a codec swap plus a\n * one-time re-encode — not a schema migration.\n *\n * Uses the built-in `node:sqlite` `DatabaseSync`, matching the monorepo's\n * other SQLite stores. The module is loaded only when a keystore opens a\n * database, so consumers that only import the Hub package do not receive\n * Node's SQLite experimental warning.\n */\nexport interface SqliteIdentitySlot extends IdentitySlot {\n /**\n * Write a **specific** keypair into this slot iff it has no identity yet,\n * and return the resulting identity. Used to import a legacy plaintext\n * identity while preserving its principal. If an identity already exists\n * it is returned unchanged (the import is a no-op).\n */\n importIdentity(ed: Keypair, wrap: X25519Keypair): Promise<Identity>;\n}\n\nexport interface SqliteIdentityKeystore extends IdentityKeystore {\n slotById(id: SlotId): SqliteIdentitySlot;\n}\n\nexport interface SqliteIdentityKeystoreOptions {\n /**\n * Path to the SQLite database file (created if missing) or `\":memory:\"`.\n * Ignored when {@link db} is provided.\n */\n readonly dbPath?: string;\n /** An existing connection to reuse (share one file across stores). */\n readonly db?: DatabaseSync;\n}\n\n/**\n * Value codec for secret-bearing columns. Identity in unencrypted mode; the\n * single choke point an AEAD codec would replace. `slotId` is threaded through\n * so an encrypting codec can bind it into the AAD.\n */\ninterface _ValueCodec {\n encode(slotId: string, plaintext: string): string;\n decode(slotId: string, stored: string): string;\n}\n\nconst _codec: _ValueCodec = {\n encode: (_slotId, plaintext) => plaintext,\n decode: (_slotId, stored) => stored,\n};\n\nconst _SCHEMA = `\nCREATE TABLE IF NOT EXISTS identity_slots (\n slot_id TEXT PRIMARY KEY,\n ed25519_priv TEXT,\n ed25519_pub TEXT,\n x25519_priv TEXT,\n x25519_pub TEXT,\n identity_created_at INTEGER,\n files_dir TEXT,\n snooze_scope TEXT,\n snooze_expires_at INTEGER,\n created_at INTEGER NOT NULL\n);\nCREATE TABLE IF NOT EXISTS identity_storage (\n slot_id TEXT NOT NULL,\n key TEXT NOT NULL,\n value TEXT NOT NULL,\n PRIMARY KEY (slot_id, key)\n);\nCREATE TABLE IF NOT EXISTS identity_keys (\n slot_id TEXT NOT NULL,\n key_json TEXT NOT NULL,\n PRIMARY KEY (slot_id, key_json)\n);\n`;\n\n/** Open (creating if needed) a SQLite-backed {@link IdentityKeystore}. */\nexport function createSqliteIdentityKeystore(\n opts: SqliteIdentityKeystoreOptions,\n): SqliteIdentityKeystore {\n const db = opts.db ?? new (_getDatabaseSync())(_requireDbPath(opts));\n db.exec(_SCHEMA);\n return new _SqliteKeystore(db);\n}\n\nfunction _requireDbPath(opts: SqliteIdentityKeystoreOptions): string {\n if (opts.dbPath === undefined) {\n throw new Error(\"createSqliteIdentityKeystore: pass either `dbPath` or `db`\");\n }\n return opts.dbPath;\n}\n\n/** Prepared statements shared by every slot handle on one connection. */\nclass _Stmts {\n readonly selIdentity;\n readonly upsertIdentity;\n readonly ensureSlot;\n readonly slotExists;\n readonly setFilesDir;\n readonly getFilesDir;\n readonly setSnooze;\n readonly getSnooze;\n readonly clearSnooze;\n readonly listKeys;\n readonly hasKey;\n readonly addKey;\n readonly delKey;\n readonly storGet;\n readonly storSet;\n readonly storDel;\n readonly storKeys;\n readonly storCount;\n readonly delSlot;\n readonly delStorage;\n readonly delKeys;\n\n constructor(db: DatabaseSync) {\n this.selIdentity = db.prepare(\n `SELECT ed25519_priv, ed25519_pub, x25519_priv, x25519_pub\n FROM identity_slots WHERE slot_id = ?`,\n );\n this.upsertIdentity = db.prepare(\n `INSERT INTO identity_slots\n (slot_id, ed25519_priv, ed25519_pub, x25519_priv, x25519_pub,\n identity_created_at, created_at)\n VALUES (?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(slot_id) DO UPDATE SET\n ed25519_priv = excluded.ed25519_priv,\n ed25519_pub = excluded.ed25519_pub,\n x25519_priv = excluded.x25519_priv,\n x25519_pub = excluded.x25519_pub,\n identity_created_at = excluded.identity_created_at`,\n );\n this.ensureSlot = db.prepare(\n `INSERT OR IGNORE INTO identity_slots (slot_id, created_at) VALUES (?, ?)`,\n );\n this.slotExists = db.prepare(`SELECT 1 FROM identity_slots WHERE slot_id = ?`);\n this.setFilesDir = db.prepare(\n `UPDATE identity_slots SET files_dir = ? WHERE slot_id = ?`,\n );\n this.getFilesDir = db.prepare(`SELECT files_dir FROM identity_slots WHERE slot_id = ?`);\n this.setSnooze = db.prepare(\n `UPDATE identity_slots SET snooze_scope = ?, snooze_expires_at = ? WHERE slot_id = ?`,\n );\n this.getSnooze = db.prepare(\n `SELECT snooze_scope, snooze_expires_at FROM identity_slots WHERE slot_id = ?`,\n );\n this.clearSnooze = db.prepare(\n `UPDATE identity_slots SET snooze_scope = NULL, snooze_expires_at = NULL WHERE slot_id = ?`,\n );\n this.listKeys = db.prepare(`SELECT key_json FROM identity_keys WHERE slot_id = ?`);\n this.hasKey = db.prepare(\n `SELECT 1 FROM identity_keys WHERE slot_id = ? AND key_json = ?`,\n );\n this.addKey = db.prepare(\n `INSERT OR IGNORE INTO identity_keys (slot_id, key_json) VALUES (?, ?)`,\n );\n this.delKey = db.prepare(\n `DELETE FROM identity_keys WHERE slot_id = ? AND key_json = ?`,\n );\n this.storGet = db.prepare(\n `SELECT value FROM identity_storage WHERE slot_id = ? AND key = ?`,\n );\n this.storSet = db.prepare(\n `INSERT OR REPLACE INTO identity_storage (slot_id, key, value) VALUES (?, ?, ?)`,\n );\n this.storDel = db.prepare(\n `DELETE FROM identity_storage WHERE slot_id = ? AND key = ?`,\n );\n this.storKeys = db.prepare(`SELECT key FROM identity_storage WHERE slot_id = ?`);\n this.storCount = db.prepare(\n `SELECT COUNT(*) AS n FROM identity_storage WHERE slot_id = ?`,\n );\n this.delSlot = db.prepare(`DELETE FROM identity_slots WHERE slot_id = ?`);\n this.delStorage = db.prepare(`DELETE FROM identity_storage WHERE slot_id = ?`);\n this.delKeys = db.prepare(`DELETE FROM identity_keys WHERE slot_id = ?`);\n }\n}\n\nclass _SqliteKeystore implements SqliteIdentityKeystore {\n private readonly _stmts: _Stmts;\n private readonly _slots = new Map<SlotId, _SqliteSlot>();\n\n constructor(db: DatabaseSync) {\n this._stmts = new _Stmts(db);\n }\n\n public slotById(id: SlotId): SqliteIdentitySlot {\n return this._slot(id);\n }\n\n private _slot(id: SlotId): _SqliteSlot {\n let existing = this._slots.get(id);\n if (existing === undefined) {\n existing = new _SqliteSlot(id, this._stmts);\n this._slots.set(id, existing);\n }\n return existing;\n }\n\n public async slotByIdAndKey(id: SlotId, key: SlotAccessKey): Promise<SlotByIdAndKeyResult> {\n const slot = this._slot(id);\n if (!(await slot._exists())) return { error: \"slotDoesNotExist\" };\n if (!(await slot.hasKey(key))) return { error: \"unknownKey\" };\n return slot;\n }\n\n public async slotByIdAndTime(id: SlotId, nowMs: number): Promise<SlotByIdAndTimeResult> {\n const slot = this._slot(id);\n // Nothing to inherit → granting access exposes nothing; authorize.\n if (!(await slot.hasState())) return slot;\n // An active snooze is a time-boxed grant covering `nowMs`.\n const snooze = await slot.getSnooze();\n if (snooze !== undefined && snooze.expiresAt > nowMs) return slot;\n return { error: \"consentRequired\" };\n }\n\n public async createSlotWithKey(id: SlotId, key: SlotAccessKey): Promise<IdentitySlot> {\n const slot = this.slotById(id);\n await slot.addKeys(key);\n return slot;\n }\n}\n\nclass _SqliteSlot implements SqliteIdentitySlot {\n private readonly _storage: _SqliteSlotStorage;\n\n constructor(\n public readonly id: SlotId,\n private readonly _s: _Stmts,\n ) {\n this._storage = new _SqliteSlotStorage(id, _s);\n }\n\n public get storage(): ManagedIdentityStorageBackend {\n return this._storage;\n }\n\n // ---- identity -------------------------------------------------------\n\n public async getOrCreateIdentity(): Promise<Identity> {\n const existing = await this.peekIdentity();\n if (existing) return existing;\n const ed = await crypto.generateKeypair();\n const wrap = await crypto.generateX25519Keypair();\n this._writeIdentity(ed, wrap);\n return new InMemoryManagedIdentity(ed, wrap);\n }\n\n public async importIdentity(ed: Keypair, wrap: X25519Keypair): Promise<Identity> {\n const existing = await this.peekIdentity();\n if (existing) return existing;\n this._writeIdentity(ed, wrap);\n return new InMemoryManagedIdentity(ed, wrap);\n }\n\n public async peekIdentity(): Promise<Identity | undefined> {\n const row = this._s.selIdentity.get(this.id) as\n | {\n ed25519_priv: string | null;\n ed25519_pub: string | null;\n x25519_priv: string | null;\n x25519_pub: string | null;\n }\n | undefined;\n if (\n row === undefined ||\n row.ed25519_priv === null ||\n row.ed25519_pub === null ||\n row.x25519_priv === null ||\n row.x25519_pub === null\n ) {\n return undefined;\n }\n const ed: Keypair = {\n privateKey: base64UrlToBytes(_codec.decode(this.id, row.ed25519_priv)),\n publicKey: base64UrlToBytes(row.ed25519_pub),\n };\n const wrap: X25519Keypair = {\n privateKey: base64UrlToBytes(_codec.decode(this.id, row.x25519_priv)),\n publicKey: base64UrlToBytes(row.x25519_pub),\n };\n return new InMemoryManagedIdentity(ed, wrap);\n }\n\n private _writeIdentity(ed: Keypair, wrap: X25519Keypair): void {\n this._s.upsertIdentity.run(\n this.id,\n _codec.encode(this.id, bytesToBase64Url(ed.privateKey)),\n bytesToBase64Url(ed.publicKey),\n _codec.encode(this.id, bytesToBase64Url(wrap.privateKey)),\n bytesToBase64Url(wrap.publicKey),\n Date.now(),\n Date.now(),\n );\n }\n\n // ---- access keys (the gate) -----------------------------------------\n\n public async listKeys(): Promise<SlotAccessKey[]> {\n const rows = this._s.listKeys.all(this.id) as { key_json: string }[];\n return rows.map((r) => _fromCanonicalKey(r.key_json));\n }\n\n public async hasKey(key: SlotAccessKey): Promise<boolean> {\n return this._s.hasKey.get(this.id, _canonicalKey(key)) !== undefined;\n }\n\n public async addKeys(...keys: SlotAccessKey[]): Promise<void> {\n if (keys.length === 0) return;\n this._ensureRow();\n for (const key of keys) {\n this._s.addKey.run(this.id, _canonicalKey(key));\n }\n }\n\n public async deleteKey(key: SlotAccessKey): Promise<boolean> {\n const res = this._s.delKey.run(this.id, _canonicalKey(key));\n return res.changes > 0;\n }\n\n /** Internal: has this slot ever been created? */\n public async _exists(): Promise<boolean> {\n return this._s.slotExists.get(this.id) !== undefined;\n }\n\n // ---- consent snooze (the time axis) ---------------------------------\n\n public async getSnooze(): Promise<IdentitySnooze | undefined> {\n const row = this._s.getSnooze.get(this.id) as\n | { snooze_scope: string | null; snooze_expires_at: number | null }\n | undefined;\n if (\n row === undefined ||\n row.snooze_scope === null ||\n row.snooze_expires_at === null\n ) {\n return undefined;\n }\n if (Date.now() >= row.snooze_expires_at) return undefined;\n return {\n scope: row.snooze_scope as IdentitySnooze[\"scope\"],\n expiresAt: row.snooze_expires_at,\n };\n }\n\n public async setSnooze(snooze: IdentitySnooze): Promise<void> {\n this._ensureRow();\n this._s.setSnooze.run(snooze.scope, snooze.expiresAt, this.id);\n }\n\n public async clearSnooze(): Promise<void> {\n this._s.clearSnooze.run(this.id);\n }\n\n // ---- associated plaintext file folder -------------------------------\n\n public async getFilesDir(): Promise<string | undefined> {\n const row = this._s.getFilesDir.get(this.id) as\n | { files_dir: string | null }\n | undefined;\n if (row === undefined || row.files_dir === null) return undefined;\n return row.files_dir;\n }\n\n public async setFilesDir(absPath: string): Promise<void> {\n this._ensureRow();\n this._s.setFilesDir.run(absPath, this.id);\n }\n\n public async hasFiles(): Promise<boolean> {\n const dir = await this.getFilesDir();\n if (dir === undefined) return false;\n try {\n const entries = await fs.readdir(dir);\n return entries.length > 0;\n } catch (e: unknown) {\n if ((e as NodeJS.ErrnoException).code === \"ENOENT\") return false;\n throw e;\n }\n }\n\n public async wipeFiles(): Promise<void> {\n const dir = await this.getFilesDir();\n if (dir === undefined) return;\n let entries: string[];\n try {\n entries = await fs.readdir(dir);\n } catch (e: unknown) {\n if ((e as NodeJS.ErrnoException).code === \"ENOENT\") return;\n throw e;\n }\n await Promise.all(\n entries.map((name) => fs.rm(path.join(dir, name), { recursive: true, force: true })),\n );\n }\n\n // ---- state ----------------------------------------------------------\n\n public async hasState(): Promise<boolean> {\n if (await this.peekIdentity()) return true;\n const { n } = this._s.storCount.get(this.id) as { n: number };\n if (n > 0) return true;\n return this.hasFiles();\n }\n\n // ---- lifecycle ------------------------------------------------------\n\n public async delete(): Promise<void> {\n const filesDir = await this.getFilesDir().catch(() => undefined);\n if (filesDir !== undefined) {\n await fs.rm(filesDir, { recursive: true, force: true });\n }\n this._s.delStorage.run(this.id);\n this._s.delKeys.run(this.id);\n this._s.delSlot.run(this.id);\n }\n\n private _ensureRow(): void {\n this._s.ensureSlot.run(this.id, Date.now());\n }\n}\n\n/** Per-slot KV backing `identity.storage::*`, one row per key in `identity_storage`. */\nclass _SqliteSlotStorage implements ManagedIdentityStorageBackend {\n constructor(\n private readonly _slotId: SlotId,\n private readonly _s: _Stmts,\n ) {}\n\n public async get(key: string): Promise<unknown | undefined> {\n const row = this._s.storGet.get(this._slotId, key) as { value: string } | undefined;\n if (row === undefined) return undefined;\n return JSON.parse(_codec.decode(this._slotId, row.value));\n }\n\n public async set(key: string, value: unknown): Promise<void> {\n this._s.ensureSlot.run(this._slotId, Date.now());\n this._s.storSet.run(this._slotId, key, _codec.encode(this._slotId, JSON.stringify(value)));\n }\n\n public async delete(key: string): Promise<boolean> {\n const res = this._s.storDel.run(this._slotId, key);\n return res.changes > 0;\n }\n\n public async list(prefix?: string): Promise<string[]> {\n const rows = this._s.storKeys.all(this._slotId) as { key: string }[];\n const keys = rows.map((r) => r.key);\n return prefix === undefined ? keys : keys.filter((k) => k.startsWith(prefix));\n }\n}\n\n/** Canonical (sorted-field) JSON for exact access-key equality. */\nfunction _canonicalKey(key: SlotAccessKey): string {\n const entries = Object.keys(key)\n .sort()\n .map((k) => [k, key[k]] as const);\n return JSON.stringify(entries);\n}\n\n/** Rebuild an access key from its canonical (sorted `[key, value]` pairs) JSON. */\nfunction _fromCanonicalKey(canonical: string): SlotAccessKey {\n const entries = JSON.parse(canonical) as [string, string][];\n return Object.fromEntries(entries);\n}\n","import * as fs from \"node:fs/promises\";\nimport * as path from \"node:path\";\nimport { createHash } from \"node:crypto\";\nimport {\n InMemoryManagedIdentity,\n type ManagedIdentityStorageBackend,\n type Identity,\n base64UrlToBytes,\n bytesToBase64Url,\n type Keypair,\n type X25519Keypair,\n crypto,\n} from \"@hediet/linkrpc\";\n\n/**\n * On-disk encrypted keystore for managed identities. State is keyed by\n * {@link SlotId} (the absolute path to the entry HTML / Node entry point).\n * Each slot owns up to four files under `storageDir`, mode `0o600`,\n * AES-256-GCM-sealed under `keystoreSecret`:\n *\n * - `<hash>.bin` — the managed identity keypair\n * - `<hash>.storage.bin` — the per-slot persistent KV store\n * - `<hash>.keys.bin` — the access-key allow-list (gate)\n * - `<hash>.snooze.bin` — the consent re-prompt suppression (optional)\n * - `<hash>.filesdir.bin`— the recorded path of the plaintext app-files\n * folder (optional; the folder itself lives\n * wherever the owner placed it, NOT here)\n *\n * `<hash>` is `sha256(id).slice(0,16)`. The slot id is bound into each\n * file's AEAD AAD so swapping files between slots fails the auth tag.\n *\n * The `keystoreSecret` itself is opaque bytes — the keystore does not\n * know or care where it came from. In the extension, it's loaded from\n * `vscode.SecretStorage`.\n */\nexport interface IdentityKeystoreOptions {\n /** Absolute directory path. Created if missing. */\n readonly storageDir: string;\n /** 32+ bytes of secret material used to seal identity files. */\n readonly keystoreSecret: Uint8Array;\n}\n\n/**\n * Stable slot identity: the absolute path to the entry HTML / Node entry\n * point. One slot ⇒ one managed identity + one storage backend, shared\n * across every approved {@link SlotAccessKey}.\n */\nexport type SlotId = string;\n\n/**\n * An access key: a small, exact-match qualifier tuple a caller must\n * present to reach a slot's identity/storage. Conventionally\n * `{ appId, bundleHash }`, but the keystore treats it opaquely. Compared\n * by canonical (sorted-field) JSON equality.\n */\nexport type SlotAccessKey = Readonly<Record<string, string>>;\n\n/**\n * Result of `slotByIdAndKey`. Either the resolved\n * slot, or a discriminated error:\n * - `slotDoesNotExist` — no slot has ever been created for this id.\n * - `unknownKey` — the slot exists but `key` is not in its\n * allow-list (a different/unapproved bundle).\n */\nexport type SlotByIdAndKeyResult =\n | IdentitySlot\n | { readonly error: \"slotDoesNotExist\" }\n | { readonly error: \"unknownKey\" };\n\n/**\n * Result of `slotByIdAndTime`. Either the resolved slot (access is currently\n * time-authorized) or a single discriminated error:\n * - `consentRequired` — the slot holds inheritable state and no active\n * snooze covers the supplied time, so the caller\n * must obtain fresh consent before granting identity.\n */\nexport type SlotByIdAndTimeResult =\n | IdentitySlot\n | { readonly error: \"consentRequired\" };\n\n/**\n * A time-boxed, scoped suppression of identity-access consent re-prompts for\n * a slot. Set from the consent dialog's \"don't ask for file changes for 24h\"\n * checkbox; read on each load to decide whether a content change can load\n * silently. Cleared by {@link IdentitySlot.delete}.\n *\n * - `scope: \"entry\"` — covers entry-HTML content changes only.\n * - `scope: \"all\"` — covers entry-HTML AND external-file content changes.\n * - `expiresAt` — absolute epoch ms; expired snoozes are ignored.\n *\n * A snooze never suppresses a *new external path* — that is always a fresh\n * blast-radius decision the user must see.\n */\nexport interface IdentitySnooze {\n readonly scope: \"entry\" | \"all\";\n readonly expiresAt: number;\n}\n\nexport interface IdentitySlot {\n readonly id: SlotId;\n\n // ---- identity + storage (shared per id) -----------------------------\n\n /** The slot's managed identity; generated + persisted on first call. */\n getOrCreateIdentity(): Promise<Identity>;\n\n /** Existing identity or `undefined` — never creates. */\n peekIdentity(): Promise<Identity | undefined>;\n\n /** Per-slot persistent KV (caps + app data). File created on first write. */\n readonly storage: ManagedIdentityStorageBackend;\n\n // ---- associated plaintext file folder -------------------------------\n\n /**\n * Absolute path of the slot's *plaintext* app-files folder, or\n * `undefined` if none has been recorded yet. The keystore only stores\n * and wipes this opaque string — the owner (the extension) chooses the\n * path (typically `<globalStorage>/app-files/<appId>` with a collision\n * counter) and performs the actual file IO.\n *\n * Recording the path in the slot is deliberate: the folder name is not\n * derivable from the app id alone (the counter breaks that), so an app\n * can only reach its own folder by looking it up through its slot. This\n * both isolates apps from each other and prevents accidental access to a\n * sibling's folder.\n */\n getFilesDir(): Promise<string | undefined>;\n\n /** Record the chosen absolute path of this slot's app-files folder. */\n setFilesDir(absPath: string): Promise<void>;\n\n /**\n * `true` iff a files folder is recorded AND it currently holds ≥1 entry.\n * Counts toward {@link hasState} so a content change to a privileged app\n * still trips the consent gate even when the app stored only files (and\n * never created an identity or KV state).\n */\n hasFiles(): Promise<boolean>;\n\n /**\n * Delete every entry inside the recorded files folder but keep the\n * folder itself and its slot record (so the path stays stable for the\n * user). No-op when no folder is recorded. Used by the consent dialog's\n * \"wipe app files\" option on a *keep-identity* load.\n */\n wipeFiles(): Promise<void>;\n\n /**\n * `true` iff there is anything a consumer could inherit: an identity\n * has been created, storage holds ≥1 entry, OR the app-files folder\n * holds ≥1 entry. When `false`, granting access exposes nothing — the\n * consent prompt can be skipped safely.\n */\n hasState(): Promise<boolean>;\n\n // ---- access keys (the gate) -----------------------------------------\n\n /** All keys currently allowed to access this slot. */\n listKeys(): Promise<SlotAccessKey[]>;\n\n /** `true` iff `key` (exact canonical match) is in the allow-list. */\n hasKey(key: SlotAccessKey): Promise<boolean>;\n\n /** Add keys to the allow-list (creating the slot if needed). Idempotent. */\n addKeys(...keys: SlotAccessKey[]): Promise<void>;\n\n /** Remove one key. Returns `true` iff it was present. */\n deleteKey(key: SlotAccessKey): Promise<boolean>;\n\n // ---- consent snooze -------------------------------------------------\n\n /**\n * The active, unexpired snooze for this slot, or `undefined`. Lazily\n * treats an expired snooze as absent (does not rewrite the file).\n */\n getSnooze(): Promise<IdentitySnooze | undefined>;\n\n /** Persist a snooze for this slot (overwrites any previous one). */\n setSnooze(snooze: IdentitySnooze): Promise<void>;\n\n /** Remove any snooze for this slot. Idempotent. */\n clearSnooze(): Promise<void>;\n\n // ---- lifecycle ------------------------------------------------------\n\n /** Wipe identity, storage, AND all access keys for this slot. */\n delete(): Promise<void>;\n}\n\nexport interface IdentityKeystore {\n /**\n * Resolve the slot for `id`, enforcing the access gate. Returns the\n * slot on success, or a discriminated error if the slot does not exist\n * or `key` is not in its allow-list. Use this on the privileged path\n * (registering `identity::*` on an iframe overlay) so an unapproved\n * bundle can never reach prior state.\n */\n slotByIdAndKey(id: SlotId, key: SlotAccessKey): Promise<SlotByIdAndKeyResult>;\n\n /**\n * Resolve the slot for `id`, gated on *time* rather than a content key.\n * Returns the slot when access is currently authorized — either the slot\n * holds nothing inheritable ({@link IdentitySlot.hasState} is `false`, so\n * granting exposes nothing) or an active, unexpired snooze covers `nowMs`.\n * Otherwise returns `consentRequired`, meaning the slot holds state the\n * caller must obtain fresh consent for. Used on the privileged path for\n * URL apps, which have no content key to present to {@link slotByIdAndKey}\n * but may inherit the slot's identity under a time-boxed grant.\n */\n slotByIdAndTime(id: SlotId, nowMs: number): Promise<SlotByIdAndTimeResult>;\n\n /**\n * Create the slot for `id` (if missing) and add `key` to its\n * allow-list. Idempotent: re-creating an existing slot just ensures the\n * key is present. Returns the resolved slot. This is the only way to\n * bootstrap a brand-new slot before {@link slotByIdAndKey} can succeed.\n */\n createSlotWithKey(id: SlotId, key: SlotAccessKey): Promise<IdentitySlot>;\n\n /**\n * Unsafe: return the slot handle for `id` with NO key check. Used to\n * inspect `hasState`/`listKeys` and to run the consent decision before\n * a key has been added. Never register the identity returned from here\n * without having checked/added a key.\n */\n slotById(id: SlotId): IdentitySlot;\n}\n\n/** Identity file payload, AES-256-GCM-decrypted. */\ninterface PlainPayload {\n readonly schemaVersion: 1;\n readonly slot: string;\n readonly ed25519: { readonly privateKey: string; readonly publicKey: string };\n readonly x25519: { readonly privateKey: string; readonly publicKey: string };\n readonly createdAt: number;\n}\n\n/** Storage file payload, AES-256-GCM-decrypted. */\ninterface StoragePayload {\n readonly schemaVersion: 1;\n readonly slot: string;\n readonly entries: Record<string, unknown>;\n readonly updatedAt: number;\n}\n\n/** Keys file payload, AES-256-GCM-decrypted. */\ninterface KeysPayload {\n readonly schemaVersion: 1;\n readonly slot: string;\n readonly keys: ReadonlyArray<Record<string, string>>;\n readonly updatedAt: number;\n}\n\n/** Snooze file payload, AES-256-GCM-decrypted. */\ninterface SnoozePayload {\n readonly schemaVersion: 1;\n readonly slot: string;\n readonly scope: \"entry\" | \"all\";\n readonly expiresAt: number;\n}\n\n/** Files-dir file payload, AES-256-GCM-decrypted. */\ninterface FilesDirPayload {\n readonly schemaVersion: 1;\n readonly slot: string;\n /** Absolute path of the plaintext app-files folder. */\n readonly dir: string;\n readonly updatedAt: number;\n}\n\nconst _FILE_SCHEMA_VERSION = 1;\nconst _DOMAIN = \"hubrpc.identity.keystore.v1\";\nconst _STORAGE_DOMAIN = \"hubrpc.identity.storage.v1\";\nconst _KEYS_DOMAIN = \"hubrpc.identity.keys.v1\";\nconst _SNOOZE_DOMAIN = \"hubrpc.identity.snooze.v1\";\nconst _FILESDIR_DOMAIN = \"hubrpc.identity.filesdir.v1\";\n\nexport function createIdentityKeystore(opts: IdentityKeystoreOptions): IdentityKeystore {\n if (opts.keystoreSecret.length < 32) {\n throw new Error(\"createIdentityKeystore: keystoreSecret must be at least 32 bytes\");\n }\n return new _IdentityKeystoreImpl(opts);\n}\n\nclass _IdentityKeystoreImpl implements IdentityKeystore {\n private readonly _slots = new Map<SlotId, _IdentitySlotImpl>();\n private readonly _opts: IdentityKeystoreOptions;\n private _dirReady = false;\n\n constructor(opts: IdentityKeystoreOptions) {\n this._opts = opts;\n }\n\n public slotById(id: SlotId): IdentitySlot {\n return this._slot(id);\n }\n\n public async slotByIdAndKey(id: SlotId, key: SlotAccessKey): Promise<SlotByIdAndKeyResult> {\n const slot = this._slot(id);\n if (!(await slot._exists())) return { error: \"slotDoesNotExist\" };\n if (!(await slot.hasKey(key))) return { error: \"unknownKey\" };\n return slot;\n }\n\n public async slotByIdAndTime(id: SlotId, nowMs: number): Promise<SlotByIdAndTimeResult> {\n const slot = this._slot(id);\n // Nothing to inherit → granting access exposes nothing; authorize.\n if (!(await slot.hasState())) return slot;\n // An active snooze is a time-boxed grant covering `nowMs`.\n const snooze = await slot.getSnooze();\n if (snooze !== undefined && snooze.expiresAt > nowMs) return slot;\n return { error: \"consentRequired\" };\n }\n\n public async createSlotWithKey(id: SlotId, key: SlotAccessKey): Promise<IdentitySlot> {\n const slot = this._slot(id);\n await slot.addKeys(key);\n return slot;\n }\n\n private _slot(id: SlotId): _IdentitySlotImpl {\n let existing = this._slots.get(id);\n if (existing) return existing;\n const hash = createHash(\"sha256\").update(id).digest(\"hex\").slice(0, 16);\n existing = new _IdentitySlotImpl({\n id,\n identityFile: path.join(this._opts.storageDir, `${hash}.bin`),\n storageFile: path.join(this._opts.storageDir, `${hash}.storage.bin`),\n keysFile: path.join(this._opts.storageDir, `${hash}.keys.bin`),\n snoozeFile: path.join(this._opts.storageDir, `${hash}.snooze.bin`),\n filesDirFile: path.join(this._opts.storageDir, `${hash}.filesdir.bin`),\n keystoreSecret: this._opts.keystoreSecret,\n ensureDir: () => this._ensureDir(),\n });\n this._slots.set(id, existing);\n return existing;\n }\n\n private async _ensureDir(): Promise<void> {\n if (this._dirReady) return;\n await fs.mkdir(this._opts.storageDir, { recursive: true, mode: 0o700 });\n this._dirReady = true;\n }\n}\n\ninterface _SlotDeps {\n readonly id: SlotId;\n readonly identityFile: string;\n readonly storageFile: string;\n readonly keysFile: string;\n readonly snoozeFile: string;\n readonly filesDirFile: string;\n readonly keystoreSecret: Uint8Array;\n readonly ensureDir: () => Promise<void>;\n}\n\nclass _IdentitySlotImpl implements IdentitySlot {\n private _identity: Identity | undefined;\n private _identityLoaded = false;\n private readonly _storage: _FileBackedStorage;\n\n private _keys: SlotAccessKey[] | undefined;\n private _keysFileExists = false;\n private _keysLoaded = false;\n private _keysWriteChain: Promise<void> = Promise.resolve();\n\n constructor(private readonly _deps: _SlotDeps) {\n this._storage = new _FileBackedStorage({\n slot: _deps.id,\n file: _deps.storageFile,\n keystoreSecret: _deps.keystoreSecret,\n ensureDir: _deps.ensureDir,\n });\n }\n\n public get id(): SlotId {\n return this._deps.id;\n }\n\n public get storage(): ManagedIdentityStorageBackend {\n return this._storage;\n }\n\n // ---- identity -------------------------------------------------------\n\n public async getOrCreateIdentity(): Promise<Identity> {\n const existing = await this.peekIdentity();\n if (existing) return existing;\n\n const ed = await crypto.generateKeypair();\n const wrap = await crypto.generateX25519Keypair();\n await this._writeIdentity(ed, wrap);\n const identity = new InMemoryManagedIdentity(ed, wrap);\n this._identity = identity;\n this._identityLoaded = true;\n return identity;\n }\n\n public async peekIdentity(): Promise<Identity | undefined> {\n if (this._identityLoaded) return this._identity;\n\n let raw: Buffer;\n try {\n raw = await fs.readFile(this._deps.identityFile);\n } catch (e: unknown) {\n const err = e as NodeJS.ErrnoException;\n if (err.code === \"ENOENT\") {\n this._identityLoaded = true;\n this._identity = undefined;\n return undefined;\n }\n throw e;\n }\n const plain = await this._decryptIdentityPayload(raw);\n const ed: Keypair = {\n privateKey: base64UrlToBytes(plain.ed25519.privateKey),\n publicKey: base64UrlToBytes(plain.ed25519.publicKey),\n };\n const wrap: X25519Keypair = {\n privateKey: base64UrlToBytes(plain.x25519.privateKey),\n publicKey: base64UrlToBytes(plain.x25519.publicKey),\n };\n const identity = new InMemoryManagedIdentity(ed, wrap);\n this._identity = identity;\n this._identityLoaded = true;\n return identity;\n }\n\n public async hasState(): Promise<boolean> {\n if (await this.peekIdentity()) return true;\n const keys = await this._storage.list();\n if (keys.length > 0) return true;\n return this.hasFiles();\n }\n\n // ---- access keys ----------------------------------------------------\n\n public async listKeys(): Promise<SlotAccessKey[]> {\n const keys = await this._loadKeys();\n return keys.map((k) => ({ ...k }));\n }\n\n public async hasKey(key: SlotAccessKey): Promise<boolean> {\n const keys = await this._loadKeys();\n const canon = _canonicalKey(key);\n return keys.some((k) => _canonicalKey(k) === canon);\n }\n\n public async addKeys(...keys: SlotAccessKey[]): Promise<void> {\n if (keys.length === 0) return;\n await this._mutateKeys((current) => {\n const seen = new Set(current.map((k) => _canonicalKey(k)));\n for (const key of keys) {\n const canon = _canonicalKey(key);\n if (!seen.has(canon)) {\n seen.add(canon);\n current.push({ ...key });\n }\n }\n });\n }\n\n public async deleteKey(key: SlotAccessKey): Promise<boolean> {\n const canon = _canonicalKey(key);\n let existed = false;\n await this._mutateKeys((current) => {\n const idx = current.findIndex((k) => _canonicalKey(k) === canon);\n if (idx >= 0) {\n existed = true;\n current.splice(idx, 1);\n }\n });\n return existed;\n }\n\n /** Internal: does this slot exist (i.e. has it ever been created)? */\n public async _exists(): Promise<boolean> {\n await this._loadKeys();\n return this._keysFileExists;\n }\n\n // ---- consent snooze -------------------------------------------------\n\n public async getSnooze(): Promise<IdentitySnooze | undefined> {\n let raw: Buffer;\n try {\n raw = await fs.readFile(this._deps.snoozeFile);\n } catch (e: unknown) {\n const err = e as NodeJS.ErrnoException;\n if (err.code === \"ENOENT\") return undefined;\n throw e;\n }\n const obj = await _aesDecrypt<SnoozePayload>(\n this._deps.keystoreSecret, _SNOOZE_DOMAIN, this._deps.id, raw,\n );\n if (obj.schemaVersion !== _FILE_SCHEMA_VERSION) {\n throw new Error(`identity snooze: unsupported schemaVersion ${obj.schemaVersion}`);\n }\n if (obj.slot !== this._deps.id) {\n throw new Error(\"identity snooze: slot mismatch\");\n }\n if (Date.now() >= obj.expiresAt) return undefined;\n return { scope: obj.scope, expiresAt: obj.expiresAt };\n }\n\n public async setSnooze(snooze: IdentitySnooze): Promise<void> {\n await this._deps.ensureDir();\n const payload: SnoozePayload = {\n schemaVersion: _FILE_SCHEMA_VERSION,\n slot: this._deps.id,\n scope: snooze.scope,\n expiresAt: snooze.expiresAt,\n };\n const ct = await _aesEncrypt(\n this._deps.keystoreSecret, _SNOOZE_DOMAIN, this._deps.id, payload,\n );\n await fs.writeFile(this._deps.snoozeFile, ct, { mode: 0o600 });\n }\n\n public async clearSnooze(): Promise<void> {\n await _safeUnlink(this._deps.snoozeFile);\n }\n\n // ---- associated plaintext file folder -------------------------------\n\n public async getFilesDir(): Promise<string | undefined> {\n let raw: Buffer;\n try {\n raw = await fs.readFile(this._deps.filesDirFile);\n } catch (e: unknown) {\n const err = e as NodeJS.ErrnoException;\n if (err.code === \"ENOENT\") return undefined;\n throw e;\n }\n const obj = await _aesDecrypt<FilesDirPayload>(\n this._deps.keystoreSecret, _FILESDIR_DOMAIN, this._deps.id, raw,\n );\n if (obj.schemaVersion !== _FILE_SCHEMA_VERSION) {\n throw new Error(`identity filesdir: unsupported schemaVersion ${obj.schemaVersion}`);\n }\n if (obj.slot !== this._deps.id) {\n throw new Error(\"identity filesdir: slot mismatch\");\n }\n return obj.dir;\n }\n\n public async setFilesDir(absPath: string): Promise<void> {\n await this._deps.ensureDir();\n const payload: FilesDirPayload = {\n schemaVersion: _FILE_SCHEMA_VERSION,\n slot: this._deps.id,\n dir: absPath,\n updatedAt: Date.now(),\n };\n const ct = await _aesEncrypt(\n this._deps.keystoreSecret, _FILESDIR_DOMAIN, this._deps.id, payload,\n );\n await fs.writeFile(this._deps.filesDirFile, ct, { mode: 0o600 });\n }\n\n public async hasFiles(): Promise<boolean> {\n const dir = await this.getFilesDir();\n if (dir === undefined) return false;\n try {\n const entries = await fs.readdir(dir);\n return entries.length > 0;\n } catch (e: unknown) {\n const err = e as NodeJS.ErrnoException;\n if (err.code === \"ENOENT\") return false;\n throw e;\n }\n }\n\n public async wipeFiles(): Promise<void> {\n const dir = await this.getFilesDir();\n if (dir === undefined) return;\n let entries: string[];\n try {\n entries = await fs.readdir(dir);\n } catch (e: unknown) {\n const err = e as NodeJS.ErrnoException;\n if (err.code === \"ENOENT\") return;\n throw e;\n }\n await Promise.all(\n entries.map((name) =>\n fs.rm(path.join(dir, name), { recursive: true, force: true }),\n ),\n );\n }\n\n // ---- lifecycle ------------------------------------------------------\n\n public async delete(): Promise<void> {\n this._identity = undefined;\n this._identityLoaded = false;\n this._keys = undefined;\n this._keysLoaded = false;\n this._keysFileExists = false;\n // Invalidate any in-flight storage buffer so a racing `set` after\n // `delete` writes against a clean baseline.\n this._storage._invalidate();\n // Wipe the plaintext files folder (if any) entirely before dropping\n // its record, so \"Reset Identity\" leaves nothing on disk.\n const filesDir = await this.getFilesDir().catch(() => undefined);\n if (filesDir !== undefined) {\n await fs.rm(filesDir, { recursive: true, force: true });\n }\n await _safeUnlink(this._deps.identityFile);\n await _safeUnlink(this._deps.storageFile);\n await _safeUnlink(this._deps.keysFile);\n await _safeUnlink(this._deps.snoozeFile);\n await _safeUnlink(this._deps.filesDirFile);\n }\n\n // ---- identity IO ----------------------------------------------------\n\n private async _writeIdentity(ed: Keypair, wrap: X25519Keypair): Promise<void> {\n await this._deps.ensureDir();\n const payload: PlainPayload = {\n schemaVersion: _FILE_SCHEMA_VERSION,\n slot: this._deps.id,\n ed25519: {\n privateKey: bytesToBase64Url(ed.privateKey),\n publicKey: bytesToBase64Url(ed.publicKey),\n },\n x25519: {\n privateKey: bytesToBase64Url(wrap.privateKey),\n publicKey: bytesToBase64Url(wrap.publicKey),\n },\n createdAt: Date.now(),\n };\n const ct = await _aesEncrypt(this._deps.keystoreSecret, _DOMAIN, this._deps.id, payload);\n await fs.writeFile(this._deps.identityFile, ct, { mode: 0o600 });\n }\n\n private async _decryptIdentityPayload(raw: Buffer): Promise<PlainPayload> {\n const obj = await _aesDecrypt<PlainPayload>(\n this._deps.keystoreSecret, _DOMAIN, this._deps.id, raw,\n );\n if (obj.schemaVersion !== _FILE_SCHEMA_VERSION) {\n throw new Error(`identity keystore: unsupported schemaVersion ${obj.schemaVersion}`);\n }\n if (obj.slot !== this._deps.id) {\n // AAD binding makes this unreachable unless the AAD constant\n // drifts — keep the check as a defence-in-depth.\n throw new Error(\"identity keystore: slot mismatch\");\n }\n return obj;\n }\n\n // ---- keys IO --------------------------------------------------------\n\n private async _loadKeys(): Promise<SlotAccessKey[]> {\n if (this._keysLoaded) return this._keys!;\n let raw: Buffer;\n try {\n raw = await fs.readFile(this._deps.keysFile);\n } catch (e: unknown) {\n const err = e as NodeJS.ErrnoException;\n if (err.code === \"ENOENT\") {\n this._keys = [];\n this._keysFileExists = false;\n this._keysLoaded = true;\n return this._keys;\n }\n throw e;\n }\n const obj = await _aesDecrypt<KeysPayload>(\n this._deps.keystoreSecret, _KEYS_DOMAIN, this._deps.id, raw,\n );\n if (obj.schemaVersion !== _FILE_SCHEMA_VERSION) {\n throw new Error(`identity keys: unsupported schemaVersion ${obj.schemaVersion}`);\n }\n if (obj.slot !== this._deps.id) {\n throw new Error(\"identity keys: slot mismatch\");\n }\n this._keys = obj.keys.map((k) => ({ ...k }));\n this._keysFileExists = true;\n this._keysLoaded = true;\n return this._keys;\n }\n\n private async _mutateKeys(fn: (keys: SlotAccessKey[]) => void): Promise<void> {\n const next = this._keysWriteChain.then(async () => {\n const keys = await this._loadKeys();\n fn(keys);\n await this._persistKeys(keys);\n });\n // Suppress unhandled rejection on the chain — callers see the\n // original promise.\n this._keysWriteChain = next.catch(() => { });\n return next;\n }\n\n private async _persistKeys(keys: SlotAccessKey[]): Promise<void> {\n await this._deps.ensureDir();\n const payload: KeysPayload = {\n schemaVersion: _FILE_SCHEMA_VERSION,\n slot: this._deps.id,\n keys: keys.map((k) => ({ ...k })),\n updatedAt: Date.now(),\n };\n const ct = await _aesEncrypt(this._deps.keystoreSecret, _KEYS_DOMAIN, this._deps.id, payload);\n await fs.writeFile(this._deps.keysFile, ct, { mode: 0o600 });\n this._keysFileExists = true;\n }\n}\n\n/** Canonical (sorted-field) JSON for exact access-key equality. */\nfunction _canonicalKey(key: SlotAccessKey): string {\n const entries = Object.keys(key)\n .sort()\n .map((k) => [k, key[k]] as const);\n return JSON.stringify(entries);\n}\n\n/**\n * File-backed per-slot storage. Lazily reads the encrypted file on first\n * access and caches the decrypted map in memory. Writes serialize\n * through `_writeChain` so concurrent `set`/`delete` calls don't\n * overwrite each other.\n */\nclass _FileBackedStorage implements ManagedIdentityStorageBackend {\n private _data: Record<string, unknown> | undefined;\n private _loaded = false;\n private _writeChain: Promise<void> = Promise.resolve();\n\n constructor(\n private readonly _opts: {\n readonly slot: string;\n readonly file: string;\n readonly keystoreSecret: Uint8Array;\n readonly ensureDir: () => Promise<void>;\n },\n ) { }\n\n /**\n * Drop the cached map (called by the slot's `delete()` so the next\n * `get` re-reads from disk, where the file is now gone).\n */\n public _invalidate(): void {\n this._data = undefined;\n this._loaded = false;\n }\n\n public async get(key: string): Promise<unknown | undefined> {\n const data = await this._load();\n return Object.prototype.hasOwnProperty.call(data, key) ? data[key] : undefined;\n }\n\n public async set(key: string, value: unknown): Promise<void> {\n await this._mutate((data) => {\n data[key] = value;\n });\n }\n\n public async delete(key: string): Promise<boolean> {\n let existed = false;\n await this._mutate((data) => {\n existed = Object.prototype.hasOwnProperty.call(data, key);\n if (existed) delete data[key];\n });\n return existed;\n }\n\n public async list(prefix?: string): Promise<string[]> {\n const data = await this._load();\n const keys = Object.keys(data);\n return prefix === undefined ? keys : keys.filter((k) => k.startsWith(prefix));\n }\n\n private async _load(): Promise<Record<string, unknown>> {\n if (this._loaded) return this._data!;\n let raw: Buffer;\n try {\n raw = await fs.readFile(this._opts.file);\n } catch (e: unknown) {\n const err = e as NodeJS.ErrnoException;\n if (err.code === \"ENOENT\") {\n this._data = {};\n this._loaded = true;\n return this._data;\n }\n throw e;\n }\n const obj = await _aesDecrypt<StoragePayload>(\n this._opts.keystoreSecret, _STORAGE_DOMAIN, this._opts.slot, raw,\n );\n if (obj.schemaVersion !== _FILE_SCHEMA_VERSION) {\n throw new Error(`identity storage: unsupported schemaVersion ${obj.schemaVersion}`);\n }\n if (obj.slot !== this._opts.slot) {\n throw new Error(\"identity storage: slot mismatch\");\n }\n this._data = { ...obj.entries };\n this._loaded = true;\n return this._data;\n }\n\n private async _mutate(fn: (data: Record<string, unknown>) => void): Promise<void> {\n const next = this._writeChain.then(async () => {\n const data = await this._load();\n fn(data);\n await this._persist(data);\n });\n // Suppress unhandled rejection on the chain — callers see the\n // original promise.\n this._writeChain = next.catch(() => { });\n return next;\n }\n\n private async _persist(data: Record<string, unknown>): Promise<void> {\n await this._opts.ensureDir();\n const payload: StoragePayload = {\n schemaVersion: _FILE_SCHEMA_VERSION,\n slot: this._opts.slot,\n entries: data,\n updatedAt: Date.now(),\n };\n const ct = await _aesEncrypt(\n this._opts.keystoreSecret, _STORAGE_DOMAIN, this._opts.slot, payload,\n );\n await fs.writeFile(this._opts.file, ct, { mode: 0o600 });\n }\n}\n\n// ---- shared AES helpers --------------------------------------------------\n\nasync function _aesEncrypt(\n secret: Uint8Array,\n domain: string,\n slot: string,\n payload: unknown,\n): Promise<Uint8Array> {\n const pt = new TextEncoder().encode(JSON.stringify(payload));\n const { createCipheriv, randomBytes } = await import(\"node:crypto\");\n const key = await _deriveKey(secret, domain);\n const iv = randomBytes(12);\n const cipher = createCipheriv(\"aes-256-gcm\", key, iv);\n cipher.setAAD(new TextEncoder().encode(`${domain}:${slot}`));\n const enc = Buffer.concat([cipher.update(pt), cipher.final()]);\n const tag = cipher.getAuthTag();\n // Layout: iv (12) || tag (16) || ciphertext\n const out = new Uint8Array(12 + 16 + enc.length);\n out.set(iv, 0);\n out.set(tag, 12);\n out.set(enc, 28);\n return out;\n}\n\nasync function _aesDecrypt<T>(\n secret: Uint8Array,\n domain: string,\n slot: string,\n raw: Buffer,\n): Promise<T> {\n if (raw.length < 12 + 16) throw new Error(`${domain}: file too short`);\n const iv = raw.subarray(0, 12);\n const tag = raw.subarray(12, 28);\n const ct = raw.subarray(28);\n const { createDecipheriv } = await import(\"node:crypto\");\n const key = await _deriveKey(secret, domain);\n const decipher = createDecipheriv(\"aes-256-gcm\", key, iv);\n decipher.setAAD(new TextEncoder().encode(`${domain}:${slot}`));\n decipher.setAuthTag(tag);\n const pt = Buffer.concat([decipher.update(ct), decipher.final()]);\n return JSON.parse(pt.toString(\"utf8\")) as T;\n}\n\nasync function _deriveKey(secret: Uint8Array, domain: string): Promise<Buffer> {\n const { createHmac } = await import(\"node:crypto\");\n // HKDF-Extract with empty salt (the secret is already random 32+\n // bytes) then HKDF-Expand with our domain label, 32-byte output.\n const prk = createHmac(\"sha256\", Buffer.alloc(32)).update(secret).digest();\n const info = Buffer.concat([\n Buffer.from(domain),\n Buffer.from([1]), // T(1)\n ]);\n return createHmac(\"sha256\", prk).update(info).digest();\n}\n\nasync function _safeUnlink(file: string): Promise<void> {\n try {\n await fs.unlink(file);\n } catch (e: unknown) {\n const err = e as NodeJS.ErrnoException;\n if (err.code !== \"ENOENT\") throw e;\n }\n}\n"],"mappings":"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"}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { printHubConfigSchema } from "./configFile.js";
|
|
3
|
+
import { serveHub } from "./serve.js";
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
//#region src/cli.ts
|
|
6
|
+
/**
|
|
7
|
+
* `linkrpc-hub` — run a hub from a declarative config file. This is the standalone
|
|
8
|
+
* server face of the shared hub engine; the same {@link HubConfig} format drives
|
|
9
|
+
* the CLI's `serve`/`-c` and the VS Code extension.
|
|
10
|
+
*/
|
|
11
|
+
async function main(argv) {
|
|
12
|
+
const program = new Command();
|
|
13
|
+
program.name("linkrpc-hub").description("Run a linkrpc hub from a declarative config file. The config format is shared with the `hub`/`linkrpc` CLI (`serve`, `-c`) and the VS Code extension.").argument("[config]", "JSON config file (see --print-schema)").option("--print-schema", "print the config JSON Schema and exit", false).option("--cmd-interactive", "forward stdin to the single cmd endpoint (errors if more than one)", false).showHelpAfterError().action(async (config, opts) => {
|
|
14
|
+
if (opts.printSchema) {
|
|
15
|
+
process.stdout.write(printHubConfigSchema() + "\n");
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (config === void 0) program.error("a <config> file is required (or pass --print-schema)");
|
|
19
|
+
await serveHub({
|
|
20
|
+
configPath: config,
|
|
21
|
+
cmdInteractive: opts.cmdInteractive,
|
|
22
|
+
log: (line) => console.log(`[linkrpc-hub] ${line}`)
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
await program.parseAsync([
|
|
26
|
+
"node",
|
|
27
|
+
"linkrpc-hub",
|
|
28
|
+
...argv
|
|
29
|
+
]);
|
|
30
|
+
}
|
|
31
|
+
main(process.argv.slice(2)).catch((err) => {
|
|
32
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
33
|
+
process.exit(1);
|
|
34
|
+
});
|
|
35
|
+
//#endregion
|
|
36
|
+
export {};
|
|
37
|
+
|
|
38
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","names":[],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * `linkrpc-hub` — run a hub from a declarative config file. This is the standalone\n * server face of the shared hub engine; the same {@link HubConfig} format drives\n * the CLI's `serve`/`-c` and the VS Code extension.\n */\nimport { Command } from 'commander';\nimport { printHubConfigSchema, serveHub } from './serve';\n\nasync function main(argv: readonly string[]): Promise<void> {\n const program = new Command();\n program\n .name('linkrpc-hub')\n .description(\n 'Run a linkrpc hub from a declarative config file. The config format is shared with '\n + 'the `hub`/`linkrpc` CLI (`serve`, `-c`) and the VS Code extension.',\n )\n .argument('[config]', 'JSON config file (see --print-schema)')\n .option('--print-schema', 'print the config JSON Schema and exit', false)\n .option('--cmd-interactive', 'forward stdin to the single cmd endpoint (errors if more than one)', false)\n .showHelpAfterError()\n .action(async (config: string | undefined, opts: { printSchema: boolean; cmdInteractive: boolean; }) => {\n if (opts.printSchema) {\n process.stdout.write(printHubConfigSchema() + '\\n');\n return;\n }\n if (config === undefined) {\n program.error('a <config> file is required (or pass --print-schema)');\n }\n await serveHub({\n configPath: config!,\n cmdInteractive: opts.cmdInteractive,\n log: (line) => console.log(`[linkrpc-hub] ${line}`),\n });\n });\n\n await program.parseAsync(['node', 'linkrpc-hub', ...argv]);\n}\n\nmain(process.argv.slice(2)).catch((err: unknown) => {\n console.error(err instanceof Error ? err.message : String(err));\n process.exit(1);\n});\n"],"mappings":";;;;;;;;;;AASA,eAAe,KAAK,MAAwC;CACxD,MAAM,UAAU,IAAI,QAAQ;CAC5B,QACK,KAAK,aAAa,CAAC,CACnB,YACG,uJAEJ,CAAC,CACA,SAAS,YAAY,uCAAuC,CAAC,CAC7D,OAAO,kBAAkB,yCAAyC,KAAK,CAAC,CACxE,OAAO,qBAAqB,sEAAsE,KAAK,CAAC,CACxG,mBAAmB,CAAC,CACpB,OAAO,OAAO,QAA4B,SAA6D;EACpG,IAAI,KAAK,aAAa;GAClB,QAAQ,OAAO,MAAM,qBAAqB,IAAI,IAAI;GAClD;EACJ;EACA,IAAI,WAAW,KAAA,GACX,QAAQ,MAAM,sDAAsD;EAExE,MAAM,SAAS;GACX,YAAY;GACZ,gBAAgB,KAAK;GACrB,MAAM,SAAS,QAAQ,IAAI,iBAAiB,MAAM;EACtD,CAAC;CACL,CAAC;CAEL,MAAM,QAAQ,WAAW;EAAC;EAAQ;EAAe,GAAG;CAAI,CAAC;AAC7D;AAEA,KAAK,QAAQ,KAAK,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,QAAiB;CAChD,QAAQ,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;CAC9D,QAAQ,KAAK,CAAC;AAClB,CAAC"}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { _ as ProvisionConfig, a as ConsentApproverConfig, c as EndpointConfigSchema, d as HubConfig, f as HubConfigSchema, g as ParticipantConnectorConfigSchema, h as ParticipantConnectorConfig, i as ConnectionTokenBinderSchema, l as ForwardCheckingConfig, m as ListenerConfigSchema, n as ConnectionHandlerSchema, o as ConsentApproverSchema, p as ListenerConfig, r as ConnectionTokenBinderConfig, s as EndpointConfig, t as ConnectionHandlerConfig, u as ForwardCheckingSchema, v as hubConfigJsonSchema, y as parseHubConfig } from "./chunks/config-BCvkg7jv.js";
|
|
2
|
+
export { ConnectionHandlerConfig, ConnectionHandlerSchema, ConnectionTokenBinderConfig, ConnectionTokenBinderSchema, ConsentApproverConfig, ConsentApproverSchema, EndpointConfig, EndpointConfigSchema, ForwardCheckingConfig, ForwardCheckingSchema, HubConfig, HubConfigSchema, ListenerConfig, ListenerConfigSchema, ParticipantConnectorConfig, ParticipantConnectorConfigSchema, ProvisionConfig, hubConfigJsonSchema, parseHubConfig };
|