@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/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,54 @@ 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"];
355
419
  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}`);
420
+ const shortName2 = intent.name.startsWith(`${contract.namespace}.`) ? intent.name.slice(contract.namespace.length + 1) : intent.name;
421
+ const reference = `${contract.namespace}/${shortName2}`;
422
+ if (grantedIntents === void 0 || grantedIntents.has(`capability://${reference}`)) {
423
+ occurrences.set(reference, [...occurrences.get(reference) ?? [], intent.name]);
424
+ }
425
+ intents.add(reference);
426
+ }
427
+ }
428
+ for (const [reference, declarations] of occurrences) {
429
+ if (declarations.length > 1) {
430
+ add?.(
431
+ "ambiguous_capability_reference",
432
+ `contracts.actionIntents.${reference}`,
433
+ `"capability://${reference}" is declared ${declarations.length} times (${declarations.join(", ")}), so a route for it cannot be resolved`
434
+ );
358
435
  }
359
436
  }
360
437
  return intents;
@@ -548,18 +625,13 @@ function validateSduiRelease(input) {
548
625
  const add = (code, path, message) => {
549
626
  findings.push({ code, path, message });
550
627
  };
551
- if (input.document.tokenSet.name !== input.tokenSet.name) {
628
+ const promotableTokenSets = [input.document.tokenSet, ...input.document.supportedTokenSets ?? []];
629
+ const promotedAgainst = promotableTokenSets.some((candidate) => candidate.name === input.tokenSet.name && candidate.version === input.tokenSet.version);
630
+ if (!promotedAgainst) {
552
631
  add(
553
632
  "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}"`
633
+ "document.tokenSet",
634
+ `document supports token sets [${promotableTokenSets.map((set) => `${set.name}@${set.version}`).join(", ")}] but the provided set is "${input.tokenSet.name}@${input.tokenSet.version}"`
563
635
  );
564
636
  }
565
637
  const packsByKey = /* @__PURE__ */ new Map();
@@ -675,9 +747,21 @@ function validateSduiRelease(input) {
675
747
  }
676
748
  const coreComponents = new Set(input.coreComponents ?? []);
677
749
  const knownViews = buildKnownViews(contracts);
678
- const knownIntents = buildKnownActionIntents(contracts);
679
750
  const grantedViews = buildGrantedReferences(input.grants.views, "grants.views");
680
751
  const grantedIntents = buildGrantedReferences(input.grants.intents, "grants.intents");
752
+ const knownIntents = buildKnownActionIntents(contracts, add, grantedIntents);
753
+ reportAmbiguousGrantedViews(contracts, grantedViews, add);
754
+ const contractNamespaces = /* @__PURE__ */ new Set();
755
+ for (const contract of contracts) {
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
+ }
763
+ contractNamespaces.add(contract.namespace);
764
+ }
681
765
  validateIntentActionDeclarations(contracts, add);
682
766
  const fragmentGrants = /* @__PURE__ */ new Map();
683
767
  for (const [fragmentId, declared] of Object.entries(input.grants.fragments ?? {})) {
@@ -802,6 +886,221 @@ function collectCoreComponents(fragment) {
802
886
  }
803
887
  return used;
804
888
  }
889
+ async function refusalReason(run, expect) {
890
+ try {
891
+ await run();
892
+ return null;
893
+ } catch (error) {
894
+ const message = error instanceof Error ? error.message : String(error);
895
+ return expect.test(message) ? message : null;
896
+ }
897
+ }
898
+ function sduiChannelChecks() {
899
+ return [
900
+ {
901
+ id: "fabric.sdui-channel.verified-release.v1",
902
+ async run(subject) {
903
+ assertSduiReleaseIntegrity(subject.validRelease);
904
+ const result = subject.channel.consume(subject.validRelease);
905
+ if (result.release.releaseDigest !== subject.validRelease.releaseDigest) {
906
+ throw new Error("channel returned a release other than the one it was given");
907
+ }
908
+ return [subject.validRelease.releaseDigest];
909
+ }
910
+ },
911
+ {
912
+ id: "fabric.sdui-channel.tampered-release-denial.v1",
913
+ async run(subject) {
914
+ const tampered = {
915
+ ...subject.validRelease,
916
+ document: { ...subject.validRelease.document, name: `${subject.validRelease.document.name}-tampered` }
917
+ };
918
+ const reason = await refusalReason(() => subject.channel.consume(tampered), /digest|integrity/i);
919
+ if (reason === null) {
920
+ throw new Error("channel did not refuse a release whose content no longer matches its digest, citing the digest");
921
+ }
922
+ for (const [label, mutate] of [
923
+ ["version", (release) => ({ ...release, document: { ...release.document, version: "9.9.9" } })],
924
+ ["grants", (release) => ({ ...release, grants: { ...release.grants, views: [...release.grants.views, "capability://smuggled/view"] } })],
925
+ ["assembly", (release) => ({ ...release, assemblyDigest: "f".repeat(64) })]
926
+ ]) {
927
+ const edited = mutate(subject.validRelease);
928
+ if (await refusalReason(() => subject.channel.consume(edited), /digest|integrity/i) === null) {
929
+ throw new Error(`channel accepted a release whose ${label} was edited after promotion`);
930
+ }
931
+ }
932
+ return ["tampered document refused", "edited version, grants and assembly refused"];
933
+ }
934
+ },
935
+ {
936
+ id: "fabric.sdui-channel.unpromoted-document-denial.v1",
937
+ async run(subject) {
938
+ const expected = /digest|integrity|release|formatVersion|must be/i;
939
+ const unpromoted = { document: subject.validRelease.document };
940
+ if (await refusalReason(() => subject.channel.consume(unpromoted), expected) === null) {
941
+ throw new Error("channel did not refuse a bare document that was never promoted, citing what was wrong with it");
942
+ }
943
+ const digestless = { ...subject.validRelease, releaseDigest: void 0 };
944
+ if (await refusalReason(() => subject.channel.consume(digestless), expected) === null) {
945
+ throw new Error("channel did not refuse a release carrying no digest, citing what was wrong with it");
946
+ }
947
+ return ["bare document refused", "digestless release refused"];
948
+ }
949
+ },
950
+ {
951
+ id: "fabric.sdui-channel.reports-missing-vocabulary.v1",
952
+ async run(subject) {
953
+ const used = collectCoreComponents(subject.validRelease.document.root);
954
+ for (const variant of subject.validRelease.document.variants ?? []) {
955
+ for (const component of collectCoreComponents(variant.root)) used.add(component);
956
+ }
957
+ if (!used.has(subject.unimplementedCoreComponent)) {
958
+ throw new Error(
959
+ `conformance subject is vacuous: validRelease never uses "${subject.unimplementedCoreComponent}", so the channel is never asked to report it`
960
+ );
961
+ }
962
+ if (subject.channel.coreComponents.includes(subject.unimplementedCoreComponent)) {
963
+ throw new Error(
964
+ `conformance subject is contradictory: the channel implements "${subject.unimplementedCoreComponent}"`
965
+ );
966
+ }
967
+ const result = subject.channel.consume(subject.validRelease);
968
+ if (result.compatible) {
969
+ throw new Error("channel reported compatible for a release using a component it does not implement");
970
+ }
971
+ if (!result.missingCoreComponents.includes(subject.unimplementedCoreComponent)) {
972
+ throw new Error("channel reported incompatible without naming the component it cannot render");
973
+ }
974
+ return [`missing: ${result.missingCoreComponents.join(", ")}`];
975
+ }
976
+ },
977
+ {
978
+ id: "fabric.sdui-channel.consumption-is-pure.v1",
979
+ async run(subject) {
980
+ const before = JSON.stringify(subject.validRelease);
981
+ subject.channel.consume(subject.validRelease);
982
+ subject.channel.consume(subject.validRelease);
983
+ if (JSON.stringify(subject.validRelease) !== before) {
984
+ throw new Error("channel mutated the release it consumed");
985
+ }
986
+ return ["release unchanged after two consumptions"];
987
+ }
988
+ }
989
+ ];
990
+ }
991
+ async function runSduiChannelChecks(subject) {
992
+ const checks = [];
993
+ for (const check of sduiChannelChecks()) {
994
+ try {
995
+ checks.push({ id: check.id, status: "passed", evidence: await check.run(subject) });
996
+ } catch (error) {
997
+ checks.push({
998
+ id: check.id,
999
+ status: "failed",
1000
+ evidence: [],
1001
+ error: error instanceof Error ? error.message : String(error)
1002
+ });
1003
+ }
1004
+ }
1005
+ return { passed: checks.every((check) => check.status === "passed"), checks };
1006
+ }
1007
+
1008
+ // references.ts
1009
+ function collectDocumentReferences(document) {
1010
+ const views = /* @__PURE__ */ new Set();
1011
+ const intents = /* @__PURE__ */ new Set();
1012
+ const trees = [document.root, ...(document.variants ?? []).map((variant) => variant.root)];
1013
+ for (const tree of trees) walk(tree, views, intents);
1014
+ return { views, intents };
1015
+ }
1016
+ function walk(fragment, views, intents) {
1017
+ if (fragment.dataRef) views.add(fragment.dataRef);
1018
+ for (const action of Object.values(fragment.actions ?? {})) {
1019
+ intents.add(action.intent);
1020
+ if (action.params) collectValues(action.params, views);
1021
+ }
1022
+ if (fragment.props) collectValues(fragment.props, views);
1023
+ for (const child of fragment.children ?? []) walk(child, views, intents);
1024
+ }
1025
+ function collectValues(props, views) {
1026
+ for (const value of Object.values(props)) collectValue(value, views);
1027
+ }
1028
+ function collectValue(value, views) {
1029
+ if (value !== null && typeof value === "object" && !Array.isArray(value) && "$ref" in value) {
1030
+ views.add(value.$ref);
1031
+ for (const [key, sibling] of Object.entries(value)) {
1032
+ if (key !== "$ref") collectValue(sibling, views);
1033
+ }
1034
+ return;
1035
+ }
1036
+ if (Array.isArray(value)) {
1037
+ for (const item of value) collectValue(item, views);
1038
+ return;
1039
+ }
1040
+ if (value !== null && typeof value === "object") collectValues(value, views);
1041
+ }
1042
+
1043
+ // entitlements.ts
1044
+ function deriveAuthorizationGrants(contracts, held, options = {}) {
1045
+ const undeclared = options.undeclaredRequirements ?? "withhold";
1046
+ const bound = options.document ? collectDocumentReferences(options.document) : void 0;
1047
+ const permissions = new Set(held.permissions ?? []);
1048
+ const entitlements = new Set(held.entitlements ?? []);
1049
+ const featureFlags = new Set(held.featureFlags ?? []);
1050
+ const views = [];
1051
+ const intents = [];
1052
+ const withheld = [];
1053
+ for (const contract of contracts) {
1054
+ if (undeclared === "withhold" && !declaresAnyRequirement(contract)) {
1055
+ withheld.push({ namespace: contract.namespace, missing: { kind: "permission", name: "(none declared)" } });
1056
+ continue;
1057
+ }
1058
+ const missing = firstMissingRequirement(contract, { permissions, entitlements, featureFlags });
1059
+ if (missing) {
1060
+ withheld.push({ namespace: contract.namespace, missing });
1061
+ continue;
1062
+ }
1063
+ for (const view of contract.views) {
1064
+ const reference = `capability://${contract.namespace}/${shortName(view.name, contract.namespace, "/")}`;
1065
+ if (!bound || bound.views.has(reference)) views.push(reference);
1066
+ }
1067
+ for (const intent of actionIntentsOf(contract)) {
1068
+ const reference = `capability://${contract.namespace}/${shortName(intent.name, contract.namespace, ".")}`;
1069
+ if (!bound || bound.intents.has(reference)) intents.push(reference);
1070
+ }
1071
+ }
1072
+ return {
1073
+ grants: { views: [...new Set(views)].sort(), intents: [...new Set(intents)].sort() },
1074
+ withheld: withheld.sort((left, right) => left.namespace < right.namespace ? -1 : left.namespace > right.namespace ? 1 : 0)
1075
+ };
1076
+ }
1077
+ function declaresAnyRequirement(contract) {
1078
+ const requirements = contract.requirements;
1079
+ return Boolean(
1080
+ requirements && ((requirements.permissions?.length ?? 0) > 0 || (requirements.entitlements?.length ?? 0) > 0 || (requirements.featureFlags?.length ?? 0) > 0)
1081
+ );
1082
+ }
1083
+ function firstMissingRequirement(contract, held) {
1084
+ const requirements = contract.requirements;
1085
+ for (const name of requirements?.permissions ?? []) {
1086
+ if (!held.permissions.has(name)) return { kind: "permission", name };
1087
+ }
1088
+ for (const name of requirements?.entitlements ?? []) {
1089
+ if (!held.entitlements.has(name)) return { kind: "entitlement", name };
1090
+ }
1091
+ for (const name of requirements?.featureFlags ?? []) {
1092
+ if (!held.featureFlags.has(name)) return { kind: "featureFlag", name };
1093
+ }
1094
+ return void 0;
1095
+ }
1096
+ function actionIntentsOf(contract) {
1097
+ const experience = contract.extensions?.["fabric.experience/v1"];
1098
+ return experience?.actionIntents ?? [];
1099
+ }
1100
+ function shortName(name, namespace, separator) {
1101
+ const prefix = `${namespace}${separator}`;
1102
+ return name.startsWith(prefix) ? name.slice(prefix.length) : name;
1103
+ }
805
1104
 
806
1105
  // schemas.ts
807
1106
  var SDUI_DOCUMENT_JSON_SCHEMA = {
@@ -834,22 +1133,34 @@ var SDUI_RELEASE_JSON_SCHEMA = {
834
1133
  }
835
1134
  };
836
1135
  export {
1136
+ RELEASE_CHANNEL_POINTER_FORMAT_VERSION,
837
1137
  SDUI_COMPONENT_PACK_FORMAT_VERSION,
838
1138
  SDUI_DOCUMENT_FORMAT_VERSION,
839
1139
  SDUI_DOCUMENT_JSON_SCHEMA,
840
1140
  SDUI_RELEASE_FORMAT_VERSION,
841
1141
  SDUI_RELEASE_JSON_SCHEMA,
1142
+ SDUI_RELEASE_V3_FORMAT_VERSION,
842
1143
  SDUI_TOKEN_SET_FORMAT_VERSION,
1144
+ activateReleaseChannelPointer,
1145
+ assertReleaseChannelPointer,
843
1146
  assertSduiAuthorizationGrants,
844
1147
  assertSduiComponentPack,
845
1148
  assertSduiDocument,
846
1149
  assertSduiFragment,
847
1150
  assertSduiRelease,
848
1151
  assertSduiReleaseIntegrity,
1152
+ assertSduiReleaseV3,
849
1153
  assertSduiReleaseValid,
850
1154
  assertSduiTokenSet,
1155
+ canonicalizeJcs,
851
1156
  computeReleaseDigest,
852
1157
  createSduiChannel,
853
- validateSduiRelease
1158
+ deriveAuthorizationGrants,
1159
+ parseCapabilityRef,
1160
+ runSduiChannelChecks,
1161
+ sduiChannelChecks,
1162
+ validateSduiRelease,
1163
+ verifySignedExperienceArtifact,
1164
+ verifySignedExperienceRelease
854
1165
  };
855
1166
  //# sourceMappingURL=index.js.map