@tinycloud/sdk-services 3.1.0-beta.1 → 3.1.0-beta.6

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
@@ -896,6 +896,106 @@ var ServiceContext = class {
896
896
  }
897
897
  };
898
898
 
899
+ // src/errors.ts
900
+ import { CAPABILITY_REGISTRY } from "@tinycloud/bootstrap";
901
+
902
+ // src/encryption/networkId.ts
903
+ var URN_PREFIX = "urn:tinycloud:encryption:";
904
+ var NETWORK_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
905
+ var PKH_EIP155_DID_RE = /^did:pkh:eip155:(\d+):(0x[a-fA-F0-9]{40})$/;
906
+ var NetworkIdError = class extends Error {
907
+ constructor(message) {
908
+ super(message);
909
+ this.name = "NetworkIdError";
910
+ }
911
+ };
912
+ function parseNetworkId(networkId) {
913
+ if (typeof networkId !== "string" || networkId.length === 0) {
914
+ throw new NetworkIdError("networkId must be a non-empty string");
915
+ }
916
+ if (!networkId.startsWith(URN_PREFIX)) {
917
+ throw new NetworkIdError(
918
+ `networkId must start with ${URN_PREFIX} (got ${JSON.stringify(networkId)})`
919
+ );
920
+ }
921
+ const body = networkId.slice(URN_PREFIX.length);
922
+ const lastColon = body.lastIndexOf(":");
923
+ if (lastColon <= 0 || lastColon === body.length - 1) {
924
+ throw new NetworkIdError(
925
+ `networkId missing ownerDid or name segment (got ${JSON.stringify(networkId)})`
926
+ );
927
+ }
928
+ const ownerDid = body.slice(0, lastColon);
929
+ const name = body.slice(lastColon + 1);
930
+ if (!ownerDid.startsWith("did:")) {
931
+ throw new NetworkIdError(
932
+ `networkId ownerDid must be a DID (got ${JSON.stringify(ownerDid)})`
933
+ );
934
+ }
935
+ const didParts = ownerDid.split(":");
936
+ if (didParts.length < 3 || didParts.some((p) => p.length === 0)) {
937
+ throw new NetworkIdError(
938
+ `networkId ownerDid is not a well-formed DID (got ${JSON.stringify(ownerDid)})`
939
+ );
940
+ }
941
+ if (!NETWORK_NAME_RE.test(name)) {
942
+ throw new NetworkIdError(
943
+ `networkId name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
944
+ );
945
+ }
946
+ return { networkId, ownerDid, name };
947
+ }
948
+ function buildNetworkId(ownerDid, name) {
949
+ if (typeof ownerDid !== "string" || !ownerDid.startsWith("did:")) {
950
+ throw new NetworkIdError("ownerDid must be a DID");
951
+ }
952
+ if (typeof name !== "string" || !NETWORK_NAME_RE.test(name)) {
953
+ throw new NetworkIdError(
954
+ `network name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
955
+ );
956
+ }
957
+ const networkId = `${URN_PREFIX}${ownerDid}:${name}`;
958
+ parseNetworkId(networkId);
959
+ return networkId;
960
+ }
961
+ function isNetworkId(networkId) {
962
+ if (typeof networkId !== "string") {
963
+ return false;
964
+ }
965
+ try {
966
+ parseNetworkId(networkId);
967
+ return true;
968
+ } catch {
969
+ return false;
970
+ }
971
+ }
972
+ function parsePkhOwnerDid(ownerDid) {
973
+ const match = ownerDid.match(PKH_EIP155_DID_RE);
974
+ if (!match) return null;
975
+ return {
976
+ chainId: match[1],
977
+ address: match[2].toLowerCase()
978
+ };
979
+ }
980
+ function ownerDidMatches(a, b) {
981
+ const aPkh = parsePkhOwnerDid(a);
982
+ const bPkh = parsePkhOwnerDid(b);
983
+ if (aPkh && bPkh) {
984
+ return aPkh.chainId === bPkh.chainId && aPkh.address === bPkh.address;
985
+ }
986
+ return a === b;
987
+ }
988
+ function networkDiscoveryKey(name) {
989
+ if (!NETWORK_NAME_RE.test(name)) {
990
+ throw new NetworkIdError(
991
+ `network name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
992
+ );
993
+ }
994
+ return `.well-known/encryption/network/${name}`;
995
+ }
996
+ var ENCRYPTION_NETWORK_URN_PREFIX = URN_PREFIX;
997
+ var NETWORK_NAME_PATTERN = NETWORK_NAME_RE;
998
+
899
999
  // src/errors.ts
900
1000
  function parsePermissionHint(value) {
901
1001
  if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
@@ -993,6 +1093,70 @@ function parseAuthError(responseText) {
993
1093
  function authUnauthorizedError(service, message, meta) {
994
1094
  return serviceError(ErrorCodes.AUTH_UNAUTHORIZED, message, service, { meta });
995
1095
  }
1096
+ var grantableActions = new Set(
1097
+ CAPABILITY_REGISTRY.map(({ urn }) => urn).filter((urn) => !urn.endsWith("/*"))
1098
+ );
1099
+ var capabilityServices = new Set(
1100
+ CAPABILITY_REGISTRY.map(({ service }) => service.slice("tinycloud.".length))
1101
+ );
1102
+ function parseCapabilityResource(resource, service) {
1103
+ if (resource.length === 0 || resource.length > 1024 || !capabilityServices.has(service) || /[\s\p{C}*#]/u.test(resource) || resource.includes("://")) return void 0;
1104
+ const segments = resource.split("/");
1105
+ if (!/^tinycloud:[A-Za-z0-9][A-Za-z0-9:._-]*$/.test(segments[0])) return void 0;
1106
+ const serviceIndex = segments.findIndex(
1107
+ (segment, index) => index > 0 && capabilityServices.has(segment)
1108
+ );
1109
+ if (serviceIndex < 1 || segments[serviceIndex] !== service) return void 0;
1110
+ if (segments.slice(1, serviceIndex).some(
1111
+ (segment) => segment.length === 0 || segment === "." || segment === ".."
1112
+ )) return void 0;
1113
+ const pathSegments = segments.slice(serviceIndex + 1);
1114
+ if (pathSegments.some((segment) => segment === "." || segment === "..")) return void 0;
1115
+ if (service !== "kv" && (pathSegments.length === 0 || pathSegments[pathSegments.length - 1] === "")) return void 0;
1116
+ return {
1117
+ space: segments.slice(0, serviceIndex).join("/"),
1118
+ path: pathSegments.join("/")
1119
+ };
1120
+ }
1121
+ function safeCapabilityResource(value, service, action) {
1122
+ if (typeof value !== "string" || value.length === 0 || value.length > 1024) return false;
1123
+ if (service === "encryption" && value.startsWith("urn:tinycloud:encryption:")) {
1124
+ if (!/^[A-Za-z0-9_][A-Za-z0-9_.:/-]*$/.test(value) || value.includes("//") || value.split("/").some((segment) => segment === "." || segment === "..")) return false;
1125
+ try {
1126
+ parseNetworkId(value);
1127
+ return true;
1128
+ } catch {
1129
+ return false;
1130
+ }
1131
+ }
1132
+ const parsed = parseCapabilityResource(value, service);
1133
+ return parsed !== void 0 && (!parsed.path.endsWith("/") || service === "kv" && action === "tinycloud.kv/list") && (parsed.path !== "" || service === "kv" && action === "tinycloud.kv/list");
1134
+ }
1135
+ function validatedCapabilityOf(error) {
1136
+ const verdict = authorizationVerdictOf(error);
1137
+ if (verdict !== "unauthenticated" && verdict !== "forbidden") return void 0;
1138
+ const seen = /* @__PURE__ */ new Set();
1139
+ let node = error;
1140
+ for (let depth = 0; depth < MAX_CAUSE_DEPTH && typeof node === "object" && node !== null && !seen.has(node); depth += 1) {
1141
+ seen.add(node);
1142
+ const record = node;
1143
+ const status = errorStatusOf(record);
1144
+ const meta = record.meta;
1145
+ if ((status === 401 || status === 403 || status === void 0 && record.code === ErrorCodes.AUTH_UNAUTHORIZED) && typeof meta === "object" && meta !== null && !Array.isArray(meta)) {
1146
+ const metadata = meta;
1147
+ const { resource, requiredAction } = metadata;
1148
+ if (typeof requiredAction === "string" && grantableActions.has(requiredAction)) {
1149
+ const actionService = requiredAction.slice("tinycloud.".length, requiredAction.indexOf("/"));
1150
+ const explicitService = record.service === void 0 ? metadata.service : record.service;
1151
+ if (safeCapabilityResource(resource, actionService, requiredAction) && (explicitService === void 0 || explicitService === actionService || explicitService === `tinycloud.${actionService}`)) {
1152
+ return { resource, requiredAction };
1153
+ }
1154
+ }
1155
+ }
1156
+ node = record.cause;
1157
+ }
1158
+ return void 0;
1159
+ }
996
1160
  var MAX_CAUSE_DEPTH = 8;
997
1161
  function errorStatusOf(node) {
998
1162
  const meta = node.meta;
@@ -1040,6 +1204,23 @@ function storageLimitReachedError(service, message, meta) {
1040
1204
  meta
1041
1205
  };
1042
1206
  }
1207
+ 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.";
1208
+ 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.";
1209
+ function parseStorageQuotaBytes(responseText) {
1210
+ const match = responseText.match(/Used:\s*(\d+)\s*bytes,\s*Limit:\s*(\d+)\s*bytes/i);
1211
+ if (!match) return void 0;
1212
+ return {
1213
+ usedBytes: parseInt(match[1], 10),
1214
+ limitBytes: parseInt(match[2], 10)
1215
+ };
1216
+ }
1217
+ function storageRejectionError(service, status, meta, responseText) {
1218
+ const quotaMeta = { ...meta, ...parseStorageQuotaBytes(responseText) };
1219
+ return status === 402 ? storageQuotaExceededError(service, STORAGE_FULL_MESSAGE, quotaMeta) : storageLimitReachedError(service, STORAGE_WRITE_TOO_LARGE_MESSAGE, quotaMeta);
1220
+ }
1221
+ function isStorageFullError(error) {
1222
+ return error?.code === ErrorCodes.STORAGE_QUOTA_EXCEEDED || error?.code === ErrorCodes.STORAGE_LIMIT_REACHED;
1223
+ }
1043
1224
  function wrapError(service, error, defaultCode = ErrorCodes.NETWORK_ERROR) {
1044
1225
  if (error instanceof Error) {
1045
1226
  if (error.name === "AbortError") {
@@ -1433,47 +1614,21 @@ var KVService = class extends BaseService {
1433
1614
  get config() {
1434
1615
  return this._config;
1435
1616
  }
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
1617
  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
- );
1618
+ if (response.status !== 402 && response.status !== 413) {
1619
+ return void 0;
1462
1620
  }
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
- );
1621
+ if (response.status === 413 && !parseStorageQuotaBytes(errorText)) {
1622
+ return void 0;
1475
1623
  }
1476
- return void 0;
1624
+ return err(
1625
+ storageRejectionError(
1626
+ "kv",
1627
+ response.status,
1628
+ { status: response.status, statusText: response.statusText, key },
1629
+ errorText
1630
+ )
1631
+ );
1477
1632
  }
1478
1633
  /**
1479
1634
  * Classify a KV 404 by reading the response body once.
@@ -1749,12 +1904,14 @@ var KVService = class extends BaseService {
1749
1904
  signal: this.combineSignals(options?.signal)
1750
1905
  });
1751
1906
  if (!response.ok) {
1752
- const errorText = await response.text();
1907
+ const errorText = response.status === 401 || response.status === 403 ? await this.readAuthorizationText(response) : await response.text();
1753
1908
  if (response.status === 401 || response.status === 403) {
1754
1909
  return this.authorizationFailure(
1755
1910
  `Failed to batch read ${keys.length} key(s)`,
1756
1911
  response,
1757
- errorText
1912
+ errorText,
1913
+ paths,
1914
+ action
1758
1915
  );
1759
1916
  }
1760
1917
  if (response.status === 413) {
@@ -1844,7 +2001,7 @@ var KVService = class extends BaseService {
1844
2001
  return text;
1845
2002
  }
1846
2003
  }
1847
- async createSignedReadUrlError(response, key) {
2004
+ async createSignedReadUrlError(response, key, path) {
1848
2005
  let errorText = response.statusText;
1849
2006
  try {
1850
2007
  const text = await response.text();
@@ -1857,7 +2014,9 @@ var KVService = class extends BaseService {
1857
2014
  return this.authorizationFailure(
1858
2015
  `Failed to create signed read URL for key ${JSON.stringify(key)}`,
1859
2016
  response,
1860
- errorText
2017
+ errorText,
2018
+ [path],
2019
+ KVAction.GET
1861
2020
  );
1862
2021
  }
1863
2022
  const code = response.status === 400 ? ErrorCodes.INVALID_INPUT : ErrorCodes.NETWORK_ERROR;
@@ -1870,21 +2029,42 @@ var KVService = class extends BaseService {
1870
2029
  )
1871
2030
  );
1872
2031
  }
2032
+ /** A known 401/403 keeps its typed status even if reading its body fails. */
2033
+ async readAuthorizationText(response) {
2034
+ try {
2035
+ return await response.text();
2036
+ } catch {
2037
+ return "";
2038
+ }
2039
+ }
1873
2040
  /**
1874
2041
  * AUTH_UNAUTHORIZED for a 401/403 response. The message keeps the HTTP
1875
2042
  * status and the server text (`<context>: <status> - <text>`, the same
1876
2043
  * shape as every other KV failure) so callers that rethrow only the message
1877
2044
  * still see the status; `meta.status` carries it for typed callers.
1878
- * Resource/ability hints are parsed from the raw body, never the message.
2045
+ * Server hints must match the requested path/action (whether the node sends
2046
+ * a relative path or full resource); metadata uses only the request-derived
2047
+ * canonical resource under the authenticated session's space.
1879
2048
  */
1880
- authorizationFailure(context, response, errorText, extraMeta = {}) {
2049
+ authorizationFailure(context, response, errorText, paths, expectedAction, extraMeta = {}) {
1881
2050
  const { resource, action } = parseAuthError(errorText);
2051
+ const requestedPath = paths.find(
2052
+ (path) => resource === path || resource === `${this.context.session.spaceId}/kv/${path}`
2053
+ );
2054
+ const capability = requestedPath !== void 0 && action === expectedAction ? validatedCapabilityOf({
2055
+ service: "kv",
2056
+ code: ErrorCodes.AUTH_UNAUTHORIZED,
2057
+ meta: {
2058
+ status: response.status,
2059
+ resource: `${this.context.session.spaceId}/kv/${requestedPath}`,
2060
+ requiredAction: action
2061
+ }
2062
+ }) : void 0;
1882
2063
  const detail = errorText.trim().length > 0 ? errorText : response.statusText || "authorization failed";
1883
2064
  return err(authUnauthorizedError("kv", `${context}: ${response.status} - ${detail}`, {
1884
2065
  status: response.status,
1885
- ...action && { requiredAction: action },
1886
- ...resource && { resource },
1887
- ...extraMeta
2066
+ ...extraMeta,
2067
+ ...capability
1888
2068
  }));
1889
2069
  }
1890
2070
  normalizeSignedReadUrlResponse(data) {
@@ -1928,13 +2108,24 @@ var KVService = class extends BaseService {
1928
2108
  );
1929
2109
  if (!response.ok) {
1930
2110
  if (response.status === 401 || response.status === 403) {
1931
- const errorText2 = await response.text();
2111
+ const errorText2 = await this.readAuthorizationText(response);
1932
2112
  const permissionHint = parsePermissionHintFromErrorText(errorText2);
2113
+ const structuredCapability = permissionHint?.service === "tinycloud.kv" && permissionHint.space === this.context.session.spaceId && permissionHint.path === path ? validatedCapabilityOf({
2114
+ service: "kv",
2115
+ code: ErrorCodes.AUTH_UNAUTHORIZED,
2116
+ meta: {
2117
+ status: response.status,
2118
+ resource: `${this.context.session.spaceId}/kv/${path}`,
2119
+ requiredAction: KVAction.GET
2120
+ }
2121
+ }) : void 0;
1933
2122
  return this.authorizationFailure(
1934
2123
  `Failed to get key ${JSON.stringify(key)}`,
1935
2124
  response,
1936
2125
  errorText2,
1937
- permissionHint === void 0 ? {} : { permissionHint }
2126
+ [path],
2127
+ KVAction.GET,
2128
+ structuredCapability ? { permissionHint, ...structuredCapability } : {}
1938
2129
  );
1939
2130
  }
1940
2131
  if (response.status === 404) {
@@ -2011,7 +2202,9 @@ var KVService = class extends BaseService {
2011
2202
  return this.authorizationFailure(
2012
2203
  `Failed to put key ${JSON.stringify(key)}`,
2013
2204
  response,
2014
- await response.text()
2205
+ await this.readAuthorizationText(response),
2206
+ [path],
2207
+ KVAction.PUT
2015
2208
  );
2016
2209
  }
2017
2210
  if (!response.ok) {
@@ -2140,7 +2333,9 @@ var KVService = class extends BaseService {
2140
2333
  return this.authorizationFailure(
2141
2334
  `Failed to batch put ${items.length} key(s)`,
2142
2335
  response,
2143
- errorText
2336
+ errorText,
2337
+ paths,
2338
+ KVAction.PUT
2144
2339
  );
2145
2340
  }
2146
2341
  const quotaError = this.handleQuotaErrorResponse(
@@ -2261,11 +2456,13 @@ var KVService = class extends BaseService {
2261
2456
  }
2262
2457
  );
2263
2458
  if (!response.ok) {
2264
- if (response.status === 401) {
2459
+ if (response.status === 401 || response.status === 403) {
2265
2460
  return this.authorizationFailure(
2266
2461
  "Failed to list keys",
2267
2462
  response,
2268
- await response.text()
2463
+ await this.readAuthorizationText(response),
2464
+ [listPath],
2465
+ KVAction.LIST
2269
2466
  );
2270
2467
  }
2271
2468
  const errorText = await response.text();
@@ -2314,11 +2511,13 @@ var KVService = class extends BaseService {
2314
2511
  options?.ifMatch === void 0 ? void 0 : { "if-match": options.ifMatch }
2315
2512
  );
2316
2513
  if (!response.ok) {
2317
- if (response.status === 401) {
2514
+ if (response.status === 401 || response.status === 403) {
2318
2515
  return this.authorizationFailure(
2319
2516
  `Failed to delete key ${JSON.stringify(key)}`,
2320
2517
  response,
2321
- await response.text()
2518
+ await this.readAuthorizationText(response),
2519
+ [path],
2520
+ KVAction.DELETE
2322
2521
  );
2323
2522
  }
2324
2523
  if (response.status === 404) {
@@ -2376,11 +2575,13 @@ var KVService = class extends BaseService {
2376
2575
  options?.signal
2377
2576
  );
2378
2577
  if (!response.ok) {
2379
- if (response.status === 401) {
2578
+ if (response.status === 401 || response.status === 403) {
2380
2579
  return this.authorizationFailure(
2381
2580
  `Failed to get metadata for key ${JSON.stringify(key)}`,
2382
2581
  response,
2383
- await response.text()
2582
+ await this.readAuthorizationText(response),
2583
+ [path],
2584
+ KVAction.HEAD
2384
2585
  );
2385
2586
  }
2386
2587
  if (response.status === 404) {
@@ -2447,7 +2648,7 @@ var KVService = class extends BaseService {
2447
2648
  signal: this.combineSignals(options?.signal)
2448
2649
  });
2449
2650
  if (!response.ok) {
2450
- return this.createSignedReadUrlError(response, key);
2651
+ return this.createSignedReadUrlError(response, key, path);
2451
2652
  }
2452
2653
  const signedUrl = this.normalizeSignedReadUrlResponse(
2453
2654
  await response.json()
@@ -2685,14 +2886,15 @@ var SQLService = class extends BaseService {
2685
2886
  };
2686
2887
  if (options?.maxRows !== void 0) body.maxRows = options.maxRows;
2687
2888
  if (options?.maxBytes !== void 0) body.maxBytes = options.maxBytes;
2889
+ const action = this.actionForSql(sql, SQLAction.READ);
2688
2890
  const response = await this.invokeSQL(
2689
2891
  dbName,
2690
- this.actionForSql(sql, SQLAction.READ),
2892
+ action,
2691
2893
  body,
2692
2894
  options?.signal
2693
2895
  );
2694
2896
  if (!response.ok) {
2695
- return this.handleErrorResponse(response, "query");
2897
+ return this.handleErrorResponse(response, "query", dbName, [action]);
2696
2898
  }
2697
2899
  const data = await response.json();
2698
2900
  return ok(data);
@@ -2721,14 +2923,15 @@ var SQLService = class extends BaseService {
2721
2923
  (statement) => this.actionForSql(statement, SQLAction.SCHEMA)
2722
2924
  )
2723
2925
  ];
2926
+ const requestedActions = this.dedupeActions(actions);
2724
2927
  const response = await this.invokeSQL(
2725
2928
  dbName,
2726
- this.dedupeActions(actions),
2929
+ requestedActions,
2727
2930
  body,
2728
2931
  options?.signal
2729
2932
  );
2730
2933
  if (!response.ok) {
2731
- return this.handleErrorResponse(response, "execute");
2934
+ return this.handleErrorResponse(response, "execute", dbName, requestedActions);
2732
2935
  }
2733
2936
  const data = await response.json();
2734
2937
  return ok(data);
@@ -2743,14 +2946,15 @@ var SQLService = class extends BaseService {
2743
2946
  return err(authRequiredError("sql"));
2744
2947
  }
2745
2948
  try {
2949
+ const requestedActions = this.actionsForSqlBatch(statements);
2746
2950
  const response = await this.invokeSQL(
2747
2951
  dbName,
2748
- this.actionsForSqlBatch(statements),
2952
+ requestedActions,
2749
2953
  { action: "batch", statements },
2750
2954
  options?.signal
2751
2955
  );
2752
2956
  if (!response.ok) {
2753
- return this.handleErrorResponse(response, "batch");
2957
+ return this.handleErrorResponse(response, "batch", dbName, requestedActions);
2754
2958
  }
2755
2959
  const data = await response.json();
2756
2960
  return ok(data);
@@ -2772,7 +2976,7 @@ var SQLService = class extends BaseService {
2772
2976
  options?.signal
2773
2977
  );
2774
2978
  if (!response.ok) {
2775
- return this.handleErrorResponse(response, "executeStatement");
2979
+ return this.handleErrorResponse(response, "executeStatement", dbName, [SQLAction.WRITE]);
2776
2980
  }
2777
2981
  const data = await response.json();
2778
2982
  return ok(data);
@@ -2794,7 +2998,7 @@ var SQLService = class extends BaseService {
2794
2998
  options?.signal
2795
2999
  );
2796
3000
  if (!response.ok) {
2797
- return this.handleErrorResponse(response, "export");
3001
+ return this.handleErrorResponse(response, "export", dbName, [SQLAction.READ]);
2798
3002
  }
2799
3003
  const resp = response;
2800
3004
  if (typeof resp.blob === "function") {
@@ -2810,21 +3014,24 @@ var SQLService = class extends BaseService {
2810
3014
  }
2811
3015
  async applyMigrationsOnDbUnlocked(dbName, options) {
2812
3016
  return this.withTelemetry("migrations.apply", dbName, async () => {
2813
- const created = await this.ensureMigrationsTable(dbName, options.signal);
2814
- if (!created.ok) return created;
2815
3017
  const listed = await this.queryOnDb(
2816
3018
  dbName,
2817
3019
  `SELECT id FROM ${MIGRATIONS_TABLE} WHERE namespace = ? ORDER BY applied_at, id`,
2818
3020
  [options.namespace],
2819
3021
  { signal: options.signal }
2820
3022
  );
2821
- if (!listed.ok) return listed;
3023
+ const tableMissing = !listed.ok && (listed.error.code === ErrorCodes.SQL_DATABASE_NOT_FOUND || isMissingTableError(listed.error.message));
3024
+ if (!listed.ok && !tableMissing) return listed;
2822
3025
  const appliedIds = new Set(
2823
- listed.data.rows.map((row) => rowValue(row, 0)).filter((id) => typeof id === "string")
3026
+ (listed.ok ? listed.data.rows : []).map((row) => rowValue(row, 0)).filter((id) => typeof id === "string")
2824
3027
  );
2825
3028
  const skipped = options.migrations.filter((migration) => appliedIds.has(migration.id)).map((migration) => migration.id);
2826
3029
  const pending = options.migrations.filter((migration) => !appliedIds.has(migration.id));
2827
3030
  const applied = [];
3031
+ if (pending.length > 0 && tableMissing) {
3032
+ const created = await this.ensureMigrationsTable(dbName, options.signal);
3033
+ if (!created.ok) return created;
3034
+ }
2828
3035
  for (const migration of pending) {
2829
3036
  const result = await this.applyOneMigration(dbName, options.namespace, migration, options.signal);
2830
3037
  if (!result.ok) return result;
@@ -2925,8 +3132,12 @@ var SQLService = class extends BaseService {
2925
3132
  }))
2926
3133
  );
2927
3134
  }
2928
- async handleErrorResponse(response, operation) {
3135
+ async handleErrorResponse(response, operation, dbName, requestedActions) {
2929
3136
  const errorText = await response.text();
3137
+ const meta = responseErrorMeta(response.status, response.statusText, errorText);
3138
+ if (response.status === 402) {
3139
+ return err(storageRejectionError("sql", 402, meta, errorText));
3140
+ }
2930
3141
  const errorBody = parseServiceErrorBody(errorText);
2931
3142
  const errorCode = this.mapHttpStatusToErrorCode(
2932
3143
  response.status,
@@ -2939,11 +3150,12 @@ var SQLService = class extends BaseService {
2939
3150
  errorText,
2940
3151
  errorBody
2941
3152
  );
2942
- const meta = responseErrorMeta(response.status, response.statusText, errorText);
2943
3153
  if (response.status === 401) {
2944
- const { resource, action } = parseAuthError(errorText);
2945
- if (action) meta.requiredAction = action;
2946
- if (resource) meta.resource = resource;
3154
+ const { action } = parseAuthError(errorText);
3155
+ if (action && requestedActions.includes(action)) {
3156
+ meta.requiredAction = action;
3157
+ meta.resource = `${this.context.session.spaceId}/sql/${dbName}`;
3158
+ }
2947
3159
  }
2948
3160
  return err(
2949
3161
  serviceError(errorCode, message, "sql", { meta })
@@ -3040,6 +3252,9 @@ function serializeSqlValues(values) {
3040
3252
  (value) => value instanceof Uint8Array ? Array.from(value) : value
3041
3253
  );
3042
3254
  }
3255
+ function isMissingTableError(message) {
3256
+ return /no such table/i.test(message);
3257
+ }
3043
3258
  function migrationKey(namespace, id) {
3044
3259
  return `${encodeURIComponent(namespace)}:${encodeURIComponent(id)}`;
3045
3260
  }
@@ -3361,6 +3576,10 @@ var DuckDbService = class extends BaseService {
3361
3576
  }
3362
3577
  async handleErrorResponse(response, operation) {
3363
3578
  const errorText = await response.text();
3579
+ const meta = responseErrorMeta(response.status, response.statusText, errorText);
3580
+ if (response.status === 402) {
3581
+ return err(storageRejectionError("duckdb", 402, meta, errorText));
3582
+ }
3364
3583
  const errorBody = parseServiceErrorBody(errorText);
3365
3584
  const errorCode = this.mapHttpStatusToErrorCode(
3366
3585
  response.status,
@@ -3373,7 +3592,6 @@ var DuckDbService = class extends BaseService {
3373
3592
  errorText,
3374
3593
  errorBody
3375
3594
  );
3376
- const meta = responseErrorMeta(response.status, response.statusText, errorText);
3377
3595
  if (response.status === 401) {
3378
3596
  const { resource, action } = parseAuthError(errorText);
3379
3597
  if (action) meta.requiredAction = action;
@@ -4345,6 +4563,9 @@ function defaultVaultMessage(input) {
4345
4563
  return input.message ?? `Public key not found for ${input.did}`;
4346
4564
  case "STORAGE_ERROR":
4347
4565
  return input.message ?? input.cause.message;
4566
+ case "STORAGE_QUOTA_EXCEEDED":
4567
+ case "STORAGE_LIMIT_REACHED":
4568
+ return input.message;
4348
4569
  }
4349
4570
  }
4350
4571
  function vaultError(input) {
@@ -4355,6 +4576,15 @@ function vaultError(input) {
4355
4576
  };
4356
4577
  return { ok: false, error };
4357
4578
  }
4579
+ function kvWriteError(context, error) {
4580
+ if (error.code === ErrorCodes.STORAGE_QUOTA_EXCEEDED || error.code === ErrorCodes.STORAGE_LIMIT_REACHED) {
4581
+ return vaultError({ code: error.code, message: error.message, meta: error.meta });
4582
+ }
4583
+ return vaultError({
4584
+ code: "STORAGE_ERROR",
4585
+ cause: new Error(`${context}: ${error.message}`)
4586
+ });
4587
+ }
4358
4588
  var DataVaultService = class extends BaseService {
4359
4589
  /**
4360
4590
  * Create a new DataVaultService instance.
@@ -4637,12 +4867,7 @@ var DataVaultService = class extends BaseService {
4637
4867
  JSON.stringify(envelopeResult.data)
4638
4868
  );
4639
4869
  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
- });
4870
+ return kvWriteError("Failed to store encrypted value", valuePutResult.error);
4646
4871
  }
4647
4872
  return { ok: true, data: void 0 };
4648
4873
  } catch (error) {
@@ -4860,12 +5085,7 @@ var DataVaultService = class extends BaseService {
4860
5085
  keyPayload
4861
5086
  );
4862
5087
  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
- });
5088
+ return kvWriteError("Failed to store key blob", keyPutResult.error);
4869
5089
  }
4870
5090
  const valuePayload = JSON.stringify({
4871
5091
  data: base64Encode(encrypted),
@@ -4876,12 +5096,7 @@ var DataVaultService = class extends BaseService {
4876
5096
  valuePayload
4877
5097
  );
4878
5098
  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
- });
5099
+ return kvWriteError("Failed to store encrypted value", valuePutResult.error);
4885
5100
  }
4886
5101
  return ok(void 0);
4887
5102
  } catch (error) {
@@ -5212,12 +5427,7 @@ var DataVaultService = class extends BaseService {
5212
5427
  grantPayload
5213
5428
  );
5214
5429
  if (!grantPutResult.ok) {
5215
- return vaultError({
5216
- code: "STORAGE_ERROR",
5217
- cause: new Error(
5218
- `Failed to store grant: ${grantPutResult.error.message}`
5219
- )
5220
- });
5430
+ return kvWriteError("Failed to store grant", grantPutResult.error);
5221
5431
  }
5222
5432
  return ok(void 0);
5223
5433
  } catch (error) {
@@ -5562,12 +5772,7 @@ var DataVaultService = class extends BaseService {
5562
5772
  keyPayload
5563
5773
  );
5564
5774
  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
- });
5775
+ return kvWriteError("Failed to store rotated key blob", keyPutResult.error);
5571
5776
  }
5572
5777
  const valuePayload = JSON.stringify({
5573
5778
  data: base64Encode(encrypted),
@@ -5578,12 +5783,7 @@ var DataVaultService = class extends BaseService {
5578
5783
  valuePayload
5579
5784
  );
5580
5785
  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
- });
5786
+ return kvWriteError("Failed to store re-encrypted value", valuePutResult.error);
5587
5787
  }
5588
5788
  for (const did of remainingGrantees) {
5589
5789
  const grantResult = await this.reencrypt(key, did);
@@ -5934,103 +6134,6 @@ function canonicalHashHex(sha256, value) {
5934
6134
  return hexEncode2(sha256(utf8Encode(canonical)));
5935
6135
  }
5936
6136
 
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
6137
  // src/encryption/types.ts
6035
6138
  var DEFAULT_ENCRYPTION_ALG = "x25519-aes256gcm/v1";
6036
6139
  var ENVELOPE_VERSION = 1;
@@ -6912,6 +7015,8 @@ export {
6912
7015
  SQLAction,
6913
7016
  SQLMigrations,
6914
7017
  SQLService,
7018
+ STORAGE_FULL_MESSAGE,
7019
+ STORAGE_WRITE_TOO_LARGE_MESSAGE,
6915
7020
  SecretsService,
6916
7021
  ServiceContext,
6917
7022
  ServiceErrorEventSchema,
@@ -6964,22 +7069,26 @@ export {
6964
7069
  hexEncode2 as hexEncode,
6965
7070
  installTinyCloudDebugGlobals,
6966
7071
  isNetworkId,
7072
+ isStorageFullError,
6967
7073
  networkDiscoveryKey,
6968
7074
  networkError,
6969
7075
  notFoundError,
6970
7076
  ok,
6971
7077
  openWrappedKey,
6972
7078
  parseAuthError,
7079
+ parseCapabilityResource,
6973
7080
  parseNetworkId,
6974
7081
  parsePermissionHint,
6975
7082
  parsePermissionHintFromErrorText,
6976
7083
  parseSecretCatalogKey,
7084
+ parseStorageQuotaBytes,
6977
7085
  permissionDeniedError,
6978
7086
  resolveSecretListPrefix,
6979
7087
  resolveSecretPath,
6980
7088
  serviceError,
6981
7089
  storageLimitReachedError,
6982
7090
  storageQuotaExceededError,
7091
+ storageRejectionError,
6983
7092
  timeoutError,
6984
7093
  tinyCloudDebugLogger,
6985
7094
  utf8Decode,
@@ -6993,6 +7102,7 @@ export {
6993
7102
  validateServiceResponseEvent,
6994
7103
  validateServiceSession,
6995
7104
  validateTelemetrySpanEvent,
7105
+ validatedCapabilityOf,
6996
7106
  verifyDecryptResponse,
6997
7107
  wrapError
6998
7108
  };