@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.
- package/CHANGELOG.md +40 -0
- package/README.md +96 -1
- package/dist/index.cjs +1130 -954
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +38 -1
- package/dist/index.d.ts +38 -1
- package/dist/index.js +1108 -935
- package/dist/index.js.map +1 -1
- package/docs/adrs/adr-0008-package-owned-issue-tracking.md +62 -0
- package/docs/adrs/adr-0009-model-resource-manifest-qualification.md +74 -0
- package/docs/adrs/index.md +2 -0
- package/docs/tdrs/index.md +1 -0
- package/docs/tdrs/tdr-0004-model-resource-manifest-qualification.md +70 -0
- package/package.json +4 -3
- package/src/index.ts +1 -0
- package/src/model-resource-manifest.ts +198 -0
|
@@ -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.
|
package/docs/adrs/index.md
CHANGED
|
@@ -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)
|
package/docs/tdrs/index.md
CHANGED
|
@@ -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.
|
|
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/
|
|
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.
|
|
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
|
+
}
|