@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.cjs CHANGED
@@ -55,6 +55,8 @@ __export(src_exports, {
55
55
  SQLAction: () => SQLAction,
56
56
  SQLMigrations: () => SQLMigrations,
57
57
  SQLService: () => SQLService,
58
+ STORAGE_FULL_MESSAGE: () => STORAGE_FULL_MESSAGE,
59
+ STORAGE_WRITE_TOO_LARGE_MESSAGE: () => STORAGE_WRITE_TOO_LARGE_MESSAGE,
58
60
  SecretsService: () => SecretsService,
59
61
  ServiceContext: () => ServiceContext,
60
62
  ServiceErrorEventSchema: () => ServiceErrorEventSchema,
@@ -73,6 +75,7 @@ __export(src_exports, {
73
75
  authExpiredError: () => authExpiredError,
74
76
  authRequiredError: () => authRequiredError,
75
77
  authUnauthorizedError: () => authUnauthorizedError,
78
+ authorizationVerdictOf: () => authorizationVerdictOf,
76
79
  base64Decode: () => base64Decode2,
77
80
  base64Encode: () => base64Encode2,
78
81
  buildCanonicalDecryptRequest: () => buildCanonicalDecryptRequest,
@@ -106,22 +109,26 @@ __export(src_exports, {
106
109
  hexEncode: () => hexEncode2,
107
110
  installTinyCloudDebugGlobals: () => installTinyCloudDebugGlobals,
108
111
  isNetworkId: () => isNetworkId,
112
+ isStorageFullError: () => isStorageFullError,
109
113
  networkDiscoveryKey: () => networkDiscoveryKey,
110
114
  networkError: () => networkError,
111
115
  notFoundError: () => notFoundError,
112
116
  ok: () => ok,
113
117
  openWrappedKey: () => openWrappedKey,
114
118
  parseAuthError: () => parseAuthError,
119
+ parseCapabilityResource: () => parseCapabilityResource,
115
120
  parseNetworkId: () => parseNetworkId,
116
121
  parsePermissionHint: () => parsePermissionHint,
117
122
  parsePermissionHintFromErrorText: () => parsePermissionHintFromErrorText,
118
123
  parseSecretCatalogKey: () => parseSecretCatalogKey,
124
+ parseStorageQuotaBytes: () => parseStorageQuotaBytes,
119
125
  permissionDeniedError: () => permissionDeniedError,
120
126
  resolveSecretListPrefix: () => resolveSecretListPrefix,
121
127
  resolveSecretPath: () => resolveSecretPath,
122
128
  serviceError: () => serviceError,
123
129
  storageLimitReachedError: () => storageLimitReachedError,
124
130
  storageQuotaExceededError: () => storageQuotaExceededError,
131
+ storageRejectionError: () => storageRejectionError,
125
132
  timeoutError: () => timeoutError,
126
133
  tinyCloudDebugLogger: () => tinyCloudDebugLogger,
127
134
  utf8Decode: () => utf8Decode,
@@ -135,6 +142,7 @@ __export(src_exports, {
135
142
  validateServiceResponseEvent: () => validateServiceResponseEvent,
136
143
  validateServiceSession: () => validateServiceSession,
137
144
  validateTelemetrySpanEvent: () => validateTelemetrySpanEvent,
145
+ validatedCapabilityOf: () => validatedCapabilityOf,
138
146
  verifyDecryptResponse: () => verifyDecryptResponse,
139
147
  wrapError: () => wrapError
140
148
  });
@@ -147,6 +155,10 @@ var ErrorCodes = {
147
155
  AUTH_EXPIRED: "AUTH_EXPIRED",
148
156
  AUTH_REQUIRED: "AUTH_REQUIRED",
149
157
  AUTH_UNAUTHORIZED: "AUTH_UNAUTHORIZED",
158
+ /** HTTP 401: the delegation the invocation cites was revoked. */
159
+ AUTH_DELEGATION_REVOKED: "AUTH_DELEGATION_REVOKED",
160
+ /** HTTP 401: a delegation the cited one rests on was revoked. */
161
+ AUTH_DELEGATION_ANCESTOR_REVOKED: "AUTH_DELEGATION_ANCESTOR_REVOKED",
150
162
  NETWORK_ERROR: "NETWORK_ERROR",
151
163
  TIMEOUT: "TIMEOUT",
152
164
  ABORTED: "ABORTED",
@@ -158,6 +170,10 @@ var ErrorCodes = {
158
170
  KV_PRECONDITION_FAILED: "KV_PRECONDITION_FAILED",
159
171
  KV_CONFLICT: "KV_CONFLICT",
160
172
  KV_RESPONSE_TOO_LARGE: "KV_RESPONSE_TOO_LARGE",
173
+ /** HTTP 410 from the change feed: discard sync state and restart without a cursor; `meta.reason` says why. */
174
+ KV_SYNC_RESET_REQUIRED: "KV_SYNC_RESET_REQUIRED",
175
+ /** HTTP 403 from the change feed: the retention grant was refused; `meta.reason` says why. */
176
+ KV_RETENTION_GRANT_REFUSED: "KV_RETENTION_GRANT_REFUSED",
161
177
  // SQL-specific errors
162
178
  SQL_ERROR: "SQL_ERROR",
163
179
  SQL_PERMISSION_DENIED: "SQL_PERMISSION_DENIED",
@@ -1038,6 +1054,106 @@ var ServiceContext = class {
1038
1054
  }
1039
1055
  };
1040
1056
 
1057
+ // src/errors.ts
1058
+ var import_bootstrap = require("@tinycloud/bootstrap");
1059
+
1060
+ // src/encryption/networkId.ts
1061
+ var URN_PREFIX = "urn:tinycloud:encryption:";
1062
+ var NETWORK_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
1063
+ var PKH_EIP155_DID_RE = /^did:pkh:eip155:(\d+):(0x[a-fA-F0-9]{40})$/;
1064
+ var NetworkIdError = class extends Error {
1065
+ constructor(message) {
1066
+ super(message);
1067
+ this.name = "NetworkIdError";
1068
+ }
1069
+ };
1070
+ function parseNetworkId(networkId) {
1071
+ if (typeof networkId !== "string" || networkId.length === 0) {
1072
+ throw new NetworkIdError("networkId must be a non-empty string");
1073
+ }
1074
+ if (!networkId.startsWith(URN_PREFIX)) {
1075
+ throw new NetworkIdError(
1076
+ `networkId must start with ${URN_PREFIX} (got ${JSON.stringify(networkId)})`
1077
+ );
1078
+ }
1079
+ const body = networkId.slice(URN_PREFIX.length);
1080
+ const lastColon = body.lastIndexOf(":");
1081
+ if (lastColon <= 0 || lastColon === body.length - 1) {
1082
+ throw new NetworkIdError(
1083
+ `networkId missing ownerDid or name segment (got ${JSON.stringify(networkId)})`
1084
+ );
1085
+ }
1086
+ const ownerDid = body.slice(0, lastColon);
1087
+ const name = body.slice(lastColon + 1);
1088
+ if (!ownerDid.startsWith("did:")) {
1089
+ throw new NetworkIdError(
1090
+ `networkId ownerDid must be a DID (got ${JSON.stringify(ownerDid)})`
1091
+ );
1092
+ }
1093
+ const didParts = ownerDid.split(":");
1094
+ if (didParts.length < 3 || didParts.some((p) => p.length === 0)) {
1095
+ throw new NetworkIdError(
1096
+ `networkId ownerDid is not a well-formed DID (got ${JSON.stringify(ownerDid)})`
1097
+ );
1098
+ }
1099
+ if (!NETWORK_NAME_RE.test(name)) {
1100
+ throw new NetworkIdError(
1101
+ `networkId name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
1102
+ );
1103
+ }
1104
+ return { networkId, ownerDid, name };
1105
+ }
1106
+ function buildNetworkId(ownerDid, name) {
1107
+ if (typeof ownerDid !== "string" || !ownerDid.startsWith("did:")) {
1108
+ throw new NetworkIdError("ownerDid must be a DID");
1109
+ }
1110
+ if (typeof name !== "string" || !NETWORK_NAME_RE.test(name)) {
1111
+ throw new NetworkIdError(
1112
+ `network name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
1113
+ );
1114
+ }
1115
+ const networkId = `${URN_PREFIX}${ownerDid}:${name}`;
1116
+ parseNetworkId(networkId);
1117
+ return networkId;
1118
+ }
1119
+ function isNetworkId(networkId) {
1120
+ if (typeof networkId !== "string") {
1121
+ return false;
1122
+ }
1123
+ try {
1124
+ parseNetworkId(networkId);
1125
+ return true;
1126
+ } catch {
1127
+ return false;
1128
+ }
1129
+ }
1130
+ function parsePkhOwnerDid(ownerDid) {
1131
+ const match = ownerDid.match(PKH_EIP155_DID_RE);
1132
+ if (!match) return null;
1133
+ return {
1134
+ chainId: match[1],
1135
+ address: match[2].toLowerCase()
1136
+ };
1137
+ }
1138
+ function ownerDidMatches(a, b) {
1139
+ const aPkh = parsePkhOwnerDid(a);
1140
+ const bPkh = parsePkhOwnerDid(b);
1141
+ if (aPkh && bPkh) {
1142
+ return aPkh.chainId === bPkh.chainId && aPkh.address === bPkh.address;
1143
+ }
1144
+ return a === b;
1145
+ }
1146
+ function networkDiscoveryKey(name) {
1147
+ if (!NETWORK_NAME_RE.test(name)) {
1148
+ throw new NetworkIdError(
1149
+ `network name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
1150
+ );
1151
+ }
1152
+ return `.well-known/encryption/network/${name}`;
1153
+ }
1154
+ var ENCRYPTION_NETWORK_URN_PREFIX = URN_PREFIX;
1155
+ var NETWORK_NAME_PATTERN = NETWORK_NAME_RE;
1156
+
1041
1157
  // src/errors.ts
1042
1158
  function parsePermissionHint(value) {
1043
1159
  if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
@@ -1097,11 +1213,31 @@ function networkError(service, message, cause) {
1097
1213
  cause
1098
1214
  };
1099
1215
  }
1100
- function timeoutError(service) {
1216
+ var RequestTimeoutError = class extends Error {
1217
+ constructor(timeoutMs) {
1218
+ super(`Request timed out after ${timeoutMs}ms.`);
1219
+ this.name = "TimeoutError";
1220
+ this.timeoutMs = timeoutMs;
1221
+ }
1222
+ };
1223
+ function isRequestTimeoutError(error) {
1224
+ return error instanceof RequestTimeoutError || error instanceof Error && error.name === "TimeoutError" && typeof error.timeoutMs === "number";
1225
+ }
1226
+ function timeoutError(service, timeoutMs, cause) {
1227
+ if (timeoutMs === void 0) {
1228
+ return {
1229
+ code: ErrorCodes.TIMEOUT,
1230
+ message: "Request timed out.",
1231
+ service,
1232
+ ...cause === void 0 ? {} : { cause }
1233
+ };
1234
+ }
1101
1235
  return {
1102
1236
  code: ErrorCodes.TIMEOUT,
1103
- message: "Request timed out.",
1104
- service
1237
+ message: `Request timed out after ${timeoutMs}ms.`,
1238
+ service,
1239
+ ...cause === void 0 ? {} : { cause },
1240
+ meta: { timeoutMs }
1105
1241
  };
1106
1242
  }
1107
1243
  function abortedError(service) {
@@ -1135,6 +1271,101 @@ function parseAuthError(responseText) {
1135
1271
  function authUnauthorizedError(service, message, meta) {
1136
1272
  return serviceError(ErrorCodes.AUTH_UNAUTHORIZED, message, service, { meta });
1137
1273
  }
1274
+ var grantableActions = new Set(
1275
+ import_bootstrap.CAPABILITY_REGISTRY.map(({ urn }) => urn).filter((urn) => !urn.endsWith("/*"))
1276
+ );
1277
+ var capabilityServices = new Set(
1278
+ import_bootstrap.CAPABILITY_REGISTRY.map(({ service }) => service.slice("tinycloud.".length))
1279
+ );
1280
+ function parseCapabilityResource(resource, service) {
1281
+ if (resource.length === 0 || resource.length > 1024 || !capabilityServices.has(service) || /[\s\p{C}*#]/u.test(resource) || resource.includes("://")) return void 0;
1282
+ const segments = resource.split("/");
1283
+ if (!/^tinycloud:[A-Za-z0-9][A-Za-z0-9:._-]*$/.test(segments[0])) return void 0;
1284
+ const serviceIndex = segments.findIndex(
1285
+ (segment, index) => index > 0 && capabilityServices.has(segment)
1286
+ );
1287
+ if (serviceIndex < 1 || segments[serviceIndex] !== service) return void 0;
1288
+ if (segments.slice(1, serviceIndex).some(
1289
+ (segment) => segment.length === 0 || segment === "." || segment === ".."
1290
+ )) return void 0;
1291
+ const pathSegments = segments.slice(serviceIndex + 1);
1292
+ if (pathSegments.some((segment) => segment === "." || segment === "..")) return void 0;
1293
+ if (service !== "kv" && (pathSegments.length === 0 || pathSegments[pathSegments.length - 1] === "")) return void 0;
1294
+ return {
1295
+ space: segments.slice(0, serviceIndex).join("/"),
1296
+ path: pathSegments.join("/")
1297
+ };
1298
+ }
1299
+ function safeCapabilityResource(value, service, action) {
1300
+ if (typeof value !== "string" || value.length === 0 || value.length > 1024) return false;
1301
+ if (service === "encryption" && value.startsWith("urn:tinycloud:encryption:")) {
1302
+ if (!/^[A-Za-z0-9_][A-Za-z0-9_.:/-]*$/.test(value) || value.includes("//") || value.split("/").some((segment) => segment === "." || segment === "..")) return false;
1303
+ try {
1304
+ parseNetworkId(value);
1305
+ return true;
1306
+ } catch {
1307
+ return false;
1308
+ }
1309
+ }
1310
+ const parsed = parseCapabilityResource(value, service);
1311
+ return parsed !== void 0 && (!parsed.path.endsWith("/") || service === "kv" && action === "tinycloud.kv/list") && (parsed.path !== "" || service === "kv" && action === "tinycloud.kv/list");
1312
+ }
1313
+ function validatedCapabilityOf(error) {
1314
+ const verdict = authorizationVerdictOf(error);
1315
+ if (verdict !== "unauthenticated" && verdict !== "forbidden") return void 0;
1316
+ const seen = /* @__PURE__ */ new Set();
1317
+ let node = error;
1318
+ for (let depth = 0; depth < MAX_CAUSE_DEPTH && typeof node === "object" && node !== null && !seen.has(node); depth += 1) {
1319
+ seen.add(node);
1320
+ const record = node;
1321
+ const status = errorStatusOf(record);
1322
+ const meta = record.meta;
1323
+ if ((status === 401 || status === 403 || status === void 0 && record.code === ErrorCodes.AUTH_UNAUTHORIZED) && typeof meta === "object" && meta !== null && !Array.isArray(meta)) {
1324
+ const metadata = meta;
1325
+ const { resource, requiredAction } = metadata;
1326
+ if (typeof requiredAction === "string" && grantableActions.has(requiredAction)) {
1327
+ const actionService = requiredAction.slice("tinycloud.".length, requiredAction.indexOf("/"));
1328
+ const explicitService = record.service === void 0 ? metadata.service : record.service;
1329
+ if (safeCapabilityResource(resource, actionService, requiredAction) && (explicitService === void 0 || explicitService === actionService || explicitService === `tinycloud.${actionService}`)) {
1330
+ return { resource, requiredAction };
1331
+ }
1332
+ }
1333
+ }
1334
+ node = record.cause;
1335
+ }
1336
+ return void 0;
1337
+ }
1338
+ var MAX_CAUSE_DEPTH = 8;
1339
+ function errorStatusOf(node) {
1340
+ const meta = node.meta;
1341
+ const candidates = [
1342
+ node.status,
1343
+ node.statusCode,
1344
+ typeof meta === "object" && meta !== null ? meta.status : void 0
1345
+ ];
1346
+ for (const candidate of candidates) {
1347
+ if (typeof candidate === "number" && Number.isInteger(candidate) && candidate >= 400 && candidate <= 599) {
1348
+ return candidate;
1349
+ }
1350
+ }
1351
+ return void 0;
1352
+ }
1353
+ function authorizationVerdictOf(error) {
1354
+ const seen = /* @__PURE__ */ new Set();
1355
+ let sawUnauthorizedCode = false;
1356
+ let node = error;
1357
+ for (let depth = 0; depth < MAX_CAUSE_DEPTH && typeof node === "object" && node !== null && !seen.has(node); depth += 1) {
1358
+ seen.add(node);
1359
+ const record = node;
1360
+ const status = errorStatusOf(record);
1361
+ if (status === 401) return "unauthenticated";
1362
+ if (status === 403) return "forbidden";
1363
+ if (status !== void 0) return "other";
1364
+ if (record.code === ErrorCodes.AUTH_UNAUTHORIZED) sawUnauthorizedCode = true;
1365
+ node = record.cause;
1366
+ }
1367
+ return sawUnauthorizedCode ? "forbidden" : void 0;
1368
+ }
1138
1369
  function storageQuotaExceededError(service, message, meta) {
1139
1370
  return {
1140
1371
  code: ErrorCodes.STORAGE_QUOTA_EXCEEDED,
@@ -1151,8 +1382,28 @@ function storageLimitReachedError(service, message, meta) {
1151
1382
  meta
1152
1383
  };
1153
1384
  }
1385
+ 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.";
1386
+ 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.";
1387
+ function parseStorageQuotaBytes(responseText) {
1388
+ const match = responseText.match(/Used:\s*(\d+)\s*bytes,\s*Limit:\s*(\d+)\s*bytes/i);
1389
+ if (!match) return void 0;
1390
+ return {
1391
+ usedBytes: parseInt(match[1], 10),
1392
+ limitBytes: parseInt(match[2], 10)
1393
+ };
1394
+ }
1395
+ function storageRejectionError(service, status, meta, responseText) {
1396
+ const quotaMeta = { ...meta, ...parseStorageQuotaBytes(responseText) };
1397
+ return status === 402 ? storageQuotaExceededError(service, STORAGE_FULL_MESSAGE, quotaMeta) : storageLimitReachedError(service, STORAGE_WRITE_TOO_LARGE_MESSAGE, quotaMeta);
1398
+ }
1399
+ function isStorageFullError(error) {
1400
+ return error?.code === ErrorCodes.STORAGE_QUOTA_EXCEEDED || error?.code === ErrorCodes.STORAGE_LIMIT_REACHED;
1401
+ }
1154
1402
  function wrapError(service, error, defaultCode = ErrorCodes.NETWORK_ERROR) {
1155
1403
  if (error instanceof Error) {
1404
+ if (isRequestTimeoutError(error)) {
1405
+ return timeoutError(service, error.timeoutMs, error);
1406
+ }
1156
1407
  if (error.name === "AbortError") {
1157
1408
  return abortedError(service);
1158
1409
  }
@@ -1177,6 +1428,7 @@ function errorResult(error) {
1177
1428
  }
1178
1429
 
1179
1430
  // src/base/BaseService.ts
1431
+ var MAX_TIMER_DELAY_MS = 2147483647;
1180
1432
  var BaseService = class {
1181
1433
  constructor() {
1182
1434
  /**
@@ -1357,6 +1609,64 @@ var BaseService = class {
1357
1609
  }
1358
1610
  return controller.signal;
1359
1611
  }
1612
+ /**
1613
+ * Create the abort signal for one request, bounded by the request timeout.
1614
+ *
1615
+ * The signal aborts when the service signs out, the context aborts, the
1616
+ * caller's `signal` aborts, or the timeout elapses, whichever comes first.
1617
+ * A timeout aborts with a {@link RequestTimeoutError} (`TimeoutError`), which
1618
+ * `wrapError` maps to `ErrorCodes.TIMEOUT`; the other sources keep their own
1619
+ * reason (normally `AbortError`, mapped to `ErrorCodes.ABORTED`).
1620
+ *
1621
+ * The timeout is `timeoutMs` when given, otherwise `config.timeout`. Only a
1622
+ * positive, finite value (at most 2^31-1 ms) applies; `undefined`, `0`, and
1623
+ * `Infinity` mean no timeout, so a per-call `0` opts out of a configured one.
1624
+ *
1625
+ * Callers must call `dispose()` once the response body has been consumed,
1626
+ * which clears the timer and removes the listeners this adds to the
1627
+ * long-lived service and context signals.
1628
+ *
1629
+ * @param signal - The caller's abort signal for this request
1630
+ * @param timeoutMs - Per-request timeout override in milliseconds
1631
+ */
1632
+ createRequestSignal(signal, timeoutMs) {
1633
+ const controller = new AbortController();
1634
+ const detachers = [];
1635
+ let timer;
1636
+ const dispose = () => {
1637
+ if (timer !== void 0) {
1638
+ clearTimeout(timer);
1639
+ timer = void 0;
1640
+ }
1641
+ for (const detach of detachers.splice(0)) {
1642
+ detach();
1643
+ }
1644
+ };
1645
+ const abort = (reason) => {
1646
+ dispose();
1647
+ controller.abort(reason);
1648
+ };
1649
+ const requestSignal = { signal: controller.signal, dispose };
1650
+ const parents = [
1651
+ this.abortController.signal,
1652
+ this.context?.abortSignal,
1653
+ signal
1654
+ ].filter((parent) => parent !== void 0);
1655
+ for (const parent of parents) {
1656
+ if (parent.aborted) {
1657
+ abort(parent.reason);
1658
+ return requestSignal;
1659
+ }
1660
+ const onAbort = () => abort(parent.reason);
1661
+ parent.addEventListener("abort", onAbort, { once: true });
1662
+ detachers.push(() => parent.removeEventListener("abort", onAbort));
1663
+ }
1664
+ const timeout = timeoutMs ?? this._config.timeout;
1665
+ if (typeof timeout === "number" && timeout > 0 && timeout <= MAX_TIMER_DELAY_MS) {
1666
+ timer = setTimeout(() => abort(new RequestTimeoutError(timeout)), timeout);
1667
+ }
1668
+ return requestSignal;
1669
+ }
1360
1670
  /**
1361
1671
  * Wrap an operation with error handling and telemetry.
1362
1672
  *
@@ -1465,6 +1775,30 @@ var PrefixedKVService = class _PrefixedKVService {
1465
1775
  removePrefix
1466
1776
  });
1467
1777
  }
1778
+ /**
1779
+ * Read the change feed for everything under this prefix. A view with an
1780
+ * empty prefix (`""` or `"/"`) is refused: the node does not serve a
1781
+ * whole-space feed.
1782
+ */
1783
+ async changes(options) {
1784
+ if (this._prefix.replace(/\/+$/, "") === "") {
1785
+ return err(serviceError(
1786
+ ErrorCodes.INVALID_INPUT,
1787
+ "KV changes needs a non-empty prefix; this view has none",
1788
+ "kv"
1789
+ ));
1790
+ }
1791
+ const syncPrefix = `${this._prefix}/`;
1792
+ const response = await this._kv.changes({ ...options, prefix: syncPrefix });
1793
+ if (!response.ok) return response;
1794
+ return ok({
1795
+ ...response.data,
1796
+ changes: response.data.changes.map((change) => ({
1797
+ ...change,
1798
+ key: change.key.startsWith(syncPrefix) ? change.key.slice(syncPrefix.length) : change.key
1799
+ }))
1800
+ });
1801
+ }
1468
1802
  /**
1469
1803
  * Delete a key.
1470
1804
  */
@@ -1517,11 +1851,24 @@ var KVAction = {
1517
1851
  PUT: "tinycloud.kv/put",
1518
1852
  LIST: "tinycloud.kv/list",
1519
1853
  DELETE: "tinycloud.kv/del",
1520
- HEAD: "tinycloud.kv/metadata"
1854
+ HEAD: "tinycloud.kv/metadata",
1855
+ /** The change feed; never implied by `*` or `tinycloud.kv/*`. */
1856
+ SYNC: "tinycloud.kv/sync",
1857
+ /** Retention attestation; presented through `retentionGrant`, never invoked or implied. */
1858
+ RETAIN: "tinycloud.kv/retain"
1521
1859
  };
1522
1860
 
1523
1861
  // src/kv/KVService.ts
1524
1862
  var MAX_KV_BATCH_READ_ITEMS = 100;
1863
+ function isJsonObject(value) {
1864
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1865
+ }
1866
+ var REVOKED_BODY = /^(?:Invalid invocation: )?delegation-(?:(revoked): [A-Za-z0-9]+|(ancestor-revoked): ancestor=[A-Za-z0-9]+ invoked=[A-Za-z0-9]+)$/;
1867
+ function revocationCodeOf(body) {
1868
+ const match = REVOKED_BODY.exec(body.trim());
1869
+ if (match === null) return void 0;
1870
+ return match[1] !== void 0 ? ErrorCodes.AUTH_DELEGATION_REVOKED : ErrorCodes.AUTH_DELEGATION_ANCESTOR_REVOKED;
1871
+ }
1525
1872
  function encodeKvBatchPartName(path) {
1526
1873
  return encodeURIComponent(path).replace(
1527
1874
  /[!'()*]/g,
@@ -1544,47 +1891,21 @@ var KVService = class extends BaseService {
1544
1891
  get config() {
1545
1892
  return this._config;
1546
1893
  }
1547
- // Parses "Used: X bytes, Limit: Y bytes" from tinycloud-node error responses
1548
- parseQuotaInfo(errorText) {
1549
- const match = errorText.match(
1550
- /Used:\s*(\d+)\s*bytes,\s*Limit:\s*(\d+)\s*bytes/i
1551
- );
1552
- if (match) {
1553
- return {
1554
- usedBytes: parseInt(match[1], 10),
1555
- limitBytes: parseInt(match[2], 10)
1556
- };
1557
- }
1558
- return void 0;
1559
- }
1560
1894
  handleQuotaErrorResponse(response, errorText, key) {
1561
- if (response.status === 402) {
1562
- const quotaInfo = this.parseQuotaInfo(errorText);
1563
- return err(
1564
- storageQuotaExceededError(
1565
- "kv",
1566
- `Storage quota exceeded for key "${key}": ${errorText}`,
1567
- {
1568
- status: response.status,
1569
- ...quotaInfo ? { usedBytes: quotaInfo.usedBytes, limitBytes: quotaInfo.limitBytes } : {}
1570
- }
1571
- )
1572
- );
1895
+ if (response.status !== 402 && response.status !== 413) {
1896
+ return void 0;
1573
1897
  }
1574
- if (response.status === 413) {
1575
- const quotaInfo = this.parseQuotaInfo(errorText);
1576
- return err(
1577
- storageLimitReachedError(
1578
- "kv",
1579
- `Storage limit reached for key "${key}": ${errorText}`,
1580
- {
1581
- status: response.status,
1582
- ...quotaInfo ? { usedBytes: quotaInfo.usedBytes, limitBytes: quotaInfo.limitBytes } : {}
1583
- }
1584
- )
1585
- );
1898
+ if (response.status === 413 && !parseStorageQuotaBytes(errorText)) {
1899
+ return void 0;
1586
1900
  }
1587
- return void 0;
1901
+ return err(
1902
+ storageRejectionError(
1903
+ "kv",
1904
+ response.status,
1905
+ { status: response.status, statusText: response.statusText, key },
1906
+ errorText
1907
+ )
1908
+ );
1588
1909
  }
1589
1910
  /**
1590
1911
  * Classify a KV 404 by reading the response body once.
@@ -1643,7 +1964,7 @@ var KVService = class extends BaseService {
1643
1964
  * @param path - Resource path
1644
1965
  * @param action - KV action
1645
1966
  * @param body - Optional request body
1646
- * @param signal - Optional abort signal
1967
+ * @param signal - Request signal from createRequestSignal()
1647
1968
  * @returns Fetch response
1648
1969
  */
1649
1970
  async invokeOperation(path, action, body, signal, extraHeaders) {
@@ -1659,7 +1980,7 @@ var KVService = class extends BaseService {
1659
1980
  method: "POST",
1660
1981
  headers: requestHeaders,
1661
1982
  body,
1662
- signal: this.combineSignals(signal)
1983
+ signal
1663
1984
  });
1664
1985
  }
1665
1986
  /**
@@ -1836,6 +2157,7 @@ var KVService = class extends BaseService {
1836
2157
  "kv"
1837
2158
  ));
1838
2159
  }
2160
+ const request = this.createRequestSignal(options?.signal, options?.timeout);
1839
2161
  try {
1840
2162
  const session = this.context.session;
1841
2163
  const invocationHeaders = this.context.invokeAny(
@@ -1857,17 +2179,18 @@ var KVService = class extends BaseService {
1857
2179
  const response = await this.context.fetch(`${this.host}/invoke`, {
1858
2180
  method: "POST",
1859
2181
  headers,
1860
- signal: this.combineSignals(options?.signal)
2182
+ signal: request.signal
1861
2183
  });
1862
2184
  if (!response.ok) {
1863
- const errorText = await response.text();
2185
+ const errorText = response.status === 401 || response.status === 403 ? await this.readAuthorizationText(response) : await response.text();
1864
2186
  if (response.status === 401 || response.status === 403) {
1865
- const { resource, action: requiredAction } = parseAuthError(errorText);
1866
- return err(authUnauthorizedError("kv", errorText, {
1867
- status: response.status,
1868
- ...requiredAction && { requiredAction },
1869
- ...resource && { resource }
1870
- }));
2187
+ return this.authorizationFailure(
2188
+ `Failed to batch read ${keys.length} key(s)`,
2189
+ response,
2190
+ errorText,
2191
+ paths,
2192
+ action
2193
+ );
1871
2194
  }
1872
2195
  if (response.status === 413) {
1873
2196
  return err(serviceError(
@@ -1921,6 +2244,8 @@ var KVService = class extends BaseService {
1921
2244
  return ok({ results, count: results.length });
1922
2245
  } catch (error) {
1923
2246
  return err(wrapError("kv", error));
2247
+ } finally {
2248
+ request.dispose();
1924
2249
  }
1925
2250
  }
1926
2251
  /**
@@ -1956,7 +2281,7 @@ var KVService = class extends BaseService {
1956
2281
  return text;
1957
2282
  }
1958
2283
  }
1959
- async createSignedReadUrlError(response, key) {
2284
+ async createSignedReadUrlError(response, key, path) {
1960
2285
  let errorText = response.statusText;
1961
2286
  try {
1962
2287
  const text = await response.text();
@@ -1966,23 +2291,62 @@ var KVService = class extends BaseService {
1966
2291
  } catch {
1967
2292
  }
1968
2293
  if (response.status === 401 || response.status === 403) {
1969
- const { resource, action } = parseAuthError(errorText);
1970
- return err(authUnauthorizedError("kv", errorText, {
1971
- status: response.status,
1972
- ...action && { requiredAction: action },
1973
- ...resource && { resource }
1974
- }));
2294
+ return this.authorizationFailure(
2295
+ `Failed to create signed read URL for key ${JSON.stringify(key)}`,
2296
+ response,
2297
+ errorText,
2298
+ [path],
2299
+ KVAction.GET
2300
+ );
1975
2301
  }
1976
2302
  const code = response.status === 400 ? ErrorCodes.INVALID_INPUT : ErrorCodes.NETWORK_ERROR;
1977
2303
  return err(
1978
2304
  serviceError(
1979
2305
  code,
1980
- `Failed to create signed read URL for key "${key}": ${response.status} - ${errorText}`,
2306
+ `Failed to create signed read URL for key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
1981
2307
  "kv",
1982
2308
  { meta: { status: response.status, statusText: response.statusText } }
1983
2309
  )
1984
2310
  );
1985
2311
  }
2312
+ /** A known 401/403 keeps its typed status even if reading its body fails. */
2313
+ async readAuthorizationText(response) {
2314
+ try {
2315
+ return await response.text();
2316
+ } catch {
2317
+ return "";
2318
+ }
2319
+ }
2320
+ /**
2321
+ * AUTH_UNAUTHORIZED for a 401/403 response. The message keeps the HTTP
2322
+ * status and the server text (`<context>: <status> - <text>`, the same
2323
+ * shape as every other KV failure) so callers that rethrow only the message
2324
+ * still see the status; `meta.status` carries it for typed callers.
2325
+ * Server hints must match the requested path/action (whether the node sends
2326
+ * a relative path or full resource); metadata uses only the request-derived
2327
+ * canonical resource under the authenticated session's space.
2328
+ */
2329
+ authorizationFailure(context, response, errorText, paths, expectedAction, extraMeta = {}) {
2330
+ const { resource, action } = parseAuthError(errorText);
2331
+ const requestedPath = paths.find(
2332
+ (path) => resource === path || resource === `${this.context.session.spaceId}/kv/${path}`
2333
+ );
2334
+ const capability = requestedPath !== void 0 && action === expectedAction ? validatedCapabilityOf({
2335
+ service: "kv",
2336
+ code: ErrorCodes.AUTH_UNAUTHORIZED,
2337
+ meta: {
2338
+ status: response.status,
2339
+ resource: `${this.context.session.spaceId}/kv/${requestedPath}`,
2340
+ requiredAction: action
2341
+ }
2342
+ }) : void 0;
2343
+ const detail = errorText.trim().length > 0 ? errorText : response.statusText || "authorization failed";
2344
+ return err(authUnauthorizedError("kv", `${context}: ${response.status} - ${detail}`, {
2345
+ status: response.status,
2346
+ ...extraMeta,
2347
+ ...capability
2348
+ }));
2349
+ }
1986
2350
  normalizeSignedReadUrlResponse(data) {
1987
2351
  if (!data || typeof data !== "object") {
1988
2352
  return void 0;
@@ -2014,25 +2378,36 @@ var KVService = class extends BaseService {
2014
2378
  ));
2015
2379
  }
2016
2380
  const path = this.getFullPath(key, options?.prefix);
2381
+ const request = this.createRequestSignal(options?.signal, options?.timeout);
2017
2382
  try {
2018
2383
  const response = await this.invokeOperation(
2019
2384
  path,
2020
2385
  KVAction.GET,
2021
2386
  void 0,
2022
- options?.signal,
2387
+ request.signal,
2023
2388
  options?.maxResponseBytes === void 0 ? void 0 : { "x-tinycloud-max-response-bytes": String(options.maxResponseBytes) }
2024
2389
  );
2025
2390
  if (!response.ok) {
2026
2391
  if (response.status === 401 || response.status === 403) {
2027
- const errorText2 = await response.text();
2028
- const { resource, action } = parseAuthError(errorText2);
2392
+ const errorText2 = await this.readAuthorizationText(response);
2029
2393
  const permissionHint = parsePermissionHintFromErrorText(errorText2);
2030
- return err(authUnauthorizedError("kv", errorText2, {
2031
- status: response.status,
2032
- ...action && { requiredAction: action },
2033
- ...resource && { resource },
2034
- ...permissionHint === void 0 ? {} : { permissionHint }
2035
- }));
2394
+ const structuredCapability = permissionHint?.service === "tinycloud.kv" && permissionHint.space === this.context.session.spaceId && permissionHint.path === path ? validatedCapabilityOf({
2395
+ service: "kv",
2396
+ code: ErrorCodes.AUTH_UNAUTHORIZED,
2397
+ meta: {
2398
+ status: response.status,
2399
+ resource: `${this.context.session.spaceId}/kv/${path}`,
2400
+ requiredAction: KVAction.GET
2401
+ }
2402
+ }) : void 0;
2403
+ return this.authorizationFailure(
2404
+ `Failed to get key ${JSON.stringify(key)}`,
2405
+ response,
2406
+ errorText2,
2407
+ [path],
2408
+ KVAction.GET,
2409
+ structuredCapability ? { permissionHint, ...structuredCapability } : {}
2410
+ );
2036
2411
  }
2037
2412
  if (response.status === 404) {
2038
2413
  return this.classifyNotFound(response, key);
@@ -2041,7 +2416,7 @@ var KVService = class extends BaseService {
2041
2416
  if (response.status === 413) {
2042
2417
  return err(serviceError(
2043
2418
  ErrorCodes.KV_RESPONSE_TOO_LARGE,
2044
- `KV value at key "${key}" exceeds the requested response limit`,
2419
+ `KV value at key ${JSON.stringify(key)} exceeds the requested response limit`,
2045
2420
  "kv",
2046
2421
  { meta: { status: response.status, statusText: response.statusText } }
2047
2422
  ));
@@ -2049,7 +2424,7 @@ var KVService = class extends BaseService {
2049
2424
  return err(
2050
2425
  serviceError(
2051
2426
  ErrorCodes.NETWORK_ERROR,
2052
- `Failed to get key "${key}": ${response.status} - ${errorText}`,
2427
+ `Failed to get key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
2053
2428
  "kv",
2054
2429
  { meta: { status: response.status, statusText: response.statusText } }
2055
2430
  )
@@ -2066,6 +2441,8 @@ var KVService = class extends BaseService {
2066
2441
  });
2067
2442
  } catch (error) {
2068
2443
  return err(wrapError("kv", error));
2444
+ } finally {
2445
+ request.dispose();
2069
2446
  }
2070
2447
  });
2071
2448
  }
@@ -2093,33 +2470,33 @@ var KVService = class extends BaseService {
2093
2470
  }
2094
2471
  const path = this.getFullPath(key, options?.prefix);
2095
2472
  const body = this.serializePutValue(value, options?.contentType);
2473
+ const request = this.createRequestSignal(options?.signal, options?.timeout);
2096
2474
  try {
2097
2475
  const response = await this.invokeOperation(
2098
2476
  path,
2099
2477
  KVAction.PUT,
2100
2478
  body,
2101
- options?.signal,
2479
+ request.signal,
2102
2480
  {
2103
2481
  ...options?.ifMatch === void 0 ? {} : { "if-match": options.ifMatch },
2104
2482
  ...options?.ifNoneMatch === void 0 ? {} : { "if-none-match": options.ifNoneMatch }
2105
2483
  }
2106
2484
  );
2107
2485
  if (response.status === 401 || response.status === 403) {
2108
- const errorText = await response.text();
2109
- const message = errorText.trim().length > 0 ? errorText : `Failed to put key "${key}": ${response.status} - ${response.statusText || "authorization failed"}`;
2110
- const { resource, action: requiredAction } = parseAuthError(errorText);
2111
- return err(authUnauthorizedError("kv", message, {
2112
- status: response.status,
2113
- ...requiredAction && { requiredAction },
2114
- ...resource && { resource }
2115
- }));
2486
+ return this.authorizationFailure(
2487
+ `Failed to put key ${JSON.stringify(key)}`,
2488
+ response,
2489
+ await this.readAuthorizationText(response),
2490
+ [path],
2491
+ KVAction.PUT
2492
+ );
2116
2493
  }
2117
2494
  if (!response.ok) {
2118
2495
  const errorText = await response.text();
2119
2496
  if (response.status === 412) {
2120
2497
  return err(serviceError(
2121
2498
  ErrorCodes.KV_PRECONDITION_FAILED,
2122
- `KV precondition failed for key "${key}"`,
2499
+ `KV precondition failed for key ${JSON.stringify(key)}`,
2123
2500
  "kv",
2124
2501
  { meta: { status: response.status, statusText: response.statusText } }
2125
2502
  ));
@@ -2127,7 +2504,7 @@ var KVService = class extends BaseService {
2127
2504
  if (response.status === 503 && (options?.ifMatch !== void 0 || options?.ifNoneMatch !== void 0)) {
2128
2505
  return err(serviceError(
2129
2506
  ErrorCodes.KV_CONFLICT,
2130
- `Concurrent KV update conflicted for key "${key}"`,
2507
+ `Concurrent KV update conflicted for key ${JSON.stringify(key)}`,
2131
2508
  "kv",
2132
2509
  { meta: { status: response.status, statusText: response.statusText } }
2133
2510
  ));
@@ -2143,7 +2520,7 @@ var KVService = class extends BaseService {
2143
2520
  return err(
2144
2521
  serviceError(
2145
2522
  ErrorCodes.KV_WRITE_FAILED,
2146
- `Failed to put key "${key}": ${response.status} - ${errorText}`,
2523
+ `Failed to put key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
2147
2524
  "kv",
2148
2525
  { meta: { status: response.status, statusText: response.statusText } }
2149
2526
  )
@@ -2155,6 +2532,8 @@ var KVService = class extends BaseService {
2155
2532
  });
2156
2533
  } catch (error) {
2157
2534
  return err(wrapError("kv", error));
2535
+ } finally {
2536
+ request.dispose();
2158
2537
  }
2159
2538
  });
2160
2539
  }
@@ -2194,6 +2573,7 @@ var KVService = class extends BaseService {
2194
2573
  seen.add(path);
2195
2574
  }
2196
2575
  let requestMayHaveDispatched = false;
2576
+ const request = this.createRequestSignal(options?.signal, options?.timeout);
2197
2577
  try {
2198
2578
  const body = new FormData();
2199
2579
  for (let index = 0; index < items.length; index++) {
@@ -2216,7 +2596,7 @@ var KVService = class extends BaseService {
2216
2596
  method: "POST",
2217
2597
  headers,
2218
2598
  body,
2219
- signal: this.combineSignals(options?.signal)
2599
+ signal: request.signal
2220
2600
  };
2221
2601
  const fetchFn = this.context.fetch;
2222
2602
  requestMayHaveDispatched = true;
@@ -2237,12 +2617,13 @@ var KVService = class extends BaseService {
2237
2617
  );
2238
2618
  }
2239
2619
  if (response.status === 401 || response.status === 403) {
2240
- const { resource, action } = parseAuthError(errorText);
2241
- return err(authUnauthorizedError("kv", errorText, {
2242
- status: response.status,
2243
- ...action && { requiredAction: action },
2244
- ...resource && { resource }
2245
- }));
2620
+ return this.authorizationFailure(
2621
+ `Failed to batch put ${items.length} key(s)`,
2622
+ response,
2623
+ errorText,
2624
+ paths,
2625
+ KVAction.PUT
2626
+ );
2246
2627
  }
2247
2628
  const quotaError = this.handleQuotaErrorResponse(
2248
2629
  response,
@@ -2266,14 +2647,16 @@ var KVService = class extends BaseService {
2266
2647
  rawBody = await response.json();
2267
2648
  } catch (jsonError) {
2268
2649
  const cause = jsonError instanceof Error ? jsonError : new Error(String(jsonError));
2650
+ const timedOut = isRequestTimeoutError(cause);
2269
2651
  return err(
2270
2652
  serviceError(
2271
- ErrorCodes.NETWORK_ERROR,
2272
- `KV batchPut response was not valid JSON: ${cause.message}`,
2653
+ timedOut ? ErrorCodes.TIMEOUT : ErrorCodes.NETWORK_ERROR,
2654
+ timedOut ? `KV batchPut response body did not arrive in time: ${cause.message}` : `KV batchPut response was not valid JSON: ${cause.message}`,
2273
2655
  "kv",
2274
2656
  {
2275
2657
  cause,
2276
2658
  meta: {
2659
+ ...timedOut ? { timeoutMs: cause.timeoutMs } : {},
2277
2660
  requestMayHaveDispatched: true,
2278
2661
  responseReceived: true,
2279
2662
  status: response.status,
@@ -2328,6 +2711,8 @@ var KVService = class extends BaseService {
2328
2711
  ...wrapped,
2329
2712
  meta: { ...wrapped.meta, requestMayHaveDispatched }
2330
2713
  });
2714
+ } finally {
2715
+ request.dispose();
2331
2716
  }
2332
2717
  });
2333
2718
  }
@@ -2350,26 +2735,27 @@ var KVService = class extends BaseService {
2350
2735
  if (options?.path) {
2351
2736
  listPath = listPath ? `${listPath}/${options.path}` : options.path;
2352
2737
  }
2738
+ const request = this.createRequestSignal(options?.signal, options?.timeout);
2353
2739
  try {
2354
2740
  const response = await this.invokeOperation(
2355
2741
  listPath,
2356
2742
  KVAction.LIST,
2357
2743
  void 0,
2358
- options?.signal,
2744
+ request.signal,
2359
2745
  options?.limit === void 0 && options?.cursor === void 0 ? void 0 : {
2360
2746
  ...options?.limit === void 0 ? {} : { "x-tinycloud-limit": String(options.limit) },
2361
2747
  ...options?.cursor === void 0 ? {} : { "x-tinycloud-cursor": options.cursor }
2362
2748
  }
2363
2749
  );
2364
2750
  if (!response.ok) {
2365
- if (response.status === 401) {
2366
- const errorText2 = await response.text();
2367
- const { resource, action } = parseAuthError(errorText2);
2368
- return err(authUnauthorizedError("kv", errorText2, {
2369
- status: response.status,
2370
- ...action && { requiredAction: action },
2371
- ...resource && { resource }
2372
- }));
2751
+ if (response.status === 401 || response.status === 403) {
2752
+ return this.authorizationFailure(
2753
+ "Failed to list keys",
2754
+ response,
2755
+ await this.readAuthorizationText(response),
2756
+ [listPath],
2757
+ KVAction.LIST
2758
+ );
2373
2759
  }
2374
2760
  const errorText = await response.text();
2375
2761
  return err(
@@ -2396,9 +2782,174 @@ var KVService = class extends BaseService {
2396
2782
  });
2397
2783
  } catch (error) {
2398
2784
  return err(wrapError("kv", error));
2785
+ } finally {
2786
+ request.dispose();
2787
+ }
2788
+ });
2789
+ }
2790
+ /**
2791
+ * Read one page of the `tinycloud.kv/sync` change feed for a prefix.
2792
+ */
2793
+ async changes(options) {
2794
+ return this.withTelemetry("changes", options?.prefix, async () => {
2795
+ if (!this.requireAuth()) {
2796
+ return err(authRequiredError("kv"));
2797
+ }
2798
+ if (typeof options?.prefix !== "string" || options.prefix.length === 0) {
2799
+ return err(serviceError(
2800
+ ErrorCodes.INVALID_INPUT,
2801
+ "KV changes prefix must be a non-empty string",
2802
+ "kv"
2803
+ ));
2804
+ }
2805
+ if (options.limit !== void 0 && (!Number.isInteger(options.limit) || options.limit < 1 || options.limit > 1e3)) {
2806
+ return err(serviceError(
2807
+ ErrorCodes.INVALID_INPUT,
2808
+ "KV changes limit must be an integer from 1 through 1000",
2809
+ "kv"
2810
+ ));
2811
+ }
2812
+ const headers = {};
2813
+ if (options.limit !== void 0) headers["x-tinycloud-limit"] = String(options.limit);
2814
+ if (options.cursor !== void 0) headers["x-tinycloud-cursor"] = options.cursor;
2815
+ if (options.retentionGrant !== void 0) {
2816
+ headers["x-tinycloud-retention-grant"] = options.retentionGrant;
2817
+ }
2818
+ const request = this.createRequestSignal(options.signal, options.timeout);
2819
+ try {
2820
+ const response = await this.invokeOperation(
2821
+ options.prefix,
2822
+ KVAction.SYNC,
2823
+ void 0,
2824
+ request.signal,
2825
+ headers
2826
+ );
2827
+ if (!response.ok) {
2828
+ return await this.changesFailure(response, options.prefix, request.signal);
2829
+ }
2830
+ const text = await response.text();
2831
+ let body;
2832
+ try {
2833
+ body = JSON.parse(text);
2834
+ } catch {
2835
+ body = void 0;
2836
+ }
2837
+ const page = this.normalizeChangesResponse(body);
2838
+ if (!page) {
2839
+ return err(serviceError(
2840
+ ErrorCodes.NETWORK_ERROR,
2841
+ "KV changes response was not a tinycloud.kv/sync page",
2842
+ "kv",
2843
+ { meta: { status: response.status, statusText: response.statusText } }
2844
+ ));
2845
+ }
2846
+ return ok(page);
2847
+ } catch (error) {
2848
+ return err(wrapError("kv", error));
2849
+ } finally {
2850
+ request.dispose();
2399
2851
  }
2400
2852
  });
2401
2853
  }
2854
+ /**
2855
+ * Map a failed `kv/sync` response. 410 and the retention 403 carry
2856
+ * `{"error":{"code","reason"}}`; a revoked grant is a 401 whose whole body
2857
+ * is the node's revocation error (see REVOKED_BODY). A body read that
2858
+ * fails because the request was cancelled or timed out rethrows, so the
2859
+ * caller sees `ABORTED`/`TIMEOUT` rather than a mapped status.
2860
+ */
2861
+ async changesFailure(response, prefix, signal) {
2862
+ if (response.status === 404) {
2863
+ return this.classifyNotFound(response, prefix);
2864
+ }
2865
+ let errorText;
2866
+ try {
2867
+ errorText = await response.text();
2868
+ } catch (error) {
2869
+ if (signal.aborted) throw error;
2870
+ errorText = "";
2871
+ }
2872
+ const meta = { status: response.status, statusText: response.statusText };
2873
+ let reason;
2874
+ let bodyCode;
2875
+ try {
2876
+ const body = JSON.parse(errorText);
2877
+ const error = isJsonObject(body) ? body.error : void 0;
2878
+ if (isJsonObject(error)) {
2879
+ if (typeof error.code === "string") bodyCode = error.code;
2880
+ if (typeof error.reason === "string") reason = error.reason;
2881
+ }
2882
+ } catch {
2883
+ }
2884
+ const context = `Failed to read KV changes for ${JSON.stringify(prefix)}`;
2885
+ if (response.status === 410) {
2886
+ return err(serviceError(
2887
+ ErrorCodes.KV_SYNC_RESET_REQUIRED,
2888
+ `${context}: the node requires a reset (${reason ?? "reason not given"}); discard sync state and restart without a cursor`,
2889
+ "kv",
2890
+ { meta: { ...meta, reason } }
2891
+ ));
2892
+ }
2893
+ if (response.status === 403 && bodyCode === "RETENTION_GRANT_REFUSED") {
2894
+ return err(serviceError(
2895
+ ErrorCodes.KV_RETENTION_GRANT_REFUSED,
2896
+ `${context}: the retention grant was refused (${reason ?? "reason not given"})`,
2897
+ "kv",
2898
+ { meta: { ...meta, reason } }
2899
+ ));
2900
+ }
2901
+ if (response.status === 401) {
2902
+ const revoked = revocationCodeOf(errorText);
2903
+ if (revoked !== void 0) {
2904
+ return err(serviceError(revoked, `${context}: 401 - ${errorText}`, "kv", { meta }));
2905
+ }
2906
+ }
2907
+ if (response.status === 401 || response.status === 403) {
2908
+ return this.authorizationFailure(context, response, errorText, [prefix], KVAction.SYNC);
2909
+ }
2910
+ return err(serviceError(
2911
+ response.status === 400 ? ErrorCodes.INVALID_INPUT : ErrorCodes.NETWORK_ERROR,
2912
+ `${context}: ${response.status} - ${errorText}`,
2913
+ "kv",
2914
+ { meta }
2915
+ ));
2916
+ }
2917
+ normalizeChangesResponse(data) {
2918
+ if (!isJsonObject(data)) return void 0;
2919
+ const { changes, more, cursor, source, authority } = data;
2920
+ 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)) {
2921
+ return void 0;
2922
+ }
2923
+ const { notBefore, expiresAt, retainUntil } = authority;
2924
+ if (notBefore !== null && typeof notBefore !== "string" || expiresAt !== null && typeof expiresAt !== "string" || retainUntil !== null && typeof retainUntil !== "string") {
2925
+ return void 0;
2926
+ }
2927
+ const parsed = [];
2928
+ for (const change of changes) {
2929
+ if (!isJsonObject(change) || typeof change.key !== "string") return void 0;
2930
+ if (change.deleted === true) {
2931
+ parsed.push({ key: change.key, deleted: true });
2932
+ continue;
2933
+ }
2934
+ const { metadata } = change;
2935
+ if (change.deleted !== false || typeof change.etag !== "string" || !isJsonObject(metadata) || !Object.values(metadata).every((value) => typeof value === "string")) {
2936
+ return void 0;
2937
+ }
2938
+ parsed.push({
2939
+ key: change.key,
2940
+ deleted: false,
2941
+ etag: change.etag,
2942
+ metadata
2943
+ });
2944
+ }
2945
+ return {
2946
+ changes: parsed,
2947
+ more,
2948
+ cursor,
2949
+ source: { nodeDid: source.nodeDid, space: source.space, prefix: source.prefix },
2950
+ authority: { notBefore, expiresAt, retainUntil }
2951
+ };
2952
+ }
2402
2953
  /**
2403
2954
  * Delete a key.
2404
2955
  */
@@ -2408,23 +2959,24 @@ var KVService = class extends BaseService {
2408
2959
  return err(authRequiredError("kv"));
2409
2960
  }
2410
2961
  const path = this.getFullPath(key, options?.prefix);
2962
+ const request = this.createRequestSignal(options?.signal, options?.timeout);
2411
2963
  try {
2412
2964
  const response = await this.invokeOperation(
2413
2965
  path,
2414
2966
  KVAction.DELETE,
2415
2967
  void 0,
2416
- options?.signal,
2968
+ request.signal,
2417
2969
  options?.ifMatch === void 0 ? void 0 : { "if-match": options.ifMatch }
2418
2970
  );
2419
2971
  if (!response.ok) {
2420
- if (response.status === 401) {
2421
- const errorText2 = await response.text();
2422
- const { resource, action } = parseAuthError(errorText2);
2423
- return err(authUnauthorizedError("kv", errorText2, {
2424
- status: response.status,
2425
- ...action && { requiredAction: action },
2426
- ...resource && { resource }
2427
- }));
2972
+ if (response.status === 401 || response.status === 403) {
2973
+ return this.authorizationFailure(
2974
+ `Failed to delete key ${JSON.stringify(key)}`,
2975
+ response,
2976
+ await this.readAuthorizationText(response),
2977
+ [path],
2978
+ KVAction.DELETE
2979
+ );
2428
2980
  }
2429
2981
  if (response.status === 404) {
2430
2982
  return this.classifyNotFound(response, key);
@@ -2433,7 +2985,7 @@ var KVService = class extends BaseService {
2433
2985
  if (response.status === 412) {
2434
2986
  return err(serviceError(
2435
2987
  ErrorCodes.KV_PRECONDITION_FAILED,
2436
- `KV precondition failed for key "${key}"`,
2988
+ `KV precondition failed for key ${JSON.stringify(key)}`,
2437
2989
  "kv",
2438
2990
  { meta: { status: response.status, statusText: response.statusText } }
2439
2991
  ));
@@ -2441,7 +2993,7 @@ var KVService = class extends BaseService {
2441
2993
  if (response.status === 503 && options?.ifMatch !== void 0) {
2442
2994
  return err(serviceError(
2443
2995
  ErrorCodes.KV_CONFLICT,
2444
- `Concurrent KV delete conflicted for key "${key}"`,
2996
+ `Concurrent KV delete conflicted for key ${JSON.stringify(key)}`,
2445
2997
  "kv",
2446
2998
  { meta: { status: response.status, statusText: response.statusText } }
2447
2999
  ));
@@ -2449,7 +3001,7 @@ var KVService = class extends BaseService {
2449
3001
  return err(
2450
3002
  serviceError(
2451
3003
  ErrorCodes.NETWORK_ERROR,
2452
- `Failed to delete key "${key}": ${response.status} - ${errorText}`,
3004
+ `Failed to delete key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
2453
3005
  "kv",
2454
3006
  { meta: { status: response.status, statusText: response.statusText } }
2455
3007
  )
@@ -2461,6 +3013,8 @@ var KVService = class extends BaseService {
2461
3013
  });
2462
3014
  } catch (error) {
2463
3015
  return err(wrapError("kv", error));
3016
+ } finally {
3017
+ request.dispose();
2464
3018
  }
2465
3019
  });
2466
3020
  }
@@ -2473,22 +3027,23 @@ var KVService = class extends BaseService {
2473
3027
  return err(authRequiredError("kv"));
2474
3028
  }
2475
3029
  const path = this.getFullPath(key, options?.prefix);
3030
+ const request = this.createRequestSignal(options?.signal, options?.timeout);
2476
3031
  try {
2477
3032
  const response = await this.invokeOperation(
2478
3033
  path,
2479
3034
  KVAction.HEAD,
2480
3035
  void 0,
2481
- options?.signal
3036
+ request.signal
2482
3037
  );
2483
3038
  if (!response.ok) {
2484
- if (response.status === 401) {
2485
- const errorText2 = await response.text();
2486
- const { resource, action } = parseAuthError(errorText2);
2487
- return err(authUnauthorizedError("kv", errorText2, {
2488
- status: response.status,
2489
- ...action && { requiredAction: action },
2490
- ...resource && { resource }
2491
- }));
3039
+ if (response.status === 401 || response.status === 403) {
3040
+ return this.authorizationFailure(
3041
+ `Failed to get metadata for key ${JSON.stringify(key)}`,
3042
+ response,
3043
+ await this.readAuthorizationText(response),
3044
+ [path],
3045
+ KVAction.HEAD
3046
+ );
2492
3047
  }
2493
3048
  if (response.status === 404) {
2494
3049
  return this.classifyNotFound(response, key);
@@ -2497,7 +3052,7 @@ var KVService = class extends BaseService {
2497
3052
  return err(
2498
3053
  serviceError(
2499
3054
  ErrorCodes.NETWORK_ERROR,
2500
- `Failed to get metadata for key "${key}": ${response.status} - ${errorText}`,
3055
+ `Failed to get metadata for key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
2501
3056
  "kv",
2502
3057
  { meta: { status: response.status, statusText: response.statusText } }
2503
3058
  )
@@ -2509,6 +3064,8 @@ var KVService = class extends BaseService {
2509
3064
  });
2510
3065
  } catch (error) {
2511
3066
  return err(wrapError("kv", error));
3067
+ } finally {
3068
+ request.dispose();
2512
3069
  }
2513
3070
  });
2514
3071
  }
@@ -2546,15 +3103,16 @@ var KVService = class extends BaseService {
2546
3103
  if (options?.etag !== void 0) {
2547
3104
  body.etag = options.etag;
2548
3105
  }
3106
+ const request = this.createRequestSignal(options?.signal, options?.timeout);
2549
3107
  try {
2550
3108
  const response = await this.context.fetch(`${this.host}/signed/kv`, {
2551
3109
  method: "POST",
2552
3110
  headers: this.withJsonContentType(headers),
2553
3111
  body: JSON.stringify(body),
2554
- signal: this.combineSignals(options?.signal)
3112
+ signal: request.signal
2555
3113
  });
2556
3114
  if (!response.ok) {
2557
- return this.createSignedReadUrlError(response, key);
3115
+ return this.createSignedReadUrlError(response, key, path);
2558
3116
  }
2559
3117
  const signedUrl = this.normalizeSignedReadUrlResponse(
2560
3118
  await response.json()
@@ -2571,6 +3129,8 @@ var KVService = class extends BaseService {
2571
3129
  return ok(signedUrl);
2572
3130
  } catch (error) {
2573
3131
  return err(wrapError("kv", error));
3132
+ } finally {
3133
+ request.dispose();
2574
3134
  }
2575
3135
  });
2576
3136
  }
@@ -2697,16 +3257,16 @@ var DatabaseHandle = class {
2697
3257
  };
2698
3258
 
2699
3259
  // src/sql/types.ts
2700
- var import_bootstrap = require("@tinycloud/bootstrap");
3260
+ var import_bootstrap2 = require("@tinycloud/bootstrap");
2701
3261
  var SQLAction = {
2702
- READ: import_bootstrap.SQL.READ,
2703
- WRITE: import_bootstrap.SQL.WRITE,
2704
- SCHEMA: import_bootstrap.SQL.SCHEMA,
2705
- ADMIN: import_bootstrap.SQL.ADMIN,
2706
- SELECT: import_bootstrap.SQL.SELECT,
2707
- EXECUTE: import_bootstrap.SQL.EXECUTE,
2708
- EXPORT: import_bootstrap.SQL.EXPORT,
2709
- ALL: import_bootstrap.SQL.ALL
3262
+ READ: import_bootstrap2.SQL.READ,
3263
+ WRITE: import_bootstrap2.SQL.WRITE,
3264
+ SCHEMA: import_bootstrap2.SQL.SCHEMA,
3265
+ ADMIN: import_bootstrap2.SQL.ADMIN,
3266
+ SELECT: import_bootstrap2.SQL.SELECT,
3267
+ EXECUTE: import_bootstrap2.SQL.EXECUTE,
3268
+ EXPORT: import_bootstrap2.SQL.EXPORT,
3269
+ ALL: import_bootstrap2.SQL.ALL
2710
3270
  };
2711
3271
 
2712
3272
  // src/sql/SQLService.ts
@@ -2784,6 +3344,7 @@ var SQLService = class extends BaseService {
2784
3344
  if (!this.requireAuth()) {
2785
3345
  return err(authRequiredError("sql"));
2786
3346
  }
3347
+ const request = this.createRequestSignal(options?.signal);
2787
3348
  try {
2788
3349
  const body = {
2789
3350
  action: "query",
@@ -2792,19 +3353,22 @@ var SQLService = class extends BaseService {
2792
3353
  };
2793
3354
  if (options?.maxRows !== void 0) body.maxRows = options.maxRows;
2794
3355
  if (options?.maxBytes !== void 0) body.maxBytes = options.maxBytes;
3356
+ const action = this.actionForSql(sql, SQLAction.READ);
2795
3357
  const response = await this.invokeSQL(
2796
3358
  dbName,
2797
- this.actionForSql(sql, SQLAction.READ),
3359
+ action,
2798
3360
  body,
2799
- options?.signal
3361
+ request.signal
2800
3362
  );
2801
3363
  if (!response.ok) {
2802
- return this.handleErrorResponse(response, "query");
3364
+ return this.handleErrorResponse(response, "query", dbName, [action]);
2803
3365
  }
2804
3366
  const data = await response.json();
2805
3367
  return ok(data);
2806
3368
  } catch (error) {
2807
3369
  return err(wrapError("sql", error));
3370
+ } finally {
3371
+ request.dispose();
2808
3372
  }
2809
3373
  });
2810
3374
  }
@@ -2813,6 +3377,7 @@ var SQLService = class extends BaseService {
2813
3377
  if (!this.requireAuth()) {
2814
3378
  return err(authRequiredError("sql"));
2815
3379
  }
3380
+ const request = this.createRequestSignal(options?.signal);
2816
3381
  try {
2817
3382
  const body = {
2818
3383
  action: "execute",
@@ -2828,19 +3393,22 @@ var SQLService = class extends BaseService {
2828
3393
  (statement) => this.actionForSql(statement, SQLAction.SCHEMA)
2829
3394
  )
2830
3395
  ];
3396
+ const requestedActions = this.dedupeActions(actions);
2831
3397
  const response = await this.invokeSQL(
2832
3398
  dbName,
2833
- this.dedupeActions(actions),
3399
+ requestedActions,
2834
3400
  body,
2835
- options?.signal
3401
+ request.signal
2836
3402
  );
2837
3403
  if (!response.ok) {
2838
- return this.handleErrorResponse(response, "execute");
3404
+ return this.handleErrorResponse(response, "execute", dbName, requestedActions);
2839
3405
  }
2840
3406
  const data = await response.json();
2841
3407
  return ok(data);
2842
3408
  } catch (error) {
2843
3409
  return err(wrapError("sql", error));
3410
+ } finally {
3411
+ request.dispose();
2844
3412
  }
2845
3413
  });
2846
3414
  }
@@ -2849,20 +3417,24 @@ var SQLService = class extends BaseService {
2849
3417
  if (!this.requireAuth()) {
2850
3418
  return err(authRequiredError("sql"));
2851
3419
  }
3420
+ const request = this.createRequestSignal(options?.signal);
2852
3421
  try {
3422
+ const requestedActions = this.actionsForSqlBatch(statements);
2853
3423
  const response = await this.invokeSQL(
2854
3424
  dbName,
2855
- this.actionsForSqlBatch(statements),
3425
+ requestedActions,
2856
3426
  { action: "batch", statements },
2857
- options?.signal
3427
+ request.signal
2858
3428
  );
2859
3429
  if (!response.ok) {
2860
- return this.handleErrorResponse(response, "batch");
3430
+ return this.handleErrorResponse(response, "batch", dbName, requestedActions);
2861
3431
  }
2862
3432
  const data = await response.json();
2863
3433
  return ok(data);
2864
3434
  } catch (error) {
2865
3435
  return err(wrapError("sql", error));
3436
+ } finally {
3437
+ request.dispose();
2866
3438
  }
2867
3439
  });
2868
3440
  }
@@ -2871,20 +3443,23 @@ var SQLService = class extends BaseService {
2871
3443
  if (!this.requireAuth()) {
2872
3444
  return err(authRequiredError("sql"));
2873
3445
  }
3446
+ const request = this.createRequestSignal(options?.signal);
2874
3447
  try {
2875
3448
  const response = await this.invokeSQL(
2876
3449
  dbName,
2877
3450
  SQLAction.WRITE,
2878
3451
  { action: "execute_statement", name, params: params ?? [] },
2879
- options?.signal
3452
+ request.signal
2880
3453
  );
2881
3454
  if (!response.ok) {
2882
- return this.handleErrorResponse(response, "executeStatement");
3455
+ return this.handleErrorResponse(response, "executeStatement", dbName, [SQLAction.WRITE]);
2883
3456
  }
2884
3457
  const data = await response.json();
2885
3458
  return ok(data);
2886
3459
  } catch (error) {
2887
3460
  return err(wrapError("sql", error));
3461
+ } finally {
3462
+ request.dispose();
2888
3463
  }
2889
3464
  });
2890
3465
  }
@@ -2893,15 +3468,16 @@ var SQLService = class extends BaseService {
2893
3468
  if (!this.requireAuth()) {
2894
3469
  return err(authRequiredError("sql"));
2895
3470
  }
3471
+ const request = this.createRequestSignal(options?.signal);
2896
3472
  try {
2897
3473
  const response = await this.invokeSQL(
2898
3474
  dbName,
2899
3475
  SQLAction.READ,
2900
3476
  { action: "export" },
2901
- options?.signal
3477
+ request.signal
2902
3478
  );
2903
3479
  if (!response.ok) {
2904
- return this.handleErrorResponse(response, "export");
3480
+ return this.handleErrorResponse(response, "export", dbName, [SQLAction.READ]);
2905
3481
  }
2906
3482
  const resp = response;
2907
3483
  if (typeof resp.blob === "function") {
@@ -2912,26 +3488,31 @@ var SQLService = class extends BaseService {
2912
3488
  return ok(text);
2913
3489
  } catch (error) {
2914
3490
  return err(wrapError("sql", error));
3491
+ } finally {
3492
+ request.dispose();
2915
3493
  }
2916
3494
  });
2917
3495
  }
2918
3496
  async applyMigrationsOnDbUnlocked(dbName, options) {
2919
3497
  return this.withTelemetry("migrations.apply", dbName, async () => {
2920
- const created = await this.ensureMigrationsTable(dbName, options.signal);
2921
- if (!created.ok) return created;
2922
3498
  const listed = await this.queryOnDb(
2923
3499
  dbName,
2924
3500
  `SELECT id FROM ${MIGRATIONS_TABLE} WHERE namespace = ? ORDER BY applied_at, id`,
2925
3501
  [options.namespace],
2926
3502
  { signal: options.signal }
2927
3503
  );
2928
- if (!listed.ok) return listed;
3504
+ const tableMissing = !listed.ok && (listed.error.code === ErrorCodes.SQL_DATABASE_NOT_FOUND || isMissingTableError(listed.error.message));
3505
+ if (!listed.ok && !tableMissing) return listed;
2929
3506
  const appliedIds = new Set(
2930
- listed.data.rows.map((row) => rowValue(row, 0)).filter((id) => typeof id === "string")
3507
+ (listed.ok ? listed.data.rows : []).map((row) => rowValue(row, 0)).filter((id) => typeof id === "string")
2931
3508
  );
2932
3509
  const skipped = options.migrations.filter((migration) => appliedIds.has(migration.id)).map((migration) => migration.id);
2933
3510
  const pending = options.migrations.filter((migration) => !appliedIds.has(migration.id));
2934
3511
  const applied = [];
3512
+ if (pending.length > 0 && tableMissing) {
3513
+ const created = await this.ensureMigrationsTable(dbName, options.signal);
3514
+ if (!created.ok) return created;
3515
+ }
2935
3516
  for (const migration of pending) {
2936
3517
  const result = await this.applyOneMigration(dbName, options.namespace, migration, options.signal);
2937
3518
  if (!result.ok) return result;
@@ -2999,7 +3580,7 @@ var SQLService = class extends BaseService {
2999
3580
  "Content-Type": "application/json"
3000
3581
  },
3001
3582
  body: JSON.stringify(body),
3002
- signal: this.combineSignals(signal)
3583
+ signal
3003
3584
  });
3004
3585
  }
3005
3586
  actionForSql(sql, fallback) {
@@ -3032,8 +3613,12 @@ var SQLService = class extends BaseService {
3032
3613
  }))
3033
3614
  );
3034
3615
  }
3035
- async handleErrorResponse(response, operation) {
3616
+ async handleErrorResponse(response, operation, dbName, requestedActions) {
3036
3617
  const errorText = await response.text();
3618
+ const meta = responseErrorMeta(response.status, response.statusText, errorText);
3619
+ if (response.status === 402) {
3620
+ return err(storageRejectionError("sql", 402, meta, errorText));
3621
+ }
3037
3622
  const errorBody = parseServiceErrorBody(errorText);
3038
3623
  const errorCode = this.mapHttpStatusToErrorCode(
3039
3624
  response.status,
@@ -3046,11 +3631,12 @@ var SQLService = class extends BaseService {
3046
3631
  errorText,
3047
3632
  errorBody
3048
3633
  );
3049
- const meta = responseErrorMeta(response.status, response.statusText, errorText);
3050
3634
  if (response.status === 401) {
3051
- const { resource, action } = parseAuthError(errorText);
3052
- if (action) meta.requiredAction = action;
3053
- if (resource) meta.resource = resource;
3635
+ const { action } = parseAuthError(errorText);
3636
+ if (action && requestedActions.includes(action)) {
3637
+ meta.requiredAction = action;
3638
+ meta.resource = `${this.context.session.spaceId}/sql/${dbName}`;
3639
+ }
3054
3640
  }
3055
3641
  return err(
3056
3642
  serviceError(errorCode, message, "sql", { meta })
@@ -3147,6 +3733,9 @@ function serializeSqlValues(values) {
3147
3733
  (value) => value instanceof Uint8Array ? Array.from(value) : value
3148
3734
  );
3149
3735
  }
3736
+ function isMissingTableError(message) {
3737
+ return /no such table/i.test(message);
3738
+ }
3150
3739
  function migrationKey(namespace, id) {
3151
3740
  return `${encodeURIComponent(namespace)}:${encodeURIComponent(id)}`;
3152
3741
  }
@@ -3192,16 +3781,16 @@ var DuckDbDatabaseHandle = class {
3192
3781
  };
3193
3782
 
3194
3783
  // src/duckdb/types.ts
3195
- var import_bootstrap2 = require("@tinycloud/bootstrap");
3784
+ var import_bootstrap3 = require("@tinycloud/bootstrap");
3196
3785
  var DuckDbAction = {
3197
- READ: import_bootstrap2.DUCKDB.READ,
3198
- WRITE: import_bootstrap2.DUCKDB.WRITE,
3199
- ADMIN: import_bootstrap2.DUCKDB.ADMIN,
3200
- DESCRIBE: import_bootstrap2.DUCKDB.DESCRIBE,
3201
- EXPORT: import_bootstrap2.DUCKDB.EXPORT,
3202
- IMPORT: import_bootstrap2.DUCKDB.IMPORT,
3203
- EXECUTE: import_bootstrap2.DUCKDB.EXECUTE,
3204
- ALL: import_bootstrap2.DUCKDB.ALL
3786
+ READ: import_bootstrap3.DUCKDB.READ,
3787
+ WRITE: import_bootstrap3.DUCKDB.WRITE,
3788
+ ADMIN: import_bootstrap3.DUCKDB.ADMIN,
3789
+ DESCRIBE: import_bootstrap3.DUCKDB.DESCRIBE,
3790
+ EXPORT: import_bootstrap3.DUCKDB.EXPORT,
3791
+ IMPORT: import_bootstrap3.DUCKDB.IMPORT,
3792
+ EXECUTE: import_bootstrap3.DUCKDB.EXECUTE,
3793
+ ALL: import_bootstrap3.DUCKDB.ALL
3205
3794
  };
3206
3795
 
3207
3796
  // src/duckdb/DuckDbService.ts
@@ -3255,12 +3844,13 @@ var DuckDbService = class extends BaseService {
3255
3844
  if (!this.requireAuth()) {
3256
3845
  return err(authRequiredError("duckdb"));
3257
3846
  }
3847
+ const request = this.createRequestSignal(options?.signal);
3258
3848
  try {
3259
3849
  const response = await this.invokeDuckDb(
3260
3850
  dbName,
3261
3851
  DuckDbAction.READ,
3262
3852
  { action: "query", sql, params: params ?? [] },
3263
- options?.signal
3853
+ request.signal
3264
3854
  );
3265
3855
  if (!response.ok) {
3266
3856
  return this.handleErrorResponse(response, "query");
@@ -3269,6 +3859,8 @@ var DuckDbService = class extends BaseService {
3269
3859
  return ok(data);
3270
3860
  } catch (error) {
3271
3861
  return err(wrapError("duckdb", error));
3862
+ } finally {
3863
+ request.dispose();
3272
3864
  }
3273
3865
  });
3274
3866
  }
@@ -3277,12 +3869,13 @@ var DuckDbService = class extends BaseService {
3277
3869
  if (!this.requireAuth()) {
3278
3870
  return err(authRequiredError("duckdb"));
3279
3871
  }
3872
+ const request = this.createRequestSignal(options?.signal);
3280
3873
  try {
3281
3874
  const response = await this.invokeDuckDb(
3282
3875
  dbName,
3283
3876
  DuckDbAction.READ,
3284
3877
  { action: "query", sql, params: params ?? [] },
3285
- options?.signal,
3878
+ request.signal,
3286
3879
  { Accept: "application/vnd.apache.arrow.stream" }
3287
3880
  );
3288
3881
  if (!response.ok) {
@@ -3292,6 +3885,8 @@ var DuckDbService = class extends BaseService {
3292
3885
  return ok(buffer);
3293
3886
  } catch (error) {
3294
3887
  return err(wrapError("duckdb", error));
3888
+ } finally {
3889
+ request.dispose();
3295
3890
  }
3296
3891
  });
3297
3892
  }
@@ -3300,6 +3895,7 @@ var DuckDbService = class extends BaseService {
3300
3895
  if (!this.requireAuth()) {
3301
3896
  return err(authRequiredError("duckdb"));
3302
3897
  }
3898
+ const request = this.createRequestSignal(options?.signal);
3303
3899
  try {
3304
3900
  const body = {
3305
3901
  action: "execute",
@@ -3313,7 +3909,7 @@ var DuckDbService = class extends BaseService {
3313
3909
  dbName,
3314
3910
  DuckDbAction.WRITE,
3315
3911
  body,
3316
- options?.signal
3912
+ request.signal
3317
3913
  );
3318
3914
  if (!response.ok) {
3319
3915
  return this.handleErrorResponse(response, "execute");
@@ -3322,6 +3918,8 @@ var DuckDbService = class extends BaseService {
3322
3918
  return ok(data);
3323
3919
  } catch (error) {
3324
3920
  return err(wrapError("duckdb", error));
3921
+ } finally {
3922
+ request.dispose();
3325
3923
  }
3326
3924
  });
3327
3925
  }
@@ -3330,6 +3928,7 @@ var DuckDbService = class extends BaseService {
3330
3928
  if (!this.requireAuth()) {
3331
3929
  return err(authRequiredError("duckdb"));
3332
3930
  }
3931
+ const request = this.createRequestSignal(options?.signal);
3333
3932
  try {
3334
3933
  const body = {
3335
3934
  action: "batch",
@@ -3342,7 +3941,7 @@ var DuckDbService = class extends BaseService {
3342
3941
  dbName,
3343
3942
  DuckDbAction.WRITE,
3344
3943
  body,
3345
- options?.signal
3944
+ request.signal
3346
3945
  );
3347
3946
  if (!response.ok) {
3348
3947
  return this.handleErrorResponse(response, "batch");
@@ -3351,6 +3950,8 @@ var DuckDbService = class extends BaseService {
3351
3950
  return ok(data);
3352
3951
  } catch (error) {
3353
3952
  return err(wrapError("duckdb", error));
3953
+ } finally {
3954
+ request.dispose();
3354
3955
  }
3355
3956
  });
3356
3957
  }
@@ -3359,12 +3960,13 @@ var DuckDbService = class extends BaseService {
3359
3960
  if (!this.requireAuth()) {
3360
3961
  return err(authRequiredError("duckdb"));
3361
3962
  }
3963
+ const request = this.createRequestSignal(options?.signal);
3362
3964
  try {
3363
3965
  const response = await this.invokeDuckDb(
3364
3966
  dbName,
3365
3967
  DuckDbAction.WRITE,
3366
3968
  { action: "executeStatement", name, params: params ?? [] },
3367
- options?.signal
3969
+ request.signal
3368
3970
  );
3369
3971
  if (!response.ok) {
3370
3972
  return this.handleErrorResponse(response, "executeStatement");
@@ -3373,6 +3975,8 @@ var DuckDbService = class extends BaseService {
3373
3975
  return ok(data);
3374
3976
  } catch (error) {
3375
3977
  return err(wrapError("duckdb", error));
3978
+ } finally {
3979
+ request.dispose();
3376
3980
  }
3377
3981
  });
3378
3982
  }
@@ -3381,12 +3985,13 @@ var DuckDbService = class extends BaseService {
3381
3985
  if (!this.requireAuth()) {
3382
3986
  return err(authRequiredError("duckdb"));
3383
3987
  }
3988
+ const request = this.createRequestSignal(options?.signal);
3384
3989
  try {
3385
3990
  const response = await this.invokeDuckDb(
3386
3991
  dbName,
3387
3992
  DuckDbAction.READ,
3388
3993
  { action: "describe" },
3389
- options?.signal
3994
+ request.signal
3390
3995
  );
3391
3996
  if (!response.ok) {
3392
3997
  return this.handleErrorResponse(response, "describe");
@@ -3395,6 +4000,8 @@ var DuckDbService = class extends BaseService {
3395
4000
  return ok(data);
3396
4001
  } catch (error) {
3397
4002
  return err(wrapError("duckdb", error));
4003
+ } finally {
4004
+ request.dispose();
3398
4005
  }
3399
4006
  });
3400
4007
  }
@@ -3403,12 +4010,13 @@ var DuckDbService = class extends BaseService {
3403
4010
  if (!this.requireAuth()) {
3404
4011
  return err(authRequiredError("duckdb"));
3405
4012
  }
4013
+ const request = this.createRequestSignal(options?.signal);
3406
4014
  try {
3407
4015
  const response = await this.invokeDuckDb(
3408
4016
  dbName,
3409
4017
  DuckDbAction.EXPORT,
3410
4018
  { action: "export" },
3411
- options?.signal
4019
+ request.signal
3412
4020
  );
3413
4021
  if (!response.ok) {
3414
4022
  return this.handleErrorResponse(response, "export");
@@ -3417,6 +4025,8 @@ var DuckDbService = class extends BaseService {
3417
4025
  return ok(blob);
3418
4026
  } catch (error) {
3419
4027
  return err(wrapError("duckdb", error));
4028
+ } finally {
4029
+ request.dispose();
3420
4030
  }
3421
4031
  });
3422
4032
  }
@@ -3425,6 +4035,7 @@ var DuckDbService = class extends BaseService {
3425
4035
  if (!this.requireAuth()) {
3426
4036
  return err(authRequiredError("duckdb"));
3427
4037
  }
4038
+ const request = this.createRequestSignal(options?.signal);
3428
4039
  try {
3429
4040
  const session = this.context.session;
3430
4041
  const headers = this.context.invoke(
@@ -3440,7 +4051,7 @@ var DuckDbService = class extends BaseService {
3440
4051
  "Content-Type": "application/x-duckdb"
3441
4052
  },
3442
4053
  body: new Blob([data]),
3443
- signal: this.combineSignals(options?.signal)
4054
+ signal: request.signal
3444
4055
  });
3445
4056
  if (!response.ok) {
3446
4057
  return this.handleErrorResponse(response, "import");
@@ -3448,6 +4059,8 @@ var DuckDbService = class extends BaseService {
3448
4059
  return ok(void 0);
3449
4060
  } catch (error) {
3450
4061
  return err(wrapError("duckdb", error));
4062
+ } finally {
4063
+ request.dispose();
3451
4064
  }
3452
4065
  });
3453
4066
  }
@@ -3463,11 +4076,15 @@ var DuckDbService = class extends BaseService {
3463
4076
  ...extraHeaders
3464
4077
  },
3465
4078
  body: JSON.stringify(body),
3466
- signal: this.combineSignals(signal)
4079
+ signal
3467
4080
  });
3468
4081
  }
3469
4082
  async handleErrorResponse(response, operation) {
3470
4083
  const errorText = await response.text();
4084
+ const meta = responseErrorMeta(response.status, response.statusText, errorText);
4085
+ if (response.status === 402) {
4086
+ return err(storageRejectionError("duckdb", 402, meta, errorText));
4087
+ }
3471
4088
  const errorBody = parseServiceErrorBody(errorText);
3472
4089
  const errorCode = this.mapHttpStatusToErrorCode(
3473
4090
  response.status,
@@ -3480,7 +4097,6 @@ var DuckDbService = class extends BaseService {
3480
4097
  errorText,
3481
4098
  errorBody
3482
4099
  );
3483
- const meta = responseErrorMeta(response.status, response.statusText, errorText);
3484
4100
  if (response.status === 401) {
3485
4101
  const { resource, action } = parseAuthError(errorText);
3486
4102
  if (action) meta.requiredAction = action;
@@ -3979,10 +4595,14 @@ async function responseError(service, message, response) {
3979
4595
  }
3980
4596
  } catch {
3981
4597
  }
3982
- return wrapError(
4598
+ const error = wrapError(
3983
4599
  service,
3984
4600
  new Error(`${message}: ${response.status} ${detail}`)
3985
4601
  );
4602
+ return {
4603
+ ...error,
4604
+ meta: { ...error.meta, status: response.status, statusText: response.statusText }
4605
+ };
3986
4606
  }
3987
4607
  function isAbortError(error) {
3988
4608
  return error instanceof DOMException && error.name === "AbortError";
@@ -4448,6 +5068,9 @@ function defaultVaultMessage(input) {
4448
5068
  return input.message ?? `Public key not found for ${input.did}`;
4449
5069
  case "STORAGE_ERROR":
4450
5070
  return input.message ?? input.cause.message;
5071
+ case "STORAGE_QUOTA_EXCEEDED":
5072
+ case "STORAGE_LIMIT_REACHED":
5073
+ return input.message;
4451
5074
  }
4452
5075
  }
4453
5076
  function vaultError(input) {
@@ -4458,6 +5081,15 @@ function vaultError(input) {
4458
5081
  };
4459
5082
  return { ok: false, error };
4460
5083
  }
5084
+ function kvWriteError(context, error) {
5085
+ if (error.code === ErrorCodes.STORAGE_QUOTA_EXCEEDED || error.code === ErrorCodes.STORAGE_LIMIT_REACHED) {
5086
+ return vaultError({ code: error.code, message: error.message, meta: error.meta });
5087
+ }
5088
+ return vaultError({
5089
+ code: "STORAGE_ERROR",
5090
+ cause: new Error(`${context}: ${error.message}`)
5091
+ });
5092
+ }
4461
5093
  var DataVaultService = class extends BaseService {
4462
5094
  /**
4463
5095
  * Create a new DataVaultService instance.
@@ -4740,12 +5372,7 @@ var DataVaultService = class extends BaseService {
4740
5372
  JSON.stringify(envelopeResult.data)
4741
5373
  );
4742
5374
  if (!valuePutResult.ok) {
4743
- return vaultError({
4744
- code: "STORAGE_ERROR",
4745
- cause: new Error(
4746
- `Failed to store encrypted value: ${valuePutResult.error.message}`
4747
- )
4748
- });
5375
+ return kvWriteError("Failed to store encrypted value", valuePutResult.error);
4749
5376
  }
4750
5377
  return { ok: true, data: void 0 };
4751
5378
  } catch (error) {
@@ -4963,12 +5590,7 @@ var DataVaultService = class extends BaseService {
4963
5590
  keyPayload
4964
5591
  );
4965
5592
  if (!keyPutResult.ok) {
4966
- return vaultError({
4967
- code: "STORAGE_ERROR",
4968
- cause: new Error(
4969
- `Failed to store key blob: ${keyPutResult.error.message}`
4970
- )
4971
- });
5593
+ return kvWriteError("Failed to store key blob", keyPutResult.error);
4972
5594
  }
4973
5595
  const valuePayload = JSON.stringify({
4974
5596
  data: base64Encode(encrypted),
@@ -4979,12 +5601,7 @@ var DataVaultService = class extends BaseService {
4979
5601
  valuePayload
4980
5602
  );
4981
5603
  if (!valuePutResult.ok) {
4982
- return vaultError({
4983
- code: "STORAGE_ERROR",
4984
- cause: new Error(
4985
- `Failed to store encrypted value: ${valuePutResult.error.message}`
4986
- )
4987
- });
5604
+ return kvWriteError("Failed to store encrypted value", valuePutResult.error);
4988
5605
  }
4989
5606
  return ok(void 0);
4990
5607
  } catch (error) {
@@ -5315,12 +5932,7 @@ var DataVaultService = class extends BaseService {
5315
5932
  grantPayload
5316
5933
  );
5317
5934
  if (!grantPutResult.ok) {
5318
- return vaultError({
5319
- code: "STORAGE_ERROR",
5320
- cause: new Error(
5321
- `Failed to store grant: ${grantPutResult.error.message}`
5322
- )
5323
- });
5935
+ return kvWriteError("Failed to store grant", grantPutResult.error);
5324
5936
  }
5325
5937
  return ok(void 0);
5326
5938
  } catch (error) {
@@ -5665,12 +6277,7 @@ var DataVaultService = class extends BaseService {
5665
6277
  keyPayload
5666
6278
  );
5667
6279
  if (!keyPutResult.ok) {
5668
- return vaultError({
5669
- code: "STORAGE_ERROR",
5670
- cause: new Error(
5671
- `Failed to store rotated key blob: ${keyPutResult.error.message}`
5672
- )
5673
- });
6280
+ return kvWriteError("Failed to store rotated key blob", keyPutResult.error);
5674
6281
  }
5675
6282
  const valuePayload = JSON.stringify({
5676
6283
  data: base64Encode(encrypted),
@@ -5681,12 +6288,7 @@ var DataVaultService = class extends BaseService {
5681
6288
  valuePayload
5682
6289
  );
5683
6290
  if (!valuePutResult.ok) {
5684
- return vaultError({
5685
- code: "STORAGE_ERROR",
5686
- cause: new Error(
5687
- `Failed to store re-encrypted value: ${valuePutResult.error.message}`
5688
- )
5689
- });
6291
+ return kvWriteError("Failed to store re-encrypted value", valuePutResult.error);
5690
6292
  }
5691
6293
  for (const did of remainingGrantees) {
5692
6294
  const grantResult = await this.reencrypt(key, did);
@@ -5895,6 +6497,7 @@ var SecretsService = class {
5895
6497
  }
5896
6498
  async listAll() {
5897
6499
  const entries = /* @__PURE__ */ new Map();
6500
+ const seenCursors = /* @__PURE__ */ new Set();
5898
6501
  let cursor;
5899
6502
  do {
5900
6503
  const result = await this.vault.listPage({
@@ -5917,6 +6520,14 @@ var SecretsService = class {
5917
6520
  "Secret catalog listing was truncated without a continuation cursor."
5918
6521
  );
5919
6522
  }
6523
+ if (cursor !== void 0) {
6524
+ if (seenCursors.has(cursor)) {
6525
+ return invalidSecretInput(
6526
+ "Secret catalog listing repeated a continuation cursor; the node does not support paged KV lists."
6527
+ );
6528
+ }
6529
+ seenCursors.add(cursor);
6530
+ }
5920
6531
  } while (cursor !== void 0);
5921
6532
  return {
5922
6533
  ok: true,
@@ -6037,103 +6648,6 @@ function canonicalHashHex(sha256, value) {
6037
6648
  return hexEncode2(sha256(utf8Encode(canonical)));
6038
6649
  }
6039
6650
 
6040
- // src/encryption/networkId.ts
6041
- var URN_PREFIX = "urn:tinycloud:encryption:";
6042
- var NETWORK_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
6043
- var PKH_EIP155_DID_RE = /^did:pkh:eip155:(\d+):(0x[a-fA-F0-9]{40})$/;
6044
- var NetworkIdError = class extends Error {
6045
- constructor(message) {
6046
- super(message);
6047
- this.name = "NetworkIdError";
6048
- }
6049
- };
6050
- function parseNetworkId(networkId) {
6051
- if (typeof networkId !== "string" || networkId.length === 0) {
6052
- throw new NetworkIdError("networkId must be a non-empty string");
6053
- }
6054
- if (!networkId.startsWith(URN_PREFIX)) {
6055
- throw new NetworkIdError(
6056
- `networkId must start with ${URN_PREFIX} (got ${JSON.stringify(networkId)})`
6057
- );
6058
- }
6059
- const body = networkId.slice(URN_PREFIX.length);
6060
- const lastColon = body.lastIndexOf(":");
6061
- if (lastColon <= 0 || lastColon === body.length - 1) {
6062
- throw new NetworkIdError(
6063
- `networkId missing ownerDid or name segment (got ${JSON.stringify(networkId)})`
6064
- );
6065
- }
6066
- const ownerDid = body.slice(0, lastColon);
6067
- const name = body.slice(lastColon + 1);
6068
- if (!ownerDid.startsWith("did:")) {
6069
- throw new NetworkIdError(
6070
- `networkId ownerDid must be a DID (got ${JSON.stringify(ownerDid)})`
6071
- );
6072
- }
6073
- const didParts = ownerDid.split(":");
6074
- if (didParts.length < 3 || didParts.some((p) => p.length === 0)) {
6075
- throw new NetworkIdError(
6076
- `networkId ownerDid is not a well-formed DID (got ${JSON.stringify(ownerDid)})`
6077
- );
6078
- }
6079
- if (!NETWORK_NAME_RE.test(name)) {
6080
- throw new NetworkIdError(
6081
- `networkId name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
6082
- );
6083
- }
6084
- return { networkId, ownerDid, name };
6085
- }
6086
- function buildNetworkId(ownerDid, name) {
6087
- if (typeof ownerDid !== "string" || !ownerDid.startsWith("did:")) {
6088
- throw new NetworkIdError("ownerDid must be a DID");
6089
- }
6090
- if (typeof name !== "string" || !NETWORK_NAME_RE.test(name)) {
6091
- throw new NetworkIdError(
6092
- `network name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
6093
- );
6094
- }
6095
- const networkId = `${URN_PREFIX}${ownerDid}:${name}`;
6096
- parseNetworkId(networkId);
6097
- return networkId;
6098
- }
6099
- function isNetworkId(networkId) {
6100
- if (typeof networkId !== "string") {
6101
- return false;
6102
- }
6103
- try {
6104
- parseNetworkId(networkId);
6105
- return true;
6106
- } catch {
6107
- return false;
6108
- }
6109
- }
6110
- function parsePkhOwnerDid(ownerDid) {
6111
- const match = ownerDid.match(PKH_EIP155_DID_RE);
6112
- if (!match) return null;
6113
- return {
6114
- chainId: match[1],
6115
- address: match[2].toLowerCase()
6116
- };
6117
- }
6118
- function ownerDidMatches(a, b) {
6119
- const aPkh = parsePkhOwnerDid(a);
6120
- const bPkh = parsePkhOwnerDid(b);
6121
- if (aPkh && bPkh) {
6122
- return aPkh.chainId === bPkh.chainId && aPkh.address === bPkh.address;
6123
- }
6124
- return a === b;
6125
- }
6126
- function networkDiscoveryKey(name) {
6127
- if (!NETWORK_NAME_RE.test(name)) {
6128
- throw new NetworkIdError(
6129
- `network name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
6130
- );
6131
- }
6132
- return `.well-known/encryption/network/${name}`;
6133
- }
6134
- var ENCRYPTION_NETWORK_URN_PREFIX = URN_PREFIX;
6135
- var NETWORK_NAME_PATTERN = NETWORK_NAME_RE;
6136
-
6137
6651
  // src/encryption/types.ts
6138
6652
  var DEFAULT_ENCRYPTION_ALG = "x25519-aes256gcm/v1";
6139
6653
  var ENVELOPE_VERSION = 1;
@@ -7016,6 +7530,8 @@ var import_decrypt_transport_response_error3 = require("@tinycloud/sdk-services/
7016
7530
  SQLAction,
7017
7531
  SQLMigrations,
7018
7532
  SQLService,
7533
+ STORAGE_FULL_MESSAGE,
7534
+ STORAGE_WRITE_TOO_LARGE_MESSAGE,
7019
7535
  SecretsService,
7020
7536
  ServiceContext,
7021
7537
  ServiceErrorEventSchema,
@@ -7034,6 +7550,7 @@ var import_decrypt_transport_response_error3 = require("@tinycloud/sdk-services/
7034
7550
  authExpiredError,
7035
7551
  authRequiredError,
7036
7552
  authUnauthorizedError,
7553
+ authorizationVerdictOf,
7037
7554
  base64Decode,
7038
7555
  base64Encode,
7039
7556
  buildCanonicalDecryptRequest,
@@ -7067,22 +7584,26 @@ var import_decrypt_transport_response_error3 = require("@tinycloud/sdk-services/
7067
7584
  hexEncode,
7068
7585
  installTinyCloudDebugGlobals,
7069
7586
  isNetworkId,
7587
+ isStorageFullError,
7070
7588
  networkDiscoveryKey,
7071
7589
  networkError,
7072
7590
  notFoundError,
7073
7591
  ok,
7074
7592
  openWrappedKey,
7075
7593
  parseAuthError,
7594
+ parseCapabilityResource,
7076
7595
  parseNetworkId,
7077
7596
  parsePermissionHint,
7078
7597
  parsePermissionHintFromErrorText,
7079
7598
  parseSecretCatalogKey,
7599
+ parseStorageQuotaBytes,
7080
7600
  permissionDeniedError,
7081
7601
  resolveSecretListPrefix,
7082
7602
  resolveSecretPath,
7083
7603
  serviceError,
7084
7604
  storageLimitReachedError,
7085
7605
  storageQuotaExceededError,
7606
+ storageRejectionError,
7086
7607
  timeoutError,
7087
7608
  tinyCloudDebugLogger,
7088
7609
  utf8Decode,
@@ -7096,6 +7617,7 @@ var import_decrypt_transport_response_error3 = require("@tinycloud/sdk-services/
7096
7617
  validateServiceResponseEvent,
7097
7618
  validateServiceSession,
7098
7619
  validateTelemetrySpanEvent,
7620
+ validatedCapabilityOf,
7099
7621
  verifyDecryptResponse,
7100
7622
  wrapError
7101
7623
  });