@fabricorg/experience-gateway 2.0.0 → 2.1.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - dafd82b: An `admission` seam on the gateway: decided after the credential is verified and before anything governed is touched, for per-tenant budgets, render-cost quotas, and abuse throttles. A refusal is `ExperienceAdmissionError`, carrying `retryAfterMs` and a `retryable` flag. `createTokenBucketAdmission` is the reference policy. The conformance suite gains `gateway-admission-refusal.v1` and its subject an optional `throttledIntent`; without one the check reports `skipped` and says admission is not proven, so an existing subject still compiles and a deployment that cannot show a refusal reads as unproven. The result type gains the `skipped` status.
8
+
9
+ ## 2.0.1
10
+
11
+ ### Patch Changes
12
+
13
+ - Updated dependencies [8436c64]
14
+ - @fabricorg/experience-runtime@0.4.0
15
+
3
16
  ## 2.0.0
4
17
 
5
18
  ### Patch Changes
package/dist/index.cjs CHANGED
@@ -20,14 +20,52 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ ExperienceAdmissionError: () => ExperienceAdmissionError,
23
24
  createExperienceGateway: () => createExperienceGateway,
24
25
  createInitiatingReleaseEvidence: () => createInitiatingReleaseEvidence,
26
+ createTokenBucketAdmission: () => createTokenBucketAdmission,
25
27
  experienceGatewayChecks: () => experienceGatewayChecks,
26
28
  runExperienceGatewayChecks: () => runExperienceGatewayChecks
27
29
  });
28
30
  module.exports = __toCommonJS(index_exports);
29
31
  var import_platform_host = require("@fabricorg/platform-host");
30
32
  var import_ports = require("@fabricorg/ports");
33
+ var ExperienceAdmissionError = class extends Error {
34
+ constructor(reason, retryAfterMs) {
35
+ super(`Experience request not admitted: ${reason}`);
36
+ this.reason = reason;
37
+ this.retryAfterMs = retryAfterMs;
38
+ this.name = "ExperienceAdmissionError";
39
+ }
40
+ reason;
41
+ retryAfterMs;
42
+ code = "EXPERIENCE_ADMISSION_REFUSED";
43
+ get retryable() {
44
+ return this.retryAfterMs !== void 0;
45
+ }
46
+ };
47
+ function createTokenBucketAdmission(options) {
48
+ if (!(options.capacity >= 1) || !(options.refillPerSecond > 0)) throw new Error("Token bucket admission needs a capacity of at least one and a positive refill rate.");
49
+ const now = options.now ?? (() => Date.now());
50
+ const key = options.key ?? ((input) => input.scope.tenantId);
51
+ const buckets = /* @__PURE__ */ new Map();
52
+ return (input) => {
53
+ const at = now();
54
+ const id = key(input);
55
+ const bucket = buckets.get(id) ?? { tokens: options.capacity, at };
56
+ const elapsedMs = Math.max(0, at - bucket.at);
57
+ const refilled = Math.min(options.capacity, bucket.tokens + elapsedMs / 1e3 * options.refillPerSecond);
58
+ if (refilled >= 1) {
59
+ const remaining = refilled - 1;
60
+ if (remaining >= options.capacity) buckets.delete(id);
61
+ else buckets.set(id, { tokens: remaining, at });
62
+ return { allowed: true };
63
+ }
64
+ buckets.set(id, { tokens: refilled, at: Math.max(at, bucket.at) });
65
+ const retryAfterMs = Math.ceil((1 - refilled) / options.refillPerSecond * 1e3);
66
+ return { allowed: false, reason: `budget for "${id}" is exhausted`, retryAfterMs };
67
+ };
68
+ }
31
69
  function experienceGatewayChecks() {
32
70
  return [
33
71
  {
@@ -71,6 +109,26 @@ function experienceGatewayChecks() {
71
109
  if (!rejected) throw new Error("Gateway accepted an intent outside trusted identity scope.");
72
110
  return ["out-of-scope intent rejected"];
73
111
  }
112
+ },
113
+ {
114
+ id: "fabric.experience.gateway-admission-refusal.v1",
115
+ async run(subject) {
116
+ if (!subject.throttledIntent) return ["skipped: the subject supplies no throttledIntent, so admission is not proven"];
117
+ const before = subject.observed.submitActionCount();
118
+ let refusal;
119
+ try {
120
+ await subject.gateway.invoke(subject.throttledIntent);
121
+ } catch (error) {
122
+ refusal = error;
123
+ }
124
+ if (!(refusal instanceof ExperienceAdmissionError)) {
125
+ throw new Error("Gateway admitted an intent the deployment's admission policy refuses, or refused it as something other than ExperienceAdmissionError.");
126
+ }
127
+ if (subject.observed.submitActionCount() !== before) {
128
+ throw new Error("A refused intent still reached PlatformHost.submitAction; admission must decide before anything governed is touched.");
129
+ }
130
+ return [`admission refused: ${refusal.reason}${refusal.retryable ? ` (retry after ${refusal.retryAfterMs}ms)` : ""}`];
131
+ }
74
132
  }
75
133
  ];
76
134
  }
