@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/kv/index.cjs
CHANGED
|
@@ -34,6 +34,10 @@ var ErrorCodes = {
|
|
|
34
34
|
AUTH_EXPIRED: "AUTH_EXPIRED",
|
|
35
35
|
AUTH_REQUIRED: "AUTH_REQUIRED",
|
|
36
36
|
AUTH_UNAUTHORIZED: "AUTH_UNAUTHORIZED",
|
|
37
|
+
/** HTTP 401: the delegation the invocation cites was revoked. */
|
|
38
|
+
AUTH_DELEGATION_REVOKED: "AUTH_DELEGATION_REVOKED",
|
|
39
|
+
/** HTTP 401: a delegation the cited one rests on was revoked. */
|
|
40
|
+
AUTH_DELEGATION_ANCESTOR_REVOKED: "AUTH_DELEGATION_ANCESTOR_REVOKED",
|
|
37
41
|
NETWORK_ERROR: "NETWORK_ERROR",
|
|
38
42
|
TIMEOUT: "TIMEOUT",
|
|
39
43
|
ABORTED: "ABORTED",
|
|
@@ -45,6 +49,10 @@ var ErrorCodes = {
|
|
|
45
49
|
KV_PRECONDITION_FAILED: "KV_PRECONDITION_FAILED",
|
|
46
50
|
KV_CONFLICT: "KV_CONFLICT",
|
|
47
51
|
KV_RESPONSE_TOO_LARGE: "KV_RESPONSE_TOO_LARGE",
|
|
52
|
+
/** HTTP 410 from the change feed: discard sync state and restart without a cursor; `meta.reason` says why. */
|
|
53
|
+
KV_SYNC_RESET_REQUIRED: "KV_SYNC_RESET_REQUIRED",
|
|
54
|
+
/** HTTP 403 from the change feed: the retention grant was refused; `meta.reason` says why. */
|
|
55
|
+
KV_RETENTION_GRANT_REFUSED: "KV_RETENTION_GRANT_REFUSED",
|
|
48
56
|
// SQL-specific errors
|
|
49
57
|
SQL_ERROR: "SQL_ERROR",
|
|
50
58
|
SQL_PERMISSION_DENIED: "SQL_PERMISSION_DENIED",
|
|
@@ -99,6 +107,55 @@ function serviceError(code, message, service, options) {
|
|
|
99
107
|
};
|
|
100
108
|
}
|
|
101
109
|
|
|
110
|
+
// src/errors.ts
|
|
111
|
+
var import_bootstrap = require("@tinycloud/bootstrap");
|
|
112
|
+
|
|
113
|
+
// src/encryption/networkId.ts
|
|
114
|
+
var URN_PREFIX = "urn:tinycloud:encryption:";
|
|
115
|
+
var NETWORK_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
116
|
+
var NetworkIdError = class extends Error {
|
|
117
|
+
constructor(message) {
|
|
118
|
+
super(message);
|
|
119
|
+
this.name = "NetworkIdError";
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
function parseNetworkId(networkId) {
|
|
123
|
+
if (typeof networkId !== "string" || networkId.length === 0) {
|
|
124
|
+
throw new NetworkIdError("networkId must be a non-empty string");
|
|
125
|
+
}
|
|
126
|
+
if (!networkId.startsWith(URN_PREFIX)) {
|
|
127
|
+
throw new NetworkIdError(
|
|
128
|
+
`networkId must start with ${URN_PREFIX} (got ${JSON.stringify(networkId)})`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
const body = networkId.slice(URN_PREFIX.length);
|
|
132
|
+
const lastColon = body.lastIndexOf(":");
|
|
133
|
+
if (lastColon <= 0 || lastColon === body.length - 1) {
|
|
134
|
+
throw new NetworkIdError(
|
|
135
|
+
`networkId missing ownerDid or name segment (got ${JSON.stringify(networkId)})`
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
const ownerDid = body.slice(0, lastColon);
|
|
139
|
+
const name = body.slice(lastColon + 1);
|
|
140
|
+
if (!ownerDid.startsWith("did:")) {
|
|
141
|
+
throw new NetworkIdError(
|
|
142
|
+
`networkId ownerDid must be a DID (got ${JSON.stringify(ownerDid)})`
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
const didParts = ownerDid.split(":");
|
|
146
|
+
if (didParts.length < 3 || didParts.some((p) => p.length === 0)) {
|
|
147
|
+
throw new NetworkIdError(
|
|
148
|
+
`networkId ownerDid is not a well-formed DID (got ${JSON.stringify(ownerDid)})`
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
if (!NETWORK_NAME_RE.test(name)) {
|
|
152
|
+
throw new NetworkIdError(
|
|
153
|
+
`networkId name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
return { networkId, ownerDid, name };
|
|
157
|
+
}
|
|
158
|
+
|
|
102
159
|
// src/errors.ts
|
|
103
160
|
function parsePermissionHint(value) {
|
|
104
161
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
@@ -143,11 +200,31 @@ function authRequiredError(service) {
|
|
|
143
200
|
service
|
|
144
201
|
};
|
|
145
202
|
}
|
|
146
|
-
|
|
203
|
+
var RequestTimeoutError = class extends Error {
|
|
204
|
+
constructor(timeoutMs) {
|
|
205
|
+
super(`Request timed out after ${timeoutMs}ms.`);
|
|
206
|
+
this.name = "TimeoutError";
|
|
207
|
+
this.timeoutMs = timeoutMs;
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
function isRequestTimeoutError(error) {
|
|
211
|
+
return error instanceof RequestTimeoutError || error instanceof Error && error.name === "TimeoutError" && typeof error.timeoutMs === "number";
|
|
212
|
+
}
|
|
213
|
+
function timeoutError(service, timeoutMs, cause) {
|
|
214
|
+
if (timeoutMs === void 0) {
|
|
215
|
+
return {
|
|
216
|
+
code: ErrorCodes.TIMEOUT,
|
|
217
|
+
message: "Request timed out.",
|
|
218
|
+
service,
|
|
219
|
+
...cause === void 0 ? {} : { cause }
|
|
220
|
+
};
|
|
221
|
+
}
|
|
147
222
|
return {
|
|
148
223
|
code: ErrorCodes.TIMEOUT,
|
|
149
|
-
message:
|
|
150
|
-
service
|
|
224
|
+
message: `Request timed out after ${timeoutMs}ms.`,
|
|
225
|
+
service,
|
|
226
|
+
...cause === void 0 ? {} : { cause },
|
|
227
|
+
meta: { timeoutMs }
|
|
151
228
|
};
|
|
152
229
|
}
|
|
153
230
|
function abortedError(service) {
|
|
@@ -167,6 +244,101 @@ function parseAuthError(responseText) {
|
|
|
167
244
|
function authUnauthorizedError(service, message, meta) {
|
|
168
245
|
return serviceError(ErrorCodes.AUTH_UNAUTHORIZED, message, service, { meta });
|
|
169
246
|
}
|
|
247
|
+
var grantableActions = new Set(
|
|
248
|
+
import_bootstrap.CAPABILITY_REGISTRY.map(({ urn }) => urn).filter((urn) => !urn.endsWith("/*"))
|
|
249
|
+
);
|
|
250
|
+
var capabilityServices = new Set(
|
|
251
|
+
import_bootstrap.CAPABILITY_REGISTRY.map(({ service }) => service.slice("tinycloud.".length))
|
|
252
|
+
);
|
|
253
|
+
function parseCapabilityResource(resource, service) {
|
|
254
|
+
if (resource.length === 0 || resource.length > 1024 || !capabilityServices.has(service) || /[\s\p{C}*#]/u.test(resource) || resource.includes("://")) return void 0;
|
|
255
|
+
const segments = resource.split("/");
|
|
256
|
+
if (!/^tinycloud:[A-Za-z0-9][A-Za-z0-9:._-]*$/.test(segments[0])) return void 0;
|
|
257
|
+
const serviceIndex = segments.findIndex(
|
|
258
|
+
(segment, index) => index > 0 && capabilityServices.has(segment)
|
|
259
|
+
);
|
|
260
|
+
if (serviceIndex < 1 || segments[serviceIndex] !== service) return void 0;
|
|
261
|
+
if (segments.slice(1, serviceIndex).some(
|
|
262
|
+
(segment) => segment.length === 0 || segment === "." || segment === ".."
|
|
263
|
+
)) return void 0;
|
|
264
|
+
const pathSegments = segments.slice(serviceIndex + 1);
|
|
265
|
+
if (pathSegments.some((segment) => segment === "." || segment === "..")) return void 0;
|
|
266
|
+
if (service !== "kv" && (pathSegments.length === 0 || pathSegments[pathSegments.length - 1] === "")) return void 0;
|
|
267
|
+
return {
|
|
268
|
+
space: segments.slice(0, serviceIndex).join("/"),
|
|
269
|
+
path: pathSegments.join("/")
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
function safeCapabilityResource(value, service, action) {
|
|
273
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 1024) return false;
|
|
274
|
+
if (service === "encryption" && value.startsWith("urn:tinycloud:encryption:")) {
|
|
275
|
+
if (!/^[A-Za-z0-9_][A-Za-z0-9_.:/-]*$/.test(value) || value.includes("//") || value.split("/").some((segment) => segment === "." || segment === "..")) return false;
|
|
276
|
+
try {
|
|
277
|
+
parseNetworkId(value);
|
|
278
|
+
return true;
|
|
279
|
+
} catch {
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
const parsed = parseCapabilityResource(value, service);
|
|
284
|
+
return parsed !== void 0 && (!parsed.path.endsWith("/") || service === "kv" && action === "tinycloud.kv/list") && (parsed.path !== "" || service === "kv" && action === "tinycloud.kv/list");
|
|
285
|
+
}
|
|
286
|
+
function validatedCapabilityOf(error) {
|
|
287
|
+
const verdict = authorizationVerdictOf(error);
|
|
288
|
+
if (verdict !== "unauthenticated" && verdict !== "forbidden") return void 0;
|
|
289
|
+
const seen = /* @__PURE__ */ new Set();
|
|
290
|
+
let node = error;
|
|
291
|
+
for (let depth = 0; depth < MAX_CAUSE_DEPTH && typeof node === "object" && node !== null && !seen.has(node); depth += 1) {
|
|
292
|
+
seen.add(node);
|
|
293
|
+
const record = node;
|
|
294
|
+
const status = errorStatusOf(record);
|
|
295
|
+
const meta = record.meta;
|
|
296
|
+
if ((status === 401 || status === 403 || status === void 0 && record.code === ErrorCodes.AUTH_UNAUTHORIZED) && typeof meta === "object" && meta !== null && !Array.isArray(meta)) {
|
|
297
|
+
const metadata = meta;
|
|
298
|
+
const { resource, requiredAction } = metadata;
|
|
299
|
+
if (typeof requiredAction === "string" && grantableActions.has(requiredAction)) {
|
|
300
|
+
const actionService = requiredAction.slice("tinycloud.".length, requiredAction.indexOf("/"));
|
|
301
|
+
const explicitService = record.service === void 0 ? metadata.service : record.service;
|
|
302
|
+
if (safeCapabilityResource(resource, actionService, requiredAction) && (explicitService === void 0 || explicitService === actionService || explicitService === `tinycloud.${actionService}`)) {
|
|
303
|
+
return { resource, requiredAction };
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
node = record.cause;
|
|
308
|
+
}
|
|
309
|
+
return void 0;
|
|
310
|
+
}
|
|
311
|
+
var MAX_CAUSE_DEPTH = 8;
|
|
312
|
+
function errorStatusOf(node) {
|
|
313
|
+
const meta = node.meta;
|
|
314
|
+
const candidates = [
|
|
315
|
+
node.status,
|
|
316
|
+
node.statusCode,
|
|
317
|
+
typeof meta === "object" && meta !== null ? meta.status : void 0
|
|
318
|
+
];
|
|
319
|
+
for (const candidate of candidates) {
|
|
320
|
+
if (typeof candidate === "number" && Number.isInteger(candidate) && candidate >= 400 && candidate <= 599) {
|
|
321
|
+
return candidate;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return void 0;
|
|
325
|
+
}
|
|
326
|
+
function authorizationVerdictOf(error) {
|
|
327
|
+
const seen = /* @__PURE__ */ new Set();
|
|
328
|
+
let sawUnauthorizedCode = false;
|
|
329
|
+
let node = error;
|
|
330
|
+
for (let depth = 0; depth < MAX_CAUSE_DEPTH && typeof node === "object" && node !== null && !seen.has(node); depth += 1) {
|
|
331
|
+
seen.add(node);
|
|
332
|
+
const record = node;
|
|
333
|
+
const status = errorStatusOf(record);
|
|
334
|
+
if (status === 401) return "unauthenticated";
|
|
335
|
+
if (status === 403) return "forbidden";
|
|
336
|
+
if (status !== void 0) return "other";
|
|
337
|
+
if (record.code === ErrorCodes.AUTH_UNAUTHORIZED) sawUnauthorizedCode = true;
|
|
338
|
+
node = record.cause;
|
|
339
|
+
}
|
|
340
|
+
return sawUnauthorizedCode ? "forbidden" : void 0;
|
|
341
|
+
}
|
|
170
342
|
function storageQuotaExceededError(service, message, meta) {
|
|
171
343
|
return {
|
|
172
344
|
code: ErrorCodes.STORAGE_QUOTA_EXCEEDED,
|
|
@@ -183,8 +355,25 @@ function storageLimitReachedError(service, message, meta) {
|
|
|
183
355
|
meta
|
|
184
356
|
};
|
|
185
357
|
}
|
|
358
|
+
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.";
|
|
359
|
+
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.";
|
|
360
|
+
function parseStorageQuotaBytes(responseText) {
|
|
361
|
+
const match = responseText.match(/Used:\s*(\d+)\s*bytes,\s*Limit:\s*(\d+)\s*bytes/i);
|
|
362
|
+
if (!match) return void 0;
|
|
363
|
+
return {
|
|
364
|
+
usedBytes: parseInt(match[1], 10),
|
|
365
|
+
limitBytes: parseInt(match[2], 10)
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
function storageRejectionError(service, status, meta, responseText) {
|
|
369
|
+
const quotaMeta = { ...meta, ...parseStorageQuotaBytes(responseText) };
|
|
370
|
+
return status === 402 ? storageQuotaExceededError(service, STORAGE_FULL_MESSAGE, quotaMeta) : storageLimitReachedError(service, STORAGE_WRITE_TOO_LARGE_MESSAGE, quotaMeta);
|
|
371
|
+
}
|
|
186
372
|
function wrapError(service, error, defaultCode = ErrorCodes.NETWORK_ERROR) {
|
|
187
373
|
if (error instanceof Error) {
|
|
374
|
+
if (isRequestTimeoutError(error)) {
|
|
375
|
+
return timeoutError(service, error.timeoutMs, error);
|
|
376
|
+
}
|
|
188
377
|
if (error.name === "AbortError") {
|
|
189
378
|
return abortedError(service);
|
|
190
379
|
}
|
|
@@ -206,6 +395,7 @@ function wrapError(service, error, defaultCode = ErrorCodes.NETWORK_ERROR) {
|
|
|
206
395
|
}
|
|
207
396
|
|
|
208
397
|
// src/base/BaseService.ts
|
|
398
|
+
var MAX_TIMER_DELAY_MS = 2147483647;
|
|
209
399
|
var BaseService = class {
|
|
210
400
|
constructor() {
|
|
211
401
|
/**
|
|
@@ -386,6 +576,64 @@ var BaseService = class {
|
|
|
386
576
|
}
|
|
387
577
|
return controller.signal;
|
|
388
578
|
}
|
|
579
|
+
/**
|
|
580
|
+
* Create the abort signal for one request, bounded by the request timeout.
|
|
581
|
+
*
|
|
582
|
+
* The signal aborts when the service signs out, the context aborts, the
|
|
583
|
+
* caller's `signal` aborts, or the timeout elapses, whichever comes first.
|
|
584
|
+
* A timeout aborts with a {@link RequestTimeoutError} (`TimeoutError`), which
|
|
585
|
+
* `wrapError` maps to `ErrorCodes.TIMEOUT`; the other sources keep their own
|
|
586
|
+
* reason (normally `AbortError`, mapped to `ErrorCodes.ABORTED`).
|
|
587
|
+
*
|
|
588
|
+
* The timeout is `timeoutMs` when given, otherwise `config.timeout`. Only a
|
|
589
|
+
* positive, finite value (at most 2^31-1 ms) applies; `undefined`, `0`, and
|
|
590
|
+
* `Infinity` mean no timeout, so a per-call `0` opts out of a configured one.
|
|
591
|
+
*
|
|
592
|
+
* Callers must call `dispose()` once the response body has been consumed,
|
|
593
|
+
* which clears the timer and removes the listeners this adds to the
|
|
594
|
+
* long-lived service and context signals.
|
|
595
|
+
*
|
|
596
|
+
* @param signal - The caller's abort signal for this request
|
|
597
|
+
* @param timeoutMs - Per-request timeout override in milliseconds
|
|
598
|
+
*/
|
|
599
|
+
createRequestSignal(signal, timeoutMs) {
|
|
600
|
+
const controller = new AbortController();
|
|
601
|
+
const detachers = [];
|
|
602
|
+
let timer;
|
|
603
|
+
const dispose = () => {
|
|
604
|
+
if (timer !== void 0) {
|
|
605
|
+
clearTimeout(timer);
|
|
606
|
+
timer = void 0;
|
|
607
|
+
}
|
|
608
|
+
for (const detach of detachers.splice(0)) {
|
|
609
|
+
detach();
|
|
610
|
+
}
|
|
611
|
+
};
|
|
612
|
+
const abort = (reason) => {
|
|
613
|
+
dispose();
|
|
614
|
+
controller.abort(reason);
|
|
615
|
+
};
|
|
616
|
+
const requestSignal = { signal: controller.signal, dispose };
|
|
617
|
+
const parents = [
|
|
618
|
+
this.abortController.signal,
|
|
619
|
+
this.context?.abortSignal,
|
|
620
|
+
signal
|
|
621
|
+
].filter((parent) => parent !== void 0);
|
|
622
|
+
for (const parent of parents) {
|
|
623
|
+
if (parent.aborted) {
|
|
624
|
+
abort(parent.reason);
|
|
625
|
+
return requestSignal;
|
|
626
|
+
}
|
|
627
|
+
const onAbort = () => abort(parent.reason);
|
|
628
|
+
parent.addEventListener("abort", onAbort, { once: true });
|
|
629
|
+
detachers.push(() => parent.removeEventListener("abort", onAbort));
|
|
630
|
+
}
|
|
631
|
+
const timeout = timeoutMs ?? this._config.timeout;
|
|
632
|
+
if (typeof timeout === "number" && timeout > 0 && timeout <= MAX_TIMER_DELAY_MS) {
|
|
633
|
+
timer = setTimeout(() => abort(new RequestTimeoutError(timeout)), timeout);
|
|
634
|
+
}
|
|
635
|
+
return requestSignal;
|
|
636
|
+
}
|
|
389
637
|
/**
|
|
390
638
|
* Wrap an operation with error handling and telemetry.
|
|
391
639
|
*
|
|
@@ -494,6 +742,30 @@ var PrefixedKVService = class _PrefixedKVService {
|
|
|
494
742
|
removePrefix
|
|
495
743
|
});
|
|
496
744
|
}
|
|
745
|
+
/**
|
|
746
|
+
* Read the change feed for everything under this prefix. A view with an
|
|
747
|
+
* empty prefix (`""` or `"/"`) is refused: the node does not serve a
|
|
748
|
+
* whole-space feed.
|
|
749
|
+
*/
|
|
750
|
+
async changes(options) {
|
|
751
|
+
if (this._prefix.replace(/\/+$/, "") === "") {
|
|
752
|
+
return err(serviceError(
|
|
753
|
+
ErrorCodes.INVALID_INPUT,
|
|
754
|
+
"KV changes needs a non-empty prefix; this view has none",
|
|
755
|
+
"kv"
|
|
756
|
+
));
|
|
757
|
+
}
|
|
758
|
+
const syncPrefix = `${this._prefix}/`;
|
|
759
|
+
const response = await this._kv.changes({ ...options, prefix: syncPrefix });
|
|
760
|
+
if (!response.ok) return response;
|
|
761
|
+
return ok({
|
|
762
|
+
...response.data,
|
|
763
|
+
changes: response.data.changes.map((change) => ({
|
|
764
|
+
...change,
|
|
765
|
+
key: change.key.startsWith(syncPrefix) ? change.key.slice(syncPrefix.length) : change.key
|
|
766
|
+
}))
|
|
767
|
+
});
|
|
768
|
+
}
|
|
497
769
|
/**
|
|
498
770
|
* Delete a key.
|
|
499
771
|
*/
|
|
@@ -546,11 +818,24 @@ var KVAction = {
|
|
|
546
818
|
PUT: "tinycloud.kv/put",
|
|
547
819
|
LIST: "tinycloud.kv/list",
|
|
548
820
|
DELETE: "tinycloud.kv/del",
|
|
549
|
-
HEAD: "tinycloud.kv/metadata"
|
|
821
|
+
HEAD: "tinycloud.kv/metadata",
|
|
822
|
+
/** The change feed; never implied by `*` or `tinycloud.kv/*`. */
|
|
823
|
+
SYNC: "tinycloud.kv/sync",
|
|
824
|
+
/** Retention attestation; presented through `retentionGrant`, never invoked or implied. */
|
|
825
|
+
RETAIN: "tinycloud.kv/retain"
|
|
550
826
|
};
|
|
551
827
|
|
|
552
828
|
// src/kv/KVService.ts
|
|
553
829
|
var MAX_KV_BATCH_READ_ITEMS = 100;
|
|
830
|
+
function isJsonObject(value) {
|
|
831
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
832
|
+
}
|
|
833
|
+
var REVOKED_BODY = /^(?:Invalid invocation: )?delegation-(?:(revoked): [A-Za-z0-9]+|(ancestor-revoked): ancestor=[A-Za-z0-9]+ invoked=[A-Za-z0-9]+)$/;
|
|
834
|
+
function revocationCodeOf(body) {
|
|
835
|
+
const match = REVOKED_BODY.exec(body.trim());
|
|
836
|
+
if (match === null) return void 0;
|
|
837
|
+
return match[1] !== void 0 ? ErrorCodes.AUTH_DELEGATION_REVOKED : ErrorCodes.AUTH_DELEGATION_ANCESTOR_REVOKED;
|
|
838
|
+
}
|
|
554
839
|
function encodeKvBatchPartName(path) {
|
|
555
840
|
return encodeURIComponent(path).replace(
|
|
556
841
|
/[!'()*]/g,
|
|
@@ -573,47 +858,21 @@ var KVService = class extends BaseService {
|
|
|
573
858
|
get config() {
|
|
574
859
|
return this._config;
|
|
575
860
|
}
|
|
576
|
-
// Parses "Used: X bytes, Limit: Y bytes" from tinycloud-node error responses
|
|
577
|
-
parseQuotaInfo(errorText) {
|
|
578
|
-
const match = errorText.match(
|
|
579
|
-
/Used:\s*(\d+)\s*bytes,\s*Limit:\s*(\d+)\s*bytes/i
|
|
580
|
-
);
|
|
581
|
-
if (match) {
|
|
582
|
-
return {
|
|
583
|
-
usedBytes: parseInt(match[1], 10),
|
|
584
|
-
limitBytes: parseInt(match[2], 10)
|
|
585
|
-
};
|
|
586
|
-
}
|
|
587
|
-
return void 0;
|
|
588
|
-
}
|
|
589
861
|
handleQuotaErrorResponse(response, errorText, key) {
|
|
590
|
-
if (response.status
|
|
591
|
-
|
|
592
|
-
return err(
|
|
593
|
-
storageQuotaExceededError(
|
|
594
|
-
"kv",
|
|
595
|
-
`Storage quota exceeded for key "${key}": ${errorText}`,
|
|
596
|
-
{
|
|
597
|
-
status: response.status,
|
|
598
|
-
...quotaInfo ? { usedBytes: quotaInfo.usedBytes, limitBytes: quotaInfo.limitBytes } : {}
|
|
599
|
-
}
|
|
600
|
-
)
|
|
601
|
-
);
|
|
862
|
+
if (response.status !== 402 && response.status !== 413) {
|
|
863
|
+
return void 0;
|
|
602
864
|
}
|
|
603
|
-
if (response.status === 413) {
|
|
604
|
-
|
|
605
|
-
return err(
|
|
606
|
-
storageLimitReachedError(
|
|
607
|
-
"kv",
|
|
608
|
-
`Storage limit reached for key "${key}": ${errorText}`,
|
|
609
|
-
{
|
|
610
|
-
status: response.status,
|
|
611
|
-
...quotaInfo ? { usedBytes: quotaInfo.usedBytes, limitBytes: quotaInfo.limitBytes } : {}
|
|
612
|
-
}
|
|
613
|
-
)
|
|
614
|
-
);
|
|
865
|
+
if (response.status === 413 && !parseStorageQuotaBytes(errorText)) {
|
|
866
|
+
return void 0;
|
|
615
867
|
}
|
|
616
|
-
return
|
|
868
|
+
return err(
|
|
869
|
+
storageRejectionError(
|
|
870
|
+
"kv",
|
|
871
|
+
response.status,
|
|
872
|
+
{ status: response.status, statusText: response.statusText, key },
|
|
873
|
+
errorText
|
|
874
|
+
)
|
|
875
|
+
);
|
|
617
876
|
}
|
|
618
877
|
/**
|
|
619
878
|
* Classify a KV 404 by reading the response body once.
|
|
@@ -672,7 +931,7 @@ var KVService = class extends BaseService {
|
|
|
672
931
|
* @param path - Resource path
|
|
673
932
|
* @param action - KV action
|
|
674
933
|
* @param body - Optional request body
|
|
675
|
-
* @param signal -
|
|
934
|
+
* @param signal - Request signal from createRequestSignal()
|
|
676
935
|
* @returns Fetch response
|
|
677
936
|
*/
|
|
678
937
|
async invokeOperation(path, action, body, signal, extraHeaders) {
|
|
@@ -688,7 +947,7 @@ var KVService = class extends BaseService {
|
|
|
688
947
|
method: "POST",
|
|
689
948
|
headers: requestHeaders,
|
|
690
949
|
body,
|
|
691
|
-
signal
|
|
950
|
+
signal
|
|
692
951
|
});
|
|
693
952
|
}
|
|
694
953
|
/**
|
|
@@ -865,6 +1124,7 @@ var KVService = class extends BaseService {
|
|
|
865
1124
|
"kv"
|
|
866
1125
|
));
|
|
867
1126
|
}
|
|
1127
|
+
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
868
1128
|
try {
|
|
869
1129
|
const session = this.context.session;
|
|
870
1130
|
const invocationHeaders = this.context.invokeAny(
|
|
@@ -886,17 +1146,18 @@ var KVService = class extends BaseService {
|
|
|
886
1146
|
const response = await this.context.fetch(`${this.host}/invoke`, {
|
|
887
1147
|
method: "POST",
|
|
888
1148
|
headers,
|
|
889
|
-
signal:
|
|
1149
|
+
signal: request.signal
|
|
890
1150
|
});
|
|
891
1151
|
if (!response.ok) {
|
|
892
|
-
const errorText = await response.text();
|
|
1152
|
+
const errorText = response.status === 401 || response.status === 403 ? await this.readAuthorizationText(response) : await response.text();
|
|
893
1153
|
if (response.status === 401 || response.status === 403) {
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
1154
|
+
return this.authorizationFailure(
|
|
1155
|
+
`Failed to batch read ${keys.length} key(s)`,
|
|
1156
|
+
response,
|
|
1157
|
+
errorText,
|
|
1158
|
+
paths,
|
|
1159
|
+
action
|
|
1160
|
+
);
|
|
900
1161
|
}
|
|
901
1162
|
if (response.status === 413) {
|
|
902
1163
|
return err(serviceError(
|
|
@@ -950,6 +1211,8 @@ var KVService = class extends BaseService {
|
|
|
950
1211
|
return ok({ results, count: results.length });
|
|
951
1212
|
} catch (error) {
|
|
952
1213
|
return err(wrapError("kv", error));
|
|
1214
|
+
} finally {
|
|
1215
|
+
request.dispose();
|
|
953
1216
|
}
|
|
954
1217
|
}
|
|
955
1218
|
/**
|
|
@@ -985,7 +1248,7 @@ var KVService = class extends BaseService {
|
|
|
985
1248
|
return text;
|
|
986
1249
|
}
|
|
987
1250
|
}
|
|
988
|
-
async createSignedReadUrlError(response, key) {
|
|
1251
|
+
async createSignedReadUrlError(response, key, path) {
|
|
989
1252
|
let errorText = response.statusText;
|
|
990
1253
|
try {
|
|
991
1254
|
const text = await response.text();
|
|
@@ -995,23 +1258,62 @@ var KVService = class extends BaseService {
|
|
|
995
1258
|
} catch {
|
|
996
1259
|
}
|
|
997
1260
|
if (response.status === 401 || response.status === 403) {
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1261
|
+
return this.authorizationFailure(
|
|
1262
|
+
`Failed to create signed read URL for key ${JSON.stringify(key)}`,
|
|
1263
|
+
response,
|
|
1264
|
+
errorText,
|
|
1265
|
+
[path],
|
|
1266
|
+
KVAction.GET
|
|
1267
|
+
);
|
|
1004
1268
|
}
|
|
1005
1269
|
const code = response.status === 400 ? ErrorCodes.INVALID_INPUT : ErrorCodes.NETWORK_ERROR;
|
|
1006
1270
|
return err(
|
|
1007
1271
|
serviceError(
|
|
1008
1272
|
code,
|
|
1009
|
-
`Failed to create signed read URL for key
|
|
1273
|
+
`Failed to create signed read URL for key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
|
|
1010
1274
|
"kv",
|
|
1011
1275
|
{ meta: { status: response.status, statusText: response.statusText } }
|
|
1012
1276
|
)
|
|
1013
1277
|
);
|
|
1014
1278
|
}
|
|
1279
|
+
/** A known 401/403 keeps its typed status even if reading its body fails. */
|
|
1280
|
+
async readAuthorizationText(response) {
|
|
1281
|
+
try {
|
|
1282
|
+
return await response.text();
|
|
1283
|
+
} catch {
|
|
1284
|
+
return "";
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
/**
|
|
1288
|
+
* AUTH_UNAUTHORIZED for a 401/403 response. The message keeps the HTTP
|
|
1289
|
+
* status and the server text (`<context>: <status> - <text>`, the same
|
|
1290
|
+
* shape as every other KV failure) so callers that rethrow only the message
|
|
1291
|
+
* still see the status; `meta.status` carries it for typed callers.
|
|
1292
|
+
* Server hints must match the requested path/action (whether the node sends
|
|
1293
|
+
* a relative path or full resource); metadata uses only the request-derived
|
|
1294
|
+
* canonical resource under the authenticated session's space.
|
|
1295
|
+
*/
|
|
1296
|
+
authorizationFailure(context, response, errorText, paths, expectedAction, extraMeta = {}) {
|
|
1297
|
+
const { resource, action } = parseAuthError(errorText);
|
|
1298
|
+
const requestedPath = paths.find(
|
|
1299
|
+
(path) => resource === path || resource === `${this.context.session.spaceId}/kv/${path}`
|
|
1300
|
+
);
|
|
1301
|
+
const capability = requestedPath !== void 0 && action === expectedAction ? validatedCapabilityOf({
|
|
1302
|
+
service: "kv",
|
|
1303
|
+
code: ErrorCodes.AUTH_UNAUTHORIZED,
|
|
1304
|
+
meta: {
|
|
1305
|
+
status: response.status,
|
|
1306
|
+
resource: `${this.context.session.spaceId}/kv/${requestedPath}`,
|
|
1307
|
+
requiredAction: action
|
|
1308
|
+
}
|
|
1309
|
+
}) : void 0;
|
|
1310
|
+
const detail = errorText.trim().length > 0 ? errorText : response.statusText || "authorization failed";
|
|
1311
|
+
return err(authUnauthorizedError("kv", `${context}: ${response.status} - ${detail}`, {
|
|
1312
|
+
status: response.status,
|
|
1313
|
+
...extraMeta,
|
|
1314
|
+
...capability
|
|
1315
|
+
}));
|
|
1316
|
+
}
|
|
1015
1317
|
normalizeSignedReadUrlResponse(data) {
|
|
1016
1318
|
if (!data || typeof data !== "object") {
|
|
1017
1319
|
return void 0;
|
|
@@ -1043,25 +1345,36 @@ var KVService = class extends BaseService {
|
|
|
1043
1345
|
));
|
|
1044
1346
|
}
|
|
1045
1347
|
const path = this.getFullPath(key, options?.prefix);
|
|
1348
|
+
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
1046
1349
|
try {
|
|
1047
1350
|
const response = await this.invokeOperation(
|
|
1048
1351
|
path,
|
|
1049
1352
|
KVAction.GET,
|
|
1050
1353
|
void 0,
|
|
1051
|
-
|
|
1354
|
+
request.signal,
|
|
1052
1355
|
options?.maxResponseBytes === void 0 ? void 0 : { "x-tinycloud-max-response-bytes": String(options.maxResponseBytes) }
|
|
1053
1356
|
);
|
|
1054
1357
|
if (!response.ok) {
|
|
1055
1358
|
if (response.status === 401 || response.status === 403) {
|
|
1056
|
-
const errorText2 = await
|
|
1057
|
-
const { resource, action } = parseAuthError(errorText2);
|
|
1359
|
+
const errorText2 = await this.readAuthorizationText(response);
|
|
1058
1360
|
const permissionHint = parsePermissionHintFromErrorText(errorText2);
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1361
|
+
const structuredCapability = permissionHint?.service === "tinycloud.kv" && permissionHint.space === this.context.session.spaceId && permissionHint.path === path ? validatedCapabilityOf({
|
|
1362
|
+
service: "kv",
|
|
1363
|
+
code: ErrorCodes.AUTH_UNAUTHORIZED,
|
|
1364
|
+
meta: {
|
|
1365
|
+
status: response.status,
|
|
1366
|
+
resource: `${this.context.session.spaceId}/kv/${path}`,
|
|
1367
|
+
requiredAction: KVAction.GET
|
|
1368
|
+
}
|
|
1369
|
+
}) : void 0;
|
|
1370
|
+
return this.authorizationFailure(
|
|
1371
|
+
`Failed to get key ${JSON.stringify(key)}`,
|
|
1372
|
+
response,
|
|
1373
|
+
errorText2,
|
|
1374
|
+
[path],
|
|
1375
|
+
KVAction.GET,
|
|
1376
|
+
structuredCapability ? { permissionHint, ...structuredCapability } : {}
|
|
1377
|
+
);
|
|
1065
1378
|
}
|
|
1066
1379
|
if (response.status === 404) {
|
|
1067
1380
|
return this.classifyNotFound(response, key);
|
|
@@ -1070,7 +1383,7 @@ var KVService = class extends BaseService {
|
|
|
1070
1383
|
if (response.status === 413) {
|
|
1071
1384
|
return err(serviceError(
|
|
1072
1385
|
ErrorCodes.KV_RESPONSE_TOO_LARGE,
|
|
1073
|
-
`KV value at key
|
|
1386
|
+
`KV value at key ${JSON.stringify(key)} exceeds the requested response limit`,
|
|
1074
1387
|
"kv",
|
|
1075
1388
|
{ meta: { status: response.status, statusText: response.statusText } }
|
|
1076
1389
|
));
|
|
@@ -1078,7 +1391,7 @@ var KVService = class extends BaseService {
|
|
|
1078
1391
|
return err(
|
|
1079
1392
|
serviceError(
|
|
1080
1393
|
ErrorCodes.NETWORK_ERROR,
|
|
1081
|
-
`Failed to get key
|
|
1394
|
+
`Failed to get key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
|
|
1082
1395
|
"kv",
|
|
1083
1396
|
{ meta: { status: response.status, statusText: response.statusText } }
|
|
1084
1397
|
)
|
|
@@ -1095,6 +1408,8 @@ var KVService = class extends BaseService {
|
|
|
1095
1408
|
});
|
|
1096
1409
|
} catch (error) {
|
|
1097
1410
|
return err(wrapError("kv", error));
|
|
1411
|
+
} finally {
|
|
1412
|
+
request.dispose();
|
|
1098
1413
|
}
|
|
1099
1414
|
});
|
|
1100
1415
|
}
|
|
@@ -1122,33 +1437,33 @@ var KVService = class extends BaseService {
|
|
|
1122
1437
|
}
|
|
1123
1438
|
const path = this.getFullPath(key, options?.prefix);
|
|
1124
1439
|
const body = this.serializePutValue(value, options?.contentType);
|
|
1440
|
+
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
1125
1441
|
try {
|
|
1126
1442
|
const response = await this.invokeOperation(
|
|
1127
1443
|
path,
|
|
1128
1444
|
KVAction.PUT,
|
|
1129
1445
|
body,
|
|
1130
|
-
|
|
1446
|
+
request.signal,
|
|
1131
1447
|
{
|
|
1132
1448
|
...options?.ifMatch === void 0 ? {} : { "if-match": options.ifMatch },
|
|
1133
1449
|
...options?.ifNoneMatch === void 0 ? {} : { "if-none-match": options.ifNoneMatch }
|
|
1134
1450
|
}
|
|
1135
1451
|
);
|
|
1136
1452
|
if (response.status === 401 || response.status === 403) {
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
}));
|
|
1453
|
+
return this.authorizationFailure(
|
|
1454
|
+
`Failed to put key ${JSON.stringify(key)}`,
|
|
1455
|
+
response,
|
|
1456
|
+
await this.readAuthorizationText(response),
|
|
1457
|
+
[path],
|
|
1458
|
+
KVAction.PUT
|
|
1459
|
+
);
|
|
1145
1460
|
}
|
|
1146
1461
|
if (!response.ok) {
|
|
1147
1462
|
const errorText = await response.text();
|
|
1148
1463
|
if (response.status === 412) {
|
|
1149
1464
|
return err(serviceError(
|
|
1150
1465
|
ErrorCodes.KV_PRECONDITION_FAILED,
|
|
1151
|
-
`KV precondition failed for key
|
|
1466
|
+
`KV precondition failed for key ${JSON.stringify(key)}`,
|
|
1152
1467
|
"kv",
|
|
1153
1468
|
{ meta: { status: response.status, statusText: response.statusText } }
|
|
1154
1469
|
));
|
|
@@ -1156,7 +1471,7 @@ var KVService = class extends BaseService {
|
|
|
1156
1471
|
if (response.status === 503 && (options?.ifMatch !== void 0 || options?.ifNoneMatch !== void 0)) {
|
|
1157
1472
|
return err(serviceError(
|
|
1158
1473
|
ErrorCodes.KV_CONFLICT,
|
|
1159
|
-
`Concurrent KV update conflicted for key
|
|
1474
|
+
`Concurrent KV update conflicted for key ${JSON.stringify(key)}`,
|
|
1160
1475
|
"kv",
|
|
1161
1476
|
{ meta: { status: response.status, statusText: response.statusText } }
|
|
1162
1477
|
));
|
|
@@ -1172,7 +1487,7 @@ var KVService = class extends BaseService {
|
|
|
1172
1487
|
return err(
|
|
1173
1488
|
serviceError(
|
|
1174
1489
|
ErrorCodes.KV_WRITE_FAILED,
|
|
1175
|
-
`Failed to put key
|
|
1490
|
+
`Failed to put key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
|
|
1176
1491
|
"kv",
|
|
1177
1492
|
{ meta: { status: response.status, statusText: response.statusText } }
|
|
1178
1493
|
)
|
|
@@ -1184,6 +1499,8 @@ var KVService = class extends BaseService {
|
|
|
1184
1499
|
});
|
|
1185
1500
|
} catch (error) {
|
|
1186
1501
|
return err(wrapError("kv", error));
|
|
1502
|
+
} finally {
|
|
1503
|
+
request.dispose();
|
|
1187
1504
|
}
|
|
1188
1505
|
});
|
|
1189
1506
|
}
|
|
@@ -1223,6 +1540,7 @@ var KVService = class extends BaseService {
|
|
|
1223
1540
|
seen.add(path);
|
|
1224
1541
|
}
|
|
1225
1542
|
let requestMayHaveDispatched = false;
|
|
1543
|
+
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
1226
1544
|
try {
|
|
1227
1545
|
const body = new FormData();
|
|
1228
1546
|
for (let index = 0; index < items.length; index++) {
|
|
@@ -1245,7 +1563,7 @@ var KVService = class extends BaseService {
|
|
|
1245
1563
|
method: "POST",
|
|
1246
1564
|
headers,
|
|
1247
1565
|
body,
|
|
1248
|
-
signal:
|
|
1566
|
+
signal: request.signal
|
|
1249
1567
|
};
|
|
1250
1568
|
const fetchFn = this.context.fetch;
|
|
1251
1569
|
requestMayHaveDispatched = true;
|
|
@@ -1266,12 +1584,13 @@ var KVService = class extends BaseService {
|
|
|
1266
1584
|
);
|
|
1267
1585
|
}
|
|
1268
1586
|
if (response.status === 401 || response.status === 403) {
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1587
|
+
return this.authorizationFailure(
|
|
1588
|
+
`Failed to batch put ${items.length} key(s)`,
|
|
1589
|
+
response,
|
|
1590
|
+
errorText,
|
|
1591
|
+
paths,
|
|
1592
|
+
KVAction.PUT
|
|
1593
|
+
);
|
|
1275
1594
|
}
|
|
1276
1595
|
const quotaError = this.handleQuotaErrorResponse(
|
|
1277
1596
|
response,
|
|
@@ -1295,14 +1614,16 @@ var KVService = class extends BaseService {
|
|
|
1295
1614
|
rawBody = await response.json();
|
|
1296
1615
|
} catch (jsonError) {
|
|
1297
1616
|
const cause = jsonError instanceof Error ? jsonError : new Error(String(jsonError));
|
|
1617
|
+
const timedOut = isRequestTimeoutError(cause);
|
|
1298
1618
|
return err(
|
|
1299
1619
|
serviceError(
|
|
1300
|
-
ErrorCodes.NETWORK_ERROR,
|
|
1301
|
-
`KV batchPut response was not valid JSON: ${cause.message}`,
|
|
1620
|
+
timedOut ? ErrorCodes.TIMEOUT : ErrorCodes.NETWORK_ERROR,
|
|
1621
|
+
timedOut ? `KV batchPut response body did not arrive in time: ${cause.message}` : `KV batchPut response was not valid JSON: ${cause.message}`,
|
|
1302
1622
|
"kv",
|
|
1303
1623
|
{
|
|
1304
1624
|
cause,
|
|
1305
1625
|
meta: {
|
|
1626
|
+
...timedOut ? { timeoutMs: cause.timeoutMs } : {},
|
|
1306
1627
|
requestMayHaveDispatched: true,
|
|
1307
1628
|
responseReceived: true,
|
|
1308
1629
|
status: response.status,
|
|
@@ -1357,6 +1678,8 @@ var KVService = class extends BaseService {
|
|
|
1357
1678
|
...wrapped,
|
|
1358
1679
|
meta: { ...wrapped.meta, requestMayHaveDispatched }
|
|
1359
1680
|
});
|
|
1681
|
+
} finally {
|
|
1682
|
+
request.dispose();
|
|
1360
1683
|
}
|
|
1361
1684
|
});
|
|
1362
1685
|
}
|
|
@@ -1379,26 +1702,27 @@ var KVService = class extends BaseService {
|
|
|
1379
1702
|
if (options?.path) {
|
|
1380
1703
|
listPath = listPath ? `${listPath}/${options.path}` : options.path;
|
|
1381
1704
|
}
|
|
1705
|
+
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
1382
1706
|
try {
|
|
1383
1707
|
const response = await this.invokeOperation(
|
|
1384
1708
|
listPath,
|
|
1385
1709
|
KVAction.LIST,
|
|
1386
1710
|
void 0,
|
|
1387
|
-
|
|
1711
|
+
request.signal,
|
|
1388
1712
|
options?.limit === void 0 && options?.cursor === void 0 ? void 0 : {
|
|
1389
1713
|
...options?.limit === void 0 ? {} : { "x-tinycloud-limit": String(options.limit) },
|
|
1390
1714
|
...options?.cursor === void 0 ? {} : { "x-tinycloud-cursor": options.cursor }
|
|
1391
1715
|
}
|
|
1392
1716
|
);
|
|
1393
1717
|
if (!response.ok) {
|
|
1394
|
-
if (response.status === 401) {
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1718
|
+
if (response.status === 401 || response.status === 403) {
|
|
1719
|
+
return this.authorizationFailure(
|
|
1720
|
+
"Failed to list keys",
|
|
1721
|
+
response,
|
|
1722
|
+
await this.readAuthorizationText(response),
|
|
1723
|
+
[listPath],
|
|
1724
|
+
KVAction.LIST
|
|
1725
|
+
);
|
|
1402
1726
|
}
|
|
1403
1727
|
const errorText = await response.text();
|
|
1404
1728
|
return err(
|
|
@@ -1425,9 +1749,174 @@ var KVService = class extends BaseService {
|
|
|
1425
1749
|
});
|
|
1426
1750
|
} catch (error) {
|
|
1427
1751
|
return err(wrapError("kv", error));
|
|
1752
|
+
} finally {
|
|
1753
|
+
request.dispose();
|
|
1754
|
+
}
|
|
1755
|
+
});
|
|
1756
|
+
}
|
|
1757
|
+
/**
|
|
1758
|
+
* Read one page of the `tinycloud.kv/sync` change feed for a prefix.
|
|
1759
|
+
*/
|
|
1760
|
+
async changes(options) {
|
|
1761
|
+
return this.withTelemetry("changes", options?.prefix, async () => {
|
|
1762
|
+
if (!this.requireAuth()) {
|
|
1763
|
+
return err(authRequiredError("kv"));
|
|
1764
|
+
}
|
|
1765
|
+
if (typeof options?.prefix !== "string" || options.prefix.length === 0) {
|
|
1766
|
+
return err(serviceError(
|
|
1767
|
+
ErrorCodes.INVALID_INPUT,
|
|
1768
|
+
"KV changes prefix must be a non-empty string",
|
|
1769
|
+
"kv"
|
|
1770
|
+
));
|
|
1771
|
+
}
|
|
1772
|
+
if (options.limit !== void 0 && (!Number.isInteger(options.limit) || options.limit < 1 || options.limit > 1e3)) {
|
|
1773
|
+
return err(serviceError(
|
|
1774
|
+
ErrorCodes.INVALID_INPUT,
|
|
1775
|
+
"KV changes limit must be an integer from 1 through 1000",
|
|
1776
|
+
"kv"
|
|
1777
|
+
));
|
|
1778
|
+
}
|
|
1779
|
+
const headers = {};
|
|
1780
|
+
if (options.limit !== void 0) headers["x-tinycloud-limit"] = String(options.limit);
|
|
1781
|
+
if (options.cursor !== void 0) headers["x-tinycloud-cursor"] = options.cursor;
|
|
1782
|
+
if (options.retentionGrant !== void 0) {
|
|
1783
|
+
headers["x-tinycloud-retention-grant"] = options.retentionGrant;
|
|
1784
|
+
}
|
|
1785
|
+
const request = this.createRequestSignal(options.signal, options.timeout);
|
|
1786
|
+
try {
|
|
1787
|
+
const response = await this.invokeOperation(
|
|
1788
|
+
options.prefix,
|
|
1789
|
+
KVAction.SYNC,
|
|
1790
|
+
void 0,
|
|
1791
|
+
request.signal,
|
|
1792
|
+
headers
|
|
1793
|
+
);
|
|
1794
|
+
if (!response.ok) {
|
|
1795
|
+
return await this.changesFailure(response, options.prefix, request.signal);
|
|
1796
|
+
}
|
|
1797
|
+
const text = await response.text();
|
|
1798
|
+
let body;
|
|
1799
|
+
try {
|
|
1800
|
+
body = JSON.parse(text);
|
|
1801
|
+
} catch {
|
|
1802
|
+
body = void 0;
|
|
1803
|
+
}
|
|
1804
|
+
const page = this.normalizeChangesResponse(body);
|
|
1805
|
+
if (!page) {
|
|
1806
|
+
return err(serviceError(
|
|
1807
|
+
ErrorCodes.NETWORK_ERROR,
|
|
1808
|
+
"KV changes response was not a tinycloud.kv/sync page",
|
|
1809
|
+
"kv",
|
|
1810
|
+
{ meta: { status: response.status, statusText: response.statusText } }
|
|
1811
|
+
));
|
|
1812
|
+
}
|
|
1813
|
+
return ok(page);
|
|
1814
|
+
} catch (error) {
|
|
1815
|
+
return err(wrapError("kv", error));
|
|
1816
|
+
} finally {
|
|
1817
|
+
request.dispose();
|
|
1428
1818
|
}
|
|
1429
1819
|
});
|
|
1430
1820
|
}
|
|
1821
|
+
/**
|
|
1822
|
+
* Map a failed `kv/sync` response. 410 and the retention 403 carry
|
|
1823
|
+
* `{"error":{"code","reason"}}`; a revoked grant is a 401 whose whole body
|
|
1824
|
+
* is the node's revocation error (see REVOKED_BODY). A body read that
|
|
1825
|
+
* fails because the request was cancelled or timed out rethrows, so the
|
|
1826
|
+
* caller sees `ABORTED`/`TIMEOUT` rather than a mapped status.
|
|
1827
|
+
*/
|
|
1828
|
+
async changesFailure(response, prefix, signal) {
|
|
1829
|
+
if (response.status === 404) {
|
|
1830
|
+
return this.classifyNotFound(response, prefix);
|
|
1831
|
+
}
|
|
1832
|
+
let errorText;
|
|
1833
|
+
try {
|
|
1834
|
+
errorText = await response.text();
|
|
1835
|
+
} catch (error) {
|
|
1836
|
+
if (signal.aborted) throw error;
|
|
1837
|
+
errorText = "";
|
|
1838
|
+
}
|
|
1839
|
+
const meta = { status: response.status, statusText: response.statusText };
|
|
1840
|
+
let reason;
|
|
1841
|
+
let bodyCode;
|
|
1842
|
+
try {
|
|
1843
|
+
const body = JSON.parse(errorText);
|
|
1844
|
+
const error = isJsonObject(body) ? body.error : void 0;
|
|
1845
|
+
if (isJsonObject(error)) {
|
|
1846
|
+
if (typeof error.code === "string") bodyCode = error.code;
|
|
1847
|
+
if (typeof error.reason === "string") reason = error.reason;
|
|
1848
|
+
}
|
|
1849
|
+
} catch {
|
|
1850
|
+
}
|
|
1851
|
+
const context = `Failed to read KV changes for ${JSON.stringify(prefix)}`;
|
|
1852
|
+
if (response.status === 410) {
|
|
1853
|
+
return err(serviceError(
|
|
1854
|
+
ErrorCodes.KV_SYNC_RESET_REQUIRED,
|
|
1855
|
+
`${context}: the node requires a reset (${reason ?? "reason not given"}); discard sync state and restart without a cursor`,
|
|
1856
|
+
"kv",
|
|
1857
|
+
{ meta: { ...meta, reason } }
|
|
1858
|
+
));
|
|
1859
|
+
}
|
|
1860
|
+
if (response.status === 403 && bodyCode === "RETENTION_GRANT_REFUSED") {
|
|
1861
|
+
return err(serviceError(
|
|
1862
|
+
ErrorCodes.KV_RETENTION_GRANT_REFUSED,
|
|
1863
|
+
`${context}: the retention grant was refused (${reason ?? "reason not given"})`,
|
|
1864
|
+
"kv",
|
|
1865
|
+
{ meta: { ...meta, reason } }
|
|
1866
|
+
));
|
|
1867
|
+
}
|
|
1868
|
+
if (response.status === 401) {
|
|
1869
|
+
const revoked = revocationCodeOf(errorText);
|
|
1870
|
+
if (revoked !== void 0) {
|
|
1871
|
+
return err(serviceError(revoked, `${context}: 401 - ${errorText}`, "kv", { meta }));
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
if (response.status === 401 || response.status === 403) {
|
|
1875
|
+
return this.authorizationFailure(context, response, errorText, [prefix], KVAction.SYNC);
|
|
1876
|
+
}
|
|
1877
|
+
return err(serviceError(
|
|
1878
|
+
response.status === 400 ? ErrorCodes.INVALID_INPUT : ErrorCodes.NETWORK_ERROR,
|
|
1879
|
+
`${context}: ${response.status} - ${errorText}`,
|
|
1880
|
+
"kv",
|
|
1881
|
+
{ meta }
|
|
1882
|
+
));
|
|
1883
|
+
}
|
|
1884
|
+
normalizeChangesResponse(data) {
|
|
1885
|
+
if (!isJsonObject(data)) return void 0;
|
|
1886
|
+
const { changes, more, cursor, source, authority } = data;
|
|
1887
|
+
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)) {
|
|
1888
|
+
return void 0;
|
|
1889
|
+
}
|
|
1890
|
+
const { notBefore, expiresAt, retainUntil } = authority;
|
|
1891
|
+
if (notBefore !== null && typeof notBefore !== "string" || expiresAt !== null && typeof expiresAt !== "string" || retainUntil !== null && typeof retainUntil !== "string") {
|
|
1892
|
+
return void 0;
|
|
1893
|
+
}
|
|
1894
|
+
const parsed = [];
|
|
1895
|
+
for (const change of changes) {
|
|
1896
|
+
if (!isJsonObject(change) || typeof change.key !== "string") return void 0;
|
|
1897
|
+
if (change.deleted === true) {
|
|
1898
|
+
parsed.push({ key: change.key, deleted: true });
|
|
1899
|
+
continue;
|
|
1900
|
+
}
|
|
1901
|
+
const { metadata } = change;
|
|
1902
|
+
if (change.deleted !== false || typeof change.etag !== "string" || !isJsonObject(metadata) || !Object.values(metadata).every((value) => typeof value === "string")) {
|
|
1903
|
+
return void 0;
|
|
1904
|
+
}
|
|
1905
|
+
parsed.push({
|
|
1906
|
+
key: change.key,
|
|
1907
|
+
deleted: false,
|
|
1908
|
+
etag: change.etag,
|
|
1909
|
+
metadata
|
|
1910
|
+
});
|
|
1911
|
+
}
|
|
1912
|
+
return {
|
|
1913
|
+
changes: parsed,
|
|
1914
|
+
more,
|
|
1915
|
+
cursor,
|
|
1916
|
+
source: { nodeDid: source.nodeDid, space: source.space, prefix: source.prefix },
|
|
1917
|
+
authority: { notBefore, expiresAt, retainUntil }
|
|
1918
|
+
};
|
|
1919
|
+
}
|
|
1431
1920
|
/**
|
|
1432
1921
|
* Delete a key.
|
|
1433
1922
|
*/
|
|
@@ -1437,23 +1926,24 @@ var KVService = class extends BaseService {
|
|
|
1437
1926
|
return err(authRequiredError("kv"));
|
|
1438
1927
|
}
|
|
1439
1928
|
const path = this.getFullPath(key, options?.prefix);
|
|
1929
|
+
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
1440
1930
|
try {
|
|
1441
1931
|
const response = await this.invokeOperation(
|
|
1442
1932
|
path,
|
|
1443
1933
|
KVAction.DELETE,
|
|
1444
1934
|
void 0,
|
|
1445
|
-
|
|
1935
|
+
request.signal,
|
|
1446
1936
|
options?.ifMatch === void 0 ? void 0 : { "if-match": options.ifMatch }
|
|
1447
1937
|
);
|
|
1448
1938
|
if (!response.ok) {
|
|
1449
|
-
if (response.status === 401) {
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1939
|
+
if (response.status === 401 || response.status === 403) {
|
|
1940
|
+
return this.authorizationFailure(
|
|
1941
|
+
`Failed to delete key ${JSON.stringify(key)}`,
|
|
1942
|
+
response,
|
|
1943
|
+
await this.readAuthorizationText(response),
|
|
1944
|
+
[path],
|
|
1945
|
+
KVAction.DELETE
|
|
1946
|
+
);
|
|
1457
1947
|
}
|
|
1458
1948
|
if (response.status === 404) {
|
|
1459
1949
|
return this.classifyNotFound(response, key);
|
|
@@ -1462,7 +1952,7 @@ var KVService = class extends BaseService {
|
|
|
1462
1952
|
if (response.status === 412) {
|
|
1463
1953
|
return err(serviceError(
|
|
1464
1954
|
ErrorCodes.KV_PRECONDITION_FAILED,
|
|
1465
|
-
`KV precondition failed for key
|
|
1955
|
+
`KV precondition failed for key ${JSON.stringify(key)}`,
|
|
1466
1956
|
"kv",
|
|
1467
1957
|
{ meta: { status: response.status, statusText: response.statusText } }
|
|
1468
1958
|
));
|
|
@@ -1470,7 +1960,7 @@ var KVService = class extends BaseService {
|
|
|
1470
1960
|
if (response.status === 503 && options?.ifMatch !== void 0) {
|
|
1471
1961
|
return err(serviceError(
|
|
1472
1962
|
ErrorCodes.KV_CONFLICT,
|
|
1473
|
-
`Concurrent KV delete conflicted for key
|
|
1963
|
+
`Concurrent KV delete conflicted for key ${JSON.stringify(key)}`,
|
|
1474
1964
|
"kv",
|
|
1475
1965
|
{ meta: { status: response.status, statusText: response.statusText } }
|
|
1476
1966
|
));
|
|
@@ -1478,7 +1968,7 @@ var KVService = class extends BaseService {
|
|
|
1478
1968
|
return err(
|
|
1479
1969
|
serviceError(
|
|
1480
1970
|
ErrorCodes.NETWORK_ERROR,
|
|
1481
|
-
`Failed to delete key
|
|
1971
|
+
`Failed to delete key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
|
|
1482
1972
|
"kv",
|
|
1483
1973
|
{ meta: { status: response.status, statusText: response.statusText } }
|
|
1484
1974
|
)
|
|
@@ -1490,6 +1980,8 @@ var KVService = class extends BaseService {
|
|
|
1490
1980
|
});
|
|
1491
1981
|
} catch (error) {
|
|
1492
1982
|
return err(wrapError("kv", error));
|
|
1983
|
+
} finally {
|
|
1984
|
+
request.dispose();
|
|
1493
1985
|
}
|
|
1494
1986
|
});
|
|
1495
1987
|
}
|
|
@@ -1502,22 +1994,23 @@ var KVService = class extends BaseService {
|
|
|
1502
1994
|
return err(authRequiredError("kv"));
|
|
1503
1995
|
}
|
|
1504
1996
|
const path = this.getFullPath(key, options?.prefix);
|
|
1997
|
+
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
1505
1998
|
try {
|
|
1506
1999
|
const response = await this.invokeOperation(
|
|
1507
2000
|
path,
|
|
1508
2001
|
KVAction.HEAD,
|
|
1509
2002
|
void 0,
|
|
1510
|
-
|
|
2003
|
+
request.signal
|
|
1511
2004
|
);
|
|
1512
2005
|
if (!response.ok) {
|
|
1513
|
-
if (response.status === 401) {
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
2006
|
+
if (response.status === 401 || response.status === 403) {
|
|
2007
|
+
return this.authorizationFailure(
|
|
2008
|
+
`Failed to get metadata for key ${JSON.stringify(key)}`,
|
|
2009
|
+
response,
|
|
2010
|
+
await this.readAuthorizationText(response),
|
|
2011
|
+
[path],
|
|
2012
|
+
KVAction.HEAD
|
|
2013
|
+
);
|
|
1521
2014
|
}
|
|
1522
2015
|
if (response.status === 404) {
|
|
1523
2016
|
return this.classifyNotFound(response, key);
|
|
@@ -1526,7 +2019,7 @@ var KVService = class extends BaseService {
|
|
|
1526
2019
|
return err(
|
|
1527
2020
|
serviceError(
|
|
1528
2021
|
ErrorCodes.NETWORK_ERROR,
|
|
1529
|
-
`Failed to get metadata for key
|
|
2022
|
+
`Failed to get metadata for key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
|
|
1530
2023
|
"kv",
|
|
1531
2024
|
{ meta: { status: response.status, statusText: response.statusText } }
|
|
1532
2025
|
)
|
|
@@ -1538,6 +2031,8 @@ var KVService = class extends BaseService {
|
|
|
1538
2031
|
});
|
|
1539
2032
|
} catch (error) {
|
|
1540
2033
|
return err(wrapError("kv", error));
|
|
2034
|
+
} finally {
|
|
2035
|
+
request.dispose();
|
|
1541
2036
|
}
|
|
1542
2037
|
});
|
|
1543
2038
|
}
|
|
@@ -1575,15 +2070,16 @@ var KVService = class extends BaseService {
|
|
|
1575
2070
|
if (options?.etag !== void 0) {
|
|
1576
2071
|
body.etag = options.etag;
|
|
1577
2072
|
}
|
|
2073
|
+
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
1578
2074
|
try {
|
|
1579
2075
|
const response = await this.context.fetch(`${this.host}/signed/kv`, {
|
|
1580
2076
|
method: "POST",
|
|
1581
2077
|
headers: this.withJsonContentType(headers),
|
|
1582
2078
|
body: JSON.stringify(body),
|
|
1583
|
-
signal:
|
|
2079
|
+
signal: request.signal
|
|
1584
2080
|
});
|
|
1585
2081
|
if (!response.ok) {
|
|
1586
|
-
return this.createSignedReadUrlError(response, key);
|
|
2082
|
+
return this.createSignedReadUrlError(response, key, path);
|
|
1587
2083
|
}
|
|
1588
2084
|
const signedUrl = this.normalizeSignedReadUrlResponse(
|
|
1589
2085
|
await response.json()
|
|
@@ -1600,6 +2096,8 @@ var KVService = class extends BaseService {
|
|
|
1600
2096
|
return ok(signedUrl);
|
|
1601
2097
|
} catch (error) {
|
|
1602
2098
|
return err(wrapError("kv", error));
|
|
2099
|
+
} finally {
|
|
2100
|
+
request.dispose();
|
|
1603
2101
|
}
|
|
1604
2102
|
});
|
|
1605
2103
|
}
|