@tinycloud/sdk-services 3.1.0-beta.1 → 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,
@@ -107,22 +109,26 @@ __export(src_exports, {
107
109
  hexEncode: () => hexEncode2,
108
110
  installTinyCloudDebugGlobals: () => installTinyCloudDebugGlobals,
109
111
  isNetworkId: () => isNetworkId,
112
+ isStorageFullError: () => isStorageFullError,
110
113
  networkDiscoveryKey: () => networkDiscoveryKey,
111
114
  networkError: () => networkError,
112
115
  notFoundError: () => notFoundError,
113
116
  ok: () => ok,
114
117
  openWrappedKey: () => openWrappedKey,
115
118
  parseAuthError: () => parseAuthError,
119
+ parseCapabilityResource: () => parseCapabilityResource,
116
120
  parseNetworkId: () => parseNetworkId,
117
121
  parsePermissionHint: () => parsePermissionHint,
118
122
  parsePermissionHintFromErrorText: () => parsePermissionHintFromErrorText,
119
123
  parseSecretCatalogKey: () => parseSecretCatalogKey,
124
+ parseStorageQuotaBytes: () => parseStorageQuotaBytes,
120
125
  permissionDeniedError: () => permissionDeniedError,
121
126
  resolveSecretListPrefix: () => resolveSecretListPrefix,
122
127
  resolveSecretPath: () => resolveSecretPath,
123
128
  serviceError: () => serviceError,
124
129
  storageLimitReachedError: () => storageLimitReachedError,
125
130
  storageQuotaExceededError: () => storageQuotaExceededError,
131
+ storageRejectionError: () => storageRejectionError,
126
132
  timeoutError: () => timeoutError,
127
133
  tinyCloudDebugLogger: () => tinyCloudDebugLogger,
128
134
  utf8Decode: () => utf8Decode,
@@ -136,6 +142,7 @@ __export(src_exports, {
136
142
  validateServiceResponseEvent: () => validateServiceResponseEvent,
137
143
  validateServiceSession: () => validateServiceSession,
138
144
  validateTelemetrySpanEvent: () => validateTelemetrySpanEvent,
145
+ validatedCapabilityOf: () => validatedCapabilityOf,
139
146
  verifyDecryptResponse: () => verifyDecryptResponse,
140
147
  wrapError: () => wrapError
141
148
  });
@@ -148,6 +155,10 @@ var ErrorCodes = {
148
155
  AUTH_EXPIRED: "AUTH_EXPIRED",
149
156
  AUTH_REQUIRED: "AUTH_REQUIRED",
150
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",
151
162
  NETWORK_ERROR: "NETWORK_ERROR",
152
163
  TIMEOUT: "TIMEOUT",
153
164
  ABORTED: "ABORTED",
@@ -159,6 +170,10 @@ var ErrorCodes = {
159
170
  KV_PRECONDITION_FAILED: "KV_PRECONDITION_FAILED",
160
171
  KV_CONFLICT: "KV_CONFLICT",
161
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",
162
177
  // SQL-specific errors
163
178
  SQL_ERROR: "SQL_ERROR",
164
179
  SQL_PERMISSION_DENIED: "SQL_PERMISSION_DENIED",
@@ -1039,6 +1054,106 @@ var ServiceContext = class {
1039
1054
  }
1040
1055
  };
1041
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
+
1042
1157
  // src/errors.ts
1043
1158
  function parsePermissionHint(value) {
1044
1159
  if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
@@ -1098,11 +1213,31 @@ function networkError(service, message, cause) {
1098
1213
  cause
1099
1214
  };
1100
1215
  }
1101
- 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
+ }
1102
1235
  return {
1103
1236
  code: ErrorCodes.TIMEOUT,
1104
- message: "Request timed out.",
1105
- service
1237
+ message: `Request timed out after ${timeoutMs}ms.`,
1238
+ service,
1239
+ ...cause === void 0 ? {} : { cause },
1240
+ meta: { timeoutMs }
1106
1241
  };
1107
1242
  }
1108
1243
  function abortedError(service) {
@@ -1136,6 +1271,70 @@ function parseAuthError(responseText) {
1136
1271
  function authUnauthorizedError(service, message, meta) {
1137
1272
  return serviceError(ErrorCodes.AUTH_UNAUTHORIZED, message, service, { meta });
1138
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
+ }
1139
1338
  var MAX_CAUSE_DEPTH = 8;
1140
1339
  function errorStatusOf(node) {
1141
1340
  const meta = node.meta;
@@ -1183,8 +1382,28 @@ function storageLimitReachedError(service, message, meta) {
1183
1382
  meta
1184
1383
  };
1185
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
+ }
1186
1402
  function wrapError(service, error, defaultCode = ErrorCodes.NETWORK_ERROR) {
1187
1403
  if (error instanceof Error) {
1404
+ if (isRequestTimeoutError(error)) {
1405
+ return timeoutError(service, error.timeoutMs, error);
1406
+ }
1188
1407
  if (error.name === "AbortError") {
1189
1408
  return abortedError(service);
1190
1409
  }
@@ -1209,6 +1428,7 @@ function errorResult(error) {
1209
1428
  }
1210
1429
 
1211
1430
  // src/base/BaseService.ts
1431
+ var MAX_TIMER_DELAY_MS = 2147483647;
1212
1432
  var BaseService = class {
1213
1433
  constructor() {
1214
1434
  /**
@@ -1389,6 +1609,64 @@ var BaseService = class {
1389
1609
  }
1390
1610
  return controller.signal;
1391
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
+ }
1392
1670
  /**
1393
1671
  * Wrap an operation with error handling and telemetry.
1394
1672
  *
@@ -1497,6 +1775,30 @@ var PrefixedKVService = class _PrefixedKVService {
1497
1775
  removePrefix
1498
1776
  });
1499
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
+ }
1500
1802
  /**
1501
1803
  * Delete a key.
1502
1804
  */
@@ -1549,11 +1851,24 @@ var KVAction = {
1549
1851
  PUT: "tinycloud.kv/put",
1550
1852
  LIST: "tinycloud.kv/list",
1551
1853
  DELETE: "tinycloud.kv/del",
1552
- 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"
1553
1859
  };
1554
1860
 
1555
1861
  // src/kv/KVService.ts
1556
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
+ }
1557
1872
  function encodeKvBatchPartName(path) {
1558
1873
  return encodeURIComponent(path).replace(
1559
1874
  /[!'()*]/g,
@@ -1576,47 +1891,21 @@ var KVService = class extends BaseService {
1576
1891
  get config() {
1577
1892
  return this._config;
1578
1893
  }
1579
- // Parses "Used: X bytes, Limit: Y bytes" from tinycloud-node error responses
1580
- parseQuotaInfo(errorText) {
1581
- const match = errorText.match(
1582
- /Used:\s*(\d+)\s*bytes,\s*Limit:\s*(\d+)\s*bytes/i
1583
- );
1584
- if (match) {
1585
- return {
1586
- usedBytes: parseInt(match[1], 10),
1587
- limitBytes: parseInt(match[2], 10)
1588
- };
1589
- }
1590
- return void 0;
1591
- }
1592
1894
  handleQuotaErrorResponse(response, errorText, key) {
1593
- if (response.status === 402) {
1594
- const quotaInfo = this.parseQuotaInfo(errorText);
1595
- return err(
1596
- storageQuotaExceededError(
1597
- "kv",
1598
- `Storage quota exceeded for key ${JSON.stringify(key)}: ${errorText}`,
1599
- {
1600
- status: response.status,
1601
- ...quotaInfo ? { usedBytes: quotaInfo.usedBytes, limitBytes: quotaInfo.limitBytes } : {}
1602
- }
1603
- )
1604
- );
1895
+ if (response.status !== 402 && response.status !== 413) {
1896
+ return void 0;
1605
1897
  }
1606
- if (response.status === 413) {
1607
- const quotaInfo = this.parseQuotaInfo(errorText);
1608
- return err(
1609
- storageLimitReachedError(
1610
- "kv",
1611
- `Storage limit reached for key ${JSON.stringify(key)}: ${errorText}`,
1612
- {
1613
- status: response.status,
1614
- ...quotaInfo ? { usedBytes: quotaInfo.usedBytes, limitBytes: quotaInfo.limitBytes } : {}
1615
- }
1616
- )
1617
- );
1898
+ if (response.status === 413 && !parseStorageQuotaBytes(errorText)) {
1899
+ return void 0;
1618
1900
  }
1619
- 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
+ );
1620
1909
  }
1621
1910
  /**
1622
1911
  * Classify a KV 404 by reading the response body once.
@@ -1675,7 +1964,7 @@ var KVService = class extends BaseService {
1675
1964
  * @param path - Resource path
1676
1965
  * @param action - KV action
1677
1966
  * @param body - Optional request body
1678
- * @param signal - Optional abort signal
1967
+ * @param signal - Request signal from createRequestSignal()
1679
1968
  * @returns Fetch response
1680
1969
  */
1681
1970
  async invokeOperation(path, action, body, signal, extraHeaders) {
@@ -1691,7 +1980,7 @@ var KVService = class extends BaseService {
1691
1980
  method: "POST",
1692
1981
  headers: requestHeaders,
1693
1982
  body,
1694
- signal: this.combineSignals(signal)
1983
+ signal
1695
1984
  });
1696
1985
  }
1697
1986
  /**
@@ -1868,6 +2157,7 @@ var KVService = class extends BaseService {
1868
2157
  "kv"
1869
2158
  ));
1870
2159
  }
2160
+ const request = this.createRequestSignal(options?.signal, options?.timeout);
1871
2161
  try {
1872
2162
  const session = this.context.session;
1873
2163
  const invocationHeaders = this.context.invokeAny(
@@ -1889,15 +2179,17 @@ var KVService = class extends BaseService {
1889
2179
  const response = await this.context.fetch(`${this.host}/invoke`, {
1890
2180
  method: "POST",
1891
2181
  headers,
1892
- signal: this.combineSignals(options?.signal)
2182
+ signal: request.signal
1893
2183
  });
1894
2184
  if (!response.ok) {
1895
- const errorText = await response.text();
2185
+ const errorText = response.status === 401 || response.status === 403 ? await this.readAuthorizationText(response) : await response.text();
1896
2186
  if (response.status === 401 || response.status === 403) {
1897
2187
  return this.authorizationFailure(
1898
2188
  `Failed to batch read ${keys.length} key(s)`,
1899
2189
  response,
1900
- errorText
2190
+ errorText,
2191
+ paths,
2192
+ action
1901
2193
  );
1902
2194
  }
1903
2195
  if (response.status === 413) {
@@ -1952,6 +2244,8 @@ var KVService = class extends BaseService {
1952
2244
  return ok({ results, count: results.length });
1953
2245
  } catch (error) {
1954
2246
  return err(wrapError("kv", error));
2247
+ } finally {
2248
+ request.dispose();
1955
2249
  }
1956
2250
  }
1957
2251
  /**
@@ -1987,7 +2281,7 @@ var KVService = class extends BaseService {
1987
2281
  return text;
1988
2282
  }
1989
2283
  }
1990
- async createSignedReadUrlError(response, key) {
2284
+ async createSignedReadUrlError(response, key, path) {
1991
2285
  let errorText = response.statusText;
1992
2286
  try {
1993
2287
  const text = await response.text();
@@ -2000,7 +2294,9 @@ var KVService = class extends BaseService {
2000
2294
  return this.authorizationFailure(
2001
2295
  `Failed to create signed read URL for key ${JSON.stringify(key)}`,
2002
2296
  response,
2003
- errorText
2297
+ errorText,
2298
+ [path],
2299
+ KVAction.GET
2004
2300
  );
2005
2301
  }
2006
2302
  const code = response.status === 400 ? ErrorCodes.INVALID_INPUT : ErrorCodes.NETWORK_ERROR;
@@ -2013,21 +2309,42 @@ var KVService = class extends BaseService {
2013
2309
  )
2014
2310
  );
2015
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
+ }
2016
2320
  /**
2017
2321
  * AUTH_UNAUTHORIZED for a 401/403 response. The message keeps the HTTP
2018
2322
  * status and the server text (`<context>: <status> - <text>`, the same
2019
2323
  * shape as every other KV failure) so callers that rethrow only the message
2020
2324
  * still see the status; `meta.status` carries it for typed callers.
2021
- * Resource/ability hints are parsed from the raw body, never the message.
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.
2022
2328
  */
2023
- authorizationFailure(context, response, errorText, extraMeta = {}) {
2329
+ authorizationFailure(context, response, errorText, paths, expectedAction, extraMeta = {}) {
2024
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;
2025
2343
  const detail = errorText.trim().length > 0 ? errorText : response.statusText || "authorization failed";
2026
2344
  return err(authUnauthorizedError("kv", `${context}: ${response.status} - ${detail}`, {
2027
2345
  status: response.status,
2028
- ...action && { requiredAction: action },
2029
- ...resource && { resource },
2030
- ...extraMeta
2346
+ ...extraMeta,
2347
+ ...capability
2031
2348
  }));
2032
2349
  }
2033
2350
  normalizeSignedReadUrlResponse(data) {
@@ -2061,23 +2378,35 @@ var KVService = class extends BaseService {
2061
2378
  ));
2062
2379
  }
2063
2380
  const path = this.getFullPath(key, options?.prefix);
2381
+ const request = this.createRequestSignal(options?.signal, options?.timeout);
2064
2382
  try {
2065
2383
  const response = await this.invokeOperation(
2066
2384
  path,
2067
2385
  KVAction.GET,
2068
2386
  void 0,
2069
- options?.signal,
2387
+ request.signal,
2070
2388
  options?.maxResponseBytes === void 0 ? void 0 : { "x-tinycloud-max-response-bytes": String(options.maxResponseBytes) }
2071
2389
  );
2072
2390
  if (!response.ok) {
2073
2391
  if (response.status === 401 || response.status === 403) {
2074
- const errorText2 = await response.text();
2392
+ const errorText2 = await this.readAuthorizationText(response);
2075
2393
  const permissionHint = parsePermissionHintFromErrorText(errorText2);
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;
2076
2403
  return this.authorizationFailure(
2077
2404
  `Failed to get key ${JSON.stringify(key)}`,
2078
2405
  response,
2079
2406
  errorText2,
2080
- permissionHint === void 0 ? {} : { permissionHint }
2407
+ [path],
2408
+ KVAction.GET,
2409
+ structuredCapability ? { permissionHint, ...structuredCapability } : {}
2081
2410
  );
2082
2411
  }
2083
2412
  if (response.status === 404) {
@@ -2112,6 +2441,8 @@ var KVService = class extends BaseService {
2112
2441
  });
2113
2442
  } catch (error) {
2114
2443
  return err(wrapError("kv", error));
2444
+ } finally {
2445
+ request.dispose();
2115
2446
  }
2116
2447
  });
2117
2448
  }
@@ -2139,12 +2470,13 @@ var KVService = class extends BaseService {
2139
2470
  }
2140
2471
  const path = this.getFullPath(key, options?.prefix);
2141
2472
  const body = this.serializePutValue(value, options?.contentType);
2473
+ const request = this.createRequestSignal(options?.signal, options?.timeout);
2142
2474
  try {
2143
2475
  const response = await this.invokeOperation(
2144
2476
  path,
2145
2477
  KVAction.PUT,
2146
2478
  body,
2147
- options?.signal,
2479
+ request.signal,
2148
2480
  {
2149
2481
  ...options?.ifMatch === void 0 ? {} : { "if-match": options.ifMatch },
2150
2482
  ...options?.ifNoneMatch === void 0 ? {} : { "if-none-match": options.ifNoneMatch }
@@ -2154,7 +2486,9 @@ var KVService = class extends BaseService {
2154
2486
  return this.authorizationFailure(
2155
2487
  `Failed to put key ${JSON.stringify(key)}`,
2156
2488
  response,
2157
- await response.text()
2489
+ await this.readAuthorizationText(response),
2490
+ [path],
2491
+ KVAction.PUT
2158
2492
  );
2159
2493
  }
2160
2494
  if (!response.ok) {
@@ -2198,6 +2532,8 @@ var KVService = class extends BaseService {
2198
2532
  });
2199
2533
  } catch (error) {
2200
2534
  return err(wrapError("kv", error));
2535
+ } finally {
2536
+ request.dispose();
2201
2537
  }
2202
2538
  });
2203
2539
  }
@@ -2237,6 +2573,7 @@ var KVService = class extends BaseService {
2237
2573
  seen.add(path);
2238
2574
  }
2239
2575
  let requestMayHaveDispatched = false;
2576
+ const request = this.createRequestSignal(options?.signal, options?.timeout);
2240
2577
  try {
2241
2578
  const body = new FormData();
2242
2579
  for (let index = 0; index < items.length; index++) {
@@ -2259,7 +2596,7 @@ var KVService = class extends BaseService {
2259
2596
  method: "POST",
2260
2597
  headers,
2261
2598
  body,
2262
- signal: this.combineSignals(options?.signal)
2599
+ signal: request.signal
2263
2600
  };
2264
2601
  const fetchFn = this.context.fetch;
2265
2602
  requestMayHaveDispatched = true;
@@ -2283,7 +2620,9 @@ var KVService = class extends BaseService {
2283
2620
  return this.authorizationFailure(
2284
2621
  `Failed to batch put ${items.length} key(s)`,
2285
2622
  response,
2286
- errorText
2623
+ errorText,
2624
+ paths,
2625
+ KVAction.PUT
2287
2626
  );
2288
2627
  }
2289
2628
  const quotaError = this.handleQuotaErrorResponse(
@@ -2308,14 +2647,16 @@ var KVService = class extends BaseService {
2308
2647
  rawBody = await response.json();
2309
2648
  } catch (jsonError) {
2310
2649
  const cause = jsonError instanceof Error ? jsonError : new Error(String(jsonError));
2650
+ const timedOut = isRequestTimeoutError(cause);
2311
2651
  return err(
2312
2652
  serviceError(
2313
- ErrorCodes.NETWORK_ERROR,
2314
- `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}`,
2315
2655
  "kv",
2316
2656
  {
2317
2657
  cause,
2318
2658
  meta: {
2659
+ ...timedOut ? { timeoutMs: cause.timeoutMs } : {},
2319
2660
  requestMayHaveDispatched: true,
2320
2661
  responseReceived: true,
2321
2662
  status: response.status,
@@ -2370,6 +2711,8 @@ var KVService = class extends BaseService {
2370
2711
  ...wrapped,
2371
2712
  meta: { ...wrapped.meta, requestMayHaveDispatched }
2372
2713
  });
2714
+ } finally {
2715
+ request.dispose();
2373
2716
  }
2374
2717
  });
2375
2718
  }
@@ -2392,23 +2735,26 @@ var KVService = class extends BaseService {
2392
2735
  if (options?.path) {
2393
2736
  listPath = listPath ? `${listPath}/${options.path}` : options.path;
2394
2737
  }
2738
+ const request = this.createRequestSignal(options?.signal, options?.timeout);
2395
2739
  try {
2396
2740
  const response = await this.invokeOperation(
2397
2741
  listPath,
2398
2742
  KVAction.LIST,
2399
2743
  void 0,
2400
- options?.signal,
2744
+ request.signal,
2401
2745
  options?.limit === void 0 && options?.cursor === void 0 ? void 0 : {
2402
2746
  ...options?.limit === void 0 ? {} : { "x-tinycloud-limit": String(options.limit) },
2403
2747
  ...options?.cursor === void 0 ? {} : { "x-tinycloud-cursor": options.cursor }
2404
2748
  }
2405
2749
  );
2406
2750
  if (!response.ok) {
2407
- if (response.status === 401) {
2751
+ if (response.status === 401 || response.status === 403) {
2408
2752
  return this.authorizationFailure(
2409
2753
  "Failed to list keys",
2410
2754
  response,
2411
- await response.text()
2755
+ await this.readAuthorizationText(response),
2756
+ [listPath],
2757
+ KVAction.LIST
2412
2758
  );
2413
2759
  }
2414
2760
  const errorText = await response.text();
@@ -2436,9 +2782,174 @@ var KVService = class extends BaseService {
2436
2782
  });
2437
2783
  } catch (error) {
2438
2784
  return err(wrapError("kv", error));
2785
+ } finally {
2786
+ request.dispose();
2439
2787
  }
2440
2788
  });
2441
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();
2851
+ }
2852
+ });
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
+ }
2442
2953
  /**
2443
2954
  * Delete a key.
2444
2955
  */
@@ -2448,20 +2959,23 @@ var KVService = class extends BaseService {
2448
2959
  return err(authRequiredError("kv"));
2449
2960
  }
2450
2961
  const path = this.getFullPath(key, options?.prefix);
2962
+ const request = this.createRequestSignal(options?.signal, options?.timeout);
2451
2963
  try {
2452
2964
  const response = await this.invokeOperation(
2453
2965
  path,
2454
2966
  KVAction.DELETE,
2455
2967
  void 0,
2456
- options?.signal,
2968
+ request.signal,
2457
2969
  options?.ifMatch === void 0 ? void 0 : { "if-match": options.ifMatch }
2458
2970
  );
2459
2971
  if (!response.ok) {
2460
- if (response.status === 401) {
2972
+ if (response.status === 401 || response.status === 403) {
2461
2973
  return this.authorizationFailure(
2462
2974
  `Failed to delete key ${JSON.stringify(key)}`,
2463
2975
  response,
2464
- await response.text()
2976
+ await this.readAuthorizationText(response),
2977
+ [path],
2978
+ KVAction.DELETE
2465
2979
  );
2466
2980
  }
2467
2981
  if (response.status === 404) {
@@ -2499,6 +3013,8 @@ var KVService = class extends BaseService {
2499
3013
  });
2500
3014
  } catch (error) {
2501
3015
  return err(wrapError("kv", error));
3016
+ } finally {
3017
+ request.dispose();
2502
3018
  }
2503
3019
  });
2504
3020
  }
@@ -2511,19 +3027,22 @@ var KVService = class extends BaseService {
2511
3027
  return err(authRequiredError("kv"));
2512
3028
  }
2513
3029
  const path = this.getFullPath(key, options?.prefix);
3030
+ const request = this.createRequestSignal(options?.signal, options?.timeout);
2514
3031
  try {
2515
3032
  const response = await this.invokeOperation(
2516
3033
  path,
2517
3034
  KVAction.HEAD,
2518
3035
  void 0,
2519
- options?.signal
3036
+ request.signal
2520
3037
  );
2521
3038
  if (!response.ok) {
2522
- if (response.status === 401) {
3039
+ if (response.status === 401 || response.status === 403) {
2523
3040
  return this.authorizationFailure(
2524
3041
  `Failed to get metadata for key ${JSON.stringify(key)}`,
2525
3042
  response,
2526
- await response.text()
3043
+ await this.readAuthorizationText(response),
3044
+ [path],
3045
+ KVAction.HEAD
2527
3046
  );
2528
3047
  }
2529
3048
  if (response.status === 404) {
@@ -2545,6 +3064,8 @@ var KVService = class extends BaseService {
2545
3064
  });
2546
3065
  } catch (error) {
2547
3066
  return err(wrapError("kv", error));
3067
+ } finally {
3068
+ request.dispose();
2548
3069
  }
2549
3070
  });
2550
3071
  }
@@ -2582,15 +3103,16 @@ var KVService = class extends BaseService {
2582
3103
  if (options?.etag !== void 0) {
2583
3104
  body.etag = options.etag;
2584
3105
  }
3106
+ const request = this.createRequestSignal(options?.signal, options?.timeout);
2585
3107
  try {
2586
3108
  const response = await this.context.fetch(`${this.host}/signed/kv`, {
2587
3109
  method: "POST",
2588
3110
  headers: this.withJsonContentType(headers),
2589
3111
  body: JSON.stringify(body),
2590
- signal: this.combineSignals(options?.signal)
3112
+ signal: request.signal
2591
3113
  });
2592
3114
  if (!response.ok) {
2593
- return this.createSignedReadUrlError(response, key);
3115
+ return this.createSignedReadUrlError(response, key, path);
2594
3116
  }
2595
3117
  const signedUrl = this.normalizeSignedReadUrlResponse(
2596
3118
  await response.json()
@@ -2607,6 +3129,8 @@ var KVService = class extends BaseService {
2607
3129
  return ok(signedUrl);
2608
3130
  } catch (error) {
2609
3131
  return err(wrapError("kv", error));
3132
+ } finally {
3133
+ request.dispose();
2610
3134
  }
2611
3135
  });
2612
3136
  }
@@ -2733,16 +3257,16 @@ var DatabaseHandle = class {
2733
3257
  };
2734
3258
 
2735
3259
  // src/sql/types.ts
2736
- var import_bootstrap = require("@tinycloud/bootstrap");
3260
+ var import_bootstrap2 = require("@tinycloud/bootstrap");
2737
3261
  var SQLAction = {
2738
- READ: import_bootstrap.SQL.READ,
2739
- WRITE: import_bootstrap.SQL.WRITE,
2740
- SCHEMA: import_bootstrap.SQL.SCHEMA,
2741
- ADMIN: import_bootstrap.SQL.ADMIN,
2742
- SELECT: import_bootstrap.SQL.SELECT,
2743
- EXECUTE: import_bootstrap.SQL.EXECUTE,
2744
- EXPORT: import_bootstrap.SQL.EXPORT,
2745
- 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
2746
3270
  };
2747
3271
 
2748
3272
  // src/sql/SQLService.ts
@@ -2820,6 +3344,7 @@ var SQLService = class extends BaseService {
2820
3344
  if (!this.requireAuth()) {
2821
3345
  return err(authRequiredError("sql"));
2822
3346
  }
3347
+ const request = this.createRequestSignal(options?.signal);
2823
3348
  try {
2824
3349
  const body = {
2825
3350
  action: "query",
@@ -2828,19 +3353,22 @@ var SQLService = class extends BaseService {
2828
3353
  };
2829
3354
  if (options?.maxRows !== void 0) body.maxRows = options.maxRows;
2830
3355
  if (options?.maxBytes !== void 0) body.maxBytes = options.maxBytes;
3356
+ const action = this.actionForSql(sql, SQLAction.READ);
2831
3357
  const response = await this.invokeSQL(
2832
3358
  dbName,
2833
- this.actionForSql(sql, SQLAction.READ),
3359
+ action,
2834
3360
  body,
2835
- options?.signal
3361
+ request.signal
2836
3362
  );
2837
3363
  if (!response.ok) {
2838
- return this.handleErrorResponse(response, "query");
3364
+ return this.handleErrorResponse(response, "query", dbName, [action]);
2839
3365
  }
2840
3366
  const data = await response.json();
2841
3367
  return ok(data);
2842
3368
  } catch (error) {
2843
3369
  return err(wrapError("sql", error));
3370
+ } finally {
3371
+ request.dispose();
2844
3372
  }
2845
3373
  });
2846
3374
  }
@@ -2849,6 +3377,7 @@ var SQLService = class extends BaseService {
2849
3377
  if (!this.requireAuth()) {
2850
3378
  return err(authRequiredError("sql"));
2851
3379
  }
3380
+ const request = this.createRequestSignal(options?.signal);
2852
3381
  try {
2853
3382
  const body = {
2854
3383
  action: "execute",
@@ -2864,19 +3393,22 @@ var SQLService = class extends BaseService {
2864
3393
  (statement) => this.actionForSql(statement, SQLAction.SCHEMA)
2865
3394
  )
2866
3395
  ];
3396
+ const requestedActions = this.dedupeActions(actions);
2867
3397
  const response = await this.invokeSQL(
2868
3398
  dbName,
2869
- this.dedupeActions(actions),
3399
+ requestedActions,
2870
3400
  body,
2871
- options?.signal
3401
+ request.signal
2872
3402
  );
2873
3403
  if (!response.ok) {
2874
- return this.handleErrorResponse(response, "execute");
3404
+ return this.handleErrorResponse(response, "execute", dbName, requestedActions);
2875
3405
  }
2876
3406
  const data = await response.json();
2877
3407
  return ok(data);
2878
3408
  } catch (error) {
2879
3409
  return err(wrapError("sql", error));
3410
+ } finally {
3411
+ request.dispose();
2880
3412
  }
2881
3413
  });
2882
3414
  }
@@ -2885,20 +3417,24 @@ var SQLService = class extends BaseService {
2885
3417
  if (!this.requireAuth()) {
2886
3418
  return err(authRequiredError("sql"));
2887
3419
  }
3420
+ const request = this.createRequestSignal(options?.signal);
2888
3421
  try {
3422
+ const requestedActions = this.actionsForSqlBatch(statements);
2889
3423
  const response = await this.invokeSQL(
2890
3424
  dbName,
2891
- this.actionsForSqlBatch(statements),
3425
+ requestedActions,
2892
3426
  { action: "batch", statements },
2893
- options?.signal
3427
+ request.signal
2894
3428
  );
2895
3429
  if (!response.ok) {
2896
- return this.handleErrorResponse(response, "batch");
3430
+ return this.handleErrorResponse(response, "batch", dbName, requestedActions);
2897
3431
  }
2898
3432
  const data = await response.json();
2899
3433
  return ok(data);
2900
3434
  } catch (error) {
2901
3435
  return err(wrapError("sql", error));
3436
+ } finally {
3437
+ request.dispose();
2902
3438
  }
2903
3439
  });
