@plasius/asset-contracts 0.4.5 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,62 @@
1
+ # ADR 0008: Package-Owned Issue Tracking
2
+
3
+ - Status: Accepted
4
+ - Date: 2026-10-23
5
+ - Tracking: site Task 1169; package Task 47; parent Feature 1153 / Epic 1147
6
+
7
+ ## Context
8
+
9
+ GitHub Issues are enabled in `Plasius-LTD/asset-contracts`, but the README says
10
+ otherwise and `package.json` directs npm bug reports to the site repository.
11
+ That stale routing obscures package ownership. Feature 1153 also needs explicit
12
+ package Tasks for model resource contracts before implementation begins.
13
+
14
+ ## Decision
15
+
16
+ Use this repository's enabled Issues for package bugs, support, and implementation
17
+ Tasks. Set `bugs.url` to `https://github.com/Plasius-LTD/asset-contracts/issues`
18
+ and document the same route in the published README. Confidential vulnerability
19
+ reports continue through `SECURITY.md`.
20
+
21
+ The organisation `Plasius-LTD-site` Project governs work definition, acceptance,
22
+ ownership, priority, and status. Epics, Features, and Stories remain in the site
23
+ repository. Package implementation Tasks are native children of their owning
24
+ Feature and Project members. Preserve historical issues and their links rather
25
+ than migrating or duplicating them.
26
+
27
+ Feature 1153 owns four follow-ups: package Tasks 48 (resource descriptors), 49
28
+ (conversion diagnostics), 50 (texture/resource roles), and 51 (promotion manifest
29
+ resource closure). The README links each exact Task. They begin as unassigned
30
+ Backlog/P2/M Project work; their live Project fields remain authoritative.
31
+ They must reuse and qualify existing public contracts and implement only
32
+ evidenced gaps. Completed GPU/model-resolution contracts and separate animation
33
+ and shader ABI Tasks retain their own scope.
34
+
35
+ These Tasks inherit `gpu.model.conversion.enabled`, evaluated by the backend
36
+ remote control plane for canonical adapter adoption. This documentation and
37
+ metadata change adds no runtime evaluation, capability, or flag mutation. Future
38
+ runtime Tasks must document enabled, disabled, and rollback behavior.
39
+
40
+ ## Alternatives
41
+
42
+ Continuing the alternate site tracker would preserve stale routing and conflict
43
+ with the repository-owned implementation Task policy. Disabling package Issues
44
+ would unnecessarily change an already working tracker. Migrating historical
45
+ issues would break continuity without helping the requested follow-up work.
46
+
47
+ ## Test and release implications
48
+
49
+ Regression tests compare npm bug metadata with the owning repository, verify
50
+ active README support/security links, and require all four follow-up links.
51
+ Verify Issues remain enabled and read back each Project field and native parent.
52
+ Run the existing frozen-install, tests, coverage, lint, types, build, package,
53
+ privacy, and dependency gates; no source/API or dependency-version change is
54
+ needed. npm's lockfile does not store `bugs.url`, so it need not change for this
55
+ metadata correction.
56
+
57
+ The support URL is public package metadata. Use a patch release through the
58
+ existing PR, exact-main push CI, and hosted `cd.yml` / `production` npm OIDC path
59
+ from ADR 0007. Verify registry support metadata, published documentation, package
60
+ integrity/provenance, and ESM/CJS/declarations. Do not assume a documentation-only
61
+ release exemption. A routing rollback is an ordinary reviewed metadata release;
62
+ it must not remove existing issues or reopen the disabled-tracker claim.
@@ -0,0 +1,74 @@
1
+ # ADR 0009: Additive Model Resource Manifest Qualification
2
+
3
+ - Status: Accepted
4
+ - Date: 2026-10-24
5
+ - Tracking: package Task 48; site Feature 1153 / Epic 1147
6
+
7
+ ## Context
8
+
9
+ The existing file and manifest factories validate descriptor syntax. They do
10
+ not bind a model-core `ResourcePackageManifest` to the exact exported file set
11
+ or prove that the declared entrypoint exists. Tightening the existing factories
12
+ would change their public compatibility contract, including callers which
13
+ construct descriptors before storage bytes exist.
14
+
15
+ ## Decision
16
+
17
+ Add `qualifyModelResourceManifest` as a separate, opt-in asynchronous boundary.
18
+ Reuse `AssetFileDescriptor`, `AssetManifest`, their existing factories, the
19
+ published model-core package inventory type, and gpu-shader's SHA-256 helper.
20
+ The qualifier requires one inventory entry and one byte payload for each
21
+ descriptor, including the entrypoint. It checks portable relative paths,
22
+ case-insensitive and file/directory collisions, canonical declared MIME types,
23
+ safe integer byte lengths, actual lengths and SHA-256 digests. Output file
24
+ order uses locale-independent code-unit comparison.
25
+
26
+ Snapshot all metadata and byte inputs before the first hash. Apply fixed count,
27
+ depth and byte ceilings, reject shared byte storage, and check cancellation and
28
+ deadline around each bounded hash. Web Crypto cannot cancel an in-flight
29
+ digest; a deadline is checked again before any successful return. No partial
30
+ manifest is returned on failure. Errors contain fixed codes, not caller data.
31
+
32
+ Keep roles, sourceAdapter and source/derived paths unchanged. Embedded resources
33
+ remain inside their exported entrypoint; they are not invented as separate
34
+ archive files. A verified model-core fixture independently checks embedded and
35
+ external resource identity and source provenance. This boundary neither parses
36
+ formats nor proves that a file internally references every required resource.
37
+ Task 51 owns promotion/resource closure, Task 50 owns detailed role mapping,
38
+ Task 12 owns animation/source relationship schemas and Task 23 owns shader ABI
39
+ qualification. Typed model envelopes still use the existing specialized factory.
40
+
41
+ Use exact published `@plasius/gpu-model-core@0.7.0` as a direct dependency because
42
+ the public input declarations reference its inventory type. The import is
43
+ type-only: no core parser, renderer or model runtime is loaded by this helper.
44
+ The full qualification audit additionally found the pre-existing development
45
+ AJV dependency `fast-uri@3.1.8` affected by newly published URI-normalization
46
+ advisories. Raise its existing override floor from `^3.1.6` to `^3.1.9` and
47
+ lock fixed `3.1.9`, retaining major 3. This targeted audit-blocker remediation
48
+ does not add runtime URI processing. No other dependency versions change.
49
+
50
+ ## Alternatives and consequences
51
+
52
+ Changing legacy factories would break staged descriptor construction. Parsing
53
+ glTF/FBX here would duplicate adapter responsibilities. Treating descriptor
54
+ hashes as byte evidence would allow mismatches to survive. The new boundary
55
+ provides byte/inventory qualification only; storage, malware inspection,
56
+ format validity and promotion remain downstream responsibilities. Qualification
57
+ is evidence at call time, not an unforgeable promotion attestation.
58
+
59
+ ## Rollout, tests and release
60
+
61
+ The backend remote evaluator owns `gpu.model.conversion.enabled`. Callers opt
62
+ into this helper when enabled; disabled or rollback paths retain their existing
63
+ factories. This library performs no live flag lookup or state mutation. There
64
+ is no UI or entitlement capability. Tests cover enabled/disabled/rollback
65
+ composition, legacy compatibility, canonical resource identity, malformed
66
+ inputs, fixed budgets, cancellation, deadlines and mutation isolation.
67
+
68
+ Update README, Unreleased changelog and TDR 0004. Require Node 24 frozen install,
69
+ tests/privacy, coverage and changed-source LCOV, lint/types/build, audits, pack
70
+ gates and packed/public ESM/CJS/types qualification. Release an additive minor
71
+ version through the existing PR, exact-main push CI and hosted
72
+ `cd.yml`/`production` OIDC path from ADR 0007. Verify public registry integrity
73
+ and provenance before downstream adoption. This Task authorizes no site
74
+ deployment or change to any existing release gate.
@@ -7,3 +7,5 @@
7
7
  - [ADR 0005: Path-Only Private-Artifact Prevention Gates](./adr-0005-path-only-private-artifact-prevention-gates.md)
