@secondlayer/cli 21.2.0 → 22.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -15,8 +15,8 @@ secondlayer --version
15
15
  Default API is `http://127.0.0.1:3800`. Override with `SECONDLAYER_API_URL`.
16
16
 
17
17
  Writes against a published bind use `INSTANCE_TOKEN` from `secondlayer init`.
18
- Loopback reads need no token. Hosted API and archive credits use
19
- `SECONDLAYER_API_KEY` (`sk-sl_*`).
18
+ Loopback reads need no token. Your hosted account (credits, archive fetches)
19
+ uses `secondlayer login` or `SECONDLAYER_API_KEY` (`sk-sl_*`).
20
20
 
21
21
  ```bash
22
22
  export SECONDLAYER_API_URL=http://127.0.0.1:3800
package/dist/cli.js CHANGED
@@ -2369,12 +2369,12 @@ async function resolveArchiveOpsBearer() {
2369
2369
  };
2370
2370
  }
2371
2371
  function ignoredEnvKeyNote(ignored) {
2372
- return ignored ? " INSTANCE_TOKEN was ignored: it unlocks this instance, not archive credits." : "";
2372
+ return ignored ? " INSTANCE_TOKEN was ignored: it unlocks this instance, not your account." : "";
2373
2373
  }
2374
2374
  async function httpArchiveOps(path, opts = {}) {
2375
2375
  const { bearer, ignoredEnvKey } = await resolveArchiveOpsBearer();
2376
2376
  if (!bearer) {
2377
- throw new CliHttpError(401, "SESSION_EXPIRED", { error: "Not logged in" }, `Archive credits need an account key. ${ARCHIVE_AUTH_HINT}${ignoredEnvKeyNote(ignoredEnvKey)}`);
2377
+ throw new CliHttpError(401, "SESSION_EXPIRED", { error: "Not logged in" }, `This needs your Secondlayer account. ${ARCHIVE_AUTH_HINT}${ignoredEnvKeyNote(ignoredEnvKey)}`);
2378
2378
  }
2379
2379
  try {
2380
2380
  return await request(`${resolveArchiveOpsUrl()}${path}`, {
@@ -2383,12 +2383,12 @@ async function httpArchiveOps(path, opts = {}) {
2383
2383
  });
2384
2384
  } catch (err) {
2385
2385
  if (err instanceof CliHttpError && err.status === 401) {
2386
- throw new CliHttpError(401, err.code, err.body, `Archive credits rejected the credential (${err.message}). ${ARCHIVE_AUTH_HINT}${ignoredEnvKeyNote(ignoredEnvKey)}`);
2386
+ throw new CliHttpError(401, err.code, err.body, `Your account rejected the credential (${err.message}). ${ARCHIVE_AUTH_HINT}${ignoredEnvKeyNote(ignoredEnvKey)}`);
2387
2387
  }
2388
2388
  throw err;
2389
2389
  }
2390
2390
  }
2391
- var CliHttpError, REQUEST_TIMEOUT_MS = 30000, ARCHIVE_AUTH_HINT = "Set SECONDLAYER_API_KEY to your account key (sk-sl_…, from /account/keys), or run `secondlayer login --credits`.", ARCHIVE_CREDENTIAL_RE;
2391
+ var CliHttpError, REQUEST_TIMEOUT_MS = 30000, ARCHIVE_AUTH_HINT = "Set SECONDLAYER_API_KEY to your account key (sk-sl_…, from /account/keys), or run `secondlayer login`.", ARCHIVE_CREDENTIAL_RE;
2392
2392
  var init_http = __esm(() => {
2393
2393
  init_api_url();
2394
2394
  init_resolve_auth();
@@ -202692,7 +202692,7 @@ var {
202692
202692
  // package.json
202693
202693
  var package_default = {
202694
202694
  name: "@secondlayer/cli",
202695
- version: "21.2.0",
202695
+ version: "22.0.0",
202696
202696
  description: "Run a Stacks index on your own hardware — set it up, restore verified history, and keep it current.",
202697
202697
  repository: {
202698
202698
  type: "git",
@@ -202732,7 +202732,7 @@ var package_default = {
202732
202732
  "@secondlayer/bundler": "^0.7.1",
202733
202733
  "@secondlayer/scaffold": "^1.6.2",
202734
202734
  "@secondlayer/sdk": "^15.1.0",
202735
- "@secondlayer/shared": "^13.1.0",
202735
+ "@secondlayer/shared": "^13.7.0",
202736
202736
  "@secondlayer/stacks": "^6.4.0",
202737
202737
  "@secondlayer/subgraphs": "^7.2.0",
202738
202738
  "@secondlayer/verify": "^0.6.0",
@@ -202811,7 +202811,7 @@ var BUY_CREDITS_COMMAND = "secondlayer credits buy";
202811
202811
  function formatInsufficientMessage(quote) {
202812
202812
  const shortfallUsd = ((quote.usdMicros - quote.balanceUsdMicros) / 1e6).toFixed(2);
202813
202813
  const balanceUsd = (quote.balanceUsdMicros / 1e6).toFixed(2);
202814
- return `Insufficient archive credits: quote $${quote.usd}, balance $${balanceUsd}, short $${shortfallUsd}. Buy more with \`${BUY_CREDITS_COMMAND}\`.`;
202814
+ return `Insufficient credits: quote $${quote.usd}, balance $${balanceUsd}, short $${shortfallUsd}. Buy more with \`${BUY_CREDITS_COMMAND}\`.`;
202815
202815
  }
