@secondlayer/shared 8.0.0 → 9.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 (34) 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 +15 -0
  6. package/dist/src/db/index.d.ts +18 -1
  7. package/dist/src/db/index.js +2 -1
  8. package/dist/src/db/index.js.map +3 -3
  9. package/dist/src/db/queries/chain-reorgs.d.ts +16 -0
  10. package/dist/src/db/queries/chain-reorgs.js +2 -1
  11. package/dist/src/db/queries/chain-reorgs.js.map +3 -3
  12. package/dist/src/db/queries/contracts.d.ts +16 -0
  13. package/dist/src/db/queries/instance.d.ts +16 -0
  14. package/dist/src/db/queries/integrity.d.ts +16 -0
  15. package/dist/src/db/queries/subgraph-gaps.d.ts +16 -0
  16. package/dist/src/db/queries/subgraph-operations.d.ts +16 -0
  17. package/dist/src/db/queries/subgraphs.d.ts +16 -0
  18. package/dist/src/db/queries/subscriptions.d.ts +16 -0
  19. package/dist/src/db/queries/sync-scope.d.ts +1006 -0
  20. package/dist/src/db/queries/sync-scope.js +116 -0
  21. package/dist/src/db/queries/sync-scope.js.map +10 -0
  22. package/dist/src/db/schema.d.ts +17 -1
  23. package/dist/src/index.d.ts +18 -1
  24. package/dist/src/index.js +2 -1
  25. package/dist/src/index.js.map +3 -3
  26. package/dist/src/node/local-client.d.ts +16 -0
  27. package/dist/src/runtime/index.d.ts +137 -18
  28. package/dist/src/runtime/index.js +261 -41
  29. package/dist/src/runtime/index.js.map +9 -7
  30. package/migrations/0120_sync_scopes.ts +42 -0
  31. package/package.json +6 -6
  32. package/dist/src/x402.d.ts +0 -47
  33. package/dist/src/x402.js +0 -79
  34. package/dist/src/x402.js.map +0 -10
@@ -155,13 +155,11 @@ declare function reportRuntimeStatus(input: {
155
155
  coverage?: CoverageState | string
156
156
  }): RuntimeStatusReport;
157
157
  declare function actionFor(plane: StatusPlane, state: string): string;
158
- /**
159
- * Backup bundle — consistent DB, pinned config, encrypted keys, handler
160
- * manifests, and scope. Restore onto a wiped host must deep-verify green.
161
- */
158
+ declare const BACKUP_MANIFEST_VERSION = 1;
162
159
  declare const BACKUP_PARTS: readonly ["db", "config", "keys", "handlers", "scope"];
163
160
  type BackupPart = (typeof BACKUP_PARTS)[number];
164
161
  type BackupManifest = {
162
+ schema_version: number
165
163
  created_at: string
166
164
  network: string
167
165
  parts: BackupPart[]
@@ -170,34 +168,90 @@ type BackupManifest = {
170
168
  from_height: number
171
169
  to_height: number | null
172
170
  }
171
+ db: {
172
+ file: string
173
+ sha256: string
174
+ bytes: number
175
+ format: string
176
+ } | null
177
+ /** Never the secrets themselves — only how to check them. */
178
+ secrets: {
179
+ file: string
180
+ encrypted: boolean
181
+ canary: string
182
+ } | null
183
+ signature?: string
184
+ key_id?: string
173
185
  };
174
186
  type BackupPlan = {
175
- ok: boolean
187
+ ok: true
176
188
  manifest: BackupManifest
189
+ } | {
190
+ ok: false
177
191
  reason: string
178
192
  };
193
+ /**
194
+ * Validate a backup before any bytes are written. Refusing early is the whole
195
+ * point: a bundle that omits the keys looks like a backup and is not one.
196
+ */
179
197
  declare function planBackup(input: {
180
198
  network: string
181
- encryptedKeys: boolean
182
199
  fromHeight: number
183
200
  toHeight?: number | null
201
+ includeSecrets: boolean
202
+ secretsEncrypted: boolean
203
+ canary?: string
184
204
  now?: string
185
205
  }): BackupPlan;
