@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.js CHANGED
@@ -5,6 +5,10 @@ var ErrorCodes = {
5
5
  AUTH_EXPIRED: "AUTH_EXPIRED",
6
6
  AUTH_REQUIRED: "AUTH_REQUIRED",
7
7
  AUTH_UNAUTHORIZED: "AUTH_UNAUTHORIZED",
8
+ /** HTTP 401: the delegation the invocation cites was revoked. */
9
+ AUTH_DELEGATION_REVOKED: "AUTH_DELEGATION_REVOKED",
10
+ /** HTTP 401: a delegation the cited one rests on was revoked. */
11
+ AUTH_DELEGATION_ANCESTOR_REVOKED: "AUTH_DELEGATION_ANCESTOR_REVOKED",
8
12
  NETWORK_ERROR: "NETWORK_ERROR",
9
13
  TIMEOUT: "TIMEOUT",
10
14
  ABORTED: "ABORTED",
@@ -16,6 +20,10 @@ var ErrorCodes = {
16
20
  KV_PRECONDITION_FAILED: "KV_PRECONDITION_FAILED",
17
21
  KV_CONFLICT: "KV_CONFLICT",
18
22
  KV_RESPONSE_TOO_LARGE: "KV_RESPONSE_TOO_LARGE",
23
+ /** HTTP 410 from the change feed: discard sync state and restart without a cursor; `meta.reason` says why. */
24
+ KV_SYNC_RESET_REQUIRED: "KV_SYNC_RESET_REQUIRED",
25
+ /** HTTP 403 from the change feed: the retention grant was refused; `meta.reason` says why. */
26
+ KV_RETENTION_GRANT_REFUSED: "KV_RETENTION_GRANT_REFUSED",
19
27
  // SQL-specific errors
20
28
  SQL_ERROR: "SQL_ERROR",
21
29
  SQL_PERMISSION_DENIED: "SQL_PERMISSION_DENIED",
@@ -896,6 +904,106 @@ var ServiceContext = class {
896
904
  }
897
905
  };
898
906
 
907
+ // src/errors.ts
908
+ import { CAPABILITY_REGISTRY } from "@tinycloud/bootstrap";
909
+
910
+ // src/encryption/networkId.ts
911
+ var URN_PREFIX = "urn:tinycloud:encryption:";
912
+ var NETWORK_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
913
+ var PKH_EIP155_DID_RE = /^did:pkh:eip155:(\d+):(0x[a-fA-F0-9]{40})$/;
914
+ var NetworkIdError = class extends Error {
915
+ constructor(message) {
916
+ super(message);
917
+ this.name = "NetworkIdError";
918
+ }
919
+ };
920
+ function parseNetworkId(networkId) {
921
+ if (typeof networkId !== "string" || networkId.length === 0) {
922
+ throw new NetworkIdError("networkId must be a non-empty string");
923
+ }
924
+ if (!networkId.startsWith(URN_PREFIX)) {
925
+ throw new NetworkIdError(
926
+ `networkId must start with ${URN_PREFIX} (got ${JSON.stringify(networkId)})`
927
+ );
928
+ }
929
+ const body = networkId.slice(URN_PREFIX.length);
930
+ const lastColon = body.lastIndexOf(":");
931
+ if (lastColon <= 0 || lastColon === body.length - 1) {
932
+ throw new NetworkIdError(
933
+ `networkId missing ownerDid or name segment (got ${JSON.stringify(networkId)})`
934
+ );
935
+ }
936
+ const ownerDid = body.slice(0, lastColon);
937
+ const name = body.slice(lastColon + 1);
938
+ if (!ownerDid.startsWith("did:")) {
939
+ throw new NetworkIdError(
940
+ `networkId ownerDid must be a DID (got ${JSON.stringify(ownerDid)})`
941
+ );
942
+ }
943
+ const didParts = ownerDid.split(":");
944
+ if (didParts.length < 3 || didParts.some((p) => p.length === 0)) {
945
+ throw new NetworkIdError(
946
+ `networkId ownerDid is not a well-formed DID (got ${JSON.stringify(ownerDid)})`
947
+ );
948
+ }
949
+ if (!NETWORK_NAME_RE.test(name)) {
950
+ throw new NetworkIdError(
951
+ `networkId name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
952
+ );
953
+ }
954
+ return { networkId, ownerDid, name };
955
+ }
956
+ function buildNetworkId(ownerDid, name) {
957
+ if (typeof ownerDid !== "string" || !ownerDid.startsWith("did:")) {
958
+ throw new NetworkIdError("ownerDid must be a DID");
959
+ }
960
+ if (typeof name !== "string" || !NETWORK_NAME_RE.test(name)) {
961
+ throw new NetworkIdError(
962
+ `network name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
963
+ );
964
+ }
965
+ const networkId = `${URN_PREFIX}${ownerDid}:${name}`;
966
+ parseNetworkId(networkId);
967
+ return networkId;
968
+ }
969
+ function isNetworkId(networkId) {
970
+ if (typeof networkId !== "string") {
971
+ return false;
972
+ }
973
+ try {
974
+ parseNetworkId(networkId);
975
+ return true;
976
+ } catch {
977
+ return false;
978
+ }
979
+ }
980
+ function parsePkhOwnerDid(ownerDid) {
981
+ const match = ownerDid.match(PKH_EIP155_DID_RE);
982
+ if (!match) return null;
983
+ return {
984
+ chainId: match[1],
985
+ address: match[2].toLowerCase()
986
+ };
987
+ }
988
+ function ownerDidMatches(a, b) {
989
+ const aPkh = parsePkhOwnerDid(a);
990
+ const bPkh = parsePkhOwnerDid(b);
991
+ if (aPkh && bPkh) {
992
+ return aPkh.chainId === bPkh.chainId && aPkh.address === bPkh.address;
993
+ }
994
+ return a === b;
995
+ }
996
+ function networkDiscoveryKey(name) {
997
+ if (!NETWORK_NAME_RE.test(name)) {
998
+ throw new NetworkIdError(
999
+ `network name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
1000
+ );
1001
+ }
1002
+ return `.well-known/encryption/network/${name}`;
1003
+ }
1004
+ var ENCRYPTION_NETWORK_URN_PREFIX = URN_PREFIX;
1005
+ var NETWORK_NAME_PATTERN = NETWORK_NAME_RE;
1006
+
899
1007
  // src/errors.ts
900
1008
  function parsePermissionHint(value) {
901
1009
  if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
@@ -955,11 +1063,31 @@ function networkError(service, message, cause) {
955
1063
  cause
956
1064
  };
957
1065
  }
958
- function timeoutError(service) {
1066
+ var RequestTimeoutError = class extends Error {
1067
+ constructor(timeoutMs) {
1068
+ super(`Request timed out after ${timeoutMs}ms.`);
1069
+ this.name = "TimeoutError";
1070
+ this.timeoutMs = timeoutMs;
1071
+ }
1072
+ };
1073
+ function isRequestTimeoutError(error) {
1074
+ return error instanceof RequestTimeoutError || error instanceof Error && error.name === "TimeoutError" && typeof error.timeoutMs === "number";
1075
+ }
1076
+ function timeoutError(service, timeoutMs, cause) {
1077
+ if (timeoutMs === void 0) {
1078
+ return {
1079
+ code: ErrorCodes.TIMEOUT,
1080
+ message: "Request timed out.",
1081
+ service,
1082
+ ...cause === void 0 ? {} : { cause }
1083
+ };
1084
+ }
959
1085
  return {
960
1086
  code: ErrorCodes.TIMEOUT,
961
- message: "Request timed out.",
962
- service
1087
+ message: `Request timed out after ${timeoutMs}ms.`,
1088
+ service,
1089
+ ...cause === void 0 ? {} : { cause },
1090
+ meta: { timeoutMs }
963
1091
  };
964
1092
  }
965
1093
  function abortedError(service) {
@@ -993,6 +1121,70 @@ function parseAuthError(responseText) {
993
1121
  function authUnauthorizedError(service, message, meta) {
994
1122
  return serviceError(ErrorCodes.AUTH_UNAUTHORIZED, message, service, { meta });
995
1123
  }
1124
+ var grantableActions = new Set(
1125
+ CAPABILITY_REGISTRY.map(({ urn }) => urn).filter((urn) => !urn.endsWith("/*"))
1126
+ );
1127
+ var capabilityServices = new Set(
1128
+ CAPABILITY_REGISTRY.map(({ service }) => service.slice("tinycloud.".length))
1129
+ );
1130
+ function parseCapabilityResource(resource, service) {
1131
+ if (resource.length === 0 || resource.length > 1024 || !capabilityServices.has(service) || /[\s\p{C}*#]/u.test(resource) || resource.includes("://")) return void 0;
1132
+ const segments = resource.split("/");
1133
+ if (!/^tinycloud:[A-Za-z0-9][A-Za-z0-9:._-]*$/.test(segments[0])) return void 0;
1134
+ const serviceIndex = segments.findIndex(
1135
+ (segment, index) => index > 0 && capabilityServices.has(segment)
1136
+ );
1137
+ if (serviceIndex < 1 || segments[serviceIndex] !== service) return void 0;
1138
+ if (segments.slice(1, serviceIndex).some(
1139
+ (segment) => segment.length === 0 || segment === "." || segment === ".."
1140
+ )) return void 0;
1141
+ const pathSegments = segments.slice(serviceIndex + 1);
1142
+ if (pathSegments.some((segment) => segment === "." || segment === "..")) return void 0;
1143
+ if (service !== "kv" && (pathSegments.length === 0 || pathSegments[pathSegments.length - 1] === "")) return void 0;
1144
+ return {
1145
+ space: segments.slice(0, serviceIndex).join("/"),
1146
+ path: pathSegments.join("/")
1147
+ };
1148
+ }
1149
+ function safeCapabilityResource(value, service, action) {
1150
+ if (typeof value !== "string" || value.length === 0 || value.length > 1024) return false;
1151
+ if (service === "encryption" && value.startsWith("urn:tinycloud:encryption:")) {
1152
+ if (!/^[A-Za-z0-9_][A-Za-z0-9_.:/-]*$/.test(value) || value.includes("//") || value.split("/").some((segment) => segment === "." || segment === "..")) return false;
1153
+ try {
1154
+ parseNetworkId(value);
1155
+ return true;
1156
+ } catch {
1157
+ return false;
1158
+ }
1159
+ }
1160
+ const parsed = parseCapabilityResource(value, service);
1161
+ return parsed !== void 0 && (!parsed.path.endsWith("/") || service === "kv" && action === "tinycloud.kv/list") && (parsed.path !== "" || service === "kv" && action === "tinycloud.kv/list");
1162
+ }
1163
+ function validatedCapabilityOf(error) {
1164
+ const verdict = authorizationVerdictOf(error);
1165
+ if (verdict !== "unauthenticated" && verdict !== "forbidden") return void 0;
1166
+ const seen = /* @__PURE__ */ new Set();
1167
+ let node = error;
1168
+ for (let depth = 0; depth < MAX_CAUSE_DEPTH && typeof node === "object" && node !== null && !seen.has(node); depth += 1) {
1169
+ seen.add(node);
1170
+ const record = node;
1171
+ const status = errorStatusOf(record);
1172
+ const meta = record.meta;
1173
+ if ((status === 401 || status === 403 || status === void 0 && record.code === ErrorCodes.AUTH_UNAUTHORIZED) && typeof meta === "object" && meta !== null && !Array.isArray(meta)) {
1174
+ const metadata = meta;
1175
+ const { resource, requiredAction } = metadata;
1176
+ if (typeof requiredAction === "string" && grantableActions.has(requiredAction)) {
1177
+ const actionService = requiredAction.slice("tinycloud.".length, requiredAction.indexOf("/"));
1178
+ const explicitService = record.service === void 0 ? metadata.service : record.service;
1179
+ if (safeCapabilityResource(resource, actionService, requiredAction) && (explicitService === void 0 || explicitService === actionService || explicitService === `tinycloud.${actionService}`)) {
1180
+ return { resource, requiredAction };
1181
+ }
1182
+ }
1183
+ }
1184
+ node = record.cause;
1185
+ }
1186
+ return void 0;
1187
+ }
996
1188
  var MAX_CAUSE_DEPTH = 8;
997
1189
  function errorStatusOf(node) {
998
1190
  const meta = node.meta;
@@ -1040,8 +1232,28 @@ function storageLimitReachedError(service, message, meta) {
1040
1232
  meta
1041
1233
  };
1042
1234
  }
1235
+ var STORAGE_FULL_MESSAGE = "TinyCloud storage is full, so this change was not saved. Reading still works. Free up space or upgrade your plan to save again.";
1236
+ var STORAGE_WRITE_TOO_LARGE_MESSAGE = "This change is larger than the TinyCloud storage you have left, so it was not saved. Reading still works. Free up space or upgrade your plan to save it.";
1237
+ function parseStorageQuotaBytes(responseText) {
1238
+ const match = responseText.match(/Used:\s*(\d+)\s*bytes,\s*Limit:\s*(\d+)\s*bytes/i);
1239
+ if (!match) return void 0;
1240
+ return {
1241
+ usedBytes: parseInt(match[1], 10),
1242
+ limitBytes: parseInt(match[2], 10)
1243
+ };
1244
+ }
1245
+ function storageRejectionError(service, status, meta, responseText) {
1246
+ const quotaMeta = { ...meta, ...parseStorageQuotaBytes(responseText) };
1247
+ return status === 402 ? storageQuotaExceededError(service, STORAGE_FULL_MESSAGE, quotaMeta) : storageLimitReachedError(service, STORAGE_WRITE_TOO_LARGE_MESSAGE, quotaMeta);
1248
+ }
1249
+ function isStorageFullError(error) {
1250
+ return error?.code === ErrorCodes.STORAGE_QUOTA_EXCEEDED || error?.code === ErrorCodes.STORAGE_LIMIT_REACHED;
1251
+ }
1043
1252
  function wrapError(service, error, defaultCode = ErrorCodes.NETWORK_ERROR) {
1044
1253
  if (error instanceof Error) {
1254
+ if (isRequestTimeoutError(error)) {
1255
+ return timeoutError(service, error.timeoutMs, error);
1256
+ }
1045
1257
  if (error.name === "AbortError") {
1046
1258
  return abortedError(service);
1047
1259
  }
@@ -1066,6 +1278,7 @@ function errorResult(error) {
1066
1278
  }
1067
1279
 
1068
1280
  // src/base/BaseService.ts
1281
+ var MAX_TIMER_DELAY_MS = 2147483647;
1069
1282
  var BaseService = class {
1070
1283
  constructor() {
1071
1284
  /**
@@ -1246,6 +1459,64 @@ var BaseService = class {
1246
1459
  }
1247
1460
  return controller.signal;
1248
1461
  }
1462
+ /**
1463
+ * Create the abort signal for one request, bounded by the request timeout.
1464
+ *
1465
+ * The signal aborts when the service signs out, the context aborts, the
1466
+ * caller's `signal` aborts, or the timeout elapses, whichever comes first.
1467
+ * A timeout aborts with a {@link RequestTimeoutError} (`TimeoutError`), which
1468
+ * `wrapError` maps to `ErrorCodes.TIMEOUT`; the other sources keep their own
1469
+ * reason (normally `AbortError`, mapped to `ErrorCodes.ABORTED`).
1470
+ *
1471
+ * The timeout is `timeoutMs` when given, otherwise `config.timeout`. Only a
1472
+ * positive, finite value (at most 2^31-1 ms) applies; `undefined`, `0`, and
1473
+ * `Infinity` mean no timeout, so a per-call `0` opts out of a configured one.
1474
+ *
1475
+ * Callers must call `dispose()` once the response body has been consumed,
1476
+ * which clears the timer and removes the listeners this adds to the
1477
+ * long-lived service and context signals.
1478
+ *
1479
+ * @param signal - The caller's abort signal for this request
1480
+ * @param timeoutMs - Per-request timeout override in milliseconds
1481
+ */
1482
+ createRequestSignal(signal, timeoutMs) {
1483
+ const controller = new AbortController();
1484
+ const detachers = [];
1485
+ let timer;
1486
+ const dispose = () => {
1487
+ if (timer !== void 0) {
1488
+ clearTimeout(timer);
1489
+ timer = void 0;
1490
+ }
1491
+ for (const detach of detachers.splice(0)) {
1492
+ detach();
1493
+ }
1494
+ };
1495
+ const abort = (reason) => {
1496
+ dispose();
1497
+ controller.abort(reason);
1498
+ };
1499
+ const requestSignal = { signal: controller.signal, dispose };
1500
+ const parents = [
1501
+ this.abortController.signal,
1502
+ this.context?.abortSignal,
1503
+ signal
1504
+ ].filter((parent) => parent !== void 0);
1505
+ for (const parent of parents) {
1506
+ if (parent.aborted) {
1507
+ abort(parent.reason);
1508
+ return requestSignal;
1509
+ }
1510
+ const onAbort = () => abort(parent.reason);
1511
+ parent.addEventListener("abort", onAbort, { once: true });
1512
+ detachers.push(() => parent.removeEventListener("abort", onAbort));
1513
+ }
1514
+ const timeout = timeoutMs ?? this._config.timeout;
1515
+ if (typeof timeout === "number" && timeout > 0 && timeout <= MAX_TIMER_DELAY_MS) {
1516
+ timer = setTimeout(() => abort(new RequestTimeoutError(timeout)), timeout);
1517
+ }
1518
+ return requestSignal;
1519
+ }
1249
1520
  /**
1250
1521
  * Wrap an operation with error handling and telemetry.
1251
1522
  *
@@ -1354,6 +1625,30 @@ var PrefixedKVService = class _PrefixedKVService {
1354
1625
  removePrefix
1355
1626
  });
1356
1627
  }
1628
+ /**
1629
+ * Read the change feed for everything under this prefix. A view with an
1630
+ * empty prefix (`""` or `"/"`) is refused: the node does not serve a
1631
+ * whole-space feed.
1632
+ */
1633
+ async changes(options) {
1634
+ if (this._prefix.replace(/\/+$/, "") === "") {
1635
+ return err(serviceError(
1636
+ ErrorCodes.INVALID_INPUT,
1637
+ "KV changes needs a non-empty prefix; this view has none",
1638
+ "kv"
1639
+ ));
1640
+ }
1641
+ const syncPrefix = `${this._prefix}/`;
1642
+ const response = await this._kv.changes({ ...options, prefix: syncPrefix });
1643
+ if (!response.ok) return response;
1644
+ return ok({
1645
+ ...response.data,
1646
+ changes: response.data.changes.map((change) => ({
1647
+ ...change,
1648
+ key: change.key.startsWith(syncPrefix) ? change.key.slice(syncPrefix.length) : change.key
1649
+ }))
1650
+ });
1651
+ }
1357
1652
  /**
1358
1653
  * Delete a key.
1359
1654
  */
@@ -1406,11 +1701,24 @@ var KVAction = {
1406
1701
  PUT: "tinycloud.kv/put",
1407
1702
  LIST: "tinycloud.kv/list",
1408
1703
  DELETE: "tinycloud.kv/del",
1409
- HEAD: "tinycloud.kv/metadata"
1704
+ HEAD: "tinycloud.kv/metadata",
1705
+ /** The change feed; never implied by `*` or `tinycloud.kv/*`. */
1706
+ SYNC: "tinycloud.kv/sync",
1707
+ /** Retention attestation; presented through `retentionGrant`, never invoked or implied. */
1708
+ RETAIN: "tinycloud.kv/retain"
1410
1709
  };
1411
1710
 
1412
1711
  // src/kv/KVService.ts
1413
1712
  var MAX_KV_BATCH_READ_ITEMS = 100;
1713
+ function isJsonObject(value) {
1714
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1715
+ }
1716
+ var REVOKED_BODY = /^(?:Invalid invocation: )?delegation-(?:(revoked): [A-Za-z0-9]+|(ancestor-revoked): ancestor=[A-Za-z0-9]+ invoked=[A-Za-z0-9]+)$/;
1717
+ function revocationCodeOf(body) {
1718
+ const match = REVOKED_BODY.exec(body.trim());
1719
+ if (match === null) return void 0;
1720
+ return match[1] !== void 0 ? ErrorCodes.AUTH_DELEGATION_REVOKED : ErrorCodes.AUTH_DELEGATION_ANCESTOR_REVOKED;
1721
+ }
1414
1722
  function encodeKvBatchPartName(path) {
1415
1723
  return encodeURIComponent(path).replace(
1416
1724
  /[!'()*]/g,
@@ -1433,47 +1741,21 @@ var KVService = class extends BaseService {
1433
1741
  get config() {
1434
1742
  return this._config;
1435
1743
  }
1436
- // Parses "Used: X bytes, Limit: Y bytes" from tinycloud-node error responses
1437
- parseQuotaInfo(errorText) {
1438
- const match = errorText.match(
1439
- /Used:\s*(\d+)\s*bytes,\s*Limit:\s*(\d+)\s*bytes/i
1440
- );
1441
- if (match) {
1442
- return {
1443
- usedBytes: parseInt(match[1], 10),
1444
- limitBytes: parseInt(match[2], 10)
1445
- };
1446
- }
1447
- return void 0;
1448
- }
1449
1744
  handleQuotaErrorResponse(response, errorText, key) {
1450
- if (response.status === 402) {
1451
- const quotaInfo = this.parseQuotaInfo(errorText);
1452
- return err(
1453
- storageQuotaExceededError(
1454
- "kv",
1455
- `Storage quota exceeded for key ${JSON.stringify(key)}: ${errorText}`,
1456
- {
1457
- status: response.status,
1458
- ...quotaInfo ? { usedBytes: quotaInfo.usedBytes, limitBytes: quotaInfo.limitBytes } : {}
1459
- }
1460
- )
1461
- );
1745
+ if (response.status !== 402 && response.status !== 413) {
1746
+ return void 0;
1462
1747
  }
1463
- if (response.status === 413) {
1464
- const quotaInfo = this.parseQuotaInfo(errorText);
1465
- return err(
1466
- storageLimitReachedError(
1467
- "kv",
1468
- `Storage limit reached for key ${JSON.stringify(key)}: ${errorText}`,
1469
- {
1470
- status: response.status,
1471
- ...quotaInfo ? { usedBytes: quotaInfo.usedBytes, limitBytes: quotaInfo.limitBytes } : {}
1472
- }
1473
- )
1474
- );
1748
+ if (response.status === 413 && !parseStorageQuotaBytes(errorText)) {
1749
+ return void 0;
1475
1750
  }
1476
- return void 0;
1751
+ return err(
1752
+ storageRejectionError(
1753
+ "kv",
1754
+ response.status,
1755
+ { status: response.status, statusText: response.statusText, key },
1756
+ errorText
1757
+ )
1758
+ );
1477
1759
  }
1478
1760
  /**
1479
1761
  * Classify a KV 404 by reading the response body once.
@@ -1532,7 +1814,7 @@ var KVService = class extends BaseService {
1532
1814
  * @param path - Resource path
1533
1815
  * @param action - KV action
1534
1816
  * @param body - Optional request body
1535
- * @param signal - Optional abort signal
1817
+ * @param signal - Request signal from createRequestSignal()
1536
1818
  * @returns Fetch response
1537
1819
  */
1538
1820
  async invokeOperation(path, action, body, signal, extraHeaders) {
@@ -1548,7 +1830,7 @@ var KVService = class extends BaseService {
1548
1830
  method: "POST",
1549
1831
  headers: requestHeaders,
1550
1832
  body,
1551
- signal: this.combineSignals(signal)
1833
+ signal
1552
1834
  });
1553
1835
  }
1554
1836
  /**
@@ -1725,6 +2007,7 @@ var KVService = class extends BaseService {
1725
2007
  "kv"
1726
2008
  ));
1727
2009
  }
2010
+ const request = this.createRequestSignal(options?.signal, options?.timeout);
1728
2011
  try {
1729
2012
  const session = this.context.session;
1730
2013
  const invocationHeaders = this.context.invokeAny(
@@ -1746,15 +2029,17 @@ var KVService = class extends BaseService {
1746
2029
  const response = await this.context.fetch(`${this.host}/invoke`, {
1747
2030
  method: "POST",
1748
2031
  headers,
1749
- signal: this.combineSignals(options?.signal)
2032
+ signal: request.signal
1750
2033
  });
1751
2034
  if (!response.ok) {
1752
- const errorText = await response.text();
2035
+ const errorText = response.status === 401 || response.status === 403 ? await this.readAuthorizationText(response) : await response.text();
1753
2036
  if (response.status === 401 || response.status === 403) {
1754
2037
  return this.authorizationFailure(
1755
2038
  `Failed to batch read ${keys.length} key(s)`,
1756
2039
  response,
1757
- errorText
2040
+ errorText,
2041
+ paths,
2042
+ action
1758
2043
  );
1759
2044
  }
1760
2045
  if (response.status === 413) {
@@ -1809,6 +2094,8 @@ var KVService = class extends BaseService {
1809
2094
  return ok({ results, count: results.length });
1810
2095
  } catch (error) {
1811
2096
  return err(wrapError("kv", error));
2097
+ } finally {
2098
+ request.dispose();
1812
2099
  }
1813
2100
  }
1814
2101
  /**
@@ -1844,7 +2131,7 @@ var KVService = class extends BaseService {
1844
2131
  return text;
1845
2132
  }
1846
2133
  }
1847
- async createSignedReadUrlError(response, key) {
2134
+ async createSignedReadUrlError(response, key, path) {
1848
2135
  let errorText = response.statusText;
1849
2136
  try {
1850
2137
  const text = await response.text();
@@ -1857,7 +2144,9 @@ var KVService = class extends BaseService {
1857
2144
  return this.authorizationFailure(
1858
2145
  `Failed to create signed read URL for key ${JSON.stringify(key)}`,
1859
2146
  response,
1860
- errorText
2147
+ errorText,
2148
+ [path],
2149
+ KVAction.GET
1861
2150
  );
1862
2151
  }
1863
2152
  const code = response.status === 400 ? ErrorCodes.INVALID_INPUT : ErrorCodes.NETWORK_ERROR;
@@ -1870,21 +2159,42 @@ var KVService = class extends BaseService {
1870
2159
  )
1871
2160
  );
1872
2161
  }
2162
+ /** A known 401/403 keeps its typed status even if reading its body fails. */
2163
+ async readAuthorizationText(response) {
2164
+ try {
2165
+ return await response.text();
2166
+ } catch {
2167
+ return "";
2168
+ }
2169
+ }
1873
2170
  /**
1874
2171
  * AUTH_UNAUTHORIZED for a 401/403 response. The message keeps the HTTP
1875
2172
  * status and the server text (`<context>: <status> - <text>`, the same
1876
2173
  * shape as every other KV failure) so callers that rethrow only the message
1877
2174
  * still see the status; `meta.status` carries it for typed callers.
1878
- * Resource/ability hints are parsed from the raw body, never the message.
2175
+ * Server hints must match the requested path/action (whether the node sends
2176
+ * a relative path or full resource); metadata uses only the request-derived
2177
+ * canonical resource under the authenticated session's space.
1879
2178
  */
1880
- authorizationFailure(context, response, errorText, extraMeta = {}) {
2179
+ authorizationFailure(context, response, errorText, paths, expectedAction, extraMeta = {}) {
1881
2180
  const { resource, action } = parseAuthError(errorText);
2181
+ const requestedPath = paths.find(
2182
+ (path) => resource === path || resource === `${this.context.session.spaceId}/kv/${path}`
2183
+ );
2184
+ const capability = requestedPath !== void 0 && action === expectedAction ? validatedCapabilityOf({
2185
+ service: "kv",
2186
+ code: ErrorCodes.AUTH_UNAUTHORIZED,
2187
+ meta: {
2188
+ status: response.status,
2189
+ resource: `${this.context.session.spaceId}/kv/${requestedPath}`,
2190
+ requiredAction: action
2191
+ }
2192
+ }) : void 0;
1882
2193
  const detail = errorText.trim().length > 0 ? errorText : response.statusText || "authorization failed";
1883
2194
  return err(authUnauthorizedError("kv", `${context}: ${response.status} - ${detail}`, {
1884
2195
  status: response.status,
1885
- ...action && { requiredAction: action },
1886
- ...resource && { resource },
1887
- ...extraMeta
2196
+ ...extraMeta,
2197
+ ...capability
1888
2198
  }));
1889
2199
  }
1890
2200
  normalizeSignedReadUrlResponse(data) {
@@ -1918,23 +2228,35 @@ var KVService = class extends BaseService {
1918
2228
  ));
1919
2229
  }
1920
2230
  const path = this.getFullPath(key, options?.prefix);
2231
+ const request = this.createRequestSignal(options?.signal, options?.timeout);
1921
2232
  try {
1922
2233
  const response = await this.invokeOperation(
1923
2234
  path,
1924
2235
  KVAction.GET,
1925
2236
  void 0,
1926
- options?.signal,
2237
+ request.signal,
1927
2238
  options?.maxResponseBytes === void 0 ? void 0 : { "x-tinycloud-max-response-bytes": String(options.maxResponseBytes) }
1928
2239
  );
1929
2240
  if (!response.ok) {
1930
2241
  if (response.status === 401 || response.status === 403) {
1931
- const errorText2 = await response.text();
2242
+ const errorText2 = await this.readAuthorizationText(response);
1932
2243
  const permissionHint = parsePermissionHintFromErrorText(errorText2);
2244
+ const structuredCapability = permissionHint?.service === "tinycloud.kv" && permissionHint.space === this.context.session.spaceId && permissionHint.path === path ? validatedCapabilityOf({
2245
+ service: "kv",
2246
+ code: ErrorCodes.AUTH_UNAUTHORIZED,
2247
+ meta: {
2248
+ status: response.status,
2249
+ resource: `${this.context.session.spaceId}/kv/${path}`,
2250
+ requiredAction: KVAction.GET
2251
+ }
2252
+ }) : void 0;
1933
2253
  return this.authorizationFailure(
1934
2254
  `Failed to get key ${JSON.stringify(key)}`,
1935
2255
  response,
1936
2256
  errorText2,
1937
- permissionHint === void 0 ? {} : { permissionHint }
2257
+ [path],
2258
+ KVAction.GET,
2259
+ structuredCapability ? { permissionHint, ...structuredCapability } : {}
1938
2260
  );
1939
2261
  }
1940
2262
  if (response.status === 404) {
@@ -1969,6 +2291,8 @@ var KVService = class extends BaseService {
1969
2291
  });
1970
2292
  } catch (error) {
1971
2293
  return err(wrapError("kv", error));
2294
+ } finally {
2295
+ request.dispose();
1972
2296
  }
1973
2297
  });
1974
2298
  }
@@ -1996,12 +2320,13 @@ var KVService = class extends BaseService {
1996
2320
  }
1997
2321
  const path = this.getFullPath(key, options?.prefix);
1998
2322
  const body = this.serializePutValue(value, options?.contentType);
2323
+ const request = this.createRequestSignal(options?.signal, options?.timeout);
1999
2324
  try {
2000
2325
  const response = await this.invokeOperation(
2001
2326
  path,
2002
2327
  KVAction.PUT,
2003
2328
  body,
2004
- options?.signal,
2329
+ request.signal,
2005
2330
  {
2006
2331
  ...options?.ifMatch === void 0 ? {} : { "if-match": options.ifMatch },
2007
2332
  ...options?.ifNoneMatch === void 0 ? {} : { "if-none-match": options.ifNoneMatch }
@@ -2011,7 +2336,9 @@ var KVService = class extends BaseService {
2011
2336
  return this.authorizationFailure(
2012
2337
  `Failed to put key ${JSON.stringify(key)}`,
2013
2338
  response,
2014
- await response.text()
2339
+ await this.readAuthorizationText(response),
2340
+ [path],
2341
+ KVAction.PUT
2015
2342
  );
2016
2343
  }
2017
2344
  if (!response.ok) {
@@ -2055,6 +2382,8 @@ var KVService = class extends BaseService {
2055
2382
  });
2056
2383
  } catch (error) {
2057
2384
  return err(wrapError("kv", error));
2385
+ } finally {
2386
+ request.dispose();
2058
2387
  }
2059
2388
  });
2060
2389
  }
@@ -2094,6 +2423,7 @@ var KVService = class extends BaseService {
2094
2423
  seen.add(path);
2095
2424
  }
2096
2425
  let requestMayHaveDispatched = false;
2426
+ const request = this.createRequestSignal(options?.signal, options?.timeout);
2097
2427
  try {
2098
2428
  const body = new FormData();
2099
2429
  for (let index = 0; index < items.length; index++) {
@@ -2116,7 +2446,7 @@ var KVService = class extends BaseService {
2116
2446
  method: "POST",
2117
2447
  headers,
2118
2448
  body,
2119
- signal: this.combineSignals(options?.signal)
2449
+ signal: request.signal
2120
2450
  };
2121
2451
  const fetchFn = this.context.fetch;
2122
2452
  requestMayHaveDispatched = true;
@@ -2140,7 +2470,9 @@ var KVService = class extends BaseService {
2140
2470
  return this.authorizationFailure(
2141
2471
  `Failed to batch put ${items.length} key(s)`,
2142
2472
  response,
2143
- errorText
2473
+ errorText,
2474
+ paths,
2475
+ KVAction.PUT
2144
2476
  );
2145
2477
  }
2146
2478
  const quotaError = this.handleQuotaErrorResponse(
@@ -2165,14 +2497,16 @@ var KVService = class extends BaseService {
2165
2497
  rawBody = await response.json();
2166
2498
  } catch (jsonError) {
2167
2499
  const cause = jsonError instanceof Error ? jsonError : new Error(String(jsonError));
2500
+ const timedOut = isRequestTimeoutError(cause);
2168
2501
  return err(
2169
2502
  serviceError(
2170
- ErrorCodes.NETWORK_ERROR,
2171
- `KV batchPut response was not valid JSON: ${cause.message}`,
2503
+ timedOut ? ErrorCodes.TIMEOUT : ErrorCodes.NETWORK_ERROR,
2504
+ timedOut ? `KV batchPut response body did not arrive in time: ${cause.message}` : `KV batchPut response was not valid JSON: ${cause.message}`,
2172
2505
  "kv",
2173
2506
  {
2174
2507
  cause,
2175
2508
  meta: {
2509
+ ...timedOut ? { timeoutMs: cause.timeoutMs } : {},
2176
2510
  requestMayHaveDispatched: true,
2177
2511
  responseReceived: true,
2178
2512
  status: response.status,
@@ -2227,6 +2561,8 @@ var KVService = class extends BaseService {
2227
2561
  ...wrapped,
2228
2562
  meta: { ...wrapped.meta, requestMayHaveDispatched }
2229
2563
  });
2564
+ } finally {
2565
+ request.dispose();
2230
2566
  }
2231
2567
  });
2232
2568
  }
@@ -2249,23 +2585,26 @@ var KVService = class extends BaseService {
2249
2585
  if (options?.path) {
2250
2586
  listPath = listPath ? `${listPath}/${options.path}` : options.path;
2251
2587
  }
2588
+ const request = this.createRequestSignal(options?.signal, options?.timeout);
2252
2589
  try {
2253
2590
  const response = await this.invokeOperation(
2254
2591
  listPath,
2255
2592
  KVAction.LIST,
2256
2593
  void 0,
2257
- options?.signal,
2594
+ request.signal,
2258
2595
  options?.limit === void 0 && options?.cursor === void 0 ? void 0 : {
2259
2596
  ...options?.limit === void 0 ? {} : { "x-tinycloud-limit": String(options.limit) },
2260
2597
  ...options?.cursor === void 0 ? {} : { "x-tinycloud-cursor": options.cursor }
2261
2598
  }
2262
2599
  );
2263
2600
  if (!response.ok) {
2264
- if (response.status === 401) {
2601
+ if (response.status === 401 || response.status === 403) {
2265
2602
  return this.authorizationFailure(
2266
2603
  "Failed to list keys",
2267
2604
  response,
2268
- await response.text()
2605
+ await this.readAuthorizationText(response),
2606
+ [listPath],
2607
+ KVAction.LIST
2269
2608
  );
2270
2609
  }
2271
2610
  const errorText = await response.text();
@@ -2293,9 +2632,174 @@ var KVService = class extends BaseService {
2293
2632
  });
2294
2633
  } catch (error) {
2295
2634
  return err(wrapError("kv", error));
2635
+ } finally {
2636
+ request.dispose();
2296
2637
  }
2297
2638
  });
2298
2639
  }
2640
+ /**
2641
+ * Read one page of the `tinycloud.kv/sync` change feed for a prefix.
2642
+ */
2643
+ async changes(options) {
2644
+ return this.withTelemetry("changes", options?.prefix, async () => {
2645
+ if (!this.requireAuth()) {
2646
+ return err(authRequiredError("kv"));
2647
+ }
2648
+ if (typeof options?.prefix !== "string" || options.prefix.length === 0) {
2649
+ return err(serviceError(
2650
+ ErrorCodes.INVALID_INPUT,
2651
+ "KV changes prefix must be a non-empty string",
2652
+ "kv"
2653
+ ));
2654
+ }
2655
+ if (options.limit !== void 0 && (!Number.isInteger(options.limit) || options.limit < 1 || options.limit > 1e3)) {
2656
+ return err(serviceError(
2657
+ ErrorCodes.INVALID_INPUT,
2658
+ "KV changes limit must be an integer from 1 through 1000",
2659
+ "kv"
2660
+ ));
2661
+ }
2662
+ const headers = {};
2663
+ if (options.limit !== void 0) headers["x-tinycloud-limit"] = String(options.limit);
2664
+ if (options.cursor !== void 0) headers["x-tinycloud-cursor"] = options.cursor;
2665
+ if (options.retentionGrant !== void 0) {
2666
+ headers["x-tinycloud-retention-grant"] = options.retentionGrant;
2667
+ }
2668
+ const request = this.createRequestSignal(options.signal, options.timeout);
2669
+ try {
2670
+ const response = await this.invokeOperation(
2671
+ options.prefix,
2672
+ KVAction.SYNC,
2673
+ void 0,
2674
+ request.signal,
2675
+ headers
2676
+ );
2677
+ if (!response.ok) {
2678
+ return await this.changesFailure(response, options.prefix, request.signal);
2679
+ }
2680
+ const text = await response.text();
2681
+ let body;
2682
+ try {
2683
+ body = JSON.parse(text);
2684
+ } catch {
2685
+ body = void 0;
2686
+ }
2687
+ const page = this.normalizeChangesResponse(body);
2688
+ if (!page) {
2689
+ return err(serviceError(
2690
+ ErrorCodes.NETWORK_ERROR,
2691
+ "KV changes response was not a tinycloud.kv/sync page",
2692
+ "kv",
2693
+ { meta: { status: response.status, statusText: response.statusText } }
2694
+ ));
2695
+ }
2696
+ return ok(page);
2697
+ } catch (error) {
2698
+ return err(wrapError("kv", error));
2699
+ } finally {
2700
+ request.dispose();
2701
+ }
2702
+ });
2703
+ }
2704
+ /**
2705
+ * Map a failed `kv/sync` response. 410 and the retention 403 carry
2706
+ * `{"error":{"code","reason"}}`; a revoked grant is a 401 whose whole body
2707
+ * is the node's revocation error (see REVOKED_BODY). A body read that
2708
+ * fails because the request was cancelled or timed out rethrows, so the
2709
+ * caller sees `ABORTED`/`TIMEOUT` rather than a mapped status.
2710
+ */
2711
+ async changesFailure(response, prefix, signal) {
2712
+ if (response.status === 404) {
2713
+ return this.classifyNotFound(response, prefix);
2714
+ }
2715
+ let errorText;
2716
+ try {
2717
+ errorText = await response.text();
2718
+ } catch (error) {
2719
+ if (signal.aborted) throw error;
2720
+ errorText = "";
2721
+ }
2722
+ const meta = { status: response.status, statusText: response.statusText };
2723
+ let reason;
2724
+ let bodyCode;
2725
+ try {
2726
+ const body = JSON.parse(errorText);
2727
+ const error = isJsonObject(body) ? body.error : void 0;
2728
+ if (isJsonObject(error)) {
2729
+ if (typeof error.code === "string") bodyCode = error.code;
2730
+ if (typeof error.reason === "string") reason = error.reason;
2731
+ }
2732
+ } catch {
2733
+ }
2734
+ const context = `Failed to read KV changes for ${JSON.stringify(prefix)}`;
2735
+ if (response.status === 410) {
2736
+ return err(serviceError(
2737
+ ErrorCodes.KV_SYNC_RESET_REQUIRED,
2738
+ `${context}: the node requires a reset (${reason ?? "reason not given"}); discard sync state and restart without a cursor`,
2739
+ "kv",
2740
+ { meta: { ...meta, reason } }
2741
+ ));
2742
+ }
2743
+ if (response.status === 403 && bodyCode === "RETENTION_GRANT_REFUSED") {
2744
+ return err(serviceError(
2745
+ ErrorCodes.KV_RETENTION_GRANT_REFUSED,
2746
+ `${context}: the retention grant was refused (${reason ?? "reason not given"})`,
2747
+ "kv",
2748
+ { meta: { ...meta, reason } }
2749
+ ));
2750
+ }
2751
+ if (response.status === 401) {
2752
+ const revoked = revocationCodeOf(errorText);
2753
+ if (revoked !== void 0) {
2754
+ return err(serviceError(revoked, `${context}: 401 - ${errorText}`, "kv", { meta }));
2755
+ }
2756
+ }
2757
+ if (response.status === 401 || response.status === 403) {
2758
+ return this.authorizationFailure(context, response, errorText, [prefix], KVAction.SYNC);
2759
+ }
2760
+ return err(serviceError(
2761
+ response.status === 400 ? ErrorCodes.INVALID_INPUT : ErrorCodes.NETWORK_ERROR,
2762
+ `${context}: ${response.status} - ${errorText}`,
2763
+ "kv",
2764
+ { meta }
2765
+ ));
2766
+ }
2767
+ normalizeChangesResponse(data) {
2768
+ if (!isJsonObject(data)) return void 0;
2769
+ const { changes, more, cursor, source, authority } = data;
2770
+ if (!Array.isArray(changes) || typeof more !== "boolean" || typeof cursor !== "string" || !isJsonObject(source) || typeof source.nodeDid !== "string" || typeof source.space !== "string" || typeof source.prefix !== "string" || !isJsonObject(authority)) {
2771
+ return void 0;
2772
+ }
2773
+ const { notBefore, expiresAt, retainUntil } = authority;
2774
+ if (notBefore !== null && typeof notBefore !== "string" || expiresAt !== null && typeof expiresAt !== "string" || retainUntil !== null && typeof retainUntil !== "string") {
2775
+ return void 0;
2776
+ }
2777
+ const parsed = [];
2778
+ for (const change of changes) {
2779
+ if (!isJsonObject(change) || typeof change.key !== "string") return void 0;
2780
+ if (change.deleted === true) {
2781
+ parsed.push({ key: change.key, deleted: true });
2782
+ continue;
2783
+ }
2784
+ const { metadata } = change;
2785
+ if (change.deleted !== false || typeof change.etag !== "string" || !isJsonObject(metadata) || !Object.values(metadata).every((value) => typeof value === "string")) {
2786
+ return void 0;
2787
+ }
2788
+ parsed.push({
2789
+ key: change.key,
2790
+ deleted: false,
2791
+ etag: change.etag,
2792
+ metadata
2793
+ });
2794
+ }
2795
+ return {
2796
+ changes: parsed,
2797
+ more,
2798
+ cursor,
2799
+ source: { nodeDid: source.nodeDid, space: source.space, prefix: source.prefix },
2800
+ authority: { notBefore, expiresAt, retainUntil }
2801
+ };
2802
+ }
2299
2803
  /**
2300
2804
  * Delete a key.
2301
2805
  */
@@ -2305,20 +2809,23 @@ var KVService = class extends BaseService {
2305
2809
  return err(authRequiredError("kv"));
2306
2810
  }
2307
2811
  const path = this.getFullPath(key, options?.prefix);
2812
+ const request = this.createRequestSignal(options?.signal, options?.timeout);
2308
2813
  try {
2309
2814
  const response = await this.invokeOperation(
2310
2815
  path,
2311
2816
  KVAction.DELETE,
2312
2817
  void 0,
2313
- options?.signal,
2818
+ request.signal,
2314
2819
  options?.ifMatch === void 0 ? void 0 : { "if-match": options.ifMatch }
2315
2820
  );
2316
2821
  if (!response.ok) {
2317
- if (response.status === 401) {
2822
+ if (response.status === 401 || response.status === 403) {
2318
2823
  return this.authorizationFailure(
2319
2824
  `Failed to delete key ${JSON.stringify(key)}`,
2320
2825
  response,
2321
- await response.text()
2826
+ await this.readAuthorizationText(response),
2827
+ [path],
2828
+ KVAction.DELETE
2322
2829
  );
2323
2830
  }
2324
2831
  if (response.status === 404) {
@@ -2356,6 +2863,8 @@ var KVService = class extends BaseService {
2356
2863
  });
2357
2864
  } catch (error) {
2358
2865
  return err(wrapError("kv", error));
2866
+ } finally {
2867
+ request.dispose();
2359
2868
  }
2360
2869
  });
2361
2870
  }
@@ -2368,19 +2877,22 @@ var KVService = class extends BaseService {
2368
2877
  return err(authRequiredError("kv"));
2369
2878
  }
2370
2879
  const path = this.getFullPath(key, options?.prefix);
2880
+ const request = this.createRequestSignal(options?.signal, options?.timeout);
2371
2881
  try {
2372
2882
  const response = await this.invokeOperation(
2373
2883
  path,
2374
2884
  KVAction.HEAD,
2375
2885
  void 0,
2376
- options?.signal
2886
+ request.signal
2377
2887
  );
2378
2888
  if (!response.ok) {
2379
- if (response.status === 401) {
2889
+ if (response.status === 401 || response.status === 403) {
2380
2890
  return this.authorizationFailure(
2381
2891
  `Failed to get metadata for key ${JSON.stringify(key)}`,
2382
2892
  response,
2383
- await response.text()
2893
+ await this.readAuthorizationText(response),
2894
+ [path],
2895
+ KVAction.HEAD
2384
2896
  );
2385
2897
  }
2386
2898
  if (response.status === 404) {
@@ -2402,6 +2914,8 @@ var KVService = class extends BaseService {
2402
2914
  });
2403
2915
  } catch (error) {
2404
2916
  return err(wrapError("kv", error));
2917
+ } finally {
2918
+ request.dispose();
2405
2919
  }
2406
2920
  });
2407
2921
  }
@@ -2439,15 +2953,16 @@ var KVService = class extends BaseService {
2439
2953
  if (options?.etag !== void 0) {
2440
2954
  body.etag = options.etag;
2441
2955
  }
2956
+ const request = this.createRequestSignal(options?.signal, options?.timeout);
2442
2957
  try {
2443
2958
  const response = await this.context.fetch(`${this.host}/signed/kv`, {
2444
2959
  method: "POST",
2445
2960
  headers: this.withJsonContentType(headers),
2446
2961
  body: JSON.stringify(body),
2447
- signal: this.combineSignals(options?.signal)
2962
+ signal: request.signal
2448
2963
  });
2449
2964
  if (!response.ok) {
2450
- return this.createSignedReadUrlError(response, key);
2965
+ return this.createSignedReadUrlError(response, key, path);
2451
2966
  }
2452
2967
  const signedUrl = this.normalizeSignedReadUrlResponse(
2453
2968
  await response.json()
@@ -2464,6 +2979,8 @@ var KVService = class extends BaseService {
2464
2979
  return ok(signedUrl);
2465
2980
  } catch (error) {
2466
2981
  return err(wrapError("kv", error));
2982
+ } finally {
2983
+ request.dispose();
2467
2984
  }
2468
2985
  });
2469
2986
  }
@@ -2677,6 +3194,7 @@ var SQLService = class extends BaseService {
2677
3194
  if (!this.requireAuth()) {
2678
3195
  return err(authRequiredError("sql"));
2679
3196
  }
3197
+ const request = this.createRequestSignal(options?.signal);
2680
3198
  try {
2681
3199
  const body = {
2682
3200
  action: "query",
@@ -2685,19 +3203,22 @@ var SQLService = class extends BaseService {
2685
3203
  };
2686
3204
  if (options?.maxRows !== void 0) body.maxRows = options.maxRows;
2687
3205
  if (options?.maxBytes !== void 0) body.maxBytes = options.maxBytes;
3206
+ const action = this.actionForSql(sql, SQLAction.READ);
2688
3207
  const response = await this.invokeSQL(
2689
3208
  dbName,
2690
- this.actionForSql(sql, SQLAction.READ),
3209
+ action,
2691
3210
  body,
2692
- options?.signal
3211
+ request.signal
2693
3212
  );
2694
3213
  if (!response.ok) {
2695
- return this.handleErrorResponse(response, "query");
3214
+ return this.handleErrorResponse(response, "query", dbName, [action]);
2696
3215
  }
2697
3216
  const data = await response.json();
2698
3217
  return ok(data);
2699
3218
  } catch (error) {
2700
3219
  return err(wrapError("sql", error));
3220
+ } finally {
3221
+ request.dispose();
2701
3222
  }
2702
3223
  });
2703
3224
  }
@@ -2706,6 +3227,7 @@ var SQLService = class extends BaseService {
2706
3227
  if (!this.requireAuth()) {
2707
3228
  return err(authRequiredError("sql"));
2708
3229
  }
3230
+ const request = this.createRequestSignal(options?.signal);
2709
3231
  try {
2710
3232
  const body = {
2711
3233
  action: "execute",
@@ -2721,19 +3243,22 @@ var SQLService = class extends BaseService {
2721
3243
  (statement) => this.actionForSql(statement, SQLAction.SCHEMA)
2722
3244
  )
2723
3245
  ];
3246
+ const requestedActions = this.dedupeActions(actions);
2724
3247
  const response = await this.invokeSQL(
2725
3248
  dbName,
2726
- this.dedupeActions(actions),
3249
+ requestedActions,
2727
3250
  body,
2728
- options?.signal
3251
+ request.signal
2729
3252
  );
2730
3253
  if (!response.ok) {
2731
- return this.handleErrorResponse(response, "execute");
3254
+ return this.handleErrorResponse(response, "execute", dbName, requestedActions);
2732
3255
  }
2733
3256
  const data = await response.json();
2734
3257
  return ok(data);
2735
3258
  } catch (error) {
2736
3259
  return err(wrapError("sql", error));
3260
+ } finally {
3261
+ request.dispose();
2737
3262
  }
2738
3263
  });
2739
3264
  }
@@ -2742,20 +3267,24 @@ var SQLService = class extends BaseService {
2742
3267
  if (!this.requireAuth()) {
2743
3268
  return err(authRequiredError("sql"));
2744
3269
  }
3270
+ const request = this.createRequestSignal(options?.signal);
2745
3271
  try {
3272
+ const requestedActions = this.actionsForSqlBatch(statements);
2746
3273
  const response = await this.invokeSQL(
2747
3274
  dbName,
2748
- this.actionsForSqlBatch(statements),
3275
+ requestedActions,
2749
3276
  { action: "batch", statements },
2750
- options?.signal
3277
+ request.signal
2751
3278
  );
2752
3279
  if (!response.ok) {
2753
- return this.handleErrorResponse(response, "batch");
3280
+ return this.handleErrorResponse(response, "batch", dbName, requestedActions);
2754
3281
  }
2755
3282
  const data = await response.json();
2756
3283
  return ok(data);
2757
3284
  } catch (error) {
2758
3285
  return err(wrapError("sql", error));
3286
+ } finally {
3287
+ request.dispose();
2759
3288
  }
2760
3289
  });
2761
3290
  }
