@fabricorg/experience-gateway 2.1.6 → 2.1.7

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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../index.ts"],"sourcesContent":["import type {\n\tGovernedActionHost,\n\tSubmitActionInput,\n\tSubmitActionResult,\n} from \"@fabricorg/platform-host\";\nimport { digestParameters } from \"@fabricorg/platform-host\";\nimport type { ProjectionHost } from \"@fabricorg/projection-host\";\nimport {\n\ttype ActorClaims,\n\tassertActorClaimsCoverScope,\n\ttype IdentityPort,\n} from \"@fabricorg/ports\";\nimport type { ExperienceIntentRequest, ExperienceReadRequest } from \"@fabricorg/experience-runtime\";\nimport type {\n\tExperienceScope,\n\tExperienceServerRuntime,\n} from \"@fabricorg/experience-runtime/server\";\n\nexport interface TrustedExperienceRequestContext {\n\t/** Presented only to IdentityPort and never persisted or forwarded. */\n\tcredential: string;\n\ttenantId: string;\n\tspaceId: string;\n}\n\nexport interface ExperienceGateway {\n\tinvoke(input: {\n\t\tcontext: TrustedExperienceRequestContext;\n\t\trequest: ExperienceIntentRequest;\n\t\texecutionReason?: \"initial\" | \"offline_replay\";\n\t\t/**\n\t\t * When the operator acted, for a replay. Required whenever\n\t\t * `executionReason` is `offline_replay`: reading it through an untyped\n\t\t * cast made omitting it the easy mistake, and omitting it silently\n\t\t * stamped receipt time into durable capture evidence.\n\t\t */\n\t\tcapturedAt?: string;\n\t}): Promise<SubmitActionResult>;\n\tread(input: {\n\t\tcontext: TrustedExperienceRequestContext;\n\t\trequest: ExperienceReadRequest;\n\t}): Promise<{\n\t\tdata: unknown;\n\t\tevidence: {\n\t\t\treleaseDigest: string;\n\t\t\tassemblyDigest: string;\n\t\t\teventCursor: string | null;\n\t\t\tsource: \"cache\" | \"replay\";\n\t\t};\n\t}>;\n}\n\nexport interface ExperienceAdmissionInput {\n\tkind: \"invoke\" | \"read\";\n\tscope: ExperienceScope;\n\tclaims: ActorClaims;\n\tplanId: string;\n\tfragmentId: string;\n\t/** The event or binding name the client named; the capability it resolves to is not yet known. */\n\tname: string;\n}\n\nexport type ExperienceAdmissionDecision =\n\t| { allowed: true }\n\t| { allowed: false; reason: string; retryAfterMs?: number };\n\n/** A request the edge would not admit. `retryable` is true when a later attempt may be admitted. */\nexport class ExperienceAdmissionError extends Error {\n\treadonly code = \"EXPERIENCE_ADMISSION_REFUSED\" as const;\n\tconstructor(readonly reason: string, readonly retryAfterMs?: number) {\n\t\tsuper(`Experience request not admitted: ${reason}`);\n\t\tthis.name = \"ExperienceAdmissionError\";\n\t}\n\tget retryable(): boolean {\n\t\treturn this.retryAfterMs !== undefined;\n\t}\n}\n\n/**\n * A reference admission policy: a token bucket per key, refilled over time.\n *\n * Enough to throttle a tenant or an actor at the edge; a deployment with a\n * shared limiter puts it behind the same option. Keys default to the tenant.\n */\nexport function createTokenBucketAdmission(options: {\n\tcapacity: number;\n\trefillPerSecond: number;\n\tkey?(input: ExperienceAdmissionInput): string;\n\tnow?(): number;\n}): (input: ExperienceAdmissionInput) => ExperienceAdmissionDecision {\n\tif (!(options.capacity >= 1) || !(options.refillPerSecond > 0)) throw new Error(\"Token bucket admission needs a capacity of at least one and a positive refill rate.\");\n\tconst now = options.now ?? (() => Date.now());\n\tconst key = options.key ?? ((input) => input.scope.tenantId);\n\tconst buckets = new Map<string, { tokens: number; at: number }>();\n\treturn (input) => {\n\t\tconst at = now();\n\t\tconst id = key(input);\n\t\tconst bucket = buckets.get(id) ?? { tokens: options.capacity, at };\n\t\t// A clock that steps backwards refills nothing; it must not debit.\n\t\tconst elapsedMs = Math.max(0, at - bucket.at);\n\t\tconst refilled = Math.min(options.capacity, bucket.tokens + (elapsedMs / 1000) * options.refillPerSecond);\n\t\tif (refilled >= 1) {\n\t\t\tconst remaining = refilled - 1;\n\t\t\t// A full bucket is the same as an absent one; forgetting it bounds the map to the active keys.\n\t\t\tif (remaining >= options.capacity) buckets.delete(id);\n\t\t\telse buckets.set(id, { tokens: remaining, at });\n\t\t\treturn { allowed: true };\n\t\t}\n\t\tbuckets.set(id, { tokens: refilled, at: Math.max(at, bucket.at) });\n\t\tconst retryAfterMs = Math.ceil(((1 - refilled) / options.refillPerSecond) * 1000);\n\t\treturn { allowed: false, reason: `budget for \"${id}\" is exhausted`, retryAfterMs };\n\t};\n}\n\nexport interface ExperienceGatewayConformanceSubject {\n\tgateway: ExperienceGateway;\n\tvalidIntent: Parameters<ExperienceGateway[\"invoke\"]>[0];\n\tdeniedIntent: Parameters<ExperienceGateway[\"invoke\"]>[0];\n\tvalidRead: Parameters<ExperienceGateway[\"read\"]>[0];\n\t/**\n\t * What the governed hosts actually saw.\n\t *\n\t * A returned invocation id proves only that a string was produced. The\n\t * point of a gateway is that mutations reach `PlatformHost.submitAction` and\n\t * reads reach `ProjectionHost.project`; a replacement that fabricates an id\n\t * and queries the database directly satisfies every observable contract\n\t * except this one, so the subject has to report the calls.\n\t */\n\tobserved: {\n\t\tsubmitActionCount(): number;\n\t\tprojectionCount(): number;\n\t};\n\t/**\n\t * An intent the deployment's admission policy refuses. Optional so an\n\t * existing subject still compiles; without it the admission check reports\n\t * `skipped` and says what was not proven, since a deployment that cannot\n\t * show one refusal has not configured admission.\n\t */\n\tthrottledIntent?: Parameters<ExperienceGateway[\"invoke\"]>[0];\n}\n\nexport interface ExperienceGatewayCheck {\n\tid:\n\t\t| \"fabric.experience.gateway-host-routing.v1\"\n\t\t| \"fabric.experience.gateway-scope-denial.v1\"\n\t\t| \"fabric.experience.gateway-projection-routing.v1\"\n\t\t| \"fabric.experience.gateway-admission-refusal.v1\";\n\trun(subject: ExperienceGatewayConformanceSubject): Promise<string[]>;\n}\n\nexport function experienceGatewayChecks(): readonly ExperienceGatewayCheck[] {\n\treturn [\n\t\t{\n\t\t\tid: \"fabric.experience.gateway-host-routing.v1\",\n\t\t\tasync run(subject) {\n\t\t\t\tconst before = subject.observed.submitActionCount();\n\t\t\t\tconst result = await subject.gateway.invoke(subject.validIntent);\n\t\t\t\tif (!result.actionInvocationId) throw new Error(\"Gateway did not return a governed Host invocation.\");\n\t\t\t\tconst submitted = subject.observed.submitActionCount() - before;\n\t\t\t\tif (submitted !== 1) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`Gateway returned an invocation id after ${submitted} calls to PlatformHost.submitAction; a mutation must reach the Host exactly once.`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\treturn [result.actionInvocationId];\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: \"fabric.experience.gateway-projection-routing.v1\",\n\t\t\tasync run(subject) {\n\t\t\t\tconst before = subject.observed.projectionCount();\n\t\t\t\tawait subject.gateway.read(subject.validRead);\n\t\t\t\tconst projected = subject.observed.projectionCount() - before;\n\t\t\t\tif (projected !== 1) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`Gateway served a read after ${projected} calls to ProjectionHost.project; a read must go through the projection host.`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\treturn [\"read routed through ProjectionHost\"];\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: \"fabric.experience.gateway-scope-denial.v1\",\n\t\t\tasync run(subject) {\n\t\t\t\tlet rejected = false;\n\t\t\t\ttry { await subject.gateway.invoke(subject.deniedIntent); } catch { rejected = true; }\n\t\t\t\tif (!rejected) throw new Error(\"Gateway accepted an intent outside trusted identity scope.\");\n\t\t\t\treturn [\"out-of-scope intent rejected\"];\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: \"fabric.experience.gateway-admission-refusal.v1\",\n\t\t\tasync run(subject) {\n\t\t\t\tif (!subject.throttledIntent) return [\"skipped: the subject supplies no throttledIntent, so admission is not proven\"];\n\t\t\t\tconst before = subject.observed.submitActionCount();\n\t\t\t\tlet refusal: unknown;\n\t\t\t\ttry { await subject.gateway.invoke(subject.throttledIntent); } catch (error) { refusal = error; }\n\t\t\t\tif (!(refusal instanceof ExperienceAdmissionError)) {\n\t\t\t\t\tthrow new Error(\"Gateway admitted an intent the deployment's admission policy refuses, or refused it as something other than ExperienceAdmissionError.\");\n\t\t\t\t}\n\t\t\t\tif (subject.observed.submitActionCount() !== before) {\n\t\t\t\t\tthrow new Error(\"A refused intent still reached PlatformHost.submitAction; admission must decide before anything governed is touched.\");\n\t\t\t\t}\n\t\t\t\treturn [`admission refused: ${refusal.reason}${refusal.retryable ? ` (retry after ${refusal.retryAfterMs}ms)` : \"\"}`];\n\t\t\t},\n\t\t},\n\t];\n}\n\nexport async function runExperienceGatewayChecks(\n\tsubject: ExperienceGatewayConformanceSubject,\n\tchecks: readonly ExperienceGatewayCheck[] = experienceGatewayChecks(),\n): Promise<{ passed: boolean; checks: Array<{ id: ExperienceGatewayCheck[\"id\"]; status: \"passed\" | \"failed\" | \"skipped\"; evidence: string[]; error?: string }> }> {\n\tconst results: Array<{ id: ExperienceGatewayCheck[\"id\"]; status: \"passed\" | \"failed\" | \"skipped\"; evidence: string[]; error?: string }> = [];\n\tfor (const check of checks) {\n\t\ttry {\n\t\t\tconst evidence = await check.run(subject);\n\t\t\tresults.push({ id: check.id, status: evidence[0]?.startsWith(\"skipped:\") ? \"skipped\" : \"passed\", evidence });\n\t\t}\n\t\tcatch (error) { results.push({ id: check.id, status: \"failed\", evidence: [], error: error instanceof Error ? error.message : String(error) }); }\n\t}\n\treturn { passed: results.every((result) => result.status !== \"failed\"), checks: results };\n}\n\nexport interface InitiatingReleaseEvidence {\n\tbind(input: SubmitActionInput, releaseDigest: string): void;\n\tresolve(input: SubmitActionInput): string | undefined;\n\trelease(input: SubmitActionInput): void;\n}\n\n/**\n * Creates an instance-scoped, non-serializable evidence seam. The Host's\n * `composition.resolveInitiatingReleaseDigest` reads this WeakMap using the\n * exact submission object. A browser field cannot populate or forge it.\n */\nexport function createInitiatingReleaseEvidence(): InitiatingReleaseEvidence {\n\tconst evidence = new WeakMap<SubmitActionInput, string>();\n\treturn {\n\t\tbind(input, releaseDigest) {\n\t\t\tevidence.set(input, releaseDigest);\n\t\t},\n\t\tresolve(input) {\n\t\t\treturn evidence.get(input);\n\t\t},\n\t\trelease(input) {\n\t\t\tevidence.delete(input);\n\t\t},\n\t};\n}\n\nexport function createExperienceGateway(options: {\n\tidentity: IdentityPort;\n\truntime: ExperienceServerRuntime;\n\thost: Pick<GovernedActionHost, \"submitAction\">;\n\tprojectionHost: Pick<ProjectionHost, \"project\">;\n\tevidence: InitiatingReleaseEvidence;\n\tassemblyDigest: string;\n\tactionVersion(actionId: `${string}.${string}`): number | undefined;\n\tnextCorrelationId(): string;\n\t/**\n\t * How old an offline capture may be and still replay.\n\t *\n\t * Required to accept replay at all. The Host's own staleness check is\n\t * conditional on the capture-authority adapter supplying an `expiresAt`, and\n\t * is skipped entirely for actions governed at capture time, so without this\n\t * a client-reported instant has no lower bound.\n\t */\n\tmaxOfflineReplayAgeMs?: number;\n\t/**\n\t * Admission control at the edge, decided after the credential is verified\n\t * and before anything governed is touched. Per-tenant request budgets,\n\t * render-cost quotas, and abuse throttles live here, as a seam the\n\t * deployment fills; the gateway itself imposes no policy. A refusal\n\t * surfaces as `ExperienceAdmissionError`, carrying `retryAfterMs` when the\n\t * caller should come back rather than give up.\n\t */\n\tadmission?(input: ExperienceAdmissionInput): Promise<ExperienceAdmissionDecision> | ExperienceAdmissionDecision;\n\tcaptureAuthority?(input: {\n\t\tactionId: `${string}.${string}`;\n\t\tscope: ExperienceScope;\n\t\tparameters: Record<string, unknown>;\n\t\tparameterDigest: string;\n\t\tcapturedAt: string;\n\t}): Promise<{ id: string; expiresAt?: string }>;\n\tnow?: () => Date;\n}): ExperienceGateway {\n\tconst now = options.now ?? (() => new Date());\n\treturn {\n\t\tasync invoke(input) {\n\t\t\tconst claims = await options.identity.verify(input.context.credential);\n\t\t\tif (!claims) throw new Error(\"Experience request credential is not authenticated.\");\n\t\t\tassertActorClaimsCoverScope(claims, {\n\t\t\t\ttenantId: input.context.tenantId,\n\t\t\t\tspaceId: input.context.spaceId,\n\t\t\t}, now());\n\t\t\tconst scope: ExperienceScope = {\n\t\t\t\ttenantId: input.context.tenantId,\n\t\t\t\tspaceId: input.context.spaceId,\n\t\t\t\tactorId: claims.subject,\n\t\t\t\tactorType: claims.actorType,\n\t\t\t};\n\t\t\tawait admit(options.admission, { kind: \"invoke\", scope, claims, planId: input.request.planId, fragmentId: input.request.fragmentId, name: input.request.eventName });\n\t\t\tconst resolved = await options.runtime.resolveIntent({\n\t\t\t\tplanId: input.request.planId,\n\t\t\t\tfragmentId: input.request.fragmentId,\n\t\t\t\teventName: input.request.eventName,\n\t\t\t\tscope,\n\t\t\t});\n\t\t\tif (resolved.assemblyDigest !== options.assemblyDigest) {\n\t\t\t\tthrow new Error(\"Experience render plan belongs to a different application assembly.\");\n\t\t\t}\n\t\t\tif (options.actionVersion(resolved.actionId) !== resolved.actionVersion) {\n\t\t\t\tthrow new Error(`Experience intent route for \"${resolved.actionId}\" does not match the registered action version.`);\n\t\t\t}\n\t\t\tif (resolved.execution?.idempotency === \"required\" && !input.request.idempotencyKey) {\n\t\t\t\tthrow new Error(`Action \"${resolved.actionId}\" requires an idempotency key.`);\n\t\t\t}\n\t\t\tif (input.executionReason === \"offline_replay\" && resolved.execution?.connectivity !== \"offline-capturable\") {\n\t\t\t\tthrow new Error(`Action \"${resolved.actionId}\" is not eligible for offline replay.`);\n\t\t\t}\n\t\t\tconst correlationId = options.nextCorrelationId();\n\t\t\t// A replay attests when the operator acted, not when the network came\n\t\t\t// back. Stamping receipt time would make capture-authority evidence\n\t\t\t// describe a moment nobody was there for.\n\t\t\tlet capturedAt: string;\n\t\t\tif (input.executionReason === \"offline_replay\") {\n\t\t\t\t// Accept it from either position, but require it: a replay with no\n\t\t\t\t// capture moment cannot be evidenced, and defaulting to receipt\n\t\t\t\t// time would attest a moment nobody was there for.\n\t\t\t\tconst supplied = input.capturedAt\n\t\t\t\t\t?? (input.request as Partial<{ capturedAt: string }>).capturedAt;\n\t\t\t\tif (typeof supplied !== \"string\") {\n\t\t\t\t\tthrow new Error(\"Offline replay requires capturedAt; receipt time cannot attest when the operator acted.\");\n\t\t\t\t}\n\t\t\t\tcapturedAt = assertCaptureMoment(supplied, now(), options.maxOfflineReplayAgeMs);\n\t\t\t} else {\n\t\t\t\tcapturedAt = now().toISOString();\n\t\t\t}\n\t\t\tconst parameterDigest = digestParameters(input.request.parameters);\n\t\t\tconst capturedAuthority = resolved.execution?.authorityMoment === \"capture\"\n\t\t\t\t? await options.captureAuthority?.({\n\t\t\t\t\tactionId: resolved.actionId,\n\t\t\t\t\tscope,\n\t\t\t\t\tparameters: input.request.parameters,\n\t\t\t\t\tparameterDigest,\n\t\t\t\t\tcapturedAt,\n\t\t\t\t})\n\t\t\t\t: undefined;\n\t\t\tif (resolved.execution?.authorityMoment === \"capture\" && !capturedAuthority) {\n\t\t\t\tthrow new Error(`Action \"${resolved.actionId}\" requires a trusted capture-authority adapter.`);\n\t\t\t}\n\t\t\tconst submission: SubmitActionInput = {\n\t\t\t\tactionId: resolved.actionId,\n\t\t\t\tparameters: input.request.parameters,\n\t\t\t\ttenantId: scope.tenantId,\n\t\t\t\tspaceId: scope.spaceId,\n\t\t\t\tactorId: scope.actorId,\n\t\t\t\tactorType: claims.actorType,\n\t\t\t\tcorrelationId,\n\t\t\t\tprovenance: {\n\t\t\t\t\tsource: \"ui\",\n\t\t\t\t\tcorrelationId,\n\t\t\t\t\t...(resolved.execution?.sensitivity === \"restricted\"\n\t\t\t\t\t\t? {}\n\t\t\t\t\t\t: { auditAttributes: { \"fabric.experience.fragment\": input.request.fragmentId } }),\n\t\t\t\t},\n\t\t\t\t...(input.request.idempotencyKey === undefined ? {} : { idempotencyKey: input.request.idempotencyKey }),\n\t\t\t\t...(input.executionReason === undefined ? {} : { executionReason: input.executionReason }),\n\t\t\t\t...(capturedAuthority === undefined ? {} : {\n\t\t\t\t\tauthorizationBindingId: capturedAuthority.id,\n\t\t\t\t\tauthorizationBinding: {\n\t\t\t\t\t\tid: capturedAuthority.id,\n\t\t\t\t\t\tgoverningMoment: \"capture\",\n\t\t\t\t\t\tcapturedAt,\n\t\t\t\t\t\t...(capturedAuthority.expiresAt === undefined ? {} : { expiresAt: capturedAuthority.expiresAt }),\n\t\t\t\t\t\ttenantId: scope.tenantId,\n\t\t\t\t\t\tactorId: scope.actorId,\n\t\t\t\t\t\tactionId: resolved.actionId,\n\t\t\t\t\t\tparameterDigest,\n\t\t\t\t\t},\n\t\t\t\t}),\n\t\t\t};\n\t\t\toptions.evidence.bind(submission, resolved.initiatingReleaseDigest);\n\t\t\ttry {\n\t\t\t\treturn await options.host.submitAction(submission);\n\t\t\t} finally {\n\t\t\t\toptions.evidence.release(submission);\n\t\t\t}\n\t\t},\n\t\tasync read(input) {\n\t\t\tconst claims = await options.identity.verify(input.context.credential);\n\t\t\tif (!claims) throw new Error(\"Experience request credential is not authenticated.\");\n\t\t\tassertActorClaimsCoverScope(claims, {\n\t\t\t\ttenantId: input.context.tenantId,\n\t\t\t\tspaceId: input.context.spaceId,\n\t\t\t}, now());\n\t\t\tconst scope: ExperienceScope = {\n\t\t\t\ttenantId: input.context.tenantId,\n\t\t\t\tspaceId: input.context.spaceId,\n\t\t\t\tactorId: claims.subject,\n\t\t\t\tactorType: claims.actorType,\n\t\t\t};\n\t\t\tawait admit(options.admission, { kind: \"read\", scope, claims, planId: input.request.planId, fragmentId: input.request.fragmentId, name: input.request.bindingName });\n\t\t\tconst resolved = await options.runtime.resolveView({\n\t\t\t\tplanId: input.request.planId,\n\t\t\t\tfragmentId: input.request.fragmentId,\n\t\t\t\tbindingName: input.request.bindingName,\n\t\t\t\tscope,\n\t\t\t});\n\t\t\tif (resolved.assemblyDigest !== options.assemblyDigest) {\n\t\t\t\tthrow new Error(\"Experience render plan belongs to a different application assembly.\");\n\t\t\t}\n\t\t\tconst result = await options.projectionHost.project({\n\t\t\t\tview: { name: resolved.name, version: resolved.version },\n\t\t\t\tscope: { tenantId: scope.tenantId, spaceId: scope.spaceId },\n\t\t\t\tactor: { id: scope.actorId, type: claims.actorType },\n\t\t\t\tparameters: input.request.parameters,\n\t\t\t});\n\t\t\treturn {\n\t\t\t\tdata: result.state,\n\t\t\t\tevidence: {\n\t\t\t\t\treleaseDigest: resolved.initiatingReleaseDigest,\n\t\t\t\t\tassemblyDigest: resolved.assemblyDigest,\n\t\t\t\t\teventCursor: result.eventCursor,\n\t\t\t\t\tsource: result.source,\n\t\t\t\t},\n\t\t\t};\n\t\t},\n\t};\n}\n\n/** RFC 3339 with a mandatory offset, so an instant means the same thing everywhere. */\nconst RFC3339 = /^\\d{4}-\\d{2}-\\d{2}[Tt]\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:[Zz]|[+-]\\d{2}:\\d{2})$/;\n\n/**\n * A supplied capture moment is client-reported, so it is bounded here rather\n * than trusted.\n *\n * Deferring the lower bound to the Host looked safe and is not: the Host checks\n * staleness only when the capture-authority adapter supplied an `expiresAt`,\n * and skips the execution-time expiry check entirely when the action's\n * authority moment is `capture`. With an adapter that omits `expiresAt` there\n * is no lower bound at all, and the value is written into durable evidence and\n * handed to the authority adapter regardless. A gateway that accepts replay\n * must therefore say how old a capture may be.\n */\nasync function admit(\n\tadmission: ((input: ExperienceAdmissionInput) => Promise<ExperienceAdmissionDecision> | ExperienceAdmissionDecision) | undefined,\n\tinput: ExperienceAdmissionInput,\n): Promise<void> {\n\tif (!admission) return;\n\tconst decision = await admission(input);\n\tif (!decision.allowed) throw new ExperienceAdmissionError(decision.reason, decision.retryAfterMs);\n}\n\nfunction assertCaptureMoment(capturedAt: string, now: Date, maxAgeMs: number | undefined): string {\n\tif (!RFC3339.test(capturedAt)) {\n\t\tthrow new Error(\"Offline replay capturedAt must be an RFC 3339 instant with an explicit offset.\");\n\t}\n\tconst parsed = Date.parse(capturedAt);\n\tif (Number.isNaN(parsed)) throw new Error(\"Offline replay capturedAt is not a parseable instant.\");\n\tif (parsed > now.getTime()) throw new Error(\"Offline replay capturedAt is in the future.\");\n\tif (maxAgeMs === undefined) {\n\t\tthrow new Error(\"Gateway accepts offline replay but declares no maxOfflineReplayAgeMs, so a capture has no lower bound.\");\n\t}\n\tif (now.getTime() - parsed > maxAgeMs) {\n\t\tthrow new Error(`Offline replay capturedAt is older than the ${maxAgeMs}ms this gateway accepts.`);\n\t}\n\treturn new Date(parsed).toISOString();\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"file":"index.cjs","names":["digestParameters"],"sources":["../index.ts"],"sourcesContent":["import type {\n\tGovernedActionHost,\n\tSubmitActionInput,\n\tSubmitActionResult,\n} from \"@fabricorg/platform-host\";\nimport { digestParameters } from \"@fabricorg/platform-host\";\nimport type { ProjectionHost } from \"@fabricorg/projection-host\";\nimport {\n\ttype ActorClaims,\n\tassertActorClaimsCoverScope,\n\ttype IdentityPort,\n} from \"@fabricorg/ports\";\nimport type { ExperienceIntentRequest, ExperienceReadRequest } from \"@fabricorg/experience-runtime\";\nimport type {\n\tExperienceScope,\n\tExperienceServerRuntime,\n} from \"@fabricorg/experience-runtime/server\";\n\nexport interface TrustedExperienceRequestContext {\n\t/** Presented only to IdentityPort and never persisted or forwarded. */\n\tcredential: string;\n\ttenantId: string;\n\tspaceId: string;\n}\n\nexport interface ExperienceGateway {\n\tinvoke(input: {\n\t\tcontext: TrustedExperienceRequestContext;\n\t\trequest: ExperienceIntentRequest;\n\t\texecutionReason?: \"initial\" | \"offline_replay\";\n\t\t/**\n\t\t * When the operator acted, for a replay. Required whenever\n\t\t * `executionReason` is `offline_replay`: reading it through an untyped\n\t\t * cast made omitting it the easy mistake, and omitting it silently\n\t\t * stamped receipt time into durable capture evidence.\n\t\t */\n\t\tcapturedAt?: string;\n\t}): Promise<SubmitActionResult>;\n\tread(input: {\n\t\tcontext: TrustedExperienceRequestContext;\n\t\trequest: ExperienceReadRequest;\n\t}): Promise<{\n\t\tdata: unknown;\n\t\tevidence: {\n\t\t\treleaseDigest: string;\n\t\t\tassemblyDigest: string;\n\t\t\teventCursor: string | null;\n\t\t\tsource: \"cache\" | \"replay\";\n\t\t};\n\t}>;\n}\n\nexport interface ExperienceAdmissionInput {\n\tkind: \"invoke\" | \"read\";\n\tscope: ExperienceScope;\n\tclaims: ActorClaims;\n\tplanId: string;\n\tfragmentId: string;\n\t/** The event or binding name the client named; the capability it resolves to is not yet known. */\n\tname: string;\n}\n\nexport type ExperienceAdmissionDecision =\n\t| { allowed: true }\n\t| { allowed: false; reason: string; retryAfterMs?: number };\n\n/** A request the edge would not admit. `retryable` is true when a later attempt may be admitted. */\nexport class ExperienceAdmissionError extends Error {\n\treadonly code = \"EXPERIENCE_ADMISSION_REFUSED\" as const;\n\tconstructor(readonly reason: string, readonly retryAfterMs?: number) {\n\t\tsuper(`Experience request not admitted: ${reason}`);\n\t\tthis.name = \"ExperienceAdmissionError\";\n\t}\n\tget retryable(): boolean {\n\t\treturn this.retryAfterMs !== undefined;\n\t}\n}\n\n/**\n * A reference admission policy: a token bucket per key, refilled over time.\n *\n * Enough to throttle a tenant or an actor at the edge; a deployment with a\n * shared limiter puts it behind the same option. Keys default to the tenant.\n */\nexport function createTokenBucketAdmission(options: {\n\tcapacity: number;\n\trefillPerSecond: number;\n\tkey?(input: ExperienceAdmissionInput): string;\n\tnow?(): number;\n}): (input: ExperienceAdmissionInput) => ExperienceAdmissionDecision {\n\tif (!(options.capacity >= 1) || !(options.refillPerSecond > 0)) throw new Error(\"Token bucket admission needs a capacity of at least one and a positive refill rate.\");\n\tconst now = options.now ?? (() => Date.now());\n\tconst key = options.key ?? ((input) => input.scope.tenantId);\n\tconst buckets = new Map<string, { tokens: number; at: number }>();\n\treturn (input) => {\n\t\tconst at = now();\n\t\tconst id = key(input);\n\t\tconst bucket = buckets.get(id) ?? { tokens: options.capacity, at };\n\t\t// A clock that steps backwards refills nothing; it must not debit.\n\t\tconst elapsedMs = Math.max(0, at - bucket.at);\n\t\tconst refilled = Math.min(options.capacity, bucket.tokens + (elapsedMs / 1000) * options.refillPerSecond);\n\t\tif (refilled >= 1) {\n\t\t\tconst remaining = refilled - 1;\n\t\t\t// A full bucket is the same as an absent one; forgetting it bounds the map to the active keys.\n\t\t\tif (remaining >= options.capacity) buckets.delete(id);\n\t\t\telse buckets.set(id, { tokens: remaining, at });\n\t\t\treturn { allowed: true };\n\t\t}\n\t\tbuckets.set(id, { tokens: refilled, at: Math.max(at, bucket.at) });\n\t\tconst retryAfterMs = Math.ceil(((1 - refilled) / options.refillPerSecond) * 1000);\n\t\treturn { allowed: false, reason: `budget for \"${id}\" is exhausted`, retryAfterMs };\n\t};\n}\n\nexport interface ExperienceGatewayConformanceSubject {\n\tgateway: ExperienceGateway;\n\tvalidIntent: Parameters<ExperienceGateway[\"invoke\"]>[0];\n\tdeniedIntent: Parameters<ExperienceGateway[\"invoke\"]>[0];\n\tvalidRead: Parameters<ExperienceGateway[\"read\"]>[0];\n\t/**\n\t * What the governed hosts actually saw.\n\t *\n\t * A returned invocation id proves only that a string was produced. The\n\t * point of a gateway is that mutations reach `PlatformHost.submitAction` and\n\t * reads reach `ProjectionHost.project`; a replacement that fabricates an id\n\t * and queries the database directly satisfies every observable contract\n\t * except this one, so the subject has to report the calls.\n\t */\n\tobserved: {\n\t\tsubmitActionCount(): number;\n\t\tprojectionCount(): number;\n\t};\n\t/**\n\t * An intent the deployment's admission policy refuses. Optional so an\n\t * existing subject still compiles; without it the admission check reports\n\t * `skipped` and says what was not proven, since a deployment that cannot\n\t * show one refusal has not configured admission.\n\t */\n\tthrottledIntent?: Parameters<ExperienceGateway[\"invoke\"]>[0];\n}\n\nexport interface ExperienceGatewayCheck {\n\tid:\n\t\t| \"fabric.experience.gateway-host-routing.v1\"\n\t\t| \"fabric.experience.gateway-scope-denial.v1\"\n\t\t| \"fabric.experience.gateway-projection-routing.v1\"\n\t\t| \"fabric.experience.gateway-admission-refusal.v1\";\n\trun(subject: ExperienceGatewayConformanceSubject): Promise<string[]>;\n}\n\nexport function experienceGatewayChecks(): readonly ExperienceGatewayCheck[] {\n\treturn [\n\t\t{\n\t\t\tid: \"fabric.experience.gateway-host-routing.v1\",\n\t\t\tasync run(subject) {\n\t\t\t\tconst before = subject.observed.submitActionCount();\n\t\t\t\tconst result = await subject.gateway.invoke(subject.validIntent);\n\t\t\t\tif (!result.actionInvocationId) throw new Error(\"Gateway did not return a governed Host invocation.\");\n\t\t\t\tconst submitted = subject.observed.submitActionCount() - before;\n\t\t\t\tif (submitted !== 1) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`Gateway returned an invocation id after ${submitted} calls to PlatformHost.submitAction; a mutation must reach the Host exactly once.`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\treturn [result.actionInvocationId];\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: \"fabric.experience.gateway-projection-routing.v1\",\n\t\t\tasync run(subject) {\n\t\t\t\tconst before = subject.observed.projectionCount();\n\t\t\t\tawait subject.gateway.read(subject.validRead);\n\t\t\t\tconst projected = subject.observed.projectionCount() - before;\n\t\t\t\tif (projected !== 1) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`Gateway served a read after ${projected} calls to ProjectionHost.project; a read must go through the projection host.`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\treturn [\"read routed through ProjectionHost\"];\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: \"fabric.experience.gateway-scope-denial.v1\",\n\t\t\tasync run(subject) {\n\t\t\t\tlet rejected = false;\n\t\t\t\ttry { await subject.gateway.invoke(subject.deniedIntent); } catch { rejected = true; }\n\t\t\t\tif (!rejected) throw new Error(\"Gateway accepted an intent outside trusted identity scope.\");\n\t\t\t\treturn [\"out-of-scope intent rejected\"];\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: \"fabric.experience.gateway-admission-refusal.v1\",\n\t\t\tasync run(subject) {\n\t\t\t\tif (!subject.throttledIntent) return [\"skipped: the subject supplies no throttledIntent, so admission is not proven\"];\n\t\t\t\tconst before = subject.observed.submitActionCount();\n\t\t\t\tlet refusal: unknown;\n\t\t\t\ttry { await subject.gateway.invoke(subject.throttledIntent); } catch (error) { refusal = error; }\n\t\t\t\tif (!(refusal instanceof ExperienceAdmissionError)) {\n\t\t\t\t\tthrow new Error(\"Gateway admitted an intent the deployment's admission policy refuses, or refused it as something other than ExperienceAdmissionError.\");\n\t\t\t\t}\n\t\t\t\tif (subject.observed.submitActionCount() !== before) {\n\t\t\t\t\tthrow new Error(\"A refused intent still reached PlatformHost.submitAction; admission must decide before anything governed is touched.\");\n\t\t\t\t}\n\t\t\t\treturn [`admission refused: ${refusal.reason}${refusal.retryable ? ` (retry after ${refusal.retryAfterMs}ms)` : \"\"}`];\n\t\t\t},\n\t\t},\n\t];\n}\n\nexport async function runExperienceGatewayChecks(\n\tsubject: ExperienceGatewayConformanceSubject,\n\tchecks: readonly ExperienceGatewayCheck[] = experienceGatewayChecks(),\n): Promise<{ passed: boolean; checks: Array<{ id: ExperienceGatewayCheck[\"id\"]; status: \"passed\" | \"failed\" | \"skipped\"; evidence: string[]; error?: string }> }> {\n\tconst results: Array<{ id: ExperienceGatewayCheck[\"id\"]; status: \"passed\" | \"failed\" | \"skipped\"; evidence: string[]; error?: string }> = [];\n\tfor (const check of checks) {\n\t\ttry {\n\t\t\tconst evidence = await check.run(subject);\n\t\t\tresults.push({ id: check.id, status: evidence[0]?.startsWith(\"skipped:\") ? \"skipped\" : \"passed\", evidence });\n\t\t}\n\t\tcatch (error) { results.push({ id: check.id, status: \"failed\", evidence: [], error: error instanceof Error ? error.message : String(error) }); }\n\t}\n\treturn { passed: results.every((result) => result.status !== \"failed\"), checks: results };\n}\n\nexport interface InitiatingReleaseEvidence {\n\tbind(input: SubmitActionInput, releaseDigest: string): void;\n\tresolve(input: SubmitActionInput): string | undefined;\n\trelease(input: SubmitActionInput): void;\n}\n\n/**\n * Creates an instance-scoped, non-serializable evidence seam. The Host's\n * `composition.resolveInitiatingReleaseDigest` reads this WeakMap using the\n * exact submission object. A browser field cannot populate or forge it.\n */\nexport function createInitiatingReleaseEvidence(): InitiatingReleaseEvidence {\n\tconst evidence = new WeakMap<SubmitActionInput, string>();\n\treturn {\n\t\tbind(input, releaseDigest) {\n\t\t\tevidence.set(input, releaseDigest);\n\t\t},\n\t\tresolve(input) {\n\t\t\treturn evidence.get(input);\n\t\t},\n\t\trelease(input) {\n\t\t\tevidence.delete(input);\n\t\t},\n\t};\n}\n\nexport function createExperienceGateway(options: {\n\tidentity: IdentityPort;\n\truntime: ExperienceServerRuntime;\n\thost: Pick<GovernedActionHost, \"submitAction\">;\n\tprojectionHost: Pick<ProjectionHost, \"project\">;\n\tevidence: InitiatingReleaseEvidence;\n\tassemblyDigest: string;\n\tactionVersion(actionId: `${string}.${string}`): number | undefined;\n\tnextCorrelationId(): string;\n\t/**\n\t * How old an offline capture may be and still replay.\n\t *\n\t * Required to accept replay at all. The Host's own staleness check is\n\t * conditional on the capture-authority adapter supplying an `expiresAt`, and\n\t * is skipped entirely for actions governed at capture time, so without this\n\t * a client-reported instant has no lower bound.\n\t */\n\tmaxOfflineReplayAgeMs?: number;\n\t/**\n\t * Admission control at the edge, decided after the credential is verified\n\t * and before anything governed is touched. Per-tenant request budgets,\n\t * render-cost quotas, and abuse throttles live here, as a seam the\n\t * deployment fills; the gateway itself imposes no policy. A refusal\n\t * surfaces as `ExperienceAdmissionError`, carrying `retryAfterMs` when the\n\t * caller should come back rather than give up.\n\t */\n\tadmission?(input: ExperienceAdmissionInput): Promise<ExperienceAdmissionDecision> | ExperienceAdmissionDecision;\n\tcaptureAuthority?(input: {\n\t\tactionId: `${string}.${string}`;\n\t\tscope: ExperienceScope;\n\t\tparameters: Record<string, unknown>;\n\t\tparameterDigest: string;\n\t\tcapturedAt: string;\n\t}): Promise<{ id: string; expiresAt?: string }>;\n\tnow?: () => Date;\n}): ExperienceGateway {\n\tconst now = options.now ?? (() => new Date());\n\treturn {\n\t\tasync invoke(input) {\n\t\t\tconst claims = await options.identity.verify(input.context.credential);\n\t\t\tif (!claims) throw new Error(\"Experience request credential is not authenticated.\");\n\t\t\tassertActorClaimsCoverScope(claims, {\n\t\t\t\ttenantId: input.context.tenantId,\n\t\t\t\tspaceId: input.context.spaceId,\n\t\t\t}, now());\n\t\t\tconst scope: ExperienceScope = {\n\t\t\t\ttenantId: input.context.tenantId,\n\t\t\t\tspaceId: input.context.spaceId,\n\t\t\t\tactorId: claims.subject,\n\t\t\t\tactorType: claims.actorType,\n\t\t\t};\n\t\t\tawait admit(options.admission, { kind: \"invoke\", scope, claims, planId: input.request.planId, fragmentId: input.request.fragmentId, name: input.request.eventName });\n\t\t\tconst resolved = await options.runtime.resolveIntent({\n\t\t\t\tplanId: input.request.planId,\n\t\t\t\tfragmentId: input.request.fragmentId,\n\t\t\t\teventName: input.request.eventName,\n\t\t\t\tscope,\n\t\t\t});\n\t\t\tif (resolved.assemblyDigest !== options.assemblyDigest) {\n\t\t\t\tthrow new Error(\"Experience render plan belongs to a different application assembly.\");\n\t\t\t}\n\t\t\tif (options.actionVersion(resolved.actionId) !== resolved.actionVersion) {\n\t\t\t\tthrow new Error(`Experience intent route for \"${resolved.actionId}\" does not match the registered action version.`);\n\t\t\t}\n\t\t\tif (resolved.execution?.idempotency === \"required\" && !input.request.idempotencyKey) {\n\t\t\t\tthrow new Error(`Action \"${resolved.actionId}\" requires an idempotency key.`);\n\t\t\t}\n\t\t\tif (input.executionReason === \"offline_replay\" && resolved.execution?.connectivity !== \"offline-capturable\") {\n\t\t\t\tthrow new Error(`Action \"${resolved.actionId}\" is not eligible for offline replay.`);\n\t\t\t}\n\t\t\tconst correlationId = options.nextCorrelationId();\n\t\t\t// A replay attests when the operator acted, not when the network came\n\t\t\t// back. Stamping receipt time would make capture-authority evidence\n\t\t\t// describe a moment nobody was there for.\n\t\t\tlet capturedAt: string;\n\t\t\tif (input.executionReason === \"offline_replay\") {\n\t\t\t\t// Accept it from either position, but require it: a replay with no\n\t\t\t\t// capture moment cannot be evidenced, and defaulting to receipt\n\t\t\t\t// time would attest a moment nobody was there for.\n\t\t\t\tconst supplied = input.capturedAt\n\t\t\t\t\t?? (input.request as Partial<{ capturedAt: string }>).capturedAt;\n\t\t\t\tif (typeof supplied !== \"string\") {\n\t\t\t\t\tthrow new Error(\"Offline replay requires capturedAt; receipt time cannot attest when the operator acted.\");\n\t\t\t\t}\n\t\t\t\tcapturedAt = assertCaptureMoment(supplied, now(), options.maxOfflineReplayAgeMs);\n\t\t\t} else {\n\t\t\t\tcapturedAt = now().toISOString();\n\t\t\t}\n\t\t\tconst parameterDigest = digestParameters(input.request.parameters);\n\t\t\tconst capturedAuthority = resolved.execution?.authorityMoment === \"capture\"\n\t\t\t\t? await options.captureAuthority?.({\n\t\t\t\t\tactionId: resolved.actionId,\n\t\t\t\t\tscope,\n\t\t\t\t\tparameters: input.request.parameters,\n\t\t\t\t\tparameterDigest,\n\t\t\t\t\tcapturedAt,\n\t\t\t\t})\n\t\t\t\t: undefined;\n\t\t\tif (resolved.execution?.authorityMoment === \"capture\" && !capturedAuthority) {\n\t\t\t\tthrow new Error(`Action \"${resolved.actionId}\" requires a trusted capture-authority adapter.`);\n\t\t\t}\n\t\t\tconst submission: SubmitActionInput = {\n\t\t\t\tactionId: resolved.actionId,\n\t\t\t\tparameters: input.request.parameters,\n\t\t\t\ttenantId: scope.tenantId,\n\t\t\t\tspaceId: scope.spaceId,\n\t\t\t\tactorId: scope.actorId,\n\t\t\t\tactorType: claims.actorType,\n\t\t\t\tcorrelationId,\n\t\t\t\tprovenance: {\n\t\t\t\t\tsource: \"ui\",\n\t\t\t\t\tcorrelationId,\n\t\t\t\t\t...(resolved.execution?.sensitivity === \"restricted\"\n\t\t\t\t\t\t? {}\n\t\t\t\t\t\t: { auditAttributes: { \"fabric.experience.fragment\": input.request.fragmentId } }),\n\t\t\t\t},\n\t\t\t\t...(input.request.idempotencyKey === undefined ? {} : { idempotencyKey: input.request.idempotencyKey }),\n\t\t\t\t...(input.executionReason === undefined ? {} : { executionReason: input.executionReason }),\n\t\t\t\t...(capturedAuthority === undefined ? {} : {\n\t\t\t\t\tauthorizationBindingId: capturedAuthority.id,\n\t\t\t\t\tauthorizationBinding: {\n\t\t\t\t\t\tid: capturedAuthority.id,\n\t\t\t\t\t\tgoverningMoment: \"capture\",\n\t\t\t\t\t\tcapturedAt,\n\t\t\t\t\t\t...(capturedAuthority.expiresAt === undefined ? {} : { expiresAt: capturedAuthority.expiresAt }),\n\t\t\t\t\t\ttenantId: scope.tenantId,\n\t\t\t\t\t\tactorId: scope.actorId,\n\t\t\t\t\t\tactionId: resolved.actionId,\n\t\t\t\t\t\tparameterDigest,\n\t\t\t\t\t},\n\t\t\t\t}),\n\t\t\t};\n\t\t\toptions.evidence.bind(submission, resolved.initiatingReleaseDigest);\n\t\t\ttry {\n\t\t\t\treturn await options.host.submitAction(submission);\n\t\t\t} finally {\n\t\t\t\toptions.evidence.release(submission);\n\t\t\t}\n\t\t},\n\t\tasync read(input) {\n\t\t\tconst claims = await options.identity.verify(input.context.credential);\n\t\t\tif (!claims) throw new Error(\"Experience request credential is not authenticated.\");\n\t\t\tassertActorClaimsCoverScope(claims, {\n\t\t\t\ttenantId: input.context.tenantId,\n\t\t\t\tspaceId: input.context.spaceId,\n\t\t\t}, now());\n\t\t\tconst scope: ExperienceScope = {\n\t\t\t\ttenantId: input.context.tenantId,\n\t\t\t\tspaceId: input.context.spaceId,\n\t\t\t\tactorId: claims.subject,\n\t\t\t\tactorType: claims.actorType,\n\t\t\t};\n\t\t\tawait admit(options.admission, { kind: \"read\", scope, claims, planId: input.request.planId, fragmentId: input.request.fragmentId, name: input.request.bindingName });\n\t\t\tconst resolved = await options.runtime.resolveView({\n\t\t\t\tplanId: input.request.planId,\n\t\t\t\tfragmentId: input.request.fragmentId,\n\t\t\t\tbindingName: input.request.bindingName,\n\t\t\t\tscope,\n\t\t\t});\n\t\t\tif (resolved.assemblyDigest !== options.assemblyDigest) {\n\t\t\t\tthrow new Error(\"Experience render plan belongs to a different application assembly.\");\n\t\t\t}\n\t\t\tconst result = await options.projectionHost.project({\n\t\t\t\tview: { name: resolved.name, version: resolved.version },\n\t\t\t\tscope: { tenantId: scope.tenantId, spaceId: scope.spaceId },\n\t\t\t\tactor: { id: scope.actorId, type: claims.actorType },\n\t\t\t\tparameters: input.request.parameters,\n\t\t\t});\n\t\t\treturn {\n\t\t\t\tdata: result.state,\n\t\t\t\tevidence: {\n\t\t\t\t\treleaseDigest: resolved.initiatingReleaseDigest,\n\t\t\t\t\tassemblyDigest: resolved.assemblyDigest,\n\t\t\t\t\teventCursor: result.eventCursor,\n\t\t\t\t\tsource: result.source,\n\t\t\t\t},\n\t\t\t};\n\t\t},\n\t};\n}\n\n/** RFC 3339 with a mandatory offset, so an instant means the same thing everywhere. */\nconst RFC3339 = /^\\d{4}-\\d{2}-\\d{2}[Tt]\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:[Zz]|[+-]\\d{2}:\\d{2})$/;\n\n/**\n * A supplied capture moment is client-reported, so it is bounded here rather\n * than trusted.\n *\n * Deferring the lower bound to the Host looked safe and is not: the Host checks\n * staleness only when the capture-authority adapter supplied an `expiresAt`,\n * and skips the execution-time expiry check entirely when the action's\n * authority moment is `capture`. With an adapter that omits `expiresAt` there\n * is no lower bound at all, and the value is written into durable evidence and\n * handed to the authority adapter regardless. A gateway that accepts replay\n * must therefore say how old a capture may be.\n */\nasync function admit(\n\tadmission: ((input: ExperienceAdmissionInput) => Promise<ExperienceAdmissionDecision> | ExperienceAdmissionDecision) | undefined,\n\tinput: ExperienceAdmissionInput,\n): Promise<void> {\n\tif (!admission) return;\n\tconst decision = await admission(input);\n\tif (!decision.allowed) throw new ExperienceAdmissionError(decision.reason, decision.retryAfterMs);\n}\n\nfunction assertCaptureMoment(capturedAt: string, now: Date, maxAgeMs: number | undefined): string {\n\tif (!RFC3339.test(capturedAt)) {\n\t\tthrow new Error(\"Offline replay capturedAt must be an RFC 3339 instant with an explicit offset.\");\n\t}\n\tconst parsed = Date.parse(capturedAt);\n\tif (Number.isNaN(parsed)) throw new Error(\"Offline replay capturedAt is not a parseable instant.\");\n\tif (parsed > now.getTime()) throw new Error(\"Offline replay capturedAt is in the future.\");\n\tif (maxAgeMs === undefined) {\n\t\tthrow new Error(\"Gateway accepts offline replay but declares no maxOfflineReplayAgeMs, so a capture has no lower bound.\");\n\t}\n\tif (now.getTime() - parsed > maxAgeMs) {\n\t\tthrow new Error(`Offline replay capturedAt is older than the ${maxAgeMs}ms this gateway accepts.`);\n\t}\n\treturn new Date(parsed).toISOString();\n}\n"],"mappings":"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"}
package/dist/index.d.cts CHANGED
@@ -1,64 +1,64 @@
1
- import { SubmitActionResult, SubmitActionInput, GovernedActionHost } from '@fabricorg/platform-host';
2
- import { ProjectionHost } from '@fabricorg/projection-host';
3
- import { ActorClaims, IdentityPort } from '@fabricorg/ports';
4
- import { ExperienceIntentRequest, ExperienceReadRequest } from '@fabricorg/experience-runtime';
5
- import { ExperienceScope, ExperienceServerRuntime } from '@fabricorg/experience-runtime/server';
6
-
7
- interface TrustedExperienceRequestContext {
8
- /** Presented only to IdentityPort and never persisted or forwarded. */
9
- credential: string;
10
- tenantId: string;
11
- spaceId: string;
1
+ import { GovernedActionHost, SubmitActionInput, SubmitActionResult } from "@fabricorg/platform-host";
2
+ import { ProjectionHost } from "@fabricorg/projection-host";
3
+ import { ActorClaims, IdentityPort } from "@fabricorg/ports";
4
+ import { ExperienceIntentRequest, ExperienceReadRequest } from "@fabricorg/experience-runtime";
5
+ import { ExperienceScope, ExperienceServerRuntime } from "@fabricorg/experience-runtime/server";
6
+ //#region index.d.ts
7
+ export interface TrustedExperienceRequestContext {
8
+ /** Presented only to IdentityPort and never persisted or forwarded. */
9
+ credential: string;
10
+ tenantId: string;
11
+ spaceId: string;
12
12
  }
13
- interface ExperienceGateway {
14
- invoke(input: {
15
- context: TrustedExperienceRequestContext;
16
- request: ExperienceIntentRequest;
17
- executionReason?: "initial" | "offline_replay";
18
- /**
19
- * When the operator acted, for a replay. Required whenever
20
- * `executionReason` is `offline_replay`: reading it through an untyped
21
- * cast made omitting it the easy mistake, and omitting it silently
22
- * stamped receipt time into durable capture evidence.
23
- */
24
- capturedAt?: string;
25
- }): Promise<SubmitActionResult>;
26
- read(input: {
27
- context: TrustedExperienceRequestContext;
28
- request: ExperienceReadRequest;
29
- }): Promise<{
30
- data: unknown;
31
- evidence: {
32
- releaseDigest: string;
33
- assemblyDigest: string;
34
- eventCursor: string | null;
35
- source: "cache" | "replay";
36
- };
37
- }>;
13
+ export interface ExperienceGateway {
14
+ invoke(input: {
15
+ context: TrustedExperienceRequestContext;
16
+ request: ExperienceIntentRequest;
17
+ executionReason?: "initial" | "offline_replay";
18
+ /**
19
+ * When the operator acted, for a replay. Required whenever
20
+ * `executionReason` is `offline_replay`: reading it through an untyped
21
+ * cast made omitting it the easy mistake, and omitting it silently
22
+ * stamped receipt time into durable capture evidence.
23
+ */
24
+ capturedAt?: string;
25
+ }): Promise<SubmitActionResult>;
26
+ read(input: {
27
+ context: TrustedExperienceRequestContext;
28
+ request: ExperienceReadRequest;
29
+ }): Promise<{
30
+ data: unknown;
31
+ evidence: {
32
+ releaseDigest: string;
33
+ assemblyDigest: string;
34
+ eventCursor: string | null;
35
+ source: "cache" | "replay";
36
+ };
37
+ }>;
38
38
  }
39
- interface ExperienceAdmissionInput {
40
- kind: "invoke" | "read";
41
- scope: ExperienceScope;
42
- claims: ActorClaims;
43
- planId: string;
44
- fragmentId: string;
45
- /** The event or binding name the client named; the capability it resolves to is not yet known. */
46
- name: string;
39
+ export interface ExperienceAdmissionInput {
40
+ kind: "invoke" | "read";
41
+ scope: ExperienceScope;
42
+ claims: ActorClaims;
43
+ planId: string;
44
+ fragmentId: string;
45
+ /** The event or binding name the client named; the capability it resolves to is not yet known. */
46
+ name: string;
47
47
  }
48
- type ExperienceAdmissionDecision = {
49
- allowed: true;
48
+ export type ExperienceAdmissionDecision = {
49
+ allowed: true;
50
50
  } | {
51
- allowed: false;
52
- reason: string;
53
- retryAfterMs?: number;
51
+ allowed: false;
52
+ reason: string;
53
+ retryAfterMs?: number;
54
54
  };
55
55
  /** A request the edge would not admit. `retryable` is true when a later attempt may be admitted. */
56
- declare class ExperienceAdmissionError extends Error {
57
- readonly reason: string;
58
- readonly retryAfterMs?: number | undefined;
59
- readonly code: "EXPERIENCE_ADMISSION_REFUSED";
60
- constructor(reason: string, retryAfterMs?: number | undefined);
61
- get retryable(): boolean;
56
+ export declare class ExperienceAdmissionError extends Error {
57
+ readonly reason: string;
58
+ readonly retryAfterMs?: number | undefined;
59
+ readonly code: "EXPERIENCE_ADMISSION_REFUSED";
60
+ constructor(reason: string, retryAfterMs?: number | undefined);
61
+ get retryable(): boolean;
62
62
  }
63
63
  /**
64
64
  * A reference admission policy: a token bucket per key, refilled over time.
@@ -66,101 +66,100 @@ declare class ExperienceAdmissionError extends Error {
66
66
  * Enough to throttle a tenant or an actor at the edge; a deployment with a
67
67
  * shared limiter puts it behind the same option. Keys default to the tenant.
68
68
  */
69
- declare function createTokenBucketAdmission(options: {
70
- capacity: number;
71
- refillPerSecond: number;
72
- key?(input: ExperienceAdmissionInput): string;
73
- now?(): number;
69
+ export declare function createTokenBucketAdmission(options: {
70
+ capacity: number;
71
+ refillPerSecond: number;
72
+ key?(input: ExperienceAdmissionInput): string;
73
+ now?(): number;
74
74
  }): (input: ExperienceAdmissionInput) => ExperienceAdmissionDecision;
75
- interface ExperienceGatewayConformanceSubject {
76
- gateway: ExperienceGateway;
77
- validIntent: Parameters<ExperienceGateway["invoke"]>[0];
78
- deniedIntent: Parameters<ExperienceGateway["invoke"]>[0];
79
- validRead: Parameters<ExperienceGateway["read"]>[0];
80
- /**
81
- * What the governed hosts actually saw.
82
- *
83
- * A returned invocation id proves only that a string was produced. The
84
- * point of a gateway is that mutations reach `PlatformHost.submitAction` and
85
- * reads reach `ProjectionHost.project`; a replacement that fabricates an id
86
- * and queries the database directly satisfies every observable contract
87
- * except this one, so the subject has to report the calls.
88
- */
89
- observed: {
90
- submitActionCount(): number;
91
- projectionCount(): number;
92
- };
93
- /**
94
- * An intent the deployment's admission policy refuses. Optional so an
95
- * existing subject still compiles; without it the admission check reports
96
- * `skipped` and says what was not proven, since a deployment that cannot
97
- * show one refusal has not configured admission.
98
- */
99
- throttledIntent?: Parameters<ExperienceGateway["invoke"]>[0];
75
+ export interface ExperienceGatewayConformanceSubject {
76
+ gateway: ExperienceGateway;
77
+ validIntent: Parameters<ExperienceGateway["invoke"]>[0];
78
+ deniedIntent: Parameters<ExperienceGateway["invoke"]>[0];
79
+ validRead: Parameters<ExperienceGateway["read"]>[0];
80
+ /**
81
+ * What the governed hosts actually saw.
82
+ *
83
+ * A returned invocation id proves only that a string was produced. The
84
+ * point of a gateway is that mutations reach `PlatformHost.submitAction` and
85
+ * reads reach `ProjectionHost.project`; a replacement that fabricates an id
86
+ * and queries the database directly satisfies every observable contract
87
+ * except this one, so the subject has to report the calls.
88
+ */
89
+ observed: {
90
+ submitActionCount(): number;
91
+ projectionCount(): number;
92
+ };
93
+ /**
94
+ * An intent the deployment's admission policy refuses. Optional so an
95
+ * existing subject still compiles; without it the admission check reports
96
+ * `skipped` and says what was not proven, since a deployment that cannot
97
+ * show one refusal has not configured admission.
98
+ */
99
+ throttledIntent?: Parameters<ExperienceGateway["invoke"]>[0];
100
100
  }
101
- interface ExperienceGatewayCheck {
102
- id: "fabric.experience.gateway-host-routing.v1" | "fabric.experience.gateway-scope-denial.v1" | "fabric.experience.gateway-projection-routing.v1" | "fabric.experience.gateway-admission-refusal.v1";
103
- run(subject: ExperienceGatewayConformanceSubject): Promise<string[]>;
101
+ export interface ExperienceGatewayCheck {
102
+ id: "fabric.experience.gateway-host-routing.v1" | "fabric.experience.gateway-scope-denial.v1" | "fabric.experience.gateway-projection-routing.v1" | "fabric.experience.gateway-admission-refusal.v1";
103
+ run(subject: ExperienceGatewayConformanceSubject): Promise<string[]>;
104
104
  }
105
- declare function experienceGatewayChecks(): readonly ExperienceGatewayCheck[];
106
- declare function runExperienceGatewayChecks(subject: ExperienceGatewayConformanceSubject, checks?: readonly ExperienceGatewayCheck[]): Promise<{
107
- passed: boolean;
108
- checks: Array<{
109
- id: ExperienceGatewayCheck["id"];
110
- status: "passed" | "failed" | "skipped";
111
- evidence: string[];
112
- error?: string;
113
- }>;
105
+ export declare function experienceGatewayChecks(): readonly ExperienceGatewayCheck[];
106
+ export declare function runExperienceGatewayChecks(subject: ExperienceGatewayConformanceSubject, checks?: readonly ExperienceGatewayCheck[]): Promise<{
107
+ passed: boolean;
108
+ checks: Array<{
109
+ id: ExperienceGatewayCheck["id"];
110
+ status: "passed" | "failed" | "skipped";
111
+ evidence: string[];
112
+ error?: string;
113
+ }>;
114
114
  }>;
115
- interface InitiatingReleaseEvidence {
116
- bind(input: SubmitActionInput, releaseDigest: string): void;
117
- resolve(input: SubmitActionInput): string | undefined;
118
- release(input: SubmitActionInput): void;
115
+ export interface InitiatingReleaseEvidence {
116
+ bind(input: SubmitActionInput, releaseDigest: string): void;
117
+ resolve(input: SubmitActionInput): string | undefined;
118
+ release(input: SubmitActionInput): void;
119
119
  }
120
120
  /**
121
121
  * Creates an instance-scoped, non-serializable evidence seam. The Host's
122
122
  * `composition.resolveInitiatingReleaseDigest` reads this WeakMap using the
123
123
  * exact submission object. A browser field cannot populate or forge it.
124
124
  */
125
- declare function createInitiatingReleaseEvidence(): InitiatingReleaseEvidence;
126
- declare function createExperienceGateway(options: {
127
- identity: IdentityPort;
128
- runtime: ExperienceServerRuntime;
129
- host: Pick<GovernedActionHost, "submitAction">;
130
- projectionHost: Pick<ProjectionHost, "project">;
131
- evidence: InitiatingReleaseEvidence;
132
- assemblyDigest: string;
133
- actionVersion(actionId: `${string}.${string}`): number | undefined;
134
- nextCorrelationId(): string;
135
- /**
136
- * How old an offline capture may be and still replay.
137
- *
138
- * Required to accept replay at all. The Host's own staleness check is
139
- * conditional on the capture-authority adapter supplying an `expiresAt`, and
140
- * is skipped entirely for actions governed at capture time, so without this
141
- * a client-reported instant has no lower bound.
142
- */
143
- maxOfflineReplayAgeMs?: number;
144
- /**
145
- * Admission control at the edge, decided after the credential is verified
146
- * and before anything governed is touched. Per-tenant request budgets,
147
- * render-cost quotas, and abuse throttles live here, as a seam the
148
- * deployment fills; the gateway itself imposes no policy. A refusal
149
- * surfaces as `ExperienceAdmissionError`, carrying `retryAfterMs` when the
150
- * caller should come back rather than give up.
151
- */
152
- admission?(input: ExperienceAdmissionInput): Promise<ExperienceAdmissionDecision> | ExperienceAdmissionDecision;
153
- captureAuthority?(input: {
154
- actionId: `${string}.${string}`;
155
- scope: ExperienceScope;
156
- parameters: Record<string, unknown>;
157
- parameterDigest: string;
158
- capturedAt: string;
159
- }): Promise<{
160
- id: string;
161
- expiresAt?: string;
162
- }>;
163
- now?: () => Date;
125
+ export declare function createInitiatingReleaseEvidence(): InitiatingReleaseEvidence;
126
+ export declare function createExperienceGateway(options: {
127
+ identity: IdentityPort;
128
+ runtime: ExperienceServerRuntime;
129
+ host: Pick<GovernedActionHost, "submitAction">;
130
+ projectionHost: Pick<ProjectionHost, "project">;
131
+ evidence: InitiatingReleaseEvidence;
132
+ assemblyDigest: string;
133
+ actionVersion(actionId: `${string}.${string}`): number | undefined;
134
+ nextCorrelationId(): string;
135
+ /**
136
+ * How old an offline capture may be and still replay.
137
+ *
138
+ * Required to accept replay at all. The Host's own staleness check is
139
+ * conditional on the capture-authority adapter supplying an `expiresAt`, and
140
+ * is skipped entirely for actions governed at capture time, so without this
141
+ * a client-reported instant has no lower bound.
142
+ */
143
+ maxOfflineReplayAgeMs?: number;
144
+ /**
145
+ * Admission control at the edge, decided after the credential is verified
146
+ * and before anything governed is touched. Per-tenant request budgets,
147
+ * render-cost quotas, and abuse throttles live here, as a seam the
148
+ * deployment fills; the gateway itself imposes no policy. A refusal
149
+ * surfaces as `ExperienceAdmissionError`, carrying `retryAfterMs` when the
150
+ * caller should come back rather than give up.
151
+ */
152
+ admission?(input: ExperienceAdmissionInput): Promise<ExperienceAdmissionDecision> | ExperienceAdmissionDecision;
153
+ captureAuthority?(input: {
154
+ actionId: `${string}.${string}`;
155
+ scope: ExperienceScope;
156
+ parameters: Record<string, unknown>;
157
+ parameterDigest: string;
158
+ capturedAt: string;
159
+ }): Promise<{
160
+ id: string;
161
+ expiresAt?: string;
162
+ }>;
163
+ now?: () => Date;
164
164
  }): ExperienceGateway;
165
-
166
- export { type ExperienceAdmissionDecision, ExperienceAdmissionError, type ExperienceAdmissionInput, type ExperienceGateway, type ExperienceGatewayCheck, type ExperienceGatewayConformanceSubject, type InitiatingReleaseEvidence, type TrustedExperienceRequestContext, createExperienceGateway, createInitiatingReleaseEvidence, createTokenBucketAdmission, experienceGatewayChecks, runExperienceGatewayChecks };
165
+ //#endregion