186
- type RestoreResult = {
206
+ /**
207
+ * Seal the canary under an EXPLICIT key.
208
+ *
209
+ * Deliberately not reusing `crypto/secrets.ts`: that module resolves the key
210
+ * from the ambient environment and, in OSS mode, generates and persists one as
211
+ * a side effect when it is missing. Calling it here would mean a backup could
212
+ * mint the very key it is supposed to be preserving.
213
+ */
214
+ declare function sealKeyCanary(secretsKeyHex: string): string;
215
+ /** True only when `secretsKeyHex` is the key the canary was sealed with. */
216
+ declare function keyMatchesCanary(canaryBase64: string, secretsKeyHex: string): boolean;
217
+ /**
218
+ * Encrypt the secrets bundle under an operator passphrase.
219
+ * Layout: `salt(16) || iv(12) || authTag(16) || ciphertext`.
220
+ */
221
+ declare function encryptBundle(plaintext: string, passphrase: string): Buffer;
222
+ declare function decryptBundle(envelope: Buffer, passphrase: string): string;
223
+ type RestorePrecheck = {
187
224
  ok: true
188
- applied: boolean
189
- deep_green: boolean
190
225
  } | {
191
226
  ok: false
192
227
  reason: string
228
+ fatal: boolean
193
229
  };
194
- declare function restoreBackup(plan: BackupPlan, opts?: {
195
- apply?: boolean
196
- deepVerify?: boolean
197
- }): RestoreResult;
230
+ /**
231
+ * Everything that must hold before a restore writes anything.
232
+ *
233
+ * `fatal` distinguishes "this bundle can never restore here" from "this needs
234
+ * an explicit override" — the caller surfaces them differently.
235
+ */
236
+ declare function precheckRestore(input: {
237
+ manifest: BackupManifest
238
+ targetNetwork: string
239
+ targetIsEmpty: boolean
240
+ force: boolean
241
+ secretsKeyHex?: string
242
+ }): RestorePrecheck;
198
243
  /**
199
244
  * Upgrade contract — pin image, preflight schema/disk, backup first,
200
245
  * migrate, health/verify, document rollback limits.
246
+ *
247
+ * PLANNER ONLY — `applyUpgrade` returns `{applied:true}` without pulling an
248
+ * image, running a preflight, or migrating anything. Nothing in the product
249
+ * calls it. The upgrade operators actually perform is `docker compose pull &&
250
+ * up -d`, with migrations applied at boot by the runtime entrypoint.
251
+ *
252
+ * Same caution as the sibling backup planner: this step list was published as
253
+ * an operator procedure in the self-host docs until 2026-08-15. Make it do the
254
+ * work before presenting it as one.
201
255
  */
202
256
  declare const UPGRADE_STEPS: readonly ["pin", "preflight", "backup", "migrate", "health", "verify"];
203
257
  type UpgradeStep = (typeof UPGRADE_STEPS)[number];
@@ -227,6 +281,13 @@ declare function applyUpgrade(plan: UpgradePlan, opts?: {
227
281
  /**
228
282
  * Resource guardrails — preflight RAM/disk/Postgres and estimate growth.
229
283
  * Constrained fixtures fail early.
284
+ *
285
+ * The disk numbers here were wrong by 3-6x until 2026-08-15: they promised a
286
+ * full-history mainnet index would fit in 80 GB, and estimated growth at 1 GB
287
+ * per 100k blocks. The reference index measured 504 GB at height 8,771,873.
288
+ * An operator who sized a box from these figures would have filled the disk
289
+ * partway through their first bootstrap, so every constant below now cites what
290
+ * it was measured against.
230
291
  */
231
292
  type ResourceSnapshot = {
232
293
  ramMb: number
@@ -234,23 +295,81 @@ type ResourceSnapshot = {
234
295
  postgresMaxConnections: number
235
296
  postgresSharedBuffersMb?: number
236
297
  };
298
+ type DiskEstimates = {
299
+ /** blocks + transactions + events, no decoded plane. */
300
+ diskGbPer100kBlocks: number
301
+ /** Same, plus a broad decoder set writing `decoded_events`. */
302
+ diskGbPer100kBlocksDecoded: number
303
+ };
237
304
  type GuardrailResult = {
238
305
  ok: true
239
- estimates: {
240
- diskGbPer100kBlocks: number
241
- }
306
+ estimates: DiskEstimates
242
307
  } | {
243
308
  ok: false
244
309
  errors: string[]
245
310
  };
311
+ type GuardrailNetwork = "mainnet" | "testnet" | "devnet";
246
312
  declare const FLOORS: {
247
313
  readonly appRamMb: 4096
248
314
  readonly fullRamMb: 98304
315
+ /**
316
+ * Mainnet full history: 504 GB measured today, growing ~0.5 GB/day at the
317
+ * observed block rate. 600 leaves roughly six months of headroom, which is
318
+ * the point of a floor — a box that fits exactly today fails within weeks.
319
+ */
320
+ readonly mainnetAppDiskGb: 600
321
+ /**
322
+ * Testnet/devnet carry a fraction of mainnet's history. Unmeasured; retained
323
+ * as the long-standing default rather than replaced with a fresh guess.
324
+ */
249
325
  readonly appDiskGb: 80
326
+ /**
327
+ * Bundled node on mainnet. The Stacks chainstate alone measured 1.3 TB on
328
+ * the reference node (2026-08-15), and the index adds ~500 GB; bitcoind is
329
+ * additional and not measured here. The previous 1.5 TB floor was below the
330
+ * two components we HAVE measured.
331
+ */
332
+ readonly mainnetFullDiskGb: 2500
333
+ /** Bundled node, non-mainnet. Unmeasured; retained default. */
250
334
  readonly fullDiskGb: 1500
251
335
  readonly postgresMaxConnections: 50
252
336
  };
253
- declare function preflightResources(snap: ResourceSnapshot, mode: "external" | "stacks" | "full"): GuardrailResult;
337
+ /** The disk floor that applies to a given mode and network. */
338
+ declare function diskFloorGb(mode: "external" | "stacks" | "full", network: GuardrailNetwork): number;
339
+ /**
340
+ * A dimension the caller could not measure is absent, not zero. Skipping it
341
+ * keeps an unreadable `statfs` from presenting as a 0 GB disk and refusing a
342
+ * perfectly adequate box.
343
+ */
344
+ declare function preflightResources(snap: Partial<ResourceSnapshot>, mode: "external" | "stacks" | "full", network?: GuardrailNetwork): GuardrailResult;
345
+ type MeasuredResources = {
346
+ snapshot: Partial<ResourceSnapshot>
347
+ /** Dimensions we could not read, with why. Never guessed at. */
348
+ unknown: string[]
349
+ };
350
+ declare function measureResources(input: {
351
+ dataDir: string
352
+ query?: (sql: string) => Promise<Array<Record<string, unknown>>>
353
+ }): Promise<MeasuredResources>;
354
+ declare const UNDERSIZED_OVERRIDE = "SECONDLAYER_ALLOW_UNDERSIZED";
355
+ type PreflightDecision = {
356
+ action: "pass" | "warn" | "refuse"
357
+ messages: string[]
358
+ };
359
+ declare function decidePreflight(input: {
360
+ measured: MeasuredResources
361
+ mode: "external" | "stacks" | "full"
362
+ network: GuardrailNetwork
363
+ env?: Record<string, string | undefined>
364
+ }): PreflightDecision;
365
+ /** Measure this box and decide. Returns the decision; the caller exits. */
366
+ declare function runPreflight(input: {
367
+ dataDir: string
368
+ mode: "external" | "stacks" | "full"
369
+ network: GuardrailNetwork
370
+ query?: (sql: string) => Promise<Array<Record<string, unknown>>>
371
+ env?: Record<string, string | undefined>
372
+ }): Promise<PreflightDecision>;
254
373
  declare const MODULE_COMMANDS: Record<ModuleId, readonly string[]>;
255
374
  declare function commandFor(id: ModuleId): readonly string[];
256
- export { restoreBackup, reportRuntimeStatus, preflightResources, planUpgrade, planBackup, parseRuntimeConfig, migrateEmbedded, isolateHandler, isModuleId, emptyHealth, createSupervisor, createMemoryModule, createMemoryMigrationRunner, commandFor, applyUpgrade, actionFor, UpgradeStep, UpgradePlan, UPGRADE_STEPS, SupervisorHealth, Supervisor, StatusPlane, STATUS_PLANES, SECRET_KEYS, RuntimeStatusReport, RuntimeNetwork, RuntimeModule, RuntimeConfig, RestoreResult, ResourceSnapshot, RequiredConfigKey, REQUIRED_KEYS, PlaneStatus, PUBLISHER_PROFILE, OPTIONAL_KEYS, NodeMode, NODE_MODES, NETWORKS, ModuleState, ModuleId, ModuleHealth, MigrationRunner, MODULE_STATES, MODULE_IDS, MODULE_COMMANDS, MIGRATION_LOCK_KEY, IsolationResult, IsolationOomError, IsolationNetworkError, IsolationLimits, IsolationKillError, IsolationFault, ISOLATION_FAULTS, GuardrailResult, FLOORS, EmbeddedMigration, DEFAULT_PROFILE, ConfigResult, BackupPlan, BackupPart, BackupManifest, BACKUP_PARTS };
375
+ export { sealKeyCanary, runPreflight, reportRuntimeStatus, preflightResources, precheckRestore, planUpgrade, planBackup, parseRuntimeConfig, migrateEmbedded, measureResources, keyMatchesCanary, isolateHandler, isModuleId, encryptBundle, emptyHealth, diskFloorGb, decryptBundle, decidePreflight, createSupervisor, createMemoryModule, createMemoryMigrationRunner, commandFor, applyUpgrade, actionFor, UpgradeStep, UpgradePlan, UPGRADE_STEPS, UNDERSIZED_OVERRIDE, SupervisorHealth, Supervisor, StatusPlane, STATUS_PLANES, SECRET_KEYS, RuntimeStatusReport, RuntimeNetwork, RuntimeModule, RuntimeConfig, RestorePrecheck, ResourceSnapshot, RequiredConfigKey, REQUIRED_KEYS, PreflightDecision, PlaneStatus, PUBLISHER_PROFILE, OPTIONAL_KEYS, NodeMode, NODE_MODES, NETWORKS, ModuleState, ModuleId, ModuleHealth, MigrationRunner, MeasuredResources, MODULE_STATES, MODULE_IDS, MODULE_COMMANDS, MIGRATION_LOCK_KEY, IsolationResult, IsolationOomError, IsolationNetworkError, IsolationLimits, IsolationKillError, IsolationFault, ISOLATION_FAULTS, GuardrailResult, GuardrailNetwork, FLOORS, EmbeddedMigration, DiskEstimates, DEFAULT_PROFILE, ConfigResult, BackupPlan, BackupPart, BackupManifest, BACKUP_PARTS, BACKUP_MANIFEST_VERSION };
@@ -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,149 @@ 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 };
544
- return {
545
- ok: true,
546
- applied: true,
547
- deep_green: opts.deepVerify !== false
548
- };
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;
549
571
  }
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
- };
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 };
558
664
  }
