@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.js
CHANGED
|
@@ -70,6 +70,55 @@ function serviceError(code, message, service, options) {
|
|
|
70
70
|
};
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
// src/errors.ts
|
|
74
|
+
import { CAPABILITY_REGISTRY } from "@tinycloud/bootstrap";
|
|
75
|
+
|
|
76
|
+
// src/encryption/networkId.ts
|
|
77
|
+
var URN_PREFIX = "urn:tinycloud:encryption:";
|
|
78
|
+
var NETWORK_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
79
|
+
var NetworkIdError = class extends Error {
|
|
80
|
+
constructor(message) {
|
|
81
|
+
super(message);
|
|
82
|
+
this.name = "NetworkIdError";
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
function parseNetworkId(networkId) {
|
|
86
|
+
if (typeof networkId !== "string" || networkId.length === 0) {
|
|
87
|
+
throw new NetworkIdError("networkId must be a non-empty string");
|
|
88
|
+
}
|
|
89
|
+
if (!networkId.startsWith(URN_PREFIX)) {
|
|
90
|
+
throw new NetworkIdError(
|
|
91
|
+
`networkId must start with ${URN_PREFIX} (got ${JSON.stringify(networkId)})`
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
const body = networkId.slice(URN_PREFIX.length);
|
|
95
|
+
const lastColon = body.lastIndexOf(":");
|
|
96
|
+
if (lastColon <= 0 || lastColon === body.length - 1) {
|
|
97
|
+
throw new NetworkIdError(
|
|
98
|
+
`networkId missing ownerDid or name segment (got ${JSON.stringify(networkId)})`
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
const ownerDid = body.slice(0, lastColon);
|
|
102
|
+
const name = body.slice(lastColon + 1);
|
|
103
|
+
if (!ownerDid.startsWith("did:")) {
|
|
104
|
+
throw new NetworkIdError(
|
|
105
|
+
`networkId ownerDid must be a DID (got ${JSON.stringify(ownerDid)})`
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
const didParts = ownerDid.split(":");
|
|
109
|
+
if (didParts.length < 3 || didParts.some((p) => p.length === 0)) {
|
|
110
|
+
throw new NetworkIdError(
|
|
111
|
+
`networkId ownerDid is not a well-formed DID (got ${JSON.stringify(ownerDid)})`
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
if (!NETWORK_NAME_RE.test(name)) {
|
|
115
|
+
throw new NetworkIdError(
|
|
116
|
+
`networkId name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
return { networkId, ownerDid, name };
|
|
120
|
+
}
|
|
121
|
+
|
|
73
122
|
// src/errors.ts
|
|
74
123
|
function parsePermissionHint(value) {
|
|
75
124
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
@@ -114,11 +163,31 @@ function authRequiredError(service) {
|
|
|
114
163
|
service
|
|
115
164
|
};
|
|
116
165
|
}
|
|
117
|
-
|
|
166
|
+
var RequestTimeoutError = class extends Error {
|
|
167
|
+
constructor(timeoutMs) {
|
|
168
|
+
super(`Request timed out after ${timeoutMs}ms.`);
|
|
169
|
+
this.name = "TimeoutError";
|
|
170
|
+
this.timeoutMs = timeoutMs;
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
function isRequestTimeoutError(error) {
|
|
174
|
+
return error instanceof RequestTimeoutError || error instanceof Error && error.name === "TimeoutError" && typeof error.timeoutMs === "number";
|
|
175
|
+
}
|
|
176
|
+
function timeoutError(service, timeoutMs, cause) {
|
|
177
|
+
if (timeoutMs === void 0) {
|
|
178
|
+
return {
|
|
179
|
+
code: ErrorCodes.TIMEOUT,
|
|
180
|
+
message: "Request timed out.",
|
|
181
|
+
service,
|
|
182
|
+
...cause === void 0 ? {} : { cause }
|
|
183
|
+
};
|
|
184
|
+
}
|
|
118
185
|
return {
|
|
119
186
|
code: ErrorCodes.TIMEOUT,
|
|
120
|
-
message:
|
|
121
|
-
service
|
|
187
|
+
message: `Request timed out after ${timeoutMs}ms.`,
|
|
188
|
+
service,
|
|
189
|
+
...cause === void 0 ? {} : { cause },
|
|
190
|
+
meta: { timeoutMs }
|
|
122
191
|
};
|
|
123
192
|
}
|
|
124
193
|
function abortedError(service) {
|
|
@@ -138,6 +207,101 @@ function parseAuthError(responseText) {
|
|
|
138
207
|
function authUnauthorizedError(service, message, meta) {
|
|
139
208
|
return serviceError(ErrorCodes.AUTH_UNAUTHORIZED, message, service, { meta });
|
|
140
209
|
}
|
|
210
|
+
var grantableActions = new Set(
|
|
211
|
+
CAPABILITY_REGISTRY.map(({ urn }) => urn).filter((urn) => !urn.endsWith("/*"))
|
|
212
|
+
);
|
|
213
|
+
var capabilityServices = new Set(
|
|
214
|
+
CAPABILITY_REGISTRY.map(({ service }) => service.slice("tinycloud.".length))
|
|
215
|
+
);
|
|
216
|
+
function parseCapabilityResource(resource, service) {
|
|
217
|
+
if (resource.length === 0 || resource.length > 1024 || !capabilityServices.has(service) || /[\s\p{C}*#]/u.test(resource) || resource.includes("://")) return void 0;
|
|
218
|
+
const segments = resource.split("/");
|
|
219
|
+
if (!/^tinycloud:[A-Za-z0-9][A-Za-z0-9:._-]*$/.test(segments[0])) return void 0;
|
|
220
|
+
const serviceIndex = segments.findIndex(
|
|
221
|
+
(segment, index) => index > 0 && capabilityServices.has(segment)
|
|
222
|
+
);
|
|
223
|
+
if (serviceIndex < 1 || segments[serviceIndex] !== service) return void 0;
|
|
224
|
+
if (segments.slice(1, serviceIndex).some(
|
|
225
|
+
(segment) => segment.length === 0 || segment === "." || segment === ".."
|
|
226
|
+
)) return void 0;
|
|
227
|
+
const pathSegments = segments.slice(serviceIndex + 1);
|
|
228
|
+
if (pathSegments.some((segment) => segment === "." || segment === "..")) return void 0;
|
|
229
|
+
if (service !== "kv" && (pathSegments.length === 0 || pathSegments[pathSegments.length - 1] === "")) return void 0;
|
|
230
|
+
return {
|
|
231
|
+
space: segments.slice(0, serviceIndex).join("/"),
|
|
232
|
+
path: pathSegments.join("/")
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
function safeCapabilityResource(value, service, action) {
|
|
236
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 1024) return false;
|
|
237
|
+
if (service === "encryption" && value.startsWith("urn:tinycloud:encryption:")) {
|
|
238
|
+
if (!/^[A-Za-z0-9_][A-Za-z0-9_.:/-]*$/.test(value) || value.includes("//") || value.split("/").some((segment) => segment === "." || segment === "..")) return false;
|
|
239
|
+
try {
|
|
240
|
+
parseNetworkId(value);
|
|
241
|
+
return true;
|
|
242
|
+
} catch {
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const parsed = parseCapabilityResource(value, service);
|
|
247
|
+
return parsed !== void 0 && (!parsed.path.endsWith("/") || service === "kv" && action === "tinycloud.kv/list") && (parsed.path !== "" || service === "kv" && action === "tinycloud.kv/list");
|
|
248
|
+
}
|
|
249
|
+
function validatedCapabilityOf(error) {
|
|
250
|
+
const verdict = authorizationVerdictOf(error);
|
|
251
|
+
if (verdict !== "unauthenticated" && verdict !== "forbidden") return void 0;
|
|
252
|
+
const seen = /* @__PURE__ */ new Set();
|
|
253
|
+
let node = error;
|
|
254
|
+
for (let depth = 0; depth < MAX_CAUSE_DEPTH && typeof node === "object" && node !== null && !seen.has(node); depth += 1) {
|
|
255
|
+
seen.add(node);
|
|
256
|
+
const record = node;
|
|
257
|
+
const status = errorStatusOf(record);
|
|
258
|
+
const meta = record.meta;
|
|
259
|
+
if ((status === 401 || status === 403 || status === void 0 && record.code === ErrorCodes.AUTH_UNAUTHORIZED) && typeof meta === "object" && meta !== null && !Array.isArray(meta)) {
|
|
260
|
+
const metadata = meta;
|
|
261
|
+
const { resource, requiredAction } = metadata;
|
|
262
|
+
if (typeof requiredAction === "string" && grantableActions.has(requiredAction)) {
|
|
263
|
+
const actionService = requiredAction.slice("tinycloud.".length, requiredAction.indexOf("/"));
|
|
264
|
+
const explicitService = record.service === void 0 ? metadata.service : record.service;
|
|
265
|
+
if (safeCapabilityResource(resource, actionService, requiredAction) && (explicitService === void 0 || explicitService === actionService || explicitService === `tinycloud.${actionService}`)) {
|
|
266
|
+
return { resource, requiredAction };
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
node = record.cause;
|
|
271
|
+
}
|
|
272
|
+
return void 0;
|
|
273
|
+
}
|
|
274
|
+
var MAX_CAUSE_DEPTH = 8;
|
|
275
|
+
function errorStatusOf(node) {
|
|
276
|
+
const meta = node.meta;
|
|
277
|
+
const candidates = [
|
|
278
|
+
node.status,
|
|
279
|
+
node.statusCode,
|
|
280
|
+
typeof meta === "object" && meta !== null ? meta.status : void 0
|
|
281
|
+
];
|
|
282
|
+
for (const candidate of candidates) {
|
|
283
|
+
if (typeof candidate === "number" && Number.isInteger(candidate) && candidate >= 400 && candidate <= 599) {
|
|
284
|
+
return candidate;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return void 0;
|
|
288
|
+
}
|
|
289
|
+
function authorizationVerdictOf(error) {
|
|
290
|
+
const seen = /* @__PURE__ */ new Set();
|
|
291
|
+
let sawUnauthorizedCode = false;
|
|
292
|
+
let node = error;
|
|
293
|
+
for (let depth = 0; depth < MAX_CAUSE_DEPTH && typeof node === "object" && node !== null && !seen.has(node); depth += 1) {
|
|
294
|
+
seen.add(node);
|
|
295
|
+
const record = node;
|
|
296
|
+
const status = errorStatusOf(record);
|
|
297
|
+
if (status === 401) return "unauthenticated";
|
|
298
|
+
if (status === 403) return "forbidden";
|
|
299
|
+
if (status !== void 0) return "other";
|
|
300
|
+
if (record.code === ErrorCodes.AUTH_UNAUTHORIZED) sawUnauthorizedCode = true;
|
|
301
|
+
node = record.cause;
|
|
302
|
+
}
|
|
303
|
+
return sawUnauthorizedCode ? "forbidden" : void 0;
|
|
304
|
+
}
|
|
141
305
|
function storageQuotaExceededError(service, message, meta) {
|
|
142
306
|
return {
|
|
143
307
|
code: ErrorCodes.STORAGE_QUOTA_EXCEEDED,
|
|
@@ -170,6 +334,9 @@ function storageRejectionError(service, status, meta, responseText) {
|
|
|
170
334
|
}
|
|
171
335
|
function wrapError(service, error, defaultCode = ErrorCodes.NETWORK_ERROR) {
|
|
172
336
|
if (error instanceof Error) {
|
|
337
|
+
if (isRequestTimeoutError(error)) {
|
|
338
|
+
return timeoutError(service, error.timeoutMs, error);
|
|
339
|
+
}
|
|
173
340
|
if (error.name === "AbortError") {
|
|
174
341
|
return abortedError(service);
|
|
175
342
|
}
|
|
@@ -191,6 +358,7 @@ function wrapError(service, error, defaultCode = ErrorCodes.NETWORK_ERROR) {
|
|
|
191
358
|
}
|
|
192
359
|
|
|
193
360
|
// src/base/BaseService.ts
|
|
361
|
+
var MAX_TIMER_DELAY_MS = 2147483647;
|
|
194
362
|
var BaseService = class {
|
|
195
363
|
constructor() {
|
|
196
364
|
/**
|
|
@@ -371,6 +539,64 @@ var BaseService = class {
|
|
|
371
539
|
}
|
|
372
540
|
return controller.signal;
|
|
373
541
|
}
|
|
542
|
+
/**
|
|
543
|
+
* Create the abort signal for one request, bounded by the request timeout.
|
|
544
|
+
*
|
|
545
|
+
* The signal aborts when the service signs out, the context aborts, the
|
|
546
|
+
* caller's `signal` aborts, or the timeout elapses, whichever comes first.
|
|
547
|
+
* A timeout aborts with a {@link RequestTimeoutError} (`TimeoutError`), which
|
|
548
|
+
* `wrapError` maps to `ErrorCodes.TIMEOUT`; the other sources keep their own
|
|
549
|
+
* reason (normally `AbortError`, mapped to `ErrorCodes.ABORTED`).
|
|
550
|
+
*
|
|
551
|
+
* The timeout is `timeoutMs` when given, otherwise `config.timeout`. Only a
|
|
552
|
+
* positive, finite value (at most 2^31-1 ms) applies; `undefined`, `0`, and
|
|
553
|
+
* `Infinity` mean no timeout, so a per-call `0` opts out of a configured one.
|
|
554
|
+
*
|
|
555
|
+
* Callers must call `dispose()` once the response body has been consumed,
|
|
556
|
+
* which clears the timer and removes the listeners this adds to the
|
|
557
|
+
* long-lived service and context signals.
|
|
558
|
+
*
|
|
559
|
+
* @param signal - The caller's abort signal for this request
|
|
560
|
+
* @param timeoutMs - Per-request timeout override in milliseconds
|
|
561
|
+
*/
|
|
562
|
+
createRequestSignal(signal, timeoutMs) {
|
|
563
|
+
const controller = new AbortController();
|
|
564
|
+
const detachers = [];
|
|
565
|
+
let timer;
|
|
566
|
+
const dispose = () => {
|
|
567
|
+
if (timer !== void 0) {
|
|
568
|
+
clearTimeout(timer);
|
|
569
|
+
timer = void 0;
|
|
570
|
+
}
|
|
571
|
+
for (const detach of detachers.splice(0)) {
|
|
572
|
+
detach();
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
const abort = (reason) => {
|
|
576
|
+
dispose();
|
|
577
|
+
controller.abort(reason);
|
|
578
|
+
};
|
|
579
|
+
const requestSignal = { signal: controller.signal, dispose };
|
|
580
|
+
const parents = [
|
|
581
|
+
this.abortController.signal,
|
|
582
|
+
this.context?.abortSignal,
|
|
583
|
+
signal
|
|
584
|
+
].filter((parent) => parent !== void 0);
|
|
585
|
+
for (const parent of parents) {
|
|
586
|
+
if (parent.aborted) {
|
|
587
|
+
abort(parent.reason);
|
|
588
|
+
return requestSignal;
|
|
589
|
+
}
|
|
590
|
+
const onAbort = () => abort(parent.reason);
|
|
591
|
+
parent.addEventListener("abort", onAbort, { once: true });
|
|
592
|
+
detachers.push(() => parent.removeEventListener("abort", onAbort));
|
|
593
|
+
}
|
|
594
|
+
const timeout = timeoutMs ?? this._config.timeout;
|
|
595
|
+
if (typeof timeout === "number" && timeout > 0 && timeout <= MAX_TIMER_DELAY_MS) {
|
|
596
|
+
timer = setTimeout(() => abort(new RequestTimeoutError(timeout)), timeout);
|
|
597
|
+
}
|
|
598
|
+
return requestSignal;
|
|
599
|
+
}
|
|
374
600
|
/**
|
|
375
601
|
* Wrap an operation with error handling and telemetry.
|
|
376
602
|
*
|
|
@@ -631,7 +857,7 @@ var KVService = class extends BaseService {
|
|
|
631
857
|
* @param path - Resource path
|
|
632
858
|
* @param action - KV action
|
|
633
859
|
* @param body - Optional request body
|
|
634
|
-
* @param signal -
|
|
860
|
+
* @param signal - Request signal from createRequestSignal()
|
|
635
861
|
* @returns Fetch response
|
|
636
862
|
*/
|
|
637
863
|
async invokeOperation(path, action, body, signal, extraHeaders) {
|
|
@@ -647,7 +873,7 @@ var KVService = class extends BaseService {
|
|
|
647
873
|
method: "POST",
|
|
648
874
|
headers: requestHeaders,
|
|
649
875
|
body,
|
|
650
|
-
signal
|
|
876
|
+
signal
|
|
651
877
|
});
|
|
652
878
|
}
|
|
653
879
|
/**
|
|
@@ -824,6 +1050,7 @@ var KVService = class extends BaseService {
|
|
|
824
1050
|
"kv"
|
|
825
1051
|
));
|
|
826
1052
|
}
|
|
1053
|
+
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
827
1054
|
try {
|
|
828
1055
|
const session = this.context.session;
|
|
829
1056
|
const invocationHeaders = this.context.invokeAny(
|
|
@@ -845,15 +1072,17 @@ var KVService = class extends BaseService {
|
|
|
845
1072
|
const response = await this.context.fetch(`${this.host}/invoke`, {
|
|
846
1073
|
method: "POST",
|
|
847
1074
|
headers,
|
|
848
|
-
signal:
|
|
1075
|
+
signal: request.signal
|
|
849
1076
|
});
|
|
850
1077
|
if (!response.ok) {
|
|
851
|
-
const errorText = await response.text();
|
|
1078
|
+
const errorText = response.status === 401 || response.status === 403 ? await this.readAuthorizationText(response) : await response.text();
|
|
852
1079
|
if (response.status === 401 || response.status === 403) {
|
|
853
1080
|
return this.authorizationFailure(
|
|
854
1081
|
`Failed to batch read ${keys.length} key(s)`,
|
|
855
1082
|
response,
|
|
856
|
-
errorText
|
|
1083
|
+
errorText,
|
|
1084
|
+
paths,
|
|
1085
|
+
action
|
|
857
1086
|
);
|
|
858
1087
|
}
|
|
859
1088
|
if (response.status === 413) {
|
|
@@ -908,6 +1137,8 @@ var KVService = class extends BaseService {
|
|
|
908
1137
|
return ok({ results, count: results.length });
|
|
909
1138
|
} catch (error) {
|
|
910
1139
|
return err(wrapError("kv", error));
|
|
1140
|
+
} finally {
|
|
1141
|
+
request.dispose();
|
|
911
1142
|
}
|
|
912
1143
|
}
|
|
913
1144
|
/**
|
|
@@ -943,7 +1174,7 @@ var KVService = class extends BaseService {
|
|
|
943
1174
|
return text;
|
|
944
1175
|
}
|
|
945
1176
|
}
|
|
946
|
-
async createSignedReadUrlError(response, key) {
|
|
1177
|
+
async createSignedReadUrlError(response, key, path) {
|
|
947
1178
|
let errorText = response.statusText;
|
|
948
1179
|
try {
|
|
949
1180
|
const text = await response.text();
|
|
@@ -956,7 +1187,9 @@ var KVService = class extends BaseService {
|
|
|
956
1187
|
return this.authorizationFailure(
|
|
957
1188
|
`Failed to create signed read URL for key ${JSON.stringify(key)}`,
|
|
958
1189
|
response,
|
|
959
|
-
errorText
|
|
1190
|
+
errorText,
|
|
1191
|
+
[path],
|
|
1192
|
+
KVAction.GET
|
|
960
1193
|
);
|
|
961
1194
|
}
|
|
962
1195
|
const code = response.status === 400 ? ErrorCodes.INVALID_INPUT : ErrorCodes.NETWORK_ERROR;
|
|
@@ -969,21 +1202,42 @@ var KVService = class extends BaseService {
|
|
|
969
1202
|
)
|
|
970
1203
|
);
|
|
971
1204
|
}
|
|
1205
|
+
/** A known 401/403 keeps its typed status even if reading its body fails. */
|
|
1206
|
+
async readAuthorizationText(response) {
|
|
1207
|
+
try {
|
|
1208
|
+
return await response.text();
|
|
1209
|
+
} catch {
|
|
1210
|
+
return "";
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
972
1213
|
/**
|
|
973
1214
|
* AUTH_UNAUTHORIZED for a 401/403 response. The message keeps the HTTP
|
|
974
1215
|
* status and the server text (`<context>: <status> - <text>`, the same
|
|
975
1216
|
* shape as every other KV failure) so callers that rethrow only the message
|
|
976
1217
|
* still see the status; `meta.status` carries it for typed callers.
|
|
977
|
-
*
|
|
1218
|
+
* Server hints must match the requested path/action (whether the node sends
|
|
1219
|
+
* a relative path or full resource); metadata uses only the request-derived
|
|
1220
|
+
* canonical resource under the authenticated session's space.
|
|
978
1221
|
*/
|
|
979
|
-
authorizationFailure(context, response, errorText, extraMeta = {}) {
|
|
1222
|
+
authorizationFailure(context, response, errorText, paths, expectedAction, extraMeta = {}) {
|
|
980
1223
|
const { resource, action } = parseAuthError(errorText);
|
|
1224
|
+
const requestedPath = paths.find(
|
|
1225
|
+
(path) => resource === path || resource === `${this.context.session.spaceId}/kv/${path}`
|
|
1226
|
+
);
|
|
1227
|
+
const capability = requestedPath !== void 0 && action === expectedAction ? validatedCapabilityOf({
|
|
1228
|
+
service: "kv",
|
|
1229
|
+
code: ErrorCodes.AUTH_UNAUTHORIZED,
|
|
1230
|
+
meta: {
|
|
1231
|
+
status: response.status,
|
|
1232
|
+
resource: `${this.context.session.spaceId}/kv/${requestedPath}`,
|
|
1233
|
+
requiredAction: action
|
|
1234
|
+
}
|
|
1235
|
+
}) : void 0;
|
|
981
1236
|
const detail = errorText.trim().length > 0 ? errorText : response.statusText || "authorization failed";
|
|
982
1237
|
return err(authUnauthorizedError("kv", `${context}: ${response.status} - ${detail}`, {
|
|
983
1238
|
status: response.status,
|
|
984
|
-
...
|
|
985
|
-
...
|
|
986
|
-
...extraMeta
|
|
1239
|
+
...extraMeta,
|
|
1240
|
+
...capability
|
|
987
1241
|
}));
|
|
988
1242
|
}
|
|
989
1243
|
normalizeSignedReadUrlResponse(data) {
|
|
@@ -1017,23 +1271,35 @@ var KVService = class extends BaseService {
|
|
|
1017
1271
|
));
|
|
1018
1272
|
}
|
|
1019
1273
|
const path = this.getFullPath(key, options?.prefix);
|
|
1274
|
+
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
1020
1275
|
try {
|
|
1021
1276
|
const response = await this.invokeOperation(
|
|
1022
1277
|
path,
|
|
1023
1278
|
KVAction.GET,
|
|
1024
1279
|
void 0,
|
|
1025
|
-
|
|
1280
|
+
request.signal,
|
|
1026
1281
|
options?.maxResponseBytes === void 0 ? void 0 : { "x-tinycloud-max-response-bytes": String(options.maxResponseBytes) }
|
|
1027
1282
|
);
|
|
1028
1283
|
if (!response.ok) {
|
|
1029
1284
|
if (response.status === 401 || response.status === 403) {
|
|
1030
|
-
const errorText2 = await
|
|
1285
|
+
const errorText2 = await this.readAuthorizationText(response);
|
|
1031
1286
|
const permissionHint = parsePermissionHintFromErrorText(errorText2);
|
|
1287
|
+
const structuredCapability = permissionHint?.service === "tinycloud.kv" && permissionHint.space === this.context.session.spaceId && permissionHint.path === path ? validatedCapabilityOf({
|
|
1288
|
+
service: "kv",
|
|
1289
|
+
code: ErrorCodes.AUTH_UNAUTHORIZED,
|
|
1290
|
+
meta: {
|
|
1291
|
+
status: response.status,
|
|
1292
|
+
resource: `${this.context.session.spaceId}/kv/${path}`,
|
|
1293
|
+
requiredAction: KVAction.GET
|
|
1294
|
+
}
|
|
1295
|
+
}) : void 0;
|
|
1032
1296
|
return this.authorizationFailure(
|
|
1033
1297
|
`Failed to get key ${JSON.stringify(key)}`,
|
|
1034
1298
|
response,
|
|
1035
1299
|
errorText2,
|
|
1036
|
-
|
|
1300
|
+
[path],
|
|
1301
|
+
KVAction.GET,
|
|
1302
|
+
structuredCapability ? { permissionHint, ...structuredCapability } : {}
|
|
1037
1303
|
);
|
|
1038
1304
|
}
|
|
1039
1305
|
if (response.status === 404) {
|
|
@@ -1068,6 +1334,8 @@ var KVService = class extends BaseService {
|
|
|
1068
1334
|
});
|
|
1069
1335
|
} catch (error) {
|
|
1070
1336
|
return err(wrapError("kv", error));
|
|
1337
|
+
} finally {
|
|
1338
|
+
request.dispose();
|
|
1071
1339
|
}
|
|
1072
1340
|
});
|
|
1073
1341
|
}
|
|
@@ -1095,12 +1363,13 @@ var KVService = class extends BaseService {
|
|
|
1095
1363
|
}
|
|
1096
1364
|
const path = this.getFullPath(key, options?.prefix);
|
|
1097
1365
|
const body = this.serializePutValue(value, options?.contentType);
|
|
1366
|
+
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
1098
1367
|
try {
|
|
1099
1368
|
const response = await this.invokeOperation(
|
|
1100
1369
|
path,
|
|
1101
1370
|
KVAction.PUT,
|
|
1102
1371
|
body,
|
|
1103
|
-
|
|
1372
|
+
request.signal,
|
|
1104
1373
|
{
|
|
1105
1374
|
...options?.ifMatch === void 0 ? {} : { "if-match": options.ifMatch },
|
|
1106
1375
|
...options?.ifNoneMatch === void 0 ? {} : { "if-none-match": options.ifNoneMatch }
|
|
@@ -1110,7 +1379,9 @@ var KVService = class extends BaseService {
|
|
|
1110
1379
|
return this.authorizationFailure(
|
|
1111
1380
|
`Failed to put key ${JSON.stringify(key)}`,
|
|
1112
1381
|
response,
|
|
1113
|
-
await
|
|
1382
|
+
await this.readAuthorizationText(response),
|
|
1383
|
+
[path],
|
|
1384
|
+
KVAction.PUT
|
|
1114
1385
|
);
|
|
1115
1386
|
}
|
|
1116
1387
|
if (!response.ok) {
|
|
@@ -1154,6 +1425,8 @@ var KVService = class extends BaseService {
|
|
|
1154
1425
|
});
|
|
1155
1426
|
} catch (error) {
|
|
1156
1427
|
return err(wrapError("kv", error));
|
|
1428
|
+
} finally {
|
|
1429
|
+
request.dispose();
|
|
1157
1430
|
}
|
|
1158
1431
|
});
|
|
1159
1432
|
}
|
|
@@ -1193,6 +1466,7 @@ var KVService = class extends BaseService {
|
|
|
1193
1466
|
seen.add(path);
|
|
1194
1467
|
}
|
|
1195
1468
|
let requestMayHaveDispatched = false;
|
|
1469
|
+
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
1196
1470
|
try {
|
|
1197
1471
|
const body = new FormData();
|
|
1198
1472
|
for (let index = 0; index < items.length; index++) {
|
|
@@ -1215,7 +1489,7 @@ var KVService = class extends BaseService {
|
|
|
1215
1489
|
method: "POST",
|
|
1216
1490
|
headers,
|
|
1217
1491
|
body,
|
|
1218
|
-
signal:
|
|
1492
|
+
signal: request.signal
|
|
1219
1493
|
};
|
|
1220
1494
|
const fetchFn = this.context.fetch;
|
|
1221
1495
|
requestMayHaveDispatched = true;
|
|
@@ -1239,7 +1513,9 @@ var KVService = class extends BaseService {
|
|
|
1239
1513
|
return this.authorizationFailure(
|
|
1240
1514
|
`Failed to batch put ${items.length} key(s)`,
|
|
1241
1515
|
response,
|
|
1242
|
-
errorText
|
|
1516
|
+
errorText,
|
|
1517
|
+
paths,
|
|
1518
|
+
KVAction.PUT
|
|
1243
1519
|
);
|
|
1244
1520
|
}
|
|
1245
1521
|
const quotaError = this.handleQuotaErrorResponse(
|
|
@@ -1264,14 +1540,16 @@ var KVService = class extends BaseService {
|
|
|
1264
1540
|
rawBody = await response.json();
|
|
1265
1541
|
} catch (jsonError) {
|
|
1266
1542
|
const cause = jsonError instanceof Error ? jsonError : new Error(String(jsonError));
|
|
1543
|
+
const timedOut = isRequestTimeoutError(cause);
|
|
1267
1544
|
return err(
|
|
1268
1545
|
serviceError(
|
|
1269
|
-
ErrorCodes.NETWORK_ERROR,
|
|
1270
|
-
`KV batchPut response was not valid JSON: ${cause.message}`,
|
|
1546
|
+
timedOut ? ErrorCodes.TIMEOUT : ErrorCodes.NETWORK_ERROR,
|
|
1547
|
+
timedOut ? `KV batchPut response body did not arrive in time: ${cause.message}` : `KV batchPut response was not valid JSON: ${cause.message}`,
|
|
1271
1548
|
"kv",
|
|
1272
1549
|
{
|
|
1273
1550
|
cause,
|
|
1274
1551
|
meta: {
|
|
1552
|
+
...timedOut ? { timeoutMs: cause.timeoutMs } : {},
|
|
1275
1553
|
requestMayHaveDispatched: true,
|
|
1276
1554
|
responseReceived: true,
|
|
1277
1555
|
status: response.status,
|
|
@@ -1326,6 +1604,8 @@ var KVService = class extends BaseService {
|
|
|
1326
1604
|
...wrapped,
|
|
1327
1605
|
meta: { ...wrapped.meta, requestMayHaveDispatched }
|
|
1328
1606
|
});
|
|
1607
|
+
} finally {
|
|
1608
|
+
request.dispose();
|
|
1329
1609
|
}
|
|
1330
1610
|
});
|
|
1331
1611
|
}
|
|
@@ -1348,23 +1628,26 @@ var KVService = class extends BaseService {
|
|
|
1348
1628
|
if (options?.path) {
|
|
1349
1629
|
listPath = listPath ? `${listPath}/${options.path}` : options.path;
|
|
1350
1630
|
}
|
|
1631
|
+
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
1351
1632
|
try {
|
|
1352
1633
|
const response = await this.invokeOperation(
|
|
1353
1634
|
listPath,
|
|
1354
1635
|
KVAction.LIST,
|
|
1355
1636
|
void 0,
|
|
1356
|
-
|
|
1637
|
+
request.signal,
|
|
1357
1638
|
options?.limit === void 0 && options?.cursor === void 0 ? void 0 : {
|
|
1358
1639
|
...options?.limit === void 0 ? {} : { "x-tinycloud-limit": String(options.limit) },
|
|
1359
1640
|
...options?.cursor === void 0 ? {} : { "x-tinycloud-cursor": options.cursor }
|
|
1360
1641
|
}
|
|
1361
1642
|
);
|
|
1362
1643
|
if (!response.ok) {
|
|
1363
|
-
if (response.status === 401) {
|
|
1644
|
+
if (response.status === 401 || response.status === 403) {
|
|
1364
1645
|
return this.authorizationFailure(
|
|
1365
1646
|
"Failed to list keys",
|
|
1366
1647
|
response,
|
|
1367
|
-
await
|
|
1648
|
+
await this.readAuthorizationText(response),
|
|
1649
|
+
[listPath],
|
|
1650
|
+
KVAction.LIST
|
|
1368
1651
|
);
|
|
1369
1652
|
}
|
|
1370
1653
|
const errorText = await response.text();
|
|
@@ -1392,6 +1675,8 @@ var KVService = class extends BaseService {
|
|
|
1392
1675
|
});
|
|
1393
1676
|
} catch (error) {
|
|
1394
1677
|
return err(wrapError("kv", error));
|
|
1678
|
+
} finally {
|
|
1679
|
+
request.dispose();
|
|
1395
1680
|
}
|
|
1396
1681
|
});
|
|
1397
1682
|
}
|
|
@@ -1404,20 +1689,23 @@ var KVService = class extends BaseService {
|
|
|
1404
1689
|
return err(authRequiredError("kv"));
|
|
1405
1690
|
}
|
|
1406
1691
|
const path = this.getFullPath(key, options?.prefix);
|
|
1692
|
+
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
1407
1693
|
try {
|
|
1408
1694
|
const response = await this.invokeOperation(
|
|
1409
1695
|
path,
|
|
1410
1696
|
KVAction.DELETE,
|
|
1411
1697
|
void 0,
|
|
1412
|
-
|
|
1698
|
+
request.signal,
|
|
1413
1699
|
options?.ifMatch === void 0 ? void 0 : { "if-match": options.ifMatch }
|
|
1414
1700
|
);
|
|
1415
1701
|
if (!response.ok) {
|
|
1416
|
-
if (response.status === 401) {
|
|
1702
|
+
if (response.status === 401 || response.status === 403) {
|
|
1417
1703
|
return this.authorizationFailure(
|
|
1418
1704
|
`Failed to delete key ${JSON.stringify(key)}`,
|
|
1419
1705
|
response,
|
|
1420
|
-
await
|
|
1706
|
+
await this.readAuthorizationText(response),
|
|
1707
|
+
[path],
|
|
1708
|
+
KVAction.DELETE
|
|
1421
1709
|
);
|
|
1422
1710
|
}
|
|
1423
1711
|
if (response.status === 404) {
|
|
@@ -1455,6 +1743,8 @@ var KVService = class extends BaseService {
|
|
|
1455
1743
|
});
|
|
1456
1744
|
} catch (error) {
|
|
1457
1745
|
return err(wrapError("kv", error));
|
|
1746
|
+
} finally {
|
|
1747
|
+
request.dispose();
|
|
1458
1748
|
}
|
|
1459
1749
|
});
|
|
1460
1750
|
}
|
|
@@ -1467,19 +1757,22 @@ var KVService = class extends BaseService {
|
|
|
1467
1757
|
return err(authRequiredError("kv"));
|
|
1468
1758
|
}
|
|
1469
1759
|
const path = this.getFullPath(key, options?.prefix);
|
|
1760
|
+
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
1470
1761
|
try {
|
|
1471
1762
|
const response = await this.invokeOperation(
|
|
1472
1763
|
path,
|
|
1473
1764
|
KVAction.HEAD,
|
|
1474
1765
|
void 0,
|
|
1475
|
-
|
|
1766
|
+
request.signal
|
|
1476
1767
|
);
|
|
1477
1768
|
if (!response.ok) {
|
|
1478
|
-
if (response.status === 401) {
|
|
1769
|
+
if (response.status === 401 || response.status === 403) {
|
|
1479
1770
|
return this.authorizationFailure(
|
|
1480
1771
|
`Failed to get metadata for key ${JSON.stringify(key)}`,
|
|
1481
1772
|
response,
|
|
1482
|
-
await
|
|
1773
|
+
await this.readAuthorizationText(response),
|
|
1774
|
+
[path],
|
|
1775
|
+
KVAction.HEAD
|
|
1483
1776
|
);
|
|
1484
1777
|
}
|
|
1485
1778
|
if (response.status === 404) {
|
|
@@ -1501,6 +1794,8 @@ var KVService = class extends BaseService {
|
|
|
1501
1794
|
});
|
|
1502
1795
|
} catch (error) {
|
|
1503
1796
|
return err(wrapError("kv", error));
|
|
1797
|
+
} finally {
|
|
1798
|
+
request.dispose();
|
|
1504
1799
|
}
|
|
1505
1800
|
});
|
|
1506
1801
|
}
|
|
@@ -1538,15 +1833,16 @@ var KVService = class extends BaseService {
|
|
|
1538
1833
|
if (options?.etag !== void 0) {
|
|
1539
1834
|
body.etag = options.etag;
|
|
1540
1835
|
}
|
|
1836
|
+
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
1541
1837
|
try {
|
|
1542
1838
|
const response = await this.context.fetch(`${this.host}/signed/kv`, {
|
|
1543
1839
|
method: "POST",
|
|
1544
1840
|
headers: this.withJsonContentType(headers),
|
|
1545
1841
|
body: JSON.stringify(body),
|
|
1546
|
-
signal:
|
|
1842
|
+
signal: request.signal
|
|
1547
1843
|
});
|
|
1548
1844
|
if (!response.ok) {
|
|
1549
|
-
return this.createSignedReadUrlError(response, key);
|
|
1845
|
+
return this.createSignedReadUrlError(response, key, path);
|
|
1550
1846
|
}
|
|
1551
1847
|
const signedUrl = this.normalizeSignedReadUrlResponse(
|
|
1552
1848
|
await response.json()
|
|
@@ -1563,6 +1859,8 @@ var KVService = class extends BaseService {
|
|
|
1563
1859
|
return ok(signedUrl);
|
|
1564
1860
|
} catch (error) {
|
|
1565
1861
|
return err(wrapError("kv", error));
|
|
1862
|
+
} finally {
|
|
1863
|
+
request.dispose();
|
|
1566
1864
|
}
|
|
1567
1865
|
});
|
|
1568
1866
|
}
|