@burojs/config 0.4.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,30 +1,18 @@
1
+ import { DEFAULT_COMPILER_LIMITS, normalizeLayerGrants } from "@burojs/core";
2
+ import { blacklistedKeyCategory, createParsedLayer, isReservedSegment, scopeRank } from "@burojs/core/internal";
1
3
  import { parseDocument } from "yaml";
4
+ //#region src/source-policy.ts
5
+ const MAX_SOURCE_DEPTH = DEFAULT_COMPILER_LIMITS.maxPortableDepth;
6
+ const SCREEN_SOURCE_LIMITS = Object.freeze({
7
+ maxBytes: 256e3,
8
+ maxDeclarations: 64,
9
+ maxDepth: MAX_SOURCE_DEPTH,
10
+ maxNodes: 5e3,
11
+ maxStringLength: 4096,
12
+ maxDiagnostics: 32
13
+ });
14
+ //#endregion
2
15
  //#region src/parse.ts
3
- const RESERVED_KEYS = new Set([
4
- "__proto__",
5
- "prototype",
6
- "constructor"
7
- ]);
8
- const FORBIDDEN_VALUE_CATEGORIES = new Map([
9
- ["action", "action"],
10
- ["actions", "action"],
11
- ["provider", "provider"],
12
- ["providers", "provider"],
13
- ["mapper", "mapper"],
14
- ["mappers", "mapper"],
15
- ["service", "service"],
16
- ["services", "service"],
17
- ["credential", "credential"],
18
- ["credentials", "credential"],
19
- ["schema", "schema"],
20
- ["schemas", "schema"],
21
- ["component", "component"],
22
- ["components", "component"],
23
- ["attachment", "attachment"],
24
- ["attachments", "attachment"],
25
- ["validator", "validator"],
26
- ["validators", "validator"]
27
- ]);
28
16
  const LAYER_KEYS = new Set([
29
17
  "id",
30
18
  "priority",
@@ -95,6 +83,7 @@ function messageFor(code) {
95
83
  case "source.byte-limit": return "The source exceeds its byte limit.";
96
84
  case "source.declaration-limit": return "The source exceeds its declaration limit.";
97
85
  case "source.depth-limit": return "The source exceeds its nesting-depth limit.";
86
+ case "source.node-limit": return "The source exceeds its value-count limit.";
98
87
  case "source.string-limit": return "The source contains an overlong string.";
99
88
  case "source.diagnostic-limit": return "Additional source diagnostics were omitted.";
100
89
  case "source.unknown-key": return "The source contains an unknown key.";
@@ -109,7 +98,7 @@ function messageFor(code) {
109
98
  case "source.forbidden-transform-op": return "The source requests a forbidden transform operation.";
110
99
  case "source.forbidden-transform-scope": return "The source requests a forbidden transform scope.";
111
100
  case "source.forbidden-capability": return "The source requests a forbidden capability.";
112
- case "source.forbidden-action": return "The source requests a forbidden action binding.";
101
+ case "source.forbidden-action": return "The source requests a forbidden action binding; a resource's flows are written under toolbar.";
113
102
  case "source.forbidden-provider": return "The source requests a forbidden provider binding.";
114
103
  case "source.forbidden-mapper": return "The source requests a forbidden mapper binding.";
115
104
  case "source.forbidden-service": return "The source requests a forbidden service value.";
@@ -121,14 +110,68 @@ function messageFor(code) {
121
110
  default: return "The source is invalid.";
122
111
  }
123
112
  }
113
+ const LIMIT_KEYS = [
114
+ "maxBytes",
115
+ "maxDeclarations",
116
+ "maxDepth",
117
+ "maxNodes",
118
+ "maxStringLength",
119
+ "maxDiagnostics"
120
+ ];
121
+ const LIMIT_KEY_SET = new Set(LIMIT_KEYS);
124
122
  function validLimits(limits) {
125
- return Object.values(limits).every((value) => Number.isSafeInteger(value) && value > 0);
123
+ return plainObject(limits) && Object.keys(limits).every((key) => LIMIT_KEY_SET.has(key)) && LIMIT_KEYS.every((key) => Number.isSafeInteger(limits[key]) && limits[key] > 0);
126
124
  }
127
125
  function validCapabilities(value) {
128
126
  return value !== null && typeof value === "object" && !Array.isArray(value) && Object.values(value).every((capabilities) => Array.isArray(capabilities) && capabilities.every((capability) => typeof capability === "string"));
129
127
  }
130
- function validPolicy(policy) {
131
- return policy !== null && typeof policy === "object" && Array.isArray(policy.allowedKinds) && Array.isArray(policy.allowedNamespaces) && Array.isArray(policy.allowedTransformOps) && (policy.maxTransformScope === "layer" || policy.maxTransformScope === "namespace" || policy.maxTransformScope === "application") && validCapabilities(policy.allowedCapabilities) && validLimits(policy.limits);
128
+ function grantsError(policy) {
129
+ try {
130
+ normalizeLayerGrants(policy.grants);
131
+ return;
132
+ } catch (error) {
133
+ return error instanceof TypeError ? error.message : "";
134
+ }
135
+ }
136
+ function policyDiagnostic(policy) {
137
+ const generic = messageFor("source.invalid-policy");
138
+ const stray = strayPolicyKey(policy);
139
+ if (stray !== void 0) return Object.freeze({
140
+ code: "source.invalid-policy",
141
+ message: `The source policy is invalid: it carries ${stray} at the top level; a policy is { grants, allowedCapabilities, limits }, with every grant field inside grants.`
142
+ });
143
+ if (!validPolicyShape(policy)) return Object.freeze({
144
+ code: "source.invalid-policy",
145
+ message: generic
146
+ });
147
+ const reason = grantsError(policy);
148
+ if (reason === void 0) return void 0;
149
+ return Object.freeze({
150
+ code: "source.invalid-policy",
151
+ message: reason === "" ? generic : `The source policy is invalid: ${reason}`
152
+ });
153
+ }
154
+ const POLICY_KEYS = new Set([
155
+ "grants",
156
+ "allowedCapabilities",
157
+ "limits"
158
+ ]);
159
+ function strayPolicyKey(policy) {
160
+ return plainObject(policy) ? Object.keys(policy).find((key) => !POLICY_KEYS.has(key)) : void 0;
161
+ }
162
+ function validPolicyShape(policy) {
163
+ return plainObject(policy) && plainObject(policy.grants) && validCapabilities(policy.allowedCapabilities) && validLimits(policy.limits);
164
+ }
165
+ function ownedPolicy(policy) {
166
+ const invalid = policyDiagnostic(policy);
167
+ if (invalid !== void 0) throw new TypeError(invalid.message);
168
+ const capabilities = Object.create(null);
169
+ for (const [kind, values] of Object.entries(policy.allowedCapabilities)) capabilities[kind] = Object.freeze([...values]);
170
+ return Object.freeze({
171
+ grants: normalizeLayerGrants(policy.grants),
172
+ allowedCapabilities: Object.freeze(capabilities),
173
+ limits: Object.freeze({ ...policy.limits })
174
+ });
132
175
  }
133
176
  function failure(diagnostics) {
134
177
  return {
@@ -149,10 +192,37 @@ function hasData(value, key) {
149
192
  const descriptor = Object.getOwnPropertyDescriptor(value, key);
150
193
  return descriptor !== void 0 && "value" in descriptor;
151
194
  }
195
+ const MAX_ENVELOPE_DEPTH = 8;
196
+ function childRole(role, key) {
197
+ switch (role) {
198
+ case "root": return key === "declarations" ? "declarations" : key === "transforms" ? "transforms" : "envelope";
199
+ case "declarations": return "declaration";
200
+ case "transforms": return "transform";
201
+ case "declaration": return key === "value" ? "value" : "envelope";
202
+ case "transform": return key === "operation" ? "operation" : "envelope";
203
+ case "operation": return key === "value" ? "value" : "envelope";
204
+ default: return role;
205
+ }
206
+ }
152
207
  function clonePortable(value, limits, diagnostics) {
153
208
  const ancestors = new WeakSet();
154
- function copy(current, depth) {
155
- if (depth > limits.maxDepth) {
209
+ const maxDepth = Math.min(limits.maxDepth, MAX_SOURCE_DEPTH);
210
+ let nodes = 0;
211
+ let nodesExceeded = false;
212
+ function overNodeBudget(children) {
213
+ if (children <= limits.maxNodes - nodes) return false;
214
+ if (!nodesExceeded) diagnostics.add("source.node-limit");
215
+ nodesExceeded = true;
216
+ return true;
217
+ }
218
+ function copy(current, depth, role) {
219
+ nodes += 1;
220
+ if (nodes > limits.maxNodes) {
221
+ if (!nodesExceeded) diagnostics.add("source.node-limit");
222
+ nodesExceeded = true;
223
+ return null;
224
+ }
225
+ if (depth > (role === "value" ? maxDepth : MAX_ENVELOPE_DEPTH)) {
156
226
  diagnostics.add("source.depth-limit");
157
227
  return null;
158
228
  }
@@ -172,6 +242,12 @@ function clonePortable(value, limits, diagnostics) {
172
242
  ancestors.add(current);
173
243
  if (Array.isArray(current)) {
174
244
  const output = [];
245
+ const length = ownData(current, "length");
246
+ const arrayLength = typeof length === "number" && Number.isSafeInteger(length) && length >= 0 ? length : void 0;
247
+ if (arrayLength !== void 0 && overNodeBudget(arrayLength)) {
248
+ ancestors.delete(current);
249
+ return null;
250
+ }
175
251
  for (const key of Reflect.ownKeys(current)) {
176
252
  if (typeof key !== "string" || key !== "length" && !/^(0|[1-9]\d*)$/u.test(key)) {
177
253
  diagnostics.add("source.invalid-layer");
@@ -180,41 +256,47 @@ function clonePortable(value, limits, diagnostics) {
180
256
  if (key === "length") continue;
181
257
  if (!hasData(current, key)) diagnostics.add("source.accessor");
182
258
  }
183
- const length = ownData(current, "length");
184
- const arrayLength = typeof length === "number" && Number.isSafeInteger(length) && length >= 0 ? length : void 0;
185
259
  if (arrayLength === void 0) diagnostics.add("source.invalid-layer");
186
- else for (let index = 0; index < arrayLength; index += 1) {
260
+ else for (let index = 0; index < arrayLength && !nodesExceeded; index += 1) {
187
261
  if (!hasData(current, String(index))) diagnostics.add("source.accessor");
188
- output.push(copy(ownData(current, String(index)), depth + 1));
262
+ output.push(copy(ownData(current, String(index)), depth + 1, childRole(role, void 0)));
189
263
  }
190
264
  ancestors.delete(current);
191
265
  return Object.freeze(output);
192
266
  }
193
267
  if (!plainObject(current)) diagnostics.add("source.invalid-layer");
194
268
  const output = Object.create(null);
195
- for (const key of Reflect.ownKeys(current)) {
269
+ const keys = Reflect.ownKeys(current);
270
+ if (overNodeBudget(keys.length)) {
271
+ ancestors.delete(current);
272
+ return null;
273
+ }
274
+ for (const key of keys) {
275
+ if (nodesExceeded) break;
196
276
  if (typeof key !== "string") {
197
277
  diagnostics.add("source.invalid-layer");
198
278
  continue;
199
279
  }
200
- if (RESERVED_KEYS.has(key)) diagnostics.add("source.reserved-key");
280
+ if (isReservedSegment(key)) diagnostics.add("source.reserved-key");
201
281
  if ([...key].length > limits.maxStringLength) diagnostics.add("source.string-limit");
202
282
  if (!hasData(current, key)) {
203
283
  diagnostics.add("source.accessor");
204
284
  continue;
205
285
  }
206
- output[key] = copy(ownData(current, key), depth + 1);
286
+ const next = childRole(role, key);
287
+ const nextDepth = next === "value" && role !== "value" ? 0 : depth + 1;
288
+ output[key] = copy(ownData(current, key), nextDepth, next);
207
289
  }
208
290
  ancestors.delete(current);
209
291
  return Object.freeze(output);
210
292
  }
211
- return copy(value, 0);
293
+ return copy(value, 0, "root");
212
294
  }
213
295
  function unknownKeys(value, allowed, diagnostics) {
214
296
  for (const key of Object.keys(value)) if (!allowed.has(key)) diagnostics.add("source.unknown-key");
215
297
  }
216
298
  function allowedNamespace(value, policy, diagnostics) {
217
- if (typeof value !== "string" || !policy.allowedNamespaces.includes(value)) {
299
+ if (typeof value !== "string" || !policy.grants.allowedNamespaces.includes(value)) {
218
300
  diagnostics.add("source.forbidden-namespace");
219
301
  return false;
220
302
  }
@@ -243,17 +325,15 @@ function validateValue(value, kind, policy, diagnostics) {
243
325
  if (!plainObject(value)) return;
244
326
  if (value.$buro === ATTACHMENT_TAG) {
245
327
  const capability = ownData(value, "capability");
246
- if (typeof capability !== "string" || !policy.allowedCapabilities[kind]?.includes(capability)) diagnostics.add("source.forbidden-capability");
328
+ const allowed = Object.hasOwn(policy.allowedCapabilities, kind) ? policy.allowedCapabilities[kind] : void 0;
329
+ if (typeof capability !== "string" || !allowed?.includes(capability)) diagnostics.add("source.forbidden-capability");
247
330
  }
248
331
  for (const key of Object.keys(value)) {
249
- const category = FORBIDDEN_VALUE_CATEGORIES.get(key);
332
+ const category = blacklistedKeyCategory(key);
250
333
  if (category !== void 0) diagnostics.add(`source.forbidden-${category}`);
251
334
  validateValue(ownData(value, key), kind, policy, diagnostics);
252
335
  }
253
336
  }
254
- function scopeRank(scope) {
255
- return scope === "layer" ? 0 : scope === "namespace" ? 1 : 2;
256
- }
257
337
  function transformOperation(value) {
258
338
  if (!plainObject(value)) return void 0;
259
339
  const operation = ownData(value, "op");
@@ -284,7 +364,7 @@ function validateTransforms(value, defaultNamespace, policy, diagnostics) {
284
364
  unknownKeys(transform, TRANSFORM_KEYS, diagnostics);
285
365
  const scope = ownData(transform, "scope") ?? "layer";
286
366
  if (scope !== "layer" && scope !== "application") diagnostics.add("source.invalid-transform");
287
- else if (scopeRank(scope) > scopeRank(policy.maxTransformScope)) diagnostics.add("source.forbidden-transform-scope");
367
+ else if (scopeRank(scope) > scopeRank(policy.grants.maxTransformScope)) diagnostics.add("source.forbidden-transform-scope");
288
368
  const select = ownData(transform, "select");
289
369
  if (!plainObject(select)) diagnostics.add("source.invalid-transform");
290
370
  else {
@@ -293,7 +373,7 @@ function validateTransforms(value, defaultNamespace, policy, diagnostics) {
293
373
  if (selectedKinds !== void 0) {
294
374
  if (!Array.isArray(selectedKinds)) diagnostics.add("source.invalid-transform");
295
375
  else for (const kind of selectedKinds) if (typeof kind !== "string") diagnostics.add("source.invalid-transform");
296
- else if (!policy.allowedKinds.includes(kind)) diagnostics.add("source.forbidden-kind");
376
+ else if (!policy.grants.allowedKinds.includes(kind)) diagnostics.add("source.forbidden-kind");
297
377
  }
298
378
  const selectedNamespaces = ownData(select, "namespaces");
299
379
  if (selectedNamespaces !== void 0) {
@@ -308,7 +388,7 @@ function validateTransforms(value, defaultNamespace, policy, diagnostics) {
308
388
  if (typeof (plainObject(operation) ? ownData(operation, "op") : void 0) === "string" && op === void 0) diagnostics.add("source.forbidden-transform-op");
309
389
  else if (op === void 0) diagnostics.add("source.invalid-transform");
310
390
  else {
311
- if (!policy.allowedTransformOps.includes(op)) diagnostics.add("source.forbidden-transform-op");
391
+ if (!policy.grants.allowedTransformOps.includes(op)) diagnostics.add("source.forbidden-transform-op");
312
392
  if (plainObject(operation)) {
313
393
  unknownKeys(operation, OPERATION_KEYS[op], diagnostics);
314
394
  if (hasData(operation, "value")) validateValue(ownData(operation, "value"), "", policy, diagnostics);
@@ -317,12 +397,10 @@ function validateTransforms(value, defaultNamespace, policy, diagnostics) {
317
397
  }
318
398
  }
319
399
  function validateLayerValue(value, policy) {
320
- if (!validPolicy(policy)) return {
400
+ const invalidPolicy = policyDiagnostic(policy);
401
+ if (invalidPolicy !== void 0) return {
321
402
  ok: false,
322
- diagnostics: Object.freeze([Object.freeze({
323
- code: "source.invalid-policy",
324
- message: messageFor("source.invalid-policy")
325
- })])
403
+ diagnostics: Object.freeze([invalidPolicy])
326
404
  };
327
405
  const diagnostics = new Diagnostics(policy.limits.maxDiagnostics);
328
406
  const cloned = clonePortable(value, policy.limits, diagnostics);
@@ -352,7 +430,7 @@ function validateLayerValue(value, policy) {
352
430
  unknownKeys(declaration, DECLARATION_KEYS, diagnostics);
353
431
  const kind = ownData(declaration, "kind");
354
432
  if (typeof kind !== "string") diagnostics.add("source.invalid-declaration");
355
- else if (!policy.allowedKinds.includes(kind)) diagnostics.add("source.forbidden-kind");
433
+ else if (!policy.grants.allowedKinds.includes(kind)) diagnostics.add("source.forbidden-kind");
356
434
  const namespace = namespaceFor(ownData(declaration, "ref"), typeof defaultNamespace === "string" ? defaultNamespace : "default", diagnostics);
357
435
  if (namespace !== void 0) allowedNamespace(namespace, policy, diagnostics);
358
436
  if (!Number.isSafeInteger(ownData(declaration, "schemaVersion")) || !hasData(declaration, "value")) diagnostics.add("source.invalid-declaration");
@@ -362,14 +440,14 @@ function validateLayerValue(value, policy) {
362
440
  if (diagnostics.values.length > 0) return failure(diagnostics);
363
441
  return {
364
442
  ok: true,
365
- value: cloned
443
+ value: createParsedLayer(cloned, normalizeLayerGrants(policy.grants))
366
444
  };
367
445
  }
368
446
  function byteLength(value) {
369
447
  return new TextEncoder().encode(value).byteLength;
370
448
  }
371
449
  function parseSource(source, policy, format) {
372
- if (!validPolicy(policy)) return validateLayerValue(null, policy);
450
+ if (policyDiagnostic(policy) !== void 0) return validateLayerValue(null, policy);
373
451
  const diagnostics = new Diagnostics(policy.limits.maxDiagnostics);
374
452
  if (byteLength(source) > policy.limits.maxBytes) {
375
453
  diagnostics.add("source.byte-limit");
@@ -399,82 +477,12 @@ function parseYamlLayer(source, policy) {
399
477
  return parseSource(source, policy, "yaml");
400
478
  }
401
479
  //#endregion
402
- //#region src/adapter.ts
403
- function diagnostic(code, message) {
404
- return Object.freeze({
405
- code,
406
- message
407
- });
408
- }
409
- function diagnosticLimit(policy) {
410
- const candidate = policy?.limits?.maxDiagnostics;
411
- return typeof candidate === "number" && Number.isSafeInteger(candidate) && candidate > 0 ? candidate : 1;
412
- }
413
- function frozenPolicy(policy) {
414
- const capabilities = Object.create(null);
415
- for (const [kind, values] of Object.entries(policy.allowedCapabilities)) capabilities[kind] = Object.freeze([...values]);
416
- const limits = Object.freeze({ ...policy.limits });
417
- return Object.freeze({
418
- allowedKinds: Object.freeze([...policy.allowedKinds]),
419
- allowedNamespaces: Object.freeze([...policy.allowedNamespaces]),
420
- allowedTransformOps: Object.freeze([...policy.allowedTransformOps]),
421
- maxTransformScope: policy.maxTransformScope,
422
- allowedCapabilities: Object.freeze(capabilities),
423
- limits
424
- });
425
- }
426
- function copyDiagnostic(value) {
427
- if (value !== null && typeof value === "object" && typeof value.code === "string" && typeof value.message === "string") return diagnostic(value.code, value.message);
428
- return diagnostic("source.invalid-adapter-diagnostic", "The source adapter returned an invalid diagnostic.");
429
- }
430
- function adaptRawConfig(input, adapter, policy) {
431
- const policyValidation = validateLayerValue(null, policy);
432
- if (!policyValidation.ok && policyValidation.diagnostics.some((entry) => entry.code === "source.invalid-policy")) return policyValidation;
433
- const ownedPolicy = frozenPolicy(policy);
434
- const diagnostics = [];
435
- const maxDiagnostics = diagnosticLimit(ownedPolicy);
436
- let limited = false;
437
- const add = (value) => {
438
- if (limited) return;
439
- if (diagnostics.length + 1 < maxDiagnostics) {
440
- diagnostics.push(value);
441
- return;
442
- }
443
- limited = true;
444
- diagnostics.push(diagnostic("source.diagnostic-limit", "Additional source diagnostics were omitted."));
445
- };
446
- const context = Object.freeze({
447
- policy: ownedPolicy,
448
- limits: ownedPolicy.limits,
449
- diagnose(code, message) {
450
- add(copyDiagnostic({
451
- code,
452
- message
453
- }));
454
- }
455
- });
456
- const result = adapter.adapt(input, context);
457
- if (result === null || typeof result !== "object") add(diagnostic("source.invalid-adapter-result", "The source adapter returned an invalid result."));
458
- else if (result.diagnostics !== void 0) {
459
- if (!Array.isArray(result.diagnostics)) add(diagnostic("source.invalid-adapter-result", "The source adapter returned an invalid result."));
460
- else for (const entry of result.diagnostics) add(copyDiagnostic(entry));
461
- }
462
- if (diagnostics.length > 0) return {
463
- ok: false,
464
- diagnostics: Object.freeze(diagnostics)
465
- };
466
- if (result === null || typeof result !== "object" || result.layer === void 0) return {
467
- ok: false,
468
- diagnostics: Object.freeze([diagnostic("source.invalid-adapter-result", "The source adapter did not return a declaration layer.")])
469
- };
470
- return validateLayerValue(result.layer, ownedPolicy);
471
- }
472
- const parseResultAdapter = Object.freeze({ adapt(input) {
473
- return input.ok ? { layer: input.value } : { diagnostics: input.diagnostics };
474
- } });
475
- //#endregion
476
480
  //#region src/runtime-source.ts
477
- function adaptationFailure(diagnostics) {
481
+ const INVALID_FORMAT = Object.freeze({
482
+ code: "source.invalid-format",
483
+ message: "The source format must be json or yaml."
484
+ });
485
+ function rejection(diagnostics) {
478
486
  return Object.freeze({
479
487
  status: "rejected",
480
488
  diagnostics: Object.freeze(diagnostics.map((entry) => Object.freeze({
@@ -483,17 +491,24 @@ function adaptationFailure(diagnostics) {
483
491
  })))
484
492
  });
485
493
  }
486
- function createConfigRuntimeSource(id, fetchRaw, adapter, policy) {
487
- return Object.freeze({
494
+ function createConfigRuntimeSource(id, options) {
495
+ const policy = ownedPolicy(options.policy);
496
+ const { load } = options;
497
+ const source = Object.freeze({
488
498
  id,
489
499
  async load(context) {
490
- const adapted = adaptRawConfig(await fetchRaw(context), adapter, policy);
491
- if (!adapted.ok) return adaptationFailure(adapted.diagnostics);
492
- return adapted.value;
500
+ const raw = await load(context);
501
+ const parsed = raw.format === "json" ? parseJsonLayer(raw.text, policy) : raw.format === "yaml" ? parseYamlLayer(raw.text, policy) : void 0;
502
+ if (parsed === void 0) return rejection([INVALID_FORMAT]);
503
+ return parsed.ok ? parsed.value : rejection(parsed.diagnostics);
493
504
  }
494
505
  });
506
+ return Object.freeze({
507
+ source,
508
+ trust: policy.grants
509
+ });
495
510
  }
496
511
  //#endregion
497
- export { adaptRawConfig, createConfigRuntimeSource, parseJsonLayer, parseResultAdapter, parseYamlLayer };
512
+ export { MAX_SOURCE_DEPTH, SCREEN_SOURCE_LIMITS, createConfigRuntimeSource, parseJsonLayer, parseYamlLayer };
498
513
 
499
514
  //# sourceMappingURL=index.js.map