@@ -2764,20 +3293,23 @@ var SQLService = class extends BaseService {
2764
3293
  if (!this.requireAuth()) {
2765
3294
  return err(authRequiredError("sql"));
2766
3295
  }
3296
+ const request = this.createRequestSignal(options?.signal);
2767
3297
  try {
2768
3298
  const response = await this.invokeSQL(
2769
3299
  dbName,
2770
3300
  SQLAction.WRITE,
2771
3301
  { action: "execute_statement", name, params: params ?? [] },
2772
- options?.signal
3302
+ request.signal
2773
3303
  );
2774
3304
  if (!response.ok) {
2775
- return this.handleErrorResponse(response, "executeStatement");
3305
+ return this.handleErrorResponse(response, "executeStatement", dbName, [SQLAction.WRITE]);
2776
3306
  }
2777
3307
  const data = await response.json();
2778
3308
  return ok(data);
2779
3309
  } catch (error) {
2780
3310
  return err(wrapError("sql", error));
3311
+ } finally {
3312
+ request.dispose();
2781
3313
  }
2782
3314
  });
2783
3315
  }
@@ -2786,15 +3318,16 @@ var SQLService = class extends BaseService {
2786
3318
  if (!this.requireAuth()) {
2787
3319
  return err(authRequiredError("sql"));
2788
3320
  }
3321
+ const request = this.createRequestSignal(options?.signal);
2789
3322
  try {
2790
3323
  const response = await this.invokeSQL(
2791
3324
  dbName,
2792
3325
  SQLAction.READ,
2793
3326
  { action: "export" },
2794
- options?.signal
3327
+ request.signal
2795
3328
  );
2796
3329
  if (!response.ok) {
2797
- return this.handleErrorResponse(response, "export");
3330
+ return this.handleErrorResponse(response, "export", dbName, [SQLAction.READ]);
2798
3331
  }
2799
3332
  const resp = response;
2800
3333
  if (typeof resp.blob === "function") {
@@ -2805,26 +3338,31 @@ var SQLService = class extends BaseService {
2805
3338
  return ok(text);
2806
3339
  } catch (error) {
2807
3340
  return err(wrapError("sql", error));
3341
+ } finally {
3342
+ request.dispose();
2808
3343
  }
2809
3344
  });
2810
3345
  }
