@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.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
  */
@@ -2998,61 +3159,22 @@ var KVService = class extends BaseService {
2998
3159
  */
2999
3160
  async delete(key, options) {
3000
3161
  return this.withTelemetry("delete", key, async () => {
3162
+ if (this.readThroughReady !== null) await this.readThroughReady;
3001
3163
  if (!this.requireAuth()) {
3002
3164
  return err(authRequiredError("kv"));
3003
3165
  }
3004
3166
  const path = this.getFullPath(key, options?.prefix);
3005
3167
  const request = this.createRequestSignal(options?.signal, options?.timeout);
3006
3168
  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)
3169
+ const network = () => this.deleteFromNetwork(key, path, options, request.signal);
3170
+ if (this.readThrough === null) return await network();
3171
+ return await this.readThrough.write({
3172
+ op: "delete",
3173
+ space: this.context.session.spaceId,
3174
+ entries: [{ path }],
3175
+ signal: request.signal,
3176
+ conditional: options?.ifMatch !== void 0,
3177
+ network
3056
3178
  });
3057
3179
  } catch (error) {
3058
3180
  return err(wrapError("kv", error));
@@ -3061,6 +3183,54 @@ var KVService = class extends BaseService {
3061
3183
  }
3062
3184
  });
3063
3185
  }
