@tinycloud/sdk-services 3.1.0 → 3.2.0-beta.3
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-DWbPbov7.d.cts → BaseService-BDQXjWMx.d.cts} +1 -1
- package/dist/{BaseService-B_UKhVht.d.ts → BaseService-Di_hVfUV.d.ts} +1 -1
- package/dist/encryption/index.cjs +7 -1
- 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 +7 -1
- package/dist/encryption/index.js.map +1 -1
- package/dist/index.cjs +683 -362
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +81 -29
- package/dist/index.d.ts +81 -29
- package/dist/index.js +674 -362
- 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 +532 -336
- package/dist/kv/index.cjs.map +1 -1
- package/dist/kv/index.d.cts +17 -468
- package/dist/kv/index.d.ts +17 -468
- package/dist/kv/index.js +526 -335
- package/dist/kv/index.js.map +1 -1
- package/dist/kv/replication/index.cjs +1375 -0
- package/dist/kv/replication/index.cjs.map +1 -0
- package/dist/kv/replication/index.d.cts +9 -0
- package/dist/kv/replication/index.d.ts +9 -0
- package/dist/kv/replication/index.js +1342 -0
- package/dist/kv/replication/index.js.map +1 -0
- package/dist/scope-BmLxwyl-.d.ts +957 -0
- package/dist/scope-D855kRDx.d.cts +957 -0
- package/dist/sql/index.cjs +12 -6
- package/dist/sql/index.cjs.map +1 -1
- package/dist/sql/index.d.cts +2 -2
- package/dist/sql/index.d.ts +2 -2
- package/dist/sql/index.js +12 -6
- package/dist/sql/index.js.map +1 -1
- package/dist/{types-CVwwxqXF.d.cts → types-7A5rHWRY.d.cts} +10 -1
- package/dist/{types-CVwwxqXF.d.ts → types-7A5rHWRY.d.ts} +10 -1
- package/package.json +18 -1
package/dist/index.js
CHANGED
|
@@ -60,7 +60,12 @@ var TelemetryEvents = {
|
|
|
60
60
|
SERVICE_ERROR: "service.error",
|
|
61
61
|
SERVICE_RETRY: "service.retry",
|
|
62
62
|
SESSION_CHANGED: "session.changed",
|
|
63
|
-
SESSION_EXPIRED: "session.expired"
|
|
63
|
+
SESSION_EXPIRED: "session.expired",
|
|
64
|
+
REPLICATION_READ: "replication.read",
|
|
65
|
+
REPLICATION_WRITE: "replication.write",
|
|
66
|
+
REPLICATION_SYNC: "replication.sync",
|
|
67
|
+
REPLICATION_STATE: "replication.state",
|
|
68
|
+
REPLICATION_DIVERGENCE: "replication.divergence"
|
|
64
69
|
};
|
|
65
70
|
function ok(data) {
|
|
66
71
|
return { ok: true, data };
|
|
@@ -86,9 +91,34 @@ var SAFE_NUMBER_FIELDS = /* @__PURE__ */ new Set([
|
|
|
86
91
|
"endedAt",
|
|
87
92
|
"startedAt",
|
|
88
93
|
"status",
|
|
89
|
-
"timestamp"
|
|
94
|
+
"timestamp",
|
|
95
|
+
"latencyMs",
|
|
96
|
+
"stalenessMs",
|
|
97
|
+
"count",
|
|
98
|
+
"changes",
|
|
99
|
+
"deleted",
|
|
100
|
+
"fetched",
|
|
101
|
+
"pages",
|
|
102
|
+
"lagMs",
|
|
103
|
+
"pendingCleared"
|
|
90
104
|
]);
|
|
91
105
|
var SAFE_BOOLEAN_FIELDS = /* @__PURE__ */ new Set(["authenticated", "ok", "persisted"]);
|
|
106
|
+
var SAFE_REPLICATION_FIELDS = {
|
|
107
|
+
source: ["replica", "network", "none"],
|
|
108
|
+
reason: ["hit", "absent", "deleted", "content_missing", "coverage_incomplete", "not_covered", "pending_write", "grant_missing", "grant_expired", "grant_revoked", "grant_not_yet_valid", "stale", "replica_error", "runtime_unsupported", "replica_unavailable", "unsupported_option", "network_cursor", "cursor_restart", "aborted", "REPLICA_BEHIND_OWN_WRITES", "REPLICA_UNPROVEN_SINCE_START", "NETWORK_REQUESTED"],
|
|
109
|
+
op: ["get", "list", "put", "delete", "batchPut"],
|
|
110
|
+
trigger: ["start", "interval", "stale_read", "manual"],
|
|
111
|
+
outcome: ["found", "not_found", "error", "committed", "failed", "ambiguous", "ok", "busy", "aborted"],
|
|
112
|
+
class: ["offline", "authority", "storage", "node"],
|
|
113
|
+
state: ["opened", "grant_installed", "grant_missing", "runtime_unsupported", "unavailable", "revoked", "recreated", "purged", "purge_failed", "pending_store_error", "pinned", "pending_cleared", "closed", "idle", "ready"],
|
|
114
|
+
kind: ["value", "missing_local", "extra_local", "keys", "order"],
|
|
115
|
+
coverage: ["empty", "bootstrapping", "complete"],
|
|
116
|
+
authority: ["valid", "expired", "revoked", "not-yet-valid"],
|
|
117
|
+
code: ["NETWORK_ERROR", "TIMEOUT", "ABORTED", "REPLICA_BUSY", "REPLICA_BEHIND_OWN_WRITES", "REPLICA_UNPROVEN_SINCE_START", "NETWORK_REQUESTED", "INTEGRITY_ERROR", "STORAGE_ERROR", "REPLICA_CLOSED", "RUNTIME_UNSUPPORTED", "GRANT_EXPIRED", "GRANT_REVOKED", "GRANT_NOT_YET_VALID", "GRANT_MISSING", "GRANT_INVALID", "SOURCE_CHANGED", "PROTOCOL_ERROR", "SCOPE_VIOLATION", "NODE_ERROR", "CONTENT_MISMATCH", "STORAGE_FULL"],
|
|
118
|
+
pendingState: ["in_flight", "committed", "ambiguous"],
|
|
119
|
+
strategy: ["session", "minted", "installed"],
|
|
120
|
+
verify: ["match", "diverged", "error", "aborted"]
|
|
121
|
+
};
|
|
92
122
|
function finiteNumber(value) {
|
|
93
123
|
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
94
124
|
}
|
|
@@ -125,6 +155,12 @@ function projectDiagnosticData(value) {
|
|
|
125
155
|
const boolean = read(value, key);
|
|
126
156
|
if (typeof boolean === "boolean") projected[key] = boolean;
|
|
127
157
|
}
|
|
158
|
+
for (const [key, allowed] of Object.entries(SAFE_REPLICATION_FIELDS)) {
|
|
159
|
+
const candidate = read(value, key);
|
|
160
|
+
if (typeof candidate === "string" && allowed.includes(candidate)) {
|
|
161
|
+
projected[key] = candidate;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
128
164
|
const url = read(value, "url");
|
|
129
165
|
if (url !== void 0) projected.url = REDACTED;
|
|
130
166
|
const error = read(value, "error");
|
|
@@ -749,6 +785,12 @@ var ServiceContext = class {
|
|
|
749
785
|
get abortSignal() {
|
|
750
786
|
return this._abortController.signal;
|
|
751
787
|
}
|
|
788
|
+
get operationAbortSignal() {
|
|
789
|
+
return this._operationAbortSignal;
|
|
790
|
+
}
|
|
791
|
+
setOperationAbortSignal(signal) {
|
|
792
|
+
this._operationAbortSignal = signal;
|
|
793
|
+
}
|
|
752
794
|
/**
|
|
753
795
|
* Permanently retire this graph after its owner installs a replacement.
|
|
754
796
|
* Unlike `abort()`, retirement never creates a fresh controller, so captured
|
|
@@ -1500,6 +1542,7 @@ var BaseService = class {
|
|
|
1500
1542
|
const parents = [
|
|
1501
1543
|
this.abortController.signal,
|
|
1502
1544
|
this.context?.abortSignal,
|
|
1545
|
+
this.context?.operationAbortSignal,
|
|
1503
1546
|
signal
|
|
1504
1547
|
].filter((parent) => parent !== void 0);
|
|
1505
1548
|
for (const parent of parents) {
|
|
@@ -1745,14 +1788,28 @@ var KVService = class extends BaseService {
|
|
|
1745
1788
|
*/
|
|
1746
1789
|
constructor(config = {}) {
|
|
1747
1790
|
super();
|
|
1791
|
+
this.readThrough = null;
|
|
1792
|
+
this.readThroughReady = null;
|
|
1748
1793
|
this._config = config;
|
|
1749
1794
|
}
|
|
1795
|
+
/** Delay KV routing until an asynchronously loaded read-through is attached. */
|
|
1796
|
+
setReadThroughReady(ready) {
|
|
1797
|
+
const settled = ready.then(() => void 0, () => void 0);
|
|
1798
|
+
this.readThroughReady = settled;
|
|
1799
|
+
void settled.then(() => {
|
|
1800
|
+
if (this.readThroughReady === settled) this.readThroughReady = null;
|
|
1801
|
+
});
|
|
1802
|
+
}
|
|
1750
1803
|
/**
|
|
1751
1804
|
* Get the service configuration.
|
|
1752
1805
|
*/
|
|
1753
1806
|
get config() {
|
|
1754
1807
|
return this._config;
|
|
1755
1808
|
}
|
|
1809
|
+
/** Install or remove the optional replication read-through policy. */
|
|
1810
|
+
setReadThrough(readThrough) {
|
|
1811
|
+
this.readThrough = readThrough;
|
|
1812
|
+
}
|
|
1756
1813
|
handleQuotaErrorResponse(response, errorText, key) {
|
|
1757
1814
|
if (response.status !== 402 && response.status !== 413) {
|
|
1758
1815
|
return void 0;
|
|
@@ -2006,6 +2063,7 @@ var KVService = class extends BaseService {
|
|
|
2006
2063
|
return aligned;
|
|
2007
2064
|
}
|
|
2008
2065
|
async batchRead(keys, action, options) {
|
|
2066
|
+
if (this.readThroughReady !== null) await this.readThroughReady;
|
|
2009
2067
|
if (!this.requireAuth()) return err(authRequiredError("kv"));
|
|
2010
2068
|
if (keys.length === 0) return ok({ results: [], count: 0 });
|
|
2011
2069
|
if (keys.length > MAX_KV_BATCH_READ_ITEMS) {
|
|
@@ -2256,6 +2314,7 @@ var KVService = class extends BaseService {
|
|
|
2256
2314
|
*/
|
|
2257
2315
|
async get(key, options) {
|
|
2258
2316
|
return this.withTelemetry("get", key, async () => {
|
|
2317
|
+
if (this.readThroughReady !== null) await this.readThroughReady;
|
|
2259
2318
|
if (!this.requireAuth()) {
|
|
2260
2319
|
return err(authRequiredError("kv"));
|
|
2261
2320
|
}
|
|
@@ -2269,64 +2328,38 @@ var KVService = class extends BaseService {
|
|
|
2269
2328
|
const path = this.getFullPath(key, options?.prefix);
|
|
2270
2329
|
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
2271
2330
|
try {
|
|
2272
|
-
const
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
response,
|
|
2295
|
-
errorText2,
|
|
2296
|
-
[path],
|
|
2297
|
-
KVAction.GET,
|
|
2298
|
-
structuredCapability ? { permissionHint, ...structuredCapability } : {}
|
|
2299
|
-
);
|
|
2300
|
-
}
|
|
2301
|
-
if (response.status === 404) {
|
|
2302
|
-
return this.classifyNotFound(response, key);
|
|
2303
|
-
}
|
|
2304
|
-
const errorText = await response.text();
|
|
2305
|
-
if (response.status === 413) {
|
|
2306
|
-
return err(serviceError(
|
|
2307
|
-
ErrorCodes.KV_RESPONSE_TOO_LARGE,
|
|
2308
|
-
`KV value at key ${JSON.stringify(key)} exceeds the requested response limit`,
|
|
2309
|
-
"kv",
|
|
2310
|
-
{ meta: { status: response.status, statusText: response.statusText } }
|
|
2311
|
-
));
|
|
2331
|
+
const network = () => this.getFromNetwork(key, path, options, request.signal);
|
|
2332
|
+
if (options?.source === "network") {
|
|
2333
|
+
const observer = this.readThrough;
|
|
2334
|
+
if (observer === null) return await network();
|
|
2335
|
+
let outcome = "error";
|
|
2336
|
+
const startedAt = Date.now();
|
|
2337
|
+
try {
|
|
2338
|
+
const result = await network();
|
|
2339
|
+
outcome = result.ok ? "found" : result.error.code === ErrorCodes.KV_NOT_FOUND ? "not_found" : "error";
|
|
2340
|
+
return result;
|
|
2341
|
+
} finally {
|
|
2342
|
+
try {
|
|
2343
|
+
observer.observeNetworkRequested({
|
|
2344
|
+
op: "get",
|
|
2345
|
+
space: this.context.session.spaceId,
|
|
2346
|
+
path,
|
|
2347
|
+
reason: "NETWORK_REQUESTED",
|
|
2348
|
+
outcome,
|
|
2349
|
+
latencyMs: Date.now() - startedAt
|
|
2350
|
+
});
|
|
2351
|
+
} catch {
|
|
2352
|
+
}
|
|
2312
2353
|
}
|
|
2313
|
-
return err(
|
|
2314
|
-
serviceError(
|
|
2315
|
-
ErrorCodes.NETWORK_ERROR,
|
|
2316
|
-
`Failed to get key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
|
|
2317
|
-
"kv",
|
|
2318
|
-
{ meta: { status: response.status, statusText: response.statusText } }
|
|
2319
|
-
)
|
|
2320
|
-
);
|
|
2321
2354
|
}
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2355
|
+
if (this.readThrough === null) return await network();
|
|
2356
|
+
return await this.readThrough.get({
|
|
2357
|
+
space: this.context.session.spaceId,
|
|
2358
|
+
key,
|
|
2359
|
+
path,
|
|
2360
|
+
options,
|
|
2361
|
+
signal: request.signal,
|
|
2362
|
+
network
|
|
2330
2363
|
});
|
|
2331
2364
|
} catch (error) {
|
|
2332
2365
|
return err(wrapError("kv", error));
|
|
@@ -2335,6 +2368,56 @@ var KVService = class extends BaseService {
|
|
|
2335
2368
|
}
|
|
2336
2369
|
});
|
|
2337
2370
|
}
|
|
2371
|
+
async getFromNetwork(key, path, options, signal) {
|
|
2372
|
+
const response = await this.invokeOperation(
|
|
2373
|
+
path,
|
|
2374
|
+
KVAction.GET,
|
|
2375
|
+
void 0,
|
|
2376
|
+
signal,
|
|
2377
|
+
options?.maxResponseBytes === void 0 ? void 0 : { "x-tinycloud-max-response-bytes": String(options.maxResponseBytes) }
|
|
2378
|
+
);
|
|
2379
|
+
if (!response.ok) {
|
|
2380
|
+
if (response.status === 401 || response.status === 403) {
|
|
2381
|
+
const errorText2 = await this.readAuthorizationText(response);
|
|
2382
|
+
const permissionHint = parsePermissionHintFromErrorText(errorText2);
|
|
2383
|
+
const structuredCapability = permissionHint?.service === "tinycloud.kv" && permissionHint.space === this.context.session.spaceId && permissionHint.path === path ? validatedCapabilityOf({
|
|
2384
|
+
service: "kv",
|
|
2385
|
+
code: ErrorCodes.AUTH_UNAUTHORIZED,
|
|
2386
|
+
meta: {
|
|
2387
|
+
status: response.status,
|
|
2388
|
+
resource: `${this.context.session.spaceId}/kv/${path}`,
|
|
2389
|
+
requiredAction: KVAction.GET
|
|
2390
|
+
}
|
|
2391
|
+
}) : void 0;
|
|
2392
|
+
return this.authorizationFailure(
|
|
2393
|
+
`Failed to get key ${JSON.stringify(key)}`,
|
|
2394
|
+
response,
|
|
2395
|
+
errorText2,
|
|
2396
|
+
[path],
|
|
2397
|
+
KVAction.GET,
|
|
2398
|
+
structuredCapability ? { permissionHint, ...structuredCapability } : {}
|
|
2399
|
+
);
|
|
2400
|
+
}
|
|
2401
|
+
if (response.status === 404) return this.classifyNotFound(response, key);
|
|
2402
|
+
const errorText = await response.text();
|
|
2403
|
+
if (response.status === 413) {
|
|
2404
|
+
return err(serviceError(
|
|
2405
|
+
ErrorCodes.KV_RESPONSE_TOO_LARGE,
|
|
2406
|
+
`KV value at key ${JSON.stringify(key)} exceeds the requested response limit`,
|
|
2407
|
+
"kv",
|
|
2408
|
+
{ meta: { status: response.status, statusText: response.statusText } }
|
|
2409
|
+
));
|
|
2410
|
+
}
|
|
2411
|
+
return err(serviceError(
|
|
2412
|
+
ErrorCodes.NETWORK_ERROR,
|
|
2413
|
+
`Failed to get key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
|
|
2414
|
+
"kv",
|
|
2415
|
+
{ meta: { status: response.status, statusText: response.statusText } }
|
|
2416
|
+
));
|
|
2417
|
+
}
|
|
2418
|
+
const data = await this.parseResponse(response, options?.raw, options?.binary);
|
|
2419
|
+
return ok({ data, headers: this.createResponseHeaders(response.headers) });
|
|
2420
|
+
}
|
|
2338
2421
|
async batchGet(keys, options) {
|
|
2339
2422
|
return this.withTelemetry(
|
|
2340
2423
|
"batchGet",
|
|
@@ -2347,6 +2430,7 @@ var KVService = class extends BaseService {
|
|
|
2347
2430
|
*/
|
|
2348
2431
|
async put(key, value, options) {
|
|
2349
2432
|
return this.withTelemetry("put", key, async () => {
|
|
2433
|
+
if (this.readThroughReady !== null) await this.readThroughReady;
|
|
2350
2434
|
if (!this.requireAuth()) {
|
|
2351
2435
|
return err(authRequiredError("kv"));
|
|
2352
2436
|
}
|
|
@@ -2361,63 +2445,15 @@ var KVService = class extends BaseService {
|
|
|
2361
2445
|
const body = this.serializePutValue(value, options?.contentType);
|
|
2362
2446
|
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
2363
2447
|
try {
|
|
2364
|
-
const
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
{
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
);
|
|
2374
|
-
if (response.status === 401 || response.status === 403) {
|
|
2375
|
-
return this.authorizationFailure(
|
|
2376
|
-
`Failed to put key ${JSON.stringify(key)}`,
|
|
2377
|
-
response,
|
|
2378
|
-
await this.readAuthorizationText(response),
|
|
2379
|
-
[path],
|
|
2380
|
-
KVAction.PUT
|
|
2381
|
-
);
|
|
2382
|
-
}
|
|
2383
|
-
if (!response.ok) {
|
|
2384
|
-
const errorText = await response.text();
|
|
2385
|
-
if (response.status === 412) {
|
|
2386
|
-
return err(serviceError(
|
|
2387
|
-
ErrorCodes.KV_PRECONDITION_FAILED,
|
|
2388
|
-
`KV precondition failed for key ${JSON.stringify(key)}`,
|
|
2389
|
-
"kv",
|
|
2390
|
-
{ meta: { status: response.status, statusText: response.statusText } }
|
|
2391
|
-
));
|
|
2392
|
-
}
|
|
2393
|
-
if (response.status === 503 && (options?.ifMatch !== void 0 || options?.ifNoneMatch !== void 0)) {
|
|
2394
|
-
return err(serviceError(
|
|
2395
|
-
ErrorCodes.KV_CONFLICT,
|
|
2396
|
-
`Concurrent KV update conflicted for key ${JSON.stringify(key)}`,
|
|
2397
|
-
"kv",
|
|
2398
|
-
{ meta: { status: response.status, statusText: response.statusText } }
|
|
2399
|
-
));
|
|
2400
|
-
}
|
|
2401
|
-
const quotaError = this.handleQuotaErrorResponse(
|
|
2402
|
-
response,
|
|
2403
|
-
errorText,
|
|
2404
|
-
key
|
|
2405
|
-
);
|
|
2406
|
-
if (quotaError) {
|
|
2407
|
-
return quotaError;
|
|
2408
|
-
}
|
|
2409
|
-
return err(
|
|
2410
|
-
serviceError(
|
|
2411
|
-
ErrorCodes.KV_WRITE_FAILED,
|
|
2412
|
-
`Failed to put key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
|
|
2413
|
-
"kv",
|
|
2414
|
-
{ meta: { status: response.status, statusText: response.statusText } }
|
|
2415
|
-
)
|
|
2416
|
-
);
|
|
2417
|
-
}
|
|
2418
|
-
return ok({
|
|
2419
|
-
data: void 0,
|
|
2420
|
-
headers: this.createResponseHeaders(response.headers)
|
|
2448
|
+
const network = () => this.putToNetwork(key, path, body, options, request.signal);
|
|
2449
|
+
if (this.readThrough === null) return await network();
|
|
2450
|
+
return await this.readThrough.write({
|
|
2451
|
+
op: "put",
|
|
2452
|
+
space: this.context.session.spaceId,
|
|
2453
|
+
entries: [{ path, body }],
|
|
2454
|
+
signal: request.signal,
|
|
2455
|
+
conditional: options?.ifMatch !== void 0 || options?.ifNoneMatch !== void 0,
|
|
2456
|
+
network
|
|
2421
2457
|
});
|
|
2422
2458
|
} catch (error) {
|
|
2423
2459
|
return err(wrapError("kv", error));
|
|
@@ -2426,11 +2462,64 @@ var KVService = class extends BaseService {
|
|
|
2426
2462
|
}
|
|
2427
2463
|
});
|
|
2428
2464
|
}
|
|
2465
|
+
async putToNetwork(key, path, body, options, signal) {
|
|
2466
|
+
const response = await this.invokeOperation(
|
|
2467
|
+
path,
|
|
2468
|
+
KVAction.PUT,
|
|
2469
|
+
body,
|
|
2470
|
+
signal,
|
|
2471
|
+
{
|
|
2472
|
+
...options?.ifMatch === void 0 ? {} : { "if-match": options.ifMatch },
|
|
2473
|
+
...options?.ifNoneMatch === void 0 ? {} : { "if-none-match": options.ifNoneMatch }
|
|
2474
|
+
}
|
|
2475
|
+
);
|
|
2476
|
+
if (response.status === 401 || response.status === 403) {
|
|
2477
|
+
return this.authorizationFailure(
|
|
2478
|
+
`Failed to put key ${JSON.stringify(key)}`,
|
|
2479
|
+
response,
|
|
2480
|
+
await this.readAuthorizationText(response),
|
|
2481
|
+
[path],
|
|
2482
|
+
KVAction.PUT
|
|
2483
|
+
);
|
|
2484
|
+
}
|
|
2485
|
+
if (!response.ok) {
|
|
2486
|
+
const errorText = await response.text();
|
|
2487
|
+
if (response.status === 412) {
|
|
2488
|
+
return err(serviceError(
|
|
2489
|
+
ErrorCodes.KV_PRECONDITION_FAILED,
|
|
2490
|
+
`KV precondition failed for key ${JSON.stringify(key)}`,
|
|
2491
|
+
"kv",
|
|
2492
|
+
{ meta: { status: response.status, statusText: response.statusText } }
|
|
2493
|
+
));
|
|
2494
|
+
}
|
|
2495
|
+
if (response.status === 503 && (options?.ifMatch !== void 0 || options?.ifNoneMatch !== void 0)) {
|
|
2496
|
+
return err(serviceError(
|
|
2497
|
+
ErrorCodes.KV_CONFLICT,
|
|
2498
|
+
`Concurrent KV update conflicted for key ${JSON.stringify(key)}`,
|
|
2499
|
+
"kv",
|
|
2500
|
+
{ meta: { status: response.status, statusText: response.statusText } }
|
|
2501
|
+
));
|
|
2502
|
+
}
|
|
2503
|
+
const quotaError = this.handleQuotaErrorResponse(response, errorText, key);
|
|
2504
|
+
if (quotaError) return quotaError;
|
|
2505
|
+
return err(serviceError(
|
|
2506
|
+
ErrorCodes.KV_WRITE_FAILED,
|
|
2507
|
+
`Failed to put key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
|
|
2508
|
+
"kv",
|
|
2509
|
+
{ meta: { status: response.status, statusText: response.statusText } }
|
|
2510
|
+
));
|
|
2511
|
+
}
|
|
2512
|
+
return ok({
|
|
2513
|
+
data: void 0,
|
|
2514
|
+
headers: this.createResponseHeaders(response.headers)
|
|
2515
|
+
});
|
|
2516
|
+
}
|
|
2429
2517
|
/**
|
|
2430
2518
|
* Store multiple values in one TinyCloud KV invocation.
|
|
2431
2519
|
*/
|
|
2432
2520
|
async batchPut(items, options) {
|
|
2433
2521
|
return this.withTelemetry("batchPut", String(items.length), async () => {
|
|
2522
|
+
if (this.readThroughReady !== null) await this.readThroughReady;
|
|
2434
2523
|
if (!this.requireAuth()) {
|
|
2435
2524
|
return err(authRequiredError("kv"));
|
|
2436
2525
|
}
|
|
@@ -2461,155 +2550,177 @@ var KVService = class extends BaseService {
|
|
|
2461
2550
|
}
|
|
2462
2551
|
seen.add(path);
|
|
2463
2552
|
}
|
|
2464
|
-
let requestMayHaveDispatched = false;
|
|
2465
2553
|
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
2466
2554
|
try {
|
|
2467
|
-
const
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
session,
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2555
|
+
const network = () => this.batchPutToNetwork(items, paths, request.signal);
|
|
2556
|
+
if (this.readThrough === null) return await network();
|
|
2557
|
+
const entries = items.map((item, index) => ({
|
|
2558
|
+
path: paths[index],
|
|
2559
|
+
body: this.serializeBatchPutValue(item)
|
|
2560
|
+
}));
|
|
2561
|
+
return await this.readThrough.write({
|
|
2562
|
+
op: "batchPut",
|
|
2563
|
+
space: session.spaceId,
|
|
2564
|
+
entries,
|
|
2565
|
+
signal: request.signal,
|
|
2566
|
+
network: () => this.batchPutToNetwork(items, paths, request.signal, entries)
|
|
2567
|
+
});
|
|
2568
|
+
} catch (error) {
|
|
2569
|
+
const wrapped = wrapError("kv", error);
|
|
2570
|
+
return err({ ...wrapped, meta: { ...wrapped.meta, requestMayHaveDispatched: false } });
|
|
2571
|
+
} finally {
|
|
2572
|
+
request.dispose();
|
|
2573
|
+
}
|
|
2574
|
+
});
|
|
2575
|
+
}
|
|
2576
|
+
async batchPutToNetwork(items, paths, signal, entries) {
|
|
2577
|
+
const session = this.context.session;
|
|
2578
|
+
let requestMayHaveDispatched = false;
|
|
2579
|
+
try {
|
|
2580
|
+
const body = new FormData();
|
|
2581
|
+
for (let index = 0; index < items.length; index++) {
|
|
2582
|
+
body.append(
|
|
2583
|
+
encodeKvBatchPartName(paths[index]),
|
|
2584
|
+
entries?.[index]?.body ?? this.serializeBatchPutValue(items[index])
|
|
2482
2585
|
);
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
return this.authorizationFailure(
|
|
2510
|
-
`Failed to batch put ${items.length} key(s)`,
|
|
2511
|
-
response,
|
|
2512
|
-
errorText,
|
|
2513
|
-
paths,
|
|
2514
|
-
KVAction.PUT
|
|
2515
|
-
);
|
|
2516
|
-
}
|
|
2517
|
-
const quotaError = this.handleQuotaErrorResponse(
|
|
2518
|
-
response,
|
|
2519
|
-
errorText,
|
|
2520
|
-
"batch"
|
|
2521
|
-
);
|
|
2522
|
-
if (quotaError) {
|
|
2523
|
-
return quotaError;
|
|
2524
|
-
}
|
|
2586
|
+
}
|
|
2587
|
+
const headers = this.context.invokeAny(
|
|
2588
|
+
session,
|
|
2589
|
+
paths.map((path) => ({
|
|
2590
|
+
spaceId: session.spaceId,
|
|
2591
|
+
service: "kv",
|
|
2592
|
+
path,
|
|
2593
|
+
action: KVAction.PUT
|
|
2594
|
+
}))
|
|
2595
|
+
);
|
|
2596
|
+
const url = `${this.host}/invoke`;
|
|
2597
|
+
const init = {
|
|
2598
|
+
method: "POST",
|
|
2599
|
+
headers,
|
|
2600
|
+
body,
|
|
2601
|
+
signal
|
|
2602
|
+
};
|
|
2603
|
+
const fetchFn = this.context.fetch;
|
|
2604
|
+
requestMayHaveDispatched = true;
|
|
2605
|
+
const response = await fetchFn(url, init);
|
|
2606
|
+
if (!response.ok) {
|
|
2607
|
+
let errorText;
|
|
2608
|
+
try {
|
|
2609
|
+
errorText = await response.text();
|
|
2610
|
+
} catch (textError) {
|
|
2611
|
+
const cause = textError instanceof Error ? textError : new Error(String(textError));
|
|
2525
2612
|
return err(
|
|
2526
2613
|
serviceError(
|
|
2527
2614
|
ErrorCodes.KV_WRITE_FAILED,
|
|
2528
|
-
`Failed to batch put ${items.length} key(s): ${response.status} - ${
|
|
2615
|
+
`Failed to batch put ${items.length} key(s): ${response.status} - <response body could not be read: ${cause.message}>`,
|
|
2529
2616
|
"kv",
|
|
2530
|
-
{ meta: { status: response.status, statusText: response.statusText } }
|
|
2617
|
+
{ cause, meta: { status: response.status, statusText: response.statusText } }
|
|
2531
2618
|
)
|
|
2532
2619
|
);
|
|
2533
2620
|
}
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
serviceError(
|
|
2542
|
-
timedOut ? ErrorCodes.TIMEOUT : ErrorCodes.NETWORK_ERROR,
|
|
2543
|
-
timedOut ? `KV batchPut response body did not arrive in time: ${cause.message}` : `KV batchPut response was not valid JSON: ${cause.message}`,
|
|
2544
|
-
"kv",
|
|
2545
|
-
{
|
|
2546
|
-
cause,
|
|
2547
|
-
meta: {
|
|
2548
|
-
...timedOut ? { timeoutMs: cause.timeoutMs } : {},
|
|
2549
|
-
requestMayHaveDispatched: true,
|
|
2550
|
-
responseReceived: true,
|
|
2551
|
-
status: response.status,
|
|
2552
|
-
outcome: "batch-unconfirmed"
|
|
2553
|
-
}
|
|
2554
|
-
}
|
|
2555
|
-
)
|
|
2621
|
+
if (response.status === 401 || response.status === 403) {
|
|
2622
|
+
return this.authorizationFailure(
|
|
2623
|
+
`Failed to batch put ${items.length} key(s)`,
|
|
2624
|
+
response,
|
|
2625
|
+
errorText,
|
|
2626
|
+
paths,
|
|
2627
|
+
KVAction.PUT
|
|
2556
2628
|
);
|
|
2557
2629
|
}
|
|
2558
|
-
const
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
{
|
|
2566
|
-
meta: {
|
|
2567
|
-
requestMayHaveDispatched: true,
|
|
2568
|
-
responseReceived: true,
|
|
2569
|
-
status: response.status,
|
|
2570
|
-
outcome: "batch-unconfirmed"
|
|
2571
|
-
}
|
|
2572
|
-
}
|
|
2573
|
-
)
|
|
2574
|
-
);
|
|
2630
|
+
const quotaError = this.handleQuotaErrorResponse(
|
|
2631
|
+
response,
|
|
2632
|
+
errorText,
|
|
2633
|
+
"batch"
|
|
2634
|
+
);
|
|
2635
|
+
if (quotaError) {
|
|
2636
|
+
return quotaError;
|
|
2575
2637
|
}
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2638
|
+
return err(
|
|
2639
|
+
serviceError(
|
|
2640
|
+
ErrorCodes.KV_WRITE_FAILED,
|
|
2641
|
+
`Failed to batch put ${items.length} key(s): ${response.status} - ${errorText}`,
|
|
2642
|
+
"kv",
|
|
2643
|
+
{ meta: { status: response.status, statusText: response.statusText } }
|
|
2644
|
+
)
|
|
2645
|
+
);
|
|
2646
|
+
}
|
|
2647
|
+
let rawBody;
|
|
2648
|
+
try {
|
|
2649
|
+
rawBody = await response.json();
|
|
2650
|
+
} catch (jsonError) {
|
|
2651
|
+
const cause = jsonError instanceof Error ? jsonError : new Error(String(jsonError));
|
|
2652
|
+
const timedOut = isRequestTimeoutError(cause);
|
|
2653
|
+
return err(
|
|
2654
|
+
serviceError(
|
|
2655
|
+
timedOut ? ErrorCodes.TIMEOUT : ErrorCodes.NETWORK_ERROR,
|
|
2656
|
+
timedOut ? `KV batchPut response body did not arrive in time: ${cause.message}` : `KV batchPut response was not valid JSON: ${cause.message}`,
|
|
2657
|
+
"kv",
|
|
2658
|
+
{
|
|
2659
|
+
cause,
|
|
2660
|
+
meta: {
|
|
2661
|
+
...timedOut ? { timeoutMs: cause.timeoutMs } : {},
|
|
2662
|
+
requestMayHaveDispatched: true,
|
|
2663
|
+
responseReceived: true,
|
|
2664
|
+
status: response.status,
|
|
2665
|
+
outcome: "batch-unconfirmed"
|
|
2592
2666
|
}
|
|
2593
|
-
|
|
2594
|
-
)
|
|
2595
|
-
|
|
2596
|
-
return ok(batchResponse);
|
|
2597
|
-
} catch (error) {
|
|
2598
|
-
const wrapped = wrapError("kv", error);
|
|
2599
|
-
return err({
|
|
2600
|
-
...wrapped,
|
|
2601
|
-
meta: { ...wrapped.meta, requestMayHaveDispatched }
|
|
2602
|
-
});
|
|
2603
|
-
} finally {
|
|
2604
|
-
request.dispose();
|
|
2667
|
+
}
|
|
2668
|
+
)
|
|
2669
|
+
);
|
|
2605
2670
|
}
|
|
2606
|
-
|
|
2671
|
+
const batchResponse = this.normalizeBatchPutResponse(rawBody);
|
|
2672
|
+
if (!batchResponse) {
|
|
2673
|
+
return err(
|
|
2674
|
+
serviceError(
|
|
2675
|
+
ErrorCodes.NETWORK_ERROR,
|
|
2676
|
+
"KV batchPut response did not include matching written keys and count",
|
|
2677
|
+
"kv",
|
|
2678
|
+
{
|
|
2679
|
+
meta: {
|
|
2680
|
+
requestMayHaveDispatched: true,
|
|
2681
|
+
responseReceived: true,
|
|
2682
|
+
status: response.status,
|
|
2683
|
+
outcome: "batch-unconfirmed"
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
)
|
|
2687
|
+
);
|
|
2688
|
+
}
|
|
2689
|
+
const requestedPaths = new Set(paths);
|
|
2690
|
+
const writtenPaths = new Set(batchResponse.written);
|
|
2691
|
+
const matchesRequest = batchResponse.count === paths.length && batchResponse.written.length === paths.length && writtenPaths.size === paths.length && batchResponse.written.every((key) => requestedPaths.has(key));
|
|
2692
|
+
if (!matchesRequest) {
|
|
2693
|
+
return err(
|
|
2694
|
+
serviceError(
|
|
2695
|
+
ErrorCodes.NETWORK_ERROR,
|
|
2696
|
+
`KV batchPut response did not confirm all ${paths.length} requested key(s) were written`,
|
|
2697
|
+
"kv",
|
|
2698
|
+
{
|
|
2699
|
+
meta: {
|
|
2700
|
+
requestMayHaveDispatched: true,
|
|
2701
|
+
responseReceived: true,
|
|
2702
|
+
status: response.status,
|
|
2703
|
+
outcome: "batch-unconfirmed"
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
)
|
|
2707
|
+
);
|
|
2708
|
+
}
|
|
2709
|
+
return ok(batchResponse);
|
|
2710
|
+
} catch (error) {
|
|
2711
|
+
const wrapped = wrapError("kv", error);
|
|
2712
|
+
return err({
|
|
2713
|
+
...wrapped,
|
|
2714
|
+
meta: { ...wrapped.meta, requestMayHaveDispatched }
|
|
2715
|
+
});
|
|
2716
|
+
}
|
|
2607
2717
|
}
|
|
2608
2718
|
/**
|
|
2609
2719
|
* List keys with optional prefix filtering.
|
|
2610
2720
|
*/
|
|
2611
2721
|
async list(options) {
|
|
2612
2722
|
return this.withTelemetry("list", options?.prefix, async () => {
|
|
2723
|
+
if (this.readThroughReady !== null) await this.readThroughReady;
|
|
2613
2724
|
if (!this.requireAuth()) {
|
|
2614
2725
|
return err(authRequiredError("kv"));
|
|
2615
2726
|
}
|
|
@@ -2626,49 +2737,44 @@ var KVService = class extends BaseService {
|
|
|
2626
2737
|
}
|
|
2627
2738
|
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
2628
2739
|
try {
|
|
2629
|
-
const
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2740
|
+
const network = () => this.listFromNetwork(listPath, options, request.signal);
|
|
2741
|
+
let page;
|
|
2742
|
+
if (options?.source === "network") {
|
|
2743
|
+
const observer = this.readThrough;
|
|
2744
|
+
if (observer === null) {
|
|
2745
|
+
page = await network();
|
|
2746
|
+
} else {
|
|
2747
|
+
let outcome = "error";
|
|
2748
|
+
const startedAt = Date.now();
|
|
2749
|
+
try {
|
|
2750
|
+
page = await network();
|
|
2751
|
+
outcome = page.ok ? "found" : "error";
|
|
2752
|
+
} finally {
|
|
2753
|
+
try {
|
|
2754
|
+
observer.observeNetworkRequested({
|
|
2755
|
+
op: "list",
|
|
2756
|
+
space: this.context.session.spaceId,
|
|
2757
|
+
path: listPath,
|
|
2758
|
+
reason: "NETWORK_REQUESTED",
|
|
2759
|
+
outcome,
|
|
2760
|
+
latencyMs: Date.now() - startedAt
|
|
2761
|
+
});
|
|
2762
|
+
} catch {
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2648
2765
|
}
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
let keys = await this.parseResponse(response, options?.raw);
|
|
2660
|
-
keys = keys ?? [];
|
|
2661
|
-
if (options?.removePrefix && listPath) {
|
|
2662
|
-
const prefixWithSlash = listPath.endsWith("/") ? listPath : `${listPath}/`;
|
|
2663
|
-
keys = keys.map(
|
|
2664
|
-
(key) => key.startsWith(prefixWithSlash) ? key.slice(prefixWithSlash.length) : key
|
|
2665
|
-
);
|
|
2766
|
+
} else if (this.readThrough === null) {
|
|
2767
|
+
page = await network();
|
|
2768
|
+
} else {
|
|
2769
|
+
page = await this.readThrough.list({
|
|
2770
|
+
space: this.context.session.spaceId,
|
|
2771
|
+
listPath,
|
|
2772
|
+
options,
|
|
2773
|
+
signal: request.signal,
|
|
2774
|
+
network
|
|
2775
|
+
});
|
|
2666
2776
|
}
|
|
2667
|
-
return ok(
|
|
2668
|
-
keys,
|
|
2669
|
-
...response.headers.get("x-tinycloud-truncated") === null ? {} : { truncated: response.headers.get("x-tinycloud-truncated") === "true" },
|
|
2670
|
-
...response.headers.get("x-tinycloud-next-cursor") === null ? {} : { nextCursor: response.headers.get("x-tinycloud-next-cursor") ?? void 0 }
|
|
2671
|
-
});
|
|
2777
|
+
return page.ok ? this.finishList(page.data, listPath, options?.removePrefix) : page;
|
|
2672
2778
|
} catch (error) {
|
|
2673
2779
|
return err(wrapError("kv", error));
|
|
2674
2780
|
} finally {
|
|
@@ -2676,6 +2782,56 @@ var KVService = class extends BaseService {
|
|
|
2676
2782
|
}
|
|
2677
2783
|
});
|
|
2678
2784
|
}
|
|
2785
|
+
async listFromNetwork(listPath, options, signal) {
|
|
2786
|
+
const response = await this.invokeOperation(
|
|
2787
|
+
listPath,
|
|
2788
|
+
KVAction.LIST,
|
|
2789
|
+
void 0,
|
|
2790
|
+
signal,
|
|
2791
|
+
options?.limit === void 0 && options?.cursor === void 0 ? void 0 : {
|
|
2792
|
+
...options?.limit === void 0 ? {} : { "x-tinycloud-limit": String(options.limit) },
|
|
2793
|
+
...options?.cursor === void 0 ? {} : { "x-tinycloud-cursor": options.cursor }
|
|
2794
|
+
}
|
|
2795
|
+
);
|
|
2796
|
+
if (!response.ok) {
|
|
2797
|
+
if (response.status === 401 || response.status === 403) {
|
|
2798
|
+
return this.authorizationFailure(
|
|
2799
|
+
"Failed to list keys",
|
|
2800
|
+
response,
|
|
2801
|
+
await this.readAuthorizationText(response),
|
|
2802
|
+
[listPath],
|
|
2803
|
+
KVAction.LIST
|
|
2804
|
+
);
|
|
2805
|
+
}
|
|
2806
|
+
const errorText = await response.text();
|
|
2807
|
+
return err(serviceError(
|
|
2808
|
+
ErrorCodes.NETWORK_ERROR,
|
|
2809
|
+
`Failed to list keys: ${response.status} - ${errorText}`,
|
|
2810
|
+
"kv",
|
|
2811
|
+
{ meta: { status: response.status, statusText: response.statusText } }
|
|
2812
|
+
));
|
|
2813
|
+
}
|
|
2814
|
+
const keys = await this.parseResponse(response, options?.raw) ?? [];
|
|
2815
|
+
return ok({
|
|
2816
|
+
keys,
|
|
2817
|
+
...response.headers.get("x-tinycloud-truncated") === null ? {} : { truncated: response.headers.get("x-tinycloud-truncated") === "true" },
|
|
2818
|
+
...response.headers.get("x-tinycloud-next-cursor") === null ? {} : { nextCursor: response.headers.get("x-tinycloud-next-cursor") ?? void 0 }
|
|
2819
|
+
});
|
|
2820
|
+
}
|
|
2821
|
+
finishList(page, listPath, removePrefix) {
|
|
2822
|
+
let keys = page.keys;
|
|
2823
|
+
if (removePrefix && listPath) {
|
|
2824
|
+
const prefixWithSlash = listPath.endsWith("/") ? listPath : `${listPath}/`;
|
|
2825
|
+
keys = keys.map(
|
|
2826
|
+
(key) => key.startsWith(prefixWithSlash) ? key.slice(prefixWithSlash.length) : key
|
|
2827
|
+
);
|
|
2828
|
+
}
|
|
2829
|
+
return ok({
|
|
2830
|
+
keys,
|
|
2831
|
+
...page.truncated === void 0 ? {} : { truncated: page.truncated },
|
|
2832
|
+
...page.nextCursor === void 0 ? {} : { nextCursor: page.nextCursor }
|
|
2833
|
+
});
|
|
2834
|
+
}
|
|
2679
2835
|
/**
|
|
2680
2836
|
* Read one page of the `tinycloud.kv/sync` change feed for a prefix.
|
|
2681
2837
|
*/
|
|
@@ -2844,61 +3000,22 @@ var KVService = class extends BaseService {
|
|
|
2844
3000
|
*/
|
|
2845
3001
|
async delete(key, options) {
|
|
2846
3002
|
return this.withTelemetry("delete", key, async () => {
|
|
3003
|
+
if (this.readThroughReady !== null) await this.readThroughReady;
|
|
2847
3004
|
if (!this.requireAuth()) {
|
|
2848
3005
|
return err(authRequiredError("kv"));
|
|
2849
3006
|
}
|
|
2850
3007
|
const path = this.getFullPath(key, options?.prefix);
|
|
2851
3008
|
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
2852
3009
|
try {
|
|
2853
|
-
const
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
return this.authorizationFailure(
|
|
2863
|
-
`Failed to delete key ${JSON.stringify(key)}`,
|
|
2864
|
-
response,
|
|
2865
|
-
await this.readAuthorizationText(response),
|
|
2866
|
-
[path],
|
|
2867
|
-
KVAction.DELETE
|
|
2868
|
-
);
|
|
2869
|
-
}
|
|
2870
|
-
if (response.status === 404) {
|
|
2871
|
-
return this.classifyNotFound(response, key);
|
|
2872
|
-
}
|
|
2873
|
-
const errorText = await response.text();
|
|
2874
|
-
if (response.status === 412) {
|
|
2875
|
-
return err(serviceError(
|
|
2876
|
-
ErrorCodes.KV_PRECONDITION_FAILED,
|
|
2877
|
-
`KV precondition failed for key ${JSON.stringify(key)}`,
|
|
2878
|
-
"kv",
|
|
2879
|
-
{ meta: { status: response.status, statusText: response.statusText } }
|
|
2880
|
-
));
|
|
2881
|
-
}
|
|
2882
|
-
if (response.status === 503 && options?.ifMatch !== void 0) {
|
|
2883
|
-
return err(serviceError(
|
|
2884
|
-
ErrorCodes.KV_CONFLICT,
|
|
2885
|
-
`Concurrent KV delete conflicted for key ${JSON.stringify(key)}`,
|
|
2886
|
-
"kv",
|
|
2887
|
-
{ meta: { status: response.status, statusText: response.statusText } }
|
|
2888
|
-
));
|
|
2889
|
-
}
|
|
2890
|
-
return err(
|
|
2891
|
-
serviceError(
|
|
2892
|
-
ErrorCodes.NETWORK_ERROR,
|
|
2893
|
-
`Failed to delete key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
|
|
2894
|
-
"kv",
|
|
2895
|
-
{ meta: { status: response.status, statusText: response.statusText } }
|
|
2896
|
-
)
|
|
2897
|
-
);
|
|
2898
|
-
}
|
|
2899
|
-
return ok({
|
|
2900
|
-
data: void 0,
|
|
2901
|
-
headers: this.createResponseHeaders(response.headers)
|
|
3010
|
+
const network = () => this.deleteFromNetwork(key, path, options, request.signal);
|
|
3011
|
+
if (this.readThrough === null) return await network();
|
|
3012
|
+
return await this.readThrough.write({
|
|
3013
|
+
op: "delete",
|
|
3014
|
+
space: this.context.session.spaceId,
|
|
3015
|
+
entries: [{ path }],
|
|
3016
|
+
signal: request.signal,
|
|
3017
|
+
conditional: options?.ifMatch !== void 0,
|
|
3018
|
+
network
|
|
2902
3019
|
});
|
|
2903
3020
|
} catch (error) {
|
|
2904
3021
|
return err(wrapError("kv", error));
|
|
@@ -2907,6 +3024,54 @@ var KVService = class extends BaseService {
|
|
|
2907
3024
|
}
|
|
2908
3025
|
});
|
|
2909
3026
|
}
|
|
3027
|
+
async deleteFromNetwork(key, path, options, signal) {
|
|
3028
|
+
const response = await this.invokeOperation(
|
|
3029
|
+
path,
|
|
3030
|
+
KVAction.DELETE,
|
|
3031
|
+
void 0,
|
|
3032
|
+
signal,
|
|
3033
|
+
options?.ifMatch === void 0 ? void 0 : { "if-match": options.ifMatch }
|
|
3034
|
+
);
|
|
3035
|
+
if (!response.ok) {
|
|
3036
|
+
if (response.status === 401 || response.status === 403) {
|
|
3037
|
+
return this.authorizationFailure(
|
|
3038
|
+
`Failed to delete key ${JSON.stringify(key)}`,
|
|
3039
|
+
response,
|
|
3040
|
+
await this.readAuthorizationText(response),
|
|
3041
|
+
[path],
|
|
3042
|
+
KVAction.DELETE
|
|
3043
|
+
);
|
|
3044
|
+
}
|
|
3045
|
+
if (response.status === 404) return this.classifyNotFound(response, key);
|
|
3046
|
+
const errorText = await response.text();
|
|
3047
|
+
if (response.status === 412) {
|
|
3048
|
+
return err(serviceError(
|
|
3049
|
+
ErrorCodes.KV_PRECONDITION_FAILED,
|
|
3050
|
+
`KV precondition failed for key ${JSON.stringify(key)}`,
|
|
3051
|
+
"kv",
|
|
3052
|
+
{ meta: { status: response.status, statusText: response.statusText } }
|
|
3053
|
+
));
|
|
3054
|
+
}
|
|
3055
|
+
if (response.status === 503 && options?.ifMatch !== void 0) {
|
|
3056
|
+
return err(serviceError(
|
|
3057
|
+
ErrorCodes.KV_CONFLICT,
|
|
3058
|
+
`Concurrent KV delete conflicted for key ${JSON.stringify(key)}`,
|
|
3059
|
+
"kv",
|
|
3060
|
+
{ meta: { status: response.status, statusText: response.statusText } }
|
|
3061
|
+
));
|
|
3062
|
+
}
|
|
3063
|
+
return err(serviceError(
|
|
3064
|
+
ErrorCodes.NETWORK_ERROR,
|
|
3065
|
+
`Failed to delete key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
|
|
3066
|
+
"kv",
|
|
3067
|
+
{ meta: { status: response.status, statusText: response.statusText } }
|
|
3068
|
+
));
|
|
3069
|
+
}
|
|
3070
|
+
return ok({
|
|
3071
|
+
data: void 0,
|
|
3072
|
+
headers: this.createResponseHeaders(response.headers)
|
|
3073
|
+
});
|
|
3074
|
+
}
|
|
2910
3075
|
/**
|
|
2911
3076
|
* Get metadata for a key without retrieving the value.
|
|
2912
3077
|
*/
|
|
@@ -2935,7 +3100,7 @@ var KVService = class extends BaseService {
|
|
|
2935
3100
|
);
|
|
2936
3101
|
}
|
|
2937
3102
|
if (response.status === 404) {
|
|
2938
|
-
return this.classifyNotFound(response, key);
|
|
3103
|
+
return await this.classifyNotFound(response, key);
|
|
2939
3104
|
}
|
|
2940
3105
|
const errorText = await response.text();
|
|
2941
3106
|
return err(
|
|
@@ -3001,7 +3166,7 @@ var KVService = class extends BaseService {
|
|
|
3001
3166
|
signal: request.signal
|
|
3002
3167
|
});
|
|
3003
3168
|
if (!response.ok) {
|
|
3004
|
-
return this.createSignedReadUrlError(response, key, path);
|
|
3169
|
+
return await this.createSignedReadUrlError(response, key, path);
|
|
3005
3170
|
}
|
|
3006
3171
|
const signedUrl = this.normalizeSignedReadUrlResponse(
|
|
3007
3172
|
await response.json()
|
|
@@ -3074,6 +3239,64 @@ var KVService = class extends BaseService {
|
|
|
3074
3239
|
*/
|
|
3075
3240
|
KVService.serviceName = "kv";
|
|
3076
3241
|
|
|
3242
|
+
// src/kv/replication/constants.ts
|
|
3243
|
+
var CLEAR_PENDING_WARNING = "Clearing pending writes surrenders read-your-writes protection for the cleared keys. If a write whose record was cleared had an unknown outcome or never finished, it may commit now or later; this device can serve an older value until the next sync after that commit, as it would for a write from another device.";
|
|
3244
|
+
|
|
3245
|
+
// src/kv/replication/identity.ts
|
|
3246
|
+
var SPACE_ID = /^(tinycloud:pkh:eip155:\d+:)([^:]+)(:.+)$/;
|
|
3247
|
+
var PRINCIPAL_DID = /^(did:pkh:eip155:\d+:)([^:]+)$/;
|
|
3248
|
+
function canonicalReplicationIdentity(raw) {
|
|
3249
|
+
if (!raw || typeof raw.host !== "string" || typeof raw.space !== "string" || typeof raw.principal !== "string") {
|
|
3250
|
+
throw new TypeError(
|
|
3251
|
+
"Replication identity host, space, and principal must be strings"
|
|
3252
|
+
);
|
|
3253
|
+
}
|
|
3254
|
+
const authority = /^https?:\/\/([^/?#]*)/i.exec(raw.host)?.[1];
|
|
3255
|
+
let url;
|
|
3256
|
+
try {
|
|
3257
|
+
url = new URL(raw.host);
|
|
3258
|
+
} catch {
|
|
3259
|
+
throw new TypeError(
|
|
3260
|
+
"Replication identity host must be an absolute HTTP(S) URL"
|
|
3261
|
+
);
|
|
3262
|
+
}
|
|
3263
|
+
if (!authority || authority.includes("@") || raw.host.includes("?") || raw.host.includes("#") || url.username || url.password) {
|
|
3264
|
+
throw new TypeError(
|
|
3265
|
+
"Replication identity host must be HTTP(S) without credentials, query, or fragment"
|
|
3266
|
+
);
|
|
3267
|
+
}
|
|
3268
|
+
const space = SPACE_ID.exec(raw.space);
|
|
3269
|
+
const principal = PRINCIPAL_DID.exec(raw.principal);
|
|
3270
|
+
if (!space || !principal) {
|
|
3271
|
+
throw new TypeError(
|
|
3272
|
+
"Replication identity requires a full TinyCloud space id and a pkh principal DID"
|
|
3273
|
+
);
|
|
3274
|
+
}
|
|
3275
|
+
return {
|
|
3276
|
+
host: `${url.origin}${url.pathname.replace(/\/+$/, "")}`,
|
|
3277
|
+
space: `${space[1]}${space[2].toLowerCase()}${space[3]}`,
|
|
3278
|
+
principal: `${principal[1]}${principal[2].toLowerCase()}`
|
|
3279
|
+
};
|
|
3280
|
+
}
|
|
3281
|
+
function replicationIdentityKey(id) {
|
|
3282
|
+
const canonical = canonicalReplicationIdentity(id);
|
|
3283
|
+
return `tc-replication/v1
|
|
3284
|
+
${canonical.host}
|
|
3285
|
+
${canonical.space}
|
|
3286
|
+
${canonical.principal}`;
|
|
3287
|
+
}
|
|
3288
|
+
|
|
3289
|
+
// src/kv/replication/scope.ts
|
|
3290
|
+
function kvPrefixCovers(prefix, key) {
|
|
3291
|
+
if (prefix === "") return true;
|
|
3292
|
+
if (prefix.endsWith("/")) return key.startsWith(prefix);
|
|
3293
|
+
return key === prefix || key.startsWith(`${prefix}/`);
|
|
3294
|
+
}
|
|
3295
|
+
function requiresSecretsOptIn(space, prefix) {
|
|
3296
|
+
if (space === "secrets" || space.endsWith(":secrets")) return true;
|
|
3297
|
+
return prefix.split("/", 1)[0] === "vault";
|
|
3298
|
+
}
|
|
3299
|
+
|
|
3077
3300
|
// src/responseErrors.ts
|
|
3078
3301
|
function parseServiceErrorBody(errorText) {
|
|
3079
3302
|
try {
|
|
@@ -3250,7 +3473,7 @@ var SQLService = class extends BaseService {
|
|
|
3250
3473
|
request.signal
|
|
3251
3474
|
);
|
|
3252
3475
|
if (!response.ok) {
|
|
3253
|
-
return this.handleErrorResponse(response, "query", dbName, [action]);
|
|
3476
|
+
return await this.handleErrorResponse(response, "query", dbName, [action]);
|
|
3254
3477
|
}
|
|
3255
3478
|
const data = await response.json();
|
|
3256
3479
|
return ok(data);
|
|
@@ -3290,7 +3513,7 @@ var SQLService = class extends BaseService {
|
|
|
3290
3513
|
request.signal
|
|
3291
3514
|
);
|
|
3292
3515
|
if (!response.ok) {
|
|
3293
|
-
return this.handleErrorResponse(response, "execute", dbName, requestedActions);
|
|
3516
|
+
return await this.handleErrorResponse(response, "execute", dbName, requestedActions);
|
|
3294
3517
|
}
|
|
3295
3518
|
const data = await response.json();
|
|
3296
3519
|
return ok(data);
|
|
@@ -3316,7 +3539,7 @@ var SQLService = class extends BaseService {
|
|
|
3316
3539
|
request.signal
|
|
3317
3540
|
);
|
|
3318
3541
|
if (!response.ok) {
|
|
3319
|
-
return this.handleErrorResponse(response, "batch", dbName, requestedActions);
|
|
3542
|
+
return await this.handleErrorResponse(response, "batch", dbName, requestedActions);
|
|
3320
3543
|
}
|
|
3321
3544
|
const data = await response.json();
|
|
3322
3545
|
return ok(data);
|
|
@@ -3341,7 +3564,7 @@ var SQLService = class extends BaseService {
|
|
|
3341
3564
|
request.signal
|
|
3342
3565
|
);
|
|
3343
3566
|
if (!response.ok) {
|
|
3344
|
-
return this.handleErrorResponse(response, "executeStatement", dbName, [SQLAction.WRITE]);
|
|
3567
|
+
return await this.handleErrorResponse(response, "executeStatement", dbName, [SQLAction.WRITE]);
|
|
3345
3568
|
}
|
|
3346
3569
|
const data = await response.json();
|
|
3347
3570
|
return ok(data);
|
|
@@ -3366,7 +3589,7 @@ var SQLService = class extends BaseService {
|
|
|
3366
3589
|
request.signal
|
|
3367
3590
|
);
|
|
3368
3591
|
if (!response.ok) {
|
|
3369
|
-
return this.handleErrorResponse(response, "export", dbName, [SQLAction.READ]);
|
|
3592
|
+
return await this.handleErrorResponse(response, "export", dbName, [SQLAction.READ]);
|
|
3370
3593
|
}
|
|
3371
3594
|
const resp = response;
|
|
3372
3595
|
if (typeof resp.blob === "function") {
|
|
@@ -3742,7 +3965,7 @@ var DuckDbService = class extends BaseService {
|
|
|
3742
3965
|
request.signal
|
|
3743
3966
|
);
|
|
3744
3967
|
if (!response.ok) {
|
|
3745
|
-
return this.handleErrorResponse(response, "query");
|
|
3968
|
+
return await this.handleErrorResponse(response, "query");
|
|
3746
3969
|
}
|
|
3747
3970
|
const data = await response.json();
|
|
3748
3971
|
return ok(data);
|
|
@@ -3768,7 +3991,7 @@ var DuckDbService = class extends BaseService {
|
|
|
3768
3991
|
{ Accept: "application/vnd.apache.arrow.stream" }
|
|
3769
3992
|
);
|
|
3770
3993
|
if (!response.ok) {
|
|
3771
|
-
return this.handleErrorResponse(response, "queryArrow");
|
|
3994
|
+
return await this.handleErrorResponse(response, "queryArrow");
|
|
3772
3995
|
}
|
|
3773
3996
|
const buffer = await response.arrayBuffer();
|
|
3774
3997
|
return ok(buffer);
|
|
@@ -3801,7 +4024,7 @@ var DuckDbService = class extends BaseService {
|
|
|
3801
4024
|
request.signal
|
|
3802
4025
|
);
|
|
3803
4026
|
if (!response.ok) {
|
|
3804
|
-
return this.handleErrorResponse(response, "execute");
|
|
4027
|
+
return await this.handleErrorResponse(response, "execute");
|
|
3805
4028
|
}
|
|
3806
4029
|
const data = await response.json();
|
|
3807
4030
|
return ok(data);
|
|
@@ -3833,7 +4056,7 @@ var DuckDbService = class extends BaseService {
|
|
|
3833
4056
|
request.signal
|
|
3834
4057
|
);
|
|
3835
4058
|
if (!response.ok) {
|
|
3836
|
-
return this.handleErrorResponse(response, "batch");
|
|
4059
|
+
return await this.handleErrorResponse(response, "batch");
|
|
3837
4060
|
}
|
|
3838
4061
|
const data = await response.json();
|
|
3839
4062
|
return ok(data);
|
|
@@ -3858,7 +4081,7 @@ var DuckDbService = class extends BaseService {
|
|
|
3858
4081
|
request.signal
|
|
3859
4082
|
);
|
|
3860
4083
|
if (!response.ok) {
|
|
3861
|
-
return this.handleErrorResponse(response, "executeStatement");
|
|
4084
|
+
return await this.handleErrorResponse(response, "executeStatement");
|
|
3862
4085
|
}
|
|
3863
4086
|
const data = await response.json();
|
|
3864
4087
|
return ok(data);
|
|
@@ -3883,7 +4106,7 @@ var DuckDbService = class extends BaseService {
|
|
|
3883
4106
|
request.signal
|
|
3884
4107
|
);
|
|
3885
4108
|
if (!response.ok) {
|
|
3886
|
-
return this.handleErrorResponse(response, "describe");
|
|
4109
|
+
return await this.handleErrorResponse(response, "describe");
|
|
3887
4110
|
}
|
|
3888
4111
|
const data = await response.json();
|
|
3889
4112
|
return ok(data);
|
|
@@ -3908,7 +4131,7 @@ var DuckDbService = class extends BaseService {
|
|
|
3908
4131
|
request.signal
|
|
3909
4132
|
);
|
|
3910
4133
|
if (!response.ok) {
|
|
3911
|
-
return this.handleErrorResponse(response, "export");
|
|
4134
|
+
return await this.handleErrorResponse(response, "export");
|
|
3912
4135
|
}
|
|
3913
4136
|
const blob = await response.blob();
|
|
3914
4137
|
return ok(blob);
|
|
@@ -3943,7 +4166,7 @@ var DuckDbService = class extends BaseService {
|
|
|
3943
4166
|
signal: request.signal
|
|
3944
4167
|
});
|
|
3945
4168
|
if (!response.ok) {
|
|
3946
|
-
return this.handleErrorResponse(response, "import");
|
|
4169
|
+
return await this.handleErrorResponse(response, "import");
|
|
3947
4170
|
}
|
|
3948
4171
|
return ok(void 0);
|
|
3949
4172
|
} catch (error) {
|
|
@@ -5292,6 +5515,12 @@ function defaultVaultMessage(input) {
|
|
|
5292
5515
|
return input.message ?? `Public key not found for ${input.did}`;
|
|
5293
5516
|
case "STORAGE_ERROR":
|
|
5294
5517
|
return input.message ?? input.cause.message;
|
|
5518
|
+
case "ALREADY_EXISTS":
|
|
5519
|
+
return input.message ?? `A value already exists at ${input.key}`;
|
|
5520
|
+
case "AUTH_UNAUTHORIZED":
|
|
5521
|
+
return input.message;
|
|
5522
|
+
case "PERMISSION_DENIED":
|
|
5523
|
+
return input.message;
|
|
5295
5524
|
case "STORAGE_QUOTA_EXCEEDED":
|
|
5296
5525
|
case "STORAGE_LIMIT_REACHED":
|
|
5297
5526
|
return input.message;
|
|
@@ -5305,13 +5534,33 @@ function vaultError(input) {
|
|
|
5305
5534
|
};
|
|
5306
5535
|
return { ok: false, error };
|
|
5307
5536
|
}
|
|
5537
|
+
function typedKvCause(error) {
|
|
5538
|
+
return Object.assign(new Error(error.message), {
|
|
5539
|
+
name: "ServiceError",
|
|
5540
|
+
code: error.code,
|
|
5541
|
+
service: error.service,
|
|
5542
|
+
...error.meta !== void 0 ? { meta: error.meta } : {},
|
|
5543
|
+
...error.cause !== void 0 ? { cause: error.cause } : {}
|
|
5544
|
+
});
|
|
5545
|
+
}
|
|
5308
5546
|
function kvWriteError(context, error) {
|
|
5309
5547
|
if (error.code === ErrorCodes.STORAGE_QUOTA_EXCEEDED || error.code === ErrorCodes.STORAGE_LIMIT_REACHED) {
|
|
5310
5548
|
return vaultError({ code: error.code, message: error.message, meta: error.meta });
|
|
5311
5549
|
}
|
|
5550
|
+
const message = `${context}: ${error.message}`;
|
|
5551
|
+
if (error.code === ErrorCodes.AUTH_UNAUTHORIZED) {
|
|
5552
|
+
const code = authorizationVerdictOf(error) === "unauthenticated" ? "AUTH_UNAUTHORIZED" : "PERMISSION_DENIED";
|
|
5553
|
+
return vaultError({
|
|
5554
|
+
code,
|
|
5555
|
+
message,
|
|
5556
|
+
...error.meta !== void 0 ? { meta: error.meta } : {},
|
|
5557
|
+
cause: typedKvCause(error)
|
|
5558
|
+
});
|
|
5559
|
+
}
|
|
5312
5560
|
return vaultError({
|
|
5313
5561
|
code: "STORAGE_ERROR",
|
|
5314
|
-
cause: new Error(
|
|
5562
|
+
cause: new Error(message, { cause: typedKvCause(error) }),
|
|
5563
|
+
...error.meta !== void 0 ? { meta: error.meta } : {}
|
|
5315
5564
|
});
|
|
5316
5565
|
}
|
|
5317
5566
|
var DataVaultService = class extends BaseService {
|
|
@@ -5591,11 +5840,18 @@ var DataVaultService = class extends BaseService {
|
|
|
5591
5840
|
cause: new Error(envelopeResult.error.message)
|
|
5592
5841
|
});
|
|
5593
5842
|
}
|
|
5594
|
-
const
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
);
|
|
5843
|
+
const valuePath = `vault/${key}`;
|
|
5844
|
+
const valueBody = JSON.stringify(envelopeResult.data);
|
|
5845
|
+
const valuePutResult = options?.ifNoneMatch !== void 0 ? await this.tc.kv.put(valuePath, valueBody, { ifNoneMatch: options.ifNoneMatch }) : await this.tc.kv.put(valuePath, valueBody);
|
|
5598
5846
|
if (!valuePutResult.ok) {
|
|
5847
|
+
if (options?.ifNoneMatch !== void 0 && valuePutResult.error.code === ErrorCodes.KV_PRECONDITION_FAILED) {
|
|
5848
|
+
return vaultError({
|
|
5849
|
+
code: "ALREADY_EXISTS",
|
|
5850
|
+
key,
|
|
5851
|
+
...valuePutResult.error.meta !== void 0 ? { meta: valuePutResult.error.meta } : {},
|
|
5852
|
+
cause: typedKvCause(valuePutResult.error)
|
|
5853
|
+
});
|
|
5854
|
+
}
|
|
5599
5855
|
return kvWriteError("Failed to store encrypted value", valuePutResult.error);
|
|
5600
5856
|
}
|
|
5601
5857
|
return { ok: true, data: void 0 };
|
|
@@ -5622,7 +5878,8 @@ var DataVaultService = class extends BaseService {
|
|
|
5622
5878
|
}
|
|
5623
5879
|
try {
|
|
5624
5880
|
const valueResult = await this.tc.kv.get(`vault/${key}`, {
|
|
5625
|
-
raw: true
|
|
5881
|
+
raw: true,
|
|
5882
|
+
...options?.source === void 0 ? {} : { source: options.source }
|
|
5626
5883
|
});
|
|
5627
5884
|
if (!valueResult.ok) {
|
|
5628
5885
|
return vaultError({ code: "KEY_NOT_FOUND", key });
|
|
@@ -5667,7 +5924,7 @@ var DataVaultService = class extends BaseService {
|
|
|
5667
5924
|
}
|
|
5668
5925
|
let valueResult;
|
|
5669
5926
|
try {
|
|
5670
|
-
valueResult = await this.tc.kv.get(`vault/${key}`, { raw: true });
|
|
5927
|
+
valueResult = await this.tc.kv.get(`vault/${key}`, { raw: true, source: "network" });
|
|
5671
5928
|
} catch {
|
|
5672
5929
|
return { status: "node_unreachable" };
|
|
5673
5930
|
}
|
|
@@ -5736,7 +5993,8 @@ var DataVaultService = class extends BaseService {
|
|
|
5736
5993
|
}
|
|
5737
5994
|
try {
|
|
5738
5995
|
const valueResult = await this.tc.kv.get(`vault/${key}`, {
|
|
5739
|
-
raw: true
|
|
5996
|
+
raw: true,
|
|
5997
|
+
source: "network"
|
|
5740
5998
|
});
|
|
5741
5999
|
if (!valueResult.ok) {
|
|
5742
6000
|
return vaultError({ code: "KEY_NOT_FOUND", key });
|
|
@@ -5763,6 +6021,14 @@ var DataVaultService = class extends BaseService {
|
|
|
5763
6021
|
if (this.usesNetworkEncryption) {
|
|
5764
6022
|
return this.putNetworkEncrypted(key, value, options);
|
|
5765
6023
|
}
|
|
6024
|
+
if (options?.ifNoneMatch !== void 0) {
|
|
6025
|
+
return vaultError({
|
|
6026
|
+
code: "STORAGE_ERROR",
|
|
6027
|
+
cause: new Error(
|
|
6028
|
+
"ifNoneMatch is only supported by network-encrypted vaults"
|
|
6029
|
+
)
|
|
6030
|
+
});
|
|
6031
|
+
}
|
|
5766
6032
|
if (!this._isUnlocked || !this.masterKey) {
|
|
5767
6033
|
return vaultError({
|
|
5768
6034
|
code: "VAULT_LOCKED",
|
|
@@ -5862,7 +6128,8 @@ var DataVaultService = class extends BaseService {
|
|
|
5862
6128
|
}
|
|
5863
6129
|
try {
|
|
5864
6130
|
const keyResult = await this.tc.kv.get(`keys/${key}`, {
|
|
5865
|
-
raw: true
|
|
6131
|
+
raw: true,
|
|
6132
|
+
...options?.source === void 0 ? {} : { source: options.source }
|
|
5866
6133
|
});
|
|
5867
6134
|
if (!keyResult.ok) {
|
|
5868
6135
|
return vaultError({ code: "KEY_NOT_FOUND", key });
|
|
@@ -5871,7 +6138,8 @@ var DataVaultService = class extends BaseService {
|
|
|
5871
6138
|
const keyBlobBytes = base64Decode(keyEnvelope.key);
|
|
5872
6139
|
const entryKey = this.crypto.decrypt(this.masterKey, keyBlobBytes);
|
|
5873
6140
|
const valueResult = await this.tc.kv.get(`vault/${key}`, {
|
|
5874
|
-
raw: true
|
|
6141
|
+
raw: true,
|
|
6142
|
+
...options?.source === void 0 ? {} : { source: options.source }
|
|
5875
6143
|
});
|
|
5876
6144
|
if (!valueResult.ok) {
|
|
5877
6145
|
return vaultError({ code: "KEY_NOT_FOUND", key });
|
|
@@ -5981,6 +6249,7 @@ var DataVaultService = class extends BaseService {
|
|
|
5981
6249
|
prefix: listPrefix,
|
|
5982
6250
|
removePrefix: true,
|
|
5983
6251
|
...options?.limit === void 0 ? {} : { limit: options.limit },
|
|
6252
|
+
...options?.source === void 0 ? {} : { source: options.source },
|
|
5984
6253
|
...options?.cursor === void 0 ? {} : { cursor: options.cursor }
|
|
5985
6254
|
});
|
|
5986
6255
|
if (!listResult.ok) {
|
|
@@ -6574,6 +6843,35 @@ var SECRET_NAME_RE = /^[A-Z][A-Z0-9_]*$/;
|
|
|
6574
6843
|
var SECRET_PREFIX = "secrets/";
|
|
6575
6844
|
var SCOPED_SECRET_PREFIX = "secrets/scoped/";
|
|
6576
6845
|
var RESERVED_SECRET_SCOPES = /* @__PURE__ */ new Set(["default", "global"]);
|
|
6846
|
+
var SECRET_SCOPE_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
6847
|
+
var MAX_SECRET_SCOPE_LENGTH = 64;
|
|
6848
|
+
var TC832_RESERVED_SCOPES = Object.freeze([
|
|
6849
|
+
"default",
|
|
6850
|
+
"global",
|
|
6851
|
+
"scoped"
|
|
6852
|
+
]);
|
|
6853
|
+
function appSecretScope(appId) {
|
|
6854
|
+
if (appId.trim() === "") {
|
|
6855
|
+
throw new Error("App id must be non-empty to derive a secret scope.");
|
|
6856
|
+
}
|
|
6857
|
+
const scope = appId.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
6858
|
+
if (scope === "") {
|
|
6859
|
+
throw new Error(
|
|
6860
|
+
`App id ${JSON.stringify(appId)} must contain at least one letter or number.`
|
|
6861
|
+
);
|
|
6862
|
+
}
|
|
6863
|
+
if (scope.length > MAX_SECRET_SCOPE_LENGTH) {
|
|
6864
|
+
throw new Error(
|
|
6865
|
+
`App id ${JSON.stringify(appId)} yields a secret scope longer than ${MAX_SECRET_SCOPE_LENGTH} characters.`
|
|
6866
|
+
);
|
|
6867
|
+
}
|
|
6868
|
+
if (TC832_RESERVED_SCOPES.includes(scope)) {
|
|
6869
|
+
throw new Error(
|
|
6870
|
+
`App id ${JSON.stringify(appId)} yields the reserved secret scope ${JSON.stringify(scope)}.`
|
|
6871
|
+
);
|
|
6872
|
+
}
|
|
6873
|
+
return scope;
|
|
6874
|
+
}
|
|
6577
6875
|
function canonicalizeSecretScope(scope) {
|
|
6578
6876
|
if (scope === void 0) {
|
|
6579
6877
|
return void 0;
|
|
@@ -6676,7 +6974,9 @@ var SecretsService = class {
|
|
|
6676
6974
|
async get(name, options) {
|
|
6677
6975
|
const secretPath = resolveSecretPathResult(name, options);
|
|
6678
6976
|
if ("ok" in secretPath) return secretPath;
|
|
6679
|
-
const result = await this.vault.get(secretPath.vaultKey)
|
|
6977
|
+
const result = options?.source === void 0 ? await this.vault.get(secretPath.vaultKey) : await this.vault.get(secretPath.vaultKey, {
|
|
6978
|
+
source: options.source
|
|
6979
|
+
});
|
|
6680
6980
|
if (!result.ok) {
|
|
6681
6981
|
return result;
|
|
6682
6982
|
}
|
|
@@ -6686,11 +6986,12 @@ var SecretsService = class {
|
|
|
6686
6986
|
const secretPath = resolveSecretPathResult(name, options);
|
|
6687
6987
|
if ("ok" in secretPath) return secretPath;
|
|
6688
6988
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6689
|
-
|
|
6989
|
+
const payload = {
|
|
6690
6990
|
value,
|
|
6691
6991
|
createdAt: now,
|
|
6692
6992
|
updatedAt: now
|
|
6693
|
-
}
|
|
6993
|
+
};
|
|
6994
|
+
return options?.ifAbsent === true ? this.vault.put(secretPath.vaultKey, payload, { ifNoneMatch: "*" }) : this.vault.put(secretPath.vaultKey, payload);
|
|
6694
6995
|
}
|
|
6695
6996
|
async delete(name, options) {
|
|
6696
6997
|
const secretPath = resolveSecretPathResult(name, options);
|
|
@@ -6709,7 +7010,8 @@ var SecretsService = class {
|
|
|
6709
7010
|
}
|
|
6710
7011
|
const result = await this.vault.list({
|
|
6711
7012
|
prefix,
|
|
6712
|
-
removePrefix: true
|
|
7013
|
+
removePrefix: true,
|
|
7014
|
+
...options?.source === void 0 ? {} : { source: options.source }
|
|
6713
7015
|
});
|
|
6714
7016
|
if (!result.ok) {
|
|
6715
7017
|
return result;
|
|
@@ -6728,6 +7030,7 @@ var SecretsService = class {
|
|
|
6728
7030
|
prefix: "secrets/",
|
|
6729
7031
|
removePrefix: true,
|
|
6730
7032
|
limit: 1e3,
|
|
7033
|
+
source: "network",
|
|
6731
7034
|
...cursor === void 0 ? {} : { cursor }
|
|
6732
7035
|
});
|
|
6733
7036
|
if (!result.ok) {
|
|
@@ -7719,6 +8022,7 @@ EncryptionService.serviceName = "encryption";
|
|
|
7719
8022
|
import { DecryptTransportResponseError as DecryptTransportResponseError3 } from "@tinycloud/sdk-services/internal/decrypt-transport-response-error";
|
|
7720
8023
|
export {
|
|
7721
8024
|
BaseService,
|
|
8025
|
+
CLEAR_PENDING_WARNING,
|
|
7722
8026
|
DECRYPT_ACTION,
|
|
7723
8027
|
DECRYPT_FACT_TYPE,
|
|
7724
8028
|
DECRYPT_RESULT_TYPE,
|
|
@@ -7745,11 +8049,13 @@ export {
|
|
|
7745
8049
|
KVListResultSchema,
|
|
7746
8050
|
KVResponseHeadersSchema,
|
|
7747
8051
|
KVService,
|
|
8052
|
+
MAX_SECRET_SCOPE_LENGTH,
|
|
7748
8053
|
NETWORK_NAME_PATTERN,
|
|
7749
8054
|
NetworkIdError,
|
|
7750
8055
|
PrefixedKVService,
|
|
7751
8056
|
RetryPolicySchema,
|
|
7752
8057
|
SECRET_NAME_RE,
|
|
8058
|
+
SECRET_SCOPE_RE,
|
|
7753
8059
|
SQLAction,
|
|
7754
8060
|
SQLMigrations,
|
|
7755
8061
|
SQLService,
|
|
@@ -7763,6 +8069,7 @@ export {
|
|
|
7763
8069
|
ServiceResponseEventSchema,
|
|
7764
8070
|
ServiceRetryEventSchema,
|
|
7765
8071
|
ServiceSessionSchema,
|
|
8072
|
+
TC832_RESERVED_SCOPES,
|
|
7766
8073
|
TelemetryEvents,
|
|
7767
8074
|
TelemetrySpanEventSchema,
|
|
7768
8075
|
TinyCloudDebugLogger,
|
|
@@ -7770,6 +8077,7 @@ export {
|
|
|
7770
8077
|
VaultHeaders,
|
|
7771
8078
|
VaultPublicSpaceKVActions,
|
|
7772
8079
|
abortedError,
|
|
8080
|
+
appSecretScope,
|
|
7773
8081
|
authExpiredError,
|
|
7774
8082
|
authRequiredError,
|
|
7775
8083
|
authUnauthorizedError,
|
|
@@ -7782,6 +8090,7 @@ export {
|
|
|
7782
8090
|
buildDecryptInvocation,
|
|
7783
8091
|
buildNetworkId,
|
|
7784
8092
|
canonicalHashHex,
|
|
8093
|
+
canonicalReplicationIdentity,
|
|
7785
8094
|
canonicalSignedResponse,
|
|
7786
8095
|
canonicalize as canonicalizeEncryptionJson,
|
|
7787
8096
|
canonicalizeSecretScope,
|
|
@@ -7808,6 +8117,7 @@ export {
|
|
|
7808
8117
|
installTinyCloudDebugGlobals,
|
|
7809
8118
|
isNetworkId,
|
|
7810
8119
|
isStorageFullError,
|
|
8120
|
+
kvPrefixCovers,
|
|
7811
8121
|
networkDiscoveryKey,
|
|
7812
8122
|
networkError,
|
|
7813
8123
|
notFoundError,
|
|
@@ -7821,6 +8131,8 @@ export {
|
|
|
7821
8131
|
parseSecretCatalogKey,
|
|
7822
8132
|
parseStorageQuotaBytes,
|
|
7823
8133
|
permissionDeniedError,
|
|
8134
|
+
replicationIdentityKey,
|
|
8135
|
+
requiresSecretsOptIn,
|
|
7824
8136
|
resolveSecretListPrefix,
|
|
7825
8137
|
resolveSecretPath,
|
|
7826
8138
|
serviceError,
|