@secondlayer/cli 16.3.1 → 17.0.0

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