@fabricorg/sdui-release 0.3.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 +125 -0
- package/LICENSE +21 -0
- package/README.md +103 -0
- package/dist/index.cjs +893 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +334 -0
- package/dist/index.d.ts +334 -0
- package/dist/index.js +855 -0
- package/dist/index.js.map +1 -0
- package/package.json +56 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,893 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
SDUI_COMPONENT_PACK_FORMAT_VERSION: () => SDUI_COMPONENT_PACK_FORMAT_VERSION,
|
|
24
|
+
SDUI_DOCUMENT_FORMAT_VERSION: () => SDUI_DOCUMENT_FORMAT_VERSION,
|
|
25
|
+
SDUI_DOCUMENT_JSON_SCHEMA: () => SDUI_DOCUMENT_JSON_SCHEMA,
|
|
26
|
+
SDUI_RELEASE_FORMAT_VERSION: () => SDUI_RELEASE_FORMAT_VERSION,
|
|
27
|
+
SDUI_RELEASE_JSON_SCHEMA: () => SDUI_RELEASE_JSON_SCHEMA,
|
|
28
|
+
SDUI_TOKEN_SET_FORMAT_VERSION: () => SDUI_TOKEN_SET_FORMAT_VERSION,
|
|
29
|
+
assertSduiAuthorizationGrants: () => assertSduiAuthorizationGrants,
|
|
30
|
+
assertSduiComponentPack: () => assertSduiComponentPack,
|
|
31
|
+
assertSduiDocument: () => assertSduiDocument,
|
|
32
|
+
assertSduiFragment: () => assertSduiFragment,
|
|
33
|
+
assertSduiRelease: () => assertSduiRelease,
|
|
34
|
+
assertSduiReleaseIntegrity: () => assertSduiReleaseIntegrity,
|
|
35
|
+
assertSduiReleaseValid: () => assertSduiReleaseValid,
|
|
36
|
+
assertSduiTokenSet: () => assertSduiTokenSet,
|
|
37
|
+
computeReleaseDigest: () => computeReleaseDigest,
|
|
38
|
+
createSduiChannel: () => createSduiChannel,
|
|
39
|
+
validateSduiRelease: () => validateSduiRelease
|
|
40
|
+
});
|
|
41
|
+
module.exports = __toCommonJS(index_exports);
|
|
42
|
+
|
|
43
|
+
// types.ts
|
|
44
|
+
var SDUI_DOCUMENT_FORMAT_VERSION = 1;
|
|
45
|
+
var SDUI_TOKEN_SET_FORMAT_VERSION = 1;
|
|
46
|
+
var SDUI_COMPONENT_PACK_FORMAT_VERSION = 1;
|
|
47
|
+
var SDUI_RELEASE_FORMAT_VERSION = 2;
|
|
48
|
+
|
|
49
|
+
// structural.ts
|
|
50
|
+
var import_platform = require("@fabricorg/platform");
|
|
51
|
+
var import_assembly = require("@fabricorg/assembly");
|
|
52
|
+
|
|
53
|
+
// helpers.ts
|
|
54
|
+
function fail(path, message) {
|
|
55
|
+
throw new Error(`SDUI ${path} ${message}.`);
|
|
56
|
+
}
|
|
57
|
+
function requireString(value, path) {
|
|
58
|
+
if (typeof value !== "string" || !value.trim()) fail(path, "must be a non-empty string");
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
function requireRecord(value, path) {
|
|
62
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) fail(path, "must be an object");
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
function requireArray(value, path) {
|
|
66
|
+
if (!Array.isArray(value)) fail(path, "must be an array");
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
function parseCapabilityRef(ref) {
|
|
70
|
+
const match = /^capability:\/\/([a-z][a-z0-9-]*)\/(.+)$/.exec(ref);
|
|
71
|
+
if (!match) return void 0;
|
|
72
|
+
return { namespace: match[1], name: match[2] };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// structural.ts
|
|
76
|
+
var TOKEN_TYPES = /* @__PURE__ */ new Set(["color", "spacing", "fontSize", "fontWeight", "radius", "border", "shadow"]);
|
|
77
|
+
function assertSduiTokenSet(value, path = "tokenSet") {
|
|
78
|
+
const tokenSet = requireRecord(value, path);
|
|
79
|
+
if (tokenSet.formatVersion !== 1) {
|
|
80
|
+
fail(`${path}.formatVersion`, `must be 1, received ${JSON.stringify(tokenSet.formatVersion)}`);
|
|
81
|
+
}
|
|
82
|
+
if (!/^[a-z][a-z0-9-]*$/.test(requireString(tokenSet.name, `${path}.name`))) {
|
|
83
|
+
fail(`${path}.name`, "must be lowercase letters, numbers and hyphens");
|
|
84
|
+
}
|
|
85
|
+
if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(requireString(tokenSet.version, `${path}.version`))) {
|
|
86
|
+
fail(`${path}.version`, "must be a semver version");
|
|
87
|
+
}
|
|
88
|
+
const tokens = requireRecord(tokenSet.tokens, `${path}.tokens`);
|
|
89
|
+
for (const [name, token] of Object.entries(tokens)) {
|
|
90
|
+
const tokenPath = `${path}.tokens.${name}`;
|
|
91
|
+
if (!/^[a-z][a-z0-9-]*$/.test(name)) fail(tokenPath, "token name must be lowercase letters, numbers and hyphens");
|
|
92
|
+
const entry = requireRecord(token, tokenPath);
|
|
93
|
+
const type = requireString(entry.type, `${tokenPath}.type`);
|
|
94
|
+
if (!TOKEN_TYPES.has(type)) fail(`${tokenPath}.type`, `must be one of ${[...TOKEN_TYPES].join(", ")}`);
|
|
95
|
+
requireString(entry.value, `${tokenPath}.value`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function assertSduiComponentPack(value, path = "componentPack") {
|
|
99
|
+
const pack = requireRecord(value, path);
|
|
100
|
+
if (pack.formatVersion !== 1) {
|
|
101
|
+
fail(`${path}.formatVersion`, `must be 1, received ${JSON.stringify(pack.formatVersion)}`);
|
|
102
|
+
}
|
|
103
|
+
if (!/^[a-z][a-z0-9-]*$/.test(requireString(pack.pack, `${path}.pack`))) {
|
|
104
|
+
fail(`${path}.pack`, "must be lowercase letters, numbers and hyphens");
|
|
105
|
+
}
|
|
106
|
+
if (!/^[a-z][a-z0-9-]*$/.test(requireString(pack.namespace, `${path}.namespace`))) {
|
|
107
|
+
fail(`${path}.namespace`, "must be lowercase letters, numbers and hyphens");
|
|
108
|
+
}
|
|
109
|
+
if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(requireString(pack.version, `${path}.version`))) {
|
|
110
|
+
fail(`${path}.version`, "must be a semver version");
|
|
111
|
+
}
|
|
112
|
+
if (!/^[0-9a-f]{64}$/.test(requireString(pack.artifactDigest, `${path}.artifactDigest`))) {
|
|
113
|
+
fail(`${path}.artifactDigest`, "must be a lowercase SHA-256 digest");
|
|
114
|
+
}
|
|
115
|
+
if (pack.remote !== void 0) {
|
|
116
|
+
const remote = requireRecord(pack.remote, `${path}.remote`);
|
|
117
|
+
requireString(remote.entry, `${path}.remote.entry`);
|
|
118
|
+
if (!/^sha(256|384|512)-[A-Za-z0-9+/]+={0,2}$/.test(requireString(remote.integrity, `${path}.remote.integrity`))) {
|
|
119
|
+
fail(`${path}.remote.integrity`, 'must be a subresource-integrity value, e.g. "sha384-\u2026"');
|
|
120
|
+
}
|
|
121
|
+
const exposes = requireRecord(remote.exposes, `${path}.remote.exposes`);
|
|
122
|
+
for (const [component, modulePath] of Object.entries(exposes)) {
|
|
123
|
+
requireString(modulePath, `${path}.remote.exposes.${component}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const components = requireRecord(pack.components, `${path}.components`);
|
|
127
|
+
for (const [name, definition] of Object.entries(components)) {
|
|
128
|
+
const componentPath = `${path}.components.${name}`;
|
|
129
|
+
if (!/^[a-z][a-z0-9-]*$/.test(name)) fail(componentPath, "component name must be lowercase letters, numbers and hyphens");
|
|
130
|
+
const def = requireRecord(definition, componentPath);
|
|
131
|
+
if (def.implements !== void 0) {
|
|
132
|
+
if (!Array.isArray(def.implements)) fail(`${componentPath}.implements`, "must be an array");
|
|
133
|
+
for (const impl of def.implements) {
|
|
134
|
+
if (typeof impl !== "string" || !impl.trim()) fail(`${componentPath}.implements`, "entries must be non-empty strings");
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (def.propsSchema !== void 0) {
|
|
138
|
+
try {
|
|
139
|
+
(0, import_platform.assertPortableJsonSchema)(def.propsSchema, `${componentPath}.propsSchema`);
|
|
140
|
+
} catch (error) {
|
|
141
|
+
fail(componentPath, error instanceof Error ? error.message : String(error));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (def.slots !== void 0) {
|
|
145
|
+
if (!Array.isArray(def.slots)) fail(`${componentPath}.slots`, "must be an array");
|
|
146
|
+
for (const slot of def.slots) {
|
|
147
|
+
if (typeof slot !== "string" || !/^[a-z][a-z0-9-]*$/.test(slot)) fail(`${componentPath}.slots`, "slot names must be lowercase letters, numbers and hyphens");
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function assertSduiFragment(value, path = "fragment") {
|
|
153
|
+
const fragment = requireRecord(value, path);
|
|
154
|
+
requireString(fragment.id, `${path}.id`);
|
|
155
|
+
if (fragment.component !== void 0) requireString(fragment.component, `${path}.component`);
|
|
156
|
+
if (fragment.slot !== void 0) {
|
|
157
|
+
if (!/^[a-z][a-z0-9-]*$/.test(requireString(fragment.slot, `${path}.slot`))) {
|
|
158
|
+
fail(`${path}.slot`, "must be lowercase letters, numbers and hyphens");
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (fragment.dataRef !== void 0) {
|
|
162
|
+
const dataRef = requireString(fragment.dataRef, `${path}.dataRef`);
|
|
163
|
+
if (!dataRef.startsWith("capability://")) fail(`${path}.dataRef`, "must be a capability:// reference");
|
|
164
|
+
}
|
|
165
|
+
if (fragment.props !== void 0) {
|
|
166
|
+
requireRecord(fragment.props, `${path}.props`);
|
|
167
|
+
}
|
|
168
|
+
if (fragment.actions !== void 0) {
|
|
169
|
+
const actions = requireRecord(fragment.actions, `${path}.actions`);
|
|
170
|
+
for (const [name, ref] of Object.entries(actions)) {
|
|
171
|
+
const actionPath = `${path}.actions.${name}`;
|
|
172
|
+
const actionRef = requireRecord(ref, actionPath);
|
|
173
|
+
requireString(actionRef.intent, `${actionPath}.intent`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (fragment.children !== void 0) {
|
|
177
|
+
const children = requireArray(fragment.children, `${path}.children`);
|
|
178
|
+
children.forEach((child, index) => assertSduiFragment(child, `${path}.children[${index}]`));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function assertSduiDocument(value, path = "document") {
|
|
182
|
+
const document = requireRecord(value, path);
|
|
183
|
+
if (document.formatVersion !== 1) {
|
|
184
|
+
fail(`${path}.formatVersion`, `must be 1, received ${JSON.stringify(document.formatVersion)}`);
|
|
185
|
+
}
|
|
186
|
+
if (!/^[a-z][a-z0-9-]*$/.test(requireString(document.name, `${path}.name`))) {
|
|
187
|
+
fail(`${path}.name`, "must be lowercase letters, numbers and hyphens");
|
|
188
|
+
}
|
|
189
|
+
if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(requireString(document.version, `${path}.version`))) {
|
|
190
|
+
fail(`${path}.version`, "must be a semver version");
|
|
191
|
+
}
|
|
192
|
+
assertSduiFragment(document.root, `${path}.root`);
|
|
193
|
+
if (document.variants !== void 0) {
|
|
194
|
+
requireArray(document.variants, `${path}.variants`).forEach((entry, index) => {
|
|
195
|
+
const variantPath = `${path}.variants[${index}]`;
|
|
196
|
+
const variant = requireRecord(entry, variantPath);
|
|
197
|
+
if (!/^[a-z][a-z0-9-]*$/.test(requireString(variant.id, `${variantPath}.id`))) {
|
|
198
|
+
fail(`${variantPath}.id`, "must be lowercase letters, numbers and hyphens");
|
|
199
|
+
}
|
|
200
|
+
if (variant.description !== void 0) requireString(variant.description, `${variantPath}.description`);
|
|
201
|
+
assertSduiFragment(variant.root, `${variantPath}.root`);
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
const tokenSet = requireRecord(document.tokenSet, `${path}.tokenSet`);
|
|
205
|
+
requireString(tokenSet.name, `${path}.tokenSet.name`);
|
|
206
|
+
requireString(tokenSet.version, `${path}.tokenSet.version`);
|
|
207
|
+
requireArray(document.componentPacks, `${path}.componentPacks`).forEach((entry, index) => {
|
|
208
|
+
const pack = requireRecord(entry, `${path}.componentPacks[${index}]`);
|
|
209
|
+
requireString(pack.pack, `${path}.componentPacks[${index}].pack`);
|
|
210
|
+
requireString(pack.namespace, `${path}.componentPacks[${index}].namespace`);
|
|
211
|
+
requireString(pack.range, `${path}.componentPacks[${index}].range`);
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
function assertSduiAuthorizationGrants(value, path = "grants") {
|
|
215
|
+
const grants = requireRecord(value, path);
|
|
216
|
+
const views = requireArray(grants.views, `${path}.views`);
|
|
217
|
+
const intents = requireArray(grants.intents, `${path}.intents`);
|
|
218
|
+
for (const [index, reference] of views.entries()) {
|
|
219
|
+
const referencePath = `${path}.views[${index}]`;
|
|
220
|
+
if (!parseCapabilityRef(requireString(reference, referencePath))) {
|
|
221
|
+
fail(referencePath, "must be a valid capability:// reference");
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
for (const [index, reference] of intents.entries()) {
|
|
225
|
+
const referencePath = `${path}.intents[${index}]`;
|
|
226
|
+
if (!parseCapabilityRef(requireString(reference, referencePath))) {
|
|
227
|
+
fail(referencePath, "must be a valid capability:// reference");
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (grants.fragments !== void 0) {
|
|
231
|
+
const fragments = requireRecord(grants.fragments, `${path}.fragments`);
|
|
232
|
+
const prototype = Object.getPrototypeOf(fragments);
|
|
233
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
234
|
+
fail(`${path}.fragments`, "must be a plain object; a fragment grant assigned through __proto__ would be silently discarded");
|
|
235
|
+
}
|
|
236
|
+
for (const [fragmentId, declared] of Object.entries(fragments)) {
|
|
237
|
+
const fragmentPath = `${path}.fragments.${fragmentId}`;
|
|
238
|
+
const scoped = requireRecord(declared, fragmentPath);
|
|
239
|
+
for (const field of ["views", "intents"]) {
|
|
240
|
+
if (scoped[field] === void 0) continue;
|
|
241
|
+
requireArray(scoped[field], `${fragmentPath}.${field}`).forEach((reference, index) => {
|
|
242
|
+
const referencePath = `${fragmentPath}.${field}[${index}]`;
|
|
243
|
+
if (!parseCapabilityRef(requireString(reference, referencePath))) {
|
|
244
|
+
fail(referencePath, "must be a valid capability:// reference");
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
function assertSduiRelease(value, path = "release") {
|
|
252
|
+
const release = requireRecord(value, path);
|
|
253
|
+
if (release.formatVersion !== 2) {
|
|
254
|
+
fail(`${path}.formatVersion`, `must be 2, received ${JSON.stringify(release.formatVersion)}`);
|
|
255
|
+
}
|
|
256
|
+
for (const field of ["releaseDigest", "assemblyDigest"]) {
|
|
257
|
+
if (!/^[0-9a-f]{64}$/.test(requireString(release[field], `${path}.${field}`))) {
|
|
258
|
+
fail(`${path}.${field}`, "must be a lowercase SHA-256 digest");
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
assertSduiDocument(release.document, `${path}.document`);
|
|
262
|
+
assertSduiTokenSet(release.tokenSet, `${path}.tokenSet`);
|
|
263
|
+
assertSduiAuthorizationGrants(release.grants, `${path}.grants`);
|
|
264
|
+
requireArray(release.componentPacks, `${path}.componentPacks`).forEach((value2, index) => {
|
|
265
|
+
const packPath = `${path}.componentPacks[${index}]`;
|
|
266
|
+
const pack = requireRecord(value2, packPath);
|
|
267
|
+
for (const field of ["pack", "namespace"]) {
|
|
268
|
+
if (!/^[a-z][a-z0-9-]*$/.test(requireString(pack[field], `${packPath}.${field}`))) {
|
|
269
|
+
fail(`${packPath}.${field}`, "must be lowercase letters, numbers and hyphens");
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(
|
|
273
|
+
requireString(pack.version, `${packPath}.version`)
|
|
274
|
+
)) {
|
|
275
|
+
fail(`${packPath}.version`, "must be a semver version");
|
|
276
|
+
}
|
|
277
|
+
const range = requireString(pack.range, `${packPath}.range`);
|
|
278
|
+
if (!(0, import_assembly.parseSemverRange)(range)) {
|
|
279
|
+
fail(`${packPath}.range`, "must be a supported semver range");
|
|
280
|
+
}
|
|
281
|
+
if (!/^[0-9a-f]{64}$/.test(requireString(pack.artifactDigest, `${packPath}.artifactDigest`))) {
|
|
282
|
+
fail(`${packPath}.artifactDigest`, "must be a lowercase SHA-256 digest");
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// validate.ts
|
|
288
|
+
var import_assembly2 = require("@fabricorg/assembly");
|
|
289
|
+
var import_gen_capability = require("@fabricorg/gen-capability");
|
|
290
|
+
|
|
291
|
+
// digest.ts
|
|
292
|
+
var import_node_crypto = require("crypto");
|
|
293
|
+
function sortJson(value) {
|
|
294
|
+
if (Array.isArray(value)) return value.map(sortJson);
|
|
295
|
+
if (value && typeof value === "object") {
|
|
296
|
+
return Object.fromEntries(
|
|
297
|
+
Object.entries(value).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key, child]) => [key, sortJson(child)])
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
return value;
|
|
301
|
+
}
|
|
302
|
+
function stableStringify(value) {
|
|
303
|
+
return JSON.stringify(sortJson(value));
|
|
304
|
+
}
|
|
305
|
+
function canonicalFragmentGrants(fragments) {
|
|
306
|
+
if (!fragments) return {};
|
|
307
|
+
return Object.fromEntries(
|
|
308
|
+
Object.keys(fragments).sort().map((fragmentId) => {
|
|
309
|
+
const declared = fragments[fragmentId];
|
|
310
|
+
return [fragmentId, {
|
|
311
|
+
...declared.views ? { views: [...declared.views].sort() } : {},
|
|
312
|
+
...declared.intents ? { intents: [...declared.intents].sort() } : {}
|
|
313
|
+
}];
|
|
314
|
+
})
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
function releaseDigest(document, tokenSet, componentPacks, grants, assemblyDigest) {
|
|
318
|
+
const canonical = stableStringify({
|
|
319
|
+
document: {
|
|
320
|
+
formatVersion: document.formatVersion,
|
|
321
|
+
name: document.name,
|
|
322
|
+
version: document.version,
|
|
323
|
+
root: document.root,
|
|
324
|
+
// Selectable trees, so two documents differing only in their variants
|
|
325
|
+
// must not share a digest. Sorted by id, because reordering the same
|
|
326
|
+
// set does not change which trees can render. Omitted entirely when
|
|
327
|
+
// absent, so a document that declares none digests exactly as it did
|
|
328
|
+
// before variants existed.
|
|
329
|
+
...document.variants?.length ? {
|
|
330
|
+
variants: [...document.variants].sort((left, right) => left.id < right.id ? -1 : left.id > right.id ? 1 : 0)
|
|
331
|
+
} : {},
|
|
332
|
+
tokenSet: document.tokenSet,
|
|
333
|
+
componentPacks: [...document.componentPacks].sort((a, b) => {
|
|
334
|
+
const key = (e) => `${e.namespace}/${e.pack}`;
|
|
335
|
+
return key(a) < key(b) ? -1 : key(a) > key(b) ? 1 : 0;
|
|
336
|
+
})
|
|
337
|
+
},
|
|
338
|
+
tokenSet: {
|
|
339
|
+
formatVersion: tokenSet.formatVersion,
|
|
340
|
+
name: tokenSet.name,
|
|
341
|
+
version: tokenSet.version,
|
|
342
|
+
tokens: tokenSet.tokens
|
|
343
|
+
},
|
|
344
|
+
componentPacks: [...componentPacks].sort((a, b) => {
|
|
345
|
+
const key = (e) => `${e.namespace}/${e.pack}`;
|
|
346
|
+
return key(a) < key(b) ? -1 : key(a) > key(b) ? 1 : 0;
|
|
347
|
+
}),
|
|
348
|
+
grants: {
|
|
349
|
+
views: [...grants.views].sort(),
|
|
350
|
+
intents: [...grants.intents].sort(),
|
|
351
|
+
// Omitted when absent for the same reason as variants: a release with
|
|
352
|
+
// no per-fragment narrowing must digest as it did before the field
|
|
353
|
+
// existed, or every previously promoted release stops verifying.
|
|
354
|
+
...grants.fragments && Object.keys(grants.fragments).length ? { fragments: canonicalFragmentGrants(grants.fragments) } : {}
|
|
355
|
+
},
|
|
356
|
+
assemblyDigest
|
|
357
|
+
});
|
|
358
|
+
return (0, import_node_crypto.createHash)("sha256").update(canonical).digest("hex");
|
|
359
|
+
}
|
|
360
|
+
function computeReleaseDigest(document, tokenSet, componentPacks, grants, assemblyDigest) {
|
|
361
|
+
return releaseDigest(document, tokenSet, componentPacks, grants, assemblyDigest);
|
|
362
|
+
}
|
|
363
|
+
function assertSduiReleaseIntegrity(release) {
|
|
364
|
+
assertSduiRelease(release);
|
|
365
|
+
const expected = releaseDigest(
|
|
366
|
+
release.document,
|
|
367
|
+
release.tokenSet,
|
|
368
|
+
release.componentPacks,
|
|
369
|
+
release.grants,
|
|
370
|
+
release.assemblyDigest
|
|
371
|
+
);
|
|
372
|
+
if (release.releaseDigest !== expected) {
|
|
373
|
+
throw new Error(`SDUI release releaseDigest does not match promoted content; expected "${expected}".`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// capabilities.ts
|
|
378
|
+
function buildKnownViews(contracts) {
|
|
379
|
+
const views = /* @__PURE__ */ new Set();
|
|
380
|
+
for (const contract of contracts) {
|
|
381
|
+
for (const view of contract.views) {
|
|
382
|
+
const name = view.name.startsWith(`${contract.namespace}/`) ? view.name.slice(contract.namespace.length + 1) : view.name;
|
|
383
|
+
views.add(`${contract.namespace}/${name}`);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return views;
|
|
387
|
+
}
|
|
388
|
+
function buildKnownActionIntents(contracts) {
|
|
389
|
+
const intents = /* @__PURE__ */ new Set();
|
|
390
|
+
for (const contract of contracts) {
|
|
391
|
+
const experience = contract.extensions?.["fabric.experience/v1"];
|
|
392
|
+
for (const intent of experience?.actionIntents ?? []) {
|
|
393
|
+
const shortName = intent.name.startsWith(`${contract.namespace}.`) ? intent.name.slice(contract.namespace.length + 1) : intent.name;
|
|
394
|
+
intents.add(`${contract.namespace}/${shortName}`);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return intents;
|
|
398
|
+
}
|
|
399
|
+
function buildGrantedReferences(references, path) {
|
|
400
|
+
const granted = /* @__PURE__ */ new Set();
|
|
401
|
+
for (const [index, reference] of references.entries()) {
|
|
402
|
+
const referencePath = `${path}[${index}]`;
|
|
403
|
+
if (!parseCapabilityRef(reference)) {
|
|
404
|
+
fail(referencePath, "must be a valid capability:// reference");
|
|
405
|
+
}
|
|
406
|
+
granted.add(reference);
|
|
407
|
+
}
|
|
408
|
+
return granted;
|
|
409
|
+
}
|
|
410
|
+
function validateIntentActionDeclarations(contracts, add) {
|
|
411
|
+
for (const contract of contracts) {
|
|
412
|
+
const experience = contract.extensions?.["fabric.experience/v1"];
|
|
413
|
+
const actions = new Set(contract.actions.map((action) => action.actionId));
|
|
414
|
+
for (const [index, intent] of (experience?.actionIntents ?? []).entries()) {
|
|
415
|
+
if (!actions.has(intent.actionId)) {
|
|
416
|
+
add(
|
|
417
|
+
"intent_action_not_found",
|
|
418
|
+
`contracts.${contract.namespace}.actionIntents[${index}].actionId`,
|
|
419
|
+
`intent "${intent.name}" references action "${intent.actionId}", which the capability does not declare`
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// walker.ts
|
|
427
|
+
var MAX_PROP_DEPTH = 64;
|
|
428
|
+
function isCapabilityRefValue(value) {
|
|
429
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) && typeof value.$ref === "string" && value.$ref.startsWith("capability://");
|
|
430
|
+
}
|
|
431
|
+
function narrowGrants(current, declared) {
|
|
432
|
+
if (declared === void 0) return current;
|
|
433
|
+
return new Set(declared.filter((reference) => current.has(reference)));
|
|
434
|
+
}
|
|
435
|
+
function walkFragment(fragment, path, context) {
|
|
436
|
+
const declared = context.fragmentGrants?.get(fragment.id);
|
|
437
|
+
const scoped = declared ? {
|
|
438
|
+
...context,
|
|
439
|
+
grantedViews: narrowGrants(context.grantedViews, declared.views),
|
|
440
|
+
grantedIntents: narrowGrants(context.grantedIntents, declared.intents)
|
|
441
|
+
} : context;
|
|
442
|
+
if (scoped.ids.has(fragment.id)) {
|
|
443
|
+
scoped.add("duplicate_fragment_id", `${path}.id`, `fragment id "${fragment.id}" is declared more than once`);
|
|
444
|
+
} else {
|
|
445
|
+
scoped.ids.add(fragment.id);
|
|
446
|
+
}
|
|
447
|
+
if (fragment.component !== void 0) {
|
|
448
|
+
const separator = fragment.component.indexOf(".");
|
|
449
|
+
if (separator === -1) {
|
|
450
|
+
if (!scoped.coreComponents.has(fragment.component)) {
|
|
451
|
+
scoped.add(
|
|
452
|
+
"unknown_component",
|
|
453
|
+
`${path}.component`,
|
|
454
|
+
`component "${fragment.component}" is not a core component or in any declared pack`
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
} else if (!scoped.knownComponents.has(fragment.component)) {
|
|
458
|
+
scoped.add(
|
|
459
|
+
"unknown_component",
|
|
460
|
+
`${path}.component`,
|
|
461
|
+
`component "${fragment.component}" is not in any declared component pack`
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
if (fragment.dataRef !== void 0) {
|
|
466
|
+
const ref = parseCapabilityRef(fragment.dataRef);
|
|
467
|
+
if (!ref) {
|
|
468
|
+
scoped.add(
|
|
469
|
+
"unauthorized_data_ref",
|
|
470
|
+
`${path}.dataRef`,
|
|
471
|
+
`"${fragment.dataRef}" is not a valid capability:// view reference`
|
|
472
|
+
);
|
|
473
|
+
} else if (!scoped.knownViews.has(`${ref.namespace}/${ref.name}`)) {
|
|
474
|
+
scoped.add(
|
|
475
|
+
"unauthorized_data_ref",
|
|
476
|
+
`${path}.dataRef`,
|
|
477
|
+
`fragment references view "${fragment.dataRef}" which no known capability publishes`
|
|
478
|
+
);
|
|
479
|
+
} else if (!scoped.grantedViews.has(fragment.dataRef)) {
|
|
480
|
+
scoped.add(
|
|
481
|
+
"denied_view_ref",
|
|
482
|
+
`${path}.dataRef`,
|
|
483
|
+
`fragment references view "${fragment.dataRef}" which is published but not granted to this release`
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
for (const [name, actionRef] of Object.entries(fragment.actions ?? {})) {
|
|
488
|
+
const actionPath = `${path}.actions.${name}`;
|
|
489
|
+
const ref = parseCapabilityRef(actionRef.intent);
|
|
490
|
+
if (!ref) {
|
|
491
|
+
scoped.add(
|
|
492
|
+
"mutation_bypass",
|
|
493
|
+
`${actionPath}.intent`,
|
|
494
|
+
`action "${name}" intent "${actionRef.intent}" is not a capability:// reference; direct mutations bypass governance`
|
|
495
|
+
);
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
if (!scoped.knownIntents.has(`${ref.namespace}/${ref.name}`)) {
|
|
499
|
+
scoped.add(
|
|
500
|
+
"mutation_bypass",
|
|
501
|
+
`${actionPath}.intent`,
|
|
502
|
+
`action "${name}" references intent "${actionRef.intent}" which no known capability declares`
|
|
503
|
+
);
|
|
504
|
+
} else if (!scoped.grantedIntents.has(actionRef.intent)) {
|
|
505
|
+
scoped.add(
|
|
506
|
+
"denied_intent_ref",
|
|
507
|
+
`${actionPath}.intent`,
|
|
508
|
+
`action "${name}" references intent "${actionRef.intent}" which is published but not granted to this release`
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
if (actionRef.params !== void 0) {
|
|
512
|
+
checkPropValues(actionRef.params, `${actionPath}.params`, scoped);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
if (fragment.props !== void 0) {
|
|
516
|
+
checkPropValues(fragment.props, `${path}.props`, scoped);
|
|
517
|
+
}
|
|
518
|
+
if (fragment.children !== void 0) {
|
|
519
|
+
fragment.children.forEach((child, index) => {
|
|
520
|
+
walkFragment(child, `${path}.children[${index}]`, scoped);
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
function checkValue(value, path, context, depth = 0) {
|
|
525
|
+
if (depth > MAX_PROP_DEPTH) {
|
|
526
|
+
context.add(
|
|
527
|
+
"invalid_fragment",
|
|
528
|
+
path,
|
|
529
|
+
`prop nesting exceeds ${MAX_PROP_DEPTH} levels; a cyclic or pathologically deep value cannot be checked`
|
|
530
|
+
);
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
if (isCapabilityRefValue(value)) {
|
|
534
|
+
const ref = parseCapabilityRef(value.$ref);
|
|
535
|
+
if (!ref) {
|
|
536
|
+
context.add(
|
|
537
|
+
"unauthorized_data_ref",
|
|
538
|
+
`${path}.$ref`,
|
|
539
|
+
`"${value.$ref}" is not a valid capability:// view reference`
|
|
540
|
+
);
|
|
541
|
+
} else if (!context.knownViews.has(`${ref.namespace}/${ref.name}`)) {
|
|
542
|
+
context.add(
|
|
543
|
+
"unauthorized_data_ref",
|
|
544
|
+
`${path}.$ref`,
|
|
545
|
+
`prop references view "${value.$ref}" which no known capability publishes`
|
|
546
|
+
);
|
|
547
|
+
} else if (!context.grantedViews.has(value.$ref)) {
|
|
548
|
+
context.add(
|
|
549
|
+
"denied_view_ref",
|
|
550
|
+
`${path}.$ref`,
|
|
551
|
+
`prop references view "${value.$ref}" which is published but not granted to this release`
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
for (const [key, sibling] of Object.entries(value)) {
|
|
555
|
+
if (key === "$ref") continue;
|
|
556
|
+
checkValue(sibling, `${path}.${key}`, context, depth + 1);
|
|
557
|
+
}
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
if (Array.isArray(value)) {
|
|
561
|
+
value.forEach((item, index) => checkValue(item, `${path}[${index}]`, context, depth + 1));
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
if (value !== null && typeof value === "object") {
|
|
565
|
+
checkPropValues(value, path, context, depth + 1);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
function checkPropValues(props, path, context, depth = 0) {
|
|
569
|
+
for (const [key, value] of Object.entries(props)) {
|
|
570
|
+
checkValue(value, `${path}.${key}`, context, depth);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// validate.ts
|
|
575
|
+
function validateSduiRelease(input) {
|
|
576
|
+
if (!input.assembly) throw new Error("An approved assembly lockfile is required for every SDUI release.");
|
|
577
|
+
assertSduiDocument(input.document);
|
|
578
|
+
assertSduiTokenSet(input.tokenSet);
|
|
579
|
+
for (const pack of input.componentPacks) assertSduiComponentPack(pack);
|
|
580
|
+
for (const contract of input.contracts) (0, import_gen_capability.assertUsageContractDocument)(contract);
|
|
581
|
+
assertSduiAuthorizationGrants(input.grants);
|
|
582
|
+
(0, import_assembly2.assertAssemblyLockfile)(input.assembly);
|
|
583
|
+
const contracts = input.contracts.map((document) => document.capability);
|
|
584
|
+
const findings = [];
|
|
585
|
+
const add = (code, path, message) => {
|
|
586
|
+
findings.push({ code, path, message });
|
|
587
|
+
};
|
|
588
|
+
if (input.document.tokenSet.name !== input.tokenSet.name) {
|
|
589
|
+
add(
|
|
590
|
+
"missing_token_set",
|
|
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) {
|
|
596
|
+
add(
|
|
597
|
+
"missing_token_set",
|
|
598
|
+
"document.tokenSet.version",
|
|
599
|
+
`document references token set version "${input.document.tokenSet.version}" but the provided set is version "${input.tokenSet.version}"`
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
const packsByKey = /* @__PURE__ */ new Map();
|
|
603
|
+
for (const pack of input.componentPacks) {
|
|
604
|
+
const key = `${pack.namespace}/${pack.pack}`;
|
|
605
|
+
const list = packsByKey.get(key) ?? [];
|
|
606
|
+
list.push(pack);
|
|
607
|
+
packsByKey.set(key, list);
|
|
608
|
+
}
|
|
609
|
+
const resolvedPacks = [];
|
|
610
|
+
const resolvedPackDefinitions = [];
|
|
611
|
+
for (const ref of input.document.componentPacks) {
|
|
612
|
+
const key = `${ref.namespace}/${ref.pack}`;
|
|
613
|
+
const available = packsByKey.get(key) ?? [];
|
|
614
|
+
const range = (0, import_assembly2.parseSemverRange)(ref.range);
|
|
615
|
+
if (!range) {
|
|
616
|
+
add(
|
|
617
|
+
"incompatible_pack",
|
|
618
|
+
`document.componentPacks.${key}`,
|
|
619
|
+
`no available version of pack "${key}" satisfies range "${ref.range}"`
|
|
620
|
+
);
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
const resolvedVersion = (0, import_assembly2.resolveVersionRange)(range, available.map((pack) => pack.version));
|
|
624
|
+
if (!resolvedVersion) {
|
|
625
|
+
add(
|
|
626
|
+
"incompatible_pack",
|
|
627
|
+
`document.componentPacks.${key}`,
|
|
628
|
+
`no available version of pack "${key}" satisfies range "${ref.range}"`
|
|
629
|
+
);
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
632
|
+
const resolved = available.find((pack) => pack.version === resolvedVersion);
|
|
633
|
+
resolvedPackDefinitions.push(resolved);
|
|
634
|
+
resolvedPacks.push({
|
|
635
|
+
pack: resolved.pack,
|
|
636
|
+
namespace: resolved.namespace,
|
|
637
|
+
version: resolved.version,
|
|
638
|
+
range: ref.range,
|
|
639
|
+
artifactDigest: resolved.artifactDigest,
|
|
640
|
+
...resolved.remote ? { remote: resolved.remote } : {}
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
{
|
|
644
|
+
for (const pack of resolvedPacks) {
|
|
645
|
+
const approved = input.assembly.componentPacks.find(
|
|
646
|
+
(candidate) => candidate.namespace === pack.namespace && candidate.pack === pack.pack && candidate.version === pack.version && candidate.artifactDigest === pack.artifactDigest
|
|
647
|
+
);
|
|
648
|
+
if (!approved) {
|
|
649
|
+
add(
|
|
650
|
+
"component_pack_not_in_assembly",
|
|
651
|
+
`componentPacks.${pack.namespace}/${pack.pack}`,
|
|
652
|
+
`pack "${pack.namespace}/${pack.pack}@${pack.version}" with artifact digest "${pack.artifactDigest}" is not in assembly "${input.assembly.assemblyDigest}"`
|
|
653
|
+
);
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
const declaredRemote = resolvedPackDefinitions.find((definition) => definition.namespace === pack.namespace && definition.pack === pack.pack && definition.version === pack.version && definition.artifactDigest === pack.artifactDigest)?.remote;
|
|
657
|
+
const remotePath = `componentPacks.${pack.namespace}/${pack.pack}.remote`;
|
|
658
|
+
if (declaredRemote && !approved.remote) {
|
|
659
|
+
add(
|
|
660
|
+
"remote_not_locked",
|
|
661
|
+
remotePath,
|
|
662
|
+
`pack "${pack.namespace}/${pack.pack}" is delivered as a federated remote, which assembly "${input.assembly.assemblyDigest}" has not locked`
|
|
663
|
+
);
|
|
664
|
+
} else if (declaredRemote && approved.remote) {
|
|
665
|
+
if (declaredRemote.entry !== approved.remote.entry) {
|
|
666
|
+
add(
|
|
667
|
+
"remote_not_locked",
|
|
668
|
+
`${remotePath}.entry`,
|
|
669
|
+
`remote entry "${declaredRemote.entry}" does not match the locked entry "${approved.remote.entry}"`
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
if (declaredRemote.integrity !== approved.remote.integrity) {
|
|
673
|
+
add(
|
|
674
|
+
"remote_not_locked",
|
|
675
|
+
`${remotePath}.integrity`,
|
|
676
|
+
`remote integrity "${declaredRemote.integrity}" does not match the locked integrity "${approved.remote.integrity}"`
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
if (canonicalExposes(declaredRemote.exposes) !== canonicalExposes(approved.remote.exposes)) {
|
|
680
|
+
add(
|
|
681
|
+
"remote_not_locked",
|
|
682
|
+
`${remotePath}.exposes`,
|
|
683
|
+
"remote exposed modules do not match the locked mapping"
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
} else if (!declaredRemote && approved.remote) {
|
|
687
|
+
add(
|
|
688
|
+
"remote_not_locked",
|
|
689
|
+
remotePath,
|
|
690
|
+
`assembly "${input.assembly.assemblyDigest}" locks a federated remote for "${pack.namespace}/${pack.pack}", but the supplied pack declares none`
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
for (const document of input.contracts) {
|
|
695
|
+
const approved = input.assembly.capabilities.find(
|
|
696
|
+
(capability) => capability.namespace === document.capability.namespace && capability.usageContractDigest === (0, import_assembly2.computeUsageContractDocumentDigest)(document)
|
|
697
|
+
);
|
|
698
|
+
if (!approved) {
|
|
699
|
+
add(
|
|
700
|
+
"capability_contract_not_in_assembly",
|
|
701
|
+
`contracts.${document.capability.namespace}`,
|
|
702
|
+
`contract "${document.capability.namespace}" is not the generated usage-contract document approved by assembly "${input.assembly.assemblyDigest}"`
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
const knownComponents = /* @__PURE__ */ new Set();
|
|
708
|
+
for (const pack of resolvedPackDefinitions) {
|
|
709
|
+
for (const name of Object.keys(pack.components)) {
|
|
710
|
+
knownComponents.add(`${pack.namespace}.${name}`);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
const coreComponents = new Set(input.coreComponents ?? []);
|
|
714
|
+
const knownViews = buildKnownViews(contracts);
|
|
715
|
+
const knownIntents = buildKnownActionIntents(contracts);
|
|
716
|
+
const grantedViews = buildGrantedReferences(input.grants.views, "grants.views");
|
|
717
|
+
const grantedIntents = buildGrantedReferences(input.grants.intents, "grants.intents");
|
|
718
|
+
validateIntentActionDeclarations(contracts, add);
|
|
719
|
+
const fragmentGrants = /* @__PURE__ */ new Map();
|
|
720
|
+
for (const [fragmentId, declared] of Object.entries(input.grants.fragments ?? {})) {
|
|
721
|
+
for (const view of declared.views ?? []) {
|
|
722
|
+
if (!grantedViews.has(view)) {
|
|
723
|
+
add(
|
|
724
|
+
"fragment_grant_exceeds_release",
|
|
725
|
+
`grants.fragments.${fragmentId}.views`,
|
|
726
|
+
`fragment "${fragmentId}" is granted view "${view}", which the release itself does not grant`
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
for (const intent of declared.intents ?? []) {
|
|
731
|
+
if (!grantedIntents.has(intent)) {
|
|
732
|
+
add(
|
|
733
|
+
"fragment_grant_exceeds_release",
|
|
734
|
+
`grants.fragments.${fragmentId}.intents`,
|
|
735
|
+
`fragment "${fragmentId}" is granted intent "${intent}", which the release itself does not grant`
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
fragmentGrants.set(fragmentId, declared);
|
|
740
|
+
}
|
|
741
|
+
const walkTree = (root, path) => {
|
|
742
|
+
walkFragment(root, path, {
|
|
743
|
+
ids: /* @__PURE__ */ new Set(),
|
|
744
|
+
knownComponents,
|
|
745
|
+
knownViews,
|
|
746
|
+
grantedViews,
|
|
747
|
+
knownIntents,
|
|
748
|
+
grantedIntents,
|
|
749
|
+
fragmentGrants,
|
|
750
|
+
coreComponents,
|
|
751
|
+
findings,
|
|
752
|
+
add
|
|
753
|
+
});
|
|
754
|
+
};
|
|
755
|
+
walkTree(input.document.root, "document.root");
|
|
756
|
+
const seenVariantIds = /* @__PURE__ */ new Set();
|
|
757
|
+
(input.document.variants ?? []).forEach((variant, index) => {
|
|
758
|
+
if (seenVariantIds.has(variant.id)) {
|
|
759
|
+
add(
|
|
760
|
+
"duplicate_variant_id",
|
|
761
|
+
`document.variants[${index}].id`,
|
|
762
|
+
`variant id "${variant.id}" is declared more than once`
|
|
763
|
+
);
|
|
764
|
+
} else {
|
|
765
|
+
seenVariantIds.add(variant.id);
|
|
766
|
+
}
|
|
767
|
+
walkTree(variant.root, `document.variants[${index}].root`);
|
|
768
|
+
});
|
|
769
|
+
if (findings.length > 0) {
|
|
770
|
+
return { valid: false, findings, release: void 0 };
|
|
771
|
+
}
|
|
772
|
+
const release = {
|
|
773
|
+
formatVersion: 2,
|
|
774
|
+
releaseDigest: releaseDigest(
|
|
775
|
+
input.document,
|
|
776
|
+
input.tokenSet,
|
|
777
|
+
resolvedPacks,
|
|
778
|
+
input.grants,
|
|
779
|
+
input.assembly.assemblyDigest
|
|
780
|
+
),
|
|
781
|
+
assemblyDigest: input.assembly.assemblyDigest,
|
|
782
|
+
document: input.document,
|
|
783
|
+
tokenSet: input.tokenSet,
|
|
784
|
+
componentPacks: [...resolvedPacks].sort((a, b) => {
|
|
785
|
+
const key = (e) => `${e.namespace}/${e.pack}`;
|
|
786
|
+
return key(a) < key(b) ? -1 : key(a) > key(b) ? 1 : 0;
|
|
787
|
+
}),
|
|
788
|
+
grants: {
|
|
789
|
+
views: [...input.grants.views].sort(),
|
|
790
|
+
intents: [...input.grants.intents].sort(),
|
|
791
|
+
...input.grants.fragments ? { fragments: canonicalFragmentGrants(input.grants.fragments) } : {}
|
|
792
|
+
}
|
|
793
|
+
};
|
|
794
|
+
return { valid: true, findings: [], release };
|
|
795
|
+
}
|
|
796
|
+
function assertSduiReleaseValid(input) {
|
|
797
|
+
const result = validateSduiRelease(input);
|
|
798
|
+
if (result.valid) return result.release;
|
|
799
|
+
throw new Error([
|
|
800
|
+
`SDUI release "${input.document.name}@${input.document.version}" is not valid:`,
|
|
801
|
+
...result.findings.map((finding) => ` - [${finding.code}] ${finding.path}: ${finding.message}`)
|
|
802
|
+
].join("\n"));
|
|
803
|
+
}
|
|
804
|
+
function canonicalExposes(exposes) {
|
|
805
|
+
return JSON.stringify(
|
|
806
|
+
Object.keys(exposes).sort().map((component) => [component, exposes[component]])
|
|
807
|
+
);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// channel.ts
|
|
811
|
+
function createSduiChannel(id, coreComponents) {
|
|
812
|
+
return {
|
|
813
|
+
id,
|
|
814
|
+
coreComponents,
|
|
815
|
+
consume(release) {
|
|
816
|
+
assertSduiReleaseIntegrity(release);
|
|
817
|
+
const used = collectCoreComponents(release.document.root);
|
|
818
|
+
for (const variant of release.document.variants ?? []) {
|
|
819
|
+
for (const component of collectCoreComponents(variant.root)) used.add(component);
|
|
820
|
+
}
|
|
821
|
+
const missing = [...used].filter((component) => !coreComponents.includes(component));
|
|
822
|
+
return {
|
|
823
|
+
release,
|
|
824
|
+
compatible: missing.length === 0,
|
|
825
|
+
missingCoreComponents: missing
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
function collectCoreComponents(fragment) {
|
|
831
|
+
const used = /* @__PURE__ */ new Set();
|
|
832
|
+
if (fragment.component !== void 0 && !fragment.component.includes(".")) {
|
|
833
|
+
used.add(fragment.component);
|
|
834
|
+
}
|
|
835
|
+
if (fragment.children !== void 0) {
|
|
836
|
+
for (const child of fragment.children) {
|
|
837
|
+
for (const component of collectCoreComponents(child)) used.add(component);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
return used;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// schemas.ts
|
|
844
|
+
var SDUI_DOCUMENT_JSON_SCHEMA = {
|
|
845
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
846
|
+
title: "Fabric SDUI document",
|
|
847
|
+
type: "object",
|
|
848
|
+
required: ["formatVersion", "name", "version", "root", "tokenSet", "componentPacks"],
|
|
849
|
+
properties: {
|
|
850
|
+
formatVersion: { type: "integer", enum: [1] },
|
|
851
|
+
name: { type: "string", pattern: "^[a-z][a-z0-9-]*$" },
|
|
852
|
+
version: { type: "string", pattern: "^\\d+\\.\\d+\\.\\d+" },
|
|
853
|
+
root: { type: "object" },
|
|
854
|
+
tokenSet: { type: "object", required: ["name", "version"] },
|
|
855
|
+
componentPacks: { type: "array" }
|
|
856
|
+
}
|
|
857
|
+
};
|
|
858
|
+
var SDUI_RELEASE_JSON_SCHEMA = {
|
|
859
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
860
|
+
title: "Fabric SDUI promoted release",
|
|
861
|
+
type: "object",
|
|
862
|
+
required: ["formatVersion", "releaseDigest", "assemblyDigest", "document", "tokenSet", "componentPacks", "grants"],
|
|
863
|
+
properties: {
|
|
864
|
+
formatVersion: { type: "integer", enum: [2] },
|
|
865
|
+
releaseDigest: { type: "string", pattern: "^[0-9a-f]{64}$" },
|
|
866
|
+
assemblyDigest: { type: "string", pattern: "^[0-9a-f]{64}$" },
|
|
867
|
+
document: { type: "object" },
|
|
868
|
+
tokenSet: { type: "object" },
|
|
869
|
+
componentPacks: { type: "array" },
|
|
870
|
+
grants: { type: "object", required: ["views", "intents"] }
|
|
871
|
+
}
|
|
872
|
+
};
|
|
873
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
874
|
+
0 && (module.exports = {
|
|
875
|
+
SDUI_COMPONENT_PACK_FORMAT_VERSION,
|
|
876
|
+
SDUI_DOCUMENT_FORMAT_VERSION,
|
|
877
|
+
SDUI_DOCUMENT_JSON_SCHEMA,
|
|
878
|
+
SDUI_RELEASE_FORMAT_VERSION,
|
|
879
|
+
SDUI_RELEASE_JSON_SCHEMA,
|
|
880
|
+
SDUI_TOKEN_SET_FORMAT_VERSION,
|
|
881
|
+
assertSduiAuthorizationGrants,
|
|
882
|
+
assertSduiComponentPack,
|
|
883
|
+
assertSduiDocument,
|
|
884
|
+
assertSduiFragment,
|
|
885
|
+
assertSduiRelease,
|
|
886
|
+
assertSduiReleaseIntegrity,
|
|
887
|
+
assertSduiReleaseValid,
|
|
888
|
+
assertSduiTokenSet,
|
|
889
|
+
computeReleaseDigest,
|
|
890
|
+
createSduiChannel,
|
|
891
|
+
validateSduiRelease
|
|
892
|
+
});
|
|
893
|
+
//# sourceMappingURL=index.cjs.map
|