2904
3440
  }
@@ -2907,20 +3443,23 @@ var SQLService = class extends BaseService {
2907
3443
  if (!this.requireAuth()) {
2908
3444
  return err(authRequiredError("sql"));
2909
3445
  }
3446
+ const request = this.createRequestSignal(options?.signal);
2910
3447
  try {
2911
3448
  const response = await this.invokeSQL(
2912
3449
  dbName,
2913
3450
  SQLAction.WRITE,
2914
3451
  { action: "execute_statement", name, params: params ?? [] },
2915
- options?.signal
3452
+ request.signal
2916
3453
  );
2917
3454
  if (!response.ok) {
2918
- return this.handleErrorResponse(response, "executeStatement");
3455
+ return this.handleErrorResponse(response, "executeStatement", dbName, [SQLAction.WRITE]);
2919
3456
  }
2920
3457
  const data = await response.json();
2921
3458
  return ok(data);
2922
3459
  } catch (error) {
2923
3460
  return err(wrapError("sql", error));
3461
+ } finally {
3462
+ request.dispose();
2924
3463
  }
2925
3464
  });
2926
3465
  }
@@ -2929,15 +3468,16 @@ var SQLService = class extends BaseService {
2929
3468
  if (!this.requireAuth()) {
2930
3469
  return err(authRequiredError("sql"));
2931
3470
  }
3471
+ const request = this.createRequestSignal(options?.signal);
2932
3472
  try {
2933
3473
  const response = await this.invokeSQL(
2934
3474
  dbName,
2935
3475
  SQLAction.READ,
2936
3476
  { action: "export" },
2937
- options?.signal
3477
+ request.signal
2938
3478
  );
2939
3479
  if (!response.ok) {
2940
- return this.handleErrorResponse(response, "export");
3480
+ return this.handleErrorResponse(response, "export", dbName, [SQLAction.READ]);
2941
3481
  }
2942
3482
  const resp = response;
2943
3483
  if (typeof resp.blob === "function") {
@@ -2948,26 +3488,31 @@ var SQLService = class extends BaseService {
2948
3488
  return ok(text);
2949
3489
  } catch (error) {
2950
3490
  return err(wrapError("sql", error));
3491
+ } finally {
3492
+ request.dispose();
2951
3493
  }
2952
3494
  });
2953
3495
  }