8
8
  - [ADR 0006: Additive PVOX Model-Resolution Contracts](./adr-0006-pvox-model-resolution-contracts.md)
9
9
  - [ADR 0007: Hosted OIDC Package Publication](./adr-0007-hosted-oidc-package-publication.md)
10
+ - [ADR 0008: Package-Owned Issue Tracking](./adr-0008-package-owned-issue-tracking.md)
11
+ - [ADR 0009: Additive Model Resource Manifest Qualification](./adr-0009-model-resource-manifest-qualification.md)
@@ -3,3 +3,4 @@
3
3
  - [TDR 0001: WGSL Shader Asset Envelope Validation](./tdr-0001-wgsl-shader-asset-envelope-validation.md)
4
4
  - [TDR 0002: Exact-Version Validation Routing](./tdr-0002-exact-version-validation-routing.md)
5
5
  - [TDR 0003: PVOX Validation and Confirmation Binding](./tdr-0003-pvox-validation-and-confirmation-binding.md)
6
+ - [TDR 0004: Model Resource Manifest Qualification](./tdr-0004-model-resource-manifest-qualification.md)
@@ -0,0 +1,70 @@
1
+ # TDR 0004: Model Resource Manifest Qualification
2
+
3
+ - Status: Accepted
4
+ - Date: 2026-10-24
5
+ - Tracking: asset-contracts Task 48; site Feature 1153
6
+ - Decision: ADR 0009
7
+
8
+ ## Inputs and sequence
9
+
10
+ 1. Accept an existing base or typed model `AssetManifest`, the published
11
+ model-core `ResourcePackageManifest`, and exact exported `Uint8Array` files
12
+ with independent content-type declarations. Optional inputs are native
13
+ AbortSignal and a 1–60,000 ms deadline (default 10,000 ms).
14
+ 2. Reject exotic metadata records, accessors, symbols, prototype keys, sparse
15
+ arrays and unsupported root fields without executing metadata getters.
16
+ Snapshot metadata using fixed count/depth/string/key limits.
17
+ 3. Reuse the existing base or specialized model factory. Keep every existing
18
+ role, source adapter and specialized field. Do not tighten legacy factories.
19
+ 4. Require portable relative paths, unique case-insensitive names and no
20
+ file/directory prefix collision. The entrypoint must be a descriptor path.
21
+ Reject unsafe integer counts and fixed file/aggregate budget overruns.
22
+ 5. Require one canonical package entry per descriptor, unique bounded logical
23
+ ids, supported core inventory roles, and MIME declaration agreement when
24
+ the optional inventory declaration exists. Do not map core roles to storage
25
+ roles or infer content type from a filename.
26
+ 6. Require one actual payload per descriptor with exact byte length and MIME
27
+ declaration. Read byte lengths and buffers through typed-array intrinsics,
28
+ rejecting shared storage. Only after all budgets and actual lengths pass,
29
+ snapshot bytes before any asynchronous operation.
30
+ 7. Sequentially compute SHA-256 through gpu-shader's existing Web Crypto helper.
31
+ Check signal/deadline before and after every digest; compare exact lowercase
32
+ digests. Return no result on any failure. Sequential hashing avoids
33
+ unbounded concurrency; no filesystem, network, log, retry or caller callback
34
+ is introduced. Deadline expiration cannot interrupt the platform digest,
35
+ but it prevents a successful return after the operation finishes.
36
+ 8. Return frozen metadata and frozen descriptors sorted by UTF-16 code units.
37
+ The result is not a storage receipt or promotion attestation. A caller must
38
+ retain the qualified bytes unchanged at its own durable boundary.
39
+
40
+ ## Canonical fixture and source fidelity
41
+
42
+ Tests import the published model-core package, independently verify a triangle
43
+ document, and build its resource graph. The derived position buffer is embedded
44
+ in a glTF data URI; the original PNG remains an external exported file. Compare
45
+ each resource's byte content/hash with its payload and exported location, and
46
+ bind document source provenance to the entrypoint digest. Embedded data is not
47
+ listed as a fictitious archive file. The fixture does not grant production
48
+ format-parser, texture-role or promotion-closure authority.
49
+
50
+ ## Rollout, compatibility and NFR evidence
51
+
52
+ Callers compose this helper behind backend-evaluated
53
+ `gpu.model.conversion.enabled`; disabled and rollback paths continue the legacy
54
+ factory. Test that enabled qualification rejects mismatched bytes while both
55
+ fallback paths preserve legacy behavior. No live control mutation is needed.
56
+ Security/privacy tests cover malformed shapes, value-free failures, traversal,
57
+ collisions, byte/hash/MIME drift, shared buffers and input mutation. Fixed
58
+ budgets and sequential work address reliability/performance; frozen snapshots
59
+ and exact inventory types address maintainability/testability. UI accessibility,
60
+ SEO and storage/encryption controls are N/A to this in-memory library boundary.
61
+
62
+ The public type requires exact `@plasius/gpu-model-core@0.7.0`; the import is
63
+ type-only. A full-audit failure also requires the targeted development-only
64
+ fast-uri override floor `^3.1.6` to `^3.1.9` and lock `3.1.8` to fixed `3.1.9`;
65
+ retain the existing AJV/major-3 dependency contract and rerun all gates. No
66
+ other dependency versions change. Require frozen Node 24 install,
67
+ full tests/privacy/coverage and changed-source LCOV, lint/types/build, dependency
68
+ tree/audits, packed ESM/CJS/types/browser-safe import, and public-registry
69
+ artifact/provenance checks. Ship an additive minor release via existing
70
+ `cd.yml`/`main`/`production` OIDC governance. Site adoption is separate.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plasius/asset-contracts",
3
- "version": "0.4.5",
3
+ "version": "0.5.0",
4
4
  "description": "Canonical contracts for Plasius asset jobs, model resolution, immutable WGSL shader assets, reviews, and promotion records.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -66,7 +66,7 @@
