@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.js CHANGED
@@ -70,6 +70,55 @@ function serviceError(code, message, service, options) {
70
70
  };
71
71
  }
72
72
 
73
+ // src/errors.ts
74
+ import { CAPABILITY_REGISTRY } from "@tinycloud/bootstrap";
75
+
76
+ // src/encryption/networkId.ts
77
+ var URN_PREFIX = "urn:tinycloud:encryption:";
78
+ var NETWORK_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
79
+ var NetworkIdError = class extends Error {
80
+ constructor(message) {
81
+ super(message);
82
+ this.name = "NetworkIdError";
83
+ }
84
+ };
85
+ function parseNetworkId(networkId) {
86
+ if (typeof networkId !== "string" || networkId.length === 0) {
87
+ throw new NetworkIdError("networkId must be a non-empty string");
88
+ }
89
+ if (!networkId.startsWith(URN_PREFIX)) {
90
+ throw new NetworkIdError(
91
+ `networkId must start with ${URN_PREFIX} (got ${JSON.stringify(networkId)})`
92
+ );
93
+ }
94
+ const body = networkId.slice(URN_PREFIX.length);
95
+ const lastColon = body.lastIndexOf(":");
96
+ if (lastColon <= 0 || lastColon === body.length - 1) {
97
+ throw new NetworkIdError(
98
+ `networkId missing ownerDid or name segment (got ${JSON.stringify(networkId)})`
99
+ );
100
+ }
101
+ const ownerDid = body.slice(0, lastColon);
102
+ const name = body.slice(lastColon + 1);
103
+ if (!ownerDid.startsWith("did:")) {
104
+ throw new NetworkIdError(
105
+ `networkId ownerDid must be a DID (got ${JSON.stringify(ownerDid)})`
106
+ );
107
+ }
108
+ const didParts = ownerDid.split(":");
109
+ if (didParts.length < 3 || didParts.some((p) => p.length === 0)) {
110
+ throw new NetworkIdError(
111
+ `networkId ownerDid is not a well-formed DID (got ${JSON.stringify(ownerDid)})`
112
+ );
113
+ }
114
+ if (!NETWORK_NAME_RE.test(name)) {
115
+ throw new NetworkIdError(
116
+ `networkId name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
117
+ );
118
+ }
119
+ return { networkId, ownerDid, name };
120
+ }
121
+
73
122
  // src/errors.ts
74
123
  function parsePermissionHint(value) {
75
124
  if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
@@ -114,11 +163,31 @@ function authRequiredError(service) {
114
163
  service
115
164
  };
116
165
  }
117
- function timeoutError(service) {
166
+ var RequestTimeoutError = class extends Error {
167
+ constructor(timeoutMs) {
168
+ super(`Request timed out after ${timeoutMs}ms.`);
169
+ this.name = "TimeoutError";
170
+ this.timeoutMs = timeoutMs;
171
+ }
172
+ };
173
+ function isRequestTimeoutError(error) {
174
+ return error instanceof RequestTimeoutError || error instanceof Error && error.name === "TimeoutError" && typeof error.timeoutMs === "number";
175
+ }
176
+ function timeoutError(service, timeoutMs, cause) {
177
+ if (timeoutMs === void 0) {
178
+ return {
179
+ code: ErrorCodes.TIMEOUT,
180
+ message: "Request timed out.",
181
+ service,
182
+ ...cause === void 0 ? {} : { cause }
183
+ };
184
+ }
118
185
  return {
119
186
  code: ErrorCodes.TIMEOUT,
120
- message: "Request timed out.",
121
- service
187
+ message: `Request timed out after ${timeoutMs}ms.`,
188
+ service,
189
+ ...cause === void 0 ? {} : { cause },
190
+ meta: { timeoutMs }
122
191
  };
123
192
  }
124
193
  function abortedError(service) {
@@ -138,6 +207,101 @@ function parseAuthError(responseText) {
138
207
  function authUnauthorizedError(service, message, meta) {
139
208
  return serviceError(ErrorCodes.AUTH_UNAUTHORIZED, message, service, { meta });
140
209
  }
210
+ var grantableActions = new Set(
211
+ CAPABILITY_REGISTRY.map(({ urn }) => urn).filter((urn) => !urn.endsWith("/*"))
212
+ );
213
+ var capabilityServices = new Set(
214
+ CAPABILITY_REGISTRY.map(({ service }) => service.slice("tinycloud.".length))
215
+ );
216
+ function parseCapabilityResource(resource, service) {
217
+ if (resource.length === 0 || resource.length > 1024 || !capabilityServices.has(service) || /[\s\p{C}*#]/u.test(resource) || resource.includes("://")) return void 0;
218
+ const segments = resource.split("/");
219
+ if (!/^tinycloud:[A-Za-z0-9][A-Za-z0-9:._-]*$/.test(segments[0])) return void 0;
220
+ const serviceIndex = segments.findIndex(
221
+ (segment, index) => index > 0 && capabilityServices.has(segment)
222
+ );
223
+ if (serviceIndex < 1 || segments[serviceIndex] !== service) return void 0;
224
+ if (segments.slice(1, serviceIndex).some(
225
+ (segment) => segment.length === 0 || segment === "." || segment === ".."
226
+ )) return void 0;
227
+ const pathSegments = segments.slice(serviceIndex + 1);
228
+ if (pathSegments.some((segment) => segment === "." || segment === "..")) return void 0;
229
+ if (service !== "kv" && (pathSegments.length === 0 || pathSegments[pathSegments.length - 1] === "")) return void 0;
230
+ return {
231
+ space: segments.slice(0, serviceIndex).join("/"),
232
+ path: pathSegments.join("/")
233
+ };
234
+ }
235
+ function safeCapabilityResource(value, service, action) {
236
+ if (typeof value !== "string" || value.length === 0 || value.length > 1024) return false;
237
+ if (service === "encryption" && value.startsWith("urn:tinycloud:encryption:")) {
238
+ if (!/^[A-Za-z0-9_][A-Za-z0-9_.:/-]*$/.test(value) || value.includes("//") || value.split("/").some((segment) => segment === "." || segment === "..")) return false;
239
+ try {
240
+ parseNetworkId(value);
241
+ return true;
242
+ } catch {
243
+ return false;
244
+ }
245
+ }
246
+ const parsed = parseCapabilityResource(value, service);
247
+ return parsed !== void 0 && (!parsed.path.endsWith("/") || service === "kv" && action === "tinycloud.kv/list") && (parsed.path !== "" || service === "kv" && action === "tinycloud.kv/list");
248
+ }
249
+ function validatedCapabilityOf(error) {
250
+ const verdict = authorizationVerdictOf(error);
251
+ if (verdict !== "unauthenticated" && verdict !== "forbidden") return void 0;
252
+ const seen = /* @__PURE__ */ new Set();
253
+ let node = error;
254
+ for (let depth = 0; depth < MAX_CAUSE_DEPTH && typeof node === "object" && node !== null && !seen.has(node); depth += 1) {
255
+ seen.add(node);
256
+ const record = node;
257
+ const status = errorStatusOf(record);
258
+ const meta = record.meta;
259
+ if ((status === 401 || status === 403 || status === void 0 && record.code === ErrorCodes.AUTH_UNAUTHORIZED) && typeof meta === "object" && meta !== null && !Array.isArray(meta)) {
260
+ const metadata = meta;
261
+ const { resource, requiredAction } = metadata;
262
+ if (typeof requiredAction === "string" && grantableActions.has(requiredAction)) {
263
+ const actionService = requiredAction.slice("tinycloud.".length, requiredAction.indexOf("/"));
264
+ const explicitService = record.service === void 0 ? metadata.service : record.service;
265
+ if (safeCapabilityResource(resource, actionService, requiredAction) && (explicitService === void 0 || explicitService === actionService || explicitService === `tinycloud.${actionService}`)) {
266
+ return { resource, requiredAction };
267
+ }
268
+ }
269
+ }
270
+ node = record.cause;
271
+ }
272
+ return void 0;
273
+ }
274
+ var MAX_CAUSE_DEPTH = 8;
275
+ function errorStatusOf(node) {
276
+ const meta = node.meta;
277
+ const candidates = [
278
+ node.status,
279
+ node.statusCode,
280
+ typeof meta === "object" && meta !== null ? meta.status : void 0
281
+ ];
282
+ for (const candidate of candidates) {
283
+ if (typeof candidate === "number" && Number.isInteger(candidate) && candidate >= 400 && candidate <= 599) {
284
+ return candidate;
285
+ }
286
+ }
287
+ return void 0;
288
+ }
289
+ function authorizationVerdictOf(error) {
290
+ const seen = /* @__PURE__ */ new Set();
291
+ let sawUnauthorizedCode = false;
292
+ let node = error;
293
+ for (let depth = 0; depth < MAX_CAUSE_DEPTH && typeof node === "object" && node !== null && !seen.has(node); depth += 1) {
294
+ seen.add(node);
295
+ const record = node;
296
+ const status = errorStatusOf(record);
297
+ if (status === 401) return "unauthenticated";
298
+ if (status === 403) return "forbidden";
299
+ if (status !== void 0) return "other";
300
+ if (record.code === ErrorCodes.AUTH_UNAUTHORIZED) sawUnauthorizedCode = true;
301
+ node = record.cause;
302
+ }
303
+ return sawUnauthorizedCode ? "forbidden" : void 0;
304
+ }
141
305
  function storageQuotaExceededError(service, message, meta) {
142
306
  return {
143
307
  code: ErrorCodes.STORAGE_QUOTA_EXCEEDED,
@@ -170,6 +334,9 @@ function storageRejectionError(service, status, meta, responseText) {
170
334
  }
171
335
  function wrapError(service, error, defaultCode = ErrorCodes.NETWORK_ERROR) {
172
336
  if (error instanceof Error) {
337
+ if (isRequestTimeoutError(error)) {
338
+ return timeoutError(service, error.timeoutMs, error);
339
+ }
173
340
  if (error.name === "AbortError") {
174
341
  return abortedError(service);
175
342
  }
@@ -191,6 +358,7 @@ function wrapError(service, error, defaultCode = ErrorCodes.NETWORK_ERROR) {
191
358
  }
192
359
 
193
360
  // src/base/BaseService.ts
361
+ var MAX_TIMER_DELAY_MS = 2147483647;
194
362
  var BaseService = class {
195
363
  constructor() {
196
364
  /**
@@ -371,6 +539,64 @@ var BaseService = class {
371
539
  }
372
540
  return controller.signal;
373
541
  }
542
+ /**
543
+ * Create the abort signal for one request, bounded by the request timeout.
544
+ *
545
+ * The signal aborts when the service signs out, the context aborts, the
546
+ * caller's `signal` aborts, or the timeout elapses, whichever comes first.
547
+ * A timeout aborts with a {@link RequestTimeoutError} (`TimeoutError`), which
548
+ * `wrapError` maps to `ErrorCodes.TIMEOUT`; the other sources keep their own
549
+ * reason (normally `AbortError`, mapped to `ErrorCodes.ABORTED`).
550
+ *
551
+ * The timeout is `timeoutMs` when given, otherwise `config.timeout`. Only a
552
+ * positive, finite value (at most 2^31-1 ms) applies; `undefined`, `0`, and
553
+ * `Infinity` mean no timeout, so a per-call `0` opts out of a configured one.
554
+ *
555
+ * Callers must call `dispose()` once the response body has been consumed,
556
+ * which clears the timer and removes the listeners this adds to the
557
+ * long-lived service and context signals.
558
+ *
559
+ * @param signal - The caller's abort signal for this request
560
+ * @param timeoutMs - Per-request timeout override in milliseconds
561
+ */
562
+ createRequestSignal(signal, timeoutMs) {
563
+ const controller = new AbortController();
564
+ const detachers = [];
565
+ let timer;
566
+ const dispose = () => {
567
+ if (timer !== void 0) {
568
+ clearTimeout(timer);
569
+ timer = void 0;
570
+ }
571
+ for (const detach of detachers.splice(0)) {
572
+ detach();
573
+ }
574
+ };
575
+ const abort = (reason) => {
576
+ dispose();
577
+ controller.abort(reason);
578
+ };
579
+ const requestSignal = { signal: controller.signal, dispose };
580
+ const parents = [
581
+ this.abortController.signal,
582
+ this.context?.abortSignal,
583
+ signal
584
+ ].filter((parent) => parent !== void 0);
585
+ for (const parent of parents) {
586
+ if (parent.aborted) {
587
+ abort(parent.reason);
588
+ return requestSignal;
589
+ }
590
+ const onAbort = () => abort(parent.reason);
591
+ parent.addEventListener("abort", onAbort, { once: true });
592
+ detachers.push(() => parent.removeEventListener("abort", onAbort));
593
+ }
594
+ const timeout = timeoutMs ?? this._config.timeout;
595
+ if (typeof timeout === "number" && timeout > 0 && timeout <= MAX_TIMER_DELAY_MS) {
596
+ timer = setTimeout(() => abort(new RequestTimeoutError(timeout)), timeout);
597
+ }
598
+ return requestSignal;
599
+ }
374
600
  /**
375
601
  * Wrap an operation with error handling and telemetry.
376
602
  *
@@ -631,7 +857,7 @@ var KVService = class extends BaseService {
631
857
  * @param path - Resource path
632
858
  * @param action - KV action
633
859
  * @param body - Optional request body
634
- * @param signal - Optional abort signal
860
+ * @param signal - Request signal from createRequestSignal()
635
861
  * @returns Fetch response
636
862
  */
637
863
  async invokeOperation(path, action, body, signal, extraHeaders) {
@@ -647,7 +873,7 @@ var KVService = class extends BaseService {
647
873
  method: "POST",
648
874
  headers: requestHeaders,
649
875
  body,
650
- signal: this.combineSignals(signal)
876
+ signal
651
877
  });
652
878
  }
653
879
  /**
@@ -824,6 +1050,7 @@ var KVService = class extends BaseService {
824
1050
  "kv"
825
1051
  ));
826
1052
  }
1053
+ const request = this.createRequestSignal(options?.signal, options?.timeout);
827
1054
  try {
828
1055
  const session = this.context.session;
829
1056
  const invocationHeaders = this.context.invokeAny(
@@ -845,15 +1072,17 @@ var KVService = class extends BaseService {
845
1072
  const response = await this.context.fetch(`${this.host}/invoke`, {
846
1073
  method: "POST",
847
1074
  headers,
848
- signal: this.combineSignals(options?.signal)
1075
+ signal: request.signal
849
1076
  });
850
1077
  if (!response.ok) {
851
- const errorText = await response.text();
1078
+ const errorText = response.status === 401 || response.status === 403 ? await this.readAuthorizationText(response) : await response.text();
852
1079
  if (response.status === 401 || response.status === 403) {
853
1080
  return this.authorizationFailure(
854
1081
  `Failed to batch read ${keys.length} key(s)`,
855
1082
  response,
856
- errorText
1083
+ errorText,
1084
+ paths,
1085
+ action
857
1086
  );
858
1087
  }
859
1088
  if (response.status === 413) {
@@ -908,6 +1137,8 @@ var KVService = class extends BaseService {
908
1137
  return ok({ results, count: results.length });
909
1138
  } catch (error) {
910
1139
  return err(wrapError("kv", error));
1140
+ } finally {
1141
+ request.dispose();
911
1142
  }
912
1143
  }
913
1144
  /**
@@ -943,7 +1174,7 @@ var KVService = class extends BaseService {
943
1174
  return text;
944
1175
  }
945
1176
  }
946
- async createSignedReadUrlError(response, key) {
1177
+ async createSignedReadUrlError(response, key, path) {
947
1178
  let errorText = response.statusText;
948
1179
  try {
949
1180
  const text = await response.text();
@@ -956,7 +1187,9 @@ var KVService = class extends BaseService {
956
1187
  return this.authorizationFailure(
957
1188
  `Failed to create signed read URL for key ${JSON.stringify(key)}`,
958
1189
  response,
959
- errorText
1190
+ errorText,
1191
+ [path],
1192
+ KVAction.GET
960
1193
  );
961
1194
  }
962
1195
  const code = response.status === 400 ? ErrorCodes.INVALID_INPUT : ErrorCodes.NETWORK_ERROR;
@@ -969,21 +1202,42 @@ var KVService = class extends BaseService {
969
1202
  )
970
1203
  );
971
1204
  }
1205
+ /** A known 401/403 keeps its typed status even if reading its body fails. */
1206
+ async readAuthorizationText(response) {
1207
+ try {
1208
+ return await response.text();
1209
+ } catch {
1210
+ return "";
1211
+ }
1212
+ }
972
1213
  /**
973
1214
  * AUTH_UNAUTHORIZED for a 401/403 response. The message keeps the HTTP
974
1215
  * status and the server text (`<context>: <status> - <text>`, the same
975
1216
  * shape as every other KV failure) so callers that rethrow only the message
976
1217
  * still see the status; `meta.status` carries it for typed callers.
977
- * Resource/ability hints are parsed from the raw body, never the message.
1218
+ * Server hints must match the requested path/action (whether the node sends
1219
+ * a relative path or full resource); metadata uses only the request-derived
1220
+ * canonical resource under the authenticated session's space.
978
1221
  */
979
- authorizationFailure(context, response, errorText, extraMeta = {}) {
1222
+ authorizationFailure(context, response, errorText, paths, expectedAction, extraMeta = {}) {
980
1223
  const { resource, action } = parseAuthError(errorText);
1224
+ const requestedPath = paths.find(
1225
+ (path) => resource === path || resource === `${this.context.session.spaceId}/kv/${path}`
1226
+ );
1227
+ const capability = requestedPath !== void 0 && action === expectedAction ? validatedCapabilityOf({
1228
+ service: "kv",
1229
+ code: ErrorCodes.AUTH_UNAUTHORIZED,
1230
+ meta: {
1231
+ status: response.status,
1232
+ resource: `${this.context.session.spaceId}/kv/${requestedPath}`,
1233
+ requiredAction: action
1234
+ }
1235
+ }) : void 0;
981
1236
  const detail = errorText.trim().length > 0 ? errorText : response.statusText || "authorization failed";
982
1237
  return err(authUnauthorizedError("kv", `${context}: ${response.status} - ${detail}`, {
983
1238
  status: response.status,
984
- ...action && { requiredAction: action },
985
- ...resource && { resource },
986
- ...extraMeta
1239
+ ...extraMeta,
1240
+ ...capability
987
1241
  }));
988
1242
  }
989
1243
  normalizeSignedReadUrlResponse(data) {
@@ -1017,23 +1271,35 @@ var KVService = class extends BaseService {
1017
1271
  ));
1018
1272
  }
1019
1273
  const path = this.getFullPath(key, options?.prefix);
1274
+ const request = this.createRequestSignal(options?.signal, options?.timeout);
1020
1275
  try {
1021
1276
  const response = await this.invokeOperation(
1022
1277
  path,
1023
1278
  KVAction.GET,
1024
1279
  void 0,
1025
- options?.signal,
1280
+ request.signal,
1026
1281
  options?.maxResponseBytes === void 0 ? void 0 : { "x-tinycloud-max-response-bytes": String(options.maxResponseBytes) }
1027
1282
  );
1028
1283
  if (!response.ok) {
1029
1284
  if (response.status === 401 || response.status === 403) {
1030
- const errorText2 = await response.text();
1285
+ const errorText2 = await this.readAuthorizationText(response);
1031
1286
  const permissionHint = parsePermissionHintFromErrorText(errorText2);
1287
+ const structuredCapability = permissionHint?.service === "tinycloud.kv" && permissionHint.space === this.context.session.spaceId && permissionHint.path === path ? validatedCapabilityOf({
1288
+ service: "kv",
1289
+ code: ErrorCodes.AUTH_UNAUTHORIZED,
1290
+ meta: {
1291
+ status: response.status,
1292
+ resource: `${this.context.session.spaceId}/kv/${path}`,
1293
+ requiredAction: KVAction.GET
1294
+ }
1295
+ }) : void 0;
1032
1296
  return this.authorizationFailure(
1033
1297
  `Failed to get key ${JSON.stringify(key)}`,
1034
1298
  response,
1035
1299
  errorText2,
1036
- permissionHint === void 0 ? {} : { permissionHint }
1300
+ [path],
1301
+ KVAction.GET,
1302
+ structuredCapability ? { permissionHint, ...structuredCapability } : {}
1037
1303
  );
1038
1304
  }
1039
1305
  if (response.status === 404) {
@@ -1068,6 +1334,8 @@ var KVService = class extends BaseService {
1068
1334
  });
1069
1335
  } catch (error) {
1070
1336
  return err(wrapError("kv", error));
1337
+ } finally {
1338
+ request.dispose();
1071
1339
  }
1072
1340
  });
1073
1341
  }
@@ -1095,12 +1363,13 @@ var KVService = class extends BaseService {
1095
1363
  }
1096
1364
  const path = this.getFullPath(key, options?.prefix);
1097
1365
  const body = this.serializePutValue(value, options?.contentType);
1366
+ const request = this.createRequestSignal(options?.signal, options?.timeout);
1098
1367
  try {
1099
1368
  const response = await this.invokeOperation(
1100
1369
  path,
1101
1370
  KVAction.PUT,
1102
1371
  body,
1103
- options?.signal,
1372
+ request.signal,
1104
1373
  {
1105
1374
  ...options?.ifMatch === void 0 ? {} : { "if-match": options.ifMatch },
1106
1375
  ...options?.ifNoneMatch === void 0 ? {} : { "if-none-match": options.ifNoneMatch }
@@ -1110,7 +1379,9 @@ var KVService = class extends BaseService {
1110
1379
  return this.authorizationFailure(
1111
1380
  `Failed to put key ${JSON.stringify(key)}`,
1112
1381
  response,
1113
- await response.text()
1382
+ await this.readAuthorizationText(response),
1383
+ [path],
1384
+ KVAction.PUT
1114
1385
  );
1115
1386
  }
1116
1387
  if (!response.ok) {
@@ -1154,6 +1425,8 @@ var KVService = class extends BaseService {
1154
1425
  });
1155
1426
  } catch (error) {
1156
1427
  return err(wrapError("kv", error));
1428
+ } finally {
1429
+ request.dispose();
1157
1430
  }
1158
1431
  });
1159
1432
  }
@@ -1193,6 +1466,7 @@ var KVService = class extends BaseService {
1193
1466
  seen.add(path);
1194
1467
  }
1195
1468
  let requestMayHaveDispatched = false;
1469
+ const request = this.createRequestSignal(options?.signal, options?.timeout);
1196
1470
  try {
1197
1471
  const body = new FormData();
1198
1472
  for (let index = 0; index < items.length; index++) {
@@ -1215,7 +1489,7 @@ var KVService = class extends BaseService {
1215
1489
  method: "POST",
1216
1490
  headers,
1217
1491
  body,
1218
- signal: this.combineSignals(options?.signal)
1492
+ signal: request.signal
1219
1493
  };
1220
1494
  const fetchFn = this.context.fetch;
1221
1495
  requestMayHaveDispatched = true;
@@ -1239,7 +1513,9 @@ var KVService = class extends BaseService {
1239
1513
  return this.authorizationFailure(
1240
1514
  `Failed to batch put ${items.length} key(s)`,
1241
1515
  response,
1242
- errorText
1516
+ errorText,
1517
+ paths,
1518
+ KVAction.PUT
1243
1519
  );
1244
1520
  }
1245
1521
  const quotaError = this.handleQuotaErrorResponse(
@@ -1264,14 +1540,16 @@ var KVService = class extends BaseService {
1264
1540
  rawBody = await response.json();
1265
1541
  } catch (jsonError) {
1266
1542
  const cause = jsonError instanceof Error ? jsonError : new Error(String(jsonError));
1543
+ const timedOut = isRequestTimeoutError(cause);
1267
1544
  return err(
1268
1545
  serviceError(
1269
- ErrorCodes.NETWORK_ERROR,
1270
- `KV batchPut response was not valid JSON: ${cause.message}`,
1546
+ timedOut ? ErrorCodes.TIMEOUT : ErrorCodes.NETWORK_ERROR,
1547
+ timedOut ? `KV batchPut response body did not arrive in time: ${cause.message}` : `KV batchPut response was not valid JSON: ${cause.message}`,
1271
1548
  "kv",
1272
1549
  {
1273
1550
  cause,
1274
1551
  meta: {
1552
+ ...timedOut ? { timeoutMs: cause.timeoutMs } : {},
1275
1553
  requestMayHaveDispatched: true,
1276
1554
  responseReceived: true,
1277
1555
  status: response.status,
@@ -1326,6 +1604,8 @@ var KVService = class extends BaseService {
1326
1604
  ...wrapped,
1327
1605
  meta: { ...wrapped.meta, requestMayHaveDispatched }
1328
1606
  });
1607
+ } finally {
1608
+ request.dispose();
1329
1609
  }
1330
1610
  });
1331
1611
  }
@@ -1348,23 +1628,26 @@ var KVService = class extends BaseService {
1348
1628
  if (options?.path) {
1349
1629
  listPath = listPath ? `${listPath}/${options.path}` : options.path;
1350
1630
  }
1631
+ const request = this.createRequestSignal(options?.signal, options?.timeout);
1351
1632
  try {
1352
1633
  const response = await this.invokeOperation(
1353
1634
  listPath,
1354
1635
  KVAction.LIST,
1355
1636
  void 0,
1356
- options?.signal,
1637
+ request.signal,
1357
1638
  options?.limit === void 0 && options?.cursor === void 0 ? void 0 : {
1358
1639
  ...options?.limit === void 0 ? {} : { "x-tinycloud-limit": String(options.limit) },
1359
1640
  ...options?.cursor === void 0 ? {} : { "x-tinycloud-cursor": options.cursor }
1360
1641
  }
1361
1642
  );
1362
1643
  if (!response.ok) {
1363
- if (response.status === 401) {
1644
+ if (response.status === 401 || response.status === 403) {
1364
1645
  return this.authorizationFailure(
1365
1646
  "Failed to list keys",
1366
1647
  response,
1367
- await response.text()
1648
+ await this.readAuthorizationText(response),
1649
+ [listPath],
1650
+ KVAction.LIST
1368
1651
  );
1369
1652
  }
1370
1653
  const errorText = await response.text();
@@ -1392,6 +1675,8 @@ var KVService = class extends BaseService {
1392
1675
  });
1393
1676
  } catch (error) {
1394
1677
  return err(wrapError("kv", error));
1678
+ } finally {
1679
+ request.dispose();
1395
1680
  }
1396
1681
  });
1397
1682
  }
@@ -1404,20 +1689,23 @@ var KVService = class extends BaseService {
1404
1689
  return err(authRequiredError("kv"));
1405
1690
  }
1406
1691
  const path = this.getFullPath(key, options?.prefix);
1692
+ const request = this.createRequestSignal(options?.signal, options?.timeout);
1407
1693
  try {
1408
1694
  const response = await this.invokeOperation(
1409
1695
  path,
1410
1696
  KVAction.DELETE,
1411
1697
  void 0,
1412
- options?.signal,
1698
+ request.signal,
1413
1699
  options?.ifMatch === void 0 ? void 0 : { "if-match": options.ifMatch }
1414
1700
  );
1415
1701
  if (!response.ok) {
1416
- if (response.status === 401) {
1702
+ if (response.status === 401 || response.status === 403) {
1417
1703
  return this.authorizationFailure(
1418
1704
  `Failed to delete key ${JSON.stringify(key)}`,
1419
1705
  response,
1420
- await response.text()
1706
+ await this.readAuthorizationText(response),
1707
+ [path],
1708
+ KVAction.DELETE
1421
1709
  );
1422
1710
  }
1423
1711
  if (response.status === 404) {
@@ -1455,6 +1743,8 @@ var KVService = class extends BaseService {
1455
1743
  });
1456
1744
  } catch (error) {
1457
1745
  return err(wrapError("kv", error));
1746
+ } finally {
1747
+ request.dispose();
1458
1748
  }
1459
1749
  });
1460
1750
  }
@@ -1467,19 +1757,22 @@ var KVService = class extends BaseService {
1467
1757
  return err(authRequiredError("kv"));
1468
1758
  }
1469
1759
  const path = this.getFullPath(key, options?.prefix);
1760
+ const request = this.createRequestSignal(options?.signal, options?.timeout);
1470
1761
  try {
1471
1762
  const response = await this.invokeOperation(
1472
1763
  path,
1473
1764
  KVAction.HEAD,
1474
1765
  void 0,
1475
- options?.signal
1766
+ request.signal
1476
1767
  );
1477
1768
  if (!response.ok) {
1478
- if (response.status === 401) {
1769
+ if (response.status === 401 || response.status === 403) {
1479
1770
  return this.authorizationFailure(
1480
1771
  `Failed to get metadata for key ${JSON.stringify(key)}`,
1481
1772
  response,
1482
- await response.text()
1773
+ await this.readAuthorizationText(response),
1774
+ [path],
1775
+ KVAction.HEAD
1483
1776
  );
1484
1777
  }
1485
1778
  if (response.status === 404) {
@@ -1501,6 +1794,8 @@ var KVService = class extends BaseService {
1501
1794
  });
1502
1795
  } catch (error) {
1503
1796
  return err(wrapError("kv", error));
1797
+ } finally {
1798
+ request.dispose();
1504
1799
  }
1505
1800
  });
1506
1801
  }
@@ -1538,15 +1833,16 @@ var KVService = class extends BaseService {
1538
1833
  if (options?.etag !== void 0) {
1539
1834
  body.etag = options.etag;
1540
1835
  }
1836
+ const request = this.createRequestSignal(options?.signal, options?.timeout);
1541
1837
  try {
1542
1838
  const response = await this.context.fetch(`${this.host}/signed/kv`, {
1543
1839
  method: "POST",
1544
1840
  headers: this.withJsonContentType(headers),
1545
1841
  body: JSON.stringify(body),
1546
- signal: this.combineSignals(options?.signal)
1842
+ signal: request.signal
1547
1843
  });
1548
1844
  if (!response.ok) {
1549
- return this.createSignedReadUrlError(response, key);
1845
+ return this.createSignedReadUrlError(response, key, path);
1550
1846
  }
1551
1847
  const signedUrl = this.normalizeSignedReadUrlResponse(
1552
1848
  await response.json()
@@ -1563,6 +1859,8 @@ var KVService = class extends BaseService {
1563
1859
  return ok(signedUrl);
1564
1860
  } catch (error) {
1565
1861
  return err(wrapError("kv", error));
1862
+ } finally {
1863
+ request.dispose();
1566
1864
  }
1567
1865
  });
1568
1866
  }