@opendatalabs/vana-sdk 3.20.1 → 3.22.0-pr.207.0a5d07b

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.
Files changed (69) hide show
  1. package/README.md +64 -0
  2. package/dist/crypto/envelope/job.cjs +263 -0
  3. package/dist/crypto/envelope/job.cjs.map +1 -0
  4. package/dist/crypto/envelope/job.d.ts +122 -0
  5. package/dist/crypto/envelope/job.js +240 -0
  6. package/dist/crypto/envelope/job.js.map +1 -0
  7. package/dist/direct/access-request-client.cjs +100 -2
  8. package/dist/direct/access-request-client.cjs.map +1 -1
  9. package/dist/direct/access-request-client.d.ts +23 -1
  10. package/dist/direct/access-request-client.js +98 -1
  11. package/dist/direct/access-request-client.js.map +1 -1
  12. package/dist/direct/controller.cjs +4 -0
  13. package/dist/direct/controller.cjs.map +1 -1
  14. package/dist/direct/controller.d.ts +10 -1
  15. package/dist/direct/controller.js +6 -1
  16. package/dist/direct/controller.js.map +1 -1
  17. package/dist/direct/types.cjs.map +1 -1
  18. package/dist/direct/types.d.ts +45 -0
  19. package/dist/direct/types.js.map +1 -1
  20. package/dist/errors.cjs +77 -0
  21. package/dist/errors.cjs.map +1 -1
  22. package/dist/errors.d.ts +123 -0
  23. package/dist/errors.js +67 -0
  24. package/dist/errors.js.map +1 -1
  25. package/dist/index.browser.d.ts +5 -1
  26. package/dist/index.browser.js +721 -24
  27. package/dist/index.browser.js.map +4 -4
  28. package/dist/index.node.cjs +1309 -107
  29. package/dist/index.node.cjs.map +4 -4
  30. package/dist/index.node.d.ts +6 -1
  31. package/dist/index.node.js +1261 -97
  32. package/dist/index.node.js.map +4 -4
  33. package/dist/protocol/derivative-questions.cjs +22 -0
  34. package/dist/protocol/derivative-questions.cjs.map +1 -1
  35. package/dist/protocol/derivative-questions.d.ts +53 -0
  36. package/dist/protocol/derivative-questions.js +19 -0
  37. package/dist/protocol/derivative-questions.js.map +1 -1
  38. package/dist/protocol/derivative-status.cjs +209 -0
  39. package/dist/protocol/derivative-status.cjs.map +1 -0
  40. package/dist/protocol/derivative-status.d.ts +196 -0
  41. package/dist/protocol/derivative-status.js +190 -0
  42. package/dist/protocol/derivative-status.js.map +1 -0
  43. package/dist/protocol/derivative-status.test.d.ts +1 -0
  44. package/dist/protocol/identity.cjs +219 -0
  45. package/dist/protocol/identity.cjs.map +1 -0
  46. package/dist/protocol/identity.d.ts +193 -0
  47. package/dist/protocol/identity.js +183 -0
  48. package/dist/protocol/identity.js.map +1 -0
  49. package/dist/protocol/identity.test.d.ts +1 -0
  50. package/dist/protocol/identity.vector.test.d.ts +1 -0
  51. package/dist/protocol/jobs-client.cjs +493 -0
  52. package/dist/protocol/jobs-client.cjs.map +1 -0
  53. package/dist/protocol/jobs-client.d.ts +167 -0
  54. package/dist/protocol/jobs-client.js +498 -0
  55. package/dist/protocol/jobs-client.js.map +1 -0
  56. package/dist/protocol/jobs-client.test.d.ts +1 -0
  57. package/dist/protocol/jobs.cjs +67 -0
  58. package/dist/protocol/jobs.cjs.map +1 -0
  59. package/dist/protocol/jobs.d.ts +185 -0
  60. package/dist/protocol/jobs.js +33 -0
  61. package/dist/protocol/jobs.js.map +1 -0
  62. package/dist/protocol/jobs.test.d.ts +1 -0
  63. package/dist/server.cjs +4 -2
  64. package/dist/server.cjs.map +1 -1
  65. package/dist/server.d.ts +2 -2
  66. package/dist/server.js +4 -2
  67. package/dist/server.js.map +1 -1
  68. package/dist/tests/mock-personal-server.d.ts +9 -0
  69. package/package.json +7 -1
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Builder-side client for encrypted Gateway jobs.
3
+ *
4
+ * @remarks
5
+ * This Node-only client fetches an owner's sealed enclave identity, signs and
6
+ * seals raw-read work, submits it to the Gateway, polls builder-visible state,
7
+ * and decrypts the bound result with the builder private key.
8
+ *
9
+ * @category Protocol
10
+ */
11
+ import { type Address, type Hex, type LocalAccount } from "viem";
12
+ import type { ECIESProvider } from "../crypto/ecies/interface.js";
13
+ import { type JobResult, type JobState, type JobStatus } from "./jobs.js";
14
+ import { type WriteSignerSource } from "./write-signer.js";
15
+ /** A viem local account that exposes the public key required by job requests. */
16
+ export type JobsBuilderAccount = Extract<LocalAccount, WriteSignerSource>;
17
+ /** Configuration captured by {@link createJobsClient}. */
18
+ export interface JobsClientOptions {
19
+ /** Gateway base URL; its origin is the Web3Signed audience. */
20
+ gatewayUrl: string;
21
+ /** Vana chain ID used by the owner identity lookup. */
22
+ chainId: number;
23
+ /** Raw builder private key, used for signing and result decryption. */
24
+ builderPrivateKey?: Hex;
25
+ /**
26
+ * viem local account used for signing. Its public key supports submission,
27
+ * but without `builderPrivateKey` this client cannot decrypt results:
28
+ * `openResult` and `readRaw` reject while submit/status/wait remain usable.
29
+ */
30
+ builderAccount?: JobsBuilderAccount;
31
+ /** HTTP implementation; defaults to `globalThis.fetch`. */
32
+ fetch?: typeof fetch;
33
+ /** ECIES implementation; defaults to the Node provider. */
34
+ ecies?: ECIESProvider;
35
+ /** Clock used when constructing a job deadline. */
36
+ now?: () => Date;
37
+ }
38
+ /** Parameters for one encrypted raw-read submission. */
39
+ export interface SubmitRawReadParams {
40
+ /** Owner whose enclave executes the read. */
41
+ owner: Address;
42
+ /** Owner-issued builder grant. */
43
+ grantId: Hex;
44
+ /** Data scope to read. */
45
+ scope: string;
46
+ /** Exact record version to require, or `null` for an unpinned read. */
47
+ pinnedVersion?: string | null;
48
+ /** Requested deadline offset, clamped to the protocol maximum. */
49
+ deadlineSeconds?: number;
50
+ /** Gateway inline-wait seconds, clamped to `0..MAX_WAIT_SECONDS`. */
51
+ wait?: number;
52
+ }
53
+ /** Result of submitting an encrypted raw-read job. */
54
+ export interface SubmitRawReadResult {
55
+ /** Client-generated job UUID. */
56
+ jobId: string;
57
+ /** Current Gateway state. */
58
+ state: JobState;
59
+ /** Full status when the Gateway returned an inline 200 response. */
60
+ job?: JobStatus;
61
+ }
62
+ /** Polling controls for {@link JobsClient.waitForJob}. */
63
+ export interface WaitForJobOptions {
64
+ /** Total polling budget in milliseconds. */
65
+ timeoutMs?: number;
66
+ /** Delay between reads; values below `CLAIM_POLL_FLOOR_MS` are raised. */
67
+ pollMs?: number;
68
+ }
69
+ /** Expected plaintext bindings for {@link JobsClient.openResult}. */
70
+ export interface OpenJobResultOptions {
71
+ /** Bind the plaintext to this job id. */
72
+ expect: {
73
+ jobId: string;
74
+ scope?: string;
75
+ version?: string | null;
76
+ };
77
+ }
78
+ /** Parameters for the submit, wait, and decrypt convenience flow. */
79
+ export interface ReadRawParams extends SubmitRawReadParams, WaitForJobOptions {
80
+ }
81
+ /** Builder operations exposed by {@link createJobsClient}. */
82
+ export interface JobsClient {
83
+ /**
84
+ * Encrypt and submit a raw-read job.
85
+ *
86
+ * @param params - Owner, grant, scope, deadline, and inline-wait controls.
87
+ * @returns The job id and current state, plus inline status on HTTP 200.
88
+ * @throws {OwnerNotReadyError} When the owner identity is not sealed.
89
+ * @throws {JobsClientError} When the Gateway or transport rejects the call.
90
+ */
91
+ submitRawRead(params: SubmitRawReadParams): Promise<SubmitRawReadResult>;
92
+ /**
93
+ * Read one builder-visible job status.
94
+ *
95
+ * @param jobId - Client-generated job UUID.
96
+ * @returns The current job status.
97
+ * @throws {JobNotFoundError} When the job is absent or belongs to another builder.
98
+ * @throws {JobsClientError} For other Gateway or transport failures.
99
+ */
100
+ getJob(jobId: string): Promise<JobStatus>;
101
+ /**
102
+ * Poll until a job completes, fails, expires, or is cancelled.
103
+ *
104
+ * @param jobId - Job UUID to poll.
105
+ * @param options - Timeout and poll cadence.
106
+ * @returns The terminal job status.
107
+ * @throws {JobTimeoutError} When the caller or job deadline is exhausted.
108
+ */
109
+ waitForJob(jobId: string, options?: WaitForJobOptions): Promise<JobStatus>;
110
+ /**
111
+ * Decrypt and validate an inline job result.
112
+ *
113
+ * @param job - Terminal job status containing `resultCiphertext`.
114
+ * @param options - Expected plaintext job, scope, and version bindings.
115
+ * @returns The validated plaintext job result.
116
+ * @remarks Decryption requires `builderPrivateKey`; `builderAccount` alone
117
+ * only supports submission, status reads, and waiting.
118
+ * @throws {JobRejectedError} When ciphertext or a raw private key is unavailable.
119
+ * @throws {JobEnvelopeError} When decrypted protocol fields or bindings differ.
120
+ */
121
+ openResult(job: JobStatus, options: OpenJobResultOptions): Promise<JobResult>;
122
+ /**
123
+ * Submit, wait for, and decrypt one raw read.
124
+ *
125
+ * @param params - Raw-read request plus polling controls.
126
+ * @returns The decrypted, binding-checked job result.
127
+ * @remarks This convenience flow decrypts the result and therefore requires
128
+ * `builderPrivateKey`; a client configured only with `builderAccount`
129
+ * cannot call `readRaw`.
130
+ * @throws {JobsClientError} When submission, polling, or decryption setup fails.
131
+ * @throws {JobEnvelopeError} When decrypted result bindings differ.
132
+ *
133
+ * @example
134
+ * ```typescript
135
+ * const result = await client.readRaw({
136
+ * owner,
137
+ * grantId,
138
+ * scope: "profile.email",
139
+ * wait: 25,
140
+ * });
141
+ * ```
142
+ */
143
+ readRaw(params: ReadRawParams): Promise<JobResult>;
144
+ }
145
+ /**
146
+ * Create a reusable Node builder client for encrypted Gateway jobs.
147
+ *
148
+ * @param options - Gateway, chain, builder signer, and injectable adapters.
149
+ * @returns A client bound to the configured builder and Gateway.
150
+ * @throws {JobRejectedError} When configuration or signer shape is invalid.
151
+ *
152
+ * @remarks
153
+ * This client is Node-only. `builderPrivateKey` supports the complete flow;
154
+ * `builderAccount` alone supports submit/status/wait but cannot decrypt, so
155
+ * `openResult` and `readRaw` require the raw private key.
156
+ *
157
+ * @example
158
+ * ```typescript
159
+ * const client = createJobsClient({
160
+ * gatewayUrl: "https://gateway.example.com",
161
+ * chainId: 14800,
162
+ * builderPrivateKey: process.env.BUILDER_PRIVATE_KEY as Hex,
163
+ * });
164
+ * const result = await client.readRaw({ owner, grantId, scope: "profile" });
165
+ * ```
166
+ */
167
+ export declare function createJobsClient(options: JobsClientOptions): JobsClient;
@@ -0,0 +1,498 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import {
3
+ isAddress,
4
+ isHex
5
+ } from "viem";
6
+ import { privateKeyToAccount, publicKeyToAddress } from "viem/accounts";
7
+ import {
8
+ buildWeb3SignedHeader,
9
+ computeBodyHash
10
+ } from "../auth/web3-signed-builder.js";
11
+ import { NodeECIESUint8Provider } from "../crypto/ecies/node.js";
12
+ import {
13
+ canonicalJobRequestBytes,
14
+ openJobResult,
15
+ sealJobRequest
16
+ } from "../crypto/envelope/job.js";
17
+ import {
18
+ BuilderUnknownError,
19
+ GrantInvalidError,
20
+ JobIdTakenError,
21
+ JobNotFoundError,
22
+ JobRejectedError,
23
+ JobRequestTooLargeError,
24
+ JobTimeoutError,
25
+ JobTransportError,
26
+ OwnerNotReadyError
27
+ } from "../errors.js";
28
+ import {
29
+ CLAIM_POLL_FLOOR_MS,
30
+ DEFAULT_JOB_DEADLINE_SECONDS,
31
+ JOB_OPERATIONS,
32
+ JOB_PROTOCOL_VERSION,
33
+ JOB_STATES,
34
+ MAX_INLINE_RESULT_BYTES,
35
+ MAX_JOB_DEADLINE_SECONDS,
36
+ MAX_WAIT_SECONDS
37
+ } from "./jobs.js";
38
+ import { resolveWriteSigner } from "./write-signer.js";
39
+ const IDENTITY_PATH = "/v1/identity";
40
+ const JOBS_PATH = "/v1/jobs";
41
+ const JOB_EXECUTE_PATH = "/v1/jobs/execute";
42
+ const AUTHORIZATION_HEADER = "Authorization";
43
+ const CONTENT_TYPE_HEADER = "Content-Type";
44
+ const JSON_CONTENT_TYPE = "application/json";
45
+ const GET_METHOD = "GET";
46
+ const POST_METHOD = "POST";
47
+ const RAW_READ_OPERATION = "raw_read";
48
+ const SEALED_IDENTITY_STATE = "sealed";
49
+ const HTTP_OK = 200;
50
+ const HTTP_ACCEPTED = 202;
51
+ const HTTP_FORBIDDEN = 403;
52
+ const HTTP_NOT_FOUND = 404;
53
+ const HTTP_CONFLICT = 409;
54
+ const HTTP_PAYLOAD_TOO_LARGE = 413;
55
+ const MILLISECONDS_PER_SECOND = 1e3;
56
+ const DEFAULT_JOB_TIMEOUT_MS = 12e4;
57
+ const DEFAULT_JOB_POLL_MS = CLAIM_POLL_FLOOR_MS;
58
+ const TERMINAL_JOB_STATES = /* @__PURE__ */ new Set([
59
+ "completed",
60
+ "failed",
61
+ "expired",
62
+ "cancelled"
63
+ ]);
64
+ function isRecord(value) {
65
+ return value !== null && typeof value === "object" && !Array.isArray(value);
66
+ }
67
+ function normalizeGateway(gatewayUrl) {
68
+ try {
69
+ const url = new URL(gatewayUrl);
70
+ if (url.pathname !== "/" || url.search !== "" || url.hash !== "") {
71
+ throw new JobRejectedError(
72
+ "gatewayUrl must be an origin without a path, query, or fragment"
73
+ );
74
+ }
75
+ return {
76
+ baseUrl: url.origin,
77
+ audience: url.origin
78
+ };
79
+ } catch (error) {
80
+ if (error instanceof JobRejectedError) throw error;
81
+ throw new JobRejectedError(
82
+ "gatewayUrl must be an absolute URL",
83
+ void 0,
84
+ null,
85
+ {
86
+ cause: error instanceof Error ? error.message : String(error)
87
+ }
88
+ );
89
+ }
90
+ }
91
+ function resolveFetch(fetchFn) {
92
+ const resolved = fetchFn ?? globalThis.fetch;
93
+ if (resolved === void 0) {
94
+ throw new JobRejectedError("No fetch implementation available");
95
+ }
96
+ return resolved;
97
+ }
98
+ function sourcePublicKey(source) {
99
+ if (isRecord(source) && typeof source["publicKey"] === "string") {
100
+ return source["publicKey"];
101
+ }
102
+ return void 0;
103
+ }
104
+ function resolveBuilder(options) {
105
+ if (options.builderPrivateKey && options.builderAccount) {
106
+ throw new JobRejectedError(
107
+ "Provide builderPrivateKey or builderAccount, not both"
108
+ );
109
+ }
110
+ let source;
111
+ try {
112
+ source = options.builderPrivateKey ? privateKeyToAccount(options.builderPrivateKey) : options.builderAccount;
113
+ } catch (error) {
114
+ throw new JobRejectedError("Invalid builder private key", void 0, null, {
115
+ cause: error instanceof Error ? error.message : String(error)
116
+ });
117
+ }
118
+ if (!source) {
119
+ throw new JobRejectedError(
120
+ "builderPrivateKey or builderAccount is required"
121
+ );
122
+ }
123
+ let signer;
124
+ try {
125
+ signer = resolveWriteSigner(source);
126
+ } catch (error) {
127
+ throw new JobRejectedError("Invalid builder signer", void 0, null, {
128
+ cause: error instanceof Error ? error.message : String(error)
129
+ });
130
+ }
131
+ const publicKey = sourcePublicKey(source);
132
+ if (!signer.address || !publicKey) {
133
+ throw new JobRejectedError(
134
+ "Builder signer must expose both address and publicKey"
135
+ );
136
+ }
137
+ return {
138
+ builder: {
139
+ address: signer.address,
140
+ publicKey,
141
+ signMessage: signer.signMessage
142
+ },
143
+ ...options.builderPrivateKey ? { privateKey: options.builderPrivateKey } : {}
144
+ };
145
+ }
146
+ function clampInteger(value, fallback, minimum, maximum, field) {
147
+ const resolved = value ?? fallback;
148
+ if (!Number.isFinite(resolved)) {
149
+ throw new JobRejectedError(`${field} must be a finite number`);
150
+ }
151
+ return Math.min(maximum, Math.max(minimum, Math.trunc(resolved)));
152
+ }
153
+ async function responseBody(response) {
154
+ try {
155
+ return await response.json();
156
+ } catch {
157
+ return null;
158
+ }
159
+ }
160
+ function gatewayErrorBody(body, fallback) {
161
+ if (!isRecord(body)) return { code: null, message: fallback };
162
+ const code = typeof body["code"] === "string" ? body["code"] : null;
163
+ const message = typeof body["error"] === "string" ? body["error"] : typeof body["message"] === "string" ? body["message"] : fallback;
164
+ const details = isRecord(body["details"]) ? body["details"] : void 0;
165
+ return { code, message, ...details ? { details } : {} };
166
+ }
167
+ async function rejectedResponse(response, fallback) {
168
+ const parsed = gatewayErrorBody(await responseBody(response), fallback);
169
+ const details = parsed.details;
170
+ if (response.status === HTTP_FORBIDDEN) {
171
+ if (parsed.code === "BUILDER_UNKNOWN") {
172
+ return new BuilderUnknownError(parsed.message, details);
173
+ }
174
+ if (parsed.code === "GRANT_INVALID") {
175
+ return new GrantInvalidError(parsed.message, details);
176
+ }
177
+ if (parsed.code === "OWNER_NOT_READY") {
178
+ return new OwnerNotReadyError(parsed.message, details);
179
+ }
180
+ }
181
+ if (response.status === HTTP_NOT_FOUND) {
182
+ return new JobNotFoundError(parsed.message, details);
183
+ }
184
+ if (response.status === HTTP_CONFLICT && parsed.code === "JOB_ID_TAKEN") {
185
+ return new JobIdTakenError(parsed.message, details);
186
+ }
187
+ if (response.status === HTTP_PAYLOAD_TOO_LARGE) {
188
+ return new JobRequestTooLargeError(parsed.message, parsed.code, details);
189
+ }
190
+ return new JobRejectedError(
191
+ parsed.message,
192
+ response.status,
193
+ parsed.code,
194
+ details
195
+ );
196
+ }
197
+ function requireStatus(value, status) {
198
+ if (!isRecord(value) || !isNonEmptyString(value["jobId"]) || typeof value["state"] !== "string" || !JOB_STATES.includes(value["state"]) || !isNonEmptyString(value["owner"]) || !isNonEmptyString(value["grantId"]) || !isNonEmptyString(value["scope"]) || !isNonEmptyString(value["operation"]) || !JOB_OPERATIONS.includes(value["operation"])) {
199
+ throw new JobRejectedError(
200
+ "Gateway response did not contain a job status",
201
+ status
202
+ );
203
+ }
204
+ return value;
205
+ }
206
+ function isNonEmptyString(value) {
207
+ return typeof value === "string" && value.length > 0;
208
+ }
209
+ function hasMatchingIdentityKey(value) {
210
+ const address = value["address"];
211
+ const publicKey = value["publicKey"];
212
+ if (typeof address !== "string" || !isAddress(address) || typeof publicKey !== "string" || !isHex(publicKey)) {
213
+ return false;
214
+ }
215
+ try {
216
+ return publicKeyToAddress(publicKey).toLowerCase() === address.toLowerCase();
217
+ } catch {
218
+ return false;
219
+ }
220
+ }
221
+ function jobDeadline(job) {
222
+ const candidate = job;
223
+ for (const field of ["deadlineAt", "deadline"]) {
224
+ if (typeof candidate[field] === "string") {
225
+ const parsed = Date.parse(candidate[field]);
226
+ if (Number.isFinite(parsed)) return parsed;
227
+ }
228
+ }
229
+ return void 0;
230
+ }
231
+ function sleep(ms) {
232
+ return new Promise((resolve) => setTimeout(resolve, ms));
233
+ }
234
+ function requireFiniteDuration(value, field) {
235
+ if (!Number.isFinite(value)) {
236
+ throw new JobRejectedError(`${field} must be a finite number`);
237
+ }
238
+ return Math.max(0, value);
239
+ }
240
+ function createJobsClient(options) {
241
+ const { baseUrl, audience } = normalizeGateway(options.gatewayUrl);
242
+ const fetchFn = resolveFetch(options.fetch);
243
+ const { builder, privateKey } = resolveBuilder(options);
244
+ const ecies = options.ecies ?? new NodeECIESUint8Provider();
245
+ const now = options.now ?? (() => /* @__PURE__ */ new Date());
246
+ async function request(url, init, description) {
247
+ try {
248
+ return await fetchFn(url, init);
249
+ } catch (error) {
250
+ throw new JobTransportError(
251
+ `${description} failed: ${error instanceof Error ? error.message : String(error)}`,
252
+ error
253
+ );
254
+ }
255
+ }
256
+ async function getIdentity(owner) {
257
+ const query = new URLSearchParams({
258
+ owner,
259
+ chainId: String(options.chainId)
260
+ });
261
+ const response = await request(
262
+ `${baseUrl}${IDENTITY_PATH}?${query.toString()}`,
263
+ { method: GET_METHOD },
264
+ "Owner identity lookup"
265
+ );
266
+ if (!response.ok) {
267
+ throw await rejectedResponse(response, "Owner identity lookup rejected");
268
+ }
269
+ const body = await responseBody(response);
270
+ if (!isRecord(body) || body["state"] !== SEALED_IDENTITY_STATE || !isRecord(body["identity"])) {
271
+ throw new OwnerNotReadyError("Owner identity is not sealed", {
272
+ state: isRecord(body) ? body["state"] : void 0
273
+ });
274
+ }
275
+ if (!hasMatchingIdentityKey(body["identity"])) {
276
+ throw new JobRejectedError(
277
+ "Gateway returned a malformed or mismatched owner identity",
278
+ response.status
279
+ );
280
+ }
281
+ return body["identity"];
282
+ }
283
+ const client = {
284
+ async submitRawRead(params) {
285
+ const identity = await getIdentity(params.owner);
286
+ const deadlineSeconds = clampInteger(
287
+ params.deadlineSeconds,
288
+ DEFAULT_JOB_DEADLINE_SECONDS,
289
+ 1,
290
+ MAX_JOB_DEADLINE_SECONDS,
291
+ "deadlineSeconds"
292
+ );
293
+ const wait = clampInteger(params.wait, 0, 0, MAX_WAIT_SECONDS, "wait");
294
+ const jobId = randomUUID();
295
+ const nowMs = now().getTime();
296
+ if (!Number.isFinite(nowMs)) {
297
+ throw new JobRejectedError("now must return a valid Date");
298
+ }
299
+ const deadline = new Date(
300
+ nowMs + deadlineSeconds * MILLISECONDS_PER_SECOND
301
+ ).toISOString();
302
+ const requestBody = {
303
+ v: JOB_PROTOCOL_VERSION,
304
+ jobId,
305
+ owner: params.owner,
306
+ builder: builder.address,
307
+ builderPublicKey: builder.publicKey,
308
+ grantId: params.grantId,
309
+ scope: params.scope,
310
+ operation: RAW_READ_OPERATION,
311
+ pinnedVersion: params.pinnedVersion ?? null,
312
+ deadline
313
+ };
314
+ const auth = await buildWeb3SignedHeader({
315
+ signMessage: builder.signMessage,
316
+ aud: audience,
317
+ method: POST_METHOD,
318
+ uri: JOB_EXECUTE_PATH,
319
+ bodyHash: computeBodyHash(canonicalJobRequestBytes(requestBody)),
320
+ nonce: randomUUID()
321
+ });
322
+ const requestCiphertext = await sealJobRequest(
323
+ { request: requestBody, auth },
324
+ identity.publicKey,
325
+ ecies
326
+ );
327
+ const submission = {
328
+ owner: requestBody.owner,
329
+ grantId: requestBody.grantId,
330
+ scope: requestBody.scope,
331
+ operation: requestBody.operation,
332
+ idempotencyKey: randomUUID(),
333
+ jobId,
334
+ deadline,
335
+ requestCiphertext
336
+ };
337
+ const bodyBytes = new TextEncoder().encode(JSON.stringify(submission));
338
+ const authorization = await buildWeb3SignedHeader({
339
+ signMessage: builder.signMessage,
340
+ aud: audience,
341
+ method: POST_METHOD,
342
+ uri: JOBS_PATH,
343
+ body: bodyBytes,
344
+ nonce: randomUUID()
345
+ });
346
+ const response = await request(
347
+ `${baseUrl}${JOBS_PATH}?wait=${wait}`,
348
+ {
349
+ method: POST_METHOD,
350
+ headers: {
351
+ [AUTHORIZATION_HEADER]: authorization,
352
+ [CONTENT_TYPE_HEADER]: JSON_CONTENT_TYPE
353
+ },
354
+ body: bodyBytes
355
+ },
356
+ "Job submission"
357
+ );
358
+ if (!response.ok) {
359
+ throw await rejectedResponse(response, "Job submission rejected");
360
+ }
361
+ const body = await responseBody(response);
362
+ if (response.status === HTTP_OK && isRecord(body)) {
363
+ const job = requireStatus(body["job"], response.status);
364
+ if (job.jobId !== jobId) {
365
+ throw new JobRejectedError(
366
+ "Gateway returned a job id that does not match the submission",
367
+ response.status
368
+ );
369
+ }
370
+ return { jobId, state: job.state, job };
371
+ }
372
+ if (response.status === HTTP_ACCEPTED && isRecord(body) && body["jobId"] === jobId && typeof body["state"] === "string" && JOB_STATES.includes(body["state"])) {
373
+ return { jobId, state: body["state"] };
374
+ }
375
+ throw new JobRejectedError(
376
+ "Gateway returned an undocumented job submission response",
377
+ response.status
378
+ );
379
+ },
380
+ async getJob(jobId) {
381
+ const path = `${JOBS_PATH}/${encodeURIComponent(jobId)}`;
382
+ const authorization = await buildWeb3SignedHeader({
383
+ signMessage: builder.signMessage,
384
+ aud: audience,
385
+ method: GET_METHOD,
386
+ uri: path,
387
+ nonce: randomUUID()
388
+ });
389
+ const response = await request(
390
+ `${baseUrl}${path}`,
391
+ {
392
+ method: GET_METHOD,
393
+ headers: { [AUTHORIZATION_HEADER]: authorization }
394
+ },
395
+ "Job status read"
396
+ );
397
+ if (!response.ok) {
398
+ throw await rejectedResponse(response, "Job status read rejected");
399
+ }
400
+ const body = await responseBody(response);
401
+ return requireStatus(
402
+ isRecord(body) ? body["job"] : void 0,
403
+ response.status
404
+ );
405
+ },
406
+ async waitForJob(jobId, waitOptions = {}) {
407
+ const timeoutMs = requireFiniteDuration(
408
+ waitOptions.timeoutMs ?? DEFAULT_JOB_TIMEOUT_MS,
409
+ "timeoutMs"
410
+ );
411
+ const pollMs = Math.max(
412
+ CLAIM_POLL_FLOOR_MS,
413
+ requireFiniteDuration(
414
+ waitOptions.pollMs ?? DEFAULT_JOB_POLL_MS,
415
+ "pollMs"
416
+ )
417
+ );
418
+ const startedAt = Date.now();
419
+ const callerDeadline = startedAt + timeoutMs;
420
+ let latest;
421
+ for (; ; ) {
422
+ if (Date.now() >= callerDeadline || latest && Date.now() >= (jobDeadline(latest) ?? Infinity)) {
423
+ throw new JobTimeoutError(
424
+ `Timed out waiting for terminal job ${jobId}`,
425
+ {
426
+ jobId,
427
+ timeoutMs,
428
+ state: latest?.state
429
+ }
430
+ );
431
+ }
432
+ latest = await client.getJob(jobId);
433
+ if (TERMINAL_JOB_STATES.has(latest.state)) return latest;
434
+ const effectiveDeadline = Math.min(
435
+ callerDeadline,
436
+ jobDeadline(latest) ?? Infinity
437
+ );
438
+ const remaining = effectiveDeadline - Date.now();
439
+ if (remaining <= 0) {
440
+ throw new JobTimeoutError(
441
+ `Timed out waiting for terminal job ${jobId}`,
442
+ {
443
+ jobId,
444
+ timeoutMs,
445
+ state: latest.state
446
+ }
447
+ );
448
+ }
449
+ await sleep(Math.min(pollMs, remaining));
450
+ }
451
+ },
452
+ async openResult(job, openOptions) {
453
+ if (!job.resultCiphertext) {
454
+ throw new JobRejectedError(
455
+ "Job status does not include inline resultCiphertext",
456
+ void 0,
457
+ null,
458
+ {
459
+ jobId: job.jobId,
460
+ state: job.state,
461
+ resultHandle: job.resultHandle,
462
+ maxInlineResultBytes: MAX_INLINE_RESULT_BYTES
463
+ }
464
+ );
465
+ }
466
+ if (!privateKey) {
467
+ throw new JobRejectedError(
468
+ "openResult requires createJobsClient({ builderPrivateKey })"
469
+ );
470
+ }
471
+ return openJobResult(
472
+ job.resultCiphertext,
473
+ privateKey,
474
+ ecies,
475
+ openOptions.expect
476
+ );
477
+ },
478
+ async readRaw(params) {
479
+ const submitted = await client.submitRawRead(params);
480
+ const job = submitted.job && TERMINAL_JOB_STATES.has(submitted.job.state) ? submitted.job : await client.waitForJob(submitted.jobId, {
481
+ timeoutMs: params.timeoutMs,
482
+ pollMs: params.pollMs
483
+ });
484
+ return client.openResult(job, {
485
+ expect: {
486
+ jobId: submitted.jobId,
487
+ scope: params.scope,
488
+ ...params.pinnedVersion !== void 0 && params.pinnedVersion !== null ? { version: params.pinnedVersion } : {}
489
+ }
490
+ });
491
+ }
492
+ };
493
+ return client;
494
+ }
495
+ export {
496
+ createJobsClient
497
+ };
498
+ //# sourceMappingURL=jobs-client.js.map