@secondlayer/cli 13.0.3 → 14.0.1

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 +28 -14
  2. package/dist/cli.js +1847 -819
  3. package/dist/cli.js.map +41 -40
  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);
2520
2557
  }
2521
- var SessionSchema, SESSION_DIR, SESSION_PATH;
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);
2566
+ }
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 = (() => {
@@ -8753,57 +8831,111 @@ var init_contract = __esm(() => {
8753
8831
  // src/parsers/clarity.ts
8754
8832
  import { promises as fs2 } from "node:fs";
8755
8833
  import { normalizeAbi } from "@secondlayer/stacks/clarity";
8756
- async function parseClarityFile(filePath) {
8757
- try {
8758
- const content = await fs2.readFile(filePath, "utf-8");
8759
- const result = parseClarityContent(content);
8760
- if (result.functions.length === 0) {
8761
- console.warn(`⚠️ No functions found in ${filePath}. For complex contracts, deploy first and use the contract address instead.`);
8762
- }
8763
- return result;
8764
- } catch (error2) {
8765
- throw new Error(`Unable to parse ${filePath}. For complex contracts, deploy first and use the contract address instead.
8766
- Original error: ${error2}`);
8767
- }
8834
+ function isWhitespace(char) {
8835
+ return char === " " || char === "\t" || char === "\r" || char === `
8836
+ `;
8768
8837
  }
8769
- function parseClarityContent(content) {
8770
- const functions = [];
8771
- const functionRegex = /\(define-(public|read-only|private)\s+\(([^)]+)\)([\s\S]*?)\)\s*$/gm;
8772
- let match = functionRegex.exec(content);
8773
- while (match !== null) {
8774
- const [, access, signature, body] = match;
8775
- const func = parseFunctionSignature(signature, access, body);
8776
- if (func) {
8777
- functions.push(func);
8838
+ function isCommentStart(source, index) {
8839
+ return source[index] === ";" && source[index + 1] === ";";
8840
+ }
8841
+ function tokenize(source) {
8842
+ const tokens = [];
8843
+ let i = 0;
8844
+ while (i < source.length) {
8845
+ const char = source[i];
8846
+ if (isCommentStart(source, i)) {
8847
+ while (i < source.length && source[i] !== `
8848
+ `)
8849
+ i++;
8850
+ continue;
8851
+ }
8852
+ if (isWhitespace(char)) {
8853
+ i++;
8854
+ continue;
8778
8855
  }
8779
- match = functionRegex.exec(content);
8856
+ if (char === '"') {
8857
+ let end2 = i + 1;
8858
+ while (end2 < source.length && source[end2] !== '"') {
8859
+ if (source[end2] === "\\")
8860
+ end2++;
8861
+ end2++;
8862
+ }
8863
+ tokens.push(source.slice(i, Math.min(end2 + 1, source.length)));
8864
+ i = end2 + 1;
8865
+ continue;
8866
+ }
8867
+ if (DELIMITERS.has(char)) {
8868
+ tokens.push(char);
8869
+ i++;
8870
+ continue;
8871
+ }
8872
+ let end = i;
8873
+ while (end < source.length && !isWhitespace(source[end]) && !DELIMITERS.has(source[end]) && source[end] !== '"' && !isCommentStart(source, end)) {
8874
+ end++;
8875
+ }
8876
+ tokens.push(source.slice(i, end));
8877
+ i = end;
8780
8878
  }
8781
- return { functions };
8879
+ return tokens;
8782
8880
  }
8783
- function parseFunctionSignature(signature, access, body) {
8784
- const parts = signature.trim().split(/\s+/);
8785
- const name = parts[0];
8786
- const args = [];
8787
- for (let i = 1;i < parts.length; i += 2) {
8788
- if (parts[i] && parts[i + 1]) {
8789
- const argName = parts[i].replace(/[()]/g, "");
8790
- const argType = parseType(parts[i + 1]);
8791
- if (argType) {
8792
- args.push({ name: argName, type: argType });
8793
- }
8881
+ function readForms(source) {
8882
+ const tokens = tokenize(source);
8883
+ let pos = 0;
8884
+ function readNode() {
8885
+ const token = tokens[pos];
8886
+ if (token === undefined)
8887
+ return null;
8888
+ if (token === "(") {
8889
+ pos++;
8890
+ return { kind: "list", items: readList() };
8891
+ }
8892
+ if (token === "{") {
8893
+ pos++;
8894
+ return { kind: "tuple", fields: readTupleFields() };
8895
+ }
8896
+ if (DELIMITERS.has(token)) {
8897
+ pos++;
8898
+ return null;
8794
8899
  }
8900
+ pos++;
8901
+ return { kind: "atom", value: token };
8902
+ }
8903
+ function readList() {
8904
+ const items = [];
8905
+ while (pos < tokens.length && tokens[pos] !== ")") {
8906
+ const node = readNode();
8907
+ if (node)
8908
+ items.push(node);
8909
+ }
8910
+ pos++;
8911
+ return items;
8912
+ }
8913
+ function readTupleFields() {
8914
+ const fields = [];
8915
+ while (pos < tokens.length && tokens[pos] !== "}") {
8916
+ const name = tokens[pos];
8917
+ pos++;
8918
+ if (name === "," || name === ":")
8919
+ continue;
8920
+ if (tokens[pos] === ":")
8921
+ pos++;
8922
+ const value = readNode();
8923
+ if (value)
8924
+ fields.push({ name, value });
8925
+ }
8926
+ pos++;
8927
+ return fields;
8795
8928
  }
8796
- const outputs = inferReturnType(body);
8797
- return {
8798
- name,
8799
- access,
8800
- args,
8801
- outputs
8802
- };
8929
+ const forms = [];
8930
+ while (pos < tokens.length) {
8931
+ const node = readNode();
8932
+ if (node)
8933
+ forms.push(node);
8934
+ }
8935
+ return forms;
8803
8936
  }
8804
- function parseType(typeStr) {
8805
- const cleaned = typeStr.replace(/[()]/g, "").trim();
8806
- switch (cleaned) {
8937
+ function atomToAbi(value) {
8938
+ switch (value) {
8807
8939
  case "uint":
8808
8940
  case "uint128":
8809
8941
  return "uint128";
@@ -8815,35 +8947,201 @@ function parseType(typeStr) {
8815
8947
  case "principal":
8816
8948
  return "principal";
8817
8949
  case "trait_reference":
8818
- return "principal";
8950
+ return "trait_reference";
8819
8951
  default:
8820
- if (cleaned.startsWith("string-ascii")) {
8821
- return { "string-ascii": { length: 256 } };
8952
+ return value.startsWith("<") ? "trait_reference" : UNKNOWN;
8953
+ }
8954
+ }
8955
+ function readLength(node) {
8956
+ if (node?.kind !== "atom")
8957
+ return null;
8958
+ const length = Number.parseInt(node.value, 10);
8959
+ return Number.isSafeInteger(length) && length >= 0 ? length : null;
8960
+ }
8961
+ function readFieldPairs(nodes) {
8962
+ return nodes.flatMap((node) => {
8963
+ if (node.kind !== "list")
8964
+ return [];
8965
+ const [name, type] = node.items;
8966
+ if (name?.kind !== "atom" || !type)
8967
+ return [];
8968
+ return [{ name: name.value, type: typeToAbi(type) }];
8969
+ });
8970
+ }
8971
+ function typeToAbi(node) {
8972
+ if (node.kind === "atom")
8973
+ return atomToAbi(node.value);
8974
+ if (node.kind === "tuple") {
8975
+ return {
8976
+ tuple: node.fields.map((field) => ({
8977
+ name: field.name,
8978
+ type: typeToAbi(field.value)
8979
+ }))
8980
+ };
8981
+ }
8982
+ const [head, ...args] = node.items;
8983
+ if (!head)
8984
+ return UNKNOWN;
8985
+ if (head.kind !== "atom") {
8986
+ const fields = readFieldPairs(node.items);
8987
+ return fields.length > 0 ? { tuple: fields } : UNKNOWN;
8988
+ }
8989
+ switch (head.value) {
8990
+ case "string-ascii": {
8991
+ const length = readLength(args[0]);
8992
+ return length === null ? UNKNOWN : { "string-ascii": { length } };
8993
+ }
8994
+ case "string-utf8": {
8995
+ const length = readLength(args[0]);
8996
+ return length === null ? UNKNOWN : { "string-utf8": { length } };
8997
+ }
8998
+ case "buff": {
8999
+ const length = readLength(args[0]);
9000
+ return length === null ? UNKNOWN : { buff: { length } };
9001
+ }
9002
+ case "optional":
9003
+ return args[0] ? { optional: typeToAbi(args[0]) } : UNKNOWN;
9004
+ case "list": {
9005
+ const length = readLength(args[0]);
9006
+ if (length === null || !args[1])
9007
+ return UNKNOWN;
9008
+ return { list: { length, type: typeToAbi(args[1]) } };
9009
+ }
9010
+ case "response":
9011
+ return args[0] && args[1] ? { response: { ok: typeToAbi(args[0]), error: typeToAbi(args[1]) } } : UNKNOWN;
9012
+ case "tuple": {
9013
+ const fields = readFieldPairs(args);
9014
+ return fields.length > 0 ? { tuple: fields } : UNKNOWN;
9015
+ }
9016
+ default:
9017
+ return UNKNOWN;
9018
+ }
9019
+ }
9020
+ function toFunction(form, access) {
9021
+ const signature = form[1];
9022
+ if (signature?.kind !== "list")
9023
+ return null;
9024
+ const [nameNode, ...argNodes] = signature.items;
9025
+ if (nameNode?.kind !== "atom")
9026
+ return null;
9027
+ const args = argNodes.flatMap((node) => {
9028
+ if (node.kind !== "list")
9029
+ return [];
9030
+ const [name, type] = node.items;
9031
+ if (name?.kind !== "atom" || !type)
9032
+ return [];
9033
+ return [{ name: name.value, type: typeToAbi(type) }];
9034
+ });
9035
+ return {
9036
+ name: nameNode.value,
9037
+ access,
9038
+ args,
9039
+ outputs: access === "public" ? { response: { ok: UNKNOWN, error: UNKNOWN } } : UNKNOWN
9040
+ };
9041
+ }
9042
+ function toMap(form) {
9043
+ const [, nameNode, key, value] = form;
9044
+ if (nameNode?.kind !== "atom" || !key || !value)
9045
+ return null;
9046
+ return { name: nameNode.value, key: typeToAbi(key), value: typeToAbi(value) };
9047
+ }
9048
+ function toVariable(form) {
9049
+ const [, nameNode, type] = form;
9050
+ if (nameNode?.kind !== "atom" || !type)
9051
+ return null;
9052
+ return { name: nameNode.value, type: typeToAbi(type), access: "variable" };
9053
+ }
9054
+ function toFungibleToken(form) {
9055
+ const nameNode = form[1];
9056
+ if (nameNode?.kind !== "atom")
9057
+ return null;
9058
+ return { name: nameNode.value };
9059
+ }
9060
+ function toNonFungibleToken(form) {
9061
+ const [, nameNode, type] = form;
9062
+ if (nameNode?.kind !== "atom")
9063
+ return null;
9064
+ return { name: nameNode.value, type: type ? typeToAbi(type) : UNKNOWN };
9065
+ }
9066
+ function parseClarityContent(content) {
9067
+ const functions = [];
9068
+ const maps = [];
9069
+ const variables = [];
9070
+ const fungibleTokens = [];
9071
+ const nonFungibleTokens = [];
9072
+ const implementedTraits = [];
9073
+ for (const form of readForms(content)) {
9074
+ if (form.kind !== "list")
9075
+ continue;
9076
+ const head = form.items[0];
9077
+ if (head?.kind !== "atom")
9078
+ continue;
9079
+ const access = FUNCTION_ACCESS[head.value];
9080
+ if (access) {
9081
+ const func = toFunction(form.items, access);
9082
+ if (func)
9083
+ functions.push(func);
9084
+ continue;
9085
+ }
9086
+ switch (head.value) {
9087
+ case "define-map": {
9088
+ const map = toMap(form.items);
9089
+ if (map)
9090
+ maps.push(map);
9091
+ break;
9092
+ }
9093
+ case "define-data-var": {
9094
+ const variable = toVariable(form.items);
9095
+ if (variable)
9096
+ variables.push(variable);
9097
+ break;
8822
9098
  }
8823
- if (cleaned.startsWith("string-utf8")) {
8824
- return { "string-utf8": { length: 256 } };
9099
+ case "define-fungible-token": {
9100
+ const token = toFungibleToken(form.items);
9101
+ if (token)
9102
+ fungibleTokens.push(token);
9103
+ break;
8825
9104
  }
8826
- if (cleaned.startsWith("buff")) {
8827
- return { buff: { length: 32 } };
9105
+ case "define-non-fungible-token": {
9106
+ const token = toNonFungibleToken(form.items);
9107
+ if (token)
9108
+ nonFungibleTokens.push(token);
9109
+ break;
8828
9110
  }
8829
- return "uint128";
8830
- }
8831
- }
8832
- function inferReturnType(body) {
8833
- if (body.includes("(ok")) {
8834
- if (body.includes("(err")) {
8835
- return {
8836
- response: {
8837
- ok: "bool",
8838
- error: "uint128"
9111
+ case "impl-trait": {
9112
+ const trait = form.items[1];
9113
+ if (trait?.kind === "atom") {
9114
+ implementedTraits.push(trait.value.replace(/^'/, ""));
8839
9115
  }
8840
- };
9116
+ break;
9117
+ }
8841
9118
  }
8842
9119
  }
8843
- if (body.includes("true") || body.includes("false")) {
8844
- return "bool";
9120
+ return {
9121
+ functions,
9122
+ ...maps.length > 0 && { maps },
9123
+ ...variables.length > 0 && { variables },
9124
+ ...fungibleTokens.length > 0 && { fungible_tokens: fungibleTokens },
9125
+ ...nonFungibleTokens.length > 0 && {
9126
+ non_fungible_tokens: nonFungibleTokens
9127
+ },
9128
+ ...implementedTraits.length > 0 && {
9129
+ implemented_traits: implementedTraits
9130
+ }
9131
+ };
9132
+ }
9133
+ async function parseClarityFile(filePath) {
9134
+ try {
9135
+ const content = await fs2.readFile(filePath, "utf-8");
9136
+ const result = parseClarityContent(content);
9137
+ if (result.functions.length === 0) {
9138
+ console.warn(`⚠️ No functions found in ${filePath}. For complex contracts, deploy first and use the contract address instead.`);
9139
+ }
9140
+ return result;
9141
+ } catch (error2) {
9142
+ throw new Error(`Unable to parse ${filePath}. For complex contracts, deploy first and use the contract address instead.
9143
+ Original error: ${error2}`);
8845
9144
  }
8846
- return "bool";
8847
9145
  }
8848
9146
  function parseApiResponse(apiResponse) {
8849
9147
  try {
@@ -8852,7 +9150,15 @@ function parseApiResponse(apiResponse) {
8852
9150
  throw new Error(`Failed to parse API response: ${error2}`);
8853
9151
  }
8854
9152
  }
8855
- var init_clarity = () => {};
9153
+ var DELIMITERS, UNKNOWN = "unknown", FUNCTION_ACCESS;
9154
+ var init_clarity = __esm(() => {
9155
+ DELIMITERS = new Set(["(", ")", "{", "}", ",", ":"]);
9156
+ FUNCTION_ACCESS = {
9157
+ "define-public": "public",
9158
+ "define-read-only": "read-only",
9159
+ "define-private": "private"
9160
+ };
9161
+ });
8856
9162
 
8857
9163
  // ../../node_modules/@sindresorhus/is/dist/index.js
8858
9164
  function isTypedArrayName(name) {
@@ -20891,10 +21197,10 @@ var isTemplateString = (templates) => Array.isArray(templates) && Array.isArray(
20891
21197
  }
20892
21198
  const nextTokens = [];
20893
21199
  let templateStart = 0;
20894
- const leadingWhitespaces = DELIMITERS.has(rawTemplate[0]);
21200
+ const leadingWhitespaces = DELIMITERS2.has(rawTemplate[0]);
20895
21201
  for (let templateIndex = 0, rawIndex = 0;templateIndex < template.length; templateIndex += 1, rawIndex += 1) {
20896
21202
  const rawCharacter = rawTemplate[rawIndex];
20897
- if (DELIMITERS.has(rawCharacter)) {
21203
+ if (DELIMITERS2.has(rawCharacter)) {
20898
21204
  if (templateStart !== templateIndex) {
20899
21205
  nextTokens.push(template.slice(templateStart, templateIndex));
20900
21206
  }
@@ -20917,7 +21223,7 @@ var isTemplateString = (templates) => Array.isArray(templates) && Array.isArray(
20917
21223
  nextTokens.push(template.slice(templateStart));
20918
21224
  }
20919
21225
  return { nextTokens, leadingWhitespaces, trailingWhitespaces };
20920
- }, DELIMITERS, ESCAPE_LENGTH, concatTokens = (tokens, nextTokens, isSeparated) => isSeparated || tokens.length === 0 || nextTokens.length === 0 ? [...tokens, ...nextTokens] : [
21226
+ }, DELIMITERS2, ESCAPE_LENGTH, concatTokens = (tokens, nextTokens, isSeparated) => isSeparated || tokens.length === 0 || nextTokens.length === 0 ? [...tokens, ...nextTokens] : [
20921
21227
  ...tokens.slice(0, -1),
20922
21228
  `${tokens.at(-1)}${nextTokens[0]}`,
20923
21229
  ...nextTokens.slice(1)
@@ -20950,7 +21256,7 @@ var isTemplateString = (templates) => Array.isArray(templates) && Array.isArray(
20950
21256
  };
20951
21257
  var init_template = __esm(() => {
20952
21258
  init_uint_array();
20953
- DELIMITERS = new Set([" ", "\t", "\r", `
21259
+ DELIMITERS2 = new Set([" ", "\t", "\r", `
20954
21260
  `]);
20955
21261
  ESCAPE_LENGTH = { x: 3, u: 5 };
20956
21262
  });
@@ -27829,6 +28135,9 @@ async function buildConfigFromInputs(parsedInputs, outPath, apiKey, defaultAddre
27829
28135
  Generated contract addresses won't match deployed addresses.
27830
28136
  Set defaultAddress in config or use deployed contract addresses.`));
27831
28137
  }
28138
+ if (parsedInputs.files.length > 0) {
28139
+ console.warn(source_default.yellow("⚠️ Return types can't be read from Clarity source and will be `any`.\n Use the clarinet() plugin or a deployed contract id for exact types."));
28140
+ }
27832
28141
  for (const file of parsedInputs.files) {
27833
28142
  const abi = await parseClarityFile(file);
27834
28143
  const name = deriveContractName(file);
@@ -28398,7 +28707,15 @@ var init_api_client = __esm(() => {
28398
28707
  });
28399
28708
 
28400
28709
  // src/lib/fs.ts
28401
- import { mkdir as mkdir2, readFile as readFile2, stat, unlink, writeFile as writeFile2 } from "node:fs/promises";
28710
+ import { createHash } from "node:crypto";
28711
+ import {
28712
+ mkdir as mkdir2,
28713
+ open,
28714
+ readFile as readFile2,
28715
+ stat,
28716
+ unlink,
28717
+ writeFile as writeFile2
28718
+ } from "node:fs/promises";
28402
28719
  async function readJsonFile(path11) {
28403
28720
  const text2 = await readFile2(path11, "utf-8");
28404
28721
  return JSON.parse(text2);
@@ -28420,6 +28737,24 @@ async function ensureDir(path11) {
28420
28737
  async function removeFile(path11) {
28421
28738
  await unlink(path11);
28422
28739
  }
28740
+ async function sha256File(path11) {
28741
+ const hash = createHash("sha256");
28742
+ const chunk = Buffer.allocUnsafe(1024 * 1024);
28743
+ const fh = await open(path11, "r");
28744
+ try {
28745
+ let position = 0;
28746
+ for (;; ) {
28747
+ const { bytesRead } = await fh.read(chunk, 0, chunk.length, position);
28748
+ if (bytesRead === 0)
28749
+ break;
28750
+ hash.update(chunk.subarray(0, bytesRead));
28751
+ position += bytesRead;
28752
+ }
28753
+ } finally {
28754
+ await fh.close();
28755
+ }
28756
+ return hash.digest("hex");
28757
+ }
28423
28758
  var init_fs = () => {};
28424
28759
 
28425
28760
  // src/lib/config.ts
@@ -28521,6 +28856,10 @@ function applyEnvOverrides(config) {
28521
28856
  const net2 = process.env.STACKS_NETWORK;
28522
28857
  if (net2 === "local" || net2 === "testnet" || net2 === "mainnet") {
28523
28858
  result.network = net2;
28859
+ } else if (net2 === "devnet") {
28860
+ result.network = "local";
28861
+ } else {
28862
+ console.error(`Warning: STACKS_NETWORK=${net2} is not mainnet, testnet, or devnet; using ${result.network}`);
28524
28863
  }
28525
28864
  }
28526
28865
  if (process.env.SL_DATA_DIR) {
@@ -28755,11 +29094,11 @@ function inspectSourceGitState(absPath) {
28755
29094
  if (tracked === null) {
28756
29095
  return { kind: "untracked" };
28757
29096
  }
28758
- const status = runGit(["status", "--porcelain", "--", absPath], cwd);
28759
- if (status === null) {
29097
+ const unstaged = runGit(["diff", "--name-only", "--", absPath], cwd);
29098
+ if (unstaged === null) {
28760
29099
  return { kind: "not-a-repo" };
28761
29100
  }
28762
- if (status.length > 0) {
29101
+ if (unstaged.length > 0) {
28763
29102
  return { kind: "modified" };
28764
29103
  }
28765
29104
  return { kind: "clean" };
@@ -28777,19 +29116,21 @@ async function readStdin() {
28777
29116
  chunks.push(chunk);
28778
29117
  return Buffer.concat(chunks).toString("utf8").trim();
28779
29118
  }
28780
- async function runTokenLogin() {
29119
+ function loginTarget(opts) {
29120
+ return opts.credits ? resolveArchiveOpsUrl() : resolveApiUrl();
29121
+ }
29122
+ async function runTokenLogin(opts) {
28781
29123
  const token = await readStdin();
28782
29124
  if (!token) {
28783
29125
  error('No token on stdin. Usage: echo "$KEY" | secondlayer login --with-token');
28784
29126
  process.exit(1);
28785
29127
  }
28786
- process.env.INSTANCE_TOKEN = token;
28787
- process.env.SL_API_KEY = token;
29128
+ const target = loginTarget(opts);
28788
29129
  let account;
28789
29130
  try {
28790
- account = await httpPlatform("/api/accounts/me");
29131
+ account = await httpAt(target, "/api/accounts/me", { bearer: token });
28791
29132
  } catch (err) {
28792
- error(err instanceof CliHttpError ? `Token rejected: ${err.message}` : err instanceof Error ? err.message : String(err));
29133
+ error(err instanceof CliHttpError ? `Token rejected by ${target}: ${err.message}` : err instanceof Error ? err.message : String(err));
28793
29134
  process.exit(1);
28794
29135
  }
28795
29136
  await writeSession({
@@ -28797,14 +29138,15 @@ async function runTokenLogin() {
28797
29138
  email: account.email,
28798
29139
  accountId: account.id,
28799
29140
  expiresAt: sessionExpiry()
28800
- });
28801
- success(`Logged in as ${account.email}`);
29141
+ }, target);
29142
+ success(`Logged in as ${account.email} (${target})`);
28802
29143
  }
28803
29144
  async function runLoginFlow(options3 = {}) {
29145
+ const target = loginTarget(options3);
28804
29146
  if (!options3.force) {
28805
- const existing = await readSession();
29147
+ const existing = await readSession(target);
28806
29148
  if (existing) {
28807
- info(`Already logged in as ${existing.email}.`);
29149
+ info(`Already logged in as ${existing.email} (${target}).`);
28808
29150
  if (!process.stdin.isTTY) {
28809
29151
  info(dim("Run 'secondlayer logout' first, or re-run with --force to switch accounts."));
28810
29152
  return;
@@ -28829,7 +29171,7 @@ async function runLoginFlow(options3 = {}) {
28829
29171
  validate: (v2) => /^.+@.+\..+$/.test(v2) ? true : "Invalid email"
28830
29172
  });
28831
29173
  try {
28832
- const res = await httpPlatformAnon("/api/auth/magic-link", {
29174
+ const res = await httpAt(target, "/api/auth/magic-link", {
28833
29175
  method: "POST",
28834
29176
  body: { email }
28835
29177
  });
@@ -28850,7 +29192,7 @@ async function runLoginFlow(options3 = {}) {
28850
29192
  validate: (v2) => /^\d{6}$/.test(v2) ? true : "Expected 6 digits"
28851
29193
  });
28852
29194
  try {
28853
- const verified = await httpPlatformAnon("/api/auth/verify", {
29195
+ const verified = await httpAt(target, "/api/auth/verify", {
28854
29196
  method: "POST",
28855
29197
  body: { email, code }
28856
29198
  });
@@ -28859,9 +29201,9 @@ async function runLoginFlow(options3 = {}) {
28859
29201
  email: verified.account.email,
28860
29202
  accountId: verified.account.id,
28861
29203
  expiresAt: sessionExpiry()
28862
- });
29204
+ }, target);
28863
29205
  success(`Logged in as ${verified.account.email}`);
28864
- info(dim("Run 'secondlayer whoami' to see your account status."));
29206
+ info(dim(options3.credits ? "Run 'secondlayer credits balance' to see your archive credits." : "Run 'secondlayer whoami' to see your account status."));
28865
29207
  } catch (err) {
28866
29208
  if (err instanceof CliHttpError) {
28867
29209
  error(err.message);
@@ -28872,10 +29214,10 @@ async function runLoginFlow(options3 = {}) {
28872
29214
  }
28873
29215
  }
28874
29216
  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", `
29217
+ 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
29218
  Examples:
28877
- $ secondlayer login
28878
- $ echo "$INSTANCE_TOKEN" | secondlayer login --with-token`).action((opts) => opts.withToken ? runTokenLogin() : runLoginFlow({ force: opts.force }));
29219
+ $ secondlayer login --credits
29220
+ $ echo "$INSTANCE_TOKEN" | secondlayer login --with-token`).action((opts) => opts.withToken ? runTokenLogin({ credits: opts.credits }) : runLoginFlow({ force: opts.force, credits: opts.credits }));
28879
29221
  }
28880
29222
  var init_login = __esm(() => {
28881
29223
  init_http();
@@ -28986,7 +29328,7 @@ __export(exports_subgraph_test, {
28986
29328
  filterHashOf: () => filterHashOf,
28987
29329
  DECODER_VERSION: () => DECODER_VERSION
28988
29330
  });
28989
- import { createHash } from "node:crypto";
29331
+ import { createHash as createHash2 } from "node:crypto";
28990
29332
  import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
28991
29333
  import { dirname as dirname4, resolve as resolve2 } from "node:path";
28992
29334
  import { Index, resolveApiKey as resolveApiKey2, resolveBaseUrl } from "@secondlayer/sdk";
@@ -28995,7 +29337,7 @@ import {
28995
29337
  } from "@secondlayer/stacks/filters";
28996
29338
  function filterHashOf(sources) {
28997
29339
  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);
29340
+ return createHash2("sha256").update(stable).digest("hex").slice(0, 16);
28999
29341
  }
29000
29342
  function cassettePath(name, dir = CASSETTE_DIR) {
29001
29343
  return resolve2(dir, `${name}.json`);
@@ -29772,7 +30114,8 @@ Check the contract id, and that the contract has emitted print events (schemas a
29772
30114
  if (opts.fromContract) {
29773
30115
  info(`Schema inferred from ${opts.fromContract} print events (${opts.tablePerTopic ? "table per topic" : "single wide table"})`);
29774
30116
  }
29775
- info(`Next: secondlayer subgraphs deploy subgraphs/${name}.ts`);
30117
+ info(`Next: git add subgraphs/${name}.ts && secondlayer subgraphs deploy subgraphs/${name}.ts`);
30118
+ note(" deploy refuses an unstaged file unless you pass --allow-uncommitted: a staged copy is the one git can recover.");
29776
30119
  });
29777
30120
  subgraphs.command("dev <file>").description("Watch a subgraph file and auto-redeploy on change").action(async (file) => {
29778
30121
  await requireLocalNetwork();
@@ -29835,9 +30178,9 @@ Stopped watching.`);
29835
30178
  });
29836
30179
  await new Promise(() => {});
29837
30180
  });
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
30181
+ 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", `
30182
+ By default the source file must be staged or committed in git (\`git add <file>\`),
30183
+ otherwise the deployed definition's only copy is the row in the database. Pass
29841
30184
  --allow-uncommitted to deploy anyway.`).action(async (file, options3) => {
29842
30185
  try {
29843
30186
  const absPath = resolve3(file);
@@ -29848,18 +30191,19 @@ deployed definition's only copy is the row in the database. Pass
29848
30191
  const gitState = inspectSourceGitState(absPath);
29849
30192
  if (gitState.kind === "untracked" || gitState.kind === "modified") {
29850
30193
  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) {
30194
+ const message = `${file} is ${reason}. Deploy it and the only copy of this definition is a row in the database.`;
30195
+ if (process.stdin.isTTY) {
29853
30196
  warn(message);
29854
30197
  const confirmed = await confirm3({
29855
- message: "Deploy anyway?"
30198
+ message: "Deploy anyway?",
30199
+ default: false
29856
30200
  });
29857
30201
  if (!confirmed) {
29858
- info("Aborted.");
30202
+ info("Nothing was deployed.");
29859
30203
  process.exit(0);
29860
30204
  }
29861
30205
  } else {
29862
- error(`${message} Commit it, or pass --allow-uncommitted.`);
30206
+ error(`${message} Stage it with \`git add ${file}\`, or pass --allow-uncommitted.`);
29863
30207
  process.exit(1);
29864
30208
  }
29865
30209
  }
@@ -29924,11 +30268,12 @@ deployed definition's only copy is the row in the database. Pass
29924
30268
  } else {
29925
30269
  warn(`startBlock change (${existingStart ?? "unset"} → ${deployStartBlock}) forces a reindex.`);
29926
30270
  }
29927
- const confirmed = await confirm3({
29928
- message: "⚠ This will drop all data and reindex from scratch. Continue?"
30271
+ const confirmed = await confirmDestructive({
30272
+ message: "This will drop all data and reindex from scratch. Continue?",
30273
+ yes: options3.yes
29929
30274
  });
29930
30275
  if (!confirmed) {
29931
- info("Aborted — nothing was deployed.");
30276
+ info("Nothing was deployed.");
29932
30277
  process.exit(0);
29933
30278
  }
29934
30279
  }
@@ -30063,8 +30408,10 @@ ${data.length} subgraph(s) total`));
30063
30408
  }
30064
30409
  });
30065
30410
  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 () => {
30411
+ const renderOnce = async (opts = {}) => {
30067
30412
  const subgraph = await getSubgraphApi(name);
30413
+ if (opts.clearScreen)
30414
+ process.stdout.write("\x1Bc");
30068
30415
  const rowCounts = Object.entries(subgraph.tables).map(([t2, info2]) => `${t2}: ${info2.rowCount}`).join(", ") || "N/A";
30069
30416
  const totalRows = Object.values(subgraph.tables).reduce((sum, info2) => sum + info2.rowCount, 0);
30070
30417
  const errorRate = subgraph.health.totalProcessed > 0 ? `${(subgraph.health.errorRate * 100).toFixed(2)}%` : "N/A";
@@ -30113,12 +30460,23 @@ Table endpoints:`));
30113
30460
  await renderOnce();
30114
30461
  return;
30115
30462
  }
30463
+ let consecutiveFailures = 0;
30116
30464
  while (true) {
30117
- process.stdout.write("\x1Bc");
30118
- const sg = await renderOnce();
30119
- if (sg && sg.status === "synced") {
30465
+ let sg;
30466
+ try {
30467
+ sg = await renderOnce({ clearScreen: true });
30468
+ consecutiveFailures = 0;
30469
+ } catch (err) {
30470
+ consecutiveFailures += 1;
30471
+ if (consecutiveFailures > WATCH_TRANSIENT_FAILURE_LIMIT)
30472
+ throw err;
30473
+ console.log(dim(`poll failed (${consecutiveFailures}/${WATCH_TRANSIENT_FAILURE_LIMIT}): ${err instanceof Error ? err.message : String(err)}; retrying`));
30474
+ await new Promise((res) => setTimeout(res, 2000));
30475
+ continue;
30476
+ }
30477
+ if (sg?.sync?.status === "synced") {
30120
30478
  console.log(dim(`
30121
- Synced — exiting watch.`));
30479
+ Synced, exiting watch.`));
30122
30480
  return;
30123
30481
  }
30124
30482
  await new Promise((res) => setTimeout(res, 2000));
@@ -30167,30 +30525,13 @@ dropping anything, use "secondlayer subgraphs backfill".
30167
30525
  Examples:
30168
30526
  $ secondlayer subgraphs reindex my-graph -y`).action(async (name, options3) => {
30169
30527
  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
- }
30528
+ const ok = await confirmDestructive({
30529
+ message: `Reindex subgraph "${name}"? ALL of its data will be dropped and rebuilt from its start block. This cannot be undone.`,
30530
+ yes: options3.yes
30531
+ });
30532
+ if (!ok) {
30533
+ info("Cancelled.");
30534
+ return;
30194
30535
  }
30195
30536
  info(`Reindexing subgraph "${name}"...`);
30196
30537
  const result = await reindexSubgraphApi(name);
@@ -30412,29 +30753,13 @@ ${rows.length} row(s)`));
30412
30753
  });
30413
30754
  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
30755
  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
- }
30756
+ const ok = await confirmDestructive({
30757
+ message: `Delete subgraph "${name}" and all its data? This cannot be undone.`,
30758
+ yes: options3.yes || options3.force
30759
+ });
30760
+ if (!ok) {
30761
+ info("Cancelled");
30762
+ return;
30438
30763
  }
30439
30764
  try {
30440
30765
  const result = await deleteSubgraphApi(name, {
@@ -30454,7 +30779,7 @@ ${rows.length} row(s)`));
30454
30779
  handleApiError(err, "delete subgraph");
30455
30780
  }
30456
30781
  });
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", `
30782
+ 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
30783
  "secondlayer subgraphs create <name>" is the documented starting point. Reach
30459
30784
  for scaffold when you need what create cannot infer: typed contract_call tables
30460
30785
  from the contract's ABI, or a trait-wide source (--trait sip-010). scaffold
@@ -30488,8 +30813,7 @@ Examples:
30488
30813
  } else {
30489
30814
  const address = contractAddress;
30490
30815
  const network = inferNetwork(address) ?? "mainnet";
30491
- const apiKey = options3.apiKey ?? process.env.STACKS_NODE_API_KEY;
30492
- const client = new StacksApiClient(network, apiKey);
30816
+ const client = new StacksApiClient(network);
30493
30817
  info(`Fetching ABI for ${address} via ${client.describeContractInfoSource()}...`);
30494
30818
  const contractInfo = await client.getContractInfo(address);
30495
30819
  const abi = parseApiResponse(contractInfo);
@@ -30601,7 +30925,7 @@ async function runSubgraphSchemaCodegen(file, options3, optionSource, defaultTar
30601
30925
  process.exit(1);
30602
30926
  }
30603
30927
  }
30604
- var ORM_ONLY_CODEGEN_FLAGS;
30928
+ var WATCH_TRANSIENT_FAILURE_LIMIT = 3, ORM_ONLY_CODEGEN_FLAGS;
30605
30929
  var init_subgraphs2 = __esm(() => {
30606
30930
  init_subgraph_scaffold();
30607
30931
  init_subgraphs();
@@ -32468,7 +32792,7 @@ import fs6 from "fs";
32468
32792
  function Postgres(a4, b3) {
32469
32793
  const options3 = parseOptions(a4, b3), subscribe = options3.no_subscribe || Subscribe(Postgres, { ...options3 });
32470
32794
  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 };
32795
+ 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
32796
  const connections = [...Array(options3.max)].map(() => connection_default(options3, queues, { onopen, onend, onclose }));
32473
32797
  const sql = Sql(handler);
32474
32798
  Object.assign(sql, {
@@ -32580,7 +32904,7 @@ function Postgres(a4, b3) {
32580
32904
  }
32581
32905
  async function reserve() {
32582
32906
  const queue = queue_default();
32583
- const c4 = open.length ? open.shift() : await new Promise((resolve5, reject) => {
32907
+ const c4 = open2.length ? open2.shift() : await new Promise((resolve5, reject) => {
32584
32908
  const query = { reserve: resolve5, reject };
32585
32909
  queries.push(query);
32586
32910
  closed.length && connect(closed.shift(), query);
@@ -32653,7 +32977,7 @@ function Postgres(a4, b3) {
32653
32977
  c4.queue.remove(c4);
32654
32978
  queue.push(c4);
32655
32979
  c4.queue = queue;
32656
- queue === open ? c4.idleTimer.start() : c4.idleTimer.cancel();
32980
+ queue === open2 ? c4.idleTimer.start() : c4.idleTimer.cancel();
32657
32981
  return c4;
32658
32982
  }
32659
32983
  function json(x2) {
@@ -32667,8 +32991,8 @@ function Postgres(a4, b3) {
32667
32991
  function handler(query) {
32668
32992
  if (ending)
32669
32993
  return query.reject(Errors.connection("CONNECTION_ENDED", options3, options3));
32670
- if (open.length)
32671
- return go(open.shift(), query);
32994
+ if (open2.length)
32995
+ return go(open2.shift(), query);
32672
32996
  if (closed.length)
32673
32997
  return connect(closed.shift(), query);
32674
32998
  busy.length ? go(busy.shift(), query) : queries.push(query);
@@ -32710,7 +33034,7 @@ function Postgres(a4, b3) {
32710
33034
  }
32711
33035
  function onopen(c4) {
32712
33036
  if (queries.length === 0)
32713
- return move(c4, open);
33037
+ return move(c4, open2);
32714
33038
  let max = Math.ceil(queries.length / (connecting.length + 1)), ready = true;
32715
33039
  while (ready && queries.length && max-- > 0) {
32716
33040
  const query = queries.shift();
@@ -33032,12 +33356,12 @@ __export(exports_dev_state, {
33032
33356
  clearDevState: () => clearDevState
33033
33357
  });
33034
33358
  import { homedir as homedir4 } from "node:os";
33035
- import { join as join11 } from "node:path";
33359
+ import { join as join10 } from "node:path";
33036
33360
  function getLogsDir() {
33037
33361
  return LOGS_DIR;
33038
33362
  }
33039
33363
  function getLogFile(service) {
33040
- return join11(LOGS_DIR, `${service}.log`);
33364
+ return join10(LOGS_DIR, `${service}.log`);
33041
33365
  }
33042
33366
  async function ensureDirs() {
33043
33367
  await Bun.$`mkdir -p ${STATE_DIR}`.quiet();
@@ -33094,9 +33418,9 @@ async function isDevRunning() {
33094
33418
  }
33095
33419
  var STATE_DIR, DEV_STATE_PATH, LOGS_DIR;
33096
33420
  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");
33421
+ STATE_DIR = join10(homedir4(), ".secondlayer");
33422
+ DEV_STATE_PATH = join10(STATE_DIR, "dev.json");
33423
+ LOGS_DIR = join10(STATE_DIR, "logs");
33100
33424
  });
33101
33425
 
33102
33426
  // src/lib/network.ts
@@ -33487,11 +33811,11 @@ var init_manager = __esm(() => {
33487
33811
  });
33488
33812
 
33489
33813
  // src/services/indexer.ts
33490
- import { dirname as dirname10, resolve as resolve6 } from "node:path";
33814
+ import { dirname as dirname10, resolve as resolve7 } from "node:path";
33491
33815
  async function startIndexer(options3) {
33492
33816
  const port = options3.port ?? 3700;
33493
33817
  const rootDir = dirname10(dirname10(dirname10(dirname10(import.meta.dir))));
33494
- const indexerPath = resolve6(rootDir, "packages/indexer/src/index.ts");
33818
+ const indexerPath = resolve7(rootDir, "packages/indexer/src/index.ts");
33495
33819
  await serviceManager.start(SERVICE_NAME, ["bun", "run", "--watch", indexerPath], {
33496
33820
  port,
33497
33821
  env: {
@@ -33507,10 +33831,10 @@ var init_indexer = __esm(() => {
33507
33831
  });
33508
33832
 
33509
33833
  // src/services/worker.ts
33510
- import { dirname as dirname11, resolve as resolve7 } from "node:path";
33834
+ import { dirname as dirname11, resolve as resolve8 } from "node:path";
33511
33835
  async function startWorker(options3) {
33512
33836
  const rootDir = dirname11(dirname11(dirname11(dirname11(import.meta.dir))));
33513
- const workerPath = resolve7(rootDir, "packages/worker/src/index.ts");
33837
+ const workerPath = resolve8(rootDir, "packages/worker/src/index.ts");
33514
33838
  await serviceManager.start(SERVICE_NAME2, ["bun", "run", "--watch", workerPath], {
33515
33839
  onStdout: options3.onLog,
33516
33840
  onStderr: options3.onLog
@@ -33522,11 +33846,11 @@ var init_worker = __esm(() => {
33522
33846
  });
33523
33847
 
33524
33848
  // src/services/api.ts
33525
- import { dirname as dirname12, resolve as resolve8 } from "node:path";
33849
+ import { dirname as dirname12, resolve as resolve9 } from "node:path";
33526
33850
  async function startApi(options3) {
33527
33851
  const port = options3.port ?? 3800;
33528
33852
  const rootDir = dirname12(dirname12(dirname12(dirname12(import.meta.dir))));
33529
- const apiPath = resolve8(rootDir, "packages/api/src/index.ts");
33853
+ const apiPath = resolve9(rootDir, "packages/api/src/index.ts");
33530
33854
  await serviceManager.start(SERVICE_NAME3, ["bun", "run", "--watch", apiPath], {
33531
33855
  port,
33532
33856
  env: {
@@ -33542,10 +33866,10 @@ var init_api2 = __esm(() => {
33542
33866
  });
33543
33867
 
33544
33868
  // src/services/subgraph-processor.ts
33545
- import { dirname as dirname13, resolve as resolve9 } from "node:path";
33869
+ import { dirname as dirname13, resolve as resolve10 } from "node:path";
33546
33870
  async function startSubgraphProcessor(options3) {
33547
33871
  const rootDir = dirname13(dirname13(dirname13(dirname13(import.meta.dir))));
33548
- const servicePath = resolve9(rootDir, "packages/subgraphs/src/service.ts");
33872
+ const servicePath = resolve10(rootDir, "packages/subgraphs/src/service.ts");
33549
33873
  await serviceManager.start(SERVICE_NAME4, ["bun", "run", "--watch", servicePath], {
33550
33874
  env: {
33551
33875
  SUBGRAPH_CONCURRENCY: String(options3.concurrency ?? 5)
@@ -33580,7 +33904,7 @@ __export(exports_dev_impl, {
33580
33904
  isDevAlreadyRunning: () => isDevAlreadyRunning
33581
33905
  });
33582
33906
  import { mkdirSync as mkdirSync5 } from "node:fs";
33583
- import { dirname as dirname14, join as join12, resolve as resolve10 } from "node:path";
33907
+ import { dirname as dirname14, join as join11, resolve as resolve11 } from "node:path";
33584
33908
  async function isDevAlreadyRunning() {
33585
33909
  if (await isDevRunning()) {
33586
33910
  const running = await getRunningServices();
@@ -33661,7 +33985,7 @@ async function runBackground(options3) {
33661
33985
  const packagesDir = dirname14(dirname14(dirname14(dirname14(import.meta.dir))));
33662
33986
  const env2 = { DATABASE_URL: databaseUrl, DEV_MODE: "true" };
33663
33987
  const apiLogFile = getLogFile("api");
33664
- const apiProc = Bun.spawn(["bun", "run", resolve10(packagesDir, "packages/api/src/index.ts")], {
33988
+ const apiProc = Bun.spawn(["bun", "run", resolve11(packagesDir, "packages/api/src/index.ts")], {
33665
33989
  env: { ...process.env, ...env2, PORT: String(apiPort) },
33666
33990
  stdout: Bun.file(apiLogFile),
33667
33991
  stderr: Bun.file(apiLogFile)
@@ -33674,7 +33998,7 @@ async function runBackground(options3) {
33674
33998
  };
33675
33999
  console.log(green(" ✓ API"), dim(`http://localhost:${apiPort}`));
33676
34000
  const indexerLogFile = getLogFile("indexer");
33677
- const indexerProc = Bun.spawn(["bun", "run", resolve10(packagesDir, "packages/indexer/src/index.ts")], {
34001
+ const indexerProc = Bun.spawn(["bun", "run", resolve11(packagesDir, "packages/indexer/src/index.ts")], {
33678
34002
  env: { ...process.env, ...env2, PORT: String(indexerPort) },
33679
34003
  stdout: Bun.file(indexerLogFile),
33680
34004
  stderr: Bun.file(indexerLogFile)
@@ -33688,7 +34012,7 @@ async function runBackground(options3) {
33688
34012
  console.log(green(" ✓ Indexer"), dim(`http://localhost:${indexerPort}`));
33689
34013
  if (options3.worker) {
33690
34014
  const workerLogFile = getLogFile("worker");
33691
- const workerProc = Bun.spawn(["bun", "run", resolve10(packagesDir, "packages/worker/src/index.ts")], {
34015
+ const workerProc = Bun.spawn(["bun", "run", resolve11(packagesDir, "packages/worker/src/index.ts")], {
33692
34016
  env: { ...process.env, ...env2 },
33693
34017
  stdout: Bun.file(workerLogFile),
33694
34018
  stderr: Bun.file(workerLogFile)
@@ -33706,7 +34030,7 @@ async function runBackground(options3) {
33706
34030
  const subgraphsProc = Bun.spawn([
33707
34031
  "bun",
33708
34032
  "run",
33709
- resolve10(packagesDir, "packages/subgraphs/src/service.ts")
34033
+ resolve11(packagesDir, "packages/subgraphs/src/service.ts")
33710
34034
  ], {
33711
34035
  env: { ...process.env, ...env2 },
33712
34036
  stdout: Bun.file(subgraphsLogFile),
@@ -34018,7 +34342,7 @@ async function restartDev() {
34018
34342
  };
34019
34343
  const apiPort = config.ports.api;
34020
34344
  const apiLogFile = getLogFile("api");
34021
- const apiProc = Bun.spawn(["bun", "run", resolve10(packagesDir, "packages/api/src/index.ts")], {
34345
+ const apiProc = Bun.spawn(["bun", "run", resolve11(packagesDir, "packages/api/src/index.ts")], {
34022
34346
  env: { ...process.env, ...env2, PORT: String(apiPort) },
34023
34347
  stdout: Bun.file(apiLogFile),
34024
34348
  stderr: Bun.file(apiLogFile)
@@ -34032,7 +34356,7 @@ async function restartDev() {
34032
34356
  console.log(green(" ✓ API"), dim(`http://localhost:${apiPort}`));
34033
34357
  const indexerPort = config.ports.indexer;
34034
34358
  const indexerLogFile = getLogFile("indexer");
34035
- const indexerProc = Bun.spawn(["bun", "run", resolve10(packagesDir, "packages/indexer/src/index.ts")], {
34359
+ const indexerProc = Bun.spawn(["bun", "run", resolve11(packagesDir, "packages/indexer/src/index.ts")], {
34036
34360
  env: { ...process.env, ...env2, PORT: String(indexerPort) },
34037
34361
  stdout: Bun.file(indexerLogFile),
34038
34362
  stderr: Bun.file(indexerLogFile)
@@ -34046,7 +34370,7 @@ async function restartDev() {
34046
34370
  console.log(green(" ✓ Indexer"), dim(`http://localhost:${indexerPort}`));
34047
34371
  if (state.services.worker) {
34048
34372
  const workerLogFile = getLogFile("worker");
34049
- const workerProc = Bun.spawn(["bun", "run", resolve10(packagesDir, "packages/worker/src/index.ts")], {
34373
+ const workerProc = Bun.spawn(["bun", "run", resolve11(packagesDir, "packages/worker/src/index.ts")], {
34050
34374
  env: { ...process.env, ...env2 },
34051
34375
  stdout: Bun.file(workerLogFile),
34052
34376
  stderr: Bun.file(workerLogFile)
@@ -34061,7 +34385,7 @@ async function restartDev() {
34061
34385
  }
34062
34386
  if (state.services.subgraphs) {
34063
34387
  const subgraphsLogFile = getLogFile("subgraphs");
34064
- const subgraphsProc = Bun.spawn(["bun", "run", resolve10(packagesDir, "packages/subgraphs/src/service.ts")], {
34388
+ const subgraphsProc = Bun.spawn(["bun", "run", resolve11(packagesDir, "packages/subgraphs/src/service.ts")], {
34065
34389
  env: { ...process.env, ...env2 },
34066
34390
  stdout: Bun.file(subgraphsLogFile),
34067
34391
  stderr: Bun.file(subgraphsLogFile)
@@ -34189,7 +34513,7 @@ async function ensureDevPostgres(dataDir) {
34189
34513
  if (check.stdout.toString().trim()) {
34190
34514
  return false;
34191
34515
  }
34192
- const pgDataDir = join12(dataDir, "postgres");
34516
+ const pgDataDir = join11(dataDir, "postgres");
34193
34517
  mkdirSync5(pgDataDir, { recursive: true });
34194
34518
  const stopped = await Bun.$`docker ps -aq -f name=secondlayer-dev-postgres`.quiet().nothrow();
34195
34519
  if (stopped.stdout.toString().trim()) {
@@ -34206,7 +34530,7 @@ async function ensureDevPostgres(dataDir) {
34206
34530
  }
34207
34531
  async function runMigrations(databaseUrl) {
34208
34532
  const packagesDir = dirname14(dirname14(dirname14(dirname14(import.meta.dir))));
34209
- const migrateScript = resolve10(packagesDir, "packages/shared/src/db/migrate.ts");
34533
+ const migrateScript = resolve11(packagesDir, "packages/shared/src/db/migrate.ts");
34210
34534
  const result = await Bun.$`DATABASE_URL=${databaseUrl} bun run ${migrateScript}`.quiet().nothrow();
34211
34535
  if (result.exitCode !== 0) {
34212
34536
  throw new Error(`Migration failed: ${result.stderr.toString()}`);
@@ -34239,7 +34563,7 @@ __export(exports_node_impl, {
34239
34563
  runSetupWizard: () => runSetupWizard,
34240
34564
  restartNode: () => restartNode
34241
34565
  });
34242
- import { confirm as confirm6, input as input3, select as select4 } from "@inquirer/prompts";
34566
+ import { confirm as confirm5, input as input3, select as select4 } from "@inquirer/prompts";
34243
34567
  async function runSetupWizard() {
34244
34568
  console.log("");
34245
34569
  console.log(blue("Stacks Node Setup Wizard"));
@@ -34276,13 +34600,13 @@ async function runSetupWizard() {
34276
34600
  ],
34277
34601
  default: "mainnet"
34278
34602
  });
34279
- const autoStartIndexer = await confirm6({
34603
+ const autoStartIndexer = await confirm5({
34280
34604
  message: "Auto-start indexer when node starts?",
34281
34605
  default: true
34282
34606
  });
34283
34607
  let indexerPort = 3700;
34284
34608
  if (autoStartIndexer) {
34285
- const customPort = await confirm6({
34609
+ const customPort = await confirm5({
34286
34610
  message: "Use default indexer port (3700)?",
34287
34611
  default: true
34288
34612
  });
@@ -34388,7 +34712,7 @@ async function stopNode(_pathOverride, force, _wait) {
34388
34712
  return;
34389
34713
  }
34390
34714
  if (!force) {
34391
- const proceed = await confirm6({
34715
+ const proceed = await confirm5({
34392
34716
  message: "Stop the Stacks node?",
34393
34717
  default: false
34394
34718
  });
@@ -34418,7 +34742,7 @@ async function restartNode(pathOverride, force, _wait) {
34418
34742
  const wasRunning = await isNodeRunning();
34419
34743
  if (wasRunning) {
34420
34744
  if (!force) {
34421
- const proceed = await confirm6({
34745
+ const proceed = await confirm5({
34422
34746
  message: "Restart the Stacks node?",
34423
34747
  default: false
34424
34748
  });
@@ -34621,10 +34945,254 @@ var init_node_impl = __esm(() => {
34621
34945
  init_output();
34622
34946
  });
34623
34947
 
34948
+ // src/lib/archive-reference.ts
34949
+ import { createHash as createHash4 } from "node:crypto";
34950
+ import { readFile as readFile5 } from "node:fs/promises";
34951
+ import { dirname as dirname16, join as join13, resolve as resolve13, sep as sep2 } from "node:path";
34952
+ import { parseRetryAfter } from "@secondlayer/sdk";
34953
+ import { ARCHIVE_ROOT_PUBLIC_KEY_PEM } from "@secondlayer/shared/archive/root-key";
34954
+ import { verifyStreamsBulkManifestSignature } from "@secondlayer/shared/streams-bulk-manifest";
34955
+ function isLatestPointer(value) {
34956
+ const pointer = value;
34957
+ return typeof pointer.snapshot_path === "string" && (value.partitions === undefined || value.partitions.length === 0);
34958
+ }
34959
+ function assertPathWithinRoot(path11, root) {
34960
+ const segments = path11.split("/");
34961
+ const leaves = path11.startsWith("/") || path11.includes("\\") || /^[a-z][a-z0-9+.-]*:/i.test(path11) || /[%?#]/.test(path11) || segments.some((part) => part === "" || part === "." || part === "..");
34962
+ if (leaves) {
34963
+ throw new Error(`archive path ${path11} would leave the archive root`);
34964
+ }
34965
+ if (root !== undefined) {
34966
+ const base2 = resolve13(root);
34967
+ const target = resolve13(base2, path11);
34968
+ if (target !== base2 && !target.startsWith(base2 + sep2)) {
34969
+ throw new Error(`archive path ${path11} would leave the archive root`);
34970
+ }
34971
+ }
34972
+ }
34973
+ async function fetchJson(url) {
34974
+ const response2 = await fetch(url, { signal: AbortSignal.timeout(30000) });
34975
+ if (!response2.ok) {
34976
+ throw new Error(`could not fetch manifest (${response2.status} ${response2.statusText})`);
34977
+ }
34978
+ return response2.json();
34979
+ }
34980
+ async function loadReference(source, options3 = {}) {
34981
+ const isRemote = /^https?:\/\//.test(source);
34982
+ let manifest;
34983
+ let root;
34984
+ if (isRemote) {
34985
+ manifest = await fetchJson(source);
34986
+ const url = new URL(source);
34987
+ url.pathname = url.pathname.replace(/\/snapshots\/[^/]+$/, "").replace(/\/[^/]+\.json$/, "");
34988
+ root = url.toString();
34989
+ } else {
34990
+ const path11 = resolve13(source);
34991
+ manifest = JSON.parse(await readFile5(path11, "utf8"));
34992
+ root = isLatestPointer(manifest) ? dirname16(path11) : dirname16(dirname16(path11));
34993
+ }
34994
+ if (!isLatestPointer(manifest)) {
34995
+ return { manifest, origin: source, root, isRemote };
34996
+ }
34997
+ const pointer = manifest;
34998
+ const snapshotPath = pointer.snapshot_path;
34999
+ if (!SNAPSHOT_PATH_PATTERN.test(snapshotPath)) {
35000
+ throw new Error(`pointer names an invalid snapshot path: ${snapshotPath} (expected snapshots/<sha256>.json)`);
35001
+ }
35002
+ if (typeof pointer.snapshot_digest !== "string" || !/^[0-9a-f]{64}$/.test(pointer.snapshot_digest)) {
35003
+ throw new Error("pointer carries no snapshot_digest, so it cannot prove which snapshot is current");
35004
+ }
35005
+ if (pointer.signature !== undefined) {
35006
+ if (!options3.publicKeyPem) {
35007
+ throw new Error("pointer is signed but no public key is available to verify it");
35008
+ }
35009
+ let verified = false;
35010
+ try {
35011
+ verified = verifyStreamsBulkManifestSignature(pointer, options3.publicKeyPem);
35012
+ } catch {
35013
+ verified = false;
35014
+ }
35015
+ if (!verified) {
35016
+ throw new Error("pointer signature did not verify against the archive key");
35017
+ }
35018
+ }
35019
+ const resolved = isRemote ? await fetchJson(`${root.replace(/\/$/, "")}/${snapshotPath}`) : JSON.parse(await readFile5(join13(root, snapshotPath), "utf8"));
35020
+ const digest = createHash4("sha256").update(JSON.stringify((({ signature: _s, key_id: _k, ...rest }) => rest)(resolved))).digest("hex");
35021
+ if (digest !== pointer.snapshot_digest) {
35022
+ throw new Error(`pointer/snapshot mismatch: latest.json names ${pointer.snapshot_digest}, resolved manifest hashes to ${digest}`);
35023
+ }
35024
+ return { manifest: resolved, origin: source, root, isRemote };
35025
+ }
35026
+ function checkSignature(manifest, publicKeyPem, insecure) {
35027
+ if (insecure)
35028
+ return { verified: false, reason: "signature check skipped" };
35029
+ if (!manifest.signature) {
35030
+ return { verified: false, reason: "manifest carries no signature" };
35031
+ }
35032
+ if (!publicKeyPem) {
35033
+ return {
35034
+ verified: false,
35035
+ reason: "no public key available to verify with"
35036
+ };
35037
+ }
35038
+ try {
35039
+ const verified = verifyStreamsBulkManifestSignature(manifest, publicKeyPem);
35040
+ return {
35041
+ verified,
35042
+ reason: verified ? undefined : "signature did not verify"
35043
+ };
35044
+ } catch (err) {
35045
+ return {
35046
+ verified: false,
35047
+ reason: err instanceof Error ? err.message : "signature check failed"
35048
+ };
35049
+ }
35050
+ }
35051
+ async function fetchHostedKey(url) {
35052
+ if (!url.startsWith("https://"))
35053
+ return;
35054
+ try {
35055
+ const response2 = await fetch(url, { signal: AbortSignal.timeout(1e4) });
35056
+ if (!response2.ok)
35057
+ return;
35058
+ const body = await response2.json();
35059
+ return typeof body.public_key_pem === "string" ? body.public_key_pem : undefined;
35060
+ } catch {
35061
+ return;
35062
+ }
35063
+ }
35064
+ async function resolveArchivePublicKey(input4) {
35065
+ if (input4.explicitPem)
35066
+ return input4.explicitPem;
35067
+ if (input4.envPem)
35068
+ return input4.envPem;
35069
+ if (input4.allowHostedApi) {
35070
+ const hosted = await fetchHostedKey(input4.hostedKeyUrl ?? HOSTED_SIGNING_KEY_URL);
35071
+ if (hosted)
35072
+ return hosted;
35073
+ }
35074
+ return ARCHIVE_ROOT_PUBLIC_KEY_PEM;
35075
+ }
35076
+ function isTransientCode(code) {
35077
+ return typeof code === "string" && (TRANSIENT_ERROR_CODES.has(code) || code.startsWith("UND_ERR_"));
35078
+ }
35079
+ function isTransientFetchFailure(err) {
35080
+ if (!(err instanceof Error))
35081
+ return false;
35082
+ if (err.name === "AbortError" || err.name === "TimeoutError")
35083
+ return true;
35084
+ const code = err.code;
35085
+ const causeCode = err.cause?.code;
35086
+ if (isTransientCode(code) || isTransientCode(causeCode))
35087
+ return true;
35088
+ return err.name === "TypeError" && /fetch failed|network|socket|connection|reset|unreachable/i.test(err.message);
35089
+ }
35090
+ function retryDelayMs(attempt, response2) {
35091
+ const retryAfter = response2 ? parseRetryAfter(response2.headers.get("retry-after")) : undefined;
35092
+ if (retryAfter !== undefined) {
35093
+ return Math.min(retryAfter * 1000, RETRY_AFTER_CAP_MS);
35094
+ }
35095
+ return RETRY_BASE_MS * 2 ** (attempt - 1);
35096
+ }
35097
+ async function fetchWithRetry(url, opts) {
35098
+ const retries = opts.retries ?? PARTITION_FETCH_RETRIES;
35099
+ const fetchImpl = opts.fetchImpl ?? fetch;
35100
+ const sleep = opts.sleep ?? ((ms) => new Promise((r2) => setTimeout(r2, ms)));
35101
+ const timeoutMs = opts.timeoutMs ?? PARTITION_FETCH_TIMEOUT_MS;
35102
+ let lastReason = "";
35103
+ for (let attempt = 1;attempt <= retries + 1; attempt++) {
35104
+ let response2;
35105
+ try {
35106
+ response2 = await fetchImpl(url, {
35107
+ signal: AbortSignal.timeout(timeoutMs)
35108
+ });
35109
+ } catch (err) {
35110
+ if (!isTransientFetchFailure(err))
35111
+ throw err;
35112
+ lastReason = err instanceof Error ? err.message : String(err);
35113
+ }
35114
+ if (response2) {
35115
+ const retryable = response2.status === 429 || response2.status >= 500;
35116
+ if (!retryable)
35117
+ return response2;
35118
+ lastReason = `${response2.status} ${response2.statusText}`.trim();
35119
+ await response2.body?.cancel().catch(() => {});
35120
+ }
35121
+ if (attempt <= retries) {
35122
+ await sleep(retryDelayMs(attempt, response2));
35123
+ }
35124
+ }
35125
+ throw new ArchiveFetchError(`could not fetch ${opts.label} after ${retries + 1} attempts (${lastReason})`, { transient: true, attempts: retries + 1 });
35126
+ }
35127
+ async function fetchViaGate(gate, partition, retried, fetchOpts) {
35128
+ const url = await gate.getUrl(partition.path, retried ? { forceRefresh: true } : undefined);
35129
+ const response2 = await fetchWithRetry(url, {
35130
+ ...fetchOpts,
35131
+ label: partition.path
35132
+ });
35133
+ if (response2.status === 403 && !retried) {
35134
+ await response2.body?.cancel().catch(() => {});
35135
+ return fetchViaGate(gate, partition, true, fetchOpts);
35136
+ }
35137
+ if (!response2.ok) {
35138
+ throw new ArchiveFetchError(`could not fetch ${partition.path} (${response2.status} ${response2.statusText})`, { transient: false, attempts: 1 });
35139
+ }
35140
+ return Buffer.from(await response2.arrayBuffer());
35141
+ }
35142
+ async function fetchVerifiedPartition(reference, partition, gate, fetchOpts) {
35143
+ assertPathWithinRoot(partition.path, reference.isRemote ? undefined : reference.root);
35144
+ let bytes;
35145
+ if (gate) {
35146
+ bytes = await fetchViaGate(gate, partition, false, fetchOpts);
35147
+ } else if (reference.isRemote) {
35148
+ const url = `${reference.root.replace(/\/$/, "")}/${partition.path}`;
35149
+ const response2 = await fetchWithRetry(url, {
35150
+ ...fetchOpts,
35151
+ label: partition.path
35152
+ });
35153
+ if (!response2.ok) {
35154
+ throw new ArchiveFetchError(`could not fetch ${partition.path} (${response2.status} ${response2.statusText})`, { transient: false, attempts: 1 });
35155
+ }
35156
+ bytes = Buffer.from(await response2.arrayBuffer());
35157
+ } else {
35158
+ bytes = await readFile5(join13(reference.root, partition.path));
35159
+ }
35160
+ const digest = createHash4("sha256").update(bytes).digest("hex");
35161
+ if (digest !== partition.sha256) {
35162
+ throw new Error(`${partition.path} failed verification: expected ${partition.sha256}, got ${digest}`);
35163
+ }
35164
+ return bytes;
35165
+ }
35166
+ 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;
35167
+ var init_archive_reference = __esm(() => {
35168
+ SNAPSHOT_PATH_PATTERN = /^snapshots\/[0-9a-f]{64}\.json$/;
35169
+ ArchiveFetchError = class ArchiveFetchError extends Error {
35170
+ transient;
35171
+ attempts;
35172
+ constructor(message, opts) {
35173
+ super(message);
35174
+ this.name = "ArchiveFetchError";
35175
+ this.transient = opts.transient;
35176
+ this.attempts = opts.attempts;
35177
+ }
35178
+ };
35179
+ TRANSIENT_ERROR_CODES = new Set([
35180
+ "ECONNRESET",
35181
+ "ECONNREFUSED",
35182
+ "ECONNABORTED",
35183
+ "ETIMEDOUT",
35184
+ "EAI_AGAIN",
35185
+ "EPIPE",
35186
+ "ENETUNREACH",
35187
+ "EHOSTUNREACH"
35188
+ ]);
35189
+ });
35190
+
34624
35191
  // src/lib/instance-init.ts
34625
35192
  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";
35193
+ import { existsSync as existsSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync7 } from "node:fs";
35194
+ import { resolve as resolve14 } from "node:path";
35195
+ import { ARCHIVE_ROOT_PUBLIC_KEY_PEM as ARCHIVE_ROOT_PUBLIC_KEY_PEM2 } from "@secondlayer/shared/archive/root-key";
34628
35196
  import { generateEd25519KeyPair } from "@secondlayer/shared/crypto/ed25519";
34629
35197
  import { INSTANCE_NETWORKS } from "@secondlayer/shared/db/queries/instance";
34630
35198
  function parseInstanceNetwork(value) {
@@ -34634,6 +35202,17 @@ function parseInstanceNetwork(value) {
34634
35202
  }
34635
35203
  throw new Error(`network must be mainnet, testnet, or devnet (got ${value})`);
34636
35204
  }
35205
+ function instanceNetworkFromEnv(fallback = "mainnet") {
35206
+ const raw = process.env.STACKS_NETWORK ?? fallback;
35207
+ try {
35208
+ return parseInstanceNetwork(raw);
35209
+ } catch (err) {
35210
+ printError(err instanceof Error ? err.message : String(err), {
35211
+ hint: "Pass --network mainnet, --network testnet, or --network devnet."
35212
+ });
35213
+ process.exit(1);
35214
+ }
35215
+ }
34637
35216
  function generateInstanceToken() {
34638
35217
  return randomBytes3(32).toString("hex");
34639
35218
  }
@@ -34657,9 +35236,9 @@ function unescapeEnvValue(raw) {
34657
35236
  `).replace(/\\"/g, '"').replace(/\\\\/g, "\\");
34658
35237
  }
34659
35238
  function readEnvValue(envPath, key) {
34660
- if (!existsSync10(envPath))
35239
+ if (!existsSync8(envPath))
34661
35240
  return null;
34662
- const match = readFileSync9(envPath, "utf8").match(new RegExp(`^${key}=(.*)$`, "m"));
35241
+ const match = readFileSync7(envPath, "utf8").match(new RegExp(`^${key}=(.*)$`, "m"));
34663
35242
  return match ? unescapeEnvValue(match[1]) : null;
34664
35243
  }
34665
35244
  function buildInstanceEnv(input4) {
@@ -34667,7 +35246,7 @@ function buildInstanceEnv(input4) {
34667
35246
  const secrets = input4.existing?.SECONDLAYER_SECRETS_KEY || generateSecretsKey();
34668
35247
  const signing = input4.existing?.STREAMS_SIGNING_PRIVATE_KEY || input4.existing?.SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY || generateSigningPrivateKey();
34669
35248
  const apiUrl = input4.apiUrl ?? "http://127.0.0.1:3800";
34670
- const archiveKey = input4.existing?.ARCHIVE_SIGNING_PUBLIC_KEY;
35249
+ const archiveKey = input4.existing?.ARCHIVE_SIGNING_PUBLIC_KEY || input4.archivePublicKeyPem || ARCHIVE_ROOT_PUBLIC_KEY_PEM2;
34671
35250
  return {
34672
35251
  INSTANCE_MODE: "oss",
34673
35252
  STACKS_NETWORK: input4.network,
@@ -34678,7 +35257,7 @@ function buildInstanceEnv(input4) {
34678
35257
  ALLOW_UNSIGNED_WEBHOOKS: "false",
34679
35258
  SL_API_URL: input4.existing?.SL_API_URL || apiUrl,
34680
35259
  SL_API_KEY: token,
34681
- ...archiveKey ? { ARCHIVE_SIGNING_PUBLIC_KEY: archiveKey } : {}
35260
+ ARCHIVE_SIGNING_PUBLIC_KEY: archiveKey
34682
35261
  };
34683
35262
  }
34684
35263
  function renderInstanceEnv(env2) {
@@ -34698,20 +35277,17 @@ function renderInstanceEnv(env2) {
34698
35277
  "# INSTANCE_TOKEN wins if the two ever disagree.",
34699
35278
  `SL_API_KEY=${env2.SL_API_KEY}`
34700
35279
  ];
34701
- if (env2.ARCHIVE_SIGNING_PUBLIC_KEY) {
34702
- lines2.push(`ARCHIVE_SIGNING_PUBLIC_KEY=${escapeEnvValue(env2.ARCHIVE_SIGNING_PUBLIC_KEY)}`);
34703
- }
34704
- lines2.push("");
35280
+ 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
35281
  return lines2.join(`
34706
35282
  `);
34707
35283
  }
34708
35284
  function writeInstanceEnv(cwd, env2, file = INSTANCE_ENV_FILE) {
34709
- const path11 = resolve15(cwd, file);
34710
- writeFileSync8(path11, renderInstanceEnv(env2), { mode: 384 });
35285
+ const path11 = resolve14(cwd, file);
35286
+ writeFileSync7(path11, renderInstanceEnv(env2), { mode: 384 });
34711
35287
  return path11;
34712
35288
  }
34713
35289
  function loadExistingInstanceEnv(cwd, file = INSTANCE_ENV_FILE) {
34714
- const path11 = resolve15(cwd, file);
35290
+ const path11 = resolve14(cwd, file);
34715
35291
  return {
34716
35292
  INSTANCE_TOKEN: readEnvValue(path11, "INSTANCE_TOKEN"),
34717
35293
  SECONDLAYER_SECRETS_KEY: readEnvValue(path11, "SECONDLAYER_SECRETS_KEY"),
@@ -34723,7 +35299,9 @@ function loadExistingInstanceEnv(cwd, file = INSTANCE_ENV_FILE) {
34723
35299
  };
34724
35300
  }
34725
35301
  var INSTANCE_ENV_FILE = ".env.local";
34726
- var init_instance_init = () => {};
35302
+ var init_instance_init = __esm(() => {
35303
+ init_output();
35304
+ });
34727
35305
 
34728
35306
  // src/lib/observer-stanza.ts
34729
35307
  function parseObserverMode(value) {
@@ -34995,9 +35573,10 @@ var init_oss_compose = () => {};
34995
35573
  import { spawn as spawn4, spawnSync as spawnSync5 } from "node:child_process";
34996
35574
  import { randomBytes as randomBytes4 } from "node:crypto";
34997
35575
  import { existsSync as existsSync11 } from "node:fs";
34998
- import { writeFile as writeFile7 } from "node:fs/promises";
35576
+ import { writeFile as writeFile6 } from "node:fs/promises";
34999
35577
  import { createServer } from "node:net";
35000
- import { join as join19, resolve as resolvePath2 } from "node:path";
35578
+ import { join as join18, resolve as resolvePath2 } from "node:path";
35579
+ import { closeDb as closeDb2, getRawClientFor } from "@secondlayer/shared/db";
35001
35580
  import { FLOORS, diskFloorGb } from "@secondlayer/shared/runtime";
35002
35581
  function parseSetupNodeMode(value) {
35003
35582
  const mode = value.trim().toLowerCase();
@@ -35090,7 +35669,7 @@ function randomHex(bytes) {
35090
35669
  return randomBytes4(bytes).toString("hex");
35091
35670
  }
35092
35671
  function resolveSecrets(config) {
35093
- const envPath = join19(config.dir, ".env");
35672
+ const envPath = join18(config.dir, ".env");
35094
35673
  const existing = config.force || !existsSync11(envPath) ? {} : readEnvFile(config.dir, envPath);
35095
35674
  const instance = buildInstanceEnv({
35096
35675
  network: config.network,
@@ -35103,14 +35682,33 @@ function resolveSecrets(config) {
35103
35682
  SL_API_KEY: existing.SL_API_KEY,
35104
35683
  ARCHIVE_SIGNING_PUBLIC_KEY: existing.ARCHIVE_SIGNING_PUBLIC_KEY
35105
35684
  },
35106
- apiUrl: `http://${normalizeLoopbackHost(config.apiPort)}`
35685
+ apiUrl: `http://${normalizeLoopbackHost(config.apiPort)}`,
35686
+ archivePublicKeyPem: config.archivePublicKeyPem
35107
35687
  });
35108
35688
  return {
35109
35689
  instance,
35110
35690
  postgresPassword: existing.POSTGRES_PASSWORD || randomHex(24),
35111
- bitcoinRpcPassword: existing.BITCOIN_RPC_PASSWORD || randomHex(24)
35691
+ bitcoinRpcPassword: existing.BITCOIN_RPC_PASSWORD || randomHex(24),
35692
+ databaseUrl: operatorEditedDatabaseUrl(existing)
35112
35693
  };
35113
35694
  }
35695
+ function operatorEditedDatabaseUrl(existing) {
35696
+ const url = existing.DATABASE_URL;
35697
+ if (!url)
35698
+ return;
35699
+ if (existing.POSTGRES_PORT && existing.POSTGRES_PASSWORD) {
35700
+ const generated = setupDatabaseUrl({ postgresPort: existing.POSTGRES_PORT }, { postgresPassword: existing.POSTGRES_PASSWORD });
35701
+ if (url === generated)
35702
+ return;
35703
+ }
35704
+ return url;
35705
+ }
35706
+ function setupDatabaseUrl(config, secrets) {
35707
+ if (secrets.databaseUrl)
35708
+ return secrets.databaseUrl;
35709
+ const password = encodeURIComponent(secrets.postgresPassword);
35710
+ return `postgres://${SETUP_POSTGRES_USER}:${password}@${normalizeLoopbackHost(config.postgresPort)}/${SETUP_POSTGRES_DB}`;
35711
+ }
35114
35712
  function normalizeLoopbackHost(apiPort) {
35115
35713
  return apiPort.includes(":") ? apiPort : `127.0.0.1:${apiPort}`;
35116
35714
  }
@@ -35119,7 +35717,9 @@ function readEnvFile(dir, envPath) {
35119
35717
  return {
35120
35718
  ...shared,
35121
35719
  POSTGRES_PASSWORD: readEnvValue(envPath, "POSTGRES_PASSWORD"),
35122
- BITCOIN_RPC_PASSWORD: readEnvValue(envPath, "BITCOIN_RPC_PASSWORD")
35720
+ POSTGRES_PORT: readEnvValue(envPath, "POSTGRES_PORT"),
35721
+ BITCOIN_RPC_PASSWORD: readEnvValue(envPath, "BITCOIN_RPC_PASSWORD"),
35722
+ DATABASE_URL: readEnvValue(envPath, "DATABASE_URL")
35123
35723
  };
35124
35724
  }
35125
35725
  function renderSetupEnv(config, secrets) {
@@ -35131,10 +35731,14 @@ function renderSetupEnv(config, secrets) {
35131
35731
  `NODE_MODE=${config.nodeMode}`,
35132
35732
  `API_PORT=${config.apiPort}`,
35133
35733
  `INDEXER_PORT=${config.indexerPort}`,
35134
- "POSTGRES_USER=secondlayer",
35734
+ `POSTGRES_USER=${SETUP_POSTGRES_USER}`,
35135
35735
  `POSTGRES_PASSWORD=${secrets.postgresPassword}`,
35136
- "POSTGRES_DB=secondlayer",
35736
+ `POSTGRES_DB=${SETUP_POSTGRES_DB}`,
35137
35737
  `POSTGRES_PORT=${config.postgresPort}`,
35738
+ "# Where bootstrap, verify, repair, and backup connect from this directory:",
35739
+ "# the compose Postgres above, published on the host. Point it elsewhere",
35740
+ "# only if you moved the index.",
35741
+ `DATABASE_URL=${escapeEnvValue(setupDatabaseUrl(config, secrets))}`,
35138
35742
  "# The instance credential. The API validates bearer tokens against it,",
35139
35743
  "# and the CLI/SDK/MCP read it first when authenticating.",
35140
35744
  `INSTANCE_TOKEN=${i3.INSTANCE_TOKEN}`,
@@ -35146,9 +35750,7 @@ function renderSetupEnv(config, secrets) {
35146
35750
  `SL_API_KEY=${i3.SL_API_KEY}`,
35147
35751
  `SL_API_URL=${i3.SL_API_URL}`
35148
35752
  ];
35149
- if (i3.ARCHIVE_SIGNING_PUBLIC_KEY) {
35150
- lines2.push(`ARCHIVE_SIGNING_PUBLIC_KEY=${escapeEnvValue(i3.ARCHIVE_SIGNING_PUBLIC_KEY)}`);
35151
- }
35753
+ 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
35754
  if (config.nodeMode === "full") {
35153
35755
  lines2.push("BITCOIN_RPC_USER=stacks", `BITCOIN_RPC_PASSWORD=${secrets.bitcoinRpcPassword}`);
35154
35756
  }
@@ -35157,22 +35759,22 @@ function renderSetupEnv(config, secrets) {
35157
35759
  `);
35158
35760
  }
35159
35761
  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 });
35762
+ const composePath = join18(config.dir, "docker-compose.yml");
35763
+ const envPath = join18(config.dir, ".env");
35764
+ await writeFile6(composePath, buildOssCompose({ owner: config.owner, imageTag: config.imageTag }));
35765
+ await writeFile6(envPath, renderSetupEnv(config, secrets), { mode: 384 });
35164
35766
  const result = { composePath, envPath };
35165
35767
  if (config.nodeMode === "full") {
35166
- const configTomlPath = join19(config.dir, "Config.toml");
35167
- await writeFile7(configTomlPath, buildOssStacksConfigToml({
35768
+ const configTomlPath = join18(config.dir, "Config.toml");
35769
+ await writeFile6(configTomlPath, buildOssStacksConfigToml({
35168
35770
  network: config.network,
35169
35771
  bitcoinRpcPassword: secrets.bitcoinRpcPassword ?? ""
35170
35772
  }));
35171
35773
  result.configTomlPath = configTomlPath;
35172
35774
  }
35173
35775
  if (config.nodeMode === "full") {
35174
- const bitcoinConfPath = join19(config.dir, "bitcoin.conf");
35175
- await writeFile7(bitcoinConfPath, buildOssBitcoinConf({ rpcPassword: secrets.bitcoinRpcPassword ?? "" }));
35776
+ const bitcoinConfPath = join18(config.dir, "bitcoin.conf");
35777
+ await writeFile6(bitcoinConfPath, buildOssBitcoinConf({ rpcPassword: secrets.bitcoinRpcPassword ?? "" }));
35176
35778
  result.bitcoinConfPath = bitcoinConfPath;
35177
35779
  }
35178
35780
  return result;
@@ -35186,6 +35788,7 @@ function runStreaming(command, args, opts) {
35186
35788
  return new Promise((resolvePromise, reject) => {
35187
35789
  const child = spawn4(command, args, {
35188
35790
  cwd: opts.cwd,
35791
+ env: opts.env ?? process.env,
35189
35792
  stdio: ["ignore", "pipe", "pipe"]
35190
35793
  });
35191
35794
  let stdout = "";
@@ -35261,15 +35864,74 @@ function cliEntry() {
35261
35864
  }
35262
35865
  return [process.execPath, entry];
35263
35866
  }
35264
- async function runBootstrap(config, against, onLine) {
35867
+ function buildChildEnv(envPath, base2 = process.env) {
35868
+ const env2 = { ...base2 };
35869
+ for (const key of CHILD_ENV_KEYS) {
35870
+ const value = readEnvValue(envPath, key);
35871
+ if (value)
35872
+ env2[key] = value;
35873
+ }
35874
+ return env2;
35875
+ }
35876
+ async function checkDatabaseReachable(databaseUrl) {
35877
+ try {
35878
+ const client3 = getRawClientFor(databaseUrl);
35879
+ await client3`SELECT 1`;
35880
+ return { ok: true };
35881
+ } catch (err) {
35882
+ return {
35883
+ ok: false,
35884
+ error: err instanceof Error ? err.message : String(err)
35885
+ };
35886
+ } finally {
35887
+ await closeDb2().catch(() => {});
35888
+ }
35889
+ }
35890
+ async function preflightBootstrapDatabase(config, secrets, emit2) {
35891
+ const databaseUrl = setupDatabaseUrl(config, secrets);
35892
+ const reachable = await checkDatabaseReachable(databaseUrl);
35893
+ if (!reachable.ok) {
35894
+ emit2({
35895
+ type: "step-error",
35896
+ step: "bootstrap",
35897
+ message: `Postgres at ${redactUrl(databaseUrl)} did not answer SELECT 1: ${reachable.error}. Check \`docker compose logs postgres\`, then re-run setup to resume.`
35898
+ });
35899
+ return null;
35900
+ }
35901
+ emit2({
35902
+ type: "step-log",
35903
+ step: "bootstrap",
35904
+ line: `database ${redactUrl(databaseUrl)}`
35905
+ });
35906
+ return databaseUrl;
35907
+ }
35908
+ async function runBootstrap(config, against, files, onLine) {
35265
35909
  const [command, ...prefix] = cliEntry();
35266
35910
  const args = [...prefix, "bootstrap", "--against", against, "--yes"];
35267
- return runStreaming(command, args, { cwd: config.dir, onLine });
35911
+ return runStreaming(command, args, {
35912
+ cwd: config.dir,
35913
+ env: buildChildEnv(files.envPath),
35914
+ onLine
35915
+ });
35268
35916
  }
35269
- async function runVerify(config, against, onLine) {
35917
+ async function runVerify(config, against, files, onLine) {
35270
35918
  const [command, ...prefix] = cliEntry();
35271
35919
  const args = [...prefix, "verify", "all", "--against", against];
35272
- return runStreaming(command, args, { cwd: config.dir, onLine });
35920
+ return runStreaming(command, args, {
35921
+ cwd: config.dir,
35922
+ env: buildChildEnv(files.envPath),
35923
+ onLine
35924
+ });
35925
+ }
35926
+ function redactUrl(url) {
35927
+ try {
35928
+ const u3 = new URL(url);
35929
+ if (u3.password)
35930
+ u3.password = "***";
35931
+ return u3.toString();
35932
+ } catch {
35933
+ return url;
35934
+ }
35273
35935
  }
35274
35936
  function buildSuccessSummary(config, secrets) {
35275
35937
  const apiUrl = `http://${normalizeLoopbackHost(config.apiPort)}`;
@@ -35341,7 +36003,11 @@ async function runSetup(initialConfig, emit2) {
35341
36003
  dir: config.dir,
35342
36004
  network: config.network,
35343
36005
  apiPort: config.apiPort,
35344
- force: config.force
36006
+ force: config.force,
36007
+ archivePublicKeyPem: await resolveArchivePublicKey({
36008
+ envPem: process.env.ARCHIVE_SIGNING_PUBLIC_KEY ?? process.env.STREAMS_SIGNING_PUBLIC_KEY,
36009
+ allowHostedApi: !isOssMode()
36010
+ })
35345
36011
  });
35346
36012
  emit2({ type: "step-done", step: "secrets" });
35347
36013
  emit2({ type: "step-start", step: "config" });
@@ -35408,7 +36074,11 @@ ${up.stderr || up.stdout}`
35408
36074
  emit2({ type: "step-skip", step: "bootstrap", reason: "--skip-bootstrap" });
35409
36075
  } else if (config.against) {
35410
36076
  emit2({ type: "step-start", step: "bootstrap" });
35411
- const res = await runBootstrap(config, config.against, (line) => emit2({ type: "step-log", step: "bootstrap", line }));
36077
+ const databaseUrl = await preflightBootstrapDatabase(config, secrets, emit2);
36078
+ if (databaseUrl === null) {
36079
+ return { ok: false, config, secrets, files, observer };
36080
+ }
36081
+ const res = await runBootstrap(config, config.against, files, (line) => emit2({ type: "step-log", step: "bootstrap", line }));
35412
36082
  if (res.code === 0) {
35413
36083
  bootstrapped = true;
35414
36084
  emit2({ type: "step-done", step: "bootstrap" });
@@ -35436,7 +36106,7 @@ ${up.stderr || up.stdout}`
35436
36106
  });
35437
36107
  } else {
35438
36108
  emit2({ type: "step-start", step: "verify" });
35439
- const res = await runVerify(config, config.against, (line) => emit2({ type: "step-log", step: "verify", line }));
36109
+ const res = await runVerify(config, config.against, files, (line) => emit2({ type: "step-log", step: "verify", line }));
35440
36110
  if (res.code === 0) {
35441
36111
  emit2({ type: "step-done", step: "verify" });
35442
36112
  } else {
@@ -35452,11 +36122,13 @@ ${up.stderr || up.stdout}`
35452
36122
  emit2({ type: "step-done", step: "summary", detail: summary });
35453
36123
  return { ok: true, config, secrets, files, observer, summary };
35454
36124
  }
35455
- var SETUP_NODE_MODES, DEFAULT_ARCHIVE_MANIFEST = "https://archive.secondlayer.tools/latest.json", MissingSetupFlagError;
36125
+ 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
36126
  var init_setup_wizard = __esm(() => {
36127
+ init_archive_reference();
35457
36128
  init_instance_init();
35458
36129
  init_observer_stanza();
35459
36130
  init_oss_compose();
36131
+ init_resolve_auth();
35460
36132
  SETUP_NODE_MODES = ["external", "stacks", "full"];
35461
36133
  MissingSetupFlagError = class MissingSetupFlagError extends Error {
35462
36134
  flag;
@@ -35466,6 +36138,12 @@ var init_setup_wizard = __esm(() => {
35466
36138
  this.flag = flag;
35467
36139
  }
35468
36140
  };
36141
+ CHILD_ENV_KEYS = [
36142
+ "DATABASE_URL",
36143
+ "ARCHIVE_SIGNING_PUBLIC_KEY",
36144
+ "SL_API_URL",
36145
+ "INSTANCE_TOKEN"
36146
+ ];
35469
36147
  });
35470
36148
 
35471
36149
  // src/lib/setup-tui.tsx
@@ -35473,6 +36151,7 @@ var exports_setup_tui = {};
35473
36151
  __export(exports_setup_tui, {
35474
36152
  runSetupTui: () => runSetupTui
35475
36153
  });
36154
+ import { resolve as resolvePath4 } from "node:path";
35476
36155
  import {
35477
36156
  CliRenderEvents,
35478
36157
  createCliRenderer
@@ -35578,7 +36257,7 @@ function App({ flags }) {
35578
36257
  apiPort: flags.apiPort ?? "127.0.0.1:3800",
35579
36258
  indexerPort: "127.0.0.1:3700",
35580
36259
  postgresPort: "127.0.0.1:5432",
35581
- dir: flags.dir ?? process.cwd(),
36260
+ dir: resolvePath4(flags.dir ?? process.cwd()),
35582
36261
  against: skipBootstrap ? undefined : against,
35583
36262
  skipBootstrap,
35584
36263
  skipVerify: !!flags.skipVerify,
@@ -35992,13 +36671,13 @@ async function runSetupTui(flags) {
35992
36671
  createRoot(renderer).render(/* @__PURE__ */ jsxDEV(App, {
35993
36672
  flags
35994
36673
  }, undefined, false, undefined, this));
35995
- return new Promise((resolve16, reject) => {
36674
+ return new Promise((resolve17, reject) => {
35996
36675
  let settled = false;
35997
36676
  const onDestroy = () => {
35998
36677
  if (settled)
35999
36678
  return;
36000
36679
  settled = true;
36001
- resolve16();
36680
+ resolve17();
36002
36681
  };
36003
36682
  const onError = (event) => {
36004
36683
  if (settled)
@@ -36098,7 +36777,7 @@ var {
36098
36777
  // package.json
36099
36778
  var package_default = {
36100
36779
  name: "@secondlayer/cli",
36101
- version: "13.0.3",
36780
+ version: "14.0.1",
36102
36781
  description: "Run a Stacks index on your own hardware — set it up, restore verified history, and keep it current.",
36103
36782
  repository: {
36104
36783
  type: "git",
@@ -36142,11 +36821,11 @@ var package_default = {
36142
36821
  "@opentui/core": "^0.5.3",
36143
36822
  "@opentui/react": "^0.5.3",
36144
36823
  "@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",
36824
+ "@secondlayer/scaffold": "^1.5.5",
36825
+ "@secondlayer/sdk": "^10.0.0",
36826
+ "@secondlayer/shared": "^11.2.0",
36827
+ "@secondlayer/stacks": "^5.0.1",
36828
+ "@secondlayer/subgraphs": "^4.1.1",
36150
36829
  esbuild: "^0.27.4",
36151
36830
  react: "^19.1.0",
36152
36831
  "smol-toml": "^1.6.1",
@@ -36201,7 +36880,7 @@ async function runBuy(opts) {
36201
36880
  human: () => {
36202
36881
  success(`Checkout for $${amount} → ${email}`);
36203
36882
  console.log(res.url);
36204
- console.log(dim("After payment: SL_API_URL=https://api.secondlayer.tools secondlayer login && secondlayer credits balance"));
36883
+ console.log(dim(`After payment: ${ARCHIVE_LOGIN_COMMAND} && secondlayer credits balance`));
36205
36884
  }
36206
36885
  });
36207
36886
  }
@@ -36585,7 +37264,11 @@ Examples:
36585
37264
  });
36586
37265
  config.command("reset").description("Reset configuration to defaults").option("-y, --yes", "Skip confirmation").action(async (opts) => {
36587
37266
  try {
36588
- if (!opts.yes && !await confirmDestructive("Reset configuration to defaults?")) {
37267
+ if (!await confirmDestructive({
37268
+ message: "Reset configuration to defaults?",
37269
+ yes: opts.yes
37270
+ })) {
37271
+ info("Cancelled.");
36589
37272
  return;
36590
37273
  }
36591
37274
  await resetConfig();
@@ -36597,7 +37280,11 @@ Examples:
36597
37280
  });
36598
37281
  config.command("delete").description("Delete all configuration (remove the config file)").option("-y, --yes", "Skip confirmation").action(async (opts) => {
36599
37282
  try {
36600
- if (!opts.yes && !await confirmDestructive("Delete the config file?")) {
37283
+ if (!await confirmDestructive({
37284
+ message: "Delete the config file?",
37285
+ yes: opts.yes
37286
+ })) {
37287
+ info("Cancelled.");
36601
37288
  return;
36602
37289
  }
36603
37290
  await clearConfig();
@@ -36608,26 +37295,6 @@ Examples:
36608
37295
  }
36609
37296
  });
36610
37297
  }
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
37298
  async function printConfigTree(cfg) {
36632
37299
  const defaults2 = getDefaultConfig();
36633
37300
  printValue("network", cfg.network, isDefaultValue(cfg, "network"));
@@ -36761,7 +37428,9 @@ function registerConsoleCommand(program2) {
36761
37428
  }
36762
37429
  // src/commands/context.ts
36763
37430
  init_http();
36764
- import { SecondLayer as SecondLayer2 } from "@secondlayer/sdk";
37431
+ import {
37432
+ SecondLayer as SecondLayer2
37433
+ } from "@secondlayer/sdk";
36765
37434
 
36766
37435
  // src/lib/instance-diagnosis.ts
36767
37436
  var BOOTSTRAP_STEP = "Restore verified history: secondlayer bootstrap --against <manifest>";
@@ -36881,8 +37550,19 @@ function registerContextCommand(program2) {
36881
37550
  const apiKey = resolveEnvKey();
36882
37551
  const sl = new SecondLayer2({ baseUrl: apiUrl, apiKey });
36883
37552
  const snapshot = await sl.context();
36884
- const diagnostics = await explain(snapshot, apiUrl, apiKey);
36885
- const payload = { ...snapshot, diagnostics };
37553
+ const values2 = {};
37554
+ const errors2 = {};
37555
+ for (const [field, entry] of Object.entries(snapshot)) {
37556
+ values2[field] = entry.value;
37557
+ if (entry.error)
37558
+ errors2[field] = entry.error;
37559
+ }
37560
+ const diagnostics = await explain(values2, apiUrl, apiKey);
37561
+ const payload = {
37562
+ ...values2,
37563
+ ...Object.keys(errors2).length > 0 ? { errors: errors2 } : {},
37564
+ diagnostics
37565
+ };
36886
37566
  output({
36887
37567
  json: o3.json,
36888
37568
  data: payload,
@@ -37002,7 +37682,6 @@ function printPublicStatus(status, diagnosis) {
37002
37682
  init_api_client();
37003
37683
  init_filter_params();
37004
37684
  init_output();
37005
- import { confirm as confirm4 } from "@inquirer/prompts";
37006
37685
  import { sign } from "@secondlayer/shared/crypto/standard-webhooks";
37007
37686
 
37008
37687
  // src/lib/subscription-validation.ts
@@ -37028,6 +37707,7 @@ init_filter_params();
37028
37707
  init_output();
37029
37708
  init_resolve_auth();
37030
37709
  import {
37710
+ chmodSync,
37031
37711
  copyFileSync,
37032
37712
  existsSync as existsSync4,
37033
37713
  mkdirSync as mkdirSync3,
@@ -37108,23 +37788,51 @@ function parseTriggersInput(opts) {
37108
37788
  return result.data;
37109
37789
  });
37110
37790
  }
37791
+ function resolveRuntime(opts) {
37792
+ if (opts.runtime)
37793
+ return opts.runtime;
37794
+ const scripted = opts.subgraph !== undefined || opts.table !== undefined || opts.url !== undefined || opts.scaffold === false;
37795
+ return scripted ? "node" : undefined;
37796
+ }
37797
+ function requireTtyFor(flag) {
37798
+ if (process.stdin.isTTY)
37799
+ return;
37800
+ error(`${flag} is required when stdin is not a TTY (no prompt can run). Pass it, or run from a terminal.`);
37801
+ process.exit(1);
37802
+ }
37111
37803
  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
- });
37804
+ let runtime = resolveRuntime(opts);
37805
+ if (runtime === undefined) {
37806
+ requireTtyFor("--runtime");
37807
+ runtime = await select3({
37808
+ message: "Runtime?",
37809
+ choices: RUNTIMES.map((r2) => ({ name: r2, value: r2 }))
37810
+ });
37811
+ }
37812
+ let subgraph = opts.subgraph;
37813
+ if (subgraph === undefined) {
37814
+ requireTtyFor("--subgraph");
37815
+ subgraph = await input2({
37816
+ message: "Subgraph name (must already be deployed):",
37817
+ validate: (v2) => v2.trim().length > 0 ? true : "required"
37818
+ });
37819
+ }
37820
+ let table = opts.table;
37821
+ if (table === undefined) {
37822
+ requireTtyFor("--table");
37823
+ table = await input2({
37824
+ message: "Table to subscribe to:",
37825
+ validate: (v2) => v2.trim().length > 0 ? true : "required"
37826
+ });
37827
+ }
37828
+ let url = opts.url;
37829
+ if (url === undefined) {
37830
+ requireTtyFor("--url");
37831
+ url = await input2({
37832
+ 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):",
37833
+ validate: (v2) => v2.startsWith("http://") || v2.startsWith("https://") ? true : "must be http(s) URL"
37834
+ });
37835
+ }
37128
37836
  return { runtime, subgraph, table, url };
37129
37837
  }
37130
37838
  function buildSubscriptionAuthConfig(authToken) {
@@ -37136,6 +37844,27 @@ function buildSubscriptionAuthConfig(authToken) {
37136
37844
  }
37137
37845
  return { authType: "bearer", token };
37138
37846
  }
37847
+ function writeSigningSecretEnv(targetDir, signingSecret) {
37848
+ const envTarget = join6(targetDir, ".env");
37849
+ const envExample = join6(targetDir, ".env.example");
37850
+ if (existsSync4(envExample) && !existsSync4(envTarget)) {
37851
+ copyFileSync(envExample, envTarget);
37852
+ chmodSync(envTarget, 384);
37853
+ }
37854
+ if (!existsSync4(envTarget)) {
37855
+ writeFileSync4(envTarget, `SIGNING_SECRET=${signingSecret}
37856
+ `, {
37857
+ mode: 384
37858
+ });
37859
+ } else {
37860
+ const cur = readFileSync5(envTarget, "utf8");
37861
+ writeFileSync4(envTarget, cur.match(/^SIGNING_SECRET=/m) ? cur.replace(/^SIGNING_SECRET=.*/m, `SIGNING_SECRET=${signingSecret}`) : `${cur}
37862
+ SIGNING_SECRET=${signingSecret}
37863
+ `, { mode: 384 });
37864
+ chmodSync(envTarget, 384);
37865
+ }
37866
+ return envTarget;
37867
+ }
37139
37868
  async function createSubscription(name, opts) {
37140
37869
  if (opts.runtime && !RUNTIMES.includes(opts.runtime)) {
37141
37870
  error(`Unknown --runtime "${opts.runtime}". Valid: ${RUNTIMES.join(", ")}`);
@@ -37167,7 +37896,7 @@ async function createSubscription(name, opts) {
37167
37896
  }
37168
37897
  const eventName = `${subgraph}.${table}.created`;
37169
37898
  const targetDir = resolve5(process.cwd(), name);
37170
- if (!opts.scaffold === false) {
37899
+ if (opts.scaffold !== false) {
37171
37900
  if (existsSync4(targetDir)) {
37172
37901
  error(`Directory already exists: ${relative(process.cwd(), targetDir)}`);
37173
37902
  process.exit(1);
@@ -37214,21 +37943,8 @@ async function createSubscription(name, opts) {
37214
37943
  subscriptionStatus = created?.status;
37215
37944
  } catch {}
37216
37945
  }
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
- }
37946
+ if (signingSecret && opts.scaffold !== false) {
37947
+ const envTarget = writeSigningSecretEnv(targetDir, signingSecret);
37232
37948
  success(`Signing secret written to ${relative(process.cwd(), envTarget)}`);
37233
37949
  } else if (signingSecret && opts.scaffold === false) {
37234
37950
  info(`Signing secret (store securely): ${signingSecret}`);
@@ -37301,7 +38017,7 @@ async function getSubscriptionClient() {
37301
38017
  return new SecondLayer3({ baseUrl: apiUrl, apiKey: ephemeralKey });
37302
38018
  }
37303
38019
  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", `
38020
+ 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
38021
  Examples:
37306
38022
  $ ${opts.examplePrefix} my-sub -s my-graph -t transfers -u https://example.com/webhook
37307
38023
  $ ${opts.examplePrefix} my-sub -s my-graph -t balances -r inngest --filter amount.gte=1000
@@ -37668,28 +38384,10 @@ function printDead(rows) {
37668
38384
  ])));
37669
38385
  }
37670
38386
  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) {
38387
+ const ok = await confirmDestructive({ message, yes });
38388
+ if (!ok)
37689
38389
  info("Cancelled");
37690
- return false;
37691
- }
37692
- return true;
38390
+ return ok;
37693
38391
  }
37694
38392
  function registerSubscriptionsCommand(program2) {
37695
38393
  const subscriptions = program2.command("subscriptions").alias("subs").description("Manage subgraph table subscriptions");
@@ -37732,7 +38430,7 @@ ${data.length} subscription(s) total`));
37732
38430
  handleApiError(err, "get subscription");
37733
38431
  }
37734
38432
  });
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", `
38433
+ 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
38434
  Examples:
37737
38435
  $ secondlayer subscriptions update my-sub --url https://example.com/hook
37738
38436
  $ secondlayer subscriptions update my-sub --filter amount.gte=1000 --max-retries 5
@@ -37917,7 +38615,7 @@ Examples:
37917
38615
  handleApiError(err, "diagnose subscription");
37918
38616
  }
37919
38617
  });
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) => {
38618
+ 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
38619
  try {
37922
38620
  const signingSecret = resolveSigningSecret(options3);
37923
38621
  const client2 = await getSubscriptionClient();
@@ -37944,7 +38642,8 @@ Examples:
37944
38642
  const res = await fetch(detail.url, {
37945
38643
  method: "POST",
37946
38644
  headers: fixture.headers,
37947
- body: fixture.body
38645
+ body: fixture.body,
38646
+ signal: AbortSignal.timeout(15000)
37948
38647
  });
37949
38648
  postResult = {
37950
38649
  status: res.status,
@@ -38418,8 +39117,9 @@ init_subgraphs2();
38418
39117
  // src/commands/streams.ts
38419
39118
  init_output();
38420
39119
  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";
39120
+ import { createHash as createHash3 } from "node:crypto";
39121
+ import { mkdir as mkdir3, open as open2, rename as rename2 } from "node:fs/promises";
39122
+ import { dirname as dirname9, isAbsolute, resolve as resolve6, sep } from "node:path";
38423
39123
  import { createStreamsClient } from "@secondlayer/sdk";
38424
39124
  import { STREAMS_EVENT_TYPES } from "@secondlayer/shared";
38425
39125
  var VALID_TYPES = STREAMS_EVENT_TYPES;
@@ -38441,6 +39141,48 @@ function dumpsClient(dumpsUrl) {
38441
39141
  dumpsBaseUrl
38442
39142
  });
38443
39143
  }
39144
+ function resolveDumpDest(to, filePath) {
39145
+ const root = resolve6(to);
39146
+ const segments = filePath.split(/[\\/]/);
39147
+ if (filePath.length === 0 || isAbsolute(filePath) || filePath.includes("\\") || segments.some((s2) => s2 === "..")) {
39148
+ throw new Error(`refusing dump path "${filePath}": paths must be relative and stay under ${root}`);
39149
+ }
39150
+ const dest = resolve6(root, filePath);
39151
+ if (!dest.startsWith(root + sep)) {
39152
+ throw new Error(`refusing dump path "${filePath}": resolves outside ${root}`);
39153
+ }
39154
+ return dest;
39155
+ }
39156
+ async function downloadDumpTo(file, dest, fetchImpl, url) {
39157
+ const res = await fetchImpl(url);
39158
+ if (!res.ok || !res.body) {
39159
+ throw new Error(`could not download dump ${file.path} (${res.status})`);
39160
+ }
39161
+ await mkdir3(dirname9(dest), { recursive: true });
39162
+ const part = `${dest}.part`;
39163
+ const hash = createHash3("sha256");
39164
+ let bytes = 0;
39165
+ const handle = await open2(part, "w");
39166
+ try {
39167
+ const reader = res.body.getReader();
39168
+ for (;; ) {
39169
+ const { value, done } = await reader.read();
39170
+ if (done)
39171
+ break;
39172
+ hash.update(value);
39173
+ bytes += value.byteLength;
39174
+ await handle.write(value);
39175
+ }
39176
+ } finally {
39177
+ await handle.close();
39178
+ }
39179
+ const digest = hash.digest("hex");
39180
+ if (digest !== file.sha256) {
39181
+ throw new Error(`dump ${file.path} sha256 mismatch (expected ${file.sha256}, got ${digest}); partial file left at ${part}`);
39182
+ }
39183
+ await rename2(part, dest);
39184
+ return bytes;
39185
+ }
38444
39186
  function parseTypes(value, flag = "--types") {
38445
39187
  if (!value)
38446
39188
  return;
@@ -38469,6 +39211,29 @@ function parseLimit(value) {
38469
39211
  }
38470
39212
  return n3;
38471
39213
  }
39214
+ function parseMaxPages(value) {
39215
+ if (value === undefined)
39216
+ return;
39217
+ const n3 = Number.parseInt(value, 10);
39218
+ if (!Number.isFinite(n3) || n3 < 1 || String(n3) !== value.trim()) {
39219
+ throw new Error("--max-pages must be a positive integer");
39220
+ }
39221
+ return n3;
39222
+ }
39223
+ function consumeHandlers(io) {
39224
+ return {
39225
+ onBatch: (events, _envelope, ctx) => {
39226
+ for (const e2 of events)
39227
+ io.stdout(JSON.stringify(e2));
39228
+ if (ctx.cursor)
39229
+ io.stderr(`# next_cursor=${ctx.cursor}`);
39230
+ },
39231
+ onReorg: (reorg, ctx) => {
39232
+ io.stdout(JSON.stringify({ kind: "reorg", ...reorg }));
39233
+ io.stderr(`# reorg at ${reorg.fork_point_height}; rewinding to ${ctx.cursor}`);
39234
+ }
39235
+ };
39236
+ }
38472
39237
  function parseHeight(value, name) {
38473
39238
  if (value === undefined)
38474
39239
  return;
@@ -38528,8 +39293,14 @@ Examples:
38528
39293
  $ secondlayer streams consume --not-types print --sender SP1...,SP2... --batch-size 500 --max-pages 10`).action(async (options3) => {
38529
39294
  try {
38530
39295
  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");
39296
+ const maxPages = parseMaxPages(options3.maxPages);
39297
+ note('# streaming events to stdout (jsonl); reorgs appear inline as {"kind":"reorg"}; next_cursor printed to stderr');
39298
+ const handlers = consumeHandlers({
39299
+ stdout: (line) => process.stdout.write(`${line}
39300
+ `),
39301
+ stderr: (line) => process.stderr.write(`${line}
39302
+ `)
39303
+ });
38533
39304
  await client2().events.consume({
38534
39305
  fromCursor: options3.cursor,
38535
39306
  types: parseTypes(options3.types),
@@ -38540,17 +39311,8 @@ Examples:
38540
39311
  batchSize,
38541
39312
  mode: "tail",
38542
39313
  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
- }
39314
+ onBatch: handlers.onBatch,
39315
+ onReorg: handlers.onReorg
38554
39316
  });
38555
39317
  } catch (err) {
38556
39318
  error(err instanceof Error ? err.message : String(err));
@@ -38582,11 +39344,9 @@ Examples:
38582
39344
  }
38583
39345
  note(`# downloading ${files.length} file(s) to ${options3.to}`);
38584
39346
  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)
39347
+ const dest = resolveDumpDest(options3.to, file.path);
39348
+ const bytes = await downloadDumpTo(file, dest, fetch, dumps.fileUrl(file));
39349
+ process.stderr.write(`# ${file.path} (${file.row_count} rows, ${bytes} bytes)
38590
39350
  `);
38591
39351
  }
38592
39352
  writeData(JSON.stringify({
@@ -39006,7 +39766,7 @@ function markFrozen(cmd) {
39006
39766
  init_config2();
39007
39767
  init_dev_state();
39008
39768
  init_output();
39009
- import { confirm as confirm5 } from "@inquirer/prompts";
39769
+ import { confirm as confirm4 } from "@inquirer/prompts";
39010
39770
  import { getDb, sql } from "@secondlayer/shared/db";
39011
39771
  import {
39012
39772
  countMissingBlocks,
@@ -39226,7 +39986,7 @@ async function resetDatabase(skipConfirm) {
39226
39986
  console.log(dim("Note: Subgraph configurations will be preserved."));
39227
39987
  console.log("");
39228
39988
  if (!skipConfirm) {
39229
- const confirmed = await confirm5({
39989
+ const confirmed = await confirm4({
39230
39990
  message: "Are you sure you want to reset the database?",
39231
39991
  default: false
39232
39992
  });
@@ -39263,7 +40023,7 @@ async function resyncDatabase(skipConfirm, backfill) {
39263
40023
  console.log(dim("Note: Subgraph configurations will be preserved."));
39264
40024
  console.log("");
39265
40025
  if (!skipConfirm) {
39266
- const confirmed = await confirm5({
40026
+ const confirmed = await confirm4({
39267
40027
  message: "Are you sure you want to resync?",
39268
40028
  default: false
39269
40029
  });
@@ -39834,9 +40594,9 @@ init_output();
39834
40594
 
39835
40595
  // src/lib/project-file.ts
39836
40596
  import { existsSync as existsSync7 } from "node:fs";
39837
- import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4 } from "node:fs/promises";
40597
+ import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
39838
40598
  import { homedir as homedir5 } from "node:os";
39839
- import { dirname as dirname15, join as join13, parse as parsePath2, resolve as resolve11 } from "node:path";
40599
+ import { dirname as dirname15, join as join12, parse as parsePath2, resolve as resolve12 } from "node:path";
39840
40600
  import { z as z4 } from "zod";
39841
40601
  var ProjectFileSchema = z4.object({
39842
40602
  slug: z4.string().min(1)
@@ -39844,11 +40604,11 @@ var ProjectFileSchema = z4.object({
39844
40604
  var FILENAME = "project";
39845
40605
  var DIRNAME = ".secondlayer";
39846
40606
  async function readActiveProject(cwd, globalDefault) {
39847
- let dir = resolve11(cwd);
39848
- const home2 = resolve11(homedir5());
40607
+ let dir = resolve12(cwd);
40608
+ const home2 = resolve12(homedir5());
39849
40609
  const fsRoot = parsePath2(dir).root;
39850
40610
  while (true) {
39851
- const candidate = join13(dir, DIRNAME, FILENAME);
40611
+ const candidate = join12(dir, DIRNAME, FILENAME);
39852
40612
  if (existsSync7(candidate)) {
39853
40613
  try {
39854
40614
  const raw = await readFile4(candidate, "utf8");
@@ -39856,7 +40616,7 @@ async function readActiveProject(cwd, globalDefault) {
39856
40616
  return { slug: parsed.slug, resolvedFrom: candidate };
39857
40617
  } catch {}
39858
40618
  }
39859
- if (existsSync7(join13(dir, ".git")))
40619
+ if (existsSync7(join12(dir, ".git")))
39860
40620
  break;
39861
40621
  if (dir === home2 || dir === fsRoot)
39862
40622
  break;
@@ -39868,57 +40628,50 @@ async function readActiveProject(cwd, globalDefault) {
39868
40628
  if (globalDefault) {
39869
40629
  return {
39870
40630
  slug: globalDefault,
39871
- resolvedFrom: join13(homedir5(), ".secondlayer", "config.json")
40631
+ resolvedFrom: join12(homedir5(), ".secondlayer", "config.json")
39872
40632
  };
39873
40633
  }
39874
40634
  return null;
39875
40635
  }
39876
40636
 
39877
40637
  // src/commands/whoami.ts
39878
- init_resolve_auth();
39879
40638
  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
- }
40639
+ program2.command("whoami", { hidden: true }).description("Show the archive credits account, credential source, and project").option("--json", "Output as JSON").action(async (options3) => {
40640
+ const creditsUrl = resolveArchiveOpsUrl();
40641
+ const { source } = await resolveArchiveOpsBearer();
39888
40642
  let account;
39889
40643
  try {
39890
- account = await httpPlatform("/api/accounts/me");
40644
+ account = await httpArchiveOps("/api/accounts/me");
39891
40645
  } catch (err) {
39892
- if (err instanceof CliHttpError && err.code === "SESSION_EXPIRED") {
39893
- error("Session expired. Run: secondlayer login");
40646
+ if (err instanceof CliHttpError) {
40647
+ error(err.message);
39894
40648
  process.exit(1);
39895
40649
  }
39896
40650
  throw err;
39897
40651
  }
39898
40652
  const config = await loadConfig2();
39899
40653
  const active = await readActiveProject(process.cwd(), config.defaultProject);
39900
- const authSource = auth.fromEnv ? "API key (env)" : "session";
40654
+ const authSource = source === "env" ? "API key (env)" : `session (${ARCHIVE_LOGIN_COMMAND})`;
39901
40655
  output({
39902
40656
  json: options3.json,
39903
40657
  data: {
39904
40658
  email: account.email,
39905
- apiUrl: auth.apiUrl,
40659
+ creditsUrl,
40660
+ apiUrl: resolveApiUrl(),
39906
40661
  authSource,
39907
40662
  project: active ? { slug: active.slug, source: active.resolvedFrom } : null
39908
40663
  },
39909
40664
  human: () => {
39910
40665
  const rows = [];
39911
40666
  rows.push(["Email", account.email]);
39912
- rows.push(["API URL", auth.apiUrl]);
40667
+ rows.push(["Credits API", creditsUrl]);
40668
+ rows.push(["Instance API", resolveApiUrl()]);
39913
40669
  rows.push(["Auth", dim(authSource)]);
39914
40670
  if (active) {
39915
40671
  rows.push(["Project", active.slug]);
39916
40672
  rows.push(["Project source", dim(active.resolvedFrom)]);
39917
40673
  } else {
39918
- rows.push([
39919
- "Project",
39920
- dim("(none — run `secondlayer project use <slug>`)")
39921
- ]);
40674
+ rows.push(["Project", dim("(none)")]);
39922
40675
  }
39923
40676
  console.log(formatKeyValue(rows));
39924
40677
  }
@@ -39933,25 +40686,34 @@ init_login();
39933
40686
  init_http();
39934
40687
  init_output();
39935
40688
  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
- }
40689
+ init_login();
40690
+ async function runLogout(opts = {}) {
40691
+ const target = loginTarget(opts);
40692
+ const session = await readSession(target);
40693
+ if (!session) {
40694
+ info(`Not logged in (${target}).`);
40695
+ return;
40696
+ }
40697
+ try {
40698
+ await httpAt(target, "/api/auth/logout", {
40699
+ method: "POST",
40700
+ bearer: session.token
40701
+ });
40702
+ } catch (err) {
40703
+ if (err instanceof CliHttpError) {
40704
+ warn(`Server logout failed (${err.code}); clearing the local session anyway`);
39949
40705
  }
39950
- await clearSession();
39951
- success("Logged out.");
39952
- });
40706
+ }
40707
+ await clearSession(target);
40708
+ success(`Logged out (${target}).`);
40709
+ }
40710
+ function registerLogoutCommand(program2) {
40711
+ 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
40712
  }
39954
40713
  // src/commands/verify.ts
40714
+ init_archive_reference();
40715
+ init_output();
40716
+ init_resolve_auth();
39955
40717
  import {
39956
40718
  compareRangeDigests,
39957
40719
  computeRangeDigest
@@ -39966,139 +40728,6 @@ import {
39966
40728
  reportVerify
39967
40729
  } from "@secondlayer/shared/coverage";
39968
40730
  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
40731
  var VERIFY_EXIT = {
40103
40732
  CLEAN: 0,
40104
40733
  DIVERGED: 1,
@@ -40121,7 +40750,7 @@ function statusLabel(status2) {
40121
40750
  return yellow("missing");
40122
40751
  }
40123
40752
  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", `
40753
+ 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
40754
  Examples:
40126
40755
  $ secondlayer verify --against https://archive.secondlayer.tools/.../snapshots/<digest>.json
40127
40756
  $ secondlayer verify raw --against ./snapshot.json --from-block 8000000 --to-block 8499999
@@ -40141,8 +40770,14 @@ Exit codes:
40141
40770
  if (fromBlock !== undefined && toBlock !== undefined && toBlock < fromBlock) {
40142
40771
  throw new Error("--to-block must be >= --from-block");
40143
40772
  }
40144
- const { manifest, origin } = await loadReference(opts.against);
40145
- const publicKey = await resolvePublicKey(opts.publicKey, process.env.SL_API_URL ?? "https://api.secondlayer.tools");
40773
+ const publicKey = await resolveArchivePublicKey({
40774
+ explicitPem: opts.publicKey,
40775
+ envPem: process.env.ARCHIVE_SIGNING_PUBLIC_KEY ?? process.env.STREAMS_SIGNING_PUBLIC_KEY,
40776
+ allowHostedApi: !isOssMode()
40777
+ });
40778
+ const { manifest, origin } = await loadReference(opts.against, {
40779
+ publicKeyPem: publicKey
40780
+ });
40146
40781
  const signature = checkSignature(manifest, publicKey, !!opts.insecure);
40147
40782
  if (!signature.verified && !opts.insecure) {
40148
40783
  printError(`Cannot trust the reference: ${signature.reason}.`, {
@@ -40297,24 +40932,27 @@ Exit codes:
40297
40932
  });
40298
40933
  process.exit(diverged.length === 0 ? VERIFY_EXIT.CLEAN : VERIFY_EXIT.DIVERGED);
40299
40934
  } catch (err) {
40300
- printError(err instanceof Error ? err.message : String(err), {
40301
- hint: "Set DATABASE_URL to the instance you want to verify."
40302
- });
40935
+ const message = err instanceof Error ? err.message : String(err);
40936
+ 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.";
40937
+ printError(message, { hint });
40303
40938
  process.exit(VERIFY_EXIT.UNANCHORED);
40304
40939
  }
40305
40940
  });
40306
40941
  }
40307
40942
  // src/commands/repair.ts
40308
- import { unlink as unlink2, writeFile as writeFile5 } from "node:fs/promises";
40309
- import { tmpdir as tmpdir2 } from "node:os";
40943
+ import { unlink as unlink3, writeFile as writeFile5 } from "node:fs/promises";
40944
+ import { tmpdir as tmpdir3 } from "node:os";
40310
40945
  import { join as join15 } from "node:path";
40311
- import { ParquetReader } from "@dsnp/parquetjs";
40312
- import { confirm as confirm7 } from "@inquirer/prompts";
40946
+ import { ParquetReader as ParquetReader2 } from "@dsnp/parquetjs";
40947
+ import {
40948
+ copyStatement,
40949
+ writeRowsToCopyStream
40950
+ } from "@secondlayer/shared/archive/copy-loader";
40313
40951
  import {
40314
40952
  compareRangeDigests as compareRangeDigests2,
40315
40953
  computeRangeDigest as computeRangeDigest2
40316
40954
  } from "@secondlayer/shared/archive/range-digest";
40317
- import { getDb as getDb3, sql as sql2 } from "@secondlayer/shared/db";
40955
+ import { getDb as getDb3, getRawClient } from "@secondlayer/shared/db";
40318
40956
 
40319
40957
  // src/lib/archive-gate.ts
40320
40958
  init_http();
@@ -40373,10 +41011,18 @@ function formatQuoteValue(quote, flow) {
40373
41011
  return `${quote.partitions} partitions ≈ $${quote.usd} · balance ${balance}`;
40374
41012
  }
40375
41013
  function shouldPromptForGatedFetch(opts) {
40376
- return !opts.yes && !opts.json;
41014
+ return !opts.yes;
41015
+ }
41016
+ function confirmationRequiredPayload(quote) {
41017
+ return {
41018
+ code: "CONFIRMATION_REQUIRED",
41019
+ message: "Nothing was fetched. Re-run with -y to approve this fetch; --json never stands in for -y.",
41020
+ quote
41021
+ };
40377
41022
  }
40378
41023
  var FETCH_BATCH_SIZE = 16;
40379
- function createGatedFetcher(paths, flow, deps = defaultDeps) {
41024
+ var PRESIGN_REFRESH_MARGIN_MS = 60000;
41025
+ function createGatedFetcher(paths, flow, deps = defaultDeps, now = Date.now) {
40380
41026
  const cache = new Map;
40381
41027
  const batchOf = new Map;
40382
41028
  for (const [index, path11] of paths.entries()) {
@@ -40386,21 +41032,29 @@ function createGatedFetcher(paths, flow, deps = defaultDeps) {
40386
41032
  const inFlight = new Map;
40387
41033
  async function fetchAndCache(batchPaths) {
40388
41034
  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);
41035
+ for (const u3 of body.urls) {
41036
+ const parsed = Date.parse(u3.expires_at);
41037
+ cache.set(u3.path, {
41038
+ url: u3.url,
41039
+ expiresAt: Number.isFinite(parsed) ? parsed : null
41040
+ });
41041
+ }
41042
+ }
41043
+ async function reissue(path11, why) {
41044
+ if (process.env.DEBUG) {
41045
+ console.error(`[archive-gate] re-issuing ${why} URL for ${path11}`);
41046
+ }
41047
+ await fetchAndCache([path11]);
41048
+ const fresh = cache.get(path11);
41049
+ if (!fresh) {
41050
+ throw new Error(`archive gate: no URL returned for ${path11}`);
41051
+ }
41052
+ return fresh.url;
40391
41053
  }
40392
41054
  return {
40393
41055
  async getUrl(path11, opts) {
40394
41056
  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;
41057
+ return reissue(path11, "expired");
40404
41058
  }
40405
41059
  const batchIndex = batchOf.get(path11);
40406
41060
  if (batchIndex === undefined) {
@@ -40417,16 +41071,47 @@ function createGatedFetcher(paths, flow, deps = defaultDeps) {
40417
41071
  }
40418
41072
  await inflight;
40419
41073
  }
40420
- const url = cache.get(path11);
40421
- if (!url)
41074
+ const cached = cache.get(path11);
41075
+ if (!cached) {
40422
41076
  throw new Error(`archive gate: no URL returned for ${path11}`);
40423
- return url;
41077
+ }
41078
+ if (cached.expiresAt !== null && cached.expiresAt - now() < PRESIGN_REFRESH_MARGIN_MS) {
41079
+ return reissue(path11, "expiring");
41080
+ }
41081
+ return cached.url;
40424
41082
  }
40425
41083
  };
40426
41084
  }
40427
41085
 
40428
41086
  // src/commands/repair.ts
41087
+ init_archive_reference();
40429
41088
  init_output();
41089
+
41090
+ // src/lib/parquet-rows.ts
41091
+ import { unlink as unlink2, writeFile as writeFile4 } from "node:fs/promises";
41092
+ import { tmpdir as tmpdir2 } from "node:os";
41093
+ import { join as join14 } from "node:path";
41094
+ import { ParquetReader } from "@dsnp/parquetjs";
41095
+ async function* readPartitionRows(bytes, label) {
41096
+ const path11 = join14(tmpdir2(), `sl-archive-${label}-${process.pid}.parquet`);
41097
+ await writeFile4(path11, bytes);
41098
+ try {
41099
+ const reader = await ParquetReader.openFile(path11);
41100
+ try {
41101
+ const cursor = reader.getCursor();
41102
+ for (let row = await cursor.next();row; row = await cursor.next()) {
41103
+ yield row;
41104
+ }
41105
+ } finally {
41106
+ await reader.close();
41107
+ }
41108
+ } finally {
41109
+ await unlink2(path11).catch(() => {});
41110
+ }
41111
+ }
41112
+
41113
+ // src/commands/repair.ts
41114
+ init_resolve_auth();
40430
41115
  var REPAIR_EXIT = {
40431
41116
  OK: 0,
40432
41117
  DIVERGENCE_REMAINS: 1,
@@ -40442,10 +41127,10 @@ function asOptionalText(value) {
40442
41127
  return value === null || value === undefined ? null : asText(value);
40443
41128
  }
40444
41129
  async function readBlocksPartition(bytes, label) {
40445
- const path11 = join15(tmpdir2(), `sl-repair-${label}-${process.pid}.parquet`);
41130
+ const path11 = join15(tmpdir3(), `sl-repair-${label}-${process.pid}.parquet`);
40446
41131
  await writeFile5(path11, bytes);
40447
41132
  try {
40448
- const reader = await ParquetReader.openFile(path11);
41133
+ const reader = await ParquetReader2.openFile(path11);
40449
41134
  const rows = [];
40450
41135
  try {
40451
41136
  const cursor = reader.getCursor();
@@ -40465,7 +41150,7 @@ async function readBlocksPartition(bytes, label) {
40465
41150
  }
40466
41151
  return rows;
40467
41152
  } finally {
40468
- await unlink2(path11).catch(() => {});
41153
+ await unlink3(path11).catch(() => {});
40469
41154
  }
40470
41155
  }
40471
41156
  function parseHeight3(value, flag) {
@@ -40503,24 +41188,92 @@ async function planRange(db, reference, partition, gate) {
40503
41188
  const extraHeights = [...localByHeight.keys()].sort((a4, b3) => a4 - b3);
40504
41189
  return { fixes, extraHeights };
40505
41190
  }
40506
- async function applyFixes(db, reference, partition, fixes, gate) {
41191
+ var CHILD_DATASETS = ["transactions", "events"];
41192
+ var REPAIR_DATASETS = [
41193
+ "blocks",
41194
+ "transactions",
41195
+ "events"
41196
+ ];
41197
+ function childPartitionFor(partitions, dataset, height) {
41198
+ return partitions.find((p2) => p2.dataset === dataset && p2.from_block <= height && p2.to_block >= height);
41199
+ }
41200
+ function planChildRewrite(partitions, heights) {
41201
+ const missing = {
41202
+ transactions: [],
41203
+ events: []
41204
+ };
41205
+ const found = new Map;
41206
+ for (const dataset of CHILD_DATASETS) {
41207
+ for (const height of heights) {
41208
+ const partition = childPartitionFor(partitions, dataset, height);
41209
+ if (!partition) {
41210
+ missing[dataset].push(height);
41211
+ continue;
41212
+ }
41213
+ found.set(height, { ...found.get(height), [dataset]: partition });
41214
+ }
41215
+ }
41216
+ const rewritable = heights.filter((h3) => {
41217
+ const entry = found.get(h3);
41218
+ return !!entry?.transactions && !!entry?.events;
41219
+ });
41220
+ const byPartition = new Map;
41221
+ for (const height of rewritable) {
41222
+ for (const dataset of CHILD_DATASETS) {
41223
+ const partition = found.get(height)?.[dataset];
41224
+ if (!partition)
41225
+ continue;
41226
+ const entry = byPartition.get(partition.path) ?? {
41227
+ partition,
41228
+ heights: []
41229
+ };
41230
+ entry.heights.push(height);
41231
+ byPartition.set(partition.path, entry);
41232
+ }
41233
+ }
41234
+ return { byPartition, missing, rewritable };
41235
+ }
41236
+ async function* onlyHeights(rows, heights) {
41237
+ for await (const row of rows) {
41238
+ if (heights.has(asNumber(row.block_height)))
41239
+ yield row;
41240
+ }
41241
+ }
41242
+ async function applyFixes(rawClient, reference, partition, fixes, gate) {
41243
+ const applied = {
41244
+ blocks: 0,
41245
+ transactions: 0,
41246
+ events: 0,
41247
+ missing: { transactions: [], events: [] },
41248
+ rewritten: []
41249
+ };
40507
41250
  if (fixes.length === 0)
40508
- return 0;
41251
+ return applied;
40509
41252
  const bytes = await fetchVerifiedPartition(reference, partition, gate);
40510
41253
  const archiveBlocks = await readBlocksPartition(bytes, `apply-${partition.from_block}`);
40511
41254
  const byHeight = new Map(archiveBlocks.map((b3) => [b3.height, b3]));
40512
41255
  const targets = fixes.map((fix) => byHeight.get(fix.height)).filter((b3) => b3 !== undefined);
40513
- await db.transaction().execute(async (tx) => {
41256
+ const heights = targets.map((b3) => b3.height);
41257
+ const children = planChildRewrite(reference.manifest.partitions ?? [], heights);
41258
+ applied.missing = children.missing;
41259
+ applied.rewritten = children.rewritable;
41260
+ const rewritable = new Set(children.rewritable);
41261
+ const childBytes = new Map;
41262
+ for (const [path11, { partition: child }] of children.byPartition) {
41263
+ childBytes.set(path11, await fetchVerifiedPartition(reference, child, gate));
41264
+ }
41265
+ await rawClient.begin(async (tx) => {
41266
+ for (const height of rewritable) {
41267
+ await tx.unsafe("DELETE FROM events WHERE block_height = $1", [height]);
41268
+ await tx.unsafe("DELETE FROM transactions WHERE block_height = $1", [
41269
+ height
41270
+ ]);
41271
+ }
40514
41272
  for (const block of targets) {
40515
- await sql2`
40516
- INSERT INTO blocks (
41273
+ await tx.unsafe(`INSERT INTO blocks (
40517
41274
  height, hash, parent_hash, burn_block_height,
40518
41275
  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
- )
41276
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, true)
40524
41277
  ON CONFLICT (height) DO UPDATE SET
40525
41278
  hash = EXCLUDED.hash,
40526
41279
  parent_hash = EXCLUDED.parent_hash,
@@ -40528,19 +41281,64 @@ async function applyFixes(db, reference, partition, fixes, gate) {
40528
41281
  burn_block_hash = EXCLUDED.burn_block_hash,
40529
41282
  index_block_hash = EXCLUDED.index_block_hash,
40530
41283
  timestamp = EXCLUDED.timestamp,
40531
- canonical = true
40532
- `.execute(tx);
41284
+ canonical = true`, [
41285
+ block.height,
41286
+ block.hash,
41287
+ block.parent_hash,
41288
+ block.burn_block_height,
41289
+ block.burn_block_hash,
41290
+ block.index_block_hash,
41291
+ block.timestamp
41292
+ ]);
41293
+ }
41294
+ applied.blocks = targets.length;
41295
+ for (const dataset of CHILD_DATASETS) {
41296
+ for (const [path11, { partition: child }] of children.byPartition) {
41297
+ if (child.dataset !== dataset)
41298
+ continue;
41299
+ const bytes2 = childBytes.get(path11);
41300
+ if (!bytes2)
41301
+ continue;
41302
+ const writable2 = await tx.unsafe(copyStatement(dataset)).writable();
41303
+ applied[dataset] += await writeRowsToCopyStream({
41304
+ writable: writable2,
41305
+ dataset,
41306
+ rows: onlyHeights(readPartitionRows(bytes2, `repair-${dataset}-${child.from_block}`), rewritable)
41307
+ });
41308
+ }
40533
41309
  }
40534
41310
  });
40535
- return targets.length;
41311
+ return applied;
41312
+ }
41313
+ async function reverify(db, reference, ranges) {
41314
+ const remaining = {
41315
+ blocks: 0,
41316
+ transactions: 0,
41317
+ events: 0
41318
+ };
41319
+ const published = reference.manifest.range_digests ?? [];
41320
+ for (const dataset of REPAIR_DATASETS) {
41321
+ const expected = published.filter((d2) => d2.dataset === dataset && ranges.some((r2) => r2.from_block === d2.from_block && r2.to_block === d2.to_block));
41322
+ const actual = [];
41323
+ for (const range of expected) {
41324
+ actual.push(await computeRangeDigest2(db, dataset, range.from_block, range.to_block));
41325
+ }
41326
+ remaining[dataset] = compareRangeDigests2(actual, expected).filter((c4) => c4.status !== "match").length;
41327
+ }
41328
+ return remaining;
40536
41329
  }
40537
41330
  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", `
41331
+ 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
41332
  Examples:
40540
41333
  $ secondlayer repair --against ./snapshot.json # plan only
40541
41334
  $ secondlayer repair --against ./snapshot.json --apply # write the fix
40542
41335
  $ secondlayer repair --against ./snapshot.json --from-block 8500000 --to-block 8549999
40543
41336
 
41337
+ A fixed block is rewritten together with its transactions and events from the
41338
+ archive's partitions for that height. When the reference carries no
41339
+ transactions or events partition for a height, the block is rewritten alone,
41340
+ the run names the height, and it exits 1.
41341
+
40544
41342
  Exit codes:
40545
41343
  0 nothing to repair, or the repair completed and re-verified clean
40546
41344
  1 divergence remains (dry-run, or repair did not fully resolve it)
@@ -40548,8 +41346,14 @@ Exit codes:
40548
41346
  try {
40549
41347
  const fromBlock = opts.fromBlock === undefined ? undefined : parseHeight3(opts.fromBlock, "--from-block");
40550
41348
  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");
41349
+ const publicKey = await resolveArchivePublicKey({
41350
+ explicitPem: opts.publicKey,
41351
+ envPem: process.env.ARCHIVE_SIGNING_PUBLIC_KEY ?? process.env.STREAMS_SIGNING_PUBLIC_KEY,
41352
+ allowHostedApi: !isOssMode()
41353
+ });
41354
+ const reference = await loadReference(opts.against, {
41355
+ publicKeyPem: publicKey
41356
+ });
40553
41357
  const signature = checkSignature(reference.manifest, publicKey, false);
40554
41358
  if (!signature.verified) {
40555
41359
  printError(`Refusing to repair from an untrusted reference: ${signature.reason}.`, {
@@ -40581,9 +41385,12 @@ Exit codes:
40581
41385
  process.exit(REPAIR_EXIT.OK);
40582
41386
  }
40583
41387
  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));
41388
+ 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)) : [];
41389
+ const fetchPaths = [...partitions, ...childPartitions].map((p2) => p2.path);
40584
41390
  let gate;
41391
+ let quoteLine;
40585
41392
  if (isOfficialArchive(reference)) {
40586
- const paths = partitions.map((p2) => p2.path);
41393
+ const paths = fetchPaths;
40587
41394
  const result = await quoteArchiveFetch(paths, "repair");
40588
41395
  if (!result.ok) {
40589
41396
  printError(result.kind === "not_configured" ? ARCHIVE_GATE_NOT_CONFIGURED_MESSAGE : result.message);
@@ -40593,11 +41400,16 @@ Exit codes:
40593
41400
  printError(formatInsufficientMessage(result.quote));
40594
41401
  process.exit(REPAIR_EXIT.UNANCHORED);
40595
41402
  }
40596
- note(` metered: ${formatQuoteValue(result.quote, "repair")}`);
41403
+ quoteLine = formatQuoteValue(result.quote, "repair");
41404
+ note(` metered: ${quoteLine}`);
40597
41405
  if (shouldPromptForGatedFetch(opts)) {
40598
- const proceed = await confirm7({
40599
- message: `Fetch ${partitions.length} partition(s) from the archive?`,
40600
- default: true
41406
+ if (opts.json) {
41407
+ writeData(JSON.stringify(confirmationRequiredPayload(result.quote)));
41408
+ process.exit(REPAIR_EXIT.UNANCHORED);
41409
+ }
41410
+ const proceed = await confirmDestructive({
41411
+ message: `Fetch ${fetchPaths.length} partition(s) from the archive?`,
41412
+ yes: opts.yes
40601
41413
  });
40602
41414
  if (!proceed) {
40603
41415
  note("Nothing was fetched.");
@@ -40616,30 +41428,64 @@ Exit codes:
40616
41428
  allFixes.push(...fixes);
40617
41429
  allExtra.push(...extraHeights);
40618
41430
  }
40619
- let applied = 0;
41431
+ const applied = { blocks: 0, transactions: 0, events: 0 };
41432
+ const missingChild = {
41433
+ transactions: [],
41434
+ events: []
41435
+ };
41436
+ let childrenRewritten = 0;
40620
41437
  if (opts.apply) {
41438
+ const rawClient = getRawClient("source");
40621
41439
  for (const { partition, fixes } of planned) {
40622
- applied += await applyFixes(db, reference, partition, fixes, gate);
40623
- }
40624
- }
40625
- let remaining = divergent.length;
41440
+ const result = await applyFixes(rawClient, reference, partition, fixes, gate);
41441
+ applied.blocks += result.blocks;
41442
+ applied.transactions += result.transactions;
41443
+ applied.events += result.events;
41444
+ missingChild.transactions.push(...result.missing.transactions);
41445
+ missingChild.events.push(...result.missing.events);
41446
+ childrenRewritten += result.rewritten.length;
41447
+ }
41448
+ }
41449
+ const missingHeights = [
41450
+ ...new Set([...missingChild.transactions, ...missingChild.events])
41451
+ ].sort((a4, b3) => a4 - b3);
41452
+ const rewritten = opts.apply ? REPAIR_DATASETS.filter((d2) => d2 === "blocks" || childrenRewritten > 0) : [];
41453
+ let remainingByDataset = {
41454
+ blocks: divergent.length,
41455
+ transactions: 0,
41456
+ events: 0
41457
+ };
40626
41458
  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;
41459
+ remainingByDataset = await reverify(db, reference, referenceDigests);
40632
41460
  }
41461
+ const remaining = remainingByDataset.blocks + remainingByDataset.transactions + remainingByDataset.events;
41462
+ const complete = !!opts.apply && remaining === 0 && missingHeights.length === 0;
40633
41463
  const report = {
40634
- status: opts.apply ? remaining === 0 ? "repaired" : "incomplete" : "plan",
41464
+ status: opts.apply ? complete ? "repaired" : "incomplete" : "plan",
40635
41465
  reference: reference.origin,
40636
41466
  divergent_ranges: divergent.length,
40637
41467
  blocks_to_replace: allFixes.filter((f2) => f2.kind === "replace").length,
40638
41468
  blocks_to_insert: allFixes.filter((f2) => f2.kind === "insert").length,
40639
41469
  local_only_heights: allExtra,
40640
- applied,
40641
- fixes: allFixes
41470
+ applied: applied.blocks,
41471
+ rows_written: applied,
41472
+ datasets_rewritten: rewritten,
41473
+ heights_missing_child_partitions: missingHeights,
41474
+ remaining_by_dataset: remainingByDataset,
41475
+ fixes: allFixes,
41476
+ metered: quoteLine ?? null
40642
41477
  };
41478
+ if (missingHeights.length > 0) {
41479
+ for (const dataset of CHILD_DATASETS) {
41480
+ const heights = missingChild[dataset];
41481
+ if (heights.length === 0)
41482
+ continue;
41483
+ warn(` the reference has no ${dataset} partition for ${heights.length} height(s): ${heights.slice(0, 5).join(", ")}${heights.length > 5 ? ", …" : ""}`);
41484
+ }
41485
+ for (const height of missingHeights.slice(0, 5)) {
41486
+ console.error(dim(` transactions/events at ${height}: run \`secondlayer bootstrap --from-block ${height} --to-block ${height}\``));
41487
+ }
41488
+ }
40643
41489
  output({
40644
41490
  json: opts.json,
40645
41491
  data: report,
@@ -40647,10 +41493,14 @@ Exit codes:
40647
41493
  const replace = report.blocks_to_replace;
40648
41494
  const insert = report.blocks_to_insert;
40649
41495
  if (opts.apply) {
40650
- if (remaining === 0) {
40651
- success(`Repaired ${applied} blocks across ${divergent.length} ranges; re-verified clean.`);
41496
+ const written = `${applied.blocks} blocks, ${applied.transactions} transactions, ${applied.events} events`;
41497
+ if (complete) {
41498
+ success(`Repaired ${applied.blocks} heights across ${divergent.length} ranges (rewrote ${written}); re-verified clean on ${rewritten.join(", ")}.`);
41499
+ } else if (remaining > 0) {
41500
+ const byDataset = REPAIR_DATASETS.map((d2) => `${remainingByDataset[d2]} ${d2}`).join(", ");
41501
+ warn(`Rewrote ${written} but ${remaining} ranges still diverge (${byDataset}).`);
40652
41502
  } else {
40653
- warn(`Applied ${applied} blocks but ${remaining} ranges still diverge.`);
41503
+ warn(`Rewrote ${written}; blocks re-verified clean, but transactions and events at ${missingHeights.length} height(s) were not rewritten.`);
40654
41504
  }
40655
41505
  } else {
40656
41506
  warn(`${divergent.length} ranges diverge — ${replace} blocks to replace, ${insert} to insert.`);
@@ -40681,31 +41531,39 @@ Exit codes:
40681
41531
  }
40682
41532
  }
40683
41533
  });
40684
- process.exit(opts.apply && remaining === 0 ? REPAIR_EXIT.OK : REPAIR_EXIT.DIVERGENCE_REMAINS);
41534
+ process.exit(complete ? REPAIR_EXIT.OK : REPAIR_EXIT.DIVERGENCE_REMAINS);
40685
41535
  } catch (err) {
40686
41536
  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.";
41537
+ if (err instanceof ArchiveFetchError && err.transient) {
41538
+ printError(message, {
41539
+ hint: "The archive could not be reached. Re-run the same command to resume; heights already repaired are kept."
41540
+ });
41541
+ process.exit(REPAIR_EXIT.DIVERGENCE_REMAINS);
41542
+ }
41543
+ 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
41544
  printError(message, { hint });
40689
41545
  process.exit(REPAIR_EXIT.UNANCHORED);
40690
41546
  }
40691
41547
  });
40692
41548
  }
40693
41549
  // 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";
41550
+ import { createHash as createHash5 } from "node:crypto";
40700
41551
  import {
40701
- copyStatement,
40702
- writeRowsToCopyStream
41552
+ copyStatement as copyStatement2,
41553
+ writeRowsToCopyStream as writeRowsToCopyStream2
40703
41554
  } 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";
41555
+ import {
41556
+ computeRangeDigest as computeRangeDigest3
41557
+ } from "@secondlayer/shared/archive/range-digest";
41558
+ import { getDb as getDb4, getRawClient as getRawClient2, sql as sql2 } from "@secondlayer/shared/db";
40706
41559
  import { upsertSyncScope } from "@secondlayer/shared/db/queries/sync-scope";
41560
+ init_archive_reference();
40707
41561
 
40708
41562
  // src/lib/bootstrap-resume.ts
41563
+ var RESUME_DATASETS = ["blocks", "transactions", "events"];
41564
+ function isResumeDataset(value) {
41565
+ return RESUME_DATASETS.includes(value);
41566
+ }
40709
41567
  function planTornImport(input4) {
40710
41568
  if (input4.hasIndexProgress) {
40711
41569
  return {
@@ -40713,21 +41571,50 @@ function planTornImport(input4) {
40713
41571
  reason: "This database already holds a completed bootstrap."
40714
41572
  };
40715
41573
  }
40716
- if (input4.maxBlockHeight === null || input4.maxBlockHeight <= 0) {
41574
+ if (RESUME_DATASETS.every((d2) => input4.highWater[d2] === null)) {
40717
41575
  return { action: "fresh" };
40718
41576
  }
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
41577
+ const truncateFrom = {
41578
+ blocks: null,
41579
+ transactions: null,
41580
+ events: null
41581
+ };
41582
+ const skipThrough = {
41583
+ blocks: 0,
41584
+ transactions: 0,
41585
+ events: 0
41586
+ };
41587
+ for (const dataset of RESUME_DATASETS) {
41588
+ const mark = input4.highWater[dataset];
41589
+ if (mark === null)
41590
+ continue;
41591
+ const ranges = input4.partitions.filter((p2) => p2.dataset === dataset).sort((a4, b3) => a4.from_block - b3.from_block);
41592
+ const sealed = ranges.filter((p2) => p2.to_block <= mark);
41593
+ skipThrough[dataset] = sealed.length > 0 ? Math.max(...sealed.map((p2) => p2.to_block)) : 0;
41594
+ const torn = ranges.find((p2) => p2.from_block <= mark && p2.to_block > mark);
41595
+ truncateFrom[dataset] = torn ? torn.from_block : null;
41596
+ }
41597
+ const minDefined = (...values2) => {
41598
+ const defined = values2.filter((v2) => v2 !== null);
41599
+ return defined.length === 0 ? null : Math.min(...defined);
41600
+ };
41601
+ truncateFrom.transactions = minDefined(truncateFrom.blocks, truncateFrom.transactions);
41602
+ truncateFrom.events = minDefined(truncateFrom.transactions, truncateFrom.events);
41603
+ const capTo = (dataset, parentSealed) => {
41604
+ if (skipThrough[dataset] <= parentSealed)
41605
+ return;
41606
+ skipThrough[dataset] = parentSealed;
41607
+ truncateFrom[dataset] = minDefined(truncateFrom[dataset], parentSealed + 1);
40727
41608
  };
41609
+ capTo("transactions", skipThrough.blocks);
41610
+ capTo("events", skipThrough.transactions);
41611
+ truncateFrom.events = minDefined(truncateFrom.transactions, truncateFrom.events);
41612
+ return { action: "resume", truncateFrom, skipThrough };
40728
41613
  }
40729
41614
  function partitionIsLoaded(partition, skipThrough) {
40730
- return partition.to_block <= skipThrough;
41615
+ if (!isResumeDataset(partition.dataset))
41616
+ return false;
41617
+ return partition.to_block <= skipThrough[partition.dataset];
40731
41618
  }
40732
41619
 
40733
41620
  // src/commands/bootstrap.ts
@@ -40750,7 +41637,7 @@ function archiveScopeBounds(partitions) {
40750
41637
  }
40751
41638
  function manifestDigest(manifest) {
40752
41639
  const { signature: _signature, key_id: _keyId, ...payload } = manifest;
40753
- return createHash3("sha256").update(JSON.stringify(payload)).digest("hex");
41640
+ return createHash5("sha256").update(JSON.stringify(payload)).digest("hex");
40754
41641
  }
40755
41642
  async function readNodeTip() {
40756
41643
  const url = process.env.STACKS_NODE_RPC_URL ?? "http://localhost:20443";
@@ -40775,27 +41662,10 @@ function formatDuration2(seconds2) {
40775
41662
  return `${(seconds2 / 3600).toFixed(1)} hours`;
40776
41663
  return `${(seconds2 / 86400).toFixed(1)} days`;
40777
41664
  }
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
41665
  async function loadPartition(reference, partition, rawClient, gate) {
40796
41666
  const bytes = await fetchVerifiedPartition(reference, partition, gate);
40797
- const writable2 = await rawClient.unsafe(copyStatement(partition.dataset)).writable();
40798
- const written = await writeRowsToCopyStream({
41667
+ const writable2 = await rawClient.unsafe(copyStatement2(partition.dataset)).writable();
41668
+ const written = await writeRowsToCopyStream2({
40799
41669
  writable: writable2,
40800
41670
  dataset: partition.dataset,
40801
41671
  rows: readPartitionRows(bytes, `${partition.dataset}-${partition.from_block}`)
@@ -40805,8 +41675,44 @@ async function loadPartition(reference, partition, rawClient, gate) {
40805
41675
  }
40806
41676
  return written;
40807
41677
  }
41678
+ var LOAD_ORDER = [
41679
+ "blocks",
41680
+ "transactions",
41681
+ "events"
41682
+ ];
41683
+ function loadOrder(partitions) {
41684
+ return LOAD_ORDER.flatMap((dataset) => partitions.filter((p2) => p2.dataset === dataset).sort((a4, b3) => a4.from_block - b3.from_block));
41685
+ }
41686
+ function parseVerifyDatasets(value) {
41687
+ if (value === undefined || value === "all") {
41688
+ return ["blocks", "transactions", "events"];
41689
+ }
41690
+ if (value === "blocks")
41691
+ return ["blocks"];
41692
+ throw new Error(`--verify must be "all" or "blocks", got "${String(value)}"`);
41693
+ }
41694
+ async function readHighWater(db) {
41695
+ const asMark = (value) => value === null || value === undefined ? null : Number(value);
41696
+ const blocks = await db.selectFrom("blocks").select(({ fn }) => fn.max("height").as("max")).executeTakeFirst();
41697
+ const transactions = await db.selectFrom("transactions").select(({ fn }) => fn.max("block_height").as("max")).executeTakeFirst();
41698
+ const events = await db.selectFrom("events").select(({ fn }) => fn.max("block_height").as("max")).executeTakeFirst();
41699
+ return {
41700
+ blocks: asMark(blocks?.max),
41701
+ transactions: asMark(transactions?.max),
41702
+ events: asMark(events?.max)
41703
+ };
41704
+ }
41705
+ async function truncateDatasetFrom(db, dataset, from) {
41706
+ if (dataset === "blocks") {
41707
+ await sql2`DELETE FROM blocks WHERE height >= ${from}`.execute(db);
41708
+ } else if (dataset === "transactions") {
41709
+ await sql2`DELETE FROM transactions WHERE block_height >= ${from}`.execute(db);
41710
+ } else {
41711
+ await sql2`DELETE FROM events WHERE block_height >= ${from}`.execute(db);
41712
+ }
41713
+ }
40808
41714
  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", `
41715
+ 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
41716
  Examples:
40811
41717
  $ secondlayer bootstrap --against https://archive.secondlayer.tools/.../snapshots/<digest>.json
40812
41718
  $ secondlayer bootstrap --against ./snapshot.json --to-block 4000000 --yes
@@ -40817,12 +41723,15 @@ Exit codes:
40817
41723
  1 restore completed but verification found divergence
40818
41724
  2 refused (non-empty target, or an untrusted reference)`).action(async (opts) => {
40819
41725
  try {
40820
- const reference = await loadReference(opts.against);
41726
+ const verifyDatasets = parseVerifyDatasets(opts.verify);
40821
41727
  const publicKey = await resolveArchivePublicKey({
40822
41728
  explicitPem: opts.publicKey,
40823
41729
  envPem: process.env.ARCHIVE_SIGNING_PUBLIC_KEY ?? process.env.STREAMS_SIGNING_PUBLIC_KEY,
40824
41730
  allowHostedApi: !isOssMode()
40825
41731
  });
41732
+ const reference = await loadReference(opts.against, {
41733
+ publicKeyPem: publicKey
41734
+ });
40826
41735
  const signature = checkSignature(reference.manifest, publicKey, false);
40827
41736
  if (!signature.verified) {
40828
41737
  printError(`Refusing to bootstrap from an untrusted archive: ${signature.reason}.`, {
@@ -40832,14 +41741,19 @@ Exit codes:
40832
41741
  }
40833
41742
  const db = getDb4();
40834
41743
  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);
41744
+ const highWater = await readHighWater(db);
40837
41745
  const toBlock = opts.toBlock === undefined ? undefined : Number(opts.toBlock);
40838
41746
  const fromBlock = opts.fromBlock === undefined ? undefined : Number(opts.fromBlock);
40839
41747
  const declared = (reference.manifest.partitions ?? []).filter((p2) => (toBlock === undefined || p2.to_block <= toBlock) && (fromBlock === undefined || p2.from_block >= fromBlock));
41748
+ if (declared.length === 0) {
41749
+ printError("The archive has no partitions for that range.", {
41750
+ hint: "Check --from-block and --to-block against the manifest's coverage."
41751
+ });
41752
+ process.exit(BOOTSTRAP_EXIT.REFUSED);
41753
+ }
40840
41754
  const resume = planTornImport({
40841
41755
  hasIndexProgress: !!progress,
40842
- maxBlockHeight,
41756
+ highWater,
40843
41757
  partitions: declared
40844
41758
  });
40845
41759
  if (resume.action === "refuse") {
@@ -40848,33 +41762,38 @@ Exit codes:
40848
41762
  });
40849
41763
  process.exit(BOOTSTRAP_EXIT.REFUSED);
40850
41764
  }
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()}.`);
41765
+ if (resume.action === "resume") {
41766
+ const truncated = [];
41767
+ for (const dataset of [...RESUME_DATASETS].reverse()) {
41768
+ const from = resume.truncateFrom[dataset];
41769
+ if (from === null)
41770
+ continue;
41771
+ await truncateDatasetFrom(db, dataset, from);
41772
+ truncated.push(`${dataset} from ${from.toLocaleString()}`);
41773
+ }
41774
+ if (truncated.length > 0) {
41775
+ note(`Resuming torn import: truncated ${truncated.join(", ")}.`);
41776
+ }
41777
+ const sealed = RESUME_DATASETS.filter((d2) => resume.skipThrough[d2] > 0).map((d2) => `${d2} through ${resume.skipThrough[d2].toLocaleString()}`);
41778
+ 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
41779
  }
40859
41780
  const partitions = declared.filter((p2) => resume.action === "fresh" || !partitionIsLoaded(p2, resume.skipThrough));
40860
41781
  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);
41782
+ note("All partitions are already loaded; verifying and finalizing the import.");
40865
41783
  }
40866
41784
  const scopeBounds = archiveScopeBounds(declared);
40867
41785
  const startHeight = scopeBounds?.start_height ?? 0;
40868
41786
  const totalRows = partitions.reduce((sum, p2) => sum + p2.row_count, 0);
40869
41787
  const totalBytes = partitions.reduce((sum, p2) => sum + p2.byte_size, 0);
40870
- const tipHeight = Math.max(...partitions.map((p2) => p2.to_block));
41788
+ const tipHeight = scopeBounds?.tip_height ?? 0;
40871
41789
  const restoreSeconds = totalRows / RESTORE_ROWS_PER_SECOND;
40872
41790
  const genesisSeconds = tipHeight / BLOCKS_PER_DAY_ESTIMATE * 86400;
40873
41791
  const gated = isOfficialArchive(reference);
40874
41792
  let quoteLine;
41793
+ let quote = null;
40875
41794
  let gate;
40876
- if (gated) {
40877
- const paths = partitions.map((p2) => p2.path);
41795
+ if (gated && partitions.length > 0) {
41796
+ const paths = loadOrder(partitions).map((p2) => p2.path);
40878
41797
  const result = await quoteArchiveFetch(paths, "bootstrap");
40879
41798
  if (!result.ok) {
40880
41799
  printError(result.kind === "not_configured" ? ARCHIVE_GATE_NOT_CONFIGURED_MESSAGE : result.message);
@@ -40884,6 +41803,7 @@ Exit codes:
40884
41803
  printError(formatInsufficientMessage(result.quote));
40885
41804
  process.exit(BOOTSTRAP_EXIT.REFUSED);
40886
41805
  }
41806
+ quote = result.quote;
40887
41807
  quoteLine = formatQuoteValue(result.quote, "bootstrap");
40888
41808
  gate = createGatedFetcher(paths, "bootstrap");
40889
41809
  }
@@ -40905,10 +41825,16 @@ Exit codes:
40905
41825
  console.error(` restore from archive ${bold(`~${formatDuration2(restoreSeconds)}`)}`);
40906
41826
  console.error("");
40907
41827
  }
40908
- if (shouldPromptForGatedFetch(opts)) {
40909
- const proceed = await confirm8({
41828
+ if (partitions.length > 0 && shouldPromptForGatedFetch(opts)) {
41829
+ if (opts.json) {
41830
+ if (quoteLine)
41831
+ note(` metered: ${quoteLine}`);
41832
+ writeData(JSON.stringify(confirmationRequiredPayload(quote)));
41833
+ process.exit(BOOTSTRAP_EXIT.REFUSED);
41834
+ }
41835
+ const proceed = await confirmDestructive({
40910
41836
  message: "Restore from the verified archive?",
40911
- default: true
41837
+ yes: opts.yes
40912
41838
  });
40913
41839
  if (!proceed) {
40914
41840
  note("Nothing was written.");
@@ -40916,12 +41842,11 @@ Exit codes:
40916
41842
  }
40917
41843
  }
40918
41844
  const nodeTipAtStart = await readNodeTip();
40919
- const order = ["blocks", "transactions", "events"];
40920
- const rawClient = getRawClient("source");
41845
+ const rawClient = getRawClient2("source");
40921
41846
  const loaded = { blocks: 0, transactions: 0, events: 0 };
40922
41847
  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);
41848
+ for (const dataset of LOAD_ORDER) {
41849
+ const datasetPartitions = loadOrder(partitions).filter((p2) => p2.dataset === dataset);
40925
41850
  for (const [index, partition] of datasetPartitions.entries()) {
40926
41851
  loaded[dataset] += await loadPartition(reference, partition, rawClient, gate);
40927
41852
  const done = index + 1;
@@ -40931,16 +41856,21 @@ Exit codes:
40931
41856
  }
40932
41857
  }
40933
41858
  }
40934
- const referenceDigests = (reference.manifest.range_digests ?? []).filter((d2) => d2.dataset === "blocks" && d2.to_block <= tipHeight);
40935
- let divergent = 0;
41859
+ const referenceDigests = (reference.manifest.range_digests ?? []).filter((d2) => verifyDatasets.includes(d2.dataset) && d2.from_block >= startHeight && d2.to_block <= tipHeight);
41860
+ const divergentRanges = [];
40936
41861
  for (const range of referenceDigests) {
40937
- const actual = await computeRangeDigest3(db, "blocks", range.from_block, range.to_block);
41862
+ const actual = await computeRangeDigest3(db, range.dataset, range.from_block, range.to_block);
40938
41863
  if (actual.digest !== range.digest || actual.row_count !== range.row_count) {
40939
- divergent++;
41864
+ divergentRanges.push({
41865
+ dataset: range.dataset,
41866
+ from_block: range.from_block,
41867
+ to_block: range.to_block
41868
+ });
40940
41869
  }
40941
41870
  }
41871
+ const divergent = divergentRanges.length;
40942
41872
  const network = process.env.STACKS_NETWORK ?? "mainnet";
40943
- await sql3`
41873
+ await sql2`
40944
41874
  INSERT INTO index_progress (
40945
41875
  network, last_indexed_block, last_contiguous_block,
40946
41876
  highest_seen_block, updated_at
@@ -40975,8 +41905,10 @@ Exit codes:
40975
41905
  start_height: startHeight,
40976
41906
  tip_height: tipHeight,
40977
41907
  rows: loaded,
41908
+ verified_datasets: verifyDatasets,
40978
41909
  ranges_verified: referenceDigests.length,
40979
41910
  divergent_ranges: divergent,
41911
+ divergent: divergentRanges,
40980
41912
  elapsed_seconds: Math.round(elapsed),
40981
41913
  resume_from: tipHeight + 1,
40982
41914
  node_tip_at_start: nodeTipAtStart,
@@ -40990,9 +41922,9 @@ Exit codes:
40990
41922
  if (divergent === 0) {
40991
41923
  success(`Restored ${loaded.blocks.toLocaleString()} blocks, ${loaded.transactions.toLocaleString()} transactions, ${loaded.events.toLocaleString()} events in ${formatDuration2(elapsed)}.`);
40992
41924
  if (referenceDigests.length > 0) {
40993
- note(` verified ${referenceDigests.length} ranges against the archive`);
41925
+ note(` verified ${referenceDigests.length} ${verifyDatasets.join("/")} ranges against the archive`);
40994
41926
  } else {
40995
- warn(" archive published no block digests — restore is unverified");
41927
+ warn(" archive published no digests for the restored range; the restore is unverified");
40996
41928
  }
40997
41929
  console.error("");
40998
41930
  console.error(dim(`Your instance holds history through ${tipHeight.toLocaleString()} and will resume at ${(tipHeight + 1).toLocaleString()}.`));
@@ -41006,7 +41938,8 @@ Exit codes:
41006
41938
  console.error(dim(" Start the indexer to continue from there. (Node unreachable — catch-up range unknown.)"));
41007
41939
  }
41008
41940
  } else {
41009
- warn(`Restored ${loaded.blocks.toLocaleString()} blocks but ${divergent} ranges do not match the archive.`);
41941
+ const byDataset = verifyDatasets.map((d2) => `${divergentRanges.filter((r2) => r2.dataset === d2).length} ${d2}`).join(", ");
41942
+ warn(`Restored ${loaded.blocks.toLocaleString()} blocks but ${divergent} ranges do not match the archive (${byDataset}).`);
41010
41943
  console.error(dim(" Investigate with: secondlayer verify --against <manifest>"));
41011
41944
  }
41012
41945
  }
@@ -41014,7 +41947,13 @@ Exit codes:
41014
41947
  process.exit(divergent === 0 ? BOOTSTRAP_EXIT.OK : BOOTSTRAP_EXIT.INCOMPLETE);
41015
41948
  } catch (err) {
41016
41949
  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.";
41950
+ if (err instanceof ArchiveFetchError && err.transient) {
41951
+ printError(message, {
41952
+ hint: "The archive could not be reached. Re-run the same command to resume; datasets already loaded are kept."
41953
+ });
41954
+ process.exit(BOOTSTRAP_EXIT.INCOMPLETE);
41955
+ }
41956
+ 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
41957
  printError(message, { hint });
41019
41958
  process.exit(BOOTSTRAP_EXIT.REFUSED);
41020
41959
  }
@@ -41024,29 +41963,66 @@ function registerBootstrapCommand(program2) {
41024
41963
  attachBootstrapCommand(program2.command("bootstrap").description("Restore chain history from a verified archive instead of syncing from genesis"));
41025
41964
  }
41026
41965
  // src/commands/uninstall.ts
41966
+ init_instance_init();
41027
41967
  init_output();
41028
41968
  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";
41969
+ import { existsSync as existsSync9, readFileSync as readFileSync8 } from "node:fs";
41970
+ import { join as join16, resolve as resolve15 } from "node:path";
41031
41971
  import { planUninstall, uninstallCommand } from "@secondlayer/shared/runtime";
41032
41972
  var UNINSTALL_EXIT = { OK: 0, FAILED: 1, REFUSED: 2 };
41033
- var DEFAULT_COMPOSE = "docker/oss/docker-compose.yml";
41973
+ var REPO_COMPOSE = "docker/oss/docker-compose.yml";
41034
41974
  function bundleHasKeys(bundleDir) {
41035
- const manifest = join17(bundleDir, "manifest.json");
41036
- if (!existsSync8(manifest))
41975
+ const manifest = join16(bundleDir, "manifest.json");
41976
+ if (!existsSync9(manifest))
41037
41977
  return false;
41038
41978
  try {
41039
- const parsed = JSON.parse(readFileSync7(manifest, "utf8"));
41979
+ const parsed = JSON.parse(readFileSync8(manifest, "utf8"));
41040
41980
  return parsed.secrets != null;
41041
41981
  } catch {
41042
41982
  return false;
41043
41983
  }
41044
41984
  }
41985
+ function resolveUninstallLayout(dir, composeOverride) {
41986
+ const setupCompose = resolve15(dir, "docker-compose.yml");
41987
+ const setupEnv = resolve15(dir, ".env");
41988
+ const initEnv = resolve15(dir, ".env.local");
41989
+ const secretsFile = [setupEnv, initEnv].find((file) => readEnvValue(file, "SECONDLAYER_SECRETS_KEY") !== null) ?? [setupEnv, initEnv].find((file) => existsSync9(file));
41990
+ if (composeOverride) {
41991
+ const composeFile = resolve15(dir, composeOverride);
41992
+ const sibling = resolve15(composeFile, "..", ".env");
41993
+ return {
41994
+ composeFile,
41995
+ envFile: existsSync9(sibling) ? sibling : undefined,
41996
+ secretsFile
41997
+ };
41998
+ }
41999
+ if (existsSync9(setupCompose)) {
42000
+ return {
42001
+ composeFile: setupCompose,
42002
+ envFile: existsSync9(setupEnv) ? setupEnv : undefined,
42003
+ secretsFile
42004
+ };
42005
+ }
42006
+ return { composeFile: resolve15(dir, REPO_COMPOSE), secretsFile };
42007
+ }
42008
+ function composeDownArgs(plan, layout) {
42009
+ const base2 = uninstallCommand(plan, layout.composeFile);
42010
+ if (!layout.envFile)
42011
+ return base2;
42012
+ const fileFlagEnd = base2.indexOf("-f") + 2;
42013
+ return [
42014
+ ...base2.slice(0, fileFlagEnd),
42015
+ "--env-file",
42016
+ layout.envFile,
42017
+ ...base2.slice(fileFlagEnd)
42018
+ ];
42019
+ }
41045
42020
  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) => {
42021
+ 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
42022
  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;
42023
+ const layout = resolveUninstallLayout(process.cwd(), opts.compose);
42024
+ const secretsPresent = !!process.env.SECONDLAYER_SECRETS_KEY || !!layout.secretsFile;
42025
+ const keysBackedUp = opts.backup ? bundleHasKeys(resolve15(opts.backup)) : false;
41050
42026
  const decision = planUninstall({
41051
42027
  purge: opts.purge === true,
41052
42028
  confirmed: opts.yes === true,
@@ -41055,16 +42031,30 @@ function registerUninstallCommand(program2) {
41055
42031
  dataDir
41056
42032
  });
41057
42033
  if (!decision.ok) {
41058
- printError(decision.reason);
42034
+ printError(decision.reason, {
42035
+ hint: layout.secretsFile ? `keys found in ${layout.secretsFile}` : undefined
42036
+ });
41059
42037
  process.exit(UNINSTALL_EXIT.REFUSED);
41060
42038
  }
41061
- const cmd = uninstallCommand(decision.plan, opts.compose);
42039
+ const cmd = composeDownArgs(decision.plan, layout);
41062
42040
  if (!opts.apply) {
41063
42041
  output({
41064
42042
  json: opts.json,
41065
- data: { dryRun: true, plan: decision.plan, command: cmd },
42043
+ data: {
42044
+ dryRun: true,
42045
+ plan: decision.plan,
42046
+ command: cmd,
42047
+ composeFile: layout.composeFile,
42048
+ envFile: layout.envFile ?? null,
42049
+ secretsFile: layout.secretsFile ?? null
42050
+ },
41066
42051
  human: () => {
41067
42052
  note("dry run — nothing removed. Pass --apply to proceed.");
42053
+ note(`compose: ${layout.composeFile}`);
42054
+ if (layout.envFile)
42055
+ note(`env: ${layout.envFile}`);
42056
+ if (layout.secretsFile)
42057
+ note(`keys: ${layout.secretsFile}`);
41068
42058
  note(`would run: docker ${cmd.join(" ")}`);
41069
42059
  for (const p2 of decision.plan.preserves) {
41070
42060
  note(`preserved · ${p2.what}: ${p2.detail}`);
@@ -41083,7 +42073,12 @@ function registerUninstallCommand(program2) {
41083
42073
  }
41084
42074
  output({
41085
42075
  json: opts.json,
41086
- data: { removed: true, plan: decision.plan },
42076
+ data: {
42077
+ removed: true,
42078
+ plan: decision.plan,
42079
+ composeFile: layout.composeFile,
42080
+ envFile: layout.envFile ?? null
42081
+ },
41087
42082
  human: () => {
41088
42083
  success("stack removed");
41089
42084
  for (const w2 of decision.warnings)
@@ -41096,18 +42091,18 @@ function registerUninstallCommand(program2) {
41096
42091
  });
41097
42092
  }
41098
42093
  // src/commands/backup.ts
42094
+ init_fs();
41099
42095
  init_output();
41100
- import { createHash as createHash4 } from "node:crypto";
41101
42096
  import {
41102
- chmodSync,
41103
- existsSync as existsSync9,
42097
+ chmodSync as chmodSync2,
42098
+ existsSync as existsSync10,
41104
42099
  mkdirSync as mkdirSync6,
41105
- readFileSync as readFileSync8,
42100
+ readFileSync as readFileSync9,
41106
42101
  statSync as statSync3,
41107
- writeFileSync as writeFileSync7
42102
+ writeFileSync as writeFileSync8
41108
42103
  } 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";
42104
+ import { join as join17, resolve as resolve16 } from "node:path";
42105
+ import { closeDb, getDb as getDb5, sql as sql3 } from "@secondlayer/shared/db";
41111
42106
  import {
41112
42107
  decryptBundle,
41113
42108
  encryptBundle,
@@ -41132,12 +42127,9 @@ function readSecretsKey() {
41132
42127
  const key = process.env.SECONDLAYER_SECRETS_KEY?.trim();
41133
42128
  return key ? key : undefined;
41134
42129
  }
41135
- function sha256File(path11) {
41136
- return createHash4("sha256").update(readFileSync8(path11)).digest("hex");
41137
- }
41138
42130
  async function readScope() {
41139
42131
  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);
42132
+ const bounds = await sql3`SELECT MIN(height)::text AS min, MAX(height)::text AS max FROM blocks WHERE canonical = true`.execute(db);
41141
42133
  const row = bounds.rows[0];
41142
42134
  const network = process.env.STACKS_NETWORK ?? "mainnet";
41143
42135
  return {
@@ -41152,9 +42144,73 @@ function requireDatabaseUrl() {
41152
42144
  throw new Error("DATABASE_URL is required");
41153
42145
  return url;
41154
42146
  }
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();
42147
+ function pgConnection(databaseUrl) {
42148
+ let parsed;
42149
+ try {
42150
+ parsed = new URL(databaseUrl);
42151
+ } catch {
42152
+ return { url: databaseUrl, env: {} };
42153
+ }
42154
+ if (!parsed.password)
42155
+ return { url: databaseUrl, env: {} };
42156
+ const password = decodeURIComponent(parsed.password);
42157
+ parsed.password = "";
42158
+ return { url: parsed.toString(), env: { PGPASSWORD: password } };
42159
+ }
42160
+ function pgDumpInvocation(databaseUrl, dumpPath) {
42161
+ const conn = pgConnection(databaseUrl);
42162
+ return {
42163
+ cmd: ["pg_dump", "-Fc", "--no-owner", "--no-acl", "-f", dumpPath, conn.url],
42164
+ env: conn.env
42165
+ };
42166
+ }
42167
+ function pgRestoreInvocation(databaseUrl, dumpPath) {
42168
+ const conn = pgConnection(databaseUrl);
42169
+ return {
42170
+ cmd: [
42171
+ "pg_restore",
42172
+ "--no-owner",
42173
+ "--no-acl",
42174
+ "--clean",
42175
+ "--if-exists",
42176
+ "--single-transaction",
42177
+ "--exit-on-error",
42178
+ "-d",
42179
+ conn.url,
42180
+ dumpPath
42181
+ ],
42182
+ env: conn.env
42183
+ };
42184
+ }
42185
+ function checkRestoredScope(manifest, restored) {
42186
+ const want = manifest.scope;
42187
+ if (want.to_height === null && restored.toHeight === null) {
42188
+ return { ok: true };
42189
+ }
42190
+ if (restored.toHeight === null) {
42191
+ return {
42192
+ ok: false,
42193
+ reason: `the restore finished but the target holds no canonical blocks; the manifest promised ${want.from_height} through ${want.to_height ?? "?"}`
42194
+ };
42195
+ }
42196
+ if (restored.fromHeight !== want.from_height || want.to_height !== null && restored.toHeight !== want.to_height) {
42197
+ return {
42198
+ ok: false,
42199
+ 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`
42200
+ };
42201
+ }
42202
+ return { ok: true };
42203
+ }
42204
+ async function runProcess(cmd, label, env2 = {}) {
42205
+ const proc = Bun.spawn(cmd, {
42206
+ stdout: "pipe",
42207
+ stderr: "pipe",
42208
+ env: { ...process.env, ...env2 }
42209
+ });
42210
+ const [stderr] = await Promise.all([
42211
+ new Response(proc.stderr).text(),
42212
+ new Response(proc.stdout).text()
42213
+ ]);
41158
42214
  const code = await proc.exited;
41159
42215
  if (code !== 0) {
41160
42216
  return { ok: false, stderr: `${label} exited ${code}: ${stderr.trim()}` };
@@ -41162,7 +42218,7 @@ async function runProcess(cmd, label) {
41162
42218
  return { ok: true, stderr };
41163
42219
  }
41164
42220
  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) => {
42221
+ 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
42222
  try {
41167
42223
  await runBackup(opts);
41168
42224
  await closeDb();
@@ -41194,11 +42250,12 @@ async function runBackup(opts) {
41194
42250
  printError(plan.reason);
41195
42251
  process.exit(BACKUP_EXIT.REFUSED);
41196
42252
  }
41197
- const outDir = resolve14(opts.out);
42253
+ const outDir = resolve16(opts.out);
41198
42254
  mkdirSync6(outDir, { recursive: true });
41199
42255
  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");
42256
+ const dumpPath = join17(outDir, DB_FILE);
42257
+ const dumpCmd = pgDumpInvocation(databaseUrl, dumpPath);
42258
+ const dump = await runProcess(dumpCmd.cmd, "pg_dump", dumpCmd.env);
41202
42259
  if (!dump.ok) {
41203
42260
  printError(dump.stderr);
41204
42261
  process.exit(BACKUP_EXIT.FAILED);
@@ -41207,7 +42264,7 @@ async function runBackup(opts) {
41207
42264
  ...plan.manifest,
41208
42265
  db: {
41209
42266
  file: DB_FILE,
41210
- sha256: sha256File(dumpPath),
42267
+ sha256: await sha256File(dumpPath),
41211
42268
  bytes: statSync3(dumpPath).size,
41212
42269
  format: "pg_dump-custom"
41213
42270
  }
@@ -41216,16 +42273,16 @@ async function runBackup(opts) {
41216
42273
  const lines2 = SECRET_ENV_KEYS.filter((k2) => process.env[k2]).map((k2) => `${k2}=${process.env[k2]}`);
41217
42274
  const sealed = encryptBundle(lines2.join(`
41218
42275
  `), passphrase);
41219
- const secretsPath = join18(outDir, SECRETS_FILE);
41220
- writeFileSync7(secretsPath, sealed);
41221
- chmodSync(secretsPath, 384);
42276
+ const secretsPath = join17(outDir, SECRETS_FILE);
42277
+ writeFileSync8(secretsPath, sealed);
42278
+ chmodSync2(secretsPath, 384);
41222
42279
  manifest.secrets = {
41223
42280
  file: SECRETS_FILE,
41224
42281
  encrypted: true,
41225
42282
  canary: sealKeyCanary(secretsKey)
41226
42283
  };
41227
42284
  }
41228
- writeFileSync7(join18(outDir, MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}
42285
+ writeFileSync8(join17(outDir, MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}
41229
42286
  `);
41230
42287
  output({
41231
42288
  json: opts.json,
@@ -41242,7 +42299,7 @@ async function runBackup(opts) {
41242
42299
  });
41243
42300
  }
41244
42301
  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) => {
42302
+ 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
42303
  try {
41247
42304
  await runRestore(opts);
41248
42305
  await closeDb();
@@ -41254,15 +42311,15 @@ function attachRestoreCommand(cmd) {
41254
42311
  }
41255
42312
  async function runRestore(opts) {
41256
42313
  const databaseUrl = requireDatabaseUrl();
41257
- const dir = resolve14(opts.from);
41258
- const manifestPath = join18(dir, MANIFEST_FILE);
41259
- if (!existsSync9(manifestPath)) {
42314
+ const dir = resolve16(opts.from);
42315
+ const manifestPath = join17(dir, MANIFEST_FILE);
42316
+ if (!existsSync10(manifestPath)) {
41260
42317
  printError(`no ${MANIFEST_FILE} in ${dir}`);
41261
42318
  process.exit(BACKUP_EXIT.REFUSED);
41262
42319
  }
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) {
42320
+ const manifest = JSON.parse(readFileSync9(manifestPath, "utf8"));
42321
+ const dumpPath = join17(dir, manifest.db?.file ?? DB_FILE);
42322
+ if (manifest.db && await sha256File(dumpPath) !== manifest.db.sha256) {
41266
42323
  printError("the database dump does not match the digest in the manifest; this bundle is corrupt or truncated");
41267
42324
  process.exit(BACKUP_EXIT.REFUSED);
41268
42325
  }
@@ -41275,7 +42332,7 @@ async function runRestore(opts) {
41275
42332
  process.exit(BACKUP_EXIT.REFUSED);
41276
42333
  }
41277
42334
  try {
41278
- const env2 = decryptBundle(readFileSync8(join18(dir, manifest.secrets.file)), passphrase);
42335
+ const env2 = decryptBundle(readFileSync9(join17(dir, manifest.secrets.file)), passphrase);
41279
42336
  bundleKey = env2.split(`
41280
42337
  `).find((l2) => l2.startsWith("SECONDLAYER_SECRETS_KEY="))?.slice("SECONDLAYER_SECRETS_KEY=".length).trim();
41281
42338
  } catch {
@@ -41288,10 +42345,10 @@ async function runRestore(opts) {
41288
42345
  process.exit(BACKUP_EXIT.REFUSED);
41289
42346
  }
41290
42347
  const db = getDb5();
41291
- const present = await sql4`SELECT to_regclass('public.blocks') IS NOT NULL AS present`.execute(db);
42348
+ const present = await sql3`SELECT to_regclass('public.blocks') IS NOT NULL AS present`.execute(db);
41292
42349
  let targetIsEmpty = true;
41293
42350
  if (present.rows[0]?.present) {
41294
- const existing = await sql4`SELECT COUNT(*)::text AS count FROM blocks`.execute(db);
42351
+ const existing = await sql3`SELECT COUNT(*)::text AS count FROM blocks`.execute(db);
41295
42352
  targetIsEmpty = (existing.rows[0]?.count ?? "0") === "0";
41296
42353
  }
41297
42354
  const check = precheckRestore({
@@ -41320,20 +42377,18 @@ async function runRestore(opts) {
41320
42377
  return;
41321
42378
  }
41322
42379
  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");
42380
+ const restoreCmd = pgRestoreInvocation(databaseUrl, dumpPath);
42381
+ const restore = await runProcess(restoreCmd.cmd, "pg_restore", restoreCmd.env);
41333
42382
  if (!restore.ok) {
41334
42383
  printError(restore.stderr);
41335
42384
  process.exit(BACKUP_EXIT.FAILED);
41336
42385
  }
42386
+ const restored = await readScope();
42387
+ const scopeCheck = checkRestoredScope(manifest, restored);
42388
+ if (!scopeCheck.ok) {
42389
+ printError(scopeCheck.reason);
42390
+ process.exit(BACKUP_EXIT.FAILED);
42391
+ }
41337
42392
  output({
41338
42393
  json: opts.json,
41339
42394
  data: { restored: true, manifest },
@@ -41353,19 +42408,26 @@ function registerRestoreCommand(program2) {
41353
42408
  attachRestoreCommand(program2.command("restore").description("Restore an instance from a backup bundle"));
41354
42409
  }
41355
42410
  // src/commands/init.ts
42411
+ init_archive_reference();
41356
42412
  init_instance_init();
41357
42413
  init_output();
42414
+ init_resolve_auth();
42415
+ var DEFAULT_INIT_API_URL = "http://127.0.0.1:3800";
41358
42416
  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");
42417
+ 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", `
42418
+ --network <mainnet|testnet|devnet> and --api-url <url> are top-level global
42419
+ flags (see \`secondlayer --help\`). Defaults: mainnet, http://127.0.0.1:3800.
42420
+ `).action(async (opts) => {
42421
+ const network = instanceNetworkFromEnv();
41364
42422
  const existing = opts.force ? {} : loadExistingInstanceEnv(process.cwd());
41365
42423
  const env2 = buildInstanceEnv({
41366
42424
  network,
41367
42425
  existing,
41368
- apiUrl: opts.apiUrl
42426
+ apiUrl: process.env.SL_API_URL ?? DEFAULT_INIT_API_URL,
42427
+ archivePublicKeyPem: await resolveArchivePublicKey({
42428
+ envPem: process.env.ARCHIVE_SIGNING_PUBLIC_KEY ?? process.env.STREAMS_SIGNING_PUBLIC_KEY,
42429
+ allowHostedApi: !isOssMode()
42430
+ })
41369
42431
  });
41370
42432
  const path11 = writeInstanceEnv(process.cwd(), env2);
41371
42433
  success(`Wrote ${INSTANCE_ENV_FILE}`);
@@ -41387,7 +42449,7 @@ function registerObserverCommand(program2) {
41387
42449
  \`secondlayer --help\`) — default mainnet, same as before.
41388
42450
  `).action((opts) => {
41389
42451
  const mode = parseObserverMode(opts.mode);
41390
- const network = parseInstanceNetwork(process.env.STACKS_NETWORK ?? "mainnet");
42452
+ const network = instanceNetworkFromEnv();
41391
42453
  const endpoint = opts.endpoint ?? defaultObserverEndpoint(network);
41392
42454
  const recovery = opts.recovery ? parseRecoverySource(opts.recovery) : undefined;
41393
42455
  if (mode === "signer-shared") {
@@ -41403,7 +42465,8 @@ init_output();
41403
42465
  init_instance_init();
41404
42466
  init_oss_compose();
41405
42467
  init_setup_wizard();
41406
- import { confirm as confirm9, input as input4, select as select5 } from "@inquirer/prompts";
42468
+ import { resolve as resolvePath3 } from "node:path";
42469
+ import { confirm as confirm6, input as input4, select as select5 } from "@inquirer/prompts";
41407
42470
 
41408
42471
  class SetupCancelledError extends Error {
41409
42472
  constructor() {
@@ -41497,7 +42560,7 @@ async function promptSetupConfig(flags) {
41497
42560
  apiPort: flags.apiPort ?? "127.0.0.1:3800",
41498
42561
  indexerPort: "127.0.0.1:3700",
41499
42562
  postgresPort: "127.0.0.1:5432",
41500
- dir: flags.dir ?? process.cwd(),
42563
+ dir: resolvePath3(flags.dir ?? process.cwd()),
41501
42564
  against: skipBootstrap ? undefined : against,
41502
42565
  skipBootstrap,
41503
42566
  skipVerify: !!flags.skipVerify,
@@ -41513,7 +42576,7 @@ async function promptSetupConfig(flags) {
41513
42576
  console.error(` api port ${config.apiPort}`);
41514
42577
  console.error(` bootstrap ${config.skipBootstrap ? "skip (sync from genesis)" : config.against}`);
41515
42578
  console.error("");
41516
- const proceed = await confirm9({ message: "Start setup?", default: true });
42579
+ const proceed = await confirm6({ message: "Start setup?", default: true });
41517
42580
  if (!proceed)
41518
42581
  throw new SetupCancelledError;
41519
42582
  return config;
@@ -41590,7 +42653,7 @@ async function runPromptedInteractive(flags) {
41590
42653
  await executeAndRender(config);
41591
42654
  }
41592
42655
  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", `
42656
+ 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
42657
  --network <mainnet|testnet|devnet> is the top-level global flag (see
41595
42658
  \`secondlayer --help\`) — required here too, just parsed one level up.
41596
42659
 
@@ -41625,44 +42688,9 @@ decision with no safe default (--network, --node-mode, and --against unless
41625
42688
  }
41626
42689
  });
41627
42690
  }
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
42691
  // src/cli.ts
41664
42692
  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)");
42693
+ 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
42694
  program.hook("preAction", (thisCommand) => {
41667
42695
  const { network, apiKey, apiUrl } = thisCommand.opts();
41668
42696
  if (network)
@@ -41678,7 +42706,8 @@ program.addHelpText("after", `
41678
42706
  Quickstart:
41679
42707
  $ secondlayer setup
41680
42708
  $ secondlayer subgraphs create my-watcher --from-contract SP....my-contract
41681
- $ secondlayer subgraphs deploy subgraphs/my-watcher.ts
42709
+ $ git add subgraphs/my-watcher.ts
42710
+ $ secondlayer subgraphs deploy subgraphs/my-watcher.ts # or --allow-uncommitted
41682
42711
 
41683
42712
  Manual / scripted setup, one step at a time:
41684
42713
  $ secondlayer init --network mainnet
@@ -41689,7 +42718,6 @@ registerSetupCommand(program);
41689
42718
  registerInitCommand(program);
41690
42719
  registerBootstrapCommand(program);
41691
42720
  registerObserverCommand(program);
41692
- registerStartCommand(program);
41693
42721
  registerConsoleCommand(program);
41694
42722
  registerLoginCommand(program);
41695
42723
  registerLogoutCommand(program);
@@ -41718,5 +42746,5 @@ program.commandsGroup("Archive:");
41718
42746
  registerCreditsCommand(program);
41719
42747
  program.parse();
41720
42748
 
41721
- //# debugId=90E04792038F19D064756E2164756E21
42749
+ //# debugId=70015BE103AF7D4B64756E2164756E21
41722
42750
  //# sourceMappingURL=cli.js.map