@fabricorg/sdui-release 0.3.0 → 0.5.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,399 @@
1
+ import { PortableJsonSchema, ActionExecutionContract } from '@fabricorg/platform';
2
+ import { UsageContractDocument } from '@fabricorg/gen-capability';
3
+ import { AssemblyLockfile } from '@fabricorg/assembly';
4
+
5
+ /**
6
+ * Version of the SDUI document contract. A vertical negotiates on this, not on
7
+ * this package's version, and it moves only when the serialized shape changes.
8
+ */
9
+ declare const SDUI_DOCUMENT_FORMAT_VERSION: 1;
10
+ /**
11
+ * Version of the SDUI token-set contract.
12
+ */
13
+ declare const SDUI_TOKEN_SET_FORMAT_VERSION: 1;
14
+ /**
15
+ * Version of the SDUI component-pack contract.
16
+ */
17
+ declare const SDUI_COMPONENT_PACK_FORMAT_VERSION: 1;
18
+ /**
19
+ * Version of the promoted SDUI release contract.
20
+ */
21
+ declare const SDUI_RELEASE_FORMAT_VERSION: 2;
22
+ /**
23
+ * A capability read reference embedded in fragment props. The `capability://`
24
+ * scheme ties a data binding to a published view; the host resolves it through
25
+ * `ProjectionHost`, never through a direct query.
26
+ */
27
+ interface SduiCapabilityRef {
28
+ $ref: `capability://${string}`;
29
+ }
30
+ /**
31
+ * A value that may appear in fragment props: a JSON literal, an array, an
32
+ * object, or a `capability://` read reference. Direct mutation endpoints,
33
+ * API URLs, or code strings are not valid fragment values.
34
+ */
35
+ type SduiFragmentValue = string | number | boolean | null | SduiFragmentValue[] | SduiCapabilityRef | {
36
+ [key: string]: SduiFragmentValue;
37
+ };
38
+ /**
39
+ * An action declared on a fragment. The `intent` must be a `capability://`
40
+ * reference to a published action intent; the host dispatches it through
41
+ * `PlatformHost`, preserving governance. A bare endpoint URL, function name,
42
+ * or inline script is a mutation bypass and is rejected.
43
+ */
44
+ interface SduiActionRef {
45
+ intent: `capability://${string}`;
46
+ params?: Record<string, SduiFragmentValue>;
47
+ }
48
+ /**
49
+ * A node in an SDUI document tree. A fragment either renders a component from
50
+ * a declared pack, binds data from a capability view, or both. Actions on a
51
+ * fragment are always capability action-intent references — never direct
52
+ * mutations.
53
+ */
54
+ interface SduiFragment {
55
+ /** Unique identifier within the document. */
56
+ id: string;
57
+ /** Namespaced component (`pack.Component`) or a core component name. */
58
+ component?: string;
59
+ /** Props for the component; values may be literals or capability refs. */
60
+ props?: Record<string, SduiFragmentValue>;
61
+ /** Named slot this fragment fills in its parent component. */
62
+ slot?: string;
63
+ /** A capability view reference for data-driven fragments. */
64
+ dataRef?: `capability://${string}`;
65
+ /** Action handlers; every entry must be a capability action-intent reference. */
66
+ actions?: Record<string, SduiActionRef>;
67
+ /** Child fragments. */
68
+ children?: SduiFragment[];
69
+ }
70
+ type SduiTokenType = "color" | "spacing" | "fontSize" | "fontWeight" | "radius" | "border" | "shadow";
71
+ interface SduiToken {
72
+ type: SduiTokenType;
73
+ value: string;
74
+ description?: string;
75
+ }
76
+ /**
77
+ * A versioned set of design tokens. Tokens are semantic, not literal CSS — a
78
+ * renderer maps them to its own platform. Two channels consuming the same
79
+ * token set apply the same visual intent through different renderings.
80
+ */
81
+ interface SduiTokenSet {
82
+ formatVersion: typeof SDUI_TOKEN_SET_FORMAT_VERSION;
83
+ name: string;
84
+ version: string;
85
+ tokens: Record<string, SduiToken>;
86
+ }
87
+ interface SduiComponentDefinition {
88
+ /** Roles this component implements (for adoption-binding validation). */
89
+ implements?: string[];
90
+ /** JSON Schema for component props. */
91
+ propsSchema?: PortableJsonSchema;
92
+ /** Named slots this component accepts. */
93
+ slots?: string[];
94
+ }
95
+ /**
96
+ * A versioned pack of SDUI components. Compatible with the adoption-bindings
97
+ * `ComponentPackManifest` but adds prop schemas and slot declarations so a
98
+ * document can be validated without a renderer.
99
+ */
100
+ interface SduiComponentPack {
101
+ formatVersion: typeof SDUI_COMPONENT_PACK_FORMAT_VERSION;
102
+ pack: string;
103
+ namespace: string;
104
+ version: string;
105
+ /** SHA-256 digest of the published component-pack artifact. */
106
+ artifactDigest: string;
107
+ /**
108
+ * Where this pack is loaded from when it is delivered as a federated
109
+ * remote. The artifact digest identifies the pack that was reviewed; this
110
+ * is what a shell actually executes, so validation requires the two to be
111
+ * locked together or the review covers something other than what runs.
112
+ */
113
+ remote?: SduiComponentPackRemote;
114
+ components: Record<string, SduiComponentDefinition>;
115
+ }
116
+ /** Delivery binding for a federated component pack. */
117
+ interface SduiComponentPackRemote {
118
+ /** Remote entry URL the shell loads. */
119
+ entry: string;
120
+ /** Subresource-integrity value for that entry, e.g. `sha384-…`. */
121
+ integrity: string;
122
+ /** Exposed module path keyed by the component name it provides. */
123
+ exposes: Record<string, string>;
124
+ }
125
+ interface SduiComponentPackReference {
126
+ pack: string;
127
+ namespace: string;
128
+ /** Semver range, e.g. `^1.0.0`. */
129
+ range: string;
130
+ }
131
+ interface SduiTokenSetReference {
132
+ name: string;
133
+ version: string;
134
+ }
135
+ /**
136
+ * A versioned SDUI document: a tree of fragments, a token-set reference, and
137
+ * the component packs it depends on. This is the build-time input; the release
138
+ * is the validated, resolved output.
139
+ */
140
+ interface SduiDocument {
141
+ formatVersion: typeof SDUI_DOCUMENT_FORMAT_VERSION;
142
+ name: string;
143
+ version: string;
144
+ /** The tree rendered when no variant is selected. */
145
+ root: SduiFragment;
146
+ /**
147
+ * Alternative trees a compositor may select at request time, for experiment
148
+ * arms and personalization. Every variant is enumerated here and validated
149
+ * against the same grants as `root`, and all of them are covered by the
150
+ * release digest.
151
+ *
152
+ * Enumeration is what makes selection safe. A selector that can produce a
153
+ * tree outside this set cannot be validated at promotion, so "selection may
154
+ * never widen grants" would be unenforceable rather than merely unenforced.
155
+ */
156
+ variants?: SduiVariant[];
157
+ /** The token set this document is authored against. */
158
+ tokenSet: SduiTokenSetReference;
159
+ /**
160
+ * Other token sets this same document may be promoted against, so one
161
+ * authored document serves several brands without forking.
162
+ *
163
+ * Each promotion still pins exactly one resolved token set, and the release
164
+ * digest still identifies precisely what a viewer receives. Carrying several
165
+ * token sets inside a single release would author once at the cost of that
166
+ * property, which is much harder to get back than it is to keep.
167
+ */
168
+ supportedTokenSets?: SduiTokenSetReference[];
169
+ componentPacks: SduiComponentPackReference[];
170
+ }
171
+ /** One selectable alternative to a document's default tree. */
172
+ interface SduiVariant {
173
+ /** Stable key the compositor matches on. Unique within the document. */
174
+ id: string;
175
+ /** The audience or experiment arm this arm serves. */
176
+ description?: string;
177
+ root: SduiFragment;
178
+ }
179
+ interface ResolvedSduiComponentPack {
180
+ pack: string;
181
+ namespace: string;
182
+ version: string;
183
+ range: string;
184
+ artifactDigest: string;
185
+ /**
186
+ * The locked delivery binding, carried onto the release so the digest
187
+ * identifies which remote a channel is expected to load. Omitting it would
188
+ * let two releases differing only in their remote share a digest.
189
+ */
190
+ remote?: SduiComponentPackRemote;
191
+ }
192
+ /**
193
+ * A promoted, validated, immutable SDUI release. It bundles a document, a
194
+ * resolved token set, and locked component packs. Two channels (web, mobile,
195
+ * CLI) can consume the same release and render it through their own renderers
196
+ * while reads and actions remain capability references.
197
+ */
198
+ interface SduiRelease {
199
+ formatVersion: typeof SDUI_RELEASE_FORMAT_VERSION;
200
+ /** SHA-256 of the canonical document, token set, resolved packs, grants, and assembly identity. */
201
+ releaseDigest: string;
202
+ /** Approved application assembly this release was validated against. */
203
+ assemblyDigest: string;
204
+ document: SduiDocument;
205
+ tokenSet: SduiTokenSet;
206
+ componentPacks: ResolvedSduiComponentPack[];
207
+ grants: SduiAuthorizationGrants;
208
+ }
209
+ type SduiFindingCode = "unknown_component" | "incompatible_pack" | "unauthorized_data_ref" | "denied_view_ref" | "mutation_bypass" | "denied_intent_ref" | "intent_action_not_found" | "invalid_fragment" | "invalid_action_ref" | "unknown_token" | "duplicate_fragment_id" | "invalid_token_set" | "invalid_component_pack" | "invalid_document" | "missing_token_set" | "missing_component_pack" | "component_pack_not_in_assembly" | "capability_contract_not_in_assembly" | "duplicate_variant_id" | "fragment_grant_exceeds_release" | "remote_not_locked" | "ambiguous_capability_reference";
210
+ interface SduiFinding {
211
+ code: SduiFindingCode;
212
+ path: string;
213
+ message: string;
214
+ }
215
+ interface SduiValidationResult {
216
+ valid: boolean;
217
+ findings: SduiFinding[];
218
+ release: SduiRelease | undefined;
219
+ }
220
+ /**
221
+ * Explicit capability references authorized for an SDUI release.
222
+ *
223
+ * The presence of a view or action in a supplied usage contract proves that
224
+ * the capability publishes it, not that this document may use it. The
225
+ * experience host must therefore provide both grant lists explicitly.
226
+ */
227
+ interface SduiAuthorizationGrants {
228
+ /** Full `capability://namespace/view` references allowed in data bindings. */
229
+ views: readonly string[];
230
+ /** Full `capability://namespace/intent` references allowed in actions. */
231
+ intents: readonly string[];
232
+ /**
233
+ * Per-fragment narrowing, keyed by fragment id.
234
+ *
235
+ * The release-wide lists above are the ceiling. A listed fragment may use
236
+ * only the references named for it, and the narrowing is inherited by that
237
+ * fragment's children, so a page assembled from several teams' fragments
238
+ * gets least privilege rather than the union of everything any of them
239
+ * needs. A fragment entry can only ever narrow: naming a reference outside
240
+ * the release-wide list is an authoring error and is reported rather than
241
+ * silently granted.
242
+ *
243
+ * Keys are fragment ids, which are unique within a tree but may repeat
244
+ * across variants, so two variants using the same fragment id share one
245
+ * entry. Give them distinct ids when they need distinct narrowing. This
246
+ * cannot widen either fragment, since narrowing stays an intersection.
247
+ */
248
+ fragments?: Record<string, SduiFragmentGrants>;
249
+ }
250
+ /** References one fragment subtree may use, drawn from the release-wide lists. */
251
+ interface SduiFragmentGrants {
252
+ views?: readonly string[];
253
+ intents?: readonly string[];
254
+ }
255
+ interface SduiValidationInput {
256
+ document: SduiDocument;
257
+ tokenSet: SduiTokenSet;
258
+ /** Available component packs for version resolution. */
259
+ componentPacks: SduiComponentPack[];
260
+ /** Capability contracts available for data-ref and action validation. */
261
+ contracts: UsageContractDocument[];
262
+ /**
263
+ * Explicit authorization grants for every capability view and action
264
+ * reference used by the document. Contract membership alone never grants
265
+ * access.
266
+ */
267
+ grants: SduiAuthorizationGrants;
268
+ /** Approved resolved application composition. */
269
+ assembly: AssemblyLockfile;
270
+ /** Core component vocabulary every renderer implements. */
271
+ coreComponents?: readonly string[];
272
+ }
273
+
274
+ declare const SDUI_RELEASE_V3_FORMAT_VERSION: 3;
275
+ declare const RELEASE_CHANNEL_POINTER_FORMAT_VERSION: 1;
276
+ interface ExperienceViewRoute {
277
+ reference: `capability://${string}`;
278
+ name: `${string}/${string}`;
279
+ version: string;
280
+ parameterSchema?: PortableJsonSchema;
281
+ parameterSchemaDigest?: string;
282
+ }
283
+ interface ExperienceIntentRoute {
284
+ reference: `capability://${string}`;
285
+ actionId: `${string}.${string}`;
286
+ version: number;
287
+ parameterSchema?: PortableJsonSchema;
288
+ parameterSchemaDigest?: string;
289
+ execution?: ActionExecutionContract;
290
+ }
291
+ interface EffectiveFragmentGrant {
292
+ treeId: "root" | string;
293
+ fragmentId: string;
294
+ views: readonly string[];
295
+ intents: readonly string[];
296
+ }
297
+ interface SduiReleaseV3 {
298
+ formatVersion: typeof SDUI_RELEASE_V3_FORMAT_VERSION;
299
+ kind: "experience-release";
300
+ issuer: string;
301
+ application: string;
302
+ releaseDigest: string;
303
+ assemblyDigest: string;
304
+ document: SduiDocument;
305
+ tokenSet: SduiTokenSet;
306
+ componentPacks: ResolvedSduiComponentPack[];
307
+ routes: {
308
+ views: ExperienceViewRoute[];
309
+ intents: ExperienceIntentRoute[];
310
+ };
311
+ effectiveGrants: EffectiveFragmentGrant[];
312
+ }
313
+ interface ReleaseChannelPointer {
314
+ formatVersion: typeof RELEASE_CHANNEL_POINTER_FORMAT_VERSION;
315
+ kind: "release-channel-pointer";
316
+ issuer: string;
317
+ application: string;
318
+ channel: string;
319
+ generation: number;
320
+ activeReleaseDigest: string;
321
+ assemblyDigest: string;
322
+ issuedAt: string;
323
+ notBefore?: string;
324
+ expiresAt?: string;
325
+ fallbackReleaseDigests?: readonly string[];
326
+ }
327
+ interface ReleaseVerificationKey {
328
+ keyId: string;
329
+ algorithm: "ES256";
330
+ key: unknown;
331
+ }
332
+ interface ReleaseTrustStore {
333
+ resolveKey(input: {
334
+ keyId: string;
335
+ issuer: string;
336
+ algorithm: "ES256";
337
+ }): ReleaseVerificationKey | undefined | Promise<ReleaseVerificationKey | undefined>;
338
+ }
339
+ interface ReleaseCryptoPort {
340
+ verify(input: {
341
+ algorithm: "ES256";
342
+ key: unknown;
343
+ signingInput: Uint8Array;
344
+ signature: Uint8Array;
345
+ }): Promise<boolean>;
346
+ digestSha256(bytes: Uint8Array): Promise<Uint8Array>;
347
+ }
348
+ interface ReleaseGenerationStore {
349
+ get(key: string): Promise<{
350
+ generation: number;
351
+ pointerDigest: string;
352
+ } | undefined>;
353
+ /**
354
+ * Atomically accepts `generation` only when it is greater than the stored
355
+ * value for `key`. Implementations must compare and write in one operation.
356
+ */
357
+ acceptIfHigher(key: string, generation: number, pointerDigest: string): Promise<boolean>;
358
+ }
359
+ interface VerifySignedExperienceArtifactInput {
360
+ compactJws: string;
361
+ expectedType: "fabric-experience-release+jws" | "fabric-release-channel-pointer+jws";
362
+ crypto: ReleaseCryptoPort;
363
+ trust: ReleaseTrustStore;
364
+ maxBytes?: number;
365
+ }
366
+ /**
367
+ * RFC 8785 JSON Canonicalization Scheme for values already restricted to the
368
+ * JSON data model. Invalid Unicode, non-finite numbers and non-JSON values are
369
+ * rejected rather than normalized differently by another runtime.
370
+ */
371
+ declare function canonicalizeJcs(value: unknown): string;
372
+ declare function verifySignedExperienceArtifact(input: VerifySignedExperienceArtifactInput): Promise<Record<string, unknown>>;
373
+ declare function verifySignedExperienceRelease(input: {
374
+ compactJws: string;
375
+ crypto: ReleaseCryptoPort;
376
+ trust: ReleaseTrustStore;
377
+ expected: {
378
+ issuer: string;
379
+ application: string;
380
+ assemblyDigest: string;
381
+ };
382
+ maxBytes?: number;
383
+ }): Promise<SduiReleaseV3>;
384
+ declare function assertSduiReleaseV3(value: unknown): asserts value is SduiReleaseV3;
385
+ declare function activateReleaseChannelPointer(input: {
386
+ compactJws: string;
387
+ crypto: ReleaseCryptoPort;
388
+ trust: ReleaseTrustStore;
389
+ generations: ReleaseGenerationStore;
390
+ expected: {
391
+ issuer: string;
392
+ application: string;
393
+ channel: string;
394
+ };
395
+ now?: Date;
396
+ }): Promise<ReleaseChannelPointer>;
397
+ declare function assertReleaseChannelPointer(value: unknown): asserts value is ReleaseChannelPointer;
398
+
399
+ export { type SduiFragmentValue as A, type SduiReleaseV3 as B, type SduiToken as C, type SduiTokenSetReference as D, type EffectiveFragmentGrant as E, type SduiTokenType as F, activateReleaseChannelPointer as G, assertReleaseChannelPointer as H, assertSduiReleaseV3 as I, canonicalizeJcs as J, verifySignedExperienceArtifact as K, verifySignedExperienceRelease as L, type ResolvedSduiComponentPack as R, type SduiAuthorizationGrants as S, type VerifySignedExperienceArtifactInput as V, type SduiComponentPack as a, type SduiDocument as b, type SduiFragment as c, type SduiRelease as d, type SduiTokenSet as e, type SduiValidationInput as f, type SduiValidationResult as g, type ExperienceIntentRoute as h, type ExperienceViewRoute as i, RELEASE_CHANNEL_POINTER_FORMAT_VERSION as j, type ReleaseChannelPointer as k, type ReleaseCryptoPort as l, type ReleaseGenerationStore as m, type ReleaseTrustStore as n, type ReleaseVerificationKey as o, SDUI_COMPONENT_PACK_FORMAT_VERSION as p, SDUI_DOCUMENT_FORMAT_VERSION as q, SDUI_RELEASE_FORMAT_VERSION as r, SDUI_RELEASE_V3_FORMAT_VERSION as s, SDUI_TOKEN_SET_FORMAT_VERSION as t, type SduiActionRef as u, type SduiCapabilityRef as v, type SduiComponentDefinition as w, type SduiComponentPackReference as x, type SduiFinding as y, type SduiFindingCode as z };
@@ -0,0 +1,286 @@
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
+ // contracts.ts
21
+ var contracts_exports = {};
22
+ __export(contracts_exports, {
23
+ RELEASE_CHANNEL_POINTER_FORMAT_VERSION: () => RELEASE_CHANNEL_POINTER_FORMAT_VERSION,
24
+ SDUI_RELEASE_V3_FORMAT_VERSION: () => SDUI_RELEASE_V3_FORMAT_VERSION,
25
+ activateReleaseChannelPointer: () => activateReleaseChannelPointer,
26
+ assertReleaseChannelPointer: () => assertReleaseChannelPointer,
27
+ assertSduiReleaseV3: () => assertSduiReleaseV3,
28
+ canonicalizeJcs: () => canonicalizeJcs,
29
+ verifySignedExperienceArtifact: () => verifySignedExperienceArtifact,
30
+ verifySignedExperienceRelease: () => verifySignedExperienceRelease
31
+ });
32
+ module.exports = __toCommonJS(contracts_exports);
33
+ var SDUI_RELEASE_V3_FORMAT_VERSION = 3;
34
+ var RELEASE_CHANNEL_POINTER_FORMAT_VERSION = 1;
35
+ var encoder = new TextEncoder();
36
+ var decoder = new TextDecoder("utf-8", { fatal: true });
37
+ var HEX_DIGEST = /^[a-f0-9]{64}$/;
38
+ function canonicalizeJcs(value) {
39
+ return canonical(value, 0);
40
+ }
41
+ function canonical(value, depth) {
42
+ if (depth > 64) throw new Error("Canonical JSON exceeds the maximum depth of 64.");
43
+ if (value === null) return "null";
44
+ if (typeof value === "boolean") return value ? "true" : "false";
45
+ if (typeof value === "number") {
46
+ if (!Number.isFinite(value)) throw new Error("Canonical JSON numbers must be finite.");
47
+ return Object.is(value, -0) ? "0" : JSON.stringify(value);
48
+ }
49
+ if (typeof value === "string") {
50
+ assertValidUnicode(value);
51
+ return JSON.stringify(value);
52
+ }
53
+ if (Array.isArray(value)) return `[${value.map((entry) => canonical(entry, depth + 1)).join(",")}]`;
54
+ if (typeof value !== "object") throw new Error(`Canonical JSON cannot encode ${typeof value}.`);
55
+ const record = value;
56
+ const keys = Object.keys(record).sort();
57
+ return `{${keys.map((key) => {
58
+ assertValidUnicode(key);
59
+ return `${JSON.stringify(key)}:${canonical(record[key], depth + 1)}`;
60
+ }).join(",")}}`;
61
+ }
62
+ function assertValidUnicode(value) {
63
+ for (let index = 0; index < value.length; index += 1) {
64
+ const code = value.charCodeAt(index);
65
+ if (code >= 55296 && code <= 56319) {
66
+ const next = value.charCodeAt(index + 1);
67
+ if (!(next >= 56320 && next <= 57343)) throw new Error("Canonical JSON contains an unpaired Unicode surrogate.");
68
+ index += 1;
69
+ } else if (code >= 56320 && code <= 57343) {
70
+ throw new Error("Canonical JSON contains an unpaired Unicode surrogate.");
71
+ }
72
+ }
73
+ }
74
+ async function verifySignedExperienceArtifact(input) {
75
+ const maxBytes = input.maxBytes ?? 1048576;
76
+ if (encoder.encode(input.compactJws).byteLength > maxBytes) {
77
+ throw new Error(`Signed experience artifact exceeds the ${maxBytes}-byte size limit.`);
78
+ }
79
+ const parts = input.compactJws.split(".");
80
+ if (parts.length !== 3 || parts.some((part) => part.length === 0)) throw new Error("Signed experience artifact must be a compact JWS.");
81
+ const header = parseSegment(parts[0], "protected header");
82
+ if (header.alg !== "ES256") throw new Error("Signed experience artifacts must use ES256.");
83
+ if (header.typ !== input.expectedType) throw new Error(`Signed experience artifact type must be ${input.expectedType}.`);
84
+ if (typeof header.kid !== "string" || !header.kid) throw new Error("Signed experience artifact requires a key id.");
85
+ const payloadBytes = decodeBase64Url(parts[1]);
86
+ const payload = parseJsonBytes(payloadBytes, "payload");
87
+ if (typeof payload.issuer !== "string" || !payload.issuer) throw new Error("Signed experience artifact requires an issuer.");
88
+ const key = await input.trust.resolveKey({ keyId: header.kid, issuer: payload.issuer, algorithm: "ES256" });
89
+ if (!key || key.algorithm !== "ES256") throw new Error("Signed experience artifact has no trusted key.");
90
+ const signature = decodeBase64Url(parts[2]);
91
+ if (signature.byteLength !== 64) throw new Error("ES256 signatures must be 64-byte JOSE signatures.");
92
+ const verified = await input.crypto.verify({
93
+ algorithm: "ES256",
94
+ key: key.key,
95
+ signingInput: encoder.encode(`${parts[0]}.${parts[1]}`),
96
+ signature
97
+ });
98
+ if (!verified) throw new Error("Signed experience artifact signature is invalid.");
99
+ return payload;
100
+ }
101
+ async function verifySignedExperienceRelease(input) {
102
+ const payload = await verifySignedExperienceArtifact({
103
+ compactJws: input.compactJws,
104
+ expectedType: "fabric-experience-release+jws",
105
+ crypto: input.crypto,
106
+ trust: input.trust,
107
+ ...input.maxBytes === void 0 ? {} : { maxBytes: input.maxBytes }
108
+ });
109
+ assertSduiReleaseV3(payload);
110
+ if (payload.issuer !== input.expected.issuer || payload.application !== input.expected.application || payload.assemblyDigest !== input.expected.assemblyDigest) {
111
+ throw new Error("Signed experience release does not match the expected issuer, application, and assembly.");
112
+ }
113
+ const { releaseDigest, ...body } = payload;
114
+ const digest = await input.crypto.digestSha256(encoder.encode(canonicalizeJcs(body)));
115
+ const expectedDigest = [...digest].map((byte) => byte.toString(16).padStart(2, "0")).join("");
116
+ if (releaseDigest !== expectedDigest) throw new Error("Signed experience release digest does not match its canonical content.");
117
+ return payload;
118
+ }
119
+ function assertSduiReleaseV3(value) {
120
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("SDUI Release v3 must be an object.");
121
+ const release = value;
122
+ if (release.formatVersion !== SDUI_RELEASE_V3_FORMAT_VERSION || release.kind !== "experience-release") {
123
+ throw new Error("SDUI Release v3 has an unsupported format.");
124
+ }
125
+ for (const field of ["issuer", "application", "releaseDigest", "assemblyDigest"]) {
126
+ if (typeof release[field] !== "string" || !release[field]) throw new Error(`SDUI Release v3 ${field} is required.`);
127
+ }
128
+ if (!HEX_DIGEST.test(release.releaseDigest) || !HEX_DIGEST.test(release.assemblyDigest)) {
129
+ throw new Error("SDUI Release v3 digests must be lowercase SHA-256 values.");
130
+ }
131
+ if (!release.document || typeof release.document !== "object" || !release.tokenSet || typeof release.tokenSet !== "object" || !Array.isArray(release.componentPacks) || !release.routes || typeof release.routes !== "object" || !Array.isArray(release.routes.views) || !Array.isArray(release.routes.intents) || !Array.isArray(release.effectiveGrants)) {
132
+ throw new Error("SDUI Release v3 is structurally incomplete.");
133
+ }
134
+ const routes = release.routes;
135
+ assertUniqueRecords(routes.views, "view route", (route) => {
136
+ const record = assertRecord(route, "view route");
137
+ assertCapabilityReference(record.reference, "view route");
138
+ assertNonEmptyString(record.name, "view route name");
139
+ assertNonEmptyString(record.version, "view route version");
140
+ if (record.parameterSchemaDigest !== void 0) {
141
+ assertHexDigest(record.parameterSchemaDigest, "view route parameterSchemaDigest");
142
+ }
143
+ return record.reference;
144
+ });
145
+ assertUniqueRecords(routes.intents, "intent route", (route) => {
146
+ const record = assertRecord(route, "intent route");
147
+ assertCapabilityReference(record.reference, "intent route");
148
+ assertNonEmptyString(record.actionId, "intent route actionId");
149
+ if (!Number.isSafeInteger(record.version) || record.version < 1) {
150
+ throw new Error("SDUI Release v3 intent route version must be a positive integer.");
151
+ }
152
+ if (record.parameterSchemaDigest !== void 0) {
153
+ assertHexDigest(record.parameterSchemaDigest, "intent route parameterSchemaDigest");
154
+ }
155
+ return record.reference;
156
+ });
157
+ assertUniqueRecords(release.effectiveGrants, "effective grant", (grant) => {
158
+ const record = assertRecord(grant, "effective grant");
159
+ assertNonEmptyString(record.treeId, "effective grant treeId");
160
+ assertNonEmptyString(record.fragmentId, "effective grant fragmentId");
161
+ if (!Array.isArray(record.views) || !record.views.every((item) => typeof item === "string") || !Array.isArray(record.intents) || !record.intents.every((item) => typeof item === "string")) {
162
+ throw new Error("SDUI Release v3 effective grant routes must be string arrays.");
163
+ }
164
+ return `${record.treeId}\0${record.fragmentId}`;
165
+ });
166
+ }
167
+ function assertRecord(value, label) {
168
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`SDUI Release v3 ${label} must be an object.`);
169
+ return value;
170
+ }
171
+ function assertNonEmptyString(value, label) {
172
+ if (typeof value !== "string" || !value) throw new Error(`SDUI Release v3 ${label} is required.`);
173
+ }
174
+ function assertHexDigest(value, label) {
175
+ if (typeof value !== "string" || !HEX_DIGEST.test(value)) throw new Error(`SDUI Release v3 ${label} must be a lowercase SHA-256 value.`);
176
+ }
177
+ function assertCapabilityReference(value, label) {
178
+ if (typeof value !== "string" || !value.startsWith("capability://")) throw new Error(`SDUI Release v3 ${label} reference is invalid.`);
179
+ }
180
+ function assertUniqueRecords(values, label, identity) {
181
+ const seen = /* @__PURE__ */ new Set();
182
+ for (const value of values) {
183
+ const key = identity(value);
184
+ if (seen.has(key)) throw new Error(`SDUI Release v3 has a duplicate ${label}.`);
185
+ seen.add(key);
186
+ }
187
+ }
188
+ async function activateReleaseChannelPointer(input) {
189
+ const payload = await verifySignedExperienceArtifact({
190
+ compactJws: input.compactJws,
191
+ expectedType: "fabric-release-channel-pointer+jws",
192
+ crypto: input.crypto,
193
+ trust: input.trust
194
+ });
195
+ assertReleaseChannelPointer(payload);
196
+ if (payload.issuer !== input.expected.issuer || payload.application !== input.expected.application || payload.channel !== input.expected.channel) {
197
+ throw new Error("Release channel pointer does not match the expected issuer, application, and channel.");
198
+ }
199
+ const now = (input.now ?? /* @__PURE__ */ new Date()).getTime();
200
+ if (payload.notBefore !== void 0 && now < parseTimestamp(payload.notBefore, "notBefore")) {
201
+ throw new Error("Release channel pointer is not active yet.");
202
+ }
203
+ if (payload.expiresAt !== void 0 && now >= parseTimestamp(payload.expiresAt, "expiresAt")) {
204
+ throw new Error("Release channel pointer has expired.");
205
+ }
206
+ const generationKey = `${payload.issuer}:${payload.application}:${payload.channel}`;
207
+ const pointerDigestBytes = await input.crypto.digestSha256(encoder.encode(canonicalizeJcs(payload)));
208
+ const pointerDigest = [...pointerDigestBytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
209
+ const current = await input.generations.get(generationKey);
210
+ if (current !== void 0 && payload.generation < current.generation) {
211
+ throw new Error("Release channel pointer is older than the accepted generation.");
212
+ }
213
+ if (current?.generation === payload.generation && current.pointerDigest !== pointerDigest) {
214
+ throw new Error("Release channel generation is already bound to a different signed pointer.");
215
+ }
216
+ if (current?.generation !== payload.generation) {
217
+ const accepted = await input.generations.acceptIfHigher(generationKey, payload.generation, pointerDigest);
218
+ if (!accepted) {
219
+ const raced = await input.generations.get(generationKey);
220
+ if (raced?.generation !== payload.generation || raced.pointerDigest !== pointerDigest) {
221
+ throw new Error("Release channel pointer lost an activation race to a different generation.");
222
+ }
223
+ }
224
+ }
225
+ return payload;
226
+ }
227
+ function assertReleaseChannelPointer(value) {
228
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Release channel pointer must be an object.");
229
+ const pointer = value;
230
+ if (pointer.formatVersion !== RELEASE_CHANNEL_POINTER_FORMAT_VERSION || pointer.kind !== "release-channel-pointer") {
231
+ throw new Error("Release channel pointer has an unsupported format.");
232
+ }
233
+ for (const field of ["issuer", "application", "channel", "issuedAt"]) {
234
+ if (typeof pointer[field] !== "string" || !pointer[field]) throw new Error(`Release channel pointer ${field} is required.`);
235
+ }
236
+ if (!Number.isSafeInteger(pointer.generation) || pointer.generation < 0) throw new Error("Release channel pointer generation must be a non-negative safe integer.");
237
+ if (!HEX_DIGEST.test(String(pointer.activeReleaseDigest)) || !HEX_DIGEST.test(String(pointer.assemblyDigest))) {
238
+ throw new Error("Release channel pointer digests must be lowercase SHA-256 values.");
239
+ }
240
+ if (pointer.fallbackReleaseDigests !== void 0 && (!Array.isArray(pointer.fallbackReleaseDigests) || !pointer.fallbackReleaseDigests.every((digest) => typeof digest === "string" && HEX_DIGEST.test(digest)) || new Set(pointer.fallbackReleaseDigests).size !== pointer.fallbackReleaseDigests.length)) {
241
+ throw new Error("Release channel pointer fallbackReleaseDigests must contain unique lowercase SHA-256 values.");
242
+ }
243
+ parseTimestamp(pointer.issuedAt, "issuedAt");
244
+ }
245
+ function parseSegment(segment, label) {
246
+ return parseJsonBytes(decodeBase64Url(segment), label);
247
+ }
248
+ function parseJsonBytes(bytes, label) {
249
+ let value;
250
+ try {
251
+ value = JSON.parse(decoder.decode(bytes));
252
+ } catch {
253
+ throw new Error(`Signed experience artifact ${label} is not valid UTF-8 JSON.`);
254
+ }
255
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`Signed experience artifact ${label} must be an object.`);
256
+ return value;
257
+ }
258
+ function decodeBase64Url(value) {
259
+ if (!/^[A-Za-z0-9_-]+$/.test(value)) throw new Error("Signed experience artifact contains invalid base64url.");
260
+ const padded = value.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(value.length / 4) * 4, "=");
261
+ let binary;
262
+ try {
263
+ binary = atob(padded);
264
+ } catch {
265
+ throw new Error("Signed experience artifact contains invalid base64url.");
266
+ }
267
+ return Uint8Array.from(binary, (character) => character.charCodeAt(0));
268
+ }
269
+ function parseTimestamp(value, field) {
270
+ if (!/(?:Z|[+-]\d{2}:\d{2})$/.test(value)) throw new Error(`Release channel pointer ${field} must be RFC 3339 with an explicit offset.`);
271
+ const parsed = Date.parse(value);
272
+ if (Number.isNaN(parsed)) throw new Error(`Release channel pointer ${field} is invalid.`);
273
+ return parsed;
274
+ }
275
+ // Annotate the CommonJS export names for ESM import in node:
276
+ 0 && (module.exports = {
277
+ RELEASE_CHANNEL_POINTER_FORMAT_VERSION,
278
+ SDUI_RELEASE_V3_FORMAT_VERSION,
279
+ activateReleaseChannelPointer,
280
+ assertReleaseChannelPointer,
281
+ assertSduiReleaseV3,
282
+ canonicalizeJcs,
283
+ verifySignedExperienceArtifact,
284
+ verifySignedExperienceRelease
285
+ });
286
+ //# sourceMappingURL=contracts.cjs.map