3186
+ async deleteFromNetwork(key, path, options, signal) {
3187
+ const response = await this.invokeOperation(
3188
+ path,
3189
+ KVAction.DELETE,
3190
+ void 0,
3191
+ signal,
3192
+ options?.ifMatch === void 0 ? void 0 : { "if-match": options.ifMatch }
3193
+ );
3194
+ if (!response.ok) {
3195
+ if (response.status === 401 || response.status === 403) {
3196
+ return this.authorizationFailure(
3197
+ `Failed to delete key ${JSON.stringify(key)}`,
3198
+ response,
3199
+ await this.readAuthorizationText(response),
3200
+ [path],
3201
+ KVAction.DELETE
3202
+ );
3203
+ }
3204
+ if (response.status === 404) return this.classifyNotFound(response, key);
3205
+ const errorText = await response.text();
3206
+ if (response.status === 412) {
3207
+ return err(serviceError(
3208
+ ErrorCodes.KV_PRECONDITION_FAILED,
3209
+ `KV precondition failed for key ${JSON.stringify(key)}`,
3210
+ "kv",
3211
+ { meta: { status: response.status, statusText: response.statusText } }
3212
+ ));
3213
+ }
3214
+ if (response.status === 503 && options?.ifMatch !== void 0) {
3215
+ return err(serviceError(
3216
+ ErrorCodes.KV_CONFLICT,
3217
+ `Concurrent KV delete conflicted for key ${JSON.stringify(key)}`,
3218
+ "kv",
3219
+ { meta: { status: response.status, statusText: response.statusText } }
3220
+ ));
3221
+ }
3222
+ return err(serviceError(
3223
+ ErrorCodes.NETWORK_ERROR,
3224
+ `Failed to delete key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
3225
+ "kv",
3226
+ { meta: { status: response.status, statusText: response.statusText } }
3227
+ ));
3228
+ }
3229
+ return ok({
3230
+ data: void 0,
3231
+ headers: this.createResponseHeaders(response.headers)
3232
+ });
3233
+ }
3064
3234
  /**
3065
3235
  * Get metadata for a key without retrieving the value.
3066
3236
  */
@@ -3228,6 +3398,64 @@ var KVService = class extends BaseService {
3228
3398
  */
3229
3399
  KVService.serviceName = "kv";
3230
3400
 
3401
+ // src/kv/replication/constants.ts
3402
+ 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.";
3403
+
3404
+ // src/kv/replication/identity.ts
3405
+ var SPACE_ID = /^(tinycloud:pkh:eip155:\d+:)([^:]+)(:.+)$/;
3406
+ var PRINCIPAL_DID = /^(did:pkh:eip155:\d+:)([^:]+)$/;
3407
+ function canonicalReplicationIdentity(raw) {
3408
+ if (!raw || typeof raw.host !== "string" || typeof raw.space !== "string" || typeof raw.principal !== "string") {
3409
+ throw new TypeError(
3410
+ "Replication identity host, space, and principal must be strings"
3411
+ );
3412
+ }
3413
+ const authority = /^https?:\/\/([^/?#]*)/i.exec(raw.host)?.[1];
3414
+ let url;
3415
+ try {
3416
+ url = new URL(raw.host);
3417
+ } catch {
3418
+ throw new TypeError(
3419
+ "Replication identity host must be an absolute HTTP(S) URL"
3420
+ );
3421
+ }
3422
+ if (!authority || authority.includes("@") || raw.host.includes("?") || raw.host.includes("#") || url.username || url.password) {
3423
+ throw new TypeError(
3424
+ "Replication identity host must be HTTP(S) without credentials, query, or fragment"
3425
+ );
3426
+ }
3427
+ const space = SPACE_ID.exec(raw.space);
3428
+ const principal = PRINCIPAL_DID.exec(raw.principal);
3429
+ if (!space || !principal) {
3430
+ throw new TypeError(
3431
+ "Replication identity requires a full TinyCloud space id and a pkh principal DID"
3432
+ );
3433
+ }
3434
+ return {
3435
+ host: `${url.origin}${url.pathname.replace(/\/+$/, "")}`,
3436
+ space: `${space[1]}${space[2].toLowerCase()}${space[3]}`,
3437
+ principal: `${principal[1]}${principal[2].toLowerCase()}`
3438
+ };
3439
+ }
3440
+ function replicationIdentityKey(id) {
3441
+ const canonical = canonicalReplicationIdentity(id);
3442
+ return `tc-replication/v1
3443
+ ${canonical.host}
3444
+ ${canonical.space}
3445
+ ${canonical.principal}`;
3446
+ }
3447
+
3448
+ // src/kv/replication/scope.ts
3449
+ function kvPrefixCovers(prefix, key) {
3450
+ if (prefix === "") return true;
3451
+ if (prefix.endsWith("/")) return key.startsWith(prefix);
3452
+ return key === prefix || key.startsWith(`${prefix}/`);
3453
+ }
3454
+ function requiresSecretsOptIn(space, prefix) {
3455
+ if (space === "secrets" || space.endsWith(":secrets")) return true;
3456
+ return prefix.split("/", 1)[0] === "vault";
3457
+ }
3458
+
3231
3459
  // src/responseErrors.ts
3232
3460
  function parseServiceErrorBody(errorText) {
3233
3461
  try {
@@ -3404,7 +3632,7 @@ var SQLService = class extends BaseService {
3404
3632
  request.signal
3405
3633
  );
3406
3634
  if (!response.ok) {
3407
- return this.handleErrorResponse(response, "query", dbName, [action]);
3635
+ return await this.handleErrorResponse(response, "query", dbName, [action]);
3408
3636
  }
3409
3637
  const data = await response.json();
3410
3638
  return ok(data);
@@ -3444,7 +3672,7 @@ var SQLService = class extends BaseService {
3444
3672
  request.signal
3445
3673
  );
3446
3674
  if (!response.ok) {
3447
- return this.handleErrorResponse(response, "execute", dbName, requestedActions);
3675
+ return await this.handleErrorResponse(response, "execute", dbName, requestedActions);
3448
3676
  }
3449
3677
  const data = await response.json();
3450
3678
  return ok(data);
@@ -3470,7 +3698,7 @@ var SQLService = class extends BaseService {
3470
3698
  request.signal
3471
3699
  );
3472
3700
  if (!response.ok) {
3473
- return this.handleErrorResponse(response, "batch", dbName, requestedActions);
3701
+ return await this.handleErrorResponse(response, "batch", dbName, requestedActions);
3474
3702
  }
3475
3703
  const data = await response.json();
3476
3704
  return ok(data);
@@ -3495,7 +3723,7 @@ var SQLService = class extends BaseService {
3495
3723
  request.signal
3496
3724
  );
3497
3725
  if (!response.ok) {
3498
- return this.handleErrorResponse(response, "executeStatement", dbName, [SQLAction.WRITE]);
3726
+ return await this.handleErrorResponse(response, "executeStatement", dbName, [SQLAction.WRITE]);
3499
3727
  }
3500
3728
  const data = await response.json();
3501
3729
  return ok(data);
@@ -3520,7 +3748,7 @@ var SQLService = class extends BaseService {
3520
3748
  request.signal
3521
3749
  );
3522
3750
  if (!response.ok) {
3523
- return this.handleErrorResponse(response, "export", dbName, [SQLAction.READ]);
3751
+ return await this.handleErrorResponse(response, "export", dbName, [SQLAction.READ]);
3524
3752
  }
3525
3753
  const resp = response;
3526
3754
  if (typeof resp.blob === "function") {
@@ -3896,7 +4124,7 @@ var DuckDbService = class extends BaseService {
3896
4124
  request.signal
3897
4125
  );
3898
4126
  if (!response.ok) {
3899
- return this.handleErrorResponse(response, "query");
4127
+ return await this.handleErrorResponse(response, "query");
3900
4128
  }
3901
4129
  const data = await response.json();
3902
4130
  return ok(data);
@@ -3922,7 +4150,7 @@ var DuckDbService = class extends BaseService {
3922
4150
  { Accept: "application/vnd.apache.arrow.stream" }
3923
4151
  );
3924
4152
  if (!response.ok) {
3925
- return this.handleErrorResponse(response, "queryArrow");
4153
+ return await this.handleErrorResponse(response, "queryArrow");
3926
4154
  }
3927
4155
  const buffer = await response.arrayBuffer();
3928
4156
  return ok(buffer);
@@ -3955,7 +4183,7 @@ var DuckDbService = class extends BaseService {
3955
4183
  request.signal
3956
4184
  );
3957
4185
  if (!response.ok) {
3958
- return this.handleErrorResponse(response, "execute");
4186
+ return await this.handleErrorResponse(response, "execute");
3959
4187
  }
3960
4188
  const data = await response.json();
3961
4189
  return ok(data);
@@ -3987,7 +4215,7 @@ var DuckDbService = class extends BaseService {
3987
4215
  request.signal
3988
4216
  );
3989
4217
  if (!response.ok) {
3990
- return this.handleErrorResponse(response, "batch");
4218
+ return await this.handleErrorResponse(response, "batch");
3991
4219
  }
3992
4220
  const data = await response.json();
3993
4221
  return ok(data);
@@ -4012,7 +4240,7 @@ var DuckDbService = class extends BaseService {
4012
4240
  request.signal
4013
4241
  );
4014
4242
  if (!response.ok) {
4015
- return this.handleErrorResponse(response, "executeStatement");
4243
+ return await this.handleErrorResponse(response, "executeStatement");
4016
4244
  }
4017
4245
  const data = await response.json();
4018
4246
  return ok(data);
@@ -4037,7 +4265,7 @@ var DuckDbService = class extends BaseService {
4037
4265
  request.signal
4038
4266
  );
4039
4267
  if (!response.ok) {
4040
- return this.handleErrorResponse(response, "describe");
4268
+ return await this.handleErrorResponse(response, "describe");
4041
4269
  }
4042
4270
  const data = await response.json();
4043
4271
  return ok(data);
@@ -4062,7 +4290,7 @@ var DuckDbService = class extends BaseService {
4062
4290
  request.signal
4063
4291
  );
4064
4292
  if (!response.ok) {
4065
- return this.handleErrorResponse(response, "export");
4293
+ return await this.handleErrorResponse(response, "export");
4066
4294
  }
4067
4295
  const blob = await response.blob();
4068
4296
  return ok(blob);
@@ -4097,7 +4325,7 @@ var DuckDbService = class extends BaseService {
4097
4325
  signal: request.signal
4098
4326
  });
4099
4327
  if (!response.ok) {
4100
- return this.handleErrorResponse(response, "import");
4328
+ return await this.handleErrorResponse(response, "import");
4101
4329
  }
4102
4330
  return ok(void 0);
4103
4331
  } catch (error) {
@@ -5809,7 +6037,8 @@ var DataVaultService = class extends BaseService {
5809
6037
  }
5810
6038
  try {
5811
6039
  const valueResult = await this.tc.kv.get(`vault/${key}`, {
5812
- raw: true
6040
+ raw: true,
6041
+ ...options?.source === void 0 ? {} : { source: options.source }
5813
6042
  });
5814
6043
  if (!valueResult.ok) {
5815
6044
  return vaultError({ code: "KEY_NOT_FOUND", key });
@@ -5854,7 +6083,7 @@ var DataVaultService = class extends BaseService {
5854
6083
  }
5855
6084
  let valueResult;
5856
6085
  try {
5857
- valueResult = await this.tc.kv.get(`vault/${key}`, { raw: true });
6086
+ valueResult = await this.tc.kv.get(`vault/${key}`, { raw: true, source: "network" });
5858
6087
  } catch {
5859
6088
  return { status: "node_unreachable" };
5860
6089
  }
@@ -5923,7 +6152,8 @@ var DataVaultService = class extends BaseService {
5923
6152
  }
5924
6153
  try {
5925
6154
  const valueResult = await this.tc.kv.get(`vault/${key}`, {
5926
- raw: true
6155
+ raw: true,
6156
+ source: "network"
5927
6157
  });
5928
6158
  if (!valueResult.ok) {
5929
6159
  return vaultError({ code: "KEY_NOT_FOUND", key });
@@ -6057,7 +6287,8 @@ var DataVaultService = class extends BaseService {
6057
6287
  }
6058
6288
  try {
6059
6289
  const keyResult = await this.tc.kv.get(`keys/${key}`, {
6060
- raw: true
6290
+ raw: true,
6291
+ ...options?.source === void 0 ? {} : { source: options.source }
6061
6292
  });
6062
6293
  if (!keyResult.ok) {
6063
6294
  return vaultError({ code: "KEY_NOT_FOUND", key });
@@ -6066,7 +6297,8 @@ var DataVaultService = class extends BaseService {
6066
6297
  const keyBlobBytes = base64Decode(keyEnvelope.key);
6067
6298
  const entryKey = this.crypto.decrypt(this.masterKey, keyBlobBytes);
6068
6299
  const valueResult = await this.tc.kv.get(`vault/${key}`, {
6069
- raw: true
6300
+ raw: true,
6301
+ ...options?.source === void 0 ? {} : { source: options.source }
6070
6302
  });
6071
6303
  if (!valueResult.ok) {
6072
6304
  return vaultError({ code: "KEY_NOT_FOUND", key });
@@ -6176,6 +6408,7 @@ var DataVaultService = class extends BaseService {
6176
6408
  prefix: listPrefix,
6177
6409
  removePrefix: true,
6178
6410
  ...options?.limit === void 0 ? {} : { limit: options.limit },
6411
+ ...options?.source === void 0 ? {} : { source: options.source },
6179
6412
  ...options?.cursor === void 0 ? {} : { cursor: options.cursor }
6180
6413
  });
6181
6414
  if (!listResult.ok) {
@@ -6900,7 +7133,9 @@ var SecretsService = class {
6900
7133
  async get(name, options) {
6901
7134
  const secretPath = resolveSecretPathResult(name, options);
6902
7135
  if ("ok" in secretPath) return secretPath;
6903
- const result = await this.vault.get(secretPath.vaultKey);
7136
+ const result = options?.source === void 0 ? await this.vault.get(secretPath.vaultKey) : await this.vault.get(secretPath.vaultKey, {
7137
+ source: options.source
7138
+ });
6904
7139
  if (!result.ok) {
6905
7140
  return result;
6906
7141
  }
@@ -6934,7 +7169,8 @@ var SecretsService = class {
6934
7169
  }
6935
7170
  const result = await this.vault.list({
6936
7171
  prefix,
6937
- removePrefix: true
7172
+ removePrefix: true,
7173
+ ...options?.source === void 0 ? {} : { source: options.source }
6938
7174
  });
6939
7175
  if (!result.ok) {
6940
7176
  return result;
@@ -6953,6 +7189,7 @@ var SecretsService = class {
6953
7189
  prefix: "secrets/",
6954
7190
  removePrefix: true,
6955
7191
  limit: 1e3,
7192
+ source: "network",
6956
7193
  ...cursor === void 0 ? {} : { cursor }
6957
7194
  });
6958
7195
  if (!result.ok) {
@@ -7945,6 +8182,7 @@ var import_decrypt_transport_response_error3 = require("@tinycloud/sdk-services/
7945
8182
  // Annotate the CommonJS export names for ESM import in node:
7946
8183
  0 && (module.exports = {
7947
8184
  BaseService,
8185
+ CLEAR_PENDING_WARNING,
7948
8186
  DECRYPT_ACTION,
7949
8187
  DECRYPT_FACT_TYPE,
7950
8188
  DECRYPT_RESULT_TYPE,
@@ -8012,6 +8250,7 @@ var import_decrypt_transport_response_error3 = require("@tinycloud/sdk-services/
8012
8250
  buildDecryptInvocation,
8013
8251
  buildNetworkId,
8014
8252
  canonicalHashHex,
8253
+ canonicalReplicationIdentity,
8015
8254
  canonicalSignedResponse,
8016
8255
  canonicalizeEncryptionJson,
8017
8256
  canonicalizeSecretScope,
@@ -8038,6 +8277,7 @@ var import_decrypt_transport_response_error3 = require("@tinycloud/sdk-services/
8038
8277
  installTinyCloudDebugGlobals,
8039
8278
  isNetworkId,
8040
8279
  isStorageFullError,
8280
+ kvPrefixCovers,
8041
8281
  networkDiscoveryKey,
8042
8282
  networkError,
8043
8283
  notFoundError,
@@ -8051,6 +8291,8 @@ var import_decrypt_transport_response_error3 = require("@tinycloud/sdk-services/
8051
8291
  parseSecretCatalogKey,
8052
8292
  parseStorageQuotaBytes,
8053
8293
  permissionDeniedError,
8294
+ replicationIdentityKey,
8295
+ requiresSecretsOptIn,
8054
8296
  resolveSecretListPrefix,
8055
8297
  resolveSecretPath,
8056
8298
  serviceError,