@secondlayer/cli 13.0.3 → 14.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 +22 -14
  2. package/dist/cli.js +1546 -749
  3. package/dist/cli.js.map +39 -38
  4. package/package.json +6 -6
package/dist/cli.js CHANGED
@@ -2481,6 +2481,15 @@ var require_commander = __commonJS((exports) => {
2481
2481
  exports.InvalidOptionArgumentError = InvalidArgumentError;
2482
2482
  });
2483
2483
 
2484
+ // src/lib/api-url.ts
2485
+ function resolveApiUrl() {
2486
+ return (process.env.SL_API_URL ?? process.env.SL_PLATFORM_API_URL ?? LOCAL_API_URL).replace(/\/+$/, "");
2487
+ }
2488
+ function resolveArchiveOpsUrl() {
2489
+ return (process.env.SL_CREDITS_API_URL ?? ARCHIVE_OPS_API_URL).replace(/\/+$/, "");
2490
+ }
2491
+ var LOCAL_API_URL = "http://127.0.0.1:3800", ARCHIVE_OPS_API_URL = "https://api.secondlayer.tools";
2492
+
2484
2493
  // src/lib/session.ts
2485
2494
  import {
2486
2495
  chmod,
@@ -2493,21 +2502,47 @@ import {
2493
2502
  import { homedir } from "node:os";
2494
2503
  import { dirname, join } from "node:path";
2495
2504
  import { z } from "zod";
2496
- async function readSession() {
2505
+ function getSessionPath() {
2506
+ return join(process.env.HOME || homedir(), ".secondlayer", "session.json");
2507
+ }
2508
+ function sessionKey(apiUrl) {
2509
+ return apiUrl.replace(/\/+$/, "");
2510
+ }
2511
+ function parseSessionFile(raw) {
2512
+ let parsed;
2513
+ try {
2514
+ parsed = JSON.parse(raw);
2515
+ } catch {
2516
+ return { sessions: {} };
2517
+ }
2518
+ const scoped = SessionFileSchema.safeParse(parsed);
2519
+ if (scoped.success)
2520
+ return scoped.data;
2521
+ const flat = SessionSchema.safeParse(parsed);
2522
+ if (flat.success) {
2523
+ return { sessions: { [sessionKey(ARCHIVE_OPS_API_URL)]: flat.data } };
2524
+ }
2525
+ return { sessions: {} };
2526
+ }
2527
+ async function readSessionFile(path2) {
2497
2528
  try {
2498
- const raw = await readFile(SESSION_PATH, "utf8");
2499
- return SessionSchema.parse(JSON.parse(raw));
2529
+ return parseSessionFile(await readFile(path2, "utf8"));
2500
2530
  } catch (err) {
2501
- if (err?.code === "ENOENT")
2502
- return null;
2503
- return null;
2531
+ if (err?.code === "ENOENT") {
2532
+ return { sessions: {} };
2533
+ }
2534
+ return { sessions: {} };
2504
2535
  }
2505
2536
  }
2506
- async function writeSession(session, path2 = SESSION_PATH) {
2537
+ async function readSession(apiUrl = resolveApiUrl(), path2 = getSessionPath()) {
2538
+ const file = await readSessionFile(path2);
2539
+ return file.sessions[sessionKey(apiUrl)] ?? null;
2540
+ }
2541
+ async function writeSessionFile(file, path2) {
2507
2542
  const dir = dirname(path2);
2508
2543
  await mkdir(dir, { recursive: true });
2509
2544
  const tmp = join(dir, `.session.${process.pid}.tmp`);
2510
- await writeFile(tmp, `${JSON.stringify(session, null, 2)}
2545
+ await writeFile(tmp, `${JSON.stringify(file, null, 2)}
2511
2546
  `, {
2512
2547
  mode: 384,
2513
2548
  encoding: "utf8"
@@ -2515,10 +2550,21 @@ async function writeSession(session, path2 = SESSION_PATH) {
2515
2550
  await chmod(tmp, 384);
2516
2551
  await rename(tmp, path2);
2517
2552
  }
2518
- async function clearSession() {
2519
- await rm(SESSION_PATH, { force: true });
2553
+ async function writeSession(session, apiUrl = resolveApiUrl(), path2 = getSessionPath()) {
2554
+ const file = await readSessionFile(path2);
2555
+ file.sessions[sessionKey(apiUrl)] = session;
2556
+ await writeSessionFile(file, path2);
2557
+ }
2558
+ async function clearSession(apiUrl = resolveApiUrl(), path2 = getSessionPath()) {
2559
+ const file = await readSessionFile(path2);
2560
+ delete file.sessions[sessionKey(apiUrl)];
2561
+ if (Object.keys(file.sessions).length === 0) {
2562
+ await rm(path2, { force: true });
2563
+ return;
2564
+ }
2565
+ await writeSessionFile(file, path2);
2520
2566
  }
2521
- var SessionSchema, SESSION_DIR, SESSION_PATH;
2567
+ var SessionSchema, SessionFileSchema;
2522
2568
  var init_session = __esm(() => {
2523
2569
  SessionSchema = z.object({
2524
2570
  token: z.string().min(1),
@@ -2526,15 +2572,13 @@ var init_session = __esm(() => {
2526
2572
  accountId: z.string().uuid(),
2527
2573
  expiresAt: z.string()
2528
2574
  });
2529
- SESSION_DIR = join(homedir(), ".secondlayer");
2530
- SESSION_PATH = join(SESSION_DIR, "session.json");
2575
+ SessionFileSchema = z.object({
2576
+ sessions: z.record(z.string(), SessionSchema)
2577
+ });
2531
2578
  });
2532
2579
 
2533
2580
  // src/lib/resolve-auth.ts
2534
2581
  import { resolveApiKey } from "@secondlayer/sdk";
2535
- function resolveApiUrl() {
2536
- return (process.env.SL_API_URL ?? process.env.SL_PLATFORM_API_URL ?? LOCAL_API_URL).replace(/\/+$/, "");
2537
- }
2538
2582
  function resolveEnvKey() {
2539
2583
  return resolveApiKey();
2540
2584
  }
@@ -2562,7 +2606,6 @@ function isOssMode() {
2562
2606
  return true;
2563
2607
  }
2564
2608
  }
2565
- var LOCAL_API_URL = "http://127.0.0.1:3800", ARCHIVE_OPS_API_URL = "https://api.secondlayer.tools";
2566
2609
  var init_resolve_auth = __esm(() => {
2567
2610
  init_session();
2568
2611
  });
@@ -2596,40 +2639,55 @@ async function request(url, opts) {
2596
2639
  return;
2597
2640
  return await res.json();
2598
2641
  }
2599
- async function httpPlatform(path2, opts = {}) {
2600
- let auth;
2601
- try {
2602
- auth = await resolveAuth();
2603
- } catch {
2604
- throw new CliHttpError(401, "SESSION_EXPIRED", { error: "Not logged in" }, isOssMode() ? "Not authenticated — set INSTANCE_TOKEN from `secondlayer init`" : "Not logged in — run `secondlayer login`");
2605
- }
2606
- return request(`${auth.apiUrl}${path2}`, {
2607
- ...opts,
2608
- bearer: auth.ephemeralKey
2609
- });
2642
+ async function httpAt(baseUrl, path2, opts = {}) {
2643
+ return request(`${baseUrl.replace(/\/+$/, "")}${path2}`, opts);
2610
2644
  }
2611
2645
  async function httpPlatformAnon(path2, opts = {}) {
2612
- return request(`${resolveApiUrl()}${path2}`, opts);
2613
- }
2614
- function resolveArchiveOpsUrl() {
2615
- return (process.env.SL_CREDITS_API_URL ?? ARCHIVE_OPS_API_URL).replace(/\/+$/, "");
2646
+ return httpAt(resolveApiUrl(), path2, opts);
2616
2647
  }
2617
2648
  async function httpArchiveOpsAnon(path2, opts = {}) {
2618
- return request(`${resolveArchiveOpsUrl()}${path2}`, opts);
2649
+ return httpAt(resolveArchiveOpsUrl(), path2, opts);
2619
2650
  }
2620
- async function httpArchiveOps(path2, opts = {}) {
2651
+ async function resolveArchiveOpsBearer() {
2621
2652
  const envKey = resolveEnvKey();
2622
- const session = envKey ? null : await readSession();
2623
- const bearer = envKey ?? session?.token;
2653
+ const session = await readSession(resolveArchiveOpsUrl());
2654
+ if (session) {
2655
+ return {
2656
+ bearer: session.token,
2657
+ source: "session",
2658
+ ignoredEnvKey: false
2659
+ };
2660
+ }
2661
+ if (envKey && ARCHIVE_CREDENTIAL_RE.test(envKey)) {
2662
+ return { bearer: envKey, source: "env", ignoredEnvKey: false };
2663
+ }
2664
+ return {
2665
+ bearer: undefined,
2666
+ source: null,
2667
+ ignoredEnvKey: envKey !== undefined
2668
+ };
2669
+ }
2670
+ function ignoredEnvKeyNote(ignored) {
2671
+ return ignored ? " INSTANCE_TOKEN was ignored: it unlocks this instance, not archive credits." : "";
2672
+ }
2673
+ async function httpArchiveOps(path2, opts = {}) {
2674
+ const { bearer, ignoredEnvKey } = await resolveArchiveOpsBearer();
2624
2675
  if (!bearer) {
2625
- throw new CliHttpError(401, "SESSION_EXPIRED", { error: "Not logged in" }, "Not logged in — run `SL_API_URL=https://api.secondlayer.tools secondlayer login`");
2676
+ throw new CliHttpError(401, "SESSION_EXPIRED", { error: "Not logged in" }, `Archive credits need a login. Run \`${ARCHIVE_LOGIN_COMMAND}\`.${ignoredEnvKeyNote(ignoredEnvKey)}`);
2677
+ }
2678
+ try {
2679
+ return await request(`${resolveArchiveOpsUrl()}${path2}`, {
2680
+ ...opts,
2681
+ bearer
2682
+ });
2683
+ } catch (err) {
2684
+ if (err instanceof CliHttpError && err.status === 401) {
2685
+ throw new CliHttpError(401, err.code, err.body, `Archive credits rejected the login (${err.message}). Run \`${ARCHIVE_LOGIN_COMMAND}\` again.${ignoredEnvKeyNote(ignoredEnvKey)}`);
2686
+ }
2687
+ throw err;
2626
2688
  }
2627
- return request(`${resolveArchiveOpsUrl()}${path2}`, {
2628
- ...opts,
2629
- bearer
2630
- });
2631
2689
  }
2632
- var CliHttpError, REQUEST_TIMEOUT_MS = 30000;
2690
+ var CliHttpError, REQUEST_TIMEOUT_MS = 30000, ARCHIVE_LOGIN_COMMAND = "secondlayer login --credits", ARCHIVE_CREDENTIAL_RE;
2633
2691
  var init_http = __esm(() => {
2634
2692
  init_resolve_auth();
2635
2693
  init_session();
@@ -2645,6 +2703,7 @@ var init_http = __esm(() => {
2645
2703
  this.body = body;
2646
2704
  }
2647
2705
  };
2706
+ ARCHIVE_CREDENTIAL_RE = /^s[ks]-sl_/;
2648
2707
  });
2649
2708
 
2650
2709
  // src/lib/output.ts
@@ -2748,6 +2807,25 @@ function formatKeyValue(pairs) {
2748
2807
  return pairs.map(([key, value]) => `${dim(key.padEnd(maxKeyLen))} ${value}`).join(`
2749
2808
  `);
2750
2809
  }
2810
+ async function confirmDestructive(opts) {
2811
+ if (opts.yes)
2812
+ return true;
2813
+ if (!process.stdin.isTTY) {
2814
+ error("Interactive prompt unavailable (stdin is not a TTY). Re-run with -y to skip confirmation.");
2815
+ process.exit(1);
2816
+ }
2817
+ const { confirm } = await import("@inquirer/prompts");
2818
+ try {
2819
+ return await confirm({ message: opts.message, default: false });
2820
+ } catch (promptErr) {
2821
+ const m = promptErr instanceof Error ? promptErr.message : String(promptErr);
2822
+ if (m.includes("ExitPromptError") || m.includes("force closed")) {
2823
+ error("Interactive prompt unavailable. Re-run with -y to skip confirmation.");
2824
+ process.exit(1);
2825
+ }
2826
+ throw promptErr;
2827
+ }
2828
+ }
2751
2829
  var colorEnabled, colors, ANSI_ESCAPE_PATTERN;
2752
2830
  var init_output = __esm(() => {
2753
2831
  colorEnabled = (() => {
@@ -28398,7 +28476,15 @@ var init_api_client = __esm(() => {
28398
28476
  });
28399
28477
 
28400
28478
  // src/lib/fs.ts
28401
- import { mkdir as mkdir2, readFile as readFile2, stat, unlink, writeFile as writeFile2 } from "node:fs/promises";
28479
+ import { createHash } from "node:crypto";
28480
+ import {
28481
+ mkdir as mkdir2,
28482
+ open,
28483
+ readFile as readFile2,
28484
+ stat,
28485
+ unlink,
28486
+ writeFile as writeFile2
28487
+ } from "node:fs/promises";
28402
28488
  async function readJsonFile(path11) {
28403
28489
  const text2 = await readFile2(path11, "utf-8");
28404
28490
  return JSON.parse(text2);
@@ -28420,6 +28506,24 @@ async function ensureDir(path11) {
28420
28506
  async function removeFile(path11) {
28421
28507
  await unlink(path11);
28422
28508
  }
28509
+ async function sha256File(path11) {
28510
+ const hash = createHash("sha256");
28511
+ const chunk = Buffer.allocUnsafe(1024 * 1024);
28512
+ const fh = await open(path11, "r");
28513
+ try {
28514
+ let position = 0;
28515
+ for (;; ) {
28516
+ const { bytesRead } = await fh.read(chunk, 0, chunk.length, position);
28517
+ if (bytesRead === 0)
28518
+ break;
28519
+ hash.update(chunk.subarray(0, bytesRead));
28520
+ position += bytesRead;
28521
+ }
28522
+ } finally {
28523
+ await fh.close();
28524
+ }
28525
+ return hash.digest("hex");
28526
+ }
28423
28527
  var init_fs = () => {};
28424
28528
 
28425
28529
  // src/lib/config.ts
@@ -28521,6 +28625,10 @@ function applyEnvOverrides(config) {
28521
28625
  const net2 = process.env.STACKS_NETWORK;
28522
28626
  if (net2 === "local" || net2 === "testnet" || net2 === "mainnet") {
28523
28627
  result.network = net2;
28628
+ } else if (net2 === "devnet") {
28629
+ result.network = "local";
28630
+ } else {
28631
+ console.error(`Warning: STACKS_NETWORK=${net2} is not mainnet, testnet, or devnet; using ${result.network}`);
28524
28632
  }
28525
28633
  }
28526
28634
  if (process.env.SL_DATA_DIR) {
@@ -28755,11 +28863,11 @@ function inspectSourceGitState(absPath) {
28755
28863
  if (tracked === null) {
28756
28864
  return { kind: "untracked" };
28757
28865
  }
28758
- const status = runGit(["status", "--porcelain", "--", absPath], cwd);
28759
- if (status === null) {
28866
+ const unstaged = runGit(["diff", "--name-only", "--", absPath], cwd);
28867
+ if (unstaged === null) {
28760
28868
  return { kind: "not-a-repo" };
28761
28869
  }
28762
- if (status.length > 0) {
28870
+ if (unstaged.length > 0) {
28763
28871
  return { kind: "modified" };
28764
28872
  }
28765
28873
  return { kind: "clean" };
@@ -28777,19 +28885,21 @@ async function readStdin() {
28777
28885
  chunks.push(chunk);
28778
28886
  return Buffer.concat(chunks).toString("utf8").trim();
28779
28887
  }
28780
- async function runTokenLogin() {
28888
+ function loginTarget(opts) {
28889
+ return opts.credits ? resolveArchiveOpsUrl() : resolveApiUrl();
28890
+ }
28891
+ async function runTokenLogin(opts) {
28781
28892
  const token = await readStdin();
28782
28893
  if (!token) {
28783
28894
  error('No token on stdin. Usage: echo "$KEY" | secondlayer login --with-token');
28784
28895
  process.exit(1);
28785
28896
  }
28786
- process.env.INSTANCE_TOKEN = token;
28787
- process.env.SL_API_KEY = token;
28897
+ const target = loginTarget(opts);
28788
28898
  let account;
28789
28899
  try {
28790
- account = await httpPlatform("/api/accounts/me");
28900
+ account = await httpAt(target, "/api/accounts/me", { bearer: token });
28791
28901
  } catch (err) {
28792
- error(err instanceof CliHttpError ? `Token rejected: ${err.message}` : err instanceof Error ? err.message : String(err));
28902
+ error(err instanceof CliHttpError ? `Token rejected by ${target}: ${err.message}` : err instanceof Error ? err.message : String(err));
28793
28903
  process.exit(1);
28794
28904
  }
28795
28905
  await writeSession({
@@ -28797,14 +28907,15 @@ async function runTokenLogin() {
28797
28907
  email: account.email,
28798
28908
  accountId: account.id,
28799
28909
  expiresAt: sessionExpiry()
28800
- });
28801
- success(`Logged in as ${account.email}`);
28910
+ }, target);
28911
+ success(`Logged in as ${account.email} (${target})`);
28802
28912
  }
28803
28913
  async function runLoginFlow(options3 = {}) {
28914
+ const target = loginTarget(options3);
28804
28915
  if (!options3.force) {
28805
- const existing = await readSession();
28916
+ const existing = await readSession(target);
28806
28917
  if (existing) {
28807
- info(`Already logged in as ${existing.email}.`);
28918
+ info(`Already logged in as ${existing.email} (${target}).`);
28808
28919
  if (!process.stdin.isTTY) {
28809
28920
  info(dim("Run 'secondlayer logout' first, or re-run with --force to switch accounts."));
28810
28921
  return;
@@ -28829,7 +28940,7 @@ async function runLoginFlow(options3 = {}) {
28829
28940
  validate: (v2) => /^.+@.+\..+$/.test(v2) ? true : "Invalid email"
28830
28941
  });
28831
28942
  try {
28832
- const res = await httpPlatformAnon("/api/auth/magic-link", {
28943
+ const res = await httpAt(target, "/api/auth/magic-link", {
28833
28944
  method: "POST",
28834
28945
  body: { email }
28835
28946
  });
@@ -28850,7 +28961,7 @@ async function runLoginFlow(options3 = {}) {
28850
28961
  validate: (v2) => /^\d{6}$/.test(v2) ? true : "Expected 6 digits"
28851
28962
  });
28852
28963
  try {
28853
- const verified = await httpPlatformAnon("/api/auth/verify", {
28964
+ const verified = await httpAt(target, "/api/auth/verify", {
28854
28965
  method: "POST",
28855
28966
  body: { email, code }
28856
28967
  });
@@ -28859,9 +28970,9 @@ async function runLoginFlow(options3 = {}) {
28859
28970
  email: verified.account.email,
28860
28971
  accountId: verified.account.id,
28861
28972
  expiresAt: sessionExpiry()
28862
- });
28973
+ }, target);
28863
28974
  success(`Logged in as ${verified.account.email}`);
28864
- info(dim("Run 'secondlayer whoami' to see your account status."));
28975
+ info(dim(options3.credits ? "Run 'secondlayer credits balance' to see your archive credits." : "Run 'secondlayer whoami' to see your account status."));
28865
28976
  } catch (err) {
28866
28977
  if (err instanceof CliHttpError) {
28867
28978
  error(err.message);
@@ -28872,10 +28983,10 @@ async function runLoginFlow(options3 = {}) {
28872
28983
  }
28873
28984
  }
28874
28985
  function registerLoginCommand(program2) {
28875
- program2.command("login", { hidden: true }).description("Log in to Secondlayer (magic-link email)").option("--force", "Skip the already-logged-in check and re-run the flow").option("--with-token", "Read an API key from stdin (non-interactive)").addHelpText("after", `
28986
+ program2.command("login", { hidden: true }).description("Log in to Secondlayer (magic-link email)").option("--force", "Skip the already-logged-in check and re-run the flow").option("--with-token", "Read an API key from stdin (non-interactive)").option("--credits", "Log in to the archive credits account used by bootstrap, repair, and credits").addHelpText("after", `
28876
28987
  Examples:
28877
- $ secondlayer login
28878
- $ echo "$INSTANCE_TOKEN" | secondlayer login --with-token`).action((opts) => opts.withToken ? runTokenLogin() : runLoginFlow({ force: opts.force }));
28988
+ $ secondlayer login --credits
28989
+ $ echo "$INSTANCE_TOKEN" | secondlayer login --with-token`).action((opts) => opts.withToken ? runTokenLogin({ credits: opts.credits }) : runLoginFlow({ force: opts.force, credits: opts.credits }));
28879
28990
  }
28880
28991
  var init_login = __esm(() => {
28881
28992
  init_http();
@@ -28986,7 +29097,7 @@ __export(exports_subgraph_test, {
28986
29097
  filterHashOf: () => filterHashOf,
28987
29098
  DECODER_VERSION: () => DECODER_VERSION
28988
29099
  });
28989
- import { createHash } from "node:crypto";
29100
+ import { createHash as createHash2 } from "node:crypto";
28990
29101
  import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
28991
29102
  import { dirname as dirname4, resolve as resolve2 } from "node:path";
28992
29103
  import { Index, resolveApiKey as resolveApiKey2, resolveBaseUrl } from "@secondlayer/sdk";
@@ -28995,7 +29106,7 @@ import {
28995
29106
  } from "@secondlayer/stacks/filters";
28996
29107
  function filterHashOf(sources) {
28997
29108
  const stable = JSON.stringify(Object.fromEntries(Object.entries(sources).sort(([a4], [b2]) => a4.localeCompare(b2))), (_k, v2) => typeof v2 === "bigint" ? `${v2}n` : v2);
28998
- return createHash("sha256").update(stable).digest("hex").slice(0, 16);
29109
+ return createHash2("sha256").update(stable).digest("hex").slice(0, 16);
28999
29110
  }
29000
29111
  function cassettePath(name, dir = CASSETTE_DIR) {
29001
29112
  return resolve2(dir, `${name}.json`);
@@ -29772,7 +29883,8 @@ Check the contract id, and that the contract has emitted print events (schemas a
29772
29883
  if (opts.fromContract) {
29773
29884
  info(`Schema inferred from ${opts.fromContract} print events (${opts.tablePerTopic ? "table per topic" : "single wide table"})`);
29774
29885
  }
29775
- info(`Next: secondlayer subgraphs deploy subgraphs/${name}.ts`);
29886
+ info(`Next: git add subgraphs/${name}.ts && secondlayer subgraphs deploy subgraphs/${name}.ts`);
29887
+ note(" deploy refuses an unstaged file unless you pass --allow-uncommitted: a staged copy is the one git can recover.");
29776
29888
  });
29777
29889
  subgraphs.command("dev <file>").description("Watch a subgraph file and auto-redeploy on change").action(async (file) => {
29778
29890
  await requireLocalNetwork();
@@ -29835,9 +29947,9 @@ Stopped watching.`);
29835
29947
  });
29836
29948
  await new Promise(() => {});
29837
29949
  });
29838
- subgraphs.command("deploy <file>").alias("update").description("Deploy a subgraph definition file (alias: update — deploy is create-or-update)").option("--start-block <n>", "Override the subgraph definition startBlock for this deploy").option("--tip-first", "Go live at chain tip immediately; history backfills in the background (requires order-tolerant handlers — commutative or insert-only writes)").option("--dry-run", "Validate and preview deploy without writing changes").option("-y, --yes", "Skip the reindex confirmation prompt").option("--strict", "Run `tsc --noEmit` against the handler before deploy (slower; catches TS type errors)").option("--allow-uncommitted", "Deploy even though the source file is not committed to git (the deployed definition will exist only in the database)").addHelpText("after", `
29839
- By default the source file must be committed to git — otherwise the
29840
- deployed definition's only copy is the row in the database. Pass
29950
+ subgraphs.command("deploy <file>").alias("update").description("Deploy a subgraph definition file (alias: update — deploy is create-or-update)").option("--start-block <n>", "Override the subgraph definition startBlock for this deploy").option("--tip-first", "Go live at chain tip immediately; history backfills in the background (requires order-tolerant handlers — commutative or insert-only writes)").option("--dry-run", "Validate and preview deploy without writing changes").option("-y, --yes", "Skip the reindex confirmation prompt").option("--strict", "Run `tsc --noEmit` against the handler before deploy (slower; catches TS type errors)").option("--allow-uncommitted", "Deploy even though the source file is not staged or committed in git (the deployed definition will exist only in the database)").addHelpText("after", `
29951
+ By default the source file must be staged or committed in git (\`git add <file>\`),
29952
+ otherwise the deployed definition's only copy is the row in the database. Pass
29841
29953
  --allow-uncommitted to deploy anyway.`).action(async (file, options3) => {
29842
29954
  try {
29843
29955
  const absPath = resolve3(file);
@@ -29848,18 +29960,19 @@ deployed definition's only copy is the row in the database. Pass
29848
29960
  const gitState = inspectSourceGitState(absPath);
29849
29961
  if (gitState.kind === "untracked" || gitState.kind === "modified") {
29850
29962
  const reason = gitState.kind === "untracked" ? "not tracked by git" : "modified with uncommitted changes";
29851
- const message = `${file} is ${reason}. If you deploy it, the only copy of this definition will be a row in the database — this has already caused two production recoveries.`;
29852
- if (process.stdout.isTTY) {
29963
+ const message = `${file} is ${reason}. Deploy it and the only copy of this definition is a row in the database.`;
29964
+ if (process.stdin.isTTY) {
29853
29965
  warn(message);
29854
29966
  const confirmed = await confirm3({
29855
- message: "Deploy anyway?"
29967
+ message: "Deploy anyway?",
29968
+ default: false
29856
29969
  });
29857
29970
  if (!confirmed) {
29858
- info("Aborted.");
29971
+ info("Nothing was deployed.");
29859
29972
  process.exit(0);
29860
29973
  }
29861
29974
  } else {
29862
- error(`${message} Commit it, or pass --allow-uncommitted.`);
29975
+ error(`${message} Stage it with \`git add ${file}\`, or pass --allow-uncommitted.`);
29863
29976
  process.exit(1);
29864
29977
  }
29865
29978
  }
@@ -29924,11 +30037,12 @@ deployed definition's only copy is the row in the database. Pass
29924
30037
  } else {
29925
30038
  warn(`startBlock change (${existingStart ?? "unset"} → ${deployStartBlock}) forces a reindex.`);
29926
30039
  }
29927
- const confirmed = await confirm3({
29928
- message: "⚠ This will drop all data and reindex from scratch. Continue?"
30040
+ const confirmed = await confirmDestructive({
30041
+ message: "This will drop all data and reindex from scratch. Continue?",
30042
+ yes: options3.yes
29929
30043
  });
29930
30044
  if (!confirmed) {
29931
- info("Aborted — nothing was deployed.");
30045
+ info("Nothing was deployed.");
29932
30046
  process.exit(0);
29933
30047
  }
29934
30048
  }
@@ -30063,8 +30177,10 @@ ${data.length} subgraph(s) total`));
30063
30177
  }
30064
30178
  });
30065
30179
  subgraphs.command("status <name>").alias("get").description("Show detailed subgraph status").option("-w, --watch", "Refresh every 2s until synced or Ctrl-C").action(async (name, options3) => {
30066
- const renderOnce = async () => {
30180
+ const renderOnce = async (opts = {}) => {
30067
30181
  const subgraph = await getSubgraphApi(name);
30182
+ if (opts.clearScreen)
30183
+ process.stdout.write("\x1Bc");
30068
30184
  const rowCounts = Object.entries(subgraph.tables).map(([t2, info2]) => `${t2}: ${info2.rowCount}`).join(", ") || "N/A";
30069
30185
  const totalRows = Object.values(subgraph.tables).reduce((sum, info2) => sum + info2.rowCount, 0);
30070
30186
  const errorRate = subgraph.health.totalProcessed > 0 ? `${(subgraph.health.errorRate * 100).toFixed(2)}%` : "N/A";
@@ -30113,12 +30229,23 @@ Table endpoints:`));
30113
30229
  await renderOnce();
30114
30230
  return;
30115
30231
  }
30232
+ let consecutiveFailures = 0;
30116
30233
  while (true) {
30117
- process.stdout.write("\x1Bc");
30118
- const sg = await renderOnce();
30119
- if (sg && sg.status === "synced") {
30234
+ let sg;
30235
+ try {
30236
+ sg = await renderOnce({ clearScreen: true });
30237
+ consecutiveFailures = 0;
30238
+ } catch (err) {
30239
+ consecutiveFailures += 1;
30240
+ if (consecutiveFailures > WATCH_TRANSIENT_FAILURE_LIMIT)
30241
+ throw err;
30242
+ console.log(dim(`poll failed (${consecutiveFailures}/${WATCH_TRANSIENT_FAILURE_LIMIT}): ${err instanceof Error ? err.message : String(err)}; retrying`));
30243
+ await new Promise((res) => setTimeout(res, 2000));
30244
+ continue;
30245
+ }
30246
+ if (sg?.sync?.status === "synced") {
30120
30247
  console.log(dim(`
30121
- Synced — exiting watch.`));
30248
+ Synced, exiting watch.`));
30122
30249
  return;
30123
30250
  }
30124
30251
  await new Promise((res) => setTimeout(res, 2000));
@@ -30167,30 +30294,13 @@ dropping anything, use "secondlayer subgraphs backfill".
30167
30294
  Examples:
30168
30295
  $ secondlayer subgraphs reindex my-graph -y`).action(async (name, options3) => {
30169
30296
  try {
30170
- if (!options3.yes) {
30171
- if (!process.stdin.isTTY) {
30172
- error("Interactive prompt unavailable (stdin is not a TTY). Re-run with -y to skip confirmation.");
30173
- process.exit(1);
30174
- }
30175
- const { confirm: confirm4 } = await import("@inquirer/prompts");
30176
- let ok = false;
30177
- try {
30178
- ok = await confirm4({
30179
- message: `Reindex subgraph "${name}"? ALL of its data will be dropped and rebuilt from its start block. This cannot be undone.`,
30180
- default: false
30181
- });
30182
- } catch (promptErr) {
30183
- const m2 = promptErr instanceof Error ? promptErr.message : String(promptErr);
30184
- if (m2.includes("ExitPromptError") || m2.includes("force closed")) {
30185
- error("Interactive prompt unavailable. Re-run with -y to skip confirmation.");
30186
- process.exit(1);
30187
- }
30188
- throw promptErr;
30189
- }
30190
- if (!ok) {
30191
- info("Cancelled.");
30192
- return;
30193
- }
30297
+ const ok = await confirmDestructive({
30298
+ message: `Reindex subgraph "${name}"? ALL of its data will be dropped and rebuilt from its start block. This cannot be undone.`,
30299
+ yes: options3.yes
30300
+ });
30301
+ if (!ok) {
30302
+ info("Cancelled.");
30303
+ return;
30194
30304
  }
30195
30305
  info(`Reindexing subgraph "${name}"...`);
30196
30306
  const result = await reindexSubgraphApi(name);
@@ -30412,29 +30522,13 @@ ${rows.length} row(s)`));
30412
30522
  });
30413
30523
  subgraphs.command("delete <name>").alias("rm").description("Delete a subgraph and its data").option("-y, --yes", "Skip confirmation").option("--force", "Cancel active operations and force delete").action(async (name, options3) => {
30414
30524
  try {
30415
- if (!options3.yes && !options3.force) {
30416
- if (!process.stdin.isTTY) {
30417
- error("Interactive prompt unavailable (stdin is not a TTY). Re-run with -y to skip confirmation.");
30418
- process.exit(1);
30419
- }
30420
- const { confirm: confirm4 } = await import("@inquirer/prompts");
30421
- let ok = false;
30422
- try {
30423
- ok = await confirm4({
30424
- message: `Delete subgraph "${name}" and all its data? This cannot be undone.`
30425
- });
30426
- } catch (promptErr) {
30427
- const m2 = promptErr instanceof Error ? promptErr.message : String(promptErr);
30428
- if (m2.includes("ExitPromptError") || m2.includes("force closed")) {
30429
- error("Interactive prompt unavailable. Re-run with -y to skip confirmation.");
30430
- process.exit(1);
30431
- }
30432
- throw promptErr;
30433
- }
30434
- if (!ok) {
30435
- info("Cancelled");
30436
- return;
30437
- }
30525
+ const ok = await confirmDestructive({
30526
+ message: `Delete subgraph "${name}" and all its data? This cannot be undone.`,
30527
+ yes: options3.yes || options3.force
30528
+ });
30529
+ if (!ok) {
30530
+ info("Cancelled");
30531
+ return;
30438
30532
  }
30439
30533
  try {
30440
30534
  const result = await deleteSubgraphApi(name, {
@@ -30454,7 +30548,7 @@ ${rows.length} row(s)`));
30454
30548
  handleApiError(err, "delete subgraph");
30455
30549
  }
30456
30550
  });
30457
- subgraphs.command("scaffold [contractAddress]").description("Generate a defineSubgraph() file from a contract ABI or trait (start with: subgraphs create)").option("-o, --output <path>", "Output file path (required)").option("-k, --api-key <key>", "Stacks node API key for direct RPC URLs").option("--functions <names>", "Comma-separated public functions to index as typed contract_call tables").option("--trait <std>", "Scaffold a trait-scoped source (sip-009|sip-010|sip-013) — no contract needed").option("--no-install", "Skip bun install after writing package.json").addHelpText("after", `
30551
+ subgraphs.command("scaffold [contractAddress]").description("Generate a defineSubgraph() file from a contract ABI or trait (start with: subgraphs create)").option("-o, --output <path>", "Output file path (required)").option("--functions <names>", "Comma-separated public functions to index as typed contract_call tables").option("--trait <std>", "Scaffold a trait-scoped source (sip-009|sip-010|sip-013) — no contract needed").option("--no-install", "Skip bun install after writing package.json").addHelpText("after", `
30458
30552
  "secondlayer subgraphs create <name>" is the documented starting point. Reach
30459
30553
  for scaffold when you need what create cannot infer: typed contract_call tables
30460
30554
  from the contract's ABI, or a trait-wide source (--trait sip-010). scaffold
@@ -30488,8 +30582,7 @@ Examples:
30488
30582
  } else {
30489
30583
  const address = contractAddress;
30490
30584
  const network = inferNetwork(address) ?? "mainnet";
30491
- const apiKey = options3.apiKey ?? process.env.STACKS_NODE_API_KEY;
30492
- const client = new StacksApiClient(network, apiKey);
30585
+ const client = new StacksApiClient(network);
30493
30586
  info(`Fetching ABI for ${address} via ${client.describeContractInfoSource()}...`);
30494
30587
  const contractInfo = await client.getContractInfo(address);
30495
30588
  const abi = parseApiResponse(contractInfo);
@@ -30601,7 +30694,7 @@ async function runSubgraphSchemaCodegen(file, options3, optionSource, defaultTar
30601
30694
  process.exit(1);
30602
30695
  }
30603
30696
  }
30604
- var ORM_ONLY_CODEGEN_FLAGS;
30697
+ var WATCH_TRANSIENT_FAILURE_LIMIT = 3, ORM_ONLY_CODEGEN_FLAGS;
30605
30698
  var init_subgraphs2 = __esm(() => {
30606
30699
  init_subgraph_scaffold();
30607
30700
  init_subgraphs();
@@ -32468,7 +32561,7 @@ import fs6 from "fs";
32468
32561
  function Postgres(a4, b3) {
32469
32562
  const options3 = parseOptions(a4, b3), subscribe = options3.no_subscribe || Subscribe(Postgres, { ...options3 });
32470
32563
  let ending = false;
32471
- const queries = queue_default(), connecting = queue_default(), reserved = queue_default(), closed = queue_default(), ended = queue_default(), open = queue_default(), busy = queue_default(), full = queue_default(), queues = { connecting, reserved, closed, ended, open, busy, full };
32564
+ const queries = queue_default(), connecting = queue_default(), reserved = queue_default(), closed = queue_default(), ended = queue_default(), open2 = queue_default(), busy = queue_default(), full = queue_default(), queues = { connecting, reserved, closed, ended, open: open2, busy, full };
32472
32565
  const connections = [...Array(options3.max)].map(() => connection_default(options3, queues, { onopen, onend, onclose }));
32473
32566
  const sql = Sql(handler);
32474
32567
  Object.assign(sql, {
@@ -32580,7 +32673,7 @@ function Postgres(a4, b3) {
32580
32673
  }
32581
32674
  async function reserve() {
32582
32675
  const queue = queue_default();
32583
- const c4 = open.length ? open.shift() : await new Promise((resolve5, reject) => {
32676
+ const c4 = open2.length ? open2.shift() : await new Promise((resolve5, reject) => {
32584
32677
  const query = { reserve: resolve5, reject };
32585
32678
  queries.push(query);
32586
32679
  closed.length && connect(closed.shift(), query);
@@ -32653,7 +32746,7 @@ function Postgres(a4, b3) {
32653
32746
  c4.queue.remove(c4);
32654
32747
  queue.push(c4);
32655
32748
  c4.queue = queue;
32656
- queue === open ? c4.idleTimer.start() : c4.idleTimer.cancel();
32749
+ queue === open2 ? c4.idleTimer.start() : c4.idleTimer.cancel();
32657
32750
  return c4;
32658
32751
  }
32659
32752
  function json(x2) {
@@ -32667,8 +32760,8 @@ function Postgres(a4, b3) {
32667
32760
  function handler(query) {
32668
32761
  if (ending)
32669
32762
  return query.reject(Errors.connection("CONNECTION_ENDED", options3, options3));
32670
- if (open.length)
32671
- return go(open.shift(), query);
32763
+ if (open2.length)
32764
+ return go(open2.shift(), query);
32672
32765
  if (closed.length)
32673
32766
  return connect(closed.shift(), query);
32674
32767
  busy.length ? go(busy.shift(), query) : queries.push(query);
@@ -32710,7 +32803,7 @@ function Postgres(a4, b3) {
32710
32803
  }
32711
32804
  function onopen(c4) {
32712
32805
  if (queries.length === 0)
32713
- return move(c4, open);
32806
+ return move(c4, open2);
32714
32807
  let max = Math.ceil(queries.length / (connecting.length + 1)), ready = true;
32715
32808
  while (ready && queries.length && max-- > 0) {
32716
32809
  const query = queries.shift();
@@ -33032,12 +33125,12 @@ __export(exports_dev_state, {
33032
33125
  clearDevState: () => clearDevState
33033
33126
  });
33034
33127
  import { homedir as homedir4 } from "node:os";
33035
- import { join as join11 } from "node:path";
33128
+ import { join as join10 } from "node:path";
33036
33129
  function getLogsDir() {
33037
33130
  return LOGS_DIR;
33038
33131
  }
33039
33132
  function getLogFile(service) {
33040
- return join11(LOGS_DIR, `${service}.log`);
33133
+ return join10(LOGS_DIR, `${service}.log`);
33041
33134
  }
33042
33135
  async function ensureDirs() {
33043
33136
  await Bun.$`mkdir -p ${STATE_DIR}`.quiet();
@@ -33094,9 +33187,9 @@ async function isDevRunning() {
33094
33187
  }
33095
33188
  var STATE_DIR, DEV_STATE_PATH, LOGS_DIR;
33096
33189
  var init_dev_state = __esm(() => {
33097
- STATE_DIR = join11(homedir4(), ".secondlayer");
33098
- DEV_STATE_PATH = join11(STATE_DIR, "dev.json");
33099
- LOGS_DIR = join11(STATE_DIR, "logs");
33190
+ STATE_DIR = join10(homedir4(), ".secondlayer");
33191
+ DEV_STATE_PATH = join10(STATE_DIR, "dev.json");
33192
+ LOGS_DIR = join10(STATE_DIR, "logs");
33100
33193
  });
33101
33194
 
33102
33195
  // src/lib/network.ts
@@ -33487,11 +33580,11 @@ var init_manager = __esm(() => {
33487
33580
  });
33488
33581
 
33489
33582
  // src/services/indexer.ts
33490
- import { dirname as dirname10, resolve as resolve6 } from "node:path";
33583
+ import { dirname as dirname10, resolve as resolve7 } from "node:path";
33491
33584
  async function startIndexer(options3) {
33492
33585
  const port = options3.port ?? 3700;
33493
33586
  const rootDir = dirname10(dirname10(dirname10(dirname10(import.meta.dir))));
33494
- const indexerPath = resolve6(rootDir, "packages/indexer/src/index.ts");
33587
+ const indexerPath = resolve7(rootDir, "packages/indexer/src/index.ts");
33495
33588
  await serviceManager.start(SERVICE_NAME, ["bun", "run", "--watch", indexerPath], {
33496
33589
  port,
33497
33590
  env: {
@@ -33507,10 +33600,10 @@ var init_indexer = __esm(() => {
33507
33600
  });
33508
33601
 
33509
33602
  // src/services/worker.ts
33510
- import { dirname as dirname11, resolve as resolve7 } from "node:path";
33603
+ import { dirname as dirname11, resolve as resolve8 } from "node:path";
33511
33604
  async function startWorker(options3) {
33512
33605
  const rootDir = dirname11(dirname11(dirname11(dirname11(import.meta.dir))));
33513
- const workerPath = resolve7(rootDir, "packages/worker/src/index.ts");
33606
+ const workerPath = resolve8(rootDir, "packages/worker/src/index.ts");
33514
33607
  await serviceManager.start(SERVICE_NAME2, ["bun", "run", "--watch", workerPath], {
33515
33608
  onStdout: options3.onLog,
33516
33609
  onStderr: options3.onLog
@@ -33522,11 +33615,11 @@ var init_worker = __esm(() => {
33522
33615
  });
33523
33616
 
33524
33617
  // src/services/api.ts
33525
- import { dirname as dirname12, resolve as resolve8 } from "node:path";
33618
+ import { dirname as dirname12, resolve as resolve9 } from "node:path";
33526
33619
  async function startApi(options3) {
33527
33620
  const port = options3.port ?? 3800;
33528
33621
  const rootDir = dirname12(dirname12(dirname12(dirname12(import.meta.dir))));
33529
- const apiPath = resolve8(rootDir, "packages/api/src/index.ts");
33622
+ const apiPath = resolve9(rootDir, "packages/api/src/index.ts");
33530
33623
  await serviceManager.start(SERVICE_NAME3, ["bun", "run", "--watch", apiPath], {
33531
33624
  port,
33532
33625
  env: {
@@ -33542,10 +33635,10 @@ var init_api2 = __esm(() => {
33542
33635
  });
33543
33636
 
33544
33637
  // src/services/subgraph-processor.ts
33545
- import { dirname as dirname13, resolve as resolve9 } from "node:path";
33638
+ import { dirname as dirname13, resolve as resolve10 } from "node:path";
33546
33639
  async function startSubgraphProcessor(options3) {
33547
33640
  const rootDir = dirname13(dirname13(dirname13(dirname13(import.meta.dir))));
33548
- const servicePath = resolve9(rootDir, "packages/subgraphs/src/service.ts");
33641
+ const servicePath = resolve10(rootDir, "packages/subgraphs/src/service.ts");
33549
33642
  await serviceManager.start(SERVICE_NAME4, ["bun", "run", "--watch", servicePath], {
33550
33643
  env: {
33551
33644
  SUBGRAPH_CONCURRENCY: String(options3.concurrency ?? 5)
@@ -33580,7 +33673,7 @@ __export(exports_dev_impl, {
33580
33673
  isDevAlreadyRunning: () => isDevAlreadyRunning
33581
33674
  });
33582
33675
  import { mkdirSync as mkdirSync5 } from "node:fs";
33583
- import { dirname as dirname14, join as join12, resolve as resolve10 } from "node:path";
33676
+ import { dirname as dirname14, join as join11, resolve as resolve11 } from "node:path";
33584
33677
  async function isDevAlreadyRunning() {
33585
33678
  if (await isDevRunning()) {
33586
33679
  const running = await getRunningServices();
@@ -33661,7 +33754,7 @@ async function runBackground(options3) {
33661
33754
  const packagesDir = dirname14(dirname14(dirname14(dirname14(import.meta.dir))));
33662
33755
  const env2 = { DATABASE_URL: databaseUrl, DEV_MODE: "true" };
33663
33756
  const apiLogFile = getLogFile("api");
33664
- const apiProc = Bun.spawn(["bun", "run", resolve10(packagesDir, "packages/api/src/index.ts")], {
33757
+ const apiProc = Bun.spawn(["bun", "run", resolve11(packagesDir, "packages/api/src/index.ts")], {
33665
33758
  env: { ...process.env, ...env2, PORT: String(apiPort) },
33666
33759
  stdout: Bun.file(apiLogFile),
33667
33760
  stderr: Bun.file(apiLogFile)
@@ -33674,7 +33767,7 @@ async function runBackground(options3) {
33674
33767
  };
33675
33768
  console.log(green(" ✓ API"), dim(`http://localhost:${apiPort}`));
33676
33769
  const indexerLogFile = getLogFile("indexer");
33677
- const indexerProc = Bun.spawn(["bun", "run", resolve10(packagesDir, "packages/indexer/src/index.ts")], {
33770
+ const indexerProc = Bun.spawn(["bun", "run", resolve11(packagesDir, "packages/indexer/src/index.ts")], {
33678
33771
  env: { ...process.env, ...env2, PORT: String(indexerPort) },
33679
33772
  stdout: Bun.file(indexerLogFile),
33680
33773
  stderr: Bun.file(indexerLogFile)
@@ -33688,7 +33781,7 @@ async function runBackground(options3) {
33688
33781
  console.log(green(" ✓ Indexer"), dim(`http://localhost:${indexerPort}`));
33689
33782
  if (options3.worker) {
33690
33783
  const workerLogFile = getLogFile("worker");
33691
- const workerProc = Bun.spawn(["bun", "run", resolve10(packagesDir, "packages/worker/src/index.ts")], {
33784
+ const workerProc = Bun.spawn(["bun", "run", resolve11(packagesDir, "packages/worker/src/index.ts")], {
33692
33785
  env: { ...process.env, ...env2 },
33693
33786
  stdout: Bun.file(workerLogFile),
33694
33787
  stderr: Bun.file(workerLogFile)
@@ -33706,7 +33799,7 @@ async function runBackground(options3) {
33706
33799
  const subgraphsProc = Bun.spawn([
33707
33800
  "bun",
33708
33801
  "run",
33709
- resolve10(packagesDir, "packages/subgraphs/src/service.ts")
33802
+ resolve11(packagesDir, "packages/subgraphs/src/service.ts")
33710
33803
  ], {
33711
33804
  env: { ...process.env, ...env2 },
33712
33805
  stdout: Bun.file(subgraphsLogFile),
@@ -34018,7 +34111,7 @@ async function restartDev() {
34018
34111
  };
34019
34112
  const apiPort = config.ports.api;
34020
34113
  const apiLogFile = getLogFile("api");
34021
- const apiProc = Bun.spawn(["bun", "run", resolve10(packagesDir, "packages/api/src/index.ts")], {
34114
+ const apiProc = Bun.spawn(["bun", "run", resolve11(packagesDir, "packages/api/src/index.ts")], {
34022
34115
  env: { ...process.env, ...env2, PORT: String(apiPort) },
34023
34116
  stdout: Bun.file(apiLogFile),
34024
34117
  stderr: Bun.file(apiLogFile)
@@ -34032,7 +34125,7 @@ async function restartDev() {
34032
34125
  console.log(green(" ✓ API"), dim(`http://localhost:${apiPort}`));
34033
34126
  const indexerPort = config.ports.indexer;
34034
34127
  const indexerLogFile = getLogFile("indexer");
34035
- const indexerProc = Bun.spawn(["bun", "run", resolve10(packagesDir, "packages/indexer/src/index.ts")], {
34128
+ const indexerProc = Bun.spawn(["bun", "run", resolve11(packagesDir, "packages/indexer/src/index.ts")], {
34036
34129
  env: { ...process.env, ...env2, PORT: String(indexerPort) },
34037
34130
  stdout: Bun.file(indexerLogFile),
34038
34131
  stderr: Bun.file(indexerLogFile)
@@ -34046,7 +34139,7 @@ async function restartDev() {
34046
34139
  console.log(green(" ✓ Indexer"), dim(`http://localhost:${indexerPort}`));
34047
34140
  if (state.services.worker) {
34048
34141
  const workerLogFile = getLogFile("worker");
34049
- const workerProc = Bun.spawn(["bun", "run", resolve10(packagesDir, "packages/worker/src/index.ts")], {
34142
+ const workerProc = Bun.spawn(["bun", "run", resolve11(packagesDir, "packages/worker/src/index.ts")], {
34050
34143
  env: { ...process.env, ...env2 },
34051
34144
  stdout: Bun.file(workerLogFile),
34052
34145
  stderr: Bun.file(workerLogFile)
@@ -34061,7 +34154,7 @@ async function restartDev() {
34061
34154
  }
34062
34155
  if (state.services.subgraphs) {
34063
34156
  const subgraphsLogFile = getLogFile("subgraphs");
34064
- const subgraphsProc = Bun.spawn(["bun", "run", resolve10(packagesDir, "packages/subgraphs/src/service.ts")], {
34157
+ const subgraphsProc = Bun.spawn(["bun", "run", resolve11(packagesDir, "packages/subgraphs/src/service.ts")], {
34065
34158
  env: { ...process.env, ...env2 },
34066
34159
  stdout: Bun.file(subgraphsLogFile),
34067
34160
  stderr: Bun.file(subgraphsLogFile)
@@ -34189,7 +34282,7 @@ async function ensureDevPostgres(dataDir) {
34189
34282
  if (check.stdout.toString().trim()) {
34190
34283
  return false;
34191
34284
  }
34192
- const pgDataDir = join12(dataDir, "postgres");
34285
+ const pgDataDir = join11(dataDir, "postgres");
34193
34286
  mkdirSync5(pgDataDir, { recursive: true });
34194
34287
  const stopped = await Bun.$`docker ps -aq -f name=secondlayer-dev-postgres`.quiet().nothrow();
34195
34288
  if (stopped.stdout.toString().trim()) {
@@ -34206,7 +34299,7 @@ async function ensureDevPostgres(dataDir) {
34206
34299
  }
34207
34300
  async function runMigrations(databaseUrl) {
34208
34301
  const packagesDir = dirname14(dirname14(dirname14(dirname14(import.meta.dir))));
34209
- const migrateScript = resolve10(packagesDir, "packages/shared/src/db/migrate.ts");
34302
+ const migrateScript = resolve11(packagesDir, "packages/shared/src/db/migrate.ts");
34210
34303
  const result = await Bun.$`DATABASE_URL=${databaseUrl} bun run ${migrateScript}`.quiet().nothrow();
34211
34304
  if (result.exitCode !== 0) {
34212
34305
  throw new Error(`Migration failed: ${result.stderr.toString()}`);
@@ -34239,7 +34332,7 @@ __export(exports_node_impl, {
34239
34332
  runSetupWizard: () => runSetupWizard,
34240
34333
  restartNode: () => restartNode
34241
34334
  });
34242
- import { confirm as confirm6, input as input3, select as select4 } from "@inquirer/prompts";
34335
+ import { confirm as confirm5, input as input3, select as select4 } from "@inquirer/prompts";
34243
34336
  async function runSetupWizard() {
34244
34337
  console.log("");
34245
34338
  console.log(blue("Stacks Node Setup Wizard"));
@@ -34276,13 +34369,13 @@ async function runSetupWizard() {
34276
34369
  ],
34277
34370
  default: "mainnet"
34278
34371
  });
34279
- const autoStartIndexer = await confirm6({
34372
+ const autoStartIndexer = await confirm5({
34280
34373
  message: "Auto-start indexer when node starts?",
34281
34374
  default: true
34282
34375
  });
34283
34376
  let indexerPort = 3700;
34284
34377
  if (autoStartIndexer) {
34285
- const customPort = await confirm6({
34378
+ const customPort = await confirm5({
34286
34379
  message: "Use default indexer port (3700)?",
34287
34380
  default: true
34288
34381
  });
@@ -34388,7 +34481,7 @@ async function stopNode(_pathOverride, force, _wait) {
34388
34481
  return;
34389
34482
  }
34390
34483
  if (!force) {
34391
- const proceed = await confirm6({
34484
+ const proceed = await confirm5({
34392
34485
  message: "Stop the Stacks node?",
34393
34486
  default: false
34394
34487
  });
@@ -34418,7 +34511,7 @@ async function restartNode(pathOverride, force, _wait) {
34418
34511
  const wasRunning = await isNodeRunning();
34419
34512
  if (wasRunning) {
34420
34513
  if (!force) {
34421
- const proceed = await confirm6({
34514
+ const proceed = await confirm5({
34422
34515
  message: "Restart the Stacks node?",
34423
34516
  default: false
34424
34517
  });
@@ -34621,10 +34714,254 @@ var init_node_impl = __esm(() => {
34621
34714
  init_output();
34622
34715
  });
34623
34716
 
34717
+ // src/lib/archive-reference.ts
34718
+ import { createHash as createHash4 } from "node:crypto";
34719
+ import { readFile as readFile5 } from "node:fs/promises";
34720
+ import { dirname as dirname16, join as join13, resolve as resolve13, sep as sep2 } from "node:path";
34721
+ import { parseRetryAfter } from "@secondlayer/sdk";
34722
+ import { ARCHIVE_ROOT_PUBLIC_KEY_PEM } from "@secondlayer/shared/archive/root-key";
34723
+ import { verifyStreamsBulkManifestSignature } from "@secondlayer/shared/streams-bulk-manifest";
34724
+ function isLatestPointer(value) {
34725
+ const pointer = value;
34726
+ return typeof pointer.snapshot_path === "string" && (value.partitions === undefined || value.partitions.length === 0);
34727
+ }
34728
+ function assertPathWithinRoot(path11, root) {
34729
+ const segments = path11.split("/");
34730
+ const leaves = path11.startsWith("/") || path11.includes("\\") || /^[a-z][a-z0-9+.-]*:/i.test(path11) || /[%?#]/.test(path11) || segments.some((part) => part === "" || part === "." || part === "..");
34731
+ if (leaves) {
34732
+ throw new Error(`archive path ${path11} would leave the archive root`);
34733
+ }
34734
+ if (root !== undefined) {
34735
+ const base2 = resolve13(root);
34736
+ const target = resolve13(base2, path11);
34737
+ if (target !== base2 && !target.startsWith(base2 + sep2)) {
34738
+ throw new Error(`archive path ${path11} would leave the archive root`);
34739
+ }
34740
+ }
34741
+ }
34742
+ async function fetchJson(url) {
34743
+ const response2 = await fetch(url, { signal: AbortSignal.timeout(30000) });
34744
+ if (!response2.ok) {
34745
+ throw new Error(`could not fetch manifest (${response2.status} ${response2.statusText})`);
34746
+ }
34747
+ return response2.json();
34748
+ }
34749
+ async function loadReference(source, options3 = {}) {
34750
+ const isRemote = /^https?:\/\//.test(source);
34751
+ let manifest;
34752
+ let root;
34753
+ if (isRemote) {
34754
+ manifest = await fetchJson(source);
34755
+ const url = new URL(source);
34756
+ url.pathname = url.pathname.replace(/\/snapshots\/[^/]+$/, "").replace(/\/[^/]+\.json$/, "");
34757
+ root = url.toString();
34758
+ } else {
34759
+ const path11 = resolve13(source);
34760
+ manifest = JSON.parse(await readFile5(path11, "utf8"));
34761
+ root = isLatestPointer(manifest) ? dirname16(path11) : dirname16(dirname16(path11));
34762
+ }
34763
+ if (!isLatestPointer(manifest)) {
34764
+ return { manifest, origin: source, root, isRemote };
34765
+ }
34766
+ const pointer = manifest;
34767
+ const snapshotPath = pointer.snapshot_path;
34768
+ if (!SNAPSHOT_PATH_PATTERN.test(snapshotPath)) {
34769
+ throw new Error(`pointer names an invalid snapshot path: ${snapshotPath} (expected snapshots/<sha256>.json)`);
34770
+ }
34771
+ if (typeof pointer.snapshot_digest !== "string" || !/^[0-9a-f]{64}$/.test(pointer.snapshot_digest)) {
34772
+ throw new Error("pointer carries no snapshot_digest, so it cannot prove which snapshot is current");
34773
+ }
34774
+ if (pointer.signature !== undefined) {
34775
+ if (!options3.publicKeyPem) {
34776
+ throw new Error("pointer is signed but no public key is available to verify it");
34777
+ }
34778
+ let verified = false;
34779
+ try {
34780
+ verified = verifyStreamsBulkManifestSignature(pointer, options3.publicKeyPem);
34781
+ } catch {
34782
+ verified = false;
34783
+ }
34784
+ if (!verified) {
34785
+ throw new Error("pointer signature did not verify against the archive key");
34786
+ }
34787
+ }
34788
+ const resolved = isRemote ? await fetchJson(`${root.replace(/\/$/, "")}/${snapshotPath}`) : JSON.parse(await readFile5(join13(root, snapshotPath), "utf8"));
34789
+ const digest = createHash4("sha256").update(JSON.stringify((({ signature: _s, key_id: _k, ...rest }) => rest)(resolved))).digest("hex");
34790
+ if (digest !== pointer.snapshot_digest) {
34791
+ throw new Error(`pointer/snapshot mismatch: latest.json names ${pointer.snapshot_digest}, resolved manifest hashes to ${digest}`);
34792
+ }
34793
+ return { manifest: resolved, origin: source, root, isRemote };
34794
+ }
34795
+ function checkSignature(manifest, publicKeyPem, insecure) {
34796
+ if (insecure)
34797
+ return { verified: false, reason: "signature check skipped" };
34798
+ if (!manifest.signature) {
34799
+ return { verified: false, reason: "manifest carries no signature" };
34800
+ }
34801
+ if (!publicKeyPem) {
34802
+ return {
34803
+ verified: false,
34804
+ reason: "no public key available to verify with"
34805
+ };
34806
+ }
34807
+ try {
34808
+ const verified = verifyStreamsBulkManifestSignature(manifest, publicKeyPem);
34809
+ return {
34810
+ verified,
34811
+ reason: verified ? undefined : "signature did not verify"
34812
+ };
34813
+ } catch (err) {
34814
+ return {
34815
+ verified: false,
34816
+ reason: err instanceof Error ? err.message : "signature check failed"
34817
+ };
34818
+ }
34819
+ }
34820
+ async function fetchHostedKey(url) {
34821
+ if (!url.startsWith("https://"))
34822
+ return;
34823
+ try {
34824
+ const response2 = await fetch(url, { signal: AbortSignal.timeout(1e4) });
34825
+ if (!response2.ok)
34826
+ return;
34827
+ const body = await response2.json();
34828
+ return typeof body.public_key_pem === "string" ? body.public_key_pem : undefined;
34829
+ } catch {
34830
+ return;
34831
+ }
34832
+ }
34833
+ async function resolveArchivePublicKey(input4) {
34834
+ if (input4.explicitPem)
34835
+ return input4.explicitPem;
34836
+ if (input4.envPem)
34837
+ return input4.envPem;
34838
+ if (input4.allowHostedApi) {
34839
+ const hosted = await fetchHostedKey(input4.hostedKeyUrl ?? HOSTED_SIGNING_KEY_URL);
34840
+ if (hosted)
34841
+ return hosted;
34842
+ }
34843
+ return ARCHIVE_ROOT_PUBLIC_KEY_PEM;
34844
+ }
34845
+ function isTransientCode(code) {
34846
+ return typeof code === "string" && (TRANSIENT_ERROR_CODES.has(code) || code.startsWith("UND_ERR_"));
34847
+ }
34848
+ function isTransientFetchFailure(err) {
34849
+ if (!(err instanceof Error))
34850
+ return false;
34851
+ if (err.name === "AbortError" || err.name === "TimeoutError")
34852
+ return true;
34853
+ const code = err.code;
34854
+ const causeCode = err.cause?.code;
34855
+ if (isTransientCode(code) || isTransientCode(causeCode))
34856
+ return true;
34857
+ return err.name === "TypeError" && /fetch failed|network|socket|connection|reset|unreachable/i.test(err.message);
34858
+ }
34859
+ function retryDelayMs(attempt, response2) {
34860
+ const retryAfter = response2 ? parseRetryAfter(response2.headers.get("retry-after")) : undefined;
34861
+ if (retryAfter !== undefined) {
34862
+ return Math.min(retryAfter * 1000, RETRY_AFTER_CAP_MS);
34863
+ }
34864
+ return RETRY_BASE_MS * 2 ** (attempt - 1);
34865
+ }
34866
+ async function fetchWithRetry(url, opts) {
34867
+ const retries = opts.retries ?? PARTITION_FETCH_RETRIES;
34868
+ const fetchImpl = opts.fetchImpl ?? fetch;
34869
+ const sleep = opts.sleep ?? ((ms) => new Promise((r2) => setTimeout(r2, ms)));
34870
+ const timeoutMs = opts.timeoutMs ?? PARTITION_FETCH_TIMEOUT_MS;
34871
+ let lastReason = "";
34872
+ for (let attempt = 1;attempt <= retries + 1; attempt++) {
34873
+ let response2;
34874
+ try {
34875
+ response2 = await fetchImpl(url, {
34876
+ signal: AbortSignal.timeout(timeoutMs)
34877
+ });
34878
+ } catch (err) {
34879
+ if (!isTransientFetchFailure(err))
34880
+ throw err;
34881
+ lastReason = err instanceof Error ? err.message : String(err);
34882
+ }
34883
+ if (response2) {
34884
+ const retryable = response2.status === 429 || response2.status >= 500;
34885
+ if (!retryable)
34886
+ return response2;
34887
+ lastReason = `${response2.status} ${response2.statusText}`.trim();
34888
+ await response2.body?.cancel().catch(() => {});
34889
+ }
34890
+ if (attempt <= retries) {
34891
+ await sleep(retryDelayMs(attempt, response2));
34892
+ }
34893
+ }
34894
+ throw new ArchiveFetchError(`could not fetch ${opts.label} after ${retries + 1} attempts (${lastReason})`, { transient: true, attempts: retries + 1 });
34895
+ }
34896
+ async function fetchViaGate(gate, partition, retried, fetchOpts) {
34897
+ const url = await gate.getUrl(partition.path, retried ? { forceRefresh: true } : undefined);
34898
+ const response2 = await fetchWithRetry(url, {
34899
+ ...fetchOpts,
34900
+ label: partition.path
34901
+ });
34902
+ if (response2.status === 403 && !retried) {
34903
+ await response2.body?.cancel().catch(() => {});
34904
+ return fetchViaGate(gate, partition, true, fetchOpts);
34905
+ }
34906
+ if (!response2.ok) {
34907
+ throw new ArchiveFetchError(`could not fetch ${partition.path} (${response2.status} ${response2.statusText})`, { transient: false, attempts: 1 });
34908
+ }
34909
+ return Buffer.from(await response2.arrayBuffer());
34910
+ }
34911
+ async function fetchVerifiedPartition(reference, partition, gate, fetchOpts) {
34912
+ assertPathWithinRoot(partition.path, reference.isRemote ? undefined : reference.root);
34913
+ let bytes;
34914
+ if (gate) {
34915
+ bytes = await fetchViaGate(gate, partition, false, fetchOpts);
34916
+ } else if (reference.isRemote) {
34917
+ const url = `${reference.root.replace(/\/$/, "")}/${partition.path}`;
34918
+ const response2 = await fetchWithRetry(url, {
34919
+ ...fetchOpts,
34920
+ label: partition.path
34921
+ });
34922
+ if (!response2.ok) {
34923
+ throw new ArchiveFetchError(`could not fetch ${partition.path} (${response2.status} ${response2.statusText})`, { transient: false, attempts: 1 });
34924
+ }
34925
+ bytes = Buffer.from(await response2.arrayBuffer());
34926
+ } else {
34927
+ bytes = await readFile5(join13(reference.root, partition.path));
34928
+ }
34929
+ const digest = createHash4("sha256").update(bytes).digest("hex");
34930
+ if (digest !== partition.sha256) {
34931
+ throw new Error(`${partition.path} failed verification: expected ${partition.sha256}, got ${digest}`);
34932
+ }
34933
+ return bytes;
34934
+ }
34935
+ var SNAPSHOT_PATH_PATTERN, HOSTED_SIGNING_KEY_URL = "https://api.secondlayer.tools/public/streams/signing-key", ArchiveFetchError, TRANSIENT_ERROR_CODES, PARTITION_FETCH_RETRIES = 3, PARTITION_FETCH_TIMEOUT_MS = 300000, RETRY_BASE_MS = 1000, RETRY_AFTER_CAP_MS = 60000;
34936
+ var init_archive_reference = __esm(() => {
34937
+ SNAPSHOT_PATH_PATTERN = /^snapshots\/[0-9a-f]{64}\.json$/;
34938
+ ArchiveFetchError = class ArchiveFetchError extends Error {
34939
+ transient;
34940
+ attempts;
34941
+ constructor(message, opts) {
34942
+ super(message);
34943
+ this.name = "ArchiveFetchError";
34944
+ this.transient = opts.transient;
34945
+ this.attempts = opts.attempts;
34946
+ }
34947
+ };
34948
+ TRANSIENT_ERROR_CODES = new Set([
34949
+ "ECONNRESET",
34950
+ "ECONNREFUSED",
34951
+ "ECONNABORTED",
34952
+ "ETIMEDOUT",
34953
+ "EAI_AGAIN",
34954
+ "EPIPE",
34955
+ "ENETUNREACH",
34956
+ "EHOSTUNREACH"
34957
+ ]);
34958
+ });
34959
+
34624
34960
  // src/lib/instance-init.ts
34625
34961
  import { randomBytes as randomBytes3 } from "node:crypto";
34626
- import { existsSync as existsSync10, readFileSync as readFileSync9, writeFileSync as writeFileSync8 } from "node:fs";
34627
- import { resolve as resolve15 } from "node:path";
34962
+ import { existsSync as existsSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync7 } from "node:fs";
34963
+ import { resolve as resolve14 } from "node:path";
34964
+ import { ARCHIVE_ROOT_PUBLIC_KEY_PEM as ARCHIVE_ROOT_PUBLIC_KEY_PEM2 } from "@secondlayer/shared/archive/root-key";
34628
34965
  import { generateEd25519KeyPair } from "@secondlayer/shared/crypto/ed25519";
34629
34966
  import { INSTANCE_NETWORKS } from "@secondlayer/shared/db/queries/instance";
34630
34967
  function parseInstanceNetwork(value) {
@@ -34634,6 +34971,17 @@ function parseInstanceNetwork(value) {
34634
34971
  }
34635
34972
  throw new Error(`network must be mainnet, testnet, or devnet (got ${value})`);
34636
34973
  }
34974
+ function instanceNetworkFromEnv(fallback = "mainnet") {
34975
+ const raw = process.env.STACKS_NETWORK ?? fallback;
34976
+ try {
34977
+ return parseInstanceNetwork(raw);
34978
+ } catch (err) {
34979
+ printError(err instanceof Error ? err.message : String(err), {
34980
+ hint: "Pass --network mainnet, --network testnet, or --network devnet."
34981
+ });
34982
+ process.exit(1);
34983
+ }
34984
+ }
34637
34985
  function generateInstanceToken() {
34638
34986
  return randomBytes3(32).toString("hex");
34639
34987
  }
@@ -34657,9 +35005,9 @@ function unescapeEnvValue(raw) {
34657
35005
  `).replace(/\\"/g, '"').replace(/\\\\/g, "\\");
34658
35006
  }
34659
35007
  function readEnvValue(envPath, key) {
34660
- if (!existsSync10(envPath))
35008
+ if (!existsSync8(envPath))
34661
35009
  return null;
34662
- const match = readFileSync9(envPath, "utf8").match(new RegExp(`^${key}=(.*)$`, "m"));
35010
+ const match = readFileSync7(envPath, "utf8").match(new RegExp(`^${key}=(.*)$`, "m"));
34663
35011
  return match ? unescapeEnvValue(match[1]) : null;
34664
35012
  }
34665
35013
  function buildInstanceEnv(input4) {
@@ -34667,7 +35015,7 @@ function buildInstanceEnv(input4) {
34667
35015
  const secrets = input4.existing?.SECONDLAYER_SECRETS_KEY || generateSecretsKey();
34668
35016
  const signing = input4.existing?.STREAMS_SIGNING_PRIVATE_KEY || input4.existing?.SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY || generateSigningPrivateKey();
34669
35017
  const apiUrl = input4.apiUrl ?? "http://127.0.0.1:3800";
34670
- const archiveKey = input4.existing?.ARCHIVE_SIGNING_PUBLIC_KEY;
35018
+ const archiveKey = input4.existing?.ARCHIVE_SIGNING_PUBLIC_KEY || input4.archivePublicKeyPem || ARCHIVE_ROOT_PUBLIC_KEY_PEM2;
34671
35019
  return {
34672
35020
  INSTANCE_MODE: "oss",
34673
35021
  STACKS_NETWORK: input4.network,
@@ -34678,7 +35026,7 @@ function buildInstanceEnv(input4) {
34678
35026
  ALLOW_UNSIGNED_WEBHOOKS: "false",
34679
35027
  SL_API_URL: input4.existing?.SL_API_URL || apiUrl,
34680
35028
  SL_API_KEY: token,
34681
- ...archiveKey ? { ARCHIVE_SIGNING_PUBLIC_KEY: archiveKey } : {}
35029
+ ARCHIVE_SIGNING_PUBLIC_KEY: archiveKey
34682
35030
  };
34683
35031
  }
34684
35032
  function renderInstanceEnv(env2) {
@@ -34698,20 +35046,17 @@ function renderInstanceEnv(env2) {
34698
35046
  "# INSTANCE_TOKEN wins if the two ever disagree.",
34699
35047
  `SL_API_KEY=${env2.SL_API_KEY}`
34700
35048
  ];
34701
- if (env2.ARCHIVE_SIGNING_PUBLIC_KEY) {
34702
- lines2.push(`ARCHIVE_SIGNING_PUBLIC_KEY=${escapeEnvValue(env2.ARCHIVE_SIGNING_PUBLIC_KEY)}`);
34703
- }
34704
- lines2.push("");
35049
+ lines2.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(env2.ARCHIVE_SIGNING_PUBLIC_KEY)}`, "");
34705
35050
  return lines2.join(`
34706
35051
  `);
34707
35052
  }
34708
35053
  function writeInstanceEnv(cwd, env2, file = INSTANCE_ENV_FILE) {
34709
- const path11 = resolve15(cwd, file);
34710
- writeFileSync8(path11, renderInstanceEnv(env2), { mode: 384 });
35054
+ const path11 = resolve14(cwd, file);
35055
+ writeFileSync7(path11, renderInstanceEnv(env2), { mode: 384 });
34711
35056
  return path11;
34712
35057
  }
34713
35058
  function loadExistingInstanceEnv(cwd, file = INSTANCE_ENV_FILE) {
34714
- const path11 = resolve15(cwd, file);
35059
+ const path11 = resolve14(cwd, file);
34715
35060
  return {
34716
35061
  INSTANCE_TOKEN: readEnvValue(path11, "INSTANCE_TOKEN"),
34717
35062
  SECONDLAYER_SECRETS_KEY: readEnvValue(path11, "SECONDLAYER_SECRETS_KEY"),
@@ -34723,7 +35068,9 @@ function loadExistingInstanceEnv(cwd, file = INSTANCE_ENV_FILE) {
34723
35068
  };
34724
35069
  }
34725
35070
  var INSTANCE_ENV_FILE = ".env.local";
34726
- var init_instance_init = () => {};
35071
+ var init_instance_init = __esm(() => {
35072
+ init_output();
35073
+ });
34727
35074
 
34728
35075
  // src/lib/observer-stanza.ts
34729
35076
  function parseObserverMode(value) {
@@ -34995,9 +35342,10 @@ var init_oss_compose = () => {};
34995
35342
  import { spawn as spawn4, spawnSync as spawnSync5 } from "node:child_process";
34996
35343
  import { randomBytes as randomBytes4 } from "node:crypto";
34997
35344
  import { existsSync as existsSync11 } from "node:fs";
34998
- import { writeFile as writeFile7 } from "node:fs/promises";
35345
+ import { writeFile as writeFile6 } from "node:fs/promises";
34999
35346
  import { createServer } from "node:net";
35000
- import { join as join19, resolve as resolvePath2 } from "node:path";
35347
+ import { join as join18, resolve as resolvePath2 } from "node:path";
35348
+ import { closeDb as closeDb2, getRawClientFor } from "@secondlayer/shared/db";
35001
35349
  import { FLOORS, diskFloorGb } from "@secondlayer/shared/runtime";
35002
35350
  function parseSetupNodeMode(value) {
35003
35351
  const mode = value.trim().toLowerCase();
@@ -35090,7 +35438,7 @@ function randomHex(bytes) {
35090
35438
  return randomBytes4(bytes).toString("hex");
35091
35439
  }
35092
35440
  function resolveSecrets(config) {
35093
- const envPath = join19(config.dir, ".env");
35441
+ const envPath = join18(config.dir, ".env");
35094
35442
  const existing = config.force || !existsSync11(envPath) ? {} : readEnvFile(config.dir, envPath);
35095
35443
  const instance = buildInstanceEnv({
35096
35444
  network: config.network,
@@ -35103,14 +35451,33 @@ function resolveSecrets(config) {
35103
35451
  SL_API_KEY: existing.SL_API_KEY,
35104
35452
  ARCHIVE_SIGNING_PUBLIC_KEY: existing.ARCHIVE_SIGNING_PUBLIC_KEY
35105
35453
  },
35106
- apiUrl: `http://${normalizeLoopbackHost(config.apiPort)}`
35454
+ apiUrl: `http://${normalizeLoopbackHost(config.apiPort)}`,
35455
+ archivePublicKeyPem: config.archivePublicKeyPem
35107
35456
  });
35108
35457
  return {
35109
35458
  instance,
35110
35459
  postgresPassword: existing.POSTGRES_PASSWORD || randomHex(24),
35111
- bitcoinRpcPassword: existing.BITCOIN_RPC_PASSWORD || randomHex(24)
35460
+ bitcoinRpcPassword: existing.BITCOIN_RPC_PASSWORD || randomHex(24),
35461
+ databaseUrl: operatorEditedDatabaseUrl(existing)
35112
35462
  };
35113
35463
  }
35464
+ function operatorEditedDatabaseUrl(existing) {
35465
+ const url = existing.DATABASE_URL;
35466
+ if (!url)
35467
+ return;
35468
+ if (existing.POSTGRES_PORT && existing.POSTGRES_PASSWORD) {
35469
+ const generated = setupDatabaseUrl({ postgresPort: existing.POSTGRES_PORT }, { postgresPassword: existing.POSTGRES_PASSWORD });
35470
+ if (url === generated)
35471
+ return;
35472
+ }
35473
+ return url;
35474
+ }
35475
+ function setupDatabaseUrl(config, secrets) {
35476
+ if (secrets.databaseUrl)
35477
+ return secrets.databaseUrl;
35478
+ const password = encodeURIComponent(secrets.postgresPassword);
35479
+ return `postgres://${SETUP_POSTGRES_USER}:${password}@${normalizeLoopbackHost(config.postgresPort)}/${SETUP_POSTGRES_DB}`;
35480
+ }
35114
35481
  function normalizeLoopbackHost(apiPort) {
35115
35482
  return apiPort.includes(":") ? apiPort : `127.0.0.1:${apiPort}`;
35116
35483
  }
@@ -35119,7 +35486,9 @@ function readEnvFile(dir, envPath) {
35119
35486
  return {
35120
35487
  ...shared,
35121
35488
  POSTGRES_PASSWORD: readEnvValue(envPath, "POSTGRES_PASSWORD"),
35122
- BITCOIN_RPC_PASSWORD: readEnvValue(envPath, "BITCOIN_RPC_PASSWORD")
35489
+ POSTGRES_PORT: readEnvValue(envPath, "POSTGRES_PORT"),
35490
+ BITCOIN_RPC_PASSWORD: readEnvValue(envPath, "BITCOIN_RPC_PASSWORD"),
35491
+ DATABASE_URL: readEnvValue(envPath, "DATABASE_URL")
35123
35492
  };
35124
35493
  }
35125
35494
  function renderSetupEnv(config, secrets) {
@@ -35131,10 +35500,14 @@ function renderSetupEnv(config, secrets) {
35131
35500
  `NODE_MODE=${config.nodeMode}`,
35132
35501
  `API_PORT=${config.apiPort}`,
35133
35502
  `INDEXER_PORT=${config.indexerPort}`,
35134
- "POSTGRES_USER=secondlayer",
35503
+ `POSTGRES_USER=${SETUP_POSTGRES_USER}`,
35135
35504
  `POSTGRES_PASSWORD=${secrets.postgresPassword}`,
35136
- "POSTGRES_DB=secondlayer",
35505
+ `POSTGRES_DB=${SETUP_POSTGRES_DB}`,
35137
35506
  `POSTGRES_PORT=${config.postgresPort}`,
35507
+ "# Where bootstrap, verify, repair, and backup connect from this directory:",
35508
+ "# the compose Postgres above, published on the host. Point it elsewhere",
35509
+ "# only if you moved the index.",
35510
+ `DATABASE_URL=${escapeEnvValue(setupDatabaseUrl(config, secrets))}`,
35138
35511
  "# The instance credential. The API validates bearer tokens against it,",
35139
35512
  "# and the CLI/SDK/MCP read it first when authenticating.",
35140
35513
  `INSTANCE_TOKEN=${i3.INSTANCE_TOKEN}`,
@@ -35146,9 +35519,7 @@ function renderSetupEnv(config, secrets) {
35146
35519
  `SL_API_KEY=${i3.SL_API_KEY}`,
35147
35520
  `SL_API_URL=${i3.SL_API_URL}`
35148
35521
  ];
35149
- if (i3.ARCHIVE_SIGNING_PUBLIC_KEY) {
35150
- lines2.push(`ARCHIVE_SIGNING_PUBLIC_KEY=${escapeEnvValue(i3.ARCHIVE_SIGNING_PUBLIC_KEY)}`);
35151
- }
35522
+ lines2.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(i3.ARCHIVE_SIGNING_PUBLIC_KEY)}`);
35152
35523
  if (config.nodeMode === "full") {
35153
35524
  lines2.push("BITCOIN_RPC_USER=stacks", `BITCOIN_RPC_PASSWORD=${secrets.bitcoinRpcPassword}`);
35154
35525
  }
@@ -35157,22 +35528,22 @@ function renderSetupEnv(config, secrets) {
35157
35528
  `);
35158
35529
  }
35159
35530
  async function writeSetupFiles(config, secrets) {
35160
- const composePath = join19(config.dir, "docker-compose.yml");
35161
- const envPath = join19(config.dir, ".env");
35162
- await writeFile7(composePath, buildOssCompose({ owner: config.owner, imageTag: config.imageTag }));
35163
- await writeFile7(envPath, renderSetupEnv(config, secrets), { mode: 384 });
35531
+ const composePath = join18(config.dir, "docker-compose.yml");
35532
+ const envPath = join18(config.dir, ".env");
35533
+ await writeFile6(composePath, buildOssCompose({ owner: config.owner, imageTag: config.imageTag }));
35534
+ await writeFile6(envPath, renderSetupEnv(config, secrets), { mode: 384 });
35164
35535
  const result = { composePath, envPath };
35165
35536
  if (config.nodeMode === "full") {
35166
- const configTomlPath = join19(config.dir, "Config.toml");
35167
- await writeFile7(configTomlPath, buildOssStacksConfigToml({
35537
+ const configTomlPath = join18(config.dir, "Config.toml");
35538
+ await writeFile6(configTomlPath, buildOssStacksConfigToml({
35168
35539
  network: config.network,
35169
35540
  bitcoinRpcPassword: secrets.bitcoinRpcPassword ?? ""
35170
35541
  }));
35171
35542
  result.configTomlPath = configTomlPath;
35172
35543
  }
35173
35544
  if (config.nodeMode === "full") {
35174
- const bitcoinConfPath = join19(config.dir, "bitcoin.conf");
35175
- await writeFile7(bitcoinConfPath, buildOssBitcoinConf({ rpcPassword: secrets.bitcoinRpcPassword ?? "" }));
35545
+ const bitcoinConfPath = join18(config.dir, "bitcoin.conf");
35546
+ await writeFile6(bitcoinConfPath, buildOssBitcoinConf({ rpcPassword: secrets.bitcoinRpcPassword ?? "" }));
35176
35547
  result.bitcoinConfPath = bitcoinConfPath;
35177
35548
  }
35178
35549
  return result;
@@ -35186,6 +35557,7 @@ function runStreaming(command, args, opts) {
35186
35557
  return new Promise((resolvePromise, reject) => {
35187
35558
  const child = spawn4(command, args, {
35188
35559
  cwd: opts.cwd,
35560
+ env: opts.env ?? process.env,
35189
35561
  stdio: ["ignore", "pipe", "pipe"]
35190
35562
  });
35191
35563
  let stdout = "";
@@ -35261,15 +35633,74 @@ function cliEntry() {
35261
35633
  }
35262
35634
  return [process.execPath, entry];
35263
35635
  }
35264
- async function runBootstrap(config, against, onLine) {
35636
+ function buildChildEnv(envPath, base2 = process.env) {
35637
+ const env2 = { ...base2 };
35638
+ for (const key of CHILD_ENV_KEYS) {
35639
+ const value = readEnvValue(envPath, key);
35640
+ if (value)
35641
+ env2[key] = value;
35642
+ }
35643
+ return env2;
35644
+ }
35645
+ async function checkDatabaseReachable(databaseUrl) {
35646
+ try {
35647
+ const client3 = getRawClientFor(databaseUrl);
35648
+ await client3`SELECT 1`;
35649
+ return { ok: true };
35650
+ } catch (err) {
35651
+ return {
35652
+ ok: false,
35653
+ error: err instanceof Error ? err.message : String(err)
35654
+ };
35655
+ } finally {
35656
+ await closeDb2().catch(() => {});
35657
+ }
35658
+ }
35659
+ async function preflightBootstrapDatabase(config, secrets, emit2) {
35660
+ const databaseUrl = setupDatabaseUrl(config, secrets);
35661
+ const reachable = await checkDatabaseReachable(databaseUrl);
35662
+ if (!reachable.ok) {
35663
+ emit2({
35664
+ type: "step-error",
35665
+ step: "bootstrap",
35666
+ message: `Postgres at ${redactUrl(databaseUrl)} did not answer SELECT 1: ${reachable.error}. Check \`docker compose logs postgres\`, then re-run setup to resume.`
35667
+ });
35668
+ return null;
35669
+ }
35670
+ emit2({
35671
+ type: "step-log",
35672
+ step: "bootstrap",
35673
+ line: `database ${redactUrl(databaseUrl)}`
35674
+ });
35675
+ return databaseUrl;
35676
+ }
35677
+ async function runBootstrap(config, against, files, onLine) {
35265
35678
  const [command, ...prefix] = cliEntry();
35266
35679
  const args = [...prefix, "bootstrap", "--against", against, "--yes"];
35267
- return runStreaming(command, args, { cwd: config.dir, onLine });
35680
+ return runStreaming(command, args, {
35681
+ cwd: config.dir,
35682
+ env: buildChildEnv(files.envPath),
35683
+ onLine
35684
+ });
35268
35685
  }
35269
- async function runVerify(config, against, onLine) {
35686
+ async function runVerify(config, against, files, onLine) {
35270
35687
  const [command, ...prefix] = cliEntry();
35271
35688
  const args = [...prefix, "verify", "all", "--against", against];
35272
- return runStreaming(command, args, { cwd: config.dir, onLine });
35689
+ return runStreaming(command, args, {
35690
+ cwd: config.dir,
35691
+ env: buildChildEnv(files.envPath),
35692
+ onLine
35693
+ });
35694
+ }
35695
+ function redactUrl(url) {
35696
+ try {
35697
+ const u3 = new URL(url);
35698
+ if (u3.password)
35699
+ u3.password = "***";
35700
+ return u3.toString();
35701
+ } catch {
35702
+ return url;
35703
+ }
35273
35704
  }
35274
35705
  function buildSuccessSummary(config, secrets) {
35275
35706
  const apiUrl = `http://${normalizeLoopbackHost(config.apiPort)}`;
@@ -35341,7 +35772,11 @@ async function runSetup(initialConfig, emit2) {
35341
35772
  dir: config.dir,
35342
35773
  network: config.network,
35343
35774
  apiPort: config.apiPort,
35344
- force: config.force
35775
+ force: config.force,
35776
+ archivePublicKeyPem: await resolveArchivePublicKey({
35777
+ envPem: process.env.ARCHIVE_SIGNING_PUBLIC_KEY ?? process.env.STREAMS_SIGNING_PUBLIC_KEY,
35778
+ allowHostedApi: !isOssMode()
35779
+ })
35345
35780
  });
35346
35781
  emit2({ type: "step-done", step: "secrets" });
35347
35782
  emit2({ type: "step-start", step: "config" });
@@ -35408,7 +35843,11 @@ ${up.stderr || up.stdout}`
35408
35843
  emit2({ type: "step-skip", step: "bootstrap", reason: "--skip-bootstrap" });
35409
35844
  } else if (config.against) {
35410
35845
  emit2({ type: "step-start", step: "bootstrap" });
35411
- const res = await runBootstrap(config, config.against, (line) => emit2({ type: "step-log", step: "bootstrap", line }));
35846
+ const databaseUrl = await preflightBootstrapDatabase(config, secrets, emit2);
35847
+ if (databaseUrl === null) {
35848
+ return { ok: false, config, secrets, files, observer };
35849
+ }
35850
+ const res = await runBootstrap(config, config.against, files, (line) => emit2({ type: "step-log", step: "bootstrap", line }));
35412
35851
  if (res.code === 0) {
35413
35852
  bootstrapped = true;
35414
35853
  emit2({ type: "step-done", step: "bootstrap" });
@@ -35436,7 +35875,7 @@ ${up.stderr || up.stdout}`
35436
35875
  });
35437
35876
  } else {
35438
35877
  emit2({ type: "step-start", step: "verify" });
35439
- const res = await runVerify(config, config.against, (line) => emit2({ type: "step-log", step: "verify", line }));
35878
+ const res = await runVerify(config, config.against, files, (line) => emit2({ type: "step-log", step: "verify", line }));
35440
35879
  if (res.code === 0) {
35441
35880
  emit2({ type: "step-done", step: "verify" });
35442
35881
  } else {
@@ -35452,11 +35891,13 @@ ${up.stderr || up.stdout}`
35452
35891
  emit2({ type: "step-done", step: "summary", detail: summary });
35453
35892
  return { ok: true, config, secrets, files, observer, summary };
35454
35893
  }
35455
- var SETUP_NODE_MODES, DEFAULT_ARCHIVE_MANIFEST = "https://archive.secondlayer.tools/latest.json", MissingSetupFlagError;
35894
+ 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;
35456
35895
  var init_setup_wizard = __esm(() => {
35896
+ init_archive_reference();
35457
35897
  init_instance_init();
35458
35898
  init_observer_stanza();
35459
35899
  init_oss_compose();
35900
+ init_resolve_auth();
35460
35901
  SETUP_NODE_MODES = ["external", "stacks", "full"];
35461
35902
  MissingSetupFlagError = class MissingSetupFlagError extends Error {
35462
35903
  flag;
@@ -35466,6 +35907,12 @@ var init_setup_wizard = __esm(() => {
35466
35907
  this.flag = flag;
35467
35908
  }
35468
35909
  };
35910
+ CHILD_ENV_KEYS = [
35911
+ "DATABASE_URL",
35912
+ "ARCHIVE_SIGNING_PUBLIC_KEY",
35913
+ "SL_API_URL",
35914
+ "INSTANCE_TOKEN"
35915
+ ];
35469
35916
  });
35470
35917
 
35471
35918
  // src/lib/setup-tui.tsx
@@ -35473,6 +35920,7 @@ var exports_setup_tui = {};
35473
35920
  __export(exports_setup_tui, {
35474
35921
  runSetupTui: () => runSetupTui
35475
35922
  });
35923
+ import { resolve as resolvePath4 } from "node:path";
35476
35924
  import {
35477
35925
  CliRenderEvents,
35478
35926
  createCliRenderer
@@ -35578,7 +36026,7 @@ function App({ flags }) {
35578
36026
  apiPort: flags.apiPort ?? "127.0.0.1:3800",
35579
36027
  indexerPort: "127.0.0.1:3700",
35580
36028
  postgresPort: "127.0.0.1:5432",
35581
- dir: flags.dir ?? process.cwd(),
36029
+ dir: resolvePath4(flags.dir ?? process.cwd()),
35582
36030
  against: skipBootstrap ? undefined : against,
35583
36031
  skipBootstrap,
35584
36032
  skipVerify: !!flags.skipVerify,
@@ -35992,13 +36440,13 @@ async function runSetupTui(flags) {
35992
36440
  createRoot(renderer).render(/* @__PURE__ */ jsxDEV(App, {
35993
36441
  flags
35994
36442
  }, undefined, false, undefined, this));
35995
- return new Promise((resolve16, reject) => {
36443
+ return new Promise((resolve17, reject) => {
35996
36444
  let settled = false;
35997
36445
  const onDestroy = () => {
35998
36446
  if (settled)
35999
36447
  return;
36000
36448
  settled = true;
36001
- resolve16();
36449
+ resolve17();
36002
36450
  };
36003
36451
  const onError = (event) => {
36004
36452
  if (settled)
@@ -36098,7 +36546,7 @@ var {
36098
36546
  // package.json
36099
36547
  var package_default = {
36100
36548
  name: "@secondlayer/cli",
36101
- version: "13.0.3",
36549
+ version: "14.0.0",
36102
36550
  description: "Run a Stacks index on your own hardware — set it up, restore verified history, and keep it current.",
36103
36551
  repository: {
36104
36552
  type: "git",
@@ -36142,11 +36590,11 @@ var package_default = {
36142
36590
  "@opentui/core": "^0.5.3",
36143
36591
  "@opentui/react": "^0.5.3",
36144
36592
  "@secondlayer/bundler": "^0.5.1",
36145
- "@secondlayer/scaffold": "^1.5.4",
36146
- "@secondlayer/sdk": "^9.0.1",
36147
- "@secondlayer/shared": "^11.0.0",
36148
- "@secondlayer/stacks": "^4.0.0",
36149
- "@secondlayer/subgraphs": "^4.0.4",
36593
+ "@secondlayer/scaffold": "^1.5.5",
36594
+ "@secondlayer/sdk": "^10.0.0",
36595
+ "@secondlayer/shared": "^11.2.0",
36596
+ "@secondlayer/stacks": "^5.0.0",
36597
+ "@secondlayer/subgraphs": "^4.1.1",
36150
36598
  esbuild: "^0.27.4",
36151
36599
  react: "^19.1.0",
36152
36600
  "smol-toml": "^1.6.1",
@@ -36201,7 +36649,7 @@ async function runBuy(opts) {
36201
36649
  human: () => {
36202
36650
  success(`Checkout for $${amount} → ${email}`);
36203
36651
  console.log(res.url);
36204
- console.log(dim("After payment: SL_API_URL=https://api.secondlayer.tools secondlayer login && secondlayer credits balance"));
36652
+ console.log(dim(`After payment: ${ARCHIVE_LOGIN_COMMAND} && secondlayer credits balance`));
36205
36653
  }
36206
36654
  });
36207
36655
  }
@@ -36585,7 +37033,11 @@ Examples:
36585
37033
  });
36586
37034
  config.command("reset").description("Reset configuration to defaults").option("-y, --yes", "Skip confirmation").action(async (opts) => {
36587
37035
  try {
36588
- if (!opts.yes && !await confirmDestructive("Reset configuration to defaults?")) {
37036
+ if (!await confirmDestructive({
37037
+ message: "Reset configuration to defaults?",
37038
+ yes: opts.yes
37039
+ })) {
37040
+ info("Cancelled.");
36589
37041
  return;
36590
37042
  }
36591
37043
  await resetConfig();
@@ -36597,7 +37049,11 @@ Examples:
36597
37049
  });
36598
37050
  config.command("delete").description("Delete all configuration (remove the config file)").option("-y, --yes", "Skip confirmation").action(async (opts) => {
36599
37051
  try {
36600
- if (!opts.yes && !await confirmDestructive("Delete the config file?")) {
37052
+ if (!await confirmDestructive({
37053
+ message: "Delete the config file?",
37054
+ yes: opts.yes
37055
+ })) {
37056
+ info("Cancelled.");
36601
37057
  return;
36602
37058
  }
36603
37059
  await clearConfig();
@@ -36608,26 +37064,6 @@ Examples:
36608
37064
  }
36609
37065
  });
36610
37066
  }
36611
- async function confirmDestructive(message) {
36612
- if (!process.stdin.isTTY) {
36613
- error("Interactive prompt unavailable (stdin is not a TTY). Re-run with -y to skip confirmation.");
36614
- process.exit(1);
36615
- }
36616
- const { confirm: confirm4 } = await import("@inquirer/prompts");
36617
- try {
36618
- const ok = await confirm4({ message, default: false });
36619
- if (!ok)
36620
- console.log("Cancelled.");
36621
- return ok;
36622
- } catch (promptErr) {
36623
- const m2 = promptErr instanceof Error ? promptErr.message : String(promptErr);
36624
- if (m2.includes("ExitPromptError") || m2.includes("force closed")) {
36625
- error("Interactive prompt unavailable. Re-run with -y to skip confirmation.");
36626
- process.exit(1);
36627
- }
36628
- throw promptErr;
36629
- }
36630
- }
36631
37067
  async function printConfigTree(cfg) {
36632
37068
  const defaults2 = getDefaultConfig();
36633
37069
  printValue("network", cfg.network, isDefaultValue(cfg, "network"));
@@ -36761,7 +37197,9 @@ function registerConsoleCommand(program2) {
36761
37197
  }
36762
37198
  // src/commands/context.ts
36763
37199
  init_http();
36764
- import { SecondLayer as SecondLayer2 } from "@secondlayer/sdk";
37200
+ import {
37201
+ SecondLayer as SecondLayer2
37202
+ } from "@secondlayer/sdk";
36765
37203
 
36766
37204
  // src/lib/instance-diagnosis.ts
36767
37205
  var BOOTSTRAP_STEP = "Restore verified history: secondlayer bootstrap --against <manifest>";
@@ -36881,8 +37319,19 @@ function registerContextCommand(program2) {
36881
37319
  const apiKey = resolveEnvKey();
36882
37320
  const sl = new SecondLayer2({ baseUrl: apiUrl, apiKey });
36883
37321
  const snapshot = await sl.context();
36884
- const diagnostics = await explain(snapshot, apiUrl, apiKey);
36885
- const payload = { ...snapshot, diagnostics };
37322
+ const values2 = {};
37323
+ const errors2 = {};
37324
+ for (const [field, entry] of Object.entries(snapshot)) {
37325
+ values2[field] = entry.value;
37326
+ if (entry.error)
37327
+ errors2[field] = entry.error;
37328
+ }
37329
+ const diagnostics = await explain(values2, apiUrl, apiKey);
37330
+ const payload = {
37331
+ ...values2,
37332
+ ...Object.keys(errors2).length > 0 ? { errors: errors2 } : {},
37333
+ diagnostics
37334
+ };
36886
37335
  output({
36887
37336
  json: o3.json,
36888
37337
  data: payload,
@@ -37002,7 +37451,6 @@ function printPublicStatus(status, diagnosis) {
37002
37451
  init_api_client();
37003
37452
  init_filter_params();
37004
37453
  init_output();
37005
- import { confirm as confirm4 } from "@inquirer/prompts";
37006
37454
  import { sign } from "@secondlayer/shared/crypto/standard-webhooks";
37007
37455
 
37008
37456
  // src/lib/subscription-validation.ts
@@ -37028,6 +37476,7 @@ init_filter_params();
37028
37476
  init_output();
37029
37477
  init_resolve_auth();
37030
37478
  import {
37479
+ chmodSync,
37031
37480
  copyFileSync,
37032
37481
  existsSync as existsSync4,
37033
37482
  mkdirSync as mkdirSync3,
@@ -37108,23 +37557,51 @@ function parseTriggersInput(opts) {
37108
37557
  return result.data;
37109
37558
  });
37110
37559
  }
37560
+ function resolveRuntime(opts) {
37561
+ if (opts.runtime)
37562
+ return opts.runtime;
37563
+ const scripted = opts.subgraph !== undefined || opts.table !== undefined || opts.url !== undefined || opts.scaffold === false;
37564
+ return scripted ? "node" : undefined;
37565
+ }
37566
+ function requireTtyFor(flag) {
37567
+ if (process.stdin.isTTY)
37568
+ return;
37569
+ error(`${flag} is required when stdin is not a TTY (no prompt can run). Pass it, or run from a terminal.`);
37570
+ process.exit(1);
37571
+ }
37111
37572
  async function promptFor(_name, opts) {
37112
- const runtime = opts.runtime ?? await select3({
37113
- message: "Runtime?",
37114
- choices: RUNTIMES.map((r2) => ({ name: r2, value: r2 }))
37115
- });
37116
- const subgraph = opts.subgraph ?? await input2({
37117
- message: "Subgraph name (must already be deployed):",
37118
- validate: (v2) => v2.trim().length > 0 ? true : "required"
37119
- });
37120
- const table = opts.table ?? await input2({
37121
- message: "Table to subscribe to:",
37122
- validate: (v2) => v2.trim().length > 0 ? true : "required"
37123
- });
37124
- const url = opts.url ?? await input2({
37125
- message: runtime === "inngest" ? "Inngest event endpoint URL (e.g. https://inn.gs/e/<KEY> or http://localhost:8288/e/<KEY>):" : runtime === "trigger" ? "Trigger.dev task URL (e.g. https://api.trigger.dev/api/v1/tasks/<TASK_ID>/trigger):" : runtime === "cloudflare" ? "Cloudflare Workflows instances URL:" : "Your HTTP receiver URL (e.g. https://yourapp.example/webhook):",
37126
- validate: (v2) => v2.startsWith("http://") || v2.startsWith("https://") ? true : "must be http(s) URL"
37127
- });
37573
+ let runtime = resolveRuntime(opts);
37574
+ if (runtime === undefined) {
37575
+ requireTtyFor("--runtime");
37576
+ runtime = await select3({
37577
+ message: "Runtime?",
37578
+ choices: RUNTIMES.map((r2) => ({ name: r2, value: r2 }))
37579
+ });
37580
+ }
37581
+ let subgraph = opts.subgraph;
37582
+ if (subgraph === undefined) {
37583
+ requireTtyFor("--subgraph");
37584
+ subgraph = await input2({
37585
+ message: "Subgraph name (must already be deployed):",
37586
+ validate: (v2) => v2.trim().length > 0 ? true : "required"
37587
+ });
37588
+ }
37589
+ let table = opts.table;
37590
+ if (table === undefined) {
37591
+ requireTtyFor("--table");
37592
+ table = await input2({
37593
+ message: "Table to subscribe to:",
37594
+ validate: (v2) => v2.trim().length > 0 ? true : "required"
37595
+ });
37596
+ }
37597
+ let url = opts.url;
37598
+ if (url === undefined) {
37599
+ requireTtyFor("--url");
37600
+ url = await input2({
37601
+ message: runtime === "inngest" ? "Inngest event endpoint URL (e.g. https://inn.gs/e/<KEY> or http://localhost:8288/e/<KEY>):" : runtime === "trigger" ? "Trigger.dev task URL (e.g. https://api.trigger.dev/api/v1/tasks/<TASK_ID>/trigger):" : runtime === "cloudflare" ? "Cloudflare Workflows instances URL:" : "Your HTTP receiver URL (e.g. https://yourapp.example/webhook):",
37602
+ validate: (v2) => v2.startsWith("http://") || v2.startsWith("https://") ? true : "must be http(s) URL"
37603
+ });
37604
+ }
37128
37605
  return { runtime, subgraph, table, url };
37129
37606
  }
37130
37607
  function buildSubscriptionAuthConfig(authToken) {
@@ -37136,6 +37613,27 @@ function buildSubscriptionAuthConfig(authToken) {
37136
37613
  }
37137
37614
  return { authType: "bearer", token };
37138
37615
  }
37616
+ function writeSigningSecretEnv(targetDir, signingSecret) {
37617
+ const envTarget = join6(targetDir, ".env");
37618
+ const envExample = join6(targetDir, ".env.example");
37619
+ if (existsSync4(envExample) && !existsSync4(envTarget)) {
37620
+ copyFileSync(envExample, envTarget);
37621
+ chmodSync(envTarget, 384);
37622
+ }
37623
+ if (!existsSync4(envTarget)) {
37624
+ writeFileSync4(envTarget, `SIGNING_SECRET=${signingSecret}
37625
+ `, {
37626
+ mode: 384
37627
+ });
37628
+ } else {
37629
+ const cur = readFileSync5(envTarget, "utf8");
37630
+ writeFileSync4(envTarget, cur.match(/^SIGNING_SECRET=/m) ? cur.replace(/^SIGNING_SECRET=.*/m, `SIGNING_SECRET=${signingSecret}`) : `${cur}
37631
+ SIGNING_SECRET=${signingSecret}
37632
+ `, { mode: 384 });
37633
+ chmodSync(envTarget, 384);
37634
+ }
37635
+ return envTarget;
37636
+ }
37139
37637
  async function createSubscription(name, opts) {
37140
37638
  if (opts.runtime && !RUNTIMES.includes(opts.runtime)) {
37141
37639
  error(`Unknown --runtime "${opts.runtime}". Valid: ${RUNTIMES.join(", ")}`);
@@ -37167,7 +37665,7 @@ async function createSubscription(name, opts) {
37167
37665
  }
37168
37666
  const eventName = `${subgraph}.${table}.created`;
37169
37667
  const targetDir = resolve5(process.cwd(), name);
37170
- if (!opts.scaffold === false) {
37668
+ if (opts.scaffold !== false) {
37171
37669
  if (existsSync4(targetDir)) {
37172
37670
  error(`Directory already exists: ${relative(process.cwd(), targetDir)}`);
37173
37671
  process.exit(1);
@@ -37214,21 +37712,8 @@ async function createSubscription(name, opts) {
37214
37712
  subscriptionStatus = created?.status;
37215
37713
  } catch {}
37216
37714
  }
37217
- if (signingSecret && !opts.scaffold === false) {
37218
- const envTarget = join6(targetDir, ".env");
37219
- const envExample = join6(targetDir, ".env.example");
37220
- if (existsSync4(envExample) && !existsSync4(envTarget)) {
37221
- copyFileSync(envExample, envTarget);
37222
- }
37223
- if (!existsSync4(envTarget)) {
37224
- writeFileSync4(envTarget, `SIGNING_SECRET=${signingSecret}
37225
- `);
37226
- } else {
37227
- const cur = readFileSync5(envTarget, "utf8");
37228
- writeFileSync4(envTarget, cur.match(/^SIGNING_SECRET=/m) ? cur.replace(/^SIGNING_SECRET=.*/m, `SIGNING_SECRET=${signingSecret}`) : `${cur}
37229
- SIGNING_SECRET=${signingSecret}
37230
- `);
37231
- }
37715
+ if (signingSecret && opts.scaffold !== false) {
37716
+ const envTarget = writeSigningSecretEnv(targetDir, signingSecret);
37232
37717
  success(`Signing secret written to ${relative(process.cwd(), envTarget)}`);
37233
37718
  } else if (signingSecret && opts.scaffold === false) {
37234
37719
  info(`Signing secret (store securely): ${signingSecret}`);
@@ -37301,7 +37786,7 @@ async function getSubscriptionClient() {
37301
37786
  return new SecondLayer3({ baseUrl: apiUrl, apiKey: ephemeralKey });
37302
37787
  }
37303
37788
  function addSubscriptionScaffold(parent, commandSpec, opts) {
37304
- parent.command(commandSpec).description(opts.description).option("-r, --runtime <runtime>", "inngest | trigger | cloudflare | node").option("-s, --subgraph <name>", "Subgraph to subscribe to").option("-t, --table <name>", "Table to subscribe to").option("-u, --url <url>", "Webhook URL").option("--auth-token <token>", "Bearer token for receiver API auth").option("--filter <kv...>", "Filter as key=value (supports .eq/.neq/.gt/.gte/.lt/.lte suffixes)").option("--trigger <json...>", `Chain-subscription trigger as JSON (repeatable), e.g. '{"type":"sbtc_deposit"}'`).option("--triggers-file <path>", "Chain-subscription triggers as a JSON array file").option("--format <format>", "Chain mode delivery format (default standard-webhooks)").option("--skip-api", "Copy template only, don't call the API").option("--no-scaffold", "Skip the local runtime template directory (webhook-only setups)").addHelpText("after", `
37789
+ parent.command(commandSpec).description(opts.description).option("-r, --runtime <runtime>", "inngest | trigger | cloudflare | node (default: node once -s/-t/-u or --no-scaffold is given; prompts only in a terminal with no flags)").option("-s, --subgraph <name>", "Subgraph to subscribe to").option("-t, --table <name>", "Table to subscribe to").option("-u, --url <url>", "Webhook URL").option("--auth-token <token>", "Bearer token for receiver API auth; a flag lands in shell history and ps, so set it in the receiver's env and pass it here only for a throwaway token").option("--filter <kv...>", "Filter as key=value (supports .eq/.neq/.gt/.gte/.lt/.lte suffixes)").option("--trigger <json...>", `Chain-subscription trigger as JSON (repeatable), e.g. '{"type":"sbtc_deposit"}'`).option("--triggers-file <path>", "Chain-subscription triggers as a JSON array file").option("--format <format>", "Chain mode delivery format (default standard-webhooks)").option("--skip-api", "Copy template only, don't call the API").option("--no-scaffold", "Skip the local runtime template directory (webhook-only setups)").addHelpText("after", `
37305
37790
  Examples:
37306
37791
  $ ${opts.examplePrefix} my-sub -s my-graph -t transfers -u https://example.com/webhook
37307
37792
  $ ${opts.examplePrefix} my-sub -s my-graph -t balances -r inngest --filter amount.gte=1000
@@ -37668,28 +38153,10 @@ function printDead(rows) {
37668
38153
  ])));
37669
38154
  }
37670
38155
  async function confirmOrExit(message, yes) {
37671
- if (yes)
37672
- return true;
37673
- if (!process.stdin.isTTY) {
37674
- error("Interactive prompt unavailable (stdin is not a TTY). Re-run with -y to skip confirmation.");
37675
- process.exit(1);
37676
- }
37677
- let ok = false;
37678
- try {
37679
- ok = await confirm4({ message });
37680
- } catch (promptErr) {
37681
- const m2 = promptErr instanceof Error ? promptErr.message : String(promptErr);
37682
- if (m2.includes("ExitPromptError") || m2.includes("force closed")) {
37683
- error("Interactive prompt unavailable. Re-run with -y to skip confirmation.");
37684
- process.exit(1);
37685
- }
37686
- throw promptErr;
37687
- }
37688
- if (!ok) {
38156
+ const ok = await confirmDestructive({ message, yes });
38157
+ if (!ok)
37689
38158
  info("Cancelled");
37690
- return false;
37691
- }
37692
- return true;
38159
+ return ok;
37693
38160
  }
37694
38161
  function registerSubscriptionsCommand(program2) {
37695
38162
  const subscriptions = program2.command("subscriptions").alias("subs").description("Manage subgraph table subscriptions");
@@ -37732,7 +38199,7 @@ ${data.length} subscription(s) total`));
37732
38199
  handleApiError(err, "get subscription");
37733
38200
  }
37734
38201
  });
37735
- subscriptions.command("update <idOrName>").description("Update subscription config").option("--name <name>", "Rename subscription").option("--url <url>", "Webhook URL").option("--auth-token <token>", "Set bearer token auth config").option("--format <format>", "standard-webhooks | inngest | trigger | cloudflare | cloudevents | raw").option("--runtime <runtime>", "inngest | trigger | cloudflare | node | none").option("--filter <kv...>", "Filter as key=value (supports .eq/.neq/.gt/.gte/.lt/.lte suffixes)").option("--clear-filter", "Replace filter with {}").option("--max-retries <n>", "Maximum delivery retries").option("--timeout-ms <n>", "Delivery timeout in milliseconds").option("--concurrency <n>", "Per-subscription delivery concurrency").option("--json", "Output as JSON").addHelpText("after", `
38202
+ subscriptions.command("update <idOrName>").description("Update subscription config").option("--name <name>", "Rename subscription").option("--url <url>", "Webhook URL").option("--auth-token <token>", "Set bearer token auth config; a flag lands in shell history and ps, so pass it here only for a throwaway token").option("--format <format>", "standard-webhooks | inngest | trigger | cloudflare | cloudevents | raw").option("--runtime <runtime>", "inngest | trigger | cloudflare | node | none").option("--filter <kv...>", "Filter as key=value (supports .eq/.neq/.gt/.gte/.lt/.lte suffixes)").option("--clear-filter", "Replace filter with {}").option("--max-retries <n>", "Maximum delivery retries").option("--timeout-ms <n>", "Delivery timeout in milliseconds").option("--concurrency <n>", "Per-subscription delivery concurrency").option("--json", "Output as JSON").addHelpText("after", `
37736
38203
  Examples:
37737
38204
  $ secondlayer subscriptions update my-sub --url https://example.com/hook
37738
38205
  $ secondlayer subscriptions update my-sub --filter amount.gte=1000 --max-retries 5
@@ -37917,7 +38384,7 @@ Examples:
37917
38384
  handleApiError(err, "diagnose subscription");
37918
38385
  }
37919
38386
  });
37920
- subscriptions.command("test <idOrName>").description("Inspect a webhook fixture; --post sends a logged test delivery via the server (--local POSTs client-side instead)").option("--signing-secret <secret>", "Signing secret override (--local only)").option("--post", "Send a test delivery via the server (logged, all formats)").option("--local", "With --post: POST the fixture client-side (not logged)").option("--json", "Output as JSON").action(async (idOrName, options3) => {
38387
+ subscriptions.command("test <idOrName>").description("Inspect a webhook fixture; --post sends a logged test delivery via the server (--local POSTs client-side instead)").option("--signing-secret <secret>", "Signing secret override (--local only); prefer SIGNING_SECRET in env, a flag lands in shell history and ps").option("--post", "Send a test delivery via the server (logged, all formats)").option("--local", "With --post: POST the fixture client-side (not logged)").option("--json", "Output as JSON").action(async (idOrName, options3) => {
37921
38388
  try {
37922
38389
  const signingSecret = resolveSigningSecret(options3);
37923
38390
  const client2 = await getSubscriptionClient();
@@ -37944,7 +38411,8 @@ Examples:
37944
38411
  const res = await fetch(detail.url, {
37945
38412
  method: "POST",
37946
38413
  headers: fixture.headers,
37947
- body: fixture.body
38414
+ body: fixture.body,
38415
+ signal: AbortSignal.timeout(15000)
37948
38416
  });
37949
38417
  postResult = {
37950
38418
  status: res.status,
@@ -38418,8 +38886,9 @@ init_subgraphs2();
38418
38886
  // src/commands/streams.ts
38419
38887
  init_output();
38420
38888
  init_resolve_auth();
38421
- import { mkdir as mkdir3, writeFile as writeFile3 } from "node:fs/promises";
38422
- import { dirname as dirname9, join as join10 } from "node:path";
38889
+ import { createHash as createHash3 } from "node:crypto";
38890
+ import { mkdir as mkdir3, open as open2, rename as rename2 } from "node:fs/promises";
38891
+ import { dirname as dirname9, isAbsolute, resolve as resolve6, sep } from "node:path";
38423
38892
  import { createStreamsClient } from "@secondlayer/sdk";
38424
38893
  import { STREAMS_EVENT_TYPES } from "@secondlayer/shared";
38425
38894
  var VALID_TYPES = STREAMS_EVENT_TYPES;
@@ -38441,6 +38910,48 @@ function dumpsClient(dumpsUrl) {
38441
38910
  dumpsBaseUrl
38442
38911
  });
38443
38912
  }
38913
+ function resolveDumpDest(to, filePath) {
38914
+ const root = resolve6(to);
38915
+ const segments = filePath.split(/[\\/]/);
38916
+ if (filePath.length === 0 || isAbsolute(filePath) || filePath.includes("\\") || segments.some((s2) => s2 === "..")) {
38917
+ throw new Error(`refusing dump path "${filePath}": paths must be relative and stay under ${root}`);
38918
+ }
38919
+ const dest = resolve6(root, filePath);
38920
+ if (!dest.startsWith(root + sep)) {
38921
+ throw new Error(`refusing dump path "${filePath}": resolves outside ${root}`);
38922
+ }
38923
+ return dest;
38924
+ }
38925
+ async function downloadDumpTo(file, dest, fetchImpl, url) {
38926
+ const res = await fetchImpl(url);
38927
+ if (!res.ok || !res.body) {
38928
+ throw new Error(`could not download dump ${file.path} (${res.status})`);
38929
+ }
38930
+ await mkdir3(dirname9(dest), { recursive: true });
38931
+ const part = `${dest}.part`;
38932
+ const hash = createHash3("sha256");
38933
+ let bytes = 0;
38934
+ const handle = await open2(part, "w");
38935
+ try {
38936
+ const reader = res.body.getReader();
38937
+ for (;; ) {
38938
+ const { value, done } = await reader.read();
38939
+ if (done)
38940
+ break;
38941
+ hash.update(value);
38942
+ bytes += value.byteLength;
38943
+ await handle.write(value);
38944
+ }
38945
+ } finally {
38946
+ await handle.close();
38947
+ }
38948
+ const digest = hash.digest("hex");
38949
+ if (digest !== file.sha256) {
38950
+ throw new Error(`dump ${file.path} sha256 mismatch (expected ${file.sha256}, got ${digest}); partial file left at ${part}`);
38951
+ }
38952
+ await rename2(part, dest);
38953
+ return bytes;
38954
+ }
38444
38955
  function parseTypes(value, flag = "--types") {
38445
38956
  if (!value)
38446
38957
  return;
@@ -38469,6 +38980,29 @@ function parseLimit(value) {
38469
38980
  }
38470
38981
  return n3;
38471
38982
  }
38983
+ function parseMaxPages(value) {
38984
+ if (value === undefined)
38985
+ return;
38986
+ const n3 = Number.parseInt(value, 10);
38987
+ if (!Number.isFinite(n3) || n3 < 1 || String(n3) !== value.trim()) {
38988
+ throw new Error("--max-pages must be a positive integer");
38989
+ }
38990
+ return n3;
38991
+ }
38992
+ function consumeHandlers(io) {
38993
+ return {
38994
+ onBatch: (events, _envelope, ctx) => {
38995
+ for (const e2 of events)
38996
+ io.stdout(JSON.stringify(e2));
38997
+ if (ctx.cursor)
38998
+ io.stderr(`# next_cursor=${ctx.cursor}`);
38999
+ },
39000
+ onReorg: (reorg, ctx) => {
39001
+ io.stdout(JSON.stringify({ kind: "reorg", ...reorg }));
39002
+ io.stderr(`# reorg at ${reorg.fork_point_height}; rewinding to ${ctx.cursor}`);
39003
+ }
39004
+ };
39005
+ }
38472
39006
  function parseHeight(value, name) {
38473
39007
  if (value === undefined)
38474
39008
  return;
@@ -38528,8 +39062,14 @@ Examples:
38528
39062
  $ secondlayer streams consume --not-types print --sender SP1...,SP2... --batch-size 500 --max-pages 10`).action(async (options3) => {
38529
39063
  try {
38530
39064
  const batchSize = parseLimit(options3.batchSize) ?? 100;
38531
- const maxPages = options3.maxPages ? Number.parseInt(options3.maxPages, 10) : undefined;
38532
- note("# streaming events to stdout (jsonl); next_cursor printed to stderr");
39065
+ const maxPages = parseMaxPages(options3.maxPages);
39066
+ note('# streaming events to stdout (jsonl); reorgs appear inline as {"kind":"reorg"}; next_cursor printed to stderr');
39067
+ const handlers = consumeHandlers({
39068
+ stdout: (line) => process.stdout.write(`${line}
39069
+ `),
39070
+ stderr: (line) => process.stderr.write(`${line}
39071
+ `)
39072
+ });
38533
39073
  await client2().events.consume({
38534
39074
  fromCursor: options3.cursor,
38535
39075
  types: parseTypes(options3.types),
@@ -38540,17 +39080,8 @@ Examples:
38540
39080
  batchSize,
38541
39081
  mode: "tail",
38542
39082
  maxPages,
38543
- onBatch: (events2, envelope) => {
38544
- for (const e2 of events2) {
38545
- process.stdout.write(`${JSON.stringify(e2)}
38546
- `);
38547
- }
38548
- if (envelope.next_cursor) {
38549
- process.stderr.write(`# next_cursor=${envelope.next_cursor}
38550
- `);
38551
- }
38552
- return envelope.next_cursor;
38553
- }
39083
+ onBatch: handlers.onBatch,
39084
+ onReorg: handlers.onReorg
38554
39085
  });
38555
39086
  } catch (err) {
38556
39087
  error(err instanceof Error ? err.message : String(err));
@@ -38582,11 +39113,9 @@ Examples:
38582
39113
  }
38583
39114
  note(`# downloading ${files.length} file(s) to ${options3.to}`);
38584
39115
  for (const file of files) {
38585
- const bytes = await dumps.download(file);
38586
- const dest = join10(options3.to, file.path);
38587
- await mkdir3(dirname9(dest), { recursive: true });
38588
- await writeFile3(dest, bytes);
38589
- process.stderr.write(`# ${file.path} (${file.row_count} rows, ${bytes.byteLength} bytes)
39116
+ const dest = resolveDumpDest(options3.to, file.path);
39117
+ const bytes = await downloadDumpTo(file, dest, fetch, dumps.fileUrl(file));
39118
+ process.stderr.write(`# ${file.path} (${file.row_count} rows, ${bytes} bytes)
38590
39119
  `);
38591
39120
  }
38592
39121
  writeData(JSON.stringify({
@@ -39006,7 +39535,7 @@ function markFrozen(cmd) {
39006
39535
  init_config2();
39007
39536
  init_dev_state();
39008
39537
  init_output();
39009
- import { confirm as confirm5 } from "@inquirer/prompts";
39538
+ import { confirm as confirm4 } from "@inquirer/prompts";
39010
39539
  import { getDb, sql } from "@secondlayer/shared/db";
39011
39540
  import {
39012
39541
  countMissingBlocks,
@@ -39226,7 +39755,7 @@ async function resetDatabase(skipConfirm) {
39226
39755
  console.log(dim("Note: Subgraph configurations will be preserved."));
39227
39756
  console.log("");
39228
39757
  if (!skipConfirm) {
39229
- const confirmed = await confirm5({
39758
+ const confirmed = await confirm4({
39230
39759
  message: "Are you sure you want to reset the database?",
39231
39760
  default: false
39232
39761
  });
@@ -39263,7 +39792,7 @@ async function resyncDatabase(skipConfirm, backfill) {
39263
39792
  console.log(dim("Note: Subgraph configurations will be preserved."));
39264
39793
  console.log("");
39265
39794
  if (!skipConfirm) {
39266
- const confirmed = await confirm5({
39795
+ const confirmed = await confirm4({
39267
39796
  message: "Are you sure you want to resync?",
39268
39797
  default: false
39269
39798
  });
@@ -39834,9 +40363,9 @@ init_output();
39834
40363
 
39835
40364
  // src/lib/project-file.ts
39836
40365
  import { existsSync as existsSync7 } from "node:fs";
39837
- import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4 } from "node:fs/promises";
40366
+ import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
39838
40367
  import { homedir as homedir5 } from "node:os";
39839
- import { dirname as dirname15, join as join13, parse as parsePath2, resolve as resolve11 } from "node:path";
40368
+ import { dirname as dirname15, join as join12, parse as parsePath2, resolve as resolve12 } from "node:path";
39840
40369
  import { z as z4 } from "zod";
39841
40370
  var ProjectFileSchema = z4.object({
39842
40371
  slug: z4.string().min(1)
@@ -39844,11 +40373,11 @@ var ProjectFileSchema = z4.object({
39844
40373
  var FILENAME = "project";
39845
40374
  var DIRNAME = ".secondlayer";
39846
40375
  async function readActiveProject(cwd, globalDefault) {
39847
- let dir = resolve11(cwd);
39848
- const home2 = resolve11(homedir5());
40376
+ let dir = resolve12(cwd);
40377
+ const home2 = resolve12(homedir5());
39849
40378
  const fsRoot = parsePath2(dir).root;
39850
40379
  while (true) {
39851
- const candidate = join13(dir, DIRNAME, FILENAME);
40380
+ const candidate = join12(dir, DIRNAME, FILENAME);
39852
40381
  if (existsSync7(candidate)) {
39853
40382
  try {
39854
40383
  const raw = await readFile4(candidate, "utf8");
@@ -39856,7 +40385,7 @@ async function readActiveProject(cwd, globalDefault) {
39856
40385
  return { slug: parsed.slug, resolvedFrom: candidate };
39857
40386
  } catch {}
39858
40387
  }
39859
- if (existsSync7(join13(dir, ".git")))
40388
+ if (existsSync7(join12(dir, ".git")))
39860
40389
  break;
39861
40390
  if (dir === home2 || dir === fsRoot)
39862
40391
  break;
@@ -39868,57 +40397,50 @@ async function readActiveProject(cwd, globalDefault) {
39868
40397
  if (globalDefault) {
39869
40398
  return {
39870
40399
  slug: globalDefault,
39871
- resolvedFrom: join13(homedir5(), ".secondlayer", "config.json")
40400
+ resolvedFrom: join12(homedir5(), ".secondlayer", "config.json")
39872
40401
  };
39873
40402
  }
39874
40403
  return null;
39875
40404
  }
39876
40405
 
39877
40406
  // src/commands/whoami.ts
39878
- init_resolve_auth();
39879
40407
  function registerWhoamiCommand(program2) {
39880
- program2.command("whoami", { hidden: true }).description("Show the active account, credential source, and project").option("--json", "Output as JSON").action(async (options3) => {
39881
- let auth;
39882
- try {
39883
- auth = await resolveAuth();
39884
- } catch {
39885
- error("Not logged in. Run: secondlayer login");
39886
- process.exit(1);
39887
- }
40408
+ program2.command("whoami", { hidden: true }).description("Show the archive credits account, credential source, and project").option("--json", "Output as JSON").action(async (options3) => {
40409
+ const creditsUrl = resolveArchiveOpsUrl();
40410
+ const { source } = await resolveArchiveOpsBearer();
39888
40411
  let account;
39889
40412
  try {
39890
- account = await httpPlatform("/api/accounts/me");
40413
+ account = await httpArchiveOps("/api/accounts/me");
39891
40414
  } catch (err) {
39892
- if (err instanceof CliHttpError && err.code === "SESSION_EXPIRED") {
39893
- error("Session expired. Run: secondlayer login");
40415
+ if (err instanceof CliHttpError) {
40416
+ error(err.message);
39894
40417
  process.exit(1);
39895
40418
  }
39896
40419
  throw err;
39897
40420
  }
39898
40421
  const config = await loadConfig2();
39899
40422
  const active = await readActiveProject(process.cwd(), config.defaultProject);
39900
- const authSource = auth.fromEnv ? "API key (env)" : "session";
40423
+ const authSource = source === "env" ? "API key (env)" : `session (${ARCHIVE_LOGIN_COMMAND})`;
39901
40424
  output({
39902
40425
  json: options3.json,
39903
40426
  data: {
39904
40427
  email: account.email,
39905
- apiUrl: auth.apiUrl,
40428
+ creditsUrl,
40429
+ apiUrl: resolveApiUrl(),
39906
40430
  authSource,
39907
40431
  project: active ? { slug: active.slug, source: active.resolvedFrom } : null
39908
40432
  },
39909
40433
  human: () => {
39910
40434
  const rows = [];
39911
40435
  rows.push(["Email", account.email]);
39912
- rows.push(["API URL", auth.apiUrl]);
40436
+ rows.push(["Credits API", creditsUrl]);
40437
+ rows.push(["Instance API", resolveApiUrl()]);
39913
40438
  rows.push(["Auth", dim(authSource)]);
39914
40439
  if (active) {
39915
40440
  rows.push(["Project", active.slug]);
39916
40441
  rows.push(["Project source", dim(active.resolvedFrom)]);
39917
40442
  } else {
39918
- rows.push([
39919
- "Project",
39920
- dim("(none — run `secondlayer project use <slug>`)")
39921
- ]);
40443
+ rows.push(["Project", dim("(none)")]);
39922
40444
  }
39923
40445
  console.log(formatKeyValue(rows));
39924
40446
  }
@@ -39933,25 +40455,34 @@ init_login();
39933
40455
  init_http();
39934
40456
  init_output();
39935
40457
  init_session();
39936
- function registerLogoutCommand(program2) {
39937
- program2.command("logout", { hidden: true }).description("Log out and revoke the local session").action(async () => {
39938
- const session = await readSession();
39939
- if (!session) {
39940
- info("Not logged in.");
39941
- return;
39942
- }
39943
- try {
39944
- await httpPlatform("/api/auth/logout", { method: "POST" });
39945
- } catch (err) {
39946
- if (err instanceof CliHttpError) {
39947
- warn(`Server logout failed (${err.code}) — clearing local session anyway`);
39948
- }
40458
+ init_login();
40459
+ async function runLogout(opts = {}) {
40460
+ const target = loginTarget(opts);
40461
+ const session = await readSession(target);
40462
+ if (!session) {
40463
+ info(`Not logged in (${target}).`);
40464
+ return;
40465
+ }
40466
+ try {
40467
+ await httpAt(target, "/api/auth/logout", {
40468
+ method: "POST",
40469
+ bearer: session.token
40470
+ });
40471
+ } catch (err) {
40472
+ if (err instanceof CliHttpError) {
40473
+ warn(`Server logout failed (${err.code}); clearing the local session anyway`);
39949
40474
  }
39950
- await clearSession();
39951
- success("Logged out.");
39952
- });
40475
+ }
40476
+ await clearSession(target);
40477
+ success(`Logged out (${target}).`);
40478
+ }
40479
+ function registerLogoutCommand(program2) {
40480
+ program2.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 }));
39953
40481
  }
39954
40482
  // src/commands/verify.ts
40483
+ init_archive_reference();
40484
+ init_output();
40485
+ init_resolve_auth();
39955
40486
  import {
39956
40487
  compareRangeDigests,
39957
40488
  computeRangeDigest
@@ -39966,139 +40497,6 @@ import {
39966
40497
  reportVerify
39967
40498
  } from "@secondlayer/shared/coverage";
39968
40499
  import { getDb as getDb2 } from "@secondlayer/shared/db";
39969
-
39970
- // src/lib/archive-reference.ts
39971
- import { createHash as createHash2 } from "node:crypto";
39972
- import { readFile as readFile5 } from "node:fs/promises";
39973
- import { dirname as dirname16, join as join14, resolve as resolve12 } from "node:path";
39974
- import {
39975
- checkKeyTrust,
39976
- publicKeyFor,
39977
- verifyRegistry
39978
- } from "@secondlayer/shared/archive/key-registry";
39979
- import { verifyStreamsBulkManifestSignature } from "@secondlayer/shared/streams-bulk-manifest";
39980
- function isLatestPointer(value) {
39981
- const pointer = value;
39982
- return typeof pointer.snapshot_path === "string" && (value.partitions === undefined || value.partitions.length === 0);
39983
- }
39984
- async function fetchJson(url) {
39985
- const response2 = await fetch(url, { signal: AbortSignal.timeout(30000) });
39986
- if (!response2.ok) {
39987
- throw new Error(`could not fetch manifest (${response2.status} ${response2.statusText})`);
39988
- }
39989
- return response2.json();
39990
- }
39991
- async function loadReference(source) {
39992
- const isRemote = /^https?:\/\//.test(source);
39993
- let manifest;
39994
- let root;
39995
- if (isRemote) {
39996
- manifest = await fetchJson(source);
39997
- const url = new URL(source);
39998
- url.pathname = url.pathname.replace(/\/snapshots\/[^/]+$/, "").replace(/\/[^/]+\.json$/, "");
39999
- root = url.toString();
40000
- } else {
40001
- const path11 = resolve12(source);
40002
- manifest = JSON.parse(await readFile5(path11, "utf8"));
40003
- root = isLatestPointer(manifest) ? dirname16(path11) : dirname16(dirname16(path11));
40004
- }
40005
- if (!isLatestPointer(manifest)) {
40006
- return { manifest, origin: source, root, isRemote };
40007
- }
40008
- const pointer = manifest;
40009
- const snapshotPath = pointer.snapshot_path;
40010
- const resolved = isRemote ? await fetchJson(`${root.replace(/\/$/, "")}/${snapshotPath}`) : JSON.parse(await readFile5(join14(root, snapshotPath), "utf8"));
40011
- if (pointer.snapshot_digest) {
40012
- const digest = createHash2("sha256").update(JSON.stringify((({ signature: _s, key_id: _k, ...rest }) => rest)(resolved))).digest("hex");
40013
- if (digest !== pointer.snapshot_digest) {
40014
- throw new Error(`pointer/snapshot mismatch: latest.json names ${pointer.snapshot_digest}, resolved manifest hashes to ${digest}`);
40015
- }
40016
- }
40017
- return { manifest: resolved, origin: source, root, isRemote };
40018
- }
40019
- function checkSignature(manifest, publicKeyPem, insecure) {
40020
- if (insecure)
40021
- return { verified: false, reason: "signature check skipped" };
40022
- if (!manifest.signature) {
40023
- return { verified: false, reason: "manifest carries no signature" };
40024
- }
40025
- if (!publicKeyPem) {
40026
- return {
40027
- verified: false,
40028
- reason: "no public key available to verify with"
40029
- };
40030
- }
40031
- try {
40032
- const verified = verifyStreamsBulkManifestSignature(manifest, publicKeyPem);
40033
- return {
40034
- verified,
40035
- reason: verified ? undefined : "signature did not verify"
40036
- };
40037
- } catch (err) {
40038
- return {
40039
- verified: false,
40040
- reason: err instanceof Error ? err.message : "signature check failed"
40041
- };
40042
- }
40043
- }
40044
- var HOSTED_API = "https://api.secondlayer.tools";
40045
- async function resolvePublicKey(explicitPem, apiUrl) {
40046
- if (explicitPem)
40047
- return explicitPem;
40048
- try {
40049
- const response2 = await fetch(`${apiUrl.replace(/\/$/, "")}/public/streams/signing-key`, { signal: AbortSignal.timeout(1e4) });
40050
- if (!response2.ok)
40051
- return;
40052
- const body = await response2.json();
40053
- return body.public_key_pem;
40054
- } catch {
40055
- return;
40056
- }
40057
- }
40058
- async function resolveArchivePublicKey(input4) {
40059
- if (input4.explicitPem)
40060
- return input4.explicitPem;
40061
- if (input4.envPem)
40062
- return input4.envPem;
40063
- if (input4.allowHostedApi) {
40064
- return resolvePublicKey(undefined, HOSTED_API);
40065
- }
40066
- return;
40067
- }
40068
- async function fetchViaGate(gate, partition, retried) {
40069
- const url = await gate.getUrl(partition.path, retried ? { forceRefresh: true } : undefined);
40070
- const response2 = await fetch(url, { signal: AbortSignal.timeout(300000) });
40071
- if (response2.status === 403 && !retried) {
40072
- return fetchViaGate(gate, partition, true);
40073
- }
40074
- if (!response2.ok) {
40075
- throw new Error(`could not fetch ${partition.path} (${response2.status} ${response2.statusText})`);
40076
- }
40077
- return Buffer.from(await response2.arrayBuffer());
40078
- }
40079
- async function fetchVerifiedPartition(reference, partition, gate) {
40080
- let bytes;
40081
- if (gate) {
40082
- bytes = await fetchViaGate(gate, partition, false);
40083
- } else if (reference.isRemote) {
40084
- const url = `${reference.root.replace(/\/$/, "")}/${partition.path}`;
40085
- const response2 = await fetch(url, { signal: AbortSignal.timeout(300000) });
40086
- if (!response2.ok) {
40087
- throw new Error(`could not fetch ${partition.path} (${response2.status} ${response2.statusText})`);
40088
- }
40089
- bytes = Buffer.from(await response2.arrayBuffer());
40090
- } else {
40091
- bytes = await readFile5(join14(reference.root, partition.path));
40092
- }
40093
- const digest = createHash2("sha256").update(bytes).digest("hex");
40094
- if (digest !== partition.sha256) {
40095
- throw new Error(`${partition.path} failed verification: expected ${partition.sha256}, got ${digest}`);
40096
- }
40097
- return bytes;
40098
- }
40099
-
40100
- // src/commands/verify.ts
40101
- init_output();
40102
40500
  var VERIFY_EXIT = {
40103
40501
  CLEAN: 0,
40104
40502
  DIVERGED: 1,
@@ -40121,7 +40519,7 @@ function statusLabel(status2) {
40121
40519
  return yellow("missing");
40122
40520
  }
40123
40521
  function registerVerifyCommand(program2) {
40124
- program2.command("verify").description("Compare local chain data against a signed archive (read-only; nothing is uploaded)").argument("[target]", "all | raw | decode:<name> | subgraph:<name>", "raw").option("--quick", "coverage/identity only (default)").option("--deep", "include semantic / scratch replay where available").option("--anchor", "require a verified archive signature").requiredOption("--against <manifest>", "archive manifest: an https URL or a local file path").option("--from-block <n>", "first height to check").option("--to-block <n>", "last height to check").option("--counts", "also compare transaction/event row counts (slower)").option("--semantic", "also recompute per-partition semantic digests locally and compare (slow: full re-stream)").option("--public-key <pem>", "pin the signing key instead of fetching it").option("--insecure", "skip the manifest signature check (not recommended)").option("--json", "Output as JSON").addHelpText("after", `
40522
+ program2.command("verify").description("Compare local chain data against a signed archive (read-only; nothing is uploaded)").argument("[target]", "all | raw | decode:<name> | subgraph:<name>", "raw").option("--quick", "coverage/identity only (default)").option("--deep", "include semantic / scratch replay where available").option("--anchor", "require a verified archive signature").requiredOption("--against <manifest>", "archive manifest: an https URL or a local file path").option("--from-block <n>", "first height to check").option("--to-block <n>", "last height to check").option("--counts", "also compare transaction/event row counts (slower)").option("--semantic", "also recompute per-partition semantic digests locally and compare (slow: full re-stream)").option("--public-key <pem>", "pin a signing key; default is the archive key built into this release").option("--insecure", "skip the manifest signature check (not recommended)").option("--json", "Output as JSON").addHelpText("after", `
40125
40523
  Examples:
40126
40524
  $ secondlayer verify --against https://archive.secondlayer.tools/.../snapshots/<digest>.json
40127
40525
  $ secondlayer verify raw --against ./snapshot.json --from-block 8000000 --to-block 8499999
@@ -40141,8 +40539,14 @@ Exit codes:
40141
40539
  if (fromBlock !== undefined && toBlock !== undefined && toBlock < fromBlock) {
40142
40540
  throw new Error("--to-block must be >= --from-block");
40143
40541
  }
40144
- const { manifest, origin } = await loadReference(opts.against);
40145
- const publicKey = await resolvePublicKey(opts.publicKey, process.env.SL_API_URL ?? "https://api.secondlayer.tools");
40542
+ const publicKey = await resolveArchivePublicKey({
40543
+ explicitPem: opts.publicKey,
40544
+ envPem: process.env.ARCHIVE_SIGNING_PUBLIC_KEY ?? process.env.STREAMS_SIGNING_PUBLIC_KEY,
40545
+ allowHostedApi: !isOssMode()
40546
+ });
40547
+ const { manifest, origin } = await loadReference(opts.against, {
40548
+ publicKeyPem: publicKey
40549
+ });
40146
40550
  const signature = checkSignature(manifest, publicKey, !!opts.insecure);
40147
40551
  if (!signature.verified && !opts.insecure) {
40148
40552
  printError(`Cannot trust the reference: ${signature.reason}.`, {
@@ -40297,24 +40701,27 @@ Exit codes:
40297
40701
  });
40298
40702
  process.exit(diverged.length === 0 ? VERIFY_EXIT.CLEAN : VERIFY_EXIT.DIVERGED);
40299
40703
  } catch (err) {
40300
- printError(err instanceof Error ? err.message : String(err), {
40301
- hint: "Set DATABASE_URL to the instance you want to verify."
40302
- });
40704
+ const message = err instanceof Error ? err.message : String(err);
40705
+ const hint = /pointer|leave the archive root/.test(message) ? "The archive pointer failed its integrity check. Pass --against the snapshot URL directly, and report this if the pointer is the official latest.json." : /could not fetch/.test(message) ? "Check the archive URL and your network connection." : "Set DATABASE_URL to the instance you want to verify.";
40706
+ printError(message, { hint });
40303
40707
  process.exit(VERIFY_EXIT.UNANCHORED);
40304
40708
  }
40305
40709
  });
40306
40710
  }
40307
40711
  // src/commands/repair.ts
40308
- import { unlink as unlink2, writeFile as writeFile5 } from "node:fs/promises";
40309
- import { tmpdir as tmpdir2 } from "node:os";
40712
+ import { unlink as unlink3, writeFile as writeFile5 } from "node:fs/promises";
40713
+ import { tmpdir as tmpdir3 } from "node:os";
40310
40714
  import { join as join15 } from "node:path";
40311
- import { ParquetReader } from "@dsnp/parquetjs";
40312
- import { confirm as confirm7 } from "@inquirer/prompts";
40715
+ import { ParquetReader as ParquetReader2 } from "@dsnp/parquetjs";
40716
+ import {
40717
+ copyStatement,
40718
+ writeRowsToCopyStream
40719
+ } from "@secondlayer/shared/archive/copy-loader";
40313
40720
  import {
40314
40721
  compareRangeDigests as compareRangeDigests2,
40315
40722
  computeRangeDigest as computeRangeDigest2
40316
40723
  } from "@secondlayer/shared/archive/range-digest";
40317
- import { getDb as getDb3, sql as sql2 } from "@secondlayer/shared/db";
40724
+ import { getDb as getDb3, getRawClient } from "@secondlayer/shared/db";
40318
40725
 
40319
40726
  // src/lib/archive-gate.ts
40320
40727
  init_http();
@@ -40373,10 +40780,18 @@ function formatQuoteValue(quote, flow) {
40373
40780
  return `${quote.partitions} partitions ≈ $${quote.usd} · balance ${balance}`;
40374
40781
  }
40375
40782
  function shouldPromptForGatedFetch(opts) {
40376
- return !opts.yes && !opts.json;
40783
+ return !opts.yes;
40784
+ }
40785
+ function confirmationRequiredPayload(quote) {
40786
+ return {
40787
+ code: "CONFIRMATION_REQUIRED",
40788
+ message: "Nothing was fetched. Re-run with -y to approve this fetch; --json never stands in for -y.",
40789
+ quote
40790
+ };
40377
40791
  }
40378
40792
  var FETCH_BATCH_SIZE = 16;
40379
- function createGatedFetcher(paths, flow, deps = defaultDeps) {
40793
+ var PRESIGN_REFRESH_MARGIN_MS = 60000;
40794
+ function createGatedFetcher(paths, flow, deps = defaultDeps, now = Date.now) {
40380
40795
  const cache = new Map;
40381
40796
  const batchOf = new Map;
40382
40797
  for (const [index, path11] of paths.entries()) {
@@ -40386,21 +40801,29 @@ function createGatedFetcher(paths, flow, deps = defaultDeps) {
40386
40801
  const inFlight = new Map;
40387
40802
  async function fetchAndCache(batchPaths) {
40388
40803
  const body = await deps.httpArchiveOps("/api/archive/fetch", { method: "POST", body: { paths: batchPaths, flow } });
40389
- for (const u3 of body.urls)
40390
- cache.set(u3.path, u3.url);
40804
+ for (const u3 of body.urls) {
40805
+ const parsed = Date.parse(u3.expires_at);
40806
+ cache.set(u3.path, {
40807
+ url: u3.url,
40808
+ expiresAt: Number.isFinite(parsed) ? parsed : null
40809
+ });
40810
+ }
40811
+ }
40812
+ async function reissue(path11, why) {
40813
+ if (process.env.DEBUG) {
40814
+ console.error(`[archive-gate] re-issuing ${why} URL for ${path11}`);
40815
+ }
40816
+ await fetchAndCache([path11]);
40817
+ const fresh = cache.get(path11);
40818
+ if (!fresh) {
40819
+ throw new Error(`archive gate: no URL returned for ${path11}`);
40820
+ }
40821
+ return fresh.url;
40391
40822
  }
40392
40823
  return {
40393
40824
  async getUrl(path11, opts) {
40394
40825
  if (opts?.forceRefresh) {
40395
- if (process.env.DEBUG) {
40396
- console.error(`[archive-gate] re-issuing expired URL for ${path11}`);
40397
- }
40398
- await fetchAndCache([path11]);
40399
- const fresh = cache.get(path11);
40400
- if (!fresh) {
40401
- throw new Error(`archive gate: no URL returned for ${path11}`);
40402
- }
40403
- return fresh;
40826
+ return reissue(path11, "expired");
40404
40827
  }
40405
40828
  const batchIndex = batchOf.get(path11);
40406
40829
  if (batchIndex === undefined) {
@@ -40417,16 +40840,47 @@ function createGatedFetcher(paths, flow, deps = defaultDeps) {
40417
40840
  }
40418
40841
  await inflight;
40419
40842
  }
40420
- const url = cache.get(path11);
40421
- if (!url)
40843
+ const cached = cache.get(path11);
40844
+ if (!cached) {
40422
40845
  throw new Error(`archive gate: no URL returned for ${path11}`);
40423
- return url;
40846
+ }
40847
+ if (cached.expiresAt !== null && cached.expiresAt - now() < PRESIGN_REFRESH_MARGIN_MS) {
40848
+ return reissue(path11, "expiring");
40849
+ }
40850
+ return cached.url;
40424
40851
  }
40425
40852
  };
40426
40853
  }
40427
40854
 
40428
40855
  // src/commands/repair.ts
40856
+ init_archive_reference();
40429
40857
  init_output();
40858
+
40859
+ // src/lib/parquet-rows.ts
40860
+ import { unlink as unlink2, writeFile as writeFile4 } from "node:fs/promises";
40861
+ import { tmpdir as tmpdir2 } from "node:os";
40862
+ import { join as join14 } from "node:path";
40863
+ import { ParquetReader } from "@dsnp/parquetjs";
40864
+ async function* readPartitionRows(bytes, label) {
40865
+ const path11 = join14(tmpdir2(), `sl-archive-${label}-${process.pid}.parquet`);
40866
+ await writeFile4(path11, bytes);
40867
+ try {
40868
+ const reader = await ParquetReader.openFile(path11);
40869
+ try {
40870
+ const cursor = reader.getCursor();
40871
+ for (let row = await cursor.next();row; row = await cursor.next()) {
40872
+ yield row;
40873
+ }
40874
+ } finally {
40875
+ await reader.close();
40876
+ }
40877
+ } finally {
40878
+ await unlink2(path11).catch(() => {});
40879
+ }
40880
+ }
40881
+
40882
+ // src/commands/repair.ts
40883
+ init_resolve_auth();
40430
40884
  var REPAIR_EXIT = {
40431
40885
  OK: 0,
40432
40886
  DIVERGENCE_REMAINS: 1,
@@ -40442,10 +40896,10 @@ function asOptionalText(value) {
40442
40896
  return value === null || value === undefined ? null : asText(value);
40443
40897
  }
40444
40898
  async function readBlocksPartition(bytes, label) {
40445
- const path11 = join15(tmpdir2(), `sl-repair-${label}-${process.pid}.parquet`);
40899
+ const path11 = join15(tmpdir3(), `sl-repair-${label}-${process.pid}.parquet`);
40446
40900
  await writeFile5(path11, bytes);
40447
40901
  try {
40448
- const reader = await ParquetReader.openFile(path11);
40902
+ const reader = await ParquetReader2.openFile(path11);
40449
40903
  const rows = [];
40450
40904
  try {
40451
40905
  const cursor = reader.getCursor();
@@ -40465,7 +40919,7 @@ async function readBlocksPartition(bytes, label) {
40465
40919
  }
40466
40920
  return rows;
40467
40921
  } finally {
40468
- await unlink2(path11).catch(() => {});
40922
+ await unlink3(path11).catch(() => {});
40469
40923
  }
40470
40924
  }
40471
40925
  function parseHeight3(value, flag) {
@@ -40503,24 +40957,92 @@ async function planRange(db, reference, partition, gate) {
40503
40957
  const extraHeights = [...localByHeight.keys()].sort((a4, b3) => a4 - b3);
40504
40958
  return { fixes, extraHeights };
40505
40959
  }
40506
- async function applyFixes(db, reference, partition, fixes, gate) {
40960
+ var CHILD_DATASETS = ["transactions", "events"];
40961
+ var REPAIR_DATASETS = [
40962
+ "blocks",
40963
+ "transactions",
40964
+ "events"
40965
+ ];
40966
+ function childPartitionFor(partitions, dataset, height) {
40967
+ return partitions.find((p2) => p2.dataset === dataset && p2.from_block <= height && p2.to_block >= height);
40968
+ }
40969
+ function planChildRewrite(partitions, heights) {
40970
+ const missing = {
40971
+ transactions: [],
40972
+ events: []
40973
+ };
40974
+ const found = new Map;
40975
+ for (const dataset of CHILD_DATASETS) {
40976
+ for (const height of heights) {
40977
+ const partition = childPartitionFor(partitions, dataset, height);
40978
+ if (!partition) {
40979
+ missing[dataset].push(height);
40980
+ continue;
40981
+ }
40982
+ found.set(height, { ...found.get(height), [dataset]: partition });
40983
+ }
40984
+ }
40985
+ const rewritable = heights.filter((h3) => {
40986
+ const entry = found.get(h3);
40987
+ return !!entry?.transactions && !!entry?.events;
40988
+ });
40989
+ const byPartition = new Map;
40990
+ for (const height of rewritable) {
40991
+ for (const dataset of CHILD_DATASETS) {
40992
+ const partition = found.get(height)?.[dataset];
40993
+ if (!partition)
40994
+ continue;
40995
+ const entry = byPartition.get(partition.path) ?? {
40996
+ partition,
40997
+ heights: []
40998
+ };
40999
+ entry.heights.push(height);
41000
+ byPartition.set(partition.path, entry);
41001
+ }
41002
+ }
41003
+ return { byPartition, missing, rewritable };
41004
+ }
41005
+ async function* onlyHeights(rows, heights) {
41006
+ for await (const row of rows) {
41007
+ if (heights.has(asNumber(row.block_height)))
41008
+ yield row;
41009
+ }
41010
+ }
41011
+ async function applyFixes(rawClient, reference, partition, fixes, gate) {
41012
+ const applied = {
41013
+ blocks: 0,
41014
+ transactions: 0,
41015
+ events: 0,
41016
+ missing: { transactions: [], events: [] },
41017
+ rewritten: []
41018
+ };
40507
41019
  if (fixes.length === 0)
40508
- return 0;
41020
+ return applied;
40509
41021
  const bytes = await fetchVerifiedPartition(reference, partition, gate);
40510
41022
  const archiveBlocks = await readBlocksPartition(bytes, `apply-${partition.from_block}`);
40511
41023
  const byHeight = new Map(archiveBlocks.map((b3) => [b3.height, b3]));
40512
41024
  const targets = fixes.map((fix) => byHeight.get(fix.height)).filter((b3) => b3 !== undefined);
40513
- await db.transaction().execute(async (tx) => {
41025
+ const heights = targets.map((b3) => b3.height);
41026
+ const children = planChildRewrite(reference.manifest.partitions ?? [], heights);
41027
+ applied.missing = children.missing;
41028
+ applied.rewritten = children.rewritable;
41029
+ const rewritable = new Set(children.rewritable);
41030
+ const childBytes = new Map;
41031
+ for (const [path11, { partition: child }] of children.byPartition) {
41032
+ childBytes.set(path11, await fetchVerifiedPartition(reference, child, gate));
41033
+ }
41034
+ await rawClient.begin(async (tx) => {
41035
+ for (const height of rewritable) {
41036
+ await tx.unsafe("DELETE FROM events WHERE block_height = $1", [height]);
41037
+ await tx.unsafe("DELETE FROM transactions WHERE block_height = $1", [
41038
+ height
41039
+ ]);
41040
+ }
40514
41041
  for (const block of targets) {
40515
- await sql2`
40516
- INSERT INTO blocks (
41042
+ await tx.unsafe(`INSERT INTO blocks (
40517
41043
  height, hash, parent_hash, burn_block_height,
40518
41044
  burn_block_hash, index_block_hash, timestamp, canonical
40519
- ) VALUES (
40520
- ${block.height}, ${block.hash}, ${block.parent_hash},
40521
- ${block.burn_block_height}, ${block.burn_block_hash},
40522
- ${block.index_block_hash}, ${block.timestamp}, true
40523
- )
41045
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, true)
40524
41046
  ON CONFLICT (height) DO UPDATE SET
40525
41047
  hash = EXCLUDED.hash,
40526
41048
  parent_hash = EXCLUDED.parent_hash,
@@ -40528,19 +41050,64 @@ async function applyFixes(db, reference, partition, fixes, gate) {
40528
41050
  burn_block_hash = EXCLUDED.burn_block_hash,
40529
41051
  index_block_hash = EXCLUDED.index_block_hash,
40530
41052
  timestamp = EXCLUDED.timestamp,
40531
- canonical = true
40532
- `.execute(tx);
41053
+ canonical = true`, [
41054
+ block.height,
41055
+ block.hash,
41056
+ block.parent_hash,
41057
+ block.burn_block_height,
41058
+ block.burn_block_hash,
41059
+ block.index_block_hash,
41060
+ block.timestamp
41061
+ ]);
41062
+ }
41063
+ applied.blocks = targets.length;
41064
+ for (const dataset of CHILD_DATASETS) {
41065
+ for (const [path11, { partition: child }] of children.byPartition) {
41066
+ if (child.dataset !== dataset)
41067
+ continue;
41068
+ const bytes2 = childBytes.get(path11);
41069
+ if (!bytes2)
41070
+ continue;
41071
+ const writable2 = await tx.unsafe(copyStatement(dataset)).writable();
41072
+ applied[dataset] += await writeRowsToCopyStream({
41073
+ writable: writable2,
41074
+ dataset,
41075
+ rows: onlyHeights(readPartitionRows(bytes2, `repair-${dataset}-${child.from_block}`), rewritable)
41076
+ });
41077
+ }
40533
41078
  }
40534
41079
  });
40535
- return targets.length;
41080
+ return applied;
41081
+ }
41082
+ async function reverify(db, reference, ranges) {
41083
+ const remaining = {
41084
+ blocks: 0,
41085
+ transactions: 0,
41086
+ events: 0
41087
+ };
41088
+ const published = reference.manifest.range_digests ?? [];
41089
+ for (const dataset of REPAIR_DATASETS) {
41090
+ const expected = published.filter((d2) => d2.dataset === dataset && ranges.some((r2) => r2.from_block === d2.from_block && r2.to_block === d2.to_block));
41091
+ const actual = [];
41092
+ for (const range of expected) {
41093
+ actual.push(await computeRangeDigest2(db, dataset, range.from_block, range.to_block));
41094
+ }
41095
+ remaining[dataset] = compareRangeDigests2(actual, expected).filter((c4) => c4.status !== "match").length;
41096
+ }
41097
+ return remaining;
40536
41098
  }
40537
41099
  function registerRepairCommand(program2) {
40538
- program2.command("repair").description("Replace local chain data that diverges from a signed archive (dry-run by default)").requiredOption("--against <manifest>", "archive manifest: an https URL or a local file path").option("--from-block <n>", "first height to consider").option("--to-block <n>", "last height to consider").option("--apply", "write the repair (default is a dry-run plan)").option("--public-key <pem>", "pin the signing key instead of fetching it").option("-y, --yes", "skip the confirmation prompt for a metered fetch").option("--json", "Output as JSON").addHelpText("after", `
41100
+ program2.command("repair").description("Replace local chain data that diverges from a signed archive (dry-run by default)").requiredOption("--against <manifest>", "archive manifest: an https URL or a local file path").option("--from-block <n>", "first height to consider").option("--to-block <n>", "last height to consider").option("--apply", "write the repair (default is a dry-run plan)").option("--public-key <pem>", "pin a signing key; default is the archive key built into this release").option("-y, --yes", "skip the confirmation prompt for a metered fetch").option("--json", "Output as JSON").addHelpText("after", `
40539
41101
  Examples:
40540
41102
  $ secondlayer repair --against ./snapshot.json # plan only
40541
41103
  $ secondlayer repair --against ./snapshot.json --apply # write the fix
40542
41104
  $ secondlayer repair --against ./snapshot.json --from-block 8500000 --to-block 8549999
40543
41105
 
41106
+ A fixed block is rewritten together with its transactions and events from the
41107
+ archive's partitions for that height. When the reference carries no
41108
+ transactions or events partition for a height, the block is rewritten alone,
41109
+ the run names the height, and it exits 1.
41110
+
40544
41111
  Exit codes:
40545
41112
  0 nothing to repair, or the repair completed and re-verified clean
40546
41113
  1 divergence remains (dry-run, or repair did not fully resolve it)
@@ -40548,8 +41115,14 @@ Exit codes:
40548
41115
  try {
40549
41116
  const fromBlock = opts.fromBlock === undefined ? undefined : parseHeight3(opts.fromBlock, "--from-block");
40550
41117
  const toBlock = opts.toBlock === undefined ? undefined : parseHeight3(opts.toBlock, "--to-block");
40551
- const reference = await loadReference(opts.against);
40552
- const publicKey = await resolvePublicKey(opts.publicKey, process.env.SL_API_URL ?? "https://api.secondlayer.tools");
41118
+ const publicKey = await resolveArchivePublicKey({
41119
+ explicitPem: opts.publicKey,
41120
+ envPem: process.env.ARCHIVE_SIGNING_PUBLIC_KEY ?? process.env.STREAMS_SIGNING_PUBLIC_KEY,
41121
+ allowHostedApi: !isOssMode()
41122
+ });
41123
+ const reference = await loadReference(opts.against, {
41124
+ publicKeyPem: publicKey
41125
+ });
40553
41126
  const signature = checkSignature(reference.manifest, publicKey, false);
40554
41127
  if (!signature.verified) {
40555
41128
  printError(`Refusing to repair from an untrusted reference: ${signature.reason}.`, {
@@ -40581,9 +41154,12 @@ Exit codes:
40581
41154
  process.exit(REPAIR_EXIT.OK);
40582
41155
  }
40583
41156
  const partitions = (reference.manifest.partitions ?? []).filter((p2) => p2.dataset === "blocks" && divergent.some((d2) => d2.from_block === p2.from_block && d2.to_block === p2.to_block));
41157
+ const childPartitions = opts.apply ? (reference.manifest.partitions ?? []).filter((p2) => (p2.dataset === "transactions" || p2.dataset === "events") && divergent.some((d2) => p2.from_block <= d2.to_block && p2.to_block >= d2.from_block)) : [];
41158
+ const fetchPaths = [...partitions, ...childPartitions].map((p2) => p2.path);
40584
41159
  let gate;
41160
+ let quoteLine;
40585
41161
  if (isOfficialArchive(reference)) {
40586
- const paths = partitions.map((p2) => p2.path);
41162
+ const paths = fetchPaths;
40587
41163
  const result = await quoteArchiveFetch(paths, "repair");
40588
41164
  if (!result.ok) {
40589
41165
  printError(result.kind === "not_configured" ? ARCHIVE_GATE_NOT_CONFIGURED_MESSAGE : result.message);
@@ -40593,11 +41169,16 @@ Exit codes:
40593
41169
  printError(formatInsufficientMessage(result.quote));
40594
41170
  process.exit(REPAIR_EXIT.UNANCHORED);
40595
41171
  }
40596
- note(` metered: ${formatQuoteValue(result.quote, "repair")}`);
41172
+ quoteLine = formatQuoteValue(result.quote, "repair");
41173
+ note(` metered: ${quoteLine}`);
40597
41174
  if (shouldPromptForGatedFetch(opts)) {
40598
- const proceed = await confirm7({
40599
- message: `Fetch ${partitions.length} partition(s) from the archive?`,
40600
- default: true
41175
+ if (opts.json) {
41176
+ writeData(JSON.stringify(confirmationRequiredPayload(result.quote)));
41177
+ process.exit(REPAIR_EXIT.UNANCHORED);
41178
+ }
41179
+ const proceed = await confirmDestructive({
41180
+ message: `Fetch ${fetchPaths.length} partition(s) from the archive?`,
41181
+ yes: opts.yes
40601
41182
  });
40602
41183
  if (!proceed) {
40603
41184
  note("Nothing was fetched.");
@@ -40616,30 +41197,64 @@ Exit codes:
40616
41197
  allFixes.push(...fixes);
40617
41198
  allExtra.push(...extraHeights);
40618
41199
  }
40619
- let applied = 0;
41200
+ const applied = { blocks: 0, transactions: 0, events: 0 };
41201
+ const missingChild = {
41202
+ transactions: [],
41203
+ events: []
41204
+ };
41205
+ let childrenRewritten = 0;
40620
41206
  if (opts.apply) {
41207
+ const rawClient = getRawClient("source");
40621
41208
  for (const { partition, fixes } of planned) {
40622
- applied += await applyFixes(db, reference, partition, fixes, gate);
40623
- }
40624
- }
40625
- let remaining = divergent.length;
41209
+ const result = await applyFixes(rawClient, reference, partition, fixes, gate);
41210
+ applied.blocks += result.blocks;
41211
+ applied.transactions += result.transactions;
41212
+ applied.events += result.events;
41213
+ missingChild.transactions.push(...result.missing.transactions);
41214
+ missingChild.events.push(...result.missing.events);
41215
+ childrenRewritten += result.rewritten.length;
41216
+ }
41217
+ }
41218
+ const missingHeights = [
41219
+ ...new Set([...missingChild.transactions, ...missingChild.events])
41220
+ ].sort((a4, b3) => a4 - b3);
41221
+ const rewritten = opts.apply ? REPAIR_DATASETS.filter((d2) => d2 === "blocks" || childrenRewritten > 0) : [];
41222
+ let remainingByDataset = {
41223
+ blocks: divergent.length,
41224
+ transactions: 0,
41225
+ events: 0
41226
+ };
40626
41227
  if (opts.apply) {
40627
- const after = [];
40628
- for (const range of referenceDigests) {
40629
- after.push(await computeRangeDigest2(db, "blocks", range.from_block, range.to_block));
40630
- }
40631
- remaining = compareRangeDigests2(after, referenceDigests).filter((c4) => c4.status !== "match").length;
41228
+ remainingByDataset = await reverify(db, reference, referenceDigests);
40632
41229
  }
41230
+ const remaining = remainingByDataset.blocks + remainingByDataset.transactions + remainingByDataset.events;
41231
+ const complete = !!opts.apply && remaining === 0 && missingHeights.length === 0;
40633
41232
  const report = {
40634
- status: opts.apply ? remaining === 0 ? "repaired" : "incomplete" : "plan",
41233
+ status: opts.apply ? complete ? "repaired" : "incomplete" : "plan",
40635
41234
  reference: reference.origin,
40636
41235
  divergent_ranges: divergent.length,
40637
41236
  blocks_to_replace: allFixes.filter((f2) => f2.kind === "replace").length,
40638
41237
  blocks_to_insert: allFixes.filter((f2) => f2.kind === "insert").length,
40639
41238
  local_only_heights: allExtra,
40640
- applied,
40641
- fixes: allFixes
41239
+ applied: applied.blocks,
41240
+ rows_written: applied,
41241
+ datasets_rewritten: rewritten,
41242
+ heights_missing_child_partitions: missingHeights,
41243
+ remaining_by_dataset: remainingByDataset,
41244
+ fixes: allFixes,
41245
+ metered: quoteLine ?? null
40642
41246
  };
41247
+ if (missingHeights.length > 0) {
41248
+ for (const dataset of CHILD_DATASETS) {
41249
+ const heights = missingChild[dataset];
41250
+ if (heights.length === 0)
41251
+ continue;
41252
+ warn(` the reference has no ${dataset} partition for ${heights.length} height(s): ${heights.slice(0, 5).join(", ")}${heights.length > 5 ? ", …" : ""}`);
41253
+ }
41254
+ for (const height of missingHeights.slice(0, 5)) {
41255
+ console.error(dim(` transactions/events at ${height}: run \`secondlayer bootstrap --from-block ${height} --to-block ${height}\``));
41256
+ }
41257
+ }
40643
41258
  output({
40644
41259
  json: opts.json,
40645
41260
  data: report,
@@ -40647,10 +41262,14 @@ Exit codes:
40647
41262
  const replace = report.blocks_to_replace;
40648
41263
  const insert = report.blocks_to_insert;
40649
41264
  if (opts.apply) {
40650
- if (remaining === 0) {
40651
- success(`Repaired ${applied} blocks across ${divergent.length} ranges; re-verified clean.`);
41265
+ const written = `${applied.blocks} blocks, ${applied.transactions} transactions, ${applied.events} events`;
41266
+ if (complete) {
41267
+ success(`Repaired ${applied.blocks} heights across ${divergent.length} ranges (rewrote ${written}); re-verified clean on ${rewritten.join(", ")}.`);
41268
+ } else if (remaining > 0) {
41269
+ const byDataset = REPAIR_DATASETS.map((d2) => `${remainingByDataset[d2]} ${d2}`).join(", ");
41270
+ warn(`Rewrote ${written} but ${remaining} ranges still diverge (${byDataset}).`);
40652
41271
  } else {
40653
- warn(`Applied ${applied} blocks but ${remaining} ranges still diverge.`);
41272
+ warn(`Rewrote ${written}; blocks re-verified clean, but transactions and events at ${missingHeights.length} height(s) were not rewritten.`);
40654
41273
  }
40655
41274
  } else {
40656
41275
  warn(`${divergent.length} ranges diverge — ${replace} blocks to replace, ${insert} to insert.`);
@@ -40681,31 +41300,39 @@ Exit codes:
40681
41300
  }
40682
41301
  }
40683
41302
  });
40684
- process.exit(opts.apply && remaining === 0 ? REPAIR_EXIT.OK : REPAIR_EXIT.DIVERGENCE_REMAINS);
41303
+ process.exit(complete ? REPAIR_EXIT.OK : REPAIR_EXIT.DIVERGENCE_REMAINS);
40685
41304
  } catch (err) {
40686
41305
  const message = err instanceof Error ? err.message : String(err);
40687
- const hint = /failed verification/.test(message) ? "The archive object does not match its signed digest — re-download it, and report this if it persists." : /could not fetch/.test(message) ? "Check the archive URL and your network connection." : "Set DATABASE_URL to the instance you want to repair.";
41306
+ if (err instanceof ArchiveFetchError && err.transient) {
41307
+ printError(message, {
41308
+ hint: "The archive could not be reached. Re-run the same command to resume; heights already repaired are kept."
41309
+ });
41310
+ process.exit(REPAIR_EXIT.DIVERGENCE_REMAINS);
41311
+ }
41312
+ const hint = /pointer|leave the archive root/.test(message) ? "The archive pointer failed its integrity check. Pass --against the snapshot URL directly, and report this if the pointer is the official latest.json." : /failed verification/.test(message) ? "The archive object does not match its signed digest. Re-download it, and report this if it persists." : /could not fetch/.test(message) ? "Check the archive URL and your network connection." : "Set DATABASE_URL to the instance you want to repair.";
40688
41313
  printError(message, { hint });
40689
41314
  process.exit(REPAIR_EXIT.UNANCHORED);
40690
41315
  }
40691
41316
  });
40692
41317
  }
40693
41318
  // src/commands/bootstrap.ts
40694
- import { createHash as createHash3 } from "node:crypto";
40695
- import { unlink as unlink3, writeFile as writeFile6 } from "node:fs/promises";
40696
- import { tmpdir as tmpdir3 } from "node:os";
40697
- import { join as join16 } from "node:path";
40698
- import { ParquetReader as ParquetReader2 } from "@dsnp/parquetjs";
40699
- import { confirm as confirm8 } from "@inquirer/prompts";
41319
+ import { createHash as createHash5 } from "node:crypto";
40700
41320
  import {
40701
- copyStatement,
40702
- writeRowsToCopyStream
41321
+ copyStatement as copyStatement2,
41322
+ writeRowsToCopyStream as writeRowsToCopyStream2
40703
41323
  } from "@secondlayer/shared/archive/copy-loader";
40704
- import { computeRangeDigest as computeRangeDigest3 } from "@secondlayer/shared/archive/range-digest";
40705
- import { getDb as getDb4, getRawClient, sql as sql3 } from "@secondlayer/shared/db";
41324
+ import {
41325
+ computeRangeDigest as computeRangeDigest3
41326
+ } from "@secondlayer/shared/archive/range-digest";
41327
+ import { getDb as getDb4, getRawClient as getRawClient2, sql as sql2 } from "@secondlayer/shared/db";
40706
41328
  import { upsertSyncScope } from "@secondlayer/shared/db/queries/sync-scope";
41329
+ init_archive_reference();
40707
41330
 
40708
41331
  // src/lib/bootstrap-resume.ts
41332
+ var RESUME_DATASETS = ["blocks", "transactions", "events"];
41333
+ function isResumeDataset(value) {
41334
+ return RESUME_DATASETS.includes(value);
41335
+ }
40709
41336
  function planTornImport(input4) {
40710
41337
  if (input4.hasIndexProgress) {
40711
41338
  return {
@@ -40713,21 +41340,50 @@ function planTornImport(input4) {
40713
41340
  reason: "This database already holds a completed bootstrap."
40714
41341
  };
40715
41342
  }
40716
- if (input4.maxBlockHeight === null || input4.maxBlockHeight <= 0) {
41343
+ if (RESUME_DATASETS.every((d2) => input4.highWater[d2] === null)) {
40717
41344
  return { action: "fresh" };
40718
41345
  }
40719
- const blockPartitions = input4.partitions.filter((p2) => p2.dataset === "blocks").sort((a4, b3) => a4.from_block - b3.from_block);
40720
- const sealed = blockPartitions.filter((p2) => p2.to_block <= (input4.maxBlockHeight ?? 0));
40721
- const skipThrough = sealed.length > 0 ? Math.max(...sealed.map((p2) => p2.to_block)) : 0;
40722
- const torn = blockPartitions.find((p2) => p2.from_block <= (input4.maxBlockHeight ?? 0) && p2.to_block > (input4.maxBlockHeight ?? 0));
40723
- return {
40724
- action: "resume",
40725
- truncateFrom: torn ? torn.from_block : null,
40726
- skipThrough
41346
+ const truncateFrom = {
41347
+ blocks: null,
41348
+ transactions: null,
41349
+ events: null
41350
+ };
41351
+ const skipThrough = {
41352
+ blocks: 0,
41353
+ transactions: 0,
41354
+ events: 0
41355
+ };
41356
+ for (const dataset of RESUME_DATASETS) {
41357
+ const mark = input4.highWater[dataset];
41358
+ if (mark === null)
41359
+ continue;
41360
+ const ranges = input4.partitions.filter((p2) => p2.dataset === dataset).sort((a4, b3) => a4.from_block - b3.from_block);
41361
+ const sealed = ranges.filter((p2) => p2.to_block <= mark);
41362
+ skipThrough[dataset] = sealed.length > 0 ? Math.max(...sealed.map((p2) => p2.to_block)) : 0;
41363
+ const torn = ranges.find((p2) => p2.from_block <= mark && p2.to_block > mark);
41364
+ truncateFrom[dataset] = torn ? torn.from_block : null;
41365
+ }
41366
+ const minDefined = (...values2) => {
41367
+ const defined = values2.filter((v2) => v2 !== null);
41368
+ return defined.length === 0 ? null : Math.min(...defined);
41369
+ };
41370
+ truncateFrom.transactions = minDefined(truncateFrom.blocks, truncateFrom.transactions);
41371
+ truncateFrom.events = minDefined(truncateFrom.transactions, truncateFrom.events);
41372
+ const capTo = (dataset, parentSealed) => {
41373
+ if (skipThrough[dataset] <= parentSealed)
41374
+ return;
41375
+ skipThrough[dataset] = parentSealed;
41376
+ truncateFrom[dataset] = minDefined(truncateFrom[dataset], parentSealed + 1);
40727
41377
  };
41378
+ capTo("transactions", skipThrough.blocks);
41379
+ capTo("events", skipThrough.transactions);
41380
+ truncateFrom.events = minDefined(truncateFrom.transactions, truncateFrom.events);
41381
+ return { action: "resume", truncateFrom, skipThrough };
40728
41382
  }
40729
41383
  function partitionIsLoaded(partition, skipThrough) {
40730
- return partition.to_block <= skipThrough;
41384
+ if (!isResumeDataset(partition.dataset))
41385
+ return false;
41386
+ return partition.to_block <= skipThrough[partition.dataset];
40731
41387
  }
40732
41388
 
40733
41389
  // src/commands/bootstrap.ts
@@ -40750,7 +41406,7 @@ function archiveScopeBounds(partitions) {
40750
41406
  }
40751
41407
  function manifestDigest(manifest) {
40752
41408
  const { signature: _signature, key_id: _keyId, ...payload } = manifest;
40753
- return createHash3("sha256").update(JSON.stringify(payload)).digest("hex");
41409
+ return createHash5("sha256").update(JSON.stringify(payload)).digest("hex");
40754
41410
  }
40755
41411
  async function readNodeTip() {
40756
41412
  const url = process.env.STACKS_NODE_RPC_URL ?? "http://localhost:20443";
@@ -40775,27 +41431,10 @@ function formatDuration2(seconds2) {
40775
41431
  return `${(seconds2 / 3600).toFixed(1)} hours`;
40776
41432
  return `${(seconds2 / 86400).toFixed(1)} days`;
40777
41433
  }
40778
- async function* readPartitionRows(bytes, label) {
40779
- const path11 = join16(tmpdir3(), `sl-bootstrap-${label}-${process.pid}.parquet`);
40780
- await writeFile6(path11, bytes);
40781
- try {
40782
- const reader = await ParquetReader2.openFile(path11);
40783
- try {
40784
- const cursor = reader.getCursor();
40785
- for (let row = await cursor.next();row; row = await cursor.next()) {
40786
- yield row;
40787
- }
40788
- } finally {
40789
- await reader.close();
40790
- }
40791
- } finally {
40792
- await unlink3(path11).catch(() => {});
40793
- }
40794
- }
40795
41434
  async function loadPartition(reference, partition, rawClient, gate) {
40796
41435
  const bytes = await fetchVerifiedPartition(reference, partition, gate);
40797
- const writable2 = await rawClient.unsafe(copyStatement(partition.dataset)).writable();
40798
- const written = await writeRowsToCopyStream({
41436
+ const writable2 = await rawClient.unsafe(copyStatement2(partition.dataset)).writable();
41437
+ const written = await writeRowsToCopyStream2({
40799
41438
  writable: writable2,
40800
41439
  dataset: partition.dataset,
40801
41440
  rows: readPartitionRows(bytes, `${partition.dataset}-${partition.from_block}`)
@@ -40805,8 +41444,44 @@ async function loadPartition(reference, partition, rawClient, gate) {
40805
41444
  }
40806
41445
  return written;
40807
41446
  }
41447
+ var LOAD_ORDER = [
41448
+ "blocks",
41449
+ "transactions",
41450
+ "events"
41451
+ ];
41452
+ function loadOrder(partitions) {
41453
+ return LOAD_ORDER.flatMap((dataset) => partitions.filter((p2) => p2.dataset === dataset).sort((a4, b3) => a4.from_block - b3.from_block));
41454
+ }
41455
+ function parseVerifyDatasets(value) {
41456
+ if (value === undefined || value === "all") {
41457
+ return ["blocks", "transactions", "events"];
41458
+ }
41459
+ if (value === "blocks")
41460
+ return ["blocks"];
41461
+ throw new Error(`--verify must be "all" or "blocks", got "${String(value)}"`);
41462
+ }
41463
+ async function readHighWater(db) {
41464
+ const asMark = (value) => value === null || value === undefined ? null : Number(value);
41465
+ const blocks = await db.selectFrom("blocks").select(({ fn }) => fn.max("height").as("max")).executeTakeFirst();
41466
+ const transactions = await db.selectFrom("transactions").select(({ fn }) => fn.max("block_height").as("max")).executeTakeFirst();
41467
+ const events = await db.selectFrom("events").select(({ fn }) => fn.max("block_height").as("max")).executeTakeFirst();
41468
+ return {
41469
+ blocks: asMark(blocks?.max),
41470
+ transactions: asMark(transactions?.max),
41471
+ events: asMark(events?.max)
41472
+ };
41473
+ }
41474
+ async function truncateDatasetFrom(db, dataset, from) {
41475
+ if (dataset === "blocks") {
41476
+ await sql2`DELETE FROM blocks WHERE height >= ${from}`.execute(db);
41477
+ } else if (dataset === "transactions") {
41478
+ await sql2`DELETE FROM transactions WHERE block_height >= ${from}`.execute(db);
41479
+ } else {
41480
+ await sql2`DELETE FROM events WHERE block_height >= ${from}`.execute(db);
41481
+ }
41482
+ }
40808
41483
  function attachBootstrapCommand(cmd) {
40809
- return cmd.requiredOption("--against <manifest>", "archive manifest: an https URL or a local file path").option("--to-block <n>", "stop at this height instead of the archive tip").option("--from-block <n>", "forward-only: restore from this height instead of genesis; earlier history is declared out of scope").option("--public-key <pem>", "pin the signing key instead of fetching it").option("-y, --yes", "skip the confirmation prompt").option("--json", "Output as JSON").addHelpText("after", `
41484
+ return cmd.requiredOption("--against <manifest>", "archive manifest: an https URL or a local file path").option("--to-block <n>", "stop at this height instead of the archive tip").option("--from-block <n>", "forward-only: restore from this height instead of genesis; earlier history is declared out of scope").option("--public-key <pem>", "pin a signing key; default is the archive key built into this release").option("--verify <datasets>", "digests checked after the load: all (blocks, transactions, events; adds minutes on a full chain) or blocks", "all").option("-y, --yes", "skip the confirmation prompt").option("--json", "Output as JSON").addHelpText("after", `
40810
41485
  Examples:
40811
41486
  $ secondlayer bootstrap --against https://archive.secondlayer.tools/.../snapshots/<digest>.json
40812
41487
  $ secondlayer bootstrap --against ./snapshot.json --to-block 4000000 --yes
@@ -40817,12 +41492,15 @@ Exit codes:
40817
41492
  1 restore completed but verification found divergence
40818
41493
  2 refused (non-empty target, or an untrusted reference)`).action(async (opts) => {
40819
41494
  try {
40820
- const reference = await loadReference(opts.against);
41495
+ const verifyDatasets = parseVerifyDatasets(opts.verify);
40821
41496
  const publicKey = await resolveArchivePublicKey({
40822
41497
  explicitPem: opts.publicKey,
40823
41498
  envPem: process.env.ARCHIVE_SIGNING_PUBLIC_KEY ?? process.env.STREAMS_SIGNING_PUBLIC_KEY,
40824
41499
  allowHostedApi: !isOssMode()
40825
41500
  });
41501
+ const reference = await loadReference(opts.against, {
41502
+ publicKeyPem: publicKey
41503
+ });
40826
41504
  const signature = checkSignature(reference.manifest, publicKey, false);
40827
41505
  if (!signature.verified) {
40828
41506
  printError(`Refusing to bootstrap from an untrusted archive: ${signature.reason}.`, {
@@ -40832,14 +41510,19 @@ Exit codes:
40832
41510
  }
40833
41511
  const db = getDb4();
40834
41512
  const progress = await db.selectFrom("index_progress").select("network").executeTakeFirst();
40835
- const maxHeightRow = await db.selectFrom("blocks").select(({ fn }) => fn.max("height").as("max")).executeTakeFirst();
40836
- const maxBlockHeight = maxHeightRow?.max === null || maxHeightRow?.max === undefined ? null : Number(maxHeightRow.max);
41513
+ const highWater = await readHighWater(db);
40837
41514
  const toBlock = opts.toBlock === undefined ? undefined : Number(opts.toBlock);
40838
41515
  const fromBlock = opts.fromBlock === undefined ? undefined : Number(opts.fromBlock);
40839
41516
  const declared = (reference.manifest.partitions ?? []).filter((p2) => (toBlock === undefined || p2.to_block <= toBlock) && (fromBlock === undefined || p2.from_block >= fromBlock));
41517
+ if (declared.length === 0) {
41518
+ printError("The archive has no partitions for that range.", {
41519
+ hint: "Check --from-block and --to-block against the manifest's coverage."
41520
+ });
41521
+ process.exit(BOOTSTRAP_EXIT.REFUSED);
41522
+ }
40840
41523
  const resume = planTornImport({
40841
41524
  hasIndexProgress: !!progress,
40842
- maxBlockHeight,
41525
+ highWater,
40843
41526
  partitions: declared
40844
41527
  });
40845
41528
  if (resume.action === "refuse") {
@@ -40848,33 +41531,38 @@ Exit codes:
40848
41531
  });
40849
41532
  process.exit(BOOTSTRAP_EXIT.REFUSED);
40850
41533
  }
40851
- if (resume.action === "resume" && resume.truncateFrom !== null) {
40852
- await sql3`DELETE FROM events WHERE block_height >= ${resume.truncateFrom}`.execute(db);
40853
- await sql3`DELETE FROM transactions WHERE block_height >= ${resume.truncateFrom}`.execute(db);
40854
- await sql3`DELETE FROM blocks WHERE height >= ${resume.truncateFrom}`.execute(db);
40855
- note(`Resuming torn import: truncated from height ${resume.truncateFrom}.`);
40856
- } else if (resume.action === "resume") {
40857
- note(`Resuming import after sealed height ${resume.skipThrough.toLocaleString()}.`);
41534
+ if (resume.action === "resume") {
41535
+ const truncated = [];
41536
+ for (const dataset of [...RESUME_DATASETS].reverse()) {
41537
+ const from = resume.truncateFrom[dataset];
41538
+ if (from === null)
41539
+ continue;
41540
+ await truncateDatasetFrom(db, dataset, from);
41541
+ truncated.push(`${dataset} from ${from.toLocaleString()}`);
41542
+ }
41543
+ if (truncated.length > 0) {
41544
+ note(`Resuming torn import: truncated ${truncated.join(", ")}.`);
41545
+ }
41546
+ const sealed = RESUME_DATASETS.filter((d2) => resume.skipThrough[d2] > 0).map((d2) => `${d2} through ${resume.skipThrough[d2].toLocaleString()}`);
41547
+ note(sealed.length > 0 ? `Resuming import after sealed partitions: ${sealed.join(", ")}.` : "Resuming import: no partition finished, so the load starts over from the first.");
40858
41548
  }
40859
41549
  const partitions = declared.filter((p2) => resume.action === "fresh" || !partitionIsLoaded(p2, resume.skipThrough));
40860
41550
  if (partitions.length === 0) {
40861
- printError("The archive has no partitions for that range.", {
40862
- hint: "Check --to-block against the manifest's coverage."
40863
- });
40864
- process.exit(BOOTSTRAP_EXIT.REFUSED);
41551
+ note("All partitions are already loaded; verifying and finalizing the import.");
40865
41552
  }
40866
41553
  const scopeBounds = archiveScopeBounds(declared);
40867
41554
  const startHeight = scopeBounds?.start_height ?? 0;
40868
41555
  const totalRows = partitions.reduce((sum, p2) => sum + p2.row_count, 0);
40869
41556
  const totalBytes = partitions.reduce((sum, p2) => sum + p2.byte_size, 0);
40870
- const tipHeight = Math.max(...partitions.map((p2) => p2.to_block));
41557
+ const tipHeight = scopeBounds?.tip_height ?? 0;
40871
41558
  const restoreSeconds = totalRows / RESTORE_ROWS_PER_SECOND;
40872
41559
  const genesisSeconds = tipHeight / BLOCKS_PER_DAY_ESTIMATE * 86400;
40873
41560
  const gated = isOfficialArchive(reference);
40874
41561
  let quoteLine;
41562
+ let quote = null;
40875
41563
  let gate;
40876
- if (gated) {
40877
- const paths = partitions.map((p2) => p2.path);
41564
+ if (gated && partitions.length > 0) {
41565
+ const paths = loadOrder(partitions).map((p2) => p2.path);
40878
41566
  const result = await quoteArchiveFetch(paths, "bootstrap");
40879
41567
  if (!result.ok) {
40880
41568
  printError(result.kind === "not_configured" ? ARCHIVE_GATE_NOT_CONFIGURED_MESSAGE : result.message);
@@ -40884,6 +41572,7 @@ Exit codes:
40884
41572
  printError(formatInsufficientMessage(result.quote));
40885
41573
  process.exit(BOOTSTRAP_EXIT.REFUSED);
40886
41574
  }
41575
+ quote = result.quote;
40887
41576
  quoteLine = formatQuoteValue(result.quote, "bootstrap");
40888
41577
  gate = createGatedFetcher(paths, "bootstrap");
40889
41578
  }
@@ -40905,10 +41594,16 @@ Exit codes:
40905
41594
  console.error(` restore from archive ${bold(`~${formatDuration2(restoreSeconds)}`)}`);
40906
41595
  console.error("");
40907
41596
  }
40908
- if (shouldPromptForGatedFetch(opts)) {
40909
- const proceed = await confirm8({
41597
+ if (partitions.length > 0 && shouldPromptForGatedFetch(opts)) {
41598
+ if (opts.json) {
41599
+ if (quoteLine)
41600
+ note(` metered: ${quoteLine}`);
41601
+ writeData(JSON.stringify(confirmationRequiredPayload(quote)));
41602
+ process.exit(BOOTSTRAP_EXIT.REFUSED);
41603
+ }
41604
+ const proceed = await confirmDestructive({
40910
41605
  message: "Restore from the verified archive?",
40911
- default: true
41606
+ yes: opts.yes
40912
41607
  });
40913
41608
  if (!proceed) {
40914
41609
  note("Nothing was written.");
@@ -40916,12 +41611,11 @@ Exit codes:
40916
41611
  }
40917
41612
  }
40918
41613
  const nodeTipAtStart = await readNodeTip();
40919
- const order = ["blocks", "transactions", "events"];
40920
- const rawClient = getRawClient("source");
41614
+ const rawClient = getRawClient2("source");
40921
41615
  const loaded = { blocks: 0, transactions: 0, events: 0 };
40922
41616
  const startedAt = Date.now();
40923
- for (const dataset of order) {
40924
- const datasetPartitions = partitions.filter((p2) => p2.dataset === dataset).sort((a4, b3) => a4.from_block - b3.from_block);
41617
+ for (const dataset of LOAD_ORDER) {
41618
+ const datasetPartitions = loadOrder(partitions).filter((p2) => p2.dataset === dataset);
40925
41619
  for (const [index, partition] of datasetPartitions.entries()) {
40926
41620
  loaded[dataset] += await loadPartition(reference, partition, rawClient, gate);
40927
41621
  const done = index + 1;
@@ -40931,16 +41625,21 @@ Exit codes:
40931
41625
  }
40932
41626
  }
40933
41627
  }
40934
- const referenceDigests = (reference.manifest.range_digests ?? []).filter((d2) => d2.dataset === "blocks" && d2.to_block <= tipHeight);
40935
- let divergent = 0;
41628
+ const referenceDigests = (reference.manifest.range_digests ?? []).filter((d2) => verifyDatasets.includes(d2.dataset) && d2.from_block >= startHeight && d2.to_block <= tipHeight);
41629
+ const divergentRanges = [];
40936
41630
  for (const range of referenceDigests) {
40937
- const actual = await computeRangeDigest3(db, "blocks", range.from_block, range.to_block);
41631
+ const actual = await computeRangeDigest3(db, range.dataset, range.from_block, range.to_block);
40938
41632
  if (actual.digest !== range.digest || actual.row_count !== range.row_count) {
40939
- divergent++;
41633
+ divergentRanges.push({
41634
+ dataset: range.dataset,
41635
+ from_block: range.from_block,
41636
+ to_block: range.to_block
41637
+ });
40940
41638
  }
40941
41639
  }
41640
+ const divergent = divergentRanges.length;
40942
41641
  const network = process.env.STACKS_NETWORK ?? "mainnet";
40943
- await sql3`
41642
+ await sql2`
40944
41643
  INSERT INTO index_progress (
40945
41644
  network, last_indexed_block, last_contiguous_block,
40946
41645
  highest_seen_block, updated_at
@@ -40975,8 +41674,10 @@ Exit codes:
40975
41674
  start_height: startHeight,
40976
41675
  tip_height: tipHeight,
40977
41676
  rows: loaded,
41677
+ verified_datasets: verifyDatasets,
40978
41678
  ranges_verified: referenceDigests.length,
40979
41679
  divergent_ranges: divergent,
41680
+ divergent: divergentRanges,
40980
41681
  elapsed_seconds: Math.round(elapsed),
40981
41682
  resume_from: tipHeight + 1,
40982
41683
  node_tip_at_start: nodeTipAtStart,
@@ -40990,9 +41691,9 @@ Exit codes:
40990
41691
  if (divergent === 0) {
40991
41692
  success(`Restored ${loaded.blocks.toLocaleString()} blocks, ${loaded.transactions.toLocaleString()} transactions, ${loaded.events.toLocaleString()} events in ${formatDuration2(elapsed)}.`);
40992
41693
  if (referenceDigests.length > 0) {
40993
- note(` verified ${referenceDigests.length} ranges against the archive`);
41694
+ note(` verified ${referenceDigests.length} ${verifyDatasets.join("/")} ranges against the archive`);
40994
41695
  } else {
40995
- warn(" archive published no block digests — restore is unverified");
41696
+ warn(" archive published no digests for the restored range; the restore is unverified");
40996
41697
  }
40997
41698
  console.error("");
40998
41699
  console.error(dim(`Your instance holds history through ${tipHeight.toLocaleString()} and will resume at ${(tipHeight + 1).toLocaleString()}.`));
@@ -41006,7 +41707,8 @@ Exit codes:
41006
41707
  console.error(dim(" Start the indexer to continue from there. (Node unreachable — catch-up range unknown.)"));
41007
41708
  }
41008
41709
  } else {
41009
- warn(`Restored ${loaded.blocks.toLocaleString()} blocks but ${divergent} ranges do not match the archive.`);
41710
+ const byDataset = verifyDatasets.map((d2) => `${divergentRanges.filter((r2) => r2.dataset === d2).length} ${d2}`).join(", ");
41711
+ warn(`Restored ${loaded.blocks.toLocaleString()} blocks but ${divergent} ranges do not match the archive (${byDataset}).`);
41010
41712
  console.error(dim(" Investigate with: secondlayer verify --against <manifest>"));
41011
41713
  }
41012
41714
  }
@@ -41014,7 +41716,13 @@ Exit codes:
41014
41716
  process.exit(divergent === 0 ? BOOTSTRAP_EXIT.OK : BOOTSTRAP_EXIT.INCOMPLETE);
41015
41717
  } catch (err) {
41016
41718
  const message = err instanceof Error ? err.message : String(err);
41017
- const hint = /failed verification/.test(message) ? "An archive object does not match its signed digest — re-download and retry." : /could not fetch/.test(message) ? "Check the archive URL and your network connection." : "Set DATABASE_URL to the (empty) instance you want to bootstrap.";
41719
+ if (err instanceof ArchiveFetchError && err.transient) {
41720
+ printError(message, {
41721
+ hint: "The archive could not be reached. Re-run the same command to resume; datasets already loaded are kept."
41722
+ });
41723
+ process.exit(BOOTSTRAP_EXIT.INCOMPLETE);
41724
+ }
41725
+ const hint = /^--verify must be/.test(message) ? "Pass --verify all (default) or --verify blocks." : /pointer|leave the archive root/.test(message) ? "The archive pointer failed its integrity check. Pass --against the snapshot URL directly, and report this if the pointer is the official latest.json." : /failed verification/.test(message) ? "An archive object does not match its signed digest. Re-download and retry." : /could not fetch/.test(message) ? "Check the archive URL and your network connection." : "Set DATABASE_URL to the (empty) instance you want to bootstrap.";
41018
41726
  printError(message, { hint });
41019
41727
  process.exit(BOOTSTRAP_EXIT.REFUSED);
41020
41728
  }
@@ -41024,29 +41732,66 @@ function registerBootstrapCommand(program2) {
41024
41732
  attachBootstrapCommand(program2.command("bootstrap").description("Restore chain history from a verified archive instead of syncing from genesis"));
41025
41733
  }
41026
41734
  // src/commands/uninstall.ts
41735
+ init_instance_init();
41027
41736
  init_output();
41028
41737
  import { spawnSync as spawnSync4 } from "node:child_process";
41029
- import { existsSync as existsSync8, readFileSync as readFileSync7 } from "node:fs";
41030
- import { join as join17, resolve as resolve13 } from "node:path";
41738
+ import { existsSync as existsSync9, readFileSync as readFileSync8 } from "node:fs";
41739
+ import { join as join16, resolve as resolve15 } from "node:path";
41031
41740
  import { planUninstall, uninstallCommand } from "@secondlayer/shared/runtime";
41032
41741
  var UNINSTALL_EXIT = { OK: 0, FAILED: 1, REFUSED: 2 };
41033
- var DEFAULT_COMPOSE = "docker/oss/docker-compose.yml";
41742
+ var REPO_COMPOSE = "docker/oss/docker-compose.yml";
41034
41743
  function bundleHasKeys(bundleDir) {
41035
- const manifest = join17(bundleDir, "manifest.json");
41036
- if (!existsSync8(manifest))
41744
+ const manifest = join16(bundleDir, "manifest.json");
41745
+ if (!existsSync9(manifest))
41037
41746
  return false;
41038
41747
  try {
41039
- const parsed = JSON.parse(readFileSync7(manifest, "utf8"));
41748
+ const parsed = JSON.parse(readFileSync8(manifest, "utf8"));
41040
41749
  return parsed.secrets != null;
41041
41750
  } catch {
41042
41751
  return false;
41043
41752
  }
41044
41753
  }
41754
+ function resolveUninstallLayout(dir, composeOverride) {
41755
+ const setupCompose = resolve15(dir, "docker-compose.yml");
41756
+ const setupEnv = resolve15(dir, ".env");
41757
+ const initEnv = resolve15(dir, ".env.local");
41758
+ const secretsFile = [setupEnv, initEnv].find((file) => readEnvValue(file, "SECONDLAYER_SECRETS_KEY") !== null) ?? [setupEnv, initEnv].find((file) => existsSync9(file));
41759
+ if (composeOverride) {
41760
+ const composeFile = resolve15(dir, composeOverride);
41761
+ const sibling = resolve15(composeFile, "..", ".env");
41762
+ return {
41763
+ composeFile,
41764
+ envFile: existsSync9(sibling) ? sibling : undefined,
41765
+ secretsFile
41766
+ };
41767
+ }
41768
+ if (existsSync9(setupCompose)) {
41769
+ return {
41770
+ composeFile: setupCompose,
41771
+ envFile: existsSync9(setupEnv) ? setupEnv : undefined,
41772
+ secretsFile
41773
+ };
41774
+ }
41775
+ return { composeFile: resolve15(dir, REPO_COMPOSE), secretsFile };
41776
+ }
41777
+ function composeDownArgs(plan, layout) {
41778
+ const base2 = uninstallCommand(plan, layout.composeFile);
41779
+ if (!layout.envFile)
41780
+ return base2;
41781
+ const fileFlagEnd = base2.indexOf("-f") + 2;
41782
+ return [
41783
+ ...base2.slice(0, fileFlagEnd),
41784
+ "--env-file",
41785
+ layout.envFile,
41786
+ ...base2.slice(fileFlagEnd)
41787
+ ];
41788
+ }
41045
41789
  function registerUninstallCommand(program2) {
41046
- program2.command("uninstall").description("Stop the stack; the index, chainstate, and keys survive").option("--compose <file>", "compose file to tear down", DEFAULT_COMPOSE).option("--purge", "also destroy the volumes (wipes the index)").option("--yes", "confirm a purge").option("--backup <dir>", "a bundle proving the keys exist elsewhere (required to purge)").option("--apply", "actually run it (default is a dry run)").option("--json", "Output as JSON").action(async (opts) => {
41790
+ program2.command("uninstall").description("Stop the stack; the index, chainstate, and keys survive").option("--compose <file>", `compose file to tear down (default: ./docker-compose.yml from setup, else ${REPO_COMPOSE})`).option("--purge", "also destroy the volumes (wipes the index)").option("--yes", "confirm a purge").option("--backup <dir>", "a bundle proving the keys exist elsewhere (required to purge)").option("--apply", "actually run it (default is a dry run)").option("--json", "Output as JSON").action(async (opts) => {
41047
41791
  const dataDir = process.env.DATA_DIR ?? "./data";
41048
- const secretsPresent = !!process.env.SECONDLAYER_SECRETS_KEY || existsSync8(resolve13(process.cwd(), ".env.local"));
41049
- const keysBackedUp = opts.backup ? bundleHasKeys(resolve13(opts.backup)) : false;
41792
+ const layout = resolveUninstallLayout(process.cwd(), opts.compose);
41793
+ const secretsPresent = !!process.env.SECONDLAYER_SECRETS_KEY || !!layout.secretsFile;
41794
+ const keysBackedUp = opts.backup ? bundleHasKeys(resolve15(opts.backup)) : false;
41050
41795
  const decision = planUninstall({
41051
41796
  purge: opts.purge === true,
41052
41797
  confirmed: opts.yes === true,
@@ -41055,16 +41800,30 @@ function registerUninstallCommand(program2) {
41055
41800
  dataDir
41056
41801
  });
41057
41802
  if (!decision.ok) {
41058
- printError(decision.reason);
41803
+ printError(decision.reason, {
41804
+ hint: layout.secretsFile ? `keys found in ${layout.secretsFile}` : undefined
41805
+ });
41059
41806
  process.exit(UNINSTALL_EXIT.REFUSED);
41060
41807
  }
41061
- const cmd = uninstallCommand(decision.plan, opts.compose);
41808
+ const cmd = composeDownArgs(decision.plan, layout);
41062
41809
  if (!opts.apply) {
41063
41810
  output({
41064
41811
  json: opts.json,
41065
- data: { dryRun: true, plan: decision.plan, command: cmd },
41812
+ data: {
41813
+ dryRun: true,
41814
+ plan: decision.plan,
41815
+ command: cmd,
41816
+ composeFile: layout.composeFile,
41817
+ envFile: layout.envFile ?? null,
41818
+ secretsFile: layout.secretsFile ?? null
41819
+ },
41066
41820
  human: () => {
41067
41821
  note("dry run — nothing removed. Pass --apply to proceed.");
41822
+ note(`compose: ${layout.composeFile}`);
41823
+ if (layout.envFile)
41824
+ note(`env: ${layout.envFile}`);
41825
+ if (layout.secretsFile)
41826
+ note(`keys: ${layout.secretsFile}`);
41068
41827
  note(`would run: docker ${cmd.join(" ")}`);
41069
41828
  for (const p2 of decision.plan.preserves) {
41070
41829
  note(`preserved · ${p2.what}: ${p2.detail}`);
@@ -41083,7 +41842,12 @@ function registerUninstallCommand(program2) {
41083
41842
  }
41084
41843
  output({
41085
41844
  json: opts.json,
41086
- data: { removed: true, plan: decision.plan },
41845
+ data: {
41846
+ removed: true,
41847
+ plan: decision.plan,
41848
+ composeFile: layout.composeFile,
41849
+ envFile: layout.envFile ?? null
41850
+ },
41087
41851
  human: () => {
41088
41852
  success("stack removed");
41089
41853
  for (const w2 of decision.warnings)
@@ -41096,18 +41860,18 @@ function registerUninstallCommand(program2) {
41096
41860
  });
41097
41861
  }
41098
41862
  // src/commands/backup.ts
41863
+ init_fs();
41099
41864
  init_output();
41100
- import { createHash as createHash4 } from "node:crypto";
41101
41865
  import {
41102
- chmodSync,
41103
- existsSync as existsSync9,
41866
+ chmodSync as chmodSync2,
41867
+ existsSync as existsSync10,
41104
41868
  mkdirSync as mkdirSync6,
41105
- readFileSync as readFileSync8,
41869
+ readFileSync as readFileSync9,
41106
41870
  statSync as statSync3,
41107
- writeFileSync as writeFileSync7
41871
+ writeFileSync as writeFileSync8
41108
41872
  } from "node:fs";
41109
- import { join as join18, resolve as resolve14 } from "node:path";
41110
- import { closeDb, getDb as getDb5, sql as sql4 } from "@secondlayer/shared/db";
41873
+ import { join as join17, resolve as resolve16 } from "node:path";
41874
+ import { closeDb, getDb as getDb5, sql as sql3 } from "@secondlayer/shared/db";
41111
41875
  import {
41112
41876
  decryptBundle,
41113
41877
  encryptBundle,
@@ -41132,12 +41896,9 @@ function readSecretsKey() {
41132
41896
  const key = process.env.SECONDLAYER_SECRETS_KEY?.trim();
41133
41897
  return key ? key : undefined;
41134
41898
  }
41135
- function sha256File(path11) {
41136
- return createHash4("sha256").update(readFileSync8(path11)).digest("hex");
41137
- }
41138
41899
  async function readScope() {
41139
41900
  const db = getDb5();
41140
- const bounds = await sql4`SELECT MIN(height)::text AS min, MAX(height)::text AS max FROM blocks WHERE canonical = true`.execute(db);
41901
+ const bounds = await sql3`SELECT MIN(height)::text AS min, MAX(height)::text AS max FROM blocks WHERE canonical = true`.execute(db);
41141
41902
  const row = bounds.rows[0];
41142
41903
  const network = process.env.STACKS_NETWORK ?? "mainnet";
41143
41904
  return {
@@ -41152,9 +41913,73 @@ function requireDatabaseUrl() {
41152
41913
  throw new Error("DATABASE_URL is required");
41153
41914
  return url;
41154
41915
  }
41155
- async function runProcess(cmd, label) {
41156
- const proc = Bun.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
41157
- const stderr = await new Response(proc.stderr).text();
41916
+ function pgConnection(databaseUrl) {
41917
+ let parsed;
41918
+ try {
41919
+ parsed = new URL(databaseUrl);
41920
+ } catch {
41921
+ return { url: databaseUrl, env: {} };
41922
+ }
41923
+ if (!parsed.password)
41924
+ return { url: databaseUrl, env: {} };
41925
+ const password = decodeURIComponent(parsed.password);
41926
+ parsed.password = "";
41927
+ return { url: parsed.toString(), env: { PGPASSWORD: password } };
41928
+ }
41929
+ function pgDumpInvocation(databaseUrl, dumpPath) {
41930
+ const conn = pgConnection(databaseUrl);
41931
+ return {
41932
+ cmd: ["pg_dump", "-Fc", "--no-owner", "--no-acl", "-f", dumpPath, conn.url],
41933
+ env: conn.env
41934
+ };
41935
+ }
41936
+ function pgRestoreInvocation(databaseUrl, dumpPath) {
41937
+ const conn = pgConnection(databaseUrl);
41938
+ return {
41939
+ cmd: [
41940
+ "pg_restore",
41941
+ "--no-owner",
41942
+ "--no-acl",
41943
+ "--clean",
41944
+ "--if-exists",
41945
+ "--single-transaction",
41946
+ "--exit-on-error",
41947
+ "-d",
41948
+ conn.url,
41949
+ dumpPath
41950
+ ],
41951
+ env: conn.env
41952
+ };
41953
+ }
41954
+ function checkRestoredScope(manifest, restored) {
41955
+ const want = manifest.scope;
41956
+ if (want.to_height === null && restored.toHeight === null) {
41957
+ return { ok: true };
41958
+ }
41959
+ if (restored.toHeight === null) {
41960
+ return {
41961
+ ok: false,
41962
+ reason: `the restore finished but the target holds no canonical blocks; the manifest promised ${want.from_height} through ${want.to_height ?? "?"}`
41963
+ };
41964
+ }
41965
+ if (restored.fromHeight !== want.from_height || want.to_height !== null && restored.toHeight !== want.to_height) {
41966
+ return {
41967
+ ok: false,
41968
+ reason: `the restore finished with blocks ${restored.fromHeight} through ${restored.toHeight}, but the manifest promised ${want.from_height} through ${want.to_height ?? "?"}; the dump does not carry the scope it claims`
41969
+ };
41970
+ }
41971
+ return { ok: true };
41972
+ }
41973
+ async function runProcess(cmd, label, env2 = {}) {
41974
+ const proc = Bun.spawn(cmd, {
41975
+ stdout: "pipe",
41976
+ stderr: "pipe",
41977
+ env: { ...process.env, ...env2 }
41978
+ });
41979
+ const [stderr] = await Promise.all([
41980
+ new Response(proc.stderr).text(),
41981
+ new Response(proc.stdout).text()
41982
+ ]);
41158
41983
  const code = await proc.exited;
41159
41984
  if (code !== 0) {
41160
41985
  return { ok: false, stderr: `${label} exited ${code}: ${stderr.trim()}` };
@@ -41162,7 +41987,7 @@ async function runProcess(cmd, label) {
41162
41987
  return { ok: true, stderr };
41163
41988
  }
41164
41989
  function attachBackupCommand(cmd) {
41165
- return cmd.requiredOption("--out <dir>", "directory to write the bundle into").option("--passphrase <passphrase>", "encrypt bundled secrets (or set SECONDLAYER_BACKUP_PASSPHRASE)").option("--no-secrets", "omit keys; the bundle restores data only").option("--json", "Output as JSON").action(async (opts) => {
41990
+ return cmd.requiredOption("--out <dir>", "directory to write the bundle into").option("--passphrase <passphrase>", "encrypt bundled secrets; prefer SECONDLAYER_BACKUP_PASSPHRASE in env, a flag lands in shell history and ps").option("--no-secrets", "omit keys; the bundle restores data only").option("--json", "Output as JSON").action(async (opts) => {
41166
41991
  try {
41167
41992
  await runBackup(opts);
41168
41993
  await closeDb();
@@ -41194,11 +42019,12 @@ async function runBackup(opts) {
41194
42019
  printError(plan.reason);
41195
42020
  process.exit(BACKUP_EXIT.REFUSED);
41196
42021
  }
41197
- const outDir = resolve14(opts.out);
42022
+ const outDir = resolve16(opts.out);
41198
42023
  mkdirSync6(outDir, { recursive: true });
41199
42024
  console.error("dumping database…");
41200
- const dumpPath = join18(outDir, DB_FILE);
41201
- const dump = await runProcess(["pg_dump", "-Fc", "--no-owner", "--no-acl", "-f", dumpPath, databaseUrl], "pg_dump");
42025
+ const dumpPath = join17(outDir, DB_FILE);
42026
+ const dumpCmd = pgDumpInvocation(databaseUrl, dumpPath);
42027
+ const dump = await runProcess(dumpCmd.cmd, "pg_dump", dumpCmd.env);
41202
42028
  if (!dump.ok) {
41203
42029
  printError(dump.stderr);
41204
42030
  process.exit(BACKUP_EXIT.FAILED);
@@ -41207,7 +42033,7 @@ async function runBackup(opts) {
41207
42033
  ...plan.manifest,
41208
42034
  db: {
41209
42035
  file: DB_FILE,
41210
- sha256: sha256File(dumpPath),
42036
+ sha256: await sha256File(dumpPath),
41211
42037
  bytes: statSync3(dumpPath).size,
41212
42038
  format: "pg_dump-custom"
41213
42039
  }
@@ -41216,16 +42042,16 @@ async function runBackup(opts) {
41216
42042
  const lines2 = SECRET_ENV_KEYS.filter((k2) => process.env[k2]).map((k2) => `${k2}=${process.env[k2]}`);
41217
42043
  const sealed = encryptBundle(lines2.join(`
41218
42044
  `), passphrase);
41219
- const secretsPath = join18(outDir, SECRETS_FILE);
41220
- writeFileSync7(secretsPath, sealed);
41221
- chmodSync(secretsPath, 384);
42045
+ const secretsPath = join17(outDir, SECRETS_FILE);
42046
+ writeFileSync8(secretsPath, sealed);
42047
+ chmodSync2(secretsPath, 384);
41222
42048
  manifest.secrets = {
41223
42049
  file: SECRETS_FILE,
41224
42050
  encrypted: true,
41225
42051
  canary: sealKeyCanary(secretsKey)
41226
42052
  };
41227
42053
  }
41228
- writeFileSync7(join18(outDir, MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}
42054
+ writeFileSync8(join17(outDir, MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}
41229
42055
  `);
41230
42056
  output({
41231
42057
  json: opts.json,
@@ -41242,7 +42068,7 @@ async function runBackup(opts) {
41242
42068
  });
41243
42069
  }
41244
42070
  function attachRestoreCommand(cmd) {
41245
- return cmd.requiredOption("--from <dir>", "bundle directory to restore from").option("--passphrase <passphrase>", "decrypt bundled secrets (or set SECONDLAYER_BACKUP_PASSPHRASE)").option("--apply", "actually restore (default is a dry run)").option("--force", "restore over a database that already holds chain data").option("--json", "Output as JSON").action(async (opts) => {
42071
+ return cmd.requiredOption("--from <dir>", "bundle directory to restore from").option("--passphrase <passphrase>", "decrypt bundled secrets; prefer SECONDLAYER_BACKUP_PASSPHRASE in env, a flag lands in shell history and ps").option("--apply", "actually restore (default is a dry run)").option("--force", "restore over a database that already holds chain data").option("--json", "Output as JSON").action(async (opts) => {
41246
42072
  try {
41247
42073
  await runRestore(opts);
41248
42074
  await closeDb();
@@ -41254,15 +42080,15 @@ function attachRestoreCommand(cmd) {
41254
42080
  }
41255
42081
  async function runRestore(opts) {
41256
42082
  const databaseUrl = requireDatabaseUrl();
41257
- const dir = resolve14(opts.from);
41258
- const manifestPath = join18(dir, MANIFEST_FILE);
41259
- if (!existsSync9(manifestPath)) {
42083
+ const dir = resolve16(opts.from);
42084
+ const manifestPath = join17(dir, MANIFEST_FILE);
42085
+ if (!existsSync10(manifestPath)) {
41260
42086
  printError(`no ${MANIFEST_FILE} in ${dir}`);
41261
42087
  process.exit(BACKUP_EXIT.REFUSED);
41262
42088
  }
41263
- const manifest = JSON.parse(readFileSync8(manifestPath, "utf8"));
41264
- const dumpPath = join18(dir, manifest.db?.file ?? DB_FILE);
41265
- if (manifest.db && sha256File(dumpPath) !== manifest.db.sha256) {
42089
+ const manifest = JSON.parse(readFileSync9(manifestPath, "utf8"));
42090
+ const dumpPath = join17(dir, manifest.db?.file ?? DB_FILE);
42091
+ if (manifest.db && await sha256File(dumpPath) !== manifest.db.sha256) {
41266
42092
  printError("the database dump does not match the digest in the manifest; this bundle is corrupt or truncated");
41267
42093
  process.exit(BACKUP_EXIT.REFUSED);
41268
42094
  }
@@ -41275,7 +42101,7 @@ async function runRestore(opts) {
41275
42101
  process.exit(BACKUP_EXIT.REFUSED);
41276
42102
  }
41277
42103
  try {
41278
- const env2 = decryptBundle(readFileSync8(join18(dir, manifest.secrets.file)), passphrase);
42104
+ const env2 = decryptBundle(readFileSync9(join17(dir, manifest.secrets.file)), passphrase);
41279
42105
  bundleKey = env2.split(`
41280
42106
  `).find((l2) => l2.startsWith("SECONDLAYER_SECRETS_KEY="))?.slice("SECONDLAYER_SECRETS_KEY=".length).trim();
41281
42107
  } catch {
@@ -41288,10 +42114,10 @@ async function runRestore(opts) {
41288
42114
  process.exit(BACKUP_EXIT.REFUSED);
41289
42115
  }
41290
42116
  const db = getDb5();
41291
- const present = await sql4`SELECT to_regclass('public.blocks') IS NOT NULL AS present`.execute(db);
42117
+ const present = await sql3`SELECT to_regclass('public.blocks') IS NOT NULL AS present`.execute(db);
41292
42118
  let targetIsEmpty = true;
41293
42119
  if (present.rows[0]?.present) {
41294
- const existing = await sql4`SELECT COUNT(*)::text AS count FROM blocks`.execute(db);
42120
+ const existing = await sql3`SELECT COUNT(*)::text AS count FROM blocks`.execute(db);
41295
42121
  targetIsEmpty = (existing.rows[0]?.count ?? "0") === "0";
41296
42122
  }
41297
42123
  const check = precheckRestore({
@@ -41320,20 +42146,18 @@ async function runRestore(opts) {
41320
42146
  return;
41321
42147
  }
41322
42148
  console.error("restoring database…");
41323
- const restore = await runProcess([
41324
- "pg_restore",
41325
- "--no-owner",
41326
- "--no-acl",
41327
- "--clean",
41328
- "--if-exists",
41329
- "-d",
41330
- databaseUrl,
41331
- dumpPath
41332
- ], "pg_restore");
42149
+ const restoreCmd = pgRestoreInvocation(databaseUrl, dumpPath);
42150
+ const restore = await runProcess(restoreCmd.cmd, "pg_restore", restoreCmd.env);
41333
42151
  if (!restore.ok) {
41334
42152
  printError(restore.stderr);
41335
42153
  process.exit(BACKUP_EXIT.FAILED);
41336
42154
  }
42155
+ const restored = await readScope();
42156
+ const scopeCheck = checkRestoredScope(manifest, restored);
42157
+ if (!scopeCheck.ok) {
42158
+ printError(scopeCheck.reason);
42159
+ process.exit(BACKUP_EXIT.FAILED);
42160
+ }
41337
42161
  output({
41338
42162
  json: opts.json,
41339
42163
  data: { restored: true, manifest },
@@ -41353,19 +42177,26 @@ function registerRestoreCommand(program2) {
41353
42177
  attachRestoreCommand(program2.command("restore").description("Restore an instance from a backup bundle"));
41354
42178
  }
41355
42179
  // src/commands/init.ts
42180
+ init_archive_reference();
41356
42181
  init_instance_init();
41357
42182
  init_output();
42183
+ init_resolve_auth();
42184
+ var DEFAULT_INIT_API_URL = "http://127.0.0.1:3800";
41358
42185
  function registerInitCommand(program2) {
41359
- program2.command("init").description("Write local env (token, secrets key, webhook signing key). Idempotent.").option("--api-url <url>", "Local API URL", "http://127.0.0.1:3800").option("--force", "Overwrite generated values even if .env.local exists").addHelpText("after", `
41360
- --network <mainnet|testnet|devnet> is the top-level global flag (see
41361
- \`secondlayer --help\`) — default mainnet, same as before.
41362
- `).action((opts) => {
41363
- const network = parseInstanceNetwork(process.env.STACKS_NETWORK ?? "mainnet");
42186
+ program2.command("init").description("Write local env (token, secrets key, webhook signing key). Idempotent.").option("--force", "Overwrite generated values even if .env.local exists").addHelpText("after", `
42187
+ --network <mainnet|testnet|devnet> and --api-url <url> are top-level global
42188
+ flags (see \`secondlayer --help\`). Defaults: mainnet, http://127.0.0.1:3800.
42189
+ `).action(async (opts) => {
42190
+ const network = instanceNetworkFromEnv();
41364
42191
  const existing = opts.force ? {} : loadExistingInstanceEnv(process.cwd());
41365
42192
  const env2 = buildInstanceEnv({
41366
42193
  network,
41367
42194
  existing,
41368
- apiUrl: opts.apiUrl
42195
+ apiUrl: process.env.SL_API_URL ?? DEFAULT_INIT_API_URL,
42196
+ archivePublicKeyPem: await resolveArchivePublicKey({
42197
+ envPem: process.env.ARCHIVE_SIGNING_PUBLIC_KEY ?? process.env.STREAMS_SIGNING_PUBLIC_KEY,
42198
+ allowHostedApi: !isOssMode()
42199
+ })
41369
42200
  });
41370
42201
  const path11 = writeInstanceEnv(process.cwd(), env2);
41371
42202
  success(`Wrote ${INSTANCE_ENV_FILE}`);
@@ -41387,7 +42218,7 @@ function registerObserverCommand(program2) {
41387
42218
  \`secondlayer --help\`) — default mainnet, same as before.
41388
42219
  `).action((opts) => {
41389
42220
  const mode = parseObserverMode(opts.mode);
41390
- const network = parseInstanceNetwork(process.env.STACKS_NETWORK ?? "mainnet");
42221
+ const network = instanceNetworkFromEnv();
41391
42222
  const endpoint = opts.endpoint ?? defaultObserverEndpoint(network);
41392
42223
  const recovery = opts.recovery ? parseRecoverySource(opts.recovery) : undefined;
41393
42224
  if (mode === "signer-shared") {
@@ -41403,7 +42234,8 @@ init_output();
41403
42234
  init_instance_init();
41404
42235
  init_oss_compose();
41405
42236
  init_setup_wizard();
41406
- import { confirm as confirm9, input as input4, select as select5 } from "@inquirer/prompts";
42237
+ import { resolve as resolvePath3 } from "node:path";
42238
+ import { confirm as confirm6, input as input4, select as select5 } from "@inquirer/prompts";
41407
42239
 
41408
42240
  class SetupCancelledError extends Error {
41409
42241
  constructor() {
@@ -41497,7 +42329,7 @@ async function promptSetupConfig(flags) {
41497
42329
  apiPort: flags.apiPort ?? "127.0.0.1:3800",
41498
42330
  indexerPort: "127.0.0.1:3700",
41499
42331
  postgresPort: "127.0.0.1:5432",
41500
- dir: flags.dir ?? process.cwd(),
42332
+ dir: resolvePath3(flags.dir ?? process.cwd()),
41501
42333
  against: skipBootstrap ? undefined : against,
41502
42334
  skipBootstrap,
41503
42335
  skipVerify: !!flags.skipVerify,
@@ -41513,7 +42345,7 @@ async function promptSetupConfig(flags) {
41513
42345
  console.error(` api port ${config.apiPort}`);
41514
42346
  console.error(` bootstrap ${config.skipBootstrap ? "skip (sync from genesis)" : config.against}`);
41515
42347
  console.error("");
41516
- const proceed = await confirm9({ message: "Start setup?", default: true });
42348
+ const proceed = await confirm6({ message: "Start setup?", default: true });
41517
42349
  if (!proceed)
41518
42350
  throw new SetupCancelledError;
41519
42351
  return config;
@@ -41590,7 +42422,7 @@ async function runPromptedInteractive(flags) {
41590
42422
  await executeAndRender(config);
41591
42423
  }
41592
42424
  function registerSetupCommand(program2) {
41593
- program2.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", process.cwd()).option("--against <manifest>", `Archive manifest to bootstrap from (suggested default: ${DEFAULT_ARCHIVE_MANIFEST})`).option("--skip-bootstrap", "Sync from genesis instead of restoring an archive").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", `
42425
+ program2.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", "Sync from genesis instead of restoring an archive").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", `
41594
42426
  --network <mainnet|testnet|devnet> is the top-level global flag (see
41595
42427
  \`secondlayer --help\`) — required here too, just parsed one level up.
41596
42428
 
@@ -41625,44 +42457,9 @@ decision with no safe default (--network, --node-mode, and --against unless
41625
42457
  }
41626
42458
  });
41627
42459
  }
41628
- // src/commands/start.ts
41629
- init_output();
41630
- import { parseRuntimeConfig } from "@secondlayer/shared/runtime";
41631
- function registerStartCommand(program2) {
41632
- program2.command("start").description("Validate one-box runtime config and print the compose command").option("--print", "print the compose invocation only").action((opts) => {
41633
- const parsed = parseRuntimeConfig({
41634
- NETWORK: process.env.NETWORK ?? process.env.STACKS_NETWORK,
41635
- DATABASE_URL: process.env.DATABASE_URL,
41636
- NODE_MODE: process.env.NODE_MODE ?? "external",
41637
- DATA_DIR: process.env.DATA_DIR ?? "/data",
41638
- API_PORT: process.env.API_PORT ?? "3800",
41639
- INDEXER_PORT: process.env.INDEXER_PORT ?? "3700",
41640
- STACKS_NODE_RPC_URL: process.env.STACKS_NODE_RPC_URL,
41641
- LISTEN_HOST: process.env.LISTEN_HOST,
41642
- LOG_LEVEL: process.env.LOG_LEVEL,
41643
- BITCOIN_RPC_PASSWORD: process.env.BITCOIN_RPC_PASSWORD
41644
- });
41645
- if (!parsed.ok) {
41646
- printError(parsed.errors.join("; "), {
41647
- hint: "Set NETWORK, DATABASE_URL, and NODE_MODE (external | stacks | full)."
41648
- });
41649
- process.exit(1);
41650
- }
41651
- const extra = parsed.config.NODE_MODE === "full" ? " --profile full-node" : "";
41652
- const cmd = `docker compose -f docker/oss/docker-compose.yml${extra} up -d`;
41653
- if (opts.print) {
41654
- console.log(cmd);
41655
- return;
41656
- }
41657
- success(`config ok (${parsed.config.NETWORK}, ${parsed.config.NODE_MODE})`);
41658
- console.log(cmd);
41659
- note(" console UI (optional): add --profile console, then `secondlayer console`");
41660
- note(" /extended view (optional): set EXTENDED_VIEW=1, then curl http://127.0.0.1:3999/extended/v1/status");
41661
- });
41662
- }
41663
42460
  // src/cli.ts
41664
42461
  var { version } = package_default;
41665
- program.name("secondlayer").alias("sl").description("Secondlayer CLI — run a Stacks index on your own hardware").version(version).option("--network <network>", "Override network (local, testnet, mainnet)").option("--api-key <key>", "Instance or archive credential (overrides INSTANCE_TOKEN)").option("--api-url <url>", "API endpoint (overrides SL_API_URL)").showSuggestionAfterError(true).showHelpAfterError("(run `secondlayer --help` to see available commands)");
42462
+ program.name("secondlayer").alias("sl").description("Secondlayer CLI — run a Stacks index on your own hardware").version(version).option("--network <network>", "Override network (mainnet, testnet, devnet)").option("--api-key <key>", "Instance or archive credential; prefer INSTANCE_TOKEN in env, a flag lands in shell history and ps").option("--api-url <url>", "API endpoint (overrides SL_API_URL)").showSuggestionAfterError(true).showHelpAfterError("(run `secondlayer --help` to see available commands)");
41666
42463
  program.hook("preAction", (thisCommand) => {
41667
42464
  const { network, apiKey, apiUrl } = thisCommand.opts();
41668
42465
  if (network)
@@ -41678,7 +42475,8 @@ program.addHelpText("after", `
41678
42475
  Quickstart:
41679
42476
  $ secondlayer setup
41680
42477
  $ secondlayer subgraphs create my-watcher --from-contract SP....my-contract
41681
- $ secondlayer subgraphs deploy subgraphs/my-watcher.ts
42478
+ $ git add subgraphs/my-watcher.ts
42479
+ $ secondlayer subgraphs deploy subgraphs/my-watcher.ts # or --allow-uncommitted
41682
42480
 
41683
42481
  Manual / scripted setup, one step at a time:
41684
42482
  $ secondlayer init --network mainnet
@@ -41689,7 +42487,6 @@ registerSetupCommand(program);
41689
42487
  registerInitCommand(program);
41690
42488
  registerBootstrapCommand(program);
41691
42489
  registerObserverCommand(program);
41692
- registerStartCommand(program);
41693
42490
  registerConsoleCommand(program);
41694
42491
  registerLoginCommand(program);
41695
42492
  registerLogoutCommand(program);
@@ -41718,5 +42515,5 @@ program.commandsGroup("Archive:");
41718
42515
  registerCreditsCommand(program);
41719
42516
  program.parse();
41720
42517
 
41721
- //# debugId=90E04792038F19D064756E2164756E21
42518
+ //# debugId=80EA4C7A072B2FCB64756E2164756E21
41722
42519
  //# sourceMappingURL=cli.js.map