@secondlayer/cli 10.4.0 → 12.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -2772,118 +2772,6 @@ var init_output = __esm(() => {
2772
2772
  ANSI_ESCAPE_PATTERN = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
2773
2773
  });
2774
2774
 
2775
- // src/lib/api-client.ts
2776
- import { ApiError, SecondLayer } from "@secondlayer/sdk";
2777
- function nextStepHint(status) {
2778
- if (status === undefined)
2779
- return;
2780
- if (status === 403)
2781
- return "You may not have access — check your active project with `secondlayer whoami`.";
2782
- if (status === 404)
2783
- return "Check the name/slug — run the matching `list` command to see what exists.";
2784
- if (status === 400 || status === 422)
2785
- return "Check the command arguments and flags — see `--help` for the expected format.";
2786
- if (status === 429)
2787
- return "Rate limited — wait a moment and retry.";
2788
- if (status >= 500)
2789
- return "Server error — retry shortly, or check `secondlayer status`.";
2790
- return;
2791
- }
2792
- function handleApiError(err, action) {
2793
- const status = err instanceof ApiError || err instanceof CliHttpError ? err.status : undefined;
2794
- if (err instanceof CliHttpError && err.code === "SESSION_EXPIRED" || status === 401) {
2795
- printError("Authentication required.", {
2796
- hint: isOssMode() ? "Set SL_API_KEY / INSTANCE_TOKEN from `secondlayer init`." : "Run `secondlayer login` to re-authenticate."
2797
- });
2798
- process.exit(1);
2799
- }
2800
- const detail = err instanceof Error ? err.message : String(err);
2801
- printError(`Failed to ${action}: ${detail}`, { hint: nextStepHint(status) });
2802
- process.exit(1);
2803
- }
2804
- function withErrorHandling(fn, options) {
2805
- return async (...args) => {
2806
- try {
2807
- await fn(...args);
2808
- } catch (err) {
2809
- if (options?.onError) {
2810
- options.onError(err);
2811
- } else {
2812
- handleApiError(err, options?.action ?? "execute command");
2813
- }
2814
- }
2815
- };
2816
- }
2817
- async function getPlatformClient() {
2818
- const { apiUrl, ephemeralKey } = await resolveAuth();
2819
- return new SecondLayer({ baseUrl: apiUrl, apiKey: ephemeralKey });
2820
- }
2821
- async function listSubgraphsApi() {
2822
- return (await getPlatformClient()).subgraphs.list();
2823
- }
2824
- async function getSubgraphApi(name) {
2825
- return (await getPlatformClient()).subgraphs.get(name);
2826
- }
2827
- async function getSubgraphOpenApi(name, options) {
2828
- return (await getPlatformClient()).subgraphs.openapi(name, options);
2829
- }
2830
- async function getSubgraphAgentSchema(name, options) {
2831
- return (await getPlatformClient()).subgraphs.schema(name, options);
2832
- }
2833
- async function getSubgraphMarkdown(name, options) {
2834
- return (await getPlatformClient()).subgraphs.markdown(name, options);
2835
- }
2836
- async function reindexSubgraphApi(name) {
2837
- return (await getPlatformClient()).subgraphs.reindex(name);
2838
- }
2839
- async function backfillSubgraphApi(name, options) {
2840
- return (await getPlatformClient()).subgraphs.backfill(name, options);
2841
- }
2842
- async function stopSubgraphApi(name) {
2843
- return (await getPlatformClient()).subgraphs.stop(name);
2844
- }
2845
- async function deleteSubgraphApi(name, options) {
2846
- return (await getPlatformClient()).subgraphs.delete(name, options);
2847
- }
2848
- async function deploySubgraphApi(data) {
2849
- return (await getPlatformClient()).subgraphs.deploy(data);
2850
- }
2851
- async function publishSubgraphApi(name) {
2852
- return (await getPlatformClient()).subgraphs.publish(name);
2853
- }
2854
- async function unpublishSubgraphApi(name) {
2855
- return (await getPlatformClient()).subgraphs.unpublish(name);
2856
- }
2857
- async function querySubgraphTable(name, table, params = {}) {
2858
- return (await getPlatformClient()).subgraphs.queryTable(name, table, params);
2859
- }
2860
- async function querySubgraphTableCount(name, table, params = {}) {
2861
- return (await getPlatformClient()).subgraphs.queryTableCount(name, table, params);
2862
- }
2863
- async function getSubgraphGaps(name, opts) {
2864
- return (await getPlatformClient()).subgraphs.gaps(name, opts);
2865
- }
2866
- async function getContractPrintSchema(contractId) {
2867
- let client;
2868
- try {
2869
- client = await getPlatformClient();
2870
- } catch {
2871
- client = new SecondLayer({ baseUrl: resolveApiUrl() });
2872
- }
2873
- return client.index.printSchema(contractId);
2874
- }
2875
- async function getAccountProfile() {
2876
- return httpPlatform("/api/accounts/me");
2877
- }
2878
- async function updateAccountProfile(data) {
2879
- return httpPlatform("/api/accounts/me", { method: "PATCH", body: data });
2880
- }
2881
- var init_api_client = __esm(() => {
2882
- init_http();
2883
- init_output();
2884
- init_resolve_auth();
2885
- });
2886
-
2887
2775
  // ../../node_modules/chalk/source/vendor/ansi-styles/index.js