2811
3346
  async applyMigrationsOnDbUnlocked(dbName, options) {
2812
3347
  return this.withTelemetry("migrations.apply", dbName, async () => {
2813
- const created = await this.ensureMigrationsTable(dbName, options.signal);
2814
- if (!created.ok) return created;
2815
3348
  const listed = await this.queryOnDb(
2816
3349
  dbName,
2817
3350
  `SELECT id FROM ${MIGRATIONS_TABLE} WHERE namespace = ? ORDER BY applied_at, id`,
2818
3351
  [options.namespace],
2819
3352
  { signal: options.signal }
2820
3353
  );
2821
- if (!listed.ok) return listed;
3354
+ const tableMissing = !listed.ok && (listed.error.code === ErrorCodes.SQL_DATABASE_NOT_FOUND || isMissingTableError(listed.error.message));
3355
+ if (!listed.ok && !tableMissing) return listed;
2822
3356
  const appliedIds = new Set(
2823
- listed.data.rows.map((row) => rowValue(row, 0)).filter((id) => typeof id === "string")
3357
+ (listed.ok ? listed.data.rows : []).map((row) => rowValue(row, 0)).filter((id) => typeof id === "string")
2824
3358
  );
2825
3359
  const skipped = options.migrations.filter((migration) => appliedIds.has(migration.id)).map((migration) => migration.id);
2826
3360
  const pending = options.migrations.filter((migration) => !appliedIds.has(migration.id));
2827
3361
  const applied = [];
3362
+ if (pending.length > 0 && tableMissing) {
3363
+ const created = await this.ensureMigrationsTable(dbName, options.signal);
3364
+ if (!created.ok) return created;
3365
+ }
2828
3366
  for (const migration of pending) {
2829
3367
  const result = await this.applyOneMigration(dbName, options.namespace, migration, options.signal);
2830
3368
  if (!result.ok) return result;
@@ -2892,7 +3430,7 @@ var SQLService = class extends BaseService {
2892
3430
  "Content-Type": "application/json"
2893
3431
  },
2894
3432
  body: JSON.stringify(body),
2895
- signal: this.combineSignals(signal)
3433
+ signal
2896
3434
  });
2897
3435
  }
2898
3436
  actionForSql(sql, fallback) {
@@ -2925,8 +3463,12 @@ var SQLService = class extends BaseService {
2925
3463
  }))