202816
202816
  var ARCHIVE_GATE_NOT_CONFIGURED_MESSAGE = "the archive gate is not configured on the server; contact the operator";
202817
202817
  var ALLOWANCE_MONTHLY_BUNDLES = 6;
@@ -204986,7 +204986,7 @@ async function runBuy(opts) {
204986
204986
  human: () => {
204987
204987
  success(`Checkout for $${amount} → ${email}`);
204988
204988
  console.log(res.url);
204989
- console.log(dim("After payment: set SECONDLAYER_API_KEY (or run `secondlayer login --credits`), then run `secondlayer credits balance`"));
204989
+ console.log(dim("After payment: set SECONDLAYER_API_KEY (or run `secondlayer login`), then run `secondlayer credits balance`"));
204990
204990
  }
204991
204991
  });
204992
204992
  }
@@ -205061,8 +205061,8 @@ async function runUsage(opts) {
205061
205061
  });
205062
205062
  }
205063
205063
  function registerCreditsCommand(program) {
205064
- const credits = program.command("credits").description("Buy and check archive credits");
205065
- credits.command("buy").description("Open a one-time Stripe checkout for archive credits").requiredOption("--email <email>", "Receipt email; becomes the account").option("--pack <usd>", `One of ${PACKS.join(", ")}`, "25").option("--json", "Output as JSON").action((opts) => runBuy(opts));
205064
+ const credits = program.command("credits").description("Buy and check your credits");
205065
+ credits.command("buy").description("Open a one-time Stripe checkout for credits").requiredOption("--email <email>", "Receipt email; becomes the account").option("--pack <usd>", `One of ${PACKS.join(", ")}`, "25").option("--json", "Output as JSON").action((opts) => runBuy(opts));
205066
205066
  credits.command("balance").description("Show prepaid archive credit balance").option("--json", "Output as JSON").action((opts) => runBalance(opts));
205067
205067
  credits.command("usage").description("Rows delivered per day: free vs paid, by source").option("--days <n>", "Days to show (max 30)", "30").option("--json", "Output as JSON").action((opts) => runUsage(opts));
205068
205068
  credits.command("refill").description("Opt-in auto-refill when the balance drops under a threshold").option("--below <usd>", "Trigger when balance is under this USD amount").option("--pack <usd>", `Pack to buy (${PACKS.join(", ")})`, "25").option("--off", "Turn auto-refill off").option("--json", "Output as JSON").action((opts) => runRefill(opts));
@@ -207801,9 +207801,32 @@ function registerWhoamiCommand(program) {
207801
207801
  // src/commands/login.ts
207802
207802
  init_api_url();
207803
207803
  init_http();
207804
+ import { hostname } from "node:os";
207805
+ import { createInterface } from "node:readline";
207806
+ import { confirm as confirm3, input } from "@inquirer/prompts";
207807
+
207808
+ // src/lib/open-browser.ts
207809
+ import { spawn as spawn3 } from "node:child_process";
207810
+ function browserCommand(url, platform = process.platform) {
207811
+ if (platform === "darwin")
207812
+ return { cmd: "open", args: [url] };
207813
+ if (platform === "win32") {
207814
+ return { cmd: "cmd", args: ["/c", "start", "", url] };
207815
+ }
207816
+ return { cmd: "xdg-open", args: [url] };
207817
+ }
207818
+ function openBrowser(url) {
207819
+ try {
207820
+ const { cmd, args } = browserCommand(url);
207821
+ const child = spawn3(cmd, args, { detached: true, stdio: "ignore" });
207822
+ child.on("error", () => {});
207823
+ child.unref();
207824
+ } catch {}
207825
+ }
207826
+
207827
+ // src/commands/login.ts
207804
207828
  init_output();
207805
207829
  init_session();
207806
- import { confirm as confirm3, input } from "@inquirer/prompts";
207807
207830
  function sessionExpiry() {
207808
207831
  return new Date(Date.now() + 90 * 24 * 60 * 60 * 1000).toISOString();
207809
207832
  }
@@ -207813,16 +207836,16 @@ async function readStdin() {
207813
207836
  chunks.push(chunk);
207814
207837
  return Buffer.concat(chunks).toString("utf8").trim();
207815
207838
  }
207816
- function loginTarget(opts) {
207817
- return opts.credits ? resolveArchiveOpsUrl() : resolveApiUrl();
207839
+ function loginTarget() {
207840
+ return resolveArchiveOpsUrl();
207818
207841
  }
207819
- async function runTokenLogin(opts) {
207842
+ async function runTokenLogin() {
207820
207843
  const token = await readStdin();
207821
207844
  if (!token) {
207822
207845
  error('No token on stdin. Usage: echo "$KEY" | secondlayer login --with-token');
207823
207846
  process.exit(1);
207824
207847
  }
207825
- const target = loginTarget(opts);
207848
+ const target = loginTarget();
207826
207849
  let account;
207827
207850
  try {
207828
207851
  account = await httpAt(target, "/api/accounts/me", { bearer: token });
@@ -207838,31 +207861,123 @@ async function runTokenLogin(opts) {
207838
207861
  }, target);
207839
207862
  success(`Logged in as ${account.email} (${target})`);
207840
207863
  }
207841
- async function runLoginFlow(options = {}) {
207842
- const target = loginTarget(options);
207843
- if (!options.force) {
207844
- const existing = await readSession(target);
207845
- if (existing) {
207846
- info(`Already logged in as ${existing.email} (${target}).`);
207847
- if (!process.stdin.isTTY) {
207848
- info(dim("Run 'secondlayer logout' first, or re-run with --force to switch accounts."));
207849
- return;
207850
- }
207851
- try {
207852
- const proceed = await confirm3({
207853
- message: "Log in as a different user?",
207854
- default: false
207855
- });
207856
- if (!proceed) {
207857
- info(dim("Run 'secondlayer logout' to sign out."));
207858
- return;
207864
+ async function shouldContinueLogin(target, options) {
207865
+ if (options.force)
207866
+ return true;
207867
+ const existing = await readSession(target);
207868
+ if (!existing)
207869
+ return true;
207870
+ info(`Already logged in as ${existing.email} (${target}).`);
207871
+ const hint = "Run 'secondlayer logout' first, or re-run with --force to switch accounts.";
207872
+ if (!process.stdin.isTTY) {
207873
+ info(dim(hint));
207874
+ return false;
207875
+ }
207876
+ try {
207877
+ const proceed = await confirm3({
207878
+ message: "Log in as a different user?",
207879
+ default: false
207880
+ });
207881
+ if (!proceed) {
207882
+ info(dim("Run 'secondlayer logout' to sign out."));
207883
+ return false;
207884
+ }
207885
+ } catch {
207886
+ info(dim(hint));
207887
+ return false;
207888
+ }
207889
+ return true;
207890
+ }
207891
+ async function pollDeviceToken(start, deps) {
207892
+ const deadline = deps.now() + start.expiresIn * 1000;
207893
+ let failures = 0;
207894
+ while (deps.now() < deadline) {
207895
+ await deps.sleep(start.interval * 1000);
207896
+ try {
207897
+ const res = await deps.post("/api/cli/device/token", { deviceCode: start.deviceCode });
207898
+ return {
207899
+ kind: "approved",
207900
+ sessionToken: res.sessionToken,
207901
+ account: res.account
207902
+ };
207903
+ } catch (err) {
207904
+ if (err instanceof CliHttpError) {
207905
+ if (err.status === 428) {
207906
+ failures = 0;
207907
+ continue;
207859
207908
  }
207860
- } catch {
207861
- info(dim("Run 'secondlayer logout' first, or re-run with --force to switch accounts."));
207862
- return;
207909
+ if (err.status === 403 && err.body.error === "access_denied") {
207910
+ return { kind: "denied" };
207911
+ }
207912
+ if (err.status === 410)
207913
+ return { kind: "expired" };
207914
+ throw err;
207863
207915
  }
207916
+ if (++failures >= 3)
207917
+ throw err;
207864
207918
  }
207865
207919
  }
207920
+ return { kind: "expired" };
207921
+ }
207922
+ function clientName() {
207923
+ return hostname().trim().slice(0, 64) || "this machine";
207924
+ }
207925
+ async function runDeviceLogin(options = {}) {
207926
+ const target = loginTarget();
207927
+ if (!await shouldContinueLogin(target, options))
207928
+ return;
207929
+ let start;
207930
+ try {
207931
+ start = await httpAt(target, "/api/cli/device", {
207932
+ method: "POST",
207933
+ body: { clientName: clientName() }
207934
+ });
207935
+ } catch (err) {
207936
+ error(err instanceof Error ? err.message : String(err));
207937
+ process.exit(1);
207938
+ }
207939
+ info(`First copy your one-time code: ${start.userCode}`);
207940
+ const readline = process.stdin.isTTY ? createInterface({ input: process.stdin }) : null;
207941
+ if (readline) {
207942
+ info("Press Enter to open secondlayer.tools in your browser...");
207943
+ readline.once("line", () => openBrowser(start.verificationUrl));
207944
+ }
207945
+ info(dim(`Or open ${start.verificationUrl}`));
207946
+ info(dim("Waiting for you to approve in the browser..."));
207947
+ let outcome;
207948
+ try {
207949
+ outcome = await pollDeviceToken(start, {
207950
+ post: (path, body) => httpAt(target, path, { method: "POST", body }),
207951
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
207952
+ now: Date.now
207953
+ });
207954
+ } catch (err) {
207955
+ readline?.close();
207956
+ error(err instanceof Error ? err.message : String(err));
207957
+ process.exit(1);
207958
+ }
207959
+ readline?.close();
207960
+ if (outcome.kind === "denied") {
207961
+ error("Cancelled in the browser.");
207962
+ process.exit(1);
207963
+ }
207964
+ if (outcome.kind === "expired") {
207965
+ error("The code expired. Run secondlayer login again.");
207966
+ process.exit(1);
207967
+ }
207968
+ await writeSession({
207969
+ token: outcome.sessionToken,
207970
+ email: outcome.account.email,
207971
+ accountId: outcome.account.id,
207972
+ expiresAt: sessionExpiry()
207973
+ }, target);
207974
+ success(`Logged in as ${outcome.account.email} on ${clientName()}`);
207975
+ info(dim("Run 'secondlayer logout' to sign this machine out."));
207976
+ }
207977
+ async function runLoginFlow(options = {}) {
207978
+ const target = loginTarget();
207979
+ if (!await shouldContinueLogin(target, options))
207980
+ return;
207866
207981
  const email = await input({
207867
207982
  message: "Email",
207868
207983
  validate: (v) => /^.+@.+\..+$/.test(v) ? true : "Invalid email"
@@ -207900,7 +208015,7 @@ async function runLoginFlow(options = {}) {
207900
208015
  expiresAt: sessionExpiry()
207901
208016
  }, target);
207902
208017
  success(`Logged in as ${verified.account.email}`);
207903
- info(dim(options.credits ? "Run 'secondlayer credits balance' to see your archive credits." : "Run 'secondlayer whoami' to see your account status."));
208018
+ info(dim("Run 'secondlayer whoami' to see your account status."));
207904
208019
  } catch (err) {
207905
208020
  if (err instanceof CliHttpError) {
207906
208021
  error(err.message);
@@ -207911,17 +208026,18 @@ async function runLoginFlow(options = {}) {
207911
208026
  }
207912
208027
  }
207913
208028
  function registerLoginCommand(program) {
207914
- program.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)").option("--credits", "Log in to the archive credits account used by bootstrap, repair, and credits").addHelpText("after", `
208029
+ program.command("login").description("Log in to Secondlayer through your browser").option("--force", "Skip the already-logged-in check and re-run the flow").option("--email", "Sign in with an emailed 6-digit code instead of a browser").option("--with-token", "Read an API key from stdin (non-interactive)").addHelpText("after", `
207915
208030
  Examples:
207916
- $ secondlayer login --credits
207917
- $ echo "$SECONDLAYER_API_KEY" | secondlayer login --with-token`).action((opts) => opts.withToken ? runTokenLogin({ credits: opts.credits }) : runLoginFlow({ force: opts.force, credits: opts.credits }));
208031
+ $ secondlayer login
208032
+ $ secondlayer login --email
208033
+ $ echo "$SECONDLAYER_API_KEY" | secondlayer login --with-token`).action((opts) => opts.withToken ? runTokenLogin() : opts.email ? runLoginFlow({ force: opts.force }) : runDeviceLogin({ force: opts.force }));
207918
208034
  }
207919
208035
  // src/commands/logout.ts
207920
208036
  init_http();
207921
208037
  init_output();
207922
208038
  init_session();
207923
- async function runLogout(opts = {}) {
207924
- const target = loginTarget(opts);
208039
+ async function runLogout() {
208040
+ const target = loginTarget();
207925
208041
  const session = await readSession(target);
207926
208042
  if (!session) {
207927
208043
  info(`Not logged in (${target}).`);
@@ -207941,7 +208057,7 @@ async function runLogout(opts = {}) {
207941
208057
  success(`Logged out (${target}).`);
207942
208058
  }
207943
208059
  function registerLogoutCommand(program) {
207944
- program.command("logout").description("Log out and revoke the local session").option("--credits", "Log out of the archive credits account used by bootstrap, repair, and credits").action((opts) => runLogout({ credits: opts.credits }));
208060
+ program.command("logout").description("Sign this machine out of your Secondlayer account").action(() => runLogout());
207945
208061
  }
207946
208062
  // src/commands/backup.ts
207947
208063
  init_fs();
@@ -208355,7 +208471,7 @@ function renderInstanceEnv(env) {
208355
208471
  `SECONDLAYER_API_URL=${env.SECONDLAYER_API_URL}`
208356
208472
  ];
208357
208473
  if (env.SECONDLAYER_API_KEY) {
208358
- lines.push("# Hosted account key for archive credits (sk-sl_*).", `SECONDLAYER_API_KEY=${env.SECONDLAYER_API_KEY}`);
208474
+ lines.push("# Hosted account key for credits and archive fetches (sk-sl_*).", `SECONDLAYER_API_KEY=${env.SECONDLAYER_API_KEY}`);
208359
208475
  }
208360
208476
  lines.push("# Archive trust root: bootstrap, verify, and repair check manifests", "# against this key. Replace it to pin a different archive.", `ARCHIVE_SIGNING_PUBLIC_KEY=${escapeEnvValue(env.ARCHIVE_SIGNING_PUBLIC_KEY)}`, "");
208361
208477
  return lines.join(`
@@ -208543,5 +208659,5 @@ registerCreditsCommand(program);
208543
208659
  registerArchiveCommand(program);
208544
208660
  program.parse();
208545
208661
 
208546
- //# debugId=FA7A9048A9D4FA1264756E2164756E21
208662
+ //# debugId=32DC3B7BA2B6602D64756E2164756E21
208547
208663
  //# sourceMappingURL=cli.js.map