@githolon/dsl 0.113.4 → 0.114.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/assets/operation-contract.wasm +0 -0
- package/compile_package.mjs +23 -3
- package/compile_proof.d.mts +7 -0
- package/compile_proof.mjs +38 -3
- package/dart/CHANGELOG.md +20 -0
- package/dart/lib/src/application_framework_calls.dart +9 -5
- package/dart/lib/src/generated_call.dart +14 -3
- package/dart/lib/src/nms_generated_runtime.dart +7 -0
- package/dart/lib/src/node_runner_transport.dart +77 -8
- package/dart/lib/src/nomos_trace.dart +107 -0
- package/dart/lib/src/reception/dart_reception.dart +148 -11
- package/dart/lib/src/rows_watch.dart +390 -0
- package/dart/lib/src/runner/node-entry.mjs +2803 -690
- package/dart/lib/src/sync_status_projection.dart +94 -1
- package/dart/lib/src/sync_status_reducer.dart +5 -2
- package/dart/lib/src/test_harness.dart +107 -41
- package/dart/lib/src/testing/test_peer.dart +107 -99
- package/dart/lib/src/testing/test_peer_host.dart +305 -0
- package/dart/lib/src/typed_watch_registry.dart +26 -3
- package/dart/pubspec.lock +1 -1
- package/dart/pubspec.yaml +2 -2
- package/dart/test/background_model_adoption_test.dart +1 -0
- package/dart/test/custodian_warming_test.dart +105 -0
- package/dart/test/fixtures/offer_watch_order_peer.dart +74 -0
- package/dart/test/kernel_author_after_read_test.dart +6 -3
- package/dart/test/kernel_fence_test.dart +4 -1
- package/dart/test/model_adoption_test.dart +17 -0
- package/dart/test/nomos_trace_test.dart +91 -0
- package/dart/test/own_offer_watch_order_test.dart +90 -0
- package/dart/test/receiving_group_open_test.dart +4 -8
- package/dart/test/replica_readiness_test.dart +14 -4
- package/dart/test/rows_watch_test.dart +331 -0
- package/dart/test/test_peer_host_selection_test.dart +49 -0
- package/dart_types/CHANGELOG.md +15 -0
- package/dart_types/lib/src/approved_model.dart +22 -5
- package/dart_types/lib/src/proto/nomos/kernel/v1/kernel.pb.dart +535 -1
- package/dart_types/lib/src/proto/nomos/kernel/v1/kernel.pbenum.dart +65 -0
- package/dart_types/lib/src/proto/nomos/runtime/v2/application_desk.pb.dart +14 -0
- package/dart_types/lib/src/proto/nomos/runtime/v2/correspondence_desk.pb.dart +359 -2
- package/dart_types/lib/src/proto/nomos/runtime/v2/correspondence_desk.pbenum.dart +65 -0
- package/dart_types/lib/src/proto/nomos/runtime/v2/model_adoption_desk.pb.dart +20 -2
- package/dart_types/lib/src/proto/nomos/runtime/v2/observability_desk.pb.dart +278 -9
- package/dart_types/lib/src/proto/nomos/runtime/v2/receiving_group_desk.pb.dart +7 -85
- package/dart_types/lib/src/proto/nomos/runtime/v2/residency_desk.pb.dart +35 -0
- package/dart_types/pubspec.yaml +1 -1
- package/package.json +4 -2
- package/src/build_provenance.ts +7 -2
- package/src/codegen_dart_client.ts +21 -9
- package/src/codegen_dart_facade.ts +1 -0
- package/src/codegen_dart_framework_calls.ts +12 -9
- package/src/codegen_dart_scenario.ts +7 -0
- package/src/codegen_ts.ts +26 -24
- package/src/compile_package_main.ts +65 -61
- package/src/engine_entry.ts +4 -48
- package/src/law_unit_set_identity.ts +44 -0
- package/src/query/compile.ts +0 -0
- package/src/release_inspection.ts +24 -0
- package/src/release_publication.ts +298 -0
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
// A published domain release, as bytes: its identity and the verification every publisher and the console
|
|
2
|
+
// apply to the uploaded units (architecture/domain_release_lifecycle.md, "Publish candidate").
|
|
3
|
+
//
|
|
4
|
+
// A release is a complete law unit set (architecture/law_units_format.md): framework, composition and the
|
|
5
|
+
// domain units the compiler emitted, each named by its unit hash. Publishing it verifies every byte against
|
|
6
|
+
// the unit hashes the kernel also derives, and names the release by an identity over its series, its
|
|
7
|
+
// predecessor and its exact (key, unit hash) set. Publication installs nothing: each target's kernel
|
|
8
|
+
// admits the same units again, from their content, when it is asked to stage them.
|
|
9
|
+
//
|
|
10
|
+
// Node-free (WebCrypto only), so the console Worker, the CLI and CI verify with this one module. The unit
|
|
11
|
+
// hash and canonical manifest follow `law_units.ts` (`lawUnitHash`, `unitManifestUsda`) and the kernel's
|
|
12
|
+
// `DomainLawUnit`; `release_publication.test.ts` proves agreement on compiled units.
|
|
13
|
+
|
|
14
|
+
import { lawUnitSetIdentityPreimage } from "./law_unit_set_identity.ts";
|
|
15
|
+
|
|
16
|
+
export const RELEASE_IDENTITY_TAG = "nomos-domain-release-v1";
|
|
17
|
+
const LAW_UNIT_HASH_TAG = "nomos-law-unit-v1";
|
|
18
|
+
const LAW_UNIT_FORMAT = "nomos.law-unit.v1";
|
|
19
|
+
export const LAW_UNIT_MANIFEST = "unit.usda";
|
|
20
|
+
|
|
21
|
+
/** The first release of a series names this as its predecessor. Explicit, never an empty default. */
|
|
22
|
+
export const NO_PREDECESSOR = "none";
|
|
23
|
+
|
|
24
|
+
const HEX64 = /^[0-9a-f]{64}$/;
|
|
25
|
+
const SERIES = /^[A-Za-z_][A-Za-z0-9_.-]{0,127}$/;
|
|
26
|
+
const UNIT_KEY = /^(framework|composition|domain:[A-Za-z_][A-Za-z0-9_.-]{0,127})$/;
|
|
27
|
+
/** Largest single file a release may carry (one upload request; the console's content store holds it in parts). */
|
|
28
|
+
export const MAX_RELEASE_FILE_BYTES = 32_000_000;
|
|
29
|
+
export const MAX_RELEASE_FILES = 4096;
|
|
30
|
+
|
|
31
|
+
export interface ReleaseFile {
|
|
32
|
+
/** The file's logical path inside its unit (`unit.usda` is the manifest). */
|
|
33
|
+
readonly path: string;
|
|
34
|
+
readonly sha256: string;
|
|
35
|
+
readonly size: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ReleaseUnit {
|
|
39
|
+
readonly key: string;
|
|
40
|
+
readonly unitHash: string;
|
|
41
|
+
/** Every file of the unit, the manifest included, sorted by path. */
|
|
42
|
+
readonly files: readonly ReleaseFile[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ReleaseCandidate {
|
|
46
|
+
/** The published domain identity this release continues: one of its domain keys. */
|
|
47
|
+
readonly series: string;
|
|
48
|
+
/** The release identity this one follows, or {@link NO_PREDECESSOR}. */
|
|
49
|
+
readonly predecessor: string;
|
|
50
|
+
readonly units: readonly ReleaseUnit[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** A refusal with a stable code; a client never parses the message. */
|
|
54
|
+
export class ReleaseVerificationError extends Error {
|
|
55
|
+
readonly code: string;
|
|
56
|
+
constructor(code: string, message: string) {
|
|
57
|
+
super(message);
|
|
58
|
+
this.code = code;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const refuse = (code: string, message: string): never => {
|
|
63
|
+
throw new ReleaseVerificationError(code, message);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const utf8 = (text: string): Uint8Array => new TextEncoder().encode(text);
|
|
67
|
+
const byteOrder = (left: string, right: string): number => (left < right ? -1 : left > right ? 1 : 0);
|
|
68
|
+
|
|
69
|
+
const hex = (bytes: ArrayBuffer): string =>
|
|
70
|
+
[...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
71
|
+
|
|
72
|
+
export async function sha256Hex(input: Uint8Array | string): Promise<string> {
|
|
73
|
+
const bytes = typeof input === "string" ? utf8(input) : input;
|
|
74
|
+
return hex(await crypto.subtle.digest("SHA-256", bytes as Uint8Array<ArrayBuffer>));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** One concatenation of tagged, NUL-separated fields, hashed once. */
|
|
78
|
+
async function taggedHash(parts: readonly string[]): Promise<string> {
|
|
79
|
+
return sha256Hex(parts.join(""));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The release identity: sha256("nomos-domain-release-v1\0" series "\0" predecessor "\0"
|
|
84
|
+
* { key "\0" unitHash "\0" }*), units sorted by key in byte order. Two clients that publish the same
|
|
85
|
+
* set over the same predecessor publish the same release; nothing else (who, when, evidence) moves it.
|
|
86
|
+
*/
|
|
87
|
+
export async function releaseIdentity(candidate: {
|
|
88
|
+
readonly series: string;
|
|
89
|
+
readonly predecessor: string;
|
|
90
|
+
readonly units: readonly { readonly key: string; readonly unitHash: string }[];
|
|
91
|
+
}): Promise<string> {
|
|
92
|
+
const parts = [`${RELEASE_IDENTITY_TAG}\0`, candidate.series, "\0", candidate.predecessor, "\0"];
|
|
93
|
+
for (const unit of [...candidate.units].sort((l, r) => byteOrder(l.key, r.key))) {
|
|
94
|
+
parts.push(unit.key, "\0", unit.unitHash, "\0");
|
|
95
|
+
}
|
|
96
|
+
return taggedHash(parts);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* THE LAW IDENTITY of a unit set (law_unit_set_identity.ts; the kernel's `law_unit_set_identity`): its sorted
|
|
101
|
+
* (key, unit hash) pairs and nothing else. A release names it through its units; every target realises it as a
|
|
102
|
+
* closure that also depends on the target kernel's composer, which is why a receipt records both.
|
|
103
|
+
*/
|
|
104
|
+
export async function lawUnitSetIdentity(units: readonly { readonly key: string; readonly unitHash: string }[]): Promise<string> {
|
|
105
|
+
return sha256Hex(lawUnitSetIdentityPreimage(units));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** The unit hash (law_units.ts `lawUnitHash`, the kernel's `DomainLawUnit`), over WebCrypto. */
|
|
109
|
+
export async function lawUnitHashOf(
|
|
110
|
+
key: string,
|
|
111
|
+
inventory: readonly { readonly logicalPath: string; readonly sha256: string }[],
|
|
112
|
+
routes: readonly { readonly selector: string; readonly planSha256: string }[],
|
|
113
|
+
): Promise<string> {
|
|
114
|
+
const parts = [`${LAW_UNIT_HASH_TAG}\0`, key, "\0", "assets\0"];
|
|
115
|
+
for (const asset of [...inventory].sort((l, r) => byteOrder(l.logicalPath, r.logicalPath))) {
|
|
116
|
+
parts.push(asset.logicalPath, "\0", asset.sha256, "\0");
|
|
117
|
+
}
|
|
118
|
+
parts.push("routes\0");
|
|
119
|
+
for (const route of [...routes].sort((l, r) => byteOrder(l.selector, r.selector))) {
|
|
120
|
+
parts.push(route.selector, "\0", route.planSha256, "\0");
|
|
121
|
+
}
|
|
122
|
+
return taggedHash(parts);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
interface ParsedManifest {
|
|
126
|
+
readonly key: string;
|
|
127
|
+
readonly kind: "domain" | "framework" | "composition";
|
|
128
|
+
readonly domain?: string;
|
|
129
|
+
readonly hash: string;
|
|
130
|
+
readonly assets: readonly { readonly logicalPath: string; readonly sha256: string }[];
|
|
131
|
+
readonly routes: readonly { readonly selector: string; readonly planSha256: string }[];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** The compiler's canonical manifest (law_units.ts `unitManifestUsda`), over WebCrypto. */
|
|
135
|
+
async function canonicalManifest(unit: ParsedManifest): Promise<string> {
|
|
136
|
+
const assetPrims = await Promise.all(unit.assets.map(async (asset) => [
|
|
137
|
+
` def NomosLawAsset "a_${await sha256Hex(asset.logicalPath)}"`,
|
|
138
|
+
" {",
|
|
139
|
+
` custom string nomos:logicalPath = ${JSON.stringify(asset.logicalPath)}`,
|
|
140
|
+
` custom string nomos:sha256 = "${asset.sha256}"`,
|
|
141
|
+
" }",
|
|
142
|
+
]));
|
|
143
|
+
const routePrims = await Promise.all(unit.routes.map(async (route) => [
|
|
144
|
+
` def NomosLawRoute "s_${await sha256Hex(route.selector)}"`,
|
|
145
|
+
" {",
|
|
146
|
+
` custom string nomos:selector = ${JSON.stringify(route.selector)}`,
|
|
147
|
+
` custom string nomos:plan = "${route.planSha256}"`,
|
|
148
|
+
" }",
|
|
149
|
+
]));
|
|
150
|
+
return [
|
|
151
|
+
"#usda 1.0",
|
|
152
|
+
"(",
|
|
153
|
+
' defaultPrim = "__NomosLawUnit_7e9c"',
|
|
154
|
+
" customLayerData = {",
|
|
155
|
+
` string "nomos:format" = "${LAW_UNIT_FORMAT}"`,
|
|
156
|
+
" }",
|
|
157
|
+
")",
|
|
158
|
+
"",
|
|
159
|
+
'def NomosLawUnit "__NomosLawUnit_7e9c"',
|
|
160
|
+
"{",
|
|
161
|
+
` custom string nomos:unit = ${JSON.stringify(unit.key)}`,
|
|
162
|
+
` custom token nomos:unitKind = "${unit.kind}"`,
|
|
163
|
+
...(unit.domain === undefined ? [] : [` custom string nomos:domain = ${JSON.stringify(unit.domain)}`]),
|
|
164
|
+
` custom string nomos:unitHash = "${unit.hash}"`,
|
|
165
|
+
"",
|
|
166
|
+
' def Scope "Assets"',
|
|
167
|
+
" {",
|
|
168
|
+
...assetPrims.flat(),
|
|
169
|
+
" }",
|
|
170
|
+
"",
|
|
171
|
+
' def Scope "Routes"',
|
|
172
|
+
" {",
|
|
173
|
+
...routePrims.flat(),
|
|
174
|
+
" }",
|
|
175
|
+
"}",
|
|
176
|
+
"",
|
|
177
|
+
].join("\n");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const LITERAL = '("(?:[^"\\\\]|\\\\.)*")';
|
|
181
|
+
function attribute(source: string, name: string, where: string): string {
|
|
182
|
+
const values = [...source.matchAll(new RegExp(`nomos:${name} = ${LITERAL}`, "g"))];
|
|
183
|
+
if (values.length !== 1) refuse("manifest-invalid", `${where}: expected one nomos:${name}`);
|
|
184
|
+
return JSON.parse(values[0]![1]!) as string;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Read a unit manifest in the compiler's canonical dialect only (release_inspection.ts
|
|
189
|
+
* `readInspectionUnits`): the attributes it names, then a byte-for-byte round trip through the canonical
|
|
190
|
+
* emitter. Any other spelling is refused rather than interpreted.
|
|
191
|
+
*/
|
|
192
|
+
export async function parseUnitManifest(source: string, where: string): Promise<ParsedManifest> {
|
|
193
|
+
const key = attribute(source, "unit", where);
|
|
194
|
+
const kind = attribute(source, "unitKind", where) as ParsedManifest["kind"];
|
|
195
|
+
if (!["domain", "framework", "composition"].includes(kind)) refuse("manifest-invalid", `${where}: unknown unit kind '${kind}'`);
|
|
196
|
+
const hash = attribute(source, "unitHash", where);
|
|
197
|
+
const domain = kind === "domain" ? attribute(source, "domain", where) : undefined;
|
|
198
|
+
if (key !== (domain === undefined ? kind : `domain:${domain}`)) refuse("manifest-invalid", `${where}: unit key and kind disagree`);
|
|
199
|
+
const assets = [...source.matchAll(/def NomosLawAsset "[^"]+"\s*\{([^}]+)\}/g)].map((match) => ({
|
|
200
|
+
logicalPath: attribute(match[1]!, "logicalPath", where), sha256: attribute(match[1]!, "sha256", where),
|
|
201
|
+
}));
|
|
202
|
+
const routes = [...source.matchAll(/def NomosLawRoute "[^"]+"\s*\{([^}]+)\}/g)].map((match) => ({
|
|
203
|
+
selector: attribute(match[1]!, "selector", where), planSha256: attribute(match[1]!, "plan", where),
|
|
204
|
+
}));
|
|
205
|
+
const parsed: ParsedManifest = { key, kind, ...(domain === undefined ? {} : { domain }), hash, assets, routes };
|
|
206
|
+
if (await canonicalManifest(parsed) !== source) refuse("manifest-noncanonical", `${where}: the manifest is not in the compiler's canonical form`);
|
|
207
|
+
return parsed;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const safePath = (path: string): boolean =>
|
|
211
|
+
path.length > 0 && path.length <= 512 && !path.startsWith("/") && !path.includes("\\")
|
|
212
|
+
&& path.split("/").every((part) => part !== "" && part !== "." && part !== "..");
|
|
213
|
+
|
|
214
|
+
/** Shape checks that need no bytes: sorted, unique, complete, well-formed. */
|
|
215
|
+
export function checkReleaseShape(candidate: ReleaseCandidate): void {
|
|
216
|
+
if (!SERIES.test(candidate.series)) refuse("release-invalid", "series must be a domain key");
|
|
217
|
+
if (candidate.predecessor !== NO_PREDECESSOR && !HEX64.test(candidate.predecessor)) {
|
|
218
|
+
refuse("release-invalid", `predecessor must be a release identity or '${NO_PREDECESSOR}'`);
|
|
219
|
+
}
|
|
220
|
+
const keys = candidate.units.map((unit) => unit.key);
|
|
221
|
+
if (!keys.every((key) => UNIT_KEY.test(key))) refuse("release-invalid", "every unit key is framework, composition or domain:<key>");
|
|
222
|
+
if (!keys.every((key, index) => index === 0 || byteOrder(keys[index - 1]!, key) < 0)) {
|
|
223
|
+
refuse("release-invalid", "units must be sorted by key, without repeats");
|
|
224
|
+
}
|
|
225
|
+
for (const required of ["framework", "composition", `domain:${candidate.series}`]) {
|
|
226
|
+
if (!keys.includes(required)) {
|
|
227
|
+
refuse("release-incomplete", `a release is a complete unit set; it lacks '${required}' (publish the compiler's whole build/<name>.units)`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
let files = 0;
|
|
231
|
+
for (const unit of candidate.units) {
|
|
232
|
+
if (!HEX64.test(unit.unitHash)) refuse("release-invalid", `${unit.key}: unitHash must be sha256 hex`);
|
|
233
|
+
const paths = unit.files.map((file) => file.path);
|
|
234
|
+
if (!paths.every((path, index) => index === 0 || byteOrder(paths[index - 1]!, path) < 0)) {
|
|
235
|
+
refuse("release-invalid", `${unit.key}: files must be sorted by path, without repeats`);
|
|
236
|
+
}
|
|
237
|
+
if (!paths.includes(LAW_UNIT_MANIFEST)) refuse("release-invalid", `${unit.key}: the unit carries no ${LAW_UNIT_MANIFEST}`);
|
|
238
|
+
for (const file of unit.files) {
|
|
239
|
+
if (!safePath(file.path)) refuse("release-invalid", `${unit.key}: unsafe file path '${file.path}'`);
|
|
240
|
+
if (!HEX64.test(file.sha256)) refuse("release-invalid", `${unit.key}/${file.path}: sha256 must be hex`);
|
|
241
|
+
if (!Number.isSafeInteger(file.size) || file.size < 0 || file.size > MAX_RELEASE_FILE_BYTES) {
|
|
242
|
+
refuse("release-invalid", `${unit.key}/${file.path}: size must be 0..${MAX_RELEASE_FILE_BYTES} bytes`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
files += unit.files.length;
|
|
246
|
+
}
|
|
247
|
+
if (files > MAX_RELEASE_FILES) refuse("release-invalid", `a release carries at most ${MAX_RELEASE_FILES} files`);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export interface VerifiedRelease {
|
|
251
|
+
readonly identity: string;
|
|
252
|
+
/** The law identity of its units ({@link lawUnitSetIdentity}). */
|
|
253
|
+
readonly unitSet: string;
|
|
254
|
+
readonly candidate: ReleaseCandidate;
|
|
255
|
+
/** The domain keys of its domain units, sorted. */
|
|
256
|
+
readonly serves: readonly string[];
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Verify a release candidate against its bytes: every file is present and is exactly the bytes its digest
|
|
261
|
+
* names, every manifest is canonical and lists exactly the unit's files, and every unit hash re-derives.
|
|
262
|
+
* `read` answers a digest's bytes from a content store, or undefined when the store lacks it.
|
|
263
|
+
*/
|
|
264
|
+
export async function verifyRelease(
|
|
265
|
+
candidate: ReleaseCandidate,
|
|
266
|
+
read: (sha256: string) => Promise<Uint8Array | undefined>,
|
|
267
|
+
): Promise<VerifiedRelease> {
|
|
268
|
+
checkReleaseShape(candidate);
|
|
269
|
+
for (const unit of candidate.units) {
|
|
270
|
+
const where = unit.key;
|
|
271
|
+
const bytesOf = new Map<string, Uint8Array>();
|
|
272
|
+
for (const file of unit.files) {
|
|
273
|
+
const bytes = await read(file.sha256);
|
|
274
|
+
if (!bytes) refuse("artifact-missing", `${where}/${file.path}: ${file.sha256} has not been uploaded`);
|
|
275
|
+
if (await sha256Hex(bytes!) !== file.sha256) refuse("artifact-digest-mismatch", `${where}/${file.path}: the stored bytes are not ${file.sha256}`);
|
|
276
|
+
if (bytes!.byteLength !== file.size) refuse("artifact-size-mismatch", `${where}/${file.path}: ${bytes!.byteLength} bytes, not ${file.size}`);
|
|
277
|
+
bytesOf.set(file.path, bytes!);
|
|
278
|
+
}
|
|
279
|
+
const manifest = await parseUnitManifest(new TextDecoder().decode(bytesOf.get(LAW_UNIT_MANIFEST)!), where);
|
|
280
|
+
if (manifest.key !== unit.key) refuse("unit-key-mismatch", `${where}: the manifest names unit '${manifest.key}'`);
|
|
281
|
+
const listed = [...manifest.assets.map((asset) => asset.logicalPath), LAW_UNIT_MANIFEST].sort(byteOrder);
|
|
282
|
+
const carried = unit.files.map((file) => file.path);
|
|
283
|
+
if (listed.length !== carried.length || listed.some((path, index) => path !== carried[index])) {
|
|
284
|
+
refuse("unit-inventory-mismatch", `${where}: the files carried are not exactly the files its manifest lists`);
|
|
285
|
+
}
|
|
286
|
+
for (const asset of manifest.assets) {
|
|
287
|
+
if (unit.files.find((file) => file.path === asset.logicalPath)!.sha256 !== asset.sha256) {
|
|
288
|
+
refuse("artifact-digest-mismatch", `${where}/${asset.logicalPath}: the manifest names another digest`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
const derived = await lawUnitHashOf(manifest.key, manifest.assets, manifest.routes);
|
|
292
|
+
if (derived !== manifest.hash || derived !== unit.unitHash) {
|
|
293
|
+
refuse("unit-hash-mismatch", `${where}: its content hashes to ${derived}, not ${unit.unitHash}`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
const serves = candidate.units.filter((unit) => unit.key.startsWith("domain:")).map((unit) => unit.key.slice("domain:".length)).sort(byteOrder);
|
|
297
|
+
return { identity: await releaseIdentity(candidate), unitSet: await lawUnitSetIdentity(candidate.units), candidate, serves };
|
|
298
|
+
}
|