@tinycloud/sdk-services 3.1.0-beta.2 → 3.1.0-beta.7
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-C8zRy0ww.d.cts} +73 -1
- package/dist/{BaseService-CuV-ZVPs.d.ts → BaseService-DU-lE-Qx.d.ts} +73 -1
- package/dist/encryption/index.cjs +191 -100
- package/dist/encryption/index.cjs.map +1 -1
- package/dist/encryption/index.d.cts +1 -1
- package/dist/encryption/index.d.ts +1 -1
- package/dist/encryption/index.js +191 -100
- package/dist/encryption/index.js.map +1 -1
- package/dist/index.cjs +446 -177
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +29 -39
- package/dist/index.d.ts +29 -39
- package/dist/index.js +426 -159
- package/dist/index.js.map +1 -1
- package/dist/kv/index.cjs +333 -35
- package/dist/kv/index.cjs.map +1 -1
- package/dist/kv/index.d.cts +26 -12
- package/dist/kv/index.d.ts +26 -12
- package/dist/kv/index.js +333 -35
- package/dist/kv/index.js.map +1 -1
- package/dist/sql/index.cjs +139 -30
- package/dist/sql/index.cjs.map +1 -1
- package/dist/sql/index.d.cts +5 -2
- package/dist/sql/index.d.ts +5 -2
- package/dist/sql/index.js +130 -21
- package/dist/sql/index.js.map +1 -1
- package/package.json +1 -1
package/dist/kv/index.cjs
CHANGED
|
@@ -99,6 +99,55 @@ function serviceError(code, message, service, options) {
|
|
|
99
99
|
};
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
// src/errors.ts
|
|
103
|
+
var import_bootstrap = require("@tinycloud/bootstrap");
|
|
104
|
+
|
|
105
|
+
// src/encryption/networkId.ts
|
|
106
|
+
var URN_PREFIX = "urn:tinycloud:encryption:";
|
|
107
|
+
var NETWORK_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
108
|
+
var NetworkIdError = class extends Error {
|
|
109
|
+
constructor(message) {
|
|
110
|
+
super(message);
|
|
111
|
+
this.name = "NetworkIdError";
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
function parseNetworkId(networkId) {
|
|
115
|
+
if (typeof networkId !== "string" || networkId.length === 0) {
|
|
116
|
+
throw new NetworkIdError("networkId must be a non-empty string");
|
|
117
|
+
}
|
|
118
|
+
if (!networkId.startsWith(URN_PREFIX)) {
|
|
119
|
+
throw new NetworkIdError(
|
|
120
|
+
`networkId must start with ${URN_PREFIX} (got ${JSON.stringify(networkId)})`
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
const body = networkId.slice(URN_PREFIX.length);
|
|
124
|
+
const lastColon = body.lastIndexOf(":");
|
|
125
|
+
if (lastColon <= 0 || lastColon === body.length - 1) {
|
|
126
|
+
throw new NetworkIdError(
|
|
127
|
+
`networkId missing ownerDid or name segment (got ${JSON.stringify(networkId)})`
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
const ownerDid = body.slice(0, lastColon);
|
|
131
|
+
const name = body.slice(lastColon + 1);
|
|
132
|
+
if (!ownerDid.startsWith("did:")) {
|
|
133
|
+
throw new NetworkIdError(
|
|
134
|
+
`networkId ownerDid must be a DID (got ${JSON.stringify(ownerDid)})`
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
const didParts = ownerDid.split(":");
|
|
138
|
+
if (didParts.length < 3 || didParts.some((p) => p.length === 0)) {
|
|
139
|
+
throw new NetworkIdError(
|
|
140
|
+
`networkId ownerDid is not a well-formed DID (got ${JSON.stringify(ownerDid)})`
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
if (!NETWORK_NAME_RE.test(name)) {
|
|
144
|
+
throw new NetworkIdError(
|
|
145
|
+
`networkId name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
return { networkId, ownerDid, name };
|
|
149
|
+
}
|
|
150
|
+
|
|
102
151
|
// src/errors.ts
|
|
103
152
|
function parsePermissionHint(value) {
|
|
104
153
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
@@ -143,11 +192,31 @@ function authRequiredError(service) {
|
|
|
143
192
|
service
|
|
144
193
|
};
|
|
145
194
|
}
|
|
146
|
-
|
|
195
|
+
var RequestTimeoutError = class extends Error {
|
|
196
|
+
constructor(timeoutMs) {
|
|
197
|
+
super(`Request timed out after ${timeoutMs}ms.`);
|
|
198
|
+
this.name = "TimeoutError";
|
|
199
|
+
this.timeoutMs = timeoutMs;
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
function isRequestTimeoutError(error) {
|
|
203
|
+
return error instanceof RequestTimeoutError || error instanceof Error && error.name === "TimeoutError" && typeof error.timeoutMs === "number";
|
|
204
|
+
}
|
|
205
|
+
function timeoutError(service, timeoutMs, cause) {
|
|
206
|
+
if (timeoutMs === void 0) {
|
|
207
|
+
return {
|
|
208
|
+
code: ErrorCodes.TIMEOUT,
|
|
209
|
+
message: "Request timed out.",
|
|
210
|
+
service,
|
|
211
|
+
...cause === void 0 ? {} : { cause }
|
|
212
|
+
};
|
|
213
|
+
}
|
|
147
214
|
return {
|
|
148
215
|
code: ErrorCodes.TIMEOUT,
|
|
149
|
-
message:
|
|
150
|
-
service
|
|
216
|
+
message: `Request timed out after ${timeoutMs}ms.`,
|
|
217
|
+
service,
|
|
218
|
+
...cause === void 0 ? {} : { cause },
|
|
219
|
+
meta: { timeoutMs }
|
|
151
220
|
};
|
|
152
221
|
}
|
|
153
222
|
function abortedError(service) {
|
|
@@ -167,6 +236,101 @@ function parseAuthError(responseText) {
|
|
|
167
236
|
function authUnauthorizedError(service, message, meta) {
|
|
168
237
|
return serviceError(ErrorCodes.AUTH_UNAUTHORIZED, message, service, { meta });
|
|
169
238
|
}
|
|
239
|
+
var grantableActions = new Set(
|
|
240
|
+
import_bootstrap.CAPABILITY_REGISTRY.map(({ urn }) => urn).filter((urn) => !urn.endsWith("/*"))
|
|
241
|
+
);
|
|
242
|
+
var capabilityServices = new Set(
|
|
243
|
+
import_bootstrap.CAPABILITY_REGISTRY.map(({ service }) => service.slice("tinycloud.".length))
|
|
244
|
+
);
|
|
245
|
+
function parseCapabilityResource(resource, service) {
|
|
246
|
+
if (resource.length === 0 || resource.length > 1024 || !capabilityServices.has(service) || /[\s\p{C}*#]/u.test(resource) || resource.includes("://")) return void 0;
|
|
247
|
+
const segments = resource.split("/");
|
|
248
|
+
if (!/^tinycloud:[A-Za-z0-9][A-Za-z0-9:._-]*$/.test(segments[0])) return void 0;
|
|
249
|
+
const serviceIndex = segments.findIndex(
|
|
250
|
+
(segment, index) => index > 0 && capabilityServices.has(segment)
|
|
251
|
+
);
|
|
252
|
+
if (serviceIndex < 1 || segments[serviceIndex] !== service) return void 0;
|
|
253
|
+
if (segments.slice(1, serviceIndex).some(
|
|
254
|
+
(segment) => segment.length === 0 || segment === "." || segment === ".."
|
|
255
|
+
)) return void 0;
|
|
256
|
+
const pathSegments = segments.slice(serviceIndex + 1);
|
|
257
|
+
if (pathSegments.some((segment) => segment === "." || segment === "..")) return void 0;
|
|
258
|
+
if (service !== "kv" && (pathSegments.length === 0 || pathSegments[pathSegments.length - 1] === "")) return void 0;
|
|
259
|
+
return {
|
|
260
|
+
space: segments.slice(0, serviceIndex).join("/"),
|
|
261
|
+
path: pathSegments.join("/")
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
function safeCapabilityResource(value, service, action) {
|
|
265
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 1024) return false;
|
|
266
|
+
if (service === "encryption" && value.startsWith("urn:tinycloud:encryption:")) {
|
|
267
|
+
if (!/^[A-Za-z0-9_][A-Za-z0-9_.:/-]*$/.test(value) || value.includes("//") || value.split("/").some((segment) => segment === "." || segment === "..")) return false;
|
|
268
|
+
try {
|
|
269
|
+
parseNetworkId(value);
|
|
270
|
+
return true;
|
|
271
|
+
} catch {
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
const parsed = parseCapabilityResource(value, service);
|
|
276
|
+
return parsed !== void 0 && (!parsed.path.endsWith("/") || service === "kv" && action === "tinycloud.kv/list") && (parsed.path !== "" || service === "kv" && action === "tinycloud.kv/list");
|
|
277
|
+
}
|
|
278
|
+
function validatedCapabilityOf(error) {
|
|
279
|
+
const verdict = authorizationVerdictOf(error);
|
|
280
|
+
if (verdict !== "unauthenticated" && verdict !== "forbidden") return void 0;
|
|
281
|
+
const seen = /* @__PURE__ */ new Set();
|
|
282
|
+
let node = error;
|
|
283
|
+
for (let depth = 0; depth < MAX_CAUSE_DEPTH && typeof node === "object" && node !== null && !seen.has(node); depth += 1) {
|
|
284
|
+
seen.add(node);
|
|
285
|
+
const record = node;
|
|
286
|
+
const status = errorStatusOf(record);
|
|
287
|
+
const meta = record.meta;
|
|
288
|
+
if ((status === 401 || status === 403 || status === void 0 && record.code === ErrorCodes.AUTH_UNAUTHORIZED) && typeof meta === "object" && meta !== null && !Array.isArray(meta)) {
|
|
289
|
+
const metadata = meta;
|
|
290
|
+
const { resource, requiredAction } = metadata;
|
|
291
|
+
if (typeof requiredAction === "string" && grantableActions.has(requiredAction)) {
|
|
292
|
+
const actionService = requiredAction.slice("tinycloud.".length, requiredAction.indexOf("/"));
|
|
293
|
+
const explicitService = record.service === void 0 ? metadata.service : record.service;
|
|
294
|
+
if (safeCapabilityResource(resource, actionService, requiredAction) && (explicitService === void 0 || explicitService === actionService || explicitService === `tinycloud.${actionService}`)) {
|
|
295
|
+
return { resource, requiredAction };
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
node = record.cause;
|
|
300
|
+
}
|
|
301
|
+
return void 0;
|
|
302
|
+
}
|
|
303
|
+
var MAX_CAUSE_DEPTH = 8;
|
|
304
|
+
function errorStatusOf(node) {
|
|
305
|
+
const meta = node.meta;
|
|
306
|
+
const candidates = [
|
|
307
|
+
node.status,
|
|
308
|
+
node.statusCode,
|
|
309
|
+
typeof meta === "object" && meta !== null ? meta.status : void 0
|
|
310
|
+
];
|
|
311
|
+
for (const candidate of candidates) {
|
|
312
|
+
if (typeof candidate === "number" && Number.isInteger(candidate) && candidate >= 400 && candidate <= 599) {
|
|
313
|
+
return candidate;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return void 0;
|
|
317
|
+
}
|
|
318
|
+
function authorizationVerdictOf(error) {
|
|
319
|
+
const seen = /* @__PURE__ */ new Set();
|
|
320
|
+
let sawUnauthorizedCode = false;
|
|
321
|
+
let node = error;
|
|
322
|
+
for (let depth = 0; depth < MAX_CAUSE_DEPTH && typeof node === "object" && node !== null && !seen.has(node); depth += 1) {
|
|
323
|
+
seen.add(node);
|
|
324
|
+
const record = node;
|
|
325
|
+
const status = errorStatusOf(record);
|
|
326
|
+
if (status === 401) return "unauthenticated";
|
|
327
|
+
if (status === 403) return "forbidden";
|
|
328
|
+
if (status !== void 0) return "other";
|
|
329
|
+
if (record.code === ErrorCodes.AUTH_UNAUTHORIZED) sawUnauthorizedCode = true;
|
|
330
|
+
node = record.cause;
|
|
331
|
+
}
|
|
332
|
+
return sawUnauthorizedCode ? "forbidden" : void 0;
|
|
333
|
+
}
|
|
170
334
|
function storageQuotaExceededError(service, message, meta) {
|
|
171
335
|
return {
|
|
172
336
|
code: ErrorCodes.STORAGE_QUOTA_EXCEEDED,
|
|
@@ -199,6 +363,9 @@ function storageRejectionError(service, status, meta, responseText) {
|
|
|
199
363
|
}
|
|
200
364
|
function wrapError(service, error, defaultCode = ErrorCodes.NETWORK_ERROR) {
|
|
201
365
|
if (error instanceof Error) {
|
|
366
|
+
if (isRequestTimeoutError(error)) {
|
|
367
|
+
return timeoutError(service, error.timeoutMs, error);
|
|
368
|
+
}
|
|
202
369
|
if (error.name === "AbortError") {
|
|
203
370
|
return abortedError(service);
|
|
204
371
|
}
|
|
@@ -220,6 +387,7 @@ function wrapError(service, error, defaultCode = ErrorCodes.NETWORK_ERROR) {
|
|
|
220
387
|
}
|
|
221
388
|
|
|
222
389
|
// src/base/BaseService.ts
|
|
390
|
+
var MAX_TIMER_DELAY_MS = 2147483647;
|
|
223
391
|
var BaseService = class {
|
|
224
392
|
constructor() {
|
|
225
393
|
/**
|
|
@@ -400,6 +568,64 @@ var BaseService = class {
|
|
|
400
568
|
}
|
|
401
569
|
return controller.signal;
|
|
402
570
|
}
|
|
571
|
+
/**
|
|
572
|
+
* Create the abort signal for one request, bounded by the request timeout.
|
|
573
|
+
*
|
|
574
|
+
* The signal aborts when the service signs out, the context aborts, the
|
|
575
|
+
* caller's `signal` aborts, or the timeout elapses, whichever comes first.
|
|
576
|
+
* A timeout aborts with a {@link RequestTimeoutError} (`TimeoutError`), which
|
|
577
|
+
* `wrapError` maps to `ErrorCodes.TIMEOUT`; the other sources keep their own
|
|
578
|
+
* reason (normally `AbortError`, mapped to `ErrorCodes.ABORTED`).
|
|
579
|
+
*
|
|
580
|
+
* The timeout is `timeoutMs` when given, otherwise `config.timeout`. Only a
|
|
581
|
+
* positive, finite value (at most 2^31-1 ms) applies; `undefined`, `0`, and
|
|
582
|
+
* `Infinity` mean no timeout, so a per-call `0` opts out of a configured one.
|
|
583
|
+
*
|
|
584
|
+
* Callers must call `dispose()` once the response body has been consumed,
|
|
585
|
+
* which clears the timer and removes the listeners this adds to the
|
|
586
|
+
* long-lived service and context signals.
|
|
587
|
+
*
|
|
588
|
+
* @param signal - The caller's abort signal for this request
|
|
589
|
+
* @param timeoutMs - Per-request timeout override in milliseconds
|
|
590
|
+
*/
|
|
591
|
+
createRequestSignal(signal, timeoutMs) {
|
|
592
|
+
const controller = new AbortController();
|
|
593
|
+
const detachers = [];
|
|
594
|
+
let timer;
|
|
595
|
+
const dispose = () => {
|
|
596
|
+
if (timer !== void 0) {
|
|
597
|
+
clearTimeout(timer);
|
|
598
|
+
timer = void 0;
|
|
599
|
+
}
|
|
600
|
+
for (const detach of detachers.splice(0)) {
|
|
601
|
+
detach();
|
|
602
|
+
}
|
|
603
|
+
};
|
|
604
|
+
const abort = (reason) => {
|
|
605
|
+
dispose();
|
|
606
|
+
controller.abort(reason);
|
|
607
|
+
};
|
|
608
|
+
const requestSignal = { signal: controller.signal, dispose };
|
|
609
|
+
const parents = [
|
|
610
|
+
this.abortController.signal,
|
|
611
|
+
this.context?.abortSignal,
|
|
612
|
+
signal
|
|
613
|
+
].filter((parent) => parent !== void 0);
|
|
614
|
+
for (const parent of parents) {
|
|
615
|
+
if (parent.aborted) {
|
|
616
|
+
abort(parent.reason);
|
|
617
|
+
return requestSignal;
|
|
618
|
+
}
|
|
619
|
+
const onAbort = () => abort(parent.reason);
|
|
620
|
+
parent.addEventListener("abort", onAbort, { once: true });
|
|
621
|
+
detachers.push(() => parent.removeEventListener("abort", onAbort));
|
|
622
|
+
}
|
|
623
|
+
const timeout = timeoutMs ?? this._config.timeout;
|
|
624
|
+
if (typeof timeout === "number" && timeout > 0 && timeout <= MAX_TIMER_DELAY_MS) {
|
|
625
|
+
timer = setTimeout(() => abort(new RequestTimeoutError(timeout)), timeout);
|
|
626
|
+
}
|
|
627
|
+
return requestSignal;
|
|
628
|
+
}
|
|
403
629
|
/**
|
|
404
630
|
* Wrap an operation with error handling and telemetry.
|
|
405
631
|
*
|
|
@@ -660,7 +886,7 @@ var KVService = class extends BaseService {
|
|
|
660
886
|
* @param path - Resource path
|
|
661
887
|
* @param action - KV action
|
|
662
888
|
* @param body - Optional request body
|
|
663
|
-
* @param signal -
|
|
889
|
+
* @param signal - Request signal from createRequestSignal()
|
|
664
890
|
* @returns Fetch response
|
|
665
891
|
*/
|
|
666
892
|
async invokeOperation(path, action, body, signal, extraHeaders) {
|
|
@@ -676,7 +902,7 @@ var KVService = class extends BaseService {
|
|
|
676
902
|
method: "POST",
|
|
677
903
|
headers: requestHeaders,
|
|
678
904
|
body,
|
|
679
|
-
signal
|
|
905
|
+
signal
|
|
680
906
|
});
|
|
681
907
|
}
|
|
682
908
|
/**
|
|
@@ -853,6 +1079,7 @@ var KVService = class extends BaseService {
|
|
|
853
1079
|
"kv"
|
|
854
1080
|
));
|
|
855
1081
|
}
|
|
1082
|
+
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
856
1083
|
try {
|
|
857
1084
|
const session = this.context.session;
|
|
858
1085
|
const invocationHeaders = this.context.invokeAny(
|
|
@@ -874,15 +1101,17 @@ var KVService = class extends BaseService {
|
|
|
874
1101
|
const response = await this.context.fetch(`${this.host}/invoke`, {
|
|
875
1102
|
method: "POST",
|
|
876
1103
|
headers,
|
|
877
|
-
signal:
|
|
1104
|
+
signal: request.signal
|
|
878
1105
|
});
|
|
879
1106
|
if (!response.ok) {
|
|
880
|
-
const errorText = await response.text();
|
|
1107
|
+
const errorText = response.status === 401 || response.status === 403 ? await this.readAuthorizationText(response) : await response.text();
|
|
881
1108
|
if (response.status === 401 || response.status === 403) {
|
|
882
1109
|
return this.authorizationFailure(
|
|
883
1110
|
`Failed to batch read ${keys.length} key(s)`,
|
|
884
1111
|
response,
|
|
885
|
-
errorText
|
|
1112
|
+
errorText,
|
|
1113
|
+
paths,
|
|
1114
|
+
action
|
|
886
1115
|
);
|
|
887
1116
|
}
|
|
888
1117
|
if (response.status === 413) {
|
|
@@ -937,6 +1166,8 @@ var KVService = class extends BaseService {
|
|
|
937
1166
|
return ok({ results, count: results.length });
|
|
938
1167
|
} catch (error) {
|
|
939
1168
|
return err(wrapError("kv", error));
|
|
1169
|
+
} finally {
|
|
1170
|
+
request.dispose();
|
|
940
1171
|
}
|
|
941
1172
|
}
|
|
942
1173
|
/**
|
|
@@ -972,7 +1203,7 @@ var KVService = class extends BaseService {
|
|
|
972
1203
|
return text;
|
|
973
1204
|
}
|
|
974
1205
|
}
|
|
975
|
-
async createSignedReadUrlError(response, key) {
|
|
1206
|
+
async createSignedReadUrlError(response, key, path) {
|
|
976
1207
|
let errorText = response.statusText;
|
|
977
1208
|
try {
|
|
978
1209
|
const text = await response.text();
|
|
@@ -985,7 +1216,9 @@ var KVService = class extends BaseService {
|
|
|
985
1216
|
return this.authorizationFailure(
|
|
986
1217
|
`Failed to create signed read URL for key ${JSON.stringify(key)}`,
|
|
987
1218
|
response,
|
|
988
|
-
errorText
|
|
1219
|
+
errorText,
|
|
1220
|
+
[path],
|
|
1221
|
+
KVAction.GET
|
|
989
1222
|
);
|
|
990
1223
|
}
|
|
991
1224
|
const code = response.status === 400 ? ErrorCodes.INVALID_INPUT : ErrorCodes.NETWORK_ERROR;
|
|
@@ -998,21 +1231,42 @@ var KVService = class extends BaseService {
|
|
|
998
1231
|
)
|
|
999
1232
|
);
|
|
1000
1233
|
}
|
|
1234
|
+
/** A known 401/403 keeps its typed status even if reading its body fails. */
|
|
1235
|
+
async readAuthorizationText(response) {
|
|
1236
|
+
try {
|
|
1237
|
+
return await response.text();
|
|
1238
|
+
} catch {
|
|
1239
|
+
return "";
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1001
1242
|
/**
|
|
1002
1243
|
* AUTH_UNAUTHORIZED for a 401/403 response. The message keeps the HTTP
|
|
1003
1244
|
* status and the server text (`<context>: <status> - <text>`, the same
|
|
1004
1245
|
* shape as every other KV failure) so callers that rethrow only the message
|
|
1005
1246
|
* still see the status; `meta.status` carries it for typed callers.
|
|
1006
|
-
*
|
|
1247
|
+
* Server hints must match the requested path/action (whether the node sends
|
|
1248
|
+
* a relative path or full resource); metadata uses only the request-derived
|
|
1249
|
+
* canonical resource under the authenticated session's space.
|
|
1007
1250
|
*/
|
|
1008
|
-
authorizationFailure(context, response, errorText, extraMeta = {}) {
|
|
1251
|
+
authorizationFailure(context, response, errorText, paths, expectedAction, extraMeta = {}) {
|
|
1009
1252
|
const { resource, action } = parseAuthError(errorText);
|
|
1253
|
+
const requestedPath = paths.find(
|
|
1254
|
+
(path) => resource === path || resource === `${this.context.session.spaceId}/kv/${path}`
|
|
1255
|
+
);
|
|
1256
|
+
const capability = requestedPath !== void 0 && action === expectedAction ? validatedCapabilityOf({
|
|
1257
|
+
service: "kv",
|
|
1258
|
+
code: ErrorCodes.AUTH_UNAUTHORIZED,
|
|
1259
|
+
meta: {
|
|
1260
|
+
status: response.status,
|
|
1261
|
+
resource: `${this.context.session.spaceId}/kv/${requestedPath}`,
|
|
1262
|
+
requiredAction: action
|
|
1263
|
+
}
|
|
1264
|
+
}) : void 0;
|
|
1010
1265
|
const detail = errorText.trim().length > 0 ? errorText : response.statusText || "authorization failed";
|
|
1011
1266
|
return err(authUnauthorizedError("kv", `${context}: ${response.status} - ${detail}`, {
|
|
1012
1267
|
status: response.status,
|
|
1013
|
-
...
|
|
1014
|
-
...
|
|
1015
|
-
...extraMeta
|
|
1268
|
+
...extraMeta,
|
|
1269
|
+
...capability
|
|
1016
1270
|
}));
|
|
1017
1271
|
}
|
|
1018
1272
|
normalizeSignedReadUrlResponse(data) {
|
|
@@ -1046,23 +1300,35 @@ var KVService = class extends BaseService {
|
|
|
1046
1300
|
));
|
|
1047
1301
|
}
|
|
1048
1302
|
const path = this.getFullPath(key, options?.prefix);
|
|
1303
|
+
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
1049
1304
|
try {
|
|
1050
1305
|
const response = await this.invokeOperation(
|
|
1051
1306
|
path,
|
|
1052
1307
|
KVAction.GET,
|
|
1053
1308
|
void 0,
|
|
1054
|
-
|
|
1309
|
+
request.signal,
|
|
1055
1310
|
options?.maxResponseBytes === void 0 ? void 0 : { "x-tinycloud-max-response-bytes": String(options.maxResponseBytes) }
|
|
1056
1311
|
);
|
|
1057
1312
|
if (!response.ok) {
|
|
1058
1313
|
if (response.status === 401 || response.status === 403) {
|
|
1059
|
-
const errorText2 = await
|
|
1314
|
+
const errorText2 = await this.readAuthorizationText(response);
|
|
1060
1315
|
const permissionHint = parsePermissionHintFromErrorText(errorText2);
|
|
1316
|
+
const structuredCapability = permissionHint?.service === "tinycloud.kv" && permissionHint.space === this.context.session.spaceId && permissionHint.path === path ? validatedCapabilityOf({
|
|
1317
|
+
service: "kv",
|
|
1318
|
+
code: ErrorCodes.AUTH_UNAUTHORIZED,
|
|
1319
|
+
meta: {
|
|
1320
|
+
status: response.status,
|
|
1321
|
+
resource: `${this.context.session.spaceId}/kv/${path}`,
|
|
1322
|
+
requiredAction: KVAction.GET
|
|
1323
|
+
}
|
|
1324
|
+
}) : void 0;
|
|
1061
1325
|
return this.authorizationFailure(
|
|
1062
1326
|
`Failed to get key ${JSON.stringify(key)}`,
|
|
1063
1327
|
response,
|
|
1064
1328
|
errorText2,
|
|
1065
|
-
|
|
1329
|
+
[path],
|
|
1330
|
+
KVAction.GET,
|
|
1331
|
+
structuredCapability ? { permissionHint, ...structuredCapability } : {}
|
|
1066
1332
|
);
|
|
1067
1333
|
}
|
|
1068
1334
|
if (response.status === 404) {
|
|
@@ -1097,6 +1363,8 @@ var KVService = class extends BaseService {
|
|
|
1097
1363
|
});
|
|
1098
1364
|
} catch (error) {
|
|
1099
1365
|
return err(wrapError("kv", error));
|
|
1366
|
+
} finally {
|
|
1367
|
+
request.dispose();
|
|
1100
1368
|
}
|
|
1101
1369
|
});
|
|
1102
1370
|
}
|
|
@@ -1124,12 +1392,13 @@ var KVService = class extends BaseService {
|
|
|
1124
1392
|
}
|
|
1125
1393
|
const path = this.getFullPath(key, options?.prefix);
|
|
1126
1394
|
const body = this.serializePutValue(value, options?.contentType);
|
|
1395
|
+
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
1127
1396
|
try {
|
|
1128
1397
|
const response = await this.invokeOperation(
|
|
1129
1398
|
path,
|
|
1130
1399
|
KVAction.PUT,
|
|
1131
1400
|
body,
|
|
1132
|
-
|
|
1401
|
+
request.signal,
|
|
1133
1402
|
{
|
|
1134
1403
|
...options?.ifMatch === void 0 ? {} : { "if-match": options.ifMatch },
|
|
1135
1404
|
...options?.ifNoneMatch === void 0 ? {} : { "if-none-match": options.ifNoneMatch }
|
|
@@ -1139,7 +1408,9 @@ var KVService = class extends BaseService {
|
|
|
1139
1408
|
return this.authorizationFailure(
|
|
1140
1409
|
`Failed to put key ${JSON.stringify(key)}`,
|
|
1141
1410
|
response,
|
|
1142
|
-
await
|
|
1411
|
+
await this.readAuthorizationText(response),
|
|
1412
|
+
[path],
|
|
1413
|
+
KVAction.PUT
|
|
1143
1414
|
);
|
|
1144
1415
|
}
|
|
1145
1416
|
if (!response.ok) {
|
|
@@ -1183,6 +1454,8 @@ var KVService = class extends BaseService {
|
|
|
1183
1454
|
});
|
|
1184
1455
|
} catch (error) {
|
|
1185
1456
|
return err(wrapError("kv", error));
|
|
1457
|
+
} finally {
|
|
1458
|
+
request.dispose();
|
|
1186
1459
|
}
|
|
1187
1460
|
});
|
|
1188
1461
|
}
|
|
@@ -1222,6 +1495,7 @@ var KVService = class extends BaseService {
|
|
|
1222
1495
|
seen.add(path);
|
|
1223
1496
|
}
|
|
1224
1497
|
let requestMayHaveDispatched = false;
|
|
1498
|
+
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
1225
1499
|
try {
|
|
1226
1500
|
const body = new FormData();
|
|
1227
1501
|
for (let index = 0; index < items.length; index++) {
|
|
@@ -1244,7 +1518,7 @@ var KVService = class extends BaseService {
|
|
|
1244
1518
|
method: "POST",
|
|
1245
1519
|
headers,
|
|
1246
1520
|
body,
|
|
1247
|
-
signal:
|
|
1521
|
+
signal: request.signal
|
|
1248
1522
|
};
|
|
1249
1523
|
const fetchFn = this.context.fetch;
|
|
1250
1524
|
requestMayHaveDispatched = true;
|
|
@@ -1268,7 +1542,9 @@ var KVService = class extends BaseService {
|
|
|
1268
1542
|
return this.authorizationFailure(
|
|
1269
1543
|
`Failed to batch put ${items.length} key(s)`,
|
|
1270
1544
|
response,
|
|
1271
|
-
errorText
|
|
1545
|
+
errorText,
|
|
1546
|
+
paths,
|
|
1547
|
+
KVAction.PUT
|
|
1272
1548
|
);
|
|
1273
1549
|
}
|
|
1274
1550
|
const quotaError = this.handleQuotaErrorResponse(
|
|
@@ -1293,14 +1569,16 @@ var KVService = class extends BaseService {
|
|
|
1293
1569
|
rawBody = await response.json();
|
|
1294
1570
|
} catch (jsonError) {
|
|
1295
1571
|
const cause = jsonError instanceof Error ? jsonError : new Error(String(jsonError));
|
|
1572
|
+
const timedOut = isRequestTimeoutError(cause);
|
|
1296
1573
|
return err(
|
|
1297
1574
|
serviceError(
|
|
1298
|
-
ErrorCodes.NETWORK_ERROR,
|
|
1299
|
-
`KV batchPut response was not valid JSON: ${cause.message}`,
|
|
1575
|
+
timedOut ? ErrorCodes.TIMEOUT : ErrorCodes.NETWORK_ERROR,
|
|
1576
|
+
timedOut ? `KV batchPut response body did not arrive in time: ${cause.message}` : `KV batchPut response was not valid JSON: ${cause.message}`,
|
|
1300
1577
|
"kv",
|
|
1301
1578
|
{
|
|
1302
1579
|
cause,
|
|
1303
1580
|
meta: {
|
|
1581
|
+
...timedOut ? { timeoutMs: cause.timeoutMs } : {},
|
|
1304
1582
|
requestMayHaveDispatched: true,
|
|
1305
1583
|
responseReceived: true,
|
|
1306
1584
|
status: response.status,
|
|
@@ -1355,6 +1633,8 @@ var KVService = class extends BaseService {
|
|
|
1355
1633
|
...wrapped,
|
|
1356
1634
|
meta: { ...wrapped.meta, requestMayHaveDispatched }
|
|
1357
1635
|
});
|
|
1636
|
+
} finally {
|
|
1637
|
+
request.dispose();
|
|
1358
1638
|
}
|
|
1359
1639
|
});
|
|
1360
1640
|
}
|
|
@@ -1377,23 +1657,26 @@ var KVService = class extends BaseService {
|
|
|
1377
1657
|
if (options?.path) {
|
|
1378
1658
|
listPath = listPath ? `${listPath}/${options.path}` : options.path;
|
|
1379
1659
|
}
|
|
1660
|
+
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
1380
1661
|
try {
|
|
1381
1662
|
const response = await this.invokeOperation(
|
|
1382
1663
|
listPath,
|
|
1383
1664
|
KVAction.LIST,
|
|
1384
1665
|
void 0,
|
|
1385
|
-
|
|
1666
|
+
request.signal,
|
|
1386
1667
|
options?.limit === void 0 && options?.cursor === void 0 ? void 0 : {
|
|
1387
1668
|
...options?.limit === void 0 ? {} : { "x-tinycloud-limit": String(options.limit) },
|
|
1388
1669
|
...options?.cursor === void 0 ? {} : { "x-tinycloud-cursor": options.cursor }
|
|
1389
1670
|
}
|
|
1390
1671
|
);
|
|
1391
1672
|
if (!response.ok) {
|
|
1392
|
-
if (response.status === 401) {
|
|
1673
|
+
if (response.status === 401 || response.status === 403) {
|
|
1393
1674
|
return this.authorizationFailure(
|
|
1394
1675
|
"Failed to list keys",
|
|
1395
1676
|
response,
|
|
1396
|
-
await
|
|
1677
|
+
await this.readAuthorizationText(response),
|
|
1678
|
+
[listPath],
|
|
1679
|
+
KVAction.LIST
|
|
1397
1680
|
);
|
|
1398
1681
|
}
|
|
1399
1682
|
const errorText = await response.text();
|
|
@@ -1421,6 +1704,8 @@ var KVService = class extends BaseService {
|
|
|
1421
1704
|
});
|
|
1422
1705
|
} catch (error) {
|
|
1423
1706
|
return err(wrapError("kv", error));
|
|
1707
|
+
} finally {
|
|
1708
|
+
request.dispose();
|
|
1424
1709
|
}
|
|
1425
1710
|
});
|
|
1426
1711
|
}
|
|
@@ -1433,20 +1718,23 @@ var KVService = class extends BaseService {
|
|
|
1433
1718
|
return err(authRequiredError("kv"));
|
|
1434
1719
|
}
|
|
1435
1720
|
const path = this.getFullPath(key, options?.prefix);
|
|
1721
|
+
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
1436
1722
|
try {
|
|
1437
1723
|
const response = await this.invokeOperation(
|
|
1438
1724
|
path,
|
|
1439
1725
|
KVAction.DELETE,
|
|
1440
1726
|
void 0,
|
|
1441
|
-
|
|
1727
|
+
request.signal,
|
|
1442
1728
|
options?.ifMatch === void 0 ? void 0 : { "if-match": options.ifMatch }
|
|
1443
1729
|
);
|
|
1444
1730
|
if (!response.ok) {
|
|
1445
|
-
if (response.status === 401) {
|
|
1731
|
+
if (response.status === 401 || response.status === 403) {
|
|
1446
1732
|
return this.authorizationFailure(
|
|
1447
1733
|
`Failed to delete key ${JSON.stringify(key)}`,
|
|
1448
1734
|
response,
|
|
1449
|
-
await
|
|
1735
|
+
await this.readAuthorizationText(response),
|
|
1736
|
+
[path],
|
|
1737
|
+
KVAction.DELETE
|
|
1450
1738
|
);
|
|
1451
1739
|
}
|
|
1452
1740
|
if (response.status === 404) {
|
|
@@ -1484,6 +1772,8 @@ var KVService = class extends BaseService {
|
|
|
1484
1772
|
});
|
|
1485
1773
|
} catch (error) {
|
|
1486
1774
|
return err(wrapError("kv", error));
|
|
1775
|
+
} finally {
|
|
1776
|
+
request.dispose();
|
|
1487
1777
|
}
|
|
1488
1778
|
});
|
|
1489
1779
|
}
|
|
@@ -1496,19 +1786,22 @@ var KVService = class extends BaseService {
|
|
|
1496
1786
|
return err(authRequiredError("kv"));
|
|
1497
1787
|
}
|
|
1498
1788
|
const path = this.getFullPath(key, options?.prefix);
|
|
1789
|
+
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
1499
1790
|
try {
|
|
1500
1791
|
const response = await this.invokeOperation(
|
|
1501
1792
|
path,
|
|
1502
1793
|
KVAction.HEAD,
|
|
1503
1794
|
void 0,
|
|
1504
|
-
|
|
1795
|
+
request.signal
|
|
1505
1796
|
);
|
|
1506
1797
|
if (!response.ok) {
|
|
1507
|
-
if (response.status === 401) {
|
|
1798
|
+
if (response.status === 401 || response.status === 403) {
|
|
1508
1799
|
return this.authorizationFailure(
|
|
1509
1800
|
`Failed to get metadata for key ${JSON.stringify(key)}`,
|
|
1510
1801
|
response,
|
|
1511
|
-
await
|
|
1802
|
+
await this.readAuthorizationText(response),
|
|
1803
|
+
[path],
|
|
1804
|
+
KVAction.HEAD
|
|
1512
1805
|
);
|
|
1513
1806
|
}
|
|
1514
1807
|
if (response.status === 404) {
|
|
@@ -1530,6 +1823,8 @@ var KVService = class extends BaseService {
|
|
|
1530
1823
|
});
|
|
1531
1824
|
} catch (error) {
|
|
1532
1825
|
return err(wrapError("kv", error));
|
|
1826
|
+
} finally {
|
|
1827
|
+
request.dispose();
|
|
1533
1828
|
}
|
|
1534
1829
|
});
|
|
1535
1830
|
}
|
|
@@ -1567,15 +1862,16 @@ var KVService = class extends BaseService {
|
|
|
1567
1862
|
if (options?.etag !== void 0) {
|
|
1568
1863
|
body.etag = options.etag;
|
|
1569
1864
|
}
|
|
1865
|
+
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
1570
1866
|
try {
|
|
1571
1867
|
const response = await this.context.fetch(`${this.host}/signed/kv`, {
|
|
1572
1868
|
method: "POST",
|
|
1573
1869
|
headers: this.withJsonContentType(headers),
|
|
1574
1870
|
body: JSON.stringify(body),
|
|
1575
|
-
signal:
|
|
1871
|
+
signal: request.signal
|
|
1576
1872
|
});
|
|
1577
1873
|
if (!response.ok) {
|
|
1578
|
-
return this.createSignedReadUrlError(response, key);
|
|
1874
|
+
return this.createSignedReadUrlError(response, key, path);
|
|
1579
1875
|
}
|
|
1580
1876
|
const signedUrl = this.normalizeSignedReadUrlResponse(
|
|
1581
1877
|
await response.json()
|
|
@@ -1592,6 +1888,8 @@ var KVService = class extends BaseService {
|
|
|
1592
1888
|
return ok(signedUrl);
|
|
1593
1889
|
} catch (error) {
|
|
1594
1890
|
return err(wrapError("kv", error));
|
|
1891
|
+
} finally {
|
|
1892
|
+
request.dispose();
|
|
1595
1893
|
}
|
|
1596
1894
|
});
|
|
1597
1895
|
}
|