@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.js
CHANGED
|
@@ -5,6 +5,10 @@ var ErrorCodes = {
|
|
|
5
5
|
AUTH_EXPIRED: "AUTH_EXPIRED",
|
|
6
6
|
AUTH_REQUIRED: "AUTH_REQUIRED",
|
|
7
7
|
AUTH_UNAUTHORIZED: "AUTH_UNAUTHORIZED",
|
|
8
|
+
/** HTTP 401: the delegation the invocation cites was revoked. */
|
|
9
|
+
AUTH_DELEGATION_REVOKED: "AUTH_DELEGATION_REVOKED",
|
|
10
|
+
/** HTTP 401: a delegation the cited one rests on was revoked. */
|
|
11
|
+
AUTH_DELEGATION_ANCESTOR_REVOKED: "AUTH_DELEGATION_ANCESTOR_REVOKED",
|
|
8
12
|
NETWORK_ERROR: "NETWORK_ERROR",
|
|
9
13
|
TIMEOUT: "TIMEOUT",
|
|
10
14
|
ABORTED: "ABORTED",
|
|
@@ -16,6 +20,10 @@ var ErrorCodes = {
|
|
|
16
20
|
KV_PRECONDITION_FAILED: "KV_PRECONDITION_FAILED",
|
|
17
21
|
KV_CONFLICT: "KV_CONFLICT",
|
|
18
22
|
KV_RESPONSE_TOO_LARGE: "KV_RESPONSE_TOO_LARGE",
|
|
23
|
+
/** HTTP 410 from the change feed: discard sync state and restart without a cursor; `meta.reason` says why. */
|
|
24
|
+
KV_SYNC_RESET_REQUIRED: "KV_SYNC_RESET_REQUIRED",
|
|
25
|
+
/** HTTP 403 from the change feed: the retention grant was refused; `meta.reason` says why. */
|
|
26
|
+
KV_RETENTION_GRANT_REFUSED: "KV_RETENTION_GRANT_REFUSED",
|
|
19
27
|
// SQL-specific errors
|
|
20
28
|
SQL_ERROR: "SQL_ERROR",
|
|
21
29
|
SQL_PERMISSION_DENIED: "SQL_PERMISSION_DENIED",
|
|
@@ -896,6 +904,106 @@ var ServiceContext = class {
|
|
|
896
904
|
}
|
|
897
905
|
};
|
|
898
906
|
|
|
907
|
+
// src/errors.ts
|
|
908
|
+
import { CAPABILITY_REGISTRY } from "@tinycloud/bootstrap";
|
|
909
|
+
|
|
910
|
+
// src/encryption/networkId.ts
|
|
911
|
+
var URN_PREFIX = "urn:tinycloud:encryption:";
|
|
912
|
+
var NETWORK_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
913
|
+
var PKH_EIP155_DID_RE = /^did:pkh:eip155:(\d+):(0x[a-fA-F0-9]{40})$/;
|
|
914
|
+
var NetworkIdError = class extends Error {
|
|
915
|
+
constructor(message) {
|
|
916
|
+
super(message);
|
|
917
|
+
this.name = "NetworkIdError";
|
|
918
|
+
}
|
|
919
|
+
};
|
|
920
|
+
function parseNetworkId(networkId) {
|
|
921
|
+
if (typeof networkId !== "string" || networkId.length === 0) {
|
|
922
|
+
throw new NetworkIdError("networkId must be a non-empty string");
|
|
923
|
+
}
|
|
924
|
+
if (!networkId.startsWith(URN_PREFIX)) {
|
|
925
|
+
throw new NetworkIdError(
|
|
926
|
+
`networkId must start with ${URN_PREFIX} (got ${JSON.stringify(networkId)})`
|
|
927
|
+
);
|
|
928
|
+
}
|
|
929
|
+
const body = networkId.slice(URN_PREFIX.length);
|
|
930
|
+
const lastColon = body.lastIndexOf(":");
|
|
931
|
+
if (lastColon <= 0 || lastColon === body.length - 1) {
|
|
932
|
+
throw new NetworkIdError(
|
|
933
|
+
`networkId missing ownerDid or name segment (got ${JSON.stringify(networkId)})`
|
|
934
|
+
);
|
|
935
|
+
}
|
|
936
|
+
const ownerDid = body.slice(0, lastColon);
|
|
937
|
+
const name = body.slice(lastColon + 1);
|
|
938
|
+
if (!ownerDid.startsWith("did:")) {
|
|
939
|
+
throw new NetworkIdError(
|
|
940
|
+
`networkId ownerDid must be a DID (got ${JSON.stringify(ownerDid)})`
|
|
941
|
+
);
|
|
942
|
+
}
|
|
943
|
+
const didParts = ownerDid.split(":");
|
|
944
|
+
if (didParts.length < 3 || didParts.some((p) => p.length === 0)) {
|
|
945
|
+
throw new NetworkIdError(
|
|
946
|
+
`networkId ownerDid is not a well-formed DID (got ${JSON.stringify(ownerDid)})`
|
|
947
|
+
);
|
|
948
|
+
}
|
|
949
|
+
if (!NETWORK_NAME_RE.test(name)) {
|
|
950
|
+
throw new NetworkIdError(
|
|
951
|
+
`networkId name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
|
|
952
|
+
);
|
|
953
|
+
}
|
|
954
|
+
return { networkId, ownerDid, name };
|
|
955
|
+
}
|
|
956
|
+
function buildNetworkId(ownerDid, name) {
|
|
957
|
+
if (typeof ownerDid !== "string" || !ownerDid.startsWith("did:")) {
|
|
958
|
+
throw new NetworkIdError("ownerDid must be a DID");
|
|
959
|
+
}
|
|
960
|
+
if (typeof name !== "string" || !NETWORK_NAME_RE.test(name)) {
|
|
961
|
+
throw new NetworkIdError(
|
|
962
|
+
`network name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
|
|
963
|
+
);
|
|
964
|
+
}
|
|
965
|
+
const networkId = `${URN_PREFIX}${ownerDid}:${name}`;
|
|
966
|
+
parseNetworkId(networkId);
|
|
967
|
+
return networkId;
|
|
968
|
+
}
|
|
969
|
+
function isNetworkId(networkId) {
|
|
970
|
+
if (typeof networkId !== "string") {
|
|
971
|
+
return false;
|
|
972
|
+
}
|
|
973
|
+
try {
|
|
974
|
+
parseNetworkId(networkId);
|
|
975
|
+
return true;
|
|
976
|
+
} catch {
|
|
977
|
+
return false;
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
function parsePkhOwnerDid(ownerDid) {
|
|
981
|
+
const match = ownerDid.match(PKH_EIP155_DID_RE);
|
|
982
|
+
if (!match) return null;
|
|
983
|
+
return {
|
|
984
|
+
chainId: match[1],
|
|
985
|
+
address: match[2].toLowerCase()
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
function ownerDidMatches(a, b) {
|
|
989
|
+
const aPkh = parsePkhOwnerDid(a);
|
|
990
|
+
const bPkh = parsePkhOwnerDid(b);
|
|
991
|
+
if (aPkh && bPkh) {
|
|
992
|
+
return aPkh.chainId === bPkh.chainId && aPkh.address === bPkh.address;
|
|
993
|
+
}
|
|
994
|
+
return a === b;
|
|
995
|
+
}
|
|
996
|
+
function networkDiscoveryKey(name) {
|
|
997
|
+
if (!NETWORK_NAME_RE.test(name)) {
|
|
998
|
+
throw new NetworkIdError(
|
|
999
|
+
`network name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
|
|
1000
|
+
);
|
|
1001
|
+
}
|
|
1002
|
+
return `.well-known/encryption/network/${name}`;
|
|
1003
|
+
}
|
|
1004
|
+
var ENCRYPTION_NETWORK_URN_PREFIX = URN_PREFIX;
|
|
1005
|
+
var NETWORK_NAME_PATTERN = NETWORK_NAME_RE;
|
|
1006
|
+
|
|
899
1007
|
// src/errors.ts
|
|
900
1008
|
function parsePermissionHint(value) {
|
|
901
1009
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
@@ -955,11 +1063,31 @@ function networkError(service, message, cause) {
|
|
|
955
1063
|
cause
|
|
956
1064
|
};
|
|
957
1065
|
}
|
|
958
|
-
|
|
1066
|
+
var RequestTimeoutError = class extends Error {
|
|
1067
|
+
constructor(timeoutMs) {
|
|
1068
|
+
super(`Request timed out after ${timeoutMs}ms.`);
|
|
1069
|
+
this.name = "TimeoutError";
|
|
1070
|
+
this.timeoutMs = timeoutMs;
|
|
1071
|
+
}
|
|
1072
|
+
};
|
|
1073
|
+
function isRequestTimeoutError(error) {
|
|
1074
|
+
return error instanceof RequestTimeoutError || error instanceof Error && error.name === "TimeoutError" && typeof error.timeoutMs === "number";
|
|
1075
|
+
}
|
|
1076
|
+
function timeoutError(service, timeoutMs, cause) {
|
|
1077
|
+
if (timeoutMs === void 0) {
|
|
1078
|
+
return {
|
|
1079
|
+
code: ErrorCodes.TIMEOUT,
|
|
1080
|
+
message: "Request timed out.",
|
|
1081
|
+
service,
|
|
1082
|
+
...cause === void 0 ? {} : { cause }
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
959
1085
|
return {
|
|
960
1086
|
code: ErrorCodes.TIMEOUT,
|
|
961
|
-
message:
|
|
962
|
-
service
|
|
1087
|
+
message: `Request timed out after ${timeoutMs}ms.`,
|
|
1088
|
+
service,
|
|
1089
|
+
...cause === void 0 ? {} : { cause },
|
|
1090
|
+
meta: { timeoutMs }
|
|
963
1091
|
};
|
|
964
1092
|
}
|
|
965
1093
|
function abortedError(service) {
|
|
@@ -993,6 +1121,101 @@ function parseAuthError(responseText) {
|
|
|
993
1121
|
function authUnauthorizedError(service, message, meta) {
|
|
994
1122
|
return serviceError(ErrorCodes.AUTH_UNAUTHORIZED, message, service, { meta });
|
|
995
1123
|
}
|
|
1124
|
+
var grantableActions = new Set(
|
|
1125
|
+
CAPABILITY_REGISTRY.map(({ urn }) => urn).filter((urn) => !urn.endsWith("/*"))
|
|
1126
|
+
);
|
|
1127
|
+
var capabilityServices = new Set(
|
|
1128
|
+
CAPABILITY_REGISTRY.map(({ service }) => service.slice("tinycloud.".length))
|
|
1129
|
+
);
|
|
1130
|
+
function parseCapabilityResource(resource, service) {
|
|
1131
|
+
if (resource.length === 0 || resource.length > 1024 || !capabilityServices.has(service) || /[\s\p{C}*#]/u.test(resource) || resource.includes("://")) return void 0;
|
|
1132
|
+
const segments = resource.split("/");
|
|
1133
|
+
if (!/^tinycloud:[A-Za-z0-9][A-Za-z0-9:._-]*$/.test(segments[0])) return void 0;
|
|
1134
|
+
const serviceIndex = segments.findIndex(
|
|
1135
|
+
(segment, index) => index > 0 && capabilityServices.has(segment)
|
|
1136
|
+
);
|
|
1137
|
+
if (serviceIndex < 1 || segments[serviceIndex] !== service) return void 0;
|
|
1138
|
+
if (segments.slice(1, serviceIndex).some(
|
|
1139
|
+
(segment) => segment.length === 0 || segment === "." || segment === ".."
|
|
1140
|
+
)) return void 0;
|
|
1141
|
+
const pathSegments = segments.slice(serviceIndex + 1);
|
|
1142
|
+
if (pathSegments.some((segment) => segment === "." || segment === "..")) return void 0;
|
|
1143
|
+
if (service !== "kv" && (pathSegments.length === 0 || pathSegments[pathSegments.length - 1] === "")) return void 0;
|
|
1144
|
+
return {
|
|
1145
|
+
space: segments.slice(0, serviceIndex).join("/"),
|
|
1146
|
+
path: pathSegments.join("/")
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
1149
|
+
function safeCapabilityResource(value, service, action) {
|
|
1150
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 1024) return false;
|
|
1151
|
+
if (service === "encryption" && value.startsWith("urn:tinycloud:encryption:")) {
|
|
1152
|
+
if (!/^[A-Za-z0-9_][A-Za-z0-9_.:/-]*$/.test(value) || value.includes("//") || value.split("/").some((segment) => segment === "." || segment === "..")) return false;
|
|
1153
|
+
try {
|
|
1154
|
+
parseNetworkId(value);
|
|
1155
|
+
return true;
|
|
1156
|
+
} catch {
|
|
1157
|
+
return false;
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
const parsed = parseCapabilityResource(value, service);
|
|
1161
|
+
return parsed !== void 0 && (!parsed.path.endsWith("/") || service === "kv" && action === "tinycloud.kv/list") && (parsed.path !== "" || service === "kv" && action === "tinycloud.kv/list");
|
|
1162
|
+
}
|
|
1163
|
+
function validatedCapabilityOf(error) {
|
|
1164
|
+
const verdict = authorizationVerdictOf(error);
|
|
1165
|
+
if (verdict !== "unauthenticated" && verdict !== "forbidden") return void 0;
|
|
1166
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1167
|
+
let node = error;
|
|
1168
|
+
for (let depth = 0; depth < MAX_CAUSE_DEPTH && typeof node === "object" && node !== null && !seen.has(node); depth += 1) {
|
|
1169
|
+
seen.add(node);
|
|
1170
|
+
const record = node;
|
|
1171
|
+
const status = errorStatusOf(record);
|
|
1172
|
+
const meta = record.meta;
|
|
1173
|
+
if ((status === 401 || status === 403 || status === void 0 && record.code === ErrorCodes.AUTH_UNAUTHORIZED) && typeof meta === "object" && meta !== null && !Array.isArray(meta)) {
|
|
1174
|
+
const metadata = meta;
|
|
1175
|
+
const { resource, requiredAction } = metadata;
|
|
1176
|
+
if (typeof requiredAction === "string" && grantableActions.has(requiredAction)) {
|
|
1177
|
+
const actionService = requiredAction.slice("tinycloud.".length, requiredAction.indexOf("/"));
|
|
1178
|
+
const explicitService = record.service === void 0 ? metadata.service : record.service;
|
|
1179
|
+
if (safeCapabilityResource(resource, actionService, requiredAction) && (explicitService === void 0 || explicitService === actionService || explicitService === `tinycloud.${actionService}`)) {
|
|
1180
|
+
return { resource, requiredAction };
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
node = record.cause;
|
|
1185
|
+
}
|
|
1186
|
+
return void 0;
|
|
1187
|
+
}
|
|
1188
|
+
var MAX_CAUSE_DEPTH = 8;
|
|
1189
|
+
function errorStatusOf(node) {
|
|
1190
|
+
const meta = node.meta;
|
|
1191
|
+
const candidates = [
|
|
1192
|
+
node.status,
|
|
1193
|
+
node.statusCode,
|
|
1194
|
+
typeof meta === "object" && meta !== null ? meta.status : void 0
|
|
1195
|
+
];
|
|
1196
|
+
for (const candidate of candidates) {
|
|
1197
|
+
if (typeof candidate === "number" && Number.isInteger(candidate) && candidate >= 400 && candidate <= 599) {
|
|
1198
|
+
return candidate;
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
return void 0;
|
|
1202
|
+
}
|
|
1203
|
+
function authorizationVerdictOf(error) {
|
|
1204
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1205
|
+
let sawUnauthorizedCode = false;
|
|
1206
|
+
let node = error;
|
|
1207
|
+
for (let depth = 0; depth < MAX_CAUSE_DEPTH && typeof node === "object" && node !== null && !seen.has(node); depth += 1) {
|
|
1208
|
+
seen.add(node);
|
|
1209
|
+
const record = node;
|
|
1210
|
+
const status = errorStatusOf(record);
|
|
1211
|
+
if (status === 401) return "unauthenticated";
|
|
1212
|
+
if (status === 403) return "forbidden";
|
|
1213
|
+
if (status !== void 0) return "other";
|
|
1214
|
+
if (record.code === ErrorCodes.AUTH_UNAUTHORIZED) sawUnauthorizedCode = true;
|
|
1215
|
+
node = record.cause;
|
|
1216
|
+
}
|
|
1217
|
+
return sawUnauthorizedCode ? "forbidden" : void 0;
|
|
1218
|
+
}
|
|
996
1219
|
function storageQuotaExceededError(service, message, meta) {
|
|
997
1220
|
return {
|
|
998
1221
|
code: ErrorCodes.STORAGE_QUOTA_EXCEEDED,
|
|
@@ -1009,8 +1232,28 @@ function storageLimitReachedError(service, message, meta) {
|
|
|
1009
1232
|
meta
|
|
1010
1233
|
};
|
|
1011
1234
|
}
|
|
1235
|
+
var STORAGE_FULL_MESSAGE = "TinyCloud storage is full, so this change was not saved. Reading still works. Free up space or upgrade your plan to save again.";
|
|
1236
|
+
var STORAGE_WRITE_TOO_LARGE_MESSAGE = "This change is larger than the TinyCloud storage you have left, so it was not saved. Reading still works. Free up space or upgrade your plan to save it.";
|
|
1237
|
+
function parseStorageQuotaBytes(responseText) {
|
|
1238
|
+
const match = responseText.match(/Used:\s*(\d+)\s*bytes,\s*Limit:\s*(\d+)\s*bytes/i);
|
|
1239
|
+
if (!match) return void 0;
|
|
1240
|
+
return {
|
|
1241
|
+
usedBytes: parseInt(match[1], 10),
|
|
1242
|
+
limitBytes: parseInt(match[2], 10)
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
function storageRejectionError(service, status, meta, responseText) {
|
|
1246
|
+
const quotaMeta = { ...meta, ...parseStorageQuotaBytes(responseText) };
|
|
1247
|
+
return status === 402 ? storageQuotaExceededError(service, STORAGE_FULL_MESSAGE, quotaMeta) : storageLimitReachedError(service, STORAGE_WRITE_TOO_LARGE_MESSAGE, quotaMeta);
|
|
1248
|
+
}
|
|
1249
|
+
function isStorageFullError(error) {
|
|
1250
|
+
return error?.code === ErrorCodes.STORAGE_QUOTA_EXCEEDED || error?.code === ErrorCodes.STORAGE_LIMIT_REACHED;
|
|
1251
|
+
}
|
|
1012
1252
|
function wrapError(service, error, defaultCode = ErrorCodes.NETWORK_ERROR) {
|
|
1013
1253
|
if (error instanceof Error) {
|
|
1254
|
+
if (isRequestTimeoutError(error)) {
|
|
1255
|
+
return timeoutError(service, error.timeoutMs, error);
|
|
1256
|
+
}
|
|
1014
1257
|
if (error.name === "AbortError") {
|
|
1015
1258
|
return abortedError(service);
|
|
1016
1259
|
}
|
|
@@ -1035,6 +1278,7 @@ function errorResult(error) {
|
|
|
1035
1278
|
}
|
|
1036
1279
|
|
|
1037
1280
|
// src/base/BaseService.ts
|
|
1281
|
+
var MAX_TIMER_DELAY_MS = 2147483647;
|
|
1038
1282
|
var BaseService = class {
|
|
1039
1283
|
constructor() {
|
|
1040
1284
|
/**
|
|
@@ -1215,6 +1459,64 @@ var BaseService = class {
|
|
|
1215
1459
|
}
|
|
1216
1460
|
return controller.signal;
|
|
1217
1461
|
}
|
|
1462
|
+
/**
|
|
1463
|
+
* Create the abort signal for one request, bounded by the request timeout.
|
|
1464
|
+
*
|
|
1465
|
+
* The signal aborts when the service signs out, the context aborts, the
|
|
1466
|
+
* caller's `signal` aborts, or the timeout elapses, whichever comes first.
|
|
1467
|
+
* A timeout aborts with a {@link RequestTimeoutError} (`TimeoutError`), which
|
|
1468
|
+
* `wrapError` maps to `ErrorCodes.TIMEOUT`; the other sources keep their own
|
|
1469
|
+
* reason (normally `AbortError`, mapped to `ErrorCodes.ABORTED`).
|
|
1470
|
+
*
|
|
1471
|
+
* The timeout is `timeoutMs` when given, otherwise `config.timeout`. Only a
|
|
1472
|
+
* positive, finite value (at most 2^31-1 ms) applies; `undefined`, `0`, and
|
|
1473
|
+
* `Infinity` mean no timeout, so a per-call `0` opts out of a configured one.
|
|
1474
|
+
*
|
|
1475
|
+
* Callers must call `dispose()` once the response body has been consumed,
|
|
1476
|
+
* which clears the timer and removes the listeners this adds to the
|
|
1477
|
+
* long-lived service and context signals.
|
|
1478
|
+
*
|
|
1479
|
+
* @param signal - The caller's abort signal for this request
|
|
1480
|
+
* @param timeoutMs - Per-request timeout override in milliseconds
|
|
1481
|
+
*/
|
|
1482
|
+
createRequestSignal(signal, timeoutMs) {
|
|
1483
|
+
const controller = new AbortController();
|
|
1484
|
+
const detachers = [];
|
|
1485
|
+
let timer;
|
|
1486
|
+
const dispose = () => {
|
|
1487
|
+
if (timer !== void 0) {
|
|
1488
|
+
clearTimeout(timer);
|
|
1489
|
+
timer = void 0;
|
|
1490
|
+
}
|
|
1491
|
+
for (const detach of detachers.splice(0)) {
|
|
1492
|
+
detach();
|
|
1493
|
+
}
|
|
1494
|
+
};
|
|
1495
|
+
const abort = (reason) => {
|
|
1496
|
+
dispose();
|
|
1497
|
+
controller.abort(reason);
|
|
1498
|
+
};
|
|
1499
|
+
const requestSignal = { signal: controller.signal, dispose };
|
|
1500
|
+
const parents = [
|
|
1501
|
+
this.abortController.signal,
|
|
1502
|
+
this.context?.abortSignal,
|
|
1503
|
+
signal
|
|
1504
|
+
].filter((parent) => parent !== void 0);
|
|
1505
|
+
for (const parent of parents) {
|
|
1506
|
+
if (parent.aborted) {
|
|
1507
|
+
abort(parent.reason);
|
|
1508
|
+
return requestSignal;
|
|
1509
|
+
}
|
|
1510
|
+
const onAbort = () => abort(parent.reason);
|
|
1511
|
+
parent.addEventListener("abort", onAbort, { once: true });
|
|
1512
|
+
detachers.push(() => parent.removeEventListener("abort", onAbort));
|
|
1513
|
+
}
|
|
1514
|
+
const timeout = timeoutMs ?? this._config.timeout;
|
|
1515
|
+
if (typeof timeout === "number" && timeout > 0 && timeout <= MAX_TIMER_DELAY_MS) {
|
|
1516
|
+
timer = setTimeout(() => abort(new RequestTimeoutError(timeout)), timeout);
|
|
1517
|
+
}
|
|
1518
|
+
return requestSignal;
|
|
1519
|
+
}
|
|
1218
1520
|
/**
|
|
1219
1521
|
* Wrap an operation with error handling and telemetry.
|
|
1220
1522
|
*
|
|
@@ -1323,6 +1625,30 @@ var PrefixedKVService = class _PrefixedKVService {
|
|
|
1323
1625
|
removePrefix
|
|
1324
1626
|
});
|
|
1325
1627
|
}
|
|
1628
|
+
/**
|
|
1629
|
+
* Read the change feed for everything under this prefix. A view with an
|
|
1630
|
+
* empty prefix (`""` or `"/"`) is refused: the node does not serve a
|
|
1631
|
+
* whole-space feed.
|
|
1632
|
+
*/
|
|
1633
|
+
async changes(options) {
|
|
1634
|
+
if (this._prefix.replace(/\/+$/, "") === "") {
|
|
1635
|
+
return err(serviceError(
|
|
1636
|
+
ErrorCodes.INVALID_INPUT,
|
|
1637
|
+
"KV changes needs a non-empty prefix; this view has none",
|
|
1638
|
+
"kv"
|
|
1639
|
+
));
|
|
1640
|
+
}
|
|
1641
|
+
const syncPrefix = `${this._prefix}/`;
|
|
1642
|
+
const response = await this._kv.changes({ ...options, prefix: syncPrefix });
|
|
1643
|
+
if (!response.ok) return response;
|
|
1644
|
+
return ok({
|
|
1645
|
+
...response.data,
|
|
1646
|
+
changes: response.data.changes.map((change) => ({
|
|
1647
|
+
...change,
|
|
1648
|
+
key: change.key.startsWith(syncPrefix) ? change.key.slice(syncPrefix.length) : change.key
|
|
1649
|
+
}))
|
|
1650
|
+
});
|
|
1651
|
+
}
|
|
1326
1652
|
/**
|
|
1327
1653
|
* Delete a key.
|
|
1328
1654
|
*/
|
|
@@ -1375,11 +1701,24 @@ var KVAction = {
|
|
|
1375
1701
|
PUT: "tinycloud.kv/put",
|
|
1376
1702
|
LIST: "tinycloud.kv/list",
|
|
1377
1703
|
DELETE: "tinycloud.kv/del",
|
|
1378
|
-
HEAD: "tinycloud.kv/metadata"
|
|
1704
|
+
HEAD: "tinycloud.kv/metadata",
|
|
1705
|
+
/** The change feed; never implied by `*` or `tinycloud.kv/*`. */
|
|
1706
|
+
SYNC: "tinycloud.kv/sync",
|
|
1707
|
+
/** Retention attestation; presented through `retentionGrant`, never invoked or implied. */
|
|
1708
|
+
RETAIN: "tinycloud.kv/retain"
|
|
1379
1709
|
};
|
|
1380
1710
|
|
|
1381
1711
|
// src/kv/KVService.ts
|
|
1382
1712
|
var MAX_KV_BATCH_READ_ITEMS = 100;
|
|
1713
|
+
function isJsonObject(value) {
|
|
1714
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1715
|
+
}
|
|
1716
|
+
var REVOKED_BODY = /^(?:Invalid invocation: )?delegation-(?:(revoked): [A-Za-z0-9]+|(ancestor-revoked): ancestor=[A-Za-z0-9]+ invoked=[A-Za-z0-9]+)$/;
|
|
1717
|
+
function revocationCodeOf(body) {
|
|
1718
|
+
const match = REVOKED_BODY.exec(body.trim());
|
|
1719
|
+
if (match === null) return void 0;
|
|
1720
|
+
return match[1] !== void 0 ? ErrorCodes.AUTH_DELEGATION_REVOKED : ErrorCodes.AUTH_DELEGATION_ANCESTOR_REVOKED;
|
|
1721
|
+
}
|
|
1383
1722
|
function encodeKvBatchPartName(path) {
|
|
1384
1723
|
return encodeURIComponent(path).replace(
|
|
1385
1724
|
/[!'()*]/g,
|
|
@@ -1402,47 +1741,21 @@ var KVService = class extends BaseService {
|
|
|
1402
1741
|
get config() {
|
|
1403
1742
|
return this._config;
|
|
1404
1743
|
}
|
|
1405
|
-
// Parses "Used: X bytes, Limit: Y bytes" from tinycloud-node error responses
|
|
1406
|
-
parseQuotaInfo(errorText) {
|
|
1407
|
-
const match = errorText.match(
|
|
1408
|
-
/Used:\s*(\d+)\s*bytes,\s*Limit:\s*(\d+)\s*bytes/i
|
|
1409
|
-
);
|
|
1410
|
-
if (match) {
|
|
1411
|
-
return {
|
|
1412
|
-
usedBytes: parseInt(match[1], 10),
|
|
1413
|
-
limitBytes: parseInt(match[2], 10)
|
|
1414
|
-
};
|
|
1415
|
-
}
|
|
1416
|
-
return void 0;
|
|
1417
|
-
}
|
|
1418
1744
|
handleQuotaErrorResponse(response, errorText, key) {
|
|
1419
|
-
if (response.status
|
|
1420
|
-
|
|
1421
|
-
return err(
|
|
1422
|
-
storageQuotaExceededError(
|
|
1423
|
-
"kv",
|
|
1424
|
-
`Storage quota exceeded for key "${key}": ${errorText}`,
|
|
1425
|
-
{
|
|
1426
|
-
status: response.status,
|
|
1427
|
-
...quotaInfo ? { usedBytes: quotaInfo.usedBytes, limitBytes: quotaInfo.limitBytes } : {}
|
|
1428
|
-
}
|
|
1429
|
-
)
|
|
1430
|
-
);
|
|
1745
|
+
if (response.status !== 402 && response.status !== 413) {
|
|
1746
|
+
return void 0;
|
|
1431
1747
|
}
|
|
1432
|
-
if (response.status === 413) {
|
|
1433
|
-
|
|
1434
|
-
return err(
|
|
1435
|
-
storageLimitReachedError(
|
|
1436
|
-
"kv",
|
|
1437
|
-
`Storage limit reached for key "${key}": ${errorText}`,
|
|
1438
|
-
{
|
|
1439
|
-
status: response.status,
|
|
1440
|
-
...quotaInfo ? { usedBytes: quotaInfo.usedBytes, limitBytes: quotaInfo.limitBytes } : {}
|
|
1441
|
-
}
|
|
1442
|
-
)
|
|
1443
|
-
);
|
|
1748
|
+
if (response.status === 413 && !parseStorageQuotaBytes(errorText)) {
|
|
1749
|
+
return void 0;
|
|
1444
1750
|
}
|
|
1445
|
-
return
|
|
1751
|
+
return err(
|
|
1752
|
+
storageRejectionError(
|
|
1753
|
+
"kv",
|
|
1754
|
+
response.status,
|
|
1755
|
+
{ status: response.status, statusText: response.statusText, key },
|
|
1756
|
+
errorText
|
|
1757
|
+
)
|
|
1758
|
+
);
|
|
1446
1759
|
}
|
|
1447
1760
|
/**
|
|
1448
1761
|
* Classify a KV 404 by reading the response body once.
|
|
@@ -1501,7 +1814,7 @@ var KVService = class extends BaseService {
|
|
|
1501
1814
|
* @param path - Resource path
|
|
1502
1815
|
* @param action - KV action
|
|
1503
1816
|
* @param body - Optional request body
|
|
1504
|
-
* @param signal -
|
|
1817
|
+
* @param signal - Request signal from createRequestSignal()
|
|
1505
1818
|
* @returns Fetch response
|
|
1506
1819
|
*/
|
|
1507
1820
|
async invokeOperation(path, action, body, signal, extraHeaders) {
|
|
@@ -1517,7 +1830,7 @@ var KVService = class extends BaseService {
|
|
|
1517
1830
|
method: "POST",
|
|
1518
1831
|
headers: requestHeaders,
|
|
1519
1832
|
body,
|
|
1520
|
-
signal
|
|
1833
|
+
signal
|
|
1521
1834
|
});
|
|
1522
1835
|
}
|
|
1523
1836
|
/**
|
|
@@ -1694,6 +2007,7 @@ var KVService = class extends BaseService {
|
|
|
1694
2007
|
"kv"
|
|
1695
2008
|
));
|
|
1696
2009
|
}
|
|
2010
|
+
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
1697
2011
|
try {
|
|
1698
2012
|
const session = this.context.session;
|
|
1699
2013
|
const invocationHeaders = this.context.invokeAny(
|
|
@@ -1715,17 +2029,18 @@ var KVService = class extends BaseService {
|
|
|
1715
2029
|
const response = await this.context.fetch(`${this.host}/invoke`, {
|
|
1716
2030
|
method: "POST",
|
|
1717
2031
|
headers,
|
|
1718
|
-
signal:
|
|
2032
|
+
signal: request.signal
|
|
1719
2033
|
});
|
|
1720
2034
|
if (!response.ok) {
|
|
1721
|
-
const errorText = await response.text();
|
|
2035
|
+
const errorText = response.status === 401 || response.status === 403 ? await this.readAuthorizationText(response) : await response.text();
|
|
1722
2036
|
if (response.status === 401 || response.status === 403) {
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
2037
|
+
return this.authorizationFailure(
|
|
2038
|
+
`Failed to batch read ${keys.length} key(s)`,
|
|
2039
|
+
response,
|
|
2040
|
+
errorText,
|
|
2041
|
+
paths,
|
|
2042
|
+
action
|
|
2043
|
+
);
|
|
1729
2044
|
}
|
|
1730
2045
|
if (response.status === 413) {
|
|
1731
2046
|
return err(serviceError(
|
|
@@ -1779,6 +2094,8 @@ var KVService = class extends BaseService {
|
|
|
1779
2094
|
return ok({ results, count: results.length });
|
|
1780
2095
|
} catch (error) {
|
|
1781
2096
|
return err(wrapError("kv", error));
|
|
2097
|
+
} finally {
|
|
2098
|
+
request.dispose();
|
|
1782
2099
|
}
|
|
1783
2100
|
}
|
|
1784
2101
|
/**
|
|
@@ -1814,7 +2131,7 @@ var KVService = class extends BaseService {
|
|
|
1814
2131
|
return text;
|
|
1815
2132
|
}
|
|
1816
2133
|
}
|
|
1817
|
-
async createSignedReadUrlError(response, key) {
|
|
2134
|
+
async createSignedReadUrlError(response, key, path) {
|
|
1818
2135
|
let errorText = response.statusText;
|
|
1819
2136
|
try {
|
|
1820
2137
|
const text = await response.text();
|
|
@@ -1824,23 +2141,62 @@ var KVService = class extends BaseService {
|
|
|
1824
2141
|
} catch {
|
|
1825
2142
|
}
|
|
1826
2143
|
if (response.status === 401 || response.status === 403) {
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
2144
|
+
return this.authorizationFailure(
|
|
2145
|
+
`Failed to create signed read URL for key ${JSON.stringify(key)}`,
|
|
2146
|
+
response,
|
|
2147
|
+
errorText,
|
|
2148
|
+
[path],
|
|
2149
|
+
KVAction.GET
|
|
2150
|
+
);
|
|
1833
2151
|
}
|
|
1834
2152
|
const code = response.status === 400 ? ErrorCodes.INVALID_INPUT : ErrorCodes.NETWORK_ERROR;
|
|
1835
2153
|
return err(
|
|
1836
2154
|
serviceError(
|
|
1837
2155
|
code,
|
|
1838
|
-
`Failed to create signed read URL for key
|
|
2156
|
+
`Failed to create signed read URL for key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
|
|
1839
2157
|
"kv",
|
|
1840
2158
|
{ meta: { status: response.status, statusText: response.statusText } }
|
|
1841
2159
|
)
|
|
1842
2160
|
);
|
|
1843
2161
|
}
|
|
2162
|
+
/** A known 401/403 keeps its typed status even if reading its body fails. */
|
|
2163
|
+
async readAuthorizationText(response) {
|
|
2164
|
+
try {
|
|
2165
|
+
return await response.text();
|
|
2166
|
+
} catch {
|
|
2167
|
+
return "";
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
/**
|
|
2171
|
+
* AUTH_UNAUTHORIZED for a 401/403 response. The message keeps the HTTP
|
|
2172
|
+
* status and the server text (`<context>: <status> - <text>`, the same
|
|
2173
|
+
* shape as every other KV failure) so callers that rethrow only the message
|
|
2174
|
+
* still see the status; `meta.status` carries it for typed callers.
|
|
2175
|
+
* Server hints must match the requested path/action (whether the node sends
|
|
2176
|
+
* a relative path or full resource); metadata uses only the request-derived
|
|
2177
|
+
* canonical resource under the authenticated session's space.
|
|
2178
|
+
*/
|
|
2179
|
+
authorizationFailure(context, response, errorText, paths, expectedAction, extraMeta = {}) {
|
|
2180
|
+
const { resource, action } = parseAuthError(errorText);
|
|
2181
|
+
const requestedPath = paths.find(
|
|
2182
|
+
(path) => resource === path || resource === `${this.context.session.spaceId}/kv/${path}`
|
|
2183
|
+
);
|
|
2184
|
+
const capability = requestedPath !== void 0 && action === expectedAction ? validatedCapabilityOf({
|
|
2185
|
+
service: "kv",
|
|
2186
|
+
code: ErrorCodes.AUTH_UNAUTHORIZED,
|
|
2187
|
+
meta: {
|
|
2188
|
+
status: response.status,
|
|
2189
|
+
resource: `${this.context.session.spaceId}/kv/${requestedPath}`,
|
|
2190
|
+
requiredAction: action
|
|
2191
|
+
}
|
|
2192
|
+
}) : void 0;
|
|
2193
|
+
const detail = errorText.trim().length > 0 ? errorText : response.statusText || "authorization failed";
|
|
2194
|
+
return err(authUnauthorizedError("kv", `${context}: ${response.status} - ${detail}`, {
|
|
2195
|
+
status: response.status,
|
|
2196
|
+
...extraMeta,
|
|
2197
|
+
...capability
|
|
2198
|
+
}));
|
|
2199
|
+
}
|
|
1844
2200
|
normalizeSignedReadUrlResponse(data) {
|
|
1845
2201
|
if (!data || typeof data !== "object") {
|
|
1846
2202
|
return void 0;
|
|
@@ -1872,25 +2228,36 @@ var KVService = class extends BaseService {
|
|
|
1872
2228
|
));
|
|
1873
2229
|
}
|
|
1874
2230
|
const path = this.getFullPath(key, options?.prefix);
|
|
2231
|
+
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
1875
2232
|
try {
|
|
1876
2233
|
const response = await this.invokeOperation(
|
|
1877
2234
|
path,
|
|
1878
2235
|
KVAction.GET,
|
|
1879
2236
|
void 0,
|
|
1880
|
-
|
|
2237
|
+
request.signal,
|
|
1881
2238
|
options?.maxResponseBytes === void 0 ? void 0 : { "x-tinycloud-max-response-bytes": String(options.maxResponseBytes) }
|
|
1882
2239
|
);
|
|
1883
2240
|
if (!response.ok) {
|
|
1884
2241
|
if (response.status === 401 || response.status === 403) {
|
|
1885
|
-
const errorText2 = await
|
|
1886
|
-
const { resource, action } = parseAuthError(errorText2);
|
|
2242
|
+
const errorText2 = await this.readAuthorizationText(response);
|
|
1887
2243
|
const permissionHint = parsePermissionHintFromErrorText(errorText2);
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
2244
|
+
const structuredCapability = permissionHint?.service === "tinycloud.kv" && permissionHint.space === this.context.session.spaceId && permissionHint.path === path ? validatedCapabilityOf({
|
|
2245
|
+
service: "kv",
|
|
2246
|
+
code: ErrorCodes.AUTH_UNAUTHORIZED,
|
|
2247
|
+
meta: {
|
|
2248
|
+
status: response.status,
|
|
2249
|
+
resource: `${this.context.session.spaceId}/kv/${path}`,
|
|
2250
|
+
requiredAction: KVAction.GET
|
|
2251
|
+
}
|
|
2252
|
+
}) : void 0;
|
|
2253
|
+
return this.authorizationFailure(
|
|
2254
|
+
`Failed to get key ${JSON.stringify(key)}`,
|
|
2255
|
+
response,
|
|
2256
|
+
errorText2,
|
|
2257
|
+
[path],
|
|
2258
|
+
KVAction.GET,
|
|
2259
|
+
structuredCapability ? { permissionHint, ...structuredCapability } : {}
|
|
2260
|
+
);
|
|
1894
2261
|
}
|
|
1895
2262
|
if (response.status === 404) {
|
|
1896
2263
|
return this.classifyNotFound(response, key);
|
|
@@ -1899,7 +2266,7 @@ var KVService = class extends BaseService {
|
|
|
1899
2266
|
if (response.status === 413) {
|
|
1900
2267
|
return err(serviceError(
|
|
1901
2268
|
ErrorCodes.KV_RESPONSE_TOO_LARGE,
|
|
1902
|
-
`KV value at key
|
|
2269
|
+
`KV value at key ${JSON.stringify(key)} exceeds the requested response limit`,
|
|
1903
2270
|
"kv",
|
|
1904
2271
|
{ meta: { status: response.status, statusText: response.statusText } }
|
|
1905
2272
|
));
|
|
@@ -1907,7 +2274,7 @@ var KVService = class extends BaseService {
|
|
|
1907
2274
|
return err(
|
|
1908
2275
|
serviceError(
|
|
1909
2276
|
ErrorCodes.NETWORK_ERROR,
|
|
1910
|
-
`Failed to get key
|
|
2277
|
+
`Failed to get key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
|
|
1911
2278
|
"kv",
|
|
1912
2279
|
{ meta: { status: response.status, statusText: response.statusText } }
|
|
1913
2280
|
)
|
|
@@ -1924,6 +2291,8 @@ var KVService = class extends BaseService {
|
|
|
1924
2291
|
});
|
|
1925
2292
|
} catch (error) {
|
|
1926
2293
|
return err(wrapError("kv", error));
|
|
2294
|
+
} finally {
|
|
2295
|
+
request.dispose();
|
|
1927
2296
|
}
|
|
1928
2297
|
});
|
|
1929
2298
|
}
|
|
@@ -1951,33 +2320,33 @@ var KVService = class extends BaseService {
|
|
|
1951
2320
|
}
|
|
1952
2321
|
const path = this.getFullPath(key, options?.prefix);
|
|
1953
2322
|
const body = this.serializePutValue(value, options?.contentType);
|
|
2323
|
+
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
1954
2324
|
try {
|
|
1955
2325
|
const response = await this.invokeOperation(
|
|
1956
2326
|
path,
|
|
1957
2327
|
KVAction.PUT,
|
|
1958
2328
|
body,
|
|
1959
|
-
|
|
2329
|
+
request.signal,
|
|
1960
2330
|
{
|
|
1961
2331
|
...options?.ifMatch === void 0 ? {} : { "if-match": options.ifMatch },
|
|
1962
2332
|
...options?.ifNoneMatch === void 0 ? {} : { "if-none-match": options.ifNoneMatch }
|
|
1963
2333
|
}
|
|
1964
2334
|
);
|
|
1965
2335
|
if (response.status === 401 || response.status === 403) {
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
}));
|
|
2336
|
+
return this.authorizationFailure(
|
|
2337
|
+
`Failed to put key ${JSON.stringify(key)}`,
|
|
2338
|
+
response,
|
|
2339
|
+
await this.readAuthorizationText(response),
|
|
2340
|
+
[path],
|
|
2341
|
+
KVAction.PUT
|
|
2342
|
+
);
|
|
1974
2343
|
}
|
|
1975
2344
|
if (!response.ok) {
|
|
1976
2345
|
const errorText = await response.text();
|
|
1977
2346
|
if (response.status === 412) {
|
|
1978
2347
|
return err(serviceError(
|
|
1979
2348
|
ErrorCodes.KV_PRECONDITION_FAILED,
|
|
1980
|
-
`KV precondition failed for key
|
|
2349
|
+
`KV precondition failed for key ${JSON.stringify(key)}`,
|
|
1981
2350
|
"kv",
|
|
1982
2351
|
{ meta: { status: response.status, statusText: response.statusText } }
|
|
1983
2352
|
));
|
|
@@ -1985,7 +2354,7 @@ var KVService = class extends BaseService {
|
|
|
1985
2354
|
if (response.status === 503 && (options?.ifMatch !== void 0 || options?.ifNoneMatch !== void 0)) {
|
|
1986
2355
|
return err(serviceError(
|
|
1987
2356
|
ErrorCodes.KV_CONFLICT,
|
|
1988
|
-
`Concurrent KV update conflicted for key
|
|
2357
|
+
`Concurrent KV update conflicted for key ${JSON.stringify(key)}`,
|
|
1989
2358
|
"kv",
|
|
1990
2359
|
{ meta: { status: response.status, statusText: response.statusText } }
|
|
1991
2360
|
));
|
|
@@ -2001,7 +2370,7 @@ var KVService = class extends BaseService {
|
|
|
2001
2370
|
return err(
|
|
2002
2371
|
serviceError(
|
|
2003
2372
|
ErrorCodes.KV_WRITE_FAILED,
|
|
2004
|
-
`Failed to put key
|
|
2373
|
+
`Failed to put key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
|
|
2005
2374
|
"kv",
|
|
2006
2375
|
{ meta: { status: response.status, statusText: response.statusText } }
|
|
2007
2376
|
)
|
|
@@ -2013,6 +2382,8 @@ var KVService = class extends BaseService {
|
|
|
2013
2382
|
});
|
|
2014
2383
|
} catch (error) {
|
|
2015
2384
|
return err(wrapError("kv", error));
|
|
2385
|
+
} finally {
|
|
2386
|
+
request.dispose();
|
|
2016
2387
|
}
|
|
2017
2388
|
});
|
|
2018
2389
|
}
|
|
@@ -2052,6 +2423,7 @@ var KVService = class extends BaseService {
|
|
|
2052
2423
|
seen.add(path);
|
|
2053
2424
|
}
|
|
2054
2425
|
let requestMayHaveDispatched = false;
|
|
2426
|
+
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
2055
2427
|
try {
|
|
2056
2428
|
const body = new FormData();
|
|
2057
2429
|
for (let index = 0; index < items.length; index++) {
|
|
@@ -2074,7 +2446,7 @@ var KVService = class extends BaseService {
|
|
|
2074
2446
|
method: "POST",
|
|
2075
2447
|
headers,
|
|
2076
2448
|
body,
|
|
2077
|
-
signal:
|
|
2449
|
+
signal: request.signal
|
|
2078
2450
|
};
|
|
2079
2451
|
const fetchFn = this.context.fetch;
|
|
2080
2452
|
requestMayHaveDispatched = true;
|
|
@@ -2095,12 +2467,13 @@ var KVService = class extends BaseService {
|
|
|
2095
2467
|
);
|
|
2096
2468
|
}
|
|
2097
2469
|
if (response.status === 401 || response.status === 403) {
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2470
|
+
return this.authorizationFailure(
|
|
2471
|
+
`Failed to batch put ${items.length} key(s)`,
|
|
2472
|
+
response,
|
|
2473
|
+
errorText,
|
|
2474
|
+
paths,
|
|
2475
|
+
KVAction.PUT
|
|
2476
|
+
);
|
|
2104
2477
|
}
|
|
2105
2478
|
const quotaError = this.handleQuotaErrorResponse(
|
|
2106
2479
|
response,
|
|
@@ -2124,14 +2497,16 @@ var KVService = class extends BaseService {
|
|
|
2124
2497
|
rawBody = await response.json();
|
|
2125
2498
|
} catch (jsonError) {
|
|
2126
2499
|
const cause = jsonError instanceof Error ? jsonError : new Error(String(jsonError));
|
|
2500
|
+
const timedOut = isRequestTimeoutError(cause);
|
|
2127
2501
|
return err(
|
|
2128
2502
|
serviceError(
|
|
2129
|
-
ErrorCodes.NETWORK_ERROR,
|
|
2130
|
-
`KV batchPut response was not valid JSON: ${cause.message}`,
|
|
2503
|
+
timedOut ? ErrorCodes.TIMEOUT : ErrorCodes.NETWORK_ERROR,
|
|
2504
|
+
timedOut ? `KV batchPut response body did not arrive in time: ${cause.message}` : `KV batchPut response was not valid JSON: ${cause.message}`,
|
|
2131
2505
|
"kv",
|
|
2132
2506
|
{
|
|
2133
2507
|
cause,
|
|
2134
2508
|
meta: {
|
|
2509
|
+
...timedOut ? { timeoutMs: cause.timeoutMs } : {},
|
|
2135
2510
|
requestMayHaveDispatched: true,
|
|
2136
2511
|
responseReceived: true,
|
|
2137
2512
|
status: response.status,
|
|
@@ -2186,6 +2561,8 @@ var KVService = class extends BaseService {
|
|
|
2186
2561
|
...wrapped,
|
|
2187
2562
|
meta: { ...wrapped.meta, requestMayHaveDispatched }
|
|
2188
2563
|
});
|
|
2564
|
+
} finally {
|
|
2565
|
+
request.dispose();
|
|
2189
2566
|
}
|
|
2190
2567
|
});
|
|
2191
2568
|
}
|
|
@@ -2208,26 +2585,27 @@ var KVService = class extends BaseService {
|
|
|
2208
2585
|
if (options?.path) {
|
|
2209
2586
|
listPath = listPath ? `${listPath}/${options.path}` : options.path;
|
|
2210
2587
|
}
|
|
2588
|
+
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
2211
2589
|
try {
|
|
2212
2590
|
const response = await this.invokeOperation(
|
|
2213
2591
|
listPath,
|
|
2214
2592
|
KVAction.LIST,
|
|
2215
2593
|
void 0,
|
|
2216
|
-
|
|
2594
|
+
request.signal,
|
|
2217
2595
|
options?.limit === void 0 && options?.cursor === void 0 ? void 0 : {
|
|
2218
2596
|
...options?.limit === void 0 ? {} : { "x-tinycloud-limit": String(options.limit) },
|
|
2219
2597
|
...options?.cursor === void 0 ? {} : { "x-tinycloud-cursor": options.cursor }
|
|
2220
2598
|
}
|
|
2221
2599
|
);
|
|
2222
2600
|
if (!response.ok) {
|
|
2223
|
-
if (response.status === 401) {
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2601
|
+
if (response.status === 401 || response.status === 403) {
|
|
2602
|
+
return this.authorizationFailure(
|
|
2603
|
+
"Failed to list keys",
|
|
2604
|
+
response,
|
|
2605
|
+
await this.readAuthorizationText(response),
|
|
2606
|
+
[listPath],
|
|
2607
|
+
KVAction.LIST
|
|
2608
|
+
);
|
|
2231
2609
|
}
|
|
2232
2610
|
const errorText = await response.text();
|
|
2233
2611
|
return err(
|
|
@@ -2254,9 +2632,174 @@ var KVService = class extends BaseService {
|
|
|
2254
2632
|
});
|
|
2255
2633
|
} catch (error) {
|
|
2256
2634
|
return err(wrapError("kv", error));
|
|
2635
|
+
} finally {
|
|
2636
|
+
request.dispose();
|
|
2637
|
+
}
|
|
2638
|
+
});
|
|
2639
|
+
}
|
|
2640
|
+
/**
|
|
2641
|
+
* Read one page of the `tinycloud.kv/sync` change feed for a prefix.
|
|
2642
|
+
*/
|
|
2643
|
+
async changes(options) {
|
|
2644
|
+
return this.withTelemetry("changes", options?.prefix, async () => {
|
|
2645
|
+
if (!this.requireAuth()) {
|
|
2646
|
+
return err(authRequiredError("kv"));
|
|
2647
|
+
}
|
|
2648
|
+
if (typeof options?.prefix !== "string" || options.prefix.length === 0) {
|
|
2649
|
+
return err(serviceError(
|
|
2650
|
+
ErrorCodes.INVALID_INPUT,
|
|
2651
|
+
"KV changes prefix must be a non-empty string",
|
|
2652
|
+
"kv"
|
|
2653
|
+
));
|
|
2654
|
+
}
|
|
2655
|
+
if (options.limit !== void 0 && (!Number.isInteger(options.limit) || options.limit < 1 || options.limit > 1e3)) {
|
|
2656
|
+
return err(serviceError(
|
|
2657
|
+
ErrorCodes.INVALID_INPUT,
|
|
2658
|
+
"KV changes limit must be an integer from 1 through 1000",
|
|
2659
|
+
"kv"
|
|
2660
|
+
));
|
|
2661
|
+
}
|
|
2662
|
+
const headers = {};
|
|
2663
|
+
if (options.limit !== void 0) headers["x-tinycloud-limit"] = String(options.limit);
|
|
2664
|
+
if (options.cursor !== void 0) headers["x-tinycloud-cursor"] = options.cursor;
|
|
2665
|
+
if (options.retentionGrant !== void 0) {
|
|
2666
|
+
headers["x-tinycloud-retention-grant"] = options.retentionGrant;
|
|
2667
|
+
}
|
|
2668
|
+
const request = this.createRequestSignal(options.signal, options.timeout);
|
|
2669
|
+
try {
|
|
2670
|
+
const response = await this.invokeOperation(
|
|
2671
|
+
options.prefix,
|
|
2672
|
+
KVAction.SYNC,
|
|
2673
|
+
void 0,
|
|
2674
|
+
request.signal,
|
|
2675
|
+
headers
|
|
2676
|
+
);
|
|
2677
|
+
if (!response.ok) {
|
|
2678
|
+
return await this.changesFailure(response, options.prefix, request.signal);
|
|
2679
|
+
}
|
|
2680
|
+
const text = await response.text();
|
|
2681
|
+
let body;
|
|
2682
|
+
try {
|
|
2683
|
+
body = JSON.parse(text);
|
|
2684
|
+
} catch {
|
|
2685
|
+
body = void 0;
|
|
2686
|
+
}
|
|
2687
|
+
const page = this.normalizeChangesResponse(body);
|
|
2688
|
+
if (!page) {
|
|
2689
|
+
return err(serviceError(
|
|
2690
|
+
ErrorCodes.NETWORK_ERROR,
|
|
2691
|
+
"KV changes response was not a tinycloud.kv/sync page",
|
|
2692
|
+
"kv",
|
|
2693
|
+
{ meta: { status: response.status, statusText: response.statusText } }
|
|
2694
|
+
));
|
|
2695
|
+
}
|
|
2696
|
+
return ok(page);
|
|
2697
|
+
} catch (error) {
|
|
2698
|
+
return err(wrapError("kv", error));
|
|
2699
|
+
} finally {
|
|
2700
|
+
request.dispose();
|
|
2257
2701
|
}
|
|
2258
2702
|
});
|
|
2259
2703
|
}
|
|
2704
|
+
/**
|
|
2705
|
+
* Map a failed `kv/sync` response. 410 and the retention 403 carry
|
|
2706
|
+
* `{"error":{"code","reason"}}`; a revoked grant is a 401 whose whole body
|
|
2707
|
+
* is the node's revocation error (see REVOKED_BODY). A body read that
|
|
2708
|
+
* fails because the request was cancelled or timed out rethrows, so the
|
|
2709
|
+
* caller sees `ABORTED`/`TIMEOUT` rather than a mapped status.
|
|
2710
|
+
*/
|
|
2711
|
+
async changesFailure(response, prefix, signal) {
|
|
2712
|
+
if (response.status === 404) {
|
|
2713
|
+
return this.classifyNotFound(response, prefix);
|
|
2714
|
+
}
|
|
2715
|
+
let errorText;
|
|
2716
|
+
try {
|
|
2717
|
+
errorText = await response.text();
|
|
2718
|
+
} catch (error) {
|
|
2719
|
+
if (signal.aborted) throw error;
|
|
2720
|
+
errorText = "";
|
|
2721
|
+
}
|
|
2722
|
+
const meta = { status: response.status, statusText: response.statusText };
|
|
2723
|
+
let reason;
|
|
2724
|
+
let bodyCode;
|
|
2725
|
+
try {
|
|
2726
|
+
const body = JSON.parse(errorText);
|
|
2727
|
+
const error = isJsonObject(body) ? body.error : void 0;
|
|
2728
|
+
if (isJsonObject(error)) {
|
|
2729
|
+
if (typeof error.code === "string") bodyCode = error.code;
|
|
2730
|
+
if (typeof error.reason === "string") reason = error.reason;
|
|
2731
|
+
}
|
|
2732
|
+
} catch {
|
|
2733
|
+
}
|
|
2734
|
+
const context = `Failed to read KV changes for ${JSON.stringify(prefix)}`;
|
|
2735
|
+
if (response.status === 410) {
|
|
2736
|
+
return err(serviceError(
|
|
2737
|
+
ErrorCodes.KV_SYNC_RESET_REQUIRED,
|
|
2738
|
+
`${context}: the node requires a reset (${reason ?? "reason not given"}); discard sync state and restart without a cursor`,
|
|
2739
|
+
"kv",
|
|
2740
|
+
{ meta: { ...meta, reason } }
|
|
2741
|
+
));
|
|
2742
|
+
}
|
|
2743
|
+
if (response.status === 403 && bodyCode === "RETENTION_GRANT_REFUSED") {
|
|
2744
|
+
return err(serviceError(
|
|
2745
|
+
ErrorCodes.KV_RETENTION_GRANT_REFUSED,
|
|
2746
|
+
`${context}: the retention grant was refused (${reason ?? "reason not given"})`,
|
|
2747
|
+
"kv",
|
|
2748
|
+
{ meta: { ...meta, reason } }
|
|
2749
|
+
));
|
|
2750
|
+
}
|
|
2751
|
+
if (response.status === 401) {
|
|
2752
|
+
const revoked = revocationCodeOf(errorText);
|
|
2753
|
+
if (revoked !== void 0) {
|
|
2754
|
+
return err(serviceError(revoked, `${context}: 401 - ${errorText}`, "kv", { meta }));
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
2757
|
+
if (response.status === 401 || response.status === 403) {
|
|
2758
|
+
return this.authorizationFailure(context, response, errorText, [prefix], KVAction.SYNC);
|
|
2759
|
+
}
|
|
2760
|
+
return err(serviceError(
|
|
2761
|
+
response.status === 400 ? ErrorCodes.INVALID_INPUT : ErrorCodes.NETWORK_ERROR,
|
|
2762
|
+
`${context}: ${response.status} - ${errorText}`,
|
|
2763
|
+
"kv",
|
|
2764
|
+
{ meta }
|
|
2765
|
+
));
|
|
2766
|
+
}
|
|
2767
|
+
normalizeChangesResponse(data) {
|
|
2768
|
+
if (!isJsonObject(data)) return void 0;
|
|
2769
|
+
const { changes, more, cursor, source, authority } = data;
|
|
2770
|
+
if (!Array.isArray(changes) || typeof more !== "boolean" || typeof cursor !== "string" || !isJsonObject(source) || typeof source.nodeDid !== "string" || typeof source.space !== "string" || typeof source.prefix !== "string" || !isJsonObject(authority)) {
|
|
2771
|
+
return void 0;
|
|
2772
|
+
}
|
|
2773
|
+
const { notBefore, expiresAt, retainUntil } = authority;
|
|
2774
|
+
if (notBefore !== null && typeof notBefore !== "string" || expiresAt !== null && typeof expiresAt !== "string" || retainUntil !== null && typeof retainUntil !== "string") {
|
|
2775
|
+
return void 0;
|
|
2776
|
+
}
|
|
2777
|
+
const parsed = [];
|
|
2778
|
+
for (const change of changes) {
|
|
2779
|
+
if (!isJsonObject(change) || typeof change.key !== "string") return void 0;
|
|
2780
|
+
if (change.deleted === true) {
|
|
2781
|
+
parsed.push({ key: change.key, deleted: true });
|
|
2782
|
+
continue;
|
|
2783
|
+
}
|
|
2784
|
+
const { metadata } = change;
|
|
2785
|
+
if (change.deleted !== false || typeof change.etag !== "string" || !isJsonObject(metadata) || !Object.values(metadata).every((value) => typeof value === "string")) {
|
|
2786
|
+
return void 0;
|
|
2787
|
+
}
|
|
2788
|
+
parsed.push({
|
|
2789
|
+
key: change.key,
|
|
2790
|
+
deleted: false,
|
|
2791
|
+
etag: change.etag,
|
|
2792
|
+
metadata
|
|
2793
|
+
});
|
|
2794
|
+
}
|
|
2795
|
+
return {
|
|
2796
|
+
changes: parsed,
|
|
2797
|
+
more,
|
|
2798
|
+
cursor,
|
|
2799
|
+
source: { nodeDid: source.nodeDid, space: source.space, prefix: source.prefix },
|
|
2800
|
+
authority: { notBefore, expiresAt, retainUntil }
|
|
2801
|
+
};
|
|
2802
|
+
}
|
|
2260
2803
|
/**
|
|
2261
2804
|
* Delete a key.
|
|
2262
2805
|
*/
|
|
@@ -2266,23 +2809,24 @@ var KVService = class extends BaseService {
|
|
|
2266
2809
|
return err(authRequiredError("kv"));
|
|
2267
2810
|
}
|
|
2268
2811
|
const path = this.getFullPath(key, options?.prefix);
|
|
2812
|
+
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
2269
2813
|
try {
|
|
2270
2814
|
const response = await this.invokeOperation(
|
|
2271
2815
|
path,
|
|
2272
2816
|
KVAction.DELETE,
|
|
2273
2817
|
void 0,
|
|
2274
|
-
|
|
2818
|
+
request.signal,
|
|
2275
2819
|
options?.ifMatch === void 0 ? void 0 : { "if-match": options.ifMatch }
|
|
2276
2820
|
);
|
|
2277
2821
|
if (!response.ok) {
|
|
2278
|
-
if (response.status === 401) {
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2822
|
+
if (response.status === 401 || response.status === 403) {
|
|
2823
|
+
return this.authorizationFailure(
|
|
2824
|
+
`Failed to delete key ${JSON.stringify(key)}`,
|
|
2825
|
+
response,
|
|
2826
|
+
await this.readAuthorizationText(response),
|
|
2827
|
+
[path],
|
|
2828
|
+
KVAction.DELETE
|
|
2829
|
+
);
|
|
2286
2830
|
}
|
|
2287
2831
|
if (response.status === 404) {
|
|
2288
2832
|
return this.classifyNotFound(response, key);
|
|
@@ -2291,7 +2835,7 @@ var KVService = class extends BaseService {
|
|
|
2291
2835
|
if (response.status === 412) {
|
|
2292
2836
|
return err(serviceError(
|
|
2293
2837
|
ErrorCodes.KV_PRECONDITION_FAILED,
|
|
2294
|
-
`KV precondition failed for key
|
|
2838
|
+
`KV precondition failed for key ${JSON.stringify(key)}`,
|
|
2295
2839
|
"kv",
|
|
2296
2840
|
{ meta: { status: response.status, statusText: response.statusText } }
|
|
2297
2841
|
));
|
|
@@ -2299,7 +2843,7 @@ var KVService = class extends BaseService {
|
|
|
2299
2843
|
if (response.status === 503 && options?.ifMatch !== void 0) {
|
|
2300
2844
|
return err(serviceError(
|
|
2301
2845
|
ErrorCodes.KV_CONFLICT,
|
|
2302
|
-
`Concurrent KV delete conflicted for key
|
|
2846
|
+
`Concurrent KV delete conflicted for key ${JSON.stringify(key)}`,
|
|
2303
2847
|
"kv",
|
|
2304
2848
|
{ meta: { status: response.status, statusText: response.statusText } }
|
|
2305
2849
|
));
|
|
@@ -2307,7 +2851,7 @@ var KVService = class extends BaseService {
|
|
|
2307
2851
|
return err(
|
|
2308
2852
|
serviceError(
|
|
2309
2853
|
ErrorCodes.NETWORK_ERROR,
|
|
2310
|
-
`Failed to delete key
|
|
2854
|
+
`Failed to delete key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
|
|
2311
2855
|
"kv",
|
|
2312
2856
|
{ meta: { status: response.status, statusText: response.statusText } }
|
|
2313
2857
|
)
|
|
@@ -2319,6 +2863,8 @@ var KVService = class extends BaseService {
|
|
|
2319
2863
|
});
|
|
2320
2864
|
} catch (error) {
|
|
2321
2865
|
return err(wrapError("kv", error));
|
|
2866
|
+
} finally {
|
|
2867
|
+
request.dispose();
|
|
2322
2868
|
}
|
|
2323
2869
|
});
|
|
2324
2870
|
}
|
|
@@ -2331,22 +2877,23 @@ var KVService = class extends BaseService {
|
|
|
2331
2877
|
return err(authRequiredError("kv"));
|
|
2332
2878
|
}
|
|
2333
2879
|
const path = this.getFullPath(key, options?.prefix);
|
|
2880
|
+
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
2334
2881
|
try {
|
|
2335
2882
|
const response = await this.invokeOperation(
|
|
2336
2883
|
path,
|
|
2337
2884
|
KVAction.HEAD,
|
|
2338
2885
|
void 0,
|
|
2339
|
-
|
|
2886
|
+
request.signal
|
|
2340
2887
|
);
|
|
2341
2888
|
if (!response.ok) {
|
|
2342
|
-
if (response.status === 401) {
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2889
|
+
if (response.status === 401 || response.status === 403) {
|
|
2890
|
+
return this.authorizationFailure(
|
|
2891
|
+
`Failed to get metadata for key ${JSON.stringify(key)}`,
|
|
2892
|
+
response,
|
|
2893
|
+
await this.readAuthorizationText(response),
|
|
2894
|
+
[path],
|
|
2895
|
+
KVAction.HEAD
|
|
2896
|
+
);
|
|
2350
2897
|
}
|
|
2351
2898
|
if (response.status === 404) {
|
|
2352
2899
|
return this.classifyNotFound(response, key);
|
|
@@ -2355,7 +2902,7 @@ var KVService = class extends BaseService {
|
|
|
2355
2902
|
return err(
|
|
2356
2903
|
serviceError(
|
|
2357
2904
|
ErrorCodes.NETWORK_ERROR,
|
|
2358
|
-
`Failed to get metadata for key
|
|
2905
|
+
`Failed to get metadata for key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
|
|
2359
2906
|
"kv",
|
|
2360
2907
|
{ meta: { status: response.status, statusText: response.statusText } }
|
|
2361
2908
|
)
|
|
@@ -2367,6 +2914,8 @@ var KVService = class extends BaseService {
|
|
|
2367
2914
|
});
|
|
2368
2915
|
} catch (error) {
|
|
2369
2916
|
return err(wrapError("kv", error));
|
|
2917
|
+
} finally {
|
|
2918
|
+
request.dispose();
|
|
2370
2919
|
}
|
|
2371
2920
|
});
|
|
2372
2921
|
}
|
|
@@ -2404,15 +2953,16 @@ var KVService = class extends BaseService {
|
|
|
2404
2953
|
if (options?.etag !== void 0) {
|
|
2405
2954
|
body.etag = options.etag;
|
|
2406
2955
|
}
|
|
2956
|
+
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
2407
2957
|
try {
|
|
2408
2958
|
const response = await this.context.fetch(`${this.host}/signed/kv`, {
|
|
2409
2959
|
method: "POST",
|
|
2410
2960
|
headers: this.withJsonContentType(headers),
|
|
2411
2961
|
body: JSON.stringify(body),
|
|
2412
|
-
signal:
|
|
2962
|
+
signal: request.signal
|
|
2413
2963
|
});
|
|
2414
2964
|
if (!response.ok) {
|
|
2415
|
-
return this.createSignedReadUrlError(response, key);
|
|
2965
|
+
return this.createSignedReadUrlError(response, key, path);
|
|
2416
2966
|
}
|
|
2417
2967
|
const signedUrl = this.normalizeSignedReadUrlResponse(
|
|
2418
2968
|
await response.json()
|
|
@@ -2429,6 +2979,8 @@ var KVService = class extends BaseService {
|
|
|
2429
2979
|
return ok(signedUrl);
|
|
2430
2980
|
} catch (error) {
|
|
2431
2981
|
return err(wrapError("kv", error));
|
|
2982
|
+
} finally {
|
|
2983
|
+
request.dispose();
|
|
2432
2984
|
}
|
|
2433
2985
|
});
|
|
2434
2986
|
}
|
|
@@ -2642,6 +3194,7 @@ var SQLService = class extends BaseService {
|
|
|
2642
3194
|
if (!this.requireAuth()) {
|
|
2643
3195
|
return err(authRequiredError("sql"));
|
|
2644
3196
|
}
|
|
3197
|
+
const request = this.createRequestSignal(options?.signal);
|
|
2645
3198
|
try {
|
|
2646
3199
|
const body = {
|
|
2647
3200
|
action: "query",
|
|
@@ -2650,19 +3203,22 @@ var SQLService = class extends BaseService {
|
|
|
2650
3203
|
};
|
|
2651
3204
|
if (options?.maxRows !== void 0) body.maxRows = options.maxRows;
|
|
2652
3205
|
if (options?.maxBytes !== void 0) body.maxBytes = options.maxBytes;
|
|
3206
|
+
const action = this.actionForSql(sql, SQLAction.READ);
|
|
2653
3207
|
const response = await this.invokeSQL(
|
|
2654
3208
|
dbName,
|
|
2655
|
-
|
|
3209
|
+
action,
|
|
2656
3210
|
body,
|
|
2657
|
-
|
|
3211
|
+
request.signal
|
|
2658
3212
|
);
|
|
2659
3213
|
if (!response.ok) {
|
|
2660
|
-
return this.handleErrorResponse(response, "query");
|
|
3214
|
+
return this.handleErrorResponse(response, "query", dbName, [action]);
|
|
2661
3215
|
}
|
|
2662
3216
|
const data = await response.json();
|
|
2663
3217
|
return ok(data);
|
|
2664
3218
|
} catch (error) {
|
|
2665
3219
|
return err(wrapError("sql", error));
|
|
3220
|
+
} finally {
|
|
3221
|
+
request.dispose();
|
|
2666
3222
|
}
|
|
2667
3223
|
});
|
|
2668
3224
|
}
|
|
@@ -2671,6 +3227,7 @@ var SQLService = class extends BaseService {
|
|
|
2671
3227
|
if (!this.requireAuth()) {
|
|
2672
3228
|
return err(authRequiredError("sql"));
|
|
2673
3229
|
}
|
|
3230
|
+
const request = this.createRequestSignal(options?.signal);
|
|
2674
3231
|
try {
|
|
2675
3232
|
const body = {
|
|
2676
3233
|
action: "execute",
|
|
@@ -2686,19 +3243,22 @@ var SQLService = class extends BaseService {
|
|
|
2686
3243
|
(statement) => this.actionForSql(statement, SQLAction.SCHEMA)
|
|
2687
3244
|
)
|
|
2688
3245
|
];
|
|
3246
|
+
const requestedActions = this.dedupeActions(actions);
|
|
2689
3247
|
const response = await this.invokeSQL(
|
|
2690
3248
|
dbName,
|
|
2691
|
-
|
|
3249
|
+
requestedActions,
|
|
2692
3250
|
body,
|
|
2693
|
-
|
|
3251
|
+
request.signal
|
|
2694
3252
|
);
|
|
2695
3253
|
if (!response.ok) {
|
|
2696
|
-
return this.handleErrorResponse(response, "execute");
|
|
3254
|
+
return this.handleErrorResponse(response, "execute", dbName, requestedActions);
|
|
2697
3255
|
}
|
|
2698
3256
|
const data = await response.json();
|
|
2699
3257
|
return ok(data);
|
|
2700
3258
|
} catch (error) {
|
|
2701
3259
|
return err(wrapError("sql", error));
|
|
3260
|
+
} finally {
|
|
3261
|
+
request.dispose();
|
|
2702
3262
|
}
|
|
2703
3263
|
});
|
|
2704
3264
|
}
|
|
@@ -2707,20 +3267,24 @@ var SQLService = class extends BaseService {
|
|
|
2707
3267
|
if (!this.requireAuth()) {
|
|
2708
3268
|
return err(authRequiredError("sql"));
|
|
2709
3269
|
}
|
|
3270
|
+
const request = this.createRequestSignal(options?.signal);
|
|
2710
3271
|
try {
|
|
3272
|
+
const requestedActions = this.actionsForSqlBatch(statements);
|
|
2711
3273
|
const response = await this.invokeSQL(
|
|
2712
3274
|
dbName,
|
|
2713
|
-
|
|
3275
|
+
requestedActions,
|
|
2714
3276
|
{ action: "batch", statements },
|
|
2715
|
-
|
|
3277
|
+
request.signal
|
|
2716
3278
|
);
|
|
2717
3279
|
if (!response.ok) {
|
|
2718
|
-
return this.handleErrorResponse(response, "batch");
|
|
3280
|
+
return this.handleErrorResponse(response, "batch", dbName, requestedActions);
|
|
2719
3281
|
}
|
|
2720
3282
|
const data = await response.json();
|
|
2721
3283
|
return ok(data);
|
|
2722
3284
|
} catch (error) {
|
|
2723
3285
|
return err(wrapError("sql", error));
|
|
3286
|
+
} finally {
|
|
3287
|
+
request.dispose();
|
|
2724
3288
|
}
|
|
2725
3289
|
});
|
|
2726
3290
|
}
|
|
@@ -2729,20 +3293,23 @@ var SQLService = class extends BaseService {
|
|
|
2729
3293
|
if (!this.requireAuth()) {
|
|
2730
3294
|
return err(authRequiredError("sql"));
|
|
2731
3295
|
}
|
|
3296
|
+
const request = this.createRequestSignal(options?.signal);
|
|
2732
3297
|
try {
|
|
2733
3298
|
const response = await this.invokeSQL(
|
|
2734
3299
|
dbName,
|
|
2735
3300
|
SQLAction.WRITE,
|
|
2736
3301
|
{ action: "execute_statement", name, params: params ?? [] },
|
|
2737
|
-
|
|
3302
|
+
request.signal
|
|
2738
3303
|
);
|
|
2739
3304
|
if (!response.ok) {
|
|
2740
|
-
return this.handleErrorResponse(response, "executeStatement");
|
|
3305
|
+
return this.handleErrorResponse(response, "executeStatement", dbName, [SQLAction.WRITE]);
|
|
2741
3306
|
}
|
|
2742
3307
|
const data = await response.json();
|
|
2743
3308
|
return ok(data);
|
|
2744
3309
|
} catch (error) {
|
|
2745
3310
|
return err(wrapError("sql", error));
|
|
3311
|
+
} finally {
|
|
3312
|
+
request.dispose();
|
|
2746
3313
|
}
|
|
2747
3314
|
});
|
|
2748
3315
|
}
|
|
@@ -2751,15 +3318,16 @@ var SQLService = class extends BaseService {
|
|
|
2751
3318
|
if (!this.requireAuth()) {
|
|
2752
3319
|
return err(authRequiredError("sql"));
|
|
2753
3320
|
}
|
|
3321
|
+
const request = this.createRequestSignal(options?.signal);
|
|
2754
3322
|
try {
|
|
2755
3323
|
const response = await this.invokeSQL(
|
|
2756
3324
|
dbName,
|
|
2757
3325
|
SQLAction.READ,
|
|
2758
3326
|
{ action: "export" },
|
|
2759
|
-
|
|
3327
|
+
request.signal
|
|
2760
3328
|
);
|
|
2761
3329
|
if (!response.ok) {
|
|
2762
|
-
return this.handleErrorResponse(response, "export");
|
|
3330
|
+
return this.handleErrorResponse(response, "export", dbName, [SQLAction.READ]);
|
|
2763
3331
|
}
|
|
2764
3332
|
const resp = response;
|
|
2765
3333
|
if (typeof resp.blob === "function") {
|
|
@@ -2770,26 +3338,31 @@ var SQLService = class extends BaseService {
|
|
|
2770
3338
|
return ok(text);
|
|
2771
3339
|
} catch (error) {
|
|
2772
3340
|
return err(wrapError("sql", error));
|
|
3341
|
+
} finally {
|
|
3342
|
+
request.dispose();
|
|
2773
3343
|
}
|
|
2774
3344
|
});
|
|
2775
3345
|
}
|
|
2776
3346
|
async applyMigrationsOnDbUnlocked(dbName, options) {
|
|
2777
3347
|
return this.withTelemetry("migrations.apply", dbName, async () => {
|
|
2778
|
-
const created = await this.ensureMigrationsTable(dbName, options.signal);
|
|
2779
|
-
if (!created.ok) return created;
|
|
2780
3348
|
const listed = await this.queryOnDb(
|
|
2781
3349
|
dbName,
|
|
2782
3350
|
`SELECT id FROM ${MIGRATIONS_TABLE} WHERE namespace = ? ORDER BY applied_at, id`,
|
|
2783
3351
|
[options.namespace],
|
|
2784
3352
|
{ signal: options.signal }
|
|
2785
3353
|
);
|
|
2786
|
-
|
|
3354
|
+
const tableMissing = !listed.ok && (listed.error.code === ErrorCodes.SQL_DATABASE_NOT_FOUND || isMissingTableError(listed.error.message));
|
|
3355
|
+
if (!listed.ok && !tableMissing) return listed;
|
|
2787
3356
|
const appliedIds = new Set(
|
|
2788
|
-
listed.data.rows.map((row) => rowValue(row, 0)).filter((id) => typeof id === "string")
|
|
3357
|
+
(listed.ok ? listed.data.rows : []).map((row) => rowValue(row, 0)).filter((id) => typeof id === "string")
|
|
2789
3358
|
);
|
|
2790
3359
|
const skipped = options.migrations.filter((migration) => appliedIds.has(migration.id)).map((migration) => migration.id);
|
|
2791
3360
|
const pending = options.migrations.filter((migration) => !appliedIds.has(migration.id));
|
|
2792
3361
|
const applied = [];
|
|
3362
|
+
if (pending.length > 0 && tableMissing) {
|
|
3363
|
+
const created = await this.ensureMigrationsTable(dbName, options.signal);
|
|
3364
|
+
if (!created.ok) return created;
|
|
3365
|
+
}
|
|
2793
3366
|
for (const migration of pending) {
|
|
2794
3367
|
const result = await this.applyOneMigration(dbName, options.namespace, migration, options.signal);
|
|
2795
3368
|
if (!result.ok) return result;
|
|
@@ -2857,7 +3430,7 @@ var SQLService = class extends BaseService {
|
|
|
2857
3430
|
"Content-Type": "application/json"
|
|
2858
3431
|
},
|
|
2859
3432
|
body: JSON.stringify(body),
|
|
2860
|
-
signal
|
|
3433
|
+
signal
|
|
2861
3434
|
});
|
|
2862
3435
|
}
|
|
2863
3436
|
actionForSql(sql, fallback) {
|
|
@@ -2890,8 +3463,12 @@ var SQLService = class extends BaseService {
|
|
|
2890
3463
|
}))
|
|
2891
3464
|
);
|
|
2892
3465
|
}
|
|
2893
|
-
async handleErrorResponse(response, operation) {
|
|
3466
|
+
async handleErrorResponse(response, operation, dbName, requestedActions) {
|
|
2894
3467
|
const errorText = await response.text();
|
|
3468
|
+
const meta = responseErrorMeta(response.status, response.statusText, errorText);
|
|
3469
|
+
if (response.status === 402) {
|
|
3470
|
+
return err(storageRejectionError("sql", 402, meta, errorText));
|
|
3471
|
+
}
|
|
2895
3472
|
const errorBody = parseServiceErrorBody(errorText);
|
|
2896
3473
|
const errorCode = this.mapHttpStatusToErrorCode(
|
|
2897
3474
|
response.status,
|
|
@@ -2904,11 +3481,12 @@ var SQLService = class extends BaseService {
|
|
|
2904
3481
|
errorText,
|
|
2905
3482
|
errorBody
|
|
2906
3483
|
);
|
|
2907
|
-
const meta = responseErrorMeta(response.status, response.statusText, errorText);
|
|
2908
3484
|
if (response.status === 401) {
|
|
2909
|
-
const {
|
|
2910
|
-
if (action
|
|
2911
|
-
|
|
3485
|
+
const { action } = parseAuthError(errorText);
|
|
3486
|
+
if (action && requestedActions.includes(action)) {
|
|
3487
|
+
meta.requiredAction = action;
|
|
3488
|
+
meta.resource = `${this.context.session.spaceId}/sql/${dbName}`;
|
|
3489
|
+
}
|
|
2912
3490
|
}
|
|
2913
3491
|
return err(
|
|
2914
3492
|
serviceError(errorCode, message, "sql", { meta })
|
|
@@ -3005,6 +3583,9 @@ function serializeSqlValues(values) {
|
|
|
3005
3583
|
(value) => value instanceof Uint8Array ? Array.from(value) : value
|
|
3006
3584
|
);
|
|
3007
3585
|
}
|
|
3586
|
+
function isMissingTableError(message) {
|
|
3587
|
+
return /no such table/i.test(message);
|
|
3588
|
+
}
|
|
3008
3589
|
function migrationKey(namespace, id) {
|
|
3009
3590
|
return `${encodeURIComponent(namespace)}:${encodeURIComponent(id)}`;
|
|
3010
3591
|
}
|
|
@@ -3113,12 +3694,13 @@ var DuckDbService = class extends BaseService {
|
|
|
3113
3694
|
if (!this.requireAuth()) {
|
|
3114
3695
|
return err(authRequiredError("duckdb"));
|
|
3115
3696
|
}
|
|
3697
|
+
const request = this.createRequestSignal(options?.signal);
|
|
3116
3698
|
try {
|
|
3117
3699
|
const response = await this.invokeDuckDb(
|
|
3118
3700
|
dbName,
|
|
3119
3701
|
DuckDbAction.READ,
|
|
3120
3702
|
{ action: "query", sql, params: params ?? [] },
|
|
3121
|
-
|
|
3703
|
+
request.signal
|
|
3122
3704
|
);
|
|
3123
3705
|
if (!response.ok) {
|
|
3124
3706
|
return this.handleErrorResponse(response, "query");
|
|
@@ -3127,6 +3709,8 @@ var DuckDbService = class extends BaseService {
|
|
|
3127
3709
|
return ok(data);
|
|
3128
3710
|
} catch (error) {
|
|
3129
3711
|
return err(wrapError("duckdb", error));
|
|
3712
|
+
} finally {
|
|
3713
|
+
request.dispose();
|
|
3130
3714
|
}
|
|
3131
3715
|
});
|
|
3132
3716
|
}
|
|
@@ -3135,12 +3719,13 @@ var DuckDbService = class extends BaseService {
|
|
|
3135
3719
|
if (!this.requireAuth()) {
|
|
3136
3720
|
return err(authRequiredError("duckdb"));
|
|
3137
3721
|
}
|
|
3722
|
+
const request = this.createRequestSignal(options?.signal);
|
|
3138
3723
|
try {
|
|
3139
3724
|
const response = await this.invokeDuckDb(
|
|
3140
3725
|
dbName,
|
|
3141
3726
|
DuckDbAction.READ,
|
|
3142
3727
|
{ action: "query", sql, params: params ?? [] },
|
|
3143
|
-
|
|
3728
|
+
request.signal,
|
|
3144
3729
|
{ Accept: "application/vnd.apache.arrow.stream" }
|
|
3145
3730
|
);
|
|
3146
3731
|
if (!response.ok) {
|
|
@@ -3150,6 +3735,8 @@ var DuckDbService = class extends BaseService {
|
|
|
3150
3735
|
return ok(buffer);
|
|
3151
3736
|
} catch (error) {
|
|
3152
3737
|
return err(wrapError("duckdb", error));
|
|
3738
|
+
} finally {
|
|
3739
|
+
request.dispose();
|
|
3153
3740
|
}
|
|
3154
3741
|
});
|
|
3155
3742
|
}
|
|
@@ -3158,6 +3745,7 @@ var DuckDbService = class extends BaseService {
|
|
|
3158
3745
|
if (!this.requireAuth()) {
|
|
3159
3746
|
return err(authRequiredError("duckdb"));
|
|
3160
3747
|
}
|
|
3748
|
+
const request = this.createRequestSignal(options?.signal);
|
|
3161
3749
|
try {
|
|
3162
3750
|
const body = {
|
|
3163
3751
|
action: "execute",
|
|
@@ -3171,7 +3759,7 @@ var DuckDbService = class extends BaseService {
|
|
|
3171
3759
|
dbName,
|
|
3172
3760
|
DuckDbAction.WRITE,
|
|
3173
3761
|
body,
|
|
3174
|
-
|
|
3762
|
+
request.signal
|
|
3175
3763
|
);
|
|
3176
3764
|
if (!response.ok) {
|
|
3177
3765
|
return this.handleErrorResponse(response, "execute");
|
|
@@ -3180,6 +3768,8 @@ var DuckDbService = class extends BaseService {
|
|
|
3180
3768
|
return ok(data);
|
|
3181
3769
|
} catch (error) {
|
|
3182
3770
|
return err(wrapError("duckdb", error));
|
|
3771
|
+
} finally {
|
|
3772
|
+
request.dispose();
|
|
3183
3773
|
}
|
|
3184
3774
|
});
|
|
3185
3775
|
}
|
|
@@ -3188,6 +3778,7 @@ var DuckDbService = class extends BaseService {
|
|
|
3188
3778
|
if (!this.requireAuth()) {
|
|
3189
3779
|
return err(authRequiredError("duckdb"));
|
|
3190
3780
|
}
|
|
3781
|
+
const request = this.createRequestSignal(options?.signal);
|
|
3191
3782
|
try {
|
|
3192
3783
|
const body = {
|
|
3193
3784
|
action: "batch",
|
|
@@ -3200,7 +3791,7 @@ var DuckDbService = class extends BaseService {
|
|
|
3200
3791
|
dbName,
|
|
3201
3792
|
DuckDbAction.WRITE,
|
|
3202
3793
|
body,
|
|
3203
|
-
|
|
3794
|
+
request.signal
|
|
3204
3795
|
);
|
|
3205
3796
|
if (!response.ok) {
|
|
3206
3797
|
return this.handleErrorResponse(response, "batch");
|
|
@@ -3209,6 +3800,8 @@ var DuckDbService = class extends BaseService {
|
|
|
3209
3800
|
return ok(data);
|
|
3210
3801
|
} catch (error) {
|
|
3211
3802
|
return err(wrapError("duckdb", error));
|
|
3803
|
+
} finally {
|
|
3804
|
+
request.dispose();
|
|
3212
3805
|
}
|
|
3213
3806
|
});
|
|
3214
3807
|
}
|
|
@@ -3217,12 +3810,13 @@ var DuckDbService = class extends BaseService {
|
|
|
3217
3810
|
if (!this.requireAuth()) {
|
|
3218
3811
|
return err(authRequiredError("duckdb"));
|
|
3219
3812
|
}
|
|
3813
|
+
const request = this.createRequestSignal(options?.signal);
|
|
3220
3814
|
try {
|
|
3221
3815
|
const response = await this.invokeDuckDb(
|
|
3222
3816
|
dbName,
|
|
3223
3817
|
DuckDbAction.WRITE,
|
|
3224
3818
|
{ action: "executeStatement", name, params: params ?? [] },
|
|
3225
|
-
|
|
3819
|
+
request.signal
|
|
3226
3820
|
);
|
|
3227
3821
|
if (!response.ok) {
|
|
3228
3822
|
return this.handleErrorResponse(response, "executeStatement");
|
|
@@ -3231,6 +3825,8 @@ var DuckDbService = class extends BaseService {
|
|
|
3231
3825
|
return ok(data);
|
|
3232
3826
|
} catch (error) {
|
|
3233
3827
|
return err(wrapError("duckdb", error));
|
|
3828
|
+
} finally {
|
|
3829
|
+
request.dispose();
|
|
3234
3830
|
}
|
|
3235
3831
|
});
|
|
3236
3832
|
}
|
|
@@ -3239,12 +3835,13 @@ var DuckDbService = class extends BaseService {
|
|
|
3239
3835
|
if (!this.requireAuth()) {
|
|
3240
3836
|
return err(authRequiredError("duckdb"));
|
|
3241
3837
|
}
|
|
3838
|
+
const request = this.createRequestSignal(options?.signal);
|
|
3242
3839
|
try {
|
|
3243
3840
|
const response = await this.invokeDuckDb(
|
|
3244
3841
|
dbName,
|
|
3245
3842
|
DuckDbAction.READ,
|
|
3246
3843
|
{ action: "describe" },
|
|
3247
|
-
|
|
3844
|
+
request.signal
|
|
3248
3845
|
);
|
|
3249
3846
|
if (!response.ok) {
|
|
3250
3847
|
return this.handleErrorResponse(response, "describe");
|
|
@@ -3253,6 +3850,8 @@ var DuckDbService = class extends BaseService {
|
|
|
3253
3850
|
return ok(data);
|
|
3254
3851
|
} catch (error) {
|
|
3255
3852
|
return err(wrapError("duckdb", error));
|
|
3853
|
+
} finally {
|
|
3854
|
+
request.dispose();
|
|
3256
3855
|
}
|
|
3257
3856
|
});
|
|
3258
3857
|
}
|
|
@@ -3261,12 +3860,13 @@ var DuckDbService = class extends BaseService {
|
|
|
3261
3860
|
if (!this.requireAuth()) {
|
|
3262
3861
|
return err(authRequiredError("duckdb"));
|
|
3263
3862
|
}
|
|
3863
|
+
const request = this.createRequestSignal(options?.signal);
|
|
3264
3864
|
try {
|
|
3265
3865
|
const response = await this.invokeDuckDb(
|
|
3266
3866
|
dbName,
|
|
3267
3867
|
DuckDbAction.EXPORT,
|
|
3268
3868
|
{ action: "export" },
|
|
3269
|
-
|
|
3869
|
+
request.signal
|
|
3270
3870
|
);
|
|
3271
3871
|
if (!response.ok) {
|
|
3272
3872
|
return this.handleErrorResponse(response, "export");
|
|
@@ -3275,6 +3875,8 @@ var DuckDbService = class extends BaseService {
|
|
|
3275
3875
|
return ok(blob);
|
|
3276
3876
|
} catch (error) {
|
|
3277
3877
|
return err(wrapError("duckdb", error));
|
|
3878
|
+
} finally {
|
|
3879
|
+
request.dispose();
|
|
3278
3880
|
}
|
|
3279
3881
|
});
|
|
3280
3882
|
}
|
|
@@ -3283,6 +3885,7 @@ var DuckDbService = class extends BaseService {
|
|
|
3283
3885
|
if (!this.requireAuth()) {
|
|
3284
3886
|
return err(authRequiredError("duckdb"));
|
|
3285
3887
|
}
|
|
3888
|
+
const request = this.createRequestSignal(options?.signal);
|
|
3286
3889
|
try {
|
|
3287
3890
|
const session = this.context.session;
|
|
3288
3891
|
const headers = this.context.invoke(
|
|
@@ -3298,7 +3901,7 @@ var DuckDbService = class extends BaseService {
|
|
|
3298
3901
|
"Content-Type": "application/x-duckdb"
|
|
3299
3902
|
},
|
|
3300
3903
|
body: new Blob([data]),
|
|
3301
|
-
signal:
|
|
3904
|
+
signal: request.signal
|
|
3302
3905
|
});
|
|
3303
3906
|
if (!response.ok) {
|
|
3304
3907
|
return this.handleErrorResponse(response, "import");
|
|
@@ -3306,6 +3909,8 @@ var DuckDbService = class extends BaseService {
|
|
|
3306
3909
|
return ok(void 0);
|
|
3307
3910
|
} catch (error) {
|
|
3308
3911
|
return err(wrapError("duckdb", error));
|
|
3912
|
+
} finally {
|
|
3913
|
+
request.dispose();
|
|
3309
3914
|
}
|
|
3310
3915
|
});
|
|
3311
3916
|
}
|
|
@@ -3321,11 +3926,15 @@ var DuckDbService = class extends BaseService {
|
|
|
3321
3926
|
...extraHeaders
|
|
3322
3927
|
},
|
|
3323
3928
|
body: JSON.stringify(body),
|
|
3324
|
-
signal
|
|
3929
|
+
signal
|
|
3325
3930
|
});
|
|
3326
3931
|
}
|
|
3327
3932
|
async handleErrorResponse(response, operation) {
|
|
3328
3933
|
const errorText = await response.text();
|
|
3934
|
+
const meta = responseErrorMeta(response.status, response.statusText, errorText);
|
|
3935
|
+
if (response.status === 402) {
|
|
3936
|
+
return err(storageRejectionError("duckdb", 402, meta, errorText));
|
|
3937
|
+
}
|
|
3329
3938
|
const errorBody = parseServiceErrorBody(errorText);
|
|
3330
3939
|
const errorCode = this.mapHttpStatusToErrorCode(
|
|
3331
3940
|
response.status,
|
|
@@ -3338,7 +3947,6 @@ var DuckDbService = class extends BaseService {
|
|
|
3338
3947
|
errorText,
|
|
3339
3948
|
errorBody
|
|
3340
3949
|
);
|
|
3341
|
-
const meta = responseErrorMeta(response.status, response.statusText, errorText);
|
|
3342
3950
|
if (response.status === 401) {
|
|
3343
3951
|
const { resource, action } = parseAuthError(errorText);
|
|
3344
3952
|
if (action) meta.requiredAction = action;
|
|
@@ -3837,10 +4445,14 @@ async function responseError(service, message, response) {
|
|
|
3837
4445
|
}
|
|
3838
4446
|
} catch {
|
|
3839
4447
|
}
|
|
3840
|
-
|
|
4448
|
+
const error = wrapError(
|
|
3841
4449
|
service,
|
|
3842
4450
|
new Error(`${message}: ${response.status} ${detail}`)
|
|
3843
4451
|
);
|
|
4452
|
+
return {
|
|
4453
|
+
...error,
|
|
4454
|
+
meta: { ...error.meta, status: response.status, statusText: response.statusText }
|
|
4455
|
+
};
|
|
3844
4456
|
}
|
|
3845
4457
|
function isAbortError(error) {
|
|
3846
4458
|
return error instanceof DOMException && error.name === "AbortError";
|
|
@@ -4306,6 +4918,9 @@ function defaultVaultMessage(input) {
|
|
|
4306
4918
|
return input.message ?? `Public key not found for ${input.did}`;
|
|
4307
4919
|
case "STORAGE_ERROR":
|
|
4308
4920
|
return input.message ?? input.cause.message;
|
|
4921
|
+
case "STORAGE_QUOTA_EXCEEDED":
|
|
4922
|
+
case "STORAGE_LIMIT_REACHED":
|
|
4923
|
+
return input.message;
|
|
4309
4924
|
}
|
|
4310
4925
|
}
|
|
4311
4926
|
function vaultError(input) {
|
|
@@ -4316,6 +4931,15 @@ function vaultError(input) {
|
|
|
4316
4931
|
};
|
|
4317
4932
|
return { ok: false, error };
|
|
4318
4933
|
}
|
|
4934
|
+
function kvWriteError(context, error) {
|
|
4935
|
+
if (error.code === ErrorCodes.STORAGE_QUOTA_EXCEEDED || error.code === ErrorCodes.STORAGE_LIMIT_REACHED) {
|
|
4936
|
+
return vaultError({ code: error.code, message: error.message, meta: error.meta });
|
|
4937
|
+
}
|
|
4938
|
+
return vaultError({
|
|
4939
|
+
code: "STORAGE_ERROR",
|
|
4940
|
+
cause: new Error(`${context}: ${error.message}`)
|
|
4941
|
+
});
|
|
4942
|
+
}
|
|
4319
4943
|
var DataVaultService = class extends BaseService {
|
|
4320
4944
|
/**
|
|
4321
4945
|
* Create a new DataVaultService instance.
|
|
@@ -4598,12 +5222,7 @@ var DataVaultService = class extends BaseService {
|
|
|
4598
5222
|
JSON.stringify(envelopeResult.data)
|
|
4599
5223
|
);
|
|
4600
5224
|
if (!valuePutResult.ok) {
|
|
4601
|
-
return
|
|
4602
|
-
code: "STORAGE_ERROR",
|
|
4603
|
-
cause: new Error(
|
|
4604
|
-
`Failed to store encrypted value: ${valuePutResult.error.message}`
|
|
4605
|
-
)
|
|
4606
|
-
});
|
|
5225
|
+
return kvWriteError("Failed to store encrypted value", valuePutResult.error);
|
|
4607
5226
|
}
|
|
4608
5227
|
return { ok: true, data: void 0 };
|
|
4609
5228
|
} catch (error) {
|
|
@@ -4821,12 +5440,7 @@ var DataVaultService = class extends BaseService {
|
|
|
4821
5440
|
keyPayload
|
|
4822
5441
|
);
|
|
4823
5442
|
if (!keyPutResult.ok) {
|
|
4824
|
-
return
|
|
4825
|
-
code: "STORAGE_ERROR",
|
|
4826
|
-
cause: new Error(
|
|
4827
|
-
`Failed to store key blob: ${keyPutResult.error.message}`
|
|
4828
|
-
)
|
|
4829
|
-
});
|
|
5443
|
+
return kvWriteError("Failed to store key blob", keyPutResult.error);
|
|
4830
5444
|
}
|
|
4831
5445
|
const valuePayload = JSON.stringify({
|
|
4832
5446
|
data: base64Encode(encrypted),
|
|
@@ -4837,12 +5451,7 @@ var DataVaultService = class extends BaseService {
|
|
|
4837
5451
|
valuePayload
|
|
4838
5452
|
);
|
|
4839
5453
|
if (!valuePutResult.ok) {
|
|
4840
|
-
return
|
|
4841
|
-
code: "STORAGE_ERROR",
|
|
4842
|
-
cause: new Error(
|
|
4843
|
-
`Failed to store encrypted value: ${valuePutResult.error.message}`
|
|
4844
|
-
)
|
|
4845
|
-
});
|
|
5454
|
+
return kvWriteError("Failed to store encrypted value", valuePutResult.error);
|
|
4846
5455
|
}
|
|
4847
5456
|
return ok(void 0);
|
|
4848
5457
|
} catch (error) {
|
|
@@ -5173,12 +5782,7 @@ var DataVaultService = class extends BaseService {
|
|
|
5173
5782
|
grantPayload
|
|
5174
5783
|
);
|
|
5175
5784
|
if (!grantPutResult.ok) {
|
|
5176
|
-
return
|
|
5177
|
-
code: "STORAGE_ERROR",
|
|
5178
|
-
cause: new Error(
|
|
5179
|
-
`Failed to store grant: ${grantPutResult.error.message}`
|
|
5180
|
-
)
|
|
5181
|
-
});
|
|
5785
|
+
return kvWriteError("Failed to store grant", grantPutResult.error);
|
|
5182
5786
|
}
|
|
5183
5787
|
return ok(void 0);
|
|
5184
5788
|
} catch (error) {
|
|
@@ -5523,12 +6127,7 @@ var DataVaultService = class extends BaseService {
|
|
|
5523
6127
|
keyPayload
|
|
5524
6128
|
);
|
|
5525
6129
|
if (!keyPutResult.ok) {
|
|
5526
|
-
return
|
|
5527
|
-
code: "STORAGE_ERROR",
|
|
5528
|
-
cause: new Error(
|
|
5529
|
-
`Failed to store rotated key blob: ${keyPutResult.error.message}`
|
|
5530
|
-
)
|
|
5531
|
-
});
|
|
6130
|
+
return kvWriteError("Failed to store rotated key blob", keyPutResult.error);
|
|
5532
6131
|
}
|
|
5533
6132
|
const valuePayload = JSON.stringify({
|
|
5534
6133
|
data: base64Encode(encrypted),
|
|
@@ -5539,12 +6138,7 @@ var DataVaultService = class extends BaseService {
|
|
|
5539
6138
|
valuePayload
|
|
5540
6139
|
);
|
|
5541
6140
|
if (!valuePutResult.ok) {
|
|
5542
|
-
return
|
|
5543
|
-
code: "STORAGE_ERROR",
|
|
5544
|
-
cause: new Error(
|
|
5545
|
-
`Failed to store re-encrypted value: ${valuePutResult.error.message}`
|
|
5546
|
-
)
|
|
5547
|
-
});
|
|
6141
|
+
return kvWriteError("Failed to store re-encrypted value", valuePutResult.error);
|
|
5548
6142
|
}
|
|
5549
6143
|
for (const did of remainingGrantees) {
|
|
5550
6144
|
const grantResult = await this.reencrypt(key, did);
|
|
@@ -5753,6 +6347,7 @@ var SecretsService = class {
|
|
|
5753
6347
|
}
|
|
5754
6348
|
async listAll() {
|
|
5755
6349
|
const entries = /* @__PURE__ */ new Map();
|
|
6350
|
+
const seenCursors = /* @__PURE__ */ new Set();
|
|
5756
6351
|
let cursor;
|
|
5757
6352
|
do {
|
|
5758
6353
|
const result = await this.vault.listPage({
|
|
@@ -5775,6 +6370,14 @@ var SecretsService = class {
|
|
|
5775
6370
|
"Secret catalog listing was truncated without a continuation cursor."
|
|
5776
6371
|
);
|
|
5777
6372
|
}
|
|
6373
|
+
if (cursor !== void 0) {
|
|
6374
|
+
if (seenCursors.has(cursor)) {
|
|
6375
|
+
return invalidSecretInput(
|
|
6376
|
+
"Secret catalog listing repeated a continuation cursor; the node does not support paged KV lists."
|
|
6377
|
+
);
|
|
6378
|
+
}
|
|
6379
|
+
seenCursors.add(cursor);
|
|
6380
|
+
}
|
|
5778
6381
|
} while (cursor !== void 0);
|
|
5779
6382
|
return {
|
|
5780
6383
|
ok: true,
|
|
@@ -5895,103 +6498,6 @@ function canonicalHashHex(sha256, value) {
|
|
|
5895
6498
|
return hexEncode2(sha256(utf8Encode(canonical)));
|
|
5896
6499
|
}
|
|
5897
6500
|
|
|
5898
|
-
// src/encryption/networkId.ts
|
|
5899
|
-
var URN_PREFIX = "urn:tinycloud:encryption:";
|
|
5900
|
-
var NETWORK_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
5901
|
-
var PKH_EIP155_DID_RE = /^did:pkh:eip155:(\d+):(0x[a-fA-F0-9]{40})$/;
|
|
5902
|
-
var NetworkIdError = class extends Error {
|
|
5903
|
-
constructor(message) {
|
|
5904
|
-
super(message);
|
|
5905
|
-
this.name = "NetworkIdError";
|
|
5906
|
-
}
|
|
5907
|
-
};
|
|
5908
|
-
function parseNetworkId(networkId) {
|
|
5909
|
-
if (typeof networkId !== "string" || networkId.length === 0) {
|
|
5910
|
-
throw new NetworkIdError("networkId must be a non-empty string");
|
|
5911
|
-
}
|
|
5912
|
-
if (!networkId.startsWith(URN_PREFIX)) {
|
|
5913
|
-
throw new NetworkIdError(
|
|
5914
|
-
`networkId must start with ${URN_PREFIX} (got ${JSON.stringify(networkId)})`
|
|
5915
|
-
);
|
|
5916
|
-
}
|
|
5917
|
-
const body = networkId.slice(URN_PREFIX.length);
|
|
5918
|
-
const lastColon = body.lastIndexOf(":");
|
|
5919
|
-
if (lastColon <= 0 || lastColon === body.length - 1) {
|
|
5920
|
-
throw new NetworkIdError(
|
|
5921
|
-
`networkId missing ownerDid or name segment (got ${JSON.stringify(networkId)})`
|
|
5922
|
-
);
|
|
5923
|
-
}
|
|
5924
|
-
const ownerDid = body.slice(0, lastColon);
|
|
5925
|
-
const name = body.slice(lastColon + 1);
|
|
5926
|
-
if (!ownerDid.startsWith("did:")) {
|
|
5927
|
-
throw new NetworkIdError(
|
|
5928
|
-
`networkId ownerDid must be a DID (got ${JSON.stringify(ownerDid)})`
|
|
5929
|
-
);
|
|
5930
|
-
}
|
|
5931
|
-
const didParts = ownerDid.split(":");
|
|
5932
|
-
if (didParts.length < 3 || didParts.some((p) => p.length === 0)) {
|
|
5933
|
-
throw new NetworkIdError(
|
|
5934
|
-
`networkId ownerDid is not a well-formed DID (got ${JSON.stringify(ownerDid)})`
|
|
5935
|
-
);
|
|
5936
|
-
}
|
|
5937
|
-
if (!NETWORK_NAME_RE.test(name)) {
|
|
5938
|
-
throw new NetworkIdError(
|
|
5939
|
-
`networkId name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
|
|
5940
|
-
);
|
|
5941
|
-
}
|
|
5942
|
-
return { networkId, ownerDid, name };
|
|
5943
|
-
}
|
|
5944
|
-
function buildNetworkId(ownerDid, name) {
|
|
5945
|
-
if (typeof ownerDid !== "string" || !ownerDid.startsWith("did:")) {
|
|
5946
|
-
throw new NetworkIdError("ownerDid must be a DID");
|
|
5947
|
-
}
|
|
5948
|
-
if (typeof name !== "string" || !NETWORK_NAME_RE.test(name)) {
|
|
5949
|
-
throw new NetworkIdError(
|
|
5950
|
-
`network name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
|
|
5951
|
-
);
|
|
5952
|
-
}
|
|
5953
|
-
const networkId = `${URN_PREFIX}${ownerDid}:${name}`;
|
|
5954
|
-
parseNetworkId(networkId);
|
|
5955
|
-
return networkId;
|
|
5956
|
-
}
|
|
5957
|
-
function isNetworkId(networkId) {
|
|
5958
|
-
if (typeof networkId !== "string") {
|
|
5959
|
-
return false;
|
|
5960
|
-
}
|
|
5961
|
-
try {
|
|
5962
|
-
parseNetworkId(networkId);
|
|
5963
|
-
return true;
|
|
5964
|
-
} catch {
|
|
5965
|
-
return false;
|
|
5966
|
-
}
|
|
5967
|
-
}
|
|
5968
|
-
function parsePkhOwnerDid(ownerDid) {
|
|
5969
|
-
const match = ownerDid.match(PKH_EIP155_DID_RE);
|
|
5970
|
-
if (!match) return null;
|
|
5971
|
-
return {
|
|
5972
|
-
chainId: match[1],
|
|
5973
|
-
address: match[2].toLowerCase()
|
|
5974
|
-
};
|
|
5975
|
-
}
|
|
5976
|
-
function ownerDidMatches(a, b) {
|
|
5977
|
-
const aPkh = parsePkhOwnerDid(a);
|
|
5978
|
-
const bPkh = parsePkhOwnerDid(b);
|
|
5979
|
-
if (aPkh && bPkh) {
|
|
5980
|
-
return aPkh.chainId === bPkh.chainId && aPkh.address === bPkh.address;
|
|
5981
|
-
}
|
|
5982
|
-
return a === b;
|
|
5983
|
-
}
|
|
5984
|
-
function networkDiscoveryKey(name) {
|
|
5985
|
-
if (!NETWORK_NAME_RE.test(name)) {
|
|
5986
|
-
throw new NetworkIdError(
|
|
5987
|
-
`network name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
|
|
5988
|
-
);
|
|
5989
|
-
}
|
|
5990
|
-
return `.well-known/encryption/network/${name}`;
|
|
5991
|
-
}
|
|
5992
|
-
var ENCRYPTION_NETWORK_URN_PREFIX = URN_PREFIX;
|
|
5993
|
-
var NETWORK_NAME_PATTERN = NETWORK_NAME_RE;
|
|
5994
|
-
|
|
5995
6501
|
// src/encryption/types.ts
|
|
5996
6502
|
var DEFAULT_ENCRYPTION_ALG = "x25519-aes256gcm/v1";
|
|
5997
6503
|
var ENVELOPE_VERSION = 1;
|
|
@@ -6873,6 +7379,8 @@ export {
|
|
|
6873
7379
|
SQLAction,
|
|
6874
7380
|
SQLMigrations,
|
|
6875
7381
|
SQLService,
|
|
7382
|
+
STORAGE_FULL_MESSAGE,
|
|
7383
|
+
STORAGE_WRITE_TOO_LARGE_MESSAGE,
|
|
6876
7384
|
SecretsService,
|
|
6877
7385
|
ServiceContext,
|
|
6878
7386
|
ServiceErrorEventSchema,
|
|
@@ -6891,6 +7399,7 @@ export {
|
|
|
6891
7399
|
authExpiredError,
|
|
6892
7400
|
authRequiredError,
|
|
6893
7401
|
authUnauthorizedError,
|
|
7402
|
+
authorizationVerdictOf,
|
|
6894
7403
|
base64Decode2 as base64Decode,
|
|
6895
7404
|
base64Encode2 as base64Encode,
|
|
6896
7405
|
buildCanonicalDecryptRequest,
|
|
@@ -6924,22 +7433,26 @@ export {
|
|
|
6924
7433
|
hexEncode2 as hexEncode,
|
|
6925
7434
|
installTinyCloudDebugGlobals,
|
|
6926
7435
|
isNetworkId,
|
|
7436
|
+
isStorageFullError,
|
|
6927
7437
|
networkDiscoveryKey,
|
|
6928
7438
|
networkError,
|
|
6929
7439
|
notFoundError,
|
|
6930
7440
|
ok,
|
|
6931
7441
|
openWrappedKey,
|
|
6932
7442
|
parseAuthError,
|
|
7443
|
+
parseCapabilityResource,
|
|
6933
7444
|
parseNetworkId,
|
|
6934
7445
|
parsePermissionHint,
|
|
6935
7446
|
parsePermissionHintFromErrorText,
|
|
6936
7447
|
parseSecretCatalogKey,
|
|
7448
|
+
parseStorageQuotaBytes,
|
|
6937
7449
|
permissionDeniedError,
|
|
6938
7450
|
resolveSecretListPrefix,
|
|
6939
7451
|
resolveSecretPath,
|
|
6940
7452
|
serviceError,
|
|
6941
7453
|
storageLimitReachedError,
|
|
6942
7454
|
storageQuotaExceededError,
|
|
7455
|
+
storageRejectionError,
|
|
6943
7456
|
timeoutError,
|
|
6944
7457
|
tinyCloudDebugLogger,
|
|
6945
7458
|
utf8Decode,
|
|
@@ -6953,6 +7466,7 @@ export {
|
|
|
6953
7466
|
validateServiceResponseEvent,
|
|
6954
7467
|
validateServiceSession,
|
|
6955
7468
|
validateTelemetrySpanEvent,
|
|
7469
|
+
validatedCapabilityOf,
|
|
6956
7470
|
verifyDecryptResponse,
|
|
6957
7471
|
wrapError
|
|
6958
7472
|
};
|