@tinycloud/node-sdk 3.2.6-beta.6 → 3.3.0-beta.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/{WasmKeyProvider-DEdSyQ0I.d.cts → TinyCloudNode-BchWB_zv.d.cts} +162 -283
  2. package/dist/{WasmKeyProvider-DEdSyQ0I.d.ts → TinyCloudNode-BchWB_zv.d.ts} +162 -283
  3. package/dist/WasmKeyProvider-5Ee7fcAw.d.cts +217 -0
  4. package/dist/WasmKeyProvider-5Ee7fcAw.d.ts +217 -0
  5. package/dist/authority-4HXYteKF.d.ts +118 -0
  6. package/dist/authority-lrA-698X.d.cts +118 -0
  7. package/dist/core.cjs +368 -147
  8. package/dist/core.cjs.map +1 -1
  9. package/dist/core.d.cts +37 -4
  10. package/dist/core.d.ts +37 -4
  11. package/dist/core.js +374 -155
  12. package/dist/core.js.map +1 -1
  13. package/dist/gated-network-BEF2XSTC.d.cts +16 -0
  14. package/dist/gated-network-BEF2XSTC.d.ts +16 -0
  15. package/dist/index.cjs +2702 -524
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.d.cts +35 -89
  18. package/dist/index.d.ts +35 -89
  19. package/dist/index.js +2702 -530
  20. package/dist/index.js.map +1 -1
  21. package/dist/replication/authority.cjs +153 -15
  22. package/dist/replication/authority.cjs.map +1 -1
  23. package/dist/replication/authority.d.cts +4 -1
  24. package/dist/replication/authority.d.ts +4 -1
  25. package/dist/replication/authority.js +147 -14
  26. package/dist/replication/authority.js.map +1 -1
  27. package/dist/replication/gated-network.cjs +90 -0
  28. package/dist/replication/gated-network.cjs.map +1 -0
  29. package/dist/replication/gated-network.d.cts +2 -0
  30. package/dist/replication/gated-network.d.ts +2 -0
  31. package/dist/replication/gated-network.js +65 -0
  32. package/dist/replication/gated-network.js.map +1 -0
  33. package/dist/replication/runtime.cjs +84 -6
  34. package/dist/replication/runtime.cjs.map +1 -1
  35. package/dist/replication/runtime.d.cts +2 -1
  36. package/dist/replication/runtime.d.ts +2 -1
  37. package/dist/replication/runtime.js +84 -6
  38. package/dist/replication/runtime.js.map +1 -1
  39. package/dist/{runtime-B5P5SRpC.d.ts → runtime-D1o9Eybt.d.cts} +14 -3
  40. package/dist/{runtime-B5P5SRpC.d.cts → runtime-v62FzQHz.d.ts} +14 -3
  41. package/package.json +14 -4
  42. package/dist/authority-DDGtccBK.d.cts +0 -96
  43. package/dist/authority-DDGtccBK.d.ts +0 -96
