@tinycloud/sdk-services 3.1.0-beta.1 → 3.1.0-beta.11
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 +739 -258
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +66 -42
- package/dist/index.d.ts +66 -42
- package/dist/index.js +714 -240
- 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 +568 -74
- package/dist/kv/index.cjs.map +1 -1
- package/dist/kv/index.d.cts +197 -15
- package/dist/kv/index.d.ts +197 -15
- package/dist/kv/index.js +568 -74
- 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/kv/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",
|
|
@@ -70,6 +78,55 @@ function serviceError(code, message, service, options) {
|
|
|
70
78
|
};
|
|
71
79
|
}
|
|
72
80
|
|
|
81
|
+
// src/errors.ts
|
|
82
|
+
import { CAPABILITY_REGISTRY } from "@tinycloud/bootstrap";
|
|
83
|
+
|
|
84
|
+
// src/encryption/networkId.ts
|
|
85
|
+
var URN_PREFIX = "urn:tinycloud:encryption:";
|
|
86
|
+
var NETWORK_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
87
|
+
var NetworkIdError = class extends Error {
|
|
88
|
+
constructor(message) {
|
|
89
|
+
super(message);
|
|
90
|
+
this.name = "NetworkIdError";
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
function parseNetworkId(networkId) {
|
|
94
|
+
if (typeof networkId !== "string" || networkId.length === 0) {
|
|
95
|
+
throw new NetworkIdError("networkId must be a non-empty string");
|
|
96
|
+
}
|
|
97
|
+
if (!networkId.startsWith(URN_PREFIX)) {
|
|
98
|
+
throw new NetworkIdError(
|
|
99
|
+
`networkId must start with ${URN_PREFIX} (got ${JSON.stringify(networkId)})`
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
const body = networkId.slice(URN_PREFIX.length);
|
|
103
|
+
const lastColon = body.lastIndexOf(":");
|
|
104
|
+
if (lastColon <= 0 || lastColon === body.length - 1) {
|
|
105
|
+
throw new NetworkIdError(
|
|
106
|
+
`networkId missing ownerDid or name segment (got ${JSON.stringify(networkId)})`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
const ownerDid = body.slice(0, lastColon);
|
|
110
|
+
const name = body.slice(lastColon + 1);
|
|
111
|
+
if (!ownerDid.startsWith("did:")) {
|
|
112
|
+
throw new NetworkIdError(
|
|
113
|
+
`networkId ownerDid must be a DID (got ${JSON.stringify(ownerDid)})`
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
const didParts = ownerDid.split(":");
|
|
117
|
+
if (didParts.length < 3 || didParts.some((p) => p.length === 0)) {
|
|
118
|
+
throw new NetworkIdError(
|
|
119
|
+
`networkId ownerDid is not a well-formed DID (got ${JSON.stringify(ownerDid)})`
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
if (!NETWORK_NAME_RE.test(name)) {
|
|
123
|
+
throw new NetworkIdError(
|
|
124
|
+
`networkId name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
return { networkId, ownerDid, name };
|
|
128
|
+
}
|
|
129
|
+
|
|
73
130
|
// src/errors.ts
|
|
74
131
|
function parsePermissionHint(value) {
|
|
75
132
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
@@ -114,11 +171,31 @@ function authRequiredError(service) {
|
|
|
114
171
|
service
|
|
115
172
|
};
|
|
116
173
|
}
|
|
117
|
-
|
|
174
|
+
var RequestTimeoutError = class extends Error {
|
|
175
|
+
constructor(timeoutMs) {
|
|
176
|
+
super(`Request timed out after ${timeoutMs}ms.`);
|
|
177
|
+
this.name = "TimeoutError";
|
|
178
|
+
this.timeoutMs = timeoutMs;
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
function isRequestTimeoutError(error) {
|
|
182
|
+
return error instanceof RequestTimeoutError || error instanceof Error && error.name === "TimeoutError" && typeof error.timeoutMs === "number";
|
|
183
|
+
}
|
|
184
|
+
function timeoutError(service, timeoutMs, cause) {
|
|
185
|
+
if (timeoutMs === void 0) {
|
|
186
|
+
return {
|
|
187
|
+
code: ErrorCodes.TIMEOUT,
|
|
188
|
+
message: "Request timed out.",
|
|
189
|
+
service,
|
|
190
|
+
...cause === void 0 ? {} : { cause }
|
|
191
|
+
};
|
|
192
|
+
}
|
|
118
193
|
return {
|
|
119
194
|
code: ErrorCodes.TIMEOUT,
|
|
120
|
-
message:
|
|
121
|
-
service
|
|
195
|
+
message: `Request timed out after ${timeoutMs}ms.`,
|
|
196
|
+
service,
|
|
197
|
+
...cause === void 0 ? {} : { cause },
|
|
198
|
+
meta: { timeoutMs }
|
|
122
199
|
};
|
|
123
200
|
}
|
|
124
201
|
function abortedError(service) {
|
|
@@ -138,6 +215,101 @@ function parseAuthError(responseText) {
|
|
|
138
215
|
function authUnauthorizedError(service, message, meta) {
|
|
139
216
|
return serviceError(ErrorCodes.AUTH_UNAUTHORIZED, message, service, { meta });
|
|
140
217
|
}
|
|
218
|
+
var grantableActions = new Set(
|
|
219
|
+
CAPABILITY_REGISTRY.map(({ urn }) => urn).filter((urn) => !urn.endsWith("/*"))
|
|
220
|
+
);
|
|
221
|
+
var capabilityServices = new Set(
|
|
222
|
+
CAPABILITY_REGISTRY.map(({ service }) => service.slice("tinycloud.".length))
|
|
223
|
+
);
|
|
224
|
+
function parseCapabilityResource(resource, service) {
|
|
225
|
+
if (resource.length === 0 || resource.length > 1024 || !capabilityServices.has(service) || /[\s\p{C}*#]/u.test(resource) || resource.includes("://")) return void 0;
|
|
226
|
+
const segments = resource.split("/");
|
|
227
|
+
if (!/^tinycloud:[A-Za-z0-9][A-Za-z0-9:._-]*$/.test(segments[0])) return void 0;
|
|
228
|
+
const serviceIndex = segments.findIndex(
|
|
229
|
+
(segment, index) => index > 0 && capabilityServices.has(segment)
|
|
230
|
+
);
|
|
231
|
+
if (serviceIndex < 1 || segments[serviceIndex] !== service) return void 0;
|
|
232
|
+
if (segments.slice(1, serviceIndex).some(
|
|
233
|
+
(segment) => segment.length === 0 || segment === "." || segment === ".."
|
|
234
|
+
)) return void 0;
|
|
235
|
+
const pathSegments = segments.slice(serviceIndex + 1);
|
|
236
|
+
if (pathSegments.some((segment) => segment === "." || segment === "..")) return void 0;
|
|
237
|
+
if (service !== "kv" && (pathSegments.length === 0 || pathSegments[pathSegments.length - 1] === "")) return void 0;
|
|
238
|
+
return {
|
|
239
|
+
space: segments.slice(0, serviceIndex).join("/"),
|
|
240
|
+
path: pathSegments.join("/")
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
function safeCapabilityResource(value, service, action) {
|
|
244
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 1024) return false;
|
|
245
|
+
if (service === "encryption" && value.startsWith("urn:tinycloud:encryption:")) {
|
|
246
|
+
if (!/^[A-Za-z0-9_][A-Za-z0-9_.:/-]*$/.test(value) || value.includes("//") || value.split("/").some((segment) => segment === "." || segment === "..")) return false;
|
|
247
|
+
try {
|
|
248
|
+
parseNetworkId(value);
|
|
249
|
+
return true;
|
|
250
|
+
} catch {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
const parsed = parseCapabilityResource(value, service);
|
|
255
|
+
return parsed !== void 0 && (!parsed.path.endsWith("/") || service === "kv" && action === "tinycloud.kv/list") && (parsed.path !== "" || service === "kv" && action === "tinycloud.kv/list");
|
|
256
|
+
}
|
|
257
|
+
function validatedCapabilityOf(error) {
|
|
258
|
+
const verdict = authorizationVerdictOf(error);
|
|
259
|
+
if (verdict !== "unauthenticated" && verdict !== "forbidden") return void 0;
|
|
260
|
+
const seen = /* @__PURE__ */ new Set();
|
|
261
|
+
let node = error;
|
|
262
|
+
for (let depth = 0; depth < MAX_CAUSE_DEPTH && typeof node === "object" && node !== null && !seen.has(node); depth += 1) {
|
|
263
|
+
seen.add(node);
|
|
264
|
+
const record = node;
|
|
265
|
+
const status = errorStatusOf(record);
|
|
266
|
+
const meta = record.meta;
|
|
267
|
+
if ((status === 401 || status === 403 || status === void 0 && record.code === ErrorCodes.AUTH_UNAUTHORIZED) && typeof meta === "object" && meta !== null && !Array.isArray(meta)) {
|
|
268
|
+
const metadata = meta;
|
|
269
|
+
const { resource, requiredAction } = metadata;
|
|
270
|
+
if (typeof requiredAction === "string" && grantableActions.has(requiredAction)) {
|
|
271
|
+
const actionService = requiredAction.slice("tinycloud.".length, requiredAction.indexOf("/"));
|
|
272
|
+
const explicitService = record.service === void 0 ? metadata.service : record.service;
|
|
273
|
+
if (safeCapabilityResource(resource, actionService, requiredAction) && (explicitService === void 0 || explicitService === actionService || explicitService === `tinycloud.${actionService}`)) {
|
|
274
|
+
return { resource, requiredAction };
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
node = record.cause;
|
|
279
|
+
}
|
|
280
|
+
return void 0;
|
|
281
|
+
}
|
|
282
|
+
var MAX_CAUSE_DEPTH = 8;
|
|
283
|
+
function errorStatusOf(node) {
|
|
284
|
+
const meta = node.meta;
|
|
285
|
+
const candidates = [
|
|
286
|
+
node.status,
|
|
287
|
+
node.statusCode,
|
|
288
|
+
typeof meta === "object" && meta !== null ? meta.status : void 0
|
|
289
|
+
];
|
|
290
|
+
for (const candidate of candidates) {
|
|
291
|
+
if (typeof candidate === "number" && Number.isInteger(candidate) && candidate >= 400 && candidate <= 599) {
|
|
292
|
+
return candidate;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return void 0;
|
|
296
|
+
}
|
|
297
|
+
function authorizationVerdictOf(error) {
|
|
298
|
+
const seen = /* @__PURE__ */ new Set();
|
|
299
|
+
let sawUnauthorizedCode = false;
|
|
300
|
+
let node = error;
|
|
301
|
+
for (let depth = 0; depth < MAX_CAUSE_DEPTH && typeof node === "object" && node !== null && !seen.has(node); depth += 1) {
|
|
302
|
+
seen.add(node);
|
|
303
|
+
const record = node;
|
|
304
|
+
const status = errorStatusOf(record);
|
|
305
|
+
if (status === 401) return "unauthenticated";
|
|
306
|
+
if (status === 403) return "forbidden";
|
|
307
|
+
if (status !== void 0) return "other";
|
|
308
|
+
if (record.code === ErrorCodes.AUTH_UNAUTHORIZED) sawUnauthorizedCode = true;
|
|
309
|
+
node = record.cause;
|
|
310
|
+
}
|
|
311
|
+
return sawUnauthorizedCode ? "forbidden" : void 0;
|
|
312
|
+
}
|
|
141
313
|
function storageQuotaExceededError(service, message, meta) {
|
|
142
314
|
return {
|
|
143
315
|
code: ErrorCodes.STORAGE_QUOTA_EXCEEDED,
|
|
@@ -154,8 +326,25 @@ function storageLimitReachedError(service, message, meta) {
|
|
|
154
326
|
meta
|
|
155
327
|
};
|
|
156
328
|
}
|
|
329
|
+
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.";
|
|
330
|
+
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.";
|
|
331
|
+
function parseStorageQuotaBytes(responseText) {
|
|
332
|
+
const match = responseText.match(/Used:\s*(\d+)\s*bytes,\s*Limit:\s*(\d+)\s*bytes/i);
|
|
333
|
+
if (!match) return void 0;
|
|
334
|
+
return {
|
|
335
|
+
usedBytes: parseInt(match[1], 10),
|
|
336
|
+
limitBytes: parseInt(match[2], 10)
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
function storageRejectionError(service, status, meta, responseText) {
|
|
340
|
+
const quotaMeta = { ...meta, ...parseStorageQuotaBytes(responseText) };
|
|
341
|
+
return status === 402 ? storageQuotaExceededError(service, STORAGE_FULL_MESSAGE, quotaMeta) : storageLimitReachedError(service, STORAGE_WRITE_TOO_LARGE_MESSAGE, quotaMeta);
|
|
342
|
+
}
|
|
157
343
|
function wrapError(service, error, defaultCode = ErrorCodes.NETWORK_ERROR) {
|
|
158
344
|
if (error instanceof Error) {
|
|
345
|
+
if (isRequestTimeoutError(error)) {
|
|
346
|
+
return timeoutError(service, error.timeoutMs, error);
|
|
347
|
+
}
|
|
159
348
|
if (error.name === "AbortError") {
|
|
160
349
|
return abortedError(service);
|
|
161
350
|
}
|
|
@@ -177,6 +366,7 @@ function wrapError(service, error, defaultCode = ErrorCodes.NETWORK_ERROR) {
|
|
|
177
366
|
}
|
|
178
367
|
|
|
179
368
|
// src/base/BaseService.ts
|
|
369
|
+
var MAX_TIMER_DELAY_MS = 2147483647;
|
|
180
370
|
var BaseService = class {
|
|
181
371
|
constructor() {
|
|
182
372
|
/**
|
|
@@ -357,6 +547,64 @@ var BaseService = class {
|
|
|
357
547
|
}
|
|
358
548
|
return controller.signal;
|
|
359
549
|
}
|
|
550
|
+
/**
|
|
551
|
+
* Create the abort signal for one request, bounded by the request timeout.
|
|
552
|
+
*
|
|
553
|
+
* The signal aborts when the service signs out, the context aborts, the
|
|
554
|
+
* caller's `signal` aborts, or the timeout elapses, whichever comes first.
|
|
555
|
+
* A timeout aborts with a {@link RequestTimeoutError} (`TimeoutError`), which
|
|
556
|
+
* `wrapError` maps to `ErrorCodes.TIMEOUT`; the other sources keep their own
|
|
557
|
+
* reason (normally `AbortError`, mapped to `ErrorCodes.ABORTED`).
|
|
558
|
+
*
|
|
559
|
+
* The timeout is `timeoutMs` when given, otherwise `config.timeout`. Only a
|
|
560
|
+
* positive, finite value (at most 2^31-1 ms) applies; `undefined`, `0`, and
|
|
561
|
+
* `Infinity` mean no timeout, so a per-call `0` opts out of a configured one.
|
|
562
|
+
*
|
|
563
|
+
* Callers must call `dispose()` once the response body has been consumed,
|
|
564
|
+
* which clears the timer and removes the listeners this adds to the
|
|
565
|
+
* long-lived service and context signals.
|
|
566
|
+
*
|
|
567
|
+
* @param signal - The caller's abort signal for this request
|
|
568
|
+
* @param timeoutMs - Per-request timeout override in milliseconds
|
|
569
|
+
*/
|
|
570
|
+
createRequestSignal(signal, timeoutMs) {
|
|
571
|
+
const controller = new AbortController();
|
|
572
|
+
const detachers = [];
|
|
573
|
+
let timer;
|
|
574
|
+
const dispose = () => {
|
|
575
|
+
if (timer !== void 0) {
|
|
576
|
+
clearTimeout(timer);
|
|
577
|
+
timer = void 0;
|
|
578
|
+
}
|
|
579
|
+
for (const detach of detachers.splice(0)) {
|
|
580
|
+
detach();
|
|
581
|
+
}
|
|
582
|
+
};
|
|
583
|
+
const abort = (reason) => {
|
|
584
|
+
dispose();
|
|
585
|
+
controller.abort(reason);
|
|
586
|
+
};
|
|
587
|
+
const requestSignal = { signal: controller.signal, dispose };
|
|
588
|
+
const parents = [
|
|
589
|
+
this.abortController.signal,
|
|
590
|
+
this.context?.abortSignal,
|
|
591
|
+
signal
|
|
592
|
+
].filter((parent) => parent !== void 0);
|
|
593
|
+
for (const parent of parents) {
|
|
594
|
+
if (parent.aborted) {
|
|
595
|
+
abort(parent.reason);
|
|
596
|
+
return requestSignal;
|
|
597
|
+
}
|
|
598
|
+
const onAbort = () => abort(parent.reason);
|
|
599
|
+
parent.addEventListener("abort", onAbort, { once: true });
|
|
600
|
+
detachers.push(() => parent.removeEventListener("abort", onAbort));
|
|
601
|
+
}
|
|
602
|
+
const timeout = timeoutMs ?? this._config.timeout;
|
|
603
|
+
if (typeof timeout === "number" && timeout > 0 && timeout <= MAX_TIMER_DELAY_MS) {
|
|
604
|
+
timer = setTimeout(() => abort(new RequestTimeoutError(timeout)), timeout);
|
|
605
|
+
}
|
|
606
|
+
return requestSignal;
|
|
607
|
+
}
|
|
360
608
|
/**
|
|
361
609
|
* Wrap an operation with error handling and telemetry.
|
|
362
610
|
*
|
|
@@ -465,6 +713,30 @@ var PrefixedKVService = class _PrefixedKVService {
|
|
|
465
713
|
removePrefix
|
|
466
714
|
});
|
|
467
715
|
}
|
|
716
|
+
/**
|
|
717
|
+
* Read the change feed for everything under this prefix. A view with an
|
|
718
|
+
* empty prefix (`""` or `"/"`) is refused: the node does not serve a
|
|
719
|
+
* whole-space feed.
|
|
720
|
+
*/
|
|
721
|
+
async changes(options) {
|
|
722
|
+
if (this._prefix.replace(/\/+$/, "") === "") {
|
|
723
|
+
return err(serviceError(
|
|
724
|
+
ErrorCodes.INVALID_INPUT,
|
|
725
|
+
"KV changes needs a non-empty prefix; this view has none",
|
|
726
|
+
"kv"
|
|
727
|
+
));
|
|
728
|
+
}
|
|
729
|
+
const syncPrefix = `${this._prefix}/`;
|
|
730
|
+
const response = await this._kv.changes({ ...options, prefix: syncPrefix });
|
|
731
|
+
if (!response.ok) return response;
|
|
732
|
+
return ok({
|
|
733
|
+
...response.data,
|
|
734
|
+
changes: response.data.changes.map((change) => ({
|
|
735
|
+
...change,
|
|
736
|
+
key: change.key.startsWith(syncPrefix) ? change.key.slice(syncPrefix.length) : change.key
|
|
737
|
+
}))
|
|
738
|
+
});
|
|
739
|
+
}
|
|
468
740
|
/**
|
|
469
741
|
* Delete a key.
|
|
470
742
|
*/
|
|
@@ -517,11 +789,24 @@ var KVAction = {
|
|
|
517
789
|
PUT: "tinycloud.kv/put",
|
|
518
790
|
LIST: "tinycloud.kv/list",
|
|
519
791
|
DELETE: "tinycloud.kv/del",
|
|
520
|
-
HEAD: "tinycloud.kv/metadata"
|
|
792
|
+
HEAD: "tinycloud.kv/metadata",
|
|
793
|
+
/** The change feed; never implied by `*` or `tinycloud.kv/*`. */
|
|
794
|
+
SYNC: "tinycloud.kv/sync",
|
|
795
|
+
/** Retention attestation; presented through `retentionGrant`, never invoked or implied. */
|
|
796
|
+
RETAIN: "tinycloud.kv/retain"
|
|
521
797
|
};
|
|
522
798
|
|
|
523
799
|
// src/kv/KVService.ts
|
|
524
800
|
var MAX_KV_BATCH_READ_ITEMS = 100;
|
|
801
|
+
function isJsonObject(value) {
|
|
802
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
803
|
+
}
|
|
804
|
+
var REVOKED_BODY = /^(?:Invalid invocation: )?delegation-(?:(revoked): [A-Za-z0-9]+|(ancestor-revoked): ancestor=[A-Za-z0-9]+ invoked=[A-Za-z0-9]+)$/;
|
|
805
|
+
function revocationCodeOf(body) {
|
|
806
|
+
const match = REVOKED_BODY.exec(body.trim());
|
|
807
|
+
if (match === null) return void 0;
|
|
808
|
+
return match[1] !== void 0 ? ErrorCodes.AUTH_DELEGATION_REVOKED : ErrorCodes.AUTH_DELEGATION_ANCESTOR_REVOKED;
|
|
809
|
+
}
|
|
525
810
|
function encodeKvBatchPartName(path) {
|
|
526
811
|
return encodeURIComponent(path).replace(
|
|
527
812
|
/[!'()*]/g,
|
|
@@ -544,47 +829,21 @@ var KVService = class extends BaseService {
|
|
|
544
829
|
get config() {
|
|
545
830
|
return this._config;
|
|
546
831
|
}
|
|
547
|
-
// Parses "Used: X bytes, Limit: Y bytes" from tinycloud-node error responses
|
|
548
|
-
parseQuotaInfo(errorText) {
|
|
549
|
-
const match = errorText.match(
|
|
550
|
-
/Used:\s*(\d+)\s*bytes,\s*Limit:\s*(\d+)\s*bytes/i
|
|
551
|
-
);
|
|
552
|
-
if (match) {
|
|
553
|
-
return {
|
|
554
|
-
usedBytes: parseInt(match[1], 10),
|
|
555
|
-
limitBytes: parseInt(match[2], 10)
|
|
556
|
-
};
|
|
557
|
-
}
|
|
558
|
-
return void 0;
|
|
559
|
-
}
|
|
560
832
|
handleQuotaErrorResponse(response, errorText, key) {
|
|
561
|
-
if (response.status
|
|
562
|
-
|
|
563
|
-
return err(
|
|
564
|
-
storageQuotaExceededError(
|
|
565
|
-
"kv",
|
|
566
|
-
`Storage quota exceeded for key ${JSON.stringify(key)}: ${errorText}`,
|
|
567
|
-
{
|
|
568
|
-
status: response.status,
|
|
569
|
-
...quotaInfo ? { usedBytes: quotaInfo.usedBytes, limitBytes: quotaInfo.limitBytes } : {}
|
|
570
|
-
}
|
|
571
|
-
)
|
|
572
|
-
);
|
|
833
|
+
if (response.status !== 402 && response.status !== 413) {
|
|
834
|
+
return void 0;
|
|
573
835
|
}
|
|
574
|
-
if (response.status === 413) {
|
|
575
|
-
|
|
576
|
-
return err(
|
|
577
|
-
storageLimitReachedError(
|
|
578
|
-
"kv",
|
|
579
|
-
`Storage limit reached for key ${JSON.stringify(key)}: ${errorText}`,
|
|
580
|
-
{
|
|
581
|
-
status: response.status,
|
|
582
|
-
...quotaInfo ? { usedBytes: quotaInfo.usedBytes, limitBytes: quotaInfo.limitBytes } : {}
|
|
583
|
-
}
|
|
584
|
-
)
|
|
585
|
-
);
|
|
836
|
+
if (response.status === 413 && !parseStorageQuotaBytes(errorText)) {
|
|
837
|
+
return void 0;
|
|
586
838
|
}
|
|
587
|
-
return
|
|
839
|
+
return err(
|
|
840
|
+
storageRejectionError(
|
|
841
|
+
"kv",
|
|
842
|
+
response.status,
|
|
843
|
+
{ status: response.status, statusText: response.statusText, key },
|
|
844
|
+
errorText
|
|
845
|
+
)
|
|
846
|
+
);
|
|
588
847
|
}
|
|
589
848
|
/**
|
|
590
849
|
* Classify a KV 404 by reading the response body once.
|
|
@@ -643,7 +902,7 @@ var KVService = class extends BaseService {
|
|
|
643
902
|
* @param path - Resource path
|
|
644
903
|
* @param action - KV action
|
|
645
904
|
* @param body - Optional request body
|
|
646
|
-
* @param signal -
|
|
905
|
+
* @param signal - Request signal from createRequestSignal()
|
|
647
906
|
* @returns Fetch response
|
|
648
907
|
*/
|
|
649
908
|
async invokeOperation(path, action, body, signal, extraHeaders) {
|
|
@@ -659,7 +918,7 @@ var KVService = class extends BaseService {
|
|
|
659
918
|
method: "POST",
|
|
660
919
|
headers: requestHeaders,
|
|
661
920
|
body,
|
|
662
|
-
signal
|
|
921
|
+
signal
|
|
663
922
|
});
|
|
664
923
|
}
|
|
665
924
|
/**
|
|
@@ -836,6 +1095,7 @@ var KVService = class extends BaseService {
|
|
|
836
1095
|
"kv"
|
|
837
1096
|
));
|
|
838
1097
|
}
|
|
1098
|
+
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
839
1099
|
try {
|
|
840
1100
|
const session = this.context.session;
|
|
841
1101
|
const invocationHeaders = this.context.invokeAny(
|
|
@@ -857,15 +1117,17 @@ var KVService = class extends BaseService {
|
|
|
857
1117
|
const response = await this.context.fetch(`${this.host}/invoke`, {
|
|
858
1118
|
method: "POST",
|
|
859
1119
|
headers,
|
|
860
|
-
signal:
|
|
1120
|
+
signal: request.signal
|
|
861
1121
|
});
|
|
862
1122
|
if (!response.ok) {
|
|
863
|
-
const errorText = await response.text();
|
|
1123
|
+
const errorText = response.status === 401 || response.status === 403 ? await this.readAuthorizationText(response) : await response.text();
|
|
864
1124
|
if (response.status === 401 || response.status === 403) {
|
|
865
1125
|
return this.authorizationFailure(
|
|
866
1126
|
`Failed to batch read ${keys.length} key(s)`,
|
|
867
1127
|
response,
|
|
868
|
-
errorText
|
|
1128
|
+
errorText,
|
|
1129
|
+
paths,
|
|
1130
|
+
action
|
|
869
1131
|
);
|
|
870
1132
|
}
|
|
871
1133
|
if (response.status === 413) {
|
|
@@ -920,6 +1182,8 @@ var KVService = class extends BaseService {
|
|
|
920
1182
|
return ok({ results, count: results.length });
|
|
921
1183
|
} catch (error) {
|
|
922
1184
|
return err(wrapError("kv", error));
|
|
1185
|
+
} finally {
|
|
1186
|
+
request.dispose();
|
|
923
1187
|
}
|
|
924
1188
|
}
|
|
925
1189
|
/**
|
|
@@ -955,7 +1219,7 @@ var KVService = class extends BaseService {
|
|
|
955
1219
|
return text;
|
|
956
1220
|
}
|
|
957
1221
|
}
|
|
958
|
-
async createSignedReadUrlError(response, key) {
|
|
1222
|
+
async createSignedReadUrlError(response, key, path) {
|
|
959
1223
|
let errorText = response.statusText;
|
|
960
1224
|
try {
|
|
961
1225
|
const text = await response.text();
|
|
@@ -968,7 +1232,9 @@ var KVService = class extends BaseService {
|
|
|
968
1232
|
return this.authorizationFailure(
|
|
969
1233
|
`Failed to create signed read URL for key ${JSON.stringify(key)}`,
|
|
970
1234
|
response,
|
|
971
|
-
errorText
|
|
1235
|
+
errorText,
|
|
1236
|
+
[path],
|
|
1237
|
+
KVAction.GET
|
|
972
1238
|
);
|
|
973
1239
|
}
|
|
974
1240
|
const code = response.status === 400 ? ErrorCodes.INVALID_INPUT : ErrorCodes.NETWORK_ERROR;
|
|
@@ -981,21 +1247,42 @@ var KVService = class extends BaseService {
|
|
|
981
1247
|
)
|
|
982
1248
|
);
|
|
983
1249
|
}
|
|
1250
|
+
/** A known 401/403 keeps its typed status even if reading its body fails. */
|
|
1251
|
+
async readAuthorizationText(response) {
|
|
1252
|
+
try {
|
|
1253
|
+
return await response.text();
|
|
1254
|
+
} catch {
|
|
1255
|
+
return "";
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
984
1258
|
/**
|
|
985
1259
|
* AUTH_UNAUTHORIZED for a 401/403 response. The message keeps the HTTP
|
|
986
1260
|
* status and the server text (`<context>: <status> - <text>`, the same
|
|
987
1261
|
* shape as every other KV failure) so callers that rethrow only the message
|
|
988
1262
|
* still see the status; `meta.status` carries it for typed callers.
|
|
989
|
-
*
|
|
1263
|
+
* Server hints must match the requested path/action (whether the node sends
|
|
1264
|
+
* a relative path or full resource); metadata uses only the request-derived
|
|
1265
|
+
* canonical resource under the authenticated session's space.
|
|
990
1266
|
*/
|
|
991
|
-
authorizationFailure(context, response, errorText, extraMeta = {}) {
|
|
1267
|
+
authorizationFailure(context, response, errorText, paths, expectedAction, extraMeta = {}) {
|
|
992
1268
|
const { resource, action } = parseAuthError(errorText);
|
|
1269
|
+
const requestedPath = paths.find(
|
|
1270
|
+
(path) => resource === path || resource === `${this.context.session.spaceId}/kv/${path}`
|
|
1271
|
+
);
|
|
1272
|
+
const capability = requestedPath !== void 0 && action === expectedAction ? validatedCapabilityOf({
|
|
1273
|
+
service: "kv",
|
|
1274
|
+
code: ErrorCodes.AUTH_UNAUTHORIZED,
|
|
1275
|
+
meta: {
|
|
1276
|
+
status: response.status,
|
|
1277
|
+
resource: `${this.context.session.spaceId}/kv/${requestedPath}`,
|
|
1278
|
+
requiredAction: action
|
|
1279
|
+
}
|
|
1280
|
+
}) : void 0;
|
|
993
1281
|
const detail = errorText.trim().length > 0 ? errorText : response.statusText || "authorization failed";
|
|
994
1282
|
return err(authUnauthorizedError("kv", `${context}: ${response.status} - ${detail}`, {
|
|
995
1283
|
status: response.status,
|
|
996
|
-
...
|
|
997
|
-
...
|
|
998
|
-
...extraMeta
|
|
1284
|
+
...extraMeta,
|
|
1285
|
+
...capability
|
|
999
1286
|
}));
|
|
1000
1287
|
}
|
|
1001
1288
|
normalizeSignedReadUrlResponse(data) {
|
|
@@ -1029,23 +1316,35 @@ var KVService = class extends BaseService {
|
|
|
1029
1316
|
));
|
|
1030
1317
|
}
|
|
1031
1318
|
const path = this.getFullPath(key, options?.prefix);
|
|
1319
|
+
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
1032
1320
|
try {
|
|
1033
1321
|
const response = await this.invokeOperation(
|
|
1034
1322
|
path,
|
|
1035
1323
|
KVAction.GET,
|
|
1036
1324
|
void 0,
|
|
1037
|
-
|
|
1325
|
+
request.signal,
|
|
1038
1326
|
options?.maxResponseBytes === void 0 ? void 0 : { "x-tinycloud-max-response-bytes": String(options.maxResponseBytes) }
|
|
1039
1327
|
);
|
|
1040
1328
|
if (!response.ok) {
|
|
1041
1329
|
if (response.status === 401 || response.status === 403) {
|
|
1042
|
-
const errorText2 = await
|
|
1330
|
+
const errorText2 = await this.readAuthorizationText(response);
|
|
1043
1331
|
const permissionHint = parsePermissionHintFromErrorText(errorText2);
|
|
1332
|
+
const structuredCapability = permissionHint?.service === "tinycloud.kv" && permissionHint.space === this.context.session.spaceId && permissionHint.path === path ? validatedCapabilityOf({
|
|
1333
|
+
service: "kv",
|
|
1334
|
+
code: ErrorCodes.AUTH_UNAUTHORIZED,
|
|
1335
|
+
meta: {
|
|
1336
|
+
status: response.status,
|
|
1337
|
+
resource: `${this.context.session.spaceId}/kv/${path}`,
|
|
1338
|
+
requiredAction: KVAction.GET
|
|
1339
|
+
}
|
|
1340
|
+
}) : void 0;
|
|
1044
1341
|
return this.authorizationFailure(
|
|
1045
1342
|
`Failed to get key ${JSON.stringify(key)}`,
|
|
1046
1343
|
response,
|
|
1047
1344
|
errorText2,
|
|
1048
|
-
|
|
1345
|
+
[path],
|
|
1346
|
+
KVAction.GET,
|
|
1347
|
+
structuredCapability ? { permissionHint, ...structuredCapability } : {}
|
|
1049
1348
|
);
|
|
1050
1349
|
}
|
|
1051
1350
|
if (response.status === 404) {
|
|
@@ -1080,6 +1379,8 @@ var KVService = class extends BaseService {
|
|
|
1080
1379
|
});
|
|
1081
1380
|
} catch (error) {
|
|
1082
1381
|
return err(wrapError("kv", error));
|
|
1382
|
+
} finally {
|
|
1383
|
+
request.dispose();
|
|
1083
1384
|
}
|
|
1084
1385
|
});
|
|
1085
1386
|
}
|
|
@@ -1107,12 +1408,13 @@ var KVService = class extends BaseService {
|
|
|
1107
1408
|
}
|
|
1108
1409
|
const path = this.getFullPath(key, options?.prefix);
|
|
1109
1410
|
const body = this.serializePutValue(value, options?.contentType);
|
|
1411
|
+
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
1110
1412
|
try {
|
|
1111
1413
|
const response = await this.invokeOperation(
|
|
1112
1414
|
path,
|
|
1113
1415
|
KVAction.PUT,
|
|
1114
1416
|
body,
|
|
1115
|
-
|
|
1417
|
+
request.signal,
|
|
1116
1418
|
{
|
|
1117
1419
|
...options?.ifMatch === void 0 ? {} : { "if-match": options.ifMatch },
|
|
1118
1420
|
...options?.ifNoneMatch === void 0 ? {} : { "if-none-match": options.ifNoneMatch }
|
|
@@ -1122,7 +1424,9 @@ var KVService = class extends BaseService {
|
|
|
1122
1424
|
return this.authorizationFailure(
|
|
1123
1425
|
`Failed to put key ${JSON.stringify(key)}`,
|
|
1124
1426
|
response,
|
|
1125
|
-
await
|
|
1427
|
+
await this.readAuthorizationText(response),
|
|
1428
|
+
[path],
|
|
1429
|
+
KVAction.PUT
|
|
1126
1430
|
);
|
|
1127
1431
|
}
|
|
1128
1432
|
if (!response.ok) {
|
|
@@ -1166,6 +1470,8 @@ var KVService = class extends BaseService {
|
|
|
1166
1470
|
});
|
|
1167
1471
|
} catch (error) {
|
|
1168
1472
|
return err(wrapError("kv", error));
|
|
1473
|
+
} finally {
|
|
1474
|
+
request.dispose();
|
|
1169
1475
|
}
|
|
1170
1476
|
});
|
|
1171
1477
|
}
|
|
@@ -1205,6 +1511,7 @@ var KVService = class extends BaseService {
|
|
|
1205
1511
|
seen.add(path);
|
|
1206
1512
|
}
|
|
1207
1513
|
let requestMayHaveDispatched = false;
|
|
1514
|
+
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
1208
1515
|
try {
|
|
1209
1516
|
const body = new FormData();
|
|
1210
1517
|
for (let index = 0; index < items.length; index++) {
|
|
@@ -1227,7 +1534,7 @@ var KVService = class extends BaseService {
|
|
|
1227
1534
|
method: "POST",
|
|
1228
1535
|
headers,
|
|
1229
1536
|
body,
|
|
1230
|
-
signal:
|
|
1537
|
+
signal: request.signal
|
|
1231
1538
|
};
|
|
1232
1539
|
const fetchFn = this.context.fetch;
|
|
1233
1540
|
requestMayHaveDispatched = true;
|
|
@@ -1251,7 +1558,9 @@ var KVService = class extends BaseService {
|
|
|
1251
1558
|
return this.authorizationFailure(
|
|
1252
1559
|
`Failed to batch put ${items.length} key(s)`,
|
|
1253
1560
|
response,
|
|
1254
|
-
errorText
|
|
1561
|
+
errorText,
|
|
1562
|
+
paths,
|
|
1563
|
+
KVAction.PUT
|
|
1255
1564
|
);
|
|
1256
1565
|
}
|
|
1257
1566
|
const quotaError = this.handleQuotaErrorResponse(
|
|
@@ -1276,14 +1585,16 @@ var KVService = class extends BaseService {
|
|
|
1276
1585
|
rawBody = await response.json();
|
|
1277
1586
|
} catch (jsonError) {
|
|
1278
1587
|
const cause = jsonError instanceof Error ? jsonError : new Error(String(jsonError));
|
|
1588
|
+
const timedOut = isRequestTimeoutError(cause);
|
|
1279
1589
|
return err(
|
|
1280
1590
|
serviceError(
|
|
1281
|
-
ErrorCodes.NETWORK_ERROR,
|
|
1282
|
-
`KV batchPut response was not valid JSON: ${cause.message}`,
|
|
1591
|
+
timedOut ? ErrorCodes.TIMEOUT : ErrorCodes.NETWORK_ERROR,
|
|
1592
|
+
timedOut ? `KV batchPut response body did not arrive in time: ${cause.message}` : `KV batchPut response was not valid JSON: ${cause.message}`,
|
|
1283
1593
|
"kv",
|
|
1284
1594
|
{
|
|
1285
1595
|
cause,
|
|
1286
1596
|
meta: {
|
|
1597
|
+
...timedOut ? { timeoutMs: cause.timeoutMs } : {},
|
|
1287
1598
|
requestMayHaveDispatched: true,
|
|
1288
1599
|
responseReceived: true,
|
|
1289
1600
|
status: response.status,
|
|
@@ -1338,6 +1649,8 @@ var KVService = class extends BaseService {
|
|
|
1338
1649
|
...wrapped,
|
|
1339
1650
|
meta: { ...wrapped.meta, requestMayHaveDispatched }
|
|
1340
1651
|
});
|
|
1652
|
+
} finally {
|
|
1653
|
+
request.dispose();
|
|
1341
1654
|
}
|
|
1342
1655
|
});
|
|
1343
1656
|
}
|
|
@@ -1360,23 +1673,26 @@ var KVService = class extends BaseService {
|
|
|
1360
1673
|
if (options?.path) {
|
|
1361
1674
|
listPath = listPath ? `${listPath}/${options.path}` : options.path;
|
|
1362
1675
|
}
|
|
1676
|
+
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
1363
1677
|
try {
|
|
1364
1678
|
const response = await this.invokeOperation(
|
|
1365
1679
|
listPath,
|
|
1366
1680
|
KVAction.LIST,
|
|
1367
1681
|
void 0,
|
|
1368
|
-
|
|
1682
|
+
request.signal,
|
|
1369
1683
|
options?.limit === void 0 && options?.cursor === void 0 ? void 0 : {
|
|
1370
1684
|
...options?.limit === void 0 ? {} : { "x-tinycloud-limit": String(options.limit) },
|
|
1371
1685
|
...options?.cursor === void 0 ? {} : { "x-tinycloud-cursor": options.cursor }
|
|
1372
1686
|
}
|
|
1373
1687
|
);
|
|
1374
1688
|
if (!response.ok) {
|
|
1375
|
-
if (response.status === 401) {
|
|
1689
|
+
if (response.status === 401 || response.status === 403) {
|
|
1376
1690
|
return this.authorizationFailure(
|
|
1377
1691
|
"Failed to list keys",
|
|
1378
1692
|
response,
|
|
1379
|
-
await
|
|
1693
|
+
await this.readAuthorizationText(response),
|
|
1694
|
+
[listPath],
|
|
1695
|
+
KVAction.LIST
|
|
1380
1696
|
);
|
|
1381
1697
|
}
|
|
1382
1698
|
const errorText = await response.text();
|
|
@@ -1404,9 +1720,174 @@ var KVService = class extends BaseService {
|
|
|
1404
1720
|
});
|
|
1405
1721
|
} catch (error) {
|
|
1406
1722
|
return err(wrapError("kv", error));
|
|
1723
|
+
} finally {
|
|
1724
|
+
request.dispose();
|
|
1407
1725
|
}
|
|
1408
1726
|
});
|
|
1409
1727
|
}
|
|
1728
|
+
/**
|
|
1729
|
+
* Read one page of the `tinycloud.kv/sync` change feed for a prefix.
|
|
1730
|
+
*/
|
|
1731
|
+
async changes(options) {
|
|
1732
|
+
return this.withTelemetry("changes", options?.prefix, async () => {
|
|
1733
|
+
if (!this.requireAuth()) {
|
|
1734
|
+
return err(authRequiredError("kv"));
|
|
1735
|
+
}
|
|
1736
|
+
if (typeof options?.prefix !== "string" || options.prefix.length === 0) {
|
|
1737
|
+
return err(serviceError(
|
|
1738
|
+
ErrorCodes.INVALID_INPUT,
|
|
1739
|
+
"KV changes prefix must be a non-empty string",
|
|
1740
|
+
"kv"
|
|
1741
|
+
));
|
|
1742
|
+
}
|
|
1743
|
+
if (options.limit !== void 0 && (!Number.isInteger(options.limit) || options.limit < 1 || options.limit > 1e3)) {
|
|
1744
|
+
return err(serviceError(
|
|
1745
|
+
ErrorCodes.INVALID_INPUT,
|
|
1746
|
+
"KV changes limit must be an integer from 1 through 1000",
|
|
1747
|
+
"kv"
|
|
1748
|
+
));
|
|
1749
|
+
}
|
|
1750
|
+
const headers = {};
|
|
1751
|
+
if (options.limit !== void 0) headers["x-tinycloud-limit"] = String(options.limit);
|
|
1752
|
+
if (options.cursor !== void 0) headers["x-tinycloud-cursor"] = options.cursor;
|
|
1753
|
+
if (options.retentionGrant !== void 0) {
|
|
1754
|
+
headers["x-tinycloud-retention-grant"] = options.retentionGrant;
|
|
1755
|
+
}
|
|
1756
|
+
const request = this.createRequestSignal(options.signal, options.timeout);
|
|
1757
|
+
try {
|
|
1758
|
+
const response = await this.invokeOperation(
|
|
1759
|
+
options.prefix,
|
|
1760
|
+
KVAction.SYNC,
|
|
1761
|
+
void 0,
|
|
1762
|
+
request.signal,
|
|
1763
|
+
headers
|
|
1764
|
+
);
|
|
1765
|
+
if (!response.ok) {
|
|
1766
|
+
return await this.changesFailure(response, options.prefix, request.signal);
|
|
1767
|
+
}
|
|
1768
|
+
const text = await response.text();
|
|
1769
|
+
let body;
|
|
1770
|
+
try {
|
|
1771
|
+
body = JSON.parse(text);
|
|
1772
|
+
} catch {
|
|
1773
|
+
body = void 0;
|
|
1774
|
+
}
|
|
1775
|
+
const page = this.normalizeChangesResponse(body);
|
|
1776
|
+
if (!page) {
|
|
1777
|
+
return err(serviceError(
|
|
1778
|
+
ErrorCodes.NETWORK_ERROR,
|
|
1779
|
+
"KV changes response was not a tinycloud.kv/sync page",
|
|
1780
|
+
"kv",
|
|
1781
|
+
{ meta: { status: response.status, statusText: response.statusText } }
|
|
1782
|
+
));
|
|
1783
|
+
}
|
|
1784
|
+
return ok(page);
|
|
1785
|
+
} catch (error) {
|
|
1786
|
+
return err(wrapError("kv", error));
|
|
1787
|
+
} finally {
|
|
1788
|
+
request.dispose();
|
|
1789
|
+
}
|
|
1790
|
+
});
|
|
1791
|
+
}
|
|
1792
|
+
/**
|
|
1793
|
+
* Map a failed `kv/sync` response. 410 and the retention 403 carry
|
|
1794
|
+
* `{"error":{"code","reason"}}`; a revoked grant is a 401 whose whole body
|
|
1795
|
+
* is the node's revocation error (see REVOKED_BODY). A body read that
|
|
1796
|
+
* fails because the request was cancelled or timed out rethrows, so the
|
|
1797
|
+
* caller sees `ABORTED`/`TIMEOUT` rather than a mapped status.
|
|
1798
|
+
*/
|
|
1799
|
+
async changesFailure(response, prefix, signal) {
|
|
1800
|
+
if (response.status === 404) {
|
|
1801
|
+
return this.classifyNotFound(response, prefix);
|
|
1802
|
+
}
|
|
1803
|
+
let errorText;
|
|
1804
|
+
try {
|
|
1805
|
+
errorText = await response.text();
|
|
1806
|
+
} catch (error) {
|
|
1807
|
+
if (signal.aborted) throw error;
|
|
1808
|
+
errorText = "";
|
|
1809
|
+
}
|
|
1810
|
+
const meta = { status: response.status, statusText: response.statusText };
|
|
1811
|
+
let reason;
|
|
1812
|
+
let bodyCode;
|
|
1813
|
+
try {
|
|
1814
|
+
const body = JSON.parse(errorText);
|
|
1815
|
+
const error = isJsonObject(body) ? body.error : void 0;
|
|
1816
|
+
if (isJsonObject(error)) {
|
|
1817
|
+
if (typeof error.code === "string") bodyCode = error.code;
|
|
1818
|
+
if (typeof error.reason === "string") reason = error.reason;
|
|
1819
|
+
}
|
|
1820
|
+
} catch {
|
|
1821
|
+
}
|
|
1822
|
+
const context = `Failed to read KV changes for ${JSON.stringify(prefix)}`;
|
|
1823
|
+
if (response.status === 410) {
|
|
1824
|
+
return err(serviceError(
|
|
1825
|
+
ErrorCodes.KV_SYNC_RESET_REQUIRED,
|
|
1826
|
+
`${context}: the node requires a reset (${reason ?? "reason not given"}); discard sync state and restart without a cursor`,
|
|
1827
|
+
"kv",
|
|
1828
|
+
{ meta: { ...meta, reason } }
|
|
1829
|
+
));
|
|
1830
|
+
}
|
|
1831
|
+
if (response.status === 403 && bodyCode === "RETENTION_GRANT_REFUSED") {
|
|
1832
|
+
return err(serviceError(
|
|
1833
|
+
ErrorCodes.KV_RETENTION_GRANT_REFUSED,
|
|
1834
|
+
`${context}: the retention grant was refused (${reason ?? "reason not given"})`,
|
|
1835
|
+
"kv",
|
|
1836
|
+
{ meta: { ...meta, reason } }
|
|
1837
|
+
));
|
|
1838
|
+
}
|
|
1839
|
+
if (response.status === 401) {
|
|
1840
|
+
const revoked = revocationCodeOf(errorText);
|
|
1841
|
+
if (revoked !== void 0) {
|
|
1842
|
+
return err(serviceError(revoked, `${context}: 401 - ${errorText}`, "kv", { meta }));
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
if (response.status === 401 || response.status === 403) {
|
|
1846
|
+
return this.authorizationFailure(context, response, errorText, [prefix], KVAction.SYNC);
|
|
1847
|
+
}
|
|
1848
|
+
return err(serviceError(
|
|
1849
|
+
response.status === 400 ? ErrorCodes.INVALID_INPUT : ErrorCodes.NETWORK_ERROR,
|
|
1850
|
+
`${context}: ${response.status} - ${errorText}`,
|
|
1851
|
+
"kv",
|
|
1852
|
+
{ meta }
|
|
1853
|
+
));
|
|
1854
|
+
}
|
|
1855
|
+
normalizeChangesResponse(data) {
|
|
1856
|
+
if (!isJsonObject(data)) return void 0;
|
|
1857
|
+
const { changes, more, cursor, source, authority } = data;
|
|
1858
|
+
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)) {
|
|
1859
|
+
return void 0;
|
|
1860
|
+
}
|
|
1861
|
+
const { notBefore, expiresAt, retainUntil } = authority;
|
|
1862
|
+
if (notBefore !== null && typeof notBefore !== "string" || expiresAt !== null && typeof expiresAt !== "string" || retainUntil !== null && typeof retainUntil !== "string") {
|
|
1863
|
+
return void 0;
|
|
1864
|
+
}
|
|
1865
|
+
const parsed = [];
|
|
1866
|
+
for (const change of changes) {
|
|
1867
|
+
if (!isJsonObject(change) || typeof change.key !== "string") return void 0;
|
|
1868
|
+
if (change.deleted === true) {
|
|
1869
|
+
parsed.push({ key: change.key, deleted: true });
|
|
1870
|
+
continue;
|
|
1871
|
+
}
|
|
1872
|
+
const { metadata } = change;
|
|
1873
|
+
if (change.deleted !== false || typeof change.etag !== "string" || !isJsonObject(metadata) || !Object.values(metadata).every((value) => typeof value === "string")) {
|
|
1874
|
+
return void 0;
|
|
1875
|
+
}
|
|
1876
|
+
parsed.push({
|
|
1877
|
+
key: change.key,
|
|
1878
|
+
deleted: false,
|
|
1879
|
+
etag: change.etag,
|
|
1880
|
+
metadata
|
|
1881
|
+
});
|
|
1882
|
+
}
|
|
1883
|
+
return {
|
|
1884
|
+
changes: parsed,
|
|
1885
|
+
more,
|
|
1886
|
+
cursor,
|
|
1887
|
+
source: { nodeDid: source.nodeDid, space: source.space, prefix: source.prefix },
|
|
1888
|
+
authority: { notBefore, expiresAt, retainUntil }
|
|
1889
|
+
};
|
|
1890
|
+
}
|
|
1410
1891
|
/**
|
|
1411
1892
|
* Delete a key.
|
|
1412
1893
|
*/
|
|
@@ -1416,20 +1897,23 @@ var KVService = class extends BaseService {
|
|
|
1416
1897
|
return err(authRequiredError("kv"));
|
|
1417
1898
|
}
|
|
1418
1899
|
const path = this.getFullPath(key, options?.prefix);
|
|
1900
|
+
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
1419
1901
|
try {
|
|
1420
1902
|
const response = await this.invokeOperation(
|
|
1421
1903
|
path,
|
|
1422
1904
|
KVAction.DELETE,
|
|
1423
1905
|
void 0,
|
|
1424
|
-
|
|
1906
|
+
request.signal,
|
|
1425
1907
|
options?.ifMatch === void 0 ? void 0 : { "if-match": options.ifMatch }
|
|
1426
1908
|
);
|
|
1427
1909
|
if (!response.ok) {
|
|
1428
|
-
if (response.status === 401) {
|
|
1910
|
+
if (response.status === 401 || response.status === 403) {
|
|
1429
1911
|
return this.authorizationFailure(
|
|
1430
1912
|
`Failed to delete key ${JSON.stringify(key)}`,
|
|
1431
1913
|
response,
|
|
1432
|
-
await
|
|
1914
|
+
await this.readAuthorizationText(response),
|
|
1915
|
+
[path],
|
|
1916
|
+
KVAction.DELETE
|
|
1433
1917
|
);
|
|
1434
1918
|
}
|
|
1435
1919
|
if (response.status === 404) {
|
|
@@ -1467,6 +1951,8 @@ var KVService = class extends BaseService {
|
|
|
1467
1951
|
});
|
|
1468
1952
|
} catch (error) {
|
|
1469
1953
|
return err(wrapError("kv", error));
|
|
1954
|
+
} finally {
|
|
1955
|
+
request.dispose();
|
|
1470
1956
|
}
|
|
1471
1957
|
});
|
|
1472
1958
|
}
|
|
@@ -1479,19 +1965,22 @@ var KVService = class extends BaseService {
|
|
|
1479
1965
|
return err(authRequiredError("kv"));
|
|
1480
1966
|
}
|
|
1481
1967
|
const path = this.getFullPath(key, options?.prefix);
|
|
1968
|
+
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
1482
1969
|
try {
|
|
1483
1970
|
const response = await this.invokeOperation(
|
|
1484
1971
|
path,
|
|
1485
1972
|
KVAction.HEAD,
|
|
1486
1973
|
void 0,
|
|
1487
|
-
|
|
1974
|
+
request.signal
|
|
1488
1975
|
);
|
|
1489
1976
|
if (!response.ok) {
|
|
1490
|
-
if (response.status === 401) {
|
|
1977
|
+
if (response.status === 401 || response.status === 403) {
|
|
1491
1978
|
return this.authorizationFailure(
|
|
1492
1979
|
`Failed to get metadata for key ${JSON.stringify(key)}`,
|
|
1493
1980
|
response,
|
|
1494
|
-
await
|
|
1981
|
+
await this.readAuthorizationText(response),
|
|
1982
|
+
[path],
|
|
1983
|
+
KVAction.HEAD
|
|
1495
1984
|
);
|
|
1496
1985
|
}
|
|
1497
1986
|
if (response.status === 404) {
|
|
@@ -1513,6 +2002,8 @@ var KVService = class extends BaseService {
|
|
|
1513
2002
|
});
|
|
1514
2003
|
} catch (error) {
|
|
1515
2004
|
return err(wrapError("kv", error));
|
|
2005
|
+
} finally {
|
|
2006
|
+
request.dispose();
|
|
1516
2007
|
}
|
|
1517
2008
|
});
|
|
1518
2009
|
}
|
|
@@ -1550,15 +2041,16 @@ var KVService = class extends BaseService {
|
|
|
1550
2041
|
if (options?.etag !== void 0) {
|
|
1551
2042
|
body.etag = options.etag;
|
|
1552
2043
|
}
|
|
2044
|
+
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
1553
2045
|
try {
|
|
1554
2046
|
const response = await this.context.fetch(`${this.host}/signed/kv`, {
|
|
1555
2047
|
method: "POST",
|
|
1556
2048
|
headers: this.withJsonContentType(headers),
|
|
1557
2049
|
body: JSON.stringify(body),
|
|
1558
|
-
signal:
|
|
2050
|
+
signal: request.signal
|
|
1559
2051
|
});
|
|
1560
2052
|
if (!response.ok) {
|
|
1561
|
-
return this.createSignedReadUrlError(response, key);
|
|
2053
|
+
return this.createSignedReadUrlError(response, key, path);
|
|
1562
2054
|
}
|
|
1563
2055
|
const signedUrl = this.normalizeSignedReadUrlResponse(
|
|
1564
2056
|
await response.json()
|
|
@@ -1575,6 +2067,8 @@ var KVService = class extends BaseService {
|
|
|
1575
2067
|
return ok(signedUrl);
|
|
1576
2068
|
} catch (error) {
|
|
1577
2069
|
return err(wrapError("kv", error));
|
|
2070
|
+
} finally {
|
|
2071
|
+
request.dispose();
|
|
1578
2072
|
}
|
|
1579
2073
|
});
|
|
1580
2074
|
}
|