@gibwork/sdk 0.0.5 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -20,19 +20,21 @@ var GibworkConfigurationError = class extends GibworkError {
20
20
  name = "GibworkConfigurationError";
21
21
  };
22
22
  var GibworkApiError = class extends GibworkError {
23
- constructor(status, body, method, path, requestId) {
23
+ constructor(status, body, method, path, requestId, retryAfter) {
24
24
  super(`Gibwork API request failed with HTTP ${status}`);
25
25
  this.status = status;
26
26
  this.body = body;
27
27
  this.method = method;
28
28
  this.path = path;
29
29
  this.requestId = requestId;
30
+ this.retryAfter = retryAfter;
30
31
  }
31
32
  status;
32
33
  body;
33
34
  method;
34
35
  path;
35
36
  requestId;
37
+ retryAfter;
36
38
  name = "GibworkApiError";
37
39
  };
38
40
  var GibworkNetworkError = class extends GibworkError {
@@ -77,6 +79,51 @@ var GibworkAmbiguousSubmitError = class extends GibworkNetworkError {
77
79
  context;
78
80
  name = "GibworkAmbiguousSubmitError";
79
81
  };
82
+ var GibworkValidationError = class extends GibworkError {
83
+ name = "GibworkValidationError";
84
+ };
85
+ var GibworkProtocolError = class extends GibworkError {
86
+ name = "GibworkProtocolError";
87
+ };
88
+ var GibworkSubmissionOperationError = class extends GibworkError {
89
+ constructor(context, options) {
90
+ super(
91
+ "Submission creation interrupted; recover using the original input/key or intent",
92
+ options
93
+ );
94
+ this.context = context;
95
+ }
96
+ context;
97
+ name = "GibworkSubmissionOperationError";
98
+ };
99
+
100
+ // src/auth/validation.ts
101
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
102
+ var UUID_V4 = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
103
+ function isRecord(value) {
104
+ return typeof value === "object" && value !== null && !Array.isArray(value);
105
+ }
106
+ function assertObjectKeys(value, keys) {
107
+ if (!isRecord(value) || Object.keys(value).some((key) => !keys.includes(key))) {
108
+ throw new GibworkValidationError(
109
+ `Expected an object containing only: ${keys.join(", ")}`
110
+ );
111
+ }
112
+ }
113
+ function isUuid(value, v4 = false) {
114
+ return typeof value === "string" && (v4 ? UUID_V4 : UUID).test(value);
115
+ }
116
+ function assertUuid(value, name, v4 = false) {
117
+ if (!isUuid(value, v4))
118
+ throw new GibworkValidationError(`${name} must be a UUID${v4 ? "v4" : ""}`);
119
+ }
120
+ function assertText(value, name, min, max) {
121
+ if (typeof value !== "string" || Array.from(value).length < min || Array.from(value).length > max) {
122
+ throw new GibworkValidationError(
123
+ `${name} must contain ${min}\u2013${max} characters`
124
+ );
125
+ }
126
+ }
80
127
  var HTML_SANITIZE_OPTIONS = {
81
128
  allowedTags: sanitizeHtml__default.default.defaults.allowedTags.concat(["img"]),
82
129
  allowedAttributes: {
@@ -231,6 +278,42 @@ function trimOptional(value) {
231
278
  const trimmed = value.trim();
232
279
  return trimmed === "" ? void 0 : trimmed;
233
280
  }
281
+ function normalizeAvailableTasksQuery(query = {}) {
282
+ assertObjectKeys(query, ["page", "limit"]);
283
+ const page = query.page === void 0 ? 1 : query.page;
284
+ const limit = query.limit === void 0 ? 15 : query.limit;
285
+ if (!Number.isSafeInteger(page) || page < 1 || !Number.isSafeInteger(limit) || limit < 1 || limit > 100 || !Number.isSafeInteger((page - 1) * limit)) {
286
+ throw new GibworkValidationError(
287
+ "page must be a positive safe integer, limit must be 1\u2013100, and the offset must be safe"
288
+ );
289
+ }
290
+ return { page, limit };
291
+ }
292
+ function normalizeCreateSubmission(input, walletAddress) {
293
+ assertObjectKeys(input, [
294
+ "content",
295
+ "referral",
296
+ "mediaIds",
297
+ "idempotencyKey"
298
+ ]);
299
+ assertText(input.content, "content", 1, 5e4);
300
+ if (input.referral != null) assertText(input.referral, "referral", 0, 200);
301
+ assertUuid(input.idempotencyKey, "idempotencyKey", true);
302
+ const mediaIds = input.mediaIds ?? [];
303
+ if (!Array.isArray(mediaIds) || mediaIds.length > 10 || new Set(mediaIds).size !== mediaIds.length) {
304
+ throw new GibworkValidationError(
305
+ "mediaIds must contain at most ten distinct UUIDv4 values"
306
+ );
307
+ }
308
+ for (const id of mediaIds) assertUuid(id, "mediaIds", true);
309
+ return {
310
+ walletAddress,
311
+ content: input.content,
312
+ referral: input.referral ?? null,
313
+ mediaIds: [...mediaIds],
314
+ idempotencyKey: input.idempotencyKey
315
+ };
316
+ }
234
317
  async function createWalletAuthHeaders(signer, descriptor, dependencies = {}) {
235
318
  const values = {
236
319
  walletAddress: signer.publicKey.toBase58(),
@@ -267,6 +350,30 @@ function buildWalletAuthMessage(descriptor, values) {
267
350
  `${descriptor.hash[0]}:${descriptor.hash[1]}`
268
351
  ].join("\n");
269
352
  }
353
+
354
+ // src/resources/submissions/submission-intent.ts
355
+ function submissionIntentPath(taskId, intentId) {
356
+ assertUuid(taskId, "taskId");
357
+ if (intentId !== void 0) assertUuid(intentId, "intentId");
358
+ return `/v2/int/tasks/${taskId}/submission-intents${intentId === void 0 ? "" : `/${intentId}`}`;
359
+ }
360
+ var statuses = [
361
+ "pending",
362
+ "submitted",
363
+ "fulfilled",
364
+ "failed",
365
+ "expired",
366
+ "requires_review"
367
+ ];
368
+ var nonempty = (value) => typeof value === "string" && value.length > 0;
369
+ var decimal = (value) => typeof value === "string" && /^\d+(\.\d+)?$/.test(value);
370
+ var natural = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
371
+ var sameId = (value, expected) => value.toLowerCase() === expected.toLowerCase();
372
+ function validateSubmissionIntent(value, taskId, intentId, paymentAttemptId) {
373
+ if (!isRecord(value) || !isUuid(value.intentId) || !isUuid(value.taskId) || !isUuid(value.taskSubmissionId) || !isUuid(value.paymentAttemptId, true) || !sameId(value.taskId, taskId) || intentId !== void 0 && !sameId(value.intentId, intentId) || paymentAttemptId !== void 0 && !sameId(value.paymentAttemptId, paymentAttemptId) || typeof value.status !== "string" || !statuses.includes(value.status) || !(value.txHash === null || nonempty(value.txHash)) || !nonempty(value.expiresAt) || !Number.isFinite(Date.parse(value.expiresAt)) || !natural(value.lastValidBlockHeight) || value.reviewReason !== void 0 && typeof value.reviewReason !== "string" || value.serializedTransaction !== void 0 && !nonempty(value.serializedTransaction) || !isRecord(value.fee) || !decimal(value.fee.amount) || !decimal(value.fee.totalDebit) || typeof value.fee.amountBaseUnits !== "string" || !/^\d+$/.test(value.fee.amountBaseUnits) || value.fee.symbol !== "USDC" || !natural(value.fee.decimals) || !nonempty(value.fee.mintAddress) || !nonempty(value.fee.destinationAddress)) {
374
+ throw new GibworkProtocolError("Invalid submission intent response");
375
+ }
376
+ }
270
377
  async function signPreparedTransaction(serializedTransaction, signer) {
271
378
  if (typeof serializedTransaction !== "string" || !serializedTransaction) {
272
379
  throw new GibworkConfigurationError(
@@ -356,6 +463,142 @@ var SubmissionsResource = class {
356
463
  transport;
357
464
  signer;
358
465
  comments;
466
+ /** Creates work through a participation fee; only fulfilled confirms creation. */
467
+ async create(taskId, input, options) {
468
+ const path = submissionIntentPath(taskId);
469
+ const { walletAddress, ...snapshot } = normalizeCreateSubmission(
470
+ input,
471
+ this.walletAddress
472
+ );
473
+ let context = {
474
+ taskId,
475
+ idempotencyKey: snapshot.idempotencyKey,
476
+ walletAddress,
477
+ ...this.transport.recoveryLocation,
478
+ phase: "prepare"
479
+ };
480
+ try {
481
+ assertNotAborted(options, path);
482
+ const prepared = await this.prepareCreate(taskId, snapshot, options);
483
+ context = {
484
+ ...context,
485
+ intentId: prepared.intentId,
486
+ paymentAttemptId: prepared.paymentAttemptId,
487
+ submissionId: prepared.taskSubmissionId,
488
+ phase: "sign"
489
+ };
490
+ if (prepared.status !== "pending") return prepared;
491
+ if (!prepared.serializedTransaction)
492
+ throw new GibworkProtocolError(
493
+ "Pending prepare response is missing its transaction"
494
+ );
495
+ assertNotAborted(options, path);
496
+ const signedTransaction = await signPreparedTransaction(
497
+ prepared.serializedTransaction,
498
+ this.signer
499
+ );
500
+ assertNotAborted(options, path);
501
+ context = { ...context, phase: "submit" };
502
+ return await this.submitCreate(
503
+ taskId,
504
+ prepared.intentId,
505
+ { paymentAttemptId: prepared.paymentAttemptId, signedTransaction },
506
+ options
507
+ );
508
+ } catch (cause) {
509
+ if (cause instanceof GibworkAmbiguousSubmitError) {
510
+ throw new GibworkAmbiguousSubmitError(
511
+ cause.method,
512
+ cause.path,
513
+ {
514
+ ...cause.context,
515
+ ...context,
516
+ operation: "create-submission",
517
+ intentId: cause.context.intentId
518
+ },
519
+ { cause }
520
+ );
521
+ }
522
+ throw new GibworkSubmissionOperationError(context, { cause });
523
+ }
524
+ }
525
+ /** Reuse the exact input and idempotency key after an interrupted prepare. */
526
+ async prepareCreate(taskId, input, options) {
527
+ const path = submissionIntentPath(taskId);
528
+ const body = normalizeCreateSubmission(input, this.walletAddress);
529
+ assertNotAborted(options, path);
530
+ const headers = await createWalletAuthHeaders(this.signer, {
531
+ operation: "gibwork:create-task-submission-intent",
532
+ method: "POST",
533
+ path,
534
+ resourceFields: [["taskId", taskId]],
535
+ hash: ["payloadHash", sha256Json(body)]
536
+ });
537
+ return this.transport.request({
538
+ method: "POST",
539
+ path,
540
+ headers,
541
+ body,
542
+ ...options ? { options } : {},
543
+ validateResponse: (value) => validateSubmissionIntent(value, taskId)
544
+ });
545
+ }
546
+ /** Never automatically retries. Recover an uncertain payment with getIntent. */
547
+ async submitCreate(taskId, intentId, input, options) {
548
+ const path = `${submissionIntentPath(taskId, intentId)}/submit`;
549
+ assertObjectKeys(input, ["paymentAttemptId", "signedTransaction"]);
550
+ assertUuid(input.paymentAttemptId, "paymentAttemptId", true);
551
+ assertText(input.signedTransaction, "signedTransaction", 1, 1e4);
552
+ const body = {
553
+ paymentAttemptId: input.paymentAttemptId,
554
+ signedTransaction: input.signedTransaction
555
+ };
556
+ return this.transport.request({
557
+ method: "POST",
558
+ path,
559
+ body,
560
+ ...options ? { options } : {},
561
+ ambiguousSubmit: {
562
+ operation: "create-submission",
563
+ taskId,
564
+ intentId,
565
+ paymentAttemptId: body.paymentAttemptId,
566
+ walletAddress: this.walletAddress,
567
+ ...this.transport.recoveryLocation
568
+ },
569
+ validateResponse: (value) => validateSubmissionIntent(
570
+ value,
571
+ taskId,
572
+ intentId,
573
+ body.paymentAttemptId
574
+ )
575
+ });
576
+ }
577
+ /** Reads/reconciles creation using the original wallet; does not report review/payout. */
578
+ async getIntent(taskId, intentId, options) {
579
+ const path = submissionIntentPath(taskId, intentId);
580
+ if (options?.signal?.aborted)
581
+ throw new GibworkRequestAbortedError("GET", path, {
582
+ cause: options.signal.reason
583
+ });
584
+ const headers = await createWalletAuthHeaders(this.signer, {
585
+ operation: "gibwork:view-task-submission-intent",
586
+ method: "GET",
587
+ path,
588
+ resourceFields: [
589
+ ["taskId", taskId],
590
+ ["intentId", intentId]
591
+ ],
592
+ hash: ["queryHash", sha256Json({})]
593
+ });
594
+ return this.transport.request({
595
+ method: "GET",
596
+ path,
597
+ headers,
598
+ ...options ? { options } : {},
599
+ validateResponse: (value) => validateSubmissionIntent(value, taskId, intentId)
600
+ });
601
+ }
359
602
  async list(taskId, query = {}, options) {
360
603
  const normalized = normalizeSubmissionPagination(query);
361
604
  const path = `/v2/int/tasks/${encodeURIComponent(taskId)}/submissions`;
@@ -375,6 +618,25 @@ var SubmissionsResource = class {
375
618
  ...options ? { options } : {}
376
619
  });
377
620
  }
621
+ async get(taskId, submissionId, options) {
622
+ const path = `/v2/int/tasks/${encodeURIComponent(taskId)}/submissions/${encodeURIComponent(submissionId)}`;
623
+ const headers = await createWalletAuthHeaders(this.signer, {
624
+ operation: "gibwork:view-task-submission",
625
+ method: "GET",
626
+ path,
627
+ resourceFields: [
628
+ ["taskId", taskId],
629
+ ["taskSubmissionId", submissionId]
630
+ ],
631
+ hash: ["queryHash", sha256Json({})]
632
+ });
633
+ return this.transport.request({
634
+ method: "GET",
635
+ path,
636
+ headers,
637
+ ...options ? { options } : {}
638
+ });
639
+ }
378
640
  async approve(taskId, submissionId, input, options) {
379
641
  const prepared = await this.prepareApproval(
380
642
  taskId,
@@ -526,6 +788,23 @@ var TasksResource = class {
526
788
  ...options ? { options } : {}
527
789
  });
528
790
  }
791
+ /** Discover tasks across creators. Eligibility is checked when preparing work. */
792
+ async listAvailable(query = {}, options) {
793
+ const normalized = normalizeAvailableTasksQuery(query);
794
+ const path = "/v2/int/tasks/available";
795
+ const headers = await this.auth({
796
+ operation: "gibwork:view-available-tasks",
797
+ method: "GET",
798
+ path,
799
+ hash: ["queryHash", sha256Json(normalized)]
800
+ });
801
+ return this.transport.request({
802
+ method: "GET",
803
+ path: `${path}?${toQueryString(normalized)}`,
804
+ headers,
805
+ ...options ? { options } : {}
806
+ });
807
+ }
529
808
  async update(taskId, input, options) {
530
809
  const path = `/v2/int/tasks/${encodeURIComponent(taskId)}`;
531
810
  const body = normalizeUpdateTask(input, this.walletAddress);
@@ -671,6 +950,16 @@ var HttpTransport = class {
671
950
  );
672
951
  }
673
952
  }
953
+ /** Recovery location without URL credentials. Retains reverse-proxy path prefixes. */
954
+ get recoveryLocation() {
955
+ const url = new URL(this.baseUrl);
956
+ url.username = "";
957
+ url.password = "";
958
+ return {
959
+ apiUrl: url.toString().replace(/\/+$/, ""),
960
+ environment: this.production ? "prod" : "stage"
961
+ };
962
+ }
674
963
  async request(request) {
675
964
  const controller = new AbortController();
676
965
  const callerSignal = request.options?.signal;
@@ -680,12 +969,6 @@ var HttpTransport = class {
680
969
  cause: callerSignal.reason
681
970
  });
682
971
  }
683
- const abortFromCaller = () => controller.abort(callerSignal?.reason);
684
- callerSignal?.addEventListener("abort", abortFromCaller, { once: true });
685
- const timeout = setTimeout(() => {
686
- timedOut = true;
687
- controller.abort();
688
- }, this.timeoutMs);
689
972
  const headers = {
690
973
  accept: "application/json",
691
974
  ...request.headers,
@@ -700,13 +983,40 @@ var HttpTransport = class {
700
983
  headers["content-type"] = "application/json";
701
984
  init.body = JSON.stringify(request.body);
702
985
  }
703
- let response;
986
+ const abortFromCaller = () => controller.abort(callerSignal?.reason);
987
+ callerSignal?.addEventListener("abort", abortFromCaller, { once: true });
988
+ let rejectOnAbort = () => {
989
+ };
990
+ const aborted = new Promise((_, reject) => {
991
+ rejectOnAbort = () => reject(controller.signal.reason);
992
+ controller.signal.addEventListener("abort", rejectOnAbort, {
993
+ once: true
994
+ });
995
+ });
996
+ const timeout = setTimeout(() => {
997
+ timedOut = true;
998
+ controller.abort();
999
+ }, this.timeoutMs);
704
1000
  try {
705
- response = await this.fetchImplementation(
706
- `${this.baseUrl}${request.path}`,
707
- init
708
- );
1001
+ const response = await Promise.race([
1002
+ this.fetchImplementation(`${this.baseUrl}${request.path}`, init),
1003
+ aborted
1004
+ ]);
1005
+ const body = await Promise.race([parseResponseBody(response), aborted]);
1006
+ if (!response.ok) {
1007
+ throw new GibworkApiError(
1008
+ response.status,
1009
+ body,
1010
+ request.method,
1011
+ request.path,
1012
+ response.headers.get("x-request-id") ?? void 0,
1013
+ response.headers.get("retry-after") ?? void 0
1014
+ );
1015
+ }
1016
+ request.validateResponse?.(body);
1017
+ return body;
709
1018
  } catch (cause) {
1019
+ if (cause instanceof GibworkApiError && cause.status < 500) throw cause;
710
1020
  if (request.ambiguousSubmit) {
711
1021
  throw new GibworkAmbiguousSubmitError(
712
1022
  request.method,
@@ -715,6 +1025,8 @@ var HttpTransport = class {
715
1025
  { cause }
716
1026
  );
717
1027
  }
1028
+ if (cause instanceof GibworkApiError || cause instanceof GibworkProtocolError)
1029
+ throw cause;
718
1030
  if (timedOut) {
719
1031
  throw new GibworkTimeoutError(
720
1032
  request.method,
@@ -729,7 +1041,7 @@ var HttpTransport = class {
729
1041
  });
730
1042
  }
731
1043
  throw new GibworkNetworkError(
732
- "Could not reach the Gibwork API",
1044
+ "Could not read the Gibwork API response",
733
1045
  request.method,
734
1046
  request.path,
735
1047
  { cause }
@@ -737,28 +1049,8 @@ var HttpTransport = class {
737
1049
  } finally {
738
1050
  clearTimeout(timeout);
739
1051
  callerSignal?.removeEventListener("abort", abortFromCaller);
1052
+ controller.signal.removeEventListener("abort", rejectOnAbort);
740
1053
  }
741
- const body = await parseResponseBody(response);
742
- if (!response.ok) {
743
- const requestId = response.headers.get("x-request-id") ?? void 0;
744
- const apiError = new GibworkApiError(
745
- response.status,
746
- body,
747
- request.method,
748
- request.path,
749
- requestId
750
- );
751
- if (request.ambiguousSubmit && response.status >= 500) {
752
- throw new GibworkAmbiguousSubmitError(
753
- request.method,
754
- request.path,
755
- request.ambiguousSubmit,
756
- { cause: apiError }
757
- );
758
- }
759
- throw apiError;
760
- }
761
- return body;
762
1054
  }
763
1055
  };
764
1056
  function normalizeBaseUrl(value) {
@@ -824,8 +1116,11 @@ exports.GibworkClient = GibworkClient;
824
1116
  exports.GibworkConfigurationError = GibworkConfigurationError;
825
1117
  exports.GibworkError = GibworkError;
826
1118
  exports.GibworkNetworkError = GibworkNetworkError;
1119
+ exports.GibworkProtocolError = GibworkProtocolError;
827
1120
  exports.GibworkRequestAbortedError = GibworkRequestAbortedError;
1121
+ exports.GibworkSubmissionOperationError = GibworkSubmissionOperationError;
828
1122
  exports.GibworkTimeoutError = GibworkTimeoutError;
1123
+ exports.GibworkValidationError = GibworkValidationError;
829
1124
  exports.signPreparedTransaction = signPreparedTransaction;
830
1125
  //# sourceMappingURL=index.cjs.map
831
1126
  //# sourceMappingURL=index.cjs.map