@@ -78,12 +136,13 @@ async function runExperienceGatewayChecks(subject, checks = experienceGatewayChe
78
136
  const results = [];
79
137
  for (const check of checks) {
80
138
  try {
81
- results.push({ id: check.id, status: "passed", evidence: await check.run(subject) });
139
+ const evidence = await check.run(subject);
140
+ results.push({ id: check.id, status: evidence[0]?.startsWith("skipped:") ? "skipped" : "passed", evidence });
82
141
  } catch (error) {
83
142
  results.push({ id: check.id, status: "failed", evidence: [], error: error instanceof Error ? error.message : String(error) });
84
143
  }
85
144
  }
86
- return { passed: results.every((result) => result.status === "passed"), checks: results };
145
+ return { passed: results.every((result) => result.status !== "failed"), checks: results };
87
146
  }
88
147
  function createInitiatingReleaseEvidence() {
89
148
  const evidence = /* @__PURE__ */ new WeakMap();
@@ -115,6 +174,7 @@ function createExperienceGateway(options) {
115
174
  actorId: claims.subject,
116
175
  actorType: claims.actorType
117
176
  };
177
+ await admit(options.admission, { kind: "invoke", scope, claims, planId: input.request.planId, fragmentId: input.request.fragmentId, name: input.request.eventName });
118
178
  const resolved = await options.runtime.resolveIntent({
119
179
  planId: input.request.planId,
120
180
  fragmentId: input.request.fragmentId,
@@ -204,6 +264,7 @@ function createExperienceGateway(options) {
204
264
  actorId: claims.subject,
205
265
  actorType: claims.actorType
206
266
  };
267
+ await admit(options.admission, { kind: "read", scope, claims, planId: input.request.planId, fragmentId: input.request.fragmentId, name: input.request.bindingName });
207
268
  const resolved = await options.runtime.resolveView({
208
269
  planId: input.request.planId,
209
270
  fragmentId: input.request.fragmentId,
@@ -232,6 +293,11 @@ function createExperienceGateway(options) {
232
293
  };
233
294
  }
234
295
  var RFC3339 = /^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:[Zz]|[+-]\d{2}:\d{2})$/;
296
+ async function admit(admission, input) {
297
+ if (!admission) return;
298
+ const decision = await admission(input);
299
+ if (!decision.allowed) throw new ExperienceAdmissionError(decision.reason, decision.retryAfterMs);
300
+ }
235
301
  function assertCaptureMoment(capturedAt, now, maxAgeMs) {
236
302
  if (!RFC3339.test(capturedAt)) {
237
303
  throw new Error("Offline replay capturedAt must be an RFC 3339 instant with an explicit offset.");
@@ -249,8 +315,10 @@ function assertCaptureMoment(capturedAt, now, maxAgeMs) {
249
315
  }
250
316
  // Annotate the CommonJS export names for ESM import in node:
251
317
  0 && (module.exports = {
318
+ ExperienceAdmissionError,
252
319
  createExperienceGateway,
253
320
  createInitiatingReleaseEvidence,
321
+ createTokenBucketAdmission,
254
322
  experienceGatewayChecks,
255
323
  runExperienceGatewayChecks
256
324
  });
@@ -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\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 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}\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\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];\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\"; evidence: string[]; error?: string }> }> {\n\tconst results: Array<{ id: ExperienceGatewayCheck[\"id\"]; status: \"passed\" | \"failed\"; evidence: string[]; error?: string }> = [];\n\tfor (const check of checks) {\n\t\ttry { results.push({ id: check.id, status: \"passed\", evidence: await check.run(subject) }); }\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 === \"passed\"), 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\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\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\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 */\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,"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":[]}
package/dist/index.d.cts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { SubmitActionResult, SubmitActionInput, GovernedActionHost } from '@fabricorg/platform-host';
2
2
  import { ProjectionHost } from '@fabricorg/projection-host';
3
- import { IdentityPort } from '@fabricorg/ports';
3
+ import { ActorClaims, IdentityPort } from '@fabricorg/ports';
4
4
  import { ExperienceIntentRequest, ExperienceReadRequest } from '@fabricorg/experience-runtime';
5
- import { ExperienceServerRuntime, ExperienceScope } from '@fabricorg/experience-runtime/server';
5
+ import { ExperienceScope, ExperienceServerRuntime } from '@fabricorg/experience-runtime/server';
6
6
 
7
7
  interface TrustedExperienceRequestContext {
8
8
  /** Presented only to IdentityPort and never persisted or forwarded. */
@@ -36,6 +36,42 @@ interface ExperienceGateway {
36
36
  };
37
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;
47
+ }
48
+ type ExperienceAdmissionDecision = {
49
+ allowed: true;
50
+ } | {
51
+ allowed: false;
52
+ reason: string;
53
+ retryAfterMs?: number;
54
+ };
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;
62
+ }
63
+ /**
64
+ * A reference admission policy: a token bucket per key, refilled over time.
65
+ *
66
+ * Enough to throttle a tenant or an actor at the edge; a deployment with a
67
+ * shared limiter puts it behind the same option. Keys default to the tenant.
68
+ */
69
+ declare function createTokenBucketAdmission(options: {
70
+ capacity: number;
71
+ refillPerSecond: number;
72
+ key?(input: ExperienceAdmissionInput): string;
73
+ now?(): number;
74
+ }): (input: ExperienceAdmissionInput) => ExperienceAdmissionDecision;
39
75
  interface ExperienceGatewayConformanceSubject {
40
76
  gateway: ExperienceGateway;
41
77
  validIntent: Parameters<ExperienceGateway["invoke"]>[0];
@@ -54,9 +90,16 @@ interface ExperienceGatewayConformanceSubject {
54
90
  submitActionCount(): number;
55
91
  projectionCount(): number;
56
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];
57
100
  }
58
101
  interface ExperienceGatewayCheck {
59
- id: "fabric.experience.gateway-host-routing.v1" | "fabric.experience.gateway-scope-denial.v1" | "fabric.experience.gateway-projection-routing.v1";
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";
60
103
  run(subject: ExperienceGatewayConformanceSubject): Promise<string[]>;
61
104
  }
62
105
  declare function experienceGatewayChecks(): readonly ExperienceGatewayCheck[];
@@ -64,7 +107,7 @@ declare function runExperienceGatewayChecks(subject: ExperienceGatewayConformanc
64
107
  passed: boolean;
65
108
  checks: Array<{
66
109
  id: ExperienceGatewayCheck["id"];
67
- status: "passed" | "failed";
110
+ status: "passed" | "failed" | "skipped";
68
111
  evidence: string[];
69
112
  error?: string;
70
113
  }>;
@@ -98,6 +141,15 @@ declare function createExperienceGateway(options: {
98
141
  * a client-reported instant has no lower bound.
99
142
  */
100
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;
101
153
  captureAuthority?(input: {
102
154
  actionId: `${string}.${string}`;
103
155
  scope: ExperienceScope;
@@ -111,4 +163,4 @@ declare function createExperienceGateway(options: {
111
163
  now?: () => Date;
112
164
  }): ExperienceGateway;
113
165
 
114
- export { type ExperienceGateway, type ExperienceGatewayCheck, type ExperienceGatewayConformanceSubject, type InitiatingReleaseEvidence, type TrustedExperienceRequestContext, createExperienceGateway, createInitiatingReleaseEvidence, experienceGatewayChecks, runExperienceGatewayChecks };
166
+ export { type ExperienceAdmissionDecision, ExperienceAdmissionError, type ExperienceAdmissionInput, type ExperienceGateway, type ExperienceGatewayCheck, type ExperienceGatewayConformanceSubject, type InitiatingReleaseEvidence, type TrustedExperienceRequestContext, createExperienceGateway, createInitiatingReleaseEvidence, createTokenBucketAdmission, experienceGatewayChecks, runExperienceGatewayChecks };
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { SubmitActionResult, SubmitActionInput, GovernedActionHost } from '@fabricorg/platform-host';
2
2
  import { ProjectionHost } from '@fabricorg/projection-host';
3
- import { IdentityPort } from '@fabricorg/ports';
3
+ import { ActorClaims, IdentityPort } from '@fabricorg/ports';
4
4
  import { ExperienceIntentRequest, ExperienceReadRequest } from '@fabricorg/experience-runtime';
5
- import { ExperienceServerRuntime, ExperienceScope } from '@fabricorg/experience-runtime/server';
5
+ import { ExperienceScope, ExperienceServerRuntime } from '@fabricorg/experience-runtime/server';
6
6
 
7
7
  interface TrustedExperienceRequestContext {
8
8
  /** Presented only to IdentityPort and never persisted or forwarded. */
@@ -36,6 +36,42 @@ interface ExperienceGateway {
36
36
  };
37
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;
47
+ }
48
+ type ExperienceAdmissionDecision = {
49
+ allowed: true;
50
+ } | {
51
+ allowed: false;
52
+ reason: string;
53
+ retryAfterMs?: number;
54
+ };
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;
62
+ }
63
+ /**
64
+ * A reference admission policy: a token bucket per key, refilled over time.
65
+ *
66
+ * Enough to throttle a tenant or an actor at the edge; a deployment with a
67
+ * shared limiter puts it behind the same option. Keys default to the tenant.
68
+ */
69
+ declare function createTokenBucketAdmission(options: {
70
+ capacity: number;
71
+ refillPerSecond: number;
72
+ key?(input: ExperienceAdmissionInput): string;
73
+ now?(): number;
74
+ }): (input: ExperienceAdmissionInput) => ExperienceAdmissionDecision;
39
75
  interface ExperienceGatewayConformanceSubject {
40
76
  gateway: ExperienceGateway;
41
77
  validIntent: Parameters<ExperienceGateway["invoke"]>[0];
@@ -54,9 +90,16 @@ interface ExperienceGatewayConformanceSubject {
54
90
  submitActionCount(): number;
55
91
  projectionCount(): number;
56
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];
57
100
  }
58
101
  interface ExperienceGatewayCheck {
59
- id: "fabric.experience.gateway-host-routing.v1" | "fabric.experience.gateway-scope-denial.v1" | "fabric.experience.gateway-projection-routing.v1";
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";
60
103
  run(subject: ExperienceGatewayConformanceSubject): Promise<string[]>;
61
104
  }
62
105
  declare function experienceGatewayChecks(): readonly ExperienceGatewayCheck[];
@@ -64,7 +107,7 @@ declare function runExperienceGatewayChecks(subject: ExperienceGatewayConformanc
64
107
  passed: boolean;
65
108
  checks: Array<{
66
109
  id: ExperienceGatewayCheck["id"];
67
- status: "passed" | "failed";
110
+ status: "passed" | "failed" | "skipped";
68
111
  evidence: string[];
69
112
  error?: string;
70
113
  }>;
@@ -98,6 +141,15 @@ declare function createExperienceGateway(options: {
98
141
  * a client-reported instant has no lower bound.
99
142
  */
100
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;
101
153
  captureAuthority?(input: {
102
154
  actionId: `${string}.${string}`;
103
155
  scope: ExperienceScope;
@@ -111,4 +163,4 @@ declare function createExperienceGateway(options: {
111
163
  now?: () => Date;
112
164
  }): ExperienceGateway;
113
165
 
114
- export { type ExperienceGateway, type ExperienceGatewayCheck, type ExperienceGatewayConformanceSubject, type InitiatingReleaseEvidence, type TrustedExperienceRequestContext, createExperienceGateway, createInitiatingReleaseEvidence, experienceGatewayChecks, runExperienceGatewayChecks };
166
+ export { type ExperienceAdmissionDecision, ExperienceAdmissionError, type ExperienceAdmissionInput, type ExperienceGateway, type ExperienceGatewayCheck, type ExperienceGatewayConformanceSubject, type InitiatingReleaseEvidence, type TrustedExperienceRequestContext, createExperienceGateway, createInitiatingReleaseEvidence, createTokenBucketAdmission, experienceGatewayChecks, runExperienceGatewayChecks };
package/dist/index.js CHANGED
@@ -3,6 +3,42 @@ import { digestParameters } from "@fabricorg/platform-host";
3
3
  import {
4
4
  assertActorClaimsCoverScope
5
5
  } from "@fabricorg/ports";
6
+ var ExperienceAdmissionError = class extends Error {
7
+ constructor(reason, retryAfterMs) {
8
+ super(`Experience request not admitted: ${reason}`);
9
+ this.reason = reason;
10
+ this.retryAfterMs = retryAfterMs;
11
+ this.name = "ExperienceAdmissionError";
12
+ }
13
+ reason;
14
+ retryAfterMs;
15
+ code = "EXPERIENCE_ADMISSION_REFUSED";
16
+ get retryable() {
17
+ return this.retryAfterMs !== void 0;
18
+ }
19
+ };
20
+ function createTokenBucketAdmission(options) {
21
+ if (!(options.capacity >= 1) || !(options.refillPerSecond > 0)) throw new Error("Token bucket admission needs a capacity of at least one and a positive refill rate.");
22
+ const now = options.now ?? (() => Date.now());
23
+ const key = options.key ?? ((input) => input.scope.tenantId);
24
+ const buckets = /* @__PURE__ */ new Map();
25
+ return (input) => {
26
+ const at = now();
27
+ const id = key(input);
28
+ const bucket = buckets.get(id) ?? { tokens: options.capacity, at };
29
+ const elapsedMs = Math.max(0, at - bucket.at);
30
+ const refilled = Math.min(options.capacity, bucket.tokens + elapsedMs / 1e3 * options.refillPerSecond);
31
+ if (refilled >= 1) {
32
+ const remaining = refilled - 1;
33
+ if (remaining >= options.capacity) buckets.delete(id);
34
+ else buckets.set(id, { tokens: remaining, at });
35
+ return { allowed: true };
36
+ }
37
+ buckets.set(id, { tokens: refilled, at: Math.max(at, bucket.at) });
38
+ const retryAfterMs = Math.ceil((1 - refilled) / options.refillPerSecond * 1e3);
39
+ return { allowed: false, reason: `budget for "${id}" is exhausted`, retryAfterMs };
40
+ };
41
+ }
6
42
  function experienceGatewayChecks() {
7
43
  return [
8
44
  {
@@ -46,6 +82,26 @@ function experienceGatewayChecks() {
46
82
  if (!rejected) throw new Error("Gateway accepted an intent outside trusted identity scope.");
47
83
  return ["out-of-scope intent rejected"];
48
84
  }
85
+ },
86
+ {
87
+ id: "fabric.experience.gateway-admission-refusal.v1",
88
+ async run(subject) {
89
+ if (!subject.throttledIntent) return ["skipped: the subject supplies no throttledIntent, so admission is not proven"];
90
+ const before = subject.observed.submitActionCount();
91
+ let refusal;
92
+ try {
93
+ await subject.gateway.invoke(subject.throttledIntent);
94
+ } catch (error) {
95
+ refusal = error;
96
+ }
97
+ if (!(refusal instanceof ExperienceAdmissionError)) {
98
+ throw new Error("Gateway admitted an intent the deployment's admission policy refuses, or refused it as something other than ExperienceAdmissionError.");
99
+ }
100
+ if (subject.observed.submitActionCount() !== before) {
101
+ throw new Error("A refused intent still reached PlatformHost.submitAction; admission must decide before anything governed is touched.");
102
+ }
103
+ return [`admission refused: ${refusal.reason}${refusal.retryable ? ` (retry after ${refusal.retryAfterMs}ms)` : ""}`];
104
+ }
49
105
  }
50
106
  ];
51
107
  }
@@ -53,12 +109,13 @@ async function runExperienceGatewayChecks(subject, checks = experienceGatewayChe
53
109
  const results = [];
54
110
  for (const check of checks) {
55
111
  try {
56
- results.push({ id: check.id, status: "passed", evidence: await check.run(subject) });
112
+ const evidence = await check.run(subject);
113
+ results.push({ id: check.id, status: evidence[0]?.startsWith("skipped:") ? "skipped" : "passed", evidence });
57
114
  } catch (error) {
58
115
  results.push({ id: check.id, status: "failed", evidence: [], error: error instanceof Error ? error.message : String(error) });
59
116
  }
60
117
  }
61
- return { passed: results.every((result) => result.status === "passed"), checks: results };
118
+ return { passed: results.every((result) => result.status !== "failed"), checks: results };
62
119
  }
63
120
  function createInitiatingReleaseEvidence() {
64
121
  const evidence = /* @__PURE__ */ new WeakMap();
@@ -90,6 +147,7 @@ function createExperienceGateway(options) {
90
147
  actorId: claims.subject,
91
148
  actorType: claims.actorType
92
149
  };
150
+ await admit(options.admission, { kind: "invoke", scope, claims, planId: input.request.planId, fragmentId: input.request.fragmentId, name: input.request.eventName });
93
151
  const resolved = await options.runtime.resolveIntent({
94
152
  planId: input.request.planId,
95
153
  fragmentId: input.request.fragmentId,
@@ -179,6 +237,7 @@ function createExperienceGateway(options) {
179
237
  actorId: claims.subject,
180
238
  actorType: claims.actorType
181
239
  };
240
+ await admit(options.admission, { kind: "read", scope, claims, planId: input.request.planId, fragmentId: input.request.fragmentId, name: input.request.bindingName });
182
241
  const resolved = await options.runtime.resolveView({
183
242
  planId: input.request.planId,
184
243
  fragmentId: input.request.fragmentId,
@@ -207,6 +266,11 @@ function createExperienceGateway(options) {
207
266
  };
208
267
  }
209
268
  var RFC3339 = /^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:[Zz]|[+-]\d{2}:\d{2})$/;
269
+ async function admit(admission, input) {
270
+ if (!admission) return;
271
+ const decision = await admission(input);
272
+ if (!decision.allowed) throw new ExperienceAdmissionError(decision.reason, decision.retryAfterMs);
273
+ }
210
274
  function assertCaptureMoment(capturedAt, now, maxAgeMs) {
211
275
  if (!RFC3339.test(capturedAt)) {
212
276
  throw new Error("Offline replay capturedAt must be an RFC 3339 instant with an explicit offset.");
@@ -223,8 +287,10 @@ function assertCaptureMoment(capturedAt, now, maxAgeMs) {
223
287
  return new Date(parsed).toISOString();
224
288
  }
225
289
  export {
290
+ ExperienceAdmissionError,
226
291
  createExperienceGateway,
227
292
  createInitiatingReleaseEvidence,
293
+ createTokenBucketAdmission,
228
294
  experienceGatewayChecks,
229
295
  runExperienceGatewayChecks
230
296
  };
package/dist/index.js.map CHANGED
@@ -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\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 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}\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\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];\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\"; evidence: string[]; error?: string }> }> {\n\tconst results: Array<{ id: ExperienceGatewayCheck[\"id\"]; status: \"passed\" | \"failed\"; evidence: string[]; error?: string }> = [];\n\tfor (const check of checks) {\n\t\ttry { results.push({ id: check.id, status: \"passed\", evidence: await check.run(subject) }); }\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 === \"passed\"), 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\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\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\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 */\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,"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":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fabricorg/experience-gateway",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "Trusted server adapter from scoped experience plans to governed Fabric Hosts.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -26,12 +26,12 @@
26
26
  "LICENSE"
27
27
  ],
28
28
  "dependencies": {
29
- "@fabricorg/experience-runtime": "^0.3.0"
29
+ "@fabricorg/experience-runtime": "^0.4.2"
30
30
  },
31
31
  "peerDependencies": {
32
- "@fabricorg/platform-host": "^7.0.0",
32
+ "@fabricorg/platform-host": "^7.2.0",
33
33
  "@fabricorg/projection-host": "^1.1.0",
34
- "@fabricorg/ports": "^0.5.0"
34
+ "@fabricorg/ports": "^0.5.4"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/node": "^22.10.0",
@@ -39,8 +39,8 @@
39
39
  "typescript": "^5.7.3",
40
40
  "vitest": "^4.1.5",
41
41
  "@fabricorg/projection-host": "^1.1.0",
42
- "@fabricorg/platform-host": "^7.0.0",
43
- "@fabricorg/ports": "^0.5.0"
42
+ "@fabricorg/ports": "^0.5.4",
43
+ "@fabricorg/platform-host": "^7.2.0"
44
44
  },
45
45
  "engines": {
46
46
  "node": ">=20"