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