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