@tinycloud/cli 1.2.0-beta.2 → 1.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.
@@ -131,6 +131,114 @@ var init_storage = __esm({
131
131
  }
132
132
  });
133
133
 
134
+ // src/lib/replication-registry.ts
135
+ function stopForSignal(signal) {
136
+ void closeReplication().finally(() => {
137
+ process.exit(signal === "SIGINT" ? 130 : 143);
138
+ });
139
+ }
140
+ function installSignalHandlers() {
141
+ if (state.installedSignalHandlers) return;
142
+ state.installedSignalHandlers = true;
143
+ state.sigintHandler = () => stopForSignal("SIGINT");
144
+ state.sigtermHandler = () => stopForSignal("SIGTERM");
145
+ process.once("SIGINT", state.sigintHandler);
146
+ process.once("SIGTERM", state.sigtermHandler);
147
+ }
148
+ function removeSignalHandlers() {
149
+ if (!state.installedSignalHandlers) return;
150
+ state.installedSignalHandlers = false;
151
+ if (state.sigintHandler) process.removeListener("SIGINT", state.sigintHandler);
152
+ if (state.sigtermHandler) process.removeListener("SIGTERM", state.sigtermHandler);
153
+ state.sigintHandler = void 0;
154
+ state.sigtermHandler = void 0;
155
+ }
156
+ function registerReplication(profile, control) {
157
+ state.controls.set(profile, control);
158
+ state.allControls.add(control);
159
+ state.generation += 1;
160
+ installSignalHandlers();
161
+ }
162
+ function replicationForProfile(profile) {
163
+ return state.controls.get(profile);
164
+ }
165
+ function registeredReplications() {
166
+ return state.controls;
167
+ }
168
+ function closeReplication(schedule = scheduleTimeout) {
169
+ if (state.closing) return state.closing;
170
+ removeSignalHandlers();
171
+ let resolveClosing;
172
+ let rejectClosing;
173
+ const closing = new Promise((resolve5, reject) => {
174
+ resolveClosing = resolve5;
175
+ rejectClosing = reject;
176
+ });
177
+ state.closing = closing;
178
+ void (async () => {
179
+ let timeout;
180
+ const deadline = new Promise((resolve5) => {
181
+ timeout = schedule(() => resolve5(true), CLOSE_TIMEOUT_MS);
182
+ });
183
+ let timedOut = false;
184
+ const abandonQueuedControls = () => {
185
+ for (const control of state.allControls) {
186
+ void Promise.resolve().then(() => control.close()).catch(() => void 0);
187
+ }
188
+ state.controls.clear();
189
+ state.allControls.clear();
190
+ };
191
+ try {
192
+ while (!timedOut) {
193
+ while (state.allControls.size > 0) {
194
+ const generation = state.generation;
195
+ const pending = [...state.allControls];
196
+ state.controls.clear();
197
+ state.allControls.clear();
198
+ const drained = Promise.allSettled(pending.map((control) => Promise.resolve().then(() => control.close()))).then(() => false);
199
+ timedOut ||= await Promise.race([drained, deadline]);
200
+ if (timedOut) {
201
+ abandonQueuedControls();
202
+ break;
203
+ }
204
+ if (state.generation === generation && state.allControls.size === 0) break;
205
+ }
206
+ if (timedOut) break;
207
+ const turn = new Promise((resolve5) => setImmediate(() => resolve5(false)));
208
+ timedOut ||= await Promise.race([turn, deadline]);
209
+ if (timedOut) abandonQueuedControls();
210
+ else if (state.allControls.size === 0) break;
211
+ }
212
+ state.closing = void 0;
213
+ if (state.allControls.size === 0) removeSignalHandlers();
214
+ resolveClosing(timedOut);
215
+ } catch (error) {
216
+ state.closing = void 0;
217
+ if (state.allControls.size === 0) removeSignalHandlers();
218
+ rejectClosing(error);
219
+ } finally {
220
+ clearTimeout(timeout);
221
+ }
222
+ })();
223
+ return closing;
224
+ }
225
+ var registryKey, globalState, state, CLOSE_TIMEOUT_MS, scheduleTimeout;
226
+ var init_replication_registry = __esm({
227
+ "src/lib/replication-registry.ts"() {
228
+ "use strict";
229
+ registryKey = /* @__PURE__ */ Symbol.for("@tinycloud/cli/replication-registry");
230
+ globalState = globalThis;
231
+ state = globalState[registryKey] ??= {
232
+ controls: /* @__PURE__ */ new Map(),
233
+ allControls: /* @__PURE__ */ new Set(),
234
+ installedSignalHandlers: false,
235
+ generation: 0
236
+ };
237
+ CLOSE_TIMEOUT_MS = 3e3;
238
+ scheduleTimeout = (callback, delayMs) => setTimeout(callback, delayMs);
239
+ }
240
+ });
241
+
134
242
  // src/output/theme.ts
135
243
  import chalk from "chalk";
136
244
  var TC_PALETTE, theme;
@@ -546,7 +654,8 @@ function handleError(error) {
546
654
  ...Object.keys(meta).length ? { meta } : {},
547
655
  warnings: mergeWarnings(drainWarnings(), operationWarnings(cliError.metadata?.warnings))
548
656
  });
549
- process.exit(cliError.exitCode);
657
+ if (registeredReplications().size === 0) return process.exit(cliError.exitCode);
658
+ return closeReplication().then(() => process.exit(cliError.exitCode));
550
659
  }
