@tinycloud/sdk-services 3.1.0-beta.2 → 3.1.0-beta.7

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/kv/index.cjs CHANGED
@@ -99,6 +99,55 @@ function serviceError(code, message, service, options) {
99
99
  };
100
100
  }
101
101
 
102
+ // src/errors.ts
103
+ var import_bootstrap = require("@tinycloud/bootstrap");
104
+
105
+ // src/encryption/networkId.ts
106
+ var URN_PREFIX = "urn:tinycloud:encryption:";
107
+ var NETWORK_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
108
+ var NetworkIdError = class extends Error {
109
+ constructor(message) {
110
+ super(message);
111
+ this.name = "NetworkIdError";
112
+ }
113
+ };
114
+ function parseNetworkId(networkId) {
115
+ if (typeof networkId !== "string" || networkId.length === 0) {
116
+ throw new NetworkIdError("networkId must be a non-empty string");
117
+ }
118
+ if (!networkId.startsWith(URN_PREFIX)) {
119
+ throw new NetworkIdError(
120
+ `networkId must start with ${URN_PREFIX} (got ${JSON.stringify(networkId)})`
121
+ );
122
+ }
123
+ const body = networkId.slice(URN_PREFIX.length);
124
+ const lastColon = body.lastIndexOf(":");
125
+ if (lastColon <= 0 || lastColon === body.length - 1) {
126
+ throw new NetworkIdError(
127
+ `networkId missing ownerDid or name segment (got ${JSON.stringify(networkId)})`
128
+ );
129
+ }
130
+ const ownerDid = body.slice(0, lastColon);
131
+ const name = body.slice(lastColon + 1);
132
+ if (!ownerDid.startsWith("did:")) {
133
+ throw new NetworkIdError(
134
+ `networkId ownerDid must be a DID (got ${JSON.stringify(ownerDid)})`
135
+ );
136
+ }
137
+ const didParts = ownerDid.split(":");
138
+ if (didParts.length < 3 || didParts.some((p) => p.length === 0)) {
139
+ throw new NetworkIdError(
140
+ `networkId ownerDid is not a well-formed DID (got ${JSON.stringify(ownerDid)})`
141
+ );
142
+ }
143
+ if (!NETWORK_NAME_RE.test(name)) {
144
+ throw new NetworkIdError(
145
+ `networkId name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
146
+ );
147
+ }
148
+ return { networkId, ownerDid, name };
149
+ }
150
+
102
151
  // src/errors.ts
103
152
  function parsePermissionHint(value) {
104
153
  if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
@@ -143,11 +192,31 @@ function authRequiredError(service) {
143
192
  service
144
193
  };
145
194
  }
146
- function timeoutError(service) {
195
+ var RequestTimeoutError = class extends Error {
196
+ constructor(timeoutMs) {
197
+ super(`Request timed out after ${timeoutMs}ms.`);
198
+ this.name = "TimeoutError";
199
+ this.timeoutMs = timeoutMs;
200
+ }
201
+ };
202
+ function isRequestTimeoutError(error) {
203
+ return error instanceof RequestTimeoutError || error instanceof Error && error.name === "TimeoutError" && typeof error.timeoutMs === "number";
204
+ }
205
+ function timeoutError(service, timeoutMs, cause) {
206
+ if (timeoutMs === void 0) {
207
+ return {
208
+ code: ErrorCodes.TIMEOUT,
209
+ message: "Request timed out.",
210
+ service,
211
+ ...cause === void 0 ? {} : { cause }
212
+ };
213
+ }
147
214
  return {
148
215
  code: ErrorCodes.TIMEOUT,
149
- message: "Request timed out.",
150
- service
216
+ message: `Request timed out after ${timeoutMs}ms.`,
217
+ service,
218
+ ...cause === void 0 ? {} : { cause },
219
+ meta: { timeoutMs }
151
220
  };
152
221
  }
153
222
  function abortedError(service) {
@@ -167,6 +236,101 @@ function parseAuthError(responseText) {
167
236
  function authUnauthorizedError(service, message, meta) {
168
237
  return serviceError(ErrorCodes.AUTH_UNAUTHORIZED, message, service, { meta });
169
238
  }
239
+ var grantableActions = new Set(
240
+ import_bootstrap.CAPABILITY_REGISTRY.map(({ urn }) => urn).filter((urn) => !urn.endsWith("/*"))
241
+ );
242
+ var capabilityServices = new Set(
243
+ import_bootstrap.CAPABILITY_REGISTRY.map(({ service }) => service.slice("tinycloud.".length))
244
+ );
245
+ function parseCapabilityResource(resource, service) {
246
+ if (resource.length === 0 || resource.length > 1024 || !capabilityServices.has(service) || /[\s\p{C}*#]/u.test(resource) || resource.includes("://")) return void 0;
247
+ const segments = resource.split("/");
248
+ if (!/^tinycloud:[A-Za-z0-9][A-Za-z0-9:._-]*$/.test(segments[0])) return void 0;
249
+ const serviceIndex = segments.findIndex(
250
+ (segment, index) => index > 0 && capabilityServices.has(segment)
251
+ );
252
+ if (serviceIndex < 1 || segments[serviceIndex] !== service) return void 0;
253
+ if (segments.slice(1, serviceIndex).some(
254
+ (segment) => segment.length === 0 || segment === "." || segment === ".."
255
+ )) return void 0;
256
+ const pathSegments = segments.slice(serviceIndex + 1);
257
+ if (pathSegments.some((segment) => segment === "." || segment === "..")) return void 0;
258
+ if (service !== "kv" && (pathSegments.length === 0 || pathSegments[pathSegments.length - 1] === "")) return void 0;
259
+ return {
260
+ space: segments.slice(0, serviceIndex).join("/"),
261
+ path: pathSegments.join("/")
262
+ };
263
+ }
264
+ function safeCapabilityResource(value, service, action) {
265
+ if (typeof value !== "string" || value.length === 0 || value.length > 1024) return false;
266
+ if (service === "encryption" && value.startsWith("urn:tinycloud:encryption:")) {
267
+ if (!/^[A-Za-z0-9_][A-Za-z0-9_.:/-]*$/.test(value) || value.includes("//") || value.split("/").some((segment) => segment === "." || segment === "..")) return false;
268
+ try {
269
+ parseNetworkId(value);
270
+ return true;
271
+ } catch {
272
+ return false;
273
+ }
274
+ }
275
+ const parsed = parseCapabilityResource(value, service);
276
+ return parsed !== void 0 && (!parsed.path.endsWith("/") || service === "kv" && action === "tinycloud.kv/list") && (parsed.path !== "" || service === "kv" && action === "tinycloud.kv/list");
277
+ }
278
+ function validatedCapabilityOf(error) {
279
+ const verdict = authorizationVerdictOf(error);
280
+ if (verdict !== "unauthenticated" && verdict !== "forbidden") return void 0;
281
+ const seen = /* @__PURE__ */ new Set();
282
+ let node = error;
283
+ for (let depth = 0; depth < MAX_CAUSE_DEPTH && typeof node === "object" && node !== null && !seen.has(node); depth += 1) {
284
+ seen.add(node);
285
+ const record = node;
286
+ const status = errorStatusOf(record);
287
+ const meta = record.meta;
288
+ if ((status === 401 || status === 403 || status === void 0 && record.code === ErrorCodes.AUTH_UNAUTHORIZED) && typeof meta === "object" && meta !== null && !Array.isArray(meta)) {
289
+ const metadata = meta;
290
+ const { resource, requiredAction } = metadata;
291
+ if (typeof requiredAction === "string" && grantableActions.has(requiredAction)) {
292
+ const actionService = requiredAction.slice("tinycloud.".length, requiredAction.indexOf("/"));
293
+ const explicitService = record.service === void 0 ? metadata.service : record.service;
294
+ if (safeCapabilityResource(resource, actionService, requiredAction) && (explicitService === void 0 || explicitService === actionService || explicitService === `tinycloud.${actionService}`)) {
295
+ return { resource, requiredAction };
296
+ }
297
+ }
298
+ }
299
+ node = record.cause;
300
+ }
301
+ return void 0;
302
+ }
303
+ var MAX_CAUSE_DEPTH = 8;
304
+ function errorStatusOf(node) {
305
+ const meta = node.meta;
306
+ const candidates = [
307
+ node.status,
308
+ node.statusCode,
309
+ typeof meta === "object" && meta !== null ? meta.status : void 0
310
+ ];
311
+ for (const candidate of candidates) {
312
+ if (typeof candidate === "number" && Number.isInteger(candidate) && candidate >= 400 && candidate <= 599) {
313
+ return candidate;
314
+ }
315
+ }
316
+ return void 0;
317
+ }
318
+ function authorizationVerdictOf(error) {
319
+ const seen = /* @__PURE__ */ new Set();
320
+ let sawUnauthorizedCode = false;
321
+ let node = error;
322
+ for (let depth = 0; depth < MAX_CAUSE_DEPTH && typeof node === "object" && node !== null && !seen.has(node); depth += 1) {
323
+ seen.add(node);
324
+ const record = node;
325
+ const status = errorStatusOf(record);
326
+ if (status === 401) return "unauthenticated";
327
+ if (status === 403) return "forbidden";
328
+ if (status !== void 0) return "other";
329
+ if (record.code === ErrorCodes.AUTH_UNAUTHORIZED) sawUnauthorizedCode = true;
330
+ node = record.cause;
331
+ }
332
+ return sawUnauthorizedCode ? "forbidden" : void 0;
333
+ }
170
334
  function storageQuotaExceededError(service, message, meta) {
171
335
  return {
172
336
  code: ErrorCodes.STORAGE_QUOTA_EXCEEDED,
@@ -199,6 +363,9 @@ function storageRejectionError(service, status, meta, responseText) {
199
363
  }
200
364
  function wrapError(service, error, defaultCode = ErrorCodes.NETWORK_ERROR) {
201
365
  if (error instanceof Error) {
366
+ if (isRequestTimeoutError(error)) {
367
+ return timeoutError(service, error.timeoutMs, error);
368
+ }
202
369
  if (error.name === "AbortError") {
203
370
  return abortedError(service);
204
371
  }
@@ -220,6 +387,7 @@ function wrapError(service, error, defaultCode = ErrorCodes.NETWORK_ERROR) {
220
387
  }
221
388
 
222
389
  // src/base/BaseService.ts
390
+ var MAX_TIMER_DELAY_MS = 2147483647;
223
391
  var BaseService = class {
224
392
  constructor() {
225
393
  /**
@@ -400,6 +568,64 @@ var BaseService = class {
400
568
  }
401
569
  return controller.signal;
402
570
  }
571
+ /**
572
+ * Create the abort signal for one request, bounded by the request timeout.
573
+ *
574
+ * The signal aborts when the service signs out, the context aborts, the
575
+ * caller's `signal` aborts, or the timeout elapses, whichever comes first.
576
+ * A timeout aborts with a {@link RequestTimeoutError} (`TimeoutError`), which
577
+ * `wrapError` maps to `ErrorCodes.TIMEOUT`; the other sources keep their own
578
+ * reason (normally `AbortError`, mapped to `ErrorCodes.ABORTED`).
579
+ *
580
+ * The timeout is `timeoutMs` when given, otherwise `config.timeout`. Only a
581
+ * positive, finite value (at most 2^31-1 ms) applies; `undefined`, `0`, and
582
+ * `Infinity` mean no timeout, so a per-call `0` opts out of a configured one.
583
+ *
584
+ * Callers must call `dispose()` once the response body has been consumed,
585
+ * which clears the timer and removes the listeners this adds to the
586
+ * long-lived service and context signals.
587
+ *
588
+ * @param signal - The caller's abort signal for this request
589
+ * @param timeoutMs - Per-request timeout override in milliseconds
590
+ */
591
+ createRequestSignal(signal, timeoutMs) {
592
+ const controller = new AbortController();
593
+ const detachers = [];
594
+ let timer;
595
+ const dispose = () => {
596
+ if (timer !== void 0) {
597
+ clearTimeout(timer);
598
+ timer = void 0;
599
+ }
600
+ for (const detach of detachers.splice(0)) {
601
+ detach();
602
+ }
603
+ };
604
+ const abort = (reason) => {
605
+ dispose();
606
+ controller.abort(reason);
607
+ };
608
+ const requestSignal = { signal: controller.signal, dispose };
609
+ const parents = [
610
+ this.abortController.signal,
611
+ this.context?.abortSignal,
612
+ signal
613
+ ].filter((parent) => parent !== void 0);
614
+ for (const parent of parents) {
615
+ if (parent.aborted) {
616
+ abort(parent.reason);
617
+ return requestSignal;
618
+ }
619
+ const onAbort = () => abort(parent.reason);
620
+ parent.addEventListener("abort", onAbort, { once: true });
621
+ detachers.push(() => parent.removeEventListener("abort", onAbort));
622
+ }
623
+ const timeout = timeoutMs ?? this._config.timeout;
624
+ if (typeof timeout === "number" && timeout > 0 && timeout <= MAX_TIMER_DELAY_MS) {
625
+ timer = setTimeout(() => abort(new RequestTimeoutError(timeout)), timeout);
626
+ }
627
+ return requestSignal;
628
+ }
403
629
  /**
404
630
  * Wrap an operation with error handling and telemetry.
405
631
  *
@@ -660,7 +886,7 @@ var KVService = class extends BaseService {
660
886
  * @param path - Resource path
661
887
  * @param action - KV action
662
888
  * @param body - Optional request body
663
- * @param signal - Optional abort signal
889
+ * @param signal - Request signal from createRequestSignal()
664
890
  * @returns Fetch response
665
891
  */
666
892
  async invokeOperation(path, action, body, signal, extraHeaders) {
@@ -676,7 +902,7 @@ var KVService = class extends BaseService {
676
902
  method: "POST",
677
903
  headers: requestHeaders,
678
904
  body,
679
- signal: this.combineSignals(signal)
905
+ signal
680
906
  });
681
907
  }
682
908
  /**
@@ -853,6 +1079,7 @@ var KVService = class extends BaseService {
853
1079
  "kv"
854
1080
  ));
855
1081
  }
1082
+ const request = this.createRequestSignal(options?.signal, options?.timeout);
856
1083
  try {
857
1084
  const session = this.context.session;
858
1085
  const invocationHeaders = this.context.invokeAny(
@@ -874,15 +1101,17 @@ var KVService = class extends BaseService {
874
1101
  const response = await this.context.fetch(`${this.host}/invoke`, {
875
1102
  method: "POST",
876
1103
  headers,
877
- signal: this.combineSignals(options?.signal)
1104
+ signal: request.signal
878
1105
  });
879
1106
  if (!response.ok) {
880
- const errorText = await response.text();
1107
+ const errorText = response.status === 401 || response.status === 403 ? await this.readAuthorizationText(response) : await response.text();
881
1108
  if (response.status === 401 || response.status === 403) {
882
1109
  return this.authorizationFailure(
883
1110
  `Failed to batch read ${keys.length} key(s)`,
884
1111
  response,
885
- errorText
1112
+ errorText,
1113
+ paths,
1114
+ action
886
1115
  );
887
1116
  }
888
1117
  if (response.status === 413) {
@@ -937,6 +1166,8 @@ var KVService = class extends BaseService {
937
1166
  return ok({ results, count: results.length });
938
1167
  } catch (error) {
939
1168
  return err(wrapError("kv", error));
1169
+ } finally {
1170
+ request.dispose();
940
1171
  }
941
1172
  }
942
1173
  /**
@@ -972,7 +1203,7 @@ var KVService = class extends BaseService {
972
1203
  return text;
973
1204
  }
974
1205
  }
975
- async createSignedReadUrlError(response, key) {
1206
+ async createSignedReadUrlError(response, key, path) {
976
1207
  let errorText = response.statusText;
977
1208
  try {
978
1209
  const text = await response.text();
@@ -985,7 +1216,9 @@ var KVService = class extends BaseService {
985
1216
  return this.authorizationFailure(
986
1217
  `Failed to create signed read URL for key ${JSON.stringify(key)}`,
987
1218
  response,
988
- errorText
1219
+ errorText,
1220
+ [path],
1221
+ KVAction.GET
989
1222
  );
990
1223
  }
991
1224
  const code = response.status === 400 ? ErrorCodes.INVALID_INPUT : ErrorCodes.NETWORK_ERROR;
@@ -998,21 +1231,42 @@ var KVService = class extends BaseService {
998
1231
  )
999
1232
  );
1000
1233
  }
1234
+ /** A known 401/403 keeps its typed status even if reading its body fails. */
1235
+ async readAuthorizationText(response) {
1236
+ try {
1237
+ return await response.text();
1238
+ } catch {
1239
+ return "";
1240
+ }
1241
+ }
1001
1242
  /**
1002
1243
  * AUTH_UNAUTHORIZED for a 401/403 response. The message keeps the HTTP
1003
1244
  * status and the server text (`<context>: <status> - <text>`, the same
1004
1245
  * shape as every other KV failure) so callers that rethrow only the message
1005
1246
  * still see the status; `meta.status` carries it for typed callers.
1006
- * Resource/ability hints are parsed from the raw body, never the message.
1247
+ * Server hints must match the requested path/action (whether the node sends
1248
+ * a relative path or full resource); metadata uses only the request-derived
1249
+ * canonical resource under the authenticated session's space.
1007
1250
  */
1008
- authorizationFailure(context, response, errorText, extraMeta = {}) {
1251
+ authorizationFailure(context, response, errorText, paths, expectedAction, extraMeta = {}) {
1009
1252
  const { resource, action } = parseAuthError(errorText);
1253
+ const requestedPath = paths.find(
1254
+ (path) => resource === path || resource === `${this.context.session.spaceId}/kv/${path}`
1255
+ );
1256
+ const capability = requestedPath !== void 0 && action === expectedAction ? validatedCapabilityOf({
1257
+ service: "kv",
1258
+ code: ErrorCodes.AUTH_UNAUTHORIZED,
1259
+ meta: {
1260
+ status: response.status,
1261
+ resource: `${this.context.session.spaceId}/kv/${requestedPath}`,
1262
+ requiredAction: action
1263
+ }
1264
+ }) : void 0;
1010
1265
  const detail = errorText.trim().length > 0 ? errorText : response.statusText || "authorization failed";
1011
1266
  return err(authUnauthorizedError("kv", `${context}: ${response.status} - ${detail}`, {
1012
1267
  status: response.status,
1013
- ...action && { requiredAction: action },
1014
- ...resource && { resource },
1015
- ...extraMeta
1268
+ ...extraMeta,
1269
+ ...capability
1016
1270
  }));
1017
1271
  }
1018
1272
  normalizeSignedReadUrlResponse(data) {
@@ -1046,23 +1300,35 @@ var KVService = class extends BaseService {
1046
1300
  ));
1047
1301
  }
1048
1302
  const path = this.getFullPath(key, options?.prefix);
1303
+ const request = this.createRequestSignal(options?.signal, options?.timeout);
1049
1304
  try {
1050
1305
  const response = await this.invokeOperation(
1051
1306
  path,
1052
1307
  KVAction.GET,
1053
1308
  void 0,
1054
- options?.signal,
1309
+ request.signal,
1055
1310
  options?.maxResponseBytes === void 0 ? void 0 : { "x-tinycloud-max-response-bytes": String(options.maxResponseBytes) }
1056
1311
  );
1057
1312
  if (!response.ok) {
1058
1313
  if (response.status === 401 || response.status === 403) {
1059
- const errorText2 = await response.text();
1314
+ const errorText2 = await this.readAuthorizationText(response);
1060
1315
  const permissionHint = parsePermissionHintFromErrorText(errorText2);
1316
+ const structuredCapability = permissionHint?.service === "tinycloud.kv" && permissionHint.space === this.context.session.spaceId && permissionHint.path === path ? validatedCapabilityOf({
1317
+ service: "kv",
1318
+ code: ErrorCodes.AUTH_UNAUTHORIZED,
1319
+ meta: {
1320
+ status: response.status,
1321
+ resource: `${this.context.session.spaceId}/kv/${path}`,
1322
+ requiredAction: KVAction.GET
1323
+ }
1324
+ }) : void 0;
1061
1325
  return this.authorizationFailure(
1062
1326
  `Failed to get key ${JSON.stringify(key)}`,
1063
1327
  response,
1064
1328
  errorText2,
1065
- permissionHint === void 0 ? {} : { permissionHint }
1329
+ [path],
1330
+ KVAction.GET,
1331
+ structuredCapability ? { permissionHint, ...structuredCapability } : {}
1066
1332
  );
1067
1333
  }
1068
1334
  if (response.status === 404) {
@@ -1097,6 +1363,8 @@ var KVService = class extends BaseService {
1097
1363
  });
1098
1364
  } catch (error) {
1099
1365
  return err(wrapError("kv", error));
1366
+ } finally {
1367
+ request.dispose();
1100
1368
  }
1101
1369
  });
1102
1370
  }
@@ -1124,12 +1392,13 @@ var KVService = class extends BaseService {
1124
1392
  }
1125
1393
  const path = this.getFullPath(key, options?.prefix);
1126
1394
  const body = this.serializePutValue(value, options?.contentType);
1395
+ const request = this.createRequestSignal(options?.signal, options?.timeout);
1127
1396
  try {
1128
1397
  const response = await this.invokeOperation(
1129
1398
  path,
1130
1399
  KVAction.PUT,
1131
1400
  body,
1132
- options?.signal,
1401
+ request.signal,
1133
1402
  {
1134
1403
  ...options?.ifMatch === void 0 ? {} : { "if-match": options.ifMatch },
1135
1404
  ...options?.ifNoneMatch === void 0 ? {} : { "if-none-match": options.ifNoneMatch }
@@ -1139,7 +1408,9 @@ var KVService = class extends BaseService {
1139
1408
  return this.authorizationFailure(
1140
1409
  `Failed to put key ${JSON.stringify(key)}`,
1141
1410
  response,
1142
- await response.text()
1411
+ await this.readAuthorizationText(response),
1412
+ [path],
1413
+ KVAction.PUT
1143
1414
  );
1144
1415
  }
1145
1416
  if (!response.ok) {
@@ -1183,6 +1454,8 @@ var KVService = class extends BaseService {
1183
1454
  });
1184
1455
  } catch (error) {
1185
1456
  return err(wrapError("kv", error));
1457
+ } finally {
1458
+ request.dispose();
1186
1459
  }
1187
1460
  });
1188
1461
  }
@@ -1222,6 +1495,7 @@ var KVService = class extends BaseService {
1222
1495
  seen.add(path);
1223
1496
  }
1224
1497
  let requestMayHaveDispatched = false;
1498
+ const request = this.createRequestSignal(options?.signal, options?.timeout);
1225
1499
  try {
1226
1500
  const body = new FormData();
1227
1501
  for (let index = 0; index < items.length; index++) {
@@ -1244,7 +1518,7 @@ var KVService = class extends BaseService {
1244
1518
  method: "POST",
1245
1519
  headers,
1246
1520
  body,
1247
- signal: this.combineSignals(options?.signal)
1521
+ signal: request.signal
1248
1522
  };
1249
1523
  const fetchFn = this.context.fetch;
1250
1524
  requestMayHaveDispatched = true;
@@ -1268,7 +1542,9 @@ var KVService = class extends BaseService {
1268
1542
  return this.authorizationFailure(
1269
1543
  `Failed to batch put ${items.length} key(s)`,
1270
1544
  response,
1271
- errorText
1545
+ errorText,
1546
+ paths,
1547
+ KVAction.PUT
1272
1548
  );
1273
1549
  }
1274
1550
  const quotaError = this.handleQuotaErrorResponse(
@@ -1293,14 +1569,16 @@ var KVService = class extends BaseService {
1293
1569
  rawBody = await response.json();
1294
1570
  } catch (jsonError) {
1295
1571
  const cause = jsonError instanceof Error ? jsonError : new Error(String(jsonError));
1572
+ const timedOut = isRequestTimeoutError(cause);
1296
1573
  return err(
1297
1574
  serviceError(
1298
- ErrorCodes.NETWORK_ERROR,
1299
- `KV batchPut response was not valid JSON: ${cause.message}`,
1575
+ timedOut ? ErrorCodes.TIMEOUT : ErrorCodes.NETWORK_ERROR,
1576
+ timedOut ? `KV batchPut response body did not arrive in time: ${cause.message}` : `KV batchPut response was not valid JSON: ${cause.message}`,
1300
1577
  "kv",
1301
1578
  {
1302
1579
  cause,
1303
1580
  meta: {
1581
+ ...timedOut ? { timeoutMs: cause.timeoutMs } : {},
1304
1582
  requestMayHaveDispatched: true,
1305
1583
  responseReceived: true,
1306
1584
  status: response.status,
@@ -1355,6 +1633,8 @@ var KVService = class extends BaseService {
1355
1633
  ...wrapped,
1356
1634
  meta: { ...wrapped.meta, requestMayHaveDispatched }
1357
1635
  });
1636
+ } finally {
1637
+ request.dispose();
1358
1638
  }
1359
1639
  });
1360
1640
  }
@@ -1377,23 +1657,26 @@ var KVService = class extends BaseService {
1377
1657
  if (options?.path) {
1378
1658
  listPath = listPath ? `${listPath}/${options.path}` : options.path;
1379
1659
  }
1660
+ const request = this.createRequestSignal(options?.signal, options?.timeout);
1380
1661
  try {
1381
1662
  const response = await this.invokeOperation(
1382
1663
  listPath,
1383
1664
  KVAction.LIST,
1384
1665
  void 0,
1385
- options?.signal,
1666
+ request.signal,
1386
1667
  options?.limit === void 0 && options?.cursor === void 0 ? void 0 : {
1387
1668
  ...options?.limit === void 0 ? {} : { "x-tinycloud-limit": String(options.limit) },
1388
1669
  ...options?.cursor === void 0 ? {} : { "x-tinycloud-cursor": options.cursor }
1389
1670
  }
1390
1671
  );
1391
1672
  if (!response.ok) {
1392
- if (response.status === 401) {
1673
+ if (response.status === 401 || response.status === 403) {
1393
1674
  return this.authorizationFailure(
1394
1675
  "Failed to list keys",
1395
1676
  response,
1396
- await response.text()
1677
+ await this.readAuthorizationText(response),
1678
+ [listPath],
1679
+ KVAction.LIST
1397
1680
  );
1398
1681
  }
1399
1682
  const errorText = await response.text();
@@ -1421,6 +1704,8 @@ var KVService = class extends BaseService {
1421
1704
  });
1422
1705
  } catch (error) {
1423
1706
  return err(wrapError("kv", error));
1707
+ } finally {
1708
+ request.dispose();
1424
1709
  }
1425
1710
  });
1426
1711
  }
@@ -1433,20 +1718,23 @@ var KVService = class extends BaseService {
1433
1718
  return err(authRequiredError("kv"));
1434
1719
  }
1435
1720
  const path = this.getFullPath(key, options?.prefix);
1721
+ const request = this.createRequestSignal(options?.signal, options?.timeout);
1436
1722
  try {
1437
1723
  const response = await this.invokeOperation(
1438
1724
  path,
1439
1725
  KVAction.DELETE,
1440
1726
  void 0,
1441
- options?.signal,
1727
+ request.signal,
1442
1728
  options?.ifMatch === void 0 ? void 0 : { "if-match": options.ifMatch }
1443
1729
  );
1444
1730
  if (!response.ok) {
1445
- if (response.status === 401) {
1731
+ if (response.status === 401 || response.status === 403) {
1446
1732
  return this.authorizationFailure(
1447
1733
  `Failed to delete key ${JSON.stringify(key)}`,
1448
1734
  response,
1449
- await response.text()
1735
+ await this.readAuthorizationText(response),
1736
+ [path],
1737
+ KVAction.DELETE
1450
1738
  );
1451
1739
  }
1452
1740
  if (response.status === 404) {
@@ -1484,6 +1772,8 @@ var KVService = class extends BaseService {
1484
1772
  });
1485
1773
  } catch (error) {
1486
1774
  return err(wrapError("kv", error));
1775
+ } finally {
1776
+ request.dispose();
1487
1777
  }
1488
1778
  });
1489
1779
  }
@@ -1496,19 +1786,22 @@ var KVService = class extends BaseService {
1496
1786
  return err(authRequiredError("kv"));
1497
1787
  }
1498
1788
  const path = this.getFullPath(key, options?.prefix);
1789
+ const request = this.createRequestSignal(options?.signal, options?.timeout);
1499
1790
  try {
1500
1791
  const response = await this.invokeOperation(
1501
1792
  path,
1502
1793
  KVAction.HEAD,
1503
1794
  void 0,
1504
- options?.signal
1795
+ request.signal
1505
1796
  );
1506
1797
  if (!response.ok) {
1507
- if (response.status === 401) {
1798
+ if (response.status === 401 || response.status === 403) {
1508
1799
  return this.authorizationFailure(
1509
1800
  `Failed to get metadata for key ${JSON.stringify(key)}`,
1510
1801
  response,
1511
- await response.text()
1802
+ await this.readAuthorizationText(response),
1803
+ [path],
1804
+ KVAction.HEAD
1512
1805
  );
1513
1806
  }
1514
1807
  if (response.status === 404) {
@@ -1530,6 +1823,8 @@ var KVService = class extends BaseService {
1530
1823
  });
1531
1824
  } catch (error) {
1532
1825
  return err(wrapError("kv", error));
1826
+ } finally {
1827
+ request.dispose();
1533
1828
  }
1534
1829
  });
1535
1830
  }
@@ -1567,15 +1862,16 @@ var KVService = class extends BaseService {
1567
1862
  if (options?.etag !== void 0) {
1568
1863
  body.etag = options.etag;
1569
1864
  }
1865
+ const request = this.createRequestSignal(options?.signal, options?.timeout);
1570
1866
  try {
1571
1867
  const response = await this.context.fetch(`${this.host}/signed/kv`, {
1572
1868
  method: "POST",
1573
1869
  headers: this.withJsonContentType(headers),
1574
1870
  body: JSON.stringify(body),
1575
- signal: this.combineSignals(options?.signal)
1871
+ signal: request.signal
1576
1872
  });
1577
1873
  if (!response.ok) {
1578
- return this.createSignedReadUrlError(response, key);
1874
+ return this.createSignedReadUrlError(response, key, path);
1579
1875
  }
1580
1876
  const signedUrl = this.normalizeSignedReadUrlResponse(
1581
1877
  await response.json()
@@ -1592,6 +1888,8 @@ var KVService = class extends BaseService {
1592
1888
  return ok(signedUrl);
1593
1889
  } catch (error) {
1594
1890
  return err(wrapError("kv", error));
1891
+ } finally {
1892
+ request.dispose();
1595
1893
  }
1596
1894
  });
1597
1895
  }