@opendatalabs/vana-sdk 3.23.0-pr.211.1da7c1b → 3.23.0-pr.211.341f55d
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/crypto/envelope/job.cjs +47 -5
- package/dist/crypto/envelope/job.cjs.map +1 -1
- package/dist/crypto/envelope/job.d.ts +4 -3
- package/dist/crypto/envelope/job.js +47 -5
- package/dist/crypto/envelope/job.js.map +1 -1
- package/dist/index.browser.js +47 -5
- package/dist/index.browser.js.map +2 -2
- package/dist/index.node.cjs +47 -5
- package/dist/index.node.cjs.map +2 -2
- package/dist/index.node.js +47 -5
- package/dist/index.node.js.map +2 -2
- package/dist/protocol/jobs.cjs.map +1 -1
- package/dist/protocol/jobs.d.ts +2 -1
- package/dist/protocol/jobs.js.map +1 -1
- package/package.json +1 -1
|
@@ -34,6 +34,8 @@ var import_jobs = require("../../protocol/jobs");
|
|
|
34
34
|
var import_encoding = require("../../utils/encoding");
|
|
35
35
|
const textDecoder = new TextDecoder();
|
|
36
36
|
const textEncoder = new TextEncoder();
|
|
37
|
+
const MAX_JOB_RESULT_HEADER_BYTES = 4096;
|
|
38
|
+
const JOB_RESULT_LENGTH_PREFIX_BYTES = 4;
|
|
37
39
|
class JobEnvelopeError extends Error {
|
|
38
40
|
constructor(message) {
|
|
39
41
|
super(message);
|
|
@@ -66,16 +68,56 @@ function requestPlaintext(envelope) {
|
|
|
66
68
|
return canonicalJsonBytes(envelope);
|
|
67
69
|
}
|
|
68
70
|
function resultPlaintext(result) {
|
|
69
|
-
|
|
71
|
+
const header = textEncoder.encode(
|
|
70
72
|
JSON.stringify({
|
|
71
73
|
v: result.v,
|
|
72
74
|
jobId: result.jobId,
|
|
73
75
|
scope: result.scope,
|
|
74
76
|
version: result.version,
|
|
75
|
-
contentType: result.contentType
|
|
76
|
-
body: result.body
|
|
77
|
+
contentType: result.contentType
|
|
77
78
|
})
|
|
78
79
|
);
|
|
80
|
+
if (header.length > MAX_JOB_RESULT_HEADER_BYTES) {
|
|
81
|
+
throw new JobEnvelopeError(
|
|
82
|
+
`Invalid job result: header exceeds ${MAX_JOB_RESULT_HEADER_BYTES} bytes`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
const plaintext = new Uint8Array(
|
|
86
|
+
JOB_RESULT_LENGTH_PREFIX_BYTES + header.length + result.body.length
|
|
87
|
+
);
|
|
88
|
+
new DataView(plaintext.buffer).setUint32(0, header.length);
|
|
89
|
+
plaintext.set(header, JOB_RESULT_LENGTH_PREFIX_BYTES);
|
|
90
|
+
plaintext.set(result.body, JOB_RESULT_LENGTH_PREFIX_BYTES + header.length);
|
|
91
|
+
return plaintext;
|
|
92
|
+
}
|
|
93
|
+
function parseResultPlaintext(plaintext) {
|
|
94
|
+
if (plaintext.length < JOB_RESULT_LENGTH_PREFIX_BYTES) {
|
|
95
|
+
throw new JobEnvelopeError(
|
|
96
|
+
"Invalid job result: missing header length prefix"
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
const headerLength = new DataView(
|
|
100
|
+
plaintext.buffer,
|
|
101
|
+
plaintext.byteOffset,
|
|
102
|
+
JOB_RESULT_LENGTH_PREFIX_BYTES
|
|
103
|
+
).getUint32(0);
|
|
104
|
+
if (headerLength > MAX_JOB_RESULT_HEADER_BYTES) {
|
|
105
|
+
throw new JobEnvelopeError(
|
|
106
|
+
`Invalid job result: header exceeds ${MAX_JOB_RESULT_HEADER_BYTES} bytes`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
const remainingLength = plaintext.length - JOB_RESULT_LENGTH_PREFIX_BYTES;
|
|
110
|
+
if (headerLength > remainingLength) {
|
|
111
|
+
throw new JobEnvelopeError(
|
|
112
|
+
"Invalid job result: header length exceeds payload"
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
const headerEnd = JOB_RESULT_LENGTH_PREFIX_BYTES + headerLength;
|
|
116
|
+
const header = parseObject(
|
|
117
|
+
plaintext.subarray(JOB_RESULT_LENGTH_PREFIX_BYTES, headerEnd),
|
|
118
|
+
"job result header"
|
|
119
|
+
);
|
|
120
|
+
return validateResult({ ...header, body: plaintext.subarray(headerEnd) });
|
|
79
121
|
}
|
|
80
122
|
function encryptedBytesToBase64(encrypted) {
|
|
81
123
|
return (0, import_encoding.toBase64)((0, import_viem.fromHex)(`0x${(0, import_interface.serializeECIES)(encrypted)}`, "bytes"));
|
|
@@ -245,7 +287,7 @@ function validateResult(value) {
|
|
|
245
287
|
for (const field of ["jobId", "scope", "contentType"]) {
|
|
246
288
|
requireString(value[field], field, "job result");
|
|
247
289
|
}
|
|
248
|
-
if (
|
|
290
|
+
if (!(value.body instanceof Uint8Array)) {
|
|
249
291
|
throw new JobEnvelopeError("Invalid job result: missing body");
|
|
250
292
|
}
|
|
251
293
|
if (value.version !== null && typeof value.version !== "string") {
|
|
@@ -283,7 +325,7 @@ async function openJobResult(sealedBytes, builderPrivateKey, ecies, expect) {
|
|
|
283
325
|
(0, import_viem.fromHex)(builderPrivateKey, "bytes"),
|
|
284
326
|
bytesToEncrypted(sealedBytes)
|
|
285
327
|
);
|
|
286
|
-
const result =
|
|
328
|
+
const result = parseResultPlaintext(plaintext);
|
|
287
329
|
if (result.jobId !== expect.jobId) {
|
|
288
330
|
throw new JobEnvelopeError(
|
|
289
331
|
`Job result ID ${result.jobId} does not match expected job ID ${expect.jobId}`
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/crypto/envelope/job.ts"],"sourcesContent":["/**\n * ECIES envelopes for encrypted job requests and results.\n *\n * Request plaintext is UTF-8 JSON with object keys sorted recursively and\n * array order preserved. Result properties follow their interface order. The\n * Request ciphertext is base64 of `iv || ephemPub || ct || mac`, as specified\n * by the ECIES provider interface. Result ciphertext is the raw concatenated\n * byte sequence stored in object storage. The Gateway hashes raw ciphertext\n * bytes, not plaintext.\n * The builder verifies the decrypted result's job ID, scope, and version\n * bindings. The PS worker verifies\n * `auth.bodyHash === sha256(canonicalJobRequestBytes(request))`; the Gateway\n * never sees the plaintext. Flow: personal-server-ts\n * `docs/260903-jobs-contract.md`, section 1.\n *\n * @category Cryptography\n */\n\nimport { sha256 } from \"@noble/hashes/sha2\";\nimport { bytesToHex, fromHex, isAddress, isHex, toHex, type Hex } from \"viem\";\nimport { CURVE, FORMAT, MAC } from \"../ecies/constants\";\nimport {\n deserializeECIES,\n ECIESError,\n serializeECIES,\n type ECIESEncrypted,\n type ECIESProvider,\n} from \"../ecies/interface\";\nimport {\n JOB_OPERATIONS,\n JOB_PROTOCOL_VERSION,\n type JobOperation,\n type JobRequest,\n type JobRequestEnvelope,\n type JobResult,\n} from \"../../protocol/jobs\";\nimport { fromBase64, toBase64 } from \"../../utils/encoding\";\n\nconst textDecoder = new TextDecoder();\nconst textEncoder = new TextEncoder();\n\n/** A job envelope or result did not match the jobs protocol. */\nexport class JobEnvelopeError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"JobEnvelopeError\";\n }\n}\n\nfunction sortJsonKeys(value: unknown): unknown {\n if (Array.isArray(value)) {\n return value.map(sortJsonKeys);\n }\n if (value !== null && typeof value === \"object\") {\n return Object.fromEntries(\n Object.keys(value)\n .sort()\n .map((key) => [\n key,\n sortJsonKeys((value as Record<string, unknown>)[key]),\n ]),\n );\n }\n return value;\n}\n\nfunction canonicalJsonBytes(value: unknown): Uint8Array {\n return textEncoder.encode(JSON.stringify(sortJsonKeys(value)));\n}\n\n/**\n * Returns the canonical UTF-8 JSON bytes committed to by the auth body hash.\n *\n * @param request - Job request to validate and serialize canonically.\n * @returns Recursively key-sorted, whitespace-free UTF-8 JSON bytes.\n * @throws {JobEnvelopeError} If the request does not match the protocol schema.\n */\nexport function canonicalJobRequestBytes(request: JobRequest): Uint8Array {\n validateJobRequest(request);\n return canonicalJsonBytes(request);\n}\n\nfunction requestPlaintext(envelope: JobRequestEnvelope): Uint8Array {\n validateRequestEnvelope(envelope);\n return canonicalJsonBytes(envelope);\n}\n\nfunction resultPlaintext(result: JobResult): Uint8Array {\n return textEncoder.encode(\n JSON.stringify({\n v: result.v,\n jobId: result.jobId,\n scope: result.scope,\n version: result.version,\n contentType: result.contentType,\n body: result.body,\n }),\n );\n}\n\nfunction encryptedBytesToBase64(\n encrypted: Awaited<ReturnType<ECIESProvider[\"encrypt\"]>>,\n): string {\n return toBase64(fromHex(`0x${serializeECIES(encrypted)}`, \"bytes\"));\n}\n\nfunction encryptedToBytes(\n encrypted: Awaited<ReturnType<ECIESProvider[\"encrypt\"]>>,\n): Uint8Array {\n const bytes = new Uint8Array(\n encrypted.iv.length +\n encrypted.ephemPublicKey.length +\n encrypted.ciphertext.length +\n encrypted.mac.length,\n );\n let offset = 0;\n bytes.set(encrypted.iv, offset);\n offset += encrypted.iv.length;\n bytes.set(encrypted.ephemPublicKey, offset);\n offset += encrypted.ephemPublicKey.length;\n bytes.set(encrypted.ciphertext, offset);\n offset += encrypted.ciphertext.length;\n bytes.set(encrypted.mac, offset);\n return bytes;\n}\n\nfunction bytesToEncrypted(bytes: Uint8Array): ECIESEncrypted {\n const absoluteMinLength = FORMAT.IV_LENGTH + 1 + MAC.LENGTH + 1;\n if (bytes.length < absoluteMinLength) {\n throw new ECIESError(\n `Invalid ECIES data: too short (${bytes.length} bytes, minimum ${absoluteMinLength} bytes required)`,\n \"DECRYPTION_FAILED\",\n );\n }\n\n const prefix = bytes[FORMAT.EPHEMERAL_KEY_OFFSET];\n if (prefix !== CURVE.PREFIX.UNCOMPRESSED) {\n throw new ECIESError(\n `Invalid ephemeral public key: must be uncompressed format (0x04 prefix), got 0x${prefix.toString(16).padStart(2, \"0\")}`,\n \"DECRYPTION_FAILED\",\n );\n }\n\n const ephemKeySize = CURVE.UNCOMPRESSED_PUBLIC_KEY_LENGTH;\n const minLength = FORMAT.IV_LENGTH + ephemKeySize + MAC.LENGTH + 1;\n if (bytes.length < minLength) {\n throw new ECIESError(\n `Invalid ECIES data: too short (${bytes.length} bytes, minimum ${minLength} bytes required)`,\n \"DECRYPTION_FAILED\",\n );\n }\n\n return {\n iv: bytes.subarray(FORMAT.IV_OFFSET, FORMAT.IV_OFFSET + FORMAT.IV_LENGTH),\n ephemPublicKey: bytes.subarray(\n FORMAT.EPHEMERAL_KEY_OFFSET,\n FORMAT.EPHEMERAL_KEY_OFFSET + ephemKeySize,\n ),\n ciphertext: bytes.subarray(\n FORMAT.EPHEMERAL_KEY_OFFSET + ephemKeySize,\n bytes.length - MAC.LENGTH,\n ),\n mac: bytes.subarray(bytes.length - MAC.LENGTH),\n };\n}\n\nfunction base64ToEncrypted(ciphertext: string) {\n return deserializeECIES(toHex(fromBase64(ciphertext)));\n}\n\nfunction parseObject(\n plaintext: Uint8Array,\n kind: string,\n): Record<string, unknown> {\n try {\n const value: unknown = JSON.parse(textDecoder.decode(plaintext));\n if (value === null || typeof value !== \"object\" || Array.isArray(value)) {\n throw new JobEnvelopeError(`Invalid ${kind}: expected an object`);\n }\n return value as Record<string, unknown>;\n } catch (error) {\n if (error instanceof JobEnvelopeError) throw error;\n throw new JobEnvelopeError(`Invalid ${kind}: malformed JSON`);\n }\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n if (value === null || typeof value !== \"object\") return false;\n const prototype = Object.getPrototypeOf(value);\n return prototype === Object.prototype || prototype === null;\n}\n\nfunction requirePlainObject(\n value: unknown,\n field: string,\n kind: string,\n): asserts value is Record<string, unknown> {\n if (!isPlainObject(value)) {\n throw new JobEnvelopeError(`Invalid ${kind}: invalid ${field}`);\n }\n}\n\nfunction requireExactKeys(\n value: Record<string, unknown>,\n expectedKeys: readonly string[],\n kind: string,\n): void {\n for (const key of expectedKeys) {\n if (!Object.hasOwn(value, key) || value[key] === undefined) {\n throw new JobEnvelopeError(`Invalid ${kind}: missing ${key}`);\n }\n }\n const expected = new Set<PropertyKey>(expectedKeys);\n for (const key of Reflect.ownKeys(value)) {\n if (!expected.has(key)) {\n throw new JobEnvelopeError(\n `Invalid ${kind}: unknown field ${String(key)}`,\n );\n }\n }\n}\n\nfunction requireString(\n value: unknown,\n field: string,\n kind: string,\n): asserts value is string {\n if (typeof value !== \"string\" || value.length === 0) {\n throw new JobEnvelopeError(`Invalid ${kind}: missing ${field}`);\n }\n}\n\nfunction validateJobRequest(value: unknown): JobRequest {\n requirePlainObject(value, \"request\", \"job request\");\n requireExactKeys(\n value,\n [\n \"v\",\n \"jobId\",\n \"owner\",\n \"builder\",\n \"builderPublicKey\",\n \"grantId\",\n \"scope\",\n \"operation\",\n \"pinnedVersion\",\n \"deadline\",\n ],\n \"job request\",\n );\n if (value.v !== JOB_PROTOCOL_VERSION) {\n throw new JobEnvelopeError(\n `Unsupported job request version: ${String(value.v)}`,\n );\n }\n requireString(value.jobId, \"jobId\", \"job request\");\n if (typeof value.owner !== \"string\" || !isAddress(value.owner)) {\n throw new JobEnvelopeError(\"Invalid job request: invalid owner\");\n }\n if (typeof value.builder !== \"string\" || !isAddress(value.builder)) {\n throw new JobEnvelopeError(\"Invalid job request: invalid builder\");\n }\n if (\n typeof value.builderPublicKey !== \"string\" ||\n !isHex(value.builderPublicKey)\n ) {\n throw new JobEnvelopeError(\"Invalid job request: invalid builderPublicKey\");\n }\n if (typeof value.grantId !== \"string\" || !isHex(value.grantId)) {\n throw new JobEnvelopeError(\"Invalid job request: invalid grantId\");\n }\n requireString(value.scope, \"scope\", \"job request\");\n if (!JOB_OPERATIONS.includes(value.operation as JobOperation)) {\n throw new JobEnvelopeError(\"Invalid job request: invalid operation\");\n }\n if (value.pinnedVersion !== null && typeof value.pinnedVersion !== \"string\") {\n throw new JobEnvelopeError(\"Invalid job request: missing pinnedVersion\");\n }\n if (\n typeof value.deadline !== \"string\" ||\n !Number.isFinite(Date.parse(value.deadline))\n ) {\n throw new JobEnvelopeError(\"Invalid job request: invalid deadline\");\n }\n return value as unknown as JobRequest;\n}\n\nfunction validateRequestEnvelope(value: unknown): JobRequestEnvelope {\n requirePlainObject(value, \"envelope\", \"job request envelope\");\n requireExactKeys(value, [\"request\", \"auth\"], \"job request envelope\");\n validateJobRequest(value.request);\n requireString(value.auth, \"auth\", \"job request envelope\");\n return value as unknown as JobRequestEnvelope;\n}\n\nfunction validateResult(value: unknown): JobResult {\n requirePlainObject(value, \"result\", \"job result\");\n requireExactKeys(\n value,\n [\"v\", \"jobId\", \"scope\", \"version\", \"contentType\", \"body\"],\n \"job result\",\n );\n if (value.v !== JOB_PROTOCOL_VERSION) {\n throw new JobEnvelopeError(\n `Unsupported job result version: ${String(value.v)}`,\n );\n }\n for (const field of [\"jobId\", \"scope\", \"contentType\"]) {\n requireString(value[field], field, \"job result\");\n }\n if (typeof value.body !== \"string\") {\n throw new JobEnvelopeError(\"Invalid job result: missing body\");\n }\n if (value.version !== null && typeof value.version !== \"string\") {\n throw new JobEnvelopeError(\"Invalid job result: missing version\");\n }\n return value as unknown as JobResult;\n}\n\n/**\n * Encrypts a validated job request envelope for a Personal Server enclave.\n *\n * The PS worker verifies\n * `auth.bodyHash === sha256(canonicalJobRequestBytes(request))`; the Gateway\n * never sees the plaintext.\n *\n * @param envelope - Request and builder Web3Signed authorization to encrypt.\n * @param enclavePublicKey - Public key returned by `GET /v1/identity?owner=`.\n * @param ecies - Injected ECIES implementation.\n * @returns Base64 ciphertext encoded as `iv || ephemPub || ct || mac`.\n * @throws {JobEnvelopeError} If the envelope or request is invalid.\n * @throws If ECIES encryption fails or the enclave public key is invalid.\n *\n * @example\n * ```ts\n * const identity = await fetch(`/v1/identity?owner=${owner}`).then((response) =>\n * response.json(),\n * );\n * const requestCiphertext = await sealJobRequest(\n * requestEnvelope,\n * identity.publicKey,\n * ecies,\n * );\n * await fetch(\"/v1/jobs\", {\n * method: \"POST\",\n * body: JSON.stringify({ ...submission, requestCiphertext }),\n * });\n * ```\n */\nexport async function sealJobRequest(\n envelope: JobRequestEnvelope,\n enclavePublicKey: Hex,\n ecies: ECIESProvider,\n): Promise<string> {\n const encrypted = await ecies.encrypt(\n fromHex(enclavePublicKey, \"bytes\"),\n requestPlaintext(envelope),\n );\n return encryptedBytesToBase64(encrypted);\n}\n\n/**\n * Decrypts and validates a job request envelope inside the enclave.\n *\n * @param ciphertext - Base64 `iv || ephemPub || ct || mac` ciphertext.\n * @param privateKey - Enclave key bytes, supplied as `Uint8Array` so the agent can zero them after use.\n * @param ecies - Injected ECIES implementation.\n * @returns The validated request envelope exactly as parsed from plaintext.\n * @throws {JobEnvelopeError} If plaintext is malformed or fails schema validation.\n * @throws If ciphertext decoding or ECIES decryption fails.\n */\nexport async function openJobRequest(\n ciphertext: string,\n privateKey: Uint8Array,\n ecies: ECIESProvider,\n): Promise<JobRequestEnvelope> {\n const plaintext = await ecies.decrypt(\n privateKey,\n base64ToEncrypted(ciphertext),\n );\n return validateRequestEnvelope(\n parseObject(plaintext, \"job request envelope\"),\n );\n}\n\n/**\n * Encrypts a validated job result for its builder and describes the ciphertext.\n *\n * `hash` is lowercase `0x` SHA-256 of the raw sealed bytes, not the `sha256:`\n * prefix used by Web3Signed `bodyHash`. `size` is the sealed byte length. They\n * equal the Gateway's `resultHash` and `resultSize`.\n *\n * @param result - Job result to validate and encrypt.\n * @param builderPublicKey - Builder wallet public key recorded in the request.\n * @param ecies - Injected ECIES implementation.\n * @returns Raw sealed bytes plus their Gateway-compatible hash and size.\n * @throws {JobEnvelopeError} If the result does not match the protocol schema.\n * @throws If ECIES encryption fails or the builder public key is invalid.\n */\nexport async function sealJobResult(\n result: JobResult,\n builderPublicKey: Hex,\n ecies: ECIESProvider,\n): Promise<{ bytes: Uint8Array; hash: Hex; size: number }> {\n validateResult(result);\n const encrypted = await ecies.encrypt(\n fromHex(builderPublicKey, \"bytes\"),\n resultPlaintext(result),\n );\n const bytes = encryptedToBytes(encrypted);\n return { bytes, hash: bytesToHex(sha256(bytes)), size: bytes.length };\n}\n\n/**\n * Decrypts a job result and verifies its builder-visible protocol bindings.\n *\n * The builder private key is a `Hex` wallet key. For `expect.version`,\n * `undefined` skips the check while `null` requires a null result version.\n *\n * @param sealedBytes - Raw `iv || ephemPub || ct || mac` bytes.\n * @param builderPrivateKey - Builder's wallet private key as hex.\n * @param ecies - Injected ECIES implementation.\n * @param expect - Required job ID and optional scope and version bindings.\n * @returns The validated result when every supplied binding matches.\n * @throws {JobEnvelopeError} If plaintext is malformed, invalid, or a binding differs.\n * @throws If ciphertext decoding or ECIES decryption fails.\n *\n * @example\n * ```ts\n * const sealedBytes = new Uint8Array(await response.arrayBuffer());\n * const result = await openJobResult(sealedBytes, key, ecies, {\n * jobId,\n * scope,\n * });\n * const body = fromBase64(result.body);\n * ```\n */\nexport async function openJobResult(\n sealedBytes: Uint8Array,\n builderPrivateKey: Hex,\n ecies: ECIESProvider,\n expect: { jobId: string; scope?: string; version?: string | null },\n): Promise<JobResult> {\n const plaintext = await ecies.decrypt(\n fromHex(builderPrivateKey, \"bytes\"),\n bytesToEncrypted(sealedBytes),\n );\n const result = validateResult(parseObject(plaintext, \"job result\"));\n if (result.jobId !== expect.jobId) {\n throw new JobEnvelopeError(\n `Job result ID ${result.jobId} does not match expected job ID ${expect.jobId}`,\n );\n }\n if (expect.scope !== undefined && result.scope !== expect.scope) {\n throw new JobEnvelopeError(\n `Job result scope ${result.scope} does not match expected scope ${expect.scope}`,\n );\n }\n if (expect.version !== undefined && result.version !== expect.version) {\n throw new JobEnvelopeError(\n `Job result version ${String(result.version)} does not match expected version ${String(expect.version)}`,\n );\n }\n return result;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/crypto/envelope/job.ts"],"sourcesContent":["/**\n * ECIES envelopes for encrypted job requests and results.\n *\n * Request plaintext is UTF-8 JSON with object keys sorted recursively and\n * array order preserved. Result plaintext is\n * `uint32be(header length) || JSON header || raw body`. The Request ciphertext\n * is base64 of `iv || ephemPub || ct || mac`, as specified\n * by the ECIES provider interface. Result ciphertext is the raw concatenated\n * byte sequence stored in object storage. The Gateway hashes raw ciphertext\n * bytes, not plaintext.\n * The builder verifies the decrypted result's job ID, scope, and version\n * bindings. The PS worker verifies\n * `auth.bodyHash === sha256(canonicalJobRequestBytes(request))`; the Gateway\n * never sees the plaintext. Flow: personal-server-ts\n * `docs/260903-jobs-contract.md`, section 1.\n *\n * @category Cryptography\n */\n\nimport { sha256 } from \"@noble/hashes/sha2\";\nimport { bytesToHex, fromHex, isAddress, isHex, toHex, type Hex } from \"viem\";\nimport { CURVE, FORMAT, MAC } from \"../ecies/constants\";\nimport {\n deserializeECIES,\n ECIESError,\n serializeECIES,\n type ECIESEncrypted,\n type ECIESProvider,\n} from \"../ecies/interface\";\nimport {\n JOB_OPERATIONS,\n JOB_PROTOCOL_VERSION,\n type JobOperation,\n type JobRequest,\n type JobRequestEnvelope,\n type JobResult,\n} from \"../../protocol/jobs\";\nimport { fromBase64, toBase64 } from \"../../utils/encoding\";\n\nconst textDecoder = new TextDecoder();\nconst textEncoder = new TextEncoder();\n/** Result metadata is only a few short fields; cap it before decoding attacker-reachable bytes. */\nconst MAX_JOB_RESULT_HEADER_BYTES = 4096;\nconst JOB_RESULT_LENGTH_PREFIX_BYTES = 4;\n\n/** A job envelope or result did not match the jobs protocol. */\nexport class JobEnvelopeError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"JobEnvelopeError\";\n }\n}\n\nfunction sortJsonKeys(value: unknown): unknown {\n if (Array.isArray(value)) {\n return value.map(sortJsonKeys);\n }\n if (value !== null && typeof value === \"object\") {\n return Object.fromEntries(\n Object.keys(value)\n .sort()\n .map((key) => [\n key,\n sortJsonKeys((value as Record<string, unknown>)[key]),\n ]),\n );\n }\n return value;\n}\n\nfunction canonicalJsonBytes(value: unknown): Uint8Array {\n return textEncoder.encode(JSON.stringify(sortJsonKeys(value)));\n}\n\n/**\n * Returns the canonical UTF-8 JSON bytes committed to by the auth body hash.\n *\n * @param request - Job request to validate and serialize canonically.\n * @returns Recursively key-sorted, whitespace-free UTF-8 JSON bytes.\n * @throws {JobEnvelopeError} If the request does not match the protocol schema.\n */\nexport function canonicalJobRequestBytes(request: JobRequest): Uint8Array {\n validateJobRequest(request);\n return canonicalJsonBytes(request);\n}\n\nfunction requestPlaintext(envelope: JobRequestEnvelope): Uint8Array {\n validateRequestEnvelope(envelope);\n return canonicalJsonBytes(envelope);\n}\n\nfunction resultPlaintext(result: JobResult): Uint8Array {\n const header = textEncoder.encode(\n JSON.stringify({\n v: result.v,\n jobId: result.jobId,\n scope: result.scope,\n version: result.version,\n contentType: result.contentType,\n }),\n );\n if (header.length > MAX_JOB_RESULT_HEADER_BYTES) {\n throw new JobEnvelopeError(\n `Invalid job result: header exceeds ${MAX_JOB_RESULT_HEADER_BYTES} bytes`,\n );\n }\n\n const plaintext = new Uint8Array(\n JOB_RESULT_LENGTH_PREFIX_BYTES + header.length + result.body.length,\n );\n new DataView(plaintext.buffer).setUint32(0, header.length);\n plaintext.set(header, JOB_RESULT_LENGTH_PREFIX_BYTES);\n plaintext.set(result.body, JOB_RESULT_LENGTH_PREFIX_BYTES + header.length);\n return plaintext;\n}\n\nfunction parseResultPlaintext(plaintext: Uint8Array): JobResult {\n if (plaintext.length < JOB_RESULT_LENGTH_PREFIX_BYTES) {\n throw new JobEnvelopeError(\n \"Invalid job result: missing header length prefix\",\n );\n }\n\n const headerLength = new DataView(\n plaintext.buffer,\n plaintext.byteOffset,\n JOB_RESULT_LENGTH_PREFIX_BYTES,\n ).getUint32(0);\n if (headerLength > MAX_JOB_RESULT_HEADER_BYTES) {\n throw new JobEnvelopeError(\n `Invalid job result: header exceeds ${MAX_JOB_RESULT_HEADER_BYTES} bytes`,\n );\n }\n const remainingLength = plaintext.length - JOB_RESULT_LENGTH_PREFIX_BYTES;\n if (headerLength > remainingLength) {\n throw new JobEnvelopeError(\n \"Invalid job result: header length exceeds payload\",\n );\n }\n\n const headerEnd = JOB_RESULT_LENGTH_PREFIX_BYTES + headerLength;\n const header = parseObject(\n plaintext.subarray(JOB_RESULT_LENGTH_PREFIX_BYTES, headerEnd),\n \"job result header\",\n );\n return validateResult({ ...header, body: plaintext.subarray(headerEnd) });\n}\n\nfunction encryptedBytesToBase64(\n encrypted: Awaited<ReturnType<ECIESProvider[\"encrypt\"]>>,\n): string {\n return toBase64(fromHex(`0x${serializeECIES(encrypted)}`, \"bytes\"));\n}\n\nfunction encryptedToBytes(\n encrypted: Awaited<ReturnType<ECIESProvider[\"encrypt\"]>>,\n): Uint8Array {\n const bytes = new Uint8Array(\n encrypted.iv.length +\n encrypted.ephemPublicKey.length +\n encrypted.ciphertext.length +\n encrypted.mac.length,\n );\n let offset = 0;\n bytes.set(encrypted.iv, offset);\n offset += encrypted.iv.length;\n bytes.set(encrypted.ephemPublicKey, offset);\n offset += encrypted.ephemPublicKey.length;\n bytes.set(encrypted.ciphertext, offset);\n offset += encrypted.ciphertext.length;\n bytes.set(encrypted.mac, offset);\n return bytes;\n}\n\nfunction bytesToEncrypted(bytes: Uint8Array): ECIESEncrypted {\n const absoluteMinLength = FORMAT.IV_LENGTH + 1 + MAC.LENGTH + 1;\n if (bytes.length < absoluteMinLength) {\n throw new ECIESError(\n `Invalid ECIES data: too short (${bytes.length} bytes, minimum ${absoluteMinLength} bytes required)`,\n \"DECRYPTION_FAILED\",\n );\n }\n\n const prefix = bytes[FORMAT.EPHEMERAL_KEY_OFFSET];\n if (prefix !== CURVE.PREFIX.UNCOMPRESSED) {\n throw new ECIESError(\n `Invalid ephemeral public key: must be uncompressed format (0x04 prefix), got 0x${prefix.toString(16).padStart(2, \"0\")}`,\n \"DECRYPTION_FAILED\",\n );\n }\n\n const ephemKeySize = CURVE.UNCOMPRESSED_PUBLIC_KEY_LENGTH;\n const minLength = FORMAT.IV_LENGTH + ephemKeySize + MAC.LENGTH + 1;\n if (bytes.length < minLength) {\n throw new ECIESError(\n `Invalid ECIES data: too short (${bytes.length} bytes, minimum ${minLength} bytes required)`,\n \"DECRYPTION_FAILED\",\n );\n }\n\n return {\n iv: bytes.subarray(FORMAT.IV_OFFSET, FORMAT.IV_OFFSET + FORMAT.IV_LENGTH),\n ephemPublicKey: bytes.subarray(\n FORMAT.EPHEMERAL_KEY_OFFSET,\n FORMAT.EPHEMERAL_KEY_OFFSET + ephemKeySize,\n ),\n ciphertext: bytes.subarray(\n FORMAT.EPHEMERAL_KEY_OFFSET + ephemKeySize,\n bytes.length - MAC.LENGTH,\n ),\n mac: bytes.subarray(bytes.length - MAC.LENGTH),\n };\n}\n\nfunction base64ToEncrypted(ciphertext: string) {\n return deserializeECIES(toHex(fromBase64(ciphertext)));\n}\n\nfunction parseObject(\n plaintext: Uint8Array,\n kind: string,\n): Record<string, unknown> {\n try {\n const value: unknown = JSON.parse(textDecoder.decode(plaintext));\n if (value === null || typeof value !== \"object\" || Array.isArray(value)) {\n throw new JobEnvelopeError(`Invalid ${kind}: expected an object`);\n }\n return value as Record<string, unknown>;\n } catch (error) {\n if (error instanceof JobEnvelopeError) throw error;\n throw new JobEnvelopeError(`Invalid ${kind}: malformed JSON`);\n }\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n if (value === null || typeof value !== \"object\") return false;\n const prototype = Object.getPrototypeOf(value);\n return prototype === Object.prototype || prototype === null;\n}\n\nfunction requirePlainObject(\n value: unknown,\n field: string,\n kind: string,\n): asserts value is Record<string, unknown> {\n if (!isPlainObject(value)) {\n throw new JobEnvelopeError(`Invalid ${kind}: invalid ${field}`);\n }\n}\n\nfunction requireExactKeys(\n value: Record<string, unknown>,\n expectedKeys: readonly string[],\n kind: string,\n): void {\n for (const key of expectedKeys) {\n if (!Object.hasOwn(value, key) || value[key] === undefined) {\n throw new JobEnvelopeError(`Invalid ${kind}: missing ${key}`);\n }\n }\n const expected = new Set<PropertyKey>(expectedKeys);\n for (const key of Reflect.ownKeys(value)) {\n if (!expected.has(key)) {\n throw new JobEnvelopeError(\n `Invalid ${kind}: unknown field ${String(key)}`,\n );\n }\n }\n}\n\nfunction requireString(\n value: unknown,\n field: string,\n kind: string,\n): asserts value is string {\n if (typeof value !== \"string\" || value.length === 0) {\n throw new JobEnvelopeError(`Invalid ${kind}: missing ${field}`);\n }\n}\n\nfunction validateJobRequest(value: unknown): JobRequest {\n requirePlainObject(value, \"request\", \"job request\");\n requireExactKeys(\n value,\n [\n \"v\",\n \"jobId\",\n \"owner\",\n \"builder\",\n \"builderPublicKey\",\n \"grantId\",\n \"scope\",\n \"operation\",\n \"pinnedVersion\",\n \"deadline\",\n ],\n \"job request\",\n );\n if (value.v !== JOB_PROTOCOL_VERSION) {\n throw new JobEnvelopeError(\n `Unsupported job request version: ${String(value.v)}`,\n );\n }\n requireString(value.jobId, \"jobId\", \"job request\");\n if (typeof value.owner !== \"string\" || !isAddress(value.owner)) {\n throw new JobEnvelopeError(\"Invalid job request: invalid owner\");\n }\n if (typeof value.builder !== \"string\" || !isAddress(value.builder)) {\n throw new JobEnvelopeError(\"Invalid job request: invalid builder\");\n }\n if (\n typeof value.builderPublicKey !== \"string\" ||\n !isHex(value.builderPublicKey)\n ) {\n throw new JobEnvelopeError(\"Invalid job request: invalid builderPublicKey\");\n }\n if (typeof value.grantId !== \"string\" || !isHex(value.grantId)) {\n throw new JobEnvelopeError(\"Invalid job request: invalid grantId\");\n }\n requireString(value.scope, \"scope\", \"job request\");\n if (!JOB_OPERATIONS.includes(value.operation as JobOperation)) {\n throw new JobEnvelopeError(\"Invalid job request: invalid operation\");\n }\n if (value.pinnedVersion !== null && typeof value.pinnedVersion !== \"string\") {\n throw new JobEnvelopeError(\"Invalid job request: missing pinnedVersion\");\n }\n if (\n typeof value.deadline !== \"string\" ||\n !Number.isFinite(Date.parse(value.deadline))\n ) {\n throw new JobEnvelopeError(\"Invalid job request: invalid deadline\");\n }\n return value as unknown as JobRequest;\n}\n\nfunction validateRequestEnvelope(value: unknown): JobRequestEnvelope {\n requirePlainObject(value, \"envelope\", \"job request envelope\");\n requireExactKeys(value, [\"request\", \"auth\"], \"job request envelope\");\n validateJobRequest(value.request);\n requireString(value.auth, \"auth\", \"job request envelope\");\n return value as unknown as JobRequestEnvelope;\n}\n\nfunction validateResult(value: unknown): JobResult {\n requirePlainObject(value, \"result\", \"job result\");\n requireExactKeys(\n value,\n [\"v\", \"jobId\", \"scope\", \"version\", \"contentType\", \"body\"],\n \"job result\",\n );\n if (value.v !== JOB_PROTOCOL_VERSION) {\n throw new JobEnvelopeError(\n `Unsupported job result version: ${String(value.v)}`,\n );\n }\n for (const field of [\"jobId\", \"scope\", \"contentType\"]) {\n requireString(value[field], field, \"job result\");\n }\n if (!(value.body instanceof Uint8Array)) {\n throw new JobEnvelopeError(\"Invalid job result: missing body\");\n }\n if (value.version !== null && typeof value.version !== \"string\") {\n throw new JobEnvelopeError(\"Invalid job result: missing version\");\n }\n return value as unknown as JobResult;\n}\n\n/**\n * Encrypts a validated job request envelope for a Personal Server enclave.\n *\n * The PS worker verifies\n * `auth.bodyHash === sha256(canonicalJobRequestBytes(request))`; the Gateway\n * never sees the plaintext.\n *\n * @param envelope - Request and builder Web3Signed authorization to encrypt.\n * @param enclavePublicKey - Public key returned by `GET /v1/identity?owner=`.\n * @param ecies - Injected ECIES implementation.\n * @returns Base64 ciphertext encoded as `iv || ephemPub || ct || mac`.\n * @throws {JobEnvelopeError} If the envelope or request is invalid.\n * @throws If ECIES encryption fails or the enclave public key is invalid.\n *\n * @example\n * ```ts\n * const identity = await fetch(`/v1/identity?owner=${owner}`).then((response) =>\n * response.json(),\n * );\n * const requestCiphertext = await sealJobRequest(\n * requestEnvelope,\n * identity.publicKey,\n * ecies,\n * );\n * await fetch(\"/v1/jobs\", {\n * method: \"POST\",\n * body: JSON.stringify({ ...submission, requestCiphertext }),\n * });\n * ```\n */\nexport async function sealJobRequest(\n envelope: JobRequestEnvelope,\n enclavePublicKey: Hex,\n ecies: ECIESProvider,\n): Promise<string> {\n const encrypted = await ecies.encrypt(\n fromHex(enclavePublicKey, \"bytes\"),\n requestPlaintext(envelope),\n );\n return encryptedBytesToBase64(encrypted);\n}\n\n/**\n * Decrypts and validates a job request envelope inside the enclave.\n *\n * @param ciphertext - Base64 `iv || ephemPub || ct || mac` ciphertext.\n * @param privateKey - Enclave key bytes, supplied as `Uint8Array` so the agent can zero them after use.\n * @param ecies - Injected ECIES implementation.\n * @returns The validated request envelope exactly as parsed from plaintext.\n * @throws {JobEnvelopeError} If plaintext is malformed or fails schema validation.\n * @throws If ciphertext decoding or ECIES decryption fails.\n */\nexport async function openJobRequest(\n ciphertext: string,\n privateKey: Uint8Array,\n ecies: ECIESProvider,\n): Promise<JobRequestEnvelope> {\n const plaintext = await ecies.decrypt(\n privateKey,\n base64ToEncrypted(ciphertext),\n );\n return validateRequestEnvelope(\n parseObject(plaintext, \"job request envelope\"),\n );\n}\n\n/**\n * Encrypts a validated job result for its builder and describes the ciphertext.\n *\n * `hash` is lowercase `0x` SHA-256 of the raw sealed bytes, not the `sha256:`\n * prefix used by Web3Signed `bodyHash`. `size` is the sealed byte length. They\n * equal the Gateway's `resultHash` and `resultSize`.\n *\n * @param result - Job result to validate and encrypt.\n * @param builderPublicKey - Builder wallet public key recorded in the request.\n * @param ecies - Injected ECIES implementation.\n * @returns Raw sealed bytes plus their Gateway-compatible hash and size.\n * @throws {JobEnvelopeError} If the result does not match the protocol schema.\n * @throws If ECIES encryption fails or the builder public key is invalid.\n */\nexport async function sealJobResult(\n result: JobResult,\n builderPublicKey: Hex,\n ecies: ECIESProvider,\n): Promise<{ bytes: Uint8Array; hash: Hex; size: number }> {\n validateResult(result);\n const encrypted = await ecies.encrypt(\n fromHex(builderPublicKey, \"bytes\"),\n resultPlaintext(result),\n );\n const bytes = encryptedToBytes(encrypted);\n return { bytes, hash: bytesToHex(sha256(bytes)), size: bytes.length };\n}\n\n/**\n * Decrypts a job result and verifies its builder-visible protocol bindings.\n *\n * The builder private key is a `Hex` wallet key. For `expect.version`,\n * `undefined` skips the check while `null` requires a null result version.\n *\n * @param sealedBytes - Raw `iv || ephemPub || ct || mac` bytes.\n * @param builderPrivateKey - Builder's wallet private key as hex.\n * @param ecies - Injected ECIES implementation.\n * @param expect - Required job ID and optional scope and version bindings.\n * @returns The validated result when every supplied binding matches.\n * @throws {JobEnvelopeError} If plaintext is malformed, invalid, or a binding differs.\n * @throws If ciphertext decoding or ECIES decryption fails.\n *\n * @example\n * ```ts\n * const sealedBytes = new Uint8Array(await response.arrayBuffer());\n * const result = await openJobResult(sealedBytes, key, ecies, {\n * jobId,\n * scope,\n * });\n * const text = new TextDecoder().decode(result.body);\n * ```\n */\nexport async function openJobResult(\n sealedBytes: Uint8Array,\n builderPrivateKey: Hex,\n ecies: ECIESProvider,\n expect: { jobId: string; scope?: string; version?: string | null },\n): Promise<JobResult> {\n const plaintext = await ecies.decrypt(\n fromHex(builderPrivateKey, \"bytes\"),\n bytesToEncrypted(sealedBytes),\n );\n const result = parseResultPlaintext(plaintext);\n if (result.jobId !== expect.jobId) {\n throw new JobEnvelopeError(\n `Job result ID ${result.jobId} does not match expected job ID ${expect.jobId}`,\n );\n }\n if (expect.scope !== undefined && result.scope !== expect.scope) {\n throw new JobEnvelopeError(\n `Job result scope ${result.scope} does not match expected scope ${expect.scope}`,\n );\n }\n if (expect.version !== undefined && result.version !== expect.version) {\n throw new JobEnvelopeError(\n `Job result version ${String(result.version)} does not match expected version ${String(expect.version)}`,\n );\n }\n return result;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* ECIES envelopes for encrypted job requests and results.
|
|
3
3
|
*
|
|
4
4
|
* Request plaintext is UTF-8 JSON with object keys sorted recursively and
|
|
5
|
-
* array order preserved. Result
|
|
6
|
-
*
|
|
5
|
+
* array order preserved. Result plaintext is
|
|
6
|
+
* `uint32be(header length) || JSON header || raw body`. The Request ciphertext
|
|
7
|
+
* is base64 of `iv || ephemPub || ct || mac`, as specified
|
|
7
8
|
* by the ECIES provider interface. Result ciphertext is the raw concatenated
|
|
8
9
|
* byte sequence stored in object storage. The Gateway hashes raw ciphertext
|
|
9
10
|
* bytes, not plaintext.
|
|
@@ -112,7 +113,7 @@ export declare function sealJobResult(result: JobResult, builderPublicKey: Hex,
|
|
|
112
113
|
* jobId,
|
|
113
114
|
* scope,
|
|
114
115
|
* });
|
|
115
|
-
* const
|
|
116
|
+
* const text = new TextDecoder().decode(result.body);
|
|
116
117
|
* ```
|
|
117
118
|
*/
|
|
118
119
|
export declare function openJobResult(sealedBytes: Uint8Array, builderPrivateKey: Hex, ecies: ECIESProvider, expect: {
|
|
@@ -13,6 +13,8 @@ import {
|
|
|
13
13
|
import { fromBase64, toBase64 } from "../../utils/encoding.js";
|
|
14
14
|
const textDecoder = new TextDecoder();
|
|
15
15
|
const textEncoder = new TextEncoder();
|
|
16
|
+
const MAX_JOB_RESULT_HEADER_BYTES = 4096;
|
|
17
|
+
const JOB_RESULT_LENGTH_PREFIX_BYTES = 4;
|
|
16
18
|
class JobEnvelopeError extends Error {
|
|
17
19
|
constructor(message) {
|
|
18
20
|
super(message);
|
|
@@ -45,16 +47,56 @@ function requestPlaintext(envelope) {
|
|
|
45
47
|
return canonicalJsonBytes(envelope);
|
|
46
48
|
}
|
|
47
49
|
function resultPlaintext(result) {
|
|
48
|
-
|
|
50
|
+
const header = textEncoder.encode(
|
|
49
51
|
JSON.stringify({
|
|
50
52
|
v: result.v,
|
|
51
53
|
jobId: result.jobId,
|
|
52
54
|
scope: result.scope,
|
|
53
55
|
version: result.version,
|
|
54
|
-
contentType: result.contentType
|
|
55
|
-
body: result.body
|
|
56
|
+
contentType: result.contentType
|
|
56
57
|
})
|
|
57
58
|
);
|
|
59
|
+
if (header.length > MAX_JOB_RESULT_HEADER_BYTES) {
|
|
60
|
+
throw new JobEnvelopeError(
|
|
61
|
+
`Invalid job result: header exceeds ${MAX_JOB_RESULT_HEADER_BYTES} bytes`
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
const plaintext = new Uint8Array(
|
|
65
|
+
JOB_RESULT_LENGTH_PREFIX_BYTES + header.length + result.body.length
|
|
66
|
+
);
|
|
67
|
+
new DataView(plaintext.buffer).setUint32(0, header.length);
|
|
68
|
+
plaintext.set(header, JOB_RESULT_LENGTH_PREFIX_BYTES);
|
|
69
|
+
plaintext.set(result.body, JOB_RESULT_LENGTH_PREFIX_BYTES + header.length);
|
|
70
|
+
return plaintext;
|
|
71
|
+
}
|
|
72
|
+
function parseResultPlaintext(plaintext) {
|
|
73
|
+
if (plaintext.length < JOB_RESULT_LENGTH_PREFIX_BYTES) {
|
|
74
|
+
throw new JobEnvelopeError(
|
|
75
|
+
"Invalid job result: missing header length prefix"
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
const headerLength = new DataView(
|
|
79
|
+
plaintext.buffer,
|
|
80
|
+
plaintext.byteOffset,
|
|
81
|
+
JOB_RESULT_LENGTH_PREFIX_BYTES
|
|
82
|
+
).getUint32(0);
|
|
83
|
+
if (headerLength > MAX_JOB_RESULT_HEADER_BYTES) {
|
|
84
|
+
throw new JobEnvelopeError(
|
|
85
|
+
`Invalid job result: header exceeds ${MAX_JOB_RESULT_HEADER_BYTES} bytes`
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
const remainingLength = plaintext.length - JOB_RESULT_LENGTH_PREFIX_BYTES;
|
|
89
|
+
if (headerLength > remainingLength) {
|
|
90
|
+
throw new JobEnvelopeError(
|
|
91
|
+
"Invalid job result: header length exceeds payload"
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
const headerEnd = JOB_RESULT_LENGTH_PREFIX_BYTES + headerLength;
|
|
95
|
+
const header = parseObject(
|
|
96
|
+
plaintext.subarray(JOB_RESULT_LENGTH_PREFIX_BYTES, headerEnd),
|
|
97
|
+
"job result header"
|
|
98
|
+
);
|
|
99
|
+
return validateResult({ ...header, body: plaintext.subarray(headerEnd) });
|
|
58
100
|
}
|
|
59
101
|
function encryptedBytesToBase64(encrypted) {
|
|
60
102
|
return toBase64(fromHex(`0x${serializeECIES(encrypted)}`, "bytes"));
|
|
@@ -224,7 +266,7 @@ function validateResult(value) {
|
|
|
224
266
|
for (const field of ["jobId", "scope", "contentType"]) {
|
|
225
267
|
requireString(value[field], field, "job result");
|
|
226
268
|
}
|
|
227
|
-
if (
|
|
269
|
+
if (!(value.body instanceof Uint8Array)) {
|
|
228
270
|
throw new JobEnvelopeError("Invalid job result: missing body");
|
|
229
271
|
}
|
|
230
272
|
if (value.version !== null && typeof value.version !== "string") {
|
|
@@ -262,7 +304,7 @@ async function openJobResult(sealedBytes, builderPrivateKey, ecies, expect) {
|
|
|
262
304
|
fromHex(builderPrivateKey, "bytes"),
|
|
263
305
|
bytesToEncrypted(sealedBytes)
|
|
264
306
|
);
|
|
265
|
-
const result =
|
|
307
|
+
const result = parseResultPlaintext(plaintext);
|
|
266
308
|
if (result.jobId !== expect.jobId) {
|
|
267
309
|
throw new JobEnvelopeError(
|
|
268
310
|
`Job result ID ${result.jobId} does not match expected job ID ${expect.jobId}`
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/crypto/envelope/job.ts"],"sourcesContent":["/**\n * ECIES envelopes for encrypted job requests and results.\n *\n * Request plaintext is UTF-8 JSON with object keys sorted recursively and\n * array order preserved. Result properties follow their interface order. The\n * Request ciphertext is base64 of `iv || ephemPub || ct || mac`, as specified\n * by the ECIES provider interface. Result ciphertext is the raw concatenated\n * byte sequence stored in object storage. The Gateway hashes raw ciphertext\n * bytes, not plaintext.\n * The builder verifies the decrypted result's job ID, scope, and version\n * bindings. The PS worker verifies\n * `auth.bodyHash === sha256(canonicalJobRequestBytes(request))`; the Gateway\n * never sees the plaintext. Flow: personal-server-ts\n * `docs/260903-jobs-contract.md`, section 1.\n *\n * @category Cryptography\n */\n\nimport { sha256 } from \"@noble/hashes/sha2\";\nimport { bytesToHex, fromHex, isAddress, isHex, toHex, type Hex } from \"viem\";\nimport { CURVE, FORMAT, MAC } from \"../ecies/constants\";\nimport {\n deserializeECIES,\n ECIESError,\n serializeECIES,\n type ECIESEncrypted,\n type ECIESProvider,\n} from \"../ecies/interface\";\nimport {\n JOB_OPERATIONS,\n JOB_PROTOCOL_VERSION,\n type JobOperation,\n type JobRequest,\n type JobRequestEnvelope,\n type JobResult,\n} from \"../../protocol/jobs\";\nimport { fromBase64, toBase64 } from \"../../utils/encoding\";\n\nconst textDecoder = new TextDecoder();\nconst textEncoder = new TextEncoder();\n\n/** A job envelope or result did not match the jobs protocol. */\nexport class JobEnvelopeError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"JobEnvelopeError\";\n }\n}\n\nfunction sortJsonKeys(value: unknown): unknown {\n if (Array.isArray(value)) {\n return value.map(sortJsonKeys);\n }\n if (value !== null && typeof value === \"object\") {\n return Object.fromEntries(\n Object.keys(value)\n .sort()\n .map((key) => [\n key,\n sortJsonKeys((value as Record<string, unknown>)[key]),\n ]),\n );\n }\n return value;\n}\n\nfunction canonicalJsonBytes(value: unknown): Uint8Array {\n return textEncoder.encode(JSON.stringify(sortJsonKeys(value)));\n}\n\n/**\n * Returns the canonical UTF-8 JSON bytes committed to by the auth body hash.\n *\n * @param request - Job request to validate and serialize canonically.\n * @returns Recursively key-sorted, whitespace-free UTF-8 JSON bytes.\n * @throws {JobEnvelopeError} If the request does not match the protocol schema.\n */\nexport function canonicalJobRequestBytes(request: JobRequest): Uint8Array {\n validateJobRequest(request);\n return canonicalJsonBytes(request);\n}\n\nfunction requestPlaintext(envelope: JobRequestEnvelope): Uint8Array {\n validateRequestEnvelope(envelope);\n return canonicalJsonBytes(envelope);\n}\n\nfunction resultPlaintext(result: JobResult): Uint8Array {\n return textEncoder.encode(\n JSON.stringify({\n v: result.v,\n jobId: result.jobId,\n scope: result.scope,\n version: result.version,\n contentType: result.contentType,\n body: result.body,\n }),\n );\n}\n\nfunction encryptedBytesToBase64(\n encrypted: Awaited<ReturnType<ECIESProvider[\"encrypt\"]>>,\n): string {\n return toBase64(fromHex(`0x${serializeECIES(encrypted)}`, \"bytes\"));\n}\n\nfunction encryptedToBytes(\n encrypted: Awaited<ReturnType<ECIESProvider[\"encrypt\"]>>,\n): Uint8Array {\n const bytes = new Uint8Array(\n encrypted.iv.length +\n encrypted.ephemPublicKey.length +\n encrypted.ciphertext.length +\n encrypted.mac.length,\n );\n let offset = 0;\n bytes.set(encrypted.iv, offset);\n offset += encrypted.iv.length;\n bytes.set(encrypted.ephemPublicKey, offset);\n offset += encrypted.ephemPublicKey.length;\n bytes.set(encrypted.ciphertext, offset);\n offset += encrypted.ciphertext.length;\n bytes.set(encrypted.mac, offset);\n return bytes;\n}\n\nfunction bytesToEncrypted(bytes: Uint8Array): ECIESEncrypted {\n const absoluteMinLength = FORMAT.IV_LENGTH + 1 + MAC.LENGTH + 1;\n if (bytes.length < absoluteMinLength) {\n throw new ECIESError(\n `Invalid ECIES data: too short (${bytes.length} bytes, minimum ${absoluteMinLength} bytes required)`,\n \"DECRYPTION_FAILED\",\n );\n }\n\n const prefix = bytes[FORMAT.EPHEMERAL_KEY_OFFSET];\n if (prefix !== CURVE.PREFIX.UNCOMPRESSED) {\n throw new ECIESError(\n `Invalid ephemeral public key: must be uncompressed format (0x04 prefix), got 0x${prefix.toString(16).padStart(2, \"0\")}`,\n \"DECRYPTION_FAILED\",\n );\n }\n\n const ephemKeySize = CURVE.UNCOMPRESSED_PUBLIC_KEY_LENGTH;\n const minLength = FORMAT.IV_LENGTH + ephemKeySize + MAC.LENGTH + 1;\n if (bytes.length < minLength) {\n throw new ECIESError(\n `Invalid ECIES data: too short (${bytes.length} bytes, minimum ${minLength} bytes required)`,\n \"DECRYPTION_FAILED\",\n );\n }\n\n return {\n iv: bytes.subarray(FORMAT.IV_OFFSET, FORMAT.IV_OFFSET + FORMAT.IV_LENGTH),\n ephemPublicKey: bytes.subarray(\n FORMAT.EPHEMERAL_KEY_OFFSET,\n FORMAT.EPHEMERAL_KEY_OFFSET + ephemKeySize,\n ),\n ciphertext: bytes.subarray(\n FORMAT.EPHEMERAL_KEY_OFFSET + ephemKeySize,\n bytes.length - MAC.LENGTH,\n ),\n mac: bytes.subarray(bytes.length - MAC.LENGTH),\n };\n}\n\nfunction base64ToEncrypted(ciphertext: string) {\n return deserializeECIES(toHex(fromBase64(ciphertext)));\n}\n\nfunction parseObject(\n plaintext: Uint8Array,\n kind: string,\n): Record<string, unknown> {\n try {\n const value: unknown = JSON.parse(textDecoder.decode(plaintext));\n if (value === null || typeof value !== \"object\" || Array.isArray(value)) {\n throw new JobEnvelopeError(`Invalid ${kind}: expected an object`);\n }\n return value as Record<string, unknown>;\n } catch (error) {\n if (error instanceof JobEnvelopeError) throw error;\n throw new JobEnvelopeError(`Invalid ${kind}: malformed JSON`);\n }\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n if (value === null || typeof value !== \"object\") return false;\n const prototype = Object.getPrototypeOf(value);\n return prototype === Object.prototype || prototype === null;\n}\n\nfunction requirePlainObject(\n value: unknown,\n field: string,\n kind: string,\n): asserts value is Record<string, unknown> {\n if (!isPlainObject(value)) {\n throw new JobEnvelopeError(`Invalid ${kind}: invalid ${field}`);\n }\n}\n\nfunction requireExactKeys(\n value: Record<string, unknown>,\n expectedKeys: readonly string[],\n kind: string,\n): void {\n for (const key of expectedKeys) {\n if (!Object.hasOwn(value, key) || value[key] === undefined) {\n throw new JobEnvelopeError(`Invalid ${kind}: missing ${key}`);\n }\n }\n const expected = new Set<PropertyKey>(expectedKeys);\n for (const key of Reflect.ownKeys(value)) {\n if (!expected.has(key)) {\n throw new JobEnvelopeError(\n `Invalid ${kind}: unknown field ${String(key)}`,\n );\n }\n }\n}\n\nfunction requireString(\n value: unknown,\n field: string,\n kind: string,\n): asserts value is string {\n if (typeof value !== \"string\" || value.length === 0) {\n throw new JobEnvelopeError(`Invalid ${kind}: missing ${field}`);\n }\n}\n\nfunction validateJobRequest(value: unknown): JobRequest {\n requirePlainObject(value, \"request\", \"job request\");\n requireExactKeys(\n value,\n [\n \"v\",\n \"jobId\",\n \"owner\",\n \"builder\",\n \"builderPublicKey\",\n \"grantId\",\n \"scope\",\n \"operation\",\n \"pinnedVersion\",\n \"deadline\",\n ],\n \"job request\",\n );\n if (value.v !== JOB_PROTOCOL_VERSION) {\n throw new JobEnvelopeError(\n `Unsupported job request version: ${String(value.v)}`,\n );\n }\n requireString(value.jobId, \"jobId\", \"job request\");\n if (typeof value.owner !== \"string\" || !isAddress(value.owner)) {\n throw new JobEnvelopeError(\"Invalid job request: invalid owner\");\n }\n if (typeof value.builder !== \"string\" || !isAddress(value.builder)) {\n throw new JobEnvelopeError(\"Invalid job request: invalid builder\");\n }\n if (\n typeof value.builderPublicKey !== \"string\" ||\n !isHex(value.builderPublicKey)\n ) {\n throw new JobEnvelopeError(\"Invalid job request: invalid builderPublicKey\");\n }\n if (typeof value.grantId !== \"string\" || !isHex(value.grantId)) {\n throw new JobEnvelopeError(\"Invalid job request: invalid grantId\");\n }\n requireString(value.scope, \"scope\", \"job request\");\n if (!JOB_OPERATIONS.includes(value.operation as JobOperation)) {\n throw new JobEnvelopeError(\"Invalid job request: invalid operation\");\n }\n if (value.pinnedVersion !== null && typeof value.pinnedVersion !== \"string\") {\n throw new JobEnvelopeError(\"Invalid job request: missing pinnedVersion\");\n }\n if (\n typeof value.deadline !== \"string\" ||\n !Number.isFinite(Date.parse(value.deadline))\n ) {\n throw new JobEnvelopeError(\"Invalid job request: invalid deadline\");\n }\n return value as unknown as JobRequest;\n}\n\nfunction validateRequestEnvelope(value: unknown): JobRequestEnvelope {\n requirePlainObject(value, \"envelope\", \"job request envelope\");\n requireExactKeys(value, [\"request\", \"auth\"], \"job request envelope\");\n validateJobRequest(value.request);\n requireString(value.auth, \"auth\", \"job request envelope\");\n return value as unknown as JobRequestEnvelope;\n}\n\nfunction validateResult(value: unknown): JobResult {\n requirePlainObject(value, \"result\", \"job result\");\n requireExactKeys(\n value,\n [\"v\", \"jobId\", \"scope\", \"version\", \"contentType\", \"body\"],\n \"job result\",\n );\n if (value.v !== JOB_PROTOCOL_VERSION) {\n throw new JobEnvelopeError(\n `Unsupported job result version: ${String(value.v)}`,\n );\n }\n for (const field of [\"jobId\", \"scope\", \"contentType\"]) {\n requireString(value[field], field, \"job result\");\n }\n if (typeof value.body !== \"string\") {\n throw new JobEnvelopeError(\"Invalid job result: missing body\");\n }\n if (value.version !== null && typeof value.version !== \"string\") {\n throw new JobEnvelopeError(\"Invalid job result: missing version\");\n }\n return value as unknown as JobResult;\n}\n\n/**\n * Encrypts a validated job request envelope for a Personal Server enclave.\n *\n * The PS worker verifies\n * `auth.bodyHash === sha256(canonicalJobRequestBytes(request))`; the Gateway\n * never sees the plaintext.\n *\n * @param envelope - Request and builder Web3Signed authorization to encrypt.\n * @param enclavePublicKey - Public key returned by `GET /v1/identity?owner=`.\n * @param ecies - Injected ECIES implementation.\n * @returns Base64 ciphertext encoded as `iv || ephemPub || ct || mac`.\n * @throws {JobEnvelopeError} If the envelope or request is invalid.\n * @throws If ECIES encryption fails or the enclave public key is invalid.\n *\n * @example\n * ```ts\n * const identity = await fetch(`/v1/identity?owner=${owner}`).then((response) =>\n * response.json(),\n * );\n * const requestCiphertext = await sealJobRequest(\n * requestEnvelope,\n * identity.publicKey,\n * ecies,\n * );\n * await fetch(\"/v1/jobs\", {\n * method: \"POST\",\n * body: JSON.stringify({ ...submission, requestCiphertext }),\n * });\n * ```\n */\nexport async function sealJobRequest(\n envelope: JobRequestEnvelope,\n enclavePublicKey: Hex,\n ecies: ECIESProvider,\n): Promise<string> {\n const encrypted = await ecies.encrypt(\n fromHex(enclavePublicKey, \"bytes\"),\n requestPlaintext(envelope),\n );\n return encryptedBytesToBase64(encrypted);\n}\n\n/**\n * Decrypts and validates a job request envelope inside the enclave.\n *\n * @param ciphertext - Base64 `iv || ephemPub || ct || mac` ciphertext.\n * @param privateKey - Enclave key bytes, supplied as `Uint8Array` so the agent can zero them after use.\n * @param ecies - Injected ECIES implementation.\n * @returns The validated request envelope exactly as parsed from plaintext.\n * @throws {JobEnvelopeError} If plaintext is malformed or fails schema validation.\n * @throws If ciphertext decoding or ECIES decryption fails.\n */\nexport async function openJobRequest(\n ciphertext: string,\n privateKey: Uint8Array,\n ecies: ECIESProvider,\n): Promise<JobRequestEnvelope> {\n const plaintext = await ecies.decrypt(\n privateKey,\n base64ToEncrypted(ciphertext),\n );\n return validateRequestEnvelope(\n parseObject(plaintext, \"job request envelope\"),\n );\n}\n\n/**\n * Encrypts a validated job result for its builder and describes the ciphertext.\n *\n * `hash` is lowercase `0x` SHA-256 of the raw sealed bytes, not the `sha256:`\n * prefix used by Web3Signed `bodyHash`. `size` is the sealed byte length. They\n * equal the Gateway's `resultHash` and `resultSize`.\n *\n * @param result - Job result to validate and encrypt.\n * @param builderPublicKey - Builder wallet public key recorded in the request.\n * @param ecies - Injected ECIES implementation.\n * @returns Raw sealed bytes plus their Gateway-compatible hash and size.\n * @throws {JobEnvelopeError} If the result does not match the protocol schema.\n * @throws If ECIES encryption fails or the builder public key is invalid.\n */\nexport async function sealJobResult(\n result: JobResult,\n builderPublicKey: Hex,\n ecies: ECIESProvider,\n): Promise<{ bytes: Uint8Array; hash: Hex; size: number }> {\n validateResult(result);\n const encrypted = await ecies.encrypt(\n fromHex(builderPublicKey, \"bytes\"),\n resultPlaintext(result),\n );\n const bytes = encryptedToBytes(encrypted);\n return { bytes, hash: bytesToHex(sha256(bytes)), size: bytes.length };\n}\n\n/**\n * Decrypts a job result and verifies its builder-visible protocol bindings.\n *\n * The builder private key is a `Hex` wallet key. For `expect.version`,\n * `undefined` skips the check while `null` requires a null result version.\n *\n * @param sealedBytes - Raw `iv || ephemPub || ct || mac` bytes.\n * @param builderPrivateKey - Builder's wallet private key as hex.\n * @param ecies - Injected ECIES implementation.\n * @param expect - Required job ID and optional scope and version bindings.\n * @returns The validated result when every supplied binding matches.\n * @throws {JobEnvelopeError} If plaintext is malformed, invalid, or a binding differs.\n * @throws If ciphertext decoding or ECIES decryption fails.\n *\n * @example\n * ```ts\n * const sealedBytes = new Uint8Array(await response.arrayBuffer());\n * const result = await openJobResult(sealedBytes, key, ecies, {\n * jobId,\n * scope,\n * });\n * const body = fromBase64(result.body);\n * ```\n */\nexport async function openJobResult(\n sealedBytes: Uint8Array,\n builderPrivateKey: Hex,\n ecies: ECIESProvider,\n expect: { jobId: string; scope?: string; version?: string | null },\n): Promise<JobResult> {\n const plaintext = await ecies.decrypt(\n fromHex(builderPrivateKey, \"bytes\"),\n bytesToEncrypted(sealedBytes),\n );\n const result = validateResult(parseObject(plaintext, \"job result\"));\n if (result.jobId !== expect.jobId) {\n throw new JobEnvelopeError(\n `Job result ID ${result.jobId} does not match expected job ID ${expect.jobId}`,\n );\n }\n if (expect.scope !== undefined && result.scope !== expect.scope) {\n throw new JobEnvelopeError(\n `Job result scope ${result.scope} does not match expected scope ${expect.scope}`,\n );\n }\n if (expect.version !== undefined && result.version !== expect.version) {\n throw new JobEnvelopeError(\n `Job result version ${String(result.version)} does not match expected version ${String(expect.version)}`,\n );\n }\n return result;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/crypto/envelope/job.ts"],"sourcesContent":["/**\n * ECIES envelopes for encrypted job requests and results.\n *\n * Request plaintext is UTF-8 JSON with object keys sorted recursively and\n * array order preserved. Result plaintext is\n * `uint32be(header length) || JSON header || raw body`. The Request ciphertext\n * is base64 of `iv || ephemPub || ct || mac`, as specified\n * by the ECIES provider interface. Result ciphertext is the raw concatenated\n * byte sequence stored in object storage. The Gateway hashes raw ciphertext\n * bytes, not plaintext.\n * The builder verifies the decrypted result's job ID, scope, and version\n * bindings. The PS worker verifies\n * `auth.bodyHash === sha256(canonicalJobRequestBytes(request))`; the Gateway\n * never sees the plaintext. Flow: personal-server-ts\n * `docs/260903-jobs-contract.md`, section 1.\n *\n * @category Cryptography\n */\n\nimport { sha256 } from \"@noble/hashes/sha2\";\nimport { bytesToHex, fromHex, isAddress, isHex, toHex, type Hex } from \"viem\";\nimport { CURVE, FORMAT, MAC } from \"../ecies/constants\";\nimport {\n deserializeECIES,\n ECIESError,\n serializeECIES,\n type ECIESEncrypted,\n type ECIESProvider,\n} from \"../ecies/interface\";\nimport {\n JOB_OPERATIONS,\n JOB_PROTOCOL_VERSION,\n type JobOperation,\n type JobRequest,\n type JobRequestEnvelope,\n type JobResult,\n} from \"../../protocol/jobs\";\nimport { fromBase64, toBase64 } from \"../../utils/encoding\";\n\nconst textDecoder = new TextDecoder();\nconst textEncoder = new TextEncoder();\n/** Result metadata is only a few short fields; cap it before decoding attacker-reachable bytes. */\nconst MAX_JOB_RESULT_HEADER_BYTES = 4096;\nconst JOB_RESULT_LENGTH_PREFIX_BYTES = 4;\n\n/** A job envelope or result did not match the jobs protocol. */\nexport class JobEnvelopeError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"JobEnvelopeError\";\n }\n}\n\nfunction sortJsonKeys(value: unknown): unknown {\n if (Array.isArray(value)) {\n return value.map(sortJsonKeys);\n }\n if (value !== null && typeof value === \"object\") {\n return Object.fromEntries(\n Object.keys(value)\n .sort()\n .map((key) => [\n key,\n sortJsonKeys((value as Record<string, unknown>)[key]),\n ]),\n );\n }\n return value;\n}\n\nfunction canonicalJsonBytes(value: unknown): Uint8Array {\n return textEncoder.encode(JSON.stringify(sortJsonKeys(value)));\n}\n\n/**\n * Returns the canonical UTF-8 JSON bytes committed to by the auth body hash.\n *\n * @param request - Job request to validate and serialize canonically.\n * @returns Recursively key-sorted, whitespace-free UTF-8 JSON bytes.\n * @throws {JobEnvelopeError} If the request does not match the protocol schema.\n */\nexport function canonicalJobRequestBytes(request: JobRequest): Uint8Array {\n validateJobRequest(request);\n return canonicalJsonBytes(request);\n}\n\nfunction requestPlaintext(envelope: JobRequestEnvelope): Uint8Array {\n validateRequestEnvelope(envelope);\n return canonicalJsonBytes(envelope);\n}\n\nfunction resultPlaintext(result: JobResult): Uint8Array {\n const header = textEncoder.encode(\n JSON.stringify({\n v: result.v,\n jobId: result.jobId,\n scope: result.scope,\n version: result.version,\n contentType: result.contentType,\n }),\n );\n if (header.length > MAX_JOB_RESULT_HEADER_BYTES) {\n throw new JobEnvelopeError(\n `Invalid job result: header exceeds ${MAX_JOB_RESULT_HEADER_BYTES} bytes`,\n );\n }\n\n const plaintext = new Uint8Array(\n JOB_RESULT_LENGTH_PREFIX_BYTES + header.length + result.body.length,\n );\n new DataView(plaintext.buffer).setUint32(0, header.length);\n plaintext.set(header, JOB_RESULT_LENGTH_PREFIX_BYTES);\n plaintext.set(result.body, JOB_RESULT_LENGTH_PREFIX_BYTES + header.length);\n return plaintext;\n}\n\nfunction parseResultPlaintext(plaintext: Uint8Array): JobResult {\n if (plaintext.length < JOB_RESULT_LENGTH_PREFIX_BYTES) {\n throw new JobEnvelopeError(\n \"Invalid job result: missing header length prefix\",\n );\n }\n\n const headerLength = new DataView(\n plaintext.buffer,\n plaintext.byteOffset,\n JOB_RESULT_LENGTH_PREFIX_BYTES,\n ).getUint32(0);\n if (headerLength > MAX_JOB_RESULT_HEADER_BYTES) {\n throw new JobEnvelopeError(\n `Invalid job result: header exceeds ${MAX_JOB_RESULT_HEADER_BYTES} bytes`,\n );\n }\n const remainingLength = plaintext.length - JOB_RESULT_LENGTH_PREFIX_BYTES;\n if (headerLength > remainingLength) {\n throw new JobEnvelopeError(\n \"Invalid job result: header length exceeds payload\",\n );\n }\n\n const headerEnd = JOB_RESULT_LENGTH_PREFIX_BYTES + headerLength;\n const header = parseObject(\n plaintext.subarray(JOB_RESULT_LENGTH_PREFIX_BYTES, headerEnd),\n \"job result header\",\n );\n return validateResult({ ...header, body: plaintext.subarray(headerEnd) });\n}\n\nfunction encryptedBytesToBase64(\n encrypted: Awaited<ReturnType<ECIESProvider[\"encrypt\"]>>,\n): string {\n return toBase64(fromHex(`0x${serializeECIES(encrypted)}`, \"bytes\"));\n}\n\nfunction encryptedToBytes(\n encrypted: Awaited<ReturnType<ECIESProvider[\"encrypt\"]>>,\n): Uint8Array {\n const bytes = new Uint8Array(\n encrypted.iv.length +\n encrypted.ephemPublicKey.length +\n encrypted.ciphertext.length +\n encrypted.mac.length,\n );\n let offset = 0;\n bytes.set(encrypted.iv, offset);\n offset += encrypted.iv.length;\n bytes.set(encrypted.ephemPublicKey, offset);\n offset += encrypted.ephemPublicKey.length;\n bytes.set(encrypted.ciphertext, offset);\n offset += encrypted.ciphertext.length;\n bytes.set(encrypted.mac, offset);\n return bytes;\n}\n\nfunction bytesToEncrypted(bytes: Uint8Array): ECIESEncrypted {\n const absoluteMinLength = FORMAT.IV_LENGTH + 1 + MAC.LENGTH + 1;\n if (bytes.length < absoluteMinLength) {\n throw new ECIESError(\n `Invalid ECIES data: too short (${bytes.length} bytes, minimum ${absoluteMinLength} bytes required)`,\n \"DECRYPTION_FAILED\",\n );\n }\n\n const prefix = bytes[FORMAT.EPHEMERAL_KEY_OFFSET];\n if (prefix !== CURVE.PREFIX.UNCOMPRESSED) {\n throw new ECIESError(\n `Invalid ephemeral public key: must be uncompressed format (0x04 prefix), got 0x${prefix.toString(16).padStart(2, \"0\")}`,\n \"DECRYPTION_FAILED\",\n );\n }\n\n const ephemKeySize = CURVE.UNCOMPRESSED_PUBLIC_KEY_LENGTH;\n const minLength = FORMAT.IV_LENGTH + ephemKeySize + MAC.LENGTH + 1;\n if (bytes.length < minLength) {\n throw new ECIESError(\n `Invalid ECIES data: too short (${bytes.length} bytes, minimum ${minLength} bytes required)`,\n \"DECRYPTION_FAILED\",\n );\n }\n\n return {\n iv: bytes.subarray(FORMAT.IV_OFFSET, FORMAT.IV_OFFSET + FORMAT.IV_LENGTH),\n ephemPublicKey: bytes.subarray(\n FORMAT.EPHEMERAL_KEY_OFFSET,\n FORMAT.EPHEMERAL_KEY_OFFSET + ephemKeySize,\n ),\n ciphertext: bytes.subarray(\n FORMAT.EPHEMERAL_KEY_OFFSET + ephemKeySize,\n bytes.length - MAC.LENGTH,\n ),\n mac: bytes.subarray(bytes.length - MAC.LENGTH),\n };\n}\n\nfunction base64ToEncrypted(ciphertext: string) {\n return deserializeECIES(toHex(fromBase64(ciphertext)));\n}\n\nfunction parseObject(\n plaintext: Uint8Array,\n kind: string,\n): Record<string, unknown> {\n try {\n const value: unknown = JSON.parse(textDecoder.decode(plaintext));\n if (value === null || typeof value !== \"object\" || Array.isArray(value)) {\n throw new JobEnvelopeError(`Invalid ${kind}: expected an object`);\n }\n return value as Record<string, unknown>;\n } catch (error) {\n if (error instanceof JobEnvelopeError) throw error;\n throw new JobEnvelopeError(`Invalid ${kind}: malformed JSON`);\n }\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n if (value === null || typeof value !== \"object\") return false;\n const prototype = Object.getPrototypeOf(value);\n return prototype === Object.prototype || prototype === null;\n}\n\nfunction requirePlainObject(\n value: unknown,\n field: string,\n kind: string,\n): asserts value is Record<string, unknown> {\n if (!isPlainObject(value)) {\n throw new JobEnvelopeError(`Invalid ${kind}: invalid ${field}`);\n }\n}\n\nfunction requireExactKeys(\n value: Record<string, unknown>,\n expectedKeys: readonly string[],\n kind: string,\n): void {\n for (const key of expectedKeys) {\n if (!Object.hasOwn(value, key) || value[key] === undefined) {\n throw new JobEnvelopeError(`Invalid ${kind}: missing ${key}`);\n }\n }\n const expected = new Set<PropertyKey>(expectedKeys);\n for (const key of Reflect.ownKeys(value)) {\n if (!expected.has(key)) {\n throw new JobEnvelopeError(\n `Invalid ${kind}: unknown field ${String(key)}`,\n );\n }\n }\n}\n\nfunction requireString(\n value: unknown,\n field: string,\n kind: string,\n): asserts value is string {\n if (typeof value !== \"string\" || value.length === 0) {\n throw new JobEnvelopeError(`Invalid ${kind}: missing ${field}`);\n }\n}\n\nfunction validateJobRequest(value: unknown): JobRequest {\n requirePlainObject(value, \"request\", \"job request\");\n requireExactKeys(\n value,\n [\n \"v\",\n \"jobId\",\n \"owner\",\n \"builder\",\n \"builderPublicKey\",\n \"grantId\",\n \"scope\",\n \"operation\",\n \"pinnedVersion\",\n \"deadline\",\n ],\n \"job request\",\n );\n if (value.v !== JOB_PROTOCOL_VERSION) {\n throw new JobEnvelopeError(\n `Unsupported job request version: ${String(value.v)}`,\n );\n }\n requireString(value.jobId, \"jobId\", \"job request\");\n if (typeof value.owner !== \"string\" || !isAddress(value.owner)) {\n throw new JobEnvelopeError(\"Invalid job request: invalid owner\");\n }\n if (typeof value.builder !== \"string\" || !isAddress(value.builder)) {\n throw new JobEnvelopeError(\"Invalid job request: invalid builder\");\n }\n if (\n typeof value.builderPublicKey !== \"string\" ||\n !isHex(value.builderPublicKey)\n ) {\n throw new JobEnvelopeError(\"Invalid job request: invalid builderPublicKey\");\n }\n if (typeof value.grantId !== \"string\" || !isHex(value.grantId)) {\n throw new JobEnvelopeError(\"Invalid job request: invalid grantId\");\n }\n requireString(value.scope, \"scope\", \"job request\");\n if (!JOB_OPERATIONS.includes(value.operation as JobOperation)) {\n throw new JobEnvelopeError(\"Invalid job request: invalid operation\");\n }\n if (value.pinnedVersion !== null && typeof value.pinnedVersion !== \"string\") {\n throw new JobEnvelopeError(\"Invalid job request: missing pinnedVersion\");\n }\n if (\n typeof value.deadline !== \"string\" ||\n !Number.isFinite(Date.parse(value.deadline))\n ) {\n throw new JobEnvelopeError(\"Invalid job request: invalid deadline\");\n }\n return value as unknown as JobRequest;\n}\n\nfunction validateRequestEnvelope(value: unknown): JobRequestEnvelope {\n requirePlainObject(value, \"envelope\", \"job request envelope\");\n requireExactKeys(value, [\"request\", \"auth\"], \"job request envelope\");\n validateJobRequest(value.request);\n requireString(value.auth, \"auth\", \"job request envelope\");\n return value as unknown as JobRequestEnvelope;\n}\n\nfunction validateResult(value: unknown): JobResult {\n requirePlainObject(value, \"result\", \"job result\");\n requireExactKeys(\n value,\n [\"v\", \"jobId\", \"scope\", \"version\", \"contentType\", \"body\"],\n \"job result\",\n );\n if (value.v !== JOB_PROTOCOL_VERSION) {\n throw new JobEnvelopeError(\n `Unsupported job result version: ${String(value.v)}`,\n );\n }\n for (const field of [\"jobId\", \"scope\", \"contentType\"]) {\n requireString(value[field], field, \"job result\");\n }\n if (!(value.body instanceof Uint8Array)) {\n throw new JobEnvelopeError(\"Invalid job result: missing body\");\n }\n if (value.version !== null && typeof value.version !== \"string\") {\n throw new JobEnvelopeError(\"Invalid job result: missing version\");\n }\n return value as unknown as JobResult;\n}\n\n/**\n * Encrypts a validated job request envelope for a Personal Server enclave.\n *\n * The PS worker verifies\n * `auth.bodyHash === sha256(canonicalJobRequestBytes(request))`; the Gateway\n * never sees the plaintext.\n *\n * @param envelope - Request and builder Web3Signed authorization to encrypt.\n * @param enclavePublicKey - Public key returned by `GET /v1/identity?owner=`.\n * @param ecies - Injected ECIES implementation.\n * @returns Base64 ciphertext encoded as `iv || ephemPub || ct || mac`.\n * @throws {JobEnvelopeError} If the envelope or request is invalid.\n * @throws If ECIES encryption fails or the enclave public key is invalid.\n *\n * @example\n * ```ts\n * const identity = await fetch(`/v1/identity?owner=${owner}`).then((response) =>\n * response.json(),\n * );\n * const requestCiphertext = await sealJobRequest(\n * requestEnvelope,\n * identity.publicKey,\n * ecies,\n * );\n * await fetch(\"/v1/jobs\", {\n * method: \"POST\",\n * body: JSON.stringify({ ...submission, requestCiphertext }),\n * });\n * ```\n */\nexport async function sealJobRequest(\n envelope: JobRequestEnvelope,\n enclavePublicKey: Hex,\n ecies: ECIESProvider,\n): Promise<string> {\n const encrypted = await ecies.encrypt(\n fromHex(enclavePublicKey, \"bytes\"),\n requestPlaintext(envelope),\n );\n return encryptedBytesToBase64(encrypted);\n}\n\n/**\n * Decrypts and validates a job request envelope inside the enclave.\n *\n * @param ciphertext - Base64 `iv || ephemPub || ct || mac` ciphertext.\n * @param privateKey - Enclave key bytes, supplied as `Uint8Array` so the agent can zero them after use.\n * @param ecies - Injected ECIES implementation.\n * @returns The validated request envelope exactly as parsed from plaintext.\n * @throws {JobEnvelopeError} If plaintext is malformed or fails schema validation.\n * @throws If ciphertext decoding or ECIES decryption fails.\n */\nexport async function openJobRequest(\n ciphertext: string,\n privateKey: Uint8Array,\n ecies: ECIESProvider,\n): Promise<JobRequestEnvelope> {\n const plaintext = await ecies.decrypt(\n privateKey,\n base64ToEncrypted(ciphertext),\n );\n return validateRequestEnvelope(\n parseObject(plaintext, \"job request envelope\"),\n );\n}\n\n/**\n * Encrypts a validated job result for its builder and describes the ciphertext.\n *\n * `hash` is lowercase `0x` SHA-256 of the raw sealed bytes, not the `sha256:`\n * prefix used by Web3Signed `bodyHash`. `size` is the sealed byte length. They\n * equal the Gateway's `resultHash` and `resultSize`.\n *\n * @param result - Job result to validate and encrypt.\n * @param builderPublicKey - Builder wallet public key recorded in the request.\n * @param ecies - Injected ECIES implementation.\n * @returns Raw sealed bytes plus their Gateway-compatible hash and size.\n * @throws {JobEnvelopeError} If the result does not match the protocol schema.\n * @throws If ECIES encryption fails or the builder public key is invalid.\n */\nexport async function sealJobResult(\n result: JobResult,\n builderPublicKey: Hex,\n ecies: ECIESProvider,\n): Promise<{ bytes: Uint8Array; hash: Hex; size: number }> {\n validateResult(result);\n const encrypted = await ecies.encrypt(\n fromHex(builderPublicKey, \"bytes\"),\n resultPlaintext(result),\n );\n const bytes = encryptedToBytes(encrypted);\n return { bytes, hash: bytesToHex(sha256(bytes)), size: bytes.length };\n}\n\n/**\n * Decrypts a job result and verifies its builder-visible protocol bindings.\n *\n * The builder private key is a `Hex` wallet key. For `expect.version`,\n * `undefined` skips the check while `null` requires a null result version.\n *\n * @param sealedBytes - Raw `iv || ephemPub || ct || mac` bytes.\n * @param builderPrivateKey - Builder's wallet private key as hex.\n * @param ecies - Injected ECIES implementation.\n * @param expect - Required job ID and optional scope and version bindings.\n * @returns The validated result when every supplied binding matches.\n * @throws {JobEnvelopeError} If plaintext is malformed, invalid, or a binding differs.\n * @throws If ciphertext decoding or ECIES decryption fails.\n *\n * @example\n * ```ts\n * const sealedBytes = new Uint8Array(await response.arrayBuffer());\n * const result = await openJobResult(sealedBytes, key, ecies, {\n * jobId,\n * scope,\n * });\n * const text = new TextDecoder().decode(result.body);\n * ```\n */\nexport async function openJobResult(\n sealedBytes: Uint8Array,\n builderPrivateKey: Hex,\n ecies: ECIESProvider,\n expect: { jobId: string; scope?: string; version?: string | null },\n): Promise<JobResult> {\n const plaintext = await ecies.decrypt(\n fromHex(builderPrivateKey, \"bytes\"),\n bytesToEncrypted(sealedBytes),\n );\n const result = parseResultPlaintext(plaintext);\n if (result.jobId !== expect.jobId) {\n throw new JobEnvelopeError(\n `Job result ID ${result.jobId} does not match expected job ID ${expect.jobId}`,\n );\n }\n if (expect.scope !== undefined && result.scope !== expect.scope) {\n throw new JobEnvelopeError(\n `Job result scope ${result.scope} does not match expected scope ${expect.scope}`,\n );\n }\n if (expect.version !== undefined && result.version !== expect.version) {\n throw new JobEnvelopeError(\n `Job result version ${String(result.version)} does not match expected version ${String(expect.version)}`,\n );\n }\n return result;\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.browser.js
CHANGED
|
@@ -31993,6 +31993,8 @@ import { sha256 as sha2565 } from "@noble/hashes/sha2";
|
|
|
31993
31993
|
import { bytesToHex as bytesToHex2, fromHex as fromHex6, isAddress, isHex, toHex as toHex7 } from "viem";
|
|
31994
31994
|
var textDecoder = new TextDecoder();
|
|
31995
31995
|
var textEncoder = new TextEncoder();
|
|
31996
|
+
var MAX_JOB_RESULT_HEADER_BYTES = 4096;
|
|
31997
|
+
var JOB_RESULT_LENGTH_PREFIX_BYTES = 4;
|
|
31996
31998
|
var JobEnvelopeError = class extends Error {
|
|
31997
31999
|
constructor(message) {
|
|
31998
32000
|
super(message);
|
|
@@ -32025,16 +32027,56 @@ function requestPlaintext(envelope) {
|
|
|
32025
32027
|
return canonicalJsonBytes(envelope);
|
|
32026
32028
|
}
|
|
32027
32029
|
function resultPlaintext(result) {
|
|
32028
|
-
|
|
32030
|
+
const header = textEncoder.encode(
|
|
32029
32031
|
JSON.stringify({
|
|
32030
32032
|
v: result.v,
|
|
32031
32033
|
jobId: result.jobId,
|
|
32032
32034
|
scope: result.scope,
|
|
32033
32035
|
version: result.version,
|
|
32034
|
-
contentType: result.contentType
|
|
32035
|
-
body: result.body
|
|
32036
|
+
contentType: result.contentType
|
|
32036
32037
|
})
|
|
32037
32038
|
);
|
|
32039
|
+
if (header.length > MAX_JOB_RESULT_HEADER_BYTES) {
|
|
32040
|
+
throw new JobEnvelopeError(
|
|
32041
|
+
`Invalid job result: header exceeds ${MAX_JOB_RESULT_HEADER_BYTES} bytes`
|
|
32042
|
+
);
|
|
32043
|
+
}
|
|
32044
|
+
const plaintext = new Uint8Array(
|
|
32045
|
+
JOB_RESULT_LENGTH_PREFIX_BYTES + header.length + result.body.length
|
|
32046
|
+
);
|
|
32047
|
+
new DataView(plaintext.buffer).setUint32(0, header.length);
|
|
32048
|
+
plaintext.set(header, JOB_RESULT_LENGTH_PREFIX_BYTES);
|
|
32049
|
+
plaintext.set(result.body, JOB_RESULT_LENGTH_PREFIX_BYTES + header.length);
|
|
32050
|
+
return plaintext;
|
|
32051
|
+
}
|
|
32052
|
+
function parseResultPlaintext(plaintext) {
|
|
32053
|
+
if (plaintext.length < JOB_RESULT_LENGTH_PREFIX_BYTES) {
|
|
32054
|
+
throw new JobEnvelopeError(
|
|
32055
|
+
"Invalid job result: missing header length prefix"
|
|
32056
|
+
);
|
|
32057
|
+
}
|
|
32058
|
+
const headerLength = new DataView(
|
|
32059
|
+
plaintext.buffer,
|
|
32060
|
+
plaintext.byteOffset,
|
|
32061
|
+
JOB_RESULT_LENGTH_PREFIX_BYTES
|
|
32062
|
+
).getUint32(0);
|
|
32063
|
+
if (headerLength > MAX_JOB_RESULT_HEADER_BYTES) {
|
|
32064
|
+
throw new JobEnvelopeError(
|
|
32065
|
+
`Invalid job result: header exceeds ${MAX_JOB_RESULT_HEADER_BYTES} bytes`
|
|
32066
|
+
);
|
|
32067
|
+
}
|
|
32068
|
+
const remainingLength = plaintext.length - JOB_RESULT_LENGTH_PREFIX_BYTES;
|
|
32069
|
+
if (headerLength > remainingLength) {
|
|
32070
|
+
throw new JobEnvelopeError(
|
|
32071
|
+
"Invalid job result: header length exceeds payload"
|
|
32072
|
+
);
|
|
32073
|
+
}
|
|
32074
|
+
const headerEnd = JOB_RESULT_LENGTH_PREFIX_BYTES + headerLength;
|
|
32075
|
+
const header = parseObject(
|
|
32076
|
+
plaintext.subarray(JOB_RESULT_LENGTH_PREFIX_BYTES, headerEnd),
|
|
32077
|
+
"job result header"
|
|
32078
|
+
);
|
|
32079
|
+
return validateResult({ ...header, body: plaintext.subarray(headerEnd) });
|
|
32038
32080
|
}
|
|
32039
32081
|
function encryptedBytesToBase64(encrypted) {
|
|
32040
32082
|
return toBase64(fromHex6(`0x${serializeECIES(encrypted)}`, "bytes"));
|
|
@@ -32204,7 +32246,7 @@ function validateResult(value) {
|
|
|
32204
32246
|
for (const field of ["jobId", "scope", "contentType"]) {
|
|
32205
32247
|
requireString(value[field], field, "job result");
|
|
32206
32248
|
}
|
|
32207
|
-
if (
|
|
32249
|
+
if (!(value.body instanceof Uint8Array)) {
|
|
32208
32250
|
throw new JobEnvelopeError("Invalid job result: missing body");
|
|
32209
32251
|
}
|
|
32210
32252
|
if (value.version !== null && typeof value.version !== "string") {
|
|
@@ -32242,7 +32284,7 @@ async function openJobResult(sealedBytes, builderPrivateKey, ecies, expect) {
|
|
|
32242
32284
|
fromHex6(builderPrivateKey, "bytes"),
|
|
32243
32285
|
bytesToEncrypted(sealedBytes)
|
|
32244
32286
|
);
|
|
32245
|
-
const result =
|
|
32287
|
+
const result = parseResultPlaintext(plaintext);
|
|
32246
32288
|
if (result.jobId !== expect.jobId) {
|
|
32247
32289
|
throw new JobEnvelopeError(
|
|
32248
32290
|
`Job result ID ${result.jobId} does not match expected job ID ${expect.jobId}`
|