@tinycloud/node-sdk 3.2.6-beta.5 → 3.3.0-beta.10

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-CcNh20QX.d.cts} +181 -286
  2. package/dist/{WasmKeyProvider-DEdSyQ0I.d.ts → TinyCloudNode-CcNh20QX.d.ts} +181 -286
  3. package/dist/WasmKeyProvider-5Ee7fcAw.d.cts +217 -0
  4. package/dist/WasmKeyProvider-5Ee7fcAw.d.ts +217 -0
  5. package/dist/authority-Cwotwza3.d.cts +118 -0
  6. package/dist/authority-D_u9a7Wl.d.ts +118 -0
  7. package/dist/core.cjs +400 -168
  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 +407 -176
  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 +2734 -545
  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 +2729 -545
  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);
@@ -20873,6 +21175,7 @@ function ownerPermissionsToAbilities(permissions) {
20873
21175
  }
20874
21176
  return { abilities, rawAbilities };
20875
21177
  }
21178
+ var TERMINAL_DELEGATION_REQUIRES_SESSION_KEY = "disableSubDelegation requires a session-key delegation";
20876
21179
  function base64UrlEncode(bytes) {
20877
21180
  const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
20878
21181
  let output = "";
@@ -20984,6 +21287,20 @@ function accountIndexWarningDiagnostic(error) {
20984
21287
  }
20985
21288
  return diagnostic;
20986
21289
  }
21290
+ function replicaRestoreRefusal(error) {
21291
+ if (error !== null && typeof error === "object" && "code" in error && error.code === "REPLICA_GATED" && "gate" in error) {
21292
+ switch (error.gate) {
21293
+ case "not_established":
21294
+ case "stamp_stale":
21295
+ case "principal_mismatch":
21296
+ case "purge_pending":
21297
+ case "logout_pending":
21298
+ case "gate_unavailable":
21299
+ return error.gate;
21300
+ }
21301
+ }
21302
+ return "gate_unavailable";
21303
+ }
20987
21304
  var _TinyCloudNode = class _TinyCloudNode {
20988
21305
  /**
20989
21306
  * Create a new TinyCloudNode instance.
@@ -21013,6 +21330,37 @@ var _TinyCloudNode = class _TinyCloudNode {
21013
21330
  this.auth = null;
21014
21331
  this.tc = null;
21015
21332
  this._chainId = 1;
21333
+ /** Mints the replica session stamp on each fresh sign-in (TC-858 §4.1). Undefined when replication is off. */
21334
+ this.establishReplicaStamp = async (session) => {
21335
+ const replication = this.config.replication;
21336
+ if (!replication?.enabled) return void 0;
21337
+ const epoch = this.sessionEpoch;
21338
+ const attempt = this.latestSessionAttempt;
21339
+ try {
21340
+ const { verifiedOwnerConsentAnchor: verifiedOwnerConsentAnchor2, establishReplicaSessionStamp: establishReplicaSessionStamp2 } = await getNodeReplicationLoaders().authority();
21341
+ if (epoch !== this.sessionEpoch || attempt !== this.latestSessionAttempt) return void 0;
21342
+ const anchor = verifiedOwnerConsentAnchor2(session, this.wasmBindings, exactVerifiedRecap);
21343
+ if (anchor === null) {
21344
+ throw Object.assign(new Error("Replica sign-in requires a verified finite owner SIWE and exact-space KV get+sync."), {
21345
+ code: "REPLICA_GATED",
21346
+ gate: "not_established"
21347
+ });
21348
+ }
21349
+ const stamp = await establishReplicaSessionStamp2(
21350
+ replication.storage,
21351
+ (0, import_sdk_core9.pkhDid)(session.address, session.chainId),
21352
+ anchor
21353
+ );
21354
+ if (epoch !== this.sessionEpoch || attempt !== this.latestSessionAttempt) return void 0;
21355
+ this.replicaSessionStamp = stamp;
21356
+ return stamp;
21357
+ } catch (error) {
21358
+ if (epoch === this.sessionEpoch && attempt === this.latestSessionAttempt) {
21359
+ this.replicaRestoreGate = replicaRestoreRefusal(error);
21360
+ }
21361
+ return void 0;
21362
+ }
21363
+ };
21016
21364
  this._baseVaults = /* @__PURE__ */ new Map();
21017
21365
  this._baseSecrets = /* @__PURE__ */ new Map();
21018
21366
  this._secrets = /* @__PURE__ */ new Map();
@@ -21137,7 +21485,7 @@ var _TinyCloudNode = class _TinyCloudNode {
21137
21485
  });
21138
21486
  return this.wasmBindings.invokeAny(invocationSession, caveatPreservingEntries, facts);
21139
21487
  };
21140
- assertValidReplicationConfig(config.replication);
21488
+ if (config.replication?.enabled) getNodeReplicationLoaders().validateConfig(config.replication);
21141
21489
  this.explicitHost = config.host;
