@seekrit/cli 0.26.0 → 0.27.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.
Files changed (2) hide show
  1. package/dist/index.js +323 -10
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import { spawn, spawnSync } from "node:child_process";
3
3
  import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
4
4
  import { z } from "zod";
5
5
  import { Command } from "commander";
6
- import { homedir, tmpdir } from "node:os";
6
+ import { homedir, hostname, tmpdir, userInfo } from "node:os";
7
7
  import { dirname, join, parse } from "node:path";
8
8
  import { createInterface } from "node:readline";
9
9
  import { Writable } from "node:stream";
@@ -1064,6 +1064,16 @@ z.object({
1064
1064
  expiresAt: z.iso.datetime().nullish()
1065
1065
  });
1066
1066
  z.object({ family: planFamilySchema });
1067
+ z.object({
1068
+ sessionId: z.string().regex(/^skc_[0-9A-Za-z]+$/),
1069
+ /** SHA-256 hash (base64url) of the full session token string. */
1070
+ tokenHash: z.string().min(1).max(128),
1071
+ /** Display-only, e.g. `miles@studio.local`. */
1072
+ deviceLabel: z.string().trim().min(1).max(120),
1073
+ /** Display-only, e.g. `cli/0.4.2`. */
1074
+ client: z.string().trim().max(60).optional()
1075
+ });
1076
+ z.object({ code: z.string().trim().min(1).max(32) });
1067
1077
  z.object({
1068
1078
  cursor: z.string().optional(),
1069
1079
  limit: z.coerce.number().int().min(1).max(200).default(50),
@@ -2139,9 +2149,12 @@ function encodeOpensshPrivateKey(seed, pub, comment) {
2139
2149
  * Format: `skt_<token id>_<private key pkcs8, base64url>`
2140
2150
  */
2141
2151
  const TOKEN_PREFIX = "skt";
2152
+ const CLI_SESSION_PREFIX = "skc";
2142
2153
  const TOKEN_ID_LENGTH = 22;
2143
2154
  const ID_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
2144
- function randomTokenId() {
2155
+ /** 32 bytes of entropy for the CLI session secret. */
2156
+ const CLI_SESSION_SECRET_BYTES = 32;
2157
+ function randomTokenId(prefix = TOKEN_PREFIX) {
2145
2158
  let out = "";
2146
2159
  while (out.length < TOKEN_ID_LENGTH) {
2147
2160
  const bytes = crypto.getRandomValues(new Uint8Array(TOKEN_ID_LENGTH - out.length));
@@ -2150,7 +2163,7 @@ function randomTokenId() {
2150
2163
  if (out.length === TOKEN_ID_LENGTH) break;
2151
2164
  }
2152
2165
  }
2153
- return `${TOKEN_PREFIX}_${out}`;
2166
+ return `${prefix}_${out}`;
2154
2167
  }
2155
2168
  async function hashToken(token) {
2156
2169
  const digest = await crypto.subtle.digest("SHA-256", utf8Encode(token));
@@ -2184,9 +2197,27 @@ async function parseServiceToken(token) {
2184
2197
  function isServiceToken(value) {
2185
2198
  return value.startsWith(`${TOKEN_PREFIX}_`);
2186
2199
  }
2200
+ async function createCliSessionToken() {
2201
+ const sessionId = randomTokenId(CLI_SESSION_PREFIX);
2202
+ const token = `${sessionId}_${toBase64Url(crypto.getRandomValues(new Uint8Array(CLI_SESSION_SECRET_BYTES)))}`;
2203
+ return {
2204
+ token,
2205
+ sessionId,
2206
+ tokenHash: await hashToken(token)
2207
+ };
2208
+ }
2209
+ /** The public `skc_…` id embedded in a CLI session token. */
2210
+ function parseCliSessionToken(token) {
2211
+ const match = /^(skc_[0-9A-Za-z]+)_([A-Za-z0-9_-]+)$/.exec(token);
2212
+ if (!match) throw new SeekritCryptoError("MALFORMED_TOKEN", "not a valid seekrit CLI session token");
2213
+ return { sessionId: match[1] };
2214
+ }
2215
+ function isCliSessionToken(value) {
2216
+ return value.startsWith(`${CLI_SESSION_PREFIX}_`);
2217
+ }
2187
2218
  //#endregion
2188
2219
  //#region package.json
2189
- var version = "0.26.0";
2220
+ var version = "0.27.0";
2190
2221
  //#endregion
2191
2222
  //#region ../../packages/api-client/src/index.ts
2192
2223
  var SeekritApiError = class extends Error {
@@ -2248,6 +2279,32 @@ var SeekritClient = class {
2248
2279
  getMyNotificationPrefs() {
2249
2280
  return this.request("GET", "/v1/me/notifications");
2250
2281
  }
2282
+ /**
2283
+ * Devices this user has authorized. `currentSessionId` is set when the caller
2284
+ * *is* a CLI session, so it can label (or revoke) itself.
2285
+ */
2286
+ listCliSessions() {
2287
+ return this.request("GET", "/v1/me/cli-sessions");
2288
+ }
2289
+ /** Sign a device out. Its token stops authenticating immediately. */
2290
+ revokeCliSession(sessionId) {
2291
+ return this.request("DELETE", `/v1/me/cli-sessions/${sessionId}`);
2292
+ }
2293
+ /** What a pending login request is asking for — for the approval screen. */
2294
+ getCliLoginRequest(code) {
2295
+ return this.request("GET", `/v1/cli-login/${encodeURIComponent(code)}`);
2296
+ }
2297
+ /**
2298
+ * Authorize a device. Requires a browser session; members with a second
2299
+ * factor must have re-entered it just now, else this rejects with
2300
+ * `mfa_required` (recoverable — prompt for a code and retry).
2301
+ */
2302
+ approveCliLogin(code) {
2303
+ return this.request("POST", `/v1/cli-login/${encodeURIComponent(code)}/approve`);
2304
+ }
2305
+ denyCliLogin(code) {
2306
+ return this.request("POST", `/v1/cli-login/${encodeURIComponent(code)}/deny`);
2307
+ }
2251
2308
  setMyNotificationPrefs(input) {
2252
2309
  return this.request("PUT", "/v1/me/notifications", input);
2253
2310
  }
@@ -2591,6 +2648,39 @@ var SeekritClient = class {
2591
2648
  return this.request("POST", `/v1/orgs/${orgId}/billing/cancel`);
2592
2649
  }
2593
2650
  };
2651
+ async function unauthenticatedPost(baseUrl, path, body, fetchImpl, client) {
2652
+ const headers = {
2653
+ accept: "application/json",
2654
+ "content-type": "application/json"
2655
+ };
2656
+ if (client) headers["x-seekrit-client"] = client;
2657
+ const res = await fetchImpl(`${baseUrl.replace(/\/$/, "")}${path}`, {
2658
+ method: "POST",
2659
+ headers,
2660
+ body: JSON.stringify(body)
2661
+ });
2662
+ if (!res.ok) {
2663
+ const fallback = { error: {
2664
+ code: "internal",
2665
+ message: `HTTP ${res.status}`
2666
+ } };
2667
+ const payload = await res.json().catch(() => fallback);
2668
+ throw new SeekritApiError(res.status, payload.error?.code ?? "internal", payload.error?.message ?? `HTTP ${res.status}`);
2669
+ }
2670
+ return await res.json();
2671
+ }
2672
+ /**
2673
+ * Open a browser-approved login request. `input.tokenHash` is the SHA-256 of a
2674
+ * session token the caller minted locally and keeps — never send the token.
2675
+ */
2676
+ function startCliLogin(baseUrl, input, options = {}) {
2677
+ return unauthenticatedPost(baseUrl, "/v1/cli-login", input, options.fetch ?? ((...args) => fetch(...args)), options.client);
2678
+ }
2679
+ /** Ask whether a human has authorized the request yet. */
2680
+ function pollCliLogin(baseUrl, code, options = {}) {
2681
+ const fetchImpl = options.fetch ?? ((...args) => fetch(...args));
2682
+ return unauthenticatedPost(baseUrl, "/v1/cli-login/poll", { code }, fetchImpl, options.client);
2683
+ }
2594
2684
  const PROJECT_FILE = "seekrit.json";
2595
2685
  function globalConfigPath() {
2596
2686
  return join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "seekrit", "config.json");
@@ -2600,6 +2690,10 @@ function readGlobalConfig() {
2600
2690
  if (!existsSync(path)) return {};
2601
2691
  return JSON.parse(readFileSync(path, "utf8"));
2602
2692
  }
2693
+ /**
2694
+ * Merge into the saved config. A key set to `undefined` is *removed* (JSON
2695
+ * drops it), which is how the login paths clear a credential they replace.
2696
+ */
2603
2697
  function writeGlobalConfig(update) {
2604
2698
  const path = globalConfigPath();
2605
2699
  const merged = {
@@ -2671,6 +2765,29 @@ function promptHidden(question) {
2671
2765
  });
2672
2766
  });
2673
2767
  }
2768
+ /**
2769
+ * Wait for the user to press Enter (or Ctrl-C). Resolves immediately when stdin
2770
+ * isn't a TTY — a piped or CI invocation has nobody to press a key, and blocking
2771
+ * there would hang `seekrit login` forever.
2772
+ */
2773
+ function promptEnter(question) {
2774
+ if (!process.stdin.isTTY) {
2775
+ process.stderr.write("\n");
2776
+ return Promise.resolve();
2777
+ }
2778
+ process.stderr.write(question);
2779
+ const rl = createInterface({
2780
+ input: process.stdin,
2781
+ output: process.stderr,
2782
+ terminal: true
2783
+ });
2784
+ return new Promise((resolve) => {
2785
+ rl.question("", () => {
2786
+ rl.close();
2787
+ resolve();
2788
+ });
2789
+ });
2790
+ }
2674
2791
  /** Read all of stdin (for `seekrit secrets set NAME -` piping). */
2675
2792
  async function readStdin() {
2676
2793
  const chunks = [];
@@ -2696,7 +2813,7 @@ function tryBuildContext(dotenvVars = {}) {
2696
2813
  const config = readGlobalConfig();
2697
2814
  const fromEnv = (key) => process.env[key] ?? dotenvVars[key];
2698
2815
  const apiUrl = fromEnv("SEEKRIT_API_URL") ?? config.apiUrl ?? "https://api.seekrit.dev";
2699
- const token = fromEnv("SEEKRIT_TOKEN") ?? config.token;
2816
+ const token = fromEnv("SEEKRIT_TOKEN") ?? config.token ?? config.sessionToken;
2700
2817
  const devUser = fromEnv("SEEKRIT_DEV_USER") ?? config.devUser;
2701
2818
  let auth;
2702
2819
  if (token) auth = {
@@ -2719,7 +2836,7 @@ function tryBuildContext(dotenvVars = {}) {
2719
2836
  }
2720
2837
  function buildContext() {
2721
2838
  const ctx = tryBuildContext();
2722
- if (!ctx) fail("no credentials found — run `seekrit login --token skt_…` (or `--client-id … --client-secret …`), or set SEEKRIT_TOKEN / SEEKRIT_CLIENT_ID + SEEKRIT_CLIENT_SECRET / SEEKRIT_DEV_USER");
2839
+ if (!ctx) fail("no credentials found — run `seekrit login` to sign in through your browser (or `seekrit login --token skt_…` / `--client-id … --client-secret …`), or set SEEKRIT_TOKEN / SEEKRIT_CLIENT_ID + SEEKRIT_CLIENT_SECRET / SEEKRIT_DEV_USER");
2723
2840
  return ctx;
2724
2841
  }
2725
2842
  function isTokenAuth(ctx) {
@@ -3787,12 +3904,16 @@ function readM2mCreds(dotenvVars = {}) {
3787
3904
  clientSecret
3788
3905
  };
3789
3906
  }
3790
- /** True when a service/dev credential is already configured explicitly. */
3907
+ /**
3908
+ * True when a service/session/dev credential is already configured explicitly.
3909
+ * A browser-authorized session counts: a human who ran `seekrit login` must not
3910
+ * be silently swapped onto a machine identity.
3911
+ */
3791
3912
  function hasExplicitCredential(dotenvVars) {
3792
3913
  const fromEnv = (key) => process.env[key] ?? dotenvVars[key];
3793
3914
  if (fromEnv("SEEKRIT_TOKEN") || fromEnv("SEEKRIT_DEV_USER")) return true;
3794
3915
  const config = readGlobalConfig();
3795
- return Boolean(config.token || config.devUser);
3916
+ return Boolean(config.token || config.sessionToken || config.devUser);
3796
3917
  }
3797
3918
  /** Mint a fresh org-scoped admin token using M2M credentials (keyless). */
3798
3919
  async function mintAdminToken(apiUrl, creds) {
@@ -4735,6 +4856,181 @@ function collect(value, acc) {
4735
4856
  return acc;
4736
4857
  }
4737
4858
  //#endregion
4859
+ //#region src/web-login.ts
4860
+ /**
4861
+ * `seekrit login` — sign in through the browser.
4862
+ *
4863
+ * The credential is born here and never leaves: we mint a CLI session token
4864
+ * locally, register only its SHA-256 hash, and wait for a human to authorize
4865
+ * that hash in the dashboard. When they do, we already hold the token — the
4866
+ * approval round-trip carries nothing secret, so there is no window in which the
4867
+ * API (or anything watching it) could learn our credential.
4868
+ *
4869
+ * The saved session authenticates as *you*, which is why it needs no org, app,
4870
+ * or environment selection: commands see every org you're a member of, and
4871
+ * decryption still runs through your own passphrase-unlocked key. Machines want
4872
+ * the opposite trade — a scoped, key-carrying credential — and keep using
4873
+ * `seekrit login --token skt_…`.
4874
+ */
4875
+ /** Open a URL in the platform's default browser. Best-effort and silent. */
4876
+ function openBrowser(url) {
4877
+ const [command, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", [
4878
+ "/c",
4879
+ "start",
4880
+ "",
4881
+ url
4882
+ ]] : ["xdg-open", [url]];
4883
+ try {
4884
+ const child = spawn(command, args, {
4885
+ stdio: "ignore",
4886
+ detached: true
4887
+ });
4888
+ child.on("error", () => void 0);
4889
+ child.unref();
4890
+ } catch {}
4891
+ }
4892
+ /** A frame of the waiting spinner, or a static line on a non-TTY. */
4893
+ const SPINNER = [
4894
+ "⠋",
4895
+ "⠙",
4896
+ "⠹",
4897
+ "⠸",
4898
+ "⠼",
4899
+ "⠴",
4900
+ "⠦",
4901
+ "⠧",
4902
+ "⠇",
4903
+ "⠏"
4904
+ ];
4905
+ function startWaitingIndicator(message) {
4906
+ if (!process.stderr.isTTY) {
4907
+ process.stderr.write(`${message}\n`);
4908
+ return () => void 0;
4909
+ }
4910
+ let frame = 0;
4911
+ const timer = setInterval(() => {
4912
+ process.stderr.write(`\r${SPINNER[frame % SPINNER.length]} ${message}`);
4913
+ frame++;
4914
+ }, 80);
4915
+ return () => {
4916
+ clearInterval(timer);
4917
+ process.stderr.write(`\r${" ".repeat(message.length + 2)}\r`);
4918
+ };
4919
+ }
4920
+ function sleep(ms) {
4921
+ return new Promise((resolve) => setTimeout(resolve, ms));
4922
+ }
4923
+ async function runWebLogin(options) {
4924
+ const config = readGlobalConfig();
4925
+ const apiUrl = options.apiUrl ?? process.env.SEEKRIT_API_URL ?? config.apiUrl ?? "https://api.seekrit.dev";
4926
+ const client = `cli/${version}`;
4927
+ const session = await createCliSessionToken();
4928
+ const deviceLabel = `${userInfo().username}@${hostname()}`;
4929
+ const started = await startCliLogin(apiUrl, {
4930
+ sessionId: session.sessionId,
4931
+ tokenHash: session.tokenHash,
4932
+ deviceLabel,
4933
+ client
4934
+ }, { client }).catch((err) => {
4935
+ fail(`couldn't start sign-in: ${err instanceof Error ? err.message : String(err)}`);
4936
+ });
4937
+ console.error(`Sign in to seekrit to authorize this device (${deviceLabel}).\n`);
4938
+ console.error(` ${started.verifyUrl}\n`);
4939
+ console.error(` code: ${started.code} — check it matches the one in your browser\n`);
4940
+ if (options.browser === false) console.error("Open that URL to continue.\n");
4941
+ else {
4942
+ await promptEnter("Press [Enter] to open it in your browser (Ctrl-C to cancel)… ");
4943
+ openBrowser(started.verifyUrl);
4944
+ }
4945
+ const stopWaiting = startWaitingIndicator("Waiting for you to authorize…");
4946
+ try {
4947
+ const deadline = Date.parse(started.requestExpiresAt);
4948
+ while (true) {
4949
+ const result = await pollCliLogin(apiUrl, started.code, { client }).catch(() => null);
4950
+ if (result?.status === "approved") {
4951
+ writeGlobalConfig({
4952
+ sessionToken: session.token,
4953
+ token: void 0,
4954
+ ...options.apiUrl ? { apiUrl: options.apiUrl } : {}
4955
+ });
4956
+ stopWaiting();
4957
+ const who = result.email ?? "your account";
4958
+ console.error(`Signed in as ${who} — this device is authorized for 90 days.`);
4959
+ if (config.token) console.error("(the service token saved here was replaced; SEEKRIT_TOKEN still wins)");
4960
+ await reportOrgs(apiUrl, session.token, client);
4961
+ return;
4962
+ }
4963
+ if (result?.status === "denied") {
4964
+ stopWaiting();
4965
+ fail("sign-in was declined in the browser");
4966
+ }
4967
+ if (result?.status === "expired" || Date.now() > deadline) {
4968
+ stopWaiting();
4969
+ fail("sign-in request expired — run `seekrit login` again");
4970
+ }
4971
+ await sleep(started.pollIntervalSeconds * 1e3);
4972
+ }
4973
+ } finally {
4974
+ stopWaiting();
4975
+ }
4976
+ }
4977
+ /**
4978
+ * Print what the new session can reach, so a successful login ends with proof it
4979
+ * works rather than a bare "ok". Best-effort: a hiccup here doesn't undo a
4980
+ * login that already succeeded.
4981
+ */
4982
+ async function reportOrgs(apiUrl, token, client) {
4983
+ try {
4984
+ const { user, orgs } = await new SeekritClient({
4985
+ baseUrl: apiUrl,
4986
+ auth: {
4987
+ type: "bearer",
4988
+ token
4989
+ },
4990
+ client
4991
+ }).me();
4992
+ for (const org of orgs) console.error(` ${org.slug} (${org.role})`);
4993
+ if (!user.hasKeys) console.error("\nNext: run `seekrit keys setup` to create your encryption keys.");
4994
+ } catch {}
4995
+ }
4996
+ /**
4997
+ * `seekrit logout` — drop the saved credential, and revoke it server-side when
4998
+ * it's a CLI session (the one credential this machine owns outright). A service
4999
+ * token is shared infrastructure that other machines may hold, so it is only
5000
+ * removed locally, never revoked out from under them.
5001
+ */
5002
+ async function runLogout() {
5003
+ const config = readGlobalConfig();
5004
+ const sessionToken = config.sessionToken;
5005
+ if (!sessionToken && !config.token && !config.devUser && !config.clientId) {
5006
+ console.error("not signed in");
5007
+ return;
5008
+ }
5009
+ if (sessionToken) {
5010
+ const api = new SeekritClient({
5011
+ baseUrl: process.env.SEEKRIT_API_URL ?? config.apiUrl ?? "https://api.seekrit.dev",
5012
+ auth: {
5013
+ type: "bearer",
5014
+ token: sessionToken
5015
+ },
5016
+ client: `cli/${version}`
5017
+ });
5018
+ try {
5019
+ const { currentSessionId } = await api.listCliSessions();
5020
+ if (currentSessionId) await api.revokeCliSession(currentSessionId);
5021
+ console.error("signed out — this device is no longer authorized");
5022
+ } catch (err) {
5023
+ console.error(`signed out locally, but couldn't revoke the session: ${err instanceof Error ? err.message : String(err)}`);
5024
+ }
5025
+ }
5026
+ writeGlobalConfig({
5027
+ sessionToken: void 0,
5028
+ token: void 0,
5029
+ devUser: void 0
5030
+ });
5031
+ if (config.clientId) console.error("(machine client credentials are kept — remove them with `seekrit login`)");
5032
+ }
5033
+ //#endregion
4738
5034
  //#region src/index.ts
4739
5035
  /** Collect repeated `--with group=env` flags into a map. */
4740
5036
  function collectKv(value, acc = {}) {
@@ -4752,11 +5048,21 @@ const program = new Command("seekrit").description("End-to-end encrypted secrets
4752
5048
  program.hook("preAction", async () => {
4753
5049
  await ensureM2mAdminToken();
4754
5050
  });
4755
- program.command("login").description("store credentials for the API").option("--token <token>", "service token (skt_…)").option("--client-id <id>", "machine (M2M) client id — auto-mints an admin token").option("--client-secret <secret>", "machine (M2M) client secret").option("--dev-user <email>", "dev-mode identity (local API with AUTH_MODE=dev)").option("--api-url <url>", "API base URL").action((options) => {
5051
+ program.command("login").description("sign in through the browser (or pass a credential to store one directly)").option("--token <token>", "service token (skt_…) — skips the browser").option("--client-id <id>", "machine (M2M) client id — auto-mints an admin token").option("--client-secret <secret>", "machine (M2M) client secret").option("--dev-user <email>", "dev-mode identity (local API with AUTH_MODE=dev)").option("--api-url <url>", "API base URL").option("--no-browser", "print the sign-in URL instead of opening it").action(async (options) => {
4756
5052
  if (options.token && !isServiceToken(options.token)) fail("token must start with skt_");
4757
5053
  if (Boolean(options.clientId) !== Boolean(options.clientSecret)) fail("--client-id and --client-secret must be given together");
5054
+ if (!(options.token || options.clientId || options.devUser)) {
5055
+ await runWebLogin({
5056
+ apiUrl: options.apiUrl,
5057
+ browser: options.browser
5058
+ });
5059
+ return;
5060
+ }
4758
5061
  writeGlobalConfig({
4759
- ...options.token ? { token: options.token } : {},
5062
+ ...options.token ? {
5063
+ token: options.token,
5064
+ sessionToken: void 0
5065
+ } : {},
4760
5066
  ...options.clientId ? { clientId: options.clientId } : {},
4761
5067
  ...options.clientSecret ? { clientSecret: options.clientSecret } : {},
4762
5068
  ...options.devUser ? { devUser: options.devUser } : {},
@@ -4764,6 +5070,9 @@ program.command("login").description("store credentials for the API").option("--
4764
5070
  });
4765
5071
  console.error("credentials saved");
4766
5072
  });
5073
+ program.command("logout").description("forget the saved credentials (revokes a browser-authorized session)").action(async () => {
5074
+ await runLogout();
5075
+ });
4767
5076
  program.command("whoami").description("show the authenticated identity").action(async () => {
4768
5077
  const ctx = buildContext();
4769
5078
  if (isTokenAuth(ctx) && ctx.auth.type === "bearer") {
@@ -4780,6 +5089,10 @@ program.command("whoami").description("show the authenticated identity").action(
4780
5089
  const { user, orgs } = await ctx.client.me();
4781
5090
  console.log(`${user.email}${user.hasKeys ? "" : " (key setup pending — run `seekrit keys setup`)"}`);
4782
5091
  for (const org of orgs) console.log(` ${org.slug} (${org.role})`);
5092
+ if (ctx.auth.type === "bearer" && isCliSessionToken(ctx.auth.token)) {
5093
+ const { sessionId } = parseCliSessionToken(ctx.auth.token);
5094
+ console.log(` via CLI session ${sessionId} (revoke it with \`seekrit logout\`)`);
5095
+ }
4783
5096
  });
4784
5097
  program.command("keys").description("manage your encryption keys").command("setup").description("generate your keypair and protect it with a passphrase").action(async () => {
4785
5098
  const ctx = buildContext();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seekrit/cli",
3
- "version": "0.26.0",
3
+ "version": "0.27.0",
4
4
  "description": "End-to-end encrypted secrets manager CLI — inject decrypted secrets into any command.",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -26,8 +26,8 @@
26
26
  "devDependencies": {
27
27
  "@types/node": "^26.1.0",
28
28
  "tsdown": "^0.22.3",
29
- "@seekrit/core": "0.0.1",
30
29
  "@seekrit/api-client": "0.0.1",
30
+ "@seekrit/core": "0.0.1",
31
31
  "@seekrit/crypto": "0.0.1"
32
32
  },
33
33
  "scripts": {