@secondlayer/cli 16.3.1 → 17.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/README.md +10 -11
- package/dist/cli.js +1235 -1887
- package/dist/cli.js.map +23 -24
- package/package.json +3 -7
package/dist/cli.js
CHANGED
|
@@ -2143,7 +2143,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2143
2143
|
|
|
2144
2144
|
// src/lib/api-url.ts
|
|
2145
2145
|
function resolveApiUrl() {
|
|
2146
|
-
return (process.env.SECONDLAYER_API_URL ?? process.env.
|
|
2146
|
+
return (process.env.SECONDLAYER_API_URL ?? process.env.SL_PLATFORM_API_URL ?? LOCAL_API_URL).replace(/\/+$/, "");
|
|
2147
2147
|
}
|
|
2148
2148
|
function resolveArchiveOpsUrl() {
|
|
2149
2149
|
return (process.env.SL_CREDITS_API_URL ?? ARCHIVE_OPS_API_URL).replace(/\/+$/, "");
|
|
@@ -2258,7 +2258,6 @@ function resolveEnvKey() {
|
|
|
2258
2258
|
function applyApiKeyFlag(apiKey) {
|
|
2259
2259
|
if (/^s[ks]-sl_/.test(apiKey)) {
|
|
2260
2260
|
process.env.SECONDLAYER_API_KEY = apiKey;
|
|
2261
|
-
process.env.SL_API_KEY = apiKey;
|
|
2262
2261
|
} else {
|
|
2263
2262
|
process.env.INSTANCE_TOKEN = apiKey;
|
|
2264
2263
|
}
|
|
@@ -2548,144 +2547,6 @@ var init_output = __esm(() => {
|
|
|
2548
2547
|
ANSI_ESCAPE_PATTERN = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
|
|
2549
2548
|
});
|
|
2550
2549
|
|
|
2551
|
-
// src/lib/archive-reference.ts
|
|
2552
|
-
import { createHash } from "node:crypto";
|
|
2553
|
-
import { readFile as readFile2 } from "node:fs/promises";
|
|
2554
|
-
import { join as join2, resolve, sep } from "node:path";
|
|
2555
|
-
import { parseRetryAfter } from "@secondlayer/sdk";
|
|
2556
|
-
import {
|
|
2557
|
-
checkSignature,
|
|
2558
|
-
loadReference,
|
|
2559
|
-
resolveArchivePublicKey
|
|
2560
|
-
} from "@secondlayer/shared/archive/reference";
|
|
2561
|
-
function assertPathWithinRoot(path, root) {
|
|
2562
|
-
const segments = path.split("/");
|
|
2563
|
-
const leaves = path.startsWith("/") || path.includes("\\") || /^[a-z][a-z0-9+.-]*:/i.test(path) || /[%?#]/.test(path) || segments.some((part) => part === "" || part === "." || part === "..");
|
|
2564
|
-
if (leaves) {
|
|
2565
|
-
throw new Error(`archive path ${path} would leave the archive root`);
|
|
2566
|
-
}
|
|
2567
|
-
if (root !== undefined) {
|
|
2568
|
-
const base = resolve(root);
|
|
2569
|
-
const target = resolve(base, path);
|
|
2570
|
-
if (target !== base && !target.startsWith(base + sep)) {
|
|
2571
|
-
throw new Error(`archive path ${path} would leave the archive root`);
|
|
2572
|
-
}
|
|
2573
|
-
}
|
|
2574
|
-
}
|
|
2575
|
-
function isTransientCode(code) {
|
|
2576
|
-
return typeof code === "string" && (TRANSIENT_ERROR_CODES.has(code) || code.startsWith("UND_ERR_"));
|
|
2577
|
-
}
|
|
2578
|
-
function isTransientFetchFailure(err) {
|
|
2579
|
-
if (!(err instanceof Error))
|
|
2580
|
-
return false;
|
|
2581
|
-
if (err.name === "AbortError" || err.name === "TimeoutError")
|
|
2582
|
-
return true;
|
|
2583
|
-
const code = err.code;
|
|
2584
|
-
const causeCode = err.cause?.code;
|
|
2585
|
-
if (isTransientCode(code) || isTransientCode(causeCode))
|
|
2586
|
-
return true;
|
|
2587
|
-
return err.name === "TypeError" && /fetch failed|network|socket|connection|reset|unreachable/i.test(err.message);
|
|
2588
|
-
}
|
|
2589
|
-
function retryDelayMs(attempt, response) {
|
|
2590
|
-
const retryAfter = response ? parseRetryAfter(response.headers.get("retry-after")) : undefined;
|
|
2591
|
-
if (retryAfter !== undefined) {
|
|
2592
|
-
return Math.min(retryAfter * 1000, RETRY_AFTER_CAP_MS);
|
|
2593
|
-
}
|
|
2594
|
-
return RETRY_BASE_MS * 2 ** (attempt - 1);
|
|
2595
|
-
}
|
|
2596
|
-
async function fetchWithRetry(url, opts) {
|
|
2597
|
-
const retries = opts.retries ?? PARTITION_FETCH_RETRIES;
|
|
2598
|
-
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
2599
|
-
const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
2600
|
-
const timeoutMs = opts.timeoutMs ?? PARTITION_FETCH_TIMEOUT_MS;
|
|
2601
|
-
let lastReason = "";
|
|
2602
|
-
for (let attempt = 1;attempt <= retries + 1; attempt++) {
|
|
2603
|
-
let response;
|
|
2604
|
-
try {
|
|
2605
|
-
response = await fetchImpl(url, {
|
|
2606
|
-
signal: AbortSignal.timeout(timeoutMs)
|
|
2607
|
-
});
|
|
2608
|
-
} catch (err) {
|
|
2609
|
-
if (!isTransientFetchFailure(err))
|
|
2610
|
-
throw err;
|
|
2611
|
-
lastReason = err instanceof Error ? err.message : String(err);
|
|
2612
|
-
}
|
|
2613
|
-
if (response) {
|
|
2614
|
-
const retryable = response.status === 429 || response.status >= 500;
|
|
2615
|
-
if (!retryable)
|
|
2616
|
-
return response;
|
|
2617
|
-
lastReason = `${response.status} ${response.statusText}`.trim();
|
|
2618
|
-
await response.body?.cancel().catch(() => {});
|
|
2619
|
-
}
|
|
2620
|
-
if (attempt <= retries) {
|
|
2621
|
-
await sleep(retryDelayMs(attempt, response));
|
|
2622
|
-
}
|
|
2623
|
-
}
|
|
2624
|
-
throw new ArchiveFetchError(`could not fetch ${opts.label} after ${retries + 1} attempts (${lastReason})`, { transient: true, attempts: retries + 1 });
|
|
2625
|
-
}
|
|
2626
|
-
async function fetchViaGate(gate, partition, retried, fetchOpts) {
|
|
2627
|
-
const url = await gate.getUrl(partition.path, retried ? { forceRefresh: true } : undefined);
|
|
2628
|
-
const response = await fetchWithRetry(url, {
|
|
2629
|
-
...fetchOpts,
|
|
2630
|
-
label: partition.path
|
|
2631
|
-
});
|
|
2632
|
-
if (response.status === 403 && !retried) {
|
|
2633
|
-
await response.body?.cancel().catch(() => {});
|
|
2634
|
-
return fetchViaGate(gate, partition, true, fetchOpts);
|
|
2635
|
-
}
|
|
2636
|
-
if (!response.ok) {
|
|
2637
|
-
throw new ArchiveFetchError(`could not fetch ${partition.path} (${response.status} ${response.statusText})`, { transient: false, attempts: 1 });
|
|
2638
|
-
}
|
|
2639
|
-
return Buffer.from(await response.arrayBuffer());
|
|
2640
|
-
}
|
|
2641
|
-
async function fetchVerifiedPartition(reference, partition, gate, fetchOpts) {
|
|
2642
|
-
assertPathWithinRoot(partition.path, reference.isRemote ? undefined : reference.root);
|
|
2643
|
-
let bytes;
|
|
2644
|
-
if (gate) {
|
|
2645
|
-
bytes = await fetchViaGate(gate, partition, false, fetchOpts);
|
|
2646
|
-
} else if (reference.isRemote) {
|
|
2647
|
-
const url = `${reference.root.replace(/\/$/, "")}/${partition.path}`;
|
|
2648
|
-
const response = await fetchWithRetry(url, {
|
|
2649
|
-
...fetchOpts,
|
|
2650
|
-
label: partition.path
|
|
2651
|
-
});
|
|
2652
|
-
if (!response.ok) {
|
|
2653
|
-
throw new ArchiveFetchError(`could not fetch ${partition.path} (${response.status} ${response.statusText})`, { transient: false, attempts: 1 });
|
|
2654
|
-
}
|
|
2655
|
-
bytes = Buffer.from(await response.arrayBuffer());
|
|
2656
|
-
} else {
|
|
2657
|
-
bytes = await readFile2(join2(reference.root, partition.path));
|
|
2658
|
-
}
|
|
2659
|
-
const digest = createHash("sha256").update(bytes).digest("hex");
|
|
2660
|
-
if (digest !== partition.sha256) {
|
|
2661
|
-
throw new Error(`${partition.path} failed verification: expected ${partition.sha256}, got ${digest}`);
|
|
2662
|
-
}
|
|
2663
|
-
return bytes;
|
|
2664
|
-
}
|
|
2665
|
-
var ArchiveFetchError, TRANSIENT_ERROR_CODES, PARTITION_FETCH_RETRIES = 3, PARTITION_FETCH_TIMEOUT_MS = 300000, RETRY_BASE_MS = 1000, RETRY_AFTER_CAP_MS = 60000;
|
|
2666
|
-
var init_archive_reference = __esm(() => {
|
|
2667
|
-
ArchiveFetchError = class ArchiveFetchError extends Error {
|
|
2668
|
-
transient;
|
|
2669
|
-
attempts;
|
|
2670
|
-
constructor(message, opts) {
|
|
2671
|
-
super(message);
|
|
2672
|
-
this.name = "ArchiveFetchError";
|
|
2673
|
-
this.transient = opts.transient;
|
|
2674
|
-
this.attempts = opts.attempts;
|
|
2675
|
-
}
|
|
2676
|
-
};
|
|
2677
|
-
TRANSIENT_ERROR_CODES = new Set([
|
|
2678
|
-
"ECONNRESET",
|
|
2679
|
-
"ECONNREFUSED",
|
|
2680
|
-
"ECONNABORTED",
|
|
2681
|
-
"ETIMEDOUT",
|
|
2682
|
-
"EAI_AGAIN",
|
|
2683
|
-
"EPIPE",
|
|
2684
|
-
"ENETUNREACH",
|
|
2685
|
-
"EHOSTUNREACH"
|
|
2686
|
-
]);
|
|
2687
|
-
});
|
|
2688
|
-
|
|
2689
2550
|
// ../../node_modules/fast-glob/out/utils/array.js
|
|
2690
2551
|
var require_array = __commonJS(function(exports) {
|
|
2691
2552
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -198545,13 +198406,13 @@ function indexReadFailure(err, ctx) {
|
|
|
198545
198406
|
const credential = resolveApiKey3() ? "INSTANCE_TOKEN is set in this shell" : "no INSTANCE_TOKEN is set in this shell";
|
|
198546
198407
|
return {
|
|
198547
198408
|
message: `Index read rejected (HTTP ${status}) while ${where}.`,
|
|
198548
|
-
hint: `${credential}. This read uses the same
|
|
198409
|
+
hint: `${credential}. This read uses the same SECONDLAYER_API_URL + INSTANCE_TOKEN pair as \`secondlayer subgraphs deploy\` — confirm both point at the instance you deployed to (\`secondlayer status\`), then retry. ${offlineHint}`
|
|
198549
198410
|
};
|
|
198550
198411
|
}
|
|
198551
198412
|
if (status === 404) {
|
|
198552
198413
|
return {
|
|
198553
198414
|
message: `Index read returned 404 while ${where}.`,
|
|
198554
|
-
hint: `${ctx.apiUrl} answered but serves no /v1/index/events — check
|
|
198415
|
+
hint: `${ctx.apiUrl} answered but serves no /v1/index/events — check SECONDLAYER_API_URL points at a Secondlayer instance, not a Stacks node.`
|
|
198555
198416
|
};
|
|
198556
198417
|
}
|
|
198557
198418
|
if (status === 429) {
|
|
@@ -202541,155 +202402,6 @@ var init_src = __esm(() => {
|
|
|
202541
202402
|
src_default = Postgres;
|
|
202542
202403
|
});
|
|
202543
202404
|
|
|
202544
|
-
// src/lib/devnet-compose.ts
|
|
202545
|
-
function buildDevnetCompose(opts = {}) {
|
|
202546
|
-
const owner = opts.owner ?? DEFAULT_IMAGE_OWNER;
|
|
202547
|
-
const tag = opts.imageTag ?? DEFAULT_IMAGE_TAG;
|
|
202548
|
-
const apiPort = opts.apiPort ?? 3800;
|
|
202549
|
-
const indexerPort = opts.indexerPort ?? 3700;
|
|
202550
|
-
const apiPublish = `127.0.0.1:${apiPort}`;
|
|
202551
|
-
const apiImage = `ghcr.io/${owner}/secondlayer-api:${tag}`;
|
|
202552
|
-
const indexerImage = `ghcr.io/${owner}/secondlayer-indexer:${tag}`;
|
|
202553
|
-
const dbUrl = "postgres://secondlayer:secondlayer@postgres:5432/secondlayer";
|
|
202554
|
-
const DEV_SECRETS_KEY = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
|
|
202555
|
-
return `# Generated by \`secondlayer devnet connect\` — do not edit by hand.
|
|
202556
|
-
# Secondlayer app services for local clarinet devnet development.
|
|
202557
|
-
name: secondlayer-devnet
|
|
202558
|
-
|
|
202559
|
-
services:
|
|
202560
|
-
postgres:
|
|
202561
|
-
image: postgres:17-alpine
|
|
202562
|
-
environment:
|
|
202563
|
-
POSTGRES_USER: secondlayer
|
|
202564
|
-
POSTGRES_PASSWORD: secondlayer
|
|
202565
|
-
POSTGRES_DB: secondlayer
|
|
202566
|
-
volumes:
|
|
202567
|
-
- postgres_data:/var/lib/postgresql/data
|
|
202568
|
-
healthcheck:
|
|
202569
|
-
test: ["CMD-SHELL", "pg_isready -U secondlayer"]
|
|
202570
|
-
interval: 5s
|
|
202571
|
-
timeout: 5s
|
|
202572
|
-
retries: 10
|
|
202573
|
-
restart: unless-stopped
|
|
202574
|
-
|
|
202575
|
-
migrate:
|
|
202576
|
-
image: ${apiImage}
|
|
202577
|
-
command: ["bun", "run", "packages/shared/src/db/migrate.ts"]
|
|
202578
|
-
environment:
|
|
202579
|
-
DATABASE_URL: ${dbUrl}
|
|
202580
|
-
depends_on:
|
|
202581
|
-
postgres:
|
|
202582
|
-
condition: service_healthy
|
|
202583
|
-
restart: "no"
|
|
202584
|
-
|
|
202585
|
-
api:
|
|
202586
|
-
image: ${apiImage}
|
|
202587
|
-
environment:
|
|
202588
|
-
INSTANCE_MODE: oss
|
|
202589
|
-
DATABASE_URL: ${dbUrl}
|
|
202590
|
-
PORT: 3800
|
|
202591
|
-
# 0.0.0.0 is forced: a container process that binds loopback is
|
|
202592
|
-
# unreachable through a published port. The real reach is the publish
|
|
202593
|
-
# spec below, handed over verbatim so /v1 reads stay keyless on devnet.
|
|
202594
|
-
LISTEN_HOST: "0.0.0.0"
|
|
202595
|
-
API_PUBLISH_ADDR: "${apiPublish}"
|
|
202596
|
-
# Boot guard reads LISTEN_HOST, so a token is required to start at all.
|
|
202597
|
-
# It gates writes only — reads are open on this loopback publish.
|
|
202598
|
-
INSTANCE_TOKEN: ${DEV_INSTANCE_TOKEN}
|
|
202599
|
-
NODE_ENV: production
|
|
202600
|
-
DATA_DIR: /data
|
|
202601
|
-
# Shared across api + subgraph-processor so the secret the api encrypts
|
|
202602
|
-
# (e.g. a webhook's signing secret) can be decrypted by the emitter.
|
|
202603
|
-
# OSS auto-generates a per-process key when unset — that splits them.
|
|
202604
|
-
# Local-dev fixed value; not a production secret.
|
|
202605
|
-
SECONDLAYER_SECRETS_KEY: ${DEV_SECRETS_KEY}
|
|
202606
|
-
ports:
|
|
202607
|
-
- "${apiPublish}:3800"
|
|
202608
|
-
volumes:
|
|
202609
|
-
- subgraphs_data:/data/subgraphs
|
|
202610
|
-
depends_on:
|
|
202611
|
-
migrate:
|
|
202612
|
-
condition: service_completed_successfully
|
|
202613
|
-
postgres:
|
|
202614
|
-
condition: service_healthy
|
|
202615
|
-
healthcheck:
|
|
202616
|
-
test: ["CMD", "curl", "-f", "http://localhost:3800/health"]
|
|
202617
|
-
interval: 10s
|
|
202618
|
-
timeout: 3s
|
|
202619
|
-
start_period: 10s
|
|
202620
|
-
retries: 5
|
|
202621
|
-
restart: unless-stopped
|
|
202622
|
-
|
|
202623
|
-
indexer:
|
|
202624
|
-
image: ${indexerImage}
|
|
202625
|
-
environment:
|
|
202626
|
-
DATABASE_URL: ${dbUrl}
|
|
202627
|
-
PORT: 3700
|
|
202628
|
-
NODE_ENV: production
|
|
202629
|
-
# Keep every network key aligned: the indexer writes index_progress under
|
|
202630
|
-
# STACKS_NETWORK, the subgraph processor reads it under NETWORK. If they
|
|
202631
|
-
# differ, catch-up finds no progress row and subgraphs never advance.
|
|
202632
|
-
NETWORKS: testnet
|
|
202633
|
-
STACKS_NETWORK: testnet
|
|
202634
|
-
NETWORK: testnet
|
|
202635
|
-
# Fed by the devnet node's event observer — never reach out to Hiro mainnet.
|
|
202636
|
-
AUTO_BACKFILL_ENABLED: "false"
|
|
202637
|
-
TIP_FOLLOWER_ENABLED: "false"
|
|
202638
|
-
# Deliberately published on every interface, unlike the api: the devnet's
|
|
202639
|
-
# stacks-node runs in its own container and POSTs events to
|
|
202640
|
-
# host.docker.internal:3700, which resolves to the host gateway address,
|
|
202641
|
-
# not 127.0.0.1. A loopback publish here would drop every block.
|
|
202642
|
-
ports:
|
|
202643
|
-
- "${indexerPort}:3700"
|
|
202644
|
-
extra_hosts:
|
|
202645
|
-
- "host.docker.internal:host-gateway"
|
|
202646
|
-
depends_on:
|
|
202647
|
-
migrate:
|
|
202648
|
-
condition: service_completed_successfully
|
|
202649
|
-
postgres:
|
|
202650
|
-
condition: service_healthy
|
|
202651
|
-
healthcheck:
|
|
202652
|
-
test: ["CMD", "curl", "-f", "http://localhost:3700/health"]
|
|
202653
|
-
interval: 10s
|
|
202654
|
-
timeout: 3s
|
|
202655
|
-
start_period: 10s
|
|
202656
|
-
retries: 5
|
|
202657
|
-
restart: unless-stopped
|
|
202658
|
-
|
|
202659
|
-
subgraph-processor:
|
|
202660
|
-
image: ${apiImage}
|
|
202661
|
-
command: ["bun", "run", "packages/subgraphs/src/service.ts"]
|
|
202662
|
-
environment:
|
|
202663
|
-
DATABASE_URL: ${dbUrl}
|
|
202664
|
-
NODE_ENV: production
|
|
202665
|
-
DATA_DIR: /data
|
|
202666
|
-
# Must match the indexer's network key (see indexer note) — catch-up reads
|
|
202667
|
-
# index_progress under NETWORK.
|
|
202668
|
-
NETWORK: testnet
|
|
202669
|
-
STACKS_NETWORK: testnet
|
|
202670
|
-
# The emitter blocks webhook delivery to private/loopback IPs by default.
|
|
202671
|
-
# On a local devnet you deliver to your own localhost receiver, so allow it.
|
|
202672
|
-
SECONDLAYER_ALLOW_PRIVATE_EGRESS: "true"
|
|
202673
|
-
# Must match the api's key so the emitter can decrypt signing secrets.
|
|
202674
|
-
SECONDLAYER_SECRETS_KEY: ${DEV_SECRETS_KEY}
|
|
202675
|
-
volumes:
|
|
202676
|
-
- subgraphs_data:/data/subgraphs
|
|
202677
|
-
depends_on:
|
|
202678
|
-
migrate:
|
|
202679
|
-
condition: service_completed_successfully
|
|
202680
|
-
postgres:
|
|
202681
|
-
condition: service_healthy
|
|
202682
|
-
healthcheck:
|
|
202683
|
-
disable: true
|
|
202684
|
-
restart: unless-stopped
|
|
202685
|
-
|
|
202686
|
-
volumes:
|
|
202687
|
-
postgres_data:
|
|
202688
|
-
subgraphs_data:
|
|
202689
|
-
`;
|
|
202690
|
-
}
|
|
202691
|
-
var DEFAULT_IMAGE_OWNER = "ryanwaits", DEFAULT_IMAGE_TAG = "latest", DEV_INSTANCE_TOKEN = "dev-instance-token";
|
|
202692
|
-
|
|
202693
202405
|
// src/lib/dev-state.ts
|
|
202694
202406
|
import { homedir as homedir4 } from "node:os";
|
|
202695
202407
|
import { join as join13 } from "node:path";
|
|
@@ -202722,1555 +202434,6 @@ var init_dev_state = __esm(() => {
|
|
|
202722
202434
|
LOGS_DIR = join13(STATE_DIR, "logs");
|
|
202723
202435
|
});
|
|
202724
202436
|
|
|
202725
|
-
// src/lib/instance-init.ts
|
|
202726
|
-
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
202727
|
-
import { existsSync as existsSync9, readFileSync as readFileSync7, writeFileSync as writeFileSync7 } from "node:fs";
|
|
202728
|
-
import { resolve as resolve10 } from "node:path";
|
|
202729
|
-
import { ARCHIVE_ROOT_PUBLIC_KEY_PEM } from "@secondlayer/shared/archive/root-key";
|
|
202730
|
-
import { generateEd25519KeyPair } from "@secondlayer/shared/crypto/ed25519";
|
|
202731
|
-
import { INSTANCE_NETWORKS } from "@secondlayer/shared/db/queries/instance";
|
|
202732
|
-
function isHostedAccountKey(value) {
|
|
202733
|
-
return value.startsWith("sk-sl_") || value.startsWith("ss-sl_");
|
|
202734
|
-
}
|
|
202735
|
-
function parseInstanceNetwork(value) {
|
|
202736
|
-
const network = value.trim().toLowerCase();
|
|
202737
|
-
if (INSTANCE_NETWORKS.includes(network)) {
|
|
202738
|
-
return network;
|
|
202739
|
-
}
|
|
202740
|
-
throw new Error(`network must be mainnet, testnet, or devnet (got ${value})`);
|
|
202741
|
-
}
|
|
202742
|
-
function instanceNetworkFromEnv(fallback = "mainnet") {
|
|
202743
|
-
const raw = process.env.STACKS_NETWORK ?? fallback;
|
|
202744
|
-
try {
|
|
202745
|
-
return parseInstanceNetwork(raw);
|
|
202746
|
-
} catch (err) {
|
|
202747
|
-
printError(err instanceof Error ? err.message : String(err), {
|
|
202748
|
-
hint: "Pass --network mainnet, --network testnet, or --network devnet."
|
|
202749
|
-
});
|
|
202750
|
-
process.exit(1);
|
|
202751
|
-
}
|
|
202752
|
-
}
|
|
202753
|
-
function generateInstanceToken() {
|
|
202754
|
-
return randomBytes3(32).toString("hex");
|
|
202755
|
-
}
|
|
202756
|
-
function generateSecretsKey() {
|
|
202757
|
-
return randomBytes3(32).toString("hex");
|
|
202758
|
-
}
|
|
202759
|
-
function generateSigningPrivateKey() {
|
|
202760
|
-
return generateEd25519KeyPair().privateKeyPem;
|
|
202761
|
-
}
|
|
202762
|
-
function escapeEnvValue(value) {
|
|
202763
|
-
if (!/[\s#"']/.test(value) && !value.includes("\\"))
|
|
202764
|
-
return value;
|
|
202765
|
-
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n")}"`;
|
|
202766
|
-
}
|
|
202767
|
-
function unescapeEnvValue(raw) {
|
|
202768
|
-
let value = raw.trim();
|
|
202769
|
-
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
202770
|
-
value = value.slice(1, -1);
|
|
202771
|
-
}
|
|
202772
|
-
return value.replace(/\\n/g, `
|
|
202773
|
-
`).replace(/\\"/g, '"').replace(/\\\\/g, "\\");
|
|
202774
|
-
}
|
|
202775
|
-
function readEnvValue(envPath, key) {
|
|
202776
|
-
if (!existsSync9(envPath))
|
|
202777
|
-
return null;
|
|
202778
|
-
const match = readFileSync7(envPath, "utf8").match(new RegExp(`^${key}=(.*)$`, "m"));
|
|
202779
|
-
return match ? unescapeEnvValue(match[1]) : null;
|
|
202780
|
-
}
|
|
202781
|
-
function buildInstanceEnv(input) {
|
|
202782
|
-
const existingAlias = input.existing?.SL_API_KEY;
|
|
202783
|
-
const aliasIsHex = existingAlias && !existingAlias.startsWith("sk-sl_") && !existingAlias.startsWith("ss-sl_");
|
|
202784
|
-
const token = input.existing?.INSTANCE_TOKEN || (aliasIsHex ? existingAlias : undefined) || generateInstanceToken();
|
|
202785
|
-
const secrets = input.existing?.SECONDLAYER_SECRETS_KEY || generateSecretsKey();
|
|
202786
|
-
const signing = input.existing?.STREAMS_SIGNING_PRIVATE_KEY || input.existing?.SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY || generateSigningPrivateKey();
|
|
202787
|
-
const apiUrl = input.existing?.SECONDLAYER_API_URL || input.existing?.SL_API_URL || input.apiUrl || "http://127.0.0.1:3800";
|
|
202788
|
-
const archiveKey = input.existing?.ARCHIVE_SIGNING_PUBLIC_KEY || input.archivePublicKeyPem || ARCHIVE_ROOT_PUBLIC_KEY_PEM;
|
|
202789
|
-
const hostedCandidate = input.existing?.SECONDLAYER_API_KEY || input.existing?.SL_API_KEY || undefined;
|
|
202790
|
-
const hostedKey = hostedCandidate && isHostedAccountKey(hostedCandidate) ? hostedCandidate : undefined;
|
|
202791
|
-
return {
|
|
202792
|
-
INSTANCE_MODE: "oss",
|
|
202793
|
-
STACKS_NETWORK: input.network,
|
|
202794
|
-
INSTANCE_TOKEN: token,
|
|
202795
|
-
SECONDLAYER_SECRETS_KEY: secrets,
|
|
202796
|
-
STREAMS_SIGNING_PRIVATE_KEY: signing,
|
|
202797
|
-
SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY: signing,
|
|
202798
|
-
ALLOW_UNSIGNED_WEBHOOKS: "false",
|
|
202799
|
-
SECONDLAYER_API_URL: apiUrl,
|
|
202800
|
-
SL_API_URL: apiUrl,
|
|
202801
|
-
...hostedKey ? { SECONDLAYER_API_KEY: hostedKey, SL_API_KEY: hostedKey } : {},
|
|
202802
|
-
ARCHIVE_SIGNING_PUBLIC_KEY: archiveKey
|
|
202803
|
-
};
|
|
202804
|
-
}
|
|
202805
|
-
function renderInstanceEnv(env) {
|
|
202806
|
-
const lines = [
|
|
202807
|
-
"# Generated by secondlayer init. Do not commit.",
|
|
202808
|
-
`INSTANCE_MODE=${env.INSTANCE_MODE}`,
|
|
202809
|
-
`STACKS_NETWORK=${env.STACKS_NETWORK}`,
|
|
202810
|
-
"# The instance credential. The API validates bearer tokens against it,",
|
|
202811
|
-
"# and the CLI/SDK/MCP read it first when authenticating.",
|
|
202812
|
-
`INSTANCE_TOKEN=${env.INSTANCE_TOKEN}`,
|
|
202813
|
-
`SECONDLAYER_SECRETS_KEY=${env.SECONDLAYER_SECRETS_KEY}`,
|
|
202814
|
-
`STREAMS_SIGNING_PRIVATE_KEY=${escapeEnvValue(env.STREAMS_SIGNING_PRIVATE_KEY)}`,
|
|
202815
|
-
`SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY=${escapeEnvValue(env.SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY)}`,
|
|
202816
|
-
`ALLOW_UNSIGNED_WEBHOOKS=${env.ALLOW_UNSIGNED_WEBHOOKS}`,
|
|
202817
|
-
`SECONDLAYER_API_URL=${env.SECONDLAYER_API_URL}`,
|
|
202818
|
-
`SL_API_URL=${env.SL_API_URL}`
|
|
202819
|
-
];
|
|
202820
|
-
if (env.SECONDLAYER_API_KEY) {
|
|
202821
|
-
lines.push("# Hosted account key for archive credits (sk-sl_*).", `SECONDLAYER_API_KEY=${env.SECONDLAYER_API_KEY}`, `SL_API_KEY=${env.SL_API_KEY ?? env.SECONDLAYER_API_KEY}`);
|
|
202822
|
-
}
|
|
202823
|
-
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)}`, "");
|
|
202824
|
-
return lines.join(`
|
|
202825
|
-
`);
|
|
202826
|
-
}
|
|
202827
|
-
function writeInstanceEnv(cwd, env, file = INSTANCE_ENV_FILE) {
|
|
202828
|
-
const path = resolve10(cwd, file);
|
|
202829
|
-
writeFileSync7(path, renderInstanceEnv(env), { mode: 384 });
|
|
202830
|
-
return path;
|
|
202831
|
-
}
|
|
202832
|
-
function loadExistingInstanceEnv(cwd, file = INSTANCE_ENV_FILE) {
|
|
202833
|
-
const path = resolve10(cwd, file);
|
|
202834
|
-
return {
|
|
202835
|
-
INSTANCE_TOKEN: readEnvValue(path, "INSTANCE_TOKEN"),
|
|
202836
|
-
SECONDLAYER_SECRETS_KEY: readEnvValue(path, "SECONDLAYER_SECRETS_KEY"),
|
|
202837
|
-
STREAMS_SIGNING_PRIVATE_KEY: readEnvValue(path, "STREAMS_SIGNING_PRIVATE_KEY"),
|
|
202838
|
-
SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY: readEnvValue(path, "SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY"),
|
|
202839
|
-
SECONDLAYER_API_URL: readEnvValue(path, "SECONDLAYER_API_URL"),
|
|
202840
|
-
SL_API_URL: readEnvValue(path, "SL_API_URL"),
|
|
202841
|
-
SECONDLAYER_API_KEY: readEnvValue(path, "SECONDLAYER_API_KEY"),
|
|
202842
|
-
SL_API_KEY: readEnvValue(path, "SL_API_KEY"),
|
|
202843
|
-
ARCHIVE_SIGNING_PUBLIC_KEY: readEnvValue(path, "ARCHIVE_SIGNING_PUBLIC_KEY")
|
|
202844
|
-
};
|
|
202845
|
-
}
|
|
202846
|
-
var INSTANCE_ENV_FILE = ".env.local";
|
|
202847
|
-
var init_instance_init = __esm(() => {
|
|
202848
|
-
init_output();
|
|
202849
|
-
});
|
|
202850
|
-
|
|
202851
|
-
// src/lib/observer-stanza.ts
|
|
202852
|
-
function parseObserverMode(value) {
|
|
202853
|
-
const mode = value.trim().toLowerCase();
|
|
202854
|
-
if (OBSERVER_MODES.includes(mode)) {
|
|
202855
|
-
return mode;
|
|
202856
|
-
}
|
|
202857
|
-
throw new UnsupportedObserverError(`observer mode must be indexer or signer-shared (got ${value})`);
|
|
202858
|
-
}
|
|
202859
|
-
function parseObserverEndpoint(value) {
|
|
202860
|
-
const trimmed = value.trim();
|
|
202861
|
-
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed) || trimmed.startsWith("unix:")) {
|
|
202862
|
-
throw new UnsupportedObserverError(`observer endpoint must be host:port, not a URL (${value})`);
|
|
202863
|
-
}
|
|
202864
|
-
const match = trimmed.match(/^([a-zA-Z0-9._-]+):(\d+)$/);
|
|
202865
|
-
if (!match) {
|
|
202866
|
-
throw new UnsupportedObserverError(`observer endpoint must be host:port (got ${value})`);
|
|
202867
|
-
}
|
|
202868
|
-
const port = Number(match[2]);
|
|
202869
|
-
if (port < 1 || port > 65535) {
|
|
202870
|
-
throw new UnsupportedObserverError(`observer port out of range (${port})`);
|
|
202871
|
-
}
|
|
202872
|
-
return trimmed;
|
|
202873
|
-
}
|
|
202874
|
-
function validateObserverStanza(input) {
|
|
202875
|
-
const endpoint = parseObserverEndpoint(input.endpoint);
|
|
202876
|
-
const host = endpoint.slice(0, endpoint.lastIndexOf(":"));
|
|
202877
|
-
if (input.network !== "devnet" && LOOPBACK_HOSTS.has(host.toLowerCase())) {
|
|
202878
|
-
throw new UnsupportedObserverError(`loopback observer endpoint is not container-visible on ${input.network}; use a docker DNS name such as indexer:3700`);
|
|
202879
|
-
}
|
|
202880
|
-
return { endpoint };
|
|
202881
|
-
}
|
|
202882
|
-
function renderObserverStanza(input) {
|
|
202883
|
-
const { endpoint } = validateObserverStanza(input);
|
|
202884
|
-
const indexer = input.mode === "indexer";
|
|
202885
|
-
const timeoutMs = indexer ? 2000 : 500;
|
|
202886
|
-
const disableRetries = !indexer;
|
|
202887
|
-
const comment = indexer ? "# Pure indexer: retry delivery. A slow observer can stall the node." : "# Signer-shared: do not retry. A missed block stays a gap until `secondlayer repair`.";
|
|
202888
|
-
return [
|
|
202889
|
-
comment,
|
|
202890
|
-
"[[events_observer]]",
|
|
202891
|
-
`endpoint = "${endpoint}"`,
|
|
202892
|
-
'events_keys = ["*"]',
|
|
202893
|
-
`timeout_ms = ${timeoutMs}`,
|
|
202894
|
-
`disable_retries = ${disableRetries}`,
|
|
202895
|
-
""
|
|
202896
|
-
].join(`
|
|
202897
|
-
`);
|
|
202898
|
-
}
|
|
202899
|
-
function defaultObserverEndpoint(network) {
|
|
202900
|
-
if (network === "devnet")
|
|
202901
|
-
return "127.0.0.1:3700";
|
|
202902
|
-
return "indexer:3700";
|
|
202903
|
-
}
|
|
202904
|
-
var OBSERVER_MODES, UnsupportedObserverError, LOOPBACK_HOSTS;
|
|
202905
|
-
var init_observer_stanza = __esm(() => {
|
|
202906
|
-
OBSERVER_MODES = ["indexer", "signer-shared"];
|
|
202907
|
-
UnsupportedObserverError = class UnsupportedObserverError extends Error {
|
|
202908
|
-
name = "UnsupportedObserverError";
|
|
202909
|
-
};
|
|
202910
|
-
LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "::1"]);
|
|
202911
|
-
});
|
|
202912
|
-
|
|
202913
|
-
// src/lib/oss-compose.ts
|
|
202914
|
-
function buildOssCompose(opts = {}) {
|
|
202915
|
-
const owner = opts.owner ?? DEFAULT_IMAGE_OWNER;
|
|
202916
|
-
const tag = opts.imageTag ?? DEFAULT_IMAGE_TAG;
|
|
202917
|
-
const runtimeImage = `ghcr.io/${owner}/secondlayer-runtime:${tag}`;
|
|
202918
|
-
return `# Generated by \`secondlayer setup\` — do not edit by hand.
|
|
202919
|
-
# Mirrors docker/oss/docker-compose.yml; see that file for the source of truth
|
|
202920
|
-
# on env vars, profiles, and third-party digest pins. \`secondlayer\` is
|
|
202921
|
-
# swapped from a repo build to the published OSS image (no checkout here).
|
|
202922
|
-
name: secondlayer
|
|
202923
|
-
|
|
202924
|
-
services:
|
|
202925
|
-
postgres:
|
|
202926
|
-
image: postgres:17-alpine@sha256:d4bb0a8c1b7bb2e29f976d099e7bfb9a5d8858cffe9e46b35cd302cd1f1f8168
|
|
202927
|
-
environment:
|
|
202928
|
-
POSTGRES_USER: \${POSTGRES_USER:-secondlayer}
|
|
202929
|
-
POSTGRES_PASSWORD: \${POSTGRES_PASSWORD:-secondlayer}
|
|
202930
|
-
POSTGRES_DB: \${POSTGRES_DB:-secondlayer}
|
|
202931
|
-
volumes:
|
|
202932
|
-
- postgres_data:/var/lib/postgresql/data
|
|
202933
|
-
ports:
|
|
202934
|
-
- "\${POSTGRES_PORT:-127.0.0.1:5432}:5432"
|
|
202935
|
-
healthcheck:
|
|
202936
|
-
test: ["CMD-SHELL", "pg_isready -U \${POSTGRES_USER:-secondlayer}"]
|
|
202937
|
-
interval: 10s
|
|
202938
|
-
timeout: 5s
|
|
202939
|
-
retries: 5
|
|
202940
|
-
restart: unless-stopped
|
|
202941
|
-
|
|
202942
|
-
secondlayer:
|
|
202943
|
-
image: ${runtimeImage}
|
|
202944
|
-
environment:
|
|
202945
|
-
INSTANCE_MODE: oss
|
|
202946
|
-
NETWORK: \${NETWORK:-\${STACKS_NETWORK:-mainnet}}
|
|
202947
|
-
STACKS_NETWORK: \${STACKS_NETWORK:-mainnet}
|
|
202948
|
-
NODE_MODE: \${NODE_MODE:-external}
|
|
202949
|
-
SECONDLAYER_ALLOW_UNDERSIZED: \${SECONDLAYER_ALLOW_UNDERSIZED:-false}
|
|
202950
|
-
DATABASE_URL: postgres://\${POSTGRES_USER:-secondlayer}:\${POSTGRES_PASSWORD:-secondlayer}@postgres:5432/\${POSTGRES_DB:-secondlayer}
|
|
202951
|
-
DATA_DIR: /data
|
|
202952
|
-
API_PORT: "3800"
|
|
202953
|
-
INDEXER_PORT: "3700"
|
|
202954
|
-
# See docker/oss/docker-compose.yml: API_PORT above is the *container*
|
|
202955
|
-
# port; \${API_PORT} in ports:/here is the *publish spec* from .env.
|
|
202956
|
-
LISTEN_HOST: "0.0.0.0"
|
|
202957
|
-
API_PUBLISH_ADDR: \${API_PORT:-127.0.0.1:3800}
|
|
202958
|
-
INSTANCE_TOKEN: \${INSTANCE_TOKEN:-}
|
|
202959
|
-
SECONDLAYER_SECRETS_KEY: \${SECONDLAYER_SECRETS_KEY:-}
|
|
202960
|
-
STREAMS_SIGNING_PRIVATE_KEY: \${STREAMS_SIGNING_PRIVATE_KEY:-}
|
|
202961
|
-
SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY: \${SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY:-}
|
|
202962
|
-
ALLOW_UNSIGNED_WEBHOOKS: \${ALLOW_UNSIGNED_WEBHOOKS:-false}
|
|
202963
|
-
LOG_LEVEL: \${LOG_LEVEL:-info}
|
|
202964
|
-
NODE_ENV: production
|
|
202965
|
-
STACKS_NODE_RPC_URL: \${STACKS_NODE_RPC_URL:-}
|
|
202966
|
-
OBSERVER_JOURNAL_ENABLED: \${OBSERVER_JOURNAL_ENABLED:-true}
|
|
202967
|
-
STREAMS_API_URL: http://127.0.0.1:3800
|
|
202968
|
-
STREAMS_INTERNAL_API_KEY: \${STREAMS_INTERNAL_API_KEY:-}
|
|
202969
|
-
SBTC_DECODER_ENABLED: \${SBTC_DECODER_ENABLED:-true}
|
|
202970
|
-
POX4_DECODER_ENABLED: \${POX4_DECODER_ENABLED:-true}
|
|
202971
|
-
BNS_DECODER_ENABLED: \${BNS_DECODER_ENABLED:-false}
|
|
202972
|
-
SUBGRAPH_SOURCE: \${SUBGRAPH_SOURCE:-streams-index}
|
|
202973
|
-
SUBGRAPH_INDEX_API_URL: http://127.0.0.1:3800
|
|
202974
|
-
BITCOIN_RPC_PASSWORD: \${BITCOIN_RPC_PASSWORD:-}
|
|
202975
|
-
ports:
|
|
202976
|
-
- "\${API_PORT:-127.0.0.1:3800}:3800"
|
|
202977
|
-
- "\${INDEXER_PORT:-127.0.0.1:3700}:3700"
|
|
202978
|
-
# \`secondlayer observer\`'s default endpoint is \`indexer:3700\` — this
|
|
202979
|
-
# alias is what makes that default actually resolve on this network,
|
|
202980
|
-
# without renaming the service. Mirrors docker/oss/docker-compose.yml.
|
|
202981
|
-
networks:
|
|
202982
|
-
default:
|
|
202983
|
-
aliases:
|
|
202984
|
-
- indexer
|
|
202985
|
-
volumes:
|
|
202986
|
-
- subgraphs_data:/data/subgraphs
|
|
202987
|
-
depends_on:
|
|
202988
|
-
postgres:
|
|
202989
|
-
condition: service_healthy
|
|
202990
|
-
healthcheck:
|
|
202991
|
-
test: ["CMD", "curl", "-f", "http://localhost:3800/health"]
|
|
202992
|
-
interval: 30s
|
|
202993
|
-
timeout: 5s
|
|
202994
|
-
start_period: 40s
|
|
202995
|
-
retries: 3
|
|
202996
|
-
restart: unless-stopped
|
|
202997
|
-
|
|
202998
|
-
bitcoind:
|
|
202999
|
-
# Digest = kylemanna/bitcoind:latest as published 2026-07-20 — see
|
|
203000
|
-
# docker/oss/docker-compose.yml for why the digest is the real pin here.
|
|
203001
|
-
image: kylemanna/bitcoind:latest@sha256:86bbcaa99bf3bf3d5df7fd6d9217b3a41194db4c4e34385064f2cfc9fa7bdf91
|
|
203002
|
-
volumes:
|
|
203003
|
-
- \${BITCOIN_DATA_DIR:-./data/bitcoin}:/bitcoin/.bitcoin
|
|
203004
|
-
ports:
|
|
203005
|
-
- "127.0.0.1:8332:8332"
|
|
203006
|
-
- "8333:8333"
|
|
203007
|
-
healthcheck:
|
|
203008
|
-
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 ]"]
|
|
203009
|
-
interval: 60s
|
|
203010
|
-
timeout: 15s
|
|
203011
|
-
start_period: 120s
|
|
203012
|
-
retries: 3
|
|
203013
|
-
restart: unless-stopped
|
|
203014
|
-
deploy:
|
|
203015
|
-
resources:
|
|
203016
|
-
limits:
|
|
203017
|
-
memory: 32G
|
|
203018
|
-
profiles: ["full-node"]
|
|
203019
|
-
|
|
203020
|
-
stacks-node:
|
|
203021
|
-
# Consensus-critical — digest-pinned to 4.0.1. See docker/oss/docker-compose.yml.
|
|
203022
|
-
image: ghcr.io/stacks-network/stacks-core:4.0.1@sha256:ceb768f881ef52a1d2792a2b4a89d81e092b1df11293b04c31ce36613c3f9711
|
|
203023
|
-
volumes:
|
|
203024
|
-
- \${STACKS_DATA_DIR:-./data/stacks}:/stacks-blockchain/data
|
|
203025
|
-
- ./Config.toml:/stacks-blockchain/Config.toml:ro
|
|
203026
|
-
ports:
|
|
203027
|
-
- "20443:20443"
|
|
203028
|
-
- "20444:20444"
|
|
203029
|
-
restart: unless-stopped
|
|
203030
|
-
command: ["stacks-node", "start", "--config", "/stacks-blockchain/Config.toml"]
|
|
203031
|
-
# full-node only — see docker/oss/docker-compose.yml. No standalone
|
|
203032
|
-
# "stacks-node, public Bitcoin" profile: nothing here wires the node at
|
|
203033
|
-
# a real public Bitcoin RPC, so that combination never actually synced.
|
|
203034
|
-
profiles: ["full-node"]
|
|
203035
|
-
|
|
203036
|
-
volumes:
|
|
203037
|
-
postgres_data:
|
|
203038
|
-
subgraphs_data:
|
|
203039
|
-
`;
|
|
203040
|
-
}
|
|
203041
|
-
function buildOssStacksConfigToml(opts) {
|
|
203042
|
-
const mode = opts.network === "mainnet" ? "mainnet" : "testnet";
|
|
203043
|
-
return `[node]
|
|
203044
|
-
working_dir = "/stacks-blockchain/data"
|
|
203045
|
-
rpc_bind = "0.0.0.0:20443"
|
|
203046
|
-
p2p_bind = "0.0.0.0:20444"
|
|
203047
|
-
seed = ""
|
|
203048
|
-
marf_compress = true
|
|
203049
|
-
bootstrap_node = "02196f005965cebe6ddc3901b7b1cc1aa7a88f305bb8c5893456b8f9a605923893@seed.mainnet.hiro.so:20444,02539449ad94e6e6392d8c1deb2b4e61f80ae2a18964349bc14336d8b903c46a8c@cet.stacksnodes.org:20444,02ececc8ce79b8adf813f13a0255f8ae58d4357309ba0cedd523d9f1a306fcfb79@sgt.stacksnodes.org:20444,0303144ba518fe7a0fb56a8a7d488f950307a4330f146e1e1458fc63fb33defe96@est.stacksnodes.org:20444"
|
|
203050
|
-
|
|
203051
|
-
# Event observer points directly at the bundled indexer service, same docker
|
|
203052
|
-
# network. Retries off / low timeout: a stuck indexer must never starve a
|
|
203053
|
-
# co-located signer of block_proposal events. See docker/oss/Config.toml.
|
|
203054
|
-
[[events_observer]]
|
|
203055
|
-
endpoint = "secondlayer:3700"
|
|
203056
|
-
events_keys = ["*"]
|
|
203057
|
-
timeout_ms = 500
|
|
203058
|
-
disable_retries = true
|
|
203059
|
-
|
|
203060
|
-
[burnchain]
|
|
203061
|
-
chain = "bitcoin"
|
|
203062
|
-
mode = "${mode}"
|
|
203063
|
-
peer_host = "bitcoind"
|
|
203064
|
-
username = "stacks"
|
|
203065
|
-
password = "${opts.bitcoinRpcPassword}"
|
|
203066
|
-
rpc_port = 8332
|
|
203067
|
-
peer_port = 8333
|
|
203068
|
-
`;
|
|
203069
|
-
}
|
|
203070
|
-
function buildOssBitcoinConf(opts) {
|
|
203071
|
-
return `server=1
|
|
203072
|
-
txindex=1
|
|
203073
|
-
rpcallowip=0.0.0.0/0
|
|
203074
|
-
rpcbind=0.0.0.0
|
|
203075
|
-
rpcuser=stacks
|
|
203076
|
-
rpcpassword=${opts.rpcPassword}
|
|
203077
|
-
# Prune: 0 = archive (required for Stacks); set to 550+ only for pruned chains.
|
|
203078
|
-
prune=0
|
|
203079
|
-
dbcache=4096
|
|
203080
|
-
`;
|
|
203081
|
-
}
|
|
203082
|
-
var init_oss_compose = () => {};
|
|
203083
|
-
|
|
203084
|
-
// src/lib/setup-wizard.ts
|
|
203085
|
-
import { spawn as spawn3, spawnSync as spawnSync5 } from "node:child_process";
|
|
203086
|
-
import { randomBytes as randomBytes4 } from "node:crypto";
|
|
203087
|
-
import { existsSync as existsSync12 } from "node:fs";
|
|
203088
|
-
import { writeFile as writeFile6 } from "node:fs/promises";
|
|
203089
|
-
import { createServer } from "node:net";
|
|
203090
|
-
import { join as join17, resolve as resolvePath2 } from "node:path";
|
|
203091
|
-
import { closeDb as closeDb2, getRawClientFor } from "@secondlayer/shared/db";
|
|
203092
|
-
import { FLOORS, diskFloorGb } from "@secondlayer/shared/runtime";
|
|
203093
|
-
function parseSetupNodeMode(value) {
|
|
203094
|
-
const mode = value.trim().toLowerCase();
|
|
203095
|
-
if (SETUP_NODE_MODES.includes(mode)) {
|
|
203096
|
-
return mode;
|
|
203097
|
-
}
|
|
203098
|
-
throw new Error(`node mode must be external, stacks, or full (got ${value})`);
|
|
203099
|
-
}
|
|
203100
|
-
function resolveNonInteractiveConfig(flags) {
|
|
203101
|
-
if (!flags.network) {
|
|
203102
|
-
throw new MissingSetupFlagError("--network", "Missing --network <mainnet|testnet|devnet>. Non-interactive setup requires it explicitly — there is no safe default.");
|
|
203103
|
-
}
|
|
203104
|
-
if (!flags.nodeMode) {
|
|
203105
|
-
throw new MissingSetupFlagError("--node-mode", "Missing --node-mode <external|stacks|full>. Non-interactive setup requires it explicitly — there is no safe default.");
|
|
203106
|
-
}
|
|
203107
|
-
if (!flags.against && !flags.skipBootstrap) {
|
|
203108
|
-
throw new MissingSetupFlagError("--against", "Missing --against <manifest-url>. Pass an archive manifest to bootstrap from, or --skip-bootstrap to index only what your node sends from now on.");
|
|
203109
|
-
}
|
|
203110
|
-
const network = parseInstanceNetwork(flags.network);
|
|
203111
|
-
const nodeMode = parseSetupNodeMode(flags.nodeMode);
|
|
203112
|
-
const dir = resolvePath2(flags.dir ?? process.cwd());
|
|
203113
|
-
return {
|
|
203114
|
-
network,
|
|
203115
|
-
nodeMode,
|
|
203116
|
-
apiPort: flags.apiPort ?? "127.0.0.1:3800",
|
|
203117
|
-
indexerPort: "127.0.0.1:3700",
|
|
203118
|
-
postgresPort: "127.0.0.1:5432",
|
|
203119
|
-
dir,
|
|
203120
|
-
against: flags.against,
|
|
203121
|
-
skipBootstrap: !!flags.skipBootstrap,
|
|
203122
|
-
skipVerify: !!flags.skipVerify,
|
|
203123
|
-
yes: !!flags.yes,
|
|
203124
|
-
force: !!flags.force,
|
|
203125
|
-
owner: flags.owner ?? DEFAULT_IMAGE_OWNER,
|
|
203126
|
-
imageTag: flags.imageTag ?? DEFAULT_IMAGE_TAG
|
|
203127
|
-
};
|
|
203128
|
-
}
|
|
203129
|
-
function guardrailPreview(nodeMode, network) {
|
|
203130
|
-
return {
|
|
203131
|
-
ramFloorMb: nodeMode === "full" ? FLOORS.fullRamMb : FLOORS.appRamMb,
|
|
203132
|
-
diskFloorGb: diskFloorGb(nodeMode, network)
|
|
203133
|
-
};
|
|
203134
|
-
}
|
|
203135
|
-
function isBunRuntime(versions = typeof process !== "undefined" ? process.versions : undefined) {
|
|
203136
|
-
return typeof versions === "object" && versions !== null && typeof versions.bun === "string";
|
|
203137
|
-
}
|
|
203138
|
-
async function checkDocker() {
|
|
203139
|
-
const probe = spawnSync5("docker", ["info"], { stdio: "ignore" });
|
|
203140
|
-
return !probe.error && probe.status === 0;
|
|
203141
|
-
}
|
|
203142
|
-
function isPortFree(host, port) {
|
|
203143
|
-
return new Promise((resolvePromise) => {
|
|
203144
|
-
const srv = createServer();
|
|
203145
|
-
srv.once("error", () => resolvePromise(false));
|
|
203146
|
-
srv.once("listening", () => srv.close(() => resolvePromise(true)));
|
|
203147
|
-
srv.listen(port, host);
|
|
203148
|
-
});
|
|
203149
|
-
}
|
|
203150
|
-
function parsePublishSpec(spec) {
|
|
203151
|
-
const idx = spec.lastIndexOf(":");
|
|
203152
|
-
if (idx === -1)
|
|
203153
|
-
return { host: "127.0.0.1", port: Number(spec) };
|
|
203154
|
-
return { host: spec.slice(0, idx), port: Number(spec.slice(idx + 1)) };
|
|
203155
|
-
}
|
|
203156
|
-
function dockerPublishedHostPorts() {
|
|
203157
|
-
const probe = spawnSync5("docker", ["ps", "--format", "{{.Ports}}"], {
|
|
203158
|
-
encoding: "utf8"
|
|
203159
|
-
});
|
|
203160
|
-
const ports = new Set;
|
|
203161
|
-
if (probe.error || probe.status !== 0 || !probe.stdout)
|
|
203162
|
-
return ports;
|
|
203163
|
-
for (const match of probe.stdout.matchAll(/:(\d+)->/g)) {
|
|
203164
|
-
ports.add(Number(match[1]));
|
|
203165
|
-
}
|
|
203166
|
-
return ports;
|
|
203167
|
-
}
|
|
203168
|
-
async function resolveAvailablePublishSpec(spec, claimedByDocker = new Set, maxAttempts = 20) {
|
|
203169
|
-
const { host, port } = parsePublishSpec(spec);
|
|
203170
|
-
for (let attempt = 0;attempt < maxAttempts; attempt++) {
|
|
203171
|
-
const candidate = port + attempt;
|
|
203172
|
-
if (claimedByDocker.has(candidate))
|
|
203173
|
-
continue;
|
|
203174
|
-
if (await isPortFree(host, candidate)) {
|
|
203175
|
-
return { spec: `${host}:${candidate}`, remapped: attempt > 0 };
|
|
203176
|
-
}
|
|
203177
|
-
}
|
|
203178
|
-
return { spec, remapped: false };
|
|
203179
|
-
}
|
|
203180
|
-
function randomHex(bytes) {
|
|
203181
|
-
return randomBytes4(bytes).toString("hex");
|
|
203182
|
-
}
|
|
203183
|
-
function resolveSecrets(config) {
|
|
203184
|
-
const envPath = join17(config.dir, ".env");
|
|
203185
|
-
const existing = config.force || !existsSync12(envPath) ? {} : readEnvFile(config.dir, envPath);
|
|
203186
|
-
const instance = buildInstanceEnv({
|
|
203187
|
-
network: config.network,
|
|
203188
|
-
existing: {
|
|
203189
|
-
INSTANCE_TOKEN: existing.INSTANCE_TOKEN,
|
|
203190
|
-
SECONDLAYER_SECRETS_KEY: existing.SECONDLAYER_SECRETS_KEY,
|
|
203191
|
-
STREAMS_SIGNING_PRIVATE_KEY: existing.STREAMS_SIGNING_PRIVATE_KEY,
|
|
203192
|
-
SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY: existing.SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY,
|
|
203193
|
-
SECONDLAYER_API_URL: existing.SECONDLAYER_API_URL,
|
|
203194
|
-
SL_API_URL: existing.SL_API_URL,
|
|
203195
|
-
SECONDLAYER_API_KEY: existing.SECONDLAYER_API_KEY,
|
|
203196
|
-
SL_API_KEY: existing.SL_API_KEY,
|
|
203197
|
-
ARCHIVE_SIGNING_PUBLIC_KEY: existing.ARCHIVE_SIGNING_PUBLIC_KEY
|
|
203198
|
-
},
|
|
203199
|
-
apiUrl: `http://${normalizeLoopbackHost(config.apiPort)}`,
|
|
203200
|
-
archivePublicKeyPem: config.archivePublicKeyPem
|
|
203201
|
-
});
|
|
203202
|
-
return {
|
|
203203
|
-
instance,
|
|
203204
|
-
postgresPassword: existing.POSTGRES_PASSWORD || randomHex(24),
|
|
203205
|
-
bitcoinRpcPassword: existing.BITCOIN_RPC_PASSWORD || randomHex(24),
|
|
203206
|
-
databaseUrl: operatorEditedDatabaseUrl(existing)
|
|
203207
|
-
};
|
|
203208
|
-
}
|
|
203209
|
-
function operatorEditedDatabaseUrl(existing) {
|
|
203210
|
-
const url = existing.DATABASE_URL;
|
|
203211
|
-
if (!url)
|
|
203212
|
-
return;
|
|
203213
|
-
if (existing.POSTGRES_PORT && existing.POSTGRES_PASSWORD) {
|
|
203214
|
-
const generated = setupDatabaseUrl({ postgresPort: existing.POSTGRES_PORT }, { postgresPassword: existing.POSTGRES_PASSWORD });
|
|
203215
|
-
if (url === generated)
|
|
203216
|
-
return;
|
|
203217
|
-
}
|
|
203218
|
-
return url;
|
|
203219
|
-
}
|
|
203220
|
-
function setupDatabaseUrl(config, secrets) {
|
|
203221
|
-
if (secrets.databaseUrl)
|
|
203222
|
-
return secrets.databaseUrl;
|
|
203223
|
-
const password = encodeURIComponent(secrets.postgresPassword);
|
|
203224
|
-
return `postgres://${SETUP_POSTGRES_USER}:${password}@${normalizeLoopbackHost(config.postgresPort)}/${SETUP_POSTGRES_DB}`;
|
|
203225
|
-
}
|
|
203226
|
-
function normalizeLoopbackHost(apiPort) {
|
|
203227
|
-
return apiPort.includes(":") ? apiPort : `127.0.0.1:${apiPort}`;
|
|
203228
|
-
}
|
|
203229
|
-
function readEnvFile(dir, envPath) {
|
|
203230
|
-
const shared = loadExistingInstanceEnv(dir, ".env");
|
|
203231
|
-
return {
|
|
203232
|
-
...shared,
|
|
203233
|
-
POSTGRES_PASSWORD: readEnvValue(envPath, "POSTGRES_PASSWORD"),
|
|
203234
|
-
POSTGRES_PORT: readEnvValue(envPath, "POSTGRES_PORT"),
|
|
203235
|
-
BITCOIN_RPC_PASSWORD: readEnvValue(envPath, "BITCOIN_RPC_PASSWORD"),
|
|
203236
|
-
DATABASE_URL: readEnvValue(envPath, "DATABASE_URL")
|
|
203237
|
-
};
|
|
203238
|
-
}
|
|
203239
|
-
function renderSetupEnv(config, secrets) {
|
|
203240
|
-
const i = secrets.instance;
|
|
203241
|
-
const lines = [
|
|
203242
|
-
"# Generated by `secondlayer setup`. Do not commit.",
|
|
203243
|
-
`NETWORK=${config.network}`,
|
|
203244
|
-
`STACKS_NETWORK=${config.network}`,
|
|
203245
|
-
`NODE_MODE=${config.nodeMode}`,
|
|
203246
|
-
`API_PORT=${config.apiPort}`,
|
|
203247
|
-
`INDEXER_PORT=${config.indexerPort}`,
|
|
203248
|
-
`POSTGRES_USER=${SETUP_POSTGRES_USER}`,
|
|
203249
|
-
`POSTGRES_PASSWORD=${secrets.postgresPassword}`,
|
|
203250
|
-
`POSTGRES_DB=${SETUP_POSTGRES_DB}`,
|
|
203251
|
-
`POSTGRES_PORT=${config.postgresPort}`,
|
|
203252
|
-
"# Where bootstrap, verify, repair, and backup connect from this directory:",
|
|
203253
|
-
"# the compose Postgres above, published on the host. Point it elsewhere",
|
|
203254
|
-
"# only if you moved the index.",
|
|
203255
|
-
`DATABASE_URL=${escapeEnvValue(setupDatabaseUrl(config, secrets))}`,
|
|
203256
|
-
"# The instance credential. The API validates bearer tokens against it,",
|
|
203257
|
-
"# and the CLI/SDK/MCP read it first when authenticating.",
|
|
203258
|
-
`INSTANCE_TOKEN=${i.INSTANCE_TOKEN}`,
|
|
203259
|
-
`SECONDLAYER_SECRETS_KEY=${i.SECONDLAYER_SECRETS_KEY}`,
|
|
203260
|
-
`STREAMS_SIGNING_PRIVATE_KEY=${escapeEnvValue(i.STREAMS_SIGNING_PRIVATE_KEY)}`,
|
|
203261
|
-
`SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY=${escapeEnvValue(i.SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY)}`,
|
|
203262
|
-
`ALLOW_UNSIGNED_WEBHOOKS=${i.ALLOW_UNSIGNED_WEBHOOKS}`,
|
|
203263
|
-
`SECONDLAYER_API_URL=${i.SECONDLAYER_API_URL}`,
|
|
203264
|
-
`SL_API_URL=${i.SL_API_URL}`
|
|
203265
|
-
];
|
|
203266
|
-
if (i.SECONDLAYER_API_KEY) {
|
|
203267
|
-
lines.push("# Hosted account key for archive credits (sk-sl_*).", `SECONDLAYER_API_KEY=${i.SECONDLAYER_API_KEY}`, `SL_API_KEY=${i.SL_API_KEY ?? i.SECONDLAYER_API_KEY}`);
|
|
203268
|
-
}
|
|
203269
|
-
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)}`);
|
|
203270
|
-
if (config.nodeMode === "full") {
|
|
203271
|
-
lines.push("BITCOIN_RPC_USER=stacks", `BITCOIN_RPC_PASSWORD=${secrets.bitcoinRpcPassword}`);
|
|
203272
|
-
}
|
|
203273
|
-
lines.push("");
|
|
203274
|
-
return lines.join(`
|
|
203275
|
-
`);
|
|
203276
|
-
}
|
|
203277
|
-
async function writeSetupFiles(config, secrets) {
|
|
203278
|
-
const composePath = join17(config.dir, "docker-compose.yml");
|
|
203279
|
-
const envPath = join17(config.dir, ".env");
|
|
203280
|
-
await writeFile6(composePath, buildOssCompose({ owner: config.owner, imageTag: config.imageTag }));
|
|
203281
|
-
await writeFile6(envPath, renderSetupEnv(config, secrets), { mode: 384 });
|
|
203282
|
-
const result = { composePath, envPath };
|
|
203283
|
-
if (config.nodeMode === "full") {
|
|
203284
|
-
const configTomlPath = join17(config.dir, "Config.toml");
|
|
203285
|
-
await writeFile6(configTomlPath, buildOssStacksConfigToml({
|
|
203286
|
-
network: config.network,
|
|
203287
|
-
bitcoinRpcPassword: secrets.bitcoinRpcPassword ?? ""
|
|
203288
|
-
}));
|
|
203289
|
-
result.configTomlPath = configTomlPath;
|
|
203290
|
-
}
|
|
203291
|
-
if (config.nodeMode === "full") {
|
|
203292
|
-
const bitcoinConfPath = join17(config.dir, "bitcoin.conf");
|
|
203293
|
-
await writeFile6(bitcoinConfPath, buildOssBitcoinConf({ rpcPassword: secrets.bitcoinRpcPassword ?? "" }));
|
|
203294
|
-
result.bitcoinConfPath = bitcoinConfPath;
|
|
203295
|
-
}
|
|
203296
|
-
return result;
|
|
203297
|
-
}
|
|
203298
|
-
function composeProfileArgs(nodeMode) {
|
|
203299
|
-
if (nodeMode === "full")
|
|
203300
|
-
return ["--profile", "full-node"];
|
|
203301
|
-
return [];
|
|
203302
|
-
}
|
|
203303
|
-
function runStreaming(command, args, opts) {
|
|
203304
|
-
return new Promise((resolvePromise, reject) => {
|
|
203305
|
-
const child = spawn3(command, args, {
|
|
203306
|
-
cwd: opts.cwd,
|
|
203307
|
-
env: opts.env ?? process.env,
|
|
203308
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
203309
|
-
});
|
|
203310
|
-
let stdout = "";
|
|
203311
|
-
let stderr = "";
|
|
203312
|
-
const pump = (stream) => (chunk) => {
|
|
203313
|
-
const text = chunk.toString("utf8");
|
|
203314
|
-
if (stream === "stdout")
|
|
203315
|
-
stdout += text;
|
|
203316
|
-
else
|
|
203317
|
-
stderr += text;
|
|
203318
|
-
if (opts.onLine) {
|
|
203319
|
-
for (const line of text.split(`
|
|
203320
|
-
`)) {
|
|
203321
|
-
if (line.length > 0)
|
|
203322
|
-
opts.onLine(line, stream);
|
|
203323
|
-
}
|
|
203324
|
-
}
|
|
203325
|
-
};
|
|
203326
|
-
child.stdout?.on("data", pump("stdout"));
|
|
203327
|
-
child.stderr?.on("data", pump("stderr"));
|
|
203328
|
-
child.once("error", reject);
|
|
203329
|
-
child.once("close", (code) => {
|
|
203330
|
-
resolvePromise({ code: code ?? 1, stdout, stderr });
|
|
203331
|
-
});
|
|
203332
|
-
});
|
|
203333
|
-
}
|
|
203334
|
-
async function dockerComposeUp(config, files, onLine) {
|
|
203335
|
-
const args = [
|
|
203336
|
-
"compose",
|
|
203337
|
-
"-f",
|
|
203338
|
-
files.composePath,
|
|
203339
|
-
"--env-file",
|
|
203340
|
-
files.envPath,
|
|
203341
|
-
...composeProfileArgs(config.nodeMode),
|
|
203342
|
-
"up",
|
|
203343
|
-
"-d"
|
|
203344
|
-
];
|
|
203345
|
-
return runStreaming("docker", args, { cwd: config.dir, onLine });
|
|
203346
|
-
}
|
|
203347
|
-
async function pollHealth(config, opts = {}) {
|
|
203348
|
-
const timeoutMs = opts.timeoutMs ?? 120000;
|
|
203349
|
-
const intervalMs = opts.intervalMs ?? 2000;
|
|
203350
|
-
const url = `http://${normalizeLoopbackHost(config.apiPort)}/health`;
|
|
203351
|
-
const deadline = Date.now() + timeoutMs;
|
|
203352
|
-
for (;; ) {
|
|
203353
|
-
try {
|
|
203354
|
-
const res = await fetch(url, { signal: AbortSignal.timeout(3000) });
|
|
203355
|
-
if (res.ok)
|
|
203356
|
-
return true;
|
|
203357
|
-
} catch {}
|
|
203358
|
-
opts.onTick?.();
|
|
203359
|
-
if (Date.now() >= deadline)
|
|
203360
|
-
return false;
|
|
203361
|
-
await new Promise((r) => setTimeout(r, intervalMs));
|
|
203362
|
-
}
|
|
203363
|
-
}
|
|
203364
|
-
function getObserverInfo(config) {
|
|
203365
|
-
if (config.nodeMode !== "external") {
|
|
203366
|
-
return { applicable: false };
|
|
203367
|
-
}
|
|
203368
|
-
const endpoint = defaultObserverEndpoint(config.network);
|
|
203369
|
-
const stanza = renderObserverStanza({
|
|
203370
|
-
mode: "indexer",
|
|
203371
|
-
endpoint,
|
|
203372
|
-
network: config.network
|
|
203373
|
-
});
|
|
203374
|
-
return { applicable: true, stanza, endpoint };
|
|
203375
|
-
}
|
|
203376
|
-
function cliEntry() {
|
|
203377
|
-
const entry = process.argv[1];
|
|
203378
|
-
if (!entry) {
|
|
203379
|
-
throw new Error("Could not resolve the secondlayer CLI entry to shell out to.");
|
|
203380
|
-
}
|
|
203381
|
-
return [process.execPath, entry];
|
|
203382
|
-
}
|
|
203383
|
-
function buildChildEnv(envPath, base = process.env) {
|
|
203384
|
-
const env = { ...base };
|
|
203385
|
-
for (const key of CHILD_ENV_KEYS) {
|
|
203386
|
-
const value = readEnvValue(envPath, key);
|
|
203387
|
-
if (value)
|
|
203388
|
-
env[key] = value;
|
|
203389
|
-
}
|
|
203390
|
-
return env;
|
|
203391
|
-
}
|
|
203392
|
-
async function checkDatabaseReachable(databaseUrl) {
|
|
203393
|
-
try {
|
|
203394
|
-
const client = getRawClientFor(databaseUrl);
|
|
203395
|
-
await client`SELECT 1`;
|
|
203396
|
-
return { ok: true };
|
|
203397
|
-
} catch (err) {
|
|
203398
|
-
return {
|
|
203399
|
-
ok: false,
|
|
203400
|
-
error: err instanceof Error ? err.message : String(err)
|
|
203401
|
-
};
|
|
203402
|
-
} finally {
|
|
203403
|
-
await closeDb2().catch(() => {});
|
|
203404
|
-
}
|
|
203405
|
-
}
|
|
203406
|
-
async function preflightBootstrapDatabase(config, secrets, emit) {
|
|
203407
|
-
const databaseUrl = setupDatabaseUrl(config, secrets);
|
|
203408
|
-
const reachable = await checkDatabaseReachable(databaseUrl);
|
|
203409
|
-
if (!reachable.ok) {
|
|
203410
|
-
emit({
|
|
203411
|
-
type: "step-error",
|
|
203412
|
-
step: "bootstrap",
|
|
203413
|
-
message: `Postgres at ${redactUrl(databaseUrl)} did not answer SELECT 1: ${reachable.error}. Check \`docker compose logs postgres\`, then re-run setup to resume.`
|
|
203414
|
-
});
|
|
203415
|
-
return null;
|
|
203416
|
-
}
|
|
203417
|
-
emit({
|
|
203418
|
-
type: "step-log",
|
|
203419
|
-
step: "bootstrap",
|
|
203420
|
-
line: `database ${redactUrl(databaseUrl)}`
|
|
203421
|
-
});
|
|
203422
|
-
return databaseUrl;
|
|
203423
|
-
}
|
|
203424
|
-
async function runBootstrap(config, against, files, onLine) {
|
|
203425
|
-
const [command, ...prefix] = cliEntry();
|
|
203426
|
-
const args = [...prefix, "bootstrap", "--against", against, "--yes"];
|
|
203427
|
-
return runStreaming(command, args, {
|
|
203428
|
-
cwd: config.dir,
|
|
203429
|
-
env: buildChildEnv(files.envPath),
|
|
203430
|
-
onLine
|
|
203431
|
-
});
|
|
203432
|
-
}
|
|
203433
|
-
async function runVerify(config, against, files, onLine) {
|
|
203434
|
-
const [command, ...prefix] = cliEntry();
|
|
203435
|
-
const args = [...prefix, "verify", "all", "--against", against];
|
|
203436
|
-
return runStreaming(command, args, {
|
|
203437
|
-
cwd: config.dir,
|
|
203438
|
-
env: buildChildEnv(files.envPath),
|
|
203439
|
-
onLine
|
|
203440
|
-
});
|
|
203441
|
-
}
|
|
203442
|
-
function redactUrl(url) {
|
|
203443
|
-
try {
|
|
203444
|
-
const u = new URL(url);
|
|
203445
|
-
if (u.password)
|
|
203446
|
-
u.password = "***";
|
|
203447
|
-
return u.toString();
|
|
203448
|
-
} catch {
|
|
203449
|
-
return url;
|
|
203450
|
-
}
|
|
203451
|
-
}
|
|
203452
|
-
function buildSuccessSummary(config, secrets) {
|
|
203453
|
-
const apiUrl = `http://${normalizeLoopbackHost(config.apiPort)}`;
|
|
203454
|
-
const lines = [
|
|
203455
|
-
"Secondlayer is up.",
|
|
203456
|
-
"",
|
|
203457
|
-
` api ${apiUrl}`,
|
|
203458
|
-
` network ${config.network}`,
|
|
203459
|
-
` node mode ${config.nodeMode}`,
|
|
203460
|
-
` dir ${config.dir}`,
|
|
203461
|
-
"",
|
|
203462
|
-
"Export the instance token for writes (reads on loopback need no credential):",
|
|
203463
|
-
` export INSTANCE_TOKEN=${secrets.instance.INSTANCE_TOKEN}`,
|
|
203464
|
-
"",
|
|
203465
|
-
"Try it:",
|
|
203466
|
-
" secondlayer subgraphs create my-balances --from-contract SP....my-contract",
|
|
203467
|
-
" secondlayer subgraphs deploy subgraphs/my-balances.ts",
|
|
203468
|
-
` curl ${apiUrl}/v1/subgraphs/my-balances/balances`,
|
|
203469
|
-
"",
|
|
203470
|
-
"Docs: https://secondlayer.tools/docs/self-host"
|
|
203471
|
-
];
|
|
203472
|
-
return lines.join(`
|
|
203473
|
-
`);
|
|
203474
|
-
}
|
|
203475
|
-
async function runSetup(initialConfig, emit) {
|
|
203476
|
-
let config = initialConfig;
|
|
203477
|
-
emit({ type: "step-start", step: "preflight" });
|
|
203478
|
-
if (!await checkDocker()) {
|
|
203479
|
-
emit({
|
|
203480
|
-
type: "step-error",
|
|
203481
|
-
step: "preflight",
|
|
203482
|
-
message: "Docker isn't reachable. Start Docker Desktop/OrbStack, or install Docker, then re-run."
|
|
203483
|
-
});
|
|
203484
|
-
return { ok: false, config };
|
|
203485
|
-
}
|
|
203486
|
-
const claimedByDocker = dockerPublishedHostPorts();
|
|
203487
|
-
const ports = {
|
|
203488
|
-
api: await resolveAvailablePublishSpec(config.apiPort, claimedByDocker),
|
|
203489
|
-
indexer: await resolveAvailablePublishSpec(config.indexerPort, claimedByDocker),
|
|
203490
|
-
postgres: await resolveAvailablePublishSpec(config.postgresPort, claimedByDocker)
|
|
203491
|
-
};
|
|
203492
|
-
config = {
|
|
203493
|
-
...config,
|
|
203494
|
-
apiPort: ports.api.spec,
|
|
203495
|
-
indexerPort: ports.indexer.spec,
|
|
203496
|
-
postgresPort: ports.postgres.spec
|
|
203497
|
-
};
|
|
203498
|
-
for (const [label, result] of [
|
|
203499
|
-
["api", ports.api],
|
|
203500
|
-
["indexer", ports.indexer],
|
|
203501
|
-
["postgres", ports.postgres]
|
|
203502
|
-
]) {
|
|
203503
|
-
if (result.remapped) {
|
|
203504
|
-
emit({
|
|
203505
|
-
type: "step-log",
|
|
203506
|
-
step: "preflight",
|
|
203507
|
-
line: `${label} port was already in use locally, using ${result.spec} instead`
|
|
203508
|
-
});
|
|
203509
|
-
}
|
|
203510
|
-
}
|
|
203511
|
-
const floors = guardrailPreview(config.nodeMode, config.network);
|
|
203512
|
-
emit({
|
|
203513
|
-
type: "step-done",
|
|
203514
|
-
step: "preflight",
|
|
203515
|
-
detail: `NODE_MODE=${config.nodeMode} on ${config.network} needs at least ${floors.ramFloorMb}MB RAM and ${floors.diskFloorGb}GB disk.`
|
|
203516
|
-
});
|
|
203517
|
-
emit({ type: "step-start", step: "secrets" });
|
|
203518
|
-
const secrets = resolveSecrets({
|
|
203519
|
-
dir: config.dir,
|
|
203520
|
-
network: config.network,
|
|
203521
|
-
apiPort: config.apiPort,
|
|
203522
|
-
force: config.force,
|
|
203523
|
-
archivePublicKeyPem: await resolveArchivePublicKey({
|
|
203524
|
-
envPem: process.env.ARCHIVE_SIGNING_PUBLIC_KEY ?? process.env.STREAMS_SIGNING_PUBLIC_KEY,
|
|
203525
|
-
allowHostedApi: !isOssMode()
|
|
203526
|
-
})
|
|
203527
|
-
});
|
|
203528
|
-
emit({ type: "step-done", step: "secrets" });
|
|
203529
|
-
emit({ type: "step-start", step: "config" });
|
|
203530
|
-
const files = await writeSetupFiles(config, secrets);
|
|
203531
|
-
emit({
|
|
203532
|
-
type: "step-done",
|
|
203533
|
-
step: "config",
|
|
203534
|
-
detail: `${files.composePath}, ${files.envPath}`
|
|
203535
|
-
});
|
|
203536
|
-
emit({ type: "step-start", step: "docker-up" });
|
|
203537
|
-
const up = await dockerComposeUp(config, files, (line) => emit({ type: "step-log", step: "docker-up", line }));
|
|
203538
|
-
if (up.code !== 0) {
|
|
203539
|
-
emit({
|
|
203540
|
-
type: "step-error",
|
|
203541
|
-
step: "docker-up",
|
|
203542
|
-
message: `docker compose up failed (exit ${up.code}).
|
|
203543
|
-
${up.stderr || up.stdout}`
|
|
203544
|
-
});
|
|
203545
|
-
return { ok: false, config, secrets, files };
|
|
203546
|
-
}
|
|
203547
|
-
const healthy = await pollHealth(config, {
|
|
203548
|
-
onTick: () => emit({
|
|
203549
|
-
type: "step-log",
|
|
203550
|
-
step: "docker-up",
|
|
203551
|
-
line: "waiting for /health…"
|
|
203552
|
-
})
|
|
203553
|
-
});
|
|
203554
|
-
if (!healthy) {
|
|
203555
|
-
emit({
|
|
203556
|
-
type: "step-error",
|
|
203557
|
-
step: "docker-up",
|
|
203558
|
-
message: "Containers started but /health never came up. Check `docker compose logs`."
|
|
203559
|
-
});
|
|
203560
|
-
return { ok: false, config, secrets, files };
|
|
203561
|
-
}
|
|
203562
|
-
emit({ type: "step-done", step: "docker-up" });
|
|
203563
|
-
emit({ type: "step-start", step: "observer" });
|
|
203564
|
-
const observer = getObserverInfo(config);
|
|
203565
|
-
if (observer.applicable && observer.stanza) {
|
|
203566
|
-
emit({
|
|
203567
|
-
type: "step-log",
|
|
203568
|
-
step: "observer",
|
|
203569
|
-
line: "ACTION REQUIRED — paste this into your Stacks node's Config.toml:"
|
|
203570
|
-
});
|
|
203571
|
-
for (const line of observer.stanza.split(`
|
|
203572
|
-
`)) {
|
|
203573
|
-
if (line.length > 0)
|
|
203574
|
-
emit({ type: "step-log", step: "observer", line });
|
|
203575
|
-
}
|
|
203576
|
-
emit({
|
|
203577
|
-
type: "step-done",
|
|
203578
|
-
step: "observer",
|
|
203579
|
-
detail: `endpoint ${observer.endpoint} — see the ACTION REQUIRED lines above`
|
|
203580
|
-
});
|
|
203581
|
-
} else {
|
|
203582
|
-
emit({
|
|
203583
|
-
type: "step-done",
|
|
203584
|
-
step: "observer",
|
|
203585
|
-
detail: "bundled node — the observer is pre-configured, nothing to paste"
|
|
203586
|
-
});
|
|
203587
|
-
}
|
|
203588
|
-
let bootstrapped = false;
|
|
203589
|
-
if (config.skipBootstrap) {
|
|
203590
|
-
emit({ type: "step-skip", step: "bootstrap", reason: "--skip-bootstrap" });
|
|
203591
|
-
} else if (config.against) {
|
|
203592
|
-
emit({ type: "step-start", step: "bootstrap" });
|
|
203593
|
-
const databaseUrl = await preflightBootstrapDatabase(config, secrets, emit);
|
|
203594
|
-
if (databaseUrl === null) {
|
|
203595
|
-
return { ok: false, config, secrets, files, observer };
|
|
203596
|
-
}
|
|
203597
|
-
const res = await runBootstrap(config, config.against, files, (line) => emit({ type: "step-log", step: "bootstrap", line }));
|
|
203598
|
-
if (res.code === 0) {
|
|
203599
|
-
bootstrapped = true;
|
|
203600
|
-
emit({ type: "step-done", step: "bootstrap" });
|
|
203601
|
-
} else {
|
|
203602
|
-
emit({
|
|
203603
|
-
type: "step-error",
|
|
203604
|
-
step: "bootstrap",
|
|
203605
|
-
message: `secondlayer bootstrap exited ${res.code}.`
|
|
203606
|
-
});
|
|
203607
|
-
}
|
|
203608
|
-
}
|
|
203609
|
-
if (config.skipVerify) {
|
|
203610
|
-
emit({ type: "step-skip", step: "verify", reason: "--skip-verify" });
|
|
203611
|
-
} else if (!config.against) {
|
|
203612
|
-
emit({
|
|
203613
|
-
type: "step-skip",
|
|
203614
|
-
step: "verify",
|
|
203615
|
-
reason: "no manifest to verify against"
|
|
203616
|
-
});
|
|
203617
|
-
} else if (!bootstrapped) {
|
|
203618
|
-
emit({
|
|
203619
|
-
type: "step-skip",
|
|
203620
|
-
step: "verify",
|
|
203621
|
-
reason: "bootstrap did not run — nothing restored yet to verify"
|
|
203622
|
-
});
|
|
203623
|
-
} else {
|
|
203624
|
-
emit({ type: "step-start", step: "verify" });
|
|
203625
|
-
const res = await runVerify(config, config.against, files, (line) => emit({ type: "step-log", step: "verify", line }));
|
|
203626
|
-
if (res.code === 0) {
|
|
203627
|
-
emit({ type: "step-done", step: "verify" });
|
|
203628
|
-
} else {
|
|
203629
|
-
emit({
|
|
203630
|
-
type: "step-error",
|
|
203631
|
-
step: "verify",
|
|
203632
|
-
message: `secondlayer verify exited ${res.code}.`
|
|
203633
|
-
});
|
|
203634
|
-
}
|
|
203635
|
-
}
|
|
203636
|
-
const summary = buildSuccessSummary(config, secrets);
|
|
203637
|
-
emit({ type: "step-start", step: "summary" });
|
|
203638
|
-
emit({ type: "step-done", step: "summary", detail: summary });
|
|
203639
|
-
return { ok: true, config, secrets, files, observer, summary };
|
|
203640
|
-
}
|
|
203641
|
-
var SETUP_NODE_MODES, DEFAULT_ARCHIVE_MANIFEST = "https://archive.secondlayer.tools/latest.json", MissingSetupFlagError, SETUP_POSTGRES_USER = "secondlayer", SETUP_POSTGRES_DB = "secondlayer", CHILD_ENV_KEYS;
|
|
203642
|
-
var init_setup_wizard = __esm(() => {
|
|
203643
|
-
init_archive_reference();
|
|
203644
|
-
init_instance_init();
|
|
203645
|
-
init_observer_stanza();
|
|
203646
|
-
init_oss_compose();
|
|
203647
|
-
init_resolve_auth();
|
|
203648
|
-
SETUP_NODE_MODES = ["external", "stacks", "full"];
|
|
203649
|
-
MissingSetupFlagError = class MissingSetupFlagError extends Error {
|
|
203650
|
-
flag;
|
|
203651
|
-
constructor(flag, message) {
|
|
203652
|
-
super(message);
|
|
203653
|
-
this.name = "MissingSetupFlagError";
|
|
203654
|
-
this.flag = flag;
|
|
203655
|
-
}
|
|
203656
|
-
};
|
|
203657
|
-
CHILD_ENV_KEYS = [
|
|
203658
|
-
"DATABASE_URL",
|
|
203659
|
-
"ARCHIVE_SIGNING_PUBLIC_KEY",
|
|
203660
|
-
"SL_API_URL",
|
|
203661
|
-
"INSTANCE_TOKEN"
|
|
203662
|
-
];
|
|
203663
|
-
});
|
|
203664
|
-
|
|
203665
|
-
// src/lib/setup-tui.tsx
|
|
203666
|
-
import { resolve as resolvePath4 } from "node:path";
|
|
203667
|
-
import {
|
|
203668
|
-
CliRenderEvents,
|
|
203669
|
-
createCliRenderer
|
|
203670
|
-
} from "@opentui/core";
|
|
203671
|
-
import { createRoot, useKeyboard, useRenderer } from "@opentui/react";
|
|
203672
|
-
import { createContext, useContext, useEffect, useRef, useState } from "react";
|
|
203673
|
-
import { jsxDEV } from "@opentui/react/jsx-dev-runtime";
|
|
203674
|
-
function useTerminalThemeMode() {
|
|
203675
|
-
const renderer = useRenderer();
|
|
203676
|
-
const [mode, setMode] = useState(renderer.themeMode ?? "dark");
|
|
203677
|
-
useEffect(() => {
|
|
203678
|
-
let cancelled = false;
|
|
203679
|
-
renderer.waitForThemeMode(500).then((resolved) => {
|
|
203680
|
-
if (!cancelled && resolved)
|
|
203681
|
-
setMode(resolved);
|
|
203682
|
-
});
|
|
203683
|
-
const onThemeMode = (next) => setMode(next);
|
|
203684
|
-
renderer.on(CliRenderEvents.THEME_MODE, onThemeMode);
|
|
203685
|
-
return () => {
|
|
203686
|
-
cancelled = true;
|
|
203687
|
-
renderer.off(CliRenderEvents.THEME_MODE, onThemeMode);
|
|
203688
|
-
};
|
|
203689
|
-
}, [renderer]);
|
|
203690
|
-
return mode;
|
|
203691
|
-
}
|
|
203692
|
-
function formatFloor(f) {
|
|
203693
|
-
return `needs ≥ ${(f.ramFloorMb / 1024).toFixed(0)}GB RAM, ≥ ${f.diskFloorGb}GB disk`;
|
|
203694
|
-
}
|
|
203695
|
-
function useSpinner(active) {
|
|
203696
|
-
const [frame, setFrame] = useState(0);
|
|
203697
|
-
useEffect(() => {
|
|
203698
|
-
if (!active)
|
|
203699
|
-
return;
|
|
203700
|
-
const id = setInterval(() => setFrame((f) => (f + 1) % SPINNER_FRAMES.length), 80);
|
|
203701
|
-
return () => clearInterval(id);
|
|
203702
|
-
}, [active]);
|
|
203703
|
-
return SPINNER_FRAMES[frame];
|
|
203704
|
-
}
|
|
203705
|
-
function App({ flags }) {
|
|
203706
|
-
const renderer = useRenderer();
|
|
203707
|
-
const themeMode = useTerminalThemeMode();
|
|
203708
|
-
const colors = themeMode === "light" ? LIGHT_COLORS : DARK_COLORS;
|
|
203709
|
-
const [stage, setStage] = useState("welcome");
|
|
203710
|
-
const [network, setNetwork] = useState(safeParseNetwork(flags.network));
|
|
203711
|
-
const [nodeMode, setNodeMode] = useState(safeParseNodeMode(flags.nodeMode));
|
|
203712
|
-
const [preview, setPreview] = useState(guardrailPreview(NODE_MODE_OPTIONS[0].value, "mainnet"));
|
|
203713
|
-
const [against, setAgainst] = useState(flags.against);
|
|
203714
|
-
const [skipBootstrap, setSkipBootstrap] = useState(!!flags.skipBootstrap);
|
|
203715
|
-
const [events, setEvents] = useState([]);
|
|
203716
|
-
const [log, setLog] = useState([]);
|
|
203717
|
-
const [fatal, setFatal] = useState(null);
|
|
203718
|
-
const [summary, setSummary] = useState(null);
|
|
203719
|
-
const started = useRef(false);
|
|
203720
|
-
function exit(code) {
|
|
203721
|
-
renderer.destroy();
|
|
203722
|
-
process.exit(code);
|
|
203723
|
-
}
|
|
203724
|
-
useKeyboard((key) => {
|
|
203725
|
-
if (key.eventType === "release")
|
|
203726
|
-
return;
|
|
203727
|
-
if ((stage === "done" || stage === "error") && (key.name === "q" || key.name === "return")) {
|
|
203728
|
-
exit(fatal ? 1 : 0);
|
|
203729
|
-
return;
|
|
203730
|
-
}
|
|
203731
|
-
if (stage === "welcome" && (key.name === "return" || key.name === "space")) {
|
|
203732
|
-
advanceFromWelcome();
|
|
203733
|
-
return;
|
|
203734
|
-
}
|
|
203735
|
-
if (stage === "welcome" && key.name === "q") {
|
|
203736
|
-
exit(0);
|
|
203737
|
-
}
|
|
203738
|
-
});
|
|
203739
|
-
function advanceFromWelcome() {
|
|
203740
|
-
if (!network) {
|
|
203741
|
-
setStage("network");
|
|
203742
|
-
} else if (!nodeMode) {
|
|
203743
|
-
setStage("nodeMode");
|
|
203744
|
-
} else if (!against && !skipBootstrap) {
|
|
203745
|
-
setStage("manifestChoice");
|
|
203746
|
-
} else {
|
|
203747
|
-
setStage("confirm");
|
|
203748
|
-
}
|
|
203749
|
-
}
|
|
203750
|
-
function afterNetwork(net) {
|
|
203751
|
-
setNetwork(net);
|
|
203752
|
-
if (!nodeMode)
|
|
203753
|
-
setStage("nodeMode");
|
|
203754
|
-
else if (!against && !skipBootstrap)
|
|
203755
|
-
setStage("manifestChoice");
|
|
203756
|
-
else
|
|
203757
|
-
setStage("confirm");
|
|
203758
|
-
}
|
|
203759
|
-
function afterNodeMode(mode) {
|
|
203760
|
-
setNodeMode(mode);
|
|
203761
|
-
if (!against && !skipBootstrap)
|
|
203762
|
-
setStage("manifestChoice");
|
|
203763
|
-
else
|
|
203764
|
-
setStage("confirm");
|
|
203765
|
-
}
|
|
203766
|
-
const config = network && nodeMode ? {
|
|
203767
|
-
network,
|
|
203768
|
-
nodeMode,
|
|
203769
|
-
apiPort: flags.apiPort ?? "127.0.0.1:3800",
|
|
203770
|
-
indexerPort: "127.0.0.1:3700",
|
|
203771
|
-
postgresPort: "127.0.0.1:5432",
|
|
203772
|
-
dir: resolvePath4(flags.dir ?? process.cwd()),
|
|
203773
|
-
against: skipBootstrap ? undefined : against,
|
|
203774
|
-
skipBootstrap,
|
|
203775
|
-
skipVerify: !!flags.skipVerify,
|
|
203776
|
-
yes: false,
|
|
203777
|
-
force: !!flags.force,
|
|
203778
|
-
owner: flags.owner ?? DEFAULT_IMAGE_OWNER,
|
|
203779
|
-
imageTag: flags.imageTag ?? DEFAULT_IMAGE_TAG
|
|
203780
|
-
} : null;
|
|
203781
|
-
useEffect(() => {
|
|
203782
|
-
if (stage !== "running" || started.current || !config)
|
|
203783
|
-
return;
|
|
203784
|
-
started.current = true;
|
|
203785
|
-
let failed = false;
|
|
203786
|
-
runSetup(config, (event) => {
|
|
203787
|
-
setEvents((prev) => [...prev, event]);
|
|
203788
|
-
if (event.type === "step-log") {
|
|
203789
|
-
setLog((prev) => [...prev.slice(-40), event.line]);
|
|
203790
|
-
}
|
|
203791
|
-
if (event.type === "step-error") {
|
|
203792
|
-
failed = true;
|
|
203793
|
-
setLog((prev) => [...prev.slice(-40), `ERROR: ${event.message}`]);
|
|
203794
|
-
}
|
|
203795
|
-
}).then((result) => {
|
|
203796
|
-
if (!result.ok) {
|
|
203797
|
-
setFatal("Setup could not finish — see the log above.");
|
|
203798
|
-
setStage("error");
|
|
203799
|
-
return;
|
|
203800
|
-
}
|
|
203801
|
-
setSummary(result.summary ?? null);
|
|
203802
|
-
setStage(failed ? "error" : "done");
|
|
203803
|
-
if (failed)
|
|
203804
|
-
setFatal("Setup finished but at least one step reported an error.");
|
|
203805
|
-
}).catch((err) => {
|
|
203806
|
-
setFatal(err instanceof Error ? err.message : String(err));
|
|
203807
|
-
setStage("error");
|
|
203808
|
-
});
|
|
203809
|
-
}, [stage, config]);
|
|
203810
|
-
return /* @__PURE__ */ jsxDEV(ColorsContext.Provider, {
|
|
203811
|
-
value: colors,
|
|
203812
|
-
children: /* @__PURE__ */ jsxDEV("box", {
|
|
203813
|
-
style: {
|
|
203814
|
-
border: true,
|
|
203815
|
-
borderStyle: "single",
|
|
203816
|
-
borderColor: colors.borderIdle,
|
|
203817
|
-
backgroundColor: colors.bg,
|
|
203818
|
-
padding: 1
|
|
203819
|
-
},
|
|
203820
|
-
title: "secondlayer setup",
|
|
203821
|
-
children: /* @__PURE__ */ jsxDEV("box", {
|
|
203822
|
-
style: { flexDirection: "column" },
|
|
203823
|
-
children: [
|
|
203824
|
-
stage === "welcome" && /* @__PURE__ */ jsxDEV(Welcome, {}, undefined, false, undefined, this),
|
|
203825
|
-
stage === "network" && /* @__PURE__ */ jsxDEV(Picker, {
|
|
203826
|
-
title: "Network",
|
|
203827
|
-
options: NETWORK_OPTIONS,
|
|
203828
|
-
onSelect: (value) => afterNetwork(parseInstanceNetwork(value))
|
|
203829
|
-
}, undefined, false, undefined, this),
|
|
203830
|
-
stage === "nodeMode" && /* @__PURE__ */ jsxDEV(Picker, {
|
|
203831
|
-
title: "Node mode",
|
|
203832
|
-
options: NODE_MODE_OPTIONS,
|
|
203833
|
-
subtitle: formatFloor(preview),
|
|
203834
|
-
onChange: (value) => setPreview(guardrailPreview(parseSetupNodeMode(value), network ?? "mainnet")),
|
|
203835
|
-
onSelect: (value) => afterNodeMode(parseSetupNodeMode(value))
|
|
203836
|
-
}, undefined, false, undefined, this),
|
|
203837
|
-
stage === "manifestChoice" && /* @__PURE__ */ jsxDEV(Picker, {
|
|
203838
|
-
title: "Bootstrap from",
|
|
203839
|
-
options: [
|
|
203840
|
-
{
|
|
203841
|
-
name: "Hosted archive (recommended)",
|
|
203842
|
-
description: DEFAULT_ARCHIVE_MANIFEST,
|
|
203843
|
-
value: "hosted"
|
|
203844
|
-
},
|
|
203845
|
-
{
|
|
203846
|
-
name: "Custom manifest URL or path",
|
|
203847
|
-
description: "Enter your own",
|
|
203848
|
-
value: "custom"
|
|
203849
|
-
},
|
|
203850
|
-
{
|
|
203851
|
-
name: "Sync from genesis",
|
|
203852
|
-
description: "Skip bootstrap — slower, no download",
|
|
203853
|
-
value: "skip"
|
|
203854
|
-
}
|
|
203855
|
-
],
|
|
203856
|
-
onSelect: (value) => {
|
|
203857
|
-
if (value === "hosted") {
|
|
203858
|
-
setAgainst(DEFAULT_ARCHIVE_MANIFEST);
|
|
203859
|
-
setStage("confirm");
|
|
203860
|
-
} else if (value === "skip") {
|
|
203861
|
-
setSkipBootstrap(true);
|
|
203862
|
-
setStage("confirm");
|
|
203863
|
-
} else {
|
|
203864
|
-
setStage("manifestInput");
|
|
203865
|
-
}
|
|
203866
|
-
}
|
|
203867
|
-
}, undefined, false, undefined, this),
|
|
203868
|
-
stage === "manifestInput" && /* @__PURE__ */ jsxDEV(ManifestInput, {
|
|
203869
|
-
onSubmit: (value) => {
|
|
203870
|
-
setAgainst(value.trim() || DEFAULT_ARCHIVE_MANIFEST);
|
|
203871
|
-
setStage("confirm");
|
|
203872
|
-
}
|
|
203873
|
-
}, undefined, false, undefined, this),
|
|
203874
|
-
stage === "confirm" && config && /* @__PURE__ */ jsxDEV(Confirm, {
|
|
203875
|
-
config,
|
|
203876
|
-
onConfirm: () => setStage("running")
|
|
203877
|
-
}, undefined, false, undefined, this),
|
|
203878
|
-
stage === "running" && /* @__PURE__ */ jsxDEV(RunningView, {
|
|
203879
|
-
events,
|
|
203880
|
-
log
|
|
203881
|
-
}, undefined, false, undefined, this),
|
|
203882
|
-
stage === "done" && summary && /* @__PURE__ */ jsxDEV(Done, {
|
|
203883
|
-
summary
|
|
203884
|
-
}, undefined, false, undefined, this),
|
|
203885
|
-
stage === "error" && /* @__PURE__ */ jsxDEV(ErrorView, {
|
|
203886
|
-
message: fatal ?? "Setup failed.",
|
|
203887
|
-
log
|
|
203888
|
-
}, undefined, false, undefined, this)
|
|
203889
|
-
]
|
|
203890
|
-
}, undefined, true, undefined, this)
|
|
203891
|
-
}, undefined, false, undefined, this)
|
|
203892
|
-
}, undefined, false, undefined, this);
|
|
203893
|
-
}
|
|
203894
|
-
function safeParseNetwork(value) {
|
|
203895
|
-
if (!value)
|
|
203896
|
-
return;
|
|
203897
|
-
try {
|
|
203898
|
-
return parseInstanceNetwork(value);
|
|
203899
|
-
} catch {
|
|
203900
|
-
return;
|
|
203901
|
-
}
|
|
203902
|
-
}
|
|
203903
|
-
function safeParseNodeMode(value) {
|
|
203904
|
-
if (!value)
|
|
203905
|
-
return;
|
|
203906
|
-
try {
|
|
203907
|
-
return parseSetupNodeMode(value);
|
|
203908
|
-
} catch {
|
|
203909
|
-
return;
|
|
203910
|
-
}
|
|
203911
|
-
}
|
|
203912
|
-
function Welcome() {
|
|
203913
|
-
const colors = useContext(ColorsContext);
|
|
203914
|
-
return /* @__PURE__ */ jsxDEV("box", {
|
|
203915
|
-
style: { flexDirection: "column" },
|
|
203916
|
-
children: [
|
|
203917
|
-
/* @__PURE__ */ jsxDEV("text", {
|
|
203918
|
-
fg: colors.bright,
|
|
203919
|
-
children: "Guided self-host setup."
|
|
203920
|
-
}, undefined, false, undefined, this),
|
|
203921
|
-
/* @__PURE__ */ jsxDEV("text", {
|
|
203922
|
-
fg: colors.muted,
|
|
203923
|
-
children: [
|
|
203924
|
-
`
|
|
203925
|
-
`,
|
|
203926
|
-
"This writes secrets, a docker-compose file, and .env into a target directory, brings the stack up, and (optionally) restores verified history from the archive.",
|
|
203927
|
-
`
|
|
203928
|
-
|
|
203929
|
-
`,
|
|
203930
|
-
"Press Enter to begin, or q to quit."
|
|
203931
|
-
]
|
|
203932
|
-
}, undefined, true, undefined, this)
|
|
203933
|
-
]
|
|
203934
|
-
}, undefined, true, undefined, this);
|
|
203935
|
-
}
|
|
203936
|
-
function Picker({
|
|
203937
|
-
title,
|
|
203938
|
-
subtitle,
|
|
203939
|
-
options,
|
|
203940
|
-
onChange,
|
|
203941
|
-
onSelect
|
|
203942
|
-
}) {
|
|
203943
|
-
const colors = useContext(ColorsContext);
|
|
203944
|
-
return /* @__PURE__ */ jsxDEV("box", {
|
|
203945
|
-
style: { flexDirection: "column" },
|
|
203946
|
-
children: [
|
|
203947
|
-
/* @__PURE__ */ jsxDEV("text", {
|
|
203948
|
-
fg: colors.bright,
|
|
203949
|
-
children: title
|
|
203950
|
-
}, undefined, false, undefined, this),
|
|
203951
|
-
subtitle && /* @__PURE__ */ jsxDEV("text", {
|
|
203952
|
-
fg: colors.accent,
|
|
203953
|
-
children: subtitle
|
|
203954
|
-
}, undefined, false, undefined, this),
|
|
203955
|
-
/* @__PURE__ */ jsxDEV("select", {
|
|
203956
|
-
style: { height: options.length * 2 + 1 },
|
|
203957
|
-
options,
|
|
203958
|
-
focused: true,
|
|
203959
|
-
showDescription: true,
|
|
203960
|
-
backgroundColor: "transparent",
|
|
203961
|
-
textColor: colors.fg,
|
|
203962
|
-
focusedBackgroundColor: colors.raise,
|
|
203963
|
-
focusedTextColor: colors.bright,
|
|
203964
|
-
selectedBackgroundColor: colors.raise,
|
|
203965
|
-
selectedTextColor: colors.accent,
|
|
203966
|
-
descriptionColor: colors.muted,
|
|
203967
|
-
selectedDescriptionColor: colors.fg,
|
|
203968
|
-
onChange: (_index, option) => {
|
|
203969
|
-
if (option)
|
|
203970
|
-
onChange?.(String(option.value));
|
|
203971
|
-
},
|
|
203972
|
-
onSelect: (_index, option) => {
|
|
203973
|
-
if (option)
|
|
203974
|
-
onSelect(String(option.value));
|
|
203975
|
-
}
|
|
203976
|
-
}, undefined, false, undefined, this),
|
|
203977
|
-
/* @__PURE__ */ jsxDEV("text", {
|
|
203978
|
-
fg: colors.muted,
|
|
203979
|
-
children: [
|
|
203980
|
-
`
|
|
203981
|
-
`,
|
|
203982
|
-
"↑↓ to move, enter to choose"
|
|
203983
|
-
]
|
|
203984
|
-
}, undefined, true, undefined, this)
|
|
203985
|
-
]
|
|
203986
|
-
}, undefined, true, undefined, this);
|
|
203987
|
-
}
|
|
203988
|
-
function ManifestInput({ onSubmit }) {
|
|
203989
|
-
const colors = useContext(ColorsContext);
|
|
203990
|
-
return /* @__PURE__ */ jsxDEV("box", {
|
|
203991
|
-
style: { flexDirection: "column" },
|
|
203992
|
-
children: [
|
|
203993
|
-
/* @__PURE__ */ jsxDEV("text", {
|
|
203994
|
-
fg: colors.bright,
|
|
203995
|
-
children: "Manifest URL or local path"
|
|
203996
|
-
}, undefined, false, undefined, this),
|
|
203997
|
-
/* @__PURE__ */ jsxDEV("text", {
|
|
203998
|
-
fg: colors.muted,
|
|
203999
|
-
children: [
|
|
204000
|
-
"Suggested: ",
|
|
204001
|
-
DEFAULT_ARCHIVE_MANIFEST
|
|
204002
|
-
]
|
|
204003
|
-
}, undefined, true, undefined, this),
|
|
204004
|
-
/* @__PURE__ */ jsxDEV("input", {
|
|
204005
|
-
focused: true,
|
|
204006
|
-
placeholder: DEFAULT_ARCHIVE_MANIFEST,
|
|
204007
|
-
textColor: colors.fg,
|
|
204008
|
-
focusedTextColor: colors.accent,
|
|
204009
|
-
placeholderColor: colors.muted,
|
|
204010
|
-
onSubmit: (value) => onSubmit(String(value ?? ""))
|
|
204011
|
-
}, undefined, false, undefined, this),
|
|
204012
|
-
/* @__PURE__ */ jsxDEV("text", {
|
|
204013
|
-
fg: colors.muted,
|
|
204014
|
-
children: [
|
|
204015
|
-
`
|
|
204016
|
-
`,
|
|
204017
|
-
"enter to accept (blank = suggested default)"
|
|
204018
|
-
]
|
|
204019
|
-
}, undefined, true, undefined, this)
|
|
204020
|
-
]
|
|
204021
|
-
}, undefined, true, undefined, this);
|
|
204022
|
-
}
|
|
204023
|
-
function Confirm({
|
|
204024
|
-
config,
|
|
204025
|
-
onConfirm
|
|
204026
|
-
}) {
|
|
204027
|
-
const colors = useContext(ColorsContext);
|
|
204028
|
-
useKeyboard((key) => {
|
|
204029
|
-
if (key.eventType === "release")
|
|
204030
|
-
return;
|
|
204031
|
-
if (key.name === "return")
|
|
204032
|
-
onConfirm();
|
|
204033
|
-
});
|
|
204034
|
-
return /* @__PURE__ */ jsxDEV("box", {
|
|
204035
|
-
style: { flexDirection: "column" },
|
|
204036
|
-
children: [
|
|
204037
|
-
/* @__PURE__ */ jsxDEV("text", {
|
|
204038
|
-
fg: colors.bright,
|
|
204039
|
-
children: "Ready"
|
|
204040
|
-
}, undefined, false, undefined, this),
|
|
204041
|
-
/* @__PURE__ */ jsxDEV("text", {
|
|
204042
|
-
fg: colors.fg,
|
|
204043
|
-
children: [
|
|
204044
|
-
`
|
|
204045
|
-
`,
|
|
204046
|
-
"network ",
|
|
204047
|
-
config.network,
|
|
204048
|
-
`
|
|
204049
|
-
`,
|
|
204050
|
-
"node mode ",
|
|
204051
|
-
config.nodeMode,
|
|
204052
|
-
`
|
|
204053
|
-
`,
|
|
204054
|
-
"dir ",
|
|
204055
|
-
config.dir,
|
|
204056
|
-
`
|
|
204057
|
-
`,
|
|
204058
|
-
"api port ",
|
|
204059
|
-
config.apiPort,
|
|
204060
|
-
`
|
|
204061
|
-
`,
|
|
204062
|
-
"bootstrap",
|
|
204063
|
-
" ",
|
|
204064
|
-
config.skipBootstrap ? "skip (sync from genesis)" : config.against,
|
|
204065
|
-
`
|
|
204066
|
-
`,
|
|
204067
|
-
"image ghcr.io/",
|
|
204068
|
-
config.owner,
|
|
204069
|
-
"/secondlayer-runtime:",
|
|
204070
|
-
config.imageTag
|
|
204071
|
-
]
|
|
204072
|
-
}, undefined, true, undefined, this),
|
|
204073
|
-
/* @__PURE__ */ jsxDEV("text", {
|
|
204074
|
-
fg: colors.accent,
|
|
204075
|
-
children: [
|
|
204076
|
-
`
|
|
204077
|
-
`,
|
|
204078
|
-
"Press Enter to start."
|
|
204079
|
-
]
|
|
204080
|
-
}, undefined, true, undefined, this)
|
|
204081
|
-
]
|
|
204082
|
-
}, undefined, true, undefined, this);
|
|
204083
|
-
}
|
|
204084
|
-
function RunningView({ events, log }) {
|
|
204085
|
-
const colors = useContext(ColorsContext);
|
|
204086
|
-
const latestByStep = new Map;
|
|
204087
|
-
for (const e of events)
|
|
204088
|
-
latestByStep.set(e.step, e);
|
|
204089
|
-
const anyActive = STEP_ORDER.some((step) => latestByStep.get(step)?.type === "step-start");
|
|
204090
|
-
const spinner = useSpinner(anyActive);
|
|
204091
|
-
return /* @__PURE__ */ jsxDEV("box", {
|
|
204092
|
-
style: { flexDirection: "column" },
|
|
204093
|
-
children: [
|
|
204094
|
-
STEP_ORDER.map((step) => {
|
|
204095
|
-
const e = latestByStep.get(step);
|
|
204096
|
-
const active = e?.type === "step-start";
|
|
204097
|
-
const symbol = !e ? "·" : active ? spinner : e.type === "step-done" ? "✓" : e.type === "step-skip" ? "–" : e.type === "step-error" ? "✗" : " ";
|
|
204098
|
-
const color = active ? colors.accent : e?.type === "step-done" ? colors.success : e?.type === "step-error" ? colors.error : e?.type === "step-skip" ? colors.muted : colors.muted;
|
|
204099
|
-
return /* @__PURE__ */ jsxDEV("text", {
|
|
204100
|
-
fg: color,
|
|
204101
|
-
children: [
|
|
204102
|
-
symbol,
|
|
204103
|
-
" ",
|
|
204104
|
-
STEP_LABEL[step]
|
|
204105
|
-
]
|
|
204106
|
-
}, step, true, undefined, this);
|
|
204107
|
-
}),
|
|
204108
|
-
/* @__PURE__ */ jsxDEV("box", {
|
|
204109
|
-
style: {
|
|
204110
|
-
border: true,
|
|
204111
|
-
borderStyle: "single",
|
|
204112
|
-
borderColor: colors.borderIdle,
|
|
204113
|
-
marginTop: 1,
|
|
204114
|
-
height: 10
|
|
204115
|
-
},
|
|
204116
|
-
title: "log",
|
|
204117
|
-
children: /* @__PURE__ */ jsxDEV("box", {
|
|
204118
|
-
style: { flexDirection: "column", padding: 1 },
|
|
204119
|
-
children: log.length === 0 ? /* @__PURE__ */ jsxDEV("text", {
|
|
204120
|
-
fg: colors.muted,
|
|
204121
|
-
children: "waiting…"
|
|
204122
|
-
}, undefined, false, undefined, this) : log.map((line, i) => /* @__PURE__ */ jsxDEV("text", {
|
|
204123
|
-
fg: line.startsWith("ERROR:") ? colors.error : colors.fg,
|
|
204124
|
-
children: line
|
|
204125
|
-
}, `${i}:${line}`, false, undefined, this))
|
|
204126
|
-
}, undefined, false, undefined, this)
|
|
204127
|
-
}, undefined, false, undefined, this)
|
|
204128
|
-
]
|
|
204129
|
-
}, undefined, true, undefined, this);
|
|
204130
|
-
}
|
|
204131
|
-
function Done({ summary }) {
|
|
204132
|
-
const colors = useContext(ColorsContext);
|
|
204133
|
-
return /* @__PURE__ */ jsxDEV("box", {
|
|
204134
|
-
style: { flexDirection: "column" },
|
|
204135
|
-
children: [
|
|
204136
|
-
/* @__PURE__ */ jsxDEV("box", {
|
|
204137
|
-
style: { backgroundColor: colors.successBg, padding: 1 },
|
|
204138
|
-
children: /* @__PURE__ */ jsxDEV("text", {
|
|
204139
|
-
fg: colors.success,
|
|
204140
|
-
children: summary
|
|
204141
|
-
}, undefined, false, undefined, this)
|
|
204142
|
-
}, undefined, false, undefined, this),
|
|
204143
|
-
/* @__PURE__ */ jsxDEV("text", {
|
|
204144
|
-
fg: colors.muted,
|
|
204145
|
-
children: [
|
|
204146
|
-
`
|
|
204147
|
-
`,
|
|
204148
|
-
"press enter or q to exit"
|
|
204149
|
-
]
|
|
204150
|
-
}, undefined, true, undefined, this)
|
|
204151
|
-
]
|
|
204152
|
-
}, undefined, true, undefined, this);
|
|
204153
|
-
}
|
|
204154
|
-
function ErrorView({ message, log }) {
|
|
204155
|
-
const colors = useContext(ColorsContext);
|
|
204156
|
-
return /* @__PURE__ */ jsxDEV("box", {
|
|
204157
|
-
style: { flexDirection: "column" },
|
|
204158
|
-
children: [
|
|
204159
|
-
/* @__PURE__ */ jsxDEV("box", {
|
|
204160
|
-
style: { backgroundColor: colors.errorBg, padding: 1 },
|
|
204161
|
-
children: /* @__PURE__ */ jsxDEV("text", {
|
|
204162
|
-
fg: colors.error,
|
|
204163
|
-
children: message
|
|
204164
|
-
}, undefined, false, undefined, this)
|
|
204165
|
-
}, undefined, false, undefined, this),
|
|
204166
|
-
log.slice(-10).map((line, i) => /* @__PURE__ */ jsxDEV("text", {
|
|
204167
|
-
fg: colors.muted,
|
|
204168
|
-
children: line
|
|
204169
|
-
}, `${i}:${line}`, false, undefined, this)),
|
|
204170
|
-
/* @__PURE__ */ jsxDEV("text", {
|
|
204171
|
-
fg: colors.muted,
|
|
204172
|
-
children: [
|
|
204173
|
-
`
|
|
204174
|
-
`,
|
|
204175
|
-
"press enter or q to exit"
|
|
204176
|
-
]
|
|
204177
|
-
}, undefined, true, undefined, this)
|
|
204178
|
-
]
|
|
204179
|
-
}, undefined, true, undefined, this);
|
|
204180
|
-
}
|
|
204181
|
-
async function runSetupTui(flags) {
|
|
204182
|
-
const renderer = await createCliRenderer({ exitOnCtrlC: true });
|
|
204183
|
-
createRoot(renderer).render(/* @__PURE__ */ jsxDEV(App, {
|
|
204184
|
-
flags
|
|
204185
|
-
}, undefined, false, undefined, this));
|
|
204186
|
-
return new Promise((resolve, reject) => {
|
|
204187
|
-
let settled = false;
|
|
204188
|
-
const onDestroy = () => {
|
|
204189
|
-
if (settled)
|
|
204190
|
-
return;
|
|
204191
|
-
settled = true;
|
|
204192
|
-
resolve();
|
|
204193
|
-
};
|
|
204194
|
-
const onError = (event) => {
|
|
204195
|
-
if (settled)
|
|
204196
|
-
return;
|
|
204197
|
-
settled = true;
|
|
204198
|
-
try {
|
|
204199
|
-
renderer.destroy();
|
|
204200
|
-
} catch {}
|
|
204201
|
-
reject(event.error ?? new Error("OpenTUI renderer error"));
|
|
204202
|
-
};
|
|
204203
|
-
renderer.once(CliRenderEvents.DESTROY, onDestroy);
|
|
204204
|
-
renderer.once(CliRenderEvents.RENDER_ERROR, onError);
|
|
204205
|
-
renderer.once(CliRenderEvents.HANDLER_ERROR, onError);
|
|
204206
|
-
});
|
|
204207
|
-
}
|
|
204208
|
-
var DARK_COLORS, LIGHT_COLORS, ColorsContext, NETWORK_OPTIONS, NODE_MODE_OPTIONS, SPINNER_FRAMES, STEP_LABEL, STEP_ORDER;
|
|
204209
|
-
var init_setup_tui = __esm(() => {
|
|
204210
|
-
init_instance_init();
|
|
204211
|
-
init_oss_compose();
|
|
204212
|
-
init_setup_wizard();
|
|
204213
|
-
DARK_COLORS = {
|
|
204214
|
-
bg: "#151515",
|
|
204215
|
-
raise: "#202020",
|
|
204216
|
-
fg: "#d7d1c9",
|
|
204217
|
-
muted: "#8d867e",
|
|
204218
|
-
bright: "#f2ede5",
|
|
204219
|
-
accent: "#29c6be",
|
|
204220
|
-
success: "#22c55e",
|
|
204221
|
-
warning: "#eab308",
|
|
204222
|
-
error: "#ef4444",
|
|
204223
|
-
borderIdle: "#FFFFFF14",
|
|
204224
|
-
successBg: "#22c55e14",
|
|
204225
|
-
errorBg: "#ef444414"
|
|
204226
|
-
};
|
|
204227
|
-
LIGHT_COLORS = {
|
|
204228
|
-
bg: "#ffffff",
|
|
204229
|
-
raise: "#f8f6f2",
|
|
204230
|
-
fg: "#1f2228",
|
|
204231
|
-
muted: "#7d8288",
|
|
204232
|
-
bright: "#0a0a0a",
|
|
204233
|
-
accent: "#29c6be",
|
|
204234
|
-
success: "#22c55e",
|
|
204235
|
-
warning: "#eab308",
|
|
204236
|
-
error: "#ef4444",
|
|
204237
|
-
borderIdle: "#0A0A0A14",
|
|
204238
|
-
successBg: "#22c55e14",
|
|
204239
|
-
errorBg: "#ef444414"
|
|
204240
|
-
};
|
|
204241
|
-
ColorsContext = createContext(DARK_COLORS);
|
|
204242
|
-
NETWORK_OPTIONS = [
|
|
204243
|
-
{
|
|
204244
|
-
name: "mainnet",
|
|
204245
|
-
description: "Production Stacks chain. Large: plan for hundreds of GB.",
|
|
204246
|
-
value: "mainnet"
|
|
204247
|
-
},
|
|
204248
|
-
{
|
|
204249
|
-
name: "testnet",
|
|
204250
|
-
description: "Stacks testnet. Small history.",
|
|
204251
|
-
value: "testnet"
|
|
204252
|
-
},
|
|
204253
|
-
{ name: "devnet", description: "Local Clarinet devnet.", value: "devnet" }
|
|
204254
|
-
];
|
|
204255
|
-
NODE_MODE_OPTIONS = SETUP_NODE_MODES.map((mode) => ({
|
|
204256
|
-
name: mode,
|
|
204257
|
-
description: mode === "external" ? "You run the Stacks node; paste an observer stanza into its Config.toml." : mode === "stacks" ? "You run the Stacks node yourself, no bundled bitcoind (same as external — no bundled-stacks-only profile exists)." : "Bundled Stacks node + bitcoind.",
|
|
204258
|
-
value: mode
|
|
204259
|
-
}));
|
|
204260
|
-
SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
204261
|
-
STEP_LABEL = {
|
|
204262
|
-
preflight: "Preflight",
|
|
204263
|
-
secrets: "Secrets",
|
|
204264
|
-
config: "Write config",
|
|
204265
|
-
"docker-up": "docker compose up",
|
|
204266
|
-
observer: "Observer stanza",
|
|
204267
|
-
bootstrap: "Bootstrap",
|
|
204268
|
-
verify: "Verify",
|
|
204269
|
-
summary: "Summary"
|
|
204270
|
-
};
|
|
204271
|
-
STEP_ORDER = Object.keys(STEP_LABEL);
|
|
204272
|
-
});
|
|
204273
|
-
|
|
204274
202437
|
// ../../node_modules/commander/index.js
|
|
204275
202438
|
var exports_commander = {};
|
|
204276
202439
|
__exportCjs(exports_commander, {
|
|
@@ -204332,7 +202495,7 @@ var {
|
|
|
204332
202495
|
// package.json
|
|
204333
202496
|
var package_default = {
|
|
204334
202497
|
name: "@secondlayer/cli",
|
|
204335
|
-
version: "
|
|
202498
|
+
version: "17.0.0",
|
|
204336
202499
|
description: "Run a Stacks index on your own hardware — set it up, restore verified history, and keep it current.",
|
|
204337
202500
|
repository: {
|
|
204338
202501
|
type: "git",
|
|
@@ -204369,16 +202532,13 @@ var package_default = {
|
|
|
204369
202532
|
"@biomejs/wasm-nodejs": "^1.9.0",
|
|
204370
202533
|
"@dsnp/parquetjs": "~1.8.8",
|
|
204371
202534
|
"@inquirer/prompts": "^8.2.0",
|
|
204372
|
-
"@opentui/core": "^0.5.3",
|
|
204373
|
-
"@opentui/react": "^0.5.3",
|
|
204374
202535
|
"@secondlayer/bundler": "^0.6.1",
|
|
204375
202536
|
"@secondlayer/scaffold": "^1.6.1",
|
|
204376
|
-
"@secondlayer/sdk": "^
|
|
204377
|
-
"@secondlayer/shared": "^11.
|
|
202537
|
+
"@secondlayer/sdk": "^12.0.0",
|
|
202538
|
+
"@secondlayer/shared": "^11.10.1",
|
|
204378
202539
|
"@secondlayer/stacks": "^6.1.0",
|
|
204379
202540
|
"@secondlayer/subgraphs": "^6.1.0",
|
|
204380
202541
|
esbuild: "^0.28.2",
|
|
204381
|
-
react: "^19.1.0",
|
|
204382
202542
|
"smol-toml": "^1.8.0",
|
|
204383
202543
|
zod: "^4.3.6"
|
|
204384
202544
|
},
|
|
@@ -204394,7 +202554,6 @@ var package_default = {
|
|
|
204394
202554
|
"@antfu/ni": "^24.4.0",
|
|
204395
202555
|
"@stacks/clarinet-sdk": "3.21.1",
|
|
204396
202556
|
"@types/bun": "^1.1.14",
|
|
204397
|
-
"@types/react": "^19.1.5",
|
|
204398
202557
|
chalk: "^5.3.0",
|
|
204399
202558
|
commander: "^14.0.0",
|
|
204400
202559
|
execa: "^9.5.3",
|
|
@@ -204553,7 +202712,146 @@ import {
|
|
|
204553
202712
|
} from "@secondlayer/shared/archive/range-digest";
|
|
204554
202713
|
import { getDb, getRawClient, sql } from "@secondlayer/shared/db";
|
|
204555
202714
|
import { upsertSyncScope } from "@secondlayer/shared/db/queries/sync-scope";
|
|
204556
|
-
|
|
202715
|
+
|
|
202716
|
+
// src/lib/archive-reference.ts
|
|
202717
|
+
import { createHash } from "node:crypto";
|
|
202718
|
+
import { readFile as readFile2 } from "node:fs/promises";
|
|
202719
|
+
import { join as join2, resolve, sep } from "node:path";
|
|
202720
|
+
import { parseRetryAfter } from "@secondlayer/sdk";
|
|
202721
|
+
import {
|
|
202722
|
+
checkSignature,
|
|
202723
|
+
loadReference,
|
|
202724
|
+
resolveArchivePublicKey
|
|
202725
|
+
} from "@secondlayer/shared/archive/reference";
|
|
202726
|
+
function assertPathWithinRoot(path, root) {
|
|
202727
|
+
const segments = path.split("/");
|
|
202728
|
+
const leaves = path.startsWith("/") || path.includes("\\") || /^[a-z][a-z0-9+.-]*:/i.test(path) || /[%?#]/.test(path) || segments.some((part) => part === "" || part === "." || part === "..");
|
|
202729
|
+
if (leaves) {
|
|
202730
|
+
throw new Error(`archive path ${path} would leave the archive root`);
|
|
202731
|
+
}
|
|
202732
|
+
if (root !== undefined) {
|
|
202733
|
+
const base = resolve(root);
|
|
202734
|
+
const target = resolve(base, path);
|
|
202735
|
+
if (target !== base && !target.startsWith(base + sep)) {
|
|
202736
|
+
throw new Error(`archive path ${path} would leave the archive root`);
|
|
202737
|
+
}
|
|
202738
|
+
}
|
|
202739
|
+
}
|
|
202740
|
+
|
|
202741
|
+
class ArchiveFetchError extends Error {
|
|
202742
|
+
transient;
|
|
202743
|
+
attempts;
|
|
202744
|
+
constructor(message, opts) {
|
|
202745
|
+
super(message);
|
|
202746
|
+
this.name = "ArchiveFetchError";
|
|
202747
|
+
this.transient = opts.transient;
|
|
202748
|
+
this.attempts = opts.attempts;
|
|
202749
|
+
}
|
|
202750
|
+
}
|
|
202751
|
+
var TRANSIENT_ERROR_CODES = new Set([
|
|
202752
|
+
"ECONNRESET",
|
|
202753
|
+
"ECONNREFUSED",
|
|
202754
|
+
"ECONNABORTED",
|
|
202755
|
+
"ETIMEDOUT",
|
|
202756
|
+
"EAI_AGAIN",
|
|
202757
|
+
"EPIPE",
|
|
202758
|
+
"ENETUNREACH",
|
|
202759
|
+
"EHOSTUNREACH"
|
|
202760
|
+
]);
|
|
202761
|
+
function isTransientCode(code) {
|
|
202762
|
+
return typeof code === "string" && (TRANSIENT_ERROR_CODES.has(code) || code.startsWith("UND_ERR_"));
|
|
202763
|
+
}
|
|
202764
|
+
function isTransientFetchFailure(err) {
|
|
202765
|
+
if (!(err instanceof Error))
|
|
202766
|
+
return false;
|
|
202767
|
+
if (err.name === "AbortError" || err.name === "TimeoutError")
|
|
202768
|
+
return true;
|
|
202769
|
+
const code = err.code;
|
|
202770
|
+
const causeCode = err.cause?.code;
|
|
202771
|
+
if (isTransientCode(code) || isTransientCode(causeCode))
|
|
202772
|
+
return true;
|
|
202773
|
+
return err.name === "TypeError" && /fetch failed|network|socket|connection|reset|unreachable/i.test(err.message);
|
|
202774
|
+
}
|
|
202775
|
+
var PARTITION_FETCH_RETRIES = 3;
|
|
202776
|
+
var PARTITION_FETCH_TIMEOUT_MS = 300000;
|
|
202777
|
+
var RETRY_BASE_MS = 1000;
|
|
202778
|
+
var RETRY_AFTER_CAP_MS = 60000;
|
|
202779
|
+
function retryDelayMs(attempt, response) {
|
|
202780
|
+
const retryAfter = response ? parseRetryAfter(response.headers.get("retry-after")) : undefined;
|
|
202781
|
+
if (retryAfter !== undefined) {
|
|
202782
|
+
return Math.min(retryAfter * 1000, RETRY_AFTER_CAP_MS);
|
|
202783
|
+
}
|
|
202784
|
+
return RETRY_BASE_MS * 2 ** (attempt - 1);
|
|
202785
|
+
}
|
|
202786
|
+
async function fetchWithRetry(url, opts) {
|
|
202787
|
+
const retries = opts.retries ?? PARTITION_FETCH_RETRIES;
|
|
202788
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
202789
|
+
const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
202790
|
+
const timeoutMs = opts.timeoutMs ?? PARTITION_FETCH_TIMEOUT_MS;
|
|
202791
|
+
let lastReason = "";
|
|
202792
|
+
for (let attempt = 1;attempt <= retries + 1; attempt++) {
|
|
202793
|
+
let response;
|
|
202794
|
+
try {
|
|
202795
|
+
response = await fetchImpl(url, {
|
|
202796
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
202797
|
+
});
|
|
202798
|
+
} catch (err) {
|
|
202799
|
+
if (!isTransientFetchFailure(err))
|
|
202800
|
+
throw err;
|
|
202801
|
+
lastReason = err instanceof Error ? err.message : String(err);
|
|
202802
|
+
}
|
|
202803
|
+
if (response) {
|
|
202804
|
+
const retryable = response.status === 429 || response.status >= 500;
|
|
202805
|
+
if (!retryable)
|
|
202806
|
+
return response;
|
|
202807
|
+
lastReason = `${response.status} ${response.statusText}`.trim();
|
|
202808
|
+
await response.body?.cancel().catch(() => {});
|
|
202809
|
+
}
|
|
202810
|
+
if (attempt <= retries) {
|
|
202811
|
+
await sleep(retryDelayMs(attempt, response));
|
|
202812
|
+
}
|
|
202813
|
+
}
|
|
202814
|
+
throw new ArchiveFetchError(`could not fetch ${opts.label} after ${retries + 1} attempts (${lastReason})`, { transient: true, attempts: retries + 1 });
|
|
202815
|
+
}
|
|
202816
|
+
async function fetchViaGate(gate, partition, retried, fetchOpts) {
|
|
202817
|
+
const url = await gate.getUrl(partition.path, retried ? { forceRefresh: true } : undefined);
|
|
202818
|
+
const response = await fetchWithRetry(url, {
|
|
202819
|
+
...fetchOpts,
|
|
202820
|
+
label: partition.path
|
|
202821
|
+
});
|
|
202822
|
+
if (response.status === 403 && !retried) {
|
|
202823
|
+
await response.body?.cancel().catch(() => {});
|
|
202824
|
+
return fetchViaGate(gate, partition, true, fetchOpts);
|
|
202825
|
+
}
|
|
202826
|
+
if (!response.ok) {
|
|
202827
|
+
throw new ArchiveFetchError(`could not fetch ${partition.path} (${response.status} ${response.statusText})`, { transient: false, attempts: 1 });
|
|
202828
|
+
}
|
|
202829
|
+
return Buffer.from(await response.arrayBuffer());
|
|
202830
|
+
}
|
|
202831
|
+
async function fetchVerifiedPartition(reference, partition, gate, fetchOpts) {
|
|
202832
|
+
assertPathWithinRoot(partition.path, reference.isRemote ? undefined : reference.root);
|
|
202833
|
+
let bytes;
|
|
202834
|
+
if (gate) {
|
|
202835
|
+
bytes = await fetchViaGate(gate, partition, false, fetchOpts);
|
|
202836
|
+
} else if (reference.isRemote) {
|
|
202837
|
+
const url = `${reference.root.replace(/\/$/, "")}/${partition.path}`;
|
|
202838
|
+
const response = await fetchWithRetry(url, {
|
|
202839
|
+
...fetchOpts,
|
|
202840
|
+
label: partition.path
|
|
202841
|
+
});
|
|
202842
|
+
if (!response.ok) {
|
|
202843
|
+
throw new ArchiveFetchError(`could not fetch ${partition.path} (${response.status} ${response.statusText})`, { transient: false, attempts: 1 });
|
|
202844
|
+
}
|
|
202845
|
+
bytes = Buffer.from(await response.arrayBuffer());
|
|
202846
|
+
} else {
|
|
202847
|
+
bytes = await readFile2(join2(reference.root, partition.path));
|
|
202848
|
+
}
|
|
202849
|
+
const digest = createHash("sha256").update(bytes).digest("hex");
|
|
202850
|
+
if (digest !== partition.sha256) {
|
|
202851
|
+
throw new Error(`${partition.path} failed verification: expected ${partition.sha256}, got ${digest}`);
|
|
202852
|
+
}
|
|
202853
|
+
return bytes;
|
|
202854
|
+
}
|
|
204557
202855
|
|
|
204558
202856
|
// src/lib/bootstrap-resume.ts
|
|
204559
202857
|
var RESUME_DATASETS = ["blocks", "transactions", "events"];
|
|
@@ -205001,7 +203299,6 @@ import {
|
|
|
205001
203299
|
computeRangeDigest as computeRangeDigest2
|
|
205002
203300
|
} from "@secondlayer/shared/archive/range-digest";
|
|
205003
203301
|
import { getDb as getDb2, getRawClient as getRawClient2 } from "@secondlayer/shared/db";
|
|
205004
|
-
init_archive_reference();
|
|
205005
203302
|
init_output();
|
|
205006
203303
|
init_resolve_auth();
|
|
205007
203304
|
var REPAIR_EXIT = {
|
|
@@ -205443,9 +203740,6 @@ function registerRepairCommand(program) {
|
|
|
205443
203740
|
}
|
|
205444
203741
|
|
|
205445
203742
|
// src/commands/verify.ts
|
|
205446
|
-
init_archive_reference();
|
|
205447
|
-
init_output();
|
|
205448
|
-
init_resolve_auth();
|
|
205449
203743
|
import {
|
|
205450
203744
|
compareRangeDigests as compareRangeDigests2,
|
|
205451
203745
|
computeRangeDigest as computeRangeDigest3
|
|
@@ -205460,6 +203754,8 @@ import {
|
|
|
205460
203754
|
reportVerify
|
|
205461
203755
|
} from "@secondlayer/shared/coverage";
|
|
205462
203756
|
import { getDb as getDb3 } from "@secondlayer/shared/db";
|
|
203757
|
+
init_output();
|
|
203758
|
+
init_resolve_auth();
|
|
205463
203759
|
var VERIFY_EXIT = {
|
|
205464
203760
|
CLEAN: 0,
|
|
205465
203761
|
DIVERGED: 1,
|
|
@@ -207510,6 +205806,157 @@ import { spawnSync as spawnSync3 } from "node:child_process";
|
|
|
207510
205806
|
import { existsSync as existsSync7, mkdirSync as mkdirSync4, writeFileSync as writeFileSync6 } from "node:fs";
|
|
207511
205807
|
import { dirname as dirname8, join as join12 } from "node:path";
|
|
207512
205808
|
|
|
205809
|
+
// src/lib/devnet-compose.ts
|
|
205810
|
+
var DEFAULT_IMAGE_OWNER = "ryanwaits";
|
|
205811
|
+
var DEFAULT_IMAGE_TAG = "latest";
|
|
205812
|
+
var DEV_INSTANCE_TOKEN = "dev-instance-token";
|
|
205813
|
+
function buildDevnetCompose(opts = {}) {
|
|
205814
|
+
const owner = opts.owner ?? DEFAULT_IMAGE_OWNER;
|
|
205815
|
+
const tag = opts.imageTag ?? DEFAULT_IMAGE_TAG;
|
|
205816
|
+
const apiPort = opts.apiPort ?? 3800;
|
|
205817
|
+
const indexerPort = opts.indexerPort ?? 3700;
|
|
205818
|
+
const apiPublish = `127.0.0.1:${apiPort}`;
|
|
205819
|
+
const apiImage = `ghcr.io/${owner}/secondlayer-api:${tag}`;
|
|
205820
|
+
const indexerImage = `ghcr.io/${owner}/secondlayer-indexer:${tag}`;
|
|
205821
|
+
const dbUrl = "postgres://secondlayer:secondlayer@postgres:5432/secondlayer";
|
|
205822
|
+
const DEV_SECRETS_KEY = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
|
|
205823
|
+
return `# Generated by \`secondlayer devnet connect\` — do not edit by hand.
|
|
205824
|
+
# Secondlayer app services for local clarinet devnet development.
|
|
205825
|
+
name: secondlayer-devnet
|
|
205826
|
+
|
|
205827
|
+
services:
|
|
205828
|
+
postgres:
|
|
205829
|
+
image: postgres:17-alpine
|
|
205830
|
+
environment:
|
|
205831
|
+
POSTGRES_USER: secondlayer
|
|
205832
|
+
POSTGRES_PASSWORD: secondlayer
|
|
205833
|
+
POSTGRES_DB: secondlayer
|
|
205834
|
+
volumes:
|
|
205835
|
+
- postgres_data:/var/lib/postgresql/data
|
|
205836
|
+
healthcheck:
|
|
205837
|
+
test: ["CMD-SHELL", "pg_isready -U secondlayer"]
|
|
205838
|
+
interval: 5s
|
|
205839
|
+
timeout: 5s
|
|
205840
|
+
retries: 10
|
|
205841
|
+
restart: unless-stopped
|
|
205842
|
+
|
|
205843
|
+
migrate:
|
|
205844
|
+
image: ${apiImage}
|
|
205845
|
+
command: ["bun", "run", "packages/shared/src/db/migrate.ts"]
|
|
205846
|
+
environment:
|
|
205847
|
+
DATABASE_URL: ${dbUrl}
|
|
205848
|
+
depends_on:
|
|
205849
|
+
postgres:
|
|
205850
|
+
condition: service_healthy
|
|
205851
|
+
restart: "no"
|
|
205852
|
+
|
|
205853
|
+
api:
|
|
205854
|
+
image: ${apiImage}
|
|
205855
|
+
environment:
|
|
205856
|
+
INSTANCE_MODE: oss
|
|
205857
|
+
DATABASE_URL: ${dbUrl}
|
|
205858
|
+
PORT: 3800
|
|
205859
|
+
# 0.0.0.0 is forced: a container process that binds loopback is
|
|
205860
|
+
# unreachable through a published port. The real reach is the publish
|
|
205861
|
+
# spec below, handed over verbatim so /v1 reads stay keyless on devnet.
|
|
205862
|
+
LISTEN_HOST: "0.0.0.0"
|
|
205863
|
+
API_PUBLISH_ADDR: "${apiPublish}"
|
|
205864
|
+
# Boot guard reads LISTEN_HOST, so a token is required to start at all.
|
|
205865
|
+
# It gates writes only — reads are open on this loopback publish.
|
|
205866
|
+
INSTANCE_TOKEN: ${DEV_INSTANCE_TOKEN}
|
|
205867
|
+
NODE_ENV: production
|
|
205868
|
+
DATA_DIR: /data
|
|
205869
|
+
# Shared across api + subgraph-processor so the secret the api encrypts
|
|
205870
|
+
# (e.g. a webhook's signing secret) can be decrypted by the emitter.
|
|
205871
|
+
# OSS auto-generates a per-process key when unset — that splits them.
|
|
205872
|
+
# Local-dev fixed value; not a production secret.
|
|
205873
|
+
SECONDLAYER_SECRETS_KEY: ${DEV_SECRETS_KEY}
|
|
205874
|
+
ports:
|
|
205875
|
+
- "${apiPublish}:3800"
|
|
205876
|
+
volumes:
|
|
205877
|
+
- subgraphs_data:/data/subgraphs
|
|
205878
|
+
depends_on:
|
|
205879
|
+
migrate:
|
|
205880
|
+
condition: service_completed_successfully
|
|
205881
|
+
postgres:
|
|
205882
|
+
condition: service_healthy
|
|
205883
|
+
healthcheck:
|
|
205884
|
+
test: ["CMD", "curl", "-f", "http://localhost:3800/health"]
|
|
205885
|
+
interval: 10s
|
|
205886
|
+
timeout: 3s
|
|
205887
|
+
start_period: 10s
|
|
205888
|
+
retries: 5
|
|
205889
|
+
restart: unless-stopped
|
|
205890
|
+
|
|
205891
|
+
indexer:
|
|
205892
|
+
image: ${indexerImage}
|
|
205893
|
+
environment:
|
|
205894
|
+
DATABASE_URL: ${dbUrl}
|
|
205895
|
+
PORT: 3700
|
|
205896
|
+
NODE_ENV: production
|
|
205897
|
+
# Keep every network key aligned: the indexer writes index_progress under
|
|
205898
|
+
# STACKS_NETWORK, the subgraph processor reads it under NETWORK. If they
|
|
205899
|
+
# differ, catch-up finds no progress row and subgraphs never advance.
|
|
205900
|
+
NETWORKS: testnet
|
|
205901
|
+
STACKS_NETWORK: testnet
|
|
205902
|
+
NETWORK: testnet
|
|
205903
|
+
# Fed by the devnet node's event observer — never reach out to Hiro mainnet.
|
|
205904
|
+
AUTO_BACKFILL_ENABLED: "false"
|
|
205905
|
+
TIP_FOLLOWER_ENABLED: "false"
|
|
205906
|
+
# Deliberately published on every interface, unlike the api: the devnet's
|
|
205907
|
+
# stacks-node runs in its own container and POSTs events to
|
|
205908
|
+
# host.docker.internal:3700, which resolves to the host gateway address,
|
|
205909
|
+
# not 127.0.0.1. A loopback publish here would drop every block.
|
|
205910
|
+
ports:
|
|
205911
|
+
- "${indexerPort}:3700"
|
|
205912
|
+
extra_hosts:
|
|
205913
|
+
- "host.docker.internal:host-gateway"
|
|
205914
|
+
depends_on:
|
|
205915
|
+
migrate:
|
|
205916
|
+
condition: service_completed_successfully
|
|
205917
|
+
postgres:
|
|
205918
|
+
condition: service_healthy
|
|
205919
|
+
healthcheck:
|
|
205920
|
+
test: ["CMD", "curl", "-f", "http://localhost:3700/health"]
|
|
205921
|
+
interval: 10s
|
|
205922
|
+
timeout: 3s
|
|
205923
|
+
start_period: 10s
|
|
205924
|
+
retries: 5
|
|
205925
|
+
restart: unless-stopped
|
|
205926
|
+
|
|
205927
|
+
subgraph-processor:
|
|
205928
|
+
image: ${apiImage}
|
|
205929
|
+
command: ["bun", "run", "packages/subgraphs/src/service.ts"]
|
|
205930
|
+
environment:
|
|
205931
|
+
DATABASE_URL: ${dbUrl}
|
|
205932
|
+
NODE_ENV: production
|
|
205933
|
+
DATA_DIR: /data
|
|
205934
|
+
# Must match the indexer's network key (see indexer note) — catch-up reads
|
|
205935
|
+
# index_progress under NETWORK.
|
|
205936
|
+
NETWORK: testnet
|
|
205937
|
+
STACKS_NETWORK: testnet
|
|
205938
|
+
# The emitter blocks webhook delivery to private/loopback IPs by default.
|
|
205939
|
+
# On a local devnet you deliver to your own localhost receiver, so allow it.
|
|
205940
|
+
SECONDLAYER_ALLOW_PRIVATE_EGRESS: "true"
|
|
205941
|
+
# Must match the api's key so the emitter can decrypt signing secrets.
|
|
205942
|
+
SECONDLAYER_SECRETS_KEY: ${DEV_SECRETS_KEY}
|
|
205943
|
+
volumes:
|
|
205944
|
+
- subgraphs_data:/data/subgraphs
|
|
205945
|
+
depends_on:
|
|
205946
|
+
migrate:
|
|
205947
|
+
condition: service_completed_successfully
|
|
205948
|
+
postgres:
|
|
205949
|
+
condition: service_healthy
|
|
205950
|
+
healthcheck:
|
|
205951
|
+
disable: true
|
|
205952
|
+
restart: unless-stopped
|
|
205953
|
+
|
|
205954
|
+
volumes:
|
|
205955
|
+
postgres_data:
|
|
205956
|
+
subgraphs_data:
|
|
205957
|
+
`;
|
|
205958
|
+
}
|
|
205959
|
+
|
|
207513
205960
|
// src/lib/devnet-config.ts
|
|
207514
205961
|
import { existsSync as existsSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "node:fs";
|
|
207515
205962
|
import { dirname as dirname7, join as join10, parse as parsePath } from "node:path";
|
|
@@ -207716,7 +206163,7 @@ function dockerCompose(composePath, args) {
|
|
|
207716
206163
|
});
|
|
207717
206164
|
return res.status ?? 1;
|
|
207718
206165
|
}
|
|
207719
|
-
var API_URL =
|
|
206166
|
+
var API_URL = resolveApiUrl();
|
|
207720
206167
|
var INDEXER_URL = process.env.INDEXER_URL ?? "http://localhost:3700";
|
|
207721
206168
|
var SERVICES = [
|
|
207722
206169
|
"indexer",
|
|
@@ -207793,7 +206240,7 @@ ${bold("Secondlayer stack up")}`);
|
|
|
207793
206240
|
console.log(`
|
|
207794
206241
|
${bold("Next:")}`);
|
|
207795
206242
|
console.log(` ${yellow("clarinet devnet start")} ${dim("# auto-deploys + streams to the indexer")}`);
|
|
207796
|
-
console.log(` ${yellow(`
|
|
206243
|
+
console.log(` ${yellow(`SECONDLAYER_API_URL=http://localhost:3800 INSTANCE_TOKEN=${DEV_INSTANCE_TOKEN} secondlayer subgraphs deploy ./subgraph.ts`)}`);
|
|
207797
206244
|
console.log(dim(`
|
|
207798
206245
|
The api is published on 127.0.0.1 only: /v1 reads need no credential.
|
|
207799
206246
|
Writes (deploys, webhooks) send INSTANCE_TOKEN=${DEV_INSTANCE_TOKEN}.`));
|
|
@@ -208492,7 +206939,7 @@ async function runInstanceDoctor(jsonOutput) {
|
|
|
208492
206939
|
detail: unreachableError ?? undefined,
|
|
208493
206940
|
nextSteps: [
|
|
208494
206941
|
"Start the instance from docker/oss: docker compose up -d",
|
|
208495
|
-
`If it runs elsewhere, point the CLI at it:
|
|
206942
|
+
`If it runs elsewhere, point the CLI at it: SECONDLAYER_API_URL=<url> (currently ${apiUrl})`
|
|
208496
206943
|
]
|
|
208497
206944
|
}
|
|
208498
206945
|
];
|
|
@@ -208920,13 +207367,132 @@ function registerLogoutCommand(program) {
|
|
|
208920
207367
|
program.command("logout", { hidden: true }).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 }));
|
|
208921
207368
|
}
|
|
208922
207369
|
// src/commands/uninstall.ts
|
|
208923
|
-
init_instance_init();
|
|
208924
|
-
init_output();
|
|
208925
|
-
init_resolve_auth();
|
|
208926
207370
|
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
208927
207371
|
import { existsSync as existsSync10, readFileSync as readFileSync8 } from "node:fs";
|
|
208928
207372
|
import { join as join15, resolve as resolve11 } from "node:path";
|
|
208929
207373
|
import { planUninstall, uninstallCommand } from "@secondlayer/shared/runtime";
|
|
207374
|
+
|
|
207375
|
+
// src/lib/instance-init.ts
|
|
207376
|
+
init_output();
|
|
207377
|
+
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
207378
|
+
import { existsSync as existsSync9, readFileSync as readFileSync7, writeFileSync as writeFileSync7 } from "node:fs";
|
|
207379
|
+
import { resolve as resolve10 } from "node:path";
|
|
207380
|
+
import { ARCHIVE_ROOT_PUBLIC_KEY_PEM } from "@secondlayer/shared/archive/root-key";
|
|
207381
|
+
import { generateEd25519KeyPair } from "@secondlayer/shared/crypto/ed25519";
|
|
207382
|
+
import { INSTANCE_NETWORKS } from "@secondlayer/shared/db/queries/instance";
|
|
207383
|
+
var INSTANCE_ENV_FILE = ".env.local";
|
|
207384
|
+
function isHostedAccountKey(value) {
|
|
207385
|
+
return value.startsWith("sk-sl_") || value.startsWith("ss-sl_");
|
|
207386
|
+
}
|
|
207387
|
+
function parseInstanceNetwork(value) {
|
|
207388
|
+
const network = value.trim().toLowerCase();
|
|
207389
|
+
if (INSTANCE_NETWORKS.includes(network)) {
|
|
207390
|
+
return network;
|
|
207391
|
+
}
|
|
207392
|
+
throw new Error(`network must be mainnet, testnet, or devnet (got ${value})`);
|
|
207393
|
+
}
|
|
207394
|
+
function instanceNetworkFromEnv(fallback = "mainnet") {
|
|
207395
|
+
const raw = process.env.STACKS_NETWORK ?? fallback;
|
|
207396
|
+
try {
|
|
207397
|
+
return parseInstanceNetwork(raw);
|
|
207398
|
+
} catch (err) {
|
|
207399
|
+
printError(err instanceof Error ? err.message : String(err), {
|
|
207400
|
+
hint: "Pass --network mainnet, --network testnet, or --network devnet."
|
|
207401
|
+
});
|
|
207402
|
+
process.exit(1);
|
|
207403
|
+
}
|
|
207404
|
+
}
|
|
207405
|
+
function generateInstanceToken() {
|
|
207406
|
+
return randomBytes3(32).toString("hex");
|
|
207407
|
+
}
|
|
207408
|
+
function generateSecretsKey() {
|
|
207409
|
+
return randomBytes3(32).toString("hex");
|
|
207410
|
+
}
|
|
207411
|
+
function generateSigningPrivateKey() {
|
|
207412
|
+
return generateEd25519KeyPair().privateKeyPem;
|
|
207413
|
+
}
|
|
207414
|
+
function escapeEnvValue(value) {
|
|
207415
|
+
if (!/[\s#"']/.test(value) && !value.includes("\\"))
|
|
207416
|
+
return value;
|
|
207417
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n")}"`;
|
|
207418
|
+
}
|
|
207419
|
+
function unescapeEnvValue(raw) {
|
|
207420
|
+
let value = raw.trim();
|
|
207421
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
207422
|
+
value = value.slice(1, -1);
|
|
207423
|
+
}
|
|
207424
|
+
return value.replace(/\\n/g, `
|
|
207425
|
+
`).replace(/\\"/g, '"').replace(/\\\\/g, "\\");
|
|
207426
|
+
}
|
|
207427
|
+
function readEnvValue(envPath, key) {
|
|
207428
|
+
if (!existsSync9(envPath))
|
|
207429
|
+
return null;
|
|
207430
|
+
const match = readFileSync7(envPath, "utf8").match(new RegExp(`^${key}=(.*)$`, "m"));
|
|
207431
|
+
return match ? unescapeEnvValue(match[1]) : null;
|
|
207432
|
+
}
|
|
207433
|
+
function buildInstanceEnv(input) {
|
|
207434
|
+
const token = input.existing?.INSTANCE_TOKEN || generateInstanceToken();
|
|
207435
|
+
const secrets = input.existing?.SECONDLAYER_SECRETS_KEY || generateSecretsKey();
|
|
207436
|
+
const signing = input.existing?.STREAMS_SIGNING_PRIVATE_KEY || input.existing?.SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY || generateSigningPrivateKey();
|
|
207437
|
+
const apiUrl = input.existing?.SECONDLAYER_API_URL || input.apiUrl || "http://127.0.0.1:3800";
|
|
207438
|
+
const archiveKey = input.existing?.ARCHIVE_SIGNING_PUBLIC_KEY || input.archivePublicKeyPem || ARCHIVE_ROOT_PUBLIC_KEY_PEM;
|
|
207439
|
+
const hostedCandidate = input.existing?.SECONDLAYER_API_KEY || undefined;
|
|
207440
|
+
const hostedKey = hostedCandidate && isHostedAccountKey(hostedCandidate) ? hostedCandidate : undefined;
|
|
207441
|
+
return {
|
|
207442
|
+
INSTANCE_MODE: "oss",
|
|
207443
|
+
STACKS_NETWORK: input.network,
|
|
207444
|
+
INSTANCE_TOKEN: token,
|
|
207445
|
+
SECONDLAYER_SECRETS_KEY: secrets,
|
|
207446
|
+
STREAMS_SIGNING_PRIVATE_KEY: signing,
|
|
207447
|
+
SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY: signing,
|
|
207448
|
+
ALLOW_UNSIGNED_WEBHOOKS: "false",
|
|
207449
|
+
SECONDLAYER_API_URL: apiUrl,
|
|
207450
|
+
...hostedKey ? { SECONDLAYER_API_KEY: hostedKey } : {},
|
|
207451
|
+
ARCHIVE_SIGNING_PUBLIC_KEY: archiveKey
|
|
207452
|
+
};
|
|
207453
|
+
}
|
|
207454
|
+
function renderInstanceEnv(env) {
|
|
207455
|
+
const lines = [
|
|
207456
|
+
"# Generated by secondlayer init. Do not commit.",
|
|
207457
|
+
`INSTANCE_MODE=${env.INSTANCE_MODE}`,
|
|
207458
|
+
`STACKS_NETWORK=${env.STACKS_NETWORK}`,
|
|
207459
|
+
"# The instance credential. The API validates bearer tokens against it,",
|
|
207460
|
+
"# and the CLI/SDK/MCP read it first when authenticating.",
|
|
207461
|
+
`INSTANCE_TOKEN=${env.INSTANCE_TOKEN}`,
|
|
207462
|
+
`SECONDLAYER_SECRETS_KEY=${env.SECONDLAYER_SECRETS_KEY}`,
|
|
207463
|
+
`STREAMS_SIGNING_PRIVATE_KEY=${escapeEnvValue(env.STREAMS_SIGNING_PRIVATE_KEY)}`,
|
|
207464
|
+
`SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY=${escapeEnvValue(env.SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY)}`,
|
|
207465
|
+
`ALLOW_UNSIGNED_WEBHOOKS=${env.ALLOW_UNSIGNED_WEBHOOKS}`,
|
|
207466
|
+
`SECONDLAYER_API_URL=${env.SECONDLAYER_API_URL}`
|
|
207467
|
+
];
|
|
207468
|
+
if (env.SECONDLAYER_API_KEY) {
|
|
207469
|
+
lines.push("# Hosted account key for archive credits (sk-sl_*).", `SECONDLAYER_API_KEY=${env.SECONDLAYER_API_KEY}`);
|
|
207470
|
+
}
|
|
207471
|
+
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)}`, "");
|
|
207472
|
+
return lines.join(`
|
|
207473
|
+
`);
|
|
207474
|
+
}
|
|
207475
|
+
function writeInstanceEnv(cwd, env, file = INSTANCE_ENV_FILE) {
|
|
207476
|
+
const path = resolve10(cwd, file);
|
|
207477
|
+
writeFileSync7(path, renderInstanceEnv(env), { mode: 384 });
|
|
207478
|
+
return path;
|
|
207479
|
+
}
|
|
207480
|
+
function loadExistingInstanceEnv(cwd, file = INSTANCE_ENV_FILE) {
|
|
207481
|
+
const path = resolve10(cwd, file);
|
|
207482
|
+
return {
|
|
207483
|
+
INSTANCE_TOKEN: readEnvValue(path, "INSTANCE_TOKEN"),
|
|
207484
|
+
SECONDLAYER_SECRETS_KEY: readEnvValue(path, "SECONDLAYER_SECRETS_KEY"),
|
|
207485
|
+
STREAMS_SIGNING_PRIVATE_KEY: readEnvValue(path, "STREAMS_SIGNING_PRIVATE_KEY"),
|
|
207486
|
+
SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY: readEnvValue(path, "SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY"),
|
|
207487
|
+
SECONDLAYER_API_URL: readEnvValue(path, "SECONDLAYER_API_URL"),
|
|
207488
|
+
SECONDLAYER_API_KEY: readEnvValue(path, "SECONDLAYER_API_KEY"),
|
|
207489
|
+
ARCHIVE_SIGNING_PUBLIC_KEY: readEnvValue(path, "ARCHIVE_SIGNING_PUBLIC_KEY")
|
|
207490
|
+
};
|
|
207491
|
+
}
|
|
207492
|
+
|
|
207493
|
+
// src/commands/uninstall.ts
|
|
207494
|
+
init_output();
|
|
207495
|
+
init_resolve_auth();
|
|
208930
207496
|
var UNINSTALL_EXIT = { OK: 0, FAILED: 1, REFUSED: 2 };
|
|
208931
207497
|
var REPO_COMPOSE = "docker/oss/docker-compose.yml";
|
|
208932
207498
|
function bundleHasKeys(bundleDir) {
|
|
@@ -209080,7 +207646,7 @@ var SECRET_ENV_KEYS = [
|
|
|
209080
207646
|
"SECONDLAYER_SECRETS_KEY",
|
|
209081
207647
|
"STREAMS_SIGNING_PRIVATE_KEY",
|
|
209082
207648
|
"SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY",
|
|
209083
|
-
"
|
|
207649
|
+
"SECONDLAYER_API_KEY",
|
|
209084
207650
|
"ARCHIVE_SIGNING_PUBLIC_KEY"
|
|
209085
207651
|
];
|
|
209086
207652
|
function readSecretsKey() {
|
|
@@ -209368,8 +207934,6 @@ function registerRestoreCommand(program) {
|
|
|
209368
207934
|
attachRestoreCommand(program.command("restore").description("Restore an instance from a backup bundle").hook("preAction", () => assertInstanceUrl()));
|
|
209369
207935
|
}
|
|
209370
207936
|
// src/commands/init.ts
|
|
209371
|
-
init_archive_reference();
|
|
209372
|
-
init_instance_init();
|
|
209373
207937
|
init_output();
|
|
209374
207938
|
init_resolve_auth();
|
|
209375
207939
|
var DEFAULT_INIT_API_URL = "http://127.0.0.1:3800";
|
|
@@ -209384,7 +207948,7 @@ flags (see \`secondlayer --help\`). Defaults: mainnet, http://127.0.0.1:3800.
|
|
|
209384
207948
|
const env = buildInstanceEnv({
|
|
209385
207949
|
network,
|
|
209386
207950
|
existing,
|
|
209387
|
-
apiUrl: process.env.
|
|
207951
|
+
apiUrl: process.env.SECONDLAYER_API_URL ?? DEFAULT_INIT_API_URL,
|
|
209388
207952
|
archivePublicKeyPem: await resolveArchivePublicKey({
|
|
209389
207953
|
envPem: process.env.ARCHIVE_SIGNING_PUBLIC_KEY ?? process.env.STREAMS_SIGNING_PUBLIC_KEY,
|
|
209390
207954
|
allowHostedApi: !isOssMode()
|
|
@@ -209394,15 +207958,73 @@ flags (see \`secondlayer --help\`). Defaults: mainnet, http://127.0.0.1:3800.
|
|
|
209394
207958
|
success(`Wrote ${INSTANCE_ENV_FILE}`);
|
|
209395
207959
|
note(formatKeyValue([
|
|
209396
207960
|
["network", env.STACKS_NETWORK],
|
|
209397
|
-
["api", env.
|
|
207961
|
+
["api", env.SECONDLAYER_API_URL],
|
|
209398
207962
|
["env", path]
|
|
209399
207963
|
]));
|
|
209400
207964
|
note("Export these or pass --env-file to compose. Do not commit the file.");
|
|
209401
207965
|
});
|
|
209402
207966
|
}
|
|
207967
|
+
// src/lib/observer-stanza.ts
|
|
207968
|
+
var OBSERVER_MODES = ["indexer", "signer-shared"];
|
|
207969
|
+
|
|
207970
|
+
class UnsupportedObserverError extends Error {
|
|
207971
|
+
name = "UnsupportedObserverError";
|
|
207972
|
+
}
|
|
207973
|
+
function parseObserverMode(value) {
|
|
207974
|
+
const mode = value.trim().toLowerCase();
|
|
207975
|
+
if (OBSERVER_MODES.includes(mode)) {
|
|
207976
|
+
return mode;
|
|
207977
|
+
}
|
|
207978
|
+
throw new UnsupportedObserverError(`observer mode must be indexer or signer-shared (got ${value})`);
|
|
207979
|
+
}
|
|
207980
|
+
function parseObserverEndpoint(value) {
|
|
207981
|
+
const trimmed = value.trim();
|
|
207982
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed) || trimmed.startsWith("unix:")) {
|
|
207983
|
+
throw new UnsupportedObserverError(`observer endpoint must be host:port, not a URL (${value})`);
|
|
207984
|
+
}
|
|
207985
|
+
const match = trimmed.match(/^([a-zA-Z0-9._-]+):(\d+)$/);
|
|
207986
|
+
if (!match) {
|
|
207987
|
+
throw new UnsupportedObserverError(`observer endpoint must be host:port (got ${value})`);
|
|
207988
|
+
}
|
|
207989
|
+
const port = Number(match[2]);
|
|
207990
|
+
if (port < 1 || port > 65535) {
|
|
207991
|
+
throw new UnsupportedObserverError(`observer port out of range (${port})`);
|
|
207992
|
+
}
|
|
207993
|
+
return trimmed;
|
|
207994
|
+
}
|
|
207995
|
+
var LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "::1"]);
|
|
207996
|
+
function validateObserverStanza(input) {
|
|
207997
|
+
const endpoint = parseObserverEndpoint(input.endpoint);
|
|
207998
|
+
const host = endpoint.slice(0, endpoint.lastIndexOf(":"));
|
|
207999
|
+
if (input.network !== "devnet" && LOOPBACK_HOSTS.has(host.toLowerCase())) {
|
|
208000
|
+
throw new UnsupportedObserverError(`loopback observer endpoint is not container-visible on ${input.network}; use a docker DNS name such as indexer:3700`);
|
|
208001
|
+
}
|
|
208002
|
+
return { endpoint };
|
|
208003
|
+
}
|
|
208004
|
+
function renderObserverStanza(input) {
|
|
208005
|
+
const { endpoint } = validateObserverStanza(input);
|
|
208006
|
+
const indexer = input.mode === "indexer";
|
|
208007
|
+
const timeoutMs = indexer ? 2000 : 500;
|
|
208008
|
+
const disableRetries = !indexer;
|
|
208009
|
+
const comment = indexer ? "# Pure indexer: retry delivery. A slow observer can stall the node." : "# Signer-shared: do not retry. A missed block stays a gap until `secondlayer repair`.";
|
|
208010
|
+
return [
|
|
208011
|
+
comment,
|
|
208012
|
+
"[[events_observer]]",
|
|
208013
|
+
`endpoint = "${endpoint}"`,
|
|
208014
|
+
'events_keys = ["*"]',
|
|
208015
|
+
`timeout_ms = ${timeoutMs}`,
|
|
208016
|
+
`disable_retries = ${disableRetries}`,
|
|
208017
|
+
""
|
|
208018
|
+
].join(`
|
|
208019
|
+
`);
|
|
208020
|
+
}
|
|
208021
|
+
function defaultObserverEndpoint(network) {
|
|
208022
|
+
if (network === "devnet")
|
|
208023
|
+
return "127.0.0.1:3700";
|
|
208024
|
+
return "indexer:3700";
|
|
208025
|
+
}
|
|
208026
|
+
|
|
209403
208027
|
// src/commands/observer.ts
|
|
209404
|
-
init_instance_init();
|
|
209405
|
-
init_observer_stanza();
|
|
209406
208028
|
init_output();
|
|
209407
208029
|
init_resolve_auth();
|
|
209408
208030
|
function registerObserverCommand(program) {
|
|
@@ -209425,12 +208047,749 @@ init_output();
|
|
|
209425
208047
|
init_resolve_auth();
|
|
209426
208048
|
|
|
209427
208049
|
// src/lib/setup-prompts.ts
|
|
209428
|
-
init_instance_init();
|
|
209429
|
-
init_oss_compose();
|
|
209430
|
-
init_setup_wizard();
|
|
209431
208050
|
import { resolve as resolvePath3 } from "node:path";
|
|
209432
208051
|
import { confirm as confirm4, input as input3, select as select4 } from "@inquirer/prompts";
|
|
209433
208052
|
|
|
208053
|
+
// src/lib/oss-compose.ts
|
|
208054
|
+
function buildOssCompose(opts = {}) {
|
|
208055
|
+
const owner = opts.owner ?? DEFAULT_IMAGE_OWNER;
|
|
208056
|
+
const tag = opts.imageTag ?? DEFAULT_IMAGE_TAG;
|
|
208057
|
+
const runtimeImage = `ghcr.io/${owner}/secondlayer-runtime:${tag}`;
|
|
208058
|
+
return `# Generated by \`secondlayer setup\` — do not edit by hand.
|
|
208059
|
+
# Mirrors docker/oss/docker-compose.yml; see that file for the source of truth
|
|
208060
|
+
# on env vars, profiles, and third-party digest pins. \`secondlayer\` is
|
|
208061
|
+
# swapped from a repo build to the published OSS image (no checkout here).
|
|
208062
|
+
name: secondlayer
|
|
208063
|
+
|
|
208064
|
+
services:
|
|
208065
|
+
postgres:
|
|
208066
|
+
image: postgres:17-alpine@sha256:d4bb0a8c1b7bb2e29f976d099e7bfb9a5d8858cffe9e46b35cd302cd1f1f8168
|
|
208067
|
+
environment:
|
|
208068
|
+
POSTGRES_USER: \${POSTGRES_USER:-secondlayer}
|
|
208069
|
+
POSTGRES_PASSWORD: \${POSTGRES_PASSWORD:-secondlayer}
|
|
208070
|
+
POSTGRES_DB: \${POSTGRES_DB:-secondlayer}
|
|
208071
|
+
volumes:
|
|
208072
|
+
- postgres_data:/var/lib/postgresql/data
|
|
208073
|
+
ports:
|
|
208074
|
+
- "\${POSTGRES_PORT:-127.0.0.1:5432}:5432"
|
|
208075
|
+
healthcheck:
|
|
208076
|
+
test: ["CMD-SHELL", "pg_isready -U \${POSTGRES_USER:-secondlayer}"]
|
|
208077
|
+
interval: 10s
|
|
208078
|
+
timeout: 5s
|
|
208079
|
+
retries: 5
|
|
208080
|
+
restart: unless-stopped
|
|
208081
|
+
|
|
208082
|
+
secondlayer:
|
|
208083
|
+
image: ${runtimeImage}
|
|
208084
|
+
environment:
|
|
208085
|
+
INSTANCE_MODE: oss
|
|
208086
|
+
NETWORK: \${NETWORK:-\${STACKS_NETWORK:-mainnet}}
|
|
208087
|
+
STACKS_NETWORK: \${STACKS_NETWORK:-mainnet}
|
|
208088
|
+
NODE_MODE: \${NODE_MODE:-external}
|
|
208089
|
+
SECONDLAYER_ALLOW_UNDERSIZED: \${SECONDLAYER_ALLOW_UNDERSIZED:-false}
|
|
208090
|
+
DATABASE_URL: postgres://\${POSTGRES_USER:-secondlayer}:\${POSTGRES_PASSWORD:-secondlayer}@postgres:5432/\${POSTGRES_DB:-secondlayer}
|
|
208091
|
+
DATA_DIR: /data
|
|
208092
|
+
API_PORT: "3800"
|
|
208093
|
+
INDEXER_PORT: "3700"
|
|
208094
|
+
# See docker/oss/docker-compose.yml: API_PORT above is the *container*
|
|
208095
|
+
# port; \${API_PORT} in ports:/here is the *publish spec* from .env.
|
|
208096
|
+
LISTEN_HOST: "0.0.0.0"
|
|
208097
|
+
API_PUBLISH_ADDR: \${API_PORT:-127.0.0.1:3800}
|
|
208098
|
+
INSTANCE_TOKEN: \${INSTANCE_TOKEN:-}
|
|
208099
|
+
SECONDLAYER_SECRETS_KEY: \${SECONDLAYER_SECRETS_KEY:-}
|
|
208100
|
+
STREAMS_SIGNING_PRIVATE_KEY: \${STREAMS_SIGNING_PRIVATE_KEY:-}
|
|
208101
|
+
SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY: \${SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY:-}
|
|
208102
|
+
ALLOW_UNSIGNED_WEBHOOKS: \${ALLOW_UNSIGNED_WEBHOOKS:-false}
|
|
208103
|
+
LOG_LEVEL: \${LOG_LEVEL:-info}
|
|
208104
|
+
NODE_ENV: production
|
|
208105
|
+
STACKS_NODE_RPC_URL: \${STACKS_NODE_RPC_URL:-}
|
|
208106
|
+
OBSERVER_JOURNAL_ENABLED: \${OBSERVER_JOURNAL_ENABLED:-true}
|
|
208107
|
+
STREAMS_API_URL: http://127.0.0.1:3800
|
|
208108
|
+
STREAMS_INTERNAL_API_KEY: \${STREAMS_INTERNAL_API_KEY:-}
|
|
208109
|
+
SBTC_DECODER_ENABLED: \${SBTC_DECODER_ENABLED:-true}
|
|
208110
|
+
POX4_DECODER_ENABLED: \${POX4_DECODER_ENABLED:-true}
|
|
208111
|
+
BNS_DECODER_ENABLED: \${BNS_DECODER_ENABLED:-false}
|
|
208112
|
+
SUBGRAPH_SOURCE: \${SUBGRAPH_SOURCE:-streams-index}
|
|
208113
|
+
SUBGRAPH_INDEX_API_URL: http://127.0.0.1:3800
|
|
208114
|
+
BITCOIN_RPC_PASSWORD: \${BITCOIN_RPC_PASSWORD:-}
|
|
208115
|
+
ports:
|
|
208116
|
+
- "\${API_PORT:-127.0.0.1:3800}:3800"
|
|
208117
|
+
- "\${INDEXER_PORT:-127.0.0.1:3700}:3700"
|
|
208118
|
+
# \`secondlayer observer\`'s default endpoint is \`indexer:3700\` — this
|
|
208119
|
+
# alias is what makes that default actually resolve on this network,
|
|
208120
|
+
# without renaming the service. Mirrors docker/oss/docker-compose.yml.
|
|
208121
|
+
networks:
|
|
208122
|
+
default:
|
|
208123
|
+
aliases:
|
|
208124
|
+
- indexer
|
|
208125
|
+
volumes:
|
|
208126
|
+
- subgraphs_data:/data/subgraphs
|
|
208127
|
+
depends_on:
|
|
208128
|
+
postgres:
|
|
208129
|
+
condition: service_healthy
|
|
208130
|
+
healthcheck:
|
|
208131
|
+
test: ["CMD", "curl", "-f", "http://localhost:3800/health"]
|
|
208132
|
+
interval: 30s
|
|
208133
|
+
timeout: 5s
|
|
208134
|
+
start_period: 40s
|
|
208135
|
+
retries: 3
|
|
208136
|
+
restart: unless-stopped
|
|
208137
|
+
|
|
208138
|
+
bitcoind:
|
|
208139
|
+
# Digest = kylemanna/bitcoind:latest as published 2026-07-20 — see
|
|
208140
|
+
# docker/oss/docker-compose.yml for why the digest is the real pin here.
|
|
208141
|
+
image: kylemanna/bitcoind:latest@sha256:86bbcaa99bf3bf3d5df7fd6d9217b3a41194db4c4e34385064f2cfc9fa7bdf91
|
|
208142
|
+
volumes:
|
|
208143
|
+
- \${BITCOIN_DATA_DIR:-./data/bitcoin}:/bitcoin/.bitcoin
|
|
208144
|
+
ports:
|
|
208145
|
+
- "127.0.0.1:8332:8332"
|
|
208146
|
+
- "8333:8333"
|
|
208147
|
+
healthcheck:
|
|
208148
|
+
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 ]"]
|
|
208149
|
+
interval: 60s
|
|
208150
|
+
timeout: 15s
|
|
208151
|
+
start_period: 120s
|
|
208152
|
+
retries: 3
|
|
208153
|
+
restart: unless-stopped
|
|
208154
|
+
deploy:
|
|
208155
|
+
resources:
|
|
208156
|
+
limits:
|
|
208157
|
+
memory: 32G
|
|
208158
|
+
profiles: ["full-node"]
|
|
208159
|
+
|
|
208160
|
+
stacks-node:
|
|
208161
|
+
# Consensus-critical — digest-pinned to 4.0.1. See docker/oss/docker-compose.yml.
|
|
208162
|
+
image: ghcr.io/stacks-network/stacks-core:4.0.1@sha256:ceb768f881ef52a1d2792a2b4a89d81e092b1df11293b04c31ce36613c3f9711
|
|
208163
|
+
volumes:
|
|
208164
|
+
- \${STACKS_DATA_DIR:-./data/stacks}:/stacks-blockchain/data
|
|
208165
|
+
- ./Config.toml:/stacks-blockchain/Config.toml:ro
|
|
208166
|
+
ports:
|
|
208167
|
+
- "20443:20443"
|
|
208168
|
+
- "20444:20444"
|
|
208169
|
+
restart: unless-stopped
|
|
208170
|
+
command: ["stacks-node", "start", "--config", "/stacks-blockchain/Config.toml"]
|
|
208171
|
+
# full-node only — see docker/oss/docker-compose.yml. No standalone
|
|
208172
|
+
# "stacks-node, public Bitcoin" profile: nothing here wires the node at
|
|
208173
|
+
# a real public Bitcoin RPC, so that combination never actually synced.
|
|
208174
|
+
profiles: ["full-node"]
|
|
208175
|
+
|
|
208176
|
+
volumes:
|
|
208177
|
+
postgres_data:
|
|
208178
|
+
subgraphs_data:
|
|
208179
|
+
`;
|
|
208180
|
+
}
|
|
208181
|
+
function buildOssStacksConfigToml(opts) {
|
|
208182
|
+
const mode = opts.network === "mainnet" ? "mainnet" : "testnet";
|
|
208183
|
+
return `[node]
|
|
208184
|
+
working_dir = "/stacks-blockchain/data"
|
|
208185
|
+
rpc_bind = "0.0.0.0:20443"
|
|
208186
|
+
p2p_bind = "0.0.0.0:20444"
|
|
208187
|
+
seed = ""
|
|
208188
|
+
marf_compress = true
|
|
208189
|
+
bootstrap_node = "02196f005965cebe6ddc3901b7b1cc1aa7a88f305bb8c5893456b8f9a605923893@seed.mainnet.hiro.so:20444,02539449ad94e6e6392d8c1deb2b4e61f80ae2a18964349bc14336d8b903c46a8c@cet.stacksnodes.org:20444,02ececc8ce79b8adf813f13a0255f8ae58d4357309ba0cedd523d9f1a306fcfb79@sgt.stacksnodes.org:20444,0303144ba518fe7a0fb56a8a7d488f950307a4330f146e1e1458fc63fb33defe96@est.stacksnodes.org:20444"
|
|
208190
|
+
|
|
208191
|
+
# Event observer points directly at the bundled indexer service, same docker
|
|
208192
|
+
# network. Retries off / low timeout: a stuck indexer must never starve a
|
|
208193
|
+
# co-located signer of block_proposal events. See docker/oss/Config.toml.
|
|
208194
|
+
[[events_observer]]
|
|
208195
|
+
endpoint = "secondlayer:3700"
|
|
208196
|
+
events_keys = ["*"]
|
|
208197
|
+
timeout_ms = 500
|
|
208198
|
+
disable_retries = true
|
|
208199
|
+
|
|
208200
|
+
[burnchain]
|
|
208201
|
+
chain = "bitcoin"
|
|
208202
|
+
mode = "${mode}"
|
|
208203
|
+
peer_host = "bitcoind"
|
|
208204
|
+
username = "stacks"
|
|
208205
|
+
password = "${opts.bitcoinRpcPassword}"
|
|
208206
|
+
rpc_port = 8332
|
|
208207
|
+
peer_port = 8333
|
|
208208
|
+
`;
|
|
208209
|
+
}
|
|
208210
|
+
function buildOssBitcoinConf(opts) {
|
|
208211
|
+
return `server=1
|
|
208212
|
+
txindex=1
|
|
208213
|
+
rpcallowip=0.0.0.0/0
|
|
208214
|
+
rpcbind=0.0.0.0
|
|
208215
|
+
rpcuser=stacks
|
|
208216
|
+
rpcpassword=${opts.rpcPassword}
|
|
208217
|
+
# Prune: 0 = archive (required for Stacks); set to 550+ only for pruned chains.
|
|
208218
|
+
prune=0
|
|
208219
|
+
dbcache=4096
|
|
208220
|
+
`;
|
|
208221
|
+
}
|
|
208222
|
+
|
|
208223
|
+
// src/lib/setup-wizard.ts
|
|
208224
|
+
import { spawn as spawn3, spawnSync as spawnSync5 } from "node:child_process";
|
|
208225
|
+
import { randomBytes as randomBytes4 } from "node:crypto";
|
|
208226
|
+
import { existsSync as existsSync12 } from "node:fs";
|
|
208227
|
+
import { writeFile as writeFile6 } from "node:fs/promises";
|
|
208228
|
+
import { createServer } from "node:net";
|
|
208229
|
+
import { join as join17, resolve as resolvePath2 } from "node:path";
|
|
208230
|
+
import { closeDb as closeDb2, getRawClientFor } from "@secondlayer/shared/db";
|
|
208231
|
+
import { FLOORS, diskFloorGb } from "@secondlayer/shared/runtime";
|
|
208232
|
+
init_resolve_auth();
|
|
208233
|
+
var SETUP_NODE_MODES = ["external", "stacks", "full"];
|
|
208234
|
+
var DEFAULT_ARCHIVE_MANIFEST = "https://archive.secondlayer.tools/latest.json";
|
|
208235
|
+
class MissingSetupFlagError extends Error {
|
|
208236
|
+
flag;
|
|
208237
|
+
constructor(flag, message) {
|
|
208238
|
+
super(message);
|
|
208239
|
+
this.name = "MissingSetupFlagError";
|
|
208240
|
+
this.flag = flag;
|
|
208241
|
+
}
|
|
208242
|
+
}
|
|
208243
|
+
function parseSetupNodeMode(value) {
|
|
208244
|
+
const mode = value.trim().toLowerCase();
|
|
208245
|
+
if (SETUP_NODE_MODES.includes(mode)) {
|
|
208246
|
+
return mode;
|
|
208247
|
+
}
|
|
208248
|
+
throw new Error(`node mode must be external, stacks, or full (got ${value})`);
|
|
208249
|
+
}
|
|
208250
|
+
function resolveNonInteractiveConfig(flags) {
|
|
208251
|
+
if (!flags.network) {
|
|
208252
|
+
throw new MissingSetupFlagError("--network", "Missing --network <mainnet|testnet|devnet>. Non-interactive setup requires it explicitly — there is no safe default.");
|
|
208253
|
+
}
|
|
208254
|
+
if (!flags.nodeMode) {
|
|
208255
|
+
throw new MissingSetupFlagError("--node-mode", "Missing --node-mode <external|stacks|full>. Non-interactive setup requires it explicitly — there is no safe default.");
|
|
208256
|
+
}
|
|
208257
|
+
const network = parseInstanceNetwork(flags.network);
|
|
208258
|
+
const nodeMode = parseSetupNodeMode(flags.nodeMode);
|
|
208259
|
+
const dir = resolvePath2(flags.dir ?? process.cwd());
|
|
208260
|
+
const skipBootstrap = !!flags.skipBootstrap;
|
|
208261
|
+
return {
|
|
208262
|
+
network,
|
|
208263
|
+
nodeMode,
|
|
208264
|
+
apiPort: flags.apiPort ?? "127.0.0.1:3800",
|
|
208265
|
+
indexerPort: "127.0.0.1:3700",
|
|
208266
|
+
postgresPort: "127.0.0.1:5432",
|
|
208267
|
+
dir,
|
|
208268
|
+
against: skipBootstrap ? undefined : flags.against ?? DEFAULT_ARCHIVE_MANIFEST,
|
|
208269
|
+
skipBootstrap,
|
|
208270
|
+
skipVerify: !!flags.skipVerify,
|
|
208271
|
+
yes: !!flags.yes,
|
|
208272
|
+
force: !!flags.force,
|
|
208273
|
+
owner: flags.owner ?? DEFAULT_IMAGE_OWNER,
|
|
208274
|
+
imageTag: flags.imageTag ?? DEFAULT_IMAGE_TAG
|
|
208275
|
+
};
|
|
208276
|
+
}
|
|
208277
|
+
function guardrailPreview(nodeMode, network) {
|
|
208278
|
+
return {
|
|
208279
|
+
ramFloorMb: nodeMode === "full" ? FLOORS.fullRamMb : FLOORS.appRamMb,
|
|
208280
|
+
diskFloorGb: diskFloorGb(nodeMode, network)
|
|
208281
|
+
};
|
|
208282
|
+
}
|
|
208283
|
+
async function checkDocker() {
|
|
208284
|
+
const probe = spawnSync5("docker", ["info"], { stdio: "ignore" });
|
|
208285
|
+
return !probe.error && probe.status === 0;
|
|
208286
|
+
}
|
|
208287
|
+
function isPortFree(host, port) {
|
|
208288
|
+
return new Promise((resolvePromise) => {
|
|
208289
|
+
const srv = createServer();
|
|
208290
|
+
srv.once("error", () => resolvePromise(false));
|
|
208291
|
+
srv.once("listening", () => srv.close(() => resolvePromise(true)));
|
|
208292
|
+
srv.listen(port, host);
|
|
208293
|
+
});
|
|
208294
|
+
}
|
|
208295
|
+
function parsePublishSpec(spec) {
|
|
208296
|
+
const idx = spec.lastIndexOf(":");
|
|
208297
|
+
if (idx === -1)
|
|
208298
|
+
return { host: "127.0.0.1", port: Number(spec) };
|
|
208299
|
+
return { host: spec.slice(0, idx), port: Number(spec.slice(idx + 1)) };
|
|
208300
|
+
}
|
|
208301
|
+
function dockerPublishedHostPorts() {
|
|
208302
|
+
const probe = spawnSync5("docker", ["ps", "--format", "{{.Ports}}"], {
|
|
208303
|
+
encoding: "utf8"
|
|
208304
|
+
});
|
|
208305
|
+
const ports = new Set;
|
|
208306
|
+
if (probe.error || probe.status !== 0 || !probe.stdout)
|
|
208307
|
+
return ports;
|
|
208308
|
+
for (const match of probe.stdout.matchAll(/:(\d+)->/g)) {
|
|
208309
|
+
ports.add(Number(match[1]));
|
|
208310
|
+
}
|
|
208311
|
+
return ports;
|
|
208312
|
+
}
|
|
208313
|
+
async function resolveAvailablePublishSpec(spec, claimedByDocker = new Set, maxAttempts = 20) {
|
|
208314
|
+
const { host, port } = parsePublishSpec(spec);
|
|
208315
|
+
for (let attempt = 0;attempt < maxAttempts; attempt++) {
|
|
208316
|
+
const candidate = port + attempt;
|
|
208317
|
+
if (claimedByDocker.has(candidate))
|
|
208318
|
+
continue;
|
|
208319
|
+
if (await isPortFree(host, candidate)) {
|
|
208320
|
+
return { spec: `${host}:${candidate}`, remapped: attempt > 0 };
|
|
208321
|
+
}
|
|
208322
|
+
}
|
|
208323
|
+
return { spec, remapped: false };
|
|
208324
|
+
}
|
|
208325
|
+
function randomHex(bytes) {
|
|
208326
|
+
return randomBytes4(bytes).toString("hex");
|
|
208327
|
+
}
|
|
208328
|
+
function resolveSecrets(config) {
|
|
208329
|
+
const envPath = join17(config.dir, ".env");
|
|
208330
|
+
const existing = config.force || !existsSync12(envPath) ? {} : readEnvFile(config.dir, envPath);
|
|
208331
|
+
const instance = buildInstanceEnv({
|
|
208332
|
+
network: config.network,
|
|
208333
|
+
existing: {
|
|
208334
|
+
INSTANCE_TOKEN: existing.INSTANCE_TOKEN,
|
|
208335
|
+
SECONDLAYER_SECRETS_KEY: existing.SECONDLAYER_SECRETS_KEY,
|
|
208336
|
+
STREAMS_SIGNING_PRIVATE_KEY: existing.STREAMS_SIGNING_PRIVATE_KEY,
|
|
208337
|
+
SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY: existing.SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY,
|
|
208338
|
+
SECONDLAYER_API_URL: existing.SECONDLAYER_API_URL,
|
|
208339
|
+
SECONDLAYER_API_KEY: existing.SECONDLAYER_API_KEY,
|
|
208340
|
+
ARCHIVE_SIGNING_PUBLIC_KEY: existing.ARCHIVE_SIGNING_PUBLIC_KEY
|
|
208341
|
+
},
|
|
208342
|
+
apiUrl: `http://${normalizeLoopbackHost(config.apiPort)}`,
|
|
208343
|
+
archivePublicKeyPem: config.archivePublicKeyPem
|
|
208344
|
+
});
|
|
208345
|
+
return {
|
|
208346
|
+
instance,
|
|
208347
|
+
postgresPassword: existing.POSTGRES_PASSWORD || randomHex(24),
|
|
208348
|
+
bitcoinRpcPassword: existing.BITCOIN_RPC_PASSWORD || randomHex(24),
|
|
208349
|
+
databaseUrl: operatorEditedDatabaseUrl(existing)
|
|
208350
|
+
};
|
|
208351
|
+
}
|
|
208352
|
+
function operatorEditedDatabaseUrl(existing) {
|
|
208353
|
+
const url = existing.DATABASE_URL;
|
|
208354
|
+
if (!url)
|
|
208355
|
+
return;
|
|
208356
|
+
if (existing.POSTGRES_PORT && existing.POSTGRES_PASSWORD) {
|
|
208357
|
+
const generated = setupDatabaseUrl({ postgresPort: existing.POSTGRES_PORT }, { postgresPassword: existing.POSTGRES_PASSWORD });
|
|
208358
|
+
if (url === generated)
|
|
208359
|
+
return;
|
|
208360
|
+
}
|
|
208361
|
+
return url;
|
|
208362
|
+
}
|
|
208363
|
+
var SETUP_POSTGRES_USER = "secondlayer";
|
|
208364
|
+
var SETUP_POSTGRES_DB = "secondlayer";
|
|
208365
|
+
function setupDatabaseUrl(config, secrets) {
|
|
208366
|
+
if (secrets.databaseUrl)
|
|
208367
|
+
return secrets.databaseUrl;
|
|
208368
|
+
const password = encodeURIComponent(secrets.postgresPassword);
|
|
208369
|
+
return `postgres://${SETUP_POSTGRES_USER}:${password}@${normalizeLoopbackHost(config.postgresPort)}/${SETUP_POSTGRES_DB}`;
|
|
208370
|
+
}
|
|
208371
|
+
function normalizeLoopbackHost(apiPort) {
|
|
208372
|
+
return apiPort.includes(":") ? apiPort : `127.0.0.1:${apiPort}`;
|
|
208373
|
+
}
|
|
208374
|
+
function readEnvFile(dir, envPath) {
|
|
208375
|
+
const shared = loadExistingInstanceEnv(dir, ".env");
|
|
208376
|
+
return {
|
|
208377
|
+
...shared,
|
|
208378
|
+
POSTGRES_PASSWORD: readEnvValue(envPath, "POSTGRES_PASSWORD"),
|
|
208379
|
+
POSTGRES_PORT: readEnvValue(envPath, "POSTGRES_PORT"),
|
|
208380
|
+
BITCOIN_RPC_PASSWORD: readEnvValue(envPath, "BITCOIN_RPC_PASSWORD"),
|
|
208381
|
+
DATABASE_URL: readEnvValue(envPath, "DATABASE_URL")
|
|
208382
|
+
};
|
|
208383
|
+
}
|
|
208384
|
+
function renderSetupEnv(config, secrets) {
|
|
208385
|
+
const i = secrets.instance;
|
|
208386
|
+
const lines = [
|
|
208387
|
+
"# Generated by `secondlayer setup`. Do not commit.",
|
|
208388
|
+
`NETWORK=${config.network}`,
|
|
208389
|
+
`STACKS_NETWORK=${config.network}`,
|
|
208390
|
+
`NODE_MODE=${config.nodeMode}`,
|
|
208391
|
+
`API_PORT=${config.apiPort}`,
|
|
208392
|
+
`INDEXER_PORT=${config.indexerPort}`,
|
|
208393
|
+
`POSTGRES_USER=${SETUP_POSTGRES_USER}`,
|
|
208394
|
+
`POSTGRES_PASSWORD=${secrets.postgresPassword}`,
|
|
208395
|
+
`POSTGRES_DB=${SETUP_POSTGRES_DB}`,
|
|
208396
|
+
`POSTGRES_PORT=${config.postgresPort}`,
|
|
208397
|
+
"# Where bootstrap, verify, repair, and backup connect from this directory:",
|
|
208398
|
+
"# the compose Postgres above, published on the host. Point it elsewhere",
|
|
208399
|
+
"# only if you moved the index.",
|
|
208400
|
+
`DATABASE_URL=${escapeEnvValue(setupDatabaseUrl(config, secrets))}`,
|
|
208401
|
+
"# The instance credential. The API validates bearer tokens against it,",
|
|
208402
|
+
"# and the CLI/SDK/MCP read it first when authenticating.",
|
|
208403
|
+
`INSTANCE_TOKEN=${i.INSTANCE_TOKEN}`,
|
|
208404
|
+
`SECONDLAYER_SECRETS_KEY=${i.SECONDLAYER_SECRETS_KEY}`,
|
|
208405
|
+
`STREAMS_SIGNING_PRIVATE_KEY=${escapeEnvValue(i.STREAMS_SIGNING_PRIVATE_KEY)}`,
|
|
208406
|
+
`SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY=${escapeEnvValue(i.SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY)}`,
|
|
208407
|
+
`ALLOW_UNSIGNED_WEBHOOKS=${i.ALLOW_UNSIGNED_WEBHOOKS}`,
|
|
208408
|
+
`SECONDLAYER_API_URL=${i.SECONDLAYER_API_URL}`
|
|
208409
|
+
];
|
|
208410
|
+
if (i.SECONDLAYER_API_KEY) {
|
|
208411
|
+
lines.push("# Hosted account key for archive credits (sk-sl_*).", `SECONDLAYER_API_KEY=${i.SECONDLAYER_API_KEY}`);
|
|
208412
|
+
}
|
|
208413
|
+
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)}`);
|
|
208414
|
+
if (config.nodeMode === "full") {
|
|
208415
|
+
lines.push("BITCOIN_RPC_USER=stacks", `BITCOIN_RPC_PASSWORD=${secrets.bitcoinRpcPassword}`);
|
|
208416
|
+
}
|
|
208417
|
+
lines.push("");
|
|
208418
|
+
return lines.join(`
|
|
208419
|
+
`);
|
|
208420
|
+
}
|
|
208421
|
+
async function writeSetupFiles(config, secrets) {
|
|
208422
|
+
const composePath = join17(config.dir, "docker-compose.yml");
|
|
208423
|
+
const envPath = join17(config.dir, ".env");
|
|
208424
|
+
await writeFile6(composePath, buildOssCompose({ owner: config.owner, imageTag: config.imageTag }));
|
|
208425
|
+
await writeFile6(envPath, renderSetupEnv(config, secrets), { mode: 384 });
|
|
208426
|
+
const result = { composePath, envPath };
|
|
208427
|
+
if (config.nodeMode === "full") {
|
|
208428
|
+
const configTomlPath = join17(config.dir, "Config.toml");
|
|
208429
|
+
await writeFile6(configTomlPath, buildOssStacksConfigToml({
|
|
208430
|
+
network: config.network,
|
|
208431
|
+
bitcoinRpcPassword: secrets.bitcoinRpcPassword ?? ""
|
|
208432
|
+
}));
|
|
208433
|
+
result.configTomlPath = configTomlPath;
|
|
208434
|
+
}
|
|
208435
|
+
if (config.nodeMode === "full") {
|
|
208436
|
+
const bitcoinConfPath = join17(config.dir, "bitcoin.conf");
|
|
208437
|
+
await writeFile6(bitcoinConfPath, buildOssBitcoinConf({ rpcPassword: secrets.bitcoinRpcPassword ?? "" }));
|
|
208438
|
+
result.bitcoinConfPath = bitcoinConfPath;
|
|
208439
|
+
}
|
|
208440
|
+
return result;
|
|
208441
|
+
}
|
|
208442
|
+
function composeProfileArgs(nodeMode) {
|
|
208443
|
+
if (nodeMode === "full")
|
|
208444
|
+
return ["--profile", "full-node"];
|
|
208445
|
+
return [];
|
|
208446
|
+
}
|
|
208447
|
+
function runStreaming(command, args, opts) {
|
|
208448
|
+
return new Promise((resolvePromise, reject) => {
|
|
208449
|
+
const child = spawn3(command, args, {
|
|
208450
|
+
cwd: opts.cwd,
|
|
208451
|
+
env: opts.env ?? process.env,
|
|
208452
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
208453
|
+
});
|
|
208454
|
+
let stdout = "";
|
|
208455
|
+
let stderr = "";
|
|
208456
|
+
const pump = (stream) => (chunk) => {
|
|
208457
|
+
const text = chunk.toString("utf8");
|
|
208458
|
+
if (stream === "stdout")
|
|
208459
|
+
stdout += text;
|
|
208460
|
+
else
|
|
208461
|
+
stderr += text;
|
|
208462
|
+
if (opts.onLine) {
|
|
208463
|
+
for (const line of text.split(`
|
|
208464
|
+
`)) {
|
|
208465
|
+
if (line.length > 0)
|
|
208466
|
+
opts.onLine(line, stream);
|
|
208467
|
+
}
|
|
208468
|
+
}
|
|
208469
|
+
};
|
|
208470
|
+
child.stdout?.on("data", pump("stdout"));
|
|
208471
|
+
child.stderr?.on("data", pump("stderr"));
|
|
208472
|
+
child.once("error", reject);
|
|
208473
|
+
child.once("close", (code) => {
|
|
208474
|
+
resolvePromise({ code: code ?? 1, stdout, stderr });
|
|
208475
|
+
});
|
|
208476
|
+
});
|
|
208477
|
+
}
|
|
208478
|
+
async function dockerComposeUp(config, files, onLine) {
|
|
208479
|
+
const args = [
|
|
208480
|
+
"compose",
|
|
208481
|
+
"-f",
|
|
208482
|
+
files.composePath,
|
|
208483
|
+
"--env-file",
|
|
208484
|
+
files.envPath,
|
|
208485
|
+
...composeProfileArgs(config.nodeMode),
|
|
208486
|
+
"up",
|
|
208487
|
+
"-d"
|
|
208488
|
+
];
|
|
208489
|
+
return runStreaming("docker", args, { cwd: config.dir, onLine });
|
|
208490
|
+
}
|
|
208491
|
+
async function pollHealth(config, opts = {}) {
|
|
208492
|
+
const timeoutMs = opts.timeoutMs ?? 120000;
|
|
208493
|
+
const intervalMs = opts.intervalMs ?? 2000;
|
|
208494
|
+
const url = `http://${normalizeLoopbackHost(config.apiPort)}/health`;
|
|
208495
|
+
const deadline = Date.now() + timeoutMs;
|
|
208496
|
+
for (;; ) {
|
|
208497
|
+
try {
|
|
208498
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(3000) });
|
|
208499
|
+
if (res.ok)
|
|
208500
|
+
return true;
|
|
208501
|
+
} catch {}
|
|
208502
|
+
opts.onTick?.();
|
|
208503
|
+
if (Date.now() >= deadline)
|
|
208504
|
+
return false;
|
|
208505
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
208506
|
+
}
|
|
208507
|
+
}
|
|
208508
|
+
function getObserverInfo(config) {
|
|
208509
|
+
if (config.nodeMode !== "external") {
|
|
208510
|
+
return { applicable: false };
|
|
208511
|
+
}
|
|
208512
|
+
const endpoint = defaultObserverEndpoint(config.network);
|
|
208513
|
+
const stanza = renderObserverStanza({
|
|
208514
|
+
mode: "indexer",
|
|
208515
|
+
endpoint,
|
|
208516
|
+
network: config.network
|
|
208517
|
+
});
|
|
208518
|
+
return { applicable: true, stanza, endpoint };
|
|
208519
|
+
}
|
|
208520
|
+
function cliEntry() {
|
|
208521
|
+
const entry = process.argv[1];
|
|
208522
|
+
if (!entry) {
|
|
208523
|
+
throw new Error("Could not resolve the secondlayer CLI entry to shell out to.");
|
|
208524
|
+
}
|
|
208525
|
+
return [process.execPath, entry];
|
|
208526
|
+
}
|
|
208527
|
+
var CHILD_ENV_KEYS = [
|
|
208528
|
+
"DATABASE_URL",
|
|
208529
|
+
"ARCHIVE_SIGNING_PUBLIC_KEY",
|
|
208530
|
+
"SECONDLAYER_API_URL",
|
|
208531
|
+
"INSTANCE_TOKEN"
|
|
208532
|
+
];
|
|
208533
|
+
function buildChildEnv(envPath, base = process.env) {
|
|
208534
|
+
const env = { ...base };
|
|
208535
|
+
for (const key of CHILD_ENV_KEYS) {
|
|
208536
|
+
const value = readEnvValue(envPath, key);
|
|
208537
|
+
if (value)
|
|
208538
|
+
env[key] = value;
|
|
208539
|
+
}
|
|
208540
|
+
return env;
|
|
208541
|
+
}
|
|
208542
|
+
async function checkDatabaseReachable(databaseUrl) {
|
|
208543
|
+
try {
|
|
208544
|
+
const client = getRawClientFor(databaseUrl);
|
|
208545
|
+
await client`SELECT 1`;
|
|
208546
|
+
return { ok: true };
|
|
208547
|
+
} catch (err) {
|
|
208548
|
+
return {
|
|
208549
|
+
ok: false,
|
|
208550
|
+
error: err instanceof Error ? err.message : String(err)
|
|
208551
|
+
};
|
|
208552
|
+
} finally {
|
|
208553
|
+
await closeDb2().catch(() => {});
|
|
208554
|
+
}
|
|
208555
|
+
}
|
|
208556
|
+
async function preflightBootstrapDatabase(config, secrets, emit) {
|
|
208557
|
+
const databaseUrl = setupDatabaseUrl(config, secrets);
|
|
208558
|
+
const reachable = await checkDatabaseReachable(databaseUrl);
|
|
208559
|
+
if (!reachable.ok) {
|
|
208560
|
+
emit({
|
|
208561
|
+
type: "step-error",
|
|
208562
|
+
step: "bootstrap",
|
|
208563
|
+
message: `Postgres at ${redactUrl(databaseUrl)} did not answer SELECT 1: ${reachable.error}. Check \`docker compose logs postgres\`, then re-run setup to resume.`
|
|
208564
|
+
});
|
|
208565
|
+
return null;
|
|
208566
|
+
}
|
|
208567
|
+
emit({
|
|
208568
|
+
type: "step-log",
|
|
208569
|
+
step: "bootstrap",
|
|
208570
|
+
line: `database ${redactUrl(databaseUrl)}`
|
|
208571
|
+
});
|
|
208572
|
+
return databaseUrl;
|
|
208573
|
+
}
|
|
208574
|
+
async function runBootstrap(config, against, files, onLine) {
|
|
208575
|
+
const [command, ...prefix] = cliEntry();
|
|
208576
|
+
const args = [...prefix, "bootstrap", "--against", against, "--yes"];
|
|
208577
|
+
return runStreaming(command, args, {
|
|
208578
|
+
cwd: config.dir,
|
|
208579
|
+
env: buildChildEnv(files.envPath),
|
|
208580
|
+
onLine
|
|
208581
|
+
});
|
|
208582
|
+
}
|
|
208583
|
+
async function runVerify(config, against, files, onLine) {
|
|
208584
|
+
const [command, ...prefix] = cliEntry();
|
|
208585
|
+
const args = [...prefix, "verify", "all", "--against", against];
|
|
208586
|
+
return runStreaming(command, args, {
|
|
208587
|
+
cwd: config.dir,
|
|
208588
|
+
env: buildChildEnv(files.envPath),
|
|
208589
|
+
onLine
|
|
208590
|
+
});
|
|
208591
|
+
}
|
|
208592
|
+
function redactUrl(url) {
|
|
208593
|
+
try {
|
|
208594
|
+
const u = new URL(url);
|
|
208595
|
+
if (u.password)
|
|
208596
|
+
u.password = "***";
|
|
208597
|
+
return u.toString();
|
|
208598
|
+
} catch {
|
|
208599
|
+
return url;
|
|
208600
|
+
}
|
|
208601
|
+
}
|
|
208602
|
+
function buildSuccessSummary(config, secrets) {
|
|
208603
|
+
const apiUrl = `http://${normalizeLoopbackHost(config.apiPort)}`;
|
|
208604
|
+
const lines = [
|
|
208605
|
+
"Secondlayer is up.",
|
|
208606
|
+
"",
|
|
208607
|
+
` api ${apiUrl}`,
|
|
208608
|
+
` network ${config.network}`,
|
|
208609
|
+
` node mode ${config.nodeMode}`,
|
|
208610
|
+
` dir ${config.dir}`,
|
|
208611
|
+
"",
|
|
208612
|
+
"Export the instance token for writes (reads on loopback need no credential):",
|
|
208613
|
+
` export INSTANCE_TOKEN=${secrets.instance.INSTANCE_TOKEN}`,
|
|
208614
|
+
"",
|
|
208615
|
+
"Try it:",
|
|
208616
|
+
" secondlayer subgraphs create my-balances --from-contract SP....my-contract",
|
|
208617
|
+
" secondlayer subgraphs deploy subgraphs/my-balances.ts",
|
|
208618
|
+
` curl ${apiUrl}/v1/subgraphs/my-balances/balances`,
|
|
208619
|
+
"",
|
|
208620
|
+
"Docs: https://secondlayer.tools/docs/self-host"
|
|
208621
|
+
];
|
|
208622
|
+
return lines.join(`
|
|
208623
|
+
`);
|
|
208624
|
+
}
|
|
208625
|
+
async function runSetup(initialConfig, emit) {
|
|
208626
|
+
let config = initialConfig;
|
|
208627
|
+
emit({ type: "step-start", step: "preflight" });
|
|
208628
|
+
if (!await checkDocker()) {
|
|
208629
|
+
emit({
|
|
208630
|
+
type: "step-error",
|
|
208631
|
+
step: "preflight",
|
|
208632
|
+
message: "Docker isn't reachable. Start Docker Desktop/OrbStack, or install Docker, then re-run."
|
|
208633
|
+
});
|
|
208634
|
+
return { ok: false, config };
|
|
208635
|
+
}
|
|
208636
|
+
const claimedByDocker = dockerPublishedHostPorts();
|
|
208637
|
+
const ports = {
|
|
208638
|
+
api: await resolveAvailablePublishSpec(config.apiPort, claimedByDocker),
|
|
208639
|
+
indexer: await resolveAvailablePublishSpec(config.indexerPort, claimedByDocker),
|
|
208640
|
+
postgres: await resolveAvailablePublishSpec(config.postgresPort, claimedByDocker)
|
|
208641
|
+
};
|
|
208642
|
+
config = {
|
|
208643
|
+
...config,
|
|
208644
|
+
apiPort: ports.api.spec,
|
|
208645
|
+
indexerPort: ports.indexer.spec,
|
|
208646
|
+
postgresPort: ports.postgres.spec
|
|
208647
|
+
};
|
|
208648
|
+
for (const [label, result] of [
|
|
208649
|
+
["api", ports.api],
|
|
208650
|
+
["indexer", ports.indexer],
|
|
208651
|
+
["postgres", ports.postgres]
|
|
208652
|
+
]) {
|
|
208653
|
+
if (result.remapped) {
|
|
208654
|
+
emit({
|
|
208655
|
+
type: "step-log",
|
|
208656
|
+
step: "preflight",
|
|
208657
|
+
line: `${label} port was already in use locally, using ${result.spec} instead`
|
|
208658
|
+
});
|
|
208659
|
+
}
|
|
208660
|
+
}
|
|
208661
|
+
const floors = guardrailPreview(config.nodeMode, config.network);
|
|
208662
|
+
emit({
|
|
208663
|
+
type: "step-done",
|
|
208664
|
+
step: "preflight",
|
|
208665
|
+
detail: `NODE_MODE=${config.nodeMode} on ${config.network} needs at least ${floors.ramFloorMb}MB RAM and ${floors.diskFloorGb}GB disk.`
|
|
208666
|
+
});
|
|
208667
|
+
emit({ type: "step-start", step: "secrets" });
|
|
208668
|
+
const secrets = resolveSecrets({
|
|
208669
|
+
dir: config.dir,
|
|
208670
|
+
network: config.network,
|
|
208671
|
+
apiPort: config.apiPort,
|
|
208672
|
+
force: config.force,
|
|
208673
|
+
archivePublicKeyPem: await resolveArchivePublicKey({
|
|
208674
|
+
envPem: process.env.ARCHIVE_SIGNING_PUBLIC_KEY ?? process.env.STREAMS_SIGNING_PUBLIC_KEY,
|
|
208675
|
+
allowHostedApi: !isOssMode()
|
|
208676
|
+
})
|
|
208677
|
+
});
|
|
208678
|
+
emit({ type: "step-done", step: "secrets" });
|
|
208679
|
+
emit({ type: "step-start", step: "config" });
|
|
208680
|
+
const files = await writeSetupFiles(config, secrets);
|
|
208681
|
+
emit({
|
|
208682
|
+
type: "step-done",
|
|
208683
|
+
step: "config",
|
|
208684
|
+
detail: `${files.composePath}, ${files.envPath}`
|
|
208685
|
+
});
|
|
208686
|
+
emit({ type: "step-start", step: "docker-up" });
|
|
208687
|
+
const up = await dockerComposeUp(config, files, (line) => emit({ type: "step-log", step: "docker-up", line }));
|
|
208688
|
+
if (up.code !== 0) {
|
|
208689
|
+
emit({
|
|
208690
|
+
type: "step-error",
|
|
208691
|
+
step: "docker-up",
|
|
208692
|
+
message: `docker compose up failed (exit ${up.code}).
|
|
208693
|
+
${up.stderr || up.stdout}`
|
|
208694
|
+
});
|
|
208695
|
+
return { ok: false, config, secrets, files };
|
|
208696
|
+
}
|
|
208697
|
+
const healthy = await pollHealth(config, {
|
|
208698
|
+
onTick: () => emit({
|
|
208699
|
+
type: "step-log",
|
|
208700
|
+
step: "docker-up",
|
|
208701
|
+
line: "waiting for /health…"
|
|
208702
|
+
})
|
|
208703
|
+
});
|
|
208704
|
+
if (!healthy) {
|
|
208705
|
+
emit({
|
|
208706
|
+
type: "step-error",
|
|
208707
|
+
step: "docker-up",
|
|
208708
|
+
message: "Containers started but /health never came up. Check `docker compose logs`."
|
|
208709
|
+
});
|
|
208710
|
+
return { ok: false, config, secrets, files };
|
|
208711
|
+
}
|
|
208712
|
+
emit({ type: "step-done", step: "docker-up" });
|
|
208713
|
+
emit({ type: "step-start", step: "observer" });
|
|
208714
|
+
const observer = getObserverInfo(config);
|
|
208715
|
+
if (observer.applicable && observer.stanza) {
|
|
208716
|
+
emit({
|
|
208717
|
+
type: "step-log",
|
|
208718
|
+
step: "observer",
|
|
208719
|
+
line: "ACTION REQUIRED — paste this into your Stacks node's Config.toml:"
|
|
208720
|
+
});
|
|
208721
|
+
for (const line of observer.stanza.split(`
|
|
208722
|
+
`)) {
|
|
208723
|
+
if (line.length > 0)
|
|
208724
|
+
emit({ type: "step-log", step: "observer", line });
|
|
208725
|
+
}
|
|
208726
|
+
emit({
|
|
208727
|
+
type: "step-done",
|
|
208728
|
+
step: "observer",
|
|
208729
|
+
detail: `endpoint ${observer.endpoint} — see the ACTION REQUIRED lines above`
|
|
208730
|
+
});
|
|
208731
|
+
} else {
|
|
208732
|
+
emit({
|
|
208733
|
+
type: "step-done",
|
|
208734
|
+
step: "observer",
|
|
208735
|
+
detail: "bundled node — the observer is pre-configured, nothing to paste"
|
|
208736
|
+
});
|
|
208737
|
+
}
|
|
208738
|
+
let bootstrapped = false;
|
|
208739
|
+
if (config.skipBootstrap) {
|
|
208740
|
+
emit({ type: "step-skip", step: "bootstrap", reason: "--skip-bootstrap" });
|
|
208741
|
+
} else if (config.against) {
|
|
208742
|
+
emit({ type: "step-start", step: "bootstrap" });
|
|
208743
|
+
const databaseUrl = await preflightBootstrapDatabase(config, secrets, emit);
|
|
208744
|
+
if (databaseUrl === null) {
|
|
208745
|
+
return { ok: false, config, secrets, files, observer };
|
|
208746
|
+
}
|
|
208747
|
+
const res = await runBootstrap(config, config.against, files, (line) => emit({ type: "step-log", step: "bootstrap", line }));
|
|
208748
|
+
if (res.code === 0) {
|
|
208749
|
+
bootstrapped = true;
|
|
208750
|
+
emit({ type: "step-done", step: "bootstrap" });
|
|
208751
|
+
} else {
|
|
208752
|
+
emit({
|
|
208753
|
+
type: "step-error",
|
|
208754
|
+
step: "bootstrap",
|
|
208755
|
+
message: `secondlayer bootstrap exited ${res.code}.`
|
|
208756
|
+
});
|
|
208757
|
+
}
|
|
208758
|
+
}
|
|
208759
|
+
if (config.skipVerify) {
|
|
208760
|
+
emit({ type: "step-skip", step: "verify", reason: "--skip-verify" });
|
|
208761
|
+
} else if (!config.against) {
|
|
208762
|
+
emit({
|
|
208763
|
+
type: "step-skip",
|
|
208764
|
+
step: "verify",
|
|
208765
|
+
reason: "no manifest to verify against"
|
|
208766
|
+
});
|
|
208767
|
+
} else if (!bootstrapped) {
|
|
208768
|
+
emit({
|
|
208769
|
+
type: "step-skip",
|
|
208770
|
+
step: "verify",
|
|
208771
|
+
reason: "bootstrap did not run — nothing restored yet to verify"
|
|
208772
|
+
});
|
|
208773
|
+
} else {
|
|
208774
|
+
emit({ type: "step-start", step: "verify" });
|
|
208775
|
+
const res = await runVerify(config, config.against, files, (line) => emit({ type: "step-log", step: "verify", line }));
|
|
208776
|
+
if (res.code === 0) {
|
|
208777
|
+
emit({ type: "step-done", step: "verify" });
|
|
208778
|
+
} else {
|
|
208779
|
+
emit({
|
|
208780
|
+
type: "step-error",
|
|
208781
|
+
step: "verify",
|
|
208782
|
+
message: `secondlayer verify exited ${res.code}.`
|
|
208783
|
+
});
|
|
208784
|
+
}
|
|
208785
|
+
}
|
|
208786
|
+
const summary = buildSuccessSummary(config, secrets);
|
|
208787
|
+
emit({ type: "step-start", step: "summary" });
|
|
208788
|
+
emit({ type: "step-done", step: "summary", detail: summary });
|
|
208789
|
+
return { ok: true, config, secrets, files, observer, summary };
|
|
208790
|
+
}
|
|
208791
|
+
|
|
208792
|
+
// src/lib/setup-prompts.ts
|
|
209434
208793
|
class SetupCancelledError extends Error {
|
|
209435
208794
|
constructor() {
|
|
209436
208795
|
super("Setup cancelled — nothing was changed.");
|
|
@@ -209546,7 +208905,6 @@ async function promptSetupConfig(flags) {
|
|
|
209546
208905
|
}
|
|
209547
208906
|
|
|
209548
208907
|
// src/commands/setup.ts
|
|
209549
|
-
init_setup_wizard();
|
|
209550
208908
|
function renderProgress(event) {
|
|
209551
208909
|
switch (event.type) {
|
|
209552
208910
|
case "step-start":
|
|
@@ -209616,18 +208974,19 @@ async function runPromptedInteractive(flags) {
|
|
|
209616
208974
|
await executeAndRender(config);
|
|
209617
208975
|
}
|
|
209618
208976
|
function registerSetupCommand(program) {
|
|
209619
|
-
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 (
|
|
208977
|
+
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", `
|
|
209620
208978
|
--network <mainnet|testnet|devnet> is the top-level global flag (see
|
|
209621
208979
|
\`secondlayer --help\`) — required here too, just parsed one level up.
|
|
209622
208980
|
|
|
209623
208981
|
Examples:
|
|
209624
208982
|
$ secondlayer setup
|
|
209625
|
-
$ secondlayer setup --yes --network mainnet --node-mode external
|
|
208983
|
+
$ secondlayer setup --yes --network mainnet --node-mode external
|
|
209626
208984
|
$ secondlayer setup --yes --network testnet --node-mode external --skip-bootstrap --skip-verify
|
|
209627
208985
|
|
|
209628
|
-
Without a TTY (piped, CI, an agent), or with --yes, the
|
|
209629
|
-
decision with no safe default (--network, --node-mode
|
|
209630
|
-
|
|
208986
|
+
Without a TTY (piped, CI, an agent), or with --yes, the prompts are skipped:
|
|
208987
|
+
every decision with no safe default (--network, --node-mode) must come from a
|
|
208988
|
+
flag or the command fails fast naming it. --against defaults to the official
|
|
208989
|
+
archive (${DEFAULT_ARCHIVE_MANIFEST}) unless --skip-bootstrap is set.
|
|
209631
208990
|
`).action(async (opts) => {
|
|
209632
208991
|
assertInstanceUrl();
|
|
209633
208992
|
const flags = {
|
|
@@ -209639,17 +208998,7 @@ decision with no safe default (--network, --node-mode, and --against unless
|
|
|
209639
208998
|
await runNonInteractive(flags);
|
|
209640
208999
|
return;
|
|
209641
209000
|
}
|
|
209642
|
-
|
|
209643
|
-
await runPromptedInteractive(flags);
|
|
209644
|
-
return;
|
|
209645
|
-
}
|
|
209646
|
-
try {
|
|
209647
|
-
await Promise.resolve().then(() => init_setup_tui());
|
|
209648
|
-
await runSetupTui(flags);
|
|
209649
|
-
} catch (err) {
|
|
209650
|
-
warn(`Interactive TUI unavailable (${err instanceof Error ? err.message : String(err)}) — falling back to guided prompts.`);
|
|
209651
|
-
await runPromptedInteractive(flags);
|
|
209652
|
-
}
|
|
209001
|
+
await runPromptedInteractive(flags);
|
|
209653
209002
|
});
|
|
209654
209003
|
}
|
|
209655
209004
|
// src/cli.ts
|
|
@@ -209664,7 +209013,6 @@ program.hook("preAction", (thisCommand) => {
|
|
|
209664
209013
|
applyApiKeyFlag(apiKey);
|
|
209665
209014
|
if (apiUrl) {
|
|
209666
209015
|
process.env.SECONDLAYER_API_URL = apiUrl;
|
|
209667
|
-
process.env.SL_API_URL = apiUrl;
|
|
209668
209016
|
}
|
|
209669
209017
|
});
|
|
209670
209018
|
program.addHelpText("after", `
|
|
@@ -209710,5 +209058,5 @@ registerCreditsCommand(program);
|
|
|
209710
209058
|
registerArchiveCommand(program);
|
|
209711
209059
|
program.parse();
|
|
209712
209060
|
|
|
209713
|
-
//# debugId=
|
|
209061
|
+
//# debugId=51F0AC96395CA7BC64756E2164756E21
|
|
209714
209062
|
//# sourceMappingURL=cli.js.map
|