21142
21490
  this.config = {
21143
21491
  ...config,
@@ -21152,6 +21500,16 @@ var _TinyCloudNode = class _TinyCloudNode {
21152
21500
  this.replicationRuntime = runtime;
21153
21501
  this.replicationControl = {
21154
21502
  status: async () => {
21503
+ const gate = this.replicaRestoreGate;
21504
+ if (gate) return this.config.replication.prefixes.map((prefix) => ({
21505
+ prefix,
21506
+ state: "gated",
21507
+ gate,
21508
+ errorCode: "REPLICA_GATED",
21509
+ pending: { inFlight: 0, committed: 0, ambiguous: 0 },
21510
+ pinned: [],
21511
+ lagMs: null
21512
+ }));
21155
21513
  const status = await runtime.then((ready) => ready.control.status());
21156
21514
  const scopeError = this.replicationScopeError;
21157
21515
  if (!scopeError) return status;
@@ -21168,7 +21526,15 @@ var _TinyCloudNode = class _TinyCloudNode {
21168
21526
  lagMs: null
21169
21527
  }));
21170
21528
  },
21171
- sync: async (input) => (await runtime).control.sync(input),
21529
+ sync: async (input) => {
21530
+ if (this.replicaRestoreGate) {
21531
+ throw Object.assign(new Error("Replica restore is gated."), {
21532
+ code: "REPLICA_GATED",
21533
+ gate: this.replicaRestoreGate
21534
+ });
21535
+ }
21536
+ return (await runtime).control.sync(input);
21537
+ },
21172
21538
  purge: (input) => {
21173
21539
  const instance = this.replicationRuntimeInstance;
21174
21540
  return instance ? instance.control.purge(input) : runtime.then((ready) => ready.control.purge(input));
@@ -21384,6 +21750,7 @@ var _TinyCloudNode = class _TinyCloudNode {
21384
21750
  signer: this.signer,
21385
21751
  signStrategy: config.signStrategy ?? { type: "auto-sign" },
21386
21752
  wasmBindings: this.wasmBindings,
21753
+ ...config.replication?.enabled === true ? { establishReplicaStamp: this.establishReplicaStamp } : {},
21387
21754
  sessionManager: this.sessionManager,
21388
21755
  sessionStorage: config.sessionStorage ?? new MemorySessionStorage(),
21389
21756
  domain: this.siweDomain,
@@ -21581,11 +21948,10 @@ var _TinyCloudNode = class _TinyCloudNode {
21581
21948
  return this.signer === null;
21582
21949
  }
21583
21950
  /**
21584
- * Get the space ID for this user.
21585
- * Available after signIn().
21951
+ * Primary space of the signed-in or restored network session, including compact sessions.
21586
21952
  */
21587
21953
  get spaceId() {
21588
- return this.auth?.tinyCloudSession?.spaceId;
21954
+ return this.auth?.tinyCloudSession?.spaceId ?? this._activeServiceSession?.spaceId;
21589
21955
  }
21590
21956
  /**
21591
21957
  * Get the account space ID for this wallet identity.
@@ -21633,6 +21999,12 @@ var _TinyCloudNode = class _TinyCloudNode {
21633
21999
  get restorableSession() {
21634
22000
  return this.currentTinyCloudSession();
21635
22001
  }
22002
+ /** The active, verified replica stamp. Undefined when the replica is refused or the session has ended. */
22003
+ get replicaStamp() {
22004
+ const stamp = this.replicaSessionStamp;
22005
+ if (!stamp || this.replicaRestoreGate !== void 0 || !this.currentTinyCloudSession()) return void 0;
22006
+ return { incarnation: stamp.incarnation, lifecycleEpoch: stamp.lifecycleEpoch };
22007
+ }
21636
22008
  /**
21637
22009
  * Sign out and end the session on this instance.
21638
22010
  *
@@ -21653,18 +22025,60 @@ var _TinyCloudNode = class _TinyCloudNode {
21653
22025
  }
21654
22026
  async endSession() {
21655
22027
  const leavingAddress = this.address ?? this._restoredTcSession?.address;
22028
+ const leavingChainId = this._chainId;
22029
+ const leavingPrincipal = this.replicaSessionPrincipal ?? (leavingAddress ? (0, import_sdk_core9.pkhDid)(leavingAddress, leavingChainId) : void 0);
21656
22030
  const leavingAuth = this.auth;
22031
+ const leavingContext = this._serviceContext;
22032
+ let denied;
21657
22033
  try {
21658
22034
  this.teardownSession();
21659
22035
  } finally {
22036
+ const denial = leavingPrincipal ? this.denyReplicaGate({ reason: "sign_out", principal: leavingPrincipal }, leavingContext) : Promise.resolve(void 0);
21660
22037
  try {
21661
- await this.accountRegistryTail;
21662
- if (this.replicationRuntime) await (await this.replicationRuntime).unbind();
22038
+ denied = await denial;
21663
22039
  } finally {
21664
- if (leavingAddress) {
21665
- await (leavingAuth ? leavingAuth.clearPersistedSession(leavingAddress) : this.config.sessionStorage?.clear(leavingAddress));
22040
+ try {
22041
+ await this.accountRegistryTail;
22042
+ await this.quiesceReplicaRuntime();
22043
+ } finally {
22044
+ if (leavingAddress) {
22045
+ await (leavingAuth ? leavingAuth.clearPersistedSession(leavingAddress) : this.config.sessionStorage?.clear(leavingAddress));
22046
+ }
22047
+ }
22048
+ }
22049
+ }
22050
+ if (denied?.status === "unguarded") {
22051
+ throw new Error(`Replica gate was not denied (${denied.code ?? "unknown"}); the replica may still be restorable.`);
22052
+ }
22053
+ }
22054
+ /**
22055
+ * Step 1 of replica sign-out. Never rejects: failures come back as `unguarded` so the session
22056
+ * is still cleared. Returns `undefined` when replication is off.
22057
+ */
22058
+ async denyReplicaGate(input, context = this._serviceContext) {
22059
+ const replication = this.config.replication;
22060
+ if (!replication?.enabled) return void 0;
22061
+ try {
22062
+ const lifecycle = replication.storage.lifecycle;
22063
+ if (lifecycle?.kind !== (replication.storage.kind === "indexeddb" ? "indexeddb" : "file")) {
22064
+ return { status: "unguarded", code: "gate_unavailable" };
22065
+ }
22066
+ const result = await lifecycle.deny(input);
22067
+ if (result.status === "denied" || result.status === "deleted" || result.status === "marker") {
22068
+ try {
22069
+ (await getNodeReplicationLoaders().authority()).reportReplicaDenial(
22070
+ result,
22071
+ input.reason,
22072
+ replication.onEvent,
22073
+ context
22074
+ );
22075
+ } catch {
21666
22076
  }
21667
22077
  }
22078
+ return result;
22079
+ } catch (error) {
22080
+ const code = typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : "gate_unavailable";
22081
+ return { status: "unguarded", code };
21668
22082
  }
21669
22083
  }
21670
22084
  /**
@@ -21684,15 +22098,9 @@ var _TinyCloudNode = class _TinyCloudNode {
21684
22098
  this._activeServiceSession = void 0;
21685
22099
  oldGraph.retire();
21686
22100
  oldCore?.retireServices();
21687
- oldAuth?.resetSession(
21688
- this.sessionManager,
21689
- this.sessionKeyId,
21690
- this.explicitHost ? [this.explicitHost] : void 0
21691
- );
21692
22101
  this.config.host = this.explicitHost ?? DEFAULT_HOST;
21693
22102
  this._chainId = 1;
21694
22103
  this._restoredNodeFeatures = void 0;
21695
- this._activeServiceSession = void 0;
21696
22104
  this._serviceContext = void 0;
21697
22105
  this._kv = void 0;
21698
22106
  this._sql = void 0;
@@ -21717,6 +22125,9 @@ var _TinyCloudNode = class _TinyCloudNode {
21717
22125
  this._bootstrapStatus = { skipped: false };
21718
22126
  this.replicationDelegateSession = void 0;
21719
22127
  this.replicationScopeError = void 0;
22128
+ this.replicaSessionPrincipal = void 0;
22129
+ this.replicaSessionStamp = void 0;
22130
+ this.replicaRestoreGate = void 0;
21720
22131
  const sessionKey = createDefaultSessionKey(this.wasmBindings);
21721
22132
  this.sessionManager = sessionKey.manager;
21722
22133
  this.sessionKeyId = sessionKey.keyId;
@@ -21726,6 +22137,9 @@ var _TinyCloudNode = class _TinyCloudNode {
21726
22137
  sessionKey.keyId,
21727
22138
  this.explicitHost ? [this.explicitHost] : void 0
21728
22139
  );
22140
+ this.replicaSessionPrincipal = void 0;
22141
+ this.replicaSessionStamp = void 0;
22142
+ this.replicaRestoreGate = void 0;
21729
22143
  this._serviceGraph = this.createServiceGraphLifetime();
21730
22144
  this.auth = oldAuth;
21731
22145
  this.tc = this.auth ? new import_sdk_core9.TinyCloud(this.auth, {
@@ -21755,11 +22169,25 @@ var _TinyCloudNode = class _TinyCloudNode {
21755
22169
  "Cannot signIn() in session-only mode. Provide a privateKey in config to create your own space."
21756
22170
  );
21757
22171
  }
21758
- const epoch = this.sessionEpoch;
21759
- const attempt = ++this.latestSessionAttempt;
21760
- await this.signOutInFlight;
22172
+ await this.signInWith(() => this.tc.signIn(options));
22173
+ }
22174
+ /**
22175
+ * Fresh OpenKey sign-in. The callback receives the SDK's original SIWE and
22176
+ * session JWK before approval; the signed result is verified against those
22177
+ * bytes before the network session or replica stamp is committed.
22178
+ */
22179
+ async signInWithOpenKey(authorizeFn, options) {
22180
+ if (!this.signer || !this.tc || !this.auth) {
22181
+ throw new Error("Cannot signInWithOpenKey() in session-only mode. Provide a signer in config.");
22182
+ }
22183
+ await this.signInWith(() => this.auth.signInWithOpenKey(authorizeFn, options));
22184
+ }
22185
+ async signInWith(performSignIn) {
22186
+ const epoch = this.sessionEpoch;
22187
+ const attempt = ++this.latestSessionAttempt;
22188
+ await this.signOutInFlight;
21761
22189
  try {
21762
- await this.establishSignIn(epoch, options);
22190
+ await this.establishSignIn(epoch, performSignIn);
21763
22191
  } catch (error) {
21764
22192
  if (epoch !== this.sessionEpoch && attempt === this.latestSessionAttempt) {
21765
22193
  await this.endSession();
@@ -21767,7 +22195,7 @@ var _TinyCloudNode = class _TinyCloudNode {
21767
22195
  throw error;
21768
22196
  }
21769
22197
  }
21770
- async establishSignIn(epoch, options) {
22198
+ async establishSignIn(epoch, performSignIn) {
21771
22199
  if (!this.signer || !this.tc) {
21772
22200
  throw new Error("Not signed in. Call signIn() first.");
21773
22201
  }
@@ -21777,6 +22205,12 @@ var _TinyCloudNode = class _TinyCloudNode {
21777
22205
  const address = (0, import_sdk_core9.canonicalizeAddress)(await this.signer.getAddress());
21778
22206
  const chainId = await this.signer.getChainId();
21779
22207
  this.assertSessionEpoch(epoch, "Sign-in");
22208
+ const nextPrincipal = (0, import_sdk_core9.pkhDid)(address, chainId);
22209
+ const priorPrincipal = this.replicaSessionPrincipal ?? (this.address ? (0, import_sdk_core9.pkhDid)(this.address, this._chainId) : void 0);
22210
+ if (this.config.replication?.enabled && priorPrincipal && priorPrincipal !== nextPrincipal) {
22211
+ const denial = await this.denyReplicaGate({ reason: "account_switch", principal: priorPrincipal, next: nextPrincipal });
22212
+ this.replicaRestoreGate = denial?.status === "unguarded" ? "gate_unavailable" : "stamp_stale";
22213
+ }
21780
22214
  if (address !== this._address || chainId !== this._chainId) this.storageFull.clear();
21781
22215
  this._address = address;
21782
22216
  this._chainId = chainId;
@@ -21794,7 +22228,20 @@ var _TinyCloudNode = class _TinyCloudNode {
21794
22228
  this.runtimePermissionGrants = [];
21795
22229
  this.confirmedHostedSpaceIds.clear();
21796
22230
  this.hostedOwnedSpaceIds.clear();
21797
- await this.tc.signIn(options);
22231
+ const signInAttempt = this.latestSessionAttempt;
22232
+ const previousReplicaGate = this.replicaRestoreGate;
22233
+ const previousReplicaStamp = this.replicaSessionStamp;
22234
+ this.replicaRestoreGate = void 0;
22235
+ this.replicaSessionStamp = void 0;
22236
+ try {
22237
+ await performSignIn();
22238
+ } catch (error) {
22239
+ if (epoch === this.sessionEpoch && signInAttempt === this.latestSessionAttempt) {
22240
+ this.replicaRestoreGate = previousReplicaGate;
22241
+ this.replicaSessionStamp = previousReplicaStamp;
22242
+ }
22243
+ throw error;
22244
+ }
21798
22245
  this.assertSessionEpoch(epoch, "Sign-in");
21799
22246
  this.cancelOwnedSpaceHosts();
21800
22247
  this.syncResolvedHostFromAuth();
@@ -21802,8 +22249,9 @@ var _TinyCloudNode = class _TinyCloudNode {
21802
22249
  this.hostedOwnedSpaceIds.add(spaceId);
21803
22250
  }
21804
22251
  this.replicationDelegateSession = void 0;
22252
+ this.replicaSessionPrincipal = (0, import_sdk_core9.pkhDid)(address, chainId);
21805
22253
  this.replicationScopeError = void 0;
21806
- if (this.replicationRuntime) await (await this.replicationRuntime).unbind();
22254
+ await this.quiesceReplicaRuntime();
21807
22255
  this.assertSessionEpoch(epoch, "Sign-in");
21808
22256
  const oldGraph = this._serviceGraph;
21809
22257
  this._serviceGraph = this.createServiceGraphLifetime();
@@ -22055,7 +22503,7 @@ var _TinyCloudNode = class _TinyCloudNode {
22055
22503
  );
22056
22504
  }
22057
22505
  }
22058
- await new Promise((resolve) => setTimeout(resolve, 100));
22506
+ await new Promise((resolve2) => setTimeout(resolve2, 100));
22059
22507
  for (const step of steps) {
22060
22508
  if (step.kind !== "activate") continue;
22061
22509
  graph.assertActive();
@@ -22390,6 +22838,9 @@ var _TinyCloudNode = class _TinyCloudNode {
22390
22838
  if (this.ownStorageFull) {
22391
22839
  return;
22392
22840
  }
22841
+ if (!this.currentSessionCoversAccountRegistry()) {
22842
+ return;
22843
+ }
22393
22844
  const controller = new AbortController();
22394
22845
  const deadline = setTimeout(
22395
22846
  () => controller.abort(new DOMException("Account registry deadline exceeded", "TimeoutError")),
@@ -22504,6 +22955,27 @@ var _TinyCloudNode = class _TinyCloudNode {
22504
22955
  (operation) => operation.service === "space" && this.actionContains(operation.action, "tinycloud.space/list")
22505
22956
  );
22506
22957
  }
22958
+ /**
22959
+ * Whether the current primary session's recap covers the account index read
22960
+ * (`tinycloud.sql/read` on the owned account space's `account` database),
22961
+ * the first call of every registry sync. As in
22962
+ * {@link currentSessionCanListSpaces}, a session with no parseable recap
22963
+ * keeps today's behavior.
22964
+ */
22965
+ currentSessionCoversAccountRegistry() {
22966
+ const session = this.currentTinyCloudSession();
22967
+ const operations = session ? this.recapOperationsFromSession(session) : [];
22968
+ if (operations.length === 0) {
22969
+ return true;
22970
+ }
22971
+ const indexRead = {
22972
+ spaceId: this.ownedSpaceId(import_sdk_core9.ACCOUNT_REGISTRY_SPACE),
22973
+ service: "sql",
22974
+ path: "account",
22975
+ action: "tinycloud.sql/read"
22976
+ };
22977
+ return operations.some((granted) => this.operationCovers(granted, indexRead));
22978
+ }
22507
22979
  async withAccountRegistryRetry(task, signal) {
22508
22980
  const delays = [250, 1e3, 3e3];
22509
22981
  const stopped = (detail) => {
@@ -22544,10 +23016,10 @@ var _TinyCloudNode = class _TinyCloudNode {
22544
23016
  }
22545
23017
  lastError = error;
22546
23018
  if (attempt < delays.length - 1) {
22547
- await new Promise((resolve, reject2) => {
23019
+ await new Promise((resolve2, reject2) => {
22548
23020
  const timer = setTimeout(() => {
22549
23021
  signal.removeEventListener("abort", onAbort);
22550
- resolve();
23022
+ resolve2();
22551
23023
  }, delays[attempt]);
22552
23024
  const onAbort = () => {
22553
23025
  clearTimeout(timer);
@@ -22787,7 +23259,7 @@ var _TinyCloudNode = class _TinyCloudNode {
22787
23259
  }
22788
23260
  entry.waiters += 1;
22789
23261
  const shared = entry;
22790
- return new Promise((resolve, reject2) => {
23262
+ return new Promise((resolve2, reject2) => {
22791
23263
  const onAbort = () => {
22792
23264
  reject2(signal.reason);
22793
23265
  shared.waiters -= 1;
@@ -22796,7 +23268,7 @@ var _TinyCloudNode = class _TinyCloudNode {
22796
23268
  if (this.ownedSpaceHosts.get(spaceId) === shared) this.ownedSpaceHosts.delete(spaceId);
22797
23269
  };
22798
23270
  signal.addEventListener("abort", onAbort, { once: true });
22799
- shared.promise.then(resolve, reject2).finally(() => signal.removeEventListener("abort", onAbort));
23271
+ shared.promise.then(resolve2, reject2).finally(() => signal.removeEventListener("abort", onAbort));
22800
23272
  });
22801
23273
  }
22802
23274
  /** Session replacement: cancel the replaced session's in-flight hosts and forget them. */
@@ -22970,11 +23442,12 @@ var _TinyCloudNode = class _TinyCloudNode {
22970
23442
  );
22971
23443
  const stagedHost = resolvedHost ?? this.config.host;
22972
23444
  let restoredNodeFeatures;
23445
+ let replicaProtocolMismatch = false;
22973
23446
  if (this.config.replication?.enabled) {
22974
23447
  try {
22975
23448
  restoredNodeFeatures = (await (0, import_sdk_core9.checkNodeInfo)(stagedHost, this.wasmBindings.protocolVersion())).features;
22976
23449
  } catch (error) {
22977
- if (!(error instanceof import_sdk_core9.VersionCheckError)) throw error;
23450
+ replicaProtocolMismatch = error instanceof import_sdk_core9.ProtocolMismatchError;
22978
23451
  }
22979
23452
  }
22980
23453
  const stagedAddress = restoredAddress;
@@ -22982,28 +23455,55 @@ var _TinyCloudNode = class _TinyCloudNode {
22982
23455
  let replicationPrincipal;
22983
23456
  let compactReplicationSession;
22984
23457
  let replicationScopeError;
23458
+ let replicationScopeUnavailable = false;
22985
23459
  if (this.config.replication?.enabled) {
22986
23460
  if (stagedAddress !== void 0) {
22987
23461
  replicationPrincipal = (0, import_sdk_core9.pkhDid)(stagedAddress, stagedChainId);
22988
23462
  } 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 = {
23463
+ try {
23464
+ const { replicationScopeFromSignedSession: replicationScopeFromSignedSession2 } = await getNodeReplicationLoaders().authority();
23465
+ const signedScope = replicationScopeFromSignedSession2({
23001
23466
  delegationHeader: sessionData.delegationHeader,
23002
23467
  delegationCid: sessionData.delegationCid,
23003
- spaceId: signedScope.space,
23004
23468
  verificationMethod: restoredVerificationMethod,
23005
- jwk: stagedJwk
23006
- };
23469
+ persistedSpace: sessionData.spaceId
23470
+ });
23471
+ if (!signedScope) {
23472
+ replicationScopeError = "Persisted space does not match the locally verified delegation scope.";
23473
+ } else {
23474
+ replicationPrincipal = signedScope.principal;
23475
+ compactReplicationSession = {
23476
+ delegationHeader: sessionData.delegationHeader,
23477
+ delegationCid: sessionData.delegationCid,
23478
+ spaceId: signedScope.space,
23479
+ verificationMethod: restoredVerificationMethod,
23480
+ jwk: stagedJwk
23481
+ };
23482
+ }
23483
+ } catch {
23484
+ replicationScopeUnavailable = true;
23485
+ replicationScopeError = "Replica scope could not be verified for this network session.";
23486
+ }
23487
+ }
23488
+ }
23489
+ let replicaRestoreGate;
23490
+ let restoredReplicaStamp;
23491
+ const replication = this.config.replication;
23492
+ if (replication?.enabled) {
23493
+ if (replicaProtocolMismatch) {
23494
+ replicaRestoreGate = "gate_unavailable";
23495
+ } else if (replicationPrincipal === void 0) {
23496
+ replicaRestoreGate = replicationScopeUnavailable ? "gate_unavailable" : "principal_mismatch";
23497
+ } else {
23498
+ try {
23499
+ const { restoreReplicaSessionStamp: restoreReplicaSessionStamp2 } = await getNodeReplicationLoaders().authority();
23500
+ restoredReplicaStamp = await restoreReplicaSessionStamp2(
23501
+ replication.storage,
23502
+ replicationPrincipal,
23503
+ sessionData.replicaStamp ?? void 0
23504
+ );
23505
+ } catch (error) {
23506
+ replicaRestoreGate = replicaRestoreRefusal(error);
23007
23507
  }
23008
23508
  }
23009
23509
  }
@@ -23096,7 +23596,10 @@ var _TinyCloudNode = class _TinyCloudNode {
23096
23596
  }
23097
23597
  this.replicationDelegateSession = compactReplicationSession;
23098
23598
  this.replicationScopeError = replicationScopeError;
23099
- if (this.replicationRuntime) await (await this.replicationRuntime).unbind();
23599
+ this.replicaRestoreGate = replicaRestoreGate;
23600
+ this.replicaSessionPrincipal = replicationPrincipal;
23601
+ this.replicaSessionStamp = restoredReplicaStamp;
23602
+ await this.quiesceReplicaRuntime();
23100
23603
  if (epoch !== this.sessionEpoch) {
23101
23604
  oldGraph.retire();
23102
23605
  oldCore?.retireServices?.();
@@ -23111,7 +23614,7 @@ var _TinyCloudNode = class _TinyCloudNode {
23111
23614
  detail: replicationScopeError
23112
23615
  });
23113
23616
  }
23114
- await this.bindReplicationRuntime(stagedGraph.serviceContext, serviceSession, stagedGraph.kv, replicationPrincipal);
23617
+ await this.bindReplicaOrGate(stagedGraph.serviceContext, serviceSession, stagedGraph.kv, replicationPrincipal);
23115
23618
  oldCore?.retireServices?.();
23116
23619
  if (epoch !== this.sessionEpoch) {
23117
23620
  if (attempt === this.latestSessionAttempt) await this.endSession();
@@ -23244,6 +23747,7 @@ var _TinyCloudNode = class _TinyCloudNode {
23244
23747
  session: input.serviceSession,
23245
23748
  invoke: graph.invoke,
23246
23749
  invokeAny: graph.invokeAny,
23750
+ signOwnerRevocation: this.signer ? (cid) => this.signOwnerRevocation(cid) : void 0,
23247
23751
  fetch: graph.fetch
23248
23752
  });
23249
23753
  const keyProvider = new WasmKeyProvider({ sessionManager: input.manager });
@@ -23587,6 +24091,7 @@ var _TinyCloudNode = class _TinyCloudNode {
23587
24091
  signer: this.signer,
23588
24092
  signStrategy: this.config.signStrategy ?? { type: "auto-sign" },
23589
24093
  wasmBindings: this.wasmBindings,
24094
+ ...this.config.replication?.enabled === true ? { establishReplicaStamp: this.establishReplicaStamp } : {},
23590
24095
  sessionManager: this.sessionManager,
23591
24096
  sessionStorage: options?.sessionStorage ?? this.config.sessionStorage ?? new MemorySessionStorage(),
23592
24097
  domain: this.siweDomain,
@@ -23645,6 +24150,7 @@ var _TinyCloudNode = class _TinyCloudNode {
23645
24150
  signer: this.signer,
23646
24151
  signStrategy: this.config.signStrategy ?? { type: "auto-sign" },
23647
24152
  wasmBindings: this.wasmBindings,
24153
+ ...this.config.replication?.enabled === true ? { establishReplicaStamp: this.establishReplicaStamp } : {},
23648
24154
  sessionManager: this.sessionManager,
23649
24155
  sessionStorage: options?.sessionStorage ?? this.config.sessionStorage ?? new MemorySessionStorage(),
23650
24156
  domain: this.siweDomain,
@@ -23725,7 +24231,7 @@ var _TinyCloudNode = class _TinyCloudNode {
23725
24231
  jwk: session.jwk
23726
24232
  };
23727
24233
  this._serviceContext.setSession(serviceSession);
23728
- await this.bindReplicationRuntime(this._serviceContext, serviceSession, this._kv);
24234
+ await this.bindReplicaOrGate(this._serviceContext, serviceSession, this._kv);
23729
24235
  this.tc.serviceContext.setSession(serviceSession);
23730
24236
  this._vault = this.createVaultService(session.spaceId, this._kv);
23731
24237
  this._vault.initialize(this._serviceContext);
@@ -24119,6 +24625,7 @@ var _TinyCloudNode = class _TinyCloudNode {
24119
24625
  session: serviceSession,
24120
24626
  invoke: graph.invoke,
24121
24627
  invokeAny: graph.invokeAny,
24628
+ signOwnerRevocation: this.signer ? (cid) => this.signOwnerRevocation(cid) : void 0,
24122
24629
  fetch: graph.fetch
24123
24630
  });
24124
24631
  this._spaceService = new import_sdk_core9.SpaceService({
@@ -24254,11 +24761,12 @@ var _TinyCloudNode = class _TinyCloudNode {
24254
24761
  * Both SharingService (single-entry) and
24255
24762
  * {@link TinyCloudNode.delegateTo} (multi-entry) drive this through
24256
24763
  * the same code path so there's exactly one place that touches the
24257
- * WASM boundary.
24764
+ * WASM boundary. `terminal` adds the node's terminal marker, which the
24765
+ * WASM builder cannot emit.
24258
24766
  *
24259
24767
  * @internal
24260
24768
  */
24261
- createDelegationWrapper(params) {
24769
+ createDelegationWrapper(params, terminal = false) {
24262
24770
  const wasmSession = {
24263
24771
  delegationHeader: params.session.delegationHeader,
24264
24772
  delegationCid: params.session.delegationCid,
@@ -24278,8 +24786,7 @@ var _TinyCloudNode = class _TinyCloudNode {
24278
24786
  params.notBeforeSecs
24279
24787
  );
24280
24788
  return {
24281
- delegation: result.delegation,
24282
- cid: result.cid,
24789
+ ...terminal ? (0, import_sdk_core9.markDelegationTerminal)(result.delegation, params.session.jwk) : { delegation: result.delegation, cid: result.cid },
24283
24790
  // Rust serde `rename_all = "camelCase"` emits `delegateDid`
24284
24791
  // (lowercase d); the TypeScript interface uses `delegateDID`
24285
24792
  // (historical, matches Delegation.delegateDID). Normalize here.
@@ -24421,7 +24928,7 @@ var _TinyCloudNode = class _TinyCloudNode {
24421
24928
  };
24422
24929
  const requestBodyDigest = base64UrlEncode(new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode((0, import_sdk_core9.canonicalizeEncryptionJson)(body)))));
24423
24930
  const request = { ...body, requestBodyDigest };
24424
- const authorization = authorizationHeader(this.invokeAnyWithRuntimePermissions(serviceSession, [{ spaceId: session.spaceId, service: "kv", path: input.resourcePath, action: "tinycloud.kv/get" }]));
24931
+ const authorization = authorizationHeader(this.wasmBindings.invoke(serviceSession, "kv", input.resourcePath, "tinycloud.kv/get"));
24425
24932
  const response = await fetch(`${this.config.host}/policy/v3/deliveries/authorize`, {
24426
24933
  method: "POST",
24427
24934
  headers: { accept: "application/json", "content-type": "application/json", authorization },
@@ -25411,7 +25918,8 @@ var _TinyCloudNode = class _TinyCloudNode {
25411
25918
  return this.delegationManager.create(params);
25412
25919
  }
25413
25920
  /**
25414
- * Revoke a delegation using the v2 DelegationManager.
25921
+ * Revoke a delegation with the owner wallet when available. Session-only
25922
+ * clients continue to use their delegated revocation authority.
25415
25923
  *
25416
25924
  * @param cid - The CID of the delegation to revoke
25417
25925
  * @returns Result indicating success or failure
@@ -25527,6 +26035,9 @@ var _TinyCloudNode = class _TinyCloudNode {
25527
26035
  effectiveExpiration = sessionExpiry;
25528
26036
  }
25529
26037
  if (options?.forceWalletSign) {
26038
+ if (options.disableSubDelegation) {
26039
+ throw new Error(TERMINAL_DELEGATION_REQUIRES_SESSION_KEY);
26040
+ }
25530
26041
  if (expandedEntries.length > 1) {
25531
26042
  throw new Error(
25532
26043
  "delegateTo with forceWalletSign=true supports at most one PermissionEntry. Multi-entry requests must go through the session-key UCAN path (drop forceWalletSign) or the legacy createDelegation method."
@@ -25562,7 +26073,8 @@ var _TinyCloudNode = class _TinyCloudNode {
25562
26073
  plan.effectiveExpiration,
25563
26074
  plan.grant,
25564
26075
  plan.operations,
25565
- onPrepared
26076
+ onPrepared,
26077
+ options?.disableSubDelegation === true
25566
26078
  );
25567
26079
  return { delegation: delegation2, prompted: false };
25568
26080
  }
@@ -25571,7 +26083,8 @@ var _TinyCloudNode = class _TinyCloudNode {
25571
26083
  expandedEntries,
25572
26084
  effectiveExpiration,
25573
26085
  session,
25574
- onPrepared
26086
+ onPrepared,
26087
+ options?.disableSubDelegation === true
25575
26088
  );
25576
26089
  return { delegation, prompted: false };
25577
26090
  }
@@ -25686,34 +26199,67 @@ var _TinyCloudNode = class _TinyCloudNode {
25686
26199
  return auth.replicationSignInEntries();
25687
26200
  }
25688
26201
  async replicationAuthority() {
25689
- const { createReplicationAuthority: createReplicationAuthority2 } = await getNodeReplicationLoaders().authority();
26202
+ const { createReplicationAuthority: createReplicationAuthority2, mintNodeReplicationGrant: mintNodeReplicationGrant2, verifiedOwnerConsentAnchor: verifiedOwnerConsentAnchor2 } = await getNodeReplicationLoaders().authority();
25690
26203
  return createReplicationAuthority2({
25691
26204
  replicationSession: () => this.currentTinyCloudSession() ?? this.replicationDelegateSession,
26205
+ verifiedConsentAnchor: (session) => {
26206
+ const current = this.currentTinyCloudSession();
26207
+ return session === current && current !== void 0 ? verifiedOwnerConsentAnchor2(current, this.wasmBindings, exactVerifiedRecap) : null;
26208
+ },
25692
26209
  siweExpiration: (siwe) => extractSiweExpiration(siwe),
25693
26210
  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
- }
26211
+ mintDelegation: (deviceDid, entries, options) => mintNodeReplicationGrant2(
26212
+ deviceDid,
26213
+ entries,
26214
+ options,
26215
+ options?.renewalOnly ? this.currentTinyCloudSession() : void 0,
26216
+ this
26217
+ )
25701
26218
  });
25702
26219
  }
25703
- async bindReplicationRuntime(context, session, kv, principal) {
26220
+ async quiesceReplicaRuntime() {
25704
26221
  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
- });
26222
+ try {
26223
+ await (await this.replicationRuntime).unbind();
26224
+ } catch {
26225
+ this.replicaRestoreGate = "gate_unavailable";
26226
+ }
26227
+ }
26228
+ async bindReplicaOrGate(context, session, kv, principal) {
26229
+ if (!this.replicationRuntime) return;
26230
+ const gate = this.replicaRestoreGate;
26231
+ const epoch = this.sessionEpoch;
26232
+ const attempt = this.latestSessionAttempt;
26233
+ try {
26234
+ const runtime = await this.replicationRuntime;
26235
+ if (epoch !== this.sessionEpoch || attempt !== this.latestSessionAttempt || this._kv !== kv || this._serviceContext !== context || gate !== this.replicaRestoreGate) return;
26236
+ if (gate) {
26237
+ await runtime.bindGatedNetworkOnly({
26238
+ context,
26239
+ session,
26240
+ gate,
26241
+ primaryKV: [{ space: session.spaceId, kv }]
26242
+ }, getNodeReplicationLoaders().gatedNetwork);
26243
+ return;
26244
+ }
26245
+ const identityPrincipal = principal ?? (this._address === void 0 ? void 0 : (0, import_sdk_core9.pkhDid)(this._address, this._chainId));
26246
+ if (identityPrincipal === void 0) return;
26247
+ const authority = await this.replicationAuthority();
26248
+ if (epoch !== this.sessionEpoch || attempt !== this.latestSessionAttempt || this._kv !== kv || this._serviceContext !== context) return;
26249
+ await runtime.bind({
26250
+ context,
26251
+ session,
26252
+ principal: identityPrincipal,
26253
+ stamp: this.replicaSessionStamp,
26254
+ authority,
26255
+ nodeFeatures: this.replicationNodeFeatures,
26256
+ primaryKV: [{ space: session.spaceId, kv }]
26257
+ });
26258
+ } catch {
26259
+ if (epoch !== this.sessionEpoch || attempt !== this.latestSessionAttempt || this._kv !== kv || this._serviceContext !== context) return;
26260
+ this.replicaRestoreGate = "gate_unavailable";
26261
+ await this.quiesceReplicaRuntime();
26262
+ }
25717
26263
  }
25718
26264
  /** Stable control facade for this node's optional replication runtime. */
25719
26265
  get replication() {
@@ -25769,7 +26315,7 @@ var _TinyCloudNode = class _TinyCloudNode {
25769
26315
  *
25770
26316
  * @internal
25771
26317
  */
25772
- async createDelegationViaWasmPath(did, entries, expirationTime, session, onPrepared) {
26318
+ async createDelegationViaWasmPath(did, entries, expirationTime, session, onPrepared, disableSubDelegation = false) {
25773
26319
  if (entries.length === 0) {
25774
26320
  throw new Error(
25775
26321
  "createDelegationViaWasmPath requires a non-empty entries array"
@@ -25829,7 +26375,7 @@ var _TinyCloudNode = class _TinyCloudNode {
25829
26375
  spaceId,
25830
26376
  abilities,
25831
26377
  expirationSecs
25832
- });
26378
+ }, disableSubDelegation);
25833
26379
  const primary = result.resources[0];
25834
26380
  const delegationHeader = { Authorization: result.delegation };
25835
26381
  const delegation = {
@@ -25839,7 +26385,7 @@ var _TinyCloudNode = class _TinyCloudNode {
25839
26385
  path: primary.path,
25840
26386
  actions: primary.actions,
25841
26387
  resources: result.resources,
25842
- disableSubDelegation: false,
26388
+ disableSubDelegation,
25843
26389
  expiry: result.expiry,
25844
26390
  delegateDID: did,
25845
26391
  ownerAddress: session.address,
@@ -25861,7 +26407,7 @@ var _TinyCloudNode = class _TinyCloudNode {
25861
26407
  }
25862
26408
  return delegation;
25863
26409
  }
25864
- async createDelegationViaRuntimeGrant(did, entries, expirationTime, grant, requestedOperations, onPrepared) {
26410
+ async createDelegationViaRuntimeGrant(did, entries, expirationTime, grant, requestedOperations, onPrepared, disableSubDelegation = false) {
25865
26411
  this.assertRuntimeGrantCaveatsPreservable(entries, requestedOperations, grant);
25866
26412
  this.assertDelegationCaveatsPreservable(entries);
25867
26413
  const result = this.createDelegationWrapper({
@@ -25870,7 +26416,7 @@ var _TinyCloudNode = class _TinyCloudNode {
25870
26416
  spaceId: grant.session.spaceId,
25871
26417
  abilities: this.permissionsToAbilities(entries),
25872
26418
  expirationSecs: Math.floor(expirationTime.getTime() / 1e3)
25873
- });
26419
+ }, disableSubDelegation);
25874
26420
  const primary = result.resources[0];
25875
26421
  const delegationHeader = { Authorization: result.delegation };
25876
26422
  const targetHost = grant.delegation.host ?? this.config.host;
@@ -25881,7 +26427,7 @@ var _TinyCloudNode = class _TinyCloudNode {
25881
26427
  path: primary.path,
25882
26428
  actions: primary.actions,
25883
26429
  resources: result.resources,
25884
- disableSubDelegation: false,
26430
+ disableSubDelegation,
25885
26431
  expiry: result.expiry,
25886
26432
  delegateDID: did,
25887
26433
  ownerAddress: grant.delegation.ownerAddress,
@@ -26465,11 +27011,10 @@ var _TinyCloudNode = class _TinyCloudNode {
26465
27011
  params.spaceIdOverride
26466
27012
  );
26467
27013
  try {
26468
- const result = await this.delegateTo(
26469
- resolvedDelegateDID,
26470
- entries,
26471
- params.expiryMs !== void 0 ? { expiry: params.expiryMs } : void 0
26472
- );
27014
+ const result = await this.delegateTo(resolvedDelegateDID, entries, {
27015
+ ...params.expiryMs !== void 0 ? { expiry: params.expiryMs } : {},
27016
+ disableSubDelegation: params.disableSubDelegation
27017
+ });
26473
27018
  return result.delegation;
26474
27019
  } catch (err2) {
26475
27020
  if (err2 instanceof import_sdk_core9.PermissionNotInManifestError) {
@@ -26490,6 +27035,9 @@ var _TinyCloudNode = class _TinyCloudNode {
26490
27035
  * @internal
26491
27036
  */
26492
27037
  async createDelegationWalletPath(params) {
27038
+ if (params.disableSubDelegation) {
27039
+ throw new Error(TERMINAL_DELEGATION_REQUIRES_SESSION_KEY);
27040
+ }
26493
27041
  if (!this.signer) {
26494
27042
  throw new Error("Cannot createDelegation() in session-only mode. Requires wallet mode.");
26495
27043
  }
@@ -26545,7 +27093,7 @@ var _TinyCloudNode = class _TinyCloudNode {
26545
27093
  spaceId: params.spaceIdOverride ?? session.spaceId,
26546
27094
  path: params.path,
26547
27095
  actions: params.actions,
26548
- disableSubDelegation: params.disableSubDelegation ?? false,
27096
+ disableSubDelegation: false,
26549
27097
  expiry: expirationTime,
26550
27098
  delegateDID: params.delegateDID,
26551
27099
  ownerAddress: session.address,
@@ -26588,7 +27136,7 @@ var _TinyCloudNode = class _TinyCloudNode {
26588
27136
  spaceId: publicSpaceId,
26589
27137
  path: "",
26590
27138
  actions: [import_sdk_core9.KV.GET, import_sdk_core9.KV.PUT, import_sdk_core9.KV.METADATA],
26591
- disableSubDelegation: params.disableSubDelegation ?? false,
27139
+ disableSubDelegation: false,
26592
27140
  expiry: expirationTime,
26593
27141
  delegateDID: params.delegateDID,
26594
27142
  ownerAddress: session.address,
@@ -26755,6 +27303,9 @@ var _TinyCloudNode = class _TinyCloudNode {
26755
27303
  if (parentDelegation.disableSubDelegation) {
26756
27304
  throw new Error("Parent delegation does not allow sub-delegation");
26757
27305
  }
27306
+ if (params.disableSubDelegation) {
27307
+ throw new Error(TERMINAL_DELEGATION_REQUIRES_SESSION_KEY);
27308
+ }
26758
27309
  const parentResources = parentDelegation.resources ?? [{
26759
27310
  service: parentDelegation.actions[0]?.split("/", 1)[0]?.replace(/^tinycloud\./, "") ?? "kv",
26760
27311
  space: parentDelegation.spaceId,
@@ -26827,7 +27378,7 @@ var _TinyCloudNode = class _TinyCloudNode {
26827
27378
  spaceId: parentDelegation.spaceId,
26828
27379
  path: requestedResources[0].path,
26829
27380
  actions: requestedResources[0].actions,
26830
- disableSubDelegation: params.disableSubDelegation ?? false,
27381
+ disableSubDelegation: false,
26831
27382
  expiry: actualExpiry,
26832
27383
  delegateDID: params.delegateDID,
26833
27384
  ownerAddress: parentDelegation.ownerAddress,
@@ -26862,9 +27413,16 @@ TinyCloudNode.registerNodeDefaults({
26862
27413
  });
26863
27414
 
26864
27415
  // src/replication/node-loader.ts
27416
+ init_config();
26865
27417
  registerReplicationLoaders({
27418
+ validateConfig: assertValidReplicationConfig,
26866
27419
  runtime: () => Promise.resolve().then(() => (init_runtime(), runtime_exports)),
26867
- authority: () => Promise.resolve().then(() => (init_authority(), authority_exports))
27420
+ authority: () => Promise.resolve().then(() => (init_authority(), authority_exports)),
27421
+ gatedNetwork: () => Promise.resolve().then(() => (init_gated_network(), gated_network_exports)),
27422
+ // Lazy by contract: replica loads only when replication is enabled. Literal specifier typed by
27423
+ // ProfileGateModule; TS2307 under moduleResolution "node" is suppressed (see replica-contract.ts).
27424
+ // @ts-ignore
27425
+ gate: () => import("@tinycloud/replica/profile-gate")
26868
27426
  });
26869
27427
 
26870
27428
  // src/index.ts
@@ -27050,12 +27608,515 @@ var import_sdk_core26 = require("@tinycloud/sdk-core");
27050
27608
  var import_sdk_core27 = require("@tinycloud/sdk-core");
27051
27609
 
27052
27610
  // src/replication/sqlite.ts
27611
+ var import_node_async_hooks = require("async_hooks");
27053
27612
  var import_node_crypto2 = require("crypto");
27613
+ var import_node_fs = require("fs");
27054
27614
  var import_promises = require("fs/promises");
27055
27615
  var import_node_path = require("path");
27056
27616
  var import_sdk_core13 = require("@tinycloud/sdk-core");
27057
27617
  var import_sdk_services3 = require("@tinycloud/sdk-services");
27618
+ var import_contractConstants = require("@tinycloud/sdk-services/kv/replication/contractConstants");
27058
27619
  init_authority();
27620
+ var REPLICA_LOGOUT_PENDING_FILE = "replica-logout-pending.json";
27621
+ function assertProfileLogoutClear(profileDir) {
27622
+ assertLogoutClear((0, import_node_path.join)(profileDir, REPLICA_LOGOUT_PENDING_FILE));
27623
+ }
27624
+ function assertLogoutClear(path) {
27625
+ if (path === void 0) return;
27626
+ try {
27627
+ (0, import_node_fs.lstatSync)(path);
27628
+ } catch (error) {
27629
+ if (isNotFound(error)) return;
27630
+ }
27631
+ throw gated("gate_unavailable");
27632
+ }
27633
+ function safeInventoryPath(path) {
27634
+ 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/"));
27635
+ }
27636
+ function logoutPrincipalDigest(principal) {
27637
+ return (0, import_node_crypto2.createHash)("sha256").update("tinycloud-replica-logout\0").update(principal, "utf8").digest("hex");
27638
+ }
27639
+ function compareInventoryString(left, right) {
27640
+ return left < right ? -1 : left > right ? 1 : 0;
27641
+ }
27642
+ function approvalEntries(report) {
27643
+ return [
27644
+ ...report.unknownEntries,
27645
+ ...report.knownEntries,
27646
+ ...report.knownRoots.map((root) => ({ ...root, kind: "directory" })),
27647
+ ...report.missingDatabases.map((path) => ({ path, kind: "missing", dev: "", ino: "" }))
27648
+ ].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));
27649
+ }
27650
+ function inventoryApprovalToken(marker, principal, entries) {
27651
+ const markerId = marker.v === 2 ? [marker.v, marker.nonce, marker.principalDigest] : [marker.v, marker.incarnation, marker.lifecycleEpoch, marker.principalDigest];
27652
+ return (0, import_node_crypto2.createHash)("sha256").update("tc-logout-approve-v1\0").update(JSON.stringify([
27653
+ markerId,
27654
+ marker.approvalSalt,
27655
+ principal,
27656
+ marker.v === 2 ? null : marker.lifecycleEpoch,
27657
+ entries
27658
+ ])).digest().subarray(0, 16).toString("hex");
27659
+ }
27660
+ function changedInventoryRefusal(marker, principal, report, entries = approvalEntries(report)) {
27661
+ const token = inventoryApprovalToken(marker, principal, entries);
27662
+ const error = localChangesRefusal(report.unknownPaths);
27663
+ 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}.`;
27664
+ return Object.assign(error, { inventoryApprovalToken: token, inventoryApprovalEntries: entries });
27665
+ }
27666
+ function storedInventoryChanged(marker, report) {
27667
+ try {
27668
+ assertApprovedInventory(report, {
27669
+ unknownEntries: marker.approvedUnknown,
27670
+ knownRoots: marker.knownRoots,
27671
+ knownEntries: marker.knownEntries,
27672
+ missingDatabases: marker.missingDatabases
27673
+ });
27674
+ return false;
27675
+ } catch (error) {
27676
+ if (typeof error === "object" && error !== null && "code" in error && error.code === "REPLICA_GATED" && "gate" in error && error.gate === "gate_unavailable") {
27677
+ return true;
27678
+ }
27679
+ throw error;
27680
+ }
27681
+ }
27682
+ function decimalIdentity(value) {
27683
+ return typeof value === "string" && /^(0|[1-9][0-9]*)$/.test(value);
27684
+ }
27685
+ function logoutMarkerOf(value) {
27686
+ if (value === null || typeof value !== "object" || Array.isArray(value)) throw gated("gate_unavailable");
27687
+ const row = value;
27688
+ 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);
27689
+ 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);
27690
+ 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(
27691
+ (path) => typeof path === "string" && safeInventoryPath(path) && path.endsWith("/replica.db")
27692
+ ) || 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");
27693
+ const optional = Number(row.discardLocalChanges === true) + Number(row.remotePending === true) + Number(row.remoteAbandoned === true);
27694
+ 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;
27695
+ 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) {
27696
+ return row;
27697
+ }
27698
+ const bound = Number(row.deniedEpoch !== void 0);
27699
+ 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)) {
27700
+ throw gated("gate_unavailable");
27701
+ }
27702
+ return row;
27703
+ }
27704
+ async function readLogoutMarker(path) {
27705
+ const value = await readJsonFile(path);
27706
+ return value === void 0 ? null : logoutMarkerOf(value);
27707
+ }
27708
+ async function orphanRoot(profileDir, name) {
27709
+ let profile;
27710
+ try {
27711
+ profile = await (0, import_promises.lstat)(profileDir);
27712
+ } catch {
27713
+ throw gated("gate_unavailable");
27714
+ }
27715
+ let pinned = false;
27716
+ if (process.platform === "linux" && /^\/proc\/self\/fd\/[1-9]\d*$/.test(profileDir) && profile.isSymbolicLink()) {
27717
+ try {
27718
+ pinned = (await (0, import_promises.stat)(profileDir)).isDirectory();
27719
+ } catch {
27720
+ throw gated("gate_unavailable");
27721
+ }
27722
+ }
27723
+ if (!profile.isDirectory() && !pinned) throw gated("gate_unavailable");
27724
+ const root = (0, import_node_path.join)(profileDir, name);
27725
+ let entry;
27726
+ try {
27727
+ entry = await (0, import_promises.lstat)(root);
27728
+ } catch (error) {
27729
+ if (isNotFound(error)) return null;
27730
+ throw error;
27731
+ }
27732
+ if (!entry.isDirectory() || await (0, import_promises.realpath)(root) !== (0, import_node_path.join)(await (0, import_promises.realpath)(profileDir), name)) {
27733
+ throw gated("gate_unavailable");
27734
+ }
27735
+ return root;
27736
+ }
27737
+ async function hasOrphanPending(profileDir) {
27738
+ const root = await orphanRoot(profileDir, "replication");
27739
+ if (root === null) return false;
27740
+ let entries;
27741
+ try {
27742
+ entries = await (0, import_promises.readdir)(root, { withFileTypes: true });
27743
+ } catch {
27744
+ return true;
27745
+ }
27746
+ for (const entry of entries) {
27747
+ if (!entry.isDirectory()) return true;
27748
+ const dir = (0, import_node_path.join)(root, entry.name);
27749
+ try {
27750
+ if (!(await (0, import_promises.lstat)(dir)).isDirectory()) return true;
27751
+ } catch {
27752
+ return true;
27753
+ }
27754
+ if (!/^[a-z2-7]{26}$/.test(entry.name)) return true;
27755
+ let children;
27756
+ try {
27757
+ children = await (0, import_promises.readdir)(dir, { withFileTypes: true });
27758
+ } catch {
27759
+ return true;
27760
+ }
27761
+ if (children.length !== 2 || children.some((child) => !child.isFile() || child.name !== "identity.json" && child.name !== "pending.json")) return true;
27762
+ const pendingFile = (0, import_node_path.join)(dir, "pending.json");
27763
+ let text;
27764
+ try {
27765
+ const pending = await (0, import_promises.lstat)(pendingFile);
27766
+ if (!pending.isFile()) return true;
27767
+ text = await (0, import_promises.readFile)(pendingFile, "utf8");
27768
+ } catch (error) {
27769
+ if (isNotFound(error)) return true;
27770
+ return true;
27771
+ }
27772
+ try {
27773
+ const identityFile = (0, import_node_path.join)(dir, "identity.json");
27774
+ if (!(await (0, import_promises.lstat)(identityFile)).isFile()) return true;
27775
+ const identity = identityRecordOf(await readJsonFile(identityFile));
27776
+ if (identity === void 0 || parsePendingState(text, pendingFile, identity).records.length !== 0) {
27777
+ return true;
27778
+ }
27779
+ } catch {
27780
+ return true;
27781
+ }
27782
+ }
27783
+ return false;
27784
+ }
27785
+ function inventoryIdentity(path, dev, ino) {
27786
+ return { path, dev: dev.toString(), ino: ino.toString() };
27787
+ }
27788
+ async function inventoryUnknown(profileDir, relative, entries) {
27789
+ const path = (0, import_node_path.join)(profileDir, relative);
27790
+ const info = await (0, import_promises.lstat)(path, { bigint: true });
27791
+ const kind = info.isDirectory() ? "directory" : info.isSymbolicLink() ? "symlink" : "file";
27792
+ entries.push({ ...inventoryIdentity(relative, info.dev, info.ino), kind });
27793
+ if (kind === "directory") {
27794
+ for (const child of await (0, import_promises.readdir)(path)) await inventoryUnknown(profileDir, `${relative}/${child}`, entries);
27795
+ }
27796
+ }
27797
+ async function inspectProfileReplicaLayout(profileDir, gateReadable, approvedMarker, legacyOnly = false, diagnosticFiles = []) {
27798
+ const report = {
27799
+ knownLegacyReplicas: 0,
27800
+ unknownLegacyPaths: [],
27801
+ unknownPaths: [],
27802
+ unknownEntries: [],
27803
+ knownRoots: [],
27804
+ knownEntries: [],
27805
+ missingDatabases: []
27806
+ };
27807
+ const legacy = await orphanRoot(profileDir, "replicas");
27808
+ const active = legacyOnly ? null : await orphanRoot(profileDir, "replication");
27809
+ for (const [name, dir] of [["replicas", legacy], ["replication", active]]) {
27810
+ if (dir === null) continue;
27811
+ const info = await (0, import_promises.lstat)(dir, { bigint: true });
27812
+ report.knownRoots.push(inventoryIdentity(name, info.dev, info.ino));
27813
+ }
27814
+ if (legacy !== null) {
27815
+ const sqlite = await import("@tinycloud/replica/sqlite");
27816
+ for (const entry of await (0, import_promises.readdir)(legacy, { withFileTypes: true })) {
27817
+ const relative = `replicas/${entry.name}`;
27818
+ const child = (0, import_node_path.join)(legacy, entry.name);
27819
+ if (!entry.isDirectory()) {
27820
+ await inventoryUnknown(profileDir, relative, report.unknownEntries);
27821
+ continue;
27822
+ }
27823
+ try {
27824
+ const layout = await sqlite.inspectSqliteReplicaLayout(child);
27825
+ report.knownRoots.push({ path: relative, ...layout.rootIdentity });
27826
+ report.unknownEntries.push(...layout.unknownEntries.map((item) => ({
27827
+ ...item,
27828
+ path: `${relative}/${item.path}`
27829
+ })));
27830
+ report.knownEntries.push(...layout.knownEntries.map((item) => ({
27831
+ ...item,
27832
+ path: `${relative}/${item.path}`
27833
+ })));
27834
+ if (layout.unknownPaths.length === 0) report.knownLegacyReplicas++;
27835
+ if (layout.unknownPaths.includes("replica.db") && !layout.unknownEntries.some((item) => item.path === "replica.db")) {
27836
+ report.missingDatabases.push(`${relative}/replica.db`);
27837
+ }
27838
+ } catch {
27839
+ await inventoryUnknown(profileDir, relative, report.unknownEntries);
27840
+ }
27841
+ }
27842
+ }
27843
+ if (active !== null) {
27844
+ const cleanOrphan = !gateReadable && (approvedMarker?.v === 2 || !await hasOrphanPending(profileDir));
27845
+ for (const entry of await (0, import_promises.readdir)(active, { withFileTypes: true })) {
27846
+ const relative = `replication/${entry.name}`;
27847
+ const dir = (0, import_node_path.join)(active, entry.name);
27848
+ if (approvedMarker?.approvedUnknown.some((item) => item.path === relative)) {
27849
+ await inventoryUnknown(profileDir, relative, report.unknownEntries);
27850
+ continue;
27851
+ }
27852
+ if (gateReadable && diagnosticFiles.includes(entry.name)) {
27853
+ const info2 = await (0, import_promises.lstat)(dir, { bigint: true });
27854
+ if (info2.isFile()) {
27855
+ report.knownEntries.push({ ...inventoryIdentity(relative, info2.dev, info2.ino), kind: "file" });
27856
+ continue;
27857
+ }
27858
+ }
27859
+ if (!gateReadable && !cleanOrphan || !entry.isDirectory() || !/^[a-z2-7]{26}$/.test(entry.name)) {
27860
+ await inventoryUnknown(profileDir, relative, report.unknownEntries);
27861
+ continue;
27862
+ }
27863
+ const info = await (0, import_promises.lstat)(dir, { bigint: true });
27864
+ report.knownRoots.push(inventoryIdentity(relative, info.dev, info.ino));
27865
+ const identityPath = (0, import_node_path.join)(dir, "identity.json");
27866
+ let identity;
27867
+ try {
27868
+ if ((await (0, import_promises.lstat)(identityPath)).isFile()) identity = identityRecordOf(await readJsonFile(identityPath));
27869
+ } catch {
27870
+ }
27871
+ if (identity === void 0) {
27872
+ const approvedRoot = approvedMarker?.knownRoots.find((item) => item.path === relative);
27873
+ if (approvedRoot?.dev === info.dev.toString() && approvedRoot.ino === info.ino.toString()) {
27874
+ const children = await (0, import_promises.readdir)(dir);
27875
+ if (children.length === 0) continue;
27876
+ if (children.length === 1 && children[0] === "replicas") {
27877
+ const replicasPath = `${relative}/replicas`;
27878
+ const replicasDir = (0, import_node_path.join)(dir, "replicas");
27879
+ const replicasInfo = await (0, import_promises.lstat)(replicasDir, { bigint: true });
27880
+ const approvedReplicas = approvedMarker.knownRoots.find((item) => item.path === replicasPath);
27881
+ if (replicasInfo.isDirectory() && !replicasInfo.isSymbolicLink() && approvedReplicas?.dev === replicasInfo.dev.toString() && approvedReplicas.ino === replicasInfo.ino.toString() && (await (0, import_promises.readdir)(replicasDir)).length === 0) {
27882
+ report.knownRoots.push(inventoryIdentity(replicasPath, replicasInfo.dev, replicasInfo.ino));
27883
+ continue;
27884
+ }
27885
+ }
27886
+ }
27887
+ report.knownRoots.pop();
27888
+ await inventoryUnknown(profileDir, relative, report.unknownEntries);
27889
+ continue;
27890
+ }
27891
+ for (const child of await (0, import_promises.readdir)(dir, { withFileTypes: true })) {
27892
+ const childRelative = `${relative}/${child.name}`;
27893
+ const childPath = (0, import_node_path.join)(dir, child.name);
27894
+ if ((child.name === "identity.json" || child.name === "device.jwk") && child.isFile()) {
27895
+ const childInfo = await (0, import_promises.lstat)(childPath, { bigint: true });
27896
+ report.knownEntries.push({ ...inventoryIdentity(childRelative, childInfo.dev, childInfo.ino), kind: "file" });
27897
+ continue;
27898
+ }
27899
+ if (child.name === "pending.json" && child.isFile()) {
27900
+ try {
27901
+ if (parsePendingState(await (0, import_promises.readFile)(childPath, "utf8"), childPath, identity).records.length === 0) {
27902
+ const childInfo = await (0, import_promises.lstat)(childPath, { bigint: true });
27903
+ report.knownEntries.push({ ...inventoryIdentity(childRelative, childInfo.dev, childInfo.ino), kind: "file" });
27904
+ continue;
27905
+ }
27906
+ } catch {
27907
+ }
27908
+ }
27909
+ if (child.name === "replicas" && child.isDirectory()) {
27910
+ const replicasInfo = await (0, import_promises.lstat)(childPath, { bigint: true });
27911
+ report.knownRoots.push(inventoryIdentity(childRelative, replicasInfo.dev, replicasInfo.ino));
27912
+ for (const replicaChild of await (0, import_promises.readdir)(childPath, { withFileTypes: true })) {
27913
+ const replicaRelative = `${childRelative}/${replicaChild.name}`;
27914
+ if (!replicaChild.isDirectory() || !/^[a-z2-7]{26}$/.test(replicaChild.name)) {
27915
+ await inventoryUnknown(profileDir, replicaRelative, report.unknownEntries);
27916
+ continue;
27917
+ }
27918
+ const sqlite = await import("@tinycloud/replica/sqlite");
27919
+ try {
27920
+ const layout = await sqlite.inspectSqliteReplicaLayout((0, import_node_path.join)(childPath, replicaChild.name), {
27921
+ allowAdapterFence: true
27922
+ });
27923
+ report.knownRoots.push({ path: replicaRelative, ...layout.rootIdentity });
27924
+ report.unknownEntries.push(...layout.unknownEntries.map((item) => ({
27925
+ ...item,
27926
+ path: `${replicaRelative}/${item.path}`
27927
+ })));
27928
+ report.knownEntries.push(...layout.knownEntries.map((item) => ({
27929
+ ...item,
27930
+ path: `${replicaRelative}/${item.path}`
27931
+ })));
27932
+ if (layout.unknownPaths.includes("replica.db") && !layout.unknownEntries.some((item) => item.path === "replica.db")) {
27933
+ report.missingDatabases.push(`${replicaRelative}/replica.db`);
27934
+ }
27935
+ } catch {
27936
+ await inventoryUnknown(profileDir, replicaRelative, report.unknownEntries);
27937
+ }
27938
+ }
27939
+ continue;
27940
+ }
27941
+ await inventoryUnknown(profileDir, childRelative, report.unknownEntries);
27942
+ }
27943
+ }
27944
+ }
27945
+ report.unknownEntries.sort((a, b) => a.path.localeCompare(b.path));
27946
+ report.knownEntries.sort((a, b) => a.path.localeCompare(b.path));
27947
+ report.knownRoots.sort((a, b) => a.path.localeCompare(b.path));
27948
+ report.missingDatabases.sort();
27949
+ report.unknownPaths = [...report.unknownEntries.map((entry) => entry.path), ...report.missingDatabases].sort();
27950
+ report.unknownLegacyPaths = [...report.unknownPaths];
27951
+ return report;
27952
+ }
27953
+ function assertApprovedInventory(current, approval, allowRemoved = false) {
27954
+ const approved = new Map(approval.unknownEntries.map((item) => [item.path, item]));
27955
+ const known = new Map(approval.knownRoots.map((item) => [item.path, item]));
27956
+ const knownFiles = new Map(approval.knownEntries.map((item) => [item.path, item]));
27957
+ for (const entry of current.unknownEntries) {
27958
+ const expected = approved.get(entry.path);
27959
+ if (expected?.kind !== entry.kind || expected.dev !== entry.dev || expected.ino !== entry.ino) {
27960
+ throw localChangesRefusal(current.unknownPaths);
27961
+ }
27962
+ }
27963
+ for (const root of current.knownRoots) {
27964
+ const expected = known.get(root.path);
27965
+ if (expected?.dev !== root.dev || expected.ino !== root.ino) throw localChangesRefusal([root.path]);
27966
+ }
27967
+ for (const entry of current.knownEntries) {
27968
+ if (allowRemoved && entry.kind === "file" && /^(?:replication\/[a-z2-7]{26}\/replicas\/[a-z2-7]{26}|replicas\/[^/]+)\/replica\.db-(?:wal|shm|journal)$/.test(entry.path)) {
27969
+ const database = entry.path.replace(/-(?:wal|shm|journal)$/, "");
27970
+ const original = knownFiles.get(database);
27971
+ const present = current.knownEntries.find((item) => item.path === database);
27972
+ if (!approved.has(entry.path) && original?.kind === "file" && present?.dev === original.dev && present.ino === original.ino) continue;
27973
+ }
27974
+ const expected = knownFiles.get(entry.path);
27975
+ if (expected?.kind !== entry.kind || expected.dev !== entry.dev || expected.ino !== entry.ino) {
27976
+ throw localChangesRefusal([entry.path]);
27977
+ }
27978
+ }
27979
+ for (const path of current.missingDatabases) {
27980
+ if (!approval.missingDatabases.includes(path) && !(allowRemoved && knownFiles.get(path)?.kind === "file")) throw localChangesRefusal([path]);
27981
+ }
27982
+ if (!allowRemoved && (current.unknownEntries.length !== approved.size || current.knownRoots.length !== known.size || current.knownEntries.length !== knownFiles.size || current.missingDatabases.length !== approval.missingDatabases.length)) {
27983
+ throw localChangesRefusal(current.unknownPaths);
27984
+ }
27985
+ }
27986
+ async function assertApprovedParents(profileDir, marker, path) {
27987
+ if (process.platform !== "linux") {
27988
+ const absolute = (0, import_node_path.resolve)(profileDir);
27989
+ let ancestor = (0, import_node_path.parse)(absolute).root;
27990
+ for (const part of absolute.slice(ancestor.length).split(import_node_path.sep).filter(Boolean)) {
27991
+ ancestor = (0, import_node_path.join)(ancestor, part);
27992
+ const details = await (0, import_promises.lstat)(ancestor, { bigint: true });
27993
+ if (!details.isDirectory() || details.isSymbolicLink()) throw localChangesRefusal([path]);
27994
+ }
27995
+ }
27996
+ let relativeParent = "";
27997
+ for (const part of path.split("/").slice(0, -1)) {
27998
+ relativeParent = relativeParent ? `${relativeParent}/${part}` : part;
27999
+ let details;
28000
+ try {
28001
+ details = await (0, import_promises.lstat)((0, import_node_path.join)(profileDir, relativeParent), { bigint: true });
28002
+ } catch (error) {
28003
+ if (isNotFound(error)) return false;
28004
+ throw error;
28005
+ }
28006
+ const expected = marker.knownRoots.find((root) => root.path === relativeParent) ?? marker.approvedUnknown.find((item) => item.path === relativeParent && item.kind === "directory");
28007
+ if (!details.isDirectory() || details.isSymbolicLink() || expected?.dev !== details.dev.toString() || expected.ino !== details.ino.toString()) {
28008
+ throw localChangesRefusal([relativeParent]);
28009
+ }
28010
+ }
28011
+ return true;
28012
+ }
28013
+ async function eraseApprovedUnknown(profileDir, marker, prefix, diagnosticFiles) {
28014
+ const entries = marker.approvedUnknown.filter((entry) => entry.path.startsWith(`${prefix}/`));
28015
+ for (const entry of entries.sort((a, b) => b.path.length - a.path.length)) {
28016
+ assertApprovedInventory(await inspectProfileReplicaLayout(profileDir, marker.v !== 2, marker, false, diagnosticFiles), {
28017
+ unknownEntries: marker.approvedUnknown,
28018
+ knownRoots: marker.knownRoots,
28019
+ knownEntries: marker.knownEntries,
28020
+ missingDatabases: marker.missingDatabases
28021
+ }, true);
28022
+ if (!await assertApprovedParents(profileDir, marker, entry.path)) continue;
28023
+ const file = (0, import_node_path.join)(profileDir, entry.path);
28024
+ let current;
28025
+ try {
28026
+ current = await (0, import_promises.lstat)(file, { bigint: true });
28027
+ } catch (error) {
28028
+ if (isNotFound(error)) continue;
28029
+ throw error;
28030
+ }
28031
+ const kind = current.isDirectory() ? "directory" : current.isSymbolicLink() ? "symlink" : "file";
28032
+ if (kind !== entry.kind || current.dev.toString() !== entry.dev || current.ino.toString() !== entry.ino) throw localChangesRefusal([entry.path]);
28033
+ if (process.platform !== "linux" && kind === "symlink") throw localChangesRefusal([entry.path]);
28034
+ if (kind === "directory") await (0, import_promises.rmdir)(file);
28035
+ else await (0, import_promises.rm)(file);
28036
+ }
28037
+ }
28038
+ async function eraseKnownFlagResidue(profileDir, marker, diagnosticFiles) {
28039
+ const diagnostics = new Set(diagnosticFiles.map((name) => `replication/${name}`));
28040
+ 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")))) {
28041
+ if (!await assertApprovedParents(profileDir, marker, entry.path)) continue;
28042
+ const file = (0, import_node_path.join)(profileDir, entry.path);
28043
+ let details;
28044
+ try {
28045
+ details = await (0, import_promises.lstat)(file, { bigint: true });
28046
+ } catch (error) {
28047
+ if (isNotFound(error)) continue;
28048
+ throw error;
28049
+ }
28050
+ if (!details.isFile() || details.dev.toString() !== entry.dev || details.ino.toString() !== entry.ino) throw localChangesRefusal([entry.path]);
28051
+ await (0, import_promises.rm)(file);
28052
+ }
28053
+ const roots = marker.knownRoots.filter((item) => item.path === "replication" || item.path.startsWith("replication/"));
28054
+ roots.sort((a, b) => b.path.split("/").length - a.path.split("/").length);
28055
+ for (const entry of roots) {
28056
+ if (!await assertApprovedParents(profileDir, marker, entry.path)) continue;
28057
+ const path = (0, import_node_path.join)(profileDir, entry.path);
28058
+ let details;
28059
+ try {
28060
+ details = await (0, import_promises.lstat)(path, { bigint: true });
28061
+ } catch (error) {
28062
+ if (isNotFound(error)) continue;
28063
+ throw error;
28064
+ }
28065
+ if (!details.isDirectory() || details.isSymbolicLink() || details.dev.toString() !== entry.dev || details.ino.toString() !== entry.ino) {
28066
+ throw localChangesRefusal([entry.path]);
28067
+ }
28068
+ await (0, import_promises.rmdir)(path);
28069
+ }
28070
+ }
28071
+ function localChangesRefusal(unknownPaths = []) {
28072
+ const error = gated("gate_unavailable");
28073
+ 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(", ")}.`);
28074
+ return error;
28075
+ }
28076
+ function gateOwnedByAnother(state, principalHash) {
28077
+ return state.principalHash !== null ? state.principalHash !== principalHash : state.eraseDeferred && state.deferredPrincipalHash !== principalHash;
28078
+ }
28079
+ function isGateUnavailable(error) {
28080
+ return typeof error === "object" && error !== null && "code" in error && error.code === "REPLICA_GATED" && "gate" in error && error.gate === "gate_unavailable";
28081
+ }
28082
+ function restoredGate() {
28083
+ const error = gated("stamp_stale");
28084
+ error.message = RESTORED_GATE_MESSAGE;
28085
+ return Object.assign(error, { reportCode: "gate_restored" });
28086
+ }
28087
+ 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.";
28088
+ function interruptedSignOut() {
28089
+ const error = gated("gate_unavailable");
28090
+ error.message = "A previous sign-out was interrupted; re-running `tc auth logout` completes it.";
28091
+ return error;
28092
+ }
28093
+ function assertSupportedProfileLayout(report) {
28094
+ if (process.platform === "linux") return;
28095
+ const link = report.unknownEntries.find((entry) => entry.kind === "symlink");
28096
+ if (link === void 0) return;
28097
+ const error = gated("gate_unavailable");
28098
+ error.message = `Replica symlink ${JSON.stringify(link.path)} cannot be removed safely on this platform.`;
28099
+ throw error;
28100
+ }
28101
+ async function fileGate() {
28102
+ return import("@tinycloud/replica/profile-gate");
28103
+ }
28104
+ function gated(reason) {
28105
+ return Object.assign(new Error(`Replica gate refused: ${reason}`), { code: "REPLICA_GATED", gate: reason });
28106
+ }
28107
+ async function requiredGuard(guard, section) {
28108
+ if (guard === void 0) throw gated("gate_unavailable");
28109
+ let entered = false;
28110
+ try {
28111
+ return await guard(async () => {
28112
+ entered = true;
28113
+ return section();
28114
+ });
28115
+ } catch (error) {
28116
+ if (!entered) throw gated("gate_unavailable");
28117
+ throw error;
28118
+ }
28119
+ }
27059
28120
  var ReplicaStorageError = class extends Error {
27060
28121
  constructor(code, message, options) {
27061
28122
  super(message);
@@ -27081,6 +28142,19 @@ async function pathExists(path) {
27081
28142
  throw storageError("STORAGE_ERROR", `Reading ${path}`, error);
27082
28143
  }
27083
28144
  }
28145
+ async function realDirectoryExists(path) {
28146
+ let stats;
28147
+ try {
28148
+ stats = await (0, import_promises.lstat)(path);
28149
+ } catch (error) {
28150
+ if (isNotFound(error)) return false;
28151
+ throw storageError("STORAGE_ERROR", `Reading ${path}`, error);
28152
+ }
28153
+ if (stats.isSymbolicLink() || !stats.isDirectory()) {
28154
+ throw new ReplicaStorageError("STORAGE_ERROR", `Refusing the replaced replica path ${path}.`);
28155
+ }
28156
+ return true;
28157
+ }
27084
28158
  function principalOf(did) {
27085
28159
  return (0, import_sdk_core13.principalDid)(did);
27086
28160
  }
@@ -27234,90 +28308,117 @@ function parsePendingState(text, file, identity) {
27234
28308
  }
27235
28309
  return state;
27236
28310
  }
27237
- var _dir, _guard, _dirSync, _file, _FilePendingWriteStore_instances, canonicalFile_fn;
28311
+ var _dir, _profileDir, _guard, _stamp, _dirSync, _logoutPendingFile, _generation, _canonicalDir, _FilePendingWriteStore_instances, withGate_fn, readFile_fn;
27238
28312
  var FilePendingWriteStore = class {
27239
- constructor(identity, dir, guard, dirSync) {
28313
+ constructor(identity, dir, profileDir, guard, stamp, dirSync, logoutPendingFile) {
27240
28314
  __privateAdd(this, _FilePendingWriteStore_instances);
27241
28315
  this.durable = true;
27242
28316
  __privateAdd(this, _dir);
28317
+ __privateAdd(this, _profileDir);
27243
28318
  __privateAdd(this, _guard);
28319
+ __privateAdd(this, _stamp);
27244
28320
  __privateAdd(this, _dirSync);
27245
- __privateAdd(this, _file);
28321
+ __privateAdd(this, _logoutPendingFile);
28322
+ __privateAdd(this, _generation);
28323
+ __privateAdd(this, _canonicalDir);
27246
28324
  this.identity = identity;
27247
28325
  __privateSet(this, _dir, dir);
28326
+ __privateSet(this, _profileDir, profileDir);
27248
28327
  __privateSet(this, _guard, guard);
28328
+ __privateSet(this, _stamp, stamp);
27249
28329
  __privateSet(this, _dirSync, dirSync);
28330
+ __privateSet(this, _logoutPendingFile, logoutPendingFile);
27250
28331
  }
27251
28332
  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);
28333
+ 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
28334
  }
27264
28335
  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
- }
28336
+ return __privateMethod(this, _FilePendingWriteStore_instances, withGate_fn).call(this, (file) => __privateMethod(this, _FilePendingWriteStore_instances, readFile_fn).call(this, file));
27280
28337
  }
27281
28338
  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
- });
28339
+ return __privateMethod(this, _FilePendingWriteStore_instances, withGate_fn).call(this, async (file) => {
28340
+ await ensurePartitionIdentity(__privateGet(this, _dir), this.identity, __privateGet(this, _dirSync));
28341
+ const currentFile = (0, import_node_path.join)(await (0, import_promises.realpath)(__privateGet(this, _dir)), "pending.json");
28342
+ if (__privateGet(this, _canonicalDir) !== void 0 && (0, import_node_path.dirname)(currentFile) !== __privateGet(this, _canonicalDir)) {
28343
+ throw new ReplicaStorageError("STORAGE_ERROR", "The pending-write partition changed while open.");
28344
+ }
28345
+ const state = await __privateMethod(this, _FilePendingWriteStore_instances, readFile_fn).call(this, currentFile) ?? { v: 2, identity: this.identity, committedEpoch: 0, seq: 0, records: [] };
28346
+ const result = await mutate(state);
28347
+ await writeJsonAtomic(currentFile, state, __privateGet(this, _dirSync));
28348
+ return result;
27302
28349
  });
27303
28350
  }
27304
28351
  };
27305
28352
  _dir = new WeakMap();
28353
+ _profileDir = new WeakMap();
27306
28354
  _guard = new WeakMap();
28355
+ _stamp = new WeakMap();
27307
28356
  _dirSync = new WeakMap();
27308
- _file = new WeakMap();
28357
+ _logoutPendingFile = new WeakMap();
28358
+ _generation = new WeakMap();
28359
+ _canonicalDir = new WeakMap();
27309
28360
  _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");
28361
+ withGate_fn = async function(section) {
28362
+ if (__privateGet(this, _stamp) === void 0) throw gated("not_established");
28363
+ if (__privateGet(this, _guard) === void 0) throw gated("gate_unavailable");
28364
+ const gate = await fileGate();
28365
+ const snapshot = await gate.readGateSnapshot(__privateGet(this, _profileDir));
28366
+ if (snapshot === void 0) throw gated("gate_unavailable");
28367
+ const [principalHash, identityKeyHash] = await Promise.all([
28368
+ gate.principalHashOf(snapshot.salt, this.identity.principal),
28369
+ gate.identityHashOf(snapshot.salt, (0, import_sdk_services3.replicationIdentityKey)(this.identity))
28370
+ ]);
28371
+ const access = {
28372
+ stamp: __privateGet(this, _stamp),
28373
+ incarnation: snapshot.incarnation,
28374
+ principalHash,
28375
+ identityKeyHash
28376
+ };
28377
+ let canonicalRoot;
28378
+ try {
28379
+ canonicalRoot = await (0, import_promises.realpath)((0, import_node_path.dirname)(__privateGet(this, _dir)));
28380
+ } catch (error) {
28381
+ if (!isNotFound(error)) throw storageError("STORAGE_ERROR", `Resolving ${(0, import_node_path.dirname)(__privateGet(this, _dir))}`, error);
28382
+ 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))));
28383
+ }
28384
+ const queueKey = (0, import_node_path.join)(canonicalRoot, (0, import_node_path.basename)(__privateGet(this, _dir)), "pending.json");
28385
+ return serializePendingWrite(queueKey, () => requiredGuard(__privateGet(this, _guard), async () => {
28386
+ assertLogoutClear(__privateGet(this, _logoutPendingFile));
28387
+ const before = await gate.assertGateAccess(__privateGet(this, _profileDir), access, snapshot.salt);
28388
+ const generation = before.pendingGenerations[identityKeyHash] ?? 0;
28389
+ if (__privateGet(this, _generation) !== void 0 && __privateGet(this, _generation) !== generation) {
28390
+ throw new ReplicaStorageError("RESET_REQUIRED", "The pending identity was erased; reopen the store.");
28391
+ }
28392
+ let file = (0, import_node_path.join)(__privateGet(this, _dir), "pending.json");
27314
28393
  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));
28394
+ file = (0, import_node_path.join)(await (0, import_promises.realpath)(__privateGet(this, _dir)), "pending.json");
28395
+ const recorded = identityRecordOf(await readJsonFile((0, import_node_path.join)((0, import_node_path.dirname)(file), "identity.json")));
28396
+ if (recorded === void 0 || !sameIdentity(recorded, this.identity)) {
28397
+ throw new ReplicaStorageError("STORAGE_ERROR", "The pending partition belongs to a different identity.");
28398
+ }
27316
28399
  } catch (error) {
27317
- throw storageError("STORAGE_ERROR", `Resolving ${file}`, error);
28400
+ if (error instanceof ReplicaStorageError) throw error;
28401
+ if (!isNotFound(error)) throw storageError("STORAGE_ERROR", `Resolving ${__privateGet(this, _dir)}`, error);
27318
28402
  }
27319
- })());
27320
- return __privateGet(this, _file);
28403
+ if (__privateGet(this, _canonicalDir) !== void 0 && (0, import_node_path.dirname)(file) !== __privateGet(this, _canonicalDir)) {
28404
+ throw new ReplicaStorageError("STORAGE_ERROR", "The pending-write partition changed while open.");
28405
+ }
28406
+ const result = await section(file);
28407
+ await gate.assertGateAccess(__privateGet(this, _profileDir), access, snapshot.salt);
28408
+ assertLogoutClear(__privateGet(this, _logoutPendingFile));
28409
+ __privateSet(this, _generation, generation);
28410
+ if (await pathExists(__privateGet(this, _dir))) __privateSet(this, _canonicalDir, await (0, import_promises.realpath)(__privateGet(this, _dir)));
28411
+ return result;
28412
+ }));
28413
+ };
28414
+ readFile_fn = async function(file) {
28415
+ try {
28416
+ return parsePendingState(await (0, import_promises.readFile)(file, "utf8"), file, this.identity);
28417
+ } catch (error) {
28418
+ if (isNotFound(error)) return void 0;
28419
+ if (error instanceof ReplicaStorageError) throw error;
28420
+ throw storageError("STORAGE_ERROR", `Reading ${file}`, error);
28421
+ }
27321
28422
  };
27322
28423
  async function loadOrCreateDevice(idDir, guard, createDevice, dirSync) {
27323
28424
  return guard(async () => {
@@ -27363,7 +28464,7 @@ async function readFence(replicaDir) {
27363
28464
  const fence = await readJsonFile((0, import_node_path.join)(replicaDir, FENCE_FILE));
27364
28465
  return typeof fence === "object" && fence !== null && "syncedThroughEpoch" in fence && typeof fence.syncedThroughEpoch === "number" ? fence.syncedThroughEpoch : 0;
27365
28466
  }
27366
- function newestGrantInfo(replica, record, pending, spec) {
28467
+ function newestGrantInfo(replica, record, pending, spec, install) {
27367
28468
  if (record === null) return null;
27368
28469
  const parsed = replica.parseUcanGrant(record.bytes);
27369
28470
  return {
@@ -27371,10 +28472,11 @@ function newestGrantInfo(replica, record, pending, spec) {
27371
28472
  parentCid: parsed.prf[0] ?? null,
27372
28473
  expiresAt: record.expiresAt === null ? null : record.expiresAt * 1e3,
27373
28474
  state: pending !== null && record === pending ? "pending" : "active",
27374
- unconstrained: ucanAttUnconstrainedFor(parsed.att, spec.space, spec.prefix)
28475
+ unconstrained: ucanAttUnconstrainedFor(parsed.att, spec.space, spec.prefix),
28476
+ ...install === null ? {} : install
27375
28477
  };
27376
28478
  }
27377
- var _device, _store, _replicaDir, _replica, _guard2, _dirSync2, _generation, _ino, _transportOverride, _closed, _inFlight, _transportFactory, _SqliteReplicaHandle_instances, assertOpen_fn, assertLive_fn, transports_fn;
28479
+ 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
28480
  var SqliteReplicaHandle = class {
27379
28481
  constructor(input) {
27380
28482
  __privateAdd(this, _SqliteReplicaHandle_instances);
@@ -27383,9 +28485,12 @@ var SqliteReplicaHandle = class {
27383
28485
  __privateAdd(this, _replicaDir);
27384
28486
  __privateAdd(this, _replica);
27385
28487
  __privateAdd(this, _guard2);
28488
+ __privateAdd(this, _read);
28489
+ __privateAdd(this, _policy);
28490
+ __privateAdd(this, _recordGrantInstall);
27386
28491
  __privateAdd(this, _dirSync2);
27387
28492
  /** The replica row generation this handle opened under — the durable fence a purge commits past (browser-parity wipe). */
27388
- __privateAdd(this, _generation);
28493
+ __privateAdd(this, _generation2);
27389
28494
  /** The inode of `replica.db` at open — a directory recreated in place is a different replica. */
27390
28495
  __privateAdd(this, _ino);
27391
28496
  __privateAdd(this, _transportOverride);
@@ -27393,64 +28498,76 @@ var SqliteReplicaHandle = class {
27393
28498
  __privateAdd(this, _inFlight, /* @__PURE__ */ new Set());
27394
28499
  __privateAdd(this, _transportFactory);
27395
28500
  this.spec = input.spec;
28501
+ this.replicaId = input.replicaId;
27396
28502
  __privateSet(this, _device, input.device);
27397
28503
  this.deviceDid = input.device.did;
27398
28504
  __privateSet(this, _store, input.store);
27399
28505
  __privateSet(this, _replicaDir, input.replicaDir);
27400
28506
  __privateSet(this, _replica, input.replica);
27401
28507
  __privateSet(this, _guard2, input.guard);
28508
+ __privateSet(this, _read, input.read);
28509
+ __privateSet(this, _policy, input.policy);
28510
+ __privateSet(this, _recordGrantInstall, input.recordGrantInstall);
27402
28511
  __privateSet(this, _transportOverride, input.transportFor);
27403
28512
  __privateSet(this, _dirSync2, input.dirSync);
27404
- __privateSet(this, _generation, input.generation);
28513
+ __privateSet(this, _generation2, input.generation);
27405
28514
  __privateSet(this, _ino, input.ino);
27406
28515
  }
27407
28516
  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: {
28517
+ return __privateGet(this, _read).call(this, async (snapshot, syncedThroughEpoch) => {
28518
+ __privateMethod(this, _SqliteReplicaHandle_instances, assertOpen_fn).call(this);
28519
+ await __privateMethod(this, _SqliteReplicaHandle_instances, assertEffective_fn).call(this, snapshot);
28520
+ const result = await new (__privateGet(this, _replica)).Replica({ store: snapshot }).get(key2);
28521
+ await __privateMethod(this, _SqliteReplicaHandle_instances, assertEffective_fn).call(this, snapshot);
28522
+ const meta = {
27448
28523
  asOf: result.meta.asOf,
27449
28524
  coverage: result.meta.coverage,
27450
28525
  authority: result.meta.authority,
27451
- syncedThroughEpoch: await readFence(__privateGet(this, _replicaDir))
28526
+ syncedThroughEpoch
28527
+ };
28528
+ switch (result.status) {
28529
+ case "present":
28530
+ return {
28531
+ status: "present",
28532
+ key: result.key,
28533
+ value: result.value,
28534
+ etag: result.etag,
28535
+ metadata: result.metadata,
28536
+ meta
28537
+ };
28538
+ case "content_missing":
28539
+ return {
28540
+ status: "content_missing",
28541
+ key: result.key,
28542
+ etag: result.etag,
28543
+ metadata: result.metadata,
28544
+ meta
28545
+ };
28546
+ default:
28547
+ return { status: result.status, key: result.key, meta };
27452
28548
  }
27453
- };
28549
+ }, true);
28550
+ }
28551
+ async list(options) {
28552
+ return __privateGet(this, _read).call(this, async (snapshot, syncedThroughEpoch) => {
28553
+ __privateMethod(this, _SqliteReplicaHandle_instances, assertOpen_fn).call(this);
28554
+ await __privateMethod(this, _SqliteReplicaHandle_instances, assertEffective_fn).call(this, snapshot);
28555
+ const result = await new (__privateGet(this, _replica)).Replica({ store: snapshot }).list({
28556
+ prefix: options.prefix,
28557
+ ...options.after === void 0 ? {} : { after: options.after },
28558
+ ...options.limit === void 0 ? {} : { limit: options.limit }
28559
+ });
28560
+ await __privateMethod(this, _SqliteReplicaHandle_instances, assertEffective_fn).call(this, snapshot);
28561
+ return {
28562
+ keys: result.entries.map((entry) => entry.key),
28563
+ meta: {
28564
+ asOf: result.meta.asOf,
28565
+ coverage: result.meta.coverage,
28566
+ authority: result.meta.authority,
28567
+ syncedThroughEpoch
28568
+ }
28569
+ };
28570
+ }, true);
27454
28571
  }
27455
28572
  /**
27456
28573
  * The newest installed grant — the pending one when a not-yet-promoted
@@ -27460,43 +28577,59 @@ var SqliteReplicaHandle = class {
27460
28577
  * grant is still reported (unconstrained: false) and never reused.
27461
28578
  */
27462
28579
  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
- );
28580
+ return __privateGet(this, _read).call(this, async (snapshot) => {
28581
+ __privateMethod(this, _SqliteReplicaHandle_instances, assertOpen_fn).call(this);
28582
+ const state = await snapshot.open();
28583
+ if (state === null) return null;
28584
+ const selected = state.pendingGrant ?? state.grant;
28585
+ const install = selected === null ? null : await snapshot.getGrantInstall(selected.cid);
28586
+ return newestGrantInfo(
28587
+ __privateGet(this, _replica),
28588
+ state.pendingGrant ?? state.grant,
28589
+ state.pendingGrant,
28590
+ this.spec,
28591
+ install
28592
+ );
28593
+ });
27472
28594
  }
27473
28595
  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()
28596
+ return __privateGet(this, _guard2).call(this, async () => {
28597
+ await __privateMethod(this, _SqliteReplicaHandle_instances, assertLive_fn).call(this);
28598
+ const parsed = __privateGet(this, _replica).parseUcanGrant(ucan);
28599
+ const state = await __privateGet(this, _store).open();
28600
+ if (state === null) {
28601
+ throw new (__privateGet(this, _replica)).ReplicaError(
28602
+ __privateGet(this, _replica).ReplicaErrorCode.NOT_FOUND,
28603
+ "The replica has not been created."
28604
+ );
28605
+ }
28606
+ __privateGet(this, _replica).assertGrantInstallable(parsed, {
28607
+ deviceDid: principalOf(__privateGet(this, _device).did),
28608
+ space: this.spec.space,
28609
+ prefix: this.spec.prefix,
28610
+ now: Date.now()
28611
+ });
28612
+ if (!ucanAttUnconstrainedFor(parsed.att, this.spec.space, this.spec.prefix)) {
28613
+ throw new (__privateGet(this, _replica)).ReplicaError(
28614
+ __privateGet(this, _replica).ReplicaErrorCode.GRANT_NOT_COVERING,
28615
+ "The grant's get or sync coverage carries caveats; re-mint instead of installing it."
28616
+ );
28617
+ }
28618
+ const now = Date.now();
28619
+ const policy = await __privateGet(this, _policy).call(this, now, true);
28620
+ if (policy === null || now >= policy.consentDeadline) {
28621
+ throw new ReplicaStorageError("CONSENT_EXPIRED", "The replica's consent window has expired.");
28622
+ }
28623
+ const installedAt = await __privateGet(this, _recordGrantInstall).call(this, parsed.cid, now);
28624
+ const installed = await __privateGet(this, _store).installGatedGrant(parsed, { ...policy, at: installedAt });
28625
+ return newestGrantInfo(__privateGet(this, _replica), parsed, parsed, this.spec, installed);
27488
28626
  });
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
28627
  }
27498
28628
  async sync(options) {
27499
- await __privateMethod(this, _SqliteReplicaHandle_instances, assertLive_fn).call(this);
28629
+ await __privateGet(this, _guard2).call(this, async () => {
28630
+ await __privateMethod(this, _SqliteReplicaHandle_instances, assertLive_fn).call(this);
28631
+ await __privateMethod(this, _SqliteReplicaHandle_instances, assertSyncEffective_fn).call(this);
28632
+ });
27500
28633
  const transportFor = await __privateMethod(this, _SqliteReplicaHandle_instances, transports_fn).call(this);
27501
28634
  const abort = new AbortController();
27502
28635
  options.signal.throwIfAborted();
@@ -27537,27 +28670,33 @@ var SqliteReplicaHandle = class {
27537
28670
  }
27538
28671
  }
27539
28672
  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
- };
28673
+ return __privateGet(this, _read).call(this, async (snapshot, syncedThroughEpoch) => {
28674
+ __privateMethod(this, _SqliteReplicaHandle_instances, assertOpen_fn).call(this);
28675
+ const engine = new (__privateGet(this, _replica)).Replica({ store: snapshot });
28676
+ const status = await engine.status();
28677
+ const state = await snapshot.open();
28678
+ const selected = state?.pendingGrant ?? state?.grant ?? null;
28679
+ const selectedInstall = selected === null ? null : await snapshot.getGrantInstall(selected.cid);
28680
+ const activeRecord = state?.grant ?? null;
28681
+ const activeInstall = activeRecord === null ? null : await snapshot.getGrantInstall(activeRecord.cid);
28682
+ const grant = state === null ? null : newestGrantInfo(
28683
+ __privateGet(this, _replica),
28684
+ state.pendingGrant ?? state.grant,
28685
+ state.pendingGrant,
28686
+ this.spec,
28687
+ selectedInstall
28688
+ );
28689
+ return {
28690
+ coverage: status.coverage,
28691
+ lastSyncAt: status.lastSyncAt,
28692
+ syncedThroughEpoch,
28693
+ 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 },
28694
+ grant,
28695
+ counts: status.counts,
28696
+ bytes: status.bytes,
28697
+ lastError: status.lastError
28698
+ };
28699
+ }, true);
27561
28700
  }
27562
28701
  async close() {
27563
28702
  if (__privateGet(this, _closed)) return;
@@ -27572,8 +28711,11 @@ _store = new WeakMap();
27572
28711
  _replicaDir = new WeakMap();
27573
28712
  _replica = new WeakMap();
27574
28713
  _guard2 = new WeakMap();
28714
+ _read = new WeakMap();
28715
+ _policy = new WeakMap();
28716
+ _recordGrantInstall = new WeakMap();
27575
28717
  _dirSync2 = new WeakMap();
27576
- _generation = new WeakMap();
28718
+ _generation2 = new WeakMap();
27577
28719
  _ino = new WeakMap();
27578
28720
  _transportOverride = new WeakMap();
27579
28721
  _closed = new WeakMap();
@@ -27608,7 +28750,7 @@ assertLive_fn = async function() {
27608
28750
  throw error;
27609
28751
  }
27610
28752
  const state = await __privateGet(this, _store).open();
27611
- if (state !== null && state.generation !== __privateGet(this, _generation)) {
28753
+ if (state !== null && state.generation !== __privateGet(this, _generation2)) {
27612
28754
  throw new (__privateGet(this, _replica)).ReplicaError(
27613
28755
  __privateGet(this, _replica).ReplicaErrorCode.RESET_REQUIRED,
27614
28756
  "The replica was purged or reopened; open it again to continue."
@@ -27650,97 +28792,1099 @@ transports_fn = async function() {
27650
28792
  })());
27651
28793
  return __privateGet(this, _transportFactory);
27652
28794
  };
28795
+ assertEffective_fn = async function(snapshot) {
28796
+ const state = await snapshot.open();
28797
+ if (state?.grant === void 0 || state.grant === null) return;
28798
+ const installed = await snapshot.getGrantInstall(state.grant.cid);
28799
+ if (installed === null || !Number.isSafeInteger(installed.effectiveExpiresAt) || Date.now() >= installed.effectiveExpiresAt || Date.now() >= installed.consentDeadline) {
28800
+ throw new ReplicaStorageError("GRANT_EXPIRED", "The replica grant's effective lifetime has expired.");
28801
+ }
28802
+ };
28803
+ assertSyncEffective_fn = async function() {
28804
+ const state = await __privateGet(this, _store).open();
28805
+ const selected = state?.pendingGrant ?? state?.grant;
28806
+ if (selected === void 0 || selected === null) return;
28807
+ const installed = await __privateGet(this, _store).getGrantInstall(selected.cid);
28808
+ const now = Date.now();
28809
+ if (installed === null || !Number.isSafeInteger(installed.effectiveExpiresAt) || now >= installed.effectiveExpiresAt || now >= installed.consentDeadline) {
28810
+ throw new ReplicaStorageError("GRANT_EXPIRED", "The replica grant's effective lifetime has expired.");
28811
+ }
28812
+ };
27653
28813
  function createSqliteReplicaStorage(options) {
27654
28814
  const root = options.dir;
27655
- const guard = options.guard ?? ((section) => section());
28815
+ 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));
28816
+ const diagnosticFiles = [...options.diagnosticFiles ?? []];
28817
+ for (const name of diagnosticFiles) {
28818
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name) || /^[a-z2-7]{26}$/.test(name)) {
28819
+ throw new TypeError(`Invalid replication diagnostics file name ${JSON.stringify(name)}.`);
28820
+ }
28821
+ }
28822
+ const inspectLayout = (gateReadable, approvedMarker, legacyOnly = false) => inspectProfileReplicaLayout(profileDir, gateReadable, approvedMarker, legacyOnly, diagnosticFiles);
28823
+ const guard = options.guard;
27656
28824
  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")
28825
+ const heldLock = new import_node_async_hooks.AsyncLocalStorage();
28826
+ let visibleStamp;
28827
+ let visiblePrincipalHash;
28828
+ let checkedGate;
28829
+ async function profileSection(section) {
28830
+ if (heldLock.getStore() === true) return section();
28831
+ return requiredGuard(guard, () => heldLock.run(true, section));
28832
+ }
28833
+ async function guarded(principal, stamp, identity, partitionId, section) {
28834
+ if (stamp === void 0) throw gated("not_established");
28835
+ if (guard === void 0) throw gated("gate_unavailable");
28836
+ const gate = await fileGate();
28837
+ const snapshot = await gate.readGateSnapshot(profileDir);
28838
+ if (snapshot === void 0) throw gated("gate_unavailable");
28839
+ const [principalHash, identityKeyHash] = await Promise.all([
28840
+ gate.principalHashOf(snapshot.salt, principal),
28841
+ identity === void 0 ? Promise.resolve(void 0) : gate.identityHashOf(snapshot.salt, (0, import_sdk_services3.replicationIdentityKey)(identity))
27663
28842
  ]);
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
28843
+ return profileSection(async () => {
28844
+ assertLogoutClear(options.logoutPendingFile);
28845
+ await gate.assertGateAccess(profileDir, {
28846
+ stamp,
28847
+ incarnation: snapshot.incarnation,
28848
+ principalHash,
28849
+ identityKeyHash,
28850
+ partitionId
28851
+ }, snapshot.salt);
28852
+ const result = await section();
28853
+ assertLogoutClear(options.logoutPendingFile);
28854
+ return result;
27679
28855
  });
27680
- let generation;
28856
+ }
28857
+ async function readGated(spec, partitionId, store, replicaDir, generation, section, withFence = false) {
28858
+ if (spec.stamp === void 0) throw gated("not_established");
28859
+ const gate = await fileGate();
28860
+ const snapshot = await gate.readGateSnapshot(profileDir);
28861
+ if (snapshot === void 0) throw gated("gate_unavailable");
28862
+ const [principalHash, identityKeyHash] = await Promise.all([
28863
+ gate.principalHashOf(snapshot.salt, spec.identity.principal),
28864
+ gate.identityHashOf(snapshot.salt, (0, import_sdk_services3.replicationIdentityKey)(spec.identity))
28865
+ ]);
28866
+ const access = {
28867
+ stamp: spec.stamp,
28868
+ incarnation: snapshot.incarnation,
28869
+ principalHash,
28870
+ identityKeyHash,
28871
+ partitionId
28872
+ };
28873
+ const path = options.gateFile ?? (0, import_node_path.join)(profileDir, import_contractConstants.REPLICA_GATE_FILE);
28874
+ let checked;
28875
+ const checkGate = async () => {
28876
+ assertLogoutClear(options.logoutPendingFile);
28877
+ let before;
28878
+ try {
28879
+ before = await (0, import_promises.stat)(path, { bigint: true });
28880
+ } catch {
28881
+ throw gated("gate_unavailable");
28882
+ }
28883
+ if (checked?.ino === before.ino && checked.size === before.size && checked.mtimeNs === before.mtimeNs) return;
28884
+ const checkedState = await gate.assertGateAccess(profileDir, access, snapshot.salt);
28885
+ if (spec.grantPolicy?.consentAnchor === null && checkedState.firstInstall[identityKeyHash] === void 0) {
28886
+ const installed = await store.open();
28887
+ if (installed !== null && (installed.grant !== null || installed.pendingGrant !== null)) throw gated("gate_unavailable");
28888
+ }
28889
+ let after;
28890
+ try {
28891
+ after = await (0, import_promises.stat)(path, { bigint: true });
28892
+ } catch {
28893
+ throw gated("gate_unavailable");
28894
+ }
28895
+ if (before.ino !== after.ino || before.size !== after.size || before.mtimeNs !== after.mtimeNs) {
28896
+ throw gated("gate_unavailable");
28897
+ }
28898
+ checked = after;
28899
+ };
28900
+ if (withFence) await checkGate();
28901
+ const fence = withFence ? await readFence(replicaDir) : 0;
28902
+ return store.readTransaction(generation, checkGate, (sqliteSnapshot) => section(sqliteSnapshot, fence));
28903
+ }
28904
+ async function identityDirs() {
28905
+ let entries;
28906
+ try {
28907
+ entries = await (0, import_promises.readdir)(root, { withFileTypes: true });
28908
+ } catch (error) {
28909
+ if (isNotFound(error)) return [];
28910
+ throw storageError("STORAGE_ERROR", `Listing ${root}`, error);
28911
+ }
27681
28912
  try {
28913
+ const marker = await readLogoutMarker((0, import_node_path.join)(profileDir, REPLICA_LOGOUT_PENDING_FILE));
28914
+ if (marker !== null) {
28915
+ const inventory = await inspectLayout(true);
28916
+ assertApprovedInventory(inventory, {
28917
+ unknownEntries: marker.approvedUnknown,
28918
+ knownRoots: marker.knownRoots,
28919
+ knownEntries: marker.knownEntries,
28920
+ missingDatabases: marker.missingDatabases
28921
+ }, true);
28922
+ }
28923
+ for (const entry of entries) {
28924
+ if (entry.isSymbolicLink() || !entry.isDirectory() || !/^[a-z2-7]{26}$/.test(entry.name)) {
28925
+ if (marker?.approvedUnknown.some((item) => item.path === `replication/${entry.name}` && (entry.isDirectory() ? item.kind === "directory" : entry.isSymbolicLink() ? item.kind === "symlink" : item.kind === "file"))) continue;
28926
+ if (diagnosticFiles.includes(entry.name)) {
28927
+ const info = await (0, import_promises.lstat)((0, import_node_path.join)(root, entry.name)).catch((error) => {
28928
+ if (isNotFound(error)) return null;
28929
+ throw error;
28930
+ });
28931
+ if (info === null || info.isFile()) continue;
28932
+ }
28933
+ throw new ReplicaStorageError("STORAGE_ERROR", `Unknown identity partition at ${root}.`);
28934
+ }
28935
+ }
28936
+ 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));
28937
+ } catch (error) {
28938
+ throw storageError("STORAGE_ERROR", `Listing ${root}`, error);
28939
+ }
28940
+ }
28941
+ async function collectIntents(principalHash, salt) {
28942
+ const gate = await fileGate();
28943
+ const intents = [];
28944
+ for (const idDir of await identityDirs()) {
28945
+ const identity = identityRecordOf(await readJsonFile((0, import_node_path.join)(idDir, "identity.json")));
28946
+ if (identity === void 0) throw new ReplicaStorageError("STORAGE_ERROR", `Missing identity at ${idDir}.`);
28947
+ if (await gate.principalHashOf(salt, identity.principal) !== principalHash) continue;
28948
+ const identityKeyHash = await gate.identityHashOf(salt, (0, import_sdk_services3.replicationIdentityKey)(identity));
28949
+ const recordedAt = Date.now();
28950
+ if (await pathExists((0, import_node_path.join)(idDir, "pending.json")) || await pathExists((0, import_node_path.join)(idDir, "device.jwk"))) {
28951
+ intents.push({ id: `pending:${identityKeyHash}`, kind: "pending", identityKeyHash, principalHash, recordedAt });
28952
+ }
28953
+ const replicasDir = (0, import_node_path.join)(idDir, "replicas");
28954
+ let entries;
27682
28955
  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
28956
+ entries = await (0, import_promises.readdir)(replicasDir, { withFileTypes: true });
28957
+ } catch (error) {
28958
+ if (isNotFound(error)) continue;
28959
+ throw storageError("STORAGE_ERROR", `Listing ${replicasDir}`, error);
28960
+ }
28961
+ for (const entry of entries) {
28962
+ if (!entry.isDirectory()) continue;
28963
+ if (!/^[a-z2-7]{26}$/.test(entry.name)) {
28964
+ throw new ReplicaStorageError("STORAGE_ERROR", `Unrecognized replica partition at ${replicasDir}.`);
28965
+ }
28966
+ const partitionId = `${identityKeyHash}:${entry.name}`;
28967
+ intents.push({ id: partitionId, kind: "partition", partitionId, principalHash, recordedAt });
28968
+ }
28969
+ }
28970
+ return intents;
28971
+ }
28972
+ async function retainGrantInstalls(gate, gateState, identityKeyHash, store) {
28973
+ for (const { cid, installedAt } of await store.grantInstallTimes()) {
28974
+ const key2 = await gate.grantInstallHashOf(gateState.salt, identityKeyHash, cid);
28975
+ await gate.recordGrantInstall(profileDir, key2, installedAt);
28976
+ }
28977
+ }
28978
+ async function removeCorruptOrphanGate(gate) {
28979
+ const path = (0, import_node_path.join)(profileDir, import_contractConstants.REPLICA_GATE_FILE);
28980
+ const before = await (0, import_promises.lstat)(path, { bigint: true });
28981
+ if (!before.isFile() || before.isSymbolicLink()) throw gated("gate_unavailable");
28982
+ let corrupt = false;
28983
+ try {
28984
+ await gate.readGateSnapshot(profileDir);
28985
+ } catch (error) {
28986
+ if (typeof error === "object" && error !== null && "code" in error && error.code === "REPLICA_GATED" && "gate" in error && error.gate === "gate_unavailable") {
28987
+ corrupt = true;
28988
+ } else throw error;
28989
+ }
28990
+ if (!corrupt) throw gated("stamp_stale");
28991
+ const current = await (0, import_promises.lstat)(path, { bigint: true });
28992
+ if (current.dev !== before.dev || current.ino !== before.ino || current.mtimeNs !== before.mtimeNs || current.size !== before.size) throw gated("stamp_stale");
28993
+ await (0, import_promises.rm)(path);
28994
+ await (options.dirSync ?? fsyncDir)(profileDir);
28995
+ if (await gate.readGateSnapshot(profileDir) !== void 0) throw gated("stamp_stale");
28996
+ }
28997
+ async function readLogoutState(gate) {
28998
+ try {
28999
+ return { read: await gate.readLogoutGate(profileDir), corrupt: false };
29000
+ } catch (error) {
29001
+ if (isGateUnavailable(error)) return { read: void 0, corrupt: true };
29002
+ throw error;
29003
+ }
29004
+ }
29005
+ async function validateV1(gate, path, marker, read, mode) {
29006
+ if (read === void 0 || read.state.incarnation !== marker.incarnation) throw gated("stamp_stale");
29007
+ const { state, floorLagging } = read;
29008
+ if (marker.deniedEpoch !== void 0) {
29009
+ if (floorLagging || !v1Denied(marker, state, marker.deniedEpoch)) throw restoredGate();
29010
+ return { marker, state, active: false };
29011
+ }
29012
+ if (!floorLagging && state.principalHash === null && state.lifecycleEpoch > marker.lifecycleEpoch && (marker.remotePending ? state.eraseDeferred && state.deferredPrincipalHash === marker.principalHash : !state.eraseDeferred && state.deferredPrincipalHash === null)) {
29013
+ return { marker: await bindDeniedEpoch(path, marker, state.lifecycleEpoch), state, active: false };
29014
+ }
29015
+ if (state.principalHash !== marker.principalHash || state.lifecycleEpoch !== marker.lifecycleEpoch) {
29016
+ throw gated("stamp_stale");
29017
+ }
29018
+ if (!floorLagging && !mode.denyActive) {
29019
+ if (!mode.active) throw gated("stamp_stale");
29020
+ return { marker, state, active: true };
29021
+ }
29022
+ if (mode.acceptLagging) return { marker, state, active: true };
29023
+ if (mode.deny === void 0) throw interruptedSignOut();
29024
+ const denied = await gate.denyGate(
29025
+ profileDir,
29026
+ { reason: "sign_out", principal: "" },
29027
+ (principalHash) => collectIntents(principalHash, state.salt),
29028
+ { salt: state.salt, principalHash: marker.principalHash },
29029
+ { deferErase: mode.deny === "deferred" }
29030
+ );
29031
+ if (denied.status !== "denied") throw gated("gate_unavailable");
29032
+ const after = await gate.readLogoutGate(profileDir);
29033
+ if (after === void 0 || after.floorLagging || !v1Denied(marker, after.state, denied.lifecycleEpoch)) {
29034
+ throw gated("gate_unavailable");
29035
+ }
29036
+ return { marker: await bindDeniedEpoch(path, marker, denied.lifecycleEpoch), state: after.state, active: false };
29037
+ }
29038
+ function v1Denied(marker, state, epoch) {
29039
+ return state.incarnation === marker.incarnation && state.principalHash === null && state.lifecycleEpoch === epoch && (state.deferredPrincipalHash === marker.principalHash || !marker.remotePending && !state.eraseDeferred && state.deferredPrincipalHash === null);
29040
+ }
29041
+ async function bindDeniedEpoch(path, marker, deniedEpoch) {
29042
+ if (marker.deniedEpoch !== void 0) throw gated("stamp_stale");
29043
+ const bound = { ...marker, deniedEpoch };
29044
+ await writeJsonAtomic(path, bound, options.dirSync);
29045
+ return bound;
29046
+ }
29047
+ async function validatedRemoteMarker(path, marker, principal, mode) {
29048
+ if (marker.principalDigest !== logoutPrincipalDigest(principal)) throw gated("principal_mismatch");
29049
+ const gate = await fileGate();
29050
+ const { read, corrupt } = await readLogoutState(gate);
29051
+ if (corrupt) throw gated("gate_unavailable");
29052
+ if (marker.v === 2) {
29053
+ if (read !== void 0) throw gated("stamp_stale");
29054
+ return { marker, state: void 0 };
29055
+ }
29056
+ if (read === void 0 || read.state.incarnation !== marker.incarnation) throw gated("stamp_stale");
29057
+ if (marker.v === 3) {
29058
+ if (read.floorLagging || read.state.lifecycleEpoch !== marker.lifecycleEpoch || read.state.principalHash !== null || read.state.eraseDeferred) throw gated("stamp_stale");
29059
+ return { marker, state: read.state };
29060
+ }
29061
+ if (marker.principalHash !== await gate.principalHashOf(read.state.salt, principal)) {
29062
+ throw gated("principal_mismatch");
29063
+ }
29064
+ const checked = await validateV1(gate, path, marker, read, mode);
29065
+ return { marker: checked.marker, state: checked.state };
29066
+ }
29067
+ async function currentLayout() {
29068
+ const { read, corrupt } = await readLogoutState(await fileGate());
29069
+ if (corrupt) throw gated("gate_unavailable");
29070
+ const report = await inspectLayout(read !== void 0);
29071
+ assertSupportedProfileLayout(report);
29072
+ return report;
29073
+ }
29074
+ async function commitPlainAbandon(path, marker, report, discardLocalChanges) {
29075
+ const { remotePending: _, discardLocalChanges: _oldDiscard, ...previous } = marker;
29076
+ const abandoned = {
29077
+ ...previous,
29078
+ remoteAbandoned: true,
29079
+ approvedUnknown: discardLocalChanges ? report.unknownEntries : [],
29080
+ knownRoots: report.knownRoots,
29081
+ knownEntries: report.knownEntries,
29082
+ missingDatabases: report.missingDatabases,
29083
+ ...discardLocalChanges ? { discardLocalChanges: true } : {}
29084
+ };
29085
+ await writeJsonAtomic(path, abandoned, options.dirSync);
29086
+ return abandoned;
29087
+ }
29088
+ function deny(input, denyOptions) {
29089
+ const deferErase = denyOptions?.deferErase === true;
29090
+ if (deferErase && input.reason !== "sign_out") {
29091
+ throw new TypeError("Deferred erase is only available for sign-out.");
29092
+ }
29093
+ visibleStamp = void 0;
29094
+ visiblePrincipalHash = void 0;
29095
+ checkedGate = void 0;
29096
+ return (async () => {
29097
+ try {
29098
+ const gate = await fileGate();
29099
+ return await profileSection(async () => {
29100
+ const snapshot = (await gate.readLogoutGate(profileDir))?.state;
29101
+ const precomputed = snapshot === void 0 ? void 0 : {
29102
+ salt: snapshot.salt,
29103
+ principalHash: await gate.principalHashOf(snapshot.salt, input.principal)
29104
+ };
29105
+ const result = await gate.denyGate(
29106
+ profileDir,
29107
+ input,
29108
+ async (principalHash) => {
29109
+ if (snapshot === void 0) throw gated("gate_unavailable");
29110
+ return collectIntents(principalHash, snapshot.salt);
29111
+ },
29112
+ precomputed,
29113
+ { deferErase }
29114
+ );
29115
+ if (result.status !== "denied") return result;
29116
+ if (input.reason === "sign_out" && snapshot !== void 0 && precomputed !== void 0) {
29117
+ const path = (0, import_node_path.join)(profileDir, REPLICA_LOGOUT_PENDING_FILE);
29118
+ const marker = await readLogoutMarker(path);
29119
+ if (marker?.v === 1 && marker.deniedEpoch === void 0 && marker.incarnation === snapshot.incarnation && marker.principalHash === precomputed.principalHash && marker.remotePending === true === deferErase) {
29120
+ await bindDeniedEpoch(path, marker, result.lifecycleEpoch);
29121
+ }
29122
+ }
29123
+ if (!deferErase) return result;
29124
+ const resumeToken = await gate.deferredGateToken(profileDir);
29125
+ if (resumeToken === null || resumeToken.deniedEpoch !== result.lifecycleEpoch) {
29126
+ return { status: "unguarded", code: "gate_unavailable" };
29127
+ }
29128
+ return { ...result, resumeToken };
27693
29129
  });
27694
29130
  } catch (error) {
27695
- if (!replica.isReplicaError(error, replica.ReplicaErrorCode.CONFIG_MISMATCH)) {
27696
- throw error;
29131
+ return { status: "unguarded", code: typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : "gate_unavailable" };
29132
+ }
29133
+ })();
29134
+ }
29135
+ const lifecycle = {
29136
+ kind: "file",
29137
+ async recover({ signal }) {
29138
+ try {
29139
+ return await lifecycle.erasePending({ signal });
29140
+ } catch (error) {
29141
+ const code = typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : "STORAGE_ERROR";
29142
+ let remaining = 0;
29143
+ try {
29144
+ remaining = (await (await fileGate()).readGateSnapshot(profileDir))?.intents.length ?? 0;
29145
+ } catch {
27697
29146
  }
29147
+ return { erased: [], failed: [{ id: "gate", code }], remaining };
27698
29148
  }
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
- );
29149
+ },
29150
+ async establishSessionStamp(principal, { consentAnchor }) {
29151
+ if (consentAnchor?.source === "first_install" || !Number.isSafeInteger(consentAnchor?.at) || consentAnchor.at < 0) {
29152
+ throw gated("not_established");
29153
+ }
29154
+ const gate = await fileGate();
29155
+ const stamp = await profileSection(async () => {
29156
+ assertLogoutClear(options.logoutPendingFile);
29157
+ return gate.establishSessionStamp(profileDir, principal, { siweIssuedAt: consentAnchor.at });
29158
+ });
29159
+ const state = await gate.readGateSnapshot(profileDir);
29160
+ if (state === void 0) throw gated("gate_unavailable");
29161
+ visiblePrincipalHash = await gate.principalHashOf(state.salt, principal);
29162
+ checkedGate = void 0;
29163
+ visibleStamp = stamp;
29164
+ return stamp;
29165
+ },
29166
+ async restoreSessionStamp(principal, persisted) {
29167
+ const gate = await fileGate();
29168
+ const stamp = await profileSection(async () => {
29169
+ assertLogoutClear(options.logoutPendingFile);
29170
+ return gate.restoreSessionStamp(profileDir, principal, persisted);
29171
+ });
29172
+ const state = await gate.readGateSnapshot(profileDir);
29173
+ if (state === void 0) throw gated("gate_unavailable");
29174
+ visiblePrincipalHash = await gate.principalHashOf(state.salt, principal);
29175
+ checkedGate = void 0;
29176
+ visibleStamp = stamp;
29177
+ return stamp;
29178
+ },
29179
+ async activationAllowed(next) {
29180
+ if (guard === void 0) return false;
29181
+ try {
29182
+ const gate = await fileGate();
29183
+ return await profileSection(async () => {
29184
+ assertLogoutClear(options.logoutPendingFile);
29185
+ const state = await gate.readGateSnapshot(profileDir);
29186
+ return state !== void 0 && !state.eraseDeferred && state.intents.length === 0 && state.principalHash !== null && state.principalHash === await gate.principalHashOf(state.salt, next);
29187
+ });
29188
+ } catch {
29189
+ return false;
27705
29190
  }
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
- );
29191
+ },
29192
+ checkDelivery(stamp) {
29193
+ if (guard === void 0 || visiblePrincipalHash === void 0) return { status: "gated", gate: "gate_unavailable" };
29194
+ try {
29195
+ assertLogoutClear(options.logoutPendingFile);
29196
+ } catch {
29197
+ return { status: "gated", gate: "gate_unavailable" };
27712
29198
  }
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
- );
29199
+ if (!stamp || visibleStamp?.incarnation !== stamp.incarnation || visibleStamp.lifecycleEpoch !== stamp.lifecycleEpoch) {
29200
+ return { status: "gated", gate: "stamp_stale" };
27718
29201
  }
27719
- generation = state.generation;
27720
- } catch (error) {
27721
- await store.close().catch(() => void 0);
27722
- throw error;
29202
+ try {
29203
+ const path = options.gateFile ?? (0, import_node_path.join)(profileDir, import_contractConstants.REPLICA_GATE_FILE);
29204
+ const stats = (0, import_node_fs.statSync)(path, { bigint: true });
29205
+ if (checkedGate === void 0 || checkedGate.ino !== stats.ino || checkedGate.mtimeNs !== stats.mtimeNs || checkedGate.size !== stats.size) {
29206
+ const raw = JSON.parse((0, import_node_fs.readFileSync)(path, "utf8"));
29207
+ if (typeof raw.incarnation !== "string" || !Number.isSafeInteger(raw.lifecycleEpoch) || raw.principalHash !== null && typeof raw.principalHash !== "string" || !Array.isArray(raw.intents)) {
29208
+ return { status: "gated", gate: "gate_unavailable" };
29209
+ }
29210
+ checkedGate = {
29211
+ ino: stats.ino,
29212
+ mtimeNs: stats.mtimeNs,
29213
+ size: stats.size,
29214
+ incarnation: raw.incarnation,
29215
+ epoch: raw.lifecycleEpoch,
29216
+ principalHash: raw.principalHash
29217
+ };
29218
+ }
29219
+ } catch {
29220
+ return { status: "gated", gate: "gate_unavailable" };
29221
+ }
29222
+ if (checkedGate.incarnation !== stamp.incarnation || checkedGate.epoch !== stamp.lifecycleEpoch) {
29223
+ return { status: "gated", gate: "stamp_stale" };
29224
+ }
29225
+ return checkedGate.principalHash === visiblePrincipalHash ? { status: "allow" } : { status: "gated", gate: "principal_mismatch" };
29226
+ },
29227
+ consentDeadline() {
29228
+ return null;
29229
+ },
29230
+ deliverable(stamp) {
29231
+ return lifecycle.checkDelivery(stamp).status === "allow";
29232
+ },
29233
+ deny,
29234
+ deferredErase() {
29235
+ return profileSection(async () => (await fileGate()).deferredGateToken(profileDir));
29236
+ },
29237
+ async resumeDeferredErase(token, { signal }) {
29238
+ signal.throwIfAborted();
29239
+ await profileSection(async () => (await fileGate()).resumeDeferredGate(profileDir, token));
29240
+ return lifecycle.erasePending({ signal });
29241
+ },
29242
+ async preflightLogout({ principal, discardLocalChanges = false }) {
29243
+ return profileSection(async () => {
29244
+ const marker = await readLogoutMarker((0, import_node_path.join)(profileDir, REPLICA_LOGOUT_PENDING_FILE));
29245
+ const gate = await fileGate();
29246
+ const { read, corrupt } = await readLogoutState(gate);
29247
+ const state = read?.state;
29248
+ let otherAccount = false;
29249
+ if (state !== void 0) {
29250
+ const hash3 = await gate.principalHashOf(state.salt, principal);
29251
+ otherAccount = gateOwnedByAnother(state, hash3);
29252
+ if (marker?.v === 1 && marker.principalHash !== hash3) throw gated("principal_mismatch");
29253
+ }
29254
+ const report = await inspectLayout(state !== void 0, void 0, otherAccount);
29255
+ assertSupportedProfileLayout(report);
29256
+ if (!discardLocalChanges && (report.unknownPaths.length > 0 || corrupt && !otherAccount || !otherAccount && (state === void 0 || state.principalHash === null && !state.eraseDeferred) && await hasOrphanPending(profileDir))) {
29257
+ throw localChangesRefusal(report.unknownPaths);
29258
+ }
29259
+ return report;
29260
+ });
29261
+ },
29262
+ async prepareLogoutPending({ principal, approval, discardLocalChanges = false, remotePending }) {
29263
+ const gate = await fileGate();
29264
+ return profileSection(async () => {
29265
+ const path = (0, import_node_path.join)(profileDir, REPLICA_LOGOUT_PENDING_FILE);
29266
+ const previous = await readLogoutMarker(path);
29267
+ const { read, corrupt: corruptGate } = await readLogoutState(gate);
29268
+ let state = read?.state;
29269
+ const principalHash = state === void 0 ? void 0 : await gate.principalHashOf(state.salt, principal);
29270
+ const otherAccount = state !== void 0 && gateOwnedByAnother(state, principalHash);
29271
+ let report = await inspectLayout(state !== void 0, void 0, otherAccount);
29272
+ assertSupportedProfileLayout(report);
29273
+ const assertApproved = (current) => {
29274
+ assertApprovedInventory(current, approval);
29275
+ if (approval.knownLegacyReplicas !== current.knownLegacyReplicas || JSON.stringify(approval.unknownPaths) !== JSON.stringify(current.unknownPaths) || JSON.stringify(approval.unknownLegacyPaths) !== JSON.stringify(current.unknownLegacyPaths)) {
29276
+ throw localChangesRefusal(current.unknownPaths);
29277
+ }
29278
+ };
29279
+ assertApproved(report);
29280
+ if (otherAccount) {
29281
+ if (previous !== null) throw gated("principal_mismatch");
29282
+ if (!discardLocalChanges && report.unknownPaths.length > 0) throw localChangesRefusal(report.unknownPaths);
29283
+ return null;
29284
+ }
29285
+ if (!discardLocalChanges && (report.unknownPaths.length > 0 || corruptGate || (state === void 0 || state.principalHash === null && !state.eraseDeferred) && await hasOrphanPending(profileDir))) {
29286
+ throw localChangesRefusal(report.unknownPaths);
29287
+ }
29288
+ if (previous !== null) {
29289
+ if (previous.principalDigest !== logoutPrincipalDigest(principal)) throw gated("principal_mismatch");
29290
+ if (corruptGate) throw gated("gate_unavailable");
29291
+ let current = previous;
29292
+ if (previous.v === 2) {
29293
+ if (read !== void 0) throw gated("stamp_stale");
29294
+ } else if (read === void 0 || previous.incarnation !== read.state.incarnation) {
29295
+ throw gated("stamp_stale");
29296
+ } else if (previous.v === 3) {
29297
+ if (read.floorLagging || read.state.lifecycleEpoch !== previous.lifecycleEpoch || read.state.principalHash !== null || read.state.eraseDeferred) throw gated("stamp_stale");
29298
+ } else {
29299
+ if (previous.principalHash !== principalHash) throw gated("principal_mismatch");
29300
+ current = (await validateV1(gate, path, previous, read, { active: true, acceptLagging: true })).marker;
29301
+ }
29302
+ assertApprovedInventory(report, {
29303
+ unknownEntries: current.approvedUnknown,
29304
+ knownRoots: current.knownRoots,
29305
+ knownEntries: current.knownEntries,
29306
+ missingDatabases: current.missingDatabases
29307
+ });
29308
+ if (current.remotePending !== remotePending || current.remoteAbandoned) {
29309
+ throw gated("gate_unavailable");
29310
+ }
29311
+ return current;
29312
+ }
29313
+ if (state?.eraseDeferred) throw gated("stamp_stale");
29314
+ if (read?.floorLagging) {
29315
+ if (state.principalHash === null) {
29316
+ state = await gate.advanceInactiveGate(profileDir);
29317
+ } else {
29318
+ const salt = state.salt;
29319
+ const denied = await gate.denyGate(
29320
+ profileDir,
29321
+ { reason: "sign_out", principal },
29322
+ (hash3) => collectIntents(hash3, salt),
29323
+ { salt, principalHash }
29324
+ );
29325
+ if (denied.status !== "denied") throw gated("gate_unavailable");
29326
+ state = await gate.readGateSnapshot(profileDir);
29327
+ }
29328
+ report = await inspectLayout(true);
29329
+ assertSupportedProfileLayout(report);
29330
+ assertApproved(report);
29331
+ }
29332
+ if (state === void 0 && report.knownRoots.length === 0 && !corruptGate) return null;
29333
+ if (state !== void 0 && state.principalHash === null && !state.eraseDeferred && report.knownRoots.length === 0 && state.intents.length === 0) return null;
29334
+ const remoteState = remotePending ? { remotePending: true } : {};
29335
+ const approvalFields = {
29336
+ principalDigest: logoutPrincipalDigest(principal),
29337
+ approvalSalt: (0, import_node_crypto2.randomBytes)(16).toString("hex"),
29338
+ approvedUnknown: discardLocalChanges ? report.unknownEntries : [],
29339
+ knownRoots: report.knownRoots,
29340
+ knownEntries: report.knownEntries,
29341
+ missingDatabases: report.missingDatabases,
29342
+ ...discardLocalChanges ? { discardLocalChanges: true } : {},
29343
+ ...remoteState
29344
+ };
29345
+ let marker;
29346
+ if (state === void 0) {
29347
+ marker = {
29348
+ v: 2,
29349
+ scope: "orphan_profile",
29350
+ nonce: (0, import_node_crypto2.randomBytes)(16).toString("hex"),
29351
+ ...approvalFields
29352
+ };
29353
+ } else if (state.principalHash === null) {
29354
+ marker = {
29355
+ v: 3,
29356
+ scope: "inactive_gate",
29357
+ incarnation: state.incarnation,
29358
+ lifecycleEpoch: state.lifecycleEpoch,
29359
+ ...approvalFields
29360
+ };
29361
+ } else {
29362
+ marker = {
29363
+ v: 1,
29364
+ incarnation: state.incarnation,
29365
+ lifecycleEpoch: state.lifecycleEpoch,
29366
+ principalHash,
29367
+ ...approvalFields
29368
+ };
29369
+ }
29370
+ await writeJsonAtomic(path, marker, options.dirSync);
29371
+ visibleStamp = void 0;
29372
+ visiblePrincipalHash = void 0;
29373
+ checkedGate = void 0;
29374
+ if (corruptGate) await removeCorruptOrphanGate(gate);
29375
+ return marker;
29376
+ });
29377
+ },
29378
+ async completeRemoteLogout({ 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) throw gated("gate_unavailable");
29383
+ const checked = await validatedRemoteMarker(path, marker, principal, { active: false });
29384
+ const { remotePending: _, ...complete } = checked.marker;
29385
+ await writeJsonAtomic(path, complete, options.dirSync);
29386
+ return complete;
29387
+ });
29388
+ },
29389
+ async remotePendingLogout({ principal }) {
29390
+ return profileSection(async () => {
29391
+ const path = (0, import_node_path.join)(profileDir, REPLICA_LOGOUT_PENDING_FILE);
29392
+ const marker = await readLogoutMarker(path);
29393
+ if (marker === null || marker.remotePending !== true) return false;
29394
+ await validatedRemoteMarker(path, marker, principal, { active: false });
29395
+ return true;
29396
+ });
29397
+ },
29398
+ async abandonRemoteLogout({ principal, approval, discardLocalChanges = false }) {
29399
+ return profileSection(async () => {
29400
+ const path = (0, import_node_path.join)(profileDir, REPLICA_LOGOUT_PENDING_FILE);
29401
+ const marker = await readLogoutMarker(path);
29402
+ if (marker === null || marker.remotePending !== true) throw gated("gate_unavailable");
29403
+ if (marker.principalDigest !== logoutPrincipalDigest(principal)) throw gated("principal_mismatch");
29404
+ const report = await currentLayout();
29405
+ if (storedInventoryChanged(marker, report)) throw changedInventoryRefusal(marker, principal, report);
29406
+ assertApprovedInventory(report, approval);
29407
+ 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) {
29408
+ throw localChangesRefusal(report.unknownPaths);
29409
+ }
29410
+ const checked = await validatedRemoteMarker(path, marker, principal, { active: true, deny: "deferred" });
29411
+ return commitPlainAbandon(path, checked.marker, report, discardLocalChanges);
29412
+ });
29413
+ },
29414
+ async approveChangedPlainLogout({ principal, approval, discardLocalChanges, inventoryApprovalToken: token }) {
29415
+ return profileSection(async () => {
29416
+ const path = (0, import_node_path.join)(profileDir, REPLICA_LOGOUT_PENDING_FILE);
29417
+ const marker = await readLogoutMarker(path);
29418
+ if (marker === null || marker.remotePending !== true) throw gated("gate_unavailable");
29419
+ if (marker.principalDigest !== logoutPrincipalDigest(principal)) throw gated("principal_mismatch");
29420
+ const report = await currentLayout();
29421
+ if (!storedInventoryChanged(marker, report)) throw gated("gate_unavailable");
29422
+ const entries = approvalEntries(report);
29423
+ if (discardLocalChanges !== true || typeof token !== "string" || !/^[0-9a-f]{32}$/.test(token) || token !== inventoryApprovalToken(marker, principal, entries)) {
29424
+ throw changedInventoryRefusal(marker, principal, report, entries);
29425
+ }
29426
+ assertApprovedInventory(report, approval);
29427
+ if (approval.knownLegacyReplicas !== report.knownLegacyReplicas || JSON.stringify(approval.unknownPaths) !== JSON.stringify(report.unknownPaths) || JSON.stringify(approval.unknownLegacyPaths) !== JSON.stringify(report.unknownLegacyPaths)) {
29428
+ throw changedInventoryRefusal(marker, principal, report, entries);
29429
+ }
29430
+ const checked = await validatedRemoteMarker(path, marker, principal, { active: true, deny: "deferred" });
29431
+ return commitPlainAbandon(path, checked.marker, report, true);
29432
+ });
29433
+ },
29434
+ async reapproveLogoutPending({ principal, approval }) {
29435
+ return profileSection(async () => {
29436
+ const path = (0, import_node_path.join)(profileDir, REPLICA_LOGOUT_PENDING_FILE);
29437
+ const marker = await readLogoutMarker(path);
29438
+ if (marker === null || marker.remotePending) throw gated("gate_unavailable");
29439
+ if (marker.principalDigest !== logoutPrincipalDigest(principal)) throw gated("principal_mismatch");
29440
+ const report = await currentLayout();
29441
+ assertApprovedInventory(report, approval);
29442
+ if (JSON.stringify(report.unknownPaths) !== JSON.stringify(approval.unknownPaths)) {
29443
+ throw localChangesRefusal(report.unknownPaths);
29444
+ }
29445
+ const checked = await validatedRemoteMarker(path, marker, principal, { active: true });
29446
+ const amended = {
29447
+ ...checked.marker,
29448
+ approvedUnknown: report.unknownEntries,
29449
+ knownRoots: report.knownRoots,
29450
+ knownEntries: report.knownEntries,
29451
+ missingDatabases: report.missingDatabases,
29452
+ ...report.unknownPaths.length > 0 ? { discardLocalChanges: true } : {}
29453
+ };
29454
+ await writeJsonAtomic(path, amended, options.dirSync);
29455
+ return amended;
29456
+ });
29457
+ },
29458
+ async recoverLogoutPending({ signal, clearCredentials, eraseLegacyReplicas }) {
29459
+ const path = (0, import_node_path.join)(profileDir, REPLICA_LOGOUT_PENDING_FILE);
29460
+ const failure = async (error) => ({
29461
+ erased: [],
29462
+ 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" }],
29463
+ remaining: Math.max(1, (await lifecycle.pendingPurge().catch(() => [])).length)
29464
+ });
29465
+ try {
29466
+ const prepared = await profileSection(async () => {
29467
+ const marker2 = await readLogoutMarker(path);
29468
+ if (marker2 === null) {
29469
+ const state2 = await (await fileGate()).readGateSnapshot(profileDir);
29470
+ return { report: { erased: [], failed: [], remaining: state2?.intents.length ?? 0 } };
29471
+ }
29472
+ signal.throwIfAborted();
29473
+ if (marker2.remotePending) throw gated("gate_unavailable");
29474
+ const gate = await fileGate();
29475
+ const { read, corrupt } = await readLogoutState(gate);
29476
+ if (corrupt) throw gated("gate_unavailable");
29477
+ let current = marker2;
29478
+ let state = read?.state;
29479
+ if (marker2.v === 2) {
29480
+ if (read !== void 0) throw gated("stamp_stale");
29481
+ } else if (read === void 0 || read.state.incarnation !== marker2.incarnation) {
29482
+ throw gated("stamp_stale");
29483
+ } else if (marker2.v === 3) {
29484
+ if (read.floorLagging || read.state.principalHash !== null || read.state.eraseDeferred || read.state.lifecycleEpoch !== marker2.lifecycleEpoch) throw gated("stamp_stale");
29485
+ } else {
29486
+ const checked = await validateV1(
29487
+ gate,
29488
+ path,
29489
+ marker2,
29490
+ read,
29491
+ { active: true, deny: "ordinary", denyActive: true }
29492
+ );
29493
+ current = checked.marker;
29494
+ state = checked.state;
29495
+ }
29496
+ await clearCredentials();
29497
+ for (const file of ["session.json", "key.json"]) {
29498
+ try {
29499
+ await (0, import_promises.lstat)((0, import_node_path.join)(profileDir, file));
29500
+ throw gated("gate_unavailable");
29501
+ } catch (error) {
29502
+ if (!isNotFound(error)) throw error;
29503
+ }
29504
+ }
29505
+ await (options.dirSync ?? fsyncDir)(profileDir);
29506
+ const report = await inspectLayout(state !== void 0, marker2);
29507
+ assertApprovedInventory(report, {
29508
+ unknownEntries: marker2.approvedUnknown,
29509
+ knownRoots: marker2.knownRoots,
29510
+ knownEntries: marker2.knownEntries,
29511
+ missingDatabases: marker2.missingDatabases
29512
+ }, true);
29513
+ if (!marker2.discardLocalChanges && report.unknownEntries.length > 0) {
29514
+ throw localChangesRefusal(report.unknownPaths);
29515
+ }
29516
+ if (current.v === 1 && state?.eraseDeferred) {
29517
+ await gate.resumeDeferredGate(profileDir, {
29518
+ deniedEpoch: current.deniedEpoch,
29519
+ principalHash: current.principalHash
29520
+ });
29521
+ }
29522
+ await eraseLegacyReplicas({
29523
+ discardLocalChanges: marker2.discardLocalChanges === true,
29524
+ approvedUnknown: marker2.approvedUnknown,
29525
+ knownRoots: marker2.knownRoots,
29526
+ knownEntries: marker2.knownEntries,
29527
+ missingDatabases: marker2.missingDatabases
29528
+ });
29529
+ try {
29530
+ await (0, import_promises.lstat)((0, import_node_path.join)(profileDir, "replicas"));
29531
+ throw gated("gate_unavailable");
29532
+ } catch (error) {
29533
+ if (!isNotFound(error)) throw error;
29534
+ }
29535
+ return { marker: current };
29536
+ });
29537
+ if ("report" in prepared) return prepared.report;
29538
+ const marker = prepared.marker;
29539
+ let erased = { erased: [], failed: [], remaining: 0 };
29540
+ if (marker.v !== 2) {
29541
+ erased = await eraseGateIntents({ signal }, marker);
29542
+ if (erased.failed.length > 0 || erased.remaining > 0) return erased;
29543
+ }
29544
+ await profileSection(async () => {
29545
+ const pending = await readLogoutMarker(path);
29546
+ if (pending === null || pending.remotePending || JSON.stringify(pending) !== JSON.stringify(marker)) {
29547
+ throw gated("stamp_stale");
29548
+ }
29549
+ const gate = await fileGate();
29550
+ const state = await gate.readGateSnapshot(profileDir);
29551
+ if (marker.v === 2 && state !== void 0) throw gated("stamp_stale");
29552
+ const report = await inspectLayout(state !== void 0, marker);
29553
+ assertApprovedInventory(report, {
29554
+ unknownEntries: marker.approvedUnknown,
29555
+ knownRoots: marker.knownRoots,
29556
+ knownEntries: marker.knownEntries,
29557
+ missingDatabases: marker.missingDatabases
29558
+ }, true);
29559
+ await eraseApprovedUnknown(profileDir, marker, "replication", diagnosticFiles);
29560
+ await eraseKnownFlagResidue(profileDir, marker, diagnosticFiles);
29561
+ const finalReport = await inspectLayout(state !== void 0, marker);
29562
+ assertApprovedInventory(finalReport, {
29563
+ unknownEntries: marker.approvedUnknown,
29564
+ knownRoots: marker.knownRoots,
29565
+ knownEntries: marker.knownEntries,
29566
+ missingDatabases: marker.missingDatabases
29567
+ }, true);
29568
+ if (finalReport.unknownPaths.length > 0) throw localChangesRefusal(finalReport.unknownPaths);
29569
+ if (finalReport.knownRoots.length > 0) throw gated("gate_unavailable");
29570
+ await (options.dirSync ?? fsyncDir)(profileDir);
29571
+ await (0, import_promises.rm)(path);
29572
+ await (options.dirSync ?? fsyncDir)(profileDir);
29573
+ });
29574
+ return erased;
29575
+ } catch (error) {
29576
+ return failure(error);
29577
+ }
29578
+ },
29579
+ async pendingPurge() {
29580
+ const gate = await fileGate();
29581
+ return (await gate.readGateSnapshot(profileDir))?.intents ?? [];
29582
+ },
29583
+ erasePending({ signal }) {
29584
+ return eraseGateIntents({ signal });
27723
29585
  }
27724
- let ino;
29586
+ };
29587
+ async function eraseGateIntents({ signal }, marker) {
29588
+ const gate = await fileGate();
29589
+ const erased = [];
29590
+ const failed = [];
29591
+ const intents = await lifecycle.pendingPurge();
27725
29592
  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
29593
+ if (marker === void 0) assertLogoutClear(options.logoutPendingFile);
29594
+ else if (marker.remotePending || await readLogoutMarker((0, import_node_path.join)(profileDir, REPLICA_LOGOUT_PENDING_FILE)) === null) {
29595
+ throw gated("gate_unavailable");
29596
+ }
29597
+ } catch {
29598
+ return { erased, failed: [{ id: "logout", code: "REPLICA_GATED" }], remaining: Math.max(1, intents.length) };
29599
+ }
29600
+ const held = await profileSection(() => gate.readGateSnapshot(profileDir));
29601
+ if (held?.eraseDeferred) return { erased, failed, remaining: held.intents.length, deferred: true };
29602
+ for (const intent of intents) {
29603
+ signal.throwIfAborted();
29604
+ try {
29605
+ await profileSection(async () => {
29606
+ if (marker === void 0) assertLogoutClear(options.logoutPendingFile);
29607
+ else {
29608
+ const pending = await readLogoutMarker((0, import_node_path.join)(profileDir, REPLICA_LOGOUT_PENDING_FILE));
29609
+ if (pending === null || pending.remotePending || JSON.stringify(pending) !== JSON.stringify(marker)) {
29610
+ throw gated("gate_unavailable");
29611
+ }
29612
+ assertApprovedInventory(await inspectLayout(true), {
29613
+ unknownEntries: marker.approvedUnknown,
29614
+ knownRoots: marker.knownRoots,
29615
+ knownEntries: marker.knownEntries,
29616
+ missingDatabases: marker.missingDatabases
29617
+ }, true);
29618
+ }
29619
+ const state = await gate.readGateSnapshot(profileDir);
29620
+ if (state?.eraseDeferred) return;
29621
+ if (!state?.intents.some((queued) => queued.id === intent.id)) return;
29622
+ let idDir;
29623
+ for (const dir of await identityDirs()) {
29624
+ const identity = identityRecordOf(await readJsonFile((0, import_node_path.join)(dir, "identity.json")));
29625
+ if (identity === void 0) throw new ReplicaStorageError("STORAGE_ERROR", `Missing identity at ${dir}.`);
29626
+ const hash3 = await gate.identityHashOf(state.salt, (0, import_sdk_services3.replicationIdentityKey)(identity));
29627
+ if (intent.kind === "pending" ? hash3 === intent.identityKeyHash : intent.partitionId.startsWith(`${hash3}:`)) {
29628
+ idDir = dir;
29629
+ break;
29630
+ }
29631
+ }
29632
+ if (intent.kind === "pending") {
29633
+ if (intent.id !== `pending:${intent.identityKeyHash}`) throw gated("gate_unavailable");
29634
+ if (idDir !== void 0) {
29635
+ for (const name of ["pending.json", "device.jwk"]) {
29636
+ const file = (0, import_node_path.join)(idDir, name);
29637
+ let info;
29638
+ try {
29639
+ info = await (0, import_promises.lstat)(file, { bigint: true });
29640
+ } catch (error) {
29641
+ if (isNotFound(error)) continue;
29642
+ throw error;
29643
+ }
29644
+ if (!info.isFile()) throw gated("gate_unavailable");
29645
+ if (marker !== void 0) {
29646
+ const relative = `replication/${(0, import_node_path.basename)(idDir)}/${name}`;
29647
+ const expected = [...marker.knownEntries, ...marker.approvedUnknown].find(
29648
+ (item) => item.path === relative
29649
+ );
29650
+ if (expected?.kind !== "file" || expected.dev !== info.dev.toString() || expected.ino !== info.ino.toString()) throw localChangesRefusal([relative]);
29651
+ }
29652
+ await (0, import_promises.rm)(file);
29653
+ }
29654
+ await fsyncDir(idDir);
29655
+ }
29656
+ } else {
29657
+ if (!/^[0-9a-f]{64}:[a-z2-7]{26}$/.test(intent.partitionId) || intent.id !== intent.partitionId) {
29658
+ throw gated("gate_unavailable");
29659
+ }
29660
+ if (idDir !== void 0) {
29661
+ const replicaDir = (0, import_node_path.join)(idDir, "replicas", intent.partitionId.slice(65));
29662
+ let dirInfo;
29663
+ if (await realDirectoryExists((0, import_node_path.join)(idDir, "replicas"))) {
29664
+ try {
29665
+ dirInfo = await (0, import_promises.lstat)(replicaDir, { bigint: true });
29666
+ } catch (error) {
29667
+ if (!isNotFound(error)) throw error;
29668
+ }
29669
+ }
29670
+ if (dirInfo !== void 0) {
29671
+ const relative = `replication/${(0, import_node_path.basename)(idDir)}/replicas/${(0, import_node_path.basename)(replicaDir)}`;
29672
+ const expectedRoot = marker?.knownRoots.find((item) => item.path === relative);
29673
+ if (!dirInfo.isDirectory() || marker !== void 0 && (expectedRoot?.dev !== dirInfo.dev.toString() || expectedRoot.ino !== dirInfo.ino.toString())) throw localChangesRefusal([relative]);
29674
+ const approvedUnknown = marker?.approvedUnknown.filter(
29675
+ (item) => item.path.startsWith(`${relative}/`)
29676
+ ).map(
29677
+ (item) => ({ ...item, path: item.path.slice(relative.length + 1) })
29678
+ );
29679
+ const expectedKnown = marker?.knownEntries.filter(
29680
+ (item) => item.path.startsWith(`${relative}/`)
29681
+ ).map(
29682
+ (item) => ({ ...item, path: item.path.slice(relative.length + 1) })
29683
+ );
29684
+ const sqlite = await import("@tinycloud/replica/sqlite");
29685
+ if (!await pathExists((0, import_node_path.join)(replicaDir, "replica.db"))) {
29686
+ const layout = await sqlite.inspectSqliteReplicaLayout(replicaDir, { allowAdapterFence: true });
29687
+ 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))) {
29688
+ throw localChangesRefusal([relative]);
29689
+ }
29690
+ await sqlite.removeSqliteReplicaLayout(replicaDir, {
29691
+ approvedUnknown,
29692
+ expectedKnown: layout.knownEntries,
29693
+ expectedRoot,
29694
+ allowAdapterFence: true,
29695
+ allowMissingDatabase: true
29696
+ });
29697
+ } else {
29698
+ let store;
29699
+ try {
29700
+ store = await sqlite.SqliteReplicaStore.open(replicaDir, { create: false, guard: profileSection });
29701
+ const row = await store.open();
29702
+ if (row !== null) {
29703
+ const lease = await store.acquireSyncLease(SYNC_LEASE_TTL_MS);
29704
+ if (lease === null) throw gated("gate_unavailable");
29705
+ try {
29706
+ if (row.revoked === null) await store.reset(lease, "purged");
29707
+ let deletionKnown = expectedKnown;
29708
+ if (marker !== void 0) {
29709
+ const current = await sqlite.inspectSqliteReplicaLayout(replicaDir, {
29710
+ allowAdapterFence: true
29711
+ });
29712
+ if (current.rootIdentity.dev !== expectedRoot?.dev || current.rootIdentity.ino !== expectedRoot.ino) throw localChangesRefusal([relative]);
29713
+ const approved = new Map(expectedKnown?.map((entry) => [entry.path, entry]));
29714
+ for (const entry of current.knownEntries) {
29715
+ const prior = approved.get(entry.path);
29716
+ if (prior?.kind === entry.kind && prior.dev === entry.dev && prior.ino === entry.ino) continue;
29717
+ if (entry.kind !== "file" || !["replica.db-wal", "replica.db-shm", "replica.db-journal"].includes(entry.path)) {
29718
+ throw localChangesRefusal([`${relative}/${entry.path}`]);
29719
+ }
29720
+ }
29721
+ deletionKnown = current.knownEntries;
29722
+ }
29723
+ await store.destroy(lease, {
29724
+ approvedUnknown,
29725
+ expectedKnown: deletionKnown,
29726
+ expectedRoot,
29727
+ allowAdapterFence: true
29728
+ });
29729
+ } finally {
29730
+ await store.releaseLease(lease).catch(() => void 0);
29731
+ }
29732
+ } else {
29733
+ await sqlite.removeSqliteReplicaLayout(replicaDir, {
29734
+ approvedUnknown,
29735
+ expectedKnown,
29736
+ expectedRoot,
29737
+ allowAdapterFence: true
29738
+ });
29739
+ }
29740
+ } finally {
29741
+ await store?.close().catch(() => void 0);
29742
+ }
29743
+ }
29744
+ await fsyncDir((0, import_node_path.join)(idDir, "replicas"));
29745
+ }
29746
+ }
29747
+ }
29748
+ await gate.clearGateIntent(profileDir, intent.id);
29749
+ erased.push(intent.id);
29750
+ });
29751
+ } catch (error) {
29752
+ failed.push({ id: intent.id, code: typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : "STORAGE_ERROR" });
29753
+ }
29754
+ }
29755
+ return { erased, failed, remaining: (await lifecycle.pendingPurge()).length };
29756
+ }
29757
+ async function openHandle(spec) {
29758
+ const [replica, sqlite] = await Promise.all([
29759
+ // @ts-ignore — TS2307 when replica's dist is absent (CI builds node-sdk first).
29760
+ import("@tinycloud/replica"),
29761
+ // @ts-ignore — same as above.
29762
+ import("@tinycloud/replica/sqlite")
29763
+ ]);
29764
+ if (spec.stamp === void 0) throw gated("not_established");
29765
+ if (guard === void 0) throw gated("gate_unavailable");
29766
+ if (spec.grantPolicy === void 0) {
29767
+ throw new ReplicaStorageError("REPLICA_INVALID_ARGUMENT", "A replica grant policy is required.");
29768
+ }
29769
+ const gate = await fileGate();
29770
+ const snapshot = await gate.readGateSnapshot(profileDir);
29771
+ if (snapshot === void 0) throw gated("gate_unavailable");
29772
+ const identityKeyHash = await gate.identityHashOf(snapshot.salt, (0, import_sdk_services3.replicationIdentityKey)(spec.identity));
29773
+ const resolvePolicy = async (at, recordFirst) => {
29774
+ const policy = spec.grantPolicy;
29775
+ if (!Number.isSafeInteger(policy.grantTtlMs) || policy.grantTtlMs <= 0 || policy.grantTtlMs > import_contractConstants.MAX_GRANT_TTL_MS) {
29776
+ throw new TypeError("A replica grant requires a finite TTL not exceeding seven days.");
29777
+ }
29778
+ const verified = policy.consentAnchor;
29779
+ if (verified !== null && (!Number.isSafeInteger(verified.at) || verified.at < 0 || verified.source === "first_install")) {
29780
+ throw new ReplicaStorageError("CONSENT_EXPIRED", "The verified consent anchor is unavailable.");
29781
+ }
29782
+ const first = recordFirst ? await gate.recordFirstInstall(profileDir, identityKeyHash, at) : await gate.readFirstInstall(profileDir, identityKeyHash);
29783
+ const anchor = verified === null ? first === void 0 ? null : { at: first, source: "first_install" } : { at: Math.min(verified.at, at), source: verified.source };
29784
+ if (anchor === null) return null;
29785
+ const deadline = anchor.at + import_contractConstants.CONSENT_WINDOW_MS;
29786
+ if (!Number.isSafeInteger(anchor.at) || anchor.at < 0 || !Number.isSafeInteger(deadline)) {
29787
+ throw new ReplicaStorageError("CONSENT_EXPIRED", "The durable consent anchor is invalid.");
29788
+ }
29789
+ return { at, ttlMs: policy.grantTtlMs, consentDeadline: deadline, consentAnchor: anchor };
29790
+ };
29791
+ let partitionId;
29792
+ const storeGuard = (section) => guarded(spec.identity.principal, spec.stamp, spec.identity, partitionId, section);
29793
+ return guarded(spec.identity.principal, spec.stamp, spec.identity, void 0, async () => {
29794
+ if ((0, import_sdk_services3.requiresSecretsOptIn)(spec.space, spec.prefix) && spec.allowSecrets !== true) {
29795
+ throw new replica.ReplicaError(
29796
+ replica.ReplicaErrorCode.SECRETS_OPT_IN_REQUIRED,
29797
+ `Replicating ${spec.space}/kv/${spec.prefix} copies secret material to disk; pass allowSecrets to opt in.`
29798
+ );
29799
+ }
29800
+ const idDir = partitionDirOf(root, spec.identity);
29801
+ await ensurePartitionIdentity(idDir, spec.identity, options.dirSync);
29802
+ const createdDevice = spec.device ?? await loadOrCreateDevice(idDir, profileSection, createDevice, options.dirSync);
29803
+ const device = { ...createdDevice, did: principalOf(createdDevice.did) };
29804
+ const replicaHash = replicaHashOf(spec.prefix, device.did);
29805
+ partitionId = `${identityKeyHash}:${replicaHash}`;
29806
+ const replicaDir = (0, import_node_path.join)(idDir, "replicas", replicaHash);
29807
+ const store = await sqlite.SqliteReplicaStore.open(replicaDir, {
29808
+ create: true,
29809
+ guard: storeGuard
29810
+ });
29811
+ let generation;
29812
+ try {
29813
+ try {
29814
+ await store.init({
29815
+ name: spec.prefix.replace(/\/+$/, "") || "replica",
29816
+ replicaId: replicaHash,
29817
+ host: spec.identity.host,
29818
+ space: spec.space,
29819
+ prefix: spec.prefix,
29820
+ deviceDid: device.did,
29821
+ allowSecrets: spec.allowSecrets,
29822
+ localReadPolicy: "whileGrantValid",
29823
+ retentionGrantCid: null
29824
+ });
29825
+ } catch (error) {
29826
+ if (!replica.isReplicaError(error, replica.ReplicaErrorCode.CONFIG_MISMATCH)) {
29827
+ throw error;
29828
+ }
29829
+ }
29830
+ const state = await store.open();
29831
+ if (state === null) {
29832
+ throw new replica.ReplicaError(
29833
+ replica.ReplicaErrorCode.NOT_FOUND,
29834
+ "The replica row vanished during init."
29835
+ );
29836
+ }
29837
+ const config = state.config;
29838
+ if (config.host !== spec.identity.host || config.space !== spec.space || config.prefix !== spec.prefix) {
29839
+ throw new replica.ReplicaError(
29840
+ replica.ReplicaErrorCode.CONFIG_MISMATCH,
29841
+ `This replica follows ${config.host} ${config.space}/kv/${config.prefix}, not ${spec.identity.host} ${spec.space}/kv/${spec.prefix}.`
29842
+ );
29843
+ }
29844
+ if (principalOf(config.deviceDid) !== principalOf(device.did)) {
29845
+ throw new replica.ReplicaError(
29846
+ replica.ReplicaErrorCode.CONFIG_MISMATCH,
29847
+ "The stored replica belongs to a different device key; purge the partition to rekey."
29848
+ );
29849
+ }
29850
+ await retainGrantInstalls(gate, snapshot, identityKeyHash, store);
29851
+ if (state.grant !== null || state.pendingGrant !== null) {
29852
+ const policy = await resolvePolicy(Date.now(), false);
29853
+ if (policy === null) throw gated("gate_unavailable");
29854
+ if (state.grant !== null) await store.recomputeGrantInstall(state.grant, policy);
29855
+ if (state.pendingGrant !== null) await store.recomputeGrantInstall(state.pendingGrant, policy);
29856
+ }
29857
+ generation = state.generation;
29858
+ } catch (error) {
29859
+ await store.close().catch(() => void 0);
29860
+ throw error;
29861
+ }
29862
+ let ino;
29863
+ try {
29864
+ ino = (await (0, import_promises.stat)((0, import_node_path.join)(replicaDir, "replica.db"))).ino;
29865
+ } catch (error) {
29866
+ if (!isNotFound(error)) throw error;
29867
+ }
29868
+ return new SqliteReplicaHandle({
29869
+ spec,
29870
+ replicaId: replicaHash,
29871
+ device,
29872
+ store,
29873
+ replicaDir,
29874
+ replica,
29875
+ guard: storeGuard,
29876
+ read: (section, withFence) => readGated(spec, partitionId, store, replicaDir, generation, section, withFence),
29877
+ policy: resolvePolicy,
29878
+ recordGrantInstall: async (cid, at) => gate.recordGrantInstall(profileDir, await gate.grantInstallHashOf(snapshot.salt, identityKeyHash, cid), at),
29879
+ transportFor: options.transportFor,
29880
+ dirSync: options.dirSync,
29881
+ generation,
29882
+ ino
29883
+ });
27741
29884
  });
27742
29885
  }
27743
29886
  async function inspectStatus(spec, { signal }) {
29887
+ assertLogoutClear(options.logoutPendingFile);
27744
29888
  const checkAbort = () => {
27745
29889
  if (signal.aborted) throw signal.reason ?? Object.assign(new Error("Replica status inspection was aborted"), { code: "ABORTED" });
27746
29890
  };
@@ -27773,6 +29917,8 @@ function createSqliteReplicaStorage(options) {
27773
29917
  import("@tinycloud/replica/sqlite")
27774
29918
  ]);
27775
29919
  checkAbort();
29920
+ const syncedThroughEpoch = await readFence(replicaDir);
29921
+ checkAbort();
27776
29922
  const inspected = await sqlite.SqliteReplicaStore.inspect(replicaDir, { signal });
27777
29923
  checkAbort();
27778
29924
  if (inspected === null) return void 0;
@@ -27780,14 +29926,17 @@ function createSqliteReplicaStorage(options) {
27780
29926
  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
29927
  throw new ReplicaStorageError("STORAGE_ERROR", `The replica at ${replicaDir} does not match the requested identity, space, prefix, or device.`);
27782
29928
  }
27783
- const syncedThroughEpoch = await readFence(replicaDir);
27784
29929
  checkAbort();
29930
+ assertLogoutClear(options.logoutPendingFile);
29931
+ const gateState = await (await fileGate()).readGateSnapshot(profileDir);
29932
+ const purgePending = gateState !== void 0 && (gateState.eraseDeferred || gateState.intents.length > 0);
27785
29933
  return {
29934
+ purgePending,
27786
29935
  coverage: status.coverage,
27787
29936
  lastSyncAt: status.lastSyncAt,
27788
29937
  syncedThroughEpoch,
27789
- authority: { state: status.authority.state, expiresAt: status.authority.expiresAt },
27790
- grant: newestGrantInfo(replica, state.pendingGrant ?? state.grant, state.pendingGrant, spec),
29938
+ 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 },
29939
+ grant: newestGrantInfo(replica, state.pendingGrant ?? state.grant, state.pendingGrant, spec, inspected.grantInstall),
27791
29940
  counts: status.counts,
27792
29941
  bytes: status.bytes,
27793
29942
  lastError: status.lastError
@@ -27800,89 +29949,124 @@ function createSqliteReplicaStorage(options) {
27800
29949
  // @ts-ignore — same as above.
27801
29950
  import("@tinycloud/replica/sqlite")
27802
29951
  ]);
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;
29952
+ return profileSection(async () => {
29953
+ const idDir = partitionDirOf(root, target.identity);
29954
+ if (!await realDirectoryExists(idDir)) return;
29955
+ const hasReplicas = await realDirectoryExists((0, import_node_path.join)(idDir, "replicas"));
29956
+ const deviceDids = [];
29957
+ const deviceFile = (0, import_node_path.join)(idDir, "device.jwk");
29958
+ const stored = await readJsonFile(deviceFile);
29959
+ if (stored !== void 0) {
29960
+ const device = deviceRecordOf(stored);
29961
+ if (device === void 0) {
29962
+ throw new ReplicaStorageError(
29963
+ "STORAGE_ERROR",
29964
+ `The replica device key at ${deviceFile} is unreadable.`
29965
+ );
27834
29966
  }
29967
+ deviceDids.push(device.did);
29968
+ }
29969
+ if (target.sessionDeviceDid !== void 0) deviceDids.push(target.sessionDeviceDid);
29970
+ const devices = [...new Map(deviceDids.map((did) => [principalOf(did), did])).values()];
29971
+ const removeLeftover = async (replicaDir) => {
29972
+ const layout = await sqlite.inspectSqliteReplicaLayout(replicaDir, { allowAdapterFence: true });
29973
+ const missingDatabase = !await pathExists((0, import_node_path.join)(replicaDir, "replica.db"));
29974
+ const unexpected = layout.unknownPaths.filter((path) => path !== "replica.db");
29975
+ if (unexpected.length !== 0 || layout.unknownPaths.includes("replica.db") !== missingDatabase || layout.unknownEntries.some((item) => item.path === "replica.db")) {
29976
+ throw new ReplicaStorageError(
29977
+ "STORAGE_ERROR",
29978
+ `Refusing to remove unexpected replica path ${(0, import_node_path.join)(replicaDir, unexpected[0] ?? "replica.db")}.`
29979
+ );
29980
+ }
29981
+ await sqlite.removeSqliteReplicaLayout(replicaDir, {
29982
+ expectedRoot: layout.rootIdentity,
29983
+ expectedKnown: layout.knownEntries,
29984
+ allowAdapterFence: true,
29985
+ allowMissingDatabase: missingDatabase
29986
+ });
29987
+ };
29988
+ let firstError;
29989
+ for (const deviceDid of devices) {
29990
+ const replicaDir = (0, import_node_path.join)(idDir, "replicas", replicaHashOf(target.prefix, deviceDid));
29991
+ if (!hasReplicas || !await realDirectoryExists(replicaDir)) continue;
27835
29992
  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
- );
29993
+ let store;
29994
+ try {
29995
+ store = await sqlite.SqliteReplicaStore.open(replicaDir, { create: false, guard: profileSection });
29996
+ } catch (error) {
29997
+ if (replica.isReplicaError(error, replica.ReplicaErrorCode.NOT_FOUND)) {
29998
+ await removeLeftover(replicaDir);
29999
+ continue;
30000
+ }
30001
+ throw error;
27848
30002
  }
27849
30003
  try {
27850
- if (state.revoked === null) {
27851
- await store.reset(lease, "purged");
30004
+ const state = await store.open();
30005
+ if (state === null) {
30006
+ await store.close().catch(() => void 0);
30007
+ await removeLeftover(replicaDir);
30008
+ continue;
30009
+ }
30010
+ const lease = await store.acquireSyncLease(SYNC_LEASE_TTL_MS);
30011
+ if (lease === null) {
30012
+ throw new replica.ReplicaError(
30013
+ replica.ReplicaErrorCode.BUSY,
30014
+ "Another process holds this replica's sync lease."
30015
+ );
30016
+ }
30017
+ try {
30018
+ const gate = await fileGate();
30019
+ const currentGate = await gate.readGateSnapshot(profileDir);
30020
+ if (currentGate !== void 0 && currentGate.principalHash !== null && currentGate.principalHash === await gate.principalHashOf(currentGate.salt, target.identity.principal)) {
30021
+ const identityKeyHash = await gate.identityHashOf(currentGate.salt, (0, import_sdk_services3.replicationIdentityKey)(target.identity));
30022
+ await retainGrantInstalls(gate, currentGate, identityKeyHash, store);
30023
+ }
30024
+ if (state.revoked === null) {
30025
+ await store.reset(lease, "purged");
30026
+ }
30027
+ await store.destroy(lease, { allowAdapterFence: true });
30028
+ } finally {
30029
+ await store.releaseLease(lease).catch(() => void 0);
27852
30030
  }
27853
- await store.destroy(lease);
27854
30031
  } finally {
27855
- await store.releaseLease(lease).catch(() => void 0);
30032
+ await store.close().catch(() => void 0);
27856
30033
  }
27857
- } finally {
27858
- await store.close().catch(() => void 0);
30034
+ } catch (error) {
30035
+ firstError ?? (firstError = error);
27859
30036
  }
27860
- } catch (error) {
27861
- firstError ?? (firstError = error);
27862
30037
  }
27863
- }
27864
- if (firstError !== void 0) throw firstError;
30038
+ if (firstError !== void 0) throw firstError;
30039
+ });
27865
30040
  }
27866
30041
  return {
27867
30042
  kind: "sqlite",
30043
+ lifecycle,
27868
30044
  open: openHandle,
27869
30045
  inspectStatus,
27870
30046
  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) {
30047
+ // File-backed pending writes are always bound to a stamped gate.
30048
+ pendingWrites(identity, opts) {
27877
30049
  return new FilePendingWriteStore(
27878
30050
  identity,
27879
30051
  partitionDirOf(root, identity),
30052
+ profileDir,
27880
30053
  guard,
27881
- options.dirSync
30054
+ opts?.stamp,
30055
+ options.dirSync,
30056
+ options.logoutPendingFile
27882
30057
  );
27883
30058
  }
27884
30059
  };
27885
30060
  }
30061
+ function createProfileReplicaPurge(options) {
30062
+ return createSqliteReplicaStorage({
30063
+ dir: (0, import_node_path.join)(options.profileDir, "replication"),
30064
+ guard: options.guard,
30065
+ dirSync: options.dirSync,
30066
+ logoutPendingFile: (0, import_node_path.join)(options.profileDir, REPLICA_LOGOUT_PENDING_FILE),
30067
+ diagnosticFiles: options.diagnosticFiles
30068
+ }).lifecycle;
30069
+ }
27886
30070
  function sqliteReplicaStorage(options) {
27887
30071
  return createSqliteReplicaStorage(options);
27888
30072
  }
@@ -27936,6 +30120,8 @@ function sqliteReplicaStorage(options) {
27936
30120
  PrefixedKVService,
27937
30121
  PrivateKeySigner,
27938
30122
  ProtocolMismatchError,
30123
+ REPLICA_LOGOUT_PENDING_FILE,
30124
+ RESTORED_GATE_MESSAGE,
27939
30125
  RUNTIME_DELEGATION_REJECTIONS,
27940
30126
  RuntimeDelegationRejectedError,
27941
30127
  SECRET_NAME_RE,
@@ -27968,6 +30154,7 @@ function sqliteReplicaStorage(options) {
27968
30154
  activateValidatedRuntimeDelegation,
27969
30155
  addressStorageKey,
27970
30156
  appSecretScope,
30157
+ assertProfileLogoutClear,
27971
30158
  buildCanonicalDecryptRequest,
27972
30159
  buildDecryptAttenuation,
27973
30160
  buildDecryptFacts,
@@ -27988,6 +30175,7 @@ function sqliteReplicaStorage(options) {
27988
30175
  composeManifestRequest,
27989
30176
  createCapabilityKeyRegistry,
27990
30177
  createOpenKeyCallbackSigningStrategy,
30178
+ createProfileReplicaPurge,
27991
30179
  createSharingService,
27992
30180
  createSpaceService,
27993
30181
  createVaultCrypto,
@@ -28024,6 +30212,7 @@ function sqliteReplicaStorage(options) {
28024
30212
  isNetworkId,
28025
30213
  isStorageFullError,
28026
30214
  loadManifest,
30215
+ logoutPrincipalDigest,
28027
30216
  makePkhSpaceId,
28028
30217
  makePublicSpaceId,
28029
30218
  networkDiscoveryKey,