package/dist/index.js CHANGED
@@ -4413,11 +4413,11 @@ var init_lib4 = __esm({
4413
4413
  init_version4();
4414
4414
  __awaiter = function(thisArg, _arguments, P, generator) {
4415
4415
  function adopt(value) {
4416
- return value instanceof P ? value : new P(function(resolve) {
4417
- resolve(value);
4416
+ return value instanceof P ? value : new P(function(resolve2) {
4417
+ resolve2(value);
4418
4418
  });
4419
4419
  }
4420
- return new (P || (P = Promise))(function(resolve, reject2) {
4420
+ return new (P || (P = Promise))(function(resolve2, reject2) {
4421
4421
  function fulfilled(value) {
4422
4422
  try {
4423
4423
  step(generator.next(value));
@@ -4433,7 +4433,7 @@ var init_lib4 = __esm({
4433
4433
  }
4434
4434
  }
4435
4435
  function step(result) {
4436
- result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
4436
+ result.done ? resolve2(result.value) : adopt(result.value).then(fulfilled, rejected);
4437
4437
  }
4438
4438
  step((generator = generator.apply(thisArg, _arguments || [])).next());
4439
4439
  });
@@ -8049,11 +8049,11 @@ var init_typed_data = __esm({
8049
8049
  init_id();
8050
8050
  __awaiter2 = function(thisArg, _arguments, P, generator) {
8051
8051
  function adopt(value) {
8052
- return value instanceof P ? value : new P(function(resolve) {
8053
- resolve(value);
8052
+ return value instanceof P ? value : new P(function(resolve2) {
8053
+ resolve2(value);
8054
8054
  });
8055
8055
  }
8056
- return new (P || (P = Promise))(function(resolve, reject2) {
8056
+ return new (P || (P = Promise))(function(resolve2, reject2) {
8057
8057
  function fulfilled(value) {
8058
8058
  try {
8059
8059
  step(generator.next(value));
@@ -8069,7 +8069,7 @@ var init_typed_data = __esm({
8069
8069
  }
8070
8070
  }
8071
8071
  function step(result) {
8072
- result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
8072
+ result.done ? resolve2(result.value) : adopt(result.value).then(fulfilled, rejected);
8073
8073
  }
8074
8074
  step((generator = generator.apply(thisArg, _arguments || [])).next());
8075
8075
  });
@@ -14911,7 +14911,7 @@ var require_scrypt = __commonJS({
14911
14911
  }
14912
14912
  const lib = {
14913
14913
  scrypt: function(password, salt, N2, r2, p, dkLen, progressCallback) {
14914
- return new Promise(function(resolve, reject2) {
14914
+ return new Promise(function(resolve2, reject2) {
14915
14915
  let lastProgress = 0;
14916
14916
  if (progressCallback) {
14917
14917
  progressCallback(0);
@@ -14923,7 +14923,7 @@ var require_scrypt = __commonJS({
14923
14923
  if (progressCallback && lastProgress !== 1) {
14924
14924
  progressCallback(1);
14925
14925
  }
14926
- resolve(new Uint8Array(key2));
14926
+ resolve2(new Uint8Array(key2));
14927
14927
  } else if (progressCallback && progress !== lastProgress) {
14928
14928
  lastProgress = progress;
14929
14929
  return progressCallback(progress);
@@ -15303,11 +15303,11 @@ var init_keystore = __esm({
15303
15303
  init_version15();
15304
15304
  __awaiter3 = function(thisArg, _arguments, P, generator) {
15305
15305
  function adopt(value) {
15306
- return value instanceof P ? value : new P(function(resolve) {
15307
- resolve(value);
15306
+ return value instanceof P ? value : new P(function(resolve2) {
15307
+ resolve2(value);
15308
15308
  });
15309
15309
  }
15310
- return new (P || (P = Promise))(function(resolve, reject2) {
15310
+ return new (P || (P = Promise))(function(resolve2, reject2) {
15311
15311
  function fulfilled(value) {
15312
15312
  try {
15313
15313
  step(generator.next(value));
@@ -15323,7 +15323,7 @@ var init_keystore = __esm({
15323
15323
  }
15324
15324
  }
15325
15325
  function step(result) {
15326
- result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
15326
+ result.done ? resolve2(result.value) : adopt(result.value).then(fulfilled, rejected);
15327
15327
  }
15328
15328
  step((generator = generator.apply(thisArg, _arguments || [])).next());
15329
15329
  });
@@ -15616,11 +15616,11 @@ var init_lib25 = __esm({
15616
15616
  init_version19();
15617
15617
  __awaiter4 = function(thisArg, _arguments, P, generator) {
15618
15618
  function adopt(value) {
15619
- return value instanceof P ? value : new P(function(resolve) {
15620
- resolve(value);
15619
+ return value instanceof P ? value : new P(function(resolve2) {
15620
+ resolve2(value);
15621
15621
  });
15622
15622
  }
15623
- return new (P || (P = Promise))(function(resolve, reject2) {
15623
+ return new (P || (P = Promise))(function(resolve2, reject2) {
15624
15624
  function fulfilled(value) {
15625
15625
  try {
15626
15626
  step(generator.next(value));
@@ -15636,7 +15636,7 @@ var init_lib25 = __esm({
15636
15636
  }
15637
15637
  }
15638
15638
  function step(result) {
15639
- result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
15639
+ result.done ? resolve2(result.value) : adopt(result.value).then(fulfilled, rejected);
15640
15640
  }
15641
15641
  step((generator = generator.apply(thisArg, _arguments || [])).next());
15642
15642
  });
@@ -15698,11 +15698,11 @@ var init_lib26 = __esm({
15698
15698
  init_version20();
15699
15699
  __awaiter5 = function(thisArg, _arguments, P, generator) {
15700
15700
  function adopt(value) {
15701
- return value instanceof P ? value : new P(function(resolve) {
15702
- resolve(value);
15701
+ return value instanceof P ? value : new P(function(resolve2) {
15702
+ resolve2(value);
15703
15703
  });
15704
15704
  }
15705
- return new (P || (P = Promise))(function(resolve, reject2) {
15705
+ return new (P || (P = Promise))(function(resolve2, reject2) {
15706
15706
  function fulfilled(value) {
15707
15707
  try {
15708
15708
  step(generator.next(value));
@@ -15718,7 +15718,7 @@ var init_lib26 = __esm({
15718
15718
  }
15719
15719
  }
15720
15720
  function step(result) {
15721
- result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
15721
+ result.done ? resolve2(result.value) : adopt(result.value).then(fulfilled, rejected);
15722
15722
  }
15723
15723
  step((generator = generator.apply(thisArg, _arguments || [])).next());
15724
15724
  });
@@ -16020,11 +16020,11 @@ var init_lib27 = __esm({
16020
16020
  init_version21();
16021
16021
  __awaiter6 = function(thisArg, _arguments, P, generator) {
16022
16022
  function adopt(value) {
16023
- return value instanceof P ? value : new P(function(resolve) {
16024
- resolve(value);
16023
+ return value instanceof P ? value : new P(function(resolve2) {
16024
+ resolve2(value);
16025
16025
  });
16026
16026
  }
16027
- return new (P || (P = Promise))(function(resolve, reject2) {
16027
+ return new (P || (P = Promise))(function(resolve2, reject2) {
16028
16028
  function fulfilled(value) {
16029
16029
  try {
16030
16030
  step(generator.next(value));
@@ -16040,7 +16040,7 @@ var init_lib27 = __esm({
16040
16040
  }
16041
16041
  }
16042
16042
  function step(result) {
16043
- result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
16043
+ result.done ? resolve2(result.value) : adopt(result.value).then(fulfilled, rejected);
16044
16044
  }
16045
16045
  step((generator = generator.apply(thisArg, _arguments || [])).next());
16046
16046
  });
@@ -16258,11 +16258,11 @@ var init_geturl = __esm({
16258
16258
  init_lib2();
16259
16259
  __awaiter7 = function(thisArg, _arguments, P, generator) {
16260
16260
  function adopt(value) {
16261
- return value instanceof P ? value : new P(function(resolve) {
16262
- resolve(value);
16261
+ return value instanceof P ? value : new P(function(resolve2) {
16262
+ resolve2(value);
16263
16263
  });
16264
16264
  }
16265
- return new (P || (P = Promise))(function(resolve, reject2) {
16265
+ return new (P || (P = Promise))(function(resolve2, reject2) {
16266
16266
  function fulfilled(value) {
16267
16267
  try {
16268
16268
  step(generator.next(value));
@@ -16278,7 +16278,7 @@ var init_geturl = __esm({
16278
16278
  }
16279
16279
  }
16280
16280
  function step(result) {
16281
- result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
16281
+ result.done ? resolve2(result.value) : adopt(result.value).then(fulfilled, rejected);
16282
16282
  }
16283
16283
  step((generator = generator.apply(thisArg, _arguments || [])).next());
16284
16284
  });
@@ -16294,8 +16294,8 @@ __export(lib_exports21, {
16294
16294
  poll: () => poll
16295
16295
  });
16296
16296
  function staller(duration) {
16297
- return new Promise((resolve) => {
16298
- setTimeout(resolve, duration);
16297
+ return new Promise((resolve2) => {
16298
+ setTimeout(resolve2, duration);
16299
16299
  });
16300
16300
  }
16301
16301
  function bodyify(value, type) {
@@ -16415,7 +16415,7 @@ function _fetchData(connection, body, processFunc) {
16415
16415
  options.headers = flatHeaders;
16416
16416
  const runningTimeout = (function() {
16417
16417
  let timer = null;
16418
- const promise = new Promise(function(resolve, reject2) {
16418
+ const promise = new Promise(function(resolve2, reject2) {
16419
16419
  if (timeout) {
16420
16420
  timer = setTimeout(() => {
16421
16421
  if (timer == null) {
@@ -16585,7 +16585,7 @@ function poll(func, options) {
16585
16585
  if (options.interval == null) {
16586
16586
  options.interval = 250;
16587
16587
  }
16588
- return new Promise(function(resolve, reject2) {
16588
+ return new Promise(function(resolve2, reject2) {
16589
16589
  let timer = null;
16590
16590
  let done = false;
16591
16591
  const cancel = () => {
@@ -16611,7 +16611,7 @@ function poll(func, options) {
16611
16611
  return func().then(function(result) {
16612
16612
  if (result !== void 0) {
16613
16613
  if (cancel()) {
16614
- resolve(result);
16614
+ resolve2(result);
16615
16615
  }
16616
16616
  } else if (options.oncePoll) {
16617
16617
  options.oncePoll.once("poll", check);
@@ -16657,11 +16657,11 @@ var init_lib28 = __esm({
16657
16657
  init_geturl();
16658
16658
  __awaiter8 = function(thisArg, _arguments, P, generator) {
16659
16659
  function adopt(value) {
16660
- return value instanceof P ? value : new P(function(resolve) {
16661
- resolve(value);
16660
+ return value instanceof P ? value : new P(function(resolve2) {
16661
+ resolve2(value);
16662
16662
  });
16663
16663
  }
16664
- return new (P || (P = Promise))(function(resolve, reject2) {
16664
+ return new (P || (P = Promise))(function(resolve2, reject2) {
16665
16665
  function fulfilled(value) {
16666
16666
  try {
16667
16667
  step(generator.next(value));
@@ -16677,7 +16677,7 @@ var init_lib28 = __esm({
16677
16677
  }
16678
16678
  }
16679
16679
  function step(result) {
16680
- result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
16680
+ result.done ? resolve2(result.value) : adopt(result.value).then(fulfilled, rejected);
16681
16681
  }
16682
16682
  step((generator = generator.apply(thisArg, _arguments || [])).next());
16683
16683
  });
@@ -17035,44 +17035,6 @@ var require_utils2 = __commonJS({
17035
17035
  }
17036
17036
  });
17037
17037
 
17038
- // src/replication/config.ts
17039
- import { assertFolderPrefix as assertPureFolderPrefix, kvPrefixCovers } from "@tinycloud/sdk-services/kv/replication/prefix";
17040
- function assertValidReplicationConfig(replication) {
17041
- if (replication === void 0 || replication.enabled !== true) return;
17042
- if (replication.storage === void 0 || replication.storage === null) {
17043
- throw new TypeError("replication.enabled requires replication.storage");
17044
- }
17045
- if (!Array.isArray(replication.prefixes) || replication.prefixes.length === 0) {
17046
- throw new TypeError("replication.prefixes must be a non-empty array");
17047
- }
17048
- for (const prefix of replication.prefixes) {
17049
- try {
17050
- assertPureFolderPrefix(prefix);
17051
- } catch (error) {
17052
- throw new TypeError(error instanceof Error ? error.message : String(error));
17053
- }
17054
- }
17055
- for (const [index, prefix] of replication.prefixes.entries()) {
17056
- for (const other of replication.prefixes.slice(index + 1)) {
17057
- if (kvPrefixCovers(prefix, other) || kvPrefixCovers(other, prefix)) {
17058
- throw new TypeError(
17059
- `replication.prefixes must not overlap: ${JSON.stringify(prefix)} and ${JSON.stringify(other)}`
17060
- );
17061
- }
17062
- }
17063
- if ((prefix === "/" || prefix.split("/", 1)[0] === "vault") && replication.allowSecrets !== true) {
17064
- throw new TypeError(
17065
- `replication prefix ${JSON.stringify(prefix)} overlaps the vault namespace; pass allowSecrets to opt in`
17066
- );
17067
- }
17068
- }
17069
- }
17070
- var init_config = __esm({
17071
- "src/replication/config.ts"() {
17072
- "use strict";
17073
- }
17074
- });
17075
-
17076
17038
  // src/replication/authority-sign-in.ts
17077
17039
  import {
17078
17040
  KV,
@@ -17130,6 +17092,49 @@ var init_authority_sign_in = __esm({
17130
17092
  }
17131
17093
  });
17132
17094
 
17095
+ // src/replication/config.ts
17096
+ import { MAX_GRANT_TTL_MS } from "@tinycloud/sdk-services/kv/replication/grantPolicyConstants";
17097
+ import { assertFolderPrefix as assertPureFolderPrefix, kvPrefixCovers } from "@tinycloud/sdk-services/kv/replication/prefix";
17098
+ function assertValidReplicationConfig(replication) {
17099
+ if (replication === void 0 || !replication.enabled) return;
17100
+ if (replication.storage === void 0 || replication.storage === null) {
17101
+ throw new TypeError("replication.enabled requires replication.storage");
17102
+ }
17103
+ if (!Array.isArray(replication.prefixes) || replication.prefixes.length === 0) {
17104
+ throw new TypeError("replication.prefixes must be a non-empty array");
17105
+ }
17106
+ const grantTtlMs = replication.grantTtlMs;
17107
+ if (grantTtlMs !== void 0 && (!Number.isFinite(grantTtlMs) || grantTtlMs <= 0 || grantTtlMs > MAX_GRANT_TTL_MS)) {
17108
+ throw new TypeError(`replication.grantTtlMs must be a positive number of milliseconds no greater than ${MAX_GRANT_TTL_MS}`);
17109
+ }
17110
+ for (const prefix of replication.prefixes) {
17111
+ try {
17112
+ assertPureFolderPrefix(prefix);
17113
+ } catch (error) {
17114
+ throw new TypeError(error instanceof Error ? error.message : String(error));
17115
+ }
17116
+ }
17117
+ for (const [index, prefix] of replication.prefixes.entries()) {
17118
+ for (const other of replication.prefixes.slice(index + 1)) {
17119
+ if (kvPrefixCovers(prefix, other) || kvPrefixCovers(other, prefix)) {
17120
+ throw new TypeError(
17121
+ `replication.prefixes must not overlap: ${JSON.stringify(prefix)} and ${JSON.stringify(other)}`
17122
+ );
17123
+ }
17124
+ }
17125
+ if ((prefix === "/" || prefix.split("/", 1)[0] === "vault") && replication.allowSecrets !== true) {
17126
+ throw new TypeError(
17127
+ `replication prefix ${JSON.stringify(prefix)} overlaps the vault namespace; pass allowSecrets to opt in`
17128
+ );
17129
+ }
17130
+ }
17131
+ }
17132
+ var init_config = __esm({
17133
+ "src/replication/config.ts"() {
17134
+ "use strict";
17135
+ }
17136
+ });
17137
+
17133
17138
  // src/replication/runtime.ts
17134
17139
  var runtime_exports = {};
17135
17140
  __export(runtime_exports, {
@@ -17173,10 +17178,23 @@ var init_runtime = __esm({
17173
17178
  this.control = {
17174
17179
  status: async () => {
17175
17180
  await this.binding;
17181
+ if (this.gatedReason) return this.options.prefixes.map((prefix) => ({
17182
+ prefix,
17183
+ state: "gated",
17184
+ gate: this.gatedReason,
17185
+ errorCode: "REPLICA_GATED",
17186
+ pending: { inFlight: 0, committed: 0, ambiguous: 0 },
17187
+ pinned: [],
17188
+ lagMs: null
17189
+ }));
17176
17190
  return this.current ? this.current.status() : [];
17177
17191
  },
17178
17192
  sync: async (input) => {
17179
17193
  await this.binding;
17194
+ if (this.gatedReason) throw Object.assign(new Error("Replica restore is gated."), {
17195
+ code: "REPLICA_GATED",
17196
+ gate: this.gatedReason
17197
+ });
17180
17198
  if (this.purgedGeneration === this.generation) return;
17181
17199
  if (this.current) await this.current.sync(input);
17182
17200
  },
@@ -17224,11 +17242,59 @@ var init_runtime = __esm({
17224
17242
  for (const kv of this.attached.keys()) kv.setReadThrough(null);
17225
17243
  this.attached.clear();
17226
17244
  }
17245
+ /** Observe a refused restore without opening replica state; loader failure leaves raw network KV usable. */
17246
+ bindGatedNetworkOnly(input, loadGatedNetwork) {
17247
+ if (this.closed || !this.options.enabled) return Promise.resolve();
17248
+ const retired = this.unbind();
17249
+ const generation = this.generation;
17250
+ this.purgedGeneration = void 0;
17251
+ this.pendingPurge = void 0;
17252
+ this.pendingPurgeResult = void 0;
17253
+ this.pending.clear();
17254
+ this.lastController = void 0;
17255
+ this.lastPending = void 0;
17256
+ this.lastIdentity = void 0;
17257
+ this.gatedReason = input.gate;
17258
+ this.primarySpace = input.session.spaceId;
17259
+ this.binding = retired.then(async () => {
17260
+ let create;
17261
+ try {
17262
+ ({ createGatedNetworkReadThrough: create } = await loadGatedNetwork());
17263
+ } catch {
17264
+ return;
17265
+ }
17266
+ if (generation !== this.generation || this.closed || typeof create !== "function") return;
17267
+ const networkOnly = create({
17268
+ space: input.session.spaceId,
17269
+ prefixes: this.options.prefixes,
17270
+ now: () => this.clock.now(),
17271
+ emit: (event) => {
17272
+ if (generation !== this.generation || this.closed || this.gated !== networkOnly) return;
17273
+ try {
17274
+ this.options.onEvent?.(event);
17275
+ } catch {
17276
+ }
17277
+ const { type, ...data } = event;
17278
+ try {
17279
+ input.context.emit(type, data);
17280
+ } catch {
17281
+ }
17282
+ }
17283
+ });
17284
+ this.gated = networkOnly;
17285
+ for (const scoped of input.primaryKV) this.attach(scoped.space, scoped.kv);
17286
+ });
17287
+ return this.binding;
17288
+ }
17227
17289
  bind(input) {
17228
17290
  if (this.closed || !this.options.enabled) return Promise.resolve();
17291
+ this.gated = void 0;
17292
+ this.gatedReason = void 0;
17229
17293
  this.bindingActive = true;
17230
17294
  const generation = ++this.generation;
17231
17295
  this.purgedGeneration = void 0;
17296
+ this.unsubscribeConsentExpired?.();
17297
+ this.unsubscribeConsentExpired = void 0;
17232
17298
  const { context, session } = input;
17233
17299
  const emit = (event) => {
17234
17300
  try {
@@ -17261,11 +17327,17 @@ var init_runtime = __esm({
17261
17327
  return;
17262
17328
  }
17263
17329
  const key2 = services.replicationIdentityKey(identity);
17264
- let pending = this.pending.get(key2);
17265
- if (!pending) {
17266
- pending = this.options.storage.pendingWrites?.(identity) ?? services.createMemoryPendingStore(identity);
17267
- this.pending.set(key2, pending);
17330
+ let cached = this.pending.get(key2);
17331
+ const stamp = input.stamp;
17332
+ if (!cached || cached.stamp?.incarnation !== stamp?.incarnation || cached.stamp?.lifecycleEpoch !== stamp?.lifecycleEpoch) {
17333
+ const store = this.options.storage.pendingWrites?.(identity, { stamp });
17334
+ if (!store && this.options.storage.lifecycle) {
17335
+ throw Object.assign(new Error("Gated replication requires a stamped pending store"), { code: "REPLICA_GATED", gate: "gate_unavailable" });
17336
+ }
17337
+ cached = { stamp, store: store ?? services.createMemoryPendingStore(identity) };
17338
+ this.pending.set(key2, cached);
17268
17339
  }
17340
+ const pending = cached.store;
17269
17341
  this.lastPending = pending;
17270
17342
  const controller = services.createKVReplication({
17271
17343
  options: resolvedOptions(this.options),
@@ -17273,6 +17345,7 @@ var init_runtime = __esm({
17273
17345
  storage: this.options.storage,
17274
17346
  identity,
17275
17347
  session: { id: session.delegationCid, did: session.verificationMethod, space: session.spaceId },
17348
+ stamp: input.stamp,
17276
17349
  authority: input.authority,
17277
17350
  pending,
17278
17351
  scheduler: this.clock,
@@ -17282,6 +17355,9 @@ var init_runtime = __esm({
17282
17355
  previous: previousClose
17283
17356
  });
17284
17357
  this.current = controller;
17358
+ this.unsubscribeConsentExpired = this.options.storage.lifecycle?.subscribeConsentExpired?.(
17359
+ (stamp2) => controller.observeConsentExpired(stamp2)
17360
+ );
17285
17361
  this.lastController = controller;
17286
17362
  this.lastIdentity = identity;
17287
17363
  for (const [kv, space] of this.attached) {
@@ -17302,17 +17378,24 @@ var init_runtime = __esm({
17302
17378
  }
17303
17379
  attach(space, kv) {
17304
17380
  this.attached.set(kv, space);
17305
- kv.setReadThrough(space === this.primarySpace ? this.current ?? null : null);
17381
+ kv.setReadThrough(space === this.primarySpace ? this.current ?? this.gated ?? null : null);
17382
+ const generation = this.generation;
17306
17383
  return this.binding.then(() => {
17307
- kv.setReadThrough(space === this.primarySpace ? this.current ?? null : null);
17384
+ if (generation === this.generation && !this.closed && this.attached.get(kv) === space) {
17385
+ kv.setReadThrough(space === this.primarySpace ? this.current ?? this.gated ?? null : null);
17386
+ }
17308
17387
  });
17309
17388
  }
17310
17389
  unbind() {
17311
17390
  this.generation++;
17312
17391
  this.bindingActive = false;
17313
17392
  this.pendingPurgeSet = false;
17393
+ this.unsubscribeConsentExpired?.();
17394
+ this.unsubscribeConsentExpired = void 0;
17314
17395
  const controller = this.current;
17315
17396
  this.current = void 0;
17397
+ this.gated = void 0;
17398
+ this.gatedReason = void 0;
17316
17399
  this.primarySpace = void 0;
17317
17400
  this.detachAll();
17318
17401
  const binding = this.binding;
@@ -17331,22 +17414,104 @@ var init_runtime = __esm({
17331
17414
  var authority_exports = {};
17332
17415
  __export(authority_exports, {
17333
17416
  AUTHORITY_EXPIRY_MARGIN_MS: () => AUTHORITY_EXPIRY_MARGIN_MS,
17417
+ assertRenewalParent: () => assertRenewalParent,
17334
17418
  assertValidReplicationConfig: () => assertValidReplicationConfig,
17335
17419
  augmentSignInEntriesWithReplication: () => augmentSignInEntriesWithReplication,
17336
17420
  createReplicationAuthority: () => createReplicationAuthority,
17421
+ establishReplicaSessionStamp: () => establishReplicaSessionStamp,
17337
17422
  hasUnrestrictedGetCoverage: () => hasUnrestrictedGetCoverage,
17423
+ mintNodeReplicationGrant: () => mintNodeReplicationGrant,
17338
17424
  replicationScopeFromSignedSession: () => replicationScopeFromSignedSession,
17425
+ reportReplicaDenial: () => reportReplicaDenial,
17426
+ restoreReplicaSessionStamp: () => restoreReplicaSessionStamp,
17339
17427
  ucanAttCovers: () => ucanAttCovers,
17340
- ucanAttUnconstrainedFor: () => ucanAttUnconstrainedFor
17428
+ ucanAttUnconstrainedFor: () => ucanAttUnconstrainedFor,
17429
+ verifiedOwnerConsentAnchor: () => verifiedOwnerConsentAnchor
17341
17430
  });
17431
+ import { SiweMessage as SiweMessage4 } from "siwe";
17342
17432
  import {
17343
17433
  actionContains,
17344
17434
  KV as KV4,
17345
17435
  canonicalizeRecapCaveats as canonicalizeRecapCaveats3,
17346
17436
  isCapabilitySubset as isCapabilitySubset3,
17437
+ principalDid,
17347
17438
  parseSpaceUri as parseSpaceUri2
17348
17439
  } from "@tinycloud/sdk-core";
17349
17440
  import { parseUcanGrant as replicaParseUcanGrant } from "@tinycloud/replica";
17441
+ function reportReplicaDenial(result, reason, onEvent, context) {
17442
+ if (result.status !== "denied" && result.status !== "deleted" && result.status !== "marker") return;
17443
+ const event = {
17444
+ type: "replication.lifecycle",
17445
+ at: (/* @__PURE__ */ new Date()).toISOString(),
17446
+ reason,
17447
+ action: result.status === "denied" ? "denied" : result.status === "deleted" ? "database_deleted" : "marker_set",
17448
+ ...result.status === "denied" ? { partitions: result.intents } : {},
17449
+ ...result.status === "marker" ? { code: result.code } : {}
17450
+ };
17451
+ try {
17452
+ onEvent?.(event);
17453
+ } catch {
17454
+ }
17455
+ const { type, ...data } = event;
17456
+ try {
17457
+ context?.emit(type, data);
17458
+ } catch {
17459
+ }
17460
+ }
17461
+ function verifiedOwnerConsentAnchor(session, wasmBindings, exactVerifiedRecap2) {
17462
+ if (typeof session.siwe !== "string" || typeof session.signature !== "string" || typeof session.address !== "string" || typeof session.chainId !== "number") return null;
17463
+ try {
17464
+ const verified = wasmBindings.validatePersistedSession?.({
17465
+ delegationHeader: session.delegationHeader,
17466
+ delegationCid: session.delegationCid,
17467
+ spaceId: session.spaceId,
17468
+ jwk: session.jwk,
17469
+ address: session.address,
17470
+ chainId: session.chainId,
17471
+ siwe: session.siwe,
17472
+ signature: session.signature
17473
+ });
17474
+ if (!verified || !Number.isSafeInteger(Date.parse(verified.expiresAt ?? ""))) return null;
17475
+ const signedIssuedAt = new SiweMessage4(session.siwe).issuedAt;
17476
+ if (typeof signedIssuedAt !== "string") return null;
17477
+ const issuedAt = Date.parse(signedIssuedAt);
17478
+ if (!Number.isSafeInteger(issuedAt) || issuedAt < 0) return null;
17479
+ const granted = exactVerifiedRecap2(verified).filter((entry) => entry.space === session.spaceId && entry.service === "kv").map((entry) => ({ ...entry, service: "tinycloud.kv" }));
17480
+ const coversGetAndSync = granted.some((entry) => isCapabilitySubset3([{
17481
+ service: "tinycloud.kv",
17482
+ space: session.spaceId,
17483
+ path: entry.path,
17484
+ actions: [KV4.GET, KV4.SYNC]
17485
+ }], granted).subset);
17486
+ return coversGetAndSync ? { at: issuedAt, source: "siwe" } : null;
17487
+ } catch {
17488
+ return null;
17489
+ }
17490
+ }
17491
+ function establishReplicaSessionStamp(storage, principal, anchor) {
17492
+ if (storage.kind === "indexeddb" && storage.lifecycle?.kind === "indexeddb") {
17493
+ return storage.lifecycle.establishSessionStamp(principal, { consentAnchor: anchor });
17494
+ }
17495
+ if (storage.kind === "sqlite" && storage.lifecycle?.kind === "file") {
17496
+ return storage.lifecycle.establishSessionStamp(principal, { consentAnchor: anchor });
17497
+ }
17498
+ throw Object.assign(new Error("Replica sign-in requires its platform lifecycle gate."), {
17499
+ code: "REPLICA_GATED",
17500
+ gate: "gate_unavailable"
17501
+ });
17502
+ }
17503
+ function restoreReplicaSessionStamp(storage, principal, persisted) {
17504
+ if (storage.kind === "indexeddb" && storage.lifecycle?.kind === "indexeddb") {
17505
+ return storage.lifecycle.restoreSessionStamp(principal, persisted);
17506
+ }
17507
+ if (storage.kind === "sqlite" && storage.lifecycle?.kind === "file") {
17508
+ return storage.lifecycle.restoreSessionStamp(principal, persisted);
17509
+ }
17510
+ throw Object.assign(new Error("Replica restore requires its platform lifecycle gate."), {
17511
+ code: "REPLICA_GATED",
17512
+ gate: "gate_unavailable"
17513
+ });
17514
+ }
17350
17515
  function replicationEntries(spaceId, prefix) {
17351
17516
  return [{
17352
17517
  service: KV_SERVICE2,
@@ -17394,6 +17559,20 @@ function ucanAttCovers(att, space, prefix, ability, options) {
17394
17559
  function ucanAttUnconstrainedFor(att, space, prefix) {
17395
17560
  return ucanAttCovers(att, space, prefix, KV_GET2) && ucanAttCovers(att, space, prefix, KV_SYNC2);
17396
17561
  }
17562
+ async function mintNodeReplicationGrant(deviceDid, entries, options, renewalSession, node) {
17563
+ if (options?.renewalOnly && !renewalSession) throw new Error("replication renewal lost its owner session");
17564
+ const delegateOptions = options?.renewalOnly ? {
17565
+ expiry: options.expiryMs,
17566
+ onPrepared: async (prepared) => {
17567
+ assertRenewalParent(prepared.delegationHeader.Authorization, renewalSession);
17568
+ }
17569
+ } : options?.expiryMs === void 0 ? void 0 : { expiry: options.expiryMs };
17570
+ const result = await node.delegateTo(principalDid(deviceDid), entries, delegateOptions);
17571
+ return {
17572
+ ucan: result.delegation.delegationHeader.Authorization,
17573
+ expiresAt: result.delegation.expiry.getTime()
17574
+ };
17575
+ }
17397
17576
  function replicationScopeFromSignedSession(input) {
17398
17577
  let grant;
17399
17578
  try {
@@ -17411,9 +17590,28 @@ function replicationScopeFromSignedSession(input) {
17411
17590
  }
17412
17591
  return void 0;
17413
17592
  }
17593
+ function assertRenewalParent(authorization, session) {
17594
+ const grant = parseUcanGrant(authorization);
17595
+ if (grant.issuer !== session.verificationMethod.split("#", 1)[0] || grant.prf.length !== 1 || grant.prf[0] !== session.delegationCid) {
17596
+ throw Object.assign(new Error("replication renewal requires the bound owner session parent"), { refused: "NOT_COVERED" });
17597
+ }
17598
+ }
17414
17599
  function createReplicationAuthority(host) {
17415
- const planOrRefusal = (entries) => {
17416
- const plan = host.planDelegation(entries);
17600
+ const boundSession = host.replicationSession();
17601
+ let verifiedAnchor = null;
17602
+ try {
17603
+ const candidate = boundSession && host.verifiedConsentAnchor?.(boundSession);
17604
+ if (candidate && Number.isFinite(candidate.at) && candidate.source !== "first_install") {
17605
+ verifiedAnchor = { at: candidate.at, source: candidate.source };
17606
+ }
17607
+ } catch {
17608
+ }
17609
+ const activeSession = () => {
17610
+ const session = host.replicationSession();
17611
+ return session !== void 0 && session === boundSession ? session : void 0;
17612
+ };
17613
+ const planOrRefusal = (entries, expiryMs) => {
17614
+ const plan = host.planDelegation(entries, expiryMs === void 0 ? void 0 : { expiry: expiryMs });
17417
17615
  if ("refused" in plan) return plan;
17418
17616
  return { path: plan.path, parentCid: plan.parentCid, expiresAt: plan.expiresAt };
17419
17617
  };
@@ -17425,12 +17623,20 @@ function createReplicationAuthority(host) {
17425
17623
  };
17426
17624
  const authority = {
17427
17625
  get sessionOnly() {
17428
- const session = host.replicationSession();
17429
- return session !== void 0 && session.siwe === void 0;
17626
+ const session = activeSession();
17627
+ return session === void 0 || session.siwe === void 0;
17628
+ },
17629
+ consentAnchor() {
17630
+ return activeSession() ? verifiedAnchor : null;
17631
+ },
17632
+ get canRenew() {
17633
+ const session = activeSession();
17634
+ return verifiedAnchor?.source === "siwe" && session !== void 0 && session.siwe !== void 0;
17430
17635
  },
17431
17636
  sessionGrant(prefix) {
17432
- const session = host.replicationSession();
17637
+ const session = activeSession();
17433
17638
  if (session === void 0) return { refused: "SESSION_EXPIRING" };
17639
+ if (verifiedAnchor === null && session.siwe !== void 0) return { refused: "NOT_COVERED" };
17434
17640
  const siweExpiry = session.siwe ? host.siweExpiration(session.siwe)?.getTime() : void 0;
17435
17641
  if (siweExpiry !== void 0 && siweExpiry <= Date.now() + AUTHORITY_EXPIRY_MARGIN_MS) {
17436
17642
  return { refused: "SESSION_EXPIRING" };
@@ -17458,8 +17664,9 @@ function createReplicationAuthority(host) {
17458
17664
  };
17459
17665
  },
17460
17666
  plan(prefix) {
17461
- const session = host.replicationSession();
17667
+ const session = activeSession();
17462
17668
  if (session === void 0) return { refused: "SESSION_EXPIRING" };
17669
+ if (verifiedAnchor === null) return { refused: "NOT_COVERED" };
17463
17670
  const sessionGrant = this.sessionGrant(prefix);
17464
17671
  if (!("refused" in sessionGrant)) {
17465
17672
  const signed2 = signedSessionGrant(session);
@@ -17473,26 +17680,30 @@ function createReplicationAuthority(host) {
17473
17680
  if (authority.sessionOnly) return sessionGrant;
17474
17681
  return planOrRefusal(replicationEntries(session.spaceId, prefix));
17475
17682
  },
17476
- async mint(deviceDid, prefix, signal) {
17477
- if (authority.sessionOnly) throw new Error("compact delegate sessions cannot mint device grants");
17478
- const session = host.replicationSession();
17479
- if (session === void 0) throw new Error("replication mint without a session");
17683
+ async mint(deviceDid, prefix, signal, options) {
17684
+ const session = activeSession();
17685
+ if (session === void 0) throw new Error("replication mint without the bound session");
17686
+ if (session.siwe === void 0) throw new Error("compact delegate sessions cannot mint device grants");
17687
+ if (verifiedAnchor?.source !== "siwe") throw new Error("replication mint requires verified owner consent");
17480
17688
  const entries = replicationEntries(session.spaceId, prefix);
17481
- const plan = planOrRefusal(entries);
17689
+ const plan = planOrRefusal(entries, options?.expiryMs);
17482
17690
  if ("refused" in plan) {
17483
17691
  throw Object.assign(
17484
17692
  new Error(`replication authority refused the mint: ${plan.refused}`),
17485
17693
  { refused: plan.refused }
17486
17694
  );
17487
17695
  }
17696
+ if (options?.renewalOnly && plan.path !== "session") {
17697
+ throw Object.assign(new Error("replication renewal requires a session parent"), { refused: "NOT_COVERED" });
17698
+ }
17488
17699
  signal.throwIfAborted();
17489
- const minted = await new Promise((resolve, reject2) => {
17700
+ const minted = await new Promise((resolve2, reject2) => {
17490
17701
  const onAbort = () => reject2(signal.reason ?? new Error("replication mint aborted"));
17491
17702
  signal.addEventListener("abort", onAbort, { once: true });
17492
- host.mintDelegation(deviceDid, entries).then(
17703
+ host.mintDelegation(deviceDid, entries, options).then(
17493
17704
  (result) => {
17494
17705
  signal.removeEventListener("abort", onAbort);
17495
- resolve(result);
17706
+ resolve2(result);
17496
17707
  },
17497
17708
  (error) => {
17498
17709
  signal.removeEventListener("abort", onAbort);
@@ -17519,6 +17730,77 @@ var init_authority = __esm({
17519
17730
  }
17520
17731
  });
17521
17732
 
17733
+ // src/replication/gated-network.ts
17734
+ var gated_network_exports = {};
17735
+ __export(gated_network_exports, {
17736
+ createGatedNetworkReadThrough: () => createGatedNetworkReadThrough
17737
+ });
17738
+ import { kvPrefixCovers as kvPrefixCovers2 } from "@tinycloud/sdk-services/kv/replication/prefix";
17739
+ function createGatedNetworkReadThrough(input) {
17740
+ const report = (op, space, path, replica, reason, outcome, latencyMs) => input.emit({
17741
+ type: "replication.read",
17742
+ at: new Date(input.now()).toISOString(),
17743
+ op,
17744
+ space,
17745
+ key: path,
17746
+ replica,
17747
+ source: reason === "cursor_restart" ? "none" : "network",
17748
+ reason,
17749
+ outcome,
17750
+ latencyMs,
17751
+ stalenessMs: null,
17752
+ coverage: null,
17753
+ authority: null,
17754
+ ...reason === "replica_gated" ? { code: "REPLICA_GATED" } : {}
17755
+ });
17756
+ const readNetwork = async (r2, op, path, prefix, reason) => {
17757
+ const started = input.now();
17758
+ let outcome = "error";
17759
+ try {
17760
+ const result = await r2.network();
17761
+ outcome = result.ok ? "found" : op === "get" && reason === "not_covered" && "error" in result && result.error.code === "KV_NOT_FOUND" ? "not_found" : "error";
17762
+ return result;
17763
+ } finally {
17764
+ report(op, r2.space, path, prefix, reason, outcome, input.now() - started);
17765
+ }
17766
+ };
17767
+ const covered = (path) => input.prefixes.find((prefix) => kvPrefixCovers2(prefix, path));
17768
+ return {
17769
+ get: (r2) => {
17770
+ if (r2.space !== input.space) return r2.network();
17771
+ if (r2.options?.source === "network") return readNetwork(r2, "get", r2.path, null, "NETWORK_REQUESTED");
17772
+ const prefix = covered(r2.path);
17773
+ return readNetwork(r2, "get", r2.path, prefix ?? null, prefix === void 0 ? "not_covered" : "replica_gated");
17774
+ },
17775
+ list: (r2) => {
17776
+ if (r2.space !== input.space) return r2.network();
17777
+ if (r2.options?.source === "network") return readNetwork(r2, "list", r2.listPath, null, "NETWORK_REQUESTED");
17778
+ if (r2.options?.cursor?.startsWith("tcr1.")) {
17779
+ report("list", r2.space, r2.listPath, null, "cursor_restart", "error", 0);
17780
+ return Promise.resolve({ ok: false, error: {
17781
+ code: "INVALID_INPUT",
17782
+ message: "Local list cursor cannot be served; restart without the cursor",
17783
+ service: "kv",
17784
+ meta: { replication: "cursor_restart" }
17785
+ } });
17786
+ }
17787
+ if (r2.options?.cursor) return readNetwork(r2, "list", r2.listPath, null, "network_cursor");
17788
+ if (r2.options?.raw) return readNetwork(r2, "list", r2.listPath, null, "unsupported_option");
17789
+ const prefix = covered(r2.listPath);
17790
+ return readNetwork(r2, "list", r2.listPath, prefix ?? null, prefix === void 0 ? "not_covered" : "replica_gated");
17791
+ },
17792
+ write: (r2) => r2.network(),
17793
+ observeNetworkRequested: (r2) => {
17794
+ if (r2.space === input.space) report(r2.op, r2.space, r2.path, null, r2.reason, r2.outcome, r2.latencyMs);
17795
+ }
17796
+ };
17797
+ }
17798
+ var init_gated_network = __esm({
17799
+ "src/replication/gated-network.ts"() {
17800
+ "use strict";
17801
+ }
17802
+ });
17803
+
17522
17804
  // src/NodeWasmBindings.ts
17523
17805
  import {
17524
17806
  invoke,
@@ -18205,7 +18487,6 @@ function deserializeDelegation(data) {
18205
18487
  }
18206
18488
 
18207
18489
  // src/TinyCloudNode.ts
18208
- init_config();
18209
18490
  import { generateNonce, SiweMessage as SiweMessage3 } from "siwe";
18210
18491
 
18211
18492
  // src/replication/module-registry.ts
@@ -18224,7 +18505,7 @@ function getNodeReplicationLoaders() {
18224
18505
  import {
18225
18506
  TinyCloud,
18226
18507
  checkNodeInfo as checkNodeInfo2,
18227
- VersionCheckError,
18508
+ ProtocolMismatchError,
18228
18509
  activateSessionWithHost as activateSessionWithHost2,
18229
18510
  authorizationVerdictOf,
18230
18511
  httpResponseError,
@@ -18275,7 +18556,6 @@ import {
18275
18556
  verifyDidKeyEd25519Signature,
18276
18557
  canonicalizeAddress as canonicalizeAddress2,
18277
18558
  pkhDid as pkhDid2,
18278
- principalDid,
18279
18559
  resolveTinyCloudHosts as resolveTinyCloudHosts2,
18280
18560
  publishLocationRecord,
18281
18561
  principalDidEquals as principalDidEquals3,
@@ -18541,6 +18821,7 @@ var NodeUserAuthorization = class {
18541
18821
  this.siweConfig = config.siweConfig;
18542
18822
  this.includeAccountRegistryPermissions = config.includeAccountRegistryPermissions ?? true;
18543
18823
  this.replication = config.replication?.enabled === true ? config.replication : void 0;
18824
+ this.establishReplicaStamp = config.establishReplicaStamp;
18544
18825
  this._manifest = config.manifest;
18545
18826
  this._capabilityRequest = config.capabilityRequest;
18546
18827
  this.sessionManager = config.sessionManager ?? this.wasm.createSessionManager();
@@ -19171,7 +19452,7 @@ var NodeUserAuthorization = class {
19171
19452
  );
19172
19453
  throw error;
19173
19454
  }
19174
- await new Promise((resolve) => setTimeout(resolve, 100));
19455
+ await new Promise((resolve2) => setTimeout(resolve2, 100));
19175
19456
  const retryResult = await activateSessionWithHost(
19176
19457
  host,
19177
19458
  this._tinyCloudSession.delegationHeader
@@ -19211,7 +19492,7 @@ var NodeUserAuthorization = class {
19211
19492
  );
19212
19493
  throw error;
19213
19494
  }
19214
- await new Promise((resolve) => setTimeout(resolve, 100));
19495
+ await new Promise((resolve2) => setTimeout(resolve2, 100));
19215
19496
  const retryResult = await activateSessionWithHost(
19216
19497
  host,
19217
19498
  this._tinyCloudSession.delegationHeader
@@ -19310,6 +19591,27 @@ var NodeUserAuthorization = class {
19310
19591
  signature: signature2
19311
19592
  };
19312
19593
  }
19594
+ persistedData(session, expiresAt, createdAt, replicaStamp) {
19595
+ return {
19596
+ address: session.address,
19597
+ chainId: session.chainId,
19598
+ sessionKey: JSON.stringify(session.jwk),
19599
+ siwe: session.siwe,
19600
+ signature: session.signature,
19601
+ tinycloudSession: {
19602
+ delegationHeader: session.delegationHeader,
19603
+ delegationCid: session.delegationCid,
19604
+ spaceId: session.spaceId,
19605
+ spaces: session.spaces,
19606
+ verificationMethod: session.verificationMethod
19607
+ },
19608
+ expiresAt,
19609
+ createdAt,
19610
+ version: "1.0",
19611
+ tinycloudHosts: this.tinycloudHosts,
19612
+ ...replicaStamp ? { replicaStamp } : {}
19613
+ };
19614
+ }
19313
19615
  /**
19314
19616
  * Sign in and create a new session.
19315
19617
  *
@@ -19389,24 +19691,13 @@ var NodeUserAuthorization = class {
19389
19691
  siwe: prepared.siwe,
19390
19692
  signature: signature2
19391
19693
  };
19392
- const persistedData = {
19393
- address,
19394
- chainId,
19395
- sessionKey: JSON.stringify(jwk),
19396
- siwe: prepared.siwe,
19397
- signature: signature2,
19398
- tinycloudSession: {
19399
- delegationHeader: session.delegationHeader,
19400
- delegationCid: session.delegationCid,
19401
- spaceId,
19402
- spaces: spacesMetadata,
19403
- verificationMethod: this.sessionManager.getDID(keyId)
19404
- },
19405
- expiresAt: expirationTime.toISOString(),
19406
- createdAt: now.toISOString(),
19407
- version: "1.0",
19408
- tinycloudHosts: this.tinycloudHosts
19409
- };
19694
+ const replicaStamp = await this.establishReplicaStamp?.(tinyCloudSession);
19695
+ const persistedData = this.persistedData(
19696
+ tinyCloudSession,
19697
+ expirationTime.toISOString(),
19698
+ now.toISOString(),
19699
+ replicaStamp
19700
+ );
19410
19701
  await this.enqueuePersistence(async () => {
19411
19702
  await this.sessionStorage.save(address, persistedData);
19412
19703
  if (generation !== this.sessionGeneration) {
@@ -19560,28 +19851,12 @@ var NodeUserAuthorization = class {
19560
19851
  siwe: prepared.siwe,
19561
19852
  signature: signature2
19562
19853
  };
19563
- const expirationMatch = prepared.siwe.match(/Expiration Time: (.+)/);
19564
- const issuedAtMatch = prepared.siwe.match(/Issued At: (.+)/);
19854
+ const expirationMatch = prepared.siwe?.match(/Expiration Time: (.+)/);
19855
+ const issuedAtMatch = prepared.siwe?.match(/Issued At: (.+)/);
19565
19856
  const expiresAt = expirationMatch?.[1] ?? new Date(Date.now() + this.sessionExpirationMs).toISOString();
19566
19857
  const createdAt = issuedAtMatch?.[1] ?? (/* @__PURE__ */ new Date()).toISOString();
19567
- const persistedData = {
19568
- address,
19569
- chainId,
19570
- sessionKey: JSON.stringify(jwk),
19571
- siwe: prepared.siwe,
19572
- signature: signature2,
19573
- tinycloudSession: {
19574
- delegationHeader: session.delegationHeader,
19575
- delegationCid: session.delegationCid,
19576
- spaceId: prepared.spaceId,
19577
- spaces: spacesMetadata,
19578
- verificationMethod: this.sessionManager.getDID(keyId)
19579
- },
19580
- expiresAt,
19581
- createdAt,
19582
- version: "1.0",
19583
- tinycloudHosts: this.tinycloudHosts
19584
- };
19858
+ const replicaStamp = await this.establishReplicaStamp?.(tinyCloudSession);
19859
+ const persistedData = this.persistedData(tinyCloudSession, expiresAt, createdAt, replicaStamp);
19585
19860
  await this.sessionStorage.save(address, persistedData);
19586
19861
  this._session = clientSession;
19587
19862
  this._tinyCloudSession = tinyCloudSession;
@@ -19644,15 +19919,36 @@ var NodeUserAuthorization = class {
19644
19919
  const { prepared, keyId, jwk } = await this.prepareSessionForSigning();
19645
19920
  const presentation = this.buildPresentationEnvelope(options?.reason);
19646
19921
  const hostHint = this.primaryTinyCloudHost;
19647
- const result = await authorizeFn({
19922
+ const originalRequest = {
19648
19923
  protocolVersion: 1,
19649
19924
  siwe: prepared.siwe,
19650
19925
  jwk,
19926
+ verificationMethod: prepared.verificationMethod,
19651
19927
  host: hostHint,
19652
19928
  keyId: options?.openkeyKeyId,
19653
19929
  presentation
19654
- });
19655
- return this.signInWithOpenKeyResult(result, prepared, keyId, jwk);
19930
+ };
19931
+ let result;
19932
+ let signed2;
19933
+ let signature2;
19934
+ for (let attempt = 0; ; ) {
19935
+ try {
19936
+ result ?? (result = await authorizeFn(originalRequest));
19937
+ signed2 = await this.verifyOpenKeyResult(result, prepared);
19938
+ signature2 = result.signature;
19939
+ break;
19940
+ } catch (error) {
19941
+ if (!options?.retryResult) throw error;
19942
+ const next = await options.retryResult(
19943
+ error instanceof Error ? error : new Error(String(error)),
19944
+ originalRequest,
19945
+ ++attempt
19946
+ );
19947
+ if (next === null) throw error;
19948
+ result = next;
19949
+ }
19950
+ }
19951
+ return this.signInWithPreparedSession(signed2, signature2, keyId, jwk);
19656
19952
  }
19657
19953
  /**
19658
19954
  * Build a `CapabilityPresentationEnvelopeV1` from `this._manifest`. The
@@ -19732,6 +20028,11 @@ var NodeUserAuthorization = class {
19732
20028
  * (resource, action) pair in the returned SIWE.
19733
20029
  */
19734
20030
  async signInWithOpenKeyResult(result, prepared, keyId, jwk) {
20031
+ const signed2 = await this.verifyOpenKeyResult(result, prepared);
20032
+ return this.signInWithPreparedSession(signed2, result.signature, keyId, jwk);
20033
+ }
20034
+ /** Validate a candidate without activating a network session, so paste retries reuse one preparation. */
20035
+ async verifyOpenKeyResult(result, prepared) {
19735
20036
  if (result.protocolVersion !== 1) {
19736
20037
  throw new Error(`Unsupported OpenKey protocol version ${result.protocolVersion}`);
19737
20038
  }
@@ -19976,17 +20277,12 @@ var NodeUserAuthorization = class {
19976
20277
  }
19977
20278
  }
19978
20279
  void nonRequiredGrantedPairs;
19979
- return this.signInWithPreparedSession(
19980
- {
19981
- siwe: result.signedMessage,
19982
- jwk: prepared.jwk,
19983
- spaceId: prepared.spaceId,
19984
- verificationMethod: prepared.verificationMethod
19985
- },
19986
- result.signature,
19987
- keyId,
19988
- jwk
19989
- );
20280
+ return {
20281
+ siwe: result.signedMessage,
20282
+ jwk: prepared.jwk,
20283
+ spaceId: prepared.spaceId,
20284
+ verificationMethod: prepared.verificationMethod
20285
+ };
19990
20286
  }
19991
20287
  /**
19992
20288
  * Clear persisted session data.
@@ -20038,7 +20334,7 @@ var NodeUserAuthorization = class {
20038
20334
  * Request signature via event emitter with timeout.
20039
20335
  */
20040
20336
  requestSignatureViaEmitter(request, emitter, timeout) {
20041
- return new Promise((resolve, reject2) => {
20337
+ return new Promise((resolve2, reject2) => {
20042
20338
  const timeoutId = setTimeout(() => {
20043
20339
  reject2(new Error("Sign request timed out"));
20044
20340
  }, timeout);
@@ -20050,7 +20346,7 @@ var NodeUserAuthorization = class {
20050
20346
  );
20051
20347
  } else {
20052
20348
  const signature2 = response.signature ?? await this.signer.signMessage(request.message);
20053
- resolve(signature2);
20349
+ resolve2(signature2);
20054
20350
  }
20055
20351
  };
20056
20352
  emitter.emit("sign-request", request, respond);
@@ -20982,6 +21278,20 @@ function accountIndexWarningDiagnostic(error) {
20982
21278
  }
20983
21279
  return diagnostic;
20984
21280
  }
21281
+ function replicaRestoreRefusal(error) {
21282
+ if (error !== null && typeof error === "object" && "code" in error && error.code === "REPLICA_GATED" && "gate" in error) {
21283
+ switch (error.gate) {
21284
+ case "not_established":
21285
+ case "stamp_stale":
21286
+ case "principal_mismatch":
21287
+ case "purge_pending":
21288
+ case "logout_pending":
21289
+ case "gate_unavailable":
21290
+ return error.gate;
21291
+ }
21292
+ }
21293
+ return "gate_unavailable";
21294
+ }
20985
21295
  var _TinyCloudNode = class _TinyCloudNode {
20986
21296
  /**
20987
21297
  * Create a new TinyCloudNode instance.
@@ -21011,6 +21321,37 @@ var _TinyCloudNode = class _TinyCloudNode {
21011
21321
  this.auth = null;
21012
21322
  this.tc = null;
21013
21323
  this._chainId = 1;
21324
+ /** Mints the replica session stamp on each fresh sign-in (TC-858 §4.1). Undefined when replication is off. */
21325
+ this.establishReplicaStamp = async (session) => {
21326
+ const replication = this.config.replication;
21327
+ if (!replication?.enabled) return void 0;
21328
+ const epoch = this.sessionEpoch;
21329
+ const attempt = this.latestSessionAttempt;
21330
+ try {
21331
+ const { verifiedOwnerConsentAnchor: verifiedOwnerConsentAnchor2, establishReplicaSessionStamp: establishReplicaSessionStamp2 } = await getNodeReplicationLoaders().authority();
21332
+ if (epoch !== this.sessionEpoch || attempt !== this.latestSessionAttempt) return void 0;
21333
+ const anchor = verifiedOwnerConsentAnchor2(session, this.wasmBindings, exactVerifiedRecap);
21334
+ if (anchor === null) {
21335
+ throw Object.assign(new Error("Replica sign-in requires a verified finite owner SIWE and exact-space KV get+sync."), {
21336
+ code: "REPLICA_GATED",
21337
+ gate: "not_established"
21338
+ });
21339
+ }
21340
+ const stamp = await establishReplicaSessionStamp2(
21341
+ replication.storage,
21342
+ pkhDid2(session.address, session.chainId),
21343
+ anchor
21344
+ );
21345
+ if (epoch !== this.sessionEpoch || attempt !== this.latestSessionAttempt) return void 0;
21346
+ this.replicaSessionStamp = stamp;
21347
+ return stamp;
21348
+ } catch (error) {
21349
+ if (epoch === this.sessionEpoch && attempt === this.latestSessionAttempt) {
21350
+ this.replicaRestoreGate = replicaRestoreRefusal(error);
21351
+ }
21352
+ return void 0;
21353
+ }
21354
+ };
21014
21355
  this._baseVaults = /* @__PURE__ */ new Map();
21015
21356
  this._baseSecrets = /* @__PURE__ */ new Map();
21016
21357
  this._secrets = /* @__PURE__ */ new Map();
@@ -21135,7 +21476,7 @@ var _TinyCloudNode = class _TinyCloudNode {
21135
21476
  });
21136
21477
  return this.wasmBindings.invokeAny(invocationSession, caveatPreservingEntries, facts);
21137
21478
  };
21138
- assertValidReplicationConfig(config.replication);
21479
+ if (config.replication?.enabled) getNodeReplicationLoaders().validateConfig(config.replication);
21139
21480
  this.explicitHost = config.host;
21140
21481
  this.config = {
21141
21482
  ...config,
@@ -21150,6 +21491,16 @@ var _TinyCloudNode = class _TinyCloudNode {
21150
21491
  this.replicationRuntime = runtime;
21151
21492
  this.replicationControl = {
21152
21493
  status: async () => {
21494
+ const gate = this.replicaRestoreGate;
21495
+ if (gate) return this.config.replication.prefixes.map((prefix) => ({
21496
+ prefix,
21497
+ state: "gated",
21498
+ gate,
21499
+ errorCode: "REPLICA_GATED",
21500
+ pending: { inFlight: 0, committed: 0, ambiguous: 0 },
21501
+ pinned: [],
21502
+ lagMs: null
21503
+ }));
21153
21504
  const status = await runtime.then((ready) => ready.control.status());
21154
21505
  const scopeError = this.replicationScopeError;
21155
21506
  if (!scopeError) return status;
@@ -21166,7 +21517,15 @@ var _TinyCloudNode = class _TinyCloudNode {
21166
21517
  lagMs: null
21167
21518
  }));
21168
21519
  },
21169
- sync: async (input) => (await runtime).control.sync(input),
21520
+ sync: async (input) => {
21521
+ if (this.replicaRestoreGate) {
21522
+ throw Object.assign(new Error("Replica restore is gated."), {
21523
+ code: "REPLICA_GATED",
21524
+ gate: this.replicaRestoreGate
21525
+ });
21526
+ }
21527
+ return (await runtime).control.sync(input);
21528
+ },
21170
21529
  purge: (input) => {
21171
21530
  const instance = this.replicationRuntimeInstance;
21172
21531
  return instance ? instance.control.purge(input) : runtime.then((ready) => ready.control.purge(input));
@@ -21382,6 +21741,7 @@ var _TinyCloudNode = class _TinyCloudNode {
21382
21741
  signer: this.signer,
21383
21742
  signStrategy: config.signStrategy ?? { type: "auto-sign" },
21384
21743
  wasmBindings: this.wasmBindings,
21744
+ ...config.replication?.enabled === true ? { establishReplicaStamp: this.establishReplicaStamp } : {},
21385
21745
  sessionManager: this.sessionManager,
21386
21746
  sessionStorage: config.sessionStorage ?? new MemorySessionStorage(),
21387
21747
  domain: this.siweDomain,
@@ -21579,11 +21939,10 @@ var _TinyCloudNode = class _TinyCloudNode {
21579
21939
  return this.signer === null;
21580
21940
  }
21581
21941
  /**
21582
- * Get the space ID for this user.
21583
- * Available after signIn().
21942
+ * Primary space of the signed-in or restored network session, including compact sessions.
21584
21943
  */
21585
21944
  get spaceId() {
21586
- return this.auth?.tinyCloudSession?.spaceId;
21945
+ return this.auth?.tinyCloudSession?.spaceId ?? this._activeServiceSession?.spaceId;
21587
21946
  }
21588
21947
  /**
21589
21948
  * Get the account space ID for this wallet identity.
@@ -21631,6 +21990,12 @@ var _TinyCloudNode = class _TinyCloudNode {
21631
21990
  get restorableSession() {
21632
21991
  return this.currentTinyCloudSession();
21633
21992
  }
21993
+ /** The active, verified replica stamp. Undefined when the replica is refused or the session has ended. */
21994
+ get replicaStamp() {
21995
+ const stamp = this.replicaSessionStamp;
21996
+ if (!stamp || this.replicaRestoreGate !== void 0 || !this.currentTinyCloudSession()) return void 0;
21997
+ return { incarnation: stamp.incarnation, lifecycleEpoch: stamp.lifecycleEpoch };
21998
+ }
21634
21999
  /**
21635
22000
  * Sign out and end the session on this instance.
21636
22001
  *
@@ -21651,18 +22016,60 @@ var _TinyCloudNode = class _TinyCloudNode {
21651
22016
  }
21652
22017
  async endSession() {
21653
22018
  const leavingAddress = this.address ?? this._restoredTcSession?.address;
22019
+ const leavingChainId = this._chainId;
22020
+ const leavingPrincipal = this.replicaSessionPrincipal ?? (leavingAddress ? pkhDid2(leavingAddress, leavingChainId) : void 0);
21654
22021
  const leavingAuth = this.auth;
22022
+ const leavingContext = this._serviceContext;
22023
+ let denied;
21655
22024
  try {
21656
22025
  this.teardownSession();
21657
22026
  } finally {
22027
+ const denial = leavingPrincipal ? this.denyReplicaGate({ reason: "sign_out", principal: leavingPrincipal }, leavingContext) : Promise.resolve(void 0);
21658
22028
  try {
21659
- await this.accountRegistryTail;
21660
- if (this.replicationRuntime) await (await this.replicationRuntime).unbind();
22029
+ denied = await denial;
21661
22030
  } finally {
21662
- if (leavingAddress) {
21663
- await (leavingAuth ? leavingAuth.clearPersistedSession(leavingAddress) : this.config.sessionStorage?.clear(leavingAddress));
22031
+ try {
22032
+ await this.accountRegistryTail;
22033
+ await this.quiesceReplicaRuntime();
22034
+ } finally {
22035
+ if (leavingAddress) {
22036
+ await (leavingAuth ? leavingAuth.clearPersistedSession(leavingAddress) : this.config.sessionStorage?.clear(leavingAddress));
22037
+ }
22038
+ }
22039
+ }
22040
+ }
22041
+ if (denied?.status === "unguarded") {
22042
+ throw new Error(`Replica gate was not denied (${denied.code ?? "unknown"}); the replica may still be restorable.`);
22043
+ }
22044
+ }
22045
+ /**
22046
+ * Step 1 of replica sign-out. Never rejects: failures come back as `unguarded` so the session
22047
+ * is still cleared. Returns `undefined` when replication is off.
22048
+ */
22049
+ async denyReplicaGate(input, context = this._serviceContext) {
22050
+ const replication = this.config.replication;
22051
+ if (!replication?.enabled) return void 0;
22052
+ try {
22053
+ const lifecycle = replication.storage.lifecycle;
22054
+ if (lifecycle?.kind !== (replication.storage.kind === "indexeddb" ? "indexeddb" : "file")) {
22055
+ return { status: "unguarded", code: "gate_unavailable" };
22056
+ }
22057
+ const result = await lifecycle.deny(input);
22058
+ if (result.status === "denied" || result.status === "deleted" || result.status === "marker") {
22059
+ try {
22060
+ (await getNodeReplicationLoaders().authority()).reportReplicaDenial(
22061
+ result,
22062
+ input.reason,
22063
+ replication.onEvent,
22064
+ context
22065
+ );
22066
+ } catch {
21664
22067
  }
21665
22068
  }
22069
+ return result;
22070
+ } catch (error) {
22071
+ const code = typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : "gate_unavailable";
22072
+ return { status: "unguarded", code };
21666
22073
  }
21667
22074
  }
21668
22075
  /**
@@ -21682,15 +22089,9 @@ var _TinyCloudNode = class _TinyCloudNode {
21682
22089
  this._activeServiceSession = void 0;
21683
22090
  oldGraph.retire();
21684
22091
  oldCore?.retireServices();
21685
- oldAuth?.resetSession(
21686
- this.sessionManager,
21687
- this.sessionKeyId,
21688
- this.explicitHost ? [this.explicitHost] : void 0
21689
- );
21690
22092
  this.config.host = this.explicitHost ?? DEFAULT_HOST;
21691
22093
  this._chainId = 1;
21692
22094
  this._restoredNodeFeatures = void 0;
21693
- this._activeServiceSession = void 0;
21694
22095
  this._serviceContext = void 0;
21695
22096
  this._kv = void 0;
21696
22097
  this._sql = void 0;
@@ -21715,6 +22116,9 @@ var _TinyCloudNode = class _TinyCloudNode {
21715
22116
  this._bootstrapStatus = { skipped: false };
21716
22117
  this.replicationDelegateSession = void 0;
21717
22118
  this.replicationScopeError = void 0;
22119
+ this.replicaSessionPrincipal = void 0;
22120
+ this.replicaSessionStamp = void 0;
22121
+ this.replicaRestoreGate = void 0;
21718
22122
  const sessionKey = createDefaultSessionKey(this.wasmBindings);
21719
22123
  this.sessionManager = sessionKey.manager;
21720
22124
  this.sessionKeyId = sessionKey.keyId;
@@ -21724,6 +22128,9 @@ var _TinyCloudNode = class _TinyCloudNode {
21724
22128
  sessionKey.keyId,
21725
22129
  this.explicitHost ? [this.explicitHost] : void 0
21726
22130
  );
22131
+ this.replicaSessionPrincipal = void 0;
22132
+ this.replicaSessionStamp = void 0;
22133
+ this.replicaRestoreGate = void 0;
21727
22134
  this._serviceGraph = this.createServiceGraphLifetime();
21728
22135
  this.auth = oldAuth;
21729
22136
  this.tc = this.auth ? new TinyCloud(this.auth, {
@@ -21753,11 +22160,25 @@ var _TinyCloudNode = class _TinyCloudNode {
21753
22160
  "Cannot signIn() in session-only mode. Provide a privateKey in config to create your own space."
21754
22161
  );
21755
22162
  }
22163
+ await this.signInWith(() => this.tc.signIn(options));
22164
+ }
22165
+ /**
22166
+ * Fresh OpenKey sign-in. The callback receives the SDK's original SIWE and
22167
+ * session JWK before approval; the signed result is verified against those
22168
+ * bytes before the network session or replica stamp is committed.
22169
+ */
22170
+ async signInWithOpenKey(authorizeFn, options) {
22171
+ if (!this.signer || !this.tc || !this.auth) {
22172
+ throw new Error("Cannot signInWithOpenKey() in session-only mode. Provide a signer in config.");
22173
+ }
22174
+ await this.signInWith(() => this.auth.signInWithOpenKey(authorizeFn, options));
22175
+ }
22176
+ async signInWith(performSignIn) {
21756
22177
  const epoch = this.sessionEpoch;
21757
22178
  const attempt = ++this.latestSessionAttempt;
21758
22179
  await this.signOutInFlight;
21759
22180
  try {
21760
- await this.establishSignIn(epoch, options);
22181
+ await this.establishSignIn(epoch, performSignIn);
21761
22182
  } catch (error) {
21762
22183
  if (epoch !== this.sessionEpoch && attempt === this.latestSessionAttempt) {
21763
22184
  await this.endSession();
@@ -21765,7 +22186,7 @@ var _TinyCloudNode = class _TinyCloudNode {
21765
22186
  throw error;
21766
22187
  }
21767
22188
  }
21768
- async establishSignIn(epoch, options) {
22189
+ async establishSignIn(epoch, performSignIn) {
21769
22190
  if (!this.signer || !this.tc) {
21770
22191
  throw new Error("Not signed in. Call signIn() first.");
21771
22192
  }
@@ -21775,6 +22196,12 @@ var _TinyCloudNode = class _TinyCloudNode {
21775
22196
  const address = canonicalizeAddress2(await this.signer.getAddress());
21776
22197
  const chainId = await this.signer.getChainId();
21777
22198
  this.assertSessionEpoch(epoch, "Sign-in");
22199
+ const nextPrincipal = pkhDid2(address, chainId);
22200
+ const priorPrincipal = this.replicaSessionPrincipal ?? (this.address ? pkhDid2(this.address, this._chainId) : void 0);
22201
+ if (this.config.replication?.enabled && priorPrincipal && priorPrincipal !== nextPrincipal) {
22202
+ const denial = await this.denyReplicaGate({ reason: "account_switch", principal: priorPrincipal, next: nextPrincipal });
22203
+ this.replicaRestoreGate = denial?.status === "unguarded" ? "gate_unavailable" : "stamp_stale";
22204
+ }
21778
22205
  if (address !== this._address || chainId !== this._chainId) this.storageFull.clear();
21779
22206
  this._address = address;
21780
22207
  this._chainId = chainId;
@@ -21792,7 +22219,20 @@ var _TinyCloudNode = class _TinyCloudNode {
21792
22219
  this.runtimePermissionGrants = [];
21793
22220
  this.confirmedHostedSpaceIds.clear();
21794
22221
  this.hostedOwnedSpaceIds.clear();
21795
- await this.tc.signIn(options);
22222
+ const signInAttempt = this.latestSessionAttempt;
22223
+ const previousReplicaGate = this.replicaRestoreGate;
22224
+ const previousReplicaStamp = this.replicaSessionStamp;
22225
+ this.replicaRestoreGate = void 0;
22226
+ this.replicaSessionStamp = void 0;
22227
+ try {
22228
+ await performSignIn();
22229
+ } catch (error) {
22230
+ if (epoch === this.sessionEpoch && signInAttempt === this.latestSessionAttempt) {
22231
+ this.replicaRestoreGate = previousReplicaGate;
22232
+ this.replicaSessionStamp = previousReplicaStamp;
22233
+ }
22234
+ throw error;
22235
+ }
21796
22236
  this.assertSessionEpoch(epoch, "Sign-in");
21797
22237
  this.cancelOwnedSpaceHosts();
21798
22238
  this.syncResolvedHostFromAuth();
@@ -21800,8 +22240,9 @@ var _TinyCloudNode = class _TinyCloudNode {
21800
22240
  this.hostedOwnedSpaceIds.add(spaceId);
21801
22241
  }
21802
22242
  this.replicationDelegateSession = void 0;
22243
+ this.replicaSessionPrincipal = pkhDid2(address, chainId);
21803
22244
  this.replicationScopeError = void 0;
21804
- if (this.replicationRuntime) await (await this.replicationRuntime).unbind();
22245
+ await this.quiesceReplicaRuntime();
21805
22246
  this.assertSessionEpoch(epoch, "Sign-in");
21806
22247
  const oldGraph = this._serviceGraph;
21807
22248
  this._serviceGraph = this.createServiceGraphLifetime();
@@ -22053,7 +22494,7 @@ var _TinyCloudNode = class _TinyCloudNode {
22053
22494
  );
22054
22495
  }
22055
22496
  }
22056
- await new Promise((resolve) => setTimeout(resolve, 100));
22497
+ await new Promise((resolve2) => setTimeout(resolve2, 100));
22057
22498
  for (const step of steps) {
22058
22499
  if (step.kind !== "activate") continue;
22059
22500
  graph.assertActive();
@@ -22388,6 +22829,9 @@ var _TinyCloudNode = class _TinyCloudNode {
22388
22829
  if (this.ownStorageFull) {
22389
22830
  return;
22390
22831
  }
22832
+ if (!this.currentSessionCoversAccountRegistry()) {
22833
+ return;
22834
+ }
22391
22835
  const controller = new AbortController();
22392
22836
  const deadline = setTimeout(
22393
22837
  () => controller.abort(new DOMException("Account registry deadline exceeded", "TimeoutError")),
@@ -22502,6 +22946,27 @@ var _TinyCloudNode = class _TinyCloudNode {
22502
22946
  (operation) => operation.service === "space" && this.actionContains(operation.action, "tinycloud.space/list")
22503
22947
  );
22504
22948
  }
22949
+ /**
22950
+ * Whether the current primary session's recap covers the account index read
22951
+ * (`tinycloud.sql/read` on the owned account space's `account` database),
22952
+ * the first call of every registry sync. As in
22953
+ * {@link currentSessionCanListSpaces}, a session with no parseable recap
22954
+ * keeps today's behavior.
22955
+ */
22956
+ currentSessionCoversAccountRegistry() {
22957
+ const session = this.currentTinyCloudSession();
22958
+ const operations = session ? this.recapOperationsFromSession(session) : [];
22959
+ if (operations.length === 0) {
22960
+ return true;
22961
+ }
22962
+ const indexRead = {
22963
+ spaceId: this.ownedSpaceId(ACCOUNT_REGISTRY_SPACE2),
22964
+ service: "sql",
22965
+ path: "account",
22966
+ action: "tinycloud.sql/read"
22967
+ };
22968
+ return operations.some((granted) => this.operationCovers(granted, indexRead));
22969
+ }
22505
22970
  async withAccountRegistryRetry(task, signal) {
22506
22971
  const delays = [250, 1e3, 3e3];
22507
22972
  const stopped = (detail) => {
@@ -22542,10 +23007,10 @@ var _TinyCloudNode = class _TinyCloudNode {
22542
23007
  }
22543
23008
  lastError = error;
22544
23009
  if (attempt < delays.length - 1) {
22545
- await new Promise((resolve, reject2) => {
23010
+ await new Promise((resolve2, reject2) => {
22546
23011
  const timer = setTimeout(() => {
22547
23012
  signal.removeEventListener("abort", onAbort);
22548
- resolve();
23013
+ resolve2();
22549
23014
  }, delays[attempt]);
22550
23015
  const onAbort = () => {
22551
23016
  clearTimeout(timer);
@@ -22785,7 +23250,7 @@ var _TinyCloudNode = class _TinyCloudNode {
22785
23250
  }
22786
23251
  entry.waiters += 1;
22787
23252
  const shared = entry;
22788
- return new Promise((resolve, reject2) => {
23253
+ return new Promise((resolve2, reject2) => {
22789
23254
  const onAbort = () => {
22790
23255
  reject2(signal.reason);
22791
23256
  shared.waiters -= 1;
@@ -22794,7 +23259,7 @@ var _TinyCloudNode = class _TinyCloudNode {
22794
23259
  if (this.ownedSpaceHosts.get(spaceId) === shared) this.ownedSpaceHosts.delete(spaceId);
22795
23260
  };
22796
23261
  signal.addEventListener("abort", onAbort, { once: true });
22797
- shared.promise.then(resolve, reject2).finally(() => signal.removeEventListener("abort", onAbort));
23262
+ shared.promise.then(resolve2, reject2).finally(() => signal.removeEventListener("abort", onAbort));
22798
23263
  });
22799
23264
  }
22800
23265
  /** Session replacement: cancel the replaced session's in-flight hosts and forget them. */
@@ -22968,11 +23433,12 @@ var _TinyCloudNode = class _TinyCloudNode {
22968
23433
  );
22969
23434
  const stagedHost = resolvedHost ?? this.config.host;
22970
23435
  let restoredNodeFeatures;
23436
+ let replicaProtocolMismatch = false;
22971
23437
  if (this.config.replication?.enabled) {
22972
23438
  try {
22973
23439
  restoredNodeFeatures = (await checkNodeInfo2(stagedHost, this.wasmBindings.protocolVersion())).features;
22974
23440
  } catch (error) {
22975
- if (!(error instanceof VersionCheckError)) throw error;
23441
+ replicaProtocolMismatch = error instanceof ProtocolMismatchError;
22976
23442
  }
22977
23443
  }
22978
23444
  const stagedAddress = restoredAddress;
@@ -22980,28 +23446,55 @@ var _TinyCloudNode = class _TinyCloudNode {
22980
23446
  let replicationPrincipal;
22981
23447
  let compactReplicationSession;
22982
23448
  let replicationScopeError;
23449
+ let replicationScopeUnavailable = false;
22983
23450
  if (this.config.replication?.enabled) {
22984
23451
  if (stagedAddress !== void 0) {
22985
23452
  replicationPrincipal = pkhDid2(stagedAddress, stagedChainId);
22986
23453
  } else {
22987
- const { replicationScopeFromSignedSession: replicationScopeFromSignedSession2 } = await getNodeReplicationLoaders().authority();
22988
- const signedScope = replicationScopeFromSignedSession2({
22989
- delegationHeader: sessionData.delegationHeader,
22990
- delegationCid: sessionData.delegationCid,
22991
- verificationMethod: restoredVerificationMethod,
22992
- persistedSpace: sessionData.spaceId
22993
- });
22994
- if (!signedScope) {
22995
- replicationScopeError = "Persisted space does not match the locally verified delegation scope.";
22996
- } else {
22997
- replicationPrincipal = signedScope.principal;
22998
- compactReplicationSession = {
23454
+ try {
23455
+ const { replicationScopeFromSignedSession: replicationScopeFromSignedSession2 } = await getNodeReplicationLoaders().authority();
23456
+ const signedScope = replicationScopeFromSignedSession2({
22999
23457
  delegationHeader: sessionData.delegationHeader,
23000
23458
  delegationCid: sessionData.delegationCid,
23001
- spaceId: signedScope.space,
23002
23459
  verificationMethod: restoredVerificationMethod,
23003
- jwk: stagedJwk
23004
- };
23460
+ persistedSpace: sessionData.spaceId
23461
+ });
23462
+ if (!signedScope) {
23463
+ replicationScopeError = "Persisted space does not match the locally verified delegation scope.";
23464
+ } else {
23465
+ replicationPrincipal = signedScope.principal;
23466
+ compactReplicationSession = {
23467
+ delegationHeader: sessionData.delegationHeader,
23468
+ delegationCid: sessionData.delegationCid,
23469
+ spaceId: signedScope.space,
23470
+ verificationMethod: restoredVerificationMethod,
23471
+ jwk: stagedJwk
23472
+ };
23473
+ }
23474
+ } catch {
23475
+ replicationScopeUnavailable = true;
23476
+ replicationScopeError = "Replica scope could not be verified for this network session.";
23477
+ }
23478
+ }
23479
+ }
23480
+ let replicaRestoreGate;
23481
+ let restoredReplicaStamp;
23482
+ const replication = this.config.replication;
23483
+ if (replication?.enabled) {
23484
+ if (replicaProtocolMismatch) {
23485
+ replicaRestoreGate = "gate_unavailable";
23486
+ } else if (replicationPrincipal === void 0) {
23487
+ replicaRestoreGate = replicationScopeUnavailable ? "gate_unavailable" : "principal_mismatch";
23488
+ } else {
23489
+ try {
23490
+ const { restoreReplicaSessionStamp: restoreReplicaSessionStamp2 } = await getNodeReplicationLoaders().authority();
23491
+ restoredReplicaStamp = await restoreReplicaSessionStamp2(
23492
+ replication.storage,
23493
+ replicationPrincipal,
23494
+ sessionData.replicaStamp ?? void 0
23495
+ );
23496
+ } catch (error) {
23497
+ replicaRestoreGate = replicaRestoreRefusal(error);
23005
23498
  }
23006
23499
  }
23007
23500
  }
@@ -23094,7 +23587,10 @@ var _TinyCloudNode = class _TinyCloudNode {
23094
23587
  }
23095
23588
  this.replicationDelegateSession = compactReplicationSession;
23096
23589
  this.replicationScopeError = replicationScopeError;
23097
- if (this.replicationRuntime) await (await this.replicationRuntime).unbind();
23590
+ this.replicaRestoreGate = replicaRestoreGate;
23591
+ this.replicaSessionPrincipal = replicationPrincipal;
23592
+ this.replicaSessionStamp = restoredReplicaStamp;
23593
+ await this.quiesceReplicaRuntime();
23098
23594
  if (epoch !== this.sessionEpoch) {
23099
23595
  oldGraph.retire();
23100
23596
  oldCore?.retireServices?.();
@@ -23109,7 +23605,7 @@ var _TinyCloudNode = class _TinyCloudNode {
23109
23605
  detail: replicationScopeError
23110
23606
  });
23111
23607
  }
23112
- await this.bindReplicationRuntime(stagedGraph.serviceContext, serviceSession, stagedGraph.kv, replicationPrincipal);
23608
+ await this.bindReplicaOrGate(stagedGraph.serviceContext, serviceSession, stagedGraph.kv, replicationPrincipal);
23113
23609
  oldCore?.retireServices?.();
23114
23610
  if (epoch !== this.sessionEpoch) {
23115
23611
  if (attempt === this.latestSessionAttempt) await this.endSession();
@@ -23242,6 +23738,7 @@ var _TinyCloudNode = class _TinyCloudNode {
23242
23738
  session: input.serviceSession,
23243
23739
  invoke: graph.invoke,
23244
23740
  invokeAny: graph.invokeAny,
23741
+ signOwnerRevocation: this.signer ? (cid) => this.signOwnerRevocation(cid) : void 0,
23245
23742
  fetch: graph.fetch
23246
23743
  });
23247
23744
  const keyProvider = new WasmKeyProvider({ sessionManager: input.manager });
@@ -23585,6 +24082,7 @@ var _TinyCloudNode = class _TinyCloudNode {
23585
24082
  signer: this.signer,
23586
24083
  signStrategy: this.config.signStrategy ?? { type: "auto-sign" },
23587
24084
  wasmBindings: this.wasmBindings,
24085
+ ...this.config.replication?.enabled === true ? { establishReplicaStamp: this.establishReplicaStamp } : {},
23588
24086
  sessionManager: this.sessionManager,
23589
24087
  sessionStorage: options?.sessionStorage ?? this.config.sessionStorage ?? new MemorySessionStorage(),
23590
24088
  domain: this.siweDomain,
@@ -23643,6 +24141,7 @@ var _TinyCloudNode = class _TinyCloudNode {
23643
24141
  signer: this.signer,
23644
24142
  signStrategy: this.config.signStrategy ?? { type: "auto-sign" },
23645
24143
  wasmBindings: this.wasmBindings,
24144
+ ...this.config.replication?.enabled === true ? { establishReplicaStamp: this.establishReplicaStamp } : {},
23646
24145
  sessionManager: this.sessionManager,
23647
24146
  sessionStorage: options?.sessionStorage ?? this.config.sessionStorage ?? new MemorySessionStorage(),
23648
24147
  domain: this.siweDomain,
@@ -23723,7 +24222,7 @@ var _TinyCloudNode = class _TinyCloudNode {
23723
24222
  jwk: session.jwk
23724
24223
  };
23725
24224
  this._serviceContext.setSession(serviceSession);
23726
- await this.bindReplicationRuntime(this._serviceContext, serviceSession, this._kv);
24225
+ await this.bindReplicaOrGate(this._serviceContext, serviceSession, this._kv);
23727
24226
  this.tc.serviceContext.setSession(serviceSession);
23728
24227
  this._vault = this.createVaultService(session.spaceId, this._kv);
23729
24228
  this._vault.initialize(this._serviceContext);
@@ -24117,6 +24616,7 @@ var _TinyCloudNode = class _TinyCloudNode {
24117
24616
  session: serviceSession,
24118
24617
  invoke: graph.invoke,
24119
24618
  invokeAny: graph.invokeAny,
24619
+ signOwnerRevocation: this.signer ? (cid) => this.signOwnerRevocation(cid) : void 0,
24120
24620
  fetch: graph.fetch
24121
24621
  });
24122
24622
  this._spaceService = new SpaceService({
@@ -25409,7 +25909,8 @@ var _TinyCloudNode = class _TinyCloudNode {
25409
25909
  return this.delegationManager.create(params);
25410
25910
  }
25411
25911
  /**
25412
- * Revoke a delegation using the v2 DelegationManager.
25912
+ * Revoke a delegation with the owner wallet when available. Session-only
25913
+ * clients continue to use their delegated revocation authority.
25413
25914
  *
25414
25915
  * @param cid - The CID of the delegation to revoke
25415
25916
  * @returns Result indicating success or failure
@@ -25684,34 +26185,67 @@ var _TinyCloudNode = class _TinyCloudNode {
25684
26185
  return auth.replicationSignInEntries();
25685
26186
  }
25686
26187
  async replicationAuthority() {
25687
- const { createReplicationAuthority: createReplicationAuthority2 } = await getNodeReplicationLoaders().authority();
26188
+ const { createReplicationAuthority: createReplicationAuthority2, mintNodeReplicationGrant: mintNodeReplicationGrant2, verifiedOwnerConsentAnchor: verifiedOwnerConsentAnchor2 } = await getNodeReplicationLoaders().authority();
25688
26189
  return createReplicationAuthority2({
25689
26190
  replicationSession: () => this.currentTinyCloudSession() ?? this.replicationDelegateSession,
26191
+ verifiedConsentAnchor: (session) => {
26192
+ const current = this.currentTinyCloudSession();
26193
+ return session === current && current !== void 0 ? verifiedOwnerConsentAnchor2(current, this.wasmBindings, exactVerifiedRecap) : null;
26194
+ },
25690
26195
  siweExpiration: (siwe) => extractSiweExpiration(siwe),
25691
26196
  planDelegation: (entries, options) => this.planDelegation(entries, options),
25692
- mintDelegation: async (deviceDid, entries) => {
25693
- const result = await this.delegateTo(principalDid(deviceDid), entries);
25694
- return {
25695
- ucan: result.delegation.delegationHeader.Authorization,
25696
- expiresAt: result.delegation.expiry.getTime()
25697
- };
25698
- }
26197
+ mintDelegation: (deviceDid, entries, options) => mintNodeReplicationGrant2(
26198
+ deviceDid,
26199
+ entries,
26200
+ options,
26201
+ options?.renewalOnly ? this.currentTinyCloudSession() : void 0,
26202
+ this
26203
+ )
25699
26204
  });
25700
26205
  }
25701
- async bindReplicationRuntime(context, session, kv, principal) {
26206
+ async quiesceReplicaRuntime() {
25702
26207
  if (!this.replicationRuntime) return;
25703
- const identityPrincipal = principal ?? (this._address === void 0 ? void 0 : pkhDid2(this._address, this._chainId));
25704
- if (identityPrincipal === void 0) return;
25705
- const runtime = await this.replicationRuntime;
25706
- const authority = await this.replicationAuthority();
25707
- await runtime.bind({
25708
- context,
25709
- session,
25710
- principal: identityPrincipal,
25711
- authority,
25712
- nodeFeatures: this.replicationNodeFeatures,
25713
- primaryKV: [{ space: session.spaceId, kv }]
25714
- });
26208
+ try {
26209
+ await (await this.replicationRuntime).unbind();
26210
+ } catch {
26211
+ this.replicaRestoreGate = "gate_unavailable";
26212
+ }
26213
+ }
26214
+ async bindReplicaOrGate(context, session, kv, principal) {
26215
+ if (!this.replicationRuntime) return;
26216
+ const gate = this.replicaRestoreGate;
26217
+ const epoch = this.sessionEpoch;
26218
+ const attempt = this.latestSessionAttempt;
26219
+ try {
26220
+ const runtime = await this.replicationRuntime;
26221
+ if (epoch !== this.sessionEpoch || attempt !== this.latestSessionAttempt || this._kv !== kv || this._serviceContext !== context || gate !== this.replicaRestoreGate) return;
26222
+ if (gate) {
26223
+ await runtime.bindGatedNetworkOnly({
26224
+ context,
26225
+ session,
26226
+ gate,
26227
+ primaryKV: [{ space: session.spaceId, kv }]
26228
+ }, getNodeReplicationLoaders().gatedNetwork);
26229
+ return;
26230
+ }
26231
+ const identityPrincipal = principal ?? (this._address === void 0 ? void 0 : pkhDid2(this._address, this._chainId));
26232
+ if (identityPrincipal === void 0) return;
26233
+ const authority = await this.replicationAuthority();
26234
+ if (epoch !== this.sessionEpoch || attempt !== this.latestSessionAttempt || this._kv !== kv || this._serviceContext !== context) return;
26235
+ await runtime.bind({
26236
+ context,
26237
+ session,
26238
+ principal: identityPrincipal,
26239
+ stamp: this.replicaSessionStamp,
26240
+ authority,
26241
+ nodeFeatures: this.replicationNodeFeatures,
26242
+ primaryKV: [{ space: session.spaceId, kv }]
26243
+ });
26244
+ } catch {
26245
+ if (epoch !== this.sessionEpoch || attempt !== this.latestSessionAttempt || this._kv !== kv || this._serviceContext !== context) return;
26246
+ this.replicaRestoreGate = "gate_unavailable";
26247
+ await this.quiesceReplicaRuntime();
26248
+ }
25715
26249
  }
25716
26250
  /** Stable control facade for this node's optional replication runtime. */
25717
26251
  get replication() {
@@ -26860,9 +27394,16 @@ TinyCloudNode.registerNodeDefaults({
26860
27394
  });
26861
27395
 
26862
27396
  // src/replication/node-loader.ts
27397
+ init_config();
26863
27398
  registerReplicationLoaders({
27399
+ validateConfig: assertValidReplicationConfig,
26864
27400
  runtime: () => Promise.resolve().then(() => (init_runtime(), runtime_exports)),
26865
- authority: () => Promise.resolve().then(() => (init_authority(), authority_exports))
27401
+ authority: () => Promise.resolve().then(() => (init_authority(), authority_exports)),
27402
+ gatedNetwork: () => Promise.resolve().then(() => (init_gated_network(), gated_network_exports)),
27403
+ // Lazy by contract: replica loads only when replication is enabled. Literal specifier typed by
27404
+ // ProfileGateModule; TS2307 under moduleResolution "node" is suppressed (see replica-contract.ts).
27405
+ // @ts-ignore
27406
+ gate: () => import("@tinycloud/replica/profile-gate")
26866
27407
  });
26867
27408
 
26868
27409
  // src/index.ts
@@ -27178,8 +27719,8 @@ import {
27178
27719
  Space
27179
27720
  } from "@tinycloud/sdk-core";
27180
27721
  import {
27181
- ProtocolMismatchError,
27182
- VersionCheckError as VersionCheckError2,
27722
+ ProtocolMismatchError as ProtocolMismatchError2,
27723
+ VersionCheckError,
27183
27724
  UnsupportedFeatureError as UnsupportedFeatureError2,
27184
27725
  checkNodeInfo as checkNodeInfo3
27185
27726
  } from "@tinycloud/sdk-core";
@@ -27187,15 +27728,518 @@ import { ServiceContext as ServiceContext4 } from "@tinycloud/sdk-core";
27187
27728
 
27188
27729
  // src/replication/sqlite.ts
27189
27730
  init_authority();
27731
+ import { AsyncLocalStorage } from "async_hooks";
27190
27732
  import { createHash as createHash2, randomBytes as randomBytes2 } from "crypto";
27191
- import { chmod, mkdir, open as openFile, readFile, realpath, rename, rm, stat } from "fs/promises";
27192
- import { basename, dirname as dirname2, join as join2 } from "path";
27733
+ import { lstatSync, readFileSync as readFileSync2, statSync } from "fs";
27734
+ import { chmod, lstat, mkdir, open as openFile, readFile, readdir, realpath, rename, rm, rmdir, stat } from "fs/promises";
27735
+ import { basename, dirname as dirname2, join as join2, parse as parse2, resolve, sep } from "path";
27193
27736
  import { KVService as KVService3, ServiceContext as ServiceContext3, principalDid as principalDid2 } from "@tinycloud/sdk-core";
27194
27737
  import {
27195
27738
  canonicalReplicationIdentity,
27196
27739
  replicationIdentityKey,
27197
27740
  requiresSecretsOptIn as requiresSecretsOptIn2
27198
27741
  } from "@tinycloud/sdk-services";
27742
+ import { CONSENT_WINDOW_MS, MAX_GRANT_TTL_MS as MAX_GRANT_TTL_MS2, REPLICA_GATE_FILE } from "@tinycloud/sdk-services/kv/replication/contractConstants";
27743
+ var REPLICA_LOGOUT_PENDING_FILE = "replica-logout-pending.json";
27744
+ function assertProfileLogoutClear(profileDir) {
27745
+ assertLogoutClear(join2(profileDir, REPLICA_LOGOUT_PENDING_FILE));
27746
+ }
27747
+ function assertLogoutClear(path) {
27748
+ if (path === void 0) return;
27749
+ try {
27750
+ lstatSync(path);
27751
+ } catch (error) {
27752
+ if (isNotFound(error)) return;
27753
+ }
27754
+ throw gated("gate_unavailable");
27755
+ }
27756
+ function safeInventoryPath(path) {
27757
+ return path.length > 0 && path.split("/").every((part) => part.length > 0 && part !== "." && part !== ".." && !part.includes("\\") && !part.includes("\0")) && (path === "replication" || path.startsWith("replication/") || path === "replicas" || path.startsWith("replicas/"));
27758
+ }
27759
+ function logoutPrincipalDigest(principal) {
27760
+ return createHash2("sha256").update("tinycloud-replica-logout\0").update(principal, "utf8").digest("hex");
27761
+ }
27762
+ function compareInventoryString(left, right) {
27763
+ return left < right ? -1 : left > right ? 1 : 0;
27764
+ }
27765
+ function approvalEntries(report) {
27766
+ return [
27767
+ ...report.unknownEntries,
27768
+ ...report.knownEntries,
27769
+ ...report.knownRoots.map((root) => ({ ...root, kind: "directory" })),
27770
+ ...report.missingDatabases.map((path) => ({ path, kind: "missing", dev: "", ino: "" }))
27771
+ ].map(({ path, kind, dev, ino }) => ({ path, kind, dev, ino })).sort((a, b) => compareInventoryString(a.path, b.path) || compareInventoryString(a.kind, b.kind) || compareInventoryString(a.dev, b.dev) || compareInventoryString(a.ino, b.ino));
27772
+ }
27773
+ function inventoryApprovalToken(marker, principal, entries) {
27774
+ const markerId = marker.v === 2 ? [marker.v, marker.nonce, marker.principalDigest] : [marker.v, marker.incarnation, marker.lifecycleEpoch, marker.principalDigest];
27775
+ return createHash2("sha256").update("tc-logout-approve-v1\0").update(JSON.stringify([
27776
+ markerId,
27777
+ marker.approvalSalt,
27778
+ principal,
27779
+ marker.v === 2 ? null : marker.lifecycleEpoch,
27780
+ entries
27781
+ ])).digest().subarray(0, 16).toString("hex");
27782
+ }
27783
+ function changedInventoryRefusal(marker, principal, report, entries = approvalEntries(report)) {
27784
+ const token = inventoryApprovalToken(marker, principal, entries);
27785
+ const error = localChangesRefusal(report.unknownPaths);
27786
+ error.message += ` Current token-bound inventory (path/kind/dev/ino): ${entries.length === 0 ? "(empty)" : entries.map((entry) => JSON.stringify(entry)).join(", ")}. To approve this exact changed inventory in a separate command, use --discard-local-changes --approve-inventory ${token}.`;
27787
+ return Object.assign(error, { inventoryApprovalToken: token, inventoryApprovalEntries: entries });
27788
+ }
27789
+ function storedInventoryChanged(marker, report) {
27790
+ try {
27791
+ assertApprovedInventory(report, {
27792
+ unknownEntries: marker.approvedUnknown,
27793
+ knownRoots: marker.knownRoots,
27794
+ knownEntries: marker.knownEntries,
27795
+ missingDatabases: marker.missingDatabases
27796
+ });
27797
+ return false;
27798
+ } catch (error) {
27799
+ if (typeof error === "object" && error !== null && "code" in error && error.code === "REPLICA_GATED" && "gate" in error && error.gate === "gate_unavailable") {
27800
+ return true;
27801
+ }
27802
+ throw error;
27803
+ }
27804
+ }
27805
+ function decimalIdentity(value) {
27806
+ return typeof value === "string" && /^(0|[1-9][0-9]*)$/.test(value);
27807
+ }
27808
+ function logoutMarkerOf(value) {
27809
+ if (value === null || typeof value !== "object" || Array.isArray(value)) throw gated("gate_unavailable");
27810
+ const row = value;
27811
+ const entry = (item) => item !== null && typeof item === "object" && !Array.isArray(item) && Object.keys(item).length === 4 && typeof item.path === "string" && safeInventoryPath(item.path) && ["file", "directory", "symlink"].includes(item.kind) && decimalIdentity(item.dev) && decimalIdentity(item.ino);
27812
+ const root = (item) => item !== null && typeof item === "object" && !Array.isArray(item) && Object.keys(item).length === 3 && typeof item.path === "string" && safeInventoryPath(item.path) && decimalIdentity(item.dev) && decimalIdentity(item.ino);
27813
+ if (typeof row.principalDigest !== "string" || !/^[0-9a-f]{64}$/.test(row.principalDigest) || typeof row.approvalSalt !== "string" || !/^[0-9a-f]{32}$/.test(row.approvalSalt) || !Array.isArray(row.approvedUnknown) || !row.approvedUnknown.every(entry) || !Array.isArray(row.knownRoots) || !row.knownRoots.every(root) || !Array.isArray(row.knownEntries) || !row.knownEntries.every(entry) || !Array.isArray(row.missingDatabases) || !row.missingDatabases.every(
27814
+ (path) => typeof path === "string" && safeInventoryPath(path) && path.endsWith("/replica.db")
27815
+ ) || row.discardLocalChanges !== void 0 && row.discardLocalChanges !== true || row.remotePending !== void 0 && row.remotePending !== true || row.remoteAbandoned !== void 0 && row.remoteAbandoned !== true || row.remotePending === true && row.remoteAbandoned === true) throw gated("gate_unavailable");
27816
+ const optional = Number(row.discardLocalChanges === true) + Number(row.remotePending === true) + Number(row.remoteAbandoned === true);
27817
+ if (row.v === 2 && Object.keys(row).length === 9 + optional && row.scope === "orphan_profile" && typeof row.nonce === "string" && /^[0-9a-f]{32}$/.test(row.nonce)) return row;
27818
+ if (row.v === 3 && Object.keys(row).length === 10 + optional && row.scope === "inactive_gate" && typeof row.incarnation === "string" && row.incarnation.length > 0 && Number.isSafeInteger(row.lifecycleEpoch) && row.lifecycleEpoch >= 0) {
27819
+ return row;
27820
+ }
27821
+ const bound = Number(row.deniedEpoch !== void 0);
27822
+ if (Object.keys(row).length !== 10 + optional + bound || row.v !== 1 || typeof row.incarnation !== "string" || row.incarnation.length === 0 || !Number.isSafeInteger(row.lifecycleEpoch) || row.lifecycleEpoch < 0 || typeof row.principalHash !== "string" || !/^[0-9a-f]{64}$/.test(row.principalHash) || bound === 1 && (!Number.isSafeInteger(row.deniedEpoch) || row.deniedEpoch <= row.lifecycleEpoch)) {
27823
+ throw gated("gate_unavailable");
27824
+ }
27825
+ return row;
27826
+ }
27827
+ async function readLogoutMarker(path) {
27828
+ const value = await readJsonFile(path);
27829
+ return value === void 0 ? null : logoutMarkerOf(value);
27830
+ }
27831
+ async function orphanRoot(profileDir, name) {
27832
+ let profile;
27833
+ try {
27834
+ profile = await lstat(profileDir);
27835
+ } catch {
27836
+ throw gated("gate_unavailable");
27837
+ }
27838
+ let pinned = false;
27839
+ if (process.platform === "linux" && /^\/proc\/self\/fd\/[1-9]\d*$/.test(profileDir) && profile.isSymbolicLink()) {
27840
+ try {
27841
+ pinned = (await stat(profileDir)).isDirectory();
27842
+ } catch {
27843
+ throw gated("gate_unavailable");
27844
+ }
27845
+ }
27846
+ if (!profile.isDirectory() && !pinned) throw gated("gate_unavailable");
27847
+ const root = join2(profileDir, name);
27848
+ let entry;
27849
+ try {
27850
+ entry = await lstat(root);
27851
+ } catch (error) {
27852
+ if (isNotFound(error)) return null;
27853
+ throw error;
27854
+ }
27855
+ if (!entry.isDirectory() || await realpath(root) !== join2(await realpath(profileDir), name)) {
27856
+ throw gated("gate_unavailable");
27857
+ }
27858
+ return root;
27859
+ }
27860
+ async function hasOrphanPending(profileDir) {
27861
+ const root = await orphanRoot(profileDir, "replication");
27862
+ if (root === null) return false;
27863
+ let entries;
27864
+ try {
27865
+ entries = await readdir(root, { withFileTypes: true });
27866
+ } catch {
27867
+ return true;
27868
+ }
27869
+ for (const entry of entries) {
27870
+ if (!entry.isDirectory()) return true;
27871
+ const dir = join2(root, entry.name);
27872
+ try {
27873
+ if (!(await lstat(dir)).isDirectory()) return true;
27874
+ } catch {
27875
+ return true;
27876
+ }
27877
+ if (!/^[a-z2-7]{26}$/.test(entry.name)) return true;
27878
+ let children;
27879
+ try {
27880
+ children = await readdir(dir, { withFileTypes: true });
27881
+ } catch {
27882
+ return true;
27883
+ }
27884
+ if (children.length !== 2 || children.some((child) => !child.isFile() || child.name !== "identity.json" && child.name !== "pending.json")) return true;
27885
+ const pendingFile = join2(dir, "pending.json");
27886
+ let text;
27887
+ try {
27888
+ const pending = await lstat(pendingFile);
27889
+ if (!pending.isFile()) return true;
27890
+ text = await readFile(pendingFile, "utf8");
27891
+ } catch (error) {
27892
+ if (isNotFound(error)) return true;
27893
+ return true;
27894
+ }
27895
+ try {
27896
+ const identityFile = join2(dir, "identity.json");
27897
+ if (!(await lstat(identityFile)).isFile()) return true;
27898
+ const identity = identityRecordOf(await readJsonFile(identityFile));
27899
+ if (identity === void 0 || parsePendingState(text, pendingFile, identity).records.length !== 0) {
27900
+ return true;
27901
+ }
27902
+ } catch {
27903
+ return true;
27904
+ }
27905
+ }
27906
+ return false;
27907
+ }
27908
+ function inventoryIdentity(path, dev, ino) {
27909
+ return { path, dev: dev.toString(), ino: ino.toString() };
27910
+ }
27911
+ async function inventoryUnknown(profileDir, relative, entries) {
27912
+ const path = join2(profileDir, relative);
27913
+ const info = await lstat(path, { bigint: true });
27914
+ const kind = info.isDirectory() ? "directory" : info.isSymbolicLink() ? "symlink" : "file";
27915
+ entries.push({ ...inventoryIdentity(relative, info.dev, info.ino), kind });
27916
+ if (kind === "directory") {
27917
+ for (const child of await readdir(path)) await inventoryUnknown(profileDir, `${relative}/${child}`, entries);
27918
+ }
27919
+ }
27920
+ async function inspectProfileReplicaLayout(profileDir, gateReadable, approvedMarker, legacyOnly = false, diagnosticFiles = []) {
27921
+ const report = {
27922
+ knownLegacyReplicas: 0,
27923
+ unknownLegacyPaths: [],
27924
+ unknownPaths: [],
27925
+ unknownEntries: [],
27926
+ knownRoots: [],
27927
+ knownEntries: [],
27928
+ missingDatabases: []
27929
+ };
27930
+ const legacy = await orphanRoot(profileDir, "replicas");
27931
+ const active = legacyOnly ? null : await orphanRoot(profileDir, "replication");
27932
+ for (const [name, dir] of [["replicas", legacy], ["replication", active]]) {
27933
+ if (dir === null) continue;
27934
+ const info = await lstat(dir, { bigint: true });
27935
+ report.knownRoots.push(inventoryIdentity(name, info.dev, info.ino));
27936
+ }
27937
+ if (legacy !== null) {
27938
+ const sqlite = await import("@tinycloud/replica/sqlite");
27939
+ for (const entry of await readdir(legacy, { withFileTypes: true })) {
27940
+ const relative = `replicas/${entry.name}`;
27941
+ const child = join2(legacy, entry.name);
27942
+ if (!entry.isDirectory()) {
27943
+ await inventoryUnknown(profileDir, relative, report.unknownEntries);
27944
+ continue;
27945
+ }
27946
+ try {
27947
+ const layout = await sqlite.inspectSqliteReplicaLayout(child);
27948
+ report.knownRoots.push({ path: relative, ...layout.rootIdentity });
27949
+ report.unknownEntries.push(...layout.unknownEntries.map((item) => ({
27950
+ ...item,
27951
+ path: `${relative}/${item.path}`
27952
+ })));
27953
+ report.knownEntries.push(...layout.knownEntries.map((item) => ({
27954
+ ...item,
27955
+ path: `${relative}/${item.path}`
27956
+ })));
27957
+ if (layout.unknownPaths.length === 0) report.knownLegacyReplicas++;
27958
+ if (layout.unknownPaths.includes("replica.db") && !layout.unknownEntries.some((item) => item.path === "replica.db")) {
27959
+ report.missingDatabases.push(`${relative}/replica.db`);
27960
+ }
27961
+ } catch {
27962
+ await inventoryUnknown(profileDir, relative, report.unknownEntries);
27963
+ }
27964
+ }
27965
+ }
27966
+ if (active !== null) {
27967
+ const cleanOrphan = !gateReadable && (approvedMarker?.v === 2 || !await hasOrphanPending(profileDir));
27968
+ for (const entry of await readdir(active, { withFileTypes: true })) {
27969
+ const relative = `replication/${entry.name}`;
27970
+ const dir = join2(active, entry.name);
27971
+ if (approvedMarker?.approvedUnknown.some((item) => item.path === relative)) {
27972
+ await inventoryUnknown(profileDir, relative, report.unknownEntries);
27973
+ continue;
27974
+ }
27975
+ if (gateReadable && diagnosticFiles.includes(entry.name)) {
27976
+ const info2 = await lstat(dir, { bigint: true });
27977
+ if (info2.isFile()) {
27978
+ report.knownEntries.push({ ...inventoryIdentity(relative, info2.dev, info2.ino), kind: "file" });
27979
+ continue;
27980
+ }
27981
+ }
27982
+ if (!gateReadable && !cleanOrphan || !entry.isDirectory() || !/^[a-z2-7]{26}$/.test(entry.name)) {
27983
+ await inventoryUnknown(profileDir, relative, report.unknownEntries);
27984
+ continue;
27985
+ }
27986
+ const info = await lstat(dir, { bigint: true });
27987
+ report.knownRoots.push(inventoryIdentity(relative, info.dev, info.ino));
27988
+ const identityPath = join2(dir, "identity.json");
27989
+ let identity;
27990
+ try {
27991
+ if ((await lstat(identityPath)).isFile()) identity = identityRecordOf(await readJsonFile(identityPath));
27992
+ } catch {
27993
+ }
27994
+ if (identity === void 0) {
27995
+ const approvedRoot = approvedMarker?.knownRoots.find((item) => item.path === relative);
27996
+ if (approvedRoot?.dev === info.dev.toString() && approvedRoot.ino === info.ino.toString()) {
27997
+ const children = await readdir(dir);
27998
+ if (children.length === 0) continue;
27999
+ if (children.length === 1 && children[0] === "replicas") {
28000
+ const replicasPath = `${relative}/replicas`;
28001
+ const replicasDir = join2(dir, "replicas");
28002
+ const replicasInfo = await lstat(replicasDir, { bigint: true });
28003
+ const approvedReplicas = approvedMarker.knownRoots.find((item) => item.path === replicasPath);
28004
+ if (replicasInfo.isDirectory() && !replicasInfo.isSymbolicLink() && approvedReplicas?.dev === replicasInfo.dev.toString() && approvedReplicas.ino === replicasInfo.ino.toString() && (await readdir(replicasDir)).length === 0) {
28005
+ report.knownRoots.push(inventoryIdentity(replicasPath, replicasInfo.dev, replicasInfo.ino));
28006
+ continue;
28007
+ }
28008
+ }
28009
+ }
28010
+ report.knownRoots.pop();
28011
+ await inventoryUnknown(profileDir, relative, report.unknownEntries);
28012
+ continue;
28013
+ }
28014
+ for (const child of await readdir(dir, { withFileTypes: true })) {
28015
+ const childRelative = `${relative}/${child.name}`;
28016
+ const childPath = join2(dir, child.name);
28017
+ if ((child.name === "identity.json" || child.name === "device.jwk") && child.isFile()) {
28018
+ const childInfo = await lstat(childPath, { bigint: true });
28019
+ report.knownEntries.push({ ...inventoryIdentity(childRelative, childInfo.dev, childInfo.ino), kind: "file" });
28020
+ continue;
28021
+ }
28022
+ if (child.name === "pending.json" && child.isFile()) {
28023
+ try {
28024
+ if (parsePendingState(await readFile(childPath, "utf8"), childPath, identity).records.length === 0) {
28025
+ const childInfo = await lstat(childPath, { bigint: true });
28026
+ report.knownEntries.push({ ...inventoryIdentity(childRelative, childInfo.dev, childInfo.ino), kind: "file" });
28027
+ continue;
28028
+ }
28029
+ } catch {
28030
+ }
28031
+ }
28032
+ if (child.name === "replicas" && child.isDirectory()) {
28033
+ const replicasInfo = await lstat(childPath, { bigint: true });
28034
+ report.knownRoots.push(inventoryIdentity(childRelative, replicasInfo.dev, replicasInfo.ino));
28035
+ for (const replicaChild of await readdir(childPath, { withFileTypes: true })) {
28036
+ const replicaRelative = `${childRelative}/${replicaChild.name}`;
28037
+ if (!replicaChild.isDirectory() || !/^[a-z2-7]{26}$/.test(replicaChild.name)) {
28038
+ await inventoryUnknown(profileDir, replicaRelative, report.unknownEntries);
28039
+ continue;
28040
+ }
28041
+ const sqlite = await import("@tinycloud/replica/sqlite");
28042
+ try {
28043
+ const layout = await sqlite.inspectSqliteReplicaLayout(join2(childPath, replicaChild.name), {
28044
+ allowAdapterFence: true
28045
+ });
28046
+ report.knownRoots.push({ path: replicaRelative, ...layout.rootIdentity });
28047
+ report.unknownEntries.push(...layout.unknownEntries.map((item) => ({
28048
+ ...item,
28049
+ path: `${replicaRelative}/${item.path}`
28050
+ })));
28051
+ report.knownEntries.push(...layout.knownEntries.map((item) => ({
28052
+ ...item,
28053
+ path: `${replicaRelative}/${item.path}`
28054
+ })));
28055
+ if (layout.unknownPaths.includes("replica.db") && !layout.unknownEntries.some((item) => item.path === "replica.db")) {
28056
+ report.missingDatabases.push(`${replicaRelative}/replica.db`);
28057
+ }
28058
+ } catch {
28059
+ await inventoryUnknown(profileDir, replicaRelative, report.unknownEntries);
28060
+ }
28061
+ }
28062
+ continue;
28063
+ }
28064
+ await inventoryUnknown(profileDir, childRelative, report.unknownEntries);
28065
+ }
28066
+ }
28067
+ }
28068
+ report.unknownEntries.sort((a, b) => a.path.localeCompare(b.path));
28069
+ report.knownEntries.sort((a, b) => a.path.localeCompare(b.path));
28070
+ report.knownRoots.sort((a, b) => a.path.localeCompare(b.path));
28071
+ report.missingDatabases.sort();
28072
+ report.unknownPaths = [...report.unknownEntries.map((entry) => entry.path), ...report.missingDatabases].sort();
28073
+ report.unknownLegacyPaths = [...report.unknownPaths];
28074
+ return report;
28075
+ }
28076
+ function assertApprovedInventory(current, approval, allowRemoved = false) {
28077
+ const approved = new Map(approval.unknownEntries.map((item) => [item.path, item]));
28078
+ const known = new Map(approval.knownRoots.map((item) => [item.path, item]));
28079
+ const knownFiles = new Map(approval.knownEntries.map((item) => [item.path, item]));
28080
+ for (const entry of current.unknownEntries) {
28081
+ const expected = approved.get(entry.path);
28082
+ if (expected?.kind !== entry.kind || expected.dev !== entry.dev || expected.ino !== entry.ino) {
28083
+ throw localChangesRefusal(current.unknownPaths);
28084
+ }
28085
+ }
28086
+ for (const root of current.knownRoots) {
28087
+ const expected = known.get(root.path);
28088
+ if (expected?.dev !== root.dev || expected.ino !== root.ino) throw localChangesRefusal([root.path]);
28089
+ }
28090
+ for (const entry of current.knownEntries) {
28091
+ if (allowRemoved && entry.kind === "file" && /^(?:replication\/[a-z2-7]{26}\/replicas\/[a-z2-7]{26}|replicas\/[^/]+)\/replica\.db-(?:wal|shm|journal)$/.test(entry.path)) {
28092
+ const database = entry.path.replace(/-(?:wal|shm|journal)$/, "");
28093
+ const original = knownFiles.get(database);
28094
+ const present = current.knownEntries.find((item) => item.path === database);
28095
+ if (!approved.has(entry.path) && original?.kind === "file" && present?.dev === original.dev && present.ino === original.ino) continue;
28096
+ }
28097
+ const expected = knownFiles.get(entry.path);
28098
+ if (expected?.kind !== entry.kind || expected.dev !== entry.dev || expected.ino !== entry.ino) {
28099
+ throw localChangesRefusal([entry.path]);
28100
+ }
28101
+ }
28102
+ for (const path of current.missingDatabases) {
28103
+ if (!approval.missingDatabases.includes(path) && !(allowRemoved && knownFiles.get(path)?.kind === "file")) throw localChangesRefusal([path]);
28104
+ }
28105
+ if (!allowRemoved && (current.unknownEntries.length !== approved.size || current.knownRoots.length !== known.size || current.knownEntries.length !== knownFiles.size || current.missingDatabases.length !== approval.missingDatabases.length)) {
28106
+ throw localChangesRefusal(current.unknownPaths);
28107
+ }
28108
+ }
28109
+ async function assertApprovedParents(profileDir, marker, path) {
28110
+ if (process.platform !== "linux") {
28111
+ const absolute = resolve(profileDir);
28112
+ let ancestor = parse2(absolute).root;
28113
+ for (const part of absolute.slice(ancestor.length).split(sep).filter(Boolean)) {
28114
+ ancestor = join2(ancestor, part);
28115
+ const details = await lstat(ancestor, { bigint: true });
28116
+ if (!details.isDirectory() || details.isSymbolicLink()) throw localChangesRefusal([path]);
28117
+ }
28118
+ }
28119
+ let relativeParent = "";
28120
+ for (const part of path.split("/").slice(0, -1)) {
28121
+ relativeParent = relativeParent ? `${relativeParent}/${part}` : part;
28122
+ let details;
28123
+ try {
28124
+ details = await lstat(join2(profileDir, relativeParent), { bigint: true });
28125
+ } catch (error) {
28126
+ if (isNotFound(error)) return false;
28127
+ throw error;
28128
+ }
28129
+ const expected = marker.knownRoots.find((root) => root.path === relativeParent) ?? marker.approvedUnknown.find((item) => item.path === relativeParent && item.kind === "directory");
28130
+ if (!details.isDirectory() || details.isSymbolicLink() || expected?.dev !== details.dev.toString() || expected.ino !== details.ino.toString()) {
28131
+ throw localChangesRefusal([relativeParent]);
28132
+ }
28133
+ }
28134
+ return true;
28135
+ }
28136
+ async function eraseApprovedUnknown(profileDir, marker, prefix, diagnosticFiles) {
28137
+ const entries = marker.approvedUnknown.filter((entry) => entry.path.startsWith(`${prefix}/`));
28138
+ for (const entry of entries.sort((a, b) => b.path.length - a.path.length)) {
28139
+ assertApprovedInventory(await inspectProfileReplicaLayout(profileDir, marker.v !== 2, marker, false, diagnosticFiles), {
28140
+ unknownEntries: marker.approvedUnknown,
28141
+ knownRoots: marker.knownRoots,
28142
+ knownEntries: marker.knownEntries,
28143
+ missingDatabases: marker.missingDatabases
28144
+ }, true);
28145
+ if (!await assertApprovedParents(profileDir, marker, entry.path)) continue;
28146
+ const file = join2(profileDir, entry.path);
28147
+ let current;
28148
+ try {
28149
+ current = await lstat(file, { bigint: true });
28150
+ } catch (error) {
28151
+ if (isNotFound(error)) continue;
28152
+ throw error;
28153
+ }
28154
+ const kind = current.isDirectory() ? "directory" : current.isSymbolicLink() ? "symlink" : "file";
28155
+ if (kind !== entry.kind || current.dev.toString() !== entry.dev || current.ino.toString() !== entry.ino) throw localChangesRefusal([entry.path]);
28156
+ if (process.platform !== "linux" && kind === "symlink") throw localChangesRefusal([entry.path]);
28157
+ if (kind === "directory") await rmdir(file);
28158
+ else await rm(file);
28159
+ }
28160
+ }
28161
+ async function eraseKnownFlagResidue(profileDir, marker, diagnosticFiles) {
28162
+ const diagnostics = new Set(diagnosticFiles.map((name) => `replication/${name}`));
28163
+ for (const entry of marker.knownEntries.filter((item) => diagnostics.has(item.path) || /^replication\/[a-z2-7]{26}\/(?:identity\.json|pending\.json|device\.jwk)$/.test(item.path)).sort((a, b) => Number(a.path.endsWith("/identity.json")) - Number(b.path.endsWith("/identity.json")))) {
28164
+ if (!await assertApprovedParents(profileDir, marker, entry.path)) continue;
28165
+ const file = join2(profileDir, entry.path);
28166
+ let details;
28167
+ try {
28168
+ details = await lstat(file, { bigint: true });
28169
+ } catch (error) {
28170
+ if (isNotFound(error)) continue;
28171
+ throw error;
28172
+ }
28173
+ if (!details.isFile() || details.dev.toString() !== entry.dev || details.ino.toString() !== entry.ino) throw localChangesRefusal([entry.path]);
28174
+ await rm(file);
28175
+ }
28176
+ const roots = marker.knownRoots.filter((item) => item.path === "replication" || item.path.startsWith("replication/"));
28177
+ roots.sort((a, b) => b.path.split("/").length - a.path.split("/").length);
28178
+ for (const entry of roots) {
28179
+ if (!await assertApprovedParents(profileDir, marker, entry.path)) continue;
28180
+ const path = join2(profileDir, entry.path);
28181
+ let details;
28182
+ try {
28183
+ details = await lstat(path, { bigint: true });
28184
+ } catch (error) {
28185
+ if (isNotFound(error)) continue;
28186
+ throw error;
28187
+ }
28188
+ if (!details.isDirectory() || details.isSymbolicLink() || details.dev.toString() !== entry.dev || details.ino.toString() !== entry.ino) {
28189
+ throw localChangesRefusal([entry.path]);
28190
+ }
28191
+ await rmdir(path);
28192
+ }
28193
+ }
28194
+ function localChangesRefusal(unknownPaths = []) {
28195
+ const error = gated("gate_unavailable");
28196
+ error.message = "Unknown or unsynced local replica data may be lost; sync first, or retry logout with --discard-local-changes." + (unknownPaths.length === 0 ? "" : ` Unknown: ${unknownPaths.map((path) => JSON.stringify(path)).join(", ")}.`);
28197
+ return error;
28198
+ }
28199
+ function gateOwnedByAnother(state, principalHash) {
28200
+ return state.principalHash !== null ? state.principalHash !== principalHash : state.eraseDeferred && state.deferredPrincipalHash !== principalHash;
28201
+ }
28202
+ function isGateUnavailable(error) {
28203
+ return typeof error === "object" && error !== null && "code" in error && error.code === "REPLICA_GATED" && "gate" in error && error.gate === "gate_unavailable";
28204
+ }
28205
+ function restoredGate() {
28206
+ const error = gated("stamp_stale");
28207
+ error.message = RESTORED_GATE_MESSAGE;
28208
+ return Object.assign(error, { reportCode: "gate_restored" });
28209
+ }
28210
+ var RESTORED_GATE_MESSAGE = "The replica gate does not match this logout's recorded sign-out; an older gate may have been restored. Local replicas stay disabled for this profile: switch the default profile first if needed (`tc profile switch <other>`), then run `tc profile delete <name>` and sign in again.";
28211
+ function interruptedSignOut() {
28212
+ const error = gated("gate_unavailable");
28213
+ error.message = "A previous sign-out was interrupted; re-running `tc auth logout` completes it.";
28214
+ return error;
28215
+ }
28216
+ function assertSupportedProfileLayout(report) {
28217
+ if (process.platform === "linux") return;
28218
+ const link = report.unknownEntries.find((entry) => entry.kind === "symlink");
28219
+ if (link === void 0) return;
28220
+ const error = gated("gate_unavailable");
28221
+ error.message = `Replica symlink ${JSON.stringify(link.path)} cannot be removed safely on this platform.`;
28222
+ throw error;
28223
+ }
28224
+ async function fileGate() {
28225
+ return import("@tinycloud/replica/profile-gate");
28226
+ }
28227
+ function gated(reason) {
28228
+ return Object.assign(new Error(`Replica gate refused: ${reason}`), { code: "REPLICA_GATED", gate: reason });
28229
+ }
28230
+ async function requiredGuard(guard, section) {
28231
+ if (guard === void 0) throw gated("gate_unavailable");
28232
+ let entered = false;
28233
+ try {
28234
+ return await guard(async () => {
28235
+ entered = true;
28236
+ return section();
28237
+ });
28238
+ } catch (error) {
28239
+ if (!entered) throw gated("gate_unavailable");
28240
+ throw error;
28241
+ }
28242
+ }
27199
28243
  var ReplicaStorageError = class extends Error {
27200
28244
  constructor(code, message, options) {
27201
28245
  super(message);
@@ -27221,6 +28265,19 @@ async function pathExists(path) {
27221
28265
  throw storageError("STORAGE_ERROR", `Reading ${path}`, error);
27222
28266
  }
27223
28267
  }
28268
+ async function realDirectoryExists(path) {
28269
+ let stats;
28270
+ try {
28271
+ stats = await lstat(path);
28272
+ } catch (error) {
28273
+ if (isNotFound(error)) return false;
28274
+ throw storageError("STORAGE_ERROR", `Reading ${path}`, error);
28275
+ }
28276
+ if (stats.isSymbolicLink() || !stats.isDirectory()) {
28277
+ throw new ReplicaStorageError("STORAGE_ERROR", `Refusing the replaced replica path ${path}.`);
28278
+ }
28279
+ return true;
28280
+ }
27224
28281
  function principalOf(did) {
27225
28282
  return principalDid2(did);
27226
28283
  }
@@ -27374,90 +28431,117 @@ function parsePendingState(text, file, identity) {
27374
28431
  }
27375
28432
  return state;
27376
28433
  }
27377
- var _dir, _guard, _dirSync, _file, _FilePendingWriteStore_instances, canonicalFile_fn;
28434
+ var _dir, _profileDir, _guard, _stamp, _dirSync, _logoutPendingFile, _generation, _canonicalDir, _FilePendingWriteStore_instances, withGate_fn, readFile_fn;
27378
28435
  var FilePendingWriteStore = class {
27379
- constructor(identity, dir, guard, dirSync) {
28436
+ constructor(identity, dir, profileDir, guard, stamp, dirSync, logoutPendingFile) {
27380
28437
  __privateAdd(this, _FilePendingWriteStore_instances);
27381
28438
  this.durable = true;
27382
28439
  __privateAdd(this, _dir);
28440
+ __privateAdd(this, _profileDir);
27383
28441
  __privateAdd(this, _guard);
28442
+ __privateAdd(this, _stamp);
27384
28443
  __privateAdd(this, _dirSync);
27385
- __privateAdd(this, _file);
28444
+ __privateAdd(this, _logoutPendingFile);
28445
+ __privateAdd(this, _generation);
28446
+ __privateAdd(this, _canonicalDir);
27386
28447
  this.identity = identity;
27387
28448
  __privateSet(this, _dir, dir);
28449
+ __privateSet(this, _profileDir, profileDir);
27388
28450
  __privateSet(this, _guard, guard);
28451
+ __privateSet(this, _stamp, stamp);
27389
28452
  __privateSet(this, _dirSync, dirSync);
28453
+ __privateSet(this, _logoutPendingFile, logoutPendingFile);
27390
28454
  }
27391
28455
  async read() {
27392
- const file = await __privateMethod(this, _FilePendingWriteStore_instances, canonicalFile_fn).call(this);
27393
- let text;
27394
- try {
27395
- text = await readFile(file, "utf8");
27396
- } catch (error) {
27397
- if (isNotFound(error)) {
27398
- return { v: 2, identity: this.identity, committedEpoch: 0, seq: 0, records: [] };
27399
- }
27400
- throw storageError("STORAGE_ERROR", `Reading ${file}`, error);
27401
- }
27402
- return parsePendingState(text, file, this.identity);
28456
+ return __privateMethod(this, _FilePendingWriteStore_instances, withGate_fn).call(this, async (file) => await __privateMethod(this, _FilePendingWriteStore_instances, readFile_fn).call(this, file) ?? { v: 2, identity: this.identity, committedEpoch: 0, seq: 0, records: [] });
27403
28457
  }
27404
28458
  async readExisting() {
27405
- let file;
28459
+ return __privateMethod(this, _FilePendingWriteStore_instances, withGate_fn).call(this, (file) => __privateMethod(this, _FilePendingWriteStore_instances, readFile_fn).call(this, file));
28460
+ }
28461
+ async update(mutate) {
28462
+ return __privateMethod(this, _FilePendingWriteStore_instances, withGate_fn).call(this, async (file) => {
28463
+ await ensurePartitionIdentity(__privateGet(this, _dir), this.identity, __privateGet(this, _dirSync));
28464
+ const currentFile = join2(await realpath(__privateGet(this, _dir)), "pending.json");
28465
+ if (__privateGet(this, _canonicalDir) !== void 0 && dirname2(currentFile) !== __privateGet(this, _canonicalDir)) {
28466
+ throw new ReplicaStorageError("STORAGE_ERROR", "The pending-write partition changed while open.");
28467
+ }
28468
+ const state = await __privateMethod(this, _FilePendingWriteStore_instances, readFile_fn).call(this, currentFile) ?? { v: 2, identity: this.identity, committedEpoch: 0, seq: 0, records: [] };
28469
+ const result = await mutate(state);
28470
+ await writeJsonAtomic(currentFile, state, __privateGet(this, _dirSync));
28471
+ return result;
28472
+ });
28473
+ }
28474
+ };
28475
+ _dir = new WeakMap();
28476
+ _profileDir = new WeakMap();
28477
+ _guard = new WeakMap();
28478
+ _stamp = new WeakMap();
28479
+ _dirSync = new WeakMap();
28480
+ _logoutPendingFile = new WeakMap();
28481
+ _generation = new WeakMap();
28482
+ _canonicalDir = new WeakMap();
28483
+ _FilePendingWriteStore_instances = new WeakSet();
28484
+ withGate_fn = async function(section) {
28485
+ if (__privateGet(this, _stamp) === void 0) throw gated("not_established");
28486
+ if (__privateGet(this, _guard) === void 0) throw gated("gate_unavailable");
28487
+ const gate = await fileGate();
28488
+ const snapshot = await gate.readGateSnapshot(__privateGet(this, _profileDir));
28489
+ if (snapshot === void 0) throw gated("gate_unavailable");
28490
+ const [principalHash, identityKeyHash] = await Promise.all([
28491
+ gate.principalHashOf(snapshot.salt, this.identity.principal),
28492
+ gate.identityHashOf(snapshot.salt, replicationIdentityKey(this.identity))
28493
+ ]);
28494
+ const access = {
28495
+ stamp: __privateGet(this, _stamp),
28496
+ incarnation: snapshot.incarnation,
28497
+ principalHash,
28498
+ identityKeyHash
28499
+ };
28500
+ let canonicalRoot;
28501
+ try {
28502
+ canonicalRoot = await realpath(dirname2(__privateGet(this, _dir)));
28503
+ } catch (error) {
28504
+ if (!isNotFound(error)) throw storageError("STORAGE_ERROR", `Resolving ${dirname2(__privateGet(this, _dir))}`, error);
28505
+ canonicalRoot = join2(await realpath(dirname2(dirname2(__privateGet(this, _dir)))), basename(dirname2(__privateGet(this, _dir))));
28506
+ }
28507
+ const queueKey = join2(canonicalRoot, basename(__privateGet(this, _dir)), "pending.json");
28508
+ return serializePendingWrite(queueKey, () => requiredGuard(__privateGet(this, _guard), async () => {
28509
+ assertLogoutClear(__privateGet(this, _logoutPendingFile));
28510
+ const before = await gate.assertGateAccess(__privateGet(this, _profileDir), access, snapshot.salt);
28511
+ const generation = before.pendingGenerations[identityKeyHash] ?? 0;
28512
+ if (__privateGet(this, _generation) !== void 0 && __privateGet(this, _generation) !== generation) {
28513
+ throw new ReplicaStorageError("RESET_REQUIRED", "The pending identity was erased; reopen the store.");
28514
+ }
28515
+ let file = join2(__privateGet(this, _dir), "pending.json");
27406
28516
  try {
27407
28517
  file = join2(await realpath(__privateGet(this, _dir)), "pending.json");
28518
+ const recorded = identityRecordOf(await readJsonFile(join2(dirname2(file), "identity.json")));
28519
+ if (recorded === void 0 || !sameIdentity(recorded, this.identity)) {
28520
+ throw new ReplicaStorageError("STORAGE_ERROR", "The pending partition belongs to a different identity.");
28521
+ }
27408
28522
  } catch (error) {
27409
- if (isNotFound(error)) return void 0;
27410
- throw storageError("STORAGE_ERROR", `Resolving pending-write state in ${__privateGet(this, _dir)}`, error);
28523
+ if (error instanceof ReplicaStorageError) throw error;
28524
+ if (!isNotFound(error)) throw storageError("STORAGE_ERROR", `Resolving ${__privateGet(this, _dir)}`, error);
27411
28525
  }
27412
- try {
27413
- const text = await readFile(file, "utf8");
27414
- return parsePendingState(text, file, this.identity);
27415
- } catch (error) {
27416
- if (isNotFound(error)) return void 0;
27417
- if (error instanceof ReplicaStorageError) throw error;
27418
- throw storageError("STORAGE_ERROR", `Reading ${file}`, error);
28526
+ if (__privateGet(this, _canonicalDir) !== void 0 && dirname2(file) !== __privateGet(this, _canonicalDir)) {
28527
+ throw new ReplicaStorageError("STORAGE_ERROR", "The pending-write partition changed while open.");
27419
28528
  }
27420
- }
27421
- async update(mutate) {
27422
- const file = await __privateMethod(this, _FilePendingWriteStore_instances, canonicalFile_fn).call(this);
27423
- return serializePendingWrite(file, async () => {
27424
- let currentFile;
27425
- try {
27426
- currentFile = join2(await realpath(dirname2(file)), basename(file));
27427
- } catch (error) {
27428
- throw storageError("STORAGE_ERROR", "Resolving the open pending-write partition", error);
27429
- }
27430
- if (currentFile !== file) {
27431
- throw new ReplicaStorageError(
27432
- "STORAGE_ERROR",
27433
- `The pending-write partition changed while open: ${dirname2(file)}.`
27434
- );
27435
- }
27436
- return __privateGet(this, _guard).call(this, async () => {
27437
- const state = await this.read();
27438
- const result = mutate(state);
27439
- await writeJsonAtomic(file, state, __privateGet(this, _dirSync));
27440
- return result;
27441
- });
27442
- });
27443
- }
28529
+ const result = await section(file);
28530
+ await gate.assertGateAccess(__privateGet(this, _profileDir), access, snapshot.salt);
28531
+ assertLogoutClear(__privateGet(this, _logoutPendingFile));
28532
+ __privateSet(this, _generation, generation);
28533
+ if (await pathExists(__privateGet(this, _dir))) __privateSet(this, _canonicalDir, await realpath(__privateGet(this, _dir)));
28534
+ return result;
28535
+ }));
27444
28536
  };
27445
- _dir = new WeakMap();
27446
- _guard = new WeakMap();
27447
- _dirSync = new WeakMap();
27448
- _file = new WeakMap();
27449
- _FilePendingWriteStore_instances = new WeakSet();
27450
- canonicalFile_fn = async function() {
27451
- __privateGet(this, _file) ?? __privateSet(this, _file, (async () => {
27452
- await ensurePartitionIdentity(__privateGet(this, _dir), this.identity, __privateGet(this, _dirSync));
27453
- const file = join2(__privateGet(this, _dir), "pending.json");
27454
- try {
27455
- return join2(await realpath(dirname2(file)), basename(file));
27456
- } catch (error) {
27457
- throw storageError("STORAGE_ERROR", `Resolving ${file}`, error);
27458
- }
27459
- })());
27460
- return __privateGet(this, _file);
28537
+ readFile_fn = async function(file) {
28538
+ try {
28539
+ return parsePendingState(await readFile(file, "utf8"), file, this.identity);
28540
+ } catch (error) {
28541
+ if (isNotFound(error)) return void 0;
28542
+ if (error instanceof ReplicaStorageError) throw error;
28543
+ throw storageError("STORAGE_ERROR", `Reading ${file}`, error);
28544
+ }
27461
28545
  };
27462
28546
  async function loadOrCreateDevice(idDir, guard, createDevice, dirSync) {
27463
28547
  return guard(async () => {
@@ -27503,7 +28587,7 @@ async function readFence(replicaDir) {
27503
28587
  const fence = await readJsonFile(join2(replicaDir, FENCE_FILE));
27504
28588
  return typeof fence === "object" && fence !== null && "syncedThroughEpoch" in fence && typeof fence.syncedThroughEpoch === "number" ? fence.syncedThroughEpoch : 0;
27505
28589
  }
27506
- function newestGrantInfo(replica, record, pending, spec) {
28590
+ function newestGrantInfo(replica, record, pending, spec, install) {
27507
28591
  if (record === null) return null;
27508
28592
  const parsed = replica.parseUcanGrant(record.bytes);
27509
28593
  return {
@@ -27511,10 +28595,11 @@ function newestGrantInfo(replica, record, pending, spec) {
27511
28595
  parentCid: parsed.prf[0] ?? null,
27512
28596
  expiresAt: record.expiresAt === null ? null : record.expiresAt * 1e3,
27513
28597
  state: pending !== null && record === pending ? "pending" : "active",
27514
- unconstrained: ucanAttUnconstrainedFor(parsed.att, spec.space, spec.prefix)
28598
+ unconstrained: ucanAttUnconstrainedFor(parsed.att, spec.space, spec.prefix),
28599
+ ...install === null ? {} : install
27515
28600
  };
27516
28601
  }
27517
- var _device, _store, _replicaDir, _replica, _guard2, _dirSync2, _generation, _ino, _transportOverride, _closed, _inFlight, _transportFactory, _SqliteReplicaHandle_instances, assertOpen_fn, assertLive_fn, transports_fn;
28602
+ var _device, _store, _replicaDir, _replica, _guard2, _read, _policy, _recordGrantInstall, _dirSync2, _generation2, _ino, _transportOverride, _closed, _inFlight, _transportFactory, _SqliteReplicaHandle_instances, assertOpen_fn, assertLive_fn, transports_fn, assertEffective_fn, assertSyncEffective_fn;
27518
28603
  var SqliteReplicaHandle = class {
27519
28604
  constructor(input) {
27520
28605
  __privateAdd(this, _SqliteReplicaHandle_instances);
@@ -27523,9 +28608,12 @@ var SqliteReplicaHandle = class {
27523
28608
  __privateAdd(this, _replicaDir);
27524
28609
  __privateAdd(this, _replica);
27525
28610
  __privateAdd(this, _guard2);
28611
+ __privateAdd(this, _read);
28612
+ __privateAdd(this, _policy);
28613
+ __privateAdd(this, _recordGrantInstall);
27526
28614
  __privateAdd(this, _dirSync2);
27527
28615
  /** The replica row generation this handle opened under — the durable fence a purge commits past (browser-parity wipe). */
27528
- __privateAdd(this, _generation);
28616
+ __privateAdd(this, _generation2);
27529
28617
  /** The inode of `replica.db` at open — a directory recreated in place is a different replica. */
27530
28618
  __privateAdd(this, _ino);
27531
28619
  __privateAdd(this, _transportOverride);
@@ -27533,64 +28621,76 @@ var SqliteReplicaHandle = class {
27533
28621
  __privateAdd(this, _inFlight, /* @__PURE__ */ new Set());
27534
28622
  __privateAdd(this, _transportFactory);
27535
28623
  this.spec = input.spec;
28624
+ this.replicaId = input.replicaId;
27536
28625
  __privateSet(this, _device, input.device);
27537
28626
  this.deviceDid = input.device.did;
27538
28627
  __privateSet(this, _store, input.store);
27539
28628
  __privateSet(this, _replicaDir, input.replicaDir);
27540
28629
  __privateSet(this, _replica, input.replica);
27541
28630
  __privateSet(this, _guard2, input.guard);
28631
+ __privateSet(this, _read, input.read);
28632
+ __privateSet(this, _policy, input.policy);
28633
+ __privateSet(this, _recordGrantInstall, input.recordGrantInstall);
27542
28634
  __privateSet(this, _transportOverride, input.transportFor);
27543
28635
  __privateSet(this, _dirSync2, input.dirSync);
27544
- __privateSet(this, _generation, input.generation);
28636
+ __privateSet(this, _generation2, input.generation);
27545
28637
  __privateSet(this, _ino, input.ino);
27546
28638
  }
27547
28639
  async get(key2) {
27548
- await __privateMethod(this, _SqliteReplicaHandle_instances, assertLive_fn).call(this);
27549
- const result = await new (__privateGet(this, _replica)).Replica({ store: __privateGet(this, _store) }).get(key2);
27550
- const meta = {
27551
- asOf: result.meta.asOf,
27552
- coverage: result.meta.coverage,
27553
- authority: result.meta.authority,
27554
- syncedThroughEpoch: await readFence(__privateGet(this, _replicaDir))
27555
- };
27556
- switch (result.status) {
27557
- case "present":
27558
- return {
27559
- status: "present",
27560
- key: result.key,
27561
- value: result.value,
27562
- etag: result.etag,
27563
- metadata: result.metadata,
27564
- meta
27565
- };
27566
- case "content_missing":
27567
- return {
27568
- status: "content_missing",
27569
- key: result.key,
27570
- etag: result.etag,
27571
- metadata: result.metadata,
27572
- meta
27573
- };
27574
- default:
27575
- return { status: result.status, key: result.key, meta };
27576
- }
27577
- }
27578
- async list(options) {
27579
- await __privateMethod(this, _SqliteReplicaHandle_instances, assertLive_fn).call(this);
27580
- const result = await new (__privateGet(this, _replica)).Replica({ store: __privateGet(this, _store) }).list({
27581
- prefix: options.prefix,
27582
- ...options.after === void 0 ? {} : { after: options.after },
27583
- ...options.limit === void 0 ? {} : { limit: options.limit }
27584
- });
27585
- return {
27586
- keys: result.entries.map((entry) => entry.key),
27587
- meta: {
28640
+ return __privateGet(this, _read).call(this, async (snapshot, syncedThroughEpoch) => {
28641
+ __privateMethod(this, _SqliteReplicaHandle_instances, assertOpen_fn).call(this);
28642
+ await __privateMethod(this, _SqliteReplicaHandle_instances, assertEffective_fn).call(this, snapshot);
28643
+ const result = await new (__privateGet(this, _replica)).Replica({ store: snapshot }).get(key2);
28644
+ await __privateMethod(this, _SqliteReplicaHandle_instances, assertEffective_fn).call(this, snapshot);
28645
+ const meta = {
27588
28646
  asOf: result.meta.asOf,
27589
28647
  coverage: result.meta.coverage,
27590
28648
  authority: result.meta.authority,
27591
- syncedThroughEpoch: await readFence(__privateGet(this, _replicaDir))
28649
+ syncedThroughEpoch
28650
+ };
28651
+ switch (result.status) {
28652
+ case "present":
28653
+ return {
28654
+ status: "present",
28655
+ key: result.key,
28656
+ value: result.value,
28657
+ etag: result.etag,
28658
+ metadata: result.metadata,
28659
+ meta
28660
+ };
28661
+ case "content_missing":
28662
+ return {
28663
+ status: "content_missing",
28664
+ key: result.key,
28665
+ etag: result.etag,
28666
+ metadata: result.metadata,
28667
+ meta
28668
+ };
28669
+ default:
28670
+ return { status: result.status, key: result.key, meta };
27592
28671
  }
27593
- };
28672
+ }, true);
28673
+ }
28674
+ async list(options) {
28675
+ return __privateGet(this, _read).call(this, async (snapshot, syncedThroughEpoch) => {
28676
+ __privateMethod(this, _SqliteReplicaHandle_instances, assertOpen_fn).call(this);
28677
+ await __privateMethod(this, _SqliteReplicaHandle_instances, assertEffective_fn).call(this, snapshot);
28678
+ const result = await new (__privateGet(this, _replica)).Replica({ store: snapshot }).list({
28679
+ prefix: options.prefix,
28680
+ ...options.after === void 0 ? {} : { after: options.after },
28681
+ ...options.limit === void 0 ? {} : { limit: options.limit }
28682
+ });
28683
+ await __privateMethod(this, _SqliteReplicaHandle_instances, assertEffective_fn).call(this, snapshot);
28684
+ return {
28685
+ keys: result.entries.map((entry) => entry.key),
28686
+ meta: {
28687
+ asOf: result.meta.asOf,
28688
+ coverage: result.meta.coverage,
28689
+ authority: result.meta.authority,
28690
+ syncedThroughEpoch
28691
+ }
28692
+ };
28693
+ }, true);
27594
28694
  }
27595
28695
  /**
27596
28696
  * The newest installed grant — the pending one when a not-yet-promoted
@@ -27600,43 +28700,59 @@ var SqliteReplicaHandle = class {
27600
28700
  * grant is still reported (unconstrained: false) and never reused.
27601
28701
  */
27602
28702
  async grant() {
27603
- await __privateMethod(this, _SqliteReplicaHandle_instances, assertLive_fn).call(this);
27604
- const state = await __privateGet(this, _store).open();
27605
- if (state === null) return null;
27606
- return newestGrantInfo(
27607
- __privateGet(this, _replica),
27608
- state.pendingGrant ?? state.grant,
27609
- state.pendingGrant,
27610
- this.spec
27611
- );
28703
+ return __privateGet(this, _read).call(this, async (snapshot) => {
28704
+ __privateMethod(this, _SqliteReplicaHandle_instances, assertOpen_fn).call(this);
28705
+ const state = await snapshot.open();
28706
+ if (state === null) return null;
28707
+ const selected = state.pendingGrant ?? state.grant;
28708
+ const install = selected === null ? null : await snapshot.getGrantInstall(selected.cid);
28709
+ return newestGrantInfo(
28710
+ __privateGet(this, _replica),
28711
+ state.pendingGrant ?? state.grant,
28712
+ state.pendingGrant,
28713
+ this.spec,
28714
+ install
28715
+ );
28716
+ });
27612
28717
  }
27613
28718
  async installGrant(ucan) {
27614
- await __privateMethod(this, _SqliteReplicaHandle_instances, assertLive_fn).call(this);
27615
- const parsed = __privateGet(this, _replica).parseUcanGrant(ucan);
27616
- const state = await __privateGet(this, _store).open();
27617
- if (state === null) {
27618
- throw new (__privateGet(this, _replica)).ReplicaError(
27619
- __privateGet(this, _replica).ReplicaErrorCode.NOT_FOUND,
27620
- "The replica has not been created."
27621
- );
27622
- }
27623
- __privateGet(this, _replica).assertGrantInstallable(parsed, {
27624
- deviceDid: principalOf(__privateGet(this, _device).did),
27625
- space: this.spec.space,
27626
- prefix: this.spec.prefix,
27627
- now: Date.now()
28719
+ return __privateGet(this, _guard2).call(this, async () => {
28720
+ await __privateMethod(this, _SqliteReplicaHandle_instances, assertLive_fn).call(this);
28721
+ const parsed = __privateGet(this, _replica).parseUcanGrant(ucan);
28722
+ const state = await __privateGet(this, _store).open();
28723
+ if (state === null) {
28724
+ throw new (__privateGet(this, _replica)).ReplicaError(
28725
+ __privateGet(this, _replica).ReplicaErrorCode.NOT_FOUND,
28726
+ "The replica has not been created."
28727
+ );
28728
+ }
28729
+ __privateGet(this, _replica).assertGrantInstallable(parsed, {
28730
+ deviceDid: principalOf(__privateGet(this, _device).did),
28731
+ space: this.spec.space,
28732
+ prefix: this.spec.prefix,
28733
+ now: Date.now()
28734
+ });
28735
+ if (!ucanAttUnconstrainedFor(parsed.att, this.spec.space, this.spec.prefix)) {
28736
+ throw new (__privateGet(this, _replica)).ReplicaError(
28737
+ __privateGet(this, _replica).ReplicaErrorCode.GRANT_NOT_COVERING,
28738
+ "The grant's get or sync coverage carries caveats; re-mint instead of installing it."
28739
+ );
28740
+ }
28741
+ const now = Date.now();
28742
+ const policy = await __privateGet(this, _policy).call(this, now, true);
28743
+ if (policy === null || now >= policy.consentDeadline) {
28744
+ throw new ReplicaStorageError("CONSENT_EXPIRED", "The replica's consent window has expired.");
28745
+ }
28746
+ const installedAt = await __privateGet(this, _recordGrantInstall).call(this, parsed.cid, now);
28747
+ const installed = await __privateGet(this, _store).installGatedGrant(parsed, { ...policy, at: installedAt });
28748
+ return newestGrantInfo(__privateGet(this, _replica), parsed, parsed, this.spec, installed);
27628
28749
  });
27629
- if (!ucanAttUnconstrainedFor(parsed.att, this.spec.space, this.spec.prefix)) {
27630
- throw new (__privateGet(this, _replica)).ReplicaError(
27631
- __privateGet(this, _replica).ReplicaErrorCode.GRANT_NOT_COVERING,
27632
- "The grant's get or sync coverage carries caveats; re-mint instead of installing it."
27633
- );
27634
- }
27635
- await __privateGet(this, _store).installGrant(parsed);
27636
- return newestGrantInfo(__privateGet(this, _replica), parsed, parsed, this.spec);
27637
28750
  }
27638
28751
  async sync(options) {
27639
- await __privateMethod(this, _SqliteReplicaHandle_instances, assertLive_fn).call(this);
28752
+ await __privateGet(this, _guard2).call(this, async () => {
28753
+ await __privateMethod(this, _SqliteReplicaHandle_instances, assertLive_fn).call(this);
28754
+ await __privateMethod(this, _SqliteReplicaHandle_instances, assertSyncEffective_fn).call(this);
28755
+ });
27640
28756
  const transportFor = await __privateMethod(this, _SqliteReplicaHandle_instances, transports_fn).call(this);
27641
28757
  const abort = new AbortController();
27642
28758
  options.signal.throwIfAborted();
@@ -27677,27 +28793,33 @@ var SqliteReplicaHandle = class {
27677
28793
  }
27678
28794
  }
27679
28795
  async status() {
27680
- await __privateMethod(this, _SqliteReplicaHandle_instances, assertLive_fn).call(this);
27681
- const engine = new (__privateGet(this, _replica)).Replica({ store: __privateGet(this, _store) });
27682
- const status = await engine.status();
27683
- const syncedThroughEpoch = await readFence(__privateGet(this, _replicaDir));
27684
- const state = await __privateGet(this, _store).open();
27685
- const grant = state === null ? null : newestGrantInfo(
27686
- __privateGet(this, _replica),
27687
- state.pendingGrant ?? state.grant,
27688
- state.pendingGrant,
27689
- this.spec
27690
- );
27691
- return {
27692
- coverage: status.coverage,
27693
- lastSyncAt: status.lastSyncAt,
27694
- syncedThroughEpoch,
27695
- authority: { state: status.authority.state, expiresAt: status.authority.expiresAt },
27696
- grant,
27697
- counts: status.counts,
27698
- bytes: status.bytes,
27699
- lastError: status.lastError
27700
- };
28796
+ return __privateGet(this, _read).call(this, async (snapshot, syncedThroughEpoch) => {
28797
+ __privateMethod(this, _SqliteReplicaHandle_instances, assertOpen_fn).call(this);
28798
+ const engine = new (__privateGet(this, _replica)).Replica({ store: snapshot });
28799
+ const status = await engine.status();
28800
+ const state = await snapshot.open();
28801
+ const selected = state?.pendingGrant ?? state?.grant ?? null;
28802
+ const selectedInstall = selected === null ? null : await snapshot.getGrantInstall(selected.cid);
28803
+ const activeRecord = state?.grant ?? null;
28804
+ const activeInstall = activeRecord === null ? null : await snapshot.getGrantInstall(activeRecord.cid);
28805
+ const grant = state === null ? null : newestGrantInfo(
28806
+ __privateGet(this, _replica),
28807
+ state.pendingGrant ?? state.grant,
28808
+ state.pendingGrant,
28809
+ this.spec,
28810
+ selectedInstall
28811
+ );
28812
+ return {
28813
+ coverage: status.coverage,
28814
+ lastSyncAt: status.lastSyncAt,
28815
+ syncedThroughEpoch,
28816
+ authority: activeRecord !== null && (activeInstall === null || Date.now() >= activeInstall.effectiveExpiresAt || !Number.isSafeInteger(activeInstall.effectiveExpiresAt)) ? { state: "expired", expiresAt: activeInstall === null ? null : new Date(activeInstall.effectiveExpiresAt).toISOString() } : { state: status.authority.state, expiresAt: status.authority.expiresAt },
28817
+ grant,
28818
+ counts: status.counts,
28819
+ bytes: status.bytes,
28820
+ lastError: status.lastError
28821
+ };
28822
+ }, true);
27701
28823
  }
27702
28824
  async close() {
27703
28825
  if (__privateGet(this, _closed)) return;
@@ -27712,8 +28834,11 @@ _store = new WeakMap();
27712
28834
  _replicaDir = new WeakMap();
27713
28835
  _replica = new WeakMap();
27714
28836
  _guard2 = new WeakMap();
28837
+ _read = new WeakMap();
28838
+ _policy = new WeakMap();
28839
+ _recordGrantInstall = new WeakMap();
27715
28840
  _dirSync2 = new WeakMap();
27716
- _generation = new WeakMap();
28841
+ _generation2 = new WeakMap();
27717
28842
  _ino = new WeakMap();
27718
28843
  _transportOverride = new WeakMap();
27719
28844
  _closed = new WeakMap();
@@ -27748,7 +28873,7 @@ assertLive_fn = async function() {
27748
28873
  throw error;
27749
28874
  }
27750
28875
  const state = await __privateGet(this, _store).open();
27751
- if (state !== null && state.generation !== __privateGet(this, _generation)) {
28876
+ if (state !== null && state.generation !== __privateGet(this, _generation2)) {
27752
28877
  throw new (__privateGet(this, _replica)).ReplicaError(
27753
28878
  __privateGet(this, _replica).ReplicaErrorCode.RESET_REQUIRED,
27754
28879
  "The replica was purged or reopened; open it again to continue."
@@ -27790,97 +28915,1099 @@ transports_fn = async function() {
27790
28915
  })());
27791
28916
  return __privateGet(this, _transportFactory);
27792
28917
  };
28918
+ assertEffective_fn = async function(snapshot) {
28919
+ const state = await snapshot.open();
28920
+ if (state?.grant === void 0 || state.grant === null) return;
28921
+ const installed = await snapshot.getGrantInstall(state.grant.cid);
28922
+ if (installed === null || !Number.isSafeInteger(installed.effectiveExpiresAt) || Date.now() >= installed.effectiveExpiresAt || Date.now() >= installed.consentDeadline) {
28923
+ throw new ReplicaStorageError("GRANT_EXPIRED", "The replica grant's effective lifetime has expired.");
28924
+ }
28925
+ };
28926
+ assertSyncEffective_fn = async function() {
28927
+ const state = await __privateGet(this, _store).open();
28928
+ const selected = state?.pendingGrant ?? state?.grant;
28929
+ if (selected === void 0 || selected === null) return;
28930
+ const installed = await __privateGet(this, _store).getGrantInstall(selected.cid);
28931
+ const now = Date.now();
28932
+ if (installed === null || !Number.isSafeInteger(installed.effectiveExpiresAt) || now >= installed.effectiveExpiresAt || now >= installed.consentDeadline) {
28933
+ throw new ReplicaStorageError("GRANT_EXPIRED", "The replica grant's effective lifetime has expired.");
28934
+ }
28935
+ };
27793
28936
  function createSqliteReplicaStorage(options) {
27794
28937
  const root = options.dir;
27795
- const guard = options.guard ?? ((section) => section());
28938
+ const profileDir = dirname2(options.gateFile ?? join2(dirname2(root), REPLICA_GATE_FILE));
28939
+ const diagnosticFiles = [...options.diagnosticFiles ?? []];
28940
+ for (const name of diagnosticFiles) {
28941
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name) || /^[a-z2-7]{26}$/.test(name)) {
28942
+ throw new TypeError(`Invalid replication diagnostics file name ${JSON.stringify(name)}.`);
28943
+ }
28944
+ }
28945
+ const inspectLayout = (gateReadable, approvedMarker, legacyOnly = false) => inspectProfileReplicaLayout(profileDir, gateReadable, approvedMarker, legacyOnly, diagnosticFiles);
28946
+ const guard = options.guard;
27796
28947
  const createDevice = options.createDevice ?? createDeviceKey;
27797
- async function openHandle(spec) {
27798
- const [replica, sqlite] = await Promise.all([
27799
- // @ts-ignore — TS2307 when replica's dist is absent (CI builds node-sdk first).
27800
- import("@tinycloud/replica"),
27801
- // @ts-ignore — same as above.
27802
- import("@tinycloud/replica/sqlite")
28948
+ const heldLock = new AsyncLocalStorage();
28949
+ let visibleStamp;
28950
+ let visiblePrincipalHash;
28951
+ let checkedGate;
28952
+ async function profileSection(section) {
28953
+ if (heldLock.getStore() === true) return section();
28954
+ return requiredGuard(guard, () => heldLock.run(true, section));
28955
+ }
28956
+ async function guarded(principal, stamp, identity, partitionId, section) {
28957
+ if (stamp === void 0) throw gated("not_established");
28958
+ if (guard === void 0) throw gated("gate_unavailable");
28959
+ const gate = await fileGate();
28960
+ const snapshot = await gate.readGateSnapshot(profileDir);
28961
+ if (snapshot === void 0) throw gated("gate_unavailable");
28962
+ const [principalHash, identityKeyHash] = await Promise.all([
28963
+ gate.principalHashOf(snapshot.salt, principal),
28964
+ identity === void 0 ? Promise.resolve(void 0) : gate.identityHashOf(snapshot.salt, replicationIdentityKey(identity))
27803
28965
  ]);
27804
- if (requiresSecretsOptIn2(spec.space, spec.prefix) && spec.allowSecrets !== true) {
27805
- throw new replica.ReplicaError(
27806
- replica.ReplicaErrorCode.SECRETS_OPT_IN_REQUIRED,
27807
- `Replicating ${spec.space}/kv/${spec.prefix} copies secret material to disk; pass allowSecrets to opt in.`
27808
- );
27809
- }
27810
- const idDir = partitionDirOf(root, spec.identity);
27811
- await ensurePartitionIdentity(idDir, spec.identity, options.dirSync);
27812
- const createdDevice = spec.device ?? await loadOrCreateDevice(idDir, guard, createDevice, options.dirSync);
27813
- const device = { ...createdDevice, did: principalOf(createdDevice.did) };
27814
- const replicaHash = replicaHashOf(spec.prefix, device.did);
27815
- const replicaDir = join2(idDir, "replicas", replicaHash);
27816
- const store = await sqlite.SqliteReplicaStore.open(replicaDir, {
27817
- create: true,
27818
- guard
28966
+ return profileSection(async () => {
28967
+ assertLogoutClear(options.logoutPendingFile);
28968
+ await gate.assertGateAccess(profileDir, {
28969
+ stamp,
28970
+ incarnation: snapshot.incarnation,
28971
+ principalHash,
28972
+ identityKeyHash,
28973
+ partitionId
28974
+ }, snapshot.salt);
28975
+ const result = await section();
28976
+ assertLogoutClear(options.logoutPendingFile);
28977
+ return result;
27819
28978
  });
27820
- let generation;
28979
+ }
28980
+ async function readGated(spec, partitionId, store, replicaDir, generation, section, withFence = false) {
28981
+ if (spec.stamp === void 0) throw gated("not_established");
28982
+ const gate = await fileGate();
28983
+ const snapshot = await gate.readGateSnapshot(profileDir);
28984
+ if (snapshot === void 0) throw gated("gate_unavailable");
28985
+ const [principalHash, identityKeyHash] = await Promise.all([
28986
+ gate.principalHashOf(snapshot.salt, spec.identity.principal),
28987
+ gate.identityHashOf(snapshot.salt, replicationIdentityKey(spec.identity))
28988
+ ]);
28989
+ const access = {
28990
+ stamp: spec.stamp,
28991
+ incarnation: snapshot.incarnation,
28992
+ principalHash,
28993
+ identityKeyHash,
28994
+ partitionId
28995
+ };
28996
+ const path = options.gateFile ?? join2(profileDir, REPLICA_GATE_FILE);
28997
+ let checked;
28998
+ const checkGate = async () => {
28999
+ assertLogoutClear(options.logoutPendingFile);
29000
+ let before;
29001
+ try {
29002
+ before = await stat(path, { bigint: true });
29003
+ } catch {
29004
+ throw gated("gate_unavailable");
29005
+ }
29006
+ if (checked?.ino === before.ino && checked.size === before.size && checked.mtimeNs === before.mtimeNs) return;
29007
+ const checkedState = await gate.assertGateAccess(profileDir, access, snapshot.salt);
29008
+ if (spec.grantPolicy?.consentAnchor === null && checkedState.firstInstall[identityKeyHash] === void 0) {
29009
+ const installed = await store.open();
29010
+ if (installed !== null && (installed.grant !== null || installed.pendingGrant !== null)) throw gated("gate_unavailable");
29011
+ }
29012
+ let after;
29013
+ try {
29014
+ after = await stat(path, { bigint: true });
29015
+ } catch {
29016
+ throw gated("gate_unavailable");
29017
+ }
29018
+ if (before.ino !== after.ino || before.size !== after.size || before.mtimeNs !== after.mtimeNs) {
29019
+ throw gated("gate_unavailable");
29020
+ }
29021
+ checked = after;
29022
+ };
29023
+ if (withFence) await checkGate();
29024
+ const fence = withFence ? await readFence(replicaDir) : 0;
29025
+ return store.readTransaction(generation, checkGate, (sqliteSnapshot) => section(sqliteSnapshot, fence));
29026
+ }
29027
+ async function identityDirs() {
29028
+ let entries;
29029
+ try {
29030
+ entries = await readdir(root, { withFileTypes: true });
29031
+ } catch (error) {
29032
+ if (isNotFound(error)) return [];
29033
+ throw storageError("STORAGE_ERROR", `Listing ${root}`, error);
29034
+ }
29035
+ try {
29036
+ const marker = await readLogoutMarker(join2(profileDir, REPLICA_LOGOUT_PENDING_FILE));
29037
+ if (marker !== null) {
29038
+ const inventory = await inspectLayout(true);
29039
+ assertApprovedInventory(inventory, {
29040
+ unknownEntries: marker.approvedUnknown,
29041
+ knownRoots: marker.knownRoots,
29042
+ knownEntries: marker.knownEntries,
29043
+ missingDatabases: marker.missingDatabases
29044
+ }, true);
29045
+ }
29046
+ for (const entry of entries) {
29047
+ if (entry.isSymbolicLink() || !entry.isDirectory() || !/^[a-z2-7]{26}$/.test(entry.name)) {
29048
+ if (marker?.approvedUnknown.some((item) => item.path === `replication/${entry.name}` && (entry.isDirectory() ? item.kind === "directory" : entry.isSymbolicLink() ? item.kind === "symlink" : item.kind === "file"))) continue;
29049
+ if (diagnosticFiles.includes(entry.name)) {
29050
+ const info = await lstat(join2(root, entry.name)).catch((error) => {
29051
+ if (isNotFound(error)) return null;
29052
+ throw error;
29053
+ });
29054
+ if (info === null || info.isFile()) continue;
29055
+ }
29056
+ throw new ReplicaStorageError("STORAGE_ERROR", `Unknown identity partition at ${root}.`);
29057
+ }
29058
+ }
29059
+ return entries.filter((entry) => entry.isDirectory() && /^[a-z2-7]{26}$/.test(entry.name) && !marker?.approvedUnknown.some((item) => item.path === `replication/${entry.name}` && item.kind === "directory")).map((entry) => join2(root, entry.name));
29060
+ } catch (error) {
29061
+ throw storageError("STORAGE_ERROR", `Listing ${root}`, error);
29062
+ }
29063
+ }
29064
+ async function collectIntents(principalHash, salt) {
29065
+ const gate = await fileGate();
29066
+ const intents = [];
29067
+ for (const idDir of await identityDirs()) {
29068
+ const identity = identityRecordOf(await readJsonFile(join2(idDir, "identity.json")));
29069
+ if (identity === void 0) throw new ReplicaStorageError("STORAGE_ERROR", `Missing identity at ${idDir}.`);
29070
+ if (await gate.principalHashOf(salt, identity.principal) !== principalHash) continue;
29071
+ const identityKeyHash = await gate.identityHashOf(salt, replicationIdentityKey(identity));
29072
+ const recordedAt = Date.now();
29073
+ if (await pathExists(join2(idDir, "pending.json")) || await pathExists(join2(idDir, "device.jwk"))) {
29074
+ intents.push({ id: `pending:${identityKeyHash}`, kind: "pending", identityKeyHash, principalHash, recordedAt });
29075
+ }
29076
+ const replicasDir = join2(idDir, "replicas");
29077
+ let entries;
29078
+ try {
29079
+ entries = await readdir(replicasDir, { withFileTypes: true });
29080
+ } catch (error) {
29081
+ if (isNotFound(error)) continue;
29082
+ throw storageError("STORAGE_ERROR", `Listing ${replicasDir}`, error);
29083
+ }
29084
+ for (const entry of entries) {
29085
+ if (!entry.isDirectory()) continue;
29086
+ if (!/^[a-z2-7]{26}$/.test(entry.name)) {
29087
+ throw new ReplicaStorageError("STORAGE_ERROR", `Unrecognized replica partition at ${replicasDir}.`);
29088
+ }
29089
+ const partitionId = `${identityKeyHash}:${entry.name}`;
29090
+ intents.push({ id: partitionId, kind: "partition", partitionId, principalHash, recordedAt });
29091
+ }
29092
+ }
29093
+ return intents;
29094
+ }
29095
+ async function retainGrantInstalls(gate, gateState, identityKeyHash, store) {
29096
+ for (const { cid, installedAt } of await store.grantInstallTimes()) {
29097
+ const key2 = await gate.grantInstallHashOf(gateState.salt, identityKeyHash, cid);
29098
+ await gate.recordGrantInstall(profileDir, key2, installedAt);
29099
+ }
29100
+ }
29101
+ async function removeCorruptOrphanGate(gate) {
29102
+ const path = join2(profileDir, REPLICA_GATE_FILE);
29103
+ const before = await lstat(path, { bigint: true });
29104
+ if (!before.isFile() || before.isSymbolicLink()) throw gated("gate_unavailable");
29105
+ let corrupt = false;
29106
+ try {
29107
+ await gate.readGateSnapshot(profileDir);
29108
+ } catch (error) {
29109
+ if (typeof error === "object" && error !== null && "code" in error && error.code === "REPLICA_GATED" && "gate" in error && error.gate === "gate_unavailable") {
29110
+ corrupt = true;
29111
+ } else throw error;
29112
+ }
29113
+ if (!corrupt) throw gated("stamp_stale");
29114
+ const current = await lstat(path, { bigint: true });
29115
+ if (current.dev !== before.dev || current.ino !== before.ino || current.mtimeNs !== before.mtimeNs || current.size !== before.size) throw gated("stamp_stale");
29116
+ await rm(path);
29117
+ await (options.dirSync ?? fsyncDir)(profileDir);
29118
+ if (await gate.readGateSnapshot(profileDir) !== void 0) throw gated("stamp_stale");
29119
+ }
29120
+ async function readLogoutState(gate) {
27821
29121
  try {
29122
+ return { read: await gate.readLogoutGate(profileDir), corrupt: false };
29123
+ } catch (error) {
29124
+ if (isGateUnavailable(error)) return { read: void 0, corrupt: true };
29125
+ throw error;
29126
+ }
29127
+ }
29128
+ async function validateV1(gate, path, marker, read, mode) {
29129
+ if (read === void 0 || read.state.incarnation !== marker.incarnation) throw gated("stamp_stale");
29130
+ const { state, floorLagging } = read;
29131
+ if (marker.deniedEpoch !== void 0) {
29132
+ if (floorLagging || !v1Denied(marker, state, marker.deniedEpoch)) throw restoredGate();
29133
+ return { marker, state, active: false };
29134
+ }
29135
+ if (!floorLagging && state.principalHash === null && state.lifecycleEpoch > marker.lifecycleEpoch && (marker.remotePending ? state.eraseDeferred && state.deferredPrincipalHash === marker.principalHash : !state.eraseDeferred && state.deferredPrincipalHash === null)) {
29136
+ return { marker: await bindDeniedEpoch(path, marker, state.lifecycleEpoch), state, active: false };
29137
+ }
29138
+ if (state.principalHash !== marker.principalHash || state.lifecycleEpoch !== marker.lifecycleEpoch) {
29139
+ throw gated("stamp_stale");
29140
+ }
29141
+ if (!floorLagging && !mode.denyActive) {
29142
+ if (!mode.active) throw gated("stamp_stale");
29143
+ return { marker, state, active: true };
29144
+ }
29145
+ if (mode.acceptLagging) return { marker, state, active: true };
29146
+ if (mode.deny === void 0) throw interruptedSignOut();
29147
+ const denied = await gate.denyGate(
29148
+ profileDir,
29149
+ { reason: "sign_out", principal: "" },
29150
+ (principalHash) => collectIntents(principalHash, state.salt),
29151
+ { salt: state.salt, principalHash: marker.principalHash },
29152
+ { deferErase: mode.deny === "deferred" }
29153
+ );
29154
+ if (denied.status !== "denied") throw gated("gate_unavailable");
29155
+ const after = await gate.readLogoutGate(profileDir);
29156
+ if (after === void 0 || after.floorLagging || !v1Denied(marker, after.state, denied.lifecycleEpoch)) {
29157
+ throw gated("gate_unavailable");
29158
+ }
29159
+ return { marker: await bindDeniedEpoch(path, marker, denied.lifecycleEpoch), state: after.state, active: false };
29160
+ }
29161
+ function v1Denied(marker, state, epoch) {
29162
+ return state.incarnation === marker.incarnation && state.principalHash === null && state.lifecycleEpoch === epoch && (state.deferredPrincipalHash === marker.principalHash || !marker.remotePending && !state.eraseDeferred && state.deferredPrincipalHash === null);
29163
+ }
29164
+ async function bindDeniedEpoch(path, marker, deniedEpoch) {
29165
+ if (marker.deniedEpoch !== void 0) throw gated("stamp_stale");
29166
+ const bound = { ...marker, deniedEpoch };
29167
+ await writeJsonAtomic(path, bound, options.dirSync);
29168
+ return bound;
29169
+ }
29170
+ async function validatedRemoteMarker(path, marker, principal, mode) {
29171
+ if (marker.principalDigest !== logoutPrincipalDigest(principal)) throw gated("principal_mismatch");
29172
+ const gate = await fileGate();
29173
+ const { read, corrupt } = await readLogoutState(gate);
29174
+ if (corrupt) throw gated("gate_unavailable");
29175
+ if (marker.v === 2) {
29176
+ if (read !== void 0) throw gated("stamp_stale");
29177
+ return { marker, state: void 0 };
29178
+ }
29179
+ if (read === void 0 || read.state.incarnation !== marker.incarnation) throw gated("stamp_stale");
29180
+ if (marker.v === 3) {
29181
+ if (read.floorLagging || read.state.lifecycleEpoch !== marker.lifecycleEpoch || read.state.principalHash !== null || read.state.eraseDeferred) throw gated("stamp_stale");
29182
+ return { marker, state: read.state };
29183
+ }
29184
+ if (marker.principalHash !== await gate.principalHashOf(read.state.salt, principal)) {
29185
+ throw gated("principal_mismatch");
29186
+ }
29187
+ const checked = await validateV1(gate, path, marker, read, mode);
29188
+ return { marker: checked.marker, state: checked.state };
29189
+ }
29190
+ async function currentLayout() {
29191
+ const { read, corrupt } = await readLogoutState(await fileGate());
29192
+ if (corrupt) throw gated("gate_unavailable");
29193
+ const report = await inspectLayout(read !== void 0);
29194
+ assertSupportedProfileLayout(report);
29195
+ return report;
29196
+ }
29197
+ async function commitPlainAbandon(path, marker, report, discardLocalChanges) {
29198
+ const { remotePending: _, discardLocalChanges: _oldDiscard, ...previous } = marker;
29199
+ const abandoned = {
29200
+ ...previous,
29201
+ remoteAbandoned: true,
29202
+ approvedUnknown: discardLocalChanges ? report.unknownEntries : [],
29203
+ knownRoots: report.knownRoots,
29204
+ knownEntries: report.knownEntries,
29205
+ missingDatabases: report.missingDatabases,
29206
+ ...discardLocalChanges ? { discardLocalChanges: true } : {}
29207
+ };
29208
+ await writeJsonAtomic(path, abandoned, options.dirSync);
29209
+ return abandoned;
29210
+ }
29211
+ function deny(input, denyOptions) {
29212
+ const deferErase = denyOptions?.deferErase === true;
29213
+ if (deferErase && input.reason !== "sign_out") {
29214
+ throw new TypeError("Deferred erase is only available for sign-out.");
29215
+ }
29216
+ visibleStamp = void 0;
29217
+ visiblePrincipalHash = void 0;
29218
+ checkedGate = void 0;
29219
+ return (async () => {
27822
29220
  try {
27823
- await store.init({
27824
- name: spec.prefix.replace(/\/+$/, "") || "replica",
27825
- replicaId: replicaHash,
27826
- host: spec.identity.host,
27827
- space: spec.space,
27828
- prefix: spec.prefix,
27829
- deviceDid: device.did,
27830
- allowSecrets: spec.allowSecrets,
27831
- localReadPolicy: "whileGrantValid",
27832
- retentionGrantCid: null
29221
+ const gate = await fileGate();
29222
+ return await profileSection(async () => {
29223
+ const snapshot = (await gate.readLogoutGate(profileDir))?.state;
29224
+ const precomputed = snapshot === void 0 ? void 0 : {
29225
+ salt: snapshot.salt,
29226
+ principalHash: await gate.principalHashOf(snapshot.salt, input.principal)
29227
+ };
29228
+ const result = await gate.denyGate(
29229
+ profileDir,
29230
+ input,
29231
+ async (principalHash) => {
29232
+ if (snapshot === void 0) throw gated("gate_unavailable");
29233
+ return collectIntents(principalHash, snapshot.salt);
29234
+ },
29235
+ precomputed,
29236
+ { deferErase }
29237
+ );
29238
+ if (result.status !== "denied") return result;
29239
+ if (input.reason === "sign_out" && snapshot !== void 0 && precomputed !== void 0) {
29240
+ const path = join2(profileDir, REPLICA_LOGOUT_PENDING_FILE);
29241
+ const marker = await readLogoutMarker(path);
29242
+ if (marker?.v === 1 && marker.deniedEpoch === void 0 && marker.incarnation === snapshot.incarnation && marker.principalHash === precomputed.principalHash && marker.remotePending === true === deferErase) {
29243
+ await bindDeniedEpoch(path, marker, result.lifecycleEpoch);
29244
+ }
29245
+ }
29246
+ if (!deferErase) return result;
29247
+ const resumeToken = await gate.deferredGateToken(profileDir);
29248
+ if (resumeToken === null || resumeToken.deniedEpoch !== result.lifecycleEpoch) {
29249
+ return { status: "unguarded", code: "gate_unavailable" };
29250
+ }
29251
+ return { ...result, resumeToken };
27833
29252
  });
27834
29253
  } catch (error) {
27835
- if (!replica.isReplicaError(error, replica.ReplicaErrorCode.CONFIG_MISMATCH)) {
27836
- throw error;
29254
+ return { status: "unguarded", code: typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : "gate_unavailable" };
29255
+ }
29256
+ })();
29257
+ }
29258
+ const lifecycle = {
29259
+ kind: "file",
29260
+ async recover({ signal }) {
29261
+ try {
29262
+ return await lifecycle.erasePending({ signal });
29263
+ } catch (error) {
29264
+ const code = typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : "STORAGE_ERROR";
29265
+ let remaining = 0;
29266
+ try {
29267
+ remaining = (await (await fileGate()).readGateSnapshot(profileDir))?.intents.length ?? 0;
29268
+ } catch {
27837
29269
  }
29270
+ return { erased: [], failed: [{ id: "gate", code }], remaining };
27838
29271
  }
27839
- const state = await store.open();
27840
- if (state === null) {
27841
- throw new replica.ReplicaError(
27842
- replica.ReplicaErrorCode.NOT_FOUND,
27843
- "The replica row vanished during init."
27844
- );
29272
+ },
29273
+ async establishSessionStamp(principal, { consentAnchor }) {
29274
+ if (consentAnchor?.source === "first_install" || !Number.isSafeInteger(consentAnchor?.at) || consentAnchor.at < 0) {
29275
+ throw gated("not_established");
29276
+ }
29277
+ const gate = await fileGate();
29278
+ const stamp = await profileSection(async () => {
29279
+ assertLogoutClear(options.logoutPendingFile);
29280
+ return gate.establishSessionStamp(profileDir, principal, { siweIssuedAt: consentAnchor.at });
29281
+ });
29282
+ const state = await gate.readGateSnapshot(profileDir);
29283
+ if (state === void 0) throw gated("gate_unavailable");
29284
+ visiblePrincipalHash = await gate.principalHashOf(state.salt, principal);
29285
+ checkedGate = void 0;
29286
+ visibleStamp = stamp;
29287
+ return stamp;
29288
+ },
29289
+ async restoreSessionStamp(principal, persisted) {
29290
+ const gate = await fileGate();
29291
+ const stamp = await profileSection(async () => {
29292
+ assertLogoutClear(options.logoutPendingFile);
29293
+ return gate.restoreSessionStamp(profileDir, principal, persisted);
29294
+ });
29295
+ const state = await gate.readGateSnapshot(profileDir);
29296
+ if (state === void 0) throw gated("gate_unavailable");
29297
+ visiblePrincipalHash = await gate.principalHashOf(state.salt, principal);
29298
+ checkedGate = void 0;
29299
+ visibleStamp = stamp;
29300
+ return stamp;
29301
+ },
29302
+ async activationAllowed(next) {
29303
+ if (guard === void 0) return false;
29304
+ try {
29305
+ const gate = await fileGate();
29306
+ return await profileSection(async () => {
29307
+ assertLogoutClear(options.logoutPendingFile);
29308
+ const state = await gate.readGateSnapshot(profileDir);
29309
+ return state !== void 0 && !state.eraseDeferred && state.intents.length === 0 && state.principalHash !== null && state.principalHash === await gate.principalHashOf(state.salt, next);
29310
+ });
29311
+ } catch {
29312
+ return false;
27845
29313
  }
27846
- const config = state.config;
27847
- if (config.host !== spec.identity.host || config.space !== spec.space || config.prefix !== spec.prefix) {
27848
- throw new replica.ReplicaError(
27849
- replica.ReplicaErrorCode.CONFIG_MISMATCH,
27850
- `This replica follows ${config.host} ${config.space}/kv/${config.prefix}, not ${spec.identity.host} ${spec.space}/kv/${spec.prefix}.`
27851
- );
29314
+ },
29315
+ checkDelivery(stamp) {
29316
+ if (guard === void 0 || visiblePrincipalHash === void 0) return { status: "gated", gate: "gate_unavailable" };
29317
+ try {
29318
+ assertLogoutClear(options.logoutPendingFile);
29319
+ } catch {
29320
+ return { status: "gated", gate: "gate_unavailable" };
27852
29321
  }
27853
- if (principalOf(config.deviceDid) !== principalOf(device.did)) {
27854
- throw new replica.ReplicaError(
27855
- replica.ReplicaErrorCode.CONFIG_MISMATCH,
27856
- "The stored replica belongs to a different device key; purge the partition to rekey."
27857
- );
29322
+ if (!stamp || visibleStamp?.incarnation !== stamp.incarnation || visibleStamp.lifecycleEpoch !== stamp.lifecycleEpoch) {
29323
+ return { status: "gated", gate: "stamp_stale" };
27858
29324
  }
27859
- generation = state.generation;
27860
- } catch (error) {
27861
- await store.close().catch(() => void 0);
27862
- throw error;
29325
+ try {
29326
+ const path = options.gateFile ?? join2(profileDir, REPLICA_GATE_FILE);
29327
+ const stats = statSync(path, { bigint: true });
29328
+ if (checkedGate === void 0 || checkedGate.ino !== stats.ino || checkedGate.mtimeNs !== stats.mtimeNs || checkedGate.size !== stats.size) {
29329
+ const raw = JSON.parse(readFileSync2(path, "utf8"));
29330
+ if (typeof raw.incarnation !== "string" || !Number.isSafeInteger(raw.lifecycleEpoch) || raw.principalHash !== null && typeof raw.principalHash !== "string" || !Array.isArray(raw.intents)) {
29331
+ return { status: "gated", gate: "gate_unavailable" };
29332
+ }
29333
+ checkedGate = {
29334
+ ino: stats.ino,
29335
+ mtimeNs: stats.mtimeNs,
29336
+ size: stats.size,
29337
+ incarnation: raw.incarnation,
29338
+ epoch: raw.lifecycleEpoch,
29339
+ principalHash: raw.principalHash
29340
+ };
29341
+ }
29342
+ } catch {
29343
+ return { status: "gated", gate: "gate_unavailable" };
29344
+ }
29345
+ if (checkedGate.incarnation !== stamp.incarnation || checkedGate.epoch !== stamp.lifecycleEpoch) {
29346
+ return { status: "gated", gate: "stamp_stale" };
29347
+ }
29348
+ return checkedGate.principalHash === visiblePrincipalHash ? { status: "allow" } : { status: "gated", gate: "principal_mismatch" };
29349
+ },
29350
+ consentDeadline() {
29351
+ return null;
29352
+ },
29353
+ deliverable(stamp) {
29354
+ return lifecycle.checkDelivery(stamp).status === "allow";
29355
+ },
29356
+ deny,
29357
+ deferredErase() {
29358
+ return profileSection(async () => (await fileGate()).deferredGateToken(profileDir));
29359
+ },
29360
+ async resumeDeferredErase(token, { signal }) {
29361
+ signal.throwIfAborted();
29362
+ await profileSection(async () => (await fileGate()).resumeDeferredGate(profileDir, token));
29363
+ return lifecycle.erasePending({ signal });
29364
+ },
29365
+ async preflightLogout({ principal, discardLocalChanges = false }) {
29366
+ return profileSection(async () => {
29367
+ const marker = await readLogoutMarker(join2(profileDir, REPLICA_LOGOUT_PENDING_FILE));
29368
+ const gate = await fileGate();
29369
+ const { read, corrupt } = await readLogoutState(gate);
29370
+ const state = read?.state;
29371
+ let otherAccount = false;
29372
+ if (state !== void 0) {
29373
+ const hash3 = await gate.principalHashOf(state.salt, principal);
29374
+ otherAccount = gateOwnedByAnother(state, hash3);
29375
+ if (marker?.v === 1 && marker.principalHash !== hash3) throw gated("principal_mismatch");
29376
+ }
29377
+ const report = await inspectLayout(state !== void 0, void 0, otherAccount);
29378
+ assertSupportedProfileLayout(report);
29379
+ if (!discardLocalChanges && (report.unknownPaths.length > 0 || corrupt && !otherAccount || !otherAccount && (state === void 0 || state.principalHash === null && !state.eraseDeferred) && await hasOrphanPending(profileDir))) {
29380
+ throw localChangesRefusal(report.unknownPaths);
29381
+ }
29382
+ return report;
29383
+ });
29384
+ },
29385
+ async prepareLogoutPending({ principal, approval, discardLocalChanges = false, remotePending }) {
29386
+ const gate = await fileGate();
29387
+ return profileSection(async () => {
29388
+ const path = join2(profileDir, REPLICA_LOGOUT_PENDING_FILE);
29389
+ const previous = await readLogoutMarker(path);
29390
+ const { read, corrupt: corruptGate } = await readLogoutState(gate);
29391
+ let state = read?.state;
29392
+ const principalHash = state === void 0 ? void 0 : await gate.principalHashOf(state.salt, principal);
29393
+ const otherAccount = state !== void 0 && gateOwnedByAnother(state, principalHash);
29394
+ let report = await inspectLayout(state !== void 0, void 0, otherAccount);
29395
+ assertSupportedProfileLayout(report);
29396
+ const assertApproved = (current) => {
29397
+ assertApprovedInventory(current, approval);
29398
+ if (approval.knownLegacyReplicas !== current.knownLegacyReplicas || JSON.stringify(approval.unknownPaths) !== JSON.stringify(current.unknownPaths) || JSON.stringify(approval.unknownLegacyPaths) !== JSON.stringify(current.unknownLegacyPaths)) {
29399
+ throw localChangesRefusal(current.unknownPaths);
29400
+ }
29401
+ };
29402
+ assertApproved(report);
29403
+ if (otherAccount) {
29404
+ if (previous !== null) throw gated("principal_mismatch");
29405
+ if (!discardLocalChanges && report.unknownPaths.length > 0) throw localChangesRefusal(report.unknownPaths);
29406
+ return null;
29407
+ }
29408
+ if (!discardLocalChanges && (report.unknownPaths.length > 0 || corruptGate || (state === void 0 || state.principalHash === null && !state.eraseDeferred) && await hasOrphanPending(profileDir))) {
29409
+ throw localChangesRefusal(report.unknownPaths);
29410
+ }
29411
+ if (previous !== null) {
29412
+ if (previous.principalDigest !== logoutPrincipalDigest(principal)) throw gated("principal_mismatch");
29413
+ if (corruptGate) throw gated("gate_unavailable");
29414
+ let current = previous;
29415
+ if (previous.v === 2) {
29416
+ if (read !== void 0) throw gated("stamp_stale");
29417
+ } else if (read === void 0 || previous.incarnation !== read.state.incarnation) {
29418
+ throw gated("stamp_stale");
29419
+ } else if (previous.v === 3) {
29420
+ if (read.floorLagging || read.state.lifecycleEpoch !== previous.lifecycleEpoch || read.state.principalHash !== null || read.state.eraseDeferred) throw gated("stamp_stale");
29421
+ } else {
29422
+ if (previous.principalHash !== principalHash) throw gated("principal_mismatch");
29423
+ current = (await validateV1(gate, path, previous, read, { active: true, acceptLagging: true })).marker;
29424
+ }
29425
+ assertApprovedInventory(report, {
29426
+ unknownEntries: current.approvedUnknown,
29427
+ knownRoots: current.knownRoots,
29428
+ knownEntries: current.knownEntries,
29429
+ missingDatabases: current.missingDatabases
29430
+ });
29431
+ if (current.remotePending !== remotePending || current.remoteAbandoned) {
29432
+ throw gated("gate_unavailable");
29433
+ }
29434
+ return current;
29435
+ }
29436
+ if (state?.eraseDeferred) throw gated("stamp_stale");
29437
+ if (read?.floorLagging) {
29438
+ if (state.principalHash === null) {
29439
+ state = await gate.advanceInactiveGate(profileDir);
29440
+ } else {
29441
+ const salt = state.salt;
29442
+ const denied = await gate.denyGate(
29443
+ profileDir,
29444
+ { reason: "sign_out", principal },
29445
+ (hash3) => collectIntents(hash3, salt),
29446
+ { salt, principalHash }
29447
+ );
29448
+ if (denied.status !== "denied") throw gated("gate_unavailable");
29449
+ state = await gate.readGateSnapshot(profileDir);
29450
+ }
29451
+ report = await inspectLayout(true);
29452
+ assertSupportedProfileLayout(report);
29453
+ assertApproved(report);
29454
+ }
29455
+ if (state === void 0 && report.knownRoots.length === 0 && !corruptGate) return null;
29456
+ if (state !== void 0 && state.principalHash === null && !state.eraseDeferred && report.knownRoots.length === 0 && state.intents.length === 0) return null;
29457
+ const remoteState = remotePending ? { remotePending: true } : {};
29458
+ const approvalFields = {
29459
+ principalDigest: logoutPrincipalDigest(principal),
29460
+ approvalSalt: randomBytes2(16).toString("hex"),
29461
+ approvedUnknown: discardLocalChanges ? report.unknownEntries : [],
29462
+ knownRoots: report.knownRoots,
29463
+ knownEntries: report.knownEntries,
29464
+ missingDatabases: report.missingDatabases,
29465
+ ...discardLocalChanges ? { discardLocalChanges: true } : {},
29466
+ ...remoteState
29467
+ };
29468
+ let marker;
29469
+ if (state === void 0) {
29470
+ marker = {
29471
+ v: 2,
29472
+ scope: "orphan_profile",
29473
+ nonce: randomBytes2(16).toString("hex"),
29474
+ ...approvalFields
29475
+ };
29476
+ } else if (state.principalHash === null) {
29477
+ marker = {
29478
+ v: 3,
29479
+ scope: "inactive_gate",
29480
+ incarnation: state.incarnation,
29481
+ lifecycleEpoch: state.lifecycleEpoch,
29482
+ ...approvalFields
29483
+ };
29484
+ } else {
29485
+ marker = {
29486
+ v: 1,
29487
+ incarnation: state.incarnation,
29488
+ lifecycleEpoch: state.lifecycleEpoch,
29489
+ principalHash,
29490
+ ...approvalFields
29491
+ };
29492
+ }
29493
+ await writeJsonAtomic(path, marker, options.dirSync);
29494
+ visibleStamp = void 0;
29495
+ visiblePrincipalHash = void 0;
29496
+ checkedGate = void 0;
29497
+ if (corruptGate) await removeCorruptOrphanGate(gate);
29498
+ return marker;
29499
+ });
29500
+ },
29501
+ async completeRemoteLogout({ principal }) {
29502
+ return profileSection(async () => {
29503
+ const path = join2(profileDir, REPLICA_LOGOUT_PENDING_FILE);
29504
+ const marker = await readLogoutMarker(path);
29505
+ if (marker === null || marker.remotePending !== true) throw gated("gate_unavailable");
29506
+ const checked = await validatedRemoteMarker(path, marker, principal, { active: false });
29507
+ const { remotePending: _, ...complete } = checked.marker;
29508
+ await writeJsonAtomic(path, complete, options.dirSync);
29509
+ return complete;
29510
+ });
29511
+ },
29512
+ async remotePendingLogout({ principal }) {
29513
+ return profileSection(async () => {
29514
+ const path = join2(profileDir, REPLICA_LOGOUT_PENDING_FILE);
29515
+ const marker = await readLogoutMarker(path);
29516
+ if (marker === null || marker.remotePending !== true) return false;
29517
+ await validatedRemoteMarker(path, marker, principal, { active: false });
29518
+ return true;
29519
+ });
29520
+ },
29521
+ async abandonRemoteLogout({ principal, approval, discardLocalChanges = false }) {
29522
+ return profileSection(async () => {
29523
+ const path = join2(profileDir, REPLICA_LOGOUT_PENDING_FILE);
29524
+ const marker = await readLogoutMarker(path);
29525
+ if (marker === null || marker.remotePending !== true) throw gated("gate_unavailable");
29526
+ if (marker.principalDigest !== logoutPrincipalDigest(principal)) throw gated("principal_mismatch");
29527
+ const report = await currentLayout();
29528
+ if (storedInventoryChanged(marker, report)) throw changedInventoryRefusal(marker, principal, report);
29529
+ assertApprovedInventory(report, approval);
29530
+ if (approval.knownLegacyReplicas !== report.knownLegacyReplicas || JSON.stringify(approval.unknownPaths) !== JSON.stringify(report.unknownPaths) || JSON.stringify(approval.unknownLegacyPaths) !== JSON.stringify(report.unknownLegacyPaths) || !discardLocalChanges && report.unknownPaths.length > 0) {
29531
+ throw localChangesRefusal(report.unknownPaths);
29532
+ }
29533
+ const checked = await validatedRemoteMarker(path, marker, principal, { active: true, deny: "deferred" });
29534
+ return commitPlainAbandon(path, checked.marker, report, discardLocalChanges);
29535
+ });
29536
+ },
29537
+ async approveChangedPlainLogout({ principal, approval, discardLocalChanges, inventoryApprovalToken: token }) {
29538
+ return profileSection(async () => {
29539
+ const path = join2(profileDir, REPLICA_LOGOUT_PENDING_FILE);
29540
+ const marker = await readLogoutMarker(path);
29541
+ if (marker === null || marker.remotePending !== true) throw gated("gate_unavailable");
29542
+ if (marker.principalDigest !== logoutPrincipalDigest(principal)) throw gated("principal_mismatch");
29543
+ const report = await currentLayout();
29544
+ if (!storedInventoryChanged(marker, report)) throw gated("gate_unavailable");
29545
+ const entries = approvalEntries(report);
29546
+ if (discardLocalChanges !== true || typeof token !== "string" || !/^[0-9a-f]{32}$/.test(token) || token !== inventoryApprovalToken(marker, principal, entries)) {
29547
+ throw changedInventoryRefusal(marker, principal, report, entries);
29548
+ }
29549
+ assertApprovedInventory(report, approval);
29550
+ if (approval.knownLegacyReplicas !== report.knownLegacyReplicas || JSON.stringify(approval.unknownPaths) !== JSON.stringify(report.unknownPaths) || JSON.stringify(approval.unknownLegacyPaths) !== JSON.stringify(report.unknownLegacyPaths)) {
29551
+ throw changedInventoryRefusal(marker, principal, report, entries);
29552
+ }
29553
+ const checked = await validatedRemoteMarker(path, marker, principal, { active: true, deny: "deferred" });
29554
+ return commitPlainAbandon(path, checked.marker, report, true);
29555
+ });
29556
+ },
29557
+ async reapproveLogoutPending({ principal, approval }) {
29558
+ return profileSection(async () => {
29559
+ const path = join2(profileDir, REPLICA_LOGOUT_PENDING_FILE);
29560
+ const marker = await readLogoutMarker(path);
29561
+ if (marker === null || marker.remotePending) throw gated("gate_unavailable");
29562
+ if (marker.principalDigest !== logoutPrincipalDigest(principal)) throw gated("principal_mismatch");
29563
+ const report = await currentLayout();
29564
+ assertApprovedInventory(report, approval);
29565
+ if (JSON.stringify(report.unknownPaths) !== JSON.stringify(approval.unknownPaths)) {
29566
+ throw localChangesRefusal(report.unknownPaths);
29567
+ }
29568
+ const checked = await validatedRemoteMarker(path, marker, principal, { active: true });
29569
+ const amended = {
29570
+ ...checked.marker,
29571
+ approvedUnknown: report.unknownEntries,
29572
+ knownRoots: report.knownRoots,
29573
+ knownEntries: report.knownEntries,
29574
+ missingDatabases: report.missingDatabases,
29575
+ ...report.unknownPaths.length > 0 ? { discardLocalChanges: true } : {}
29576
+ };
29577
+ await writeJsonAtomic(path, amended, options.dirSync);
29578
+ return amended;
29579
+ });
29580
+ },
29581
+ async recoverLogoutPending({ signal, clearCredentials, eraseLegacyReplicas }) {
29582
+ const path = join2(profileDir, REPLICA_LOGOUT_PENDING_FILE);
29583
+ const failure = async (error) => ({
29584
+ erased: [],
29585
+ failed: [{ id: "logout", code: typeof error === "object" && error !== null && "reportCode" in error && typeof error.reportCode === "string" ? error.reportCode : typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code === "REPLICA_GATED" && "gate" in error && typeof error.gate === "string" ? error.gate : error.code : "STORAGE_ERROR" }],
29586
+ remaining: Math.max(1, (await lifecycle.pendingPurge().catch(() => [])).length)
29587
+ });
29588
+ try {
29589
+ const prepared = await profileSection(async () => {
29590
+ const marker2 = await readLogoutMarker(path);
29591
+ if (marker2 === null) {
29592
+ const state2 = await (await fileGate()).readGateSnapshot(profileDir);
29593
+ return { report: { erased: [], failed: [], remaining: state2?.intents.length ?? 0 } };
29594
+ }
29595
+ signal.throwIfAborted();
29596
+ if (marker2.remotePending) throw gated("gate_unavailable");
29597
+ const gate = await fileGate();
29598
+ const { read, corrupt } = await readLogoutState(gate);
29599
+ if (corrupt) throw gated("gate_unavailable");
29600
+ let current = marker2;
29601
+ let state = read?.state;
29602
+ if (marker2.v === 2) {
29603
+ if (read !== void 0) throw gated("stamp_stale");
29604
+ } else if (read === void 0 || read.state.incarnation !== marker2.incarnation) {
29605
+ throw gated("stamp_stale");
29606
+ } else if (marker2.v === 3) {
29607
+ if (read.floorLagging || read.state.principalHash !== null || read.state.eraseDeferred || read.state.lifecycleEpoch !== marker2.lifecycleEpoch) throw gated("stamp_stale");
29608
+ } else {
29609
+ const checked = await validateV1(
29610
+ gate,
29611
+ path,
29612
+ marker2,
29613
+ read,
29614
+ { active: true, deny: "ordinary", denyActive: true }
29615
+ );
29616
+ current = checked.marker;
29617
+ state = checked.state;
29618
+ }
29619
+ await clearCredentials();
29620
+ for (const file of ["session.json", "key.json"]) {
29621
+ try {
29622
+ await lstat(join2(profileDir, file));
29623
+ throw gated("gate_unavailable");
29624
+ } catch (error) {
29625
+ if (!isNotFound(error)) throw error;
29626
+ }
29627
+ }
29628
+ await (options.dirSync ?? fsyncDir)(profileDir);
29629
+ const report = await inspectLayout(state !== void 0, marker2);
29630
+ assertApprovedInventory(report, {
29631
+ unknownEntries: marker2.approvedUnknown,
29632
+ knownRoots: marker2.knownRoots,
29633
+ knownEntries: marker2.knownEntries,
29634
+ missingDatabases: marker2.missingDatabases
29635
+ }, true);
29636
+ if (!marker2.discardLocalChanges && report.unknownEntries.length > 0) {
29637
+ throw localChangesRefusal(report.unknownPaths);
29638
+ }
29639
+ if (current.v === 1 && state?.eraseDeferred) {
29640
+ await gate.resumeDeferredGate(profileDir, {
29641
+ deniedEpoch: current.deniedEpoch,
29642
+ principalHash: current.principalHash
29643
+ });
29644
+ }
29645
+ await eraseLegacyReplicas({
29646
+ discardLocalChanges: marker2.discardLocalChanges === true,
29647
+ approvedUnknown: marker2.approvedUnknown,
29648
+ knownRoots: marker2.knownRoots,
29649
+ knownEntries: marker2.knownEntries,
29650
+ missingDatabases: marker2.missingDatabases
29651
+ });
29652
+ try {
29653
+ await lstat(join2(profileDir, "replicas"));
29654
+ throw gated("gate_unavailable");
29655
+ } catch (error) {
29656
+ if (!isNotFound(error)) throw error;
29657
+ }
29658
+ return { marker: current };
29659
+ });
29660
+ if ("report" in prepared) return prepared.report;
29661
+ const marker = prepared.marker;
29662
+ let erased = { erased: [], failed: [], remaining: 0 };
29663
+ if (marker.v !== 2) {
29664
+ erased = await eraseGateIntents({ signal }, marker);
29665
+ if (erased.failed.length > 0 || erased.remaining > 0) return erased;
29666
+ }
29667
+ await profileSection(async () => {
29668
+ const pending = await readLogoutMarker(path);
29669
+ if (pending === null || pending.remotePending || JSON.stringify(pending) !== JSON.stringify(marker)) {
29670
+ throw gated("stamp_stale");
29671
+ }
29672
+ const gate = await fileGate();
29673
+ const state = await gate.readGateSnapshot(profileDir);
29674
+ if (marker.v === 2 && state !== void 0) throw gated("stamp_stale");
29675
+ const report = await inspectLayout(state !== void 0, marker);
29676
+ assertApprovedInventory(report, {
29677
+ unknownEntries: marker.approvedUnknown,
29678
+ knownRoots: marker.knownRoots,
29679
+ knownEntries: marker.knownEntries,
29680
+ missingDatabases: marker.missingDatabases
29681
+ }, true);
29682
+ await eraseApprovedUnknown(profileDir, marker, "replication", diagnosticFiles);
29683
+ await eraseKnownFlagResidue(profileDir, marker, diagnosticFiles);
29684
+ const finalReport = await inspectLayout(state !== void 0, marker);
29685
+ assertApprovedInventory(finalReport, {
29686
+ unknownEntries: marker.approvedUnknown,
29687
+ knownRoots: marker.knownRoots,
29688
+ knownEntries: marker.knownEntries,
29689
+ missingDatabases: marker.missingDatabases
29690
+ }, true);
29691
+ if (finalReport.unknownPaths.length > 0) throw localChangesRefusal(finalReport.unknownPaths);
29692
+ if (finalReport.knownRoots.length > 0) throw gated("gate_unavailable");
29693
+ await (options.dirSync ?? fsyncDir)(profileDir);
29694
+ await rm(path);
29695
+ await (options.dirSync ?? fsyncDir)(profileDir);
29696
+ });
29697
+ return erased;
29698
+ } catch (error) {
29699
+ return failure(error);
29700
+ }
29701
+ },
29702
+ async pendingPurge() {
29703
+ const gate = await fileGate();
29704
+ return (await gate.readGateSnapshot(profileDir))?.intents ?? [];
29705
+ },
29706
+ erasePending({ signal }) {
29707
+ return eraseGateIntents({ signal });
27863
29708
  }
27864
- let ino;
29709
+ };
29710
+ async function eraseGateIntents({ signal }, marker) {
29711
+ const gate = await fileGate();
29712
+ const erased = [];
29713
+ const failed = [];
29714
+ const intents = await lifecycle.pendingPurge();
27865
29715
  try {
27866
- ino = (await stat(join2(replicaDir, "replica.db"))).ino;
27867
- } catch (error) {
27868
- if (!isNotFound(error)) throw error;
27869
- }
27870
- return new SqliteReplicaHandle({
27871
- spec,
27872
- device,
27873
- store,
27874
- replicaDir,
27875
- replica,
27876
- guard,
27877
- transportFor: options.transportFor,
27878
- dirSync: options.dirSync,
27879
- generation,
27880
- ino
29716
+ if (marker === void 0) assertLogoutClear(options.logoutPendingFile);
29717
+ else if (marker.remotePending || await readLogoutMarker(join2(profileDir, REPLICA_LOGOUT_PENDING_FILE)) === null) {
29718
+ throw gated("gate_unavailable");
29719
+ }
29720
+ } catch {
29721
+ return { erased, failed: [{ id: "logout", code: "REPLICA_GATED" }], remaining: Math.max(1, intents.length) };
29722
+ }
29723
+ const held = await profileSection(() => gate.readGateSnapshot(profileDir));
29724
+ if (held?.eraseDeferred) return { erased, failed, remaining: held.intents.length, deferred: true };
29725
+ for (const intent of intents) {
29726
+ signal.throwIfAborted();
29727
+ try {
29728
+ await profileSection(async () => {
29729
+ if (marker === void 0) assertLogoutClear(options.logoutPendingFile);
29730
+ else {
29731
+ const pending = await readLogoutMarker(join2(profileDir, REPLICA_LOGOUT_PENDING_FILE));
29732
+ if (pending === null || pending.remotePending || JSON.stringify(pending) !== JSON.stringify(marker)) {
29733
+ throw gated("gate_unavailable");
29734
+ }
29735
+ assertApprovedInventory(await inspectLayout(true), {
29736
+ unknownEntries: marker.approvedUnknown,
29737
+ knownRoots: marker.knownRoots,
29738
+ knownEntries: marker.knownEntries,
29739
+ missingDatabases: marker.missingDatabases
29740
+ }, true);
29741
+ }
29742
+ const state = await gate.readGateSnapshot(profileDir);
29743
+ if (state?.eraseDeferred) return;
29744
+ if (!state?.intents.some((queued) => queued.id === intent.id)) return;
29745
+ let idDir;
29746
+ for (const dir of await identityDirs()) {
29747
+ const identity = identityRecordOf(await readJsonFile(join2(dir, "identity.json")));
29748
+ if (identity === void 0) throw new ReplicaStorageError("STORAGE_ERROR", `Missing identity at ${dir}.`);
29749
+ const hash3 = await gate.identityHashOf(state.salt, replicationIdentityKey(identity));
29750
+ if (intent.kind === "pending" ? hash3 === intent.identityKeyHash : intent.partitionId.startsWith(`${hash3}:`)) {
29751
+ idDir = dir;
29752
+ break;
29753
+ }
29754
+ }
29755
+ if (intent.kind === "pending") {
29756
+ if (intent.id !== `pending:${intent.identityKeyHash}`) throw gated("gate_unavailable");
29757
+ if (idDir !== void 0) {
29758
+ for (const name of ["pending.json", "device.jwk"]) {
29759
+ const file = join2(idDir, name);
29760
+ let info;
29761
+ try {
29762
+ info = await lstat(file, { bigint: true });
29763
+ } catch (error) {
29764
+ if (isNotFound(error)) continue;
29765
+ throw error;
29766
+ }
29767
+ if (!info.isFile()) throw gated("gate_unavailable");
29768
+ if (marker !== void 0) {
29769
+ const relative = `replication/${basename(idDir)}/${name}`;
29770
+ const expected = [...marker.knownEntries, ...marker.approvedUnknown].find(
29771
+ (item) => item.path === relative
29772
+ );
29773
+ if (expected?.kind !== "file" || expected.dev !== info.dev.toString() || expected.ino !== info.ino.toString()) throw localChangesRefusal([relative]);
29774
+ }
29775
+ await rm(file);
29776
+ }
29777
+ await fsyncDir(idDir);
29778
+ }
29779
+ } else {
29780
+ if (!/^[0-9a-f]{64}:[a-z2-7]{26}$/.test(intent.partitionId) || intent.id !== intent.partitionId) {
29781
+ throw gated("gate_unavailable");
29782
+ }
29783
+ if (idDir !== void 0) {
29784
+ const replicaDir = join2(idDir, "replicas", intent.partitionId.slice(65));
29785
+ let dirInfo;
29786
+ if (await realDirectoryExists(join2(idDir, "replicas"))) {
29787
+ try {
29788
+ dirInfo = await lstat(replicaDir, { bigint: true });
29789
+ } catch (error) {
29790
+ if (!isNotFound(error)) throw error;
29791
+ }
29792
+ }
29793
+ if (dirInfo !== void 0) {
29794
+ const relative = `replication/${basename(idDir)}/replicas/${basename(replicaDir)}`;
29795
+ const expectedRoot = marker?.knownRoots.find((item) => item.path === relative);
29796
+ if (!dirInfo.isDirectory() || marker !== void 0 && (expectedRoot?.dev !== dirInfo.dev.toString() || expectedRoot.ino !== dirInfo.ino.toString())) throw localChangesRefusal([relative]);
29797
+ const approvedUnknown = marker?.approvedUnknown.filter(
29798
+ (item) => item.path.startsWith(`${relative}/`)
29799
+ ).map(
29800
+ (item) => ({ ...item, path: item.path.slice(relative.length + 1) })
29801
+ );
29802
+ const expectedKnown = marker?.knownEntries.filter(
29803
+ (item) => item.path.startsWith(`${relative}/`)
29804
+ ).map(
29805
+ (item) => ({ ...item, path: item.path.slice(relative.length + 1) })
29806
+ );
29807
+ const sqlite = await import("@tinycloud/replica/sqlite");
29808
+ if (!await pathExists(join2(replicaDir, "replica.db"))) {
29809
+ const layout = await sqlite.inspectSqliteReplicaLayout(replicaDir, { allowAdapterFence: true });
29810
+ if (!layout.unknownPaths.includes("replica.db") || layout.unknownEntries.some((item) => item.path === "replica.db") || layout.unknownPaths.some((path) => path !== "replica.db" && !approvedUnknown?.some((item) => item.path === path))) {
29811
+ throw localChangesRefusal([relative]);
29812
+ }
29813
+ await sqlite.removeSqliteReplicaLayout(replicaDir, {
29814
+ approvedUnknown,
29815
+ expectedKnown: layout.knownEntries,
29816
+ expectedRoot,
29817
+ allowAdapterFence: true,
29818
+ allowMissingDatabase: true
29819
+ });
29820
+ } else {
29821
+ let store;
29822
+ try {
29823
+ store = await sqlite.SqliteReplicaStore.open(replicaDir, { create: false, guard: profileSection });
29824
+ const row = await store.open();
29825
+ if (row !== null) {
29826
+ const lease = await store.acquireSyncLease(SYNC_LEASE_TTL_MS);
29827
+ if (lease === null) throw gated("gate_unavailable");
29828
+ try {
29829
+ if (row.revoked === null) await store.reset(lease, "purged");
29830
+ let deletionKnown = expectedKnown;
29831
+ if (marker !== void 0) {
29832
+ const current = await sqlite.inspectSqliteReplicaLayout(replicaDir, {
29833
+ allowAdapterFence: true
29834
+ });
29835
+ if (current.rootIdentity.dev !== expectedRoot?.dev || current.rootIdentity.ino !== expectedRoot.ino) throw localChangesRefusal([relative]);
29836
+ const approved = new Map(expectedKnown?.map((entry) => [entry.path, entry]));
29837
+ for (const entry of current.knownEntries) {
29838
+ const prior = approved.get(entry.path);
29839
+ if (prior?.kind === entry.kind && prior.dev === entry.dev && prior.ino === entry.ino) continue;
29840
+ if (entry.kind !== "file" || !["replica.db-wal", "replica.db-shm", "replica.db-journal"].includes(entry.path)) {
29841
+ throw localChangesRefusal([`${relative}/${entry.path}`]);
29842
+ }
29843
+ }
29844
+ deletionKnown = current.knownEntries;
29845
+ }
29846
+ await store.destroy(lease, {
29847
+ approvedUnknown,
29848
+ expectedKnown: deletionKnown,
29849
+ expectedRoot,
29850
+ allowAdapterFence: true
29851
+ });
29852
+ } finally {
29853
+ await store.releaseLease(lease).catch(() => void 0);
29854
+ }
29855
+ } else {
29856
+ await sqlite.removeSqliteReplicaLayout(replicaDir, {
29857
+ approvedUnknown,
29858
+ expectedKnown,
29859
+ expectedRoot,
29860
+ allowAdapterFence: true
29861
+ });
29862
+ }
29863
+ } finally {
29864
+ await store?.close().catch(() => void 0);
29865
+ }
29866
+ }
29867
+ await fsyncDir(join2(idDir, "replicas"));
29868
+ }
29869
+ }
29870
+ }
29871
+ await gate.clearGateIntent(profileDir, intent.id);
29872
+ erased.push(intent.id);
29873
+ });
29874
+ } catch (error) {
29875
+ failed.push({ id: intent.id, code: typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : "STORAGE_ERROR" });
29876
+ }
29877
+ }
29878
+ return { erased, failed, remaining: (await lifecycle.pendingPurge()).length };
29879
+ }
29880
+ async function openHandle(spec) {
29881
+ const [replica, sqlite] = await Promise.all([
29882
+ // @ts-ignore — TS2307 when replica's dist is absent (CI builds node-sdk first).
29883
+ import("@tinycloud/replica"),
29884
+ // @ts-ignore — same as above.
29885
+ import("@tinycloud/replica/sqlite")
29886
+ ]);
29887
+ if (spec.stamp === void 0) throw gated("not_established");
29888
+ if (guard === void 0) throw gated("gate_unavailable");
29889
+ if (spec.grantPolicy === void 0) {
29890
+ throw new ReplicaStorageError("REPLICA_INVALID_ARGUMENT", "A replica grant policy is required.");
29891
+ }
29892
+ const gate = await fileGate();
29893
+ const snapshot = await gate.readGateSnapshot(profileDir);
29894
+ if (snapshot === void 0) throw gated("gate_unavailable");
29895
+ const identityKeyHash = await gate.identityHashOf(snapshot.salt, replicationIdentityKey(spec.identity));
29896
+ const resolvePolicy = async (at, recordFirst) => {
29897
+ const policy = spec.grantPolicy;
29898
+ if (!Number.isSafeInteger(policy.grantTtlMs) || policy.grantTtlMs <= 0 || policy.grantTtlMs > MAX_GRANT_TTL_MS2) {
29899
+ throw new TypeError("A replica grant requires a finite TTL not exceeding seven days.");
29900
+ }
29901
+ const verified = policy.consentAnchor;
29902
+ if (verified !== null && (!Number.isSafeInteger(verified.at) || verified.at < 0 || verified.source === "first_install")) {
29903
+ throw new ReplicaStorageError("CONSENT_EXPIRED", "The verified consent anchor is unavailable.");
29904
+ }
29905
+ const first = recordFirst ? await gate.recordFirstInstall(profileDir, identityKeyHash, at) : await gate.readFirstInstall(profileDir, identityKeyHash);
29906
+ const anchor = verified === null ? first === void 0 ? null : { at: first, source: "first_install" } : { at: Math.min(verified.at, at), source: verified.source };
29907
+ if (anchor === null) return null;
29908
+ const deadline = anchor.at + CONSENT_WINDOW_MS;
29909
+ if (!Number.isSafeInteger(anchor.at) || anchor.at < 0 || !Number.isSafeInteger(deadline)) {
29910
+ throw new ReplicaStorageError("CONSENT_EXPIRED", "The durable consent anchor is invalid.");
29911
+ }
29912
+ return { at, ttlMs: policy.grantTtlMs, consentDeadline: deadline, consentAnchor: anchor };
29913
+ };
29914
+ let partitionId;
29915
+ const storeGuard = (section) => guarded(spec.identity.principal, spec.stamp, spec.identity, partitionId, section);
29916
+ return guarded(spec.identity.principal, spec.stamp, spec.identity, void 0, async () => {
29917
+ if (requiresSecretsOptIn2(spec.space, spec.prefix) && spec.allowSecrets !== true) {
29918
+ throw new replica.ReplicaError(
29919
+ replica.ReplicaErrorCode.SECRETS_OPT_IN_REQUIRED,
29920
+ `Replicating ${spec.space}/kv/${spec.prefix} copies secret material to disk; pass allowSecrets to opt in.`
29921
+ );
29922
+ }
29923
+ const idDir = partitionDirOf(root, spec.identity);
29924
+ await ensurePartitionIdentity(idDir, spec.identity, options.dirSync);
29925
+ const createdDevice = spec.device ?? await loadOrCreateDevice(idDir, profileSection, createDevice, options.dirSync);
29926
+ const device = { ...createdDevice, did: principalOf(createdDevice.did) };
29927
+ const replicaHash = replicaHashOf(spec.prefix, device.did);
29928
+ partitionId = `${identityKeyHash}:${replicaHash}`;
29929
+ const replicaDir = join2(idDir, "replicas", replicaHash);
29930
+ const store = await sqlite.SqliteReplicaStore.open(replicaDir, {
29931
+ create: true,
29932
+ guard: storeGuard
29933
+ });
29934
+ let generation;
29935
+ try {
29936
+ try {
29937
+ await store.init({
29938
+ name: spec.prefix.replace(/\/+$/, "") || "replica",
29939
+ replicaId: replicaHash,
29940
+ host: spec.identity.host,
29941
+ space: spec.space,
29942
+ prefix: spec.prefix,
29943
+ deviceDid: device.did,
29944
+ allowSecrets: spec.allowSecrets,
29945
+ localReadPolicy: "whileGrantValid",
29946
+ retentionGrantCid: null
29947
+ });
29948
+ } catch (error) {
29949
+ if (!replica.isReplicaError(error, replica.ReplicaErrorCode.CONFIG_MISMATCH)) {
29950
+ throw error;
29951
+ }
29952
+ }
29953
+ const state = await store.open();
29954
+ if (state === null) {
29955
+ throw new replica.ReplicaError(
29956
+ replica.ReplicaErrorCode.NOT_FOUND,
29957
+ "The replica row vanished during init."
29958
+ );
29959
+ }
29960
+ const config = state.config;
29961
+ if (config.host !== spec.identity.host || config.space !== spec.space || config.prefix !== spec.prefix) {
29962
+ throw new replica.ReplicaError(
29963
+ replica.ReplicaErrorCode.CONFIG_MISMATCH,
29964
+ `This replica follows ${config.host} ${config.space}/kv/${config.prefix}, not ${spec.identity.host} ${spec.space}/kv/${spec.prefix}.`
29965
+ );
29966
+ }
29967
+ if (principalOf(config.deviceDid) !== principalOf(device.did)) {
29968
+ throw new replica.ReplicaError(
29969
+ replica.ReplicaErrorCode.CONFIG_MISMATCH,
29970
+ "The stored replica belongs to a different device key; purge the partition to rekey."
29971
+ );
29972
+ }
29973
+ await retainGrantInstalls(gate, snapshot, identityKeyHash, store);
29974
+ if (state.grant !== null || state.pendingGrant !== null) {
29975
+ const policy = await resolvePolicy(Date.now(), false);
29976
+ if (policy === null) throw gated("gate_unavailable");
29977
+ if (state.grant !== null) await store.recomputeGrantInstall(state.grant, policy);
29978
+ if (state.pendingGrant !== null) await store.recomputeGrantInstall(state.pendingGrant, policy);
29979
+ }
29980
+ generation = state.generation;
29981
+ } catch (error) {
29982
+ await store.close().catch(() => void 0);
29983
+ throw error;
29984
+ }
29985
+ let ino;
29986
+ try {
29987
+ ino = (await stat(join2(replicaDir, "replica.db"))).ino;
29988
+ } catch (error) {
29989
+ if (!isNotFound(error)) throw error;
29990
+ }
29991
+ return new SqliteReplicaHandle({
29992
+ spec,
29993
+ replicaId: replicaHash,
29994
+ device,
29995
+ store,
29996
+ replicaDir,
29997
+ replica,
29998
+ guard: storeGuard,
29999
+ read: (section, withFence) => readGated(spec, partitionId, store, replicaDir, generation, section, withFence),
30000
+ policy: resolvePolicy,
30001
+ recordGrantInstall: async (cid, at) => gate.recordGrantInstall(profileDir, await gate.grantInstallHashOf(snapshot.salt, identityKeyHash, cid), at),
30002
+ transportFor: options.transportFor,
30003
+ dirSync: options.dirSync,
30004
+ generation,
30005
+ ino
30006
+ });
27881
30007
  });
27882
30008
  }
27883
30009
  async function inspectStatus(spec, { signal }) {
30010
+ assertLogoutClear(options.logoutPendingFile);
27884
30011
  const checkAbort = () => {
27885
30012
  if (signal.aborted) throw signal.reason ?? Object.assign(new Error("Replica status inspection was aborted"), { code: "ABORTED" });
27886
30013
  };
@@ -27913,6 +30040,8 @@ function createSqliteReplicaStorage(options) {
27913
30040
  import("@tinycloud/replica/sqlite")
27914
30041
  ]);
27915
30042
  checkAbort();
30043
+ const syncedThroughEpoch = await readFence(replicaDir);
30044
+ checkAbort();
27916
30045
  const inspected = await sqlite.SqliteReplicaStore.inspect(replicaDir, { signal });
27917
30046
  checkAbort();
27918
30047
  if (inspected === null) return void 0;
@@ -27920,14 +30049,17 @@ function createSqliteReplicaStorage(options) {
27920
30049
  if (state.config.host !== spec.identity.host || state.config.space !== spec.space || state.config.prefix !== spec.prefix || principalOf(state.config.deviceDid) !== principalOf(device.did)) {
27921
30050
  throw new ReplicaStorageError("STORAGE_ERROR", `The replica at ${replicaDir} does not match the requested identity, space, prefix, or device.`);
27922
30051
  }
27923
- const syncedThroughEpoch = await readFence(replicaDir);
27924
30052
  checkAbort();
30053
+ assertLogoutClear(options.logoutPendingFile);
30054
+ const gateState = await (await fileGate()).readGateSnapshot(profileDir);
30055
+ const purgePending = gateState !== void 0 && (gateState.eraseDeferred || gateState.intents.length > 0);
27925
30056
  return {
30057
+ purgePending,
27926
30058
  coverage: status.coverage,
27927
30059
  lastSyncAt: status.lastSyncAt,
27928
30060
  syncedThroughEpoch,
27929
- authority: { state: status.authority.state, expiresAt: status.authority.expiresAt },
27930
- grant: newestGrantInfo(replica, state.pendingGrant ?? state.grant, state.pendingGrant, spec),
30061
+ authority: state.grant !== null && (inspected.activeGrantInstall === null || Date.now() >= inspected.activeGrantInstall.effectiveExpiresAt) ? { state: "expired", expiresAt: inspected.activeGrantInstall === null ? null : new Date(inspected.activeGrantInstall.effectiveExpiresAt).toISOString() } : { state: status.authority.state, expiresAt: status.authority.expiresAt },
30062
+ grant: newestGrantInfo(replica, state.pendingGrant ?? state.grant, state.pendingGrant, spec, inspected.grantInstall),
27931
30063
  counts: status.counts,
27932
30064
  bytes: status.bytes,
27933
30065
  lastError: status.lastError
@@ -27940,89 +30072,124 @@ function createSqliteReplicaStorage(options) {
27940
30072
  // @ts-ignore — same as above.
27941
30073
  import("@tinycloud/replica/sqlite")
27942
30074
  ]);
27943
- const idDir = partitionDirOf(root, target.identity);
27944
- if (!await pathExists(idDir)) return;
27945
- const deviceDids = [];
27946
- const deviceFile = join2(idDir, "device.jwk");
27947
- const stored = await readJsonFile(deviceFile);
27948
- if (stored !== void 0) {
27949
- const device = deviceRecordOf(stored);
27950
- if (device === void 0) {
27951
- throw new ReplicaStorageError(
27952
- "STORAGE_ERROR",
27953
- `The replica device key at ${deviceFile} is unreadable.`
27954
- );
27955
- }
27956
- deviceDids.push(device.did);
27957
- }
27958
- if (target.sessionDeviceDid !== void 0) deviceDids.push(target.sessionDeviceDid);
27959
- const devices = [...new Map(deviceDids.map((did) => [principalOf(did), did])).values()];
27960
- let firstError;
27961
- for (const deviceDid of devices) {
27962
- const replicaDir = join2(idDir, "replicas", replicaHashOf(target.prefix, deviceDid));
27963
- if (!await pathExists(replicaDir)) continue;
27964
- try {
27965
- let store;
27966
- try {
27967
- store = await sqlite.SqliteReplicaStore.open(replicaDir, { create: false, guard });
27968
- } catch (error) {
27969
- if (replica.isReplicaError(error, replica.ReplicaErrorCode.NOT_FOUND)) {
27970
- await guard(() => rm(replicaDir, { recursive: true, force: true }));
27971
- continue;
27972
- }
27973
- throw error;
30075
+ return profileSection(async () => {
30076
+ const idDir = partitionDirOf(root, target.identity);
30077
+ if (!await realDirectoryExists(idDir)) return;
30078
+ const hasReplicas = await realDirectoryExists(join2(idDir, "replicas"));
30079
+ const deviceDids = [];
30080
+ const deviceFile = join2(idDir, "device.jwk");
30081
+ const stored = await readJsonFile(deviceFile);
30082
+ if (stored !== void 0) {
30083
+ const device = deviceRecordOf(stored);
30084
+ if (device === void 0) {
30085
+ throw new ReplicaStorageError(
30086
+ "STORAGE_ERROR",
30087
+ `The replica device key at ${deviceFile} is unreadable.`
30088
+ );
30089
+ }
30090
+ deviceDids.push(device.did);
30091
+ }
30092
+ if (target.sessionDeviceDid !== void 0) deviceDids.push(target.sessionDeviceDid);
30093
+ const devices = [...new Map(deviceDids.map((did) => [principalOf(did), did])).values()];
30094
+ const removeLeftover = async (replicaDir) => {
30095
+ const layout = await sqlite.inspectSqliteReplicaLayout(replicaDir, { allowAdapterFence: true });
30096
+ const missingDatabase = !await pathExists(join2(replicaDir, "replica.db"));
30097
+ const unexpected = layout.unknownPaths.filter((path) => path !== "replica.db");
30098
+ if (unexpected.length !== 0 || layout.unknownPaths.includes("replica.db") !== missingDatabase || layout.unknownEntries.some((item) => item.path === "replica.db")) {
30099
+ throw new ReplicaStorageError(
30100
+ "STORAGE_ERROR",
30101
+ `Refusing to remove unexpected replica path ${join2(replicaDir, unexpected[0] ?? "replica.db")}.`
30102
+ );
27974
30103
  }
30104
+ await sqlite.removeSqliteReplicaLayout(replicaDir, {
30105
+ expectedRoot: layout.rootIdentity,
30106
+ expectedKnown: layout.knownEntries,
30107
+ allowAdapterFence: true,
30108
+ allowMissingDatabase: missingDatabase
30109
+ });
30110
+ };
30111
+ let firstError;
30112
+ for (const deviceDid of devices) {
30113
+ const replicaDir = join2(idDir, "replicas", replicaHashOf(target.prefix, deviceDid));
30114
+ if (!hasReplicas || !await realDirectoryExists(replicaDir)) continue;
27975
30115
  try {
27976
- const state = await store.open();
27977
- if (state === null) {
27978
- await store.close().catch(() => void 0);
27979
- await guard(() => rm(replicaDir, { recursive: true, force: true }));
27980
- continue;
27981
- }
27982
- const lease = await store.acquireSyncLease(SYNC_LEASE_TTL_MS);
27983
- if (lease === null) {
27984
- throw new replica.ReplicaError(
27985
- replica.ReplicaErrorCode.BUSY,
27986
- "Another process holds this replica's sync lease."
27987
- );
30116
+ let store;
30117
+ try {
30118
+ store = await sqlite.SqliteReplicaStore.open(replicaDir, { create: false, guard: profileSection });
30119
+ } catch (error) {
30120
+ if (replica.isReplicaError(error, replica.ReplicaErrorCode.NOT_FOUND)) {
30121
+ await removeLeftover(replicaDir);
30122
+ continue;
30123
+ }
30124
+ throw error;
27988
30125
  }
27989
30126
  try {
27990
- if (state.revoked === null) {
27991
- await store.reset(lease, "purged");
30127
+ const state = await store.open();
30128
+ if (state === null) {
30129
+ await store.close().catch(() => void 0);
30130
+ await removeLeftover(replicaDir);
30131
+ continue;
30132
+ }
30133
+ const lease = await store.acquireSyncLease(SYNC_LEASE_TTL_MS);
30134
+ if (lease === null) {
30135
+ throw new replica.ReplicaError(
30136
+ replica.ReplicaErrorCode.BUSY,
30137
+ "Another process holds this replica's sync lease."
30138
+ );
30139
+ }
30140
+ try {
30141
+ const gate = await fileGate();
30142
+ const currentGate = await gate.readGateSnapshot(profileDir);
30143
+ if (currentGate !== void 0 && currentGate.principalHash !== null && currentGate.principalHash === await gate.principalHashOf(currentGate.salt, target.identity.principal)) {
30144
+ const identityKeyHash = await gate.identityHashOf(currentGate.salt, replicationIdentityKey(target.identity));
30145
+ await retainGrantInstalls(gate, currentGate, identityKeyHash, store);
30146
+ }
30147
+ if (state.revoked === null) {
30148
+ await store.reset(lease, "purged");
30149
+ }
30150
+ await store.destroy(lease, { allowAdapterFence: true });
30151
+ } finally {
30152
+ await store.releaseLease(lease).catch(() => void 0);
27992
30153
  }
27993
- await store.destroy(lease);
27994
30154
  } finally {
27995
- await store.releaseLease(lease).catch(() => void 0);
30155
+ await store.close().catch(() => void 0);
27996
30156
  }
27997
- } finally {
27998
- await store.close().catch(() => void 0);
30157
+ } catch (error) {
30158
+ firstError ?? (firstError = error);
27999
30159
  }
28000
- } catch (error) {
28001
- firstError ?? (firstError = error);
28002
30160
  }
28003
- }
28004
- if (firstError !== void 0) throw firstError;
30161
+ if (firstError !== void 0) throw firstError;
30162
+ });
28005
30163
  }
28006
30164
  return {
28007
30165
  kind: "sqlite",
30166
+ lifecycle,
28008
30167
  open: openHandle,
28009
30168
  inspectStatus,
28010
30169
  purge: purgePartition,
28011
- // Always the durable partitioned file store (review): the runtime
28012
- // must never fall back to volatile in-memory pending state — a
28013
- // restart that lost ambiguous write records would serve stale data.
28014
- // Without a guard the file store still durably serializes each update;
28015
- // the guard only adds cross-process read-modify-write exclusion.
28016
- pendingWrites(identity) {
30170
+ // File-backed pending writes are always bound to a stamped gate.
30171
+ pendingWrites(identity, opts) {
28017
30172
  return new FilePendingWriteStore(
28018
30173
  identity,
28019
30174
  partitionDirOf(root, identity),
30175
+ profileDir,
28020
30176
  guard,
28021
- options.dirSync
30177
+ opts?.stamp,
30178
+ options.dirSync,
30179
+ options.logoutPendingFile
28022
30180
  );
28023
30181
  }
28024
30182
  };
28025
30183
  }
30184
+ function createProfileReplicaPurge(options) {
30185
+ return createSqliteReplicaStorage({
30186
+ dir: join2(options.profileDir, "replication"),
30187
+ guard: options.guard,
30188
+ dirSync: options.dirSync,
30189
+ logoutPendingFile: join2(options.profileDir, REPLICA_LOGOUT_PENDING_FILE),
30190
+ diagnosticFiles: options.diagnosticFiles
30191
+ }).lifecycle;
30192
+ }
28026
30193
  function sqliteReplicaStorage(options) {
28027
30194
  return createSqliteReplicaStorage(options);
28028
30195
  }
@@ -28074,7 +30241,9 @@ export {
28074
30241
  PermissionNotInManifestError2 as PermissionNotInManifestError,
28075
30242
  PrefixedKVService,
28076
30243
  PrivateKeySigner,
28077
- ProtocolMismatchError,
30244
+ ProtocolMismatchError2 as ProtocolMismatchError,
30245
+ REPLICA_LOGOUT_PENDING_FILE,
30246
+ RESTORED_GATE_MESSAGE,
28078
30247
  RUNTIME_DELEGATION_REJECTIONS,
28079
30248
  RuntimeDelegationRejectedError,
28080
30249
  SECRET_NAME_RE,
@@ -28101,12 +30270,13 @@ export {
28101
30270
  VAULT_PERMISSION_SERVICE,
28102
30271
  VaultHeaders,
28103
30272
  VaultPublicSpaceKVActions,
28104
- VersionCheckError2 as VersionCheckError,
30273
+ VersionCheckError,
28105
30274
  WasmKeyProvider,
28106
30275
  activateCompactRuntimeDelegation,
28107
30276
  activateValidatedRuntimeDelegation,
28108
30277
  addressStorageKey,
28109
30278
  appSecretScope,
30279
+ assertProfileLogoutClear,
28110
30280
  buildCanonicalDecryptRequest,
28111
30281
  buildDecryptAttenuation,
28112
30282
  buildDecryptFacts,
@@ -28127,6 +30297,7 @@ export {
28127
30297
  composeManifestRequest2 as composeManifestRequest,
28128
30298
  createCapabilityKeyRegistry,
28129
30299
  createOpenKeyCallbackSigningStrategy,
30300
+ createProfileReplicaPurge,
28130
30301
  createSharingService,
28131
30302
  createSpaceService,
28132
30303
  createVaultCrypto2 as createVaultCrypto,
@@ -28163,6 +30334,7 @@ export {
28163
30334
  isNetworkId,
28164
30335
  isStorageFullError2 as isStorageFullError,
28165
30336
  loadManifest,
30337
+ logoutPrincipalDigest,
28166
30338
  makePkhSpaceId2 as makePkhSpaceId,
28167
30339
  makePublicSpaceId2 as makePublicSpaceId,
28168
30340
  networkDiscoveryKey,