66
66
  "url": "git+https://github.com/Plasius-LTD/asset-contracts.git"
67
67
  },
68
68
  "bugs": {
69
- "url": "https://github.com/Plasius-LTD/plasius-ltd-site/issues"
69
+ "url": "https://github.com/Plasius-LTD/asset-contracts/issues"
70
70
  },
71
71
  "homepage": "https://github.com/Plasius-LTD/asset-contracts#readme",
72
72
  "devDependencies": {
@@ -103,12 +103,13 @@
103
103
  "overrides": {
104
104
  "brace-expansion": "5.0.12",
105
105
  "esbuild": "^0.28.1",
106
- "fast-uri": "^3.1.6",
106
+ "fast-uri": "^3.1.9",
107
107
  "minimatch": "^10.2.1",
108
108
  "nanoid": "3.3.18",
109
109
  "postcss": "8.5.26"
110
110
  },
111
111
  "dependencies": {
112
+ "@plasius/gpu-model-core": "0.7.0",
112
113
  "@plasius/gpu-shader": "^0.1.10"
113
114
  }
114
115
  }
package/src/index.ts CHANGED
@@ -38,6 +38,7 @@ export * from "./model-resolution.js";
38
38
  export * from "./pvox-hash-preimage.js";
39
39
  export * from "./pvox-model-resolution.js";
