@secondlayer/cli 16.3.0 → 16.4.0

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