@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.cjs CHANGED
@@ -4408,11 +4408,11 @@ var init_lib4 = __esm({
4408
4408
  init_version4();
4409
4409
  __awaiter = function(thisArg, _arguments, P, generator) {
4410
4410
  function adopt(value) {
4411
- return value instanceof P ? value : new P(function(resolve) {
4412
- resolve(value);
4411
+ return value instanceof P ? value : new P(function(resolve2) {
4412
+ resolve2(value);
4413
4413
  });
4414
4414
  }
4415
- return new (P || (P = Promise))(function(resolve, reject2) {
4415
+ return new (P || (P = Promise))(function(resolve2, reject2) {
4416
4416
  function fulfilled(value) {
4417
4417
  try {
4418
4418
  step(generator.next(value));
@@ -4428,7 +4428,7 @@ var init_lib4 = __esm({
4428
4428
  }
4429
4429
  }
4430
4430
  function step(result) {
4431
- result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
4431
+ result.done ? resolve2(result.value) : adopt(result.value).then(fulfilled, rejected);
4432
4432
  }
4433
4433
  step((generator = generator.apply(thisArg, _arguments || [])).next());
4434
4434
  });
@@ -8044,11 +8044,11 @@ var init_typed_data = __esm({
8044
8044
  init_id();
8045
8045
  __awaiter2 = function(thisArg, _arguments, P, generator) {
8046
8046
  function adopt(value) {
8047
- return value instanceof P ? value : new P(function(resolve) {
8048
- resolve(value);
8047
+ return value instanceof P ? value : new P(function(resolve2) {
8048
+ resolve2(value);
8049
8049
  });
8050
8050
  }
8051
- return new (P || (P = Promise))(function(resolve, reject2) {
8051
+ return new (P || (P = Promise))(function(resolve2, reject2) {
8052
8052
  function fulfilled(value) {
8053
8053
  try {
8054
8054
  step(generator.next(value));
@@ -8064,7 +8064,7 @@ var init_typed_data = __esm({
8064
8064
  }
8065
8065
  }
8066
8066
  function step(result) {
8067
- result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
8067
+ result.done ? resolve2(result.value) : adopt(result.value).then(fulfilled, rejected);
8068
8068
  }
8069
8069
  step((generator = generator.apply(thisArg, _arguments || [])).next());
8070
8070
  });
@@ -14906,7 +14906,7 @@ var require_scrypt = __commonJS({
14906
14906
  }
14907
14907
  const lib = {
14908
14908
  scrypt: function(password, salt, N2, r2, p, dkLen, progressCallback) {
14909
- return new Promise(function(resolve, reject2) {
14909
+ return new Promise(function(resolve2, reject2) {
14910
14910
  let lastProgress = 0;
14911
14911
  if (progressCallback) {
14912
14912
  progressCallback(0);
@@ -14918,7 +14918,7 @@ var require_scrypt = __commonJS({
14918
14918
  if (progressCallback && lastProgress !== 1) {
14919
14919
  progressCallback(1);
14920
14920
  }
14921
- resolve(new Uint8Array(key2));
14921
+ resolve2(new Uint8Array(key2));
14922
14922
  } else if (progressCallback && progress !== lastProgress) {
14923
14923
  lastProgress = progress;
14924
14924
  return progressCallback(progress);
@@ -15298,11 +15298,11 @@ var init_keystore = __esm({
15298
15298
  init_version15();
15299
15299
  __awaiter3 = function(thisArg, _arguments, P, generator) {
15300
15300
  function adopt(value) {
15301
- return value instanceof P ? value : new P(function(resolve) {
15302
- resolve(value);
15301
+ return value instanceof P ? value : new P(function(resolve2) {
15302
+ resolve2(value);
15303
15303
  });
15304
15304
  }
15305
- return new (P || (P = Promise))(function(resolve, reject2) {
15305
+ return new (P || (P = Promise))(function(resolve2, reject2) {
15306
15306
  function fulfilled(value) {
15307
15307
  try {
15308
15308
  step(generator.next(value));
@@ -15318,7 +15318,7 @@ var init_keystore = __esm({
15318
15318
  }
15319
15319
  }
15320
15320
  function step(result) {
15321
- result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
15321
+ result.done ? resolve2(result.value) : adopt(result.value).then(fulfilled, rejected);
15322
15322
  }
15323
15323
  step((generator = generator.apply(thisArg, _arguments || [])).next());
15324
15324
  });
@@ -15611,11 +15611,11 @@ var init_lib25 = __esm({
15611
15611
  init_version19();
15612
15612
  __awaiter4 = function(thisArg, _arguments, P, generator) {
15613
15613
  function adopt(value) {
15614
- return value instanceof P ? value : new P(function(resolve) {
15615
- resolve(value);
15614
+ return value instanceof P ? value : new P(function(resolve2) {
15615
+ resolve2(value);
15616
15616
  });
15617
15617
  }
15618
- return new (P || (P = Promise))(function(resolve, reject2) {
15618
+ return new (P || (P = Promise))(function(resolve2, reject2) {
15619
15619
  function fulfilled(value) {
15620
15620
  try {
15621
15621
  step(generator.next(value));
@@ -15631,7 +15631,7 @@ var init_lib25 = __esm({
15631
15631
  }
15632
15632
  }
15633
15633
  function step(result) {
15634
- result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
15634
+ result.done ? resolve2(result.value) : adopt(result.value).then(fulfilled, rejected);
15635
15635
  }
15636
15636
  step((generator = generator.apply(thisArg, _arguments || [])).next());
15637
15637
  });
@@ -15693,11 +15693,11 @@ var init_lib26 = __esm({
15693
15693
  init_version20();
15694
15694
  __awaiter5 = function(thisArg, _arguments, P, generator) {
15695
15695
  function adopt(value) {
15696
- return value instanceof P ? value : new P(function(resolve) {
15697
- resolve(value);
15696
+ return value instanceof P ? value : new P(function(resolve2) {
15697
+ resolve2(value);
15698
15698
  });
15699
15699
  }
15700
- return new (P || (P = Promise))(function(resolve, reject2) {
15700
+ return new (P || (P = Promise))(function(resolve2, reject2) {
15701
15701
  function fulfilled(value) {
15702
15702
  try {
15703
15703
  step(generator.next(value));
@@ -15713,7 +15713,7 @@ var init_lib26 = __esm({
15713
15713
  }
15714
15714
  }
15715
15715
  function step(result) {
15716
- result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
15716
+ result.done ? resolve2(result.value) : adopt(result.value).then(fulfilled, rejected);
15717
15717
  }
15718
15718
  step((generator = generator.apply(thisArg, _arguments || [])).next());
15719
15719
  });
@@ -16015,11 +16015,11 @@ var init_lib27 = __esm({
16015
16015
  init_version21();
16016
16016
  __awaiter6 = function(thisArg, _arguments, P, generator) {
16017
16017
  function adopt(value) {
16018
- return value instanceof P ? value : new P(function(resolve) {
16019
- resolve(value);
16018
+ return value instanceof P ? value : new P(function(resolve2) {
16019
+ resolve2(value);
16020
16020
  });
16021
16021
  }
16022
- return new (P || (P = Promise))(function(resolve, reject2) {
16022
+ return new (P || (P = Promise))(function(resolve2, reject2) {
16023
16023
  function fulfilled(value) {
16024
16024
  try {
16025
16025
  step(generator.next(value));
@@ -16035,7 +16035,7 @@ var init_lib27 = __esm({
16035
16035
  }
16036
16036
  }
16037
16037
  function step(result) {
16038
- result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
16038
+ result.done ? resolve2(result.value) : adopt(result.value).then(fulfilled, rejected);
16039
16039
  }
16040
16040
  step((generator = generator.apply(thisArg, _arguments || [])).next());
16041
16041
  });
@@ -16253,11 +16253,11 @@ var init_geturl = __esm({
16253
16253
  init_lib2();
16254
16254
  __awaiter7 = function(thisArg, _arguments, P, generator) {
16255
16255
  function adopt(value) {
16256
- return value instanceof P ? value : new P(function(resolve) {
16257
- resolve(value);
16256
+ return value instanceof P ? value : new P(function(resolve2) {
16257
+ resolve2(value);
16258
16258
  });
16259
16259
  }
16260
- return new (P || (P = Promise))(function(resolve, reject2) {
16260
+ return new (P || (P = Promise))(function(resolve2, reject2) {
16261
16261
  function fulfilled(value) {
16262
16262
  try {
16263
16263
  step(generator.next(value));
@@ -16273,7 +16273,7 @@ var init_geturl = __esm({
16273
16273
  }
16274
16274
  }
16275
16275
  function step(result) {
16276
- result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
16276
+ result.done ? resolve2(result.value) : adopt(result.value).then(fulfilled, rejected);
16277
16277
  }
16278
16278
  step((generator = generator.apply(thisArg, _arguments || [])).next());
16279
16279
  });
@@ -16289,8 +16289,8 @@ __export(lib_exports21, {
16289
16289
  poll: () => poll
16290
16290
  });
16291
16291
  function staller(duration) {
16292
- return new Promise((resolve) => {
16293
- setTimeout(resolve, duration);
16292
+ return new Promise((resolve2) => {
16293
+ setTimeout(resolve2, duration);
16294
16294
  });
16295
16295
  }
16296
16296
  function bodyify(value, type) {
@@ -16410,7 +16410,7 @@ function _fetchData(connection, body, processFunc) {
16410
16410
  options.headers = flatHeaders;
16411
16411
  const runningTimeout = (function() {
16412
16412
  let timer = null;
16413
- const promise = new Promise(function(resolve, reject2) {
16413
+ const promise = new Promise(function(resolve2, reject2) {
16414
16414
  if (timeout) {
16415
16415
  timer = setTimeout(() => {
16416
16416
  if (timer == null) {
@@ -16580,7 +16580,7 @@ function poll(func, options) {
16580
16580
  if (options.interval == null) {
16581
16581
  options.interval = 250;
16582
16582
  }
16583
- return new Promise(function(resolve, reject2) {
16583
+ return new Promise(function(resolve2, reject2) {
16584
16584
  let timer = null;
16585
16585
  let done = false;
16586
16586
  const cancel = () => {
@@ -16606,7 +16606,7 @@ function poll(func, options) {
16606
16606
  return func().then(function(result) {
16607
16607
  if (result !== void 0) {
16608
16608
  if (cancel()) {
16609
- resolve(result);
16609
+ resolve2(result);
16610
16610
  }
16611
16611
  } else if (options.oncePoll) {
16612
16612
  options.oncePoll.once("poll", check);
@@ -16652,11 +16652,11 @@ var init_lib28 = __esm({
16652
16652
  init_geturl();
16653
16653
  __awaiter8 = function(thisArg, _arguments, P, generator) {
16654
16654
  function adopt(value) {
16655
- return value instanceof P ? value : new P(function(resolve) {
16656
- resolve(value);
16655
+ return value instanceof P ? value : new P(function(resolve2) {
16656
+ resolve2(value);
16657
16657
  });
16658
16658
  }
16659
- return new (P || (P = Promise))(function(resolve, reject2) {
16659
+ return new (P || (P = Promise))(function(resolve2, reject2) {
16660
16660
  function fulfilled(value) {
16661
16661
  try {
16662
16662
  step(generator.next(value));
@@ -16672,7 +16672,7 @@ var init_lib28 = __esm({
16672
16672
  }
16673
16673
  }
16674
16674
  function step(result) {
16675
- result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
16675
+ result.done ? resolve2(result.value) : adopt(result.value).then(fulfilled, rejected);
16676
16676
  }
16677
16677
  step((generator = generator.apply(thisArg, _arguments || [])).next());
16678
16678
  });
@@ -17030,45 +17030,6 @@ var require_utils2 = __commonJS({
17030
17030
  }
17031
17031
  });
17032
17032
 
17033
- // src/replication/config.ts
17034
- function assertValidReplicationConfig(replication) {
17035
- if (replication === void 0 || replication.enabled !== true) return;
17036
- if (replication.storage === void 0 || replication.storage === null) {
17037
- throw new TypeError("replication.enabled requires replication.storage");
17038
- }
17039
- if (!Array.isArray(replication.prefixes) || replication.prefixes.length === 0) {
17040
- throw new TypeError("replication.prefixes must be a non-empty array");
17041
- }
17042
- for (const prefix of replication.prefixes) {
17043
- try {
17044
- (0, import_prefix.assertFolderPrefix)(prefix);
17045
- } catch (error) {
17046
- throw new TypeError(error instanceof Error ? error.message : String(error));
17047
- }
17048
- }
17049
- for (const [index, prefix] of replication.prefixes.entries()) {
17050
- for (const other of replication.prefixes.slice(index + 1)) {
17051
- if ((0, import_prefix.kvPrefixCovers)(prefix, other) || (0, import_prefix.kvPrefixCovers)(other, prefix)) {
17052
- throw new TypeError(
17053
- `replication.prefixes must not overlap: ${JSON.stringify(prefix)} and ${JSON.stringify(other)}`
17054
- );
17055
- }
17056
- }
17057
- if ((prefix === "/" || prefix.split("/", 1)[0] === "vault") && replication.allowSecrets !== true) {
17058
- throw new TypeError(
17059
- `replication prefix ${JSON.stringify(prefix)} overlaps the vault namespace; pass allowSecrets to opt in`
17060
- );
17061
- }
17062
- }
17063
- }
17064
- var import_prefix;
17065
- var init_config = __esm({
17066
- "src/replication/config.ts"() {
17067
- "use strict";
17068
- import_prefix = require("@tinycloud/sdk-services/kv/replication/prefix");
17069
- }
17070
- });
17071
-
17072
17033
  // src/replication/authority-sign-in.ts
17073
17034
  function hasUnrestrictedGetCoverage(entries, spaceId, prefix) {
17074
17035
  return (0, import_sdk_core2.isCapabilitySubset)(
@@ -17082,7 +17043,7 @@ function augmentSignInEntriesWithReplication(input) {
17082
17043
  if (replication === void 0) return out;
17083
17044
  const eligiblePaths = [];
17084
17045
  for (const prefix of replication.prefixes) {
17085
- (0, import_prefix2.assertFolderPrefix)(prefix);
17046
+ (0, import_prefix.assertFolderPrefix)(prefix);
17086
17047
  const path = prefix === "/" ? "" : prefix;
17087
17048
  if ((0, import_sdk_services.requiresSecretsOptIn)(input.primarySpaceId, prefix) && replication.allowSecrets !== true) {
17088
17049
  continue;
@@ -17110,19 +17071,63 @@ function augmentSignInEntriesWithReplication(input) {
17110
17071
  }
17111
17072
  return out;
17112
17073
  }
17113
- var import_sdk_core2, import_sdk_services, import_prefix2, KV_GET, KV_SYNC, KV_SERVICE;
17074
+ var import_sdk_core2, import_sdk_services, import_prefix, KV_GET, KV_SYNC, KV_SERVICE;
17114
17075
  var init_authority_sign_in = __esm({
17115
17076
  "src/replication/authority-sign-in.ts"() {
17116
17077
  "use strict";
17117
17078
  import_sdk_core2 = require("@tinycloud/sdk-core");
17118
17079
  import_sdk_services = require("@tinycloud/sdk-services");
17119
- import_prefix2 = require("@tinycloud/sdk-services/kv/replication/prefix");
17080
+ import_prefix = require("@tinycloud/sdk-services/kv/replication/prefix");
17120
17081
  KV_GET = import_sdk_core2.KV.GET;
17121
17082
  KV_SYNC = import_sdk_core2.KV.SYNC;
17122
17083
  KV_SERVICE = "tinycloud.kv";
17123
17084
  }
17124
17085
  });
17125
17086
 
17087
+ // src/replication/config.ts
17088
+ function assertValidReplicationConfig(replication) {
17089
+ if (replication === void 0 || !replication.enabled) return;
17090
+ if (replication.storage === void 0 || replication.storage === null) {
17091
+ throw new TypeError("replication.enabled requires replication.storage");
17092
+ }
17093
+ if (!Array.isArray(replication.prefixes) || replication.prefixes.length === 0) {
17094
+ throw new TypeError("replication.prefixes must be a non-empty array");
17095
+ }
17096
+ const grantTtlMs = replication.grantTtlMs;
17097
+ if (grantTtlMs !== void 0 && (!Number.isFinite(grantTtlMs) || grantTtlMs <= 0 || grantTtlMs > import_grantPolicyConstants.MAX_GRANT_TTL_MS)) {
17098
+ throw new TypeError(`replication.grantTtlMs must be a positive number of milliseconds no greater than ${import_grantPolicyConstants.MAX_GRANT_TTL_MS}`);
17099
+ }
17100
+ for (const prefix of replication.prefixes) {
17101
+ try {
17102
+ (0, import_prefix2.assertFolderPrefix)(prefix);
17103
+ } catch (error) {
17104
+ throw new TypeError(error instanceof Error ? error.message : String(error));
17105
+ }
17106
+ }
17107
+ for (const [index, prefix] of replication.prefixes.entries()) {
17108
+ for (const other of replication.prefixes.slice(index + 1)) {
17109
+ if ((0, import_prefix2.kvPrefixCovers)(prefix, other) || (0, import_prefix2.kvPrefixCovers)(other, prefix)) {
17110
+ throw new TypeError(
17111
+ `replication.prefixes must not overlap: ${JSON.stringify(prefix)} and ${JSON.stringify(other)}`
17112
+ );
17113
+ }
17114
+ }
17115
+ if ((prefix === "/" || prefix.split("/", 1)[0] === "vault") && replication.allowSecrets !== true) {
17116
+ throw new TypeError(
17117
+ `replication prefix ${JSON.stringify(prefix)} overlaps the vault namespace; pass allowSecrets to opt in`
17118
+ );
17119
+ }
17120
+ }
17121
+ }
17122
+ var import_grantPolicyConstants, import_prefix2;
17123
+ var init_config = __esm({
17124
+ "src/replication/config.ts"() {
17125
+ "use strict";
17126
+ import_grantPolicyConstants = require("@tinycloud/sdk-services/kv/replication/grantPolicyConstants");
17127
+ import_prefix2 = require("@tinycloud/sdk-services/kv/replication/prefix");
17128
+ }
17129
+ });
17130
+
17126
17131
  // src/replication/runtime.ts
17127
17132
  var runtime_exports = {};
17128
17133
  __export(runtime_exports, {
@@ -17166,10 +17171,23 @@ var init_runtime = __esm({
17166
17171
  this.control = {
17167
17172
  status: async () => {
17168
17173
  await this.binding;
17174
+ if (this.gatedReason) return this.options.prefixes.map((prefix) => ({
17175
+ prefix,
17176
+ state: "gated",
17177
+ gate: this.gatedReason,
17178
+ errorCode: "REPLICA_GATED",
17179
+ pending: { inFlight: 0, committed: 0, ambiguous: 0 },
17180
+ pinned: [],
17181
+ lagMs: null
17182
+ }));
17169
17183
  return this.current ? this.current.status() : [];
17170
17184
  },
17171
17185
  sync: async (input) => {
17172
17186
  await this.binding;
17187
+ if (this.gatedReason) throw Object.assign(new Error("Replica restore is gated."), {
17188
+ code: "REPLICA_GATED",
17189
+ gate: this.gatedReason
17190
+ });
17173
17191
  if (this.purgedGeneration === this.generation) return;
17174
17192
  if (this.current) await this.current.sync(input);
17175
17193
  },
@@ -17217,11 +17235,59 @@ var init_runtime = __esm({
17217
17235
  for (const kv of this.attached.keys()) kv.setReadThrough(null);
17218
17236
  this.attached.clear();
17219
17237
  }
17238
+ /** Observe a refused restore without opening replica state; loader failure leaves raw network KV usable. */
17239
+ bindGatedNetworkOnly(input, loadGatedNetwork) {
17240
+ if (this.closed || !this.options.enabled) return Promise.resolve();
17241
+ const retired = this.unbind();
17242
+ const generation = this.generation;
17243
+ this.purgedGeneration = void 0;
17244
+ this.pendingPurge = void 0;
17245
+ this.pendingPurgeResult = void 0;
17246
+ this.pending.clear();
17247
+ this.lastController = void 0;
17248
+ this.lastPending = void 0;
17249
+ this.lastIdentity = void 0;
17250
+ this.gatedReason = input.gate;
17251
+ this.primarySpace = input.session.spaceId;
17252
+ this.binding = retired.then(async () => {
17253
+ let create;
17254
+ try {
17255
+ ({ createGatedNetworkReadThrough: create } = await loadGatedNetwork());
17256
+ } catch {
17257
+ return;
17258
+ }
17259
+ if (generation !== this.generation || this.closed || typeof create !== "function") return;
17260
+ const networkOnly = create({
17261
+ space: input.session.spaceId,
17262
+ prefixes: this.options.prefixes,
17263
+ now: () => this.clock.now(),
17264
+ emit: (event) => {
17265
+ if (generation !== this.generation || this.closed || this.gated !== networkOnly) return;
17266
+ try {
17267
+ this.options.onEvent?.(event);
17268
+ } catch {
17269
+ }
17270
+ const { type, ...data } = event;
17271
+ try {
17272
+ input.context.emit(type, data);
17273
+ } catch {
17274
+ }
17275
+ }
17276
+ });
17277
+ this.gated = networkOnly;
17278
+ for (const scoped of input.primaryKV) this.attach(scoped.space, scoped.kv);
17279
+ });
17280
+ return this.binding;
17281
+ }
17220
17282
  bind(input) {
17221
17283
  if (this.closed || !this.options.enabled) return Promise.resolve();
17284
+ this.gated = void 0;
17285
+ this.gatedReason = void 0;
17222
17286
  this.bindingActive = true;
17223
17287
  const generation = ++this.generation;
17224
17288
  this.purgedGeneration = void 0;
17289
+ this.unsubscribeConsentExpired?.();
17290
+ this.unsubscribeConsentExpired = void 0;
17225
17291
  const { context, session } = input;
17226
17292
  const emit = (event) => {
17227
17293
  try {
@@ -17254,11 +17320,17 @@ var init_runtime = __esm({
17254
17320
  return;
17255
17321
  }
17256
17322
  const key2 = services.replicationIdentityKey(identity);
17257
- let pending = this.pending.get(key2);
17258
- if (!pending) {
17259
- pending = this.options.storage.pendingWrites?.(identity) ?? services.createMemoryPendingStore(identity);
17260
- this.pending.set(key2, pending);
17323
+ let cached = this.pending.get(key2);
17324
+ const stamp = input.stamp;
17325
+ if (!cached || cached.stamp?.incarnation !== stamp?.incarnation || cached.stamp?.lifecycleEpoch !== stamp?.lifecycleEpoch) {
17326
+ const store = this.options.storage.pendingWrites?.(identity, { stamp });
17327
+ if (!store && this.options.storage.lifecycle) {
17328
+ throw Object.assign(new Error("Gated replication requires a stamped pending store"), { code: "REPLICA_GATED", gate: "gate_unavailable" });
17329
+ }
17330
+ cached = { stamp, store: store ?? services.createMemoryPendingStore(identity) };
17331
+ this.pending.set(key2, cached);
17261
17332
  }
17333
+ const pending = cached.store;
17262
17334
  this.lastPending = pending;
17263
17335
  const controller = services.createKVReplication({
17264
17336
  options: resolvedOptions(this.options),
@@ -17266,6 +17338,7 @@ var init_runtime = __esm({
17266
17338
  storage: this.options.storage,
17267
17339
  identity,
17268
17340
  session: { id: session.delegationCid, did: session.verificationMethod, space: session.spaceId },
17341
+ stamp: input.stamp,
17269
17342
  authority: input.authority,
17270
17343
  pending,
17271
17344
  scheduler: this.clock,
@@ -17275,6 +17348,9 @@ var init_runtime = __esm({
17275
17348
  previous: previousClose
17276
17349
  });
17277
17350
  this.current = controller;
17351
+ this.unsubscribeConsentExpired = this.options.storage.lifecycle?.subscribeConsentExpired?.(
17352
+ (stamp2) => controller.observeConsentExpired(stamp2)
17353
+ );
17278
17354
  this.lastController = controller;
17279
17355
  this.lastIdentity = identity;
17280
17356
  for (const [kv, space] of this.attached) {
@@ -17295,17 +17371,24 @@ var init_runtime = __esm({
17295
17371
  }
17296
17372
  attach(space, kv) {
17297
17373
  this.attached.set(kv, space);
17298
- kv.setReadThrough(space === this.primarySpace ? this.current ?? null : null);
17374
+ kv.setReadThrough(space === this.primarySpace ? this.current ?? this.gated ?? null : null);
17375
+ const generation = this.generation;
17299
17376
  return this.binding.then(() => {
17300
- kv.setReadThrough(space === this.primarySpace ? this.current ?? null : null);
17377
+ if (generation === this.generation && !this.closed && this.attached.get(kv) === space) {
17378
+ kv.setReadThrough(space === this.primarySpace ? this.current ?? this.gated ?? null : null);
17379
+ }
17301
17380
  });
17302
17381
  }
17303
17382
  unbind() {
17304
17383
  this.generation++;
17305
17384
  this.bindingActive = false;
17306
17385
  this.pendingPurgeSet = false;
17386
+ this.unsubscribeConsentExpired?.();
17387
+ this.unsubscribeConsentExpired = void 0;
17307
17388
  const controller = this.current;
17308
17389
  this.current = void 0;
17390
+ this.gated = void 0;
17391
+ this.gatedReason = void 0;
17309
17392
  this.primarySpace = void 0;
17310
17393
  this.detachAll();
17311
17394
  const binding = this.binding;
@@ -17324,14 +17407,94 @@ var init_runtime = __esm({
17324
17407
  var authority_exports = {};
17325
17408
  __export(authority_exports, {
17326
17409
  AUTHORITY_EXPIRY_MARGIN_MS: () => AUTHORITY_EXPIRY_MARGIN_MS,
17410
+ assertRenewalParent: () => assertRenewalParent,
17327
17411
  assertValidReplicationConfig: () => assertValidReplicationConfig,
17328
17412
  augmentSignInEntriesWithReplication: () => augmentSignInEntriesWithReplication,
17329
17413
  createReplicationAuthority: () => createReplicationAuthority,
17414
+ establishReplicaSessionStamp: () => establishReplicaSessionStamp,
17330
17415
  hasUnrestrictedGetCoverage: () => hasUnrestrictedGetCoverage,
17416
+ mintNodeReplicationGrant: () => mintNodeReplicationGrant,
17331
17417
  replicationScopeFromSignedSession: () => replicationScopeFromSignedSession,
17418
+ reportReplicaDenial: () => reportReplicaDenial,
17419
+ restoreReplicaSessionStamp: () => restoreReplicaSessionStamp,
17332
17420
  ucanAttCovers: () => ucanAttCovers,
17333
- ucanAttUnconstrainedFor: () => ucanAttUnconstrainedFor
17421
+ ucanAttUnconstrainedFor: () => ucanAttUnconstrainedFor,
17422
+ verifiedOwnerConsentAnchor: () => verifiedOwnerConsentAnchor
17334
17423
  });
17424
+ function reportReplicaDenial(result, reason, onEvent, context) {
17425
+ if (result.status !== "denied" && result.status !== "deleted" && result.status !== "marker") return;
17426
+ const event = {
17427
+ type: "replication.lifecycle",
17428
+ at: (/* @__PURE__ */ new Date()).toISOString(),
17429
+ reason,
17430
+ action: result.status === "denied" ? "denied" : result.status === "deleted" ? "database_deleted" : "marker_set",
17431
+ ...result.status === "denied" ? { partitions: result.intents } : {},
17432
+ ...result.status === "marker" ? { code: result.code } : {}
17433
+ };
17434
+ try {
17435
+ onEvent?.(event);
17436
+ } catch {
17437
+ }
17438
+ const { type, ...data } = event;
17439
+ try {
17440
+ context?.emit(type, data);
17441
+ } catch {
17442
+ }
17443
+ }
17444
+ function verifiedOwnerConsentAnchor(session, wasmBindings, exactVerifiedRecap2) {
17445
+ if (typeof session.siwe !== "string" || typeof session.signature !== "string" || typeof session.address !== "string" || typeof session.chainId !== "number") return null;
17446
+ try {
17447
+ const verified = wasmBindings.validatePersistedSession?.({
17448
+ delegationHeader: session.delegationHeader,
17449
+ delegationCid: session.delegationCid,
17450
+ spaceId: session.spaceId,
17451
+ jwk: session.jwk,
17452
+ address: session.address,
17453
+ chainId: session.chainId,
17454
+ siwe: session.siwe,
17455
+ signature: session.signature
17456
+ });
17457
+ if (!verified || !Number.isSafeInteger(Date.parse(verified.expiresAt ?? ""))) return null;
17458
+ const signedIssuedAt = new import_siwe3.SiweMessage(session.siwe).issuedAt;
17459
+ if (typeof signedIssuedAt !== "string") return null;
17460
+ const issuedAt = Date.parse(signedIssuedAt);
17461
+ if (!Number.isSafeInteger(issuedAt) || issuedAt < 0) return null;
17462
+ const granted = exactVerifiedRecap2(verified).filter((entry) => entry.space === session.spaceId && entry.service === "kv").map((entry) => ({ ...entry, service: "tinycloud.kv" }));
17463
+ const coversGetAndSync = granted.some((entry) => (0, import_sdk_core11.isCapabilitySubset)([{
17464
+ service: "tinycloud.kv",
17465
+ space: session.spaceId,
17466
+ path: entry.path,
17467
+ actions: [import_sdk_core11.KV.GET, import_sdk_core11.KV.SYNC]
17468
+ }], granted).subset);
17469
+ return coversGetAndSync ? { at: issuedAt, source: "siwe" } : null;
17470
+ } catch {
17471
+ return null;
17472
+ }
17473
+ }
17474
+ function establishReplicaSessionStamp(storage, principal, anchor) {
17475
+ if (storage.kind === "indexeddb" && storage.lifecycle?.kind === "indexeddb") {
17476
+ return storage.lifecycle.establishSessionStamp(principal, { consentAnchor: anchor });
17477
+ }
17478
+ if (storage.kind === "sqlite" && storage.lifecycle?.kind === "file") {
17479
+ return storage.lifecycle.establishSessionStamp(principal, { consentAnchor: anchor });
17480
+ }
17481
+ throw Object.assign(new Error("Replica sign-in requires its platform lifecycle gate."), {
17482
+ code: "REPLICA_GATED",
17483
+ gate: "gate_unavailable"
17484
+ });
17485
+ }
17486
+ function restoreReplicaSessionStamp(storage, principal, persisted) {
17487
+ if (storage.kind === "indexeddb" && storage.lifecycle?.kind === "indexeddb") {
17488
+ return storage.lifecycle.restoreSessionStamp(principal, persisted);
17489
+ }
17490
+ if (storage.kind === "sqlite" && storage.lifecycle?.kind === "file") {
17491
+ return storage.lifecycle.restoreSessionStamp(principal, persisted);
17492
+ }
17493
+ throw Object.assign(new Error("Replica restore requires its platform lifecycle gate."), {
17494
+ code: "REPLICA_GATED",
17495
+ gate: "gate_unavailable"
17496
+ });
17497
+ }
17335
17498
  function replicationEntries(spaceId, prefix) {
17336
17499
  return [{
17337
17500
  service: KV_SERVICE2,
@@ -17379,6 +17542,20 @@ function ucanAttCovers(att, space, prefix, ability, options) {
17379
17542
  function ucanAttUnconstrainedFor(att, space, prefix) {
17380
17543
  return ucanAttCovers(att, space, prefix, KV_GET2) && ucanAttCovers(att, space, prefix, KV_SYNC2);
17381
17544
  }
17545
+ async function mintNodeReplicationGrant(deviceDid, entries, options, renewalSession, node) {
17546
+ if (options?.renewalOnly && !renewalSession) throw new Error("replication renewal lost its owner session");
17547
+ const delegateOptions = options?.renewalOnly ? {
17548
+ expiry: options.expiryMs,
17549
+ onPrepared: async (prepared) => {
17550
+ assertRenewalParent(prepared.delegationHeader.Authorization, renewalSession);
17551
+ }
17552
+ } : options?.expiryMs === void 0 ? void 0 : { expiry: options.expiryMs };
17553
+ const result = await node.delegateTo((0, import_sdk_core11.principalDid)(deviceDid), entries, delegateOptions);
17554
+ return {
17555
+ ucan: result.delegation.delegationHeader.Authorization,
17556
+ expiresAt: result.delegation.expiry.getTime()
17557
+ };
17558
+ }
17382
17559
  function replicationScopeFromSignedSession(input) {
17383
17560
  let grant;
17384
17561
  try {
@@ -17396,9 +17573,28 @@ function replicationScopeFromSignedSession(input) {
17396
17573
  }
17397
17574
  return void 0;
17398
17575
  }
17576
+ function assertRenewalParent(authorization, session) {
17577
+ const grant = parseUcanGrant(authorization);
17578
+ if (grant.issuer !== session.verificationMethod.split("#", 1)[0] || grant.prf.length !== 1 || grant.prf[0] !== session.delegationCid) {
17579
+ throw Object.assign(new Error("replication renewal requires the bound owner session parent"), { refused: "NOT_COVERED" });
17580
+ }
17581
+ }
17399
17582
  function createReplicationAuthority(host) {
17400
- const planOrRefusal = (entries) => {
17401
- const plan = host.planDelegation(entries);
17583
+ const boundSession = host.replicationSession();
17584
+ let verifiedAnchor = null;
17585
+ try {
17586
+ const candidate = boundSession && host.verifiedConsentAnchor?.(boundSession);
17587
+ if (candidate && Number.isFinite(candidate.at) && candidate.source !== "first_install") {
17588
+ verifiedAnchor = { at: candidate.at, source: candidate.source };
17589
+ }
17590
+ } catch {
17591
+ }
17592
+ const activeSession = () => {
17593
+ const session = host.replicationSession();
17594
+ return session !== void 0 && session === boundSession ? session : void 0;
17595
+ };
17596
+ const planOrRefusal = (entries, expiryMs) => {
17597
+ const plan = host.planDelegation(entries, expiryMs === void 0 ? void 0 : { expiry: expiryMs });
17402
17598
  if ("refused" in plan) return plan;
17403
17599
  return { path: plan.path, parentCid: plan.parentCid, expiresAt: plan.expiresAt };
17404
17600
  };
@@ -17410,12 +17606,20 @@ function createReplicationAuthority(host) {
17410
17606
  };
17411
17607
  const authority = {
17412
17608
  get sessionOnly() {
17413
- const session = host.replicationSession();
17414
- return session !== void 0 && session.siwe === void 0;
17609
+ const session = activeSession();
17610
+ return session === void 0 || session.siwe === void 0;
17611
+ },
17612
+ consentAnchor() {
17613
+ return activeSession() ? verifiedAnchor : null;
17614
+ },
17615
+ get canRenew() {
17616
+ const session = activeSession();
17617
+ return verifiedAnchor?.source === "siwe" && session !== void 0 && session.siwe !== void 0;
17415
17618
  },
17416
17619
  sessionGrant(prefix) {
17417
- const session = host.replicationSession();
17620
+ const session = activeSession();
17418
17621
  if (session === void 0) return { refused: "SESSION_EXPIRING" };
17622
+ if (verifiedAnchor === null && session.siwe !== void 0) return { refused: "NOT_COVERED" };
17419
17623
  const siweExpiry = session.siwe ? host.siweExpiration(session.siwe)?.getTime() : void 0;
17420
17624
  if (siweExpiry !== void 0 && siweExpiry <= Date.now() + AUTHORITY_EXPIRY_MARGIN_MS) {
17421
17625
  return { refused: "SESSION_EXPIRING" };
@@ -17443,8 +17647,9 @@ function createReplicationAuthority(host) {
17443
17647
  };
17444
17648
  },
17445
17649
  plan(prefix) {
17446
- const session = host.replicationSession();
17650
+ const session = activeSession();
17447
17651
  if (session === void 0) return { refused: "SESSION_EXPIRING" };
17652
+ if (verifiedAnchor === null) return { refused: "NOT_COVERED" };
17448
17653
  const sessionGrant = this.sessionGrant(prefix);
17449
17654
  if (!("refused" in sessionGrant)) {
17450
17655
  const signed2 = signedSessionGrant(session);
@@ -17458,26 +17663,30 @@ function createReplicationAuthority(host) {
17458
17663
  if (authority.sessionOnly) return sessionGrant;
17459
17664
  return planOrRefusal(replicationEntries(session.spaceId, prefix));
17460
17665
  },
17461
- async mint(deviceDid, prefix, signal) {
17462
- if (authority.sessionOnly) throw new Error("compact delegate sessions cannot mint device grants");
17463
- const session = host.replicationSession();
17464
- if (session === void 0) throw new Error("replication mint without a session");
17666
+ async mint(deviceDid, prefix, signal, options) {
17667
+ const session = activeSession();
17668
+ if (session === void 0) throw new Error("replication mint without the bound session");
17669
+ if (session.siwe === void 0) throw new Error("compact delegate sessions cannot mint device grants");
17670
+ if (verifiedAnchor?.source !== "siwe") throw new Error("replication mint requires verified owner consent");
17465
17671
  const entries = replicationEntries(session.spaceId, prefix);
17466
- const plan = planOrRefusal(entries);
17672
+ const plan = planOrRefusal(entries, options?.expiryMs);
17467
17673
  if ("refused" in plan) {
17468
17674
  throw Object.assign(
17469
17675
  new Error(`replication authority refused the mint: ${plan.refused}`),
17470
17676
  { refused: plan.refused }
17471
17677
  );
17472
17678
  }
17679
+ if (options?.renewalOnly && plan.path !== "session") {
17680
+ throw Object.assign(new Error("replication renewal requires a session parent"), { refused: "NOT_COVERED" });
17681
+ }
17473
17682
  signal.throwIfAborted();
17474
- const minted = await new Promise((resolve, reject2) => {
17683
+ const minted = await new Promise((resolve2, reject2) => {
17475
17684
  const onAbort = () => reject2(signal.reason ?? new Error("replication mint aborted"));
17476
17685
  signal.addEventListener("abort", onAbort, { once: true });
17477
- host.mintDelegation(deviceDid, entries).then(
17686
+ host.mintDelegation(deviceDid, entries, options).then(
17478
17687
  (result) => {
17479
17688
  signal.removeEventListener("abort", onAbort);
17480
- resolve(result);
17689
+ resolve2(result);
17481
17690
  },
17482
17691
  (error) => {
17483
17692
  signal.removeEventListener("abort", onAbort);
@@ -17490,10 +17699,11 @@ function createReplicationAuthority(host) {
17490
17699
  };
17491
17700
  return authority;
17492
17701
  }
17493
- var import_sdk_core11, import_replica, parseUcanGrant, KV_GET2, KV_SYNC2, KV_SERVICE2, AUTHORITY_EXPIRY_MARGIN_MS;
17702
+ var import_siwe3, import_sdk_core11, import_replica, parseUcanGrant, KV_GET2, KV_SYNC2, KV_SERVICE2, AUTHORITY_EXPIRY_MARGIN_MS;
17494
17703
  var init_authority = __esm({
17495
17704
  "src/replication/authority.ts"() {
17496
17705
  "use strict";
17706
+ import_siwe3 = require("siwe");
17497
17707
  import_sdk_core11 = require("@tinycloud/sdk-core");
17498
17708
  import_replica = require("@tinycloud/replica");
17499
17709
  init_config();
@@ -17506,6 +17716,78 @@ var init_authority = __esm({
17506
17716
  }
17507
17717
  });
17508
17718
 
17719
+ // src/replication/gated-network.ts
17720
+ var gated_network_exports = {};
17721
+ __export(gated_network_exports, {
17722
+ createGatedNetworkReadThrough: () => createGatedNetworkReadThrough
17723
+ });
17724
+ function createGatedNetworkReadThrough(input) {
17725
+ const report = (op, space, path, replica, reason, outcome, latencyMs) => input.emit({
17726
+ type: "replication.read",
17727
+ at: new Date(input.now()).toISOString(),
17728
+ op,
17729
+ space,
17730
+ key: path,
17731
+ replica,
17732
+ source: reason === "cursor_restart" ? "none" : "network",
17733
+ reason,
17734
+ outcome,
17735
+ latencyMs,
17736
+ stalenessMs: null,
17737
+ coverage: null,
17738
+ authority: null,
17739
+ ...reason === "replica_gated" ? { code: "REPLICA_GATED" } : {}
17740
+ });
17741
+ const readNetwork = async (r2, op, path, prefix, reason) => {
17742
+ const started = input.now();
17743
+ let outcome = "error";
17744
+ try {
17745
+ const result = await r2.network();
17746
+ outcome = result.ok ? "found" : op === "get" && reason === "not_covered" && "error" in result && result.error.code === "KV_NOT_FOUND" ? "not_found" : "error";
17747
+ return result;
17748
+ } finally {
17749
+ report(op, r2.space, path, prefix, reason, outcome, input.now() - started);
17750
+ }
17751
+ };
17752
+ const covered = (path) => input.prefixes.find((prefix) => (0, import_prefix3.kvPrefixCovers)(prefix, path));
17753
+ return {
17754
+ get: (r2) => {
17755
+ if (r2.space !== input.space) return r2.network();
17756
+ if (r2.options?.source === "network") return readNetwork(r2, "get", r2.path, null, "NETWORK_REQUESTED");
17757
+ const prefix = covered(r2.path);
17758
+ return readNetwork(r2, "get", r2.path, prefix ?? null, prefix === void 0 ? "not_covered" : "replica_gated");
17759
+ },
17760
+ list: (r2) => {
17761
+ if (r2.space !== input.space) return r2.network();
17762
+ if (r2.options?.source === "network") return readNetwork(r2, "list", r2.listPath, null, "NETWORK_REQUESTED");
17763
+ if (r2.options?.cursor?.startsWith("tcr1.")) {
17764
+ report("list", r2.space, r2.listPath, null, "cursor_restart", "error", 0);
17765
+ return Promise.resolve({ ok: false, error: {
17766
+ code: "INVALID_INPUT",
17767
+ message: "Local list cursor cannot be served; restart without the cursor",
17768
+ service: "kv",
17769
+ meta: { replication: "cursor_restart" }
17770
+ } });
17771
+ }
17772
+ if (r2.options?.cursor) return readNetwork(r2, "list", r2.listPath, null, "network_cursor");
17773
+ if (r2.options?.raw) return readNetwork(r2, "list", r2.listPath, null, "unsupported_option");
17774
+ const prefix = covered(r2.listPath);
17775
+ return readNetwork(r2, "list", r2.listPath, prefix ?? null, prefix === void 0 ? "not_covered" : "replica_gated");
17776
+ },
17777
+ write: (r2) => r2.network(),
17778
+ observeNetworkRequested: (r2) => {
17779
+ if (r2.space === input.space) report(r2.op, r2.space, r2.path, null, r2.reason, r2.outcome, r2.latencyMs);
17780
+ }
17781
+ };
17782
+ }
17783
+ var import_prefix3;
17784
+ var init_gated_network = __esm({
17785
+ "src/replication/gated-network.ts"() {
17786
+ "use strict";
17787
+ import_prefix3 = require("@tinycloud/sdk-services/kv/replication/prefix");
17788
+ }
17789
+ });
17790
+
17509
17791
  // src/index.ts
17510
17792
  var src_exports = {};
17511
17793
  __export(src_exports, {
@@ -17557,6 +17839,8 @@ __export(src_exports, {
17557
17839
  PrefixedKVService: () => import_sdk_core17.PrefixedKVService,
17558
17840
  PrivateKeySigner: () => PrivateKeySigner,
17559
17841
  ProtocolMismatchError: () => import_sdk_core26.ProtocolMismatchError,
17842
+ REPLICA_LOGOUT_PENDING_FILE: () => REPLICA_LOGOUT_PENDING_FILE,
17843
+ RESTORED_GATE_MESSAGE: () => RESTORED_GATE_MESSAGE,
17560
17844
  RUNTIME_DELEGATION_REJECTIONS: () => RUNTIME_DELEGATION_REJECTIONS,
17561
17845
  RuntimeDelegationRejectedError: () => RuntimeDelegationRejectedError,
17562
17846
  SECRET_NAME_RE: () => import_sdk_core20.SECRET_NAME_RE,
@@ -17589,6 +17873,7 @@ __export(src_exports, {
17589
17873
  activateValidatedRuntimeDelegation: () => activateValidatedRuntimeDelegation,
17590
17874
  addressStorageKey: () => import_sdk_core15.addressStorageKey,
17591
17875
  appSecretScope: () => import_sdk_core20.appSecretScope,
17876
+ assertProfileLogoutClear: () => assertProfileLogoutClear,
17592
17877
  buildCanonicalDecryptRequest: () => import_sdk_core21.buildCanonicalDecryptRequest,
17593
17878
  buildDecryptAttenuation: () => import_sdk_core21.buildDecryptAttenuation,
17594
17879
  buildDecryptFacts: () => import_sdk_core21.buildDecryptFacts,
@@ -17609,6 +17894,7 @@ __export(src_exports, {
17609
17894
  composeManifestRequest: () => import_sdk_core16.composeManifestRequest,
17610
17895
  createCapabilityKeyRegistry: () => import_sdk_core24.createCapabilityKeyRegistry,
17611
17896
  createOpenKeyCallbackSigningStrategy: () => import_sdk_core3.createOpenKeyCallbackSigningStrategy,
17897
+ createProfileReplicaPurge: () => createProfileReplicaPurge,
17612
17898
  createSharingService: () => import_sdk_core23.createSharingService,
17613
17899
  createSpaceService: () => import_sdk_core25.createSpaceService,
17614
17900
  createVaultCrypto: () => import_sdk_core20.createVaultCrypto,
@@ -17645,6 +17931,7 @@ __export(src_exports, {
17645
17931
  isNetworkId: () => import_sdk_core21.isNetworkId,
17646
17932
  isStorageFullError: () => import_sdk_core15.isStorageFullError,
17647
17933
  loadManifest: () => import_sdk_core16.loadManifest,
17934
+ logoutPrincipalDigest: () => logoutPrincipalDigest,
17648
17935
  makePkhSpaceId: () => import_sdk_core15.makePkhSpaceId,
17649
17936
  makePublicSpaceId: () => import_sdk_core25.makePublicSpaceId,
17650
17937
  networkDiscoveryKey: () => import_sdk_core21.networkDiscoveryKey,
@@ -18330,7 +18617,6 @@ function deserializeDelegation(data) {
18330
18617
 
18331
18618
  // src/TinyCloudNode.ts
18332
18619
  var import_siwe2 = require("siwe");
18333
- init_config();
18334
18620
 
18335
18621
  // src/replication/module-registry.ts
18336
18622
  var nodeLoaders;
@@ -18559,6 +18845,7 @@ var NodeUserAuthorization = class {
18559
18845
  this.siweConfig = config.siweConfig;
18560
18846
  this.includeAccountRegistryPermissions = config.includeAccountRegistryPermissions ?? true;
18561
18847
  this.replication = config.replication?.enabled === true ? config.replication : void 0;
18848
+ this.establishReplicaStamp = config.establishReplicaStamp;
18562
18849
  this._manifest = config.manifest;
18563
18850
  this._capabilityRequest = config.capabilityRequest;
18564
18851
  this.sessionManager = config.sessionManager ?? this.wasm.createSessionManager();
@@ -19189,7 +19476,7 @@ var NodeUserAuthorization = class {
19189
19476
  );
19190
19477
  throw error;
19191
19478
  }
19192
- await new Promise((resolve) => setTimeout(resolve, 100));
19479
+ await new Promise((resolve2) => setTimeout(resolve2, 100));
19193
19480
  const retryResult = await (0, import_sdk_core4.activateSessionWithHost)(
19194
19481
  host,
19195
19482
  this._tinyCloudSession.delegationHeader
@@ -19229,7 +19516,7 @@ var NodeUserAuthorization = class {
19229
19516
  );
19230
19517
  throw error;
19231
19518
  }
19232
- await new Promise((resolve) => setTimeout(resolve, 100));
19519
+ await new Promise((resolve2) => setTimeout(resolve2, 100));
19233
19520
  const retryResult = await (0, import_sdk_core4.activateSessionWithHost)(
19234
19521
  host,
19235
19522
  this._tinyCloudSession.delegationHeader
@@ -19328,6 +19615,27 @@ var NodeUserAuthorization = class {
19328
19615
  signature: signature2
19329
19616
  };
19330
19617
  }
19618
+ persistedData(session, expiresAt, createdAt, replicaStamp) {
19619
+ return {
19620
+ address: session.address,
19621
+ chainId: session.chainId,
19622
+ sessionKey: JSON.stringify(session.jwk),
19623
+ siwe: session.siwe,
19624
+ signature: session.signature,
19625
+ tinycloudSession: {
19626
+ delegationHeader: session.delegationHeader,
19627
+ delegationCid: session.delegationCid,
19628
+ spaceId: session.spaceId,
19629
+ spaces: session.spaces,
19630
+ verificationMethod: session.verificationMethod
19631
+ },
19632
+ expiresAt,
19633
+ createdAt,
19634
+ version: "1.0",
19635
+ tinycloudHosts: this.tinycloudHosts,
19636
+ ...replicaStamp ? { replicaStamp } : {}
19637
+ };
19638
+ }
19331
19639
  /**
19332
19640
  * Sign in and create a new session.
19333
19641
  *
@@ -19407,24 +19715,13 @@ var NodeUserAuthorization = class {
19407
19715
  siwe: prepared.siwe,
19408
19716
  signature: signature2
19409
19717
  };
19410
- const persistedData = {
19411
- address,
19412
- chainId,
19413
- sessionKey: JSON.stringify(jwk),
19414
- siwe: prepared.siwe,
19415
- signature: signature2,
19416
- tinycloudSession: {
19417
- delegationHeader: session.delegationHeader,
19418
- delegationCid: session.delegationCid,
19419
- spaceId,
19420
- spaces: spacesMetadata,
19421
- verificationMethod: this.sessionManager.getDID(keyId)
19422
- },
19423
- expiresAt: expirationTime.toISOString(),
19424
- createdAt: now.toISOString(),
19425
- version: "1.0",
19426
- tinycloudHosts: this.tinycloudHosts
19427
- };
19718
+ const replicaStamp = await this.establishReplicaStamp?.(tinyCloudSession);
19719
+ const persistedData = this.persistedData(
19720
+ tinyCloudSession,
19721
+ expirationTime.toISOString(),
19722
+ now.toISOString(),
19723
+ replicaStamp
19724
+ );
19428
19725
  await this.enqueuePersistence(async () => {
19429
19726
  await this.sessionStorage.save(address, persistedData);
19430
19727
  if (generation !== this.sessionGeneration) {
@@ -19578,28 +19875,12 @@ var NodeUserAuthorization = class {
19578
19875
  siwe: prepared.siwe,
19579
19876
  signature: signature2
19580
19877
  };
19581
- const expirationMatch = prepared.siwe.match(/Expiration Time: (.+)/);
19582
- const issuedAtMatch = prepared.siwe.match(/Issued At: (.+)/);
19878
+ const expirationMatch = prepared.siwe?.match(/Expiration Time: (.+)/);
19879
+ const issuedAtMatch = prepared.siwe?.match(/Issued At: (.+)/);
19583
19880
  const expiresAt = expirationMatch?.[1] ?? new Date(Date.now() + this.sessionExpirationMs).toISOString();
19584
19881
  const createdAt = issuedAtMatch?.[1] ?? (/* @__PURE__ */ new Date()).toISOString();
19585
- const persistedData = {
19586
- address,
19587
- chainId,
19588
- sessionKey: JSON.stringify(jwk),
19589
- siwe: prepared.siwe,
19590
- signature: signature2,
19591
- tinycloudSession: {
19592
- delegationHeader: session.delegationHeader,
19593
- delegationCid: session.delegationCid,
19594
- spaceId: prepared.spaceId,
19595
- spaces: spacesMetadata,
19596
- verificationMethod: this.sessionManager.getDID(keyId)
19597
- },
19598
- expiresAt,
19599
- createdAt,
19600
- version: "1.0",
19601
- tinycloudHosts: this.tinycloudHosts
19602
- };
19882
+ const replicaStamp = await this.establishReplicaStamp?.(tinyCloudSession);
19883
+ const persistedData = this.persistedData(tinyCloudSession, expiresAt, createdAt, replicaStamp);
19603
19884
  await this.sessionStorage.save(address, persistedData);
19604
19885
  this._session = clientSession;
19605
19886
  this._tinyCloudSession = tinyCloudSession;
@@ -19662,15 +19943,36 @@ var NodeUserAuthorization = class {
19662
19943
  const { prepared, keyId, jwk } = await this.prepareSessionForSigning();
19663
19944
  const presentation = this.buildPresentationEnvelope(options?.reason);
19664
19945
  const hostHint = this.primaryTinyCloudHost;
19665
- const result = await authorizeFn({
19946
+ const originalRequest = {
19666
19947
  protocolVersion: 1,
19667
19948
  siwe: prepared.siwe,
19668
19949
  jwk,
19950
+ verificationMethod: prepared.verificationMethod,
19669
19951
  host: hostHint,
19670
19952
  keyId: options?.openkeyKeyId,
19671
19953
  presentation
19672
- });
19673
- return this.signInWithOpenKeyResult(result, prepared, keyId, jwk);
19954
+ };
19955
+ let result;
19956
+ let signed2;
19957
+ let signature2;
19958
+ for (let attempt = 0; ; ) {
19959
+ try {
19960
+ result ?? (result = await authorizeFn(originalRequest));
19961
+ signed2 = await this.verifyOpenKeyResult(result, prepared);
19962
+ signature2 = result.signature;
19963
+ break;
19964
+ } catch (error) {
19965
+ if (!options?.retryResult) throw error;
19966
+ const next = await options.retryResult(
19967
+ error instanceof Error ? error : new Error(String(error)),
19968
+ originalRequest,
19969
+ ++attempt
19970
+ );
19971
+ if (next === null) throw error;
19972
+ result = next;
19973
+ }
19974
+ }
19975
+ return this.signInWithPreparedSession(signed2, signature2, keyId, jwk);
19674
19976
  }
19675
19977
  /**
19676
19978
  * Build a `CapabilityPresentationEnvelopeV1` from `this._manifest`. The
@@ -19750,6 +20052,11 @@ var NodeUserAuthorization = class {
19750
20052
  * (resource, action) pair in the returned SIWE.
19751
20053
  */
19752
20054
  async signInWithOpenKeyResult(result, prepared, keyId, jwk) {
20055
+ const signed2 = await this.verifyOpenKeyResult(result, prepared);
20056
+ return this.signInWithPreparedSession(signed2, result.signature, keyId, jwk);
20057
+ }
20058
+ /** Validate a candidate without activating a network session, so paste retries reuse one preparation. */
20059
+ async verifyOpenKeyResult(result, prepared) {
19753
20060
  if (result.protocolVersion !== 1) {
19754
20061
  throw new Error(`Unsupported OpenKey protocol version ${result.protocolVersion}`);
19755
20062
  }
@@ -19994,17 +20301,12 @@ var NodeUserAuthorization = class {
19994
20301
  }
19995
20302
  }
19996
20303
  void nonRequiredGrantedPairs;
19997
- return this.signInWithPreparedSession(
19998
- {
19999
- siwe: result.signedMessage,
20000
- jwk: prepared.jwk,
20001
- spaceId: prepared.spaceId,
20002
- verificationMethod: prepared.verificationMethod
20003
- },
20004
- result.signature,
20005
- keyId,
20006
- jwk
20007
- );
20304
+ return {
20305
+ siwe: result.signedMessage,
20306
+ jwk: prepared.jwk,
20307
+ spaceId: prepared.spaceId,
20308
+ verificationMethod: prepared.verificationMethod
20309
+ };
20008
20310
  }
20009
20311
  /**
20010
20312
  * Clear persisted session data.
@@ -20056,7 +20358,7 @@ var NodeUserAuthorization = class {
20056
20358
  * Request signature via event emitter with timeout.
20057
20359
  */
20058
20360
  requestSignatureViaEmitter(request, emitter, timeout) {
20059
- return new Promise((resolve, reject2) => {
20361
+ return new Promise((resolve2, reject2) => {
20060
20362
  const timeoutId = setTimeout(() => {
20061
20363
  reject2(new Error("Sign request timed out"));
20062
20364
  }, timeout);
@@ -20068,7 +20370,7 @@ var NodeUserAuthorization = class {
20068
20370
  );
20069
20371
  } else {
20070
20372
  const signature2 = response.signature ?? await this.signer.signMessage(request.message);
20071
- resolve(signature2);
20373
+ resolve2(signature2);
20072
20374
  }
20073
20375
  };
20074
20376
  emitter.emit("sign-request", request, respond);
@@ -20984,6 +21286,20 @@ function accountIndexWarningDiagnostic(error) {
20984
21286
  }
20985
21287
  return diagnostic;
20986
21288
  }
21289
+ function replicaRestoreRefusal(error) {
21290
+ if (error !== null && typeof error === "object" && "code" in error && error.code === "REPLICA_GATED" && "gate" in error) {
21291
+ switch (error.gate) {
21292
+ case "not_established":
21293
+ case "stamp_stale":
21294
+ case "principal_mismatch":
21295
+ case "purge_pending":
21296
+ case "logout_pending":
21297
+ case "gate_unavailable":
21298
+ return error.gate;
21299
+ }
21300
+ }
21301
+ return "gate_unavailable";
21302
+ }
20987
21303
  var _TinyCloudNode = class _TinyCloudNode {
20988
21304
  /**
20989
21305
  * Create a new TinyCloudNode instance.
@@ -21013,6 +21329,37 @@ var _TinyCloudNode = class _TinyCloudNode {
21013
21329
  this.auth = null;
21014
21330
  this.tc = null;
21015
21331
  this._chainId = 1;
21332
+ /** Mints the replica session stamp on each fresh sign-in (TC-858 §4.1). Undefined when replication is off. */
21333
+ this.establishReplicaStamp = async (session) => {
21334
+ const replication = this.config.replication;
21335
+ if (!replication?.enabled) return void 0;
21336
+ const epoch = this.sessionEpoch;
21337
+ const attempt = this.latestSessionAttempt;
21338
+ try {
21339
+ const { verifiedOwnerConsentAnchor: verifiedOwnerConsentAnchor2, establishReplicaSessionStamp: establishReplicaSessionStamp2 } = await getNodeReplicationLoaders().authority();
21340
+ if (epoch !== this.sessionEpoch || attempt !== this.latestSessionAttempt) return void 0;
21341
+ const anchor = verifiedOwnerConsentAnchor2(session, this.wasmBindings, exactVerifiedRecap);
21342
+ if (anchor === null) {
21343
+ throw Object.assign(new Error("Replica sign-in requires a verified finite owner SIWE and exact-space KV get+sync."), {
21344
+ code: "REPLICA_GATED",
21345
+ gate: "not_established"
21346
+ });
21347
+ }
21348
+ const stamp = await establishReplicaSessionStamp2(
21349
+ replication.storage,
21350
+ (0, import_sdk_core9.pkhDid)(session.address, session.chainId),
21351
+ anchor
21352
+ );
21353
+ if (epoch !== this.sessionEpoch || attempt !== this.latestSessionAttempt) return void 0;
21354
+ this.replicaSessionStamp = stamp;
21355
+ return stamp;
21356
+ } catch (error) {
21357
+ if (epoch === this.sessionEpoch && attempt === this.latestSessionAttempt) {
21358
+ this.replicaRestoreGate = replicaRestoreRefusal(error);
21359
+ }
21360
+ return void 0;
21361
+ }
21362
+ };
21016
21363
  this._baseVaults = /* @__PURE__ */ new Map();
21017
21364
  this._baseSecrets = /* @__PURE__ */ new Map();
21018
21365
  this._secrets = /* @__PURE__ */ new Map();
@@ -21137,7 +21484,7 @@ var _TinyCloudNode = class _TinyCloudNode {
21137
21484
  });
21138
21485
  return this.wasmBindings.invokeAny(invocationSession, caveatPreservingEntries, facts);
21139
21486
  };
21140
- assertValidReplicationConfig(config.replication);
21487
+ if (config.replication?.enabled) getNodeReplicationLoaders().validateConfig(config.replication);
21141
21488
  this.explicitHost = config.host;
21142
21489
  this.config = {
21143
21490
  ...config,
@@ -21152,6 +21499,16 @@ var _TinyCloudNode = class _TinyCloudNode {
21152
21499
  this.replicationRuntime = runtime;
21153
21500
  this.replicationControl = {
21154
21501
  status: async () => {
21502
+ const gate = this.replicaRestoreGate;
21503
+ if (gate) return this.config.replication.prefixes.map((prefix) => ({
21504
+ prefix,
21505
+ state: "gated",
21506
+ gate,
21507
+ errorCode: "REPLICA_GATED",
21508
+ pending: { inFlight: 0, committed: 0, ambiguous: 0 },
21509
+ pinned: [],
21510
+ lagMs: null
21511
+ }));
21155
21512
  const status = await runtime.then((ready) => ready.control.status());
21156
21513
  const scopeError = this.replicationScopeError;
21157
21514
  if (!scopeError) return status;
@@ -21168,7 +21525,15 @@ var _TinyCloudNode = class _TinyCloudNode {
21168
21525
  lagMs: null
21169
21526
  }));
21170
21527
  },
21171
- sync: async (input) => (await runtime).control.sync(input),
21528
+ sync: async (input) => {
21529
+ if (this.replicaRestoreGate) {
21530
+ throw Object.assign(new Error("Replica restore is gated."), {
21531
+ code: "REPLICA_GATED",
21532
+ gate: this.replicaRestoreGate
21533
+ });
21534
+ }
21535
+ return (await runtime).control.sync(input);
21536
+ },
21172
21537
  purge: (input) => {
21173
21538
  const instance = this.replicationRuntimeInstance;
21174
21539
  return instance ? instance.control.purge(input) : runtime.then((ready) => ready.control.purge(input));
@@ -21384,6 +21749,7 @@ var _TinyCloudNode = class _TinyCloudNode {
21384
21749
  signer: this.signer,
21385
21750
  signStrategy: config.signStrategy ?? { type: "auto-sign" },
21386
21751
  wasmBindings: this.wasmBindings,
21752
+ ...config.replication?.enabled === true ? { establishReplicaStamp: this.establishReplicaStamp } : {},
21387
21753
  sessionManager: this.sessionManager,
21388
21754
  sessionStorage: config.sessionStorage ?? new MemorySessionStorage(),
21389
21755
  domain: this.siweDomain,
@@ -21581,11 +21947,10 @@ var _TinyCloudNode = class _TinyCloudNode {
21581
21947
  return this.signer === null;
21582
21948
  }
21583
21949
  /**
21584
- * Get the space ID for this user.
21585
- * Available after signIn().
21950
+ * Primary space of the signed-in or restored network session, including compact sessions.
21586
21951
  */
21587
21952
  get spaceId() {
21588
- return this.auth?.tinyCloudSession?.spaceId;
21953
+ return this.auth?.tinyCloudSession?.spaceId ?? this._activeServiceSession?.spaceId;
21589
21954
  }
21590
21955
  /**
21591
21956
  * Get the account space ID for this wallet identity.
@@ -21633,6 +21998,12 @@ var _TinyCloudNode = class _TinyCloudNode {
21633
21998
  get restorableSession() {
21634
21999
  return this.currentTinyCloudSession();
21635
22000
  }
22001
+ /** The active, verified replica stamp. Undefined when the replica is refused or the session has ended. */
22002
+ get replicaStamp() {
22003
+ const stamp = this.replicaSessionStamp;
22004
+ if (!stamp || this.replicaRestoreGate !== void 0 || !this.currentTinyCloudSession()) return void 0;
22005
+ return { incarnation: stamp.incarnation, lifecycleEpoch: stamp.lifecycleEpoch };
22006
+ }
21636
22007
  /**
21637
22008
  * Sign out and end the session on this instance.
21638
22009
  *
@@ -21653,18 +22024,60 @@ var _TinyCloudNode = class _TinyCloudNode {
21653
22024
  }
21654
22025
  async endSession() {
21655
22026
  const leavingAddress = this.address ?? this._restoredTcSession?.address;
22027
+ const leavingChainId = this._chainId;
22028
+ const leavingPrincipal = this.replicaSessionPrincipal ?? (leavingAddress ? (0, import_sdk_core9.pkhDid)(leavingAddress, leavingChainId) : void 0);
21656
22029
  const leavingAuth = this.auth;
22030
+ const leavingContext = this._serviceContext;
22031
+ let denied;
21657
22032
  try {
21658
22033
  this.teardownSession();
21659
22034
  } finally {
22035
+ const denial = leavingPrincipal ? this.denyReplicaGate({ reason: "sign_out", principal: leavingPrincipal }, leavingContext) : Promise.resolve(void 0);
21660
22036
  try {
21661
- await this.accountRegistryTail;
21662
- if (this.replicationRuntime) await (await this.replicationRuntime).unbind();
22037
+ denied = await denial;
21663
22038
  } finally {
21664
- if (leavingAddress) {
21665
- await (leavingAuth ? leavingAuth.clearPersistedSession(leavingAddress) : this.config.sessionStorage?.clear(leavingAddress));
22039
+ try {
22040
+ await this.accountRegistryTail;
22041
+ await this.quiesceReplicaRuntime();
22042
+ } finally {
22043
+ if (leavingAddress) {
22044
+ await (leavingAuth ? leavingAuth.clearPersistedSession(leavingAddress) : this.config.sessionStorage?.clear(leavingAddress));
22045
+ }
22046
+ }
22047
+ }
22048
+ }
22049
+ if (denied?.status === "unguarded") {
22050
+ throw new Error(`Replica gate was not denied (${denied.code ?? "unknown"}); the replica may still be restorable.`);
22051
+ }
22052
+ }
22053
+ /**
22054
+ * Step 1 of replica sign-out. Never rejects: failures come back as `unguarded` so the session
22055
+ * is still cleared. Returns `undefined` when replication is off.
22056
+ */
22057
+ async denyReplicaGate(input, context = this._serviceContext) {
22058
+ const replication = this.config.replication;
22059
+ if (!replication?.enabled) return void 0;
22060
+ try {
22061
+ const lifecycle = replication.storage.lifecycle;
22062
+ if (lifecycle?.kind !== (replication.storage.kind === "indexeddb" ? "indexeddb" : "file")) {
22063
+ return { status: "unguarded", code: "gate_unavailable" };
22064
+ }
22065
+ const result = await lifecycle.deny(input);
22066
+ if (result.status === "denied" || result.status === "deleted" || result.status === "marker") {
22067
+ try {
22068
+ (await getNodeReplicationLoaders().authority()).reportReplicaDenial(
22069
+ result,
22070
+ input.reason,
22071
+ replication.onEvent,
22072
+ context
22073
+ );
22074
+ } catch {
21666
22075
  }
21667
22076
  }
22077
+ return result;
22078
+ } catch (error) {
22079
+ const code = typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : "gate_unavailable";
22080
+ return { status: "unguarded", code };
21668
22081
  }
21669
22082
  }
21670
22083
  /**
@@ -21684,15 +22097,9 @@ var _TinyCloudNode = class _TinyCloudNode {
21684
22097
  this._activeServiceSession = void 0;
21685
22098
  oldGraph.retire();
21686
22099
  oldCore?.retireServices();
21687
- oldAuth?.resetSession(
21688
- this.sessionManager,
21689
- this.sessionKeyId,
21690
- this.explicitHost ? [this.explicitHost] : void 0
21691
- );
21692
22100
  this.config.host = this.explicitHost ?? DEFAULT_HOST;
21693
22101
  this._chainId = 1;
21694
22102
  this._restoredNodeFeatures = void 0;
21695
- this._activeServiceSession = void 0;
21696
22103
  this._serviceContext = void 0;
21697
22104
  this._kv = void 0;
21698
22105
  this._sql = void 0;
@@ -21717,6 +22124,9 @@ var _TinyCloudNode = class _TinyCloudNode {
21717
22124
  this._bootstrapStatus = { skipped: false };
21718
22125
  this.replicationDelegateSession = void 0;
21719
22126
  this.replicationScopeError = void 0;
22127
+ this.replicaSessionPrincipal = void 0;
22128
+ this.replicaSessionStamp = void 0;
22129
+ this.replicaRestoreGate = void 0;
21720
22130
  const sessionKey = createDefaultSessionKey(this.wasmBindings);
21721
22131
  this.sessionManager = sessionKey.manager;
21722
22132
  this.sessionKeyId = sessionKey.keyId;
@@ -21726,6 +22136,9 @@ var _TinyCloudNode = class _TinyCloudNode {
21726
22136
  sessionKey.keyId,
21727
22137
  this.explicitHost ? [this.explicitHost] : void 0
21728
22138
  );
22139
+ this.replicaSessionPrincipal = void 0;
22140
+ this.replicaSessionStamp = void 0;
22141
+ this.replicaRestoreGate = void 0;
21729
22142
  this._serviceGraph = this.createServiceGraphLifetime();
21730
22143
  this.auth = oldAuth;
21731
22144
  this.tc = this.auth ? new import_sdk_core9.TinyCloud(this.auth, {
@@ -21755,11 +22168,25 @@ var _TinyCloudNode = class _TinyCloudNode {
21755
22168
  "Cannot signIn() in session-only mode. Provide a privateKey in config to create your own space."
21756
22169
  );
21757
22170
  }
21758
- const epoch = this.sessionEpoch;
21759
- const attempt = ++this.latestSessionAttempt;
21760
- await this.signOutInFlight;
22171
+ await this.signInWith(() => this.tc.signIn(options));
22172
+ }
22173
+ /**
22174
+ * Fresh OpenKey sign-in. The callback receives the SDK's original SIWE and
22175
+ * session JWK before approval; the signed result is verified against those
22176
+ * bytes before the network session or replica stamp is committed.
22177
+ */
22178
+ async signInWithOpenKey(authorizeFn, options) {
22179
+ if (!this.signer || !this.tc || !this.auth) {
22180
+ throw new Error("Cannot signInWithOpenKey() in session-only mode. Provide a signer in config.");
22181
+ }
22182
+ await this.signInWith(() => this.auth.signInWithOpenKey(authorizeFn, options));
22183
+ }
22184
+ async signInWith(performSignIn) {
22185
+ const epoch = this.sessionEpoch;
22186
+ const attempt = ++this.latestSessionAttempt;
22187
+ await this.signOutInFlight;
21761
22188
  try {
21762
- await this.establishSignIn(epoch, options);
22189
+ await this.establishSignIn(epoch, performSignIn);
21763
22190
  } catch (error) {
21764
22191
  if (epoch !== this.sessionEpoch && attempt === this.latestSessionAttempt) {
21765
22192
  await this.endSession();
@@ -21767,7 +22194,7 @@ var _TinyCloudNode = class _TinyCloudNode {
21767
22194
  throw error;
21768
22195
  }
21769
22196
  }
21770
- async establishSignIn(epoch, options) {
22197
+ async establishSignIn(epoch, performSignIn) {
21771
22198
  if (!this.signer || !this.tc) {
21772
22199
  throw new Error("Not signed in. Call signIn() first.");
21773
22200
  }
@@ -21777,6 +22204,12 @@ var _TinyCloudNode = class _TinyCloudNode {
21777
22204
  const address = (0, import_sdk_core9.canonicalizeAddress)(await this.signer.getAddress());
21778
22205
  const chainId = await this.signer.getChainId();
21779
22206
  this.assertSessionEpoch(epoch, "Sign-in");
22207
+ const nextPrincipal = (0, import_sdk_core9.pkhDid)(address, chainId);
22208
+ const priorPrincipal = this.replicaSessionPrincipal ?? (this.address ? (0, import_sdk_core9.pkhDid)(this.address, this._chainId) : void 0);
22209
+ if (this.config.replication?.enabled && priorPrincipal && priorPrincipal !== nextPrincipal) {
22210
+ const denial = await this.denyReplicaGate({ reason: "account_switch", principal: priorPrincipal, next: nextPrincipal });
22211
+ this.replicaRestoreGate = denial?.status === "unguarded" ? "gate_unavailable" : "stamp_stale";
22212
+ }
21780
22213
  if (address !== this._address || chainId !== this._chainId) this.storageFull.clear();
21781
22214
  this._address = address;
21782
22215
  this._chainId = chainId;
@@ -21794,7 +22227,20 @@ var _TinyCloudNode = class _TinyCloudNode {
21794
22227
  this.runtimePermissionGrants = [];
21795
22228
  this.confirmedHostedSpaceIds.clear();
21796
22229
  this.hostedOwnedSpaceIds.clear();
21797
- await this.tc.signIn(options);
22230
+ const signInAttempt = this.latestSessionAttempt;
22231
+ const previousReplicaGate = this.replicaRestoreGate;
22232
+ const previousReplicaStamp = this.replicaSessionStamp;
22233
+ this.replicaRestoreGate = void 0;
22234
+ this.replicaSessionStamp = void 0;
22235
+ try {
22236
+ await performSignIn();
22237
+ } catch (error) {
22238
+ if (epoch === this.sessionEpoch && signInAttempt === this.latestSessionAttempt) {
22239
+ this.replicaRestoreGate = previousReplicaGate;
22240
+ this.replicaSessionStamp = previousReplicaStamp;
22241
+ }
22242
+ throw error;
22243
+ }
21798
22244
  this.assertSessionEpoch(epoch, "Sign-in");
21799
22245
  this.cancelOwnedSpaceHosts();
21800
22246
  this.syncResolvedHostFromAuth();
@@ -21802,8 +22248,9 @@ var _TinyCloudNode = class _TinyCloudNode {
21802
22248
  this.hostedOwnedSpaceIds.add(spaceId);
21803
22249
  }
21804
22250
  this.replicationDelegateSession = void 0;
22251
+ this.replicaSessionPrincipal = (0, import_sdk_core9.pkhDid)(address, chainId);
21805
22252
  this.replicationScopeError = void 0;
21806
- if (this.replicationRuntime) await (await this.replicationRuntime).unbind();
22253
+ await this.quiesceReplicaRuntime();
21807
22254
  this.assertSessionEpoch(epoch, "Sign-in");
21808
22255
  const oldGraph = this._serviceGraph;
21809
22256
  this._serviceGraph = this.createServiceGraphLifetime();
@@ -22055,7 +22502,7 @@ var _TinyCloudNode = class _TinyCloudNode {
22055
22502
  );
22056
22503
  }
22057
22504
  }
22058
- await new Promise((resolve) => setTimeout(resolve, 100));
22505
+ await new Promise((resolve2) => setTimeout(resolve2, 100));
22059
22506
  for (const step of steps) {
22060
22507
  if (step.kind !== "activate") continue;
22061
22508
  graph.assertActive();
@@ -22390,6 +22837,9 @@ var _TinyCloudNode = class _TinyCloudNode {
22390
22837
  if (this.ownStorageFull) {
22391
22838
  return;
22392
22839
  }
22840
+ if (!this.currentSessionCoversAccountRegistry()) {
22841
+ return;
22842
+ }
22393
22843
  const controller = new AbortController();
22394
22844
  const deadline = setTimeout(
22395
22845
  () => controller.abort(new DOMException("Account registry deadline exceeded", "TimeoutError")),
@@ -22504,6 +22954,27 @@ var _TinyCloudNode = class _TinyCloudNode {
22504
22954
  (operation) => operation.service === "space" && this.actionContains(operation.action, "tinycloud.space/list")
22505
22955
  );
22506
22956
  }
22957
+ /**
22958
+ * Whether the current primary session's recap covers the account index read
22959
+ * (`tinycloud.sql/read` on the owned account space's `account` database),
22960
+ * the first call of every registry sync. As in
22961
+ * {@link currentSessionCanListSpaces}, a session with no parseable recap
22962
+ * keeps today's behavior.
22963
+ */
22964
+ currentSessionCoversAccountRegistry() {
22965
+ const session = this.currentTinyCloudSession();
22966
+ const operations = session ? this.recapOperationsFromSession(session) : [];
22967
+ if (operations.length === 0) {
22968
+ return true;
22969
+ }
22970
+ const indexRead = {
22971
+ spaceId: this.ownedSpaceId(import_sdk_core9.ACCOUNT_REGISTRY_SPACE),
22972
+ service: "sql",
22973
+ path: "account",
22974
+ action: "tinycloud.sql/read"
22975
+ };
22976
+ return operations.some((granted) => this.operationCovers(granted, indexRead));
22977
+ }
22507
22978
  async withAccountRegistryRetry(task, signal) {
22508
22979
  const delays = [250, 1e3, 3e3];
22509
22980
  const stopped = (detail) => {
@@ -22544,10 +23015,10 @@ var _TinyCloudNode = class _TinyCloudNode {
22544
23015
  }
22545
23016
  lastError = error;
22546
23017
  if (attempt < delays.length - 1) {
22547
- await new Promise((resolve, reject2) => {
23018
+ await new Promise((resolve2, reject2) => {
22548
23019
  const timer = setTimeout(() => {
22549
23020
  signal.removeEventListener("abort", onAbort);
22550
- resolve();
23021
+ resolve2();
22551
23022
  }, delays[attempt]);
22552
23023
  const onAbort = () => {
22553
23024
  clearTimeout(timer);
@@ -22787,7 +23258,7 @@ var _TinyCloudNode = class _TinyCloudNode {
22787
23258
  }
22788
23259
  entry.waiters += 1;
22789
23260
  const shared = entry;
22790
- return new Promise((resolve, reject2) => {
23261
+ return new Promise((resolve2, reject2) => {
22791
23262
  const onAbort = () => {
22792
23263
  reject2(signal.reason);
22793
23264
  shared.waiters -= 1;
@@ -22796,7 +23267,7 @@ var _TinyCloudNode = class _TinyCloudNode {
22796
23267
  if (this.ownedSpaceHosts.get(spaceId) === shared) this.ownedSpaceHosts.delete(spaceId);
22797
23268
  };
22798
23269
  signal.addEventListener("abort", onAbort, { once: true });
22799
- shared.promise.then(resolve, reject2).finally(() => signal.removeEventListener("abort", onAbort));
23270
+ shared.promise.then(resolve2, reject2).finally(() => signal.removeEventListener("abort", onAbort));
22800
23271
  });
22801
23272
  }
22802
23273
  /** Session replacement: cancel the replaced session's in-flight hosts and forget them. */
@@ -22970,11 +23441,12 @@ var _TinyCloudNode = class _TinyCloudNode {
22970
23441
  );
22971
23442
  const stagedHost = resolvedHost ?? this.config.host;
22972
23443
  let restoredNodeFeatures;
23444
+ let replicaProtocolMismatch = false;
22973
23445
  if (this.config.replication?.enabled) {
22974
23446
  try {
22975
23447
  restoredNodeFeatures = (await (0, import_sdk_core9.checkNodeInfo)(stagedHost, this.wasmBindings.protocolVersion())).features;
22976
23448
  } catch (error) {
22977
- if (!(error instanceof import_sdk_core9.VersionCheckError)) throw error;
23449
+ replicaProtocolMismatch = error instanceof import_sdk_core9.ProtocolMismatchError;
22978
23450
  }
22979
23451
  }
22980
23452
  const stagedAddress = restoredAddress;
@@ -22982,28 +23454,55 @@ var _TinyCloudNode = class _TinyCloudNode {
22982
23454
  let replicationPrincipal;
22983
23455
  let compactReplicationSession;
22984
23456
  let replicationScopeError;
23457
+ let replicationScopeUnavailable = false;
22985
23458
  if (this.config.replication?.enabled) {
22986
23459
  if (stagedAddress !== void 0) {
22987
23460
  replicationPrincipal = (0, import_sdk_core9.pkhDid)(stagedAddress, stagedChainId);
22988
23461
  } else {
22989
- const { replicationScopeFromSignedSession: replicationScopeFromSignedSession2 } = await getNodeReplicationLoaders().authority();
22990
- const signedScope = replicationScopeFromSignedSession2({
22991
- delegationHeader: sessionData.delegationHeader,
22992
- delegationCid: sessionData.delegationCid,
22993
- verificationMethod: restoredVerificationMethod,
22994
- persistedSpace: sessionData.spaceId
22995
- });
22996
- if (!signedScope) {
22997
- replicationScopeError = "Persisted space does not match the locally verified delegation scope.";
22998
- } else {
22999
- replicationPrincipal = signedScope.principal;
23000
- compactReplicationSession = {
23462
+ try {
23463
+ const { replicationScopeFromSignedSession: replicationScopeFromSignedSession2 } = await getNodeReplicationLoaders().authority();
23464
+ const signedScope = replicationScopeFromSignedSession2({
23001
23465
  delegationHeader: sessionData.delegationHeader,
23002
23466
  delegationCid: sessionData.delegationCid,
23003
- spaceId: signedScope.space,
23004
23467
  verificationMethod: restoredVerificationMethod,
23005
- jwk: stagedJwk
23006
- };
23468
+ persistedSpace: sessionData.spaceId
23469
+ });
23470
+ if (!signedScope) {
23471
+ replicationScopeError = "Persisted space does not match the locally verified delegation scope.";
23472
+ } else {
23473
+ replicationPrincipal = signedScope.principal;
23474
+ compactReplicationSession = {
23475
+ delegationHeader: sessionData.delegationHeader,
23476
+ delegationCid: sessionData.delegationCid,
23477
+ spaceId: signedScope.space,
23478
+ verificationMethod: restoredVerificationMethod,
23479
+ jwk: stagedJwk
23480
+ };
23481
+ }
23482
+ } catch {
23483
+ replicationScopeUnavailable = true;
23484
+ replicationScopeError = "Replica scope could not be verified for this network session.";
23485
+ }
23486
+ }
23487
+ }
23488
+ let replicaRestoreGate;
23489
+ let restoredReplicaStamp;
23490
+ const replication = this.config.replication;
23491
+ if (replication?.enabled) {
23492
+ if (replicaProtocolMismatch) {
23493
+ replicaRestoreGate = "gate_unavailable";
23494
+ } else if (replicationPrincipal === void 0) {
23495
+ replicaRestoreGate = replicationScopeUnavailable ? "gate_unavailable" : "principal_mismatch";
23496
+ } else {
23497
+ try {
23498
+ const { restoreReplicaSessionStamp: restoreReplicaSessionStamp2 } = await getNodeReplicationLoaders().authority();
23499
+ restoredReplicaStamp = await restoreReplicaSessionStamp2(
23500
+ replication.storage,
23501
+ replicationPrincipal,
23502
+ sessionData.replicaStamp ?? void 0
23503
+ );
23504
+ } catch (error) {
23505
+ replicaRestoreGate = replicaRestoreRefusal(error);
23007
23506
  }
23008
23507
  }
23009
23508
  }
@@ -23096,7 +23595,10 @@ var _TinyCloudNode = class _TinyCloudNode {
23096
23595
  }
23097
23596
  this.replicationDelegateSession = compactReplicationSession;
23098
23597
  this.replicationScopeError = replicationScopeError;
23099
- if (this.replicationRuntime) await (await this.replicationRuntime).unbind();
23598
+ this.replicaRestoreGate = replicaRestoreGate;
23599
+ this.replicaSessionPrincipal = replicationPrincipal;
23600
+ this.replicaSessionStamp = restoredReplicaStamp;
23601
+ await this.quiesceReplicaRuntime();
23100
23602
  if (epoch !== this.sessionEpoch) {
23101
23603
  oldGraph.retire();
23102
23604
  oldCore?.retireServices?.();
@@ -23111,7 +23613,7 @@ var _TinyCloudNode = class _TinyCloudNode {
23111
23613
  detail: replicationScopeError
23112
23614
  });
23113
23615
  }
23114
- await this.bindReplicationRuntime(stagedGraph.serviceContext, serviceSession, stagedGraph.kv, replicationPrincipal);
23616
+ await this.bindReplicaOrGate(stagedGraph.serviceContext, serviceSession, stagedGraph.kv, replicationPrincipal);
23115
23617
  oldCore?.retireServices?.();
23116
23618
  if (epoch !== this.sessionEpoch) {
23117
23619
  if (attempt === this.latestSessionAttempt) await this.endSession();
@@ -23244,6 +23746,7 @@ var _TinyCloudNode = class _TinyCloudNode {
23244
23746
  session: input.serviceSession,
23245
23747
  invoke: graph.invoke,
23246
23748
  invokeAny: graph.invokeAny,
23749
+ signOwnerRevocation: this.signer ? (cid) => this.signOwnerRevocation(cid) : void 0,
23247
23750
  fetch: graph.fetch
23248
23751
  });
23249
23752
  const keyProvider = new WasmKeyProvider({ sessionManager: input.manager });
@@ -23587,6 +24090,7 @@ var _TinyCloudNode = class _TinyCloudNode {
23587
24090
  signer: this.signer,
23588
24091
  signStrategy: this.config.signStrategy ?? { type: "auto-sign" },
23589
24092
  wasmBindings: this.wasmBindings,
24093
+ ...this.config.replication?.enabled === true ? { establishReplicaStamp: this.establishReplicaStamp } : {},
23590
24094
  sessionManager: this.sessionManager,
23591
24095
  sessionStorage: options?.sessionStorage ?? this.config.sessionStorage ?? new MemorySessionStorage(),
23592
24096
  domain: this.siweDomain,
@@ -23645,6 +24149,7 @@ var _TinyCloudNode = class _TinyCloudNode {
23645
24149
  signer: this.signer,
23646
24150
  signStrategy: this.config.signStrategy ?? { type: "auto-sign" },
23647
24151
  wasmBindings: this.wasmBindings,
24152
+ ...this.config.replication?.enabled === true ? { establishReplicaStamp: this.establishReplicaStamp } : {},
23648
24153
  sessionManager: this.sessionManager,
23649
24154
  sessionStorage: options?.sessionStorage ?? this.config.sessionStorage ?? new MemorySessionStorage(),
23650
24155
  domain: this.siweDomain,
@@ -23725,7 +24230,7 @@ var _TinyCloudNode = class _TinyCloudNode {
23725
24230
  jwk: session.jwk
23726
24231
  };
23727
24232
  this._serviceContext.setSession(serviceSession);
23728
- await this.bindReplicationRuntime(this._serviceContext, serviceSession, this._kv);
24233
+ await this.bindReplicaOrGate(this._serviceContext, serviceSession, this._kv);
23729
24234
  this.tc.serviceContext.setSession(serviceSession);
23730
24235
  this._vault = this.createVaultService(session.spaceId, this._kv);
23731
24236
  this._vault.initialize(this._serviceContext);
@@ -24119,6 +24624,7 @@ var _TinyCloudNode = class _TinyCloudNode {
24119
24624
  session: serviceSession,
24120
24625
  invoke: graph.invoke,
24121
24626
  invokeAny: graph.invokeAny,
24627
+ signOwnerRevocation: this.signer ? (cid) => this.signOwnerRevocation(cid) : void 0,
24122
24628
  fetch: graph.fetch
24123
24629
  });
24124
24630
  this._spaceService = new import_sdk_core9.SpaceService({
@@ -25411,7 +25917,8 @@ var _TinyCloudNode = class _TinyCloudNode {
25411
25917
  return this.delegationManager.create(params);
25412
25918
  }
25413
25919
  /**
25414
- * Revoke a delegation using the v2 DelegationManager.
25920
+ * Revoke a delegation with the owner wallet when available. Session-only
25921
+ * clients continue to use their delegated revocation authority.
25415
25922
  *
25416
25923
  * @param cid - The CID of the delegation to revoke
25417
25924
  * @returns Result indicating success or failure
@@ -25686,34 +26193,67 @@ var _TinyCloudNode = class _TinyCloudNode {
25686
26193
  return auth.replicationSignInEntries();
25687
26194
  }
25688
26195
  async replicationAuthority() {
25689
- const { createReplicationAuthority: createReplicationAuthority2 } = await getNodeReplicationLoaders().authority();
26196
+ const { createReplicationAuthority: createReplicationAuthority2, mintNodeReplicationGrant: mintNodeReplicationGrant2, verifiedOwnerConsentAnchor: verifiedOwnerConsentAnchor2 } = await getNodeReplicationLoaders().authority();
25690
26197
  return createReplicationAuthority2({
25691
26198
  replicationSession: () => this.currentTinyCloudSession() ?? this.replicationDelegateSession,
26199
+ verifiedConsentAnchor: (session) => {
26200
+ const current = this.currentTinyCloudSession();
26201
+ return session === current && current !== void 0 ? verifiedOwnerConsentAnchor2(current, this.wasmBindings, exactVerifiedRecap) : null;
26202
+ },
25692
26203
  siweExpiration: (siwe) => extractSiweExpiration(siwe),
25693
26204
  planDelegation: (entries, options) => this.planDelegation(entries, options),
25694
- mintDelegation: async (deviceDid, entries) => {
25695
- const result = await this.delegateTo((0, import_sdk_core9.principalDid)(deviceDid), entries);
25696
- return {
25697
- ucan: result.delegation.delegationHeader.Authorization,
25698
- expiresAt: result.delegation.expiry.getTime()
25699
- };
25700
- }
26205
+ mintDelegation: (deviceDid, entries, options) => mintNodeReplicationGrant2(
26206
+ deviceDid,
26207
+ entries,
26208
+ options,
26209
+ options?.renewalOnly ? this.currentTinyCloudSession() : void 0,
26210
+ this
26211
+ )
25701
26212
  });
25702
26213
  }
25703
- async bindReplicationRuntime(context, session, kv, principal) {
26214
+ async quiesceReplicaRuntime() {
25704
26215
  if (!this.replicationRuntime) return;
25705
- const identityPrincipal = principal ?? (this._address === void 0 ? void 0 : (0, import_sdk_core9.pkhDid)(this._address, this._chainId));
25706
- if (identityPrincipal === void 0) return;
25707
- const runtime = await this.replicationRuntime;
25708
- const authority = await this.replicationAuthority();
25709
- await runtime.bind({
25710
- context,
25711
- session,
25712
- principal: identityPrincipal,
25713
- authority,
25714
- nodeFeatures: this.replicationNodeFeatures,
25715
- primaryKV: [{ space: session.spaceId, kv }]
25716
- });
26216
+ try {
26217
+ await (await this.replicationRuntime).unbind();
26218
+ } catch {
26219
+ this.replicaRestoreGate = "gate_unavailable";
26220
+ }
26221
+ }
26222
+ async bindReplicaOrGate(context, session, kv, principal) {
26223
+ if (!this.replicationRuntime) return;
26224
+ const gate = this.replicaRestoreGate;
26225
+ const epoch = this.sessionEpoch;
26226
+ const attempt = this.latestSessionAttempt;
26227
+ try {
26228
+ const runtime = await this.replicationRuntime;
26229
+ if (epoch !== this.sessionEpoch || attempt !== this.latestSessionAttempt || this._kv !== kv || this._serviceContext !== context || gate !== this.replicaRestoreGate) return;
26230
+ if (gate) {
26231
+ await runtime.bindGatedNetworkOnly({
26232
+ context,
26233
+ session,
26234
+ gate,
26235
+ primaryKV: [{ space: session.spaceId, kv }]
26236
+ }, getNodeReplicationLoaders().gatedNetwork);
26237
+ return;
26238
+ }
26239
+ const identityPrincipal = principal ?? (this._address === void 0 ? void 0 : (0, import_sdk_core9.pkhDid)(this._address, this._chainId));
26240
+ if (identityPrincipal === void 0) return;
26241
+ const authority = await this.replicationAuthority();
26242
+ if (epoch !== this.sessionEpoch || attempt !== this.latestSessionAttempt || this._kv !== kv || this._serviceContext !== context) return;
26243
+ await runtime.bind({
26244
+ context,
26245
+ session,
26246
+ principal: identityPrincipal,
26247
+ stamp: this.replicaSessionStamp,
26248
+ authority,
26249
+ nodeFeatures: this.replicationNodeFeatures,
26250
+ primaryKV: [{ space: session.spaceId, kv }]
26251
+ });
26252
+ } catch {
26253
+ if (epoch !== this.sessionEpoch || attempt !== this.latestSessionAttempt || this._kv !== kv || this._serviceContext !== context) return;
26254
+ this.replicaRestoreGate = "gate_unavailable";
26255
+ await this.quiesceReplicaRuntime();
26256
+ }
25717
26257
  }
25718
26258
  /** Stable control facade for this node's optional replication runtime. */
25719
26259
  get replication() {
@@ -26862,9 +27402,16 @@ TinyCloudNode.registerNodeDefaults({
26862
27402
  });
26863
27403
 
26864
27404
  // src/replication/node-loader.ts
27405
+ init_config();
26865
27406
  registerReplicationLoaders({
27407
+ validateConfig: assertValidReplicationConfig,
26866
27408
  runtime: () => Promise.resolve().then(() => (init_runtime(), runtime_exports)),
26867
- authority: () => Promise.resolve().then(() => (init_authority(), authority_exports))
27409
+ authority: () => Promise.resolve().then(() => (init_authority(), authority_exports)),
27410
+ gatedNetwork: () => Promise.resolve().then(() => (init_gated_network(), gated_network_exports)),
27411
+ // Lazy by contract: replica loads only when replication is enabled. Literal specifier typed by
27412
+ // ProfileGateModule; TS2307 under moduleResolution "node" is suppressed (see replica-contract.ts).
27413
+ // @ts-ignore
27414
+ gate: () => import("@tinycloud/replica/profile-gate")
26868
27415
  });
26869
27416
 
26870
27417
  // src/index.ts
@@ -27050,12 +27597,515 @@ var import_sdk_core26 = require("@tinycloud/sdk-core");
27050
27597
  var import_sdk_core27 = require("@tinycloud/sdk-core");
27051
27598
 
27052
27599
  // src/replication/sqlite.ts
27600
+ var import_node_async_hooks = require("async_hooks");
27053
27601
  var import_node_crypto2 = require("crypto");
27602
+ var import_node_fs = require("fs");
27054
27603
  var import_promises = require("fs/promises");
27055
27604
  var import_node_path = require("path");
27056
27605
  var import_sdk_core13 = require("@tinycloud/sdk-core");
27057
27606
  var import_sdk_services3 = require("@tinycloud/sdk-services");
27607
+ var import_contractConstants = require("@tinycloud/sdk-services/kv/replication/contractConstants");
27058
27608
  init_authority();
27609
+ var REPLICA_LOGOUT_PENDING_FILE = "replica-logout-pending.json";
27610
+ function assertProfileLogoutClear(profileDir) {
27611
+ assertLogoutClear((0, import_node_path.join)(profileDir, REPLICA_LOGOUT_PENDING_FILE));
27612
+ }
27613
+ function assertLogoutClear(path) {
27614
+ if (path === void 0) return;
27615
+ try {
27616
+ (0, import_node_fs.lstatSync)(path);
27617
+ } catch (error) {
27618
+ if (isNotFound(error)) return;
27619
+ }
27620
+ throw gated("gate_unavailable");
27621
+ }
27622
+ function safeInventoryPath(path) {
27623
+ 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/"));
27624
+ }
27625
+ function logoutPrincipalDigest(principal) {
27626
+ return (0, import_node_crypto2.createHash)("sha256").update("tinycloud-replica-logout\0").update(principal, "utf8").digest("hex");
27627
+ }
27628
+ function compareInventoryString(left, right) {
27629
+ return left < right ? -1 : left > right ? 1 : 0;
27630
+ }
27631
+ function approvalEntries(report) {
27632
+ return [
27633
+ ...report.unknownEntries,
27634
+ ...report.knownEntries,
27635
+ ...report.knownRoots.map((root) => ({ ...root, kind: "directory" })),
27636
+ ...report.missingDatabases.map((path) => ({ path, kind: "missing", dev: "", ino: "" }))
27637
+ ].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));
27638
+ }
27639
+ function inventoryApprovalToken(marker, principal, entries) {
27640
+ const markerId = marker.v === 2 ? [marker.v, marker.nonce, marker.principalDigest] : [marker.v, marker.incarnation, marker.lifecycleEpoch, marker.principalDigest];
27641
+ return (0, import_node_crypto2.createHash)("sha256").update("tc-logout-approve-v1\0").update(JSON.stringify([
27642
+ markerId,
27643
+ marker.approvalSalt,
27644
+ principal,
27645
+ marker.v === 2 ? null : marker.lifecycleEpoch,
27646
+ entries
27647
+ ])).digest().subarray(0, 16).toString("hex");
27648
+ }
27649
+ function changedInventoryRefusal(marker, principal, report, entries = approvalEntries(report)) {
27650
+ const token = inventoryApprovalToken(marker, principal, entries);
27651
+ const error = localChangesRefusal(report.unknownPaths);
27652
+ 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}.`;
27653
+ return Object.assign(error, { inventoryApprovalToken: token, inventoryApprovalEntries: entries });
27654
+ }
27655
+ function storedInventoryChanged(marker, report) {
27656
+ try {
27657
+ assertApprovedInventory(report, {
27658
+ unknownEntries: marker.approvedUnknown,
27659
+ knownRoots: marker.knownRoots,
27660
+ knownEntries: marker.knownEntries,
27661
+ missingDatabases: marker.missingDatabases
27662
+ });
27663
+ return false;
27664
+ } catch (error) {
27665
+ if (typeof error === "object" && error !== null && "code" in error && error.code === "REPLICA_GATED" && "gate" in error && error.gate === "gate_unavailable") {
27666
+ return true;
27667
+ }
27668
+ throw error;
27669
+ }
27670
+ }
27671
+ function decimalIdentity(value) {
27672
+ return typeof value === "string" && /^(0|[1-9][0-9]*)$/.test(value);
27673
+ }
27674
+ function logoutMarkerOf(value) {
27675
+ if (value === null || typeof value !== "object" || Array.isArray(value)) throw gated("gate_unavailable");
27676
+ const row = value;
27677
+ 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);
27678
+ 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);
27679
+ 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(
27680
+ (path) => typeof path === "string" && safeInventoryPath(path) && path.endsWith("/replica.db")
27681
+ ) || 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");
27682
+ const optional = Number(row.discardLocalChanges === true) + Number(row.remotePending === true) + Number(row.remoteAbandoned === true);
27683
+ 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;
27684
+ 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) {
27685
+ return row;
27686
+ }
27687
+ const bound = Number(row.deniedEpoch !== void 0);
27688
+ 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)) {
27689
+ throw gated("gate_unavailable");
27690
+ }
27691
+ return row;
27692
+ }
27693
+ async function readLogoutMarker(path) {
27694
+ const value = await readJsonFile(path);
27695
+ return value === void 0 ? null : logoutMarkerOf(value);
27696
+ }
27697
+ async function orphanRoot(profileDir, name) {
27698
+ let profile;
27699
+ try {
27700
+ profile = await (0, import_promises.lstat)(profileDir);
27701
+ } catch {
27702
+ throw gated("gate_unavailable");
27703
+ }
27704
+ let pinned = false;
27705
+ if (process.platform === "linux" && /^\/proc\/self\/fd\/[1-9]\d*$/.test(profileDir) && profile.isSymbolicLink()) {
27706
+ try {
27707
+ pinned = (await (0, import_promises.stat)(profileDir)).isDirectory();
27708
+ } catch {
27709
+ throw gated("gate_unavailable");
27710
+ }
27711
+ }
27712
+ if (!profile.isDirectory() && !pinned) throw gated("gate_unavailable");
27713
+ const root = (0, import_node_path.join)(profileDir, name);
27714
+ let entry;
27715
+ try {
27716
+ entry = await (0, import_promises.lstat)(root);
27717
+ } catch (error) {
27718
+ if (isNotFound(error)) return null;
27719
+ throw error;
27720
+ }
27721
+ if (!entry.isDirectory() || await (0, import_promises.realpath)(root) !== (0, import_node_path.join)(await (0, import_promises.realpath)(profileDir), name)) {
27722
+ throw gated("gate_unavailable");
27723
+ }
27724
+ return root;
27725
+ }
27726
+ async function hasOrphanPending(profileDir) {
27727
+ const root = await orphanRoot(profileDir, "replication");
27728
+ if (root === null) return false;
27729
+ let entries;
27730
+ try {
27731
+ entries = await (0, import_promises.readdir)(root, { withFileTypes: true });
27732
+ } catch {
27733
+ return true;
27734
+ }
27735
+ for (const entry of entries) {
27736
+ if (!entry.isDirectory()) return true;
27737
+ const dir = (0, import_node_path.join)(root, entry.name);
27738
+ try {
27739
+ if (!(await (0, import_promises.lstat)(dir)).isDirectory()) return true;
27740
+ } catch {
27741
+ return true;
27742
+ }
27743
+ if (!/^[a-z2-7]{26}$/.test(entry.name)) return true;
27744
+ let children;
27745
+ try {
27746
+ children = await (0, import_promises.readdir)(dir, { withFileTypes: true });
27747
+ } catch {
27748
+ return true;
27749
+ }
27750
+ if (children.length !== 2 || children.some((child) => !child.isFile() || child.name !== "identity.json" && child.name !== "pending.json")) return true;
27751
+ const pendingFile = (0, import_node_path.join)(dir, "pending.json");
27752
+ let text;
27753
+ try {
27754
+ const pending = await (0, import_promises.lstat)(pendingFile);
27755
+ if (!pending.isFile()) return true;
27756
+ text = await (0, import_promises.readFile)(pendingFile, "utf8");
27757
+ } catch (error) {
27758
+ if (isNotFound(error)) return true;
27759
+ return true;
27760
+ }
27761
+ try {
27762
+ const identityFile = (0, import_node_path.join)(dir, "identity.json");
27763
+ if (!(await (0, import_promises.lstat)(identityFile)).isFile()) return true;
27764
+ const identity = identityRecordOf(await readJsonFile(identityFile));
27765
+ if (identity === void 0 || parsePendingState(text, pendingFile, identity).records.length !== 0) {
27766
+ return true;
27767
+ }
27768
+ } catch {
27769
+ return true;
27770
+ }
27771
+ }
27772
+ return false;
27773
+ }
27774
+ function inventoryIdentity(path, dev, ino) {
27775
+ return { path, dev: dev.toString(), ino: ino.toString() };
27776
+ }
27777
+ async function inventoryUnknown(profileDir, relative, entries) {
27778
+ const path = (0, import_node_path.join)(profileDir, relative);
27779
+ const info = await (0, import_promises.lstat)(path, { bigint: true });
27780
+ const kind = info.isDirectory() ? "directory" : info.isSymbolicLink() ? "symlink" : "file";
27781
+ entries.push({ ...inventoryIdentity(relative, info.dev, info.ino), kind });
27782
+ if (kind === "directory") {
27783
+ for (const child of await (0, import_promises.readdir)(path)) await inventoryUnknown(profileDir, `${relative}/${child}`, entries);
27784
+ }
27785
+ }
27786
+ async function inspectProfileReplicaLayout(profileDir, gateReadable, approvedMarker, legacyOnly = false, diagnosticFiles = []) {
27787
+ const report = {
27788
+ knownLegacyReplicas: 0,
27789
+ unknownLegacyPaths: [],
27790
+ unknownPaths: [],
27791
+ unknownEntries: [],
27792
+ knownRoots: [],
27793
+ knownEntries: [],
27794
+ missingDatabases: []
27795
+ };
27796
+ const legacy = await orphanRoot(profileDir, "replicas");
27797
+ const active = legacyOnly ? null : await orphanRoot(profileDir, "replication");
27798
+ for (const [name, dir] of [["replicas", legacy], ["replication", active]]) {
27799
+ if (dir === null) continue;
27800
+ const info = await (0, import_promises.lstat)(dir, { bigint: true });
27801
+ report.knownRoots.push(inventoryIdentity(name, info.dev, info.ino));
27802
+ }
27803
+ if (legacy !== null) {
27804
+ const sqlite = await import("@tinycloud/replica/sqlite");
27805
+ for (const entry of await (0, import_promises.readdir)(legacy, { withFileTypes: true })) {
27806
+ const relative = `replicas/${entry.name}`;
27807
+ const child = (0, import_node_path.join)(legacy, entry.name);
27808
+ if (!entry.isDirectory()) {
27809
+ await inventoryUnknown(profileDir, relative, report.unknownEntries);
27810
+ continue;
27811
+ }
27812
+ try {
27813
+ const layout = await sqlite.inspectSqliteReplicaLayout(child);
27814
+ report.knownRoots.push({ path: relative, ...layout.rootIdentity });
27815
+ report.unknownEntries.push(...layout.unknownEntries.map((item) => ({
27816
+ ...item,
27817
+ path: `${relative}/${item.path}`
27818
+ })));
27819
+ report.knownEntries.push(...layout.knownEntries.map((item) => ({
27820
+ ...item,
27821
+ path: `${relative}/${item.path}`
27822
+ })));
27823
+ if (layout.unknownPaths.length === 0) report.knownLegacyReplicas++;
27824
+ if (layout.unknownPaths.includes("replica.db") && !layout.unknownEntries.some((item) => item.path === "replica.db")) {
27825
+ report.missingDatabases.push(`${relative}/replica.db`);
27826
+ }
27827
+ } catch {
27828
+ await inventoryUnknown(profileDir, relative, report.unknownEntries);
27829
+ }
27830
+ }
27831
+ }
27832
+ if (active !== null) {
27833
+ const cleanOrphan = !gateReadable && (approvedMarker?.v === 2 || !await hasOrphanPending(profileDir));
27834
+ for (const entry of await (0, import_promises.readdir)(active, { withFileTypes: true })) {
27835
+ const relative = `replication/${entry.name}`;
27836
+ const dir = (0, import_node_path.join)(active, entry.name);
27837
+ if (approvedMarker?.approvedUnknown.some((item) => item.path === relative)) {
27838
+ await inventoryUnknown(profileDir, relative, report.unknownEntries);
27839
+ continue;
27840
+ }
27841
+ if (gateReadable && diagnosticFiles.includes(entry.name)) {
27842
+ const info2 = await (0, import_promises.lstat)(dir, { bigint: true });
27843
+ if (info2.isFile()) {
27844
+ report.knownEntries.push({ ...inventoryIdentity(relative, info2.dev, info2.ino), kind: "file" });
27845
+ continue;
27846
+ }
27847
+ }
27848
+ if (!gateReadable && !cleanOrphan || !entry.isDirectory() || !/^[a-z2-7]{26}$/.test(entry.name)) {
27849
+ await inventoryUnknown(profileDir, relative, report.unknownEntries);
27850
+ continue;
27851
+ }
27852
+ const info = await (0, import_promises.lstat)(dir, { bigint: true });
27853
+ report.knownRoots.push(inventoryIdentity(relative, info.dev, info.ino));
27854
+ const identityPath = (0, import_node_path.join)(dir, "identity.json");
27855
+ let identity;
27856
+ try {
27857
+ if ((await (0, import_promises.lstat)(identityPath)).isFile()) identity = identityRecordOf(await readJsonFile(identityPath));
27858
+ } catch {
27859
+ }
27860
+ if (identity === void 0) {
27861
+ const approvedRoot = approvedMarker?.knownRoots.find((item) => item.path === relative);
27862
+ if (approvedRoot?.dev === info.dev.toString() && approvedRoot.ino === info.ino.toString()) {
27863
+ const children = await (0, import_promises.readdir)(dir);
27864
+ if (children.length === 0) continue;
27865
+ if (children.length === 1 && children[0] === "replicas") {
27866
+ const replicasPath = `${relative}/replicas`;
27867
+ const replicasDir = (0, import_node_path.join)(dir, "replicas");
27868
+ const replicasInfo = await (0, import_promises.lstat)(replicasDir, { bigint: true });
27869
+ const approvedReplicas = approvedMarker.knownRoots.find((item) => item.path === replicasPath);
27870
+ if (replicasInfo.isDirectory() && !replicasInfo.isSymbolicLink() && approvedReplicas?.dev === replicasInfo.dev.toString() && approvedReplicas.ino === replicasInfo.ino.toString() && (await (0, import_promises.readdir)(replicasDir)).length === 0) {
27871
+ report.knownRoots.push(inventoryIdentity(replicasPath, replicasInfo.dev, replicasInfo.ino));
27872
+ continue;
27873
+ }
27874
+ }
27875
+ }
27876
+ report.knownRoots.pop();
27877
+ await inventoryUnknown(profileDir, relative, report.unknownEntries);
27878
+ continue;
27879
+ }
27880
+ for (const child of await (0, import_promises.readdir)(dir, { withFileTypes: true })) {
27881
+ const childRelative = `${relative}/${child.name}`;
27882
+ const childPath = (0, import_node_path.join)(dir, child.name);
27883
+ if ((child.name === "identity.json" || child.name === "device.jwk") && child.isFile()) {
27884
+ const childInfo = await (0, import_promises.lstat)(childPath, { bigint: true });
27885
+ report.knownEntries.push({ ...inventoryIdentity(childRelative, childInfo.dev, childInfo.ino), kind: "file" });
27886
+ continue;
27887
+ }
27888
+ if (child.name === "pending.json" && child.isFile()) {
27889
+ try {
27890
+ if (parsePendingState(await (0, import_promises.readFile)(childPath, "utf8"), childPath, identity).records.length === 0) {
27891
+ const childInfo = await (0, import_promises.lstat)(childPath, { bigint: true });
27892
+ report.knownEntries.push({ ...inventoryIdentity(childRelative, childInfo.dev, childInfo.ino), kind: "file" });
27893
+ continue;
27894
+ }
27895
+ } catch {
27896
+ }
27897
+ }
27898
+ if (child.name === "replicas" && child.isDirectory()) {
27899
+ const replicasInfo = await (0, import_promises.lstat)(childPath, { bigint: true });
27900
+ report.knownRoots.push(inventoryIdentity(childRelative, replicasInfo.dev, replicasInfo.ino));
27901
+ for (const replicaChild of await (0, import_promises.readdir)(childPath, { withFileTypes: true })) {
27902
+ const replicaRelative = `${childRelative}/${replicaChild.name}`;
27903
+ if (!replicaChild.isDirectory() || !/^[a-z2-7]{26}$/.test(replicaChild.name)) {
27904
+ await inventoryUnknown(profileDir, replicaRelative, report.unknownEntries);
27905
+ continue;
27906
+ }
27907
+ const sqlite = await import("@tinycloud/replica/sqlite");
27908
+ try {
27909
+ const layout = await sqlite.inspectSqliteReplicaLayout((0, import_node_path.join)(childPath, replicaChild.name), {
27910
+ allowAdapterFence: true
27911
+ });
27912
+ report.knownRoots.push({ path: replicaRelative, ...layout.rootIdentity });
27913
+ report.unknownEntries.push(...layout.unknownEntries.map((item) => ({
27914
+ ...item,
27915
+ path: `${replicaRelative}/${item.path}`
27916
+ })));
27917
+ report.knownEntries.push(...layout.knownEntries.map((item) => ({
27918
+ ...item,
27919
+ path: `${replicaRelative}/${item.path}`
27920
+ })));
27921
+ if (layout.unknownPaths.includes("replica.db") && !layout.unknownEntries.some((item) => item.path === "replica.db")) {
27922
+ report.missingDatabases.push(`${replicaRelative}/replica.db`);
27923
+ }
27924
+ } catch {
27925
+ await inventoryUnknown(profileDir, replicaRelative, report.unknownEntries);
27926
+ }
27927
+ }
27928
+ continue;
27929
+ }
27930
+ await inventoryUnknown(profileDir, childRelative, report.unknownEntries);
27931
+ }
27932
+ }
27933
+ }
27934
+ report.unknownEntries.sort((a, b) => a.path.localeCompare(b.path));
27935
+ report.knownEntries.sort((a, b) => a.path.localeCompare(b.path));
27936
+ report.knownRoots.sort((a, b) => a.path.localeCompare(b.path));
27937
+ report.missingDatabases.sort();
27938
+ report.unknownPaths = [...report.unknownEntries.map((entry) => entry.path), ...report.missingDatabases].sort();
27939
+ report.unknownLegacyPaths = [...report.unknownPaths];
27940
+ return report;
27941
+ }
27942
+ function assertApprovedInventory(current, approval, allowRemoved = false) {
27943
+ const approved = new Map(approval.unknownEntries.map((item) => [item.path, item]));
27944
+ const known = new Map(approval.knownRoots.map((item) => [item.path, item]));
27945
+ const knownFiles = new Map(approval.knownEntries.map((item) => [item.path, item]));
27946
+ for (const entry of current.unknownEntries) {
27947
+ const expected = approved.get(entry.path);
27948
+ if (expected?.kind !== entry.kind || expected.dev !== entry.dev || expected.ino !== entry.ino) {
27949
+ throw localChangesRefusal(current.unknownPaths);
27950
+ }
27951
+ }
27952
+ for (const root of current.knownRoots) {
27953
+ const expected = known.get(root.path);
27954
+ if (expected?.dev !== root.dev || expected.ino !== root.ino) throw localChangesRefusal([root.path]);
27955
+ }
27956
+ for (const entry of current.knownEntries) {
27957
+ if (allowRemoved && entry.kind === "file" && /^(?:replication\/[a-z2-7]{26}\/replicas\/[a-z2-7]{26}|replicas\/[^/]+)\/replica\.db-(?:wal|shm|journal)$/.test(entry.path)) {
27958
+ const database = entry.path.replace(/-(?:wal|shm|journal)$/, "");
27959
+ const original = knownFiles.get(database);
27960
+ const present = current.knownEntries.find((item) => item.path === database);
27961
+ if (!approved.has(entry.path) && original?.kind === "file" && present?.dev === original.dev && present.ino === original.ino) continue;
27962
+ }
27963
+ const expected = knownFiles.get(entry.path);
27964
+ if (expected?.kind !== entry.kind || expected.dev !== entry.dev || expected.ino !== entry.ino) {
27965
+ throw localChangesRefusal([entry.path]);
27966
+ }
27967
+ }
27968
+ for (const path of current.missingDatabases) {
27969
+ if (!approval.missingDatabases.includes(path) && !(allowRemoved && knownFiles.get(path)?.kind === "file")) throw localChangesRefusal([path]);
27970
+ }
27971
+ if (!allowRemoved && (current.unknownEntries.length !== approved.size || current.knownRoots.length !== known.size || current.knownEntries.length !== knownFiles.size || current.missingDatabases.length !== approval.missingDatabases.length)) {
27972
+ throw localChangesRefusal(current.unknownPaths);
27973
+ }
27974
+ }
27975
+ async function assertApprovedParents(profileDir, marker, path) {
27976
+ if (process.platform !== "linux") {
27977
+ const absolute = (0, import_node_path.resolve)(profileDir);
27978
+ let ancestor = (0, import_node_path.parse)(absolute).root;
27979
+ for (const part of absolute.slice(ancestor.length).split(import_node_path.sep).filter(Boolean)) {
27980
+ ancestor = (0, import_node_path.join)(ancestor, part);
27981
+ const details = await (0, import_promises.lstat)(ancestor, { bigint: true });
27982
+ if (!details.isDirectory() || details.isSymbolicLink()) throw localChangesRefusal([path]);
27983
+ }
27984
+ }
27985
+ let relativeParent = "";
27986
+ for (const part of path.split("/").slice(0, -1)) {
27987
+ relativeParent = relativeParent ? `${relativeParent}/${part}` : part;
27988
+ let details;
27989
+ try {
27990
+ details = await (0, import_promises.lstat)((0, import_node_path.join)(profileDir, relativeParent), { bigint: true });
27991
+ } catch (error) {
27992
+ if (isNotFound(error)) return false;
27993
+ throw error;
27994
+ }
27995
+ const expected = marker.knownRoots.find((root) => root.path === relativeParent) ?? marker.approvedUnknown.find((item) => item.path === relativeParent && item.kind === "directory");
27996
+ if (!details.isDirectory() || details.isSymbolicLink() || expected?.dev !== details.dev.toString() || expected.ino !== details.ino.toString()) {
27997
+ throw localChangesRefusal([relativeParent]);
27998
+ }
27999
+ }
28000
+ return true;
28001
+ }
28002
+ async function eraseApprovedUnknown(profileDir, marker, prefix, diagnosticFiles) {
28003
+ const entries = marker.approvedUnknown.filter((entry) => entry.path.startsWith(`${prefix}/`));
28004
+ for (const entry of entries.sort((a, b) => b.path.length - a.path.length)) {
28005
+ assertApprovedInventory(await inspectProfileReplicaLayout(profileDir, marker.v !== 2, marker, false, diagnosticFiles), {
28006
+ unknownEntries: marker.approvedUnknown,
28007
+ knownRoots: marker.knownRoots,
28008
+ knownEntries: marker.knownEntries,
28009
+ missingDatabases: marker.missingDatabases
28010
+ }, true);
28011
+ if (!await assertApprovedParents(profileDir, marker, entry.path)) continue;
28012
+ const file = (0, import_node_path.join)(profileDir, entry.path);
28013
+ let current;
28014
+ try {
28015
+ current = await (0, import_promises.lstat)(file, { bigint: true });
28016
+ } catch (error) {
28017
+ if (isNotFound(error)) continue;
28018
+ throw error;
28019
+ }
28020
+ const kind = current.isDirectory() ? "directory" : current.isSymbolicLink() ? "symlink" : "file";
28021
+ if (kind !== entry.kind || current.dev.toString() !== entry.dev || current.ino.toString() !== entry.ino) throw localChangesRefusal([entry.path]);
28022
+ if (process.platform !== "linux" && kind === "symlink") throw localChangesRefusal([entry.path]);
28023
+ if (kind === "directory") await (0, import_promises.rmdir)(file);
28024
+ else await (0, import_promises.rm)(file);
28025
+ }
28026
+ }
28027
+ async function eraseKnownFlagResidue(profileDir, marker, diagnosticFiles) {
28028
+ const diagnostics = new Set(diagnosticFiles.map((name) => `replication/${name}`));
28029
+ 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")))) {
28030
+ if (!await assertApprovedParents(profileDir, marker, entry.path)) continue;
28031
+ const file = (0, import_node_path.join)(profileDir, entry.path);
28032
+ let details;
28033
+ try {
28034
+ details = await (0, import_promises.lstat)(file, { bigint: true });
28035
+ } catch (error) {
28036
+ if (isNotFound(error)) continue;
28037
+ throw error;
28038
+ }
28039
+ if (!details.isFile() || details.dev.toString() !== entry.dev || details.ino.toString() !== entry.ino) throw localChangesRefusal([entry.path]);
28040
+ await (0, import_promises.rm)(file);
28041
+ }
28042
+ const roots = marker.knownRoots.filter((item) => item.path === "replication" || item.path.startsWith("replication/"));
28043
+ roots.sort((a, b) => b.path.split("/").length - a.path.split("/").length);
28044
+ for (const entry of roots) {
28045
+ if (!await assertApprovedParents(profileDir, marker, entry.path)) continue;
28046
+ const path = (0, import_node_path.join)(profileDir, entry.path);
28047
+ let details;
28048
+ try {
28049
+ details = await (0, import_promises.lstat)(path, { bigint: true });
28050
+ } catch (error) {
28051
+ if (isNotFound(error)) continue;
28052
+ throw error;
28053
+ }
28054
+ if (!details.isDirectory() || details.isSymbolicLink() || details.dev.toString() !== entry.dev || details.ino.toString() !== entry.ino) {
28055
+ throw localChangesRefusal([entry.path]);
28056
+ }
28057
+ await (0, import_promises.rmdir)(path);
28058
+ }
28059
+ }
28060
+ function localChangesRefusal(unknownPaths = []) {
28061
+ const error = gated("gate_unavailable");
28062
+ 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(", ")}.`);
28063
+ return error;
28064
+ }
28065
+ function gateOwnedByAnother(state, principalHash) {
28066
+ return state.principalHash !== null ? state.principalHash !== principalHash : state.eraseDeferred && state.deferredPrincipalHash !== principalHash;
28067
+ }
28068
+ function isGateUnavailable(error) {
28069
+ return typeof error === "object" && error !== null && "code" in error && error.code === "REPLICA_GATED" && "gate" in error && error.gate === "gate_unavailable";
28070
+ }
28071
+ function restoredGate() {
28072
+ const error = gated("stamp_stale");
28073
+ error.message = RESTORED_GATE_MESSAGE;
28074
+ return Object.assign(error, { reportCode: "gate_restored" });
28075
+ }
28076
+ 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.";
28077
+ function interruptedSignOut() {
28078
+ const error = gated("gate_unavailable");
28079
+ error.message = "A previous sign-out was interrupted; re-running `tc auth logout` completes it.";
28080
+ return error;
28081
+ }
28082
+ function assertSupportedProfileLayout(report) {
28083
+ if (process.platform === "linux") return;
28084
+ const link = report.unknownEntries.find((entry) => entry.kind === "symlink");
28085
+ if (link === void 0) return;
28086
+ const error = gated("gate_unavailable");
28087
+ error.message = `Replica symlink ${JSON.stringify(link.path)} cannot be removed safely on this platform.`;
28088
+ throw error;
28089
+ }
28090
+ async function fileGate() {
28091
+ return import("@tinycloud/replica/profile-gate");
28092
+ }
28093
+ function gated(reason) {
28094
+ return Object.assign(new Error(`Replica gate refused: ${reason}`), { code: "REPLICA_GATED", gate: reason });
28095
+ }
28096
+ async function requiredGuard(guard, section) {
28097
+ if (guard === void 0) throw gated("gate_unavailable");
28098
+ let entered = false;
28099
+ try {
28100
+ return await guard(async () => {
28101
+ entered = true;
28102
+ return section();
28103
+ });
28104
+ } catch (error) {
28105
+ if (!entered) throw gated("gate_unavailable");
28106
+ throw error;
28107
+ }
28108
+ }
27059
28109
  var ReplicaStorageError = class extends Error {
27060
28110
  constructor(code, message, options) {
27061
28111
  super(message);
@@ -27081,6 +28131,19 @@ async function pathExists(path) {
27081
28131
  throw storageError("STORAGE_ERROR", `Reading ${path}`, error);
27082
28132
  }
27083
28133
  }
28134
+ async function realDirectoryExists(path) {
28135
+ let stats;
28136
+ try {
28137
+ stats = await (0, import_promises.lstat)(path);
28138
+ } catch (error) {
28139
+ if (isNotFound(error)) return false;
28140
+ throw storageError("STORAGE_ERROR", `Reading ${path}`, error);
28141
+ }
28142
+ if (stats.isSymbolicLink() || !stats.isDirectory()) {
28143
+ throw new ReplicaStorageError("STORAGE_ERROR", `Refusing the replaced replica path ${path}.`);
28144
+ }
28145
+ return true;
28146
+ }
27084
28147
  function principalOf(did) {
27085
28148
  return (0, import_sdk_core13.principalDid)(did);
27086
28149
  }
@@ -27234,90 +28297,117 @@ function parsePendingState(text, file, identity) {
27234
28297
  }
27235
28298
  return state;
27236
28299
  }
27237
- var _dir, _guard, _dirSync, _file, _FilePendingWriteStore_instances, canonicalFile_fn;
28300
+ var _dir, _profileDir, _guard, _stamp, _dirSync, _logoutPendingFile, _generation, _canonicalDir, _FilePendingWriteStore_instances, withGate_fn, readFile_fn;
27238
28301
  var FilePendingWriteStore = class {
27239
- constructor(identity, dir, guard, dirSync) {
28302
+ constructor(identity, dir, profileDir, guard, stamp, dirSync, logoutPendingFile) {
27240
28303
  __privateAdd(this, _FilePendingWriteStore_instances);
27241
28304
  this.durable = true;
27242
28305
  __privateAdd(this, _dir);
28306
+ __privateAdd(this, _profileDir);
27243
28307
  __privateAdd(this, _guard);
28308
+ __privateAdd(this, _stamp);
27244
28309
  __privateAdd(this, _dirSync);
27245
- __privateAdd(this, _file);
28310
+ __privateAdd(this, _logoutPendingFile);
28311
+ __privateAdd(this, _generation);
28312
+ __privateAdd(this, _canonicalDir);
27246
28313
  this.identity = identity;
27247
28314
  __privateSet(this, _dir, dir);
28315
+ __privateSet(this, _profileDir, profileDir);
27248
28316
  __privateSet(this, _guard, guard);
28317
+ __privateSet(this, _stamp, stamp);
27249
28318
  __privateSet(this, _dirSync, dirSync);
28319
+ __privateSet(this, _logoutPendingFile, logoutPendingFile);
27250
28320
  }
27251
28321
  async read() {
27252
- const file = await __privateMethod(this, _FilePendingWriteStore_instances, canonicalFile_fn).call(this);
27253
- let text;
27254
- try {
27255
- text = await (0, import_promises.readFile)(file, "utf8");
27256
- } catch (error) {
27257
- if (isNotFound(error)) {
27258
- return { v: 2, identity: this.identity, committedEpoch: 0, seq: 0, records: [] };
27259
- }
27260
- throw storageError("STORAGE_ERROR", `Reading ${file}`, error);
27261
- }
27262
- return parsePendingState(text, file, this.identity);
28322
+ 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: [] });
27263
28323
  }
27264
28324
  async readExisting() {
27265
- let file;
27266
- try {
27267
- file = (0, import_node_path.join)(await (0, import_promises.realpath)(__privateGet(this, _dir)), "pending.json");
27268
- } catch (error) {
27269
- if (isNotFound(error)) return void 0;
27270
- throw storageError("STORAGE_ERROR", `Resolving pending-write state in ${__privateGet(this, _dir)}`, error);
27271
- }
27272
- try {
27273
- const text = await (0, import_promises.readFile)(file, "utf8");
27274
- return parsePendingState(text, file, this.identity);
27275
- } catch (error) {
27276
- if (isNotFound(error)) return void 0;
27277
- if (error instanceof ReplicaStorageError) throw error;
27278
- throw storageError("STORAGE_ERROR", `Reading ${file}`, error);
27279
- }
28325
+ return __privateMethod(this, _FilePendingWriteStore_instances, withGate_fn).call(this, (file) => __privateMethod(this, _FilePendingWriteStore_instances, readFile_fn).call(this, file));
27280
28326
  }
27281
28327
  async update(mutate) {
27282
- const file = await __privateMethod(this, _FilePendingWriteStore_instances, canonicalFile_fn).call(this);
27283
- return serializePendingWrite(file, async () => {
27284
- let currentFile;
27285
- try {
27286
- currentFile = (0, import_node_path.join)(await (0, import_promises.realpath)((0, import_node_path.dirname)(file)), (0, import_node_path.basename)(file));
27287
- } catch (error) {
27288
- throw storageError("STORAGE_ERROR", "Resolving the open pending-write partition", error);
27289
- }
27290
- if (currentFile !== file) {
27291
- throw new ReplicaStorageError(
27292
- "STORAGE_ERROR",
27293
- `The pending-write partition changed while open: ${(0, import_node_path.dirname)(file)}.`
27294
- );
27295
- }
27296
- return __privateGet(this, _guard).call(this, async () => {
27297
- const state = await this.read();
27298
- const result = mutate(state);
27299
- await writeJsonAtomic(file, state, __privateGet(this, _dirSync));
27300
- return result;
27301
- });
28328
+ return __privateMethod(this, _FilePendingWriteStore_instances, withGate_fn).call(this, async (file) => {
28329
+ await ensurePartitionIdentity(__privateGet(this, _dir), this.identity, __privateGet(this, _dirSync));
28330
+ const currentFile = (0, import_node_path.join)(await (0, import_promises.realpath)(__privateGet(this, _dir)), "pending.json");
28331
+ if (__privateGet(this, _canonicalDir) !== void 0 && (0, import_node_path.dirname)(currentFile) !== __privateGet(this, _canonicalDir)) {
28332
+ throw new ReplicaStorageError("STORAGE_ERROR", "The pending-write partition changed while open.");
28333
+ }
28334
+ const state = await __privateMethod(this, _FilePendingWriteStore_instances, readFile_fn).call(this, currentFile) ?? { v: 2, identity: this.identity, committedEpoch: 0, seq: 0, records: [] };
28335
+ const result = await mutate(state);
28336
+ await writeJsonAtomic(currentFile, state, __privateGet(this, _dirSync));
28337
+ return result;
27302
28338
  });
27303
28339
  }
27304
28340
  };
27305
28341
  _dir = new WeakMap();
28342
+ _profileDir = new WeakMap();
27306
28343
  _guard = new WeakMap();
28344
+ _stamp = new WeakMap();
27307
28345
  _dirSync = new WeakMap();
27308
- _file = new WeakMap();
28346
+ _logoutPendingFile = new WeakMap();
28347
+ _generation = new WeakMap();
28348
+ _canonicalDir = new WeakMap();
27309
28349
  _FilePendingWriteStore_instances = new WeakSet();
27310
- canonicalFile_fn = async function() {
27311
- __privateGet(this, _file) ?? __privateSet(this, _file, (async () => {
27312
- await ensurePartitionIdentity(__privateGet(this, _dir), this.identity, __privateGet(this, _dirSync));
27313
- const file = (0, import_node_path.join)(__privateGet(this, _dir), "pending.json");
28350
+ withGate_fn = async function(section) {
28351
+ if (__privateGet(this, _stamp) === void 0) throw gated("not_established");
28352
+ if (__privateGet(this, _guard) === void 0) throw gated("gate_unavailable");
28353
+ const gate = await fileGate();
28354
+ const snapshot = await gate.readGateSnapshot(__privateGet(this, _profileDir));
28355
+ if (snapshot === void 0) throw gated("gate_unavailable");
28356
+ const [principalHash, identityKeyHash] = await Promise.all([
28357
+ gate.principalHashOf(snapshot.salt, this.identity.principal),
28358
+ gate.identityHashOf(snapshot.salt, (0, import_sdk_services3.replicationIdentityKey)(this.identity))
28359
+ ]);
28360
+ const access = {
28361
+ stamp: __privateGet(this, _stamp),
28362
+ incarnation: snapshot.incarnation,
28363
+ principalHash,
28364
+ identityKeyHash
28365
+ };
28366
+ let canonicalRoot;
28367
+ try {
28368
+ canonicalRoot = await (0, import_promises.realpath)((0, import_node_path.dirname)(__privateGet(this, _dir)));
28369
+ } catch (error) {
28370
+ if (!isNotFound(error)) throw storageError("STORAGE_ERROR", `Resolving ${(0, import_node_path.dirname)(__privateGet(this, _dir))}`, error);
28371
+ canonicalRoot = (0, import_node_path.join)(await (0, import_promises.realpath)((0, import_node_path.dirname)((0, import_node_path.dirname)(__privateGet(this, _dir)))), (0, import_node_path.basename)((0, import_node_path.dirname)(__privateGet(this, _dir))));
28372
+ }
28373
+ const queueKey = (0, import_node_path.join)(canonicalRoot, (0, import_node_path.basename)(__privateGet(this, _dir)), "pending.json");
28374
+ return serializePendingWrite(queueKey, () => requiredGuard(__privateGet(this, _guard), async () => {
28375
+ assertLogoutClear(__privateGet(this, _logoutPendingFile));
28376
+ const before = await gate.assertGateAccess(__privateGet(this, _profileDir), access, snapshot.salt);
28377
+ const generation = before.pendingGenerations[identityKeyHash] ?? 0;
28378
+ if (__privateGet(this, _generation) !== void 0 && __privateGet(this, _generation) !== generation) {
28379
+ throw new ReplicaStorageError("RESET_REQUIRED", "The pending identity was erased; reopen the store.");
28380
+ }
28381
+ let file = (0, import_node_path.join)(__privateGet(this, _dir), "pending.json");
27314
28382
  try {
27315
- return (0, import_node_path.join)(await (0, import_promises.realpath)((0, import_node_path.dirname)(file)), (0, import_node_path.basename)(file));
28383
+ file = (0, import_node_path.join)(await (0, import_promises.realpath)(__privateGet(this, _dir)), "pending.json");
28384
+ const recorded = identityRecordOf(await readJsonFile((0, import_node_path.join)((0, import_node_path.dirname)(file), "identity.json")));
28385
+ if (recorded === void 0 || !sameIdentity(recorded, this.identity)) {
28386
+ throw new ReplicaStorageError("STORAGE_ERROR", "The pending partition belongs to a different identity.");
28387
+ }
27316
28388
  } catch (error) {
27317
- throw storageError("STORAGE_ERROR", `Resolving ${file}`, error);
28389
+ if (error instanceof ReplicaStorageError) throw error;
28390
+ if (!isNotFound(error)) throw storageError("STORAGE_ERROR", `Resolving ${__privateGet(this, _dir)}`, error);
27318
28391
  }
27319
- })());
27320
- return __privateGet(this, _file);
28392
+ if (__privateGet(this, _canonicalDir) !== void 0 && (0, import_node_path.dirname)(file) !== __privateGet(this, _canonicalDir)) {
28393
+ throw new ReplicaStorageError("STORAGE_ERROR", "The pending-write partition changed while open.");
28394
+ }
28395
+ const result = await section(file);
28396
+ await gate.assertGateAccess(__privateGet(this, _profileDir), access, snapshot.salt);
28397
+ assertLogoutClear(__privateGet(this, _logoutPendingFile));
28398
+ __privateSet(this, _generation, generation);
28399
+ if (await pathExists(__privateGet(this, _dir))) __privateSet(this, _canonicalDir, await (0, import_promises.realpath)(__privateGet(this, _dir)));
28400
+ return result;
28401
+ }));
28402
+ };
28403
+ readFile_fn = async function(file) {
28404
+ try {
28405
+ return parsePendingState(await (0, import_promises.readFile)(file, "utf8"), file, this.identity);
28406
+ } catch (error) {
28407
+ if (isNotFound(error)) return void 0;
28408
+ if (error instanceof ReplicaStorageError) throw error;
28409
+ throw storageError("STORAGE_ERROR", `Reading ${file}`, error);
28410
+ }
27321
28411
  };
27322
28412
  async function loadOrCreateDevice(idDir, guard, createDevice, dirSync) {
27323
28413
  return guard(async () => {
@@ -27363,7 +28453,7 @@ async function readFence(replicaDir) {
27363
28453
  const fence = await readJsonFile((0, import_node_path.join)(replicaDir, FENCE_FILE));
27364
28454
  return typeof fence === "object" && fence !== null && "syncedThroughEpoch" in fence && typeof fence.syncedThroughEpoch === "number" ? fence.syncedThroughEpoch : 0;
27365
28455
  }
27366
- function newestGrantInfo(replica, record, pending, spec) {
28456
+ function newestGrantInfo(replica, record, pending, spec, install) {
27367
28457
  if (record === null) return null;
27368
28458
  const parsed = replica.parseUcanGrant(record.bytes);
27369
28459
  return {
@@ -27371,10 +28461,11 @@ function newestGrantInfo(replica, record, pending, spec) {
27371
28461
  parentCid: parsed.prf[0] ?? null,
27372
28462
  expiresAt: record.expiresAt === null ? null : record.expiresAt * 1e3,
27373
28463
  state: pending !== null && record === pending ? "pending" : "active",
27374
- unconstrained: ucanAttUnconstrainedFor(parsed.att, spec.space, spec.prefix)
28464
+ unconstrained: ucanAttUnconstrainedFor(parsed.att, spec.space, spec.prefix),
28465
+ ...install === null ? {} : install
27375
28466
  };
27376
28467
  }
27377
- var _device, _store, _replicaDir, _replica, _guard2, _dirSync2, _generation, _ino, _transportOverride, _closed, _inFlight, _transportFactory, _SqliteReplicaHandle_instances, assertOpen_fn, assertLive_fn, transports_fn;
28468
+ 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;
27378
28469
  var SqliteReplicaHandle = class {
27379
28470
  constructor(input) {
27380
28471
  __privateAdd(this, _SqliteReplicaHandle_instances);
@@ -27383,9 +28474,12 @@ var SqliteReplicaHandle = class {
27383
28474
  __privateAdd(this, _replicaDir);
27384
28475
  __privateAdd(this, _replica);
27385
28476
  __privateAdd(this, _guard2);
28477
+ __privateAdd(this, _read);
28478
+ __privateAdd(this, _policy);
28479
+ __privateAdd(this, _recordGrantInstall);
27386
28480
  __privateAdd(this, _dirSync2);
27387
28481
  /** The replica row generation this handle opened under — the durable fence a purge commits past (browser-parity wipe). */
27388
- __privateAdd(this, _generation);
28482
+ __privateAdd(this, _generation2);
27389
28483
  /** The inode of `replica.db` at open — a directory recreated in place is a different replica. */
27390
28484
  __privateAdd(this, _ino);
27391
28485
  __privateAdd(this, _transportOverride);
@@ -27393,64 +28487,76 @@ var SqliteReplicaHandle = class {
27393
28487
  __privateAdd(this, _inFlight, /* @__PURE__ */ new Set());
27394
28488
  __privateAdd(this, _transportFactory);
27395
28489
  this.spec = input.spec;
28490
+ this.replicaId = input.replicaId;
27396
28491
  __privateSet(this, _device, input.device);
27397
28492
  this.deviceDid = input.device.did;
27398
28493
  __privateSet(this, _store, input.store);
27399
28494
  __privateSet(this, _replicaDir, input.replicaDir);
27400
28495
  __privateSet(this, _replica, input.replica);
27401
28496
  __privateSet(this, _guard2, input.guard);
28497
+ __privateSet(this, _read, input.read);
28498
+ __privateSet(this, _policy, input.policy);
28499
+ __privateSet(this, _recordGrantInstall, input.recordGrantInstall);
27402
28500
  __privateSet(this, _transportOverride, input.transportFor);
27403
28501
  __privateSet(this, _dirSync2, input.dirSync);
27404
- __privateSet(this, _generation, input.generation);
28502
+ __privateSet(this, _generation2, input.generation);
27405
28503
  __privateSet(this, _ino, input.ino);
27406
28504
  }
27407
28505
  async get(key2) {
27408
- await __privateMethod(this, _SqliteReplicaHandle_instances, assertLive_fn).call(this);
27409
- const result = await new (__privateGet(this, _replica)).Replica({ store: __privateGet(this, _store) }).get(key2);
27410
- const meta = {
27411
- asOf: result.meta.asOf,
27412
- coverage: result.meta.coverage,
27413
- authority: result.meta.authority,
27414
- syncedThroughEpoch: await readFence(__privateGet(this, _replicaDir))
27415
- };
27416
- switch (result.status) {
27417
- case "present":
27418
- return {
27419
- status: "present",
27420
- key: result.key,
27421
- value: result.value,
27422
- etag: result.etag,
27423
- metadata: result.metadata,
27424
- meta
27425
- };
27426
- case "content_missing":
27427
- return {
27428
- status: "content_missing",
27429
- key: result.key,
27430
- etag: result.etag,
27431
- metadata: result.metadata,
27432
- meta
27433
- };
27434
- default:
27435
- return { status: result.status, key: result.key, meta };
27436
- }
27437
- }
27438
- async list(options) {
27439
- await __privateMethod(this, _SqliteReplicaHandle_instances, assertLive_fn).call(this);
27440
- const result = await new (__privateGet(this, _replica)).Replica({ store: __privateGet(this, _store) }).list({
27441
- prefix: options.prefix,
27442
- ...options.after === void 0 ? {} : { after: options.after },
27443
- ...options.limit === void 0 ? {} : { limit: options.limit }
27444
- });
27445
- return {
27446
- keys: result.entries.map((entry) => entry.key),
27447
- meta: {
28506
+ return __privateGet(this, _read).call(this, async (snapshot, syncedThroughEpoch) => {
28507
+ __privateMethod(this, _SqliteReplicaHandle_instances, assertOpen_fn).call(this);
28508
+ await __privateMethod(this, _SqliteReplicaHandle_instances, assertEffective_fn).call(this, snapshot);
28509
+ const result = await new (__privateGet(this, _replica)).Replica({ store: snapshot }).get(key2);
28510
+ await __privateMethod(this, _SqliteReplicaHandle_instances, assertEffective_fn).call(this, snapshot);
28511
+ const meta = {
27448
28512
  asOf: result.meta.asOf,
27449
28513
  coverage: result.meta.coverage,
27450
28514
  authority: result.meta.authority,
27451
- syncedThroughEpoch: await readFence(__privateGet(this, _replicaDir))
28515
+ syncedThroughEpoch
28516
+ };
28517
+ switch (result.status) {
28518
+ case "present":
28519
+ return {
28520
+ status: "present",
28521
+ key: result.key,
28522
+ value: result.value,
28523
+ etag: result.etag,
28524
+ metadata: result.metadata,
28525
+ meta
28526
+ };
28527
+ case "content_missing":
28528
+ return {
28529
+ status: "content_missing",
28530
+ key: result.key,
28531
+ etag: result.etag,
28532
+ metadata: result.metadata,
28533
+ meta
28534
+ };
28535
+ default:
28536
+ return { status: result.status, key: result.key, meta };
27452
28537
  }
27453
- };
28538
+ }, true);
28539
+ }
28540
+ async list(options) {
28541
+ return __privateGet(this, _read).call(this, async (snapshot, syncedThroughEpoch) => {
28542
+ __privateMethod(this, _SqliteReplicaHandle_instances, assertOpen_fn).call(this);
28543
+ await __privateMethod(this, _SqliteReplicaHandle_instances, assertEffective_fn).call(this, snapshot);
28544
+ const result = await new (__privateGet(this, _replica)).Replica({ store: snapshot }).list({
28545
+ prefix: options.prefix,
28546
+ ...options.after === void 0 ? {} : { after: options.after },
28547
+ ...options.limit === void 0 ? {} : { limit: options.limit }
28548
+ });
28549
+ await __privateMethod(this, _SqliteReplicaHandle_instances, assertEffective_fn).call(this, snapshot);
28550
+ return {
28551
+ keys: result.entries.map((entry) => entry.key),
28552
+ meta: {
28553
+ asOf: result.meta.asOf,
28554
+ coverage: result.meta.coverage,
28555
+ authority: result.meta.authority,
28556
+ syncedThroughEpoch
28557
+ }
28558
+ };
28559
+ }, true);
27454
28560
  }
27455
28561
  /**
27456
28562
  * The newest installed grant — the pending one when a not-yet-promoted
@@ -27460,43 +28566,59 @@ var SqliteReplicaHandle = class {
27460
28566
  * grant is still reported (unconstrained: false) and never reused.
27461
28567
  */
27462
28568
  async grant() {
27463
- await __privateMethod(this, _SqliteReplicaHandle_instances, assertLive_fn).call(this);
27464
- const state = await __privateGet(this, _store).open();
27465
- if (state === null) return null;
27466
- return newestGrantInfo(
27467
- __privateGet(this, _replica),
27468
- state.pendingGrant ?? state.grant,
27469
- state.pendingGrant,
27470
- this.spec
27471
- );
28569
+ return __privateGet(this, _read).call(this, async (snapshot) => {
28570
+ __privateMethod(this, _SqliteReplicaHandle_instances, assertOpen_fn).call(this);
28571
+ const state = await snapshot.open();
28572
+ if (state === null) return null;
28573
+ const selected = state.pendingGrant ?? state.grant;
28574
+ const install = selected === null ? null : await snapshot.getGrantInstall(selected.cid);
28575
+ return newestGrantInfo(
28576
+ __privateGet(this, _replica),
28577
+ state.pendingGrant ?? state.grant,
28578
+ state.pendingGrant,
28579
+ this.spec,
28580
+ install
28581
+ );
28582
+ });
27472
28583
  }
27473
28584
  async installGrant(ucan) {
27474
- await __privateMethod(this, _SqliteReplicaHandle_instances, assertLive_fn).call(this);
27475
- const parsed = __privateGet(this, _replica).parseUcanGrant(ucan);
27476
- const state = await __privateGet(this, _store).open();
27477
- if (state === null) {
27478
- throw new (__privateGet(this, _replica)).ReplicaError(
27479
- __privateGet(this, _replica).ReplicaErrorCode.NOT_FOUND,
27480
- "The replica has not been created."
27481
- );
27482
- }
27483
- __privateGet(this, _replica).assertGrantInstallable(parsed, {
27484
- deviceDid: principalOf(__privateGet(this, _device).did),
27485
- space: this.spec.space,
27486
- prefix: this.spec.prefix,
27487
- now: Date.now()
28585
+ return __privateGet(this, _guard2).call(this, async () => {
28586
+ await __privateMethod(this, _SqliteReplicaHandle_instances, assertLive_fn).call(this);
28587
+ const parsed = __privateGet(this, _replica).parseUcanGrant(ucan);
28588
+ const state = await __privateGet(this, _store).open();
28589
+ if (state === null) {
28590
+ throw new (__privateGet(this, _replica)).ReplicaError(
28591
+ __privateGet(this, _replica).ReplicaErrorCode.NOT_FOUND,
28592
+ "The replica has not been created."
28593
+ );
28594
+ }
28595
+ __privateGet(this, _replica).assertGrantInstallable(parsed, {
28596
+ deviceDid: principalOf(__privateGet(this, _device).did),
28597
+ space: this.spec.space,
28598
+ prefix: this.spec.prefix,
28599
+ now: Date.now()
28600
+ });
28601
+ if (!ucanAttUnconstrainedFor(parsed.att, this.spec.space, this.spec.prefix)) {
28602
+ throw new (__privateGet(this, _replica)).ReplicaError(
28603
+ __privateGet(this, _replica).ReplicaErrorCode.GRANT_NOT_COVERING,
28604
+ "The grant's get or sync coverage carries caveats; re-mint instead of installing it."
28605
+ );
28606
+ }
28607
+ const now = Date.now();
28608
+ const policy = await __privateGet(this, _policy).call(this, now, true);
28609
+ if (policy === null || now >= policy.consentDeadline) {
28610
+ throw new ReplicaStorageError("CONSENT_EXPIRED", "The replica's consent window has expired.");
28611
+ }
28612
+ const installedAt = await __privateGet(this, _recordGrantInstall).call(this, parsed.cid, now);
28613
+ const installed = await __privateGet(this, _store).installGatedGrant(parsed, { ...policy, at: installedAt });
28614
+ return newestGrantInfo(__privateGet(this, _replica), parsed, parsed, this.spec, installed);
27488
28615
  });
27489
- if (!ucanAttUnconstrainedFor(parsed.att, this.spec.space, this.spec.prefix)) {
27490
- throw new (__privateGet(this, _replica)).ReplicaError(
27491
- __privateGet(this, _replica).ReplicaErrorCode.GRANT_NOT_COVERING,
27492
- "The grant's get or sync coverage carries caveats; re-mint instead of installing it."
27493
- );
27494
- }
27495
- await __privateGet(this, _store).installGrant(parsed);
27496
- return newestGrantInfo(__privateGet(this, _replica), parsed, parsed, this.spec);
27497
28616
  }
27498
28617
  async sync(options) {
27499
- await __privateMethod(this, _SqliteReplicaHandle_instances, assertLive_fn).call(this);
28618
+ await __privateGet(this, _guard2).call(this, async () => {
28619
+ await __privateMethod(this, _SqliteReplicaHandle_instances, assertLive_fn).call(this);
28620
+ await __privateMethod(this, _SqliteReplicaHandle_instances, assertSyncEffective_fn).call(this);
28621
+ });
27500
28622
  const transportFor = await __privateMethod(this, _SqliteReplicaHandle_instances, transports_fn).call(this);
27501
28623
  const abort = new AbortController();
27502
28624
  options.signal.throwIfAborted();
@@ -27537,27 +28659,33 @@ var SqliteReplicaHandle = class {
27537
28659
  }
27538
28660
  }
27539
28661
  async status() {
27540
- await __privateMethod(this, _SqliteReplicaHandle_instances, assertLive_fn).call(this);
27541
- const engine = new (__privateGet(this, _replica)).Replica({ store: __privateGet(this, _store) });
27542
- const status = await engine.status();
27543
- const syncedThroughEpoch = await readFence(__privateGet(this, _replicaDir));
27544
- const state = await __privateGet(this, _store).open();
27545
- const grant = state === null ? null : newestGrantInfo(
27546
- __privateGet(this, _replica),
27547
- state.pendingGrant ?? state.grant,
27548
- state.pendingGrant,
27549
- this.spec
27550
- );
27551
- return {
27552
- coverage: status.coverage,
27553
- lastSyncAt: status.lastSyncAt,
27554
- syncedThroughEpoch,
27555
- authority: { state: status.authority.state, expiresAt: status.authority.expiresAt },
27556
- grant,
27557
- counts: status.counts,
27558
- bytes: status.bytes,
27559
- lastError: status.lastError
27560
- };
28662
+ return __privateGet(this, _read).call(this, async (snapshot, syncedThroughEpoch) => {
28663
+ __privateMethod(this, _SqliteReplicaHandle_instances, assertOpen_fn).call(this);
28664
+ const engine = new (__privateGet(this, _replica)).Replica({ store: snapshot });
28665
+ const status = await engine.status();
28666
+ const state = await snapshot.open();
28667
+ const selected = state?.pendingGrant ?? state?.grant ?? null;
28668
+ const selectedInstall = selected === null ? null : await snapshot.getGrantInstall(selected.cid);
28669
+ const activeRecord = state?.grant ?? null;
28670
+ const activeInstall = activeRecord === null ? null : await snapshot.getGrantInstall(activeRecord.cid);
28671
+ const grant = state === null ? null : newestGrantInfo(
28672
+ __privateGet(this, _replica),
28673
+ state.pendingGrant ?? state.grant,
28674
+ state.pendingGrant,
28675
+ this.spec,
28676
+ selectedInstall
28677
+ );
28678
+ return {
28679
+ coverage: status.coverage,
28680
+ lastSyncAt: status.lastSyncAt,
28681
+ syncedThroughEpoch,
28682
+ 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 },
28683
+ grant,
28684
+ counts: status.counts,
28685
+ bytes: status.bytes,
28686
+ lastError: status.lastError
28687
+ };
28688
+ }, true);
27561
28689
  }
27562
28690
  async close() {
27563
28691
  if (__privateGet(this, _closed)) return;
@@ -27572,8 +28700,11 @@ _store = new WeakMap();
27572
28700
  _replicaDir = new WeakMap();
27573
28701
  _replica = new WeakMap();
27574
28702
  _guard2 = new WeakMap();
28703
+ _read = new WeakMap();
28704
+ _policy = new WeakMap();
28705
+ _recordGrantInstall = new WeakMap();
27575
28706
  _dirSync2 = new WeakMap();
27576
- _generation = new WeakMap();
28707
+ _generation2 = new WeakMap();
27577
28708
  _ino = new WeakMap();
27578
28709
  _transportOverride = new WeakMap();
27579
28710
  _closed = new WeakMap();
@@ -27608,7 +28739,7 @@ assertLive_fn = async function() {
27608
28739
  throw error;
27609
28740
  }
27610
28741
  const state = await __privateGet(this, _store).open();
27611
- if (state !== null && state.generation !== __privateGet(this, _generation)) {
28742
+ if (state !== null && state.generation !== __privateGet(this, _generation2)) {
27612
28743
  throw new (__privateGet(this, _replica)).ReplicaError(
27613
28744
  __privateGet(this, _replica).ReplicaErrorCode.RESET_REQUIRED,
27614
28745
  "The replica was purged or reopened; open it again to continue."
@@ -27650,97 +28781,1099 @@ transports_fn = async function() {
27650
28781
  })());
27651
28782
  return __privateGet(this, _transportFactory);
27652
28783
  };
28784
+ assertEffective_fn = async function(snapshot) {
28785
+ const state = await snapshot.open();
28786
+ if (state?.grant === void 0 || state.grant === null) return;
28787
+ const installed = await snapshot.getGrantInstall(state.grant.cid);
28788
+ if (installed === null || !Number.isSafeInteger(installed.effectiveExpiresAt) || Date.now() >= installed.effectiveExpiresAt || Date.now() >= installed.consentDeadline) {
28789
+ throw new ReplicaStorageError("GRANT_EXPIRED", "The replica grant's effective lifetime has expired.");
28790
+ }
28791
+ };
28792
+ assertSyncEffective_fn = async function() {
28793
+ const state = await __privateGet(this, _store).open();
28794
+ const selected = state?.pendingGrant ?? state?.grant;
28795
+ if (selected === void 0 || selected === null) return;
28796
+ const installed = await __privateGet(this, _store).getGrantInstall(selected.cid);
28797
+ const now = Date.now();
28798
+ if (installed === null || !Number.isSafeInteger(installed.effectiveExpiresAt) || now >= installed.effectiveExpiresAt || now >= installed.consentDeadline) {
28799
+ throw new ReplicaStorageError("GRANT_EXPIRED", "The replica grant's effective lifetime has expired.");
28800
+ }
28801
+ };
27653
28802
  function createSqliteReplicaStorage(options) {
27654
28803
  const root = options.dir;
27655
- const guard = options.guard ?? ((section) => section());
28804
+ const profileDir = (0, import_node_path.dirname)(options.gateFile ?? (0, import_node_path.join)((0, import_node_path.dirname)(root), import_contractConstants.REPLICA_GATE_FILE));
28805
+ const diagnosticFiles = [...options.diagnosticFiles ?? []];
28806
+ for (const name of diagnosticFiles) {
28807
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name) || /^[a-z2-7]{26}$/.test(name)) {
28808
+ throw new TypeError(`Invalid replication diagnostics file name ${JSON.stringify(name)}.`);
28809
+ }
28810
+ }
28811
+ const inspectLayout = (gateReadable, approvedMarker, legacyOnly = false) => inspectProfileReplicaLayout(profileDir, gateReadable, approvedMarker, legacyOnly, diagnosticFiles);
28812
+ const guard = options.guard;
27656
28813
  const createDevice = options.createDevice ?? createDeviceKey;
27657
- async function openHandle(spec) {
27658
- const [replica, sqlite] = await Promise.all([
27659
- // @ts-ignore — TS2307 when replica's dist is absent (CI builds node-sdk first).
27660
- import("@tinycloud/replica"),
27661
- // @ts-ignore — same as above.
27662
- import("@tinycloud/replica/sqlite")
28814
+ const heldLock = new import_node_async_hooks.AsyncLocalStorage();
28815
+ let visibleStamp;
28816
+ let visiblePrincipalHash;
28817
+ let checkedGate;
28818
+ async function profileSection(section) {
28819
+ if (heldLock.getStore() === true) return section();
28820
+ return requiredGuard(guard, () => heldLock.run(true, section));
28821
+ }
28822
+ async function guarded(principal, stamp, identity, partitionId, section) {
28823
+ if (stamp === void 0) throw gated("not_established");
28824
+ if (guard === void 0) throw gated("gate_unavailable");
28825
+ const gate = await fileGate();
28826
+ const snapshot = await gate.readGateSnapshot(profileDir);
28827
+ if (snapshot === void 0) throw gated("gate_unavailable");
28828
+ const [principalHash, identityKeyHash] = await Promise.all([
28829
+ gate.principalHashOf(snapshot.salt, principal),
28830
+ identity === void 0 ? Promise.resolve(void 0) : gate.identityHashOf(snapshot.salt, (0, import_sdk_services3.replicationIdentityKey)(identity))
27663
28831
  ]);
27664
- if ((0, import_sdk_services3.requiresSecretsOptIn)(spec.space, spec.prefix) && spec.allowSecrets !== true) {
27665
- throw new replica.ReplicaError(
27666
- replica.ReplicaErrorCode.SECRETS_OPT_IN_REQUIRED,
27667
- `Replicating ${spec.space}/kv/${spec.prefix} copies secret material to disk; pass allowSecrets to opt in.`
27668
- );
27669
- }
27670
- const idDir = partitionDirOf(root, spec.identity);
27671
- await ensurePartitionIdentity(idDir, spec.identity, options.dirSync);
27672
- const createdDevice = spec.device ?? await loadOrCreateDevice(idDir, guard, createDevice, options.dirSync);
27673
- const device = { ...createdDevice, did: principalOf(createdDevice.did) };
27674
- const replicaHash = replicaHashOf(spec.prefix, device.did);
27675
- const replicaDir = (0, import_node_path.join)(idDir, "replicas", replicaHash);
27676
- const store = await sqlite.SqliteReplicaStore.open(replicaDir, {
27677
- create: true,
27678
- guard
28832
+ return profileSection(async () => {
28833
+ assertLogoutClear(options.logoutPendingFile);
28834
+ await gate.assertGateAccess(profileDir, {
28835
+ stamp,
28836
+ incarnation: snapshot.incarnation,
28837
+ principalHash,
28838
+ identityKeyHash,
28839
+ partitionId
28840
+ }, snapshot.salt);
28841
+ const result = await section();
28842
+ assertLogoutClear(options.logoutPendingFile);
28843
+ return result;
27679
28844
  });
27680
- let generation;
28845
+ }
28846
+ async function readGated(spec, partitionId, store, replicaDir, generation, section, withFence = false) {
28847
+ if (spec.stamp === void 0) throw gated("not_established");
28848
+ const gate = await fileGate();
28849
+ const snapshot = await gate.readGateSnapshot(profileDir);
28850
+ if (snapshot === void 0) throw gated("gate_unavailable");
28851
+ const [principalHash, identityKeyHash] = await Promise.all([
28852
+ gate.principalHashOf(snapshot.salt, spec.identity.principal),
28853
+ gate.identityHashOf(snapshot.salt, (0, import_sdk_services3.replicationIdentityKey)(spec.identity))
28854
+ ]);
28855
+ const access = {
28856
+ stamp: spec.stamp,
28857
+ incarnation: snapshot.incarnation,
28858
+ principalHash,
28859
+ identityKeyHash,
28860
+ partitionId
28861
+ };
28862
+ const path = options.gateFile ?? (0, import_node_path.join)(profileDir, import_contractConstants.REPLICA_GATE_FILE);
28863
+ let checked;
28864
+ const checkGate = async () => {
28865
+ assertLogoutClear(options.logoutPendingFile);
28866
+ let before;
28867
+ try {
28868
+ before = await (0, import_promises.stat)(path, { bigint: true });
28869
+ } catch {
28870
+ throw gated("gate_unavailable");
28871
+ }
28872
+ if (checked?.ino === before.ino && checked.size === before.size && checked.mtimeNs === before.mtimeNs) return;
28873
+ const checkedState = await gate.assertGateAccess(profileDir, access, snapshot.salt);
28874
+ if (spec.grantPolicy?.consentAnchor === null && checkedState.firstInstall[identityKeyHash] === void 0) {
28875
+ const installed = await store.open();
28876
+ if (installed !== null && (installed.grant !== null || installed.pendingGrant !== null)) throw gated("gate_unavailable");
28877
+ }
28878
+ let after;
28879
+ try {
28880
+ after = await (0, import_promises.stat)(path, { bigint: true });
28881
+ } catch {
28882
+ throw gated("gate_unavailable");
28883
+ }
28884
+ if (before.ino !== after.ino || before.size !== after.size || before.mtimeNs !== after.mtimeNs) {
28885
+ throw gated("gate_unavailable");
28886
+ }
28887
+ checked = after;
28888
+ };
28889
+ if (withFence) await checkGate();
28890
+ const fence = withFence ? await readFence(replicaDir) : 0;
28891
+ return store.readTransaction(generation, checkGate, (sqliteSnapshot) => section(sqliteSnapshot, fence));
28892
+ }
28893
+ async function identityDirs() {
28894
+ let entries;
28895
+ try {
28896
+ entries = await (0, import_promises.readdir)(root, { withFileTypes: true });
28897
+ } catch (error) {
28898
+ if (isNotFound(error)) return [];
28899
+ throw storageError("STORAGE_ERROR", `Listing ${root}`, error);
28900
+ }
28901
+ try {
28902
+ const marker = await readLogoutMarker((0, import_node_path.join)(profileDir, REPLICA_LOGOUT_PENDING_FILE));
28903
+ if (marker !== null) {
28904
+ const inventory = await inspectLayout(true);
28905
+ assertApprovedInventory(inventory, {
28906
+ unknownEntries: marker.approvedUnknown,
28907
+ knownRoots: marker.knownRoots,
28908
+ knownEntries: marker.knownEntries,
28909
+ missingDatabases: marker.missingDatabases
28910
+ }, true);
28911
+ }
28912
+ for (const entry of entries) {
28913
+ if (entry.isSymbolicLink() || !entry.isDirectory() || !/^[a-z2-7]{26}$/.test(entry.name)) {
28914
+ if (marker?.approvedUnknown.some((item) => item.path === `replication/${entry.name}` && (entry.isDirectory() ? item.kind === "directory" : entry.isSymbolicLink() ? item.kind === "symlink" : item.kind === "file"))) continue;
28915
+ if (diagnosticFiles.includes(entry.name)) {
28916
+ const info = await (0, import_promises.lstat)((0, import_node_path.join)(root, entry.name)).catch((error) => {
28917
+ if (isNotFound(error)) return null;
28918
+ throw error;
28919
+ });
28920
+ if (info === null || info.isFile()) continue;
28921
+ }
28922
+ throw new ReplicaStorageError("STORAGE_ERROR", `Unknown identity partition at ${root}.`);
28923
+ }
28924
+ }
28925
+ 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) => (0, import_node_path.join)(root, entry.name));
28926
+ } catch (error) {
28927
+ throw storageError("STORAGE_ERROR", `Listing ${root}`, error);
28928
+ }
28929
+ }
28930
+ async function collectIntents(principalHash, salt) {
28931
+ const gate = await fileGate();
28932
+ const intents = [];
28933
+ for (const idDir of await identityDirs()) {
28934
+ const identity = identityRecordOf(await readJsonFile((0, import_node_path.join)(idDir, "identity.json")));
28935
+ if (identity === void 0) throw new ReplicaStorageError("STORAGE_ERROR", `Missing identity at ${idDir}.`);
28936
+ if (await gate.principalHashOf(salt, identity.principal) !== principalHash) continue;
28937
+ const identityKeyHash = await gate.identityHashOf(salt, (0, import_sdk_services3.replicationIdentityKey)(identity));
28938
+ const recordedAt = Date.now();
28939
+ if (await pathExists((0, import_node_path.join)(idDir, "pending.json")) || await pathExists((0, import_node_path.join)(idDir, "device.jwk"))) {
28940
+ intents.push({ id: `pending:${identityKeyHash}`, kind: "pending", identityKeyHash, principalHash, recordedAt });
28941
+ }
28942
+ const replicasDir = (0, import_node_path.join)(idDir, "replicas");
28943
+ let entries;
28944
+ try {
28945
+ entries = await (0, import_promises.readdir)(replicasDir, { withFileTypes: true });
28946
+ } catch (error) {
28947
+ if (isNotFound(error)) continue;
28948
+ throw storageError("STORAGE_ERROR", `Listing ${replicasDir}`, error);
28949
+ }
28950
+ for (const entry of entries) {
28951
+ if (!entry.isDirectory()) continue;
28952
+ if (!/^[a-z2-7]{26}$/.test(entry.name)) {
28953
+ throw new ReplicaStorageError("STORAGE_ERROR", `Unrecognized replica partition at ${replicasDir}.`);
28954
+ }
28955
+ const partitionId = `${identityKeyHash}:${entry.name}`;
28956
+ intents.push({ id: partitionId, kind: "partition", partitionId, principalHash, recordedAt });
28957
+ }
28958
+ }
28959
+ return intents;
28960
+ }
28961
+ async function retainGrantInstalls(gate, gateState, identityKeyHash, store) {
28962
+ for (const { cid, installedAt } of await store.grantInstallTimes()) {
28963
+ const key2 = await gate.grantInstallHashOf(gateState.salt, identityKeyHash, cid);
28964
+ await gate.recordGrantInstall(profileDir, key2, installedAt);
28965
+ }
28966
+ }
28967
+ async function removeCorruptOrphanGate(gate) {
28968
+ const path = (0, import_node_path.join)(profileDir, import_contractConstants.REPLICA_GATE_FILE);
28969
+ const before = await (0, import_promises.lstat)(path, { bigint: true });
28970
+ if (!before.isFile() || before.isSymbolicLink()) throw gated("gate_unavailable");
28971
+ let corrupt = false;
28972
+ try {
28973
+ await gate.readGateSnapshot(profileDir);
28974
+ } catch (error) {
28975
+ if (typeof error === "object" && error !== null && "code" in error && error.code === "REPLICA_GATED" && "gate" in error && error.gate === "gate_unavailable") {
28976
+ corrupt = true;
28977
+ } else throw error;
28978
+ }
28979
+ if (!corrupt) throw gated("stamp_stale");
28980
+ const current = await (0, import_promises.lstat)(path, { bigint: true });
28981
+ if (current.dev !== before.dev || current.ino !== before.ino || current.mtimeNs !== before.mtimeNs || current.size !== before.size) throw gated("stamp_stale");
28982
+ await (0, import_promises.rm)(path);
28983
+ await (options.dirSync ?? fsyncDir)(profileDir);
28984
+ if (await gate.readGateSnapshot(profileDir) !== void 0) throw gated("stamp_stale");
28985
+ }
28986
+ async function readLogoutState(gate) {
27681
28987
  try {
28988
+ return { read: await gate.readLogoutGate(profileDir), corrupt: false };
28989
+ } catch (error) {
28990
+ if (isGateUnavailable(error)) return { read: void 0, corrupt: true };
28991
+ throw error;
28992
+ }
28993
+ }
28994
+ async function validateV1(gate, path, marker, read, mode) {
28995
+ if (read === void 0 || read.state.incarnation !== marker.incarnation) throw gated("stamp_stale");
28996
+ const { state, floorLagging } = read;
28997
+ if (marker.deniedEpoch !== void 0) {
28998
+ if (floorLagging || !v1Denied(marker, state, marker.deniedEpoch)) throw restoredGate();
28999
+ return { marker, state, active: false };
29000
+ }
29001
+ if (!floorLagging && state.principalHash === null && state.lifecycleEpoch > marker.lifecycleEpoch && (marker.remotePending ? state.eraseDeferred && state.deferredPrincipalHash === marker.principalHash : !state.eraseDeferred && state.deferredPrincipalHash === null)) {
29002
+ return { marker: await bindDeniedEpoch(path, marker, state.lifecycleEpoch), state, active: false };
29003
+ }
29004
+ if (state.principalHash !== marker.principalHash || state.lifecycleEpoch !== marker.lifecycleEpoch) {
29005
+ throw gated("stamp_stale");
29006
+ }
29007
+ if (!floorLagging && !mode.denyActive) {
29008
+ if (!mode.active) throw gated("stamp_stale");
29009
+ return { marker, state, active: true };
29010
+ }
29011
+ if (mode.acceptLagging) return { marker, state, active: true };
29012
+ if (mode.deny === void 0) throw interruptedSignOut();
29013
+ const denied = await gate.denyGate(
29014
+ profileDir,
29015
+ { reason: "sign_out", principal: "" },
29016
+ (principalHash) => collectIntents(principalHash, state.salt),
29017
+ { salt: state.salt, principalHash: marker.principalHash },
29018
+ { deferErase: mode.deny === "deferred" }
29019
+ );
29020
+ if (denied.status !== "denied") throw gated("gate_unavailable");
29021
+ const after = await gate.readLogoutGate(profileDir);
29022
+ if (after === void 0 || after.floorLagging || !v1Denied(marker, after.state, denied.lifecycleEpoch)) {
29023
+ throw gated("gate_unavailable");
29024
+ }
29025
+ return { marker: await bindDeniedEpoch(path, marker, denied.lifecycleEpoch), state: after.state, active: false };
29026
+ }
29027
+ function v1Denied(marker, state, epoch) {
29028
+ return state.incarnation === marker.incarnation && state.principalHash === null && state.lifecycleEpoch === epoch && (state.deferredPrincipalHash === marker.principalHash || !marker.remotePending && !state.eraseDeferred && state.deferredPrincipalHash === null);
29029
+ }
29030
+ async function bindDeniedEpoch(path, marker, deniedEpoch) {
29031
+ if (marker.deniedEpoch !== void 0) throw gated("stamp_stale");
29032
+ const bound = { ...marker, deniedEpoch };
29033
+ await writeJsonAtomic(path, bound, options.dirSync);
29034
+ return bound;
29035
+ }
29036
+ async function validatedRemoteMarker(path, marker, principal, mode) {
29037
+ if (marker.principalDigest !== logoutPrincipalDigest(principal)) throw gated("principal_mismatch");
29038
+ const gate = await fileGate();
29039
+ const { read, corrupt } = await readLogoutState(gate);
29040
+ if (corrupt) throw gated("gate_unavailable");
29041
+ if (marker.v === 2) {
29042
+ if (read !== void 0) throw gated("stamp_stale");
29043
+ return { marker, state: void 0 };
29044
+ }
29045
+ if (read === void 0 || read.state.incarnation !== marker.incarnation) throw gated("stamp_stale");
29046
+ if (marker.v === 3) {
29047
+ if (read.floorLagging || read.state.lifecycleEpoch !== marker.lifecycleEpoch || read.state.principalHash !== null || read.state.eraseDeferred) throw gated("stamp_stale");
29048
+ return { marker, state: read.state };
29049
+ }
29050
+ if (marker.principalHash !== await gate.principalHashOf(read.state.salt, principal)) {
29051
+ throw gated("principal_mismatch");
29052
+ }
29053
+ const checked = await validateV1(gate, path, marker, read, mode);
29054
+ return { marker: checked.marker, state: checked.state };
29055
+ }
29056
+ async function currentLayout() {
29057
+ const { read, corrupt } = await readLogoutState(await fileGate());
29058
+ if (corrupt) throw gated("gate_unavailable");
29059
+ const report = await inspectLayout(read !== void 0);
29060
+ assertSupportedProfileLayout(report);
29061
+ return report;
29062
+ }
29063
+ async function commitPlainAbandon(path, marker, report, discardLocalChanges) {
29064
+ const { remotePending: _, discardLocalChanges: _oldDiscard, ...previous } = marker;
29065
+ const abandoned = {
29066
+ ...previous,
29067
+ remoteAbandoned: true,
29068
+ approvedUnknown: discardLocalChanges ? report.unknownEntries : [],
29069
+ knownRoots: report.knownRoots,
29070
+ knownEntries: report.knownEntries,
29071
+ missingDatabases: report.missingDatabases,
29072
+ ...discardLocalChanges ? { discardLocalChanges: true } : {}
29073
+ };
29074
+ await writeJsonAtomic(path, abandoned, options.dirSync);
29075
+ return abandoned;
29076
+ }
29077
+ function deny(input, denyOptions) {
29078
+ const deferErase = denyOptions?.deferErase === true;
29079
+ if (deferErase && input.reason !== "sign_out") {
29080
+ throw new TypeError("Deferred erase is only available for sign-out.");
29081
+ }
29082
+ visibleStamp = void 0;
29083
+ visiblePrincipalHash = void 0;
29084
+ checkedGate = void 0;
29085
+ return (async () => {
27682
29086
  try {
27683
- await store.init({
27684
- name: spec.prefix.replace(/\/+$/, "") || "replica",
27685
- replicaId: replicaHash,
27686
- host: spec.identity.host,
27687
- space: spec.space,
27688
- prefix: spec.prefix,
27689
- deviceDid: device.did,
27690
- allowSecrets: spec.allowSecrets,
27691
- localReadPolicy: "whileGrantValid",
27692
- retentionGrantCid: null
29087
+ const gate = await fileGate();
29088
+ return await profileSection(async () => {
29089
+ const snapshot = (await gate.readLogoutGate(profileDir))?.state;
29090
+ const precomputed = snapshot === void 0 ? void 0 : {
29091
+ salt: snapshot.salt,
29092
+ principalHash: await gate.principalHashOf(snapshot.salt, input.principal)
29093
+ };
29094
+ const result = await gate.denyGate(
29095
+ profileDir,
29096
+ input,
29097
+ async (principalHash) => {
29098
+ if (snapshot === void 0) throw gated("gate_unavailable");
29099
+ return collectIntents(principalHash, snapshot.salt);
29100
+ },
29101
+ precomputed,
29102
+ { deferErase }
29103
+ );
29104
+ if (result.status !== "denied") return result;
29105
+ if (input.reason === "sign_out" && snapshot !== void 0 && precomputed !== void 0) {
29106
+ const path = (0, import_node_path.join)(profileDir, REPLICA_LOGOUT_PENDING_FILE);
29107
+ const marker = await readLogoutMarker(path);
29108
+ if (marker?.v === 1 && marker.deniedEpoch === void 0 && marker.incarnation === snapshot.incarnation && marker.principalHash === precomputed.principalHash && marker.remotePending === true === deferErase) {
29109
+ await bindDeniedEpoch(path, marker, result.lifecycleEpoch);
29110
+ }
29111
+ }
29112
+ if (!deferErase) return result;
29113
+ const resumeToken = await gate.deferredGateToken(profileDir);
29114
+ if (resumeToken === null || resumeToken.deniedEpoch !== result.lifecycleEpoch) {
29115
+ return { status: "unguarded", code: "gate_unavailable" };
29116
+ }
29117
+ return { ...result, resumeToken };
27693
29118
  });
27694
29119
  } catch (error) {
27695
- if (!replica.isReplicaError(error, replica.ReplicaErrorCode.CONFIG_MISMATCH)) {
27696
- throw error;
29120
+ return { status: "unguarded", code: typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : "gate_unavailable" };
29121
+ }
29122
+ })();
29123
+ }
29124
+ const lifecycle = {
29125
+ kind: "file",
29126
+ async recover({ signal }) {
29127
+ try {
29128
+ return await lifecycle.erasePending({ signal });
29129
+ } catch (error) {
29130
+ const code = typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : "STORAGE_ERROR";
29131
+ let remaining = 0;
29132
+ try {
29133
+ remaining = (await (await fileGate()).readGateSnapshot(profileDir))?.intents.length ?? 0;
29134
+ } catch {
27697
29135
  }
29136
+ return { erased: [], failed: [{ id: "gate", code }], remaining };
27698
29137
  }
27699
- const state = await store.open();
27700
- if (state === null) {
27701
- throw new replica.ReplicaError(
27702
- replica.ReplicaErrorCode.NOT_FOUND,
27703
- "The replica row vanished during init."
27704
- );
29138
+ },
29139
+ async establishSessionStamp(principal, { consentAnchor }) {
29140
+ if (consentAnchor?.source === "first_install" || !Number.isSafeInteger(consentAnchor?.at) || consentAnchor.at < 0) {
29141
+ throw gated("not_established");
29142
+ }
29143
+ const gate = await fileGate();
29144
+ const stamp = await profileSection(async () => {
29145
+ assertLogoutClear(options.logoutPendingFile);
29146
+ return gate.establishSessionStamp(profileDir, principal, { siweIssuedAt: consentAnchor.at });
29147
+ });
29148
+ const state = await gate.readGateSnapshot(profileDir);
29149
+ if (state === void 0) throw gated("gate_unavailable");
29150
+ visiblePrincipalHash = await gate.principalHashOf(state.salt, principal);
29151
+ checkedGate = void 0;
29152
+ visibleStamp = stamp;
29153
+ return stamp;
29154
+ },
29155
+ async restoreSessionStamp(principal, persisted) {
29156
+ const gate = await fileGate();
29157
+ const stamp = await profileSection(async () => {
29158
+ assertLogoutClear(options.logoutPendingFile);
29159
+ return gate.restoreSessionStamp(profileDir, principal, persisted);
29160
+ });
29161
+ const state = await gate.readGateSnapshot(profileDir);
29162
+ if (state === void 0) throw gated("gate_unavailable");
29163
+ visiblePrincipalHash = await gate.principalHashOf(state.salt, principal);
29164
+ checkedGate = void 0;
29165
+ visibleStamp = stamp;
29166
+ return stamp;
29167
+ },
29168
+ async activationAllowed(next) {
29169
+ if (guard === void 0) return false;
29170
+ try {
29171
+ const gate = await fileGate();
29172
+ return await profileSection(async () => {
29173
+ assertLogoutClear(options.logoutPendingFile);
29174
+ const state = await gate.readGateSnapshot(profileDir);
29175
+ return state !== void 0 && !state.eraseDeferred && state.intents.length === 0 && state.principalHash !== null && state.principalHash === await gate.principalHashOf(state.salt, next);
29176
+ });
29177
+ } catch {
29178
+ return false;
27705
29179
  }
27706
- const config = state.config;
27707
- if (config.host !== spec.identity.host || config.space !== spec.space || config.prefix !== spec.prefix) {
27708
- throw new replica.ReplicaError(
27709
- replica.ReplicaErrorCode.CONFIG_MISMATCH,
27710
- `This replica follows ${config.host} ${config.space}/kv/${config.prefix}, not ${spec.identity.host} ${spec.space}/kv/${spec.prefix}.`
27711
- );
29180
+ },
29181
+ checkDelivery(stamp) {
29182
+ if (guard === void 0 || visiblePrincipalHash === void 0) return { status: "gated", gate: "gate_unavailable" };
29183
+ try {
29184
+ assertLogoutClear(options.logoutPendingFile);
29185
+ } catch {
29186
+ return { status: "gated", gate: "gate_unavailable" };
27712
29187
  }
27713
- if (principalOf(config.deviceDid) !== principalOf(device.did)) {
27714
- throw new replica.ReplicaError(
27715
- replica.ReplicaErrorCode.CONFIG_MISMATCH,
27716
- "The stored replica belongs to a different device key; purge the partition to rekey."
27717
- );
29188
+ if (!stamp || visibleStamp?.incarnation !== stamp.incarnation || visibleStamp.lifecycleEpoch !== stamp.lifecycleEpoch) {
29189
+ return { status: "gated", gate: "stamp_stale" };
27718
29190
  }
27719
- generation = state.generation;
27720
- } catch (error) {
27721
- await store.close().catch(() => void 0);
27722
- throw error;
29191
+ try {
29192
+ const path = options.gateFile ?? (0, import_node_path.join)(profileDir, import_contractConstants.REPLICA_GATE_FILE);
29193
+ const stats = (0, import_node_fs.statSync)(path, { bigint: true });
29194
+ if (checkedGate === void 0 || checkedGate.ino !== stats.ino || checkedGate.mtimeNs !== stats.mtimeNs || checkedGate.size !== stats.size) {
29195
+ const raw = JSON.parse((0, import_node_fs.readFileSync)(path, "utf8"));
29196
+ if (typeof raw.incarnation !== "string" || !Number.isSafeInteger(raw.lifecycleEpoch) || raw.principalHash !== null && typeof raw.principalHash !== "string" || !Array.isArray(raw.intents)) {
29197
+ return { status: "gated", gate: "gate_unavailable" };
29198
+ }
29199
+ checkedGate = {
29200
+ ino: stats.ino,
29201
+ mtimeNs: stats.mtimeNs,
29202
+ size: stats.size,
29203
+ incarnation: raw.incarnation,
29204
+ epoch: raw.lifecycleEpoch,
29205
+ principalHash: raw.principalHash
29206
+ };
29207
+ }
29208
+ } catch {
29209
+ return { status: "gated", gate: "gate_unavailable" };
29210
+ }
29211
+ if (checkedGate.incarnation !== stamp.incarnation || checkedGate.epoch !== stamp.lifecycleEpoch) {
29212
+ return { status: "gated", gate: "stamp_stale" };
29213
+ }
29214
+ return checkedGate.principalHash === visiblePrincipalHash ? { status: "allow" } : { status: "gated", gate: "principal_mismatch" };
29215
+ },
29216
+ consentDeadline() {
29217
+ return null;
29218
+ },
29219
+ deliverable(stamp) {
29220
+ return lifecycle.checkDelivery(stamp).status === "allow";
29221
+ },
29222
+ deny,
29223
+ deferredErase() {
29224
+ return profileSection(async () => (await fileGate()).deferredGateToken(profileDir));
29225
+ },
29226
+ async resumeDeferredErase(token, { signal }) {
29227
+ signal.throwIfAborted();
29228
+ await profileSection(async () => (await fileGate()).resumeDeferredGate(profileDir, token));
29229
+ return lifecycle.erasePending({ signal });
29230
+ },
29231
+ async preflightLogout({ principal, discardLocalChanges = false }) {
29232
+ return profileSection(async () => {
29233
+ const marker = await readLogoutMarker((0, import_node_path.join)(profileDir, REPLICA_LOGOUT_PENDING_FILE));
29234
+ const gate = await fileGate();
29235
+ const { read, corrupt } = await readLogoutState(gate);
29236
+ const state = read?.state;
29237
+ let otherAccount = false;
29238
+ if (state !== void 0) {
29239
+ const hash3 = await gate.principalHashOf(state.salt, principal);
29240
+ otherAccount = gateOwnedByAnother(state, hash3);
29241
+ if (marker?.v === 1 && marker.principalHash !== hash3) throw gated("principal_mismatch");
29242
+ }
29243
+ const report = await inspectLayout(state !== void 0, void 0, otherAccount);
29244
+ assertSupportedProfileLayout(report);
29245
+ if (!discardLocalChanges && (report.unknownPaths.length > 0 || corrupt && !otherAccount || !otherAccount && (state === void 0 || state.principalHash === null && !state.eraseDeferred) && await hasOrphanPending(profileDir))) {
29246
+ throw localChangesRefusal(report.unknownPaths);
29247
+ }
29248
+ return report;
29249
+ });
29250
+ },
29251
+ async prepareLogoutPending({ principal, approval, discardLocalChanges = false, remotePending }) {
29252
+ const gate = await fileGate();
29253
+ return profileSection(async () => {
29254
+ const path = (0, import_node_path.join)(profileDir, REPLICA_LOGOUT_PENDING_FILE);
29255
+ const previous = await readLogoutMarker(path);
29256
+ const { read, corrupt: corruptGate } = await readLogoutState(gate);
29257
+ let state = read?.state;
29258
+ const principalHash = state === void 0 ? void 0 : await gate.principalHashOf(state.salt, principal);
29259
+ const otherAccount = state !== void 0 && gateOwnedByAnother(state, principalHash);
29260
+ let report = await inspectLayout(state !== void 0, void 0, otherAccount);
29261
+ assertSupportedProfileLayout(report);
29262
+ const assertApproved = (current) => {
29263
+ assertApprovedInventory(current, approval);
29264
+ if (approval.knownLegacyReplicas !== current.knownLegacyReplicas || JSON.stringify(approval.unknownPaths) !== JSON.stringify(current.unknownPaths) || JSON.stringify(approval.unknownLegacyPaths) !== JSON.stringify(current.unknownLegacyPaths)) {
29265
+ throw localChangesRefusal(current.unknownPaths);
29266
+ }
29267
+ };
29268
+ assertApproved(report);
29269
+ if (otherAccount) {
29270
+ if (previous !== null) throw gated("principal_mismatch");
29271
+ if (!discardLocalChanges && report.unknownPaths.length > 0) throw localChangesRefusal(report.unknownPaths);
29272
+ return null;
29273
+ }
29274
+ if (!discardLocalChanges && (report.unknownPaths.length > 0 || corruptGate || (state === void 0 || state.principalHash === null && !state.eraseDeferred) && await hasOrphanPending(profileDir))) {
29275
+ throw localChangesRefusal(report.unknownPaths);
29276
+ }
29277
+ if (previous !== null) {
29278
+ if (previous.principalDigest !== logoutPrincipalDigest(principal)) throw gated("principal_mismatch");
29279
+ if (corruptGate) throw gated("gate_unavailable");
29280
+ let current = previous;
29281
+ if (previous.v === 2) {
29282
+ if (read !== void 0) throw gated("stamp_stale");
29283
+ } else if (read === void 0 || previous.incarnation !== read.state.incarnation) {
29284
+ throw gated("stamp_stale");
29285
+ } else if (previous.v === 3) {
29286
+ if (read.floorLagging || read.state.lifecycleEpoch !== previous.lifecycleEpoch || read.state.principalHash !== null || read.state.eraseDeferred) throw gated("stamp_stale");
29287
+ } else {
29288
+ if (previous.principalHash !== principalHash) throw gated("principal_mismatch");
29289
+ current = (await validateV1(gate, path, previous, read, { active: true, acceptLagging: true })).marker;
29290
+ }
29291
+ assertApprovedInventory(report, {
29292
+ unknownEntries: current.approvedUnknown,
29293
+ knownRoots: current.knownRoots,
29294
+ knownEntries: current.knownEntries,
29295
+ missingDatabases: current.missingDatabases
29296
+ });
29297
+ if (current.remotePending !== remotePending || current.remoteAbandoned) {
29298
+ throw gated("gate_unavailable");
29299
+ }
29300
+ return current;
29301
+ }
29302
+ if (state?.eraseDeferred) throw gated("stamp_stale");
29303
+ if (read?.floorLagging) {
29304
+ if (state.principalHash === null) {
29305
+ state = await gate.advanceInactiveGate(profileDir);
29306
+ } else {
29307
+ const salt = state.salt;
29308
+ const denied = await gate.denyGate(
29309
+ profileDir,
29310
+ { reason: "sign_out", principal },
29311
+ (hash3) => collectIntents(hash3, salt),
29312
+ { salt, principalHash }
29313
+ );
29314
+ if (denied.status !== "denied") throw gated("gate_unavailable");
29315
+ state = await gate.readGateSnapshot(profileDir);
29316
+ }
29317
+ report = await inspectLayout(true);
29318
+ assertSupportedProfileLayout(report);
29319
+ assertApproved(report);
29320
+ }
29321
+ if (state === void 0 && report.knownRoots.length === 0 && !corruptGate) return null;
29322
+ if (state !== void 0 && state.principalHash === null && !state.eraseDeferred && report.knownRoots.length === 0 && state.intents.length === 0) return null;
29323
+ const remoteState = remotePending ? { remotePending: true } : {};
29324
+ const approvalFields = {
29325
+ principalDigest: logoutPrincipalDigest(principal),
29326
+ approvalSalt: (0, import_node_crypto2.randomBytes)(16).toString("hex"),
29327
+ approvedUnknown: discardLocalChanges ? report.unknownEntries : [],
29328
+ knownRoots: report.knownRoots,
29329
+ knownEntries: report.knownEntries,
29330
+ missingDatabases: report.missingDatabases,
29331
+ ...discardLocalChanges ? { discardLocalChanges: true } : {},
29332
+ ...remoteState
29333
+ };
29334
+ let marker;
29335
+ if (state === void 0) {
29336
+ marker = {
29337
+ v: 2,
29338
+ scope: "orphan_profile",
29339
+ nonce: (0, import_node_crypto2.randomBytes)(16).toString("hex"),
29340
+ ...approvalFields
29341
+ };
29342
+ } else if (state.principalHash === null) {
29343
+ marker = {
29344
+ v: 3,
29345
+ scope: "inactive_gate",
29346
+ incarnation: state.incarnation,
29347
+ lifecycleEpoch: state.lifecycleEpoch,
29348
+ ...approvalFields
29349
+ };
29350
+ } else {
29351
+ marker = {
29352
+ v: 1,
29353
+ incarnation: state.incarnation,
29354
+ lifecycleEpoch: state.lifecycleEpoch,
29355
+ principalHash,
29356
+ ...approvalFields
29357
+ };
29358
+ }
29359
+ await writeJsonAtomic(path, marker, options.dirSync);
29360
+ visibleStamp = void 0;
29361
+ visiblePrincipalHash = void 0;
29362
+ checkedGate = void 0;
29363
+ if (corruptGate) await removeCorruptOrphanGate(gate);
29364
+ return marker;
29365
+ });
29366
+ },
29367
+ async completeRemoteLogout({ principal }) {
29368
+ return profileSection(async () => {
29369
+ const path = (0, import_node_path.join)(profileDir, REPLICA_LOGOUT_PENDING_FILE);
29370
+ const marker = await readLogoutMarker(path);
29371
+ if (marker === null || marker.remotePending !== true) throw gated("gate_unavailable");
29372
+ const checked = await validatedRemoteMarker(path, marker, principal, { active: false });
29373
+ const { remotePending: _, ...complete } = checked.marker;
29374
+ await writeJsonAtomic(path, complete, options.dirSync);
29375
+ return complete;
29376
+ });
29377
+ },
29378
+ async remotePendingLogout({ principal }) {
29379
+ return profileSection(async () => {
29380
+ const path = (0, import_node_path.join)(profileDir, REPLICA_LOGOUT_PENDING_FILE);
29381
+ const marker = await readLogoutMarker(path);
29382
+ if (marker === null || marker.remotePending !== true) return false;
29383
+ await validatedRemoteMarker(path, marker, principal, { active: false });
29384
+ return true;
29385
+ });
29386
+ },
29387
+ async abandonRemoteLogout({ principal, approval, discardLocalChanges = false }) {
29388
+ return profileSection(async () => {
29389
+ const path = (0, import_node_path.join)(profileDir, REPLICA_LOGOUT_PENDING_FILE);
29390
+ const marker = await readLogoutMarker(path);
29391
+ if (marker === null || marker.remotePending !== true) throw gated("gate_unavailable");
29392
+ if (marker.principalDigest !== logoutPrincipalDigest(principal)) throw gated("principal_mismatch");
29393
+ const report = await currentLayout();
29394
+ if (storedInventoryChanged(marker, report)) throw changedInventoryRefusal(marker, principal, report);
29395
+ assertApprovedInventory(report, approval);
29396
+ 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) {
29397
+ throw localChangesRefusal(report.unknownPaths);
29398
+ }
29399
+ const checked = await validatedRemoteMarker(path, marker, principal, { active: true, deny: "deferred" });
29400
+ return commitPlainAbandon(path, checked.marker, report, discardLocalChanges);
29401
+ });
29402
+ },
29403
+ async approveChangedPlainLogout({ principal, approval, discardLocalChanges, inventoryApprovalToken: token }) {
29404
+ return profileSection(async () => {
29405
+ const path = (0, import_node_path.join)(profileDir, REPLICA_LOGOUT_PENDING_FILE);
29406
+ const marker = await readLogoutMarker(path);
29407
+ if (marker === null || marker.remotePending !== true) throw gated("gate_unavailable");
29408
+ if (marker.principalDigest !== logoutPrincipalDigest(principal)) throw gated("principal_mismatch");
29409
+ const report = await currentLayout();
29410
+ if (!storedInventoryChanged(marker, report)) throw gated("gate_unavailable");
29411
+ const entries = approvalEntries(report);
29412
+ if (discardLocalChanges !== true || typeof token !== "string" || !/^[0-9a-f]{32}$/.test(token) || token !== inventoryApprovalToken(marker, principal, entries)) {
29413
+ throw changedInventoryRefusal(marker, principal, report, entries);
29414
+ }
29415
+ assertApprovedInventory(report, approval);
29416
+ if (approval.knownLegacyReplicas !== report.knownLegacyReplicas || JSON.stringify(approval.unknownPaths) !== JSON.stringify(report.unknownPaths) || JSON.stringify(approval.unknownLegacyPaths) !== JSON.stringify(report.unknownLegacyPaths)) {
29417
+ throw changedInventoryRefusal(marker, principal, report, entries);
29418
+ }
29419
+ const checked = await validatedRemoteMarker(path, marker, principal, { active: true, deny: "deferred" });
29420
+ return commitPlainAbandon(path, checked.marker, report, true);
29421
+ });
29422
+ },
29423
+ async reapproveLogoutPending({ principal, approval }) {
29424
+ return profileSection(async () => {
29425
+ const path = (0, import_node_path.join)(profileDir, REPLICA_LOGOUT_PENDING_FILE);
29426
+ const marker = await readLogoutMarker(path);
29427
+ if (marker === null || marker.remotePending) throw gated("gate_unavailable");
29428
+ if (marker.principalDigest !== logoutPrincipalDigest(principal)) throw gated("principal_mismatch");
29429
+ const report = await currentLayout();
29430
+ assertApprovedInventory(report, approval);
29431
+ if (JSON.stringify(report.unknownPaths) !== JSON.stringify(approval.unknownPaths)) {
29432
+ throw localChangesRefusal(report.unknownPaths);
29433
+ }
29434
+ const checked = await validatedRemoteMarker(path, marker, principal, { active: true });
29435
+ const amended = {
29436
+ ...checked.marker,
29437
+ approvedUnknown: report.unknownEntries,
29438
+ knownRoots: report.knownRoots,
29439
+ knownEntries: report.knownEntries,
29440
+ missingDatabases: report.missingDatabases,
29441
+ ...report.unknownPaths.length > 0 ? { discardLocalChanges: true } : {}
29442
+ };
29443
+ await writeJsonAtomic(path, amended, options.dirSync);
29444
+ return amended;
29445
+ });
29446
+ },
29447
+ async recoverLogoutPending({ signal, clearCredentials, eraseLegacyReplicas }) {
29448
+ const path = (0, import_node_path.join)(profileDir, REPLICA_LOGOUT_PENDING_FILE);
29449
+ const failure = async (error) => ({
29450
+ erased: [],
29451
+ 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" }],
29452
+ remaining: Math.max(1, (await lifecycle.pendingPurge().catch(() => [])).length)
29453
+ });
29454
+ try {
29455
+ const prepared = await profileSection(async () => {
29456
+ const marker2 = await readLogoutMarker(path);
29457
+ if (marker2 === null) {
29458
+ const state2 = await (await fileGate()).readGateSnapshot(profileDir);
29459
+ return { report: { erased: [], failed: [], remaining: state2?.intents.length ?? 0 } };
29460
+ }
29461
+ signal.throwIfAborted();
29462
+ if (marker2.remotePending) throw gated("gate_unavailable");
29463
+ const gate = await fileGate();
29464
+ const { read, corrupt } = await readLogoutState(gate);
29465
+ if (corrupt) throw gated("gate_unavailable");
29466
+ let current = marker2;
29467
+ let state = read?.state;
29468
+ if (marker2.v === 2) {
29469
+ if (read !== void 0) throw gated("stamp_stale");
29470
+ } else if (read === void 0 || read.state.incarnation !== marker2.incarnation) {
29471
+ throw gated("stamp_stale");
29472
+ } else if (marker2.v === 3) {
29473
+ if (read.floorLagging || read.state.principalHash !== null || read.state.eraseDeferred || read.state.lifecycleEpoch !== marker2.lifecycleEpoch) throw gated("stamp_stale");
29474
+ } else {
29475
+ const checked = await validateV1(
29476
+ gate,
29477
+ path,
29478
+ marker2,
29479
+ read,
29480
+ { active: true, deny: "ordinary", denyActive: true }
29481
+ );
29482
+ current = checked.marker;
29483
+ state = checked.state;
29484
+ }
29485
+ await clearCredentials();
29486
+ for (const file of ["session.json", "key.json"]) {
29487
+ try {
29488
+ await (0, import_promises.lstat)((0, import_node_path.join)(profileDir, file));
29489
+ throw gated("gate_unavailable");
29490
+ } catch (error) {
29491
+ if (!isNotFound(error)) throw error;
29492
+ }
29493
+ }
29494
+ await (options.dirSync ?? fsyncDir)(profileDir);
29495
+ const report = await inspectLayout(state !== void 0, marker2);
29496
+ assertApprovedInventory(report, {
29497
+ unknownEntries: marker2.approvedUnknown,
29498
+ knownRoots: marker2.knownRoots,
29499
+ knownEntries: marker2.knownEntries,
29500
+ missingDatabases: marker2.missingDatabases
29501
+ }, true);
29502
+ if (!marker2.discardLocalChanges && report.unknownEntries.length > 0) {
29503
+ throw localChangesRefusal(report.unknownPaths);
29504
+ }
29505
+ if (current.v === 1 && state?.eraseDeferred) {
29506
+ await gate.resumeDeferredGate(profileDir, {
29507
+ deniedEpoch: current.deniedEpoch,
29508
+ principalHash: current.principalHash
29509
+ });
29510
+ }
29511
+ await eraseLegacyReplicas({
29512
+ discardLocalChanges: marker2.discardLocalChanges === true,
29513
+ approvedUnknown: marker2.approvedUnknown,
29514
+ knownRoots: marker2.knownRoots,
29515
+ knownEntries: marker2.knownEntries,
29516
+ missingDatabases: marker2.missingDatabases
29517
+ });
29518
+ try {
29519
+ await (0, import_promises.lstat)((0, import_node_path.join)(profileDir, "replicas"));
29520
+ throw gated("gate_unavailable");
29521
+ } catch (error) {
29522
+ if (!isNotFound(error)) throw error;
29523
+ }
29524
+ return { marker: current };
29525
+ });
29526
+ if ("report" in prepared) return prepared.report;
29527
+ const marker = prepared.marker;
29528
+ let erased = { erased: [], failed: [], remaining: 0 };
29529
+ if (marker.v !== 2) {
29530
+ erased = await eraseGateIntents({ signal }, marker);
29531
+ if (erased.failed.length > 0 || erased.remaining > 0) return erased;
29532
+ }
29533
+ await profileSection(async () => {
29534
+ const pending = await readLogoutMarker(path);
29535
+ if (pending === null || pending.remotePending || JSON.stringify(pending) !== JSON.stringify(marker)) {
29536
+ throw gated("stamp_stale");
29537
+ }
29538
+ const gate = await fileGate();
29539
+ const state = await gate.readGateSnapshot(profileDir);
29540
+ if (marker.v === 2 && state !== void 0) throw gated("stamp_stale");
29541
+ const report = await inspectLayout(state !== void 0, marker);
29542
+ assertApprovedInventory(report, {
29543
+ unknownEntries: marker.approvedUnknown,
29544
+ knownRoots: marker.knownRoots,
29545
+ knownEntries: marker.knownEntries,
29546
+ missingDatabases: marker.missingDatabases
29547
+ }, true);
29548
+ await eraseApprovedUnknown(profileDir, marker, "replication", diagnosticFiles);
29549
+ await eraseKnownFlagResidue(profileDir, marker, diagnosticFiles);
29550
+ const finalReport = await inspectLayout(state !== void 0, marker);
29551
+ assertApprovedInventory(finalReport, {
29552
+ unknownEntries: marker.approvedUnknown,
29553
+ knownRoots: marker.knownRoots,
29554
+ knownEntries: marker.knownEntries,
29555
+ missingDatabases: marker.missingDatabases
29556
+ }, true);
29557
+ if (finalReport.unknownPaths.length > 0) throw localChangesRefusal(finalReport.unknownPaths);
29558
+ if (finalReport.knownRoots.length > 0) throw gated("gate_unavailable");
29559
+ await (options.dirSync ?? fsyncDir)(profileDir);
29560
+ await (0, import_promises.rm)(path);
29561
+ await (options.dirSync ?? fsyncDir)(profileDir);
29562
+ });
29563
+ return erased;
29564
+ } catch (error) {
29565
+ return failure(error);
29566
+ }
29567
+ },
29568
+ async pendingPurge() {
29569
+ const gate = await fileGate();
29570
+ return (await gate.readGateSnapshot(profileDir))?.intents ?? [];
29571
+ },
29572
+ erasePending({ signal }) {
29573
+ return eraseGateIntents({ signal });
27723
29574
  }
27724
- let ino;
29575
+ };
29576
+ async function eraseGateIntents({ signal }, marker) {
29577
+ const gate = await fileGate();
29578
+ const erased = [];
29579
+ const failed = [];
29580
+ const intents = await lifecycle.pendingPurge();
27725
29581
  try {
27726
- ino = (await (0, import_promises.stat)((0, import_node_path.join)(replicaDir, "replica.db"))).ino;
27727
- } catch (error) {
27728
- if (!isNotFound(error)) throw error;
27729
- }
27730
- return new SqliteReplicaHandle({
27731
- spec,
27732
- device,
27733
- store,
27734
- replicaDir,
27735
- replica,
27736
- guard,
27737
- transportFor: options.transportFor,
27738
- dirSync: options.dirSync,
27739
- generation,
27740
- ino
29582
+ if (marker === void 0) assertLogoutClear(options.logoutPendingFile);
29583
+ else if (marker.remotePending || await readLogoutMarker((0, import_node_path.join)(profileDir, REPLICA_LOGOUT_PENDING_FILE)) === null) {
29584
+ throw gated("gate_unavailable");
29585
+ }
29586
+ } catch {
29587
+ return { erased, failed: [{ id: "logout", code: "REPLICA_GATED" }], remaining: Math.max(1, intents.length) };
29588
+ }
29589
+ const held = await profileSection(() => gate.readGateSnapshot(profileDir));
29590
+ if (held?.eraseDeferred) return { erased, failed, remaining: held.intents.length, deferred: true };
29591
+ for (const intent of intents) {
29592
+ signal.throwIfAborted();
29593
+ try {
29594
+ await profileSection(async () => {
29595
+ if (marker === void 0) assertLogoutClear(options.logoutPendingFile);
29596
+ else {
29597
+ const pending = await readLogoutMarker((0, import_node_path.join)(profileDir, REPLICA_LOGOUT_PENDING_FILE));
29598
+ if (pending === null || pending.remotePending || JSON.stringify(pending) !== JSON.stringify(marker)) {
29599
+ throw gated("gate_unavailable");
29600
+ }
29601
+ assertApprovedInventory(await inspectLayout(true), {
29602
+ unknownEntries: marker.approvedUnknown,
29603
+ knownRoots: marker.knownRoots,
29604
+ knownEntries: marker.knownEntries,
29605
+ missingDatabases: marker.missingDatabases
29606
+ }, true);
29607
+ }
29608
+ const state = await gate.readGateSnapshot(profileDir);
29609
+ if (state?.eraseDeferred) return;
29610
+ if (!state?.intents.some((queued) => queued.id === intent.id)) return;
29611
+ let idDir;
29612
+ for (const dir of await identityDirs()) {
29613
+ const identity = identityRecordOf(await readJsonFile((0, import_node_path.join)(dir, "identity.json")));
29614
+ if (identity === void 0) throw new ReplicaStorageError("STORAGE_ERROR", `Missing identity at ${dir}.`);
29615
+ const hash3 = await gate.identityHashOf(state.salt, (0, import_sdk_services3.replicationIdentityKey)(identity));
29616
+ if (intent.kind === "pending" ? hash3 === intent.identityKeyHash : intent.partitionId.startsWith(`${hash3}:`)) {
29617
+ idDir = dir;
29618
+ break;
29619
+ }
29620
+ }
29621
+ if (intent.kind === "pending") {
29622
+ if (intent.id !== `pending:${intent.identityKeyHash}`) throw gated("gate_unavailable");
29623
+ if (idDir !== void 0) {
29624
+ for (const name of ["pending.json", "device.jwk"]) {
29625
+ const file = (0, import_node_path.join)(idDir, name);
29626
+ let info;
29627
+ try {
29628
+ info = await (0, import_promises.lstat)(file, { bigint: true });
29629
+ } catch (error) {
29630
+ if (isNotFound(error)) continue;
29631
+ throw error;
29632
+ }
29633
+ if (!info.isFile()) throw gated("gate_unavailable");
29634
+ if (marker !== void 0) {
29635
+ const relative = `replication/${(0, import_node_path.basename)(idDir)}/${name}`;
29636
+ const expected = [...marker.knownEntries, ...marker.approvedUnknown].find(
29637
+ (item) => item.path === relative
29638
+ );
29639
+ if (expected?.kind !== "file" || expected.dev !== info.dev.toString() || expected.ino !== info.ino.toString()) throw localChangesRefusal([relative]);
29640
+ }
29641
+ await (0, import_promises.rm)(file);
29642
+ }
29643
+ await fsyncDir(idDir);
29644
+ }
29645
+ } else {
29646
+ if (!/^[0-9a-f]{64}:[a-z2-7]{26}$/.test(intent.partitionId) || intent.id !== intent.partitionId) {
29647
+ throw gated("gate_unavailable");
29648
+ }
29649
+ if (idDir !== void 0) {
29650
+ const replicaDir = (0, import_node_path.join)(idDir, "replicas", intent.partitionId.slice(65));
29651
+ let dirInfo;
29652
+ if (await realDirectoryExists((0, import_node_path.join)(idDir, "replicas"))) {
29653
+ try {
29654
+ dirInfo = await (0, import_promises.lstat)(replicaDir, { bigint: true });
29655
+ } catch (error) {
29656
+ if (!isNotFound(error)) throw error;
29657
+ }
29658
+ }
29659
+ if (dirInfo !== void 0) {
29660
+ const relative = `replication/${(0, import_node_path.basename)(idDir)}/replicas/${(0, import_node_path.basename)(replicaDir)}`;
29661
+ const expectedRoot = marker?.knownRoots.find((item) => item.path === relative);
29662
+ if (!dirInfo.isDirectory() || marker !== void 0 && (expectedRoot?.dev !== dirInfo.dev.toString() || expectedRoot.ino !== dirInfo.ino.toString())) throw localChangesRefusal([relative]);
29663
+ const approvedUnknown = marker?.approvedUnknown.filter(
29664
+ (item) => item.path.startsWith(`${relative}/`)
29665
+ ).map(
29666
+ (item) => ({ ...item, path: item.path.slice(relative.length + 1) })
29667
+ );
29668
+ const expectedKnown = marker?.knownEntries.filter(
29669
+ (item) => item.path.startsWith(`${relative}/`)
29670
+ ).map(
29671
+ (item) => ({ ...item, path: item.path.slice(relative.length + 1) })
29672
+ );
29673
+ const sqlite = await import("@tinycloud/replica/sqlite");
29674
+ if (!await pathExists((0, import_node_path.join)(replicaDir, "replica.db"))) {
29675
+ const layout = await sqlite.inspectSqliteReplicaLayout(replicaDir, { allowAdapterFence: true });
29676
+ 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))) {
29677
+ throw localChangesRefusal([relative]);
29678
+ }
29679
+ await sqlite.removeSqliteReplicaLayout(replicaDir, {
29680
+ approvedUnknown,
29681
+ expectedKnown: layout.knownEntries,
29682
+ expectedRoot,
29683
+ allowAdapterFence: true,
29684
+ allowMissingDatabase: true
29685
+ });
29686
+ } else {
29687
+ let store;
29688
+ try {
29689
+ store = await sqlite.SqliteReplicaStore.open(replicaDir, { create: false, guard: profileSection });
29690
+ const row = await store.open();
29691
+ if (row !== null) {
29692
+ const lease = await store.acquireSyncLease(SYNC_LEASE_TTL_MS);
29693
+ if (lease === null) throw gated("gate_unavailable");
29694
+ try {
29695
+ if (row.revoked === null) await store.reset(lease, "purged");
29696
+ let deletionKnown = expectedKnown;
29697
+ if (marker !== void 0) {
29698
+ const current = await sqlite.inspectSqliteReplicaLayout(replicaDir, {
29699
+ allowAdapterFence: true
29700
+ });
29701
+ if (current.rootIdentity.dev !== expectedRoot?.dev || current.rootIdentity.ino !== expectedRoot.ino) throw localChangesRefusal([relative]);
29702
+ const approved = new Map(expectedKnown?.map((entry) => [entry.path, entry]));
29703
+ for (const entry of current.knownEntries) {
29704
+ const prior = approved.get(entry.path);
29705
+ if (prior?.kind === entry.kind && prior.dev === entry.dev && prior.ino === entry.ino) continue;
29706
+ if (entry.kind !== "file" || !["replica.db-wal", "replica.db-shm", "replica.db-journal"].includes(entry.path)) {
29707
+ throw localChangesRefusal([`${relative}/${entry.path}`]);
29708
+ }
29709
+ }
29710
+ deletionKnown = current.knownEntries;
29711
+ }
29712
+ await store.destroy(lease, {
29713
+ approvedUnknown,
29714
+ expectedKnown: deletionKnown,
29715
+ expectedRoot,
29716
+ allowAdapterFence: true
29717
+ });
29718
+ } finally {
29719
+ await store.releaseLease(lease).catch(() => void 0);
29720
+ }
29721
+ } else {
29722
+ await sqlite.removeSqliteReplicaLayout(replicaDir, {
29723
+ approvedUnknown,
29724
+ expectedKnown,
29725
+ expectedRoot,
29726
+ allowAdapterFence: true
29727
+ });
29728
+ }
29729
+ } finally {
29730
+ await store?.close().catch(() => void 0);
29731
+ }
29732
+ }
29733
+ await fsyncDir((0, import_node_path.join)(idDir, "replicas"));
29734
+ }
29735
+ }
29736
+ }
29737
+ await gate.clearGateIntent(profileDir, intent.id);
29738
+ erased.push(intent.id);
29739
+ });
29740
+ } catch (error) {
29741
+ failed.push({ id: intent.id, code: typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : "STORAGE_ERROR" });
29742
+ }
29743
+ }
29744
+ return { erased, failed, remaining: (await lifecycle.pendingPurge()).length };
29745
+ }
29746
+ async function openHandle(spec) {
29747
+ const [replica, sqlite] = await Promise.all([
29748
+ // @ts-ignore — TS2307 when replica's dist is absent (CI builds node-sdk first).
29749
+ import("@tinycloud/replica"),
29750
+ // @ts-ignore — same as above.
29751
+ import("@tinycloud/replica/sqlite")
29752
+ ]);
29753
+ if (spec.stamp === void 0) throw gated("not_established");
29754
+ if (guard === void 0) throw gated("gate_unavailable");
29755
+ if (spec.grantPolicy === void 0) {
29756
+ throw new ReplicaStorageError("REPLICA_INVALID_ARGUMENT", "A replica grant policy is required.");
29757
+ }
29758
+ const gate = await fileGate();
29759
+ const snapshot = await gate.readGateSnapshot(profileDir);
29760
+ if (snapshot === void 0) throw gated("gate_unavailable");
29761
+ const identityKeyHash = await gate.identityHashOf(snapshot.salt, (0, import_sdk_services3.replicationIdentityKey)(spec.identity));
29762
+ const resolvePolicy = async (at, recordFirst) => {
29763
+ const policy = spec.grantPolicy;
29764
+ if (!Number.isSafeInteger(policy.grantTtlMs) || policy.grantTtlMs <= 0 || policy.grantTtlMs > import_contractConstants.MAX_GRANT_TTL_MS) {
29765
+ throw new TypeError("A replica grant requires a finite TTL not exceeding seven days.");
29766
+ }
29767
+ const verified = policy.consentAnchor;
29768
+ if (verified !== null && (!Number.isSafeInteger(verified.at) || verified.at < 0 || verified.source === "first_install")) {
29769
+ throw new ReplicaStorageError("CONSENT_EXPIRED", "The verified consent anchor is unavailable.");
29770
+ }
29771
+ const first = recordFirst ? await gate.recordFirstInstall(profileDir, identityKeyHash, at) : await gate.readFirstInstall(profileDir, identityKeyHash);
29772
+ const anchor = verified === null ? first === void 0 ? null : { at: first, source: "first_install" } : { at: Math.min(verified.at, at), source: verified.source };
29773
+ if (anchor === null) return null;
29774
+ const deadline = anchor.at + import_contractConstants.CONSENT_WINDOW_MS;
29775
+ if (!Number.isSafeInteger(anchor.at) || anchor.at < 0 || !Number.isSafeInteger(deadline)) {
29776
+ throw new ReplicaStorageError("CONSENT_EXPIRED", "The durable consent anchor is invalid.");
29777
+ }
29778
+ return { at, ttlMs: policy.grantTtlMs, consentDeadline: deadline, consentAnchor: anchor };
29779
+ };
29780
+ let partitionId;
29781
+ const storeGuard = (section) => guarded(spec.identity.principal, spec.stamp, spec.identity, partitionId, section);
29782
+ return guarded(spec.identity.principal, spec.stamp, spec.identity, void 0, async () => {
29783
+ if ((0, import_sdk_services3.requiresSecretsOptIn)(spec.space, spec.prefix) && spec.allowSecrets !== true) {
29784
+ throw new replica.ReplicaError(
29785
+ replica.ReplicaErrorCode.SECRETS_OPT_IN_REQUIRED,
29786
+ `Replicating ${spec.space}/kv/${spec.prefix} copies secret material to disk; pass allowSecrets to opt in.`
29787
+ );
29788
+ }
29789
+ const idDir = partitionDirOf(root, spec.identity);
29790
+ await ensurePartitionIdentity(idDir, spec.identity, options.dirSync);
29791
+ const createdDevice = spec.device ?? await loadOrCreateDevice(idDir, profileSection, createDevice, options.dirSync);
29792
+ const device = { ...createdDevice, did: principalOf(createdDevice.did) };
29793
+ const replicaHash = replicaHashOf(spec.prefix, device.did);
29794
+ partitionId = `${identityKeyHash}:${replicaHash}`;
29795
+ const replicaDir = (0, import_node_path.join)(idDir, "replicas", replicaHash);
29796
+ const store = await sqlite.SqliteReplicaStore.open(replicaDir, {
29797
+ create: true,
29798
+ guard: storeGuard
29799
+ });
29800
+ let generation;
29801
+ try {
29802
+ try {
29803
+ await store.init({
29804
+ name: spec.prefix.replace(/\/+$/, "") || "replica",
29805
+ replicaId: replicaHash,
29806
+ host: spec.identity.host,
29807
+ space: spec.space,
29808
+ prefix: spec.prefix,
29809
+ deviceDid: device.did,
29810
+ allowSecrets: spec.allowSecrets,
29811
+ localReadPolicy: "whileGrantValid",
29812
+ retentionGrantCid: null
29813
+ });
29814
+ } catch (error) {
29815
+ if (!replica.isReplicaError(error, replica.ReplicaErrorCode.CONFIG_MISMATCH)) {
29816
+ throw error;
29817
+ }
29818
+ }
29819
+ const state = await store.open();
29820
+ if (state === null) {
29821
+ throw new replica.ReplicaError(
29822
+ replica.ReplicaErrorCode.NOT_FOUND,
29823
+ "The replica row vanished during init."
29824
+ );
29825
+ }
29826
+ const config = state.config;
29827
+ if (config.host !== spec.identity.host || config.space !== spec.space || config.prefix !== spec.prefix) {
29828
+ throw new replica.ReplicaError(
29829
+ replica.ReplicaErrorCode.CONFIG_MISMATCH,
29830
+ `This replica follows ${config.host} ${config.space}/kv/${config.prefix}, not ${spec.identity.host} ${spec.space}/kv/${spec.prefix}.`
29831
+ );
29832
+ }
29833
+ if (principalOf(config.deviceDid) !== principalOf(device.did)) {
29834
+ throw new replica.ReplicaError(
29835
+ replica.ReplicaErrorCode.CONFIG_MISMATCH,
29836
+ "The stored replica belongs to a different device key; purge the partition to rekey."
29837
+ );
29838
+ }
29839
+ await retainGrantInstalls(gate, snapshot, identityKeyHash, store);
29840
+ if (state.grant !== null || state.pendingGrant !== null) {
29841
+ const policy = await resolvePolicy(Date.now(), false);
29842
+ if (policy === null) throw gated("gate_unavailable");
29843
+ if (state.grant !== null) await store.recomputeGrantInstall(state.grant, policy);
29844
+ if (state.pendingGrant !== null) await store.recomputeGrantInstall(state.pendingGrant, policy);
29845
+ }
29846
+ generation = state.generation;
29847
+ } catch (error) {
29848
+ await store.close().catch(() => void 0);
29849
+ throw error;
29850
+ }
29851
+ let ino;
29852
+ try {
29853
+ ino = (await (0, import_promises.stat)((0, import_node_path.join)(replicaDir, "replica.db"))).ino;
29854
+ } catch (error) {
29855
+ if (!isNotFound(error)) throw error;
29856
+ }
29857
+ return new SqliteReplicaHandle({
29858
+ spec,
29859
+ replicaId: replicaHash,
29860
+ device,
29861
+ store,
29862
+ replicaDir,
29863
+ replica,
29864
+ guard: storeGuard,
29865
+ read: (section, withFence) => readGated(spec, partitionId, store, replicaDir, generation, section, withFence),
29866
+ policy: resolvePolicy,
29867
+ recordGrantInstall: async (cid, at) => gate.recordGrantInstall(profileDir, await gate.grantInstallHashOf(snapshot.salt, identityKeyHash, cid), at),
29868
+ transportFor: options.transportFor,
29869
+ dirSync: options.dirSync,
29870
+ generation,
29871
+ ino
29872
+ });
27741
29873
  });
27742
29874
  }
27743
29875
  async function inspectStatus(spec, { signal }) {
29876
+ assertLogoutClear(options.logoutPendingFile);
27744
29877
  const checkAbort = () => {
27745
29878
  if (signal.aborted) throw signal.reason ?? Object.assign(new Error("Replica status inspection was aborted"), { code: "ABORTED" });
27746
29879
  };
@@ -27773,6 +29906,8 @@ function createSqliteReplicaStorage(options) {
27773
29906
  import("@tinycloud/replica/sqlite")
27774
29907
  ]);
27775
29908
  checkAbort();
29909
+ const syncedThroughEpoch = await readFence(replicaDir);
29910
+ checkAbort();
27776
29911
  const inspected = await sqlite.SqliteReplicaStore.inspect(replicaDir, { signal });
27777
29912
  checkAbort();
27778
29913
  if (inspected === null) return void 0;
@@ -27780,14 +29915,17 @@ function createSqliteReplicaStorage(options) {
27780
29915
  if (state.config.host !== spec.identity.host || state.config.space !== spec.space || state.config.prefix !== spec.prefix || principalOf(state.config.deviceDid) !== principalOf(device.did)) {
27781
29916
  throw new ReplicaStorageError("STORAGE_ERROR", `The replica at ${replicaDir} does not match the requested identity, space, prefix, or device.`);
27782
29917
  }
27783
- const syncedThroughEpoch = await readFence(replicaDir);
27784
29918
  checkAbort();
29919
+ assertLogoutClear(options.logoutPendingFile);
29920
+ const gateState = await (await fileGate()).readGateSnapshot(profileDir);
29921
+ const purgePending = gateState !== void 0 && (gateState.eraseDeferred || gateState.intents.length > 0);
27785
29922
  return {
29923
+ purgePending,
27786
29924
  coverage: status.coverage,
27787
29925
  lastSyncAt: status.lastSyncAt,
27788
29926
  syncedThroughEpoch,
27789
- authority: { state: status.authority.state, expiresAt: status.authority.expiresAt },
27790
- grant: newestGrantInfo(replica, state.pendingGrant ?? state.grant, state.pendingGrant, spec),
29927
+ 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 },
29928
+ grant: newestGrantInfo(replica, state.pendingGrant ?? state.grant, state.pendingGrant, spec, inspected.grantInstall),
27791
29929
  counts: status.counts,
27792
29930
  bytes: status.bytes,
27793
29931
  lastError: status.lastError
@@ -27800,89 +29938,124 @@ function createSqliteReplicaStorage(options) {
27800
29938
  // @ts-ignore — same as above.
27801
29939
  import("@tinycloud/replica/sqlite")
27802
29940
  ]);
27803
- const idDir = partitionDirOf(root, target.identity);
27804
- if (!await pathExists(idDir)) return;
27805
- const deviceDids = [];
27806
- const deviceFile = (0, import_node_path.join)(idDir, "device.jwk");
27807
- const stored = await readJsonFile(deviceFile);
27808
- if (stored !== void 0) {
27809
- const device = deviceRecordOf(stored);
27810
- if (device === void 0) {
27811
- throw new ReplicaStorageError(
27812
- "STORAGE_ERROR",
27813
- `The replica device key at ${deviceFile} is unreadable.`
27814
- );
27815
- }
27816
- deviceDids.push(device.did);
27817
- }
27818
- if (target.sessionDeviceDid !== void 0) deviceDids.push(target.sessionDeviceDid);
27819
- const devices = [...new Map(deviceDids.map((did) => [principalOf(did), did])).values()];
27820
- let firstError;
27821
- for (const deviceDid of devices) {
27822
- const replicaDir = (0, import_node_path.join)(idDir, "replicas", replicaHashOf(target.prefix, deviceDid));
27823
- if (!await pathExists(replicaDir)) continue;
27824
- try {
27825
- let store;
27826
- try {
27827
- store = await sqlite.SqliteReplicaStore.open(replicaDir, { create: false, guard });
27828
- } catch (error) {
27829
- if (replica.isReplicaError(error, replica.ReplicaErrorCode.NOT_FOUND)) {
27830
- await guard(() => (0, import_promises.rm)(replicaDir, { recursive: true, force: true }));
27831
- continue;
27832
- }
27833
- throw error;
29941
+ return profileSection(async () => {
29942
+ const idDir = partitionDirOf(root, target.identity);
29943
+ if (!await realDirectoryExists(idDir)) return;
29944
+ const hasReplicas = await realDirectoryExists((0, import_node_path.join)(idDir, "replicas"));
29945
+ const deviceDids = [];
29946
+ const deviceFile = (0, import_node_path.join)(idDir, "device.jwk");
29947
+ const stored = await readJsonFile(deviceFile);
29948
+ if (stored !== void 0) {
29949
+ const device = deviceRecordOf(stored);
29950
+ if (device === void 0) {
29951
+ throw new ReplicaStorageError(
29952
+ "STORAGE_ERROR",
29953
+ `The replica device key at ${deviceFile} is unreadable.`
29954
+ );
29955
+ }
29956
+ deviceDids.push(device.did);
29957
+ }
29958
+ if (target.sessionDeviceDid !== void 0) deviceDids.push(target.sessionDeviceDid);
29959
+ const devices = [...new Map(deviceDids.map((did) => [principalOf(did), did])).values()];
29960
+ const removeLeftover = async (replicaDir) => {
29961
+ const layout = await sqlite.inspectSqliteReplicaLayout(replicaDir, { allowAdapterFence: true });
29962
+ const missingDatabase = !await pathExists((0, import_node_path.join)(replicaDir, "replica.db"));
29963
+ const unexpected = layout.unknownPaths.filter((path) => path !== "replica.db");
29964
+ if (unexpected.length !== 0 || layout.unknownPaths.includes("replica.db") !== missingDatabase || layout.unknownEntries.some((item) => item.path === "replica.db")) {
29965
+ throw new ReplicaStorageError(
29966
+ "STORAGE_ERROR",
29967
+ `Refusing to remove unexpected replica path ${(0, import_node_path.join)(replicaDir, unexpected[0] ?? "replica.db")}.`
29968
+ );
27834
29969
  }
29970
+ await sqlite.removeSqliteReplicaLayout(replicaDir, {
29971
+ expectedRoot: layout.rootIdentity,
29972
+ expectedKnown: layout.knownEntries,
29973
+ allowAdapterFence: true,
29974
+ allowMissingDatabase: missingDatabase
29975
+ });
29976
+ };
29977
+ let firstError;
29978
+ for (const deviceDid of devices) {
29979
+ const replicaDir = (0, import_node_path.join)(idDir, "replicas", replicaHashOf(target.prefix, deviceDid));
29980
+ if (!hasReplicas || !await realDirectoryExists(replicaDir)) continue;
27835
29981
  try {
27836
- const state = await store.open();
27837
- if (state === null) {
27838
- await store.close().catch(() => void 0);
27839
- await guard(() => (0, import_promises.rm)(replicaDir, { recursive: true, force: true }));
27840
- continue;
27841
- }
27842
- const lease = await store.acquireSyncLease(SYNC_LEASE_TTL_MS);
27843
- if (lease === null) {
27844
- throw new replica.ReplicaError(
27845
- replica.ReplicaErrorCode.BUSY,
27846
- "Another process holds this replica's sync lease."
27847
- );
29982
+ let store;
29983
+ try {
29984
+ store = await sqlite.SqliteReplicaStore.open(replicaDir, { create: false, guard: profileSection });
29985
+ } catch (error) {
29986
+ if (replica.isReplicaError(error, replica.ReplicaErrorCode.NOT_FOUND)) {
29987
+ await removeLeftover(replicaDir);
29988
+ continue;
29989
+ }
29990
+ throw error;
27848
29991
  }
27849
29992
  try {
27850
- if (state.revoked === null) {
27851
- await store.reset(lease, "purged");
29993
+ const state = await store.open();
29994
+ if (state === null) {
29995
+ await store.close().catch(() => void 0);
29996
+ await removeLeftover(replicaDir);
29997
+ continue;
29998
+ }
29999
+ const lease = await store.acquireSyncLease(SYNC_LEASE_TTL_MS);
30000
+ if (lease === null) {
30001
+ throw new replica.ReplicaError(
30002
+ replica.ReplicaErrorCode.BUSY,
30003
+ "Another process holds this replica's sync lease."
30004
+ );
30005
+ }
30006
+ try {
30007
+ const gate = await fileGate();
30008
+ const currentGate = await gate.readGateSnapshot(profileDir);
30009
+ if (currentGate !== void 0 && currentGate.principalHash !== null && currentGate.principalHash === await gate.principalHashOf(currentGate.salt, target.identity.principal)) {
30010
+ const identityKeyHash = await gate.identityHashOf(currentGate.salt, (0, import_sdk_services3.replicationIdentityKey)(target.identity));
30011
+ await retainGrantInstalls(gate, currentGate, identityKeyHash, store);
30012
+ }
30013
+ if (state.revoked === null) {
30014
+ await store.reset(lease, "purged");
30015
+ }
30016
+ await store.destroy(lease, { allowAdapterFence: true });
30017
+ } finally {
30018
+ await store.releaseLease(lease).catch(() => void 0);
27852
30019
  }
27853
- await store.destroy(lease);
27854
30020
  } finally {
27855
- await store.releaseLease(lease).catch(() => void 0);
30021
+ await store.close().catch(() => void 0);
27856
30022
  }
27857
- } finally {
27858
- await store.close().catch(() => void 0);
30023
+ } catch (error) {
30024
+ firstError ?? (firstError = error);
27859
30025
  }
27860
- } catch (error) {
27861
- firstError ?? (firstError = error);
27862
30026
  }
27863
- }
27864
- if (firstError !== void 0) throw firstError;
30027
+ if (firstError !== void 0) throw firstError;
30028
+ });
27865
30029
  }
27866
30030
  return {
27867
30031
  kind: "sqlite",
30032
+ lifecycle,
27868
30033
  open: openHandle,
27869
30034
  inspectStatus,
27870
30035
  purge: purgePartition,
27871
- // Always the durable partitioned file store (review): the runtime
27872
- // must never fall back to volatile in-memory pending state — a
27873
- // restart that lost ambiguous write records would serve stale data.
27874
- // Without a guard the file store still durably serializes each update;
27875
- // the guard only adds cross-process read-modify-write exclusion.
27876
- pendingWrites(identity) {
30036
+ // File-backed pending writes are always bound to a stamped gate.
30037
+ pendingWrites(identity, opts) {
27877
30038
  return new FilePendingWriteStore(
27878
30039
  identity,
27879
30040
  partitionDirOf(root, identity),
30041
+ profileDir,
27880
30042
  guard,
27881
- options.dirSync
30043
+ opts?.stamp,
30044
+ options.dirSync,
30045
+ options.logoutPendingFile
27882
30046
  );
27883
30047
  }
27884
30048
  };
27885
30049
  }
30050
+ function createProfileReplicaPurge(options) {
30051
+ return createSqliteReplicaStorage({
30052
+ dir: (0, import_node_path.join)(options.profileDir, "replication"),
30053
+ guard: options.guard,
30054
+ dirSync: options.dirSync,
30055
+ logoutPendingFile: (0, import_node_path.join)(options.profileDir, REPLICA_LOGOUT_PENDING_FILE),
30056
+ diagnosticFiles: options.diagnosticFiles
30057
+ }).lifecycle;
30058
+ }
27886
30059
  function sqliteReplicaStorage(options) {
27887
30060
  return createSqliteReplicaStorage(options);
27888
30061
  }
@@ -27936,6 +30109,8 @@ function sqliteReplicaStorage(options) {
27936
30109
  PrefixedKVService,
27937
30110
  PrivateKeySigner,
27938
30111
  ProtocolMismatchError,
30112
+ REPLICA_LOGOUT_PENDING_FILE,
30113
+ RESTORED_GATE_MESSAGE,
27939
30114
  RUNTIME_DELEGATION_REJECTIONS,
27940
30115
  RuntimeDelegationRejectedError,
27941
30116
  SECRET_NAME_RE,
@@ -27968,6 +30143,7 @@ function sqliteReplicaStorage(options) {
27968
30143
  activateValidatedRuntimeDelegation,
27969
30144
  addressStorageKey,
27970
30145
  appSecretScope,
30146
+ assertProfileLogoutClear,
27971
30147
  buildCanonicalDecryptRequest,
27972
30148
  buildDecryptAttenuation,
27973
30149
  buildDecryptFacts,
@@ -27988,6 +30164,7 @@ function sqliteReplicaStorage(options) {
27988
30164
  composeManifestRequest,
27989
30165
  createCapabilityKeyRegistry,
27990
30166
  createOpenKeyCallbackSigningStrategy,
30167
+ createProfileReplicaPurge,
27991
30168
  createSharingService,
27992
30169
  createSpaceService,
27993
30170
  createVaultCrypto,
@@ -28024,6 +30201,7 @@ function sqliteReplicaStorage(options) {
28024
30201
  isNetworkId,
28025
30202
  isStorageFullError,
28026
30203
  loadManifest,
30204
+ logoutPrincipalDigest,
28027
30205
  makePkhSpaceId,
28028
30206
  makePublicSpaceId,
28029
30207
  networkDiscoveryKey,