551
660
  function safeCliRecovery(value, context) {
552
661
  if (typeof value !== "object" || value === null || Array.isArray(value) || !("action" in value)) return void 0;
@@ -656,6 +765,7 @@ var activeProfileName, CLIError, SETUP_KEYS;
656
765
  var init_errors = __esm({
657
766
  "src/output/errors.ts"() {
658
767
  "use strict";
768
+ init_replication_registry();
659
769
  init_constants();
660
770
  init_formatter();
661
771
  init_storage2();
@@ -1214,11 +1324,11 @@ var init_md = __esm({
1214
1324
  if (len % 4)
1215
1325
  throw new Error("_sha2: outputLen should be aligned to 32bit");
1216
1326
  const outLen = len / 4;
1217
- const state = this.get();
1218
- if (outLen > state.length)
1327
+ const state2 = this.get();
1328
+ if (outLen > state2.length)
1219
1329
  throw new Error("_sha2: outputLen bigger than state");
1220
1330
  for (let i = 0; i < outLen; i++)
1221
- oview.setUint32(4 * i, state[i], isLE2);
1331
+ oview.setUint32(4 * i, state2[i], isLE2);
1222
1332
  }
1223
1333
  digest() {
1224
1334
  const { buffer, outputLen } = this;
@@ -6401,6 +6511,12 @@ function projectDiagnosticData(value) {
6401
6511
  const boolean = read(value, key);
6402
6512
  if (typeof boolean === "boolean") projected[key] = boolean;
6403
6513
  }
6514
+ for (const [key, allowed] of Object.entries(SAFE_REPLICATION_FIELDS)) {
6515
+ const candidate = read(value, key);
6516
+ if (typeof candidate === "string" && allowed.includes(candidate)) {
6517
+ projected[key] = candidate;
6518
+ }
6519
+ }
6404
6520
  const url = read(value, "url");
6405
6521
  if (url !== void 0) projected.url = REDACTED;
6406
6522
  const error = read(value, "error");
@@ -8322,7 +8438,7 @@ function encOk(data) {
8322
8438
  function encErr(error) {
8323
8439
  return { ok: false, error };
8324
8440
  }
8325
- var import_decrypt_transport_response_error, ErrorCodes, defaultRetryPolicy, TelemetryEvents, REDACTED, SAFE_NUMBER_FIELDS, SAFE_BOOLEAN_FIELDS, DEBUG_FLAG, MAX_EVENTS, TinyCloudDebugLogger, tinyCloudDebugLogger, ServiceErrorSchema, GenericResultSchema, KVResponseHeadersSchema, GenericKVResponseSchema, KVListResponseSchema, KVListResultSchema, ServiceRequestEventSchema, ServiceResponseEventSchema, ServiceErrorEventSchema, ServiceRetryEventSchema, TelemetrySpanEventSchema, RetryPolicySchema, ServiceSessionSchema, URN_PREFIX, NETWORK_NAME_RE, PKH_EIP155_DID_RE, NetworkIdError, RequestTimeoutError, grantableActions, capabilityServices, MAX_CAUSE_DEPTH2, STORAGE_FULL_MESSAGE, STORAGE_WRITE_TOO_LARGE_MESSAGE, MAX_TIMER_DELAY_MS, BaseService, PrefixedKVService, DEFAULT_SIGNED_READ_URL_EXPIRY_MS, KVAction, MAX_KV_BATCH_READ_ITEMS, pathByteEncoder, REVOKED_BODY, KVService, SQLMigrations, DatabaseHandle, SQLAction, DDL_TOKENS, MIGRATIONS_TABLE, MIGRATIONS_SCHEMA, MIGRATIONS_META_NAMESPACE, MIGRATIONS_META_ID, SQLService, DuckDbDatabaseHandle, DuckDbAction, DuckDbService, AsyncQueue, HOOK_STREAM_RETRY_BASE_DELAY_MS, HOOK_STREAM_RETRY_MAX_DELAY_MS, HOOK_STREAM_HEALTHY_MS, HooksService, SUPERSEDED, STREAM_ERROR_NAMES, STREAM_ERROR_CODES, VaultVersionConfig, CURRENT_VAULT_VERSION, VaultHeaders, DB_NAME, DB_VERSION, STORE_NAME, WRAP_KEY_ID, DataVaultService, SECRET_NAME_RE, SECRET_PREFIX, SCOPED_SECRET_PREFIX, RESERVED_SECRET_SCOPES, TC832_RESERVED_SCOPES, HEX, DEFAULT_ENCRYPTION_ALG, ENVELOPE_VERSION, DEFAULT_KEY_VERSION, DECRYPT_FACT_TYPE, DECRYPT_RESULT_TYPE, EncryptionService;
8441
+ var import_decrypt_transport_response_error, ErrorCodes, defaultRetryPolicy, TelemetryEvents, REDACTED, SAFE_NUMBER_FIELDS, SAFE_BOOLEAN_FIELDS, SAFE_REPLICATION_FIELDS, DEBUG_FLAG, MAX_EVENTS, TinyCloudDebugLogger, tinyCloudDebugLogger, ServiceErrorSchema, GenericResultSchema, KVResponseHeadersSchema, GenericKVResponseSchema, KVListResponseSchema, KVListResultSchema, ServiceRequestEventSchema, ServiceResponseEventSchema, ServiceErrorEventSchema, ServiceRetryEventSchema, TelemetrySpanEventSchema, RetryPolicySchema, ServiceSessionSchema, URN_PREFIX, NETWORK_NAME_RE, PKH_EIP155_DID_RE, NetworkIdError, RequestTimeoutError, grantableActions, capabilityServices, MAX_CAUSE_DEPTH2, STORAGE_FULL_MESSAGE, STORAGE_WRITE_TOO_LARGE_MESSAGE, MAX_TIMER_DELAY_MS, BaseService, PrefixedKVService, DEFAULT_SIGNED_READ_URL_EXPIRY_MS, KVAction, MAX_KV_BATCH_READ_ITEMS, pathByteEncoder, REVOKED_BODY, KVService, SQLMigrations, DatabaseHandle, SQLAction, DDL_TOKENS, MIGRATIONS_TABLE, MIGRATIONS_SCHEMA, MIGRATIONS_META_NAMESPACE, MIGRATIONS_META_ID, SQLService, DuckDbDatabaseHandle, DuckDbAction, DuckDbService, AsyncQueue, HOOK_STREAM_RETRY_BASE_DELAY_MS, HOOK_STREAM_RETRY_MAX_DELAY_MS, HOOK_STREAM_HEALTHY_MS, HooksService, SUPERSEDED, STREAM_ERROR_NAMES, STREAM_ERROR_CODES, VaultVersionConfig, CURRENT_VAULT_VERSION, VaultHeaders, DB_NAME, DB_VERSION, STORE_NAME, WRAP_KEY_ID, DataVaultService, SECRET_NAME_RE, SECRET_PREFIX, SCOPED_SECRET_PREFIX, RESERVED_SECRET_SCOPES, TC832_RESERVED_SCOPES, HEX, DEFAULT_ENCRYPTION_ALG, ENVELOPE_VERSION, DEFAULT_KEY_VERSION, DECRYPT_FACT_TYPE, DECRYPT_RESULT_TYPE, EncryptionService;
8326
8442
  var init_dist2 = __esm({
8327
8443
  "../sdk-services/dist/index.js"() {
8328
8444
  "use strict";
@@ -8392,7 +8508,12 @@ var init_dist2 = __esm({
8392
8508
  SERVICE_ERROR: "service.error",
8393
8509
  SERVICE_RETRY: "service.retry",
8394
8510
  SESSION_CHANGED: "session.changed",
8395
- SESSION_EXPIRED: "session.expired"
8511
+ SESSION_EXPIRED: "session.expired",
8512
+ REPLICATION_READ: "replication.read",
8513
+ REPLICATION_WRITE: "replication.write",
8514
+ REPLICATION_SYNC: "replication.sync",
8515
+ REPLICATION_STATE: "replication.state",
8516
+ REPLICATION_DIVERGENCE: "replication.divergence"
8396
8517
  };
8397
8518
  REDACTED = "[REDACTED]";
8398
8519
  SAFE_NUMBER_FIELDS = /* @__PURE__ */ new Set([
@@ -8401,9 +8522,34 @@ var init_dist2 = __esm({
8401
8522
  "endedAt",
8402
8523
  "startedAt",
8403
8524
  "status",
8404
- "timestamp"
8525
+ "timestamp",
8526
+ "latencyMs",
8527
+ "stalenessMs",
8528
+ "count",
8529
+ "changes",
8530
+ "deleted",
8531
+ "fetched",
8532
+ "pages",
8533
+ "lagMs",
8534
+ "pendingCleared"
8405
8535
  ]);
8406
8536
  SAFE_BOOLEAN_FIELDS = /* @__PURE__ */ new Set(["authenticated", "ok", "persisted"]);
8537
+ SAFE_REPLICATION_FIELDS = {
8538
+ source: ["replica", "network", "none"],
8539
+ 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"],
8540
+ op: ["get", "list", "put", "delete", "batchPut"],
8541
+ trigger: ["start", "interval", "stale_read", "manual"],
8542
+ outcome: ["found", "not_found", "error", "committed", "failed", "ambiguous", "ok", "busy", "aborted"],
8543
+ class: ["offline", "authority", "storage", "node"],
8544
+ state: ["opened", "grant_installed", "grant_missing", "runtime_unsupported", "unavailable", "revoked", "recreated", "purged", "purge_failed", "pending_store_error", "pinned", "pending_cleared", "closed", "idle", "ready"],
8545
+ kind: ["value", "missing_local", "extra_local", "keys", "order"],
8546
+ coverage: ["empty", "bootstrapping", "complete"],
8547
+ authority: ["valid", "expired", "revoked", "not-yet-valid"],
8548
+ 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"],
8549
+ pendingState: ["in_flight", "committed", "ambiguous"],
8550
+ strategy: ["session", "minted", "installed"],
8551
+ verify: ["match", "diverged", "error", "aborted"]
8552
+ };
8407
8553
  DEBUG_FLAG = "TinyCloud_debug";
8408
8554
  MAX_EVENTS = 1e3;
8409
8555
  TinyCloudDebugLogger = class {
@@ -8854,6 +9000,7 @@ var init_dist2 = __esm({
8854
9000
  const parents = [
8855
9001
  this.abortController.signal,
8856
9002
  this.context?.abortSignal,
9003
+ this.context?.operationAbortSignal,
8857
9004
  signal
8858
9005
  ].filter((parent) => parent !== void 0);
8859
9006
  for (const parent of parents) {
@@ -9068,14 +9215,28 @@ var init_dist2 = __esm({
9068
9215
  */
9069
9216
  constructor(config = {}) {
9070
9217
  super();
9218
+ this.readThrough = null;
9219
+ this.readThroughReady = null;
9071
9220
  this._config = config;
9072
9221
  }
9222
+ /** Delay KV routing until an asynchronously loaded read-through is attached. */
9223
+ setReadThroughReady(ready) {
9224
+ const settled = ready.then(() => void 0, () => void 0);
9225
+ this.readThroughReady = settled;
9226
+ void settled.then(() => {
9227
+ if (this.readThroughReady === settled) this.readThroughReady = null;
9228
+ });
9229
+ }
9073
9230
  /**
9074
9231
  * Get the service configuration.
9075
9232
  */
9076
9233
  get config() {
9077
9234
  return this._config;
9078
9235
  }
9236
+ /** Install or remove the optional replication read-through policy. */
9237
+ setReadThrough(readThrough) {
9238
+ this.readThrough = readThrough;
9239
+ }
9079
9240
  handleQuotaErrorResponse(response, errorText, key) {
9080
9241
  if (response.status !== 402 && response.status !== 413) {
9081
9242
  return void 0;
@@ -9329,6 +9490,7 @@ var init_dist2 = __esm({
9329
9490
  return aligned;
9330
9491
  }
9331
9492
  async batchRead(keys, action, options) {
9493
+ if (this.readThroughReady !== null) await this.readThroughReady;
9332
9494
  if (!this.requireAuth()) return err(authRequiredError("kv"));
9333
9495
  if (keys.length === 0) return ok({ results: [], count: 0 });
9334
9496
  if (keys.length > MAX_KV_BATCH_READ_ITEMS) {
@@ -9579,6 +9741,7 @@ var init_dist2 = __esm({
9579
9741
  */
9580
9742
  async get(key, options) {
9581
9743
  return this.withTelemetry("get", key, async () => {
9744
+ if (this.readThroughReady !== null) await this.readThroughReady;
9582
9745
  if (!this.requireAuth()) {
9583
9746
  return err(authRequiredError("kv"));
9584
9747
  }
@@ -9592,64 +9755,38 @@ var init_dist2 = __esm({
9592
9755
  const path = this.getFullPath(key, options?.prefix);
9593
9756
  const request = this.createRequestSignal(options?.signal, options?.timeout);
9594
9757
  try {
9595
- const response = await this.invokeOperation(
9596
- path,
9597
- KVAction.GET,
9598
- void 0,
9599
- request.signal,
9600
- options?.maxResponseBytes === void 0 ? void 0 : { "x-tinycloud-max-response-bytes": String(options.maxResponseBytes) }
9601
- );
9602
- if (!response.ok) {
9603
- if (response.status === 401 || response.status === 403) {
9604
- const errorText2 = await this.readAuthorizationText(response);
9605
- const permissionHint = parsePermissionHintFromErrorText(errorText2);
9606
- const structuredCapability = permissionHint?.service === "tinycloud.kv" && permissionHint.space === this.context.session.spaceId && permissionHint.path === path ? validatedCapabilityOf2({
9607
- service: "kv",
9608
- code: ErrorCodes.AUTH_UNAUTHORIZED,
9609
- meta: {
9610
- status: response.status,
9611
- resource: `${this.context.session.spaceId}/kv/${path}`,
9612
- requiredAction: KVAction.GET
9613
- }
9614
- }) : void 0;
9615
- return this.authorizationFailure(
9616
- `Failed to get key ${JSON.stringify(key)}`,
9617
- response,
9618
- errorText2,
9619
- [path],
9620
- KVAction.GET,
9621
- structuredCapability ? { permissionHint, ...structuredCapability } : {}
9622
- );
9623
- }
9624
- if (response.status === 404) {
9625
- return await this.classifyNotFound(response, key);
9626
- }
9627
- const errorText = await response.text();
9628
- if (response.status === 413) {
9629
- return err(serviceError(
9630
- ErrorCodes.KV_RESPONSE_TOO_LARGE,
9631
- `KV value at key ${JSON.stringify(key)} exceeds the requested response limit`,
9632
- "kv",
9633
- { meta: { status: response.status, statusText: response.statusText } }
9634
- ));
9758
+ const network = () => this.getFromNetwork(key, path, options, request.signal);
9759
+ if (options?.source === "network") {
9760
+ const observer = this.readThrough;
9761
+ if (observer === null) return await network();
9762
+ let outcome = "error";
9763
+ const startedAt = Date.now();
9764
+ try {
9765
+ const result = await network();
9766
+ outcome = result.ok ? "found" : result.error.code === ErrorCodes.KV_NOT_FOUND ? "not_found" : "error";
9767
+ return result;
9768
+ } finally {
9769
+ try {
9770
+ observer.observeNetworkRequested({
9771
+ op: "get",
9772
+ space: this.context.session.spaceId,
9773
+ path,
9774
+ reason: "NETWORK_REQUESTED",
9775
+ outcome,
9776
+ latencyMs: Date.now() - startedAt
9777
+ });
9778
+ } catch {
9779
+ }
9635
9780
  }
9636
- return err(
9637
- serviceError(
9638
- ErrorCodes.NETWORK_ERROR,
9639
- `Failed to get key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
9640
- "kv",
9641
- { meta: { status: response.status, statusText: response.statusText } }
9642
- )
9643
- );
9644
9781
  }
9645
- const data = await this.parseResponse(
9646
- response,
9647
- options?.raw,
9648
- options?.binary
9649
- );
9650
- return ok({
9651
- data,
9652
- headers: this.createResponseHeaders(response.headers)
9782
+ if (this.readThrough === null) return await network();
9783
+ return await this.readThrough.get({
9784
+ space: this.context.session.spaceId,
9785
+ key,
9786
+ path,
9787
+ options,
9788
+ signal: request.signal,
9789
+ network
9653
9790
  });
9654
9791
  } catch (error) {
9655
9792
  return err(wrapError2("kv", error));
@@ -9658,6 +9795,56 @@ var init_dist2 = __esm({
9658
9795
  }
9659
9796
  });
9660
9797
  }
9798
+ async getFromNetwork(key, path, options, signal) {
9799
+ const response = await this.invokeOperation(
9800
+ path,
9801
+ KVAction.GET,
9802
+ void 0,
9803
+ signal,
9804
+ options?.maxResponseBytes === void 0 ? void 0 : { "x-tinycloud-max-response-bytes": String(options.maxResponseBytes) }
9805
+ );
9806
+ if (!response.ok) {
9807
+ if (response.status === 401 || response.status === 403) {
9808
+ const errorText2 = await this.readAuthorizationText(response);
9809
+ const permissionHint = parsePermissionHintFromErrorText(errorText2);
9810
+ const structuredCapability = permissionHint?.service === "tinycloud.kv" && permissionHint.space === this.context.session.spaceId && permissionHint.path === path ? validatedCapabilityOf2({
9811
+ service: "kv",
9812
+ code: ErrorCodes.AUTH_UNAUTHORIZED,
9813
+ meta: {
9814
+ status: response.status,
9815
+ resource: `${this.context.session.spaceId}/kv/${path}`,
9816
+ requiredAction: KVAction.GET
9817
+ }
9818
+ }) : void 0;
9819
+ return this.authorizationFailure(
9820
+ `Failed to get key ${JSON.stringify(key)}`,
9821
+ response,
9822
+ errorText2,
9823
+ [path],
9824
+ KVAction.GET,
9825
+ structuredCapability ? { permissionHint, ...structuredCapability } : {}
9826
+ );
9827
+ }
9828
+ if (response.status === 404) return this.classifyNotFound(response, key);
9829
+ const errorText = await response.text();
9830
+ if (response.status === 413) {
9831
+ return err(serviceError(
9832
+ ErrorCodes.KV_RESPONSE_TOO_LARGE,
9833
+ `KV value at key ${JSON.stringify(key)} exceeds the requested response limit`,
9834
+ "kv",
9835
+ { meta: { status: response.status, statusText: response.statusText } }
9836
+ ));
9837
+ }
9838
+ return err(serviceError(
9839
+ ErrorCodes.NETWORK_ERROR,
9840
+ `Failed to get key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
9841
+ "kv",
9842
+ { meta: { status: response.status, statusText: response.statusText } }
9843
+ ));
9844
+ }
9845
+ const data = await this.parseResponse(response, options?.raw, options?.binary);
9846
+ return ok({ data, headers: this.createResponseHeaders(response.headers) });
9847
+ }
9661
9848
  async batchGet(keys, options) {
9662
9849
  return this.withTelemetry(
9663
9850
  "batchGet",
@@ -9670,6 +9857,7 @@ var init_dist2 = __esm({
9670
9857
  */
9671
9858
  async put(key, value, options) {
9672
9859
  return this.withTelemetry("put", key, async () => {
9860
+ if (this.readThroughReady !== null) await this.readThroughReady;
9673
9861
  if (!this.requireAuth()) {
9674
9862
  return err(authRequiredError("kv"));
9675
9863
  }
@@ -9684,63 +9872,15 @@ var init_dist2 = __esm({
9684
9872
  const body = this.serializePutValue(value, options?.contentType);
9685
9873
  const request = this.createRequestSignal(options?.signal, options?.timeout);
9686
9874
  try {
9687
- const response = await this.invokeOperation(
9688
- path,
9689
- KVAction.PUT,
9690
- body,
9691
- request.signal,
9692
- {
9693
- ...options?.ifMatch === void 0 ? {} : { "if-match": options.ifMatch },
9694
- ...options?.ifNoneMatch === void 0 ? {} : { "if-none-match": options.ifNoneMatch }
9695
- }
9696
- );
9697
- if (response.status === 401 || response.status === 403) {
9698
- return this.authorizationFailure(
9699
- `Failed to put key ${JSON.stringify(key)}`,
9700
- response,
9701
- await this.readAuthorizationText(response),
9702
- [path],
9703
- KVAction.PUT
9704
- );
9705
- }
9706
- if (!response.ok) {
9707
- const errorText = await response.text();
9708
- if (response.status === 412) {
9709
- return err(serviceError(
9710
- ErrorCodes.KV_PRECONDITION_FAILED,
9711
- `KV precondition failed for key ${JSON.stringify(key)}`,
9712
- "kv",
9713
- { meta: { status: response.status, statusText: response.statusText } }
9714
- ));
9715
- }
9716
- if (response.status === 503 && (options?.ifMatch !== void 0 || options?.ifNoneMatch !== void 0)) {
9717
- return err(serviceError(
9718
- ErrorCodes.KV_CONFLICT,
9719
- `Concurrent KV update conflicted for key ${JSON.stringify(key)}`,
9720
- "kv",
9721
- { meta: { status: response.status, statusText: response.statusText } }
9722
- ));
9723
- }
9724
- const quotaError = this.handleQuotaErrorResponse(
9725
- response,
9726
- errorText,
9727
- key
9728
- );
9729
- if (quotaError) {
9730
- return quotaError;
9731
- }
9732
- return err(
9733
- serviceError(
9734
- ErrorCodes.KV_WRITE_FAILED,
9735
- `Failed to put key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
9736
- "kv",
9737
- { meta: { status: response.status, statusText: response.statusText } }
9738
- )
9739
- );
9740
- }
9741
- return ok({
9742
- data: void 0,
9743
- headers: this.createResponseHeaders(response.headers)
9875
+ const network = () => this.putToNetwork(key, path, body, options, request.signal);
9876
+ if (this.readThrough === null) return await network();
9877
+ return await this.readThrough.write({
9878
+ op: "put",
9879
+ space: this.context.session.spaceId,
9880
+ entries: [{ path, body }],
9881
+ signal: request.signal,
9882
+ conditional: options?.ifMatch !== void 0 || options?.ifNoneMatch !== void 0,
9883
+ network
9744
9884
  });
9745
9885
  } catch (error) {
9746
9886
  return err(wrapError2("kv", error));
@@ -9749,11 +9889,64 @@ var init_dist2 = __esm({
9749
9889
  }
9750
9890
  });
9751
9891
  }
9892
+ async putToNetwork(key, path, body, options, signal) {
9893
+ const response = await this.invokeOperation(
9894
+ path,
9895
+ KVAction.PUT,
9896
+ body,
9897
+ signal,
9898
+ {
9899
+ ...options?.ifMatch === void 0 ? {} : { "if-match": options.ifMatch },
9900
+ ...options?.ifNoneMatch === void 0 ? {} : { "if-none-match": options.ifNoneMatch }
9901
+ }
9902
+ );
9903
+ if (response.status === 401 || response.status === 403) {
9904
+ return this.authorizationFailure(
9905
+ `Failed to put key ${JSON.stringify(key)}`,
9906
+ response,
9907
+ await this.readAuthorizationText(response),
9908
+ [path],
9909
+ KVAction.PUT
9910
+ );
9911
+ }
9912
+ if (!response.ok) {
9913
+ const errorText = await response.text();
9914
+ if (response.status === 412) {
9915
+ return err(serviceError(
9916
+ ErrorCodes.KV_PRECONDITION_FAILED,
9917
+ `KV precondition failed for key ${JSON.stringify(key)}`,
9918
+ "kv",
9919
+ { meta: { status: response.status, statusText: response.statusText } }
9920
+ ));
9921
+ }
9922
+ if (response.status === 503 && (options?.ifMatch !== void 0 || options?.ifNoneMatch !== void 0)) {
9923
+ return err(serviceError(
9924
+ ErrorCodes.KV_CONFLICT,
9925
+ `Concurrent KV update conflicted for key ${JSON.stringify(key)}`,
9926
+ "kv",
9927
+ { meta: { status: response.status, statusText: response.statusText } }
9928
+ ));
9929
+ }
9930
+ const quotaError = this.handleQuotaErrorResponse(response, errorText, key);
9931
+ if (quotaError) return quotaError;
9932
+ return err(serviceError(
9933
+ ErrorCodes.KV_WRITE_FAILED,
9934
+ `Failed to put key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
9935
+ "kv",
9936
+ { meta: { status: response.status, statusText: response.statusText } }
9937
+ ));
9938
+ }
9939
+ return ok({
9940
+ data: void 0,
9941
+ headers: this.createResponseHeaders(response.headers)
9942
+ });
9943
+ }
9752
9944
  /**
9753
9945
  * Store multiple values in one TinyCloud KV invocation.
9754
9946
  */
9755
9947
  async batchPut(items, options) {
9756
9948
  return this.withTelemetry("batchPut", String(items.length), async () => {
9949
+ if (this.readThroughReady !== null) await this.readThroughReady;
9757
9950
  if (!this.requireAuth()) {
9758
9951
  return err(authRequiredError("kv"));
9759
9952
  }
@@ -9784,155 +9977,177 @@ var init_dist2 = __esm({
9784
9977
  }
9785
9978
  seen.add(path);
9786
9979
  }
9787
- let requestMayHaveDispatched = false;
9788
9980
  const request = this.createRequestSignal(options?.signal, options?.timeout);
9789
9981
  try {
9790
- const body = new FormData();
9791
- for (let index = 0; index < items.length; index++) {
9792
- body.append(
9793
- encodeKvBatchPartName(paths[index]),
9794
- this.serializeBatchPutValue(items[index])
9795
- );
9796
- }
9797
- const headers = this.context.invokeAny(
9798
- session,
9799
- paths.map((path) => ({
9800
- spaceId: session.spaceId,
9801
- service: "kv",
9802
- path,
9803
- action: KVAction.PUT
9804
- }))
9982
+ const network = () => this.batchPutToNetwork(items, paths, request.signal);
9983
+ if (this.readThrough === null) return await network();
9984
+ const entries = items.map((item, index) => ({
9985
+ path: paths[index],
9986
+ body: this.serializeBatchPutValue(item)
9987
+ }));
9988
+ return await this.readThrough.write({
9989
+ op: "batchPut",
9990
+ space: session.spaceId,
9991
+ entries,
9992
+ signal: request.signal,
9993
+ network: () => this.batchPutToNetwork(items, paths, request.signal, entries)
9994
+ });
9995
+ } catch (error) {
9996
+ const wrapped = wrapError2("kv", error);
9997
+ return err({ ...wrapped, meta: { ...wrapped.meta, requestMayHaveDispatched: false } });
9998
+ } finally {
9999
+ request.dispose();
10000
+ }
10001
+ });
10002
+ }
10003
+ async batchPutToNetwork(items, paths, signal, entries) {
10004
+ const session = this.context.session;
10005
+ let requestMayHaveDispatched = false;
10006
+ try {
10007
+ const body = new FormData();
10008
+ for (let index = 0; index < items.length; index++) {
10009
+ body.append(
10010
+ encodeKvBatchPartName(paths[index]),
10011
+ entries?.[index]?.body ?? this.serializeBatchPutValue(items[index])
9805
10012
  );
9806
- const url = `${this.host}/invoke`;
9807
- const init = {
9808
- method: "POST",
9809
- headers,
9810
- body,
9811
- signal: request.signal
9812
- };
9813
- const fetchFn = this.context.fetch;
9814
- requestMayHaveDispatched = true;
9815
- const response = await fetchFn(url, init);
9816
- if (!response.ok) {
9817
- let errorText;
9818
- try {
9819
- errorText = await response.text();
9820
- } catch (textError) {
9821
- const cause = textError instanceof Error ? textError : new Error(String(textError));
9822
- return err(
9823
- serviceError(
9824
- ErrorCodes.KV_WRITE_FAILED,
9825
- `Failed to batch put ${items.length} key(s): ${response.status} - <response body could not be read: ${cause.message}>`,
9826
- "kv",
9827
- { cause, meta: { status: response.status, statusText: response.statusText } }
9828
- )
9829
- );
9830
- }
9831
- if (response.status === 401 || response.status === 403) {
9832
- return this.authorizationFailure(
9833
- `Failed to batch put ${items.length} key(s)`,
9834
- response,
9835
- errorText,
9836
- paths,
9837
- KVAction.PUT
9838
- );
9839
- }
9840
- const quotaError = this.handleQuotaErrorResponse(
9841
- response,
9842
- errorText,
9843
- "batch"
9844
- );
9845
- if (quotaError) {
9846
- return quotaError;
9847
- }
10013
+ }
10014
+ const headers = this.context.invokeAny(
10015
+ session,
10016
+ paths.map((path) => ({
10017
+ spaceId: session.spaceId,
10018
+ service: "kv",
10019
+ path,
10020
+ action: KVAction.PUT
10021
+ }))
10022
+ );
10023
+ const url = `${this.host}/invoke`;
10024
+ const init = {
10025
+ method: "POST",
10026
+ headers,
10027
+ body,
10028
+ signal
10029
+ };
10030
+ const fetchFn = this.context.fetch;
10031
+ requestMayHaveDispatched = true;
10032
+ const response = await fetchFn(url, init);
10033
+ if (!response.ok) {
10034
+ let errorText;
10035
+ try {
10036
+ errorText = await response.text();
10037
+ } catch (textError) {
10038
+ const cause = textError instanceof Error ? textError : new Error(String(textError));
9848
10039
  return err(
9849
10040
  serviceError(
9850
10041
  ErrorCodes.KV_WRITE_FAILED,
9851
- `Failed to batch put ${items.length} key(s): ${response.status} - ${errorText}`,
10042
+ `Failed to batch put ${items.length} key(s): ${response.status} - <response body could not be read: ${cause.message}>`,
9852
10043
  "kv",
9853
- { meta: { status: response.status, statusText: response.statusText } }
10044
+ { cause, meta: { status: response.status, statusText: response.statusText } }
9854
10045
  )
9855
10046
  );
9856
10047
  }
9857
- let rawBody;
9858
- try {
9859
- rawBody = await response.json();
9860
- } catch (jsonError) {
9861
- const cause = jsonError instanceof Error ? jsonError : new Error(String(jsonError));
9862
- const timedOut = isRequestTimeoutError(cause);
9863
- return err(
9864
- serviceError(
9865
- timedOut ? ErrorCodes.TIMEOUT : ErrorCodes.NETWORK_ERROR,
9866
- timedOut ? `KV batchPut response body did not arrive in time: ${cause.message}` : `KV batchPut response was not valid JSON: ${cause.message}`,
9867
- "kv",
9868
- {
9869
- cause,
9870
- meta: {
9871
- ...timedOut ? { timeoutMs: cause.timeoutMs } : {},
9872
- requestMayHaveDispatched: true,
9873
- responseReceived: true,
9874
- status: response.status,
9875
- outcome: "batch-unconfirmed"
9876
- }
9877
- }
9878
- )
10048
+ if (response.status === 401 || response.status === 403) {
10049
+ return this.authorizationFailure(
10050
+ `Failed to batch put ${items.length} key(s)`,
10051
+ response,
10052
+ errorText,
10053
+ paths,
10054
+ KVAction.PUT
9879
10055
  );
9880
10056
  }
9881
- const batchResponse = this.normalizeBatchPutResponse(rawBody);
9882
- if (!batchResponse) {
9883
- return err(
9884
- serviceError(
9885
- ErrorCodes.NETWORK_ERROR,
9886
- "KV batchPut response did not include matching written keys and count",
9887
- "kv",
9888
- {
9889
- meta: {
9890
- requestMayHaveDispatched: true,
9891
- responseReceived: true,
9892
- status: response.status,
9893
- outcome: "batch-unconfirmed"
9894
- }
9895
- }
9896
- )
9897
- );
10057
+ const quotaError = this.handleQuotaErrorResponse(
10058
+ response,
10059
+ errorText,
10060
+ "batch"
10061
+ );
10062
+ if (quotaError) {
10063
+ return quotaError;
9898
10064
  }
9899
- const requestedPaths = new Set(paths);
9900
- const writtenPaths = new Set(batchResponse.written);
9901
- const matchesRequest = batchResponse.count === paths.length && batchResponse.written.length === paths.length && writtenPaths.size === paths.length && batchResponse.written.every((key) => requestedPaths.has(key));
9902
- if (!matchesRequest) {
9903
- return err(
9904
- serviceError(
9905
- ErrorCodes.NETWORK_ERROR,
9906
- `KV batchPut response did not confirm all ${paths.length} requested key(s) were written`,
9907
- "kv",
9908
- {
9909
- meta: {
9910
- requestMayHaveDispatched: true,
9911
- responseReceived: true,
9912
- status: response.status,
9913
- outcome: "batch-unconfirmed"
9914
- }
10065
+ return err(
10066
+ serviceError(
10067
+ ErrorCodes.KV_WRITE_FAILED,
10068
+ `Failed to batch put ${items.length} key(s): ${response.status} - ${errorText}`,
10069
+ "kv",
10070
+ { meta: { status: response.status, statusText: response.statusText } }
10071
+ )
10072
+ );
10073
+ }
10074
+ let rawBody;
10075
+ try {
10076
+ rawBody = await response.json();
10077
+ } catch (jsonError) {
10078
+ const cause = jsonError instanceof Error ? jsonError : new Error(String(jsonError));
10079
+ const timedOut = isRequestTimeoutError(cause);
10080
+ return err(
10081
+ serviceError(
10082
+ timedOut ? ErrorCodes.TIMEOUT : ErrorCodes.NETWORK_ERROR,
10083
+ timedOut ? `KV batchPut response body did not arrive in time: ${cause.message}` : `KV batchPut response was not valid JSON: ${cause.message}`,
10084
+ "kv",
10085
+ {
10086
+ cause,
10087
+ meta: {
10088
+ ...timedOut ? { timeoutMs: cause.timeoutMs } : {},
10089
+ requestMayHaveDispatched: true,
10090
+ responseReceived: true,
10091
+ status: response.status,
10092
+ outcome: "batch-unconfirmed"
9915
10093
  }
9916
- )
9917
- );
9918
- }
9919
- return ok(batchResponse);
9920
- } catch (error) {
9921
- const wrapped = wrapError2("kv", error);
9922
- return err({
9923
- ...wrapped,
9924
- meta: { ...wrapped.meta, requestMayHaveDispatched }
9925
- });
9926
- } finally {
9927
- request.dispose();
10094
+ }
10095
+ )
10096
+ );
9928
10097
  }
9929
- });
10098
+ const batchResponse = this.normalizeBatchPutResponse(rawBody);
10099
+ if (!batchResponse) {
10100
+ return err(
10101
+ serviceError(
10102
+ ErrorCodes.NETWORK_ERROR,
10103
+ "KV batchPut response did not include matching written keys and count",
10104
+ "kv",
10105
+ {
10106
+ meta: {
10107
+ requestMayHaveDispatched: true,
10108
+ responseReceived: true,
10109
+ status: response.status,
10110
+ outcome: "batch-unconfirmed"
10111
+ }
10112
+ }
10113
+ )
10114
+ );
10115
+ }
10116
+ const requestedPaths = new Set(paths);
10117
+ const writtenPaths = new Set(batchResponse.written);
10118
+ const matchesRequest = batchResponse.count === paths.length && batchResponse.written.length === paths.length && writtenPaths.size === paths.length && batchResponse.written.every((key) => requestedPaths.has(key));
10119
+ if (!matchesRequest) {
10120
+ return err(
10121
+ serviceError(
10122
+ ErrorCodes.NETWORK_ERROR,
10123
+ `KV batchPut response did not confirm all ${paths.length} requested key(s) were written`,
10124
+ "kv",
10125
+ {
10126
+ meta: {
10127
+ requestMayHaveDispatched: true,
10128
+ responseReceived: true,
10129
+ status: response.status,
10130
+ outcome: "batch-unconfirmed"
10131
+ }
10132
+ }
10133
+ )
10134
+ );
10135
+ }
10136
+ return ok(batchResponse);
10137
+ } catch (error) {
10138
+ const wrapped = wrapError2("kv", error);
10139
+ return err({
10140
+ ...wrapped,
10141
+ meta: { ...wrapped.meta, requestMayHaveDispatched }
10142
+ });
10143
+ }
9930
10144
  }
9931
10145
  /**
9932
10146
  * List keys with optional prefix filtering.
9933
10147
  */
9934
10148
  async list(options) {
9935
10149
  return this.withTelemetry("list", options?.prefix, async () => {
10150
+ if (this.readThroughReady !== null) await this.readThroughReady;
9936
10151
  if (!this.requireAuth()) {
9937
10152
  return err(authRequiredError("kv"));
9938
10153
  }
@@ -9949,49 +10164,44 @@ var init_dist2 = __esm({
9949
10164
  }
9950
10165
  const request = this.createRequestSignal(options?.signal, options?.timeout);
9951
10166
  try {
9952
- const response = await this.invokeOperation(
9953
- listPath,
9954
- KVAction.LIST,
9955
- void 0,
9956
- request.signal,
9957
- options?.limit === void 0 && options?.cursor === void 0 ? void 0 : {
9958
- ...options?.limit === void 0 ? {} : { "x-tinycloud-limit": String(options.limit) },
9959
- ...options?.cursor === void 0 ? {} : { "x-tinycloud-cursor": options.cursor }
9960
- }
9961
- );
9962
- if (!response.ok) {
9963
- if (response.status === 401 || response.status === 403) {
9964
- return this.authorizationFailure(
9965
- "Failed to list keys",
9966
- response,
9967
- await this.readAuthorizationText(response),
9968
- [listPath],
9969
- KVAction.LIST
9970
- );
10167
+ const network = () => this.listFromNetwork(listPath, options, request.signal);
10168
+ let page;
10169
+ if (options?.source === "network") {
10170
+ const observer = this.readThrough;
10171
+ if (observer === null) {
10172
+ page = await network();
10173
+ } else {
10174
+ let outcome = "error";
10175
+ const startedAt = Date.now();
10176
+ try {
10177
+ page = await network();
10178
+ outcome = page.ok ? "found" : "error";
10179
+ } finally {
10180
+ try {
10181
+ observer.observeNetworkRequested({
10182
+ op: "list",
10183
+ space: this.context.session.spaceId,
10184
+ path: listPath,
10185
+ reason: "NETWORK_REQUESTED",
10186
+ outcome,
10187
+ latencyMs: Date.now() - startedAt
10188
+ });
10189
+ } catch {
10190
+ }
10191
+ }
9971
10192
  }
9972
- const errorText = await response.text();
9973
- return err(
9974
- serviceError(
9975
- ErrorCodes.NETWORK_ERROR,
9976
- `Failed to list keys: ${response.status} - ${errorText}`,
9977
- "kv",
9978
- { meta: { status: response.status, statusText: response.statusText } }
9979
- )
9980
- );
9981
- }
9982
- let keys = await this.parseResponse(response, options?.raw);
9983
- keys = keys ?? [];
9984
- if (options?.removePrefix && listPath) {
9985
- const prefixWithSlash = listPath.endsWith("/") ? listPath : `${listPath}/`;
9986
- keys = keys.map(
9987
- (key) => key.startsWith(prefixWithSlash) ? key.slice(prefixWithSlash.length) : key
9988
- );
10193
+ } else if (this.readThrough === null) {
10194
+ page = await network();
10195
+ } else {
10196
+ page = await this.readThrough.list({
10197
+ space: this.context.session.spaceId,
10198
+ listPath,
10199
+ options,
10200
+ signal: request.signal,
10201
+ network
10202
+ });
9989
10203
  }
9990
- return ok({
9991
- keys,
9992
- ...response.headers.get("x-tinycloud-truncated") === null ? {} : { truncated: response.headers.get("x-tinycloud-truncated") === "true" },
9993
- ...response.headers.get("x-tinycloud-next-cursor") === null ? {} : { nextCursor: response.headers.get("x-tinycloud-next-cursor") ?? void 0 }
9994
- });
10204
+ return page.ok ? this.finishList(page.data, listPath, options?.removePrefix) : page;
9995
10205
  } catch (error) {
9996
10206
  return err(wrapError2("kv", error));
9997
10207
  } finally {
@@ -9999,6 +10209,56 @@ var init_dist2 = __esm({
9999
10209
  }
10000
10210
  });
10001
10211
  }
10212
+ async listFromNetwork(listPath, options, signal) {
10213
+ const response = await this.invokeOperation(
10214
+ listPath,
10215
+ KVAction.LIST,
10216
+ void 0,
10217
+ signal,
10218
+ options?.limit === void 0 && options?.cursor === void 0 ? void 0 : {
10219
+ ...options?.limit === void 0 ? {} : { "x-tinycloud-limit": String(options.limit) },
10220
+ ...options?.cursor === void 0 ? {} : { "x-tinycloud-cursor": options.cursor }
10221
+ }
10222
+ );
10223
+ if (!response.ok) {
10224
+ if (response.status === 401 || response.status === 403) {
10225
+ return this.authorizationFailure(
10226
+ "Failed to list keys",
10227
+ response,
10228
+ await this.readAuthorizationText(response),
10229
+ [listPath],
10230
+ KVAction.LIST
10231
+ );
10232
+ }
10233
+ const errorText = await response.text();
10234
+ return err(serviceError(
10235
+ ErrorCodes.NETWORK_ERROR,
10236
+ `Failed to list keys: ${response.status} - ${errorText}`,
10237
+ "kv",
10238
+ { meta: { status: response.status, statusText: response.statusText } }
10239
+ ));
10240
+ }
10241
+ const keys = await this.parseResponse(response, options?.raw) ?? [];
10242
+ return ok({
10243
+ keys,
10244
+ ...response.headers.get("x-tinycloud-truncated") === null ? {} : { truncated: response.headers.get("x-tinycloud-truncated") === "true" },
10245
+ ...response.headers.get("x-tinycloud-next-cursor") === null ? {} : { nextCursor: response.headers.get("x-tinycloud-next-cursor") ?? void 0 }
10246
+ });
10247
+ }
10248
+ finishList(page, listPath, removePrefix) {
10249
+ let keys = page.keys;
10250
+ if (removePrefix && listPath) {
10251
+ const prefixWithSlash = listPath.endsWith("/") ? listPath : `${listPath}/`;
10252
+ keys = keys.map(
10253
+ (key) => key.startsWith(prefixWithSlash) ? key.slice(prefixWithSlash.length) : key
10254
+ );
10255
+ }
10256
+ return ok({
10257
+ keys,
10258
+ ...page.truncated === void 0 ? {} : { truncated: page.truncated },
10259
+ ...page.nextCursor === void 0 ? {} : { nextCursor: page.nextCursor }
10260
+ });
10261
+ }
10002
10262
  /**
10003
10263
  * Read one page of the `tinycloud.kv/sync` change feed for a prefix.
10004
10264
  */
@@ -10167,61 +10427,22 @@ var init_dist2 = __esm({
10167
10427
  */
10168
10428
  async delete(key, options) {
10169
10429
  return this.withTelemetry("delete", key, async () => {
10430
+ if (this.readThroughReady !== null) await this.readThroughReady;
10170
10431
  if (!this.requireAuth()) {
10171
10432
  return err(authRequiredError("kv"));
10172
10433
  }
10173
10434
  const path = this.getFullPath(key, options?.prefix);
10174
10435
  const request = this.createRequestSignal(options?.signal, options?.timeout);
10175
10436
  try {
10176
- const response = await this.invokeOperation(
10177
- path,
10178
- KVAction.DELETE,
10179
- void 0,
10180
- request.signal,
10181
- options?.ifMatch === void 0 ? void 0 : { "if-match": options.ifMatch }
10182
- );
10183
- if (!response.ok) {
10184
- if (response.status === 401 || response.status === 403) {
10185
- return this.authorizationFailure(
10186
- `Failed to delete key ${JSON.stringify(key)}`,
10187
- response,
10188
- await this.readAuthorizationText(response),
10189
- [path],
10190
- KVAction.DELETE
10191
- );
10192
- }
10193
- if (response.status === 404) {
10194
- return await this.classifyNotFound(response, key);
10195
- }
10196
- const errorText = await response.text();
10197
- if (response.status === 412) {
10198
- return err(serviceError(
10199
- ErrorCodes.KV_PRECONDITION_FAILED,
10200
- `KV precondition failed for key ${JSON.stringify(key)}`,
10201
- "kv",
10202
- { meta: { status: response.status, statusText: response.statusText } }
10203
- ));
10204
- }
10205
- if (response.status === 503 && options?.ifMatch !== void 0) {
10206
- return err(serviceError(
10207
- ErrorCodes.KV_CONFLICT,
10208
- `Concurrent KV delete conflicted for key ${JSON.stringify(key)}`,
10209
- "kv",
10210
- { meta: { status: response.status, statusText: response.statusText } }
10211
- ));
10212
- }
10213
- return err(
10214
- serviceError(
10215
- ErrorCodes.NETWORK_ERROR,
10216
- `Failed to delete key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
10217
- "kv",
10218
- { meta: { status: response.status, statusText: response.statusText } }
10219
- )
10220
- );
10221
- }
10222
- return ok({
10223
- data: void 0,
10224
- headers: this.createResponseHeaders(response.headers)
10437
+ const network = () => this.deleteFromNetwork(key, path, options, request.signal);
10438
+ if (this.readThrough === null) return await network();
10439
+ return await this.readThrough.write({
10440
+ op: "delete",
10441
+ space: this.context.session.spaceId,
10442
+ entries: [{ path }],
10443
+ signal: request.signal,
10444
+ conditional: options?.ifMatch !== void 0,
10445
+ network
10225
10446
  });
10226
10447
  } catch (error) {
10227
10448
  return err(wrapError2("kv", error));
@@ -10230,6 +10451,54 @@ var init_dist2 = __esm({
10230
10451
  }
10231
10452
  });
10232
10453
  }
10454
+ async deleteFromNetwork(key, path, options, signal) {
10455
+ const response = await this.invokeOperation(
10456
+ path,
10457
+ KVAction.DELETE,
10458
+ void 0,
10459
+ signal,
10460
+ options?.ifMatch === void 0 ? void 0 : { "if-match": options.ifMatch }
10461
+ );
10462
+ if (!response.ok) {
10463
+ if (response.status === 401 || response.status === 403) {
10464
+ return this.authorizationFailure(
10465
+ `Failed to delete key ${JSON.stringify(key)}`,
10466
+ response,
10467
+ await this.readAuthorizationText(response),
10468
+ [path],
10469
+ KVAction.DELETE
10470
+ );
10471
+ }
10472
+ if (response.status === 404) return this.classifyNotFound(response, key);
10473
+ const errorText = await response.text();
10474
+ if (response.status === 412) {
10475
+ return err(serviceError(
10476
+ ErrorCodes.KV_PRECONDITION_FAILED,
10477
+ `KV precondition failed for key ${JSON.stringify(key)}`,
10478
+ "kv",
10479
+ { meta: { status: response.status, statusText: response.statusText } }
10480
+ ));
10481
+ }
10482
+ if (response.status === 503 && options?.ifMatch !== void 0) {
10483
+ return err(serviceError(
10484
+ ErrorCodes.KV_CONFLICT,
10485
+ `Concurrent KV delete conflicted for key ${JSON.stringify(key)}`,
10486
+ "kv",
10487
+ { meta: { status: response.status, statusText: response.statusText } }
10488
+ ));
10489
+ }
10490
+ return err(serviceError(
10491
+ ErrorCodes.NETWORK_ERROR,
10492
+ `Failed to delete key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
10493
+ "kv",
10494
+ { meta: { status: response.status, statusText: response.statusText } }
10495
+ ));
10496
+ }
10497
+ return ok({
10498
+ data: void 0,
10499
+ headers: this.createResponseHeaders(response.headers)
10500
+ });
10501
+ }
10233
10502
  /**
10234
10503
  * Get metadata for a key without retrieving the value.
10235
10504
  */
@@ -10525,7 +10794,7 @@ var init_dist2 = __esm({
10525
10794
  request.signal
10526
10795
  );
10527
10796
  if (!response.ok) {
10528
- return this.handleErrorResponse(response, "query", dbName, [action]);
10797
+ return await this.handleErrorResponse(response, "query", dbName, [action]);
10529
10798
  }
10530
10799
  const data = await response.json();
10531
10800
  return ok(data);
@@ -10565,7 +10834,7 @@ var init_dist2 = __esm({
10565
10834
  request.signal
10566
10835
  );
10567
10836
  if (!response.ok) {
10568
- return this.handleErrorResponse(response, "execute", dbName, requestedActions2);
10837
+ return await this.handleErrorResponse(response, "execute", dbName, requestedActions2);
10569
10838
  }
10570
10839
  const data = await response.json();
10571
10840
  return ok(data);
@@ -10591,7 +10860,7 @@ var init_dist2 = __esm({
10591
10860
  request.signal
10592
10861
  );
10593
10862
  if (!response.ok) {
10594
- return this.handleErrorResponse(response, "batch", dbName, requestedActions2);
10863
+ return await this.handleErrorResponse(response, "batch", dbName, requestedActions2);
10595
10864
  }
10596
10865
  const data = await response.json();
10597
10866
  return ok(data);
@@ -10616,7 +10885,7 @@ var init_dist2 = __esm({
10616
10885
  request.signal
10617
10886
  );
10618
10887
  if (!response.ok) {
10619
- return this.handleErrorResponse(response, "executeStatement", dbName, [SQLAction.WRITE]);
10888
+ return await this.handleErrorResponse(response, "executeStatement", dbName, [SQLAction.WRITE]);
10620
10889
  }
10621
10890
  const data = await response.json();
10622
10891
  return ok(data);
@@ -10641,7 +10910,7 @@ var init_dist2 = __esm({
10641
10910
  request.signal
10642
10911
  );
10643
10912
  if (!response.ok) {
10644
- return this.handleErrorResponse(response, "export", dbName, [SQLAction.READ]);
10913
+ return await this.handleErrorResponse(response, "export", dbName, [SQLAction.READ]);
10645
10914
  }
10646
10915
  const resp = response;
10647
10916
  if (typeof resp.blob === "function") {
@@ -10928,7 +11197,7 @@ var init_dist2 = __esm({
10928
11197
  request.signal
10929
11198
  );
10930
11199
  if (!response.ok) {
10931
- return this.handleErrorResponse(response, "query");
11200
+ return await this.handleErrorResponse(response, "query");
10932
11201
  }
10933
11202
  const data = await response.json();
10934
11203
  return ok(data);
@@ -10954,7 +11223,7 @@ var init_dist2 = __esm({
10954
11223
  { Accept: "application/vnd.apache.arrow.stream" }
10955
11224
  );
10956
11225
  if (!response.ok) {
10957
- return this.handleErrorResponse(response, "queryArrow");
11226
+ return await this.handleErrorResponse(response, "queryArrow");
10958
11227
  }
10959
11228
  const buffer = await response.arrayBuffer();
10960
11229
  return ok(buffer);
@@ -10987,7 +11256,7 @@ var init_dist2 = __esm({
10987
11256
  request.signal
10988
11257
  );
10989
11258
  if (!response.ok) {
10990
- return this.handleErrorResponse(response, "execute");
11259
+ return await this.handleErrorResponse(response, "execute");
10991
11260
  }
10992
11261
  const data = await response.json();
10993
11262
  return ok(data);
@@ -11019,7 +11288,7 @@ var init_dist2 = __esm({
11019
11288
  request.signal
11020
11289
  );
11021
11290
  if (!response.ok) {
11022
- return this.handleErrorResponse(response, "batch");
11291
+ return await this.handleErrorResponse(response, "batch");
11023
11292
  }
11024
11293
  const data = await response.json();
11025
11294
  return ok(data);
@@ -11044,7 +11313,7 @@ var init_dist2 = __esm({
11044
11313
  request.signal
11045
11314
  );
11046
11315
  if (!response.ok) {
11047
- return this.handleErrorResponse(response, "executeStatement");
11316
+ return await this.handleErrorResponse(response, "executeStatement");
11048
11317
  }
11049
11318
  const data = await response.json();
11050
11319
  return ok(data);
@@ -11069,7 +11338,7 @@ var init_dist2 = __esm({
11069
11338
  request.signal
11070
11339
  );
11071
11340
  if (!response.ok) {
11072
- return this.handleErrorResponse(response, "describe");
11341
+ return await this.handleErrorResponse(response, "describe");
11073
11342
  }
11074
11343
  const data = await response.json();
11075
11344
  return ok(data);
@@ -11094,7 +11363,7 @@ var init_dist2 = __esm({
11094
11363
  request.signal
11095
11364
  );
11096
11365
  if (!response.ok) {
11097
- return this.handleErrorResponse(response, "export");
11366
+ return await this.handleErrorResponse(response, "export");
11098
11367
  }
11099
11368
  const blob = await response.blob();
11100
11369
  return ok(blob);
@@ -11129,7 +11398,7 @@ var init_dist2 = __esm({
11129
11398
  signal: request.signal
11130
11399
  });
11131
11400
  if (!response.ok) {
11132
- return this.handleErrorResponse(response, "import");
11401
+ return await this.handleErrorResponse(response, "import");
11133
11402
  }
11134
11403
  return ok(void 0);
11135
11404
  } catch (error) {
@@ -12139,7 +12408,8 @@ var init_dist2 = __esm({
12139
12408
  }
12140
12409
  try {
12141
12410
  const valueResult = await this.tc.kv.get(`vault/${key}`, {
12142
- raw: true
12411
+ raw: true,
12412
+ ...options?.source === void 0 ? {} : { source: options.source }
12143
12413
  });
12144
12414
  if (!valueResult.ok) {
12145
12415
  return vaultError({ code: "KEY_NOT_FOUND", key });
@@ -12184,7 +12454,7 @@ var init_dist2 = __esm({
12184
12454
  }
12185
12455
  let valueResult;
12186
12456
  try {
12187
- valueResult = await this.tc.kv.get(`vault/${key}`, { raw: true });
12457
+ valueResult = await this.tc.kv.get(`vault/${key}`, { raw: true, source: "network" });
12188
12458
  } catch {
12189
12459
  return { status: "node_unreachable" };
12190
12460
  }
@@ -12253,7 +12523,8 @@ var init_dist2 = __esm({
12253
12523
  }
12254
12524
  try {
12255
12525
  const valueResult = await this.tc.kv.get(`vault/${key}`, {
12256
- raw: true
12526
+ raw: true,
12527
+ source: "network"
12257
12528
  });
12258
12529
  if (!valueResult.ok) {
12259
12530
  return vaultError({ code: "KEY_NOT_FOUND", key });
@@ -12387,7 +12658,8 @@ var init_dist2 = __esm({
12387
12658
  }
12388
12659
  try {
12389
12660
  const keyResult = await this.tc.kv.get(`keys/${key}`, {
12390
- raw: true
12661
+ raw: true,
12662
+ ...options?.source === void 0 ? {} : { source: options.source }
12391
12663
  });
12392
12664
  if (!keyResult.ok) {
12393
12665
  return vaultError({ code: "KEY_NOT_FOUND", key });
@@ -12396,7 +12668,8 @@ var init_dist2 = __esm({
12396
12668
  const keyBlobBytes = base64Decode(keyEnvelope.key);
12397
12669
  const entryKey = this.crypto.decrypt(this.masterKey, keyBlobBytes);
12398
12670
  const valueResult = await this.tc.kv.get(`vault/${key}`, {
12399
- raw: true
12671
+ raw: true,
12672
+ ...options?.source === void 0 ? {} : { source: options.source }
12400
12673
  });
12401
12674
  if (!valueResult.ok) {
12402
12675
  return vaultError({ code: "KEY_NOT_FOUND", key });
@@ -12506,6 +12779,7 @@ var init_dist2 = __esm({
12506
12779
  prefix: listPrefix,
12507
12780
  removePrefix: true,
12508
12781
  ...options?.limit === void 0 ? {} : { limit: options.limit },
12782
+ ...options?.source === void 0 ? {} : { source: options.source },
12509
12783
  ...options?.cursor === void 0 ? {} : { cursor: options.cursor }
12510
12784
  });
12511
12785
  if (!listResult.ok) {
@@ -14264,7 +14538,8 @@ async function loadManifestPermissions(source, profile, options = {}) {
14264
14538
  service,
14265
14539
  space: String(manifest.space ?? "applications"),
14266
14540
  path: resolvedPath,
14267
- actions
14541
+ actions,
14542
+ ...Array.isArray(entry.caveats) ? { caveats: entry.caveats } : {}
14268
14543
  };
14269
14544
  });
14270
14545
  permissions.push(...await secretPermissionsFromAppManifest(manifest, profile, options.ownerDid));
@@ -14511,13 +14786,127 @@ var init_delegation_expiry = __esm({
14511
14786
  }
14512
14787
  });
14513
14788
 
14789
+ // src/lib/replication-log.ts
14790
+ import { appendFileSync, chmodSync, mkdirSync, renameSync, statSync, writeFileSync } from "fs";
14791
+ import { dirname as dirname2, join as join4 } from "path";
14792
+ function appendReplicationEvent(root, event) {
14793
+ mkdirSync(root, { recursive: true, mode: DIR_MODE });
14794
+ chmodSync(root, DIR_MODE);
14795
+ const path = join4(root, "events.jsonl");
14796
+ let size = 0;
14797
+ try {
14798
+ size = statSync(path).size;
14799
+ } catch (error) {
14800
+ if (error.code !== "ENOENT") throw error;
14801
+ }
14802
+ const line = `${JSON.stringify(event)}
14803
+ `;
14804
+ if (size + Buffer.byteLength(line) > MAX_LOG_BYTES) {
14805
+ const rotated = join4(root, "events.jsonl.1");
14806
+ try {
14807
+ renameSync(path, rotated);
14808
+ } catch (error) {
14809
+ if (error.code !== "ENOENT") throw error;
14810
+ }
14811
+ writeFileSync(path, line, { mode: LOG_MODE });
14812
+ } else {
14813
+ appendFileSync(path, line, { mode: LOG_MODE });
14814
+ }
14815
+ chmodSync(path, LOG_MODE);
14816
+ }
14817
+ function replicationLogPath(profileRoot) {
14818
+ return join4(profileRoot, "replication", "events.jsonl");
14819
+ }
14820
+ function createReplicationEventSink(profileRoot, options) {
14821
+ const pinnedNotices = /* @__PURE__ */ new Set();
14822
+ const missingGrantWarnings = /* @__PURE__ */ new Set();
14823
+ return (event) => {
14824
+ appendProfileReplicationEvent(profileRoot, event);
14825
+ if (event.type === "replication.state" && event.state === "grant_missing") {
14826
+ const prefix = event.replica ?? event.space ?? "configured prefix";
14827
+ const key = `${event.space ?? ""}\0${event.replica ?? ""}`;
14828
+ if (!missingGrantWarnings.has(key)) {
14829
+ missingGrantWarnings.add(key);
14830
+ process.stderr.write(`[replication] Warning: no usable kv/get+sync grant for ${prefix}; reads use the network (${event.code ?? "grant_missing"}).
14831
+ `);
14832
+ }
14833
+ }
14834
+ if (options.debug) {
14835
+ if (event.type === "replication.read") {
14836
+ const detail = event.source === "replica" ? "replica hit" : `${event.source} ${event.reason}`;
14837
+ const syncStatus = event.syncedBeforeRead === void 0 ? "" : ` syncedBeforeRead:${event.syncedBeforeRead}`;
14838
+ const syncError = event.syncError === void 0 ? "" : ` syncError:${event.syncError}`;
14839
+ process.stderr.write(`[replication] ${event.op} ${event.key} \u2190 ${detail} ${event.latencyMs}ms${syncStatus}${syncError}${event.stalenessMs === null ? "" : ` (synced ${Math.round(event.stalenessMs / 1e3)}s ago)`}
14840
+ `);
14841
+ } else if (event.type === "replication.write") {
14842
+ process.stderr.write(`[replication] ${event.op} ${event.keys.join(",")} ${event.outcome}${event.code ? ` ${event.code}` : ""}
14843
+ `);
14844
+ } else if (event.type === "replication.sync") {
14845
+ const pending = event.pendingCleared === void 0 ? "" : ` pendingCleared:${event.pendingCleared}`;
14846
+ process.stderr.write(`[replication] sync ${event.replica} ${event.outcome}${event.code ? ` ${event.code}` : ""}${pending}
14847
+ `);
14848
+ }
14849
+ }
14850
+ if (event.type === "replication.read" && (event.pendingState === "in_flight" || event.pendingState === "ambiguous") && !options.quiet && !pinnedNotices.has(event.key)) {
14851
+ pinnedNotices.add(event.key);
14852
+ process.stderr.write(`[replication] ${event.key} reads from the network: an earlier write's outcome is unknown (${event.pendingState}${event.code ? `, ${event.code}` : ""}). Review: tc replica report
14853
+ `);
14854
+ }
14855
+ };
14856
+ }
14857
+ function appendProfileReplicationEvent(profileRoot, event) {
14858
+ appendReplicationEvent(dirname2(replicationLogPath(profileRoot)), event);
14859
+ }
14860
+ var MAX_LOG_BYTES, LOG_MODE, DIR_MODE;
14861
+ var init_replication_log = __esm({
14862
+ "src/lib/replication-log.ts"() {
14863
+ "use strict";
14864
+ MAX_LOG_BYTES = 5 * 1024 * 1024;
14865
+ LOG_MODE = 384;
14866
+ DIR_MODE = 448;
14867
+ }
14868
+ });
14869
+
14870
+ // src/lib/replication-config.ts
14871
+ import { join as join5 } from "path";
14872
+ import { profilePath, withProfileLock as withProfileLock2 } from "@tinycloud/operations/state";
14873
+ import { sqliteReplicaStorage } from "@tinycloud/node-sdk";
14874
+ function createCliReplicationConfig(profile, settings, output2, enabled) {
14875
+ if (!enabled || !settings || settings.prefixes.length === 0) return void 0;
14876
+ const root = profilePath(profile);
14877
+ return {
14878
+ enabled: true,
14879
+ prefixes: settings.prefixes,
14880
+ allowSecrets: settings.allowSecrets === true,
14881
+ mode: "foreground",
14882
+ verify: process.env.TC_REPLICATION_VERIFY === "1",
14883
+ maxStalenessMs: Number(process.env.TC_REPLICATION_MAX_STALENESS_MS ?? 6e4),
14884
+ staleSyncTimeoutMs: Number(process.env.TC_REPLICATION_SYNC_TIMEOUT_MS ?? 1e4),
14885
+ storage: sqliteReplicaStorage({
14886
+ dir: join5(root, "replication"),
14887
+ guard: (section) => withProfileLock2(profile, section, { requireProfile: true, timeoutMs: 35e3 })
14888
+ }),
14889
+ onEvent: createReplicationEventSink(root, output2)
14890
+ };
14891
+ }
14892
+ var init_replication_config = __esm({
14893
+ "src/lib/replication-config.ts"() {
14894
+ "use strict";
14895
+ init_replication_log();
14896
+ }
14897
+ });
14898
+
14514
14899
  // src/lib/sdk.ts
14515
14900
  var sdk_exports = {};
14516
14901
  __export(sdk_exports, {
14517
14902
  bootstrapDelegatedSession: () => bootstrapDelegatedSession,
14903
+ closeReplication: () => closeReplication,
14518
14904
  createSDKInstance: () => createSDKInstance,
14519
14905
  ensureAuthenticated: () => ensureAuthenticated,
14520
14906
  jwkHasPrivateParameter: () => jwkHasPrivateParameter,
14907
+ registerReplication: () => registerReplication,
14908
+ registeredReplications: () => registeredReplications,
14909
+ replicationForProfile: () => replicationForProfile,
14521
14910
  restoreProfileSessionNode: () => restoreProfileSessionNode,
14522
14911
  selectSignerJwk: () => selectSignerJwk,
14523
14912
  storedSessionVerifiesLocally: () => storedSessionVerifiesLocally
@@ -14632,10 +15021,21 @@ async function createSDKInstance(ctx, options) {
14632
15021
  );
14633
15022
  }
14634
15023
  if (profile?.authMethod === "local" && effectivePrivateKey) {
15024
+ const replication2 = createCliReplicationConfig(
15025
+ ctx.profile,
15026
+ profile?.replication,
15027
+ { debug: ctx.replicationDebug, quiet: ctx.quiet },
15028
+ ctx.replication
15029
+ );
15030
+ if (ctx.replication && !replication2) {
15031
+ process.stderr.write("[replication] Enabled, but this profile has no configured prefixes; reads use the network.\n");
15032
+ }
14635
15033
  const node2 = new TinyCloudNode({
14636
15034
  host: ctx.host,
14637
- privateKey: effectivePrivateKey
15035
+ privateKey: effectivePrivateKey,
15036
+ ...replication2 ? { replication: replication2 } : {}
14638
15037
  });
15038
+ if (node2.replication) registerReplication(ctx.profile, node2.replication);
14639
15039
  const restoredOwnSession = Boolean(session && session.delegationHeader && session.delegationCid && session.spaceId);
14640
15040
  if (restoredOwnSession) {
14641
15041
  await restoreProfileSession(node2, ctx.profile, profile, persistedSessionData(ctx.profile, profile, session, key));
@@ -14651,10 +15051,21 @@ async function createSDKInstance(ctx, options) {
14651
15051
  }
14652
15052
  return node2;
14653
15053
  }
15054
+ const replication = createCliReplicationConfig(
15055
+ ctx.profile,
15056
+ profile?.replication,
15057
+ { debug: ctx.replicationDebug, quiet: ctx.quiet },
15058
+ ctx.replication
15059
+ );
15060
+ if (ctx.replication && !replication) {
15061
+ process.stderr.write("[replication] Enabled, but this profile has no configured prefixes; reads use the network.\n");
15062
+ }
14654
15063
  const node = new TinyCloudNode({
14655
15064
  host: ctx.host,
14656
- privateKey: options?.privateKey
15065
+ privateKey: options?.privateKey,
15066
+ ...replication ? { replication } : {}
14657
15067
  });
15068
+ if (node.replication) registerReplication(ctx.profile, node.replication);
14658
15069
  if (options?.privateKey) {
14659
15070
  await node.signIn();
14660
15071
  } else if (session && session.delegationHeader && session.delegationCid && session.spaceId) {
@@ -14796,6 +15207,9 @@ var init_sdk = __esm({
14796
15207
  init_session_expired();
14797
15208
  init_warnings();
14798
15209
  init_delegation_expiry();
15210
+ init_replication_config();
15211
+ init_replication_registry();
15212
+ init_replication_registry();
14799
15213
  BOOTSTRAP_PROFILE_FIELDS = ["sessionDid", "spaceId"];
14800
15214
  }
14801
15215
  });
@@ -14936,14 +15350,14 @@ var init_host = __esm({
14936
15350
 
14937
15351
  // src/config/profiles.ts
14938
15352
  import { chmod as chmod2, lstat, readdir as readdir2, rm as rm2, rmdir } from "fs/promises";
14939
- import { join as join4 } from "path";
15353
+ import { join as join6 } from "path";
14940
15354
  import {
14941
- profilePath,
15355
+ profilePath as profilePath2,
14942
15356
  readSession,
14943
15357
  recordProfileDeletion,
14944
15358
  refuseWriteToDeletedProfile as refuseWriteToDeletedProfile2,
14945
15359
  removeSession,
14946
- withProfileLock as withProfileLock2,
15360
+ withProfileLock as withProfileLock3,
14947
15361
  writeSession
14948
15362
  } from "@tinycloud/operations/state";
14949
15363
  var ProfileManager;
@@ -14961,7 +15375,7 @@ var init_profiles = __esm({
14961
15375
  * it around a whole read-modify-write, not just each write.
14962
15376
  */
14963
15377
  static async withLock(name, action, options) {
14964
- return withProfileLock2(name, action, options);
15378
+ return withProfileLock3(name, action, options);
14965
15379
  }
14966
15380
  /**
14967
15381
  * Read-modify-write of profile.json under the profile lock, so concurrent
@@ -14987,7 +15401,7 @@ var init_profiles = __esm({
14987
15401
  /** Owner-only profile directory (0700), created or tightened before any write into it. */
14988
15402
  static async ensureProfileDir(name) {
14989
15403
  await _ProfileManager.ensureConfigDir();
14990
- const profileDir = join4(PROFILES_DIR, name);
15404
+ const profileDir = join6(PROFILES_DIR, name);
14991
15405
  await ensureDir(profileDir);
14992
15406
  await chmod2(profileDir, PRIVATE_DIR_MODE);
14993
15407
  return profileDir;
@@ -15016,8 +15430,8 @@ var init_profiles = __esm({
15016
15430
  * Throws CLIError if the profile doesn't exist.
15017
15431
  */
15018
15432
  static async getProfile(name) {
15019
- const profilePath4 = join4(PROFILES_DIR, name, "profile.json");
15020
- const profile = await readJson(profilePath4);
15433
+ const profilePath5 = join6(PROFILES_DIR, name, "profile.json");
15434
+ const profile = await readJson(profilePath5);
15021
15435
  if (!profile) {
15022
15436
  throw new CLIError(
15023
15437
  "PROFILE_NOT_FOUND",
@@ -15032,18 +15446,18 @@ var init_profiles = __esm({
15032
15446
  */
15033
15447
  static async setProfile(name, data) {
15034
15448
  await _ProfileManager.withLock(name, async () => {
15035
- await writeJson(join4(await _ProfileManager.ensureProfileDir(name), "profile.json"), data);
15449
+ await writeJson(join6(await _ProfileManager.ensureProfileDir(name), "profile.json"), data);
15036
15450
  });
15037
15451
  }
15038
15452
  /** Removes profile.json under the profile lock (rollback of a profile a failed login created). */
15039
15453
  static async removeProfileConfig(name) {
15040
- await _ProfileManager.withLock(name, () => rm2(join4(PROFILES_DIR, name, "profile.json"), { force: true }));
15454
+ await _ProfileManager.withLock(name, () => rm2(join6(PROFILES_DIR, name, "profile.json"), { force: true }));
15041
15455
  }
15042
15456
  /**
15043
15457
  * Returns true if a profile directory exists.
15044
15458
  */
15045
15459
  static async profileExists(name) {
15046
- return fileExists(join4(PROFILES_DIR, name, "profile.json"));
15460
+ return fileExists(join6(PROFILES_DIR, name, "profile.json"));
15047
15461
  }
15048
15462
  /**
15049
15463
  * Returns an array of profile directory names.
@@ -15067,7 +15481,7 @@ var init_profiles = __esm({
15067
15481
  static async deleteProfile(name) {
15068
15482
  let profileDir;
15069
15483
  try {
15070
- profileDir = profilePath(name);
15484
+ profileDir = profilePath2(name);
15071
15485
  } catch {
15072
15486
  throw new CLIError(
15073
15487
  "INVALID_PROFILE_NAME",
@@ -15092,11 +15506,11 @@ var init_profiles = __esm({
15092
15506
  return;
15093
15507
  }
15094
15508
  await _ProfileManager.withLock(name, async () => {
15095
- for (const file of ["session.json", "key.json"]) await rm2(join4(profileDir, file), { force: true });
15509
+ for (const file of ["session.json", "key.json"]) await rm2(join6(profileDir, file), { force: true });
15096
15510
  for (const entry of await readdir2(profileDir)) {
15097
- if (entry !== ".lock" && entry !== "profile.json") await rm2(join4(profileDir, entry), { recursive: true, force: true });
15511
+ if (entry !== ".lock" && entry !== "profile.json") await rm2(join6(profileDir, entry), { recursive: true, force: true });
15098
15512
  }
15099
- await rm2(join4(profileDir, "profile.json"), { force: true });
15513
+ await rm2(join6(profileDir, "profile.json"), { force: true });
15100
15514
  await recordProfileDeletion(name);
15101
15515
  });
15102
15516
  await rmdir(profileDir).catch((error) => {
@@ -15108,7 +15522,7 @@ var init_profiles = __esm({
15108
15522
  * Returns the parsed JWK for a profile, or null if no key exists.
15109
15523
  */
15110
15524
  static async getKey(name) {
15111
- return readJson(join4(PROFILES_DIR, name, "key.json"));
15525
+ return readJson(join6(PROFILES_DIR, name, "key.json"));
15112
15526
  }
15113
15527
  /**
15114
15528
  * Saves a JWK key under the profile lock (0600, in an owner-only profile
@@ -15118,12 +15532,12 @@ var init_profiles = __esm({
15118
15532
  static async setKey(name, jwk) {
15119
15533
  await _ProfileManager.withLock(name, async () => {
15120
15534
  await refuseWriteToDeletedProfile2(name);
15121
- await writeJson(join4(await _ProfileManager.ensureProfileDir(name), "key.json"), jwk);
15535
+ await writeJson(join6(await _ProfileManager.ensureProfileDir(name), "key.json"), jwk);
15122
15536
  });
15123
15537
  }
15124
15538
  /** Removes key.json under the profile lock (rollback of a key a failed login created). */
15125
15539
  static async removeKey(name) {
15126
- await _ProfileManager.withLock(name, () => rm2(join4(PROFILES_DIR, name, "key.json"), { force: true }));
15540
+ await _ProfileManager.withLock(name, () => rm2(join6(PROFILES_DIR, name, "key.json"), { force: true }));
15127
15541
  }
15128
15542
  // ── Session management ──────────────────────────────────────────────
15129
15543
  /**
@@ -15150,7 +15564,7 @@ var init_profiles = __esm({
15150
15564
  * created or tightened to 0700.
15151
15565
  */
15152
15566
  static async getCacheDir(name) {
15153
- const cacheDir = join4(await _ProfileManager.ensureProfileDir(name), "cache");
15567
+ const cacheDir = join6(await _ProfileManager.ensureProfileDir(name), "cache");
15154
15568
  await ensureDir(cacheDir);
15155
15569
  await chmod2(cacheDir, PRIVATE_DIR_MODE);
15156
15570
  return cacheDir;
@@ -15173,11 +15587,20 @@ var init_profiles = __esm({
15173
15587
  const config = await _ProfileManager.getConfig();
15174
15588
  const profile = options.profile ?? process.env.TC_PROFILE ?? config.defaultProfile ?? DEFAULT_PROFILE;
15175
15589
  let profileHost;
15590
+ let profileConfig;
15176
15591
  try {
15177
- const profileConfig = await _ProfileManager.getProfile(profile);
15592
+ profileConfig = await _ProfileManager.getProfile(profile);
15178
15593
  profileHost = profileConfig.host;
15179
15594
  } catch {
15180
15595
  }
15596
+ const envReplication = process.env.TC_REPLICATION?.toLowerCase();
15597
+ const envEnabled = envReplication === "1" || envReplication === "true" || envReplication === "on";
15598
+ const explicitEnvOff = envReplication === "0" || envReplication === "false" || envReplication === "off";
15599
+ if (envReplication !== void 0 && !envEnabled && !explicitEnvOff) {
15600
+ throw new CLIError("INVALID_ARGUMENT", "TC_REPLICATION must be one of 1, true, on, 0, false or off.", ExitCode.USAGE_ERROR);
15601
+ }
15602
+ const explicitOff = options.noReplication === true || options.replication === false;
15603
+ const replication = options.replication === true ? true : explicitOff ? false : envEnabled;
15181
15604
  const explicitHost = options.host ?? process.env.TC_HOST;
15182
15605
  let host = explicitHost ?? profileHost ?? DEFAULT_HOST;
15183
15606
  let localDiscovery;
@@ -15197,6 +15620,8 @@ var init_profiles = __esm({
15197
15620
  verbose: options.verbose ?? false,
15198
15621
  noCache: options.noCache ?? false,
15199
15622
  quiet: options.quiet ?? false,
15623
+ replication,
15624
+ replicationDebug: options.replicationDebug === true || process.env.TC_REPLICATION_DEBUG === "1",
15200
15625
  ...localDiscovery === void 0 ? {} : { localDiscovery }
15201
15626
  };
15202
15627
  }
@@ -15223,7 +15648,7 @@ function registerAccountCommand(program) {
15223
15648
  assertOk(status);
15224
15649
  outputJson(status.data);
15225
15650
  } catch (error) {
15226
- handleError(error);
15651
+ return handleError(error);
15227
15652
  }
15228
15653
  });
15229
15654
  const apps = account.command("apps").description("Manage account application registry");
@@ -15254,7 +15679,7 @@ function registerAccountCommand(program) {
15254
15679
  ) + "\n"
15255
15680
  );
15256
15681
  } catch (error) {
15257
- handleError(error);
15682
+ return handleError(error);
15258
15683
  }
15259
15684
  });
15260
15685
  const spaces = account.command("spaces").description("Manage account space registry");
@@ -15285,7 +15710,7 @@ function registerAccountCommand(program) {
15285
15710
  ) + "\n"
15286
15711
  );
15287
15712
  } catch (error) {
15288
- handleError(error);
15713
+ return handleError(error);
15289
15714
  }
15290
15715
  });
15291
15716
  spaces.command("info <space-id>").description("Show a registered account space").action(async (spaceId, _options, cmd) => {
@@ -15295,7 +15720,7 @@ function registerAccountCommand(program) {
15295
15720
  assertOk(result);
15296
15721
  outputJson(formatSpace(result.data));
15297
15722
  } catch (error) {
15298
- handleError(error);
15723
+ return handleError(error);
15299
15724
  }
15300
15725
  });
15301
15726
  spaces.command("register <space-id>").description("Register a space in account/spaces").requiredOption("--name <name>", "Display name for the space").requiredOption("--owner <did>", "Owner DID for the space").option("--type <type>", "Space type: owned, delegated, or discovered", "discovered").option("--permission <permission...>", "Permission strings for the space").action(async (spaceId, options, cmd) => {
@@ -15312,7 +15737,7 @@ function registerAccountCommand(program) {
15312
15737
  assertOk(result);
15313
15738
  outputJson({ space: formatSpace(result.data), registered: true });
15314
15739
  } catch (error) {
15315
- handleError(error);
15740
+ return handleError(error);
15316
15741
  }
15317
15742
  });
15318
15743
  spaces.command("sync").description("Register currently accessible spaces into account/spaces").action(async (_options, cmd) => {
@@ -15322,7 +15747,7 @@ function registerAccountCommand(program) {
15322
15747
  assertOk(result);
15323
15748
  outputJson({ spaces: result.data.map(formatSpace), count: result.data.length, synced: true });
15324
15749
  } catch (error) {
15325
- handleError(error);
15750
+ return handleError(error);
15326
15751
  }
15327
15752
  });
15328
15753
  spaces.command("remove <space-id>").alias("delete").description("Remove a space registry entry").action(async (spaceId, _options, cmd) => {
@@ -15332,7 +15757,7 @@ function registerAccountCommand(program) {
15332
15757
  assertOk(result);
15333
15758
  outputJson({ spaceId, removed: true });
15334
15759
  } catch (error) {
15335
- handleError(error);
15760
+ return handleError(error);
15336
15761
  }
15337
15762
  });
15338
15763
  apps.command("info <app-id>").description("Show a registered account application").action(async (appId, _options, cmd) => {
@@ -15342,7 +15767,7 @@ function registerAccountCommand(program) {
15342
15767
  assertOk(result);
15343
15768
  outputJson(result.data);
15344
15769
  } catch (error) {
15345
- handleError(error);
15770
+ return handleError(error);
15346
15771
  }
15347
15772
  });
15348
15773
  apps.command("register <manifest>").description("Register an app manifest in account/applications").action(async (manifestSource, _options, cmd) => {
@@ -15353,7 +15778,7 @@ function registerAccountCommand(program) {
15353
15778
  assertOk(result);
15354
15779
  outputJson({ application: result.data, registered: true });
15355
15780
  } catch (error) {
15356
- handleError(error);
15781
+ return handleError(error);
15357
15782
  }
15358
15783
  });
15359
15784
  apps.command("remove <app-id>").alias("delete").description("Remove an application registry entry").action(async (appId, _options, cmd) => {
@@ -15363,7 +15788,7 @@ function registerAccountCommand(program) {
15363
15788
  assertOk(result);
15364
15789
  outputJson({ appId, removed: true });
15365
15790
  } catch (error) {
15366
- handleError(error);
15791
+ return handleError(error);
15367
15792
  }
15368
15793
  });
15369
15794
  const delegations = account.command("delegations").description("View and revoke account delegations");
@@ -15399,7 +15824,7 @@ function registerAccountCommand(program) {
15399
15824
  ) + "\n"
15400
15825
  );
15401
15826
  } catch (error) {
15402
- handleError(error);
15827
+ return handleError(error);
15403
15828
  }
15404
15829
  });
15405
15830
  delegations.command("revoke <cid>").description("Revoke an active delegation granted by this account").requiredOption("--space <space>", "Space name or ID containing the delegation").action(async (cid, options, cmd) => {
@@ -15409,7 +15834,7 @@ function registerAccountCommand(program) {
15409
15834
  assertOk(result);
15410
15835
  outputJson({ cid, space: options.space, revoked: true });
15411
15836
  } catch (error) {
15412
- handleError(error);
15837
+ return handleError(error);
15413
15838
  }
15414
15839
  });
15415
15840
  const index = account.command("index").description("Manage the materialized account SQLite index");
@@ -15420,7 +15845,7 @@ function registerAccountCommand(program) {
15420
15845
  assertOk(result);
15421
15846
  outputJson({ ...result.data, ensured: true });
15422
15847
  } catch (error) {
15423
- handleError(error);
15848
+ return handleError(error);
15424
15849
  }
15425
15850
  });
15426
15851
  index.command("status").description("Show account SQLite index sync status").action(async (_options, cmd) => {
@@ -15430,7 +15855,7 @@ function registerAccountCommand(program) {
15430
15855
  assertOk(result);
15431
15856
  outputJson(result.data);
15432
15857
  } catch (error) {
15433
- handleError(error);
15858
+ return handleError(error);
15434
15859
  }
15435
15860
  });
15436
15861
  index.command("rebuild").description("Rebuild account SQLite index from canonical account data").action(async (_options, cmd) => {
@@ -15440,7 +15865,7 @@ function registerAccountCommand(program) {
15440
15865
  assertOk(result);
15441
15866
  outputJson(result.data);
15442
15867
  } catch (error) {
15443
- handleError(error);
15868
+ return handleError(error);
15444
15869
  }
15445
15870
  });
15446
15871
  index.command("query <sql>").description("Query the materialized account SQLite index").option("--params <json>", "Bind parameters as a JSON array for ? placeholders").action(async (sql, options, cmd) => {
@@ -15451,7 +15876,7 @@ function registerAccountCommand(program) {
15451
15876
  assertOk(result);
15452
15877
  outputJson(result.data);
15453
15878
  } catch (error) {
15454
- handleError(error);
15879
+ return handleError(error);
15455
15880
  }
15456
15881
  });
15457
15882
  const billing = account.command("billing").description("Open account billing");
@@ -15463,7 +15888,7 @@ function registerAccountCommand(program) {
15463
15888
  }
15464
15889
  outputJson({ url: ACCOUNT_BILLING_URL, opened: Boolean(options.open) });
15465
15890
  } catch (error) {
15466
- handleError(error);
15891
+ return handleError(error);
15467
15892
  }
15468
15893
  });
15469
15894
  }
@@ -15529,15 +15954,15 @@ import { get as httpGet } from "http";
15529
15954
  import { get as httpsGet } from "https";
15530
15955
  import { spawn } from "child_process";
15531
15956
  import { mkdir as mkdir3, readFile as readFile5, writeFile as writeFile2 } from "fs/promises";
15532
- import { dirname as dirname2 } from "path";
15957
+ import { dirname as dirname3 } from "path";
15533
15958
  import { createInterface as createInterface2 } from "readline";
15534
15959
  import { grantAuthRequest, principalDidEquals, signedCompactUcanExpiry as signedCompactUcanExpiry3 } from "@tinycloud/node-sdk";
15535
15960
  import { invokeOperation } from "@tinycloud/operations";
15536
15961
 
15537
15962
  // src/lib/profile-replicas.ts
15538
- import { lstat as lstat2, readdir as readdir4, realpath } from "fs/promises";
15539
- import { join as join6, sep } from "path";
15540
- import { ProfileDeletedError as ProfileDeletedError2, profilePath as profilePath2, withProfileLock as withProfileLock3 } from "@tinycloud/operations/state";
15963
+ import { lstat as lstat2, readdir as readdir4, realpath, rm as rm4 } from "fs/promises";
15964
+ import { join as join8, relative, sep } from "path";
15965
+ import { ProfileDeletedError as ProfileDeletedError2, profilePath as profilePath3, withProfileLock as withProfileLock4 } from "@tinycloud/operations/state";
15541
15966
 
15542
15967
  // ../../node_modules/@noble/hashes/esm/blake3.js
15543
15968
  init_md();
@@ -15772,8 +16197,8 @@ var BLAKE3 = class _BLAKE3 extends BLAKE2 {
15772
16197
  }
15773
16198
  _cloneInto(to) {
15774
16199
  to = super._cloneInto(to);
15775
- const { IV, flags, state, chunkPos, posOut, chunkOut, stack, chunksDone } = this;
15776
- to.state.set(state.slice());
16200
+ const { IV, flags, state: state2, chunkPos, posOut, chunkOut, stack, chunksDone } = this;
16201
+ to.state.set(state2.slice());
15777
16202
  to.stack = stack.map((i) => Uint32Array.from(i));
15778
16203
  to.IV.set(IV);
15779
16204
  to.flags = flags;
@@ -17435,6 +17860,11 @@ function kvPrefixCovers(prefix, key) {
17435
17860
  if (prefix.endsWith("/")) return key.startsWith(prefix);
17436
17861
  return key === prefix || key.startsWith(`${prefix}/`);
17437
17862
  }
17863
+ function authorityPathCovers(grantPath, requestedPath) {
17864
+ if (grantPath === "" || grantPath === "/") return true;
17865
+ if (grantPath.endsWith("/")) return requestedPath.startsWith(grantPath);
17866
+ return requestedPath === grantPath;
17867
+ }
17438
17868
  function requiresSecretsOptIn(space, prefix) {
17439
17869
  if (space === "secrets" || space.endsWith(":secrets")) return true;
17440
17870
  const firstSegment = prefix.split("/", 1)[0];
@@ -17464,11 +17894,11 @@ function effectiveAuthority(input) {
17464
17894
  const notBefore = maxIso(input.window?.notBefore ?? null, input.grant?.notBefore ?? null);
17465
17895
  const expiresAt = minIso(input.window?.expiresAt ?? null, input.grant?.expiresAt ?? null);
17466
17896
  const retainUntil = input.policy === "retainAfterExpiry" ? input.window?.retainUntil ?? null : null;
17467
- let state = "valid";
17468
- if (input.revoked !== null) state = "revoked";
17469
- else if (notBefore !== null && input.now < Date.parse(notBefore)) state = "not-yet-valid";
17470
- else if (expiresAt !== null && input.now >= Date.parse(expiresAt)) state = "expired";
17471
- return { state, notBefore, expiresAt, retainUntil, retentionRevoked: (input.retentionRevoked ?? null) !== null };
17897
+ let state2 = "valid";
17898
+ if (input.revoked !== null) state2 = "revoked";
17899
+ else if (notBefore !== null && input.now < Date.parse(notBefore)) state2 = "not-yet-valid";
17900
+ else if (expiresAt !== null && input.now >= Date.parse(expiresAt)) state2 = "expired";
17901
+ return { state: state2, notBefore, expiresAt, retainUntil, retentionRevoked: (input.retentionRevoked ?? null) !== null };
17472
17902
  }
17473
17903
  function assertReadable(authority, now) {
17474
17904
  switch (authority.state) {
@@ -17533,29 +17963,29 @@ var Replica = class {
17533
17963
  this.#onCommit = options.onCommit;
17534
17964
  }
17535
17965
  async #state() {
17536
- const state = await this.#store.open();
17537
- if (state === null) throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, "The replica has not been created.");
17538
- return state;
17966
+ const state2 = await this.#store.open();
17967
+ if (state2 === null) throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, "The replica has not been created.");
17968
+ return state2;
17539
17969
  }
17540
17970
  /** Throws unless local reads are allowed now; returns the meta every read reports. */
17541
- #readMeta(state) {
17971
+ #readMeta(state2) {
17542
17972
  const now = this.#now();
17543
17973
  const authority = assertReadable(
17544
17974
  effectiveAuthority({
17545
- window: state.authority,
17546
- grant: state.grant,
17547
- revoked: state.revoked,
17548
- retentionRevoked: state.retentionRevoked,
17549
- policy: state.config.localReadPolicy,
17975
+ window: state2.authority,
17976
+ grant: state2.grant,
17977
+ revoked: state2.revoked,
17978
+ retentionRevoked: state2.retentionRevoked,
17979
+ policy: state2.config.localReadPolicy,
17550
17980
  now
17551
17981
  }),
17552
17982
  now
17553
17983
  );
17554
17984
  return {
17555
17985
  consistency: "observed",
17556
- source: { host: state.config.host, nodeDid: state.nodeDid, space: state.config.space, prefix: state.config.prefix },
17557
- asOf: state.lastSyncAt,
17558
- coverage: state.coverage,
17986
+ source: { host: state2.config.host, nodeDid: state2.nodeDid, space: state2.config.space, prefix: state2.config.prefix },
17987
+ asOf: state2.lastSyncAt,
17988
+ coverage: state2.coverage,
17559
17989
  authority
17560
17990
  };
17561
17991
  }
@@ -17568,13 +17998,13 @@ var Replica = class {
17568
17998
  return this.#readMeta(await this.#state());
17569
17999
  }
17570
18000
  async get(key, options = {}) {
17571
- const state = await this.#state();
17572
- this.#readMeta(state);
17573
- if (!kvPrefixCovers(state.config.prefix, key)) return { status: "not_covered", key, meta: await this.#confirmedMeta() };
18001
+ const state2 = await this.#state();
18002
+ this.#readMeta(state2);
18003
+ if (!kvPrefixCovers(state2.config.prefix, key)) return { status: "not_covered", key, meta: await this.#confirmedMeta() };
17574
18004
  for (let attempt = 0; ; attempt += 1) {
17575
18005
  const entry = await this.#store.get(key);
17576
18006
  if (entry === void 0) {
17577
- const status = state.coverage === "complete" ? "absent" : "coverage_incomplete";
18007
+ const status = state2.coverage === "complete" ? "absent" : "coverage_incomplete";
17578
18008
  return { status, key, meta: await this.#confirmedMeta() };
17579
18009
  }
17580
18010
  if (entry.deleted) return { status: "deleted", key, meta: await this.#confirmedMeta() };
@@ -17597,9 +18027,9 @@ var Replica = class {
17597
18027
  }
17598
18028
  /** Live keys under `prefix` (a plain string prefix within the replica's scope). */
17599
18029
  async list(options = {}) {
17600
- const state = await this.#state();
17601
- this.#readMeta(state);
17602
- const scope = state.config.prefix;
18030
+ const state2 = await this.#state();
18031
+ this.#readMeta(state2);
18032
+ const scope = state2.config.prefix;
17603
18033
  const prefix = options.prefix ?? "";
17604
18034
  if (prefix !== "" && !scope.startsWith(prefix) && !kvPrefixCovers(scope, prefix)) {
17605
18035
  throw new ReplicaError(
@@ -17706,9 +18136,9 @@ var Replica = class {
17706
18136
  return { notBefore: authority.notBefore, expiresAt: authority.expiresAt };
17707
18137
  }
17708
18138
  async #syncUnder(attempt, limit) {
17709
- const state = await this.#state();
17710
- const { config } = state;
17711
- this.#syncWindow(attempt.promote === null ? state.authority : null, attempt.grant);
18139
+ const state2 = await this.#state();
18140
+ const { config } = state2;
18141
+ this.#syncWindow(attempt.promote === null ? state2.authority : null, attempt.grant);
17712
18142
  const report = {
17713
18143
  pages: 0,
17714
18144
  changes: 0,
@@ -17717,14 +18147,14 @@ var Replica = class {
17717
18147
  contentMissing: 0,
17718
18148
  repaired: 0,
17719
18149
  resets: 0,
17720
- coverage: state.coverage,
18150
+ coverage: state2.coverage,
17721
18151
  cursorAdvanced: false,
17722
18152
  promotedGrant: false,
17723
18153
  blobsCollected: 0
17724
18154
  };
17725
- let cursor = state.cursor;
17726
- let pinned = state.nodeDid;
17727
- let coverage = state.coverage;
18155
+ let cursor = state2.cursor;
18156
+ let pinned = state2.nodeDid;
18157
+ let coverage = state2.coverage;
17728
18158
  const retentionGrantCid = config.retentionGrantCid;
17729
18159
  for (; ; ) {
17730
18160
  let page;
@@ -17885,17 +18315,17 @@ var Replica = class {
17885
18315
  }
17886
18316
  await this.#store.renewLease(attempt.lease, this.#leaseTtlMs);
17887
18317
  if (changes.length === 0) continue;
17888
- const state = await this.#state();
18318
+ const state2 = await this.#state();
17889
18319
  await this.#store.applyPage(attempt.lease, {
17890
18320
  changes,
17891
18321
  blobs,
17892
18322
  cursor,
17893
- source: { nodeDid: nodeDid ?? "", space: state.config.space, prefix: state.config.prefix },
18323
+ source: { nodeDid: nodeDid ?? "", space: state2.config.space, prefix: state2.config.prefix },
17894
18324
  authority: null,
17895
18325
  coverage,
17896
18326
  at: new Date(this.#now()).toISOString(),
17897
18327
  retentionGrantCid,
17898
- window: this.#syncWindow(state.authority, attempt.grant),
18328
+ window: this.#syncWindow(state2.authority, attempt.grant),
17899
18329
  complete: false,
17900
18330
  promoteGrant: null
17901
18331
  });
@@ -18062,7 +18492,7 @@ function kvPathIn(resource, space) {
18062
18492
  function grantCovers(grant, space, prefix, ability) {
18063
18493
  for (const [resource, abilities] of Object.entries(grant.att)) {
18064
18494
  const path = kvPathIn(resource, space);
18065
- if (path === void 0 || !kvPrefixCovers(path, prefix)) continue;
18495
+ if (path === void 0 || !authorityPathCovers(path, prefix)) continue;
18066
18496
  if (ability in abilities) return true;
18067
18497
  if (ability !== "tinycloud.kv/sync" && ability !== "tinycloud.kv/retain" && "tinycloud.kv/*" in abilities) return true;
18068
18498
  }
@@ -18084,8 +18514,9 @@ function assertGrantInstallable(grant, input) {
18084
18514
  `The grant was issued to ${grant.audience}, not this device (${input.deviceDid}).`
18085
18515
  );
18086
18516
  }
18517
+ const requiredPaths = input.prefix.endsWith("/") ? [input.prefix] : [input.prefix, `${input.prefix}/`];
18087
18518
  for (const ability of ["tinycloud.kv/sync", "tinycloud.kv/get"]) {
18088
- if (!grantCovers(grant, input.space, input.prefix, ability)) {
18519
+ if (!requiredPaths.every((path) => grantCovers(grant, input.space, path, ability))) {
18089
18520
  throw new ReplicaError(
18090
18521
  ReplicaErrorCode.GRANT_NOT_COVERING,
18091
18522
  `The grant does not carry ${ability} on ${input.space}/kv/${input.prefix}.`,
@@ -18238,7 +18669,7 @@ function kvSyncTransport(kv) {
18238
18669
  import { randomBytes as randomBytes2 } from "crypto";
18239
18670
  import { constants as fsConstants } from "fs";
18240
18671
  import { chmod as chmod3, link, mkdir as mkdir2, open as open2, readFile as readFile4, readdir as readdir3, rm as rm3, rmdir as rmdir2, stat as stat2, unlink } from "fs/promises";
18241
- import { join as join5 } from "path";
18672
+ import { join as join7 } from "path";
18242
18673
  var ReplicaErrorCode2 = {
18243
18674
  /** Another process holds the sync lease. */
18244
18675
  BUSY: "REPLICA_BUSY",
@@ -18325,11 +18756,11 @@ function effectiveAuthority2(input) {
18325
18756
  const notBefore = maxIso2(input.window?.notBefore ?? null, input.grant?.notBefore ?? null);
18326
18757
  const expiresAt = minIso2(input.window?.expiresAt ?? null, input.grant?.expiresAt ?? null);
18327
18758
  const retainUntil = input.policy === "retainAfterExpiry" ? input.window?.retainUntil ?? null : null;
18328
- let state = "valid";
18329
- if (input.revoked !== null) state = "revoked";
18330
- else if (notBefore !== null && input.now < Date.parse(notBefore)) state = "not-yet-valid";
18331
- else if (expiresAt !== null && input.now >= Date.parse(expiresAt)) state = "expired";
18332
- return { state, notBefore, expiresAt, retainUntil, retentionRevoked: (input.retentionRevoked ?? null) !== null };
18759
+ let state2 = "valid";
18760
+ if (input.revoked !== null) state2 = "revoked";
18761
+ else if (notBefore !== null && input.now < Date.parse(notBefore)) state2 = "not-yet-valid";
18762
+ else if (expiresAt !== null && input.now >= Date.parse(expiresAt)) state2 = "expired";
18763
+ return { state: state2, notBefore, expiresAt, retainUntil, retentionRevoked: (input.retentionRevoked ?? null) !== null };
18333
18764
  }
18334
18765
  function wrap(native) {
18335
18766
  const statements = /* @__PURE__ */ new Map();
@@ -18348,8 +18779,20 @@ function wrap(native) {
18348
18779
  get: (sql, ...params) => prepared(sql).get(...params) ?? void 0,
18349
18780
  all: (sql, ...params) => prepared(sql).all(...params),
18350
18781
  close: () => {
18351
- statements.clear();
18352
- native.close();
18782
+ let failure;
18783
+ try {
18784
+ for (const statement of statements.values()) {
18785
+ try {
18786
+ statement.finalize?.();
18787
+ } catch (error) {
18788
+ failure ??= error;
18789
+ }
18790
+ }
18791
+ } finally {
18792
+ statements.clear();
18793
+ native.close();
18794
+ }
18795
+ if (failure !== void 0) throw failure;
18353
18796
  }
18354
18797
  };
18355
18798
  }
@@ -18363,7 +18806,7 @@ async function loadSqlite() {
18363
18806
  if (runtime.Bun !== void 0) {
18364
18807
  const module2 = await load(["bun", "sqlite"].join(":"));
18365
18808
  const Database = module2.Database;
18366
- return (path) => wrap(new Database(path, { create: true }));
18809
+ return (path, options) => wrap(new Database(path, { create: !options?.readonly, ...options?.readonly ? { readonly: true } : {} }));
18367
18810
  }
18368
18811
  const version = runtime.process?.versions?.node ?? "0.0.0";
18369
18812
  if (!nodeSqliteSupported(version)) {
@@ -18388,7 +18831,7 @@ async function loadSqlite() {
18388
18831
  process2.emitWarning = emitWarning;
18389
18832
  }
18390
18833
  const DatabaseSync = module.DatabaseSync;
18391
- return (path) => wrap(new DatabaseSync(path));
18834
+ return (path, options) => wrap(options?.readonly ? new DatabaseSync(path, { readOnly: true }) : new DatabaseSync(path));
18392
18835
  }
18393
18836
  var SCHEMA_VERSION = 1;
18394
18837
  var DURABILITY = "sqlite-wal-synchronous-full+fsynced-blobs";
@@ -18506,12 +18949,60 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
18506
18949
  this.#faults = options[FAULTS] ?? {};
18507
18950
  this.#guard = options.guard ?? ((section) => section());
18508
18951
  }
18952
+ /**
18953
+ * Read-only status snapshot; does not create a replica, migrate, recover
18954
+ * revocation cleanup, or enter the mutation guard. An existing database is
18955
+ * opened normally in read-only WAL mode so committed WAL state is visible.
18956
+ */
18957
+ static async inspect(dir, options = {}) {
18958
+ const checkAbort = () => {
18959
+ if (options.signal?.aborted) throw options.signal.reason ?? new ReplicaError2(ReplicaErrorCode2.CLOSED, "Replica status inspection was cancelled.");
18960
+ };
18961
+ checkAbort();
18962
+ const dbPath = join7(dir, "replica.db");
18963
+ let ino;
18964
+ try {
18965
+ ino = (await stat2(dbPath)).ino;
18966
+ } catch (error) {
18967
+ if (errnoOf(error) === "ENOENT") return null;
18968
+ throw storageError(error, "Inspecting the replica");
18969
+ }
18970
+ checkAbort();
18971
+ const opener = await loadSqlite();
18972
+ checkAbort();
18973
+ let db;
18974
+ try {
18975
+ db = opener(dbPath, { readonly: true });
18976
+ db.exec("PRAGMA busy_timeout = 0");
18977
+ checkAbort();
18978
+ const store = new _SqliteReplicaStore(dir, db, ino, { create: false, ...options.now ? { now: options.now } : {} });
18979
+ for (let attempt = 0; attempt < 2; attempt++) {
18980
+ const before = db.get("PRAGMA data_version").data_version;
18981
+ const state2 = await store.open();
18982
+ checkAbort();
18983
+ if (state2 === null) return null;
18984
+ const status = await store.status();
18985
+ await new Promise((resolve5) => setImmediate(resolve5));
18986
+ checkAbort();
18987
+ const after = db.get("PRAGMA data_version").data_version;
18988
+ if (before === after) return { state: state2, status };
18989
+ }
18990
+ throw new ReplicaError2(ReplicaErrorCode2.BUSY, "The replica changed during status inspection.");
18991
+ } catch (error) {
18992
+ if (/SQLITE_BUSY|database is locked/i.test(error instanceof Error ? error.message : String(error))) {
18993
+ throw new ReplicaError2(ReplicaErrorCode2.BUSY, "The replica is busy during status inspection.", void 0, { cause: error });
18994
+ }
18995
+ throw storageError(error, "Inspecting the replica");
18996
+ } finally {
18997
+ db?.close();
18998
+ }
18999
+ }
18509
19000
  /**
18510
19001
  * Open the replica store in `dir`. With `create: false` a missing replica is
18511
19002
  * REPLICA_NOT_FOUND and nothing is written to disk.
18512
19003
  */
18513
19004
  static async open(dir, options) {
18514
- const dbPath = join5(dir, "replica.db");
19005
+ const dbPath = join7(dir, "replica.db");
18515
19006
  if (!options.create) {
18516
19007
  const exists = await stat2(dbPath).then(
18517
19008
  () => true,
@@ -18523,8 +19014,8 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
18523
19014
  let store;
18524
19015
  try {
18525
19016
  if (options.create) {
18526
- await mkdir2(join5(dir, "blobs", ".tmp"), { recursive: true, mode: 448 });
18527
- await Promise.all([chmod3(dir, 448), chmod3(join5(dir, "blobs"), 448), chmod3(join5(dir, "blobs", ".tmp"), 448)]);
19017
+ await mkdir2(join7(dir, "blobs", ".tmp"), { recursive: true, mode: 448 });
19018
+ await Promise.all([chmod3(dir, 448), chmod3(join7(dir, "blobs"), 448), chmod3(join7(dir, "blobs", ".tmp"), 448)]);
18528
19019
  const handle = await open2(dbPath, fsConstants.O_CREAT | fsConstants.O_RDWR, 384);
18529
19020
  await handle.close();
18530
19021
  await chmod3(dbPath, 384);
@@ -18565,7 +19056,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
18565
19056
  }
18566
19057
  /** Refuse to touch the filesystem for a replica that was deleted (or replaced) under us. */
18567
19058
  async #assertPresent() {
18568
- const ino = await stat2(join5(this.dir, "replica.db")).then(
19059
+ const ino = await stat2(join7(this.dir, "replica.db")).then(
18569
19060
  (stats) => stats.ino,
18570
19061
  () => void 0
18571
19062
  );
@@ -18797,7 +19288,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
18797
19288
  return held;
18798
19289
  }
18799
19290
  #blobPath(hash) {
18800
- return join5(this.dir, "blobs", hash.slice(0, 2), hash);
19291
+ return join7(this.dir, "blobs", hash.slice(0, 2), hash);
18801
19292
  }
18802
19293
  /**
18803
19294
  * Make every blob durable before any row references it: write to a fresh
@@ -18814,11 +19305,11 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
18814
19305
  () => false
18815
19306
  );
18816
19307
  if (exists) continue;
18817
- const shard = join5(this.dir, "blobs", hash.slice(0, 2));
19308
+ const shard = join7(this.dir, "blobs", hash.slice(0, 2));
18818
19309
  await mkdir2(shard, { mode: 448 }).catch((error) => {
18819
19310
  if (errnoOf(error) !== "EEXIST") throw error;
18820
19311
  });
18821
- const temp = join5(this.dir, "blobs", ".tmp", `${hash}.${randomBytes2(6).toString("hex")}`);
19312
+ const temp = join7(this.dir, "blobs", ".tmp", `${hash}.${randomBytes2(6).toString("hex")}`);
18822
19313
  const handle = await open2(temp, fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_WRONLY, 384);
18823
19314
  try {
18824
19315
  await handle.writeFile(bytes);
@@ -18834,7 +19325,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
18834
19325
  await unlink(temp);
18835
19326
  }
18836
19327
  dirs.add(shard);
18837
- dirs.add(join5(this.dir, "blobs"));
19328
+ dirs.add(join7(this.dir, "blobs"));
18838
19329
  }
18839
19330
  for (const dir of dirs) {
18840
19331
  const handle = await open2(dir, fsConstants.O_RDONLY);
@@ -18942,12 +19433,12 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
18942
19433
  /** Unlink every blob not in `keep`, and every temp file. Only a missing directory counts as empty. */
18943
19434
  async #unlinkBlobs(keep) {
18944
19435
  let removed = 0;
18945
- const root = join5(this.dir, "blobs");
19436
+ const root = join7(this.dir, "blobs");
18946
19437
  for (const shard of await entriesOf(root)) {
18947
- const shardPath = join5(root, shard);
19438
+ const shardPath = join7(root, shard);
18948
19439
  if (shard === ".tmp") {
18949
19440
  for (const name of await entriesOf(shardPath)) {
18950
- await rm3(join5(shardPath, name), { force: true });
19441
+ await rm3(join7(shardPath, name), { force: true });
18951
19442
  removed += 1;
18952
19443
  }
18953
19444
  continue;
@@ -18958,7 +19449,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
18958
19449
  kept += 1;
18959
19450
  continue;
18960
19451
  }
18961
- await rm3(join5(shardPath, name), { force: true });
19452
+ await rm3(join7(shardPath, name), { force: true });
18962
19453
  removed += 1;
18963
19454
  }
18964
19455
  if (kept === 0) await rmdir2(shardPath).catch(() => void 0);
@@ -18967,9 +19458,9 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
18967
19458
  }
18968
19459
  /** Whether any content file (a blob or a temp file) is on disk. */
18969
19460
  async #holdsContentFiles() {
18970
- const root = join5(this.dir, "blobs");
19461
+ const root = join7(this.dir, "blobs");
18971
19462
  for (const shard of await entriesOf(root)) {
18972
- if ((await entriesOf(join5(root, shard))).length > 0) return true;
19463
+ if ((await entriesOf(join7(root, shard))).length > 0) return true;
18973
19464
  }
18974
19465
  return false;
18975
19466
  }
@@ -19150,8 +19641,8 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
19150
19641
  }
19151
19642
  }
19152
19643
  async status(now = this.#now()) {
19153
- const state = await this.open();
19154
- if (state === null) throw new ReplicaError2(ReplicaErrorCode2.NOT_FOUND, "The replica has not been created.");
19644
+ const state2 = await this.open();
19645
+ if (state2 === null) throw new ReplicaError2(ReplicaErrorCode2.NOT_FOUND, "The replica has not been created.");
19155
19646
  const row = this.#row();
19156
19647
  const counts = this.#db.get(
19157
19648
  `SELECT
@@ -19162,41 +19653,41 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
19162
19653
  FROM entry`
19163
19654
  );
19164
19655
  const { retentionRevoked: _retentionRevoked, ...authority } = effectiveAuthority2({
19165
- window: state.authority,
19166
- grant: state.grant,
19167
- revoked: state.revoked,
19168
- retentionRevoked: state.retentionRevoked,
19169
- policy: state.config.localReadPolicy,
19656
+ window: state2.authority,
19657
+ grant: state2.grant,
19658
+ revoked: state2.revoked,
19659
+ retentionRevoked: state2.retentionRevoked,
19660
+ policy: state2.config.localReadPolicy,
19170
19661
  now
19171
19662
  });
19172
19663
  return {
19173
- name: state.config.name,
19174
- replicaId: state.config.replicaId,
19175
- source: { host: state.config.host, nodeDid: state.nodeDid, space: state.config.space, prefix: state.config.prefix },
19664
+ name: state2.config.name,
19665
+ replicaId: state2.config.replicaId,
19666
+ source: { host: state2.config.host, nodeDid: state2.nodeDid, space: state2.config.space, prefix: state2.config.prefix },
19176
19667
  device: {
19177
- did: state.config.deviceDid,
19178
- delegationCid: state.grant?.cid ?? null,
19179
- pendingDelegationCid: state.pendingGrant?.cid ?? null,
19180
- pendingDelegationError: state.pendingGrantError
19668
+ did: state2.config.deviceDid,
19669
+ delegationCid: state2.grant?.cid ?? null,
19670
+ pendingDelegationCid: state2.pendingGrant?.cid ?? null,
19671
+ pendingDelegationError: state2.pendingGrantError
19181
19672
  },
19182
19673
  authority: {
19183
19674
  ...authority,
19184
- localReadPolicy: state.config.localReadPolicy,
19185
- retentionGrantCid: state.config.retentionGrantCid,
19186
- revokedDetail: state.revoked,
19187
- retentionRevokedDetail: state.retentionRevoked
19675
+ localReadPolicy: state2.config.localReadPolicy,
19676
+ retentionGrantCid: state2.config.retentionGrantCid,
19677
+ revokedDetail: state2.revoked,
19678
+ retentionRevokedDetail: state2.retentionRevoked
19188
19679
  },
19189
19680
  consistency: "observed",
19190
- coverage: state.coverage,
19191
- lastSyncAt: state.lastSyncAt,
19192
- lastCompleteAt: state.lastCompleteAt,
19681
+ coverage: state2.coverage,
19682
+ lastSyncAt: state2.lastSyncAt,
19683
+ lastCompleteAt: state2.lastCompleteAt,
19193
19684
  counts: { keys: Number(counts.keys), contentMissing: Number(counts.missing), tombstones: Number(counts.tombstones) },
19194
19685
  bytes: Number(counts.bytes),
19195
19686
  durability: DURABILITY,
19196
19687
  syncing: row.lease_holder !== null && row.lease_expires_at !== null && row.lease_expires_at > now,
19197
19688
  purgePending: row.purge_pending === 1,
19198
- lastError: state.lastError,
19199
- lastReset: state.lastReset
19689
+ lastError: state2.lastError,
19690
+ lastReset: state2.lastReset
19200
19691
  };
19201
19692
  }
19202
19693
  async close() {
@@ -19229,14 +19720,14 @@ async function safeReplicaRoot(profile, root) {
19229
19720
  if (!rootStats.isDirectory()) {
19230
19721
  throw refuseUnsafeReplicaPath(profile, `the replicas path "${root}" is not a real directory`);
19231
19722
  }
19232
- const [profileRealPath, rootRealPath] = await Promise.all([realpath(profilePath2(profile)), realpath(root)]);
19723
+ const [profileRealPath, rootRealPath] = await Promise.all([realpath(profilePath3(profile)), realpath(root)]);
19233
19724
  if (!rootRealPath.startsWith(profileRealPath + sep)) {
19234
19725
  throw refuseUnsafeReplicaPath(profile, `the replicas directory "${root}" resolves outside the selected profile`);
19235
19726
  }
19236
19727
  return rootRealPath;
19237
19728
  }
19238
19729
  async function safeReplicaEntry(profile, root, entry) {
19239
- const entryPath = join6(root, entry.name);
19730
+ const entryPath = join8(root, entry.name);
19240
19731
  const rootRealPath = await safeReplicaRoot(profile, root);
19241
19732
  if (rootRealPath === null) {
19242
19733
  throw refuseUnsafeReplicaPath(profile, `the replicas directory "${root}" disappeared`);
@@ -19254,9 +19745,9 @@ async function safeReplicaEntry(profile, root, entry) {
19254
19745
  }
19255
19746
  return entryPath;
19256
19747
  }
19257
- async function removeProfileReplicas(profile) {
19258
- return withProfileLock3(profile, async () => {
19259
- const root = join6(profilePath2(profile), "replicas");
19748
+ async function removeProfileReplicas(profile, heldLeases) {
19749
+ return withProfileLock4(profile, async () => {
19750
+ const root = join8(profilePath3(profile), "replicas");
19260
19751
  if (await safeReplicaRoot(profile, root) === null) return [];
19261
19752
  const replicas = (await readdir4(root, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
19262
19753
  if (replicas.length === 0) return [];
@@ -19270,7 +19761,7 @@ async function removeProfileReplicas(profile) {
19270
19761
  create: false,
19271
19762
  guard: async (action) => {
19272
19763
  try {
19273
- return await withProfileLock3(profile, action, { requireProfile: true });
19764
+ return await withProfileLock4(profile, action, { requireProfile: true });
19274
19765
  } catch (error) {
19275
19766
  if (error instanceof ProfileDeletedError2) {
19276
19767
  throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, `Profile "${profile}" was deleted; its replicas are gone and nothing was written.`);
@@ -19283,7 +19774,7 @@ async function removeProfileReplicas(profile) {
19283
19774
  if (await store.open() === null) {
19284
19775
  throw new CLIError("REPLICA_PURGE_FAILED", `Replica "${entry.name}" has no initialized store; logout cleared the session but removed no replicas.`, ExitCode.ERROR);
19285
19776
  }
19286
- const lease = await store.acquireSyncLease(SYNC_LEASE_MS);
19777
+ const lease = heldLeases?.get(entryPath) ?? await store.acquireSyncLease(SYNC_LEASE_MS);
19287
19778
  if (lease === null) {
19288
19779
  throw new CLIError("REPLICA_BUSY", `Cannot log out: replica "${entry.name}" is syncing. The session was cleared and no replicas were removed.`, ExitCode.ERROR);
19289
19780
  }
@@ -19295,7 +19786,7 @@ async function removeProfileReplicas(profile) {
19295
19786
  await replica.store.destroy(replica.lease);
19296
19787
  removed.push(replica.name);
19297
19788
  } catch (error) {
19298
- const replicaStillExists = await lstat2(join6(root, replica.name)).then(
19789
+ const replicaStillExists = await lstat2(join8(root, replica.name)).then(
19299
19790
  () => true,
19300
19791
  (statError) => statError.code !== "ENOENT"
19301
19792
  );
@@ -19318,8 +19809,248 @@ async function removeProfileReplicas(profile) {
19318
19809
  ExitCode.ERROR
19319
19810
  );
19320
19811
  } finally {
19812
+ await Promise.all(opened.map(async ({ name, store, lease }) => {
19813
+ if (lease !== null && heldLeases?.has(join8(root, name)) !== true) await store.releaseLease(lease).catch(() => void 0);
19814
+ await store.close().catch(() => void 0);
19815
+ }));
19816
+ }
19817
+ });
19818
+ }
19819
+ async function removeProfileReplication(profile, heldLeases) {
19820
+ return withProfileLock4(profile, async () => {
19821
+ const root = join8(profilePath3(profile), "replication");
19822
+ const rootReal = await safeReplicaRoot(profile, root);
19823
+ if (rootReal === null) return [];
19824
+ const profileReal = await realpath(profilePath3(profile));
19825
+ const partitions = await readdir4(root, { withFileTypes: true });
19826
+ const opened = [];
19827
+ const removed = [];
19828
+ let deleting = false;
19829
+ try {
19830
+ for (const partition of partitions) {
19831
+ const partitionPath = join8(root, partition.name);
19832
+ const partitionStats = await lstat2(partitionPath);
19833
+ if (partition.isSymbolicLink() || partitionStats.isSymbolicLink()) {
19834
+ throw refuseUnsafeReplicaPath(profile, `replication entry "${partition.name}" is a symlink`);
19835
+ }
19836
+ if (partitionStats.isDirectory()) {
19837
+ const partitionReal = await realpath(partitionPath);
19838
+ if (!partitionReal.startsWith(rootReal + sep) || !partitionReal.startsWith(profileReal + sep)) {
19839
+ throw refuseUnsafeReplicaPath(profile, `replication entry "${partition.name}" resolves outside the selected profile`);
19840
+ }
19841
+ const replicasRoot2 = join8(partitionPath, "replicas");
19842
+ const replicasStats = await lstat2(replicasRoot2).catch((error) => {
19843
+ if (error.code === "ENOENT") return null;
19844
+ throw error;
19845
+ });
19846
+ if (!replicasStats) continue;
19847
+ if (replicasStats.isSymbolicLink() || !replicasStats.isDirectory()) {
19848
+ throw refuseUnsafeReplicaPath(profile, `replication replicas path "${replicasRoot2}" is not a real directory`);
19849
+ }
19850
+ const replicas = await readdir4(replicasRoot2, { withFileTypes: true });
19851
+ for (const replica of replicas) {
19852
+ const replicaPath = join8(replicasRoot2, replica.name);
19853
+ const replicaStats = await lstat2(replicaPath);
19854
+ if (replica.isSymbolicLink() || replicaStats.isSymbolicLink() || !replicaStats.isDirectory()) {
19855
+ throw refuseUnsafeReplicaPath(profile, `replication replica "${replica.name}" is not a real directory`);
19856
+ }
19857
+ const replicaReal = await realpath(replicaPath);
19858
+ if (!replicaReal.startsWith(rootReal + sep)) {
19859
+ throw refuseUnsafeReplicaPath(profile, `replication replica "${replica.name}" resolves outside the replication root`);
19860
+ }
19861
+ const database = await lstat2(join8(replicaPath, "replica.db")).catch((error) => {
19862
+ if (error.code === "ENOENT") return null;
19863
+ throw error;
19864
+ });
19865
+ if (!database) continue;
19866
+ if (database.isSymbolicLink() || !database.isFile()) {
19867
+ throw refuseUnsafeReplicaPath(profile, `replication database "${replica.name}" is not a regular file`);
19868
+ }
19869
+ }
19870
+ } else if (!partitionStats.isFile()) {
19871
+ throw refuseUnsafeReplicaPath(profile, `replication path "${partition.name}" is not a regular file or directory`);
19872
+ }
19873
+ }
19874
+ const hasReplicaDatabase = await Promise.all(partitions.filter((entry) => entry.isDirectory()).map(async (partition) => {
19875
+ return Promise.all((await readdir4(join8(root, partition.name, "replicas"), { withFileTypes: true }).catch(() => [])).filter((entry) => entry.isDirectory()).map((entry) => lstat2(join8(root, partition.name, "replicas", entry.name, "replica.db")).then(() => true, () => false)));
19876
+ })).then((values) => values.some((partition) => partition.some(Boolean)));
19877
+ if (hasReplicaDatabase) await loadSqlite();
19878
+ for (const partition of partitions.filter((entry) => entry.isDirectory())) {
19879
+ const replicasRoot2 = join8(root, partition.name, "replicas");
19880
+ for (const entry of await readdir4(replicasRoot2, { withFileTypes: true }).catch(() => [])) {
19881
+ if (!entry.isDirectory()) continue;
19882
+ const database = await lstat2(join8(replicasRoot2, entry.name, "replica.db")).catch((error) => {
19883
+ if (error.code === "ENOENT") return null;
19884
+ throw error;
19885
+ });
19886
+ if (!database) continue;
19887
+ const name = `replication/${partition.name}/${entry.name}`;
19888
+ const store = await SqliteReplicaStore.open(join8(replicasRoot2, entry.name), { create: false, guard: async (action) => withProfileLock4(profile, action, { requireProfile: true }) });
19889
+ if (await store.open() === null) {
19890
+ await store.close();
19891
+ throw new CLIError("REPLICA_PURGE_FAILED", `Logout cleared the session, but flag replica "${name}" has no initialized store.`, ExitCode.ERROR);
19892
+ }
19893
+ const lease = heldLeases?.get(join8(replicasRoot2, entry.name)) ?? await store.acquireSyncLease(SYNC_LEASE_MS);
19894
+ if (lease === null) {
19895
+ await store.close();
19896
+ throw new CLIError("REPLICA_BUSY", `Cannot log out: flag replica "${name}" is syncing. The session was cleared and no replicas were removed.`, ExitCode.ERROR);
19897
+ }
19898
+ opened.push({ name, path: join8(replicasRoot2, entry.name), store, lease });
19899
+ }
19900
+ }
19901
+ deleting = true;
19902
+ for (const item of opened) {
19903
+ await item.store.destroy(item.lease);
19904
+ removed.push(item.name);
19905
+ }
19906
+ await rm4(root, { recursive: true });
19907
+ return removed;
19908
+ } catch (error) {
19909
+ if (deleting) {
19910
+ const remaining = opened.filter(({ name }) => !removed.includes(name)).map(({ name }) => name);
19911
+ throw new CLIError(
19912
+ "REPLICA_PURGE_FAILED",
19913
+ `Logout cleared the session, but replication cleanup failed. Removed: ${removed.join(", ") || "none"}. Not removed: ${remaining.join(", ") || "none"}. ${error instanceof Error ? error.message : String(error)}`,
19914
+ ExitCode.ERROR,
19915
+ { replicasRemoved: removed, replicasRemaining: remaining }
19916
+ );
19917
+ }
19918
+ if (error instanceof CLIError) throw error;
19919
+ throw new CLIError("REPLICA_PURGE_FAILED", `Logout cleared the session, but replication cleanup failed; no flag replicas were removed. ${error instanceof Error ? error.message : String(error)}`, ExitCode.ERROR);
19920
+ } finally {
19921
+ await Promise.all(opened.map(async ({ path, store, lease }) => {
19922
+ if (heldLeases?.has(path) !== true) await store.releaseLease(lease).catch(() => void 0);
19923
+ await store.close().catch(() => void 0);
19924
+ }));
19925
+ }
19926
+ });
19927
+ }
19928
+ async function acquireLogoutLeases(profile) {
19929
+ return withProfileLock4(profile, async () => {
19930
+ let sqliteLoaded = false;
19931
+ const opened = [];
19932
+ try {
19933
+ const legacyRoot = join8(profilePath3(profile), "replicas");
19934
+ if (await safeReplicaRoot(profile, legacyRoot) !== null) {
19935
+ const entries = (await readdir4(legacyRoot, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
19936
+ for (const entry of entries) {
19937
+ const path = await safeReplicaEntry(profile, legacyRoot, entry);
19938
+ if (!sqliteLoaded) {
19939
+ await loadSqlite();
19940
+ sqliteLoaded = true;
19941
+ }
19942
+ const store = await SqliteReplicaStore.open(path, { create: false, guard: async (action) => withProfileLock4(profile, action, { requireProfile: true }) });
19943
+ if (await store.open() === null) {
19944
+ await store.close();
19945
+ throw new CLIError("REPLICA_PURGE_FAILED", `Replica "${entry.name}" has no initialized store; logout cleared the session but removed no replicas.`, ExitCode.ERROR);
19946
+ }
19947
+ const lease = await store.acquireSyncLease(SYNC_LEASE_MS);
19948
+ if (lease === null) {
19949
+ await store.close();
19950
+ throw new CLIError("REPLICA_BUSY", `Cannot log out: replica "${entry.name}" is syncing. The session was cleared and no replicas were removed.`, ExitCode.ERROR);
19951
+ }
19952
+ opened.push({ path, store, lease });
19953
+ }
19954
+ }
19955
+ const replicationRoot = join8(profilePath3(profile), "replication");
19956
+ const replicationReal = await safeReplicaRoot(profile, replicationRoot);
19957
+ if (replicationReal !== null) {
19958
+ const partitions = await readdir4(replicationRoot, { withFileTypes: true });
19959
+ for (const partition of partitions) {
19960
+ const partitionPath = join8(replicationRoot, partition.name);
19961
+ const partitionStats = await lstat2(partitionPath);
19962
+ if (partition.isSymbolicLink() || partitionStats.isSymbolicLink()) {
19963
+ throw refuseUnsafeReplicaPath(profile, `replication entry "${partition.name}" is a symlink`);
19964
+ }
19965
+ if (!partitionStats.isDirectory()) {
19966
+ if (!partitionStats.isFile()) throw refuseUnsafeReplicaPath(profile, `replication path "${partition.name}" is not a regular file or directory`);
19967
+ continue;
19968
+ }
19969
+ const partitionReal = await realpath(partitionPath);
19970
+ if (!partitionReal.startsWith(replicationReal + sep)) {
19971
+ throw refuseUnsafeReplicaPath(profile, `replication entry "${partition.name}" resolves outside the replication root`);
19972
+ }
19973
+ const replicasRoot2 = join8(partitionPath, "replicas");
19974
+ const replicasStats = await lstat2(replicasRoot2).catch((error) => {
19975
+ if (error.code === "ENOENT") return null;
19976
+ throw error;
19977
+ });
19978
+ if (!replicasStats) continue;
19979
+ if (replicasStats.isSymbolicLink() || !replicasStats.isDirectory()) {
19980
+ throw refuseUnsafeReplicaPath(profile, `replication replicas path "${replicasRoot2}" is not a real directory`);
19981
+ }
19982
+ for (const replica of await readdir4(replicasRoot2, { withFileTypes: true })) {
19983
+ const path = join8(replicasRoot2, replica.name);
19984
+ const stats = await lstat2(path);
19985
+ if (!replica.isDirectory() || replica.isSymbolicLink() || stats.isSymbolicLink() || !stats.isDirectory()) {
19986
+ throw refuseUnsafeReplicaPath(profile, `replication replica "${replica.name}" is not a real directory`);
19987
+ }
19988
+ const database = await lstat2(join8(path, "replica.db")).catch((error) => {
19989
+ if (error.code === "ENOENT") return null;
19990
+ throw error;
19991
+ });
19992
+ if (!database) continue;
19993
+ if (database.isSymbolicLink() || !database.isFile()) {
19994
+ throw refuseUnsafeReplicaPath(profile, `replication database "${replica.name}" is not a regular file`);
19995
+ }
19996
+ const replicaReal = await realpath(path);
19997
+ if (!replicaReal.startsWith(replicationReal + sep)) {
19998
+ throw refuseUnsafeReplicaPath(profile, `replication replica "${replica.name}" resolves outside the replication root`);
19999
+ }
20000
+ if (!sqliteLoaded) {
20001
+ await loadSqlite();
20002
+ sqliteLoaded = true;
20003
+ }
20004
+ const store = await SqliteReplicaStore.open(path, { create: false, guard: async (action) => withProfileLock4(profile, action, { requireProfile: true }) });
20005
+ if (await store.open() === null) {
20006
+ await store.close();
20007
+ throw new CLIError("REPLICA_PURGE_FAILED", `Logout cleared the session, but flag replica "replication/${partition.name}/${replica.name}" has no initialized store.`, ExitCode.ERROR);
20008
+ }
20009
+ const lease = await store.acquireSyncLease(SYNC_LEASE_MS);
20010
+ if (lease === null) {
20011
+ await store.close();
20012
+ throw new CLIError("REPLICA_BUSY", `Cannot log out: flag replica "replication/${partition.name}/${replica.name}" is syncing. The session was cleared and no replicas were removed.`, ExitCode.ERROR);
20013
+ }
20014
+ opened.push({ path, store, lease });
20015
+ }
20016
+ }
20017
+ }
20018
+ return opened;
20019
+ } catch (error) {
19321
20020
  await Promise.all(opened.map(async ({ store, lease }) => {
19322
- if (lease !== null) await store.releaseLease(lease).catch(() => void 0);
20021
+ await store.releaseLease(lease).catch(() => void 0);
20022
+ await store.close().catch(() => void 0);
20023
+ }));
20024
+ throw error;
20025
+ }
20026
+ });
20027
+ }
20028
+ async function removeProfileReplicasAndReplication(profile) {
20029
+ return withProfileLock4(profile, async () => {
20030
+ const acquired = await acquireLogoutLeases(profile);
20031
+ const heldLeases = new Map(acquired.map(({ path, lease }) => [path, lease]));
20032
+ let legacyRemoved = [];
20033
+ try {
20034
+ legacyRemoved = await removeProfileReplicas(profile, heldLeases);
20035
+ const replication = await removeProfileReplication(profile, heldLeases);
20036
+ return [...legacyRemoved, ...replication];
20037
+ } catch (error) {
20038
+ const partialRemoved = error instanceof CLIError && Array.isArray(error.metadata?.replicasRemoved) ? error.metadata.replicasRemoved.filter((name) => typeof name === "string") : [];
20039
+ const removed = [.../* @__PURE__ */ new Set([...legacyRemoved, ...partialRemoved])];
20040
+ const removedSet = new Set(removed);
20041
+ const remaining = acquired.map(({ path }) => {
20042
+ const name = relative(profilePath3(profile), path).split(sep).join("/");
20043
+ return name.startsWith("replication/") ? name.replace("/replicas/", "/") : name.split("/").at(-1);
20044
+ }).filter((name) => !removedSet.has(name));
20045
+ throw new CLIError(
20046
+ error instanceof CLIError ? error.code : "REPLICA_PURGE_FAILED",
20047
+ `Logout cleared the session, but replica cleanup failed. Removed: ${removed.join(", ") || "none"}. Not removed: ${remaining.join(", ") || "none"}.`,
20048
+ error instanceof CLIError ? error.exitCode : ExitCode.ERROR,
20049
+ { replicasRemoved: removed, replicasRemaining: remaining }
20050
+ );
20051
+ } finally {
20052
+ await Promise.all(acquired.map(async ({ store, lease }) => {
20053
+ await store.releaseLease(lease).catch(() => void 0);
19323
20054
  await store.close().catch(() => void 0);
19324
20055
  }));
19325
20056
  }
@@ -19648,6 +20379,7 @@ import {
19648
20379
  } from "crypto";
19649
20380
 
19650
20381
  // src/auth/local-key.ts
20382
+ init_replication_config();
19651
20383
  import { TCWSessionManager, importKey, initPanicHook } from "@tinycloud/node-sdk-wasm";
19652
20384
  import { PrivateKeySigner } from "@tinycloud/node-sdk";
19653
20385
  import { randomBytes as randomBytes3 } from "crypto";
@@ -19693,10 +20425,18 @@ async function generateLocalIdentity(chainId = 1) {
19693
20425
  }
19694
20426
  async function localKeySignIn(options) {
19695
20427
  const { TinyCloudNode: TinyCloudNode3 } = await import("@tinycloud/node-sdk");
20428
+ const profile = options.profile ?? "default";
20429
+ const replication = createCliReplicationConfig(
20430
+ profile,
20431
+ options.replication,
20432
+ { debug: false, quiet: true },
20433
+ options.replication !== void 0
20434
+ );
19696
20435
  const node = new TinyCloudNode3({
19697
20436
  privateKey: options.privateKey,
19698
20437
  host: options.host,
19699
- autoCreateSpace: true
20438
+ autoCreateSpace: true,
20439
+ ...replication ? { replication } : {}
19700
20440
  });
19701
20441
  await node.signIn();
19702
20442
  const address = await new PrivateKeySigner(options.privateKey).getAddress();
@@ -19826,6 +20566,15 @@ function sessionExpiresAt(session) {
19826
20566
  function ownerSpaceId(space, ownerDid) {
19827
20567
  return space.startsWith("tinycloud:") ? space : `tinycloud:${ownerDid.slice("did:".length)}:${space}`;
19828
20568
  }
20569
+ function sameLoginSpace(left, right, ownerDid) {
20570
+ const normalizedLeft = normalizePkhIdentifier(ownerDid === void 0 ? left : ownerSpaceId(left, ownerDid));
20571
+ const normalizedRight = normalizePkhIdentifier(ownerDid === void 0 ? right : ownerSpaceId(right, ownerDid));
20572
+ if (normalizedLeft === normalizedRight) return true;
20573
+ if (ownerDid !== void 0) return false;
20574
+ if (left.startsWith("tinycloud:") && !right.startsWith("tinycloud:")) return left.split(":").at(-1) === right;
20575
+ if (right.startsWith("tinycloud:") && !left.startsWith("tinycloud:")) return right.split(":").at(-1) === left;
20576
+ return false;
20577
+ }
19829
20578
  function permissionTuples(permissions, ownerDid) {
19830
20579
  return new Set(permissions.flatMap((permission) => actionTuples(permission, ownerDid)));
19831
20580
  }
@@ -20091,11 +20840,11 @@ function assertSessionReplaceable(profileName2, snapshot, ownerDid, scope, newEx
20091
20840
  ExitCode.USAGE_ERROR
20092
20841
  );
20093
20842
  }
20094
- async function restore(profileName2, state) {
20843
+ async function restore(profileName2, state2) {
20095
20844
  const writes = [
20096
- () => state.key === null ? ProfileManager.removeKey(profileName2) : ProfileManager.setKey(profileName2, state.key),
20097
- () => state.session === null ? ProfileManager.clearSession(profileName2) : ProfileManager.setSession(profileName2, state.session),
20098
- () => state.profile === null ? ProfileManager.removeProfileConfig(profileName2) : ProfileManager.setProfile(profileName2, state.profile)
20845
+ () => state2.key === null ? ProfileManager.removeKey(profileName2) : ProfileManager.setKey(profileName2, state2.key),
20846
+ () => state2.session === null ? ProfileManager.clearSession(profileName2) : ProfileManager.setSession(profileName2, state2.session),
20847
+ () => state2.profile === null ? ProfileManager.removeProfileConfig(profileName2) : ProfileManager.setProfile(profileName2, state2.profile)
20099
20848
  ];
20100
20849
  const failures = [];
20101
20850
  for (const write of writes) {
@@ -21017,6 +21766,79 @@ init_session_expired();
21017
21766
  init_delegation_expiry();
21018
21767
  init_space();
21019
21768
  init_permissions();
21769
+
21770
+ // src/auth/replication-login.ts
21771
+ import { grantPathsForSelector, isCapabilitySubset } from "@tinycloud/sdk-core";
21772
+ init_errors();
21773
+ init_constants();
21774
+ function unconstrained(caveats) {
21775
+ return (caveats ?? []).every((caveat) => Object.keys(caveat).length === 0);
21776
+ }
21777
+ function addReplicationLoginEntries(request, primarySpace, options) {
21778
+ const result = [...request];
21779
+ for (const prefix of options.prefixes) {
21780
+ if (requiresSecretsOptIn(primarySpace, prefix) && options.allowSecrets !== true) {
21781
+ throw new CLIError("SECRETS_OPT_IN_REQUIRED", `Replication prefix ${JSON.stringify(prefix)} in ${primarySpace} requires --replication-allow-secrets.`, ExitCode.USAGE_ERROR);
21782
+ }
21783
+ const requested = {
21784
+ service: "tinycloud.kv",
21785
+ space: primarySpace,
21786
+ path: prefix,
21787
+ actions: ["tinycloud.kv/get"]
21788
+ };
21789
+ const required = grantPathsForSelector(prefix).map((path) => ({ ...requested, path }));
21790
+ const matching = request.filter(
21791
+ (entry) => sameLoginSpace(entry.space ?? "", primarySpace, options.ownerDid)
21792
+ );
21793
+ const authority = matching.map((entry) => {
21794
+ const { caveats: _caveats, ...grant } = entry;
21795
+ return { ...grant, space: primarySpace };
21796
+ });
21797
+ if (!isCapabilitySubset(required, authority).subset) {
21798
+ const caveatedAtRequestedPath = matching.filter((entry) => !unconstrained(entry.caveats)).map((entry) => {
21799
+ const { caveats: _caveats, ...grant } = entry;
21800
+ return { ...grant, space: primarySpace };
21801
+ });
21802
+ if (isCapabilitySubset([requested], caveatedAtRequestedPath).subset) {
21803
+ throw new CLIError("REPLICATION_PREFIX_CAVEATED", `Replication prefix ${JSON.stringify(prefix)} is covered only by caveated get authority.`, ExitCode.USAGE_ERROR);
21804
+ }
21805
+ throw new CLIError("REPLICATION_PREFIX_OUTSIDE_SCOPE", `Replication prefix ${JSON.stringify(prefix)} has no covering get in the login request.`, ExitCode.USAGE_ERROR);
21806
+ }
21807
+ const unrestricted = matching.filter((entry) => unconstrained(entry.caveats)).map((entry) => {
21808
+ const { caveats: _caveats, ...grant } = entry;
21809
+ return { ...grant, space: primarySpace };
21810
+ });
21811
+ if (!isCapabilitySubset(required, unrestricted).subset) {
21812
+ throw new CLIError("REPLICATION_PREFIX_CAVEATED", `Replication prefix ${JSON.stringify(prefix)} is covered only by caveated get authority.`, ExitCode.USAGE_ERROR);
21813
+ }
21814
+ const service = "tinycloud.kv";
21815
+ const actions = [`${service}/get`, `${service}/sync`];
21816
+ for (const path of grantPathsForSelector(prefix)) {
21817
+ const existing = result.find((entry) => entry.service === service && entry.space === primarySpace && entry.path === path && actions.every((action) => entry.actions.includes(action)) && unconstrained(entry.caveats));
21818
+ if (!existing) result.push({ service, space: primarySpace, path, actions });
21819
+ }
21820
+ }
21821
+ return result;
21822
+ }
21823
+ function buildReplicationLoginRequest(request, options) {
21824
+ if (options.prefixes.length === 0) return request;
21825
+ if (request === void 0) {
21826
+ const space = options.ownerDid === void 0 ? "default" : ownerSpaceId("default", options.ownerDid);
21827
+ const defaults = options.ownerDid === void 0 ? [
21828
+ { service: "tinycloud.kv", space, path: "", actions: ["tinycloud.kv/put", "tinycloud.kv/get", "tinycloud.kv/del", "tinycloud.kv/list", "tinycloud.kv/metadata"] },
21829
+ { service: "tinycloud.sql", space, path: "", actions: ["tinycloud.sql/read", "tinycloud.sql/write", "tinycloud.sql/admin"] },
21830
+ { service: "tinycloud.capabilities", space, path: "", actions: ["tinycloud.capabilities/read"] }
21831
+ ] : ownerLoginPermissions(options.ownerDid);
21832
+ return addReplicationLoginEntries(defaults, space, options);
21833
+ }
21834
+ const primary = request.find((entry) => entry.service === "tinycloud.kv" && entry.space !== void 0)?.space ?? request.find((entry) => entry.space !== void 0)?.space;
21835
+ if (!primary) {
21836
+ throw new CLIError("INVALID_LOGIN_SCOPE", "Replication login needs a primary space in the requested manifest.", ExitCode.USAGE_ERROR);
21837
+ }
21838
+ return addReplicationLoginEntries(request, primary, options);
21839
+ }
21840
+
21841
+ // src/commands/auth.ts
21020
21842
  function resolveOpenKeyHost(profile) {
21021
21843
  return process.env.TC_OPENKEY_HOST ?? profile.openkeyHost ?? DEFAULT_OPENKEY_HOST;
21022
21844
  }
@@ -21049,7 +21871,7 @@ async function promptAuthMethod() {
21049
21871
  function registerAuthCommand(program, options) {
21050
21872
  const auth = program.command("auth").description("Authentication management");
21051
21873
  registerAuthGrantsCommand(auth, { openKeyAcquisition: options?.openKeyAcquisition });
21052
- auth.command("login").description("Authenticate with TinyCloud").option("--device", "Approve on another device (e.g. a phone) through OpenKey device authorization; requires a KV-scoped --manifest (SQL and secret decrypt need browser or --paste login)").option("--paste", "Use manual paste mode instead of browser callback").option("--no-popup", "Print the OpenKey URL without opening a browser").option("--method <method>", "Authentication method: local or openkey").option("--manifest <fileOrBase64>", `Request only this manifest's permissions (one space, plus raw encryption network entries such as a secrets decrypt grant); ${SHARE_PUBLISHING_MANIFEST_REF} covers \`tc share publish\``).option("--expiry <duration>", "OpenKey session lifetime, e.g. 1h or 7d (device login: at most 30d, default 30d)").option("--owner <did>", "Sign in as this owner (did:pkh:eip155:CHAIN:ADDRESS): OpenKey preselects that key, e.g. one that is not the account's primary key, and an approval by any other identity is refused. With --manifest, also names the secrets owner for a manifest's `secrets` on a profile with no recorded owner").option("--replace-session", "Scoped or device login: replace this profile's live session even though the new scope would narrow, change or shorten it (prefer a new profile)").action(async (options2, cmd) => {
21874
+ auth.command("login").description("Authenticate with TinyCloud").option("--device", "Approve on another device (e.g. a phone) through OpenKey device authorization; requires a KV-scoped --manifest (SQL and secret decrypt need browser or --paste login)").option("--paste", "Use manual paste mode instead of browser callback").option("--no-popup", "Print the OpenKey URL without opening a browser").option("--method <method>", "Authentication method: local or openkey").option("--manifest <fileOrBase64>", `Request only this manifest's permissions (one space, plus raw encryption network entries such as a secrets decrypt grant); ${SHARE_PUBLISHING_MANIFEST_REF} covers \`tc share publish\``).option("--expiry <duration>", "OpenKey session lifetime, e.g. 1h or 7d (device login: at most 30d, default 30d)").option("--owner <did>", "Sign in as this owner (did:pkh:eip155:CHAIN:ADDRESS): OpenKey preselects that key, e.g. one that is not the account's primary key, and an approval by any other identity is refused. With --manifest, also names the secrets owner for a manifest's `secrets` on a profile with no recorded owner").option("--replace-session", "Scoped or device login: replace this profile's live session even though the new scope would narrow, change or shorten it (prefer a new profile)").option("--replication-prefix <prefix>", "Add replication get+sync authority for this prefix (repeatable)", (prefix, values = []) => [...values, prefix]).option("--replication-allow-secrets", "Allow replication of secret material (network-wide decrypt warning)").action(async (options2, cmd) => {
21053
21875
  try {
21054
21876
  if (options2.device && options2.paste) {
21055
21877
  throw new CLIError("INVALID_ARGUMENT", "--device and --paste are mutually exclusive.", ExitCode.USAGE_ERROR);
@@ -21069,19 +21891,31 @@ function registerAuthCommand(program, options) {
21069
21891
  const ctx = await ProfileManager.resolveContext(globalOpts, { storedSession: "unused" });
21070
21892
  const owner = options2.owner === void 0 ? void 0 : canonicalOwnerDid(options2.owner);
21071
21893
  const permissions = options2.manifest ? await loadManifestPermissions(options2.manifest, ctx.profile, { allowLogicalSpaces: true, ownerDid: owner, device: options2.device === true }) : void 0;
21894
+ const profileConfig = await ProfileManager.getProfile(ctx.profile).catch((error) => {
21895
+ if (error instanceof CLIError && error.code === "PROFILE_NOT_FOUND") return void 0;
21896
+ throw error;
21897
+ });
21898
+ const replicationOptions = {
21899
+ prefixes: options2.replicationPrefix ?? profileConfig?.replication?.prefixes ?? [],
21900
+ ...options2.replicationAllowSecrets === true || profileConfig?.replication?.allowSecrets === true ? { allowSecrets: true } : {},
21901
+ ...owner === void 0 ? {} : { ownerDid: owner }
21902
+ };
21903
+ const loginPermissions = permissions === void 0 ? void 0 : buildReplicationLoginRequest(permissions, replicationOptions);
21072
21904
  const persistHost = globalOpts.host !== void 0;
21073
21905
  if (options2.device) {
21074
21906
  const { profile, result } = await loginWithDeviceAuthorization({
21075
21907
  profileName: ctx.profile,
21076
21908
  nodeOrigin: ctx.host,
21077
21909
  shareOrigin: DEFAULT_SHARE_ORIGIN,
21078
- permissions,
21910
+ permissions: loginPermissions,
21079
21911
  ...options2.expiry === void 0 ? {} : { expiry: parseRequestedExpiry(parseExpiryOption(options2.expiry)) },
21080
21912
  reason: "Allow this TinyCloud CLI profile to use the permissions in the requested manifest.",
21081
21913
  expectedOwner: owner,
21082
21914
  replaceSession: options2.replaceSession === true,
21083
21915
  persistHost
21084
21916
  });
21917
+ await persistReplicationLogin(ctx.profile, replicationOptions);
21918
+ if (replicationOptions.allowSecrets) writeReplicationSecretsWarning();
21085
21919
  reportDeclined(result.declined);
21086
21920
  outputJson({
21087
21921
  authenticated: true,
@@ -21120,20 +21954,26 @@ function registerAuthCommand(program, options) {
21120
21954
  );
21121
21955
  }
21122
21956
  if (method === "local") {
21123
- await handleLocalAuth(ctx.profile, ctx.host);
21957
+ buildReplicationLoginRequest(void 0, replicationOptions);
21958
+ await handleLocalAuth(ctx.profile, ctx.host, { replication: replicationOptions });
21959
+ await persistReplicationLogin(ctx.profile, replicationOptions);
21960
+ if (replicationOptions.allowSecrets) writeReplicationSecretsWarning();
21124
21961
  } else {
21125
21962
  await handleOpenKeyAuth(ctx.profile, ctx.host, {
21126
21963
  paste: options2.paste,
21127
21964
  noPopup: options2.popup === false,
21128
- permissions,
21965
+ permissions: loginPermissions,
21966
+ replication: replicationOptions,
21129
21967
  expiry: parseExpiryOption(options2.expiry),
21130
21968
  expectedOwner: owner,
21131
21969
  replaceSession: options2.replaceSession === true,
21132
21970
  persistHost
21133
21971
  });
21972
+ await persistReplicationLogin(ctx.profile, replicationOptions);
21973
+ if (replicationOptions.allowSecrets) writeReplicationSecretsWarning();
21134
21974
  }
21135
21975
  } catch (error) {
21136
- handleError(error);
21976
+ return handleError(error);
21137
21977
  }
21138
21978
  });
21139
21979
  auth.command("logout").description("Clear the local session and remove local replicas. Logout is local; delegations and grants on the node stay valid until they expire. Use `tc delegation revoke <cid>` to end access now.").option("--keep-replicas", "Keep this profile's local replicas").action(async (options2, cmd) => {
@@ -21142,7 +21982,7 @@ function registerAuthCommand(program, options) {
21142
21982
  const ctx = await ProfileManager.resolveContext(globalOpts, { storedSession: "unused" });
21143
21983
  await ProfileManager.getProfile(ctx.profile);
21144
21984
  await ProfileManager.clearSession(ctx.profile);
21145
- const replicasRemoved = options2.keepReplicas ? [] : await removeProfileReplicas(ctx.profile);
21985
+ const replicasRemoved = options2.keepReplicas ? [] : await removeProfileReplicasAndReplication(ctx.profile);
21146
21986
  outputJson({
21147
21987
  profile: ctx.profile,
21148
21988
  authenticated: false,
@@ -21151,7 +21991,7 @@ function registerAuthCommand(program, options) {
21151
21991
  warning: "Logout is local; delegations and grants on the node stay valid until they expire. Use `tc delegation revoke <cid>` to end access now."
21152
21992
  });
21153
21993
  } catch (error) {
21154
- handleError(error);
21994
+ return handleError(error);
21155
21995
  }
21156
21996
  });
21157
21997
  auth.command("rotate").description("Rotate the active profile session key").option("--paste", "Use manual paste mode instead of browser callback").option("--no-popup", "Print the OpenKey URL without opening a browser").action(async (options2, cmd) => {
@@ -21163,7 +22003,7 @@ function registerAuthCommand(program, options) {
21163
22003
  noPopup: options2.popup === false
21164
22004
  });
21165
22005
  } catch (error) {
21166
- handleError(error);
22006
+ return handleError(error);
21167
22007
  }
21168
22008
  });
21169
22009
  auth.command("status").description("Show current authentication state").action(async (_options, cmd) => {
@@ -21214,7 +22054,7 @@ function registerAuthCommand(program, options) {
21214
22054
  process.stdout.write(formatField("Has Key", hasKey !== null) + "\n");
21215
22055
  }
21216
22056
  } catch (error) {
21217
- handleError(error);
22057
+ return handleError(error);
21218
22058
  }
21219
22059
  });
21220
22060
  auth.command("request").description("Create a TinyCloud permission request artifact").option(
@@ -21367,7 +22207,7 @@ function registerAuthCommand(program, options) {
21367
22207
  expiry
21368
22208
  });
21369
22209
  } catch (error) {
21370
- handleError(error);
22210
+ return handleError(error);
21371
22211
  }
21372
22212
  });
21373
22213
  auth.command("import [source]").description("Import a TinyCloud delegation or permission request artifact").option("--stdin", "Read the JSON artifact from stdin").option("--paste", "Read the JSON artifact from stdin").action(async (source, options2, cmd) => {
@@ -21508,7 +22348,7 @@ function registerAuthCommand(program, options) {
21508
22348
  expiry: importExpiry.toISOString()
21509
22349
  });
21510
22350
  } catch (error) {
21511
- handleError(error);
22351
+ return handleError(error);
21512
22352
  }
21513
22353
  });
21514
22354
  auth.command("grant [request]").description("Grant a TinyCloud permission request artifact to its requester").option("--stdin", "Read the JSON request artifact from stdin").option("--paste", "Read the JSON request artifact from stdin").option("--yes", "Skip local-key TTY confirmation", false).action(async (source, options2, cmd) => {
@@ -21549,7 +22389,7 @@ function registerAuthCommand(program, options) {
21549
22389
  });
21550
22390
  outputJson(grant);
21551
22391
  } catch (error) {
21552
- handleError(error);
22392
+ return handleError(error);
21553
22393
  }
21554
22394
  });
21555
22395
  auth.command("retry [requestId]").description("Check whether a stored permission request is now satisfied").option("--last", "Use the latest stored permission request for this profile").option("--exec", "Run the captured command when the request is covered").action(async (requestId, options2, cmd) => {
@@ -21592,7 +22432,7 @@ function registerAuthCommand(program, options) {
21592
22432
  command: isPermissionRequestArtifact(artifact) ? artifact.command ?? null : null
21593
22433
  });
21594
22434
  } catch (error) {
21595
- handleError(error);
22435
+ return handleError(error);
21596
22436
  }
21597
22437
  });
21598
22438
  auth.command("caps").description("Show granted capabilities for the active session").option("--diff <spec>", "Show missing capabilities for a spec").option("--history", "Show recent permission grants").action(async (options2, cmd) => {
@@ -21650,7 +22490,7 @@ function registerAuthCommand(program, options) {
21650
22490
  ) + "\n");
21651
22491
  }
21652
22492
  } catch (error) {
21653
- handleError(error);
22493
+ return handleError(error);
21654
22494
  }
21655
22495
  });
21656
22496
  auth.command("whoami").description("Show identity information").action(async (_options, cmd) => {
@@ -21691,13 +22531,13 @@ function registerAuthCommand(program, options) {
21691
22531
  process.stdout.write(formatField("Authenticated", authenticated) + "\n");
21692
22532
  }
21693
22533
  } catch (error) {
21694
- handleError(error);
22534
+ return handleError(error);
21695
22535
  }
21696
22536
  });
21697
22537
  }
21698
22538
  async function emitPermissionRequestArtifact(artifact, emitOption) {
21699
22539
  if (typeof emitOption === "string" && emitOption.length > 0) {
21700
- await mkdir3(dirname2(emitOption), { recursive: true });
22540
+ await mkdir3(dirname3(emitOption), { recursive: true });
21701
22541
  await writeFile2(emitOption, JSON.stringify(artifact, null, 2) + "\n", "utf8");
21702
22542
  outputJson({
21703
22543
  emitted: true,
@@ -22178,8 +23018,13 @@ async function rotateAuthKey(profileName2, host, options = {}) {
22178
23018
  const { jwk, did } = await withSpinner("Generating session key...", async () => {
22179
23019
  return generateKey();
22180
23020
  });
23021
+ let permissions;
22181
23022
  await ProfileManager.withLock(profileName2, async () => {
22182
23023
  const current = await ProfileManager.getProfile(profileName2);
23024
+ if (current.replication?.prefixes.length) {
23025
+ const previousSession = await ProfileManager.getSession(profileName2);
23026
+ if (Array.isArray(previousSession?.permissions)) permissions = previousSession.permissions;
23027
+ }
22183
23028
  await ProfileManager.setKey(profileName2, jwk);
22184
23029
  await ProfileManager.clearSession(profileName2);
22185
23030
  await ProfileManager.setProfile(profileName2, {
@@ -22194,7 +23039,8 @@ async function rotateAuthKey(profileName2, host, options = {}) {
22194
23039
  });
22195
23040
  const result = await refreshOpenKeySession(profileName2, host, {
22196
23041
  paste: options.paste,
22197
- noPopup: options.noPopup
23042
+ noPopup: options.noPopup,
23043
+ permissions
22198
23044
  });
22199
23045
  outputRotationResult(result.profile, profileName2, oldDid, "openkey");
22200
23046
  }
@@ -22241,7 +23087,7 @@ async function handleLocalAuth(profileName2, host, options = {}) {
22241
23087
  let address;
22242
23088
  let did;
22243
23089
  let sessionDid = profile?.sessionDid;
22244
- if ((profile?.authMethod === "local" || posture === "local-owner-key") && profile.privateKey) {
23090
+ if (profile && (profile.authMethod === "local" || posture === "local-owner-key") && profile.privateKey) {
22245
23091
  privateKey = profile.privateKey;
22246
23092
  address = profile.address ?? await deriveAddress(privateKey);
22247
23093
  did = profile.did.startsWith("did:pkh:") ? profile.did : addressToDID(address, profile.chainId ?? DEFAULT_CHAIN_ID);
@@ -22275,7 +23121,12 @@ async function handleLocalAuth(profileName2, host, options = {}) {
22275
23121
  sessionDid ??= keyToDID(hasKey);
22276
23122
  }
22277
23123
  const sessionResult = await withSpinner("Signing in...", async () => {
22278
- return localKeySignIn({ privateKey, host });
23124
+ const replication = options.replication ?? profile?.replication;
23125
+ return localKeySignIn({
23126
+ privateKey,
23127
+ host,
23128
+ ...replication?.prefixes.length ? { profile: profileName2, replication } : {}
23129
+ });
22279
23130
  });
22280
23131
  const session = {
22281
23132
  authMethod: "local",
@@ -22353,6 +23204,22 @@ ${declined.map((permission) => ` ${compactPermission(permission)}`).join("\n")}
22353
23204
  `);
22354
23205
  }
22355
23206
  }
23207
+ async function persistReplicationLogin(profileName2, settings) {
23208
+ await ProfileManager.updateProfile(profileName2, (profile) => {
23209
+ const { replication: _previous, ...rest } = profile;
23210
+ return settings.prefixes.length === 0 ? rest : {
23211
+ ...rest,
23212
+ replication: {
23213
+ prefixes: [...settings.prefixes],
23214
+ ...settings.allowSecrets === true ? { allowSecrets: true } : {}
23215
+ }
23216
+ };
23217
+ });
23218
+ }
23219
+ function writeReplicationSecretsWarning() {
23220
+ process.stderr.write(`${theme.warn("Warning:")} replication copies encrypted secret material to this device. The allow-secrets option does not grant decrypt.
23221
+ `);
23222
+ }
22356
23223
  async function refreshOpenKeySession(profileName2, host, options = {}) {
22357
23224
  const snapshot = await readProfileSnapshot(profileName2);
22358
23225
  const profile = snapshot.profile ?? await ProfileManager.getProfile(profileName2);
@@ -22364,14 +23231,22 @@ async function refreshOpenKeySession(profileName2, host, options = {}) {
22364
23231
  ExitCode.AUTH_REQUIRED
22365
23232
  );
22366
23233
  }
22367
- if (options.permissions !== void 0) validateLoginPermissions(options.permissions);
22368
- const permissions = options.permissions === void 0 ? void 0 : scopedLoginPermissions(options.permissions);
22369
- if (permissions !== void 0) {
22370
- assertNotLocalOwner(profileName2, profile, "Scoped browser login");
22371
- }
22372
23234
  const expiry = options.expiry === void 0 ? void 0 : parseRequestedExpiry(options.expiry);
22373
23235
  const openKeyExpiry = expiry === void 0 ? void 0 : openKeyExpiryParam(expiry);
22374
23236
  const expectedOwner = expectedOwnerFor(profileName2, profile, options.expectedOwner);
23237
+ const replication = options.replication ?? profile.replication ?? { prefixes: [] };
23238
+ const previousSession = options.replication === void 0 && profile.replication?.prefixes.length ? await ProfileManager.getSession(profileName2) : null;
23239
+ const rememberedPermissions = Array.isArray(previousSession?.permissions) ? previousSession.permissions : void 0;
23240
+ const baseRequest = options.permissions ?? rememberedPermissions;
23241
+ const requested = buildReplicationLoginRequest(baseRequest, {
23242
+ ...replication,
23243
+ ...expectedOwner === void 0 ? {} : { ownerDid: expectedOwner }
23244
+ });
23245
+ if (requested !== void 0) validateLoginPermissions(requested);
23246
+ const permissions = requested === void 0 ? void 0 : scopedLoginPermissions(requested);
23247
+ if (permissions !== void 0) {
23248
+ assertNotLocalOwner(profileName2, profile, "Scoped browser login");
23249
+ }
22375
23250
  if (permissions !== void 0 && options.replaceSession !== true) {
22376
23251
  const estimatedExpiry = expiry === void 0 ? void 0 : new Date(Date.now() + expiry.durationMs).toISOString();
22377
23252
  assertSessionReplaceable(profileName2, snapshot, expectedOwner, permissions, estimatedExpiry);
@@ -22500,6 +23375,9 @@ _tc() {
22500
23375
  '(-v --verbose)'{-v,--verbose}'[Enable verbose output]' \\
22501
23376
  '--no-cache[Disable caching]' \\
22502
23377
  '(-q --quiet)'{-q,--quiet}'[Suppress non-essential output]' \\
23378
+ '--replication[Enable read-through replication]' \\
23379
+ '--no-replication[Disable replication]' \\
23380
+ '--replication-debug[Write replication diagnostics to stderr]' \\
22503
23381
  '1:command:->cmd' \\
22504
23382
  '*::arg:->args'
22505
23383
 
@@ -22559,6 +23437,9 @@ complete -c tc -l host -s H -d "TinyCloud node URL"
22559
23437
  complete -c tc -l verbose -s v -d "Enable verbose output"
22560
23438
  complete -c tc -l no-cache -d "Disable caching"
22561
23439
  complete -c tc -l quiet -s q -d "Suppress non-essential output"
23440
+ complete -c tc -l replication -d "Enable read-through replication"
23441
+ complete -c tc -l no-replication -d "Disable replication"
23442
+ complete -c tc -l replication-debug -d "Write replication diagnostics to stderr"
22562
23443
  `;
22563
23444
  }
22564
23445
 
@@ -22593,7 +23474,7 @@ function registerContextCommand(program) {
22593
23474
  access: "not-tested"
22594
23475
  });
22595
23476
  } catch (error) {
22596
- handleError(error);
23477
+ return handleError(error);
22597
23478
  }
22598
23479
  });
22599
23480
  }
@@ -22642,7 +23523,7 @@ function registerDelegationCommand(program, configuration = {}) {
22642
23523
  expiry: expiry.toISOString()
22643
23524
  });
22644
23525
  } catch (error) {
22645
- handleError(error);
23526
+ return handleError(error);
22646
23527
  }
22647
23528
  });
22648
23529
  delegation.command("list").description("List delegations").option("--granted", "Show only delegations I've granted").option("--received", "Show only delegations I've received").action(async (options, cmd) => {
@@ -22674,7 +23555,7 @@ function registerDelegationCommand(program, configuration = {}) {
22674
23555
  count: delegations.length
22675
23556
  });
22676
23557
  } catch (error) {
22677
- handleError(error);
23558
+ return handleError(error);
22678
23559
  }
22679
23560
  });
22680
23561
  delegation.command("info <cid>").description("Get delegation details").action(async (cid, _options, cmd) => {
@@ -22688,7 +23569,7 @@ function registerDelegationCommand(program, configuration = {}) {
22688
23569
  }
22689
23570
  outputJson(result.data);
22690
23571
  } catch (error) {
22691
- handleError(error);
23572
+ return handleError(error);
22692
23573
  }
22693
23574
  });
22694
23575
  delegation.command("revoke <cid>").description("Revoke a delegation").option("--yes", "Skip local-key TTY confirmation", false).action(async (cid, options, cmd) => {
@@ -22706,7 +23587,7 @@ function registerDelegationCommand(program, configuration = {}) {
22706
23587
  openKeyAcquisition: configuration.openKeyAcquisition
22707
23588
  }));
22708
23589
  } catch (error) {
22709
- handleError(error);
23590
+ return handleError(error);
22710
23591
  }
22711
23592
  });
22712
23593
  }
@@ -22818,7 +23699,7 @@ function registerDoctorCommand(program) {
22818
23699
  }
22819
23700
  }
22820
23701
  } catch (error) {
22821
- handleError(error);
23702
+ return handleError(error);
22822
23703
  }
22823
23704
  });
22824
23705
  }
@@ -22862,7 +23743,7 @@ ${rowCount} row${rowCount === 1 ? "" : "s"} returned`) + "\n");
22862
23743
  }
22863
23744
  }
22864
23745
  } catch (error) {
22865
- handleError(error);
23746
+ return handleError(error);
22866
23747
  }
22867
23748
  });
22868
23749
  duckdb.command("execute <sql>").description("Run INSERT/UPDATE/DELETE/DDL statement").option("--db <name>", "Database name", "default").option("--params <json>", "Bind parameters as JSON array").action(async (sqlStr, options, cmd) => {
@@ -22880,7 +23761,7 @@ ${rowCount} row${rowCount === 1 ? "" : "s"} returned`) + "\n");
22880
23761
  }
22881
23762
  outputJson({ changes: result.data.changes });
22882
23763
  } catch (error) {
22883
- handleError(error);
23764
+ return handleError(error);
22884
23765
  }
22885
23766
  });
22886
23767
  duckdb.command("describe").description("Show database schema (tables, columns, views)").option("--db <name>", "Database name", "default").action(async (options, cmd) => {
@@ -22925,7 +23806,7 @@ ${rowCount} row${rowCount === 1 ? "" : "s"} returned`) + "\n");
22925
23806
  }
22926
23807
  }
22927
23808
  } catch (error) {
22928
- handleError(error);
23809
+ return handleError(error);
22929
23810
  }
22930
23811
  });
22931
23812
  duckdb.command("export").description("Export database as binary file").option("--db <name>", "Database name", "default").option("-o, --output <file>", "Output file path", "export.duckdb").action(async (options, cmd) => {
@@ -22950,7 +23831,7 @@ ${rowCount} row${rowCount === 1 ? "" : "s"} returned`) + "\n");
22950
23831
  sizeHuman: formatBytes(blob.size)
22951
23832
  });
22952
23833
  } catch (error) {
22953
- handleError(error);
23834
+ return handleError(error);
22954
23835
  }
22955
23836
  });
22956
23837
  duckdb.command("import <file>").description("Import a DuckDB database file").option("--db <name>", "Database name", "default").action(async (file, options, cmd) => {
@@ -22974,7 +23855,7 @@ ${rowCount} row${rowCount === 1 ? "" : "s"} returned`) + "\n");
22974
23855
  imported: true
22975
23856
  });
22976
23857
  } catch (error) {
22977
- handleError(error);
23858
+ return handleError(error);
22978
23859
  }
22979
23860
  });
22980
23861
  }
@@ -23013,7 +23894,7 @@ function registerEnableCommand(program) {
23013
23894
  };
23014
23895
  output(value, () => result.declined.length === 0 ? `Share enabled for profile ${context.profile}.` : `Share enabled for profile ${context.profile} with ${result.declined.length} capability group(s) declined; publishing may fail.`);
23015
23896
  } catch (error) {
23016
- handleError(error);
23897
+ return handleError(error);
23017
23898
  }
23018
23899
  });
23019
23900
  }
@@ -23085,7 +23966,7 @@ function registerInitCommand(program) {
23085
23966
  authenticated: true
23086
23967
  });
23087
23968
  } catch (error) {
23088
- handleError(error);
23969
+ return handleError(error);
23089
23970
  }
23090
23971
  });
23091
23972
  }
@@ -23098,8 +23979,8 @@ init_storage();
23098
23979
  init_errors();
23099
23980
  init_constants();
23100
23981
  import { randomUUID as randomUUID2 } from "crypto";
23101
- import { lstat as lstat3, open as open3, rename as rename2, rm as rm4 } from "fs/promises";
23102
- import { basename as basename2, dirname as dirname3, join as join7 } from "path";
23982
+ import { lstat as lstat3, open as open3, rename as rename2, rm as rm5 } from "fs/promises";
23983
+ import { basename as basename2, dirname as dirname4, join as join9 } from "path";
23103
23984
  async function validatePrivateOutput(path, label = "output") {
23104
23985
  try {
23105
23986
  const destination = await lstat3(path);
@@ -23110,7 +23991,7 @@ async function validatePrivateOutput(path, label = "output") {
23110
23991
  if (error?.code !== "ENOENT") throw error;
23111
23992
  }
23112
23993
  try {
23113
- const parent = await lstat3(dirname3(path));
23994
+ const parent = await lstat3(dirname4(path));
23114
23995
  if (!parent.isDirectory()) {
23115
23996
  throw new CLIError("INVALID_ARGUMENT", `${label} "${path}" requires an existing directory parent.`, ExitCode.USAGE_ERROR);
23116
23997
  }
@@ -23121,8 +24002,8 @@ async function validatePrivateOutput(path, label = "output") {
23121
24002
  }
23122
24003
  async function writePrivateOutput(path, value, label = "output") {
23123
24004
  await validatePrivateOutput(path, label);
23124
- const parentPath = dirname3(path);
23125
- const temp = join7(parentPath, `.${basename2(path)}.${randomUUID2()}.tmp`);
24005
+ const parentPath = dirname4(path);
24006
+ const temp = join9(parentPath, `.${basename2(path)}.${randomUUID2()}.tmp`);
23126
24007
  let created = false;
23127
24008
  try {
23128
24009
  const handle = await open3(temp, "wx", PRIVATE_FILE_MODE);
@@ -23135,7 +24016,7 @@ async function writePrivateOutput(path, value, label = "output") {
23135
24016
  }
23136
24017
  await rename2(temp, path);
23137
24018
  } catch (error) {
23138
- if (created) await rm4(temp, { force: true }).catch(() => void 0);
24019
+ if (created) await rm5(temp, { force: true }).catch(() => void 0);
23139
24020
  const code3 = error instanceof Error && "code" in error && typeof error.code === "string" ? ` (${error.code})` : "";
23140
24021
  throw new CLIError("ERROR", `Could not write ${label.toLowerCase()} "${path}"${code3}.`, ExitCode.ERROR);
23141
24022
  }
@@ -23228,7 +24109,7 @@ function registerKvCommand(program) {
23228
24109
  process.stdout.write(content + "\n");
23229
24110
  }
23230
24111
  } catch (error) {
23231
- handleError(error);
24112
+ return handleError(error);
23232
24113
  }
23233
24114
  });
23234
24115
  kv.command("put <key> [value]").description("Set a value").option("--file <path>", "Read value from file").option("--stdin", "Read value from stdin").option("--space <name|uri>", "Target a non-primary space (short name or full URI)").action(async (key, value, options, cmd) => {
@@ -23263,7 +24144,7 @@ function registerKvCommand(program) {
23263
24144
  }
23264
24145
  outputJson({ key, written: true });
23265
24146
  } catch (error) {
23266
- handleError(error);
24147
+ return handleError(error);
23267
24148
  }
23268
24149
  });
23269
24150
  kv.command("delete <key>").description("Delete a key").option("--space <name|uri>", "Target a non-primary space (short name or full URI)").action(async (key, options, cmd) => {
@@ -23279,7 +24160,7 @@ function registerKvCommand(program) {
23279
24160
  }
23280
24161
  outputJson({ key, deleted: true });
23281
24162
  } catch (error) {
23282
- handleError(error);
24163
+ return handleError(error);
23283
24164
  }
23284
24165
  });
23285
24166
  kv.command("list").description("List keys").option("--prefix <prefix>", "Filter by key prefix").option("--space <name|uri>", "Target a non-primary space (short name or full URI)").action(async (options, cmd) => {
@@ -23315,7 +24196,7 @@ function registerKvCommand(program) {
23315
24196
  }
23316
24197
  }
23317
24198
  } catch (error) {
23318
- handleError(error);
24199
+ return handleError(error);
23319
24200
  }
23320
24201
  });
23321
24202
  kv.command("head <key>").description("Get metadata for a key (no body)").option("--space <name|uri>", "Target a non-primary space (short name or full URI)").action(async (key, options, cmd) => {
@@ -23341,7 +24222,7 @@ function registerKvCommand(program) {
23341
24222
  metadata: result.data.headers ?? {}
23342
24223
  });
23343
24224
  } catch (error) {
23344
- handleError(error);
24225
+ return handleError(error);
23345
24226
  }
23346
24227
  });
23347
24228
  }
@@ -23453,7 +24334,7 @@ ${theme.heading("Permissions")}
23453
24334
  process.stdout.write(formatTable(["service", "path", "actions"], rows) + "\n");
23454
24335
  }
23455
24336
  } catch (error) {
23456
- handleError(error);
24337
+ return handleError(error);
23457
24338
  }
23458
24339
  });
23459
24340
  }
@@ -23512,7 +24393,7 @@ function registerNodeCommand(program) {
23512
24393
  if (error instanceof TypeError && error.message.includes("fetch")) {
23513
24394
  outputJson({ healthy: false, host: (await ProfileManager.resolveContext(cmd.optsWithGlobals(), { storedSession: "unused" })).host, error: "Connection refused" });
23514
24395
  } else {
23515
- handleError(error);
24396
+ return handleError(error);
23516
24397
  }
23517
24398
  }
23518
24399
  });
@@ -23527,7 +24408,7 @@ function registerNodeCommand(program) {
23527
24408
  const data = await response.json();
23528
24409
  outputJson({ ...data, host: ctx.host });
23529
24410
  } catch (error) {
23530
- handleError(error);
24411
+ return handleError(error);
23531
24412
  }
23532
24413
  });
23533
24414
  node.command("status").description("Combined health and version info").action(async (_options, cmd) => {
@@ -23552,7 +24433,7 @@ function registerNodeCommand(program) {
23552
24433
  ...versionData
23553
24434
  });
23554
24435
  } catch (error) {
23555
- handleError(error);
24436
+ return handleError(error);
23556
24437
  }
23557
24438
  });
23558
24439
  }
@@ -23612,7 +24493,7 @@ function registerProfileCommand(program) {
23612
24493
  }
23613
24494
  }
23614
24495
  } catch (error) {
23615
- handleError(error);
24496
+ return handleError(error);
23616
24497
  }
23617
24498
  });
23618
24499
  profile.command("create <name>").description("Create a new profile").option("--host <url>", "TinyCloud node URL").option(
@@ -23646,7 +24527,7 @@ function registerProfileCommand(program) {
23646
24527
  });
23647
24528
  outputJson({ profile: name, did, host, posture, operatorType, created: true });
23648
24529
  } catch (error) {
23649
- handleError(error);
24530
+ return handleError(error);
23650
24531
  }
23651
24532
  });
23652
24533
  profile.command("show [name]").description("Show profile details").action(async (name, _options, cmd) => {
@@ -23685,7 +24566,7 @@ function registerProfileCommand(program) {
23685
24566
  process.stdout.write(formatField("Created", p.createdAt) + "\n");
23686
24567
  }
23687
24568
  } catch (error) {
23688
- handleError(error);
24569
+ return handleError(error);
23689
24570
  }
23690
24571
  });
23691
24572
  profile.command("switch <name>").description("Set default profile").action(async (name, _options, cmd) => {
@@ -23697,7 +24578,7 @@ function registerProfileCommand(program) {
23697
24578
  await ProfileManager.setConfig({ ...config, defaultProfile: name });
23698
24579
  outputJson({ defaultProfile: name, switched: true });
23699
24580
  } catch (error) {
23700
- handleError(error);
24581
+ return handleError(error);
23701
24582
  }
23702
24583
  });
23703
24584
  profile.command("set-default-space [name]").description("Set (or clear) the default space used when --space is omitted").option("--profile <name>", "Profile to modify (defaults to the active profile)").option("--unset", "Clear the default space so commands fall back to the primary space").addHelpText("after", `
@@ -23736,7 +24617,7 @@ Examples:
23736
24617
  await ProfileManager.updateProfile(profileName2, (p) => ({ ...p, defaultSpace }));
23737
24618
  outputJson({ profile: profileName2, defaultSpace: defaultSpace ?? null, updated: true });
23738
24619
  } catch (error) {
23739
- handleError(error);
24620
+ return handleError(error);
23740
24621
  }
23741
24622
  });
23742
24623
  profile.command("delete <name>").description("Delete a profile").action(async (name, _options, cmd) => {
@@ -23755,7 +24636,7 @@ Examples:
23755
24636
  await ProfileManager.deleteProfile(name);
23756
24637
  outputJson({ profile: name, deleted: true });
23757
24638
  } catch (error) {
23758
- handleError(error);
24639
+ return handleError(error);
23759
24640
  }
23760
24641
  });
23761
24642
  }
@@ -23779,16 +24660,105 @@ function parseOperatorType(raw) {
23779
24660
  }
23780
24661
 
23781
24662
  // src/commands/replica.ts
24663
+ init_sdk();
24664
+ init_replication_registry();
23782
24665
  import { randomBytes as randomBytes5 } from "crypto";
23783
24666
  import { constants as fsConstants2 } from "fs";
23784
- import { open as open4, readdir as readdir5, rename as rename3, stat as stat3, unlink as unlink2 } from "fs/promises";
23785
- import { basename as basename3, dirname as dirname4, join as join8, resolve as resolve2 } from "path";
24667
+ import { open as open4, readdir as readdir5, rename as rename3, stat as stat3, unlink as unlink2, readFile as readFile9, lstat as lstat4 } from "fs/promises";
24668
+ import { basename as basename3, dirname as dirname5, join as join10, resolve as resolve2 } from "path";
24669
+
24670
+ // src/lib/replication-report.ts
24671
+ var CLEAR_PENDING_WARNING = "Clearing pending writes stops pinning keys whose last write had an unknown outcome or never finished. If such a write did commit, or commits later, this device can serve the older value for that key until the next sync after the commit, as it would for a write from another device.";
24672
+ function percentile(values, fraction) {
24673
+ if (values.length === 0) return null;
24674
+ values.sort((left, right) => left - right);
24675
+ return values[Math.ceil(fraction * values.length) - 1] ?? values[values.length - 1];
24676
+ }
24677
+ function createReplicationReport(since, events, replicas, partitions, warning) {
24678
+ const reads = events.filter((event) => event.type === "replication.read");
24679
+ const replicaReads = reads.filter((event) => event.source === "replica");
24680
+ const sourceLatency = /* @__PURE__ */ new Map();
24681
+ const readsBySourceReason = {};
24682
+ for (const event of reads) {
24683
+ const key = `${event.source}:${event.reason}`;
24684
+ readsBySourceReason[key] = (readsBySourceReason[key] ?? 0) + 1;
24685
+ const values = sourceLatency.get(event.source) ?? [];
24686
+ values.push(event.latencyMs);
24687
+ sourceLatency.set(event.source, values);
24688
+ }
24689
+ const latencyMs = Object.fromEntries([...sourceLatency].map(([source, values]) => [source, {
24690
+ p50: percentile([...values], 0.5),
24691
+ p95: percentile([...values], 0.95)
24692
+ }]));
24693
+ const staleness = reads.flatMap((event) => event.stalenessMs === null ? [] : [event.stalenessMs]);
24694
+ const syncs = {};
24695
+ const writes = {};
24696
+ const divergences = events.filter((event) => event.type === "replication.divergence").slice(-20);
24697
+ for (const event of events) {
24698
+ if (event.type === "replication.sync") {
24699
+ const key = `${event.class ?? "none"}:${event.code ?? event.outcome}`;
24700
+ syncs[key] = (syncs[key] ?? 0) + 1;
24701
+ } else if (event.type === "replication.write") {
24702
+ writes[event.outcome] = (writes[event.outcome] ?? 0) + 1;
24703
+ }
24704
+ }
24705
+ return {
24706
+ since,
24707
+ totals: {
24708
+ reads: reads.length,
24709
+ replicaReads: replicaReads.length,
24710
+ hitRatio: reads.length === 0 ? 0 : replicaReads.length / reads.length,
24711
+ writes: events.filter((event) => event.type === "replication.write").length,
24712
+ divergences: events.filter((event) => event.type === "replication.divergence").length
24713
+ },
24714
+ readsBySourceReason,
24715
+ latencyMs,
24716
+ stalenessMs: { p50: percentile([...staleness], 0.5), p95: percentile([...staleness], 0.95), max: staleness.length ? Math.max(...staleness) : null },
24717
+ syncs,
24718
+ writes,
24719
+ divergences,
24720
+ replicas,
24721
+ partitions,
24722
+ ...warning === void 0 ? {} : { warning }
24723
+ };
24724
+ }
24725
+ function renderReplicationReport(report) {
24726
+ const lines = [
24727
+ `Replication report (since ${report.since})`,
24728
+ `Reads: ${report.totals.reads}; replica: ${report.totals.replicaReads}; hit ratio: ${(report.totals.hitRatio * 100).toFixed(1)}%`,
24729
+ `Latency ms: ${Object.entries(report.latencyMs).map(([source, value]) => `${source} p50=${value.p50 ?? "\u2014"} p95=${value.p95 ?? "\u2014"}`).join("; ") || "none"}`,
24730
+ `Staleness ms: p50=${report.stalenessMs.p50 ?? "\u2014"} p95=${report.stalenessMs.p95 ?? "\u2014"} max=${report.stalenessMs.max ?? "\u2014"}`,
24731
+ `Syncs: ${Object.entries(report.syncs).map(([key, count]) => `${key}=${count}`).join(", ") || "none"}`,
24732
+ `Writes: ${Object.entries(report.writes).map(([key, count]) => `${key}=${count}`).join(", ") || "none"}`,
24733
+ `Read source/reason: ${Object.entries(report.readsBySourceReason).map(([key, count]) => `${key}=${count}`).join(", ") || "none"}`,
24734
+ `Divergences: ${report.totals.divergences}; showing ${report.divergences.length} of ${report.totals.divergences}`,
24735
+ `Replicas: ${report.replicas.length}`
24736
+ ];
24737
+ for (const divergence of report.divergences) {
24738
+ if (divergence.type === "replication.divergence") {
24739
+ lines.push(` ${divergence.space} ${divergence.key}: ${divergence.kind} stalenessMs=${divergence.stalenessMs}`);
24740
+ }
24741
+ }
24742
+ for (const replica of report.replicas) {
24743
+ const grant = replica.grant ? ` grant=${replica.grant.cid} parent=${replica.grant.parentCid ?? "\u2014"} expires=${replica.grant.expiresAt ?? "\u2014"}` : " grant=missing";
24744
+ lines.push(` ${replica.prefix}: ${replica.state}; pending in_flight=${replica.pending.inFlight} committed=${replica.pending.committed} ambiguous=${replica.pending.ambiguous}${grant}`);
24745
+ for (const pinned of replica.pinned) lines.push(` pinned ${pinned.key} ${pinned.state} ${pinned.op} since=${pinned.since} code=${pinned.code ?? "\u2014"} likelyOrphaned=${pinned.likelyOrphaned}`);
24746
+ }
24747
+ for (const partition of report.partitions) {
24748
+ lines.push(`Partition ${partition.idHash}: host=${partition.host ?? "unknown"} space=${partition.space ?? "unknown"} pinned=${partition.pinned}`);
24749
+ }
24750
+ if (report.partitions.some((partition) => partition.pinned > 100)) lines.push("Warning: a partition holds more than 100 pinned records.");
24751
+ if (report.warning) lines.push(`Warning: ${report.warning}`);
24752
+ return lines.join("\n");
24753
+ }
24754
+
24755
+ // src/commands/replica.ts
23786
24756
  import {
23787
24757
  ProfileDeletedError as ProfileDeletedError3,
23788
- profilePath as profilePath3,
24758
+ profilePath as profilePath4,
23789
24759
  readAdditionalDelegations as readAdditionalDelegations2,
23790
24760
  readSession as readSession2,
23791
- withProfileLock as withProfileLock4
24761
+ withProfileLock as withProfileLock5
23792
24762
  } from "@tinycloud/operations/state";
23793
24763
  init_profiles();
23794
24764
  init_constants();
@@ -23796,6 +24766,8 @@ init_errors();
23796
24766
  init_formatter();
23797
24767
  init_theme();
23798
24768
  var EXIT_BY_CODE = {
24769
+ [ReplicaErrorCode.INVALID_ARGUMENT]: ExitCode.USAGE_ERROR,
24770
+ [ReplicaErrorCode.CLOSED]: ExitCode.ERROR,
23799
24771
  [ReplicaErrorCode.BUSY]: ExitCode.ERROR,
23800
24772
  [ReplicaErrorCode.RUNTIME_UNSUPPORTED]: ExitCode.ERROR,
23801
24773
  [ReplicaErrorCode.STORAGE_ERROR]: ExitCode.ERROR,
@@ -23845,7 +24817,7 @@ async function run(action) {
23845
24817
  await loadSqlite();
23846
24818
  await action();
23847
24819
  } catch (error) {
23848
- handleError(toCliError(error));
24820
+ return handleError(toCliError(error));
23849
24821
  }
23850
24822
  }
23851
24823
  async function profileName(cmd) {
@@ -23856,13 +24828,13 @@ async function profileName(cmd) {
23856
24828
  return config?.defaultProfile ?? DEFAULT_PROFILE;
23857
24829
  }
23858
24830
  function replicasRoot(profile) {
23859
- return join8(profilePath3(profile), "replicas");
24831
+ return join10(profilePath4(profile), "replicas");
23860
24832
  }
23861
24833
  async function replicaNames(profile) {
23862
24834
  const names = [];
23863
24835
  for (const name of await readdir5(replicasRoot(profile)).catch(() => [])) {
23864
24836
  if (!NAME.test(name)) continue;
23865
- const exists = await stat3(join8(replicasRoot(profile), name, "replica.db")).then(
24837
+ const exists = await stat3(join10(replicasRoot(profile), name, "replica.db")).then(
23866
24838
  () => true,
23867
24839
  () => false
23868
24840
  );
@@ -23888,7 +24860,7 @@ async function existingReplicaName(profile, option) {
23888
24860
  function profileGuard(profile) {
23889
24861
  return async (section) => {
23890
24862
  try {
23891
- return await withProfileLock4(profile, section, { requireProfile: true });
24863
+ return await withProfileLock5(profile, section, { requireProfile: true });
23892
24864
  } catch (error) {
23893
24865
  if (error instanceof ProfileDeletedError3) {
23894
24866
  throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, `Profile "${profile}" was deleted; its replicas are gone and nothing was written.`);
@@ -23898,12 +24870,12 @@ function profileGuard(profile) {
23898
24870
  };
23899
24871
  }
23900
24872
  async function openReplica(profile, name) {
23901
- return SqliteReplicaStore.open(join8(replicasRoot(profile), name), { create: false, guard: profileGuard(profile) });
24873
+ return SqliteReplicaStore.open(join10(replicasRoot(profile), name), { create: false, guard: profileGuard(profile) });
23902
24874
  }
23903
24875
  async function createReplica(profile, config) {
23904
24876
  const guard = profileGuard(profile);
23905
24877
  return guard(async () => {
23906
- const store = await SqliteReplicaStore.open(join8(replicasRoot(profile), config.name), { create: true, guard });
24878
+ const store = await SqliteReplicaStore.open(join10(replicasRoot(profile), config.name), { create: true, guard });
23907
24879
  if (await store.open() === null) await store.init(config);
23908
24880
  return store;
23909
24881
  });
@@ -24035,7 +25007,7 @@ function describeStatus(status) {
24035
25007
  }
24036
25008
  async function writeFileAtomic(path, bytes) {
24037
25009
  const target = resolve2(path);
24038
- const temp = join8(dirname4(target), `.${basename3(target)}.${randomBytes5(6).toString("hex")}.tmp`);
25010
+ const temp = join10(dirname5(target), `.${basename3(target)}.${randomBytes5(6).toString("hex")}.tmp`);
24039
25011
  const handle = await open4(temp, fsConstants2.O_CREAT | fsConstants2.O_EXCL | fsConstants2.O_WRONLY, 384);
24040
25012
  try {
24041
25013
  await handle.writeFile(bytes);
@@ -24061,6 +25033,49 @@ function replicaSecretsWarning(space, prefix, secretsAllowed) {
24061
25033
  if (!secretsAllowed || !requiresSecretsOptIn(space, prefix)) return void 0;
24062
25034
  return `This replica stores the ciphertext of every secret under "${prefix}". A tinycloud.encryption/decrypt grant covers the whole encryption network, not one secret (TC-755), so whoever holds decrypt can open every replicated secret.`;
24063
25035
  }
25036
+ async function readReplicationEvents(profile, sinceMs) {
25037
+ const root = join10(profilePath4(profile), "replication");
25038
+ const events = [];
25039
+ for (const name of ["events.jsonl.1", "events.jsonl"]) {
25040
+ const data = await readFile9(join10(root, name), "utf8").catch((error) => {
25041
+ if (error.code === "ENOENT") return "";
25042
+ throw error;
25043
+ });
25044
+ for (const line of data.split("\n")) {
25045
+ if (!line) continue;
25046
+ try {
25047
+ const event = JSON.parse(line);
25048
+ if (Number.isFinite(Date.parse(event.at)) && Date.parse(event.at) >= sinceMs) events.push(event);
25049
+ } catch {
25050
+ }
25051
+ }
25052
+ }
25053
+ return events;
25054
+ }
25055
+ async function replicationPartitions(profile) {
25056
+ const root = join10(profilePath4(profile), "replication");
25057
+ const entries = await readdir5(root, { withFileTypes: true }).catch((error) => {
25058
+ if (error.code === "ENOENT") return [];
25059
+ throw error;
25060
+ });
25061
+ const partitions = [];
25062
+ for (const entry of entries) {
25063
+ if (!entry.isDirectory() || !/^[a-z2-7]{26}$/.test(entry.name)) continue;
25064
+ const directory = join10(root, entry.name);
25065
+ const info = await lstat4(directory);
25066
+ if (info.isSymbolicLink() || !info.isDirectory()) continue;
25067
+ const identity = await readFile9(join10(directory, "identity.json"), "utf8").then((value) => JSON.parse(value)).catch(() => null);
25068
+ const pending = await readFile9(join10(directory, "pending.json"), "utf8").then((value) => JSON.parse(value)).catch(() => null);
25069
+ const pinned = pending?.records?.filter((record2) => record2.state === "in_flight" || record2.state === "ambiguous").length ?? 0;
25070
+ partitions.push({
25071
+ idHash: entry.name,
25072
+ host: typeof identity?.host === "string" ? identity.host : null,
25073
+ space: typeof identity?.space === "string" ? identity.space : null,
25074
+ pinned
25075
+ });
25076
+ }
25077
+ return partitions.sort((left, right) => left.idHash.localeCompare(right.idHash));
25078
+ }
24064
25079
  function registerReplicaCommand(program) {
24065
25080
  const replica = program.command("replica").description("Durable read-only local replicas of a KV prefix (sync online, read offline) [beta]").addHelpText(
24066
25081
  "after",
@@ -24076,6 +25091,53 @@ SECRETS_OPT_IN_REQUIRED; 3 grant missing; 4 key absent, deleted, content missing
24076
25091
  or coverage incomplete; 5 grant expired, revoked or not yet valid; 6 network;
24077
25092
  7 node, protocol or integrity error; 10 storage full.`
24078
25093
  );
25094
+ replica.command("report").description("Report replication reads, syncs, pending writes and local identity partitions").option("--since <duration>", "Include events from the last duration (default: 24h)", "24h").option("--purge", "Purge configured replicas for the current identity").option("--clear-pending", "Clear ambiguous and likely orphaned in-flight writes").action(
25095
+ (options, cmd) => run(async () => {
25096
+ const globals = cmd.optsWithGlobals();
25097
+ const context = await ProfileManager.resolveContext(globals);
25098
+ const node = context.replication ? await ensureAuthenticated(context) : void 0;
25099
+ const profile = await profileName(cmd);
25100
+ let durationMs;
25101
+ try {
25102
+ durationMs = parseDuration(options.since);
25103
+ } catch (error) {
25104
+ throw new CLIError("USAGE_ERROR", error instanceof Error ? error.message : String(error), ExitCode.USAGE_ERROR);
25105
+ }
25106
+ const control = node?.replication ?? replicationForProfile(profile);
25107
+ let cleared;
25108
+ let purged;
25109
+ let warning;
25110
+ if (options.clearPending || options.purge) {
25111
+ if (!control) throw new CLIError("REPLICATION_UNAVAILABLE", "No replication runtime is registered for this profile.", ExitCode.ERROR);
25112
+ warning = CLEAR_PENDING_WARNING;
25113
+ process.stderr.write(`Warning: ${warning}
25114
+ `);
25115
+ if (options.clearPending) cleared = await control.clearPending();
25116
+ if (options.purge) purged = await control.purge();
25117
+ }
25118
+ const sinceMs = Date.now() - durationMs;
25119
+ const [events, partitions, replicas] = await Promise.all([
25120
+ readReplicationEvents(profile, sinceMs),
25121
+ replicationPartitions(profile),
25122
+ node?.replication ? node.replication.status() : control ? control.status() : Promise.resolve([])
25123
+ ]);
25124
+ const report = createReplicationReport(options.since, events, replicas, partitions, warning);
25125
+ if (shouldOutputJson()) {
25126
+ outputJson({
25127
+ ...report,
25128
+ ...cleared === void 0 ? {} : { cleared },
25129
+ ...purged === void 0 ? {} : { purged }
25130
+ });
25131
+ } else {
25132
+ process.stdout.write(`${renderReplicationReport(report)}
25133
+ `);
25134
+ if (cleared !== void 0) process.stdout.write(`Cleared ${cleared} pending write record(s).
25135
+ `);
25136
+ if (purged !== void 0) process.stdout.write(`Purge: ${JSON.stringify(purged)}
25137
+ `);
25138
+ }
25139
+ })
25140
+ );
24079
25141
  replica.command("sync").description("Create or update a replica from its source host").option("--replica <name>", "Replica name (default: the profile's only replica, or one named after the prefix)").option("--space <id|name>", "Space to replicate (first sync; default: the space of the sync grant)").option("--prefix <prefix>", "KV prefix to replicate, e.g. notes/ (first sync)").option("--retention-grant <cid>", "CID of a tinycloud.kv/retain grant: keep local reads after the sync grant expires").option("--limit <n>", "Feed page size (1-1000)", (value) => Number.parseInt(value, 10)).option("--allow-secrets", "Opt in to copying ciphertext under a secrets prefix; replica grants do not include decrypt").action(
24080
25142
  (options, cmd) => run(async () => {
24081
25143
  const profile = await profileName(cmd);
@@ -24095,8 +25157,8 @@ or coverage incomplete; 5 grant expired, revoked or not yet valid; 6 network;
24095
25157
  let store;
24096
25158
  if (existing.includes(name)) {
24097
25159
  store = await openReplica(profile, name);
24098
- const state = await store.open();
24099
- const differs = syncOptionsDiffer(state.config, {
25160
+ const state2 = await store.open();
25161
+ const differs = syncOptionsDiffer(state2.config, {
24100
25162
  ...options.space === void 0 ? {} : { space: options.space },
24101
25163
  ...options.prefix === void 0 ? {} : { prefix: options.prefix },
24102
25164
  ...globals.host === void 0 ? {} : { host: globals.host }
@@ -24136,24 +25198,24 @@ or coverage incomplete; 5 grant expired, revoked or not yet valid; 6 network;
24136
25198
  });
24137
25199
  }
24138
25200
  try {
24139
- const state = await store.open();
24140
- const { space, prefix, host } = state.config;
24141
- if (requiresSecretsOptIn(space, prefix) && !state.config.allowSecrets && options.allowSecrets !== true) {
25201
+ const state2 = await store.open();
25202
+ const { space, prefix, host } = state2.config;
25203
+ if (requiresSecretsOptIn(space, prefix) && !state2.config.allowSecrets && options.allowSecrets !== true) {
24142
25204
  throw new ReplicaError(ReplicaErrorCode.SECRETS_OPT_IN_REQUIRED, `Replicating ${space}/kv/${prefix} needs --allow-secrets.`);
24143
25205
  }
24144
25206
  let grant;
24145
25207
  try {
24146
- grant = chooseGrant(grants, state.config.deviceDid, space, prefix);
25208
+ grant = chooseGrant(grants, state2.config.deviceDid, space, prefix);
24147
25209
  } catch (error) {
24148
- if (!isReplicaError(error, ReplicaErrorCode.GRANT_MISSING) || (state.grant ?? state.pendingGrant) === null) throw error;
25210
+ if (!isReplicaError(error, ReplicaErrorCode.GRANT_MISSING) || (state2.grant ?? state2.pendingGrant) === null) throw error;
24149
25211
  }
24150
25212
  if (grant !== void 0) await store.installGrant(grant);
24151
- if (options.retentionGrant !== void 0 && options.retentionGrant !== state.config.retentionGrantCid) {
25213
+ if (options.retentionGrant !== void 0 && options.retentionGrant !== state2.config.retentionGrantCid) {
24152
25214
  await store.setRetentionGrant(options.retentionGrant === "" ? null : options.retentionGrant);
24153
25215
  }
24154
25216
  const current = await store.open();
24155
25217
  const transportFor = await grantTransports({ host, space, deviceDid: current.config.deviceDid, jwk });
24156
- const warning = replicaSecretsWarning(space, prefix, options.allowSecrets === true || state.config.allowSecrets);
25218
+ const warning = replicaSecretsWarning(space, prefix, options.allowSecrets === true || state2.config.allowSecrets);
24157
25219
  if (warning !== void 0) process.stderr.write(`Warning: ${warning}
24158
25220
  `);
24159
25221
  const report = await new Replica({ store, transportFor }).sync(options.limit === void 0 ? {} : { limit: options.limit });
@@ -24283,8 +25345,8 @@ init_theme();
24283
25345
  init_errors();
24284
25346
  init_warnings();
24285
25347
  init_constants();
24286
- import { readFile as readFile9 } from "fs/promises";
24287
- import { join as join9 } from "path";
25348
+ import { readFile as readFile10 } from "fs/promises";
25349
+ import { join as join11 } from "path";
24288
25350
  import { homedir, hostname, userInfo } from "os";
24289
25351
  import { appSecretScope } from "@tinycloud/sdk-core";
24290
25352
  import { invokeOperation as invokeOperation2 } from "@tinycloud/operations";
@@ -24813,7 +25875,7 @@ function resolveSecretListPrefix(options = {}) {
24813
25875
  }
24814
25876
  function resolveProfilesDir() {
24815
25877
  const home = process.env.TC_HOME ?? process.env.HOME ?? process.env.USERPROFILE ?? homedir();
24816
- return join9(home, ".tinycloud", "profiles");
25878
+ return join11(home, ".tinycloud", "profiles");
24817
25879
  }
24818
25880
  async function ensureSecretsNode(ctx, options, openKeyAcquisition, selectedProfile) {
24819
25881
  const auth = authOptions(options);
@@ -25176,7 +26238,7 @@ function permissionsFromDelegation2(delegation) {
25176
26238
  }
25177
26239
  async function loadDelegationCandidates(source) {
25178
26240
  try {
25179
- const raw = JSON.parse(await readFile9(source, "utf8"));
26241
+ const raw = JSON.parse(await readFile10(source, "utf8"));
25180
26242
  return normalizeDelegationCandidates(raw, source);
25181
26243
  } catch (error) {
25182
26244
  if (error?.code !== "ENOENT") {
@@ -25195,8 +26257,8 @@ async function loadDelegationCandidates(source) {
25195
26257
  }
25196
26258
  }
25197
26259
  try {
25198
- const importedPath = join9(resolveProfilesDir(), source, "additional-delegations.json");
25199
- const raw = JSON.parse(await readFile9(importedPath, "utf8"));
26260
+ const importedPath = join11(resolveProfilesDir(), source, "additional-delegations.json");
26261
+ const raw = JSON.parse(await readFile10(importedPath, "utf8"));
25200
26262
  return normalizeDelegationCandidates(raw, source);
25201
26263
  } catch (error) {
25202
26264
  if (error?.code === "ENOENT") {
@@ -25521,7 +26583,7 @@ function registerSecretsCommand(program, openKeyAcquisition) {
25521
26583
  scopeOptions,
25522
26584
  space: spaceUri,
25523
26585
  label: `Checking secret ${name}...`,
25524
- operation: () => secrets2.get(name, scopeOptions)
26586
+ operation: () => secrets2.get(name, { ...scopeOptions, source: "network" })
25525
26587
  });
25526
26588
  if (result.ok) {
25527
26589
  secret = {
@@ -25590,7 +26652,7 @@ function registerSecretsCommand(program, openKeyAcquisition) {
25590
26652
  scopeOptions,
25591
26653
  space: spaceUri,
25592
26654
  label: "Listing secrets...",
25593
- operation: () => secrets2.list(scopeOptions)
26655
+ operation: () => secrets2.list({ ...scopeOptions, source: "network" })
25594
26656
  });
25595
26657
  if (!result.ok) {
25596
26658
  throw cliErrorFromService(result.error);
@@ -25703,7 +26765,7 @@ function registerSecretsCommand(program, openKeyAcquisition) {
25703
26765
  throw new CLIError("USAGE_ERROR", "Provide only one of: value argument, --file, or --stdin", ExitCode.USAGE_ERROR);
25704
26766
  }
25705
26767
  if (options.file) {
25706
- secretValue = await readFile9(options.file, "utf-8");
26768
+ secretValue = await readFile10(options.file, "utf-8");
25707
26769
  } else if (options.stdin) {
25708
26770
  secretValue = (await readStdin3()).toString("utf-8");
25709
26771
  } else {
@@ -32735,12 +33797,12 @@ async function notifyShare(input) {
32735
33797
  if (input.signal?.aborted) throw new ShareNotifyError("share delivery was cancelled");
32736
33798
  attempts += 1;
32737
33799
  try {
32738
- const state = await input.adapter.deliver({ shareId: input.shareId, recipient, idempotencyKey, ...input.record === void 0 ? {} : { record: input.record }, ...input.signal === void 0 ? {} : { signal: input.signal } });
33800
+ const state2 = await input.adapter.deliver({ shareId: input.shareId, recipient, idempotencyKey, ...input.record === void 0 ? {} : { record: input.record }, ...input.signal === void 0 ? {} : { signal: input.signal } });
32739
33801
  return {
32740
33802
  protocol: "tinycloud-share",
32741
33803
  version: 1,
32742
33804
  shareId: input.shareId,
32743
- state: state === "delivered" && input.record?.deliveredRecipients?.includes(recipient) ? "already-delivered" : state,
33805
+ state: state2 === "delivered" && input.record?.deliveredRecipients?.includes(recipient) ? "already-delivered" : state2,
32744
33806
  idempotencyKey,
32745
33807
  attempts
32746
33808
  };
@@ -34211,9 +35273,9 @@ function receiveJson(result, path) {
34211
35273
 
34212
35274
  // src/share/io.ts
34213
35275
  import { constants } from "fs";
34214
- import { lstat as lstat4, mkdir as mkdir4, mkdtemp, open as open7, readFile as readFile10, realpath as realpath2, stat as stat4, link as link2, rename as rename4, rm as rm5, unlink as unlink3 } from "fs/promises";
35276
+ import { lstat as lstat5, mkdir as mkdir4, mkdtemp, open as open7, readFile as readFile11, realpath as realpath2, stat as stat4, link as link2, rename as rename4, rm as rm6, unlink as unlink3 } from "fs/promises";
34215
35277
  import { randomBytes as randomBytes6 } from "crypto";
34216
- import { basename as basename4, join as join10, resolve as resolve3, sep as sep2 } from "path";
35278
+ import { basename as basename4, join as join12, resolve as resolve3, sep as sep2 } from "path";
34217
35279
  init_errors();
34218
35280
  var MAX_SHARE_STDIN_BYTES = 100 * 1024 * 1024;
34219
35281
  var MAX_SHARE_URL_BYTES = 64 * 1024;
@@ -34258,7 +35320,7 @@ async function readShareInput(input, name, limit = MAX_SHARE_STDIN_BYTES) {
34258
35320
  const path = resolve3(input);
34259
35321
  const info = await stat4(path);
34260
35322
  if (!info.isFile() || info.size > limit) throw new Error("MAX_BYTES_EXCEEDED");
34261
- const bytes = new Uint8Array(await readFile10(path));
35323
+ const bytes = new Uint8Array(await readFile11(path));
34262
35324
  if (bytes.byteLength > limit) throw new Error("MAX_BYTES_EXCEEDED");
34263
35325
  return { bytes, filename };
34264
35326
  }
@@ -34267,9 +35329,9 @@ async function assertDirectory(path) {
34267
35329
  const segments = absolute.split(sep2).filter(Boolean);
34268
35330
  let current = absolute.startsWith(sep2) ? sep2 : "";
34269
35331
  for (const segment of segments) {
34270
- current = current === sep2 ? join10(current, segment) : join10(current, segment);
35332
+ current = current === sep2 ? join12(current, segment) : join12(current, segment);
34271
35333
  try {
34272
- const info = await lstat4(current);
35334
+ const info = await lstat5(current);
34273
35335
  if (info.isSymbolicLink()) {
34274
35336
  const canonical = await realpath2(current);
34275
35337
  if (current !== "/tmp" && current !== "/var") throw new Error("OUTPUT_EXISTS");
@@ -34278,7 +35340,7 @@ async function assertDirectory(path) {
34278
35340
  } catch (error) {
34279
35341
  if (error.code !== "ENOENT") throw error;
34280
35342
  await mkdir4(current, { mode: 448 });
34281
- const created = await lstat4(current);
35343
+ const created = await lstat5(current);
34282
35344
  if (created.isSymbolicLink() || !created.isDirectory()) throw new Error("OUTPUT_EXISTS");
34283
35345
  }
34284
35346
  }
@@ -34289,23 +35351,23 @@ async function writeShareOutput(directory, filename, bytes, force) {
34289
35351
  const safeName = safeFilename(filename);
34290
35352
  const directoryHandle = await open7(outputDirectory, constants.O_RDONLY | (constants.O_DIRECTORY ?? 0) | (constants.O_NOFOLLOW ?? 0));
34291
35353
  const stableDirectory = await realpath2(outputDirectory);
34292
- const outputPath = join10(stableDirectory, safeName);
35354
+ const outputPath = join12(stableDirectory, safeName);
34293
35355
  const directoryIdentity = await directoryHandle.stat();
34294
35356
  const assertStableDirectory = async () => {
34295
35357
  const current = await stat4(stableDirectory);
34296
35358
  if (current.dev !== directoryIdentity.dev || current.ino !== directoryIdentity.ino) throw new Error("OUTPUT_EXISTS");
34297
35359
  };
34298
35360
  await assertStableDirectory();
34299
- const stagingDirectory = await mkdtemp(join10(stableDirectory, ".tinycloud-share-stage-"));
34300
- const stagingInfo = await lstat4(stagingDirectory);
35361
+ const stagingDirectory = await mkdtemp(join12(stableDirectory, ".tinycloud-share-stage-"));
35362
+ const stagingInfo = await lstat5(stagingDirectory);
34301
35363
  if (!stagingInfo.isDirectory() || (stagingInfo.mode & 511) !== 448) throw new Error("OUTPUT_EXISTS");
34302
- const stagingPath = join10(stagingDirectory, `.tinycloud-share-${randomBytes6(16).toString("hex")}.tmp`);
35364
+ const stagingPath = join12(stagingDirectory, `.tinycloud-share-${randomBytes6(16).toString("hex")}.tmp`);
34303
35365
  let temporaryPath;
34304
35366
  let handle;
34305
35367
  try {
34306
35368
  await assertStableDirectory();
34307
35369
  try {
34308
- const existing = await lstat4(outputPath);
35370
+ const existing = await lstat5(outputPath);
34309
35371
  if (existing.isSymbolicLink()) throw new Error("UNSAFE_FILENAME");
34310
35372
  } catch (error) {
34311
35373
  if (error.code !== "ENOENT") throw error;
@@ -34333,10 +35395,10 @@ async function writeShareOutput(directory, filename, bytes, force) {
34333
35395
  if (temporaryPath !== void 0) await unlink3(temporaryPath);
34334
35396
  } catch {
34335
35397
  }
34336
- await rm5(stagingDirectory, { recursive: true, force: true });
35398
+ await rm6(stagingDirectory, { recursive: true, force: true });
34337
35399
  await directoryHandle.close();
34338
35400
  }
34339
- return join10(outputDirectory, safeName);
35401
+ return join12(outputDirectory, safeName);
34340
35402
  }
34341
35403
 
34342
35404
  // src/share/errors.ts
@@ -34619,7 +35681,7 @@ function registerShareCommand(program) {
34619
35681
  if (json) writeJson2({ ...redactPublishedShare(result), expiryClamped: result.metadata.expiryClamped === true, ...notification === void 0 ? {} : { notification } });
34620
35682
  else publishHuman(result);
34621
35683
  } catch (error) {
34622
- handleError(shareCliError(error));
35684
+ return handleError(shareCliError(error));
34623
35685
  }
34624
35686
  });
34625
35687
  share.command("inspect [url]").description("Verify a share link and print safe metadata").option("--stdin", "Read the complete URL from stdin").option("--json", "Print versioned redacted JSON").option("--viewer-origin <origin>", "Require this canonical Share origin", SHARE_ORIGIN).action(async (url, options, command) => {
@@ -34629,7 +35691,7 @@ function registerShareCommand(program) {
34629
35691
  if (json) writeJson2(result);
34630
35692
  else inspectHuman(result);
34631
35693
  } catch (error) {
34632
- handleError(shareCliError(error));
35694
+ return handleError(shareCliError(error));
34633
35695
  }
34634
35696
  });
34635
35697
  share.command("receive [url]").description("Verify and receive a share link").option("--stdin", "Read the complete URL from stdin").option("--output <directory>", "Create the file in this directory").option("--stdout", "Write verified plaintext bytes to stdout").option("--force", "Allow replacing an existing non-symlink output").option("--max-bytes <bytes>", "Bound received content bytes").option("--json", "Print versioned redacted JSON").option("--viewer-origin <origin>", "Require this canonical Share origin", SHARE_ORIGIN).action(async (url, options, command) => {
@@ -34677,7 +35739,7 @@ function registerShareCommand(program) {
34677
35739
  if (json) receiveJson(result, output2);
34678
35740
  else receiveHuman(output2);
34679
35741
  } catch (error) {
34680
- handleError(shareCliError(error));
35742
+ return handleError(shareCliError(error));
34681
35743
  }
34682
35744
  });
34683
35745
  share.command("list").description("List encrypted sender history without complete bearer URLs").option("--json", "Print versioned redacted JSON").action(async (options, command) => {
@@ -34688,7 +35750,7 @@ function registerShareCommand(program) {
34688
35750
  if (json) writeJson2({ protocol: "tinycloud-share", version: 1, shares: result });
34689
35751
  else process.stdout.write(result.map((item) => `${item.shareId} ${item.target} ${item.expiresAt}`).join("\n") + (result.length ? "\n" : ""));
34690
35752
  } catch (error) {
34691
- handleError(shareCliError(error));
35753
+ return handleError(shareCliError(error));
34692
35754
  }
34693
35755
  });
34694
35756
  share.command("show <id>").description("Show one redacted sender-history record").option("--reveal-link", "Explicitly include the complete link").option("--json", "Print versioned redacted JSON").action(async (id, options, command) => {
@@ -34700,7 +35762,7 @@ function registerShareCommand(program) {
34700
35762
  if (json) writeJson2({ protocol: "tinycloud-share", version: 1, share: result });
34701
35763
  else writeJson2(result);
34702
35764
  } catch (error) {
34703
- handleError(shareCliError(error));
35765
+ return handleError(shareCliError(error));
34704
35766
  }
34705
35767
  });
34706
35768
  share.command("notify <id>").description("Retry idempotent delivery within 5 minutes of publication; after that publish a new share").requiredOption("--to <address>", "Recipient email").option("--json", "Print versioned JSON").action(async (id, options, command) => {
@@ -34727,7 +35789,7 @@ function registerShareCommand(program) {
34727
35789
  if (result.reason === "delivery-window-expired") process.stderr.write(NOTIFY_WINDOW_MESSAGE);
34728
35790
  if (result.state === "partial-failure") process.exitCode = 9;
34729
35791
  } catch (error) {
34730
- handleError(shareCliError(error, "notify"));
35792
+ return handleError(shareCliError(error, "notify"));
34731
35793
  }
34732
35794
  });
34733
35795
  share.command("revoke <id>").description("Revoke addressed shares; report bearer retention honestly").option("--ancestor", "Revoke the owner delegation ancestry").option("--json", "Print versioned JSON").action(async (id, options, command) => {
@@ -34744,7 +35806,7 @@ function registerShareCommand(program) {
34744
35806
  else process.stdout.write(`${result.state}
34745
35807
  `);
34746
35808
  } catch (error) {
34747
- handleError(shareCliError(error));
35809
+ return handleError(shareCliError(error));
34748
35810
  }
34749
35811
  });
34750
35812
  }
@@ -34759,7 +35821,7 @@ init_host();
34759
35821
  init_theme();
34760
35822
  import { randomBytes as randomBytes7 } from "crypto";
34761
35823
  import { mkdir as mkdir5, writeFile as writeFile4 } from "fs/promises";
34762
- import { dirname as dirname5 } from "path";
35824
+ import { dirname as dirname6 } from "path";
34763
35825
  function didWithoutFragment2(did) {
34764
35826
  const fragment = did.indexOf("#");
34765
35827
  return fragment === -1 ? did : did.slice(0, fragment);
@@ -34790,7 +35852,7 @@ function registerSpaceCommand(program) {
34790
35852
  }
34791
35853
  }
34792
35854
  } catch (error) {
34793
- handleError(error);
35855
+ return handleError(error);
34794
35856
  }
34795
35857
  });
34796
35858
  space.command("create <name>").alias("host").description("Create (host) one of your owned spaces by name").action(async (name, _options, cmd) => {
@@ -34801,7 +35863,7 @@ function registerSpaceCommand(program) {
34801
35863
  const spaceId = await node.hostOwnedSpace(name);
34802
35864
  outputJson({ spaceId, name, hosted: true });
34803
35865
  } catch (error) {
34804
- handleError(error);
35866
+ return handleError(error);
34805
35867
  }
34806
35868
  });
34807
35869
  space.command("host-request <name>").description("Emit a request asking the space owner to host it (delegate-only)").option("--emit [file]", "Write the request artifact to file (or stdout when no path)").addHelpText("after", `
@@ -34848,7 +35910,7 @@ it directly with \`tc space host <name>\` (no request needed).
34848
35910
  };
34849
35911
  await emitHostRequestArtifact(artifact, options.emit);
34850
35912
  } catch (error) {
34851
- handleError(error);
35913
+ return handleError(error);
34852
35914
  }
34853
35915
  });
34854
35916
  space.command("info [space-id]").description("Get space info").action(async (spaceId, _options, cmd) => {
@@ -34868,7 +35930,7 @@ it directly with \`tc space host <name>\` (no request needed).
34868
35930
  host: ctx.host
34869
35931
  });
34870
35932
  } catch (error) {
34871
- handleError(error);
35933
+ return handleError(error);
34872
35934
  }
34873
35935
  });
34874
35936
  space.command("switch <name>").description("Switch active space").action(async (name, _options, cmd) => {
@@ -34878,13 +35940,13 @@ it directly with \`tc space host <name>\` (no request needed).
34878
35940
  await ProfileManager.updateProfile(ctx.profile, (profile) => ({ ...profile, spaceName: name }));
34879
35941
  outputJson({ profile: ctx.profile, spaceName: name, switched: true });
34880
35942
  } catch (error) {
34881
- handleError(error);
35943
+ return handleError(error);
34882
35944
  }
34883
35945
  });
34884
35946
  }
34885
35947
  async function emitHostRequestArtifact(artifact, emitOption) {
34886
35948
  if (typeof emitOption === "string" && emitOption.length > 0) {
34887
- await mkdir5(dirname5(emitOption), { recursive: true });
35949
+ await mkdir5(dirname6(emitOption), { recursive: true });
34888
35950
  await writeFile4(emitOption, JSON.stringify(artifact, null, 2) + "\n", "utf8");
34889
35951
  outputJson({
34890
35952
  emitted: true,
@@ -34991,7 +36053,7 @@ ${rowCount} row${rowCount === 1 ? "" : "s"} returned`) + "\n");
34991
36053
  }
34992
36054
  }
34993
36055
  } catch (error) {
34994
- handleError(error);
36056
+ return handleError(error);
34995
36057
  }
34996
36058
  });
34997
36059
  sql.command("execute <sql>").description("Run a write or schema statement").option("--db <name>", "SQLite database name within the current space", "default").option("--space <name|uri>", "Target a non-primary space (short name or full URI)").option("--params <json>", "Bind parameters as a JSON array for ? placeholders").addHelpText("after", `
@@ -35024,7 +36086,7 @@ Output:
35024
36086
  lastInsertRowId: result.data.lastInsertRowId
35025
36087
  });
35026
36088
  } catch (error) {
35027
- handleError(error);
36089
+ return handleError(error);
35028
36090
  }
35029
36091
  });
35030
36092
  sql.command("export").description("Export a SQLite database as a binary .db file").option("--db <name>", "SQLite database name within the current space", "default").option("--space <name|uri>", "Target a non-primary space (short name or full URI)").option("-o, --output <file>", "Output file path", "export.db").addHelpText("after", `
@@ -35059,7 +36121,7 @@ Output:
35059
36121
  sizeHuman: formatBytes(blob.size)
35060
36122
  });
35061
36123
  } catch (error) {
35062
- handleError(error);
36124
+ return handleError(error);
35063
36125
  }
35064
36126
  });
35065
36127
  sql.command("copy").description("Copy rows between SQL databases (optionally across spaces)").requiredOption("--from-db <name>", "Source database name").requiredOption("--to-db <name>", "Destination database name").option("--from-space <name|uri>", "Source space (defaults to primary)").option("--to-space <name|uri>", "Destination space (defaults to primary)").option("--table <name...>", "Restrict copy to specific tables (repeat or comma-separated)").option("--dry-run", "Print the plan without writing", false).addHelpText("after", `
@@ -35162,7 +36224,7 @@ Notes:
35162
36224
  tables: plan.map((p) => ({ table: p.table, rowsRead: p.rows, rowsWritten: p.copied }))
35163
36225
  });
35164
36226
  } catch (error) {
35165
- handleError(error);
36227
+ return handleError(error);
35166
36228
  }
35167
36229
  });
35168
36230
  }
@@ -35218,7 +36280,7 @@ function registerStatusCommand(program) {
35218
36280
  }
35219
36281
  process.stdout.write(formatStatus(summary));
35220
36282
  } catch (error) {
35221
- handleError(error);
36283
+ return handleError(error);
35222
36284
  }
35223
36285
  });
35224
36286
  }
@@ -35519,10 +36581,10 @@ function formatSession(session) {
35519
36581
  return `${theme.success("present")}${formatExpiresAt(session.expiresAt)}`;
35520
36582
  }
35521
36583
  function formatDelegation2(delegation) {
35522
- const state = delegation.expired === true ? theme.warn("expired") : theme.success("active");
36584
+ const state2 = delegation.expired === true ? theme.warn("expired") : theme.success("active");
35523
36585
  return [
35524
36586
  delegation.cid,
35525
- state,
36587
+ state2,
35526
36588
  formatExpiresAt(delegation.expiresAt).trim(),
35527
36589
  plural(delegation.permissions.length, "permission")
35528
36590
  ].filter(Boolean).join(" ");
@@ -35599,13 +36661,13 @@ function registerUpgradeCommand(program) {
35599
36661
  `);
35600
36662
  }
35601
36663
  } catch (error) {
35602
- handleError(error);
36664
+ return handleError(error);
35603
36665
  }
35604
36666
  });
35605
36667
  }
35606
36668
 
35607
36669
  // src/commands/vault.ts
35608
- import { readFile as readFile11 } from "fs/promises";
36670
+ import { readFile as readFile12 } from "fs/promises";
35609
36671
  init_profiles();
35610
36672
  init_formatter();
35611
36673
  init_errors();
@@ -35648,7 +36710,7 @@ function registerVaultCommand(program) {
35648
36710
  await withSpinner("Unlocking vault...", () => unlockVault(node, privateKey));
35649
36711
  outputJson({ unlocked: true });
35650
36712
  } catch (error) {
35651
- handleError(error);
36713
+ return handleError(error);
35652
36714
  }
35653
36715
  });
35654
36716
  vault.command("put <key> [value]").description("Encrypt and store a value").option("--file <path>", "Read value from file").option("--stdin", "Read value from stdin").option("--private-key <hex>", "Ethereum private key (or set TC_PRIVATE_KEY)").action(async (key, value, options, cmd) => {
@@ -35667,7 +36729,7 @@ function registerVaultCommand(program) {
35667
36729
  throw new CLIError("USAGE_ERROR", "Provide only one of: value argument, --file, or --stdin", ExitCode.USAGE_ERROR);
35668
36730
  }
35669
36731
  if (options.file) {
35670
- putValue = new Uint8Array(await readFile11(options.file));
36732
+ putValue = new Uint8Array(await readFile12(options.file));
35671
36733
  } else if (options.stdin) {
35672
36734
  putValue = new Uint8Array(await readStdin4());
35673
36735
  } else {
@@ -35679,7 +36741,7 @@ function registerVaultCommand(program) {
35679
36741
  }
35680
36742
  outputJson({ key, written: true });
35681
36743
  } catch (error) {
35682
- handleError(error);
36744
+ return handleError(error);
35683
36745
  }
35684
36746
  });
35685
36747
  vault.command("get <key>").description("Decrypt and retrieve a value").option("--raw", "Output raw value (no JSON wrapping)").option("-o, --output <file>", "Write value to file").option("--private-key <hex>", "Ethereum private key (or set TC_PRIVATE_KEY)").action(async (key, options, cmd) => {
@@ -35689,7 +36751,7 @@ function registerVaultCommand(program) {
35689
36751
  const privateKey = resolvePrivateKey(options);
35690
36752
  const node = await ensureAuthenticated(ctx, { privateKey });
35691
36753
  await withSpinner("Unlocking vault...", () => unlockVault(node, privateKey));
35692
- const result = await withSpinner(`Getting ${key}...`, () => node.vault.get(key));
36754
+ const result = await withSpinner(`Getting ${key}...`, () => node.vault.get(key, { source: "network" }));
35693
36755
  if (!result.ok) {
35694
36756
  if (result.error.code === "NOT_FOUND") {
35695
36757
  throw new CLIError("NOT_FOUND", `Key "${key}" not found`, ExitCode.NOT_FOUND);
@@ -35713,7 +36775,7 @@ function registerVaultCommand(program) {
35713
36775
  data: data instanceof Uint8Array ? Buffer.from(data).toString("base64") : data
35714
36776
  });
35715
36777
  } catch (error) {
35716
- handleError(error);
36778
+ return handleError(error);
35717
36779
  }
35718
36780
  });
35719
36781
  vault.command("delete <key>").description("Delete an encrypted key").option("--private-key <hex>", "Ethereum private key (or set TC_PRIVATE_KEY)").action(async (key, options, cmd) => {
@@ -35729,7 +36791,7 @@ function registerVaultCommand(program) {
35729
36791
  }
35730
36792
  outputJson({ key, deleted: true });
35731
36793
  } catch (error) {
35732
- handleError(error);
36794
+ return handleError(error);
35733
36795
  }
35734
36796
  });
35735
36797
  vault.command("list").description("List vault keys").option("--prefix <prefix>", "Filter by key prefix").option("--private-key <hex>", "Ethereum private key (or set TC_PRIVATE_KEY)").action(async (options, cmd) => {
@@ -35739,7 +36801,7 @@ function registerVaultCommand(program) {
35739
36801
  const privateKey = resolvePrivateKey(options);
35740
36802
  const node = await ensureAuthenticated(ctx, { privateKey });
35741
36803
  await withSpinner("Unlocking vault...", () => unlockVault(node, privateKey));
35742
- const listOptions = options.prefix ? { prefix: options.prefix } : void 0;
36804
+ const listOptions = options.prefix ? { prefix: options.prefix, source: "network" } : { source: "network" };
35743
36805
  const result = await withSpinner("Listing vault keys...", () => node.vault.list(listOptions));
35744
36806
  if (!result.ok) {
35745
36807
  throw cliErrorFromService(result.error);
@@ -35752,7 +36814,7 @@ function registerVaultCommand(program) {
35752
36814
  prefix: options.prefix ?? null
35753
36815
  });
35754
36816
  } catch (error) {
35755
- handleError(error);
36817
+ return handleError(error);
35756
36818
  }
35757
36819
  });
35758
36820
  vault.command("head <key>").description("Get metadata for a vault key").option("--private-key <hex>", "Ethereum private key (or set TC_PRIVATE_KEY)").action(async (key, options, cmd) => {
@@ -35776,13 +36838,13 @@ function registerVaultCommand(program) {
35776
36838
  metadata: result.data.headers ?? result.data
35777
36839
  });
35778
36840
  } catch (error) {
35779
- handleError(error);
36841
+ return handleError(error);
35780
36842
  }
35781
36843
  });
35782
36844
  }
35783
36845
 
35784
36846
  // src/commands/vars.ts
35785
- import { readFile as readFile12 } from "fs/promises";
36847
+ import { readFile as readFile13 } from "fs/promises";
35786
36848
  init_profiles();
35787
36849
  init_formatter();
35788
36850
  init_errors();
@@ -35827,7 +36889,7 @@ function registerVarsCommand(program) {
35827
36889
  count: keyList.length
35828
36890
  });
35829
36891
  } catch (error) {
35830
- handleError(error);
36892
+ return handleError(error);
35831
36893
  }
35832
36894
  });
35833
36895
  vars.command("get <name>").description("Get a variable value").option("--raw", "Output raw value (no JSON wrapping)").option("-o, --output <file>", "Write value to file").option("--private-key <hex>", "Ethereum private key (or set TC_PRIVATE_KEY)").action(async (name, options, cmd) => {
@@ -35869,7 +36931,7 @@ function registerVarsCommand(program) {
35869
36931
  }
35870
36932
  outputJson({ name, value });
35871
36933
  } catch (error) {
35872
- handleError(error);
36934
+ return handleError(error);
35873
36935
  }
35874
36936
  });
35875
36937
  vars.command("put <name> [value]").description("Set a variable").option("--file <path>", "Read value from file").option("--stdin", "Read value from stdin").option("--private-key <hex>", "Ethereum private key (or set TC_PRIVATE_KEY)").action(async (name, value, options, cmd) => {
@@ -35887,7 +36949,7 @@ function registerVarsCommand(program) {
35887
36949
  throw new CLIError("USAGE_ERROR", "Provide only one of: value argument, --file, or --stdin", ExitCode.USAGE_ERROR);
35888
36950
  }
35889
36951
  if (options.file) {
35890
- varValue = await readFile12(options.file, "utf-8");
36952
+ varValue = await readFile13(options.file, "utf-8");
35891
36953
  } else if (options.stdin) {
35892
36954
  varValue = (await readStdin5()).toString("utf-8");
35893
36955
  } else {
@@ -35904,7 +36966,7 @@ function registerVarsCommand(program) {
35904
36966
  }
35905
36967
  outputJson({ name, written: true });
35906
36968
  } catch (error) {
35907
- handleError(error);
36969
+ return handleError(error);
35908
36970
  }
35909
36971
  });
35910
36972
  vars.command("delete <name>").description("Delete a variable").option("--private-key <hex>", "Ethereum private key (or set TC_PRIVATE_KEY)").action(async (name, options, cmd) => {
@@ -35920,7 +36982,7 @@ function registerVarsCommand(program) {
35920
36982
  }
35921
36983
  outputJson({ name, deleted: true });
35922
36984
  } catch (error) {
35923
- handleError(error);
36985
+ return handleError(error);
35924
36986
  }
35925
36987
  });
35926
36988
  }