@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 +176 -0
- package/README.md +10 -3
- package/dist/chunk-BTWHVBZ3.js +255 -0
- package/dist/chunk-BTWHVBZ3.js.map +1 -0
- package/dist/contracts-4fL-Yh38.d.cts +399 -0
- package/dist/contracts-4fL-Yh38.d.ts +399 -0
- package/dist/contracts.cjs +286 -0
- package/dist/contracts.cjs.map +1 -0
- package/dist/contracts.d.cts +4 -0
- package/dist/contracts.d.ts +4 -0
- package/dist/contracts.js +21 -0
- package/dist/contracts.js.map +1 -0
- package/dist/index.cjs +579 -23
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +120 -261
- package/dist/index.d.ts +120 -261
- package/dist/index.js +331 -20
- package/dist/index.js.map +1 -1
- package/dist/node-crypto.cjs +51 -0
- package/dist/node-crypto.cjs.map +1 -0
- package/dist/node-crypto.d.cts +9 -0
- package/dist/node-crypto.d.ts +9 -0
- package/dist/node-crypto.js +26 -0
- package/dist/node-crypto.js.map +1 -0
- package/package.json +30 -6
package/dist/index.cjs
CHANGED
|
@@ -20,23 +20,35 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
RELEASE_CHANNEL_POINTER_FORMAT_VERSION: () => RELEASE_CHANNEL_POINTER_FORMAT_VERSION,
|
|
23
24
|
SDUI_COMPONENT_PACK_FORMAT_VERSION: () => SDUI_COMPONENT_PACK_FORMAT_VERSION,
|
|
24
25
|
SDUI_DOCUMENT_FORMAT_VERSION: () => SDUI_DOCUMENT_FORMAT_VERSION,
|
|
25
26
|
SDUI_DOCUMENT_JSON_SCHEMA: () => SDUI_DOCUMENT_JSON_SCHEMA,
|
|
26
27
|
SDUI_RELEASE_FORMAT_VERSION: () => SDUI_RELEASE_FORMAT_VERSION,
|
|
27
28
|
SDUI_RELEASE_JSON_SCHEMA: () => SDUI_RELEASE_JSON_SCHEMA,
|
|
29
|
+
SDUI_RELEASE_V3_FORMAT_VERSION: () => SDUI_RELEASE_V3_FORMAT_VERSION,
|
|
28
30
|
SDUI_TOKEN_SET_FORMAT_VERSION: () => SDUI_TOKEN_SET_FORMAT_VERSION,
|
|
31
|
+
activateReleaseChannelPointer: () => activateReleaseChannelPointer,
|
|
32
|
+
assertReleaseChannelPointer: () => assertReleaseChannelPointer,
|
|
29
33
|
assertSduiAuthorizationGrants: () => assertSduiAuthorizationGrants,
|
|
30
34
|
assertSduiComponentPack: () => assertSduiComponentPack,
|
|
31
35
|
assertSduiDocument: () => assertSduiDocument,
|
|
32
36
|
assertSduiFragment: () => assertSduiFragment,
|
|
33
37
|
assertSduiRelease: () => assertSduiRelease,
|
|
34
38
|
assertSduiReleaseIntegrity: () => assertSduiReleaseIntegrity,
|
|
39
|
+
assertSduiReleaseV3: () => assertSduiReleaseV3,
|
|
35
40
|
assertSduiReleaseValid: () => assertSduiReleaseValid,
|
|
36
41
|
assertSduiTokenSet: () => assertSduiTokenSet,
|
|
42
|
+
canonicalizeJcs: () => canonicalizeJcs,
|
|
37
43
|
computeReleaseDigest: () => computeReleaseDigest,
|
|
38
44
|
createSduiChannel: () => createSduiChannel,
|
|
39
|
-
|
|
45
|
+
deriveAuthorizationGrants: () => deriveAuthorizationGrants,
|
|
46
|
+
parseCapabilityRef: () => parseCapabilityRef,
|
|
47
|
+
runSduiChannelChecks: () => runSduiChannelChecks,
|
|
48
|
+
sduiChannelChecks: () => sduiChannelChecks,
|
|
49
|
+
validateSduiRelease: () => validateSduiRelease,
|
|
50
|
+
verifySignedExperienceArtifact: () => verifySignedExperienceArtifact,
|
|
51
|
+
verifySignedExperienceRelease: () => verifySignedExperienceRelease
|
|
40
52
|
});
|
|
41
53
|
module.exports = __toCommonJS(index_exports);
|
|
42
54
|
|
|
@@ -66,8 +78,9 @@ function requireArray(value, path) {
|
|
|
66
78
|
if (!Array.isArray(value)) fail(path, "must be an array");
|
|
67
79
|
return value;
|
|
68
80
|
}
|
|
81
|
+
var CAPABILITY_REF = /^capability:\/\/([a-z][a-z0-9-]*)\/([a-z][a-z0-9._-]*(?:\/[a-z0-9._-]+)*)$/;
|
|
69
82
|
function parseCapabilityRef(ref) {
|
|
70
|
-
const match =
|
|
83
|
+
const match = CAPABILITY_REF.exec(ref);
|
|
71
84
|
if (!match) return void 0;
|
|
72
85
|
return { namespace: match[1], name: match[2] };
|
|
73
86
|
}
|
|
@@ -202,8 +215,20 @@ function assertSduiDocument(value, path = "document") {
|
|
|
202
215
|
});
|
|
203
216
|
}
|
|
204
217
|
const tokenSet = requireRecord(document.tokenSet, `${path}.tokenSet`);
|
|
205
|
-
|
|
206
|
-
|
|
218
|
+
assertTokenSetReference(tokenSet, `${path}.tokenSet`);
|
|
219
|
+
if (document.supportedTokenSets !== void 0) {
|
|
220
|
+
const seenTokenSets = /* @__PURE__ */ new Set();
|
|
221
|
+
requireArray(document.supportedTokenSets, `${path}.supportedTokenSets`).forEach((entry, index) => {
|
|
222
|
+
const supportedPath = `${path}.supportedTokenSets[${index}]`;
|
|
223
|
+
const supported = requireRecord(entry, supportedPath);
|
|
224
|
+
assertTokenSetReference(supported, supportedPath);
|
|
225
|
+
const identity = `${String(supported.name)}\0${String(supported.version)}`;
|
|
226
|
+
if (seenTokenSets.has(identity)) {
|
|
227
|
+
fail(supportedPath, `declares token set "${String(supported.name)}@${String(supported.version)}" a second time`);
|
|
228
|
+
}
|
|
229
|
+
seenTokenSets.add(identity);
|
|
230
|
+
});
|
|
231
|
+
}
|
|
207
232
|
requireArray(document.componentPacks, `${path}.componentPacks`).forEach((entry, index) => {
|
|
208
233
|
const pack = requireRecord(entry, `${path}.componentPacks[${index}]`);
|
|
209
234
|
requireString(pack.pack, `${path}.componentPacks[${index}].pack`);
|
|
@@ -283,6 +308,14 @@ function assertSduiRelease(value, path = "release") {
|
|
|
283
308
|
}
|
|
284
309
|
});
|
|
285
310
|
}
|
|
311
|
+
function assertTokenSetReference(reference, path) {
|
|
312
|
+
if (!/^[a-z][a-z0-9-]*$/.test(requireString(reference.name, `${path}.name`))) {
|
|
313
|
+
fail(`${path}.name`, "must be lowercase letters, numbers and hyphens");
|
|
314
|
+
}
|
|
315
|
+
if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(requireString(reference.version, `${path}.version`))) {
|
|
316
|
+
fail(`${path}.version`, "must be a semver version");
|
|
317
|
+
}
|
|
318
|
+
}
|
|
286
319
|
|
|
287
320
|
// validate.ts
|
|
288
321
|
var import_assembly2 = require("@fabricorg/assembly");
|
|
@@ -315,7 +348,7 @@ function canonicalFragmentGrants(fragments) {
|
|
|
315
348
|
);
|
|
316
349
|
}
|
|
317
350
|
function releaseDigest(document, tokenSet, componentPacks, grants, assemblyDigest) {
|
|
318
|
-
const
|
|
351
|
+
const canonical2 = stableStringify({
|
|
319
352
|
document: {
|
|
320
353
|
formatVersion: document.formatVersion,
|
|
321
354
|
name: document.name,
|
|
@@ -330,6 +363,16 @@ function releaseDigest(document, tokenSet, componentPacks, grants, assemblyDiges
|
|
|
330
363
|
variants: [...document.variants].sort((left, right) => left.id < right.id ? -1 : left.id > right.id ? 1 : 0)
|
|
331
364
|
} : {},
|
|
332
365
|
tokenSet: document.tokenSet,
|
|
366
|
+
// Which brands a document may be promoted against is part of what was
|
|
367
|
+
// authored. Sorted, and omitted when absent so a document declaring
|
|
368
|
+
// none digests exactly as it did before the field existed.
|
|
369
|
+
...document.supportedTokenSets?.length ? {
|
|
370
|
+
supportedTokenSets: [...document.supportedTokenSets].sort((left, right) => {
|
|
371
|
+
if (left.name !== right.name) return left.name < right.name ? -1 : 1;
|
|
372
|
+
if (left.version !== right.version) return left.version < right.version ? -1 : 1;
|
|
373
|
+
return 0;
|
|
374
|
+
})
|
|
375
|
+
} : {},
|
|
333
376
|
componentPacks: [...document.componentPacks].sort((a, b) => {
|
|
334
377
|
const key = (e) => `${e.namespace}/${e.pack}`;
|
|
335
378
|
return key(a) < key(b) ? -1 : key(a) > key(b) ? 1 : 0;
|
|
@@ -355,7 +398,7 @@ function releaseDigest(document, tokenSet, componentPacks, grants, assemblyDiges
|
|
|
355
398
|
},
|
|
356
399
|
assemblyDigest
|
|
357
400
|
});
|
|
358
|
-
return (0, import_node_crypto.createHash)("sha256").update(
|
|
401
|
+
return (0, import_node_crypto.createHash)("sha256").update(canonical2).digest("hex");
|
|
359
402
|
}
|
|
360
403
|
function computeReleaseDigest(document, tokenSet, componentPacks, grants, assemblyDigest) {
|
|
361
404
|
return releaseDigest(document, tokenSet, componentPacks, grants, assemblyDigest);
|
|
@@ -379,19 +422,54 @@ function buildKnownViews(contracts) {
|
|
|
379
422
|
const views = /* @__PURE__ */ new Set();
|
|
380
423
|
for (const contract of contracts) {
|
|
381
424
|
for (const view of contract.views) {
|
|
382
|
-
|
|
383
|
-
views.add(`${contract.namespace}/${name}`);
|
|
425
|
+
views.add(`${contract.namespace}/${shortViewName(view.name, contract.namespace)}`);
|
|
384
426
|
}
|
|
385
427
|
}
|
|
386
428
|
return views;
|
|
387
429
|
}
|
|
388
|
-
function
|
|
430
|
+
function shortViewName(name, namespace) {
|
|
431
|
+
return name.startsWith(`${namespace}/`) ? name.slice(namespace.length + 1) : name;
|
|
432
|
+
}
|
|
433
|
+
function reportAmbiguousGrantedViews(contracts, grantedViews, add) {
|
|
434
|
+
const occurrences = /* @__PURE__ */ new Map();
|
|
435
|
+
for (const contract of contracts) {
|
|
436
|
+
for (const view of contract.views) {
|
|
437
|
+
const reference = `capability://${contract.namespace}/${shortViewName(view.name, contract.namespace)}`;
|
|
438
|
+
if (!grantedViews.has(reference)) continue;
|
|
439
|
+
occurrences.set(reference, [...occurrences.get(reference) ?? [], `${view.name}@${view.version}`]);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
for (const [reference, declarations] of occurrences) {
|
|
443
|
+
if (declarations.length > 1) {
|
|
444
|
+
add(
|
|
445
|
+
"ambiguous_capability_reference",
|
|
446
|
+
`grants.views.${reference}`,
|
|
447
|
+
`"${reference}" is granted but addresses ${declarations.length} views (${declarations.join(", ")}); a versionless reference cannot pick one`
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
function buildKnownActionIntents(contracts, add, grantedIntents) {
|
|
389
453
|
const intents = /* @__PURE__ */ new Set();
|
|
454
|
+
const occurrences = /* @__PURE__ */ new Map();
|
|
390
455
|
for (const contract of contracts) {
|
|
391
456
|
const experience = contract.extensions?.["fabric.experience/v1"];
|
|
392
457
|
for (const intent of experience?.actionIntents ?? []) {
|
|
393
|
-
const
|
|
394
|
-
|
|
458
|
+
const shortName2 = intent.name.startsWith(`${contract.namespace}.`) ? intent.name.slice(contract.namespace.length + 1) : intent.name;
|
|
459
|
+
const reference = `${contract.namespace}/${shortName2}`;
|
|
460
|
+
if (grantedIntents === void 0 || grantedIntents.has(`capability://${reference}`)) {
|
|
461
|
+
occurrences.set(reference, [...occurrences.get(reference) ?? [], intent.name]);
|
|
462
|
+
}
|
|
463
|
+
intents.add(reference);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
for (const [reference, declarations] of occurrences) {
|
|
467
|
+
if (declarations.length > 1) {
|
|
468
|
+
add?.(
|
|
469
|
+
"ambiguous_capability_reference",
|
|
470
|
+
`contracts.actionIntents.${reference}`,
|
|
471
|
+
`"capability://${reference}" is declared ${declarations.length} times (${declarations.join(", ")}), so a route for it cannot be resolved`
|
|
472
|
+
);
|
|
395
473
|
}
|
|
396
474
|
}
|
|
397
475
|
return intents;
|
|
@@ -585,18 +663,13 @@ function validateSduiRelease(input) {
|
|
|
585
663
|
const add = (code, path, message) => {
|
|
586
664
|
findings.push({ code, path, message });
|
|
587
665
|
};
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
"document.tokenSet.name",
|
|
592
|
-
`document references token set "${input.document.tokenSet.name}" but the provided set is "${input.tokenSet.name}"`
|
|
593
|
-
);
|
|
594
|
-
}
|
|
595
|
-
if (input.document.tokenSet.version !== input.tokenSet.version) {
|
|
666
|
+
const promotableTokenSets = [input.document.tokenSet, ...input.document.supportedTokenSets ?? []];
|
|
667
|
+
const promotedAgainst = promotableTokenSets.some((candidate) => candidate.name === input.tokenSet.name && candidate.version === input.tokenSet.version);
|
|
668
|
+
if (!promotedAgainst) {
|
|
596
669
|
add(
|
|
597
670
|
"missing_token_set",
|
|
598
|
-
"document.tokenSet
|
|
599
|
-
`document
|
|
671
|
+
"document.tokenSet",
|
|
672
|
+
`document supports token sets [${promotableTokenSets.map((set) => `${set.name}@${set.version}`).join(", ")}] but the provided set is "${input.tokenSet.name}@${input.tokenSet.version}"`
|
|
600
673
|
);
|
|
601
674
|
}
|
|
602
675
|
const packsByKey = /* @__PURE__ */ new Map();
|
|
@@ -712,9 +785,21 @@ function validateSduiRelease(input) {
|
|
|
712
785
|
}
|
|
713
786
|
const coreComponents = new Set(input.coreComponents ?? []);
|
|
714
787
|
const knownViews = buildKnownViews(contracts);
|
|
715
|
-
const knownIntents = buildKnownActionIntents(contracts);
|
|
716
788
|
const grantedViews = buildGrantedReferences(input.grants.views, "grants.views");
|
|
717
789
|
const grantedIntents = buildGrantedReferences(input.grants.intents, "grants.intents");
|
|
790
|
+
const knownIntents = buildKnownActionIntents(contracts, add, grantedIntents);
|
|
791
|
+
reportAmbiguousGrantedViews(contracts, grantedViews, add);
|
|
792
|
+
const contractNamespaces = /* @__PURE__ */ new Set();
|
|
793
|
+
for (const contract of contracts) {
|
|
794
|
+
if (contractNamespaces.has(contract.namespace)) {
|
|
795
|
+
add(
|
|
796
|
+
"ambiguous_capability_reference",
|
|
797
|
+
`contracts.${contract.namespace}`,
|
|
798
|
+
`contract for "${contract.namespace}" is supplied more than once`
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
contractNamespaces.add(contract.namespace);
|
|
802
|
+
}
|
|
718
803
|
validateIntentActionDeclarations(contracts, add);
|
|
719
804
|
const fragmentGrants = /* @__PURE__ */ new Map();
|
|
720
805
|
for (const [fragmentId, declared] of Object.entries(input.grants.fragments ?? {})) {
|
|
@@ -839,6 +924,221 @@ function collectCoreComponents(fragment) {
|
|
|
839
924
|
}
|
|
840
925
|
return used;
|
|
841
926
|
}
|
|
927
|
+
async function refusalReason(run, expect) {
|
|
928
|
+
try {
|
|
929
|
+
await run();
|
|
930
|
+
return null;
|
|
931
|
+
} catch (error) {
|
|
932
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
933
|
+
return expect.test(message) ? message : null;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
function sduiChannelChecks() {
|
|
937
|
+
return [
|
|
938
|
+
{
|
|
939
|
+
id: "fabric.sdui-channel.verified-release.v1",
|
|
940
|
+
async run(subject) {
|
|
941
|
+
assertSduiReleaseIntegrity(subject.validRelease);
|
|
942
|
+
const result = subject.channel.consume(subject.validRelease);
|
|
943
|
+
if (result.release.releaseDigest !== subject.validRelease.releaseDigest) {
|
|
944
|
+
throw new Error("channel returned a release other than the one it was given");
|
|
945
|
+
}
|
|
946
|
+
return [subject.validRelease.releaseDigest];
|
|
947
|
+
}
|
|
948
|
+
},
|
|
949
|
+
{
|
|
950
|
+
id: "fabric.sdui-channel.tampered-release-denial.v1",
|
|
951
|
+
async run(subject) {
|
|
952
|
+
const tampered = {
|
|
953
|
+
...subject.validRelease,
|
|
954
|
+
document: { ...subject.validRelease.document, name: `${subject.validRelease.document.name}-tampered` }
|
|
955
|
+
};
|
|
956
|
+
const reason = await refusalReason(() => subject.channel.consume(tampered), /digest|integrity/i);
|
|
957
|
+
if (reason === null) {
|
|
958
|
+
throw new Error("channel did not refuse a release whose content no longer matches its digest, citing the digest");
|
|
959
|
+
}
|
|
960
|
+
for (const [label, mutate] of [
|
|
961
|
+
["version", (release) => ({ ...release, document: { ...release.document, version: "9.9.9" } })],
|
|
962
|
+
["grants", (release) => ({ ...release, grants: { ...release.grants, views: [...release.grants.views, "capability://smuggled/view"] } })],
|
|
963
|
+
["assembly", (release) => ({ ...release, assemblyDigest: "f".repeat(64) })]
|
|
964
|
+
]) {
|
|
965
|
+
const edited = mutate(subject.validRelease);
|
|
966
|
+
if (await refusalReason(() => subject.channel.consume(edited), /digest|integrity/i) === null) {
|
|
967
|
+
throw new Error(`channel accepted a release whose ${label} was edited after promotion`);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
return ["tampered document refused", "edited version, grants and assembly refused"];
|
|
971
|
+
}
|
|
972
|
+
},
|
|
973
|
+
{
|
|
974
|
+
id: "fabric.sdui-channel.unpromoted-document-denial.v1",
|
|
975
|
+
async run(subject) {
|
|
976
|
+
const expected = /digest|integrity|release|formatVersion|must be/i;
|
|
977
|
+
const unpromoted = { document: subject.validRelease.document };
|
|
978
|
+
if (await refusalReason(() => subject.channel.consume(unpromoted), expected) === null) {
|
|
979
|
+
throw new Error("channel did not refuse a bare document that was never promoted, citing what was wrong with it");
|
|
980
|
+
}
|
|
981
|
+
const digestless = { ...subject.validRelease, releaseDigest: void 0 };
|
|
982
|
+
if (await refusalReason(() => subject.channel.consume(digestless), expected) === null) {
|
|
983
|
+
throw new Error("channel did not refuse a release carrying no digest, citing what was wrong with it");
|
|
984
|
+
}
|
|
985
|
+
return ["bare document refused", "digestless release refused"];
|
|
986
|
+
}
|
|
987
|
+
},
|
|
988
|
+
{
|
|
989
|
+
id: "fabric.sdui-channel.reports-missing-vocabulary.v1",
|
|
990
|
+
async run(subject) {
|
|
991
|
+
const used = collectCoreComponents(subject.validRelease.document.root);
|
|
992
|
+
for (const variant of subject.validRelease.document.variants ?? []) {
|
|
993
|
+
for (const component of collectCoreComponents(variant.root)) used.add(component);
|
|
994
|
+
}
|
|
995
|
+
if (!used.has(subject.unimplementedCoreComponent)) {
|
|
996
|
+
throw new Error(
|
|
997
|
+
`conformance subject is vacuous: validRelease never uses "${subject.unimplementedCoreComponent}", so the channel is never asked to report it`
|
|
998
|
+
);
|
|
999
|
+
}
|
|
1000
|
+
if (subject.channel.coreComponents.includes(subject.unimplementedCoreComponent)) {
|
|
1001
|
+
throw new Error(
|
|
1002
|
+
`conformance subject is contradictory: the channel implements "${subject.unimplementedCoreComponent}"`
|
|
1003
|
+
);
|
|
1004
|
+
}
|
|
1005
|
+
const result = subject.channel.consume(subject.validRelease);
|
|
1006
|
+
if (result.compatible) {
|
|
1007
|
+
throw new Error("channel reported compatible for a release using a component it does not implement");
|
|
1008
|
+
}
|
|
1009
|
+
if (!result.missingCoreComponents.includes(subject.unimplementedCoreComponent)) {
|
|
1010
|
+
throw new Error("channel reported incompatible without naming the component it cannot render");
|
|
1011
|
+
}
|
|
1012
|
+
return [`missing: ${result.missingCoreComponents.join(", ")}`];
|
|
1013
|
+
}
|
|
1014
|
+
},
|
|
1015
|
+
{
|
|
1016
|
+
id: "fabric.sdui-channel.consumption-is-pure.v1",
|
|
1017
|
+
async run(subject) {
|
|
1018
|
+
const before = JSON.stringify(subject.validRelease);
|
|
1019
|
+
subject.channel.consume(subject.validRelease);
|
|
1020
|
+
subject.channel.consume(subject.validRelease);
|
|
1021
|
+
if (JSON.stringify(subject.validRelease) !== before) {
|
|
1022
|
+
throw new Error("channel mutated the release it consumed");
|
|
1023
|
+
}
|
|
1024
|
+
return ["release unchanged after two consumptions"];
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
];
|
|
1028
|
+
}
|
|
1029
|
+
async function runSduiChannelChecks(subject) {
|
|
1030
|
+
const checks = [];
|
|
1031
|
+
for (const check of sduiChannelChecks()) {
|
|
1032
|
+
try {
|
|
1033
|
+
checks.push({ id: check.id, status: "passed", evidence: await check.run(subject) });
|
|
1034
|
+
} catch (error) {
|
|
1035
|
+
checks.push({
|
|
1036
|
+
id: check.id,
|
|
1037
|
+
status: "failed",
|
|
1038
|
+
evidence: [],
|
|
1039
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
return { passed: checks.every((check) => check.status === "passed"), checks };
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
// references.ts
|
|
1047
|
+
function collectDocumentReferences(document) {
|
|
1048
|
+
const views = /* @__PURE__ */ new Set();
|
|
1049
|
+
const intents = /* @__PURE__ */ new Set();
|
|
1050
|
+
const trees = [document.root, ...(document.variants ?? []).map((variant) => variant.root)];
|
|
1051
|
+
for (const tree of trees) walk(tree, views, intents);
|
|
1052
|
+
return { views, intents };
|
|
1053
|
+
}
|
|
1054
|
+
function walk(fragment, views, intents) {
|
|
1055
|
+
if (fragment.dataRef) views.add(fragment.dataRef);
|
|
1056
|
+
for (const action of Object.values(fragment.actions ?? {})) {
|
|
1057
|
+
intents.add(action.intent);
|
|
1058
|
+
if (action.params) collectValues(action.params, views);
|
|
1059
|
+
}
|
|
1060
|
+
if (fragment.props) collectValues(fragment.props, views);
|
|
1061
|
+
for (const child of fragment.children ?? []) walk(child, views, intents);
|
|
1062
|
+
}
|
|
1063
|
+
function collectValues(props, views) {
|
|
1064
|
+
for (const value of Object.values(props)) collectValue(value, views);
|
|
1065
|
+
}
|
|
1066
|
+
function collectValue(value, views) {
|
|
1067
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value) && "$ref" in value) {
|
|
1068
|
+
views.add(value.$ref);
|
|
1069
|
+
for (const [key, sibling] of Object.entries(value)) {
|
|
1070
|
+
if (key !== "$ref") collectValue(sibling, views);
|
|
1071
|
+
}
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
if (Array.isArray(value)) {
|
|
1075
|
+
for (const item of value) collectValue(item, views);
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
if (value !== null && typeof value === "object") collectValues(value, views);
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
// entitlements.ts
|
|
1082
|
+
function deriveAuthorizationGrants(contracts, held, options = {}) {
|
|
1083
|
+
const undeclared = options.undeclaredRequirements ?? "withhold";
|
|
1084
|
+
const bound = options.document ? collectDocumentReferences(options.document) : void 0;
|
|
1085
|
+
const permissions = new Set(held.permissions ?? []);
|
|
1086
|
+
const entitlements = new Set(held.entitlements ?? []);
|
|
1087
|
+
const featureFlags = new Set(held.featureFlags ?? []);
|
|
1088
|
+
const views = [];
|
|
1089
|
+
const intents = [];
|
|
1090
|
+
const withheld = [];
|
|
1091
|
+
for (const contract of contracts) {
|
|
1092
|
+
if (undeclared === "withhold" && !declaresAnyRequirement(contract)) {
|
|
1093
|
+
withheld.push({ namespace: contract.namespace, missing: { kind: "permission", name: "(none declared)" } });
|
|
1094
|
+
continue;
|
|
1095
|
+
}
|
|
1096
|
+
const missing = firstMissingRequirement(contract, { permissions, entitlements, featureFlags });
|
|
1097
|
+
if (missing) {
|
|
1098
|
+
withheld.push({ namespace: contract.namespace, missing });
|
|
1099
|
+
continue;
|
|
1100
|
+
}
|
|
1101
|
+
for (const view of contract.views) {
|
|
1102
|
+
const reference = `capability://${contract.namespace}/${shortName(view.name, contract.namespace, "/")}`;
|
|
1103
|
+
if (!bound || bound.views.has(reference)) views.push(reference);
|
|
1104
|
+
}
|
|
1105
|
+
for (const intent of actionIntentsOf(contract)) {
|
|
1106
|
+
const reference = `capability://${contract.namespace}/${shortName(intent.name, contract.namespace, ".")}`;
|
|
1107
|
+
if (!bound || bound.intents.has(reference)) intents.push(reference);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
return {
|
|
1111
|
+
grants: { views: [...new Set(views)].sort(), intents: [...new Set(intents)].sort() },
|
|
1112
|
+
withheld: withheld.sort((left, right) => left.namespace < right.namespace ? -1 : left.namespace > right.namespace ? 1 : 0)
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
function declaresAnyRequirement(contract) {
|
|
1116
|
+
const requirements = contract.requirements;
|
|
1117
|
+
return Boolean(
|
|
1118
|
+
requirements && ((requirements.permissions?.length ?? 0) > 0 || (requirements.entitlements?.length ?? 0) > 0 || (requirements.featureFlags?.length ?? 0) > 0)
|
|
1119
|
+
);
|
|
1120
|
+
}
|
|
1121
|
+
function firstMissingRequirement(contract, held) {
|
|
1122
|
+
const requirements = contract.requirements;
|
|
1123
|
+
for (const name of requirements?.permissions ?? []) {
|
|
1124
|
+
if (!held.permissions.has(name)) return { kind: "permission", name };
|
|
1125
|
+
}
|
|
1126
|
+
for (const name of requirements?.entitlements ?? []) {
|
|
1127
|
+
if (!held.entitlements.has(name)) return { kind: "entitlement", name };
|
|
1128
|
+
}
|
|
1129
|
+
for (const name of requirements?.featureFlags ?? []) {
|
|
1130
|
+
if (!held.featureFlags.has(name)) return { kind: "featureFlag", name };
|
|
1131
|
+
}
|
|
1132
|
+
return void 0;
|
|
1133
|
+
}
|
|
1134
|
+
function actionIntentsOf(contract) {
|
|
1135
|
+
const experience = contract.extensions?.["fabric.experience/v1"];
|
|
1136
|
+
return experience?.actionIntents ?? [];
|
|
1137
|
+
}
|
|
1138
|
+
function shortName(name, namespace, separator) {
|
|
1139
|
+
const prefix = `${namespace}${separator}`;
|
|
1140
|
+
return name.startsWith(prefix) ? name.slice(prefix.length) : name;
|
|
1141
|
+
}
|
|
842
1142
|
|
|
843
1143
|
// schemas.ts
|
|
844
1144
|
var SDUI_DOCUMENT_JSON_SCHEMA = {
|
|
@@ -870,24 +1170,280 @@ var SDUI_RELEASE_JSON_SCHEMA = {
|
|
|
870
1170
|
grants: { type: "object", required: ["views", "intents"] }
|
|
871
1171
|
}
|
|
872
1172
|
};
|
|
1173
|
+
|
|
1174
|
+
// contracts.ts
|
|
1175
|
+
var SDUI_RELEASE_V3_FORMAT_VERSION = 3;
|
|
1176
|
+
var RELEASE_CHANNEL_POINTER_FORMAT_VERSION = 1;
|
|
1177
|
+
var encoder = new TextEncoder();
|
|
1178
|
+
var decoder = new TextDecoder("utf-8", { fatal: true });
|
|
1179
|
+
var HEX_DIGEST = /^[a-f0-9]{64}$/;
|
|
1180
|
+
function canonicalizeJcs(value) {
|
|
1181
|
+
return canonical(value, 0);
|
|
1182
|
+
}
|
|
1183
|
+
function canonical(value, depth) {
|
|
1184
|
+
if (depth > 64) throw new Error("Canonical JSON exceeds the maximum depth of 64.");
|
|
1185
|
+
if (value === null) return "null";
|
|
1186
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
1187
|
+
if (typeof value === "number") {
|
|
1188
|
+
if (!Number.isFinite(value)) throw new Error("Canonical JSON numbers must be finite.");
|
|
1189
|
+
return Object.is(value, -0) ? "0" : JSON.stringify(value);
|
|
1190
|
+
}
|
|
1191
|
+
if (typeof value === "string") {
|
|
1192
|
+
assertValidUnicode(value);
|
|
1193
|
+
return JSON.stringify(value);
|
|
1194
|
+
}
|
|
1195
|
+
if (Array.isArray(value)) return `[${value.map((entry) => canonical(entry, depth + 1)).join(",")}]`;
|
|
1196
|
+
if (typeof value !== "object") throw new Error(`Canonical JSON cannot encode ${typeof value}.`);
|
|
1197
|
+
const record = value;
|
|
1198
|
+
const keys = Object.keys(record).sort();
|
|
1199
|
+
return `{${keys.map((key) => {
|
|
1200
|
+
assertValidUnicode(key);
|
|
1201
|
+
return `${JSON.stringify(key)}:${canonical(record[key], depth + 1)}`;
|
|
1202
|
+
}).join(",")}}`;
|
|
1203
|
+
}
|
|
1204
|
+
function assertValidUnicode(value) {
|
|
1205
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
1206
|
+
const code = value.charCodeAt(index);
|
|
1207
|
+
if (code >= 55296 && code <= 56319) {
|
|
1208
|
+
const next = value.charCodeAt(index + 1);
|
|
1209
|
+
if (!(next >= 56320 && next <= 57343)) throw new Error("Canonical JSON contains an unpaired Unicode surrogate.");
|
|
1210
|
+
index += 1;
|
|
1211
|
+
} else if (code >= 56320 && code <= 57343) {
|
|
1212
|
+
throw new Error("Canonical JSON contains an unpaired Unicode surrogate.");
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
async function verifySignedExperienceArtifact(input) {
|
|
1217
|
+
const maxBytes = input.maxBytes ?? 1048576;
|
|
1218
|
+
if (encoder.encode(input.compactJws).byteLength > maxBytes) {
|
|
1219
|
+
throw new Error(`Signed experience artifact exceeds the ${maxBytes}-byte size limit.`);
|
|
1220
|
+
}
|
|
1221
|
+
const parts = input.compactJws.split(".");
|
|
1222
|
+
if (parts.length !== 3 || parts.some((part) => part.length === 0)) throw new Error("Signed experience artifact must be a compact JWS.");
|
|
1223
|
+
const header = parseSegment(parts[0], "protected header");
|
|
1224
|
+
if (header.alg !== "ES256") throw new Error("Signed experience artifacts must use ES256.");
|
|
1225
|
+
if (header.typ !== input.expectedType) throw new Error(`Signed experience artifact type must be ${input.expectedType}.`);
|
|
1226
|
+
if (typeof header.kid !== "string" || !header.kid) throw new Error("Signed experience artifact requires a key id.");
|
|
1227
|
+
const payloadBytes = decodeBase64Url(parts[1]);
|
|
1228
|
+
const payload = parseJsonBytes(payloadBytes, "payload");
|
|
1229
|
+
if (typeof payload.issuer !== "string" || !payload.issuer) throw new Error("Signed experience artifact requires an issuer.");
|
|
1230
|
+
const key = await input.trust.resolveKey({ keyId: header.kid, issuer: payload.issuer, algorithm: "ES256" });
|
|
1231
|
+
if (!key || key.algorithm !== "ES256") throw new Error("Signed experience artifact has no trusted key.");
|
|
1232
|
+
const signature = decodeBase64Url(parts[2]);
|
|
1233
|
+
if (signature.byteLength !== 64) throw new Error("ES256 signatures must be 64-byte JOSE signatures.");
|
|
1234
|
+
const verified = await input.crypto.verify({
|
|
1235
|
+
algorithm: "ES256",
|
|
1236
|
+
key: key.key,
|
|
1237
|
+
signingInput: encoder.encode(`${parts[0]}.${parts[1]}`),
|
|
1238
|
+
signature
|
|
1239
|
+
});
|
|
1240
|
+
if (!verified) throw new Error("Signed experience artifact signature is invalid.");
|
|
1241
|
+
return payload;
|
|
1242
|
+
}
|
|
1243
|
+
async function verifySignedExperienceRelease(input) {
|
|
1244
|
+
const payload = await verifySignedExperienceArtifact({
|
|
1245
|
+
compactJws: input.compactJws,
|
|
1246
|
+
expectedType: "fabric-experience-release+jws",
|
|
1247
|
+
crypto: input.crypto,
|
|
1248
|
+
trust: input.trust,
|
|
1249
|
+
...input.maxBytes === void 0 ? {} : { maxBytes: input.maxBytes }
|
|
1250
|
+
});
|
|
1251
|
+
assertSduiReleaseV3(payload);
|
|
1252
|
+
if (payload.issuer !== input.expected.issuer || payload.application !== input.expected.application || payload.assemblyDigest !== input.expected.assemblyDigest) {
|
|
1253
|
+
throw new Error("Signed experience release does not match the expected issuer, application, and assembly.");
|
|
1254
|
+
}
|
|
1255
|
+
const { releaseDigest: releaseDigest2, ...body } = payload;
|
|
1256
|
+
const digest = await input.crypto.digestSha256(encoder.encode(canonicalizeJcs(body)));
|
|
1257
|
+
const expectedDigest = [...digest].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
1258
|
+
if (releaseDigest2 !== expectedDigest) throw new Error("Signed experience release digest does not match its canonical content.");
|
|
1259
|
+
return payload;
|
|
1260
|
+
}
|
|
1261
|
+
function assertSduiReleaseV3(value) {
|
|
1262
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("SDUI Release v3 must be an object.");
|
|
1263
|
+
const release = value;
|
|
1264
|
+
if (release.formatVersion !== SDUI_RELEASE_V3_FORMAT_VERSION || release.kind !== "experience-release") {
|
|
1265
|
+
throw new Error("SDUI Release v3 has an unsupported format.");
|
|
1266
|
+
}
|
|
1267
|
+
for (const field of ["issuer", "application", "releaseDigest", "assemblyDigest"]) {
|
|
1268
|
+
if (typeof release[field] !== "string" || !release[field]) throw new Error(`SDUI Release v3 ${field} is required.`);
|
|
1269
|
+
}
|
|
1270
|
+
if (!HEX_DIGEST.test(release.releaseDigest) || !HEX_DIGEST.test(release.assemblyDigest)) {
|
|
1271
|
+
throw new Error("SDUI Release v3 digests must be lowercase SHA-256 values.");
|
|
1272
|
+
}
|
|
1273
|
+
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)) {
|
|
1274
|
+
throw new Error("SDUI Release v3 is structurally incomplete.");
|
|
1275
|
+
}
|
|
1276
|
+
const routes = release.routes;
|
|
1277
|
+
assertUniqueRecords(routes.views, "view route", (route) => {
|
|
1278
|
+
const record = assertRecord(route, "view route");
|
|
1279
|
+
assertCapabilityReference(record.reference, "view route");
|
|
1280
|
+
assertNonEmptyString(record.name, "view route name");
|
|
1281
|
+
assertNonEmptyString(record.version, "view route version");
|
|
1282
|
+
if (record.parameterSchemaDigest !== void 0) {
|
|
1283
|
+
assertHexDigest(record.parameterSchemaDigest, "view route parameterSchemaDigest");
|
|
1284
|
+
}
|
|
1285
|
+
return record.reference;
|
|
1286
|
+
});
|
|
1287
|
+
assertUniqueRecords(routes.intents, "intent route", (route) => {
|
|
1288
|
+
const record = assertRecord(route, "intent route");
|
|
1289
|
+
assertCapabilityReference(record.reference, "intent route");
|
|
1290
|
+
assertNonEmptyString(record.actionId, "intent route actionId");
|
|
1291
|
+
if (!Number.isSafeInteger(record.version) || record.version < 1) {
|
|
1292
|
+
throw new Error("SDUI Release v3 intent route version must be a positive integer.");
|
|
1293
|
+
}
|
|
1294
|
+
if (record.parameterSchemaDigest !== void 0) {
|
|
1295
|
+
assertHexDigest(record.parameterSchemaDigest, "intent route parameterSchemaDigest");
|
|
1296
|
+
}
|
|
1297
|
+
return record.reference;
|
|
1298
|
+
});
|
|
1299
|
+
assertUniqueRecords(release.effectiveGrants, "effective grant", (grant) => {
|
|
1300
|
+
const record = assertRecord(grant, "effective grant");
|
|
1301
|
+
assertNonEmptyString(record.treeId, "effective grant treeId");
|
|
1302
|
+
assertNonEmptyString(record.fragmentId, "effective grant fragmentId");
|
|
1303
|
+
if (!Array.isArray(record.views) || !record.views.every((item) => typeof item === "string") || !Array.isArray(record.intents) || !record.intents.every((item) => typeof item === "string")) {
|
|
1304
|
+
throw new Error("SDUI Release v3 effective grant routes must be string arrays.");
|
|
1305
|
+
}
|
|
1306
|
+
return `${record.treeId}\0${record.fragmentId}`;
|
|
1307
|
+
});
|
|
1308
|
+
}
|
|
1309
|
+
function assertRecord(value, label) {
|
|
1310
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`SDUI Release v3 ${label} must be an object.`);
|
|
1311
|
+
return value;
|
|
1312
|
+
}
|
|
1313
|
+
function assertNonEmptyString(value, label) {
|
|
1314
|
+
if (typeof value !== "string" || !value) throw new Error(`SDUI Release v3 ${label} is required.`);
|
|
1315
|
+
}
|
|
1316
|
+
function assertHexDigest(value, label) {
|
|
1317
|
+
if (typeof value !== "string" || !HEX_DIGEST.test(value)) throw new Error(`SDUI Release v3 ${label} must be a lowercase SHA-256 value.`);
|
|
1318
|
+
}
|
|
1319
|
+
function assertCapabilityReference(value, label) {
|
|
1320
|
+
if (typeof value !== "string" || !value.startsWith("capability://")) throw new Error(`SDUI Release v3 ${label} reference is invalid.`);
|
|
1321
|
+
}
|
|
1322
|
+
function assertUniqueRecords(values, label, identity) {
|
|
1323
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1324
|
+
for (const value of values) {
|
|
1325
|
+
const key = identity(value);
|
|
1326
|
+
if (seen.has(key)) throw new Error(`SDUI Release v3 has a duplicate ${label}.`);
|
|
1327
|
+
seen.add(key);
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
async function activateReleaseChannelPointer(input) {
|
|
1331
|
+
const payload = await verifySignedExperienceArtifact({
|
|
1332
|
+
compactJws: input.compactJws,
|
|
1333
|
+
expectedType: "fabric-release-channel-pointer+jws",
|
|
1334
|
+
crypto: input.crypto,
|
|
1335
|
+
trust: input.trust
|
|
1336
|
+
});
|
|
1337
|
+
assertReleaseChannelPointer(payload);
|
|
1338
|
+
if (payload.issuer !== input.expected.issuer || payload.application !== input.expected.application || payload.channel !== input.expected.channel) {
|
|
1339
|
+
throw new Error("Release channel pointer does not match the expected issuer, application, and channel.");
|
|
1340
|
+
}
|
|
1341
|
+
const now = (input.now ?? /* @__PURE__ */ new Date()).getTime();
|
|
1342
|
+
if (payload.notBefore !== void 0 && now < parseTimestamp(payload.notBefore, "notBefore")) {
|
|
1343
|
+
throw new Error("Release channel pointer is not active yet.");
|
|
1344
|
+
}
|
|
1345
|
+
if (payload.expiresAt !== void 0 && now >= parseTimestamp(payload.expiresAt, "expiresAt")) {
|
|
1346
|
+
throw new Error("Release channel pointer has expired.");
|
|
1347
|
+
}
|
|
1348
|
+
const generationKey = `${payload.issuer}:${payload.application}:${payload.channel}`;
|
|
1349
|
+
const pointerDigestBytes = await input.crypto.digestSha256(encoder.encode(canonicalizeJcs(payload)));
|
|
1350
|
+
const pointerDigest = [...pointerDigestBytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
1351
|
+
const current = await input.generations.get(generationKey);
|
|
1352
|
+
if (current !== void 0 && payload.generation < current.generation) {
|
|
1353
|
+
throw new Error("Release channel pointer is older than the accepted generation.");
|
|
1354
|
+
}
|
|
1355
|
+
if (current?.generation === payload.generation && current.pointerDigest !== pointerDigest) {
|
|
1356
|
+
throw new Error("Release channel generation is already bound to a different signed pointer.");
|
|
1357
|
+
}
|
|
1358
|
+
if (current?.generation !== payload.generation) {
|
|
1359
|
+
const accepted = await input.generations.acceptIfHigher(generationKey, payload.generation, pointerDigest);
|
|
1360
|
+
if (!accepted) {
|
|
1361
|
+
const raced = await input.generations.get(generationKey);
|
|
1362
|
+
if (raced?.generation !== payload.generation || raced.pointerDigest !== pointerDigest) {
|
|
1363
|
+
throw new Error("Release channel pointer lost an activation race to a different generation.");
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
return payload;
|
|
1368
|
+
}
|
|
1369
|
+
function assertReleaseChannelPointer(value) {
|
|
1370
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Release channel pointer must be an object.");
|
|
1371
|
+
const pointer = value;
|
|
1372
|
+
if (pointer.formatVersion !== RELEASE_CHANNEL_POINTER_FORMAT_VERSION || pointer.kind !== "release-channel-pointer") {
|
|
1373
|
+
throw new Error("Release channel pointer has an unsupported format.");
|
|
1374
|
+
}
|
|
1375
|
+
for (const field of ["issuer", "application", "channel", "issuedAt"]) {
|
|
1376
|
+
if (typeof pointer[field] !== "string" || !pointer[field]) throw new Error(`Release channel pointer ${field} is required.`);
|
|
1377
|
+
}
|
|
1378
|
+
if (!Number.isSafeInteger(pointer.generation) || pointer.generation < 0) throw new Error("Release channel pointer generation must be a non-negative safe integer.");
|
|
1379
|
+
if (!HEX_DIGEST.test(String(pointer.activeReleaseDigest)) || !HEX_DIGEST.test(String(pointer.assemblyDigest))) {
|
|
1380
|
+
throw new Error("Release channel pointer digests must be lowercase SHA-256 values.");
|
|
1381
|
+
}
|
|
1382
|
+
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)) {
|
|
1383
|
+
throw new Error("Release channel pointer fallbackReleaseDigests must contain unique lowercase SHA-256 values.");
|
|
1384
|
+
}
|
|
1385
|
+
parseTimestamp(pointer.issuedAt, "issuedAt");
|
|
1386
|
+
}
|
|
1387
|
+
function parseSegment(segment, label) {
|
|
1388
|
+
return parseJsonBytes(decodeBase64Url(segment), label);
|
|
1389
|
+
}
|
|
1390
|
+
function parseJsonBytes(bytes, label) {
|
|
1391
|
+
let value;
|
|
1392
|
+
try {
|
|
1393
|
+
value = JSON.parse(decoder.decode(bytes));
|
|
1394
|
+
} catch {
|
|
1395
|
+
throw new Error(`Signed experience artifact ${label} is not valid UTF-8 JSON.`);
|
|
1396
|
+
}
|
|
1397
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`Signed experience artifact ${label} must be an object.`);
|
|
1398
|
+
return value;
|
|
1399
|
+
}
|
|
1400
|
+
function decodeBase64Url(value) {
|
|
1401
|
+
if (!/^[A-Za-z0-9_-]+$/.test(value)) throw new Error("Signed experience artifact contains invalid base64url.");
|
|
1402
|
+
const padded = value.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(value.length / 4) * 4, "=");
|
|
1403
|
+
let binary;
|
|
1404
|
+
try {
|
|
1405
|
+
binary = atob(padded);
|
|
1406
|
+
} catch {
|
|
1407
|
+
throw new Error("Signed experience artifact contains invalid base64url.");
|
|
1408
|
+
}
|
|
1409
|
+
return Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
|
1410
|
+
}
|
|
1411
|
+
function parseTimestamp(value, field) {
|
|
1412
|
+
if (!/(?:Z|[+-]\d{2}:\d{2})$/.test(value)) throw new Error(`Release channel pointer ${field} must be RFC 3339 with an explicit offset.`);
|
|
1413
|
+
const parsed = Date.parse(value);
|
|
1414
|
+
if (Number.isNaN(parsed)) throw new Error(`Release channel pointer ${field} is invalid.`);
|
|
1415
|
+
return parsed;
|
|
1416
|
+
}
|
|
873
1417
|
// Annotate the CommonJS export names for ESM import in node:
|
|
874
1418
|
0 && (module.exports = {
|
|
1419
|
+
RELEASE_CHANNEL_POINTER_FORMAT_VERSION,
|
|
875
1420
|
SDUI_COMPONENT_PACK_FORMAT_VERSION,
|
|
876
1421
|
SDUI_DOCUMENT_FORMAT_VERSION,
|
|
877
1422
|
SDUI_DOCUMENT_JSON_SCHEMA,
|
|
878
1423
|
SDUI_RELEASE_FORMAT_VERSION,
|
|
879
1424
|
SDUI_RELEASE_JSON_SCHEMA,
|
|
1425
|
+
SDUI_RELEASE_V3_FORMAT_VERSION,
|
|
880
1426
|
SDUI_TOKEN_SET_FORMAT_VERSION,
|
|
1427
|
+
activateReleaseChannelPointer,
|
|
1428
|
+
assertReleaseChannelPointer,
|
|
881
1429
|
assertSduiAuthorizationGrants,
|
|
882
1430
|
assertSduiComponentPack,
|
|
883
1431
|
assertSduiDocument,
|
|
884
1432
|
assertSduiFragment,
|
|
885
1433
|
assertSduiRelease,
|
|
886
1434
|
assertSduiReleaseIntegrity,
|
|
1435
|
+
assertSduiReleaseV3,
|
|
887
1436
|
assertSduiReleaseValid,
|
|
888
1437
|
assertSduiTokenSet,
|
|
1438
|
+
canonicalizeJcs,
|
|
889
1439
|
computeReleaseDigest,
|
|
890
1440
|
createSduiChannel,
|
|
891
|
-
|
|
1441
|
+
deriveAuthorizationGrants,
|
|
1442
|
+
parseCapabilityRef,
|
|
1443
|
+
runSduiChannelChecks,
|
|
1444
|
+
sduiChannelChecks,
|
|
1445
|
+
validateSduiRelease,
|
|
1446
|
+
verifySignedExperienceArtifact,
|
|
1447
|
+
verifySignedExperienceRelease
|
|
892
1448
|
});
|
|
893
1449
|
//# sourceMappingURL=index.cjs.map
|