@secondlayer/shared 8.0.0 → 10.0.0

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 (36) hide show
  1. package/dist/src/archive/range-digest.d.ts +16 -0
  2. package/dist/src/archive/semantic-digest-builder.d.ts +16 -0
  3. package/dist/src/archive/status.js +11 -3
  4. package/dist/src/archive/status.js.map +3 -3
  5. package/dist/src/coverage/index.d.ts +16 -13
  6. package/dist/src/coverage/index.js +1 -11
  7. package/dist/src/coverage/index.js.map +3 -3
  8. package/dist/src/db/index.d.ts +18 -1
  9. package/dist/src/db/index.js +2 -1
  10. package/dist/src/db/index.js.map +3 -3
  11. package/dist/src/db/queries/chain-reorgs.d.ts +16 -0
  12. package/dist/src/db/queries/chain-reorgs.js +2 -1
  13. package/dist/src/db/queries/chain-reorgs.js.map +3 -3
  14. package/dist/src/db/queries/contracts.d.ts +16 -0
  15. package/dist/src/db/queries/instance.d.ts +16 -0
  16. package/dist/src/db/queries/integrity.d.ts +16 -0
  17. package/dist/src/db/queries/subgraph-gaps.d.ts +16 -0
  18. package/dist/src/db/queries/subgraph-operations.d.ts +16 -0
  19. package/dist/src/db/queries/subgraphs.d.ts +16 -0
  20. package/dist/src/db/queries/subscriptions.d.ts +16 -0
  21. package/dist/src/db/queries/sync-scope.d.ts +1006 -0
  22. package/dist/src/db/queries/sync-scope.js +116 -0
  23. package/dist/src/db/queries/sync-scope.js.map +10 -0
  24. package/dist/src/db/schema.d.ts +17 -1
  25. package/dist/src/index.d.ts +18 -1
  26. package/dist/src/index.js +2 -1
  27. package/dist/src/index.js.map +3 -3
  28. package/dist/src/node/local-client.d.ts +16 -0
  29. package/dist/src/runtime/index.d.ts +188 -18
  30. package/dist/src/runtime/index.js +322 -38
  31. package/dist/src/runtime/index.js.map +10 -7
  32. package/migrations/0120_sync_scopes.ts +42 -0
  33. package/package.json +7 -6
  34. package/dist/src/x402.d.ts +0 -47
  35. package/dist/src/x402.js +0 -79
  36. package/dist/src/x402.js.map +0 -10
@@ -337,7 +337,11 @@ var KNOWN = new Set([
337
337
  "SUBGRAPH_INDEX_API_URL",
338
338
  "SBTC_DECODER_ENABLED",
339
339
  "POX4_DECODER_ENABLED",
340
- "BNS_DECODER_ENABLED"
340
+ "BNS_DECODER_ENABLED",
341
+ "ARCHIVE_DIR",
342
+ "ARCHIVE_PUBLIC_DIR",
343
+ "SECONDLAYER_ALLOW_UNDERSIZED",
344
+ "SECONDLAYER_SYNC_START_HEIGHT"
341
345
  ]);