2954
3496
  async applyMigrationsOnDbUnlocked(dbName, options) {
2955
3497
  return this.withTelemetry("migrations.apply", dbName, async () => {
2956
- const created = await this.ensureMigrationsTable(dbName, options.signal);
2957
- if (!created.ok) return created;
2958
3498
  const listed = await this.queryOnDb(
2959
3499
  dbName,
2960
3500
  `SELECT id FROM ${MIGRATIONS_TABLE} WHERE namespace = ? ORDER BY applied_at, id`,
2961
3501
  [options.namespace],
2962
3502
  { signal: options.signal }
2963
3503
  );
2964
- 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;
2965
3506
  const appliedIds = new Set(
2966
- 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")
2967
3508
  );
2968
3509
  const skipped = options.migrations.filter((migration) => appliedIds.has(migration.id)).map((migration) => migration.id);
2969
3510
  const pending = options.migrations.filter((migration) => !appliedIds.has(migration.id));
2970
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
+ }
2971
3516
  for (const migration of pending) {
2972
3517
  const result = await this.applyOneMigration(dbName, options.namespace, migration, options.signal);
2973
3518
  if (!result.ok) return result;
@@ -3035,7 +3580,7 @@ var SQLService = class extends BaseService {
3035
3580
  "Content-Type": "application/json"
3036
3581
  },
3037
3582
  body: JSON.stringify(body),
3038
- signal: this.combineSignals(signal)
3583
+ signal
3039
3584
  });
3040
3585
  }
