@tinycloud/sdk-services 3.2.0-beta.2 → 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.
Files changed (39) hide show
  1. package/dist/{BaseService-DWbPbov7.d.cts → BaseService-BDQXjWMx.d.cts} +1 -1
  2. package/dist/{BaseService-B_UKhVht.d.ts → BaseService-Di_hVfUV.d.ts} +1 -1
  3. package/dist/encryption/index.cjs +7 -1
  4. package/dist/encryption/index.cjs.map +1 -1
  5. package/dist/encryption/index.d.cts +2 -2
  6. package/dist/encryption/index.d.ts +2 -2
  7. package/dist/encryption/index.js +7 -1
  8. package/dist/encryption/index.js.map +1 -1
  9. package/dist/index.cjs +595 -353
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.d.cts +36 -26
  12. package/dist/index.d.ts +36 -26
  13. package/dist/index.js +590 -353
  14. package/dist/index.js.map +1 -1
  15. package/dist/internal/decrypt-transport-response-error.d.cts +1 -1
  16. package/dist/internal/decrypt-transport-response-error.d.ts +1 -1
  17. package/dist/kv/index.cjs +530 -334
  18. package/dist/kv/index.cjs.map +1 -1
  19. package/dist/kv/index.d.cts +17 -468
  20. package/dist/kv/index.d.ts +17 -468
  21. package/dist/kv/index.js +524 -333
  22. package/dist/kv/index.js.map +1 -1
  23. package/dist/kv/replication/index.cjs +1375 -0
  24. package/dist/kv/replication/index.cjs.map +1 -0
  25. package/dist/kv/replication/index.d.cts +9 -0
  26. package/dist/kv/replication/index.d.ts +9 -0
  27. package/dist/kv/replication/index.js +1342 -0
  28. package/dist/kv/replication/index.js.map +1 -0
  29. package/dist/scope-BmLxwyl-.d.ts +957 -0
  30. package/dist/scope-D855kRDx.d.cts +957 -0
  31. package/dist/sql/index.cjs +12 -6
  32. package/dist/sql/index.cjs.map +1 -1
  33. package/dist/sql/index.d.cts +2 -2
  34. package/dist/sql/index.d.ts +2 -2
  35. package/dist/sql/index.js +12 -6
  36. package/dist/sql/index.js.map +1 -1
  37. package/dist/{types-CVwwxqXF.d.cts → types-7A5rHWRY.d.cts} +10 -1
  38. package/dist/{types-CVwwxqXF.d.ts → types-7A5rHWRY.d.ts} +10 -1
  39. 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 response = await this.invokeOperation(
2273
- path,
2274
- KVAction.GET,
2275
- void 0,
2276
- request.signal,
2277
- options?.maxResponseBytes === void 0 ? void 0 : { "x-tinycloud-max-response-bytes": String(options.maxResponseBytes) }
2278
- );
2279
- if (!response.ok) {
2280
- if (response.status === 401 || response.status === 403) {
2281
- const errorText2 = await this.readAuthorizationText(response);
2282
- const permissionHint = parsePermissionHintFromErrorText(errorText2);
2283
- const structuredCapability = permissionHint?.service === "tinycloud.kv" && permissionHint.space === this.context.session.spaceId && permissionHint.path === path ? validatedCapabilityOf({
2284
- service: "kv",
2285
- code: ErrorCodes.AUTH_UNAUTHORIZED,
2286
- meta: {
2287
- status: response.status,
2288
- resource: `${this.context.session.spaceId}/kv/${path}`,
2289
- requiredAction: KVAction.GET
2290
- }
2291
- }) : void 0;
2292
- return this.authorizationFailure(
2293
- `Failed to get key ${JSON.stringify(key)}`,
2294
- response,
2295
- errorText2,
2296
- [path],
2297
- KVAction.GET,
2298
- structuredCapability ? { permissionHint, ...structuredCapability } : {}
2299
- );
2300
- }
2301
- if (response.status === 404) {
2302
- return await 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
- const data = await this.parseResponse(
2323
- response,
2324
- options?.raw,
2325
- options?.binary
2326
- );
2327
- return ok({
2328
- data,
2329
- headers: this.createResponseHeaders(response.headers)
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 response = await this.invokeOperation(
2365
- path,
2366
- KVAction.PUT,
2367
- body,
2368
- request.signal,
2369
- {
2370
- ...options?.ifMatch === void 0 ? {} : { "if-match": options.ifMatch },
2371
- ...options?.ifNoneMatch === void 0 ? {} : { "if-none-match": options.ifNoneMatch }
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 body = new FormData();
2468
- for (let index = 0; index < items.length; index++) {
2469
- body.append(
2470
- encodeKvBatchPartName(paths[index]),
2471
- this.serializeBatchPutValue(items[index])
2472
- );
2473
- }
2474
- const headers = this.context.invokeAny(
2475
- session,
2476
- paths.map((path) => ({
2477
- spaceId: session.spaceId,
2478
- service: "kv",
2479
- path,
2480
- action: KVAction.PUT
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
- const url = `${this.host}/invoke`;
2484
- const init = {
2485
- method: "POST",
2486
- headers,
2487
- body,
2488
- signal: request.signal
2489
- };
2490
- const fetchFn = this.context.fetch;
2491
- requestMayHaveDispatched = true;
2492
- const response = await fetchFn(url, init);
2493
- if (!response.ok) {
2494
- let errorText;
2495
- try {
2496
- errorText = await response.text();
2497
- } catch (textError) {
2498
- const cause = textError instanceof Error ? textError : new Error(String(textError));
2499
- return err(
2500
- serviceError(
2501
- ErrorCodes.KV_WRITE_FAILED,
2502
- `Failed to batch put ${items.length} key(s): ${response.status} - <response body could not be read: ${cause.message}>`,
2503
- "kv",
2504
- { cause, meta: { status: response.status, statusText: response.statusText } }
2505
- )
2506
- );
2507
- }
2508
- if (response.status === 401 || response.status === 403) {
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} - ${errorText}`,
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
- let rawBody;
2535
- try {
2536
- rawBody = await response.json();
2537
- } catch (jsonError) {
2538
- const cause = jsonError instanceof Error ? jsonError : new Error(String(jsonError));
2539
- const timedOut = isRequestTimeoutError(cause);
2540
- return err(
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 batchResponse = this.normalizeBatchPutResponse(rawBody);
2559
- if (!batchResponse) {
2560
- return err(
2561
- serviceError(
2562
- ErrorCodes.NETWORK_ERROR,
2563
- "KV batchPut response did not include matching written keys and count",
2564
- "kv",
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
- const requestedPaths = new Set(paths);
2577
- const writtenPaths = new Set(batchResponse.written);
2578
- const matchesRequest = batchResponse.count === paths.length && batchResponse.written.length === paths.length && writtenPaths.size === paths.length && batchResponse.written.every((key) => requestedPaths.has(key));
2579
- if (!matchesRequest) {
2580
- return err(
2581
- serviceError(
2582
- ErrorCodes.NETWORK_ERROR,
2583
- `KV batchPut response did not confirm all ${paths.length} requested key(s) were written`,
2584
- "kv",
2585
- {
2586
- meta: {
2587
- requestMayHaveDispatched: true,
2588
- responseReceived: true,
2589
- status: response.status,
2590
- outcome: "batch-unconfirmed"
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 response = await this.invokeOperation(
2630
- listPath,
2631
- KVAction.LIST,
2632
- void 0,
2633
- request.signal,
2634
- options?.limit === void 0 && options?.cursor === void 0 ? void 0 : {
2635
- ...options?.limit === void 0 ? {} : { "x-tinycloud-limit": String(options.limit) },
2636
- ...options?.cursor === void 0 ? {} : { "x-tinycloud-cursor": options.cursor }
2637
- }
2638
- );
2639
- if (!response.ok) {
2640
- if (response.status === 401 || response.status === 403) {
2641
- return this.authorizationFailure(
2642
- "Failed to list keys",
2643
- response,
2644
- await this.readAuthorizationText(response),
2645
- [listPath],
2646
- KVAction.LIST
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
- const errorText = await response.text();
2650
- return err(
2651
- serviceError(
2652
- ErrorCodes.NETWORK_ERROR,
2653
- `Failed to list keys: ${response.status} - ${errorText}`,
2654
- "kv",
2655
- { meta: { status: response.status, statusText: response.statusText } }
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 response = await this.invokeOperation(
2854
- path,
2855
- KVAction.DELETE,
2856
- void 0,
2857
- request.signal,
2858
- options?.ifMatch === void 0 ? void 0 : { "if-match": options.ifMatch }
2859
- );
2860
- if (!response.ok) {
2861
- if (response.status === 401 || response.status === 403) {
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 await 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
  */
@@ -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) {
@@ -5655,7 +5878,8 @@ var DataVaultService = class extends BaseService {
5655
5878
  }
5656
5879
  try {
5657
5880
  const valueResult = await this.tc.kv.get(`vault/${key}`, {
5658
- raw: true
5881
+ raw: true,
5882
+ ...options?.source === void 0 ? {} : { source: options.source }
5659
5883
  });
5660
5884
  if (!valueResult.ok) {
5661
5885
  return vaultError({ code: "KEY_NOT_FOUND", key });
@@ -5700,7 +5924,7 @@ var DataVaultService = class extends BaseService {
5700
5924
  }
5701
5925
  let valueResult;
5702
5926
  try {
5703
- valueResult = await this.tc.kv.get(`vault/${key}`, { raw: true });
5927
+ valueResult = await this.tc.kv.get(`vault/${key}`, { raw: true, source: "network" });
5704
5928
  } catch {
5705
5929
  return { status: "node_unreachable" };
5706
5930
  }
@@ -5769,7 +5993,8 @@ var DataVaultService = class extends BaseService {
5769
5993
  }
5770
5994
  try {
5771
5995
  const valueResult = await this.tc.kv.get(`vault/${key}`, {
5772
- raw: true
5996
+ raw: true,
5997
+ source: "network"
5773
5998
  });
5774
5999
  if (!valueResult.ok) {
5775
6000
  return vaultError({ code: "KEY_NOT_FOUND", key });
@@ -5903,7 +6128,8 @@ var DataVaultService = class extends BaseService {
5903
6128
  }
5904
6129
  try {
5905
6130
  const keyResult = await this.tc.kv.get(`keys/${key}`, {
5906
- raw: true
6131
+ raw: true,
6132
+ ...options?.source === void 0 ? {} : { source: options.source }
5907
6133
  });
5908
6134
  if (!keyResult.ok) {
5909
6135
  return vaultError({ code: "KEY_NOT_FOUND", key });
@@ -5912,7 +6138,8 @@ var DataVaultService = class extends BaseService {
5912
6138
  const keyBlobBytes = base64Decode(keyEnvelope.key);
5913
6139
  const entryKey = this.crypto.decrypt(this.masterKey, keyBlobBytes);
5914
6140
  const valueResult = await this.tc.kv.get(`vault/${key}`, {
5915
- raw: true
6141
+ raw: true,
6142
+ ...options?.source === void 0 ? {} : { source: options.source }
5916
6143
  });
5917
6144
  if (!valueResult.ok) {
5918
6145
  return vaultError({ code: "KEY_NOT_FOUND", key });
@@ -6022,6 +6249,7 @@ var DataVaultService = class extends BaseService {
6022
6249
  prefix: listPrefix,
6023
6250
  removePrefix: true,
6024
6251
  ...options?.limit === void 0 ? {} : { limit: options.limit },
6252
+ ...options?.source === void 0 ? {} : { source: options.source },
6025
6253
  ...options?.cursor === void 0 ? {} : { cursor: options.cursor }
6026
6254
  });
6027
6255
  if (!listResult.ok) {
@@ -6746,7 +6974,9 @@ var SecretsService = class {
6746
6974
  async get(name, options) {
6747
6975
  const secretPath = resolveSecretPathResult(name, options);
6748
6976
  if ("ok" in secretPath) return secretPath;
6749
- 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
+ });
6750
6980
  if (!result.ok) {
6751
6981
  return result;
6752
6982
  }
@@ -6780,7 +7010,8 @@ var SecretsService = class {
6780
7010
  }
6781
7011
  const result = await this.vault.list({
6782
7012
  prefix,
6783
- removePrefix: true
7013
+ removePrefix: true,
7014
+ ...options?.source === void 0 ? {} : { source: options.source }
6784
7015
  });
6785
7016
  if (!result.ok) {
6786
7017
  return result;
@@ -6799,6 +7030,7 @@ var SecretsService = class {
6799
7030
  prefix: "secrets/",
6800
7031
  removePrefix: true,
6801
7032
  limit: 1e3,
7033
+ source: "network",
6802
7034
  ...cursor === void 0 ? {} : { cursor }
6803
7035
  });
6804
7036
  if (!result.ok) {
@@ -7790,6 +8022,7 @@ EncryptionService.serviceName = "encryption";
7790
8022
  import { DecryptTransportResponseError as DecryptTransportResponseError3 } from "@tinycloud/sdk-services/internal/decrypt-transport-response-error";
7791
8023
  export {
7792
8024
  BaseService,
8025
+ CLEAR_PENDING_WARNING,
7793
8026
  DECRYPT_ACTION,
7794
8027
  DECRYPT_FACT_TYPE,
7795
8028
  DECRYPT_RESULT_TYPE,
@@ -7857,6 +8090,7 @@ export {
7857
8090
  buildDecryptInvocation,
7858
8091
  buildNetworkId,
7859
8092
  canonicalHashHex,
8093
+ canonicalReplicationIdentity,
7860
8094
  canonicalSignedResponse,
7861
8095
  canonicalize as canonicalizeEncryptionJson,
7862
8096
  canonicalizeSecretScope,
@@ -7883,6 +8117,7 @@ export {
7883
8117
  installTinyCloudDebugGlobals,
7884
8118
  isNetworkId,
7885
8119
  isStorageFullError,
8120
+ kvPrefixCovers,
7886
8121
  networkDiscoveryKey,
7887
8122
  networkError,
7888
8123
  notFoundError,
@@ -7896,6 +8131,8 @@ export {
7896
8131
  parseSecretCatalogKey,
7897
8132
  parseStorageQuotaBytes,
7898
8133
  permissionDeniedError,
8134
+ replicationIdentityKey,
8135
+ requiresSecretsOptIn,
7899
8136
  resolveSecretListPrefix,
7900
8137
  resolveSecretPath,
7901
8138
  serviceError,