@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.
- package/README.md +28 -14
- package/dist/cli.js +1847 -819
- package/dist/cli.js.map +41 -40
- 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
|
-
|
|
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
|
-
|
|
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
|
|
2503
|
-
|
|
2531
|
+
if (err?.code === "ENOENT") {
|
|
2532
|
+
return { sessions: {} };
|
|
2533
|
+
}
|
|
2534
|
+
return { sessions: {} };
|
|
2504
2535
|
}
|
|
2505
2536
|
}
|
|
2506
|
-
async function
|
|
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(
|
|
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
|
|
2519
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2530
|
-
|
|
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
|
|
2600
|
-
|
|
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
|
|
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
|
|
2649
|
+
return httpAt(resolveArchiveOpsUrl(), path2, opts);
|
|
2619
2650
|
}
|
|
2620
|
-
async function
|
|
2651
|
+
async function resolveArchiveOpsBearer() {
|
|
2621
2652
|
const envKey = resolveEnvKey();
|
|
2622
|
-
const session =
|
|
2623
|
-
|
|
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" },
|
|
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
|
-
|
|
8757
|
-
|
|
8758
|
-
|
|
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
|
|
8770
|
-
|
|
8771
|
-
|
|
8772
|
-
|
|
8773
|
-
|
|
8774
|
-
|
|
8775
|
-
|
|
8776
|
-
|
|
8777
|
-
|
|
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
|
-
|
|
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
|
|
8879
|
+
return tokens;
|
|
8782
8880
|
}
|
|
8783
|
-
function
|
|
8784
|
-
const
|
|
8785
|
-
|
|
8786
|
-
|
|
8787
|
-
|
|
8788
|
-
if (
|
|
8789
|
-
|
|
8790
|
-
|
|
8791
|
-
|
|
8792
|
-
|
|
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
|
|
8797
|
-
|
|
8798
|
-
|
|
8799
|
-
|
|
8800
|
-
|
|
8801
|
-
|
|
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
|
|
8805
|
-
|
|
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 "
|
|
8950
|
+
return "trait_reference";
|
|
8819
8951
|
default:
|
|
8820
|
-
|
|
8821
|
-
|
|
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
|
-
|
|
8824
|
-
|
|
9099
|
+
case "define-fungible-token": {
|
|
9100
|
+
const token = toFungibleToken(form.items);
|
|
9101
|
+
if (token)
|
|
9102
|
+
fungibleTokens.push(token);
|
|
9103
|
+
break;
|
|
8825
9104
|
}
|
|
8826
|
-
|
|
8827
|
-
|
|
9105
|
+
case "define-non-fungible-token": {
|
|
9106
|
+
const token = toNonFungibleToken(form.items);
|
|
9107
|
+
if (token)
|
|
9108
|
+
nonFungibleTokens.push(token);
|
|
9109
|
+
break;
|
|
8828
9110
|
}
|
|
8829
|
-
|
|
8830
|
-
|
|
8831
|
-
|
|
8832
|
-
|
|
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
|
-
|
|
8844
|
-
|
|
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
|
|
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 =
|
|
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 (
|
|
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
|
-
},
|
|
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
|
-
|
|
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 {
|
|
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
|
|
28759
|
-
if (
|
|
29097
|
+
const unstaged = runGit(["diff", "--name-only", "--", absPath], cwd);
|
|
29098
|
+
if (unstaged === null) {
|
|
28760
29099
|
return { kind: "not-a-repo" };
|
|
28761
29100
|
}
|
|
28762
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
28787
|
-
process.env.SL_API_KEY = token;
|
|
29128
|
+
const target = loginTarget(opts);
|
|
28788
29129
|
let account;
|
|
28789
29130
|
try {
|
|
28790
|
-
account = await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
29839
|
-
By default the source file must be committed
|
|
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}.
|
|
29852
|
-
if (process.
|
|
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("
|
|
30202
|
+
info("Nothing was deployed.");
|
|
29859
30203
|
process.exit(0);
|
|
29860
30204
|
}
|
|
29861
30205
|
} else {
|
|
29862
|
-
error(`${message}
|
|
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
|
|
29928
|
-
message: "
|
|
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("
|
|
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
|
-
|
|
30118
|
-
|
|
30119
|
-
|
|
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
|
|
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
|
-
|
|
30171
|
-
|
|
30172
|
-
|
|
30173
|
-
|
|
30174
|
-
|
|
30175
|
-
|
|
30176
|
-
|
|
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
|
-
|
|
30416
|
-
|
|
30417
|
-
|
|
30418
|
-
|
|
30419
|
-
|
|
30420
|
-
|
|
30421
|
-
|
|
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("
|
|
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
|
|
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(),
|
|
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 =
|
|
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 ===
|
|
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 (
|
|
32671
|
-
return go(
|
|
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,
|
|
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
|
|
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
|
|
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 =
|
|
33098
|
-
DEV_STATE_PATH =
|
|
33099
|
-
LOGS_DIR =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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",
|
|
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",
|
|
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",
|
|
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
|
-
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
34627
|
-
import { resolve as
|
|
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 (!
|
|
35239
|
+
if (!existsSync8(envPath))
|
|
34661
35240
|
return null;
|
|
34662
|
-
const match =
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
34710
|
-
|
|
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 =
|
|
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
|
|
35576
|
+
import { writeFile as writeFile6 } from "node:fs/promises";
|
|
34999
35577
|
import { createServer } from "node:net";
|
|
35000
|
-
import { join as
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
35734
|
+
`POSTGRES_USER=${SETUP_POSTGRES_USER}`,
|
|
35135
35735
|
`POSTGRES_PASSWORD=${secrets.postgresPassword}`,
|
|
35136
|
-
|
|
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
|
-
|
|
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 =
|
|
35161
|
-
const envPath =
|
|
35162
|
-
await
|
|
35163
|
-
await
|
|
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 =
|
|
35167
|
-
await
|
|
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 =
|
|
35175
|
-
await
|
|
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
|
-
|
|
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, {
|
|
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, {
|
|
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
|
|
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((
|
|
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
|
-
|
|
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: "
|
|
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.
|
|
36146
|
-
"@secondlayer/sdk": "^
|
|
36147
|
-
"@secondlayer/shared": "^11.
|
|
36148
|
-
"@secondlayer/stacks": "^
|
|
36149
|
-
"@secondlayer/subgraphs": "^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(
|
|
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 (!
|
|
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 (!
|
|
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 {
|
|
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
|
|
36885
|
-
const
|
|
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
|
-
|
|
37113
|
-
|
|
37114
|
-
|
|
37115
|
-
|
|
37116
|
-
|
|
37117
|
-
|
|
37118
|
-
|
|
37119
|
-
}
|
|
37120
|
-
|
|
37121
|
-
|
|
37122
|
-
|
|
37123
|
-
|
|
37124
|
-
|
|
37125
|
-
|
|
37126
|
-
|
|
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 (
|
|
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 &&
|
|
37218
|
-
const envTarget =
|
|
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
|
-
|
|
37672
|
-
|
|
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
|
-
|
|
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 {
|
|
38422
|
-
import {
|
|
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 =
|
|
38532
|
-
note(
|
|
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:
|
|
38544
|
-
|
|
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
|
|
38586
|
-
const
|
|
38587
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
39848
|
-
const home2 =
|
|
40607
|
+
let dir = resolve12(cwd);
|
|
40608
|
+
const home2 = resolve12(homedir5());
|
|
39849
40609
|
const fsRoot = parsePath2(dir).root;
|
|
39850
40610
|
while (true) {
|
|
39851
|
-
const candidate =
|
|
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(
|
|
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:
|
|
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
|
|
39881
|
-
|
|
39882
|
-
|
|
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
|
|
40644
|
+
account = await httpArchiveOps("/api/accounts/me");
|
|
39891
40645
|
} catch (err) {
|
|
39892
|
-
if (err instanceof CliHttpError
|
|
39893
|
-
error(
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
39937
|
-
|
|
39938
|
-
|
|
39939
|
-
|
|
39940
|
-
|
|
39941
|
-
|
|
39942
|
-
|
|
39943
|
-
|
|
39944
|
-
|
|
39945
|
-
|
|
39946
|
-
|
|
39947
|
-
|
|
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
|
-
|
|
39951
|
-
|
|
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
|
|
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
|
|
40145
|
-
|
|
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
|
-
|
|
40301
|
-
|
|
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
|
|
40309
|
-
import { tmpdir as
|
|
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 {
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
40421
|
-
if (!
|
|
41074
|
+
const cached = cache.get(path11);
|
|
41075
|
+
if (!cached) {
|
|
40422
41076
|
throw new Error(`archive gate: no URL returned for ${path11}`);
|
|
40423
|
-
|
|
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(
|
|
41130
|
+
const path11 = join15(tmpdir3(), `sl-repair-${label}-${process.pid}.parquet`);
|
|
40446
41131
|
await writeFile5(path11, bytes);
|
|
40447
41132
|
try {
|
|
40448
|
-
const reader = await
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
40552
|
-
|
|
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 =
|
|
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
|
-
|
|
41403
|
+
quoteLine = formatQuoteValue(result.quote, "repair");
|
|
41404
|
+
note(` metered: ${quoteLine}`);
|
|
40597
41405
|
if (shouldPromptForGatedFetch(opts)) {
|
|
40598
|
-
|
|
40599
|
-
|
|
40600
|
-
|
|
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
|
-
|
|
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
|
-
|
|
40623
|
-
|
|
40624
|
-
|
|
40625
|
-
|
|
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
|
-
|
|
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 ?
|
|
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
|
-
|
|
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
|
-
|
|
40651
|
-
|
|
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(`
|
|
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(
|
|
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
|
-
|
|
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
|
|
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 {
|
|
40705
|
-
|
|
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 (
|
|
41574
|
+
if (RESUME_DATASETS.every((d2) => input4.highWater[d2] === null)) {
|
|
40717
41575
|
return { action: "fresh" };
|
|
40718
41576
|
}
|
|
40719
|
-
const
|
|
40720
|
-
|
|
40721
|
-
|
|
40722
|
-
|
|
40723
|
-
|
|
40724
|
-
|
|
40725
|
-
|
|
40726
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
40798
|
-
const written = await
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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"
|
|
40852
|
-
|
|
40853
|
-
|
|
40854
|
-
|
|
40855
|
-
|
|
40856
|
-
|
|
40857
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
40924
|
-
const datasetPartitions = partitions.filter((p2) => p2.dataset === dataset)
|
|
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
|
|
40935
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
41030
|
-
import { join as
|
|
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
|
|
41973
|
+
var REPO_COMPOSE = "docker/oss/docker-compose.yml";
|
|
41034
41974
|
function bundleHasKeys(bundleDir) {
|
|
41035
|
-
const manifest =
|
|
41036
|
-
if (!
|
|
41975
|
+
const manifest = join16(bundleDir, "manifest.json");
|
|
41976
|
+
if (!existsSync9(manifest))
|
|
41037
41977
|
return false;
|
|
41038
41978
|
try {
|
|
41039
|
-
const parsed = JSON.parse(
|
|
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>",
|
|
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
|
|
41049
|
-
const
|
|
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 =
|
|
42039
|
+
const cmd = composeDownArgs(decision.plan, layout);
|
|
41062
42040
|
if (!opts.apply) {
|
|
41063
42041
|
output({
|
|
41064
42042
|
json: opts.json,
|
|
41065
|
-
data: {
|
|
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: {
|
|
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
|
|
42097
|
+
chmodSync as chmodSync2,
|
|
42098
|
+
existsSync as existsSync10,
|
|
41104
42099
|
mkdirSync as mkdirSync6,
|
|
41105
|
-
readFileSync as
|
|
42100
|
+
readFileSync as readFileSync9,
|
|
41106
42101
|
statSync as statSync3,
|
|
41107
|
-
writeFileSync as
|
|
42102
|
+
writeFileSync as writeFileSync8
|
|
41108
42103
|
} from "node:fs";
|
|
41109
|
-
import { join as
|
|
41110
|
-
import { closeDb, getDb as getDb5, sql as
|
|
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
|
|
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
|
-
|
|
41156
|
-
|
|
41157
|
-
|
|
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
|
|
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 =
|
|
42253
|
+
const outDir = resolve16(opts.out);
|
|
41198
42254
|
mkdirSync6(outDir, { recursive: true });
|
|
41199
42255
|
console.error("dumping database…");
|
|
41200
|
-
const dumpPath =
|
|
41201
|
-
const
|
|
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 =
|
|
41220
|
-
|
|
41221
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
41258
|
-
const manifestPath =
|
|
41259
|
-
if (!
|
|
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(
|
|
41264
|
-
const dumpPath =
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
41324
|
-
|
|
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("--
|
|
41360
|
-
--network <mainnet|testnet|devnet>
|
|
41361
|
-
\`secondlayer --help\`)
|
|
41362
|
-
`).action((opts) => {
|
|
41363
|
-
const network =
|
|
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:
|
|
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 =
|
|
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 {
|
|
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
|
|
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",
|
|
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 (
|
|
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
|
-
$
|
|
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=
|
|
42749
|
+
//# debugId=70015BE103AF7D4B64756E2164756E21
|
|
41722
42750
|
//# sourceMappingURL=cli.js.map
|