@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/node.cjs CHANGED
@@ -24,19 +24,21 @@ var GibworkConfigurationError = class extends GibworkError {
24
24
  name = "GibworkConfigurationError";
25
25
  };
26
26
  var GibworkApiError = class extends GibworkError {
27
- constructor(status, body, method, path, requestId) {
27
+ constructor(status, body, method, path, requestId, retryAfter) {
28
28
  super(`Gibwork API request failed with HTTP ${status}`);
29
29
  this.status = status;
30
30
  this.body = body;
31
31
  this.method = method;
32
32
  this.path = path;
33
33
  this.requestId = requestId;
34
+ this.retryAfter = retryAfter;
34
35
  }
35
36
  status;
36
37
  body;
37
38
  method;
38
39
  path;
39
40
  requestId;
41
+ retryAfter;
40
42
  name = "GibworkApiError";
41
43
  };
42
44
  var GibworkNetworkError = class extends GibworkError {
@@ -81,6 +83,51 @@ var GibworkAmbiguousSubmitError = class extends GibworkNetworkError {
81
83
  context;
82
84
  name = "GibworkAmbiguousSubmitError";
83
85
  };
86
+ var GibworkValidationError = class extends GibworkError {
87
+ name = "GibworkValidationError";
88
+ };
89
+ var GibworkProtocolError = class extends GibworkError {
90
+ name = "GibworkProtocolError";
91
+ };
92
+ var GibworkSubmissionOperationError = class extends GibworkError {
93
+ constructor(context, options) {
94
+ super(
95
+ "Submission creation interrupted; recover using the original input/key or intent",
96
+ options
97
+ );
98
+ this.context = context;
99
+ }
100
+ context;
101
+ name = "GibworkSubmissionOperationError";
102
+ };
103
+
104
+ // src/auth/validation.ts
105
+ 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;
106
+ 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;
107
+ function isRecord(value) {
108
+ return typeof value === "object" && value !== null && !Array.isArray(value);
109
+ }
110
+ function assertObjectKeys(value, keys) {
111
+ if (!isRecord(value) || Object.keys(value).some((key) => !keys.includes(key))) {
112
+ throw new GibworkValidationError(
113
+ `Expected an object containing only: ${keys.join(", ")}`
114
+ );
115
+ }
116
+ }
117
+ function isUuid(value, v4 = false) {
118
+ return typeof value === "string" && (v4 ? UUID_V4 : UUID).test(value);
119
+ }
120
+ function assertUuid(value, name, v4 = false) {
121
+ if (!isUuid(value, v4))
122
+ throw new GibworkValidationError(`${name} must be a UUID${v4 ? "v4" : ""}`);
123
+ }
124
+ function assertText(value, name, min, max) {
125
+ if (typeof value !== "string" || Array.from(value).length < min || Array.from(value).length > max) {
126
+ throw new GibworkValidationError(
127
+ `${name} must contain ${min}\u2013${max} characters`
128
+ );
129
+ }
130
+ }
84
131
  var HTML_SANITIZE_OPTIONS = {
85
132
  allowedTags: sanitizeHtml__default.default.defaults.allowedTags.concat(["img"]),
86
133
  allowedAttributes: {
@@ -235,6 +282,42 @@ function trimOptional(value) {
235
282
  const trimmed = value.trim();
236
283
  return trimmed === "" ? void 0 : trimmed;
237
284
  }
285
+ function normalizeAvailableTasksQuery(query = {}) {
286
+ assertObjectKeys(query, ["page", "limit"]);
287
+ const page = query.page === void 0 ? 1 : query.page;
288
+ const limit = query.limit === void 0 ? 15 : query.limit;
289
+ if (!Number.isSafeInteger(page) || page < 1 || !Number.isSafeInteger(limit) || limit < 1 || limit > 100 || !Number.isSafeInteger((page - 1) * limit)) {
290
+ throw new GibworkValidationError(
291
+ "page must be a positive safe integer, limit must be 1\u2013100, and the offset must be safe"
292
+ );
293
+ }
294
+ return { page, limit };
295
+ }
296
+ function normalizeCreateSubmission(input, walletAddress) {
297
+ assertObjectKeys(input, [
298
+ "content",
299
+ "referral",
300
+ "mediaIds",
301
+ "idempotencyKey"
302
+ ]);
303
+ assertText(input.content, "content", 1, 5e4);
304
+ if (input.referral != null) assertText(input.referral, "referral", 0, 200);
305
+ assertUuid(input.idempotencyKey, "idempotencyKey", true);
306
+ const mediaIds = input.mediaIds ?? [];
307
+ if (!Array.isArray(mediaIds) || mediaIds.length > 10 || new Set(mediaIds).size !== mediaIds.length) {
308
+ throw new GibworkValidationError(
309
+ "mediaIds must contain at most ten distinct UUIDv4 values"
310
+ );
311
+ }
312
+ for (const id of mediaIds) assertUuid(id, "mediaIds", true);
313
+ return {
314
+ walletAddress,
315
+ content: input.content,
316
+ referral: input.referral ?? null,
317
+ mediaIds: [...mediaIds],
318
+ idempotencyKey: input.idempotencyKey
319
+ };
320
+ }
238
321
  async function createWalletAuthHeaders(signer, descriptor, dependencies = {}) {
239
322
  const values = {
240
323
  walletAddress: signer.publicKey.toBase58(),
@@ -271,6 +354,30 @@ function buildWalletAuthMessage(descriptor, values) {
271
354
  `${descriptor.hash[0]}:${descriptor.hash[1]}`
272
355
  ].join("\n");
273
356
  }
357
+
358
+ // src/resources/submissions/submission-intent.ts
359
+ function submissionIntentPath(taskId, intentId) {
360
+ assertUuid(taskId, "taskId");
361
+ if (intentId !== void 0) assertUuid(intentId, "intentId");
362
+ return `/v2/int/tasks/${taskId}/submission-intents${intentId === void 0 ? "" : `/${intentId}`}`;
363
+ }
364
+ var statuses = [
365
+ "pending",
366
+ "submitted",
367
+ "fulfilled",
368
+ "failed",
369
+ "expired",
370
+ "requires_review"
371
+ ];
372
+ var nonempty = (value) => typeof value === "string" && value.length > 0;
373
+ var decimal = (value) => typeof value === "string" && /^\d+(\.\d+)?$/.test(value);
374
+ var natural = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
375
+ var sameId = (value, expected) => value.toLowerCase() === expected.toLowerCase();
376
+ function validateSubmissionIntent(value, taskId, intentId, paymentAttemptId) {
377
+ 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)) {
378
+ throw new GibworkProtocolError("Invalid submission intent response");
379
+ }
380
+ }
274
381
  async function signPreparedTransaction(serializedTransaction, signer) {
275
382
  if (typeof serializedTransaction !== "string" || !serializedTransaction) {
276
383
  throw new GibworkConfigurationError(
@@ -360,6 +467,142 @@ var SubmissionsResource = class {
360
467
  transport;
361
468
  signer;
362
469
  comments;
470
+ /** Creates work through a participation fee; only fulfilled confirms creation. */
471
+ async create(taskId, input, options) {
472
+ const path = submissionIntentPath(taskId);
473
+ const { walletAddress, ...snapshot } = normalizeCreateSubmission(
474
+ input,
475
+ this.walletAddress
476
+ );
477
+ let context = {
478
+ taskId,
479
+ idempotencyKey: snapshot.idempotencyKey,
480
+ walletAddress,
481
+ ...this.transport.recoveryLocation,
482
+ phase: "prepare"
483
+ };
484
+ try {
485
+ assertNotAborted(options, path);
486
+ const prepared = await this.prepareCreate(taskId, snapshot, options);
487
+ context = {
488
+ ...context,
489
+ intentId: prepared.intentId,
490
+ paymentAttemptId: prepared.paymentAttemptId,
491
+ submissionId: prepared.taskSubmissionId,
492
+ phase: "sign"
493
+ };
494
+ if (prepared.status !== "pending") return prepared;
495
+ if (!prepared.serializedTransaction)
496
+ throw new GibworkProtocolError(
497
+ "Pending prepare response is missing its transaction"
498
+ );
499
+ assertNotAborted(options, path);
500
+ const signedTransaction = await signPreparedTransaction(
501
+ prepared.serializedTransaction,
502
+ this.signer
503
+ );
504
+ assertNotAborted(options, path);
505
+ context = { ...context, phase: "submit" };
506
+ return await this.submitCreate(
507
+ taskId,
508
+ prepared.intentId,
509
+ { paymentAttemptId: prepared.paymentAttemptId, signedTransaction },
510
+ options
511
+ );
512
+ } catch (cause) {
513
+ if (cause instanceof GibworkAmbiguousSubmitError) {
514
+ throw new GibworkAmbiguousSubmitError(
515
+ cause.method,
516
+ cause.path,
517
+ {
518
+ ...cause.context,
519
+ ...context,
520
+ operation: "create-submission",
521
+ intentId: cause.context.intentId
522
+ },
523
+ { cause }
524
+ );
525
+ }
526
+ throw new GibworkSubmissionOperationError(context, { cause });
527
+ }
528
+ }
529
+ /** Reuse the exact input and idempotency key after an interrupted prepare. */
530
+ async prepareCreate(taskId, input, options) {
531
+ const path = submissionIntentPath(taskId);
532
+ const body = normalizeCreateSubmission(input, this.walletAddress);
533
+ assertNotAborted(options, path);
534
+ const headers = await createWalletAuthHeaders(this.signer, {
535
+ operation: "gibwork:create-task-submission-intent",
536
+ method: "POST",
537
+ path,
538
+ resourceFields: [["taskId", taskId]],
539
+ hash: ["payloadHash", sha256Json(body)]
540
+ });
541
+ return this.transport.request({
542
+ method: "POST",
543
+ path,
544
+ headers,
545
+ body,
546
+ ...options ? { options } : {},
547
+ validateResponse: (value) => validateSubmissionIntent(value, taskId)
548
+ });
549
+ }
550
+ /** Never automatically retries. Recover an uncertain payment with getIntent. */
551
+ async submitCreate(taskId, intentId, input, options) {
552
+ const path = `${submissionIntentPath(taskId, intentId)}/submit`;
553
+ assertObjectKeys(input, ["paymentAttemptId", "signedTransaction"]);
554
+ assertUuid(input.paymentAttemptId, "paymentAttemptId", true);
555
+ assertText(input.signedTransaction, "signedTransaction", 1, 1e4);
556
+ const body = {
557
+ paymentAttemptId: input.paymentAttemptId,
558
+ signedTransaction: input.signedTransaction
559
+ };
560
+ return this.transport.request({
561
+ method: "POST",
562
+ path,
563
+ body,
564
+ ...options ? { options } : {},
565
+ ambiguousSubmit: {
566
+ operation: "create-submission",
567
+ taskId,
568
+ intentId,
569
+ paymentAttemptId: body.paymentAttemptId,
570
+ walletAddress: this.walletAddress,
571
+ ...this.transport.recoveryLocation
572
+ },
573
+ validateResponse: (value) => validateSubmissionIntent(
574
+ value,
575
+ taskId,
576
+ intentId,
577
+ body.paymentAttemptId
578
+ )
579
+ });
580
+ }
581
+ /** Reads/reconciles creation using the original wallet; does not report review/payout. */
582
+ async getIntent(taskId, intentId, options) {
583
+ const path = submissionIntentPath(taskId, intentId);
584
+ if (options?.signal?.aborted)
585
+ throw new GibworkRequestAbortedError("GET", path, {
586
+ cause: options.signal.reason
587
+ });
588
+ const headers = await createWalletAuthHeaders(this.signer, {
589
+ operation: "gibwork:view-task-submission-intent",
590
+ method: "GET",
591
+ path,
592
+ resourceFields: [
593
+ ["taskId", taskId],
594
+ ["intentId", intentId]
595
+ ],
596
+ hash: ["queryHash", sha256Json({})]
597
+ });
598
+ return this.transport.request({
599
+ method: "GET",
600
+ path,
601
+ headers,
602
+ ...options ? { options } : {},
603
+ validateResponse: (value) => validateSubmissionIntent(value, taskId, intentId)
604
+ });
605
+ }
363
606
  async list(taskId, query = {}, options) {
364
607
  const normalized = normalizeSubmissionPagination(query);
365
608
  const path = `/v2/int/tasks/${encodeURIComponent(taskId)}/submissions`;
@@ -379,6 +622,25 @@ var SubmissionsResource = class {
379
622
  ...options ? { options } : {}
380
623
  });
381
624
  }
625
+ async get(taskId, submissionId, options) {
626
+ const path = `/v2/int/tasks/${encodeURIComponent(taskId)}/submissions/${encodeURIComponent(submissionId)}`;
627
+ const headers = await createWalletAuthHeaders(this.signer, {
628
+ operation: "gibwork:view-task-submission",
629
+ method: "GET",
630
+ path,
631
+ resourceFields: [
632
+ ["taskId", taskId],
633
+ ["taskSubmissionId", submissionId]
634
+ ],
635
+ hash: ["queryHash", sha256Json({})]
636
+ });
637
+ return this.transport.request({
638
+ method: "GET",
639
+ path,
640
+ headers,
641
+ ...options ? { options } : {}
642
+ });
643
+ }
382
644
  async approve(taskId, submissionId, input, options) {
383
645
  const prepared = await this.prepareApproval(
384
646
  taskId,
@@ -530,6 +792,23 @@ var TasksResource = class {
530
792
  ...options ? { options } : {}
531
793
  });
532
794
  }
795
+ /** Discover tasks across creators. Eligibility is checked when preparing work. */
796
+ async listAvailable(query = {}, options) {
797
+ const normalized = normalizeAvailableTasksQuery(query);
798
+ const path = "/v2/int/tasks/available";
799
+ const headers = await this.auth({
800
+ operation: "gibwork:view-available-tasks",
801
+ method: "GET",
802
+ path,
803
+ hash: ["queryHash", sha256Json(normalized)]
804
+ });
805
+ return this.transport.request({
806
+ method: "GET",
807
+ path: `${path}?${toQueryString(normalized)}`,
808
+ headers,
809
+ ...options ? { options } : {}
810
+ });
811
+ }
533
812
  async update(taskId, input, options) {
534
813
  const path = `/v2/int/tasks/${encodeURIComponent(taskId)}`;
535
814
  const body = normalizeUpdateTask(input, this.walletAddress);
@@ -675,6 +954,16 @@ var HttpTransport = class {
675
954
  );
676
955
  }
677
956
  }
957
+ /** Recovery location without URL credentials. Retains reverse-proxy path prefixes. */
958
+ get recoveryLocation() {
959
+ const url = new URL(this.baseUrl);
960
+ url.username = "";
961
+ url.password = "";
962
+ return {
963
+ apiUrl: url.toString().replace(/\/+$/, ""),
964
+ environment: this.production ? "prod" : "stage"
965
+ };
966
+ }
678
967
  async request(request) {
679
968
  const controller = new AbortController();
680
969
  const callerSignal = request.options?.signal;
@@ -684,12 +973,6 @@ var HttpTransport = class {
684
973
  cause: callerSignal.reason
685
974
  });
686
975
  }
687
- const abortFromCaller = () => controller.abort(callerSignal?.reason);
688
- callerSignal?.addEventListener("abort", abortFromCaller, { once: true });
689
- const timeout = setTimeout(() => {
690
- timedOut = true;
691
- controller.abort();
692
- }, this.timeoutMs);
693
976
  const headers = {
694
977
  accept: "application/json",
695
978
  ...request.headers,
@@ -704,13 +987,40 @@ var HttpTransport = class {
704
987
  headers["content-type"] = "application/json";
705
988
  init.body = JSON.stringify(request.body);
706
989
  }
707
- let response;
990
+ const abortFromCaller = () => controller.abort(callerSignal?.reason);
991
+ callerSignal?.addEventListener("abort", abortFromCaller, { once: true });
992
+ let rejectOnAbort = () => {
993
+ };
994
+ const aborted = new Promise((_, reject) => {
995
+ rejectOnAbort = () => reject(controller.signal.reason);
996
+ controller.signal.addEventListener("abort", rejectOnAbort, {
997
+ once: true
998
+ });
999
+ });
1000
+ const timeout = setTimeout(() => {
1001
+ timedOut = true;
1002
+ controller.abort();
1003
+ }, this.timeoutMs);
708
1004
  try {
709
- response = await this.fetchImplementation(
710
- `${this.baseUrl}${request.path}`,
711
- init
712
- );
1005
+ const response = await Promise.race([
1006
+ this.fetchImplementation(`${this.baseUrl}${request.path}`, init),
1007
+ aborted
1008
+ ]);
1009
+ const body = await Promise.race([parseResponseBody(response), aborted]);
1010
+ if (!response.ok) {
1011
+ throw new GibworkApiError(
1012
+ response.status,
1013
+ body,
1014
+ request.method,
1015
+ request.path,
1016
+ response.headers.get("x-request-id") ?? void 0,
1017
+ response.headers.get("retry-after") ?? void 0
1018
+ );
1019
+ }
1020
+ request.validateResponse?.(body);
1021
+ return body;
713
1022
  } catch (cause) {
1023
+ if (cause instanceof GibworkApiError && cause.status < 500) throw cause;
714
1024
  if (request.ambiguousSubmit) {
715
1025
  throw new GibworkAmbiguousSubmitError(
716
1026
  request.method,
@@ -719,6 +1029,8 @@ var HttpTransport = class {
719
1029
  { cause }
720
1030
  );
721
1031
  }
1032
+ if (cause instanceof GibworkApiError || cause instanceof GibworkProtocolError)
1033
+ throw cause;
722
1034
  if (timedOut) {
723
1035
  throw new GibworkTimeoutError(
724
1036
  request.method,
@@ -733,7 +1045,7 @@ var HttpTransport = class {
733
1045
  });
734
1046
  }
735
1047
  throw new GibworkNetworkError(
736
- "Could not reach the Gibwork API",
1048
+ "Could not read the Gibwork API response",
737
1049
  request.method,
738
1050
  request.path,
739
1051
  { cause }
@@ -741,28 +1053,8 @@ var HttpTransport = class {
741
1053
  } finally {
742
1054
  clearTimeout(timeout);
743
1055
  callerSignal?.removeEventListener("abort", abortFromCaller);
1056
+ controller.signal.removeEventListener("abort", rejectOnAbort);
744
1057
  }
745
- const body = await parseResponseBody(response);
746
- if (!response.ok) {
747
- const requestId = response.headers.get("x-request-id") ?? void 0;
748
- const apiError = new GibworkApiError(
749
- response.status,
750
- body,
751
- request.method,
752
- request.path,
753
- requestId
754
- );
755
- if (request.ambiguousSubmit && response.status >= 500) {
756
- throw new GibworkAmbiguousSubmitError(
757
- request.method,
758
- request.path,
759
- request.ambiguousSubmit,
760
- { cause: apiError }
761
- );
762
- }
763
- throw apiError;
764
- }
765
- return body;
766
1058
  }
767
1059
  };
768
1060
  function normalizeBaseUrl(value) {
@@ -904,8 +1196,11 @@ exports.GibworkClient = GibworkClient;
904
1196
  exports.GibworkConfigurationError = GibworkConfigurationError;
905
1197
  exports.GibworkError = GibworkError;
906
1198
  exports.GibworkNetworkError = GibworkNetworkError;
1199
+ exports.GibworkProtocolError = GibworkProtocolError;
907
1200
  exports.GibworkRequestAbortedError = GibworkRequestAbortedError;
1201
+ exports.GibworkSubmissionOperationError = GibworkSubmissionOperationError;
908
1202
  exports.GibworkTimeoutError = GibworkTimeoutError;
1203
+ exports.GibworkValidationError = GibworkValidationError;
909
1204
  exports.createGibworkClient = createGibworkClient;
910
1205
  exports.createKeypairSigner = createKeypairSigner;
911
1206
  exports.keypairFromPrivateKey = keypairFromPrivateKey;