@tinycloud/sdk-services 3.2.0-beta.2 → 3.2.0-beta.5

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 (45) 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 +605 -357
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.d.cts +37 -26
  12. package/dist/index.d.ts +37 -26
  13. package/dist/index.js +600 -357
  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 +540 -338
  18. package/dist/kv/index.cjs.map +1 -1
  19. package/dist/kv/index.d.cts +18 -468
  20. package/dist/kv/index.d.ts +18 -468
  21. package/dist/kv/index.js +534 -337
  22. package/dist/kv/index.js.map +1 -1
  23. package/dist/kv/replication/index.cjs +1398 -0
  24. package/dist/kv/replication/index.cjs.map +1 -0
  25. package/dist/kv/replication/index.d.cts +10 -0
  26. package/dist/kv/replication/index.d.ts +10 -0
  27. package/dist/kv/replication/index.js +1365 -0
  28. package/dist/kv/replication/index.js.map +1 -0
  29. package/dist/kv/replication/prefix.cjs +43 -0
  30. package/dist/kv/replication/prefix.cjs.map +1 -0
  31. package/dist/kv/replication/prefix.d.cts +6 -0
  32. package/dist/kv/replication/prefix.d.ts +6 -0
  33. package/dist/kv/replication/prefix.js +17 -0
  34. package/dist/kv/replication/prefix.js.map +1 -0
  35. package/dist/scope-BKge7zDT.d.ts +957 -0
  36. package/dist/scope-C05VVAXH.d.cts +957 -0
  37. package/dist/sql/index.cjs +12 -6
  38. package/dist/sql/index.cjs.map +1 -1
  39. package/dist/sql/index.d.cts +2 -2
  40. package/dist/sql/index.d.ts +2 -2
  41. package/dist/sql/index.js +12 -6
  42. package/dist/sql/index.js.map +1 -1
  43. package/dist/{types-CVwwxqXF.d.cts → types-7A5rHWRY.d.cts} +10 -1
  44. package/dist/{types-CVwwxqXF.d.ts → types-7A5rHWRY.d.ts} +10 -1
  45. package/package.json +31 -1
