@tinycloud/sdk-services 3.0.0 → 3.1.0-beta.10

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.js CHANGED
@@ -5,6 +5,10 @@ var ErrorCodes = {
5
5
  AUTH_EXPIRED: "AUTH_EXPIRED",
6
6
  AUTH_REQUIRED: "AUTH_REQUIRED",
7
7
  AUTH_UNAUTHORIZED: "AUTH_UNAUTHORIZED",
8
+ /** HTTP 401: the delegation the invocation cites was revoked. */
9
+ AUTH_DELEGATION_REVOKED: "AUTH_DELEGATION_REVOKED",
10
+ /** HTTP 401: a delegation the cited one rests on was revoked. */
11
+ AUTH_DELEGATION_ANCESTOR_REVOKED: "AUTH_DELEGATION_ANCESTOR_REVOKED",
8
12
  NETWORK_ERROR: "NETWORK_ERROR",
9
13
  TIMEOUT: "TIMEOUT",
10
14
  ABORTED: "ABORTED",
@@ -16,6 +20,10 @@ var ErrorCodes = {
16
20
  KV_PRECONDITION_FAILED: "KV_PRECONDITION_FAILED",
17
21
  KV_CONFLICT: "KV_CONFLICT",
18
22
  KV_RESPONSE_TOO_LARGE: "KV_RESPONSE_TOO_LARGE",
23
+ /** HTTP 410 from the change feed: discard sync state and restart without a cursor; `meta.reason` says why. */
24
+ KV_SYNC_RESET_REQUIRED: "KV_SYNC_RESET_REQUIRED",
25
+ /** HTTP 403 from the change feed: the retention grant was refused; `meta.reason` says why. */
26
+ KV_RETENTION_GRANT_REFUSED: "KV_RETENTION_GRANT_REFUSED",
19
27
  // SQL-specific errors
20
28
  SQL_ERROR: "SQL_ERROR",
21
29
  SQL_PERMISSION_DENIED: "SQL_PERMISSION_DENIED",
@@ -896,6 +904,106 @@ var ServiceContext = class {
896
904
  }
897
905
  };
898
906
 
907
+ // src/errors.ts
908
+ import { CAPABILITY_REGISTRY } from "@tinycloud/bootstrap";
909
+
910
+ // src/encryption/networkId.ts
911
+ var URN_PREFIX = "urn:tinycloud:encryption:";
912
+ var NETWORK_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
913
+ var PKH_EIP155_DID_RE = /^did:pkh:eip155:(\d+):(0x[a-fA-F0-9]{40})$/;
914
+ var NetworkIdError = class extends Error {
915
+ constructor(message) {
916
+ super(message);
917
+ this.name = "NetworkIdError";
918
+ }
919
+ };
920
+ function parseNetworkId(networkId) {
921
+ if (typeof networkId !== "string" || networkId.length === 0) {
922
+ throw new NetworkIdError("networkId must be a non-empty string");
923
+ }
924
+ if (!networkId.startsWith(URN_PREFIX)) {
925
+ throw new NetworkIdError(
926
+ `networkId must start with ${URN_PREFIX} (got ${JSON.stringify(networkId)})`
927
+ );
928
+ }
929
+ const body = networkId.slice(URN_PREFIX.length);
930
+ const lastColon = body.lastIndexOf(":");
931
+ if (lastColon <= 0 || lastColon === body.length - 1) {
932
+ throw new NetworkIdError(
933
+ `networkId missing ownerDid or name segment (got ${JSON.stringify(networkId)})`
934
+ );
935
+ }
936
+ const ownerDid = body.slice(0, lastColon);
937
+ const name = body.slice(lastColon + 1);
938
+ if (!ownerDid.startsWith("did:")) {
939
+ throw new NetworkIdError(
940
+ `networkId ownerDid must be a DID (got ${JSON.stringify(ownerDid)})`
941
+ );
942
+ }
943
+ const didParts = ownerDid.split(":");
944
+ if (didParts.length < 3 || didParts.some((p) => p.length === 0)) {
945
+ throw new NetworkIdError(
946
+ `networkId ownerDid is not a well-formed DID (got ${JSON.stringify(ownerDid)})`
947
+ );
948
+ }
949
+ if (!NETWORK_NAME_RE.test(name)) {
950
+ throw new NetworkIdError(
951
+ `networkId name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
952
+ );
953
+ }
954
+ return { networkId, ownerDid, name };
955
+ }
956
+ function buildNetworkId(ownerDid, name) {
957
+ if (typeof ownerDid !== "string" || !ownerDid.startsWith("did:")) {
958
+ throw new NetworkIdError("ownerDid must be a DID");
959
+ }
960
+ if (typeof name !== "string" || !NETWORK_NAME_RE.test(name)) {
961
+ throw new NetworkIdError(
962
+ `network name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
963
+ );
964
+ }
965
+ const networkId = `${URN_PREFIX}${ownerDid}:${name}`;
966
+ parseNetworkId(networkId);
967
+ return networkId;
968
+ }
969
+ function isNetworkId(networkId) {
970
+ if (typeof networkId !== "string") {
971
+ return false;
972
+ }
973
+ try {
974
+ parseNetworkId(networkId);
975
+ return true;
976
+ } catch {
977
+ return false;
978
+ }
979
+ }
980
+ function parsePkhOwnerDid(ownerDid) {
981
+ const match = ownerDid.match(PKH_EIP155_DID_RE);
982
+ if (!match) return null;
983
+ return {
984
+ chainId: match[1],
985
+ address: match[2].toLowerCase()
986
+ };
987
+ }
988
+ function ownerDidMatches(a, b) {
989
+ const aPkh = parsePkhOwnerDid(a);
990
+ const bPkh = parsePkhOwnerDid(b);
991
+ if (aPkh && bPkh) {
992
+ return aPkh.chainId === bPkh.chainId && aPkh.address === bPkh.address;
993
+ }
994
+ return a === b;
995
+ }
996
+ function networkDiscoveryKey(name) {
997
+ if (!NETWORK_NAME_RE.test(name)) {
998
+ throw new NetworkIdError(
999
+ `network name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
1000
+ );
1001
+ }
1002
+ return `.well-known/encryption/network/${name}`;
1003
+ }
1004
+ var ENCRYPTION_NETWORK_URN_PREFIX = URN_PREFIX;
1005
+ var NETWORK_NAME_PATTERN = NETWORK_NAME_RE;
1006
+
899
1007
  // src/errors.ts
900
1008
  function parsePermissionHint(value) {
901
1009
  if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
@@ -955,11 +1063,31 @@ function networkError(service, message, cause) {
955
1063
  cause
956
1064
  };
957
1065
  }
958
- function timeoutError(service) {
1066
+ var RequestTimeoutError = class extends Error {
1067
+ constructor(timeoutMs) {
1068
+ super(`Request timed out after ${timeoutMs}ms.`);
1069
+ this.name = "TimeoutError";
1070
+ this.timeoutMs = timeoutMs;
1071
+ }
1072
+ };
1073
+ function isRequestTimeoutError(error) {
1074
+ return error instanceof RequestTimeoutError || error instanceof Error && error.name === "TimeoutError" && typeof error.timeoutMs === "number";
1075
+ }
1076
+ function timeoutError(service, timeoutMs, cause) {
1077
+ if (timeoutMs === void 0) {
1078
+ return {
1079
+ code: ErrorCodes.TIMEOUT,
1080
+ message: "Request timed out.",
1081
+ service,
1082
+ ...cause === void 0 ? {} : { cause }
1083
+ };
1084
+ }
959
1085
  return {
960
1086
  code: ErrorCodes.TIMEOUT,
961
- message: "Request timed out.",
962
- service
1087
+ message: `Request timed out after ${timeoutMs}ms.`,
1088
+ service,
1089
+ ...cause === void 0 ? {} : { cause },
1090
+ meta: { timeoutMs }
963
1091
  };
964
1092
  }
965
1093
  function abortedError(service) {
@@ -993,6 +1121,101 @@ function parseAuthError(responseText) {
993
1121
  function authUnauthorizedError(service, message, meta) {
994
1122
  return serviceError(ErrorCodes.AUTH_UNAUTHORIZED, message, service, { meta });
995
1123
  }
1124
+ var grantableActions = new Set(
1125
+ CAPABILITY_REGISTRY.map(({ urn }) => urn).filter((urn) => !urn.endsWith("/*"))
1126
+ );
1127
+ var capabilityServices = new Set(
1128
+ CAPABILITY_REGISTRY.map(({ service }) => service.slice("tinycloud.".length))
1129
+ );
1130
+ function parseCapabilityResource(resource, service) {
1131
+ if (resource.length === 0 || resource.length > 1024 || !capabilityServices.has(service) || /[\s\p{C}*#]/u.test(resource) || resource.includes("://")) return void 0;
1132
+ const segments = resource.split("/");
1133
+ if (!/^tinycloud:[A-Za-z0-9][A-Za-z0-9:._-]*$/.test(segments[0])) return void 0;
1134
+ const serviceIndex = segments.findIndex(
1135
+ (segment, index) => index > 0 && capabilityServices.has(segment)
1136
+ );
1137
+ if (serviceIndex < 1 || segments[serviceIndex] !== service) return void 0;
1138
+ if (segments.slice(1, serviceIndex).some(
1139
+ (segment) => segment.length === 0 || segment === "." || segment === ".."
1140
+ )) return void 0;
1141
+ const pathSegments = segments.slice(serviceIndex + 1);
1142
+ if (pathSegments.some((segment) => segment === "." || segment === "..")) return void 0;
1143
+ if (service !== "kv" && (pathSegments.length === 0 || pathSegments[pathSegments.length - 1] === "")) return void 0;
1144
+ return {
1145
+ space: segments.slice(0, serviceIndex).join("/"),
1146
+ path: pathSegments.join("/")
1147
+ };
1148
+ }
1149
+ function safeCapabilityResource(value, service, action) {
1150
+ if (typeof value !== "string" || value.length === 0 || value.length > 1024) return false;
1151
+ if (service === "encryption" && value.startsWith("urn:tinycloud:encryption:")) {
1152
+ if (!/^[A-Za-z0-9_][A-Za-z0-9_.:/-]*$/.test(value) || value.includes("//") || value.split("/").some((segment) => segment === "." || segment === "..")) return false;
1153
+ try {
1154
+ parseNetworkId(value);
1155
+ return true;
1156
+ } catch {
1157
+ return false;
1158
+ }
1159
+ }
1160
+ const parsed = parseCapabilityResource(value, service);
1161
+ return parsed !== void 0 && (!parsed.path.endsWith("/") || service === "kv" && action === "tinycloud.kv/list") && (parsed.path !== "" || service === "kv" && action === "tinycloud.kv/list");
1162
+ }
1163
+ function validatedCapabilityOf(error) {
1164
+ const verdict = authorizationVerdictOf(error);
1165
+ if (verdict !== "unauthenticated" && verdict !== "forbidden") return void 0;
1166
+ const seen = /* @__PURE__ */ new Set();
1167
+ let node = error;
1168
+ for (let depth = 0; depth < MAX_CAUSE_DEPTH && typeof node === "object" && node !== null && !seen.has(node); depth += 1) {
1169
+ seen.add(node);
1170
+ const record = node;
1171
+ const status = errorStatusOf(record);
1172
+ const meta = record.meta;
1173
+ if ((status === 401 || status === 403 || status === void 0 && record.code === ErrorCodes.AUTH_UNAUTHORIZED) && typeof meta === "object" && meta !== null && !Array.isArray(meta)) {
1174
+ const metadata = meta;
1175
+ const { resource, requiredAction } = metadata;
1176
+ if (typeof requiredAction === "string" && grantableActions.has(requiredAction)) {
1177
+ const actionService = requiredAction.slice("tinycloud.".length, requiredAction.indexOf("/"));
1178
+ const explicitService = record.service === void 0 ? metadata.service : record.service;
1179
+ if (safeCapabilityResource(resource, actionService, requiredAction) && (explicitService === void 0 || explicitService === actionService || explicitService === `tinycloud.${actionService}`)) {
1180
+ return { resource, requiredAction };
1181
+ }
1182
+ }
1183
+ }
1184
+ node = record.cause;
1185
+ }
1186
+ return void 0;
1187
+ }
1188
+ var MAX_CAUSE_DEPTH = 8;
1189
+ function errorStatusOf(node) {
1190
+ const meta = node.meta;
1191
+ const candidates = [
1192
+ node.status,
1193
+ node.statusCode,
1194
+ typeof meta === "object" && meta !== null ? meta.status : void 0
1195
+ ];
1196
+ for (const candidate of candidates) {
1197
+ if (typeof candidate === "number" && Number.isInteger(candidate) && candidate >= 400 && candidate <= 599) {
1198
+ return candidate;
1199
+ }
1200
+ }
1201
+ return void 0;
1202
+ }
1203
+ function authorizationVerdictOf(error) {
1204
+ const seen = /* @__PURE__ */ new Set();
1205
+ let sawUnauthorizedCode = false;
1206
+ let node = error;
1207
+ for (let depth = 0; depth < MAX_CAUSE_DEPTH && typeof node === "object" && node !== null && !seen.has(node); depth += 1) {
1208
+ seen.add(node);
1209
+ const record = node;
1210
+ const status = errorStatusOf(record);
1211
+ if (status === 401) return "unauthenticated";
1212
+ if (status === 403) return "forbidden";
1213
+ if (status !== void 0) return "other";
1214
+ if (record.code === ErrorCodes.AUTH_UNAUTHORIZED) sawUnauthorizedCode = true;
1215
+ node = record.cause;
1216
+ }
1217
+ return sawUnauthorizedCode ? "forbidden" : void 0;
1218
+ }
996
1219
  function storageQuotaExceededError(service, message, meta) {
997
1220
  return {
998
1221
  code: ErrorCodes.STORAGE_QUOTA_EXCEEDED,
@@ -1009,8 +1232,28 @@ function storageLimitReachedError(service, message, meta) {
1009
1232
  meta
1010
1233
  };
1011
1234
  }
1235
+ var STORAGE_FULL_MESSAGE = "TinyCloud storage is full, so this change was not saved. Reading still works. Free up space or upgrade your plan to save again.";
1236
+ var STORAGE_WRITE_TOO_LARGE_MESSAGE = "This change is larger than the TinyCloud storage you have left, so it was not saved. Reading still works. Free up space or upgrade your plan to save it.";
1237
+ function parseStorageQuotaBytes(responseText) {
1238
+ const match = responseText.match(/Used:\s*(\d+)\s*bytes,\s*Limit:\s*(\d+)\s*bytes/i);
1239
+ if (!match) return void 0;
1240
+ return {
1241
+ usedBytes: parseInt(match[1], 10),
1242
+ limitBytes: parseInt(match[2], 10)
1243
+ };
1244
+ }
1245
+ function storageRejectionError(service, status, meta, responseText) {
1246
+ const quotaMeta = { ...meta, ...parseStorageQuotaBytes(responseText) };
1247
+ return status === 402 ? storageQuotaExceededError(service, STORAGE_FULL_MESSAGE, quotaMeta) : storageLimitReachedError(service, STORAGE_WRITE_TOO_LARGE_MESSAGE, quotaMeta);
1248
+ }
1249
+ function isStorageFullError(error) {
1250
+ return error?.code === ErrorCodes.STORAGE_QUOTA_EXCEEDED || error?.code === ErrorCodes.STORAGE_LIMIT_REACHED;
1251
+ }
1012
1252
  function wrapError(service, error, defaultCode = ErrorCodes.NETWORK_ERROR) {
1013
1253
  if (error instanceof Error) {
1254
+ if (isRequestTimeoutError(error)) {
1255
+ return timeoutError(service, error.timeoutMs, error);
1256
+ }
1014
1257
  if (error.name === "AbortError") {
1015
1258
  return abortedError(service);
1016
1259
  }
@@ -1035,6 +1278,7 @@ function errorResult(error) {
1035
1278
  }
1036
1279
 
1037
1280
  // src/base/BaseService.ts
1281
+ var MAX_TIMER_DELAY_MS = 2147483647;
1038
1282
  var BaseService = class {
1039
1283
  constructor() {
1040
1284
  /**
@@ -1215,6 +1459,64 @@ var BaseService = class {
1215
1459
  }
1216
1460
  return controller.signal;
1217
1461
  }
1462
+ /**
1463
+ * Create the abort signal for one request, bounded by the request timeout.
1464
+ *
1465
+ * The signal aborts when the service signs out, the context aborts, the
1466
+ * caller's `signal` aborts, or the timeout elapses, whichever comes first.
1467
+ * A timeout aborts with a {@link RequestTimeoutError} (`TimeoutError`), which
1468
+ * `wrapError` maps to `ErrorCodes.TIMEOUT`; the other sources keep their own
1469
+ * reason (normally `AbortError`, mapped to `ErrorCodes.ABORTED`).
1470
+ *
1471
+ * The timeout is `timeoutMs` when given, otherwise `config.timeout`. Only a
1472
+ * positive, finite value (at most 2^31-1 ms) applies; `undefined`, `0`, and
1473
+ * `Infinity` mean no timeout, so a per-call `0` opts out of a configured one.
1474
+ *
1475
+ * Callers must call `dispose()` once the response body has been consumed,
1476
+ * which clears the timer and removes the listeners this adds to the
1477
+ * long-lived service and context signals.
1478
+ *
1479
+ * @param signal - The caller's abort signal for this request
1480
+ * @param timeoutMs - Per-request timeout override in milliseconds
1481
+ */
1482
+ createRequestSignal(signal, timeoutMs) {
1483
+ const controller = new AbortController();
1484
+ const detachers = [];
1485
+ let timer;
1486
+ const dispose = () => {
1487
+ if (timer !== void 0) {
1488
+ clearTimeout(timer);
1489
+ timer = void 0;
1490
+ }
1491
+ for (const detach of detachers.splice(0)) {
1492
+ detach();
1493
+ }
1494
+ };
1495
+ const abort = (reason) => {
1496
+ dispose();
1497
+ controller.abort(reason);
1498
+ };
1499
+ const requestSignal = { signal: controller.signal, dispose };
1500
+ const parents = [
1501
+ this.abortController.signal,
1502
+ this.context?.abortSignal,
1503
+ signal
1504
+ ].filter((parent) => parent !== void 0);
1505
+ for (const parent of parents) {
1506
+ if (parent.aborted) {
1507
+ abort(parent.reason);
1508
+ return requestSignal;
1509
+ }
1510
+ const onAbort = () => abort(parent.reason);
1511
+ parent.addEventListener("abort", onAbort, { once: true });
1512
+ detachers.push(() => parent.removeEventListener("abort", onAbort));
1513
+ }
1514
+ const timeout = timeoutMs ?? this._config.timeout;
1515
+ if (typeof timeout === "number" && timeout > 0 && timeout <= MAX_TIMER_DELAY_MS) {
1516
+ timer = setTimeout(() => abort(new RequestTimeoutError(timeout)), timeout);
1517
+ }
1518
+ return requestSignal;
1519
+ }
1218
1520
  /**
1219
1521
  * Wrap an operation with error handling and telemetry.
1220
1522
  *
@@ -1323,6 +1625,30 @@ var PrefixedKVService = class _PrefixedKVService {
1323
1625
  removePrefix
1324
1626
  });
1325
1627
  }
1628
+ /**
1629
+ * Read the change feed for everything under this prefix. A view with an
1630
+ * empty prefix (`""` or `"/"`) is refused: the node does not serve a
1631
+ * whole-space feed.
1632
+ */
1633
+ async changes(options) {
1634
+ if (this._prefix.replace(/\/+$/, "") === "") {
1635
+ return err(serviceError(
1636
+ ErrorCodes.INVALID_INPUT,
1637
+ "KV changes needs a non-empty prefix; this view has none",
1638
+ "kv"
1639
+ ));
1640
+ }
1641
+ const syncPrefix = `${this._prefix}/`;
1642
+ const response = await this._kv.changes({ ...options, prefix: syncPrefix });
1643
+ if (!response.ok) return response;
1644
+ return ok({
1645
+ ...response.data,
1646
+ changes: response.data.changes.map((change) => ({
1647
+ ...change,
1648
+ key: change.key.startsWith(syncPrefix) ? change.key.slice(syncPrefix.length) : change.key
1649
+ }))
1650
+ });
1651
+ }
1326
1652
  /**
1327
1653
  * Delete a key.
1328
1654
  */
@@ -1375,11 +1701,24 @@ var KVAction = {
1375
1701
  PUT: "tinycloud.kv/put",
1376
1702
  LIST: "tinycloud.kv/list",
1377
1703
  DELETE: "tinycloud.kv/del",
1378
- HEAD: "tinycloud.kv/metadata"
1704
+ HEAD: "tinycloud.kv/metadata",
1705
+ /** The change feed; never implied by `*` or `tinycloud.kv/*`. */
1706
+ SYNC: "tinycloud.kv/sync",
1707
+ /** Retention attestation; presented through `retentionGrant`, never invoked or implied. */
1708
+ RETAIN: "tinycloud.kv/retain"
1379
1709
  };
1380
1710
 
1381
1711
  // src/kv/KVService.ts
1382
1712
  var MAX_KV_BATCH_READ_ITEMS = 100;
1713
+ function isJsonObject(value) {
1714
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1715
+ }
1716
+ var REVOKED_BODY = /^(?:Invalid invocation: )?delegation-(?:(revoked): [A-Za-z0-9]+|(ancestor-revoked): ancestor=[A-Za-z0-9]+ invoked=[A-Za-z0-9]+)$/;
1717
+ function revocationCodeOf(body) {
1718
+ const match = REVOKED_BODY.exec(body.trim());
1719
+ if (match === null) return void 0;
1720
+ return match[1] !== void 0 ? ErrorCodes.AUTH_DELEGATION_REVOKED : ErrorCodes.AUTH_DELEGATION_ANCESTOR_REVOKED;
1721
+ }
1383
1722
  function encodeKvBatchPartName(path) {
1384
1723
  return encodeURIComponent(path).replace(
1385
1724
  /[!'()*]/g,
@@ -1402,47 +1741,21 @@ var KVService = class extends BaseService {
1402
1741
  get config() {
1403
1742
  return this._config;
1404
1743
  }
1405
- // Parses "Used: X bytes, Limit: Y bytes" from tinycloud-node error responses
1406
- parseQuotaInfo(errorText) {
1407
- const match = errorText.match(
1408
- /Used:\s*(\d+)\s*bytes,\s*Limit:\s*(\d+)\s*bytes/i
1409
- );
1410
- if (match) {
1411
- return {
1412
- usedBytes: parseInt(match[1], 10),
1413
- limitBytes: parseInt(match[2], 10)
1414
- };
1415
- }
1416
- return void 0;
1417
- }
1418
1744
  handleQuotaErrorResponse(response, errorText, key) {
1419
- if (response.status === 402) {
1420
- const quotaInfo = this.parseQuotaInfo(errorText);
1421
- return err(
1422
- storageQuotaExceededError(
1423
- "kv",
1424
- `Storage quota exceeded for key "${key}": ${errorText}`,
1425
- {
1426
- status: response.status,
1427
- ...quotaInfo ? { usedBytes: quotaInfo.usedBytes, limitBytes: quotaInfo.limitBytes } : {}
1428
- }
1429
- )
1430
- );
1745
+ if (response.status !== 402 && response.status !== 413) {
1746
+ return void 0;
1431
1747
  }
1432
- if (response.status === 413) {
1433
- const quotaInfo = this.parseQuotaInfo(errorText);
1434
- return err(
1435
- storageLimitReachedError(
1436
- "kv",
1437
- `Storage limit reached for key "${key}": ${errorText}`,
1438
- {
1439
- status: response.status,
1440
- ...quotaInfo ? { usedBytes: quotaInfo.usedBytes, limitBytes: quotaInfo.limitBytes } : {}
1441
- }
1442
- )
1443
- );
1748
+ if (response.status === 413 && !parseStorageQuotaBytes(errorText)) {
1749
+ return void 0;
1444
1750
  }
1445
- return void 0;
1751
+ return err(
1752
+ storageRejectionError(
1753
+ "kv",
1754
+ response.status,
1755
+ { status: response.status, statusText: response.statusText, key },
1756
+ errorText
1757
+ )
1758
+ );
1446
1759
  }
1447
1760
  /**
1448
1761
  * Classify a KV 404 by reading the response body once.
@@ -1501,7 +1814,7 @@ var KVService = class extends BaseService {
1501
1814
  * @param path - Resource path
1502
1815
  * @param action - KV action
1503
1816
  * @param body - Optional request body
1504
- * @param signal - Optional abort signal
1817
+ * @param signal - Request signal from createRequestSignal()
1505
1818
  * @returns Fetch response
1506
1819
  */
1507
1820
  async invokeOperation(path, action, body, signal, extraHeaders) {
@@ -1517,7 +1830,7 @@ var KVService = class extends BaseService {
1517
1830
  method: "POST",
1518
1831
  headers: requestHeaders,
1519
1832
  body,
1520
- signal: this.combineSignals(signal)
1833
+ signal
1521
1834
  });
1522
1835
  }
1523
1836
  /**
@@ -1694,6 +2007,7 @@ var KVService = class extends BaseService {
1694
2007
  "kv"
1695
2008
  ));
1696
2009
  }
2010
+ const request = this.createRequestSignal(options?.signal, options?.timeout);
1697
2011
  try {
1698
2012
  const session = this.context.session;
1699
2013
  const invocationHeaders = this.context.invokeAny(
@@ -1715,17 +2029,18 @@ var KVService = class extends BaseService {
1715
2029
  const response = await this.context.fetch(`${this.host}/invoke`, {
1716
2030
  method: "POST",
1717
2031
  headers,
1718
- signal: this.combineSignals(options?.signal)
2032
+ signal: request.signal
1719
2033
  });
1720
2034
  if (!response.ok) {
1721
- const errorText = await response.text();
2035
+ const errorText = response.status === 401 || response.status === 403 ? await this.readAuthorizationText(response) : await response.text();
1722
2036
  if (response.status === 401 || response.status === 403) {
1723
- const { resource, action: requiredAction } = parseAuthError(errorText);
1724
- return err(authUnauthorizedError("kv", errorText, {
1725
- status: response.status,
1726
- ...requiredAction && { requiredAction },
1727
- ...resource && { resource }
1728
- }));
2037
+ return this.authorizationFailure(
2038
+ `Failed to batch read ${keys.length} key(s)`,
2039
+ response,
2040
+ errorText,
2041
+ paths,
2042
+ action
2043
+ );
1729
2044
  }
1730
2045
  if (response.status === 413) {
1731
2046
  return err(serviceError(
@@ -1779,6 +2094,8 @@ var KVService = class extends BaseService {
1779
2094
  return ok({ results, count: results.length });
1780
2095
  } catch (error) {
1781
2096
  return err(wrapError("kv", error));
2097
+ } finally {
2098
+ request.dispose();
1782
2099
  }
1783
2100
  }
1784
2101
  /**
@@ -1814,7 +2131,7 @@ var KVService = class extends BaseService {
1814
2131
  return text;
1815
2132
  }
1816
2133
  }
1817
- async createSignedReadUrlError(response, key) {
2134
+ async createSignedReadUrlError(response, key, path) {
1818
2135
  let errorText = response.statusText;
1819
2136
  try {
1820
2137
  const text = await response.text();
@@ -1824,23 +2141,62 @@ var KVService = class extends BaseService {
1824
2141
  } catch {
1825
2142
  }
1826
2143
  if (response.status === 401 || response.status === 403) {
1827
- const { resource, action } = parseAuthError(errorText);
1828
- return err(authUnauthorizedError("kv", errorText, {
1829
- status: response.status,
1830
- ...action && { requiredAction: action },
1831
- ...resource && { resource }
1832
- }));
2144
+ return this.authorizationFailure(
2145
+ `Failed to create signed read URL for key ${JSON.stringify(key)}`,
2146
+ response,
2147
+ errorText,
2148
+ [path],
2149
+ KVAction.GET
2150
+ );
1833
2151
  }
1834
2152
  const code = response.status === 400 ? ErrorCodes.INVALID_INPUT : ErrorCodes.NETWORK_ERROR;
1835
2153
  return err(
1836
2154
  serviceError(
1837
2155
  code,
1838
- `Failed to create signed read URL for key "${key}": ${response.status} - ${errorText}`,
2156
+ `Failed to create signed read URL for key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
1839
2157
  "kv",
1840
2158
  { meta: { status: response.status, statusText: response.statusText } }
1841
2159
  )
1842
2160
  );
1843
2161
  }
2162
+ /** A known 401/403 keeps its typed status even if reading its body fails. */
2163
+ async readAuthorizationText(response) {
2164
+ try {
2165
+ return await response.text();
2166
+ } catch {
2167
+ return "";
2168
+ }
2169
+ }
2170
+ /**
2171
+ * AUTH_UNAUTHORIZED for a 401/403 response. The message keeps the HTTP
2172
+ * status and the server text (`<context>: <status> - <text>`, the same
2173
+ * shape as every other KV failure) so callers that rethrow only the message
2174
+ * still see the status; `meta.status` carries it for typed callers.
2175
+ * Server hints must match the requested path/action (whether the node sends
2176
+ * a relative path or full resource); metadata uses only the request-derived
2177
+ * canonical resource under the authenticated session's space.
2178
+ */
2179
+ authorizationFailure(context, response, errorText, paths, expectedAction, extraMeta = {}) {
2180
+ const { resource, action } = parseAuthError(errorText);
2181
+ const requestedPath = paths.find(
2182
+ (path) => resource === path || resource === `${this.context.session.spaceId}/kv/${path}`
2183
+ );
2184
+ const capability = requestedPath !== void 0 && action === expectedAction ? validatedCapabilityOf({
2185
+ service: "kv",
2186
+ code: ErrorCodes.AUTH_UNAUTHORIZED,
2187
+ meta: {
2188
+ status: response.status,
2189
+ resource: `${this.context.session.spaceId}/kv/${requestedPath}`,
2190
+ requiredAction: action
2191
+ }
2192
+ }) : void 0;
2193
+ const detail = errorText.trim().length > 0 ? errorText : response.statusText || "authorization failed";
2194
+ return err(authUnauthorizedError("kv", `${context}: ${response.status} - ${detail}`, {
2195
+ status: response.status,
2196
+ ...extraMeta,
2197
+ ...capability
2198
+ }));
2199
+ }
1844
2200
  normalizeSignedReadUrlResponse(data) {
1845
2201
  if (!data || typeof data !== "object") {
1846
2202
  return void 0;
@@ -1872,25 +2228,36 @@ var KVService = class extends BaseService {
1872
2228
  ));
1873
2229
  }
1874
2230
  const path = this.getFullPath(key, options?.prefix);
2231
+ const request = this.createRequestSignal(options?.signal, options?.timeout);
1875
2232
  try {
1876
2233
  const response = await this.invokeOperation(
1877
2234
  path,
1878
2235
  KVAction.GET,
1879
2236
  void 0,
1880
- options?.signal,
2237
+ request.signal,
1881
2238
  options?.maxResponseBytes === void 0 ? void 0 : { "x-tinycloud-max-response-bytes": String(options.maxResponseBytes) }
1882
2239
  );
1883
2240
  if (!response.ok) {
1884
2241
  if (response.status === 401 || response.status === 403) {
1885
- const errorText2 = await response.text();
1886
- const { resource, action } = parseAuthError(errorText2);
2242
+ const errorText2 = await this.readAuthorizationText(response);
1887
2243
  const permissionHint = parsePermissionHintFromErrorText(errorText2);
1888
- return err(authUnauthorizedError("kv", errorText2, {
1889
- status: response.status,
1890
- ...action && { requiredAction: action },
1891
- ...resource && { resource },
1892
- ...permissionHint === void 0 ? {} : { permissionHint }
1893
- }));
2244
+ const structuredCapability = permissionHint?.service === "tinycloud.kv" && permissionHint.space === this.context.session.spaceId && permissionHint.path === path ? validatedCapabilityOf({
2245
+ service: "kv",
2246
+ code: ErrorCodes.AUTH_UNAUTHORIZED,
2247
+ meta: {
2248
+ status: response.status,
2249
+ resource: `${this.context.session.spaceId}/kv/${path}`,
2250
+ requiredAction: KVAction.GET
2251
+ }
2252
+ }) : void 0;
2253
+ return this.authorizationFailure(
2254
+ `Failed to get key ${JSON.stringify(key)}`,
2255
+ response,
2256
+ errorText2,
2257
+ [path],
2258
+ KVAction.GET,
2259
+ structuredCapability ? { permissionHint, ...structuredCapability } : {}
2260
+ );
1894
2261
  }
1895
2262
  if (response.status === 404) {
1896
2263
  return this.classifyNotFound(response, key);
@@ -1899,7 +2266,7 @@ var KVService = class extends BaseService {
1899
2266
  if (response.status === 413) {
1900
2267
  return err(serviceError(
1901
2268
  ErrorCodes.KV_RESPONSE_TOO_LARGE,
1902
- `KV value at key "${key}" exceeds the requested response limit`,
2269
+ `KV value at key ${JSON.stringify(key)} exceeds the requested response limit`,
1903
2270
  "kv",
1904
2271
  { meta: { status: response.status, statusText: response.statusText } }
1905
2272
  ));
@@ -1907,7 +2274,7 @@ var KVService = class extends BaseService {
1907
2274
  return err(
1908
2275
  serviceError(
1909
2276
  ErrorCodes.NETWORK_ERROR,
1910
- `Failed to get key "${key}": ${response.status} - ${errorText}`,
2277
+ `Failed to get key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
1911
2278
  "kv",
1912
2279
  { meta: { status: response.status, statusText: response.statusText } }
1913
2280
  )
@@ -1924,6 +2291,8 @@ var KVService = class extends BaseService {
1924
2291
  });
1925
2292
  } catch (error) {
1926
2293
  return err(wrapError("kv", error));
2294
+ } finally {
2295
+ request.dispose();
1927
2296
  }
1928
2297
  });
1929
2298
  }
@@ -1951,33 +2320,33 @@ var KVService = class extends BaseService {
1951
2320
  }
1952
2321
  const path = this.getFullPath(key, options?.prefix);
1953
2322
  const body = this.serializePutValue(value, options?.contentType);
2323
+ const request = this.createRequestSignal(options?.signal, options?.timeout);
1954
2324
  try {
1955
2325
  const response = await this.invokeOperation(
1956
2326
  path,
1957
2327
  KVAction.PUT,
1958
2328
  body,
1959
- options?.signal,
2329
+ request.signal,
1960
2330
  {
1961
2331
  ...options?.ifMatch === void 0 ? {} : { "if-match": options.ifMatch },
1962
2332
  ...options?.ifNoneMatch === void 0 ? {} : { "if-none-match": options.ifNoneMatch }
1963
2333
  }
1964
2334
  );
1965
2335
  if (response.status === 401 || response.status === 403) {
1966
- const errorText = await response.text();
1967
- const message = errorText.trim().length > 0 ? errorText : `Failed to put key "${key}": ${response.status} - ${response.statusText || "authorization failed"}`;
1968
- const { resource, action: requiredAction } = parseAuthError(errorText);
1969
- return err(authUnauthorizedError("kv", message, {
1970
- status: response.status,
1971
- ...requiredAction && { requiredAction },
1972
- ...resource && { resource }
1973
- }));
2336
+ return this.authorizationFailure(
2337
+ `Failed to put key ${JSON.stringify(key)}`,
2338
+ response,
2339
+ await this.readAuthorizationText(response),
2340
+ [path],
2341
+ KVAction.PUT
2342
+ );
1974
2343
  }
1975
2344
  if (!response.ok) {
1976
2345
  const errorText = await response.text();
1977
2346
  if (response.status === 412) {
1978
2347
  return err(serviceError(
1979
2348
  ErrorCodes.KV_PRECONDITION_FAILED,
1980
- `KV precondition failed for key "${key}"`,
2349
+ `KV precondition failed for key ${JSON.stringify(key)}`,
1981
2350
  "kv",
1982
2351
  { meta: { status: response.status, statusText: response.statusText } }
1983
2352
  ));
@@ -1985,7 +2354,7 @@ var KVService = class extends BaseService {
1985
2354
  if (response.status === 503 && (options?.ifMatch !== void 0 || options?.ifNoneMatch !== void 0)) {
1986
2355
  return err(serviceError(
1987
2356
  ErrorCodes.KV_CONFLICT,
1988
- `Concurrent KV update conflicted for key "${key}"`,
2357
+ `Concurrent KV update conflicted for key ${JSON.stringify(key)}`,
1989
2358
  "kv",
1990
2359
  { meta: { status: response.status, statusText: response.statusText } }
1991
2360
  ));
@@ -2001,7 +2370,7 @@ var KVService = class extends BaseService {
2001
2370
  return err(
2002
2371
  serviceError(
2003
2372
  ErrorCodes.KV_WRITE_FAILED,
2004
- `Failed to put key "${key}": ${response.status} - ${errorText}`,
2373
+ `Failed to put key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
2005
2374
  "kv",
2006
2375
  { meta: { status: response.status, statusText: response.statusText } }
2007
2376
  )
@@ -2013,6 +2382,8 @@ var KVService = class extends BaseService {
2013
2382
  });
2014
2383
  } catch (error) {
2015
2384
  return err(wrapError("kv", error));
2385
+ } finally {
2386
+ request.dispose();
2016
2387
  }
2017
2388
  });
2018
2389
  }
@@ -2052,6 +2423,7 @@ var KVService = class extends BaseService {
2052
2423
  seen.add(path);
2053
2424
  }
2054
2425
  let requestMayHaveDispatched = false;
2426
+ const request = this.createRequestSignal(options?.signal, options?.timeout);
2055
2427
  try {
2056
2428
  const body = new FormData();
2057
2429
  for (let index = 0; index < items.length; index++) {
@@ -2074,7 +2446,7 @@ var KVService = class extends BaseService {
2074
2446
  method: "POST",
2075
2447
  headers,
2076
2448
  body,
2077
- signal: this.combineSignals(options?.signal)
2449
+ signal: request.signal
2078
2450
  };
2079
2451
  const fetchFn = this.context.fetch;
2080
2452
  requestMayHaveDispatched = true;
@@ -2095,12 +2467,13 @@ var KVService = class extends BaseService {
2095
2467
  );
2096
2468
  }
2097
2469
  if (response.status === 401 || response.status === 403) {
2098
- const { resource, action } = parseAuthError(errorText);
2099
- return err(authUnauthorizedError("kv", errorText, {
2100
- status: response.status,
2101
- ...action && { requiredAction: action },
2102
- ...resource && { resource }
2103
- }));
2470
+ return this.authorizationFailure(
2471
+ `Failed to batch put ${items.length} key(s)`,
2472
+ response,
2473
+ errorText,
2474
+ paths,
2475
+ KVAction.PUT
2476
+ );
2104
2477
  }
2105
2478
  const quotaError = this.handleQuotaErrorResponse(
2106
2479
  response,
@@ -2124,14 +2497,16 @@ var KVService = class extends BaseService {
2124
2497
  rawBody = await response.json();
2125
2498
  } catch (jsonError) {
2126
2499
  const cause = jsonError instanceof Error ? jsonError : new Error(String(jsonError));
2500
+ const timedOut = isRequestTimeoutError(cause);
2127
2501
  return err(
2128
2502
  serviceError(
2129
- ErrorCodes.NETWORK_ERROR,
2130
- `KV batchPut response was not valid JSON: ${cause.message}`,
2503
+ timedOut ? ErrorCodes.TIMEOUT : ErrorCodes.NETWORK_ERROR,
2504
+ timedOut ? `KV batchPut response body did not arrive in time: ${cause.message}` : `KV batchPut response was not valid JSON: ${cause.message}`,
2131
2505
  "kv",
2132
2506
  {
2133
2507
  cause,
2134
2508
  meta: {
2509
+ ...timedOut ? { timeoutMs: cause.timeoutMs } : {},
2135
2510
  requestMayHaveDispatched: true,
2136
2511
  responseReceived: true,
2137
2512
  status: response.status,
@@ -2186,6 +2561,8 @@ var KVService = class extends BaseService {
2186
2561
  ...wrapped,
2187
2562
  meta: { ...wrapped.meta, requestMayHaveDispatched }
2188
2563
  });
2564
+ } finally {
2565
+ request.dispose();
2189
2566
  }
2190
2567
  });
2191
2568
  }
@@ -2208,26 +2585,27 @@ var KVService = class extends BaseService {
2208
2585
  if (options?.path) {
2209
2586
  listPath = listPath ? `${listPath}/${options.path}` : options.path;
2210
2587
  }
2588
+ const request = this.createRequestSignal(options?.signal, options?.timeout);
2211
2589
  try {
2212
2590
  const response = await this.invokeOperation(
2213
2591
  listPath,
2214
2592
  KVAction.LIST,
2215
2593
  void 0,
2216
- options?.signal,
2594
+ request.signal,
2217
2595
  options?.limit === void 0 && options?.cursor === void 0 ? void 0 : {
2218
2596
  ...options?.limit === void 0 ? {} : { "x-tinycloud-limit": String(options.limit) },
2219
2597
  ...options?.cursor === void 0 ? {} : { "x-tinycloud-cursor": options.cursor }
2220
2598
  }
2221
2599
  );
2222
2600
  if (!response.ok) {
2223
- if (response.status === 401) {
2224
- const errorText2 = await response.text();
2225
- const { resource, action } = parseAuthError(errorText2);
2226
- return err(authUnauthorizedError("kv", errorText2, {
2227
- status: response.status,
2228
- ...action && { requiredAction: action },
2229
- ...resource && { resource }
2230
- }));
2601
+ if (response.status === 401 || response.status === 403) {
2602
+ return this.authorizationFailure(
2603
+ "Failed to list keys",
2604
+ response,
2605
+ await this.readAuthorizationText(response),
2606
+ [listPath],
2607
+ KVAction.LIST
2608
+ );
2231
2609
  }
2232
2610
  const errorText = await response.text();
2233
2611
  return err(
@@ -2254,9 +2632,174 @@ var KVService = class extends BaseService {
2254
2632
  });
2255
2633
  } catch (error) {
2256
2634
  return err(wrapError("kv", error));
2635
+ } finally {
2636
+ request.dispose();
2637
+ }
2638
+ });
2639
+ }
2640
+ /**
2641
+ * Read one page of the `tinycloud.kv/sync` change feed for a prefix.
2642
+ */
2643
+ async changes(options) {
2644
+ return this.withTelemetry("changes", options?.prefix, async () => {
2645
+ if (!this.requireAuth()) {
2646
+ return err(authRequiredError("kv"));
2647
+ }
2648
+ if (typeof options?.prefix !== "string" || options.prefix.length === 0) {
2649
+ return err(serviceError(
2650
+ ErrorCodes.INVALID_INPUT,
2651
+ "KV changes prefix must be a non-empty string",
2652
+ "kv"
2653
+ ));
2654
+ }
2655
+ if (options.limit !== void 0 && (!Number.isInteger(options.limit) || options.limit < 1 || options.limit > 1e3)) {
2656
+ return err(serviceError(
2657
+ ErrorCodes.INVALID_INPUT,
2658
+ "KV changes limit must be an integer from 1 through 1000",
2659
+ "kv"
2660
+ ));
2661
+ }
2662
+ const headers = {};
2663
+ if (options.limit !== void 0) headers["x-tinycloud-limit"] = String(options.limit);
2664
+ if (options.cursor !== void 0) headers["x-tinycloud-cursor"] = options.cursor;
2665
+ if (options.retentionGrant !== void 0) {
2666
+ headers["x-tinycloud-retention-grant"] = options.retentionGrant;
2667
+ }
2668
+ const request = this.createRequestSignal(options.signal, options.timeout);
2669
+ try {
2670
+ const response = await this.invokeOperation(
2671
+ options.prefix,
2672
+ KVAction.SYNC,
2673
+ void 0,
2674
+ request.signal,
2675
+ headers
2676
+ );
2677
+ if (!response.ok) {
2678
+ return await this.changesFailure(response, options.prefix, request.signal);
2679
+ }
2680
+ const text = await response.text();
2681
+ let body;
2682
+ try {
2683
+ body = JSON.parse(text);
2684
+ } catch {
2685
+ body = void 0;
2686
+ }
2687
+ const page = this.normalizeChangesResponse(body);
2688
+ if (!page) {
2689
+ return err(serviceError(
2690
+ ErrorCodes.NETWORK_ERROR,
2691
+ "KV changes response was not a tinycloud.kv/sync page",
2692
+ "kv",
2693
+ { meta: { status: response.status, statusText: response.statusText } }
2694
+ ));
2695
+ }
2696
+ return ok(page);
2697
+ } catch (error) {
2698
+ return err(wrapError("kv", error));
2699
+ } finally {
2700
+ request.dispose();
2257
2701
  }
2258
2702
  });
2259
2703
  }
2704
+ /**
2705
+ * Map a failed `kv/sync` response. 410 and the retention 403 carry
2706
+ * `{"error":{"code","reason"}}`; a revoked grant is a 401 whose whole body
2707
+ * is the node's revocation error (see REVOKED_BODY). A body read that
2708
+ * fails because the request was cancelled or timed out rethrows, so the
2709
+ * caller sees `ABORTED`/`TIMEOUT` rather than a mapped status.
2710
+ */
2711
+ async changesFailure(response, prefix, signal) {
2712
+ if (response.status === 404) {
2713
+ return this.classifyNotFound(response, prefix);
2714
+ }
2715
+ let errorText;
2716
+ try {
2717
+ errorText = await response.text();
2718
+ } catch (error) {
2719
+ if (signal.aborted) throw error;
2720
+ errorText = "";
2721
+ }
2722
+ const meta = { status: response.status, statusText: response.statusText };
2723
+ let reason;
2724
+ let bodyCode;
2725
+ try {
2726
+ const body = JSON.parse(errorText);
2727
+ const error = isJsonObject(body) ? body.error : void 0;
2728
+ if (isJsonObject(error)) {
2729
+ if (typeof error.code === "string") bodyCode = error.code;
2730
+ if (typeof error.reason === "string") reason = error.reason;
2731
+ }
2732
+ } catch {
2733
+ }
2734
+ const context = `Failed to read KV changes for ${JSON.stringify(prefix)}`;
2735
+ if (response.status === 410) {
2736
+ return err(serviceError(
2737
+ ErrorCodes.KV_SYNC_RESET_REQUIRED,
2738
+ `${context}: the node requires a reset (${reason ?? "reason not given"}); discard sync state and restart without a cursor`,
2739
+ "kv",
2740
+ { meta: { ...meta, reason } }
2741
+ ));
2742
+ }
2743
+ if (response.status === 403 && bodyCode === "RETENTION_GRANT_REFUSED") {
2744
+ return err(serviceError(
2745
+ ErrorCodes.KV_RETENTION_GRANT_REFUSED,
2746
+ `${context}: the retention grant was refused (${reason ?? "reason not given"})`,
2747
+ "kv",
2748
+ { meta: { ...meta, reason } }
2749
+ ));
2750
+ }
2751
+ if (response.status === 401) {
2752
+ const revoked = revocationCodeOf(errorText);
2753
+ if (revoked !== void 0) {
2754
+ return err(serviceError(revoked, `${context}: 401 - ${errorText}`, "kv", { meta }));
2755
+ }
2756
+ }
2757
+ if (response.status === 401 || response.status === 403) {
2758
+ return this.authorizationFailure(context, response, errorText, [prefix], KVAction.SYNC);
2759
+ }
2760
+ return err(serviceError(
2761
+ response.status === 400 ? ErrorCodes.INVALID_INPUT : ErrorCodes.NETWORK_ERROR,
2762
+ `${context}: ${response.status} - ${errorText}`,
2763
+ "kv",
2764
+ { meta }
2765
+ ));
2766
+ }
2767
+ normalizeChangesResponse(data) {
2768
+ if (!isJsonObject(data)) return void 0;
2769
+ const { changes, more, cursor, source, authority } = data;
2770
+ 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)) {
2771
+ return void 0;
2772
+ }
2773
+ const { notBefore, expiresAt, retainUntil } = authority;
2774
+ if (notBefore !== null && typeof notBefore !== "string" || expiresAt !== null && typeof expiresAt !== "string" || retainUntil !== null && typeof retainUntil !== "string") {
2775
+ return void 0;
2776
+ }
2777
+ const parsed = [];
2778
+ for (const change of changes) {
2779
+ if (!isJsonObject(change) || typeof change.key !== "string") return void 0;
2780
+ if (change.deleted === true) {
2781
+ parsed.push({ key: change.key, deleted: true });
2782
+ continue;
2783
+ }
2784
+ const { metadata } = change;
2785
+ if (change.deleted !== false || typeof change.etag !== "string" || !isJsonObject(metadata) || !Object.values(metadata).every((value) => typeof value === "string")) {
2786
+ return void 0;
2787
+ }
2788
+ parsed.push({
2789
+ key: change.key,
2790
+ deleted: false,
2791
+ etag: change.etag,
2792
+ metadata
2793
+ });
2794
+ }
2795
+ return {
2796
+ changes: parsed,
2797
+ more,
2798
+ cursor,
2799
+ source: { nodeDid: source.nodeDid, space: source.space, prefix: source.prefix },
2800
+ authority: { notBefore, expiresAt, retainUntil }
2801
+ };
2802
+ }
2260
2803
  /**
2261
2804
  * Delete a key.
2262
2805
  */
@@ -2266,23 +2809,24 @@ var KVService = class extends BaseService {
2266
2809
  return err(authRequiredError("kv"));
2267
2810
  }
2268
2811
  const path = this.getFullPath(key, options?.prefix);
2812
+ const request = this.createRequestSignal(options?.signal, options?.timeout);
2269
2813
  try {
2270
2814
  const response = await this.invokeOperation(
2271
2815
  path,
2272
2816
  KVAction.DELETE,
2273
2817
  void 0,
2274
- options?.signal,
2818
+ request.signal,
2275
2819
  options?.ifMatch === void 0 ? void 0 : { "if-match": options.ifMatch }
2276
2820
  );
2277
2821
  if (!response.ok) {
2278
- if (response.status === 401) {
2279
- const errorText2 = await response.text();
2280
- const { resource, action } = parseAuthError(errorText2);
2281
- return err(authUnauthorizedError("kv", errorText2, {
2282
- status: response.status,
2283
- ...action && { requiredAction: action },
2284
- ...resource && { resource }
2285
- }));
2822
+ if (response.status === 401 || response.status === 403) {
2823
+ return this.authorizationFailure(
2824
+ `Failed to delete key ${JSON.stringify(key)}`,
2825
+ response,
2826
+ await this.readAuthorizationText(response),
2827
+ [path],
2828
+ KVAction.DELETE
2829
+ );
2286
2830
  }
2287
2831
  if (response.status === 404) {
2288
2832
  return this.classifyNotFound(response, key);
@@ -2291,7 +2835,7 @@ var KVService = class extends BaseService {
2291
2835
  if (response.status === 412) {
2292
2836
  return err(serviceError(
2293
2837
  ErrorCodes.KV_PRECONDITION_FAILED,
2294
- `KV precondition failed for key "${key}"`,
2838
+ `KV precondition failed for key ${JSON.stringify(key)}`,
2295
2839
  "kv",
2296
2840
  { meta: { status: response.status, statusText: response.statusText } }
2297
2841
  ));
@@ -2299,7 +2843,7 @@ var KVService = class extends BaseService {
2299
2843
  if (response.status === 503 && options?.ifMatch !== void 0) {
2300
2844
  return err(serviceError(
2301
2845
  ErrorCodes.KV_CONFLICT,
2302
- `Concurrent KV delete conflicted for key "${key}"`,
2846
+ `Concurrent KV delete conflicted for key ${JSON.stringify(key)}`,
2303
2847
  "kv",
2304
2848
  { meta: { status: response.status, statusText: response.statusText } }
2305
2849
  ));
@@ -2307,7 +2851,7 @@ var KVService = class extends BaseService {
2307
2851
  return err(
2308
2852
  serviceError(
2309
2853
  ErrorCodes.NETWORK_ERROR,
2310
- `Failed to delete key "${key}": ${response.status} - ${errorText}`,
2854
+ `Failed to delete key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
2311
2855
  "kv",
2312
2856
  { meta: { status: response.status, statusText: response.statusText } }
2313
2857
  )
@@ -2319,6 +2863,8 @@ var KVService = class extends BaseService {
2319
2863
  });
2320
2864
  } catch (error) {
2321
2865
  return err(wrapError("kv", error));
2866
+ } finally {
2867
+ request.dispose();
2322
2868
  }
2323
2869
  });
2324
2870
  }
@@ -2331,22 +2877,23 @@ var KVService = class extends BaseService {
2331
2877
  return err(authRequiredError("kv"));
2332
2878
  }
2333
2879
  const path = this.getFullPath(key, options?.prefix);
2880
+ const request = this.createRequestSignal(options?.signal, options?.timeout);
2334
2881
  try {
2335
2882
  const response = await this.invokeOperation(
2336
2883
  path,
2337
2884
  KVAction.HEAD,
2338
2885
  void 0,
2339
- options?.signal
2886
+ request.signal
2340
2887
  );
2341
2888
  if (!response.ok) {
2342
- if (response.status === 401) {
2343
- const errorText2 = await response.text();
2344
- const { resource, action } = parseAuthError(errorText2);
2345
- return err(authUnauthorizedError("kv", errorText2, {
2346
- status: response.status,
2347
- ...action && { requiredAction: action },
2348
- ...resource && { resource }
2349
- }));
2889
+ if (response.status === 401 || response.status === 403) {
2890
+ return this.authorizationFailure(
2891
+ `Failed to get metadata for key ${JSON.stringify(key)}`,
2892
+ response,
2893
+ await this.readAuthorizationText(response),
2894
+ [path],
2895
+ KVAction.HEAD
2896
+ );
2350
2897
  }
2351
2898
  if (response.status === 404) {
2352
2899
  return this.classifyNotFound(response, key);
@@ -2355,7 +2902,7 @@ var KVService = class extends BaseService {
2355
2902
  return err(
2356
2903
  serviceError(
2357
2904
  ErrorCodes.NETWORK_ERROR,
2358
- `Failed to get metadata for key "${key}": ${response.status} - ${errorText}`,
2905
+ `Failed to get metadata for key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
2359
2906
  "kv",
2360
2907
  { meta: { status: response.status, statusText: response.statusText } }
2361
2908
  )
@@ -2367,6 +2914,8 @@ var KVService = class extends BaseService {
2367
2914
  });
2368
2915
  } catch (error) {
2369
2916
  return err(wrapError("kv", error));
2917
+ } finally {
2918
+ request.dispose();
2370
2919
  }
2371
2920
  });
2372
2921
  }
@@ -2404,15 +2953,16 @@ var KVService = class extends BaseService {
2404
2953
  if (options?.etag !== void 0) {
2405
2954
  body.etag = options.etag;
2406
2955
  }
2956
+ const request = this.createRequestSignal(options?.signal, options?.timeout);
2407
2957
  try {
2408
2958
  const response = await this.context.fetch(`${this.host}/signed/kv`, {
2409
2959
  method: "POST",
2410
2960
  headers: this.withJsonContentType(headers),
2411
2961
  body: JSON.stringify(body),
2412
- signal: this.combineSignals(options?.signal)
2962
+ signal: request.signal
2413
2963
  });
2414
2964
  if (!response.ok) {
2415
- return this.createSignedReadUrlError(response, key);
2965
+ return this.createSignedReadUrlError(response, key, path);
2416
2966
  }
2417
2967
  const signedUrl = this.normalizeSignedReadUrlResponse(
2418
2968
  await response.json()
@@ -2429,6 +2979,8 @@ var KVService = class extends BaseService {
2429
2979
  return ok(signedUrl);
2430
2980
  } catch (error) {
2431
2981
  return err(wrapError("kv", error));
2982
+ } finally {
2983
+ request.dispose();
2432
2984
  }
2433
2985
  });
2434
2986
  }
@@ -2642,6 +3194,7 @@ var SQLService = class extends BaseService {
2642
3194
  if (!this.requireAuth()) {
2643
3195
  return err(authRequiredError("sql"));
2644
3196
  }
3197
+ const request = this.createRequestSignal(options?.signal);
2645
3198
  try {
2646
3199
  const body = {
2647
3200
  action: "query",
@@ -2650,19 +3203,22 @@ var SQLService = class extends BaseService {
2650
3203
  };
2651
3204
  if (options?.maxRows !== void 0) body.maxRows = options.maxRows;
2652
3205
  if (options?.maxBytes !== void 0) body.maxBytes = options.maxBytes;
3206
+ const action = this.actionForSql(sql, SQLAction.READ);
2653
3207
  const response = await this.invokeSQL(
2654
3208
  dbName,
2655
- this.actionForSql(sql, SQLAction.READ),
3209
+ action,
2656
3210
  body,
2657
- options?.signal
3211
+ request.signal
2658
3212
  );
2659
3213
  if (!response.ok) {
2660
- return this.handleErrorResponse(response, "query");
3214
+ return this.handleErrorResponse(response, "query", dbName, [action]);
2661
3215
  }
2662
3216
  const data = await response.json();
2663
3217
  return ok(data);
2664
3218
  } catch (error) {
2665
3219
  return err(wrapError("sql", error));
3220
+ } finally {
3221
+ request.dispose();
2666
3222
  }
2667
3223
  });
2668
3224
  }
@@ -2671,6 +3227,7 @@ var SQLService = class extends BaseService {
2671
3227
  if (!this.requireAuth()) {
2672
3228
  return err(authRequiredError("sql"));
2673
3229
  }
3230
+ const request = this.createRequestSignal(options?.signal);
2674
3231
  try {
2675
3232
  const body = {
2676
3233
  action: "execute",
@@ -2686,19 +3243,22 @@ var SQLService = class extends BaseService {
2686
3243
  (statement) => this.actionForSql(statement, SQLAction.SCHEMA)
2687
3244
  )
2688
3245
  ];
3246
+ const requestedActions = this.dedupeActions(actions);
2689
3247
  const response = await this.invokeSQL(
2690
3248
  dbName,
2691
- this.dedupeActions(actions),
3249
+ requestedActions,
2692
3250
  body,
2693
- options?.signal
3251
+ request.signal
2694
3252
  );
2695
3253
  if (!response.ok) {
2696
- return this.handleErrorResponse(response, "execute");
3254
+ return this.handleErrorResponse(response, "execute", dbName, requestedActions);
2697
3255
  }
2698
3256
  const data = await response.json();
2699
3257
  return ok(data);
2700
3258
  } catch (error) {
2701
3259
  return err(wrapError("sql", error));
3260
+ } finally {
3261
+ request.dispose();
2702
3262
  }
2703
3263
  });
2704
3264
  }
@@ -2707,20 +3267,24 @@ var SQLService = class extends BaseService {
2707
3267
  if (!this.requireAuth()) {
2708
3268
  return err(authRequiredError("sql"));
2709
3269
  }
3270
+ const request = this.createRequestSignal(options?.signal);
2710
3271
  try {
3272
+ const requestedActions = this.actionsForSqlBatch(statements);
2711
3273
  const response = await this.invokeSQL(
2712
3274
  dbName,
2713
- this.actionsForSqlBatch(statements),
3275
+ requestedActions,
2714
3276
  { action: "batch", statements },
2715
- options?.signal
3277
+ request.signal
2716
3278
  );
2717
3279
  if (!response.ok) {
2718
- return this.handleErrorResponse(response, "batch");
3280
+ return this.handleErrorResponse(response, "batch", dbName, requestedActions);
2719
3281
  }
2720
3282
  const data = await response.json();
2721
3283
  return ok(data);
2722
3284
  } catch (error) {
2723
3285
  return err(wrapError("sql", error));
3286
+ } finally {
3287
+ request.dispose();
2724
3288
  }
2725
3289
  });
2726
3290
  }
@@ -2729,20 +3293,23 @@ var SQLService = class extends BaseService {
2729
3293
  if (!this.requireAuth()) {
2730
3294
  return err(authRequiredError("sql"));
2731
3295
  }
3296
+ const request = this.createRequestSignal(options?.signal);
2732
3297
  try {
2733
3298
  const response = await this.invokeSQL(
2734
3299
  dbName,
2735
3300
  SQLAction.WRITE,
2736
3301
  { action: "execute_statement", name, params: params ?? [] },
2737
- options?.signal
3302
+ request.signal
2738
3303
  );
2739
3304
  if (!response.ok) {
2740
- return this.handleErrorResponse(response, "executeStatement");
3305
+ return this.handleErrorResponse(response, "executeStatement", dbName, [SQLAction.WRITE]);
2741
3306
  }
2742
3307
  const data = await response.json();
2743
3308
  return ok(data);
2744
3309
  } catch (error) {
2745
3310
  return err(wrapError("sql", error));
3311
+ } finally {
3312
+ request.dispose();
2746
3313
  }
2747
3314
  });
2748
3315
  }
@@ -2751,15 +3318,16 @@ var SQLService = class extends BaseService {
2751
3318
  if (!this.requireAuth()) {
2752
3319
  return err(authRequiredError("sql"));
2753
3320
  }
3321
+ const request = this.createRequestSignal(options?.signal);
2754
3322
  try {
2755
3323
  const response = await this.invokeSQL(
2756
3324
  dbName,
2757
3325
  SQLAction.READ,
2758
3326
  { action: "export" },
2759
- options?.signal
3327
+ request.signal
2760
3328
  );
2761
3329
  if (!response.ok) {
2762
- return this.handleErrorResponse(response, "export");
3330
+ return this.handleErrorResponse(response, "export", dbName, [SQLAction.READ]);
2763
3331
  }
2764
3332
  const resp = response;
2765
3333
  if (typeof resp.blob === "function") {
@@ -2770,26 +3338,31 @@ var SQLService = class extends BaseService {
2770
3338
  return ok(text);
2771
3339
  } catch (error) {
2772
3340
  return err(wrapError("sql", error));
3341
+ } finally {
3342
+ request.dispose();
2773
3343
  }
2774
3344
  });
2775
3345
  }
2776
3346
  async applyMigrationsOnDbUnlocked(dbName, options) {
2777
3347
  return this.withTelemetry("migrations.apply", dbName, async () => {
2778
- const created = await this.ensureMigrationsTable(dbName, options.signal);
2779
- if (!created.ok) return created;
2780
3348
  const listed = await this.queryOnDb(
2781
3349
  dbName,
2782
3350
  `SELECT id FROM ${MIGRATIONS_TABLE} WHERE namespace = ? ORDER BY applied_at, id`,
2783
3351
  [options.namespace],
2784
3352
  { signal: options.signal }
2785
3353
  );
2786
- if (!listed.ok) return listed;
3354
+ const tableMissing = !listed.ok && (listed.error.code === ErrorCodes.SQL_DATABASE_NOT_FOUND || isMissingTableError(listed.error.message));
3355
+ if (!listed.ok && !tableMissing) return listed;
2787
3356
  const appliedIds = new Set(
2788
- listed.data.rows.map((row) => rowValue(row, 0)).filter((id) => typeof id === "string")
3357
+ (listed.ok ? listed.data.rows : []).map((row) => rowValue(row, 0)).filter((id) => typeof id === "string")
2789
3358
  );
2790
3359
  const skipped = options.migrations.filter((migration) => appliedIds.has(migration.id)).map((migration) => migration.id);
2791
3360
  const pending = options.migrations.filter((migration) => !appliedIds.has(migration.id));
2792
3361
  const applied = [];
3362
+ if (pending.length > 0 && tableMissing) {
3363
+ const created = await this.ensureMigrationsTable(dbName, options.signal);
3364
+ if (!created.ok) return created;
3365
+ }
2793
3366
  for (const migration of pending) {
2794
3367
  const result = await this.applyOneMigration(dbName, options.namespace, migration, options.signal);
2795
3368
  if (!result.ok) return result;
@@ -2857,7 +3430,7 @@ var SQLService = class extends BaseService {
2857
3430
  "Content-Type": "application/json"
2858
3431
  },
2859
3432
  body: JSON.stringify(body),
2860
- signal: this.combineSignals(signal)
3433
+ signal
2861
3434
  });
2862
3435
  }
2863
3436
  actionForSql(sql, fallback) {
@@ -2890,8 +3463,12 @@ var SQLService = class extends BaseService {
2890
3463
  }))
2891
3464
  );
2892
3465
  }
2893
- async handleErrorResponse(response, operation) {
3466
+ async handleErrorResponse(response, operation, dbName, requestedActions) {
2894
3467
  const errorText = await response.text();
3468
+ const meta = responseErrorMeta(response.status, response.statusText, errorText);
3469
+ if (response.status === 402) {
3470
+ return err(storageRejectionError("sql", 402, meta, errorText));
3471
+ }
2895
3472
  const errorBody = parseServiceErrorBody(errorText);
2896
3473
  const errorCode = this.mapHttpStatusToErrorCode(
2897
3474
  response.status,
@@ -2904,11 +3481,12 @@ var SQLService = class extends BaseService {
2904
3481
  errorText,
2905
3482
  errorBody
2906
3483
  );
2907
- const meta = responseErrorMeta(response.status, response.statusText, errorText);
2908
3484
  if (response.status === 401) {
2909
- const { resource, action } = parseAuthError(errorText);
2910
- if (action) meta.requiredAction = action;
2911
- if (resource) meta.resource = resource;
3485
+ const { action } = parseAuthError(errorText);
3486
+ if (action && requestedActions.includes(action)) {
3487
+ meta.requiredAction = action;
3488
+ meta.resource = `${this.context.session.spaceId}/sql/${dbName}`;
3489
+ }
2912
3490
  }
2913
3491
  return err(
2914
3492
  serviceError(errorCode, message, "sql", { meta })
@@ -3005,6 +3583,9 @@ function serializeSqlValues(values) {
3005
3583
  (value) => value instanceof Uint8Array ? Array.from(value) : value
3006
3584
  );
3007
3585
  }
3586
+ function isMissingTableError(message) {
3587
+ return /no such table/i.test(message);
3588
+ }
3008
3589
  function migrationKey(namespace, id) {
3009
3590
  return `${encodeURIComponent(namespace)}:${encodeURIComponent(id)}`;
3010
3591
  }
@@ -3113,12 +3694,13 @@ var DuckDbService = class extends BaseService {
3113
3694
  if (!this.requireAuth()) {
3114
3695
  return err(authRequiredError("duckdb"));
3115
3696
  }
3697
+ const request = this.createRequestSignal(options?.signal);
3116
3698
  try {
3117
3699
  const response = await this.invokeDuckDb(
3118
3700
  dbName,
3119
3701
  DuckDbAction.READ,
3120
3702
  { action: "query", sql, params: params ?? [] },
3121
- options?.signal
3703
+ request.signal
3122
3704
  );
3123
3705
  if (!response.ok) {
3124
3706
  return this.handleErrorResponse(response, "query");
@@ -3127,6 +3709,8 @@ var DuckDbService = class extends BaseService {
3127
3709
  return ok(data);
3128
3710
  } catch (error) {
3129
3711
  return err(wrapError("duckdb", error));
3712
+ } finally {
3713
+ request.dispose();
3130
3714
  }
3131
3715
  });
3132
3716
  }
@@ -3135,12 +3719,13 @@ var DuckDbService = class extends BaseService {
3135
3719
  if (!this.requireAuth()) {
3136
3720
  return err(authRequiredError("duckdb"));
3137
3721
  }
3722
+ const request = this.createRequestSignal(options?.signal);
3138
3723
  try {
3139
3724
  const response = await this.invokeDuckDb(
3140
3725
  dbName,
3141
3726
  DuckDbAction.READ,
3142
3727
  { action: "query", sql, params: params ?? [] },
3143
- options?.signal,
3728
+ request.signal,
3144
3729
  { Accept: "application/vnd.apache.arrow.stream" }
3145
3730
  );
3146
3731
  if (!response.ok) {
@@ -3150,6 +3735,8 @@ var DuckDbService = class extends BaseService {
3150
3735
  return ok(buffer);
3151
3736
  } catch (error) {
3152
3737
  return err(wrapError("duckdb", error));
3738
+ } finally {
3739
+ request.dispose();
3153
3740
  }
3154
3741
  });
3155
3742
  }
@@ -3158,6 +3745,7 @@ var DuckDbService = class extends BaseService {
3158
3745
  if (!this.requireAuth()) {
3159
3746
  return err(authRequiredError("duckdb"));
3160
3747
  }
3748
+ const request = this.createRequestSignal(options?.signal);
3161
3749
  try {
3162
3750
  const body = {
3163
3751
  action: "execute",
@@ -3171,7 +3759,7 @@ var DuckDbService = class extends BaseService {
3171
3759
  dbName,
3172
3760
  DuckDbAction.WRITE,
3173
3761
  body,
3174
- options?.signal
3762
+ request.signal
3175
3763
  );
3176
3764
  if (!response.ok) {
3177
3765
  return this.handleErrorResponse(response, "execute");
@@ -3180,6 +3768,8 @@ var DuckDbService = class extends BaseService {
3180
3768
  return ok(data);
3181
3769
  } catch (error) {
3182
3770
  return err(wrapError("duckdb", error));
3771
+ } finally {
3772
+ request.dispose();
3183
3773
  }
3184
3774
  });
3185
3775
  }
@@ -3188,6 +3778,7 @@ var DuckDbService = class extends BaseService {
3188
3778
  if (!this.requireAuth()) {
3189
3779
  return err(authRequiredError("duckdb"));
3190
3780
  }
3781
+ const request = this.createRequestSignal(options?.signal);
3191
3782
  try {
3192
3783
  const body = {
3193
3784
  action: "batch",
@@ -3200,7 +3791,7 @@ var DuckDbService = class extends BaseService {
3200
3791
  dbName,
3201
3792
  DuckDbAction.WRITE,
3202
3793
  body,
3203
- options?.signal
3794
+ request.signal
3204
3795
  );
3205
3796
  if (!response.ok) {
3206
3797
  return this.handleErrorResponse(response, "batch");
@@ -3209,6 +3800,8 @@ var DuckDbService = class extends BaseService {
3209
3800
  return ok(data);
3210
3801
  } catch (error) {
3211
3802
  return err(wrapError("duckdb", error));
3803
+ } finally {
3804
+ request.dispose();
3212
3805
  }
3213
3806
  });
3214
3807
  }
@@ -3217,12 +3810,13 @@ var DuckDbService = class extends BaseService {
3217
3810
  if (!this.requireAuth()) {
3218
3811
  return err(authRequiredError("duckdb"));
3219
3812
  }
3813
+ const request = this.createRequestSignal(options?.signal);
3220
3814
  try {
3221
3815
  const response = await this.invokeDuckDb(
3222
3816
  dbName,
3223
3817
  DuckDbAction.WRITE,
3224
3818
  { action: "executeStatement", name, params: params ?? [] },
3225
- options?.signal
3819
+ request.signal
3226
3820
  );
3227
3821
  if (!response.ok) {
3228
3822
  return this.handleErrorResponse(response, "executeStatement");
@@ -3231,6 +3825,8 @@ var DuckDbService = class extends BaseService {
3231
3825
  return ok(data);
3232
3826
  } catch (error) {
3233
3827
  return err(wrapError("duckdb", error));
3828
+ } finally {
3829
+ request.dispose();
3234
3830
  }
3235
3831
  });
3236
3832
  }
@@ -3239,12 +3835,13 @@ var DuckDbService = class extends BaseService {
3239
3835
  if (!this.requireAuth()) {
3240
3836
  return err(authRequiredError("duckdb"));
3241
3837
  }
3838
+ const request = this.createRequestSignal(options?.signal);
3242
3839
  try {
3243
3840
  const response = await this.invokeDuckDb(
3244
3841
  dbName,
3245
3842
  DuckDbAction.READ,
3246
3843
  { action: "describe" },
3247
- options?.signal
3844
+ request.signal
3248
3845
  );
3249
3846
  if (!response.ok) {
3250
3847
  return this.handleErrorResponse(response, "describe");
@@ -3253,6 +3850,8 @@ var DuckDbService = class extends BaseService {
3253
3850
  return ok(data);
3254
3851
  } catch (error) {
3255
3852
  return err(wrapError("duckdb", error));
3853
+ } finally {
3854
+ request.dispose();
3256
3855
  }
3257
3856
  });
3258
3857
  }
@@ -3261,12 +3860,13 @@ var DuckDbService = class extends BaseService {
3261
3860
  if (!this.requireAuth()) {
3262
3861
  return err(authRequiredError("duckdb"));
3263
3862
  }
3863
+ const request = this.createRequestSignal(options?.signal);
3264
3864
  try {
3265
3865
  const response = await this.invokeDuckDb(
3266
3866
  dbName,
3267
3867
  DuckDbAction.EXPORT,
3268
3868
  { action: "export" },
3269
- options?.signal
3869
+ request.signal
3270
3870
  );
3271
3871
  if (!response.ok) {
3272
3872
  return this.handleErrorResponse(response, "export");
@@ -3275,6 +3875,8 @@ var DuckDbService = class extends BaseService {
3275
3875
  return ok(blob);
3276
3876
  } catch (error) {
3277
3877
  return err(wrapError("duckdb", error));
3878
+ } finally {
3879
+ request.dispose();
3278
3880
  }
3279
3881
  });
3280
3882
  }
@@ -3283,6 +3885,7 @@ var DuckDbService = class extends BaseService {
3283
3885
  if (!this.requireAuth()) {
3284
3886
  return err(authRequiredError("duckdb"));
3285
3887
  }
3888
+ const request = this.createRequestSignal(options?.signal);
3286
3889
  try {
3287
3890
  const session = this.context.session;
3288
3891
  const headers = this.context.invoke(
@@ -3298,7 +3901,7 @@ var DuckDbService = class extends BaseService {
3298
3901
  "Content-Type": "application/x-duckdb"
3299
3902
  },
3300
3903
  body: new Blob([data]),
3301
- signal: this.combineSignals(options?.signal)
3904
+ signal: request.signal
3302
3905
  });
3303
3906
  if (!response.ok) {
3304
3907
  return this.handleErrorResponse(response, "import");
@@ -3306,6 +3909,8 @@ var DuckDbService = class extends BaseService {
3306
3909
  return ok(void 0);
3307
3910
  } catch (error) {
3308
3911
  return err(wrapError("duckdb", error));
3912
+ } finally {
3913
+ request.dispose();
3309
3914
  }
3310
3915
  });
3311
3916
  }
@@ -3321,11 +3926,15 @@ var DuckDbService = class extends BaseService {
3321
3926
  ...extraHeaders
3322
3927
  },
3323
3928
  body: JSON.stringify(body),
3324
- signal: this.combineSignals(signal)
3929
+ signal
3325
3930
  });
3326
3931
  }
3327
3932
  async handleErrorResponse(response, operation) {
3328
3933
  const errorText = await response.text();
3934
+ const meta = responseErrorMeta(response.status, response.statusText, errorText);
3935
+ if (response.status === 402) {
3936
+ return err(storageRejectionError("duckdb", 402, meta, errorText));
3937
+ }
3329
3938
  const errorBody = parseServiceErrorBody(errorText);
3330
3939
  const errorCode = this.mapHttpStatusToErrorCode(
3331
3940
  response.status,
@@ -3338,7 +3947,6 @@ var DuckDbService = class extends BaseService {
3338
3947
  errorText,
3339
3948
  errorBody
3340
3949
  );
3341
- const meta = responseErrorMeta(response.status, response.statusText, errorText);
3342
3950
  if (response.status === 401) {
3343
3951
  const { resource, action } = parseAuthError(errorText);
3344
3952
  if (action) meta.requiredAction = action;
@@ -3837,10 +4445,14 @@ async function responseError(service, message, response) {
3837
4445
  }
3838
4446
  } catch {
3839
4447
  }
3840
- return wrapError(
4448
+ const error = wrapError(
3841
4449
  service,
3842
4450
  new Error(`${message}: ${response.status} ${detail}`)
3843
4451
  );
4452
+ return {
4453
+ ...error,
4454
+ meta: { ...error.meta, status: response.status, statusText: response.statusText }
4455
+ };
3844
4456
  }
3845
4457
  function isAbortError(error) {
3846
4458
  return error instanceof DOMException && error.name === "AbortError";
@@ -4306,6 +4918,9 @@ function defaultVaultMessage(input) {
4306
4918
  return input.message ?? `Public key not found for ${input.did}`;
4307
4919
  case "STORAGE_ERROR":
4308
4920
  return input.message ?? input.cause.message;
4921
+ case "STORAGE_QUOTA_EXCEEDED":
4922
+ case "STORAGE_LIMIT_REACHED":
4923
+ return input.message;
4309
4924
  }
4310
4925
  }
4311
4926
  function vaultError(input) {
@@ -4316,6 +4931,15 @@ function vaultError(input) {
4316
4931
  };
4317
4932
  return { ok: false, error };
4318
4933
  }
4934
+ function kvWriteError(context, error) {
4935
+ if (error.code === ErrorCodes.STORAGE_QUOTA_EXCEEDED || error.code === ErrorCodes.STORAGE_LIMIT_REACHED) {
4936
+ return vaultError({ code: error.code, message: error.message, meta: error.meta });
4937
+ }
4938
+ return vaultError({
4939
+ code: "STORAGE_ERROR",
4940
+ cause: new Error(`${context}: ${error.message}`)
4941
+ });
4942
+ }
4319
4943
  var DataVaultService = class extends BaseService {
4320
4944
  /**
4321
4945
  * Create a new DataVaultService instance.
@@ -4598,12 +5222,7 @@ var DataVaultService = class extends BaseService {
4598
5222
  JSON.stringify(envelopeResult.data)
4599
5223
  );
4600
5224
  if (!valuePutResult.ok) {
4601
- return vaultError({
4602
- code: "STORAGE_ERROR",
4603
- cause: new Error(
4604
- `Failed to store encrypted value: ${valuePutResult.error.message}`
4605
- )
4606
- });
5225
+ return kvWriteError("Failed to store encrypted value", valuePutResult.error);
4607
5226
  }
4608
5227
  return { ok: true, data: void 0 };
4609
5228
  } catch (error) {
@@ -4821,12 +5440,7 @@ var DataVaultService = class extends BaseService {
4821
5440
  keyPayload
4822
5441
  );
4823
5442
  if (!keyPutResult.ok) {
4824
- return vaultError({
4825
- code: "STORAGE_ERROR",
4826
- cause: new Error(
4827
- `Failed to store key blob: ${keyPutResult.error.message}`
4828
- )
4829
- });
5443
+ return kvWriteError("Failed to store key blob", keyPutResult.error);
4830
5444
  }
4831
5445
  const valuePayload = JSON.stringify({
4832
5446
  data: base64Encode(encrypted),
@@ -4837,12 +5451,7 @@ var DataVaultService = class extends BaseService {
4837
5451
  valuePayload
4838
5452
  );
4839
5453
  if (!valuePutResult.ok) {
4840
- return vaultError({
4841
- code: "STORAGE_ERROR",
4842
- cause: new Error(
4843
- `Failed to store encrypted value: ${valuePutResult.error.message}`
4844
- )
4845
- });
5454
+ return kvWriteError("Failed to store encrypted value", valuePutResult.error);
4846
5455
  }
4847
5456
  return ok(void 0);
4848
5457
  } catch (error) {
@@ -5173,12 +5782,7 @@ var DataVaultService = class extends BaseService {
5173
5782
  grantPayload
5174
5783
  );
5175
5784
  if (!grantPutResult.ok) {
5176
- return vaultError({
5177
- code: "STORAGE_ERROR",
5178
- cause: new Error(
5179
- `Failed to store grant: ${grantPutResult.error.message}`
5180
- )
5181
- });
5785
+ return kvWriteError("Failed to store grant", grantPutResult.error);
5182
5786
  }
5183
5787
  return ok(void 0);
5184
5788
  } catch (error) {
@@ -5523,12 +6127,7 @@ var DataVaultService = class extends BaseService {
5523
6127
  keyPayload
5524
6128
  );
5525
6129
  if (!keyPutResult.ok) {
5526
- return vaultError({
5527
- code: "STORAGE_ERROR",
5528
- cause: new Error(
5529
- `Failed to store rotated key blob: ${keyPutResult.error.message}`
5530
- )
5531
- });
6130
+ return kvWriteError("Failed to store rotated key blob", keyPutResult.error);
5532
6131
  }
5533
6132
  const valuePayload = JSON.stringify({
5534
6133
  data: base64Encode(encrypted),
@@ -5539,12 +6138,7 @@ var DataVaultService = class extends BaseService {
5539
6138
  valuePayload
5540
6139
  );
5541
6140
  if (!valuePutResult.ok) {
5542
- return vaultError({
5543
- code: "STORAGE_ERROR",
5544
- cause: new Error(
5545
- `Failed to store re-encrypted value: ${valuePutResult.error.message}`
5546
- )
5547
- });
6141
+ return kvWriteError("Failed to store re-encrypted value", valuePutResult.error);
5548
6142
  }
5549
6143
  for (const did of remainingGrantees) {
5550
6144
  const grantResult = await this.reencrypt(key, did);
@@ -5753,6 +6347,7 @@ var SecretsService = class {
5753
6347
  }
5754
6348
  async listAll() {
5755
6349
  const entries = /* @__PURE__ */ new Map();
6350
+ const seenCursors = /* @__PURE__ */ new Set();
5756
6351
  let cursor;
5757
6352
  do {
5758
6353
  const result = await this.vault.listPage({
@@ -5775,6 +6370,14 @@ var SecretsService = class {
5775
6370
  "Secret catalog listing was truncated without a continuation cursor."
5776
6371
  );
5777
6372
  }
6373
+ if (cursor !== void 0) {
6374
+ if (seenCursors.has(cursor)) {
6375
+ return invalidSecretInput(
6376
+ "Secret catalog listing repeated a continuation cursor; the node does not support paged KV lists."
6377
+ );
6378
+ }
6379
+ seenCursors.add(cursor);
6380
+ }
5778
6381
  } while (cursor !== void 0);
5779
6382
  return {
5780
6383
  ok: true,
@@ -5895,103 +6498,6 @@ function canonicalHashHex(sha256, value) {
5895
6498
  return hexEncode2(sha256(utf8Encode(canonical)));
5896
6499
  }
5897
6500
 
5898
- // src/encryption/networkId.ts
5899
- var URN_PREFIX = "urn:tinycloud:encryption:";
5900
- var NETWORK_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
5901
- var PKH_EIP155_DID_RE = /^did:pkh:eip155:(\d+):(0x[a-fA-F0-9]{40})$/;
5902
- var NetworkIdError = class extends Error {
5903
- constructor(message) {
5904
- super(message);
5905
- this.name = "NetworkIdError";
5906
- }
5907
- };
5908
- function parseNetworkId(networkId) {
5909
- if (typeof networkId !== "string" || networkId.length === 0) {
5910
- throw new NetworkIdError("networkId must be a non-empty string");
5911
- }
5912
- if (!networkId.startsWith(URN_PREFIX)) {
5913
- throw new NetworkIdError(
5914
- `networkId must start with ${URN_PREFIX} (got ${JSON.stringify(networkId)})`
5915
- );
5916
- }
5917
- const body = networkId.slice(URN_PREFIX.length);
5918
- const lastColon = body.lastIndexOf(":");
5919
- if (lastColon <= 0 || lastColon === body.length - 1) {
5920
- throw new NetworkIdError(
5921
- `networkId missing ownerDid or name segment (got ${JSON.stringify(networkId)})`
5922
- );
5923
- }
5924
- const ownerDid = body.slice(0, lastColon);
5925
- const name = body.slice(lastColon + 1);
5926
- if (!ownerDid.startsWith("did:")) {
5927
- throw new NetworkIdError(
5928
- `networkId ownerDid must be a DID (got ${JSON.stringify(ownerDid)})`
5929
- );
5930
- }
5931
- const didParts = ownerDid.split(":");
5932
- if (didParts.length < 3 || didParts.some((p) => p.length === 0)) {
5933
- throw new NetworkIdError(
5934
- `networkId ownerDid is not a well-formed DID (got ${JSON.stringify(ownerDid)})`
5935
- );
5936
- }
5937
- if (!NETWORK_NAME_RE.test(name)) {
5938
- throw new NetworkIdError(
5939
- `networkId name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
5940
- );
5941
- }
5942
- return { networkId, ownerDid, name };
5943
- }
5944
- function buildNetworkId(ownerDid, name) {
5945
- if (typeof ownerDid !== "string" || !ownerDid.startsWith("did:")) {
5946
- throw new NetworkIdError("ownerDid must be a DID");
5947
- }
5948
- if (typeof name !== "string" || !NETWORK_NAME_RE.test(name)) {
5949
- throw new NetworkIdError(
5950
- `network name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
5951
- );
5952
- }
5953
- const networkId = `${URN_PREFIX}${ownerDid}:${name}`;
5954
- parseNetworkId(networkId);
5955
- return networkId;
5956
- }
5957
- function isNetworkId(networkId) {
5958
- if (typeof networkId !== "string") {
5959
- return false;
5960
- }
5961
- try {
5962
- parseNetworkId(networkId);
5963
- return true;
5964
- } catch {
5965
- return false;
5966
- }
5967
- }
5968
- function parsePkhOwnerDid(ownerDid) {
5969
- const match = ownerDid.match(PKH_EIP155_DID_RE);
5970
- if (!match) return null;
5971
- return {
5972
- chainId: match[1],
5973
- address: match[2].toLowerCase()
5974
- };
5975
- }
5976
- function ownerDidMatches(a, b) {
5977
- const aPkh = parsePkhOwnerDid(a);
5978
- const bPkh = parsePkhOwnerDid(b);
5979
- if (aPkh && bPkh) {
5980
- return aPkh.chainId === bPkh.chainId && aPkh.address === bPkh.address;
5981
- }
5982
- return a === b;
5983
- }
5984
- function networkDiscoveryKey(name) {
5985
- if (!NETWORK_NAME_RE.test(name)) {
5986
- throw new NetworkIdError(
5987
- `network name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
5988
- );
5989
- }
5990
- return `.well-known/encryption/network/${name}`;
5991
- }
5992
- var ENCRYPTION_NETWORK_URN_PREFIX = URN_PREFIX;
5993
- var NETWORK_NAME_PATTERN = NETWORK_NAME_RE;
5994
-
5995
6501
  // src/encryption/types.ts
5996
6502
  var DEFAULT_ENCRYPTION_ALG = "x25519-aes256gcm/v1";
5997
6503
  var ENVELOPE_VERSION = 1;
@@ -6873,6 +7379,8 @@ export {
6873
7379
  SQLAction,
6874
7380
  SQLMigrations,
6875
7381
  SQLService,
7382
+ STORAGE_FULL_MESSAGE,
7383
+ STORAGE_WRITE_TOO_LARGE_MESSAGE,
6876
7384
  SecretsService,
6877
7385
  ServiceContext,
6878
7386
  ServiceErrorEventSchema,
@@ -6891,6 +7399,7 @@ export {
6891
7399
  authExpiredError,
6892
7400
  authRequiredError,
6893
7401
  authUnauthorizedError,
7402
+ authorizationVerdictOf,
6894
7403
  base64Decode2 as base64Decode,
6895
7404
  base64Encode2 as base64Encode,
6896
7405
  buildCanonicalDecryptRequest,
@@ -6924,22 +7433,26 @@ export {
6924
7433
  hexEncode2 as hexEncode,
6925
7434
  installTinyCloudDebugGlobals,
6926
7435
  isNetworkId,
7436
+ isStorageFullError,
6927
7437
  networkDiscoveryKey,
6928
7438
  networkError,
6929
7439
  notFoundError,
6930
7440
  ok,
6931
7441
  openWrappedKey,
6932
7442
  parseAuthError,
7443
+ parseCapabilityResource,
6933
7444
  parseNetworkId,
6934
7445
  parsePermissionHint,
6935
7446
  parsePermissionHintFromErrorText,
6936
7447
  parseSecretCatalogKey,
7448
+ parseStorageQuotaBytes,
6937
7449
  permissionDeniedError,
6938
7450
  resolveSecretListPrefix,
6939
7451
  resolveSecretPath,
6940
7452
  serviceError,
6941
7453
  storageLimitReachedError,
6942
7454
  storageQuotaExceededError,
7455
+ storageRejectionError,
6943
7456
  timeoutError,
6944
7457
  tinyCloudDebugLogger,
6945
7458
  utf8Decode,
@@ -6953,6 +7466,7 @@ export {
6953
7466
  validateServiceResponseEvent,
6954
7467
  validateServiceSession,
6955
7468
  validateTelemetrySpanEvent,
7469
+ validatedCapabilityOf,
6956
7470
  verifyDecryptResponse,
6957
7471
  wrapError
6958
7472
  };