@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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,181 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 8ce5e06: Derive release grants from what an actor holds, and complete the port catalog.
8
+
9
+ `deriveAuthorizationGrants` turns usage contracts plus an actor's permissions, entitlements and
10
+ feature flags into an `SduiAuthorizationGrants`. A shell that answers "what may this actor see" from
11
+ its own configuration, while grants are hand-maintained elsewhere and enforcement happens in a third
12
+ place, produces three sources that must agree with nothing forcing them to. Deriving them makes
13
+ navigation a projection of the same declarations. It is navigation-shaped rather than enforcement:
14
+ the Host still re-checks authority at execution and `ProjectionHost` still authorizes every query.
15
+ Withheld capabilities are returned with the requirement that was missing, so an absent entry is
16
+ explainable.
17
+
18
+ `ContentPort`, `PaymentsPort`, `SearchPort` and `CustomerRecordPort` join flags, design tokens and
19
+ identity, each with the suite an adapter must pass. The suites encode what each port class actually
20
+ gets wrong: content inventing a default for an unauthored key or carrying a capability reference,
21
+ money expressed as a float or an idempotency key that moves it twice, search hits mistaken for
22
+ authorization rather than pointers a governed read still resolves, and redelivery duplicating a
23
+ customer record. `PaymentOutcome` includes `ambiguous`, which maps onto the platform's existing
24
+ adapter outcomes so an uncertain charge reconciles instead of being retried.
25
+
26
+ - a598fb9: Close the remaining review findings.
27
+
28
+ **One capability-reference grammar.** `sdui-release` accepted any name after the namespace while
29
+ `sdui-composer` rejected `/`, so a view named `orders/reports/daily` produced a release that validated
30
+ and then died at promotion with nothing wrong upstream to point at. Both now share
31
+ `parseCapabilityRef`, tightened to lowercase path segments matching what the platform permits for a
32
+ view name. Two views in one contract that reduce to the same reference are reported as
33
+ `ambiguous_capability_reference` at validation rather than surfacing as an ambiguous route later.
34
+
35
+ **A bounded replay window.** A client-supplied `capturedAt` had no lower bound. Deferring it to the
36
+ Host looked safe and was not: the Host checks staleness only when the capture-authority adapter
37
+ supplies an `expiresAt`, and skips the execution-time expiry check entirely for actions governed at
38
+ capture. A gateway that accepts replay must now declare `maxOfflineReplayAgeMs`, and the instant must
39
+ be RFC 3339 with an explicit offset.
40
+
41
+ **One token-set grammar.** `document.tokenSet` accepted names and versions that `supportedTokenSets`
42
+ rejected, while validation treats the two fields as interchangeable.
43
+
44
+ **Port requirements narrow as they are checked.** A requirement for a standard _and_ a version could
45
+ be satisfied by two different adapters covering one constraint each. `requirement.standard.version`
46
+ was declared and never read.
47
+
48
+ - 8f75ac9: Add the channel conformance suite and multi-brand promotion.
49
+
50
+ `sduiChannelChecks` and `runSduiChannelChecks` certify that a channel refuses anything that is not a
51
+ promoted, digest-verified release: tampered content, a bare document that was never promoted, and a
52
+ release carrying no digest. It also requires a channel to report missing core vocabulary rather than
53
+ render anyway, and to leave the release unmutated. The rule previously existed only in the type
54
+ signature of the one implementation Fabric ships, while every client writes their own renderer. See
55
+ ADR 0016.
56
+
57
+ `SduiDocument.supportedTokenSets` lets one authored document be promoted against several brands.
58
+ Each promotion still pins exactly one resolved token set, so a release digest continues to identify
59
+ precisely what a viewer received — the property that carrying several token sets inside one release
60
+ would have given up. The field is optional and a document that omits it digests exactly as before.
61
+
62
+ - 9a5b18a: Close what a fifth review found, including a regression the previous fix introduced.
63
+
64
+ **Ambiguity fired on legal documents.** Counting every declared view made publishing a view at two
65
+ versions a hard failure, and view versioning is a first-class pattern: the platform and the usage
66
+ contract both key a view on `name@version`. It also failed a release over a capability that release
67
+ never touched. Ambiguity is now reported for _granted_ references only, where a versionless reference
68
+ genuinely cannot pick between two versions, and a contract supplied twice gets its own finding
69
+ instead of making every one of its views look ambiguous.
70
+
71
+ **A failed replay is `unknown`, not `refused`.** `failed` is reachable after adapter steps succeeded
72
+ and the domain transaction committed, so telling an operator the action was definitely refused could
73
+ be as wrong as telling them it was applied. Only policy blocks and validation failures are decided
74
+ before any effect. A replay adapter that returns nothing is surfaced rather than counted as applied,
75
+ which was the one line the previous rewrite left untouched.
76
+
77
+ **`parked` and `expired` carry their records**, and reconciliation carries the invocation id. Bare
78
+ counts left nobody able to say which action needs approving or whose work was dropped, and external
79
+ reconciliation needs the id the type exists to enable.
80
+
81
+ Settled-failure classification gained the superseded-action-version case and dropped two it had
82
+ wrong: a future-dated capture comes good as clocks correct, and a gateway missing its replay window
83
+ is a deployment mistake somebody will fix. Discarding either would destroy work.
84
+
85
+ **`assertPortSuiteHasTeeth` takes a set of broken adapters.** Requiring one stub to trip every check
86
+ was unsatisfiable for any suite with a fixture-liveness guard, since the guard bites on the
87
+ complement of what it guards, and two reference ports could not pass their own suites. Port ids now
88
+ permit a hyphen in the first segment, so a port can be defined for every name a manifest can declare,
89
+ and `certifyAdapter` validates its definition rather than issuing evidence the gate reports as
90
+ missing.
91
+
92
+ - 9b2e55b: Close what a fourth review found: each earlier fix closed one instance of a class and left its
93
+ siblings.
94
+
95
+ **Replay disposition is now exhaustive over `ActionStatus`.** The previous fix enumerated the two
96
+ statuses that looked like refusals, so `failed`, `validation_failed` and `waiting_for_approval` fell
97
+ through and were deleted from the queue and reported to the operator as applied — the same bug the
98
+ fix set out to kill, one layer in. Any returned outcome now means the platform holds the invocation,
99
+ so the capture leaves the queue either way; what differs is whether a person needs to hear about it.
100
+ An undeterminable adapter outcome is reported as `unknown` rather than `refused`, because the effect
101
+ may well have happened. `isSettledReplayFailure` learned the refusals the sibling commit introduced,
102
+ including a capture rejected for age, which can never come good.
103
+
104
+ **A replay without `capturedAt` is refused.** It was reachable only through an untyped cast, so
105
+ omitting it skipped the age check entirely and stamped receipt time into durable capture evidence.
106
+
107
+ **A capability can name a namespaced port.** `definePort` required a dot and the manifest validator
108
+ forbade one, so no real capability could name a defined port and the certification gate was
109
+ unreachable. Certification is also checked structurally rather than by the presence of a property, so
110
+ a hand-written claim no longer satisfies the gate that exists to demand evidence, and a port
111
+ requirement narrows its candidates as each constraint is applied.
112
+
113
+ **`assertPortSuiteHasTeeth` names every vacuous check** rather than returning on the first one that
114
+ bites, and a reference port is actually run through it. The payments control asserts its result, so
115
+ an adapter that never charges no longer certifies. The capability-reference grammar matches the
116
+ platform's own view-name rule instead of being stricter than it, and ambiguity is counted by
117
+ occurrence across views and intents rather than by comparing names within one contract.
118
+
119
+ - 0d1f6f6: Close what a third independent review found, including two defects introduced by the previous fix.
120
+
121
+ **Offline replay counted refusals as successes.** `PlatformHost` reports a refused capture by
122
+ _returning_ `reconciliation_required` with the reason, not by throwing, so the queue took its success
123
+ branch, deleted the capture and reported it applied. The reconciliation path was unreachable. Replay
124
+ now reads the returned outcome. The already-applied branch is removed entirely: a repeated key
125
+ returns the original invocation without complaint, and an idempotency _conflict_ means the key was
126
+ reused for a materially different command, so that capture was never applied and is kept rather than
127
+ discarded.
128
+
129
+ **`deriveAuthorizationGrants` defaulted open and reached the signed artifact.** Requirements are
130
+ optional metadata, so a capability declaring none granted everything to an actor holding nothing.
131
+ Grants are also carried onto a release unexamined and promotion signs a route for each, so a surplus
132
+ grant became a signed route to a capability the screen never renders. Undeclared requirements now
133
+ withhold by default, and `document` scopes the result to references the screen actually binds.
134
+
135
+ Several conformance checks could not fail: a search suite passed an adapter returning nothing, a
136
+ payments suite passed a double-charger that omitted a provider reference, an identity suite certified
137
+ timestamps the scope check then rejected on every request, an MFE probe could be refused for an
138
+ unrelated reason, and a channel check accepted any exception as a refusal. Each now proves what it
139
+ claims. Promotion configuration rejects anything reading like a key rather than two spellings,
140
+ rejects unknown fields, and constrains the application name that decides the default output path.
141
+
142
+ ### Patch Changes
143
+
144
+ - 274d948: Close the gaps a second independent review found.
145
+
146
+ Experience promotion now binds the usage contracts it compiles routes from to the assembly the
147
+ release was validated against. Promotion previously accepted arbitrary contracts while copying the
148
+ release's assembly digest through unchanged, so a substituted contract could remap an already-granted
149
+ `capability://` reference onto a different installed action. PlatformHost still authorized that
150
+ action, which made this an integrity failure of what a release exposes rather than an authorization
151
+ bypass, and invisible downstream. `@fabricorg/assembly` exports `computeAssemblyLockfileDigest` so
152
+ tooling can build a self-consistent lockfile; the validator already recomputed it.
153
+
154
+ The MFE loader gains `fabric.mfe-loader.mismatched-digest-denial.v1`, which supplies a well-formed
155
+ integrity value that cannot match the approved bundle. The previous integrity check supplied only a
156
+ malformed value, so it proved a regex ran; a transport that ignored integrity entirely certified
157
+ clean. Since same-realm MFE code carries the application's own authority, a false integrity
158
+ certification is worth catching in the suite rather than in production.
159
+
160
+ The SDUI channel suite no longer certifies a channel it never exercised: the missing-vocabulary check
161
+ now runs against the subject rather than a fresh built-in one, rejects a vacuous or contradictory
162
+ subject instead of passing it, requires a refusal to name the digest so an unrelated crash cannot
163
+ read as correct behaviour, and edits several digest-covered fields so a channel cannot pass by
164
+ special-casing one fixture. `supportedTokenSets` uses a total comparator over its fields rather than
165
+ an interpolated key, and duplicate or malformed token-set references are rejected.
166
+
167
+ - Updated dependencies [274d948]
168
+ - @fabricorg/assembly@0.4.0
169
+
170
+ ## 0.4.0
171
+
172
+ ### Minor Changes
173
+
174
+ - Add browser-safe Release v3 contracts, RFC 8785 canonicalization, async ES256
175
+ verification, exact compiled routes, tree-scoped effective grants, and
176
+ issuer/application/channel-bound monotonic release pointers while preserving
177
+ Release v2.
178
+
3
179
  ## 0.3.0