342
346
  var OWN_PREFIXES = [
343
347
  "SECONDLAYER_",
@@ -499,6 +503,14 @@ function actionFor(plane, state) {
499
503
  return "inspect logs";
500
504
  }
501
505
  // src/runtime/backup.ts
506
+ import {
507
+ createCipheriv,
508
+ createDecipheriv,
509
+ randomBytes,
510
+ scryptSync,
511
+ timingSafeEqual
512
+ } from "node:crypto";
513
+ var BACKUP_MANIFEST_VERSION = 1;
502
514
  var BACKUP_PARTS = [
503
515
  "db",
504
516
  "config",
@@ -506,55 +518,209 @@ var BACKUP_PARTS = [
506
518
  "handlers",
507
519
  "scope"
508
520
  ];
521
+ var CANARY_PLAINTEXT = "secondlayer-secrets-key-canary-v1";
522
+ var IV_LEN = 12;
523
+ var TAG_LEN = 16;
524
+ var SALT_LEN = 16;
525
+ var SCRYPT_COST = 2 ** 15;
526
+ var SCRYPT_MAXMEM = 64 * 1024 * 1024;
527
+ var SCRYPT_OPTS = { N: SCRYPT_COST, maxmem: SCRYPT_MAXMEM };
509
528
  function planBackup(input) {
510
- if (!input.encryptedKeys) {
529
+ if (input.fromHeight < 0) {
530
+ return { ok: false, reason: "scope from_height must be >= 0" };
531
+ }
532
+ if (input.toHeight !== null && input.toHeight !== undefined && input.toHeight < input.fromHeight) {
533
+ return { ok: false, reason: "scope to_height is below from_height" };
534
+ }
535
+ if (input.includeSecrets && !input.secretsEncrypted) {
511
536
  return {
512
537
  ok: false,
513
- manifest: emptyManifest(input),
514
- reason: "keys must be encrypted"
538
+ reason: "refusing to write plaintext secrets; supply a passphrase or pass the explicit plaintext override"
515
539
  };
516
540
  }
517
- if (input.fromHeight < 0) {
541
+ if (input.includeSecrets && !input.canary) {
518
542
  return {
519
543
  ok: false,
520
- manifest: emptyManifest(input),
521
- reason: "scope from_height must be >= 0"
544
+ reason: "secrets bundle requires a key canary so a restore can prove the key matches"
522
545
  };
523
546
  }
547
+ const parts = input.includeSecrets ? [...BACKUP_PARTS] : BACKUP_PARTS.filter((p) => p !== "keys");
524
548
  return {
525
549
  ok: true,
526
550
  manifest: {
527
- created_at: input.now ?? "1970-01-01T00:00:00.000Z",
551
+ schema_version: BACKUP_MANIFEST_VERSION,
552
+ created_at: input.now ?? new Date().toISOString(),
528
553
  network: input.network,
529
- parts: [...BACKUP_PARTS],
530
- encrypted: true,
554
+ parts,
555
+ encrypted: input.secretsEncrypted,
531
556
  scope: {
532
557
  from_height: input.fromHeight,
533
558
  to_height: input.toHeight ?? null
534
- }
535
- },
536
- reason: "dry-run backup bundle"
559
+ },
560
+ db: null,
561
+ secrets: null
562
+ }
537
563
  };
538
564
  }
539
- function restoreBackup(plan, opts) {
540
- if (!plan.ok)
541
- return { ok: false, reason: plan.reason };
542
- if (!opts?.apply)
543
- return { ok: true, applied: false, deep_green: false };
565
+ function secretsKeyBuffer(hexKey) {
566
+ const key = Buffer.from(hexKey.trim(), "hex");
567
+ if (key.length !== 32) {
568
+ throw new Error("SECONDLAYER_SECRETS_KEY must be 32 bytes hex");
569
+ }
570
+ return key;
571
+ }
572
+ function sealKeyCanary(secretsKeyHex) {
573
+ const key = secretsKeyBuffer(secretsKeyHex);
574
+ const iv = randomBytes(IV_LEN);
575
+ const cipher = createCipheriv("aes-256-gcm", key, iv);
576
+ const ciphertext = Buffer.concat([
577
+ cipher.update(CANARY_PLAINTEXT, "utf8"),
578
+ cipher.final()
579
+ ]);
580
+ return Buffer.concat([iv, cipher.getAuthTag(), ciphertext]).toString("base64");
581
+ }
582
+ function keyMatchesCanary(canaryBase64, secretsKeyHex) {
583
+ try {
584
+ const key = secretsKeyBuffer(secretsKeyHex);
585
+ const envelope = Buffer.from(canaryBase64, "base64");
586
+ const iv = envelope.subarray(0, IV_LEN);
587
+ const tag = envelope.subarray(IV_LEN, IV_LEN + TAG_LEN);
588
+ const ciphertext = envelope.subarray(IV_LEN + TAG_LEN);
589
+ const decipher = createDecipheriv("aes-256-gcm", key, iv);
590
+ decipher.setAuthTag(tag);
591
+ const plaintext = Buffer.concat([
592
+ decipher.update(ciphertext),
593
+ decipher.final()
594
+ ]);
595
+ const expected = Buffer.from(CANARY_PLAINTEXT, "utf8");
596
+ return plaintext.length === expected.length && timingSafeEqual(plaintext, expected);
597
+ } catch {
598
+ return false;
599
+ }
600
+ }
601
+ function encryptBundle(plaintext, passphrase) {
602
+ if (!passphrase)
603
+ throw new Error("passphrase is required");
604
+ const salt = randomBytes(SALT_LEN);
605
+ const key = scryptSync(passphrase, salt, 32, SCRYPT_OPTS);
606
+ const iv = randomBytes(IV_LEN);
607
+ const cipher = createCipheriv("aes-256-gcm", key, iv);
608
+ const ciphertext = Buffer.concat([
609
+ cipher.update(plaintext, "utf8"),
610
+ cipher.final()
611
+ ]);
612
+ return Buffer.concat([salt, iv, cipher.getAuthTag(), ciphertext]);
613
+ }
614
+ function decryptBundle(envelope, passphrase) {
615
+ const salt = envelope.subarray(0, SALT_LEN);
616
+ const iv = envelope.subarray(SALT_LEN, SALT_LEN + IV_LEN);
617
+ const tag = envelope.subarray(SALT_LEN + IV_LEN, SALT_LEN + IV_LEN + TAG_LEN);
618
+ const ciphertext = envelope.subarray(SALT_LEN + IV_LEN + TAG_LEN);
619
+ const key = scryptSync(passphrase, salt, 32, SCRYPT_OPTS);
620
+ const decipher = createDecipheriv("aes-256-gcm", key, iv);
621
+ decipher.setAuthTag(tag);
622
+ return decipher.update(ciphertext).toString("utf8") + decipher.final("utf8");
623
+ }
624
+ function precheckRestore(input) {
625
+ if (input.manifest.schema_version !== BACKUP_MANIFEST_VERSION) {
626
+ return {
627
+ ok: false,
628
+ fatal: true,
629
+ reason: `bundle schema_version ${input.manifest.schema_version} is not supported (expected ${BACKUP_MANIFEST_VERSION})`
630
+ };
631
+ }
632
+ if (input.manifest.network !== input.targetNetwork) {
633
+ return {
634
+ ok: false,
635
+ fatal: true,
636
+ reason: `bundle is ${input.manifest.network}; this instance is ${input.targetNetwork}`
637
+ };
638
+ }
639
+ if (!input.targetIsEmpty && !input.force) {
640
+ return {
641
+ ok: false,
642
+ fatal: false,
643
+ reason: "target database already holds chain data; restoring would overwrite it"
644
+ };
645
+ }
646
+ const secrets = input.manifest.secrets;
647
+ if (secrets) {
648
+ if (!input.secretsKeyHex) {
649
+ return {
650
+ ok: false,
651
+ fatal: false,
652
+ reason: "bundle carries encrypted columns but no secrets key was supplied; restoring now would leave them unreadable"
653
+ };
654
+ }
655
+ if (!keyMatchesCanary(secrets.canary, input.secretsKeyHex)) {
656
+ return {
657
+ ok: false,
658
+ fatal: true,
659
+ reason: "the supplied SECONDLAYER_SECRETS_KEY does not match the one this backup was taken with; encrypted columns would be permanently unreadable"
660
+ };
661
+ }
662
+ }
663
+ return { ok: true };
664
+ }
665
+ // src/runtime/uninstall.ts
666
+ var REMOVABLE = ["containers", "networks", "handler-cache"];
667
+ var PRESERVED = ["index", "chainstate", "secrets", "backups"];
668
+ function planUninstall(input) {
669
+ const preserves = [
670
+ {
671
+ what: "index",
672
+ detail: "the Postgres volume — rebuildable from the archive, but slow"
673
+ },
674
+ {
675
+ what: "chainstate",
676
+ detail: "bundled node data, if any — re-syncable"
677
+ },
678
+ {
679
+ what: "secrets",
680
+ detail: `${input.dataDir} and .env.local — NOT recoverable if lost`
681
+ },
682
+ { what: "backups", detail: "any bundles already written" }
683
+ ];
684
+ if (!input.purge) {
685
+ return {
686
+ ok: true,
687
+ warnings: [],
688
+ plan: {
689
+ removes: [...REMOVABLE],
690
+ preserves,
691
+ destroys: [],
692
+ purge: false
693
+ }
694
+ };
695
+ }
696
+ if (!input.confirmed) {
697
+ return {
698
+ ok: false,
699
+ reason: "purge destroys the index and every volume; re-run with the explicit confirmation flag"
700
+ };
701
+ }
702
+ if (input.secretsPresent && !input.keysBackedUp) {
703
+ return {
704
+ ok: false,
705
+ reason: "refusing to purge: this instance holds keys with no backup bundle. Run `secondlayer backup` first — the index can be rebuilt from the archive, the keys cannot."
706
+ };
707
+ }
544
708
  return {
545
709
  ok: true,
546
- applied: true,
547
- deep_green: opts.deepVerify !== false
710
+ warnings: [
711
+ "purge removes the index; a rebuild from the archive takes hours"
712
+ ],
713
+ plan: {
714
+ removes: [...REMOVABLE],
715
+ preserves: preserves.filter((p) => p.what === "secrets"),
716
+ destroys: ["postgres_data", "subgraphs_data"],
717
+ purge: true
718
+ }
548
719
  };
549
720
  }
550
- function emptyManifest(input) {
551
- return {
552
- created_at: input.now ?? "1970-01-01T00:00:00.000Z",
553
- network: input.network,
554
- parts: [],
555
- encrypted: false,
556
- scope: { from_height: input.fromHeight, to_height: input.toHeight ?? null }
557
- };
721
+ function uninstallCommand(plan, composeFile) {
722
+ const base = ["compose", "-f", composeFile, "down", "--remove-orphans"];
723
+ return plan.purge ? [...base, "-v"] : base;
558
724
  }
559
725
  // src/runtime/upgrade.ts
560
726
  var UPGRADE_STEPS = [
@@ -634,29 +800,133 @@ function applyUpgrade(plan, opts) {
634
800
  return { ok: true, applied: true, reason: "upgrade applied" };
635
801
  }
636
802
  // src/runtime/guardrails.ts
803
+ var MEASURED_DISK = {
804
+ diskGbPer100kBlocks: 3,
805
+ diskGbPer100kBlocksDecoded: 6
806
+ };
637
807
  var FLOORS = {
638
808
  appRamMb: 4096,
639
809
  fullRamMb: 98304,
810
+ mainnetAppDiskGb: 600,
640
811
  appDiskGb: 80,
812
+ mainnetFullDiskGb: 2500,
641
813
  fullDiskGb: 1500,
642
814
  postgresMaxConnections: 50
643
815
  };
644
- function preflightResources(snap, mode) {
816
+ function diskFloorGb(mode, network) {
817
+ if (mode === "full") {
818
+ return network === "mainnet" ? FLOORS.mainnetFullDiskGb : FLOORS.fullDiskGb;
819
+ }
820
+ return network === "mainnet" ? FLOORS.mainnetAppDiskGb : FLOORS.appDiskGb;
821
+ }
822
+ function preflightResources(snap, mode, network = "mainnet") {
645
823
  const errors = [];
646
824
  const ramFloor = mode === "full" ? FLOORS.fullRamMb : FLOORS.appRamMb;
647
- const diskFloor = mode === "full" ? FLOORS.fullDiskGb : FLOORS.appDiskGb;
648
- if (snap.ramMb < ramFloor) {
825
+ const diskFloor = diskFloorGb(mode, network);
826
+ if (snap.ramMb !== undefined && snap.ramMb < ramFloor) {
649
827
  errors.push(`RAM ${snap.ramMb}MB below ${ramFloor}MB for NODE_MODE=${mode}`);
650
828
  }
651
- if (snap.diskGb < diskFloor) {
652
- errors.push(`disk ${snap.diskGb}GB below ${diskFloor}GB for NODE_MODE=${mode}`);
829
+ if (snap.diskGb !== undefined && snap.diskGb < diskFloor) {
830
+ errors.push(`disk ${snap.diskGb}GB below ${diskFloor}GB for NODE_MODE=${mode} on ${network}`);
653
831
  }
654
- if (snap.postgresMaxConnections < FLOORS.postgresMaxConnections) {
832
+ if (snap.postgresMaxConnections !== undefined && snap.postgresMaxConnections < FLOORS.postgresMaxConnections) {
655
833
  errors.push(`Postgres max_connections ${snap.postgresMaxConnections} below ${FLOORS.postgresMaxConnections}`);
656
834
  }
657
835
  if (errors.length > 0)
658
836
  return { ok: false, errors };
659
- return { ok: true, estimates: { diskGbPer100kBlocks: 1 } };
837
+ return { ok: true, estimates: MEASURED_DISK };
838
+ }
839
+ // src/runtime/resources.ts
840
+ import { statfs } from "node:fs/promises";
841
+ import { totalmem } from "node:os";
842
+ var BYTES_PER_MB = 1024 * 1024;
843
+ var BYTES_PER_GB = 1024 * 1024 * 1024;
844
+ async function measureRamMb() {
845
+ try {
846
+ const total = totalmem();
847
+ return total > 0 ? Math.floor(total / BYTES_PER_MB) : null;
848
+ } catch {
849
+ return null;
850
+ }
851
+ }
852
+ async function measureDiskGb(path) {
853
+ try {
854
+ const stats = await statfs(path);
855
+ const totalBytes = Number(stats.blocks) * Number(stats.bsize);
856
+ return totalBytes > 0 ? Math.floor(totalBytes / BYTES_PER_GB) : null;
857
+ } catch {
858
+ return null;
859
+ }
860
+ }
861
+ async function measurePostgresMaxConnections(query) {
862
+ try {
863
+ const rows = await query("SHOW max_connections");
864
+ const raw = rows[0]?.max_connections;
865
+ const value = Number(raw);
866
+ return Number.isFinite(value) && value > 0 ? value : null;
867
+ } catch {
868
+ return null;
869
+ }
870
+ }
871
+ async function measureResources(input) {
872
+ const unknown = [];
873
+ const snapshot = {};
874
+ const ramMb = await measureRamMb();
875
+ if (ramMb === null)
876
+ unknown.push("RAM");
877
+ else
878
+ snapshot.ramMb = ramMb;
879
+ const diskGb = await measureDiskGb(input.dataDir);
880
+ if (diskGb === null)
881
+ unknown.push(`disk (${input.dataDir} unreadable)`);
882
+ else
883
+ snapshot.diskGb = diskGb;
884
+ if (input.query) {
885
+ const conns = await measurePostgresMaxConnections(input.query);
886
+ if (conns === null)
887
+ unknown.push("Postgres max_connections");
888
+ else
889
+ snapshot.postgresMaxConnections = conns;
890
+ } else {
891
+ unknown.push("Postgres max_connections (no database handle)");
892
+ }
893
+ return { snapshot, unknown };
894
+ }
895
+
896
+ // src/runtime/preflight.ts
897
+ var UNDERSIZED_OVERRIDE = "SECONDLAYER_ALLOW_UNDERSIZED";
898
+ function isOverridden(env) {
899
+ return env[UNDERSIZED_OVERRIDE]?.trim().toLowerCase() === "true";
900
+ }
901
+ function decidePreflight(input) {
902
+ const env = input.env ?? {};
903
+ const result = preflightResources(input.measured.snapshot, input.mode, input.network);
904
+ const messages = [];
905
+ if (input.measured.unknown.length > 0) {
906
+ messages.push(`resource preflight could not measure: ${input.measured.unknown.join(", ")}`);
907
+ }
908
+ if (result.ok) {
909
+ return { action: messages.length > 0 ? "warn" : "pass", messages };
910
+ }
911
+ messages.push(...result.errors);
912
+ if (isOverridden(env)) {
913
+ messages.push(`${UNDERSIZED_OVERRIDE}=true — starting anyway. This box is below the documented floor; expect to run out of disk as history accumulates.`);
914
+ return { action: "warn", messages };
915
+ }
916
+ messages.push(`Refusing to start. Resize the box, or set ${UNDERSIZED_OVERRIDE}=true to proceed at your own risk. Sizing guidance: https://www.secondlayer.tools/docs/self-host#guardrails`);
917
+ return { action: "refuse", messages };
918
+ }
919
+ async function runPreflight(input) {
920
+ const measured = await measureResources({
921
+ dataDir: input.dataDir,
922
+ query: input.query
923
+ });
924
+ return decidePreflight({
925
+ measured,
926
+ mode: input.mode,
927
+ network: input.network,
928
+ env: input.env
929
+ });
660
930
  }
661
931
  // src/runtime/commands.ts
662
932
  var MODULE_COMMANDS = {
@@ -676,16 +946,26 @@ function commandFor(id) {
676
946
  return MODULE_COMMANDS[id];
677
947
  }
678
948
  export {
679
- restoreBackup,
949
+ uninstallCommand,
950
+ sealKeyCanary,
951
+ runPreflight,
680
952
  reportRuntimeStatus,
681
953
  preflightResources,
954
+ precheckRestore,
682
955
  planUpgrade,
956
+ planUninstall,
683
957
  planBackup,
684
958
  parseRuntimeConfig,
685
959
  migrateEmbedded,
960
+ measureResources,
961
+ keyMatchesCanary,
686
962
  isolateHandler,
687
963
  isModuleId,
964
+ encryptBundle,
688
965
  emptyHealth,
966
+ diskFloorGb,
967
+ decryptBundle,
968
+ decidePreflight,
689
969
  createSupervisor,
690
970
  createMemoryModule,
691
971
  createMemoryMigrationRunner,
@@ -693,10 +973,13 @@ export {
693
973
  applyUpgrade,
694
974
  actionFor,
695
975
  UPGRADE_STEPS,
976
+ UNDERSIZED_OVERRIDE,
696
977
  STATUS_PLANES,
697
978
  SECRET_KEYS,
698
979
  REQUIRED_KEYS,
980
+ REMOVABLE,
699
981
  PUBLISHER_PROFILE,
982
+ PRESERVED,
700
983
  OPTIONAL_KEYS,
701
984
  NODE_MODES,
702
985
  NETWORKS,
@@ -710,8 +993,9 @@ export {
710
993
  ISOLATION_FAULTS,
711
994
  FLOORS,
712
995
  DEFAULT_PROFILE,
713
- BACKUP_PARTS
996
+ BACKUP_PARTS,
997
+ BACKUP_MANIFEST_VERSION
714
998
  };
715
999
 
716
- //# debugId=0C264B17247A3CFD64756E2164756E21
1000
+ //# debugId=57C29D3188B4875364756E2164756E21
717
1001
  //# sourceMappingURL=index.js.map