@tinycloud/replica 0.2.4-beta.3 → 0.3.0-beta.4

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.
package/dist/sqlite.js CHANGED
@@ -1,8 +1,9 @@
1
1
  // src/sqlite/store.ts
2
+ import { AsyncLocalStorage } from "async_hooks";
2
3
  import { randomBytes } from "crypto";
3
- import { closeSync, constants as fsConstants, fstatSync, lstatSync, openSync, readdirSync, readlinkSync } from "fs";
4
- import { chmod, link, lstat, mkdir, open, readFile, readdir, rm, rmdir, stat, unlink } from "fs/promises";
5
- import { dirname, join, resolve } from "path";
4
+ import { closeSync, constants as fsConstants2, fstatSync, lstatSync, openSync, readdirSync, readlinkSync } from "fs";
5
+ import { chmod, link, lstat as lstat2, mkdir, open as open2, readFile, readdir as readdir2, rm, rmdir as rmdir2, stat, unlink as unlink2 } from "fs/promises";
6
+ import { dirname, join as join2, resolve as resolve2 } from "path";
6
7
 
7
8
  // src/errors.ts
8
9
  var ReplicaErrorCode = {
@@ -60,7 +61,13 @@ var ReplicaErrorCode = {
60
61
  /** A stored blob no longer hashes to its name. */
61
62
  INTEGRITY_ERROR: "INTEGRITY_ERROR",
62
63
  /** The replica handle was closed; in-flight calls settle with this code. */
63
- CLOSED: "REPLICA_CLOSED"
64
+ CLOSED: "REPLICA_CLOSED",
65
+ /** The lifecycle gate refused the operation (shared contract `REPLICA_GATED`); `detail.gate` says why. */
66
+ GATED: "REPLICA_GATED",
67
+ /** Quota admission refused a page (`maxBytes` or the free-space floor); nothing committed. */
68
+ STORAGE_BUDGET: "STORAGE_BUDGET",
69
+ /** The consent deadline passed: the grant is not served until the user consents again. */
70
+ CONSENT_EXPIRED: "CONSENT_EXPIRED"
64
71
  };
65
72
  var REPLICA_ERROR = /* @__PURE__ */ Symbol.for("tinycloud.replica.error");
66
73
  var ReplicaError = class extends Error {
@@ -197,32 +204,13 @@ async function loadSqlite() {
197
204
  return (path, options) => wrap(options?.readonly ? new DatabaseSync(path, { readOnly: true }) : new DatabaseSync(path));
198
205
  }
199
206
 
200
- // src/sqlite/store.ts
201
- var SCHEMA_VERSION = 1;
202
- var sqliteOpenQueue = Promise.resolve();
203
- async function acquireSqliteOpen() {
204
- const previous = sqliteOpenQueue;
205
- let release;
206
- sqliteOpenQueue = new Promise((resolve2) => {
207
- release = resolve2;
208
- });
209
- await previous;
210
- return release;
211
- }
212
- async function captureNamedPath(path) {
213
- const names = [];
214
- for (let current = resolve(path); ; current = dirname(current)) {
215
- names.push(current);
216
- if (dirname(current) === current) break;
217
- }
218
- const identities = [];
219
- for (const name of names.reverse()) {
220
- const stats = await lstat(name, { bigint: true });
221
- identities.push({ path: name, dev: stats.dev, ino: stats.ino });
222
- }
223
- return identities;
224
- }
225
- var DURABILITY = "sqlite-wal-synchronous-full+fsynced-blobs";
207
+ // src/sqlite/layout.ts
208
+ import { constants as fsConstants } from "fs";
209
+ import { lstat, open, readdir, rmdir, unlink } from "fs/promises";
210
+ import { join, parse, resolve, sep } from "path";
211
+
212
+ // src/sqlite/schema.ts
213
+ var SCHEMA_VERSION = 2;
226
214
  var SCHEMA = `
227
215
  CREATE TABLE IF NOT EXISTS replica (
228
216
  id INTEGER PRIMARY KEY CHECK (id = 1),
@@ -282,7 +270,403 @@ CREATE TABLE IF NOT EXISTS entry (
282
270
  )
283
271
  ) STRICT, WITHOUT ROWID;
284
272
  CREATE INDEX IF NOT EXISTS entry_hash ON entry (hash) WHERE hash IS NOT NULL;
273
+ CREATE TABLE IF NOT EXISTS grant_install (
274
+ cid TEXT PRIMARY KEY,
275
+ installed_at INTEGER NOT NULL,
276
+ effective_expires_at INTEGER NOT NULL,
277
+ consent_deadline INTEGER NOT NULL,
278
+ consent_anchor_at INTEGER NOT NULL,
279
+ consent_anchor_source TEXT NOT NULL
280
+ ) STRICT, WITHOUT ROWID;
285
281
  `;
282
+
283
+ // src/sqlite/layout.ts
284
+ var POLICY_COLUMNS = [
285
+ "grant_installed_at REAL",
286
+ "grant_effective_expires_at REAL",
287
+ "grant_consent_deadline REAL",
288
+ "grant_consent_anchor_at REAL",
289
+ "grant_consent_anchor_source TEXT",
290
+ "pending_grant_installed_at REAL",
291
+ "pending_grant_effective_expires_at REAL",
292
+ "pending_grant_consent_deadline REAL",
293
+ "pending_grant_consent_anchor_at REAL",
294
+ "pending_grant_consent_anchor_source TEXT"
295
+ ];
296
+ var normalizeSchema = (sql) => sql.replace(" IF NOT EXISTS", "").replace(/\s+/g, " ").trim();
297
+ var SOURCE_SCHEMA = SCHEMA.trim().split(";").map((sql) => sql.trim()).filter(Boolean).map((sql) => {
298
+ const match = /^CREATE (TABLE|INDEX) IF NOT EXISTS ([a-z_]+)/.exec(sql);
299
+ if (match === null) throw new Error("Invalid source replica schema.");
300
+ return { type: match[1].toLowerCase(), name: match[2], sql: normalizeSchema(sql) };
301
+ });
302
+ var V1_SCHEMA = SOURCE_SCHEMA.filter((entry) => entry.name !== "grant_install");
303
+ var POLICY_REPLICA = V1_SCHEMA.find((entry) => entry.name === "replica").sql.replace(/\) STRICT$/, `, ${POLICY_COLUMNS.join(", ")}) STRICT`);
304
+ var POLICY_SCHEMA = V1_SCHEMA.map((entry) => entry.name === "replica" ? { ...entry, sql: POLICY_REPLICA } : entry);
305
+ async function sourceMirrorSchema(path) {
306
+ let db;
307
+ try {
308
+ db = (await loadSqlite())(path, { readonly: true });
309
+ db.exec("BEGIN");
310
+ const version = db.get("PRAGMA user_version")?.user_version;
311
+ if (version !== 1 && version !== SCHEMA_VERSION) return false;
312
+ const rows = db.all(
313
+ "SELECT type, name, sql FROM sqlite_schema WHERE sql IS NOT NULL ORDER BY name"
314
+ );
315
+ const expected = version === 1 ? V1_SCHEMA : SOURCE_SCHEMA;
316
+ const matches = (schema) => rows.length === schema.length && rows.every((row) => {
317
+ const source = schema.find((entry) => entry.name === row.name);
318
+ return source?.type === row.type && source.sql === normalizeSchema(row.sql);
319
+ });
320
+ if (matches(expected)) return true;
321
+ if (version !== 2) return false;
322
+ return matches(POLICY_SCHEMA);
323
+ } catch {
324
+ return false;
325
+ } finally {
326
+ try {
327
+ db?.close();
328
+ } catch {
329
+ }
330
+ }
331
+ }
332
+ var SQLITE_FILE = /^(?:replica\.db|replica\.db-(?:wal|shm|journal))$/;
333
+ var SHARD = /^[0-9a-f]{2}$/;
334
+ var HASH = /^[0-9a-f]{64}$/;
335
+ var TEMP = /^[0-9a-f]{64}\.[0-9a-f]{12}$/;
336
+ function sourceKind(parent, name, allowAdapterFence) {
337
+ if (parent === "") {
338
+ if (SQLITE_FILE.test(name) || allowAdapterFence && (name === "fence.json" || /^fence[.]json[.][0-9a-f]{12}[.]tmp$/.test(name))) return "file";
339
+ return name === "blobs" ? "directory" : "unknown";
340
+ }
341
+ if (parent === "blobs") return name === ".tmp" || SHARD.test(name) ? "directory" : "unknown";
342
+ if (parent === "blobs/.tmp") return TEMP.test(name) ? "file" : "unknown";
343
+ if (parent.startsWith("blobs/") && SHARD.test(parent.slice(6))) {
344
+ return HASH.test(name) && name.startsWith(parent.slice(6)) ? "file" : "unknown";
345
+ }
346
+ return "unknown";
347
+ }
348
+ function sameFile(a, b) {
349
+ return a.dev === b.dev && a.ino === b.ino;
350
+ }
351
+ function identityOf(path, kind, details) {
352
+ return { path, kind, dev: details.dev.toString(), ino: details.ino.toString() };
353
+ }
354
+ function fileKind(details) {
355
+ if (details.isFile()) return "file";
356
+ if (details.isDirectory()) return "directory";
357
+ if (details.isSymbolicLink()) return "symlink";
358
+ throw new Error("Unsupported replica entry type.");
359
+ }
360
+ async function pathComponents(path) {
361
+ const absolute = resolve(path);
362
+ const parts = absolute.slice(parse(absolute).root.length).split(sep).filter(Boolean);
363
+ let current = parse(absolute).root;
364
+ const components = [];
365
+ for (const part of ["", ...parts]) {
366
+ if (part) current = join(current, part);
367
+ const details = await lstat(current, { bigint: true });
368
+ if (details.isSymbolicLink()) throw new Error(`Symlink in replica path: ${current}`);
369
+ if (!details.isDirectory()) throw new Error(`Replica path component is not a directory: ${current}`);
370
+ components.push({ path: current, identity: details });
371
+ }
372
+ return components;
373
+ }
374
+ async function pinnedDirectory(path, parent, relative = "") {
375
+ const components = process.platform === "linux" ? void 0 : parent?.components ?? await pathComponents(path);
376
+ const before = await lstat(path, { bigint: true });
377
+ if (before.isSymbolicLink()) throw new Error(`Symlink in replica path: ${path}`);
378
+ if (!before.isDirectory()) throw new Error("Replica directory is not a real directory.");
379
+ const handle = await open(path, fsConstants.O_RDONLY | fsConstants.O_DIRECTORY | fsConstants.O_NOFOLLOW);
380
+ try {
381
+ if (!sameFile(before, await handle.stat({ bigint: true }))) {
382
+ throw new Error("Replica directory changed during inspection.");
383
+ }
384
+ return {
385
+ handle,
386
+ identity: before,
387
+ parent,
388
+ components,
389
+ children: [],
390
+ relative,
391
+ name: path,
392
+ path: process.platform === "linux" ? `/proc/self/fd/${handle.fd}` : path
393
+ };
394
+ } catch (error) {
395
+ await handle.close();
396
+ throw error;
397
+ }
398
+ }
399
+ async function checkDirectory(directory) {
400
+ const details = await lstat(directory.name, { bigint: true });
401
+ if (!details.isDirectory() || !sameFile(directory.identity, details)) {
402
+ throw new Error(`Replica directory changed: ${directory.relative || directory.name}`);
403
+ }
404
+ if (directory.parent) await checkDirectory(directory.parent);
405
+ else if (directory.components) {
406
+ for (const component of directory.components) {
407
+ const current = await lstat(component.path, { bigint: true });
408
+ if (current.isSymbolicLink() || !current.isDirectory() || !sameFile(component.identity, current)) {
409
+ throw new Error(`Replica path component changed: ${component.path}`);
410
+ }
411
+ }
412
+ }
413
+ }
414
+ async function inspect(dir, allowAdapterFence) {
415
+ const root = await pinnedDirectory(dir);
416
+ const directories = [root];
417
+ const unknownPaths = [];
418
+ const knownEntries = [];
419
+ const unknownEntries = [];
420
+ let hasDatabase = false;
421
+ try {
422
+ async function walk(parent) {
423
+ await checkDirectory(parent);
424
+ for (const name of (await readdir(parent.path)).sort()) {
425
+ await checkDirectory(parent);
426
+ const relative = parent.relative ? `${parent.relative}/${name}` : name;
427
+ const path = join(parent.path, name);
428
+ const details = await lstat(path, { bigint: true });
429
+ if (process.platform !== "linux" && details.isSymbolicLink()) {
430
+ throw new Error(`Symlink in replica path: ${relative}`);
431
+ }
432
+ const kind = fileKind(details);
433
+ const child = { path, relative, kind, identity: details, parent };
434
+ parent.children.push(child);
435
+ if (parent.relative === "" && name === "replica.db" && kind === "file") hasDatabase = true;
436
+ if (sourceKind(parent.relative, name, allowAdapterFence) !== kind) {
437
+ unknownPaths.push(relative);
438
+ unknownEntries.push(identityOf(relative, kind, details));
439
+ } else {
440
+ knownEntries.push(identityOf(relative, kind, details));
441
+ }
442
+ if (kind === "directory") {
443
+ const directory = await pinnedDirectory(path, parent, relative);
444
+ if (!sameFile(details, directory.identity)) {
445
+ await directory.handle.close();
446
+ throw new Error(`Replica directory changed during inspection: ${relative}`);
447
+ }
448
+ child.directory = directory;
449
+ directories.push(directory);
450
+ await walk(directory);
451
+ }
452
+ }
453
+ }
454
+ await walk(root);
455
+ if (!hasDatabase) {
456
+ unknownPaths.push("replica.db");
457
+ } else if (!await sourceMirrorSchema(join(root.path, "replica.db"))) {
458
+ const index = knownEntries.findIndex((entry) => entry.path === "replica.db");
459
+ if (index >= 0) unknownEntries.push(knownEntries.splice(index, 1)[0]);
460
+ unknownPaths.push("replica.db");
461
+ }
462
+ await checkDirectory(root);
463
+ const names = new Set(await readdir(root.path));
464
+ for (let index = root.children.length - 1; index >= 0; index--) {
465
+ const child = root.children[index];
466
+ if (!names.has(child.relative)) {
467
+ if (child.directory) throw new Error(`Replica directory vanished: ${child.relative}`);
468
+ root.children.splice(index, 1);
469
+ for (const list of [knownEntries, unknownEntries]) {
470
+ const entryIndex = list.findIndex((entry) => entry.path === child.relative);
471
+ if (entryIndex >= 0) list.splice(entryIndex, 1);
472
+ }
473
+ const pathIndex = unknownPaths.indexOf(child.relative);
474
+ if (pathIndex >= 0) unknownPaths.splice(pathIndex, 1);
475
+ continue;
476
+ }
477
+ const current = await lstat(child.path, { bigint: true });
478
+ if (!sameFile(child.identity, current) || fileKind(current) !== child.kind) {
479
+ throw new Error(`Replica child changed during schema inspection: ${child.relative}`);
480
+ }
481
+ }
482
+ for (const name of names) {
483
+ if (root.children.some((child) => child.relative === name)) continue;
484
+ const path = join(root.path, name);
485
+ const details = await lstat(path, { bigint: true });
486
+ if (!/^replica\.db-(?:wal|shm|journal)$/.test(name) || !details.isFile()) {
487
+ throw new Error(`Unexpected replica child during schema inspection: ${name}`);
488
+ }
489
+ root.children.push({ path, relative: name, kind: "file", identity: details, parent: root });
490
+ knownEntries.push(identityOf(name, "file", details));
491
+ }
492
+ return { root, directories, knownEntries, unknownPaths, unknownEntries };
493
+ } catch (error) {
494
+ await closeDirectories(directories);
495
+ throw error;
496
+ }
497
+ }
498
+ async function closeDirectories(directories) {
499
+ for (const directory of directories.reverse()) await directory.handle.close();
500
+ }
501
+ async function inspectSqliteReplicaLayout(dir, options = {}) {
502
+ const inventory = await inspect(dir, options.allowAdapterFence === true);
503
+ try {
504
+ return {
505
+ knownEntries: inventory.knownEntries,
506
+ unknownPaths: inventory.unknownPaths,
507
+ unknownEntries: inventory.unknownEntries,
508
+ rootIdentity: { dev: inventory.root.identity.dev.toString(), ino: inventory.root.identity.ino.toString() }
509
+ };
510
+ } finally {
511
+ await closeDirectories(inventory.directories);
512
+ }
513
+ }
514
+ function approvedEntries(entries) {
515
+ const approved = /* @__PURE__ */ new Map();
516
+ for (const entry of entries) {
517
+ if (typeof entry !== "object" || entry === null || Object.keys(entry).sort().join(",") !== "dev,ino,kind,path" || typeof entry.path !== "string" || !entry.path || entry.path.split("/").some((part) => !part || part === "." || part === ".." || part.includes("\\")) || !["file", "directory", "symlink"].includes(entry.kind) || typeof entry.dev !== "string" || !/^(0|[1-9][0-9]*)$/.test(entry.dev) || typeof entry.ino !== "string" || !/^(0|[1-9][0-9]*)$/.test(entry.ino) || approved.has(entry.path)) {
518
+ throw new Error("Invalid or duplicate replica approval.");
519
+ }
520
+ approved.set(entry.path, entry);
521
+ }
522
+ return approved;
523
+ }
524
+ var LAYOUT_FAULTS = /* @__PURE__ */ Symbol("tinycloud.replica.layout-faults");
525
+ async function removeSqliteReplicaLayout(dir, options = {}) {
526
+ const inventory = await inspect(dir, options.allowAdapterFence === true);
527
+ try {
528
+ if (options.expectedRoot && (options.expectedRoot.dev !== inventory.root.identity.dev.toString() || options.expectedRoot.ino !== inventory.root.identity.ino.toString())) {
529
+ throw new Error("Replica root identity changed since approval.");
530
+ }
531
+ const approved = approvedEntries(options.approvedUnknown ?? []);
532
+ for (const entry of inventory.unknownEntries) {
533
+ const match = approved.get(entry.path);
534
+ if (!match || match.kind !== entry.kind || match.dev !== entry.dev || match.ino !== entry.ino) {
535
+ throw new Error(`Unknown replica child or approval identity mismatch: ${entry.path}`);
536
+ }
537
+ }
538
+ if (options.expectedKnown) {
539
+ const expected = approvedEntries(options.expectedKnown);
540
+ for (const entry of inventory.knownEntries) {
541
+ const match = expected.get(entry.path);
542
+ if (!match || match.kind !== entry.kind || match.dev !== entry.dev || match.ino !== entry.ino) {
543
+ throw new Error(`Known replica child changed since preflight: ${entry.path}`);
544
+ }
545
+ }
546
+ }
547
+ if (options.allowMissingDatabase && inventory.knownEntries.some((entry) => entry.path === "replica.db")) {
548
+ throw new Error("Replica database appeared after approval of its absence.");
549
+ }
550
+ if (inventory.unknownPaths.length !== inventory.unknownEntries.length && inventory.root.children.length > 0 && !options.allowMissingDatabase) {
551
+ throw new Error("Replica database is missing; cannot approve removal.");
552
+ }
553
+ async function removeDirectory(directory) {
554
+ for (const child of directory.children.sort((a, b) => a.relative === "replica.db" ? 1 : b.relative === "replica.db" ? -1 : 0)) {
555
+ if (child.directory) {
556
+ await removeDirectory(child.directory);
557
+ }
558
+ await options[LAYOUT_FAULTS]?.beforeUnlink?.(child.relative);
559
+ await checkDirectory(directory);
560
+ const details = await lstat(child.path, { bigint: true });
561
+ if (process.platform !== "linux" && details.isSymbolicLink()) {
562
+ throw new Error(`Symlink in replica path: ${child.relative}`);
563
+ }
564
+ if (fileKind(details) !== child.kind || !sameFile(child.identity, details)) {
565
+ throw new Error(`Replica child identity changed: ${child.relative}`);
566
+ }
567
+ if (child.directory) await rmdir(child.path);
568
+ else await unlink(child.path);
569
+ }
570
+ }
571
+ await removeDirectory(inventory.root);
572
+ await checkDirectory(inventory.root);
573
+ await rmdir(inventory.root.name);
574
+ } finally {
575
+ await closeDirectories(inventory.directories);
576
+ }
577
+ }
578
+
579
+ // src/sqlite/store.ts
580
+ var sqliteOpenQueue = Promise.resolve();
581
+ async function acquireSqliteOpen() {
582
+ const previous = sqliteOpenQueue;
583
+ let release;
584
+ sqliteOpenQueue = new Promise((resolve3) => {
585
+ release = resolve3;
586
+ });
587
+ await previous;
588
+ return release;
589
+ }
590
+ async function captureNamedPath(path) {
591
+ const names = [];
592
+ for (let current = resolve2(path); ; current = dirname(current)) {
593
+ names.push(current);
594
+ if (dirname(current) === current) break;
595
+ }
596
+ const identities = [];
597
+ for (const name of names.reverse()) {
598
+ const stats = await lstat2(name, { bigint: true });
599
+ identities.push({ path: name, dev: stats.dev, ino: stats.ino });
600
+ }
601
+ return identities;
602
+ }
603
+ async function openVerifiedDatabase(path, opener, options = {}) {
604
+ const leaf = await open2(path, fsConstants2.O_RDONLY | fsConstants2.O_NOFOLLOW);
605
+ let db;
606
+ try {
607
+ const identity = await leaf.stat();
608
+ const named = await lstat2(path);
609
+ if (!identity.isFile() || !named.isFile() || named.dev !== identity.dev || named.ino !== identity.ino || options.expected !== void 0 && (options.expected.dev !== identity.dev || options.expected.ino !== identity.ino)) {
610
+ throw new Error("The replica database changed before SQLite opened it.");
611
+ }
612
+ const previousFds = process.platform === "linux" && options.discoverDescriptors !== false ? new Map(readdirSync("/proc/self/fd").map((fd) => {
613
+ try {
614
+ return [fd, readlinkSync(`/proc/self/fd/${fd}`)];
615
+ } catch {
616
+ return [fd, ""];
617
+ }
618
+ })) : null;
619
+ db = options.readonly ? opener(path, { readonly: true }) : opener(path);
620
+ const afterOpen = lstatSync(path);
621
+ if (!afterOpen.isFile() || afterOpen.dev !== identity.dev || afterOpen.ino !== identity.ino) {
622
+ throw new Error("The replica database changed while SQLite opened it.");
623
+ }
624
+ if (previousFds) db.get("SELECT count(*) FROM sqlite_master");
625
+ if (previousFds) {
626
+ const databases = readdirSync("/proc/self/fd").filter((fd) => {
627
+ try {
628
+ const target = readlinkSync(`/proc/self/fd/${fd}`);
629
+ return target.endsWith("/replica.db") && previousFds.get(fd) !== target;
630
+ } catch {
631
+ return false;
632
+ }
633
+ });
634
+ const sharedPager = options.readonly && databases.length === 0 && [...previousFds].some(([fd, target]) => {
635
+ if (Number(fd) === leaf.fd || !target.endsWith("/replica.db")) return false;
636
+ try {
637
+ const actual = fstatSync(Number(fd));
638
+ return actual.dev === identity.dev && actual.ino === identity.ino;
639
+ } catch {
640
+ return false;
641
+ }
642
+ });
643
+ if (!databases.length && !sharedPager || databases.some((fd) => {
644
+ try {
645
+ const actual = fstatSync(Number(fd));
646
+ return actual.dev !== identity.dev || actual.ino !== identity.ino;
647
+ } catch {
648
+ return true;
649
+ }
650
+ })) {
651
+ throw new Error("SQLite opened a different replica database.");
652
+ }
653
+ }
654
+ const after = lstatSync(path);
655
+ if (!after.isFile() || after.dev !== identity.dev || after.ino !== identity.ino) {
656
+ throw new Error("The replica database changed while SQLite opened it.");
657
+ }
658
+ const checked = db;
659
+ db = void 0;
660
+ return { db: checked, identity };
661
+ } finally {
662
+ try {
663
+ db?.close();
664
+ } finally {
665
+ await leaf.close();
666
+ }
667
+ }
668
+ }
669
+ var DURABILITY = "sqlite-wal-synchronous-full+fsynced-blobs";
286
670
  function grantOf(cid, bytes, audience, issuer, nbf, exp) {
287
671
  if (cid === null || bytes === null || audience === null || issuer === null) return null;
288
672
  return { cid, bytes: new Uint8Array(bytes), audience, issuer, notBefore: nbf, expiresAt: exp };
@@ -313,7 +697,7 @@ function errnoOf(error) {
313
697
  }
314
698
  async function entriesOf(path) {
315
699
  try {
316
- return await readdir(path);
700
+ return await readdir2(path);
317
701
  } catch (error) {
318
702
  if (errnoOf(error) === "ENOENT") return [];
319
703
  throw error;
@@ -345,12 +729,13 @@ var sharedStores = new FinalizationRegistry((shared) => {
345
729
  async function acquireWritableMutation(shared) {
346
730
  const previous = shared.mutationTail;
347
731
  let release;
348
- shared.mutationTail = new Promise((resolve2) => {
349
- release = resolve2;
732
+ shared.mutationTail = new Promise((resolve3) => {
733
+ release = resolve3;
350
734
  });
351
735
  await previous;
352
736
  return release;
353
737
  }
738
+ var activeReadSection = new AsyncLocalStorage();
354
739
  var SqliteReplicaStore = class _SqliteReplicaStore {
355
740
  dir;
356
741
  #database;
@@ -364,9 +749,10 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
364
749
  #activeMutations = 0;
365
750
  #drained;
366
751
  #shared;
752
+ #readonlySnapshot;
367
753
  #pinnedDir;
368
754
  #directoryFd;
369
- #holder = `${process.pid}-${randomBytes(6).toString("hex")}`;
755
+ #holder;
370
756
  #now;
371
757
  #faults;
372
758
  #guard;
@@ -374,11 +760,12 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
374
760
  #ino;
375
761
  #dev;
376
762
  #namedAncestors;
377
- constructor(dir, db, identity, options, directoryFd, namedAncestors = [], shared) {
763
+ constructor(dir, db, identity, options, directoryFd, namedAncestors = [], shared, readonlySnapshot = false) {
378
764
  this.dir = dir;
379
765
  this.#pinnedDir = shared?.pinnedDir ?? (directoryFd !== void 0 && process.platform === "linux" ? `/proc/self/fd/${directoryFd}` : dir);
380
766
  this.#directoryFd = directoryFd;
381
767
  this.#shared = shared;
768
+ this.#readonlySnapshot = readonlySnapshot;
382
769
  if (shared) {
383
770
  shared.references++;
384
771
  sharedStores.register(this, shared, this);
@@ -403,10 +790,10 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
403
790
  if (options.signal?.aborted) throw options.signal.reason ?? new ReplicaError(ReplicaErrorCode.CLOSED, "Replica status inspection was cancelled.");
404
791
  };
405
792
  checkAbort();
406
- const dbPath = join(dir, "replica.db");
793
+ const dbPath = join2(dir, "replica.db");
407
794
  let identity;
408
795
  try {
409
- identity = await lstat(dbPath);
796
+ identity = await lstat2(dbPath);
410
797
  if (!identity.isFile()) return null;
411
798
  } catch (error) {
412
799
  if (errnoOf(error) === "ENOENT") return null;
@@ -416,21 +803,30 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
416
803
  const opener = await loadSqlite();
417
804
  checkAbort();
418
805
  let db;
806
+ let releaseOpen;
419
807
  try {
420
- db = opener(dbPath, { readonly: true });
808
+ releaseOpen = await acquireSqliteOpen();
809
+ const checked = await openVerifiedDatabase(dbPath, opener, { readonly: true, expected: identity });
810
+ db = checked.db;
811
+ releaseOpen();
812
+ releaseOpen = void 0;
421
813
  db.exec("PRAGMA busy_timeout = 0");
422
814
  checkAbort();
423
- const store = new _SqliteReplicaStore(dir, db, identity, { create: false, ...options.now ? { now: options.now } : {} });
815
+ const store = new _SqliteReplicaStore(dir, db, checked.identity, { create: false, ...options.now ? { now: options.now } : {} });
424
816
  for (let attempt = 0; attempt < 2; attempt++) {
425
817
  const before = db.get("PRAGMA data_version").data_version;
426
818
  const state = await store.open();
427
819
  checkAbort();
428
820
  if (state === null) return null;
429
821
  const status = await store.status();
430
- await new Promise((resolve2) => setImmediate(resolve2));
822
+ const latest = state.pendingGrant ?? state.grant;
823
+ const version = db.get("PRAGMA user_version")?.user_version ?? 0;
824
+ const grantInstall = latest === null || version < 2 ? null : await store.getGrantInstall(latest.cid);
825
+ const activeGrantInstall = state.grant === null || version < 2 ? null : state.grant === latest ? grantInstall : await store.getGrantInstall(state.grant.cid);
826
+ await new Promise((resolve3) => setImmediate(resolve3));
431
827
  checkAbort();
432
828
  const after = db.get("PRAGMA data_version").data_version;
433
- if (before === after) return { state, status };
829
+ if (before === after) return { state, status, grantInstall, activeGrantInstall };
434
830
  }
435
831
  throw new ReplicaError(ReplicaErrorCode.BUSY, "The replica changed during status inspection.");
436
832
  } catch (error) {
@@ -439,7 +835,15 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
439
835
  }
440
836
  throw storageError(error, "Inspecting the replica");
441
837
  } finally {
442
- db?.close();
838
+ releaseOpen?.();
839
+ if (db) {
840
+ const releaseClose = await acquireSqliteOpen();
841
+ try {
842
+ db.close();
843
+ } finally {
844
+ releaseClose();
845
+ }
846
+ }
443
847
  }
444
848
  }
445
849
  /**
@@ -447,9 +851,9 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
447
851
  * REPLICA_NOT_FOUND and nothing is written to disk.
448
852
  */
449
853
  static async open(dir, options) {
450
- const namedDbPath = join(dir, "replica.db");
854
+ const namedDbPath = join2(dir, "replica.db");
451
855
  if (!options.create) {
452
- const exists = await lstat(namedDbPath).then(
856
+ const exists = await lstat2(namedDbPath).then(
453
857
  (stats) => stats.isFile(),
454
858
  () => false
455
859
  );
@@ -461,7 +865,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
461
865
  try {
462
866
  if (options.create) await mkdir(dir, { recursive: true, mode: 448 });
463
867
  if (process.platform === "linux") {
464
- directoryFd = openSync(dir, fsConstants.O_RDONLY | fsConstants.O_DIRECTORY | fsConstants.O_NOFOLLOW);
868
+ directoryFd = openSync(dir, fsConstants2.O_RDONLY | fsConstants2.O_DIRECTORY | fsConstants2.O_NOFOLLOW);
465
869
  }
466
870
  const opened = directoryFd === void 0 ? lstatSync(dir, { bigint: true }) : fstatSync(directoryFd, { bigint: true });
467
871
  if (!opened.isDirectory() || opened.isSymbolicLink() || options.expectedDirectory && (opened.dev !== BigInt(options.expectedDirectory.dev) || opened.ino !== BigInt(options.expectedDirectory.ino))) {
@@ -472,11 +876,11 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
472
876
  throw new Error("The replica directory changed while checking its ancestry.");
473
877
  }
474
878
  const pinnedDir = directoryFd === void 0 ? dir : `/proc/self/fd/${directoryFd}`;
475
- const dbPath = join(pinnedDir, "replica.db");
879
+ const dbPath = join2(pinnedDir, "replica.db");
476
880
  if (options.create) {
477
- await mkdir(join(pinnedDir, "blobs", ".tmp"), { recursive: true, mode: 448 });
478
- await Promise.all([chmod(pinnedDir, 448), chmod(join(pinnedDir, "blobs"), 448), chmod(join(pinnedDir, "blobs", ".tmp"), 448)]);
479
- const handle = await open(dbPath, fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_RDWR | fsConstants.O_NOFOLLOW, 384).catch((error) => {
881
+ await mkdir(join2(pinnedDir, "blobs", ".tmp"), { recursive: true, mode: 448 });
882
+ await Promise.all([chmod(pinnedDir, 448), chmod(join2(pinnedDir, "blobs"), 448), chmod(join2(pinnedDir, "blobs", ".tmp"), 448)]);
883
+ const handle = await open2(dbPath, fsConstants2.O_CREAT | fsConstants2.O_EXCL | fsConstants2.O_RDWR | fsConstants2.O_NOFOLLOW, 384).catch((error) => {
480
884
  if (errnoOf(error) === "EEXIST") return null;
481
885
  throw error;
482
886
  });
@@ -488,11 +892,11 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
488
892
  }
489
893
  }
490
894
  }
491
- const leaf = await open(dbPath, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
895
+ const leaf = await open2(dbPath, fsConstants2.O_RDONLY | fsConstants2.O_NOFOLLOW);
492
896
  let db;
493
897
  try {
494
898
  const identity = await leaf.stat();
495
- const named = await lstat(dbPath);
899
+ const named = await lstat2(dbPath);
496
900
  if (!identity.isFile() || !named.isFile() || named.dev !== identity.dev || named.ino !== identity.ino) {
497
901
  throw new Error("The replica database changed before SQLite opened it.");
498
902
  }
@@ -507,7 +911,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
507
911
  throw new Error("The replica directory changed before its checked SQLite handle could be shared.");
508
912
  }
509
913
  const pinned = active.directoryFd === void 0 ? lstatSync(active.pinnedDir, { bigint: true }) : fstatSync(active.directoryFd, { bigint: true });
510
- const activeLeaf = lstatSync(join(active.pinnedDir, "replica.db"));
914
+ const activeLeaf = lstatSync(join2(active.pinnedDir, "replica.db"));
511
915
  if (!pinned.isDirectory() || pinned.dev !== opened.dev || pinned.ino !== opened.ino || !activeLeaf.isFile() || activeLeaf.dev !== identity.dev || activeLeaf.ino !== identity.ino || active.directoryFd === void 0 && active.namedPath !== dir) {
512
916
  throw new Error("The live replica changed before its checked SQLite handle could be shared.");
513
917
  }
@@ -574,7 +978,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
574
978
  db.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`);
575
979
  db.exec("COMMIT");
576
980
  }
577
- const shared = options.sqlite ? void 0 : {
981
+ const shared = {
578
982
  key,
579
983
  db,
580
984
  directoryFd,
@@ -586,7 +990,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
586
990
  mutationTail: Promise.resolve()
587
991
  };
588
992
  store = new _SqliteReplicaStore(dir, db, identity, options, directoryFd, namedAncestors, shared);
589
- if (shared) writableStores.set(key, shared);
993
+ if (!options.sqlite) writableStores.set(key, shared);
590
994
  db = void 0;
591
995
  }
592
996
  } finally {
@@ -614,12 +1018,109 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
614
1018
  throw error;
615
1019
  }
616
1020
  }
1021
+ /**
1022
+ * A gated read borrows this controller's already-verified writable handle.
1023
+ * Borrow before the first await: the parent may close in the callback, but
1024
+ * its pin and native handle remain live until the snapshot releases them.
1025
+ * Serialize BEGIN through COMMIT/ROLLBACK with writes on the shared handle.
1026
+ */
1027
+ async readTransaction(generation, checkGate, section) {
1028
+ this.#assertAccepting();
1029
+ const shared = this.#shared;
1030
+ if (!shared) throw new ReplicaError(ReplicaErrorCode.CLOSED, "The replica handle is closed.");
1031
+ const snapshot = new _SqliteReplicaStore(
1032
+ this.dir,
1033
+ shared.db,
1034
+ { dev: this.#dev, ino: this.#ino },
1035
+ { create: false, now: this.#now },
1036
+ shared.directoryFd,
1037
+ this.#namedAncestors,
1038
+ shared,
1039
+ true
1040
+ );
1041
+ let release;
1042
+ let active = false;
1043
+ let callbackFailed = false;
1044
+ try {
1045
+ release = await acquireWritableMutation(shared);
1046
+ await snapshot.#assertReadIdentity();
1047
+ snapshot.#db.exec("BEGIN");
1048
+ active = true;
1049
+ const tombstone = snapshot.#db.get("SELECT generation FROM replica WHERE id = 1");
1050
+ try {
1051
+ await checkGate();
1052
+ } catch (error) {
1053
+ callbackFailed = true;
1054
+ throw error;
1055
+ }
1056
+ if (tombstone === void 0 || tombstone.generation !== generation) {
1057
+ throw new ReplicaError(ReplicaErrorCode.RESET_REQUIRED, "The replica generation changed; reopen it to continue.");
1058
+ }
1059
+ let result;
1060
+ const context = { controller: this, active: true };
1061
+ try {
1062
+ try {
1063
+ result = await activeReadSection.run(context, () => section(snapshot));
1064
+ } catch (error) {
1065
+ callbackFailed = true;
1066
+ throw error;
1067
+ }
1068
+ } finally {
1069
+ context.active = false;
1070
+ }
1071
+ try {
1072
+ await checkGate();
1073
+ } catch (error) {
1074
+ callbackFailed = true;
1075
+ throw error;
1076
+ }
1077
+ await snapshot.#assertReadIdentity();
1078
+ snapshot.#db.exec("COMMIT");
1079
+ active = false;
1080
+ return result;
1081
+ } catch (error) {
1082
+ if (callbackFailed) throw error;
1083
+ if (isReplicaError(error) || typeof error === "object" && error !== null && "code" in error && error.code === "REPLICA_GATED") throw error;
1084
+ if (["ENOENT", "ELOOP", "ENOTDIR"].includes(String(errnoOf(error))) || /replica database changed|SQLite opened a different replica database/i.test(error instanceof Error ? error.message : String(error))) {
1085
+ throw new ReplicaError(ReplicaErrorCode.RESET_REQUIRED, "The replica was replaced; reopen it to continue.", void 0, { cause: error });
1086
+ }
1087
+ throw storageError(error, "Reading the replica snapshot");
1088
+ } finally {
1089
+ if (active) {
1090
+ try {
1091
+ snapshot.#db.exec("ROLLBACK");
1092
+ } catch {
1093
+ }
1094
+ }
1095
+ try {
1096
+ await snapshot.close();
1097
+ } finally {
1098
+ release?.();
1099
+ }
1100
+ }
1101
+ }
1102
+ async #assertReadIdentity() {
1103
+ const shared = this.#shared;
1104
+ const pinned = shared.directoryFd === void 0 ? lstatSync(shared.pinnedDir) : fstatSync(shared.directoryFd);
1105
+ const namedDir = lstatSync(this.dir);
1106
+ const namedDb = lstatSync(join2(this.dir, "replica.db"));
1107
+ const pinnedDb = lstatSync(join2(shared.pinnedDir, "replica.db"));
1108
+ if (!pinned.isDirectory() || !namedDir.isDirectory() || pinned.dev !== namedDir.dev || pinned.ino !== namedDir.ino || !namedDb.isFile() || !pinnedDb.isFile() || namedDb.dev !== this.#dev || namedDb.ino !== this.#ino || pinnedDb.dev !== this.#dev || pinnedDb.ino !== this.#ino) {
1109
+ throw new ReplicaError(ReplicaErrorCode.RESET_REQUIRED, "The replica was replaced during its read; reopen it to continue.");
1110
+ }
1111
+ if (this.#namedAncestors.length) {
1112
+ const ancestors = await captureNamedPath(this.dir);
1113
+ if (ancestors.length !== this.#namedAncestors.length || ancestors.some((entry, index) => entry.dev !== this.#namedAncestors[index].dev || entry.ino !== this.#namedAncestors[index].ino)) {
1114
+ throw new ReplicaError(ReplicaErrorCode.RESET_REQUIRED, "The replica directory was replaced; reopen it to continue.");
1115
+ }
1116
+ }
1117
+ }
617
1118
  #row() {
618
1119
  return this.#db.get("SELECT * FROM replica WHERE id = 1");
619
1120
  }
620
1121
  /** Refuse to touch the filesystem for a replica that was deleted (or replaced) under us. */
621
1122
  async #assertPresent() {
622
- const identity = await lstat(join(this.#pinnedDir, "replica.db")).catch(() => null);
1123
+ const identity = await lstat2(join2(this.#pinnedDir, "replica.db")).catch(() => null);
623
1124
  if (!identity?.isFile() || identity.dev !== this.#dev || identity.ino !== this.#ino) {
624
1125
  throw new ReplicaError(
625
1126
  ReplicaErrorCode.NOT_FOUND,
@@ -636,6 +1137,16 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
636
1137
  /** A short mutation section: guarded (profile lock) and checked against the database this store opened. */
637
1138
  #mutate(section) {
638
1139
  this.#assertAccepting();
1140
+ if (this.#readonlySnapshot) {
1141
+ throw new ReplicaError(ReplicaErrorCode.STORAGE_ERROR, "A read-only replica snapshot cannot mutate the shared database.");
1142
+ }
1143
+ const read = activeReadSection.getStore();
1144
+ if (read?.active && read.controller === this) {
1145
+ throw new ReplicaError(
1146
+ ReplicaErrorCode.STORAGE_ERROR,
1147
+ "mutation attempted on the same controller while its readTransaction section is active; this would deadlock"
1148
+ );
1149
+ }
639
1150
  this.#activeMutations++;
640
1151
  return (async () => {
641
1152
  try {
@@ -779,6 +1290,107 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
779
1290
  );
780
1291
  });
781
1292
  }
1293
+ /** Per-CID first-install anchor and policy bound share the pending-grant transaction. */
1294
+ #grantInstall(cid) {
1295
+ const row = this.#db.get(
1296
+ "SELECT installed_at, effective_expires_at, consent_deadline, consent_anchor_at, consent_anchor_source FROM grant_install WHERE cid = ?",
1297
+ cid
1298
+ );
1299
+ if (row === void 0) return null;
1300
+ if (!(row.consent_anchor_source === "siwe" || row.consent_anchor_source === "delegation_iat" || row.consent_anchor_source === "delegation_nbf" || row.consent_anchor_source === "first_install") || !Number.isSafeInteger(row.installed_at) || row.installed_at < 0 || !Number.isSafeInteger(row.effective_expires_at) || row.effective_expires_at < 0 || !Number.isSafeInteger(row.consent_deadline) || row.consent_deadline < 0 || !Number.isSafeInteger(row.consent_anchor_at) || row.consent_anchor_at < 0) {
1301
+ throw new ReplicaError(ReplicaErrorCode.GRANT_EXPIRED, "The stored grant expiry or consent anchor is invalid.");
1302
+ }
1303
+ return {
1304
+ installedAt: row.installed_at,
1305
+ effectiveExpiresAt: row.effective_expires_at,
1306
+ consentDeadline: row.consent_deadline,
1307
+ consentAnchor: { at: row.consent_anchor_at, source: row.consent_anchor_source }
1308
+ };
1309
+ }
1310
+ async getGrantInstall(cid) {
1311
+ return this.#grantInstall(cid);
1312
+ }
1313
+ /** Every installed CID, including superseded ones, for an origin-wide purge floor. */
1314
+ async grantInstallTimes() {
1315
+ const rows = this.#db.all(
1316
+ "SELECT cid, installed_at FROM grant_install"
1317
+ );
1318
+ return rows.map((row) => {
1319
+ if (typeof row.cid !== "string" || row.cid.length === 0 || !Number.isSafeInteger(row.installed_at) || row.installed_at < 0) {
1320
+ throw new ReplicaError(ReplicaErrorCode.GRANT_EXPIRED, "The stored grant install time is invalid.");
1321
+ }
1322
+ return { cid: row.cid, installedAt: row.installed_at };
1323
+ });
1324
+ }
1325
+ /** Phase 2 installed grants cannot commit a page after their shorter local TTL. */
1326
+ #assertInstalledGrantTime() {
1327
+ const row = this.#row();
1328
+ const cid = row?.pending_grant_cid ?? row?.grant_cid;
1329
+ if (cid === void 0 || cid === null) return;
1330
+ const installed = this.#grantInstall(cid);
1331
+ const now = this.#now();
1332
+ if (installed !== null && (now >= installed.effectiveExpiresAt || now >= installed.consentDeadline)) {
1333
+ throw new ReplicaError(ReplicaErrorCode.GRANT_EXPIRED, "The installed grant expired before this page committed.");
1334
+ }
1335
+ }
1336
+ #storeGrantPolicy(g, policy, existing) {
1337
+ const installedAt = existing?.installedAt ?? policy.at;
1338
+ const effectiveExpiresAt = Math.min(
1339
+ g.expiresAt === null ? Infinity : g.expiresAt * 1e3,
1340
+ installedAt + policy.ttlMs,
1341
+ policy.consentDeadline
1342
+ );
1343
+ if (!Number.isSafeInteger(installedAt) || installedAt < 0 || !Number.isSafeInteger(effectiveExpiresAt) || effectiveExpiresAt < 0 || !Number.isSafeInteger(policy.consentDeadline) || policy.consentDeadline < 0 || !Number.isSafeInteger(policy.consentAnchor.at) || policy.consentAnchor.at < 0 || !Number.isSafeInteger(policy.ttlMs) || policy.ttlMs <= 0) {
1344
+ throw new ReplicaError(ReplicaErrorCode.GRANT_EXPIRED, "The replica grant has no finite effective expiry.");
1345
+ }
1346
+ this.#db.run(
1347
+ `INSERT INTO grant_install (cid, installed_at, effective_expires_at, consent_deadline, consent_anchor_at, consent_anchor_source)
1348
+ VALUES (?, ?, ?, ?, ?, ?)
1349
+ ON CONFLICT (cid) DO UPDATE SET effective_expires_at = excluded.effective_expires_at,
1350
+ consent_deadline = excluded.consent_deadline, consent_anchor_at = excluded.consent_anchor_at,
1351
+ consent_anchor_source = excluded.consent_anchor_source`,
1352
+ g.cid,
1353
+ installedAt,
1354
+ effectiveExpiresAt,
1355
+ policy.consentDeadline,
1356
+ policy.consentAnchor.at,
1357
+ policy.consentAnchor.source
1358
+ );
1359
+ return { installedAt, effectiveExpiresAt, consentDeadline: policy.consentDeadline, consentAnchor: policy.consentAnchor };
1360
+ }
1361
+ async installGatedGrant(g, policy) {
1362
+ return this.#guardedWrite("Installing the gated grant", () => {
1363
+ const row = this.#row();
1364
+ if (row === void 0) throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, "The replica has not been created.");
1365
+ const previous = this.#grantInstall(g.cid);
1366
+ if (previous === null && (row.grant_cid === g.cid || row.pending_grant_cid === g.cid)) {
1367
+ throw new ReplicaError(ReplicaErrorCode.GRANT_EXPIRED, "The installed grant has no durable first-install time.");
1368
+ }
1369
+ const next = this.#storeGrantPolicy(g, policy, previous);
1370
+ if (next.effectiveExpiresAt <= this.#now()) {
1371
+ throw new ReplicaError(ReplicaErrorCode.GRANT_EXPIRED, "The replica grant expired under the current policy.");
1372
+ }
1373
+ if (row.grant_cid !== g.cid && row.pending_grant_cid !== g.cid) {
1374
+ this.#db.run(
1375
+ `UPDATE replica SET pending_grant_cid = ?, pending_grant_bytes = ?, pending_grant_audience = ?,
1376
+ pending_grant_issuer = ?, pending_grant_nbf = ?, pending_grant_exp = ?, pending_grant_error = NULL WHERE id = 1`,
1377
+ g.cid,
1378
+ g.bytes,
1379
+ g.audience,
1380
+ g.issuer,
1381
+ g.notBefore,
1382
+ g.expiresAt
1383
+ );
1384
+ }
1385
+ return next;
1386
+ });
1387
+ }
1388
+ async recomputeGrantInstall(g, policy) {
1389
+ return this.#guardedWrite("Recomputing the gated grant", () => {
1390
+ const previous = this.#grantInstall(g.cid);
1391
+ return previous === null ? null : this.#storeGrantPolicy(g, policy, previous);
1392
+ });
1393
+ }
782
1394
  async installGrant(g) {
783
1395
  await this.#guardedWrite("Installing the grant", () => {
784
1396
  const row = this.#row();
@@ -837,13 +1449,14 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
837
1449
  const now = this.#now();
838
1450
  if (row.lease_holder !== null && row.lease_expires_at !== null && row.lease_expires_at > now) return null;
839
1451
  const token = row.lease_token + 1;
1452
+ const holder = this.#holder ??= `${process.pid}-${randomBytes(6).toString("hex")}`;
840
1453
  this.#db.run(
841
1454
  "UPDATE replica SET lease_token = ?, lease_holder = ?, lease_expires_at = ? WHERE id = 1",
842
1455
  token,
843
- this.#holder,
1456
+ holder,
844
1457
  now + ttlMs
845
1458
  );
846
- return { token, holder: this.#holder };
1459
+ return { token, holder };
847
1460
  });
848
1461
  }
849
1462
  async renewLease(t, ttlMs) {
@@ -875,7 +1488,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
875
1488
  return held;
876
1489
  }
877
1490
  #blobPath(hash) {
878
- return join(this.#pinnedDir, "blobs", hash.slice(0, 2), hash);
1491
+ return join2(this.#pinnedDir, "blobs", hash.slice(0, 2), hash);
879
1492
  }
880
1493
  /**
881
1494
  * Make every blob durable before any row references it: write to a fresh
@@ -892,12 +1505,12 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
892
1505
  () => false
893
1506
  );
894
1507
  if (exists) continue;
895
- const shard = join(this.#pinnedDir, "blobs", hash.slice(0, 2));
1508
+ const shard = join2(this.#pinnedDir, "blobs", hash.slice(0, 2));
896
1509
  await mkdir(shard, { mode: 448 }).catch((error) => {
897
1510
  if (errnoOf(error) !== "EEXIST") throw error;
898
1511
  });
899
- const temp = join(this.#pinnedDir, "blobs", ".tmp", `${hash}.${randomBytes(6).toString("hex")}`);
900
- const handle = await open(temp, fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_WRONLY, 384);
1512
+ const temp = join2(this.#pinnedDir, "blobs", ".tmp", `${hash}.${randomBytes(6).toString("hex")}`);
1513
+ const handle = await open2(temp, fsConstants2.O_CREAT | fsConstants2.O_EXCL | fsConstants2.O_WRONLY, 384);
901
1514
  try {
902
1515
  await handle.writeFile(bytes);
903
1516
  await handle.sync();
@@ -909,13 +1522,13 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
909
1522
  } catch (error) {
910
1523
  if (errnoOf(error) !== "EEXIST") throw error;
911
1524
  } finally {
912
- await unlink(temp);
1525
+ await unlink2(temp);
913
1526
  }
914
1527
  dirs.add(shard);
915
- dirs.add(join(this.#pinnedDir, "blobs"));
1528
+ dirs.add(join2(this.#pinnedDir, "blobs"));
916
1529
  }
917
1530
  for (const dir of dirs) {
918
- const handle = await open(dir, fsConstants.O_RDONLY);
1531
+ const handle = await open2(dir, fsConstants2.O_RDONLY);
919
1532
  try {
920
1533
  await handle.sync();
921
1534
  } finally {
@@ -926,6 +1539,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
926
1539
  async applyPage(t, p) {
927
1540
  await this.#mutate(async () => {
928
1541
  this.#checkLease(t);
1542
+ this.#assertInstalledGrantTime();
929
1543
  const reused = /* @__PURE__ */ new Map();
930
1544
  for (const change of p.changes) {
931
1545
  if (change.deleted || !change.content || p.blobs.has(change.hash) || reused.has(change.hash)) continue;
@@ -947,6 +1561,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
947
1561
  try {
948
1562
  await this.#write("Committing a sync page", () => {
949
1563
  this.#checkLease(t);
1564
+ this.#assertInstalledGrantTime();
950
1565
  const now = this.#now();
951
1566
  if (p.window.notBefore !== null && now < Date.parse(p.window.notBefore)) {
952
1567
  throw new ReplicaError(ReplicaErrorCode.GRANT_NOT_YET_VALID, `The grant is not valid before ${p.window.notBefore}; nothing was committed.`);
@@ -1020,12 +1635,12 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
1020
1635
  /** Unlink every blob not in `keep`, and every temp file. Only a missing directory counts as empty. */
1021
1636
  async #unlinkBlobs(keep) {
1022
1637
  let removed = 0;
1023
- const root = join(this.#pinnedDir, "blobs");
1638
+ const root = join2(this.#pinnedDir, "blobs");
1024
1639
  for (const shard of await entriesOf(root)) {
1025
- const shardPath = join(root, shard);
1640
+ const shardPath = join2(root, shard);
1026
1641
  if (shard === ".tmp") {
1027
1642
  for (const name of await entriesOf(shardPath)) {
1028
- await rm(join(shardPath, name), { force: true });
1643
+ await rm(join2(shardPath, name), { force: true });
1029
1644
  removed += 1;
1030
1645
  }
1031
1646
  continue;
@@ -1036,18 +1651,18 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
1036
1651
  kept += 1;
1037
1652
  continue;
1038
1653
  }
1039
- await rm(join(shardPath, name), { force: true });
1654
+ await rm(join2(shardPath, name), { force: true });
1040
1655
  removed += 1;
1041
1656
  }
1042
- if (kept === 0) await rmdir(shardPath).catch(() => void 0);
1657
+ if (kept === 0) await rmdir2(shardPath).catch(() => void 0);
1043
1658
  }
1044
1659
  return removed;
1045
1660
  }
1046
1661
  /** Whether any content file (a blob or a temp file) is on disk. */
1047
1662
  async #holdsContentFiles() {
1048
- const root = join(this.#pinnedDir, "blobs");
1663
+ const root = join2(this.#pinnedDir, "blobs");
1049
1664
  for (const shard of await entriesOf(root)) {
1050
- if ((await entriesOf(join(root, shard))).length > 0) return true;
1665
+ if ((await entriesOf(join2(root, shard))).length > 0) return true;
1051
1666
  }
1052
1667
  return false;
1053
1668
  }
@@ -1101,13 +1716,10 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
1101
1716
  }
1102
1717
  }
1103
1718
  /**
1104
- * Remove this replica's directory for good (`tc replica reset --purge`).
1105
- * Fenced like a commit: `t` must still be a live lease on the database this
1106
- * store opened, checked under the write lock, so a paused process can
1107
- * never remove a replica another process took over or recreated. Allowed
1108
- * on a revoked replica. The store is unusable afterwards; close it.
1719
+ * Remove the source-created mirror layout under a live lease. Unknown files
1720
+ * are removed only when their exact preflight identities were approved.
1109
1721
  */
1110
- async destroy(t) {
1722
+ async destroy(t, options = {}) {
1111
1723
  await this.#mutate(async () => {
1112
1724
  const release = this.#shared ? await acquireWritableMutation(this.#shared) : void 0;
1113
1725
  try {
@@ -1123,87 +1735,31 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
1123
1735
  throw new ReplicaError(ReplicaErrorCode.RUNTIME_UNSUPPORTED, "Replica removal requires an opened or identity-checked directory.");
1124
1736
  }
1125
1737
  const assertDirectory = async () => {
1126
- const current = await lstat(this.dir, { bigint: true });
1738
+ const current = await lstat2(this.dir, { bigint: true });
1127
1739
  if (!current.isDirectory() || current.dev !== expected.dev || current.ino !== expected.ino) {
1128
1740
  throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, "The replica directory changed during removal.");
1129
1741
  }
1130
1742
  return current;
1131
1743
  };
1132
1744
  const pinned = await assertDirectory();
1133
- await this.#assertPresent();
1134
- if (process.platform === "linux") {
1135
- for (const entry of await readdir(this.#pinnedDir)) {
1136
- await rm(join(this.#pinnedDir, entry), { recursive: true, force: true });
1137
- }
1138
- await assertDirectory();
1139
- await rmdir(this.dir);
1140
- } else {
1141
- const ancestors = this.#namedAncestors;
1142
- const checkAncestors = async () => {
1143
- for (const expected2 of ancestors) {
1144
- const current = await lstat(expected2.path, { bigint: true });
1145
- if (current.dev !== expected2.dev || current.ino !== expected2.ino) {
1146
- throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, "The replica ancestry changed during removal.");
1147
- }
1745
+ const assertNamedPath = async () => {
1746
+ for (const ancestor of this.#namedAncestors) {
1747
+ const current = await lstat2(ancestor.path, { bigint: true });
1748
+ if (current.dev !== ancestor.dev || current.ino !== ancestor.ino) {
1749
+ throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, "The replica ancestry changed during removal.");
1148
1750
  }
1149
- };
1150
- const directories = /* @__PURE__ */ new Map([[this.dir, pinned]]);
1151
- const files = /* @__PURE__ */ new Map();
1152
- const check = async (path, expected2, directory) => {
1153
- if (path !== this.dir) {
1154
- const parent = dirname(path);
1155
- await check(parent, directories.get(parent), true);
1156
- }
1157
- await checkAncestors();
1158
- const current = await lstat(path, { bigint: true });
1159
- if ((directory ? !current.isDirectory() : !current.isFile()) || current.dev !== expected2.dev || current.ino !== expected2.ino) {
1160
- throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, "The replica layout changed during removal.");
1161
- }
1162
- };
1163
- const record = async (path, directory) => {
1164
- const parent = dirname(path);
1165
- await check(parent, directories.get(parent), true);
1166
- const current = await lstat(path, { bigint: true });
1167
- if (!(directory ? current.isDirectory() : current.isFile())) {
1168
- throw new ReplicaError(ReplicaErrorCode.STORAGE_ERROR, "Unexpected replica layout during removal.");
1169
- }
1170
- (directory ? directories : files).set(path, current);
1171
- };
1172
- const blobs = join(this.dir, "blobs");
1173
- for (const name of await readdir(this.dir)) {
1174
- if (name === "blobs") await record(blobs, true);
1175
- else if (["replica.db", "replica.db-wal", "replica.db-shm"].includes(name)) await record(join(this.dir, name), false);
1176
- else throw new ReplicaError(ReplicaErrorCode.STORAGE_ERROR, "Unexpected replica layout during removal.");
1177
- }
1178
- if (directories.has(blobs)) {
1179
- await check(blobs, directories.get(blobs), true);
1180
- for (const shard of await readdir(blobs)) {
1181
- if (shard !== ".tmp" && !/^[0-9a-f]{2}$/.test(shard)) {
1182
- throw new ReplicaError(ReplicaErrorCode.STORAGE_ERROR, "Unexpected replica blob shard during removal.");
1183
- }
1184
- const shardPath = join(blobs, shard);
1185
- await record(shardPath, true);
1186
- await check(shardPath, directories.get(shardPath), true);
1187
- for (const name of await readdir(shardPath)) {
1188
- if (shard === ".tmp" ? !/^[0-9a-f]{64}\.[0-9a-f]{12}$/.test(name) : !isHash(name) || !name.startsWith(shard)) {
1189
- throw new ReplicaError(ReplicaErrorCode.STORAGE_ERROR, "Unexpected replica blob file during removal.");
1190
- }
1191
- await record(join(shardPath, name), false);
1192
- }
1193
- }
1194
- }
1195
- for (const [path, identity] of files) {
1196
- await check(path, identity, false);
1197
- await unlink(path);
1198
- }
1199
- for (const [path, identity] of [...directories].reverse()) {
1200
- if (path === this.dir) continue;
1201
- await check(path, identity, true);
1202
- await rmdir(path);
1203
1751
  }
1204
1752
  await assertDirectory();
1205
- await checkAncestors();
1206
- await rmdir(this.dir);
1753
+ };
1754
+ await this.#assertPresent();
1755
+ try {
1756
+ await removeSqliteReplicaLayout(this.dir, {
1757
+ ...options,
1758
+ expectedRoot: options.expectedRoot ?? { dev: pinned.dev.toString(), ino: pinned.ino.toString() }
1759
+ });
1760
+ } catch (error) {
1761
+ await assertNamedPath();
1762
+ throw error;
1207
1763
  }
1208
1764
  } catch (error) {
1209
1765
  throw storageError(error, "Removing the replica");
@@ -1377,8 +1933,8 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
1377
1933
  this.#closing = true;
1378
1934
  this.#closePromise = (async () => {
1379
1935
  if (this.#activeMutations !== 0) {
1380
- await new Promise((resolve2) => {
1381
- this.#drained = resolve2;
1936
+ await new Promise((resolve3) => {
1937
+ this.#drained = resolve3;
1382
1938
  });
1383
1939
  }
1384
1940
  this.#closed = true;
@@ -1402,7 +1958,9 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
1402
1958
  export {
1403
1959
  DURABILITY,
1404
1960
  SqliteReplicaStore,
1961
+ inspectSqliteReplicaLayout,
1405
1962
  loadSqlite,
1406
- nodeSqliteSupported
1963
+ nodeSqliteSupported,
1964
+ removeSqliteReplicaLayout
1407
1965
  };
1408
1966
  //# sourceMappingURL=sqlite.js.map