4
180
 
5
181
  ### Minor Changes
package/README.md CHANGED
@@ -3,6 +3,12 @@
3
3
  Versioned SDUI document, fragment, token-set, component-pack, and promoted release
4
4
  contracts validated independently of any renderer.
5
5
 
6
+ The browser-safe `@fabricorg/sdui-release/contracts` entry adds Release v3 route and
7
+ tree-scoped grant contracts, RFC 8785 canonicalization, async crypto adapters, ES256
8
+ JWS verification, and signed monotonic release-channel pointers. It contains no Node
9
+ builtin or runtime Platform import. Release v2 and the existing root entry remain
10
+ compatible.
11
+
6
12
  ## What this package owns
7
13
 
8
14
  - **`SduiDocument`** — a versioned tree of fragments with a token-set reference and
@@ -78,7 +84,7 @@ validateSduiRelease({
78
84
  });
79
85
  ```
80
86
 
81
- An experience host or adoption-binding build can derive these references from
87
+ An experience composer or adoption-binding build can derive these references from
82
88
  its reviewed grants. Supplying a contract, or merely finding a matching
83
89
  capability reference, never authorizes a read or mutation.
84
90
 
@@ -99,5 +105,6 @@ fragments and forwards action intents through
99
105
  This package depends on `@fabricorg/assembly` for shared semver resolution,
100
106
  `@fabricorg/gen-capability` for `UsageContractDocument` validation, and
101
107
  `@fabricorg/platform` for `PortableJsonSchema`. It does not depend on a renderer,
102
- a host, or a specific runtime. SDUI rendering, theming, and composition remain
103
- outside Fabric Platform, as established by ADR 0006.
108
+ a Host, or a specific runtime. SDUI rendering, theming, and composition remain
109
+ outside the portable kernel and governed Hosts. Separately versioned experience-tier
110
+ packages support those concerns as established by ADR 0015.
@@ -0,0 +1,255 @@
1
+ // contracts.ts
2
+ var SDUI_RELEASE_V3_FORMAT_VERSION = 3;
3
+ var RELEASE_CHANNEL_POINTER_FORMAT_VERSION = 1;
4
+ var encoder = new TextEncoder();
5
+ var decoder = new TextDecoder("utf-8", { fatal: true });
6
+ var HEX_DIGEST = /^[a-f0-9]{64}$/;
7
+ function canonicalizeJcs(value) {
8
+ return canonical(value, 0);
9
+ }
10
+ function canonical(value, depth) {
11
+ if (depth > 64) throw new Error("Canonical JSON exceeds the maximum depth of 64.");
12
+ if (value === null) return "null";
13
+ if (typeof value === "boolean") return value ? "true" : "false";
14
+ if (typeof value === "number") {
15
+ if (!Number.isFinite(value)) throw new Error("Canonical JSON numbers must be finite.");
16
+ return Object.is(value, -0) ? "0" : JSON.stringify(value);
17
+ }
18
+ if (typeof value === "string") {
19
+ assertValidUnicode(value);
20
+ return JSON.stringify(value);
21
+ }
22
+ if (Array.isArray(value)) return `[${value.map((entry) => canonical(entry, depth + 1)).join(",")}]`;
23
+ if (typeof value !== "object") throw new Error(`Canonical JSON cannot encode ${typeof value}.`);
24
+ const record = value;
25
+ const keys = Object.keys(record).sort();
26
+ return `{${keys.map((key) => {
27
+ assertValidUnicode(key);
28
+ return `${JSON.stringify(key)}:${canonical(record[key], depth + 1)}`;
29
+ }).join(",")}}`;
30
+ }
31
+ function assertValidUnicode(value) {
32
+ for (let index = 0; index < value.length; index += 1) {
33
+ const code = value.charCodeAt(index);
34
+ if (code >= 55296 && code <= 56319) {
35
+ const next = value.charCodeAt(index + 1);
36
+ if (!(next >= 56320 && next <= 57343)) throw new Error("Canonical JSON contains an unpaired Unicode surrogate.");
37
+ index += 1;
38
+ } else if (code >= 56320 && code <= 57343) {
39
+ throw new Error("Canonical JSON contains an unpaired Unicode surrogate.");
40
+ }
41
+ }
42
+ }
43
+ async function verifySignedExperienceArtifact(input) {
44
+ const maxBytes = input.maxBytes ?? 1048576;
45
+ if (encoder.encode(input.compactJws).byteLength > maxBytes) {
46
+ throw new Error(`Signed experience artifact exceeds the ${maxBytes}-byte size limit.`);
47
+ }
48
+ const parts = input.compactJws.split(".");
49
+ if (parts.length !== 3 || parts.some((part) => part.length === 0)) throw new Error("Signed experience artifact must be a compact JWS.");
50
+ const header = parseSegment(parts[0], "protected header");
51
+ if (header.alg !== "ES256") throw new Error("Signed experience artifacts must use ES256.");
52
+ if (header.typ !== input.expectedType) throw new Error(`Signed experience artifact type must be ${input.expectedType}.`);
53
+ if (typeof header.kid !== "string" || !header.kid) throw new Error("Signed experience artifact requires a key id.");
54
+ const payloadBytes = decodeBase64Url(parts[1]);
55
+ const payload = parseJsonBytes(payloadBytes, "payload");
56
+ if (typeof payload.issuer !== "string" || !payload.issuer) throw new Error("Signed experience artifact requires an issuer.");
57
+ const key = await input.trust.resolveKey({ keyId: header.kid, issuer: payload.issuer, algorithm: "ES256" });
58
+ if (!key || key.algorithm !== "ES256") throw new Error("Signed experience artifact has no trusted key.");
59
+ const signature = decodeBase64Url(parts[2]);
60
+ if (signature.byteLength !== 64) throw new Error("ES256 signatures must be 64-byte JOSE signatures.");
61
+ const verified = await input.crypto.verify({
62
+ algorithm: "ES256",
63
+ key: key.key,
64
+ signingInput: encoder.encode(`${parts[0]}.${parts[1]}`),
65
+ signature
66
+ });
67
+ if (!verified) throw new Error("Signed experience artifact signature is invalid.");
68
+ return payload;
69
+ }
70
+ async function verifySignedExperienceRelease(input) {
71
+ const payload = await verifySignedExperienceArtifact({
72
+ compactJws: input.compactJws,
73
+ expectedType: "fabric-experience-release+jws",
74
+ crypto: input.crypto,
75
+ trust: input.trust,
76
+ ...input.maxBytes === void 0 ? {} : { maxBytes: input.maxBytes }
77
+ });
78
+ assertSduiReleaseV3(payload);
79
+ if (payload.issuer !== input.expected.issuer || payload.application !== input.expected.application || payload.assemblyDigest !== input.expected.assemblyDigest) {
80
+ throw new Error("Signed experience release does not match the expected issuer, application, and assembly.");
81
+ }
82
+ const { releaseDigest, ...body } = payload;
83
+ const digest = await input.crypto.digestSha256(encoder.encode(canonicalizeJcs(body)));
84
+ const expectedDigest = [...digest].map((byte) => byte.toString(16).padStart(2, "0")).join("");
85
+ if (releaseDigest !== expectedDigest) throw new Error("Signed experience release digest does not match its canonical content.");
86
+ return payload;
87
+ }
88
+ function assertSduiReleaseV3(value) {
89
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("SDUI Release v3 must be an object.");
90
+ const release = value;
91
+ if (release.formatVersion !== SDUI_RELEASE_V3_FORMAT_VERSION || release.kind !== "experience-release") {
92
+ throw new Error("SDUI Release v3 has an unsupported format.");
93
+ }
94
+ for (const field of ["issuer", "application", "releaseDigest", "assemblyDigest"]) {
95
+ if (typeof release[field] !== "string" || !release[field]) throw new Error(`SDUI Release v3 ${field} is required.`);
96
+ }
97
+ if (!HEX_DIGEST.test(release.releaseDigest) || !HEX_DIGEST.test(release.assemblyDigest)) {
98
+ throw new Error("SDUI Release v3 digests must be lowercase SHA-256 values.");
99
+ }
100
+ 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)) {
101
+ throw new Error("SDUI Release v3 is structurally incomplete.");
102
+ }
103
+ const routes = release.routes;
104
+ assertUniqueRecords(routes.views, "view route", (route) => {
105
+ const record = assertRecord(route, "view route");
106
+ assertCapabilityReference(record.reference, "view route");
107
+ assertNonEmptyString(record.name, "view route name");
108
+ assertNonEmptyString(record.version, "view route version");
109
+ if (record.parameterSchemaDigest !== void 0) {
110
+ assertHexDigest(record.parameterSchemaDigest, "view route parameterSchemaDigest");
111
+ }
112
+ return record.reference;
113
+ });
114
+ assertUniqueRecords(routes.intents, "intent route", (route) => {
115
+ const record = assertRecord(route, "intent route");
116
+ assertCapabilityReference(record.reference, "intent route");
117
+ assertNonEmptyString(record.actionId, "intent route actionId");
118
+ if (!Number.isSafeInteger(record.version) || record.version < 1) {
119
+ throw new Error("SDUI Release v3 intent route version must be a positive integer.");
120
+ }
121
+ if (record.parameterSchemaDigest !== void 0) {
122
+ assertHexDigest(record.parameterSchemaDigest, "intent route parameterSchemaDigest");
123
+ }
124
+ return record.reference;
125
+ });
126
+ assertUniqueRecords(release.effectiveGrants, "effective grant", (grant) => {
127
+ const record = assertRecord(grant, "effective grant");
128
+ assertNonEmptyString(record.treeId, "effective grant treeId");
129
+ assertNonEmptyString(record.fragmentId, "effective grant fragmentId");
130
+ if (!Array.isArray(record.views) || !record.views.every((item) => typeof item === "string") || !Array.isArray(record.intents) || !record.intents.every((item) => typeof item === "string")) {
131
+ throw new Error("SDUI Release v3 effective grant routes must be string arrays.");
132
+ }
133
+ return `${record.treeId}\0${record.fragmentId}`;
134
+ });
135
+ }
136
+ function assertRecord(value, label) {
137
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`SDUI Release v3 ${label} must be an object.`);
138
+ return value;
139
+ }
140
+ function assertNonEmptyString(value, label) {
141
+ if (typeof value !== "string" || !value) throw new Error(`SDUI Release v3 ${label} is required.`);
142
+ }
143
+ function assertHexDigest(value, label) {
144
+ if (typeof value !== "string" || !HEX_DIGEST.test(value)) throw new Error(`SDUI Release v3 ${label} must be a lowercase SHA-256 value.`);
145
+ }
146
+ function assertCapabilityReference(value, label) {
147
+ if (typeof value !== "string" || !value.startsWith("capability://")) throw new Error(`SDUI Release v3 ${label} reference is invalid.`);
148
+ }
149
+ function assertUniqueRecords(values, label, identity) {
150
+ const seen = /* @__PURE__ */ new Set();
151
+ for (const value of values) {
152
+ const key = identity(value);
153
+ if (seen.has(key)) throw new Error(`SDUI Release v3 has a duplicate ${label}.`);
154
+ seen.add(key);
155
+ }
156
+ }
157
+ async function activateReleaseChannelPointer(input) {
158
+ const payload = await verifySignedExperienceArtifact({
159
+ compactJws: input.compactJws,
160
+ expectedType: "fabric-release-channel-pointer+jws",
161
+ crypto: input.crypto,
162
+ trust: input.trust
163
+ });
164
+ assertReleaseChannelPointer(payload);
165
+ if (payload.issuer !== input.expected.issuer || payload.application !== input.expected.application || payload.channel !== input.expected.channel) {
166
+ throw new Error("Release channel pointer does not match the expected issuer, application, and channel.");
167
+ }
168
+ const now = (input.now ?? /* @__PURE__ */ new Date()).getTime();
169
+ if (payload.notBefore !== void 0 && now < parseTimestamp(payload.notBefore, "notBefore")) {
170
+ throw new Error("Release channel pointer is not active yet.");
171
+ }
172
+ if (payload.expiresAt !== void 0 && now >= parseTimestamp(payload.expiresAt, "expiresAt")) {
173
+ throw new Error("Release channel pointer has expired.");
174
+ }
175
+ const generationKey = `${payload.issuer}:${payload.application}:${payload.channel}`;
176
+ const pointerDigestBytes = await input.crypto.digestSha256(encoder.encode(canonicalizeJcs(payload)));
177
+ const pointerDigest = [...pointerDigestBytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
178
+ const current = await input.generations.get(generationKey);
179
+ if (current !== void 0 && payload.generation < current.generation) {
180
+ throw new Error("Release channel pointer is older than the accepted generation.");
181
+ }
182
+ if (current?.generation === payload.generation && current.pointerDigest !== pointerDigest) {
183
+ throw new Error("Release channel generation is already bound to a different signed pointer.");
184
+ }
185
+ if (current?.generation !== payload.generation) {
186
+ const accepted = await input.generations.acceptIfHigher(generationKey, payload.generation, pointerDigest);
187
+ if (!accepted) {
188
+ const raced = await input.generations.get(generationKey);
189
+ if (raced?.generation !== payload.generation || raced.pointerDigest !== pointerDigest) {
190
+ throw new Error("Release channel pointer lost an activation race to a different generation.");
191
+ }
192
+ }
193
+ }
194
+ return payload;
195
+ }
196
+ function assertReleaseChannelPointer(value) {
197
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Release channel pointer must be an object.");
198
+ const pointer = value;
199
+ if (pointer.formatVersion !== RELEASE_CHANNEL_POINTER_FORMAT_VERSION || pointer.kind !== "release-channel-pointer") {
200
+ throw new Error("Release channel pointer has an unsupported format.");
201
+ }
202
+ for (const field of ["issuer", "application", "channel", "issuedAt"]) {
203
+ if (typeof pointer[field] !== "string" || !pointer[field]) throw new Error(`Release channel pointer ${field} is required.`);
204
+ }
205
+ if (!Number.isSafeInteger(pointer.generation) || pointer.generation < 0) throw new Error("Release channel pointer generation must be a non-negative safe integer.");
206
+ if (!HEX_DIGEST.test(String(pointer.activeReleaseDigest)) || !HEX_DIGEST.test(String(pointer.assemblyDigest))) {
207
+ throw new Error("Release channel pointer digests must be lowercase SHA-256 values.");
208
+ }
209
+ 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)) {
210
+ throw new Error("Release channel pointer fallbackReleaseDigests must contain unique lowercase SHA-256 values.");
211
+ }
212
+ parseTimestamp(pointer.issuedAt, "issuedAt");
213
+ }
214
+ function parseSegment(segment, label) {
215
+ return parseJsonBytes(decodeBase64Url(segment), label);
216
+ }
217
+ function parseJsonBytes(bytes, label) {
218
+ let value;
219
+ try {
220
+ value = JSON.parse(decoder.decode(bytes));
221
+ } catch {
222
+ throw new Error(`Signed experience artifact ${label} is not valid UTF-8 JSON.`);
223
+ }
224
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`Signed experience artifact ${label} must be an object.`);
225
+ return value;
226
+ }
227
+ function decodeBase64Url(value) {
228
+ if (!/^[A-Za-z0-9_-]+$/.test(value)) throw new Error("Signed experience artifact contains invalid base64url.");
229
+ const padded = value.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(value.length / 4) * 4, "=");
230
+ let binary;
231
+ try {
232
+ binary = atob(padded);
233
+ } catch {
234
+ throw new Error("Signed experience artifact contains invalid base64url.");
235
+ }
236
+ return Uint8Array.from(binary, (character) => character.charCodeAt(0));
237
+ }
238
+ function parseTimestamp(value, field) {
239
+ if (!/(?:Z|[+-]\d{2}:\d{2})$/.test(value)) throw new Error(`Release channel pointer ${field} must be RFC 3339 with an explicit offset.`);
240
+ const parsed = Date.parse(value);
241
+ if (Number.isNaN(parsed)) throw new Error(`Release channel pointer ${field} is invalid.`);
242
+ return parsed;
243
+ }
244
+
245
+ export {
246
+ SDUI_RELEASE_V3_FORMAT_VERSION,
247
+ RELEASE_CHANNEL_POINTER_FORMAT_VERSION,
248
+ canonicalizeJcs,
249
+ verifySignedExperienceArtifact,
250
+ verifySignedExperienceRelease,
251
+ assertSduiReleaseV3,
252
+ activateReleaseChannelPointer,
253
+ assertReleaseChannelPointer
254
+ };
255
+ //# sourceMappingURL=chunk-BTWHVBZ3.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../contracts.ts"],"sourcesContent":["import type { ActionExecutionContract, PortableJsonSchema } from \"@fabricorg/platform\";\nimport type { SduiDocument, SduiTokenSet, ResolvedSduiComponentPack } from \"./types\";\nexport type {\n\tSduiActionRef,\n\tSduiDocument,\n\tSduiFragment,\n\tSduiFragmentValue,\n\tSduiTokenSet,\n\tResolvedSduiComponentPack,\n} from \"./types\";\n\nexport const SDUI_RELEASE_V3_FORMAT_VERSION = 3 as const;\nexport const RELEASE_CHANNEL_POINTER_FORMAT_VERSION = 1 as const;\n\nexport interface ExperienceViewRoute {\n\treference: `capability://${string}`;\n\tname: `${string}/${string}`;\n\tversion: string;\n\tparameterSchema?: PortableJsonSchema;\n\tparameterSchemaDigest?: string;\n}\n\nexport interface ExperienceIntentRoute {\n\treference: `capability://${string}`;\n\tactionId: `${string}.${string}`;\n\tversion: number;\n\tparameterSchema?: PortableJsonSchema;\n\tparameterSchemaDigest?: string;\n\texecution?: ActionExecutionContract;\n}\n\nexport interface EffectiveFragmentGrant {\n\ttreeId: \"root\" | string;\n\tfragmentId: string;\n\tviews: readonly string[];\n\tintents: readonly string[];\n}\n\nexport interface SduiReleaseV3 {\n\tformatVersion: typeof SDUI_RELEASE_V3_FORMAT_VERSION;\n\tkind: \"experience-release\";\n\tissuer: string;\n\tapplication: string;\n\treleaseDigest: string;\n\tassemblyDigest: string;\n\tdocument: SduiDocument;\n\ttokenSet: SduiTokenSet;\n\tcomponentPacks: ResolvedSduiComponentPack[];\n\troutes: {\n\t\tviews: ExperienceViewRoute[];\n\t\tintents: ExperienceIntentRoute[];\n\t};\n\teffectiveGrants: EffectiveFragmentGrant[];\n}\n\nexport interface ReleaseChannelPointer {\n\tformatVersion: typeof RELEASE_CHANNEL_POINTER_FORMAT_VERSION;\n\tkind: \"release-channel-pointer\";\n\tissuer: string;\n\tapplication: string;\n\tchannel: string;\n\tgeneration: number;\n\tactiveReleaseDigest: string;\n\tassemblyDigest: string;\n\tissuedAt: string;\n\tnotBefore?: string;\n\texpiresAt?: string;\n\tfallbackReleaseDigests?: readonly string[];\n}\n\nexport interface ReleaseVerificationKey {\n\tkeyId: string;\n\talgorithm: \"ES256\";\n\tkey: unknown;\n}\n\nexport interface ReleaseTrustStore {\n\tresolveKey(input: { keyId: string; issuer: string; algorithm: \"ES256\" }): ReleaseVerificationKey | undefined | Promise<ReleaseVerificationKey | undefined>;\n}\n\nexport interface ReleaseCryptoPort {\n\tverify(input: {\n\t\talgorithm: \"ES256\";\n\t\tkey: unknown;\n\t\tsigningInput: Uint8Array;\n\t\tsignature: Uint8Array;\n\t}): Promise<boolean>;\n\tdigestSha256(bytes: Uint8Array): Promise<Uint8Array>;\n}\n\nexport interface ReleaseGenerationStore {\n\tget(key: string): Promise<{ generation: number; pointerDigest: string } | undefined>;\n\t/**\n\t * Atomically accepts `generation` only when it is greater than the stored\n\t * value for `key`. Implementations must compare and write in one operation.\n\t */\n\tacceptIfHigher(key: string, generation: number, pointerDigest: string): Promise<boolean>;\n}\n\nexport interface VerifySignedExperienceArtifactInput {\n\tcompactJws: string;\n\texpectedType: \"fabric-experience-release+jws\" | \"fabric-release-channel-pointer+jws\";\n\tcrypto: ReleaseCryptoPort;\n\ttrust: ReleaseTrustStore;\n\tmaxBytes?: number;\n}\n\nconst encoder = new TextEncoder();\nconst decoder = new TextDecoder(\"utf-8\", { fatal: true });\nconst HEX_DIGEST = /^[a-f0-9]{64}$/;\n\n/**\n * RFC 8785 JSON Canonicalization Scheme for values already restricted to the\n * JSON data model. Invalid Unicode, non-finite numbers and non-JSON values are\n * rejected rather than normalized differently by another runtime.\n */\nexport function canonicalizeJcs(value: unknown): string {\n\treturn canonical(value, 0);\n}\n\nfunction canonical(value: unknown, depth: number): string {\n\tif (depth > 64) throw new Error(\"Canonical JSON exceeds the maximum depth of 64.\");\n\tif (value === null) return \"null\";\n\tif (typeof value === \"boolean\") return value ? \"true\" : \"false\";\n\tif (typeof value === \"number\") {\n\t\tif (!Number.isFinite(value)) throw new Error(\"Canonical JSON numbers must be finite.\");\n\t\treturn Object.is(value, -0) ? \"0\" : JSON.stringify(value);\n\t}\n\tif (typeof value === \"string\") {\n\t\tassertValidUnicode(value);\n\t\treturn JSON.stringify(value);\n\t}\n\tif (Array.isArray(value)) return `[${value.map((entry) => canonical(entry, depth + 1)).join(\",\")}]`;\n\tif (typeof value !== \"object\") throw new Error(`Canonical JSON cannot encode ${typeof value}.`);\n\tconst record = value as Record<string, unknown>;\n\tconst keys = Object.keys(record).sort();\n\treturn `{${keys.map((key) => {\n\t\tassertValidUnicode(key);\n\t\treturn `${JSON.stringify(key)}:${canonical(record[key], depth + 1)}`;\n\t}).join(\",\")}}`;\n}\n\nfunction assertValidUnicode(value: string): void {\n\tfor (let index = 0; index < value.length; index += 1) {\n\t\tconst code = value.charCodeAt(index);\n\t\tif (code >= 0xd800 && code <= 0xdbff) {\n\t\t\tconst next = value.charCodeAt(index + 1);\n\t\t\tif (!(next >= 0xdc00 && next <= 0xdfff)) throw new Error(\"Canonical JSON contains an unpaired Unicode surrogate.\");\n\t\t\tindex += 1;\n\t\t} else if (code >= 0xdc00 && code <= 0xdfff) {\n\t\t\tthrow new Error(\"Canonical JSON contains an unpaired Unicode surrogate.\");\n\t\t}\n\t}\n}\n\nexport async function verifySignedExperienceArtifact(\n\tinput: VerifySignedExperienceArtifactInput,\n): Promise<Record<string, unknown>> {\n\tconst maxBytes = input.maxBytes ?? 1_048_576;\n\tif (encoder.encode(input.compactJws).byteLength > maxBytes) {\n\t\tthrow new Error(`Signed experience artifact exceeds the ${maxBytes}-byte size limit.`);\n\t}\n\tconst parts = input.compactJws.split(\".\");\n\tif (parts.length !== 3 || parts.some((part) => part.length === 0)) throw new Error(\"Signed experience artifact must be a compact JWS.\");\n\tconst header = parseSegment(parts[0]!, \"protected header\");\n\tif (header.alg !== \"ES256\") throw new Error(\"Signed experience artifacts must use ES256.\");\n\tif (header.typ !== input.expectedType) throw new Error(`Signed experience artifact type must be ${input.expectedType}.`);\n\tif (typeof header.kid !== \"string\" || !header.kid) throw new Error(\"Signed experience artifact requires a key id.\");\n\tconst payloadBytes = decodeBase64Url(parts[1]!);\n\tconst payload = parseJsonBytes(payloadBytes, \"payload\");\n\tif (typeof payload.issuer !== \"string\" || !payload.issuer) throw new Error(\"Signed experience artifact requires an issuer.\");\n\tconst key = await input.trust.resolveKey({ keyId: header.kid, issuer: payload.issuer, algorithm: \"ES256\" });\n\tif (!key || key.algorithm !== \"ES256\") throw new Error(\"Signed experience artifact has no trusted key.\");\n\tconst signature = decodeBase64Url(parts[2]!);\n\tif (signature.byteLength !== 64) throw new Error(\"ES256 signatures must be 64-byte JOSE signatures.\");\n\tconst verified = await input.crypto.verify({\n\t\talgorithm: \"ES256\",\n\t\tkey: key.key,\n\t\tsigningInput: encoder.encode(`${parts[0]}.${parts[1]}`),\n\t\tsignature,\n\t});\n\tif (!verified) throw new Error(\"Signed experience artifact signature is invalid.\");\n\treturn payload;\n}\n\nexport async function verifySignedExperienceRelease(input: {\n\tcompactJws: string;\n\tcrypto: ReleaseCryptoPort;\n\ttrust: ReleaseTrustStore;\n\texpected: { issuer: string; application: string; assemblyDigest: string };\n\tmaxBytes?: number;\n}): Promise<SduiReleaseV3> {\n\tconst payload = await verifySignedExperienceArtifact({\n\t\tcompactJws: input.compactJws,\n\t\texpectedType: \"fabric-experience-release+jws\",\n\t\tcrypto: input.crypto,\n\t\ttrust: input.trust,\n\t\t...(input.maxBytes === undefined ? {} : { maxBytes: input.maxBytes }),\n\t});\n\tassertSduiReleaseV3(payload);\n\tif (payload.issuer !== input.expected.issuer\n\t\t|| payload.application !== input.expected.application\n\t\t|| payload.assemblyDigest !== input.expected.assemblyDigest) {\n\t\tthrow new Error(\"Signed experience release does not match the expected issuer, application, and assembly.\");\n\t}\n\tconst { releaseDigest, ...body } = payload;\n\tconst digest = await input.crypto.digestSha256(encoder.encode(canonicalizeJcs(body)));\n\tconst expectedDigest = [...digest].map((byte) => byte.toString(16).padStart(2, \"0\")).join(\"\");\n\tif (releaseDigest !== expectedDigest) throw new Error(\"Signed experience release digest does not match its canonical content.\");\n\treturn payload;\n}\n\nexport function assertSduiReleaseV3(value: unknown): asserts value is SduiReleaseV3 {\n\tif (!value || typeof value !== \"object\" || Array.isArray(value)) throw new Error(\"SDUI Release v3 must be an object.\");\n\tconst release = value as Record<string, unknown>;\n\tif (release.formatVersion !== SDUI_RELEASE_V3_FORMAT_VERSION || release.kind !== \"experience-release\") {\n\t\tthrow new Error(\"SDUI Release v3 has an unsupported format.\");\n\t}\n\tfor (const field of [\"issuer\", \"application\", \"releaseDigest\", \"assemblyDigest\"] as const) {\n\t\tif (typeof release[field] !== \"string\" || !release[field]) throw new Error(`SDUI Release v3 ${field} is required.`);\n\t}\n\tif (!HEX_DIGEST.test(release.releaseDigest as string) || !HEX_DIGEST.test(release.assemblyDigest as string)) {\n\t\tthrow new Error(\"SDUI Release v3 digests must be lowercase SHA-256 values.\");\n\t}\n\tif (!release.document || typeof release.document !== \"object\"\n\t\t|| !release.tokenSet || typeof release.tokenSet !== \"object\"\n\t\t|| !Array.isArray(release.componentPacks)\n\t\t|| !release.routes || typeof release.routes !== \"object\"\n\t\t|| !Array.isArray((release.routes as Record<string, unknown>).views)\n\t\t|| !Array.isArray((release.routes as Record<string, unknown>).intents)\n\t\t|| !Array.isArray(release.effectiveGrants)) {\n\t\tthrow new Error(\"SDUI Release v3 is structurally incomplete.\");\n\t}\n\tconst routes = release.routes as { views: unknown[]; intents: unknown[] };\n\tassertUniqueRecords(routes.views, \"view route\", (route) => {\n\t\tconst record = assertRecord(route, \"view route\");\n\t\tassertCapabilityReference(record.reference, \"view route\");\n\t\tassertNonEmptyString(record.name, \"view route name\");\n\t\tassertNonEmptyString(record.version, \"view route version\");\n\t\tif (record.parameterSchemaDigest !== undefined) {\n\t\t\tassertHexDigest(record.parameterSchemaDigest, \"view route parameterSchemaDigest\");\n\t\t}\n\t\treturn record.reference as string;\n\t});\n\tassertUniqueRecords(routes.intents, \"intent route\", (route) => {\n\t\tconst record = assertRecord(route, \"intent route\");\n\t\tassertCapabilityReference(record.reference, \"intent route\");\n\t\tassertNonEmptyString(record.actionId, \"intent route actionId\");\n\t\tif (!Number.isSafeInteger(record.version) || (record.version as number) < 1) {\n\t\t\tthrow new Error(\"SDUI Release v3 intent route version must be a positive integer.\");\n\t\t}\n\t\tif (record.parameterSchemaDigest !== undefined) {\n\t\t\tassertHexDigest(record.parameterSchemaDigest, \"intent route parameterSchemaDigest\");\n\t\t}\n\t\treturn record.reference as string;\n\t});\n\tassertUniqueRecords(release.effectiveGrants as unknown[], \"effective grant\", (grant) => {\n\t\tconst record = assertRecord(grant, \"effective grant\");\n\t\tassertNonEmptyString(record.treeId, \"effective grant treeId\");\n\t\tassertNonEmptyString(record.fragmentId, \"effective grant fragmentId\");\n\t\tif (!Array.isArray(record.views) || !record.views.every((item) => typeof item === \"string\")\n\t\t\t|| !Array.isArray(record.intents) || !record.intents.every((item) => typeof item === \"string\")) {\n\t\t\tthrow new Error(\"SDUI Release v3 effective grant routes must be string arrays.\");\n\t\t}\n\t\treturn `${record.treeId as string}\\0${record.fragmentId as string}`;\n\t});\n}\n\nfunction assertRecord(value: unknown, label: string): Record<string, unknown> {\n\tif (!value || typeof value !== \"object\" || Array.isArray(value)) throw new Error(`SDUI Release v3 ${label} must be an object.`);\n\treturn value as Record<string, unknown>;\n}\n\nfunction assertNonEmptyString(value: unknown, label: string): asserts value is string {\n\tif (typeof value !== \"string\" || !value) throw new Error(`SDUI Release v3 ${label} is required.`);\n}\n\nfunction assertHexDigest(value: unknown, label: string): asserts value is string {\n\tif (typeof value !== \"string\" || !HEX_DIGEST.test(value)) throw new Error(`SDUI Release v3 ${label} must be a lowercase SHA-256 value.`);\n}\n\nfunction assertCapabilityReference(value: unknown, label: string): asserts value is `capability://${string}` {\n\tif (typeof value !== \"string\" || !value.startsWith(\"capability://\")) throw new Error(`SDUI Release v3 ${label} reference is invalid.`);\n}\n\nfunction assertUniqueRecords(values: unknown[], label: string, identity: (value: unknown) => string): void {\n\tconst seen = new Set<string>();\n\tfor (const value of values) {\n\t\tconst key = identity(value);\n\t\tif (seen.has(key)) throw new Error(`SDUI Release v3 has a duplicate ${label}.`);\n\t\tseen.add(key);\n\t}\n}\n\nexport async function activateReleaseChannelPointer(input: {\n\tcompactJws: string;\n\tcrypto: ReleaseCryptoPort;\n\ttrust: ReleaseTrustStore;\n\tgenerations: ReleaseGenerationStore;\n\texpected: { issuer: string; application: string; channel: string };\n\tnow?: Date;\n}): Promise<ReleaseChannelPointer> {\n\tconst payload = await verifySignedExperienceArtifact({\n\t\tcompactJws: input.compactJws,\n\t\texpectedType: \"fabric-release-channel-pointer+jws\",\n\t\tcrypto: input.crypto,\n\t\ttrust: input.trust,\n\t});\n\tassertReleaseChannelPointer(payload);\n\tif (payload.issuer !== input.expected.issuer\n\t\t|| payload.application !== input.expected.application\n\t\t|| payload.channel !== input.expected.channel) {\n\t\tthrow new Error(\"Release channel pointer does not match the expected issuer, application, and channel.\");\n\t}\n\tconst now = (input.now ?? new Date()).getTime();\n\tif (payload.notBefore !== undefined && now < parseTimestamp(payload.notBefore, \"notBefore\")) {\n\t\tthrow new Error(\"Release channel pointer is not active yet.\");\n\t}\n\tif (payload.expiresAt !== undefined && now >= parseTimestamp(payload.expiresAt, \"expiresAt\")) {\n\t\tthrow new Error(\"Release channel pointer has expired.\");\n\t}\n\tconst generationKey = `${payload.issuer}:${payload.application}:${payload.channel}`;\n\tconst pointerDigestBytes = await input.crypto.digestSha256(encoder.encode(canonicalizeJcs(payload)));\n\tconst pointerDigest = [...pointerDigestBytes].map((byte) => byte.toString(16).padStart(2, \"0\")).join(\"\");\n\tconst current = await input.generations.get(generationKey);\n\tif (current !== undefined && payload.generation < current.generation) {\n\t\tthrow new Error(\"Release channel pointer is older than the accepted generation.\");\n\t}\n\tif (current?.generation === payload.generation && current.pointerDigest !== pointerDigest) {\n\t\tthrow new Error(\"Release channel generation is already bound to a different signed pointer.\");\n\t}\n\tif (current?.generation !== payload.generation) {\n\t\tconst accepted = await input.generations.acceptIfHigher(generationKey, payload.generation, pointerDigest);\n\t\tif (!accepted) {\n\t\t\tconst raced = await input.generations.get(generationKey);\n\t\t\tif (raced?.generation !== payload.generation || raced.pointerDigest !== pointerDigest) {\n\t\t\t\tthrow new Error(\"Release channel pointer lost an activation race to a different generation.\");\n\t\t\t}\n\t\t}\n\t}\n\treturn payload;\n}\n\nexport function assertReleaseChannelPointer(value: unknown): asserts value is ReleaseChannelPointer {\n\tif (!value || typeof value !== \"object\" || Array.isArray(value)) throw new Error(\"Release channel pointer must be an object.\");\n\tconst pointer = value as Record<string, unknown>;\n\tif (pointer.formatVersion !== RELEASE_CHANNEL_POINTER_FORMAT_VERSION || pointer.kind !== \"release-channel-pointer\") {\n\t\tthrow new Error(\"Release channel pointer has an unsupported format.\");\n\t}\n\tfor (const field of [\"issuer\", \"application\", \"channel\", \"issuedAt\"] as const) {\n\t\tif (typeof pointer[field] !== \"string\" || !pointer[field]) throw new Error(`Release channel pointer ${field} is required.`);\n\t}\n\tif (!Number.isSafeInteger(pointer.generation) || (pointer.generation as number) < 0) throw new Error(\"Release channel pointer generation must be a non-negative safe integer.\");\n\tif (!HEX_DIGEST.test(String(pointer.activeReleaseDigest)) || !HEX_DIGEST.test(String(pointer.assemblyDigest))) {\n\t\tthrow new Error(\"Release channel pointer digests must be lowercase SHA-256 values.\");\n\t}\n\tif (pointer.fallbackReleaseDigests !== undefined\n\t\t&& (!Array.isArray(pointer.fallbackReleaseDigests)\n\t\t\t|| !pointer.fallbackReleaseDigests.every((digest) => typeof digest === \"string\" && HEX_DIGEST.test(digest))\n\t\t\t|| new Set(pointer.fallbackReleaseDigests).size !== pointer.fallbackReleaseDigests.length)) {\n\t\tthrow new Error(\"Release channel pointer fallbackReleaseDigests must contain unique lowercase SHA-256 values.\");\n\t}\n\tparseTimestamp(pointer.issuedAt as string, \"issuedAt\");\n}\n\nfunction parseSegment(segment: string, label: string): Record<string, unknown> {\n\treturn parseJsonBytes(decodeBase64Url(segment), label);\n}\n\nfunction parseJsonBytes(bytes: Uint8Array, label: string): Record<string, unknown> {\n\tlet value: unknown;\n\ttry { value = JSON.parse(decoder.decode(bytes)); }\n\tcatch { throw new Error(`Signed experience artifact ${label} is not valid UTF-8 JSON.`); }\n\tif (!value || typeof value !== \"object\" || Array.isArray(value)) throw new Error(`Signed experience artifact ${label} must be an object.`);\n\treturn value as Record<string, unknown>;\n}\n\nfunction decodeBase64Url(value: string): Uint8Array {\n\tif (!/^[A-Za-z0-9_-]+$/.test(value)) throw new Error(\"Signed experience artifact contains invalid base64url.\");\n\tconst padded = value.replace(/-/g, \"+\").replace(/_/g, \"/\").padEnd(Math.ceil(value.length / 4) * 4, \"=\");\n\tlet binary: string;\n\ttry { binary = atob(padded); }\n\tcatch { throw new Error(\"Signed experience artifact contains invalid base64url.\"); }\n\treturn Uint8Array.from(binary, (character) => character.charCodeAt(0));\n}\n\nfunction parseTimestamp(value: string, field: string): number {\n\tif (!/(?:Z|[+-]\\d{2}:\\d{2})$/.test(value)) throw new Error(`Release channel pointer ${field} must be RFC 3339 with an explicit offset.`);\n\tconst parsed = Date.parse(value);\n\tif (Number.isNaN(parsed)) throw new Error(`Release channel pointer ${field} is invalid.`);\n\treturn parsed;\n}\n"],"mappings":"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","names":[]}