@opendatalabs/vana-sdk 3.22.0-pr.208.e9c19e9 → 3.22.0-pr.209.13d545d
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 +26 -87
- package/dist/crypto/envelope/job.cjs.map +1 -1
- package/dist/crypto/envelope/job.d.ts +9 -95
- package/dist/crypto/envelope/job.js +27 -91
- package/dist/crypto/envelope/job.js.map +1 -1
- package/dist/direct/escrow-payment.cjs.map +1 -1
- package/dist/direct/escrow-payment.d.ts +2 -2
- package/dist/direct/escrow-payment.js.map +1 -1
- package/dist/index.browser.d.ts +2 -2
- package/dist/index.browser.js +39 -265
- package/dist/index.browser.js.map +3 -3
- package/dist/index.node.cjs +36 -267
- package/dist/index.node.cjs.map +3 -3
- package/dist/index.node.d.ts +2 -2
- package/dist/index.node.js +49 -275
- package/dist/index.node.js.map +3 -3
- package/dist/protocol/eip712.cjs +2 -28
- package/dist/protocol/eip712.cjs.map +1 -1
- package/dist/protocol/eip712.d.ts +0 -70
- package/dist/protocol/eip712.js +1 -24
- package/dist/protocol/eip712.js.map +1 -1
- package/dist/protocol/escrow.cjs +2 -154
- package/dist/protocol/escrow.cjs.map +1 -1
- package/dist/protocol/escrow.d.ts +2 -145
- package/dist/protocol/escrow.js +1 -152
- package/dist/protocol/escrow.js.map +1 -1
- package/dist/protocol/gateway.cjs.map +1 -1
- package/dist/protocol/gateway.d.ts +38 -8
- package/dist/protocol/gateway.js.map +1 -1
- package/dist/protocol/jobs.cjs.map +1 -1
- package/dist/protocol/jobs.d.ts +1 -23
- package/dist/protocol/jobs.js.map +1 -1
- package/package.json +1 -1
|
@@ -29,7 +29,6 @@ module.exports = __toCommonJS(job_exports);
|
|
|
29
29
|
var import_sha2 = require("@noble/hashes/sha2");
|
|
30
30
|
var import_viem = require("viem");
|
|
31
31
|
var import_interface = require("../ecies/interface");
|
|
32
|
-
var import_jobs = require("../../protocol/jobs");
|
|
33
32
|
var import_encoding = require("../../utils/encoding");
|
|
34
33
|
const textDecoder = new TextDecoder();
|
|
35
34
|
const textEncoder = new TextEncoder();
|
|
@@ -57,11 +56,9 @@ function canonicalJsonBytes(value) {
|
|
|
57
56
|
return textEncoder.encode(JSON.stringify(sortJsonKeys(value)));
|
|
58
57
|
}
|
|
59
58
|
function canonicalJobRequestBytes(request) {
|
|
60
|
-
validateJobRequest(request);
|
|
61
59
|
return canonicalJsonBytes(request);
|
|
62
60
|
}
|
|
63
61
|
function requestPlaintext(envelope) {
|
|
64
|
-
validateRequestEnvelope(envelope);
|
|
65
62
|
return canonicalJsonBytes(envelope);
|
|
66
63
|
}
|
|
67
64
|
function resultPlaintext(result) {
|
|
@@ -94,99 +91,42 @@ function parseObject(plaintext, kind) {
|
|
|
94
91
|
throw new JobEnvelopeError(`Invalid ${kind}: malformed JSON`);
|
|
95
92
|
}
|
|
96
93
|
}
|
|
97
|
-
function isPlainObject(value) {
|
|
98
|
-
if (value === null || typeof value !== "object") return false;
|
|
99
|
-
const prototype = Object.getPrototypeOf(value);
|
|
100
|
-
return prototype === Object.prototype || prototype === null;
|
|
101
|
-
}
|
|
102
|
-
function requirePlainObject(value, field, kind) {
|
|
103
|
-
if (!isPlainObject(value)) {
|
|
104
|
-
throw new JobEnvelopeError(`Invalid ${kind}: invalid ${field}`);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
function requireExactKeys(value, expectedKeys, kind) {
|
|
108
|
-
for (const key of expectedKeys) {
|
|
109
|
-
if (!Object.hasOwn(value, key) || value[key] === void 0) {
|
|
110
|
-
throw new JobEnvelopeError(`Invalid ${kind}: missing ${key}`);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
const expected = new Set(expectedKeys);
|
|
114
|
-
for (const key of Reflect.ownKeys(value)) {
|
|
115
|
-
if (!expected.has(key)) {
|
|
116
|
-
throw new JobEnvelopeError(
|
|
117
|
-
`Invalid ${kind}: unknown field ${String(key)}`
|
|
118
|
-
);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
94
|
function requireString(value, field, kind) {
|
|
123
95
|
if (typeof value !== "string" || value.length === 0) {
|
|
124
96
|
throw new JobEnvelopeError(`Invalid ${kind}: missing ${field}`);
|
|
125
97
|
}
|
|
126
98
|
}
|
|
127
|
-
function
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
"owner",
|
|
135
|
-
"builder",
|
|
136
|
-
"builderPublicKey",
|
|
137
|
-
"grantId",
|
|
138
|
-
"scope",
|
|
139
|
-
"operation",
|
|
140
|
-
"pinnedVersion",
|
|
141
|
-
"deadline"
|
|
142
|
-
],
|
|
143
|
-
"job request"
|
|
144
|
-
);
|
|
145
|
-
if (value.v !== import_jobs.JOB_PROTOCOL_VERSION) {
|
|
99
|
+
function validateRequestEnvelope(value) {
|
|
100
|
+
const request = value.request;
|
|
101
|
+
if (request === null || typeof request !== "object" || Array.isArray(request)) {
|
|
102
|
+
throw new JobEnvelopeError("Invalid job request envelope: missing request");
|
|
103
|
+
}
|
|
104
|
+
const fields = request;
|
|
105
|
+
if (fields.v !== 1) {
|
|
146
106
|
throw new JobEnvelopeError(
|
|
147
|
-
`Unsupported job request version: ${String(
|
|
107
|
+
`Unsupported job request version: ${String(fields.v)}`
|
|
148
108
|
);
|
|
149
109
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
requireString(value.scope, "scope", "job request");
|
|
164
|
-
if (!import_jobs.JOB_OPERATIONS.includes(value.operation)) {
|
|
165
|
-
throw new JobEnvelopeError("Invalid job request: invalid operation");
|
|
166
|
-
}
|
|
167
|
-
if (value.pinnedVersion !== null && typeof value.pinnedVersion !== "string") {
|
|
110
|
+
for (const field of [
|
|
111
|
+
"jobId",
|
|
112
|
+
"owner",
|
|
113
|
+
"builder",
|
|
114
|
+
"builderPublicKey",
|
|
115
|
+
"grantId",
|
|
116
|
+
"scope",
|
|
117
|
+
"operation",
|
|
118
|
+
"deadline"
|
|
119
|
+
]) {
|
|
120
|
+
requireString(fields[field], field, "job request");
|
|
121
|
+
}
|
|
122
|
+
if (fields.pinnedVersion !== null && typeof fields.pinnedVersion !== "string") {
|
|
168
123
|
throw new JobEnvelopeError("Invalid job request: missing pinnedVersion");
|
|
169
124
|
}
|
|
170
|
-
if (typeof value.deadline !== "string" || !Number.isFinite(Date.parse(value.deadline))) {
|
|
171
|
-
throw new JobEnvelopeError("Invalid job request: invalid deadline");
|
|
172
|
-
}
|
|
173
|
-
return value;
|
|
174
|
-
}
|
|
175
|
-
function validateRequestEnvelope(value) {
|
|
176
|
-
requirePlainObject(value, "envelope", "job request envelope");
|
|
177
|
-
requireExactKeys(value, ["request", "auth"], "job request envelope");
|
|
178
|
-
validateJobRequest(value.request);
|
|
179
125
|
requireString(value.auth, "auth", "job request envelope");
|
|
180
126
|
return value;
|
|
181
127
|
}
|
|
182
128
|
function validateResult(value) {
|
|
183
|
-
|
|
184
|
-
requireExactKeys(
|
|
185
|
-
value,
|
|
186
|
-
["v", "jobId", "scope", "version", "contentType", "body"],
|
|
187
|
-
"job result"
|
|
188
|
-
);
|
|
189
|
-
if (value.v !== import_jobs.JOB_PROTOCOL_VERSION) {
|
|
129
|
+
if (value.v !== 1) {
|
|
190
130
|
throw new JobEnvelopeError(
|
|
191
131
|
`Unsupported job result version: ${String(value.v)}`
|
|
192
132
|
);
|
|
@@ -202,10 +142,10 @@ function validateResult(value) {
|
|
|
202
142
|
}
|
|
203
143
|
return value;
|
|
204
144
|
}
|
|
205
|
-
async function sealJobRequest(
|
|
145
|
+
async function sealJobRequest(e, enclavePublicKey, ecies) {
|
|
206
146
|
const encrypted = await ecies.encrypt(
|
|
207
147
|
(0, import_viem.fromHex)(enclavePublicKey, "bytes"),
|
|
208
|
-
requestPlaintext(
|
|
148
|
+
requestPlaintext(e)
|
|
209
149
|
);
|
|
210
150
|
return encryptedBytesToBase64(encrypted);
|
|
211
151
|
}
|
|
@@ -218,11 +158,10 @@ async function openJobRequest(ciphertext, privateKey, ecies) {
|
|
|
218
158
|
parseObject(plaintext, "job request envelope")
|
|
219
159
|
);
|
|
220
160
|
}
|
|
221
|
-
async function sealJobResult(
|
|
222
|
-
validateResult(result);
|
|
161
|
+
async function sealJobResult(r, builderPublicKey, ecies) {
|
|
223
162
|
const encrypted = await ecies.encrypt(
|
|
224
163
|
(0, import_viem.fromHex)(builderPublicKey, "bytes"),
|
|
225
|
-
resultPlaintext(
|
|
164
|
+
resultPlaintext(r)
|
|
226
165
|
);
|
|
227
166
|
const ciphertext = encryptedBytesToBase64(encrypted);
|
|
228
167
|
const bytes = (0, import_encoding.fromBase64)(ciphertext);
|
|
@@ -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 * wire ciphertext is base64 of `iv || ephemPub || ct || mac`, as specified by\n * the ECIES provider interface. The Gateway hashes raw ciphertext bytes, not\n * 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 {\n deserializeECIES,\n serializeECIES,\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 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 decoded ciphertext bytes, not the\n * `sha256:` prefix used by Web3Signed `bodyHash`. `size` is that decoded byte\n * length. They 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 Base64 ciphertext plus its 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<{ ciphertext: string; hash: Hex; size: number }> {\n validateResult(result);\n const encrypted = await ecies.encrypt(\n fromHex(builderPublicKey, \"bytes\"),\n resultPlaintext(result),\n );\n const ciphertext = encryptedBytesToBase64(encrypted);\n const bytes = fromBase64(ciphertext);\n return { ciphertext, 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 ciphertext - Base64 `iv || ephemPub || ct || mac` ciphertext.\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 result = await openJobResult(\n * status.resultCiphertext,\n * key,\n * ecies,\n * { jobId, scope },\n * );\n * const body = fromBase64(result.body);\n * ```\n */\nexport async function openJobResult(\n ciphertext: string,\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 base64ToEncrypted(ciphertext),\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 properties follow their interface order. The\n * wire ciphertext is base64 of `iv || ephemPub || ct || mac`, as specified by\n * the ECIES provider interface. The Gateway hashes raw ciphertext bytes, not\n * plaintext.\n * Per the jobs flow in contract section 1, the builder verifies the decrypted\n * result's job ID, scope, and version bindings.\n * Gateway and PS worker verify\n * `auth.bodyHash === sha256(canonicalJobRequestBytes(request))`.\n *\n * @category Cryptography\n */\n\nimport { sha256 } from \"@noble/hashes/sha2\";\nimport { bytesToHex, fromHex, toHex, type Hex } from \"viem\";\nimport {\n deserializeECIES,\n serializeECIES,\n type ECIESProvider,\n} from \"../ecies/interface\";\nimport type {\n JobRequest,\n JobRequestEnvelope,\n JobResult,\n} from \"../../protocol/jobs\";\nimport { fromBase64, toBase64 } from \"../../utils/encoding\";\n\nconst textDecoder = new TextDecoder();\nconst textEncoder = new TextEncoder();\n\n/** A decrypted job envelope 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/** Canonical UTF-8 JSON bytes committed to by a job request's auth body hash. */\nexport function canonicalJobRequestBytes(request: JobRequest): Uint8Array {\n return canonicalJsonBytes(request);\n}\n\nfunction requestPlaintext(envelope: JobRequestEnvelope): Uint8Array {\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 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 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 validateRequestEnvelope(\n value: Record<string, unknown>,\n): JobRequestEnvelope {\n const request = value.request;\n if (\n request === null ||\n typeof request !== \"object\" ||\n Array.isArray(request)\n ) {\n throw new JobEnvelopeError(\"Invalid job request envelope: missing request\");\n }\n const fields = request as Record<string, unknown>;\n if (fields.v !== 1) {\n throw new JobEnvelopeError(\n `Unsupported job request version: ${String(fields.v)}`,\n );\n }\n for (const field of [\n \"jobId\",\n \"owner\",\n \"builder\",\n \"builderPublicKey\",\n \"grantId\",\n \"scope\",\n \"operation\",\n \"deadline\",\n ]) {\n requireString(fields[field], field, \"job request\");\n }\n if (\n fields.pinnedVersion !== null &&\n typeof fields.pinnedVersion !== \"string\"\n ) {\n throw new JobEnvelopeError(\"Invalid job request: missing pinnedVersion\");\n }\n requireString(value.auth, \"auth\", \"job request envelope\");\n return value as unknown as JobRequestEnvelope;\n}\n\nfunction validateResult(value: Record<string, unknown>): JobResult {\n if (value.v !== 1) {\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\nexport async function sealJobRequest(\n e: JobRequestEnvelope,\n enclavePublicKey: Hex,\n ecies: ECIESProvider,\n): Promise<string> {\n const encrypted = await ecies.encrypt(\n fromHex(enclavePublicKey, \"bytes\"),\n requestPlaintext(e),\n );\n return encryptedBytesToBase64(encrypted);\n}\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\nexport async function sealJobResult(\n r: JobResult,\n builderPublicKey: Hex,\n ecies: ECIESProvider,\n): Promise<{ ciphertext: string; hash: Hex; size: number }> {\n const encrypted = await ecies.encrypt(\n fromHex(builderPublicKey, \"bytes\"),\n resultPlaintext(r),\n );\n const ciphertext = encryptedBytesToBase64(encrypted);\n const bytes = fromBase64(ciphertext);\n return { ciphertext, hash: bytesToHex(sha256(bytes)), size: bytes.length };\n}\n\nexport async function openJobResult(\n ciphertext: string,\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 base64ToEncrypted(ciphertext),\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":[]}
|
|
@@ -6,115 +6,29 @@
|
|
|
6
6
|
* wire ciphertext is base64 of `iv || ephemPub || ct || mac`, as specified by
|
|
7
7
|
* the ECIES provider interface. The Gateway hashes raw ciphertext bytes, not
|
|
8
8
|
* plaintext.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* `docs/260903-jobs-contract.md`, section 1.
|
|
9
|
+
* Per the jobs flow in contract section 1, the builder verifies the decrypted
|
|
10
|
+
* result's job ID, scope, and version bindings.
|
|
11
|
+
* Gateway and PS worker verify
|
|
12
|
+
* `auth.bodyHash === sha256(canonicalJobRequestBytes(request))`.
|
|
14
13
|
*
|
|
15
14
|
* @category Cryptography
|
|
16
15
|
*/
|
|
17
16
|
import { type Hex } from "viem";
|
|
18
17
|
import { type ECIESProvider } from "../ecies/interface.js";
|
|
19
|
-
import {
|
|
20
|
-
/** A job envelope
|
|
18
|
+
import type { JobRequest, JobRequestEnvelope, JobResult } from "../../protocol/jobs.js";
|
|
19
|
+
/** A decrypted job envelope did not match the jobs protocol. */
|
|
21
20
|
export declare class JobEnvelopeError extends Error {
|
|
22
21
|
constructor(message: string);
|
|
23
22
|
}
|
|
24
|
-
/**
|
|
25
|
-
* Returns the canonical UTF-8 JSON bytes committed to by the auth body hash.
|
|
26
|
-
*
|
|
27
|
-
* @param request - Job request to validate and serialize canonically.
|
|
28
|
-
* @returns Recursively key-sorted, whitespace-free UTF-8 JSON bytes.
|
|
29
|
-
* @throws {JobEnvelopeError} If the request does not match the protocol schema.
|
|
30
|
-
*/
|
|
23
|
+
/** Canonical UTF-8 JSON bytes committed to by a job request's auth body hash. */
|
|
31
24
|
export declare function canonicalJobRequestBytes(request: JobRequest): Uint8Array;
|
|
32
|
-
|
|
33
|
-
* Encrypts a validated job request envelope for a Personal Server enclave.
|
|
34
|
-
*
|
|
35
|
-
* The PS worker verifies
|
|
36
|
-
* `auth.bodyHash === sha256(canonicalJobRequestBytes(request))`; the Gateway
|
|
37
|
-
* never sees the plaintext.
|
|
38
|
-
*
|
|
39
|
-
* @param envelope - Request and builder Web3Signed authorization to encrypt.
|
|
40
|
-
* @param enclavePublicKey - Public key returned by `GET /v1/identity?owner=`.
|
|
41
|
-
* @param ecies - Injected ECIES implementation.
|
|
42
|
-
* @returns Base64 ciphertext encoded as `iv || ephemPub || ct || mac`.
|
|
43
|
-
* @throws {JobEnvelopeError} If the envelope or request is invalid.
|
|
44
|
-
* @throws If ECIES encryption fails or the enclave public key is invalid.
|
|
45
|
-
*
|
|
46
|
-
* @example
|
|
47
|
-
* ```ts
|
|
48
|
-
* const identity = await fetch(`/v1/identity?owner=${owner}`).then((response) =>
|
|
49
|
-
* response.json(),
|
|
50
|
-
* );
|
|
51
|
-
* const requestCiphertext = await sealJobRequest(
|
|
52
|
-
* requestEnvelope,
|
|
53
|
-
* identity.publicKey,
|
|
54
|
-
* ecies,
|
|
55
|
-
* );
|
|
56
|
-
* await fetch("/v1/jobs", {
|
|
57
|
-
* method: "POST",
|
|
58
|
-
* body: JSON.stringify({ ...submission, requestCiphertext }),
|
|
59
|
-
* });
|
|
60
|
-
* ```
|
|
61
|
-
*/
|
|
62
|
-
export declare function sealJobRequest(envelope: JobRequestEnvelope, enclavePublicKey: Hex, ecies: ECIESProvider): Promise<string>;
|
|
63
|
-
/**
|
|
64
|
-
* Decrypts and validates a job request envelope inside the enclave.
|
|
65
|
-
*
|
|
66
|
-
* @param ciphertext - Base64 `iv || ephemPub || ct || mac` ciphertext.
|
|
67
|
-
* @param privateKey - Enclave key bytes, supplied as `Uint8Array` so the agent can zero them after use.
|
|
68
|
-
* @param ecies - Injected ECIES implementation.
|
|
69
|
-
* @returns The validated request envelope exactly as parsed from plaintext.
|
|
70
|
-
* @throws {JobEnvelopeError} If plaintext is malformed or fails schema validation.
|
|
71
|
-
* @throws If ciphertext decoding or ECIES decryption fails.
|
|
72
|
-
*/
|
|
25
|
+
export declare function sealJobRequest(e: JobRequestEnvelope, enclavePublicKey: Hex, ecies: ECIESProvider): Promise<string>;
|
|
73
26
|
export declare function openJobRequest(ciphertext: string, privateKey: Uint8Array, ecies: ECIESProvider): Promise<JobRequestEnvelope>;
|
|
74
|
-
|
|
75
|
-
* Encrypts a validated job result for its builder and describes the ciphertext.
|
|
76
|
-
*
|
|
77
|
-
* `hash` is lowercase `0x` SHA-256 of decoded ciphertext bytes, not the
|
|
78
|
-
* `sha256:` prefix used by Web3Signed `bodyHash`. `size` is that decoded byte
|
|
79
|
-
* length. They equal the Gateway's `resultHash` and `resultSize`.
|
|
80
|
-
*
|
|
81
|
-
* @param result - Job result to validate and encrypt.
|
|
82
|
-
* @param builderPublicKey - Builder wallet public key recorded in the request.
|
|
83
|
-
* @param ecies - Injected ECIES implementation.
|
|
84
|
-
* @returns Base64 ciphertext plus its Gateway-compatible hash and size.
|
|
85
|
-
* @throws {JobEnvelopeError} If the result does not match the protocol schema.
|
|
86
|
-
* @throws If ECIES encryption fails or the builder public key is invalid.
|
|
87
|
-
*/
|
|
88
|
-
export declare function sealJobResult(result: JobResult, builderPublicKey: Hex, ecies: ECIESProvider): Promise<{
|
|
27
|
+
export declare function sealJobResult(r: JobResult, builderPublicKey: Hex, ecies: ECIESProvider): Promise<{
|
|
89
28
|
ciphertext: string;
|
|
90
29
|
hash: Hex;
|
|
91
30
|
size: number;
|
|
92
31
|
}>;
|
|
93
|
-
/**
|
|
94
|
-
* Decrypts a job result and verifies its builder-visible protocol bindings.
|
|
95
|
-
*
|
|
96
|
-
* The builder private key is a `Hex` wallet key. For `expect.version`,
|
|
97
|
-
* `undefined` skips the check while `null` requires a null result version.
|
|
98
|
-
*
|
|
99
|
-
* @param ciphertext - Base64 `iv || ephemPub || ct || mac` ciphertext.
|
|
100
|
-
* @param builderPrivateKey - Builder's wallet private key as hex.
|
|
101
|
-
* @param ecies - Injected ECIES implementation.
|
|
102
|
-
* @param expect - Required job ID and optional scope and version bindings.
|
|
103
|
-
* @returns The validated result when every supplied binding matches.
|
|
104
|
-
* @throws {JobEnvelopeError} If plaintext is malformed, invalid, or a binding differs.
|
|
105
|
-
* @throws If ciphertext decoding or ECIES decryption fails.
|
|
106
|
-
*
|
|
107
|
-
* @example
|
|
108
|
-
* ```ts
|
|
109
|
-
* const result = await openJobResult(
|
|
110
|
-
* status.resultCiphertext,
|
|
111
|
-
* key,
|
|
112
|
-
* ecies,
|
|
113
|
-
* { jobId, scope },
|
|
114
|
-
* );
|
|
115
|
-
* const body = fromBase64(result.body);
|
|
116
|
-
* ```
|
|
117
|
-
*/
|
|
118
32
|
export declare function openJobResult(ciphertext: string, builderPrivateKey: Hex, ecies: ECIESProvider, expect: {
|
|
119
33
|
jobId: string;
|
|
120
34
|
scope?: string;
|
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
import { sha256 } from "@noble/hashes/sha2";
|
|
2
|
-
import { bytesToHex, fromHex,
|
|
2
|
+
import { bytesToHex, fromHex, toHex } from "viem";
|
|
3
3
|
import {
|
|
4
4
|
deserializeECIES,
|
|
5
5
|
serializeECIES
|
|
6
6
|
} from "../ecies/interface.js";
|
|
7
|
-
import {
|
|
8
|
-
JOB_OPERATIONS,
|
|
9
|
-
JOB_PROTOCOL_VERSION
|
|
10
|
-
} from "../../protocol/jobs.js";
|
|
11
7
|
import { fromBase64, toBase64 } from "../../utils/encoding.js";
|
|
12
8
|
const textDecoder = new TextDecoder();
|
|
13
9
|
const textEncoder = new TextEncoder();
|
|
@@ -35,11 +31,9 @@ function canonicalJsonBytes(value) {
|
|
|
35
31
|
return textEncoder.encode(JSON.stringify(sortJsonKeys(value)));
|
|
36
32
|
}
|
|
37
33
|
function canonicalJobRequestBytes(request) {
|
|
38
|
-
validateJobRequest(request);
|
|
39
34
|
return canonicalJsonBytes(request);
|
|
40
35
|
}
|
|
41
36
|
function requestPlaintext(envelope) {
|
|
42
|
-
validateRequestEnvelope(envelope);
|
|
43
37
|
return canonicalJsonBytes(envelope);
|
|
44
38
|
}
|
|
45
39
|
function resultPlaintext(result) {
|
|
@@ -72,99 +66,42 @@ function parseObject(plaintext, kind) {
|
|
|
72
66
|
throw new JobEnvelopeError(`Invalid ${kind}: malformed JSON`);
|
|
73
67
|
}
|
|
74
68
|
}
|
|
75
|
-
function isPlainObject(value) {
|
|
76
|
-
if (value === null || typeof value !== "object") return false;
|
|
77
|
-
const prototype = Object.getPrototypeOf(value);
|
|
78
|
-
return prototype === Object.prototype || prototype === null;
|
|
79
|
-
}
|
|
80
|
-
function requirePlainObject(value, field, kind) {
|
|
81
|
-
if (!isPlainObject(value)) {
|
|
82
|
-
throw new JobEnvelopeError(`Invalid ${kind}: invalid ${field}`);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
function requireExactKeys(value, expectedKeys, kind) {
|
|
86
|
-
for (const key of expectedKeys) {
|
|
87
|
-
if (!Object.hasOwn(value, key) || value[key] === void 0) {
|
|
88
|
-
throw new JobEnvelopeError(`Invalid ${kind}: missing ${key}`);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
const expected = new Set(expectedKeys);
|
|
92
|
-
for (const key of Reflect.ownKeys(value)) {
|
|
93
|
-
if (!expected.has(key)) {
|
|
94
|
-
throw new JobEnvelopeError(
|
|
95
|
-
`Invalid ${kind}: unknown field ${String(key)}`
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
69
|
function requireString(value, field, kind) {
|
|
101
70
|
if (typeof value !== "string" || value.length === 0) {
|
|
102
71
|
throw new JobEnvelopeError(`Invalid ${kind}: missing ${field}`);
|
|
103
72
|
}
|
|
104
73
|
}
|
|
105
|
-
function
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
"owner",
|
|
113
|
-
"builder",
|
|
114
|
-
"builderPublicKey",
|
|
115
|
-
"grantId",
|
|
116
|
-
"scope",
|
|
117
|
-
"operation",
|
|
118
|
-
"pinnedVersion",
|
|
119
|
-
"deadline"
|
|
120
|
-
],
|
|
121
|
-
"job request"
|
|
122
|
-
);
|
|
123
|
-
if (value.v !== JOB_PROTOCOL_VERSION) {
|
|
74
|
+
function validateRequestEnvelope(value) {
|
|
75
|
+
const request = value.request;
|
|
76
|
+
if (request === null || typeof request !== "object" || Array.isArray(request)) {
|
|
77
|
+
throw new JobEnvelopeError("Invalid job request envelope: missing request");
|
|
78
|
+
}
|
|
79
|
+
const fields = request;
|
|
80
|
+
if (fields.v !== 1) {
|
|
124
81
|
throw new JobEnvelopeError(
|
|
125
|
-
`Unsupported job request version: ${String(
|
|
82
|
+
`Unsupported job request version: ${String(fields.v)}`
|
|
126
83
|
);
|
|
127
84
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
requireString(value.scope, "scope", "job request");
|
|
142
|
-
if (!JOB_OPERATIONS.includes(value.operation)) {
|
|
143
|
-
throw new JobEnvelopeError("Invalid job request: invalid operation");
|
|
144
|
-
}
|
|
145
|
-
if (value.pinnedVersion !== null && typeof value.pinnedVersion !== "string") {
|
|
85
|
+
for (const field of [
|
|
86
|
+
"jobId",
|
|
87
|
+
"owner",
|
|
88
|
+
"builder",
|
|
89
|
+
"builderPublicKey",
|
|
90
|
+
"grantId",
|
|
91
|
+
"scope",
|
|
92
|
+
"operation",
|
|
93
|
+
"deadline"
|
|
94
|
+
]) {
|
|
95
|
+
requireString(fields[field], field, "job request");
|
|
96
|
+
}
|
|
97
|
+
if (fields.pinnedVersion !== null && typeof fields.pinnedVersion !== "string") {
|
|
146
98
|
throw new JobEnvelopeError("Invalid job request: missing pinnedVersion");
|
|
147
99
|
}
|
|
148
|
-
if (typeof value.deadline !== "string" || !Number.isFinite(Date.parse(value.deadline))) {
|
|
149
|
-
throw new JobEnvelopeError("Invalid job request: invalid deadline");
|
|
150
|
-
}
|
|
151
|
-
return value;
|
|
152
|
-
}
|
|
153
|
-
function validateRequestEnvelope(value) {
|
|
154
|
-
requirePlainObject(value, "envelope", "job request envelope");
|
|
155
|
-
requireExactKeys(value, ["request", "auth"], "job request envelope");
|
|
156
|
-
validateJobRequest(value.request);
|
|
157
100
|
requireString(value.auth, "auth", "job request envelope");
|
|
158
101
|
return value;
|
|
159
102
|
}
|
|
160
103
|
function validateResult(value) {
|
|
161
|
-
|
|
162
|
-
requireExactKeys(
|
|
163
|
-
value,
|
|
164
|
-
["v", "jobId", "scope", "version", "contentType", "body"],
|
|
165
|
-
"job result"
|
|
166
|
-
);
|
|
167
|
-
if (value.v !== JOB_PROTOCOL_VERSION) {
|
|
104
|
+
if (value.v !== 1) {
|
|
168
105
|
throw new JobEnvelopeError(
|
|
169
106
|
`Unsupported job result version: ${String(value.v)}`
|
|
170
107
|
);
|
|
@@ -180,10 +117,10 @@ function validateResult(value) {
|
|
|
180
117
|
}
|
|
181
118
|
return value;
|
|
182
119
|
}
|
|
183
|
-
async function sealJobRequest(
|
|
120
|
+
async function sealJobRequest(e, enclavePublicKey, ecies) {
|
|
184
121
|
const encrypted = await ecies.encrypt(
|
|
185
122
|
fromHex(enclavePublicKey, "bytes"),
|
|
186
|
-
requestPlaintext(
|
|
123
|
+
requestPlaintext(e)
|
|
187
124
|
);
|
|
188
125
|
return encryptedBytesToBase64(encrypted);
|
|
189
126
|
}
|
|
@@ -196,11 +133,10 @@ async function openJobRequest(ciphertext, privateKey, ecies) {
|
|
|
196
133
|
parseObject(plaintext, "job request envelope")
|
|
197
134
|
);
|
|
198
135
|
}
|
|
199
|
-
async function sealJobResult(
|
|
200
|
-
validateResult(result);
|
|
136
|
+
async function sealJobResult(r, builderPublicKey, ecies) {
|
|
201
137
|
const encrypted = await ecies.encrypt(
|
|
202
138
|
fromHex(builderPublicKey, "bytes"),
|
|
203
|
-
resultPlaintext(
|
|
139
|
+
resultPlaintext(r)
|
|
204
140
|
);
|
|
205
141
|
const ciphertext = encryptedBytesToBase64(encrypted);
|
|
206
142
|
const bytes = fromBase64(ciphertext);
|
|
@@ -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 * wire ciphertext is base64 of `iv || ephemPub || ct || mac`, as specified by\n * the ECIES provider interface. The Gateway hashes raw ciphertext bytes, not\n * 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 {\n deserializeECIES,\n serializeECIES,\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 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 decoded ciphertext bytes, not the\n * `sha256:` prefix used by Web3Signed `bodyHash`. `size` is that decoded byte\n * length. They 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 Base64 ciphertext plus its 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<{ ciphertext: string; hash: Hex; size: number }> {\n validateResult(result);\n const encrypted = await ecies.encrypt(\n fromHex(builderPublicKey, \"bytes\"),\n resultPlaintext(result),\n );\n const ciphertext = encryptedBytesToBase64(encrypted);\n const bytes = fromBase64(ciphertext);\n return { ciphertext, 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 ciphertext - Base64 `iv || ephemPub || ct || mac` ciphertext.\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 result = await openJobResult(\n * status.resultCiphertext,\n * key,\n * ecies,\n * { jobId, scope },\n * );\n * const body = fromBase64(result.body);\n * ```\n */\nexport async function openJobResult(\n ciphertext: string,\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 base64ToEncrypted(ciphertext),\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 properties follow their interface order. The\n * wire ciphertext is base64 of `iv || ephemPub || ct || mac`, as specified by\n * the ECIES provider interface. The Gateway hashes raw ciphertext bytes, not\n * plaintext.\n * Per the jobs flow in contract section 1, the builder verifies the decrypted\n * result's job ID, scope, and version bindings.\n * Gateway and PS worker verify\n * `auth.bodyHash === sha256(canonicalJobRequestBytes(request))`.\n *\n * @category Cryptography\n */\n\nimport { sha256 } from \"@noble/hashes/sha2\";\nimport { bytesToHex, fromHex, toHex, type Hex } from \"viem\";\nimport {\n deserializeECIES,\n serializeECIES,\n type ECIESProvider,\n} from \"../ecies/interface\";\nimport type {\n JobRequest,\n JobRequestEnvelope,\n JobResult,\n} from \"../../protocol/jobs\";\nimport { fromBase64, toBase64 } from \"../../utils/encoding\";\n\nconst textDecoder = new TextDecoder();\nconst textEncoder = new TextEncoder();\n\n/** A decrypted job envelope 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/** Canonical UTF-8 JSON bytes committed to by a job request's auth body hash. */\nexport function canonicalJobRequestBytes(request: JobRequest): Uint8Array {\n return canonicalJsonBytes(request);\n}\n\nfunction requestPlaintext(envelope: JobRequestEnvelope): Uint8Array {\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 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 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 validateRequestEnvelope(\n value: Record<string, unknown>,\n): JobRequestEnvelope {\n const request = value.request;\n if (\n request === null ||\n typeof request !== \"object\" ||\n Array.isArray(request)\n ) {\n throw new JobEnvelopeError(\"Invalid job request envelope: missing request\");\n }\n const fields = request as Record<string, unknown>;\n if (fields.v !== 1) {\n throw new JobEnvelopeError(\n `Unsupported job request version: ${String(fields.v)}`,\n );\n }\n for (const field of [\n \"jobId\",\n \"owner\",\n \"builder\",\n \"builderPublicKey\",\n \"grantId\",\n \"scope\",\n \"operation\",\n \"deadline\",\n ]) {\n requireString(fields[field], field, \"job request\");\n }\n if (\n fields.pinnedVersion !== null &&\n typeof fields.pinnedVersion !== \"string\"\n ) {\n throw new JobEnvelopeError(\"Invalid job request: missing pinnedVersion\");\n }\n requireString(value.auth, \"auth\", \"job request envelope\");\n return value as unknown as JobRequestEnvelope;\n}\n\nfunction validateResult(value: Record<string, unknown>): JobResult {\n if (value.v !== 1) {\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\nexport async function sealJobRequest(\n e: JobRequestEnvelope,\n enclavePublicKey: Hex,\n ecies: ECIESProvider,\n): Promise<string> {\n const encrypted = await ecies.encrypt(\n fromHex(enclavePublicKey, \"bytes\"),\n requestPlaintext(e),\n );\n return encryptedBytesToBase64(encrypted);\n}\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\nexport async function sealJobResult(\n r: JobResult,\n builderPublicKey: Hex,\n ecies: ECIESProvider,\n): Promise<{ ciphertext: string; hash: Hex; size: number }> {\n const encrypted = await ecies.encrypt(\n fromHex(builderPublicKey, \"bytes\"),\n resultPlaintext(r),\n );\n const ciphertext = encryptedBytesToBase64(encrypted);\n const bytes = fromBase64(ciphertext);\n return { ciphertext, hash: bytesToHex(sha256(bytes)), size: bytes.length };\n}\n\nexport async function openJobResult(\n ciphertext: string,\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 base64ToEncrypted(ciphertext),\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":[]}
|