@fabricorg/experience-gateway 1.0.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 ADDED
@@ -0,0 +1,101 @@
1
+ # Changelog
2
+
3
+ ## 1.0.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 337e89c: Make offline replay truthful and give three conformance suites teeth.
8
+
9
+ Offline replay carried no marker and dropped the capture moment. An adapter had to remember to set
10
+ `executionReason` for the gateway to apply offline eligibility rules, and capture-authority evidence
11
+ was stamped at receipt, so it attested a moment nobody was there for. `ExperienceReplayRequest` now
12
+ carries `executionReason` and `capturedAt` structurally, and the gateway uses the supplied instant,
13
+ bounded so it must parse and must not be in the future.
14
+
15
+ Replay resolved to `completed` or `failed`, which collapsed three outcomes the platform deliberately
16
+ distinguishes and kept every failure queued. A capture the platform had already applied counted as a
17
+ failure and retried, when replaying an idempotent action twice is the mechanism working; a capture
18
+ refused on authority grounds retried until its TTL rather than surfacing. `replay()` now returns
19
+ `reconciled` alongside the counts, holding each settled refusal and the reason given. Unrecognised
20
+ errors stay transient, so an unfamiliar failure is retried rather than discarded.
21
+
22
+ Gateway conformance accepted any result carrying an `actionInvocationId`, so a replacement that
23
+ fabricated an id and queried data directly passed; the subject now reports what the governed hosts
24
+ observed, and reads are certified through `ProjectionHost`. Component-pack certification accepted one
25
+ placeholder for every declared component and could not see a fresh function per call, because every
26
+ function stringifies to `undefined`. Renderers must now be distinct per component, undeclared names
27
+ must resolve to nothing, and determinism compares identity for anything not JSON-representable.
28
+
29
+ - a598fb9: Close the remaining review findings.
30
+
31
+ **One capability-reference grammar.** `sdui-release` accepted any name after the namespace while
32
+ `sdui-composer` rejected `/`, so a view named `orders/reports/daily` produced a release that validated
33
+ and then died at promotion with nothing wrong upstream to point at. Both now share
34
+ `parseCapabilityRef`, tightened to lowercase path segments matching what the platform permits for a
35
+ view name. Two views in one contract that reduce to the same reference are reported as
36
+ `ambiguous_capability_reference` at validation rather than surfacing as an ambiguous route later.
37
+
38
+ **A bounded replay window.** A client-supplied `capturedAt` had no lower bound. Deferring it to the
39
+ Host looked safe and was not: the Host checks staleness only when the capture-authority adapter
40
+ supplies an `expiresAt`, and skips the execution-time expiry check entirely for actions governed at
41
+ capture. A gateway that accepts replay must now declare `maxOfflineReplayAgeMs`, and the instant must
42
+ be RFC 3339 with an explicit offset.
43
+
44
+ **One token-set grammar.** `document.tokenSet` accepted names and versions that `supportedTokenSets`
45
+ rejected, while validation treats the two fields as interchangeable.
46
+
47
+ **Port requirements narrow as they are checked.** A requirement for a standard _and_ a version could
48
+ be satisfied by two different adapters covering one constraint each. `requirement.standard.version`
49
+ was declared and never read.
50
+
51
+ - 9b2e55b: Close what a fourth review found: each earlier fix closed one instance of a class and left its
52
+ siblings.
53
+
54
+ **Replay disposition is now exhaustive over `ActionStatus`.** The previous fix enumerated the two
55
+ statuses that looked like refusals, so `failed`, `validation_failed` and `waiting_for_approval` fell
56
+ through and were deleted from the queue and reported to the operator as applied — the same bug the
57
+ fix set out to kill, one layer in. Any returned outcome now means the platform holds the invocation,
58
+ so the capture leaves the queue either way; what differs is whether a person needs to hear about it.
59
+ An undeterminable adapter outcome is reported as `unknown` rather than `refused`, because the effect
60
+ may well have happened. `isSettledReplayFailure` learned the refusals the sibling commit introduced,
61
+ including a capture rejected for age, which can never come good.
62
+
63
+ **A replay without `capturedAt` is refused.** It was reachable only through an untyped cast, so
64
+ omitting it skipped the age check entirely and stamped receipt time into durable capture evidence.
65
+
66
+ **A capability can name a namespaced port.** `definePort` required a dot and the manifest validator
67
+ forbade one, so no real capability could name a defined port and the certification gate was
68
+ unreachable. Certification is also checked structurally rather than by the presence of a property, so
69
+ a hand-written claim no longer satisfies the gate that exists to demand evidence, and a port
70
+ requirement narrows its candidates as each constraint is applied.
71
+
72
+ **`assertPortSuiteHasTeeth` names every vacuous check** rather than returning on the first one that
73
+ bites, and a reference port is actually run through it. The payments control asserts its result, so
74
+ an adapter that never charges no longer certifies. The capability-reference grammar matches the
75
+ platform's own view-name rule instead of being stricter than it, and ambiguity is counted by
76
+ occurrence across views and intents rather than by comparing names within one contract.
77
+
78
+ ### Patch Changes
79
+
80
+ - Updated dependencies [8ce5e06]
81
+ - Updated dependencies [337e89c]
82
+ - Updated dependencies [a598fb9]
83
+ - Updated dependencies [9a5b18a]
84
+ - Updated dependencies [9b2e55b]
85
+ - Updated dependencies [0d1f6f6]
86
+ - @fabricorg/ports@0.4.0
87
+ - @fabricorg/experience-runtime@0.2.0
88
+ - @fabricorg/platform-host@7.0.0
89
+
90
+ ## 0.1.0
91
+
92
+ ### Minor Changes
93
+
94
+ - Add trusted view and intent routing, exact action-version enforcement,
95
+ capture-authority adapters, public conformance, and non-forgeable initiating-release
96
+ evidence.
97
+
98
+ ### Patch Changes
99
+
100
+ - Updated dependencies
101
+ - @fabricorg/experience-runtime@0.1.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Fabric
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,17 @@
1
+ # `@fabricorg/experience-gateway`
2
+
3
+ Trusted server adapter between an experience runtime and Fabric Hosts. It verifies
4
+ identity and scope through `IdentityPort`, resolves only server-held plan handles,
5
+ routes reads through ProjectionHost, and submits mutations through PlatformHost.
6
+
7
+ Configure PlatformHost's `composition.resolveInitiatingReleaseDigest` with
8
+ `createInitiatingReleaseEvidence().resolve`. Release evidence is held in an
9
+ instance-scoped WeakMap and is never accepted from browser request data.
10
+
11
+ Provide `actionVersion` from the registered application module set so pinned release
12
+ routes cannot silently invoke another action version. Actions governed at the
13
+ `capture` authority moment additionally require a trusted `captureAuthority` adapter;
14
+ the browser cannot manufacture an authorization binding.
15
+
16
+ Replacement gateways can run `experienceGatewayChecks` as a public compatibility
17
+ suite.
package/dist/index.cjs ADDED
@@ -0,0 +1,257 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ createExperienceGateway: () => createExperienceGateway,
24
+ createInitiatingReleaseEvidence: () => createInitiatingReleaseEvidence,
25
+ experienceGatewayChecks: () => experienceGatewayChecks,
26
+ runExperienceGatewayChecks: () => runExperienceGatewayChecks
27
+ });
28
+ module.exports = __toCommonJS(index_exports);
29
+ var import_platform_host = require("@fabricorg/platform-host");
30
+ var import_ports = require("@fabricorg/ports");
31
+ function experienceGatewayChecks() {
32
+ return [
33
+ {
34
+ id: "fabric.experience.gateway-host-routing.v1",
35
+ async run(subject) {
36
+ const before = subject.observed.submitActionCount();
37
+ const result = await subject.gateway.invoke(subject.validIntent);
38
+ if (!result.actionInvocationId) throw new Error("Gateway did not return a governed Host invocation.");
39
+ const submitted = subject.observed.submitActionCount() - before;
40
+ if (submitted !== 1) {
41
+ throw new Error(
42
+ `Gateway returned an invocation id after ${submitted} calls to PlatformHost.submitAction; a mutation must reach the Host exactly once.`
43
+ );
44
+ }
45
+ return [result.actionInvocationId];
46
+ }
47
+ },
48
+ {
49
+ id: "fabric.experience.gateway-projection-routing.v1",
50
+ async run(subject) {
51
+ const before = subject.observed.projectionCount();
52
+ await subject.gateway.read(subject.validRead);
53
+ const projected = subject.observed.projectionCount() - before;
54
+ if (projected !== 1) {
55
+ throw new Error(
56
+ `Gateway served a read after ${projected} calls to ProjectionHost.project; a read must go through the projection host.`
57
+ );
58
+ }
59
+ return ["read routed through ProjectionHost"];
60
+ }
61
+ },
62
+ {
63
+ id: "fabric.experience.gateway-scope-denial.v1",
64
+ async run(subject) {
65
+ let rejected = false;
66
+ try {
67
+ await subject.gateway.invoke(subject.deniedIntent);
68
+ } catch {
69
+ rejected = true;
70
+ }
71
+ if (!rejected) throw new Error("Gateway accepted an intent outside trusted identity scope.");
72
+ return ["out-of-scope intent rejected"];
73
+ }
74
+ }
75
+ ];
76
+ }
77
+ async function runExperienceGatewayChecks(subject, checks = experienceGatewayChecks()) {
78
+ const results = [];
79
+ for (const check of checks) {
80
+ try {
81
+ results.push({ id: check.id, status: "passed", evidence: await check.run(subject) });
82
+ } catch (error) {
83
+ results.push({ id: check.id, status: "failed", evidence: [], error: error instanceof Error ? error.message : String(error) });
84
+ }
85
+ }
86
+ return { passed: results.every((result) => result.status === "passed"), checks: results };
87
+ }
88
+ function createInitiatingReleaseEvidence() {
89
+ const evidence = /* @__PURE__ */ new WeakMap();
90
+ return {
91
+ bind(input, releaseDigest) {
92
+ evidence.set(input, releaseDigest);
93
+ },
94
+ resolve(input) {
95
+ return evidence.get(input);
96
+ },
97
+ release(input) {
98
+ evidence.delete(input);
99
+ }
100
+ };
101
+ }
102
+ function createExperienceGateway(options) {
103
+ const now = options.now ?? (() => /* @__PURE__ */ new Date());
104
+ return {
105
+ async invoke(input) {
106
+ const claims = await options.identity.verify(input.context.credential);
107
+ if (!claims) throw new Error("Experience request credential is not authenticated.");
108
+ (0, import_ports.assertActorClaimsCoverScope)(claims, {
109
+ tenantId: input.context.tenantId,
110
+ spaceId: input.context.spaceId
111
+ }, now());
112
+ const scope = {
113
+ tenantId: input.context.tenantId,
114
+ spaceId: input.context.spaceId,
115
+ actorId: claims.subject,
116
+ actorType: claims.actorType
117
+ };
118
+ const resolved = await options.runtime.resolveIntent({
119
+ planId: input.request.planId,
120
+ fragmentId: input.request.fragmentId,
121
+ eventName: input.request.eventName,
122
+ scope
123
+ });
124
+ if (resolved.assemblyDigest !== options.assemblyDigest) {
125
+ throw new Error("Experience render plan belongs to a different application assembly.");
126
+ }
127
+ if (options.actionVersion(resolved.actionId) !== resolved.actionVersion) {
128
+ throw new Error(`Experience intent route for "${resolved.actionId}" does not match the registered action version.`);
129
+ }
130
+ if (resolved.execution?.idempotency === "required" && !input.request.idempotencyKey) {
131
+ throw new Error(`Action "${resolved.actionId}" requires an idempotency key.`);
132
+ }
133
+ if (input.executionReason === "offline_replay" && resolved.execution?.connectivity !== "offline-capturable") {
134
+ throw new Error(`Action "${resolved.actionId}" is not eligible for offline replay.`);
135
+ }
136
+ const correlationId = options.nextCorrelationId();
137
+ let capturedAt;
138
+ if (input.executionReason === "offline_replay") {
139
+ const supplied = input.capturedAt ?? input.request.capturedAt;
140
+ if (typeof supplied !== "string") {
141
+ throw new Error("Offline replay requires capturedAt; receipt time cannot attest when the operator acted.");
142
+ }
143
+ capturedAt = assertCaptureMoment(supplied, now(), options.maxOfflineReplayAgeMs);
144
+ } else {
145
+ capturedAt = now().toISOString();
146
+ }
147
+ const parameterDigest = (0, import_platform_host.digestParameters)(input.request.parameters);
148
+ const capturedAuthority = resolved.execution?.authorityMoment === "capture" ? await options.captureAuthority?.({
149
+ actionId: resolved.actionId,
150
+ scope,
151
+ parameters: input.request.parameters,
152
+ parameterDigest,
153
+ capturedAt
154
+ }) : void 0;
155
+ if (resolved.execution?.authorityMoment === "capture" && !capturedAuthority) {
156
+ throw new Error(`Action "${resolved.actionId}" requires a trusted capture-authority adapter.`);
157
+ }
158
+ const submission = {
159
+ actionId: resolved.actionId,
160
+ parameters: input.request.parameters,
161
+ tenantId: scope.tenantId,
162
+ spaceId: scope.spaceId,
163
+ actorId: scope.actorId,
164
+ actorType: claims.actorType,
165
+ correlationId,
166
+ provenance: {
167
+ source: "ui",
168
+ correlationId,
169
+ ...resolved.execution?.sensitivity === "restricted" ? {} : { auditAttributes: { "fabric.experience.fragment": input.request.fragmentId } }
170
+ },
171
+ ...input.request.idempotencyKey === void 0 ? {} : { idempotencyKey: input.request.idempotencyKey },
172
+ ...input.executionReason === void 0 ? {} : { executionReason: input.executionReason },
173
+ ...capturedAuthority === void 0 ? {} : {
174
+ authorizationBindingId: capturedAuthority.id,
175
+ authorizationBinding: {
176
+ id: capturedAuthority.id,
177
+ governingMoment: "capture",
178
+ capturedAt,
179
+ ...capturedAuthority.expiresAt === void 0 ? {} : { expiresAt: capturedAuthority.expiresAt },
180
+ tenantId: scope.tenantId,
181
+ actorId: scope.actorId,
182
+ actionId: resolved.actionId,
183
+ parameterDigest
184
+ }
185
+ }
186
+ };
187
+ options.evidence.bind(submission, resolved.initiatingReleaseDigest);
188
+ try {
189
+ return await options.host.submitAction(submission);
190
+ } finally {
191
+ options.evidence.release(submission);
192
+ }
193
+ },
194
+ async read(input) {
195
+ const claims = await options.identity.verify(input.context.credential);
196
+ if (!claims) throw new Error("Experience request credential is not authenticated.");
197
+ (0, import_ports.assertActorClaimsCoverScope)(claims, {
198
+ tenantId: input.context.tenantId,
199
+ spaceId: input.context.spaceId
200
+ }, now());
201
+ const scope = {
202
+ tenantId: input.context.tenantId,
203
+ spaceId: input.context.spaceId,
204
+ actorId: claims.subject,
205
+ actorType: claims.actorType
206
+ };
207
+ const resolved = await options.runtime.resolveView({
208
+ planId: input.request.planId,
209
+ fragmentId: input.request.fragmentId,
210
+ bindingName: input.request.bindingName,
211
+ scope
212
+ });
213
+ if (resolved.assemblyDigest !== options.assemblyDigest) {
214
+ throw new Error("Experience render plan belongs to a different application assembly.");
215
+ }
216
+ const result = await options.projectionHost.project({
217
+ view: { name: resolved.name, version: resolved.version },
218
+ scope: { tenantId: scope.tenantId, spaceId: scope.spaceId },
219
+ actor: { id: scope.actorId, type: claims.actorType },
220
+ parameters: input.request.parameters
221
+ });
222
+ return {
223
+ data: result.state,
224
+ evidence: {
225
+ releaseDigest: resolved.initiatingReleaseDigest,
226
+ assemblyDigest: resolved.assemblyDigest,
227
+ eventCursor: result.eventCursor,
228
+ source: result.source
229
+ }
230
+ };
231
+ }
232
+ };
233
+ }
234
+ var RFC3339 = /^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:[Zz]|[+-]\d{2}:\d{2})$/;
235
+ function assertCaptureMoment(capturedAt, now, maxAgeMs) {
236
+ if (!RFC3339.test(capturedAt)) {
237
+ throw new Error("Offline replay capturedAt must be an RFC 3339 instant with an explicit offset.");
238
+ }
239
+ const parsed = Date.parse(capturedAt);
240
+ if (Number.isNaN(parsed)) throw new Error("Offline replay capturedAt is not a parseable instant.");
241
+ if (parsed > now.getTime()) throw new Error("Offline replay capturedAt is in the future.");
242
+ if (maxAgeMs === void 0) {
243
+ throw new Error("Gateway accepts offline replay but declares no maxOfflineReplayAgeMs, so a capture has no lower bound.");
244
+ }
245
+ if (now.getTime() - parsed > maxAgeMs) {
246
+ throw new Error(`Offline replay capturedAt is older than the ${maxAgeMs}ms this gateway accepts.`);
247
+ }
248
+ return new Date(parsed).toISOString();
249
+ }
250
+ // Annotate the CommonJS export names for ESM import in node:
251
+ 0 && (module.exports = {
252
+ createExperienceGateway,
253
+ createInitiatingReleaseEvidence,
254
+ experienceGatewayChecks,
255
+ runExperienceGatewayChecks
256
+ });
257
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +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":[]}
@@ -0,0 +1,114 @@
1
+ import { SubmitActionResult, SubmitActionInput, GovernedActionHost } from '@fabricorg/platform-host';
2
+ import { ProjectionHost } from '@fabricorg/projection-host';
3
+ import { IdentityPort } from '@fabricorg/ports';
4
+ import { ExperienceIntentRequest, ExperienceReadRequest } from '@fabricorg/experience-runtime';
5
+ import { ExperienceServerRuntime, ExperienceScope } from '@fabricorg/experience-runtime/server';
6
+
7
+ interface TrustedExperienceRequestContext {
8
+ /** Presented only to IdentityPort and never persisted or forwarded. */
9
+ credential: string;
10
+ tenantId: string;
11
+ spaceId: string;
12
+ }
13
+ interface ExperienceGateway {
14
+ invoke(input: {
15
+ context: TrustedExperienceRequestContext;
16
+ request: ExperienceIntentRequest;
17
+ executionReason?: "initial" | "offline_replay";
18
+ /**
19
+ * When the operator acted, for a replay. Required whenever
20
+ * `executionReason` is `offline_replay`: reading it through an untyped
21
+ * cast made omitting it the easy mistake, and omitting it silently
22
+ * stamped receipt time into durable capture evidence.
23
+ */
24
+ capturedAt?: string;
25
+ }): Promise<SubmitActionResult>;
26
+ read(input: {
27
+ context: TrustedExperienceRequestContext;
28
+ request: ExperienceReadRequest;
29
+ }): Promise<{
30
+ data: unknown;
31
+ evidence: {
32
+ releaseDigest: string;
33
+ assemblyDigest: string;
34
+ eventCursor: string | null;
35
+ source: "cache" | "replay";
36
+ };
37
+ }>;
38
+ }
39
+ interface ExperienceGatewayConformanceSubject {
40
+ gateway: ExperienceGateway;
41
+ validIntent: Parameters<ExperienceGateway["invoke"]>[0];
42
+ deniedIntent: Parameters<ExperienceGateway["invoke"]>[0];
43
+ validRead: Parameters<ExperienceGateway["read"]>[0];
44
+ /**
45
+ * What the governed hosts actually saw.
46
+ *
47
+ * A returned invocation id proves only that a string was produced. The
48
+ * point of a gateway is that mutations reach `PlatformHost.submitAction` and
49
+ * reads reach `ProjectionHost.project`; a replacement that fabricates an id
50
+ * and queries the database directly satisfies every observable contract
51
+ * except this one, so the subject has to report the calls.
52
+ */
53
+ observed: {
54
+ submitActionCount(): number;
55
+ projectionCount(): number;
56
+ };
57
+ }
58
+ interface ExperienceGatewayCheck {
59
+ id: "fabric.experience.gateway-host-routing.v1" | "fabric.experience.gateway-scope-denial.v1" | "fabric.experience.gateway-projection-routing.v1";
60
+ run(subject: ExperienceGatewayConformanceSubject): Promise<string[]>;
61
+ }
62
+ declare function experienceGatewayChecks(): readonly ExperienceGatewayCheck[];
63
+ declare function runExperienceGatewayChecks(subject: ExperienceGatewayConformanceSubject, checks?: readonly ExperienceGatewayCheck[]): Promise<{
64
+ passed: boolean;
65
+ checks: Array<{
66
+ id: ExperienceGatewayCheck["id"];
67
+ status: "passed" | "failed";
68
+ evidence: string[];
69
+ error?: string;
70
+ }>;
71
+ }>;
72
+ interface InitiatingReleaseEvidence {
73
+ bind(input: SubmitActionInput, releaseDigest: string): void;
74
+ resolve(input: SubmitActionInput): string | undefined;
75
+ release(input: SubmitActionInput): void;
76
+ }
77
+ /**
78
+ * Creates an instance-scoped, non-serializable evidence seam. The Host's
79
+ * `composition.resolveInitiatingReleaseDigest` reads this WeakMap using the
80
+ * exact submission object. A browser field cannot populate or forge it.
81
+ */
82
+ declare function createInitiatingReleaseEvidence(): InitiatingReleaseEvidence;
83
+ declare function createExperienceGateway(options: {
84
+ identity: IdentityPort;
85
+ runtime: ExperienceServerRuntime;
86
+ host: Pick<GovernedActionHost, "submitAction">;
87
+ projectionHost: Pick<ProjectionHost, "project">;
88
+ evidence: InitiatingReleaseEvidence;
89
+ assemblyDigest: string;
90
+ actionVersion(actionId: `${string}.${string}`): number | undefined;
91
+ nextCorrelationId(): string;
92
+ /**
93
+ * How old an offline capture may be and still replay.
94
+ *
95
+ * Required to accept replay at all. The Host's own staleness check is
96
+ * conditional on the capture-authority adapter supplying an `expiresAt`, and
97
+ * is skipped entirely for actions governed at capture time, so without this
98
+ * a client-reported instant has no lower bound.
99
+ */
100
+ maxOfflineReplayAgeMs?: number;
101
+ captureAuthority?(input: {
102
+ actionId: `${string}.${string}`;
103
+ scope: ExperienceScope;
104
+ parameters: Record<string, unknown>;
105
+ parameterDigest: string;
106
+ capturedAt: string;
107
+ }): Promise<{
108
+ id: string;
109
+ expiresAt?: string;
110
+ }>;
111
+ now?: () => Date;
112
+ }): ExperienceGateway;
113
+
114
+ export { type ExperienceGateway, type ExperienceGatewayCheck, type ExperienceGatewayConformanceSubject, type InitiatingReleaseEvidence, type TrustedExperienceRequestContext, createExperienceGateway, createInitiatingReleaseEvidence, experienceGatewayChecks, runExperienceGatewayChecks };
@@ -0,0 +1,114 @@
1
+ import { SubmitActionResult, SubmitActionInput, GovernedActionHost } from '@fabricorg/platform-host';
2
+ import { ProjectionHost } from '@fabricorg/projection-host';
3
+ import { IdentityPort } from '@fabricorg/ports';
4
+ import { ExperienceIntentRequest, ExperienceReadRequest } from '@fabricorg/experience-runtime';
5
+ import { ExperienceServerRuntime, ExperienceScope } from '@fabricorg/experience-runtime/server';
6
+
7
+ interface TrustedExperienceRequestContext {
8
+ /** Presented only to IdentityPort and never persisted or forwarded. */
9
+ credential: string;
10
+ tenantId: string;
11
+ spaceId: string;
12
+ }
13
+ interface ExperienceGateway {
14
+ invoke(input: {
15
+ context: TrustedExperienceRequestContext;
16
+ request: ExperienceIntentRequest;
17
+ executionReason?: "initial" | "offline_replay";
18
+ /**
19
+ * When the operator acted, for a replay. Required whenever
20
+ * `executionReason` is `offline_replay`: reading it through an untyped
21
+ * cast made omitting it the easy mistake, and omitting it silently
22
+ * stamped receipt time into durable capture evidence.
23
+ */
24
+ capturedAt?: string;
25
+ }): Promise<SubmitActionResult>;
26
+ read(input: {
27
+ context: TrustedExperienceRequestContext;
28
+ request: ExperienceReadRequest;
29
+ }): Promise<{
30
+ data: unknown;
31
+ evidence: {
32
+ releaseDigest: string;
33
+ assemblyDigest: string;
34
+ eventCursor: string | null;
35
+ source: "cache" | "replay";
36
+ };
37
+ }>;
38
+ }
39
+ interface ExperienceGatewayConformanceSubject {
40
+ gateway: ExperienceGateway;
41
+ validIntent: Parameters<ExperienceGateway["invoke"]>[0];
42
+ deniedIntent: Parameters<ExperienceGateway["invoke"]>[0];
43
+ validRead: Parameters<ExperienceGateway["read"]>[0];
44
+ /**
45
+ * What the governed hosts actually saw.
46
+ *
47
+ * A returned invocation id proves only that a string was produced. The
48
+ * point of a gateway is that mutations reach `PlatformHost.submitAction` and
49
+ * reads reach `ProjectionHost.project`; a replacement that fabricates an id
50
+ * and queries the database directly satisfies every observable contract
51
+ * except this one, so the subject has to report the calls.
52
+ */
53
+ observed: {
54
+ submitActionCount(): number;
55
+ projectionCount(): number;
56
+ };
57
+ }
58
+ interface ExperienceGatewayCheck {
59
+ id: "fabric.experience.gateway-host-routing.v1" | "fabric.experience.gateway-scope-denial.v1" | "fabric.experience.gateway-projection-routing.v1";
60
+ run(subject: ExperienceGatewayConformanceSubject): Promise<string[]>;
61
+ }
62
+ declare function experienceGatewayChecks(): readonly ExperienceGatewayCheck[];
63
+ declare function runExperienceGatewayChecks(subject: ExperienceGatewayConformanceSubject, checks?: readonly ExperienceGatewayCheck[]): Promise<{
64
+ passed: boolean;
65
+ checks: Array<{
66
+ id: ExperienceGatewayCheck["id"];
67
+ status: "passed" | "failed";
68
+ evidence: string[];
69
+ error?: string;
70
+ }>;
71
+ }>;
72
+ interface InitiatingReleaseEvidence {
73
+ bind(input: SubmitActionInput, releaseDigest: string): void;
74
+ resolve(input: SubmitActionInput): string | undefined;
75
+ release(input: SubmitActionInput): void;
76
+ }
77
+ /**
78
+ * Creates an instance-scoped, non-serializable evidence seam. The Host's
79
+ * `composition.resolveInitiatingReleaseDigest` reads this WeakMap using the
80
+ * exact submission object. A browser field cannot populate or forge it.
81
+ */
82
+ declare function createInitiatingReleaseEvidence(): InitiatingReleaseEvidence;
83
+ declare function createExperienceGateway(options: {
84
+ identity: IdentityPort;
85
+ runtime: ExperienceServerRuntime;
86
+ host: Pick<GovernedActionHost, "submitAction">;
87
+ projectionHost: Pick<ProjectionHost, "project">;
88
+ evidence: InitiatingReleaseEvidence;
89
+ assemblyDigest: string;
90
+ actionVersion(actionId: `${string}.${string}`): number | undefined;
91
+ nextCorrelationId(): string;
92
+ /**
93
+ * How old an offline capture may be and still replay.
94
+ *
95
+ * Required to accept replay at all. The Host's own staleness check is
96
+ * conditional on the capture-authority adapter supplying an `expiresAt`, and
97
+ * is skipped entirely for actions governed at capture time, so without this
98
+ * a client-reported instant has no lower bound.
99
+ */
100
+ maxOfflineReplayAgeMs?: number;
101
+ captureAuthority?(input: {
102
+ actionId: `${string}.${string}`;
103
+ scope: ExperienceScope;
104
+ parameters: Record<string, unknown>;
105
+ parameterDigest: string;
106
+ capturedAt: string;
107
+ }): Promise<{
108
+ id: string;
109
+ expiresAt?: string;
110
+ }>;
111
+ now?: () => Date;
112
+ }): ExperienceGateway;
113
+
114
+ export { type ExperienceGateway, type ExperienceGatewayCheck, type ExperienceGatewayConformanceSubject, type InitiatingReleaseEvidence, type TrustedExperienceRequestContext, createExperienceGateway, createInitiatingReleaseEvidence, experienceGatewayChecks, runExperienceGatewayChecks };
package/dist/index.js ADDED
@@ -0,0 +1,231 @@
1
+ // index.ts
2
+ import { digestParameters } from "@fabricorg/platform-host";
3
+ import {
4
+ assertActorClaimsCoverScope
5
+ } from "@fabricorg/ports";
6
+ function experienceGatewayChecks() {
7
+ return [
8
+ {
9
+ id: "fabric.experience.gateway-host-routing.v1",
10
+ async run(subject) {
11
+ const before = subject.observed.submitActionCount();
12
+ const result = await subject.gateway.invoke(subject.validIntent);
13
+ if (!result.actionInvocationId) throw new Error("Gateway did not return a governed Host invocation.");
14
+ const submitted = subject.observed.submitActionCount() - before;
15
+ if (submitted !== 1) {
16
+ throw new Error(
17
+ `Gateway returned an invocation id after ${submitted} calls to PlatformHost.submitAction; a mutation must reach the Host exactly once.`
18
+ );
19
+ }
20
+ return [result.actionInvocationId];
21
+ }
22
+ },
23
+ {
24
+ id: "fabric.experience.gateway-projection-routing.v1",
25
+ async run(subject) {
26
+ const before = subject.observed.projectionCount();
27
+ await subject.gateway.read(subject.validRead);
28
+ const projected = subject.observed.projectionCount() - before;
29
+ if (projected !== 1) {
30
+ throw new Error(
31
+ `Gateway served a read after ${projected} calls to ProjectionHost.project; a read must go through the projection host.`
32
+ );
33
+ }
34
+ return ["read routed through ProjectionHost"];
35
+ }
36
+ },
37
+ {
38
+ id: "fabric.experience.gateway-scope-denial.v1",
39
+ async run(subject) {
40
+ let rejected = false;
41
+ try {
42
+ await subject.gateway.invoke(subject.deniedIntent);
43
+ } catch {
44
+ rejected = true;
45
+ }
46
+ if (!rejected) throw new Error("Gateway accepted an intent outside trusted identity scope.");
47
+ return ["out-of-scope intent rejected"];
48
+ }
49
+ }
50
+ ];
51
+ }
52
+ async function runExperienceGatewayChecks(subject, checks = experienceGatewayChecks()) {
53
+ const results = [];
54
+ for (const check of checks) {
55
+ try {
56
+ results.push({ id: check.id, status: "passed", evidence: await check.run(subject) });
57
+ } catch (error) {
58
+ results.push({ id: check.id, status: "failed", evidence: [], error: error instanceof Error ? error.message : String(error) });
59
+ }
60
+ }
61
+ return { passed: results.every((result) => result.status === "passed"), checks: results };
62
+ }
63
+ function createInitiatingReleaseEvidence() {
64
+ const evidence = /* @__PURE__ */ new WeakMap();
65
+ return {
66
+ bind(input, releaseDigest) {
67
+ evidence.set(input, releaseDigest);
68
+ },
69
+ resolve(input) {
70
+ return evidence.get(input);
71
+ },
72
+ release(input) {
73
+ evidence.delete(input);
74
+ }
75
+ };
76
+ }
77
+ function createExperienceGateway(options) {
78
+ const now = options.now ?? (() => /* @__PURE__ */ new Date());
79
+ return {
80
+ async invoke(input) {
81
+ const claims = await options.identity.verify(input.context.credential);
82
+ if (!claims) throw new Error("Experience request credential is not authenticated.");
83
+ assertActorClaimsCoverScope(claims, {
84
+ tenantId: input.context.tenantId,
85
+ spaceId: input.context.spaceId
86
+ }, now());
87
+ const scope = {
88
+ tenantId: input.context.tenantId,
89
+ spaceId: input.context.spaceId,
90
+ actorId: claims.subject,
91
+ actorType: claims.actorType
92
+ };
93
+ const resolved = await options.runtime.resolveIntent({
94
+ planId: input.request.planId,
95
+ fragmentId: input.request.fragmentId,
96
+ eventName: input.request.eventName,
97
+ scope
98
+ });
99
+ if (resolved.assemblyDigest !== options.assemblyDigest) {
100
+ throw new Error("Experience render plan belongs to a different application assembly.");
101
+ }
102
+ if (options.actionVersion(resolved.actionId) !== resolved.actionVersion) {
103
+ throw new Error(`Experience intent route for "${resolved.actionId}" does not match the registered action version.`);
104
+ }
105
+ if (resolved.execution?.idempotency === "required" && !input.request.idempotencyKey) {
106
+ throw new Error(`Action "${resolved.actionId}" requires an idempotency key.`);
107
+ }
108
+ if (input.executionReason === "offline_replay" && resolved.execution?.connectivity !== "offline-capturable") {
109
+ throw new Error(`Action "${resolved.actionId}" is not eligible for offline replay.`);
110
+ }
111
+ const correlationId = options.nextCorrelationId();
112
+ let capturedAt;
113
+ if (input.executionReason === "offline_replay") {
114
+ const supplied = input.capturedAt ?? input.request.capturedAt;
115
+ if (typeof supplied !== "string") {
116
+ throw new Error("Offline replay requires capturedAt; receipt time cannot attest when the operator acted.");
117
+ }
118
+ capturedAt = assertCaptureMoment(supplied, now(), options.maxOfflineReplayAgeMs);
119
+ } else {
120
+ capturedAt = now().toISOString();
121
+ }
122
+ const parameterDigest = digestParameters(input.request.parameters);
123
+ const capturedAuthority = resolved.execution?.authorityMoment === "capture" ? await options.captureAuthority?.({
124
+ actionId: resolved.actionId,
125
+ scope,
126
+ parameters: input.request.parameters,
127
+ parameterDigest,
128
+ capturedAt
129
+ }) : void 0;
130
+ if (resolved.execution?.authorityMoment === "capture" && !capturedAuthority) {
131
+ throw new Error(`Action "${resolved.actionId}" requires a trusted capture-authority adapter.`);
132
+ }
133
+ const submission = {
134
+ actionId: resolved.actionId,
135
+ parameters: input.request.parameters,
136
+ tenantId: scope.tenantId,
137
+ spaceId: scope.spaceId,
138
+ actorId: scope.actorId,
139
+ actorType: claims.actorType,
140
+ correlationId,
141
+ provenance: {
142
+ source: "ui",
143
+ correlationId,
144
+ ...resolved.execution?.sensitivity === "restricted" ? {} : { auditAttributes: { "fabric.experience.fragment": input.request.fragmentId } }
145
+ },
146
+ ...input.request.idempotencyKey === void 0 ? {} : { idempotencyKey: input.request.idempotencyKey },
147
+ ...input.executionReason === void 0 ? {} : { executionReason: input.executionReason },
148
+ ...capturedAuthority === void 0 ? {} : {
149
+ authorizationBindingId: capturedAuthority.id,
150
+ authorizationBinding: {
151
+ id: capturedAuthority.id,
152
+ governingMoment: "capture",
153
+ capturedAt,
154
+ ...capturedAuthority.expiresAt === void 0 ? {} : { expiresAt: capturedAuthority.expiresAt },
155
+ tenantId: scope.tenantId,
156
+ actorId: scope.actorId,
157
+ actionId: resolved.actionId,
158
+ parameterDigest
159
+ }
160
+ }
161
+ };
162
+ options.evidence.bind(submission, resolved.initiatingReleaseDigest);
163
+ try {
164
+ return await options.host.submitAction(submission);
165
+ } finally {
166
+ options.evidence.release(submission);
167
+ }
168
+ },
169
+ async read(input) {
170
+ const claims = await options.identity.verify(input.context.credential);
171
+ if (!claims) throw new Error("Experience request credential is not authenticated.");
172
+ assertActorClaimsCoverScope(claims, {
173
+ tenantId: input.context.tenantId,
174
+ spaceId: input.context.spaceId
175
+ }, now());
176
+ const scope = {
177
+ tenantId: input.context.tenantId,
178
+ spaceId: input.context.spaceId,
179
+ actorId: claims.subject,
180
+ actorType: claims.actorType
181
+ };
182
+ const resolved = await options.runtime.resolveView({
183
+ planId: input.request.planId,
184
+ fragmentId: input.request.fragmentId,
185
+ bindingName: input.request.bindingName,
186
+ scope
187
+ });
188
+ if (resolved.assemblyDigest !== options.assemblyDigest) {
189
+ throw new Error("Experience render plan belongs to a different application assembly.");
190
+ }
191
+ const result = await options.projectionHost.project({
192
+ view: { name: resolved.name, version: resolved.version },
193
+ scope: { tenantId: scope.tenantId, spaceId: scope.spaceId },
194
+ actor: { id: scope.actorId, type: claims.actorType },
195
+ parameters: input.request.parameters
196
+ });
197
+ return {
198
+ data: result.state,
199
+ evidence: {
200
+ releaseDigest: resolved.initiatingReleaseDigest,
201
+ assemblyDigest: resolved.assemblyDigest,
202
+ eventCursor: result.eventCursor,
203
+ source: result.source
204
+ }
205
+ };
206
+ }
207
+ };
208
+ }
209
+ var RFC3339 = /^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:[Zz]|[+-]\d{2}:\d{2})$/;
210
+ function assertCaptureMoment(capturedAt, now, maxAgeMs) {
211
+ if (!RFC3339.test(capturedAt)) {
212
+ throw new Error("Offline replay capturedAt must be an RFC 3339 instant with an explicit offset.");
213
+ }
214
+ const parsed = Date.parse(capturedAt);
215
+ if (Number.isNaN(parsed)) throw new Error("Offline replay capturedAt is not a parseable instant.");
216
+ if (parsed > now.getTime()) throw new Error("Offline replay capturedAt is in the future.");
217
+ if (maxAgeMs === void 0) {
218
+ throw new Error("Gateway accepts offline replay but declares no maxOfflineReplayAgeMs, so a capture has no lower bound.");
219
+ }
220
+ if (now.getTime() - parsed > maxAgeMs) {
221
+ throw new Error(`Offline replay capturedAt is older than the ${maxAgeMs}ms this gateway accepts.`);
222
+ }
223
+ return new Date(parsed).toISOString();
224
+ }
225
+ export {
226
+ createExperienceGateway,
227
+ createInitiatingReleaseEvidence,
228
+ experienceGatewayChecks,
229
+ runExperienceGatewayChecks
230
+ };
231
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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":";AAKA,SAAS,wBAAwB;AAEjC;AAAA,EACC;AAAA,OAEM;AAqEA,SAAS,0BAA6D;AAC5E,SAAO;AAAA,IACN;AAAA,MACC,IAAI;AAAA,MACJ,MAAM,IAAI,SAAS;AAClB,cAAM,SAAS,QAAQ,SAAS,kBAAkB;AAClD,cAAM,SAAS,MAAM,QAAQ,QAAQ,OAAO,QAAQ,WAAW;AAC/D,YAAI,CAAC,OAAO,mBAAoB,OAAM,IAAI,MAAM,oDAAoD;AACpG,cAAM,YAAY,QAAQ,SAAS,kBAAkB,IAAI;AACzD,YAAI,cAAc,GAAG;AACpB,gBAAM,IAAI;AAAA,YACT,2CAA2C,SAAS;AAAA,UACrD;AAAA,QACD;AACA,eAAO,CAAC,OAAO,kBAAkB;AAAA,MAClC;AAAA,IACD;AAAA,IACA;AAAA,MACC,IAAI;AAAA,MACJ,MAAM,IAAI,SAAS;AAClB,cAAM,SAAS,QAAQ,SAAS,gBAAgB;AAChD,cAAM,QAAQ,QAAQ,KAAK,QAAQ,SAAS;AAC5C,cAAM,YAAY,QAAQ,SAAS,gBAAgB,IAAI;AACvD,YAAI,cAAc,GAAG;AACpB,gBAAM,IAAI;AAAA,YACT,+BAA+B,SAAS;AAAA,UACzC;AAAA,QACD;AACA,eAAO,CAAC,oCAAoC;AAAA,MAC7C;AAAA,IACD;AAAA,IACA;AAAA,MACC,IAAI;AAAA,MACJ,MAAM,IAAI,SAAS;AAClB,YAAI,WAAW;AACf,YAAI;AAAE,gBAAM,QAAQ,QAAQ,OAAO,QAAQ,YAAY;AAAA,QAAG,QAAQ;AAAE,qBAAW;AAAA,QAAM;AACrF,YAAI,CAAC,SAAU,OAAM,IAAI,MAAM,4DAA4D;AAC3F,eAAO,CAAC,8BAA8B;AAAA,MACvC;AAAA,IACD;AAAA,EACD;AACD;AAEA,eAAsB,2BACrB,SACA,SAA4C,wBAAwB,GACiF;AACrJ,QAAM,UAAwH,CAAC;AAC/H,aAAW,SAAS,QAAQ;AAC3B,QAAI;AAAE,cAAQ,KAAK,EAAE,IAAI,MAAM,IAAI,QAAQ,UAAU,UAAU,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;AAAA,IAAG,SACrF,OAAO;AAAE,cAAQ,KAAK,EAAE,IAAI,MAAM,IAAI,QAAQ,UAAU,UAAU,CAAC,GAAG,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAAE,CAAC;AAAA,IAAG;AAAA,EAChJ;AACA,SAAO,EAAE,QAAQ,QAAQ,MAAM,CAAC,WAAW,OAAO,WAAW,QAAQ,GAAG,QAAQ,QAAQ;AACzF;AAaO,SAAS,kCAA6D;AAC5E,QAAM,WAAW,oBAAI,QAAmC;AACxD,SAAO;AAAA,IACN,KAAK,OAAO,eAAe;AAC1B,eAAS,IAAI,OAAO,aAAa;AAAA,IAClC;AAAA,IACA,QAAQ,OAAO;AACd,aAAO,SAAS,IAAI,KAAK;AAAA,IAC1B;AAAA,IACA,QAAQ,OAAO;AACd,eAAS,OAAO,KAAK;AAAA,IACtB;AAAA,EACD;AACD;AAEO,SAAS,wBAAwB,SA0BlB;AACrB,QAAM,MAAM,QAAQ,QAAQ,MAAM,oBAAI,KAAK;AAC3C,SAAO;AAAA,IACN,MAAM,OAAO,OAAO;AACnB,YAAM,SAAS,MAAM,QAAQ,SAAS,OAAO,MAAM,QAAQ,UAAU;AACrE,UAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,qDAAqD;AAClF,kCAA4B,QAAQ;AAAA,QACnC,UAAU,MAAM,QAAQ;AAAA,QACxB,SAAS,MAAM,QAAQ;AAAA,MACxB,GAAG,IAAI,CAAC;AACR,YAAM,QAAyB;AAAA,QAC9B,UAAU,MAAM,QAAQ;AAAA,QACxB,SAAS,MAAM,QAAQ;AAAA,QACvB,SAAS,OAAO;AAAA,QAChB,WAAW,OAAO;AAAA,MACnB;AACA,YAAM,WAAW,MAAM,QAAQ,QAAQ,cAAc;AAAA,QACpD,QAAQ,MAAM,QAAQ;AAAA,QACtB,YAAY,MAAM,QAAQ;AAAA,QAC1B,WAAW,MAAM,QAAQ;AAAA,QACzB;AAAA,MACD,CAAC;AACD,UAAI,SAAS,mBAAmB,QAAQ,gBAAgB;AACvD,cAAM,IAAI,MAAM,qEAAqE;AAAA,MACtF;AACA,UAAI,QAAQ,cAAc,SAAS,QAAQ,MAAM,SAAS,eAAe;AACxE,cAAM,IAAI,MAAM,gCAAgC,SAAS,QAAQ,iDAAiD;AAAA,MACnH;AACA,UAAI,SAAS,WAAW,gBAAgB,cAAc,CAAC,MAAM,QAAQ,gBAAgB;AACpF,cAAM,IAAI,MAAM,WAAW,SAAS,QAAQ,gCAAgC;AAAA,MAC7E;AACA,UAAI,MAAM,oBAAoB,oBAAoB,SAAS,WAAW,iBAAiB,sBAAsB;AAC5G,cAAM,IAAI,MAAM,WAAW,SAAS,QAAQ,uCAAuC;AAAA,MACpF;AACA,YAAM,gBAAgB,QAAQ,kBAAkB;AAIhD,UAAI;AACJ,UAAI,MAAM,oBAAoB,kBAAkB;AAI/C,cAAM,WAAW,MAAM,cAClB,MAAM,QAA4C;AACvD,YAAI,OAAO,aAAa,UAAU;AACjC,gBAAM,IAAI,MAAM,yFAAyF;AAAA,QAC1G;AACA,qBAAa,oBAAoB,UAAU,IAAI,GAAG,QAAQ,qBAAqB;AAAA,MAChF,OAAO;AACN,qBAAa,IAAI,EAAE,YAAY;AAAA,MAChC;AACA,YAAM,kBAAkB,iBAAiB,MAAM,QAAQ,UAAU;AACjE,YAAM,oBAAoB,SAAS,WAAW,oBAAoB,YAC/D,MAAM,QAAQ,mBAAmB;AAAA,QAClC,UAAU,SAAS;AAAA,QACnB;AAAA,QACA,YAAY,MAAM,QAAQ;AAAA,QAC1B;AAAA,QACA;AAAA,MACD,CAAC,IACC;AACH,UAAI,SAAS,WAAW,oBAAoB,aAAa,CAAC,mBAAmB;AAC5E,cAAM,IAAI,MAAM,WAAW,SAAS,QAAQ,iDAAiD;AAAA,MAC9F;AACA,YAAM,aAAgC;AAAA,QACrC,UAAU,SAAS;AAAA,QACnB,YAAY,MAAM,QAAQ;AAAA,QAC1B,UAAU,MAAM;AAAA,QAChB,SAAS,MAAM;AAAA,QACf,SAAS,MAAM;AAAA,QACf,WAAW,OAAO;AAAA,QAClB;AAAA,QACA,YAAY;AAAA,UACX,QAAQ;AAAA,UACR;AAAA,UACA,GAAI,SAAS,WAAW,gBAAgB,eACrC,CAAC,IACD,EAAE,iBAAiB,EAAE,8BAA8B,MAAM,QAAQ,WAAW,EAAE;AAAA,QAClF;AAAA,QACA,GAAI,MAAM,QAAQ,mBAAmB,SAAY,CAAC,IAAI,EAAE,gBAAgB,MAAM,QAAQ,eAAe;AAAA,QACrG,GAAI,MAAM,oBAAoB,SAAY,CAAC,IAAI,EAAE,iBAAiB,MAAM,gBAAgB;AAAA,QACxF,GAAI,sBAAsB,SAAY,CAAC,IAAI;AAAA,UAC1C,wBAAwB,kBAAkB;AAAA,UAC1C,sBAAsB;AAAA,YACrB,IAAI,kBAAkB;AAAA,YACtB,iBAAiB;AAAA,YACjB;AAAA,YACA,GAAI,kBAAkB,cAAc,SAAY,CAAC,IAAI,EAAE,WAAW,kBAAkB,UAAU;AAAA,YAC9F,UAAU,MAAM;AAAA,YAChB,SAAS,MAAM;AAAA,YACf,UAAU,SAAS;AAAA,YACnB;AAAA,UACD;AAAA,QACD;AAAA,MACD;AACA,cAAQ,SAAS,KAAK,YAAY,SAAS,uBAAuB;AAClE,UAAI;AACH,eAAO,MAAM,QAAQ,KAAK,aAAa,UAAU;AAAA,MAClD,UAAE;AACD,gBAAQ,SAAS,QAAQ,UAAU;AAAA,MACpC;AAAA,IACD;AAAA,IACA,MAAM,KAAK,OAAO;AACjB,YAAM,SAAS,MAAM,QAAQ,SAAS,OAAO,MAAM,QAAQ,UAAU;AACrE,UAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,qDAAqD;AAClF,kCAA4B,QAAQ;AAAA,QACnC,UAAU,MAAM,QAAQ;AAAA,QACxB,SAAS,MAAM,QAAQ;AAAA,MACxB,GAAG,IAAI,CAAC;AACR,YAAM,QAAyB;AAAA,QAC9B,UAAU,MAAM,QAAQ;AAAA,QACxB,SAAS,MAAM,QAAQ;AAAA,QACvB,SAAS,OAAO;AAAA,QAChB,WAAW,OAAO;AAAA,MACnB;AACA,YAAM,WAAW,MAAM,QAAQ,QAAQ,YAAY;AAAA,QAClD,QAAQ,MAAM,QAAQ;AAAA,QACtB,YAAY,MAAM,QAAQ;AAAA,QAC1B,aAAa,MAAM,QAAQ;AAAA,QAC3B;AAAA,MACD,CAAC;AACD,UAAI,SAAS,mBAAmB,QAAQ,gBAAgB;AACvD,cAAM,IAAI,MAAM,qEAAqE;AAAA,MACtF;AACA,YAAM,SAAS,MAAM,QAAQ,eAAe,QAAQ;AAAA,QACnD,MAAM,EAAE,MAAM,SAAS,MAAM,SAAS,SAAS,QAAQ;AAAA,QACvD,OAAO,EAAE,UAAU,MAAM,UAAU,SAAS,MAAM,QAAQ;AAAA,QAC1D,OAAO,EAAE,IAAI,MAAM,SAAS,MAAM,OAAO,UAAU;AAAA,QACnD,YAAY,MAAM,QAAQ;AAAA,MAC3B,CAAC;AACD,aAAO;AAAA,QACN,MAAM,OAAO;AAAA,QACb,UAAU;AAAA,UACT,eAAe,SAAS;AAAA,UACxB,gBAAgB,SAAS;AAAA,UACzB,aAAa,OAAO;AAAA,UACpB,QAAQ,OAAO;AAAA,QAChB;AAAA,MACD;AAAA,IACD;AAAA,EACD;AACD;AAGA,IAAM,UAAU;AAchB,SAAS,oBAAoB,YAAoB,KAAW,UAAsC;AACjG,MAAI,CAAC,QAAQ,KAAK,UAAU,GAAG;AAC9B,UAAM,IAAI,MAAM,gFAAgF;AAAA,EACjG;AACA,QAAM,SAAS,KAAK,MAAM,UAAU;AACpC,MAAI,OAAO,MAAM,MAAM,EAAG,OAAM,IAAI,MAAM,uDAAuD;AACjG,MAAI,SAAS,IAAI,QAAQ,EAAG,OAAM,IAAI,MAAM,6CAA6C;AACzF,MAAI,aAAa,QAAW;AAC3B,UAAM,IAAI,MAAM,wGAAwG;AAAA,EACzH;AACA,MAAI,IAAI,QAAQ,IAAI,SAAS,UAAU;AACtC,UAAM,IAAI,MAAM,+CAA+C,QAAQ,0BAA0B;AAAA,EAClG;AACA,SAAO,IAAI,KAAK,MAAM,EAAE,YAAY;AACrC;","names":[]}
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@fabricorg/experience-gateway",
3
+ "version": "1.0.0",
4
+ "description": "Trusted server adapter from scoped experience plans to governed Fabric Hosts.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "main": "./dist/index.cjs",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "import": {
13
+ "types": "./dist/index.d.ts",
14
+ "default": "./dist/index.js"
15
+ },
16
+ "require": {
17
+ "types": "./dist/index.d.cts",
18
+ "default": "./dist/index.cjs"
19
+ }
20
+ }
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "README.md",
25
+ "CHANGELOG.md",
26
+ "LICENSE"
27
+ ],
28
+ "dependencies": {
29
+ "@fabricorg/experience-runtime": "^0.2.0"
30
+ },
31
+ "peerDependencies": {
32
+ "@fabricorg/platform-host": "^7.0.0",
33
+ "@fabricorg/projection-host": "^1.1.0",
34
+ "@fabricorg/ports": "^0.4.0"
35
+ },
36
+ "devDependencies": {
37
+ "@types/node": "^22.10.0",
38
+ "tsup": "^8.5.0",
39
+ "typescript": "^5.7.3",
40
+ "vitest": "^4.1.5",
41
+ "@fabricorg/platform-host": "^7.0.0",
42
+ "@fabricorg/projection-host": "^1.1.0",
43
+ "@fabricorg/ports": "^0.4.0"
44
+ },
45
+ "engines": {
46
+ "node": ">=20"
47
+ },
48
+ "publishConfig": {
49
+ "access": "public"
50
+ },
51
+ "scripts": {
52
+ "clean": "rm -rf dist tsconfig.tsbuildinfo",
53
+ "build": "tsup",
54
+ "type-check": "tsc --noEmit",
55
+ "test": "vitest run"
56
+ }
57
+ }