@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/dist/index.js CHANGED
@@ -1,3 +1,14 @@
1
+ import {
2
+ RELEASE_CHANNEL_POINTER_FORMAT_VERSION,
3
+ SDUI_RELEASE_V3_FORMAT_VERSION,
4
+ activateReleaseChannelPointer,
5
+ assertReleaseChannelPointer,
6
+ assertSduiReleaseV3,
7
+ canonicalizeJcs,
8
+ verifySignedExperienceArtifact,
9
+ verifySignedExperienceRelease
10
+ } from "./chunk-BTWHVBZ3.js";
11
+
1
12
  // types.ts
2
13
  var SDUI_DOCUMENT_FORMAT_VERSION = 1;
3
14
  var SDUI_TOKEN_SET_FORMAT_VERSION = 1;
@@ -24,8 +35,9 @@ function requireArray(value, path) {
24
35
  if (!Array.isArray(value)) fail(path, "must be an array");
25
36
  return value;
26
37
  }
38
+ var CAPABILITY_REF = /^capability:\/\/([a-z][a-z0-9-]*)\/([a-z][a-z0-9._-]*(?:\/[a-z0-9._-]+)*)$/;
27
39
  function parseCapabilityRef(ref) {
28
- const match = /^capability:\/\/([a-z][a-z0-9-]*)\/(.+)$/.exec(ref);
40
+ const match = CAPABILITY_REF.exec(ref);
29
41
  if (!match) return void 0;
30
42
  return { namespace: match[1], name: match[2] };
31
43
  }
@@ -160,8 +172,20 @@ function assertSduiDocument(value, path = "document") {
160
172
  });
161
173
  }
162
174
  const tokenSet = requireRecord(document.tokenSet, `${path}.tokenSet`);
163
- requireString(tokenSet.name, `${path}.tokenSet.name`);
164
- requireString(tokenSet.version, `${path}.tokenSet.version`);
175
+ assertTokenSetReference(tokenSet, `${path}.tokenSet`);
176
+ if (document.supportedTokenSets !== void 0) {
177
+ const seenTokenSets = /* @__PURE__ */ new Set();
178
+ requireArray(document.supportedTokenSets, `${path}.supportedTokenSets`).forEach((entry, index) => {
179
+ const supportedPath = `${path}.supportedTokenSets[${index}]`;
180
+ const supported = requireRecord(entry, supportedPath);
181
+ assertTokenSetReference(supported, supportedPath);
182
+ const identity = `${String(supported.name)}\0${String(supported.version)}`;
183
+ if (seenTokenSets.has(identity)) {
184
+ fail(supportedPath, `declares token set "${String(supported.name)}@${String(supported.version)}" a second time`);
185
+ }
186
+ seenTokenSets.add(identity);
187
+ });
188
+ }
165
189
  requireArray(document.componentPacks, `${path}.componentPacks`).forEach((entry, index) => {
166
190
  const pack = requireRecord(entry, `${path}.componentPacks[${index}]`);
167
191
  requireString(pack.pack, `${path}.componentPacks[${index}].pack`);
@@ -241,6 +265,14 @@ function assertSduiRelease(value, path = "release") {
241
265
  }
242
266
  });
243
267
  }
268
+ function assertTokenSetReference(reference, path) {
269
+ if (!/^[a-z][a-z0-9-]*$/.test(requireString(reference.name, `${path}.name`))) {
270
+ fail(`${path}.name`, "must be lowercase letters, numbers and hyphens");
271
+ }
272
+ if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(requireString(reference.version, `${path}.version`))) {
273
+ fail(`${path}.version`, "must be a semver version");
274
+ }
275
+ }
244
276
 
245
277
  // validate.ts