40
40
  export * from "./pvox-model-resolution-schema.js";
41
+ export * from "./model-resource-manifest.js";
41
42
  export {
42
43
  GPU_SHADER_STORE_FEATURE_FLAG,
43
44
  GPU_SHADER_STYLE_SELECTION_CAPABILITY,
@@ -0,0 +1,198 @@
1
+ import type { ResourcePackageManifest } from "@plasius/gpu-model-core";
2
+ import { computeSha256 } from "@plasius/gpu-shader";
3
+ import {
4
+ createAssetManifest, createModelAssetManifest,
5
+ type AssetManifest, type ModelAssetManifestInput,
6
+ } from "./index.js";
7
+
8
+ /** Fixed ceilings for the opt-in, in-memory model package qualification boundary. */
9
+ export const MODEL_RESOURCE_MANIFEST_LIMITS = Object.freeze({
10
+ files: 16_384, fileBytes: 64 * 1024 * 1024, totalBytes: 128 * 1024 * 1024,
11
+ metadataValues: 262_144, metadataDepth: 16, timeoutMs: 60_000,
12
+ });
13
+
14
+ /** Exported file bytes and their independently supplied canonical MIME declaration. */
15
+ export interface ModelResourceFileInput {
16
+ readonly path: string;
17
+ readonly bytes: Uint8Array;
18
+ readonly contentType: string;
19
+ }
20
+
21
+ /** Complete exported inventory; embedded resources remain inside their owning file. */
22
+ export interface ModelResourceManifestQualificationInput {
23
+ readonly manifest: AssetManifest;
24
+ readonly packageManifest: ResourcePackageManifest;
25
+ readonly files: readonly ModelResourceFileInput[];
26
+ readonly signal?: AbortSignal;
27
+ readonly timeoutMs?: number;
28
+ }
29
+
30
+ /** Value-free failure, suitable for callers' bounded diagnostic projections. */
31
+ export class ModelResourceManifestError extends TypeError {
32
+ constructor(public readonly code: "invalid-input" | "limit-exceeded" | "path-conflict" | "inventory-mismatch" | "content-mismatch" | "aborted" | "deadline-exceeded") {
33
+ super(`Invalid model resource manifest: ${code}`);
34
+ this.name = "ModelResourceManifestError";
35
+ }
36
+ }
37
+ function fail(code: ModelResourceManifestError["code"] = "invalid-input"): never {
38
+ throw new ModelResourceManifestError(code);
39
+ }
40
+ function record(value: unknown): Record<string, unknown> {
41
+ if (!value || typeof value !== "object" || ![Object.prototype, null].includes(Object.getPrototypeOf(value))) fail();
42
+ const output: Record<string, unknown> = Object.create(null) as Record<string, unknown>;
43
+ const keys = Reflect.ownKeys(value);
44
+ if (keys.length > MODEL_RESOURCE_MANIFEST_LIMITS.files) fail("limit-exceeded");
45
+ for (const key of keys) {
46
+ if (typeof key !== "string" || ["__proto__", "constructor", "prototype"].includes(key)) fail();
47
+ if (key.length > 128) fail("limit-exceeded");
48
+ const property = Object.getOwnPropertyDescriptor(value, key)!;
49
+ if (!("value" in property) || !property.enumerable) fail();
50
+ output[key] = property.value;
51
+ }
52
+ return output;
53
+ }
54
+ function list(value: unknown): unknown[] {
55
+ if (!Array.isArray(value) || Object.getPrototypeOf(value) !== Array.prototype) fail();
56
+ if (value.length > MODEL_RESOURCE_MANIFEST_LIMITS.files) fail("limit-exceeded");
57
+ if (Reflect.ownKeys(value).length !== value.length + 1) fail();
58
+ const output: unknown[] = [];
59
+ for (let i = 0; i < value.length; i++) {
60
+ const property = Object.getOwnPropertyDescriptor(value, String(i));
61
+ if (!property || !("value" in property) || !property.enumerable) fail();
62
+ output.push(property.value);
63
+ }
64
+ return output;
65
+ }
66
+ function snapshot(value: unknown, budget: { remaining: number }, depth = 0): unknown {
67
+ if (--budget.remaining < 0 || depth > MODEL_RESOURCE_MANIFEST_LIMITS.metadataDepth) fail("limit-exceeded");
68
+ if (value === null || value === undefined || typeof value === "boolean") return value;
69
+ if (typeof value === "string") {
70
+ if (value.length > 2048) fail("limit-exceeded");
71
+ return value;
72
+ }
73
+ if (typeof value === "number") {
74
+ if (!Number.isFinite(value)) fail();
75
+ return value;
76
+ }
77
+ if (Array.isArray(value)) return Object.freeze(list(value).map((item) => snapshot(item, budget, depth + 1)));
78
+ const fields = record(value);
79
+ for (const key of Object.keys(fields)) fields[key] = snapshot(fields[key], budget, depth + 1);
80
+ return Object.freeze(fields);
81
+ }
82
+ function path(value: unknown): string {
83
+ if (typeof value !== "string" || value.length > 1024 || !value.length) fail();
84
+ for (const part of value.split("/")) {
85
+ if (part.length > 255 || !/^[A-Za-z0-9_][A-Za-z0-9_.-]*$/u.test(part)
86
+ || part.endsWith(".") || /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(?:\.|$)/iu.test(part)) fail();
87
+ }
88
+ return value;
89
+ }
90
+ function contentType(value: unknown): string {
91
+ if (typeof value !== "string" || value.length > 128
92
+ || !/^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+(?:; charset=utf-8)?$/u.test(value)) fail();
93
+ return value;
94
+ }
95
+ function uniquePaths(paths: readonly string[]): void {
96
+ const seen = new Set<string>();
97
+ for (const value of paths) {
98
+ const normalized = path(value).toLowerCase();
99
+ if (seen.has(normalized)) fail("path-conflict");
100
+ seen.add(normalized);
101
+ }
102
+ for (const value of seen) {
103
+ for (let offset = value.indexOf("/"); offset !== -1; offset = value.indexOf("/", offset + 1)) {
104
+ if (seen.has(value.slice(0, offset))) fail("path-conflict");
105
+ }
106
+ }
107
+ }
108
+
109
+ const typedArrayPrototype = Object.getPrototypeOf(Uint8Array.prototype);
110
+ const byteLengthGetter = Object.getOwnPropertyDescriptor(typedArrayPrototype, "byteLength")!.get!;
111
+ const bufferGetter = Object.getOwnPropertyDescriptor(typedArrayPrototype, "buffer")!.get!;
112
+
113
+ /**
114
+ * Binds an exported model inventory to exact file bytes without parsing formats,
115
+ * fetching resources or changing legacy factories. Callers own remote rollout,
116
+ * content inspection and durable storage. The result is evidence at call time,
117
+ * not a promotion attestation. All inputs are copied before the first digest.
118
+ */
119
+ export async function qualifyModelResourceManifest(input: ModelResourceManifestQualificationInput): Promise<AssetManifest> {
120
+ try { return await qualify(input); }
121
+ catch (error) {
122
+ if (error instanceof ModelResourceManifestError) throw error;
123
+ return fail();
124
+ }
125
+ }
126
+
127
+ async function qualify(input: ModelResourceManifestQualificationInput): Promise<AssetManifest> {
128
+ const started = performance.now();
129
+ const root = record(input);
130
+ if (Object.keys(root).some((key) => !["manifest", "packageManifest", "files", "signal", "timeoutMs"].includes(key))) fail();
131
+ const timeout = root.timeoutMs ?? 10_000;
132
+ if (typeof timeout !== "number" || !Number.isSafeInteger(timeout) || timeout < 1 || timeout > MODEL_RESOURCE_MANIFEST_LIMITS.timeoutMs) fail();
133
+ const signal = root.signal;
134
+ if (signal !== undefined && !(signal instanceof AbortSignal)) fail();
135
+ const check = (): void => {
136
+ if (signal?.aborted) fail("aborted");
137
+ if (performance.now() - started >= timeout) fail("deadline-exceeded");
138
+ };
139
+ check();
140
+ const budget = { remaining: MODEL_RESOURCE_MANIFEST_LIMITS.metadataValues };
141
+ const rawManifest = snapshot(root.manifest, budget) as AssetManifest;
142
+ const inventory = record(snapshot(root.packageManifest, budget));
143
+ if (Object.keys(inventory).some((key) => !["format", "entries"].includes(key))
144
+ || !["zip", "tar", "directory-manifest", "inline"].includes(inventory.format as string)) fail();
145
+ let manifest: AssetManifest;
146
+ try {
147
+ manifest = rawManifest.assetKind === "model"
148
+ ? createModelAssetManifest(rawManifest as ModelAssetManifestInput)
149
+ : createAssetManifest(rawManifest);
150
+ } catch { return fail(); }
151
+ uniquePaths(manifest.files.map((file) => file.path));
152
+ if (!manifest.files.some((file) => file.path === path(manifest.entrypoint))) fail("inventory-mismatch");
153
+ const descriptors = new Map(manifest.files.map((file) => [file.path, file]));
154
+ let total = 0;
155
+ for (const file of manifest.files) {
156
+ if (!Number.isSafeInteger(file.byteLength)) fail();
157
+ if (file.byteLength > MODEL_RESOURCE_MANIFEST_LIMITS.fileBytes) fail("limit-exceeded");
158
+ total += file.byteLength;
159
+ if (total > MODEL_RESOURCE_MANIFEST_LIMITS.totalBytes) fail("limit-exceeded");
160
+ contentType(file.contentType);
161
+ }
162
+ const entries = list(inventory.entries).map(record);
163
+ uniquePaths(entries.map((entry) => path(entry.path)));
164
+ const ids = new Set<string>();
165
+ for (const entry of entries) {
166
+ if (Object.keys(entry).some((key) => !["id", "path", "role", "contentType"].includes(key))
167
+ || typeof entry.id !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u.test(entry.id)
168
+ || ids.has(entry.id) || !["mesh", "material", "texture", "animation", "auxiliary"].includes(entry.role as string)) fail();
169
+ ids.add(entry.id);
170
+ const descriptor = descriptors.get(entry.path as string);
171
+ if (!descriptor) fail("inventory-mismatch");
172
+ if (entry.contentType !== undefined && contentType(entry.contentType) !== descriptor.contentType) fail("content-mismatch");
173
+ }
174
+ if (entries.length !== descriptors.size) fail("inventory-mismatch");
175
+ const payloads = list(root.files).map((value) => {
176
+ const file = record(value);
177
+ if (Object.keys(file).length !== 3 || Object.keys(file).some((key) => !["path", "bytes", "contentType"].includes(key))) fail();
178
+ const filePath = path(file.path);
179
+ if (!(file.bytes instanceof Uint8Array) || Object.getPrototypeOf(file.bytes) !== Uint8Array.prototype
180
+ || !(bufferGetter.call(file.bytes) instanceof ArrayBuffer)) fail();
181
+ const descriptor = descriptors.get(filePath);
182
+ if (!descriptor) fail("inventory-mismatch");
183
+ if (byteLengthGetter.call(file.bytes) !== descriptor.byteLength || contentType(file.contentType) !== descriptor.contentType) fail("content-mismatch");
184
+ return { path: filePath, bytes: file.bytes, descriptor };
185
+ });
186
+ uniquePaths(payloads.map((file) => file.path));
187
+ if (payloads.length !== descriptors.size) fail("inventory-mismatch");
188
+ // Descriptor budgets and actual lengths were checked before allocating copies.
189
+ const copies = payloads.map((file) => ({ ...file, bytes: new Uint8Array(file.bytes) }));
190
+ for (const file of copies) {
191
+ check();
192
+ let hash: string;
193
+ try { hash = await computeSha256(file.bytes); } catch { return fail("content-mismatch"); }
194
+ check();
195
+ if (hash !== file.descriptor.sha256) fail("content-mismatch");
196
+ }
197
+ return Object.freeze({ ...manifest, files: Object.freeze([...manifest.files].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0)) });
198
+ }