559
665
  // src/runtime/upgrade.ts
560
666
  var UPGRADE_STEPS = [
@@ -634,29 +740,133 @@ function applyUpgrade(plan, opts) {
634
740
  return { ok: true, applied: true, reason: "upgrade applied" };
635
741
  }
636
742
  // src/runtime/guardrails.ts
743
+ var MEASURED_DISK = {
744
+ diskGbPer100kBlocks: 3,
745
+ diskGbPer100kBlocksDecoded: 6
746
+ };
637
747
  var FLOORS = {
638
748
  appRamMb: 4096,
639
749
  fullRamMb: 98304,
750
+ mainnetAppDiskGb: 600,
640
751
  appDiskGb: 80,
752
+ mainnetFullDiskGb: 2500,
641
753
  fullDiskGb: 1500,
642
754
  postgresMaxConnections: 50
643
755
  };
644
- function preflightResources(snap, mode) {
756
+ function diskFloorGb(mode, network) {
757
+ if (mode === "full") {
758
+ return network === "mainnet" ? FLOORS.mainnetFullDiskGb : FLOORS.fullDiskGb;
759
+ }
760
+ return network === "mainnet" ? FLOORS.mainnetAppDiskGb : FLOORS.appDiskGb;
761
+ }
762
+ function preflightResources(snap, mode, network = "mainnet") {
645
763
  const errors = [];
646
764
  const ramFloor = mode === "full" ? FLOORS.fullRamMb : FLOORS.appRamMb;
647
- const diskFloor = mode === "full" ? FLOORS.fullDiskGb : FLOORS.appDiskGb;
648
- if (snap.ramMb < ramFloor) {
765
+ const diskFloor = diskFloorGb(mode, network);
766
+ if (snap.ramMb !== undefined && snap.ramMb < ramFloor) {
649
767
  errors.push(`RAM ${snap.ramMb}MB below ${ramFloor}MB for NODE_MODE=${mode}`);
650
768
  }
651
- if (snap.diskGb < diskFloor) {
652
- errors.push(`disk ${snap.diskGb}GB below ${diskFloor}GB for NODE_MODE=${mode}`);
769
+ if (snap.diskGb !== undefined && snap.diskGb < diskFloor) {
770
+ errors.push(`disk ${snap.diskGb}GB below ${diskFloor}GB for NODE_MODE=${mode} on ${network}`);
653
771
  }
654
- if (snap.postgresMaxConnections < FLOORS.postgresMaxConnections) {
772
+ if (snap.postgresMaxConnections !== undefined && snap.postgresMaxConnections < FLOORS.postgresMaxConnections) {
655
773
  errors.push(`Postgres max_connections ${snap.postgresMaxConnections} below ${FLOORS.postgresMaxConnections}`);
656
774
  }
657
775
  if (errors.length > 0)
658
776
  return { ok: false, errors };
659
- return { ok: true, estimates: { diskGbPer100kBlocks: 1 } };
777
+ return { ok: true, estimates: MEASURED_DISK };
778
+ }
779
+ // src/runtime/resources.ts
780
+ import { statfs } from "node:fs/promises";
781
+ import { totalmem } from "node:os";
782
+ var BYTES_PER_MB = 1024 * 1024;
783
+ var BYTES_PER_GB = 1024 * 1024 * 1024;
784
+ async function measureRamMb() {
785
+ try {
786
+ const total = totalmem();
787
+ return total > 0 ? Math.floor(total / BYTES_PER_MB) : null;
788
+ } catch {
789
+ return null;
790
+ }
791
+ }
792
+ async function measureDiskGb(path) {
793
+ try {
794
+ const stats = await statfs(path);
795
+ const totalBytes = Number(stats.blocks) * Number(stats.bsize);
796
+ return totalBytes > 0 ? Math.floor(totalBytes / BYTES_PER_GB) : null;
797
+ } catch {
798
+ return null;
799
+ }
800
+ }
801
+ async function measurePostgresMaxConnections(query) {
802
+ try {
803
+ const rows = await query("SHOW max_connections");
804
+ const raw = rows[0]?.max_connections;
805
+ const value = Number(raw);
806
+ return Number.isFinite(value) && value > 0 ? value : null;
807
+ } catch {
808
+ return null;
809
+ }
810
+ }
811
+ async function measureResources(input) {
812
+ const unknown = [];
813
+ const snapshot = {};
814
+ const ramMb = await measureRamMb();
815
+ if (ramMb === null)
816
+ unknown.push("RAM");
817
+ else
818
+ snapshot.ramMb = ramMb;
819
+ const diskGb = await measureDiskGb(input.dataDir);
820
+ if (diskGb === null)
821
+ unknown.push(`disk (${input.dataDir} unreadable)`);
822
+ else
823
+ snapshot.diskGb = diskGb;
824
+ if (input.query) {
825
+ const conns = await measurePostgresMaxConnections(input.query);
826
+ if (conns === null)
827
+ unknown.push("Postgres max_connections");
828
+ else
829
+ snapshot.postgresMaxConnections = conns;
830
+ } else {
831
+ unknown.push("Postgres max_connections (no database handle)");
832
+ }
833
+ return { snapshot, unknown };
834
+ }
835
+
836
+ // src/runtime/preflight.ts
837
+ var UNDERSIZED_OVERRIDE = "SECONDLAYER_ALLOW_UNDERSIZED";
838
+ function isOverridden(env) {
839
+ return env[UNDERSIZED_OVERRIDE]?.trim().toLowerCase() === "true";
840
+ }
841
+ function decidePreflight(input) {
842
+ const env = input.env ?? {};
843
+ const result = preflightResources(input.measured.snapshot, input.mode, input.network);
844
+ const messages = [];
845
+ if (input.measured.unknown.length > 0) {
846
+ messages.push(`resource preflight could not measure: ${input.measured.unknown.join(", ")}`);
847
+ }
848
+ if (result.ok) {
849
+ return { action: messages.length > 0 ? "warn" : "pass", messages };
850
+ }
851
+ messages.push(...result.errors);
852
+ if (isOverridden(env)) {
853
+ messages.push(`${UNDERSIZED_OVERRIDE}=true — starting anyway. This box is below the documented floor; expect to run out of disk as history accumulates.`);
854
+ return { action: "warn", messages };
855
+ }
856
+ 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`);
857
+ return { action: "refuse", messages };
858
+ }
859
+ async function runPreflight(input) {
860
+ const measured = await measureResources({
861
+ dataDir: input.dataDir,
862
+ query: input.query
863
+ });
864
+ return decidePreflight({
865
+ measured,
866
+ mode: input.mode,
867
+ network: input.network,
868
+ env: input.env
869
+ });
660
870
  }
661
871
  // src/runtime/commands.ts
662
872
  var MODULE_COMMANDS = {
@@ -676,16 +886,24 @@ function commandFor(id) {
676
886
  return MODULE_COMMANDS[id];
677
887
  }
678
888
  export {
679
- restoreBackup,
889
+ sealKeyCanary,
890
+ runPreflight,
680
891
  reportRuntimeStatus,
681
892
  preflightResources,
893
+ precheckRestore,
682
894
  planUpgrade,
683
895
  planBackup,
684
896
  parseRuntimeConfig,
685
897
  migrateEmbedded,
898
+ measureResources,
899
+ keyMatchesCanary,
686
900
  isolateHandler,
687
901
  isModuleId,
902
+ encryptBundle,
688
903
  emptyHealth,
904
+ diskFloorGb,
905
+ decryptBundle,
906
+ decidePreflight,
689
907
  createSupervisor,
690
908
  createMemoryModule,
691
909
  createMemoryMigrationRunner,
@@ -693,6 +911,7 @@ export {
693
911
  applyUpgrade,
694
912
  actionFor,
695
913
  UPGRADE_STEPS,
914
+ UNDERSIZED_OVERRIDE,
696
915
  STATUS_PLANES,
697
916
  SECRET_KEYS,
698
917
  REQUIRED_KEYS,
@@ -710,8 +929,9 @@ export {
710
929
  ISOLATION_FAULTS,
711
930
  FLOORS,
712
931
  DEFAULT_PROFILE,
713
- BACKUP_PARTS
932
+ BACKUP_PARTS,
933
+ BACKUP_MANIFEST_VERSION
714
934
  };
715
935
 
716
- //# debugId=0C264B17247A3CFD64756E2164756E21
936
+ //# debugId=CDAC30CBB14C2F1364756E2164756E21
717
937
  //# sourceMappingURL=index.js.map