@secondlayer/cli 9.9.0 → 10.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -2481,6 +2481,25 @@ var require_commander = __commonJS((exports) => {
2481
2481
  exports.InvalidOptionArgumentError = InvalidArgumentError;
2482
2482
  });
2483
2483
 
2484
+ // ../../node_modules/commander/esm.mjs
2485
+ var import__, program, createCommand, createArgument, createOption, CommanderError, InvalidArgumentError, InvalidOptionArgumentError, Command, Argument, Option, Help;
2486
+ var init_esm = __esm(() => {
2487
+ import__ = __toESM(require_commander(), 1);
2488
+ ({
2489
+ program,
2490
+ createCommand,
2491
+ createArgument,
2492
+ createOption,
2493
+ CommanderError,
2494
+ InvalidArgumentError,
2495
+ InvalidOptionArgumentError,
2496
+ Command,
2497
+ Argument,
2498
+ Option,
2499
+ Help
2500
+ } = import__.default);
2501
+ });
2502
+
2484
2503
  // src/lib/session.ts
2485
2504
  import {
2486
2505
  chmod,
@@ -2543,6 +2562,9 @@ async function resolveAuth() {
2543
2562
  if (envKey) {
2544
2563
  return { apiUrl, ephemeralKey: envKey, fromEnv: true };
2545
2564
  }
2565
+ if (isOssMode()) {
2566
+ return { apiUrl, ephemeralKey: "", fromEnv: true };
2567
+ }
2546
2568
  const session = await readSession();
2547
2569
  if (!session) {
2548
2570
  const err = new Error("Not logged in — run `sl login`");
@@ -2551,6 +2573,9 @@ async function resolveAuth() {
2551
2573
  }
2552
2574
  return { apiUrl, ephemeralKey: session.token, fromEnv: false };
2553
2575
  }
2576
+ function isOssMode() {
2577
+ return !!process.env.SL_API_URL;
2578
+ }
2554
2579
  var DEFAULT_API_URL = "https://api.secondlayer.tools";
2555
2580
  var init_resolve_auth = __esm(() => {
2556
2581
  init_session();
@@ -2590,7 +2615,7 @@ async function httpPlatform(path2, opts = {}) {
2590
2615
  try {
2591
2616
  auth = await resolveAuth();
2592
2617
  } catch {
2593
- throw new CliHttpError(401, "SESSION_EXPIRED", { error: "Not logged in" }, "Not logged in — run `sl login`");
2618
+ throw new CliHttpError(401, "SESSION_EXPIRED", { error: "Not logged in" }, isOssMode() ? "Not authenticated — set SL_API_KEY from `sl init`" : "Not logged in — run `sl login`");
2594
2619
  }
2595
2620
  return request(`${auth.apiUrl}${path2}`, {
2596
2621
  ...opts,
@@ -2745,7 +2770,7 @@ function handleApiError(err, action) {
2745
2770
  const status = err instanceof ApiError || err instanceof CliHttpError ? err.status : undefined;
2746
2771
  if (err instanceof CliHttpError && err.code === "SESSION_EXPIRED" || status === 401) {
2747
2772
  printError("Authentication required.", {
2748
- hint: "Run `sl login` to re-authenticate."
2773
+ hint: isOssMode() ? "Set SL_API_KEY / INSTANCE_TOKEN from `sl init`." : "Run `sl login` to re-authenticate."
2749
2774
  });
2750
2775
  process.exit(1);
2751
2776
  }
@@ -28826,7 +28851,7 @@ async function runLoginFlow(options3 = {}) {
28826
28851
  }
28827
28852
  }
28828
28853
  function registerLoginCommand(program2) {
28829
- program2.command("login").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", `
28854
+ 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", `
28830
28855
  Examples:
28831
28856
  $ sl login
28832
28857
  $ echo "$SL_API_KEY" | sl login --with-token`).action((opts) => opts.withToken ? runTokenLogin() : runLoginFlow({ force: opts.force }));
@@ -28839,6 +28864,8 @@ var init_login = __esm(() => {
28839
28864
 
28840
28865
  // src/lib/require-auth.ts
28841
28866
  async function requireAuth() {
28867
+ if (isOssMode())
28868
+ return;
28842
28869
  const session = await readSession();
28843
28870
  if (session)
28844
28871
  return;
@@ -28848,6 +28875,7 @@ async function requireAuth() {
28848
28875
  var init_require_auth = __esm(() => {
28849
28876
  init_login();
28850
28877
  init_output();
28878
+ init_resolve_auth();
28851
28879
  init_session();
28852
28880
  });
28853
28881
 
@@ -30104,7 +30132,7 @@ Stopped watching.`);
30104
30132
  });
30105
30133
  await new Promise(() => {});
30106
30134
  });
30107
- 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("--database-url <url>", "BYO data plane: write the subgraph's schema/rows to your own Postgres. With --dry-run, prints the DDL + grant script and verifies the connection.").option("--strict", "Run `tsc --noEmit` against the handler before deploy (slower; catches TS type errors)").option("--visibility <visibility>", "Read visibility: public (anon /v1 reads, global name claim) or private (your key only). Defaults: managed → public, BYO → private.").option("--allow-uncommitted", "Deploy even though the source file is not committed to git (the deployed definition will exist only in the database)").action(async (file, options3) => {
30135
+ 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("--database-url <url>", "BYO data plane: write the subgraph's schema/rows to your own Postgres. With --dry-run, prints the DDL + grant script and verifies the connection.").option("--strict", "Run `tsc --noEmit` against the handler before deploy (slower; catches TS type errors)").addOption(new Option("--visibility <visibility>", "Hosted only. Public/private namespace.").hideHelp()).option("--allow-uncommitted", "Deploy even though the source file is not committed to git (the deployed definition will exist only in the database)").action(async (file, options3) => {
30108
30136
  try {
30109
30137
  const absPath = resolve3(file);
30110
30138
  if (!options3.dryRun) {
@@ -30132,7 +30160,7 @@ Stopped watching.`);
30132
30160
  }
30133
30161
  }
30134
30162
  const config = await loadConfig2();
30135
- if (config.network !== "local") {
30163
+ if (config.network !== "local" && !isOssMode()) {
30136
30164
  await requireAuth();
30137
30165
  }
30138
30166
  const dryRun = options3.dryRun;
@@ -30666,7 +30694,7 @@ ${rows.length} row(s)`));
30666
30694
  handleApiError(err, "delete subgraph");
30667
30695
  }
30668
30696
  });
30669
- subgraphs.command("publish <name>").description("Make a subgraph publicly readable on /v1/subgraphs/<name> (claims the name globally, no key needed to read)").action(async (name) => {
30697
+ subgraphs.command("publish <name>", { hidden: true }).description("Make a subgraph publicly readable on /v1/subgraphs/<name> (claims the name globally, no key needed to read)").action(async (name) => {
30670
30698
  try {
30671
30699
  await requireAuth();
30672
30700
  const result = await publishSubgraphApi(name);
@@ -30684,7 +30712,7 @@ ${rows.length} row(s)`));
30684
30712
  handleApiError(err, "publish subgraph");
30685
30713
  }
30686
30714
  });
30687
- subgraphs.command("unpublish <name>").description("Make a subgraph private again — reads require your API key, the public name claim is released").action(async (name) => {
30715
+ subgraphs.command("unpublish <name>", { hidden: true }).description("Make a subgraph private again — reads require your API key, the public name claim is released").action(async (name) => {
30688
30716
  try {
30689
30717
  await requireAuth();
30690
30718
  await unpublishSubgraphApi(name);
@@ -30839,6 +30867,7 @@ async function runSubgraphSchemaCodegen(file, options3, optionSource, defaultTar
30839
30867
  }
30840
30868
  var ORM_ONLY_CODEGEN_FLAGS;
30841
30869
  var init_subgraphs2 = __esm(() => {
30870
+ init_esm();
30842
30871
  init_subgraph_scaffold();
30843
30872
  init_subgraphs();
30844
30873
  init_api_client();
@@ -30850,6 +30879,7 @@ var init_subgraphs2 = __esm(() => {
30850
30879
  init_output();
30851
30880
  init_require_auth();
30852
30881
  init_resolve_auth();
30882
+ init_resolve_auth();
30853
30883
  init_clarity();
30854
30884
  init_subgraph();
30855
30885
  init_api();
@@ -33160,12 +33190,12 @@ __export(exports_dev_state, {
33160
33190
  clearDevState: () => clearDevState
33161
33191
  });
33162
33192
  import { homedir as homedir4 } from "node:os";
33163
- import { join as join10 } from "node:path";
33193
+ import { join as join11 } from "node:path";
33164
33194
  function getLogsDir() {
33165
33195
  return LOGS_DIR;
33166
33196
  }
33167
33197
  function getLogFile(service) {
33168
- return join10(LOGS_DIR, `${service}.log`);
33198
+ return join11(LOGS_DIR, `${service}.log`);
33169
33199
  }
33170
33200
  async function ensureDirs() {
33171
33201
  await Bun.$`mkdir -p ${STATE_DIR}`.quiet();
@@ -33222,9 +33252,9 @@ async function isDevRunning() {
33222
33252
  }
33223
33253
  var STATE_DIR, DEV_STATE_PATH, LOGS_DIR;
33224
33254
  var init_dev_state = __esm(() => {
33225
- STATE_DIR = join10(homedir4(), ".secondlayer");
33226
- DEV_STATE_PATH = join10(STATE_DIR, "dev.json");
33227
- LOGS_DIR = join10(STATE_DIR, "logs");
33255
+ STATE_DIR = join11(homedir4(), ".secondlayer");
33256
+ DEV_STATE_PATH = join11(STATE_DIR, "dev.json");
33257
+ LOGS_DIR = join11(STATE_DIR, "logs");
33228
33258
  });
33229
33259
 
33230
33260
  // src/lib/network.ts
@@ -33708,7 +33738,7 @@ __export(exports_dev_impl, {
33708
33738
  isDevAlreadyRunning: () => isDevAlreadyRunning
33709
33739
  });
33710
33740
  import { mkdirSync as mkdirSync5 } from "node:fs";
33711
- import { dirname as dirname14, join as join11, resolve as resolve10 } from "node:path";
33741
+ import { dirname as dirname14, join as join12, resolve as resolve10 } from "node:path";
33712
33742
  async function isDevAlreadyRunning() {
33713
33743
  if (await isDevRunning()) {
33714
33744
  const running = await getRunningServices();
@@ -34317,7 +34347,7 @@ async function ensureDevPostgres(dataDir) {
34317
34347
  if (check.stdout.toString().trim()) {
34318
34348
  return false;
34319
34349
  }
34320
- const pgDataDir = join11(dataDir, "postgres");
34350
+ const pgDataDir = join12(dataDir, "postgres");
34321
34351
  mkdirSync5(pgDataDir, { recursive: true });
34322
34352
  const stopped = await Bun.$`docker ps -aq -f name=secondlayer-dev-postgres`.quiet().nothrow();
34323
34353
  if (stopped.stdout.toString().trim()) {
@@ -34749,75 +34779,12 @@ var init_node_impl = __esm(() => {
34749
34779
  init_output();
34750
34780
  });
34751
34781
 
34752
- // src/commands/init.ts
34753
- var exports_init = {};
34754
- __export(exports_init, {
34755
- init: () => init2
34756
- });
34757
- import { promises as fs7 } from "node:fs";
34758
- import path11 from "node:path";
34759
- async function init2() {
34760
- const configPath = path11.join(process.cwd(), "secondlayer.config.ts");
34761
- try {
34762
- await fs7.access(configPath);
34763
- console.log(source_default.yellow("⚠ secondlayer.config.ts already exists"));
34764
- return;
34765
- } catch {}
34766
- const hasClarinetProject = await fileExists2("./Clarinet.toml");
34767
- let config;
34768
- if (hasClarinetProject) {
34769
- config = `import { defineConfig } from '@secondlayer/cli';
34770
- import { clarinet } from '@secondlayer/cli/plugins';
34771
-
34772
- export default defineConfig({
34773
- out: './src/generated/contracts.ts',
34774
- plugins: [
34775
- clarinet() // Found Clarinet.toml in current directory
34776
- ]
34777
- });`;
34778
- } else {
34779
- config = `import { defineConfig } from '@secondlayer/cli';
34780
-
34781
- export default defineConfig({
34782
- out: './src/generated/contracts.ts',
34783
- plugins: [],
34784
- });`;
34785
- }
34786
- await fs7.writeFile(configPath, config);
34787
- console.log(source_default.green("✓ Created `secondlayer.config.ts`"));
34788
- console.log("\nRun `secondlayer generate` to generate type-safe interfaces, functions, and hooks!");
34789
- }
34790
- async function fileExists2(filePath) {
34791
- try {
34792
- await fs7.access(filePath);
34793
- return true;
34794
- } catch {
34795
- return false;
34796
- }
34797
- }
34798
- var init_init = __esm(() => {
34799
- init_source();
34800
- });
34801
-
34802
- // ../../node_modules/commander/esm.mjs
34803
- var import__ = __toESM(require_commander(), 1);
34804
- var {
34805
- program,
34806
- createCommand,
34807
- createArgument,
34808
- createOption,
34809
- CommanderError,
34810
- InvalidArgumentError,
34811
- InvalidOptionArgumentError,
34812
- Command,
34813
- Argument,
34814
- Option,
34815
- Help
34816
- } = import__.default;
34782
+ // src/cli.ts
34783
+ init_esm();
34817
34784
  // package.json
34818
34785
  var package_default = {
34819
34786
  name: "@secondlayer/cli",
34820
- version: "9.9.0",
34787
+ version: "10.0.0",
34821
34788
  repository: {
34822
34789
  type: "git",
34823
34790
  url: "git+https://github.com/ryanwaits/secondlayer.git",
@@ -34861,7 +34828,7 @@ var package_default = {
34861
34828
  "@secondlayer/bundler": "^0.4.1",
34862
34829
  "@secondlayer/scaffold": "^1.5.1",
34863
34830
  "@secondlayer/sdk": "^6.51.0",
34864
- "@secondlayer/shared": "^7.8.0",
34831
+ "@secondlayer/shared": "^7.9.0",
34865
34832
  "@secondlayer/stacks": "^3.3.1",
34866
34833
  "@secondlayer/subgraphs": "^3.26.4",
34867
34834
  esbuild: "^0.27.4",
@@ -35003,7 +34970,7 @@ async function applyProfileUpdate(options, getOptionValueSource) {
35003
34970
  });
35004
34971
  }
35005
34972
  function registerAccountCommand(program2) {
35006
- const account = program2.command("account").description("Manage your account profile");
34973
+ const account = program2.command("account", { hidden: true }).description("Manage your account profile");
35007
34974
  addBillingCommand(account);
35008
34975
  account.command("get").description("Show your account profile").option("--json", "Output as JSON").action(withErrorHandling(async (options) => showProfile(options.json), { action: "show profile" }));
35009
34976
  account.command("update").description("Update your public profile").option("--name <name>", "Set display name").option("--bio <bio>", "Set bio").option("--slug <slug>", "Set public URL slug").option("--notify-reindex-complete", "Email me when a subgraph reindex finishes").option("--no-notify-reindex-complete", "Don't email me when a subgraph reindex finishes").option("--json", "Output as JSON").action(withErrorHandling(async (options, command) => applyProfileUpdate(options, (key) => command.getOptionValueSource(key)), {
@@ -36522,7 +36489,7 @@ POST result (client-side, not logged):`));
36522
36489
  // src/commands/devnet.ts
36523
36490
  import { spawnSync as spawnSync2 } from "node:child_process";
36524
36491
  import { existsSync as existsSync6, mkdirSync as mkdirSync4, writeFileSync as writeFileSync6 } from "node:fs";
36525
- import { dirname as dirname8, join as join8 } from "node:path";
36492
+ import { dirname as dirname8, join as join9 } from "node:path";
36526
36493
 
36527
36494
  // src/lib/devnet-compose.ts
36528
36495
  var DEFAULT_IMAGE_OWNER = "ryanwaits";
@@ -36536,6 +36503,7 @@ function buildDevnetCompose(opts = {}) {
36536
36503
  const indexerImage = `ghcr.io/${owner}/secondlayer-indexer:${tag}`;
36537
36504
  const dbUrl = "postgres://secondlayer:secondlayer@postgres:5432/secondlayer";
36538
36505
  const DEV_SECRETS_KEY = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
36506
+ const DEV_INSTANCE_TOKEN = "dev-instance-token";
36539
36507
  return `# Generated by \`sl devnet connect\` — do not edit by hand.
36540
36508
  # Secondlayer app services for local clarinet devnet development.
36541
36509
  name: secondlayer-devnet
@@ -36572,6 +36540,8 @@ services:
36572
36540
  INSTANCE_MODE: oss
36573
36541
  DATABASE_URL: ${dbUrl}
36574
36542
  PORT: 3800
36543
+ LISTEN_HOST: "0.0.0.0"
36544
+ INSTANCE_TOKEN: ${DEV_INSTANCE_TOKEN}
36575
36545
  NODE_ENV: production
36576
36546
  DATA_DIR: /data
36577
36547
  # Shared across api + subgraph-processor so the secret the api encrypts
@@ -36719,7 +36689,138 @@ stacks_node_events_observers = [${entry}]
36719
36689
 
36720
36690
  // src/commands/devnet.ts
36721
36691
  init_output();
36722
- var COMPOSE_REL = join8(".secondlayer", "docker-compose.yml");
36692
+
36693
+ // src/commands/faucet.ts
36694
+ import { randomBytes as randomBytes2 } from "node:crypto";
36695
+ import { readFile as readFile3 } from "node:fs/promises";
36696
+ import { join as join8 } from "node:path";
36697
+ import { http as http3, createWalletClient } from "@secondlayer/stacks";
36698
+ import { mnemonicToAccount } from "@secondlayer/stacks/accounts";
36699
+ import { devnet } from "@secondlayer/stacks/chains";
36700
+ import { Cl } from "@secondlayer/stacks/clarity";
36701
+ import { parse as parseToml2 } from "smol-toml";
36702
+ init_output();
36703
+ var DUST_LIMIT_SATS = 546n;
36704
+ var DEFAULT_AMOUNT_SATS = 1000000n;
36705
+ async function readDeployerMnemonic(projectDir) {
36706
+ const path11 = join8(projectDir, "settings", "Devnet.toml");
36707
+ let raw;
36708
+ try {
36709
+ raw = await readFile3(path11, "utf8");
36710
+ } catch {
36711
+ throw new Error(`could not read ${path11}`);
36712
+ }
36713
+ const parsed = parseToml2(raw);
36714
+ const mnemonic = parsed.accounts?.deployer?.mnemonic;
36715
+ if (!mnemonic) {
36716
+ throw new Error(`no accounts.deployer.mnemonic in ${path11}`);
36717
+ }
36718
+ return mnemonic;
36719
+ }
36720
+ async function readBurnAnchor(rpcUrl) {
36721
+ const info2 = await (await fetch(`${rpcUrl.replace(/\/$/, "")}/v2/info`, {
36722
+ signal: AbortSignal.timeout(1e4)
36723
+ })).json();
36724
+ const height = info2.burn_block_height;
36725
+ if (typeof height !== "number") {
36726
+ throw new Error("node did not report a burn block height");
36727
+ }
36728
+ const burn = await (await fetch(`${rpcUrl.replace(/\/$/, "")}/extended/v2/burn-blocks/${height}`, { signal: AbortSignal.timeout(1e4) })).json();
36729
+ if (!burn.burn_block_hash) {
36730
+ throw new Error(`node did not report a hash for burn block ${height}`);
36731
+ }
36732
+ return { height, hash: burn.burn_block_hash.replace(/^0x/, "") };
36733
+ }
36734
+ function parseAmount(value) {
36735
+ if (value === undefined)
36736
+ return DEFAULT_AMOUNT_SATS;
36737
+ const parsed = BigInt(value);
36738
+ if (parsed < DUST_LIMIT_SATS) {
36739
+ throw new Error(`--amount must be at least ${DUST_LIMIT_SATS} sats (the protocol dust limit), got ${parsed}`);
36740
+ }
36741
+ return parsed;
36742
+ }
36743
+ function registerFaucetCommand(devnetCommand) {
36744
+ devnetCommand.command("faucet").description("Mint mock sBTC to an address on your devnet").requiredOption("--to <address>", "recipient Stacks address").option("--amount <sats>", `amount in sats (default ${DEFAULT_AMOUNT_SATS}, minimum ${DUST_LIMIT_SATS})`).option("--contract <id>", "sBTC deposit contract (default: <deployer>.sbtc-deposit)").option("--rpc-url <url>", "devnet node RPC (default http://localhost:3999)").option("--json", "Output as JSON").addHelpText("after", `
36745
+ Examples:
36746
+ $ sl devnet faucet --to ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM
36747
+ $ sl devnet faucet --to ST2... --amount 5000000
36748
+
36749
+ Requires a devnet running with the sBTC contract set deployed by your
36750
+ Clarinet deployer — that deployer is the signer set on your own chain, which
36751
+ is what makes minting possible without Bitcoin.`).action(async (opts) => {
36752
+ try {
36753
+ const network = process.env.STACKS_NETWORK ?? "devnet";
36754
+ if (network !== "devnet" && network !== "local") {
36755
+ printError(`sl devnet faucet only works on devnet, not ${network}.`, {
36756
+ hint: "Mock sBTC is mintable because you deployed the contracts. On testnet or mainnet the signer set is not yours."
36757
+ });
36758
+ process.exit(2);
36759
+ }
36760
+ const rpcUrl = opts.rpcUrl ?? "http://localhost:3999";
36761
+ const amount = parseAmount(opts.amount);
36762
+ const projectDir = findClarinetProject(process.cwd());
36763
+ if (!projectDir) {
36764
+ printError("No Clarinet project found.", {
36765
+ hint: "Run this from a project containing Clarinet.toml, or pass --contract explicitly."
36766
+ });
36767
+ process.exit(2);
36768
+ }
36769
+ const mnemonic = await readDeployerMnemonic(projectDir);
36770
+ const account = mnemonicToAccount(mnemonic, {
36771
+ addressVersion: devnet.addressVersion.singleSig
36772
+ });
36773
+ const contract = opts.contract ?? `${account.address}.sbtc-deposit`;
36774
+ const anchor = await readBurnAnchor(rpcUrl);
36775
+ const client2 = createWalletClient({
36776
+ account,
36777
+ chain: devnet,
36778
+ transport: http3(rpcUrl)
36779
+ });
36780
+ const depositTxid = randomBytes2(32).toString("hex");
36781
+ const sweepTxid = randomBytes2(32).toString("hex");
36782
+ note(` minting ${amount} sats to ${opts.to}`);
36783
+ note(` via ${contract} as signer ${account.address}`);
36784
+ const txid = await client2.callContract({
36785
+ contract,
36786
+ functionName: "complete-deposit-wrapper",
36787
+ functionArgs: [
36788
+ Cl.bufferFromHex(depositTxid),
36789
+ Cl.uint(0),
36790
+ Cl.uint(amount),
36791
+ Cl.principal(opts.to),
36792
+ Cl.bufferFromHex(anchor.hash),
36793
+ Cl.uint(anchor.height),
36794
+ Cl.bufferFromHex(sweepTxid)
36795
+ ],
36796
+ postConditionMode: "allow"
36797
+ });
36798
+ output({
36799
+ json: opts.json,
36800
+ data: {
36801
+ txid,
36802
+ recipient: opts.to,
36803
+ amount_sats: amount.toString(),
36804
+ contract,
36805
+ burn_height: anchor.height
36806
+ },
36807
+ human: () => {
36808
+ success(`Minted ${amount} sats of mock sBTC to ${opts.to}.`);
36809
+ note(` tx ${txid}`);
36810
+ console.error(dim(" Mock sBTC on your devnet — not the canonical asset."));
36811
+ }
36812
+ });
36813
+ } catch (err) {
36814
+ const message = err instanceof Error ? err.message : String(err);
36815
+ const hint = /ECONNREFUSED|fetch failed/.test(message) ? "Is your devnet running? Start it with `clarinet devnet start`." : /NoSuchContract|does not exist/.test(message) ? "Deploy the sBTC contract set to your devnet first, or pass --contract." : "Check that your Clarinet deployer matches the sBTC contract deployer.";
36816
+ printError(message, { hint });
36817
+ process.exit(1);
36818
+ }
36819
+ });
36820
+ }
36821
+
36822
+ // src/commands/devnet.ts
36823
+ var COMPOSE_REL = join9(".secondlayer", "docker-compose.yml");
36723
36824
  function ensureDocker() {
36724
36825
  const probe = spawnSync2("docker", ["info"], { stdio: "ignore" });
36725
36826
  if (probe.error || probe.status !== 0) {
@@ -36747,23 +36848,24 @@ var SERVICES = [
36747
36848
  "migrate"
36748
36849
  ];
36749
36850
  function registerDevnetCommand(program2) {
36750
- const devnet = program2.command("devnet").description("Run Secondlayer services against a local Clarinet devnet");
36751
- devnet.command("connect").description("Point your clarinet project's devnet at a local Secondlayer stack and start it").option("--project <dir>", "Clarinet project directory (defaults to the nearest Clarinet.toml)").option("--image-tag <tag>", "Published image tag to run", DEFAULT_IMAGE_TAG).option("--owner <owner>", "ghcr image owner (namespace) to pull from").option("--no-up", "Patch config + write compose without starting docker").action(async (options3) => {
36851
+ const devnet2 = program2.command("devnet").description("Run Secondlayer services against a local Clarinet devnet");
36852
+ registerFaucetCommand(devnet2);
36853
+ devnet2.command("connect").description("Point your clarinet project's devnet at a local Secondlayer stack and start it").option("--project <dir>", "Clarinet project directory (defaults to the nearest Clarinet.toml)").option("--image-tag <tag>", "Published image tag to run", DEFAULT_IMAGE_TAG).option("--owner <owner>", "ghcr image owner (namespace) to pull from").option("--no-up", "Patch config + write compose without starting docker").action(async (options3) => {
36752
36854
  await connect(options3);
36753
36855
  });
36754
- devnet.command("down").description("Stop the local Secondlayer stack started by `sl devnet connect`").option("--project <dir>", "Clarinet project directory").option("--purge", "Also remove volumes (wipes the local index)").action(async (options3) => {
36856
+ devnet2.command("down").description("Stop the local Secondlayer stack started by `sl devnet connect`").option("--project <dir>", "Clarinet project directory").option("--purge", "Also remove volumes (wipes the local index)").action(async (options3) => {
36755
36857
  await down(options3);
36756
36858
  });
36757
- devnet.command("status").description("Snapshot of the local stack: ingest, subgraphs, and recent activity").option("-w, --watch", "Refresh every 2s until Ctrl-C").option("--limit <n>", "Recent activity rows to show", "12").action(async (options3) => {
36859
+ devnet2.command("status").description("Snapshot of the local stack: ingest, subgraphs, and recent activity").option("-w, --watch", "Refresh every 2s until Ctrl-C").option("--limit <n>", "Recent activity rows to show", "12").action(async (options3) => {
36758
36860
  await status(options3);
36759
36861
  });
36760
- devnet.command("logs [service]").description("Tail stack logs — all services, or one of: indexer, api, subgraph-processor, postgres").option("--project <dir>", "Clarinet project directory").option("-f, --follow", "Follow log output").option("-n, --lines <n>", "Lines to show from the end of each log").action(async (service, options3) => {
36862
+ devnet2.command("logs [service]").description("Tail stack logs — all services, or one of: indexer, api, subgraph-processor, postgres").option("--project <dir>", "Clarinet project directory").option("-f, --follow", "Follow log output").option("-n, --lines <n>", "Lines to show from the end of each log").action(async (service, options3) => {
36761
36863
  await logs(service, options3);
36762
36864
  });
36763
36865
  }
36764
36866
  function resolveProject(explicit) {
36765
36867
  if (explicit) {
36766
- if (!existsSync6(join8(explicit, "Clarinet.toml"))) {
36868
+ if (!existsSync6(join9(explicit, "Clarinet.toml"))) {
36767
36869
  error(`No Clarinet.toml in ${explicit}`);
36768
36870
  process.exit(1);
36769
36871
  }
@@ -36779,7 +36881,7 @@ function resolveProject(explicit) {
36779
36881
  }
36780
36882
  async function connect(options3) {
36781
36883
  const project = resolveProject(options3.project);
36782
- const devnetToml = join8(project, "settings", "Devnet.toml");
36884
+ const devnetToml = join9(project, "settings", "Devnet.toml");
36783
36885
  mkdirSync4(dirname8(devnetToml), { recursive: true });
36784
36886
  const result = ensureEventObserver(devnetToml, DEFAULT_INDEXER_OBSERVER);
36785
36887
  console.log(`${green("✓")} found Clarinet.toml`);
@@ -36789,7 +36891,7 @@ async function connect(options3) {
36789
36891
  console.log(`${green("✓")} ${result.status === "created" ? "created" : "patched"} settings/Devnet.toml`);
36790
36892
  console.log(dim(` + stacks_node_events_observers = ["${DEFAULT_INDEXER_OBSERVER}"]`));
36791
36893
  }
36792
- const composePath = join8(project, COMPOSE_REL);
36894
+ const composePath = join9(project, COMPOSE_REL);
36793
36895
  mkdirSync4(dirname8(composePath), { recursive: true });
36794
36896
  writeFileSync6(composePath, buildDevnetCompose({ owner: options3.owner, imageTag: options3.imageTag }));
36795
36897
  console.log(`${green("✓")} wrote ${COMPOSE_REL}`);
@@ -36819,7 +36921,7 @@ Stop with: sl devnet down`));
36819
36921
  }
36820
36922
  async function down(options3) {
36821
36923
  const project = resolveProject(options3.project);
36822
- const composePath = join8(project, COMPOSE_REL);
36924
+ const composePath = join9(project, COMPOSE_REL);
36823
36925
  if (!existsSync6(composePath)) {
36824
36926
  error(`No ${COMPOSE_REL} in ${project} — nothing to stop. Run \`sl devnet connect\` first.`);
36825
36927
  process.exit(1);
@@ -36834,7 +36936,7 @@ async function down(options3) {
36834
36936
  }
36835
36937
  async function logs(service, options3) {
36836
36938
  const project = resolveProject(options3.project);
36837
- const composePath = join8(project, COMPOSE_REL);
36939
+ const composePath = join9(project, COMPOSE_REL);
36838
36940
  if (!existsSync6(composePath)) {
36839
36941
  error(`No ${COMPOSE_REL} in ${project} — run \`sl devnet connect\` first.`);
36840
36942
  process.exit(1);
@@ -36960,7 +37062,7 @@ init_subgraphs2();
36960
37062
  init_output();
36961
37063
  init_resolve_auth();
36962
37064
  import { mkdir as mkdir3, writeFile as writeFile3 } from "node:fs/promises";
36963
- import { dirname as dirname9, join as join9 } from "node:path";
37065
+ import { dirname as dirname9, join as join10 } from "node:path";
36964
37066
  import { createStreamsClient } from "@secondlayer/sdk";
36965
37067
  import { STREAMS_EVENT_TYPES } from "@secondlayer/shared";
36966
37068
  var DEFAULT_BASE_URL3 = "https://api.secondlayer.tools";
@@ -37136,7 +37238,7 @@ Examples:
37136
37238
  note(`# downloading ${files.length} file(s) to ${options3.to}`);
37137
37239
  for (const file of files) {
37138
37240
  const bytes = await dumps.download(file);
37139
- const dest = join9(options3.to, file.path);
37241
+ const dest = join10(options3.to, file.path);
37140
37242
  await mkdir3(dirname9(dest), { recursive: true });
37141
37243
  await writeFile3(dest, bytes);
37142
37244
  process.stderr.write(`# ${file.path} (${file.row_count} rows, ${bytes.byteLength} bytes)
@@ -37185,7 +37287,7 @@ init_resolve_auth();
37185
37287
  import { SecondLayer as SecondLayer4 } from "@secondlayer/sdk";
37186
37288
  var DEFAULT_BASE_URL4 = "https://api.secondlayer.tools";
37187
37289
  function registerKeysCommand(program2) {
37188
- const keys = program2.command("keys").description("Manage API keys");
37290
+ const keys = program2.command("keys", { hidden: true }).description("Manage API keys");
37189
37291
  keys.command("create").description("Mint a scoped streams/index read key (requires an account-level key)").option("--product <product>", "Key scope: streams or index", "streams").option("--name <name>", "Optional label for the key").option("--json", "Print the full response as JSON").action(async (o3) => {
37190
37292
  if (o3.product && o3.product !== "streams" && o3.product !== "index") {
37191
37293
  error("--product must be 'streams' or 'index'");
@@ -38424,9 +38526,9 @@ init_output();
38424
38526
 
38425
38527
  // src/lib/project-file.ts
38426
38528
  import { existsSync as existsSync7 } from "node:fs";
38427
- import { mkdir as mkdir4, readFile as readFile3, writeFile as writeFile4 } from "node:fs/promises";
38529
+ import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4 } from "node:fs/promises";
38428
38530
  import { homedir as homedir5 } from "node:os";
38429
- import { dirname as dirname15, join as join12, parse as parsePath2, resolve as resolve11 } from "node:path";
38531
+ import { dirname as dirname15, join as join13, parse as parsePath2, resolve as resolve11 } from "node:path";
38430
38532
  import { z as z4 } from "zod";
38431
38533
  var ProjectFileSchema = z4.object({
38432
38534
  slug: z4.string().min(1)
@@ -38438,15 +38540,15 @@ async function readActiveProject(cwd, globalDefault) {
38438
38540
  const home2 = resolve11(homedir5());
38439
38541
  const fsRoot = parsePath2(dir).root;
38440
38542
  while (true) {
38441
- const candidate = join12(dir, DIRNAME, FILENAME);
38543
+ const candidate = join13(dir, DIRNAME, FILENAME);
38442
38544
  if (existsSync7(candidate)) {
38443
38545
  try {
38444
- const raw = await readFile3(candidate, "utf8");
38546
+ const raw = await readFile4(candidate, "utf8");
38445
38547
  const parsed = ProjectFileSchema.parse(JSON.parse(raw));
38446
38548
  return { slug: parsed.slug, resolvedFrom: candidate };
38447
38549
  } catch {}
38448
38550
  }
38449
- if (existsSync7(join12(dir, ".git")))
38551
+ if (existsSync7(join13(dir, ".git")))
38450
38552
  break;
38451
38553
  if (dir === home2 || dir === fsRoot)
38452
38554
  break;
@@ -38458,15 +38560,15 @@ async function readActiveProject(cwd, globalDefault) {
38458
38560
  if (globalDefault) {
38459
38561
  return {
38460
38562
  slug: globalDefault,
38461
- resolvedFrom: join12(homedir5(), ".secondlayer", "config.json")
38563
+ resolvedFrom: join13(homedir5(), ".secondlayer", "config.json")
38462
38564
  };
38463
38565
  }
38464
38566
  return null;
38465
38567
  }
38466
38568
  async function writeActiveProject(slug, cwd) {
38467
- const dir = join12(resolve11(cwd), DIRNAME);
38569
+ const dir = join13(resolve11(cwd), DIRNAME);
38468
38570
  await mkdir4(dir, { recursive: true });
38469
- const file = join12(dir, FILENAME);
38571
+ const file = join13(dir, FILENAME);
38470
38572
  await writeFile4(file, `${JSON.stringify({ slug }, null, 2)}
38471
38573
  `, "utf8");
38472
38574
  return file;
@@ -38475,7 +38577,7 @@ async function writeActiveProject(slug, cwd) {
38475
38577
  // src/commands/whoami.ts
38476
38578
  init_resolve_auth();
38477
38579
  function registerWhoamiCommand(program2) {
38478
- program2.command("whoami").description("Show the active account, credential source, and project").option("--json", "Output as JSON").action(async (options3) => {
38580
+ program2.command("whoami", { hidden: true }).description("Show the active account, credential source, and project").option("--json", "Output as JSON").action(async (options3) => {
38479
38581
  let auth;
38480
38582
  try {
38481
38583
  auth = await resolveAuth();
@@ -38531,7 +38633,7 @@ init_http();
38531
38633
  init_output();
38532
38634
  init_session();
38533
38635
  function registerLogoutCommand(program2) {
38534
- program2.command("logout").description("Log out and revoke the local session").action(async () => {
38636
+ program2.command("logout", { hidden: true }).description("Log out and revoke the local session").action(async () => {
38535
38637
  const session = await readSession();
38536
38638
  if (!session) {
38537
38639
  info("Not logged in.");
@@ -38553,7 +38655,7 @@ init_config2();
38553
38655
  init_http();
38554
38656
  init_output();
38555
38657
  function registerProjectCommand(program2) {
38556
- const project = program2.command("projects").alias("project").description("Manage your Secondlayer project");
38658
+ const project = program2.command("projects", { hidden: true }).alias("project").description("Manage your Secondlayer project");
38557
38659
  project.command("list").alias("ls").description("List projects in your account").option("--json", "Output as JSON").action(async (options3) => {
38558
38660
  try {
38559
38661
  const res = await httpPlatform("/api/projects");
@@ -38632,12 +38734,16 @@ import {
38632
38734
  compareRangeDigests,
38633
38735
  computeRangeDigest
38634
38736
  } from "@secondlayer/shared/archive/range-digest";
38737
+ import {
38738
+ comparePartitionSemanticDigests,
38739
+ computePartitionSemanticDigest
38740
+ } from "@secondlayer/shared/archive/semantic-digest-builder";
38635
38741
  import { getDb as getDb2 } from "@secondlayer/shared/db";
38636
38742
 
38637
38743
  // src/lib/archive-reference.ts
38638
38744
  import { createHash as createHash2 } from "node:crypto";
38639
- import { readFile as readFile4 } from "node:fs/promises";
38640
- import { dirname as dirname16, join as join13, resolve as resolve12 } from "node:path";
38745
+ import { readFile as readFile5 } from "node:fs/promises";
38746
+ import { dirname as dirname16, join as join14, resolve as resolve12 } from "node:path";
38641
38747
  import {
38642
38748
  checkKeyTrust,
38643
38749
  publicKeyFor,
@@ -38666,7 +38772,7 @@ async function loadReference(source) {
38666
38772
  root = url.toString();
38667
38773
  } else {
38668
38774
  const path11 = resolve12(source);
38669
- manifest = JSON.parse(await readFile4(path11, "utf8"));
38775
+ manifest = JSON.parse(await readFile5(path11, "utf8"));
38670
38776
  root = isLatestPointer(manifest) ? dirname16(path11) : dirname16(dirname16(path11));
38671
38777
  }
38672
38778
  if (!isLatestPointer(manifest)) {
@@ -38674,7 +38780,7 @@ async function loadReference(source) {
38674
38780
  }
38675
38781
  const pointer = manifest;
38676
38782
  const snapshotPath = pointer.snapshot_path;
38677
- const resolved = isRemote ? await fetchJson(`${root.replace(/\/$/, "")}/${snapshotPath}`) : JSON.parse(await readFile4(join13(root, snapshotPath), "utf8"));
38783
+ const resolved = isRemote ? await fetchJson(`${root.replace(/\/$/, "")}/${snapshotPath}`) : JSON.parse(await readFile5(join14(root, snapshotPath), "utf8"));
38678
38784
  if (pointer.snapshot_digest) {
38679
38785
  const digest = createHash2("sha256").update(JSON.stringify((({ signature: _s, key_id: _k, ...rest }) => rest)(resolved))).digest("hex");
38680
38786
  if (digest !== pointer.snapshot_digest) {
@@ -38708,6 +38814,7 @@ function checkSignature(manifest, publicKeyPem, insecure) {
38708
38814
  };
38709
38815
  }
38710
38816
  }
38817
+ var HOSTED_API = "https://api.secondlayer.tools";
38711
38818
  async function resolvePublicKey(explicitPem, apiUrl) {
38712
38819
  if (explicitPem)
38713
38820
  return explicitPem;
@@ -38721,6 +38828,16 @@ async function resolvePublicKey(explicitPem, apiUrl) {
38721
38828
  return;
38722
38829
  }
38723
38830
  }
38831
+ async function resolveArchivePublicKey(input4) {
38832
+ if (input4.explicitPem)
38833
+ return input4.explicitPem;
38834
+ if (input4.envPem)
38835
+ return input4.envPem;
38836
+ if (input4.allowHostedApi) {
38837
+ return resolvePublicKey(undefined, HOSTED_API);
38838
+ }
38839
+ return;
38840
+ }
38724
38841
  async function fetchVerifiedPartition(reference, partition) {
38725
38842
  let bytes;
38726
38843
  if (reference.isRemote) {
@@ -38731,7 +38848,7 @@ async function fetchVerifiedPartition(reference, partition) {
38731
38848
  }
38732
38849
  bytes = Buffer.from(await response2.arrayBuffer());
38733
38850
  } else {
38734
- bytes = await readFile4(join13(reference.root, partition.path));
38851
+ bytes = await readFile5(join14(reference.root, partition.path));
38735
38852
  }
38736
38853
  const digest = createHash2("sha256").update(bytes).digest("hex");
38737
38854
  if (digest !== partition.sha256) {
@@ -38764,7 +38881,7 @@ function statusLabel(status2) {
38764
38881
  return yellow("missing");
38765
38882
  }
38766
38883
  function registerVerifyCommand(program2) {
38767
- program2.command("verify").description("Compare local chain data against a signed archive (read-only; nothing is uploaded)").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("--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", `
38884
+ program2.command("verify").description("Compare local chain data against a signed archive (read-only; nothing is uploaded)").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", `
38768
38885
  Examples:
38769
38886
  $ sl verify --against https://archive.secondlayer.tools/.../snapshots/<digest>.json
38770
38887
  $ sl verify --against ./snapshot.json --from-block 8000000 --to-block 8499999
@@ -38844,13 +38961,52 @@ Exit codes:
38844
38961
  });
38845
38962
  }
38846
38963
  }
38847
- const all2 = [...comparisons, ...countChecks];
38964
+ const semanticChecks = [];
38965
+ const referenceSemantic = (manifest.partition_semantic_digests ?? []).filter((d2) => {
38966
+ if (fromBlock !== undefined && d2.to_block < fromBlock)
38967
+ return false;
38968
+ if (toBlock !== undefined && d2.from_block > toBlock)
38969
+ return false;
38970
+ return true;
38971
+ });
38972
+ if (opts.semantic) {
38973
+ if (referenceSemantic.length === 0) {
38974
+ warn("Reference publishes no semantic digests; --semantic had nothing to compare.");
38975
+ } else {
38976
+ note(`Recomputing ${referenceSemantic.length} partition semantic digests (this is the slow pass).`);
38977
+ const localSemantic = [];
38978
+ let semanticDone = 0;
38979
+ for (const partition of referenceSemantic) {
38980
+ localSemantic.push(await computePartitionSemanticDigest(db, partition.dataset, partition.from_block, partition.to_block));
38981
+ semanticDone++;
38982
+ if (semanticDone % 5 === 0 || semanticDone === referenceSemantic.length) {
38983
+ note(` checked ${semanticDone}/${referenceSemantic.length} partitions`);
38984
+ }
38985
+ }
38986
+ semanticChecks.push(...comparePartitionSemanticDigests(localSemantic, referenceSemantic));
38987
+ }
38988
+ }
38989
+ const all2 = [
38990
+ ...comparisons,
38991
+ ...countChecks,
38992
+ ...semanticChecks.map((s2) => ({
38993
+ dataset: s2.dataset,
38994
+ from_block: s2.from_block,
38995
+ to_block: s2.to_block,
38996
+ status: s2.status,
38997
+ expected_digest: s2.expected_digest,
38998
+ actual_digest: s2.actual_digest,
38999
+ expected_rows: s2.expected_rows,
39000
+ actual_rows: s2.actual_rows
39001
+ }))
39002
+ ];
38848
39003
  const diverged = all2.filter((c4) => c4.status !== "match");
38849
39004
  const report = {
38850
39005
  status: diverged.length === 0 ? "clean" : "diverged",
38851
39006
  reference: origin,
38852
39007
  signature_verified: signature.verified,
38853
39008
  ranges_checked: all2.length,
39009
+ semantic_checks: semanticChecks.length,
38854
39010
  divergent_ranges: diverged
38855
39011
  };
38856
39012
  output({
@@ -38891,7 +39047,7 @@ Exit codes:
38891
39047
  // src/commands/repair.ts
38892
39048
  import { unlink as unlink2, writeFile as writeFile5 } from "node:fs/promises";
38893
39049
  import { tmpdir as tmpdir2 } from "node:os";
38894
- import { join as join14 } from "node:path";
39050
+ import { join as join15 } from "node:path";
38895
39051
  import { ParquetReader } from "@dsnp/parquetjs";
38896
39052
  import {
38897
39053
  compareRangeDigests as compareRangeDigests2,
@@ -38914,7 +39070,7 @@ function asOptionalText(value) {
38914
39070
  return value === null || value === undefined ? null : asText(value);
38915
39071
  }
38916
39072
  async function readBlocksPartition(bytes, label) {
38917
- const path11 = join14(tmpdir2(), `sl-repair-${label}-${process.pid}.parquet`);
39073
+ const path11 = join15(tmpdir2(), `sl-repair-${label}-${process.pid}.parquet`);
38918
39074
  await writeFile5(path11, bytes);
38919
39075
  try {
38920
39076
  const reader = await ParquetReader.openFile(path11);
@@ -39140,7 +39296,7 @@ Exit codes:
39140
39296
  // src/commands/bootstrap.ts
39141
39297
  import { unlink as unlink3, writeFile as writeFile6 } from "node:fs/promises";
39142
39298
  import { tmpdir as tmpdir3 } from "node:os";
39143
- import { join as join15 } from "node:path";
39299
+ import { join as join16 } from "node:path";
39144
39300
  import { ParquetReader as ParquetReader2 } from "@dsnp/parquetjs";
39145
39301
  import { confirm as confirm7 } from "@inquirer/prompts";
39146
39302
  import {
@@ -39149,7 +39305,35 @@ import {
39149
39305
  } from "@secondlayer/shared/archive/copy-loader";
39150
39306
  import { computeRangeDigest as computeRangeDigest3 } from "@secondlayer/shared/archive/range-digest";
39151
39307
  import { getDb as getDb4, getRawClient, sql as sql3 } from "@secondlayer/shared/db";
39308
+
39309
+ // src/lib/bootstrap-resume.ts
39310
+ function planTornImport(input4) {
39311
+ if (input4.hasIndexProgress) {
39312
+ return {
39313
+ action: "refuse",
39314
+ reason: "This database already holds a completed bootstrap."
39315
+ };
39316
+ }
39317
+ if (input4.maxBlockHeight === null || input4.maxBlockHeight <= 0) {
39318
+ return { action: "fresh" };
39319
+ }
39320
+ const blockPartitions = input4.partitions.filter((p2) => p2.dataset === "blocks").sort((a4, b3) => a4.from_block - b3.from_block);
39321
+ const sealed = blockPartitions.filter((p2) => p2.to_block <= (input4.maxBlockHeight ?? 0));
39322
+ const skipThrough = sealed.length > 0 ? Math.max(...sealed.map((p2) => p2.to_block)) : 0;
39323
+ const torn = blockPartitions.find((p2) => p2.from_block <= (input4.maxBlockHeight ?? 0) && p2.to_block > (input4.maxBlockHeight ?? 0));
39324
+ return {
39325
+ action: "resume",
39326
+ truncateFrom: torn ? torn.from_block : null,
39327
+ skipThrough
39328
+ };
39329
+ }
39330
+ function partitionIsLoaded(partition, skipThrough) {
39331
+ return partition.to_block <= skipThrough;
39332
+ }
39333
+
39334
+ // src/commands/bootstrap.ts
39152
39335
  init_output();
39336
+ init_resolve_auth();
39153
39337
  var BOOTSTRAP_EXIT = {
39154
39338
  OK: 0,
39155
39339
  INCOMPLETE: 1,
@@ -39181,7 +39365,7 @@ function formatDuration2(seconds2) {
39181
39365
  return `${(seconds2 / 86400).toFixed(1)} days`;
39182
39366
  }
39183
39367
  async function* readPartitionRows(bytes, label) {
39184
- const path11 = join15(tmpdir3(), `sl-bootstrap-${label}-${process.pid}.parquet`);
39368
+ const path11 = join16(tmpdir3(), `sl-bootstrap-${label}-${process.pid}.parquet`);
39185
39369
  await writeFile6(path11, bytes);
39186
39370
  try {
39187
39371
  const reader = await ParquetReader2.openFile(path11);
@@ -39210,8 +39394,8 @@ async function loadPartition(reference, partition, rawClient) {
39210
39394
  }
39211
39395
  return written;
39212
39396
  }
39213
- function registerBootstrapCommand(program2) {
39214
- program2.command("bootstrap").description("Restore chain history from a verified archive instead of syncing from genesis").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("--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", `
39397
+ function attachBootstrapCommand(cmd) {
39398
+ 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("--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", `
39215
39399
  Examples:
39216
39400
  $ sl bootstrap --against https://archive.secondlayer.tools/.../snapshots/<digest>.json
39217
39401
  $ sl bootstrap --against ./snapshot.json --to-block 4000000 --yes
@@ -39222,7 +39406,11 @@ Exit codes:
39222
39406
  2 refused (non-empty target, or an untrusted reference)`).action(async (opts) => {
39223
39407
  try {
39224
39408
  const reference = await loadReference(opts.against);
39225
- const publicKey = await resolvePublicKey(opts.publicKey, process.env.SL_API_URL ?? "https://api.secondlayer.tools");
39409
+ const publicKey = await resolveArchivePublicKey({
39410
+ explicitPem: opts.publicKey,
39411
+ envPem: process.env.ARCHIVE_SIGNING_PUBLIC_KEY ?? process.env.STREAMS_SIGNING_PUBLIC_KEY,
39412
+ allowHostedApi: !isOssMode()
39413
+ });
39226
39414
  const signature = checkSignature(reference.manifest, publicKey, false);
39227
39415
  if (!signature.verified) {
39228
39416
  printError(`Refusing to bootstrap from an untrusted archive: ${signature.reason}.`, {
@@ -39231,15 +39419,31 @@ Exit codes:
39231
39419
  process.exit(BOOTSTRAP_EXIT.REFUSED);
39232
39420
  }
39233
39421
  const db = getDb4();
39234
- const existing = await db.selectFrom("blocks").select(({ fn }) => fn.countAll().as("count")).executeTakeFirst();
39235
- if (Number(existing?.count ?? 0) > 0) {
39236
- printError("This database already holds blocks.", {
39422
+ const progress = await db.selectFrom("index_progress").select("network").executeTakeFirst();
39423
+ const maxHeightRow = await db.selectFrom("blocks").select(({ fn }) => fn.max("height").as("max")).executeTakeFirst();
39424
+ const maxBlockHeight = maxHeightRow?.max === null || maxHeightRow?.max === undefined ? null : Number(maxHeightRow.max);
39425
+ const toBlock = opts.toBlock === undefined ? undefined : Number(opts.toBlock);
39426
+ const declared = (reference.manifest.partitions ?? []).filter((p2) => toBlock === undefined || p2.to_block <= toBlock);
39427
+ const resume = planTornImport({
39428
+ hasIndexProgress: !!progress,
39429
+ maxBlockHeight,
39430
+ partitions: declared
39431
+ });
39432
+ if (resume.action === "refuse") {
39433
+ printError(resume.reason, {
39237
39434
  hint: "Bootstrap is for a fresh instance. To fix an existing one, use `sl repair`."
39238
39435
  });
39239
39436
  process.exit(BOOTSTRAP_EXIT.REFUSED);
39240
39437
  }
39241
- const toBlock = opts.toBlock === undefined ? undefined : Number(opts.toBlock);
39242
- const partitions = (reference.manifest.partitions ?? []).filter((p2) => toBlock === undefined || p2.to_block <= toBlock);
39438
+ if (resume.action === "resume" && resume.truncateFrom !== null) {
39439
+ await sql3`DELETE FROM events WHERE block_height >= ${resume.truncateFrom}`.execute(db);
39440
+ await sql3`DELETE FROM transactions WHERE block_height >= ${resume.truncateFrom}`.execute(db);
39441
+ await sql3`DELETE FROM blocks WHERE height >= ${resume.truncateFrom}`.execute(db);
39442
+ note(`Resuming torn import: truncated from height ${resume.truncateFrom}.`);
39443
+ } else if (resume.action === "resume") {
39444
+ note(`Resuming import after sealed height ${resume.skipThrough.toLocaleString()}.`);
39445
+ }
39446
+ const partitions = declared.filter((p2) => resume.action === "fresh" || !partitionIsLoaded(p2, resume.skipThrough));
39243
39447
  if (partitions.length === 0) {
39244
39448
  printError("The archive has no partitions for that range.", {
39245
39449
  hint: "Check --to-block against the manifest's coverage."
@@ -39364,138 +39568,217 @@ Exit codes:
39364
39568
  }
39365
39569
  });
39366
39570
  }
39367
- // src/commands/faucet.ts
39368
- import { randomBytes as randomBytes2 } from "node:crypto";
39369
- import { readFile as readFile5 } from "node:fs/promises";
39370
- import { join as join16 } from "node:path";
39371
- import { http as http3, createWalletClient } from "@secondlayer/stacks";
39372
- import { mnemonicToAccount } from "@secondlayer/stacks/accounts";
39373
- import { devnet } from "@secondlayer/stacks/chains";
39374
- import { Cl } from "@secondlayer/stacks/clarity";
39375
- import { parse as parseToml2 } from "smol-toml";
39571
+ function registerBootstrapCommand(program2) {
39572
+ attachBootstrapCommand(program2.command("bootstrap").description("Restore chain history from a verified archive instead of syncing from genesis"));
39573
+ }
39574
+ // src/lib/instance-init.ts
39575
+ import { randomBytes as randomBytes3 } from "node:crypto";
39576
+ import { existsSync as existsSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync7 } from "node:fs";
39577
+ import { resolve as resolve13 } from "node:path";
39578
+ import { generateEd25519KeyPair } from "@secondlayer/shared/crypto/ed25519";
39579
+ import { INSTANCE_NETWORKS } from "@secondlayer/shared/db/queries/instance";
39580
+ var INSTANCE_ENV_FILE = ".env.local";
39581
+ function parseInstanceNetwork(value) {
39582
+ const network = value.trim().toLowerCase();
39583
+ if (INSTANCE_NETWORKS.includes(network)) {
39584
+ return network;
39585
+ }
39586
+ throw new Error(`network must be mainnet, testnet, or devnet (got ${value})`);
39587
+ }
39588
+ function generateInstanceToken() {
39589
+ return randomBytes3(32).toString("hex");
39590
+ }
39591
+ function generateSecretsKey() {
39592
+ return randomBytes3(32).toString("hex");
39593
+ }
39594
+ function generateSigningPrivateKey() {
39595
+ return generateEd25519KeyPair().privateKeyPem;
39596
+ }
39597
+ function escapeEnvValue(value) {
39598
+ if (!/[\s#"']/.test(value) && !value.includes("\\"))
39599
+ return value;
39600
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n")}"`;
39601
+ }
39602
+ function unescapeEnvValue(raw) {
39603
+ let value = raw.trim();
39604
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
39605
+ value = value.slice(1, -1);
39606
+ }
39607
+ return value.replace(/\\n/g, `
39608
+ `).replace(/\\"/g, '"').replace(/\\\\/g, "\\");
39609
+ }
39610
+ function readEnvValue(envPath, key) {
39611
+ if (!existsSync8(envPath))
39612
+ return null;
39613
+ const match = readFileSync7(envPath, "utf8").match(new RegExp(`^${key}=(.*)$`, "m"));
39614
+ return match ? unescapeEnvValue(match[1]) : null;
39615
+ }
39616
+ function buildInstanceEnv(input4) {
39617
+ const token = input4.existing?.INSTANCE_TOKEN || input4.existing?.SL_API_KEY || generateInstanceToken();
39618
+ const secrets = input4.existing?.SECONDLAYER_SECRETS_KEY || generateSecretsKey();
39619
+ const signing = input4.existing?.STREAMS_SIGNING_PRIVATE_KEY || input4.existing?.SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY || generateSigningPrivateKey();
39620
+ const apiUrl = input4.apiUrl ?? "http://127.0.0.1:3800";
39621
+ const archiveKey = input4.existing?.ARCHIVE_SIGNING_PUBLIC_KEY;
39622
+ return {
39623
+ INSTANCE_MODE: "oss",
39624
+ STACKS_NETWORK: input4.network,
39625
+ INSTANCE_TOKEN: token,
39626
+ SECONDLAYER_SECRETS_KEY: secrets,
39627
+ STREAMS_SIGNING_PRIVATE_KEY: signing,
39628
+ SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY: signing,
39629
+ ALLOW_UNSIGNED_WEBHOOKS: "false",
39630
+ SL_API_URL: input4.existing?.SL_API_URL || apiUrl,
39631
+ SL_API_KEY: token,
39632
+ ...archiveKey ? { ARCHIVE_SIGNING_PUBLIC_KEY: archiveKey } : {}
39633
+ };
39634
+ }
39635
+ function renderInstanceEnv(env2) {
39636
+ const lines2 = [
39637
+ "# Generated by sl init. Do not commit.",
39638
+ `INSTANCE_MODE=${env2.INSTANCE_MODE}`,
39639
+ `STACKS_NETWORK=${env2.STACKS_NETWORK}`,
39640
+ `INSTANCE_TOKEN=${env2.INSTANCE_TOKEN}`,
39641
+ `SECONDLAYER_SECRETS_KEY=${env2.SECONDLAYER_SECRETS_KEY}`,
39642
+ `STREAMS_SIGNING_PRIVATE_KEY=${escapeEnvValue(env2.STREAMS_SIGNING_PRIVATE_KEY)}`,
39643
+ `SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY=${escapeEnvValue(env2.SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY)}`,
39644
+ `ALLOW_UNSIGNED_WEBHOOKS=${env2.ALLOW_UNSIGNED_WEBHOOKS}`,
39645
+ `SL_API_URL=${env2.SL_API_URL}`,
39646
+ `SL_API_KEY=${env2.SL_API_KEY}`
39647
+ ];
39648
+ if (env2.ARCHIVE_SIGNING_PUBLIC_KEY) {
39649
+ lines2.push(`ARCHIVE_SIGNING_PUBLIC_KEY=${escapeEnvValue(env2.ARCHIVE_SIGNING_PUBLIC_KEY)}`);
39650
+ }
39651
+ lines2.push("");
39652
+ return lines2.join(`
39653
+ `);
39654
+ }
39655
+ function writeInstanceEnv(cwd, env2, file = INSTANCE_ENV_FILE) {
39656
+ const path11 = resolve13(cwd, file);
39657
+ writeFileSync7(path11, renderInstanceEnv(env2), { mode: 384 });
39658
+ return path11;
39659
+ }
39660
+ function loadExistingInstanceEnv(cwd, file = INSTANCE_ENV_FILE) {
39661
+ const path11 = resolve13(cwd, file);
39662
+ return {
39663
+ INSTANCE_TOKEN: readEnvValue(path11, "INSTANCE_TOKEN"),
39664
+ SECONDLAYER_SECRETS_KEY: readEnvValue(path11, "SECONDLAYER_SECRETS_KEY"),
39665
+ STREAMS_SIGNING_PRIVATE_KEY: readEnvValue(path11, "STREAMS_SIGNING_PRIVATE_KEY"),
39666
+ SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY: readEnvValue(path11, "SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY"),
39667
+ SL_API_URL: readEnvValue(path11, "SL_API_URL"),
39668
+ SL_API_KEY: readEnvValue(path11, "SL_API_KEY"),
39669
+ ARCHIVE_SIGNING_PUBLIC_KEY: readEnvValue(path11, "ARCHIVE_SIGNING_PUBLIC_KEY")
39670
+ };
39671
+ }
39672
+
39673
+ // src/commands/init.ts
39376
39674
  init_output();
39377
- var DUST_LIMIT_SATS = 546n;
39378
- var DEFAULT_AMOUNT_SATS = 1000000n;
39379
- async function readDeployerMnemonic(projectDir) {
39380
- const path11 = join16(projectDir, "settings", "Devnet.toml");
39381
- let raw;
39382
- try {
39383
- raw = await readFile5(path11, "utf8");
39384
- } catch {
39385
- throw new Error(`could not read ${path11}`);
39675
+ function registerInitCommand(program2) {
39676
+ program2.command("init").description("Write local env (token, secrets key, webhook signing key). Idempotent.").option("--network <network>", "mainnet, testnet, or devnet", process.env.STACKS_NETWORK ?? "mainnet").option("--api-url <url>", "Local API URL", "http://127.0.0.1:3800").option("--force", "Overwrite generated values even if .env.local exists").action((opts) => {
39677
+ const network = parseInstanceNetwork(opts.network);
39678
+ const existing = opts.force ? {} : loadExistingInstanceEnv(process.cwd());
39679
+ const env2 = buildInstanceEnv({
39680
+ network,
39681
+ existing,
39682
+ apiUrl: opts.apiUrl
39683
+ });
39684
+ const path11 = writeInstanceEnv(process.cwd(), env2);
39685
+ success(`Wrote ${INSTANCE_ENV_FILE}`);
39686
+ note(formatKeyValue([
39687
+ ["network", env2.STACKS_NETWORK],
39688
+ ["api", env2.SL_API_URL],
39689
+ ["env", path11]
39690
+ ]));
39691
+ note("Export these or pass --env-file to compose. Do not commit the file.");
39692
+ });
39693
+ }
39694
+ // src/lib/observer-stanza.ts
39695
+ var OBSERVER_MODES = ["indexer", "signer-shared"];
39696
+ var RECOVERY_SOURCES = ["journal", "archive"];
39697
+
39698
+ class UnsupportedObserverError extends Error {
39699
+ name = "UnsupportedObserverError";
39700
+ }
39701
+ function parseObserverMode(value) {
39702
+ const mode = value.trim().toLowerCase();
39703
+ if (OBSERVER_MODES.includes(mode)) {
39704
+ return mode;
39386
39705
  }
39387
- const parsed = parseToml2(raw);
39388
- const mnemonic = parsed.accounts?.deployer?.mnemonic;
39389
- if (!mnemonic) {
39390
- throw new Error(`no accounts.deployer.mnemonic in ${path11}`);
39706
+ throw new UnsupportedObserverError(`observer mode must be indexer or signer-shared (got ${value})`);
39707
+ }
39708
+ function parseRecoverySource(value) {
39709
+ const source = value.trim().toLowerCase();
39710
+ if (RECOVERY_SOURCES.includes(source)) {
39711
+ return source;
39391
39712
  }
39392
- return mnemonic;
39713
+ throw new UnsupportedObserverError(`recovery source must be journal or archive (got ${value})`);
39393
39714
  }
39394
- async function readBurnAnchor(rpcUrl) {
39395
- const info2 = await (await fetch(`${rpcUrl.replace(/\/$/, "")}/v2/info`, {
39396
- signal: AbortSignal.timeout(1e4)
39397
- })).json();
39398
- const height = info2.burn_block_height;
39399
- if (typeof height !== "number") {
39400
- throw new Error("node did not report a burn block height");
39715
+ function parseObserverEndpoint(value) {
39716
+ const trimmed = value.trim();
39717
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed) || trimmed.startsWith("unix:")) {
39718
+ throw new UnsupportedObserverError(`observer endpoint must be host:port, not a URL (${value})`);
39401
39719
  }
39402
- const burn = await (await fetch(`${rpcUrl.replace(/\/$/, "")}/extended/v2/burn-blocks/${height}`, { signal: AbortSignal.timeout(1e4) })).json();
39403
- if (!burn.burn_block_hash) {
39404
- throw new Error(`node did not report a hash for burn block ${height}`);
39720
+ const match = trimmed.match(/^([a-zA-Z0-9._-]+):(\d+)$/);
39721
+ if (!match) {
39722
+ throw new UnsupportedObserverError(`observer endpoint must be host:port (got ${value})`);
39405
39723
  }
39406
- return { height, hash: burn.burn_block_hash.replace(/^0x/, "") };
39724
+ const port = Number(match[2]);
39725
+ if (port < 1 || port > 65535) {
39726
+ throw new UnsupportedObserverError(`observer port out of range (${port})`);
39727
+ }
39728
+ return trimmed;
39407
39729
  }
39408
- function parseAmount(value) {
39409
- if (value === undefined)
39410
- return DEFAULT_AMOUNT_SATS;
39411
- const parsed = BigInt(value);
39412
- if (parsed < DUST_LIMIT_SATS) {
39413
- throw new Error(`--amount must be at least ${DUST_LIMIT_SATS} sats (the protocol dust limit), got ${parsed}`);
39730
+ var LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "::1"]);
39731
+ function validateObserverStanza(input4) {
39732
+ const endpoint = parseObserverEndpoint(input4.endpoint);
39733
+ const host = endpoint.slice(0, endpoint.lastIndexOf(":"));
39734
+ if (input4.network !== "devnet" && LOOPBACK_HOSTS.has(host.toLowerCase())) {
39735
+ throw new UnsupportedObserverError(`loopback observer endpoint is not container-visible on ${input4.network}; use a docker DNS name such as indexer:3700`);
39414
39736
  }
39415
- return parsed;
39737
+ if (input4.mode === "signer-shared" && !input4.recovery) {
39738
+ throw new UnsupportedObserverError("signer-shared mode requires --recovery journal or --recovery archive");
39739
+ }
39740
+ return { endpoint };
39741
+ }
39742
+ function renderObserverStanza(input4) {
39743
+ const { endpoint } = validateObserverStanza(input4);
39744
+ const indexer = input4.mode === "indexer";
39745
+ const timeoutMs = indexer ? 2000 : 500;
39746
+ const disableRetries = !indexer;
39747
+ const comment = indexer ? "# Pure indexer: retry delivery. A slow observer can stall the node." : "# Signer-shared: do not retry. Missed blocks refill from the journal/archive.";
39748
+ return [
39749
+ comment,
39750
+ "[[events_observer]]",
39751
+ `endpoint = "${endpoint}"`,
39752
+ 'events_keys = ["*"]',
39753
+ `timeout_ms = ${timeoutMs}`,
39754
+ `disable_retries = ${disableRetries}`,
39755
+ ""
39756
+ ].join(`
39757
+ `);
39758
+ }
39759
+ function defaultObserverEndpoint(network) {
39760
+ if (network === "devnet")
39761
+ return "127.0.0.1:3700";
39762
+ return "indexer:3700";
39416
39763
  }
39417
- function registerFaucetCommand(program2) {
39418
- const faucet = program2.command("faucet").description("Mint test assets on a local devnet");
39419
- faucet.command("sbtc").description("Mint mock sBTC to an address on your local devnet").requiredOption("--to <address>", "recipient Stacks address").option("--amount <sats>", `amount in sats (default ${DEFAULT_AMOUNT_SATS}, minimum ${DUST_LIMIT_SATS})`).option("--contract <id>", "sBTC deposit contract (default: <deployer>.sbtc-deposit)").option("--rpc-url <url>", "devnet node RPC (default http://localhost:3999)").option("--json", "Output as JSON").addHelpText("after", `
39420
- Examples:
39421
- $ sl faucet sbtc --to ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM
39422
- $ sl faucet sbtc --to ST2... --amount 5000000
39423
39764
 
39424
- Requires a devnet running with the sBTC contract set deployed by your
39425
- Clarinet deployer — that deployer is the signer set on your own chain, which
39426
- is what makes minting possible without Bitcoin.`).action(async (opts) => {
39427
- try {
39428
- const network = process.env.STACKS_NETWORK ?? "devnet";
39429
- if (network !== "devnet" && network !== "local") {
39430
- printError(`sl faucet only works on devnet, not ${network}.`, {
39431
- hint: "Mock sBTC is mintable because you deployed the contracts. On testnet or mainnet the signer set is not yours."
39432
- });
39433
- process.exit(2);
39434
- }
39435
- const rpcUrl = opts.rpcUrl ?? "http://localhost:3999";
39436
- const amount = parseAmount(opts.amount);
39437
- const projectDir = findClarinetProject(process.cwd());
39438
- if (!projectDir) {
39439
- printError("No Clarinet project found.", {
39440
- hint: "Run this from a project containing Clarinet.toml, or pass --contract explicitly."
39441
- });
39442
- process.exit(2);
39443
- }
39444
- const mnemonic = await readDeployerMnemonic(projectDir);
39445
- const account = mnemonicToAccount(mnemonic, {
39446
- addressVersion: devnet.addressVersion.singleSig
39447
- });
39448
- const contract = opts.contract ?? `${account.address}.sbtc-deposit`;
39449
- const anchor = await readBurnAnchor(rpcUrl);
39450
- const client3 = createWalletClient({
39451
- account,
39452
- chain: devnet,
39453
- transport: http3(rpcUrl)
39454
- });
39455
- const depositTxid = randomBytes2(32).toString("hex");
39456
- const sweepTxid = randomBytes2(32).toString("hex");
39457
- note(` minting ${amount} sats to ${opts.to}`);
39458
- note(` via ${contract} as signer ${account.address}`);
39459
- const txid = await client3.callContract({
39460
- contract,
39461
- functionName: "complete-deposit-wrapper",
39462
- functionArgs: [
39463
- Cl.bufferFromHex(depositTxid),
39464
- Cl.uint(0),
39465
- Cl.uint(amount),
39466
- Cl.principal(opts.to),
39467
- Cl.bufferFromHex(anchor.hash),
39468
- Cl.uint(anchor.height),
39469
- Cl.bufferFromHex(sweepTxid)
39470
- ],
39471
- postConditionMode: "allow"
39472
- });
39473
- output({
39474
- json: opts.json,
39475
- data: {
39476
- txid,
39477
- recipient: opts.to,
39478
- amount_sats: amount.toString(),
39479
- contract,
39480
- burn_height: anchor.height
39481
- },
39482
- human: () => {
39483
- success(`Minted ${amount} sats of mock sBTC to ${opts.to}.`);
39484
- note(` tx ${txid}`);
39485
- console.error(dim(" Mock sBTC on your devnet — not the canonical asset."));
39486
- }
39487
- });
39488
- } catch (err) {
39489
- const message = err instanceof Error ? err.message : String(err);
39490
- const hint = /ECONNREFUSED|fetch failed/.test(message) ? "Is your devnet running? Start it with `clarinet devnet start`." : /NoSuchContract|does not exist/.test(message) ? "Deploy the sBTC contract set to your devnet first, or pass --contract." : "Check that your Clarinet deployer matches the sBTC contract deployer.";
39491
- printError(message, { hint });
39492
- process.exit(1);
39765
+ // src/commands/observer.ts
39766
+ init_output();
39767
+ function registerObserverCommand(program2) {
39768
+ program2.command("observer").description("Print the Stacks events_observer stanza for this node mode").option("--mode <mode>", "indexer (retry) or signer-shared (no retry)", "indexer").option("--endpoint <host:port>", "Observer callback the node will POST to").option("--recovery <source>", "Required for signer-shared: journal or archive").option("--network <network>", "mainnet, testnet, or devnet", process.env.STACKS_NETWORK ?? "mainnet").action((opts) => {
39769
+ const mode = parseObserverMode(opts.mode);
39770
+ const network = parseInstanceNetwork(opts.network);
39771
+ const endpoint = opts.endpoint ?? defaultObserverEndpoint(network);
39772
+ const recovery = opts.recovery ? parseRecoverySource(opts.recovery) : undefined;
39773
+ if (mode === "signer-shared") {
39774
+ note("Signer-shared nodes skip observer retries. Pair with a journal or archive for completeness.");
39493
39775
  }
39776
+ writeData(renderObserverStanza({ mode, endpoint, network, recovery }));
39494
39777
  });
39495
39778
  }
39496
39779
  // src/cli.ts
39497
39780
  var { version } = package_default;
39498
- program.name("secondlayer").alias("sl").description("SecondLayer CLI — dedicated Stacks indexing + real-time subgraphs").version(version).option("--network <network>", "Override network (local, testnet, mainnet)").option("--api-key <key>", "API credential (overrides SL_API_KEY)").option("--api-url <url>", "API endpoint (overrides SL_API_URL)").showSuggestionAfterError(true).showHelpAfterError("(run `sl --help` to see available commands)");
39781
+ program.name("secondlayer").alias("sl").description("Secondlayer CLI — self-hosted Stacks data runtime").version(version).option("--network <network>", "Override network (local, testnet, mainnet)").option("--api-key <key>", "API credential (overrides SL_API_KEY)").option("--api-url <url>", "API endpoint (overrides SL_API_URL)").showSuggestionAfterError(true).showHelpAfterError("(run `sl --help` to see available commands)");
39499
39782
  program.hook("preAction", (thisCommand) => {
39500
39783
  const { network, apiKey, apiUrl } = thisCommand.opts();
39501
39784
  if (network)
@@ -39507,16 +39790,15 @@ program.hook("preAction", (thisCommand) => {
39507
39790
  });
39508
39791
  program.addHelpText("after", `
39509
39792
  Quickstart:
39510
- $ sl login # Authenticate (magic-link email)
39793
+ $ sl init --network mainnet
39794
+ $ sl bootstrap --against <manifest>
39511
39795
  $ sl subgraphs create my-watcher --template sip-010-balances
39512
39796
  $ sl subgraphs deploy subgraphs/my-watcher.ts
39513
- $ sl subgraphs status my-watcher
39514
39797
  `);
39515
39798
  program.commandsGroup("Getting started:");
39516
- program.command("init").description("Initialize a new secondlayer.config.ts file").action(async () => {
39517
- const { init: init3 } = await Promise.resolve().then(() => (init_init(), exports_init));
39518
- await init3();
39519
- });
39799
+ registerInitCommand(program);
39800
+ registerBootstrapCommand(program);
39801
+ registerObserverCommand(program);
39520
39802
  registerLoginCommand(program);
39521
39803
  registerLogoutCommand(program);
39522
39804
  registerWhoamiCommand(program);
@@ -39544,13 +39826,11 @@ Examples:
39544
39826
  program.commandsGroup("Local development:");
39545
39827
  registerLocalCommand(program);
39546
39828
  registerDevnetCommand(program);
39547
- registerFaucetCommand(program);
39548
39829
  program.commandsGroup("Diagnostics:");
39549
39830
  registerStatusCommand(program);
39550
39831
  registerDoctorCommand(program);
39551
39832
  registerConfigCommand(program);
39552
39833
  registerContextCommand(program);
39553
- registerBootstrapCommand(program);
39554
39834
  registerVerifyCommand(program);
39555
39835
  registerRepairCommand(program);
39556
39836
  program.commandsGroup("Account:");
@@ -39558,5 +39838,5 @@ registerAccountCommand(program);
39558
39838
  registerKeysCommand(program);
39559
39839
  program.parse();
39560
39840
 
39561
- //# debugId=F913417C10273C0264756E2164756E21
39841
+ //# debugId=8F8395688EA7983464756E2164756E21
39562
39842
  //# sourceMappingURL=cli.js.map