@fabricorg/sdui-release 0.3.0 → 0.6.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 +209 -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 +603 -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 +355 -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 +32 -8
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,56 @@ 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"];
|
|
457
|
+
const declaredActions = new Set(contract.actions.map((action) => action.actionId));
|
|
392
458
|
for (const intent of experience?.actionIntents ?? []) {
|
|
393
|
-
|
|
394
|
-
|
|
459
|
+
if (!declaredActions.has(intent.actionId)) continue;
|
|
460
|
+
const shortName2 = intent.name.startsWith(`${contract.namespace}.`) ? intent.name.slice(contract.namespace.length + 1) : intent.name;
|
|
461
|
+
const reference = `${contract.namespace}/${shortName2}`;
|
|
462
|
+
if (grantedIntents === void 0 || grantedIntents.has(`capability://${reference}`)) {
|
|
463
|
+
occurrences.set(reference, [...occurrences.get(reference) ?? [], intent.name]);
|
|
464
|
+
}
|
|
465
|
+
intents.add(reference);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
for (const [reference, declarations] of occurrences) {
|
|
469
|
+
if (declarations.length > 1) {
|
|
470
|
+
add?.(
|
|
471
|
+
"ambiguous_capability_reference",
|
|
472
|
+
`contracts.actionIntents.${reference}`,
|
|
473
|
+
`"capability://${reference}" is declared ${declarations.length} times (${declarations.join(", ")}), so a route for it cannot be resolved`
|
|
474
|
+
);
|
|
395
475
|
}
|
|
396
476
|
}
|
|
397
477
|
return intents;
|
|
@@ -585,18 +665,13 @@ function validateSduiRelease(input) {
|
|
|
585
665
|
const add = (code, path, message) => {
|
|
586
666
|
findings.push({ code, path, message });
|
|
587
667
|
};
|
|
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) {
|
|
668
|
+
const promotableTokenSets = [input.document.tokenSet, ...input.document.supportedTokenSets ?? []];
|
|
669
|
+
const promotedAgainst = promotableTokenSets.some((candidate) => candidate.name === input.tokenSet.name && candidate.version === input.tokenSet.version);
|
|
670
|
+
if (!promotedAgainst) {
|
|
596
671
|
add(
|
|
597
672
|
"missing_token_set",
|
|
598
|
-
"document.tokenSet
|
|
599
|
-
`document
|
|
673
|
+
"document.tokenSet",
|
|
674
|
+
`document supports token sets [${promotableTokenSets.map((set) => `${set.name}@${set.version}`).join(", ")}] but the provided set is "${input.tokenSet.name}@${input.tokenSet.version}"`
|
|
600
675
|
);
|
|
601
676
|
}
|
|
602
677
|
const packsByKey = /* @__PURE__ */ new Map();
|
|
@@ -712,9 +787,43 @@ function validateSduiRelease(input) {
|
|
|
712
787
|
}
|
|
713
788
|
const coreComponents = new Set(input.coreComponents ?? []);
|
|
714
789
|
const knownViews = buildKnownViews(contracts);
|
|
715
|
-
const knownIntents = buildKnownActionIntents(contracts);
|
|
716
790
|
const grantedViews = buildGrantedReferences(input.grants.views, "grants.views");
|
|
717
791
|
const grantedIntents = buildGrantedReferences(input.grants.intents, "grants.intents");
|
|
792
|
+
const contractNamespaces = /* @__PURE__ */ new Set();
|
|
793
|
+
const distinctContracts = contracts.filter((contract) => {
|
|
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
|
+
return false;
|
|
801
|
+
}
|
|
802
|
+
contractNamespaces.add(contract.namespace);
|
|
803
|
+
return true;
|
|
804
|
+
});
|
|
805
|
+
const knownIntents = buildKnownActionIntents(distinctContracts, add, grantedIntents);
|
|
806
|
+
reportAmbiguousGrantedViews(distinctContracts, grantedViews, add);
|
|
807
|
+
for (const reference of grantedViews) {
|
|
808
|
+
const parsed = parseCapabilityRef(reference);
|
|
809
|
+
if (parsed && !knownViews.has(`${parsed.namespace}/${parsed.name}`)) {
|
|
810
|
+
add(
|
|
811
|
+
"unauthorized_data_ref",
|
|
812
|
+
`grants.views.${reference}`,
|
|
813
|
+
`release grants "${reference}", which no supplied capability publishes; promotion would fail to route it`
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
for (const reference of grantedIntents) {
|
|
818
|
+
const parsed = parseCapabilityRef(reference);
|
|
819
|
+
if (parsed && !knownIntents.has(`${parsed.namespace}/${parsed.name}`)) {
|
|
820
|
+
add(
|
|
821
|
+
"mutation_bypass",
|
|
822
|
+
`grants.intents.${reference}`,
|
|
823
|
+
`release grants "${reference}", which no supplied capability declares as an intent; promotion would fail to route it`
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
718
827
|
validateIntentActionDeclarations(contracts, add);
|
|
719
828
|
const fragmentGrants = /* @__PURE__ */ new Map();
|
|
720
829
|
for (const [fragmentId, declared] of Object.entries(input.grants.fragments ?? {})) {
|
|
@@ -839,6 +948,221 @@ function collectCoreComponents(fragment) {
|
|
|
839
948
|
}
|
|
840
949
|
return used;
|
|
841
950
|
}
|
|
951
|
+
async function refusalReason(run, expect) {
|
|
952
|
+
try {
|
|
953
|
+
await run();
|
|
954
|
+
return null;
|
|
955
|
+
} catch (error) {
|
|
956
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
957
|
+
return expect.test(message) ? message : null;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
function sduiChannelChecks() {
|
|
961
|
+
return [
|
|
962
|
+
{
|
|
963
|
+
id: "fabric.sdui-channel.verified-release.v1",
|
|
964
|
+
async run(subject) {
|
|
965
|
+
assertSduiReleaseIntegrity(subject.validRelease);
|
|
966
|
+
const result = subject.channel.consume(subject.validRelease);
|
|
967
|
+
if (result.release.releaseDigest !== subject.validRelease.releaseDigest) {
|
|
968
|
+
throw new Error("channel returned a release other than the one it was given");
|
|
969
|
+
}
|
|
970
|
+
return [subject.validRelease.releaseDigest];
|
|
971
|
+
}
|
|
972
|
+
},
|
|
973
|
+
{
|
|
974
|
+
id: "fabric.sdui-channel.tampered-release-denial.v1",
|
|
975
|
+
async run(subject) {
|
|
976
|
+
const tampered = {
|
|
977
|
+
...subject.validRelease,
|
|
978
|
+
document: { ...subject.validRelease.document, name: `${subject.validRelease.document.name}-tampered` }
|
|
979
|
+
};
|
|
980
|
+
const reason = await refusalReason(() => subject.channel.consume(tampered), /digest|integrity/i);
|
|
981
|
+
if (reason === null) {
|
|
982
|
+
throw new Error("channel did not refuse a release whose content no longer matches its digest, citing the digest");
|
|
983
|
+
}
|
|
984
|
+
for (const [label, mutate] of [
|
|
985
|
+
["version", (release) => ({ ...release, document: { ...release.document, version: "9.9.9" } })],
|
|
986
|
+
["grants", (release) => ({ ...release, grants: { ...release.grants, views: [...release.grants.views, "capability://smuggled/view"] } })],
|
|
987
|
+
["assembly", (release) => ({ ...release, assemblyDigest: "f".repeat(64) })]
|
|
988
|
+
]) {
|
|
989
|
+
const edited = mutate(subject.validRelease);
|
|
990
|
+
if (await refusalReason(() => subject.channel.consume(edited), /digest|integrity/i) === null) {
|
|
991
|
+
throw new Error(`channel accepted a release whose ${label} was edited after promotion`);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
return ["tampered document refused", "edited version, grants and assembly refused"];
|
|
995
|
+
}
|
|
996
|
+
},
|
|
997
|
+
{
|
|
998
|
+
id: "fabric.sdui-channel.unpromoted-document-denial.v1",
|
|
999
|
+
async run(subject) {
|
|
1000
|
+
const expected = /digest|integrity|release|formatVersion|must be/i;
|
|
1001
|
+
const unpromoted = { document: subject.validRelease.document };
|
|
1002
|
+
if (await refusalReason(() => subject.channel.consume(unpromoted), expected) === null) {
|
|
1003
|
+
throw new Error("channel did not refuse a bare document that was never promoted, citing what was wrong with it");
|
|
1004
|
+
}
|
|
1005
|
+
const digestless = { ...subject.validRelease, releaseDigest: void 0 };
|
|
1006
|
+
if (await refusalReason(() => subject.channel.consume(digestless), expected) === null) {
|
|
1007
|
+
throw new Error("channel did not refuse a release carrying no digest, citing what was wrong with it");
|
|
1008
|
+
}
|
|
1009
|
+
return ["bare document refused", "digestless release refused"];
|
|
1010
|
+
}
|
|
1011
|
+
},
|
|
1012
|
+
{
|
|
1013
|
+
id: "fabric.sdui-channel.reports-missing-vocabulary.v1",
|
|
1014
|
+
async run(subject) {
|
|
1015
|
+
const used = collectCoreComponents(subject.validRelease.document.root);
|
|
1016
|
+
for (const variant of subject.validRelease.document.variants ?? []) {
|
|
1017
|
+
for (const component of collectCoreComponents(variant.root)) used.add(component);
|
|
1018
|
+
}
|
|
1019
|
+
if (!used.has(subject.unimplementedCoreComponent)) {
|
|
1020
|
+
throw new Error(
|
|
1021
|
+
`conformance subject is vacuous: validRelease never uses "${subject.unimplementedCoreComponent}", so the channel is never asked to report it`
|
|
1022
|
+
);
|
|
1023
|
+
}
|
|
1024
|
+
if (subject.channel.coreComponents.includes(subject.unimplementedCoreComponent)) {
|
|
1025
|
+
throw new Error(
|
|
1026
|
+
`conformance subject is contradictory: the channel implements "${subject.unimplementedCoreComponent}"`
|
|
1027
|
+
);
|
|
1028
|
+
}
|
|
1029
|
+
const result = subject.channel.consume(subject.validRelease);
|
|
1030
|
+
if (result.compatible) {
|
|
1031
|
+
throw new Error("channel reported compatible for a release using a component it does not implement");
|
|
1032
|
+
}
|
|
1033
|
+
if (!result.missingCoreComponents.includes(subject.unimplementedCoreComponent)) {
|
|
1034
|
+
throw new Error("channel reported incompatible without naming the component it cannot render");
|
|
1035
|
+
}
|
|
1036
|
+
return [`missing: ${result.missingCoreComponents.join(", ")}`];
|
|
1037
|
+
}
|
|
1038
|
+
},
|
|
1039
|
+
{
|
|
1040
|
+
id: "fabric.sdui-channel.consumption-is-pure.v1",
|
|
1041
|
+
async run(subject) {
|
|
1042
|
+
const before = JSON.stringify(subject.validRelease);
|
|
1043
|
+
subject.channel.consume(subject.validRelease);
|
|
1044
|
+
subject.channel.consume(subject.validRelease);
|
|
1045
|
+
if (JSON.stringify(subject.validRelease) !== before) {
|
|
1046
|
+
throw new Error("channel mutated the release it consumed");
|
|
1047
|
+
}
|
|
1048
|
+
return ["release unchanged after two consumptions"];
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
];
|
|
1052
|
+
}
|
|
1053
|
+
async function runSduiChannelChecks(subject) {
|
|
1054
|
+
const checks = [];
|
|
1055
|
+
for (const check of sduiChannelChecks()) {
|
|
1056
|
+
try {
|
|
1057
|
+
checks.push({ id: check.id, status: "passed", evidence: await check.run(subject) });
|
|
1058
|
+
} catch (error) {
|
|
1059
|
+
checks.push({
|
|
1060
|
+
id: check.id,
|
|
1061
|
+
status: "failed",
|
|
1062
|
+
evidence: [],
|
|
1063
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
return { passed: checks.every((check) => check.status === "passed"), checks };
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
// references.ts
|
|
1071
|
+
function collectDocumentReferences(document) {
|
|
1072
|
+
const views = /* @__PURE__ */ new Set();
|
|
1073
|
+
const intents = /* @__PURE__ */ new Set();
|
|
1074
|
+
const trees = [document.root, ...(document.variants ?? []).map((variant) => variant.root)];
|
|
1075
|
+
for (const tree of trees) walk(tree, views, intents);
|
|
1076
|
+
return { views, intents };
|
|
1077
|
+
}
|
|
1078
|
+
function walk(fragment, views, intents) {
|
|
1079
|
+
if (fragment.dataRef) views.add(fragment.dataRef);
|
|
1080
|
+
for (const action of Object.values(fragment.actions ?? {})) {
|
|
1081
|
+
intents.add(action.intent);
|
|
1082
|
+
if (action.params) collectValues(action.params, views);
|
|
1083
|
+
}
|
|
1084
|
+
if (fragment.props) collectValues(fragment.props, views);
|
|
1085
|
+
for (const child of fragment.children ?? []) walk(child, views, intents);
|
|
1086
|
+
}
|
|
1087
|
+
function collectValues(props, views) {
|
|
1088
|
+
for (const value of Object.values(props)) collectValue(value, views);
|
|
1089
|
+
}
|
|
1090
|
+
function collectValue(value, views) {
|
|
1091
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value) && "$ref" in value) {
|
|
1092
|
+
views.add(value.$ref);
|
|
1093
|
+
for (const [key, sibling] of Object.entries(value)) {
|
|
1094
|
+
if (key !== "$ref") collectValue(sibling, views);
|
|
1095
|
+
}
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
if (Array.isArray(value)) {
|
|
1099
|
+
for (const item of value) collectValue(item, views);
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
if (value !== null && typeof value === "object") collectValues(value, views);
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
// entitlements.ts
|
|
1106
|
+
function deriveAuthorizationGrants(contracts, held, options = {}) {
|
|
1107
|
+
const undeclared = options.undeclaredRequirements ?? "withhold";
|
|
1108
|
+
const bound = options.document ? collectDocumentReferences(options.document) : void 0;
|
|
1109
|
+
const permissions = new Set(held.permissions ?? []);
|
|
1110
|
+
const entitlements = new Set(held.entitlements ?? []);
|
|
1111
|
+
const featureFlags = new Set(held.featureFlags ?? []);
|
|
1112
|
+
const views = [];
|
|
1113
|
+
const intents = [];
|
|
1114
|
+
const withheld = [];
|
|
1115
|
+
for (const contract of contracts) {
|
|
1116
|
+
if (undeclared === "withhold" && !declaresAnyRequirement(contract)) {
|
|
1117
|
+
withheld.push({ namespace: contract.namespace, missing: { kind: "permission", name: "(none declared)" } });
|
|
1118
|
+
continue;
|
|
1119
|
+
}
|
|
1120
|
+
const missing = firstMissingRequirement(contract, { permissions, entitlements, featureFlags });
|
|
1121
|
+
if (missing) {
|
|
1122
|
+
withheld.push({ namespace: contract.namespace, missing });
|
|
1123
|
+
continue;
|
|
1124
|
+
}
|
|
1125
|
+
for (const view of contract.views) {
|
|
1126
|
+
const reference = `capability://${contract.namespace}/${shortName(view.name, contract.namespace, "/")}`;
|
|
1127
|
+
if (!bound || bound.views.has(reference)) views.push(reference);
|
|
1128
|
+
}
|
|
1129
|
+
for (const intent of actionIntentsOf(contract)) {
|
|
1130
|
+
const reference = `capability://${contract.namespace}/${shortName(intent.name, contract.namespace, ".")}`;
|
|
1131
|
+
if (!bound || bound.intents.has(reference)) intents.push(reference);
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
return {
|
|
1135
|
+
grants: { views: [...new Set(views)].sort(), intents: [...new Set(intents)].sort() },
|
|
1136
|
+
withheld: withheld.sort((left, right) => left.namespace < right.namespace ? -1 : left.namespace > right.namespace ? 1 : 0)
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1139
|
+
function declaresAnyRequirement(contract) {
|
|
1140
|
+
const requirements = contract.requirements;
|
|
1141
|
+
return Boolean(
|
|
1142
|
+
requirements && ((requirements.permissions?.length ?? 0) > 0 || (requirements.entitlements?.length ?? 0) > 0 || (requirements.featureFlags?.length ?? 0) > 0)
|
|
1143
|
+
);
|
|
1144
|
+
}
|
|
1145
|
+
function firstMissingRequirement(contract, held) {
|
|
1146
|
+
const requirements = contract.requirements;
|
|
1147
|
+
for (const name of requirements?.permissions ?? []) {
|
|
1148
|
+
if (!held.permissions.has(name)) return { kind: "permission", name };
|
|
1149
|
+
}
|
|
1150
|
+
for (const name of requirements?.entitlements ?? []) {
|
|
1151
|
+
if (!held.entitlements.has(name)) return { kind: "entitlement", name };
|
|
1152
|
+
}
|
|
1153
|
+
for (const name of requirements?.featureFlags ?? []) {
|
|
1154
|
+
if (!held.featureFlags.has(name)) return { kind: "featureFlag", name };
|
|
1155
|
+
}
|
|
1156
|
+
return void 0;
|
|
1157
|
+
}
|
|
1158
|
+
function actionIntentsOf(contract) {
|
|
1159
|
+
const experience = contract.extensions?.["fabric.experience/v1"];
|
|
1160
|
+
return experience?.actionIntents ?? [];
|
|
1161
|
+
}
|
|
1162
|
+
function shortName(name, namespace, separator) {
|
|
1163
|
+
const prefix = `${namespace}${separator}`;
|
|
1164
|
+
return name.startsWith(prefix) ? name.slice(prefix.length) : name;
|
|
1165
|
+
}
|
|
842
1166
|
|
|
843
1167
|
// schemas.ts
|
|
844
1168
|
var SDUI_DOCUMENT_JSON_SCHEMA = {
|
|
@@ -870,24 +1194,280 @@ var SDUI_RELEASE_JSON_SCHEMA = {
|
|
|
870
1194
|
grants: { type: "object", required: ["views", "intents"] }
|
|
871
1195
|
}
|
|
872
1196
|
};
|
|
1197
|
+
|
|
1198
|
+
// contracts.ts
|
|
1199
|
+
var SDUI_RELEASE_V3_FORMAT_VERSION = 3;
|
|
1200
|
+
var RELEASE_CHANNEL_POINTER_FORMAT_VERSION = 1;
|
|
1201
|
+
var encoder = new TextEncoder();
|
|
1202
|
+
var decoder = new TextDecoder("utf-8", { fatal: true });
|
|
1203
|
+
var HEX_DIGEST = /^[a-f0-9]{64}$/;
|
|
1204
|
+
function canonicalizeJcs(value) {
|
|
1205
|
+
return canonical(value, 0);
|
|
1206
|
+
}
|
|
1207
|
+
function canonical(value, depth) {
|
|
1208
|
+
if (depth > 64) throw new Error("Canonical JSON exceeds the maximum depth of 64.");
|
|
1209
|
+
if (value === null) return "null";
|
|
1210
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
1211
|
+
if (typeof value === "number") {
|
|
1212
|
+
if (!Number.isFinite(value)) throw new Error("Canonical JSON numbers must be finite.");
|
|
1213
|
+
return Object.is(value, -0) ? "0" : JSON.stringify(value);
|
|
1214
|
+
}
|
|
1215
|
+
if (typeof value === "string") {
|
|
1216
|
+
assertValidUnicode(value);
|
|
1217
|
+
return JSON.stringify(value);
|
|
1218
|
+
}
|
|
1219
|
+
if (Array.isArray(value)) return `[${value.map((entry) => canonical(entry, depth + 1)).join(",")}]`;
|
|
1220
|
+
if (typeof value !== "object") throw new Error(`Canonical JSON cannot encode ${typeof value}.`);
|
|
1221
|
+
const record = value;
|
|
1222
|
+
const keys = Object.keys(record).sort();
|
|
1223
|
+
return `{${keys.map((key) => {
|
|
1224
|
+
assertValidUnicode(key);
|
|
1225
|
+
return `${JSON.stringify(key)}:${canonical(record[key], depth + 1)}`;
|
|
1226
|
+
}).join(",")}}`;
|
|
1227
|
+
}
|
|
1228
|
+
function assertValidUnicode(value) {
|
|
1229
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
1230
|
+
const code = value.charCodeAt(index);
|
|
1231
|
+
if (code >= 55296 && code <= 56319) {
|
|
1232
|
+
const next = value.charCodeAt(index + 1);
|
|
1233
|
+
if (!(next >= 56320 && next <= 57343)) throw new Error("Canonical JSON contains an unpaired Unicode surrogate.");
|
|
1234
|
+
index += 1;
|
|
1235
|
+
} else if (code >= 56320 && code <= 57343) {
|
|
1236
|
+
throw new Error("Canonical JSON contains an unpaired Unicode surrogate.");
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
async function verifySignedExperienceArtifact(input) {
|
|
1241
|
+
const maxBytes = input.maxBytes ?? 1048576;
|
|
1242
|
+
if (encoder.encode(input.compactJws).byteLength > maxBytes) {
|
|
1243
|
+
throw new Error(`Signed experience artifact exceeds the ${maxBytes}-byte size limit.`);
|
|
1244
|
+
}
|
|
1245
|
+
const parts = input.compactJws.split(".");
|
|
1246
|
+
if (parts.length !== 3 || parts.some((part) => part.length === 0)) throw new Error("Signed experience artifact must be a compact JWS.");
|
|
1247
|
+
const header = parseSegment(parts[0], "protected header");
|
|
1248
|
+
if (header.alg !== "ES256") throw new Error("Signed experience artifacts must use ES256.");
|
|
1249
|
+
if (header.typ !== input.expectedType) throw new Error(`Signed experience artifact type must be ${input.expectedType}.`);
|
|
1250
|
+
if (typeof header.kid !== "string" || !header.kid) throw new Error("Signed experience artifact requires a key id.");
|
|
1251
|
+
const payloadBytes = decodeBase64Url(parts[1]);
|
|
1252
|
+
const payload = parseJsonBytes(payloadBytes, "payload");
|
|
1253
|
+
if (typeof payload.issuer !== "string" || !payload.issuer) throw new Error("Signed experience artifact requires an issuer.");
|
|
1254
|
+
const key = await input.trust.resolveKey({ keyId: header.kid, issuer: payload.issuer, algorithm: "ES256" });
|
|
1255
|
+
if (!key || key.algorithm !== "ES256") throw new Error("Signed experience artifact has no trusted key.");
|
|
1256
|
+
const signature = decodeBase64Url(parts[2]);
|
|
1257
|
+
if (signature.byteLength !== 64) throw new Error("ES256 signatures must be 64-byte JOSE signatures.");
|
|
1258
|
+
const verified = await input.crypto.verify({
|
|
1259
|
+
algorithm: "ES256",
|
|
1260
|
+
key: key.key,
|
|
1261
|
+
signingInput: encoder.encode(`${parts[0]}.${parts[1]}`),
|
|
1262
|
+
signature
|
|
1263
|
+
});
|
|
1264
|
+
if (!verified) throw new Error("Signed experience artifact signature is invalid.");
|
|
1265
|
+
return payload;
|
|
1266
|
+
}
|
|
1267
|
+
async function verifySignedExperienceRelease(input) {
|
|
1268
|
+
const payload = await verifySignedExperienceArtifact({
|
|
1269
|
+
compactJws: input.compactJws,
|
|
1270
|
+
expectedType: "fabric-experience-release+jws",
|
|
1271
|
+
crypto: input.crypto,
|
|
1272
|
+
trust: input.trust,
|
|
1273
|
+
...input.maxBytes === void 0 ? {} : { maxBytes: input.maxBytes }
|
|
1274
|
+
});
|
|
1275
|
+
assertSduiReleaseV3(payload);
|
|
1276
|
+
if (payload.issuer !== input.expected.issuer || payload.application !== input.expected.application || payload.assemblyDigest !== input.expected.assemblyDigest) {
|
|
1277
|
+
throw new Error("Signed experience release does not match the expected issuer, application, and assembly.");
|
|
1278
|
+
}
|
|
1279
|
+
const { releaseDigest: releaseDigest2, ...body } = payload;
|
|
1280
|
+
const digest = await input.crypto.digestSha256(encoder.encode(canonicalizeJcs(body)));
|
|
1281
|
+
const expectedDigest = [...digest].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
1282
|
+
if (releaseDigest2 !== expectedDigest) throw new Error("Signed experience release digest does not match its canonical content.");
|
|
1283
|
+
return payload;
|
|
1284
|
+
}
|
|
1285
|
+
function assertSduiReleaseV3(value) {
|
|
1286
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("SDUI Release v3 must be an object.");
|
|
1287
|
+
const release = value;
|
|
1288
|
+
if (release.formatVersion !== SDUI_RELEASE_V3_FORMAT_VERSION || release.kind !== "experience-release") {
|
|
1289
|
+
throw new Error("SDUI Release v3 has an unsupported format.");
|
|
1290
|
+
}
|
|
1291
|
+
for (const field of ["issuer", "application", "releaseDigest", "assemblyDigest"]) {
|
|
1292
|
+
if (typeof release[field] !== "string" || !release[field]) throw new Error(`SDUI Release v3 ${field} is required.`);
|
|
1293
|
+
}
|
|
1294
|
+
if (!HEX_DIGEST.test(release.releaseDigest) || !HEX_DIGEST.test(release.assemblyDigest)) {
|
|
1295
|
+
throw new Error("SDUI Release v3 digests must be lowercase SHA-256 values.");
|
|
1296
|
+
}
|
|
1297
|
+
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)) {
|
|
1298
|
+
throw new Error("SDUI Release v3 is structurally incomplete.");
|
|
1299
|
+
}
|
|
1300
|
+
const routes = release.routes;
|
|
1301
|
+
assertUniqueRecords(routes.views, "view route", (route) => {
|
|
1302
|
+
const record = assertRecord(route, "view route");
|
|
1303
|
+
assertCapabilityReference(record.reference, "view route");
|
|
1304
|
+
assertNonEmptyString(record.name, "view route name");
|
|
1305
|
+
assertNonEmptyString(record.version, "view route version");
|
|
1306
|
+
if (record.parameterSchemaDigest !== void 0) {
|
|
1307
|
+
assertHexDigest(record.parameterSchemaDigest, "view route parameterSchemaDigest");
|
|
1308
|
+
}
|
|
1309
|
+
return record.reference;
|
|
1310
|
+
});
|
|
1311
|
+
assertUniqueRecords(routes.intents, "intent route", (route) => {
|
|
1312
|
+
const record = assertRecord(route, "intent route");
|
|
1313
|
+
assertCapabilityReference(record.reference, "intent route");
|
|
1314
|
+
assertNonEmptyString(record.actionId, "intent route actionId");
|
|
1315
|
+
if (!Number.isSafeInteger(record.version) || record.version < 1) {
|
|
1316
|
+
throw new Error("SDUI Release v3 intent route version must be a positive integer.");
|
|
1317
|
+
}
|
|
1318
|
+
if (record.parameterSchemaDigest !== void 0) {
|
|
1319
|
+
assertHexDigest(record.parameterSchemaDigest, "intent route parameterSchemaDigest");
|
|
1320
|
+
}
|
|
1321
|
+
return record.reference;
|
|
1322
|
+
});
|
|
1323
|
+
assertUniqueRecords(release.effectiveGrants, "effective grant", (grant) => {
|
|
1324
|
+
const record = assertRecord(grant, "effective grant");
|
|
1325
|
+
assertNonEmptyString(record.treeId, "effective grant treeId");
|
|
1326
|
+
assertNonEmptyString(record.fragmentId, "effective grant fragmentId");
|
|
1327
|
+
if (!Array.isArray(record.views) || !record.views.every((item) => typeof item === "string") || !Array.isArray(record.intents) || !record.intents.every((item) => typeof item === "string")) {
|
|
1328
|
+
throw new Error("SDUI Release v3 effective grant routes must be string arrays.");
|
|
1329
|
+
}
|
|
1330
|
+
return `${record.treeId}\0${record.fragmentId}`;
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1333
|
+
function assertRecord(value, label) {
|
|
1334
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`SDUI Release v3 ${label} must be an object.`);
|
|
1335
|
+
return value;
|
|
1336
|
+
}
|
|
1337
|
+
function assertNonEmptyString(value, label) {
|
|
1338
|
+
if (typeof value !== "string" || !value) throw new Error(`SDUI Release v3 ${label} is required.`);
|
|
1339
|
+
}
|
|
1340
|
+
function assertHexDigest(value, label) {
|
|
1341
|
+
if (typeof value !== "string" || !HEX_DIGEST.test(value)) throw new Error(`SDUI Release v3 ${label} must be a lowercase SHA-256 value.`);
|
|
1342
|
+
}
|
|
1343
|
+
function assertCapabilityReference(value, label) {
|
|
1344
|
+
if (typeof value !== "string" || !value.startsWith("capability://")) throw new Error(`SDUI Release v3 ${label} reference is invalid.`);
|
|
1345
|
+
}
|
|
1346
|
+
function assertUniqueRecords(values, label, identity) {
|
|
1347
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1348
|
+
for (const value of values) {
|
|
1349
|
+
const key = identity(value);
|
|
1350
|
+
if (seen.has(key)) throw new Error(`SDUI Release v3 has a duplicate ${label}.`);
|
|
1351
|
+
seen.add(key);
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
async function activateReleaseChannelPointer(input) {
|
|
1355
|
+
const payload = await verifySignedExperienceArtifact({
|
|
1356
|
+
compactJws: input.compactJws,
|
|
1357
|
+
expectedType: "fabric-release-channel-pointer+jws",
|
|
1358
|
+
crypto: input.crypto,
|
|
1359
|
+
trust: input.trust
|
|
1360
|
+
});
|
|
1361
|
+
assertReleaseChannelPointer(payload);
|
|
1362
|
+
if (payload.issuer !== input.expected.issuer || payload.application !== input.expected.application || payload.channel !== input.expected.channel) {
|
|
1363
|
+
throw new Error("Release channel pointer does not match the expected issuer, application, and channel.");
|
|
1364
|
+
}
|
|
1365
|
+
const now = (input.now ?? /* @__PURE__ */ new Date()).getTime();
|
|
1366
|
+
if (payload.notBefore !== void 0 && now < parseTimestamp(payload.notBefore, "notBefore")) {
|
|
1367
|
+
throw new Error("Release channel pointer is not active yet.");
|
|
1368
|
+
}
|
|
1369
|
+
if (payload.expiresAt !== void 0 && now >= parseTimestamp(payload.expiresAt, "expiresAt")) {
|
|
1370
|
+
throw new Error("Release channel pointer has expired.");
|
|
1371
|
+
}
|
|
1372
|
+
const generationKey = `${payload.issuer}:${payload.application}:${payload.channel}`;
|
|
1373
|
+
const pointerDigestBytes = await input.crypto.digestSha256(encoder.encode(canonicalizeJcs(payload)));
|
|
1374
|
+
const pointerDigest = [...pointerDigestBytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
1375
|
+
const current = await input.generations.get(generationKey);
|
|
1376
|
+
if (current !== void 0 && payload.generation < current.generation) {
|
|
1377
|
+
throw new Error("Release channel pointer is older than the accepted generation.");
|
|
1378
|
+
}
|
|
1379
|
+
if (current?.generation === payload.generation && current.pointerDigest !== pointerDigest) {
|
|
1380
|
+
throw new Error("Release channel generation is already bound to a different signed pointer.");
|
|
1381
|
+
}
|
|
1382
|
+
if (current?.generation !== payload.generation) {
|
|
1383
|
+
const accepted = await input.generations.acceptIfHigher(generationKey, payload.generation, pointerDigest);
|
|
1384
|
+
if (!accepted) {
|
|
1385
|
+
const raced = await input.generations.get(generationKey);
|
|
1386
|
+
if (raced?.generation !== payload.generation || raced.pointerDigest !== pointerDigest) {
|
|
1387
|
+
throw new Error("Release channel pointer lost an activation race to a different generation.");
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
return payload;
|
|
1392
|
+
}
|
|
1393
|
+
function assertReleaseChannelPointer(value) {
|
|
1394
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Release channel pointer must be an object.");
|
|
1395
|
+
const pointer = value;
|
|
1396
|
+
if (pointer.formatVersion !== RELEASE_CHANNEL_POINTER_FORMAT_VERSION || pointer.kind !== "release-channel-pointer") {
|
|
1397
|
+
throw new Error("Release channel pointer has an unsupported format.");
|
|
1398
|
+
}
|
|
1399
|
+
for (const field of ["issuer", "application", "channel", "issuedAt"]) {
|
|
1400
|
+
if (typeof pointer[field] !== "string" || !pointer[field]) throw new Error(`Release channel pointer ${field} is required.`);
|
|
1401
|
+
}
|
|
1402
|
+
if (!Number.isSafeInteger(pointer.generation) || pointer.generation < 0) throw new Error("Release channel pointer generation must be a non-negative safe integer.");
|
|
1403
|
+
if (!HEX_DIGEST.test(String(pointer.activeReleaseDigest)) || !HEX_DIGEST.test(String(pointer.assemblyDigest))) {
|
|
1404
|
+
throw new Error("Release channel pointer digests must be lowercase SHA-256 values.");
|
|
1405
|
+
}
|
|
1406
|
+
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)) {
|
|
1407
|
+
throw new Error("Release channel pointer fallbackReleaseDigests must contain unique lowercase SHA-256 values.");
|
|
1408
|
+
}
|
|
1409
|
+
parseTimestamp(pointer.issuedAt, "issuedAt");
|
|
1410
|
+
}
|
|
1411
|
+
function parseSegment(segment, label) {
|
|
1412
|
+
return parseJsonBytes(decodeBase64Url(segment), label);
|
|
1413
|
+
}
|
|
1414
|
+
function parseJsonBytes(bytes, label) {
|
|
1415
|
+
let value;
|
|
1416
|
+
try {
|
|
1417
|
+
value = JSON.parse(decoder.decode(bytes));
|
|
1418
|
+
} catch {
|
|
1419
|
+
throw new Error(`Signed experience artifact ${label} is not valid UTF-8 JSON.`);
|
|
1420
|
+
}
|
|
1421
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`Signed experience artifact ${label} must be an object.`);
|
|
1422
|
+
return value;
|
|
1423
|
+
}
|
|
1424
|
+
function decodeBase64Url(value) {
|
|
1425
|
+
if (!/^[A-Za-z0-9_-]+$/.test(value)) throw new Error("Signed experience artifact contains invalid base64url.");
|
|
1426
|
+
const padded = value.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(value.length / 4) * 4, "=");
|
|
1427
|
+
let binary;
|
|
1428
|
+
try {
|
|
1429
|
+
binary = atob(padded);
|
|
1430
|
+
} catch {
|
|
1431
|
+
throw new Error("Signed experience artifact contains invalid base64url.");
|
|
1432
|
+
}
|
|
1433
|
+
return Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
|
1434
|
+
}
|
|
1435
|
+
function parseTimestamp(value, field) {
|
|
1436
|
+
if (!/(?:Z|[+-]\d{2}:\d{2})$/.test(value)) throw new Error(`Release channel pointer ${field} must be RFC 3339 with an explicit offset.`);
|
|
1437
|
+
const parsed = Date.parse(value);
|
|
1438
|
+
if (Number.isNaN(parsed)) throw new Error(`Release channel pointer ${field} is invalid.`);
|
|
1439
|
+
return parsed;
|
|
1440
|
+
}
|
|
873
1441
|
// Annotate the CommonJS export names for ESM import in node:
|
|
874
1442
|
0 && (module.exports = {
|
|
1443
|
+
RELEASE_CHANNEL_POINTER_FORMAT_VERSION,
|
|
875
1444
|
SDUI_COMPONENT_PACK_FORMAT_VERSION,
|
|
876
1445
|
SDUI_DOCUMENT_FORMAT_VERSION,
|
|
877
1446
|
SDUI_DOCUMENT_JSON_SCHEMA,
|
|
878
1447
|
SDUI_RELEASE_FORMAT_VERSION,
|
|
879
1448
|
SDUI_RELEASE_JSON_SCHEMA,
|
|
1449
|
+
SDUI_RELEASE_V3_FORMAT_VERSION,
|
|
880
1450
|
SDUI_TOKEN_SET_FORMAT_VERSION,
|
|
1451
|
+
activateReleaseChannelPointer,
|
|
1452
|
+
assertReleaseChannelPointer,
|
|
881
1453
|
assertSduiAuthorizationGrants,
|
|
882
1454
|
assertSduiComponentPack,
|
|
883
1455
|
assertSduiDocument,
|
|
884
1456
|
assertSduiFragment,
|
|
885
1457
|
assertSduiRelease,
|
|
886
1458
|
assertSduiReleaseIntegrity,
|
|
1459
|
+
assertSduiReleaseV3,
|
|
887
1460
|
assertSduiReleaseValid,
|
|
888
1461
|
assertSduiTokenSet,
|
|
1462
|
+
canonicalizeJcs,
|
|
889
1463
|
computeReleaseDigest,
|
|
890
1464
|
createSduiChannel,
|
|
891
|
-
|
|
1465
|
+
deriveAuthorizationGrants,
|
|
1466
|
+
parseCapabilityRef,
|
|
1467
|
+
runSduiChannelChecks,
|
|
1468
|
+
sduiChannelChecks,
|
|
1469
|
+
validateSduiRelease,
|
|
1470
|
+
verifySignedExperienceArtifact,
|
|
1471
|
+
verifySignedExperienceRelease
|
|
892
1472
|
});
|
|
893
1473
|
//# sourceMappingURL=index.cjs.map
|