@vivipilot/render-manifest 0.1.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/README.md +13 -0
- package/dist/index.d.ts +113 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +289 -0
- package/dist/index.js.map +1 -0
- package/package.json +32 -0
- package/src/index.test.ts +186 -0
- package/src/index.ts +444 -0
- package/tsconfig.json +26 -0
package/README.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# @vivipilot/render-manifest
|
|
2
|
+
|
|
3
|
+
Shared schema and cryptographic helpers for Vivipilot signed render manifests.
|
|
4
|
+
|
|
5
|
+
This package is the trust boundary between Vivipilot Cloud and local rendering:
|
|
6
|
+
|
|
7
|
+
- Cloud generates and signs `vivipilot.renderManifest.v1` manifests.
|
|
8
|
+
- CLI/MCP/browser renderers verify signatures with public keys.
|
|
9
|
+
- Large assets are referenced by immutable URLs plus SHA-256 integrity metadata.
|
|
10
|
+
- Paid CLI/MCP manifests do not expire unless an `expiresAt` field is explicitly set for internal/test use.
|
|
11
|
+
|
|
12
|
+
The package intentionally has no runtime dependencies. It uses WebCrypto Ed25519 so the same verifier can run in browsers, Node CLI code, and server runtimes.
|
|
13
|
+
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
export declare const RENDER_MANIFEST_SCHEMA_VERSION: "vivipilot.renderManifest.v1";
|
|
2
|
+
export declare const RENDER_MANIFEST_SIGNATURE_ALG: "Ed25519";
|
|
3
|
+
export type JsonPrimitive = null | boolean | number | string;
|
|
4
|
+
export type JsonValue = JsonPrimitive | JsonValue[] | {
|
|
5
|
+
[key: string]: JsonValue;
|
|
6
|
+
};
|
|
7
|
+
export type JsonObject = {
|
|
8
|
+
[key: string]: JsonValue;
|
|
9
|
+
};
|
|
10
|
+
export type RenderManifestResolution = "720p" | "1080p" | "2k" | "4k";
|
|
11
|
+
export type RenderManifestFormat = "mp4" | "webm" | "gif" | "mov";
|
|
12
|
+
export type RenderManifestEngine = "pixi-dw-scene-v1" | "three-scene-v1" | "mixed";
|
|
13
|
+
export type RenderManifestCreditSource = "paid_topup" | "paid_enterprise" | "internal_test";
|
|
14
|
+
export type RenderManifestAsset = {
|
|
15
|
+
assetId: string;
|
|
16
|
+
url: string;
|
|
17
|
+
sha256: string;
|
|
18
|
+
mimeType: string;
|
|
19
|
+
byteLength: number;
|
|
20
|
+
role?: string;
|
|
21
|
+
cacheKey?: string;
|
|
22
|
+
};
|
|
23
|
+
export type RenderManifestEntitlement = {
|
|
24
|
+
maxResolution: RenderManifestResolution;
|
|
25
|
+
watermark: boolean;
|
|
26
|
+
commercialUse: boolean;
|
|
27
|
+
allowedFormats?: RenderManifestFormat[];
|
|
28
|
+
};
|
|
29
|
+
export type RenderManifestBilling = {
|
|
30
|
+
creditTransactionId: string;
|
|
31
|
+
creditsCharged: number;
|
|
32
|
+
idempotencyKey: string;
|
|
33
|
+
creditSource: RenderManifestCreditSource;
|
|
34
|
+
};
|
|
35
|
+
export type RenderManifestCanvas = {
|
|
36
|
+
width: number;
|
|
37
|
+
height: number;
|
|
38
|
+
fps: number;
|
|
39
|
+
};
|
|
40
|
+
export type RenderManifestRenderPayload = {
|
|
41
|
+
engine: RenderManifestEngine;
|
|
42
|
+
canvas: RenderManifestCanvas;
|
|
43
|
+
durationFrames: number;
|
|
44
|
+
backgroundColor?: string;
|
|
45
|
+
overlays: JsonValue[];
|
|
46
|
+
assets?: RenderManifestAsset[];
|
|
47
|
+
metadata?: JsonObject;
|
|
48
|
+
};
|
|
49
|
+
export type RenderManifestIntegrity = {
|
|
50
|
+
canonicalPayloadHash: string;
|
|
51
|
+
sceneHash: string;
|
|
52
|
+
};
|
|
53
|
+
export type RenderManifestSignature = {
|
|
54
|
+
alg: typeof RENDER_MANIFEST_SIGNATURE_ALG;
|
|
55
|
+
keyId: string;
|
|
56
|
+
value: string;
|
|
57
|
+
};
|
|
58
|
+
export type VivipilotRenderManifestV1 = {
|
|
59
|
+
schema: typeof RENDER_MANIFEST_SCHEMA_VERSION;
|
|
60
|
+
manifestId: string;
|
|
61
|
+
generationId: string;
|
|
62
|
+
userId: string;
|
|
63
|
+
projectId?: string;
|
|
64
|
+
createdAt: string;
|
|
65
|
+
expiresAt?: string;
|
|
66
|
+
engineVersion: string;
|
|
67
|
+
entitlement: RenderManifestEntitlement;
|
|
68
|
+
billing: RenderManifestBilling;
|
|
69
|
+
render: RenderManifestRenderPayload;
|
|
70
|
+
integrity: RenderManifestIntegrity;
|
|
71
|
+
signature: RenderManifestSignature;
|
|
72
|
+
};
|
|
73
|
+
export type UnsignedVivipilotRenderManifestV1 = Omit<VivipilotRenderManifestV1, "signature">;
|
|
74
|
+
export type RenderManifestPublicKey = {
|
|
75
|
+
keyId: string;
|
|
76
|
+
publicKey: string;
|
|
77
|
+
};
|
|
78
|
+
export type PublicKeyResolver = Record<string, string> | RenderManifestPublicKey[] | ((keyId: string) => string | undefined | Promise<string | undefined>);
|
|
79
|
+
export type SignRenderManifestOptions = {
|
|
80
|
+
keyId: string;
|
|
81
|
+
privateKey: CryptoKey | JsonWebKey | string;
|
|
82
|
+
crypto?: Crypto;
|
|
83
|
+
};
|
|
84
|
+
export type VerifyRenderManifestOptions = {
|
|
85
|
+
publicKeys: PublicKeyResolver;
|
|
86
|
+
crypto?: Crypto;
|
|
87
|
+
now?: Date;
|
|
88
|
+
checkExpiry?: boolean;
|
|
89
|
+
};
|
|
90
|
+
export type RenderManifestVerificationSuccess = {
|
|
91
|
+
ok: true;
|
|
92
|
+
manifest: VivipilotRenderManifestV1;
|
|
93
|
+
canonicalPayloadHash: string;
|
|
94
|
+
};
|
|
95
|
+
export type RenderManifestVerificationFailure = {
|
|
96
|
+
ok: false;
|
|
97
|
+
reason: "invalid_shape" | "invalid_canonical_payload_hash" | "invalid_signature" | "missing_public_key" | "expired" | "crypto_unavailable" | "invalid_key";
|
|
98
|
+
message: string;
|
|
99
|
+
};
|
|
100
|
+
export type RenderManifestVerificationResult = RenderManifestVerificationSuccess | RenderManifestVerificationFailure;
|
|
101
|
+
export declare function canonicalizeJson(value: unknown): string;
|
|
102
|
+
export declare function bytesToBase64Url(bytes: Uint8Array): string;
|
|
103
|
+
export declare function base64UrlToBytes(value: string): Uint8Array;
|
|
104
|
+
export declare function sha256Hex(value: string | Uint8Array, cryptoImpl?: Crypto): Promise<string>;
|
|
105
|
+
export declare function canonicalizeManifestForSigning(manifest: VivipilotRenderManifestV1 | UnsignedVivipilotRenderManifestV1): string;
|
|
106
|
+
export declare function computeCanonicalPayloadHash(manifest: VivipilotRenderManifestV1 | UnsignedVivipilotRenderManifestV1, cryptoImpl?: Crypto): Promise<string>;
|
|
107
|
+
export declare function withCanonicalPayloadHash(manifest: UnsignedVivipilotRenderManifestV1, cryptoImpl?: Crypto): Promise<UnsignedVivipilotRenderManifestV1>;
|
|
108
|
+
export declare function signRenderManifest(manifest: UnsignedVivipilotRenderManifestV1, options: SignRenderManifestOptions): Promise<VivipilotRenderManifestV1>;
|
|
109
|
+
export declare function validateRenderManifestShape(value: unknown): RenderManifestVerificationResult;
|
|
110
|
+
export declare function isManifestExpired(manifest: VivipilotRenderManifestV1, now?: Date): boolean;
|
|
111
|
+
export declare function verifyRenderManifest(value: unknown, options: VerifyRenderManifestOptions): Promise<RenderManifestVerificationResult>;
|
|
112
|
+
export declare function verifyAssetDigest(asset: RenderManifestAsset, bytes: Uint8Array, cryptoImpl?: Crypto): Promise<boolean>;
|
|
113
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,8BAA8B,EAAG,6BAAsC,CAAC;AACrF,eAAO,MAAM,6BAA6B,EAAG,SAAkB,CAAC;AAEhE,MAAM,MAAM,aAAa,GAAG,IAAI,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,CAAC;AAC7D,MAAM,MAAM,SAAS,GAAG,aAAa,GAAG,SAAS,EAAE,GAAG;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAAC;AACnF,MAAM,MAAM,UAAU,GAAG;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAAC;AAEtD,MAAM,MAAM,wBAAwB,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,IAAI,CAAC;AACtE,MAAM,MAAM,oBAAoB,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,KAAK,CAAC;AAClE,MAAM,MAAM,oBAAoB,GAAG,kBAAkB,GAAG,gBAAgB,GAAG,OAAO,CAAC;AACnF,MAAM,MAAM,0BAA0B,GAAG,YAAY,GAAG,iBAAiB,GAAG,eAAe,CAAC;AAE5F,MAAM,MAAM,mBAAmB,GAAG;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,yBAAyB,GAAG;IACtC,aAAa,EAAE,wBAAwB,CAAC;IACxC,SAAS,EAAE,OAAO,CAAC;IACnB,aAAa,EAAE,OAAO,CAAC;IACvB,cAAc,CAAC,EAAE,oBAAoB,EAAE,CAAC;CACzC,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,mBAAmB,EAAE,MAAM,CAAC;IAC5B,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,0BAA0B,CAAC;CAC1C,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;CACb,CAAC;AAEF,MAAM,MAAM,2BAA2B,GAAG;IACxC,MAAM,EAAE,oBAAoB,CAAC;IAC7B,MAAM,EAAE,oBAAoB,CAAC;IAC7B,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,EAAE,SAAS,EAAE,CAAC;IACtB,MAAM,CAAC,EAAE,mBAAmB,EAAE,CAAC;IAC/B,QAAQ,CAAC,EAAE,UAAU,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC,oBAAoB,EAAE,MAAM,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC,GAAG,EAAE,OAAO,6BAA6B,CAAC;IAC1C,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,yBAAyB,GAAG;IACtC,MAAM,EAAE,OAAO,8BAA8B,CAAC;IAC9C,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,yBAAyB,CAAC;IACvC,OAAO,EAAE,qBAAqB,CAAC;IAC/B,MAAM,EAAE,2BAA2B,CAAC;IACpC,SAAS,EAAE,uBAAuB,CAAC;IACnC,SAAS,EAAE,uBAAuB,CAAC;CACpC,CAAC;AAEF,MAAM,MAAM,iCAAiC,GAAG,IAAI,CAAC,yBAAyB,EAAE,WAAW,CAAC,CAAC;AAE7F,MAAM,MAAM,uBAAuB,GAAG;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GACzB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACtB,uBAAuB,EAAE,GACzB,CAAC,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;AAE1E,MAAM,MAAM,yBAAyB,GAAG;IACtC,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,SAAS,GAAG,UAAU,GAAG,MAAM,CAAC;IAC5C,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,2BAA2B,GAAG;IACxC,UAAU,EAAE,iBAAiB,CAAC;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,IAAI,CAAC;IACX,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,iCAAiC,GAAG;IAC9C,EAAE,EAAE,IAAI,CAAC;IACT,QAAQ,EAAE,yBAAyB,CAAC;IACpC,oBAAoB,EAAE,MAAM,CAAC;CAC9B,CAAC;AAEF,MAAM,MAAM,iCAAiC,GAAG;IAC9C,EAAE,EAAE,KAAK,CAAC;IACV,MAAM,EACF,eAAe,GACf,gCAAgC,GAChC,mBAAmB,GACnB,oBAAoB,GACpB,SAAS,GACT,oBAAoB,GACpB,aAAa,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,gCAAgC,GACxC,iCAAiC,GACjC,iCAAiC,CAAC;AA2CtC,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAavD;AAgBD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAE1D;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,CAO1D;AAkBD,wBAAsB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAGhG;AAmBD,wBAAgB,8BAA8B,CAAC,QAAQ,EAAE,yBAAyB,GAAG,iCAAiC,GAAG,MAAM,CAE9H;AAED,wBAAsB,2BAA2B,CAC/C,QAAQ,EAAE,yBAAyB,GAAG,iCAAiC,EACvE,UAAU,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,MAAM,CAAC,CAEjB;AAED,wBAAsB,wBAAwB,CAC5C,QAAQ,EAAE,iCAAiC,EAC3C,UAAU,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,iCAAiC,CAAC,CAS5C;AA8BD,wBAAsB,kBAAkB,CACtC,QAAQ,EAAE,iCAAiC,EAC3C,OAAO,EAAE,yBAAyB,GACjC,OAAO,CAAC,yBAAyB,CAAC,CAiBpC;AAYD,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,OAAO,GAAG,gCAAgC,CAgD5F;AAQD,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,yBAAyB,EAAE,GAAG,OAAa,GAAG,OAAO,CAEhG;AAED,wBAAsB,oBAAoB,CACxC,KAAK,EAAE,OAAO,EACd,OAAO,EAAE,2BAA2B,GACnC,OAAO,CAAC,gCAAgC,CAAC,CAyC3C;AAED,wBAAsB,iBAAiB,CAAC,KAAK,EAAE,mBAAmB,EAAE,KAAK,EAAE,UAAU,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAI5H"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
export const RENDER_MANIFEST_SCHEMA_VERSION = "vivipilot.renderManifest.v1";
|
|
2
|
+
export const RENDER_MANIFEST_SIGNATURE_ALG = "Ed25519";
|
|
3
|
+
const ED25519_ALGORITHM = { name: RENDER_MANIFEST_SIGNATURE_ALG };
|
|
4
|
+
const HEX_SHA256_PATTERN = /^[a-f0-9]{64}$/i;
|
|
5
|
+
function fail(reason, message) {
|
|
6
|
+
return { ok: false, reason, message };
|
|
7
|
+
}
|
|
8
|
+
function isRecord(value) {
|
|
9
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
10
|
+
}
|
|
11
|
+
function isNonEmptyString(value) {
|
|
12
|
+
return typeof value === "string" && value.length > 0;
|
|
13
|
+
}
|
|
14
|
+
function isFinitePositiveNumber(value) {
|
|
15
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
16
|
+
}
|
|
17
|
+
function assertJsonCompatible(value, path = "$") {
|
|
18
|
+
if (value === null)
|
|
19
|
+
return;
|
|
20
|
+
const kind = typeof value;
|
|
21
|
+
if (kind === "string" || kind === "boolean")
|
|
22
|
+
return;
|
|
23
|
+
if (kind === "number") {
|
|
24
|
+
if (!Number.isFinite(value))
|
|
25
|
+
throw new Error(`Non-finite number at ${path}`);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (Array.isArray(value)) {
|
|
29
|
+
value.forEach((item, index) => assertJsonCompatible(item, `${path}[${index}]`));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (kind === "object") {
|
|
33
|
+
for (const [key, entryValue] of Object.entries(value)) {
|
|
34
|
+
if (entryValue === undefined)
|
|
35
|
+
throw new Error(`Undefined value at ${path}.${key}`);
|
|
36
|
+
assertJsonCompatible(entryValue, `${path}.${key}`);
|
|
37
|
+
}
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
throw new Error(`Unsupported JSON value at ${path}`);
|
|
41
|
+
}
|
|
42
|
+
export function canonicalizeJson(value) {
|
|
43
|
+
assertJsonCompatible(value);
|
|
44
|
+
if (value === null || typeof value !== "object")
|
|
45
|
+
return JSON.stringify(value);
|
|
46
|
+
if (Array.isArray(value))
|
|
47
|
+
return `[${value.map((item) => canonicalizeJson(item)).join(",")}]`;
|
|
48
|
+
const entries = Object.entries(value)
|
|
49
|
+
.filter(([, entryValue]) => entryValue !== undefined)
|
|
50
|
+
.sort(([left], [right]) => left.localeCompare(right));
|
|
51
|
+
return `{${entries
|
|
52
|
+
.map(([key, entryValue]) => `${JSON.stringify(key)}:${canonicalizeJson(entryValue)}`)
|
|
53
|
+
.join(",")}}`;
|
|
54
|
+
}
|
|
55
|
+
function utf8ToBytes(value) {
|
|
56
|
+
return new TextEncoder().encode(value);
|
|
57
|
+
}
|
|
58
|
+
function bytesToBinary(bytes) {
|
|
59
|
+
const chunkSize = 0x8000;
|
|
60
|
+
let binary = "";
|
|
61
|
+
for (let offset = 0; offset < bytes.length; offset += chunkSize) {
|
|
62
|
+
const chunk = bytes.slice(offset, offset + chunkSize);
|
|
63
|
+
binary += String.fromCharCode(...chunk);
|
|
64
|
+
}
|
|
65
|
+
return binary;
|
|
66
|
+
}
|
|
67
|
+
export function bytesToBase64Url(bytes) {
|
|
68
|
+
return btoa(bytesToBinary(bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
69
|
+
}
|
|
70
|
+
export function base64UrlToBytes(value) {
|
|
71
|
+
const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
72
|
+
const padded = normalized.padEnd(normalized.length + ((4 - (normalized.length % 4)) % 4), "=");
|
|
73
|
+
const binary = atob(padded);
|
|
74
|
+
const bytes = new Uint8Array(binary.length);
|
|
75
|
+
for (let index = 0; index < binary.length; index++)
|
|
76
|
+
bytes[index] = binary.charCodeAt(index);
|
|
77
|
+
return bytes;
|
|
78
|
+
}
|
|
79
|
+
function toArrayBuffer(bytes) {
|
|
80
|
+
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
81
|
+
}
|
|
82
|
+
function getCrypto(cryptoImpl) {
|
|
83
|
+
return cryptoImpl ?? globalThis.crypto ?? null;
|
|
84
|
+
}
|
|
85
|
+
async function sha256Bytes(value, cryptoImpl) {
|
|
86
|
+
const cryptoRuntime = getCrypto(cryptoImpl);
|
|
87
|
+
if (!cryptoRuntime?.subtle)
|
|
88
|
+
throw new Error("crypto_unavailable");
|
|
89
|
+
const input = typeof value === "string" ? utf8ToBytes(value) : value;
|
|
90
|
+
const digest = await cryptoRuntime.subtle.digest("SHA-256", input);
|
|
91
|
+
return new Uint8Array(digest);
|
|
92
|
+
}
|
|
93
|
+
export async function sha256Hex(value, cryptoImpl) {
|
|
94
|
+
const bytes = await sha256Bytes(value, cryptoImpl);
|
|
95
|
+
return [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
96
|
+
}
|
|
97
|
+
function stripSignature(manifest) {
|
|
98
|
+
const unsigned = { ...manifest };
|
|
99
|
+
delete unsigned.signature;
|
|
100
|
+
return unsigned;
|
|
101
|
+
}
|
|
102
|
+
function payloadForCanonicalHash(manifest) {
|
|
103
|
+
const unsigned = stripSignature(manifest);
|
|
104
|
+
return {
|
|
105
|
+
...unsigned,
|
|
106
|
+
integrity: {
|
|
107
|
+
...unsigned.integrity,
|
|
108
|
+
canonicalPayloadHash: "",
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
export function canonicalizeManifestForSigning(manifest) {
|
|
113
|
+
return canonicalizeJson(stripSignature(manifest));
|
|
114
|
+
}
|
|
115
|
+
export async function computeCanonicalPayloadHash(manifest, cryptoImpl) {
|
|
116
|
+
return sha256Hex(canonicalizeJson(payloadForCanonicalHash(manifest)), cryptoImpl);
|
|
117
|
+
}
|
|
118
|
+
export async function withCanonicalPayloadHash(manifest, cryptoImpl) {
|
|
119
|
+
const canonicalPayloadHash = await computeCanonicalPayloadHash(manifest, cryptoImpl);
|
|
120
|
+
return {
|
|
121
|
+
...manifest,
|
|
122
|
+
integrity: {
|
|
123
|
+
...manifest.integrity,
|
|
124
|
+
canonicalPayloadHash,
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
async function importPrivateKey(privateKey, cryptoRuntime) {
|
|
129
|
+
if (privateKey instanceof CryptoKey)
|
|
130
|
+
return privateKey;
|
|
131
|
+
if (typeof privateKey === "string") {
|
|
132
|
+
return cryptoRuntime.subtle.importKey("pkcs8", toArrayBuffer(base64UrlToBytes(privateKey)), ED25519_ALGORITHM, false, ["sign"]);
|
|
133
|
+
}
|
|
134
|
+
return cryptoRuntime.subtle.importKey("jwk", privateKey, ED25519_ALGORITHM, false, ["sign"]);
|
|
135
|
+
}
|
|
136
|
+
async function importPublicKey(publicKey, cryptoRuntime) {
|
|
137
|
+
if (publicKey instanceof CryptoKey)
|
|
138
|
+
return publicKey;
|
|
139
|
+
if (typeof publicKey === "string") {
|
|
140
|
+
return cryptoRuntime.subtle.importKey("raw", toArrayBuffer(base64UrlToBytes(publicKey)), ED25519_ALGORITHM, false, ["verify"]);
|
|
141
|
+
}
|
|
142
|
+
return cryptoRuntime.subtle.importKey("jwk", publicKey, ED25519_ALGORITHM, false, ["verify"]);
|
|
143
|
+
}
|
|
144
|
+
export async function signRenderManifest(manifest, options) {
|
|
145
|
+
const cryptoRuntime = getCrypto(options.crypto);
|
|
146
|
+
if (!cryptoRuntime?.subtle)
|
|
147
|
+
throw new Error("crypto_unavailable");
|
|
148
|
+
const preparedManifest = await withCanonicalPayloadHash(manifest, cryptoRuntime);
|
|
149
|
+
const signingPayload = utf8ToBytes(canonicalizeManifestForSigning(preparedManifest));
|
|
150
|
+
const privateKey = await importPrivateKey(options.privateKey, cryptoRuntime);
|
|
151
|
+
const signature = await cryptoRuntime.subtle.sign(ED25519_ALGORITHM, privateKey, signingPayload);
|
|
152
|
+
return {
|
|
153
|
+
...preparedManifest,
|
|
154
|
+
signature: {
|
|
155
|
+
alg: RENDER_MANIFEST_SIGNATURE_ALG,
|
|
156
|
+
keyId: options.keyId,
|
|
157
|
+
value: bytesToBase64Url(new Uint8Array(signature)),
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
function validateAssetShape(asset) {
|
|
162
|
+
if (!isRecord(asset))
|
|
163
|
+
return false;
|
|
164
|
+
return isNonEmptyString(asset.assetId)
|
|
165
|
+
&& isNonEmptyString(asset.url)
|
|
166
|
+
&& isNonEmptyString(asset.sha256)
|
|
167
|
+
&& HEX_SHA256_PATTERN.test(asset.sha256)
|
|
168
|
+
&& isNonEmptyString(asset.mimeType)
|
|
169
|
+
&& isFinitePositiveNumber(asset.byteLength);
|
|
170
|
+
}
|
|
171
|
+
export function validateRenderManifestShape(value) {
|
|
172
|
+
if (!isRecord(value))
|
|
173
|
+
return fail("invalid_shape", "Manifest must be an object.");
|
|
174
|
+
if (value.schema !== RENDER_MANIFEST_SCHEMA_VERSION)
|
|
175
|
+
return fail("invalid_shape", "Unsupported render manifest schema.");
|
|
176
|
+
if (!isNonEmptyString(value.manifestId))
|
|
177
|
+
return fail("invalid_shape", "manifestId is required.");
|
|
178
|
+
if (!isNonEmptyString(value.generationId))
|
|
179
|
+
return fail("invalid_shape", "generationId is required.");
|
|
180
|
+
if (!isNonEmptyString(value.userId))
|
|
181
|
+
return fail("invalid_shape", "userId is required.");
|
|
182
|
+
if (!isNonEmptyString(value.createdAt) || Number.isNaN(Date.parse(value.createdAt)))
|
|
183
|
+
return fail("invalid_shape", "createdAt must be an ISO date string.");
|
|
184
|
+
if (value.expiresAt !== undefined && (!isNonEmptyString(value.expiresAt) || Number.isNaN(Date.parse(value.expiresAt)))) {
|
|
185
|
+
return fail("invalid_shape", "expiresAt must be an ISO date string when provided.");
|
|
186
|
+
}
|
|
187
|
+
if (!isNonEmptyString(value.engineVersion))
|
|
188
|
+
return fail("invalid_shape", "engineVersion is required.");
|
|
189
|
+
if (!isRecord(value.entitlement))
|
|
190
|
+
return fail("invalid_shape", "entitlement is required.");
|
|
191
|
+
if (!isRecord(value.billing))
|
|
192
|
+
return fail("invalid_shape", "billing is required.");
|
|
193
|
+
if (!isNonEmptyString(value.billing.creditTransactionId))
|
|
194
|
+
return fail("invalid_shape", "billing.creditTransactionId is required.");
|
|
195
|
+
if (!isFinitePositiveNumber(value.billing.creditsCharged))
|
|
196
|
+
return fail("invalid_shape", "billing.creditsCharged must be positive.");
|
|
197
|
+
if (!isNonEmptyString(value.billing.idempotencyKey))
|
|
198
|
+
return fail("invalid_shape", "billing.idempotencyKey is required.");
|
|
199
|
+
if (!isNonEmptyString(value.billing.creditSource))
|
|
200
|
+
return fail("invalid_shape", "billing.creditSource is required.");
|
|
201
|
+
if (!isRecord(value.render))
|
|
202
|
+
return fail("invalid_shape", "render payload is required.");
|
|
203
|
+
if (!isRecord(value.render.canvas))
|
|
204
|
+
return fail("invalid_shape", "render.canvas is required.");
|
|
205
|
+
if (!isFinitePositiveNumber(value.render.canvas.width) || !isFinitePositiveNumber(value.render.canvas.height) || !isFinitePositiveNumber(value.render.canvas.fps)) {
|
|
206
|
+
return fail("invalid_shape", "render.canvas dimensions and fps must be positive.");
|
|
207
|
+
}
|
|
208
|
+
if (!isFinitePositiveNumber(value.render.durationFrames))
|
|
209
|
+
return fail("invalid_shape", "render.durationFrames must be positive.");
|
|
210
|
+
if (!Array.isArray(value.render.overlays))
|
|
211
|
+
return fail("invalid_shape", "render.overlays must be an array.");
|
|
212
|
+
if (value.render.assets !== undefined && (!Array.isArray(value.render.assets) || !value.render.assets.every(validateAssetShape))) {
|
|
213
|
+
return fail("invalid_shape", "render.assets must contain immutable asset references with SHA-256 hashes.");
|
|
214
|
+
}
|
|
215
|
+
if (!isRecord(value.integrity))
|
|
216
|
+
return fail("invalid_shape", "integrity is required.");
|
|
217
|
+
if (!isNonEmptyString(value.integrity.canonicalPayloadHash) || !HEX_SHA256_PATTERN.test(value.integrity.canonicalPayloadHash)) {
|
|
218
|
+
return fail("invalid_shape", "integrity.canonicalPayloadHash must be a SHA-256 hex digest.");
|
|
219
|
+
}
|
|
220
|
+
if (!isNonEmptyString(value.integrity.sceneHash) || !HEX_SHA256_PATTERN.test(value.integrity.sceneHash)) {
|
|
221
|
+
return fail("invalid_shape", "integrity.sceneHash must be a SHA-256 hex digest.");
|
|
222
|
+
}
|
|
223
|
+
if (!isRecord(value.signature))
|
|
224
|
+
return fail("invalid_shape", "signature is required.");
|
|
225
|
+
if (value.signature.alg !== RENDER_MANIFEST_SIGNATURE_ALG)
|
|
226
|
+
return fail("invalid_shape", "Unsupported signature algorithm.");
|
|
227
|
+
if (!isNonEmptyString(value.signature.keyId))
|
|
228
|
+
return fail("invalid_shape", "signature.keyId is required.");
|
|
229
|
+
if (!isNonEmptyString(value.signature.value))
|
|
230
|
+
return fail("invalid_shape", "signature.value is required.");
|
|
231
|
+
return {
|
|
232
|
+
ok: true,
|
|
233
|
+
manifest: value,
|
|
234
|
+
canonicalPayloadHash: value.integrity.canonicalPayloadHash,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
async function resolvePublicKey(keyId, resolver) {
|
|
238
|
+
if (typeof resolver === "function")
|
|
239
|
+
return resolver(keyId);
|
|
240
|
+
if (Array.isArray(resolver))
|
|
241
|
+
return resolver.find((entry) => entry.keyId === keyId)?.publicKey;
|
|
242
|
+
return resolver[keyId];
|
|
243
|
+
}
|
|
244
|
+
export function isManifestExpired(manifest, now = new Date()) {
|
|
245
|
+
return typeof manifest.expiresAt === "string" && Date.parse(manifest.expiresAt) <= now.getTime();
|
|
246
|
+
}
|
|
247
|
+
export async function verifyRenderManifest(value, options) {
|
|
248
|
+
const shape = validateRenderManifestShape(value);
|
|
249
|
+
if (!shape.ok)
|
|
250
|
+
return shape;
|
|
251
|
+
const manifest = shape.manifest;
|
|
252
|
+
const checkExpiry = options.checkExpiry ?? true;
|
|
253
|
+
if (checkExpiry && isManifestExpired(manifest, options.now ?? new Date())) {
|
|
254
|
+
return fail("expired", "Render manifest has expired.");
|
|
255
|
+
}
|
|
256
|
+
let canonicalPayloadHash;
|
|
257
|
+
try {
|
|
258
|
+
canonicalPayloadHash = await computeCanonicalPayloadHash(manifest, options.crypto);
|
|
259
|
+
}
|
|
260
|
+
catch (error) {
|
|
261
|
+
return fail("crypto_unavailable", error instanceof Error ? error.message : "Unable to hash manifest payload.");
|
|
262
|
+
}
|
|
263
|
+
if (canonicalPayloadHash !== manifest.integrity.canonicalPayloadHash) {
|
|
264
|
+
return fail("invalid_canonical_payload_hash", "Manifest canonical payload hash does not match payload.");
|
|
265
|
+
}
|
|
266
|
+
const publicKeyMaterial = await resolvePublicKey(manifest.signature.keyId, options.publicKeys);
|
|
267
|
+
if (!publicKeyMaterial)
|
|
268
|
+
return fail("missing_public_key", `No public key registered for keyId ${manifest.signature.keyId}.`);
|
|
269
|
+
const cryptoRuntime = getCrypto(options.crypto);
|
|
270
|
+
if (!cryptoRuntime?.subtle)
|
|
271
|
+
return fail("crypto_unavailable", "WebCrypto subtle crypto is unavailable.");
|
|
272
|
+
try {
|
|
273
|
+
const publicKey = await importPublicKey(publicKeyMaterial, cryptoRuntime);
|
|
274
|
+
const isValid = await cryptoRuntime.subtle.verify(ED25519_ALGORITHM, publicKey, toArrayBuffer(base64UrlToBytes(manifest.signature.value)), utf8ToBytes(canonicalizeManifestForSigning(manifest)));
|
|
275
|
+
if (!isValid)
|
|
276
|
+
return fail("invalid_signature", "Manifest signature is invalid.");
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
return fail("invalid_key", error instanceof Error ? error.message : "Unable to import or verify public key.");
|
|
280
|
+
}
|
|
281
|
+
return { ok: true, manifest, canonicalPayloadHash };
|
|
282
|
+
}
|
|
283
|
+
export async function verifyAssetDigest(asset, bytes, cryptoImpl) {
|
|
284
|
+
if (asset.byteLength !== bytes.byteLength)
|
|
285
|
+
return false;
|
|
286
|
+
const digest = await sha256Hex(bytes, cryptoImpl);
|
|
287
|
+
return digest.toLowerCase() === asset.sha256.toLowerCase();
|
|
288
|
+
}
|
|
289
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vivipilot/render-manifest",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Signed Vivipilot render manifest schema and verification helpers",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"typecheck": "tsc --noEmit",
|
|
17
|
+
"test": "vitest run"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"vivipilot",
|
|
21
|
+
"render-manifest",
|
|
22
|
+
"signature",
|
|
23
|
+
"local-rendering"
|
|
24
|
+
],
|
|
25
|
+
"author": "",
|
|
26
|
+
"license": "ISC",
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/node": "^25.2.0",
|
|
29
|
+
"typescript": "^5.3.0",
|
|
30
|
+
"vitest": "4.0.18"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
RENDER_MANIFEST_SCHEMA_VERSION,
|
|
4
|
+
base64UrlToBytes,
|
|
5
|
+
bytesToBase64Url,
|
|
6
|
+
canonicalizeJson,
|
|
7
|
+
computeCanonicalPayloadHash,
|
|
8
|
+
sha256Hex,
|
|
9
|
+
signRenderManifest,
|
|
10
|
+
verifyAssetDigest,
|
|
11
|
+
verifyRenderManifest,
|
|
12
|
+
type UnsignedVivipilotRenderManifestV1,
|
|
13
|
+
} from "./index";
|
|
14
|
+
|
|
15
|
+
async function createTestKeyPair() {
|
|
16
|
+
const keyPair = await crypto.subtle.generateKey(
|
|
17
|
+
{ name: "Ed25519" },
|
|
18
|
+
true,
|
|
19
|
+
["sign", "verify"],
|
|
20
|
+
) as CryptoKeyPair;
|
|
21
|
+
|
|
22
|
+
const publicKey = bytesToBase64Url(new Uint8Array(await crypto.subtle.exportKey("raw", keyPair.publicKey)));
|
|
23
|
+
const privateKey = bytesToBase64Url(new Uint8Array(await crypto.subtle.exportKey("pkcs8", keyPair.privateKey)));
|
|
24
|
+
return { publicKey, privateKey };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function baseManifest(): UnsignedVivipilotRenderManifestV1 {
|
|
28
|
+
return {
|
|
29
|
+
schema: RENDER_MANIFEST_SCHEMA_VERSION,
|
|
30
|
+
manifestId: "manifest_123",
|
|
31
|
+
generationId: "gen_123",
|
|
32
|
+
userId: "user_123",
|
|
33
|
+
projectId: "project_123",
|
|
34
|
+
createdAt: "2026-06-27T10:00:00.000Z",
|
|
35
|
+
engineVersion: "vivipilot-renderer@0.1.0",
|
|
36
|
+
entitlement: {
|
|
37
|
+
maxResolution: "1080p",
|
|
38
|
+
watermark: false,
|
|
39
|
+
commercialUse: true,
|
|
40
|
+
allowedFormats: ["mp4", "webm"],
|
|
41
|
+
},
|
|
42
|
+
billing: {
|
|
43
|
+
creditTransactionId: "txn_123",
|
|
44
|
+
creditsCharged: 40,
|
|
45
|
+
idempotencyKey: "idem_123",
|
|
46
|
+
creditSource: "paid_topup",
|
|
47
|
+
},
|
|
48
|
+
render: {
|
|
49
|
+
engine: "pixi-dw-scene-v1",
|
|
50
|
+
canvas: { width: 1280, height: 720, fps: 30 },
|
|
51
|
+
durationFrames: 120,
|
|
52
|
+
backgroundColor: "#ffffff",
|
|
53
|
+
overlays: [
|
|
54
|
+
{
|
|
55
|
+
id: "overlay_1",
|
|
56
|
+
type: "motion_graphics",
|
|
57
|
+
pixiScene: {
|
|
58
|
+
type: "pixi_scene",
|
|
59
|
+
schema: "vivipilot.pixi-motion.v1",
|
|
60
|
+
content: "Test scene",
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
assets: [
|
|
65
|
+
{
|
|
66
|
+
assetId: "asset_1",
|
|
67
|
+
url: "https://cdn.example.com/immutable/asset_1.png",
|
|
68
|
+
sha256: "a".repeat(64),
|
|
69
|
+
mimeType: "image/png",
|
|
70
|
+
byteLength: 4,
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
integrity: {
|
|
75
|
+
canonicalPayloadHash: "",
|
|
76
|
+
sceneHash: "b".repeat(64),
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
describe("render manifest canonicalization", () => {
|
|
82
|
+
it("canonicalizes object keys deterministically", () => {
|
|
83
|
+
expect(canonicalizeJson({ b: 1, a: { d: false, c: true } })).toBe('{"a":{"c":true,"d":false},"b":1}');
|
|
84
|
+
expect(canonicalizeJson({ a: { c: true, d: false }, b: 1 })).toBe('{"a":{"c":true,"d":false},"b":1}');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("round-trips base64url bytes", () => {
|
|
88
|
+
const bytes = new Uint8Array([0, 1, 2, 250, 251, 252, 253, 254, 255]);
|
|
89
|
+
expect(base64UrlToBytes(bytesToBase64Url(bytes))).toEqual(bytes);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("render manifest signing", () => {
|
|
94
|
+
it("signs and verifies an untampered manifest", async () => {
|
|
95
|
+
const keys = await createTestKeyPair();
|
|
96
|
+
const signed = await signRenderManifest(baseManifest(), {
|
|
97
|
+
keyId: "test-key",
|
|
98
|
+
privateKey: keys.privateKey,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const result = await verifyRenderManifest(signed, {
|
|
102
|
+
publicKeys: { "test-key": keys.publicKey },
|
|
103
|
+
now: new Date("2026-06-27T10:01:00.000Z"),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
expect(result.ok).toBe(true);
|
|
107
|
+
if (result.ok) {
|
|
108
|
+
expect(result.canonicalPayloadHash).toBe(signed.integrity.canonicalPayloadHash);
|
|
109
|
+
expect(result.manifest.signature.keyId).toBe("test-key");
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("rejects a tampered manifest payload", async () => {
|
|
114
|
+
const keys = await createTestKeyPair();
|
|
115
|
+
const signed = await signRenderManifest(baseManifest(), {
|
|
116
|
+
keyId: "test-key",
|
|
117
|
+
privateKey: keys.privateKey,
|
|
118
|
+
});
|
|
119
|
+
const tampered = {
|
|
120
|
+
...signed,
|
|
121
|
+
render: {
|
|
122
|
+
...signed.render,
|
|
123
|
+
durationFrames: signed.render.durationFrames + 1,
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const result = await verifyRenderManifest(tampered, {
|
|
128
|
+
publicKeys: { "test-key": keys.publicKey },
|
|
129
|
+
now: new Date("2026-06-27T10:01:00.000Z"),
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
expect(result.ok).toBe(false);
|
|
133
|
+
if (!result.ok) expect(result.reason).toBe("invalid_canonical_payload_hash");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("rejects expired internal/test manifests when expiresAt is present", async () => {
|
|
137
|
+
const keys = await createTestKeyPair();
|
|
138
|
+
const signed = await signRenderManifest({
|
|
139
|
+
...baseManifest(),
|
|
140
|
+
expiresAt: "2026-06-27T10:10:00.000Z",
|
|
141
|
+
}, {
|
|
142
|
+
keyId: "test-key",
|
|
143
|
+
privateKey: keys.privateKey,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const result = await verifyRenderManifest(signed, {
|
|
147
|
+
publicKeys: { "test-key": keys.publicKey },
|
|
148
|
+
now: new Date("2026-06-27T10:11:00.000Z"),
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
expect(result.ok).toBe(false);
|
|
152
|
+
if (!result.ok) expect(result.reason).toBe("expired");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("hashes asset bytes for immutable media checks", async () => {
|
|
156
|
+
const bytes = new TextEncoder().encode("asset");
|
|
157
|
+
const asset = {
|
|
158
|
+
assetId: "asset_1",
|
|
159
|
+
url: "https://cdn.example.com/immutable/asset_1.txt",
|
|
160
|
+
sha256: await sha256Hex(bytes),
|
|
161
|
+
mimeType: "text/plain",
|
|
162
|
+
byteLength: bytes.byteLength,
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
expect(await verifyAssetDigest(asset, bytes)).toBe(true);
|
|
166
|
+
expect(await verifyAssetDigest(asset, new TextEncoder().encode("tampered"))).toBe(false);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("computes a stable hash that excludes the signature and current hash field", async () => {
|
|
170
|
+
const keys = await createTestKeyPair();
|
|
171
|
+
const unsigned = baseManifest();
|
|
172
|
+
const signed = await signRenderManifest(unsigned, {
|
|
173
|
+
keyId: "test-key",
|
|
174
|
+
privateKey: keys.privateKey,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
expect(await computeCanonicalPayloadHash(unsigned)).toBe(signed.integrity.canonicalPayloadHash);
|
|
178
|
+
expect(await computeCanonicalPayloadHash({
|
|
179
|
+
...signed,
|
|
180
|
+
integrity: {
|
|
181
|
+
...signed.integrity,
|
|
182
|
+
canonicalPayloadHash: "c".repeat(64),
|
|
183
|
+
},
|
|
184
|
+
})).toBe(signed.integrity.canonicalPayloadHash);
|
|
185
|
+
});
|
|
186
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
export const RENDER_MANIFEST_SCHEMA_VERSION = "vivipilot.renderManifest.v1" as const;
|
|
2
|
+
export const RENDER_MANIFEST_SIGNATURE_ALG = "Ed25519" as const;
|
|
3
|
+
|
|
4
|
+
export type JsonPrimitive = null | boolean | number | string;
|
|
5
|
+
export type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue };
|
|
6
|
+
export type JsonObject = { [key: string]: JsonValue };
|
|
7
|
+
|
|
8
|
+
export type RenderManifestResolution = "720p" | "1080p" | "2k" | "4k";
|
|
9
|
+
export type RenderManifestFormat = "mp4" | "webm" | "gif" | "mov";
|
|
10
|
+
export type RenderManifestEngine = "pixi-dw-scene-v1" | "three-scene-v1" | "mixed";
|
|
11
|
+
export type RenderManifestCreditSource = "paid_topup" | "paid_enterprise" | "internal_test";
|
|
12
|
+
|
|
13
|
+
export type RenderManifestAsset = {
|
|
14
|
+
assetId: string;
|
|
15
|
+
url: string;
|
|
16
|
+
sha256: string;
|
|
17
|
+
mimeType: string;
|
|
18
|
+
byteLength: number;
|
|
19
|
+
role?: string;
|
|
20
|
+
cacheKey?: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type RenderManifestEntitlement = {
|
|
24
|
+
maxResolution: RenderManifestResolution;
|
|
25
|
+
watermark: boolean;
|
|
26
|
+
commercialUse: boolean;
|
|
27
|
+
allowedFormats?: RenderManifestFormat[];
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type RenderManifestBilling = {
|
|
31
|
+
creditTransactionId: string;
|
|
32
|
+
creditsCharged: number;
|
|
33
|
+
idempotencyKey: string;
|
|
34
|
+
creditSource: RenderManifestCreditSource;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type RenderManifestCanvas = {
|
|
38
|
+
width: number;
|
|
39
|
+
height: number;
|
|
40
|
+
fps: number;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type RenderManifestRenderPayload = {
|
|
44
|
+
engine: RenderManifestEngine;
|
|
45
|
+
canvas: RenderManifestCanvas;
|
|
46
|
+
durationFrames: number;
|
|
47
|
+
backgroundColor?: string;
|
|
48
|
+
overlays: JsonValue[];
|
|
49
|
+
assets?: RenderManifestAsset[];
|
|
50
|
+
metadata?: JsonObject;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export type RenderManifestIntegrity = {
|
|
54
|
+
canonicalPayloadHash: string;
|
|
55
|
+
sceneHash: string;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export type RenderManifestSignature = {
|
|
59
|
+
alg: typeof RENDER_MANIFEST_SIGNATURE_ALG;
|
|
60
|
+
keyId: string;
|
|
61
|
+
value: string;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export type VivipilotRenderManifestV1 = {
|
|
65
|
+
schema: typeof RENDER_MANIFEST_SCHEMA_VERSION;
|
|
66
|
+
manifestId: string;
|
|
67
|
+
generationId: string;
|
|
68
|
+
userId: string;
|
|
69
|
+
projectId?: string;
|
|
70
|
+
createdAt: string;
|
|
71
|
+
expiresAt?: string;
|
|
72
|
+
engineVersion: string;
|
|
73
|
+
entitlement: RenderManifestEntitlement;
|
|
74
|
+
billing: RenderManifestBilling;
|
|
75
|
+
render: RenderManifestRenderPayload;
|
|
76
|
+
integrity: RenderManifestIntegrity;
|
|
77
|
+
signature: RenderManifestSignature;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export type UnsignedVivipilotRenderManifestV1 = Omit<VivipilotRenderManifestV1, "signature">;
|
|
81
|
+
|
|
82
|
+
export type RenderManifestPublicKey = {
|
|
83
|
+
keyId: string;
|
|
84
|
+
publicKey: string;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export type PublicKeyResolver =
|
|
88
|
+
| Record<string, string>
|
|
89
|
+
| RenderManifestPublicKey[]
|
|
90
|
+
| ((keyId: string) => string | undefined | Promise<string | undefined>);
|
|
91
|
+
|
|
92
|
+
export type SignRenderManifestOptions = {
|
|
93
|
+
keyId: string;
|
|
94
|
+
privateKey: CryptoKey | JsonWebKey | string;
|
|
95
|
+
crypto?: Crypto;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export type VerifyRenderManifestOptions = {
|
|
99
|
+
publicKeys: PublicKeyResolver;
|
|
100
|
+
crypto?: Crypto;
|
|
101
|
+
now?: Date;
|
|
102
|
+
checkExpiry?: boolean;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export type RenderManifestVerificationSuccess = {
|
|
106
|
+
ok: true;
|
|
107
|
+
manifest: VivipilotRenderManifestV1;
|
|
108
|
+
canonicalPayloadHash: string;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export type RenderManifestVerificationFailure = {
|
|
112
|
+
ok: false;
|
|
113
|
+
reason:
|
|
114
|
+
| "invalid_shape"
|
|
115
|
+
| "invalid_canonical_payload_hash"
|
|
116
|
+
| "invalid_signature"
|
|
117
|
+
| "missing_public_key"
|
|
118
|
+
| "expired"
|
|
119
|
+
| "crypto_unavailable"
|
|
120
|
+
| "invalid_key";
|
|
121
|
+
message: string;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export type RenderManifestVerificationResult =
|
|
125
|
+
| RenderManifestVerificationSuccess
|
|
126
|
+
| RenderManifestVerificationFailure;
|
|
127
|
+
|
|
128
|
+
const ED25519_ALGORITHM: Algorithm = { name: RENDER_MANIFEST_SIGNATURE_ALG };
|
|
129
|
+
const HEX_SHA256_PATTERN = /^[a-f0-9]{64}$/i;
|
|
130
|
+
|
|
131
|
+
function fail(reason: RenderManifestVerificationFailure["reason"], message: string): RenderManifestVerificationFailure {
|
|
132
|
+
return { ok: false, reason, message };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
136
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function isNonEmptyString(value: unknown): value is string {
|
|
140
|
+
return typeof value === "string" && value.length > 0;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function isFinitePositiveNumber(value: unknown): value is number {
|
|
144
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function assertJsonCompatible(value: unknown, path = "$"): asserts value is JsonValue {
|
|
148
|
+
if (value === null) return;
|
|
149
|
+
const kind = typeof value;
|
|
150
|
+
if (kind === "string" || kind === "boolean") return;
|
|
151
|
+
if (kind === "number") {
|
|
152
|
+
if (!Number.isFinite(value)) throw new Error(`Non-finite number at ${path}`);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (Array.isArray(value)) {
|
|
156
|
+
value.forEach((item, index) => assertJsonCompatible(item, `${path}[${index}]`));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (kind === "object") {
|
|
160
|
+
for (const [key, entryValue] of Object.entries(value as Record<string, unknown>)) {
|
|
161
|
+
if (entryValue === undefined) throw new Error(`Undefined value at ${path}.${key}`);
|
|
162
|
+
assertJsonCompatible(entryValue, `${path}.${key}`);
|
|
163
|
+
}
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
throw new Error(`Unsupported JSON value at ${path}`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function canonicalizeJson(value: unknown): string {
|
|
170
|
+
assertJsonCompatible(value);
|
|
171
|
+
|
|
172
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
173
|
+
if (Array.isArray(value)) return `[${value.map((item) => canonicalizeJson(item)).join(",")}]`;
|
|
174
|
+
|
|
175
|
+
const entries = Object.entries(value)
|
|
176
|
+
.filter(([, entryValue]) => entryValue !== undefined)
|
|
177
|
+
.sort(([left], [right]) => left.localeCompare(right));
|
|
178
|
+
|
|
179
|
+
return `{${entries
|
|
180
|
+
.map(([key, entryValue]) => `${JSON.stringify(key)}:${canonicalizeJson(entryValue)}`)
|
|
181
|
+
.join(",")}}`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function utf8ToBytes(value: string): Uint8Array {
|
|
185
|
+
return new TextEncoder().encode(value);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function bytesToBinary(bytes: Uint8Array): string {
|
|
189
|
+
const chunkSize = 0x8000;
|
|
190
|
+
let binary = "";
|
|
191
|
+
for (let offset = 0; offset < bytes.length; offset += chunkSize) {
|
|
192
|
+
const chunk = bytes.slice(offset, offset + chunkSize);
|
|
193
|
+
binary += String.fromCharCode(...chunk);
|
|
194
|
+
}
|
|
195
|
+
return binary;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function bytesToBase64Url(bytes: Uint8Array): string {
|
|
199
|
+
return btoa(bytesToBinary(bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function base64UrlToBytes(value: string): Uint8Array {
|
|
203
|
+
const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
204
|
+
const padded = normalized.padEnd(normalized.length + ((4 - (normalized.length % 4)) % 4), "=");
|
|
205
|
+
const binary = atob(padded);
|
|
206
|
+
const bytes = new Uint8Array(binary.length);
|
|
207
|
+
for (let index = 0; index < binary.length; index++) bytes[index] = binary.charCodeAt(index);
|
|
208
|
+
return bytes;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function toArrayBuffer(bytes: Uint8Array): ArrayBuffer {
|
|
212
|
+
return (bytes.buffer as ArrayBuffer).slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function getCrypto(cryptoImpl?: Crypto): Crypto | null {
|
|
216
|
+
return cryptoImpl ?? globalThis.crypto ?? null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function sha256Bytes(value: string | Uint8Array, cryptoImpl?: Crypto): Promise<Uint8Array> {
|
|
220
|
+
const cryptoRuntime = getCrypto(cryptoImpl);
|
|
221
|
+
if (!cryptoRuntime?.subtle) throw new Error("crypto_unavailable");
|
|
222
|
+
const input = typeof value === "string" ? utf8ToBytes(value) : value;
|
|
223
|
+
const digest = await cryptoRuntime.subtle.digest("SHA-256", input as any);
|
|
224
|
+
return new Uint8Array(digest);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export async function sha256Hex(value: string | Uint8Array, cryptoImpl?: Crypto): Promise<string> {
|
|
228
|
+
const bytes = await sha256Bytes(value, cryptoImpl);
|
|
229
|
+
return [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function stripSignature(manifest: VivipilotRenderManifestV1 | UnsignedVivipilotRenderManifestV1): UnsignedVivipilotRenderManifestV1 {
|
|
233
|
+
const unsigned = { ...(manifest as VivipilotRenderManifestV1) };
|
|
234
|
+
delete (unsigned as Partial<VivipilotRenderManifestV1>).signature;
|
|
235
|
+
return unsigned;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function payloadForCanonicalHash(manifest: VivipilotRenderManifestV1 | UnsignedVivipilotRenderManifestV1): UnsignedVivipilotRenderManifestV1 {
|
|
239
|
+
const unsigned = stripSignature(manifest);
|
|
240
|
+
return {
|
|
241
|
+
...unsigned,
|
|
242
|
+
integrity: {
|
|
243
|
+
...unsigned.integrity,
|
|
244
|
+
canonicalPayloadHash: "",
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function canonicalizeManifestForSigning(manifest: VivipilotRenderManifestV1 | UnsignedVivipilotRenderManifestV1): string {
|
|
250
|
+
return canonicalizeJson(stripSignature(manifest));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export async function computeCanonicalPayloadHash(
|
|
254
|
+
manifest: VivipilotRenderManifestV1 | UnsignedVivipilotRenderManifestV1,
|
|
255
|
+
cryptoImpl?: Crypto,
|
|
256
|
+
): Promise<string> {
|
|
257
|
+
return sha256Hex(canonicalizeJson(payloadForCanonicalHash(manifest)), cryptoImpl);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export async function withCanonicalPayloadHash(
|
|
261
|
+
manifest: UnsignedVivipilotRenderManifestV1,
|
|
262
|
+
cryptoImpl?: Crypto,
|
|
263
|
+
): Promise<UnsignedVivipilotRenderManifestV1> {
|
|
264
|
+
const canonicalPayloadHash = await computeCanonicalPayloadHash(manifest, cryptoImpl);
|
|
265
|
+
return {
|
|
266
|
+
...manifest,
|
|
267
|
+
integrity: {
|
|
268
|
+
...manifest.integrity,
|
|
269
|
+
canonicalPayloadHash,
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function importPrivateKey(privateKey: CryptoKey | JsonWebKey | string, cryptoRuntime: Crypto): Promise<CryptoKey> {
|
|
275
|
+
if (privateKey instanceof CryptoKey) return privateKey;
|
|
276
|
+
if (typeof privateKey === "string") {
|
|
277
|
+
return cryptoRuntime.subtle.importKey(
|
|
278
|
+
"pkcs8",
|
|
279
|
+
toArrayBuffer(base64UrlToBytes(privateKey)),
|
|
280
|
+
ED25519_ALGORITHM,
|
|
281
|
+
false,
|
|
282
|
+
["sign"],
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
return cryptoRuntime.subtle.importKey("jwk", privateKey, ED25519_ALGORITHM, false, ["sign"]);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async function importPublicKey(publicKey: CryptoKey | JsonWebKey | string, cryptoRuntime: Crypto): Promise<CryptoKey> {
|
|
289
|
+
if (publicKey instanceof CryptoKey) return publicKey;
|
|
290
|
+
if (typeof publicKey === "string") {
|
|
291
|
+
return cryptoRuntime.subtle.importKey(
|
|
292
|
+
"raw",
|
|
293
|
+
toArrayBuffer(base64UrlToBytes(publicKey)),
|
|
294
|
+
ED25519_ALGORITHM,
|
|
295
|
+
false,
|
|
296
|
+
["verify"],
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
return cryptoRuntime.subtle.importKey("jwk", publicKey, ED25519_ALGORITHM, false, ["verify"]);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export async function signRenderManifest(
|
|
303
|
+
manifest: UnsignedVivipilotRenderManifestV1,
|
|
304
|
+
options: SignRenderManifestOptions,
|
|
305
|
+
): Promise<VivipilotRenderManifestV1> {
|
|
306
|
+
const cryptoRuntime = getCrypto(options.crypto);
|
|
307
|
+
if (!cryptoRuntime?.subtle) throw new Error("crypto_unavailable");
|
|
308
|
+
|
|
309
|
+
const preparedManifest = await withCanonicalPayloadHash(manifest, cryptoRuntime);
|
|
310
|
+
const signingPayload = utf8ToBytes(canonicalizeManifestForSigning(preparedManifest));
|
|
311
|
+
const privateKey = await importPrivateKey(options.privateKey, cryptoRuntime);
|
|
312
|
+
const signature = await cryptoRuntime.subtle.sign(ED25519_ALGORITHM, privateKey, signingPayload as any);
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
...preparedManifest,
|
|
316
|
+
signature: {
|
|
317
|
+
alg: RENDER_MANIFEST_SIGNATURE_ALG,
|
|
318
|
+
keyId: options.keyId,
|
|
319
|
+
value: bytesToBase64Url(new Uint8Array(signature)),
|
|
320
|
+
},
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function validateAssetShape(asset: unknown): boolean {
|
|
325
|
+
if (!isRecord(asset)) return false;
|
|
326
|
+
return isNonEmptyString(asset.assetId)
|
|
327
|
+
&& isNonEmptyString(asset.url)
|
|
328
|
+
&& isNonEmptyString(asset.sha256)
|
|
329
|
+
&& HEX_SHA256_PATTERN.test(asset.sha256)
|
|
330
|
+
&& isNonEmptyString(asset.mimeType)
|
|
331
|
+
&& isFinitePositiveNumber(asset.byteLength);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export function validateRenderManifestShape(value: unknown): RenderManifestVerificationResult {
|
|
335
|
+
if (!isRecord(value)) return fail("invalid_shape", "Manifest must be an object.");
|
|
336
|
+
if (value.schema !== RENDER_MANIFEST_SCHEMA_VERSION) return fail("invalid_shape", "Unsupported render manifest schema.");
|
|
337
|
+
if (!isNonEmptyString(value.manifestId)) return fail("invalid_shape", "manifestId is required.");
|
|
338
|
+
if (!isNonEmptyString(value.generationId)) return fail("invalid_shape", "generationId is required.");
|
|
339
|
+
if (!isNonEmptyString(value.userId)) return fail("invalid_shape", "userId is required.");
|
|
340
|
+
if (!isNonEmptyString(value.createdAt) || Number.isNaN(Date.parse(value.createdAt))) return fail("invalid_shape", "createdAt must be an ISO date string.");
|
|
341
|
+
if (value.expiresAt !== undefined && (!isNonEmptyString(value.expiresAt) || Number.isNaN(Date.parse(value.expiresAt)))) {
|
|
342
|
+
return fail("invalid_shape", "expiresAt must be an ISO date string when provided.");
|
|
343
|
+
}
|
|
344
|
+
if (!isNonEmptyString(value.engineVersion)) return fail("invalid_shape", "engineVersion is required.");
|
|
345
|
+
|
|
346
|
+
if (!isRecord(value.entitlement)) return fail("invalid_shape", "entitlement is required.");
|
|
347
|
+
if (!isRecord(value.billing)) return fail("invalid_shape", "billing is required.");
|
|
348
|
+
if (!isNonEmptyString(value.billing.creditTransactionId)) return fail("invalid_shape", "billing.creditTransactionId is required.");
|
|
349
|
+
if (!isFinitePositiveNumber(value.billing.creditsCharged)) return fail("invalid_shape", "billing.creditsCharged must be positive.");
|
|
350
|
+
if (!isNonEmptyString(value.billing.idempotencyKey)) return fail("invalid_shape", "billing.idempotencyKey is required.");
|
|
351
|
+
if (!isNonEmptyString(value.billing.creditSource)) return fail("invalid_shape", "billing.creditSource is required.");
|
|
352
|
+
|
|
353
|
+
if (!isRecord(value.render)) return fail("invalid_shape", "render payload is required.");
|
|
354
|
+
if (!isRecord(value.render.canvas)) return fail("invalid_shape", "render.canvas is required.");
|
|
355
|
+
if (!isFinitePositiveNumber(value.render.canvas.width) || !isFinitePositiveNumber(value.render.canvas.height) || !isFinitePositiveNumber(value.render.canvas.fps)) {
|
|
356
|
+
return fail("invalid_shape", "render.canvas dimensions and fps must be positive.");
|
|
357
|
+
}
|
|
358
|
+
if (!isFinitePositiveNumber(value.render.durationFrames)) return fail("invalid_shape", "render.durationFrames must be positive.");
|
|
359
|
+
if (!Array.isArray(value.render.overlays)) return fail("invalid_shape", "render.overlays must be an array.");
|
|
360
|
+
if (value.render.assets !== undefined && (!Array.isArray(value.render.assets) || !value.render.assets.every(validateAssetShape))) {
|
|
361
|
+
return fail("invalid_shape", "render.assets must contain immutable asset references with SHA-256 hashes.");
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (!isRecord(value.integrity)) return fail("invalid_shape", "integrity is required.");
|
|
365
|
+
if (!isNonEmptyString(value.integrity.canonicalPayloadHash) || !HEX_SHA256_PATTERN.test(value.integrity.canonicalPayloadHash)) {
|
|
366
|
+
return fail("invalid_shape", "integrity.canonicalPayloadHash must be a SHA-256 hex digest.");
|
|
367
|
+
}
|
|
368
|
+
if (!isNonEmptyString(value.integrity.sceneHash) || !HEX_SHA256_PATTERN.test(value.integrity.sceneHash)) {
|
|
369
|
+
return fail("invalid_shape", "integrity.sceneHash must be a SHA-256 hex digest.");
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (!isRecord(value.signature)) return fail("invalid_shape", "signature is required.");
|
|
373
|
+
if (value.signature.alg !== RENDER_MANIFEST_SIGNATURE_ALG) return fail("invalid_shape", "Unsupported signature algorithm.");
|
|
374
|
+
if (!isNonEmptyString(value.signature.keyId)) return fail("invalid_shape", "signature.keyId is required.");
|
|
375
|
+
if (!isNonEmptyString(value.signature.value)) return fail("invalid_shape", "signature.value is required.");
|
|
376
|
+
|
|
377
|
+
return {
|
|
378
|
+
ok: true,
|
|
379
|
+
manifest: value as VivipilotRenderManifestV1,
|
|
380
|
+
canonicalPayloadHash: value.integrity.canonicalPayloadHash,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
async function resolvePublicKey(keyId: string, resolver: PublicKeyResolver): Promise<string | undefined> {
|
|
385
|
+
if (typeof resolver === "function") return resolver(keyId);
|
|
386
|
+
if (Array.isArray(resolver)) return resolver.find((entry) => entry.keyId === keyId)?.publicKey;
|
|
387
|
+
return resolver[keyId];
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export function isManifestExpired(manifest: VivipilotRenderManifestV1, now = new Date()): boolean {
|
|
391
|
+
return typeof manifest.expiresAt === "string" && Date.parse(manifest.expiresAt) <= now.getTime();
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export async function verifyRenderManifest(
|
|
395
|
+
value: unknown,
|
|
396
|
+
options: VerifyRenderManifestOptions,
|
|
397
|
+
): Promise<RenderManifestVerificationResult> {
|
|
398
|
+
const shape = validateRenderManifestShape(value);
|
|
399
|
+
if (!shape.ok) return shape;
|
|
400
|
+
|
|
401
|
+
const manifest = shape.manifest;
|
|
402
|
+
const checkExpiry = options.checkExpiry ?? true;
|
|
403
|
+
if (checkExpiry && isManifestExpired(manifest, options.now ?? new Date())) {
|
|
404
|
+
return fail("expired", "Render manifest has expired.");
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
let canonicalPayloadHash: string;
|
|
408
|
+
try {
|
|
409
|
+
canonicalPayloadHash = await computeCanonicalPayloadHash(manifest, options.crypto);
|
|
410
|
+
} catch (error) {
|
|
411
|
+
return fail("crypto_unavailable", error instanceof Error ? error.message : "Unable to hash manifest payload.");
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (canonicalPayloadHash !== manifest.integrity.canonicalPayloadHash) {
|
|
415
|
+
return fail("invalid_canonical_payload_hash", "Manifest canonical payload hash does not match payload.");
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const publicKeyMaterial = await resolvePublicKey(manifest.signature.keyId, options.publicKeys);
|
|
419
|
+
if (!publicKeyMaterial) return fail("missing_public_key", `No public key registered for keyId ${manifest.signature.keyId}.`);
|
|
420
|
+
|
|
421
|
+
const cryptoRuntime = getCrypto(options.crypto);
|
|
422
|
+
if (!cryptoRuntime?.subtle) return fail("crypto_unavailable", "WebCrypto subtle crypto is unavailable.");
|
|
423
|
+
|
|
424
|
+
try {
|
|
425
|
+
const publicKey = await importPublicKey(publicKeyMaterial, cryptoRuntime);
|
|
426
|
+
const isValid = await cryptoRuntime.subtle.verify(
|
|
427
|
+
ED25519_ALGORITHM,
|
|
428
|
+
publicKey,
|
|
429
|
+
toArrayBuffer(base64UrlToBytes(manifest.signature.value)),
|
|
430
|
+
utf8ToBytes(canonicalizeManifestForSigning(manifest)) as any,
|
|
431
|
+
);
|
|
432
|
+
if (!isValid) return fail("invalid_signature", "Manifest signature is invalid.");
|
|
433
|
+
} catch (error) {
|
|
434
|
+
return fail("invalid_key", error instanceof Error ? error.message : "Unable to import or verify public key.");
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return { ok: true, manifest, canonicalPayloadHash };
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
export async function verifyAssetDigest(asset: RenderManifestAsset, bytes: Uint8Array, cryptoImpl?: Crypto): Promise<boolean> {
|
|
441
|
+
if (asset.byteLength !== bytes.byteLength) return false;
|
|
442
|
+
const digest = await sha256Hex(bytes, cryptoImpl);
|
|
443
|
+
return digest.toLowerCase() === asset.sha256.toLowerCase();
|
|
444
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"lib": ["ES2020", "DOM"],
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noImplicitAny": true,
|
|
9
|
+
"strictNullChecks": true,
|
|
10
|
+
"noUnusedLocals": true,
|
|
11
|
+
"noUnusedParameters": true,
|
|
12
|
+
"noImplicitReturns": true,
|
|
13
|
+
"noFallthroughCasesInSwitch": true,
|
|
14
|
+
"esModuleInterop": true,
|
|
15
|
+
"skipLibCheck": true,
|
|
16
|
+
"forceConsistentCasingInFileNames": true,
|
|
17
|
+
"declaration": true,
|
|
18
|
+
"declarationMap": true,
|
|
19
|
+
"sourceMap": true,
|
|
20
|
+
"outDir": "./dist",
|
|
21
|
+
"rootDir": "./src",
|
|
22
|
+
"isolatedModules": true
|
|
23
|
+
},
|
|
24
|
+
"include": ["src/**/*"],
|
|
25
|
+
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
|
|
26
|
+
}
|