3041
3586
  actionForSql(sql, fallback) {
@@ -3068,8 +3613,12 @@ var SQLService = class extends BaseService {
3068
3613
  }))
3069
3614
  );
3070
3615
  }
3071
- async handleErrorResponse(response, operation) {
3616
+ async handleErrorResponse(response, operation, dbName, requestedActions) {
3072
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
+ }
3073
3622
  const errorBody = parseServiceErrorBody(errorText);
3074
3623
  const errorCode = this.mapHttpStatusToErrorCode(
3075
3624
  response.status,
@@ -3082,11 +3631,12 @@ var SQLService = class extends BaseService {
3082
3631
  errorText,
3083
3632
  errorBody
3084
3633
  );
3085
- const meta = responseErrorMeta(response.status, response.statusText, errorText);
3086
3634
  if (response.status === 401) {
3087
- const { resource, action } = parseAuthError(errorText);
3088
- if (action) meta.requiredAction = action;
3089
- 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
+ }
3090
3640
  }
3091
3641
  return err(
3092
3642
  serviceError(errorCode, message, "sql", { meta })
@@ -3183,6 +3733,9 @@ function serializeSqlValues(values) {
3183
3733
  (value) => value instanceof Uint8Array ? Array.from(value) : value
3184
3734
  );
3185
3735
  }
3736
+ function isMissingTableError(message) {
3737
+ return /no such table/i.test(message);
3738
+ }
3186
3739
  function migrationKey(namespace, id) {
3187
3740
  return `${encodeURIComponent(namespace)}:${encodeURIComponent(id)}`;
3188
3741
  }
@@ -3228,16 +3781,16 @@ var DuckDbDatabaseHandle = class {
3228
3781
  };
3229
3782
 
3230
3783
  // src/duckdb/types.ts
3231
- var import_bootstrap2 = require("@tinycloud/bootstrap");
3784
+ var import_bootstrap3 = require("@tinycloud/bootstrap");
3232
3785
  var DuckDbAction = {
3233
- READ: import_bootstrap2.DUCKDB.READ,
3234
- WRITE: import_bootstrap2.DUCKDB.WRITE,
3235
- ADMIN: import_bootstrap2.DUCKDB.ADMIN,
3236
- DESCRIBE: import_bootstrap2.DUCKDB.DESCRIBE,
3237
- EXPORT: import_bootstrap2.DUCKDB.EXPORT,
3238
- IMPORT: import_bootstrap2.DUCKDB.IMPORT,
3239
- EXECUTE: import_bootstrap2.DUCKDB.EXECUTE,
3240
- 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
3241
3794
  };
3242
3795
 
3243
3796
  // src/duckdb/DuckDbService.ts
@@ -3291,12 +3844,13 @@ var DuckDbService = class extends BaseService {
3291
3844
  if (!this.requireAuth()) {
3292
3845
  return err(authRequiredError("duckdb"));
3293
3846
  }
3847
+ const request = this.createRequestSignal(options?.signal);
3294
3848
  try {
3295
3849
  const response = await this.invokeDuckDb(
3296
3850
  dbName,
3297
3851
  DuckDbAction.READ,
3298
3852
  { action: "query", sql, params: params ?? [] },
3299
- options?.signal
3853
+ request.signal
3300
3854
  );
3301
3855
  if (!response.ok) {
3302
3856
  return this.handleErrorResponse(response, "query");
@@ -3305,6 +3859,8 @@ var DuckDbService = class extends BaseService {
3305
3859
  return ok(data);
3306
3860
  } catch (error) {
3307
3861
  return err(wrapError("duckdb", error));
3862
+ } finally {
3863
+ request.dispose();
3308
3864
  }
3309
3865
  });
3310
3866
  }
@@ -3313,12 +3869,13 @@ var DuckDbService = class extends BaseService {
3313
3869
  if (!this.requireAuth()) {
3314
3870
  return err(authRequiredError("duckdb"));
3315
3871
  }
3872
+ const request = this.createRequestSignal(options?.signal);
3316
3873
  try {
3317
3874
  const response = await this.invokeDuckDb(
3318
3875
  dbName,
3319
3876
  DuckDbAction.READ,
3320
3877
  { action: "query", sql, params: params ?? [] },
3321
- options?.signal,
3878
+ request.signal,
3322
3879
  { Accept: "application/vnd.apache.arrow.stream" }
3323
3880
  );
3324
3881
  if (!response.ok) {
@@ -3328,6 +3885,8 @@ var DuckDbService = class extends BaseService {
3328
3885
  return ok(buffer);
3329
3886
  } catch (error) {
3330
3887
  return err(wrapError("duckdb", error));
3888
+ } finally {
3889
+ request.dispose();
3331
3890
  }
3332
3891
  });
3333
3892
  }
@@ -3336,6 +3895,7 @@ var DuckDbService = class extends BaseService {
3336
3895
  if (!this.requireAuth()) {
3337
3896
  return err(authRequiredError("duckdb"));
3338
3897
  }
3898
+ const request = this.createRequestSignal(options?.signal);
3339
3899
  try {
3340
3900
  const body = {
3341
3901
  action: "execute",
@@ -3349,7 +3909,7 @@ var DuckDbService = class extends BaseService {
3349
3909
  dbName,
3350
3910
  DuckDbAction.WRITE,
3351
3911
  body,
3352
- options?.signal
3912
+ request.signal
3353
3913
  );
3354
3914
  if (!response.ok) {
3355
3915
  return this.handleErrorResponse(response, "execute");
@@ -3358,6 +3918,8 @@ var DuckDbService = class extends BaseService {
3358
3918
  return ok(data);
3359
3919
  } catch (error) {
3360
3920
  return err(wrapError("duckdb", error));
3921
+ } finally {
3922
+ request.dispose();
3361
3923
  }
3362
3924
  });
3363
3925
  }
@@ -3366,6 +3928,7 @@ var DuckDbService = class extends BaseService {
3366
3928
  if (!this.requireAuth()) {
3367
3929
  return err(authRequiredError("duckdb"));
3368
3930
  }
3931
+ const request = this.createRequestSignal(options?.signal);
3369
3932
  try {
3370
3933
  const body = {
3371
3934
  action: "batch",
@@ -3378,7 +3941,7 @@ var DuckDbService = class extends BaseService {
3378
3941
  dbName,
3379
3942
  DuckDbAction.WRITE,
3380
3943
  body,
3381
- options?.signal
3944
+ request.signal
3382
3945
  );
3383
3946
  if (!response.ok) {
3384
3947
  return this.handleErrorResponse(response, "batch");
@@ -3387,6 +3950,8 @@ var DuckDbService = class extends BaseService {
3387
3950
  return ok(data);
3388
3951
  } catch (error) {
3389
3952
  return err(wrapError("duckdb", error));
3953
+ } finally {
3954
+ request.dispose();
3390
3955
  }
3391
3956
  });
3392
3957
  }
@@ -3395,12 +3960,13 @@ var DuckDbService = class extends BaseService {
3395
3960
  if (!this.requireAuth()) {
3396
3961
  return err(authRequiredError("duckdb"));
3397
3962
  }
3963
+ const request = this.createRequestSignal(options?.signal);
3398
3964
  try {
3399
3965
  const response = await this.invokeDuckDb(
3400
3966
  dbName,
3401
3967
  DuckDbAction.WRITE,
3402
3968
  { action: "executeStatement", name, params: params ?? [] },
3403
- options?.signal
3969
+ request.signal
3404
3970
  );
3405
3971
  if (!response.ok) {
3406
3972
  return this.handleErrorResponse(response, "executeStatement");
@@ -3409,6 +3975,8 @@ var DuckDbService = class extends BaseService {
3409
3975
  return ok(data);
3410
3976
  } catch (error) {
3411
3977
  return err(wrapError("duckdb", error));
3978
+ } finally {
3979
+ request.dispose();
3412
3980
  }
3413
3981
  });
3414
3982
  }
@@ -3417,12 +3985,13 @@ var DuckDbService = class extends BaseService {
3417
3985
  if (!this.requireAuth()) {
3418
3986
  return err(authRequiredError("duckdb"));
3419
3987
  }
3988
+ const request = this.createRequestSignal(options?.signal);
3420
3989
  try {
3421
3990
  const response = await this.invokeDuckDb(
3422
3991
  dbName,
3423
3992
  DuckDbAction.READ,
3424
3993
  { action: "describe" },
3425
- options?.signal
3994
+ request.signal
3426
3995
  );
3427
3996
  if (!response.ok) {
3428
3997
  return this.handleErrorResponse(response, "describe");
@@ -3431,6 +4000,8 @@ var DuckDbService = class extends BaseService {
3431
4000
  return ok(data);
3432
4001
  } catch (error) {
3433
4002
  return err(wrapError("duckdb", error));
4003
+ } finally {
4004
+ request.dispose();
3434
4005
  }
3435
4006
  });
3436
4007
  }
@@ -3439,12 +4010,13 @@ var DuckDbService = class extends BaseService {
3439
4010
  if (!this.requireAuth()) {
3440
4011
  return err(authRequiredError("duckdb"));
3441
4012
  }
4013
+ const request = this.createRequestSignal(options?.signal);
3442
4014
  try {
3443
4015
  const response = await this.invokeDuckDb(
3444
4016
  dbName,
3445
4017
  DuckDbAction.EXPORT,
3446
4018
  { action: "export" },
3447
- options?.signal
4019
+ request.signal
3448
4020
  );
3449
4021
  if (!response.ok) {
3450
4022
  return this.handleErrorResponse(response, "export");
@@ -3453,6 +4025,8 @@ var DuckDbService = class extends BaseService {
3453
4025
  return ok(blob);
3454
4026
  } catch (error) {
3455
4027
  return err(wrapError("duckdb", error));
4028
+ } finally {
4029
+ request.dispose();
3456
4030
  }
3457
4031
  });
3458
4032
  }
@@ -3461,6 +4035,7 @@ var DuckDbService = class extends BaseService {
3461
4035
  if (!this.requireAuth()) {
3462
4036
  return err(authRequiredError("duckdb"));
3463
4037
  }
4038
+ const request = this.createRequestSignal(options?.signal);
3464
4039
  try {
3465
4040
  const session = this.context.session;
3466
4041
  const headers = this.context.invoke(
@@ -3476,7 +4051,7 @@ var DuckDbService = class extends BaseService {
3476
4051
  "Content-Type": "application/x-duckdb"
3477
4052
  },
3478
4053
  body: new Blob([data]),
3479
- signal: this.combineSignals(options?.signal)
4054
+ signal: request.signal
3480
4055
  });
3481
4056
  if (!response.ok) {
3482
4057
  return this.handleErrorResponse(response, "import");
@@ -3484,6 +4059,8 @@ var DuckDbService = class extends BaseService {
3484
4059
  return ok(void 0);
3485
4060
  } catch (error) {
3486
4061
  return err(wrapError("duckdb", error));
4062
+ } finally {
4063
+ request.dispose();
3487
4064
  }
3488
4065
  });
3489
4066
  }
@@ -3499,11 +4076,15 @@ var DuckDbService = class extends BaseService {
3499
4076
  ...extraHeaders
3500
4077
  },
3501
4078
  body: JSON.stringify(body),
3502
- signal: this.combineSignals(signal)
4079
+ signal
3503
4080
  });
