@gigadrive/sdk 0.8.6 → 0.9.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.mjs CHANGED
@@ -22,11 +22,17 @@ var ApiError = class extends GigadriveError {
22
22
  status;
23
23
  /** An optional machine-readable error code from the API response body. */
24
24
  code;
25
- constructor(message, status, code) {
25
+ /**
26
+ * An optional finer-grained reason next to {@link code}, for example `daily_add_limit` with
27
+ * `quota_exceeded`, or `other_organization` with `domain_in_use`.
28
+ */
29
+ reason;
30
+ constructor(message, status, code, reason) {
26
31
  super(message);
27
32
  this.name = "ApiError";
28
33
  this.status = status;
29
34
  this.code = code;
35
+ this.reason = reason;
30
36
  }
31
37
  };
32
38
  var UploadError = class extends GigadriveError {
@@ -514,10 +520,13 @@ var HttpClient = class {
514
520
  async toApiError(response) {
515
521
  let message;
516
522
  let code;
523
+ let reason;
517
524
  try {
518
525
  const body = await response.json();
519
526
  if (typeof body.error === "string") {
520
527
  message = body.error;
528
+ if (typeof body.code === "string") code = body.code;
529
+ if (typeof body.reason === "string") reason = body.reason;
521
530
  } else if (body.error && typeof body.error === "object") {
522
531
  message = body.error.message ?? response.statusText;
523
532
  code = body.error.code;
@@ -527,7 +536,7 @@ var HttpClient = class {
527
536
  } catch {
528
537
  message = response.statusText;
529
538
  }
530
- return new ApiError(message, response.status, code);
539
+ return new ApiError(message, response.status, code, reason);
531
540
  }
532
541
  buildUrl(path, query) {
533
542
  const url = `${this.baseUrl}${path}`;
@@ -887,6 +896,7 @@ var ApplicationRequestsResource = class extends BaseResource {
887
896
  };
888
897
 
889
898
  // src/upload/checksum.ts
899
+ var EMPTY_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
890
900
  var toHex = (buffer) => {
891
901
  const bytes = new Uint8Array(buffer);
892
902
  let hex = "";
@@ -1005,6 +1015,8 @@ var inferContentType = (nameOrKey) => {
1005
1015
  };
1006
1016
 
1007
1017
  // src/upload/source.ts
1018
+ var noop = () => {
1019
+ };
1008
1020
  var isNode = () => typeof process !== "undefined" && !!process.versions?.node;
1009
1021
  var toBytes = async (data) => {
1010
1022
  if (typeof Blob !== "undefined" && data instanceof Blob) return new Uint8Array(await data.arrayBuffer());
@@ -1051,7 +1063,8 @@ var resolveUploadSource = async (input, options = {}) => {
1051
1063
  size,
1052
1064
  contentType,
1053
1065
  checksums: buildChecksums(input, sha256),
1054
- requiresFiniteChunkSize: false
1066
+ requiresFiniteChunkSize: false,
1067
+ release: noop
1055
1068
  };
1056
1069
  }
1057
1070
  if (input.path !== void 0) {
@@ -1059,22 +1072,34 @@ var resolveUploadSource = async (input, options = {}) => {
1059
1072
  const fs = await import("fs");
1060
1073
  const size = input.contentLength ?? fs.statSync(input.path).size;
1061
1074
  const sha256 = input.checksumSha256 ?? (hash ? (await hashNodeFile(input.path)).sha256 : "");
1062
- const tusFile = fs.createReadStream(input.path);
1063
- return { tusFile, size, contentType, checksums: buildChecksums(input, sha256), requiresFiniteChunkSize: true };
1075
+ const stream = fs.createReadStream(input.path);
1076
+ return {
1077
+ tusFile: stream,
1078
+ size,
1079
+ contentType,
1080
+ checksums: buildChecksums(input, sha256),
1081
+ requiresFiniteChunkSize: true,
1082
+ release: () => {
1083
+ stream.on("error", noop);
1084
+ stream.destroy();
1085
+ }
1086
+ };
1064
1087
  }
1065
1088
  if (input.stream !== void 0) {
1066
1089
  if (input.contentLength === void 0) {
1067
1090
  throw new Error("Uploading from a stream requires contentLength to be provided.");
1068
1091
  }
1069
- if (hash && !input.checksumSha256) {
1092
+ const sha256 = input.checksumSha256 ?? (input.contentLength === 0 ? EMPTY_SHA256 : void 0);
1093
+ if (hash && !sha256) {
1070
1094
  throw new Error("Uploading from a stream requires checksumSha256 to be provided.");
1071
1095
  }
1072
1096
  return {
1073
1097
  tusFile: input.stream,
1074
1098
  size: input.contentLength,
1075
1099
  contentType,
1076
- checksums: buildChecksums(input, input.checksumSha256 ?? ""),
1077
- requiresFiniteChunkSize: true
1100
+ checksums: buildChecksums(input, sha256 ?? ""),
1101
+ requiresFiniteChunkSize: true,
1102
+ release: noop
1078
1103
  };
1079
1104
  }
1080
1105
  throw new Error("No upload source provided. Pass one of: data, path, or stream.");
@@ -1447,7 +1472,7 @@ var StorageUploadSessionsResource = class extends BaseResource {
1447
1472
  async uploadToUrl(url, source, options) {
1448
1473
  const resolved = await resolveUploadSource({ key: "", ...source }, { hash: false });
1449
1474
  const headers = { "Tus-Resumable": "1.0.0", ...options?.headers };
1450
- await runResolvedUpload(this.transport, url, resolved, options, headers).catch(toUploadError);
1475
+ await runResolvedUpload(this.transport, url, resolved, options, headers).catch(toUploadError).finally(resolved.release);
1451
1476
  }
1452
1477
  /**
1453
1478
  * Resume an interrupted upload to a previously issued signed upload URL. The
@@ -1464,6 +1489,7 @@ var StorageUploadSessionsResource = class extends BaseResource {
1464
1489
 
1465
1490
  // src/resources/application-storage.ts
1466
1491
  var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1492
+ var buildStorageObjectUrl = (cdnHostname, key) => `https://${cdnHostname}/${encodeURIComponent(key).replace(/%2F/g, "/")}`;
1467
1493
  var ApplicationStorageResource = class {
1468
1494
  constructor(httpClient, transport = tusUploadTransport, defaultApplicationId) {
1469
1495
  this.defaultApplicationId = defaultApplicationId;
@@ -1493,6 +1519,10 @@ var ApplicationStorageResource = class {
1493
1519
  * Node filesystem `path`, or a Node readable `stream` (with `contentLength`
1494
1520
  * and `checksumSha256`). The content type is inferred from `key` when omitted.
1495
1521
  *
1522
+ * Empty (zero-byte) files are supported for every input kind. The API stores
1523
+ * them during session creation, so no bytes are transferred, `onProgress` is
1524
+ * not called, `waitForCompletion` does not poll, and `object` is always set.
1525
+ *
1496
1526
  * @param input - What and where to upload, plus optional transfer options.
1497
1527
  * @returns The upload session and public object URL (and the finalized object when `waitForCompletion` is set).
1498
1528
  * @throws {@link UploadError} if the byte upload fails.
@@ -1523,19 +1553,42 @@ var ApplicationStorageResource = class {
1523
1553
  checksumSha1: input.checksumSha1,
1524
1554
  checksumMd5: input.checksumMd5
1525
1555
  });
1526
- const { session, upload } = await this.uploadSessions.create(
1527
- applicationId,
1528
- bucketRef,
1529
- {
1530
- key: input.key,
1531
- contentLength: resolved.size,
1532
- checksumSha256: resolved.checksums.sha256,
1533
- contentType: resolved.contentType,
1534
- checksumSha1: resolved.checksums.sha1,
1535
- checksumMd5: resolved.checksums.md5
1536
- },
1537
- { environment }
1538
- );
1556
+ if (input.signal?.aborted) {
1557
+ resolved.release();
1558
+ throw createAbortError();
1559
+ }
1560
+ let created;
1561
+ try {
1562
+ created = await this.uploadSessions.create(
1563
+ applicationId,
1564
+ bucketRef,
1565
+ {
1566
+ key: input.key,
1567
+ contentLength: resolved.size,
1568
+ checksumSha256: resolved.checksums.sha256,
1569
+ contentType: resolved.contentType,
1570
+ checksumSha1: resolved.checksums.sha1,
1571
+ checksumMd5: resolved.checksums.md5
1572
+ },
1573
+ { environment }
1574
+ );
1575
+ } catch (error) {
1576
+ resolved.release();
1577
+ throw error;
1578
+ }
1579
+ const { session, upload } = created;
1580
+ if (upload === null) {
1581
+ resolved.release();
1582
+ const object = created.object;
1583
+ if (!object) {
1584
+ throw new UploadError("The upload session completed without returning the stored object.");
1585
+ }
1586
+ const url = created.publicObjectUrl ?? buildStorageObjectUrl(
1587
+ (await this.buckets.get(applicationId, bucketRef, { environment })).cdnHostname,
1588
+ object.key
1589
+ );
1590
+ return { session, url, object };
1591
+ }
1539
1592
  await runResolvedUpload(
1540
1593
  this.transport,
1541
1594
  upload.url,
@@ -1550,7 +1603,7 @@ var ApplicationStorageResource = class {
1550
1603
  },
1551
1604
  // Forward any required headers the API issued with the session.
1552
1605
  { "Tus-Resumable": "1.0.0", ...upload.headers }
1553
- ).catch(toUploadError);
1606
+ ).catch(toUploadError).finally(resolved.release);
1554
1607
  if (input.waitForCompletion) {
1555
1608
  const options = completionOptions ?? {};
1556
1609
  const completed = await this.waitForCompletion(applicationId, bucketRef, session.id, {
@@ -1629,6 +1682,201 @@ var ApplicationStorageResource = class {
1629
1682
  }
1630
1683
  };
1631
1684
 
1685
+ // src/resources/domains.ts
1686
+ var DomainNotActiveError = class extends GigadriveError {
1687
+ /** The domain as of the last successful check. */
1688
+ domain;
1689
+ /** `timeout` when the wait ran out, `removing` when the domain was removed, otherwise the state that ended it. */
1690
+ reason;
1691
+ constructor(domain, reason) {
1692
+ const detail = domain.error ? `: ${domain.error.message}` : "";
1693
+ super(
1694
+ reason === "timeout" ? `${domain.unicodeHostname} is not active yet (state: ${domain.state})${detail}` : `${domain.unicodeHostname} is ${reason}${detail}`
1695
+ );
1696
+ this.name = "DomainNotActiveError";
1697
+ this.domain = domain;
1698
+ this.reason = reason;
1699
+ }
1700
+ };
1701
+ var SERVING_STATES = /* @__PURE__ */ new Set(["active", "degraded"]);
1702
+ var isTransient = (error) => error instanceof ApiError ? error.status === 429 || error.status >= 500 : error instanceof TypeError;
1703
+ var pollSignal = (ms, signal) => {
1704
+ const controller = new AbortController();
1705
+ const timer = setTimeout(() => controller.abort(new DOMException("The operation timed out.", "TimeoutError")), ms);
1706
+ const onAbort = () => controller.abort(signal?.reason);
1707
+ if (signal?.aborted) onAbort();
1708
+ else signal?.addEventListener("abort", onAbort, { once: true });
1709
+ return {
1710
+ signal: controller.signal,
1711
+ dispose: () => {
1712
+ clearTimeout(timer);
1713
+ signal?.removeEventListener("abort", onAbort);
1714
+ }
1715
+ };
1716
+ };
1717
+ var sleep = (ms, signal) => new Promise((resolve, reject) => {
1718
+ if (signal?.aborted) {
1719
+ reject(signal.reason instanceof Error ? signal.reason : new GigadriveError("Aborted"));
1720
+ return;
1721
+ }
1722
+ const timer = setTimeout(() => {
1723
+ signal?.removeEventListener("abort", onAbort);
1724
+ resolve();
1725
+ }, ms);
1726
+ const onAbort = () => {
1727
+ clearTimeout(timer);
1728
+ reject(signal?.reason instanceof Error ? signal.reason : new GigadriveError("Aborted"));
1729
+ };
1730
+ signal?.addEventListener("abort", onAbort, { once: true });
1731
+ });
1732
+ var ApplicationDomainsResource = class extends BaseResource {
1733
+ /**
1734
+ * List the custom domains of an application.
1735
+ *
1736
+ * @example
1737
+ * ```ts
1738
+ * const { items } = await client.applications.domains.list('app-id');
1739
+ * for (const domain of items) console.log(domain.hostname, domain.state);
1740
+ * ```
1741
+ */
1742
+ async list(applicationId, query) {
1743
+ return this.httpClient.get(`/applications/${applicationId}/domains`, {
1744
+ query
1745
+ });
1746
+ }
1747
+ /**
1748
+ * Attach a hostname to an application.
1749
+ *
1750
+ * @returns The new domain, including the `requiredRecords` to publish at your DNS provider.
1751
+ *
1752
+ * @example
1753
+ * ```ts
1754
+ * const domain = await client.applications.domains.add('app-id', { hostname: 'shop.example.com' });
1755
+ * for (const record of domain.requiredRecords) console.log(record.type, record.host, record.value);
1756
+ * ```
1757
+ */
1758
+ async add(applicationId, data) {
1759
+ return this.httpClient.post(`/applications/${applicationId}/domains`, data);
1760
+ }
1761
+ /** Get one custom domain with its current state. */
1762
+ async get(applicationId, domainId, options) {
1763
+ const path = `/applications/${applicationId}/domains/${domainId}`;
1764
+ return options?.signal ? this.httpClient.get(path, { signal: options.signal }) : this.httpClient.get(path);
1765
+ }
1766
+ /**
1767
+ * Change what a domain serves, or make it the primary domain.
1768
+ *
1769
+ * @example
1770
+ * ```ts
1771
+ * await client.applications.domains.update('app-id', 'domain-id', {
1772
+ * target: { type: 'redirect', to: 'www.example.com' },
1773
+ * });
1774
+ * ```
1775
+ */
1776
+ async update(applicationId, domainId, data) {
1777
+ return this.httpClient.patch(`/applications/${applicationId}/domains/${domainId}`, data);
1778
+ }
1779
+ /** Check the domain now instead of waiting for the next scheduled check. Rate limited. */
1780
+ async refresh(applicationId, domainId) {
1781
+ return this.httpClient.post(`/applications/${applicationId}/domains/${domainId}/refresh`);
1782
+ }
1783
+ /** Stop serving the domain and remove it. */
1784
+ async remove(applicationId, domainId) {
1785
+ return this.httpClient.delete(`/applications/${applicationId}/domains/${domainId}`);
1786
+ }
1787
+ /**
1788
+ * Move a hostname another organization attached to this application. Verify the domain for your
1789
+ * organization first and keep its TXT record published: the claim checks the record again.
1790
+ */
1791
+ async claim(applicationId, hostname) {
1792
+ return this.httpClient.post(`/applications/${applicationId}/domains/claim`, { hostname });
1793
+ }
1794
+ /**
1795
+ * Poll a domain until it serves traffic.
1796
+ *
1797
+ * Rate limits, server errors and network failures do not end the wait; the next poll retries until
1798
+ * the timeout. Aborting the signal or reaching the timeout also cancels a request in flight.
1799
+ *
1800
+ * @returns The domain once it is `active` (or `degraded`, which also serves).
1801
+ * @throws {DomainNotActiveError} When it fails, is suspended or removed, or the timeout runs out.
1802
+ *
1803
+ * @example
1804
+ * ```ts
1805
+ * const domain = await client.applications.domains.add('app-id', { hostname: 'shop.example.com' });
1806
+ * await client.applications.domains.waitUntilActive('app-id', domain.id, {
1807
+ * onState: (current) => console.log(current.state),
1808
+ * });
1809
+ * ```
1810
+ */
1811
+ async waitUntilActive(applicationId, domainId, options = {}) {
1812
+ const { timeoutMs = 10 * 6e4, intervalMs = 5e3, signal, onState } = options;
1813
+ const deadline = Date.now() + timeoutMs;
1814
+ let last;
1815
+ for (; ; ) {
1816
+ const poll = pollSignal(Math.max(deadline - Date.now(), 1), signal);
1817
+ let domain;
1818
+ try {
1819
+ domain = await this.get(applicationId, domainId, { signal: poll.signal });
1820
+ } catch (error) {
1821
+ if (signal?.aborted) throw error;
1822
+ if (last && error instanceof ApiError && error.status === 404) throw new DomainNotActiveError(last, "removing");
1823
+ if (!poll.signal.aborted && !isTransient(error)) throw error;
1824
+ if (Date.now() >= deadline && !last) throw error;
1825
+ } finally {
1826
+ poll.dispose();
1827
+ }
1828
+ if (domain) {
1829
+ if (domain.state !== last?.state) onState?.(domain);
1830
+ last = domain;
1831
+ if (SERVING_STATES.has(domain.state)) return domain;
1832
+ if (domain.state === "failed" || domain.state === "suspended" || domain.state === "removing") {
1833
+ throw new DomainNotActiveError(domain, domain.state);
1834
+ }
1835
+ }
1836
+ const untilDeadline = deadline - Date.now();
1837
+ if (untilDeadline <= 0) {
1838
+ if (last) throw new DomainNotActiveError(last, "timeout");
1839
+ throw new GigadriveError("Timed out before the domain could be read");
1840
+ }
1841
+ await sleep(Math.min(intervalMs, untilDeadline), signal);
1842
+ }
1843
+ }
1844
+ };
1845
+ var OrganizationDomainsResource = class extends BaseResource {
1846
+ /** List the organization's claimed and verified domains. */
1847
+ async list(organizationId, query) {
1848
+ return this.httpClient.get(`/organizations/${organizationId}/domains`, {
1849
+ query
1850
+ });
1851
+ }
1852
+ /**
1853
+ * Claim a domain for verification.
1854
+ *
1855
+ * @returns The claim, including the TXT `record` to publish.
1856
+ *
1857
+ * @example
1858
+ * ```ts
1859
+ * const claim = await client.organizations.domains.add('org-id', 'example.com');
1860
+ * console.log(claim.record.host, claim.record.value);
1861
+ * ```
1862
+ */
1863
+ async add(organizationId, name) {
1864
+ return this.httpClient.post(`/organizations/${organizationId}/domains`, { name });
1865
+ }
1866
+ /** Get one claimed or verified domain. */
1867
+ async get(organizationId, domainId) {
1868
+ return this.httpClient.get(`/organizations/${organizationId}/domains/${domainId}`);
1869
+ }
1870
+ /** Check the TXT record now. Rate limited. */
1871
+ async verify(organizationId, domainId) {
1872
+ return this.httpClient.post(`/organizations/${organizationId}/domains/${domainId}/verify`);
1873
+ }
1874
+ /** Remove a domain. Refused while custom domains still rely on it. */
1875
+ async remove(organizationId, domainId) {
1876
+ return this.httpClient.delete(`/organizations/${organizationId}/domains/${domainId}`);
1877
+ }
1878
+ };
1879
+
1632
1880
  // src/resources/applications.ts
1633
1881
  var ApplicationsResource = class extends BaseResource {
1634
1882
  /**
@@ -1670,11 +1918,22 @@ var ApplicationsResource = class extends BaseResource {
1670
1918
  * ```
1671
1919
  */
1672
1920
  requests;
1921
+ /**
1922
+ * Custom domains attached to the application.
1923
+ *
1924
+ * @example
1925
+ * ```ts
1926
+ * const domain = await client.applications.domains.add('app-id', { hostname: 'shop.example.com' });
1927
+ * await client.applications.domains.waitUntilActive('app-id', domain.id);
1928
+ * ```
1929
+ */
1930
+ domains;
1673
1931
  constructor(httpClient, defaultApplicationId) {
1674
1932
  super(httpClient);
1675
1933
  this.envVars = new ApplicationEnvVarsResource(this.httpClient);
1676
1934
  this.storage = new ApplicationStorageResource(this.httpClient, void 0, defaultApplicationId);
1677
1935
  this.requests = new ApplicationRequestsResource(this.httpClient);
1936
+ this.domains = new ApplicationDomainsResource(this.httpClient);
1678
1937
  }
1679
1938
  /**
1680
1939
  * List applications the authenticated actor has access to.
@@ -2300,12 +2559,23 @@ var OrganizationsResource = class extends BaseResource {
2300
2559
  * ```
2301
2560
  */
2302
2561
  products;
2562
+ /**
2563
+ * Domains the organization verified. A verified domain lets every application attach hostnames
2564
+ * below it without another TXT record.
2565
+ *
2566
+ * @example
2567
+ * ```ts
2568
+ * const claim = await client.organizations.domains.add('org-id', 'example.com');
2569
+ * ```
2570
+ */
2571
+ domains;
2303
2572
  constructor(...args) {
2304
2573
  super(...args);
2305
2574
  this.envVars = new OrganizationEnvVarsResource(this.httpClient);
2306
2575
  this.aiGateway = new OrganizationAiGatewayResource(this.httpClient);
2307
2576
  this.members = new OrganizationMembersResource(this.httpClient);
2308
2577
  this.products = new OrganizationProductsResource(this.httpClient);
2578
+ this.domains = new OrganizationDomainsResource(this.httpClient);
2309
2579
  }
2310
2580
  /**
2311
2581
  * List organizations the authenticated actor has access to.
@@ -2359,6 +2629,783 @@ var OrganizationsResource = class extends BaseResource {
2359
2629
  }
2360
2630
  };
2361
2631
 
2632
+ // src/queues/encoding.ts
2633
+ var DURATION_PATTERN = /^(\d+(?:\.\d+)?)\s*(ms|s|m|h|d)$/;
2634
+ var UNIT_SECONDS = { ms: 1e-3, s: 1, m: 60, h: 3600, d: 86400 };
2635
+ var durationToSeconds = (value) => {
2636
+ let seconds;
2637
+ if (typeof value === "number") {
2638
+ seconds = value;
2639
+ } else {
2640
+ const match = DURATION_PATTERN.exec(value.trim());
2641
+ if (!match) throw new Error(`Invalid duration "${value}". Use seconds or a string such as 30s, 10m, 12h or 7d.`);
2642
+ seconds = Number(match[1]) * UNIT_SECONDS[match[2]];
2643
+ }
2644
+ if (!Number.isFinite(seconds) || seconds < 0) throw new Error(`Invalid duration "${String(value)}"`);
2645
+ return Math.ceil(seconds);
2646
+ };
2647
+ var textEncoder = new TextEncoder();
2648
+ var textDecoder = new TextDecoder();
2649
+ var utf8Encode = (value) => textEncoder.encode(value);
2650
+ var utf8Decode = (bytes) => textDecoder.decode(bytes);
2651
+ var bytesToBase64 = (bytes) => {
2652
+ let binary = "";
2653
+ for (let index = 0; index < bytes.length; index += 32768) {
2654
+ binary += String.fromCharCode(...bytes.subarray(index, index + 32768));
2655
+ }
2656
+ return btoa(binary);
2657
+ };
2658
+ var base64ToBytes = (value) => {
2659
+ const binary = atob(value);
2660
+ const bytes = new Uint8Array(binary.length);
2661
+ for (let index = 0; index < binary.length; index++) bytes[index] = binary.charCodeAt(index);
2662
+ return bytes;
2663
+ };
2664
+ var toHex2 = (buffer) => Array.from(new Uint8Array(buffer), (byte) => byte.toString(16).padStart(2, "0")).join("");
2665
+ var toArrayBuffer = (bytes) => {
2666
+ const copy = new ArrayBuffer(bytes.byteLength);
2667
+ new Uint8Array(copy).set(bytes);
2668
+ return copy;
2669
+ };
2670
+
2671
+ // src/queues/errors.ts
2672
+ var RetryLaterError = class extends GigadriveError {
2673
+ /** Seconds until the next delivery. */
2674
+ delaySeconds;
2675
+ constructor(delay2, message = "Message deferred") {
2676
+ super(message);
2677
+ this.name = "RetryLaterError";
2678
+ this.delaySeconds = durationToSeconds(delay2);
2679
+ }
2680
+ };
2681
+ var NonRetryableError = class extends GigadriveError {
2682
+ constructor(message) {
2683
+ super(message);
2684
+ this.name = "NonRetryableError";
2685
+ }
2686
+ };
2687
+
2688
+ // src/queues/signature.ts
2689
+ var QUEUE_SIGNATURE_HEADER = "x-gigadrive-queue-signature";
2690
+ var QUEUE_SIGNATURE_TOLERANCE_MS = 5 * 6e4;
2691
+ var QUEUE_SIGNING_SECRET_ENV = "GIGADRIVE_QUEUE_SIGNING_SECRET";
2692
+ var hmacSha256Hex = async (secret, payload) => {
2693
+ const key = await crypto.subtle.importKey(
2694
+ "raw",
2695
+ toArrayBuffer(utf8Encode(secret)),
2696
+ { name: "HMAC", hash: "SHA-256" },
2697
+ false,
2698
+ ["sign"]
2699
+ );
2700
+ return toHex2(await crypto.subtle.sign("HMAC", key, toArrayBuffer(utf8Encode(payload))));
2701
+ };
2702
+ var signedPayload = async (input, timestamp) => {
2703
+ const body = typeof input.body === "string" ? utf8Encode(input.body) : input.body;
2704
+ const bodyHash = toHex2(await crypto.subtle.digest("SHA-256", toArrayBuffer(body)));
2705
+ return `${String(timestamp)}.${input.queue}.${input.messageId}.${String(input.attempt)}.${bodyHash}`;
2706
+ };
2707
+ var safeEqual = (left, right) => {
2708
+ if (left.length !== right.length) return false;
2709
+ let difference = 0;
2710
+ for (let index = 0; index < left.length; index++) difference |= left.charCodeAt(index) ^ right.charCodeAt(index);
2711
+ return difference === 0;
2712
+ };
2713
+ async function signQueueDelivery(input, timestamp = Date.now()) {
2714
+ return `t=${String(timestamp)},v1=${await hmacSha256Hex(input.secret, await signedPayload(input, timestamp))}`;
2715
+ }
2716
+ async function verifyQueueSignature(header, input, now = Date.now()) {
2717
+ if (!header || !input.secret) return false;
2718
+ const fields = /* @__PURE__ */ new Map();
2719
+ for (const part of header.split(",")) {
2720
+ const separator = part.indexOf("=");
2721
+ if (separator > 0) fields.set(part.slice(0, separator).trim(), part.slice(separator + 1).trim());
2722
+ }
2723
+ const timestamp = Number(fields.get("t"));
2724
+ const signature = fields.get("v1");
2725
+ if (!Number.isSafeInteger(timestamp) || !signature || Math.abs(now - timestamp) > QUEUE_SIGNATURE_TOLERANCE_MS) {
2726
+ return false;
2727
+ }
2728
+ const expected = await hmacSha256Hex(input.secret, await signedPayload(input, timestamp));
2729
+ return safeEqual(signature.toLowerCase(), expected);
2730
+ }
2731
+
2732
+ // src/queues/queue.ts
2733
+ var QUEUE_DELIVERY_HEADERS = {
2734
+ name: "x-gigadrive-queue-name",
2735
+ messageId: "x-gigadrive-queue-message-id",
2736
+ attempt: "x-gigadrive-queue-attempt",
2737
+ createdAt: "x-gigadrive-queue-created-at",
2738
+ expiresAt: "x-gigadrive-queue-expires-at",
2739
+ deadline: "x-gigadrive-queue-deadline",
2740
+ groupKey: "x-gigadrive-queue-group-key",
2741
+ deduplicationKey: "x-gigadrive-queue-deduplication-key",
2742
+ deploymentId: "x-gigadrive-queue-deployment-id",
2743
+ deploymentFallback: "x-gigadrive-queue-deployment-fallback",
2744
+ signature: QUEUE_SIGNATURE_HEADER
2745
+ };
2746
+ var QUEUE_RESPONSE_HEADERS = {
2747
+ /** On a 2xx: deliver again after this many seconds without spending an attempt. */
2748
+ retryAfter: "x-gigadrive-queue-retry-after",
2749
+ /** `dead-letter`: stop retrying now, whatever the status. */
2750
+ action: "x-gigadrive-queue-action",
2751
+ /**
2752
+ * Reason recorded on the message. Read only together with `action:
2753
+ * dead-letter`; for any other failure the platform records `HTTP <status>`
2754
+ * and the first 512 bytes of the response body.
2755
+ */
2756
+ error: "x-gigadrive-queue-error"
2757
+ };
2758
+ var isJsonContentType = (contentType) => /^application\/(?:[\w.+-]+\+)?json\b/i.test(contentType);
2759
+ var defaultQueueCodec = {
2760
+ encode(payload) {
2761
+ if (payload instanceof Uint8Array || payload instanceof ArrayBuffer) {
2762
+ const bytes = payload instanceof Uint8Array ? payload : new Uint8Array(payload);
2763
+ return { body: bytesToBase64(bytes), encoding: "base64", contentType: "application/octet-stream" };
2764
+ }
2765
+ const json = JSON.stringify(payload);
2766
+ if (json === void 0) throw new TypeError("Queue payloads must be JSON-serializable or binary");
2767
+ return { body: json, encoding: "utf8", contentType: "application/json" };
2768
+ },
2769
+ decode(body, contentType) {
2770
+ if (isJsonContentType(contentType)) return body.length === 0 ? null : JSON.parse(utf8Decode(body));
2771
+ if (/^text\//i.test(contentType)) return utf8Decode(body);
2772
+ return body;
2773
+ }
2774
+ };
2775
+ var errorText = (error) => (error instanceof Error ? error.message : String(error)).slice(0, 2e3);
2776
+ var headerText = (text) => text.replace(/[^\x20-\x7e]/g, " ").trim().slice(0, 500);
2777
+ var toDate = (value) => {
2778
+ if (!value) return null;
2779
+ const date = new Date(/^\d+$/.test(value) ? Number(value) : value);
2780
+ return Number.isNaN(date.getTime()) ? null : date;
2781
+ };
2782
+ var BATCH_LIMIT = 100;
2783
+ var ACK_FLUSH_MS = 25;
2784
+ var ACK_RETRY_DELAYS_MS = [200, 1e3];
2785
+ var EMPTY_POLL_MIN_MS = 1e3;
2786
+ var EMPTY_POLL_MAX_MS = 5e3;
2787
+ var sleep2 = (ms, signal) => new Promise((resolve) => {
2788
+ if (signal?.aborted) return resolve();
2789
+ const timer = setTimeout(done, ms);
2790
+ function done() {
2791
+ clearTimeout(timer);
2792
+ signal?.removeEventListener("abort", done);
2793
+ resolve();
2794
+ }
2795
+ signal?.addEventListener("abort", done, { once: true });
2796
+ });
2797
+ var Queue = class {
2798
+ constructor(name, resource, options = {}) {
2799
+ this.name = name;
2800
+ this.resource = resource;
2801
+ this.options = options;
2802
+ this.codec = options.codec ?? defaultQueueCodec;
2803
+ }
2804
+ name;
2805
+ resource;
2806
+ options;
2807
+ codec;
2808
+ get scope() {
2809
+ return { applicationId: this.options.applicationId, environment: this.options.environment };
2810
+ }
2811
+ toMessage(payload, options = {}) {
2812
+ const encoded = this.codec.encode(payload);
2813
+ const at = options.at === void 0 ? void 0 : new Date(options.at);
2814
+ if (at && Number.isNaN(at.getTime())) throw new ConfigurationError("Invalid `at` time for a queue message");
2815
+ return {
2816
+ body: encoded.body,
2817
+ encoding: encoded.encoding,
2818
+ contentType: encoded.contentType,
2819
+ ...options.headers ? { headers: options.headers } : {},
2820
+ ...at ? { deliverAt: at.toISOString() } : {},
2821
+ ...options.delay !== void 0 && !at ? { delaySeconds: durationToSeconds(options.delay) } : {},
2822
+ ...options.groupKey ? { groupKey: options.groupKey } : {},
2823
+ ...options.deduplicationKey ? { deduplicationKey: options.deduplicationKey } : {},
2824
+ ...options.deploymentId ? { deploymentId: options.deploymentId } : {}
2825
+ };
2826
+ }
2827
+ decode(body, contentType) {
2828
+ const decoded = this.codec.decode(body, contentType);
2829
+ return this.options.validate ? this.options.validate(decoded) : decoded;
2830
+ }
2831
+ /**
2832
+ * Sends one message.
2833
+ *
2834
+ * @example
2835
+ * ```ts
2836
+ * await emails.send({ to: 'jane@example.com' });
2837
+ * await emails.send({ to: 'jane@example.com' }, { delay: '10m' });
2838
+ * await emails.send(reminder, { at: new Date('2027-01-01T09:00:00Z'), deduplicationKey: `reminder:${id}` });
2839
+ * ```
2840
+ *
2841
+ * @throws {@link ApiError} when the message is refused, for example with
2842
+ * `code: 'backlog_full'` when your organization's queued storage is full.
2843
+ * Its `status` is 429 when sending again later may succeed and 400 when it
2844
+ * will not, so retry logic that keys on the status stays correct.
2845
+ */
2846
+ async send(payload, options) {
2847
+ const result = await this.resource().send(this.name, this.toMessage(payload, options), {
2848
+ ...this.scope,
2849
+ autoCreate: this.options.autoCreate
2850
+ });
2851
+ if (result.messageId === null) throw new ApiError(result.error, result.retryable ? 429 : 400, result.code);
2852
+ return { messageId: result.messageId, deduplicated: result.deduplicated, deliverAt: new Date(result.deliverAt) };
2853
+ }
2854
+ /**
2855
+ * Sends many messages, 100 per request. Results come back in input order;
2856
+ * a refused message does not fail the rest; its error carries status 429
2857
+ * when it may be sent again later and 400 when it may not. Chunks are split by count only:
2858
+ * a request whose message bodies exceed 4 MiB in total is refused whole.
2859
+ */
2860
+ async sendBatch(messages) {
2861
+ const results = [];
2862
+ for (let index = 0; index < messages.length; index += BATCH_LIMIT) {
2863
+ const chunk = messages.slice(index, index + BATCH_LIMIT);
2864
+ const sent = await this.resource().sendBatch(
2865
+ this.name,
2866
+ chunk.map(({ payload, ...options }) => this.toMessage(payload, options)),
2867
+ { ...this.scope, autoCreate: this.options.autoCreate }
2868
+ );
2869
+ for (const result of sent) {
2870
+ results.push(
2871
+ result.messageId === null ? { messageId: null, error: new ApiError(result.error, result.retryable ? 429 : 400, result.code) } : { messageId: result.messageId, deduplicated: result.deduplicated, deliverAt: new Date(result.deliverAt) }
2872
+ );
2873
+ }
2874
+ }
2875
+ return results;
2876
+ }
2877
+ /** Deletes a message that has not been processed yet, such as a scheduled send. */
2878
+ async cancel(messageId) {
2879
+ await this.resource().deleteMessage(this.name, messageId, this.scope);
2880
+ }
2881
+ /**
2882
+ * Leases messages from a pull queue. Waits up to `wait` (at most 20
2883
+ * seconds) for the first message, then returns what is available.
2884
+ */
2885
+ async receive(options = {}) {
2886
+ const records = await this.resource().receive(
2887
+ this.name,
2888
+ {
2889
+ maxMessages: options.maxMessages ?? 1,
2890
+ ...options.wait === void 0 ? {} : { waitSeconds: Math.min(durationToSeconds(options.wait), 20) },
2891
+ ...options.visibilityTimeout === void 0 ? {} : { visibilityTimeoutSeconds: durationToSeconds(options.visibilityTimeout) }
2892
+ },
2893
+ { ...this.scope, signal: options.signal }
2894
+ );
2895
+ return records.map((record) => this.toReceived(record));
2896
+ }
2897
+ toReceived(record) {
2898
+ const resource = this.resource();
2899
+ const receipt = record.receipt ?? "";
2900
+ const bytes = record.encoding === "base64" ? base64ToBytes(record.body) : utf8Encode(record.body);
2901
+ const nack = (request) => resource.nack(this.name, record.id, { receipt, ...request }, this.scope).then(() => void 0);
2902
+ let payload;
2903
+ let decodeError;
2904
+ try {
2905
+ payload = this.decode(bytes, record.contentType);
2906
+ } catch (error) {
2907
+ decodeError = error;
2908
+ }
2909
+ return {
2910
+ messageId: record.id,
2911
+ queue: this.name,
2912
+ attempt: record.attempts,
2913
+ createdAt: toDate(record.createdAt),
2914
+ groupKey: record.groupKey,
2915
+ deduplicationKey: record.deduplicationKey,
2916
+ deploymentId: record.deploymentId,
2917
+ headers: record.headers,
2918
+ receipt,
2919
+ leaseExpiresAt: toDate(record.leaseExpiresAt),
2920
+ get payload() {
2921
+ if (decodeError !== void 0) throw new NonRetryableError(`Undecodable payload: ${errorText(decodeError)}`);
2922
+ return payload;
2923
+ },
2924
+ ack: () => resource.ack(this.name, record.id, receipt, this.scope).then(() => void 0),
2925
+ retry: (options = {}) => nack({
2926
+ ...options.delay === void 0 ? {} : { delaySeconds: durationToSeconds(options.delay) },
2927
+ ...options.error ? { error: options.error } : {}
2928
+ }),
2929
+ defer: (delay2) => nack({ delaySeconds: durationToSeconds(delay2), countAttempt: false }),
2930
+ deadLetter: (reason) => nack({ deadLetter: true, ...reason ? { error: reason } : {} }),
2931
+ extendLease: (duration) => resource.extend(this.name, record.id, receipt, durationToSeconds(duration), this.scope).then(() => void 0)
2932
+ };
2933
+ }
2934
+ /**
2935
+ * Receives and processes messages until `signal` aborts, or until the queue
2936
+ * is empty with `stopWhenEmpty`. A handler that returns acknowledges the
2937
+ * message; {@link RetryLaterError} defers it, {@link NonRetryableError}
2938
+ * dead-letters it, and any other error retries it with backoff.
2939
+ *
2940
+ * @example
2941
+ * ```ts
2942
+ * const controller = new AbortController();
2943
+ * await jobs.consume(async (job) => processJob(job), { maxMessages: 10, signal: controller.signal });
2944
+ * ```
2945
+ */
2946
+ async consume(handler, options = {}) {
2947
+ const wait = options.wait ?? (options.stopWhenEmpty ? 0 : 20);
2948
+ const waitMs = Math.min(durationToSeconds(wait), 20) * 1e3;
2949
+ const pending = [];
2950
+ let timer;
2951
+ const sendAcks = async (batch) => {
2952
+ const resource = this.resource();
2953
+ for (let attempt = 0; ; attempt += 1) {
2954
+ try {
2955
+ const { results } = await resource.ackBatch(
2956
+ this.name,
2957
+ batch.map((message) => ({ messageId: message.messageId, receipt: message.receipt })),
2958
+ this.scope
2959
+ );
2960
+ return batch.map((_message, index) => {
2961
+ const result = results[index];
2962
+ if (result?.acknowledged || attempt > 0 && result?.code === "message_not_found") return void 0;
2963
+ const code = result?.code ?? "no_result";
2964
+ return new ApiError(
2965
+ `Acknowledging message failed: ${code}`,
2966
+ code === "message_not_found" ? 404 : 409,
2967
+ code
2968
+ );
2969
+ });
2970
+ } catch (error) {
2971
+ if (error instanceof ApiError && error.status === 404 && error.code !== "queue_not_found") {
2972
+ const settled = await Promise.allSettled(
2973
+ batch.map((message) => resource.ack(this.name, message.messageId, message.receipt, this.scope))
2974
+ );
2975
+ return settled.map(
2976
+ (outcome) => outcome.status === "fulfilled" ? void 0 : outcome.reason instanceof Error ? outcome.reason : new Error(errorText(outcome.reason))
2977
+ );
2978
+ }
2979
+ const retryable = !(error instanceof ApiError) || error.status >= 500 || error.status === 429;
2980
+ const delay2 = ACK_RETRY_DELAYS_MS[attempt];
2981
+ if (!retryable || delay2 === void 0) throw error;
2982
+ await sleep2(delay2, void 0);
2983
+ }
2984
+ }
2985
+ };
2986
+ const flush = () => {
2987
+ clearTimeout(timer);
2988
+ timer = void 0;
2989
+ const batch = pending.splice(0, BATCH_LIMIT);
2990
+ if (pending.length > 0) timer = setTimeout(flush, 0);
2991
+ if (batch.length === 0) return;
2992
+ sendAcks(batch.map(({ message }) => message)).then((errors) => batch.forEach(({ settled }, index) => settled(errors[index]))).catch((error) => batch.forEach(({ settled }) => settled(error)));
2993
+ };
2994
+ const acknowledge = (message) => new Promise((resolve, reject) => {
2995
+ pending.push({
2996
+ message,
2997
+ settled: (error) => {
2998
+ if (error === void 0) resolve();
2999
+ else reject(error instanceof Error ? error : new Error(errorText(error)));
3000
+ }
3001
+ });
3002
+ if (pending.length >= BATCH_LIMIT) flush();
3003
+ else timer ??= setTimeout(flush, ACK_FLUSH_MS);
3004
+ });
3005
+ while (!options.signal?.aborted) {
3006
+ let messages;
3007
+ const started = Date.now();
3008
+ try {
3009
+ messages = await this.receive({
3010
+ maxMessages: options.maxMessages ?? 10,
3011
+ wait,
3012
+ visibilityTimeout: options.visibilityTimeout,
3013
+ signal: options.signal
3014
+ });
3015
+ } catch (error) {
3016
+ if (options.signal?.aborted) return;
3017
+ throw error;
3018
+ }
3019
+ if (messages.length === 0) {
3020
+ if (options.stopWhenEmpty) return;
3021
+ const remaining = Math.max(waitMs, EMPTY_POLL_MIN_MS) - (Date.now() - started);
3022
+ if (remaining > 0) await sleep2(Math.min(remaining, EMPTY_POLL_MAX_MS), options.signal);
3023
+ continue;
3024
+ }
3025
+ await Promise.all(
3026
+ messages.map(async (message) => {
3027
+ let outcome;
3028
+ try {
3029
+ await handler(message.payload, message);
3030
+ outcome = acknowledge(message);
3031
+ } catch (error) {
3032
+ outcome = error instanceof RetryLaterError ? message.defer(error.delaySeconds) : error instanceof NonRetryableError ? message.deadLetter(error.message) : message.retry({ error: errorText(error) });
3033
+ }
3034
+ try {
3035
+ await outcome;
3036
+ } catch (settleError) {
3037
+ options.onError?.(settleError);
3038
+ }
3039
+ })
3040
+ );
3041
+ }
3042
+ }
3043
+ /**
3044
+ * Builds a push consumer: a `(request: Request) => Promise<Response>`
3045
+ * function for the queue's consumer path, such as a Next.js route handler.
3046
+ *
3047
+ * It verifies the delivery signature, decodes the payload and calls
3048
+ * `handler`. Returning acknowledges the message. Throw
3049
+ * {@link RetryLaterError} to defer it without spending an attempt,
3050
+ * {@link NonRetryableError} to dead-letter it, or anything else to retry
3051
+ * with backoff.
3052
+ *
3053
+ * @example
3054
+ * ```ts
3055
+ * // app/api/queues/emails/route.ts
3056
+ * export const POST = emails.handler(async (email, { attempt }) => {
3057
+ * await sendEmail(email);
3058
+ * });
3059
+ * ```
3060
+ */
3061
+ handler(handler) {
3062
+ return async (request) => {
3063
+ const header = (name) => request.headers.get(name);
3064
+ const messageId = header(QUEUE_DELIVERY_HEADERS.messageId);
3065
+ const queueName = header(QUEUE_DELIVERY_HEADERS.name);
3066
+ const attempt = Number(header(QUEUE_DELIVERY_HEADERS.attempt) ?? "1");
3067
+ if (!messageId || !queueName) {
3068
+ return Response.json({ error: "Not a queue delivery" }, { status: 400 });
3069
+ }
3070
+ if (queueName !== this.name) {
3071
+ console.error(`[gigadrive] Rejected a delivery for queue "${queueName}" at the handler of "${this.name}"`);
3072
+ return Response.json({ error: `This endpoint consumes queue "${this.name}"` }, { status: 400 });
3073
+ }
3074
+ const body = new Uint8Array(await request.arrayBuffer());
3075
+ if (!this.options.allowUnsigned) {
3076
+ const secret = this.options.signingSecret ?? readSigningSecret();
3077
+ if (!secret) {
3078
+ console.error(
3079
+ `[gigadrive] Rejected a queue delivery: ${QUEUE_SIGNING_SECRET_ENV} is not set. Deployments receive it automatically; set allowUnsigned for local development.`
3080
+ );
3081
+ return Response.json({ error: "Queue signing secret is not configured" }, { status: 500 });
3082
+ }
3083
+ const valid = await verifyQueueSignature(header(QUEUE_SIGNATURE_HEADER), {
3084
+ secret,
3085
+ queue: queueName,
3086
+ messageId,
3087
+ attempt,
3088
+ body
3089
+ });
3090
+ if (!valid) return Response.json({ error: "Invalid queue signature" }, { status: 401 });
3091
+ }
3092
+ const meta = {
3093
+ messageId,
3094
+ queue: queueName,
3095
+ attempt,
3096
+ createdAt: toDate(header(QUEUE_DELIVERY_HEADERS.createdAt)),
3097
+ groupKey: header(QUEUE_DELIVERY_HEADERS.groupKey),
3098
+ deduplicationKey: header(QUEUE_DELIVERY_HEADERS.deduplicationKey),
3099
+ deploymentId: header(QUEUE_DELIVERY_HEADERS.deploymentId),
3100
+ headers: Object.fromEntries(
3101
+ [...request.headers.entries()].filter(([name]) => !name.startsWith("x-gigadrive-queue-"))
3102
+ )
3103
+ };
3104
+ let payload;
3105
+ try {
3106
+ payload = this.decode(body, header("content-type") ?? "application/octet-stream");
3107
+ } catch (error) {
3108
+ return deadLetterResponse(`Undecodable payload: ${errorText(error)}`);
3109
+ }
3110
+ try {
3111
+ await handler(payload, meta);
3112
+ return Response.json({ ok: true });
3113
+ } catch (error) {
3114
+ if (error instanceof RetryLaterError) {
3115
+ return Response.json(
3116
+ { ok: true, retryAfter: error.delaySeconds },
3117
+ { headers: { [QUEUE_RESPONSE_HEADERS.retryAfter]: String(error.delaySeconds) } }
3118
+ );
3119
+ }
3120
+ if (error instanceof NonRetryableError) return deadLetterResponse(error.message);
3121
+ console.error(`[gigadrive] Queue handler for "${queueName}" failed (attempt ${String(attempt)})`, error);
3122
+ return Response.json({ error: errorText(error) }, { status: 500 });
3123
+ }
3124
+ };
3125
+ }
3126
+ /** Creates the queue, or applies `settings` to it. Use it to make a push queue from code. */
3127
+ async ensure(settings = {}) {
3128
+ return this.resource().ensure(this.name, settings, this.scope);
3129
+ }
3130
+ /** The queue's settings and live depth. */
3131
+ async info() {
3132
+ return this.resource().get(this.name, this.scope);
3133
+ }
3134
+ /** Stops deliveries until {@link resume}. Sends are still accepted. */
3135
+ async pause() {
3136
+ await this.resource().pause(this.name, this.scope);
3137
+ }
3138
+ async resume() {
3139
+ await this.resource().resume(this.name, this.scope);
3140
+ }
3141
+ /** Deletes every message, or only the dead letters. */
3142
+ async purge(scope = "all") {
3143
+ return (await this.resource().purge(this.name, scope, this.scope)).purgedMessages;
3144
+ }
3145
+ /** Moves dead letters back for another round of attempts. */
3146
+ async redrive(messageIds) {
3147
+ return (await this.resource().redrive(this.name, messageIds, this.scope)).redrivenMessages;
3148
+ }
3149
+ /**
3150
+ * Creates or updates a cron schedule that sends `payload` into this queue.
3151
+ *
3152
+ * @example
3153
+ * ```ts
3154
+ * await digests.schedule('weekday-digest', { cron: '0 8 * * MON-FRI', timezone: 'Europe/Berlin', payload: { kind: 'digest' } });
3155
+ * ```
3156
+ */
3157
+ async schedule(name, schedule) {
3158
+ const encoded = schedule.payload === void 0 ? void 0 : this.codec.encode(schedule.payload);
3159
+ if (encoded?.encoding === "base64") throw new ConfigurationError("Scheduled payloads must be text or JSON");
3160
+ const input = {
3161
+ cron: schedule.cron,
3162
+ ...schedule.timezone ? { timezone: schedule.timezone } : {},
3163
+ ...encoded ? { body: encoded.body, contentType: encoded.contentType } : {},
3164
+ ...schedule.headers ? { headers: schedule.headers } : {},
3165
+ ...schedule.enabled === void 0 ? {} : { enabled: schedule.enabled }
3166
+ };
3167
+ try {
3168
+ return await this.resource().createSchedule(this.name, name, input, this.scope);
3169
+ } catch (error) {
3170
+ if (error instanceof ApiError && error.code === "schedule_exists") {
3171
+ return this.resource().updateSchedule(this.name, name, input, this.scope);
3172
+ }
3173
+ throw error;
3174
+ }
3175
+ }
3176
+ /** Deletes a schedule. */
3177
+ async unschedule(name) {
3178
+ await this.resource().deleteSchedule(this.name, name, this.scope);
3179
+ }
3180
+ };
3181
+ var deadLetterResponse = (reason) => Response.json(
3182
+ { error: reason },
3183
+ {
3184
+ status: 422,
3185
+ headers: { [QUEUE_RESPONSE_HEADERS.action]: "dead-letter", [QUEUE_RESPONSE_HEADERS.error]: headerText(reason) }
3186
+ }
3187
+ );
3188
+ var readSigningSecret = () => {
3189
+ const env = globalThis.process?.env;
3190
+ return env?.[QUEUE_SIGNING_SECRET_ENV] || void 0;
3191
+ };
3192
+
3193
+ // src/resources/queues.ts
3194
+ var QueuesResource = class extends BaseResource {
3195
+ constructor(httpClient, defaultApplicationId) {
3196
+ super(httpClient);
3197
+ this.defaultApplicationId = defaultApplicationId;
3198
+ }
3199
+ defaultApplicationId;
3200
+ /**
3201
+ * Returns a typed handle for one queue. Nothing is sent until you call a
3202
+ * method on it, and the queue does not need to exist: the first send
3203
+ * creates a pull queue with default settings.
3204
+ *
3205
+ * @typeParam T - The payload type sent and received through this queue.
3206
+ * @param name - Queue name, unique in the environment and case-sensitive.
3207
+ * @param options - Scope, codec and default send options.
3208
+ */
3209
+ queue(name, options = {}) {
3210
+ return new Queue(name, () => this, options);
3211
+ }
3212
+ base(options) {
3213
+ return `/applications/${encodeURIComponent(
3214
+ resolveStorageApplicationId(this.defaultApplicationId, options?.applicationId)
3215
+ )}/queues`;
3216
+ }
3217
+ path(name, options, suffix = "") {
3218
+ return `${this.base(options)}/${encodeURIComponent(name)}${suffix}`;
3219
+ }
3220
+ /** Lists the environment's queues with their live depth. */
3221
+ async list(options) {
3222
+ return this.httpClient.get(this.base(options), { query: { environment: options?.environment } });
3223
+ }
3224
+ /** Fetches one queue with its live depth. */
3225
+ async get(name, options) {
3226
+ return this.httpClient.get(this.path(name, options), { query: { environment: options?.environment } });
3227
+ }
3228
+ /**
3229
+ * Creates a queue. Fails with `queue_exists` when the name is taken; use
3230
+ * {@link ensure} for create-or-update.
3231
+ */
3232
+ async create(name, settings = {}, options) {
3233
+ return this.httpClient.post(this.base(options), { name, environment: options?.environment, ...settings });
3234
+ }
3235
+ /** Creates the queue, or applies `settings` to the existing one. */
3236
+ async ensure(name, settings = {}, options) {
3237
+ return this.httpClient.put(this.path(name, options), { environment: options?.environment, ...settings });
3238
+ }
3239
+ /** Changes a queue's settings. Messages already queued keep flowing. */
3240
+ async update(name, settings, options) {
3241
+ return this.httpClient.patch(this.path(name, options), { environment: options?.environment, ...settings });
3242
+ }
3243
+ /** Deletes a queue, its messages and its schedules. */
3244
+ async delete(name, options) {
3245
+ return this.httpClient.delete(this.path(name, options), { query: { environment: options?.environment } });
3246
+ }
3247
+ /** Stops deliveries. Sends are still accepted and wait until you resume. */
3248
+ async pause(name, options) {
3249
+ return this.httpClient.post(this.path(name, options, "/pause"), { environment: options?.environment });
3250
+ }
3251
+ /** Restarts deliveries after {@link pause}. */
3252
+ async resume(name, options) {
3253
+ return this.httpClient.post(this.path(name, options, "/resume"), { environment: options?.environment });
3254
+ }
3255
+ /** Deletes every message, or only the dead letters. */
3256
+ async purge(name, scope = "all", options) {
3257
+ return this.httpClient.post(this.path(name, options, "/purge"), { environment: options?.environment, scope });
3258
+ }
3259
+ /** Moves dead letters back to the queue with fresh attempts: all of them, or the given ids. */
3260
+ async redrive(name, messageIds, options) {
3261
+ return this.httpClient.post(this.path(name, options, "/redrive"), {
3262
+ environment: options?.environment,
3263
+ ...messageIds ? { messageIds } : {}
3264
+ });
3265
+ }
3266
+ /**
3267
+ * Sends one message.
3268
+ *
3269
+ * @param autoCreate - Create a pull queue with default settings when the name is new. Default `true`.
3270
+ */
3271
+ async send(name, message, options) {
3272
+ return this.httpClient.post(this.path(name, options, "/messages"), {
3273
+ environment: options?.environment,
3274
+ autoCreate: options?.autoCreate,
3275
+ ...message
3276
+ });
3277
+ }
3278
+ /**
3279
+ * Sends up to 100 messages in one request. Results come back in input
3280
+ * order; one rejected message does not fail the others.
3281
+ */
3282
+ async sendBatch(name, messages, options) {
3283
+ const { results } = await this.httpClient.post(
3284
+ this.path(name, options, "/messages"),
3285
+ { environment: options?.environment, autoCreate: options?.autoCreate, messages }
3286
+ );
3287
+ return results;
3288
+ }
3289
+ /** Lists messages in one state without leasing them. Bodies are cut to a preview. */
3290
+ async listMessages(name, query = {}, options) {
3291
+ return this.httpClient.get(this.path(name, options, "/messages"), {
3292
+ query: { environment: options?.environment, ...query }
3293
+ });
3294
+ }
3295
+ /** Fetches one message without leasing it. */
3296
+ async getMessage(name, messageId, options) {
3297
+ return this.httpClient.get(this.path(name, options, `/messages/${encodeURIComponent(messageId)}`), {
3298
+ query: { environment: options?.environment }
3299
+ });
3300
+ }
3301
+ /**
3302
+ * Cancels a message that has not been delivered yet, such as a scheduled
3303
+ * send you no longer want. A message being delivered or dead-lettered
3304
+ * answers 400 `invalid_request`; settle it from its consumer, or purge or
3305
+ * redrive dead letters.
3306
+ */
3307
+ async deleteMessage(name, messageId, options) {
3308
+ return this.httpClient.delete(this.path(name, options, `/messages/${encodeURIComponent(messageId)}`), {
3309
+ query: { environment: options?.environment }
3310
+ });
3311
+ }
3312
+ /**
3313
+ * Leases up to `maxMessages` messages from a pull queue, waiting up to
3314
+ * `waitSeconds` (at most 20) for the first one. Each must be acknowledged
3315
+ * with its receipt before the lease ends, or it is delivered again.
3316
+ */
3317
+ async receive(name, request = {}, options) {
3318
+ const { messages } = await this.httpClient.post(
3319
+ this.path(name, options, "/messages/receive"),
3320
+ { environment: options?.environment, ...request },
3321
+ { signal: options?.signal }
3322
+ );
3323
+ return messages;
3324
+ }
3325
+ /** Acknowledges a received message, deleting it. */
3326
+ async ack(name, messageId, receipt, options) {
3327
+ return this.httpClient.post(this.path(name, options, `/messages/${encodeURIComponent(messageId)}/ack`), {
3328
+ environment: options?.environment,
3329
+ receipt
3330
+ });
3331
+ }
3332
+ /**
3333
+ * Acknowledges up to 100 received messages in one request. Each settles on
3334
+ * its own: `results` reports, in order, which were acknowledged and why any
3335
+ * were not (`message_not_found`, `receipt_mismatch`). A batch counts one
3336
+ * operation per started ten acknowledged messages.
3337
+ */
3338
+ async ackBatch(name, messages, options) {
3339
+ return this.httpClient.post(this.path(name, options, "/messages/ack"), {
3340
+ environment: options?.environment,
3341
+ messages
3342
+ });
3343
+ }
3344
+ /**
3345
+ * Releases a received message: retry with backoff (or after `delaySeconds`),
3346
+ * or dead-letter it now with `deadLetter: true`. `countAttempt: false`
3347
+ * reschedules without spending an attempt.
3348
+ */
3349
+ async nack(name, messageId, request, options) {
3350
+ return this.httpClient.post(this.path(name, options, `/messages/${encodeURIComponent(messageId)}/nack`), {
3351
+ environment: options?.environment,
3352
+ ...request
3353
+ });
3354
+ }
3355
+ /** Extends a received message's lease to `visibilityTimeoutSeconds` from now. */
3356
+ async extend(name, messageId, receipt, visibilityTimeoutSeconds, options) {
3357
+ return this.httpClient.post(this.path(name, options, `/messages/${encodeURIComponent(messageId)}/extend`), {
3358
+ environment: options?.environment,
3359
+ receipt,
3360
+ visibilityTimeoutSeconds
3361
+ });
3362
+ }
3363
+ /** Lists a queue's schedules. */
3364
+ async listSchedules(name, options) {
3365
+ return this.httpClient.get(this.path(name, options, "/schedules"), {
3366
+ query: { environment: options?.environment }
3367
+ });
3368
+ }
3369
+ /**
3370
+ * Creates a schedule that sends a message into the queue on a cron.
3371
+ *
3372
+ * @example
3373
+ * ```ts
3374
+ * await client.queues.createSchedule('emails', 'daily-digest', {
3375
+ * cron: '0 8 * * MON-FRI',
3376
+ * timezone: 'Europe/Berlin',
3377
+ * json: { kind: 'digest' },
3378
+ * });
3379
+ * ```
3380
+ */
3381
+ async createSchedule(name, schedule, input, options) {
3382
+ return this.httpClient.post(this.path(name, options, "/schedules"), {
3383
+ environment: options?.environment,
3384
+ name: schedule,
3385
+ ...input
3386
+ });
3387
+ }
3388
+ /** Changes a schedule. Its next run is recomputed when the cron or time zone changes. */
3389
+ async updateSchedule(name, schedule, input, options) {
3390
+ return this.httpClient.patch(this.path(name, options, `/schedules/${encodeURIComponent(schedule)}`), {
3391
+ environment: options?.environment,
3392
+ ...input
3393
+ });
3394
+ }
3395
+ /** Deletes a schedule. Messages it already sent stay queued. */
3396
+ async deleteSchedule(name, schedule, options) {
3397
+ return this.httpClient.delete(this.path(name, options, `/schedules/${encodeURIComponent(schedule)}`), {
3398
+ query: { environment: options?.environment }
3399
+ });
3400
+ }
3401
+ /** Sends the schedule's message now, outside its cron. */
3402
+ async triggerSchedule(name, schedule, options) {
3403
+ return this.httpClient.post(this.path(name, options, `/schedules/${encodeURIComponent(schedule)}/trigger`), {
3404
+ environment: options?.environment
3405
+ });
3406
+ }
3407
+ };
3408
+
2362
3409
  // src/resources/sticky-sessions.ts
2363
3410
  var StickySessionsResource = class extends BaseResource {
2364
3411
  /**
@@ -2402,6 +3449,12 @@ var GigadriveClient = class {
2402
3449
  stickySessions;
2403
3450
  /** Managed image optimization inspection and cache purge operations. */
2404
3451
  imageOptimization;
3452
+ /**
3453
+ * Network Queues: manage queues and schedules, send, receive and
3454
+ * acknowledge messages. `client.queues.queue<T>(name)` returns a typed
3455
+ * handle bound to one queue.
3456
+ */
3457
+ queues;
2405
3458
  constructor(config = {}) {
2406
3459
  const baseUrl = config.baseUrl ?? readEnv("GIGADRIVE_API_BASE_URL") ?? DEFAULT_BASE_URL2;
2407
3460
  const applicationId = config.applicationId ?? readEnv("GIGADRIVE_APPLICATION_ID");
@@ -2417,6 +3470,7 @@ var GigadriveClient = class {
2417
3470
  this.apiKeys = new ApiKeysResource(httpClient);
2418
3471
  this.stickySessions = new StickySessionsResource(httpClient);
2419
3472
  this.imageOptimization = new ImageOptimizationResource(httpClient);
3473
+ this.queues = new QueuesResource(httpClient, applicationId);
2420
3474
  }
2421
3475
  };
2422
3476
 
@@ -2431,6 +3485,161 @@ async function* paginate(fetchPage) {
2431
3485
  cursor = page.nextCursor;
2432
3486
  } while (cursor);
2433
3487
  }
3488
+
3489
+ // src/queues/default-client.ts
3490
+ var defaultClient;
3491
+ var defaultQueuesResource = () => {
3492
+ defaultClient ??= new GigadriveClient();
3493
+ return defaultClient.queues;
3494
+ };
3495
+ function queue(name, options = {}) {
3496
+ const { client, ...queueOptions } = options;
3497
+ return new Queue(name, () => client?.queues ?? defaultQueuesResource(), queueOptions);
3498
+ }
3499
+
3500
+ // src/queues/workflow.ts
3501
+ var WORKFLOW_CONSUMER_PATH = "/.well-known/workflow/v1/flow";
3502
+ var WORKFLOW_QUEUE_NAME_HEADER = "x-workflow-queue-name";
3503
+ var WORKFLOW_PREFIX = /^__(?:[a-z][a-z0-9]*_)?wkf_workflow_/;
3504
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
3505
+ var RESERVED_HEADER = /^(host|content-length|content-type|connection|transfer-encoding|te|upgrade|keep-alive|proxy-.*|authorization|cookie|x-gigadrive-.*|x-nebula-.*|x-substrate-.*|x-vercel-.*|x-forwarded-.*|x-bunny-.*|x-real-ip|true-client-ip|forwarded)$/i;
3506
+ var encodeWorkflowMessage = (message) => JSON.stringify(
3507
+ message,
3508
+ (_key, value) => value instanceof Uint8Array ? { __type: "Uint8Array", data: bytesToBase64(value) } : value
3509
+ );
3510
+ var decodeWorkflowMessage = (text) => JSON.parse(text, (_key, value) => {
3511
+ if (typeof value === "object" && value !== null && value.__type === "Uint8Array" && typeof value.data === "string") {
3512
+ return base64ToBytes(value.data);
3513
+ }
3514
+ return value;
3515
+ });
3516
+ var readEnv2 = (name) => globalThis.process?.env?.[name] || void 0;
3517
+ var deduplicationKeyFor = async (idempotencyKey) => idempotencyKey.length <= 256 ? idempotencyKey : `wkf:sha256:${toHex2(await crypto.subtle.digest("SHA-256", toArrayBuffer(utf8Encode(idempotencyKey))))}`;
3518
+ function createWorkflowQueue(options = {}) {
3519
+ const resource = () => options.client?.queues ?? defaultQueuesResource();
3520
+ const scope = { applicationId: options.applicationId, environment: options.environment };
3521
+ const ensured = /* @__PURE__ */ new Map();
3522
+ const physicalQueue = async (queueName) => {
3523
+ const prefix = WORKFLOW_PREFIX.exec(queueName)?.[0];
3524
+ if (!prefix) throw new Error(`"${queueName}" is not a Workflow SDK queue name`);
3525
+ let ready = ensured.get(prefix);
3526
+ if (!ready) {
3527
+ const consumerPath = options.consumerPath ?? WORKFLOW_CONSUMER_PATH;
3528
+ ready = options.queueSettings ? resource().ensure(prefix, { ...options.queueSettings, consumerPath }, scope) : (
3529
+ // Create-only: a queue that exists keeps the settings someone gave it in the console.
3530
+ resource().create(prefix, { maxAttempts: 100, visibilityTimeoutSeconds: 900, deadLetter: true, consumerPath }, scope).catch((error) => {
3531
+ if (error instanceof ApiError && error.code === "queue_exists") return void 0;
3532
+ throw error;
3533
+ })
3534
+ );
3535
+ ensured.set(prefix, ready);
3536
+ ready.catch(() => ensured.delete(prefix));
3537
+ }
3538
+ await ready;
3539
+ return prefix;
3540
+ };
3541
+ const toMessage = async (queueName, message, opts = {}) => {
3542
+ const headers = Object.fromEntries(
3543
+ Object.entries(opts.headers ?? {}).filter(([name]) => !RESERVED_HEADER.test(name))
3544
+ );
3545
+ headers[WORKFLOW_QUEUE_NAME_HEADER] = queueName;
3546
+ const pin = opts.deploymentId ?? options.deploymentId ?? readEnv2("GIGADRIVE_DEPLOYMENT_ID");
3547
+ return {
3548
+ body: encodeWorkflowMessage(message),
3549
+ contentType: "application/json",
3550
+ headers,
3551
+ ...opts.delaySeconds && opts.delaySeconds > 0 ? { delaySeconds: Math.ceil(opts.delaySeconds) } : {},
3552
+ ...opts.idempotencyKey ? { deduplicationKey: await deduplicationKeyFor(opts.idempotencyKey) } : {},
3553
+ ...pin && UUID.test(pin) ? { deploymentId: pin } : {}
3554
+ };
3555
+ };
3556
+ return {
3557
+ getDeploymentId: () => Promise.resolve(options.deploymentId ?? readEnv2("GIGADRIVE_DEPLOYMENT_ID") ?? "local"),
3558
+ async queue(queueName, message, opts) {
3559
+ const physical = await physicalQueue(queueName);
3560
+ const result = await resource().send(physical, await toMessage(queueName, message, opts), {
3561
+ ...scope,
3562
+ autoCreate: false
3563
+ });
3564
+ if (result.messageId === null) throw new Error(`Workflow message was refused: ${result.error}`);
3565
+ return { messageId: result.messageId };
3566
+ },
3567
+ async queueBatch(queueName, messages) {
3568
+ const physical = await physicalQueue(queueName);
3569
+ const results = [];
3570
+ for (let index = 0; index < messages.length; index += 100) {
3571
+ const chunk = messages.slice(index, index + 100);
3572
+ const sent = await resource().sendBatch(
3573
+ physical,
3574
+ await Promise.all(chunk.map(({ message, opts }) => toMessage(queueName, message, opts))),
3575
+ { ...scope, autoCreate: false }
3576
+ );
3577
+ for (const result of sent) {
3578
+ results.push(
3579
+ result.messageId === null ? { messageId: null, error: result.error, retryable: result.retryable } : { messageId: result.messageId }
3580
+ );
3581
+ }
3582
+ }
3583
+ return results;
3584
+ },
3585
+ createQueueHandler(queueNamePrefix, handler) {
3586
+ return async (request) => {
3587
+ const messageId = request.headers.get(QUEUE_DELIVERY_HEADERS.messageId);
3588
+ const physicalName = request.headers.get(QUEUE_DELIVERY_HEADERS.name);
3589
+ const attempt = Number(request.headers.get(QUEUE_DELIVERY_HEADERS.attempt) ?? "1");
3590
+ const queueName = request.headers.get(WORKFLOW_QUEUE_NAME_HEADER);
3591
+ if (!messageId || !physicalName || !queueName) {
3592
+ return Response.json({ error: "Missing required headers" }, { status: 400 });
3593
+ }
3594
+ const body = new Uint8Array(await request.arrayBuffer());
3595
+ if (!options.allowUnsigned) {
3596
+ const secret = options.signingSecret ?? readSigningSecret();
3597
+ const valid = secret !== void 0 && await verifyQueueSignature(request.headers.get(QUEUE_SIGNATURE_HEADER), {
3598
+ secret,
3599
+ queue: physicalName,
3600
+ messageId,
3601
+ attempt,
3602
+ body
3603
+ });
3604
+ if (!valid) return Response.json({ error: "Invalid queue signature" }, { status: 401 });
3605
+ }
3606
+ if (physicalName !== queueNamePrefix || !queueName.startsWith(queueNamePrefix)) {
3607
+ return Response.json({ error: "Unhandled queue" }, { status: 400 });
3608
+ }
3609
+ let message;
3610
+ try {
3611
+ message = decodeWorkflowMessage(utf8Decode(body));
3612
+ } catch {
3613
+ return Response.json(
3614
+ { error: "Malformed workflow message" },
3615
+ { status: 422, headers: { [QUEUE_RESPONSE_HEADERS.action]: "dead-letter" } }
3616
+ );
3617
+ }
3618
+ try {
3619
+ const result = await handler(message, {
3620
+ attempt,
3621
+ queueName,
3622
+ messageId,
3623
+ requestId: request.headers.get("x-request-id") ?? void 0
3624
+ });
3625
+ if (typeof message === "object" && message !== null && message.invoke === true) {
3626
+ return Response.json({ result });
3627
+ }
3628
+ const timeoutSeconds = typeof result === "object" && result !== null ? result.timeoutSeconds : void 0;
3629
+ if (typeof timeoutSeconds === "number" && Number.isFinite(timeoutSeconds) && timeoutSeconds >= 0) {
3630
+ return Response.json(
3631
+ { timeoutSeconds },
3632
+ { headers: { [QUEUE_RESPONSE_HEADERS.retryAfter]: String(Math.ceil(timeoutSeconds)) } }
3633
+ );
3634
+ }
3635
+ return Response.json({ ok: true });
3636
+ } catch (error) {
3637
+ return Response.json(String(error), { status: 500 });
3638
+ }
3639
+ };
3640
+ }
3641
+ };
3642
+ }
2434
3643
  export {
2435
3644
  AiGatewayAudioResource,
2436
3645
  AiGatewayBudgetsResource,
@@ -2440,6 +3649,7 @@ export {
2440
3649
  AiGatewayVideosResource,
2441
3650
  ApiError,
2442
3651
  ApiKeysResource,
3652
+ ApplicationDomainsResource,
2443
3653
  ApplicationEnvVarsResource,
2444
3654
  ApplicationRequestsResource,
2445
3655
  ApplicationStorageResource,
@@ -2447,14 +3657,25 @@ export {
2447
3657
  AuthenticationError,
2448
3658
  ConfigurationError,
2449
3659
  DeploymentsResource,
3660
+ DomainNotActiveError,
2450
3661
  GigadriveClient,
2451
3662
  GigadriveError,
2452
3663
  ImageOptimizationResource,
3664
+ NonRetryableError,
2453
3665
  OrganizationAiGatewayResource,
3666
+ OrganizationDomainsResource,
2454
3667
  OrganizationEnvVarsResource,
2455
3668
  OrganizationMembersResource,
2456
3669
  OrganizationProductsResource,
2457
3670
  OrganizationsResource,
3671
+ QUEUE_DELIVERY_HEADERS,
3672
+ QUEUE_RESPONSE_HEADERS,
3673
+ QUEUE_SIGNATURE_HEADER,
3674
+ QUEUE_SIGNATURE_TOLERANCE_MS,
3675
+ QUEUE_SIGNING_SECRET_ENV,
3676
+ Queue,
3677
+ QueuesResource,
3678
+ RetryLaterError,
2458
3679
  StickySessionsResource,
2459
3680
  StorageBucketsResource,
2460
3681
  StorageObjectsResource,
@@ -2462,8 +3683,16 @@ export {
2462
3683
  StorageUploadSessionsResource,
2463
3684
  UploadError,
2464
3685
  UploadSessionExpiredError,
3686
+ WORKFLOW_CONSUMER_PATH,
3687
+ WORKFLOW_QUEUE_NAME_HEADER,
2465
3688
  createManagedImageUrl,
3689
+ createWorkflowQueue,
3690
+ defaultQueueCodec,
3691
+ durationToSeconds,
2466
3692
  paginate,
2467
- parseSSEStream
3693
+ parseSSEStream,
3694
+ queue,
3695
+ signQueueDelivery,
3696
+ verifyQueueSignature
2468
3697
  };
2469
3698
  //# sourceMappingURL=index.mjs.map