@uwmd/core 1.7.0 → 1.9.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/agents/bancroft.js +4 -1
- package/dist/agents/bancroft.js.map +1 -1
- package/dist/asset-class.d.ts +98 -0
- package/dist/asset-class.d.ts.map +1 -0
- package/dist/asset-class.js +226 -0
- package/dist/asset-class.js.map +1 -0
- package/dist/browser.d.ts +26 -8
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +25 -5
- package/dist/browser.js.map +1 -1
- package/dist/calc/errors.d.ts +1 -1
- package/dist/calc/errors.d.ts.map +1 -1
- package/dist/calc/errors.js.map +1 -1
- package/dist/calc/evaluator.js +25 -1
- package/dist/calc/evaluator.js.map +1 -1
- package/dist/calc/prng.d.ts +59 -0
- package/dist/calc/prng.d.ts.map +1 -0
- package/dist/calc/prng.js +181 -0
- package/dist/calc/prng.js.map +1 -0
- package/dist/calc/sensitivity.d.ts +76 -0
- package/dist/calc/sensitivity.d.ts.map +1 -0
- package/dist/calc/sensitivity.js +160 -0
- package/dist/calc/sensitivity.js.map +1 -0
- package/dist/calc/stochastic.d.ts +97 -0
- package/dist/calc/stochastic.d.ts.map +1 -0
- package/dist/calc/stochastic.js +265 -0
- package/dist/calc/stochastic.js.map +1 -0
- package/dist/cascade.d.ts.map +1 -1
- package/dist/cascade.js +8 -4
- package/dist/cascade.js.map +1 -1
- package/dist/cli-args.d.ts +37 -0
- package/dist/cli-args.d.ts.map +1 -0
- package/dist/cli-args.js +93 -0
- package/dist/cli-args.js.map +1 -0
- package/dist/cli.js +155 -75
- package/dist/cli.js.map +1 -1
- package/dist/editor.d.ts +18 -1
- package/dist/editor.d.ts.map +1 -1
- package/dist/editor.js +68 -7
- package/dist/editor.js.map +1 -1
- package/dist/format-rules.d.ts +46 -0
- package/dist/format-rules.d.ts.map +1 -0
- package/dist/format-rules.js +134 -0
- package/dist/format-rules.js.map +1 -0
- package/dist/format.d.ts +4 -0
- package/dist/format.d.ts.map +1 -1
- package/dist/format.js +34 -4
- package/dist/format.js.map +1 -1
- package/dist/index.d.ts +27 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +26 -5
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +16 -4
- package/dist/init.js.map +1 -1
- package/dist/integrity-canonical.d.ts.map +1 -1
- package/dist/integrity-canonical.js +10 -2
- package/dist/integrity-canonical.js.map +1 -1
- package/dist/integrity.d.ts +92 -4
- package/dist/integrity.d.ts.map +1 -1
- package/dist/integrity.js +195 -9
- package/dist/integrity.js.map +1 -1
- package/dist/lease-up.d.ts +134 -0
- package/dist/lease-up.d.ts.map +1 -0
- package/dist/lease-up.js +196 -0
- package/dist/lease-up.js.map +1 -0
- package/dist/migrate-source-tags.d.ts +34 -0
- package/dist/migrate-source-tags.d.ts.map +1 -0
- package/dist/migrate-source-tags.js +179 -0
- package/dist/migrate-source-tags.js.map +1 -0
- package/dist/module-runtime.d.ts +57 -0
- package/dist/module-runtime.d.ts.map +1 -0
- package/dist/module-runtime.js +167 -0
- package/dist/module-runtime.js.map +1 -0
- package/dist/module-signing.d.ts +85 -0
- package/dist/module-signing.d.ts.map +1 -0
- package/dist/module-signing.js +136 -0
- package/dist/module-signing.js.map +1 -0
- package/dist/modules.d.ts +42 -0
- package/dist/modules.d.ts.map +1 -1
- package/dist/modules.js +124 -1
- package/dist/modules.js.map +1 -1
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +28 -1
- package/dist/parser.js.map +1 -1
- package/dist/protocol.d.ts +249 -24
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +387 -33
- package/dist/protocol.js.map +1 -1
- package/dist/renderer.d.ts +10 -0
- package/dist/renderer.d.ts.map +1 -1
- package/dist/renderer.js +72 -8
- package/dist/renderer.js.map +1 -1
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +9 -3
- package/dist/runner.js.map +1 -1
- package/dist/types.d.ts +124 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +44 -0
- package/dist/types.js.map +1 -1
- package/dist/validator.d.ts +26 -2
- package/dist/validator.d.ts.map +1 -1
- package/dist/validator.js +336 -19
- package/dist/validator.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +6 -2
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// Module runtime: actually *running* what a module manifest declares.
|
|
2
|
+
//
|
|
3
|
+
// Until now the module system registered manifests and stopped there. A
|
|
4
|
+
// manifest's `calculations` were reachable only by a host that pulled them out
|
|
5
|
+
// and evaluated them itself; its `validations` were shape-checked at load and
|
|
6
|
+
// then never executed by anything; its `sections` were declared and never
|
|
7
|
+
// looked for. Every assumption in the types was unverified, because nothing
|
|
8
|
+
// consumed them (RFC 0006 §Motivation).
|
|
9
|
+
//
|
|
10
|
+
// This file is the consumer. Three functions, in dependency order:
|
|
11
|
+
//
|
|
12
|
+
// evaluateModuleCalculations — the module's calcs, in declaration order,
|
|
13
|
+
// each seeing the ones before it
|
|
14
|
+
// checkModuleSections — declared `required: true` sections present
|
|
15
|
+
// validateAgainstModules — the module's `validations`, plus the above
|
|
16
|
+
//
|
|
17
|
+
// **No new evaluation machinery.** A validation `rule` is a safe expression in
|
|
18
|
+
// exactly the §VIII.1 grammar the calc engine already parses, so it runs
|
|
19
|
+
// through `evaluateCalc` like any other declaration. A module that could
|
|
20
|
+
// evaluate rules the calc engine cannot would be a second, unsandboxed
|
|
21
|
+
// expression language reachable from a third-party manifest, which is the one
|
|
22
|
+
// thing the module system must never become.
|
|
23
|
+
import { evaluateCalc } from './calc/index.js';
|
|
24
|
+
/**
|
|
25
|
+
* Evaluate every calculation the applicable modules declare, in declaration
|
|
26
|
+
* order, threading each result into the next as `prior_results`.
|
|
27
|
+
*
|
|
28
|
+
* Order matters and is the manifest's: `revpar_index` divides by `revpar`, so
|
|
29
|
+
* `revpar` must already be in `prior_results` when it runs. Sorting or
|
|
30
|
+
* parallelizing would break that for no gain — and a module author who orders
|
|
31
|
+
* their declarations wrongly gets a `CALC-REF` error naming the missing id,
|
|
32
|
+
* which is a better failure than a silent `null`.
|
|
33
|
+
*/
|
|
34
|
+
export function evaluateModuleCalculations(parsed, registry, options = {}) {
|
|
35
|
+
const outcomes = [];
|
|
36
|
+
const prior = {};
|
|
37
|
+
for (const manifest of applicableModules(parsed, registry, options)) {
|
|
38
|
+
for (const decl of manifest.calculations ?? []) {
|
|
39
|
+
const result = evaluateCalc(decl, { parsed, prior_results: prior, locale: 'en-US' });
|
|
40
|
+
outcomes.push({ module_id: manifest.id, result });
|
|
41
|
+
// Only successful results are published. This is principle rather than
|
|
42
|
+
// effect: an unresolved identifier already evaluates to `null` (§VIII.2),
|
|
43
|
+
// so a dependent sees `null` either way — but presenting a failed
|
|
44
|
+
// computation's value as a value is a different claim, and one this
|
|
45
|
+
// runtime should never make.
|
|
46
|
+
//
|
|
47
|
+
// The effect the null-propagation DOES have is that a broken calc
|
|
48
|
+
// degrades its dependents to `null`, which is indistinguishable from
|
|
49
|
+
// "inputs absent". `validateAgainstModules` reports the original failure
|
|
50
|
+
// as `MOD-CALC-ERROR` precisely so the cause is visible; without that,
|
|
51
|
+
// one typo in a formula quietly disables every rule downstream of it.
|
|
52
|
+
if (result.ok)
|
|
53
|
+
prior[decl.id] = result.value;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return outcomes;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Run the applicable modules' `validations` and section requirements.
|
|
60
|
+
*
|
|
61
|
+
* A rule is an assertion of what must be TRUE. It fires an issue when it
|
|
62
|
+
* evaluates to `false` — and, deliberately, **not** when it evaluates to
|
|
63
|
+
* `null`. Null is the calc engine's "the inputs are absent", and a document
|
|
64
|
+
* that simply does not carry `hotel_brand` has not violated a rule about
|
|
65
|
+
* franchise fees; it has said nothing about them. Reporting absence as
|
|
66
|
+
* violation would make every module rule fire on every partial file, which is
|
|
67
|
+
* most files most of the time.
|
|
68
|
+
*
|
|
69
|
+
* A rule that fails to *evaluate* — an unknown function, an unusable argument —
|
|
70
|
+
* is reported under `MOD-RULE-ERROR` rather than swallowed, and a module
|
|
71
|
+
* *calculation* that fails is reported under `MOD-CALC-ERROR`. A silently
|
|
72
|
+
* skipped rule is a rule the author believes is protecting them.
|
|
73
|
+
*/
|
|
74
|
+
export function validateAgainstModules(parsed, registry, options = {}) {
|
|
75
|
+
const issues = [];
|
|
76
|
+
const modules = applicableModules(parsed, registry, options);
|
|
77
|
+
for (const manifest of modules) {
|
|
78
|
+
issues.push(...checkModuleSections(parsed, manifest));
|
|
79
|
+
}
|
|
80
|
+
const prior = {};
|
|
81
|
+
for (const { module_id, result } of evaluateModuleCalculations(parsed, registry, options)) {
|
|
82
|
+
if (result.ok) {
|
|
83
|
+
prior[result.calc_id] = result.value;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
// A module calc that cannot evaluate is reported, not just skipped. Its
|
|
87
|
+
// dependents will resolve to `null` and their rules will fall silent, so
|
|
88
|
+
// this issue is often the only trace that anything went wrong.
|
|
89
|
+
issues.push({
|
|
90
|
+
code: 'MOD-CALC-ERROR',
|
|
91
|
+
severity: 'error',
|
|
92
|
+
message: `Module '${module_id}' calculation '${result.calc_id}' failed to evaluate: [${result.error?.code}] ${result.error?.message}`,
|
|
93
|
+
remediation: `Fix the formula in '${module_id}'. Any rule that reads '${result.calc_id}' is silently inconclusive until it is corrected.`,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
for (const manifest of modules) {
|
|
97
|
+
for (const rule of manifest.validations ?? []) {
|
|
98
|
+
issues.push(...runRule(parsed, manifest, rule, prior));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return issues;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Sections a module declares `required: true` must be present.
|
|
105
|
+
*
|
|
106
|
+
* Only presence. Validating a section's contents against its declared JSON
|
|
107
|
+
* Schema would need a JSON Schema validator, and `@uwmd/core` takes no such
|
|
108
|
+
* dependency — see the layering invariant. The schema in the manifest is
|
|
109
|
+
* normative and a host that already has a validator SHOULD apply it; core
|
|
110
|
+
* checks the part it can check honestly rather than shipping a
|
|
111
|
+
* half-implemented subset of JSON Schema that quietly accepts what a real
|
|
112
|
+
* validator would reject.
|
|
113
|
+
*/
|
|
114
|
+
export function checkModuleSections(parsed, manifest) {
|
|
115
|
+
const issues = [];
|
|
116
|
+
for (const section of manifest.sections ?? []) {
|
|
117
|
+
if (!section.required)
|
|
118
|
+
continue;
|
|
119
|
+
if (parsed.sections[section.id] === undefined) {
|
|
120
|
+
issues.push({
|
|
121
|
+
code: 'MOD-SECTION-MISSING',
|
|
122
|
+
severity: 'error',
|
|
123
|
+
section: section.id,
|
|
124
|
+
message: `Module '${manifest.id}' requires section '${section.id}' (${section.display_name}), which is missing.`,
|
|
125
|
+
remediation: `Add a '${section.id}' block, or stop loading '${manifest.id}' for this document.`,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return issues;
|
|
130
|
+
}
|
|
131
|
+
function runRule(parsed, manifest, rule, prior) {
|
|
132
|
+
const result = evaluateCalc({ id: rule.code, label: rule.code, formula: rule.rule, deterministic: true }, { parsed, prior_results: prior, locale: 'en-US' });
|
|
133
|
+
if (!result.ok) {
|
|
134
|
+
return [
|
|
135
|
+
{
|
|
136
|
+
code: 'MOD-RULE-ERROR',
|
|
137
|
+
severity: 'error',
|
|
138
|
+
message: `Module '${manifest.id}' rule '${rule.code}' failed to evaluate: [${result.error?.code}] ${result.error?.message}`,
|
|
139
|
+
remediation: `Fix the rule expression in '${manifest.id}', or stop loading the module until it is corrected.`,
|
|
140
|
+
},
|
|
141
|
+
];
|
|
142
|
+
}
|
|
143
|
+
// Absent inputs (`null`) are not violations — see the doc comment above.
|
|
144
|
+
if (result.value !== false)
|
|
145
|
+
return [];
|
|
146
|
+
return [
|
|
147
|
+
{
|
|
148
|
+
code: rule.code,
|
|
149
|
+
severity: rule.severity,
|
|
150
|
+
message: rule.message,
|
|
151
|
+
// Named so a reader can tell a module's finding from a built-in one
|
|
152
|
+
// without memorizing which prefixes belong to whom.
|
|
153
|
+
remediation: `Reported by module '${manifest.id}'.`,
|
|
154
|
+
},
|
|
155
|
+
];
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Modules that apply to this document: those declaring its asset class, plus
|
|
159
|
+
* those declaring none at all (which a manifest uses to mean "any class").
|
|
160
|
+
*/
|
|
161
|
+
function applicableModules(parsed, registry, options) {
|
|
162
|
+
const assetClass = options.assetClass ?? parsed.frontmatter?.asset_class;
|
|
163
|
+
return registry.modules.filter((m) => m.asset_classes === undefined ||
|
|
164
|
+
m.asset_classes.length === 0 ||
|
|
165
|
+
(assetClass !== undefined && m.asset_classes.includes(assetClass)));
|
|
166
|
+
}
|
|
167
|
+
//# sourceMappingURL=module-runtime.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"module-runtime.js","sourceRoot":"","sources":["../src/module-runtime.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { ModuleManifest, ModuleSignature, ProtocolError } from './protocol.js';
|
|
2
|
+
/** The one scheme protocol 1.x implements. See `ModuleSignature.scheme`. */
|
|
3
|
+
export declare const MODULE_SIGNATURE_SCHEME: "uwmd-keystore";
|
|
4
|
+
/**
|
|
5
|
+
* The exact bytes a module signature covers: RFC 8785 canonical JSON of the
|
|
6
|
+
* manifest with `signature` removed.
|
|
7
|
+
*
|
|
8
|
+
* Removed rather than nulled, so that signing a manifest and then verifying it
|
|
9
|
+
* produce byte-identical input — the failure mode where a signature verifies
|
|
10
|
+
* before it is attached and not after.
|
|
11
|
+
*/
|
|
12
|
+
export declare function moduleSigningPayload(manifest: ModuleManifest): string;
|
|
13
|
+
export type ModuleSignatureFailure =
|
|
14
|
+
/** The manifest carries no `signature` at all. */
|
|
15
|
+
'missing'
|
|
16
|
+
/** `scheme` names something this verifier does not implement. */
|
|
17
|
+
| 'unsupported_scheme'
|
|
18
|
+
/** `signature` is present but structurally invalid. */
|
|
19
|
+
| 'malformed'
|
|
20
|
+
/** `kid` names a key the host's store does not hold. */
|
|
21
|
+
| 'unknown_key'
|
|
22
|
+
/** The signature did not validate over the canonical manifest. */
|
|
23
|
+
| 'invalid';
|
|
24
|
+
export type ModuleSignatureVerdict = {
|
|
25
|
+
ok: true;
|
|
26
|
+
kid: string;
|
|
27
|
+
identity?: string;
|
|
28
|
+
} | {
|
|
29
|
+
ok: false;
|
|
30
|
+
reason: ModuleSignatureFailure;
|
|
31
|
+
error: ProtocolError;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Supplied by `@uwmd/signing`. Mirrors `BlockSignatureVerifier` rather than
|
|
35
|
+
* reusing it, because a module signature carries a `scheme` a block signature
|
|
36
|
+
* has no concept of; collapsing them would mean one of the two lies about its
|
|
37
|
+
* input.
|
|
38
|
+
*/
|
|
39
|
+
export interface ModuleSignatureVerifier {
|
|
40
|
+
verify(payload: string, signature: ModuleSignature): Promise<{
|
|
41
|
+
ok: true;
|
|
42
|
+
} | {
|
|
43
|
+
ok: false;
|
|
44
|
+
reason: 'unknown_kid' | 'bad_signature' | 'malformed';
|
|
45
|
+
}>;
|
|
46
|
+
}
|
|
47
|
+
export interface VerifyModuleSignatureOptions {
|
|
48
|
+
/** Without one, only the structural checks run and the verdict is `malformed`-or-better. */
|
|
49
|
+
verifier?: ModuleSignatureVerifier;
|
|
50
|
+
/**
|
|
51
|
+
* Identity allow-list. When set, a valid signature whose `identity` is absent
|
|
52
|
+
* or unlisted is refused.
|
|
53
|
+
*
|
|
54
|
+
* Note what this can and cannot do: `identity` is a claim inside the signed
|
|
55
|
+
* bytes, so a signature proves the *key holder* asserted it, never that the
|
|
56
|
+
* assertion is true. It is only as good as the host's decision to bind that
|
|
57
|
+
* `kid` to that identity in its key store.
|
|
58
|
+
*/
|
|
59
|
+
allowedIdentities?: readonly string[];
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Verify a module manifest's signature.
|
|
63
|
+
*
|
|
64
|
+
* The three refusals a host must keep apart are `missing` (nothing was
|
|
65
|
+
* claimed), `unknown_key` (something was claimed and this host cannot check
|
|
66
|
+
* it), and `invalid` (something was claimed and it is false). They call for
|
|
67
|
+
* three different responses — decide a policy, load a key, reject the module —
|
|
68
|
+
* and a verifier that reports them as one verdict makes all three
|
|
69
|
+
* indistinguishable at the point where the host has to act.
|
|
70
|
+
*/
|
|
71
|
+
export declare function verifyModuleSignature(manifest: ModuleManifest, options?: VerifyModuleSignatureOptions): Promise<ModuleSignatureVerdict>;
|
|
72
|
+
/**
|
|
73
|
+
* Structural checks, run before any cryptography.
|
|
74
|
+
*
|
|
75
|
+
* Separated out because they are the part a host can run with no key store at
|
|
76
|
+
* all, and because a malformed signature deserves its own code: telling an
|
|
77
|
+
* author "this did not verify" when the real problem is a missing `signed_at`
|
|
78
|
+
* sends them looking for tampering that never happened.
|
|
79
|
+
*/
|
|
80
|
+
export declare function checkSignatureShape(signature: unknown): {
|
|
81
|
+
ok: false;
|
|
82
|
+
reason: ModuleSignatureFailure;
|
|
83
|
+
error: ProtocolError;
|
|
84
|
+
} | null;
|
|
85
|
+
//# sourceMappingURL=module-signing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"module-signing.d.ts","sourceRoot":"","sources":["../src/module-signing.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAGpF,4EAA4E;AAC5E,eAAO,MAAM,uBAAuB,EAAG,eAAwB,CAAC;AAEhE;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,cAAc,GAAG,MAAM,CAGrE;AAID,MAAM,MAAM,sBAAsB;AAChC,kDAAkD;AAChD,SAAS;AACX,iEAAiE;GAC/D,oBAAoB;AACtB,uDAAuD;GACrD,WAAW;AACb,wDAAwD;GACtD,aAAa;AACf,kEAAkE;GAChE,SAAS,CAAC;AAEd,MAAM,MAAM,sBAAsB,GAC9B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GAC5C;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,sBAAsB,CAAC;IAAC,KAAK,EAAE,aAAa,CAAA;CAAE,CAAC;AAExE;;;;;GAKG;AACH,MAAM,WAAW,uBAAuB;IACtC,MAAM,CACJ,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,eAAe,GACzB,OAAO,CAAC;QAAE,EAAE,EAAE,IAAI,CAAA;KAAE,GAAG;QAAE,EAAE,EAAE,KAAK,CAAC;QAAC,MAAM,EAAE,aAAa,GAAG,eAAe,GAAG,WAAW,CAAA;KAAE,CAAC,CAAC;CACjG;AAED,MAAM,WAAW,4BAA4B;IAC3C,4FAA4F;IAC5F,QAAQ,CAAC,EAAE,uBAAuB,CAAC;IACnC;;;;;;;;OAQG;IACH,iBAAiB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACvC;AAsCD;;;;;;;;;GASG;AACH,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,cAAc,EACxB,OAAO,GAAE,4BAAiC,GACzC,OAAO,CAAC,sBAAsB,CAAC,CA+CjC;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,OAAO,GACjB;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,sBAAsB,CAAC;IAAC,KAAK,EAAE,aAAa,CAAA;CAAE,GAAG,IAAI,CAmC5E"}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// Module manifest signatures (protocol §X.1, RFC 0002).
|
|
2
|
+
//
|
|
3
|
+
// A module manifest is executable surface: it declares calculations whose
|
|
4
|
+
// formulas the calc engine evaluates and validations that can flip a deal from
|
|
5
|
+
// blocking to advisory. A host that loads one from npm, a URL, or a colleague's
|
|
6
|
+
// directory has, without this, no way to ask "is this the manifest the author
|
|
7
|
+
// published?"
|
|
8
|
+
//
|
|
9
|
+
// The layering is the same one block signatures use, for the same reason. Core
|
|
10
|
+
// owns the crypto-free half — the canonical bytes a signature covers, the
|
|
11
|
+
// structural checks, and the verdict taxonomy — while the algorithms live in
|
|
12
|
+
// `@uwmd/signing` and arrive through an injected verifier. Reading and loading a
|
|
13
|
+
// module must never require cryptography.
|
|
14
|
+
//
|
|
15
|
+
// Advisory by design: the protocol fixes what "signature valid" *means* so two
|
|
16
|
+
// hosts agree, and leaves what to *do* about an unsigned or invalid module to
|
|
17
|
+
// host policy (§X.1.4).
|
|
18
|
+
import { canonicalizeExact } from './integrity-canonical.js';
|
|
19
|
+
import { UW_SIGNATURE_ALGORITHMS } from './types.js';
|
|
20
|
+
/** The one scheme protocol 1.x implements. See `ModuleSignature.scheme`. */
|
|
21
|
+
export const MODULE_SIGNATURE_SCHEME = 'uwmd-keystore';
|
|
22
|
+
/**
|
|
23
|
+
* The exact bytes a module signature covers: RFC 8785 canonical JSON of the
|
|
24
|
+
* manifest with `signature` removed.
|
|
25
|
+
*
|
|
26
|
+
* Removed rather than nulled, so that signing a manifest and then verifying it
|
|
27
|
+
* produce byte-identical input — the failure mode where a signature verifies
|
|
28
|
+
* before it is attached and not after.
|
|
29
|
+
*/
|
|
30
|
+
export function moduleSigningPayload(manifest) {
|
|
31
|
+
const { signature: _omitted, ...rest } = manifest;
|
|
32
|
+
return canonicalizeExact(rest);
|
|
33
|
+
}
|
|
34
|
+
const CODES = {
|
|
35
|
+
missing: 'PROTO-MOD-068',
|
|
36
|
+
unsupported_scheme: 'PROTO-MOD-069',
|
|
37
|
+
malformed: 'PROTO-MOD-070',
|
|
38
|
+
unknown_key: 'PROTO-MOD-071',
|
|
39
|
+
invalid: 'PROTO-MOD-072',
|
|
40
|
+
};
|
|
41
|
+
const REMEDIATIONS = {
|
|
42
|
+
missing: 'Sign the manifest, or relax the host policy to accept unsigned modules (signatures are advisory at the protocol level).',
|
|
43
|
+
unsupported_scheme: `Re-sign under the '${MODULE_SIGNATURE_SCHEME}' scheme, or use a host that implements the named scheme.`,
|
|
44
|
+
malformed: 'Re-sign the manifest; the signature object is not a well-formed ModuleSignature.',
|
|
45
|
+
unknown_key: 'Add the signing key to the host key store under that kid, or re-sign with a key the host trusts.',
|
|
46
|
+
invalid: 'The manifest changed after it was signed. Restore the published manifest, or re-sign the version you intend to ship.',
|
|
47
|
+
};
|
|
48
|
+
function fail(reason, message) {
|
|
49
|
+
return {
|
|
50
|
+
ok: false,
|
|
51
|
+
reason,
|
|
52
|
+
error: {
|
|
53
|
+
category: 'module',
|
|
54
|
+
code: CODES[reason],
|
|
55
|
+
message,
|
|
56
|
+
pointer: 'signature',
|
|
57
|
+
remediation: REMEDIATIONS[reason],
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Verify a module manifest's signature.
|
|
63
|
+
*
|
|
64
|
+
* The three refusals a host must keep apart are `missing` (nothing was
|
|
65
|
+
* claimed), `unknown_key` (something was claimed and this host cannot check
|
|
66
|
+
* it), and `invalid` (something was claimed and it is false). They call for
|
|
67
|
+
* three different responses — decide a policy, load a key, reject the module —
|
|
68
|
+
* and a verifier that reports them as one verdict makes all three
|
|
69
|
+
* indistinguishable at the point where the host has to act.
|
|
70
|
+
*/
|
|
71
|
+
export async function verifyModuleSignature(manifest, options = {}) {
|
|
72
|
+
const signature = manifest.signature;
|
|
73
|
+
if (signature === undefined) {
|
|
74
|
+
return fail('missing', `Module '${manifest.id}' carries no signature.`);
|
|
75
|
+
}
|
|
76
|
+
const structural = checkSignatureShape(signature);
|
|
77
|
+
if (structural)
|
|
78
|
+
return structural;
|
|
79
|
+
if (!options.verifier) {
|
|
80
|
+
return fail('unknown_key', `Module '${manifest.id}' is signed by key '${signature.kid}', but this host has no signature backend to check it with.`);
|
|
81
|
+
}
|
|
82
|
+
const verdict = await options.verifier.verify(moduleSigningPayload(manifest), signature);
|
|
83
|
+
if (!verdict.ok) {
|
|
84
|
+
if (verdict.reason === 'unknown_kid') {
|
|
85
|
+
return fail('unknown_key', `Module '${manifest.id}' is signed by key '${signature.kid}', which this host's key store does not hold.`);
|
|
86
|
+
}
|
|
87
|
+
return fail('invalid', `Module '${manifest.id}' signature did not verify (${verdict.reason}).`);
|
|
88
|
+
}
|
|
89
|
+
if (options.allowedIdentities) {
|
|
90
|
+
const identity = signature.identity;
|
|
91
|
+
if (identity === undefined || !options.allowedIdentities.includes(identity)) {
|
|
92
|
+
return fail('invalid', `Module '${manifest.id}' is validly signed by '${signature.kid}' but its identity ${identity === undefined ? 'is absent' : `'${identity}' is not`} on this host's allow-list.`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
ok: true,
|
|
97
|
+
kid: signature.kid,
|
|
98
|
+
...(signature.identity !== undefined ? { identity: signature.identity } : {}),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Structural checks, run before any cryptography.
|
|
103
|
+
*
|
|
104
|
+
* Separated out because they are the part a host can run with no key store at
|
|
105
|
+
* all, and because a malformed signature deserves its own code: telling an
|
|
106
|
+
* author "this did not verify" when the real problem is a missing `signed_at`
|
|
107
|
+
* sends them looking for tampering that never happened.
|
|
108
|
+
*/
|
|
109
|
+
export function checkSignatureShape(signature) {
|
|
110
|
+
if (typeof signature !== 'object' || signature === null || Array.isArray(signature)) {
|
|
111
|
+
return fail('malformed', 'signature must be an object.');
|
|
112
|
+
}
|
|
113
|
+
const sig = signature;
|
|
114
|
+
if (sig['scheme'] !== MODULE_SIGNATURE_SCHEME) {
|
|
115
|
+
return fail('unsupported_scheme', `signature.scheme must be '${MODULE_SIGNATURE_SCHEME}' (got ${JSON.stringify(sig['scheme'])}).`);
|
|
116
|
+
}
|
|
117
|
+
if (typeof sig['alg'] !== 'string' ||
|
|
118
|
+
!UW_SIGNATURE_ALGORITHMS.includes(sig['alg'])) {
|
|
119
|
+
return fail('malformed', `signature.alg must be one of ${UW_SIGNATURE_ALGORITHMS.join(', ')}.`);
|
|
120
|
+
}
|
|
121
|
+
for (const key of ['kid', 'sig', 'signed_at']) {
|
|
122
|
+
if (typeof sig[key] !== 'string' || sig[key].length === 0) {
|
|
123
|
+
return fail('malformed', `signature.${key} must be a non-empty string.`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (sig['identity'] !== undefined && typeof sig['identity'] !== 'string') {
|
|
127
|
+
return fail('malformed', 'signature.identity, when present, must be a string.');
|
|
128
|
+
}
|
|
129
|
+
for (const key of Object.keys(sig)) {
|
|
130
|
+
if (!['scheme', 'alg', 'kid', 'sig', 'signed_at', 'identity'].includes(key)) {
|
|
131
|
+
return fail('malformed', `signature has unknown key '${key}'.`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=module-signing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"module-signing.js","sourceRoot":"","sources":["../src/module-signing.ts"],"names":[],"mappings":"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"}
|
package/dist/modules.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ModuleLoadResult, type ModuleManifest, type ProtocolError, type ViewerTier } from './protocol.js';
|
|
2
|
+
import { type ModuleSignatureVerifier } from './module-signing.js';
|
|
2
3
|
export interface ModuleRegistry {
|
|
3
4
|
modules: readonly ModuleManifest[];
|
|
4
5
|
byId: ReadonlyMap<string, ModuleManifest>;
|
|
@@ -11,14 +12,55 @@ export interface LoadModuleOptions {
|
|
|
11
12
|
formatVersion?: string;
|
|
12
13
|
alreadyLoaded?: readonly ModuleManifest[];
|
|
13
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* What a host does about module signatures (§X.1.4, RFC 0002).
|
|
17
|
+
*
|
|
18
|
+
* - `ignore` — do not look. The default, and what every host did before RFC
|
|
19
|
+
* 0002; a signature is carried through untouched.
|
|
20
|
+
* - `verify-if-present` — an unsigned module loads, but a *bad* signature
|
|
21
|
+
* refuses. The pragmatic setting during adoption: it never punishes an
|
|
22
|
+
* author who has not signed yet, and never lets a broken claim through.
|
|
23
|
+
* - `require` — an unsigned module refuses too.
|
|
24
|
+
*
|
|
25
|
+
* `verify-if-present` and `require` both refuse on `unknown_key`, and that is
|
|
26
|
+
* deliberate: a host that cannot check a signature has not established the
|
|
27
|
+
* module is safe, and treating "I have no key for this" as success would make
|
|
28
|
+
* the whole policy decorative.
|
|
29
|
+
*/
|
|
30
|
+
export type ModuleSignaturePolicy = 'ignore' | 'verify-if-present' | 'require';
|
|
31
|
+
export interface LoadModuleAsyncOptions extends LoadModuleOptions {
|
|
32
|
+
/** Defaults to `ignore`. */
|
|
33
|
+
signaturePolicy?: ModuleSignaturePolicy;
|
|
34
|
+
/** Supplied by `@uwmd/signing`. Absent under a checking policy means every signed module refuses. */
|
|
35
|
+
signatureVerifier?: ModuleSignatureVerifier;
|
|
36
|
+
/** Optional identity allow-list; see `VerifyModuleSignatureOptions`. */
|
|
37
|
+
allowedIdentities?: readonly string[];
|
|
38
|
+
}
|
|
14
39
|
export interface CreateModuleRegistryOptions extends LoadModuleOptions {
|
|
15
40
|
modules: readonly ModuleManifest[];
|
|
16
41
|
}
|
|
42
|
+
export interface CreateModuleRegistryAsyncOptions extends LoadModuleAsyncOptions {
|
|
43
|
+
modules: readonly ModuleManifest[];
|
|
44
|
+
}
|
|
17
45
|
export declare class ModuleRegistryError extends Error {
|
|
18
46
|
readonly errors: readonly ProtocolError[];
|
|
19
47
|
constructor(errors: readonly ProtocolError[]);
|
|
20
48
|
}
|
|
21
49
|
export declare function loadModuleManifest(candidate: unknown, opts?: LoadModuleOptions): ModuleLoadResult;
|
|
50
|
+
/**
|
|
51
|
+
* `loadModuleManifest` plus a signature-policy gate.
|
|
52
|
+
*
|
|
53
|
+
* Separate and async for the same reason `applyEditAsync` is: verification
|
|
54
|
+
* needs Web Crypto, which is async, and the synchronous loader is on a path
|
|
55
|
+
* plenty of hosts call where signatures are none of their business. A host that
|
|
56
|
+
* does not check signatures keeps the sync function and pays nothing.
|
|
57
|
+
*
|
|
58
|
+
* Structural validation runs first regardless of policy — a manifest that is
|
|
59
|
+
* not a valid manifest should say so, not report a signature problem.
|
|
60
|
+
*/
|
|
61
|
+
export declare function loadModuleManifestAsync(candidate: unknown, opts?: LoadModuleAsyncOptions): Promise<ModuleLoadResult>;
|
|
62
|
+
/** `createModuleRegistry` with the same signature-policy gate. */
|
|
63
|
+
export declare function createModuleRegistryAsync(opts: CreateModuleRegistryAsyncOptions): Promise<ModuleRegistry>;
|
|
22
64
|
export declare function createModuleRegistry(opts: CreateModuleRegistryOptions): ModuleRegistry;
|
|
23
65
|
export declare function getModuleCalculationsForAssetClass(registry: ModuleRegistry, asset_class: string): import("./protocol.js").ModuleCalcDecl[];
|
|
24
66
|
//# sourceMappingURL=modules.d.ts.map
|
package/dist/modules.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"modules.d.ts","sourceRoot":"","sources":["../src/modules.ts"],"names":[],"mappings":"AAUA,OAAO,EAGL,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,UAAU,EAChB,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"modules.d.ts","sourceRoot":"","sources":["../src/modules.ts"],"names":[],"mappings":"AAUA,OAAO,EAGL,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,UAAU,EAChB,MAAM,eAAe,CAAC;AAEvB,OAAO,EAGL,KAAK,uBAAuB,EAC7B,MAAM,qBAAqB,CAAC;AAiE7B,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,SAAS,cAAc,EAAE,CAAC;IACnC,IAAI,EAAE,WAAW,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IAC1C,YAAY,EAAE,WAAW,CAAC,MAAM,EAAE,SAAS,cAAc,EAAE,CAAC,CAAC;IAC7D,wBAAwB,CAAC,WAAW,EAAE,MAAM,GAAG,cAAc,EAAE,CAAC;CACjE;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,EAAE,UAAU,CAAC;IACtB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,SAAS,cAAc,EAAE,CAAC;CAC3C;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,qBAAqB,GAAG,QAAQ,GAAG,mBAAmB,GAAG,SAAS,CAAC;AAE/E,MAAM,WAAW,sBAAuB,SAAQ,iBAAiB;IAC/D,4BAA4B;IAC5B,eAAe,CAAC,EAAE,qBAAqB,CAAC;IACxC,qGAAqG;IACrG,iBAAiB,CAAC,EAAE,uBAAuB,CAAC;IAC5C,wEAAwE;IACxE,iBAAiB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACvC;AAED,MAAM,WAAW,2BAA4B,SAAQ,iBAAiB;IACpE,OAAO,EAAE,SAAS,cAAc,EAAE,CAAC;CACpC;AAED,MAAM,WAAW,gCAAiC,SAAQ,sBAAsB;IAC9E,OAAO,EAAE,SAAS,cAAc,EAAE,CAAC;CACpC;AAED,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,QAAQ,CAAC,MAAM,EAAE,SAAS,aAAa,EAAE,CAAC;gBAE9B,MAAM,EAAE,SAAS,aAAa,EAAE;CAK7C;AAED,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,OAAO,EAClB,IAAI,GAAE,iBAAsB,GAC3B,gBAAgB,CAMlB;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,uBAAuB,CAC3C,SAAS,EAAE,OAAO,EAClB,IAAI,GAAE,sBAA2B,GAChC,OAAO,CAAC,gBAAgB,CAAC,CAe3B;AAED,kEAAkE;AAClE,wBAAsB,yBAAyB,CAC7C,IAAI,EAAE,gCAAgC,GACrC,OAAO,CAAC,cAAc,CAAC,CAWzB;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,2BAA2B,GAAG,cAAc,CAoDtF;AAED,wBAAgB,kCAAkC,CAChD,QAAQ,EAAE,cAAc,EACxB,WAAW,EAAE,MAAM,4CAKpB"}
|
package/dist/modules.js
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
import { parseExpression } from './calc/parser.js';
|
|
9
9
|
import { MAX_ROUND_TO } from './calc/quantize.js';
|
|
10
10
|
import { FORMAT_VERSION, PROTOCOL_VERSION, } from './protocol.js';
|
|
11
|
+
import { parseAssetClass } from './asset-class.js';
|
|
12
|
+
import { checkSignatureShape, verifyModuleSignature, } from './module-signing.js';
|
|
11
13
|
import { ASSET_CLASSES } from './types.js';
|
|
12
14
|
const TIERS = [
|
|
13
15
|
'tier-1-reader',
|
|
@@ -43,7 +45,8 @@ const MANIFEST_KEYS = [
|
|
|
43
45
|
'manifest_version', 'id', 'name', 'version', 'description', 'authors', 'license',
|
|
44
46
|
'requires_protocol', 'requires_format', 'requires_tier', 'asset_classes',
|
|
45
47
|
'deal_stages', 'sections', 'calculations', 'validations', 'thresholds',
|
|
46
|
-
'view_models', 'ui', 'agent_layers', 'depends_on',
|
|
48
|
+
'view_models', 'ui', 'agent_layers', 'depends_on', 'signature',
|
|
49
|
+
'declares_asset_classes',
|
|
47
50
|
];
|
|
48
51
|
const SECTION_KEYS = ['id', 'display_name', 'schema', 'required'];
|
|
49
52
|
const CALC_KEYS = ['id', 'label', 'formula', 'unit', 'round_to', 'deterministic'];
|
|
@@ -78,6 +81,48 @@ export function loadModuleManifest(candidate, opts = {}) {
|
|
|
78
81
|
? { ok: false, errors }
|
|
79
82
|
: { ok: true, manifest: freezeManifest(manifest), errors: [] };
|
|
80
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* `loadModuleManifest` plus a signature-policy gate.
|
|
86
|
+
*
|
|
87
|
+
* Separate and async for the same reason `applyEditAsync` is: verification
|
|
88
|
+
* needs Web Crypto, which is async, and the synchronous loader is on a path
|
|
89
|
+
* plenty of hosts call where signatures are none of their business. A host that
|
|
90
|
+
* does not check signatures keeps the sync function and pays nothing.
|
|
91
|
+
*
|
|
92
|
+
* Structural validation runs first regardless of policy — a manifest that is
|
|
93
|
+
* not a valid manifest should say so, not report a signature problem.
|
|
94
|
+
*/
|
|
95
|
+
export async function loadModuleManifestAsync(candidate, opts = {}) {
|
|
96
|
+
const base = loadModuleManifest(candidate, opts);
|
|
97
|
+
if (!base.ok || !base.manifest)
|
|
98
|
+
return base;
|
|
99
|
+
const policy = opts.signaturePolicy ?? 'ignore';
|
|
100
|
+
if (policy === 'ignore')
|
|
101
|
+
return base;
|
|
102
|
+
const manifest = base.manifest;
|
|
103
|
+
if (manifest.signature === undefined && policy === 'verify-if-present')
|
|
104
|
+
return base;
|
|
105
|
+
const verdict = await verifyModuleSignature(manifest, {
|
|
106
|
+
...(opts.signatureVerifier ? { verifier: opts.signatureVerifier } : {}),
|
|
107
|
+
...(opts.allowedIdentities ? { allowedIdentities: opts.allowedIdentities } : {}),
|
|
108
|
+
});
|
|
109
|
+
return verdict.ok ? base : { ok: false, errors: [verdict.error] };
|
|
110
|
+
}
|
|
111
|
+
/** `createModuleRegistry` with the same signature-policy gate. */
|
|
112
|
+
export async function createModuleRegistryAsync(opts) {
|
|
113
|
+
const errors = [];
|
|
114
|
+
for (const candidate of opts.modules) {
|
|
115
|
+
const result = await loadModuleManifestAsync(candidate, opts);
|
|
116
|
+
if (!result.ok)
|
|
117
|
+
errors.push(...result.errors);
|
|
118
|
+
}
|
|
119
|
+
// Refuse before building anything. A registry assembled from a set that
|
|
120
|
+
// included a module the policy rejects would be a registry the host was told
|
|
121
|
+
// not to have, whatever it did with the error list afterwards.
|
|
122
|
+
if (errors.length > 0)
|
|
123
|
+
throw new ModuleRegistryError(errors);
|
|
124
|
+
return createModuleRegistry(opts);
|
|
125
|
+
}
|
|
81
126
|
export function createModuleRegistry(opts) {
|
|
82
127
|
const loaded = [];
|
|
83
128
|
const errors = [];
|
|
@@ -152,6 +197,15 @@ function validateModuleManifest(manifest, opts) {
|
|
|
152
197
|
if (!Array.isArray(manifest.authors) || manifest.authors.length === 0 || !manifest.authors.every((a) => typeof a === 'string' && a.length > 0)) {
|
|
153
198
|
errors.push(moduleError('PROTO-MOD-005', 'Module authors must be a non-empty string array.', 'authors'));
|
|
154
199
|
}
|
|
200
|
+
// Shape-check `signature` regardless of policy. A host that ignores
|
|
201
|
+
// signatures still must not accept a manifest carrying a malformed one:
|
|
202
|
+
// "ignore" is a decision not to *verify*, not a licence to admit nonsense
|
|
203
|
+
// into a frozen manifest other code will read.
|
|
204
|
+
if (manifest.signature !== undefined) {
|
|
205
|
+
const shape = checkSignatureShape(manifest.signature);
|
|
206
|
+
if (shape)
|
|
207
|
+
errors.push(shape.error);
|
|
208
|
+
}
|
|
155
209
|
if (!TIERS.includes(manifest.requires_tier)) {
|
|
156
210
|
errors.push(moduleError('PROTO-MOD-006', 'Module requires_tier is not a valid viewer tier.', 'requires_tier'));
|
|
157
211
|
}
|
|
@@ -174,6 +228,7 @@ function validateModuleManifest(manifest, opts) {
|
|
|
174
228
|
}
|
|
175
229
|
}
|
|
176
230
|
}
|
|
231
|
+
validateDeclaredAssetClasses(errors, manifest);
|
|
177
232
|
if (manifest.deal_stages !== undefined) {
|
|
178
233
|
if (!Array.isArray(manifest.deal_stages)) {
|
|
179
234
|
errors.push(moduleError('PROTO-MOD-010', 'deal_stages must be an array.', 'deal_stages'));
|
|
@@ -564,6 +619,71 @@ function boundLength(errors, value, key, min, max) {
|
|
|
564
619
|
errors.push(moduleError('PROTO-MOD-065', `${key} must be between ${min} and ${max} characters (got ${value.length}).`, key));
|
|
565
620
|
}
|
|
566
621
|
}
|
|
622
|
+
const ASSET_CLASS_DECL_KEYS = [
|
|
623
|
+
'id', 'display_name', 'fallback', 'required_sections', 'optional_sections',
|
|
624
|
+
];
|
|
625
|
+
/**
|
|
626
|
+
* Validate `declares_asset_classes` (§X.2, RFC 0003).
|
|
627
|
+
*
|
|
628
|
+
* The identifier grammar is enforced here rather than only at document-parse
|
|
629
|
+
* time, so a module that would introduce an unusable class is refused at load
|
|
630
|
+
* — before any file references it and produces a confusing failure two layers
|
|
631
|
+
* away from the cause.
|
|
632
|
+
*/
|
|
633
|
+
function validateDeclaredAssetClasses(errors, manifest) {
|
|
634
|
+
const declarations = manifest.declares_asset_classes;
|
|
635
|
+
if (declarations === undefined)
|
|
636
|
+
return;
|
|
637
|
+
if (!Array.isArray(declarations)) {
|
|
638
|
+
errors.push(moduleError('PROTO-MOD-073', 'declares_asset_classes must be an array.', 'declares_asset_classes'));
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
const seen = new Set();
|
|
642
|
+
for (const [idx, raw] of declarations.entries()) {
|
|
643
|
+
const pointer = `declares_asset_classes[${idx}]`;
|
|
644
|
+
if (!isRecord(raw)) {
|
|
645
|
+
errors.push(moduleError('PROTO-MOD-073', 'Each declaration must be an object.', pointer));
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
rejectUnknownKeys(errors, raw, ASSET_CLASS_DECL_KEYS, pointer);
|
|
649
|
+
const id = raw['id'];
|
|
650
|
+
if (typeof id !== 'string') {
|
|
651
|
+
errors.push(moduleError('PROTO-MOD-074', 'declaration id must be a string.', `${pointer}.id`));
|
|
652
|
+
}
|
|
653
|
+
else {
|
|
654
|
+
const identity = parseAssetClass(id);
|
|
655
|
+
if (!identity.ok) {
|
|
656
|
+
errors.push(moduleError('PROTO-MOD-074', identity.error.message, `${pointer}.id`));
|
|
657
|
+
}
|
|
658
|
+
else if (identity.kind === 'builtin') {
|
|
659
|
+
// Declaring a builtin is not an extension, it is a redefinition. The
|
|
660
|
+
// `asset_classes` field is how a module *enhances* one.
|
|
661
|
+
errors.push(moduleError('PROTO-MOD-074', `'${id}' is a builtin asset class; use asset_classes to enhance it rather than declares_asset_classes to redefine it.`, `${pointer}.id`));
|
|
662
|
+
}
|
|
663
|
+
else if (seen.has(id)) {
|
|
664
|
+
errors.push(moduleError('PROTO-MOD-075', `Asset class '${id}' is declared more than once by this module.`, `${pointer}.id`));
|
|
665
|
+
}
|
|
666
|
+
else {
|
|
667
|
+
seen.add(id);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
if (typeof raw['display_name'] !== 'string' || raw['display_name'].length === 0) {
|
|
671
|
+
errors.push(moduleError('PROTO-MOD-076', 'declaration display_name must be a non-empty string.', `${pointer}.display_name`));
|
|
672
|
+
}
|
|
673
|
+
const fallback = raw['fallback'];
|
|
674
|
+
if (fallback !== undefined && !ASSET_CLASSES.includes(fallback)) {
|
|
675
|
+
errors.push(moduleError('PROTO-MOD-077', `declaration fallback must be a builtin asset class (got ${JSON.stringify(fallback)}).`, `${pointer}.fallback`));
|
|
676
|
+
}
|
|
677
|
+
for (const key of ['required_sections', 'optional_sections']) {
|
|
678
|
+
const value = raw[key];
|
|
679
|
+
if (value === undefined)
|
|
680
|
+
continue;
|
|
681
|
+
if (!Array.isArray(value) || !value.every((v) => typeof v === 'string' && v.length > 0)) {
|
|
682
|
+
errors.push(moduleError('PROTO-MOD-078', `${key} must be an array of non-empty section ids.`, `${pointer}.${key}`));
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
567
687
|
function freezeManifest(manifest) {
|
|
568
688
|
return Object.freeze({
|
|
569
689
|
...manifest,
|
|
@@ -576,6 +696,9 @@ function freezeManifest(manifest) {
|
|
|
576
696
|
view_models: manifest.view_models ? Object.freeze([...manifest.view_models]) : undefined,
|
|
577
697
|
agent_layers: manifest.agent_layers ? Object.freeze([...manifest.agent_layers]) : undefined,
|
|
578
698
|
depends_on: manifest.depends_on ? Object.freeze([...manifest.depends_on]) : undefined,
|
|
699
|
+
declares_asset_classes: manifest.declares_asset_classes
|
|
700
|
+
? Object.freeze([...manifest.declares_asset_classes])
|
|
701
|
+
: undefined,
|
|
579
702
|
});
|
|
580
703
|
}
|
|
581
704
|
function isRecord(value) {
|