package/dist/index.cjs CHANGED
@@ -21,6 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var src_exports = {};
22
22
  __export(src_exports, {
23
23
  BaseService: () => BaseService,
24
+ CLEAR_PENDING_WARNING: () => CLEAR_PENDING_WARNING,
24
25
  DECRYPT_ACTION: () => DECRYPT_ACTION,
25
26
  DECRYPT_FACT_TYPE: () => DECRYPT_FACT_TYPE,
26
27
  DECRYPT_RESULT_TYPE: () => DECRYPT_RESULT_TYPE,
@@ -88,6 +89,7 @@ __export(src_exports, {
88
89
  buildDecryptInvocation: () => buildDecryptInvocation,
89
90
  buildNetworkId: () => buildNetworkId,
90
91
  canonicalHashHex: () => canonicalHashHex,
92
+ canonicalReplicationIdentity: () => canonicalReplicationIdentity,
91
93
  canonicalSignedResponse: () => canonicalSignedResponse,
92
94
  canonicalizeEncryptionJson: () => canonicalize,
93
95
  canonicalizeSecretScope: () => canonicalizeSecretScope,
@@ -114,6 +116,7 @@ __export(src_exports, {
114
116
  installTinyCloudDebugGlobals: () => installTinyCloudDebugGlobals,
115
117
  isNetworkId: () => isNetworkId,
116
118
  isStorageFullError: () => isStorageFullError,
119
+ kvPrefixCovers: () => kvPrefixCovers,
117
120
  networkDiscoveryKey: () => networkDiscoveryKey,
118
121
  networkError: () => networkError,
119
122
  notFoundError: () => notFoundError,
@@ -127,6 +130,8 @@ __export(src_exports, {
127
130
  parseSecretCatalogKey: () => parseSecretCatalogKey,
128
131
  parseStorageQuotaBytes: () => parseStorageQuotaBytes,
129
132
  permissionDeniedError: () => permissionDeniedError,
133
+ replicationIdentityKey: () => replicationIdentityKey,
134
+ requiresSecretsOptIn: () => requiresSecretsOptIn,
130
135
  resolveSecretListPrefix: () => resolveSecretListPrefix,
131
136
  resolveSecretPath: () => resolveSecretPath,
132
137
  serviceError: () => serviceError,
@@ -214,7 +219,12 @@ var TelemetryEvents = {
214
219
  SERVICE_ERROR: "service.error",
215
220
  SERVICE_RETRY: "service.retry",
216
221
  SESSION_CHANGED: "session.changed",
217
- SESSION_EXPIRED: "session.expired"
222
+ SESSION_EXPIRED: "session.expired",
223
+ REPLICATION_READ: "replication.read",
224
+ REPLICATION_WRITE: "replication.write",
225
+ REPLICATION_SYNC: "replication.sync",
226
+ REPLICATION_STATE: "replication.state",
227
+ REPLICATION_DIVERGENCE: "replication.divergence"
218
228
  };
219
229
  function ok(data) {
220
230
  return { ok: true, data };
@@ -240,9 +250,34 @@ var SAFE_NUMBER_FIELDS = /* @__PURE__ */ new Set([
240
250
  "endedAt",
241
251
  "startedAt",
242
252
  "status",
243
- "timestamp"
253
+ "timestamp",
254
+ "latencyMs",
255
+ "stalenessMs",
256
+ "count",
257
+ "changes",
258
+ "deleted",
259
+ "fetched",
260
+ "pages",
261
+ "lagMs",
262
+ "pendingCleared"
244
263
  ]);
245
264
  var SAFE_BOOLEAN_FIELDS = /* @__PURE__ */ new Set(["authenticated", "ok", "persisted"]);
265
+ var SAFE_REPLICATION_FIELDS = {
266
+ source: ["replica", "network", "none"],
267
+ 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"],
268
+ op: ["get", "list", "put", "delete", "batchPut"],
269
+ trigger: ["start", "interval", "stale_read", "manual"],
270
+ outcome: ["found", "not_found", "error", "committed", "failed", "ambiguous", "ok", "busy", "aborted"],
271
+ class: ["offline", "authority", "storage", "node"],
272
+ state: ["opened", "grant_installed", "grant_missing", "runtime_unsupported", "unavailable", "revoked", "recreated", "purged", "purge_failed", "pending_store_error", "pinned", "pending_cleared", "closed", "idle", "ready"],
273
+ kind: ["value", "missing_local", "extra_local", "keys", "order"],
274
+ coverage: ["empty", "bootstrapping", "complete"],
275
+ authority: ["valid", "expired", "revoked", "not-yet-valid"],
276
+ 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"],
277
+ pendingState: ["in_flight", "committed", "ambiguous"],
278
+ strategy: ["session", "minted", "installed"],
279
+ verify: ["match", "diverged", "error", "aborted"]
280
+ };
246
281
  function finiteNumber(value) {
247
282
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
248
283
  }
@@ -279,6 +314,12 @@ function projectDiagnosticData(value) {
279
314
  const boolean = read(value, key);
280
315
  if (typeof boolean === "boolean") projected[key] = boolean;
281
316
  }
317
+ for (const [key, allowed] of Object.entries(SAFE_REPLICATION_FIELDS)) {
318
+ const candidate = read(value, key);
319
+ if (typeof candidate === "string" && allowed.includes(candidate)) {
320
+ projected[key] = candidate;
321
+ }
322
+ }
282
323
  const url = read(value, "url");
283
324
  if (url !== void 0) projected.url = REDACTED;
284
325
  const error = read(value, "error");
@@ -903,6 +944,12 @@ var ServiceContext = class {
903
944
  get abortSignal() {
904
945
  return this._abortController.signal;
905
946
  }
947
+ get operationAbortSignal() {
948
+ return this._operationAbortSignal;
949
+ }
950
+ setOperationAbortSignal(signal) {
951
+ this._operationAbortSignal = signal;
952
+ }
906
953
  /**
907
954
  * Permanently retire this graph after its owner installs a replacement.
908
955
  * Unlike `abort()`, retirement never creates a fresh controller, so captured
@@ -1654,6 +1701,7 @@ var BaseService = class {
1654
1701
  const parents = [
1655
1702
  this.abortController.signal,
1656
1703
  this.context?.abortSignal,
1704
+ this.context?.operationAbortSignal,
1657
1705
  signal
1658
1706
  ].filter((parent) => parent !== void 0);
1659
1707
  for (const parent of parents) {
@@ -1899,14 +1947,28 @@ var KVService = class extends BaseService {
1899
1947
  */
1900
1948
  constructor(config = {}) {
1901
1949
  super();
1950
+ this.readThrough = null;
1951
+ this.readThroughReady = null;
1902
1952
  this._config = config;
1903
1953
  }
1954
+ /** Delay KV routing until an asynchronously loaded read-through is attached. */
1955
+ setReadThroughReady(ready) {
1956
+ const settled = ready.then(() => void 0, () => void 0);
1957
+ this.readThroughReady = settled;
1958
+ void settled.then(() => {
1959
+ if (this.readThroughReady === settled) this.readThroughReady = null;
1960
+ });
1961
+ }
1904
1962
  /**
1905
1963
  * Get the service configuration.
1906
1964
  */
1907
1965
  get config() {
1908
1966
  return this._config;
1909
1967
  }
1968
+ /** Install or remove the optional replication read-through policy. */
1969
+ setReadThrough(readThrough) {
1970
+ this.readThrough = readThrough;
1971
+ }
1910
1972
  handleQuotaErrorResponse(response, errorText, key) {
1911
1973
  if (response.status !== 402 && response.status !== 413) {
1912
1974
  return void 0;
@@ -2160,6 +2222,7 @@ var KVService = class extends BaseService {
2160
2222
  return aligned;
2161
2223
  }
2162
2224
  async batchRead(keys, action, options) {
2225
+ if (this.readThroughReady !== null) await this.readThroughReady;
2163
2226
  if (!this.requireAuth()) return err(authRequiredError("kv"));
2164
2227
  if (keys.length === 0) return ok({ results: [], count: 0 });
2165
2228
  if (keys.length > MAX_KV_BATCH_READ_ITEMS) {
@@ -2410,6 +2473,7 @@ var KVService = class extends BaseService {
2410
2473
  */
2411
2474
  async get(key, options) {
2412
2475
  return this.withTelemetry("get", key, async () => {
2476
+ if (this.readThroughReady !== null) await this.readThroughReady;
2413
2477
  if (!this.requireAuth()) {
2414
2478
  return err(authRequiredError("kv"));
2415
2479
  }
@@ -2423,64 +2487,38 @@ var KVService = class extends BaseService {
2423
2487
  const path = this.getFullPath(key, options?.prefix);
2424
2488
  const request = this.createRequestSignal(options?.signal, options?.timeout);
2425
2489
  try {
2426
- const response = await this.invokeOperation(
2427
- path,
2428
- KVAction.GET,
2429
- void 0,
2430
- request.signal,
2431
- options?.maxResponseBytes === void 0 ? void 0 : { "x-tinycloud-max-response-bytes": String(options.maxResponseBytes) }
2432
- );
2433
- if (!response.ok) {
2434
- if (response.status === 401 || response.status === 403) {
2435
- const errorText2 = await this.readAuthorizationText(response);
2436
- const permissionHint = parsePermissionHintFromErrorText(errorText2);
2437
- const structuredCapability = permissionHint?.service === "tinycloud.kv" && permissionHint.space === this.context.session.spaceId && permissionHint.path === path ? validatedCapabilityOf({
2438
- service: "kv",
2439
- code: ErrorCodes.AUTH_UNAUTHORIZED,
2440
- meta: {
2441
- status: response.status,
2442
- resource: `${this.context.session.spaceId}/kv/${path}`,
2443
- requiredAction: KVAction.GET
2444
- }
2445
- }) : void 0;
2446
- return this.authorizationFailure(
2447
- `Failed to get key ${JSON.stringify(key)}`,
2448
- response,
2449
- errorText2,
2450
- [path],
2451
- KVAction.GET,
2452
- structuredCapability ? { permissionHint, ...structuredCapability } : {}
2453
- );
2454
- }
2455
- if (response.status === 404) {
2456
- return await this.classifyNotFound(response, key);
2457
- }
2458
- const errorText = await response.text();
2459
- if (response.status === 413) {
2460
- return err(serviceError(
2461
- ErrorCodes.KV_RESPONSE_TOO_LARGE,
2462
- `KV value at key ${JSON.stringify(key)} exceeds the requested response limit`,
2463
- "kv",
2464
- { meta: { status: response.status, statusText: response.statusText } }
2465
- ));
2490
+ const network = () => this.getFromNetwork(key, path, options, request.signal);
2491
+ if (options?.source === "network") {
2492
+ const observer = this.readThrough;
2493
+ if (observer === null) return await network();
2494
+ let outcome = "error";
2495
+ const startedAt = Date.now();
2496
+ try {
2497
+ const result = await network();
2498
+ outcome = result.ok ? "found" : result.error.code === ErrorCodes.KV_NOT_FOUND ? "not_found" : "error";
2499
+ return result;
2500
+ } finally {
2501
+ try {
2502
+ observer.observeNetworkRequested({
2503
+ op: "get",
2504
+ space: this.context.session.spaceId,
2505
+ path,
2506
+ reason: "NETWORK_REQUESTED",
2507
+ outcome,
2508
+ latencyMs: Date.now() - startedAt
2509
+ });
2510
+ } catch {
2511
+ }
2466
2512
  }
2467
- return err(
2468
- serviceError(
2469
- ErrorCodes.NETWORK_ERROR,
2470
- `Failed to get key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
2471
- "kv",
2472
- { meta: { status: response.status, statusText: response.statusText } }
2473
- )
2474
- );
2475
2513
  }
2476
- const data = await this.parseResponse(
2477
- response,
2478
- options?.raw,
2479
- options?.binary
2480
- );
2481
- return ok({
2482
- data,
2483
- headers: this.createResponseHeaders(response.headers)
2514
+ if (this.readThrough === null) return await network();
2515
+ return await this.readThrough.get({
2516
+ space: this.context.session.spaceId,
2517
+ key,
2518
+ path,
2519
+ options,
2520
+ signal: request.signal,
2521
+ network
2484
2522
  });
2485
2523
  } catch (error) {
2486
2524
  return err(wrapError("kv", error));
@@ -2489,6 +2527,56 @@ var KVService = class extends BaseService {
2489
2527
  }
2490
2528
  });
2491
2529
  }
2530
+ async getFromNetwork(key, path, options, signal) {
2531
+ const response = await this.invokeOperation(
2532
+ path,
2533
+ KVAction.GET,
2534
+ void 0,
2535
+ signal,
2536
+ options?.maxResponseBytes === void 0 ? void 0 : { "x-tinycloud-max-response-bytes": String(options.maxResponseBytes) }
2537
+ );
2538
+ if (!response.ok) {
2539
+ if (response.status === 401 || response.status === 403) {
2540
+ const errorText2 = await this.readAuthorizationText(response);
2541
+ const permissionHint = parsePermissionHintFromErrorText(errorText2);
2542
+ const structuredCapability = permissionHint?.service === "tinycloud.kv" && permissionHint.space === this.context.session.spaceId && permissionHint.path === path ? validatedCapabilityOf({
2543
+ service: "kv",
2544
+ code: ErrorCodes.AUTH_UNAUTHORIZED,
2545
+ meta: {
2546
+ status: response.status,
2547
+ resource: `${this.context.session.spaceId}/kv/${path}`,
2548
+ requiredAction: KVAction.GET
2549
+ }
2550
+ }) : void 0;
2551
+ return this.authorizationFailure(
2552
+ `Failed to get key ${JSON.stringify(key)}`,
2553
+ response,
2554
+ errorText2,
2555
+ [path],
2556
+ KVAction.GET,
2557
+ structuredCapability ? { permissionHint, ...structuredCapability } : {}
2558
+ );
2559
+ }
2560
+ if (response.status === 404) return this.classifyNotFound(response, key);
2561
+ const errorText = await response.text();
2562
+ if (response.status === 413) {
2563
+ return err(serviceError(
2564
+ ErrorCodes.KV_RESPONSE_TOO_LARGE,
2565
+ `KV value at key ${JSON.stringify(key)} exceeds the requested response limit`,
2566
+ "kv",
2567
+ { meta: { status: response.status, statusText: response.statusText } }
2568
+ ));
2569
+ }
2570
+ return err(serviceError(
2571
+ ErrorCodes.NETWORK_ERROR,
2572
+ `Failed to get key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
2573
+ "kv",
2574
+ { meta: { status: response.status, statusText: response.statusText } }
2575
+ ));
2576
+ }
2577
+ const data = await this.parseResponse(response, options?.raw, options?.binary);
2578
+ return ok({ data, headers: this.createResponseHeaders(response.headers) });
2579
+ }
2492
2580
  async batchGet(keys, options) {
2493
2581
  return this.withTelemetry(
2494
2582
  "batchGet",
@@ -2501,6 +2589,7 @@ var KVService = class extends BaseService {
2501
2589
  */
2502
2590
  async put(key, value, options) {
2503
2591
  return this.withTelemetry("put", key, async () => {
2592
+ if (this.readThroughReady !== null) await this.readThroughReady;
2504
2593
  if (!this.requireAuth()) {
2505
2594
  return err(authRequiredError("kv"));
2506
2595
  }
@@ -2515,63 +2604,15 @@ var KVService = class extends BaseService {
2515
2604
  const body = this.serializePutValue(value, options?.contentType);
2516
2605
  const request = this.createRequestSignal(options?.signal, options?.timeout);
2517
2606
  try {
2518
- const response = await this.invokeOperation(
2519
- path,
2520
- KVAction.PUT,
2521
- body,
2522
- request.signal,
2523
- {
2524
- ...options?.ifMatch === void 0 ? {} : { "if-match": options.ifMatch },
2525
- ...options?.ifNoneMatch === void 0 ? {} : { "if-none-match": options.ifNoneMatch }
2526
- }
2527
- );
2528
- if (response.status === 401 || response.status === 403) {
2529
- return this.authorizationFailure(
2530
- `Failed to put key ${JSON.stringify(key)}`,
2531
- response,
2532
- await this.readAuthorizationText(response),
2533
- [path],
2534
- KVAction.PUT
2535
- );
2536
- }
2537
- if (!response.ok) {
2538
- const errorText = await response.text();
2539
- if (response.status === 412) {
2540
- return err(serviceError(
2541
- ErrorCodes.KV_PRECONDITION_FAILED,
2542
- `KV precondition failed for key ${JSON.stringify(key)}`,
2543
- "kv",
2544
- { meta: { status: response.status, statusText: response.statusText } }
2545
- ));
2546
- }
2547
- if (response.status === 503 && (options?.ifMatch !== void 0 || options?.ifNoneMatch !== void 0)) {
2548
- return err(serviceError(
2549
- ErrorCodes.KV_CONFLICT,
2550
- `Concurrent KV update conflicted for key ${JSON.stringify(key)}`,
2551
- "kv",
2552
- { meta: { status: response.status, statusText: response.statusText } }
2553
- ));
2554
- }
2555
- const quotaError = this.handleQuotaErrorResponse(
2556
- response,
2557
- errorText,
2558
- key
2559
- );
2560
- if (quotaError) {
2561
- return quotaError;
2562
- }
2563
- return err(
2564
- serviceError(
2565
- ErrorCodes.KV_WRITE_FAILED,
2566
- `Failed to put key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
2567
- "kv",
2568
- { meta: { status: response.status, statusText: response.statusText } }
2569
- )
2570
- );
2571
- }
2572
- return ok({
2573
- data: void 0,
2574
- headers: this.createResponseHeaders(response.headers)
2607
+ const network = () => this.putToNetwork(key, path, body, options, request.signal);
2608
+ if (this.readThrough === null) return await network();
2609
+ return await this.readThrough.write({
2610
+ op: "put",
2611
+ space: this.context.session.spaceId,
2612
+ entries: [{ path, body }],
2613
+ signal: request.signal,
2614
+ conditional: options?.ifMatch !== void 0 || options?.ifNoneMatch !== void 0,
2615
+ network
2575
2616
  });
2576
2617
  } catch (error) {
2577
2618
  return err(wrapError("kv", error));
@@ -2580,11 +2621,64 @@ var KVService = class extends BaseService {
2580
2621
  }
2581
2622
  });
2582
2623
  }
2624
+ async putToNetwork(key, path, body, options, signal) {
2625
+ const response = await this.invokeOperation(
2626
+ path,
2627
+ KVAction.PUT,
2628
+ body,
2629
+ signal,
2630
+ {
2631
+ ...options?.ifMatch === void 0 ? {} : { "if-match": options.ifMatch },
2632
+ ...options?.ifNoneMatch === void 0 ? {} : { "if-none-match": options.ifNoneMatch }
2633
+ }
2634
+ );
2635
+ if (response.status === 401 || response.status === 403) {
2636
+ return this.authorizationFailure(
2637
+ `Failed to put key ${JSON.stringify(key)}`,
2638
+ response,
2639
+ await this.readAuthorizationText(response),
2640
+ [path],
2641
+ KVAction.PUT
2642
+ );
2643
+ }
2644
+ if (!response.ok) {
2645
+ const errorText = await response.text();
2646
+ if (response.status === 412) {
2647
+ return err(serviceError(
2648
+ ErrorCodes.KV_PRECONDITION_FAILED,
2649
+ `KV precondition failed for key ${JSON.stringify(key)}`,
2650
+ "kv",
2651
+ { meta: { status: response.status, statusText: response.statusText } }
2652
+ ));
2653
+ }
2654
+ if (response.status === 503 && (options?.ifMatch !== void 0 || options?.ifNoneMatch !== void 0)) {
2655
+ return err(serviceError(
2656
+ ErrorCodes.KV_CONFLICT,
2657
+ `Concurrent KV update conflicted for key ${JSON.stringify(key)}`,
2658
+ "kv",
2659
+ { meta: { status: response.status, statusText: response.statusText } }
2660
+ ));
2661
+ }
2662
+ const quotaError = this.handleQuotaErrorResponse(response, errorText, key);
2663
+ if (quotaError) return quotaError;
2664
+ return err(serviceError(
2665
+ ErrorCodes.KV_WRITE_FAILED,
2666
+ `Failed to put key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
2667
+ "kv",
2668
+ { meta: { status: response.status, statusText: response.statusText } }
2669
+ ));
2670
+ }
2671
+ return ok({
2672
+ data: void 0,
2673
+ headers: this.createResponseHeaders(response.headers)
2674
+ });
2675
+ }
2583
2676
  /**
2584
2677
  * Store multiple values in one TinyCloud KV invocation.
2585
2678
  */
2586
2679
  async batchPut(items, options) {
2587
2680
  return this.withTelemetry("batchPut", String(items.length), async () => {
2681
+ if (this.readThroughReady !== null) await this.readThroughReady;
2588
2682
  if (!this.requireAuth()) {
2589
2683
  return err(authRequiredError("kv"));
2590
2684
  }
@@ -2615,155 +2709,177 @@ var KVService = class extends BaseService {
2615
2709
  }
2616
2710
  seen.add(path);
2617
2711
  }
2618
- let requestMayHaveDispatched = false;
2619
2712
  const request = this.createRequestSignal(options?.signal, options?.timeout);
2620
2713
  try {
2621
- const body = new FormData();
2622
- for (let index = 0; index < items.length; index++) {
2623
- body.append(
2624
- encodeKvBatchPartName(paths[index]),
2625
- this.serializeBatchPutValue(items[index])
2626
- );
2627
- }
2628
- const headers = this.context.invokeAny(
2629
- session,
2630
- paths.map((path) => ({
2631
- spaceId: session.spaceId,
2632
- service: "kv",
2633
- path,
2634
- action: KVAction.PUT
2635
- }))
2714
+ const network = () => this.batchPutToNetwork(items, paths, request.signal);
2715
+ if (this.readThrough === null) return await network();
2716
+ const entries = items.map((item, index) => ({
2717
+ path: paths[index],
2718
+ body: this.serializeBatchPutValue(item)
2719
+ }));
2720
+ return await this.readThrough.write({
2721
+ op: "batchPut",
2722
+ space: session.spaceId,
2723
+ entries,
2724
+ signal: request.signal,
2725
+ network: () => this.batchPutToNetwork(items, paths, request.signal, entries)
2726
+ });
2727
+ } catch (error) {
2728
+ const wrapped = wrapError("kv", error);
2729
+ return err({ ...wrapped, meta: { ...wrapped.meta, requestMayHaveDispatched: false } });
2730
+ } finally {
2731
+ request.dispose();
2732
+ }
2733
+ });
2734
+ }
2735
+ async batchPutToNetwork(items, paths, signal, entries) {
2736
+ const session = this.context.session;
2737
+ let requestMayHaveDispatched = false;
2738
+ try {
2739
+ const body = new FormData();
2740
+ for (let index = 0; index < items.length; index++) {
2741
+ body.append(
2742
+ encodeKvBatchPartName(paths[index]),
2743
+ entries?.[index]?.body ?? this.serializeBatchPutValue(items[index])
2636
2744
  );
2637
- const url = `${this.host}/invoke`;
2638
- const init = {
2639
- method: "POST",
2640
- headers,
2641
- body,
2642
- signal: request.signal
2643
- };
2644
- const fetchFn = this.context.fetch;
2645
- requestMayHaveDispatched = true;
2646
- const response = await fetchFn(url, init);
2647
- if (!response.ok) {
2648
- let errorText;
2649
- try {
2650
- errorText = await response.text();
2651
- } catch (textError) {
2652
- const cause = textError instanceof Error ? textError : new Error(String(textError));
2653
- return err(
2654
- serviceError(
2655
- ErrorCodes.KV_WRITE_FAILED,
2656
- `Failed to batch put ${items.length} key(s): ${response.status} - <response body could not be read: ${cause.message}>`,
2657
- "kv",
2658
- { cause, meta: { status: response.status, statusText: response.statusText } }
2659
- )
2660
- );
2661
- }
2662
- if (response.status === 401 || response.status === 403) {
2663
- return this.authorizationFailure(
2664
- `Failed to batch put ${items.length} key(s)`,
2665
- response,
2666
- errorText,
2667
- paths,
2668
- KVAction.PUT
2669
- );
2670
- }
2671
- const quotaError = this.handleQuotaErrorResponse(
2672
- response,
2673
- errorText,
2674
- "batch"
2675
- );
2676
- if (quotaError) {
2677
- return quotaError;
2678
- }
2745
+ }
2746
+ const headers = this.context.invokeAny(
2747
+ session,
2748
+ paths.map((path) => ({
2749
+ spaceId: session.spaceId,
2750
+ service: "kv",
2751
+ path,
2752
+ action: KVAction.PUT
2753
+ }))
2754
+ );
2755
+ const url = `${this.host}/invoke`;
2756
+ const init = {
2757
+ method: "POST",
2758
+ headers,
2759
+ body,
2760
+ signal
2761
+ };
2762
+ const fetchFn = this.context.fetch;
2763
+ requestMayHaveDispatched = true;
2764
+ const response = await fetchFn(url, init);
2765
+ if (!response.ok) {
2766
+ let errorText;
2767
+ try {
2768
+ errorText = await response.text();
2769
+ } catch (textError) {
2770
+ const cause = textError instanceof Error ? textError : new Error(String(textError));
2679
2771
  return err(
2680
2772
  serviceError(
2681
2773
  ErrorCodes.KV_WRITE_FAILED,
2682
- `Failed to batch put ${items.length} key(s): ${response.status} - ${errorText}`,
2774
+ `Failed to batch put ${items.length} key(s): ${response.status} - <response body could not be read: ${cause.message}>`,
2683
2775
  "kv",
2684
- { meta: { status: response.status, statusText: response.statusText } }
2776
+ { cause, meta: { status: response.status, statusText: response.statusText } }
2685
2777
  )
2686
2778
  );
2687
2779
  }
2688
- let rawBody;
2689
- try {
2690
- rawBody = await response.json();
2691
- } catch (jsonError) {
2692
- const cause = jsonError instanceof Error ? jsonError : new Error(String(jsonError));
2693
- const timedOut = isRequestTimeoutError(cause);
2694
- return err(
2695
- serviceError(
2696
- timedOut ? ErrorCodes.TIMEOUT : ErrorCodes.NETWORK_ERROR,
2697
- timedOut ? `KV batchPut response body did not arrive in time: ${cause.message}` : `KV batchPut response was not valid JSON: ${cause.message}`,
2698
- "kv",
2699
- {
2700
- cause,
2701
- meta: {
2702
- ...timedOut ? { timeoutMs: cause.timeoutMs } : {},
2703
- requestMayHaveDispatched: true,
2704
- responseReceived: true,
2705
- status: response.status,
2706
- outcome: "batch-unconfirmed"
2707
- }
2708
- }
2709
- )
2780
+ if (response.status === 401 || response.status === 403) {
2781
+ return this.authorizationFailure(
2782
+ `Failed to batch put ${items.length} key(s)`,
2783
+ response,
2784
+ errorText,
2785
+ paths,
2786
+ KVAction.PUT
2710
2787
  );
2711
2788
  }
2712
- const batchResponse = this.normalizeBatchPutResponse(rawBody);
2713
- if (!batchResponse) {
2714
- return err(
2715
- serviceError(
2716
- ErrorCodes.NETWORK_ERROR,
2717
- "KV batchPut response did not include matching written keys and count",
2718
- "kv",
2719
- {
2720
- meta: {
2721
- requestMayHaveDispatched: true,
2722
- responseReceived: true,
2723
- status: response.status,
2724
- outcome: "batch-unconfirmed"
2725
- }
2726
- }
2727
- )
2728
- );
2789
+ const quotaError = this.handleQuotaErrorResponse(
2790
+ response,
2791
+ errorText,
2792
+ "batch"
2793
+ );
2794
+ if (quotaError) {
2795
+ return quotaError;
2729
2796
  }
2730
- const requestedPaths = new Set(paths);
2731
- const writtenPaths = new Set(batchResponse.written);
2732
- const matchesRequest = batchResponse.count === paths.length && batchResponse.written.length === paths.length && writtenPaths.size === paths.length && batchResponse.written.every((key) => requestedPaths.has(key));
2733
- if (!matchesRequest) {
2734
- return err(
2735
- serviceError(
2736
- ErrorCodes.NETWORK_ERROR,
2737
- `KV batchPut response did not confirm all ${paths.length} requested key(s) were written`,
2738
- "kv",
2739
- {
2740
- meta: {
2741
- requestMayHaveDispatched: true,
2742
- responseReceived: true,
2743
- status: response.status,
2744
- outcome: "batch-unconfirmed"
2745
- }
2797
+ return err(
2798
+ serviceError(
2799
+ ErrorCodes.KV_WRITE_FAILED,
2800
+ `Failed to batch put ${items.length} key(s): ${response.status} - ${errorText}`,
2801
+ "kv",
2802
+ { meta: { status: response.status, statusText: response.statusText } }
2803
+ )
2804
+ );
2805
+ }
2806
+ let rawBody;
2807
+ try {
2808
+ rawBody = await response.json();
2809
+ } catch (jsonError) {
2810
+ const cause = jsonError instanceof Error ? jsonError : new Error(String(jsonError));
2811
+ const timedOut = isRequestTimeoutError(cause);
2812
+ return err(
2813
+ serviceError(
2814
+ timedOut ? ErrorCodes.TIMEOUT : ErrorCodes.NETWORK_ERROR,
2815
+ timedOut ? `KV batchPut response body did not arrive in time: ${cause.message}` : `KV batchPut response was not valid JSON: ${cause.message}`,
2816
+ "kv",
2817
+ {
2818
+ cause,
2819
+ meta: {
2820
+ ...timedOut ? { timeoutMs: cause.timeoutMs } : {},
2821
+ requestMayHaveDispatched: true,
2822
+ responseReceived: true,
2823
+ status: response.status,
2824
+ outcome: "batch-unconfirmed"
2746
2825
  }
2747
- )
2748
- );
2749
- }
2750
- return ok(batchResponse);
2751
- } catch (error) {
2752
- const wrapped = wrapError("kv", error);
2753
- return err({
2754
- ...wrapped,
2755
- meta: { ...wrapped.meta, requestMayHaveDispatched }
2756
- });
2757
- } finally {
2758
- request.dispose();
2826
+ }
2827
+ )
2828
+ );
2759
2829
  }
2760
- });
2830
+ const batchResponse = this.normalizeBatchPutResponse(rawBody);
2831
+ if (!batchResponse) {
2832
+ return err(
2833
+ serviceError(
2834
+ ErrorCodes.NETWORK_ERROR,
2835
+ "KV batchPut response did not include matching written keys and count",
2836
+ "kv",
2837
+ {
2838
+ meta: {
2839
+ requestMayHaveDispatched: true,
2840
+ responseReceived: true,
2841
+ status: response.status,
2842
+ outcome: "batch-unconfirmed"
2843
+ }
2844
+ }
2845
+ )
2846
+ );
2847
+ }
2848
+ const requestedPaths = new Set(paths);
2849
+ const writtenPaths = new Set(batchResponse.written);
2850
+ const matchesRequest = batchResponse.count === paths.length && batchResponse.written.length === paths.length && writtenPaths.size === paths.length && batchResponse.written.every((key) => requestedPaths.has(key));
2851
+ if (!matchesRequest) {
2852
+ return err(
2853
+ serviceError(
2854
+ ErrorCodes.NETWORK_ERROR,
2855
+ `KV batchPut response did not confirm all ${paths.length} requested key(s) were written`,
2856
+ "kv",
2857
+ {
2858
+ meta: {
2859
+ requestMayHaveDispatched: true,
2860
+ responseReceived: true,
2861
+ status: response.status,
2862
+ outcome: "batch-unconfirmed"
2863
+ }
2864
+ }
2865
+ )
2866
+ );
2867
+ }
2868
+ return ok(batchResponse);
2869
+ } catch (error) {
2870
+ const wrapped = wrapError("kv", error);
2871
+ return err({
2872
+ ...wrapped,
2873
+ meta: { ...wrapped.meta, requestMayHaveDispatched }
2874
+ });
2875
+ }
2761
2876
  }
2762
2877
  /**
2763
2878
  * List keys with optional prefix filtering.
2764
2879
  */
2765
2880
  async list(options) {
2766
2881
  return this.withTelemetry("list", options?.prefix, async () => {
2882
+ if (this.readThroughReady !== null) await this.readThroughReady;
2767
2883
  if (!this.requireAuth()) {
2768
2884
  return err(authRequiredError("kv"));
2769
2885
  }
@@ -2780,49 +2896,44 @@ var KVService = class extends BaseService {
2780
2896
  }
2781
2897
  const request = this.createRequestSignal(options?.signal, options?.timeout);
2782
2898
  try {
2783
- const response = await this.invokeOperation(
2784
- listPath,
2785
- KVAction.LIST,
2786
- void 0,
2787
- request.signal,
2788
- options?.limit === void 0 && options?.cursor === void 0 ? void 0 : {
2789
- ...options?.limit === void 0 ? {} : { "x-tinycloud-limit": String(options.limit) },
2790
- ...options?.cursor === void 0 ? {} : { "x-tinycloud-cursor": options.cursor }
2791
- }
2792
- );
2793
- if (!response.ok) {
2794
- if (response.status === 401 || response.status === 403) {
2795
- return this.authorizationFailure(
2796
- "Failed to list keys",
2797
- response,
2798
- await this.readAuthorizationText(response),
2799
- [listPath],
2800
- KVAction.LIST
2801
- );
2899
+ const network = () => this.listFromNetwork(listPath, options, request.signal);
2900
+ let page;
2901
+ if (options?.source === "network") {
2902
+ const observer = this.readThrough;
2903
+ if (observer === null) {
2904
+ page = await network();
2905
+ } else {
2906
+ let outcome = "error";
2907
+ const startedAt = Date.now();
2908
+ try {
2909
+ page = await network();
2910
+ outcome = page.ok ? "found" : "error";
2911
+ } finally {
2912
+ try {
2913
+ observer.observeNetworkRequested({
2914
+ op: "list",
2915
+ space: this.context.session.spaceId,
2916
+ path: listPath,
2917
+ reason: "NETWORK_REQUESTED",
2918
+ outcome,
2919
+ latencyMs: Date.now() - startedAt
2920
+ });
2921
+ } catch {
2922
+ }
2923
+ }
2802
2924
  }
2803
- const errorText = await response.text();
2804
- return err(
2805
- serviceError(
2806
- ErrorCodes.NETWORK_ERROR,
2807
- `Failed to list keys: ${response.status} - ${errorText}`,
2808
- "kv",
2809
- { meta: { status: response.status, statusText: response.statusText } }
2810
- )
2811
- );
2812
- }
2813
- let keys = await this.parseResponse(response, options?.raw);
2814
- keys = keys ?? [];
2815
- if (options?.removePrefix && listPath) {
2816
- const prefixWithSlash = listPath.endsWith("/") ? listPath : `${listPath}/`;
2817
- keys = keys.map(
2818
- (key) => key.startsWith(prefixWithSlash) ? key.slice(prefixWithSlash.length) : key
2819
- );
2925
+ } else if (this.readThrough === null) {
2926
+ page = await network();
2927
+ } else {
2928
+ page = await this.readThrough.list({
2929
+ space: this.context.session.spaceId,
2930
+ listPath,
2931
+ options,
2932
+ signal: request.signal,
2933
+ network
2934
+ });
2820
2935
  }
2821
- return ok({
2822
- keys,
2823
- ...response.headers.get("x-tinycloud-truncated") === null ? {} : { truncated: response.headers.get("x-tinycloud-truncated") === "true" },
2824
- ...response.headers.get("x-tinycloud-next-cursor") === null ? {} : { nextCursor: response.headers.get("x-tinycloud-next-cursor") ?? void 0 }
2825
- });
2936
+ return page.ok ? this.finishList(page.data, listPath, options?.removePrefix) : page;
2826
2937
  } catch (error) {
2827
2938
  return err(wrapError("kv", error));
2828
2939
  } finally {
@@ -2830,6 +2941,56 @@ var KVService = class extends BaseService {
2830
2941
  }
2831
2942
  });
2832
2943
  }
2944
+ async listFromNetwork(listPath, options, signal) {
2945
+ const response = await this.invokeOperation(
2946
+ listPath,
2947
+ KVAction.LIST,
2948
+ void 0,
2949
+ signal,
2950
+ options?.limit === void 0 && options?.cursor === void 0 ? void 0 : {
2951
+ ...options?.limit === void 0 ? {} : { "x-tinycloud-limit": String(options.limit) },
2952
+ ...options?.cursor === void 0 ? {} : { "x-tinycloud-cursor": options.cursor }
2953
+ }
2954
+ );
2955
+ if (!response.ok) {
2956
+ if (response.status === 401 || response.status === 403) {
2957
+ return this.authorizationFailure(
2958
+ "Failed to list keys",
2959
+ response,
2960
+ await this.readAuthorizationText(response),
2961
+ [listPath],
2962
+ KVAction.LIST
2963
+ );
2964
+ }
2965
+ const errorText = await response.text();
2966
+ return err(serviceError(
2967
+ ErrorCodes.NETWORK_ERROR,
2968
+ `Failed to list keys: ${response.status} - ${errorText}`,
2969
+ "kv",
2970
+ { meta: { status: response.status, statusText: response.statusText } }
2971
+ ));
2972
+ }
2973
+ const keys = await this.parseResponse(response, options?.raw) ?? [];
2974
+ return ok({
2975
+ keys,
2976
+ ...response.headers.get("x-tinycloud-truncated") === null ? {} : { truncated: response.headers.get("x-tinycloud-truncated") === "true" },
2977
+ ...response.headers.get("x-tinycloud-next-cursor") === null ? {} : { nextCursor: response.headers.get("x-tinycloud-next-cursor") ?? void 0 }
2978
+ });
2979
+ }
2980
+ finishList(page, listPath, removePrefix) {
2981
+ let keys = page.keys;
2982
+ if (removePrefix && listPath) {
2983
+ const prefixWithSlash = listPath.endsWith("/") ? listPath : `${listPath}/`;
2984
+ keys = keys.map(
2985
+ (key) => key.startsWith(prefixWithSlash) ? key.slice(prefixWithSlash.length) : key
2986
+ );
2987
+ }
2988
+ return ok({
2989
+ keys,
2990
+ ...page.truncated === void 0 ? {} : { truncated: page.truncated },
2991
+ ...page.nextCursor === void 0 ? {} : { nextCursor: page.nextCursor }
2992
+ });
2993
+ }
2833
2994
  /**
2834
2995
  * Read one page of the `tinycloud.kv/sync` change feed for a prefix.
2835
2996
  */
@@ -2852,6 +3013,7 @@ var KVService = class extends BaseService {
2852
3013
  "kv"
2853
3014
  ));
2854
3015
  }
3016
+ const requestPrefix = options.prefix === "/" ? "" : options.prefix;
2855
3017
  const headers = {};
2856
3018
  if (options.limit !== void 0) headers["x-tinycloud-limit"] = String(options.limit);
2857
3019
  if (options.cursor !== void 0) headers["x-tinycloud-cursor"] = options.cursor;
@@ -2861,7 +3023,7 @@ var KVService = class extends BaseService {
2861
3023
  const request = this.createRequestSignal(options.signal, options.timeout);
2862
3024
  try {
2863
3025
  const response = await this.invokeOperation(
2864
- options.prefix,
3026
+ requestPrefix,
2865
3027
  KVAction.SYNC,
2866
3028
  void 0,
2867
3029
  request.signal,
@@ -2877,7 +3039,7 @@ var KVService = class extends BaseService {
2877
3039
  } catch {
2878
3040
  body = void 0;
2879
3041
  }
2880
- const page = this.normalizeChangesResponse(body);
3042
+ const page = this.normalizeChangesResponse(body, options.prefix);
2881
3043
  if (!page) {
2882
3044
  return err(serviceError(
2883
3045
  ErrorCodes.NETWORK_ERROR,
@@ -2957,7 +3119,7 @@ var KVService = class extends BaseService {
2957
3119
  { meta }
2958
3120
  ));
2959
3121
  }
2960
- normalizeChangesResponse(data) {
3122
+ normalizeChangesResponse(data, requestedPrefix) {
2961
3123
  if (!isJsonObject(data)) return void 0;
2962
3124
  const { changes, more, cursor, source, authority } = data;
2963
3125
  if (!Array.isArray(changes) || typeof more !== "boolean" || typeof cursor !== "string" || !isJsonObject(source) || typeof source.nodeDid !== "string" || typeof source.space !== "string" || typeof source.prefix !== "string" || !isJsonObject(authority)) {
@@ -2989,7 +3151,11 @@ var KVService = class extends BaseService {
2989
3151
  changes: parsed,
2990
3152
  more,
2991
3153
  cursor,
2992
- source: { nodeDid: source.nodeDid, space: source.space, prefix: source.prefix },
3154
+ source: {
3155
+ nodeDid: source.nodeDid,
3156
+ space: source.space,
3157
+ prefix: requestedPrefix === "/" && source.prefix === "" ? "/" : source.prefix
3158
+ },
2993
3159
  authority: { notBefore, expiresAt, retainUntil }
2994
3160
  };
2995
3161
  }
@@ -2998,61 +3164,22 @@ var KVService = class extends BaseService {
2998
3164
  */
2999
3165
  async delete(key, options) {
3000
3166
  return this.withTelemetry("delete", key, async () => {
3167
+ if (this.readThroughReady !== null) await this.readThroughReady;
3001
3168
  if (!this.requireAuth()) {
3002
3169
  return err(authRequiredError("kv"));
3003
3170
  }
3004
3171
  const path = this.getFullPath(key, options?.prefix);
3005
3172
  const request = this.createRequestSignal(options?.signal, options?.timeout);
3006
3173
  try {
3007
- const response = await this.invokeOperation(
3008
- path,
3009
- KVAction.DELETE,
3010
- void 0,
3011
- request.signal,
3012
- options?.ifMatch === void 0 ? void 0 : { "if-match": options.ifMatch }
3013
- );
3014
- if (!response.ok) {
3015
- if (response.status === 401 || response.status === 403) {
3016
- return this.authorizationFailure(
3017
- `Failed to delete key ${JSON.stringify(key)}`,
3018
- response,
3019
- await this.readAuthorizationText(response),
3020
- [path],
3021
- KVAction.DELETE
3022
- );
3023
- }
3024
- if (response.status === 404) {
3025
- return await this.classifyNotFound(response, key);
3026
- }
3027
- const errorText = await response.text();
3028
- if (response.status === 412) {
3029
- return err(serviceError(
3030
- ErrorCodes.KV_PRECONDITION_FAILED,
3031
- `KV precondition failed for key ${JSON.stringify(key)}`,
3032
- "kv",
3033
- { meta: { status: response.status, statusText: response.statusText } }
3034
- ));
3035
- }
3036
- if (response.status === 503 && options?.ifMatch !== void 0) {
3037
- return err(serviceError(
3038
- ErrorCodes.KV_CONFLICT,
3039
- `Concurrent KV delete conflicted for key ${JSON.stringify(key)}`,
3040
- "kv",
3041
- { meta: { status: response.status, statusText: response.statusText } }
3042
- ));
3043
- }
3044
- return err(
3045
- serviceError(
3046
- ErrorCodes.NETWORK_ERROR,
3047
- `Failed to delete key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
3048
- "kv",
3049
- { meta: { status: response.status, statusText: response.statusText } }
3050
- )
3051
- );
3052
- }
3053
- return ok({
3054
- data: void 0,
3055
- headers: this.createResponseHeaders(response.headers)
3174
+ const network = () => this.deleteFromNetwork(key, path, options, request.signal);
3175
+ if (this.readThrough === null) return await network();
3176
+ return await this.readThrough.write({
3177
+ op: "delete",
3178
+ space: this.context.session.spaceId,
3179
+ entries: [{ path }],
3180
+ signal: request.signal,
3181
+ conditional: options?.ifMatch !== void 0,
3182
+ network
3056
3183
  });
3057
3184
  } catch (error) {
3058
3185
  return err(wrapError("kv", error));
@@ -3061,6 +3188,54 @@ var KVService = class extends BaseService {
3061
3188
  }
3062
3189
  });
3063
3190
  }
3191
+ async deleteFromNetwork(key, path, options, signal) {
3192
+ const response = await this.invokeOperation(
3193
+ path,
3194
+ KVAction.DELETE,
3195
+ void 0,
3196
+ signal,
3197
+ options?.ifMatch === void 0 ? void 0 : { "if-match": options.ifMatch }
3198
+ );
3199
+ if (!response.ok) {
3200
+ if (response.status === 401 || response.status === 403) {
3201
+ return this.authorizationFailure(
3202
+ `Failed to delete key ${JSON.stringify(key)}`,
3203
+ response,
3204
+ await this.readAuthorizationText(response),
3205
+ [path],
3206
+ KVAction.DELETE
3207
+ );
3208
+ }
3209
+ if (response.status === 404) return this.classifyNotFound(response, key);
3210
+ const errorText = await response.text();
3211
+ if (response.status === 412) {
3212
+ return err(serviceError(
3213
+ ErrorCodes.KV_PRECONDITION_FAILED,
3214
+ `KV precondition failed for key ${JSON.stringify(key)}`,
3215
+ "kv",
3216
+ { meta: { status: response.status, statusText: response.statusText } }
3217
+ ));
3218
+ }
3219
+ if (response.status === 503 && options?.ifMatch !== void 0) {
3220
+ return err(serviceError(
3221
+ ErrorCodes.KV_CONFLICT,
3222
+ `Concurrent KV delete conflicted for key ${JSON.stringify(key)}`,
3223
+ "kv",
3224
+ { meta: { status: response.status, statusText: response.statusText } }
3225
+ ));
3226
+ }
3227
+ return err(serviceError(
3228
+ ErrorCodes.NETWORK_ERROR,
3229
+ `Failed to delete key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
3230
+ "kv",
3231
+ { meta: { status: response.status, statusText: response.statusText } }
3232
+ ));
3233
+ }
3234
+ return ok({
3235
+ data: void 0,
3236
+ headers: this.createResponseHeaders(response.headers)
3237
+ });
3238
+ }
3064
3239
  /**
3065
3240
  * Get metadata for a key without retrieving the value.
3066
3241
  */
@@ -3228,6 +3403,65 @@ var KVService = class extends BaseService {
3228
3403
  */
3229
3404
  KVService.serviceName = "kv";
3230
3405
 
3406
+ // src/kv/replication/constants.ts
3407
+ 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.";
3408
+
3409
+ // src/kv/replication/identity.ts
3410
+ var SPACE_ID = /^(tinycloud:pkh:eip155:\d+:)([^:]+)(:.+)$/;
3411
+ var PRINCIPAL_DID = /^(did:pkh:eip155:\d+:)([^:]+)$/;
3412
+ function canonicalReplicationIdentity(raw) {
3413
+ if (!raw || typeof raw.host !== "string" || typeof raw.space !== "string" || typeof raw.principal !== "string") {
3414
+ throw new TypeError(
3415
+ "Replication identity host, space, and principal must be strings"
3416
+ );
3417
+ }
3418
+ const authority = /^https?:\/\/([^/?#]*)/i.exec(raw.host)?.[1];
3419
+ let url;
3420
+ try {
3421
+ url = new URL(raw.host);
3422
+ } catch {
3423
+ throw new TypeError(
3424
+ "Replication identity host must be an absolute HTTP(S) URL"
3425
+ );
3426
+ }
3427
+ if (!authority || authority.includes("@") || raw.host.includes("?") || raw.host.includes("#") || url.username || url.password) {
3428
+ throw new TypeError(
3429
+ "Replication identity host must be HTTP(S) without credentials, query, or fragment"
3430
+ );
3431
+ }
3432
+ const space = SPACE_ID.exec(raw.space);
3433
+ const principal = PRINCIPAL_DID.exec(raw.principal);
3434
+ if (!space || !principal) {
3435
+ throw new TypeError(
3436
+ "Replication identity requires a full TinyCloud space id and a pkh principal DID"
3437
+ );
3438
+ }
3439
+ return {
3440
+ host: `${url.origin}${url.pathname.replace(/\/+$/, "")}`,
3441
+ space: `${space[1]}${space[2].toLowerCase()}${space[3]}`,
3442
+ principal: `${principal[1]}${principal[2].toLowerCase()}`
3443
+ };
3444
+ }
3445
+ function replicationIdentityKey(id) {
3446
+ const canonical = canonicalReplicationIdentity(id);
3447
+ return `tc-replication/v1
3448
+ ${canonical.host}
3449
+ ${canonical.space}
3450
+ ${canonical.principal}`;
3451
+ }
3452
+
3453
+ // src/kv/replication/prefix.ts
3454
+ function kvPrefixCovers(prefix, key) {
3455
+ return prefix === "/" || key.startsWith(prefix);
3456
+ }
3457
+
3458
+ // src/kv/replication/scope.ts
3459
+ function requiresSecretsOptIn(space, prefix) {
3460
+ if (prefix === "/") return true;
3461
+ if (space === "secrets" || space.endsWith(":secrets")) return true;
3462
+ return prefix.split("/", 1)[0] === "vault";
3463
+ }
3464
+
3231
3465
  // src/responseErrors.ts
3232
3466
  function parseServiceErrorBody(errorText) {
3233
3467
  try {
@@ -3404,7 +3638,7 @@ var SQLService = class extends BaseService {
3404
3638
  request.signal
3405
3639
  );
3406
3640
  if (!response.ok) {
3407
- return this.handleErrorResponse(response, "query", dbName, [action]);
3641
+ return await this.handleErrorResponse(response, "query", dbName, [action]);
3408
3642
  }
3409
3643
  const data = await response.json();
3410
3644
  return ok(data);
@@ -3444,7 +3678,7 @@ var SQLService = class extends BaseService {
3444
3678
  request.signal
3445
3679
  );
3446
3680
  if (!response.ok) {
3447
- return this.handleErrorResponse(response, "execute", dbName, requestedActions);
3681
+ return await this.handleErrorResponse(response, "execute", dbName, requestedActions);
3448
3682
  }
3449
3683
  const data = await response.json();
3450
3684
  return ok(data);
@@ -3470,7 +3704,7 @@ var SQLService = class extends BaseService {
3470
3704
  request.signal
3471
3705
  );
3472
3706
  if (!response.ok) {
3473
- return this.handleErrorResponse(response, "batch", dbName, requestedActions);
3707
+ return await this.handleErrorResponse(response, "batch", dbName, requestedActions);
3474
3708
  }
3475
3709
  const data = await response.json();
3476
3710
  return ok(data);
@@ -3495,7 +3729,7 @@ var SQLService = class extends BaseService {
3495
3729
  request.signal
3496
3730
  );
3497
3731
  if (!response.ok) {
3498
- return this.handleErrorResponse(response, "executeStatement", dbName, [SQLAction.WRITE]);
3732
+ return await this.handleErrorResponse(response, "executeStatement", dbName, [SQLAction.WRITE]);
3499
3733
  }
3500
3734
  const data = await response.json();
3501
3735
  return ok(data);
@@ -3520,7 +3754,7 @@ var SQLService = class extends BaseService {
3520
3754
  request.signal
3521
3755
  );
3522
3756
  if (!response.ok) {
3523
- return this.handleErrorResponse(response, "export", dbName, [SQLAction.READ]);
3757
+ return await this.handleErrorResponse(response, "export", dbName, [SQLAction.READ]);
3524
3758
  }
3525
3759
  const resp = response;
3526
3760
  if (typeof resp.blob === "function") {
@@ -3896,7 +4130,7 @@ var DuckDbService = class extends BaseService {
3896
4130
  request.signal
3897
4131
  );
3898
4132
  if (!response.ok) {
3899
- return this.handleErrorResponse(response, "query");
4133
+ return await this.handleErrorResponse(response, "query");
3900
4134
  }
3901
4135
  const data = await response.json();
3902
4136
  return ok(data);
@@ -3922,7 +4156,7 @@ var DuckDbService = class extends BaseService {
3922
4156
  { Accept: "application/vnd.apache.arrow.stream" }
3923
4157
  );
3924
4158
  if (!response.ok) {
3925
- return this.handleErrorResponse(response, "queryArrow");
4159
+ return await this.handleErrorResponse(response, "queryArrow");
3926
4160
  }
3927
4161
  const buffer = await response.arrayBuffer();
3928
4162
  return ok(buffer);
@@ -3955,7 +4189,7 @@ var DuckDbService = class extends BaseService {
3955
4189
  request.signal
3956
4190
  );
3957
4191
  if (!response.ok) {
3958
- return this.handleErrorResponse(response, "execute");
4192
+ return await this.handleErrorResponse(response, "execute");
3959
4193
  }
3960
4194
  const data = await response.json();
3961
4195
  return ok(data);
@@ -3987,7 +4221,7 @@ var DuckDbService = class extends BaseService {
3987
4221
  request.signal
3988
4222
  );
3989
4223
  if (!response.ok) {
3990
- return this.handleErrorResponse(response, "batch");
4224
+ return await this.handleErrorResponse(response, "batch");
3991
4225
  }
3992
4226
  const data = await response.json();
3993
4227
  return ok(data);
@@ -4012,7 +4246,7 @@ var DuckDbService = class extends BaseService {
4012
4246
  request.signal
4013
4247
  );
4014
4248
  if (!response.ok) {
4015
- return this.handleErrorResponse(response, "executeStatement");
4249
+ return await this.handleErrorResponse(response, "executeStatement");
4016
4250
  }
4017
4251
  const data = await response.json();
4018
4252
  return ok(data);
@@ -4037,7 +4271,7 @@ var DuckDbService = class extends BaseService {
4037
4271
  request.signal
4038
4272
  );
4039
4273
  if (!response.ok) {
4040
- return this.handleErrorResponse(response, "describe");
4274
+ return await this.handleErrorResponse(response, "describe");
4041
4275
  }
4042
4276
  const data = await response.json();
4043
4277
  return ok(data);
@@ -4062,7 +4296,7 @@ var DuckDbService = class extends BaseService {
4062
4296
  request.signal
4063
4297
  );
4064
4298
  if (!response.ok) {
4065
- return this.handleErrorResponse(response, "export");
4299
+ return await this.handleErrorResponse(response, "export");
4066
4300
  }
4067
4301
  const blob = await response.blob();
4068
4302
  return ok(blob);
@@ -4097,7 +4331,7 @@ var DuckDbService = class extends BaseService {
4097
4331
  signal: request.signal
4098
4332
  });
4099
4333
  if (!response.ok) {
4100
- return this.handleErrorResponse(response, "import");
4334
+ return await this.handleErrorResponse(response, "import");
4101
4335
  }
4102
4336
  return ok(void 0);
4103
4337
  } catch (error) {
@@ -5809,7 +6043,8 @@ var DataVaultService = class extends BaseService {
5809
6043
  }
5810
6044
  try {
5811
6045
  const valueResult = await this.tc.kv.get(`vault/${key}`, {
5812
- raw: true
6046
+ raw: true,
6047
+ ...options?.source === void 0 ? {} : { source: options.source }
5813
6048
  });
5814
6049
  if (!valueResult.ok) {
5815
6050
  return vaultError({ code: "KEY_NOT_FOUND", key });
@@ -5854,7 +6089,7 @@ var DataVaultService = class extends BaseService {
5854
6089
  }
5855
6090
  let valueResult;
5856
6091
  try {
5857
- valueResult = await this.tc.kv.get(`vault/${key}`, { raw: true });
6092
+ valueResult = await this.tc.kv.get(`vault/${key}`, { raw: true, source: "network" });
5858
6093
  } catch {
5859
6094
  return { status: "node_unreachable" };
5860
6095
  }
@@ -5923,7 +6158,8 @@ var DataVaultService = class extends BaseService {
5923
6158
  }
5924
6159
  try {
5925
6160
  const valueResult = await this.tc.kv.get(`vault/${key}`, {
5926
- raw: true
6161
+ raw: true,
6162
+ source: "network"
5927
6163
  });
5928
6164
  if (!valueResult.ok) {
5929
6165
  return vaultError({ code: "KEY_NOT_FOUND", key });
@@ -6057,7 +6293,8 @@ var DataVaultService = class extends BaseService {
6057
6293
  }
6058
6294
  try {
6059
6295
  const keyResult = await this.tc.kv.get(`keys/${key}`, {
6060
- raw: true
6296
+ raw: true,
6297
+ ...options?.source === void 0 ? {} : { source: options.source }
6061
6298
  });
6062
6299
  if (!keyResult.ok) {
6063
6300
  return vaultError({ code: "KEY_NOT_FOUND", key });
@@ -6066,7 +6303,8 @@ var DataVaultService = class extends BaseService {
6066
6303
  const keyBlobBytes = base64Decode(keyEnvelope.key);
6067
6304
  const entryKey = this.crypto.decrypt(this.masterKey, keyBlobBytes);
6068
6305
  const valueResult = await this.tc.kv.get(`vault/${key}`, {
6069
- raw: true
6306
+ raw: true,
6307
+ ...options?.source === void 0 ? {} : { source: options.source }
6070
6308
  });
6071
6309
  if (!valueResult.ok) {
6072
6310
  return vaultError({ code: "KEY_NOT_FOUND", key });
@@ -6176,6 +6414,7 @@ var DataVaultService = class extends BaseService {
6176
6414
  prefix: listPrefix,
6177
6415
  removePrefix: true,
6178
6416
  ...options?.limit === void 0 ? {} : { limit: options.limit },
6417
+ ...options?.source === void 0 ? {} : { source: options.source },
6179
6418
  ...options?.cursor === void 0 ? {} : { cursor: options.cursor }
6180
6419
  });
6181
6420
  if (!listResult.ok) {
@@ -6900,7 +7139,9 @@ var SecretsService = class {
6900
7139
  async get(name, options) {
6901
7140
  const secretPath = resolveSecretPathResult(name, options);
6902
7141
  if ("ok" in secretPath) return secretPath;
6903
- const result = await this.vault.get(secretPath.vaultKey);
7142
+ const result = options?.source === void 0 ? await this.vault.get(secretPath.vaultKey) : await this.vault.get(secretPath.vaultKey, {
7143
+ source: options.source
7144
+ });
6904
7145
  if (!result.ok) {
6905
7146
  return result;
6906
7147
  }
@@ -6934,7 +7175,8 @@ var SecretsService = class {
6934
7175
  }
6935
7176
  const result = await this.vault.list({
6936
7177
  prefix,
6937
- removePrefix: true
7178
+ removePrefix: true,
7179
+ ...options?.source === void 0 ? {} : { source: options.source }
6938
7180
  });
6939
7181
  if (!result.ok) {
6940
7182
  return result;
@@ -6953,6 +7195,7 @@ var SecretsService = class {
6953
7195
  prefix: "secrets/",
6954
7196
  removePrefix: true,
6955
7197
  limit: 1e3,
7198
+ source: "network",
6956
7199
  ...cursor === void 0 ? {} : { cursor }
6957
7200
  });
6958
7201
  if (!result.ok) {
@@ -7945,6 +8188,7 @@ var import_decrypt_transport_response_error3 = require("@tinycloud/sdk-services/
7945
8188
  // Annotate the CommonJS export names for ESM import in node:
7946
8189
  0 && (module.exports = {
7947
8190
  BaseService,
8191
+ CLEAR_PENDING_WARNING,
7948
8192
  DECRYPT_ACTION,
7949
8193
  DECRYPT_FACT_TYPE,
7950
8194
  DECRYPT_RESULT_TYPE,
@@ -8012,6 +8256,7 @@ var import_decrypt_transport_response_error3 = require("@tinycloud/sdk-services/
8012
8256
  buildDecryptInvocation,
8013
8257
  buildNetworkId,
8014
8258
  canonicalHashHex,
8259
+ canonicalReplicationIdentity,
8015
8260
  canonicalSignedResponse,
8016
8261
  canonicalizeEncryptionJson,
8017
8262
  canonicalizeSecretScope,
@@ -8038,6 +8283,7 @@ var import_decrypt_transport_response_error3 = require("@tinycloud/sdk-services/
8038
8283
  installTinyCloudDebugGlobals,
8039
8284
  isNetworkId,
8040
8285
  isStorageFullError,
8286
+ kvPrefixCovers,
8041
8287
  networkDiscoveryKey,
8042
8288
  networkError,
8043
8289
  notFoundError,
@@ -8051,6 +8297,8 @@ var import_decrypt_transport_response_error3 = require("@tinycloud/sdk-services/
8051
8297
  parseSecretCatalogKey,
8052
8298
  parseStorageQuotaBytes,
8053
8299
  permissionDeniedError,
8300
+ replicationIdentityKey,
8301
+ requiresSecretsOptIn,
8054
8302
  resolveSecretListPrefix,
8055
8303
  resolveSecretPath,
8056
8304
  serviceError,