@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.
- package/dist/index.js +323 -10
- 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
|
-
|
|
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 `${
|
|
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.
|
|
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_…`
|
|
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
|
-
/**
|
|
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("
|
|
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 ? {
|
|
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.
|
|
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": {
|