@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.
- package/CHANGELOG.md +131 -0
- package/LICENSE +21 -0
- package/README.md +20 -0
- package/dist/client.cjs +146 -0
- package/dist/client.cjs.map +1 -0
- package/dist/client.d.cts +23 -0
- package/dist/client.d.ts +23 -0
- package/dist/client.js +119 -0
- package/dist/client.js.map +1 -0
- package/dist/contracts.cjs +19 -0
- package/dist/contracts.cjs.map +1 -0
- package/dist/contracts.d.cts +95 -0
- package/dist/contracts.d.ts +95 -0
- package/dist/contracts.js +1 -0
- package/dist/contracts.js.map +1 -0
- package/dist/offline.cjs +231 -0
- package/dist/offline.cjs.map +1 -0
- package/dist/offline.d.cts +87 -0
- package/dist/offline.d.ts +87 -0
- package/dist/offline.js +205 -0
- package/dist/offline.js.map +1 -0
- package/dist/server.cjs +244 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +100 -0
- package/dist/server.d.ts +100 -0
- package/dist/server.js +219 -0
- package/dist/server.js.map +1 -0
- package/package.json +95 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.2.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
|
+
- 9a5b18a: Close what a fifth review found, including a regression the previous fix introduced.
|
|
30
|
+
|
|
31
|
+
**Ambiguity fired on legal documents.** Counting every declared view made publishing a view at two
|
|
32
|
+
versions a hard failure, and view versioning is a first-class pattern: the platform and the usage
|
|
33
|
+
contract both key a view on `name@version`. It also failed a release over a capability that release
|
|
34
|
+
never touched. Ambiguity is now reported for _granted_ references only, where a versionless reference
|
|
35
|
+
genuinely cannot pick between two versions, and a contract supplied twice gets its own finding
|
|
36
|
+
instead of making every one of its views look ambiguous.
|
|
37
|
+
|
|
38
|
+
**A failed replay is `unknown`, not `refused`.** `failed` is reachable after adapter steps succeeded
|
|
39
|
+
and the domain transaction committed, so telling an operator the action was definitely refused could
|
|
40
|
+
be as wrong as telling them it was applied. Only policy blocks and validation failures are decided
|
|
41
|
+
before any effect. A replay adapter that returns nothing is surfaced rather than counted as applied,
|
|
42
|
+
which was the one line the previous rewrite left untouched.
|
|
43
|
+
|
|
44
|
+
**`parked` and `expired` carry their records**, and reconciliation carries the invocation id. Bare
|
|
45
|
+
counts left nobody able to say which action needs approving or whose work was dropped, and external
|
|
46
|
+
reconciliation needs the id the type exists to enable.
|
|
47
|
+
|
|
48
|
+
Settled-failure classification gained the superseded-action-version case and dropped two it had
|
|
49
|
+
wrong: a future-dated capture comes good as clocks correct, and a gateway missing its replay window
|
|
50
|
+
is a deployment mistake somebody will fix. Discarding either would destroy work.
|
|
51
|
+
|
|
52
|
+
**`assertPortSuiteHasTeeth` takes a set of broken adapters.** Requiring one stub to trip every check
|
|
53
|
+
was unsatisfiable for any suite with a fixture-liveness guard, since the guard bites on the
|
|
54
|
+
complement of what it guards, and two reference ports could not pass their own suites. Port ids now
|
|
55
|
+
permit a hyphen in the first segment, so a port can be defined for every name a manifest can declare,
|
|
56
|
+
and `certifyAdapter` validates its definition rather than issuing evidence the gate reports as
|
|
57
|
+
missing.
|
|
58
|
+
|
|
59
|
+
- 9b2e55b: Close what a fourth review found: each earlier fix closed one instance of a class and left its
|
|
60
|
+
siblings.
|
|
61
|
+
|
|
62
|
+
**Replay disposition is now exhaustive over `ActionStatus`.** The previous fix enumerated the two
|
|
63
|
+
statuses that looked like refusals, so `failed`, `validation_failed` and `waiting_for_approval` fell
|
|
64
|
+
through and were deleted from the queue and reported to the operator as applied — the same bug the
|
|
65
|
+
fix set out to kill, one layer in. Any returned outcome now means the platform holds the invocation,
|
|
66
|
+
so the capture leaves the queue either way; what differs is whether a person needs to hear about it.
|
|
67
|
+
An undeterminable adapter outcome is reported as `unknown` rather than `refused`, because the effect
|
|
68
|
+
may well have happened. `isSettledReplayFailure` learned the refusals the sibling commit introduced,
|
|
69
|
+
including a capture rejected for age, which can never come good.
|
|
70
|
+
|
|
71
|
+
**A replay without `capturedAt` is refused.** It was reachable only through an untyped cast, so
|
|
72
|
+
omitting it skipped the age check entirely and stamped receipt time into durable capture evidence.
|
|
73
|
+
|
|
74
|
+
**A capability can name a namespaced port.** `definePort` required a dot and the manifest validator
|
|
75
|
+
forbade one, so no real capability could name a defined port and the certification gate was
|
|
76
|
+
unreachable. Certification is also checked structurally rather than by the presence of a property, so
|
|
77
|
+
a hand-written claim no longer satisfies the gate that exists to demand evidence, and a port
|
|
78
|
+
requirement narrows its candidates as each constraint is applied.
|
|
79
|
+
|
|
80
|
+
**`assertPortSuiteHasTeeth` names every vacuous check** rather than returning on the first one that
|
|
81
|
+
bites, and a reference port is actually run through it. The payments control asserts its result, so
|
|
82
|
+
an adapter that never charges no longer certifies. The capability-reference grammar matches the
|
|
83
|
+
platform's own view-name rule instead of being stricter than it, and ambiguity is counted by
|
|
84
|
+
occurrence across views and intents rather than by comparing names within one contract.
|
|
85
|
+
|
|
86
|
+
- 0d1f6f6: Close what a third independent review found, including two defects introduced by the previous fix.
|
|
87
|
+
|
|
88
|
+
**Offline replay counted refusals as successes.** `PlatformHost` reports a refused capture by
|
|
89
|
+
_returning_ `reconciliation_required` with the reason, not by throwing, so the queue took its success
|
|
90
|
+
branch, deleted the capture and reported it applied. The reconciliation path was unreachable. Replay
|
|
91
|
+
now reads the returned outcome. The already-applied branch is removed entirely: a repeated key
|
|
92
|
+
returns the original invocation without complaint, and an idempotency _conflict_ means the key was
|
|
93
|
+
reused for a materially different command, so that capture was never applied and is kept rather than
|
|
94
|
+
discarded.
|
|
95
|
+
|
|
96
|
+
**`deriveAuthorizationGrants` defaulted open and reached the signed artifact.** Requirements are
|
|
97
|
+
optional metadata, so a capability declaring none granted everything to an actor holding nothing.
|
|
98
|
+
Grants are also carried onto a release unexamined and promotion signs a route for each, so a surplus
|
|
99
|
+
grant became a signed route to a capability the screen never renders. Undeclared requirements now
|
|
100
|
+
withhold by default, and `document` scopes the result to references the screen actually binds.
|
|
101
|
+
|
|
102
|
+
Several conformance checks could not fail: a search suite passed an adapter returning nothing, a
|
|
103
|
+
payments suite passed a double-charger that omitted a provider reference, an identity suite certified
|
|
104
|
+
timestamps the scope check then rejected on every request, an MFE probe could be refused for an
|
|
105
|
+
unrelated reason, and a channel check accepted any exception as a refusal. Each now proves what it
|
|
106
|
+
claims. Promotion configuration rejects anything reading like a key rather than two spellings,
|
|
107
|
+
rejects unknown fields, and constrains the application name that decides the default output path.
|
|
108
|
+
|
|
109
|
+
### Patch Changes
|
|
110
|
+
|
|
111
|
+
- Updated dependencies [8ce5e06]
|
|
112
|
+
- Updated dependencies [a598fb9]
|
|
113
|
+
- Updated dependencies [8f75ac9]
|
|
114
|
+
- Updated dependencies [9a5b18a]
|
|
115
|
+
- Updated dependencies [9b2e55b]
|
|
116
|
+
- Updated dependencies [0d1f6f6]
|
|
117
|
+
- Updated dependencies [274d948]
|
|
118
|
+
- @fabricorg/sdui-release@0.5.0
|
|
119
|
+
|
|
120
|
+
## 0.1.0
|
|
121
|
+
|
|
122
|
+
### Minor Changes
|
|
123
|
+
|
|
124
|
+
- Add signed channel and release admission, scoped render plans, fragment-event
|
|
125
|
+
sessions, governed view resolution, public conformance, and encrypted-store-required
|
|
126
|
+
offline capture contracts.
|
|
127
|
+
|
|
128
|
+
### Patch Changes
|
|
129
|
+
|
|
130
|
+
- Updated dependencies
|
|
131
|
+
- @fabricorg/sdui-release@0.4.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,20 @@
|
|
|
1
|
+
# `@fabricorg/experience-runtime`
|
|
2
|
+
|
|
3
|
+
The supported experience composition runtime, split by trust:
|
|
4
|
+
|
|
5
|
+
- `/server` verifies admitted releases and issues scope-bound render plans.
|
|
6
|
+
- `/client` consumes plans through fragment events and named bindings.
|
|
7
|
+
- `/offline` captures eligible events for governed replay.
|
|
8
|
+
- the root export contains serializable contracts only.
|
|
9
|
+
|
|
10
|
+
Client checks detect accidents. They are not authorization. Server plan resolution,
|
|
11
|
+
ProjectionHost, and PlatformHost remain authoritative.
|
|
12
|
+
|
|
13
|
+
Offline capture stores no credentials or identity claims and requires an
|
|
14
|
+
encrypted-at-rest adapter in production. A capture expires no later than its scoped
|
|
15
|
+
plan, so applications that support a longer offline window must issue an explicitly
|
|
16
|
+
bounded offline-capable plan. Replay always returns through the gateway and current
|
|
17
|
+
Host authority rules.
|
|
18
|
+
|
|
19
|
+
Replacement runtimes can run `experienceServerChecks` and replacement clients can run
|
|
20
|
+
`experienceClientChecks` without importing Fabric internals.
|
package/dist/client.cjs
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
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
|
+
// client.ts
|
|
21
|
+
var client_exports = {};
|
|
22
|
+
__export(client_exports, {
|
|
23
|
+
createExperienceSession: () => createExperienceSession,
|
|
24
|
+
experienceClientChecks: () => experienceClientChecks,
|
|
25
|
+
runExperienceClientChecks: () => runExperienceClientChecks
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(client_exports);
|
|
28
|
+
function createExperienceSession(plan, gateway) {
|
|
29
|
+
const fragments = indexFragments(plan.root);
|
|
30
|
+
return {
|
|
31
|
+
plan,
|
|
32
|
+
async invoke(fragmentId, eventName, parameters, idempotencyKey) {
|
|
33
|
+
const fragment = fragments.get(fragmentId);
|
|
34
|
+
if (!fragment?.events?.includes(eventName)) {
|
|
35
|
+
throw new Error(`Fragment event "${fragmentId}.${eventName}" is not in the render plan.`);
|
|
36
|
+
}
|
|
37
|
+
return gateway.invoke({
|
|
38
|
+
planId: plan.planId,
|
|
39
|
+
fragmentId,
|
|
40
|
+
eventName,
|
|
41
|
+
parameters,
|
|
42
|
+
...idempotencyKey === void 0 ? {} : { idempotencyKey }
|
|
43
|
+
});
|
|
44
|
+
},
|
|
45
|
+
async read(fragmentId, bindingName, parameters) {
|
|
46
|
+
const fragment = fragments.get(fragmentId);
|
|
47
|
+
if (!fragment?.bindings?.includes(bindingName)) {
|
|
48
|
+
throw new Error(`Fragment binding "${fragmentId}.${bindingName}" is not in the render plan.`);
|
|
49
|
+
}
|
|
50
|
+
return gateway.read({
|
|
51
|
+
planId: plan.planId,
|
|
52
|
+
fragmentId,
|
|
53
|
+
bindingName,
|
|
54
|
+
...parameters === void 0 ? {} : { parameters }
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function experienceClientChecks() {
|
|
60
|
+
const plan = {
|
|
61
|
+
formatVersion: 1,
|
|
62
|
+
planId: "conformance-plan",
|
|
63
|
+
application: "conformance",
|
|
64
|
+
channel: "test",
|
|
65
|
+
releaseDigest: "a".repeat(64),
|
|
66
|
+
assemblyDigest: "b".repeat(64),
|
|
67
|
+
treeId: "root",
|
|
68
|
+
tokenSet: { name: "conformance", version: "1.0.0" },
|
|
69
|
+
expiresAt: "2099-01-01T00:00:00.000Z",
|
|
70
|
+
root: { id: "root", component: "Action", events: ["press"], bindings: ["summary"] }
|
|
71
|
+
};
|
|
72
|
+
return [
|
|
73
|
+
{
|
|
74
|
+
id: "fabric.experience.client-tuple-routing.v1",
|
|
75
|
+
async run(subject) {
|
|
76
|
+
const calls = [];
|
|
77
|
+
const session = subject.create(plan, {
|
|
78
|
+
async invoke(request) {
|
|
79
|
+
calls.push(`${request.fragmentId}.${request.eventName}`);
|
|
80
|
+
return { status: "accepted" };
|
|
81
|
+
},
|
|
82
|
+
async read(request) {
|
|
83
|
+
calls.push(`${request.fragmentId}.${request.bindingName}`);
|
|
84
|
+
return { data: { ok: true } };
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
await session.invoke("root", "press", {});
|
|
88
|
+
await session.read("root", "summary");
|
|
89
|
+
if (calls.join(",") !== "root.press,root.summary") throw new Error("Client did not route through fragment-event and fragment-binding tuples.");
|
|
90
|
+
return calls;
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: "fabric.experience.client-plan-denial.v1",
|
|
95
|
+
async run(subject) {
|
|
96
|
+
let called = false;
|
|
97
|
+
const session = subject.create(plan, {
|
|
98
|
+
async invoke() {
|
|
99
|
+
called = true;
|
|
100
|
+
return { status: "accepted" };
|
|
101
|
+
},
|
|
102
|
+
async read() {
|
|
103
|
+
called = true;
|
|
104
|
+
return { data: null };
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
let rejected = false;
|
|
108
|
+
try {
|
|
109
|
+
await session.invoke("root", "undeclared", {});
|
|
110
|
+
} catch {
|
|
111
|
+
rejected = true;
|
|
112
|
+
}
|
|
113
|
+
if (!rejected || called) throw new Error("Client forwarded an event that was absent from the scoped plan.");
|
|
114
|
+
return ["undeclared event rejected locally"];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
];
|
|
118
|
+
}
|
|
119
|
+
async function runExperienceClientChecks(subject, checks = experienceClientChecks()) {
|
|
120
|
+
const results = [];
|
|
121
|
+
for (const check of checks) {
|
|
122
|
+
try {
|
|
123
|
+
results.push({ id: check.id, status: "passed", evidence: await check.run(subject) });
|
|
124
|
+
} catch (error) {
|
|
125
|
+
results.push({ id: check.id, status: "failed", evidence: [], error: error instanceof Error ? error.message : String(error) });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return { passed: results.every((result) => result.status === "passed"), checks: results };
|
|
129
|
+
}
|
|
130
|
+
function indexFragments(root) {
|
|
131
|
+
const fragments = /* @__PURE__ */ new Map();
|
|
132
|
+
const visit = (fragment) => {
|
|
133
|
+
if (fragments.has(fragment.id)) throw new Error(`Render plan repeats fragment "${fragment.id}".`);
|
|
134
|
+
fragments.set(fragment.id, fragment);
|
|
135
|
+
for (const child of fragment.children ?? []) visit(child);
|
|
136
|
+
};
|
|
137
|
+
visit(root);
|
|
138
|
+
return fragments;
|
|
139
|
+
}
|
|
140
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
141
|
+
0 && (module.exports = {
|
|
142
|
+
createExperienceSession,
|
|
143
|
+
experienceClientChecks,
|
|
144
|
+
runExperienceClientChecks
|
|
145
|
+
});
|
|
146
|
+
//# sourceMappingURL=client.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../client.ts"],"sourcesContent":["import type {\n\tExperienceGatewayClient,\n\tExperienceSession,\n\tRenderPlanFragment,\n\tScopedRenderPlan,\n} from \"./contracts\";\n\nexport type {\n\tExperienceGatewayClient,\n\tExperienceIntentRequest,\n\tExperienceReadRequest,\n\tExperienceSession,\n\tRenderPlanFragment,\n\tScopedRenderPlan,\n} from \"./contracts\";\n\nexport function createExperienceSession(\n\tplan: ScopedRenderPlan,\n\tgateway: ExperienceGatewayClient,\n): ExperienceSession {\n\tconst fragments = indexFragments(plan.root);\n\treturn {\n\t\tplan,\n\t\tasync invoke(fragmentId, eventName, parameters, idempotencyKey) {\n\t\t\tconst fragment = fragments.get(fragmentId);\n\t\t\tif (!fragment?.events?.includes(eventName)) {\n\t\t\t\tthrow new Error(`Fragment event \"${fragmentId}.${eventName}\" is not in the render plan.`);\n\t\t\t}\n\t\t\treturn gateway.invoke({\n\t\t\t\tplanId: plan.planId,\n\t\t\t\tfragmentId,\n\t\t\t\teventName,\n\t\t\t\tparameters,\n\t\t\t\t...(idempotencyKey === undefined ? {} : { idempotencyKey }),\n\t\t\t});\n\t\t},\n\t\tasync read(fragmentId, bindingName, parameters) {\n\t\t\tconst fragment = fragments.get(fragmentId);\n\t\t\tif (!fragment?.bindings?.includes(bindingName)) {\n\t\t\t\tthrow new Error(`Fragment binding \"${fragmentId}.${bindingName}\" is not in the render plan.`);\n\t\t\t}\n\t\t\treturn gateway.read({\n\t\t\t\tplanId: plan.planId,\n\t\t\t\tfragmentId,\n\t\t\t\tbindingName,\n\t\t\t\t...(parameters === undefined ? {} : { parameters }),\n\t\t\t});\n\t\t},\n\t};\n}\n\nexport interface ExperienceClientConformanceSubject {\n\tcreate(plan: ScopedRenderPlan, gateway: ExperienceGatewayClient): ExperienceSession;\n}\n\nexport interface ExperienceClientCheck {\n\tid: \"fabric.experience.client-tuple-routing.v1\" | \"fabric.experience.client-plan-denial.v1\";\n\trun(subject: ExperienceClientConformanceSubject): Promise<string[]>;\n}\n\nexport function experienceClientChecks(): readonly ExperienceClientCheck[] {\n\tconst plan: ScopedRenderPlan = {\n\t\tformatVersion: 1,\n\t\tplanId: \"conformance-plan\",\n\t\tapplication: \"conformance\",\n\t\tchannel: \"test\",\n\t\treleaseDigest: \"a\".repeat(64),\n\t\tassemblyDigest: \"b\".repeat(64),\n\t\ttreeId: \"root\",\n\t\ttokenSet: { name: \"conformance\", version: \"1.0.0\" },\n\t\texpiresAt: \"2099-01-01T00:00:00.000Z\",\n\t\troot: { id: \"root\", component: \"Action\", events: [\"press\"], bindings: [\"summary\"] },\n\t};\n\treturn [\n\t\t{\n\t\t\tid: \"fabric.experience.client-tuple-routing.v1\",\n\t\t\tasync run(subject) {\n\t\t\t\tconst calls: string[] = [];\n\t\t\t\tconst session = subject.create(plan, {\n\t\t\t\t\tasync invoke(request) { calls.push(`${request.fragmentId}.${request.eventName}`); return { status: \"accepted\" }; },\n\t\t\t\t\tasync read(request) { calls.push(`${request.fragmentId}.${request.bindingName}`); return { data: { ok: true } }; },\n\t\t\t\t});\n\t\t\t\tawait session.invoke(\"root\", \"press\", {});\n\t\t\t\tawait session.read(\"root\", \"summary\");\n\t\t\t\tif (calls.join(\",\") !== \"root.press,root.summary\") throw new Error(\"Client did not route through fragment-event and fragment-binding tuples.\");\n\t\t\t\treturn calls;\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: \"fabric.experience.client-plan-denial.v1\",\n\t\t\tasync run(subject) {\n\t\t\t\tlet called = false;\n\t\t\t\tconst session = subject.create(plan, {\n\t\t\t\t\tasync invoke() { called = true; return { status: \"accepted\" }; },\n\t\t\t\t\tasync read() { called = true; return { data: null }; },\n\t\t\t\t});\n\t\t\t\tlet rejected = false;\n\t\t\t\ttry { await session.invoke(\"root\", \"undeclared\", {}); } catch { rejected = true; }\n\t\t\t\tif (!rejected || called) throw new Error(\"Client forwarded an event that was absent from the scoped plan.\");\n\t\t\t\treturn [\"undeclared event rejected locally\"];\n\t\t\t},\n\t\t},\n\t];\n}\n\nexport async function runExperienceClientChecks(\n\tsubject: ExperienceClientConformanceSubject,\n\tchecks: readonly ExperienceClientCheck[] = experienceClientChecks(),\n): Promise<{ passed: boolean; checks: Array<{ id: ExperienceClientCheck[\"id\"]; status: \"passed\" | \"failed\"; evidence: string[]; error?: string }> }> {\n\tconst results: Array<{ id: ExperienceClientCheck[\"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\nfunction indexFragments(root: RenderPlanFragment): Map<string, RenderPlanFragment> {\n\tconst fragments = new Map<string, RenderPlanFragment>();\n\tconst visit = (fragment: RenderPlanFragment): void => {\n\t\tif (fragments.has(fragment.id)) throw new Error(`Render plan repeats fragment \"${fragment.id}\".`);\n\t\tfragments.set(fragment.id, fragment);\n\t\tfor (const child of fragment.children ?? []) visit(child);\n\t};\n\tvisit(root);\n\treturn fragments;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ScopedRenderPlan, ExperienceGatewayClient, ExperienceSession } from './contracts.cjs';
|
|
2
|
+
export { ExperienceIntentRequest, ExperienceReadRequest, RenderPlanFragment } from './contracts.cjs';
|
|
3
|
+
|
|
4
|
+
declare function createExperienceSession(plan: ScopedRenderPlan, gateway: ExperienceGatewayClient): ExperienceSession;
|
|
5
|
+
interface ExperienceClientConformanceSubject {
|
|
6
|
+
create(plan: ScopedRenderPlan, gateway: ExperienceGatewayClient): ExperienceSession;
|
|
7
|
+
}
|
|
8
|
+
interface ExperienceClientCheck {
|
|
9
|
+
id: "fabric.experience.client-tuple-routing.v1" | "fabric.experience.client-plan-denial.v1";
|
|
10
|
+
run(subject: ExperienceClientConformanceSubject): Promise<string[]>;
|
|
11
|
+
}
|
|
12
|
+
declare function experienceClientChecks(): readonly ExperienceClientCheck[];
|
|
13
|
+
declare function runExperienceClientChecks(subject: ExperienceClientConformanceSubject, checks?: readonly ExperienceClientCheck[]): Promise<{
|
|
14
|
+
passed: boolean;
|
|
15
|
+
checks: Array<{
|
|
16
|
+
id: ExperienceClientCheck["id"];
|
|
17
|
+
status: "passed" | "failed";
|
|
18
|
+
evidence: string[];
|
|
19
|
+
error?: string;
|
|
20
|
+
}>;
|
|
21
|
+
}>;
|
|
22
|
+
|
|
23
|
+
export { type ExperienceClientCheck, type ExperienceClientConformanceSubject, ExperienceGatewayClient, ExperienceSession, ScopedRenderPlan, createExperienceSession, experienceClientChecks, runExperienceClientChecks };
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ScopedRenderPlan, ExperienceGatewayClient, ExperienceSession } from './contracts.js';
|
|
2
|
+
export { ExperienceIntentRequest, ExperienceReadRequest, RenderPlanFragment } from './contracts.js';
|
|
3
|
+
|
|
4
|
+
declare function createExperienceSession(plan: ScopedRenderPlan, gateway: ExperienceGatewayClient): ExperienceSession;
|
|
5
|
+
interface ExperienceClientConformanceSubject {
|
|
6
|
+
create(plan: ScopedRenderPlan, gateway: ExperienceGatewayClient): ExperienceSession;
|
|
7
|
+
}
|
|
8
|
+
interface ExperienceClientCheck {
|
|
9
|
+
id: "fabric.experience.client-tuple-routing.v1" | "fabric.experience.client-plan-denial.v1";
|
|
10
|
+
run(subject: ExperienceClientConformanceSubject): Promise<string[]>;
|
|
11
|
+
}
|
|
12
|
+
declare function experienceClientChecks(): readonly ExperienceClientCheck[];
|
|
13
|
+
declare function runExperienceClientChecks(subject: ExperienceClientConformanceSubject, checks?: readonly ExperienceClientCheck[]): Promise<{
|
|
14
|
+
passed: boolean;
|
|
15
|
+
checks: Array<{
|
|
16
|
+
id: ExperienceClientCheck["id"];
|
|
17
|
+
status: "passed" | "failed";
|
|
18
|
+
evidence: string[];
|
|
19
|
+
error?: string;
|
|
20
|
+
}>;
|
|
21
|
+
}>;
|
|
22
|
+
|
|
23
|
+
export { type ExperienceClientCheck, type ExperienceClientConformanceSubject, ExperienceGatewayClient, ExperienceSession, ScopedRenderPlan, createExperienceSession, experienceClientChecks, runExperienceClientChecks };
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// client.ts
|
|
2
|
+
function createExperienceSession(plan, gateway) {
|
|
3
|
+
const fragments = indexFragments(plan.root);
|
|
4
|
+
return {
|
|
5
|
+
plan,
|
|
6
|
+
async invoke(fragmentId, eventName, parameters, idempotencyKey) {
|
|
7
|
+
const fragment = fragments.get(fragmentId);
|
|
8
|
+
if (!fragment?.events?.includes(eventName)) {
|
|
9
|
+
throw new Error(`Fragment event "${fragmentId}.${eventName}" is not in the render plan.`);
|
|
10
|
+
}
|
|
11
|
+
return gateway.invoke({
|
|
12
|
+
planId: plan.planId,
|
|
13
|
+
fragmentId,
|
|
14
|
+
eventName,
|
|
15
|
+
parameters,
|
|
16
|
+
...idempotencyKey === void 0 ? {} : { idempotencyKey }
|
|
17
|
+
});
|
|
18
|
+
},
|
|
19
|
+
async read(fragmentId, bindingName, parameters) {
|
|
20
|
+
const fragment = fragments.get(fragmentId);
|
|
21
|
+
if (!fragment?.bindings?.includes(bindingName)) {
|
|
22
|
+
throw new Error(`Fragment binding "${fragmentId}.${bindingName}" is not in the render plan.`);
|
|
23
|
+
}
|
|
24
|
+
return gateway.read({
|
|
25
|
+
planId: plan.planId,
|
|
26
|
+
fragmentId,
|
|
27
|
+
bindingName,
|
|
28
|
+
...parameters === void 0 ? {} : { parameters }
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function experienceClientChecks() {
|
|
34
|
+
const plan = {
|
|
35
|
+
formatVersion: 1,
|
|
36
|
+
planId: "conformance-plan",
|
|
37
|
+
application: "conformance",
|
|
38
|
+
channel: "test",
|
|
39
|
+
releaseDigest: "a".repeat(64),
|
|
40
|
+
assemblyDigest: "b".repeat(64),
|
|
41
|
+
treeId: "root",
|
|
42
|
+
tokenSet: { name: "conformance", version: "1.0.0" },
|
|
43
|
+
expiresAt: "2099-01-01T00:00:00.000Z",
|
|
44
|
+
root: { id: "root", component: "Action", events: ["press"], bindings: ["summary"] }
|
|
45
|
+
};
|
|
46
|
+
return [
|
|
47
|
+
{
|
|
48
|
+
id: "fabric.experience.client-tuple-routing.v1",
|
|
49
|
+
async run(subject) {
|
|
50
|
+
const calls = [];
|
|
51
|
+
const session = subject.create(plan, {
|
|
52
|
+
async invoke(request) {
|
|
53
|
+
calls.push(`${request.fragmentId}.${request.eventName}`);
|
|
54
|
+
return { status: "accepted" };
|
|
55
|
+
},
|
|
56
|
+
async read(request) {
|
|
57
|
+
calls.push(`${request.fragmentId}.${request.bindingName}`);
|
|
58
|
+
return { data: { ok: true } };
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
await session.invoke("root", "press", {});
|
|
62
|
+
await session.read("root", "summary");
|
|
63
|
+
if (calls.join(",") !== "root.press,root.summary") throw new Error("Client did not route through fragment-event and fragment-binding tuples.");
|
|
64
|
+
return calls;
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: "fabric.experience.client-plan-denial.v1",
|
|
69
|
+
async run(subject) {
|
|
70
|
+
let called = false;
|
|
71
|
+
const session = subject.create(plan, {
|
|
72
|
+
async invoke() {
|
|
73
|
+
called = true;
|
|
74
|
+
return { status: "accepted" };
|
|
75
|
+
},
|
|
76
|
+
async read() {
|
|
77
|
+
called = true;
|
|
78
|
+
return { data: null };
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
let rejected = false;
|
|
82
|
+
try {
|
|
83
|
+
await session.invoke("root", "undeclared", {});
|
|
84
|
+
} catch {
|
|
85
|
+
rejected = true;
|
|
86
|
+
}
|
|
87
|
+
if (!rejected || called) throw new Error("Client forwarded an event that was absent from the scoped plan.");
|
|
88
|
+
return ["undeclared event rejected locally"];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
];
|
|
92
|
+
}
|
|
93
|
+
async function runExperienceClientChecks(subject, checks = experienceClientChecks()) {
|
|
94
|
+
const results = [];
|
|
95
|
+
for (const check of checks) {
|
|
96
|
+
try {
|
|
97
|
+
results.push({ id: check.id, status: "passed", evidence: await check.run(subject) });
|
|
98
|
+
} catch (error) {
|
|
99
|
+
results.push({ id: check.id, status: "failed", evidence: [], error: error instanceof Error ? error.message : String(error) });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return { passed: results.every((result) => result.status === "passed"), checks: results };
|
|
103
|
+
}
|
|
104
|
+
function indexFragments(root) {
|
|
105
|
+
const fragments = /* @__PURE__ */ new Map();
|
|
106
|
+
const visit = (fragment) => {
|
|
107
|
+
if (fragments.has(fragment.id)) throw new Error(`Render plan repeats fragment "${fragment.id}".`);
|
|
108
|
+
fragments.set(fragment.id, fragment);
|
|
109
|
+
for (const child of fragment.children ?? []) visit(child);
|
|
110
|
+
};
|
|
111
|
+
visit(root);
|
|
112
|
+
return fragments;
|
|
113
|
+
}
|
|
114
|
+
export {
|
|
115
|
+
createExperienceSession,
|
|
116
|
+
experienceClientChecks,
|
|
117
|
+
runExperienceClientChecks
|
|
118
|
+
};
|
|
119
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../client.ts"],"sourcesContent":["import type {\n\tExperienceGatewayClient,\n\tExperienceSession,\n\tRenderPlanFragment,\n\tScopedRenderPlan,\n} from \"./contracts\";\n\nexport type {\n\tExperienceGatewayClient,\n\tExperienceIntentRequest,\n\tExperienceReadRequest,\n\tExperienceSession,\n\tRenderPlanFragment,\n\tScopedRenderPlan,\n} from \"./contracts\";\n\nexport function createExperienceSession(\n\tplan: ScopedRenderPlan,\n\tgateway: ExperienceGatewayClient,\n): ExperienceSession {\n\tconst fragments = indexFragments(plan.root);\n\treturn {\n\t\tplan,\n\t\tasync invoke(fragmentId, eventName, parameters, idempotencyKey) {\n\t\t\tconst fragment = fragments.get(fragmentId);\n\t\t\tif (!fragment?.events?.includes(eventName)) {\n\t\t\t\tthrow new Error(`Fragment event \"${fragmentId}.${eventName}\" is not in the render plan.`);\n\t\t\t}\n\t\t\treturn gateway.invoke({\n\t\t\t\tplanId: plan.planId,\n\t\t\t\tfragmentId,\n\t\t\t\teventName,\n\t\t\t\tparameters,\n\t\t\t\t...(idempotencyKey === undefined ? {} : { idempotencyKey }),\n\t\t\t});\n\t\t},\n\t\tasync read(fragmentId, bindingName, parameters) {\n\t\t\tconst fragment = fragments.get(fragmentId);\n\t\t\tif (!fragment?.bindings?.includes(bindingName)) {\n\t\t\t\tthrow new Error(`Fragment binding \"${fragmentId}.${bindingName}\" is not in the render plan.`);\n\t\t\t}\n\t\t\treturn gateway.read({\n\t\t\t\tplanId: plan.planId,\n\t\t\t\tfragmentId,\n\t\t\t\tbindingName,\n\t\t\t\t...(parameters === undefined ? {} : { parameters }),\n\t\t\t});\n\t\t},\n\t};\n}\n\nexport interface ExperienceClientConformanceSubject {\n\tcreate(plan: ScopedRenderPlan, gateway: ExperienceGatewayClient): ExperienceSession;\n}\n\nexport interface ExperienceClientCheck {\n\tid: \"fabric.experience.client-tuple-routing.v1\" | \"fabric.experience.client-plan-denial.v1\";\n\trun(subject: ExperienceClientConformanceSubject): Promise<string[]>;\n}\n\nexport function experienceClientChecks(): readonly ExperienceClientCheck[] {\n\tconst plan: ScopedRenderPlan = {\n\t\tformatVersion: 1,\n\t\tplanId: \"conformance-plan\",\n\t\tapplication: \"conformance\",\n\t\tchannel: \"test\",\n\t\treleaseDigest: \"a\".repeat(64),\n\t\tassemblyDigest: \"b\".repeat(64),\n\t\ttreeId: \"root\",\n\t\ttokenSet: { name: \"conformance\", version: \"1.0.0\" },\n\t\texpiresAt: \"2099-01-01T00:00:00.000Z\",\n\t\troot: { id: \"root\", component: \"Action\", events: [\"press\"], bindings: [\"summary\"] },\n\t};\n\treturn [\n\t\t{\n\t\t\tid: \"fabric.experience.client-tuple-routing.v1\",\n\t\t\tasync run(subject) {\n\t\t\t\tconst calls: string[] = [];\n\t\t\t\tconst session = subject.create(plan, {\n\t\t\t\t\tasync invoke(request) { calls.push(`${request.fragmentId}.${request.eventName}`); return { status: \"accepted\" }; },\n\t\t\t\t\tasync read(request) { calls.push(`${request.fragmentId}.${request.bindingName}`); return { data: { ok: true } }; },\n\t\t\t\t});\n\t\t\t\tawait session.invoke(\"root\", \"press\", {});\n\t\t\t\tawait session.read(\"root\", \"summary\");\n\t\t\t\tif (calls.join(\",\") !== \"root.press,root.summary\") throw new Error(\"Client did not route through fragment-event and fragment-binding tuples.\");\n\t\t\t\treturn calls;\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: \"fabric.experience.client-plan-denial.v1\",\n\t\t\tasync run(subject) {\n\t\t\t\tlet called = false;\n\t\t\t\tconst session = subject.create(plan, {\n\t\t\t\t\tasync invoke() { called = true; return { status: \"accepted\" }; },\n\t\t\t\t\tasync read() { called = true; return { data: null }; },\n\t\t\t\t});\n\t\t\t\tlet rejected = false;\n\t\t\t\ttry { await session.invoke(\"root\", \"undeclared\", {}); } catch { rejected = true; }\n\t\t\t\tif (!rejected || called) throw new Error(\"Client forwarded an event that was absent from the scoped plan.\");\n\t\t\t\treturn [\"undeclared event rejected locally\"];\n\t\t\t},\n\t\t},\n\t];\n}\n\nexport async function runExperienceClientChecks(\n\tsubject: ExperienceClientConformanceSubject,\n\tchecks: readonly ExperienceClientCheck[] = experienceClientChecks(),\n): Promise<{ passed: boolean; checks: Array<{ id: ExperienceClientCheck[\"id\"]; status: \"passed\" | \"failed\"; evidence: string[]; error?: string }> }> {\n\tconst results: Array<{ id: ExperienceClientCheck[\"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\nfunction indexFragments(root: RenderPlanFragment): Map<string, RenderPlanFragment> {\n\tconst fragments = new Map<string, RenderPlanFragment>();\n\tconst visit = (fragment: RenderPlanFragment): void => {\n\t\tif (fragments.has(fragment.id)) throw new Error(`Render plan repeats fragment \"${fragment.id}\".`);\n\t\tfragments.set(fragment.id, fragment);\n\t\tfor (const child of fragment.children ?? []) visit(child);\n\t};\n\tvisit(root);\n\treturn fragments;\n}\n"],"mappings":";AAgBO,SAAS,wBACf,MACA,SACoB;AACpB,QAAM,YAAY,eAAe,KAAK,IAAI;AAC1C,SAAO;AAAA,IACN;AAAA,IACA,MAAM,OAAO,YAAY,WAAW,YAAY,gBAAgB;AAC/D,YAAM,WAAW,UAAU,IAAI,UAAU;AACzC,UAAI,CAAC,UAAU,QAAQ,SAAS,SAAS,GAAG;AAC3C,cAAM,IAAI,MAAM,mBAAmB,UAAU,IAAI,SAAS,8BAA8B;AAAA,MACzF;AACA,aAAO,QAAQ,OAAO;AAAA,QACrB,QAAQ,KAAK;AAAA,QACb;AAAA,QACA;AAAA,QACA;AAAA,QACA,GAAI,mBAAmB,SAAY,CAAC,IAAI,EAAE,eAAe;AAAA,MAC1D,CAAC;AAAA,IACF;AAAA,IACA,MAAM,KAAK,YAAY,aAAa,YAAY;AAC/C,YAAM,WAAW,UAAU,IAAI,UAAU;AACzC,UAAI,CAAC,UAAU,UAAU,SAAS,WAAW,GAAG;AAC/C,cAAM,IAAI,MAAM,qBAAqB,UAAU,IAAI,WAAW,8BAA8B;AAAA,MAC7F;AACA,aAAO,QAAQ,KAAK;AAAA,QACnB,QAAQ,KAAK;AAAA,QACb;AAAA,QACA;AAAA,QACA,GAAI,eAAe,SAAY,CAAC,IAAI,EAAE,WAAW;AAAA,MAClD,CAAC;AAAA,IACF;AAAA,EACD;AACD;AAWO,SAAS,yBAA2D;AAC1E,QAAM,OAAyB;AAAA,IAC9B,eAAe;AAAA,IACf,QAAQ;AAAA,IACR,aAAa;AAAA,IACb,SAAS;AAAA,IACT,eAAe,IAAI,OAAO,EAAE;AAAA,IAC5B,gBAAgB,IAAI,OAAO,EAAE;AAAA,IAC7B,QAAQ;AAAA,IACR,UAAU,EAAE,MAAM,eAAe,SAAS,QAAQ;AAAA,IAClD,WAAW;AAAA,IACX,MAAM,EAAE,IAAI,QAAQ,WAAW,UAAU,QAAQ,CAAC,OAAO,GAAG,UAAU,CAAC,SAAS,EAAE;AAAA,EACnF;AACA,SAAO;AAAA,IACN;AAAA,MACC,IAAI;AAAA,MACJ,MAAM,IAAI,SAAS;AAClB,cAAM,QAAkB,CAAC;AACzB,cAAM,UAAU,QAAQ,OAAO,MAAM;AAAA,UACpC,MAAM,OAAO,SAAS;AAAE,kBAAM,KAAK,GAAG,QAAQ,UAAU,IAAI,QAAQ,SAAS,EAAE;AAAG,mBAAO,EAAE,QAAQ,WAAW;AAAA,UAAG;AAAA,UACjH,MAAM,KAAK,SAAS;AAAE,kBAAM,KAAK,GAAG,QAAQ,UAAU,IAAI,QAAQ,WAAW,EAAE;AAAG,mBAAO,EAAE,MAAM,EAAE,IAAI,KAAK,EAAE;AAAA,UAAG;AAAA,QAClH,CAAC;AACD,cAAM,QAAQ,OAAO,QAAQ,SAAS,CAAC,CAAC;AACxC,cAAM,QAAQ,KAAK,QAAQ,SAAS;AACpC,YAAI,MAAM,KAAK,GAAG,MAAM,0BAA2B,OAAM,IAAI,MAAM,0EAA0E;AAC7I,eAAO;AAAA,MACR;AAAA,IACD;AAAA,IACA;AAAA,MACC,IAAI;AAAA,MACJ,MAAM,IAAI,SAAS;AAClB,YAAI,SAAS;AACb,cAAM,UAAU,QAAQ,OAAO,MAAM;AAAA,UACpC,MAAM,SAAS;AAAE,qBAAS;AAAM,mBAAO,EAAE,QAAQ,WAAW;AAAA,UAAG;AAAA,UAC/D,MAAM,OAAO;AAAE,qBAAS;AAAM,mBAAO,EAAE,MAAM,KAAK;AAAA,UAAG;AAAA,QACtD,CAAC;AACD,YAAI,WAAW;AACf,YAAI;AAAE,gBAAM,QAAQ,OAAO,QAAQ,cAAc,CAAC,CAAC;AAAA,QAAG,QAAQ;AAAE,qBAAW;AAAA,QAAM;AACjF,YAAI,CAAC,YAAY,OAAQ,OAAM,IAAI,MAAM,iEAAiE;AAC1G,eAAO,CAAC,mCAAmC;AAAA,MAC5C;AAAA,IACD;AAAA,EACD;AACD;AAEA,eAAsB,0BACrB,SACA,SAA2C,uBAAuB,GACkF;AACpJ,QAAM,UAAuH,CAAC;AAC9H,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;AAEA,SAAS,eAAe,MAA2D;AAClF,QAAM,YAAY,oBAAI,IAAgC;AACtD,QAAM,QAAQ,CAAC,aAAuC;AACrD,QAAI,UAAU,IAAI,SAAS,EAAE,EAAG,OAAM,IAAI,MAAM,iCAAiC,SAAS,EAAE,IAAI;AAChG,cAAU,IAAI,SAAS,IAAI,QAAQ;AACnC,eAAW,SAAS,SAAS,YAAY,CAAC,EAAG,OAAM,KAAK;AAAA,EACzD;AACA,QAAM,IAAI;AACV,SAAO;AACR;","names":[]}
|
|
@@ -0,0 +1,19 @@
|
|
|
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 __copyProps = (to, from, except, desc) => {
|
|
7
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
8
|
+
for (let key of __getOwnPropNames(from))
|
|
9
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
10
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
11
|
+
}
|
|
12
|
+
return to;
|
|
13
|
+
};
|
|
14
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
15
|
+
|
|
16
|
+
// contracts.ts
|
|
17
|
+
var contracts_exports = {};
|
|
18
|
+
module.exports = __toCommonJS(contracts_exports);
|
|
19
|
+
//# sourceMappingURL=contracts.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../contracts.ts"],"sourcesContent":["export interface ExperienceScope {\n\ttenantId: string;\n\tspaceId: string;\n\tactorId: string;\n\tactorType: string;\n}\n\nexport interface RenderPlanFragment {\n\tid: string;\n\tcomponent?: string;\n\tprops?: Record<string, unknown>;\n\tslot?: string;\n\tevents?: readonly string[];\n\tofflineEvents?: readonly string[];\n\tbindings?: readonly string[];\n\tchildren?: readonly RenderPlanFragment[];\n}\n\nexport interface ScopedRenderPlan {\n\tformatVersion: 1;\n\tplanId: string;\n\tapplication: string;\n\tchannel: string;\n\treleaseDigest: string;\n\tassemblyDigest: string;\n\ttreeId: string;\n\troot: RenderPlanFragment;\n\ttokenSet: { name: string; version: string };\n\texpiresAt: string;\n}\n\nexport interface ExperienceIntentRequest {\n\tplanId: string;\n\tfragmentId: string;\n\teventName: string;\n\tparameters: Record<string, unknown>;\n\tidempotencyKey?: string;\n}\n\n/**\n * A replay of an action captured while disconnected.\n *\n * The marker and the capture moment travel with the request rather than by\n * adapter convention. An adapter that forwarded a replay as an ordinary intent\n * would skip the Host's offline eligibility and reconciliation rules, and\n * capture-authority evidence stamped at receipt cannot truthfully attest when\n * the operator actually acted.\n */\nexport interface ExperienceReplayRequest extends ExperienceIntentRequest {\n\texecutionReason: \"offline_replay\";\n\t/** RFC 3339 instant the action was captured on the device. */\n\tcapturedAt: string;\n\tidempotencyKey: string;\n}\n\nexport interface ExperienceReadRequest {\n\tplanId: string;\n\tfragmentId: string;\n\tbindingName: string;\n\tparameters?: Record<string, unknown>;\n}\n\nexport interface ExperienceGatewayClient {\n\tinvoke(request: ExperienceIntentRequest): Promise<unknown>;\n\treplay?(request: ExperienceReplayRequest): Promise<ExperienceInvocationOutcome>;\n\tread(request: ExperienceReadRequest): Promise<{ data: unknown; stale?: boolean }>;\n}\n\n/**\n * What a governed submission comes back as.\n *\n * The Host reports a refusal by *returning* `reconciliation_required` with the\n * reason, not by throwing: a lapsed authority is a business outcome rather than\n * an error. Anything reading only the exception path will count a refusal as a\n * success.\n */\nexport interface ExperienceInvocationOutcome {\n\tstatus?: string;\n\treconciliation?: { kind?: string; message?: string };\n\t/** Present when an adapter's own outcome could not be determined. */\n\tadapterReconciliation?: unknown;\n\t[key: string]: unknown;\n}\n\nexport interface ExperienceSession {\n\treadonly plan: ScopedRenderPlan;\n\tinvoke(fragmentId: string, eventName: string, parameters: Record<string, unknown>, idempotencyKey?: string): Promise<unknown>;\n\tread(fragmentId: string, bindingName: string, parameters?: Record<string, unknown>): Promise<{ data: unknown; stale?: boolean }>;\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAAA;AAAA;","names":[]}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
interface ExperienceScope {
|
|
2
|
+
tenantId: string;
|
|
3
|
+
spaceId: string;
|
|
4
|
+
actorId: string;
|
|
5
|
+
actorType: string;
|
|
6
|
+
}
|
|
7
|
+
interface RenderPlanFragment {
|
|
8
|
+
id: string;
|
|
9
|
+
component?: string;
|
|
10
|
+
props?: Record<string, unknown>;
|
|
11
|
+
slot?: string;
|
|
12
|
+
events?: readonly string[];
|
|
13
|
+
offlineEvents?: readonly string[];
|
|
14
|
+
bindings?: readonly string[];
|
|
15
|
+
children?: readonly RenderPlanFragment[];
|
|
16
|
+
}
|
|
17
|
+
interface ScopedRenderPlan {
|
|
18
|
+
formatVersion: 1;
|
|
19
|
+
planId: string;
|
|
20
|
+
application: string;
|
|
21
|
+
channel: string;
|
|
22
|
+
releaseDigest: string;
|
|
23
|
+
assemblyDigest: string;
|
|
24
|
+
treeId: string;
|
|
25
|
+
root: RenderPlanFragment;
|
|
26
|
+
tokenSet: {
|
|
27
|
+
name: string;
|
|
28
|
+
version: string;
|
|
29
|
+
};
|
|
30
|
+
expiresAt: string;
|
|
31
|
+
}
|
|
32
|
+
interface ExperienceIntentRequest {
|
|
33
|
+
planId: string;
|
|
34
|
+
fragmentId: string;
|
|
35
|
+
eventName: string;
|
|
36
|
+
parameters: Record<string, unknown>;
|
|
37
|
+
idempotencyKey?: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* A replay of an action captured while disconnected.
|
|
41
|
+
*
|
|
42
|
+
* The marker and the capture moment travel with the request rather than by
|
|
43
|
+
* adapter convention. An adapter that forwarded a replay as an ordinary intent
|
|
44
|
+
* would skip the Host's offline eligibility and reconciliation rules, and
|
|
45
|
+
* capture-authority evidence stamped at receipt cannot truthfully attest when
|
|
46
|
+
* the operator actually acted.
|
|
47
|
+
*/
|
|
48
|
+
interface ExperienceReplayRequest extends ExperienceIntentRequest {
|
|
49
|
+
executionReason: "offline_replay";
|
|
50
|
+
/** RFC 3339 instant the action was captured on the device. */
|
|
51
|
+
capturedAt: string;
|
|
52
|
+
idempotencyKey: string;
|
|
53
|
+
}
|
|
54
|
+
interface ExperienceReadRequest {
|
|
55
|
+
planId: string;
|
|
56
|
+
fragmentId: string;
|
|
57
|
+
bindingName: string;
|
|
58
|
+
parameters?: Record<string, unknown>;
|
|
59
|
+
}
|
|
60
|
+
interface ExperienceGatewayClient {
|
|
61
|
+
invoke(request: ExperienceIntentRequest): Promise<unknown>;
|
|
62
|
+
replay?(request: ExperienceReplayRequest): Promise<ExperienceInvocationOutcome>;
|
|
63
|
+
read(request: ExperienceReadRequest): Promise<{
|
|
64
|
+
data: unknown;
|
|
65
|
+
stale?: boolean;
|
|
66
|
+
}>;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* What a governed submission comes back as.
|
|
70
|
+
*
|
|
71
|
+
* The Host reports a refusal by *returning* `reconciliation_required` with the
|
|
72
|
+
* reason, not by throwing: a lapsed authority is a business outcome rather than
|
|
73
|
+
* an error. Anything reading only the exception path will count a refusal as a
|
|
74
|
+
* success.
|
|
75
|
+
*/
|
|
76
|
+
interface ExperienceInvocationOutcome {
|
|
77
|
+
status?: string;
|
|
78
|
+
reconciliation?: {
|
|
79
|
+
kind?: string;
|
|
80
|
+
message?: string;
|
|
81
|
+
};
|
|
82
|
+
/** Present when an adapter's own outcome could not be determined. */
|
|
83
|
+
adapterReconciliation?: unknown;
|
|
84
|
+
[key: string]: unknown;
|
|
85
|
+
}
|
|
86
|
+
interface ExperienceSession {
|
|
87
|
+
readonly plan: ScopedRenderPlan;
|
|
88
|
+
invoke(fragmentId: string, eventName: string, parameters: Record<string, unknown>, idempotencyKey?: string): Promise<unknown>;
|
|
89
|
+
read(fragmentId: string, bindingName: string, parameters?: Record<string, unknown>): Promise<{
|
|
90
|
+
data: unknown;
|
|
91
|
+
stale?: boolean;
|
|
92
|
+
}>;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export type { ExperienceGatewayClient, ExperienceIntentRequest, ExperienceInvocationOutcome, ExperienceReadRequest, ExperienceReplayRequest, ExperienceScope, ExperienceSession, RenderPlanFragment, ScopedRenderPlan };
|