246
278
  import {
@@ -293,6 +325,16 @@ function releaseDigest(document, tokenSet, componentPacks, grants, assemblyDiges
293
325
  variants: [...document.variants].sort((left, right) => left.id < right.id ? -1 : left.id > right.id ? 1 : 0)
294
326
  } : {},
295
327
  tokenSet: document.tokenSet,
328
+ // Which brands a document may be promoted against is part of what was
329
+ // authored. Sorted, and omitted when absent so a document declaring
330
+ // none digests exactly as it did before the field existed.
331
+ ...document.supportedTokenSets?.length ? {
332
+ supportedTokenSets: [...document.supportedTokenSets].sort((left, right) => {
333
+ if (left.name !== right.name) return left.name < right.name ? -1 : 1;
334
+ if (left.version !== right.version) return left.version < right.version ? -1 : 1;
335
+ return 0;
336
+ })
337
+ } : {},
296
338
  componentPacks: [...document.componentPacks].sort((a, b) => {
297
339
  const key = (e) => `${e.namespace}/${e.pack}`;
298
340
  return key(a) < key(b) ? -1 : key(a) > key(b) ? 1 : 0;
@@ -342,19 +384,56 @@ function buildKnownViews(contracts) {
342
384
  const views = /* @__PURE__ */ new Set();
343
385
  for (const contract of contracts) {
344
386
  for (const view of contract.views) {
345
- const name = view.name.startsWith(`${contract.namespace}/`) ? view.name.slice(contract.namespace.length + 1) : view.name;
346
- views.add(`${contract.namespace}/${name}`);
387
+ views.add(`${contract.namespace}/${shortViewName(view.name, contract.namespace)}`);
347
388
  }
348
389
  }
349
390
  return views;
350
391
  }
351
- function buildKnownActionIntents(contracts) {
392
+ function shortViewName(name, namespace) {
393
+ return name.startsWith(`${namespace}/`) ? name.slice(namespace.length + 1) : name;
394
+ }
395
+ function reportAmbiguousGrantedViews(contracts, grantedViews, add) {
396
+ const occurrences = /* @__PURE__ */ new Map();
397
+ for (const contract of contracts) {
398
+ for (const view of contract.views) {
399
+ const reference = `capability://${contract.namespace}/${shortViewName(view.name, contract.namespace)}`;
400
+ if (!grantedViews.has(reference)) continue;
401
+ occurrences.set(reference, [...occurrences.get(reference) ?? [], `${view.name}@${view.version}`]);
402
+ }
403
+ }
404
+ for (const [reference, declarations] of occurrences) {
405
+ if (declarations.length > 1) {
406
+ add(
407
+ "ambiguous_capability_reference",
408
+ `grants.views.${reference}`,
409
+ `"${reference}" is granted but addresses ${declarations.length} views (${declarations.join(", ")}); a versionless reference cannot pick one`
410
+ );
411
+ }
412
+ }
413
+ }
414
+ function buildKnownActionIntents(contracts, add, grantedIntents) {
352
415
  const intents = /* @__PURE__ */ new Set();
416
+ const occurrences = /* @__PURE__ */ new Map();
353
417
  for (const contract of contracts) {
354
418
  const experience = contract.extensions?.["fabric.experience/v1"];
419
+ const declaredActions = new Set(contract.actions.map((action) => action.actionId));
355
420
  for (const intent of experience?.actionIntents ?? []) {
356
- const shortName = intent.name.startsWith(`${contract.namespace}.`) ? intent.name.slice(contract.namespace.length + 1) : intent.name;
357
- intents.add(`${contract.namespace}/${shortName}`);
421
+ if (!declaredActions.has(intent.actionId)) continue;
422
+ const shortName2 = intent.name.startsWith(`${contract.namespace}.`) ? intent.name.slice(contract.namespace.length + 1) : intent.name;
423
+ const reference = `${contract.namespace}/${shortName2}`;
424
+ if (grantedIntents === void 0 || grantedIntents.has(`capability://${reference}`)) {
425
+ occurrences.set(reference, [...occurrences.get(reference) ?? [], intent.name]);
426
+ }
427
+ intents.add(reference);
428
+ }
429
+ }
430
+ for (const [reference, declarations] of occurrences) {
431
+ if (declarations.length > 1) {
432
+ add?.(
433
+ "ambiguous_capability_reference",
434
+ `contracts.actionIntents.${reference}`,
435
+ `"capability://${reference}" is declared ${declarations.length} times (${declarations.join(", ")}), so a route for it cannot be resolved`
436
+ );
358
437
  }
359
438
  }
360
439
  return intents;
@@ -548,18 +627,13 @@ function validateSduiRelease(input) {
548
627
  const add = (code, path, message) => {
549
628
  findings.push({ code, path, message });
550
629
  };
551
- if (input.document.tokenSet.name !== input.tokenSet.name) {
630
+ const promotableTokenSets = [input.document.tokenSet, ...input.document.supportedTokenSets ?? []];
631
+ const promotedAgainst = promotableTokenSets.some((candidate) => candidate.name === input.tokenSet.name && candidate.version === input.tokenSet.version);
632
+ if (!promotedAgainst) {
552
633
  add(
553
634
  "missing_token_set",
554
- "document.tokenSet.name",
555
- `document references token set "${input.document.tokenSet.name}" but the provided set is "${input.tokenSet.name}"`
556
- );
557
- }
558
- if (input.document.tokenSet.version !== input.tokenSet.version) {
559
- add(
560
- "missing_token_set",
561
- "document.tokenSet.version",
562
- `document references token set version "${input.document.tokenSet.version}" but the provided set is version "${input.tokenSet.version}"`
635
+ "document.tokenSet",
636
+ `document supports token sets [${promotableTokenSets.map((set) => `${set.name}@${set.version}`).join(", ")}] but the provided set is "${input.tokenSet.name}@${input.tokenSet.version}"`
563
637
  );
564
638
  }
565
639
  const packsByKey = /* @__PURE__ */ new Map();
@@ -675,9 +749,43 @@ function validateSduiRelease(input) {
675
749
  }
676
750
  const coreComponents = new Set(input.coreComponents ?? []);
677
751
  const knownViews = buildKnownViews(contracts);
678
- const knownIntents = buildKnownActionIntents(contracts);
679
752
  const grantedViews = buildGrantedReferences(input.grants.views, "grants.views");
680
753
  const grantedIntents = buildGrantedReferences(input.grants.intents, "grants.intents");
754
+ const contractNamespaces = /* @__PURE__ */ new Set();
755
+ const distinctContracts = contracts.filter((contract) => {
756
+ if (contractNamespaces.has(contract.namespace)) {
757
+ add(
758
+ "ambiguous_capability_reference",
759
+ `contracts.${contract.namespace}`,
760
+ `contract for "${contract.namespace}" is supplied more than once`
761
+ );
762
+ return false;
763
+ }
764
+ contractNamespaces.add(contract.namespace);
765
+ return true;
766
+ });
767
+ const knownIntents = buildKnownActionIntents(distinctContracts, add, grantedIntents);
768
+ reportAmbiguousGrantedViews(distinctContracts, grantedViews, add);
769
+ for (const reference of grantedViews) {
770
+ const parsed = parseCapabilityRef(reference);
771
+ if (parsed && !knownViews.has(`${parsed.namespace}/${parsed.name}`)) {
772
+ add(
773
+ "unauthorized_data_ref",
774
+ `grants.views.${reference}`,
775
+ `release grants "${reference}", which no supplied capability publishes; promotion would fail to route it`
776
+ );
777
+ }
778
+ }
779
+ for (const reference of grantedIntents) {
780
+ const parsed = parseCapabilityRef(reference);
781
+ if (parsed && !knownIntents.has(`${parsed.namespace}/${parsed.name}`)) {
782
+ add(
783
+ "mutation_bypass",
784
+ `grants.intents.${reference}`,
785
+ `release grants "${reference}", which no supplied capability declares as an intent; promotion would fail to route it`
786
+ );
787
+ }
788
+ }
681
789
  validateIntentActionDeclarations(contracts, add);
682
790
  const fragmentGrants = /* @__PURE__ */ new Map();
683
791
  for (const [fragmentId, declared] of Object.entries(input.grants.fragments ?? {})) {
@@ -802,6 +910,221 @@ function collectCoreComponents(fragment) {
802
910
  }
803
911
  return used;
804
912
  }
913
+ async function refusalReason(run, expect) {
914
+ try {
915
+ await run();
916
+ return null;
917
+ } catch (error) {
918
+ const message = error instanceof Error ? error.message : String(error);
919
+ return expect.test(message) ? message : null;
920
+ }
921
+ }
922
+ function sduiChannelChecks() {
923
+ return [
924
+ {
925
+ id: "fabric.sdui-channel.verified-release.v1",
926
+ async run(subject) {
927
+ assertSduiReleaseIntegrity(subject.validRelease);
928
+ const result = subject.channel.consume(subject.validRelease);
929
+ if (result.release.releaseDigest !== subject.validRelease.releaseDigest) {
930
+ throw new Error("channel returned a release other than the one it was given");
931
+ }
932
+ return [subject.validRelease.releaseDigest];
933
+ }
934
+ },
935
+ {
936
+ id: "fabric.sdui-channel.tampered-release-denial.v1",
937
+ async run(subject) {
938
+ const tampered = {
939
+ ...subject.validRelease,
940
+ document: { ...subject.validRelease.document, name: `${subject.validRelease.document.name}-tampered` }
941
+ };
942
+ const reason = await refusalReason(() => subject.channel.consume(tampered), /digest|integrity/i);
943
+ if (reason === null) {
944
+ throw new Error("channel did not refuse a release whose content no longer matches its digest, citing the digest");
945
+ }
946
+ for (const [label, mutate] of [
947
+ ["version", (release) => ({ ...release, document: { ...release.document, version: "9.9.9" } })],
948
+ ["grants", (release) => ({ ...release, grants: { ...release.grants, views: [...release.grants.views, "capability://smuggled/view"] } })],
949
+ ["assembly", (release) => ({ ...release, assemblyDigest: "f".repeat(64) })]
950
+ ]) {
951
+ const edited = mutate(subject.validRelease);
952
+ if (await refusalReason(() => subject.channel.consume(edited), /digest|integrity/i) === null) {
953
+ throw new Error(`channel accepted a release whose ${label} was edited after promotion`);
954
+ }
955
+ }
956
+ return ["tampered document refused", "edited version, grants and assembly refused"];
957
+ }
958
+ },
959
+ {
960
+ id: "fabric.sdui-channel.unpromoted-document-denial.v1",
961
+ async run(subject) {
962
+ const expected = /digest|integrity|release|formatVersion|must be/i;
963
+ const unpromoted = { document: subject.validRelease.document };
964
+ if (await refusalReason(() => subject.channel.consume(unpromoted), expected) === null) {
965
+ throw new Error("channel did not refuse a bare document that was never promoted, citing what was wrong with it");
966
+ }
967
+ const digestless = { ...subject.validRelease, releaseDigest: void 0 };
968
+ if (await refusalReason(() => subject.channel.consume(digestless), expected) === null) {
969
+ throw new Error("channel did not refuse a release carrying no digest, citing what was wrong with it");
970
+ }
971
+ return ["bare document refused", "digestless release refused"];
972
+ }
973
+ },
974
+ {
975
+ id: "fabric.sdui-channel.reports-missing-vocabulary.v1",
976
+ async run(subject) {
977
+ const used = collectCoreComponents(subject.validRelease.document.root);
978
+ for (const variant of subject.validRelease.document.variants ?? []) {
979
+ for (const component of collectCoreComponents(variant.root)) used.add(component);
980
+ }
981
+ if (!used.has(subject.unimplementedCoreComponent)) {
982
+ throw new Error(
983
+ `conformance subject is vacuous: validRelease never uses "${subject.unimplementedCoreComponent}", so the channel is never asked to report it`
984
+ );
985
+ }
986
+ if (subject.channel.coreComponents.includes(subject.unimplementedCoreComponent)) {
987
+ throw new Error(
988
+ `conformance subject is contradictory: the channel implements "${subject.unimplementedCoreComponent}"`
989
+ );
990
+ }
991
+ const result = subject.channel.consume(subject.validRelease);
992
+ if (result.compatible) {
993
+ throw new Error("channel reported compatible for a release using a component it does not implement");
994
+ }
995
+ if (!result.missingCoreComponents.includes(subject.unimplementedCoreComponent)) {
996
+ throw new Error("channel reported incompatible without naming the component it cannot render");
997
+ }
998
+ return [`missing: ${result.missingCoreComponents.join(", ")}`];
999
+ }
1000
+ },
1001
+ {
1002
+ id: "fabric.sdui-channel.consumption-is-pure.v1",
1003
+ async run(subject) {
1004
+ const before = JSON.stringify(subject.validRelease);
1005
+ subject.channel.consume(subject.validRelease);
1006
+ subject.channel.consume(subject.validRelease);
1007
+ if (JSON.stringify(subject.validRelease) !== before) {
1008
+ throw new Error("channel mutated the release it consumed");
1009
+ }
1010
+ return ["release unchanged after two consumptions"];
1011
+ }
1012
+ }
1013
+ ];
1014
+ }
1015
+ async function runSduiChannelChecks(subject) {
1016
+ const checks = [];
1017
+ for (const check of sduiChannelChecks()) {
1018
+ try {
1019
+ checks.push({ id: check.id, status: "passed", evidence: await check.run(subject) });
1020
+ } catch (error) {
1021
+ checks.push({
1022
+ id: check.id,
1023
+ status: "failed",
1024
+ evidence: [],
1025
+ error: error instanceof Error ? error.message : String(error)
1026
+ });
1027
+ }
1028
+ }
1029
+ return { passed: checks.every((check) => check.status === "passed"), checks };
1030
+ }
1031
+
1032
+ // references.ts
1033
+ function collectDocumentReferences(document) {
1034
+ const views = /* @__PURE__ */ new Set();
1035
+ const intents = /* @__PURE__ */ new Set();
1036
+ const trees = [document.root, ...(document.variants ?? []).map((variant) => variant.root)];
1037
+ for (const tree of trees) walk(tree, views, intents);
1038
+ return { views, intents };
1039
+ }
1040
+ function walk(fragment, views, intents) {
1041
+ if (fragment.dataRef) views.add(fragment.dataRef);
1042
+ for (const action of Object.values(fragment.actions ?? {})) {
1043
+ intents.add(action.intent);
1044
+ if (action.params) collectValues(action.params, views);
1045
+ }
1046
+ if (fragment.props) collectValues(fragment.props, views);
1047
+ for (const child of fragment.children ?? []) walk(child, views, intents);
1048
+ }
1049
+ function collectValues(props, views) {
1050
+ for (const value of Object.values(props)) collectValue(value, views);
1051
+ }
1052
+ function collectValue(value, views) {
1053
+ if (value !== null && typeof value === "object" && !Array.isArray(value) && "$ref" in value) {
1054
+ views.add(value.$ref);
1055
+ for (const [key, sibling] of Object.entries(value)) {
1056
+ if (key !== "$ref") collectValue(sibling, views);
1057
+ }
1058
+ return;
1059
+ }
1060
+ if (Array.isArray(value)) {
1061
+ for (const item of value) collectValue(item, views);
1062
+ return;
1063
+ }
1064
+ if (value !== null && typeof value === "object") collectValues(value, views);
1065
+ }
1066
+
1067
+ // entitlements.ts
1068
+ function deriveAuthorizationGrants(contracts, held, options = {}) {
1069
+ const undeclared = options.undeclaredRequirements ?? "withhold";
1070
+ const bound = options.document ? collectDocumentReferences(options.document) : void 0;
1071
+ const permissions = new Set(held.permissions ?? []);
1072
+ const entitlements = new Set(held.entitlements ?? []);
1073
+ const featureFlags = new Set(held.featureFlags ?? []);
1074
+ const views = [];
1075
+ const intents = [];
1076
+ const withheld = [];
1077
+ for (const contract of contracts) {
1078
+ if (undeclared === "withhold" && !declaresAnyRequirement(contract)) {
1079
+ withheld.push({ namespace: contract.namespace, missing: { kind: "permission", name: "(none declared)" } });
1080
+ continue;
1081
+ }
1082
+ const missing = firstMissingRequirement(contract, { permissions, entitlements, featureFlags });
1083
+ if (missing) {
1084
+ withheld.push({ namespace: contract.namespace, missing });
1085
+ continue;
1086
+ }
1087
+ for (const view of contract.views) {
1088
+ const reference = `capability://${contract.namespace}/${shortName(view.name, contract.namespace, "/")}`;
1089
+ if (!bound || bound.views.has(reference)) views.push(reference);
1090
+ }
1091
+ for (const intent of actionIntentsOf(contract)) {
1092
+ const reference = `capability://${contract.namespace}/${shortName(intent.name, contract.namespace, ".")}`;
1093
+ if (!bound || bound.intents.has(reference)) intents.push(reference);
1094
+ }
1095
+ }
1096
+ return {
1097
+ grants: { views: [...new Set(views)].sort(), intents: [...new Set(intents)].sort() },
1098
+ withheld: withheld.sort((left, right) => left.namespace < right.namespace ? -1 : left.namespace > right.namespace ? 1 : 0)
1099
+ };
1100
+ }
1101
+ function declaresAnyRequirement(contract) {
1102
+ const requirements = contract.requirements;
1103
+ return Boolean(
1104
+ requirements && ((requirements.permissions?.length ?? 0) > 0 || (requirements.entitlements?.length ?? 0) > 0 || (requirements.featureFlags?.length ?? 0) > 0)
1105
+ );
1106
+ }
1107
+ function firstMissingRequirement(contract, held) {
1108
+ const requirements = contract.requirements;
1109
+ for (const name of requirements?.permissions ?? []) {
1110
+ if (!held.permissions.has(name)) return { kind: "permission", name };
1111
+ }
1112
+ for (const name of requirements?.entitlements ?? []) {
1113
+ if (!held.entitlements.has(name)) return { kind: "entitlement", name };
1114
+ }
1115
+ for (const name of requirements?.featureFlags ?? []) {
1116
+ if (!held.featureFlags.has(name)) return { kind: "featureFlag", name };
1117
+ }
1118
+ return void 0;
1119
+ }
1120
+ function actionIntentsOf(contract) {
1121
+ const experience = contract.extensions?.["fabric.experience/v1"];
1122
+ return experience?.actionIntents ?? [];
1123
+ }
1124
+ function shortName(name, namespace, separator) {
1125
+ const prefix = `${namespace}${separator}`;
1126
+ return name.startsWith(prefix) ? name.slice(prefix.length) : name;
1127
+ }
805
1128
 
806
1129
  // schemas.ts
807
1130
  var SDUI_DOCUMENT_JSON_SCHEMA = {
@@ -834,22 +1157,34 @@ var SDUI_RELEASE_JSON_SCHEMA = {
834
1157
  }
835
1158
  };
836
1159
  export {
1160
+ RELEASE_CHANNEL_POINTER_FORMAT_VERSION,
837
1161
  SDUI_COMPONENT_PACK_FORMAT_VERSION,
838
1162
  SDUI_DOCUMENT_FORMAT_VERSION,
839
1163
  SDUI_DOCUMENT_JSON_SCHEMA,
840
1164
  SDUI_RELEASE_FORMAT_VERSION,
841
1165
  SDUI_RELEASE_JSON_SCHEMA,
1166
+ SDUI_RELEASE_V3_FORMAT_VERSION,
842
1167
  SDUI_TOKEN_SET_FORMAT_VERSION,
1168
+ activateReleaseChannelPointer,
1169
+ assertReleaseChannelPointer,
843
1170
  assertSduiAuthorizationGrants,
844
1171
  assertSduiComponentPack,
845
1172
  assertSduiDocument,
846
1173
  assertSduiFragment,
847
1174
  assertSduiRelease,
848
1175
  assertSduiReleaseIntegrity,
1176
+ assertSduiReleaseV3,
849
1177
  assertSduiReleaseValid,
850
1178
  assertSduiTokenSet,
1179
+ canonicalizeJcs,
851
1180
  computeReleaseDigest,
852
1181
  createSduiChannel,
853
- validateSduiRelease
1182
+ deriveAuthorizationGrants,
1183
+ parseCapabilityRef,
1184
+ runSduiChannelChecks,
1185
+ sduiChannelChecks,
1186
+ validateSduiRelease,
1187
+ verifySignedExperienceArtifact,
1188
+ verifySignedExperienceRelease
854
1189
  };
855
1190
  //# sourceMappingURL=index.js.map