@kumwe/studio-core 0.1.0-alpha.8 → 0.1.0-beta.2
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/README.md +84 -4
- package/THIRD_PARTY_NOTICES.md +9 -0
- package/dist/authentication-lifetime.d.ts +16 -0
- package/dist/authentication-lifetime.d.ts.map +1 -0
- package/dist/authentication-lifetime.js +1 -0
- package/dist/binding-projection.d.ts +47 -0
- package/dist/binding-projection.d.ts.map +1 -0
- package/dist/binding-projection.js +1 -0
- package/dist/canonical.js +1 -100
- package/dist/clone.js +1 -4
- package/dist/commands.d.ts +1 -1
- package/dist/commands.d.ts.map +1 -1
- package/dist/commands.js +1 -799
- package/dist/contextual-session.d.ts +112 -0
- package/dist/contextual-session.d.ts.map +1 -0
- package/dist/contextual-session.js +1 -0
- package/dist/contributions.d.ts +63 -2
- package/dist/contributions.d.ts.map +1 -1
- package/dist/contributions.js +1 -302
- package/dist/deployment-validation.d.ts +10 -0
- package/dist/deployment-validation.d.ts.map +1 -0
- package/dist/deployment-validation.js +1 -0
- package/dist/entry-commands.js +1 -36
- package/dist/extension-sdk.d.ts +12 -6
- package/dist/extension-sdk.d.ts.map +1 -1
- package/dist/extension-sdk.js +1 -188
- package/dist/history.d.ts +12 -2
- package/dist/history.d.ts.map +1 -1
- package/dist/history.js +1 -63
- package/dist/host-session.d.ts +93 -0
- package/dist/host-session.d.ts.map +1 -0
- package/dist/host-session.js +1 -0
- package/dist/http-authoring-contract.d.ts +29 -0
- package/dist/http-authoring-contract.d.ts.map +1 -0
- package/dist/http-authoring-contract.js +1 -0
- package/dist/http-host-adapter.d.ts +173 -0
- package/dist/http-host-adapter.d.ts.map +1 -0
- package/dist/http-host-adapter.js +1 -0
- package/dist/index.d.ts +13 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -18
- package/dist/layout.d.ts +66 -0
- package/dist/layout.d.ts.map +1 -0
- package/dist/layout.js +1 -0
- package/dist/migrations.js +1 -83
- package/dist/model-commands.js +1 -21
- package/dist/modes.js +1 -280
- package/dist/negotiation.js +1 -65
- package/dist/production-values.d.ts +12 -0
- package/dist/production-values.d.ts.map +1 -0
- package/dist/production-values.js +1 -0
- package/dist/production.d.ts +86 -0
- package/dist/production.d.ts.map +1 -0
- package/dist/production.js +1 -0
- package/dist/profile-validator.d.ts.map +1 -1
- package/dist/profile-validator.js +1 -887
- package/dist/recipes.js +1 -34
- package/dist/registry.js +1 -56
- package/dist/schema-profile.js +1 -1015
- package/dist/semver.js +1 -128
- package/dist/session-policy.d.ts +23 -0
- package/dist/session-policy.d.ts.map +1 -0
- package/dist/session-policy.js +1 -0
- package/dist/session.d.ts +14 -2
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +1 -178
- package/dist/strict-json.d.ts +7 -0
- package/dist/strict-json.d.ts.map +1 -0
- package/dist/strict-json.js +2 -0
- package/dist/url-policy.js +1 -248
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +1 -398
- package/package.json +5 -3
- package/dist/canonical.js.map +0 -1
- package/dist/clone.js.map +0 -1
- package/dist/commands.js.map +0 -1
- package/dist/contributions.js.map +0 -1
- package/dist/entry-commands.js.map +0 -1
- package/dist/extension-sdk.js.map +0 -1
- package/dist/history.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/migrations.js.map +0 -1
- package/dist/model-commands.js.map +0 -1
- package/dist/modes.js.map +0 -1
- package/dist/negotiation.js.map +0 -1
- package/dist/profile-validator.js.map +0 -1
- package/dist/recipes.js.map +0 -1
- package/dist/registry.js.map +0 -1
- package/dist/schema-profile.js.map +0 -1
- package/dist/semver.js.map +0 -1
- package/dist/session.js.map +0 -1
- package/dist/url-policy.js.map +0 -1
- package/dist/validation.js.map +0 -1
package/dist/url-policy.js
CHANGED
|
@@ -1,248 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* Canonical policy for external resource URLs (remote media imports, embeds).
|
|
3
|
-
* Studio never fetches author-supplied URLs itself; hosts that do MUST apply
|
|
4
|
-
* this validator before any fetch. The policy is purely lexical and
|
|
5
|
-
* deterministic — it classifies the WHATWG-parsed URL without network access,
|
|
6
|
-
* DNS resolution, clocks, or locale sensitivity. DNS-rebinding defence,
|
|
7
|
-
* redirect re-validation, and response verification therefore remain host
|
|
8
|
-
* runtime obligations of the hardened fetcher.
|
|
9
|
-
*/
|
|
10
|
-
export const STUDIO_DEFAULT_URL_POLICY = Object.freeze({
|
|
11
|
-
allowedSchemes: Object.freeze(['https:']),
|
|
12
|
-
allowPrivateHosts: false,
|
|
13
|
-
maxLength: 2_048,
|
|
14
|
-
});
|
|
15
|
-
/**
|
|
16
|
-
* Validate one external resource URL against the policy. The candidate is
|
|
17
|
-
* parsed with the WHATWG URL parser and classification operates on the parsed
|
|
18
|
-
* `hostname`, so decimal, octal, hexadecimal, and dotted-partial IPv4
|
|
19
|
-
* spellings (`2130706433`, `0x7f000001`, `017700000001`, `127.1`) are
|
|
20
|
-
* normalized before range checks and cannot smuggle a private address past
|
|
21
|
-
* the policy. Punycode and IDN hosts that name none of the disallowed ranges
|
|
22
|
-
* are accepted lexically; whether such a name ultimately resolves to a
|
|
23
|
-
* private address is a DNS-time question the host fetcher must re-check.
|
|
24
|
-
*/
|
|
25
|
-
export function validateExternalUrl(candidate, policy = STUDIO_DEFAULT_URL_POLICY) {
|
|
26
|
-
if (candidate.length > policy.maxLength) {
|
|
27
|
-
return { ok: false, reason: 'url-too-long' };
|
|
28
|
-
}
|
|
29
|
-
let parsed;
|
|
30
|
-
try {
|
|
31
|
-
parsed = new URL(candidate);
|
|
32
|
-
}
|
|
33
|
-
catch {
|
|
34
|
-
return { ok: false, reason: 'malformed' };
|
|
35
|
-
}
|
|
36
|
-
if (!policy.allowedSchemes.includes(parsed.protocol)) {
|
|
37
|
-
return { ok: false, reason: 'scheme-not-allowed' };
|
|
38
|
-
}
|
|
39
|
-
if (parsed.username.length > 0 || parsed.password.length > 0) {
|
|
40
|
-
return { ok: false, reason: 'credentials-in-url' };
|
|
41
|
-
}
|
|
42
|
-
if (!isPermittedHost(parsed.hostname, policy.allowPrivateHosts)) {
|
|
43
|
-
return { ok: false, reason: 'host-not-allowed' };
|
|
44
|
-
}
|
|
45
|
-
return { ok: true, url: parsed.href };
|
|
46
|
-
}
|
|
47
|
-
function isPermittedHost(hostname, allowPrivateHosts) {
|
|
48
|
-
if (hostname.length === 0) {
|
|
49
|
-
return false;
|
|
50
|
-
}
|
|
51
|
-
if (allowPrivateHosts) {
|
|
52
|
-
return true;
|
|
53
|
-
}
|
|
54
|
-
const trimmed = hostname.endsWith('.') ? hostname.slice(0, -1) : hostname;
|
|
55
|
-
if (trimmed.length === 0) {
|
|
56
|
-
return false;
|
|
57
|
-
}
|
|
58
|
-
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
|
|
59
|
-
const groups = parseIpv6Groups(trimmed.slice(1, -1));
|
|
60
|
-
return groups !== undefined && !isDisallowedIpv6(groups);
|
|
61
|
-
}
|
|
62
|
-
const host = trimmed.toLowerCase();
|
|
63
|
-
if (host === 'localhost' || host.endsWith('.localhost')) {
|
|
64
|
-
return false;
|
|
65
|
-
}
|
|
66
|
-
if (host.endsWith('.local') || host.endsWith('.internal') || host.endsWith('.home.arpa')) {
|
|
67
|
-
return false;
|
|
68
|
-
}
|
|
69
|
-
const address = parseIpv4Host(host);
|
|
70
|
-
if (address !== undefined) {
|
|
71
|
-
return !isDisallowedIpv4(address);
|
|
72
|
-
}
|
|
73
|
-
return true;
|
|
74
|
-
}
|
|
75
|
-
function isDisallowedIpv4(address) {
|
|
76
|
-
const octet1 = Math.floor(address / 16_777_216) % 256;
|
|
77
|
-
const octet2 = Math.floor(address / 65_536) % 256;
|
|
78
|
-
return (address === 0 || // 0.0.0.0 unspecified
|
|
79
|
-
address === 4_294_967_295 || // 255.255.255.255 broadcast
|
|
80
|
-
octet1 === 127 || // 127.0.0.0/8 loopback
|
|
81
|
-
octet1 === 10 || // 10.0.0.0/8 private
|
|
82
|
-
(octet1 === 172 && octet2 >= 16 && octet2 <= 31) || // 172.16.0.0/12 private
|
|
83
|
-
(octet1 === 192 && octet2 === 168) || // 192.168.0.0/16 private
|
|
84
|
-
(octet1 === 169 && octet2 === 254) || // 169.254.0.0/16 link-local
|
|
85
|
-
(octet1 === 100 && octet2 >= 64 && octet2 <= 127) // 100.64.0.0/10 CGNAT
|
|
86
|
-
);
|
|
87
|
-
}
|
|
88
|
-
function isDisallowedIpv6(groups) {
|
|
89
|
-
const group = (index) => groups[index] ?? 0;
|
|
90
|
-
if (groups.every((piece) => piece === 0)) {
|
|
91
|
-
return true; // :: unspecified
|
|
92
|
-
}
|
|
93
|
-
if (groups.slice(0, 7).every((piece) => piece === 0) && group(7) === 1) {
|
|
94
|
-
return true; // ::1 loopback
|
|
95
|
-
}
|
|
96
|
-
if ((group(0) & 0xff_c0) === 0xfe_80) {
|
|
97
|
-
return true; // fe80::/10 link-local
|
|
98
|
-
}
|
|
99
|
-
if ((group(0) & 0xfe_00) === 0xfc_00) {
|
|
100
|
-
return true; // fc00::/7 unique-local
|
|
101
|
-
}
|
|
102
|
-
if (groups.slice(0, 5).every((piece) => piece === 0) &&
|
|
103
|
-
(group(5) === 0xff_ff || group(5) === 0)) {
|
|
104
|
-
// ::ffff:0:0/96 IPv4-mapped and the deprecated ::/96 IPv4-compatible
|
|
105
|
-
// block classify as their embedded IPv4 address.
|
|
106
|
-
return isDisallowedIpv4(group(6) * 65_536 + group(7));
|
|
107
|
-
}
|
|
108
|
-
return false;
|
|
109
|
-
}
|
|
110
|
-
/**
|
|
111
|
-
* Interpret a hostname the way the WHATWG IPv4 host parser does: up to four
|
|
112
|
-
* dot-separated numbers in decimal, octal (leading `0`), or hexadecimal
|
|
113
|
-
* (leading `0x`), where the final number may span the remaining octets.
|
|
114
|
-
* Returns the 32-bit address, or `undefined` when the host is not an IPv4
|
|
115
|
-
* candidate and must be treated as a name.
|
|
116
|
-
*/
|
|
117
|
-
function parseIpv4Host(host) {
|
|
118
|
-
if (!/^[0-9A-Fa-fXx.]+$/u.test(host)) {
|
|
119
|
-
return undefined;
|
|
120
|
-
}
|
|
121
|
-
const parts = host.split('.');
|
|
122
|
-
if (parts.length > 4) {
|
|
123
|
-
return undefined;
|
|
124
|
-
}
|
|
125
|
-
const numbers = [];
|
|
126
|
-
for (const part of parts) {
|
|
127
|
-
const value = parseIpv4Number(part);
|
|
128
|
-
if (value === undefined) {
|
|
129
|
-
return undefined;
|
|
130
|
-
}
|
|
131
|
-
numbers.push(value);
|
|
132
|
-
}
|
|
133
|
-
const last = numbers[numbers.length - 1];
|
|
134
|
-
if (last === undefined) {
|
|
135
|
-
return undefined;
|
|
136
|
-
}
|
|
137
|
-
const prefix = numbers.slice(0, -1);
|
|
138
|
-
if (prefix.some((octet) => octet > 255) || last > 256 ** (4 - prefix.length) - 1) {
|
|
139
|
-
return undefined;
|
|
140
|
-
}
|
|
141
|
-
let address = last;
|
|
142
|
-
for (let index = 0; index < prefix.length; index += 1) {
|
|
143
|
-
address += (prefix[index] ?? 0) * 256 ** (3 - index);
|
|
144
|
-
}
|
|
145
|
-
return address;
|
|
146
|
-
}
|
|
147
|
-
function parseIpv4Number(part) {
|
|
148
|
-
if (part.length === 0) {
|
|
149
|
-
return undefined;
|
|
150
|
-
}
|
|
151
|
-
let digits = part;
|
|
152
|
-
let radix = 10;
|
|
153
|
-
if (digits.length >= 2 && (digits.startsWith('0x') || digits.startsWith('0X'))) {
|
|
154
|
-
digits = digits.slice(2);
|
|
155
|
-
radix = 16;
|
|
156
|
-
if (digits.length === 0) {
|
|
157
|
-
return 0;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
else if (digits.length >= 2 && digits.startsWith('0')) {
|
|
161
|
-
digits = digits.slice(1);
|
|
162
|
-
radix = 8;
|
|
163
|
-
}
|
|
164
|
-
const pattern = radix === 16 ? /^[0-9A-Fa-f]+$/u : radix === 8 ? /^[0-7]+$/u : /^[0-9]+$/u;
|
|
165
|
-
if (!pattern.test(digits)) {
|
|
166
|
-
return undefined;
|
|
167
|
-
}
|
|
168
|
-
const value = Number.parseInt(digits, radix);
|
|
169
|
-
return value > 4_294_967_295 ? undefined : value;
|
|
170
|
-
}
|
|
171
|
-
/**
|
|
172
|
-
* Parse a bracket-stripped IPv6 literal into its eight 16-bit groups,
|
|
173
|
-
* expanding one `::` compression and accepting a trailing dotted-quad
|
|
174
|
-
* (IPv4-in-IPv6) suffix. Returns `undefined` for anything else so callers
|
|
175
|
-
* fail closed on hosts this parser cannot classify.
|
|
176
|
-
*/
|
|
177
|
-
function parseIpv6Groups(literal) {
|
|
178
|
-
if (literal.length === 0 || literal.includes('%')) {
|
|
179
|
-
return undefined;
|
|
180
|
-
}
|
|
181
|
-
const marker = literal.indexOf('::');
|
|
182
|
-
if (marker === -1) {
|
|
183
|
-
const pieces = parseIpv6Pieces(literal, true);
|
|
184
|
-
return pieces?.length === 8 ? pieces : undefined;
|
|
185
|
-
}
|
|
186
|
-
if (literal.includes('::', marker + 1)) {
|
|
187
|
-
return undefined;
|
|
188
|
-
}
|
|
189
|
-
const head = parseIpv6Pieces(literal.slice(0, marker), false);
|
|
190
|
-
const tail = parseIpv6Pieces(literal.slice(marker + 2), true);
|
|
191
|
-
if (head === undefined || tail === undefined) {
|
|
192
|
-
return undefined;
|
|
193
|
-
}
|
|
194
|
-
const missing = 8 - head.length - tail.length;
|
|
195
|
-
if (missing < 1) {
|
|
196
|
-
return undefined;
|
|
197
|
-
}
|
|
198
|
-
const zeros = [];
|
|
199
|
-
for (let index = 0; index < missing; index += 1) {
|
|
200
|
-
zeros.push(0);
|
|
201
|
-
}
|
|
202
|
-
return [...head, ...zeros, ...tail];
|
|
203
|
-
}
|
|
204
|
-
function parseIpv6Pieces(text, allowIpv4Suffix) {
|
|
205
|
-
if (text.length === 0) {
|
|
206
|
-
return [];
|
|
207
|
-
}
|
|
208
|
-
const parts = text.split(':');
|
|
209
|
-
const pieces = [];
|
|
210
|
-
for (let index = 0; index < parts.length; index += 1) {
|
|
211
|
-
const part = parts[index];
|
|
212
|
-
if (part === undefined || part.length === 0) {
|
|
213
|
-
return undefined;
|
|
214
|
-
}
|
|
215
|
-
if (allowIpv4Suffix && index === parts.length - 1 && part.includes('.')) {
|
|
216
|
-
const embedded = parseDottedQuad(part);
|
|
217
|
-
if (embedded === undefined) {
|
|
218
|
-
return undefined;
|
|
219
|
-
}
|
|
220
|
-
pieces.push(Math.floor(embedded / 65_536), embedded % 65_536);
|
|
221
|
-
continue;
|
|
222
|
-
}
|
|
223
|
-
if (!/^[0-9A-Fa-f]{1,4}$/u.test(part)) {
|
|
224
|
-
return undefined;
|
|
225
|
-
}
|
|
226
|
-
pieces.push(Number.parseInt(part, 16));
|
|
227
|
-
}
|
|
228
|
-
return pieces;
|
|
229
|
-
}
|
|
230
|
-
function parseDottedQuad(text) {
|
|
231
|
-
const parts = text.split('.');
|
|
232
|
-
if (parts.length !== 4) {
|
|
233
|
-
return undefined;
|
|
234
|
-
}
|
|
235
|
-
let address = 0;
|
|
236
|
-
for (const part of parts) {
|
|
237
|
-
if (!/^(?:0|[1-9][0-9]{0,2})$/u.test(part)) {
|
|
238
|
-
return undefined;
|
|
239
|
-
}
|
|
240
|
-
const octet = Number(part);
|
|
241
|
-
if (octet > 255) {
|
|
242
|
-
return undefined;
|
|
243
|
-
}
|
|
244
|
-
address = address * 256 + octet;
|
|
245
|
-
}
|
|
246
|
-
return address;
|
|
247
|
-
}
|
|
248
|
-
//# sourceMappingURL=url-policy.js.map
|
|
1
|
+
export const STUDIO_DEFAULT_URL_POLICY=Object.freeze({allowedSchemes:Object.freeze([`https:`]),allowPrivateHosts:!1,maxLength:2048});export function validateExternalUrl(t,r=STUDIO_DEFAULT_URL_POLICY){if(t.length>r.maxLength)return{ok:!1,reason:`url-too-long`};let i;try{i=new URL(t)}catch{return{ok:!1,reason:`malformed`}}return r.allowedSchemes.includes(i.protocol)?i.username.length>0||i.password.length>0?{ok:!1,reason:`credentials-in-url`}:n(i.hostname,r.allowPrivateHosts)?{ok:!0,url:i.href}:{ok:!1,reason:`host-not-allowed`}:{ok:!1,reason:`scheme-not-allowed`}}function n(e,t){if(e.length===0)return!1;if(t)return!0;let n=e.endsWith(`.`)?e.slice(0,-1):e;if(n.length===0)return!1;if(n.startsWith(`[`)&&n.endsWith(`]`)){let e=s(n.slice(1,-1));return e!==void 0&&!i(e)}let o=n.toLowerCase();if(o===`localhost`||o.endsWith(`.localhost`)||o.endsWith(`.local`)||o.endsWith(`.internal`)||o.endsWith(`.home.arpa`))return!1;let c=a(o);return c===void 0||!r(c)}function r(e){let t=Math.floor(e/16777216)%256,n=Math.floor(e/65536)%256;return e===0||e===4294967295||t===127||t===10||t===172&&n>=16&&n<=31||t===192&&n===168||t===169&&n===254||t===100&&n>=64&&n<=127}function i(e){let t=t=>e[t]??0;return e.every(e=>e===0)||e.slice(0,7).every(e=>e===0)&&t(7)===1||(t(0)&65472)==65152||(t(0)&65024)==64512?!0:e.slice(0,5).every(e=>e===0)&&(t(5)===65535||t(5)===0)?r(t(6)*65536+t(7)):!1}function a(e){if(!/^[0-9A-Fa-fXx.]+$/u.test(e))return;let t=e.split(`.`);if(t.length>4)return;let n=[];for(let e of t){let t=o(e);if(t===void 0)return;n.push(t)}let r=n[n.length-1];if(r===void 0)return;let i=n.slice(0,-1);if(i.some(e=>e>255)||r>256**(4-i.length)-1)return;let a=r;for(let e=0;e<i.length;e+=1)a+=(i[e]??0)*256**(3-e);return a}function o(e){if(e.length===0)return;let t=e,n=10;if(t.length>=2&&(t.startsWith(`0x`)||t.startsWith(`0X`))){if(t=t.slice(2),n=16,t.length===0)return 0}else t.length>=2&&t.startsWith(`0`)&&(t=t.slice(1),n=8);if(!(n===16?/^[0-9A-Fa-f]+$/u:n===8?/^[0-7]+$/u:/^[0-9]+$/u).test(t))return;let r=Number.parseInt(t,n);return r>4294967295?void 0:r}function s(e){if(e.length===0||e.includes(`%`))return;let t=e.indexOf(`::`);if(t===-1){let t=c(e,!0);return t?.length===8?t:void 0}if(e.includes(`::`,t+1))return;let n=c(e.slice(0,t),!1),r=c(e.slice(t+2),!0);if(n===void 0||r===void 0)return;let i=8-n.length-r.length;if(i<1)return;let a=[];for(let e=0;e<i;e+=1)a.push(0);return[...n,...a,...r]}function c(e,t){if(e.length===0)return[];let n=e.split(`:`),r=[];for(let e=0;e<n.length;e+=1){let i=n[e];if(i===void 0||i.length===0)return;if(t&&e===n.length-1&&i.includes(`.`)){let e=l(i);if(e===void 0)return;r.push(Math.floor(e/65536),e%65536);continue}if(!/^[0-9A-Fa-f]{1,4}$/u.test(i))return;r.push(Number.parseInt(i,16))}return r}function l(e){let t=e.split(`.`);if(t.length!==4)return;let n=0;for(let e of t){if(!/^(?:0|[1-9][0-9]{0,2})$/u.test(e))return;let t=Number(e);if(t>255)return;n=n*256+t}return n}
|
package/dist/validation.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"AAAA,OAAO,EASL,KAAK,gBAAgB,EACtB,MAAM,wBAAwB,CAAC;AAMhC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAEnD,MAAM,WAAW,0BAA0B;IACzC,UAAU,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACjC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,yBAAyB;IACxC,WAAW,EAAE,gBAAgB,EAAE,CAAC;IAChC,KAAK,EAAE,OAAO,CAAC;CAChB;AA8BD,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,OAAO,EACjB,QAAQ,EAAE,aAAa,EACvB,OAAO,GAAE,0BAA+B,GACvC,yBAAyB,CA+G3B"}
|
package/dist/validation.js
CHANGED
|
@@ -1,398 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { compileProfileSchema, } from './profile-validator.js';
|
|
3
|
-
// The canonical Blueprint schema is interpreted, not code-generated, so
|
|
4
|
-
// boot-path validation stays eval-free and runs under a CSP that forbids
|
|
5
|
-
// string-to-code compilation (TH-013).
|
|
6
|
-
const validateBlueprintSchema = compileProfileSchema(blueprintSchema, {
|
|
7
|
-
schemas: [commonSchema],
|
|
8
|
-
});
|
|
9
|
-
const propertyValidators = new WeakMap();
|
|
10
|
-
const MAX_BLUEPRINT_JSON_BYTES = 16 * 1_024 * 1_024;
|
|
11
|
-
const MAX_BLUEPRINT_JSON_DEPTH = 64;
|
|
12
|
-
const MAX_BLUEPRINT_JSON_VALUES = 1_000_000;
|
|
13
|
-
const MAX_JSON_CONTAINER_ENTRIES = 10_000;
|
|
14
|
-
export function validateBlueprint(document, registry, options = {}) {
|
|
15
|
-
const diagnostics = [];
|
|
16
|
-
const maximumDepth = validationLimit(options.maximumDepth, 32, 'maximumDepth');
|
|
17
|
-
const maximumNodes = validationLimit(options.maximumNodes, 5_000, 'maximumNodes');
|
|
18
|
-
const nodePreflightDiagnostics = preflightBlueprintNodes(document, maximumDepth, maximumNodes);
|
|
19
|
-
if (nodePreflightDiagnostics.length > 0) {
|
|
20
|
-
return { diagnostics: nodePreflightDiagnostics, valid: false };
|
|
21
|
-
}
|
|
22
|
-
const valuePreflightDiagnostic = preflightBlueprintJson(document);
|
|
23
|
-
if (valuePreflightDiagnostic !== undefined) {
|
|
24
|
-
return { diagnostics: [valuePreflightDiagnostic], valid: false };
|
|
25
|
-
}
|
|
26
|
-
if (!validateBlueprintSchema.validate(document)) {
|
|
27
|
-
diagnostics.push(...schemaDiagnostics(validateBlueprintSchema.errors));
|
|
28
|
-
return { diagnostics, valid: false };
|
|
29
|
-
}
|
|
30
|
-
const blueprint = document;
|
|
31
|
-
const identifiers = new Set();
|
|
32
|
-
const blockLocks = indexBlockLocks(blueprint.dependencyLock.blocks, diagnostics);
|
|
33
|
-
let nodeCount = 0;
|
|
34
|
-
const stack = blueprint.roots.map((node) => ({ depth: 1, node })).reverse();
|
|
35
|
-
while (stack.length > 0) {
|
|
36
|
-
const frame = stack.pop();
|
|
37
|
-
if (frame === undefined) {
|
|
38
|
-
break;
|
|
39
|
-
}
|
|
40
|
-
const { depth, node } = frame;
|
|
41
|
-
nodeCount += 1;
|
|
42
|
-
if (nodeCount > maximumNodes) {
|
|
43
|
-
break;
|
|
44
|
-
}
|
|
45
|
-
if (depth > maximumDepth) {
|
|
46
|
-
diagnostics.push(diagnostic('maximum-depth', `Node depth exceeds the configured limit of ${maximumDepth}.`, node.id));
|
|
47
|
-
continue;
|
|
48
|
-
}
|
|
49
|
-
if (identifiers.has(node.id)) {
|
|
50
|
-
diagnostics.push(diagnostic('duplicate-node-id', `Node identifier ${node.id} is not unique.`, node.id));
|
|
51
|
-
}
|
|
52
|
-
identifiers.add(node.id);
|
|
53
|
-
const registration = registry.resolveRegistration(node.type, node.version);
|
|
54
|
-
if (registration === undefined) {
|
|
55
|
-
diagnostics.push(diagnostic('block-unavailable', `Block ${node.type}@${node.version} is not registered.`, node.id));
|
|
56
|
-
}
|
|
57
|
-
else {
|
|
58
|
-
validateBlockLock(node, registration.definition, registration.verifiedIntegrity, blockLocks.get(blockKey(node.type, node.version)), diagnostics);
|
|
59
|
-
validateNodeProperties(node, registration.definition, registry, diagnostics);
|
|
60
|
-
validateSlots(node, registration.definition, diagnostics);
|
|
61
|
-
}
|
|
62
|
-
validateBindings(node, options.fieldPaths, diagnostics);
|
|
63
|
-
const childCollections = Object.values(node.slots);
|
|
64
|
-
for (let collectionIndex = childCollections.length - 1; collectionIndex >= 0; collectionIndex -= 1) {
|
|
65
|
-
const children = childCollections[collectionIndex];
|
|
66
|
-
if (children === undefined) {
|
|
67
|
-
continue;
|
|
68
|
-
}
|
|
69
|
-
for (let childIndex = children.length - 1; childIndex >= 0; childIndex -= 1) {
|
|
70
|
-
const child = children[childIndex];
|
|
71
|
-
if (child !== undefined) {
|
|
72
|
-
stack.push({ depth: depth + 1, node: child });
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
if (nodeCount > maximumNodes) {
|
|
78
|
-
diagnostics.push(diagnostic('maximum-nodes', `Blueprint contains more than the configured limit of ${maximumNodes} nodes.`));
|
|
79
|
-
}
|
|
80
|
-
return {
|
|
81
|
-
diagnostics,
|
|
82
|
-
valid: diagnostics.every((entry) => entry.severity !== 'blocking' && entry.severity !== 'error'),
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
function preflightBlueprintNodes(document, maximumDepth, maximumNodes) {
|
|
86
|
-
if (!isRecord(document) || !Array.isArray(document.roots)) {
|
|
87
|
-
return [];
|
|
88
|
-
}
|
|
89
|
-
const roots = document.roots;
|
|
90
|
-
if (roots.length > maximumNodes) {
|
|
91
|
-
return [
|
|
92
|
-
diagnostic('maximum-nodes', `Blueprint contains more than the configured limit of ${maximumNodes} nodes.`),
|
|
93
|
-
];
|
|
94
|
-
}
|
|
95
|
-
const seen = new WeakSet();
|
|
96
|
-
let scheduled = roots.length;
|
|
97
|
-
const stack = roots.map((value) => ({ depth: 1, value })).reverse();
|
|
98
|
-
while (stack.length > 0) {
|
|
99
|
-
const frame = stack.pop();
|
|
100
|
-
if (frame === undefined || !isRecord(frame.value)) {
|
|
101
|
-
continue;
|
|
102
|
-
}
|
|
103
|
-
if (seen.has(frame.value)) {
|
|
104
|
-
return [diagnostic('cyclic-blueprint', 'Blueprint nodes must form an acyclic JSON tree.')];
|
|
105
|
-
}
|
|
106
|
-
seen.add(frame.value);
|
|
107
|
-
if (frame.depth > maximumDepth) {
|
|
108
|
-
return [
|
|
109
|
-
diagnostic('maximum-depth', `Node depth exceeds the configured limit of ${maximumDepth}.`, typeof frame.value.id === 'string' ? frame.value.id : undefined),
|
|
110
|
-
];
|
|
111
|
-
}
|
|
112
|
-
if (!isRecord(frame.value.slots)) {
|
|
113
|
-
continue;
|
|
114
|
-
}
|
|
115
|
-
for (const children of Object.values(frame.value.slots)) {
|
|
116
|
-
if (!Array.isArray(children)) {
|
|
117
|
-
continue;
|
|
118
|
-
}
|
|
119
|
-
scheduled += children.length;
|
|
120
|
-
if (scheduled > maximumNodes) {
|
|
121
|
-
return [
|
|
122
|
-
diagnostic('maximum-nodes', `Blueprint contains more than the configured limit of ${maximumNodes} nodes.`),
|
|
123
|
-
];
|
|
124
|
-
}
|
|
125
|
-
for (let index = children.length - 1; index >= 0; index -= 1) {
|
|
126
|
-
stack.push({ depth: frame.depth + 1, value: children[index] });
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
return [];
|
|
131
|
-
}
|
|
132
|
-
function preflightBlueprintJson(document) {
|
|
133
|
-
const seen = new WeakSet();
|
|
134
|
-
const stack = [{ depth: 0, value: document }];
|
|
135
|
-
let approximateBytes = 0;
|
|
136
|
-
let valueCount = 0;
|
|
137
|
-
while (stack.length > 0) {
|
|
138
|
-
const frame = stack.pop();
|
|
139
|
-
if (frame === undefined) {
|
|
140
|
-
break;
|
|
141
|
-
}
|
|
142
|
-
valueCount += 1;
|
|
143
|
-
if (valueCount > MAX_BLUEPRINT_JSON_VALUES) {
|
|
144
|
-
return diagnostic('maximum-json-values', `Blueprint exceeds the fixed alpha limit of ${MAX_BLUEPRINT_JSON_VALUES} JSON values.`);
|
|
145
|
-
}
|
|
146
|
-
if (frame.depth > MAX_BLUEPRINT_JSON_DEPTH) {
|
|
147
|
-
return diagnostic('maximum-value-depth', `Blueprint JSON value depth exceeds the fixed alpha limit of ${MAX_BLUEPRINT_JSON_DEPTH}.`);
|
|
148
|
-
}
|
|
149
|
-
const { value } = frame;
|
|
150
|
-
if (value === null) {
|
|
151
|
-
approximateBytes += 4;
|
|
152
|
-
}
|
|
153
|
-
else if (typeof value === 'boolean') {
|
|
154
|
-
approximateBytes += value ? 4 : 5;
|
|
155
|
-
}
|
|
156
|
-
else if (typeof value === 'number' && Number.isFinite(value)) {
|
|
157
|
-
approximateBytes += String(value).length;
|
|
158
|
-
}
|
|
159
|
-
else if (typeof value === 'string') {
|
|
160
|
-
approximateBytes += jsonStringByteLength(value);
|
|
161
|
-
}
|
|
162
|
-
else if (Array.isArray(value)) {
|
|
163
|
-
if (!isDenseJsonArray(value)) {
|
|
164
|
-
return diagnostic('non-json-value', 'Blueprint arrays must be dense JSON arrays.');
|
|
165
|
-
}
|
|
166
|
-
if (value.length > MAX_JSON_CONTAINER_ENTRIES) {
|
|
167
|
-
return diagnostic('maximum-array-items', `Blueprint arrays cannot exceed ${MAX_JSON_CONTAINER_ENTRIES} items.`);
|
|
168
|
-
}
|
|
169
|
-
if (seen.has(value)) {
|
|
170
|
-
return diagnostic('cyclic-blueprint', 'Blueprint must be an acyclic JSON document.');
|
|
171
|
-
}
|
|
172
|
-
seen.add(value);
|
|
173
|
-
approximateBytes += value.length + 2;
|
|
174
|
-
for (let index = value.length - 1; index >= 0; index -= 1) {
|
|
175
|
-
stack.push({ depth: frame.depth + 1, value: value[index] });
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
else if (isJsonRecord(value)) {
|
|
179
|
-
if (seen.has(value)) {
|
|
180
|
-
return diagnostic('cyclic-blueprint', 'Blueprint must be an acyclic JSON document.');
|
|
181
|
-
}
|
|
182
|
-
seen.add(value);
|
|
183
|
-
const entries = Object.entries(value);
|
|
184
|
-
if (entries.length > MAX_JSON_CONTAINER_ENTRIES) {
|
|
185
|
-
return diagnostic('maximum-object-properties', `Blueprint objects cannot exceed ${MAX_JSON_CONTAINER_ENTRIES} properties.`);
|
|
186
|
-
}
|
|
187
|
-
approximateBytes += entries.length + 2;
|
|
188
|
-
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
189
|
-
const entry = entries[index];
|
|
190
|
-
if (entry === undefined) {
|
|
191
|
-
continue;
|
|
192
|
-
}
|
|
193
|
-
const [key, child] = entry;
|
|
194
|
-
if (!isSafeJsonMemberName(key)) {
|
|
195
|
-
return diagnostic('unsafe-json-member', 'Blueprint contains an unsafe JSON object member name.');
|
|
196
|
-
}
|
|
197
|
-
approximateBytes += jsonStringByteLength(key) + 1;
|
|
198
|
-
stack.push({ depth: frame.depth + 1, value: child });
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
else {
|
|
202
|
-
return diagnostic('non-json-value', 'Blueprint must contain only JSON-compatible values.');
|
|
203
|
-
}
|
|
204
|
-
if (approximateBytes > MAX_BLUEPRINT_JSON_BYTES) {
|
|
205
|
-
return diagnostic('maximum-json-bytes', `Blueprint exceeds the fixed alpha limit of ${MAX_BLUEPRINT_JSON_BYTES} encoded bytes.`);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
return undefined;
|
|
209
|
-
}
|
|
210
|
-
function isDenseJsonArray(value) {
|
|
211
|
-
if (Object.getPrototypeOf(value) !== Array.prototype ||
|
|
212
|
-
Object.getOwnPropertySymbols(value).length) {
|
|
213
|
-
return false;
|
|
214
|
-
}
|
|
215
|
-
const ownNames = Object.getOwnPropertyNames(value);
|
|
216
|
-
return (ownNames.length === value.length + 1 &&
|
|
217
|
-
ownNames[value.length] === 'length' &&
|
|
218
|
-
ownNames.slice(0, -1).every((name, index) => name === String(index)));
|
|
219
|
-
}
|
|
220
|
-
function isJsonRecord(value) {
|
|
221
|
-
if (!isRecord(value) || Object.getOwnPropertySymbols(value).length > 0) {
|
|
222
|
-
return false;
|
|
223
|
-
}
|
|
224
|
-
const prototype = Object.getPrototypeOf(value);
|
|
225
|
-
if (prototype !== Object.prototype && prototype !== null) {
|
|
226
|
-
return false;
|
|
227
|
-
}
|
|
228
|
-
return Object.getOwnPropertyNames(value).length === Object.keys(value).length;
|
|
229
|
-
}
|
|
230
|
-
function isSafeJsonMemberName(value) {
|
|
231
|
-
if (value.length === 0 ||
|
|
232
|
-
value.length > 200 ||
|
|
233
|
-
value === '__proto__' ||
|
|
234
|
-
value === 'prototype' ||
|
|
235
|
-
value === 'constructor') {
|
|
236
|
-
return false;
|
|
237
|
-
}
|
|
238
|
-
for (let index = 0; index < value.length; index += 1) {
|
|
239
|
-
const code = value.charCodeAt(index);
|
|
240
|
-
if (code <= 0x1f || code === 0x7f) {
|
|
241
|
-
return false;
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
return true;
|
|
245
|
-
}
|
|
246
|
-
function jsonStringByteLength(value) {
|
|
247
|
-
let bytes = 2;
|
|
248
|
-
for (let index = 0; index < value.length; index += 1) {
|
|
249
|
-
const code = value.charCodeAt(index);
|
|
250
|
-
if (code <= 0x1f) {
|
|
251
|
-
bytes += 6;
|
|
252
|
-
}
|
|
253
|
-
else if (code === 0x22 || code === 0x5c) {
|
|
254
|
-
bytes += 2;
|
|
255
|
-
}
|
|
256
|
-
else if (code <= 0x7f) {
|
|
257
|
-
bytes += 1;
|
|
258
|
-
}
|
|
259
|
-
else if (code <= 0x7ff) {
|
|
260
|
-
bytes += 2;
|
|
261
|
-
}
|
|
262
|
-
else if (code >= 0xd800 && code <= 0xdbff) {
|
|
263
|
-
const next = value.charCodeAt(index + 1);
|
|
264
|
-
if (next >= 0xdc00 && next <= 0xdfff) {
|
|
265
|
-
bytes += 4;
|
|
266
|
-
index += 1;
|
|
267
|
-
}
|
|
268
|
-
else {
|
|
269
|
-
bytes += 3;
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
else {
|
|
273
|
-
bytes += 3;
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
return bytes;
|
|
277
|
-
}
|
|
278
|
-
function validationLimit(value, fallback, name) {
|
|
279
|
-
const result = value ?? fallback;
|
|
280
|
-
if (!Number.isInteger(result) || result < 1) {
|
|
281
|
-
throw new RangeError(`${name} must be a positive integer.`);
|
|
282
|
-
}
|
|
283
|
-
return result;
|
|
284
|
-
}
|
|
285
|
-
function isRecord(value) {
|
|
286
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
287
|
-
}
|
|
288
|
-
function schemaDiagnostics(errors) {
|
|
289
|
-
return (errors ?? []).map((error) => diagnostic(`schema-${error.keyword}`, error.message, undefined, error.instancePath));
|
|
290
|
-
}
|
|
291
|
-
function validateNodeProperties(node, definition, registry, diagnostics) {
|
|
292
|
-
const key = `${definition.type}@${definition.version}`;
|
|
293
|
-
let cache = propertyValidators.get(registry);
|
|
294
|
-
if (cache === undefined) {
|
|
295
|
-
cache = new Map();
|
|
296
|
-
propertyValidators.set(registry, cache);
|
|
297
|
-
}
|
|
298
|
-
let validator = cache.get(key);
|
|
299
|
-
if (validator === undefined) {
|
|
300
|
-
const compiled = compileProfileSchema(definition.propertySchema);
|
|
301
|
-
cache.set(key, compiled);
|
|
302
|
-
validator = compiled;
|
|
303
|
-
}
|
|
304
|
-
if (!validator.validate(node.properties)) {
|
|
305
|
-
diagnostics.push(...schemaDiagnostics(validator.errors).map((entry) => ({
|
|
306
|
-
...entry,
|
|
307
|
-
code: `studio.validation/block-properties-${entry.code.split('/').at(-1) ?? 'invalid'}`,
|
|
308
|
-
location: { ...entry.location, nodeId: node.id },
|
|
309
|
-
})));
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
function indexBlockLocks(locks, diagnostics) {
|
|
313
|
-
const indexed = new Map();
|
|
314
|
-
for (const lock of locks) {
|
|
315
|
-
const key = blockKey(lock.type, lock.version);
|
|
316
|
-
if (indexed.has(key)) {
|
|
317
|
-
diagnostics.push(diagnostic('block-lock-duplicate', `Blueprint dependency lock repeats block ${lock.type}@${lock.version}.`));
|
|
318
|
-
}
|
|
319
|
-
else {
|
|
320
|
-
indexed.set(key, lock);
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
return indexed;
|
|
324
|
-
}
|
|
325
|
-
function validateBlockLock(node, definition, verifiedIntegrity, lock, diagnostics) {
|
|
326
|
-
if (lock === undefined) {
|
|
327
|
-
diagnostics.push(diagnostic('block-lock-missing', `Block ${node.type}@${node.version} is absent from the Blueprint dependency lock.`, node.id));
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
330
|
-
if (lock.revision !== definition.revision) {
|
|
331
|
-
diagnostics.push(diagnostic('block-lock-revision-mismatch', `Block ${node.type}@${node.version} resolves to revision ${definition.revision}, not locked revision ${lock.revision}.`, node.id));
|
|
332
|
-
}
|
|
333
|
-
if (lock.integrity !== undefined && verifiedIntegrity === undefined) {
|
|
334
|
-
diagnostics.push(diagnostic('block-lock-integrity-unverified', `Block ${node.type}@${node.version} has a locked integrity value that the registry cannot verify.`, node.id));
|
|
335
|
-
}
|
|
336
|
-
else if (lock.integrity !== undefined && lock.integrity !== verifiedIntegrity) {
|
|
337
|
-
diagnostics.push(diagnostic('block-lock-integrity-mismatch', `Block ${node.type}@${node.version} does not match its locked integrity value.`, node.id));
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
function blockKey(type, version) {
|
|
341
|
-
return `${type}@${version}`;
|
|
342
|
-
}
|
|
343
|
-
function validateSlots(node, definition, diagnostics) {
|
|
344
|
-
const slots = new Map(definition.slots.map((slot) => [slot.id, slot]));
|
|
345
|
-
for (const [slotName, children] of Object.entries(node.slots)) {
|
|
346
|
-
const slot = slots.get(slotName);
|
|
347
|
-
if (slot === undefined) {
|
|
348
|
-
diagnostics.push(diagnostic('slot-unknown', `Slot ${slotName} is not declared by ${node.type}.`, node.id));
|
|
349
|
-
continue;
|
|
350
|
-
}
|
|
351
|
-
if (children.length > slot.maximum) {
|
|
352
|
-
diagnostics.push(diagnostic('slot-maximum', `Slot ${slotName} accepts at most ${slot.maximum} children.`, node.id));
|
|
353
|
-
}
|
|
354
|
-
for (const child of children) {
|
|
355
|
-
if (slot.accepts.types !== undefined && !slot.accepts.types.includes(child.type)) {
|
|
356
|
-
diagnostics.push(diagnostic('slot-rejects-type', `Slot ${slotName} does not accept ${child.type}.`, child.id));
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
for (const slot of slots.values()) {
|
|
361
|
-
const count = Object.hasOwn(node.slots, slot.id) ? (node.slots[slot.id]?.length ?? 0) : 0;
|
|
362
|
-
if (count < slot.minimum) {
|
|
363
|
-
diagnostics.push(diagnostic('slot-minimum', `Slot ${slot.id} requires at least ${slot.minimum} children.`, node.id));
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
function validateBindings(node, fieldPaths, diagnostics) {
|
|
368
|
-
if (fieldPaths === undefined) {
|
|
369
|
-
return;
|
|
370
|
-
}
|
|
371
|
-
for (const binding of Object.values(node.bindings)) {
|
|
372
|
-
if (binding.source.kind !== 'entry-field') {
|
|
373
|
-
continue;
|
|
374
|
-
}
|
|
375
|
-
const path = binding.source.fieldPath.join('.');
|
|
376
|
-
if (!fieldPaths.has(path)) {
|
|
377
|
-
diagnostics.push(diagnostic('field-unavailable', `Field ${path} is not available to this Studio configuration.`, node.id));
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
function diagnostic(name, message, nodeId, jsonPointer) {
|
|
382
|
-
const result = {
|
|
383
|
-
code: `studio.validation/${name}`,
|
|
384
|
-
message: { defaultMessage: message, key: `studio.validation/${name}` },
|
|
385
|
-
severity: 'error',
|
|
386
|
-
};
|
|
387
|
-
if (nodeId !== undefined || jsonPointer !== undefined) {
|
|
388
|
-
result.location = {};
|
|
389
|
-
if (nodeId !== undefined) {
|
|
390
|
-
result.location.nodeId = nodeId;
|
|
391
|
-
}
|
|
392
|
-
if (jsonPointer !== undefined) {
|
|
393
|
-
result.location.jsonPointer = jsonPointer;
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
return result;
|
|
397
|
-
}
|
|
398
|
-
//# sourceMappingURL=validation.js.map
|
|
1
|
+
import{blueprintSchema as e,commonSchema as t}from"@kumwe/studio-protocol";import{compileProfileSchema as n}from"./profile-validator.js";const r=n(e,{schemas:[t]}),i=new WeakMap,a=16777216,o=1e6,s=1e4;export function validateBlueprint(e,t,n={}){let i=[],a=h(n.maximumDepth,32,`maximumDepth`),o=h(n.maximumNodes,5e3,`maximumNodes`),s=l(e,a,o);if(s.length>0)return{diagnostics:s,valid:!1};let c=u(e);if(c!==void 0)return{diagnostics:[c],valid:!1};if(!r.validate(e))return i.push(..._(r.errors)),{diagnostics:i,valid:!1};let d=e,f=new Set,p=S(d.dependencyLock.blocks,i),m=0,g=d.roots.map(e=>({depth:1,node:e})).reverse();for(;g.length>0;){let e=g.pop();if(e===void 0)break;let{depth:r,node:s}=e;if(m+=1,m>o)break;if(r>a){i.push(D(`maximum-depth`,`Node depth exceeds the configured limit of ${a}.`,s.id));continue}f.has(s.id)&&i.push(D(`duplicate-node-id`,`Node identifier ${s.id} is not unique.`,s.id)),f.add(s.id);let c=t.resolveRegistration(s.type,s.version);c===void 0?i.push(D(`block-unavailable`,`Block ${s.type}@${s.version} is not registered.`,s.id)):(C(s,c.definition,c.verifiedIntegrity,p.get(w(s.type,s.version)),i),v(s,c.definition,t,i),T(s,c.definition,i)),E(s,n.fieldPaths,i);let l=Object.values(s.slots);for(let e=l.length-1;e>=0;--e){let t=l[e];if(t!==void 0)for(let e=t.length-1;e>=0;--e){let n=t[e];n!==void 0&&g.push({depth:r+1,node:n})}}}return m>o&&i.push(D(`maximum-nodes`,`Blueprint contains more than the configured limit of ${o} nodes.`)),{diagnostics:i,valid:i.every(e=>e.severity!==`blocking`&&e.severity!==`error`)}}function l(e,t,n){if(!g(e)||!Array.isArray(e.roots))return[];let r=e.roots;if(r.length>n)return[D(`maximum-nodes`,`Blueprint contains more than the configured limit of ${n} nodes.`)];let i=new WeakSet,a=r.length,o=r.map(e=>({depth:1,value:e})).reverse();for(;o.length>0;){let e=o.pop();if(!(e===void 0||!g(e.value))){if(i.has(e.value))return[D(`cyclic-blueprint`,`Blueprint nodes must form an acyclic JSON tree.`)];if(i.add(e.value),e.depth>t)return[D(`maximum-depth`,`Node depth exceeds the configured limit of ${t}.`,typeof e.value.id==`string`?e.value.id:void 0)];if(g(e.value.slots)){for(let t of Object.values(e.value.slots))if(Array.isArray(t)){if(a+=t.length,a>n)return[D(`maximum-nodes`,`Blueprint contains more than the configured limit of ${n} nodes.`)];for(let n=t.length-1;n>=0;--n)o.push({depth:e.depth+1,value:t[n]})}}}}return[]}function u(e){let t=new WeakSet,n=[{depth:0,value:e}],r=0,i=0;for(;n.length>0;){let e=n.pop();if(e===void 0)break;if(i+=1,i>o)return D(`maximum-json-values`,`Blueprint exceeds the fixed alpha limit of ${o} JSON values.`);if(e.depth>64)return D(`maximum-value-depth`,`Blueprint JSON value depth exceeds the fixed alpha limit of 64.`);let{value:c}=e;if(c===null)r+=4;else if(typeof c==`boolean`)r+=c?4:5;else if(typeof c==`number`&&Number.isFinite(c))r+=String(c).length;else if(typeof c==`string`)r+=m(c);else if(Array.isArray(c)){if(!d(c))return D(`non-json-value`,`Blueprint arrays must be dense JSON arrays.`);if(c.length>s)return D(`maximum-array-items`,`Blueprint arrays cannot exceed ${s} items.`);if(t.has(c))return D(`cyclic-blueprint`,`Blueprint must be an acyclic JSON document.`);t.add(c),r+=c.length+2;for(let t=c.length-1;t>=0;--t)n.push({depth:e.depth+1,value:c[t]})}else if(f(c)){if(t.has(c))return D(`cyclic-blueprint`,`Blueprint must be an acyclic JSON document.`);t.add(c);let i=Object.entries(c);if(i.length>s)return D(`maximum-object-properties`,`Blueprint objects cannot exceed ${s} properties.`);r+=i.length+2;for(let t=i.length-1;t>=0;--t){let a=i[t];if(a===void 0)continue;let[o,s]=a;if(!p(o))return D(`unsafe-json-member`,`Blueprint contains an unsafe JSON object member name.`);r+=m(o)+1,n.push({depth:e.depth+1,value:s})}}else return D(`non-json-value`,`Blueprint must contain only JSON-compatible values.`);if(r>a)return D(`maximum-json-bytes`,`Blueprint exceeds the fixed alpha limit of ${a} encoded bytes.`)}}function d(e){if(Object.getPrototypeOf(e)!==Array.prototype||Object.getOwnPropertySymbols(e).length)return!1;let t=Object.getOwnPropertyNames(e);return t.length===e.length+1&&t[e.length]===`length`&&t.slice(0,-1).every((e,t)=>e===String(t))}function f(e){if(!g(e)||Object.getOwnPropertySymbols(e).length>0)return!1;let t=Object.getPrototypeOf(e);return t!==Object.prototype&&t!==null?!1:Object.getOwnPropertyNames(e).length===Object.keys(e).length}function p(e){if(e.length===0||e.length>200||e===`__proto__`||e===`prototype`||e===`constructor`)return!1;for(let t=0;t<e.length;t+=1){let n=e.charCodeAt(t);if(n<=31||n===127)return!1}return!0}function m(e){let t=2;for(let n=0;n<e.length;n+=1){let r=e.charCodeAt(n);if(r<=31)t+=6;else if(r===34||r===92)t+=2;else if(r<=127)t+=1;else if(r<=2047)t+=2;else if(r>=55296&&r<=56319){let r=e.charCodeAt(n+1);r>=56320&&r<=57343?(t+=4,n+=1):t+=3}else t+=3}return t}function h(e,t,n){let r=e??t;if(!Number.isInteger(r)||r<1)throw RangeError(`${n} must be a positive integer.`);return r}function g(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function _(e){return(e??[]).map(e=>D(`schema-${e.keyword}`,e.message,void 0,e.instancePath))}function v(e,t,r,a){let o=`${t.type}@${t.version}`,s=i.get(r);s===void 0&&(s=new Map,i.set(r,s));let c=s.get(o);if(c===void 0){let e=n(t.propertySchema);s.set(o,e),c=e}y(e,c,e.properties,void 0,a);let l=new Map;for(let t of Object.keys(e.responsive??{}).sort(O)){let n=e.responsive?.[t];if(n!==void 0)for(let r of Object.keys(n).sort(O)){let i=n[r];if(i===void 0)continue;let a=l.get(r)??{...e.properties};a[t]=i,l.set(r,a)}}for(let t of[...l.keys()].sort(O)){let n=l.get(t);n!==void 0&&y(e,c,n,t,a)}}function y(e,t,n,r,i){t.validate(n)||i.push(..._(t.errors).map(t=>({...t,code:`studio.validation/block-properties-${t.code.split(`/`).at(-1)??`invalid`}`,location:{...t.location,nodeId:e.id,...r===void 0?{}:{jsonPointer:b(t.location?.jsonPointer,r)}}})))}function b(e,t){let n=e?.split(`/`).slice(1)??[],r=n.shift();return r===void 0?`/responsive/${x(t)}`:[``,`responsive`,r,x(t),...n].join(`/`)}function x(e){return e.replaceAll(`~`,`~0`).replaceAll(`/`,`~1`)}function S(e,t){let n=new Map;for(let r of e){let e=w(r.type,r.version);n.has(e)?t.push(D(`block-lock-duplicate`,`Blueprint dependency lock repeats block ${r.type}@${r.version}.`)):n.set(e,r)}return n}function C(e,t,n,r,i){if(r===void 0){i.push(D(`block-lock-missing`,`Block ${e.type}@${e.version} is absent from the Blueprint dependency lock.`,e.id));return}r.revision!==t.revision&&i.push(D(`block-lock-revision-mismatch`,`Block ${e.type}@${e.version} resolves to revision ${t.revision}, not locked revision ${r.revision}.`,e.id)),r.integrity!==void 0&&n===void 0?i.push(D(`block-lock-integrity-unverified`,`Block ${e.type}@${e.version} has a locked integrity value that the registry cannot verify.`,e.id)):r.integrity!==void 0&&r.integrity!==n&&i.push(D(`block-lock-integrity-mismatch`,`Block ${e.type}@${e.version} does not match its locked integrity value.`,e.id))}function w(e,t){return`${e}@${t}`}function T(e,t,n){let r=new Map(t.slots.map(e=>[e.id,e]));for(let[t,i]of Object.entries(e.slots)){let a=r.get(t);if(a===void 0){n.push(D(`slot-unknown`,`Slot ${t} is not declared by ${e.type}.`,e.id));continue}i.length>a.maximum&&n.push(D(`slot-maximum`,`Slot ${t} accepts at most ${a.maximum} children.`,e.id));for(let e of i)a.accepts.types!==void 0&&!a.accepts.types.includes(e.type)&&n.push(D(`slot-rejects-type`,`Slot ${t} does not accept ${e.type}.`,e.id))}for(let t of r.values())(Object.hasOwn(e.slots,t.id)?e.slots[t.id]?.length??0:0)<t.minimum&&n.push(D(`slot-minimum`,`Slot ${t.id} requires at least ${t.minimum} children.`,e.id))}function E(e,t,n){if(t!==void 0)for(let r of Object.values(e.bindings)){if(r.source.kind!==`entry-field`)continue;let i=r.source.fieldPath.join(`.`);t.has(i)||n.push(D(`field-unavailable`,`Field ${i} is not available to this Studio configuration.`,e.id))}}function D(e,t,n,r){let i={code:`studio.validation/${e}`,message:{defaultMessage:t,key:`studio.validation/${e}`},severity:`error`};return(n!==void 0||r!==void 0)&&(i.location={},n!==void 0&&(i.location.nodeId=n),r!==void 0&&(i.location.jsonPointer=r)),i}function O(e,t){return e<t?-1:+(e>t)}
|