@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.
- package/dist/src/archive/range-digest.d.ts +16 -0
- package/dist/src/archive/semantic-digest-builder.d.ts +16 -0
- package/dist/src/archive/status.js +11 -3
- package/dist/src/archive/status.js.map +3 -3
- package/dist/src/coverage/index.d.ts +15 -0
- package/dist/src/db/index.d.ts +18 -1
- package/dist/src/db/index.js +2 -1
- package/dist/src/db/index.js.map +3 -3
- package/dist/src/db/queries/chain-reorgs.d.ts +16 -0
- package/dist/src/db/queries/chain-reorgs.js +2 -1
- package/dist/src/db/queries/chain-reorgs.js.map +3 -3
- package/dist/src/db/queries/contracts.d.ts +16 -0
- package/dist/src/db/queries/instance.d.ts +16 -0
- package/dist/src/db/queries/integrity.d.ts +16 -0
- package/dist/src/db/queries/subgraph-gaps.d.ts +16 -0
- package/dist/src/db/queries/subgraph-operations.d.ts +16 -0
- package/dist/src/db/queries/subgraphs.d.ts +16 -0
- package/dist/src/db/queries/subscriptions.d.ts +16 -0
- package/dist/src/db/queries/sync-scope.d.ts +1006 -0
- package/dist/src/db/queries/sync-scope.js +116 -0
- package/dist/src/db/queries/sync-scope.js.map +10 -0
- package/dist/src/db/schema.d.ts +17 -1
- package/dist/src/index.d.ts +18 -1
- package/dist/src/index.js +2 -1
- package/dist/src/index.js.map +3 -3
- package/dist/src/node/local-client.d.ts +16 -0
- package/dist/src/runtime/index.d.ts +137 -18
- package/dist/src/runtime/index.js +261 -41
- package/dist/src/runtime/index.js.map +9 -7
- package/migrations/0120_sync_scopes.ts +42 -0
- package/package.json +6 -6
- package/dist/src/x402.d.ts +0 -47
- package/dist/src/x402.js +0 -79
- 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:
|
|
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
|
-
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
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 {
|
|
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 (
|
|
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
|
-
|
|
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.
|
|
541
|
+
if (input.includeSecrets && !input.canary) {
|
|
518
542
|
return {
|
|
519
543
|
ok: false,
|
|
520
|
-
|
|
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
|
-
|
|
551
|
+
schema_version: BACKUP_MANIFEST_VERSION,
|
|
552
|
+
created_at: input.now ?? new Date().toISOString(),
|
|
528
553
|
network: input.network,
|
|
529
|
-
parts
|
|
530
|
-
encrypted:
|
|
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
|
-
|
|
559
|
+
},
|
|
560
|
+
db: null,
|
|
561
|
+
secrets: null
|
|
562
|
+
}
|
|
537
563
|
};
|
|
538
564
|
}
|
|
539
|
-
function
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
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
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
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
|
|
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
|
|
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:
|
|
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
|
-
|
|
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=
|
|
936
|
+
//# debugId=CDAC30CBB14C2F1364756E2164756E21
|
|
717
937
|
//# sourceMappingURL=index.js.map
|