@tinycloud/sdk-services 3.1.0-beta.6 → 3.1.0-beta.9

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
@@ -155,6 +155,10 @@ var ErrorCodes = {
155
155
  AUTH_EXPIRED: "AUTH_EXPIRED",
156
156
  AUTH_REQUIRED: "AUTH_REQUIRED",
157
157
  AUTH_UNAUTHORIZED: "AUTH_UNAUTHORIZED",
158
+ /** HTTP 401: the delegation the invocation cites was revoked. */
159
+ AUTH_DELEGATION_REVOKED: "AUTH_DELEGATION_REVOKED",
160
+ /** HTTP 401: a delegation the cited one rests on was revoked. */
161
+ AUTH_DELEGATION_ANCESTOR_REVOKED: "AUTH_DELEGATION_ANCESTOR_REVOKED",
158
162
  NETWORK_ERROR: "NETWORK_ERROR",
159
163
  TIMEOUT: "TIMEOUT",
160
164
  ABORTED: "ABORTED",
@@ -166,6 +170,10 @@ var ErrorCodes = {
166
170
  KV_PRECONDITION_FAILED: "KV_PRECONDITION_FAILED",
167
171
  KV_CONFLICT: "KV_CONFLICT",
168
172
  KV_RESPONSE_TOO_LARGE: "KV_RESPONSE_TOO_LARGE",
173
+ /** HTTP 410 from the change feed: discard sync state and restart without a cursor; `meta.reason` says why. */
174
+ KV_SYNC_RESET_REQUIRED: "KV_SYNC_RESET_REQUIRED",
175
+ /** HTTP 403 from the change feed: the retention grant was refused; `meta.reason` says why. */
176
+ KV_RETENTION_GRANT_REFUSED: "KV_RETENTION_GRANT_REFUSED",
169
177
  // SQL-specific errors
170
178
  SQL_ERROR: "SQL_ERROR",
171
179
  SQL_PERMISSION_DENIED: "SQL_PERMISSION_DENIED",
@@ -1205,11 +1213,31 @@ function networkError(service, message, cause) {
1205
1213
  cause
1206
1214
  };
1207
1215
  }
1208
- function timeoutError(service) {
1216
+ var RequestTimeoutError = class extends Error {
1217
+ constructor(timeoutMs) {
1218
+ super(`Request timed out after ${timeoutMs}ms.`);
1219
+ this.name = "TimeoutError";
1220
+ this.timeoutMs = timeoutMs;
1221
+ }
1222
+ };
1223
+ function isRequestTimeoutError(error) {
1224
+ return error instanceof RequestTimeoutError || error instanceof Error && error.name === "TimeoutError" && typeof error.timeoutMs === "number";
1225
+ }
1226
+ function timeoutError(service, timeoutMs, cause) {
1227
+ if (timeoutMs === void 0) {
1228
+ return {
1229
+ code: ErrorCodes.TIMEOUT,
1230
+ message: "Request timed out.",
1231
+ service,
1232
+ ...cause === void 0 ? {} : { cause }
1233
+ };
1234
+ }
1209
1235
  return {
1210
1236
  code: ErrorCodes.TIMEOUT,
1211
- message: "Request timed out.",
1212
- service
1237
+ message: `Request timed out after ${timeoutMs}ms.`,
1238
+ service,
1239
+ ...cause === void 0 ? {} : { cause },
1240
+ meta: { timeoutMs }
1213
1241
  };
1214
1242
  }
1215
1243
  function abortedError(service) {
@@ -1373,6 +1401,9 @@ function isStorageFullError(error) {
1373
1401
  }
1374
1402
  function wrapError(service, error, defaultCode = ErrorCodes.NETWORK_ERROR) {
1375
1403
  if (error instanceof Error) {
1404
+ if (isRequestTimeoutError(error)) {
1405
+ return timeoutError(service, error.timeoutMs, error);
1406
+ }
1376
1407
  if (error.name === "AbortError") {
1377
1408
  return abortedError(service);
1378
1409
  }
@@ -1397,6 +1428,7 @@ function errorResult(error) {
1397
1428
  }
1398
1429
 
1399
1430
  // src/base/BaseService.ts
1431
+ var MAX_TIMER_DELAY_MS = 2147483647;
1400
1432
  var BaseService = class {
1401
1433
  constructor() {
1402
1434
  /**
@@ -1577,6 +1609,64 @@ var BaseService = class {
1577
1609
  }
1578
1610
  return controller.signal;
1579
1611
  }
1612
+ /**
1613
+ * Create the abort signal for one request, bounded by the request timeout.
1614
+ *
1615
+ * The signal aborts when the service signs out, the context aborts, the
1616
+ * caller's `signal` aborts, or the timeout elapses, whichever comes first.
1617
+ * A timeout aborts with a {@link RequestTimeoutError} (`TimeoutError`), which
1618
+ * `wrapError` maps to `ErrorCodes.TIMEOUT`; the other sources keep their own
1619
+ * reason (normally `AbortError`, mapped to `ErrorCodes.ABORTED`).
1620
+ *
1621
+ * The timeout is `timeoutMs` when given, otherwise `config.timeout`. Only a
1622
+ * positive, finite value (at most 2^31-1 ms) applies; `undefined`, `0`, and
1623
+ * `Infinity` mean no timeout, so a per-call `0` opts out of a configured one.
1624
+ *
1625
+ * Callers must call `dispose()` once the response body has been consumed,
1626
+ * which clears the timer and removes the listeners this adds to the
1627
+ * long-lived service and context signals.
1628
+ *
1629
+ * @param signal - The caller's abort signal for this request
1630
+ * @param timeoutMs - Per-request timeout override in milliseconds
1631
+ */
1632
+ createRequestSignal(signal, timeoutMs) {
1633
+ const controller = new AbortController();
1634
+ const detachers = [];
1635
+ let timer;
1636
+ const dispose = () => {
1637
+ if (timer !== void 0) {
1638
+ clearTimeout(timer);
1639
+ timer = void 0;
1640
+ }
1641
+ for (const detach of detachers.splice(0)) {
1642
+ detach();
1643
+ }
1644
+ };
1645
+ const abort = (reason) => {
1646
+ dispose();
1647
+ controller.abort(reason);
1648
+ };
1649
+ const requestSignal = { signal: controller.signal, dispose };
1650
+ const parents = [
1651
+ this.abortController.signal,
1652
+ this.context?.abortSignal,
1653
+ signal
1654
+ ].filter((parent) => parent !== void 0);
1655
+ for (const parent of parents) {
1656
+ if (parent.aborted) {
1657
+ abort(parent.reason);
1658
+ return requestSignal;
1659
+ }
1660
+ const onAbort = () => abort(parent.reason);
1661
+ parent.addEventListener("abort", onAbort, { once: true });
1662
+ detachers.push(() => parent.removeEventListener("abort", onAbort));
1663
+ }
1664
+ const timeout = timeoutMs ?? this._config.timeout;
1665
+ if (typeof timeout === "number" && timeout > 0 && timeout <= MAX_TIMER_DELAY_MS) {
1666
+ timer = setTimeout(() => abort(new RequestTimeoutError(timeout)), timeout);
1667
+ }
1668
+ return requestSignal;
1669
+ }
1580
1670
  /**
1581
1671
  * Wrap an operation with error handling and telemetry.
1582
1672
  *
@@ -1685,6 +1775,30 @@ var PrefixedKVService = class _PrefixedKVService {
1685
1775
  removePrefix
1686
1776
  });
1687
1777
  }
1778
+ /**
1779
+ * Read the change feed for everything under this prefix. A view with an
1780
+ * empty prefix (`""` or `"/"`) is refused: the node does not serve a
1781
+ * whole-space feed.
1782
+ */
1783
+ async changes(options) {
1784
+ if (this._prefix.replace(/\/+$/, "") === "") {
1785
+ return err(serviceError(
1786
+ ErrorCodes.INVALID_INPUT,
1787
+ "KV changes needs a non-empty prefix; this view has none",
1788
+ "kv"
1789
+ ));
1790
+ }
1791
+ const syncPrefix = `${this._prefix}/`;
1792
+ const response = await this._kv.changes({ ...options, prefix: syncPrefix });
1793
+ if (!response.ok) return response;
1794
+ return ok({
1795
+ ...response.data,
1796
+ changes: response.data.changes.map((change) => ({
1797
+ ...change,
1798
+ key: change.key.startsWith(syncPrefix) ? change.key.slice(syncPrefix.length) : change.key
1799
+ }))
1800
+ });
1801
+ }
1688
1802
  /**
1689
1803
  * Delete a key.
1690
1804
  */
@@ -1737,11 +1851,18 @@ var KVAction = {
1737
1851
  PUT: "tinycloud.kv/put",
1738
1852
  LIST: "tinycloud.kv/list",
1739
1853
  DELETE: "tinycloud.kv/del",
1740
- HEAD: "tinycloud.kv/metadata"
1854
+ HEAD: "tinycloud.kv/metadata",
1855
+ /** The change feed; never implied by `*` or `tinycloud.kv/*`. */
1856
+ SYNC: "tinycloud.kv/sync",
1857
+ /** Retention attestation; presented through `retentionGrant`, never invoked or implied. */
1858
+ RETAIN: "tinycloud.kv/retain"
1741
1859
  };
1742
1860
 
1743
1861
  // src/kv/KVService.ts
1744
1862
  var MAX_KV_BATCH_READ_ITEMS = 100;
1863
+ function isJsonObject(value) {
1864
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1865
+ }
1745
1866
  function encodeKvBatchPartName(path) {
1746
1867
  return encodeURIComponent(path).replace(
1747
1868
  /[!'()*]/g,
@@ -1837,7 +1958,7 @@ var KVService = class extends BaseService {
1837
1958
  * @param path - Resource path
1838
1959
  * @param action - KV action
1839
1960
  * @param body - Optional request body
1840
- * @param signal - Optional abort signal
1961
+ * @param signal - Request signal from createRequestSignal()
1841
1962
  * @returns Fetch response
1842
1963
  */
1843
1964
  async invokeOperation(path, action, body, signal, extraHeaders) {
@@ -1853,7 +1974,7 @@ var KVService = class extends BaseService {
1853
1974
  method: "POST",
1854
1975
  headers: requestHeaders,
1855
1976
  body,
1856
- signal: this.combineSignals(signal)
1977
+ signal
1857
1978
  });
1858
1979
  }
1859
1980
  /**
@@ -2030,6 +2151,7 @@ var KVService = class extends BaseService {
2030
2151
  "kv"
2031
2152
  ));
2032
2153
  }
2154
+ const request = this.createRequestSignal(options?.signal, options?.timeout);
2033
2155
  try {
2034
2156
  const session = this.context.session;
2035
2157
  const invocationHeaders = this.context.invokeAny(
@@ -2051,7 +2173,7 @@ var KVService = class extends BaseService {
2051
2173
  const response = await this.context.fetch(`${this.host}/invoke`, {
2052
2174
  method: "POST",
2053
2175
  headers,
2054
- signal: this.combineSignals(options?.signal)
2176
+ signal: request.signal
2055
2177
  });
2056
2178
  if (!response.ok) {
2057
2179
  const errorText = response.status === 401 || response.status === 403 ? await this.readAuthorizationText(response) : await response.text();
@@ -2116,6 +2238,8 @@ var KVService = class extends BaseService {
2116
2238
  return ok({ results, count: results.length });
2117
2239
  } catch (error) {
2118
2240
  return err(wrapError("kv", error));
2241
+ } finally {
2242
+ request.dispose();
2119
2243
  }
2120
2244
  }
2121
2245
  /**
@@ -2248,12 +2372,13 @@ var KVService = class extends BaseService {
2248
2372
  ));
2249
2373
  }
2250
2374
  const path = this.getFullPath(key, options?.prefix);
2375
+ const request = this.createRequestSignal(options?.signal, options?.timeout);
2251
2376
  try {
2252
2377
  const response = await this.invokeOperation(
2253
2378
  path,
2254
2379
  KVAction.GET,
2255
2380
  void 0,
2256
- options?.signal,
2381
+ request.signal,
2257
2382
  options?.maxResponseBytes === void 0 ? void 0 : { "x-tinycloud-max-response-bytes": String(options.maxResponseBytes) }
2258
2383
  );
2259
2384
  if (!response.ok) {
@@ -2310,6 +2435,8 @@ var KVService = class extends BaseService {
2310
2435
  });
2311
2436
  } catch (error) {
2312
2437
  return err(wrapError("kv", error));
2438
+ } finally {
2439
+ request.dispose();
2313
2440
  }
2314
2441
  });
2315
2442
  }
@@ -2337,12 +2464,13 @@ var KVService = class extends BaseService {
2337
2464
  }
2338
2465
  const path = this.getFullPath(key, options?.prefix);
2339
2466
  const body = this.serializePutValue(value, options?.contentType);
2467
+ const request = this.createRequestSignal(options?.signal, options?.timeout);
2340
2468
  try {
2341
2469
  const response = await this.invokeOperation(
2342
2470
  path,
2343
2471
  KVAction.PUT,
2344
2472
  body,
2345
- options?.signal,
2473
+ request.signal,
2346
2474
  {
2347
2475
  ...options?.ifMatch === void 0 ? {} : { "if-match": options.ifMatch },
2348
2476
  ...options?.ifNoneMatch === void 0 ? {} : { "if-none-match": options.ifNoneMatch }
@@ -2398,6 +2526,8 @@ var KVService = class extends BaseService {
2398
2526
  });
2399
2527
  } catch (error) {
2400
2528
  return err(wrapError("kv", error));
2529
+ } finally {
2530
+ request.dispose();
2401
2531
  }
2402
2532
  });
2403
2533
  }
@@ -2437,6 +2567,7 @@ var KVService = class extends BaseService {
2437
2567
  seen.add(path);
2438
2568
  }
2439
2569
  let requestMayHaveDispatched = false;
2570
+ const request = this.createRequestSignal(options?.signal, options?.timeout);
2440
2571
  try {
2441
2572
  const body = new FormData();
2442
2573
  for (let index = 0; index < items.length; index++) {
@@ -2459,7 +2590,7 @@ var KVService = class extends BaseService {
2459
2590
  method: "POST",
2460
2591
  headers,
2461
2592
  body,
2462
- signal: this.combineSignals(options?.signal)
2593
+ signal: request.signal
2463
2594
  };
2464
2595
  const fetchFn = this.context.fetch;
2465
2596
  requestMayHaveDispatched = true;
@@ -2510,14 +2641,16 @@ var KVService = class extends BaseService {
2510
2641
  rawBody = await response.json();
2511
2642
  } catch (jsonError) {
2512
2643
  const cause = jsonError instanceof Error ? jsonError : new Error(String(jsonError));
2644
+ const timedOut = isRequestTimeoutError(cause);
2513
2645
  return err(
2514
2646
  serviceError(
2515
- ErrorCodes.NETWORK_ERROR,
2516
- `KV batchPut response was not valid JSON: ${cause.message}`,
2647
+ timedOut ? ErrorCodes.TIMEOUT : ErrorCodes.NETWORK_ERROR,
2648
+ timedOut ? `KV batchPut response body did not arrive in time: ${cause.message}` : `KV batchPut response was not valid JSON: ${cause.message}`,
2517
2649
  "kv",
2518
2650
  {
2519
2651
  cause,
2520
2652
  meta: {
2653
+ ...timedOut ? { timeoutMs: cause.timeoutMs } : {},
2521
2654
  requestMayHaveDispatched: true,
2522
2655
  responseReceived: true,
2523
2656
  status: response.status,
@@ -2572,6 +2705,8 @@ var KVService = class extends BaseService {
2572
2705
  ...wrapped,
2573
2706
  meta: { ...wrapped.meta, requestMayHaveDispatched }
2574
2707
  });
2708
+ } finally {
2709
+ request.dispose();
2575
2710
  }
2576
2711
  });
2577
2712
  }
@@ -2594,12 +2729,13 @@ var KVService = class extends BaseService {
2594
2729
  if (options?.path) {
2595
2730
  listPath = listPath ? `${listPath}/${options.path}` : options.path;
2596
2731
  }
2732
+ const request = this.createRequestSignal(options?.signal, options?.timeout);
2597
2733
  try {
2598
2734
  const response = await this.invokeOperation(
2599
2735
  listPath,
2600
2736
  KVAction.LIST,
2601
2737
  void 0,
2602
- options?.signal,
2738
+ request.signal,
2603
2739
  options?.limit === void 0 && options?.cursor === void 0 ? void 0 : {
2604
2740
  ...options?.limit === void 0 ? {} : { "x-tinycloud-limit": String(options.limit) },
2605
2741
  ...options?.cursor === void 0 ? {} : { "x-tinycloud-cursor": options.cursor }
@@ -2640,9 +2776,174 @@ var KVService = class extends BaseService {
2640
2776
  });
2641
2777
  } catch (error) {
2642
2778
  return err(wrapError("kv", error));
2779
+ } finally {
2780
+ request.dispose();
2781
+ }
2782
+ });
2783
+ }
2784
+ /**
2785
+ * Read one page of the `tinycloud.kv/sync` change feed for a prefix.
2786
+ */
2787
+ async changes(options) {
2788
+ return this.withTelemetry("changes", options?.prefix, async () => {
2789
+ if (!this.requireAuth()) {
2790
+ return err(authRequiredError("kv"));
2791
+ }
2792
+ if (typeof options?.prefix !== "string" || options.prefix.length === 0) {
2793
+ return err(serviceError(
2794
+ ErrorCodes.INVALID_INPUT,
2795
+ "KV changes prefix must be a non-empty string",
2796
+ "kv"
2797
+ ));
2798
+ }
2799
+ if (options.limit !== void 0 && (!Number.isInteger(options.limit) || options.limit < 1 || options.limit > 1e3)) {
2800
+ return err(serviceError(
2801
+ ErrorCodes.INVALID_INPUT,
2802
+ "KV changes limit must be an integer from 1 through 1000",
2803
+ "kv"
2804
+ ));
2805
+ }
2806
+ const headers = {};
2807
+ if (options.limit !== void 0) headers["x-tinycloud-limit"] = String(options.limit);
2808
+ if (options.cursor !== void 0) headers["x-tinycloud-cursor"] = options.cursor;
2809
+ if (options.retentionGrant !== void 0) {
2810
+ headers["x-tinycloud-retention-grant"] = options.retentionGrant;
2811
+ }
2812
+ const request = this.createRequestSignal(options.signal, options.timeout);
2813
+ try {
2814
+ const response = await this.invokeOperation(
2815
+ options.prefix,
2816
+ KVAction.SYNC,
2817
+ void 0,
2818
+ request.signal,
2819
+ headers
2820
+ );
2821
+ if (!response.ok) {
2822
+ return await this.changesFailure(response, options.prefix, request.signal);
2823
+ }
2824
+ const text = await response.text();
2825
+ let body;
2826
+ try {
2827
+ body = JSON.parse(text);
2828
+ } catch {
2829
+ body = void 0;
2830
+ }
2831
+ const page = this.normalizeChangesResponse(body);
2832
+ if (!page) {
2833
+ return err(serviceError(
2834
+ ErrorCodes.NETWORK_ERROR,
2835
+ "KV changes response was not a tinycloud.kv/sync page",
2836
+ "kv",
2837
+ { meta: { status: response.status, statusText: response.statusText } }
2838
+ ));
2839
+ }
2840
+ return ok(page);
2841
+ } catch (error) {
2842
+ return err(wrapError("kv", error));
2843
+ } finally {
2844
+ request.dispose();
2643
2845
  }
2644
2846
  });
2645
2847
  }
2848
+ /**
2849
+ * Map a failed `kv/sync` response. 410 and the retention 403 carry
2850
+ * `{"error":{"code","reason"}}`; a revoked grant is a 401 whose text names
2851
+ * `delegation-revoked` or `delegation-ancestor-revoked`. A body read that
2852
+ * fails because the request was cancelled or timed out rethrows, so the
2853
+ * caller sees `ABORTED`/`TIMEOUT` rather than a mapped status.
2854
+ */
2855
+ async changesFailure(response, prefix, signal) {
2856
+ if (response.status === 404) {
2857
+ return this.classifyNotFound(response, prefix);
2858
+ }
2859
+ let errorText;
2860
+ try {
2861
+ errorText = await response.text();
2862
+ } catch (error) {
2863
+ if (signal.aborted) throw error;
2864
+ errorText = "";
2865
+ }
2866
+ const meta = { status: response.status, statusText: response.statusText };
2867
+ let reason;
2868
+ let bodyCode;
2869
+ try {
2870
+ const body = JSON.parse(errorText);
2871
+ const error = isJsonObject(body) ? body.error : void 0;
2872
+ if (isJsonObject(error)) {
2873
+ if (typeof error.code === "string") bodyCode = error.code;
2874
+ if (typeof error.reason === "string") reason = error.reason;
2875
+ }
2876
+ } catch {
2877
+ }
2878
+ const context = `Failed to read KV changes for ${JSON.stringify(prefix)}`;
2879
+ if (response.status === 410) {
2880
+ return err(serviceError(
2881
+ ErrorCodes.KV_SYNC_RESET_REQUIRED,
2882
+ `${context}: the node requires a reset (${reason ?? "reason not given"}); discard sync state and restart without a cursor`,
2883
+ "kv",
2884
+ { meta: { ...meta, reason } }
2885
+ ));
2886
+ }
2887
+ if (response.status === 403 && bodyCode === "RETENTION_GRANT_REFUSED") {
2888
+ return err(serviceError(
2889
+ ErrorCodes.KV_RETENTION_GRANT_REFUSED,
2890
+ `${context}: the retention grant was refused (${reason ?? "reason not given"})`,
2891
+ "kv",
2892
+ { meta: { ...meta, reason } }
2893
+ ));
2894
+ }
2895
+ if (response.status === 401) {
2896
+ const revoked = /\bdelegation-ancestor-revoked\b/.test(errorText) ? ErrorCodes.AUTH_DELEGATION_ANCESTOR_REVOKED : /\bdelegation-revoked\b/.test(errorText) ? ErrorCodes.AUTH_DELEGATION_REVOKED : void 0;
2897
+ if (revoked !== void 0) {
2898
+ return err(serviceError(revoked, `${context}: 401 - ${errorText}`, "kv", { meta }));
2899
+ }
2900
+ }
2901
+ if (response.status === 401 || response.status === 403) {
2902
+ return this.authorizationFailure(context, response, errorText, [prefix], KVAction.SYNC);
2903
+ }
2904
+ return err(serviceError(
2905
+ response.status === 400 ? ErrorCodes.INVALID_INPUT : ErrorCodes.NETWORK_ERROR,
2906
+ `${context}: ${response.status} - ${errorText}`,
2907
+ "kv",
2908
+ { meta }
2909
+ ));
2910
+ }
2911
+ normalizeChangesResponse(data) {
2912
+ if (!isJsonObject(data)) return void 0;
2913
+ const { changes, more, cursor, source, authority } = data;
2914
+ if (!Array.isArray(changes) || typeof more !== "boolean" || typeof cursor !== "string" || !isJsonObject(source) || typeof source.nodeDid !== "string" || typeof source.space !== "string" || typeof source.prefix !== "string" || !isJsonObject(authority)) {
2915
+ return void 0;
2916
+ }
2917
+ const { notBefore, expiresAt, retainUntil } = authority;
2918
+ if (notBefore !== null && typeof notBefore !== "string" || expiresAt !== null && typeof expiresAt !== "string" || retainUntil !== null && typeof retainUntil !== "string") {
2919
+ return void 0;
2920
+ }
2921
+ const parsed = [];
2922
+ for (const change of changes) {
2923
+ if (!isJsonObject(change) || typeof change.key !== "string") return void 0;
2924
+ if (change.deleted === true) {
2925
+ parsed.push({ key: change.key, deleted: true });
2926
+ continue;
2927
+ }
2928
+ const { metadata } = change;
2929
+ if (change.deleted !== false || typeof change.etag !== "string" || !isJsonObject(metadata) || !Object.values(metadata).every((value) => typeof value === "string")) {
2930
+ return void 0;
2931
+ }
2932
+ parsed.push({
2933
+ key: change.key,
2934
+ deleted: false,
2935
+ etag: change.etag,
2936
+ metadata
2937
+ });
2938
+ }
2939
+ return {
2940
+ changes: parsed,
2941
+ more,
2942
+ cursor,
2943
+ source: { nodeDid: source.nodeDid, space: source.space, prefix: source.prefix },
2944
+ authority: { notBefore, expiresAt, retainUntil }
2945
+ };
2946
+ }
2646
2947
  /**
2647
2948
  * Delete a key.
2648
2949
  */
@@ -2652,12 +2953,13 @@ var KVService = class extends BaseService {
2652
2953
  return err(authRequiredError("kv"));
2653
2954
  }
2654
2955
  const path = this.getFullPath(key, options?.prefix);
2956
+ const request = this.createRequestSignal(options?.signal, options?.timeout);
2655
2957
  try {
2656
2958
  const response = await this.invokeOperation(
2657
2959
  path,
2658
2960
  KVAction.DELETE,
2659
2961
  void 0,
2660
- options?.signal,
2962
+ request.signal,
2661
2963
  options?.ifMatch === void 0 ? void 0 : { "if-match": options.ifMatch }
2662
2964
  );
2663
2965
  if (!response.ok) {
@@ -2705,6 +3007,8 @@ var KVService = class extends BaseService {
2705
3007
  });
2706
3008
  } catch (error) {
2707
3009
  return err(wrapError("kv", error));
3010
+ } finally {
3011
+ request.dispose();
2708
3012
  }
2709
3013
  });
2710
3014
  }
@@ -2717,12 +3021,13 @@ var KVService = class extends BaseService {
2717
3021
  return err(authRequiredError("kv"));
2718
3022
  }
2719
3023
  const path = this.getFullPath(key, options?.prefix);
3024
+ const request = this.createRequestSignal(options?.signal, options?.timeout);
2720
3025
  try {
2721
3026
  const response = await this.invokeOperation(
2722
3027
  path,
2723
3028
  KVAction.HEAD,
2724
3029
  void 0,
2725
- options?.signal
3030
+ request.signal
2726
3031
  );
2727
3032
  if (!response.ok) {
2728
3033
  if (response.status === 401 || response.status === 403) {
@@ -2753,6 +3058,8 @@ var KVService = class extends BaseService {
2753
3058
  });
2754
3059
  } catch (error) {
2755
3060
  return err(wrapError("kv", error));
3061
+ } finally {
3062
+ request.dispose();
2756
3063
  }
2757
3064
  });
2758
3065
  }
@@ -2790,12 +3097,13 @@ var KVService = class extends BaseService {
2790
3097
  if (options?.etag !== void 0) {
2791
3098
  body.etag = options.etag;
2792
3099
  }
3100
+ const request = this.createRequestSignal(options?.signal, options?.timeout);
2793
3101
  try {
2794
3102
  const response = await this.context.fetch(`${this.host}/signed/kv`, {
2795
3103
  method: "POST",
2796
3104
  headers: this.withJsonContentType(headers),
2797
3105
  body: JSON.stringify(body),
2798
- signal: this.combineSignals(options?.signal)
3106
+ signal: request.signal
2799
3107
  });
2800
3108
  if (!response.ok) {
2801
3109
  return this.createSignedReadUrlError(response, key, path);
@@ -2815,6 +3123,8 @@ var KVService = class extends BaseService {
2815
3123
  return ok(signedUrl);
2816
3124
  } catch (error) {
2817
3125
  return err(wrapError("kv", error));
3126
+ } finally {
3127
+ request.dispose();
2818
3128
  }
2819
3129
  });
2820
3130
  }
@@ -3028,6 +3338,7 @@ var SQLService = class extends BaseService {
3028
3338
  if (!this.requireAuth()) {
3029
3339
  return err(authRequiredError("sql"));
3030
3340
  }
3341
+ const request = this.createRequestSignal(options?.signal);
3031
3342
  try {
3032
3343
  const body = {
3033
3344
  action: "query",
@@ -3041,7 +3352,7 @@ var SQLService = class extends BaseService {
3041
3352
  dbName,
3042
3353
  action,
3043
3354
  body,
3044
- options?.signal
3355
+ request.signal
3045
3356
  );
3046
3357
  if (!response.ok) {
3047
3358
  return this.handleErrorResponse(response, "query", dbName, [action]);
@@ -3050,6 +3361,8 @@ var SQLService = class extends BaseService {
3050
3361
  return ok(data);
3051
3362
  } catch (error) {
3052
3363
  return err(wrapError("sql", error));
3364
+ } finally {
3365
+ request.dispose();
3053
3366
  }
3054
3367
  });
3055
3368
  }
@@ -3058,6 +3371,7 @@ var SQLService = class extends BaseService {
3058
3371
  if (!this.requireAuth()) {
3059
3372
  return err(authRequiredError("sql"));
3060
3373
  }
3374
+ const request = this.createRequestSignal(options?.signal);
3061
3375
  try {
3062
3376
  const body = {
3063
3377
  action: "execute",
@@ -3078,7 +3392,7 @@ var SQLService = class extends BaseService {
3078
3392
  dbName,
3079
3393
  requestedActions,
3080
3394
  body,
3081
- options?.signal
3395
+ request.signal
3082
3396
  );
3083
3397
  if (!response.ok) {
3084
3398
  return this.handleErrorResponse(response, "execute", dbName, requestedActions);
@@ -3087,6 +3401,8 @@ var SQLService = class extends BaseService {
3087
3401
  return ok(data);
3088
3402
  } catch (error) {
3089
3403
  return err(wrapError("sql", error));
3404
+ } finally {
3405
+ request.dispose();
3090
3406
  }
3091
3407
  });
3092
3408
  }
@@ -3095,13 +3411,14 @@ var SQLService = class extends BaseService {
3095
3411
  if (!this.requireAuth()) {
3096
3412
  return err(authRequiredError("sql"));
3097
3413
  }
3414
+ const request = this.createRequestSignal(options?.signal);
3098
3415
  try {
3099
3416
  const requestedActions = this.actionsForSqlBatch(statements);
3100
3417
  const response = await this.invokeSQL(
3101
3418
  dbName,
3102
3419
  requestedActions,
3103
3420
  { action: "batch", statements },
3104
- options?.signal
3421
+ request.signal
3105
3422
  );
3106
3423
  if (!response.ok) {
3107
3424
  return this.handleErrorResponse(response, "batch", dbName, requestedActions);
@@ -3110,6 +3427,8 @@ var SQLService = class extends BaseService {
3110
3427
  return ok(data);
3111
3428
  } catch (error) {
3112
3429
  return err(wrapError("sql", error));
3430
+ } finally {
3431
+ request.dispose();
3113
3432
  }
3114
3433
  });
3115
3434
  }
@@ -3118,12 +3437,13 @@ var SQLService = class extends BaseService {
3118
3437
  if (!this.requireAuth()) {
3119
3438
  return err(authRequiredError("sql"));
3120
3439
  }
3440
+ const request = this.createRequestSignal(options?.signal);
3121
3441
  try {
3122
3442
  const response = await this.invokeSQL(
3123
3443
  dbName,
3124
3444
  SQLAction.WRITE,
3125
3445
  { action: "execute_statement", name, params: params ?? [] },
3126
- options?.signal
3446
+ request.signal
3127
3447
  );
3128
3448
  if (!response.ok) {
3129
3449
  return this.handleErrorResponse(response, "executeStatement", dbName, [SQLAction.WRITE]);
@@ -3132,6 +3452,8 @@ var SQLService = class extends BaseService {
3132
3452
  return ok(data);
3133
3453
  } catch (error) {
3134
3454
  return err(wrapError("sql", error));
3455
+ } finally {
3456
+ request.dispose();
3135
3457
  }
3136
3458
  });
3137
3459
  }
@@ -3140,12 +3462,13 @@ var SQLService = class extends BaseService {
3140
3462
  if (!this.requireAuth()) {
3141
3463
  return err(authRequiredError("sql"));
3142
3464
  }
3465
+ const request = this.createRequestSignal(options?.signal);
3143
3466
  try {
3144
3467
  const response = await this.invokeSQL(
3145
3468
  dbName,
3146
3469
  SQLAction.READ,
3147
3470
  { action: "export" },
3148
- options?.signal
3471
+ request.signal
3149
3472
  );
3150
3473
  if (!response.ok) {
3151
3474
  return this.handleErrorResponse(response, "export", dbName, [SQLAction.READ]);
@@ -3159,6 +3482,8 @@ var SQLService = class extends BaseService {
3159
3482
  return ok(text);
3160
3483
  } catch (error) {
3161
3484
  return err(wrapError("sql", error));
3485
+ } finally {
3486
+ request.dispose();
3162
3487
  }
3163
3488
  });
3164
3489
  }
@@ -3249,7 +3574,7 @@ var SQLService = class extends BaseService {
3249
3574
  "Content-Type": "application/json"
3250
3575
  },
3251
3576
  body: JSON.stringify(body),
3252
- signal: this.combineSignals(signal)
3577
+ signal
3253
3578
  });
3254
3579
  }
3255
3580
  actionForSql(sql, fallback) {
@@ -3513,12 +3838,13 @@ var DuckDbService = class extends BaseService {
3513
3838
  if (!this.requireAuth()) {
3514
3839
  return err(authRequiredError("duckdb"));
3515
3840
  }
3841
+ const request = this.createRequestSignal(options?.signal);
3516
3842
  try {
3517
3843
  const response = await this.invokeDuckDb(
3518
3844
  dbName,
3519
3845
  DuckDbAction.READ,
3520
3846
  { action: "query", sql, params: params ?? [] },
3521
- options?.signal
3847
+ request.signal
3522
3848
  );
3523
3849
  if (!response.ok) {
3524
3850
  return this.handleErrorResponse(response, "query");
@@ -3527,6 +3853,8 @@ var DuckDbService = class extends BaseService {
3527
3853
  return ok(data);
3528
3854
  } catch (error) {
3529
3855
  return err(wrapError("duckdb", error));
3856
+ } finally {
3857
+ request.dispose();
3530
3858
  }
3531
3859
  });
3532
3860
  }
@@ -3535,12 +3863,13 @@ var DuckDbService = class extends BaseService {
3535
3863
  if (!this.requireAuth()) {
3536
3864
  return err(authRequiredError("duckdb"));
3537
3865
  }
3866
+ const request = this.createRequestSignal(options?.signal);
3538
3867
  try {
3539
3868
  const response = await this.invokeDuckDb(
3540
3869
  dbName,
3541
3870
  DuckDbAction.READ,
3542
3871
  { action: "query", sql, params: params ?? [] },
3543
- options?.signal,
3872
+ request.signal,
3544
3873
  { Accept: "application/vnd.apache.arrow.stream" }
3545
3874
  );
3546
3875
  if (!response.ok) {
@@ -3550,6 +3879,8 @@ var DuckDbService = class extends BaseService {
3550
3879
  return ok(buffer);
3551
3880
  } catch (error) {
3552
3881
  return err(wrapError("duckdb", error));
3882
+ } finally {
3883
+ request.dispose();
3553
3884
  }
3554
3885
  });
3555
3886
  }
@@ -3558,6 +3889,7 @@ var DuckDbService = class extends BaseService {
3558
3889
  if (!this.requireAuth()) {
3559
3890
  return err(authRequiredError("duckdb"));
3560
3891
  }
3892
+ const request = this.createRequestSignal(options?.signal);
3561
3893
  try {
3562
3894
  const body = {
3563
3895
  action: "execute",
@@ -3571,7 +3903,7 @@ var DuckDbService = class extends BaseService {
3571
3903
  dbName,
3572
3904
  DuckDbAction.WRITE,
3573
3905
  body,
3574
- options?.signal
3906
+ request.signal
3575
3907
  );
3576
3908
  if (!response.ok) {
3577
3909
  return this.handleErrorResponse(response, "execute");
@@ -3580,6 +3912,8 @@ var DuckDbService = class extends BaseService {
3580
3912
  return ok(data);
3581
3913
  } catch (error) {
3582
3914
  return err(wrapError("duckdb", error));
3915
+ } finally {
3916
+ request.dispose();
3583
3917
  }
3584
3918
  });
3585
3919
  }
@@ -3588,6 +3922,7 @@ var DuckDbService = class extends BaseService {
3588
3922
  if (!this.requireAuth()) {
3589
3923
  return err(authRequiredError("duckdb"));
3590
3924
  }
3925
+ const request = this.createRequestSignal(options?.signal);
3591
3926
  try {
3592
3927
  const body = {
3593
3928
  action: "batch",
@@ -3600,7 +3935,7 @@ var DuckDbService = class extends BaseService {
3600
3935
  dbName,
3601
3936
  DuckDbAction.WRITE,
3602
3937
  body,
3603
- options?.signal
3938
+ request.signal
3604
3939
  );
3605
3940
  if (!response.ok) {
3606
3941
  return this.handleErrorResponse(response, "batch");
@@ -3609,6 +3944,8 @@ var DuckDbService = class extends BaseService {
3609
3944
  return ok(data);
3610
3945
  } catch (error) {
3611
3946
  return err(wrapError("duckdb", error));
3947
+ } finally {
3948
+ request.dispose();
3612
3949
  }
3613
3950
  });
3614
3951
  }
@@ -3617,12 +3954,13 @@ var DuckDbService = class extends BaseService {
3617
3954
  if (!this.requireAuth()) {
3618
3955
  return err(authRequiredError("duckdb"));
3619
3956
  }
3957
+ const request = this.createRequestSignal(options?.signal);
3620
3958
  try {
3621
3959
  const response = await this.invokeDuckDb(
3622
3960
  dbName,
3623
3961
  DuckDbAction.WRITE,
3624
3962
  { action: "executeStatement", name, params: params ?? [] },
3625
- options?.signal
3963
+ request.signal
3626
3964
  );
3627
3965
  if (!response.ok) {
3628
3966
  return this.handleErrorResponse(response, "executeStatement");
@@ -3631,6 +3969,8 @@ var DuckDbService = class extends BaseService {
3631
3969
  return ok(data);
3632
3970
  } catch (error) {
3633
3971
  return err(wrapError("duckdb", error));
3972
+ } finally {
3973
+ request.dispose();
3634
3974
  }
3635
3975
  });
3636
3976
  }
@@ -3639,12 +3979,13 @@ var DuckDbService = class extends BaseService {
3639
3979
  if (!this.requireAuth()) {
3640
3980
  return err(authRequiredError("duckdb"));
3641
3981
  }
3982
+ const request = this.createRequestSignal(options?.signal);
3642
3983
  try {
3643
3984
  const response = await this.invokeDuckDb(
3644
3985
  dbName,
3645
3986
  DuckDbAction.READ,
3646
3987
  { action: "describe" },
3647
- options?.signal
3988
+ request.signal
3648
3989
  );
3649
3990
  if (!response.ok) {
3650
3991
  return this.handleErrorResponse(response, "describe");
@@ -3653,6 +3994,8 @@ var DuckDbService = class extends BaseService {
3653
3994
  return ok(data);
3654
3995
  } catch (error) {
3655
3996
  return err(wrapError("duckdb", error));
3997
+ } finally {
3998
+ request.dispose();
3656
3999
  }
3657
4000
  });
3658
4001
  }
@@ -3661,12 +4004,13 @@ var DuckDbService = class extends BaseService {
3661
4004
  if (!this.requireAuth()) {
3662
4005
  return err(authRequiredError("duckdb"));
3663
4006
  }
4007
+ const request = this.createRequestSignal(options?.signal);
3664
4008
  try {
3665
4009
  const response = await this.invokeDuckDb(
3666
4010
  dbName,
3667
4011
  DuckDbAction.EXPORT,
3668
4012
  { action: "export" },
3669
- options?.signal
4013
+ request.signal
3670
4014
  );
3671
4015
  if (!response.ok) {
3672
4016
  return this.handleErrorResponse(response, "export");
@@ -3675,6 +4019,8 @@ var DuckDbService = class extends BaseService {
3675
4019
  return ok(blob);
3676
4020
  } catch (error) {
3677
4021
  return err(wrapError("duckdb", error));
4022
+ } finally {
4023
+ request.dispose();
3678
4024
  }
3679
4025
  });
3680
4026
  }
@@ -3683,6 +4029,7 @@ var DuckDbService = class extends BaseService {
3683
4029
  if (!this.requireAuth()) {
3684
4030
  return err(authRequiredError("duckdb"));
3685
4031
  }
4032
+ const request = this.createRequestSignal(options?.signal);
3686
4033
  try {
3687
4034
  const session = this.context.session;
3688
4035
  const headers = this.context.invoke(
@@ -3698,7 +4045,7 @@ var DuckDbService = class extends BaseService {
3698
4045
  "Content-Type": "application/x-duckdb"
3699
4046
  },
3700
4047
  body: new Blob([data]),
3701
- signal: this.combineSignals(options?.signal)
4048
+ signal: request.signal
3702
4049
  });
3703
4050
  if (!response.ok) {
3704
4051
  return this.handleErrorResponse(response, "import");
@@ -3706,6 +4053,8 @@ var DuckDbService = class extends BaseService {
3706
4053
  return ok(void 0);
3707
4054
  } catch (error) {
3708
4055
  return err(wrapError("duckdb", error));
4056
+ } finally {
4057
+ request.dispose();
3709
4058
  }
3710
4059
  });
3711
4060
  }
@@ -3721,7 +4070,7 @@ var DuckDbService = class extends BaseService {
3721
4070
  ...extraHeaders
3722
4071
  },
3723
4072
  body: JSON.stringify(body),
3724
- signal: this.combineSignals(signal)
4073
+ signal
3725
4074
  });
3726
4075
  }
3727
4076
  async handleErrorResponse(response, operation) {
@@ -6142,6 +6491,7 @@ var SecretsService = class {
6142
6491
  }
6143
6492
  async listAll() {
6144
6493
  const entries = /* @__PURE__ */ new Map();
6494
+ const seenCursors = /* @__PURE__ */ new Set();
6145
6495
  let cursor;
6146
6496
  do {
6147
6497
  const result = await this.vault.listPage({
@@ -6164,6 +6514,14 @@ var SecretsService = class {
6164
6514
  "Secret catalog listing was truncated without a continuation cursor."
6165
6515
  );
6166
6516
  }
6517
+ if (cursor !== void 0) {
6518
+ if (seenCursors.has(cursor)) {
6519
+ return invalidSecretInput(
6520
+ "Secret catalog listing repeated a continuation cursor; the node does not support paged KV lists."
6521
+ );
6522
+ }
6523
+ seenCursors.add(cursor);
6524
+ }
6167
6525
  } while (cursor !== void 0);
6168
6526
  return {
6169
6527
  ok: true,