@tinycloud/sdk-services 3.1.0 → 3.2.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +683 -362
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.d.cts +81 -29
  12. package/dist/index.d.ts +81 -29
  13. package/dist/index.js +674 -362
  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 +532 -336
  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 +526 -335
  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,
@@ -47,11 +48,13 @@ __export(src_exports, {
47
48
  KVListResultSchema: () => KVListResultSchema,
48
49
  KVResponseHeadersSchema: () => KVResponseHeadersSchema,
49
50
  KVService: () => KVService,
51
+ MAX_SECRET_SCOPE_LENGTH: () => MAX_SECRET_SCOPE_LENGTH,
50
52
  NETWORK_NAME_PATTERN: () => NETWORK_NAME_PATTERN,
51
53
  NetworkIdError: () => NetworkIdError,
52
54
  PrefixedKVService: () => PrefixedKVService,
53
55
  RetryPolicySchema: () => RetryPolicySchema,
54
56
  SECRET_NAME_RE: () => SECRET_NAME_RE,
57
+ SECRET_SCOPE_RE: () => SECRET_SCOPE_RE,
55
58
  SQLAction: () => SQLAction,
56
59
  SQLMigrations: () => SQLMigrations,
57
60
  SQLService: () => SQLService,
@@ -65,6 +68,7 @@ __export(src_exports, {
65
68
  ServiceResponseEventSchema: () => ServiceResponseEventSchema,
66
69
  ServiceRetryEventSchema: () => ServiceRetryEventSchema,
67
70
  ServiceSessionSchema: () => ServiceSessionSchema,
71
+ TC832_RESERVED_SCOPES: () => TC832_RESERVED_SCOPES,
68
72
  TelemetryEvents: () => TelemetryEvents,
69
73
  TelemetrySpanEventSchema: () => TelemetrySpanEventSchema,
70
74
  TinyCloudDebugLogger: () => TinyCloudDebugLogger,
@@ -72,6 +76,7 @@ __export(src_exports, {
72
76
  VaultHeaders: () => VaultHeaders,
73
77
  VaultPublicSpaceKVActions: () => VaultPublicSpaceKVActions,
74
78
  abortedError: () => abortedError,
79
+ appSecretScope: () => appSecretScope,
75
80
  authExpiredError: () => authExpiredError,
76
81
  authRequiredError: () => authRequiredError,
77
82
  authUnauthorizedError: () => authUnauthorizedError,
@@ -84,6 +89,7 @@ __export(src_exports, {
84
89
  buildDecryptInvocation: () => buildDecryptInvocation,
85
90
  buildNetworkId: () => buildNetworkId,
86
91
  canonicalHashHex: () => canonicalHashHex,
92
+ canonicalReplicationIdentity: () => canonicalReplicationIdentity,
87
93
  canonicalSignedResponse: () => canonicalSignedResponse,
88
94
  canonicalizeEncryptionJson: () => canonicalize,
89
95
  canonicalizeSecretScope: () => canonicalizeSecretScope,
@@ -110,6 +116,7 @@ __export(src_exports, {
110
116
  installTinyCloudDebugGlobals: () => installTinyCloudDebugGlobals,
111
117
  isNetworkId: () => isNetworkId,
112
118
  isStorageFullError: () => isStorageFullError,
119
+ kvPrefixCovers: () => kvPrefixCovers,
113
120
  networkDiscoveryKey: () => networkDiscoveryKey,
114
121
  networkError: () => networkError,
115
122
  notFoundError: () => notFoundError,
@@ -123,6 +130,8 @@ __export(src_exports, {
123
130
  parseSecretCatalogKey: () => parseSecretCatalogKey,
124
131
  parseStorageQuotaBytes: () => parseStorageQuotaBytes,
125
132
  permissionDeniedError: () => permissionDeniedError,
133
+ replicationIdentityKey: () => replicationIdentityKey,
134
+ requiresSecretsOptIn: () => requiresSecretsOptIn,
126
135
  resolveSecretListPrefix: () => resolveSecretListPrefix,
127
136
  resolveSecretPath: () => resolveSecretPath,
128
137
  serviceError: () => serviceError,
@@ -210,7 +219,12 @@ var TelemetryEvents = {
210
219
  SERVICE_ERROR: "service.error",
211
220
  SERVICE_RETRY: "service.retry",
212
221
  SESSION_CHANGED: "session.changed",
213
- 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"
214
228
  };
215
229
  function ok(data) {
216
230
  return { ok: true, data };
@@ -236,9 +250,34 @@ var SAFE_NUMBER_FIELDS = /* @__PURE__ */ new Set([
236
250
  "endedAt",
237
251
  "startedAt",
238
252
  "status",
239
- "timestamp"
253
+ "timestamp",
254
+ "latencyMs",
255
+ "stalenessMs",
256
+ "count",
257
+ "changes",
258
+ "deleted",
259
+ "fetched",
260
+ "pages",
261
+ "lagMs",
262
+ "pendingCleared"
240
263
  ]);
241
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
+ };
242
281
  function finiteNumber(value) {
243
282
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
244
283
  }
@@ -275,6 +314,12 @@ function projectDiagnosticData(value) {
275
314
  const boolean = read(value, key);
276
315
  if (typeof boolean === "boolean") projected[key] = boolean;
277
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
+ }
278
323
  const url = read(value, "url");
279
324
  if (url !== void 0) projected.url = REDACTED;
280
325
  const error = read(value, "error");
@@ -899,6 +944,12 @@ var ServiceContext = class {
899
944
  get abortSignal() {
900
945
  return this._abortController.signal;
901
946
  }
947
+ get operationAbortSignal() {
948
+ return this._operationAbortSignal;
949
+ }
950
+ setOperationAbortSignal(signal) {
951
+ this._operationAbortSignal = signal;
952
+ }
902
953
  /**
903
954
  * Permanently retire this graph after its owner installs a replacement.
904
955
  * Unlike `abort()`, retirement never creates a fresh controller, so captured
@@ -1650,6 +1701,7 @@ var BaseService = class {
1650
1701
  const parents = [
1651
1702
  this.abortController.signal,
1652
1703
  this.context?.abortSignal,
1704
+ this.context?.operationAbortSignal,
1653
1705
  signal
1654
1706
  ].filter((parent) => parent !== void 0);
1655
1707
  for (const parent of parents) {
@@ -1895,14 +1947,28 @@ var KVService = class extends BaseService {
1895
1947
  */
1896
1948
  constructor(config = {}) {
1897
1949
  super();
1950
+ this.readThrough = null;
1951
+ this.readThroughReady = null;
1898
1952
  this._config = config;
1899
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
+ }
1900
1962
  /**
1901
1963
  * Get the service configuration.
1902
1964
  */
1903
1965
  get config() {
1904
1966
  return this._config;
1905
1967
  }
1968
+ /** Install or remove the optional replication read-through policy. */
1969
+ setReadThrough(readThrough) {
1970
+ this.readThrough = readThrough;
1971
+ }
1906
1972
  handleQuotaErrorResponse(response, errorText, key) {
1907
1973
  if (response.status !== 402 && response.status !== 413) {
1908
1974
  return void 0;
@@ -2156,6 +2222,7 @@ var KVService = class extends BaseService {
2156
2222
  return aligned;
2157
2223
  }
2158
2224
  async batchRead(keys, action, options) {
2225
+ if (this.readThroughReady !== null) await this.readThroughReady;
2159
2226
  if (!this.requireAuth()) return err(authRequiredError("kv"));
2160
2227
  if (keys.length === 0) return ok({ results: [], count: 0 });
2161
2228
  if (keys.length > MAX_KV_BATCH_READ_ITEMS) {
@@ -2406,6 +2473,7 @@ var KVService = class extends BaseService {
2406
2473
  */
2407
2474
  async get(key, options) {
2408
2475
  return this.withTelemetry("get", key, async () => {
2476
+ if (this.readThroughReady !== null) await this.readThroughReady;
2409
2477
  if (!this.requireAuth()) {
2410
2478
  return err(authRequiredError("kv"));
2411
2479
  }
@@ -2419,64 +2487,38 @@ var KVService = class extends BaseService {
2419
2487
  const path = this.getFullPath(key, options?.prefix);
2420
2488
  const request = this.createRequestSignal(options?.signal, options?.timeout);
2421
2489
  try {
2422
- const response = await this.invokeOperation(
2423
- path,
2424
- KVAction.GET,
2425
- void 0,
2426
- request.signal,
2427
- options?.maxResponseBytes === void 0 ? void 0 : { "x-tinycloud-max-response-bytes": String(options.maxResponseBytes) }
2428
- );
2429
- if (!response.ok) {
2430
- if (response.status === 401 || response.status === 403) {
2431
- const errorText2 = await this.readAuthorizationText(response);
2432
- const permissionHint = parsePermissionHintFromErrorText(errorText2);
2433
- const structuredCapability = permissionHint?.service === "tinycloud.kv" && permissionHint.space === this.context.session.spaceId && permissionHint.path === path ? validatedCapabilityOf({
2434
- service: "kv",
2435
- code: ErrorCodes.AUTH_UNAUTHORIZED,
2436
- meta: {
2437
- status: response.status,
2438
- resource: `${this.context.session.spaceId}/kv/${path}`,
2439
- requiredAction: KVAction.GET
2440
- }
2441
- }) : void 0;
2442
- return this.authorizationFailure(
2443
- `Failed to get key ${JSON.stringify(key)}`,
2444
- response,
2445
- errorText2,
2446
- [path],
2447
- KVAction.GET,
2448
- structuredCapability ? { permissionHint, ...structuredCapability } : {}
2449
- );
2450
- }
2451
- if (response.status === 404) {
2452
- return this.classifyNotFound(response, key);
2453
- }
2454
- const errorText = await response.text();
2455
- if (response.status === 413) {
2456
- return err(serviceError(
2457
- ErrorCodes.KV_RESPONSE_TOO_LARGE,
2458
- `KV value at key ${JSON.stringify(key)} exceeds the requested response limit`,
2459
- "kv",
2460
- { meta: { status: response.status, statusText: response.statusText } }
2461
- ));
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
+ }
2462
2512
  }
2463
- return err(
2464
- serviceError(
2465
- ErrorCodes.NETWORK_ERROR,
2466
- `Failed to get key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
2467
- "kv",
2468
- { meta: { status: response.status, statusText: response.statusText } }
2469
- )
2470
- );
2471
2513
  }
2472
- const data = await this.parseResponse(
2473
- response,
2474
- options?.raw,
2475
- options?.binary
2476
- );
2477
- return ok({
2478
- data,
2479
- 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
2480
2522
  });
2481
2523
  } catch (error) {
2482
2524
  return err(wrapError("kv", error));
@@ -2485,6 +2527,56 @@ var KVService = class extends BaseService {
2485
2527
  }
2486
2528
  });
2487
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
+ }
2488
2580
  async batchGet(keys, options) {
2489
2581
  return this.withTelemetry(
2490
2582
  "batchGet",
@@ -2497,6 +2589,7 @@ var KVService = class extends BaseService {
2497
2589
  */
2498
2590
  async put(key, value, options) {
2499
2591
  return this.withTelemetry("put", key, async () => {
2592
+ if (this.readThroughReady !== null) await this.readThroughReady;
2500
2593
  if (!this.requireAuth()) {
2501
2594
  return err(authRequiredError("kv"));
2502
2595
  }
@@ -2511,63 +2604,15 @@ var KVService = class extends BaseService {
2511
2604
  const body = this.serializePutValue(value, options?.contentType);
2512
2605
  const request = this.createRequestSignal(options?.signal, options?.timeout);
2513
2606
  try {
2514
- const response = await this.invokeOperation(
2515
- path,
2516
- KVAction.PUT,
2517
- body,
2518
- request.signal,
2519
- {
2520
- ...options?.ifMatch === void 0 ? {} : { "if-match": options.ifMatch },
2521
- ...options?.ifNoneMatch === void 0 ? {} : { "if-none-match": options.ifNoneMatch }
2522
- }
2523
- );
2524
- if (response.status === 401 || response.status === 403) {
2525
- return this.authorizationFailure(
2526
- `Failed to put key ${JSON.stringify(key)}`,
2527
- response,
2528
- await this.readAuthorizationText(response),
2529
- [path],
2530
- KVAction.PUT
2531
- );
2532
- }
2533
- if (!response.ok) {
2534
- const errorText = await response.text();
2535
- if (response.status === 412) {
2536
- return err(serviceError(
2537
- ErrorCodes.KV_PRECONDITION_FAILED,
2538
- `KV precondition failed for key ${JSON.stringify(key)}`,
2539
- "kv",
2540
- { meta: { status: response.status, statusText: response.statusText } }
2541
- ));
2542
- }
2543
- if (response.status === 503 && (options?.ifMatch !== void 0 || options?.ifNoneMatch !== void 0)) {
2544
- return err(serviceError(
2545
- ErrorCodes.KV_CONFLICT,
2546
- `Concurrent KV update conflicted for key ${JSON.stringify(key)}`,
2547
- "kv",
2548
- { meta: { status: response.status, statusText: response.statusText } }
2549
- ));
2550
- }
2551
- const quotaError = this.handleQuotaErrorResponse(
2552
- response,
2553
- errorText,
2554
- key
2555
- );
2556
- if (quotaError) {
2557
- return quotaError;
2558
- }
2559
- return err(
2560
- serviceError(
2561
- ErrorCodes.KV_WRITE_FAILED,
2562
- `Failed to put key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
2563
- "kv",
2564
- { meta: { status: response.status, statusText: response.statusText } }
2565
- )
2566
- );
2567
- }
2568
- return ok({
2569
- data: void 0,
2570
- 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
2571
2616
  });
2572
2617
  } catch (error) {
2573
2618
  return err(wrapError("kv", error));
@@ -2576,11 +2621,64 @@ var KVService = class extends BaseService {
2576
2621
  }
2577
2622
  });
2578
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
+ }
2579
2676
  /**
2580
2677
  * Store multiple values in one TinyCloud KV invocation.
2581
2678
  */
2582
2679
  async batchPut(items, options) {
2583
2680
  return this.withTelemetry("batchPut", String(items.length), async () => {
2681
+ if (this.readThroughReady !== null) await this.readThroughReady;
2584
2682
  if (!this.requireAuth()) {
2585
2683
  return err(authRequiredError("kv"));
2586
2684
  }
@@ -2611,155 +2709,177 @@ var KVService = class extends BaseService {
2611
2709
  }
2612
2710
  seen.add(path);
2613
2711
  }
2614
- let requestMayHaveDispatched = false;
2615
2712
  const request = this.createRequestSignal(options?.signal, options?.timeout);
2616
2713
  try {
2617
- const body = new FormData();
2618
- for (let index = 0; index < items.length; index++) {
2619
- body.append(
2620
- encodeKvBatchPartName(paths[index]),
2621
- this.serializeBatchPutValue(items[index])
2622
- );
2623
- }
2624
- const headers = this.context.invokeAny(
2625
- session,
2626
- paths.map((path) => ({
2627
- spaceId: session.spaceId,
2628
- service: "kv",
2629
- path,
2630
- action: KVAction.PUT
2631
- }))
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])
2632
2744
  );
2633
- const url = `${this.host}/invoke`;
2634
- const init = {
2635
- method: "POST",
2636
- headers,
2637
- body,
2638
- signal: request.signal
2639
- };
2640
- const fetchFn = this.context.fetch;
2641
- requestMayHaveDispatched = true;
2642
- const response = await fetchFn(url, init);
2643
- if (!response.ok) {
2644
- let errorText;
2645
- try {
2646
- errorText = await response.text();
2647
- } catch (textError) {
2648
- const cause = textError instanceof Error ? textError : new Error(String(textError));
2649
- return err(
2650
- serviceError(
2651
- ErrorCodes.KV_WRITE_FAILED,
2652
- `Failed to batch put ${items.length} key(s): ${response.status} - <response body could not be read: ${cause.message}>`,
2653
- "kv",
2654
- { cause, meta: { status: response.status, statusText: response.statusText } }
2655
- )
2656
- );
2657
- }
2658
- if (response.status === 401 || response.status === 403) {
2659
- return this.authorizationFailure(
2660
- `Failed to batch put ${items.length} key(s)`,
2661
- response,
2662
- errorText,
2663
- paths,
2664
- KVAction.PUT
2665
- );
2666
- }
2667
- const quotaError = this.handleQuotaErrorResponse(
2668
- response,
2669
- errorText,
2670
- "batch"
2671
- );
2672
- if (quotaError) {
2673
- return quotaError;
2674
- }
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));
2675
2771
  return err(
2676
2772
  serviceError(
2677
2773
  ErrorCodes.KV_WRITE_FAILED,
2678
- `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}>`,
2679
2775
  "kv",
2680
- { meta: { status: response.status, statusText: response.statusText } }
2776
+ { cause, meta: { status: response.status, statusText: response.statusText } }
2681
2777
  )
2682
2778
  );
2683
2779
  }
2684
- let rawBody;
2685
- try {
2686
- rawBody = await response.json();
2687
- } catch (jsonError) {
2688
- const cause = jsonError instanceof Error ? jsonError : new Error(String(jsonError));
2689
- const timedOut = isRequestTimeoutError(cause);
2690
- return err(
2691
- serviceError(
2692
- timedOut ? ErrorCodes.TIMEOUT : ErrorCodes.NETWORK_ERROR,
2693
- timedOut ? `KV batchPut response body did not arrive in time: ${cause.message}` : `KV batchPut response was not valid JSON: ${cause.message}`,
2694
- "kv",
2695
- {
2696
- cause,
2697
- meta: {
2698
- ...timedOut ? { timeoutMs: cause.timeoutMs } : {},
2699
- requestMayHaveDispatched: true,
2700
- responseReceived: true,
2701
- status: response.status,
2702
- outcome: "batch-unconfirmed"
2703
- }
2704
- }
2705
- )
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
2706
2787
  );
2707
2788
  }
2708
- const batchResponse = this.normalizeBatchPutResponse(rawBody);
2709
- if (!batchResponse) {
2710
- return err(
2711
- serviceError(
2712
- ErrorCodes.NETWORK_ERROR,
2713
- "KV batchPut response did not include matching written keys and count",
2714
- "kv",
2715
- {
2716
- meta: {
2717
- requestMayHaveDispatched: true,
2718
- responseReceived: true,
2719
- status: response.status,
2720
- outcome: "batch-unconfirmed"
2721
- }
2722
- }
2723
- )
2724
- );
2789
+ const quotaError = this.handleQuotaErrorResponse(
2790
+ response,
2791
+ errorText,
2792
+ "batch"
2793
+ );
2794
+ if (quotaError) {
2795
+ return quotaError;
2725
2796
  }
2726
- const requestedPaths = new Set(paths);
2727
- const writtenPaths = new Set(batchResponse.written);
2728
- const matchesRequest = batchResponse.count === paths.length && batchResponse.written.length === paths.length && writtenPaths.size === paths.length && batchResponse.written.every((key) => requestedPaths.has(key));
2729
- if (!matchesRequest) {
2730
- return err(
2731
- serviceError(
2732
- ErrorCodes.NETWORK_ERROR,
2733
- `KV batchPut response did not confirm all ${paths.length} requested key(s) were written`,
2734
- "kv",
2735
- {
2736
- meta: {
2737
- requestMayHaveDispatched: true,
2738
- responseReceived: true,
2739
- status: response.status,
2740
- outcome: "batch-unconfirmed"
2741
- }
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"
2742
2825
  }
2743
- )
2744
- );
2745
- }
2746
- return ok(batchResponse);
2747
- } catch (error) {
2748
- const wrapped = wrapError("kv", error);
2749
- return err({
2750
- ...wrapped,
2751
- meta: { ...wrapped.meta, requestMayHaveDispatched }
2752
- });
2753
- } finally {
2754
- request.dispose();
2826
+ }
2827
+ )
2828
+ );
2755
2829
  }
2756
- });
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
+ }
2757
2876
  }
2758
2877
  /**
2759
2878
  * List keys with optional prefix filtering.
2760
2879
  */
2761
2880
  async list(options) {
2762
2881
  return this.withTelemetry("list", options?.prefix, async () => {
2882
+ if (this.readThroughReady !== null) await this.readThroughReady;
2763
2883
  if (!this.requireAuth()) {
2764
2884
  return err(authRequiredError("kv"));
2765
2885
  }
@@ -2776,49 +2896,44 @@ var KVService = class extends BaseService {
2776
2896
  }
2777
2897
  const request = this.createRequestSignal(options?.signal, options?.timeout);
2778
2898
  try {
2779
- const response = await this.invokeOperation(
2780
- listPath,
2781
- KVAction.LIST,
2782
- void 0,
2783
- request.signal,
2784
- options?.limit === void 0 && options?.cursor === void 0 ? void 0 : {
2785
- ...options?.limit === void 0 ? {} : { "x-tinycloud-limit": String(options.limit) },
2786
- ...options?.cursor === void 0 ? {} : { "x-tinycloud-cursor": options.cursor }
2787
- }
2788
- );
2789
- if (!response.ok) {
2790
- if (response.status === 401 || response.status === 403) {
2791
- return this.authorizationFailure(
2792
- "Failed to list keys",
2793
- response,
2794
- await this.readAuthorizationText(response),
2795
- [listPath],
2796
- KVAction.LIST
2797
- );
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
+ }
2798
2924
  }
2799
- const errorText = await response.text();
2800
- return err(
2801
- serviceError(
2802
- ErrorCodes.NETWORK_ERROR,
2803
- `Failed to list keys: ${response.status} - ${errorText}`,
2804
- "kv",
2805
- { meta: { status: response.status, statusText: response.statusText } }
2806
- )
2807
- );
2808
- }
2809
- let keys = await this.parseResponse(response, options?.raw);
2810
- keys = keys ?? [];
2811
- if (options?.removePrefix && listPath) {
2812
- const prefixWithSlash = listPath.endsWith("/") ? listPath : `${listPath}/`;
2813
- keys = keys.map(
2814
- (key) => key.startsWith(prefixWithSlash) ? key.slice(prefixWithSlash.length) : key
2815
- );
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
+ });
2816
2935
  }
2817
- return ok({
2818
- keys,
2819
- ...response.headers.get("x-tinycloud-truncated") === null ? {} : { truncated: response.headers.get("x-tinycloud-truncated") === "true" },
2820
- ...response.headers.get("x-tinycloud-next-cursor") === null ? {} : { nextCursor: response.headers.get("x-tinycloud-next-cursor") ?? void 0 }
2821
- });
2936
+ return page.ok ? this.finishList(page.data, listPath, options?.removePrefix) : page;
2822
2937
  } catch (error) {
2823
2938
  return err(wrapError("kv", error));
2824
2939
  } finally {
@@ -2826,6 +2941,56 @@ var KVService = class extends BaseService {
2826
2941
  }
2827
2942
  });
2828
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
+ }
2829
2994
  /**
2830
2995
  * Read one page of the `tinycloud.kv/sync` change feed for a prefix.
2831
2996
  */
@@ -2994,61 +3159,22 @@ var KVService = class extends BaseService {
2994
3159
  */
2995
3160
  async delete(key, options) {
2996
3161
  return this.withTelemetry("delete", key, async () => {
3162
+ if (this.readThroughReady !== null) await this.readThroughReady;
2997
3163
  if (!this.requireAuth()) {
2998
3164
  return err(authRequiredError("kv"));
2999
3165
  }
3000
3166
  const path = this.getFullPath(key, options?.prefix);
3001
3167
  const request = this.createRequestSignal(options?.signal, options?.timeout);
3002
3168
  try {
3003
- const response = await this.invokeOperation(
3004
- path,
3005
- KVAction.DELETE,
3006
- void 0,
3007
- request.signal,
3008
- options?.ifMatch === void 0 ? void 0 : { "if-match": options.ifMatch }
3009
- );
3010
- if (!response.ok) {
3011
- if (response.status === 401 || response.status === 403) {
3012
- return this.authorizationFailure(
3013
- `Failed to delete key ${JSON.stringify(key)}`,
3014
- response,
3015
- await this.readAuthorizationText(response),
3016
- [path],
3017
- KVAction.DELETE
3018
- );
3019
- }
3020
- if (response.status === 404) {
3021
- return this.classifyNotFound(response, key);
3022
- }
3023
- const errorText = await response.text();
3024
- if (response.status === 412) {
3025
- return err(serviceError(
3026
- ErrorCodes.KV_PRECONDITION_FAILED,
3027
- `KV precondition failed for key ${JSON.stringify(key)}`,
3028
- "kv",
3029
- { meta: { status: response.status, statusText: response.statusText } }
3030
- ));
3031
- }
3032
- if (response.status === 503 && options?.ifMatch !== void 0) {
3033
- return err(serviceError(
3034
- ErrorCodes.KV_CONFLICT,
3035
- `Concurrent KV delete conflicted for key ${JSON.stringify(key)}`,
3036
- "kv",
3037
- { meta: { status: response.status, statusText: response.statusText } }
3038
- ));
3039
- }
3040
- return err(
3041
- serviceError(
3042
- ErrorCodes.NETWORK_ERROR,
3043
- `Failed to delete key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
3044
- "kv",
3045
- { meta: { status: response.status, statusText: response.statusText } }
3046
- )
3047
- );
3048
- }
3049
- return ok({
3050
- data: void 0,
3051
- 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
3052
3178
  });
3053
3179
  } catch (error) {
3054
3180
  return err(wrapError("kv", error));
@@ -3057,6 +3183,54 @@ var KVService = class extends BaseService {
3057
3183
  }
3058
3184
  });
3059
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
+ }
3060
3234
  /**
3061
3235
  * Get metadata for a key without retrieving the value.
3062
3236
  */
@@ -3085,7 +3259,7 @@ var KVService = class extends BaseService {
3085
3259
  );
3086
3260
  }
3087
3261
  if (response.status === 404) {
3088
- return this.classifyNotFound(response, key);
3262
+ return await this.classifyNotFound(response, key);
3089
3263
  }
3090
3264
  const errorText = await response.text();
3091
3265
  return err(
@@ -3151,7 +3325,7 @@ var KVService = class extends BaseService {
3151
3325
  signal: request.signal
3152
3326
  });
3153
3327
  if (!response.ok) {
3154
- return this.createSignedReadUrlError(response, key, path);
3328
+ return await this.createSignedReadUrlError(response, key, path);
3155
3329
  }
3156
3330
  const signedUrl = this.normalizeSignedReadUrlResponse(
3157
3331
  await response.json()
@@ -3224,6 +3398,64 @@ var KVService = class extends BaseService {
3224
3398
  */
3225
3399
  KVService.serviceName = "kv";
3226
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
+
3227
3459
  // src/responseErrors.ts
3228
3460
  function parseServiceErrorBody(errorText) {
3229
3461
  try {
@@ -3400,7 +3632,7 @@ var SQLService = class extends BaseService {
3400
3632
  request.signal
3401
3633
  );
3402
3634
  if (!response.ok) {
3403
- return this.handleErrorResponse(response, "query", dbName, [action]);
3635
+ return await this.handleErrorResponse(response, "query", dbName, [action]);
3404
3636
  }
3405
3637
  const data = await response.json();
3406
3638
  return ok(data);
@@ -3440,7 +3672,7 @@ var SQLService = class extends BaseService {
3440
3672
  request.signal
3441
3673
  );
3442
3674
  if (!response.ok) {
3443
- return this.handleErrorResponse(response, "execute", dbName, requestedActions);
3675
+ return await this.handleErrorResponse(response, "execute", dbName, requestedActions);
3444
3676
  }
3445
3677
  const data = await response.json();
3446
3678
  return ok(data);
@@ -3466,7 +3698,7 @@ var SQLService = class extends BaseService {
3466
3698
  request.signal
3467
3699
  );
3468
3700
  if (!response.ok) {
3469
- return this.handleErrorResponse(response, "batch", dbName, requestedActions);
3701
+ return await this.handleErrorResponse(response, "batch", dbName, requestedActions);
3470
3702
  }
3471
3703
  const data = await response.json();
3472
3704
  return ok(data);
@@ -3491,7 +3723,7 @@ var SQLService = class extends BaseService {
3491
3723
  request.signal
3492
3724
  );
3493
3725
  if (!response.ok) {
3494
- return this.handleErrorResponse(response, "executeStatement", dbName, [SQLAction.WRITE]);
3726
+ return await this.handleErrorResponse(response, "executeStatement", dbName, [SQLAction.WRITE]);
3495
3727
  }
3496
3728
  const data = await response.json();
3497
3729
  return ok(data);
@@ -3516,7 +3748,7 @@ var SQLService = class extends BaseService {
3516
3748
  request.signal
3517
3749
  );
3518
3750
  if (!response.ok) {
3519
- return this.handleErrorResponse(response, "export", dbName, [SQLAction.READ]);
3751
+ return await this.handleErrorResponse(response, "export", dbName, [SQLAction.READ]);
3520
3752
  }
3521
3753
  const resp = response;
3522
3754
  if (typeof resp.blob === "function") {
@@ -3892,7 +4124,7 @@ var DuckDbService = class extends BaseService {
3892
4124
  request.signal
3893
4125
  );
3894
4126
  if (!response.ok) {
3895
- return this.handleErrorResponse(response, "query");
4127
+ return await this.handleErrorResponse(response, "query");
3896
4128
  }
3897
4129
  const data = await response.json();
3898
4130
  return ok(data);
@@ -3918,7 +4150,7 @@ var DuckDbService = class extends BaseService {
3918
4150
  { Accept: "application/vnd.apache.arrow.stream" }
3919
4151
  );
3920
4152
  if (!response.ok) {
3921
- return this.handleErrorResponse(response, "queryArrow");
4153
+ return await this.handleErrorResponse(response, "queryArrow");
3922
4154
  }
3923
4155
  const buffer = await response.arrayBuffer();
3924
4156
  return ok(buffer);
@@ -3951,7 +4183,7 @@ var DuckDbService = class extends BaseService {
3951
4183
  request.signal
3952
4184
  );
3953
4185
  if (!response.ok) {
3954
- return this.handleErrorResponse(response, "execute");
4186
+ return await this.handleErrorResponse(response, "execute");
3955
4187
  }
3956
4188
  const data = await response.json();
3957
4189
  return ok(data);
@@ -3983,7 +4215,7 @@ var DuckDbService = class extends BaseService {
3983
4215
  request.signal
3984
4216
  );
3985
4217
  if (!response.ok) {
3986
- return this.handleErrorResponse(response, "batch");
4218
+ return await this.handleErrorResponse(response, "batch");
3987
4219
  }
3988
4220
  const data = await response.json();
3989
4221
  return ok(data);
@@ -4008,7 +4240,7 @@ var DuckDbService = class extends BaseService {
4008
4240
  request.signal
4009
4241
  );
4010
4242
  if (!response.ok) {
4011
- return this.handleErrorResponse(response, "executeStatement");
4243
+ return await this.handleErrorResponse(response, "executeStatement");
4012
4244
  }
4013
4245
  const data = await response.json();
4014
4246
  return ok(data);
@@ -4033,7 +4265,7 @@ var DuckDbService = class extends BaseService {
4033
4265
  request.signal
4034
4266
  );
4035
4267
  if (!response.ok) {
4036
- return this.handleErrorResponse(response, "describe");
4268
+ return await this.handleErrorResponse(response, "describe");
4037
4269
  }
4038
4270
  const data = await response.json();
4039
4271
  return ok(data);
@@ -4058,7 +4290,7 @@ var DuckDbService = class extends BaseService {
4058
4290
  request.signal
4059
4291
  );
4060
4292
  if (!response.ok) {
4061
- return this.handleErrorResponse(response, "export");
4293
+ return await this.handleErrorResponse(response, "export");
4062
4294
  }
4063
4295
  const blob = await response.blob();
4064
4296
  return ok(blob);
@@ -4093,7 +4325,7 @@ var DuckDbService = class extends BaseService {
4093
4325
  signal: request.signal
4094
4326
  });
4095
4327
  if (!response.ok) {
4096
- return this.handleErrorResponse(response, "import");
4328
+ return await this.handleErrorResponse(response, "import");
4097
4329
  }
4098
4330
  return ok(void 0);
4099
4331
  } catch (error) {
@@ -5442,6 +5674,12 @@ function defaultVaultMessage(input) {
5442
5674
  return input.message ?? `Public key not found for ${input.did}`;
5443
5675
  case "STORAGE_ERROR":
5444
5676
  return input.message ?? input.cause.message;
5677
+ case "ALREADY_EXISTS":
5678
+ return input.message ?? `A value already exists at ${input.key}`;
5679
+ case "AUTH_UNAUTHORIZED":
5680
+ return input.message;
5681
+ case "PERMISSION_DENIED":
5682
+ return input.message;
5445
5683
  case "STORAGE_QUOTA_EXCEEDED":
5446
5684
  case "STORAGE_LIMIT_REACHED":
5447
5685
  return input.message;
@@ -5455,13 +5693,33 @@ function vaultError(input) {
5455
5693
  };
5456
5694
  return { ok: false, error };
5457
5695
  }
5696
+ function typedKvCause(error) {
5697
+ return Object.assign(new Error(error.message), {
5698
+ name: "ServiceError",
5699
+ code: error.code,
5700
+ service: error.service,
5701
+ ...error.meta !== void 0 ? { meta: error.meta } : {},
5702
+ ...error.cause !== void 0 ? { cause: error.cause } : {}
5703
+ });
5704
+ }
5458
5705
  function kvWriteError(context, error) {
5459
5706
  if (error.code === ErrorCodes.STORAGE_QUOTA_EXCEEDED || error.code === ErrorCodes.STORAGE_LIMIT_REACHED) {
5460
5707
  return vaultError({ code: error.code, message: error.message, meta: error.meta });
5461
5708
  }
5709
+ const message = `${context}: ${error.message}`;
5710
+ if (error.code === ErrorCodes.AUTH_UNAUTHORIZED) {
5711
+ const code = authorizationVerdictOf(error) === "unauthenticated" ? "AUTH_UNAUTHORIZED" : "PERMISSION_DENIED";
5712
+ return vaultError({
5713
+ code,
5714
+ message,
5715
+ ...error.meta !== void 0 ? { meta: error.meta } : {},
5716
+ cause: typedKvCause(error)
5717
+ });
5718
+ }
5462
5719
  return vaultError({
5463
5720
  code: "STORAGE_ERROR",
5464
- cause: new Error(`${context}: ${error.message}`)
5721
+ cause: new Error(message, { cause: typedKvCause(error) }),
5722
+ ...error.meta !== void 0 ? { meta: error.meta } : {}
5465
5723
  });
5466
5724
  }
5467
5725
  var DataVaultService = class extends BaseService {
@@ -5741,11 +5999,18 @@ var DataVaultService = class extends BaseService {
5741
5999
  cause: new Error(envelopeResult.error.message)
5742
6000
  });
5743
6001
  }
5744
- const valuePutResult = await this.tc.kv.put(
5745
- `vault/${key}`,
5746
- JSON.stringify(envelopeResult.data)
5747
- );
6002
+ const valuePath = `vault/${key}`;
6003
+ const valueBody = JSON.stringify(envelopeResult.data);
6004
+ const valuePutResult = options?.ifNoneMatch !== void 0 ? await this.tc.kv.put(valuePath, valueBody, { ifNoneMatch: options.ifNoneMatch }) : await this.tc.kv.put(valuePath, valueBody);
5748
6005
  if (!valuePutResult.ok) {
6006
+ if (options?.ifNoneMatch !== void 0 && valuePutResult.error.code === ErrorCodes.KV_PRECONDITION_FAILED) {
6007
+ return vaultError({
6008
+ code: "ALREADY_EXISTS",
6009
+ key,
6010
+ ...valuePutResult.error.meta !== void 0 ? { meta: valuePutResult.error.meta } : {},
6011
+ cause: typedKvCause(valuePutResult.error)
6012
+ });
6013
+ }
5749
6014
  return kvWriteError("Failed to store encrypted value", valuePutResult.error);
5750
6015
  }
5751
6016
  return { ok: true, data: void 0 };
@@ -5772,7 +6037,8 @@ var DataVaultService = class extends BaseService {
5772
6037
  }
5773
6038
  try {
5774
6039
  const valueResult = await this.tc.kv.get(`vault/${key}`, {
5775
- raw: true
6040
+ raw: true,
6041
+ ...options?.source === void 0 ? {} : { source: options.source }
5776
6042
  });
5777
6043
  if (!valueResult.ok) {
5778
6044
  return vaultError({ code: "KEY_NOT_FOUND", key });
@@ -5817,7 +6083,7 @@ var DataVaultService = class extends BaseService {
5817
6083
  }
5818
6084
  let valueResult;
5819
6085
  try {
5820
- valueResult = await this.tc.kv.get(`vault/${key}`, { raw: true });
6086
+ valueResult = await this.tc.kv.get(`vault/${key}`, { raw: true, source: "network" });
5821
6087
  } catch {
5822
6088
  return { status: "node_unreachable" };
5823
6089
  }
@@ -5886,7 +6152,8 @@ var DataVaultService = class extends BaseService {
5886
6152
  }
5887
6153
  try {
5888
6154
  const valueResult = await this.tc.kv.get(`vault/${key}`, {
5889
- raw: true
6155
+ raw: true,
6156
+ source: "network"
5890
6157
  });
5891
6158
  if (!valueResult.ok) {
5892
6159
  return vaultError({ code: "KEY_NOT_FOUND", key });
@@ -5913,6 +6180,14 @@ var DataVaultService = class extends BaseService {
5913
6180
  if (this.usesNetworkEncryption) {
5914
6181
  return this.putNetworkEncrypted(key, value, options);
5915
6182
  }
6183
+ if (options?.ifNoneMatch !== void 0) {
6184
+ return vaultError({
6185
+ code: "STORAGE_ERROR",
6186
+ cause: new Error(
6187
+ "ifNoneMatch is only supported by network-encrypted vaults"
6188
+ )
6189
+ });
6190
+ }
5916
6191
  if (!this._isUnlocked || !this.masterKey) {
5917
6192
  return vaultError({
5918
6193
  code: "VAULT_LOCKED",
@@ -6012,7 +6287,8 @@ var DataVaultService = class extends BaseService {
6012
6287
  }
6013
6288
  try {
6014
6289
  const keyResult = await this.tc.kv.get(`keys/${key}`, {
6015
- raw: true
6290
+ raw: true,
6291
+ ...options?.source === void 0 ? {} : { source: options.source }
6016
6292
  });
6017
6293
  if (!keyResult.ok) {
6018
6294
  return vaultError({ code: "KEY_NOT_FOUND", key });
@@ -6021,7 +6297,8 @@ var DataVaultService = class extends BaseService {
6021
6297
  const keyBlobBytes = base64Decode(keyEnvelope.key);
6022
6298
  const entryKey = this.crypto.decrypt(this.masterKey, keyBlobBytes);
6023
6299
  const valueResult = await this.tc.kv.get(`vault/${key}`, {
6024
- raw: true
6300
+ raw: true,
6301
+ ...options?.source === void 0 ? {} : { source: options.source }
6025
6302
  });
6026
6303
  if (!valueResult.ok) {
6027
6304
  return vaultError({ code: "KEY_NOT_FOUND", key });
@@ -6131,6 +6408,7 @@ var DataVaultService = class extends BaseService {
6131
6408
  prefix: listPrefix,
6132
6409
  removePrefix: true,
6133
6410
  ...options?.limit === void 0 ? {} : { limit: options.limit },
6411
+ ...options?.source === void 0 ? {} : { source: options.source },
6134
6412
  ...options?.cursor === void 0 ? {} : { cursor: options.cursor }
6135
6413
  });
6136
6414
  if (!listResult.ok) {
@@ -6724,6 +7002,35 @@ var SECRET_NAME_RE = /^[A-Z][A-Z0-9_]*$/;
6724
7002
  var SECRET_PREFIX = "secrets/";
6725
7003
  var SCOPED_SECRET_PREFIX = "secrets/scoped/";
6726
7004
  var RESERVED_SECRET_SCOPES = /* @__PURE__ */ new Set(["default", "global"]);
7005
+ var SECRET_SCOPE_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
7006
+ var MAX_SECRET_SCOPE_LENGTH = 64;
7007
+ var TC832_RESERVED_SCOPES = Object.freeze([
7008
+ "default",
7009
+ "global",
7010
+ "scoped"
7011
+ ]);
7012
+ function appSecretScope(appId) {
7013
+ if (appId.trim() === "") {
7014
+ throw new Error("App id must be non-empty to derive a secret scope.");
7015
+ }
7016
+ const scope = appId.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
7017
+ if (scope === "") {
7018
+ throw new Error(
7019
+ `App id ${JSON.stringify(appId)} must contain at least one letter or number.`
7020
+ );
7021
+ }
7022
+ if (scope.length > MAX_SECRET_SCOPE_LENGTH) {
7023
+ throw new Error(
7024
+ `App id ${JSON.stringify(appId)} yields a secret scope longer than ${MAX_SECRET_SCOPE_LENGTH} characters.`
7025
+ );
7026
+ }
7027
+ if (TC832_RESERVED_SCOPES.includes(scope)) {
7028
+ throw new Error(
7029
+ `App id ${JSON.stringify(appId)} yields the reserved secret scope ${JSON.stringify(scope)}.`
7030
+ );
7031
+ }
7032
+ return scope;
7033
+ }
6727
7034
  function canonicalizeSecretScope(scope) {
6728
7035
  if (scope === void 0) {
6729
7036
  return void 0;
@@ -6826,7 +7133,9 @@ var SecretsService = class {
6826
7133
  async get(name, options) {
6827
7134
  const secretPath = resolveSecretPathResult(name, options);
6828
7135
  if ("ok" in secretPath) return secretPath;
6829
- 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
+ });
6830
7139
  if (!result.ok) {
6831
7140
  return result;
6832
7141
  }
@@ -6836,11 +7145,12 @@ var SecretsService = class {
6836
7145
  const secretPath = resolveSecretPathResult(name, options);
6837
7146
  if ("ok" in secretPath) return secretPath;
6838
7147
  const now = (/* @__PURE__ */ new Date()).toISOString();
6839
- return this.vault.put(secretPath.vaultKey, {
7148
+ const payload = {
6840
7149
  value,
6841
7150
  createdAt: now,
6842
7151
  updatedAt: now
6843
- });
7152
+ };
7153
+ return options?.ifAbsent === true ? this.vault.put(secretPath.vaultKey, payload, { ifNoneMatch: "*" }) : this.vault.put(secretPath.vaultKey, payload);
6844
7154
  }
6845
7155
  async delete(name, options) {
6846
7156
  const secretPath = resolveSecretPathResult(name, options);
@@ -6859,7 +7169,8 @@ var SecretsService = class {
6859
7169
  }
6860
7170
  const result = await this.vault.list({
6861
7171
  prefix,
6862
- removePrefix: true
7172
+ removePrefix: true,
7173
+ ...options?.source === void 0 ? {} : { source: options.source }
6863
7174
  });
6864
7175
  if (!result.ok) {
6865
7176
  return result;
@@ -6878,6 +7189,7 @@ var SecretsService = class {
6878
7189
  prefix: "secrets/",
6879
7190
  removePrefix: true,
6880
7191
  limit: 1e3,
7192
+ source: "network",
6881
7193
  ...cursor === void 0 ? {} : { cursor }
6882
7194
  });
6883
7195
  if (!result.ok) {
@@ -7870,6 +8182,7 @@ var import_decrypt_transport_response_error3 = require("@tinycloud/sdk-services/
7870
8182
  // Annotate the CommonJS export names for ESM import in node:
7871
8183
  0 && (module.exports = {
7872
8184
  BaseService,
8185
+ CLEAR_PENDING_WARNING,
7873
8186
  DECRYPT_ACTION,
7874
8187
  DECRYPT_FACT_TYPE,
7875
8188
  DECRYPT_RESULT_TYPE,
@@ -7896,11 +8209,13 @@ var import_decrypt_transport_response_error3 = require("@tinycloud/sdk-services/
7896
8209
  KVListResultSchema,
7897
8210
  KVResponseHeadersSchema,
7898
8211
  KVService,
8212
+ MAX_SECRET_SCOPE_LENGTH,
7899
8213
  NETWORK_NAME_PATTERN,
7900
8214
  NetworkIdError,
7901
8215
  PrefixedKVService,
7902
8216
  RetryPolicySchema,
7903
8217
  SECRET_NAME_RE,
8218
+ SECRET_SCOPE_RE,
7904
8219
  SQLAction,
7905
8220
  SQLMigrations,
7906
8221
  SQLService,
@@ -7914,6 +8229,7 @@ var import_decrypt_transport_response_error3 = require("@tinycloud/sdk-services/
7914
8229
  ServiceResponseEventSchema,
7915
8230
  ServiceRetryEventSchema,
7916
8231
  ServiceSessionSchema,
8232
+ TC832_RESERVED_SCOPES,
7917
8233
  TelemetryEvents,
7918
8234
  TelemetrySpanEventSchema,
7919
8235
  TinyCloudDebugLogger,
@@ -7921,6 +8237,7 @@ var import_decrypt_transport_response_error3 = require("@tinycloud/sdk-services/
7921
8237
  VaultHeaders,
7922
8238
  VaultPublicSpaceKVActions,
7923
8239
  abortedError,
8240
+ appSecretScope,
7924
8241
  authExpiredError,
7925
8242
  authRequiredError,
7926
8243
  authUnauthorizedError,
@@ -7933,6 +8250,7 @@ var import_decrypt_transport_response_error3 = require("@tinycloud/sdk-services/
7933
8250
  buildDecryptInvocation,
7934
8251
  buildNetworkId,
7935
8252
  canonicalHashHex,
8253
+ canonicalReplicationIdentity,
7936
8254
  canonicalSignedResponse,
7937
8255
  canonicalizeEncryptionJson,
7938
8256
  canonicalizeSecretScope,
@@ -7959,6 +8277,7 @@ var import_decrypt_transport_response_error3 = require("@tinycloud/sdk-services/
7959
8277
  installTinyCloudDebugGlobals,
7960
8278
  isNetworkId,
7961
8279
  isStorageFullError,
8280
+ kvPrefixCovers,
7962
8281
  networkDiscoveryKey,
7963
8282
  networkError,
7964
8283
  notFoundError,
@@ -7972,6 +8291,8 @@ var import_decrypt_transport_response_error3 = require("@tinycloud/sdk-services/
7972
8291
  parseSecretCatalogKey,
7973
8292
  parseStorageQuotaBytes,
7974
8293
  permissionDeniedError,
8294
+ replicationIdentityKey,
8295
+ requiresSecretsOptIn,
7975
8296
  resolveSecretListPrefix,
7976
8297
  resolveSecretPath,
7977
8298
  serviceError,