@contractspec/lib.overlay-engine 3.7.6 → 3.7.7
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 +62 -74
- package/dist/index.d.ts +4 -4
- package/dist/index.js +135 -134
- package/dist/node/index.js +135 -134
- package/dist/node/signer.js +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/runtime.d.ts +2 -2
- package/dist/signer.js +1 -1
- package/dist/types.d.ts +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,85 +1,73 @@
|
|
|
1
1
|
# @contractspec/lib.overlay-engine
|
|
2
2
|
|
|
3
|
-
Website: https://contractspec.io
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
Runtime utilities for executing **OverlaySpecs** inside ContractSpec applications. The library tracks signed overlays, validates their safety guarantees, merges multi-scope overlays, and exposes React helpers to render personalized layouts without bespoke code.
|
|
7
|
-
|
|
8
|
-
## Features
|
|
9
|
-
|
|
10
|
-
- Type-safe OverlaySpec definitions that mirror the docs.
|
|
11
|
-
- Cryptographic signing and verification (Ed25519, RSA-PSS).
|
|
12
|
-
- Registry + validator that enforces policy boundaries.
|
|
13
|
-
- Deterministic merge engine for tenant / role / user overlays.
|
|
14
|
-
- Runtime helpers + React hooks for applying overlays to form/data view field definitions.
|
|
15
|
-
|
|
16
|
-
## Usage
|
|
17
|
-
|
|
18
|
-
```ts
|
|
19
|
-
import {
|
|
20
|
-
OverlayEngine,
|
|
21
|
-
OverlayRegistry,
|
|
22
|
-
defineOverlay,
|
|
23
|
-
signOverlay,
|
|
24
|
-
} from '@contractspec/lib.overlay-engine';
|
|
25
|
-
|
|
26
|
-
const registry = new OverlayRegistry();
|
|
27
|
-
const engine = new OverlayEngine({ registry });
|
|
28
|
-
|
|
29
|
-
const overlay = defineOverlay({
|
|
30
|
-
overlayId: 'acme-order-form',
|
|
31
|
-
version: '1.0.0',
|
|
32
|
-
appliesTo: {
|
|
33
|
-
capability: 'billing.createOrder',
|
|
34
|
-
tenantId: 'acme',
|
|
35
|
-
},
|
|
36
|
-
modifications: [
|
|
37
|
-
{ type: 'hideField', field: 'internalNotes' },
|
|
38
|
-
{ type: 'renameLabel', field: 'customerReference', newLabel: 'PO Number' },
|
|
39
|
-
],
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
const signed = await signOverlay(overlay, PRIVATE_KEY_PEM, { keyId: 'acme' });
|
|
43
|
-
registry.register(signed);
|
|
44
|
-
|
|
45
|
-
const result = engine.apply({
|
|
46
|
-
target: {
|
|
47
|
-
fields: baseFields,
|
|
48
|
-
},
|
|
49
|
-
context: { tenantId: 'acme', userId: 'u_123' },
|
|
50
|
-
capability: 'billing.createOrder',
|
|
51
|
-
});
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
See `docs/tech/personalization/overlay-engine.md` for additional details.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
3
|
+
Website: https://contractspec.io
|
|
77
4
|
|
|
5
|
+
**Runtime overlay engine for ContractSpec personalization and adaptive UI rendering.**
|
|
78
6
|
|
|
7
|
+
## What It Provides
|
|
79
8
|
|
|
9
|
+
- **Layer**: lib.
|
|
10
|
+
- **Consumers**: personalization, example-shared-ui, bundles.
|
|
11
|
+
- Related ContractSpec packages include `@contractspec/lib.contracts-spec`, `@contractspec/tool.bun`, `@contractspec/tool.typescript`.
|
|
12
|
+
- Related ContractSpec packages include `@contractspec/lib.contracts-spec`, `@contractspec/tool.bun`, `@contractspec/tool.typescript`.
|
|
80
13
|
|
|
14
|
+
## Installation
|
|
81
15
|
|
|
16
|
+
`npm install @contractspec/lib.overlay-engine`
|
|
82
17
|
|
|
18
|
+
or
|
|
83
19
|
|
|
20
|
+
`bun add @contractspec/lib.overlay-engine`
|
|
84
21
|
|
|
22
|
+
## Usage
|
|
85
23
|
|
|
24
|
+
Import the root entrypoint from `@contractspec/lib.overlay-engine`, or choose a documented subpath when you only need one part of the package surface.
|
|
25
|
+
|
|
26
|
+
## Architecture
|
|
27
|
+
|
|
28
|
+
- `src/index.ts` is the root public barrel and package entrypoint.
|
|
29
|
+
- `src/merger.ts` is part of the package's public or composition surface.
|
|
30
|
+
- `src/react.ts` is part of the package's public or composition surface.
|
|
31
|
+
- `src/registry.ts` is part of the package's public or composition surface.
|
|
32
|
+
- `src/runtime.ts` is part of the package's public or composition surface.
|
|
33
|
+
- `src/signer.ts` is part of the package's public or composition surface.
|
|
34
|
+
- `src/spec.ts` is part of the package's public or composition surface.
|
|
35
|
+
- `src/types.ts` is shared public type definitions.
|
|
36
|
+
|
|
37
|
+
## Public Entry Points
|
|
38
|
+
|
|
39
|
+
- Export `.` resolves through `./src/index.ts`.
|
|
40
|
+
- Export `./merger` resolves through `./src/merger.ts`.
|
|
41
|
+
- Export `./react` resolves through `./src/react.ts`.
|
|
42
|
+
- Export `./registry` resolves through `./src/registry.ts`.
|
|
43
|
+
- Export `./runtime` resolves through `./src/runtime.ts`.
|
|
44
|
+
- Export `./signer` resolves through `./src/signer.ts`.
|
|
45
|
+
- Export `./spec` resolves through `./src/spec.ts`.
|
|
46
|
+
- Export `./types` resolves through `./src/types.ts`.
|
|
47
|
+
- Export `./validator` resolves through `./src/validator.ts`.
|
|
48
|
+
|
|
49
|
+
## Local Commands
|
|
50
|
+
|
|
51
|
+
- `bun run dev` — contractspec-bun-build dev
|
|
52
|
+
- `bun run build` — bun run prebuild && bun run build:bundle && bun run build:types
|
|
53
|
+
- `bun run test` — bun test --pass-with-no-tests
|
|
54
|
+
- `bun run lint` — bun lint:fix
|
|
55
|
+
- `bun run lint:check` — biome check .
|
|
56
|
+
- `bun run lint:fix` — biome check --write --unsafe --only=nursery/useSortedClasses . && biome check --write .
|
|
57
|
+
- `bun run typecheck` — tsc --noEmit
|
|
58
|
+
- `bun run publish:pkg` — bun publish --tolerate-republish --ignore-scripts --verbose
|
|
59
|
+
- `bun run publish:pkg:canary` — bun publish:pkg --tag canary
|
|
60
|
+
- `bun run clean` — rimraf dist .turbo
|
|
61
|
+
- `bun run build:bundle` — contractspec-bun-build transpile
|
|
62
|
+
- `bun run build:types` — contractspec-bun-build types
|
|
63
|
+
- `bun run prebuild` — contractspec-bun-build prebuild
|
|
64
|
+
|
|
65
|
+
## Recent Updates
|
|
66
|
+
|
|
67
|
+
- Replace eslint+prettier by biomejs to optimize speed.
|
|
68
|
+
|
|
69
|
+
## Notes
|
|
70
|
+
|
|
71
|
+
- Overlay spec schema is a contract — changes are breaking for all consumers.
|
|
72
|
+
- Signer must preserve cryptographic integrity; do not alter signing algorithm without migration.
|
|
73
|
+
- Merger logic must be idempotent — applying the same overlay twice must produce identical results.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
export * from './
|
|
2
|
-
export * from './types';
|
|
1
|
+
export * from './merger';
|
|
3
2
|
export * from './registry';
|
|
4
|
-
export * from './validator';
|
|
5
3
|
export * from './runtime';
|
|
6
|
-
export * from './merger';
|
|
7
4
|
export * from './signer';
|
|
5
|
+
export * from './spec';
|
|
6
|
+
export * from './types';
|
|
7
|
+
export * from './validator';
|
package/dist/index.js
CHANGED
|
@@ -1,15 +1,127 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
// src/
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
2
|
+
// src/merger.ts
|
|
3
|
+
function applyOverlayModifications(target, overlays, options = {}) {
|
|
4
|
+
if (!overlays.length) {
|
|
5
|
+
return target;
|
|
6
|
+
}
|
|
7
|
+
const states = target.fields.map((field) => ({
|
|
8
|
+
key: field.key,
|
|
9
|
+
field: { ...field },
|
|
10
|
+
hidden: field.visible === false
|
|
11
|
+
}));
|
|
12
|
+
const fieldMap = new Map(states.map((state) => [state.key, state]));
|
|
13
|
+
let orderSequence = target.fields.map((field) => field.key);
|
|
14
|
+
const handleMissing = (field, overlayId) => {
|
|
15
|
+
if (options.strict) {
|
|
16
|
+
throw new Error(`Overlay "${overlayId}" referenced unknown field "${field}".`);
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
overlays.forEach((overlay) => {
|
|
20
|
+
overlay.modifications.forEach((modification) => {
|
|
21
|
+
switch (modification.type) {
|
|
22
|
+
case "hideField": {
|
|
23
|
+
const state = fieldMap.get(modification.field);
|
|
24
|
+
if (!state)
|
|
25
|
+
return handleMissing(modification.field, overlay.overlayId);
|
|
26
|
+
state.hidden = true;
|
|
27
|
+
state.field.visible = false;
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
case "renameLabel": {
|
|
31
|
+
const state = fieldMap.get(modification.field);
|
|
32
|
+
if (!state)
|
|
33
|
+
return handleMissing(modification.field, overlay.overlayId);
|
|
34
|
+
state.field.label = modification.newLabel;
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
case "setDefault": {
|
|
38
|
+
const state = fieldMap.get(modification.field);
|
|
39
|
+
if (!state)
|
|
40
|
+
return handleMissing(modification.field, overlay.overlayId);
|
|
41
|
+
state.field.defaultValue = modification.value;
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
case "addHelpText": {
|
|
45
|
+
const state = fieldMap.get(modification.field);
|
|
46
|
+
if (!state)
|
|
47
|
+
return handleMissing(modification.field, overlay.overlayId);
|
|
48
|
+
state.field.helpText = modification.text;
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
case "makeRequired": {
|
|
52
|
+
const state = fieldMap.get(modification.field);
|
|
53
|
+
if (!state)
|
|
54
|
+
return handleMissing(modification.field, overlay.overlayId);
|
|
55
|
+
state.field.required = modification.required ?? true;
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
case "reorderFields": {
|
|
59
|
+
const { filtered, missing } = normalizeOrderList(modification.fields, fieldMap);
|
|
60
|
+
if (missing.length && options.strict) {
|
|
61
|
+
missing.forEach((field) => handleMissing(field, overlay.overlayId));
|
|
62
|
+
}
|
|
63
|
+
orderSequence = applyReorder(orderSequence, filtered);
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
default:
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
const visibleFields = [];
|
|
72
|
+
const seen = new Set;
|
|
73
|
+
orderSequence.forEach((key) => {
|
|
74
|
+
const state = fieldMap.get(key);
|
|
75
|
+
if (!state || state.hidden) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
seen.add(key);
|
|
79
|
+
visibleFields.push(state.field);
|
|
80
|
+
});
|
|
81
|
+
states.forEach((state) => {
|
|
82
|
+
if (state.hidden || seen.has(state.key)) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
visibleFields.push(state.field);
|
|
86
|
+
});
|
|
87
|
+
visibleFields.forEach((field, index) => {
|
|
88
|
+
field.order = index;
|
|
89
|
+
field.visible = true;
|
|
90
|
+
});
|
|
91
|
+
return {
|
|
92
|
+
...target,
|
|
93
|
+
fields: visibleFields
|
|
94
|
+
};
|
|
12
95
|
}
|
|
96
|
+
function normalizeOrderList(fields, fieldMap) {
|
|
97
|
+
const filtered = [];
|
|
98
|
+
const missing = [];
|
|
99
|
+
const seen = new Set;
|
|
100
|
+
fields.forEach((field) => {
|
|
101
|
+
if (!field?.trim()) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (!fieldMap.has(field)) {
|
|
105
|
+
missing.push(field);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (seen.has(field)) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
seen.add(field);
|
|
112
|
+
filtered.push(field);
|
|
113
|
+
});
|
|
114
|
+
return { filtered, missing };
|
|
115
|
+
}
|
|
116
|
+
function applyReorder(sequence, orderedFields) {
|
|
117
|
+
if (!orderedFields.length) {
|
|
118
|
+
return sequence;
|
|
119
|
+
}
|
|
120
|
+
const orderedSet = new Set(orderedFields);
|
|
121
|
+
const remainder = sequence.filter((key) => !orderedSet.has(key));
|
|
122
|
+
return [...orderedFields, ...remainder];
|
|
123
|
+
}
|
|
124
|
+
|
|
13
125
|
// src/validator.ts
|
|
14
126
|
var TARGET_KEYS = [
|
|
15
127
|
"capability",
|
|
@@ -270,129 +382,6 @@ function matches(appliesTo, ctx) {
|
|
|
270
382
|
return true;
|
|
271
383
|
}
|
|
272
384
|
|
|
273
|
-
// src/merger.ts
|
|
274
|
-
function applyOverlayModifications(target, overlays, options = {}) {
|
|
275
|
-
if (!overlays.length) {
|
|
276
|
-
return target;
|
|
277
|
-
}
|
|
278
|
-
const states = target.fields.map((field) => ({
|
|
279
|
-
key: field.key,
|
|
280
|
-
field: { ...field },
|
|
281
|
-
hidden: field.visible === false
|
|
282
|
-
}));
|
|
283
|
-
const fieldMap = new Map(states.map((state) => [state.key, state]));
|
|
284
|
-
let orderSequence = target.fields.map((field) => field.key);
|
|
285
|
-
const handleMissing = (field, overlayId) => {
|
|
286
|
-
if (options.strict) {
|
|
287
|
-
throw new Error(`Overlay "${overlayId}" referenced unknown field "${field}".`);
|
|
288
|
-
}
|
|
289
|
-
};
|
|
290
|
-
overlays.forEach((overlay) => {
|
|
291
|
-
overlay.modifications.forEach((modification) => {
|
|
292
|
-
switch (modification.type) {
|
|
293
|
-
case "hideField": {
|
|
294
|
-
const state = fieldMap.get(modification.field);
|
|
295
|
-
if (!state)
|
|
296
|
-
return handleMissing(modification.field, overlay.overlayId);
|
|
297
|
-
state.hidden = true;
|
|
298
|
-
state.field.visible = false;
|
|
299
|
-
break;
|
|
300
|
-
}
|
|
301
|
-
case "renameLabel": {
|
|
302
|
-
const state = fieldMap.get(modification.field);
|
|
303
|
-
if (!state)
|
|
304
|
-
return handleMissing(modification.field, overlay.overlayId);
|
|
305
|
-
state.field.label = modification.newLabel;
|
|
306
|
-
break;
|
|
307
|
-
}
|
|
308
|
-
case "setDefault": {
|
|
309
|
-
const state = fieldMap.get(modification.field);
|
|
310
|
-
if (!state)
|
|
311
|
-
return handleMissing(modification.field, overlay.overlayId);
|
|
312
|
-
state.field.defaultValue = modification.value;
|
|
313
|
-
break;
|
|
314
|
-
}
|
|
315
|
-
case "addHelpText": {
|
|
316
|
-
const state = fieldMap.get(modification.field);
|
|
317
|
-
if (!state)
|
|
318
|
-
return handleMissing(modification.field, overlay.overlayId);
|
|
319
|
-
state.field.helpText = modification.text;
|
|
320
|
-
break;
|
|
321
|
-
}
|
|
322
|
-
case "makeRequired": {
|
|
323
|
-
const state = fieldMap.get(modification.field);
|
|
324
|
-
if (!state)
|
|
325
|
-
return handleMissing(modification.field, overlay.overlayId);
|
|
326
|
-
state.field.required = modification.required ?? true;
|
|
327
|
-
break;
|
|
328
|
-
}
|
|
329
|
-
case "reorderFields": {
|
|
330
|
-
const { filtered, missing } = normalizeOrderList(modification.fields, fieldMap);
|
|
331
|
-
if (missing.length && options.strict) {
|
|
332
|
-
missing.forEach((field) => handleMissing(field, overlay.overlayId));
|
|
333
|
-
}
|
|
334
|
-
orderSequence = applyReorder(orderSequence, filtered);
|
|
335
|
-
break;
|
|
336
|
-
}
|
|
337
|
-
default:
|
|
338
|
-
break;
|
|
339
|
-
}
|
|
340
|
-
});
|
|
341
|
-
});
|
|
342
|
-
const visibleFields = [];
|
|
343
|
-
const seen = new Set;
|
|
344
|
-
orderSequence.forEach((key) => {
|
|
345
|
-
const state = fieldMap.get(key);
|
|
346
|
-
if (!state || state.hidden) {
|
|
347
|
-
return;
|
|
348
|
-
}
|
|
349
|
-
seen.add(key);
|
|
350
|
-
visibleFields.push(state.field);
|
|
351
|
-
});
|
|
352
|
-
states.forEach((state) => {
|
|
353
|
-
if (state.hidden || seen.has(state.key)) {
|
|
354
|
-
return;
|
|
355
|
-
}
|
|
356
|
-
visibleFields.push(state.field);
|
|
357
|
-
});
|
|
358
|
-
visibleFields.forEach((field, index) => {
|
|
359
|
-
field.order = index;
|
|
360
|
-
field.visible = true;
|
|
361
|
-
});
|
|
362
|
-
return {
|
|
363
|
-
...target,
|
|
364
|
-
fields: visibleFields
|
|
365
|
-
};
|
|
366
|
-
}
|
|
367
|
-
function normalizeOrderList(fields, fieldMap) {
|
|
368
|
-
const filtered = [];
|
|
369
|
-
const missing = [];
|
|
370
|
-
const seen = new Set;
|
|
371
|
-
fields.forEach((field) => {
|
|
372
|
-
if (!field?.trim()) {
|
|
373
|
-
return;
|
|
374
|
-
}
|
|
375
|
-
if (!fieldMap.has(field)) {
|
|
376
|
-
missing.push(field);
|
|
377
|
-
return;
|
|
378
|
-
}
|
|
379
|
-
if (seen.has(field)) {
|
|
380
|
-
return;
|
|
381
|
-
}
|
|
382
|
-
seen.add(field);
|
|
383
|
-
filtered.push(field);
|
|
384
|
-
});
|
|
385
|
-
return { filtered, missing };
|
|
386
|
-
}
|
|
387
|
-
function applyReorder(sequence, orderedFields) {
|
|
388
|
-
if (!orderedFields.length) {
|
|
389
|
-
return sequence;
|
|
390
|
-
}
|
|
391
|
-
const orderedSet = new Set(orderedFields);
|
|
392
|
-
const remainder = sequence.filter((key) => !orderedSet.has(key));
|
|
393
|
-
return [...orderedFields, ...remainder];
|
|
394
|
-
}
|
|
395
|
-
|
|
396
385
|
// src/runtime.ts
|
|
397
386
|
class OverlayEngine {
|
|
398
387
|
registry;
|
|
@@ -445,7 +434,6 @@ function extractContext(params) {
|
|
|
445
434
|
}
|
|
446
435
|
|
|
447
436
|
// src/signer.ts
|
|
448
|
-
import stringify from "fast-json-stable-stringify";
|
|
449
437
|
import {
|
|
450
438
|
constants,
|
|
451
439
|
createPrivateKey,
|
|
@@ -453,6 +441,7 @@ import {
|
|
|
453
441
|
sign,
|
|
454
442
|
verify
|
|
455
443
|
} from "crypto";
|
|
444
|
+
import stringify from "fast-json-stable-stringify";
|
|
456
445
|
function signOverlay(spec, privateKey, options = {}) {
|
|
457
446
|
const algorithm = options.algorithm ?? "ed25519";
|
|
458
447
|
const keyObject = typeof privateKey === "string" || Buffer.isBuffer(privateKey) ? createPrivateKey(privateKey) : privateKey;
|
|
@@ -519,6 +508,18 @@ function toIso(value) {
|
|
|
519
508
|
}
|
|
520
509
|
return value.toISOString();
|
|
521
510
|
}
|
|
511
|
+
|
|
512
|
+
// src/spec.ts
|
|
513
|
+
var OVERLAY_SCOPE_ORDER = [
|
|
514
|
+
"tenantId",
|
|
515
|
+
"role",
|
|
516
|
+
"userId",
|
|
517
|
+
"device",
|
|
518
|
+
"tags"
|
|
519
|
+
];
|
|
520
|
+
function defineOverlay(spec) {
|
|
521
|
+
return spec;
|
|
522
|
+
}
|
|
522
523
|
export {
|
|
523
524
|
verifyOverlaySignature,
|
|
524
525
|
validateOverlaySpec,
|
package/dist/node/index.js
CHANGED
|
@@ -1,14 +1,126 @@
|
|
|
1
|
-
// src/
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
// src/merger.ts
|
|
2
|
+
function applyOverlayModifications(target, overlays, options = {}) {
|
|
3
|
+
if (!overlays.length) {
|
|
4
|
+
return target;
|
|
5
|
+
}
|
|
6
|
+
const states = target.fields.map((field) => ({
|
|
7
|
+
key: field.key,
|
|
8
|
+
field: { ...field },
|
|
9
|
+
hidden: field.visible === false
|
|
10
|
+
}));
|
|
11
|
+
const fieldMap = new Map(states.map((state) => [state.key, state]));
|
|
12
|
+
let orderSequence = target.fields.map((field) => field.key);
|
|
13
|
+
const handleMissing = (field, overlayId) => {
|
|
14
|
+
if (options.strict) {
|
|
15
|
+
throw new Error(`Overlay "${overlayId}" referenced unknown field "${field}".`);
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
overlays.forEach((overlay) => {
|
|
19
|
+
overlay.modifications.forEach((modification) => {
|
|
20
|
+
switch (modification.type) {
|
|
21
|
+
case "hideField": {
|
|
22
|
+
const state = fieldMap.get(modification.field);
|
|
23
|
+
if (!state)
|
|
24
|
+
return handleMissing(modification.field, overlay.overlayId);
|
|
25
|
+
state.hidden = true;
|
|
26
|
+
state.field.visible = false;
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
case "renameLabel": {
|
|
30
|
+
const state = fieldMap.get(modification.field);
|
|
31
|
+
if (!state)
|
|
32
|
+
return handleMissing(modification.field, overlay.overlayId);
|
|
33
|
+
state.field.label = modification.newLabel;
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
case "setDefault": {
|
|
37
|
+
const state = fieldMap.get(modification.field);
|
|
38
|
+
if (!state)
|
|
39
|
+
return handleMissing(modification.field, overlay.overlayId);
|
|
40
|
+
state.field.defaultValue = modification.value;
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
case "addHelpText": {
|
|
44
|
+
const state = fieldMap.get(modification.field);
|
|
45
|
+
if (!state)
|
|
46
|
+
return handleMissing(modification.field, overlay.overlayId);
|
|
47
|
+
state.field.helpText = modification.text;
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
case "makeRequired": {
|
|
51
|
+
const state = fieldMap.get(modification.field);
|
|
52
|
+
if (!state)
|
|
53
|
+
return handleMissing(modification.field, overlay.overlayId);
|
|
54
|
+
state.field.required = modification.required ?? true;
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
case "reorderFields": {
|
|
58
|
+
const { filtered, missing } = normalizeOrderList(modification.fields, fieldMap);
|
|
59
|
+
if (missing.length && options.strict) {
|
|
60
|
+
missing.forEach((field) => handleMissing(field, overlay.overlayId));
|
|
61
|
+
}
|
|
62
|
+
orderSequence = applyReorder(orderSequence, filtered);
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
default:
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
const visibleFields = [];
|
|
71
|
+
const seen = new Set;
|
|
72
|
+
orderSequence.forEach((key) => {
|
|
73
|
+
const state = fieldMap.get(key);
|
|
74
|
+
if (!state || state.hidden) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
seen.add(key);
|
|
78
|
+
visibleFields.push(state.field);
|
|
79
|
+
});
|
|
80
|
+
states.forEach((state) => {
|
|
81
|
+
if (state.hidden || seen.has(state.key)) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
visibleFields.push(state.field);
|
|
85
|
+
});
|
|
86
|
+
visibleFields.forEach((field, index) => {
|
|
87
|
+
field.order = index;
|
|
88
|
+
field.visible = true;
|
|
89
|
+
});
|
|
90
|
+
return {
|
|
91
|
+
...target,
|
|
92
|
+
fields: visibleFields
|
|
93
|
+
};
|
|
11
94
|
}
|
|
95
|
+
function normalizeOrderList(fields, fieldMap) {
|
|
96
|
+
const filtered = [];
|
|
97
|
+
const missing = [];
|
|
98
|
+
const seen = new Set;
|
|
99
|
+
fields.forEach((field) => {
|
|
100
|
+
if (!field?.trim()) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (!fieldMap.has(field)) {
|
|
104
|
+
missing.push(field);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (seen.has(field)) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
seen.add(field);
|
|
111
|
+
filtered.push(field);
|
|
112
|
+
});
|
|
113
|
+
return { filtered, missing };
|
|
114
|
+
}
|
|
115
|
+
function applyReorder(sequence, orderedFields) {
|
|
116
|
+
if (!orderedFields.length) {
|
|
117
|
+
return sequence;
|
|
118
|
+
}
|
|
119
|
+
const orderedSet = new Set(orderedFields);
|
|
120
|
+
const remainder = sequence.filter((key) => !orderedSet.has(key));
|
|
121
|
+
return [...orderedFields, ...remainder];
|
|
122
|
+
}
|
|
123
|
+
|
|
12
124
|
// src/validator.ts
|
|
13
125
|
var TARGET_KEYS = [
|
|
14
126
|
"capability",
|
|
@@ -269,129 +381,6 @@ function matches(appliesTo, ctx) {
|
|
|
269
381
|
return true;
|
|
270
382
|
}
|
|
271
383
|
|
|
272
|
-
// src/merger.ts
|
|
273
|
-
function applyOverlayModifications(target, overlays, options = {}) {
|
|
274
|
-
if (!overlays.length) {
|
|
275
|
-
return target;
|
|
276
|
-
}
|
|
277
|
-
const states = target.fields.map((field) => ({
|
|
278
|
-
key: field.key,
|
|
279
|
-
field: { ...field },
|
|
280
|
-
hidden: field.visible === false
|
|
281
|
-
}));
|
|
282
|
-
const fieldMap = new Map(states.map((state) => [state.key, state]));
|
|
283
|
-
let orderSequence = target.fields.map((field) => field.key);
|
|
284
|
-
const handleMissing = (field, overlayId) => {
|
|
285
|
-
if (options.strict) {
|
|
286
|
-
throw new Error(`Overlay "${overlayId}" referenced unknown field "${field}".`);
|
|
287
|
-
}
|
|
288
|
-
};
|
|
289
|
-
overlays.forEach((overlay) => {
|
|
290
|
-
overlay.modifications.forEach((modification) => {
|
|
291
|
-
switch (modification.type) {
|
|
292
|
-
case "hideField": {
|
|
293
|
-
const state = fieldMap.get(modification.field);
|
|
294
|
-
if (!state)
|
|
295
|
-
return handleMissing(modification.field, overlay.overlayId);
|
|
296
|
-
state.hidden = true;
|
|
297
|
-
state.field.visible = false;
|
|
298
|
-
break;
|
|
299
|
-
}
|
|
300
|
-
case "renameLabel": {
|
|
301
|
-
const state = fieldMap.get(modification.field);
|
|
302
|
-
if (!state)
|
|
303
|
-
return handleMissing(modification.field, overlay.overlayId);
|
|
304
|
-
state.field.label = modification.newLabel;
|
|
305
|
-
break;
|
|
306
|
-
}
|
|
307
|
-
case "setDefault": {
|
|
308
|
-
const state = fieldMap.get(modification.field);
|
|
309
|
-
if (!state)
|
|
310
|
-
return handleMissing(modification.field, overlay.overlayId);
|
|
311
|
-
state.field.defaultValue = modification.value;
|
|
312
|
-
break;
|
|
313
|
-
}
|
|
314
|
-
case "addHelpText": {
|
|
315
|
-
const state = fieldMap.get(modification.field);
|
|
316
|
-
if (!state)
|
|
317
|
-
return handleMissing(modification.field, overlay.overlayId);
|
|
318
|
-
state.field.helpText = modification.text;
|
|
319
|
-
break;
|
|
320
|
-
}
|
|
321
|
-
case "makeRequired": {
|
|
322
|
-
const state = fieldMap.get(modification.field);
|
|
323
|
-
if (!state)
|
|
324
|
-
return handleMissing(modification.field, overlay.overlayId);
|
|
325
|
-
state.field.required = modification.required ?? true;
|
|
326
|
-
break;
|
|
327
|
-
}
|
|
328
|
-
case "reorderFields": {
|
|
329
|
-
const { filtered, missing } = normalizeOrderList(modification.fields, fieldMap);
|
|
330
|
-
if (missing.length && options.strict) {
|
|
331
|
-
missing.forEach((field) => handleMissing(field, overlay.overlayId));
|
|
332
|
-
}
|
|
333
|
-
orderSequence = applyReorder(orderSequence, filtered);
|
|
334
|
-
break;
|
|
335
|
-
}
|
|
336
|
-
default:
|
|
337
|
-
break;
|
|
338
|
-
}
|
|
339
|
-
});
|
|
340
|
-
});
|
|
341
|
-
const visibleFields = [];
|
|
342
|
-
const seen = new Set;
|
|
343
|
-
orderSequence.forEach((key) => {
|
|
344
|
-
const state = fieldMap.get(key);
|
|
345
|
-
if (!state || state.hidden) {
|
|
346
|
-
return;
|
|
347
|
-
}
|
|
348
|
-
seen.add(key);
|
|
349
|
-
visibleFields.push(state.field);
|
|
350
|
-
});
|
|
351
|
-
states.forEach((state) => {
|
|
352
|
-
if (state.hidden || seen.has(state.key)) {
|
|
353
|
-
return;
|
|
354
|
-
}
|
|
355
|
-
visibleFields.push(state.field);
|
|
356
|
-
});
|
|
357
|
-
visibleFields.forEach((field, index) => {
|
|
358
|
-
field.order = index;
|
|
359
|
-
field.visible = true;
|
|
360
|
-
});
|
|
361
|
-
return {
|
|
362
|
-
...target,
|
|
363
|
-
fields: visibleFields
|
|
364
|
-
};
|
|
365
|
-
}
|
|
366
|
-
function normalizeOrderList(fields, fieldMap) {
|
|
367
|
-
const filtered = [];
|
|
368
|
-
const missing = [];
|
|
369
|
-
const seen = new Set;
|
|
370
|
-
fields.forEach((field) => {
|
|
371
|
-
if (!field?.trim()) {
|
|
372
|
-
return;
|
|
373
|
-
}
|
|
374
|
-
if (!fieldMap.has(field)) {
|
|
375
|
-
missing.push(field);
|
|
376
|
-
return;
|
|
377
|
-
}
|
|
378
|
-
if (seen.has(field)) {
|
|
379
|
-
return;
|
|
380
|
-
}
|
|
381
|
-
seen.add(field);
|
|
382
|
-
filtered.push(field);
|
|
383
|
-
});
|
|
384
|
-
return { filtered, missing };
|
|
385
|
-
}
|
|
386
|
-
function applyReorder(sequence, orderedFields) {
|
|
387
|
-
if (!orderedFields.length) {
|
|
388
|
-
return sequence;
|
|
389
|
-
}
|
|
390
|
-
const orderedSet = new Set(orderedFields);
|
|
391
|
-
const remainder = sequence.filter((key) => !orderedSet.has(key));
|
|
392
|
-
return [...orderedFields, ...remainder];
|
|
393
|
-
}
|
|
394
|
-
|
|
395
384
|
// src/runtime.ts
|
|
396
385
|
class OverlayEngine {
|
|
397
386
|
registry;
|
|
@@ -444,7 +433,6 @@ function extractContext(params) {
|
|
|
444
433
|
}
|
|
445
434
|
|
|
446
435
|
// src/signer.ts
|
|
447
|
-
import stringify from "fast-json-stable-stringify";
|
|
448
436
|
import {
|
|
449
437
|
constants,
|
|
450
438
|
createPrivateKey,
|
|
@@ -452,6 +440,7 @@ import {
|
|
|
452
440
|
sign,
|
|
453
441
|
verify
|
|
454
442
|
} from "crypto";
|
|
443
|
+
import stringify from "fast-json-stable-stringify";
|
|
455
444
|
function signOverlay(spec, privateKey, options = {}) {
|
|
456
445
|
const algorithm = options.algorithm ?? "ed25519";
|
|
457
446
|
const keyObject = typeof privateKey === "string" || Buffer.isBuffer(privateKey) ? createPrivateKey(privateKey) : privateKey;
|
|
@@ -518,6 +507,18 @@ function toIso(value) {
|
|
|
518
507
|
}
|
|
519
508
|
return value.toISOString();
|
|
520
509
|
}
|
|
510
|
+
|
|
511
|
+
// src/spec.ts
|
|
512
|
+
var OVERLAY_SCOPE_ORDER = [
|
|
513
|
+
"tenantId",
|
|
514
|
+
"role",
|
|
515
|
+
"userId",
|
|
516
|
+
"device",
|
|
517
|
+
"tags"
|
|
518
|
+
];
|
|
519
|
+
function defineOverlay(spec) {
|
|
520
|
+
return spec;
|
|
521
|
+
}
|
|
521
522
|
export {
|
|
522
523
|
verifyOverlaySignature,
|
|
523
524
|
validateOverlaySpec,
|
package/dist/node/signer.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
// src/signer.ts
|
|
2
|
-
import stringify from "fast-json-stable-stringify";
|
|
3
2
|
import {
|
|
4
3
|
constants,
|
|
5
4
|
createPrivateKey,
|
|
@@ -7,6 +6,7 @@ import {
|
|
|
7
6
|
sign,
|
|
8
7
|
verify
|
|
9
8
|
} from "crypto";
|
|
9
|
+
import stringify from "fast-json-stable-stringify";
|
|
10
10
|
function signOverlay(spec, privateKey, options = {}) {
|
|
11
11
|
const algorithm = options.algorithm ?? "ed25519";
|
|
12
12
|
const keyObject = typeof privateKey === "string" || Buffer.isBuffer(privateKey) ? createPrivateKey(privateKey) : privateKey;
|
package/dist/react.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type DependencyList } from 'react';
|
|
2
|
-
import type { OverlayRenderable } from './types';
|
|
3
2
|
import type { OverlayApplyParams, OverlayRuntimeResult } from './runtime';
|
|
4
3
|
import { OverlayEngine } from './runtime';
|
|
4
|
+
import type { OverlayRenderable } from './types';
|
|
5
5
|
export declare function useOverlay<T extends OverlayRenderable>(engine: OverlayEngine | undefined, params: OverlayApplyParams<T>, deps?: DependencyList): OverlayRuntimeResult<T>;
|
|
6
6
|
export declare function useOverlayFields<T extends OverlayRenderable>(engine: OverlayEngine | undefined, params: OverlayApplyParams<T>, deps?: DependencyList): T['fields'];
|
package/dist/runtime.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type ApplyOverlayOptions } from './merger';
|
|
2
|
-
import {
|
|
2
|
+
import { type OverlayLookup, OverlayRegistry } from './registry';
|
|
3
3
|
import type { SignedOverlaySpec } from './spec';
|
|
4
|
-
import type {
|
|
4
|
+
import type { OverlayAuditEvent, OverlayRenderable } from './types';
|
|
5
5
|
export interface OverlayEngineOptions {
|
|
6
6
|
registry: OverlayRegistry;
|
|
7
7
|
audit?: (event: OverlayAuditEvent) => void;
|
package/dist/signer.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// src/signer.ts
|
|
3
|
-
import stringify from "fast-json-stable-stringify";
|
|
4
3
|
import {
|
|
5
4
|
constants,
|
|
6
5
|
createPrivateKey,
|
|
@@ -8,6 +7,7 @@ import {
|
|
|
8
7
|
sign,
|
|
9
8
|
verify
|
|
10
9
|
} from "crypto";
|
|
10
|
+
import stringify from "fast-json-stable-stringify";
|
|
11
11
|
function signOverlay(spec, privateKey, options = {}) {
|
|
12
12
|
const algorithm = options.algorithm ?? "ed25519";
|
|
13
13
|
const keyObject = typeof privateKey === "string" || Buffer.isBuffer(privateKey) ? createPrivateKey(privateKey) : privateKey;
|
package/dist/types.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contractspec/lib.overlay-engine",
|
|
3
|
-
"version": "3.7.
|
|
3
|
+
"version": "3.7.7",
|
|
4
4
|
"description": "Runtime overlay engine for ContractSpec personalization and adaptive UI rendering.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"contractspec",
|
|
@@ -25,14 +25,14 @@
|
|
|
25
25
|
"dev": "contractspec-bun-build dev",
|
|
26
26
|
"clean": "rimraf dist .turbo",
|
|
27
27
|
"lint": "bun lint:fix",
|
|
28
|
-
"lint:fix": "
|
|
29
|
-
"lint:check": "
|
|
28
|
+
"lint:fix": "biome check --write --unsafe --only=nursery/useSortedClasses . && biome check --write .",
|
|
29
|
+
"lint:check": "biome check .",
|
|
30
30
|
"test": "bun test --pass-with-no-tests",
|
|
31
31
|
"prebuild": "contractspec-bun-build prebuild",
|
|
32
32
|
"typecheck": "tsc --noEmit"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@contractspec/lib.contracts-spec": "
|
|
35
|
+
"@contractspec/lib.contracts-spec": "4.0.0",
|
|
36
36
|
"fast-json-stable-stringify": "^2.1.0"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|