@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.cjs CHANGED
@@ -55,6 +55,8 @@ __export(src_exports, {
55
55
  SQLAction: () => SQLAction,
56
56
  SQLMigrations: () => SQLMigrations,
57
57
  SQLService: () => SQLService,
58
+ STORAGE_FULL_MESSAGE: () => STORAGE_FULL_MESSAGE,
59
+ STORAGE_WRITE_TOO_LARGE_MESSAGE: () => STORAGE_WRITE_TOO_LARGE_MESSAGE,
58
60
  SecretsService: () => SecretsService,
59
61
  ServiceContext: () => ServiceContext,
60
62
  ServiceErrorEventSchema: () => ServiceErrorEventSchema,
@@ -107,22 +109,26 @@ __export(src_exports, {
107
109
  hexEncode: () => hexEncode2,
108
110
  installTinyCloudDebugGlobals: () => installTinyCloudDebugGlobals,
109
111
  isNetworkId: () => isNetworkId,
112
+ isStorageFullError: () => isStorageFullError,
110
113
  networkDiscoveryKey: () => networkDiscoveryKey,
111
114
  networkError: () => networkError,
112
115
  notFoundError: () => notFoundError,
113
116
  ok: () => ok,
114
117
  openWrappedKey: () => openWrappedKey,
115
118
  parseAuthError: () => parseAuthError,
119
+ parseCapabilityResource: () => parseCapabilityResource,
116
120
  parseNetworkId: () => parseNetworkId,
117
121
  parsePermissionHint: () => parsePermissionHint,
118
122
  parsePermissionHintFromErrorText: () => parsePermissionHintFromErrorText,
119
123
  parseSecretCatalogKey: () => parseSecretCatalogKey,
124
+ parseStorageQuotaBytes: () => parseStorageQuotaBytes,
120
125
  permissionDeniedError: () => permissionDeniedError,
121
126
  resolveSecretListPrefix: () => resolveSecretListPrefix,
122
127
  resolveSecretPath: () => resolveSecretPath,
123
128
  serviceError: () => serviceError,
124
129
  storageLimitReachedError: () => storageLimitReachedError,
125
130
  storageQuotaExceededError: () => storageQuotaExceededError,
131
+ storageRejectionError: () => storageRejectionError,
126
132
  timeoutError: () => timeoutError,
127
133
  tinyCloudDebugLogger: () => tinyCloudDebugLogger,
128
134
  utf8Decode: () => utf8Decode,
@@ -136,6 +142,7 @@ __export(src_exports, {
136
142
  validateServiceResponseEvent: () => validateServiceResponseEvent,
137
143
  validateServiceSession: () => validateServiceSession,
138
144
  validateTelemetrySpanEvent: () => validateTelemetrySpanEvent,
145
+ validatedCapabilityOf: () => validatedCapabilityOf,
139
146
  verifyDecryptResponse: () => verifyDecryptResponse,
140
147
  wrapError: () => wrapError
141
148
  });
@@ -1039,6 +1046,106 @@ var ServiceContext = class {
1039
1046
  }
1040
1047
  };
1041
1048
 
1049
+ // src/errors.ts
1050
+ var import_bootstrap = require("@tinycloud/bootstrap");
1051
+
1052
+ // src/encryption/networkId.ts
1053
+ var URN_PREFIX = "urn:tinycloud:encryption:";
1054
+ var NETWORK_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
1055
+ var PKH_EIP155_DID_RE = /^did:pkh:eip155:(\d+):(0x[a-fA-F0-9]{40})$/;
1056
+ var NetworkIdError = class extends Error {
1057
+ constructor(message) {
1058
+ super(message);
1059
+ this.name = "NetworkIdError";
1060
+ }
1061
+ };
1062
+ function parseNetworkId(networkId) {
1063
+ if (typeof networkId !== "string" || networkId.length === 0) {
1064
+ throw new NetworkIdError("networkId must be a non-empty string");
1065
+ }
1066
+ if (!networkId.startsWith(URN_PREFIX)) {
1067
+ throw new NetworkIdError(
1068
+ `networkId must start with ${URN_PREFIX} (got ${JSON.stringify(networkId)})`
1069
+ );
1070
+ }
1071
+ const body = networkId.slice(URN_PREFIX.length);
1072
+ const lastColon = body.lastIndexOf(":");
1073
+ if (lastColon <= 0 || lastColon === body.length - 1) {
1074
+ throw new NetworkIdError(
1075
+ `networkId missing ownerDid or name segment (got ${JSON.stringify(networkId)})`
1076
+ );
1077
+ }
1078
+ const ownerDid = body.slice(0, lastColon);
1079
+ const name = body.slice(lastColon + 1);
1080
+ if (!ownerDid.startsWith("did:")) {
1081
+ throw new NetworkIdError(
1082
+ `networkId ownerDid must be a DID (got ${JSON.stringify(ownerDid)})`
1083
+ );
1084
+ }
1085
+ const didParts = ownerDid.split(":");
1086
+ if (didParts.length < 3 || didParts.some((p) => p.length === 0)) {
1087
+ throw new NetworkIdError(
1088
+ `networkId ownerDid is not a well-formed DID (got ${JSON.stringify(ownerDid)})`
1089
+ );
1090
+ }
1091
+ if (!NETWORK_NAME_RE.test(name)) {
1092
+ throw new NetworkIdError(
1093
+ `networkId name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
1094
+ );
1095
+ }
1096
+ return { networkId, ownerDid, name };
1097
+ }
1098
+ function buildNetworkId(ownerDid, name) {
1099
+ if (typeof ownerDid !== "string" || !ownerDid.startsWith("did:")) {
1100
+ throw new NetworkIdError("ownerDid must be a DID");
1101
+ }
1102
+ if (typeof name !== "string" || !NETWORK_NAME_RE.test(name)) {
1103
+ throw new NetworkIdError(
1104
+ `network name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
1105
+ );
1106
+ }
1107
+ const networkId = `${URN_PREFIX}${ownerDid}:${name}`;
1108
+ parseNetworkId(networkId);
1109
+ return networkId;
1110
+ }
1111
+ function isNetworkId(networkId) {
1112
+ if (typeof networkId !== "string") {
1113
+ return false;
1114
+ }
1115
+ try {
1116
+ parseNetworkId(networkId);
1117
+ return true;
1118
+ } catch {
1119
+ return false;
1120
+ }
1121
+ }
1122
+ function parsePkhOwnerDid(ownerDid) {
1123
+ const match = ownerDid.match(PKH_EIP155_DID_RE);
1124
+ if (!match) return null;
1125
+ return {
1126
+ chainId: match[1],
1127
+ address: match[2].toLowerCase()
1128
+ };
1129
+ }
1130
+ function ownerDidMatches(a, b) {
1131
+ const aPkh = parsePkhOwnerDid(a);
1132
+ const bPkh = parsePkhOwnerDid(b);
1133
+ if (aPkh && bPkh) {
1134
+ return aPkh.chainId === bPkh.chainId && aPkh.address === bPkh.address;
1135
+ }
1136
+ return a === b;
1137
+ }
1138
+ function networkDiscoveryKey(name) {
1139
+ if (!NETWORK_NAME_RE.test(name)) {
1140
+ throw new NetworkIdError(
1141
+ `network name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
1142
+ );
1143
+ }
1144
+ return `.well-known/encryption/network/${name}`;
1145
+ }
1146
+ var ENCRYPTION_NETWORK_URN_PREFIX = URN_PREFIX;
1147
+ var NETWORK_NAME_PATTERN = NETWORK_NAME_RE;
1148
+
1042
1149
  // src/errors.ts
1043
1150
  function parsePermissionHint(value) {
1044
1151
  if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
@@ -1136,6 +1243,70 @@ function parseAuthError(responseText) {
1136
1243
  function authUnauthorizedError(service, message, meta) {
1137
1244
  return serviceError(ErrorCodes.AUTH_UNAUTHORIZED, message, service, { meta });
1138
1245
  }
1246
+ var grantableActions = new Set(
1247
+ import_bootstrap.CAPABILITY_REGISTRY.map(({ urn }) => urn).filter((urn) => !urn.endsWith("/*"))
1248
+ );
1249
+ var capabilityServices = new Set(
1250
+ import_bootstrap.CAPABILITY_REGISTRY.map(({ service }) => service.slice("tinycloud.".length))
1251
+ );
1252
+ function parseCapabilityResource(resource, service) {
1253
+ if (resource.length === 0 || resource.length > 1024 || !capabilityServices.has(service) || /[\s\p{C}*#]/u.test(resource) || resource.includes("://")) return void 0;
1254
+ const segments = resource.split("/");
1255
+ if (!/^tinycloud:[A-Za-z0-9][A-Za-z0-9:._-]*$/.test(segments[0])) return void 0;
1256
+ const serviceIndex = segments.findIndex(
1257
+ (segment, index) => index > 0 && capabilityServices.has(segment)
1258
+ );
1259
+ if (serviceIndex < 1 || segments[serviceIndex] !== service) return void 0;
1260
+ if (segments.slice(1, serviceIndex).some(
1261
+ (segment) => segment.length === 0 || segment === "." || segment === ".."
1262
+ )) return void 0;
1263
+ const pathSegments = segments.slice(serviceIndex + 1);
1264
+ if (pathSegments.some((segment) => segment === "." || segment === "..")) return void 0;
1265
+ if (service !== "kv" && (pathSegments.length === 0 || pathSegments[pathSegments.length - 1] === "")) return void 0;
1266
+ return {
1267
+ space: segments.slice(0, serviceIndex).join("/"),
1268
+ path: pathSegments.join("/")
1269
+ };
1270
+ }
1271
+ function safeCapabilityResource(value, service, action) {
1272
+ if (typeof value !== "string" || value.length === 0 || value.length > 1024) return false;
1273
+ if (service === "encryption" && value.startsWith("urn:tinycloud:encryption:")) {
1274
+ if (!/^[A-Za-z0-9_][A-Za-z0-9_.:/-]*$/.test(value) || value.includes("//") || value.split("/").some((segment) => segment === "." || segment === "..")) return false;
1275
+ try {
1276
+ parseNetworkId(value);
1277
+ return true;
1278
+ } catch {
1279
+ return false;
1280
+ }
1281
+ }
1282
+ const parsed = parseCapabilityResource(value, service);
1283
+ return parsed !== void 0 && (!parsed.path.endsWith("/") || service === "kv" && action === "tinycloud.kv/list") && (parsed.path !== "" || service === "kv" && action === "tinycloud.kv/list");
1284
+ }
1285
+ function validatedCapabilityOf(error) {
1286
+ const verdict = authorizationVerdictOf(error);
1287
+ if (verdict !== "unauthenticated" && verdict !== "forbidden") return void 0;
1288
+ const seen = /* @__PURE__ */ new Set();
1289
+ let node = error;
1290
+ for (let depth = 0; depth < MAX_CAUSE_DEPTH && typeof node === "object" && node !== null && !seen.has(node); depth += 1) {
1291
+ seen.add(node);
1292
+ const record = node;
1293
+ const status = errorStatusOf(record);
1294
+ const meta = record.meta;
1295
+ if ((status === 401 || status === 403 || status === void 0 && record.code === ErrorCodes.AUTH_UNAUTHORIZED) && typeof meta === "object" && meta !== null && !Array.isArray(meta)) {
1296
+ const metadata = meta;
1297
+ const { resource, requiredAction } = metadata;
1298
+ if (typeof requiredAction === "string" && grantableActions.has(requiredAction)) {
1299
+ const actionService = requiredAction.slice("tinycloud.".length, requiredAction.indexOf("/"));
1300
+ const explicitService = record.service === void 0 ? metadata.service : record.service;
1301
+ if (safeCapabilityResource(resource, actionService, requiredAction) && (explicitService === void 0 || explicitService === actionService || explicitService === `tinycloud.${actionService}`)) {
1302
+ return { resource, requiredAction };
1303
+ }
1304
+ }
1305
+ }
1306
+ node = record.cause;
1307
+ }
1308
+ return void 0;
1309
+ }
1139
1310
  var MAX_CAUSE_DEPTH = 8;
1140
1311
  function errorStatusOf(node) {
1141
1312
  const meta = node.meta;
@@ -1183,6 +1354,23 @@ function storageLimitReachedError(service, message, meta) {
1183
1354
  meta
1184
1355
  };
1185
1356
  }
1357
+ 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.";
1358
+ 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.";
1359
+ function parseStorageQuotaBytes(responseText) {
1360
+ const match = responseText.match(/Used:\s*(\d+)\s*bytes,\s*Limit:\s*(\d+)\s*bytes/i);
1361
+ if (!match) return void 0;
1362
+ return {
1363
+ usedBytes: parseInt(match[1], 10),
1364
+ limitBytes: parseInt(match[2], 10)
1365
+ };
1366
+ }
1367
+ function storageRejectionError(service, status, meta, responseText) {
1368
+ const quotaMeta = { ...meta, ...parseStorageQuotaBytes(responseText) };
1369
+ return status === 402 ? storageQuotaExceededError(service, STORAGE_FULL_MESSAGE, quotaMeta) : storageLimitReachedError(service, STORAGE_WRITE_TOO_LARGE_MESSAGE, quotaMeta);
1370
+ }
1371
+ function isStorageFullError(error) {
1372
+ return error?.code === ErrorCodes.STORAGE_QUOTA_EXCEEDED || error?.code === ErrorCodes.STORAGE_LIMIT_REACHED;
1373
+ }
1186
1374
  function wrapError(service, error, defaultCode = ErrorCodes.NETWORK_ERROR) {
1187
1375
  if (error instanceof Error) {
1188
1376
  if (error.name === "AbortError") {
@@ -1576,47 +1764,21 @@ var KVService = class extends BaseService {
1576
1764
  get config() {
1577
1765
  return this._config;
1578
1766
  }
1579
- // Parses "Used: X bytes, Limit: Y bytes" from tinycloud-node error responses
1580
- parseQuotaInfo(errorText) {
1581
- const match = errorText.match(
1582
- /Used:\s*(\d+)\s*bytes,\s*Limit:\s*(\d+)\s*bytes/i
1583
- );
1584
- if (match) {
1585
- return {
1586
- usedBytes: parseInt(match[1], 10),
1587
- limitBytes: parseInt(match[2], 10)
1588
- };
1589
- }
1590
- return void 0;
1591
- }
1592
1767
  handleQuotaErrorResponse(response, errorText, key) {
1593
- if (response.status === 402) {
1594
- const quotaInfo = this.parseQuotaInfo(errorText);
1595
- return err(
1596
- storageQuotaExceededError(
1597
- "kv",
1598
- `Storage quota exceeded for key ${JSON.stringify(key)}: ${errorText}`,
1599
- {
1600
- status: response.status,
1601
- ...quotaInfo ? { usedBytes: quotaInfo.usedBytes, limitBytes: quotaInfo.limitBytes } : {}
1602
- }
1603
- )
1604
- );
1768
+ if (response.status !== 402 && response.status !== 413) {
1769
+ return void 0;
1605
1770
  }
1606
- if (response.status === 413) {
1607
- const quotaInfo = this.parseQuotaInfo(errorText);
1608
- return err(
1609
- storageLimitReachedError(
1610
- "kv",
1611
- `Storage limit reached for key ${JSON.stringify(key)}: ${errorText}`,
1612
- {
1613
- status: response.status,
1614
- ...quotaInfo ? { usedBytes: quotaInfo.usedBytes, limitBytes: quotaInfo.limitBytes } : {}
1615
- }
1616
- )
1617
- );
1771
+ if (response.status === 413 && !parseStorageQuotaBytes(errorText)) {
1772
+ return void 0;
1618
1773
  }
1619
- return void 0;
1774
+ return err(
1775
+ storageRejectionError(
1776
+ "kv",
1777
+ response.status,
1778
+ { status: response.status, statusText: response.statusText, key },
1779
+ errorText
1780
+ )
1781
+ );
1620
1782
  }
1621
1783
  /**
1622
1784
  * Classify a KV 404 by reading the response body once.
@@ -1892,12 +2054,14 @@ var KVService = class extends BaseService {
1892
2054
  signal: this.combineSignals(options?.signal)
1893
2055
  });
1894
2056
  if (!response.ok) {
1895
- const errorText = await response.text();
2057
+ const errorText = response.status === 401 || response.status === 403 ? await this.readAuthorizationText(response) : await response.text();
1896
2058
  if (response.status === 401 || response.status === 403) {
1897
2059
  return this.authorizationFailure(
1898
2060
  `Failed to batch read ${keys.length} key(s)`,
1899
2061
  response,
1900
- errorText
2062
+ errorText,
2063
+ paths,
2064
+ action
1901
2065
  );
1902
2066
  }
1903
2067
  if (response.status === 413) {
@@ -1987,7 +2151,7 @@ var KVService = class extends BaseService {
1987
2151
  return text;
1988
2152
  }
1989
2153
  }
1990
- async createSignedReadUrlError(response, key) {
2154
+ async createSignedReadUrlError(response, key, path) {
1991
2155
  let errorText = response.statusText;
1992
2156
  try {
1993
2157
  const text = await response.text();
@@ -2000,7 +2164,9 @@ var KVService = class extends BaseService {
2000
2164
  return this.authorizationFailure(
2001
2165
  `Failed to create signed read URL for key ${JSON.stringify(key)}`,
2002
2166
  response,
2003
- errorText
2167
+ errorText,
2168
+ [path],
2169
+ KVAction.GET
2004
2170
  );
2005
2171
  }
2006
2172
  const code = response.status === 400 ? ErrorCodes.INVALID_INPUT : ErrorCodes.NETWORK_ERROR;
@@ -2013,21 +2179,42 @@ var KVService = class extends BaseService {
2013
2179
  )
2014
2180
  );
2015
2181
  }
2182
+ /** A known 401/403 keeps its typed status even if reading its body fails. */
2183
+ async readAuthorizationText(response) {
2184
+ try {
2185
+ return await response.text();
2186
+ } catch {
2187
+ return "";
2188
+ }
2189
+ }
2016
2190
  /**
2017
2191
  * AUTH_UNAUTHORIZED for a 401/403 response. The message keeps the HTTP
2018
2192
  * status and the server text (`<context>: <status> - <text>`, the same
2019
2193
  * shape as every other KV failure) so callers that rethrow only the message
2020
2194
  * still see the status; `meta.status` carries it for typed callers.
2021
- * Resource/ability hints are parsed from the raw body, never the message.
2195
+ * Server hints must match the requested path/action (whether the node sends
2196
+ * a relative path or full resource); metadata uses only the request-derived
2197
+ * canonical resource under the authenticated session's space.
2022
2198
  */
2023
- authorizationFailure(context, response, errorText, extraMeta = {}) {
2199
+ authorizationFailure(context, response, errorText, paths, expectedAction, extraMeta = {}) {
2024
2200
  const { resource, action } = parseAuthError(errorText);
2201
+ const requestedPath = paths.find(
2202
+ (path) => resource === path || resource === `${this.context.session.spaceId}/kv/${path}`
2203
+ );
2204
+ const capability = requestedPath !== void 0 && action === expectedAction ? validatedCapabilityOf({
2205
+ service: "kv",
2206
+ code: ErrorCodes.AUTH_UNAUTHORIZED,
2207
+ meta: {
2208
+ status: response.status,
2209
+ resource: `${this.context.session.spaceId}/kv/${requestedPath}`,
2210
+ requiredAction: action
2211
+ }
2212
+ }) : void 0;
2025
2213
  const detail = errorText.trim().length > 0 ? errorText : response.statusText || "authorization failed";
2026
2214
  return err(authUnauthorizedError("kv", `${context}: ${response.status} - ${detail}`, {
2027
2215
  status: response.status,
2028
- ...action && { requiredAction: action },
2029
- ...resource && { resource },
2030
- ...extraMeta
2216
+ ...extraMeta,
2217
+ ...capability
2031
2218
  }));
2032
2219
  }
2033
2220
  normalizeSignedReadUrlResponse(data) {
@@ -2071,13 +2258,24 @@ var KVService = class extends BaseService {
2071
2258
  );
2072
2259
  if (!response.ok) {
2073
2260
  if (response.status === 401 || response.status === 403) {
2074
- const errorText2 = await response.text();
2261
+ const errorText2 = await this.readAuthorizationText(response);
2075
2262
  const permissionHint = parsePermissionHintFromErrorText(errorText2);
2263
+ const structuredCapability = permissionHint?.service === "tinycloud.kv" && permissionHint.space === this.context.session.spaceId && permissionHint.path === path ? validatedCapabilityOf({
2264
+ service: "kv",
2265
+ code: ErrorCodes.AUTH_UNAUTHORIZED,
2266
+ meta: {
2267
+ status: response.status,
2268
+ resource: `${this.context.session.spaceId}/kv/${path}`,
2269
+ requiredAction: KVAction.GET
2270
+ }
2271
+ }) : void 0;
2076
2272
  return this.authorizationFailure(
2077
2273
  `Failed to get key ${JSON.stringify(key)}`,
2078
2274
  response,
2079
2275
  errorText2,
2080
- permissionHint === void 0 ? {} : { permissionHint }
2276
+ [path],
2277
+ KVAction.GET,
2278
+ structuredCapability ? { permissionHint, ...structuredCapability } : {}
2081
2279
  );
2082
2280
  }
2083
2281
  if (response.status === 404) {
@@ -2154,7 +2352,9 @@ var KVService = class extends BaseService {
2154
2352
  return this.authorizationFailure(
2155
2353
  `Failed to put key ${JSON.stringify(key)}`,
2156
2354
  response,
2157
- await response.text()
2355
+ await this.readAuthorizationText(response),
2356
+ [path],
2357
+ KVAction.PUT
2158
2358
  );
2159
2359
  }
2160
2360
  if (!response.ok) {
@@ -2283,7 +2483,9 @@ var KVService = class extends BaseService {
2283
2483
  return this.authorizationFailure(
2284
2484
  `Failed to batch put ${items.length} key(s)`,
2285
2485
  response,
2286
- errorText
2486
+ errorText,
2487
+ paths,
2488
+ KVAction.PUT
2287
2489
  );
2288
2490
  }
2289
2491
  const quotaError = this.handleQuotaErrorResponse(
@@ -2404,11 +2606,13 @@ var KVService = class extends BaseService {
2404
2606
  }
2405
2607
  );
2406
2608
  if (!response.ok) {
2407
- if (response.status === 401) {
2609
+ if (response.status === 401 || response.status === 403) {
2408
2610
  return this.authorizationFailure(
2409
2611
  "Failed to list keys",
2410
2612
  response,
2411
- await response.text()
2613
+ await this.readAuthorizationText(response),
2614
+ [listPath],
2615
+ KVAction.LIST
2412
2616
  );
2413
2617
  }
2414
2618
  const errorText = await response.text();
@@ -2457,11 +2661,13 @@ var KVService = class extends BaseService {
2457
2661
  options?.ifMatch === void 0 ? void 0 : { "if-match": options.ifMatch }
2458
2662
  );
2459
2663
  if (!response.ok) {
2460
- if (response.status === 401) {
2664
+ if (response.status === 401 || response.status === 403) {
2461
2665
  return this.authorizationFailure(
2462
2666
  `Failed to delete key ${JSON.stringify(key)}`,
2463
2667
  response,
2464
- await response.text()
2668
+ await this.readAuthorizationText(response),
2669
+ [path],
2670
+ KVAction.DELETE
2465
2671
  );
2466
2672
  }
2467
2673
  if (response.status === 404) {
@@ -2519,11 +2725,13 @@ var KVService = class extends BaseService {
2519
2725
  options?.signal
2520
2726
  );
2521
2727
  if (!response.ok) {
2522
- if (response.status === 401) {
2728
+ if (response.status === 401 || response.status === 403) {
2523
2729
  return this.authorizationFailure(
2524
2730
  `Failed to get metadata for key ${JSON.stringify(key)}`,
2525
2731
  response,
2526
- await response.text()
2732
+ await this.readAuthorizationText(response),
2733
+ [path],
2734
+ KVAction.HEAD
2527
2735
  );
2528
2736
  }
2529
2737
  if (response.status === 404) {
@@ -2590,7 +2798,7 @@ var KVService = class extends BaseService {
2590
2798
  signal: this.combineSignals(options?.signal)
2591
2799
  });
2592
2800
  if (!response.ok) {
2593
- return this.createSignedReadUrlError(response, key);
2801
+ return this.createSignedReadUrlError(response, key, path);
2594
2802
  }
2595
2803
  const signedUrl = this.normalizeSignedReadUrlResponse(
2596
2804
  await response.json()
@@ -2733,16 +2941,16 @@ var DatabaseHandle = class {
2733
2941
  };
2734
2942
 
2735
2943
  // src/sql/types.ts
2736
- var import_bootstrap = require("@tinycloud/bootstrap");
2944
+ var import_bootstrap2 = require("@tinycloud/bootstrap");
2737
2945
  var SQLAction = {
2738
- READ: import_bootstrap.SQL.READ,
2739
- WRITE: import_bootstrap.SQL.WRITE,
2740
- SCHEMA: import_bootstrap.SQL.SCHEMA,
2741
- ADMIN: import_bootstrap.SQL.ADMIN,
2742
- SELECT: import_bootstrap.SQL.SELECT,
2743
- EXECUTE: import_bootstrap.SQL.EXECUTE,
2744
- EXPORT: import_bootstrap.SQL.EXPORT,
2745
- ALL: import_bootstrap.SQL.ALL
2946
+ READ: import_bootstrap2.SQL.READ,
2947
+ WRITE: import_bootstrap2.SQL.WRITE,
2948
+ SCHEMA: import_bootstrap2.SQL.SCHEMA,
2949
+ ADMIN: import_bootstrap2.SQL.ADMIN,
2950
+ SELECT: import_bootstrap2.SQL.SELECT,
2951
+ EXECUTE: import_bootstrap2.SQL.EXECUTE,
2952
+ EXPORT: import_bootstrap2.SQL.EXPORT,
2953
+ ALL: import_bootstrap2.SQL.ALL
2746
2954
  };
2747
2955
 
2748
2956
  // src/sql/SQLService.ts
@@ -2828,14 +3036,15 @@ var SQLService = class extends BaseService {
2828
3036
  };
2829
3037
  if (options?.maxRows !== void 0) body.maxRows = options.maxRows;
2830
3038
  if (options?.maxBytes !== void 0) body.maxBytes = options.maxBytes;
3039
+ const action = this.actionForSql(sql, SQLAction.READ);
2831
3040
  const response = await this.invokeSQL(
2832
3041
  dbName,
2833
- this.actionForSql(sql, SQLAction.READ),
3042
+ action,
2834
3043
  body,
2835
3044
  options?.signal
2836
3045
  );
2837
3046
  if (!response.ok) {
2838
- return this.handleErrorResponse(response, "query");
3047
+ return this.handleErrorResponse(response, "query", dbName, [action]);
2839
3048
  }
2840
3049
  const data = await response.json();
2841
3050
  return ok(data);
@@ -2864,14 +3073,15 @@ var SQLService = class extends BaseService {
2864
3073
  (statement) => this.actionForSql(statement, SQLAction.SCHEMA)
2865
3074
  )
2866
3075
  ];
3076
+ const requestedActions = this.dedupeActions(actions);
2867
3077
  const response = await this.invokeSQL(
2868
3078
  dbName,
2869
- this.dedupeActions(actions),
3079
+ requestedActions,
2870
3080
  body,
2871
3081
  options?.signal
2872
3082
  );
2873
3083
  if (!response.ok) {
2874
- return this.handleErrorResponse(response, "execute");
3084
+ return this.handleErrorResponse(response, "execute", dbName, requestedActions);
2875
3085
  }
2876
3086
  const data = await response.json();
2877
3087
  return ok(data);
@@ -2886,14 +3096,15 @@ var SQLService = class extends BaseService {
2886
3096
  return err(authRequiredError("sql"));
2887
3097
  }
2888
3098
  try {
3099
+ const requestedActions = this.actionsForSqlBatch(statements);
2889
3100
  const response = await this.invokeSQL(
2890
3101
  dbName,
2891
- this.actionsForSqlBatch(statements),
3102
+ requestedActions,
2892
3103
  { action: "batch", statements },
2893
3104
  options?.signal
2894
3105
  );
2895
3106
  if (!response.ok) {
2896
- return this.handleErrorResponse(response, "batch");
3107
+ return this.handleErrorResponse(response, "batch", dbName, requestedActions);
2897
3108
  }
2898
3109
  const data = await response.json();
2899
3110
  return ok(data);
@@ -2915,7 +3126,7 @@ var SQLService = class extends BaseService {
2915
3126
  options?.signal
2916
3127
  );
2917
3128
  if (!response.ok) {
2918
- return this.handleErrorResponse(response, "executeStatement");
3129
+ return this.handleErrorResponse(response, "executeStatement", dbName, [SQLAction.WRITE]);
2919
3130
  }
2920
3131
  const data = await response.json();
2921
3132
  return ok(data);
@@ -2937,7 +3148,7 @@ var SQLService = class extends BaseService {
2937
3148
  options?.signal
2938
3149
  );
2939
3150
  if (!response.ok) {
2940
- return this.handleErrorResponse(response, "export");
3151
+ return this.handleErrorResponse(response, "export", dbName, [SQLAction.READ]);
2941
3152
  }
2942
3153
  const resp = response;
2943
3154
  if (typeof resp.blob === "function") {
@@ -2953,21 +3164,24 @@ var SQLService = class extends BaseService {
2953
3164
  }
2954
3165
  async applyMigrationsOnDbUnlocked(dbName, options) {
2955
3166
  return this.withTelemetry("migrations.apply", dbName, async () => {
2956
- const created = await this.ensureMigrationsTable(dbName, options.signal);
2957
- if (!created.ok) return created;
2958
3167
  const listed = await this.queryOnDb(
2959
3168
  dbName,
2960
3169
  `SELECT id FROM ${MIGRATIONS_TABLE} WHERE namespace = ? ORDER BY applied_at, id`,
2961
3170
  [options.namespace],
2962
3171
  { signal: options.signal }
2963
3172
  );
2964
- if (!listed.ok) return listed;
3173
+ const tableMissing = !listed.ok && (listed.error.code === ErrorCodes.SQL_DATABASE_NOT_FOUND || isMissingTableError(listed.error.message));
3174
+ if (!listed.ok && !tableMissing) return listed;
2965
3175
  const appliedIds = new Set(
2966
- listed.data.rows.map((row) => rowValue(row, 0)).filter((id) => typeof id === "string")
3176
+ (listed.ok ? listed.data.rows : []).map((row) => rowValue(row, 0)).filter((id) => typeof id === "string")
2967
3177
  );
2968
3178
  const skipped = options.migrations.filter((migration) => appliedIds.has(migration.id)).map((migration) => migration.id);
2969
3179
  const pending = options.migrations.filter((migration) => !appliedIds.has(migration.id));
2970
3180
  const applied = [];
3181
+ if (pending.length > 0 && tableMissing) {
3182
+ const created = await this.ensureMigrationsTable(dbName, options.signal);
3183
+ if (!created.ok) return created;
3184
+ }
2971
3185
  for (const migration of pending) {
2972
3186
  const result = await this.applyOneMigration(dbName, options.namespace, migration, options.signal);
2973
3187
  if (!result.ok) return result;
@@ -3068,8 +3282,12 @@ var SQLService = class extends BaseService {
3068
3282
  }))
3069
3283
  );
3070
3284
  }
3071
- async handleErrorResponse(response, operation) {
3285
+ async handleErrorResponse(response, operation, dbName, requestedActions) {
3072
3286
  const errorText = await response.text();
3287
+ const meta = responseErrorMeta(response.status, response.statusText, errorText);
3288
+ if (response.status === 402) {
3289
+ return err(storageRejectionError("sql", 402, meta, errorText));
3290
+ }
3073
3291
  const errorBody = parseServiceErrorBody(errorText);
3074
3292
  const errorCode = this.mapHttpStatusToErrorCode(
3075
3293
  response.status,
@@ -3082,11 +3300,12 @@ var SQLService = class extends BaseService {
3082
3300
  errorText,
3083
3301
  errorBody
3084
3302
  );
3085
- const meta = responseErrorMeta(response.status, response.statusText, errorText);
3086
3303
  if (response.status === 401) {
3087
- const { resource, action } = parseAuthError(errorText);
3088
- if (action) meta.requiredAction = action;
3089
- if (resource) meta.resource = resource;
3304
+ const { action } = parseAuthError(errorText);
3305
+ if (action && requestedActions.includes(action)) {
3306
+ meta.requiredAction = action;
3307
+ meta.resource = `${this.context.session.spaceId}/sql/${dbName}`;
3308
+ }
3090
3309
  }
3091
3310
  return err(
3092
3311
  serviceError(errorCode, message, "sql", { meta })
@@ -3183,6 +3402,9 @@ function serializeSqlValues(values) {
3183
3402
  (value) => value instanceof Uint8Array ? Array.from(value) : value
3184
3403
  );
3185
3404
  }
3405
+ function isMissingTableError(message) {
3406
+ return /no such table/i.test(message);
3407
+ }
3186
3408
  function migrationKey(namespace, id) {
3187
3409
  return `${encodeURIComponent(namespace)}:${encodeURIComponent(id)}`;
3188
3410
  }
@@ -3228,16 +3450,16 @@ var DuckDbDatabaseHandle = class {
3228
3450
  };
3229
3451
 
3230
3452
  // src/duckdb/types.ts
3231
- var import_bootstrap2 = require("@tinycloud/bootstrap");
3453
+ var import_bootstrap3 = require("@tinycloud/bootstrap");
3232
3454
  var DuckDbAction = {
3233
- READ: import_bootstrap2.DUCKDB.READ,
3234
- WRITE: import_bootstrap2.DUCKDB.WRITE,
3235
- ADMIN: import_bootstrap2.DUCKDB.ADMIN,
3236
- DESCRIBE: import_bootstrap2.DUCKDB.DESCRIBE,
3237
- EXPORT: import_bootstrap2.DUCKDB.EXPORT,
3238
- IMPORT: import_bootstrap2.DUCKDB.IMPORT,
3239
- EXECUTE: import_bootstrap2.DUCKDB.EXECUTE,
3240
- ALL: import_bootstrap2.DUCKDB.ALL
3455
+ READ: import_bootstrap3.DUCKDB.READ,
3456
+ WRITE: import_bootstrap3.DUCKDB.WRITE,
3457
+ ADMIN: import_bootstrap3.DUCKDB.ADMIN,
3458
+ DESCRIBE: import_bootstrap3.DUCKDB.DESCRIBE,
3459
+ EXPORT: import_bootstrap3.DUCKDB.EXPORT,
3460
+ IMPORT: import_bootstrap3.DUCKDB.IMPORT,
3461
+ EXECUTE: import_bootstrap3.DUCKDB.EXECUTE,
3462
+ ALL: import_bootstrap3.DUCKDB.ALL
3241
3463
  };
3242
3464
 
3243
3465
  // src/duckdb/DuckDbService.ts
@@ -3504,6 +3726,10 @@ var DuckDbService = class extends BaseService {
3504
3726
  }
3505
3727
  async handleErrorResponse(response, operation) {
3506
3728
  const errorText = await response.text();
3729
+ const meta = responseErrorMeta(response.status, response.statusText, errorText);
3730
+ if (response.status === 402) {
3731
+ return err(storageRejectionError("duckdb", 402, meta, errorText));
3732
+ }
3507
3733
  const errorBody = parseServiceErrorBody(errorText);
3508
3734
  const errorCode = this.mapHttpStatusToErrorCode(
3509
3735
  response.status,
@@ -3516,7 +3742,6 @@ var DuckDbService = class extends BaseService {
3516
3742
  errorText,
3517
3743
  errorBody
3518
3744
  );
3519
- const meta = responseErrorMeta(response.status, response.statusText, errorText);
3520
3745
  if (response.status === 401) {
3521
3746
  const { resource, action } = parseAuthError(errorText);
3522
3747
  if (action) meta.requiredAction = action;
@@ -4488,6 +4713,9 @@ function defaultVaultMessage(input) {
4488
4713
  return input.message ?? `Public key not found for ${input.did}`;
4489
4714
  case "STORAGE_ERROR":
4490
4715
  return input.message ?? input.cause.message;
4716
+ case "STORAGE_QUOTA_EXCEEDED":
4717
+ case "STORAGE_LIMIT_REACHED":
4718
+ return input.message;
4491
4719
  }
4492
4720
  }
4493
4721
  function vaultError(input) {
@@ -4498,6 +4726,15 @@ function vaultError(input) {
4498
4726
  };
4499
4727
  return { ok: false, error };
4500
4728
  }
4729
+ function kvWriteError(context, error) {
4730
+ if (error.code === ErrorCodes.STORAGE_QUOTA_EXCEEDED || error.code === ErrorCodes.STORAGE_LIMIT_REACHED) {
4731
+ return vaultError({ code: error.code, message: error.message, meta: error.meta });
4732
+ }
4733
+ return vaultError({
4734
+ code: "STORAGE_ERROR",
4735
+ cause: new Error(`${context}: ${error.message}`)
4736
+ });
4737
+ }
4501
4738
  var DataVaultService = class extends BaseService {
4502
4739
  /**
4503
4740
  * Create a new DataVaultService instance.
@@ -4780,12 +5017,7 @@ var DataVaultService = class extends BaseService {
4780
5017
  JSON.stringify(envelopeResult.data)
4781
5018
  );
4782
5019
  if (!valuePutResult.ok) {
4783
- return vaultError({
4784
- code: "STORAGE_ERROR",
4785
- cause: new Error(
4786
- `Failed to store encrypted value: ${valuePutResult.error.message}`
4787
- )
4788
- });
5020
+ return kvWriteError("Failed to store encrypted value", valuePutResult.error);
4789
5021
  }
4790
5022
  return { ok: true, data: void 0 };
4791
5023
  } catch (error) {
@@ -5003,12 +5235,7 @@ var DataVaultService = class extends BaseService {
5003
5235
  keyPayload
5004
5236
  );
5005
5237
  if (!keyPutResult.ok) {
5006
- return vaultError({
5007
- code: "STORAGE_ERROR",
5008
- cause: new Error(
5009
- `Failed to store key blob: ${keyPutResult.error.message}`
5010
- )
5011
- });
5238
+ return kvWriteError("Failed to store key blob", keyPutResult.error);
5012
5239
  }
5013
5240
  const valuePayload = JSON.stringify({
5014
5241
  data: base64Encode(encrypted),
@@ -5019,12 +5246,7 @@ var DataVaultService = class extends BaseService {
5019
5246
  valuePayload
5020
5247
  );
5021
5248
  if (!valuePutResult.ok) {
5022
- return vaultError({
5023
- code: "STORAGE_ERROR",
5024
- cause: new Error(
5025
- `Failed to store encrypted value: ${valuePutResult.error.message}`
5026
- )
5027
- });
5249
+ return kvWriteError("Failed to store encrypted value", valuePutResult.error);
5028
5250
  }
5029
5251
  return ok(void 0);
5030
5252
  } catch (error) {
@@ -5355,12 +5577,7 @@ var DataVaultService = class extends BaseService {
5355
5577
  grantPayload
5356
5578
  );
5357
5579
  if (!grantPutResult.ok) {
5358
- return vaultError({
5359
- code: "STORAGE_ERROR",
5360
- cause: new Error(
5361
- `Failed to store grant: ${grantPutResult.error.message}`
5362
- )
5363
- });
5580
+ return kvWriteError("Failed to store grant", grantPutResult.error);
5364
5581
  }
5365
5582
  return ok(void 0);
5366
5583
  } catch (error) {
@@ -5705,12 +5922,7 @@ var DataVaultService = class extends BaseService {
5705
5922
  keyPayload
5706
5923
  );
5707
5924
  if (!keyPutResult.ok) {
5708
- return vaultError({
5709
- code: "STORAGE_ERROR",
5710
- cause: new Error(
5711
- `Failed to store rotated key blob: ${keyPutResult.error.message}`
5712
- )
5713
- });
5925
+ return kvWriteError("Failed to store rotated key blob", keyPutResult.error);
5714
5926
  }
5715
5927
  const valuePayload = JSON.stringify({
5716
5928
  data: base64Encode(encrypted),
@@ -5721,12 +5933,7 @@ var DataVaultService = class extends BaseService {
5721
5933
  valuePayload
5722
5934
  );
5723
5935
  if (!valuePutResult.ok) {
5724
- return vaultError({
5725
- code: "STORAGE_ERROR",
5726
- cause: new Error(
5727
- `Failed to store re-encrypted value: ${valuePutResult.error.message}`
5728
- )
5729
- });
5936
+ return kvWriteError("Failed to store re-encrypted value", valuePutResult.error);
5730
5937
  }
5731
5938
  for (const did of remainingGrantees) {
5732
5939
  const grantResult = await this.reencrypt(key, did);
@@ -6077,103 +6284,6 @@ function canonicalHashHex(sha256, value) {
6077
6284
  return hexEncode2(sha256(utf8Encode(canonical)));
6078
6285
  }
6079
6286
 
6080
- // src/encryption/networkId.ts
6081
- var URN_PREFIX = "urn:tinycloud:encryption:";
6082
- var NETWORK_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
6083
- var PKH_EIP155_DID_RE = /^did:pkh:eip155:(\d+):(0x[a-fA-F0-9]{40})$/;
6084
- var NetworkIdError = class extends Error {
6085
- constructor(message) {
6086
- super(message);
6087
- this.name = "NetworkIdError";
6088
- }
6089
- };
6090
- function parseNetworkId(networkId) {
6091
- if (typeof networkId !== "string" || networkId.length === 0) {
6092
- throw new NetworkIdError("networkId must be a non-empty string");
6093
- }
6094
- if (!networkId.startsWith(URN_PREFIX)) {
6095
- throw new NetworkIdError(
6096
- `networkId must start with ${URN_PREFIX} (got ${JSON.stringify(networkId)})`
6097
- );
6098
- }
6099
- const body = networkId.slice(URN_PREFIX.length);
6100
- const lastColon = body.lastIndexOf(":");
6101
- if (lastColon <= 0 || lastColon === body.length - 1) {
6102
- throw new NetworkIdError(
6103
- `networkId missing ownerDid or name segment (got ${JSON.stringify(networkId)})`
6104
- );
6105
- }
6106
- const ownerDid = body.slice(0, lastColon);
6107
- const name = body.slice(lastColon + 1);
6108
- if (!ownerDid.startsWith("did:")) {
6109
- throw new NetworkIdError(
6110
- `networkId ownerDid must be a DID (got ${JSON.stringify(ownerDid)})`
6111
- );
6112
- }
6113
- const didParts = ownerDid.split(":");
6114
- if (didParts.length < 3 || didParts.some((p) => p.length === 0)) {
6115
- throw new NetworkIdError(
6116
- `networkId ownerDid is not a well-formed DID (got ${JSON.stringify(ownerDid)})`
6117
- );
6118
- }
6119
- if (!NETWORK_NAME_RE.test(name)) {
6120
- throw new NetworkIdError(
6121
- `networkId name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
6122
- );
6123
- }
6124
- return { networkId, ownerDid, name };
6125
- }
6126
- function buildNetworkId(ownerDid, name) {
6127
- if (typeof ownerDid !== "string" || !ownerDid.startsWith("did:")) {
6128
- throw new NetworkIdError("ownerDid must be a DID");
6129
- }
6130
- if (typeof name !== "string" || !NETWORK_NAME_RE.test(name)) {
6131
- throw new NetworkIdError(
6132
- `network name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
6133
- );
6134
- }
6135
- const networkId = `${URN_PREFIX}${ownerDid}:${name}`;
6136
- parseNetworkId(networkId);
6137
- return networkId;
6138
- }
6139
- function isNetworkId(networkId) {
6140
- if (typeof networkId !== "string") {
6141
- return false;
6142
- }
6143
- try {
6144
- parseNetworkId(networkId);
6145
- return true;
6146
- } catch {
6147
- return false;
6148
- }
6149
- }
6150
- function parsePkhOwnerDid(ownerDid) {
6151
- const match = ownerDid.match(PKH_EIP155_DID_RE);
6152
- if (!match) return null;
6153
- return {
6154
- chainId: match[1],
6155
- address: match[2].toLowerCase()
6156
- };
6157
- }
6158
- function ownerDidMatches(a, b) {
6159
- const aPkh = parsePkhOwnerDid(a);
6160
- const bPkh = parsePkhOwnerDid(b);
6161
- if (aPkh && bPkh) {
6162
- return aPkh.chainId === bPkh.chainId && aPkh.address === bPkh.address;
6163
- }
6164
- return a === b;
6165
- }
6166
- function networkDiscoveryKey(name) {
6167
- if (!NETWORK_NAME_RE.test(name)) {
6168
- throw new NetworkIdError(
6169
- `network name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
6170
- );
6171
- }
6172
- return `.well-known/encryption/network/${name}`;
6173
- }
6174
- var ENCRYPTION_NETWORK_URN_PREFIX = URN_PREFIX;
6175
- var NETWORK_NAME_PATTERN = NETWORK_NAME_RE;
6176
-
6177
6287
  // src/encryption/types.ts
6178
6288
  var DEFAULT_ENCRYPTION_ALG = "x25519-aes256gcm/v1";
6179
6289
  var ENVELOPE_VERSION = 1;
@@ -7056,6 +7166,8 @@ var import_decrypt_transport_response_error3 = require("@tinycloud/sdk-services/
7056
7166
  SQLAction,
7057
7167
  SQLMigrations,
7058
7168
  SQLService,
7169
+ STORAGE_FULL_MESSAGE,
7170
+ STORAGE_WRITE_TOO_LARGE_MESSAGE,
7059
7171
  SecretsService,
7060
7172
  ServiceContext,
7061
7173
  ServiceErrorEventSchema,
@@ -7108,22 +7220,26 @@ var import_decrypt_transport_response_error3 = require("@tinycloud/sdk-services/
7108
7220
  hexEncode,
7109
7221
  installTinyCloudDebugGlobals,
7110
7222
  isNetworkId,
7223
+ isStorageFullError,
7111
7224
  networkDiscoveryKey,
7112
7225
  networkError,
7113
7226
  notFoundError,
7114
7227
  ok,
7115
7228
  openWrappedKey,
7116
7229
  parseAuthError,
7230
+ parseCapabilityResource,
7117
7231
  parseNetworkId,
7118
7232
  parsePermissionHint,
7119
7233
  parsePermissionHintFromErrorText,
7120
7234
  parseSecretCatalogKey,
7235
+ parseStorageQuotaBytes,
7121
7236
  permissionDeniedError,
7122
7237
  resolveSecretListPrefix,
7123
7238
  resolveSecretPath,
7124
7239
  serviceError,
7125
7240
  storageLimitReachedError,
7126
7241
  storageQuotaExceededError,
7242
+ storageRejectionError,
7127
7243
  timeoutError,
7128
7244
  tinyCloudDebugLogger,
7129
7245
  utf8Decode,
@@ -7137,6 +7253,7 @@ var import_decrypt_transport_response_error3 = require("@tinycloud/sdk-services/
7137
7253
  validateServiceResponseEvent,
7138
7254
  validateServiceSession,
7139
7255
  validateTelemetrySpanEvent,
7256
+ validatedCapabilityOf,
7140
7257
  verifyDecryptResponse,
7141
7258
  wrapError
7142
7259
  });