2888
2776
  function assembleStyles() {
2889
2777
  const codes = new Map;
@@ -10526,9 +10414,9 @@ function lowercaseKeys(object) {
10526
10414
 
10527
10415
  // ../../node_modules/responselike/index.js
10528
10416
  import { Readable as ReadableStream2 } from "node:stream";
10529
- var Response;
10417
+ var Response2;
10530
10418
  var init_responselike = __esm(() => {
10531
- Response = class Response extends ReadableStream2 {
10419
+ Response2 = class Response2 extends ReadableStream2 {
10532
10420
  statusCode;
10533
10421
  headers;
10534
10422
  body;
@@ -10966,7 +10854,7 @@ class CacheableRequest {
10966
10854
  response.once("end", resolve);
10967
10855
  });
10968
10856
  const headers = convertHeaders(revalidatedPolicy.policy.responseHeaders());
10969
- response = new Response({ statusCode: revalidate.statusCode, headers, body: revalidate.body, url: revalidate.url });
10857
+ response = new Response2({ statusCode: revalidate.statusCode, headers, body: revalidate.body, url: revalidate.url });
10970
10858
  response.cachePolicy = revalidatedPolicy.policy;
10971
10859
  response.fromCache = true;
10972
10860
  }
@@ -11042,7 +10930,7 @@ class CacheableRequest {
11042
10930
  const policy = import_http_cache_semantics.default.fromObject(cacheEntry.cachePolicy);
11043
10931
  if (policy.satisfiesWithoutRevalidation(options_) && !options_.forceRefresh) {
11044
10932
  const headers = convertHeaders(policy.responseHeaders());
11045
- const response = new Response({ statusCode: cacheEntry.statusCode, headers, body: cacheEntry.body, url: cacheEntry.url });
10933
+ const response = new Response2({ statusCode: cacheEntry.statusCode, headers, body: cacheEntry.body, url: cacheEntry.url });
11046
10934
  response.cachePolicy = policy;
11047
10935
  response.fromCache = true;
11048
10936
  ee.emit("response", response);
@@ -28400,6 +28288,93 @@ var init_subgraphs = __esm(() => {
28400
28288
  init_format();
28401
28289
  });
28402
28290
 
28291
+ // src/lib/api-client.ts
28292
+ import { ApiError, SecondLayer } from "@secondlayer/sdk";
28293
+ function nextStepHint(status) {
28294
+ if (status === undefined)
28295
+ return;
28296
+ if (status === 403)
28297
+ return "You may not have access — check your active project with `secondlayer whoami`.";
28298
+ if (status === 404)
28299
+ return "Check the name/slug — run the matching `list` command to see what exists.";
28300
+ if (status === 400 || status === 422)
28301
+ return "Check the command arguments and flags — see `--help` for the expected format.";
28302
+ if (status === 429)
28303
+ return "Rate limited — wait a moment and retry.";
28304
+ if (status >= 500)
28305
+ return "Server error — retry shortly, or check `secondlayer status`.";
28306
+ return;
28307
+ }
28308
+ function handleApiError(err, action2) {
28309
+ const status = err instanceof ApiError || err instanceof CliHttpError ? err.status : undefined;
28310
+ if (err instanceof CliHttpError && err.code === "SESSION_EXPIRED" || status === 401) {
28311
+ printError("Authentication required.", {
28312
+ hint: isOssMode() ? "Set SL_API_KEY / INSTANCE_TOKEN from `secondlayer init`." : "Run `secondlayer login` to re-authenticate."
28313
+ });
28314
+ process.exit(1);
28315
+ }
28316
+ const detail = err instanceof Error ? err.message : String(err);
28317
+ printError(`Failed to ${action2}: ${detail}`, { hint: nextStepHint(status) });
28318
+ process.exit(1);
28319
+ }
28320
+ async function getPlatformClient() {
28321
+ const { apiUrl, ephemeralKey } = await resolveAuth();
28322
+ return new SecondLayer({ baseUrl: apiUrl, apiKey: ephemeralKey });
28323
+ }
28324
+ async function listSubgraphsApi() {
28325
+ return (await getPlatformClient()).subgraphs.list();
28326
+ }
28327
+ async function getSubgraphApi(name) {
28328
+ return (await getPlatformClient()).subgraphs.get(name);
28329
+ }
28330
+ async function getSubgraphOpenApi(name, options3) {
28331
+ return (await getPlatformClient()).subgraphs.openapi(name, options3);
28332
+ }
28333
+ async function getSubgraphAgentSchema(name, options3) {
28334
+ return (await getPlatformClient()).subgraphs.schema(name, options3);
28335
+ }
28336
+ async function getSubgraphMarkdown(name, options3) {
28337
+ return (await getPlatformClient()).subgraphs.markdown(name, options3);
28338
+ }
28339
+ async function reindexSubgraphApi(name) {
28340
+ return (await getPlatformClient()).subgraphs.reindex(name);
28341
+ }
28342
+ async function backfillSubgraphApi(name, options3) {
28343
+ return (await getPlatformClient()).subgraphs.backfill(name, options3);
28344
+ }
28345
+ async function stopSubgraphApi(name) {
28346
+ return (await getPlatformClient()).subgraphs.stop(name);
28347
+ }
28348
+ async function deleteSubgraphApi(name, options3) {
28349
+ return (await getPlatformClient()).subgraphs.delete(name, options3);
28350
+ }
28351
+ async function deploySubgraphApi(data) {
28352
+ return (await getPlatformClient()).subgraphs.deploy(data);
28353
+ }
28354
+ async function querySubgraphTable(name, table, params = {}) {
28355
+ return (await getPlatformClient()).subgraphs.queryTable(name, table, params);
28356
+ }
28357
+ async function querySubgraphTableCount(name, table, params = {}) {
28358
+ return (await getPlatformClient()).subgraphs.queryTableCount(name, table, params);
28359
+ }
28360
+ async function getSubgraphGaps(name, opts) {
28361
+ return (await getPlatformClient()).subgraphs.gaps(name, opts);
28362
+ }
28363
+ async function getContractPrintSchema(contractId) {
28364
+ let client;
28365
+ try {
28366
+ client = await getPlatformClient();
28367
+ } catch {
28368
+ client = new SecondLayer({ baseUrl: resolveApiUrl() });
28369
+ }
28370
+ return client.index.printSchema(contractId);
28371
+ }
28372
+ var init_api_client = __esm(() => {
28373
+ init_http();
28374
+ init_output();
28375
+ init_resolve_auth();
28376
+ });
28377
+
28403
28378
  // src/lib/fs.ts
28404
28379
  import { mkdir as mkdir2, readFile as readFile2, stat, unlink, writeFile as writeFile2 } from "node:fs/promises";
28405
28380
  async function readJsonFile(path11) {
@@ -28903,23 +28878,8 @@ var init_require_auth = __esm(() => {
28903
28878
  });
28904
28879
 
28905
28880
  // src/templates/subgraph.ts
28906
- function generateSubgraphTemplate(name, slug = "basic") {
28907
- const body = (() => {
28908
- switch (slug) {
28909
- case "sip-010-balances":
28910
- return sip010Balances(name);
28911
- case "sbtc-flows":
28912
- return sbtcFlows(name);
28913
- case "pox-stacking":
28914
- return poxStacking(name);
28915
- case "bns-names":
28916
- return bnsNames(name);
28917
- default:
28918
- return basic(name);
28919
- }
28920
- })();
28921
- return `${nextStepsHeader(name)}
28922
- ${body}`;
28881
+ function generateSubgraphStarter(name) {
28882
+ return `${nextStepsHeader(name)}${starter(name)}`;
28923
28883
  }
28924
28884
  function nextStepsHeader(name) {
28925
28885
  return `// ───────────────────────────────────────────────────────────────────
@@ -28928,20 +28888,19 @@ function nextStepsHeader(name) {
28928
28888
  // 1. Edit the source filter + schema below to match what you want to track.
28929
28889
  // 2. Edit the handler at the bottom — it runs once per matching event.
28930
28890
  // 3. Deploy: secondlayer subgraphs deploy ${name}.ts
28931
- // (You'll be prompted to log in if this is your first remote deploy.)
28932
28891
  // 4. Wait for sync: secondlayer subgraphs status ${name}
28933
- // Mainnet backfill from genesis can take an hour or more depending on
28934
- // your filter scope. Use --start-block to skip ahead.
28892
+ // Backfill from genesis can take an hour or more depending on your
28893
+ // filter scope. Use --start-block to skip ahead.
28935
28894
  // 5. Query: secondlayer subgraphs query ${name} <table-name>
28936
- // Or hit the auto-generated REST endpoint listed in the deploy output.
28895
+ // Or hit the REST endpoint listed in the deploy output.
28937
28896
  //
28938
- // Bind a typed Subscription to any table you write here — see
28939
- // https://www.secondlayer.tools/docs/subscriptions
28897
+ // Prefer generating from a real contract:
28898
+ // secondlayer subgraphs create ${name} --from-contract SP....my-contract
28940
28899
  // ───────────────────────────────────────────────────────────────────
28941
28900
 
28942
28901
  `;
28943
28902
  }
28944
- function basic(name) {
28903
+ function starter(name) {
28945
28904
  return `import { defineSubgraph } from "@secondlayer/subgraphs";
28946
28905
 
28947
28906
  export default defineSubgraph({
@@ -28949,7 +28908,7 @@ export default defineSubgraph({
28949
28908
  version: "1.0.0",
28950
28909
  description: "TODO: describe what this subgraph tracks",
28951
28910
 
28952
- // Sources define what blockchain data this subgraph processes.
28911
+ // Sources define what chain data this subgraph processes.
28953
28912
  // Each source is named — the name becomes the handler key.
28954
28913
  //
28955
28914
  // Filter types:
@@ -28994,345 +28953,6 @@ export default defineSubgraph({
28994
28953
  });
28995
28954
  `;
28996
28955
  }
28997
- function sip010Balances(name) {
28998
- return `import { defineSchema, defineSubgraph, readContractAt, type TypedSubgraphContext } from "@secondlayer/subgraphs";
28999
-
29000
- /**
29001
- * Track SIP-010 token balances per (asset_identifier, holder).
29002
- * Mirrors the shape of the curated sBTC token-events view
29003
- * surface — but works for ANY SIP-010 token. Constrain to a single
29004
- * token by adding \`assetIdentifier: "SP...token::token-name"\` to each
29005
- * source filter.
29006
- *
29007
- * Balances are moved with \`ctx.increment\`, which applies a DELTA in one
29008
- * atomic statement. That matters: a read-modify-write (findOne → compute →
29009
- * upsert) loses concurrent updates to the same row, and it is not replay-safe
29010
- * under \`backfillMode: "concurrent"\`. Deltas commute, so order doesn't
29011
- * matter and a reorg rewind reverses cleanly via the journal.
29012
- *
29013
- * Token metadata (decimals/symbol) comes from the token contract itself via
29014
- * \`readContractAt\`, so \`amount\` stops being a bare integer nobody can
29015
- * render. The read is pinned to the block being processed — a handler stays a
29016
- * pure function of its block — and \`cache: "contract-constant"\` says these
29017
- * two values can never change for a given token, so each is fetched ONCE
29018
- * instead of once per block.
29019
- *
29020
- * Query examples once deployed:
29021
- * GET /v1/subgraphs/${name}/balances?_search=SP1...
29022
- * GET /v1/subgraphs/${name}/balances?holder=SP1...
29023
- * GET /v1/subgraphs/${name}/tokens
29024
- */
29025
-
29026
- /** The two SIP-010 read-only functions this subgraph calls. */
29027
- const SIP010_ABI = {
29028
- functions: [
29029
- { name: "get-decimals", access: "read-only", args: [], outputs: { response: { ok: "uint128", error: "uint128" } } },
29030
- { name: "get-symbol", access: "read-only", args: [], outputs: { response: { ok: { "string-ascii": { length: 32 } }, error: "uint128" } } },
29031
- ],
29032
- } as const;
29033
-
29034
- /** Tokens already labelled in this process — metadata is immutable, so one
29035
- * resolution per token is enough even across a backfill. */
29036
- const labelled = new Set<string>();
29037
-
29038
- // Hoisted so the helper below can be typed against it.
29039
- const schema = defineSchema({
29040
- balances: {
29041
- columns: {
29042
- asset_identifier: { type: "text", indexed: true, search: true },
29043
- holder: { type: "principal", indexed: true, search: true },
29044
- amount: { type: "uint" },
29045
- },
29046
- // increment() upserts on this key.
29047
- uniqueKeys: [["asset_identifier", "holder"]],
29048
- },
29049
- tokens: {
29050
- columns: {
29051
- asset_identifier: { type: "text", indexed: true },
29052
- contract_id: { type: "text" },
29053
- symbol: { type: "text" },
29054
- decimals: { type: "uint" },
29055
- },
29056
- uniqueKeys: [["asset_identifier"]],
29057
- },
29058
- });
29059
-
29060
- export default defineSubgraph({
29061
- name: "${name}",
29062
- version: "1.0.0",
29063
- description: "Per-token balance tracking for any SIP-010 asset",
29064
-
29065
- sources: {
29066
- transfer: { type: "ft_transfer" },
29067
- mint: { type: "ft_mint" },
29068
- burn: { type: "ft_burn" },
29069
- },
29070
-
29071
- schema,
29072
-
29073
- handlers: {
29074
- transfer: async (event, ctx) => {
29075
- await labelToken(event.assetIdentifier, ctx);
29076
- const amount = BigInt(event.amount ?? 0);
29077
- if (event.sender) {
29078
- ctx.increment("balances", { asset_identifier: event.assetIdentifier, holder: event.sender }, { amount: -amount });
29079
- }
29080
- if (event.recipient) {
29081
- ctx.increment("balances", { asset_identifier: event.assetIdentifier, holder: event.recipient }, { amount });
29082
- }
29083
- },
29084
- mint: (event, ctx) => {
29085
- if (event.recipient) {
29086
- ctx.increment("balances", { asset_identifier: event.assetIdentifier, holder: event.recipient }, { amount: BigInt(event.amount ?? 0) });
29087
- }
29088
- },
29089
- burn: (event, ctx) => {
29090
- if (event.sender) {
29091
- ctx.increment("balances", { asset_identifier: event.assetIdentifier, holder: event.sender }, { amount: -BigInt(event.amount ?? 0) });
29092
- }
29093
- },
29094
- },
29095
- });
29096
-
29097
- /** Resolve a token's symbol + decimals from its contract, once. */
29098
- async function labelToken(
29099
- assetIdentifier: string | undefined,
29100
- ctx: TypedSubgraphContext<typeof schema>,
29101
- ) {
29102
- if (!assetIdentifier || labelled.has(assetIdentifier)) return;
29103
- labelled.add(assetIdentifier);
29104
- const contractId = assetIdentifier.split("::")[0];
29105
- const token = readContractAt(ctx, contractId, SIP010_ABI, { cache: "contract-constant" });
29106
- const [decimals, symbol] = await Promise.all([
29107
- token.read.getDecimals({}),
29108
- token.read.getSymbol({}),
29109
- ]);
29110
- ctx.upsert("tokens", { asset_identifier: assetIdentifier }, { asset_identifier: assetIdentifier, contract_id: contractId, symbol, decimals });
29111
- }
29112
- `;
29113
- }
29114
- function sbtcFlows(name) {
29115
- return `import { defineSubgraph } from "@secondlayer/subgraphs";
29116
-
29117
- /**
29118
- * Track sBTC protocol flows: deposits, withdrawals, signer rotations,
29119
- * governance updates — your own indexed view of the sbtc-registry contract.
29120
- *
29121
- * Source contract: sbtc-registry (mainnet).
29122
- *
29123
- * Query examples once deployed:
29124
- * GET /v1/subgraphs/${name}/flows?topic=completed-deposit
29125
- * GET /v1/subgraphs/${name}/flows?topic=withdrawal-create
29126
- */
29127
- export default defineSubgraph({
29128
- name: "${name}",
29129
- version: "1.0.0",
29130
- description: "sBTC deposits, withdrawals, signer rotations, governance",
29131
-
29132
- // Skip pre-sBTC history. Raise this (e.g., to a recent block near tip) for
29133
- // a smaller backfill, or lower it if you need every sBTC event from genesis.
29134
- startBlock: 860000,
29135
-
29136
- sources: {
29137
- registry: {
29138
- type: "print_event",
29139
- contractId: "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-registry",
29140
- },
29141
- },
29142
-
29143
- schema: {
29144
- flows: {
29145
- columns: {
29146
- topic: { type: "text", indexed: true, search: true },
29147
- request_id: { type: "uint", nullable: true, indexed: true },
29148
- amount: { type: "text", nullable: true },
29149
- sender: { type: "principal", nullable: true, indexed: true },
29150
- bitcoin_txid: { type: "text", nullable: true, search: true },
29151
- burn_height: { type: "uint", nullable: true },
29152
- },
29153
- },
29154
- },
29155
-
29156
- handlers: {
29157
- registry: (event, ctx) => {
29158
- if (!event.topic) return;
29159
- // event.data is untyped across topics — cast to the fields you read, or
29160
- // declare \`prints\` per topic on the source for fully typed event.data.
29161
- const data = event.data as {
29162
- requestId?: bigint;
29163
- amount?: bigint | string;
29164
- sender?: string;
29165
- bitcoinTxid?: string;
29166
- burnHeight?: bigint;
29167
- };
29168
- ctx.insert("flows", {
29169
- topic: event.topic,
29170
- request_id: data.requestId ?? null,
29171
- amount: data.amount != null ? String(data.amount) : null,
29172
- sender: data.sender ?? null,
29173
- bitcoin_txid: data.bitcoinTxid ?? null,
29174
- burn_height: data.burnHeight ?? null,
29175
- });
29176
- },
29177
- },
29178
- });
29179
- `;
29180
- }
29181
- function poxStacking(name) {
29182
- return `import { defineSubgraph } from "@secondlayer/subgraphs";
29183
- import { POX5_ABI } from "@secondlayer/stacks/pox5";
29184
-
29185
- /**
29186
- * Track PoX-5 \`stake\` calls with their arguments already decoded.
29187
- *
29188
- * pox-5 replaced pox-4 when Epoch 4.0 activated (Bitcoin block 960,230,
29189
- * 2026-07-30). SIP-045 reshaped the API: stacking became bonds + staking, so
29190
- * the entry point is \`stake\`, not \`stack-stx\`.
29191
- *
29192
- * The source pairs \`abi\` with \`functionName\`, which is what makes
29193
- * \`event.input\` a typed, named argument set instead of positional
29194
- * \`args[0] as bigint\`. Add one source per function you want to track.
29195
- *
29196
- * Query examples once deployed:
29197
- * GET /v1/subgraphs/${name}/calls?caller=SP1...
29198
- * GET /v1/subgraphs/${name}/calls?caller=SP1...
29199
- */
29200
- export default defineSubgraph({
29201
- name: "${name}",
29202
- version: "1.0.0",
29203
- description: "PoX-5 stake calls with decoded arguments",
29204
-
29205
- sources: {
29206
- pox: {
29207
- type: "contract_call",
29208
- // pox-5 is the live PoX contract since Epoch 4.0 activated at Bitcoin
29209
- // block 960,230 (2026-07-30). Stacking history before that is pox-4 —
29210
- // point a second source at \`.pox-4\` if you need it.
29211
- contractId: "SP000000000000000000002Q6VF78.pox-5",
29212
- // \`abi\` + \`functionName\` together are what type \`event.input\`: the
29213
- // arguments cannot be typed without knowing which function was called.
29214
- // POX5_ABI ships \`as const\` in @secondlayer/stacks, so there is no
29215
- // hand-copied ABI to drift. Add a source per function you care about.
29216
- functionName: "stake",
29217
- abi: POX5_ABI,
29218
- },
29219
- },
29220
-
29221
- schema: {
29222
- calls: {
29223
- columns: {
29224
- function_name: { type: "text", indexed: true, search: true },
29225
- caller: { type: "principal", indexed: true, search: true },
29226
- amount_ustx: { type: "uint" },
29227
- num_cycles: { type: "uint" },
29228
- result_ok: { type: "boolean" },
29229
- },
29230
- },
29231
- },
29232
-
29233
- handlers: {
29234
- pox: (event, ctx) => {
29235
- // \`event.input\` is the decoded, named, typed argument set — no
29236
- // \`args[0] as bigint\`. Hover \`amountUstx\` and it is a bigint.
29237
- const resultHex = event.resultHex ?? "";
29238
- ctx.insert("calls", {
29239
- function_name: event.functionName || ctx.tx.functionName || "",
29240
- caller: ctx.tx.sender,
29241
- amount_ustx: event.input.amountUstx,
29242
- num_cycles: event.input.numCycles,
29243
- result_ok: resultHex.startsWith("0x07"), // 0x07 = response-ok type tag
29244
- });
29245
- },
29246
- },
29247
- });
29248
- `;
29249
- }
29250
- function bnsNames(name) {
29251
- return `import { defineSubgraph } from "@secondlayer/subgraphs";
29252
-
29253
- /**
29254
- * Track BNS-V2 name lifecycle events — registrations, transfers,
29255
- * renewals, burns, airdrops.
29256
- *
29257
- * Source: BNS-V2 print events (topic-discriminated payloads).
29258
- *
29259
- * Query examples once deployed:
29260
- * GET /v1/subgraphs/${name}/names?owner=SP1...
29261
- * GET /v1/subgraphs/${name}/names?_search=alice
29262
- */
29263
- export default defineSubgraph({
29264
- name: "${name}",
29265
- version: "1.0.0",
29266
- description: "BNS-V2 name ownership and lifecycle",
29267
-
29268
- sources: {
29269
- bns: {
29270
- type: "print_event",
29271
- contractId: "SP2QEZ06AGJ3RKJPBV14SY1V5BBFNAW33D96YPGZF.BNS-V2",
29272
- },
29273
- },
29274
-
29275
- schema: {
29276
- names: {
29277
- columns: {
29278
- topic: { type: "text", indexed: true },
29279
- namespace: { type: "text", indexed: true, search: true },
29280
- name: { type: "text", indexed: true, search: true },
29281
- fqn: { type: "text", indexed: true, search: true },
29282
- owner: { type: "principal", nullable: true, indexed: true, search: true },
29283
- },
29284
- },
29285
- },
29286
-
29287
- handlers: {
29288
- bns: (event, ctx) => {
29289
- if (!event.topic) return;
29290
- const data = event.data as { namespace?: unknown; name?: unknown; owner?: string };
29291
- const namespace = decodeBuffUtf8(data.namespace);
29292
- const nameLabel = decodeBuffUtf8(data.name);
29293
- if (!namespace || !nameLabel) return;
29294
- ctx.insert("names", {
29295
- topic: event.topic,
29296
- namespace,
29297
- name: nameLabel,
29298
- fqn: \`\${nameLabel}.\${namespace}\`,
29299
- owner: event.topic === "burn-name" ? null : (data.owner ?? null),
29300
- });
29301
- },
29302
- },
29303
- });
29304
-
29305
- function decodeBuffUtf8(value: unknown): string | null {
29306
- if (typeof value !== "string") return null;
29307
- const hex = value.startsWith("0x") ? value.slice(2) : value;
29308
- if (hex.length === 0) return null;
29309
- const bytes = new Uint8Array(hex.length / 2);
29310
- for (let i = 0; i < bytes.length; i++) {
29311
- bytes[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
29312
- }
29313
- let end = bytes.length;
29314
- while (end > 0 && bytes[end - 1] === 0) end -= 1;
29315
- return new TextDecoder("utf-8").decode(bytes.subarray(0, end));
29316
- }
29317
- `;
29318
- }
29319
- var SUBGRAPH_TEMPLATE_SLUGS, SUBGRAPH_TEMPLATE_DESCRIPTIONS;
29320
- var init_subgraph = __esm(() => {
29321
- SUBGRAPH_TEMPLATE_SLUGS = [
29322
- "basic",
29323
- "sip-010-balances",
29324
- "sbtc-flows",
29325
- "pox-stacking",
29326
- "bns-names"
29327
- ];
29328
- SUBGRAPH_TEMPLATE_DESCRIPTIONS = {
29329
- basic: "Empty starter — pick any source filter type",
29330
- "sip-010-balances": "SIP-010 token balances (transfers + mints + burns)",
29331
- "sbtc-flows": "sBTC protocol flows (deposits, withdrawals, signer rotations)",
29332
- "pox-stacking": "PoX-4 stacking lifecycle calls",
29333
- "bns-names": "BNS-V2 name ownership and lifecycle"
29334
- };
29335
- });
29336
28956
 
29337
28957
  // src/commands/subgraph-test.ts
29338
28958
  var exports_subgraph_test = {};
@@ -29587,7 +29207,6 @@ import {
29587
29207
  generatePrintPayloadTypes,
29588
29208
  generatePrintSchemaSubgraph
29589
29209
  } from "@secondlayer/scaffold";
29590
- import { ByoBreakingChangeError } from "@secondlayer/sdk";
29591
29210
  import { TRAIT_STANDARDS } from "@secondlayer/stacks/clarity";
29592
29211
  async function loadSubgraphWithDepCheck(absPath) {
29593
29212
  try {
@@ -29940,27 +29559,6 @@ ${dim("Columns")}`);
29940
29559
  const deployTarget = context.bundled ? "tenant API" : "local database";
29941
29560
  info(`Dry run only. No ${deployTarget} changes were made.`);
29942
29561
  }
29943
- function printByoBreakingPlan(details) {
29944
- error("Refusing breaking schema change on BYO subgraph (no data dropped).");
29945
- console.log(`
29946
- Breaking changes:`);
29947
- for (const r2 of details.reasons)
29948
- console.log(` ${red("✗")} ${r2}`);
29949
- console.log(`
29950
- To rebuild manually, run on YOUR database:`);
29951
- console.log(` ${details.plan.dropStatement}`);
29952
- console.log(` ${details.plan.statements.join(`;
29953
- `)}`);
29954
- console.log(`
29955
- Grant (first deploy only):`);
29956
- for (const line of details.plan.grantScript.split(`
29957
- `)) {
29958
- console.log(` ${dim(line)}`);
29959
- }
29960
- console.log(`
29961
- ${yellow("This DROPS all rows in that schema, then rebuilds it. Re-deploy after.")}`);
29962
- console.log(dim("(--force destructive rebuild not yet supported — manual DROP required.)"));
29963
- }
29964
29562
  function formatDuration(seconds2) {
29965
29563
  if (seconds2 < 90)
29966
29564
  return `${Math.max(1, Math.round(seconds2))}s`;
@@ -30016,27 +29614,15 @@ function formatSubgraphSync(sync) {
30016
29614
  }
30017
29615
  function registerSubgraphsCommand(program2) {
30018
29616
  const subgraphs = program2.command("subgraphs").description("Manage materialized subgraphs");
30019
- subgraphs.command("create <name>").description("Create a new subgraph definition file").option("--template <slug>", `Starter template (one of: ${SUBGRAPH_TEMPLATE_SLUGS.join(", ")})`).option("--from-contract <contractId>", "Generate sources/schema/handlers from the contract's observed print events (requires network)").option("--table-per-topic", "With --from-contract: one table per print topic instead of a single wide table").addHelpText("after", `
29617
+ subgraphs.command("create <name>").description("Create a new subgraph definition file").option("--from-contract <contractId>", "Generate sources/schema/handlers from the contract's observed print events (requires network)").option("--table-per-topic", "With --from-contract: one table per print topic instead of a single wide table").addHelpText("after", `
30020
29618
  Examples:
30021
29619
  $ secondlayer subgraphs create my-graph
30022
- $ secondlayer subgraphs create token-balances --template sip-010-balances
30023
29620
  $ secondlayer subgraphs create sbtc-flows --from-contract SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-registry
30024
29621
  $ secondlayer subgraphs create sbtc-flows --from-contract SM3....sbtc-registry --table-per-topic`).action(async (name, opts) => {
30025
- if (opts.fromContract && opts.template) {
30026
- error("--from-contract and --template are mutually exclusive");
30027
- process.exit(1);
30028
- }
30029
29622
  if (opts.tablePerTopic && !opts.fromContract) {
30030
29623
  error("--table-per-topic requires --from-contract");
30031
29624
  process.exit(1);
30032
29625
  }
30033
- const slug = opts.template ?? "basic";
30034
- if (!opts.fromContract && !SUBGRAPH_TEMPLATE_SLUGS.includes(slug)) {
30035
- error(`Unknown template "${opts.template}". Available templates:
30036
- ${SUBGRAPH_TEMPLATE_SLUGS.map((s2) => ` ${s2.padEnd(20)} ${SUBGRAPH_TEMPLATE_DESCRIPTIONS[s2]}`).join(`
30037
- `)}`);
30038
- process.exit(1);
30039
- }
30040
29626
  const dir = resolve3("subgraphs");
30041
29627
  const filePath = resolve3(dir, `${name}.ts`);
30042
29628
  if (existsSync3(filePath)) {
@@ -30065,7 +29651,7 @@ Check the contract id, and that the contract has emitted print events (schemas a
30065
29651
  sample: schema.sample
30066
29652
  }));
30067
29653
  } else {
30068
- content = generateSubgraphTemplate(name, slug);
29654
+ content = generateSubgraphStarter(name);
30069
29655
  }
30070
29656
  if (!existsSync3(dir)) {
30071
29657
  mkdirSync2(dir, { recursive: true });
@@ -30074,8 +29660,6 @@ Check the contract id, and that the contract has emitted print events (schemas a
30074
29660
  success(`Created ${filePath}`);
30075
29661
  if (opts.fromContract) {
30076
29662
  info(`Schema inferred from ${opts.fromContract} print events (${opts.tablePerTopic ? "table per topic" : "single wide table"})`);
30077
- } else if (slug !== "basic") {
30078
- info(`Template: ${slug} — ${SUBGRAPH_TEMPLATE_DESCRIPTIONS[slug]}`);
30079
29663
  }
30080
29664
  info(`Next: secondlayer subgraphs deploy subgraphs/${name}.ts`);
30081
29665
  });
@@ -30140,7 +29724,7 @@ Stopped watching.`);
30140
29724
  });
30141
29725
  await new Promise(() => {});
30142
29726
  });
30143
- 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) => {
29727
+ 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)").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) => {
30144
29728
  try {
30145
29729
  const absPath = resolve3(file);
30146
29730
  if (!options3.dryRun) {
@@ -30173,15 +29757,6 @@ Stopped watching.`);
30173
29757
  }
30174
29758
  const dryRun = options3.dryRun;
30175
29759
  const visibility = parseVisibilityOption(options3.visibility);
30176
- if (visibility === "public" && options3.databaseUrl && !options3.yes) {
30177
- const confirmed = await confirm3({
30178
- message: "⚠ Public BYO subgraph: anonymous reads will query YOUR database. Continue?"
30179
- });
30180
- if (!confirmed) {
30181
- info("Aborted.");
30182
- process.exit(0);
30183
- }
30184
- }
30185
29760
  const startBlock = parseStartBlockOption(options3.startBlock);
30186
29761
  if (startBlock !== undefined) {
30187
29762
  warn(`--start-block ${startBlock} overrides the definition's startBlock for this deploy.`);
@@ -30246,9 +29821,6 @@ Stopped watching.`);
30246
29821
  }
30247
29822
  }
30248
29823
  }
30249
- if (options3.databaseUrl) {
30250
- info("BYO data plane: schema + rows will live in your database. The server verifies the connection before deploying.");
30251
- }
30252
29824
  const result = await deploySubgraphApi({
30253
29825
  name: effectiveDef.name,
30254
29826
  version: undefined,
@@ -30258,12 +29830,8 @@ Stopped watching.`);
30258
29830
  handlerCode,
30259
29831
  sourceCode: source,
30260
29832
  ...deployStartBlock !== undefined ? { startBlock: deployStartBlock } : {},
30261
- ...options3.databaseUrl ? { databaseUrl: options3.databaseUrl } : {},
30262
29833
  ...visibility ? { visibility } : {}
30263
29834
  });
30264
- if (result.start_block_clamped) {
30265
- info(` Free tier indexes forward from deploy (start block ${result.start_block}) — upgrade for genesis backfill.`);
30266
- }
30267
29835
  for (const w2 of result.warnings ?? [])
30268
29836
  warn(w2);
30269
29837
  const printDeployFooter = async () => {
@@ -30276,7 +29844,6 @@ Stopped watching.`);
30276
29844
  info(` Share: ${apiUrl}/v1/subgraphs/${effectiveDef.name} (public — no key needed)`);
30277
29845
  } else if (firstTable) {
30278
29846
  info(` REST: ${apiUrl}/api/subgraphs/${effectiveDef.name}/${firstTable}`);
30279
- info(` Publish: secondlayer subgraphs publish ${effectiveDef.name} (open anon /v1 reads)`);
30280
29847
  }
30281
29848
  if (result.reindexStarted) {
30282
29849
  const denom = result.estimatedEvents != null ? ` ~${result.estimatedEvents.toLocaleString()} events —` : "";
@@ -30356,14 +29923,6 @@ Stopped watching.`);
30356
29923
  await closeDb();
30357
29924
  }
30358
29925
  } catch (err) {
30359
- if (err instanceof ByoBreakingChangeError) {
30360
- printByoBreakingPlan(err.details);
30361
- process.exit(1);
30362
- }
30363
- if (err && typeof err === "object" && err.code === "BYO_BREAKING_CHANGE" && err.details) {
30364
- printByoBreakingPlan(err.details);
30365
- process.exit(1);
30366
- }
30367
29926
  error(`Failed to deploy subgraph: ${err}`);
30368
29927
  process.exit(1);
30369
29928
  }
@@ -30700,33 +30259,6 @@ ${rows.length} row(s)`));
30700
30259
  handleApiError(err, "delete subgraph");
30701
30260
  }
30702
30261
  });
30703
- 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) => {
30704
- try {
30705
- await requireAuth();
30706
- const result = await publishSubgraphApi(name);
30707
- success(`Subgraph "${name}" is now public`);
30708
- try {
30709
- const { apiUrl } = await resolveAuth();
30710
- info(` Share: ${apiUrl}${result.url} (no key needed)`);
30711
- } catch {}
30712
- } catch (err) {
30713
- const status = err?.status;
30714
- if (status === 409) {
30715
- error(`Public name "${name}" is already taken by another account. Rename the subgraph (redeploy under a new name) or keep it private.`);
30716
- process.exit(1);
30717
- }
30718
- handleApiError(err, "publish subgraph");
30719
- }
30720
- });
30721
- 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) => {
30722
- try {
30723
- await requireAuth();
30724
- await unpublishSubgraphApi(name);
30725
- success(`Subgraph "${name}" is now private (reads need your key)`);
30726
- } catch (err) {
30727
- handleApiError(err, "unpublish subgraph");
30728
- }
30729
- });
30730
30262
  subgraphs.command("scaffold [contractAddress]").description("Scaffold a defineSubgraph() file from a contract or trait").option("-o, --output <path>", "Output file path (required)").option("-k, --api-key <key>", "Stacks node API key for direct RPC URLs").option("--functions <names>", "Comma-separated public functions to index as typed contract_call tables").option("--trait <std>", "Scaffold a trait-scoped source (sip-009|sip-010|sip-013) — no contract needed").option("--no-install", "Skip bun install after writing package.json").addHelpText("after", `
30731
30263
  Examples:
30732
30264
  $ secondlayer subgraphs scaffold SP3D6PV2ACBPEKYJTCMH7HEN02KP87QSP8KTEH335.megapont-ape-club-nft -o subgraphs/apes.ts
@@ -30887,7 +30419,6 @@ var init_subgraphs2 = __esm(() => {
30887
30419
  init_resolve_auth();
30888
30420
  init_resolve_auth();
30889
30421
  init_clarity();
30890
- init_subgraph();
30891
30422
  init_api();
30892
30423
  init_format();
30893
30424
  ORM_ONLY_CODEGEN_FLAGS = {
@@ -34789,7 +34320,7 @@ init_esm();
34789
34320
  // package.json
34790
34321
  var package_default = {
34791
34322
  name: "@secondlayer/cli",
34792
- version: "10.4.0",
34323
+ version: "12.0.0",
34793
34324
  repository: {
34794
34325
  type: "git",
34795
34326
  url: "git+https://github.com/ryanwaits/secondlayer.git",
@@ -34830,12 +34361,12 @@ var package_default = {
34830
34361
  "@biomejs/wasm-nodejs": "^1.9.0",
34831
34362
  "@dsnp/parquetjs": "^1.8.8",
34832
34363
  "@inquirer/prompts": "^8.2.0",
34833
- "@secondlayer/bundler": "^0.5.0",
34834
- "@secondlayer/scaffold": "^1.5.1",
34835
- "@secondlayer/sdk": "^6.54.1",
34836
- "@secondlayer/shared": "^7.11.2",
34837
- "@secondlayer/stacks": "^3.3.1",
34838
- "@secondlayer/subgraphs": "^3.26.4",
34364
+ "@secondlayer/bundler": "^0.5.1",
34365
+ "@secondlayer/scaffold": "^1.5.4",
34366
+ "@secondlayer/sdk": "^8.0.0",
34367
+ "@secondlayer/shared": "^9.0.0",
34368
+ "@secondlayer/stacks": "^4.0.0",
34369
+ "@secondlayer/subgraphs": "^4.0.1",
34839
34370
  esbuild: "^0.27.4",
34840
34371
  "smol-toml": "^1.6.1",
34841
34372
  zod: "^4.3.6"
@@ -34855,134 +34386,6 @@ var package_default = {
34855
34386
  }
34856
34387
  };
34857
34388
 
34858
- // src/commands/account.ts
34859
- init_api_client();
34860
- init_output();
34861
-
34862
- // src/commands/billing.ts
34863
- init_http();
34864
- init_output();
34865
- async function runBillingStatus(json) {
34866
- let res;
34867
- try {
34868
- res = await httpPlatform("/api/billing/status");
34869
- } catch (err) {
34870
- if (err instanceof CliHttpError) {
34871
- error(err.message);
34872
- process.exit(1);
34873
- }
34874
- throw err;
34875
- }
34876
- output({ json, data: res, human: () => renderBillingStatus(res) });
34877
- }
34878
- function addBillingCommand(parent) {
34879
- parent.command("billing").description("Show archive credit balance (hidden; use secondlayer credits)").option("--json", "Output as JSON").action((options) => runBillingStatus(options.json));
34880
- }
34881
- function renderBillingStatus(res) {
34882
- if (!res.subscription) {
34883
- console.log(formatKeyValue([
34884
- ["Credits", dim("secondlayer credits balance")],
34885
- [
34886
- "Buy",
34887
- dim("secondlayer credits buy --email you@example.com --pack 25")
34888
- ]
34889
- ]));
34890
- return;
34891
- }
34892
- const rows = [];
34893
- rows.push(["Plan", res.plan]);
34894
- rows.push([
34895
- "Customer",
34896
- res.stripeCustomerId ?? dim("(none — no subscription yet)")
34897
- ]);
34898
- const sub = res.subscription;
34899
- rows.push(["Subscription", `${sub.id} (${sub.status})`]);
34900
- if (sub.tier)
34901
- rows.push(["Tier", sub.tier]);
34902
- if (sub.amountCents !== null && sub.interval) {
34903
- const dollars = (sub.amountCents / 100).toFixed(2);
34904
- rows.push(["Price", `$${dollars} / ${sub.interval}`]);
34905
- }
34906
- if (sub.trialEnd) {
34907
- const days = Math.max(0, Math.round((new Date(sub.trialEnd).getTime() - Date.now()) / 86400000));
34908
- rows.push([
34909
- "Trial ends",
34910
- `${formatDate(sub.trialEnd)} (${days}d remaining)`
34911
- ]);
34912
- }
34913
- if (sub.currentPeriodEnd) {
34914
- rows.push(["Renews", formatDate(sub.currentPeriodEnd)]);
34915
- }
34916
- if (sub.cancelAtPeriodEnd) {
34917
- rows.push(["Cancels at period end", "yes"]);
34918
- }
34919
- if (sub.discount) {
34920
- const off = sub.discount.percentOff !== null ? `${sub.discount.percentOff}% off` : sub.discount.amountOff !== null ? `$${(sub.discount.amountOff / 100).toFixed(2)} off` : "discount";
34921
- const label = sub.discount.code ? `${sub.discount.code} (${sub.discount.name ?? "coupon"})` : sub.discount.name ?? "applied";
34922
- rows.push(["Discount", `${label} — ${off}, ${sub.discount.duration}`]);
34923
- }
34924
- console.log(formatKeyValue(rows));
34925
- }
34926
- function formatDate(iso) {
34927
- return new Date(iso).toISOString().slice(0, 10);
34928
- }
34929
-
34930
- // src/commands/account.ts
34931
- async function showProfile(json) {
34932
- const profile = await getAccountProfile();
34933
- output({
34934
- json,
34935
- data: profile,
34936
- human: () => console.log(formatKeyValue([
34937
- ["Email", profile.email],
34938
- ["Plan", profile.plan],
34939
- ["Display Name", profile.displayName ?? dim("—")],
34940
- ["Bio", profile.bio ?? dim("—")],
34941
- ["Slug", profile.slug ?? dim("—")],
34942
- [
34943
- "Notify on reindex complete",
34944
- profile.notifyReindexComplete ? "on" : "off"
34945
- ]
34946
- ]))
34947
- });
34948
- }
34949
- async function applyProfileUpdate(options, getOptionValueSource) {
34950
- const data = {};
34951
- if (options.name)
34952
- data.display_name = options.name;
34953
- if (options.bio)
34954
- data.bio = options.bio;
34955
- if (options.slug)
34956
- data.slug = options.slug;
34957
- if (getOptionValueSource("notifyReindexComplete") === "cli") {
34958
- data.notify_reindex_complete = options.notifyReindexComplete;
34959
- }
34960
- const updated = await updateAccountProfile(data);
34961
- output({
34962
- json: options.json,
34963
- data: updated,
34964
- human: () => {
34965
- success("Profile updated");
34966
- console.log(formatKeyValue([
34967
- ["Display Name", updated.displayName ?? dim("—")],
34968
- ["Bio", updated.bio ?? dim("—")],
34969
- ["Slug", updated.slug ?? dim("—")],
34970
- [
34971
- "Notify on reindex complete",
34972
- updated.notifyReindexComplete ? "on" : "off"
34973
- ]
34974
- ]));
34975
- }
34976
- });
34977
- }
34978
- function registerAccountCommand(program2) {
34979
- const account = program2.command("account", { hidden: true }).description("Manage your account profile");
34980
- addBillingCommand(account);
34981
- account.command("get").description("Show your account profile").option("--json", "Output as JSON").action(withErrorHandling(async (options) => showProfile(options.json), { action: "show profile" }));
34982
- 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)), {
34983
- action: "update profile"
34984
- }));
34985
- }
34986
34389
  // src/commands/credits.ts
34987
34390
  init_http();
34988
34391
  init_output();
@@ -37409,46 +36812,6 @@ Examples:
37409
36812
  // src/commands/index.ts
37410
36813
  init_index_api();
37411
36814
 
37412
- // src/commands/keys.ts
37413
- init_output();
37414
- init_resolve_auth();
37415
- import { SecondLayer as SecondLayer4 } from "@secondlayer/sdk";
37416
- function registerKeysCommand(program2) {
37417
- const keys = program2.command("keys", { hidden: true }).description("Manage API keys");
37418
- 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) => {
37419
- if (o3.product && o3.product !== "streams" && o3.product !== "index") {
37420
- error("--product must be 'streams' or 'index'");
37421
- process.exit(1);
37422
- }
37423
- const apiKey = resolveEnvKey();
37424
- if (!apiKey) {
37425
- error("No API key set. Export SL_API_KEY with an account-level key (issue one at https://www.secondlayer.tools/platform/api-keys).");
37426
- process.exit(1);
37427
- }
37428
- try {
37429
- const sl = new SecondLayer4({
37430
- baseUrl: resolveApiUrl(),
37431
- apiKey
37432
- });
37433
- const minted = await sl.apiKeys.create({
37434
- product: o3.product === "index" ? "index" : "streams",
37435
- name: o3.name
37436
- });
37437
- output({
37438
- json: o3.json,
37439
- data: minted,
37440
- human: () => {
37441
- success(`Created ${minted.product} key (${minted.prefix}…)`);
37442
- note("This key is shown once — store it now:");
37443
- writeData(minted.key);
37444
- }
37445
- });
37446
- } catch (err) {
37447
- error(err instanceof Error ? err.message : String(err));
37448
- process.exit(1);
37449
- }
37450
- });
37451
- }
37452
36815
  // src/commands/doctor.ts
37453
36816
  init_config2();
37454
36817
 
@@ -38662,14 +38025,6 @@ async function readActiveProject(cwd, globalDefault) {
38662
38025
  }
38663
38026
  return null;
38664
38027
  }
38665
- async function writeActiveProject(slug, cwd) {
38666
- const dir = join13(resolve11(cwd), DIRNAME);
38667
- await mkdir4(dir, { recursive: true });
38668
- const file = join13(dir, FILENAME);
38669
- await writeFile4(file, `${JSON.stringify({ slug }, null, 2)}
38670
- `, "utf8");
38671
- return file;
38672
- }
38673
38028
 
38674
38029
  // src/commands/whoami.ts
38675
38030
  init_resolve_auth();
@@ -38699,7 +38054,6 @@ function registerWhoamiCommand(program2) {
38699
38054
  json: options3.json,
38700
38055
  data: {
38701
38056
  email: account.email,
38702
- plan: account.plan,
38703
38057
  apiUrl: auth.apiUrl,
38704
38058
  authSource,
38705
38059
  project: active ? { slug: active.slug, source: active.resolvedFrom } : null
@@ -38707,7 +38061,6 @@ function registerWhoamiCommand(program2) {
38707
38061
  human: () => {
38708
38062
  const rows = [];
38709
38063
  rows.push(["Email", account.email]);
38710
- rows.push(["Plan", account.plan]);
38711
38064
  rows.push(["API URL", auth.apiUrl]);
38712
38065
  rows.push(["Auth", dim(authSource)]);
38713
38066
  if (active) {
@@ -38750,85 +38103,6 @@ function registerLogoutCommand(program2) {
38750
38103
  success("Logged out.");
38751
38104
  });
38752
38105
  }
38753
- // src/commands/project.ts
38754
- init_config2();
38755
- init_http();
38756
- init_output();
38757
- function registerProjectCommand(program2) {
38758
- const project = program2.command("projects", { hidden: true }).alias("project").description("Manage your Secondlayer project");
38759
- project.command("list").alias("ls").description("List projects in your account").option("--json", "Output as JSON").action(async (options3) => {
38760
- try {
38761
- const res = await httpPlatform("/api/projects");
38762
- const active = await readActiveProject(process.cwd(), (await loadConfig2()).defaultProject);
38763
- output({
38764
- json: options3.json,
38765
- data: { projects: res.projects, active: active?.slug ?? null },
38766
- human: () => {
38767
- if (res.projects.length === 0) {
38768
- info("No project found.");
38769
- return;
38770
- }
38771
- const rows = res.projects.map((p2) => [
38772
- p2.slug === active?.slug ? `* ${p2.slug}` : ` ${p2.slug}`,
38773
- p2.name,
38774
- p2.network,
38775
- new Date(p2.createdAt).toLocaleDateString()
38776
- ]);
38777
- console.log(formatTable(["", "Name", "Network", "Created"], rows));
38778
- if (active) {
38779
- console.log(dim(`Active: ${active.slug} (from ${active.resolvedFrom})`));
38780
- }
38781
- }
38782
- });
38783
- } catch (err) {
38784
- handleProjectError(err);
38785
- }
38786
- });
38787
- project.command("use <slug>").description("Bind this directory to a project (writes ./.secondlayer/project)").action(async (slug) => {
38788
- try {
38789
- await httpPlatform(`/api/projects/${encodeURIComponent(slug)}`);
38790
- } catch (err) {
38791
- if (err instanceof CliHttpError && err.status === 404) {
38792
- error(`Project "${slug}" not found — run 'secondlayer project list' to see available projects`);
38793
- process.exit(1);
38794
- }
38795
- handleProjectError(err);
38796
- }
38797
- const path11 = await writeActiveProject(slug, process.cwd());
38798
- success(`Bound to project "${slug}"`);
38799
- info(dim(`Written to ${path11}`));
38800
- info(dim("Tip: add `.secondlayer/` to .gitignore — it's account-personal."));
38801
- });
38802
- project.command("get").description("Show the active project for this directory").option("--json", "Output as JSON").action(async (options3) => {
38803
- const config = await loadConfig2();
38804
- const active = await readActiveProject(process.cwd(), config.defaultProject);
38805
- output({
38806
- json: options3.json,
38807
- data: active ? { slug: active.slug, source: active.resolvedFrom } : null,
38808
- human: () => {
38809
- if (!active) {
38810
- info("No active project.");
38811
- info(dim("Run 'secondlayer project use <slug>' to bind this directory."));
38812
- return;
38813
- }
38814
- console.log(active.slug);
38815
- console.log(dim(`(from ${active.resolvedFrom})`));
38816
- }
38817
- });
38818
- });
38819
- }
38820
- function handleProjectError(err) {
38821
- if (err instanceof CliHttpError) {
38822
- if (err.code === "SESSION_EXPIRED") {
38823
- error("Session expired. Run: secondlayer login");
38824
- process.exit(1);
38825
- }
38826
- error(err.message);
38827
- process.exit(1);
38828
- }
38829
- error(err instanceof Error ? err.message : String(err));
38830
- process.exit(1);
38831
- }
38832
38106
  // src/commands/verify.ts
38833
38107
  import {
38834
38108
  compareRangeDigests,
@@ -39419,6 +38693,7 @@ Exit codes:
39419
38693
  });
39420
38694
  }
39421
38695
  // src/commands/bootstrap.ts
38696
+ import { createHash as createHash3 } from "node:crypto";
39422
38697
  import { unlink as unlink3, writeFile as writeFile6 } from "node:fs/promises";
39423
38698
  import { tmpdir as tmpdir3 } from "node:os";
39424
38699
  import { join as join16 } from "node:path";
@@ -39430,6 +38705,7 @@ import {
39430
38705
  } from "@secondlayer/shared/archive/copy-loader";
39431
38706
  import { computeRangeDigest as computeRangeDigest3 } from "@secondlayer/shared/archive/range-digest";
39432
38707
  import { getDb as getDb4, getRawClient, sql as sql3 } from "@secondlayer/shared/db";
38708
+ import { upsertSyncScope } from "@secondlayer/shared/db/queries/sync-scope";
39433
38709
 
39434
38710
  // src/lib/bootstrap-resume.ts
39435
38711
  function planTornImport(input4) {
@@ -39466,6 +38742,18 @@ var BOOTSTRAP_EXIT = {
39466
38742
  };
39467
38743
  var BLOCKS_PER_DAY_ESTIMATE = 5400;
39468
38744
  var RESTORE_ROWS_PER_SECOND = 30000;
38745
+ function archiveScopeBounds(partitions) {
38746
+ if (partitions.length === 0)
38747
+ return null;
38748
+ return {
38749
+ start_height: Math.min(...partitions.map((p2) => p2.from_block)),
38750
+ tip_height: Math.max(...partitions.map((p2) => p2.to_block))
38751
+ };
38752
+ }
38753
+ function manifestDigest(manifest) {
38754
+ const { signature: _signature, key_id: _keyId, ...payload } = manifest;
38755
+ return createHash3("sha256").update(JSON.stringify(payload)).digest("hex");
38756
+ }
39469
38757
  async function readNodeTip() {
39470
38758
  const url = process.env.STACKS_NODE_RPC_URL ?? "http://localhost:20443";
39471
38759
  try {
@@ -39520,10 +38808,11 @@ async function loadPartition(reference, partition, rawClient) {
39520
38808
  return written;
39521
38809
  }
39522
38810
  function attachBootstrapCommand(cmd) {
39523
- 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", `
38811
+ return cmd.requiredOption("--against <manifest>", "archive manifest: an https URL or a local file path").option("--to-block <n>", "stop at this height instead of the archive tip").option("--from-block <n>", "forward-only: restore from this height instead of genesis; earlier history is declared out of scope").option("--public-key <pem>", "pin the signing key instead of fetching it").option("-y, --yes", "skip the confirmation prompt").option("--json", "Output as JSON").addHelpText("after", `
39524
38812
  Examples:
39525
38813
  $ secondlayer bootstrap --against https://archive.secondlayer.tools/.../snapshots/<digest>.json
39526
38814
  $ secondlayer bootstrap --against ./snapshot.json --to-block 4000000 --yes
38815
+ $ secondlayer bootstrap --against ./snapshot.json --from-block 8000000 --yes
39527
38816
 
39528
38817
  Exit codes:
39529
38818
  0 restored and verified
@@ -39548,7 +38837,8 @@ Exit codes:
39548
38837
  const maxHeightRow = await db.selectFrom("blocks").select(({ fn }) => fn.max("height").as("max")).executeTakeFirst();
39549
38838
  const maxBlockHeight = maxHeightRow?.max === null || maxHeightRow?.max === undefined ? null : Number(maxHeightRow.max);
39550
38839
  const toBlock = opts.toBlock === undefined ? undefined : Number(opts.toBlock);
39551
- const declared = (reference.manifest.partitions ?? []).filter((p2) => toBlock === undefined || p2.to_block <= toBlock);
38840
+ const fromBlock = opts.fromBlock === undefined ? undefined : Number(opts.fromBlock);
38841
+ const declared = (reference.manifest.partitions ?? []).filter((p2) => (toBlock === undefined || p2.to_block <= toBlock) && (fromBlock === undefined || p2.from_block >= fromBlock));
39552
38842
  const resume = planTornImport({
39553
38843
  hasIndexProgress: !!progress,
39554
38844
  maxBlockHeight,
@@ -39575,6 +38865,8 @@ Exit codes:
39575
38865
  });
39576
38866
  process.exit(BOOTSTRAP_EXIT.REFUSED);
39577
38867
  }
38868
+ const scopeBounds = archiveScopeBounds(declared);
38869
+ const startHeight = scopeBounds?.start_height ?? 0;
39578
38870
  const totalRows = partitions.reduce((sum, p2) => sum + p2.row_count, 0);
39579
38871
  const totalBytes = partitions.reduce((sum, p2) => sum + p2.byte_size, 0);
39580
38872
  const tipHeight = Math.max(...partitions.map((p2) => p2.to_block));
@@ -39584,7 +38876,10 @@ Exit codes:
39584
38876
  console.error("");
39585
38877
  console.error(formatKeyValue([
39586
38878
  ["archive", reference.origin],
39587
- ["coverage", `genesis → ${tipHeight.toLocaleString()}`],
38879
+ [
38880
+ "coverage",
38881
+ `${startHeight === 0 ? "genesis" : startHeight.toLocaleString()} → ${tipHeight.toLocaleString()}`
38882
+ ],
39588
38883
  ["rows", totalRows.toLocaleString()],
39589
38884
  ["download", `${(totalBytes / 1e9).toFixed(1)} GB`],
39590
38885
  ["signature", "verified"]
@@ -39642,6 +38937,17 @@ Exit codes:
39642
38937
  highest_seen_block = GREATEST(index_progress.highest_seen_block, EXCLUDED.highest_seen_block),
39643
38938
  updated_at = NOW()
39644
38939
  `.execute(db);
38940
+ const genesisRow = startHeight === 0 ? await db.selectFrom("blocks").select("hash").where("canonical", "=", true).where("height", "=", 0).executeTakeFirst() : undefined;
38941
+ await upsertSyncScope(db, {
38942
+ network,
38943
+ start_height: startHeight,
38944
+ target_height: null,
38945
+ bootstrap: {
38946
+ source: "archive",
38947
+ manifest_digest: manifestDigest(reference.manifest),
38948
+ genesis_hash: genesisRow?.hash ?? null
38949
+ }
38950
+ });
39645
38951
  const seam = nodeTipAtStart !== null && nodeTipAtStart > tipHeight ? {
39646
38952
  nodeTip: nodeTipAtStart,
39647
38953
  gap: nodeTipAtStart - tipHeight
@@ -39650,6 +38956,7 @@ Exit codes:
39650
38956
  const report = {
39651
38957
  status: divergent === 0 ? "restored" : "divergent",
39652
38958
  archive: reference.origin,
38959
+ start_height: startHeight,
39653
38960
  tip_height: tipHeight,
39654
38961
  rows: loaded,
39655
38962
  ranges_verified: referenceDigests.length,
@@ -39672,6 +38979,9 @@ Exit codes:
39672
38979
  }
39673
38980
  console.error("");
39674
38981
  console.error(dim(`Your instance holds history through ${tipHeight.toLocaleString()} and will resume at ${(tipHeight + 1).toLocaleString()}.`));
38982
+ if (startHeight > 0) {
38983
+ console.error(dim(` Scope starts at ${startHeight.toLocaleString()} — earlier history is declared out of scope, not missing.`));
38984
+ }
39675
38985
  if (seam) {
39676
38986
  console.error(dim(`The chain advanced to ${seam.nodeTip.toLocaleString()} while restoring — ${seam.gap.toLocaleString()} blocks to catch up.`));
39677
38987
  console.error(dim(" Start the indexer; it backfills that range before following the tip."));
@@ -39696,10 +39006,265 @@ Exit codes:
39696
39006
  function registerBootstrapCommand(program2) {
39697
39007
  attachBootstrapCommand(program2.command("bootstrap").description("Restore chain history from a verified archive instead of syncing from genesis"));
39698
39008
  }
39009
+ // src/commands/backup.ts
39010
+ init_output();
39011
+ import { createHash as createHash4 } from "node:crypto";
39012
+ import {
39013
+ chmodSync,
39014
+ existsSync as existsSync8,
39015
+ mkdirSync as mkdirSync6,
39016
+ readFileSync as readFileSync7,
39017
+ statSync as statSync3,
39018
+ writeFileSync as writeFileSync7
39019
+ } from "node:fs";
39020
+ import { join as join17, resolve as resolve13 } from "node:path";
39021
+ import { getDb as getDb5, sql as sql4 } from "@secondlayer/shared/db";
39022
+ import {
39023
+ decryptBundle,
39024
+ encryptBundle,
39025
+ keyMatchesCanary,
39026
+ planBackup,
39027
+ precheckRestore,
39028
+ sealKeyCanary
39029
+ } from "@secondlayer/shared/runtime";
39030
+ var BACKUP_EXIT = { OK: 0, FAILED: 1, REFUSED: 2 };
39031
+ var MANIFEST_FILE = "manifest.json";
39032
+ var DB_FILE = "db.dump";
39033
+ var SECRETS_FILE = "secrets.enc";
39034
+ var SECRET_ENV_KEYS = [
39035
+ "INSTANCE_TOKEN",
39036
+ "SECONDLAYER_SECRETS_KEY",
39037
+ "STREAMS_SIGNING_PRIVATE_KEY",
39038
+ "SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY",
39039
+ "SL_API_KEY",
39040
+ "ARCHIVE_SIGNING_PUBLIC_KEY"
39041
+ ];
39042
+ function readSecretsKey() {
39043
+ const key = process.env.SECONDLAYER_SECRETS_KEY?.trim();
39044
+ return key ? key : undefined;
39045
+ }
39046
+ function sha256File(path11) {
39047
+ return createHash4("sha256").update(readFileSync7(path11)).digest("hex");
39048
+ }
39049
+ async function readScope() {
39050
+ const db = getDb5();
39051
+ const bounds = await sql4`SELECT MIN(height)::text AS min, MAX(height)::text AS max FROM blocks WHERE canonical = true`.execute(db);
39052
+ const row = bounds.rows[0];
39053
+ const network = process.env.STACKS_NETWORK ?? "mainnet";
39054
+ return {
39055
+ network,
39056
+ fromHeight: row?.min ? Number(row.min) : 0,
39057
+ toHeight: row?.max ? Number(row.max) : null
39058
+ };
39059
+ }
39060
+ function requireDatabaseUrl() {
39061
+ const url = process.env.DATABASE_URL?.trim();
39062
+ if (!url)
39063
+ throw new Error("DATABASE_URL is required");
39064
+ return url;
39065
+ }
39066
+ async function runProcess(cmd, label) {
39067
+ const proc = Bun.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
39068
+ const stderr = await new Response(proc.stderr).text();
39069
+ const code = await proc.exited;
39070
+ if (code !== 0) {
39071
+ return { ok: false, stderr: `${label} exited ${code}: ${stderr.trim()}` };
39072
+ }
39073
+ return { ok: true, stderr };
39074
+ }
39075
+ function attachBackupCommand(cmd) {
39076
+ return cmd.requiredOption("--out <dir>", "directory to write the bundle into").option("--passphrase <passphrase>", "encrypt bundled secrets (or set SECONDLAYER_BACKUP_PASSPHRASE)").option("--no-secrets", "omit keys; the bundle restores data only").option("--json", "Output as JSON").action(async (opts) => {
39077
+ try {
39078
+ await runBackup(opts);
39079
+ } catch (error2) {
39080
+ printError(error2 instanceof Error ? error2.message : String(error2));
39081
+ process.exit(BACKUP_EXIT.FAILED);
39082
+ }
39083
+ });
39084
+ }
39085
+ async function runBackup(opts) {
39086
+ const databaseUrl = requireDatabaseUrl();
39087
+ const includeSecrets = opts.secrets !== false;
39088
+ const passphrase = opts.passphrase ?? process.env.SECONDLAYER_BACKUP_PASSPHRASE ?? "";
39089
+ const secretsKey = readSecretsKey();
39090
+ if (includeSecrets && !secretsKey) {
39091
+ printError("SECONDLAYER_SECRETS_KEY is not set, so this backup cannot carry the keys that decrypt your data. Set it, or pass --no-secrets to take a data-only bundle.");
39092
+ process.exit(BACKUP_EXIT.REFUSED);
39093
+ }
39094
+ const scope = await readScope();
39095
+ const plan = planBackup({
39096
+ network: scope.network,
39097
+ fromHeight: scope.fromHeight,
39098
+ toHeight: scope.toHeight,
39099
+ includeSecrets,
39100
+ secretsEncrypted: includeSecrets && passphrase.length > 0,
39101
+ canary: secretsKey ? sealKeyCanary(secretsKey) : undefined
39102
+ });
39103
+ if (!plan.ok) {
39104
+ printError(plan.reason);
39105
+ process.exit(BACKUP_EXIT.REFUSED);
39106
+ }
39107
+ const outDir = resolve13(opts.out);
39108
+ mkdirSync6(outDir, { recursive: true });
39109
+ console.error("dumping database…");
39110
+ const dumpPath = join17(outDir, DB_FILE);
39111
+ const dump = await runProcess(["pg_dump", "-Fc", "--no-owner", "--no-acl", "-f", dumpPath, databaseUrl], "pg_dump");
39112
+ if (!dump.ok) {
39113
+ printError(dump.stderr);
39114
+ process.exit(BACKUP_EXIT.FAILED);
39115
+ }
39116
+ const manifest = {
39117
+ ...plan.manifest,
39118
+ db: {
39119
+ file: DB_FILE,
39120
+ sha256: sha256File(dumpPath),
39121
+ bytes: statSync3(dumpPath).size,
39122
+ format: "pg_dump-custom"
39123
+ }
39124
+ };
39125
+ if (includeSecrets && secretsKey) {
39126
+ const lines2 = SECRET_ENV_KEYS.filter((k2) => process.env[k2]).map((k2) => `${k2}=${process.env[k2]}`);
39127
+ const sealed = encryptBundle(lines2.join(`
39128
+ `), passphrase);
39129
+ const secretsPath = join17(outDir, SECRETS_FILE);
39130
+ writeFileSync7(secretsPath, sealed);
39131
+ chmodSync(secretsPath, 384);
39132
+ manifest.secrets = {
39133
+ file: SECRETS_FILE,
39134
+ encrypted: true,
39135
+ canary: sealKeyCanary(secretsKey)
39136
+ };
39137
+ }
39138
+ writeFileSync7(join17(outDir, MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}
39139
+ `);
39140
+ output({
39141
+ json: opts.json,
39142
+ data: { out: outDir, manifest },
39143
+ human: () => {
39144
+ success(`backup written to ${outDir}`);
39145
+ note(`blocks ${manifest.scope.from_height}–${manifest.scope.to_height ?? "?"} · ${(manifest.db?.bytes ?? 0) / 1024 ** 2 | 0} MB`);
39146
+ if (!manifest.secrets) {
39147
+ warn("no keys in this bundle — it can restore data, but not a working instance");
39148
+ } else {
39149
+ note("keys are encrypted; keep the passphrase somewhere else");
39150
+ }
39151
+ }
39152
+ });
39153
+ }
39154
+ function attachRestoreCommand(cmd) {
39155
+ return cmd.requiredOption("--from <dir>", "bundle directory to restore from").option("--passphrase <passphrase>", "decrypt bundled secrets (or set SECONDLAYER_BACKUP_PASSPHRASE)").option("--apply", "actually restore (default is a dry run)").option("--force", "restore over a database that already holds chain data").option("--json", "Output as JSON").action(async (opts) => {
39156
+ try {
39157
+ await runRestore(opts);
39158
+ } catch (error2) {
39159
+ printError(error2 instanceof Error ? error2.message : String(error2));
39160
+ process.exit(BACKUP_EXIT.FAILED);
39161
+ }
39162
+ });
39163
+ }
39164
+ async function runRestore(opts) {
39165
+ const databaseUrl = requireDatabaseUrl();
39166
+ const dir = resolve13(opts.from);
39167
+ const manifestPath = join17(dir, MANIFEST_FILE);
39168
+ if (!existsSync8(manifestPath)) {
39169
+ printError(`no ${MANIFEST_FILE} in ${dir}`);
39170
+ process.exit(BACKUP_EXIT.REFUSED);
39171
+ }
39172
+ const manifest = JSON.parse(readFileSync7(manifestPath, "utf8"));
39173
+ const dumpPath = join17(dir, manifest.db?.file ?? DB_FILE);
39174
+ if (manifest.db && sha256File(dumpPath) !== manifest.db.sha256) {
39175
+ printError("the database dump does not match the digest in the manifest; this bundle is corrupt or truncated");
39176
+ process.exit(BACKUP_EXIT.REFUSED);
39177
+ }
39178
+ const envKey = readSecretsKey();
39179
+ let bundleKey;
39180
+ if (manifest.secrets) {
39181
+ const passphrase = opts.passphrase ?? process.env.SECONDLAYER_BACKUP_PASSPHRASE ?? "";
39182
+ if (!passphrase) {
39183
+ printError("this bundle carries encrypted keys; supply --passphrase to read them");
39184
+ process.exit(BACKUP_EXIT.REFUSED);
39185
+ }
39186
+ try {
39187
+ const env2 = decryptBundle(readFileSync7(join17(dir, manifest.secrets.file)), passphrase);
39188
+ bundleKey = env2.split(`
39189
+ `).find((l2) => l2.startsWith("SECONDLAYER_SECRETS_KEY="))?.slice("SECONDLAYER_SECRETS_KEY=".length).trim();
39190
+ } catch {
39191
+ printError("could not decrypt the secrets bundle — wrong passphrase?");
39192
+ process.exit(BACKUP_EXIT.REFUSED);
39193
+ }
39194
+ }
39195
+ if (envKey && manifest.secrets && !keyMatchesCanary(manifest.secrets.canary, envKey)) {
39196
+ printError("SECONDLAYER_SECRETS_KEY in this environment is NOT the key this backup was taken with. Restoring would leave every encrypted column unreadable. Unset it to install the bundle's key, or point this restore at the matching instance.");
39197
+ process.exit(BACKUP_EXIT.REFUSED);
39198
+ }
39199
+ const db = getDb5();
39200
+ const present = await sql4`SELECT to_regclass('public.blocks') IS NOT NULL AS present`.execute(db);
39201
+ let targetIsEmpty = true;
39202
+ if (present.rows[0]?.present) {
39203
+ const existing = await sql4`SELECT COUNT(*)::text AS count FROM blocks`.execute(db);
39204
+ targetIsEmpty = (existing.rows[0]?.count ?? "0") === "0";
39205
+ }
39206
+ const check = precheckRestore({
39207
+ manifest,
39208
+ targetNetwork: process.env.STACKS_NETWORK ?? "mainnet",
39209
+ targetIsEmpty,
39210
+ force: opts.force === true,
39211
+ secretsKeyHex: envKey ?? bundleKey
39212
+ });
39213
+ if (!check.ok) {
39214
+ printError(check.reason);
39215
+ process.exit(BACKUP_EXIT.REFUSED);
39216
+ }
39217
+ if (!opts.apply) {
39218
+ output({
39219
+ json: opts.json,
39220
+ data: { dryRun: true, manifest, targetIsEmpty },
39221
+ human: () => {
39222
+ note("dry run — nothing written. Pass --apply to restore.");
39223
+ note(`would restore ${manifest.network} blocks ${manifest.scope.from_height}–${manifest.scope.to_height ?? "?"}`);
39224
+ if (bundleKey && !envKey) {
39225
+ note("after restoring, install the bundle's keys before starting the runtime");
39226
+ }
39227
+ }
39228
+ });
39229
+ return;
39230
+ }
39231
+ console.error("restoring database…");
39232
+ const restore = await runProcess([
39233
+ "pg_restore",
39234
+ "--no-owner",
39235
+ "--no-acl",
39236
+ "--clean",
39237
+ "--if-exists",
39238
+ "-d",
39239
+ databaseUrl,
39240
+ dumpPath
39241
+ ], "pg_restore");
39242
+ if (!restore.ok) {
39243
+ printError(restore.stderr);
39244
+ process.exit(BACKUP_EXIT.FAILED);
39245
+ }
39246
+ output({
39247
+ json: opts.json,
39248
+ data: { restored: true, manifest },
39249
+ human: () => {
39250
+ success("restore complete");
39251
+ if (bundleKey && !envKey) {
39252
+ warn("set SECONDLAYER_SECRETS_KEY from the bundle before starting the runtime, or it will generate a new one and the restored data will not decrypt");
39253
+ }
39254
+ note("verify with: secondlayer verify all --against <manifest>");
39255
+ }
39256
+ });
39257
+ }
39258
+ function registerBackupCommand(program2) {
39259
+ attachBackupCommand(program2.command("backup").description("Write a restorable bundle: index, keys, and scope"));
39260
+ }
39261
+ function registerRestoreCommand(program2) {
39262
+ attachRestoreCommand(program2.command("restore").description("Restore an instance from a backup bundle"));
39263
+ }
39699
39264
  // src/lib/instance-init.ts
39700
39265
  import { randomBytes as randomBytes3 } from "node:crypto";
39701
- import { existsSync as existsSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync7 } from "node:fs";
39702
- import { resolve as resolve13 } from "node:path";
39266
+ import { existsSync as existsSync9, readFileSync as readFileSync8, writeFileSync as writeFileSync8 } from "node:fs";
39267
+ import { resolve as resolve14 } from "node:path";
39703
39268
  import { generateEd25519KeyPair } from "@secondlayer/shared/crypto/ed25519";
39704
39269
  import { INSTANCE_NETWORKS } from "@secondlayer/shared/db/queries/instance";
39705
39270
  var INSTANCE_ENV_FILE = ".env.local";
@@ -39733,9 +39298,9 @@ function unescapeEnvValue(raw) {
39733
39298
  `).replace(/\\"/g, '"').replace(/\\\\/g, "\\");
39734
39299
  }
39735
39300
  function readEnvValue(envPath, key) {
39736
- if (!existsSync8(envPath))
39301
+ if (!existsSync9(envPath))
39737
39302
  return null;
39738
- const match = readFileSync7(envPath, "utf8").match(new RegExp(`^${key}=(.*)$`, "m"));
39303
+ const match = readFileSync8(envPath, "utf8").match(new RegExp(`^${key}=(.*)$`, "m"));
39739
39304
  return match ? unescapeEnvValue(match[1]) : null;
39740
39305
  }
39741
39306
  function buildInstanceEnv(input4) {
@@ -39778,12 +39343,12 @@ function renderInstanceEnv(env2) {
39778
39343
  `);
39779
39344
  }
39780
39345
  function writeInstanceEnv(cwd, env2, file = INSTANCE_ENV_FILE) {
39781
- const path11 = resolve13(cwd, file);
39782
- writeFileSync7(path11, renderInstanceEnv(env2), { mode: 384 });
39346
+ const path11 = resolve14(cwd, file);
39347
+ writeFileSync8(path11, renderInstanceEnv(env2), { mode: 384 });
39783
39348
  return path11;
39784
39349
  }
39785
39350
  function loadExistingInstanceEnv(cwd, file = INSTANCE_ENV_FILE) {
39786
- const path11 = resolve13(cwd, file);
39351
+ const path11 = resolve14(cwd, file);
39787
39352
  return {
39788
39353
  INSTANCE_TOKEN: readEnvValue(path11, "INSTANCE_TOKEN"),
39789
39354
  SECONDLAYER_SECRETS_KEY: readEnvValue(path11, "SECONDLAYER_SECRETS_KEY"),
@@ -39969,7 +39534,6 @@ registerSubscriptionsCommand(program);
39969
39534
  registerStreamsCommand(program);
39970
39535
  registerIndexCommand(program);
39971
39536
  program.commandsGroup("Project & codegen:");
39972
- registerProjectCommand(program);
39973
39537
  registerCodegenCommand(program);
39974
39538
  var configureGenerate = (cmd) => cmd.option("-c, --config <path>", "Path to config file").option("-o, --output <path>", "Output file path (required when using direct files)").option("-k, --api-key <key>", "Stacks node API key for direct RPC URLs").option("-w, --watch", "Watch for changes").action(async (files, options3) => {
39975
39539
  const { deprecatedCodegenNotice: deprecatedCodegenNotice2 } = await Promise.resolve().then(() => (init_codegen(), exports_codegen));
@@ -39994,11 +39558,11 @@ registerConfigCommand(program);
39994
39558
  registerContextCommand(program);
39995
39559
  registerVerifyCommand(program);
39996
39560
  registerRepairCommand(program);
39561
+ registerBackupCommand(program);
39562
+ registerRestoreCommand(program);
39997
39563
  program.commandsGroup("Account:");
39998
39564
  registerCreditsCommand(program);
39999
- registerAccountCommand(program);
40000
- registerKeysCommand(program);
40001
39565
  program.parse();
40002
39566
 
40003
- //# debugId=1A69CC6C65B95E8B64756E2164756E21
39567
+ //# debugId=D0F621CCA5D6F2E664756E2164756E21
40004
39568
  //# sourceMappingURL=cli.js.map