@fabricorg/experience-runtime 0.2.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.
@@ -0,0 +1,205 @@
1
+ // offline.ts
2
+ function createOfflineCaptureQueue(options) {
3
+ if (options.store.protection !== "encrypted-at-rest" && !options.allowEphemeralStore) {
4
+ throw new Error("Production offline capture requires an encrypted-at-rest store.");
5
+ }
6
+ if (!options.gateway.replay) throw new Error("Offline capture requires a gateway replay adapter.");
7
+ const fragments = indexFragments(options.plan.root);
8
+ const now = options.now ?? (() => /* @__PURE__ */ new Date());
9
+ return {
10
+ async capture(fragmentId, eventName, parameters, idempotencyKey) {
11
+ const fragment = fragments.get(fragmentId);
12
+ if (!fragment?.offlineEvents?.includes(eventName)) {
13
+ throw new Error(`Fragment event "${fragmentId}.${eventName}" is not offline-capturable.`);
14
+ }
15
+ if (!idempotencyKey || idempotencyKey.length > 256) throw new Error("Offline capture requires a bounded stable idempotency key.");
16
+ const capturedAt = now();
17
+ const captureExpiry = capturedAt.getTime() + (options.captureTtlMs ?? 864e5);
18
+ const planExpiry = Date.parse(options.plan.expiresAt);
19
+ if (!Number.isFinite(planExpiry) || planExpiry <= capturedAt.getTime()) throw new Error("Offline capture requires an unexpired render plan.");
20
+ const safeParameters = cloneOfflineParameters(parameters);
21
+ const captureId = options.nextCaptureId();
22
+ if (!captureId.trim()) throw new Error("Offline capture id is required.");
23
+ const record = {
24
+ captureId,
25
+ planId: options.plan.planId,
26
+ releaseDigest: options.plan.releaseDigest,
27
+ fragmentId,
28
+ eventName,
29
+ parameters: safeParameters,
30
+ idempotencyKey,
31
+ capturedAt: capturedAt.toISOString(),
32
+ expiresAt: new Date(Math.min(captureExpiry, planExpiry)).toISOString()
33
+ };
34
+ await options.store.put(record);
35
+ return record;
36
+ },
37
+ async replay() {
38
+ let completed = 0;
39
+ let failed = 0;
40
+ const parked = [];
41
+ const expired = [];
42
+ const reconciled = [];
43
+ for (const record of await options.store.list()) {
44
+ if (Date.parse(record.expiresAt) <= now().getTime()) {
45
+ await options.store.remove(record.captureId);
46
+ expired.push(record);
47
+ continue;
48
+ }
49
+ const request = {
50
+ planId: record.planId,
51
+ fragmentId: record.fragmentId,
52
+ eventName: record.eventName,
53
+ parameters: record.parameters,
54
+ idempotencyKey: record.idempotencyKey,
55
+ executionReason: "offline_replay",
56
+ capturedAt: record.capturedAt
57
+ };
58
+ try {
59
+ const outcome = await options.gateway.replay(request);
60
+ await options.store.remove(record.captureId);
61
+ const disposition = dispositionOf(outcome);
62
+ const actionInvocationId = typeof outcome?.actionInvocationId === "string" ? outcome.actionInvocationId : void 0;
63
+ if (disposition.kind === "attention") {
64
+ reconciled.push({
65
+ record,
66
+ status: disposition.status,
67
+ ...actionInvocationId === void 0 ? {} : { actionInvocationId },
68
+ certainty: disposition.certainty,
69
+ reason: disposition.reason
70
+ });
71
+ } else if (disposition.kind === "parked") {
72
+ parked.push({ record, ...actionInvocationId === void 0 ? {} : { actionInvocationId } });
73
+ } else {
74
+ completed += 1;
75
+ }
76
+ } catch (error) {
77
+ if (isSettledReplayFailure(error)) {
78
+ await options.store.remove(record.captureId);
79
+ reconciled.push({ record, status: "not_delivered", certainty: "refused", reason: messageOf(error) });
80
+ } else {
81
+ failed += 1;
82
+ }
83
+ }
84
+ }
85
+ return { completed, parked, failed, expired, reconciled };
86
+ }
87
+ };
88
+ }
89
+ function cloneOfflineParameters(parameters) {
90
+ let nodes = 0;
91
+ const visit = (value, depth, key) => {
92
+ nodes += 1;
93
+ if (nodes > 1e4 || depth > 20) throw new Error("Offline capture parameters exceed structural limits.");
94
+ if (key !== void 0 && /(?:authorization|bearer|cookie|credential|password|secret|token|claims?)/i.test(key)) {
95
+ throw new Error(`Offline capture parameters may not contain credential or claim field "${key}".`);
96
+ }
97
+ if (value === null || typeof value === "string" || typeof value === "boolean") return;
98
+ if (typeof value === "number" && Number.isFinite(value)) return;
99
+ if (Array.isArray(value)) {
100
+ value.forEach((entry) => visit(entry, depth + 1));
101
+ return;
102
+ }
103
+ if (value && typeof value === "object" && Object.getPrototypeOf(value) === Object.prototype) {
104
+ for (const [childKey, child] of Object.entries(value)) visit(child, depth + 1, childKey);
105
+ return;
106
+ }
107
+ throw new Error("Offline capture parameters must contain only portable JSON values.");
108
+ };
109
+ visit(parameters, 0);
110
+ const serialized = JSON.stringify(parameters);
111
+ if (new TextEncoder().encode(serialized).byteLength > 65536) throw new Error("Offline capture parameters exceed the 65536-byte limit.");
112
+ return structuredClone(parameters);
113
+ }
114
+ function indexFragments(root) {
115
+ const result = /* @__PURE__ */ new Map();
116
+ const visit = (fragment) => {
117
+ result.set(fragment.id, fragment);
118
+ for (const child of fragment.children ?? []) visit(child);
119
+ };
120
+ visit(root);
121
+ return result;
122
+ }
123
+ function createMemoryOfflineCaptureStore() {
124
+ const records = /* @__PURE__ */ new Map();
125
+ return {
126
+ protection: "ephemeral",
127
+ async put(record) {
128
+ if (records.has(record.captureId)) throw new Error(`Offline capture "${record.captureId}" already exists.`);
129
+ records.set(record.captureId, structuredClone(record));
130
+ },
131
+ async list() {
132
+ return [...records.values()].map((record) => structuredClone(record));
133
+ },
134
+ async remove(captureId) {
135
+ records.delete(captureId);
136
+ }
137
+ };
138
+ }
139
+ function messageOf(error) {
140
+ return error instanceof Error ? error.message : String(error);
141
+ }
142
+ function dispositionOf(outcome) {
143
+ if (!outcome || typeof outcome !== "object") {
144
+ return {
145
+ kind: "attention",
146
+ status: "no_outcome",
147
+ certainty: "unknown",
148
+ reason: "the replay adapter returned no outcome, so whether the action was applied is unknown"
149
+ };
150
+ }
151
+ const status = typeof outcome.status === "string" ? outcome.status : "";
152
+ const reason = outcome.reconciliation?.message ?? outcome.reconciliation?.kind ?? (typeof outcome.error === "string" ? outcome.error : status);
153
+ switch (status) {
154
+ case "pending":
155
+ case "running":
156
+ case "completed":
157
+ return { kind: "delivered" };
158
+ case "waiting_for_approval":
159
+ return { kind: "parked" };
160
+ case "reconciliation_required":
161
+ return {
162
+ kind: "attention",
163
+ status,
164
+ certainty: outcome.adapterReconciliation === void 0 ? "refused" : "unknown",
165
+ reason
166
+ };
167
+ case "blocked_by_policy":
168
+ case "validation_failed":
169
+ return { kind: "attention", status, certainty: "refused", reason };
170
+ case "failed":
171
+ return { kind: "attention", status, certainty: "unknown", reason };
172
+ default:
173
+ return { kind: "attention", status: status || "unknown", certainty: "unknown", reason };
174
+ }
175
+ }
176
+ function isSettledReplayFailure(error) {
177
+ return [
178
+ // The action was never offline-eligible.
179
+ /not eligible for offline replay/i,
180
+ // The plan no longer grants, no longer exists, or was never this actor's.
181
+ /is not granted by the render plan/i,
182
+ // A plan minted against a superseded deployment. Same class as the
183
+ // assembly mismatch on the line below it, and equally permanent.
184
+ /does not match the registered action version/i,
185
+ /render plan was not found/i,
186
+ /render plan has expired/i,
187
+ /scope does not match/i,
188
+ /belongs to a different application assembly/i,
189
+ // A capture only gets older, so an age refusal can never come good. This
190
+ // one is independent of the capture TTL, so nothing else retires it.
191
+ /capturedAt is older than/i,
192
+ // Malformed beyond repair: passes the grammar, names no real instant.
193
+ /capturedAt is not a parseable instant/i,
194
+ /capturedAt must be an RFC 3339/i
195
+ // Deliberately absent: a capture dated in the future comes good as real
196
+ // time advances, and a gateway missing its replay window is a
197
+ // deployment mistake somebody will fix. Discarding either would destroy
198
+ // work that a clock correction or a config change would have replayed.
199
+ ].some((pattern) => pattern.test(messageOf(error)));
200
+ }
201
+ export {
202
+ createMemoryOfflineCaptureStore,
203
+ createOfflineCaptureQueue
204
+ };
205
+ //# sourceMappingURL=offline.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../offline.ts"],"sourcesContent":["import type {\n\tExperienceGatewayClient,\n\tExperienceInvocationOutcome,\n\tExperienceReplayRequest,\n\tRenderPlanFragment,\n\tScopedRenderPlan,\n} from \"./contracts\";\n\nexport interface OfflineCaptureRecord {\n\tcaptureId: string;\n\tplanId: string;\n\treleaseDigest: string;\n\tfragmentId: string;\n\teventName: string;\n\tparameters: Record<string, unknown>;\n\tidempotencyKey: string;\n\tcapturedAt: string;\n\texpiresAt: string;\n}\n\nexport interface OfflineCaptureStore {\n\tprotection: \"encrypted-at-rest\" | \"ephemeral\";\n\tput(record: OfflineCaptureRecord): Promise<void>;\n\tlist(): Promise<readonly OfflineCaptureRecord[]>;\n\tremove(captureId: string): Promise<void>;\n}\n\n/** A delivered capture whose outcome a person needs to see. */\nexport interface OfflineReconciliation {\n\trecord: OfflineCaptureRecord;\n\t/** The status the platform returned, so a caller can tell these apart. */\n\tstatus: string;\n\t/**\n\t * The invocation the platform recorded, when it got that far.\n\t *\n\t * `recordExternalReconciliation` needs it, so an outcome of `unknown` that\n\t * omits it cannot be reconciled from this summary — which is the whole\n\t * reason the entry exists.\n\t */\n\tactionInvocationId?: string;\n\t/**\n\t * Whether the effect is known not to have happened, or is simply unknown.\n\t * An adapter that timed out mid-charge is `unknown`: telling an operator it\n\t * was refused would be as wrong as telling them it succeeded.\n\t */\n\tcertainty: \"refused\" | \"unknown\";\n\treason: string;\n}\n\n/** A delivered capture waiting on a human. */\nexport interface OfflineParked {\n\trecord: OfflineCaptureRecord;\n\tactionInvocationId?: string;\n}\n\nexport interface OfflineReplaySummary {\n\t/** Delivered and progressing: the platform has the invocation. */\n\tcompleted: number;\n\t/**\n\t * Delivered and parked awaiting a human decision.\n\t *\n\t * Carries the records, because a bare count leaves nobody able to say which\n\t * action somebody now has to approve.\n\t */\n\tparked: OfflineParked[];\n\t/** Never delivered, and worth another attempt. Still queued. */\n\tfailed: number;\n\t/**\n\t * Outlived the capture TTL or the render plan before it could be sent.\n\t *\n\t * Also carries its records: a capture that expired never reached the\n\t * platform, so this is work the operator did that nobody has.\n\t */\n\texpired: OfflineCaptureRecord[];\n\t/**\n\t * Delivered, and something a person needs to see: refused, invalid, or an\n\t * outcome nobody can determine. Also covers a delivery that can never\n\t * succeed, since retrying one only delays the moment somebody is told.\n\t */\n\treconciled: OfflineReconciliation[];\n}\n\nexport interface OfflineCaptureQueue {\n\tcapture(fragmentId: string, eventName: string, parameters: Record<string, unknown>, idempotencyKey: string): Promise<OfflineCaptureRecord>;\n\treplay(): Promise<OfflineReplaySummary>;\n}\n\nexport function createOfflineCaptureQueue(options: {\n\tplan: ScopedRenderPlan;\n\tstore: OfflineCaptureStore;\n\tgateway: ExperienceGatewayClient;\n\tnextCaptureId(): string;\n\tnow?: () => Date;\n\tcaptureTtlMs?: number;\n\tallowEphemeralStore?: boolean;\n}): OfflineCaptureQueue {\n\tif (options.store.protection !== \"encrypted-at-rest\" && !options.allowEphemeralStore) {\n\t\tthrow new Error(\"Production offline capture requires an encrypted-at-rest store.\");\n\t}\n\tif (!options.gateway.replay) throw new Error(\"Offline capture requires a gateway replay adapter.\");\n\tconst fragments = indexFragments(options.plan.root);\n\tconst now = options.now ?? (() => new Date());\n\treturn {\n\t\tasync capture(fragmentId, eventName, parameters, idempotencyKey) {\n\t\t\tconst fragment = fragments.get(fragmentId);\n\t\t\tif (!fragment?.offlineEvents?.includes(eventName)) {\n\t\t\t\tthrow new Error(`Fragment event \"${fragmentId}.${eventName}\" is not offline-capturable.`);\n\t\t\t}\n\t\t\tif (!idempotencyKey || idempotencyKey.length > 256) throw new Error(\"Offline capture requires a bounded stable idempotency key.\");\n\t\t\tconst capturedAt = now();\n\t\t\tconst captureExpiry = capturedAt.getTime() + (options.captureTtlMs ?? 86_400_000);\n\t\t\tconst planExpiry = Date.parse(options.plan.expiresAt);\n\t\t\tif (!Number.isFinite(planExpiry) || planExpiry <= capturedAt.getTime()) throw new Error(\"Offline capture requires an unexpired render plan.\");\n\t\t\tconst safeParameters = cloneOfflineParameters(parameters);\n\t\t\tconst captureId = options.nextCaptureId();\n\t\t\tif (!captureId.trim()) throw new Error(\"Offline capture id is required.\");\n\t\t\tconst record: OfflineCaptureRecord = {\n\t\t\t\tcaptureId,\n\t\t\t\tplanId: options.plan.planId,\n\t\t\t\treleaseDigest: options.plan.releaseDigest,\n\t\t\t\tfragmentId,\n\t\t\t\teventName,\n\t\t\t\tparameters: safeParameters,\n\t\t\t\tidempotencyKey,\n\t\t\t\tcapturedAt: capturedAt.toISOString(),\n\t\t\t\texpiresAt: new Date(Math.min(captureExpiry, planExpiry)).toISOString(),\n\t\t\t};\n\t\t\tawait options.store.put(record);\n\t\t\treturn record;\n\t\t},\n\t\tasync replay() {\n\t\t\tlet completed = 0;\n\t\t\tlet failed = 0;\n\t\t\tconst parked: OfflineParked[] = [];\n\t\t\tconst expired: OfflineCaptureRecord[] = [];\n\t\t\tconst reconciled: OfflineReconciliation[] = [];\n\t\t\tfor (const record of await options.store.list()) {\n\t\t\t\tif (Date.parse(record.expiresAt) <= now().getTime()) {\n\t\t\t\t\tawait options.store.remove(record.captureId);\n\t\t\t\t\texpired.push(record);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tconst request: ExperienceReplayRequest = {\n\t\t\t\t\tplanId: record.planId,\n\t\t\t\t\tfragmentId: record.fragmentId,\n\t\t\t\t\teventName: record.eventName,\n\t\t\t\t\tparameters: record.parameters,\n\t\t\t\t\tidempotencyKey: record.idempotencyKey,\n\t\t\t\t\texecutionReason: \"offline_replay\",\n\t\t\t\t\tcapturedAt: record.capturedAt,\n\t\t\t\t};\n\t\t\t\ttry {\n\t\t\t\t\tconst outcome = await options.gateway.replay!(request);\n\n\t\t\t\t\t// Anything returned means the platform durably holds the\n\t\t\t\t\t// invocation, so the capture has done its job either way and\n\t\t\t\t\t// leaves the queue. What differs is whether a person needs to\n\t\t\t\t\t// hear about it, which is decided over the whole status set\n\t\t\t\t\t// rather than the two that were obviously refusals.\n\t\t\t\t\tawait options.store.remove(record.captureId);\n\t\t\t\t\tconst disposition = dispositionOf(outcome);\n\t\t\t\t\tconst actionInvocationId = typeof outcome?.actionInvocationId === \"string\"\n\t\t\t\t\t\t? outcome.actionInvocationId\n\t\t\t\t\t\t: undefined;\n\t\t\t\t\tif (disposition.kind === \"attention\") {\n\t\t\t\t\t\treconciled.push({\n\t\t\t\t\t\t\trecord,\n\t\t\t\t\t\t\tstatus: disposition.status,\n\t\t\t\t\t\t\t...(actionInvocationId === undefined ? {} : { actionInvocationId }),\n\t\t\t\t\t\t\tcertainty: disposition.certainty,\n\t\t\t\t\t\t\treason: disposition.reason,\n\t\t\t\t\t\t});\n\t\t\t\t\t} else if (disposition.kind === \"parked\") {\n\t\t\t\t\t\tparked.push({ record, ...(actionInvocationId === undefined ? {} : { actionInvocationId }) });\n\t\t\t\t\t} else {\n\t\t\t\t\t\tcompleted += 1;\n\t\t\t\t\t}\n\t\t\t\t} catch (error) {\n\t\t\t\t\t// Only genuine throws reach here: the plan expired, the\n\t\t\t\t\t// scope no longer matches, the event is not offline-eligible.\n\t\t\t\t\t// Those are settled, and retrying one only delays the moment\n\t\t\t\t\t// somebody is told. Anything unrecognised stays transient, so\n\t\t\t\t\t// an unfamiliar failure is retried rather than discarded.\n\t\t\t\t\tif (isSettledReplayFailure(error)) {\n\t\t\t\t\t\tawait options.store.remove(record.captureId);\n\t\t\t\t\t\t// Never delivered, and never will be. The effect certainly\n\t\t\t\t\t\t// did not happen, which is more than can be said for a\n\t\t\t\t\t\t// delivery whose outcome came back unknown.\n\t\t\t\t\t\treconciled.push({ record, status: \"not_delivered\", certainty: \"refused\", reason: messageOf(error) });\n\t\t\t\t\t} else {\n\t\t\t\t\t\tfailed += 1;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn { completed, parked, failed, expired, reconciled };\n\t\t},\n\t};\n}\n\nfunction cloneOfflineParameters(parameters: Record<string, unknown>): Record<string, unknown> {\n\tlet nodes = 0;\n\tconst visit = (value: unknown, depth: number, key?: string): void => {\n\t\tnodes += 1;\n\t\tif (nodes > 10_000 || depth > 20) throw new Error(\"Offline capture parameters exceed structural limits.\");\n\t\tif (key !== undefined && /(?:authorization|bearer|cookie|credential|password|secret|token|claims?)/i.test(key)) {\n\t\t\tthrow new Error(`Offline capture parameters may not contain credential or claim field \"${key}\".`);\n\t\t}\n\t\tif (value === null || typeof value === \"string\" || typeof value === \"boolean\") return;\n\t\tif (typeof value === \"number\" && Number.isFinite(value)) return;\n\t\tif (Array.isArray(value)) {\n\t\t\tvalue.forEach((entry) => visit(entry, depth + 1));\n\t\t\treturn;\n\t\t}\n\t\tif (value && typeof value === \"object\" && Object.getPrototypeOf(value) === Object.prototype) {\n\t\t\tfor (const [childKey, child] of Object.entries(value)) visit(child, depth + 1, childKey);\n\t\t\treturn;\n\t\t}\n\t\tthrow new Error(\"Offline capture parameters must contain only portable JSON values.\");\n\t};\n\tvisit(parameters, 0);\n\tconst serialized = JSON.stringify(parameters);\n\tif (new TextEncoder().encode(serialized).byteLength > 65_536) throw new Error(\"Offline capture parameters exceed the 65536-byte limit.\");\n\treturn structuredClone(parameters);\n}\n\nfunction indexFragments(root: RenderPlanFragment): Map<string, RenderPlanFragment> {\n\tconst result = new Map<string, RenderPlanFragment>();\n\tconst visit = (fragment: RenderPlanFragment): void => {\n\t\tresult.set(fragment.id, fragment);\n\t\tfor (const child of fragment.children ?? []) visit(child);\n\t};\n\tvisit(root);\n\treturn result;\n}\n\nexport function createMemoryOfflineCaptureStore(): OfflineCaptureStore {\n\tconst records = new Map<string, OfflineCaptureRecord>();\n\treturn {\n\t\tprotection: \"ephemeral\",\n\t\tasync put(record) {\n\t\t\tif (records.has(record.captureId)) throw new Error(`Offline capture \"${record.captureId}\" already exists.`);\n\t\t\trecords.set(record.captureId, structuredClone(record));\n\t\t},\n\t\tasync list() {\n\t\t\treturn [...records.values()].map((record) => structuredClone(record));\n\t\t},\n\t\tasync remove(captureId) {\n\t\t\trecords.delete(captureId);\n\t\t},\n\t};\n}\n\nfunction messageOf(error: unknown): string {\n\treturn error instanceof Error ? error.message : String(error);\n}\n\ntype ReplayDisposition =\n\t| { kind: \"delivered\" }\n\t| { kind: \"parked\" }\n\t| { kind: \"attention\"; status: string; certainty: \"refused\" | \"unknown\"; reason: string };\n\n/**\n * Read a returned outcome across the platform's whole status set.\n *\n * Enumerating the statuses that looked like refusals left the rest falling\n * through to \"applied\", which is how a validation failure got reported to an\n * operator as success. The switch covers every `ActionStatus`, and the default\n * arm surfaces anything else rather than counting it a success — the status\n * arrives as a string over a transport, so this is a runtime guarantee and not\n * a compile-time one.\n *\n * There is no \"already applied\" branch. A key that legitimately repeats returns\n * the original invocation without complaint, and a genuine idempotency conflict\n * means the key was reused for a materially different command, so that capture\n * was never applied and must not be discarded.\n */\nfunction dispositionOf(outcome: ExperienceInvocationOutcome | undefined): ReplayDisposition {\n\t// An adapter that returns nothing has told us nothing. Reading that as\n\t// success is the failure this whole switch exists to prevent, and an absent\n\t// outcome deserves at least the suspicion an empty one gets.\n\tif (!outcome || typeof outcome !== \"object\") {\n\t\treturn {\n\t\t\tkind: \"attention\",\n\t\t\tstatus: \"no_outcome\",\n\t\t\tcertainty: \"unknown\",\n\t\t\treason: \"the replay adapter returned no outcome, so whether the action was applied is unknown\",\n\t\t};\n\t}\n\tconst status = typeof outcome.status === \"string\" ? outcome.status : \"\";\n\tconst reason = outcome.reconciliation?.message\n\t\t?? outcome.reconciliation?.kind\n\t\t?? (typeof outcome.error === \"string\" ? outcome.error : status);\n\n\tswitch (status) {\n\t\tcase \"pending\":\n\t\tcase \"running\":\n\t\tcase \"completed\":\n\t\t\treturn { kind: \"delivered\" };\n\t\tcase \"waiting_for_approval\":\n\t\t\treturn { kind: \"parked\" };\n\t\tcase \"reconciliation_required\":\n\t\t\t// An adapter whose outcome could not be determined also arrives here.\n\t\t\t// The external effect may well have happened, so calling it refused\n\t\t\t// would be as wrong as calling it applied.\n\t\t\treturn {\n\t\t\t\tkind: \"attention\",\n\t\t\t\tstatus,\n\t\t\t\tcertainty: outcome.adapterReconciliation === undefined ? \"refused\" : \"unknown\",\n\t\t\t\treason,\n\t\t\t};\n\t\tcase \"blocked_by_policy\":\n\t\tcase \"validation_failed\":\n\t\t\t// Both are decided before any effect: policy refuses at the gate and\n\t\t\t// validation refuses the parameters.\n\t\t\treturn { kind: \"attention\", status, certainty: \"refused\", reason };\n\t\tcase \"failed\":\n\t\t\t// Not the same. A handler or adapter step can fail after earlier\n\t\t\t// steps succeeded and the domain transaction committed, so telling an\n\t\t\t// operator this was refused could be as wrong as calling it applied.\n\t\t\treturn { kind: \"attention\", status, certainty: \"unknown\", reason };\n\t\tdefault:\n\t\t\t// An unfamiliar status is surfaced rather than counted as success.\n\t\t\treturn { kind: \"attention\", status: status || \"unknown\", certainty: \"unknown\", reason };\n\t}\n}\n\n/**\n * A thrown failure the platform will never accept on a later attempt.\n *\n * Deliberately narrow, and matched on what the gateway and runtime actually\n * throw. Anything else is transient, because keeping a capture costs a retry\n * and discarding one costs the operator their work.\n */\nfunction isSettledReplayFailure(error: unknown): boolean {\n\treturn [\n\t\t// The action was never offline-eligible.\n\t\t/not eligible for offline replay/i,\n\t\t// The plan no longer grants, no longer exists, or was never this actor's.\n\t\t/is not granted by the render plan/i,\n\t\t// A plan minted against a superseded deployment. Same class as the\n\t\t// assembly mismatch on the line below it, and equally permanent.\n\t\t/does not match the registered action version/i,\n\t\t/render plan was not found/i,\n\t\t/render plan has expired/i,\n\t\t/scope does not match/i,\n\t\t/belongs to a different application assembly/i,\n\t\t// A capture only gets older, so an age refusal can never come good. This\n\t\t// one is independent of the capture TTL, so nothing else retires it.\n\t\t/capturedAt is older than/i,\n\t\t// Malformed beyond repair: passes the grammar, names no real instant.\n\t\t/capturedAt is not a parseable instant/i,\n\t\t/capturedAt must be an RFC 3339/i,\n\t\t// Deliberately absent: a capture dated in the future comes good as real\n\t\t// time advances, and a gateway missing its replay window is a\n\t\t// deployment mistake somebody will fix. Discarding either would destroy\n\t\t// work that a clock correction or a config change would have replayed.\n\t].some((pattern) => pattern.test(messageOf(error)));\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,244 @@
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
+ // server.ts
21
+ var server_exports = {};
22
+ __export(server_exports, {
23
+ createExperienceServerRuntime: () => createExperienceServerRuntime,
24
+ createMemoryExperiencePlanStore: () => createMemoryExperiencePlanStore,
25
+ experienceServerChecks: () => experienceServerChecks,
26
+ runExperienceServerChecks: () => runExperienceServerChecks
27
+ });
28
+ module.exports = __toCommonJS(server_exports);
29
+ var import_contracts = require("@fabricorg/sdui-release/contracts");
30
+ function experienceServerChecks() {
31
+ return [
32
+ {
33
+ id: "fabric.experience.plan-opaque.v1",
34
+ async run(subject) {
35
+ const plan = await subject.runtime.issuePlan(subject.issue);
36
+ const serialized = JSON.stringify(plan);
37
+ if (serialized.includes("capability://")) throw new Error("Public render plan exposes a capability reference.");
38
+ return [plan.planId];
39
+ }
40
+ },
41
+ {
42
+ id: "fabric.experience.scope-isolation.v1",
43
+ async run(subject) {
44
+ const plan = await subject.runtime.issuePlan(subject.issue);
45
+ try {
46
+ await subject.runtime.resolveIntent({
47
+ planId: plan.planId,
48
+ ...subject.intent,
49
+ scope: subject.wrongScope
50
+ });
51
+ throw new Error("Runtime accepted an intent from the wrong trusted scope.");
52
+ } catch (error) {
53
+ if (error instanceof Error && error.message === "Runtime accepted an intent from the wrong trusted scope.") throw error;
54
+ }
55
+ return ["wrong scope rejected"];
56
+ }
57
+ },
58
+ {
59
+ id: "fabric.experience.intent-resolution.v1",
60
+ async run(subject) {
61
+ const plan = await subject.runtime.issuePlan(subject.issue);
62
+ const resolved = await subject.runtime.resolveIntent({
63
+ planId: plan.planId,
64
+ ...subject.intent,
65
+ scope: subject.issue.scope
66
+ });
67
+ return [`${resolved.actionId}@${resolved.actionVersion}`];
68
+ }
69
+ }
70
+ ];
71
+ }
72
+ async function runExperienceServerChecks(subject, checks = experienceServerChecks()) {
73
+ const results = [];
74
+ for (const check of checks) {
75
+ try {
76
+ results.push({ id: check.id, status: "passed", evidence: await check.run(subject) });
77
+ } catch (error) {
78
+ results.push({ id: check.id, status: "failed", evidence: [], error: error instanceof Error ? error.message : String(error) });
79
+ }
80
+ }
81
+ return { passed: results.every((result) => result.status === "passed"), checks: results };
82
+ }
83
+ function createExperienceServerRuntime(options) {
84
+ const now = options.now ?? (() => /* @__PURE__ */ new Date());
85
+ return {
86
+ async issuePlan(input) {
87
+ const admitted = await options.resolveChannel({ application: input.application, channel: input.channel });
88
+ const pointer = await (0, import_contracts.activateReleaseChannelPointer)({
89
+ compactJws: admitted.pointerJws,
90
+ crypto: options.crypto,
91
+ trust: options.trust,
92
+ generations: options.generations,
93
+ expected: { issuer: options.issuer, application: input.application, channel: input.channel },
94
+ now: now()
95
+ });
96
+ const release = await (0, import_contracts.verifySignedExperienceRelease)({
97
+ compactJws: admitted.releaseJws,
98
+ crypto: options.crypto,
99
+ trust: options.trust,
100
+ expected: {
101
+ issuer: options.issuer,
102
+ application: input.application,
103
+ assemblyDigest: pointer.assemblyDigest
104
+ }
105
+ });
106
+ if (release.releaseDigest !== pointer.activeReleaseDigest) {
107
+ throw new Error("Selected experience release does not match the active release-channel pointer.");
108
+ }
109
+ const selected = selectTree(release, input.variantId);
110
+ const planId = options.nextOpaqueId();
111
+ if (!planId || /capability:\/\//.test(planId)) throw new Error("Experience plan id must be opaque.");
112
+ const intents = /* @__PURE__ */ new Map();
113
+ const views = /* @__PURE__ */ new Map();
114
+ const root = toRenderPlanFragment(selected.root, release, selected.treeId, intents, views);
115
+ const publicPlan = Object.freeze({
116
+ formatVersion: 1,
117
+ planId,
118
+ application: input.application,
119
+ channel: input.channel,
120
+ releaseDigest: release.releaseDigest,
121
+ assemblyDigest: release.assemblyDigest,
122
+ treeId: selected.treeId,
123
+ root,
124
+ tokenSet: { name: release.tokenSet.name, version: release.tokenSet.version },
125
+ expiresAt: new Date(now().getTime() + (input.ttlMs ?? 3e5)).toISOString()
126
+ });
127
+ if (JSON.stringify(publicPlan).includes("capability://")) {
128
+ throw new Error("Public render plan exposes a capability reference.");
129
+ }
130
+ await options.store.save({ publicPlan, scope: { ...input.scope }, intents, views });
131
+ return publicPlan;
132
+ },
133
+ async resolveIntent(input) {
134
+ const state = await options.store.load(input.planId);
135
+ if (!state) throw new Error("Experience render plan was not found.");
136
+ if (Date.parse(state.publicPlan.expiresAt) <= now().getTime()) throw new Error("Experience render plan has expired.");
137
+ if (!sameScope(state.scope, input.scope)) throw new Error("Experience render plan scope does not match trusted request scope.");
138
+ const intent = state.intents.get(`${input.fragmentId}\0${input.eventName}`);
139
+ if (!intent) throw new Error(`Fragment event "${input.fragmentId}.${input.eventName}" is not granted by the render plan.`);
140
+ return {
141
+ actionId: intent.route.actionId,
142
+ actionVersion: intent.route.version,
143
+ execution: intent.route.execution,
144
+ initiatingReleaseDigest: state.publicPlan.releaseDigest,
145
+ assemblyDigest: state.publicPlan.assemblyDigest
146
+ };
147
+ },
148
+ async resolveView(input) {
149
+ const state = await loadPlan(options.store, input.planId, input.scope, now());
150
+ const view = state.views.get(`${input.fragmentId}\0${input.bindingName}`);
151
+ if (!view) throw new Error(`Fragment binding "${input.fragmentId}.${input.bindingName}" is not granted by the render plan.`);
152
+ return {
153
+ name: view.route.name,
154
+ version: view.route.version,
155
+ initiatingReleaseDigest: state.publicPlan.releaseDigest,
156
+ assemblyDigest: state.publicPlan.assemblyDigest
157
+ };
158
+ }
159
+ };
160
+ }
161
+ function selectTree(release, variantId) {
162
+ if (variantId === void 0) return { treeId: "root", root: release.document.root };
163
+ const variant = release.document.variants?.find((candidate) => candidate.id === variantId);
164
+ if (!variant) throw new Error(`Variant "${variantId}" is not enumerated by the promoted release.`);
165
+ return { treeId: variant.id, root: variant.root };
166
+ }
167
+ function toRenderPlanFragment(fragment, release, treeId, intents, views) {
168
+ const effective = release.effectiveGrants.find((grant) => grant.treeId === treeId && grant.fragmentId === fragment.id);
169
+ if (!effective) throw new Error(`Promoted release has no effective grant for "${treeId}:${fragment.id}".`);
170
+ const events = [];
171
+ const offlineEvents = [];
172
+ const bindings = [];
173
+ if (fragment.dataRef !== void 0) {
174
+ if (!effective.views.includes(fragment.dataRef)) throw new Error(`Fragment data binding "${fragment.id}" exceeds its effective grants.`);
175
+ const route = release.routes.views.find((candidate) => candidate.reference === fragment.dataRef);
176
+ if (!route) throw new Error(`Fragment data binding "${fragment.id}" has no compiled view route.`);
177
+ views.set(`${fragment.id}\0$data`, { fragmentId: fragment.id, bindingName: "$data", route });
178
+ bindings.push("$data");
179
+ }
180
+ for (const [eventName, action] of Object.entries(fragment.actions ?? {})) {
181
+ if (!effective.intents.includes(action.intent)) throw new Error(`Fragment event "${fragment.id}.${eventName}" exceeds its effective grants.`);
182
+ const route = release.routes.intents.find((candidate) => candidate.reference === action.intent);
183
+ if (!route) throw new Error(`Fragment event "${fragment.id}.${eventName}" has no compiled intent route.`);
184
+ intents.set(`${fragment.id}\0${eventName}`, { fragmentId: fragment.id, eventName, route });
185
+ events.push(eventName);
186
+ if (route.execution?.connectivity === "offline-capturable") offlineEvents.push(eventName);
187
+ }
188
+ return Object.freeze({
189
+ id: fragment.id,
190
+ ...fragment.component === void 0 ? {} : { component: fragment.component },
191
+ ...fragment.props === void 0 ? {} : { props: stripCapabilityReferences(fragment.props) },
192
+ ...fragment.slot === void 0 ? {} : { slot: fragment.slot },
193
+ ...events.length === 0 ? {} : { events: Object.freeze(events.sort()) },
194
+ ...offlineEvents.length === 0 ? {} : { offlineEvents: Object.freeze(offlineEvents.sort()) },
195
+ ...bindings.length === 0 ? {} : { bindings: Object.freeze(bindings) },
196
+ ...fragment.children === void 0 ? {} : {
197
+ children: Object.freeze(fragment.children.map((child) => toRenderPlanFragment(child, release, treeId, intents, views)))
198
+ }
199
+ });
200
+ }
201
+ async function loadPlan(store, planId, scope, now) {
202
+ const state = await store.load(planId);
203
+ if (!state) throw new Error("Experience render plan was not found.");
204
+ if (Date.parse(state.publicPlan.expiresAt) <= now.getTime()) throw new Error("Experience render plan has expired.");
205
+ if (!sameScope(state.scope, scope)) throw new Error("Experience render plan scope does not match trusted request scope.");
206
+ return state;
207
+ }
208
+ function stripCapabilityReferences(value) {
209
+ const strip = (candidate) => {
210
+ if (Array.isArray(candidate)) return candidate.map(strip);
211
+ if (candidate && typeof candidate === "object") {
212
+ if ("$ref" in candidate) return void 0;
213
+ return Object.fromEntries(Object.entries(candidate).flatMap(([key, entry]) => {
214
+ const stripped = strip(entry);
215
+ return stripped === void 0 ? [] : [[key, stripped]];
216
+ }));
217
+ }
218
+ return candidate;
219
+ };
220
+ return strip(value);
221
+ }
222
+ function sameScope(left, right) {
223
+ return left.tenantId === right.tenantId && left.spaceId === right.spaceId && left.actorId === right.actorId && left.actorType === right.actorType;
224
+ }
225
+ function createMemoryExperiencePlanStore() {
226
+ const plans = /* @__PURE__ */ new Map();
227
+ return {
228
+ async save(state) {
229
+ if (plans.has(state.publicPlan.planId)) throw new Error(`Experience render plan "${state.publicPlan.planId}" already exists.`);
230
+ plans.set(state.publicPlan.planId, state);
231
+ },
232
+ async load(planId) {
233
+ return plans.get(planId) ?? null;
234
+ }
235
+ };
236
+ }
237
+ // Annotate the CommonJS export names for ESM import in node:
238
+ 0 && (module.exports = {
239
+ createExperienceServerRuntime,
240
+ createMemoryExperiencePlanStore,
241
+ experienceServerChecks,
242
+ runExperienceServerChecks
243
+ });
244
+ //# sourceMappingURL=server.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../server.ts"],"sourcesContent":["import type {\n\tReleaseCryptoPort,\n\tReleaseGenerationStore,\n\tReleaseTrustStore,\n\tExperienceIntentRoute,\n\tExperienceViewRoute,\n\tSduiFragment,\n\tSduiFragmentValue,\n\tSduiReleaseV3,\n} from \"@fabricorg/sdui-release/contracts\";\nimport {\n\tactivateReleaseChannelPointer,\n\tverifySignedExperienceRelease,\n} from \"@fabricorg/sdui-release/contracts\";\nimport type {\n\tExperienceScope,\n\tRenderPlanFragment,\n\tScopedRenderPlan,\n} from \"./contracts\";\n\nexport type {\n\tExperienceScope,\n\tRenderPlanFragment,\n\tScopedRenderPlan,\n} from \"./contracts\";\n\nexport interface ExperiencePlanState {\n\tpublicPlan: ScopedRenderPlan;\n\tscope: ExperienceScope;\n\tintents: ReadonlyMap<string, {\n\t\tfragmentId: string;\n\t\teventName: string;\n\t\troute: ExperienceIntentRoute;\n\t}>;\n\tviews: ReadonlyMap<string, {\n\t\tfragmentId: string;\n\t\tbindingName: string;\n\t\troute: ExperienceViewRoute;\n\t}>;\n}\n\nexport interface ExperiencePlanStore {\n\tsave(state: ExperiencePlanState): Promise<void>;\n\tload(planId: string): Promise<ExperiencePlanState | null>;\n}\n\nexport interface IssueRenderPlanInput {\n\tapplication: string;\n\tchannel: string;\n\tscope: ExperienceScope;\n\tvariantId?: string;\n\tttlMs?: number;\n}\n\nexport interface ResolveIntentInput {\n\tplanId: string;\n\tfragmentId: string;\n\teventName: string;\n\tscope: ExperienceScope;\n}\n\nexport interface ResolvedExperienceIntent {\n\tactionId: `${string}.${string}`;\n\tactionVersion: number;\n\texecution: ExperienceIntentRoute[\"execution\"];\n\tinitiatingReleaseDigest: string;\n\tassemblyDigest: string;\n}\n\nexport interface ResolvedExperienceView {\n\tname: `${string}/${string}`;\n\tversion: string;\n\tinitiatingReleaseDigest: string;\n\tassemblyDigest: string;\n}\n\nexport interface ExperienceServerRuntime {\n\tissuePlan(input: IssueRenderPlanInput): Promise<ScopedRenderPlan>;\n\tresolveIntent(input: ResolveIntentInput): Promise<ResolvedExperienceIntent>;\n\tresolveView(input: {\n\t\tplanId: string;\n\t\tfragmentId: string;\n\t\tbindingName: string;\n\t\tscope: ExperienceScope;\n\t}): Promise<ResolvedExperienceView>;\n}\n\nexport interface ExperienceServerConformanceSubject {\n\truntime: ExperienceServerRuntime;\n\tissue: IssueRenderPlanInput;\n\tintent: { fragmentId: string; eventName: string };\n\twrongScope: ExperienceScope;\n}\n\nexport interface ExperienceServerCheck {\n\tid: \"fabric.experience.plan-opaque.v1\" | \"fabric.experience.scope-isolation.v1\" | \"fabric.experience.intent-resolution.v1\";\n\trun(subject: ExperienceServerConformanceSubject): Promise<string[]>;\n}\n\nexport function experienceServerChecks(): readonly ExperienceServerCheck[] {\n\treturn [\n\t\t{\n\t\t\tid: \"fabric.experience.plan-opaque.v1\",\n\t\t\tasync run(subject) {\n\t\t\t\tconst plan = await subject.runtime.issuePlan(subject.issue);\n\t\t\t\tconst serialized = JSON.stringify(plan);\n\t\t\t\tif (serialized.includes(\"capability://\")) throw new Error(\"Public render plan exposes a capability reference.\");\n\t\t\t\treturn [plan.planId];\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: \"fabric.experience.scope-isolation.v1\",\n\t\t\tasync run(subject) {\n\t\t\t\tconst plan = await subject.runtime.issuePlan(subject.issue);\n\t\t\t\ttry {\n\t\t\t\t\tawait subject.runtime.resolveIntent({\n\t\t\t\t\t\tplanId: plan.planId,\n\t\t\t\t\t\t...subject.intent,\n\t\t\t\t\t\tscope: subject.wrongScope,\n\t\t\t\t\t});\n\t\t\t\t\tthrow new Error(\"Runtime accepted an intent from the wrong trusted scope.\");\n\t\t\t\t} catch (error) {\n\t\t\t\t\tif (error instanceof Error && error.message === \"Runtime accepted an intent from the wrong trusted scope.\") throw error;\n\t\t\t\t}\n\t\t\t\treturn [\"wrong scope rejected\"];\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: \"fabric.experience.intent-resolution.v1\",\n\t\t\tasync run(subject) {\n\t\t\t\tconst plan = await subject.runtime.issuePlan(subject.issue);\n\t\t\t\tconst resolved = await subject.runtime.resolveIntent({\n\t\t\t\t\tplanId: plan.planId,\n\t\t\t\t\t...subject.intent,\n\t\t\t\t\tscope: subject.issue.scope,\n\t\t\t\t});\n\t\t\t\treturn [`${resolved.actionId}@${resolved.actionVersion}`];\n\t\t\t},\n\t\t},\n\t];\n}\n\nexport async function runExperienceServerChecks(\n\tsubject: ExperienceServerConformanceSubject,\n\tchecks: readonly ExperienceServerCheck[] = experienceServerChecks(),\n): Promise<{\n\tpassed: boolean;\n\tchecks: Array<{ id: ExperienceServerCheck[\"id\"]; status: \"passed\" | \"failed\"; evidence: string[]; error?: string }>;\n}> {\n\tconst results: Array<{ id: ExperienceServerCheck[\"id\"]; status: \"passed\" | \"failed\"; evidence: string[]; error?: string }> = [];\n\tfor (const check of checks) {\n\t\ttry {\n\t\t\tresults.push({ id: check.id, status: \"passed\", evidence: await check.run(subject) });\n\t\t} catch (error) {\n\t\t\tresults.push({ id: check.id, status: \"failed\", evidence: [], error: error instanceof Error ? error.message : String(error) });\n\t\t}\n\t}\n\treturn { passed: results.every((result) => result.status === \"passed\"), checks: results };\n}\n\nexport function createExperienceServerRuntime(options: {\n\tstore: ExperiencePlanStore;\n\tresolveChannel(input: { application: string; channel: string }): Promise<{\n\t\tpointerJws: string;\n\t\treleaseJws: string;\n\t}>;\n\tissuer: string;\n\tcrypto: ReleaseCryptoPort;\n\ttrust: ReleaseTrustStore;\n\tgenerations: ReleaseGenerationStore;\n\tnextOpaqueId(): string;\n\tnow?: () => Date;\n}): ExperienceServerRuntime {\n\tconst now = options.now ?? (() => new Date());\n\treturn {\n\t\tasync issuePlan(input) {\n\t\t\tconst admitted = await options.resolveChannel({ application: input.application, channel: input.channel });\n\t\t\tconst pointer = await activateReleaseChannelPointer({\n\t\t\t\tcompactJws: admitted.pointerJws,\n\t\t\t\tcrypto: options.crypto,\n\t\t\t\ttrust: options.trust,\n\t\t\t\tgenerations: options.generations,\n\t\t\t\texpected: { issuer: options.issuer, application: input.application, channel: input.channel },\n\t\t\t\tnow: now(),\n\t\t\t});\n\t\t\tconst release = await verifySignedExperienceRelease({\n\t\t\t\tcompactJws: admitted.releaseJws,\n\t\t\t\tcrypto: options.crypto,\n\t\t\t\ttrust: options.trust,\n\t\t\t\texpected: {\n\t\t\t\t\tissuer: options.issuer,\n\t\t\t\t\tapplication: input.application,\n\t\t\t\t\tassemblyDigest: pointer.assemblyDigest,\n\t\t\t\t},\n\t\t\t});\n\t\t\tif (release.releaseDigest !== pointer.activeReleaseDigest) {\n\t\t\t\tthrow new Error(\"Selected experience release does not match the active release-channel pointer.\");\n\t\t\t}\n\t\t\tconst selected = selectTree(release, input.variantId);\n\t\t\tconst planId = options.nextOpaqueId();\n\t\t\tif (!planId || /capability:\\/\\//.test(planId)) throw new Error(\"Experience plan id must be opaque.\");\n\t\t\tconst intents = new Map<string, ExperiencePlanState[\"intents\"] extends ReadonlyMap<string, infer Value> ? Value : never>();\n\t\t\tconst views = new Map<string, ExperiencePlanState[\"views\"] extends ReadonlyMap<string, infer Value> ? Value : never>();\n\t\t\tconst root = toRenderPlanFragment(selected.root, release, selected.treeId, intents, views);\n\t\t\tconst publicPlan: ScopedRenderPlan = Object.freeze({\n\t\t\t\tformatVersion: 1,\n\t\t\t\tplanId,\n\t\t\t\tapplication: input.application,\n\t\t\t\tchannel: input.channel,\n\t\t\t\treleaseDigest: release.releaseDigest,\n\t\t\t\tassemblyDigest: release.assemblyDigest,\n\t\t\t\ttreeId: selected.treeId,\n\t\t\t\troot,\n\t\t\t\ttokenSet: { name: release.tokenSet.name, version: release.tokenSet.version },\n\t\t\t\texpiresAt: new Date(now().getTime() + (input.ttlMs ?? 300_000)).toISOString(),\n\t\t\t});\n\t\t\tif (JSON.stringify(publicPlan).includes(\"capability://\")) {\n\t\t\t\tthrow new Error(\"Public render plan exposes a capability reference.\");\n\t\t\t}\n\t\t\tawait options.store.save({ publicPlan, scope: { ...input.scope }, intents, views });\n\t\t\treturn publicPlan;\n\t\t},\n\t\tasync resolveIntent(input) {\n\t\t\tconst state = await options.store.load(input.planId);\n\t\t\tif (!state) throw new Error(\"Experience render plan was not found.\");\n\t\t\tif (Date.parse(state.publicPlan.expiresAt) <= now().getTime()) throw new Error(\"Experience render plan has expired.\");\n\t\t\tif (!sameScope(state.scope, input.scope)) throw new Error(\"Experience render plan scope does not match trusted request scope.\");\n\t\t\tconst intent = state.intents.get(`${input.fragmentId}\\u0000${input.eventName}`);\n\t\t\tif (!intent) throw new Error(`Fragment event \"${input.fragmentId}.${input.eventName}\" is not granted by the render plan.`);\n\t\t\treturn {\n\t\t\t\tactionId: intent.route.actionId,\n\t\t\t\tactionVersion: intent.route.version,\n\t\t\t\texecution: intent.route.execution,\n\t\t\t\tinitiatingReleaseDigest: state.publicPlan.releaseDigest,\n\t\t\t\tassemblyDigest: state.publicPlan.assemblyDigest,\n\t\t\t};\n\t\t},\n\t\tasync resolveView(input) {\n\t\t\tconst state = await loadPlan(options.store, input.planId, input.scope, now());\n\t\t\tconst view = state.views.get(`${input.fragmentId}\\u0000${input.bindingName}`);\n\t\t\tif (!view) throw new Error(`Fragment binding \"${input.fragmentId}.${input.bindingName}\" is not granted by the render plan.`);\n\t\t\treturn {\n\t\t\t\tname: view.route.name,\n\t\t\t\tversion: view.route.version,\n\t\t\t\tinitiatingReleaseDigest: state.publicPlan.releaseDigest,\n\t\t\t\tassemblyDigest: state.publicPlan.assemblyDigest,\n\t\t\t};\n\t\t},\n\t};\n}\n\nfunction selectTree(release: SduiReleaseV3, variantId?: string): { treeId: string; root: SduiFragment } {\n\tif (variantId === undefined) return { treeId: \"root\", root: release.document.root };\n\tconst variant = release.document.variants?.find((candidate) => candidate.id === variantId);\n\tif (!variant) throw new Error(`Variant \"${variantId}\" is not enumerated by the promoted release.`);\n\treturn { treeId: variant.id, root: variant.root };\n}\n\nfunction toRenderPlanFragment(\n\tfragment: SduiFragment,\n\trelease: SduiReleaseV3,\n\ttreeId: string,\n\tintents: Map<string, { fragmentId: string; eventName: string; route: ExperienceIntentRoute }>,\n\tviews: Map<string, { fragmentId: string; bindingName: string; route: ExperienceViewRoute }>,\n): RenderPlanFragment {\n\tconst effective = release.effectiveGrants.find((grant) => grant.treeId === treeId && grant.fragmentId === fragment.id);\n\tif (!effective) throw new Error(`Promoted release has no effective grant for \"${treeId}:${fragment.id}\".`);\n\tconst events: string[] = [];\n\tconst offlineEvents: string[] = [];\n\tconst bindings: string[] = [];\n\tif (fragment.dataRef !== undefined) {\n\t\tif (!effective.views.includes(fragment.dataRef)) throw new Error(`Fragment data binding \"${fragment.id}\" exceeds its effective grants.`);\n\t\tconst route = release.routes.views.find((candidate) => candidate.reference === fragment.dataRef);\n\t\tif (!route) throw new Error(`Fragment data binding \"${fragment.id}\" has no compiled view route.`);\n\t\tviews.set(`${fragment.id}\\u0000$data`, { fragmentId: fragment.id, bindingName: \"$data\", route });\n\t\tbindings.push(\"$data\");\n\t}\n\tfor (const [eventName, action] of Object.entries(fragment.actions ?? {})) {\n\t\tif (!effective.intents.includes(action.intent)) throw new Error(`Fragment event \"${fragment.id}.${eventName}\" exceeds its effective grants.`);\n\t\tconst route = release.routes.intents.find((candidate) => candidate.reference === action.intent);\n\t\tif (!route) throw new Error(`Fragment event \"${fragment.id}.${eventName}\" has no compiled intent route.`);\n\t\tintents.set(`${fragment.id}\\u0000${eventName}`, { fragmentId: fragment.id, eventName, route });\n\t\tevents.push(eventName);\n\t\tif (route.execution?.connectivity === \"offline-capturable\") offlineEvents.push(eventName);\n\t}\n\treturn Object.freeze({\n\t\tid: fragment.id,\n\t\t...(fragment.component === undefined ? {} : { component: fragment.component }),\n\t\t...(fragment.props === undefined ? {} : { props: stripCapabilityReferences(fragment.props) }),\n\t\t...(fragment.slot === undefined ? {} : { slot: fragment.slot }),\n\t\t...(events.length === 0 ? {} : { events: Object.freeze(events.sort()) }),\n\t\t...(offlineEvents.length === 0 ? {} : { offlineEvents: Object.freeze(offlineEvents.sort()) }),\n\t\t...(bindings.length === 0 ? {} : { bindings: Object.freeze(bindings) }),\n\t\t...(fragment.children === undefined ? {} : {\n\t\t\tchildren: Object.freeze(fragment.children.map((child) => toRenderPlanFragment(child, release, treeId, intents, views))),\n\t\t}),\n\t});\n}\n\nasync function loadPlan(\n\tstore: ExperiencePlanStore,\n\tplanId: string,\n\tscope: ExperienceScope,\n\tnow: Date,\n): Promise<ExperiencePlanState> {\n\tconst state = await store.load(planId);\n\tif (!state) throw new Error(\"Experience render plan was not found.\");\n\tif (Date.parse(state.publicPlan.expiresAt) <= now.getTime()) throw new Error(\"Experience render plan has expired.\");\n\tif (!sameScope(state.scope, scope)) throw new Error(\"Experience render plan scope does not match trusted request scope.\");\n\treturn state;\n}\n\nfunction stripCapabilityReferences(value: Record<string, SduiFragmentValue>): Record<string, unknown> {\n\tconst strip = (candidate: SduiFragmentValue): unknown => {\n\t\tif (Array.isArray(candidate)) return candidate.map(strip);\n\t\tif (candidate && typeof candidate === \"object\") {\n\t\t\tif (\"$ref\" in candidate) return undefined;\n\t\t\treturn Object.fromEntries(Object.entries(candidate).flatMap(([key, entry]) => {\n\t\t\t\tconst stripped = strip(entry);\n\t\t\t\treturn stripped === undefined ? [] : [[key, stripped]];\n\t\t\t}));\n\t\t}\n\t\treturn candidate;\n\t};\n\treturn strip(value) as Record<string, unknown>;\n}\n\nfunction sameScope(left: ExperienceScope, right: ExperienceScope): boolean {\n\treturn left.tenantId === right.tenantId\n\t\t&& left.spaceId === right.spaceId\n\t\t&& left.actorId === right.actorId\n\t\t&& left.actorType === right.actorType;\n}\n\nexport function createMemoryExperiencePlanStore(): ExperiencePlanStore {\n\tconst plans = new Map<string, ExperiencePlanState>();\n\treturn {\n\t\tasync save(state) {\n\t\t\tif (plans.has(state.publicPlan.planId)) throw new Error(`Experience render plan \"${state.publicPlan.planId}\" already exists.`);\n\t\t\tplans.set(state.publicPlan.planId, state);\n\t\t},\n\t\tasync load(planId) {\n\t\t\treturn plans.get(planId) ?? null;\n\t\t},\n\t};\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,100 @@
1
+ import { ExperienceIntentRoute, ExperienceViewRoute, ReleaseCryptoPort, ReleaseTrustStore, ReleaseGenerationStore } from '@fabricorg/sdui-release/contracts';
2
+ import { ScopedRenderPlan, ExperienceScope } from './contracts.cjs';
3
+ export { RenderPlanFragment } from './contracts.cjs';
4
+
5
+ interface ExperiencePlanState {
6
+ publicPlan: ScopedRenderPlan;
7
+ scope: ExperienceScope;
8
+ intents: ReadonlyMap<string, {
9
+ fragmentId: string;
10
+ eventName: string;
11
+ route: ExperienceIntentRoute;
12
+ }>;
13
+ views: ReadonlyMap<string, {
14
+ fragmentId: string;
15
+ bindingName: string;
16
+ route: ExperienceViewRoute;
17
+ }>;
18
+ }
19
+ interface ExperiencePlanStore {
20
+ save(state: ExperiencePlanState): Promise<void>;
21
+ load(planId: string): Promise<ExperiencePlanState | null>;
22
+ }
23
+ interface IssueRenderPlanInput {
24
+ application: string;
25
+ channel: string;
26
+ scope: ExperienceScope;
27
+ variantId?: string;
28
+ ttlMs?: number;
29
+ }
30
+ interface ResolveIntentInput {
31
+ planId: string;
32
+ fragmentId: string;
33
+ eventName: string;
34
+ scope: ExperienceScope;
35
+ }
36
+ interface ResolvedExperienceIntent {
37
+ actionId: `${string}.${string}`;
38
+ actionVersion: number;
39
+ execution: ExperienceIntentRoute["execution"];
40
+ initiatingReleaseDigest: string;
41
+ assemblyDigest: string;
42
+ }
43
+ interface ResolvedExperienceView {
44
+ name: `${string}/${string}`;
45
+ version: string;
46
+ initiatingReleaseDigest: string;
47
+ assemblyDigest: string;
48
+ }
49
+ interface ExperienceServerRuntime {
50
+ issuePlan(input: IssueRenderPlanInput): Promise<ScopedRenderPlan>;
51
+ resolveIntent(input: ResolveIntentInput): Promise<ResolvedExperienceIntent>;
52
+ resolveView(input: {
53
+ planId: string;
54
+ fragmentId: string;
55
+ bindingName: string;
56
+ scope: ExperienceScope;
57
+ }): Promise<ResolvedExperienceView>;
58
+ }
59
+ interface ExperienceServerConformanceSubject {
60
+ runtime: ExperienceServerRuntime;
61
+ issue: IssueRenderPlanInput;
62
+ intent: {
63
+ fragmentId: string;
64
+ eventName: string;
65
+ };
66
+ wrongScope: ExperienceScope;
67
+ }
68
+ interface ExperienceServerCheck {
69
+ id: "fabric.experience.plan-opaque.v1" | "fabric.experience.scope-isolation.v1" | "fabric.experience.intent-resolution.v1";
70
+ run(subject: ExperienceServerConformanceSubject): Promise<string[]>;
71
+ }
72
+ declare function experienceServerChecks(): readonly ExperienceServerCheck[];
73
+ declare function runExperienceServerChecks(subject: ExperienceServerConformanceSubject, checks?: readonly ExperienceServerCheck[]): Promise<{
74
+ passed: boolean;
75
+ checks: Array<{
76
+ id: ExperienceServerCheck["id"];
77
+ status: "passed" | "failed";
78
+ evidence: string[];
79
+ error?: string;
80
+ }>;
81
+ }>;
82
+ declare function createExperienceServerRuntime(options: {
83
+ store: ExperiencePlanStore;
84
+ resolveChannel(input: {
85
+ application: string;
86
+ channel: string;
87
+ }): Promise<{
88
+ pointerJws: string;
89
+ releaseJws: string;
90
+ }>;
91
+ issuer: string;
92
+ crypto: ReleaseCryptoPort;
93
+ trust: ReleaseTrustStore;
94
+ generations: ReleaseGenerationStore;
95
+ nextOpaqueId(): string;
96
+ now?: () => Date;
97
+ }): ExperienceServerRuntime;
98
+ declare function createMemoryExperiencePlanStore(): ExperiencePlanStore;
99
+
100
+ export { type ExperiencePlanState, type ExperiencePlanStore, ExperienceScope, type ExperienceServerCheck, type ExperienceServerConformanceSubject, type ExperienceServerRuntime, type IssueRenderPlanInput, type ResolveIntentInput, type ResolvedExperienceIntent, type ResolvedExperienceView, ScopedRenderPlan, createExperienceServerRuntime, createMemoryExperiencePlanStore, experienceServerChecks, runExperienceServerChecks };