@secondlayer/cli 20.1.2 → 21.1.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/README.md +6 -22
- package/dist/cli.js +216 -1246
- package/dist/cli.js.map +10 -15
- package/package.json +6 -6
package/dist/cli.js
CHANGED
|
@@ -29020,6 +29020,33 @@ function sourcesNeedingAbi(sources) {
|
|
|
29020
29020
|
}
|
|
29021
29021
|
return out;
|
|
29022
29022
|
}
|
|
29023
|
+
function sourcesNamingState(sources) {
|
|
29024
|
+
if (!sources || typeof sources !== "object")
|
|
29025
|
+
return [];
|
|
29026
|
+
const out = [];
|
|
29027
|
+
for (const [name, raw] of Object.entries(sources)) {
|
|
29028
|
+
const src = raw;
|
|
29029
|
+
if (typeof src?.contractId !== "string" || src.contractId.includes("*") || src.trait !== undefined) {
|
|
29030
|
+
continue;
|
|
29031
|
+
}
|
|
29032
|
+
if ((src.type === "map_set" || src.type === "map_insert" || src.type === "map_delete") && typeof src.map === "string") {
|
|
29033
|
+
out.push({
|
|
29034
|
+
name,
|
|
29035
|
+
contractId: src.contractId,
|
|
29036
|
+
kind: "map",
|
|
29037
|
+
stateName: src.map
|
|
29038
|
+
});
|
|
29039
|
+
} else if (src.type === "var_set" && typeof src.varName === "string") {
|
|
29040
|
+
out.push({
|
|
29041
|
+
name,
|
|
29042
|
+
contractId: src.contractId,
|
|
29043
|
+
kind: "var",
|
|
29044
|
+
stateName: src.varName
|
|
29045
|
+
});
|
|
29046
|
+
}
|
|
29047
|
+
}
|
|
29048
|
+
return out;
|
|
29049
|
+
}
|
|
29023
29050
|
async function withDerivedAbis(source, client = new StacksApiClient) {
|
|
29024
29051
|
let sources;
|
|
29025
29052
|
try {
|
|
@@ -29028,16 +29055,22 @@ async function withDerivedAbis(source, client = new StacksApiClient) {
|
|
|
29028
29055
|
return { source, abis: {} };
|
|
29029
29056
|
}
|
|
29030
29057
|
const needed = sourcesNeedingAbi(sources);
|
|
29031
|
-
|
|
29058
|
+
const stateSources = sourcesNamingState(sources);
|
|
29059
|
+
if (needed.length === 0 && stateSources.length === 0) {
|
|
29032
29060
|
return { source, abis: {} };
|
|
29061
|
+
}
|
|
29033
29062
|
const byContract = new Map;
|
|
29034
29063
|
const abis = {};
|
|
29035
|
-
|
|
29064
|
+
const interfaceOf = (contractId) => {
|
|
29036
29065
|
let pending = byContract.get(contractId);
|
|
29037
29066
|
if (!pending) {
|
|
29038
29067
|
pending = client.getContractInfo(contractId).then(normalizeAbi3);
|
|
29039
29068
|
byContract.set(contractId, pending);
|
|
29040
29069
|
}
|
|
29070
|
+
return pending;
|
|
29071
|
+
};
|
|
29072
|
+
for (const { name, contractId } of needed) {
|
|
29073
|
+
const pending = interfaceOf(contractId);
|
|
29041
29074
|
try {
|
|
29042
29075
|
abis[name] = await pending;
|
|
29043
29076
|
} catch (err) {
|
|
@@ -29045,6 +29078,19 @@ async function withDerivedAbis(source, client = new StacksApiClient) {
|
|
|
29045
29078
|
throw new Error(`Could not fetch the abi for source "${name}" (${contractId}): ${reason}. Pass abi on the source instead (normalizeAbi() from @secondlayer/stacks/clarity).`);
|
|
29046
29079
|
}
|
|
29047
29080
|
}
|
|
29081
|
+
for (const { name, contractId, kind, stateName } of stateSources) {
|
|
29082
|
+
let contract;
|
|
29083
|
+
try {
|
|
29084
|
+
contract = await interfaceOf(contractId);
|
|
29085
|
+
} catch {
|
|
29086
|
+
continue;
|
|
29087
|
+
}
|
|
29088
|
+
const defined = ((kind === "map" ? contract.maps : contract.variables) ?? []).map((entry) => entry.name);
|
|
29089
|
+
if (!defined.includes(stateName)) {
|
|
29090
|
+
const noun = kind === "map" ? "map" : "variable";
|
|
29091
|
+
throw new Error(`Source "${name}" reads ${noun} "${stateName}", which ${contractId} does not define. Its ${noun}s: ${defined.join(", ") || "none"}.`);
|
|
29092
|
+
}
|
|
29093
|
+
}
|
|
29048
29094
|
return { source: injectSourceAbis(source, abis), abis };
|
|
29049
29095
|
}
|
|
29050
29096
|
var init_derive_abi = __esm(() => {
|
|
@@ -29423,7 +29469,7 @@ var init_git_status = () => {};
|
|
|
29423
29469
|
|
|
29424
29470
|
// ../../node_modules/typescript/lib/typescript.js
|
|
29425
29471
|
var require_typescript = __commonJS(function(exports, module) {
|
|
29426
|
-
var __dirname = "/Users
|
|
29472
|
+
var __dirname = "/private/tmp/claude-501/-Users-ryan-projects-secondlayer/f3803aac-4d84-4ad1-b9ab-de61be6a7179/scratchpad/rel/node_modules/typescript/lib", __filename = "/private/tmp/claude-501/-Users-ryan-projects-secondlayer/f3803aac-4d84-4ad1-b9ab-de61be6a7179/scratchpad/rel/node_modules/typescript/lib/typescript.js";
|
|
29427
29473
|
/*! *****************************************************************************
|
|
29428
29474
|
Copyright (c) Microsoft Corporation. All rights reserved.
|
|
29429
29475
|
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
|
@@ -202646,7 +202692,7 @@ var {
|
|
|
202646
202692
|
// package.json
|
|
202647
202693
|
var package_default = {
|
|
202648
202694
|
name: "@secondlayer/cli",
|
|
202649
|
-
version: "
|
|
202695
|
+
version: "21.1.0",
|
|
202650
202696
|
description: "Run a Stacks index on your own hardware — set it up, restore verified history, and keep it current.",
|
|
202651
202697
|
repository: {
|
|
202652
202698
|
type: "git",
|
|
@@ -202685,11 +202731,11 @@ var package_default = {
|
|
|
202685
202731
|
"@inquirer/prompts": "^8.2.0",
|
|
202686
202732
|
"@secondlayer/bundler": "^0.7.1",
|
|
202687
202733
|
"@secondlayer/scaffold": "^1.6.2",
|
|
202688
|
-
"@secondlayer/sdk": "^15.0.
|
|
202689
|
-
"@secondlayer/shared": "^
|
|
202690
|
-
"@secondlayer/stacks": "^6.
|
|
202691
|
-
"@secondlayer/subgraphs": "^7.0
|
|
202692
|
-
"@secondlayer/verify": "^0.
|
|
202734
|
+
"@secondlayer/sdk": "^15.0.3",
|
|
202735
|
+
"@secondlayer/shared": "^13.0.0",
|
|
202736
|
+
"@secondlayer/stacks": "^6.3.0",
|
|
202737
|
+
"@secondlayer/subgraphs": "^7.1.0",
|
|
202738
|
+
"@secondlayer/verify": "^0.6.0",
|
|
202693
202739
|
esbuild: "^0.28.2",
|
|
202694
202740
|
"smol-toml": "^1.9.1",
|
|
202695
202741
|
zod: "^4.3.6"
|
|
@@ -204523,13 +204569,12 @@ function statusLabel(status) {
|
|
|
204523
204569
|
return yellow("missing");
|
|
204524
204570
|
}
|
|
204525
204571
|
function attachVerifyCommand(cmd) {
|
|
204526
|
-
return cmd.argument("[target]", "all | raw
|
|
204572
|
+
return cmd.argument("[target]", "all | raw (decode:<name> and subgraph:<name> have no archive digests yet)", "raw").option("--quick", "coverage/identity only (default)").option("--deep", "include semantic / scratch replay where available").option("--anchor", "require a verified archive signature").option("--against <manifest>", "archive manifest: an https URL or a local file path (required)").option("--from-block <n>", "first height to check").option("--to-block <n>", "last height to check").option("--counts", "also compare transaction/event row counts (slower)").option("--semantic", "also recompute per-partition semantic digests locally and compare (slow: full re-stream)").option("--public-key <pem>", "pin a signing key; default is the archive key built into this release").option("--insecure", "skip the manifest signature check (not recommended)").option("--json", "Output as JSON").addHelpText("after", `
|
|
204527
204573
|
Examples:
|
|
204528
204574
|
$ secondlayer verify --against https://archive.secondlayer.tools/.../snapshots/<digest>.json
|
|
204529
204575
|
$ secondlayer verify raw --against ./snapshot.json --from-block 8000000 --to-block 8499999
|
|
204530
204576
|
$ secondlayer verify all --against ./snapshot.json --json
|
|
204531
|
-
$ secondlayer verify
|
|
204532
|
-
$ secondlayer verify subgraph:sbtc --against ./snapshot.json --deep
|
|
204577
|
+
$ secondlayer verify subgraph <name> --replay
|
|
204533
204578
|
|
|
204534
204579
|
Exit codes:
|
|
204535
204580
|
0 local data matches the archive
|
|
@@ -204577,9 +204622,28 @@ Exit codes:
|
|
|
204577
204622
|
});
|
|
204578
204623
|
process.exit(VERIFY_EXIT.UNANCHORED);
|
|
204579
204624
|
}
|
|
204580
|
-
const
|
|
204581
|
-
|
|
204582
|
-
|
|
204625
|
+
const unanchoredJson = (detail) => output({
|
|
204626
|
+
json: opts.json,
|
|
204627
|
+
data: {
|
|
204628
|
+
...reportVerify({ target, mode, anchored: false, detail }),
|
|
204629
|
+
status: "unanchored",
|
|
204630
|
+
reference: origin,
|
|
204631
|
+
reason: detail
|
|
204632
|
+
},
|
|
204633
|
+
human: () => {}
|
|
204634
|
+
});
|
|
204635
|
+
const published = (manifest.range_digests ?? []).filter((d) => datasetMatchesTarget(d.dataset, target));
|
|
204636
|
+
if (published.length === 0) {
|
|
204637
|
+
const label = target.kind === "decode" ? `decode:${target.name}` : target.kind === "subgraph" ? `subgraph:${target.name}` : target.kind;
|
|
204638
|
+
const datasets = [
|
|
204639
|
+
...new Set((manifest.range_digests ?? []).map((d) => d.dataset))
|
|
204640
|
+
].sort();
|
|
204641
|
+
const hint = target.kind === "decode" ? "Check the events decoders read from with: secondlayer verify raw --against <manifest>" : target.kind === "subgraph" ? "Recompute a state subgraph's rows with: secondlayer verify subgraph <name> --replay" : "Check the manifest you passed to --against.";
|
|
204642
|
+
printError(`This archive publishes no digests for ${label}. It has: ${datasets.join(", ") || "none"}.`, { hint });
|
|
204643
|
+
unanchoredJson("no digests");
|
|
204644
|
+
process.exit(VERIFY_EXIT.UNANCHORED);
|
|
204645
|
+
}
|
|
204646
|
+
const reference = published.filter((d) => {
|
|
204583
204647
|
if (fromBlock !== undefined && d.to_block < fromBlock)
|
|
204584
204648
|
return false;
|
|
204585
204649
|
if (toBlock !== undefined && d.from_block > toBlock)
|
|
@@ -204590,6 +204654,7 @@ Exit codes:
|
|
|
204590
204654
|
printError("The reference publishes no digests for that range.", {
|
|
204591
204655
|
hint: "Check --from-block/--to-block against the manifest's coverage."
|
|
204592
204656
|
});
|
|
204657
|
+
unanchoredJson("no digests for that range");
|
|
204593
204658
|
process.exit(VERIFY_EXIT.UNANCHORED);
|
|
204594
204659
|
}
|
|
204595
204660
|
const db = getDb3();
|
|
@@ -207809,268 +207874,19 @@ async function runLogout(opts = {}) {
|
|
|
207809
207874
|
function registerLogoutCommand(program) {
|
|
207810
207875
|
program.command("logout").description("Log out and revoke the local session").option("--credits", "Log out of the archive credits account used by bootstrap, repair, and credits").action((opts) => runLogout({ credits: opts.credits }));
|
|
207811
207876
|
}
|
|
207812
|
-
// src/commands/uninstall.ts
|
|
207813
|
-
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
207814
|
-
import { existsSync as existsSync9, readFileSync as readFileSync8 } from "node:fs";
|
|
207815
|
-
import { join as join14, resolve as resolve10 } from "node:path";
|
|
207816
|
-
import { planUninstall, uninstallCommand } from "@secondlayer/shared/runtime";
|
|
207817
|
-
|
|
207818
|
-
// src/lib/instance-init.ts
|
|
207819
|
-
init_output();
|
|
207820
|
-
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
207821
|
-
import { existsSync as existsSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "node:fs";
|
|
207822
|
-
import { resolve as resolve9 } from "node:path";
|
|
207823
|
-
import { ARCHIVE_ROOT_PUBLIC_KEY_PEM } from "@secondlayer/shared/archive/root-key";
|
|
207824
|
-
import { generateEd25519KeyPair } from "@secondlayer/shared/crypto/ed25519";
|
|
207825
|
-
import { INSTANCE_NETWORKS } from "@secondlayer/shared/db/queries/instance";
|
|
207826
|
-
var INSTANCE_ENV_FILE = ".env.local";
|
|
207827
|
-
function isHostedAccountKey(value) {
|
|
207828
|
-
return value.startsWith("sk-sl_") || value.startsWith("ss-sl_");
|
|
207829
|
-
}
|
|
207830
|
-
function parseInstanceNetwork(value) {
|
|
207831
|
-
const network = value.trim().toLowerCase();
|
|
207832
|
-
if (INSTANCE_NETWORKS.includes(network)) {
|
|
207833
|
-
return network;
|
|
207834
|
-
}
|
|
207835
|
-
throw new Error(`network must be mainnet, testnet, or devnet (got ${value})`);
|
|
207836
|
-
}
|
|
207837
|
-
function instanceNetworkFromEnv(fallback = "mainnet") {
|
|
207838
|
-
const raw = process.env.STACKS_NETWORK ?? fallback;
|
|
207839
|
-
try {
|
|
207840
|
-
return parseInstanceNetwork(raw);
|
|
207841
|
-
} catch (err) {
|
|
207842
|
-
printError(err instanceof Error ? err.message : String(err), {
|
|
207843
|
-
hint: "Pass --network mainnet, --network testnet, or --network devnet."
|
|
207844
|
-
});
|
|
207845
|
-
process.exit(1);
|
|
207846
|
-
}
|
|
207847
|
-
}
|
|
207848
|
-
function generateInstanceToken() {
|
|
207849
|
-
return randomBytes3(32).toString("hex");
|
|
207850
|
-
}
|
|
207851
|
-
function generateSecretsKey() {
|
|
207852
|
-
return randomBytes3(32).toString("hex");
|
|
207853
|
-
}
|
|
207854
|
-
function generateSigningPrivateKey() {
|
|
207855
|
-
return generateEd25519KeyPair().privateKeyPem;
|
|
207856
|
-
}
|
|
207857
|
-
function escapeEnvValue(value) {
|
|
207858
|
-
if (!/[\s#"']/.test(value) && !value.includes("\\"))
|
|
207859
|
-
return value;
|
|
207860
|
-
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n")}"`;
|
|
207861
|
-
}
|
|
207862
|
-
function unescapeEnvValue(raw) {
|
|
207863
|
-
let value = raw.trim();
|
|
207864
|
-
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
207865
|
-
value = value.slice(1, -1);
|
|
207866
|
-
}
|
|
207867
|
-
return value.replace(/\\n/g, `
|
|
207868
|
-
`).replace(/\\"/g, '"').replace(/\\\\/g, "\\");
|
|
207869
|
-
}
|
|
207870
|
-
function readEnvValue(envPath, key) {
|
|
207871
|
-
if (!existsSync8(envPath))
|
|
207872
|
-
return null;
|
|
207873
|
-
const match = readFileSync7(envPath, "utf8").match(new RegExp(`^${key}=(.*)$`, "m"));
|
|
207874
|
-
return match ? unescapeEnvValue(match[1]) : null;
|
|
207875
|
-
}
|
|
207876
|
-
function buildInstanceEnv(input) {
|
|
207877
|
-
const token = input.existing?.INSTANCE_TOKEN || generateInstanceToken();
|
|
207878
|
-
const secrets = input.existing?.SECONDLAYER_SECRETS_KEY || generateSecretsKey();
|
|
207879
|
-
const signing = input.existing?.STREAMS_SIGNING_PRIVATE_KEY || input.existing?.SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY || generateSigningPrivateKey();
|
|
207880
|
-
const apiUrl = input.existing?.SECONDLAYER_API_URL || input.apiUrl || "http://127.0.0.1:3800";
|
|
207881
|
-
const archiveKey = input.existing?.ARCHIVE_SIGNING_PUBLIC_KEY || input.archivePublicKeyPem || ARCHIVE_ROOT_PUBLIC_KEY_PEM;
|
|
207882
|
-
const hostedCandidate = input.existing?.SECONDLAYER_API_KEY || undefined;
|
|
207883
|
-
const hostedKey = hostedCandidate && isHostedAccountKey(hostedCandidate) ? hostedCandidate : undefined;
|
|
207884
|
-
return {
|
|
207885
|
-
INSTANCE_MODE: "oss",
|
|
207886
|
-
STACKS_NETWORK: input.network,
|
|
207887
|
-
INSTANCE_TOKEN: token,
|
|
207888
|
-
SECONDLAYER_SECRETS_KEY: secrets,
|
|
207889
|
-
STREAMS_SIGNING_PRIVATE_KEY: signing,
|
|
207890
|
-
SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY: signing,
|
|
207891
|
-
ALLOW_UNSIGNED_WEBHOOKS: "false",
|
|
207892
|
-
SECONDLAYER_API_URL: apiUrl,
|
|
207893
|
-
...hostedKey ? { SECONDLAYER_API_KEY: hostedKey } : {},
|
|
207894
|
-
ARCHIVE_SIGNING_PUBLIC_KEY: archiveKey
|
|
207895
|
-
};
|
|
207896
|
-
}
|
|
207897
|
-
function renderInstanceEnv(env) {
|
|
207898
|
-
const lines = [
|
|
207899
|
-
"# Generated by secondlayer init. Do not commit.",
|
|
207900
|
-
`INSTANCE_MODE=${env.INSTANCE_MODE}`,
|
|
207901
|
-
`STACKS_NETWORK=${env.STACKS_NETWORK}`,
|
|
207902
|
-
"# The instance credential. The API validates bearer tokens against it,",
|
|
207903
|
-
"# and the CLI/SDK/MCP read it first when authenticating.",
|
|
207904
|
-
`INSTANCE_TOKEN=${env.INSTANCE_TOKEN}`,
|
|
207905
|
-
`SECONDLAYER_SECRETS_KEY=${env.SECONDLAYER_SECRETS_KEY}`,
|
|
207906
|
-
`STREAMS_SIGNING_PRIVATE_KEY=${escapeEnvValue(env.STREAMS_SIGNING_PRIVATE_KEY)}`,
|
|
207907
|
-
`SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY=${escapeEnvValue(env.SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY)}`,
|
|
207908
|
-
`ALLOW_UNSIGNED_WEBHOOKS=${env.ALLOW_UNSIGNED_WEBHOOKS}`,
|
|
207909
|
-
`SECONDLAYER_API_URL=${env.SECONDLAYER_API_URL}`
|
|
207910
|
-
];
|
|
207911
|
-
if (env.SECONDLAYER_API_KEY) {
|
|
207912
|
-
lines.push("# Hosted account key for archive credits (sk-sl_*).", `SECONDLAYER_API_KEY=${env.SECONDLAYER_API_KEY}`);
|
|
207913
|
-
}
|
|
207914
|
-
lines.push("# Archive trust root: bootstrap, verify, and repair check manifests", "# against this key. Replace it to pin a different archive.", `ARCHIVE_SIGNING_PUBLIC_KEY=${escapeEnvValue(env.ARCHIVE_SIGNING_PUBLIC_KEY)}`, "");
|
|
207915
|
-
return lines.join(`
|
|
207916
|
-
`);
|
|
207917
|
-
}
|
|
207918
|
-
function writeInstanceEnv(cwd, env, file = INSTANCE_ENV_FILE) {
|
|
207919
|
-
const path = resolve9(cwd, file);
|
|
207920
|
-
writeFileSync6(path, renderInstanceEnv(env), { mode: 384 });
|
|
207921
|
-
return path;
|
|
207922
|
-
}
|
|
207923
|
-
function loadExistingInstanceEnv(cwd, file = INSTANCE_ENV_FILE) {
|
|
207924
|
-
const path = resolve9(cwd, file);
|
|
207925
|
-
return {
|
|
207926
|
-
INSTANCE_TOKEN: readEnvValue(path, "INSTANCE_TOKEN"),
|
|
207927
|
-
SECONDLAYER_SECRETS_KEY: readEnvValue(path, "SECONDLAYER_SECRETS_KEY"),
|
|
207928
|
-
STREAMS_SIGNING_PRIVATE_KEY: readEnvValue(path, "STREAMS_SIGNING_PRIVATE_KEY"),
|
|
207929
|
-
SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY: readEnvValue(path, "SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY"),
|
|
207930
|
-
SECONDLAYER_API_URL: readEnvValue(path, "SECONDLAYER_API_URL"),
|
|
207931
|
-
SECONDLAYER_API_KEY: readEnvValue(path, "SECONDLAYER_API_KEY"),
|
|
207932
|
-
ARCHIVE_SIGNING_PUBLIC_KEY: readEnvValue(path, "ARCHIVE_SIGNING_PUBLIC_KEY")
|
|
207933
|
-
};
|
|
207934
|
-
}
|
|
207935
|
-
|
|
207936
|
-
// src/commands/uninstall.ts
|
|
207937
|
-
init_output();
|
|
207938
|
-
init_resolve_auth();
|
|
207939
|
-
var UNINSTALL_EXIT = { OK: 0, FAILED: 1, REFUSED: 2 };
|
|
207940
|
-
var REPO_COMPOSE = "docker/oss/docker-compose.yml";
|
|
207941
|
-
function bundleHasKeys(bundleDir) {
|
|
207942
|
-
const manifest = join14(bundleDir, "manifest.json");
|
|
207943
|
-
if (!existsSync9(manifest))
|
|
207944
|
-
return false;
|
|
207945
|
-
try {
|
|
207946
|
-
const parsed = JSON.parse(readFileSync8(manifest, "utf8"));
|
|
207947
|
-
return parsed.secrets != null;
|
|
207948
|
-
} catch {
|
|
207949
|
-
return false;
|
|
207950
|
-
}
|
|
207951
|
-
}
|
|
207952
|
-
function resolveUninstallLayout(dir, composeOverride) {
|
|
207953
|
-
const setupCompose = resolve10(dir, "docker-compose.yml");
|
|
207954
|
-
const setupEnv = resolve10(dir, ".env");
|
|
207955
|
-
const initEnv = resolve10(dir, ".env.local");
|
|
207956
|
-
const secretsFile = [setupEnv, initEnv].find((file) => readEnvValue(file, "SECONDLAYER_SECRETS_KEY") !== null) ?? [setupEnv, initEnv].find((file) => existsSync9(file));
|
|
207957
|
-
if (composeOverride) {
|
|
207958
|
-
const composeFile = resolve10(dir, composeOverride);
|
|
207959
|
-
const sibling = resolve10(composeFile, "..", ".env");
|
|
207960
|
-
return {
|
|
207961
|
-
composeFile,
|
|
207962
|
-
envFile: existsSync9(sibling) ? sibling : undefined,
|
|
207963
|
-
secretsFile
|
|
207964
|
-
};
|
|
207965
|
-
}
|
|
207966
|
-
if (existsSync9(setupCompose)) {
|
|
207967
|
-
return {
|
|
207968
|
-
composeFile: setupCompose,
|
|
207969
|
-
envFile: existsSync9(setupEnv) ? setupEnv : undefined,
|
|
207970
|
-
secretsFile
|
|
207971
|
-
};
|
|
207972
|
-
}
|
|
207973
|
-
return { composeFile: resolve10(dir, REPO_COMPOSE), secretsFile };
|
|
207974
|
-
}
|
|
207975
|
-
function composeDownArgs(plan, layout) {
|
|
207976
|
-
const base = uninstallCommand(plan, layout.composeFile);
|
|
207977
|
-
if (!layout.envFile)
|
|
207978
|
-
return base;
|
|
207979
|
-
const fileFlagEnd = base.indexOf("-f") + 2;
|
|
207980
|
-
return [
|
|
207981
|
-
...base.slice(0, fileFlagEnd),
|
|
207982
|
-
"--env-file",
|
|
207983
|
-
layout.envFile,
|
|
207984
|
-
...base.slice(fileFlagEnd)
|
|
207985
|
-
];
|
|
207986
|
-
}
|
|
207987
|
-
function registerUninstallCommand(program) {
|
|
207988
|
-
program.command("uninstall").description("Stop the stack; the index, chainstate, and keys survive").option("--compose <file>", `compose file to tear down (default: ./docker-compose.yml from setup, else ${REPO_COMPOSE})`).option("--purge", "also destroy the volumes (wipes the index)").option("--yes", "confirm a purge").option("--backup <dir>", "a bundle proving the keys exist elsewhere (required to purge)").option("--apply", "actually run it (default is a dry run)").option("--json", "Output as JSON").action(async (opts) => {
|
|
207989
|
-
assertInstanceUrl();
|
|
207990
|
-
const dataDir = process.env.DATA_DIR ?? "./data";
|
|
207991
|
-
const layout = resolveUninstallLayout(process.cwd(), opts.compose);
|
|
207992
|
-
const secretsPresent = !!process.env.SECONDLAYER_SECRETS_KEY || !!layout.secretsFile;
|
|
207993
|
-
const keysBackedUp = opts.backup ? bundleHasKeys(resolve10(opts.backup)) : false;
|
|
207994
|
-
const decision = planUninstall({
|
|
207995
|
-
purge: opts.purge === true,
|
|
207996
|
-
confirmed: opts.yes === true,
|
|
207997
|
-
keysBackedUp,
|
|
207998
|
-
secretsPresent,
|
|
207999
|
-
dataDir
|
|
208000
|
-
});
|
|
208001
|
-
if (!decision.ok) {
|
|
208002
|
-
printError(decision.reason, {
|
|
208003
|
-
hint: layout.secretsFile ? `keys found in ${layout.secretsFile}` : undefined
|
|
208004
|
-
});
|
|
208005
|
-
process.exit(UNINSTALL_EXIT.REFUSED);
|
|
208006
|
-
}
|
|
208007
|
-
const cmd = composeDownArgs(decision.plan, layout);
|
|
208008
|
-
if (!opts.apply) {
|
|
208009
|
-
output({
|
|
208010
|
-
json: opts.json,
|
|
208011
|
-
data: {
|
|
208012
|
-
dryRun: true,
|
|
208013
|
-
plan: decision.plan,
|
|
208014
|
-
command: cmd,
|
|
208015
|
-
composeFile: layout.composeFile,
|
|
208016
|
-
envFile: layout.envFile ?? null,
|
|
208017
|
-
secretsFile: layout.secretsFile ?? null
|
|
208018
|
-
},
|
|
208019
|
-
human: () => {
|
|
208020
|
-
note("dry run — nothing removed. Pass --apply to proceed.");
|
|
208021
|
-
note(`compose: ${layout.composeFile}`);
|
|
208022
|
-
if (layout.envFile)
|
|
208023
|
-
note(`env: ${layout.envFile}`);
|
|
208024
|
-
if (layout.secretsFile)
|
|
208025
|
-
note(`keys: ${layout.secretsFile}`);
|
|
208026
|
-
note(`would run: docker ${cmd.join(" ")}`);
|
|
208027
|
-
for (const p of decision.plan.preserves) {
|
|
208028
|
-
note(`preserved · ${p.what}: ${p.detail}`);
|
|
208029
|
-
}
|
|
208030
|
-
for (const d of decision.plan.destroys) {
|
|
208031
|
-
warn(`DESTROYED · volume ${d}`);
|
|
208032
|
-
}
|
|
208033
|
-
}
|
|
208034
|
-
});
|
|
208035
|
-
return;
|
|
208036
|
-
}
|
|
208037
|
-
const res = spawnSync4("docker", cmd, { stdio: "inherit" });
|
|
208038
|
-
if ((res.status ?? 1) !== 0) {
|
|
208039
|
-
printError(`docker ${cmd.join(" ")} exited ${res.status}`);
|
|
208040
|
-
process.exit(UNINSTALL_EXIT.FAILED);
|
|
208041
|
-
}
|
|
208042
|
-
output({
|
|
208043
|
-
json: opts.json,
|
|
208044
|
-
data: {
|
|
208045
|
-
removed: true,
|
|
208046
|
-
plan: decision.plan,
|
|
208047
|
-
composeFile: layout.composeFile,
|
|
208048
|
-
envFile: layout.envFile ?? null
|
|
208049
|
-
},
|
|
208050
|
-
human: () => {
|
|
208051
|
-
success("stack removed");
|
|
208052
|
-
for (const w of decision.warnings)
|
|
208053
|
-
warn(w);
|
|
208054
|
-
for (const p of decision.plan.preserves) {
|
|
208055
|
-
note(`preserved · ${p.what}: ${p.detail}`);
|
|
208056
|
-
}
|
|
208057
|
-
}
|
|
208058
|
-
});
|
|
208059
|
-
});
|
|
208060
|
-
}
|
|
208061
207877
|
// src/commands/backup.ts
|
|
208062
207878
|
init_fs();
|
|
208063
207879
|
init_output();
|
|
208064
207880
|
init_resolve_auth();
|
|
208065
207881
|
import {
|
|
208066
207882
|
chmodSync,
|
|
208067
|
-
existsSync as
|
|
207883
|
+
existsSync as existsSync8,
|
|
208068
207884
|
mkdirSync as mkdirSync4,
|
|
208069
|
-
readFileSync as
|
|
207885
|
+
readFileSync as readFileSync7,
|
|
208070
207886
|
statSync as statSync2,
|
|
208071
|
-
writeFileSync as
|
|
207887
|
+
writeFileSync as writeFileSync6
|
|
208072
207888
|
} from "node:fs";
|
|
208073
|
-
import { join as
|
|
207889
|
+
import { join as join14, resolve as resolve9 } from "node:path";
|
|
208074
207890
|
import { closeDb, getDb as getDb4, sql as sql2 } from "@secondlayer/shared/db";
|
|
208075
207891
|
import {
|
|
208076
207892
|
decryptBundle,
|
|
@@ -208219,10 +208035,10 @@ async function runBackup(opts) {
|
|
|
208219
208035
|
printError(plan.reason);
|
|
208220
208036
|
process.exit(BACKUP_EXIT.REFUSED);
|
|
208221
208037
|
}
|
|
208222
|
-
const outDir =
|
|
208038
|
+
const outDir = resolve9(opts.out);
|
|
208223
208039
|
mkdirSync4(outDir, { recursive: true });
|
|
208224
208040
|
console.error("dumping database…");
|
|
208225
|
-
const dumpPath =
|
|
208041
|
+
const dumpPath = join14(outDir, DB_FILE);
|
|
208226
208042
|
const dumpCmd = pgDumpInvocation(databaseUrl, dumpPath);
|
|
208227
208043
|
const dump = await runProcess(dumpCmd.cmd, "pg_dump", dumpCmd.env);
|
|
208228
208044
|
if (!dump.ok) {
|
|
@@ -208242,8 +208058,8 @@ async function runBackup(opts) {
|
|
|
208242
208058
|
const lines = SECRET_ENV_KEYS.filter((k) => process.env[k]).map((k) => `${k}=${process.env[k]}`);
|
|
208243
208059
|
const sealed = encryptBundle(lines.join(`
|
|
208244
208060
|
`), passphrase);
|
|
208245
|
-
const secretsPath =
|
|
208246
|
-
|
|
208061
|
+
const secretsPath = join14(outDir, SECRETS_FILE);
|
|
208062
|
+
writeFileSync6(secretsPath, sealed);
|
|
208247
208063
|
chmodSync(secretsPath, 384);
|
|
208248
208064
|
manifest.secrets = {
|
|
208249
208065
|
file: SECRETS_FILE,
|
|
@@ -208251,7 +208067,7 @@ async function runBackup(opts) {
|
|
|
208251
208067
|
canary: sealKeyCanary(secretsKey)
|
|
208252
208068
|
};
|
|
208253
208069
|
}
|
|
208254
|
-
|
|
208070
|
+
writeFileSync6(join14(outDir, MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}
|
|
208255
208071
|
`);
|
|
208256
208072
|
output({
|
|
208257
208073
|
json: opts.json,
|
|
@@ -208280,14 +208096,14 @@ function attachRestoreCommand(cmd) {
|
|
|
208280
208096
|
}
|
|
208281
208097
|
async function runRestore(opts) {
|
|
208282
208098
|
const databaseUrl = requireDatabaseUrl();
|
|
208283
|
-
const dir =
|
|
208284
|
-
const manifestPath =
|
|
208285
|
-
if (!
|
|
208099
|
+
const dir = resolve9(opts.from);
|
|
208100
|
+
const manifestPath = join14(dir, MANIFEST_FILE);
|
|
208101
|
+
if (!existsSync8(manifestPath)) {
|
|
208286
208102
|
printError(`no ${MANIFEST_FILE} in ${dir}`);
|
|
208287
208103
|
process.exit(BACKUP_EXIT.REFUSED);
|
|
208288
208104
|
}
|
|
208289
|
-
const manifest = JSON.parse(
|
|
208290
|
-
const dumpPath =
|
|
208105
|
+
const manifest = JSON.parse(readFileSync7(manifestPath, "utf8"));
|
|
208106
|
+
const dumpPath = join14(dir, manifest.db?.file ?? DB_FILE);
|
|
208291
208107
|
if (manifest.db && await sha256File(dumpPath) !== manifest.db.sha256) {
|
|
208292
208108
|
printError("the database dump does not match the digest in the manifest; this bundle is corrupt or truncated");
|
|
208293
208109
|
process.exit(BACKUP_EXIT.REFUSED);
|
|
@@ -208301,7 +208117,7 @@ async function runRestore(opts) {
|
|
|
208301
208117
|
process.exit(BACKUP_EXIT.REFUSED);
|
|
208302
208118
|
}
|
|
208303
208119
|
try {
|
|
208304
|
-
const env = decryptBundle(
|
|
208120
|
+
const env = decryptBundle(readFileSync7(join14(dir, manifest.secrets.file)), passphrase);
|
|
208305
208121
|
bundleKey = env.split(`
|
|
208306
208122
|
`).find((l) => l.startsWith("SECONDLAYER_SECRETS_KEY="))?.slice("SECONDLAYER_SECRETS_KEY=".length).trim();
|
|
208307
208123
|
} catch {
|
|
@@ -208376,6 +208192,124 @@ function registerBackupCommand(program) {
|
|
|
208376
208192
|
function registerRestoreCommand(program) {
|
|
208377
208193
|
attachRestoreCommand(program.command("restore").description("Restore an instance from a backup bundle").hook("preAction", () => assertInstanceUrl()));
|
|
208378
208194
|
}
|
|
208195
|
+
// src/lib/instance-init.ts
|
|
208196
|
+
init_output();
|
|
208197
|
+
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
208198
|
+
import { existsSync as existsSync9, readFileSync as readFileSync8, writeFileSync as writeFileSync7 } from "node:fs";
|
|
208199
|
+
import { resolve as resolve10 } from "node:path";
|
|
208200
|
+
import { ARCHIVE_ROOT_PUBLIC_KEY_PEM } from "@secondlayer/shared/archive/root-key";
|
|
208201
|
+
import { generateEd25519KeyPair } from "@secondlayer/shared/crypto/ed25519";
|
|
208202
|
+
import { INSTANCE_NETWORKS } from "@secondlayer/shared/db/queries/instance";
|
|
208203
|
+
var INSTANCE_ENV_FILE = ".env.local";
|
|
208204
|
+
function isHostedAccountKey(value) {
|
|
208205
|
+
return value.startsWith("sk-sl_") || value.startsWith("ss-sl_");
|
|
208206
|
+
}
|
|
208207
|
+
function parseInstanceNetwork(value) {
|
|
208208
|
+
const network = value.trim().toLowerCase();
|
|
208209
|
+
if (INSTANCE_NETWORKS.includes(network)) {
|
|
208210
|
+
return network;
|
|
208211
|
+
}
|
|
208212
|
+
throw new Error(`network must be mainnet, testnet, or devnet (got ${value})`);
|
|
208213
|
+
}
|
|
208214
|
+
function instanceNetworkFromEnv(fallback = "mainnet") {
|
|
208215
|
+
const raw = process.env.STACKS_NETWORK ?? fallback;
|
|
208216
|
+
try {
|
|
208217
|
+
return parseInstanceNetwork(raw);
|
|
208218
|
+
} catch (err) {
|
|
208219
|
+
printError(err instanceof Error ? err.message : String(err), {
|
|
208220
|
+
hint: "Pass --network mainnet, --network testnet, or --network devnet."
|
|
208221
|
+
});
|
|
208222
|
+
process.exit(1);
|
|
208223
|
+
}
|
|
208224
|
+
}
|
|
208225
|
+
function generateInstanceToken() {
|
|
208226
|
+
return randomBytes3(32).toString("hex");
|
|
208227
|
+
}
|
|
208228
|
+
function generateSecretsKey() {
|
|
208229
|
+
return randomBytes3(32).toString("hex");
|
|
208230
|
+
}
|
|
208231
|
+
function generateSigningPrivateKey() {
|
|
208232
|
+
return generateEd25519KeyPair().privateKeyPem;
|
|
208233
|
+
}
|
|
208234
|
+
function escapeEnvValue(value) {
|
|
208235
|
+
if (!/[\s#"']/.test(value) && !value.includes("\\"))
|
|
208236
|
+
return value;
|
|
208237
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n")}"`;
|
|
208238
|
+
}
|
|
208239
|
+
function unescapeEnvValue(raw) {
|
|
208240
|
+
let value = raw.trim();
|
|
208241
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
208242
|
+
value = value.slice(1, -1);
|
|
208243
|
+
}
|
|
208244
|
+
return value.replace(/\\n/g, `
|
|
208245
|
+
`).replace(/\\"/g, '"').replace(/\\\\/g, "\\");
|
|
208246
|
+
}
|
|
208247
|
+
function readEnvValue(envPath, key) {
|
|
208248
|
+
if (!existsSync9(envPath))
|
|
208249
|
+
return null;
|
|
208250
|
+
const match = readFileSync8(envPath, "utf8").match(new RegExp(`^${key}=(.*)$`, "m"));
|
|
208251
|
+
return match ? unescapeEnvValue(match[1]) : null;
|
|
208252
|
+
}
|
|
208253
|
+
function buildInstanceEnv(input) {
|
|
208254
|
+
const token = input.existing?.INSTANCE_TOKEN || generateInstanceToken();
|
|
208255
|
+
const secrets = input.existing?.SECONDLAYER_SECRETS_KEY || generateSecretsKey();
|
|
208256
|
+
const signing = input.existing?.STREAMS_SIGNING_PRIVATE_KEY || input.existing?.SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY || generateSigningPrivateKey();
|
|
208257
|
+
const apiUrl = input.existing?.SECONDLAYER_API_URL || input.apiUrl || "http://127.0.0.1:3800";
|
|
208258
|
+
const archiveKey = input.existing?.ARCHIVE_SIGNING_PUBLIC_KEY || input.archivePublicKeyPem || ARCHIVE_ROOT_PUBLIC_KEY_PEM;
|
|
208259
|
+
const hostedCandidate = input.existing?.SECONDLAYER_API_KEY || undefined;
|
|
208260
|
+
const hostedKey = hostedCandidate && isHostedAccountKey(hostedCandidate) ? hostedCandidate : undefined;
|
|
208261
|
+
return {
|
|
208262
|
+
INSTANCE_MODE: "oss",
|
|
208263
|
+
STACKS_NETWORK: input.network,
|
|
208264
|
+
INSTANCE_TOKEN: token,
|
|
208265
|
+
SECONDLAYER_SECRETS_KEY: secrets,
|
|
208266
|
+
STREAMS_SIGNING_PRIVATE_KEY: signing,
|
|
208267
|
+
SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY: signing,
|
|
208268
|
+
ALLOW_UNSIGNED_WEBHOOKS: "false",
|
|
208269
|
+
SECONDLAYER_API_URL: apiUrl,
|
|
208270
|
+
...hostedKey ? { SECONDLAYER_API_KEY: hostedKey } : {},
|
|
208271
|
+
ARCHIVE_SIGNING_PUBLIC_KEY: archiveKey
|
|
208272
|
+
};
|
|
208273
|
+
}
|
|
208274
|
+
function renderInstanceEnv(env) {
|
|
208275
|
+
const lines = [
|
|
208276
|
+
"# Generated by secondlayer init. Do not commit.",
|
|
208277
|
+
`INSTANCE_MODE=${env.INSTANCE_MODE}`,
|
|
208278
|
+
`STACKS_NETWORK=${env.STACKS_NETWORK}`,
|
|
208279
|
+
"# The instance credential. The API validates bearer tokens against it,",
|
|
208280
|
+
"# and the CLI/SDK/MCP read it first when authenticating.",
|
|
208281
|
+
`INSTANCE_TOKEN=${env.INSTANCE_TOKEN}`,
|
|
208282
|
+
`SECONDLAYER_SECRETS_KEY=${env.SECONDLAYER_SECRETS_KEY}`,
|
|
208283
|
+
`STREAMS_SIGNING_PRIVATE_KEY=${escapeEnvValue(env.STREAMS_SIGNING_PRIVATE_KEY)}`,
|
|
208284
|
+
`SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY=${escapeEnvValue(env.SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY)}`,
|
|
208285
|
+
`ALLOW_UNSIGNED_WEBHOOKS=${env.ALLOW_UNSIGNED_WEBHOOKS}`,
|
|
208286
|
+
`SECONDLAYER_API_URL=${env.SECONDLAYER_API_URL}`
|
|
208287
|
+
];
|
|
208288
|
+
if (env.SECONDLAYER_API_KEY) {
|
|
208289
|
+
lines.push("# Hosted account key for archive credits (sk-sl_*).", `SECONDLAYER_API_KEY=${env.SECONDLAYER_API_KEY}`);
|
|
208290
|
+
}
|
|
208291
|
+
lines.push("# Archive trust root: bootstrap, verify, and repair check manifests", "# against this key. Replace it to pin a different archive.", `ARCHIVE_SIGNING_PUBLIC_KEY=${escapeEnvValue(env.ARCHIVE_SIGNING_PUBLIC_KEY)}`, "");
|
|
208292
|
+
return lines.join(`
|
|
208293
|
+
`);
|
|
208294
|
+
}
|
|
208295
|
+
function writeInstanceEnv(cwd, env, file = INSTANCE_ENV_FILE) {
|
|
208296
|
+
const path = resolve10(cwd, file);
|
|
208297
|
+
writeFileSync7(path, renderInstanceEnv(env), { mode: 384 });
|
|
208298
|
+
return path;
|
|
208299
|
+
}
|
|
208300
|
+
function loadExistingInstanceEnv(cwd, file = INSTANCE_ENV_FILE) {
|
|
208301
|
+
const path = resolve10(cwd, file);
|
|
208302
|
+
return {
|
|
208303
|
+
INSTANCE_TOKEN: readEnvValue(path, "INSTANCE_TOKEN"),
|
|
208304
|
+
SECONDLAYER_SECRETS_KEY: readEnvValue(path, "SECONDLAYER_SECRETS_KEY"),
|
|
208305
|
+
STREAMS_SIGNING_PRIVATE_KEY: readEnvValue(path, "STREAMS_SIGNING_PRIVATE_KEY"),
|
|
208306
|
+
SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY: readEnvValue(path, "SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY"),
|
|
208307
|
+
SECONDLAYER_API_URL: readEnvValue(path, "SECONDLAYER_API_URL"),
|
|
208308
|
+
SECONDLAYER_API_KEY: readEnvValue(path, "SECONDLAYER_API_KEY"),
|
|
208309
|
+
ARCHIVE_SIGNING_PUBLIC_KEY: readEnvValue(path, "ARCHIVE_SIGNING_PUBLIC_KEY")
|
|
208310
|
+
};
|
|
208311
|
+
}
|
|
208312
|
+
|
|
208379
208313
|
// src/commands/init.ts
|
|
208380
208314
|
init_output();
|
|
208381
208315
|
init_resolve_auth();
|
|
@@ -208485,968 +208419,6 @@ function registerObserverCommand(program) {
|
|
|
208485
208419
|
writeData(renderObserverStanza({ mode, endpoint, network }));
|
|
208486
208420
|
});
|
|
208487
208421
|
}
|
|
208488
|
-
// src/commands/setup.ts
|
|
208489
|
-
init_output();
|
|
208490
|
-
init_resolve_auth();
|
|
208491
|
-
|
|
208492
|
-
// src/lib/setup-prompts.ts
|
|
208493
|
-
import { resolve as resolvePath3 } from "node:path";
|
|
208494
|
-
import { confirm as confirm4, input as input2, select as select3 } from "@inquirer/prompts";
|
|
208495
|
-
|
|
208496
|
-
// src/lib/oss-compose.ts
|
|
208497
|
-
function buildOssCompose(opts = {}) {
|
|
208498
|
-
const owner = opts.owner ?? DEFAULT_IMAGE_OWNER;
|
|
208499
|
-
const tag = opts.imageTag ?? DEFAULT_IMAGE_TAG;
|
|
208500
|
-
const runtimeImage = `ghcr.io/${owner}/secondlayer-runtime:${tag}`;
|
|
208501
|
-
return `# Generated by \`secondlayer setup\` — do not edit by hand.
|
|
208502
|
-
# Mirrors docker/oss/docker-compose.yml; see that file for the source of truth
|
|
208503
|
-
# on env vars, profiles, and third-party digest pins. \`secondlayer\` is
|
|
208504
|
-
# swapped from a repo build to the published OSS image (no checkout here).
|
|
208505
|
-
name: secondlayer
|
|
208506
|
-
|
|
208507
|
-
services:
|
|
208508
|
-
postgres:
|
|
208509
|
-
image: postgres:17-alpine@sha256:d4bb0a8c1b7bb2e29f976d099e7bfb9a5d8858cffe9e46b35cd302cd1f1f8168
|
|
208510
|
-
environment:
|
|
208511
|
-
POSTGRES_USER: \${POSTGRES_USER:-secondlayer}
|
|
208512
|
-
POSTGRES_PASSWORD: \${POSTGRES_PASSWORD:-secondlayer}
|
|
208513
|
-
POSTGRES_DB: \${POSTGRES_DB:-secondlayer}
|
|
208514
|
-
# Postgres ships with a 128 MB cache, too small for a full-chain sync:
|
|
208515
|
-
# inserts stall on index reads from disk. ~25% of RAM is the usual size.
|
|
208516
|
-
command: ["postgres", "-c", "shared_buffers=\${POSTGRES_SHARED_BUFFERS:-1GB}"]
|
|
208517
|
-
volumes:
|
|
208518
|
-
- postgres_data:/var/lib/postgresql/data
|
|
208519
|
-
ports:
|
|
208520
|
-
- "\${POSTGRES_PORT:-127.0.0.1:5432}:5432"
|
|
208521
|
-
healthcheck:
|
|
208522
|
-
test: ["CMD-SHELL", "pg_isready -U \${POSTGRES_USER:-secondlayer}"]
|
|
208523
|
-
interval: 10s
|
|
208524
|
-
timeout: 5s
|
|
208525
|
-
retries: 5
|
|
208526
|
-
restart: unless-stopped
|
|
208527
|
-
|
|
208528
|
-
secondlayer:
|
|
208529
|
-
image: ${runtimeImage}
|
|
208530
|
-
environment:
|
|
208531
|
-
INSTANCE_MODE: oss
|
|
208532
|
-
NETWORK: \${NETWORK:-\${STACKS_NETWORK:-mainnet}}
|
|
208533
|
-
STACKS_NETWORK: \${STACKS_NETWORK:-mainnet}
|
|
208534
|
-
NODE_MODE: \${NODE_MODE:-external}
|
|
208535
|
-
SECONDLAYER_ALLOW_UNDERSIZED: \${SECONDLAYER_ALLOW_UNDERSIZED:-false}
|
|
208536
|
-
DATABASE_URL: postgres://\${POSTGRES_USER:-secondlayer}:\${POSTGRES_PASSWORD:-secondlayer}@postgres:5432/\${POSTGRES_DB:-secondlayer}
|
|
208537
|
-
DATA_DIR: /data
|
|
208538
|
-
API_PORT: "3800"
|
|
208539
|
-
INDEXER_PORT: "3700"
|
|
208540
|
-
# See docker/oss/docker-compose.yml: API_PORT above is the *container*
|
|
208541
|
-
# port; \${API_PORT} in ports:/here is the *publish spec* from .env.
|
|
208542
|
-
LISTEN_HOST: "0.0.0.0"
|
|
208543
|
-
API_PUBLISH_ADDR: \${API_PORT:-127.0.0.1:3800}
|
|
208544
|
-
INSTANCE_TOKEN: \${INSTANCE_TOKEN:-}
|
|
208545
|
-
SECONDLAYER_SECRETS_KEY: \${SECONDLAYER_SECRETS_KEY:-}
|
|
208546
|
-
STREAMS_SIGNING_PRIVATE_KEY: \${STREAMS_SIGNING_PRIVATE_KEY:-}
|
|
208547
|
-
SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY: \${SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY:-}
|
|
208548
|
-
ALLOW_UNSIGNED_WEBHOOKS: \${ALLOW_UNSIGNED_WEBHOOKS:-false}
|
|
208549
|
-
LOG_LEVEL: \${LOG_LEVEL:-info}
|
|
208550
|
-
NODE_ENV: production
|
|
208551
|
-
STACKS_NODE_RPC_URL: \${STACKS_NODE_RPC_URL:-}
|
|
208552
|
-
OBSERVER_JOURNAL_ENABLED: \${OBSERVER_JOURNAL_ENABLED:-true}
|
|
208553
|
-
STREAMS_API_URL: http://127.0.0.1:3800
|
|
208554
|
-
STREAMS_INTERNAL_API_KEY: \${STREAMS_INTERNAL_API_KEY:-}
|
|
208555
|
-
SBTC_DECODER_ENABLED: \${SBTC_DECODER_ENABLED:-true}
|
|
208556
|
-
POX4_DECODER_ENABLED: \${POX4_DECODER_ENABLED:-true}
|
|
208557
|
-
BNS_DECODER_ENABLED: \${BNS_DECODER_ENABLED:-false}
|
|
208558
|
-
SUBGRAPH_SOURCE: \${SUBGRAPH_SOURCE:-streams-index}
|
|
208559
|
-
SUBGRAPH_INDEX_API_URL: http://127.0.0.1:3800
|
|
208560
|
-
BITCOIN_RPC_PASSWORD: \${BITCOIN_RPC_PASSWORD:-}
|
|
208561
|
-
ports:
|
|
208562
|
-
- "\${API_PORT:-127.0.0.1:3800}:3800"
|
|
208563
|
-
- "\${INDEXER_PORT:-127.0.0.1:3700}:3700"
|
|
208564
|
-
# \`secondlayer observer\`'s default endpoint is \`indexer:3700\` — this
|
|
208565
|
-
# alias is what makes that default actually resolve on this network,
|
|
208566
|
-
# without renaming the service. Mirrors docker/oss/docker-compose.yml.
|
|
208567
|
-
networks:
|
|
208568
|
-
default:
|
|
208569
|
-
aliases:
|
|
208570
|
-
- indexer
|
|
208571
|
-
volumes:
|
|
208572
|
-
- subgraphs_data:/data/subgraphs
|
|
208573
|
-
depends_on:
|
|
208574
|
-
postgres:
|
|
208575
|
-
condition: service_healthy
|
|
208576
|
-
healthcheck:
|
|
208577
|
-
test: ["CMD", "curl", "-f", "http://localhost:3800/health"]
|
|
208578
|
-
interval: 30s
|
|
208579
|
-
timeout: 5s
|
|
208580
|
-
start_period: 40s
|
|
208581
|
-
retries: 3
|
|
208582
|
-
restart: unless-stopped
|
|
208583
|
-
|
|
208584
|
-
bitcoind:
|
|
208585
|
-
# Digest = kylemanna/bitcoind:latest as published 2026-07-20 — see
|
|
208586
|
-
# docker/oss/docker-compose.yml for why the digest is the real pin here.
|
|
208587
|
-
image: kylemanna/bitcoind:latest@sha256:86bbcaa99bf3bf3d5df7fd6d9217b3a41194db4c4e34385064f2cfc9fa7bdf91
|
|
208588
|
-
volumes:
|
|
208589
|
-
- \${BITCOIN_DATA_DIR:-./data/bitcoin}:/bitcoin/.bitcoin
|
|
208590
|
-
ports:
|
|
208591
|
-
- "127.0.0.1:8332:8332"
|
|
208592
|
-
- "8333:8333"
|
|
208593
|
-
healthcheck:
|
|
208594
|
-
test: ["CMD-SHELL", "bitcoin-cli -rpcuser=\${BITCOIN_RPC_USER:-stacks} -rpcpassword=\${BITCOIN_RPC_PASSWORD:-changeme} getblockchaininfo | grep -q '\\"blocks\\"' && [ $$(bitcoin-cli -rpcuser=\${BITCOIN_RPC_USER:-stacks} -rpcpassword=\${BITCOIN_RPC_PASSWORD:-changeme} getblockcount) -gt 666050 ]"]
|
|
208595
|
-
interval: 60s
|
|
208596
|
-
timeout: 15s
|
|
208597
|
-
start_period: 120s
|
|
208598
|
-
retries: 3
|
|
208599
|
-
restart: unless-stopped
|
|
208600
|
-
deploy:
|
|
208601
|
-
resources:
|
|
208602
|
-
limits:
|
|
208603
|
-
memory: 32G
|
|
208604
|
-
profiles: ["full-node"]
|
|
208605
|
-
|
|
208606
|
-
stacks-node:
|
|
208607
|
-
# Consensus-critical — digest-pinned to 4.0.5. See docker/oss/docker-compose.yml.
|
|
208608
|
-
image: ghcr.io/stacks-network/stacks-core:4.0.5@sha256:51aa7bf6f990b99fa010ebb23abe78653cf1f1d129ab8e7d88b1fe11a403c97d
|
|
208609
|
-
volumes:
|
|
208610
|
-
- \${STACKS_DATA_DIR:-./data/stacks}:/stacks-blockchain/data
|
|
208611
|
-
- ./Config.toml:/stacks-blockchain/Config.toml:ro
|
|
208612
|
-
ports:
|
|
208613
|
-
- "20443:20443"
|
|
208614
|
-
- "20444:20444"
|
|
208615
|
-
restart: unless-stopped
|
|
208616
|
-
command: ["stacks-node", "start", "--config", "/stacks-blockchain/Config.toml"]
|
|
208617
|
-
# full-node only — see docker/oss/docker-compose.yml. No standalone
|
|
208618
|
-
# "stacks-node, public Bitcoin" profile: nothing here wires the node at
|
|
208619
|
-
# a real public Bitcoin RPC, so that combination never actually synced.
|
|
208620
|
-
profiles: ["full-node"]
|
|
208621
|
-
|
|
208622
|
-
volumes:
|
|
208623
|
-
postgres_data:
|
|
208624
|
-
subgraphs_data:
|
|
208625
|
-
`;
|
|
208626
|
-
}
|
|
208627
|
-
function buildOssStacksConfigToml(opts) {
|
|
208628
|
-
const mode = opts.network === "mainnet" ? "mainnet" : "testnet";
|
|
208629
|
-
return `[node]
|
|
208630
|
-
working_dir = "/stacks-blockchain/data"
|
|
208631
|
-
rpc_bind = "0.0.0.0:20443"
|
|
208632
|
-
p2p_bind = "0.0.0.0:20444"
|
|
208633
|
-
seed = ""
|
|
208634
|
-
marf_compress = true
|
|
208635
|
-
bootstrap_node = "02196f005965cebe6ddc3901b7b1cc1aa7a88f305bb8c5893456b8f9a605923893@seed.mainnet.hiro.so:20444,02539449ad94e6e6392d8c1deb2b4e61f80ae2a18964349bc14336d8b903c46a8c@cet.stacksnodes.org:20444,02ececc8ce79b8adf813f13a0255f8ae58d4357309ba0cedd523d9f1a306fcfb79@sgt.stacksnodes.org:20444,0303144ba518fe7a0fb56a8a7d488f950307a4330f146e1e1458fc63fb33defe96@est.stacksnodes.org:20444"
|
|
208636
|
-
|
|
208637
|
-
# Event observer points directly at the bundled indexer service, same docker
|
|
208638
|
-
# network. Retries off / low timeout: a stuck indexer must never starve a
|
|
208639
|
-
# co-located signer of block_proposal events. See docker/oss/Config.toml.
|
|
208640
|
-
[[events_observer]]
|
|
208641
|
-
endpoint = "secondlayer:3700"
|
|
208642
|
-
events_keys = ["*"]
|
|
208643
|
-
timeout_ms = 500
|
|
208644
|
-
disable_retries = true
|
|
208645
|
-
|
|
208646
|
-
[burnchain]
|
|
208647
|
-
chain = "bitcoin"
|
|
208648
|
-
mode = "${mode}"
|
|
208649
|
-
peer_host = "bitcoind"
|
|
208650
|
-
username = "stacks"
|
|
208651
|
-
password = "${opts.bitcoinRpcPassword}"
|
|
208652
|
-
rpc_port = 8332
|
|
208653
|
-
peer_port = 8333
|
|
208654
|
-
`;
|
|
208655
|
-
}
|
|
208656
|
-
function buildOssBitcoinConf(opts) {
|
|
208657
|
-
return `server=1
|
|
208658
|
-
txindex=1
|
|
208659
|
-
rpcallowip=0.0.0.0/0
|
|
208660
|
-
rpcbind=0.0.0.0
|
|
208661
|
-
rpcuser=stacks
|
|
208662
|
-
rpcpassword=${opts.rpcPassword}
|
|
208663
|
-
# Prune: 0 = archive (required for Stacks); set to 550+ only for pruned chains.
|
|
208664
|
-
prune=0
|
|
208665
|
-
dbcache=4096
|
|
208666
|
-
`;
|
|
208667
|
-
}
|
|
208668
|
-
|
|
208669
|
-
// src/lib/setup-wizard.ts
|
|
208670
|
-
import { spawn as spawn3, spawnSync as spawnSync5 } from "node:child_process";
|
|
208671
|
-
import { randomBytes as randomBytes4 } from "node:crypto";
|
|
208672
|
-
import { existsSync as existsSync11 } from "node:fs";
|
|
208673
|
-
import { writeFile as writeFile6 } from "node:fs/promises";
|
|
208674
|
-
import { createServer } from "node:net";
|
|
208675
|
-
import { join as join16, resolve as resolvePath2 } from "node:path";
|
|
208676
|
-
import { closeDb as closeDb2, getRawClientFor } from "@secondlayer/shared/db";
|
|
208677
|
-
import { FLOORS, diskFloorGb } from "@secondlayer/shared/runtime";
|
|
208678
|
-
init_resolve_auth();
|
|
208679
|
-
var SETUP_NODE_MODES = ["external", "stacks", "full"];
|
|
208680
|
-
var DEFAULT_ARCHIVE_MANIFEST = "https://archive.secondlayer.tools/latest.json";
|
|
208681
|
-
class MissingSetupFlagError extends Error {
|
|
208682
|
-
flag;
|
|
208683
|
-
constructor(flag, message) {
|
|
208684
|
-
super(message);
|
|
208685
|
-
this.name = "MissingSetupFlagError";
|
|
208686
|
-
this.flag = flag;
|
|
208687
|
-
}
|
|
208688
|
-
}
|
|
208689
|
-
function parseSetupNodeMode(value) {
|
|
208690
|
-
const mode = value.trim().toLowerCase();
|
|
208691
|
-
if (SETUP_NODE_MODES.includes(mode)) {
|
|
208692
|
-
return mode;
|
|
208693
|
-
}
|
|
208694
|
-
throw new Error(`node mode must be external, stacks, or full (got ${value})`);
|
|
208695
|
-
}
|
|
208696
|
-
function resolveNonInteractiveConfig(flags) {
|
|
208697
|
-
if (!flags.network) {
|
|
208698
|
-
throw new MissingSetupFlagError("--network", "Missing --network <mainnet|testnet|devnet>. Non-interactive setup requires it explicitly — there is no safe default.");
|
|
208699
|
-
}
|
|
208700
|
-
if (!flags.nodeMode) {
|
|
208701
|
-
throw new MissingSetupFlagError("--node-mode", "Missing --node-mode <external|stacks|full>. Non-interactive setup requires it explicitly — there is no safe default.");
|
|
208702
|
-
}
|
|
208703
|
-
const network = parseInstanceNetwork(flags.network);
|
|
208704
|
-
const nodeMode = parseSetupNodeMode(flags.nodeMode);
|
|
208705
|
-
const dir = resolvePath2(flags.dir ?? process.cwd());
|
|
208706
|
-
const skipBootstrap = !!flags.skipBootstrap;
|
|
208707
|
-
return {
|
|
208708
|
-
network,
|
|
208709
|
-
nodeMode,
|
|
208710
|
-
apiPort: flags.apiPort ?? "127.0.0.1:3800",
|
|
208711
|
-
indexerPort: "127.0.0.1:3700",
|
|
208712
|
-
postgresPort: "127.0.0.1:5432",
|
|
208713
|
-
dir,
|
|
208714
|
-
against: skipBootstrap ? undefined : flags.against ?? DEFAULT_ARCHIVE_MANIFEST,
|
|
208715
|
-
skipBootstrap,
|
|
208716
|
-
skipVerify: !!flags.skipVerify,
|
|
208717
|
-
yes: !!flags.yes,
|
|
208718
|
-
force: !!flags.force,
|
|
208719
|
-
owner: flags.owner ?? DEFAULT_IMAGE_OWNER,
|
|
208720
|
-
imageTag: flags.imageTag ?? DEFAULT_IMAGE_TAG
|
|
208721
|
-
};
|
|
208722
|
-
}
|
|
208723
|
-
function guardrailPreview(nodeMode, network) {
|
|
208724
|
-
return {
|
|
208725
|
-
ramFloorMb: nodeMode === "full" ? FLOORS.fullRamMb : FLOORS.appRamMb,
|
|
208726
|
-
diskFloorGb: diskFloorGb(nodeMode, network)
|
|
208727
|
-
};
|
|
208728
|
-
}
|
|
208729
|
-
async function checkDocker() {
|
|
208730
|
-
const probe = spawnSync5("docker", ["info"], { stdio: "ignore" });
|
|
208731
|
-
return !probe.error && probe.status === 0;
|
|
208732
|
-
}
|
|
208733
|
-
function isPortFree(host, port) {
|
|
208734
|
-
return new Promise((resolvePromise) => {
|
|
208735
|
-
const srv = createServer();
|
|
208736
|
-
srv.once("error", () => resolvePromise(false));
|
|
208737
|
-
srv.once("listening", () => srv.close(() => resolvePromise(true)));
|
|
208738
|
-
srv.listen(port, host);
|
|
208739
|
-
});
|
|
208740
|
-
}
|
|
208741
|
-
function parsePublishSpec(spec) {
|
|
208742
|
-
const idx = spec.lastIndexOf(":");
|
|
208743
|
-
if (idx === -1)
|
|
208744
|
-
return { host: "127.0.0.1", port: Number(spec) };
|
|
208745
|
-
return { host: spec.slice(0, idx), port: Number(spec.slice(idx + 1)) };
|
|
208746
|
-
}
|
|
208747
|
-
function dockerPublishedHostPorts() {
|
|
208748
|
-
const probe = spawnSync5("docker", ["ps", "--format", "{{.Ports}}"], {
|
|
208749
|
-
encoding: "utf8"
|
|
208750
|
-
});
|
|
208751
|
-
const ports = new Set;
|
|
208752
|
-
if (probe.error || probe.status !== 0 || !probe.stdout)
|
|
208753
|
-
return ports;
|
|
208754
|
-
for (const match of probe.stdout.matchAll(/:(\d+)->/g)) {
|
|
208755
|
-
ports.add(Number(match[1]));
|
|
208756
|
-
}
|
|
208757
|
-
return ports;
|
|
208758
|
-
}
|
|
208759
|
-
async function resolveAvailablePublishSpec(spec, claimedByDocker = new Set, maxAttempts = 20) {
|
|
208760
|
-
const { host, port } = parsePublishSpec(spec);
|
|
208761
|
-
for (let attempt = 0;attempt < maxAttempts; attempt++) {
|
|
208762
|
-
const candidate = port + attempt;
|
|
208763
|
-
if (claimedByDocker.has(candidate))
|
|
208764
|
-
continue;
|
|
208765
|
-
if (await isPortFree(host, candidate)) {
|
|
208766
|
-
return { spec: `${host}:${candidate}`, remapped: attempt > 0 };
|
|
208767
|
-
}
|
|
208768
|
-
}
|
|
208769
|
-
return { spec, remapped: false };
|
|
208770
|
-
}
|
|
208771
|
-
function randomHex(bytes) {
|
|
208772
|
-
return randomBytes4(bytes).toString("hex");
|
|
208773
|
-
}
|
|
208774
|
-
function resolveSecrets(config) {
|
|
208775
|
-
const envPath = join16(config.dir, ".env");
|
|
208776
|
-
const existing = config.force || !existsSync11(envPath) ? {} : readEnvFile(config.dir, envPath);
|
|
208777
|
-
const instance = buildInstanceEnv({
|
|
208778
|
-
network: config.network,
|
|
208779
|
-
existing: {
|
|
208780
|
-
INSTANCE_TOKEN: existing.INSTANCE_TOKEN,
|
|
208781
|
-
SECONDLAYER_SECRETS_KEY: existing.SECONDLAYER_SECRETS_KEY,
|
|
208782
|
-
STREAMS_SIGNING_PRIVATE_KEY: existing.STREAMS_SIGNING_PRIVATE_KEY,
|
|
208783
|
-
SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY: existing.SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY,
|
|
208784
|
-
SECONDLAYER_API_URL: existing.SECONDLAYER_API_URL,
|
|
208785
|
-
SECONDLAYER_API_KEY: existing.SECONDLAYER_API_KEY,
|
|
208786
|
-
ARCHIVE_SIGNING_PUBLIC_KEY: existing.ARCHIVE_SIGNING_PUBLIC_KEY
|
|
208787
|
-
},
|
|
208788
|
-
apiUrl: `http://${normalizeLoopbackHost(config.apiPort)}`,
|
|
208789
|
-
archivePublicKeyPem: config.archivePublicKeyPem
|
|
208790
|
-
});
|
|
208791
|
-
return {
|
|
208792
|
-
instance,
|
|
208793
|
-
postgresPassword: existing.POSTGRES_PASSWORD || randomHex(24),
|
|
208794
|
-
bitcoinRpcPassword: existing.BITCOIN_RPC_PASSWORD || randomHex(24),
|
|
208795
|
-
databaseUrl: operatorEditedDatabaseUrl(existing)
|
|
208796
|
-
};
|
|
208797
|
-
}
|
|
208798
|
-
function operatorEditedDatabaseUrl(existing) {
|
|
208799
|
-
const url = existing.DATABASE_URL;
|
|
208800
|
-
if (!url)
|
|
208801
|
-
return;
|
|
208802
|
-
if (existing.POSTGRES_PORT && existing.POSTGRES_PASSWORD) {
|
|
208803
|
-
const generated = setupDatabaseUrl({ postgresPort: existing.POSTGRES_PORT }, { postgresPassword: existing.POSTGRES_PASSWORD });
|
|
208804
|
-
if (url === generated)
|
|
208805
|
-
return;
|
|
208806
|
-
}
|
|
208807
|
-
return url;
|
|
208808
|
-
}
|
|
208809
|
-
var SETUP_POSTGRES_USER = "secondlayer";
|
|
208810
|
-
var SETUP_POSTGRES_DB = "secondlayer";
|
|
208811
|
-
function setupDatabaseUrl(config, secrets) {
|
|
208812
|
-
if (secrets.databaseUrl)
|
|
208813
|
-
return secrets.databaseUrl;
|
|
208814
|
-
const password = encodeURIComponent(secrets.postgresPassword);
|
|
208815
|
-
return `postgres://${SETUP_POSTGRES_USER}:${password}@${normalizeLoopbackHost(config.postgresPort)}/${SETUP_POSTGRES_DB}`;
|
|
208816
|
-
}
|
|
208817
|
-
function normalizeLoopbackHost(apiPort) {
|
|
208818
|
-
return apiPort.includes(":") ? apiPort : `127.0.0.1:${apiPort}`;
|
|
208819
|
-
}
|
|
208820
|
-
function readEnvFile(dir, envPath) {
|
|
208821
|
-
const shared = loadExistingInstanceEnv(dir, ".env");
|
|
208822
|
-
return {
|
|
208823
|
-
...shared,
|
|
208824
|
-
POSTGRES_PASSWORD: readEnvValue(envPath, "POSTGRES_PASSWORD"),
|
|
208825
|
-
POSTGRES_PORT: readEnvValue(envPath, "POSTGRES_PORT"),
|
|
208826
|
-
BITCOIN_RPC_PASSWORD: readEnvValue(envPath, "BITCOIN_RPC_PASSWORD"),
|
|
208827
|
-
DATABASE_URL: readEnvValue(envPath, "DATABASE_URL")
|
|
208828
|
-
};
|
|
208829
|
-
}
|
|
208830
|
-
function renderSetupEnv(config, secrets) {
|
|
208831
|
-
const i = secrets.instance;
|
|
208832
|
-
const lines = [
|
|
208833
|
-
"# Generated by `secondlayer setup`. Do not commit.",
|
|
208834
|
-
`NETWORK=${config.network}`,
|
|
208835
|
-
`STACKS_NETWORK=${config.network}`,
|
|
208836
|
-
`NODE_MODE=${config.nodeMode}`,
|
|
208837
|
-
`API_PORT=${config.apiPort}`,
|
|
208838
|
-
`INDEXER_PORT=${config.indexerPort}`,
|
|
208839
|
-
`POSTGRES_USER=${SETUP_POSTGRES_USER}`,
|
|
208840
|
-
`POSTGRES_PASSWORD=${secrets.postgresPassword}`,
|
|
208841
|
-
`POSTGRES_DB=${SETUP_POSTGRES_DB}`,
|
|
208842
|
-
`POSTGRES_PORT=${config.postgresPort}`,
|
|
208843
|
-
"# Where bootstrap, verify, repair, and backup connect from this directory:",
|
|
208844
|
-
"# the compose Postgres above, published on the host. Point it elsewhere",
|
|
208845
|
-
"# only if you moved the index.",
|
|
208846
|
-
`DATABASE_URL=${escapeEnvValue(setupDatabaseUrl(config, secrets))}`,
|
|
208847
|
-
"# The instance credential. The API validates bearer tokens against it,",
|
|
208848
|
-
"# and the CLI/SDK/MCP read it first when authenticating.",
|
|
208849
|
-
`INSTANCE_TOKEN=${i.INSTANCE_TOKEN}`,
|
|
208850
|
-
`SECONDLAYER_SECRETS_KEY=${i.SECONDLAYER_SECRETS_KEY}`,
|
|
208851
|
-
`STREAMS_SIGNING_PRIVATE_KEY=${escapeEnvValue(i.STREAMS_SIGNING_PRIVATE_KEY)}`,
|
|
208852
|
-
`SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY=${escapeEnvValue(i.SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY)}`,
|
|
208853
|
-
`ALLOW_UNSIGNED_WEBHOOKS=${i.ALLOW_UNSIGNED_WEBHOOKS}`,
|
|
208854
|
-
`SECONDLAYER_API_URL=${i.SECONDLAYER_API_URL}`
|
|
208855
|
-
];
|
|
208856
|
-
if (i.SECONDLAYER_API_KEY) {
|
|
208857
|
-
lines.push("# Hosted account key for archive credits (sk-sl_*).", `SECONDLAYER_API_KEY=${i.SECONDLAYER_API_KEY}`);
|
|
208858
|
-
}
|
|
208859
|
-
lines.push("# Archive trust root: bootstrap, verify, and repair check manifests", "# against this key. Replace it to pin a different archive.", `ARCHIVE_SIGNING_PUBLIC_KEY=${escapeEnvValue(i.ARCHIVE_SIGNING_PUBLIC_KEY)}`);
|
|
208860
|
-
if (config.nodeMode === "full") {
|
|
208861
|
-
lines.push("BITCOIN_RPC_USER=stacks", `BITCOIN_RPC_PASSWORD=${secrets.bitcoinRpcPassword}`);
|
|
208862
|
-
}
|
|
208863
|
-
lines.push("");
|
|
208864
|
-
return lines.join(`
|
|
208865
|
-
`);
|
|
208866
|
-
}
|
|
208867
|
-
async function writeSetupFiles(config, secrets) {
|
|
208868
|
-
const composePath = join16(config.dir, "docker-compose.yml");
|
|
208869
|
-
const envPath = join16(config.dir, ".env");
|
|
208870
|
-
await writeFile6(composePath, buildOssCompose({ owner: config.owner, imageTag: config.imageTag }));
|
|
208871
|
-
await writeFile6(envPath, renderSetupEnv(config, secrets), { mode: 384 });
|
|
208872
|
-
const result = { composePath, envPath };
|
|
208873
|
-
if (config.nodeMode === "full") {
|
|
208874
|
-
const configTomlPath = join16(config.dir, "Config.toml");
|
|
208875
|
-
await writeFile6(configTomlPath, buildOssStacksConfigToml({
|
|
208876
|
-
network: config.network,
|
|
208877
|
-
bitcoinRpcPassword: secrets.bitcoinRpcPassword ?? ""
|
|
208878
|
-
}));
|
|
208879
|
-
result.configTomlPath = configTomlPath;
|
|
208880
|
-
}
|
|
208881
|
-
if (config.nodeMode === "full") {
|
|
208882
|
-
const bitcoinConfPath = join16(config.dir, "bitcoin.conf");
|
|
208883
|
-
await writeFile6(bitcoinConfPath, buildOssBitcoinConf({ rpcPassword: secrets.bitcoinRpcPassword ?? "" }));
|
|
208884
|
-
result.bitcoinConfPath = bitcoinConfPath;
|
|
208885
|
-
}
|
|
208886
|
-
return result;
|
|
208887
|
-
}
|
|
208888
|
-
function composeProfileArgs(nodeMode) {
|
|
208889
|
-
if (nodeMode === "full")
|
|
208890
|
-
return ["--profile", "full-node"];
|
|
208891
|
-
return [];
|
|
208892
|
-
}
|
|
208893
|
-
function runStreaming(command, args, opts) {
|
|
208894
|
-
return new Promise((resolvePromise, reject) => {
|
|
208895
|
-
const child = spawn3(command, args, {
|
|
208896
|
-
cwd: opts.cwd,
|
|
208897
|
-
env: opts.env ?? process.env,
|
|
208898
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
208899
|
-
});
|
|
208900
|
-
let stdout = "";
|
|
208901
|
-
let stderr = "";
|
|
208902
|
-
const pump = (stream) => (chunk) => {
|
|
208903
|
-
const text = chunk.toString("utf8");
|
|
208904
|
-
if (stream === "stdout")
|
|
208905
|
-
stdout += text;
|
|
208906
|
-
else
|
|
208907
|
-
stderr += text;
|
|
208908
|
-
if (opts.onLine) {
|
|
208909
|
-
for (const line of text.split(`
|
|
208910
|
-
`)) {
|
|
208911
|
-
if (line.length > 0)
|
|
208912
|
-
opts.onLine(line, stream);
|
|
208913
|
-
}
|
|
208914
|
-
}
|
|
208915
|
-
};
|
|
208916
|
-
child.stdout?.on("data", pump("stdout"));
|
|
208917
|
-
child.stderr?.on("data", pump("stderr"));
|
|
208918
|
-
child.once("error", reject);
|
|
208919
|
-
child.once("close", (code) => {
|
|
208920
|
-
resolvePromise({ code: code ?? 1, stdout, stderr });
|
|
208921
|
-
});
|
|
208922
|
-
});
|
|
208923
|
-
}
|
|
208924
|
-
async function dockerComposeUp(config, files, onLine) {
|
|
208925
|
-
const args = [
|
|
208926
|
-
"compose",
|
|
208927
|
-
"-f",
|
|
208928
|
-
files.composePath,
|
|
208929
|
-
"--env-file",
|
|
208930
|
-
files.envPath,
|
|
208931
|
-
...composeProfileArgs(config.nodeMode),
|
|
208932
|
-
"up",
|
|
208933
|
-
"-d"
|
|
208934
|
-
];
|
|
208935
|
-
return runStreaming("docker", args, { cwd: config.dir, onLine });
|
|
208936
|
-
}
|
|
208937
|
-
async function pollHealth(config, opts = {}) {
|
|
208938
|
-
const timeoutMs = opts.timeoutMs ?? 120000;
|
|
208939
|
-
const intervalMs = opts.intervalMs ?? 2000;
|
|
208940
|
-
const url = `http://${normalizeLoopbackHost(config.apiPort)}/health`;
|
|
208941
|
-
const deadline = Date.now() + timeoutMs;
|
|
208942
|
-
for (;; ) {
|
|
208943
|
-
try {
|
|
208944
|
-
const res = await fetch(url, { signal: AbortSignal.timeout(3000) });
|
|
208945
|
-
if (res.ok)
|
|
208946
|
-
return true;
|
|
208947
|
-
} catch {}
|
|
208948
|
-
opts.onTick?.();
|
|
208949
|
-
if (Date.now() >= deadline)
|
|
208950
|
-
return false;
|
|
208951
|
-
await new Promise((r) => setTimeout(r, intervalMs));
|
|
208952
|
-
}
|
|
208953
|
-
}
|
|
208954
|
-
function getObserverInfo(config) {
|
|
208955
|
-
if (config.nodeMode !== "external") {
|
|
208956
|
-
return { applicable: false };
|
|
208957
|
-
}
|
|
208958
|
-
const endpoint = defaultObserverEndpoint(config.network);
|
|
208959
|
-
const stanza = renderObserverStanza({
|
|
208960
|
-
mode: "indexer",
|
|
208961
|
-
endpoint,
|
|
208962
|
-
network: config.network
|
|
208963
|
-
});
|
|
208964
|
-
return { applicable: true, stanza, endpoint };
|
|
208965
|
-
}
|
|
208966
|
-
function cliEntry() {
|
|
208967
|
-
const entry = process.argv[1];
|
|
208968
|
-
if (!entry) {
|
|
208969
|
-
throw new Error("Could not resolve the secondlayer CLI entry to shell out to.");
|
|
208970
|
-
}
|
|
208971
|
-
return [process.execPath, entry];
|
|
208972
|
-
}
|
|
208973
|
-
var CHILD_ENV_KEYS = [
|
|
208974
|
-
"DATABASE_URL",
|
|
208975
|
-
"ARCHIVE_SIGNING_PUBLIC_KEY",
|
|
208976
|
-
"SECONDLAYER_API_URL",
|
|
208977
|
-
"INSTANCE_TOKEN"
|
|
208978
|
-
];
|
|
208979
|
-
function buildChildEnv(envPath, base = process.env) {
|
|
208980
|
-
const env = { ...base };
|
|
208981
|
-
for (const key of CHILD_ENV_KEYS) {
|
|
208982
|
-
const value = readEnvValue(envPath, key);
|
|
208983
|
-
if (value)
|
|
208984
|
-
env[key] = value;
|
|
208985
|
-
}
|
|
208986
|
-
return env;
|
|
208987
|
-
}
|
|
208988
|
-
async function checkDatabaseReachable(databaseUrl) {
|
|
208989
|
-
try {
|
|
208990
|
-
const client = getRawClientFor(databaseUrl);
|
|
208991
|
-
await client`SELECT 1`;
|
|
208992
|
-
return { ok: true };
|
|
208993
|
-
} catch (err) {
|
|
208994
|
-
return {
|
|
208995
|
-
ok: false,
|
|
208996
|
-
error: err instanceof Error ? err.message : String(err)
|
|
208997
|
-
};
|
|
208998
|
-
} finally {
|
|
208999
|
-
await closeDb2().catch(() => {});
|
|
209000
|
-
}
|
|
209001
|
-
}
|
|
209002
|
-
async function preflightBootstrapDatabase(config, secrets, emit) {
|
|
209003
|
-
const databaseUrl = setupDatabaseUrl(config, secrets);
|
|
209004
|
-
const reachable = await checkDatabaseReachable(databaseUrl);
|
|
209005
|
-
if (!reachable.ok) {
|
|
209006
|
-
emit({
|
|
209007
|
-
type: "step-error",
|
|
209008
|
-
step: "bootstrap",
|
|
209009
|
-
message: `Postgres at ${redactUrl(databaseUrl)} did not answer SELECT 1: ${reachable.error}. Check \`docker compose logs postgres\`, then re-run setup to resume.`
|
|
209010
|
-
});
|
|
209011
|
-
return null;
|
|
209012
|
-
}
|
|
209013
|
-
emit({
|
|
209014
|
-
type: "step-log",
|
|
209015
|
-
step: "bootstrap",
|
|
209016
|
-
line: `database ${redactUrl(databaseUrl)}`
|
|
209017
|
-
});
|
|
209018
|
-
return databaseUrl;
|
|
209019
|
-
}
|
|
209020
|
-
async function runBootstrap(config, against, files, onLine) {
|
|
209021
|
-
const [command, ...prefix] = cliEntry();
|
|
209022
|
-
const args = [...prefix, "bootstrap", "--against", against, "--yes"];
|
|
209023
|
-
return runStreaming(command, args, {
|
|
209024
|
-
cwd: config.dir,
|
|
209025
|
-
env: buildChildEnv(files.envPath),
|
|
209026
|
-
onLine
|
|
209027
|
-
});
|
|
209028
|
-
}
|
|
209029
|
-
async function runVerify(config, against, files, onLine) {
|
|
209030
|
-
const [command, ...prefix] = cliEntry();
|
|
209031
|
-
const args = [...prefix, "verify", "all", "--against", against];
|
|
209032
|
-
return runStreaming(command, args, {
|
|
209033
|
-
cwd: config.dir,
|
|
209034
|
-
env: buildChildEnv(files.envPath),
|
|
209035
|
-
onLine
|
|
209036
|
-
});
|
|
209037
|
-
}
|
|
209038
|
-
function redactUrl(url) {
|
|
209039
|
-
try {
|
|
209040
|
-
const u = new URL(url);
|
|
209041
|
-
if (u.password)
|
|
209042
|
-
u.password = "***";
|
|
209043
|
-
return u.toString();
|
|
209044
|
-
} catch {
|
|
209045
|
-
return url;
|
|
209046
|
-
}
|
|
209047
|
-
}
|
|
209048
|
-
function buildSuccessSummary(config, secrets) {
|
|
209049
|
-
const apiUrl = `http://${normalizeLoopbackHost(config.apiPort)}`;
|
|
209050
|
-
const lines = [
|
|
209051
|
-
"Secondlayer is up.",
|
|
209052
|
-
"",
|
|
209053
|
-
` api ${apiUrl}`,
|
|
209054
|
-
` network ${config.network}`,
|
|
209055
|
-
` node mode ${config.nodeMode}`,
|
|
209056
|
-
` dir ${config.dir}`,
|
|
209057
|
-
"",
|
|
209058
|
-
"Export the instance token for writes (reads on loopback need no credential):",
|
|
209059
|
-
` export INSTANCE_TOKEN=${secrets.instance.INSTANCE_TOKEN}`,
|
|
209060
|
-
"",
|
|
209061
|
-
"Try it:",
|
|
209062
|
-
" secondlayer subgraphs create my-balances --from-contract SP....my-contract",
|
|
209063
|
-
" secondlayer subgraphs deploy subgraphs/my-balances.ts",
|
|
209064
|
-
` curl ${apiUrl}/v1/subgraphs/my-balances/balances`,
|
|
209065
|
-
"",
|
|
209066
|
-
"Docs: https://secondlayer.tools/docs/self-host"
|
|
209067
|
-
];
|
|
209068
|
-
return lines.join(`
|
|
209069
|
-
`);
|
|
209070
|
-
}
|
|
209071
|
-
async function runSetup(initialConfig, emit) {
|
|
209072
|
-
let config = initialConfig;
|
|
209073
|
-
emit({ type: "step-start", step: "preflight" });
|
|
209074
|
-
if (!await checkDocker()) {
|
|
209075
|
-
emit({
|
|
209076
|
-
type: "step-error",
|
|
209077
|
-
step: "preflight",
|
|
209078
|
-
message: "Docker isn't reachable. Start Docker Desktop/OrbStack, or install Docker, then re-run."
|
|
209079
|
-
});
|
|
209080
|
-
return { ok: false, config };
|
|
209081
|
-
}
|
|
209082
|
-
const claimedByDocker = dockerPublishedHostPorts();
|
|
209083
|
-
const ports = {
|
|
209084
|
-
api: await resolveAvailablePublishSpec(config.apiPort, claimedByDocker),
|
|
209085
|
-
indexer: await resolveAvailablePublishSpec(config.indexerPort, claimedByDocker),
|
|
209086
|
-
postgres: await resolveAvailablePublishSpec(config.postgresPort, claimedByDocker)
|
|
209087
|
-
};
|
|
209088
|
-
config = {
|
|
209089
|
-
...config,
|
|
209090
|
-
apiPort: ports.api.spec,
|
|
209091
|
-
indexerPort: ports.indexer.spec,
|
|
209092
|
-
postgresPort: ports.postgres.spec
|
|
209093
|
-
};
|
|
209094
|
-
for (const [label, result] of [
|
|
209095
|
-
["api", ports.api],
|
|
209096
|
-
["indexer", ports.indexer],
|
|
209097
|
-
["postgres", ports.postgres]
|
|
209098
|
-
]) {
|
|
209099
|
-
if (result.remapped) {
|
|
209100
|
-
emit({
|
|
209101
|
-
type: "step-log",
|
|
209102
|
-
step: "preflight",
|
|
209103
|
-
line: `${label} port was already in use locally, using ${result.spec} instead`
|
|
209104
|
-
});
|
|
209105
|
-
}
|
|
209106
|
-
}
|
|
209107
|
-
const floors = guardrailPreview(config.nodeMode, config.network);
|
|
209108
|
-
emit({
|
|
209109
|
-
type: "step-done",
|
|
209110
|
-
step: "preflight",
|
|
209111
|
-
detail: `NODE_MODE=${config.nodeMode} on ${config.network} needs at least ${floors.ramFloorMb}MB RAM and ${floors.diskFloorGb}GB disk.`
|
|
209112
|
-
});
|
|
209113
|
-
emit({ type: "step-start", step: "secrets" });
|
|
209114
|
-
const secrets = resolveSecrets({
|
|
209115
|
-
dir: config.dir,
|
|
209116
|
-
network: config.network,
|
|
209117
|
-
apiPort: config.apiPort,
|
|
209118
|
-
force: config.force,
|
|
209119
|
-
archivePublicKeyPem: await resolveArchivePublicKey({
|
|
209120
|
-
envPem: process.env.ARCHIVE_SIGNING_PUBLIC_KEY ?? process.env.STREAMS_SIGNING_PUBLIC_KEY,
|
|
209121
|
-
allowHostedApi: !isOssMode()
|
|
209122
|
-
})
|
|
209123
|
-
});
|
|
209124
|
-
emit({ type: "step-done", step: "secrets" });
|
|
209125
|
-
emit({ type: "step-start", step: "config" });
|
|
209126
|
-
const files = await writeSetupFiles(config, secrets);
|
|
209127
|
-
emit({
|
|
209128
|
-
type: "step-done",
|
|
209129
|
-
step: "config",
|
|
209130
|
-
detail: `${files.composePath}, ${files.envPath}`
|
|
209131
|
-
});
|
|
209132
|
-
emit({ type: "step-start", step: "docker-up" });
|
|
209133
|
-
const up = await dockerComposeUp(config, files, (line) => emit({ type: "step-log", step: "docker-up", line }));
|
|
209134
|
-
if (up.code !== 0) {
|
|
209135
|
-
emit({
|
|
209136
|
-
type: "step-error",
|
|
209137
|
-
step: "docker-up",
|
|
209138
|
-
message: `docker compose up failed (exit ${up.code}).
|
|
209139
|
-
${up.stderr || up.stdout}`
|
|
209140
|
-
});
|
|
209141
|
-
return { ok: false, config, secrets, files };
|
|
209142
|
-
}
|
|
209143
|
-
const healthy = await pollHealth(config, {
|
|
209144
|
-
onTick: () => emit({
|
|
209145
|
-
type: "step-log",
|
|
209146
|
-
step: "docker-up",
|
|
209147
|
-
line: "waiting for /health…"
|
|
209148
|
-
})
|
|
209149
|
-
});
|
|
209150
|
-
if (!healthy) {
|
|
209151
|
-
emit({
|
|
209152
|
-
type: "step-error",
|
|
209153
|
-
step: "docker-up",
|
|
209154
|
-
message: "Containers started but /health never came up. Check `docker compose logs`."
|
|
209155
|
-
});
|
|
209156
|
-
return { ok: false, config, secrets, files };
|
|
209157
|
-
}
|
|
209158
|
-
emit({ type: "step-done", step: "docker-up" });
|
|
209159
|
-
emit({ type: "step-start", step: "observer" });
|
|
209160
|
-
const observer = getObserverInfo(config);
|
|
209161
|
-
if (observer.applicable && observer.stanza) {
|
|
209162
|
-
emit({
|
|
209163
|
-
type: "step-log",
|
|
209164
|
-
step: "observer",
|
|
209165
|
-
line: "ACTION REQUIRED — paste this into your Stacks node's Config.toml:"
|
|
209166
|
-
});
|
|
209167
|
-
for (const line of observer.stanza.split(`
|
|
209168
|
-
`)) {
|
|
209169
|
-
if (line.length > 0)
|
|
209170
|
-
emit({ type: "step-log", step: "observer", line });
|
|
209171
|
-
}
|
|
209172
|
-
emit({
|
|
209173
|
-
type: "step-done",
|
|
209174
|
-
step: "observer",
|
|
209175
|
-
detail: `endpoint ${observer.endpoint} — see the ACTION REQUIRED lines above`
|
|
209176
|
-
});
|
|
209177
|
-
} else {
|
|
209178
|
-
emit({
|
|
209179
|
-
type: "step-done",
|
|
209180
|
-
step: "observer",
|
|
209181
|
-
detail: "bundled node — the observer is pre-configured, nothing to paste"
|
|
209182
|
-
});
|
|
209183
|
-
}
|
|
209184
|
-
let bootstrapped = false;
|
|
209185
|
-
if (config.skipBootstrap) {
|
|
209186
|
-
emit({ type: "step-skip", step: "bootstrap", reason: "--skip-bootstrap" });
|
|
209187
|
-
} else if (config.against) {
|
|
209188
|
-
emit({ type: "step-start", step: "bootstrap" });
|
|
209189
|
-
const databaseUrl = await preflightBootstrapDatabase(config, secrets, emit);
|
|
209190
|
-
if (databaseUrl === null) {
|
|
209191
|
-
return { ok: false, config, secrets, files, observer };
|
|
209192
|
-
}
|
|
209193
|
-
const res = await runBootstrap(config, config.against, files, (line) => emit({ type: "step-log", step: "bootstrap", line }));
|
|
209194
|
-
if (res.code === 0) {
|
|
209195
|
-
bootstrapped = true;
|
|
209196
|
-
emit({ type: "step-done", step: "bootstrap" });
|
|
209197
|
-
} else {
|
|
209198
|
-
emit({
|
|
209199
|
-
type: "step-error",
|
|
209200
|
-
step: "bootstrap",
|
|
209201
|
-
message: `secondlayer bootstrap exited ${res.code}.`
|
|
209202
|
-
});
|
|
209203
|
-
}
|
|
209204
|
-
}
|
|
209205
|
-
if (config.skipVerify) {
|
|
209206
|
-
emit({ type: "step-skip", step: "verify", reason: "--skip-verify" });
|
|
209207
|
-
} else if (!config.against) {
|
|
209208
|
-
emit({
|
|
209209
|
-
type: "step-skip",
|
|
209210
|
-
step: "verify",
|
|
209211
|
-
reason: "no manifest to verify against"
|
|
209212
|
-
});
|
|
209213
|
-
} else if (!bootstrapped) {
|
|
209214
|
-
emit({
|
|
209215
|
-
type: "step-skip",
|
|
209216
|
-
step: "verify",
|
|
209217
|
-
reason: "bootstrap did not run — nothing restored yet to verify"
|
|
209218
|
-
});
|
|
209219
|
-
} else {
|
|
209220
|
-
emit({ type: "step-start", step: "verify" });
|
|
209221
|
-
const res = await runVerify(config, config.against, files, (line) => emit({ type: "step-log", step: "verify", line }));
|
|
209222
|
-
if (res.code === 0) {
|
|
209223
|
-
emit({ type: "step-done", step: "verify" });
|
|
209224
|
-
} else {
|
|
209225
|
-
emit({
|
|
209226
|
-
type: "step-error",
|
|
209227
|
-
step: "verify",
|
|
209228
|
-
message: `secondlayer verify exited ${res.code}.`
|
|
209229
|
-
});
|
|
209230
|
-
}
|
|
209231
|
-
}
|
|
209232
|
-
const summary = buildSuccessSummary(config, secrets);
|
|
209233
|
-
emit({ type: "step-start", step: "summary" });
|
|
209234
|
-
emit({ type: "step-done", step: "summary", detail: summary });
|
|
209235
|
-
return { ok: true, config, secrets, files, observer, summary };
|
|
209236
|
-
}
|
|
209237
|
-
|
|
209238
|
-
// src/lib/setup-prompts.ts
|
|
209239
|
-
class SetupCancelledError extends Error {
|
|
209240
|
-
constructor() {
|
|
209241
|
-
super("Setup cancelled — nothing was changed.");
|
|
209242
|
-
this.name = "SetupCancelledError";
|
|
209243
|
-
}
|
|
209244
|
-
}
|
|
209245
|
-
function safeParse(value, parse) {
|
|
209246
|
-
if (!value)
|
|
209247
|
-
return;
|
|
209248
|
-
try {
|
|
209249
|
-
return parse(value);
|
|
209250
|
-
} catch {
|
|
209251
|
-
return;
|
|
209252
|
-
}
|
|
209253
|
-
}
|
|
209254
|
-
var NODE_MODE_DESCRIPTIONS = {
|
|
209255
|
-
external: "You run the Stacks node; paste an observer stanza into its Config.toml.",
|
|
209256
|
-
stacks: "You run the Stacks node yourself, no bundled bitcoind (same as external).",
|
|
209257
|
-
full: "Bundled Stacks node + bitcoind."
|
|
209258
|
-
};
|
|
209259
|
-
async function promptSetupConfig(flags) {
|
|
209260
|
-
const network = safeParse(flags.network, parseInstanceNetwork) ?? await select3({
|
|
209261
|
-
message: "Network",
|
|
209262
|
-
choices: [
|
|
209263
|
-
{
|
|
209264
|
-
name: "mainnet",
|
|
209265
|
-
value: "mainnet",
|
|
209266
|
-
description: "Production Stacks chain. Large: plan for hundreds of GB."
|
|
209267
|
-
},
|
|
209268
|
-
{
|
|
209269
|
-
name: "testnet",
|
|
209270
|
-
value: "testnet",
|
|
209271
|
-
description: "Stacks testnet. Small history."
|
|
209272
|
-
},
|
|
209273
|
-
{
|
|
209274
|
-
name: "devnet",
|
|
209275
|
-
value: "devnet",
|
|
209276
|
-
description: "Local Clarinet devnet."
|
|
209277
|
-
}
|
|
209278
|
-
]
|
|
209279
|
-
});
|
|
209280
|
-
const nodeMode = safeParse(flags.nodeMode, parseSetupNodeMode) ?? await select3({
|
|
209281
|
-
message: "Node mode",
|
|
209282
|
-
choices: SETUP_NODE_MODES.map((mode) => {
|
|
209283
|
-
const floor = guardrailPreview(mode, network);
|
|
209284
|
-
return {
|
|
209285
|
-
name: mode,
|
|
209286
|
-
value: mode,
|
|
209287
|
-
description: `${NODE_MODE_DESCRIPTIONS[mode]} needs ≥ ${(floor.ramFloorMb / 1024).toFixed(0)}GB RAM, ≥ ${floor.diskFloorGb}GB disk.`
|
|
209288
|
-
};
|
|
209289
|
-
})
|
|
209290
|
-
});
|
|
209291
|
-
let against = flags.against;
|
|
209292
|
-
let skipBootstrap = !!flags.skipBootstrap;
|
|
209293
|
-
if (!skipBootstrap && !against) {
|
|
209294
|
-
const choice = await select3({
|
|
209295
|
-
message: "Bootstrap from",
|
|
209296
|
-
choices: [
|
|
209297
|
-
{
|
|
209298
|
-
name: "Hosted archive (recommended)",
|
|
209299
|
-
value: "hosted",
|
|
209300
|
-
description: DEFAULT_ARCHIVE_MANIFEST
|
|
209301
|
-
},
|
|
209302
|
-
{
|
|
209303
|
-
name: "Custom manifest URL or path",
|
|
209304
|
-
value: "custom",
|
|
209305
|
-
description: "Enter your own"
|
|
209306
|
-
},
|
|
209307
|
-
{
|
|
209308
|
-
name: "Skip the archive",
|
|
209309
|
-
value: "skip",
|
|
209310
|
-
description: "Index only what your node sends from now on"
|
|
209311
|
-
}
|
|
209312
|
-
]
|
|
209313
|
-
});
|
|
209314
|
-
if (choice === "hosted") {
|
|
209315
|
-
against = DEFAULT_ARCHIVE_MANIFEST;
|
|
209316
|
-
} else if (choice === "skip") {
|
|
209317
|
-
skipBootstrap = true;
|
|
209318
|
-
} else {
|
|
209319
|
-
against = (await input2({
|
|
209320
|
-
message: "Manifest URL or local path",
|
|
209321
|
-
default: DEFAULT_ARCHIVE_MANIFEST
|
|
209322
|
-
})).trim();
|
|
209323
|
-
}
|
|
209324
|
-
}
|
|
209325
|
-
const config = {
|
|
209326
|
-
network,
|
|
209327
|
-
nodeMode,
|
|
209328
|
-
apiPort: flags.apiPort ?? "127.0.0.1:3800",
|
|
209329
|
-
indexerPort: "127.0.0.1:3700",
|
|
209330
|
-
postgresPort: "127.0.0.1:5432",
|
|
209331
|
-
dir: resolvePath3(flags.dir ?? process.cwd()),
|
|
209332
|
-
against: skipBootstrap ? undefined : against,
|
|
209333
|
-
skipBootstrap,
|
|
209334
|
-
skipVerify: !!flags.skipVerify,
|
|
209335
|
-
yes: false,
|
|
209336
|
-
force: !!flags.force,
|
|
209337
|
-
owner: flags.owner ?? DEFAULT_IMAGE_OWNER,
|
|
209338
|
-
imageTag: flags.imageTag ?? DEFAULT_IMAGE_TAG
|
|
209339
|
-
};
|
|
209340
|
-
console.error("");
|
|
209341
|
-
console.error(` network ${config.network}`);
|
|
209342
|
-
console.error(` node mode ${config.nodeMode}`);
|
|
209343
|
-
console.error(` dir ${config.dir}`);
|
|
209344
|
-
console.error(` api port ${config.apiPort}`);
|
|
209345
|
-
console.error(` bootstrap ${config.skipBootstrap ? "skip (from the node's tip)" : config.against}`);
|
|
209346
|
-
console.error("");
|
|
209347
|
-
const proceed = await confirm4({ message: "Start setup?", default: true });
|
|
209348
|
-
if (!proceed)
|
|
209349
|
-
throw new SetupCancelledError;
|
|
209350
|
-
return config;
|
|
209351
|
-
}
|
|
209352
|
-
|
|
209353
|
-
// src/commands/setup.ts
|
|
209354
|
-
function renderProgress(event) {
|
|
209355
|
-
switch (event.type) {
|
|
209356
|
-
case "step-start":
|
|
209357
|
-
note(`→ ${event.step}`);
|
|
209358
|
-
return;
|
|
209359
|
-
case "step-log":
|
|
209360
|
-
note(` ${event.line}`);
|
|
209361
|
-
return;
|
|
209362
|
-
case "step-done":
|
|
209363
|
-
success(event.step + (event.detail ? `: ${event.detail}` : ""));
|
|
209364
|
-
return;
|
|
209365
|
-
case "step-skip":
|
|
209366
|
-
note(` skipped ${event.step} (${event.reason})`);
|
|
209367
|
-
return;
|
|
209368
|
-
case "step-error":
|
|
209369
|
-
printError(`${event.step} failed: ${event.message}`);
|
|
209370
|
-
return;
|
|
209371
|
-
}
|
|
209372
|
-
}
|
|
209373
|
-
async function executeAndRender(config) {
|
|
209374
|
-
console.error(bold("Secondlayer setup"));
|
|
209375
|
-
console.error(dim(` network=${config.network} node-mode=${config.nodeMode} dir=${config.dir}`));
|
|
209376
|
-
console.error("");
|
|
209377
|
-
let failed = false;
|
|
209378
|
-
const result = await runSetup(config, (event) => {
|
|
209379
|
-
if (event.type === "step-error")
|
|
209380
|
-
failed = true;
|
|
209381
|
-
renderProgress(event);
|
|
209382
|
-
});
|
|
209383
|
-
if (!result.ok) {
|
|
209384
|
-
process.exit(1);
|
|
209385
|
-
}
|
|
209386
|
-
if (failed) {
|
|
209387
|
-
warn("Setup finished with at least one step reporting an error above.");
|
|
209388
|
-
}
|
|
209389
|
-
console.error("");
|
|
209390
|
-
if (result.summary)
|
|
209391
|
-
console.log(result.summary);
|
|
209392
|
-
process.exit(failed ? 1 : 0);
|
|
209393
|
-
}
|
|
209394
|
-
async function runNonInteractive(flags) {
|
|
209395
|
-
let config;
|
|
209396
|
-
try {
|
|
209397
|
-
config = resolveNonInteractiveConfig(flags);
|
|
209398
|
-
} catch (err) {
|
|
209399
|
-
if (err instanceof MissingSetupFlagError) {
|
|
209400
|
-
printError(err.message, { hint: `Pass ${err.flag} and re-run.` });
|
|
209401
|
-
} else {
|
|
209402
|
-
printError(err instanceof Error ? err.message : String(err));
|
|
209403
|
-
}
|
|
209404
|
-
process.exit(1);
|
|
209405
|
-
}
|
|
209406
|
-
await executeAndRender(config);
|
|
209407
|
-
}
|
|
209408
|
-
async function runPromptedInteractive(flags) {
|
|
209409
|
-
let config;
|
|
209410
|
-
try {
|
|
209411
|
-
config = await promptSetupConfig(flags);
|
|
209412
|
-
} catch (err) {
|
|
209413
|
-
if (err instanceof SetupCancelledError) {
|
|
209414
|
-
note(err.message);
|
|
209415
|
-
process.exit(0);
|
|
209416
|
-
}
|
|
209417
|
-
printError(err instanceof Error ? err.message : String(err));
|
|
209418
|
-
process.exit(1);
|
|
209419
|
-
}
|
|
209420
|
-
await executeAndRender(config);
|
|
209421
|
-
}
|
|
209422
|
-
function registerSetupCommand(program) {
|
|
209423
|
-
program.command("setup").description("Guided self-host onboarding: secrets, config, docker, bootstrap, verify — one command").option("--node-mode <mode>", "external, stacks, or full (required)").option("--api-port <spec>", "API publish spec", "127.0.0.1:3800").option("--dir <path>", "Target directory for compose + .env", ".").option("--against <manifest>", `Archive manifest to bootstrap from (default: ${DEFAULT_ARCHIVE_MANIFEST})`).option("--skip-bootstrap", "Skip the archive restore; index only what your node sends from now on").option("--skip-verify", "Skip the post-bootstrap verify pass").option("--yes", "Non-interactive: skip the prompts, require flags, never ask").option("--force", "Regenerate secrets even if a .env already exists in --dir").option("--owner <owner>", "ghcr image owner (namespace) to pull from").option("--image-tag <tag>", "Published image tag to run").addHelpText("after", `
|
|
209424
|
-
--network <mainnet|testnet|devnet> is the top-level global flag (see
|
|
209425
|
-
\`secondlayer --help\`) — required here too, just parsed one level up.
|
|
209426
|
-
|
|
209427
|
-
Examples:
|
|
209428
|
-
$ secondlayer setup
|
|
209429
|
-
$ secondlayer setup --yes --network mainnet --node-mode external
|
|
209430
|
-
$ secondlayer setup --yes --network testnet --node-mode external --skip-bootstrap --skip-verify
|
|
209431
|
-
|
|
209432
|
-
Without a TTY (piped, CI, an agent), or with --yes, the prompts are skipped:
|
|
209433
|
-
every decision with no safe default (--network, --node-mode) must come from a
|
|
209434
|
-
flag or the command fails fast naming it. --against defaults to the official
|
|
209435
|
-
archive (${DEFAULT_ARCHIVE_MANIFEST}) unless --skip-bootstrap is set.
|
|
209436
|
-
`).action(async (opts) => {
|
|
209437
|
-
assertInstanceUrl();
|
|
209438
|
-
const flags = {
|
|
209439
|
-
...opts,
|
|
209440
|
-
network: opts.network ?? process.env.STACKS_NETWORK
|
|
209441
|
-
};
|
|
209442
|
-
const interactive = process.stdout.isTTY && !flags.yes;
|
|
209443
|
-
if (!interactive) {
|
|
209444
|
-
await runNonInteractive(flags);
|
|
209445
|
-
return;
|
|
209446
|
-
}
|
|
209447
|
-
await runPromptedInteractive(flags);
|
|
209448
|
-
});
|
|
209449
|
-
}
|
|
209450
208422
|
// src/cli.ts
|
|
209451
208423
|
init_resolve_auth();
|
|
209452
208424
|
var { version } = package_default;
|
|
@@ -209463,17 +208435,16 @@ program.hook("preAction", (thisCommand) => {
|
|
|
209463
208435
|
});
|
|
209464
208436
|
program.addHelpText("after", `
|
|
209465
208437
|
Quickstart:
|
|
209466
|
-
$ secondlayer setup
|
|
209467
208438
|
$ secondlayer subgraphs create my-watcher --from-contract SP....my-contract
|
|
209468
208439
|
$ git add subgraphs/my-watcher.ts
|
|
209469
208440
|
$ secondlayer subgraphs deploy subgraphs/my-watcher.ts # or --allow-uncommitted
|
|
209470
208441
|
|
|
209471
|
-
|
|
208442
|
+
Self-host, from docker/oss in a checkout:
|
|
209472
208443
|
$ secondlayer init --network mainnet
|
|
208444
|
+
$ docker compose up -d
|
|
209473
208445
|
$ secondlayer bootstrap --against <manifest>
|
|
209474
208446
|
`);
|
|
209475
208447
|
program.commandsGroup("Getting started:");
|
|
209476
|
-
registerSetupCommand(program);
|
|
209477
208448
|
registerInitCommand(program);
|
|
209478
208449
|
registerBootstrapCommand(program);
|
|
209479
208450
|
registerObserverCommand(program);
|
|
@@ -209498,11 +208469,10 @@ registerVerifyCommand(program);
|
|
|
209498
208469
|
registerRepairCommand(program);
|
|
209499
208470
|
registerBackupCommand(program);
|
|
209500
208471
|
registerRestoreCommand(program);
|
|
209501
|
-
registerUninstallCommand(program);
|
|
209502
208472
|
program.commandsGroup("Archive:");
|
|
209503
208473
|
registerCreditsCommand(program);
|
|
209504
208474
|
registerArchiveCommand(program);
|
|
209505
208475
|
program.parse();
|
|
209506
208476
|
|
|
209507
|
-
//# debugId=
|
|
208477
|
+
//# debugId=36A335C6FBC2D29F64756E2164756E21
|
|
209508
208478
|
//# sourceMappingURL=cli.js.map
|