@secondlayer/cli 9.10.0 → 10.1.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 +15 -4
- package/dist/cli.js +406 -103
- package/dist/cli.js.map +26 -22
- package/package.json +3 -3
package/dist/cli.js
CHANGED
|
@@ -2481,6 +2481,25 @@ var require_commander = __commonJS((exports) => {
|
|
|
2481
2481
|
exports.InvalidOptionArgumentError = InvalidArgumentError;
|
|
2482
2482
|
});
|
|
2483
2483
|
|
|
2484
|
+
// ../../node_modules/commander/esm.mjs
|
|
2485
|
+
var import__, program, createCommand, createArgument, createOption, CommanderError, InvalidArgumentError, InvalidOptionArgumentError, Command, Argument, Option, Help;
|
|
2486
|
+
var init_esm = __esm(() => {
|
|
2487
|
+
import__ = __toESM(require_commander(), 1);
|
|
2488
|
+
({
|
|
2489
|
+
program,
|
|
2490
|
+
createCommand,
|
|
2491
|
+
createArgument,
|
|
2492
|
+
createOption,
|
|
2493
|
+
CommanderError,
|
|
2494
|
+
InvalidArgumentError,
|
|
2495
|
+
InvalidOptionArgumentError,
|
|
2496
|
+
Command,
|
|
2497
|
+
Argument,
|
|
2498
|
+
Option,
|
|
2499
|
+
Help
|
|
2500
|
+
} = import__.default);
|
|
2501
|
+
});
|
|
2502
|
+
|
|
2484
2503
|
// src/lib/session.ts
|
|
2485
2504
|
import {
|
|
2486
2505
|
chmod,
|
|
@@ -2543,6 +2562,9 @@ async function resolveAuth() {
|
|
|
2543
2562
|
if (envKey) {
|
|
2544
2563
|
return { apiUrl, ephemeralKey: envKey, fromEnv: true };
|
|
2545
2564
|
}
|
|
2565
|
+
if (isOssMode()) {
|
|
2566
|
+
return { apiUrl, ephemeralKey: "", fromEnv: true };
|
|
2567
|
+
}
|
|
2546
2568
|
const session = await readSession();
|
|
2547
2569
|
if (!session) {
|
|
2548
2570
|
const err = new Error("Not logged in — run `sl login`");
|
|
@@ -2551,6 +2573,9 @@ async function resolveAuth() {
|
|
|
2551
2573
|
}
|
|
2552
2574
|
return { apiUrl, ephemeralKey: session.token, fromEnv: false };
|
|
2553
2575
|
}
|
|
2576
|
+
function isOssMode() {
|
|
2577
|
+
return !!process.env.SL_API_URL;
|
|
2578
|
+
}
|
|
2554
2579
|
var DEFAULT_API_URL = "https://api.secondlayer.tools";
|
|
2555
2580
|
var init_resolve_auth = __esm(() => {
|
|
2556
2581
|
init_session();
|
|
@@ -2590,7 +2615,7 @@ async function httpPlatform(path2, opts = {}) {
|
|
|
2590
2615
|
try {
|
|
2591
2616
|
auth = await resolveAuth();
|
|
2592
2617
|
} catch {
|
|
2593
|
-
throw new CliHttpError(401, "SESSION_EXPIRED", { error: "Not logged in" }, "Not logged in — run `sl login`");
|
|
2618
|
+
throw new CliHttpError(401, "SESSION_EXPIRED", { error: "Not logged in" }, isOssMode() ? "Not authenticated — set SL_API_KEY from `sl init`" : "Not logged in — run `sl login`");
|
|
2594
2619
|
}
|
|
2595
2620
|
return request(`${auth.apiUrl}${path2}`, {
|
|
2596
2621
|
...opts,
|
|
@@ -2745,7 +2770,7 @@ function handleApiError(err, action) {
|
|
|
2745
2770
|
const status = err instanceof ApiError || err instanceof CliHttpError ? err.status : undefined;
|
|
2746
2771
|
if (err instanceof CliHttpError && err.code === "SESSION_EXPIRED" || status === 401) {
|
|
2747
2772
|
printError("Authentication required.", {
|
|
2748
|
-
hint: "Run `sl login` to re-authenticate."
|
|
2773
|
+
hint: isOssMode() ? "Set SL_API_KEY / INSTANCE_TOKEN from `sl init`." : "Run `sl login` to re-authenticate."
|
|
2749
2774
|
});
|
|
2750
2775
|
process.exit(1);
|
|
2751
2776
|
}
|
|
@@ -28826,7 +28851,7 @@ async function runLoginFlow(options3 = {}) {
|
|
|
28826
28851
|
}
|
|
28827
28852
|
}
|
|
28828
28853
|
function registerLoginCommand(program2) {
|
|
28829
|
-
program2.command("login").description("Log in to Secondlayer (magic-link email)").option("--force", "Skip the already-logged-in check and re-run the flow").option("--with-token", "Read an API key from stdin (non-interactive)").addHelpText("after", `
|
|
28854
|
+
program2.command("login", { hidden: true }).description("Log in to Secondlayer (magic-link email)").option("--force", "Skip the already-logged-in check and re-run the flow").option("--with-token", "Read an API key from stdin (non-interactive)").addHelpText("after", `
|
|
28830
28855
|
Examples:
|
|
28831
28856
|
$ sl login
|
|
28832
28857
|
$ echo "$SL_API_KEY" | sl login --with-token`).action((opts) => opts.withToken ? runTokenLogin() : runLoginFlow({ force: opts.force }));
|
|
@@ -28839,6 +28864,8 @@ var init_login = __esm(() => {
|
|
|
28839
28864
|
|
|
28840
28865
|
// src/lib/require-auth.ts
|
|
28841
28866
|
async function requireAuth() {
|
|
28867
|
+
if (isOssMode())
|
|
28868
|
+
return;
|
|
28842
28869
|
const session = await readSession();
|
|
28843
28870
|
if (session)
|
|
28844
28871
|
return;
|
|
@@ -28848,6 +28875,7 @@ async function requireAuth() {
|
|
|
28848
28875
|
var init_require_auth = __esm(() => {
|
|
28849
28876
|
init_login();
|
|
28850
28877
|
init_output();
|
|
28878
|
+
init_resolve_auth();
|
|
28851
28879
|
init_session();
|
|
28852
28880
|
});
|
|
28853
28881
|
|
|
@@ -30104,7 +30132,7 @@ Stopped watching.`);
|
|
|
30104
30132
|
});
|
|
30105
30133
|
await new Promise(() => {});
|
|
30106
30134
|
});
|
|
30107
|
-
subgraphs.command("deploy <file>").alias("update").description("Deploy a subgraph definition file (alias: update — deploy is create-or-update)").option("--start-block <n>", "Override the subgraph definition startBlock for this deploy").option("--tip-first", "Go live at chain tip immediately; history backfills in the background (requires order-tolerant handlers — commutative or insert-only writes)").option("--dry-run", "Validate and preview deploy without writing changes").option("-y, --yes", "Skip the reindex confirmation prompt").option("--database-url <url>", "BYO data plane: write the subgraph's schema/rows to your own Postgres. With --dry-run, prints the DDL + grant script and verifies the connection.").option("--strict", "Run `tsc --noEmit` against the handler before deploy (slower; catches TS type errors)").
|
|
30135
|
+
subgraphs.command("deploy <file>").alias("update").description("Deploy a subgraph definition file (alias: update — deploy is create-or-update)").option("--start-block <n>", "Override the subgraph definition startBlock for this deploy").option("--tip-first", "Go live at chain tip immediately; history backfills in the background (requires order-tolerant handlers — commutative or insert-only writes)").option("--dry-run", "Validate and preview deploy without writing changes").option("-y, --yes", "Skip the reindex confirmation prompt").option("--database-url <url>", "BYO data plane: write the subgraph's schema/rows to your own Postgres. With --dry-run, prints the DDL + grant script and verifies the connection.").option("--strict", "Run `tsc --noEmit` against the handler before deploy (slower; catches TS type errors)").addOption(new Option("--visibility <visibility>", "Hosted only. Public/private namespace.").hideHelp()).option("--allow-uncommitted", "Deploy even though the source file is not committed to git (the deployed definition will exist only in the database)").action(async (file, options3) => {
|
|
30108
30136
|
try {
|
|
30109
30137
|
const absPath = resolve3(file);
|
|
30110
30138
|
if (!options3.dryRun) {
|
|
@@ -30132,7 +30160,7 @@ Stopped watching.`);
|
|
|
30132
30160
|
}
|
|
30133
30161
|
}
|
|
30134
30162
|
const config = await loadConfig2();
|
|
30135
|
-
if (config.network !== "local") {
|
|
30163
|
+
if (config.network !== "local" && !isOssMode()) {
|
|
30136
30164
|
await requireAuth();
|
|
30137
30165
|
}
|
|
30138
30166
|
const dryRun = options3.dryRun;
|
|
@@ -30666,7 +30694,7 @@ ${rows.length} row(s)`));
|
|
|
30666
30694
|
handleApiError(err, "delete subgraph");
|
|
30667
30695
|
}
|
|
30668
30696
|
});
|
|
30669
|
-
subgraphs.command("publish <name>").description("Make a subgraph publicly readable on /v1/subgraphs/<name> (claims the name globally, no key needed to read)").action(async (name) => {
|
|
30697
|
+
subgraphs.command("publish <name>", { hidden: true }).description("Make a subgraph publicly readable on /v1/subgraphs/<name> (claims the name globally, no key needed to read)").action(async (name) => {
|
|
30670
30698
|
try {
|
|
30671
30699
|
await requireAuth();
|
|
30672
30700
|
const result = await publishSubgraphApi(name);
|
|
@@ -30684,7 +30712,7 @@ ${rows.length} row(s)`));
|
|
|
30684
30712
|
handleApiError(err, "publish subgraph");
|
|
30685
30713
|
}
|
|
30686
30714
|
});
|
|
30687
|
-
subgraphs.command("unpublish <name>").description("Make a subgraph private again — reads require your API key, the public name claim is released").action(async (name) => {
|
|
30715
|
+
subgraphs.command("unpublish <name>", { hidden: true }).description("Make a subgraph private again — reads require your API key, the public name claim is released").action(async (name) => {
|
|
30688
30716
|
try {
|
|
30689
30717
|
await requireAuth();
|
|
30690
30718
|
await unpublishSubgraphApi(name);
|
|
@@ -30839,6 +30867,7 @@ async function runSubgraphSchemaCodegen(file, options3, optionSource, defaultTar
|
|
|
30839
30867
|
}
|
|
30840
30868
|
var ORM_ONLY_CODEGEN_FLAGS;
|
|
30841
30869
|
var init_subgraphs2 = __esm(() => {
|
|
30870
|
+
init_esm();
|
|
30842
30871
|
init_subgraph_scaffold();
|
|
30843
30872
|
init_subgraphs();
|
|
30844
30873
|
init_api_client();
|
|
@@ -30850,6 +30879,7 @@ var init_subgraphs2 = __esm(() => {
|
|
|
30850
30879
|
init_output();
|
|
30851
30880
|
init_require_auth();
|
|
30852
30881
|
init_resolve_auth();
|
|
30882
|
+
init_resolve_auth();
|
|
30853
30883
|
init_clarity();
|
|
30854
30884
|
init_subgraph();
|
|
30855
30885
|
init_api();
|
|
@@ -34749,75 +34779,12 @@ var init_node_impl = __esm(() => {
|
|
|
34749
34779
|
init_output();
|
|
34750
34780
|
});
|
|
34751
34781
|
|
|
34752
|
-
// src/
|
|
34753
|
-
|
|
34754
|
-
__export(exports_init, {
|
|
34755
|
-
init: () => init2
|
|
34756
|
-
});
|
|
34757
|
-
import { promises as fs7 } from "node:fs";
|
|
34758
|
-
import path11 from "node:path";
|
|
34759
|
-
async function init2() {
|
|
34760
|
-
const configPath = path11.join(process.cwd(), "secondlayer.config.ts");
|
|
34761
|
-
try {
|
|
34762
|
-
await fs7.access(configPath);
|
|
34763
|
-
console.log(source_default.yellow("⚠ secondlayer.config.ts already exists"));
|
|
34764
|
-
return;
|
|
34765
|
-
} catch {}
|
|
34766
|
-
const hasClarinetProject = await fileExists2("./Clarinet.toml");
|
|
34767
|
-
let config;
|
|
34768
|
-
if (hasClarinetProject) {
|
|
34769
|
-
config = `import { defineConfig } from '@secondlayer/cli';
|
|
34770
|
-
import { clarinet } from '@secondlayer/cli/plugins';
|
|
34771
|
-
|
|
34772
|
-
export default defineConfig({
|
|
34773
|
-
out: './src/generated/contracts.ts',
|
|
34774
|
-
plugins: [
|
|
34775
|
-
clarinet() // Found Clarinet.toml in current directory
|
|
34776
|
-
]
|
|
34777
|
-
});`;
|
|
34778
|
-
} else {
|
|
34779
|
-
config = `import { defineConfig } from '@secondlayer/cli';
|
|
34780
|
-
|
|
34781
|
-
export default defineConfig({
|
|
34782
|
-
out: './src/generated/contracts.ts',
|
|
34783
|
-
plugins: [],
|
|
34784
|
-
});`;
|
|
34785
|
-
}
|
|
34786
|
-
await fs7.writeFile(configPath, config);
|
|
34787
|
-
console.log(source_default.green("✓ Created `secondlayer.config.ts`"));
|
|
34788
|
-
console.log("\nRun `secondlayer generate` to generate type-safe interfaces, functions, and hooks!");
|
|
34789
|
-
}
|
|
34790
|
-
async function fileExists2(filePath) {
|
|
34791
|
-
try {
|
|
34792
|
-
await fs7.access(filePath);
|
|
34793
|
-
return true;
|
|
34794
|
-
} catch {
|
|
34795
|
-
return false;
|
|
34796
|
-
}
|
|
34797
|
-
}
|
|
34798
|
-
var init_init = __esm(() => {
|
|
34799
|
-
init_source();
|
|
34800
|
-
});
|
|
34801
|
-
|
|
34802
|
-
// ../../node_modules/commander/esm.mjs
|
|
34803
|
-
var import__ = __toESM(require_commander(), 1);
|
|
34804
|
-
var {
|
|
34805
|
-
program,
|
|
34806
|
-
createCommand,
|
|
34807
|
-
createArgument,
|
|
34808
|
-
createOption,
|
|
34809
|
-
CommanderError,
|
|
34810
|
-
InvalidArgumentError,
|
|
34811
|
-
InvalidOptionArgumentError,
|
|
34812
|
-
Command,
|
|
34813
|
-
Argument,
|
|
34814
|
-
Option,
|
|
34815
|
-
Help
|
|
34816
|
-
} = import__.default;
|
|
34782
|
+
// src/cli.ts
|
|
34783
|
+
init_esm();
|
|
34817
34784
|
// package.json
|
|
34818
34785
|
var package_default = {
|
|
34819
34786
|
name: "@secondlayer/cli",
|
|
34820
|
-
version: "
|
|
34787
|
+
version: "10.1.0",
|
|
34821
34788
|
repository: {
|
|
34822
34789
|
type: "git",
|
|
34823
34790
|
url: "git+https://github.com/ryanwaits/secondlayer.git",
|
|
@@ -34858,10 +34825,10 @@ var package_default = {
|
|
|
34858
34825
|
"@biomejs/wasm-nodejs": "^1.9.0",
|
|
34859
34826
|
"@dsnp/parquetjs": "^1.8.8",
|
|
34860
34827
|
"@inquirer/prompts": "^8.2.0",
|
|
34861
|
-
"@secondlayer/bundler": "^0.
|
|
34828
|
+
"@secondlayer/bundler": "^0.5.0",
|
|
34862
34829
|
"@secondlayer/scaffold": "^1.5.1",
|
|
34863
34830
|
"@secondlayer/sdk": "^6.51.0",
|
|
34864
|
-
"@secondlayer/shared": "^7.
|
|
34831
|
+
"@secondlayer/shared": "^7.10.0",
|
|
34865
34832
|
"@secondlayer/stacks": "^3.3.1",
|
|
34866
34833
|
"@secondlayer/subgraphs": "^3.26.4",
|
|
34867
34834
|
esbuild: "^0.27.4",
|
|
@@ -35003,7 +34970,7 @@ async function applyProfileUpdate(options, getOptionValueSource) {
|
|
|
35003
34970
|
});
|
|
35004
34971
|
}
|
|
35005
34972
|
function registerAccountCommand(program2) {
|
|
35006
|
-
const account = program2.command("account").description("Manage your account profile");
|
|
34973
|
+
const account = program2.command("account", { hidden: true }).description("Manage your account profile");
|
|
35007
34974
|
addBillingCommand(account);
|
|
35008
34975
|
account.command("get").description("Show your account profile").option("--json", "Output as JSON").action(withErrorHandling(async (options) => showProfile(options.json), { action: "show profile" }));
|
|
35009
34976
|
account.command("update").description("Update your public profile").option("--name <name>", "Set display name").option("--bio <bio>", "Set bio").option("--slug <slug>", "Set public URL slug").option("--notify-reindex-complete", "Email me when a subgraph reindex finishes").option("--no-notify-reindex-complete", "Don't email me when a subgraph reindex finishes").option("--json", "Output as JSON").action(withErrorHandling(async (options, command) => applyProfileUpdate(options, (key) => command.getOptionValueSource(key)), {
|
|
@@ -36536,6 +36503,7 @@ function buildDevnetCompose(opts = {}) {
|
|
|
36536
36503
|
const indexerImage = `ghcr.io/${owner}/secondlayer-indexer:${tag}`;
|
|
36537
36504
|
const dbUrl = "postgres://secondlayer:secondlayer@postgres:5432/secondlayer";
|
|
36538
36505
|
const DEV_SECRETS_KEY = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
|
|
36506
|
+
const DEV_INSTANCE_TOKEN = "dev-instance-token";
|
|
36539
36507
|
return `# Generated by \`sl devnet connect\` — do not edit by hand.
|
|
36540
36508
|
# Secondlayer app services for local clarinet devnet development.
|
|
36541
36509
|
name: secondlayer-devnet
|
|
@@ -36572,6 +36540,8 @@ services:
|
|
|
36572
36540
|
INSTANCE_MODE: oss
|
|
36573
36541
|
DATABASE_URL: ${dbUrl}
|
|
36574
36542
|
PORT: 3800
|
|
36543
|
+
LISTEN_HOST: "0.0.0.0"
|
|
36544
|
+
INSTANCE_TOKEN: ${DEV_INSTANCE_TOKEN}
|
|
36575
36545
|
NODE_ENV: production
|
|
36576
36546
|
DATA_DIR: /data
|
|
36577
36547
|
# Shared across api + subgraph-processor so the secret the api encrypts
|
|
@@ -37317,7 +37287,7 @@ init_resolve_auth();
|
|
|
37317
37287
|
import { SecondLayer as SecondLayer4 } from "@secondlayer/sdk";
|
|
37318
37288
|
var DEFAULT_BASE_URL4 = "https://api.secondlayer.tools";
|
|
37319
37289
|
function registerKeysCommand(program2) {
|
|
37320
|
-
const keys = program2.command("keys").description("Manage API keys");
|
|
37290
|
+
const keys = program2.command("keys", { hidden: true }).description("Manage API keys");
|
|
37321
37291
|
keys.command("create").description("Mint a scoped streams/index read key (requires an account-level key)").option("--product <product>", "Key scope: streams or index", "streams").option("--name <name>", "Optional label for the key").option("--json", "Print the full response as JSON").action(async (o3) => {
|
|
37322
37292
|
if (o3.product && o3.product !== "streams" && o3.product !== "index") {
|
|
37323
37293
|
error("--product must be 'streams' or 'index'");
|
|
@@ -38607,7 +38577,7 @@ async function writeActiveProject(slug, cwd) {
|
|
|
38607
38577
|
// src/commands/whoami.ts
|
|
38608
38578
|
init_resolve_auth();
|
|
38609
38579
|
function registerWhoamiCommand(program2) {
|
|
38610
|
-
program2.command("whoami").description("Show the active account, credential source, and project").option("--json", "Output as JSON").action(async (options3) => {
|
|
38580
|
+
program2.command("whoami", { hidden: true }).description("Show the active account, credential source, and project").option("--json", "Output as JSON").action(async (options3) => {
|
|
38611
38581
|
let auth;
|
|
38612
38582
|
try {
|
|
38613
38583
|
auth = await resolveAuth();
|
|
@@ -38663,7 +38633,7 @@ init_http();
|
|
|
38663
38633
|
init_output();
|
|
38664
38634
|
init_session();
|
|
38665
38635
|
function registerLogoutCommand(program2) {
|
|
38666
|
-
program2.command("logout").description("Log out and revoke the local session").action(async () => {
|
|
38636
|
+
program2.command("logout", { hidden: true }).description("Log out and revoke the local session").action(async () => {
|
|
38667
38637
|
const session = await readSession();
|
|
38668
38638
|
if (!session) {
|
|
38669
38639
|
info("Not logged in.");
|
|
@@ -38685,7 +38655,7 @@ init_config2();
|
|
|
38685
38655
|
init_http();
|
|
38686
38656
|
init_output();
|
|
38687
38657
|
function registerProjectCommand(program2) {
|
|
38688
|
-
const project = program2.command("projects").alias("project").description("Manage your Secondlayer project");
|
|
38658
|
+
const project = program2.command("projects", { hidden: true }).alias("project").description("Manage your Secondlayer project");
|
|
38689
38659
|
project.command("list").alias("ls").description("List projects in your account").option("--json", "Output as JSON").action(async (options3) => {
|
|
38690
38660
|
try {
|
|
38691
38661
|
const res = await httpPlatform("/api/projects");
|
|
@@ -38764,6 +38734,15 @@ import {
|
|
|
38764
38734
|
compareRangeDigests,
|
|
38765
38735
|
computeRangeDigest
|
|
38766
38736
|
} from "@secondlayer/shared/archive/range-digest";
|
|
38737
|
+
import {
|
|
38738
|
+
comparePartitionSemanticDigests,
|
|
38739
|
+
computePartitionSemanticDigest
|
|
38740
|
+
} from "@secondlayer/shared/archive/semantic-digest-builder";
|
|
38741
|
+
import {
|
|
38742
|
+
datasetMatchesTarget,
|
|
38743
|
+
parseVerifyTarget,
|
|
38744
|
+
reportVerify
|
|
38745
|
+
} from "@secondlayer/shared/coverage";
|
|
38767
38746
|
import { getDb as getDb2 } from "@secondlayer/shared/db";
|
|
38768
38747
|
|
|
38769
38748
|
// src/lib/archive-reference.ts
|
|
@@ -38840,6 +38819,7 @@ function checkSignature(manifest, publicKeyPem, insecure) {
|
|
|
38840
38819
|
};
|
|
38841
38820
|
}
|
|
38842
38821
|
}
|
|
38822
|
+
var HOSTED_API = "https://api.secondlayer.tools";
|
|
38843
38823
|
async function resolvePublicKey(explicitPem, apiUrl) {
|
|
38844
38824
|
if (explicitPem)
|
|
38845
38825
|
return explicitPem;
|
|
@@ -38853,6 +38833,16 @@ async function resolvePublicKey(explicitPem, apiUrl) {
|
|
|
38853
38833
|
return;
|
|
38854
38834
|
}
|
|
38855
38835
|
}
|
|
38836
|
+
async function resolveArchivePublicKey(input4) {
|
|
38837
|
+
if (input4.explicitPem)
|
|
38838
|
+
return input4.explicitPem;
|
|
38839
|
+
if (input4.envPem)
|
|
38840
|
+
return input4.envPem;
|
|
38841
|
+
if (input4.allowHostedApi) {
|
|
38842
|
+
return resolvePublicKey(undefined, HOSTED_API);
|
|
38843
|
+
}
|
|
38844
|
+
return;
|
|
38845
|
+
}
|
|
38856
38846
|
async function fetchVerifiedPartition(reference, partition) {
|
|
38857
38847
|
let bytes;
|
|
38858
38848
|
if (reference.isRemote) {
|
|
@@ -38896,17 +38886,21 @@ function statusLabel(status2) {
|
|
|
38896
38886
|
return yellow("missing");
|
|
38897
38887
|
}
|
|
38898
38888
|
function registerVerifyCommand(program2) {
|
|
38899
|
-
program2.command("verify").description("Compare local chain data against a signed archive (read-only; nothing is uploaded)").requiredOption("--against <manifest>", "archive manifest: an https URL or a local file path").option("--from-block <n>", "first height to check").option("--to-block <n>", "last height to check").option("--counts", "also compare transaction/event row counts (slower)").option("--public-key <pem>", "pin the signing key instead of fetching it").option("--insecure", "skip the manifest signature check (not recommended)").option("--json", "Output as JSON").addHelpText("after", `
|
|
38889
|
+
program2.command("verify").description("Compare local chain data against a signed archive (read-only; nothing is uploaded)").argument("[target]", "all | raw | decode:<name> | subgraph:<name>", "raw").option("--quick", "coverage/identity only (default)").option("--deep", "include semantic / scratch replay where available").option("--anchor", "require a verified archive signature").requiredOption("--against <manifest>", "archive manifest: an https URL or a local file path").option("--from-block <n>", "first height to check").option("--to-block <n>", "last height to check").option("--counts", "also compare transaction/event row counts (slower)").option("--semantic", "also recompute per-partition semantic digests locally and compare (slow: full re-stream)").option("--public-key <pem>", "pin the signing key instead of fetching it").option("--insecure", "skip the manifest signature check (not recommended)").option("--json", "Output as JSON").addHelpText("after", `
|
|
38900
38890
|
Examples:
|
|
38901
38891
|
$ sl verify --against https://archive.secondlayer.tools/.../snapshots/<digest>.json
|
|
38902
|
-
$ sl verify --against ./snapshot.json --from-block 8000000 --to-block 8499999
|
|
38903
|
-
$ sl verify --against ./snapshot.json --
|
|
38892
|
+
$ sl verify raw --against ./snapshot.json --from-block 8000000 --to-block 8499999
|
|
38893
|
+
$ sl verify all --against ./snapshot.json --json
|
|
38894
|
+
$ sl verify decode:ft_transfer --against ./snapshot.json --quick
|
|
38895
|
+
$ sl verify subgraph:sbtc --against ./snapshot.json --deep
|
|
38904
38896
|
|
|
38905
38897
|
Exit codes:
|
|
38906
38898
|
0 local data matches the archive
|
|
38907
38899
|
1 divergence found (divergent ranges are listed)
|
|
38908
|
-
2 unanchored — reference unavailable or unverifiable`).action(async (opts) => {
|
|
38900
|
+
2 unanchored — reference unavailable or unverifiable`).action(async (targetArg, opts) => {
|
|
38909
38901
|
try {
|
|
38902
|
+
const target = parseVerifyTarget(targetArg);
|
|
38903
|
+
const mode = opts.deep ? "deep" : opts.anchor ? "anchor" : "quick";
|
|
38910
38904
|
const fromBlock = opts.fromBlock === undefined ? undefined : parseHeight2(opts.fromBlock, "--from-block");
|
|
38911
38905
|
const toBlock = opts.toBlock === undefined ? undefined : parseHeight2(opts.toBlock, "--to-block");
|
|
38912
38906
|
if (fromBlock !== undefined && toBlock !== undefined && toBlock < fromBlock) {
|
|
@@ -38922,6 +38916,12 @@ Exit codes:
|
|
|
38922
38916
|
output({
|
|
38923
38917
|
json: opts.json,
|
|
38924
38918
|
data: {
|
|
38919
|
+
...reportVerify({
|
|
38920
|
+
target,
|
|
38921
|
+
mode,
|
|
38922
|
+
anchored: false,
|
|
38923
|
+
detail: signature.reason
|
|
38924
|
+
}),
|
|
38925
38925
|
status: "unanchored",
|
|
38926
38926
|
reference: origin,
|
|
38927
38927
|
reason: signature.reason
|
|
@@ -38931,6 +38931,8 @@ Exit codes:
|
|
|
38931
38931
|
process.exit(VERIFY_EXIT.UNANCHORED);
|
|
38932
38932
|
}
|
|
38933
38933
|
const reference = (manifest.range_digests ?? []).filter((d2) => {
|
|
38934
|
+
if (!datasetMatchesTarget(d2.dataset, target))
|
|
38935
|
+
return false;
|
|
38934
38936
|
if (fromBlock !== undefined && d2.to_block < fromBlock)
|
|
38935
38937
|
return false;
|
|
38936
38938
|
if (toBlock !== undefined && d2.from_block > toBlock)
|
|
@@ -38976,13 +38978,60 @@ Exit codes:
|
|
|
38976
38978
|
});
|
|
38977
38979
|
}
|
|
38978
38980
|
}
|
|
38979
|
-
const
|
|
38981
|
+
const semanticChecks = [];
|
|
38982
|
+
const referenceSemantic = (manifest.partition_semantic_digests ?? []).filter((d2) => {
|
|
38983
|
+
if (!datasetMatchesTarget(d2.dataset, target))
|
|
38984
|
+
return false;
|
|
38985
|
+
if (fromBlock !== undefined && d2.to_block < fromBlock)
|
|
38986
|
+
return false;
|
|
38987
|
+
if (toBlock !== undefined && d2.from_block > toBlock)
|
|
38988
|
+
return false;
|
|
38989
|
+
return true;
|
|
38990
|
+
});
|
|
38991
|
+
if (opts.semantic || opts.deep) {
|
|
38992
|
+
if (referenceSemantic.length === 0) {
|
|
38993
|
+
warn("Reference publishes no semantic digests; --semantic had nothing to compare.");
|
|
38994
|
+
} else {
|
|
38995
|
+
note(`Recomputing ${referenceSemantic.length} partition semantic digests (this is the slow pass).`);
|
|
38996
|
+
const localSemantic = [];
|
|
38997
|
+
let semanticDone = 0;
|
|
38998
|
+
for (const partition of referenceSemantic) {
|
|
38999
|
+
localSemantic.push(await computePartitionSemanticDigest(db, partition.dataset, partition.from_block, partition.to_block));
|
|
39000
|
+
semanticDone++;
|
|
39001
|
+
if (semanticDone % 5 === 0 || semanticDone === referenceSemantic.length) {
|
|
39002
|
+
note(` checked ${semanticDone}/${referenceSemantic.length} partitions`);
|
|
39003
|
+
}
|
|
39004
|
+
}
|
|
39005
|
+
semanticChecks.push(...comparePartitionSemanticDigests(localSemantic, referenceSemantic));
|
|
39006
|
+
}
|
|
39007
|
+
}
|
|
39008
|
+
const all2 = [
|
|
39009
|
+
...comparisons,
|
|
39010
|
+
...countChecks,
|
|
39011
|
+
...semanticChecks.map((s2) => ({
|
|
39012
|
+
dataset: s2.dataset,
|
|
39013
|
+
from_block: s2.from_block,
|
|
39014
|
+
to_block: s2.to_block,
|
|
39015
|
+
status: s2.status,
|
|
39016
|
+
expected_digest: s2.expected_digest,
|
|
39017
|
+
actual_digest: s2.actual_digest,
|
|
39018
|
+
expected_rows: s2.expected_rows,
|
|
39019
|
+
actual_rows: s2.actual_rows
|
|
39020
|
+
}))
|
|
39021
|
+
];
|
|
38980
39022
|
const diverged = all2.filter((c4) => c4.status !== "match");
|
|
39023
|
+
const verdict = reportVerify({
|
|
39024
|
+
target,
|
|
39025
|
+
mode,
|
|
39026
|
+
diverged: diverged.length > 0
|
|
39027
|
+
});
|
|
38981
39028
|
const report = {
|
|
38982
|
-
|
|
39029
|
+
...verdict,
|
|
39030
|
+
status: verdict.status,
|
|
38983
39031
|
reference: origin,
|
|
38984
39032
|
signature_verified: signature.verified,
|
|
38985
39033
|
ranges_checked: all2.length,
|
|
39034
|
+
semantic_checks: semanticChecks.length,
|
|
38986
39035
|
divergent_ranges: diverged
|
|
38987
39036
|
};
|
|
38988
39037
|
output({
|
|
@@ -39281,7 +39330,35 @@ import {
|
|
|
39281
39330
|
} from "@secondlayer/shared/archive/copy-loader";
|
|
39282
39331
|
import { computeRangeDigest as computeRangeDigest3 } from "@secondlayer/shared/archive/range-digest";
|
|
39283
39332
|
import { getDb as getDb4, getRawClient, sql as sql3 } from "@secondlayer/shared/db";
|
|
39333
|
+
|
|
39334
|
+
// src/lib/bootstrap-resume.ts
|
|
39335
|
+
function planTornImport(input4) {
|
|
39336
|
+
if (input4.hasIndexProgress) {
|
|
39337
|
+
return {
|
|
39338
|
+
action: "refuse",
|
|
39339
|
+
reason: "This database already holds a completed bootstrap."
|
|
39340
|
+
};
|
|
39341
|
+
}
|
|
39342
|
+
if (input4.maxBlockHeight === null || input4.maxBlockHeight <= 0) {
|
|
39343
|
+
return { action: "fresh" };
|
|
39344
|
+
}
|
|
39345
|
+
const blockPartitions = input4.partitions.filter((p2) => p2.dataset === "blocks").sort((a4, b3) => a4.from_block - b3.from_block);
|
|
39346
|
+
const sealed = blockPartitions.filter((p2) => p2.to_block <= (input4.maxBlockHeight ?? 0));
|
|
39347
|
+
const skipThrough = sealed.length > 0 ? Math.max(...sealed.map((p2) => p2.to_block)) : 0;
|
|
39348
|
+
const torn = blockPartitions.find((p2) => p2.from_block <= (input4.maxBlockHeight ?? 0) && p2.to_block > (input4.maxBlockHeight ?? 0));
|
|
39349
|
+
return {
|
|
39350
|
+
action: "resume",
|
|
39351
|
+
truncateFrom: torn ? torn.from_block : null,
|
|
39352
|
+
skipThrough
|
|
39353
|
+
};
|
|
39354
|
+
}
|
|
39355
|
+
function partitionIsLoaded(partition, skipThrough) {
|
|
39356
|
+
return partition.to_block <= skipThrough;
|
|
39357
|
+
}
|
|
39358
|
+
|
|
39359
|
+
// src/commands/bootstrap.ts
|
|
39284
39360
|
init_output();
|
|
39361
|
+
init_resolve_auth();
|
|
39285
39362
|
var BOOTSTRAP_EXIT = {
|
|
39286
39363
|
OK: 0,
|
|
39287
39364
|
INCOMPLETE: 1,
|
|
@@ -39342,8 +39419,8 @@ async function loadPartition(reference, partition, rawClient) {
|
|
|
39342
39419
|
}
|
|
39343
39420
|
return written;
|
|
39344
39421
|
}
|
|
39345
|
-
function
|
|
39346
|
-
|
|
39422
|
+
function attachBootstrapCommand(cmd) {
|
|
39423
|
+
return cmd.requiredOption("--against <manifest>", "archive manifest: an https URL or a local file path").option("--to-block <n>", "stop at this height instead of the archive tip").option("--public-key <pem>", "pin the signing key instead of fetching it").option("-y, --yes", "skip the confirmation prompt").option("--json", "Output as JSON").addHelpText("after", `
|
|
39347
39424
|
Examples:
|
|
39348
39425
|
$ sl bootstrap --against https://archive.secondlayer.tools/.../snapshots/<digest>.json
|
|
39349
39426
|
$ sl bootstrap --against ./snapshot.json --to-block 4000000 --yes
|
|
@@ -39354,7 +39431,11 @@ Exit codes:
|
|
|
39354
39431
|
2 refused (non-empty target, or an untrusted reference)`).action(async (opts) => {
|
|
39355
39432
|
try {
|
|
39356
39433
|
const reference = await loadReference(opts.against);
|
|
39357
|
-
const publicKey = await
|
|
39434
|
+
const publicKey = await resolveArchivePublicKey({
|
|
39435
|
+
explicitPem: opts.publicKey,
|
|
39436
|
+
envPem: process.env.ARCHIVE_SIGNING_PUBLIC_KEY ?? process.env.STREAMS_SIGNING_PUBLIC_KEY,
|
|
39437
|
+
allowHostedApi: !isOssMode()
|
|
39438
|
+
});
|
|
39358
39439
|
const signature = checkSignature(reference.manifest, publicKey, false);
|
|
39359
39440
|
if (!signature.verified) {
|
|
39360
39441
|
printError(`Refusing to bootstrap from an untrusted archive: ${signature.reason}.`, {
|
|
@@ -39363,15 +39444,31 @@ Exit codes:
|
|
|
39363
39444
|
process.exit(BOOTSTRAP_EXIT.REFUSED);
|
|
39364
39445
|
}
|
|
39365
39446
|
const db = getDb4();
|
|
39366
|
-
const
|
|
39367
|
-
|
|
39368
|
-
|
|
39447
|
+
const progress = await db.selectFrom("index_progress").select("network").executeTakeFirst();
|
|
39448
|
+
const maxHeightRow = await db.selectFrom("blocks").select(({ fn }) => fn.max("height").as("max")).executeTakeFirst();
|
|
39449
|
+
const maxBlockHeight = maxHeightRow?.max === null || maxHeightRow?.max === undefined ? null : Number(maxHeightRow.max);
|
|
39450
|
+
const toBlock = opts.toBlock === undefined ? undefined : Number(opts.toBlock);
|
|
39451
|
+
const declared = (reference.manifest.partitions ?? []).filter((p2) => toBlock === undefined || p2.to_block <= toBlock);
|
|
39452
|
+
const resume = planTornImport({
|
|
39453
|
+
hasIndexProgress: !!progress,
|
|
39454
|
+
maxBlockHeight,
|
|
39455
|
+
partitions: declared
|
|
39456
|
+
});
|
|
39457
|
+
if (resume.action === "refuse") {
|
|
39458
|
+
printError(resume.reason, {
|
|
39369
39459
|
hint: "Bootstrap is for a fresh instance. To fix an existing one, use `sl repair`."
|
|
39370
39460
|
});
|
|
39371
39461
|
process.exit(BOOTSTRAP_EXIT.REFUSED);
|
|
39372
39462
|
}
|
|
39373
|
-
|
|
39374
|
-
|
|
39463
|
+
if (resume.action === "resume" && resume.truncateFrom !== null) {
|
|
39464
|
+
await sql3`DELETE FROM events WHERE block_height >= ${resume.truncateFrom}`.execute(db);
|
|
39465
|
+
await sql3`DELETE FROM transactions WHERE block_height >= ${resume.truncateFrom}`.execute(db);
|
|
39466
|
+
await sql3`DELETE FROM blocks WHERE height >= ${resume.truncateFrom}`.execute(db);
|
|
39467
|
+
note(`Resuming torn import: truncated from height ${resume.truncateFrom}.`);
|
|
39468
|
+
} else if (resume.action === "resume") {
|
|
39469
|
+
note(`Resuming import after sealed height ${resume.skipThrough.toLocaleString()}.`);
|
|
39470
|
+
}
|
|
39471
|
+
const partitions = declared.filter((p2) => resume.action === "fresh" || !partitionIsLoaded(p2, resume.skipThrough));
|
|
39375
39472
|
if (partitions.length === 0) {
|
|
39376
39473
|
printError("The archive has no partitions for that range.", {
|
|
39377
39474
|
hint: "Check --to-block against the manifest's coverage."
|
|
@@ -39496,9 +39593,217 @@ Exit codes:
|
|
|
39496
39593
|
}
|
|
39497
39594
|
});
|
|
39498
39595
|
}
|
|
39596
|
+
function registerBootstrapCommand(program2) {
|
|
39597
|
+
attachBootstrapCommand(program2.command("bootstrap").description("Restore chain history from a verified archive instead of syncing from genesis"));
|
|
39598
|
+
}
|
|
39599
|
+
// src/lib/instance-init.ts
|
|
39600
|
+
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
39601
|
+
import { existsSync as existsSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync7 } from "node:fs";
|
|
39602
|
+
import { resolve as resolve13 } from "node:path";
|
|
39603
|
+
import { generateEd25519KeyPair } from "@secondlayer/shared/crypto/ed25519";
|
|
39604
|
+
import { INSTANCE_NETWORKS } from "@secondlayer/shared/db/queries/instance";
|
|
39605
|
+
var INSTANCE_ENV_FILE = ".env.local";
|
|
39606
|
+
function parseInstanceNetwork(value) {
|
|
39607
|
+
const network = value.trim().toLowerCase();
|
|
39608
|
+
if (INSTANCE_NETWORKS.includes(network)) {
|
|
39609
|
+
return network;
|
|
39610
|
+
}
|
|
39611
|
+
throw new Error(`network must be mainnet, testnet, or devnet (got ${value})`);
|
|
39612
|
+
}
|
|
39613
|
+
function generateInstanceToken() {
|
|
39614
|
+
return randomBytes3(32).toString("hex");
|
|
39615
|
+
}
|
|
39616
|
+
function generateSecretsKey() {
|
|
39617
|
+
return randomBytes3(32).toString("hex");
|
|
39618
|
+
}
|
|
39619
|
+
function generateSigningPrivateKey() {
|
|
39620
|
+
return generateEd25519KeyPair().privateKeyPem;
|
|
39621
|
+
}
|
|
39622
|
+
function escapeEnvValue(value) {
|
|
39623
|
+
if (!/[\s#"']/.test(value) && !value.includes("\\"))
|
|
39624
|
+
return value;
|
|
39625
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n")}"`;
|
|
39626
|
+
}
|
|
39627
|
+
function unescapeEnvValue(raw) {
|
|
39628
|
+
let value = raw.trim();
|
|
39629
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
39630
|
+
value = value.slice(1, -1);
|
|
39631
|
+
}
|
|
39632
|
+
return value.replace(/\\n/g, `
|
|
39633
|
+
`).replace(/\\"/g, '"').replace(/\\\\/g, "\\");
|
|
39634
|
+
}
|
|
39635
|
+
function readEnvValue(envPath, key) {
|
|
39636
|
+
if (!existsSync8(envPath))
|
|
39637
|
+
return null;
|
|
39638
|
+
const match = readFileSync7(envPath, "utf8").match(new RegExp(`^${key}=(.*)$`, "m"));
|
|
39639
|
+
return match ? unescapeEnvValue(match[1]) : null;
|
|
39640
|
+
}
|
|
39641
|
+
function buildInstanceEnv(input4) {
|
|
39642
|
+
const token = input4.existing?.INSTANCE_TOKEN || input4.existing?.SL_API_KEY || generateInstanceToken();
|
|
39643
|
+
const secrets = input4.existing?.SECONDLAYER_SECRETS_KEY || generateSecretsKey();
|
|
39644
|
+
const signing = input4.existing?.STREAMS_SIGNING_PRIVATE_KEY || input4.existing?.SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY || generateSigningPrivateKey();
|
|
39645
|
+
const apiUrl = input4.apiUrl ?? "http://127.0.0.1:3800";
|
|
39646
|
+
const archiveKey = input4.existing?.ARCHIVE_SIGNING_PUBLIC_KEY;
|
|
39647
|
+
return {
|
|
39648
|
+
INSTANCE_MODE: "oss",
|
|
39649
|
+
STACKS_NETWORK: input4.network,
|
|
39650
|
+
INSTANCE_TOKEN: token,
|
|
39651
|
+
SECONDLAYER_SECRETS_KEY: secrets,
|
|
39652
|
+
STREAMS_SIGNING_PRIVATE_KEY: signing,
|
|
39653
|
+
SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY: signing,
|
|
39654
|
+
ALLOW_UNSIGNED_WEBHOOKS: "false",
|
|
39655
|
+
SL_API_URL: input4.existing?.SL_API_URL || apiUrl,
|
|
39656
|
+
SL_API_KEY: token,
|
|
39657
|
+
...archiveKey ? { ARCHIVE_SIGNING_PUBLIC_KEY: archiveKey } : {}
|
|
39658
|
+
};
|
|
39659
|
+
}
|
|
39660
|
+
function renderInstanceEnv(env2) {
|
|
39661
|
+
const lines2 = [
|
|
39662
|
+
"# Generated by sl init. Do not commit.",
|
|
39663
|
+
`INSTANCE_MODE=${env2.INSTANCE_MODE}`,
|
|
39664
|
+
`STACKS_NETWORK=${env2.STACKS_NETWORK}`,
|
|
39665
|
+
`INSTANCE_TOKEN=${env2.INSTANCE_TOKEN}`,
|
|
39666
|
+
`SECONDLAYER_SECRETS_KEY=${env2.SECONDLAYER_SECRETS_KEY}`,
|
|
39667
|
+
`STREAMS_SIGNING_PRIVATE_KEY=${escapeEnvValue(env2.STREAMS_SIGNING_PRIVATE_KEY)}`,
|
|
39668
|
+
`SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY=${escapeEnvValue(env2.SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY)}`,
|
|
39669
|
+
`ALLOW_UNSIGNED_WEBHOOKS=${env2.ALLOW_UNSIGNED_WEBHOOKS}`,
|
|
39670
|
+
`SL_API_URL=${env2.SL_API_URL}`,
|
|
39671
|
+
`SL_API_KEY=${env2.SL_API_KEY}`
|
|
39672
|
+
];
|
|
39673
|
+
if (env2.ARCHIVE_SIGNING_PUBLIC_KEY) {
|
|
39674
|
+
lines2.push(`ARCHIVE_SIGNING_PUBLIC_KEY=${escapeEnvValue(env2.ARCHIVE_SIGNING_PUBLIC_KEY)}`);
|
|
39675
|
+
}
|
|
39676
|
+
lines2.push("");
|
|
39677
|
+
return lines2.join(`
|
|
39678
|
+
`);
|
|
39679
|
+
}
|
|
39680
|
+
function writeInstanceEnv(cwd, env2, file = INSTANCE_ENV_FILE) {
|
|
39681
|
+
const path11 = resolve13(cwd, file);
|
|
39682
|
+
writeFileSync7(path11, renderInstanceEnv(env2), { mode: 384 });
|
|
39683
|
+
return path11;
|
|
39684
|
+
}
|
|
39685
|
+
function loadExistingInstanceEnv(cwd, file = INSTANCE_ENV_FILE) {
|
|
39686
|
+
const path11 = resolve13(cwd, file);
|
|
39687
|
+
return {
|
|
39688
|
+
INSTANCE_TOKEN: readEnvValue(path11, "INSTANCE_TOKEN"),
|
|
39689
|
+
SECONDLAYER_SECRETS_KEY: readEnvValue(path11, "SECONDLAYER_SECRETS_KEY"),
|
|
39690
|
+
STREAMS_SIGNING_PRIVATE_KEY: readEnvValue(path11, "STREAMS_SIGNING_PRIVATE_KEY"),
|
|
39691
|
+
SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY: readEnvValue(path11, "SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY"),
|
|
39692
|
+
SL_API_URL: readEnvValue(path11, "SL_API_URL"),
|
|
39693
|
+
SL_API_KEY: readEnvValue(path11, "SL_API_KEY"),
|
|
39694
|
+
ARCHIVE_SIGNING_PUBLIC_KEY: readEnvValue(path11, "ARCHIVE_SIGNING_PUBLIC_KEY")
|
|
39695
|
+
};
|
|
39696
|
+
}
|
|
39697
|
+
|
|
39698
|
+
// src/commands/init.ts
|
|
39699
|
+
init_output();
|
|
39700
|
+
function registerInitCommand(program2) {
|
|
39701
|
+
program2.command("init").description("Write local env (token, secrets key, webhook signing key). Idempotent.").option("--network <network>", "mainnet, testnet, or devnet", process.env.STACKS_NETWORK ?? "mainnet").option("--api-url <url>", "Local API URL", "http://127.0.0.1:3800").option("--force", "Overwrite generated values even if .env.local exists").action((opts) => {
|
|
39702
|
+
const network = parseInstanceNetwork(opts.network);
|
|
39703
|
+
const existing = opts.force ? {} : loadExistingInstanceEnv(process.cwd());
|
|
39704
|
+
const env2 = buildInstanceEnv({
|
|
39705
|
+
network,
|
|
39706
|
+
existing,
|
|
39707
|
+
apiUrl: opts.apiUrl
|
|
39708
|
+
});
|
|
39709
|
+
const path11 = writeInstanceEnv(process.cwd(), env2);
|
|
39710
|
+
success(`Wrote ${INSTANCE_ENV_FILE}`);
|
|
39711
|
+
note(formatKeyValue([
|
|
39712
|
+
["network", env2.STACKS_NETWORK],
|
|
39713
|
+
["api", env2.SL_API_URL],
|
|
39714
|
+
["env", path11]
|
|
39715
|
+
]));
|
|
39716
|
+
note("Export these or pass --env-file to compose. Do not commit the file.");
|
|
39717
|
+
});
|
|
39718
|
+
}
|
|
39719
|
+
// src/lib/observer-stanza.ts
|
|
39720
|
+
var OBSERVER_MODES = ["indexer", "signer-shared"];
|
|
39721
|
+
var RECOVERY_SOURCES = ["journal", "archive"];
|
|
39722
|
+
|
|
39723
|
+
class UnsupportedObserverError extends Error {
|
|
39724
|
+
name = "UnsupportedObserverError";
|
|
39725
|
+
}
|
|
39726
|
+
function parseObserverMode(value) {
|
|
39727
|
+
const mode = value.trim().toLowerCase();
|
|
39728
|
+
if (OBSERVER_MODES.includes(mode)) {
|
|
39729
|
+
return mode;
|
|
39730
|
+
}
|
|
39731
|
+
throw new UnsupportedObserverError(`observer mode must be indexer or signer-shared (got ${value})`);
|
|
39732
|
+
}
|
|
39733
|
+
function parseRecoverySource(value) {
|
|
39734
|
+
const source = value.trim().toLowerCase();
|
|
39735
|
+
if (RECOVERY_SOURCES.includes(source)) {
|
|
39736
|
+
return source;
|
|
39737
|
+
}
|
|
39738
|
+
throw new UnsupportedObserverError(`recovery source must be journal or archive (got ${value})`);
|
|
39739
|
+
}
|
|
39740
|
+
function parseObserverEndpoint(value) {
|
|
39741
|
+
const trimmed = value.trim();
|
|
39742
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed) || trimmed.startsWith("unix:")) {
|
|
39743
|
+
throw new UnsupportedObserverError(`observer endpoint must be host:port, not a URL (${value})`);
|
|
39744
|
+
}
|
|
39745
|
+
const match = trimmed.match(/^([a-zA-Z0-9._-]+):(\d+)$/);
|
|
39746
|
+
if (!match) {
|
|
39747
|
+
throw new UnsupportedObserverError(`observer endpoint must be host:port (got ${value})`);
|
|
39748
|
+
}
|
|
39749
|
+
const port = Number(match[2]);
|
|
39750
|
+
if (port < 1 || port > 65535) {
|
|
39751
|
+
throw new UnsupportedObserverError(`observer port out of range (${port})`);
|
|
39752
|
+
}
|
|
39753
|
+
return trimmed;
|
|
39754
|
+
}
|
|
39755
|
+
var LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "::1"]);
|
|
39756
|
+
function validateObserverStanza(input4) {
|
|
39757
|
+
const endpoint = parseObserverEndpoint(input4.endpoint);
|
|
39758
|
+
const host = endpoint.slice(0, endpoint.lastIndexOf(":"));
|
|
39759
|
+
if (input4.network !== "devnet" && LOOPBACK_HOSTS.has(host.toLowerCase())) {
|
|
39760
|
+
throw new UnsupportedObserverError(`loopback observer endpoint is not container-visible on ${input4.network}; use a docker DNS name such as indexer:3700`);
|
|
39761
|
+
}
|
|
39762
|
+
if (input4.mode === "signer-shared" && !input4.recovery) {
|
|
39763
|
+
throw new UnsupportedObserverError("signer-shared mode requires --recovery journal or --recovery archive");
|
|
39764
|
+
}
|
|
39765
|
+
return { endpoint };
|
|
39766
|
+
}
|
|
39767
|
+
function renderObserverStanza(input4) {
|
|
39768
|
+
const { endpoint } = validateObserverStanza(input4);
|
|
39769
|
+
const indexer = input4.mode === "indexer";
|
|
39770
|
+
const timeoutMs = indexer ? 2000 : 500;
|
|
39771
|
+
const disableRetries = !indexer;
|
|
39772
|
+
const comment = indexer ? "# Pure indexer: retry delivery. A slow observer can stall the node." : "# Signer-shared: do not retry. Missed blocks refill from the journal/archive.";
|
|
39773
|
+
return [
|
|
39774
|
+
comment,
|
|
39775
|
+
"[[events_observer]]",
|
|
39776
|
+
`endpoint = "${endpoint}"`,
|
|
39777
|
+
'events_keys = ["*"]',
|
|
39778
|
+
`timeout_ms = ${timeoutMs}`,
|
|
39779
|
+
`disable_retries = ${disableRetries}`,
|
|
39780
|
+
""
|
|
39781
|
+
].join(`
|
|
39782
|
+
`);
|
|
39783
|
+
}
|
|
39784
|
+
function defaultObserverEndpoint(network) {
|
|
39785
|
+
if (network === "devnet")
|
|
39786
|
+
return "127.0.0.1:3700";
|
|
39787
|
+
return "indexer:3700";
|
|
39788
|
+
}
|
|
39789
|
+
|
|
39790
|
+
// src/commands/observer.ts
|
|
39791
|
+
init_output();
|
|
39792
|
+
function registerObserverCommand(program2) {
|
|
39793
|
+
program2.command("observer").description("Print the Stacks events_observer stanza for this node mode").option("--mode <mode>", "indexer (retry) or signer-shared (no retry)", "indexer").option("--endpoint <host:port>", "Observer callback the node will POST to").option("--recovery <source>", "Required for signer-shared: journal or archive").option("--network <network>", "mainnet, testnet, or devnet", process.env.STACKS_NETWORK ?? "mainnet").action((opts) => {
|
|
39794
|
+
const mode = parseObserverMode(opts.mode);
|
|
39795
|
+
const network = parseInstanceNetwork(opts.network);
|
|
39796
|
+
const endpoint = opts.endpoint ?? defaultObserverEndpoint(network);
|
|
39797
|
+
const recovery = opts.recovery ? parseRecoverySource(opts.recovery) : undefined;
|
|
39798
|
+
if (mode === "signer-shared") {
|
|
39799
|
+
note("Signer-shared nodes skip observer retries. Pair with a journal or archive for completeness.");
|
|
39800
|
+
}
|
|
39801
|
+
writeData(renderObserverStanza({ mode, endpoint, network, recovery }));
|
|
39802
|
+
});
|
|
39803
|
+
}
|
|
39499
39804
|
// src/cli.ts
|
|
39500
39805
|
var { version } = package_default;
|
|
39501
|
-
program.name("secondlayer").alias("sl").description("
|
|
39806
|
+
program.name("secondlayer").alias("sl").description("Secondlayer CLI — self-hosted Stacks data runtime").version(version).option("--network <network>", "Override network (local, testnet, mainnet)").option("--api-key <key>", "API credential (overrides SL_API_KEY)").option("--api-url <url>", "API endpoint (overrides SL_API_URL)").showSuggestionAfterError(true).showHelpAfterError("(run `sl --help` to see available commands)");
|
|
39502
39807
|
program.hook("preAction", (thisCommand) => {
|
|
39503
39808
|
const { network, apiKey, apiUrl } = thisCommand.opts();
|
|
39504
39809
|
if (network)
|
|
@@ -39510,16 +39815,15 @@ program.hook("preAction", (thisCommand) => {
|
|
|
39510
39815
|
});
|
|
39511
39816
|
program.addHelpText("after", `
|
|
39512
39817
|
Quickstart:
|
|
39513
|
-
$ sl
|
|
39818
|
+
$ sl init --network mainnet
|
|
39819
|
+
$ sl bootstrap --against <manifest>
|
|
39514
39820
|
$ sl subgraphs create my-watcher --template sip-010-balances
|
|
39515
39821
|
$ sl subgraphs deploy subgraphs/my-watcher.ts
|
|
39516
|
-
$ sl subgraphs status my-watcher
|
|
39517
39822
|
`);
|
|
39518
39823
|
program.commandsGroup("Getting started:");
|
|
39519
|
-
program
|
|
39520
|
-
|
|
39521
|
-
|
|
39522
|
-
});
|
|
39824
|
+
registerInitCommand(program);
|
|
39825
|
+
registerBootstrapCommand(program);
|
|
39826
|
+
registerObserverCommand(program);
|
|
39523
39827
|
registerLoginCommand(program);
|
|
39524
39828
|
registerLogoutCommand(program);
|
|
39525
39829
|
registerWhoamiCommand(program);
|
|
@@ -39552,7 +39856,6 @@ registerStatusCommand(program);
|
|
|
39552
39856
|
registerDoctorCommand(program);
|
|
39553
39857
|
registerConfigCommand(program);
|
|
39554
39858
|
registerContextCommand(program);
|
|
39555
|
-
registerBootstrapCommand(program);
|
|
39556
39859
|
registerVerifyCommand(program);
|
|
39557
39860
|
registerRepairCommand(program);
|
|
39558
39861
|
program.commandsGroup("Account:");
|
|
@@ -39560,5 +39863,5 @@ registerAccountCommand(program);
|
|
|
39560
39863
|
registerKeysCommand(program);
|
|
39561
39864
|
program.parse();
|
|
39562
39865
|
|
|
39563
|
-
//# debugId=
|
|
39866
|
+
//# debugId=43937D51F562F20264756E2164756E21
|
|
39564
39867
|
//# sourceMappingURL=cli.js.map
|