2926
3464
  );
2927
3465
  }
2928
- async handleErrorResponse(response, operation) {
3466
+ async handleErrorResponse(response, operation, dbName, requestedActions) {
2929
3467
  const errorText = await response.text();
3468
+ const meta = responseErrorMeta(response.status, response.statusText, errorText);
3469
+ if (response.status === 402) {
3470
+ return err(storageRejectionError("sql", 402, meta, errorText));
3471
+ }
2930
3472
  const errorBody = parseServiceErrorBody(errorText);
2931
3473
  const errorCode = this.mapHttpStatusToErrorCode(
2932
3474
  response.status,
@@ -2939,11 +3481,12 @@ var SQLService = class extends BaseService {
2939
3481
  errorText,
2940
3482
  errorBody
2941
3483
  );
2942
- const meta = responseErrorMeta(response.status, response.statusText, errorText);
2943
3484
  if (response.status === 401) {
2944
- const { resource, action } = parseAuthError(errorText);
2945
- if (action) meta.requiredAction = action;
2946
- if (resource) meta.resource = resource;
3485
+ const { action } = parseAuthError(errorText);
3486
+ if (action && requestedActions.includes(action)) {
3487
+ meta.requiredAction = action;
3488
+ meta.resource = `${this.context.session.spaceId}/sql/${dbName}`;
3489
+ }
2947
3490
  }
2948
3491
  return err(
2949
3492
  serviceError(errorCode, message, "sql", { meta })
@@ -3040,6 +3583,9 @@ function serializeSqlValues(values) {
3040
3583
  (value) => value instanceof Uint8Array ? Array.from(value) : value
3041
3584
  );
3042
3585
  }
3586
+ function isMissingTableError(message) {
3587
+ return /no such table/i.test(message);
3588
+ }
3043
3589
  function migrationKey(namespace, id) {
3044
3590
  return `${encodeURIComponent(namespace)}:${encodeURIComponent(id)}`;
3045
3591
  }
@@ -3148,12 +3694,13 @@ var DuckDbService = class extends BaseService {
3148
3694
  if (!this.requireAuth()) {
3149
3695
  return err(authRequiredError("duckdb"));
3150
3696
  }
3697
+ const request = this.createRequestSignal(options?.signal);
3151
3698
  try {
3152
3699
  const response = await this.invokeDuckDb(
3153
3700
  dbName,
3154
3701
  DuckDbAction.READ,
3155
3702
  { action: "query", sql, params: params ?? [] },
3156
- options?.signal
3703
+ request.signal
3157
3704
  );
3158
3705
  if (!response.ok) {
3159
3706
  return this.handleErrorResponse(response, "query");
@@ -3162,6 +3709,8 @@ var DuckDbService = class extends BaseService {
3162
3709
  return ok(data);
3163
3710
  } catch (error) {
3164
3711
  return err(wrapError("duckdb", error));
3712
+ } finally {
3713
+ request.dispose();
3165
3714
  }
3166
3715
  });
3167
3716
  }
@@ -3170,12 +3719,13 @@ var DuckDbService = class extends BaseService {
3170
3719
  if (!this.requireAuth()) {
3171
3720
  return err(authRequiredError("duckdb"));
3172
3721
  }
3722
+ const request = this.createRequestSignal(options?.signal);
3173
3723
  try {
3174
3724
  const response = await this.invokeDuckDb(
3175
3725
  dbName,
3176
3726
  DuckDbAction.READ,
3177
3727
  { action: "query", sql, params: params ?? [] },
3178
- options?.signal,
3728
+ request.signal,
3179
3729
  { Accept: "application/vnd.apache.arrow.stream" }
3180
3730
  );
3181
3731
  if (!response.ok) {
@@ -3185,6 +3735,8 @@ var DuckDbService = class extends BaseService {
3185
3735
  return ok(buffer);
3186
3736
  } catch (error) {
3187
3737
  return err(wrapError("duckdb", error));
3738
+ } finally {
3739
+ request.dispose();
3188
3740
  }
3189
3741
  });
3190
3742
  }
@@ -3193,6 +3745,7 @@ var DuckDbService = class extends BaseService {
3193
3745
  if (!this.requireAuth()) {
3194
3746
  return err(authRequiredError("duckdb"));
3195
3747
  }
3748
+ const request = this.createRequestSignal(options?.signal);
3196
3749
  try {
3197
3750
  const body = {
3198
3751
  action: "execute",
@@ -3206,7 +3759,7 @@ var DuckDbService = class extends BaseService {
3206
3759
  dbName,
3207
3760
  DuckDbAction.WRITE,
3208
3761
  body,
3209
- options?.signal
3762
+ request.signal
3210
3763
  );
3211
3764
  if (!response.ok) {
3212
3765
  return this.handleErrorResponse(response, "execute");
@@ -3215,6 +3768,8 @@ var DuckDbService = class extends BaseService {
3215
3768
  return ok(data);
3216
3769
  } catch (error) {
3217
3770
  return err(wrapError("duckdb", error));
3771
+ } finally {
3772
+ request.dispose();
3218
3773
  }
3219
3774
  });
3220
3775
  }
@@ -3223,6 +3778,7 @@ var DuckDbService = class extends BaseService {
3223
3778
  if (!this.requireAuth()) {
3224
3779
  return err(authRequiredError("duckdb"));
3225
3780
  }
3781
+ const request = this.createRequestSignal(options?.signal);
3226
3782
  try {
3227
3783
  const body = {
3228
3784
  action: "batch",
@@ -3235,7 +3791,7 @@ var DuckDbService = class extends BaseService {
3235
3791
  dbName,
3236
3792
  DuckDbAction.WRITE,
3237
3793
  body,
3238
- options?.signal
3794
+ request.signal
3239
3795
  );
3240
3796
  if (!response.ok) {
3241
3797
  return this.handleErrorResponse(response, "batch");
@@ -3244,6 +3800,8 @@ var DuckDbService = class extends BaseService {
3244
3800
  return ok(data);
3245
3801
  } catch (error) {
3246
3802
  return err(wrapError("duckdb", error));
3803
+ } finally {
3804
+ request.dispose();
3247
3805
  }
3248
3806
  });
3249
3807
  }
@@ -3252,12 +3810,13 @@ var DuckDbService = class extends BaseService {
3252
3810
  if (!this.requireAuth()) {
3253
3811
  return err(authRequiredError("duckdb"));
3254
3812
  }
3813
+ const request = this.createRequestSignal(options?.signal);
3255
3814
  try {
3256
3815
  const response = await this.invokeDuckDb(
3257
3816
  dbName,
3258
3817
  DuckDbAction.WRITE,
3259
3818
  { action: "executeStatement", name, params: params ?? [] },
3260
- options?.signal
3819
+ request.signal
3261
3820
  );
3262
3821
  if (!response.ok) {
3263
3822
  return this.handleErrorResponse(response, "executeStatement");
@@ -3266,6 +3825,8 @@ var DuckDbService = class extends BaseService {
3266
3825
  return ok(data);
3267
3826
  } catch (error) {
3268
3827
  return err(wrapError("duckdb", error));
3828
+ } finally {
3829
+ request.dispose();
3269
3830
  }
3270
3831
  });
3271
3832
  }
@@ -3274,12 +3835,13 @@ var DuckDbService = class extends BaseService {
3274
3835
  if (!this.requireAuth()) {
3275
3836
  return err(authRequiredError("duckdb"));
3276
3837
  }
3838
+ const request = this.createRequestSignal(options?.signal);
3277
3839
  try {
3278
3840
  const response = await this.invokeDuckDb(
3279
3841
  dbName,
3280
3842
  DuckDbAction.READ,
3281
3843
  { action: "describe" },
3282
- options?.signal
3844
+ request.signal
3283
3845
  );
3284
3846
  if (!response.ok) {
3285
3847
  return this.handleErrorResponse(response, "describe");
@@ -3288,6 +3850,8 @@ var DuckDbService = class extends BaseService {
3288
3850
  return ok(data);
3289
3851
  } catch (error) {
3290
3852
  return err(wrapError("duckdb", error));
3853
+ } finally {
3854
+ request.dispose();
3291
3855
  }
3292
3856
  });
3293
3857
  }
@@ -3296,12 +3860,13 @@ var DuckDbService = class extends BaseService {
3296
3860
  if (!this.requireAuth()) {
3297
3861
  return err(authRequiredError("duckdb"));
3298
3862
  }
3863
+ const request = this.createRequestSignal(options?.signal);
3299
3864
  try {
3300
3865
  const response = await this.invokeDuckDb(
3301
3866
  dbName,
3302
3867
  DuckDbAction.EXPORT,
3303
3868
  { action: "export" },
3304
- options?.signal
3869
+ request.signal
3305
3870
  );
3306
3871
  if (!response.ok) {
3307
3872
  return this.handleErrorResponse(response, "export");
@@ -3310,6 +3875,8 @@ var DuckDbService = class extends BaseService {
3310
3875
  return ok(blob);
3311
3876
  } catch (error) {
3312
3877
  return err(wrapError("duckdb", error));
3878
+ } finally {
3879
+ request.dispose();
3313
3880
  }
3314
3881
  });
3315
3882
  }
@@ -3318,6 +3885,7 @@ var DuckDbService = class extends BaseService {
3318
3885
  if (!this.requireAuth()) {
3319
3886
  return err(authRequiredError("duckdb"));
3320
3887
  }
3888
+ const request = this.createRequestSignal(options?.signal);
3321
3889
  try {
3322
3890
  const session = this.context.session;
3323
3891
  const headers = this.context.invoke(
@@ -3333,7 +3901,7 @@ var DuckDbService = class extends BaseService {
3333
3901
  "Content-Type": "application/x-duckdb"
3334
3902
  },
3335
3903
  body: new Blob([data]),
3336
- signal: this.combineSignals(options?.signal)
3904
+ signal: request.signal
3337
3905
  });
3338
3906
  if (!response.ok) {
3339
3907
  return this.handleErrorResponse(response, "import");
@@ -3341,6 +3909,8 @@ var DuckDbService = class extends BaseService {
3341
3909
  return ok(void 0);
3342
3910
  } catch (error) {
3343
3911
  return err(wrapError("duckdb", error));
3912
+ } finally {
3913
+ request.dispose();
3344
3914
  }
3345
3915
  });
3346
3916
  }
@@ -3356,11 +3926,15 @@ var DuckDbService = class extends BaseService {
3356
3926
  ...extraHeaders
3357
3927
  },
3358
3928
  body: JSON.stringify(body),
3359
- signal: this.combineSignals(signal)
3929
+ signal
3360
3930
  });
3361
3931
  }
3362
3932
  async handleErrorResponse(response, operation) {
3363
3933
  const errorText = await response.text();
3934
+ const meta = responseErrorMeta(response.status, response.statusText, errorText);
3935
+ if (response.status === 402) {
3936
+ return err(storageRejectionError("duckdb", 402, meta, errorText));
3937
+ }
3364
3938
  const errorBody = parseServiceErrorBody(errorText);
3365
3939
  const errorCode = this.mapHttpStatusToErrorCode(
3366
3940
  response.status,
@@ -3373,7 +3947,6 @@ var DuckDbService = class extends BaseService {
3373
3947
  errorText,
3374
3948
  errorBody
3375
3949
  );
3376
- const meta = responseErrorMeta(response.status, response.statusText, errorText);
3377
3950
  if (response.status === 401) {
3378
3951
  const { resource, action } = parseAuthError(errorText);
3379
3952
  if (action) meta.requiredAction = action;
@@ -4345,6 +4918,9 @@ function defaultVaultMessage(input) {
4345
4918
  return input.message ?? `Public key not found for ${input.did}`;
4346
4919
  case "STORAGE_ERROR":
4347
4920
  return input.message ?? input.cause.message;
4921
+ case "STORAGE_QUOTA_EXCEEDED":
4922
+ case "STORAGE_LIMIT_REACHED":
4923
+ return input.message;
4348
4924
  }
4349
4925
  }
4350
4926
  function vaultError(input) {
@@ -4355,6 +4931,15 @@ function vaultError(input) {
4355
4931
  };
4356
4932
  return { ok: false, error };
4357
4933
  }
4934
+ function kvWriteError(context, error) {
4935
+ if (error.code === ErrorCodes.STORAGE_QUOTA_EXCEEDED || error.code === ErrorCodes.STORAGE_LIMIT_REACHED) {
4936
+ return vaultError({ code: error.code, message: error.message, meta: error.meta });
4937
+ }
4938
+ return vaultError({
4939
+ code: "STORAGE_ERROR",
4940
+ cause: new Error(`${context}: ${error.message}`)
4941
+ });
4942
+ }
4358
4943
  var DataVaultService = class extends BaseService {
4359
4944
  /**
4360
4945
  * Create a new DataVaultService instance.
@@ -4637,12 +5222,7 @@ var DataVaultService = class extends BaseService {
4637
5222
  JSON.stringify(envelopeResult.data)
4638
5223
  );
4639
5224
  if (!valuePutResult.ok) {
4640
- return vaultError({
4641
- code: "STORAGE_ERROR",
4642
- cause: new Error(
4643
- `Failed to store encrypted value: ${valuePutResult.error.message}`
4644
- )
4645
- });
5225
+ return kvWriteError("Failed to store encrypted value", valuePutResult.error);
4646
5226
  }
4647
5227
  return { ok: true, data: void 0 };
4648
5228
  } catch (error) {
@@ -4860,12 +5440,7 @@ var DataVaultService = class extends BaseService {
4860
5440
  keyPayload
4861
5441
  );
4862
5442
  if (!keyPutResult.ok) {
4863
- return vaultError({
4864
- code: "STORAGE_ERROR",
4865
- cause: new Error(
4866
- `Failed to store key blob: ${keyPutResult.error.message}`
4867
- )
4868
- });
5443
+ return kvWriteError("Failed to store key blob", keyPutResult.error);
4869
5444
  }
4870
5445
  const valuePayload = JSON.stringify({
4871
5446
  data: base64Encode(encrypted),
@@ -4876,12 +5451,7 @@ var DataVaultService = class extends BaseService {
4876
5451
  valuePayload
4877
5452
  );
4878
5453
  if (!valuePutResult.ok) {
4879
- return vaultError({
4880
- code: "STORAGE_ERROR",
4881
- cause: new Error(
4882
- `Failed to store encrypted value: ${valuePutResult.error.message}`
4883
- )
4884
- });
5454
+ return kvWriteError("Failed to store encrypted value", valuePutResult.error);
4885
5455
  }
4886
5456
  return ok(void 0);
4887
5457
  } catch (error) {
@@ -5212,12 +5782,7 @@ var DataVaultService = class extends BaseService {
5212
5782
  grantPayload
5213
5783
  );
5214
5784
  if (!grantPutResult.ok) {
5215
- return vaultError({
5216
- code: "STORAGE_ERROR",
5217
- cause: new Error(
5218
- `Failed to store grant: ${grantPutResult.error.message}`
5219
- )
5220
- });
5785
+ return kvWriteError("Failed to store grant", grantPutResult.error);
5221
5786
  }
5222
5787
  return ok(void 0);
5223
5788
  } catch (error) {
@@ -5562,12 +6127,7 @@ var DataVaultService = class extends BaseService {
5562
6127
  keyPayload
5563
6128
  );
5564
6129
  if (!keyPutResult.ok) {
5565
- return vaultError({
5566
- code: "STORAGE_ERROR",
5567
- cause: new Error(
5568
- `Failed to store rotated key blob: ${keyPutResult.error.message}`
5569
- )
5570
- });
6130
+ return kvWriteError("Failed to store rotated key blob", keyPutResult.error);
5571
6131
  }
5572
6132
  const valuePayload = JSON.stringify({
5573
6133
  data: base64Encode(encrypted),
@@ -5578,12 +6138,7 @@ var DataVaultService = class extends BaseService {
5578
6138
  valuePayload
5579
6139
  );
5580
6140
  if (!valuePutResult.ok) {
5581
- return vaultError({
5582
- code: "STORAGE_ERROR",
5583
- cause: new Error(
5584
- `Failed to store re-encrypted value: ${valuePutResult.error.message}`
5585
- )
5586
- });
6141
+ return kvWriteError("Failed to store re-encrypted value", valuePutResult.error);
5587
6142
  }
5588
6143
  for (const did of remainingGrantees) {
5589
6144
  const grantResult = await this.reencrypt(key, did);
@@ -5792,6 +6347,7 @@ var SecretsService = class {
5792
6347
  }
5793
6348
  async listAll() {
5794
6349
  const entries = /* @__PURE__ */ new Map();
6350
+ const seenCursors = /* @__PURE__ */ new Set();
5795
6351
  let cursor;
5796
6352
  do {
5797
6353
  const result = await this.vault.listPage({
@@ -5814,6 +6370,14 @@ var SecretsService = class {
5814
6370
  "Secret catalog listing was truncated without a continuation cursor."
5815
6371
  );
5816
6372
  }
6373
+ if (cursor !== void 0) {
6374
+ if (seenCursors.has(cursor)) {
6375
+ return invalidSecretInput(
6376
+ "Secret catalog listing repeated a continuation cursor; the node does not support paged KV lists."
6377
+ );
6378
+ }
6379
+ seenCursors.add(cursor);
6380
+ }
5817
6381
  } while (cursor !== void 0);
5818
6382
  return {
5819
6383
  ok: true,
@@ -5934,103 +6498,6 @@ function canonicalHashHex(sha256, value) {
5934
6498
  return hexEncode2(sha256(utf8Encode(canonical)));
5935
6499
  }
5936
6500
 
5937
- // src/encryption/networkId.ts
5938
- var URN_PREFIX = "urn:tinycloud:encryption:";
5939
- var NETWORK_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
5940
- var PKH_EIP155_DID_RE = /^did:pkh:eip155:(\d+):(0x[a-fA-F0-9]{40})$/;
5941
- var NetworkIdError = class extends Error {
5942
- constructor(message) {
5943
- super(message);
5944
- this.name = "NetworkIdError";
5945
- }
5946
- };
5947
- function parseNetworkId(networkId) {
5948
- if (typeof networkId !== "string" || networkId.length === 0) {
5949
- throw new NetworkIdError("networkId must be a non-empty string");
5950
- }
5951
- if (!networkId.startsWith(URN_PREFIX)) {
5952
- throw new NetworkIdError(
5953
- `networkId must start with ${URN_PREFIX} (got ${JSON.stringify(networkId)})`
5954
- );
5955
- }
5956
- const body = networkId.slice(URN_PREFIX.length);
5957
- const lastColon = body.lastIndexOf(":");
5958
- if (lastColon <= 0 || lastColon === body.length - 1) {
5959
- throw new NetworkIdError(
5960
- `networkId missing ownerDid or name segment (got ${JSON.stringify(networkId)})`
5961
- );
5962
- }
5963
- const ownerDid = body.slice(0, lastColon);
5964
- const name = body.slice(lastColon + 1);
5965
- if (!ownerDid.startsWith("did:")) {
5966
- throw new NetworkIdError(
5967
- `networkId ownerDid must be a DID (got ${JSON.stringify(ownerDid)})`
5968
- );
5969
- }
5970
- const didParts = ownerDid.split(":");
5971
- if (didParts.length < 3 || didParts.some((p) => p.length === 0)) {
5972
- throw new NetworkIdError(
5973
- `networkId ownerDid is not a well-formed DID (got ${JSON.stringify(ownerDid)})`
5974
- );
5975
- }
5976
- if (!NETWORK_NAME_RE.test(name)) {
5977
- throw new NetworkIdError(
5978
- `networkId name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
5979
- );
5980
- }
5981
- return { networkId, ownerDid, name };
5982
- }
5983
- function buildNetworkId(ownerDid, name) {
5984
- if (typeof ownerDid !== "string" || !ownerDid.startsWith("did:")) {
5985
- throw new NetworkIdError("ownerDid must be a DID");
5986
- }
5987
- if (typeof name !== "string" || !NETWORK_NAME_RE.test(name)) {
5988
- throw new NetworkIdError(
5989
- `network name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
5990
- );
5991
- }
5992
- const networkId = `${URN_PREFIX}${ownerDid}:${name}`;
5993
- parseNetworkId(networkId);
5994
- return networkId;
5995
- }
5996
- function isNetworkId(networkId) {
5997
- if (typeof networkId !== "string") {
5998
- return false;
5999
- }
6000
- try {
6001
- parseNetworkId(networkId);
6002
- return true;
6003
- } catch {
6004
- return false;
6005
- }
6006
- }
6007
- function parsePkhOwnerDid(ownerDid) {
6008
- const match = ownerDid.match(PKH_EIP155_DID_RE);
6009
- if (!match) return null;
6010
- return {
6011
- chainId: match[1],
6012
- address: match[2].toLowerCase()
6013
- };
6014
- }
6015
- function ownerDidMatches(a, b) {
6016
- const aPkh = parsePkhOwnerDid(a);
6017
- const bPkh = parsePkhOwnerDid(b);
6018
- if (aPkh && bPkh) {
6019
- return aPkh.chainId === bPkh.chainId && aPkh.address === bPkh.address;
6020
- }
6021
- return a === b;
6022
- }
6023
- function networkDiscoveryKey(name) {
6024
- if (!NETWORK_NAME_RE.test(name)) {
6025
- throw new NetworkIdError(
6026
- `network name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
6027
- );
6028
- }
6029
- return `.well-known/encryption/network/${name}`;
6030
- }
6031
- var ENCRYPTION_NETWORK_URN_PREFIX = URN_PREFIX;
6032
- var NETWORK_NAME_PATTERN = NETWORK_NAME_RE;
6033
-
6034
6501
  // src/encryption/types.ts
6035
6502
  var DEFAULT_ENCRYPTION_ALG = "x25519-aes256gcm/v1";
6036
6503
  var ENVELOPE_VERSION = 1;
@@ -6912,6 +7379,8 @@ export {
6912
7379
  SQLAction,
6913
7380
  SQLMigrations,
6914
7381
  SQLService,
7382
+ STORAGE_FULL_MESSAGE,
7383
+ STORAGE_WRITE_TOO_LARGE_MESSAGE,
6915
7384
  SecretsService,
6916
7385
  ServiceContext,
6917
7386
  ServiceErrorEventSchema,
@@ -6964,22 +7433,26 @@ export {
6964
7433
  hexEncode2 as hexEncode,
6965
7434
  installTinyCloudDebugGlobals,
6966
7435
  isNetworkId,
7436
+ isStorageFullError,
6967
7437
  networkDiscoveryKey,
6968
7438
  networkError,
6969
7439
  notFoundError,
6970
7440
  ok,
6971
7441
  openWrappedKey,
6972
7442
  parseAuthError,
7443
+ parseCapabilityResource,
6973
7444
  parseNetworkId,
6974
7445
  parsePermissionHint,
6975
7446
  parsePermissionHintFromErrorText,
6976
7447
  parseSecretCatalogKey,
7448
+ parseStorageQuotaBytes,
6977
7449
  permissionDeniedError,
6978
7450
  resolveSecretListPrefix,
6979
7451
  resolveSecretPath,
6980
7452
  serviceError,
6981
7453
  storageLimitReachedError,
6982
7454
  storageQuotaExceededError,
7455
+ storageRejectionError,
6983
7456
  timeoutError,
6984
7457
  tinyCloudDebugLogger,
6985
7458
  utf8Decode,
@@ -6993,6 +7466,7 @@ export {
6993
7466
  validateServiceResponseEvent,
6994
7467
  validateServiceSession,
6995
7468
  validateTelemetrySpanEvent,
7469
+ validatedCapabilityOf,
6996
7470
  verifyDecryptResponse,
6997
7471
  wrapError
6998
7472
  };