3504
4081
  }
3505
4082
  async handleErrorResponse(response, operation) {
3506
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
+ }
3507
4088
  const errorBody = parseServiceErrorBody(errorText);
3508
4089
  const errorCode = this.mapHttpStatusToErrorCode(
3509
4090
  response.status,
@@ -3516,7 +4097,6 @@ var DuckDbService = class extends BaseService {
3516
4097
  errorText,
3517
4098
  errorBody
3518
4099
  );
3519
- const meta = responseErrorMeta(response.status, response.statusText, errorText);
3520
4100
  if (response.status === 401) {
3521
4101
  const { resource, action } = parseAuthError(errorText);
3522
4102
  if (action) meta.requiredAction = action;
@@ -4488,6 +5068,9 @@ function defaultVaultMessage(input) {
4488
5068
  return input.message ?? `Public key not found for ${input.did}`;
4489
5069
  case "STORAGE_ERROR":
4490
5070
  return input.message ?? input.cause.message;
5071
+ case "STORAGE_QUOTA_EXCEEDED":
5072
+ case "STORAGE_LIMIT_REACHED":
5073
+ return input.message;
4491
5074
  }
4492
5075
  }
4493
5076
  function vaultError(input) {
@@ -4498,6 +5081,15 @@ function vaultError(input) {
4498
5081
  };
4499
5082
  return { ok: false, error };
4500
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
+ }
4501
5093
  var DataVaultService = class extends BaseService {
4502
5094
  /**
4503
5095
  * Create a new DataVaultService instance.
@@ -4780,12 +5372,7 @@ var DataVaultService = class extends BaseService {
4780
5372
  JSON.stringify(envelopeResult.data)
4781
5373
  );
4782
5374
  if (!valuePutResult.ok) {
4783
- return vaultError({
4784
- code: "STORAGE_ERROR",
4785
- cause: new Error(
4786
- `Failed to store encrypted value: ${valuePutResult.error.message}`
4787
- )
4788
- });
5375
+ return kvWriteError("Failed to store encrypted value", valuePutResult.error);
4789
5376
  }
4790
5377
  return { ok: true, data: void 0 };
4791
5378
  } catch (error) {
@@ -5003,12 +5590,7 @@ var DataVaultService = class extends BaseService {
5003
5590
  keyPayload
5004
5591
  );
5005
5592
  if (!keyPutResult.ok) {
5006
- return vaultError({
5007
- code: "STORAGE_ERROR",
5008
- cause: new Error(
5009
- `Failed to store key blob: ${keyPutResult.error.message}`
5010
- )
5011
- });
5593
+ return kvWriteError("Failed to store key blob", keyPutResult.error);
5012
5594
  }
5013
5595
  const valuePayload = JSON.stringify({
5014
5596
  data: base64Encode(encrypted),
@@ -5019,12 +5601,7 @@ var DataVaultService = class extends BaseService {
5019
5601
  valuePayload
5020
5602
  );
5021
5603
  if (!valuePutResult.ok) {
5022
- return vaultError({
5023
- code: "STORAGE_ERROR",
5024
- cause: new Error(
5025
- `Failed to store encrypted value: ${valuePutResult.error.message}`
5026
- )
5027
- });
5604
+ return kvWriteError("Failed to store encrypted value", valuePutResult.error);
5028
5605
  }
5029
5606
  return ok(void 0);
5030
5607
  } catch (error) {
@@ -5355,12 +5932,7 @@ var DataVaultService = class extends BaseService {
5355
5932
  grantPayload
5356
5933
  );
5357
5934
  if (!grantPutResult.ok) {
5358
- return vaultError({
5359
- code: "STORAGE_ERROR",
5360
- cause: new Error(
5361
- `Failed to store grant: ${grantPutResult.error.message}`
5362
- )
5363
- });
5935
+ return kvWriteError("Failed to store grant", grantPutResult.error);
5364
5936
  }
5365
5937
  return ok(void 0);
5366
5938
  } catch (error) {
@@ -5705,12 +6277,7 @@ var DataVaultService = class extends BaseService {
5705
6277
  keyPayload
5706
6278
  );
5707
6279
  if (!keyPutResult.ok) {
5708
- return vaultError({
5709
- code: "STORAGE_ERROR",
5710
- cause: new Error(
5711
- `Failed to store rotated key blob: ${keyPutResult.error.message}`
5712
- )
5713
- });
6280
+ return kvWriteError("Failed to store rotated key blob", keyPutResult.error);
5714
6281
  }
5715
6282
  const valuePayload = JSON.stringify({
5716
6283
  data: base64Encode(encrypted),
@@ -5721,12 +6288,7 @@ var DataVaultService = class extends BaseService {
5721
6288
  valuePayload
5722
6289
  );
5723
6290
  if (!valuePutResult.ok) {
5724
- return vaultError({
5725
- code: "STORAGE_ERROR",
5726
- cause: new Error(
5727
- `Failed to store re-encrypted value: ${valuePutResult.error.message}`
5728
- )
5729
- });
6291
+ return kvWriteError("Failed to store re-encrypted value", valuePutResult.error);
5730
6292
  }
5731
6293
  for (const did of remainingGrantees) {
5732
6294
  const grantResult = await this.reencrypt(key, did);
@@ -5935,6 +6497,7 @@ var SecretsService = class {
5935
6497
  }
5936
6498
  async listAll() {
5937
6499
  const entries = /* @__PURE__ */ new Map();
6500
+ const seenCursors = /* @__PURE__ */ new Set();
5938
6501
  let cursor;
5939
6502
  do {
5940
6503
  const result = await this.vault.listPage({
@@ -5957,6 +6520,14 @@ var SecretsService = class {
5957
6520
  "Secret catalog listing was truncated without a continuation cursor."
5958
6521
  );
5959
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
+ }
5960
6531
  } while (cursor !== void 0);
5961
6532
  return {
5962
6533
  ok: true,
@@ -6077,103 +6648,6 @@ function canonicalHashHex(sha256, value) {
6077
6648
  return hexEncode2(sha256(utf8Encode(canonical)));
6078
6649
  }
6079
6650
 
6080
- // src/encryption/networkId.ts
6081
- var URN_PREFIX = "urn:tinycloud:encryption:";
6082
- var NETWORK_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
6083
- var PKH_EIP155_DID_RE = /^did:pkh:eip155:(\d+):(0x[a-fA-F0-9]{40})$/;
6084
- var NetworkIdError = class extends Error {
6085
- constructor(message) {
6086
- super(message);
6087
- this.name = "NetworkIdError";
6088
- }
6089
- };
6090
- function parseNetworkId(networkId) {
6091
- if (typeof networkId !== "string" || networkId.length === 0) {
6092
- throw new NetworkIdError("networkId must be a non-empty string");
6093
- }
6094
- if (!networkId.startsWith(URN_PREFIX)) {
6095
- throw new NetworkIdError(
6096
- `networkId must start with ${URN_PREFIX} (got ${JSON.stringify(networkId)})`
6097
- );
6098
- }
6099
- const body = networkId.slice(URN_PREFIX.length);
6100
- const lastColon = body.lastIndexOf(":");
6101
- if (lastColon <= 0 || lastColon === body.length - 1) {
6102
- throw new NetworkIdError(
6103
- `networkId missing ownerDid or name segment (got ${JSON.stringify(networkId)})`
6104
- );
6105
- }
6106
- const ownerDid = body.slice(0, lastColon);
6107
- const name = body.slice(lastColon + 1);
6108
- if (!ownerDid.startsWith("did:")) {
6109
- throw new NetworkIdError(
6110
- `networkId ownerDid must be a DID (got ${JSON.stringify(ownerDid)})`
6111
- );
6112
- }
6113
- const didParts = ownerDid.split(":");
6114
- if (didParts.length < 3 || didParts.some((p) => p.length === 0)) {
6115
- throw new NetworkIdError(
6116
- `networkId ownerDid is not a well-formed DID (got ${JSON.stringify(ownerDid)})`
6117
- );
6118
- }
6119
- if (!NETWORK_NAME_RE.test(name)) {
6120
- throw new NetworkIdError(
6121
- `networkId name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
6122
- );
6123
- }
6124
- return { networkId, ownerDid, name };
6125
- }
6126
- function buildNetworkId(ownerDid, name) {
6127
- if (typeof ownerDid !== "string" || !ownerDid.startsWith("did:")) {
6128
- throw new NetworkIdError("ownerDid must be a DID");
6129
- }
6130
- if (typeof name !== "string" || !NETWORK_NAME_RE.test(name)) {
6131
- throw new NetworkIdError(
6132
- `network name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
6133
- );
6134
- }
6135
- const networkId = `${URN_PREFIX}${ownerDid}:${name}`;
6136
- parseNetworkId(networkId);
6137
- return networkId;
6138
- }
6139
- function isNetworkId(networkId) {
6140
- if (typeof networkId !== "string") {
6141
- return false;
6142
- }
6143
- try {
6144
- parseNetworkId(networkId);
6145
- return true;
6146
- } catch {
6147
- return false;
6148
- }
6149
- }
6150
- function parsePkhOwnerDid(ownerDid) {
6151
- const match = ownerDid.match(PKH_EIP155_DID_RE);
6152
- if (!match) return null;
6153
- return {
6154
- chainId: match[1],
6155
- address: match[2].toLowerCase()
6156
- };
6157
- }
6158
- function ownerDidMatches(a, b) {
6159
- const aPkh = parsePkhOwnerDid(a);
6160
- const bPkh = parsePkhOwnerDid(b);
6161
- if (aPkh && bPkh) {
6162
- return aPkh.chainId === bPkh.chainId && aPkh.address === bPkh.address;
6163
- }
6164
- return a === b;
6165
- }
6166
- function networkDiscoveryKey(name) {
6167
- if (!NETWORK_NAME_RE.test(name)) {
6168
- throw new NetworkIdError(
6169
- `network name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
6170
- );
6171
- }
6172
- return `.well-known/encryption/network/${name}`;
6173
- }
6174
- var ENCRYPTION_NETWORK_URN_PREFIX = URN_PREFIX;
6175
- var NETWORK_NAME_PATTERN = NETWORK_NAME_RE;
6176
-
6177
6651
  // src/encryption/types.ts
6178
6652
  var DEFAULT_ENCRYPTION_ALG = "x25519-aes256gcm/v1";
6179
6653
  var ENVELOPE_VERSION = 1;
@@ -7056,6 +7530,8 @@ var import_decrypt_transport_response_error3 = require("@tinycloud/sdk-services/
7056
7530
  SQLAction,
7057
7531
  SQLMigrations,
7058
7532
  SQLService,
7533
+ STORAGE_FULL_MESSAGE,
7534
+ STORAGE_WRITE_TOO_LARGE_MESSAGE,
7059
7535
  SecretsService,
7060
7536
  ServiceContext,
7061
7537
  ServiceErrorEventSchema,
@@ -7108,22 +7584,26 @@ var import_decrypt_transport_response_error3 = require("@tinycloud/sdk-services/
7108
7584
  hexEncode,
7109
7585
  installTinyCloudDebugGlobals,
7110
7586
  isNetworkId,
7587
+ isStorageFullError,
7111
7588
  networkDiscoveryKey,
7112
7589
  networkError,
7113
7590
  notFoundError,
7114
7591
  ok,
7115
7592
  openWrappedKey,
7116
7593
  parseAuthError,
7594
+ parseCapabilityResource,
7117
7595
  parseNetworkId,
7118
7596
  parsePermissionHint,
7119
7597
  parsePermissionHintFromErrorText,
7120
7598
  parseSecretCatalogKey,
7599
+ parseStorageQuotaBytes,
7121
7600
  permissionDeniedError,
7122
7601
  resolveSecretListPrefix,
7123
7602
  resolveSecretPath,
7124
7603
  serviceError,
7125
7604
  storageLimitReachedError,
7126
7605
  storageQuotaExceededError,
7606
+ storageRejectionError,
7127
7607
  timeoutError,
7128
7608
  tinyCloudDebugLogger,
7129
7609
  utf8Decode,
@@ -7137,6 +7617,7 @@ var import_decrypt_transport_response_error3 = require("@tinycloud/sdk-services/
7137
7617
  validateServiceResponseEvent,
7138
7618
  validateServiceSession,
7139
7619
  validateTelemetrySpanEvent,
7620
+ validatedCapabilityOf,
7140
7621
  verifyDecryptResponse,
7141
7622
  wrapError
7142
7623
  });