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