@opendatalabs/vana-sdk 3.23.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -0
- package/dist/auth/errors.cjs +1 -1
- package/dist/auth/web3-signed.cjs +1 -1
- package/dist/crypto/envelope/job-stream.test.d.ts +1 -0
- package/dist/crypto/envelope/job.cjs +593 -38
- package/dist/crypto/envelope/job.cjs.map +1 -1
- package/dist/crypto/envelope/job.d.ts +44 -27
- package/dist/crypto/envelope/job.js +589 -37
- package/dist/crypto/envelope/job.js.map +1 -1
- package/dist/direct/access-request-client.cjs +1 -1
- package/dist/direct/connect-flow.cjs +43 -1
- package/dist/direct/connect-flow.cjs.map +1 -1
- package/dist/direct/connect-flow.d.ts +12 -0
- package/dist/direct/connect-flow.js +43 -1
- package/dist/direct/connect-flow.js.map +1 -1
- package/dist/direct/controller.cjs +1 -1
- package/dist/direct/errors.cjs +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/direct/personal-server-read.cjs +2 -2
- package/dist/direct/use-direct-vana-connect.cjs +2 -1
- package/dist/direct/use-direct-vana-connect.cjs.map +1 -1
- package/dist/direct/use-direct-vana-connect.d.ts +3 -1
- package/dist/direct/use-direct-vana-connect.js +2 -1
- package/dist/direct/use-direct-vana-connect.js.map +1 -1
- package/dist/error-entry-points.test.d.ts +1 -0
- package/dist/errors.cjs +7 -0
- package/dist/errors.cjs.map +1 -1
- package/dist/errors.d.ts +10 -0
- package/dist/errors.js +6 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.browser.d.ts +4 -4
- package/dist/index.browser.js +882 -518
- package/dist/index.browser.js.map +4 -4
- package/dist/index.node.cjs +1035 -705
- package/dist/index.node.cjs.map +4 -4
- package/dist/index.node.d.ts +4 -4
- package/dist/index.node.js +989 -567
- package/dist/index.node.js.map +4 -4
- package/dist/protocol/data-point-deletion.cjs +1 -1
- package/dist/protocol/derivative-questions.cjs +1 -1
- package/dist/protocol/derivative-status.cjs +1 -1
- package/dist/protocol/eip712.cjs +28 -2
- package/dist/protocol/eip712.cjs.map +1 -1
- package/dist/protocol/eip712.d.ts +70 -0
- package/dist/protocol/eip712.js +24 -1
- package/dist/protocol/eip712.js.map +1 -1
- package/dist/protocol/escrow.cjs +154 -2
- package/dist/protocol/escrow.cjs.map +1 -1
- package/dist/protocol/escrow.d.ts +145 -2
- package/dist/protocol/escrow.js +152 -1
- package/dist/protocol/escrow.js.map +1 -1
- package/dist/protocol/fixtures/moksha-identity.json +20 -0
- package/dist/protocol/gateway.cjs +1 -1
- package/dist/protocol/gateway.cjs.map +1 -1
- package/dist/protocol/gateway.d.ts +8 -38
- package/dist/protocol/gateway.js.map +1 -1
- package/dist/protocol/identity.cjs +7 -2
- package/dist/protocol/identity.cjs.map +1 -1
- package/dist/protocol/identity.js +7 -2
- package/dist/protocol/identity.js.map +1 -1
- package/dist/protocol/jobs-client.cjs +73 -21
- package/dist/protocol/jobs-client.cjs.map +1 -1
- package/dist/protocol/jobs-client.d.ts +7 -5
- package/dist/protocol/jobs-client.js +74 -21
- package/dist/protocol/jobs-client.js.map +1 -1
- package/dist/protocol/jobs.cjs +0 -3
- package/dist/protocol/jobs.cjs.map +1 -1
- package/dist/protocol/jobs.d.ts +21 -13
- package/dist/protocol/jobs.js +0 -2
- package/dist/protocol/jobs.js.map +1 -1
- package/dist/protocol/lineage.cjs +1 -1
- package/dist/protocol/personal-server-data.cjs +1 -1
- package/dist/protocol/personal-server-write.cjs +1 -1
- package/dist/protocol/write-request.cjs +1 -1
- package/dist/protocol/write-signer.cjs +1 -1
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js.map +1 -1
- package/dist/session-relay/client.cjs +1 -1
- package/dist/session-relay/errors.cjs +1 -1
- package/dist/session-relay/index.cjs +1 -1
- package/package.json +1 -1
|
@@ -18,21 +18,40 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
18
18
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
19
|
var job_exports = {};
|
|
20
20
|
__export(job_exports, {
|
|
21
|
+
JOB_RESULT_CHUNK_BYTES: () => JOB_RESULT_CHUNK_BYTES,
|
|
22
|
+
JOB_RESULT_FORMAT_VERSION: () => JOB_RESULT_FORMAT_VERSION,
|
|
21
23
|
JobEnvelopeError: () => JobEnvelopeError,
|
|
22
24
|
canonicalJobRequestBytes: () => canonicalJobRequestBytes,
|
|
23
25
|
openJobRequest: () => openJobRequest,
|
|
24
26
|
openJobResult: () => openJobResult,
|
|
27
|
+
openJobResultStream: () => openJobResultStream,
|
|
25
28
|
sealJobRequest: () => sealJobRequest,
|
|
26
|
-
sealJobResult: () => sealJobResult
|
|
29
|
+
sealJobResult: () => sealJobResult,
|
|
30
|
+
sealJobResultStream: () => sealJobResultStream
|
|
27
31
|
});
|
|
28
32
|
module.exports = __toCommonJS(job_exports);
|
|
29
33
|
var import_sha2 = require("@noble/hashes/sha2");
|
|
30
34
|
var import_viem = require("viem");
|
|
35
|
+
var import_constants = require("../ecies/constants");
|
|
31
36
|
var import_interface = require("../ecies/interface");
|
|
32
37
|
var import_jobs = require("../../protocol/jobs");
|
|
33
38
|
var import_encoding = require("../../utils/encoding");
|
|
34
39
|
const textDecoder = new TextDecoder();
|
|
35
40
|
const textEncoder = new TextEncoder();
|
|
41
|
+
const MAX_JOB_RESULT_HEADER_BYTES = 4096;
|
|
42
|
+
const WRAPPED_KEY_LENGTH_BYTES = 4;
|
|
43
|
+
const FRAME_LENGTH_BYTES = 4;
|
|
44
|
+
const FRAME_FLAGS_BYTES = 1;
|
|
45
|
+
const AES_KEY_BYTES = 32;
|
|
46
|
+
const AES_GCM_NONCE_BYTES = 12;
|
|
47
|
+
const AES_GCM_TAG_BYTES = 16;
|
|
48
|
+
const WRAPPED_METADATA_LENGTH_BYTES = 4;
|
|
49
|
+
const WRAPPED_KEY_PLAINTEXT_PREFIX_BYTES = AES_KEY_BYTES + AES_GCM_NONCE_BYTES + WRAPPED_METADATA_LENGTH_BYTES;
|
|
50
|
+
const MAX_WRAPPED_KEY_BYTES = 8192;
|
|
51
|
+
const FINAL_FRAME_FLAG = 1;
|
|
52
|
+
const MAX_CHUNK_INDEX = 4294967295;
|
|
53
|
+
const JOB_RESULT_FORMAT_VERSION = 2;
|
|
54
|
+
const JOB_RESULT_CHUNK_BYTES = 1024 * 1024;
|
|
36
55
|
class JobEnvelopeError extends Error {
|
|
37
56
|
constructor(message) {
|
|
38
57
|
super(message);
|
|
@@ -64,21 +83,68 @@ function requestPlaintext(envelope) {
|
|
|
64
83
|
validateRequestEnvelope(envelope);
|
|
65
84
|
return canonicalJsonBytes(envelope);
|
|
66
85
|
}
|
|
67
|
-
function
|
|
68
|
-
return
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
body: result.body
|
|
76
|
-
})
|
|
77
|
-
);
|
|
86
|
+
function resultMetadata(result) {
|
|
87
|
+
return {
|
|
88
|
+
v: result.v,
|
|
89
|
+
jobId: result.jobId,
|
|
90
|
+
scope: result.scope,
|
|
91
|
+
version: result.version,
|
|
92
|
+
contentType: result.contentType
|
|
93
|
+
};
|
|
78
94
|
}
|
|
79
95
|
function encryptedBytesToBase64(encrypted) {
|
|
80
96
|
return (0, import_encoding.toBase64)((0, import_viem.fromHex)(`0x${(0, import_interface.serializeECIES)(encrypted)}`, "bytes"));
|
|
81
97
|
}
|
|
98
|
+
function encryptedToBytes(encrypted) {
|
|
99
|
+
const bytes = new Uint8Array(
|
|
100
|
+
encrypted.iv.length + encrypted.ephemPublicKey.length + encrypted.ciphertext.length + encrypted.mac.length
|
|
101
|
+
);
|
|
102
|
+
let offset = 0;
|
|
103
|
+
bytes.set(encrypted.iv, offset);
|
|
104
|
+
offset += encrypted.iv.length;
|
|
105
|
+
bytes.set(encrypted.ephemPublicKey, offset);
|
|
106
|
+
offset += encrypted.ephemPublicKey.length;
|
|
107
|
+
bytes.set(encrypted.ciphertext, offset);
|
|
108
|
+
offset += encrypted.ciphertext.length;
|
|
109
|
+
bytes.set(encrypted.mac, offset);
|
|
110
|
+
return bytes;
|
|
111
|
+
}
|
|
112
|
+
function bytesToEncrypted(bytes) {
|
|
113
|
+
const absoluteMinLength = import_constants.FORMAT.IV_LENGTH + 1 + import_constants.MAC.LENGTH + 1;
|
|
114
|
+
if (bytes.length < absoluteMinLength) {
|
|
115
|
+
throw new import_interface.ECIESError(
|
|
116
|
+
`Invalid ECIES data: too short (${bytes.length} bytes, minimum ${absoluteMinLength} bytes required)`,
|
|
117
|
+
"DECRYPTION_FAILED"
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
const prefix = bytes[import_constants.FORMAT.EPHEMERAL_KEY_OFFSET];
|
|
121
|
+
if (prefix !== import_constants.CURVE.PREFIX.UNCOMPRESSED) {
|
|
122
|
+
throw new import_interface.ECIESError(
|
|
123
|
+
`Invalid ephemeral public key: must be uncompressed format (0x04 prefix), got 0x${prefix.toString(16).padStart(2, "0")}`,
|
|
124
|
+
"DECRYPTION_FAILED"
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
const ephemKeySize = import_constants.CURVE.UNCOMPRESSED_PUBLIC_KEY_LENGTH;
|
|
128
|
+
const minLength = import_constants.FORMAT.IV_LENGTH + ephemKeySize + import_constants.MAC.LENGTH + 1;
|
|
129
|
+
if (bytes.length < minLength) {
|
|
130
|
+
throw new import_interface.ECIESError(
|
|
131
|
+
`Invalid ECIES data: too short (${bytes.length} bytes, minimum ${minLength} bytes required)`,
|
|
132
|
+
"DECRYPTION_FAILED"
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
iv: bytes.subarray(import_constants.FORMAT.IV_OFFSET, import_constants.FORMAT.IV_OFFSET + import_constants.FORMAT.IV_LENGTH),
|
|
137
|
+
ephemPublicKey: bytes.subarray(
|
|
138
|
+
import_constants.FORMAT.EPHEMERAL_KEY_OFFSET,
|
|
139
|
+
import_constants.FORMAT.EPHEMERAL_KEY_OFFSET + ephemKeySize
|
|
140
|
+
),
|
|
141
|
+
ciphertext: bytes.subarray(
|
|
142
|
+
import_constants.FORMAT.EPHEMERAL_KEY_OFFSET + ephemKeySize,
|
|
143
|
+
bytes.length - import_constants.MAC.LENGTH
|
|
144
|
+
),
|
|
145
|
+
mac: bytes.subarray(bytes.length - import_constants.MAC.LENGTH)
|
|
146
|
+
};
|
|
147
|
+
}
|
|
82
148
|
function base64ToEncrypted(ciphertext) {
|
|
83
149
|
return (0, import_interface.deserializeECIES)((0, import_viem.toHex)((0, import_encoding.fromBase64)(ciphertext)));
|
|
84
150
|
}
|
|
@@ -179,11 +245,11 @@ function validateRequestEnvelope(value) {
|
|
|
179
245
|
requireString(value.auth, "auth", "job request envelope");
|
|
180
246
|
return value;
|
|
181
247
|
}
|
|
182
|
-
function
|
|
183
|
-
requirePlainObject(value, "
|
|
248
|
+
function validateResultMetadata(value) {
|
|
249
|
+
requirePlainObject(value, "metadata", "job result");
|
|
184
250
|
requireExactKeys(
|
|
185
251
|
value,
|
|
186
|
-
["v", "jobId", "scope", "version", "contentType"
|
|
252
|
+
["v", "jobId", "scope", "version", "contentType"],
|
|
187
253
|
"job result"
|
|
188
254
|
);
|
|
189
255
|
if (value.v !== import_jobs.JOB_PROTOCOL_VERSION) {
|
|
@@ -194,14 +260,222 @@ function validateResult(value) {
|
|
|
194
260
|
for (const field of ["jobId", "scope", "contentType"]) {
|
|
195
261
|
requireString(value[field], field, "job result");
|
|
196
262
|
}
|
|
197
|
-
if (typeof value.body !== "string") {
|
|
198
|
-
throw new JobEnvelopeError("Invalid job result: missing body");
|
|
199
|
-
}
|
|
200
263
|
if (value.version !== null && typeof value.version !== "string") {
|
|
201
264
|
throw new JobEnvelopeError("Invalid job result: missing version");
|
|
202
265
|
}
|
|
203
266
|
return value;
|
|
204
267
|
}
|
|
268
|
+
function validateResult(value) {
|
|
269
|
+
requirePlainObject(value, "result", "job result");
|
|
270
|
+
requireExactKeys(
|
|
271
|
+
value,
|
|
272
|
+
["v", "jobId", "scope", "version", "contentType", "body"],
|
|
273
|
+
"job result"
|
|
274
|
+
);
|
|
275
|
+
validateResultMetadata(resultMetadata(value));
|
|
276
|
+
if (!(value.body instanceof Uint8Array)) {
|
|
277
|
+
throw new JobEnvelopeError("Invalid job result: missing body");
|
|
278
|
+
}
|
|
279
|
+
return value;
|
|
280
|
+
}
|
|
281
|
+
function webCrypto() {
|
|
282
|
+
if (typeof globalThis.crypto === "undefined" || typeof globalThis.crypto.getRandomValues !== "function" || !globalThis.crypto.subtle) {
|
|
283
|
+
throw new JobEnvelopeError(
|
|
284
|
+
"Job result streaming requires the Web Crypto API"
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
return globalThis.crypto;
|
|
288
|
+
}
|
|
289
|
+
function uint32(value) {
|
|
290
|
+
const bytes = new Uint8Array(4);
|
|
291
|
+
new DataView(bytes.buffer).setUint32(0, value);
|
|
292
|
+
return bytes;
|
|
293
|
+
}
|
|
294
|
+
function readUint32(bytes, offset = 0) {
|
|
295
|
+
return new DataView(bytes.buffer, bytes.byteOffset + offset, 4).getUint32(0);
|
|
296
|
+
}
|
|
297
|
+
function wrappedKeyPlaintext(contentKey, noncePrefix, metadataBytes) {
|
|
298
|
+
const plaintext = new Uint8Array(
|
|
299
|
+
WRAPPED_KEY_PLAINTEXT_PREFIX_BYTES + metadataBytes.length
|
|
300
|
+
);
|
|
301
|
+
plaintext.set(contentKey, 0);
|
|
302
|
+
plaintext.set(noncePrefix, AES_KEY_BYTES);
|
|
303
|
+
new DataView(plaintext.buffer).setUint32(
|
|
304
|
+
AES_KEY_BYTES + AES_GCM_NONCE_BYTES,
|
|
305
|
+
metadataBytes.length
|
|
306
|
+
);
|
|
307
|
+
plaintext.set(metadataBytes, WRAPPED_KEY_PLAINTEXT_PREFIX_BYTES);
|
|
308
|
+
return plaintext;
|
|
309
|
+
}
|
|
310
|
+
function parseWrappedKeyPlaintext(plaintext) {
|
|
311
|
+
if (plaintext.length < WRAPPED_KEY_PLAINTEXT_PREFIX_BYTES) {
|
|
312
|
+
throw new JobEnvelopeError("Invalid job result: wrapped key is truncated");
|
|
313
|
+
}
|
|
314
|
+
const metadataLength = readUint32(
|
|
315
|
+
plaintext,
|
|
316
|
+
AES_KEY_BYTES + AES_GCM_NONCE_BYTES
|
|
317
|
+
);
|
|
318
|
+
if (metadataLength > MAX_JOB_RESULT_HEADER_BYTES) {
|
|
319
|
+
throw new JobEnvelopeError(
|
|
320
|
+
`Invalid job result: header exceeds ${MAX_JOB_RESULT_HEADER_BYTES} bytes`
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
if (plaintext.length !== WRAPPED_KEY_PLAINTEXT_PREFIX_BYTES + metadataLength) {
|
|
324
|
+
throw new JobEnvelopeError(
|
|
325
|
+
"Invalid job result: header length exceeds payload"
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
const metadataBytes = plaintext.slice(WRAPPED_KEY_PLAINTEXT_PREFIX_BYTES);
|
|
329
|
+
const metadata = validateResultMetadata(
|
|
330
|
+
parseObject(metadataBytes, "job result header")
|
|
331
|
+
);
|
|
332
|
+
return {
|
|
333
|
+
contentKey: plaintext.slice(0, AES_KEY_BYTES),
|
|
334
|
+
noncePrefix: plaintext.slice(
|
|
335
|
+
AES_KEY_BYTES,
|
|
336
|
+
AES_KEY_BYTES + AES_GCM_NONCE_BYTES
|
|
337
|
+
),
|
|
338
|
+
metadataBytes,
|
|
339
|
+
metadata
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
async function importAesKey(contentKey, usage) {
|
|
343
|
+
return webCrypto().subtle.importKey(
|
|
344
|
+
"raw",
|
|
345
|
+
contentKey,
|
|
346
|
+
{ name: "AES-GCM" },
|
|
347
|
+
false,
|
|
348
|
+
[usage]
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
function chunkNonce(noncePrefix, chunkIndex) {
|
|
352
|
+
if (chunkIndex > MAX_CHUNK_INDEX) {
|
|
353
|
+
throw new JobEnvelopeError("Invalid job result: too many chunks");
|
|
354
|
+
}
|
|
355
|
+
const nonce = noncePrefix.slice();
|
|
356
|
+
const view = new DataView(nonce.buffer, nonce.byteOffset, nonce.byteLength);
|
|
357
|
+
view.setUint32(8, view.getUint32(8) ^ chunkIndex);
|
|
358
|
+
return nonce;
|
|
359
|
+
}
|
|
360
|
+
function chunkAad(metadataBytes, chunkIndex, final, plaintextLength) {
|
|
361
|
+
const aad = new Uint8Array(14 + metadataBytes.length);
|
|
362
|
+
aad[0] = JOB_RESULT_FORMAT_VERSION;
|
|
363
|
+
aad.set(uint32(metadataBytes.length), 1);
|
|
364
|
+
aad.set(metadataBytes, 5);
|
|
365
|
+
aad.set(uint32(chunkIndex), 5 + metadataBytes.length);
|
|
366
|
+
aad[9 + metadataBytes.length] = final ? FINAL_FRAME_FLAG : 0;
|
|
367
|
+
aad.set(uint32(plaintextLength), 10 + metadataBytes.length);
|
|
368
|
+
return aad;
|
|
369
|
+
}
|
|
370
|
+
async function encryptResultChunk(key, noncePrefix, metadataBytes, plaintext, chunkIndex, final) {
|
|
371
|
+
const encrypted = new Uint8Array(
|
|
372
|
+
await webCrypto().subtle.encrypt(
|
|
373
|
+
{
|
|
374
|
+
name: "AES-GCM",
|
|
375
|
+
iv: chunkNonce(noncePrefix, chunkIndex),
|
|
376
|
+
additionalData: chunkAad(
|
|
377
|
+
metadataBytes,
|
|
378
|
+
chunkIndex,
|
|
379
|
+
final,
|
|
380
|
+
plaintext.length
|
|
381
|
+
),
|
|
382
|
+
tagLength: AES_GCM_TAG_BYTES * 8
|
|
383
|
+
},
|
|
384
|
+
key,
|
|
385
|
+
plaintext
|
|
386
|
+
)
|
|
387
|
+
);
|
|
388
|
+
const frame = new Uint8Array(
|
|
389
|
+
FRAME_LENGTH_BYTES + FRAME_FLAGS_BYTES + encrypted.length
|
|
390
|
+
);
|
|
391
|
+
new DataView(frame.buffer).setUint32(0, encrypted.length);
|
|
392
|
+
frame[FRAME_LENGTH_BYTES] = final ? FINAL_FRAME_FLAG : 0;
|
|
393
|
+
frame.set(encrypted, FRAME_LENGTH_BYTES + FRAME_FLAGS_BYTES);
|
|
394
|
+
return frame;
|
|
395
|
+
}
|
|
396
|
+
function verifyResultBindings(metadata, expect) {
|
|
397
|
+
if (metadata.jobId !== expect.jobId) {
|
|
398
|
+
throw new JobEnvelopeError(
|
|
399
|
+
`Job result ID ${metadata.jobId} does not match expected job ID ${expect.jobId}`
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
if (expect.scope !== void 0 && metadata.scope !== expect.scope) {
|
|
403
|
+
throw new JobEnvelopeError(
|
|
404
|
+
`Job result scope ${metadata.scope} does not match expected scope ${expect.scope}`
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
if (expect.version !== void 0 && metadata.version !== expect.version) {
|
|
408
|
+
throw new JobEnvelopeError(
|
|
409
|
+
`Job result version ${String(metadata.version)} does not match expected version ${String(expect.version)}`
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
class ByteStreamReader {
|
|
414
|
+
reader;
|
|
415
|
+
current;
|
|
416
|
+
offset = 0;
|
|
417
|
+
ended = false;
|
|
418
|
+
constructor(stream) {
|
|
419
|
+
this.reader = stream.getReader();
|
|
420
|
+
}
|
|
421
|
+
async ensureBytes() {
|
|
422
|
+
while (!this.current || this.offset === this.current.length) {
|
|
423
|
+
if (this.ended) return false;
|
|
424
|
+
const next = await this.reader.read();
|
|
425
|
+
if (next.done) {
|
|
426
|
+
this.ended = true;
|
|
427
|
+
this.current = void 0;
|
|
428
|
+
return false;
|
|
429
|
+
}
|
|
430
|
+
if (!(next.value instanceof Uint8Array)) {
|
|
431
|
+
throw new JobEnvelopeError(
|
|
432
|
+
"Invalid job result: stream yielded non-byte data"
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
this.current = next.value;
|
|
436
|
+
this.offset = 0;
|
|
437
|
+
}
|
|
438
|
+
return true;
|
|
439
|
+
}
|
|
440
|
+
async readExactOrNull(length) {
|
|
441
|
+
if (!await this.ensureBytes()) return null;
|
|
442
|
+
const bytes = new Uint8Array(length);
|
|
443
|
+
let written = 0;
|
|
444
|
+
while (written < length) {
|
|
445
|
+
if (!await this.ensureBytes()) {
|
|
446
|
+
throw new JobEnvelopeError("Invalid job result: truncated payload");
|
|
447
|
+
}
|
|
448
|
+
const available = this.current.length - this.offset;
|
|
449
|
+
const count = Math.min(available, length - written);
|
|
450
|
+
bytes.set(
|
|
451
|
+
this.current.subarray(this.offset, this.offset + count),
|
|
452
|
+
written
|
|
453
|
+
);
|
|
454
|
+
this.offset += count;
|
|
455
|
+
written += count;
|
|
456
|
+
}
|
|
457
|
+
return bytes;
|
|
458
|
+
}
|
|
459
|
+
async readExact(length) {
|
|
460
|
+
const bytes = await this.readExactOrNull(length);
|
|
461
|
+
if (!bytes) {
|
|
462
|
+
throw new JobEnvelopeError("Invalid job result: truncated payload");
|
|
463
|
+
}
|
|
464
|
+
return bytes;
|
|
465
|
+
}
|
|
466
|
+
async hasRemaining() {
|
|
467
|
+
return this.ensureBytes();
|
|
468
|
+
}
|
|
469
|
+
async cancel(reason) {
|
|
470
|
+
await this.reader.cancel(reason);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
function normalizeChunkError(error) {
|
|
474
|
+
if (error instanceof JobEnvelopeError) return error;
|
|
475
|
+
return new JobEnvelopeError(
|
|
476
|
+
`Invalid job result: chunk authentication failed${error instanceof Error ? `: ${error.message}` : ""}`
|
|
477
|
+
);
|
|
478
|
+
}
|
|
205
479
|
async function sealJobRequest(envelope, enclavePublicKey, ecies) {
|
|
206
480
|
const encrypted = await ecies.encrypt(
|
|
207
481
|
(0, import_viem.fromHex)(enclavePublicKey, "bytes"),
|
|
@@ -218,46 +492,327 @@ async function openJobRequest(ciphertext, privateKey, ecies) {
|
|
|
218
492
|
parseObject(plaintext, "job request envelope")
|
|
219
493
|
);
|
|
220
494
|
}
|
|
221
|
-
async function
|
|
222
|
-
|
|
223
|
-
const
|
|
495
|
+
async function sealJobResultStream(metadataValue, builderPublicKey, ecies) {
|
|
496
|
+
const metadata = validateResultMetadata(metadataValue);
|
|
497
|
+
const metadataBytes = canonicalJsonBytes(metadata);
|
|
498
|
+
if (metadataBytes.length > MAX_JOB_RESULT_HEADER_BYTES) {
|
|
499
|
+
throw new JobEnvelopeError(
|
|
500
|
+
`Invalid job result: header exceeds ${MAX_JOB_RESULT_HEADER_BYTES} bytes`
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
const crypto = webCrypto();
|
|
504
|
+
const contentKey = crypto.getRandomValues(new Uint8Array(AES_KEY_BYTES));
|
|
505
|
+
const noncePrefix = crypto.getRandomValues(
|
|
506
|
+
new Uint8Array(AES_GCM_NONCE_BYTES)
|
|
507
|
+
);
|
|
508
|
+
const key = await importAesKey(contentKey, "encrypt");
|
|
509
|
+
const keyPlaintext = wrappedKeyPlaintext(
|
|
510
|
+
contentKey,
|
|
511
|
+
noncePrefix,
|
|
512
|
+
metadataBytes
|
|
513
|
+
);
|
|
514
|
+
const encryptedKey = await ecies.encrypt(
|
|
224
515
|
(0, import_viem.fromHex)(builderPublicKey, "bytes"),
|
|
225
|
-
|
|
516
|
+
keyPlaintext
|
|
226
517
|
);
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
518
|
+
contentKey.fill(0);
|
|
519
|
+
keyPlaintext.fill(0);
|
|
520
|
+
const wrappedKey = encryptedToBytes(encryptedKey);
|
|
521
|
+
if (wrappedKey.length > MAX_WRAPPED_KEY_BYTES) {
|
|
522
|
+
throw new JobEnvelopeError("Invalid job result: wrapped key is too large");
|
|
523
|
+
}
|
|
524
|
+
const header = new Uint8Array(
|
|
525
|
+
1 + WRAPPED_KEY_LENGTH_BYTES + wrappedKey.length
|
|
526
|
+
);
|
|
527
|
+
header[0] = JOB_RESULT_FORMAT_VERSION;
|
|
528
|
+
new DataView(header.buffer).setUint32(1, wrappedKey.length);
|
|
529
|
+
header.set(wrappedKey, 1 + WRAPPED_KEY_LENGTH_BYTES);
|
|
530
|
+
let pending = new Uint8Array(JOB_RESULT_CHUNK_BYTES);
|
|
531
|
+
let pendingLength = 0;
|
|
532
|
+
let chunkIndex = 0;
|
|
533
|
+
const transform = new TransformStream({
|
|
534
|
+
async transform(chunk, controller) {
|
|
535
|
+
if (!(chunk instanceof Uint8Array)) {
|
|
536
|
+
throw new JobEnvelopeError(
|
|
537
|
+
"Invalid job result: stream yielded non-byte data"
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
let offset = 0;
|
|
541
|
+
while (offset < chunk.length) {
|
|
542
|
+
if (pendingLength === JOB_RESULT_CHUNK_BYTES) {
|
|
543
|
+
controller.enqueue(
|
|
544
|
+
await encryptResultChunk(
|
|
545
|
+
key,
|
|
546
|
+
noncePrefix,
|
|
547
|
+
metadataBytes,
|
|
548
|
+
pending,
|
|
549
|
+
chunkIndex,
|
|
550
|
+
false
|
|
551
|
+
)
|
|
552
|
+
);
|
|
553
|
+
chunkIndex += 1;
|
|
554
|
+
pending = new Uint8Array(JOB_RESULT_CHUNK_BYTES);
|
|
555
|
+
pendingLength = 0;
|
|
556
|
+
}
|
|
557
|
+
const count = Math.min(
|
|
558
|
+
chunk.length - offset,
|
|
559
|
+
JOB_RESULT_CHUNK_BYTES - pendingLength
|
|
560
|
+
);
|
|
561
|
+
pending.set(chunk.subarray(offset, offset + count), pendingLength);
|
|
562
|
+
pendingLength += count;
|
|
563
|
+
offset += count;
|
|
564
|
+
}
|
|
565
|
+
},
|
|
566
|
+
async flush(controller) {
|
|
567
|
+
controller.enqueue(
|
|
568
|
+
await encryptResultChunk(
|
|
569
|
+
key,
|
|
570
|
+
noncePrefix,
|
|
571
|
+
metadataBytes,
|
|
572
|
+
pending.subarray(0, pendingLength),
|
|
573
|
+
chunkIndex,
|
|
574
|
+
true
|
|
575
|
+
)
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
return { header, transform };
|
|
230
580
|
}
|
|
231
|
-
async function
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
581
|
+
async function sealJobResult(result, builderPublicKey, ecies) {
|
|
582
|
+
validateResult(result);
|
|
583
|
+
const sealed = await sealJobResultStream(
|
|
584
|
+
resultMetadata(result),
|
|
585
|
+
builderPublicKey,
|
|
586
|
+
ecies
|
|
235
587
|
);
|
|
236
|
-
const
|
|
237
|
-
|
|
588
|
+
const frameCount = Math.max(
|
|
589
|
+
1,
|
|
590
|
+
Math.ceil(result.body.length / JOB_RESULT_CHUNK_BYTES)
|
|
591
|
+
);
|
|
592
|
+
const size = sealed.header.length + result.body.length + frameCount * (FRAME_LENGTH_BYTES + FRAME_FLAGS_BYTES + AES_GCM_TAG_BYTES);
|
|
593
|
+
const bytes = new Uint8Array(size);
|
|
594
|
+
bytes.set(sealed.header, 0);
|
|
595
|
+
let outputOffset = sealed.header.length;
|
|
596
|
+
const reader = sealed.transform.readable.getReader();
|
|
597
|
+
const drain = (async () => {
|
|
598
|
+
for (; ; ) {
|
|
599
|
+
const next = await reader.read();
|
|
600
|
+
if (next.done) break;
|
|
601
|
+
bytes.set(next.value, outputOffset);
|
|
602
|
+
outputOffset += next.value.length;
|
|
603
|
+
}
|
|
604
|
+
})();
|
|
605
|
+
const writer = sealed.transform.writable.getWriter();
|
|
606
|
+
const fill = (async () => {
|
|
607
|
+
for (let offset = 0; offset < result.body.length; offset += JOB_RESULT_CHUNK_BYTES) {
|
|
608
|
+
await writer.write(
|
|
609
|
+
result.body.subarray(offset, offset + JOB_RESULT_CHUNK_BYTES)
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
await writer.close();
|
|
613
|
+
})();
|
|
614
|
+
await Promise.all([drain, fill]);
|
|
615
|
+
if (outputOffset !== bytes.length) {
|
|
616
|
+
throw new JobEnvelopeError("Invalid job result: sealed size mismatch");
|
|
617
|
+
}
|
|
618
|
+
return { bytes, hash: (0, import_viem.bytesToHex)((0, import_sha2.sha256)(bytes)), size };
|
|
619
|
+
}
|
|
620
|
+
async function openJobResultStream(bytesStream, builderPrivateKey, ecies, expect) {
|
|
621
|
+
const reader = new ByteStreamReader(bytesStream);
|
|
622
|
+
const headerPrefix = await reader.readExact(1 + WRAPPED_KEY_LENGTH_BYTES);
|
|
623
|
+
const formatVersion = headerPrefix[0];
|
|
624
|
+
if (formatVersion !== JOB_RESULT_FORMAT_VERSION) {
|
|
238
625
|
throw new JobEnvelopeError(
|
|
239
|
-
`
|
|
626
|
+
`Unsupported job result format: ${String(formatVersion)}`
|
|
240
627
|
);
|
|
241
628
|
}
|
|
242
|
-
|
|
629
|
+
const wrappedKeyLength = readUint32(headerPrefix, 1);
|
|
630
|
+
if (wrappedKeyLength === 0 || wrappedKeyLength > MAX_WRAPPED_KEY_BYTES) {
|
|
631
|
+
throw new JobEnvelopeError(
|
|
632
|
+
"Invalid job result: invalid wrapped key length"
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
const wrappedKey = await reader.readExact(wrappedKeyLength);
|
|
636
|
+
const keyPlaintext = await ecies.decrypt(
|
|
637
|
+
(0, import_viem.fromHex)(builderPrivateKey, "bytes"),
|
|
638
|
+
bytesToEncrypted(wrappedKey)
|
|
639
|
+
);
|
|
640
|
+
const parsed = parseWrappedKeyPlaintext(keyPlaintext);
|
|
641
|
+
verifyResultBindings(parsed.metadata, expect);
|
|
642
|
+
const key = await importAesKey(parsed.contentKey, "decrypt");
|
|
643
|
+
parsed.contentKey.fill(0);
|
|
644
|
+
keyPlaintext.fill(0);
|
|
645
|
+
let chunkIndex = 0;
|
|
646
|
+
let finished = false;
|
|
647
|
+
const body = new ReadableStream({
|
|
648
|
+
async pull(controller) {
|
|
649
|
+
if (finished) {
|
|
650
|
+
controller.close();
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
try {
|
|
654
|
+
const frameHeader = await reader.readExactOrNull(
|
|
655
|
+
FRAME_LENGTH_BYTES + FRAME_FLAGS_BYTES
|
|
656
|
+
);
|
|
657
|
+
if (!frameHeader) {
|
|
658
|
+
throw new JobEnvelopeError("Invalid job result: missing final chunk");
|
|
659
|
+
}
|
|
660
|
+
const encryptedLength = readUint32(frameHeader);
|
|
661
|
+
if (encryptedLength < AES_GCM_TAG_BYTES || encryptedLength > JOB_RESULT_CHUNK_BYTES + AES_GCM_TAG_BYTES) {
|
|
662
|
+
throw new JobEnvelopeError(
|
|
663
|
+
"Invalid job result: invalid chunk length"
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
const flags = frameHeader[FRAME_LENGTH_BYTES];
|
|
667
|
+
if (flags !== 0 && flags !== FINAL_FRAME_FLAG) {
|
|
668
|
+
throw new JobEnvelopeError("Invalid job result: invalid chunk flags");
|
|
669
|
+
}
|
|
670
|
+
const final = flags === FINAL_FRAME_FLAG;
|
|
671
|
+
const plaintextLength = encryptedLength - AES_GCM_TAG_BYTES;
|
|
672
|
+
if (!final && plaintextLength !== JOB_RESULT_CHUNK_BYTES) {
|
|
673
|
+
throw new JobEnvelopeError(
|
|
674
|
+
"Invalid job result: short non-final chunk"
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
const encrypted = await reader.readExact(encryptedLength);
|
|
678
|
+
const plaintext = new Uint8Array(
|
|
679
|
+
await webCrypto().subtle.decrypt(
|
|
680
|
+
{
|
|
681
|
+
name: "AES-GCM",
|
|
682
|
+
iv: chunkNonce(parsed.noncePrefix, chunkIndex),
|
|
683
|
+
additionalData: chunkAad(
|
|
684
|
+
parsed.metadataBytes,
|
|
685
|
+
chunkIndex,
|
|
686
|
+
final,
|
|
687
|
+
plaintextLength
|
|
688
|
+
),
|
|
689
|
+
tagLength: AES_GCM_TAG_BYTES * 8
|
|
690
|
+
},
|
|
691
|
+
key,
|
|
692
|
+
encrypted
|
|
693
|
+
)
|
|
694
|
+
);
|
|
695
|
+
if (plaintext.length !== plaintextLength) {
|
|
696
|
+
throw new JobEnvelopeError(
|
|
697
|
+
"Invalid job result: decrypted chunk length mismatch"
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
chunkIndex += 1;
|
|
701
|
+
if (final) {
|
|
702
|
+
if (await reader.hasRemaining()) {
|
|
703
|
+
throw new JobEnvelopeError(
|
|
704
|
+
"Invalid job result: bytes follow final chunk"
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
finished = true;
|
|
708
|
+
}
|
|
709
|
+
controller.enqueue(plaintext);
|
|
710
|
+
if (finished) controller.close();
|
|
711
|
+
} catch (error) {
|
|
712
|
+
controller.error(normalizeChunkError(error));
|
|
713
|
+
void reader.cancel(error).catch(() => void 0);
|
|
714
|
+
}
|
|
715
|
+
},
|
|
716
|
+
cancel(reason) {
|
|
717
|
+
return reader.cancel(reason);
|
|
718
|
+
}
|
|
719
|
+
});
|
|
720
|
+
return { metadata: parsed.metadata, body };
|
|
721
|
+
}
|
|
722
|
+
function bufferedResultBodySize(sealedBytes) {
|
|
723
|
+
if (sealedBytes.length < 1 + WRAPPED_KEY_LENGTH_BYTES) {
|
|
724
|
+
throw new JobEnvelopeError("Invalid job result: truncated payload");
|
|
725
|
+
}
|
|
726
|
+
if (sealedBytes[0] !== JOB_RESULT_FORMAT_VERSION) {
|
|
243
727
|
throw new JobEnvelopeError(
|
|
244
|
-
`
|
|
728
|
+
`Unsupported job result format: ${String(sealedBytes[0])}`
|
|
245
729
|
);
|
|
246
730
|
}
|
|
247
|
-
|
|
731
|
+
const wrappedKeyLength = readUint32(sealedBytes, 1);
|
|
732
|
+
if (wrappedKeyLength === 0 || wrappedKeyLength > MAX_WRAPPED_KEY_BYTES) {
|
|
248
733
|
throw new JobEnvelopeError(
|
|
249
|
-
|
|
734
|
+
"Invalid job result: invalid wrapped key length"
|
|
250
735
|
);
|
|
251
736
|
}
|
|
252
|
-
|
|
737
|
+
let offset = 1 + WRAPPED_KEY_LENGTH_BYTES + wrappedKeyLength;
|
|
738
|
+
if (offset > sealedBytes.length) {
|
|
739
|
+
throw new JobEnvelopeError("Invalid job result: truncated payload");
|
|
740
|
+
}
|
|
741
|
+
let bodySize = 0;
|
|
742
|
+
let sawFinal = false;
|
|
743
|
+
while (offset < sealedBytes.length) {
|
|
744
|
+
if (sealedBytes.length - offset < FRAME_LENGTH_BYTES + FRAME_FLAGS_BYTES) {
|
|
745
|
+
throw new JobEnvelopeError("Invalid job result: truncated payload");
|
|
746
|
+
}
|
|
747
|
+
const encryptedLength = readUint32(sealedBytes, offset);
|
|
748
|
+
const flags = sealedBytes[offset + FRAME_LENGTH_BYTES];
|
|
749
|
+
if (encryptedLength < AES_GCM_TAG_BYTES || encryptedLength > JOB_RESULT_CHUNK_BYTES + AES_GCM_TAG_BYTES || flags !== 0 && flags !== FINAL_FRAME_FLAG) {
|
|
750
|
+
throw new JobEnvelopeError("Invalid job result: invalid chunk framing");
|
|
751
|
+
}
|
|
752
|
+
const plaintextLength = encryptedLength - AES_GCM_TAG_BYTES;
|
|
753
|
+
if (flags === 0 && plaintextLength !== JOB_RESULT_CHUNK_BYTES) {
|
|
754
|
+
throw new JobEnvelopeError("Invalid job result: short non-final chunk");
|
|
755
|
+
}
|
|
756
|
+
offset += FRAME_LENGTH_BYTES + FRAME_FLAGS_BYTES;
|
|
757
|
+
if (encryptedLength > sealedBytes.length - offset) {
|
|
758
|
+
throw new JobEnvelopeError("Invalid job result: truncated payload");
|
|
759
|
+
}
|
|
760
|
+
offset += encryptedLength;
|
|
761
|
+
bodySize += plaintextLength;
|
|
762
|
+
if (flags === FINAL_FRAME_FLAG) {
|
|
763
|
+
sawFinal = true;
|
|
764
|
+
if (offset !== sealedBytes.length) {
|
|
765
|
+
throw new JobEnvelopeError(
|
|
766
|
+
"Invalid job result: bytes follow final chunk"
|
|
767
|
+
);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
if (!sawFinal) {
|
|
772
|
+
throw new JobEnvelopeError("Invalid job result: missing final chunk");
|
|
773
|
+
}
|
|
774
|
+
return bodySize;
|
|
775
|
+
}
|
|
776
|
+
async function openJobResult(sealedBytes, builderPrivateKey, ecies, expect) {
|
|
777
|
+
const bodySize = bufferedResultBodySize(sealedBytes);
|
|
778
|
+
const stream = new ReadableStream({
|
|
779
|
+
start(controller) {
|
|
780
|
+
controller.enqueue(sealedBytes);
|
|
781
|
+
controller.close();
|
|
782
|
+
}
|
|
783
|
+
});
|
|
784
|
+
const opened = await openJobResultStream(
|
|
785
|
+
stream,
|
|
786
|
+
builderPrivateKey,
|
|
787
|
+
ecies,
|
|
788
|
+
expect
|
|
789
|
+
);
|
|
790
|
+
const body = new Uint8Array(bodySize);
|
|
791
|
+
let offset = 0;
|
|
792
|
+
await opened.body.pipeTo(
|
|
793
|
+
new WritableStream({
|
|
794
|
+
write(chunk) {
|
|
795
|
+
body.set(chunk, offset);
|
|
796
|
+
offset += chunk.length;
|
|
797
|
+
}
|
|
798
|
+
})
|
|
799
|
+
);
|
|
800
|
+
if (offset !== body.length) {
|
|
801
|
+
throw new JobEnvelopeError("Invalid job result: opened size mismatch");
|
|
802
|
+
}
|
|
803
|
+
return { ...opened.metadata, body };
|
|
253
804
|
}
|
|
254
805
|
// Annotate the CommonJS export names for ESM import in node:
|
|
255
806
|
0 && (module.exports = {
|
|
807
|
+
JOB_RESULT_CHUNK_BYTES,
|
|
808
|
+
JOB_RESULT_FORMAT_VERSION,
|
|
256
809
|
JobEnvelopeError,
|
|
257
810
|
canonicalJobRequestBytes,
|
|
258
811
|
openJobRequest,
|
|
259
812
|
openJobResult,
|
|
813
|
+
openJobResultStream,
|
|
260
814
|
sealJobRequest,
|
|
261
|
-
sealJobResult
|
|
815
|
+
sealJobResult,
|
|
816
|
+
sealJobResultStream
|
|
262
817
|
});
|
|
263
818
|
//# sourceMappingURL=job.cjs.map
|