@barekey/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,49 @@
1
+ import { access, readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ async function fileExists(filePath) {
4
+ try {
5
+ await access(filePath);
6
+ return true;
7
+ }
8
+ catch {
9
+ return false;
10
+ }
11
+ }
12
+ async function findBarekeyConfigPath(startDirectory) {
13
+ let current = path.resolve(startDirectory);
14
+ while (true) {
15
+ const candidate = path.join(current, "barekey.json");
16
+ if (await fileExists(candidate)) {
17
+ return candidate;
18
+ }
19
+ const parent = path.dirname(current);
20
+ if (parent === current) {
21
+ return null;
22
+ }
23
+ current = parent;
24
+ }
25
+ }
26
+ export async function loadRuntimeConfig() {
27
+ const configPath = await findBarekeyConfigPath(process.cwd());
28
+ if (configPath === null) {
29
+ return null;
30
+ }
31
+ const raw = await readFile(configPath, "utf8");
32
+ const parsed = JSON.parse(raw);
33
+ return {
34
+ path: configPath,
35
+ config: {
36
+ org: typeof parsed.organization === "string"
37
+ ? parsed.organization.trim()
38
+ : typeof parsed.org === "string"
39
+ ? parsed.org.trim()
40
+ : undefined,
41
+ project: typeof parsed.project === "string" ? parsed.project.trim() : undefined,
42
+ environment: typeof parsed.environment === "string"
43
+ ? parsed.environment.trim()
44
+ : typeof parsed.stage === "string"
45
+ ? parsed.stage.trim()
46
+ : undefined,
47
+ },
48
+ };
49
+ }
@@ -0,0 +1,20 @@
1
+ export type TypegenManifest = {
2
+ orgId: string;
3
+ orgSlug: string;
4
+ projectSlug: string;
5
+ stageSlug: string;
6
+ generatedAtMs: number;
7
+ manifestVersion: string;
8
+ variables: Array<{
9
+ name: string;
10
+ kind: "secret" | "ab_roll" | "rollout";
11
+ declaredType: "string" | "boolean" | "int64" | "float" | "date" | "json";
12
+ required: boolean;
13
+ updatedAtMs: number;
14
+ typeScriptType: string;
15
+ }>;
16
+ };
17
+ export declare function writeTypegenFile(input: {
18
+ manifest: TypegenManifest;
19
+ outPath: string;
20
+ }): Promise<void>;
@@ -0,0 +1,14 @@
1
+ import { writeFile } from "node:fs/promises";
2
+ export async function writeTypegenFile(input) {
3
+ const keys = input.manifest.variables
4
+ .map((row) => row.name)
5
+ .sort((left, right) => left.localeCompare(right));
6
+ const unionLines = keys.map((key) => ` | ${JSON.stringify(key)}`).join("\n");
7
+ const mapLines = input.manifest.variables
8
+ .slice()
9
+ .sort((left, right) => left.name.localeCompare(right.name))
10
+ .map((row) => ` ${JSON.stringify(row.name)}: ${row.typeScriptType};`)
11
+ .join("\n");
12
+ const contents = `/* eslint-disable */\n/* This file is generated by barekey typegen. */\n\nimport type { BarekeyTemporalInstant } from "@barekey/sdk";\nimport "@barekey/sdk";\n\ndeclare module "@barekey/sdk" {\n interface BarekeyGeneratedTypeMap {\n${mapLines}\n }\n}\n\nexport type BarekeyKnownKey =\n${unionLines.length > 0 ? unionLines : " never"};\n\nexport const barekeyManifestVersion = ${JSON.stringify(input.manifest.manifestVersion)};\n`;
13
+ await writeFile(input.outPath, contents, "utf8");
14
+ }
@@ -0,0 +1,13 @@
1
+ export type CliCredentials = {
2
+ accessToken: string;
3
+ refreshToken: string;
4
+ accessTokenExpiresAtMs: number;
5
+ refreshTokenExpiresAtMs: number;
6
+ clerkUserId: string;
7
+ orgId: string;
8
+ orgSlug: string;
9
+ };
10
+ export type CliConfig = {
11
+ baseUrl: string;
12
+ activeAccountId: string;
13
+ };
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@barekey/cli",
3
+ "version": "0.1.0",
4
+ "description": "Barekey command line interface",
5
+ "type": "module",
6
+ "bin": {
7
+ "barekey": "./dist/index.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc -p tsconfig.json",
11
+ "typecheck": "tsc -p tsconfig.json --noEmit",
12
+ "start": "node dist/index.js"
13
+ },
14
+ "dependencies": {
15
+ "@clack/prompts": "^0.11.0",
16
+ "@barekey/sdk": "^0.1.0",
17
+ "commander": "^14.0.1",
18
+ "open": "^10.2.0",
19
+ "picocolors": "^1.1.1"
20
+ },
21
+ "devDependencies": {
22
+ "@types/node": "^24.10.1",
23
+ "typescript": "^5.9.3"
24
+ },
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
28
+ "license": "BSD-3-Clause",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "https://github.com/usebarekey/cli.git"
32
+ },
33
+ "homepage": "https://github.com/usebarekey/cli#readme",
34
+ "bugs": {
35
+ "url": "https://github.com/usebarekey/cli/issues"
36
+ }
37
+ }
@@ -0,0 +1,86 @@
1
+ import { postJson } from "./http.js";
2
+ import type { CliCredentials } from "./types.js";
3
+ import { loadConfig, loadCredentials, saveCredentials } from "./credentials-store.js";
4
+
5
+ type CliAuthProvider = {
6
+ getAccessToken(): Promise<string>;
7
+ onAuthError?(): Promise<void>;
8
+ };
9
+
10
+ export function createCliAuthProvider(): CliAuthProvider {
11
+ let cachedCredentials: CliCredentials | null = null;
12
+ let forceRefresh = false;
13
+
14
+ async function readCurrentCredentials(): Promise<{
15
+ baseUrl: string;
16
+ accountId: string;
17
+ credentials: CliCredentials;
18
+ }> {
19
+ const config = await loadConfig();
20
+ if (config === null) {
21
+ throw new Error("Not logged in. Run barekey login first.");
22
+ }
23
+
24
+ const credentials = await loadCredentials(config.activeAccountId);
25
+ if (credentials === null) {
26
+ throw new Error("CLI credentials are missing. Run barekey login again.");
27
+ }
28
+
29
+ cachedCredentials = credentials;
30
+
31
+ return {
32
+ baseUrl: config.baseUrl,
33
+ accountId: config.activeAccountId,
34
+ credentials,
35
+ };
36
+ }
37
+
38
+ async function refreshIfNeeded(): Promise<CliCredentials> {
39
+ const { baseUrl, accountId, credentials } = await readCurrentCredentials();
40
+ const now = Date.now();
41
+ if (!forceRefresh && credentials.accessTokenExpiresAtMs > now + 10_000) {
42
+ return credentials;
43
+ }
44
+
45
+ const refreshed = await postJson<{
46
+ accessToken: string;
47
+ refreshToken: string;
48
+ accessTokenExpiresAtMs: number;
49
+ refreshTokenExpiresAtMs: number;
50
+ clerkUserId: string;
51
+ orgId: string;
52
+ orgSlug: string;
53
+ }>({
54
+ baseUrl,
55
+ path: "/v1/cli/token/refresh",
56
+ payload: {
57
+ refreshToken: credentials.refreshToken,
58
+ },
59
+ });
60
+
61
+ const nextCredentials: CliCredentials = {
62
+ accessToken: refreshed.accessToken,
63
+ refreshToken: refreshed.refreshToken,
64
+ accessTokenExpiresAtMs: refreshed.accessTokenExpiresAtMs,
65
+ refreshTokenExpiresAtMs: refreshed.refreshTokenExpiresAtMs,
66
+ clerkUserId: refreshed.clerkUserId,
67
+ orgId: refreshed.orgId,
68
+ orgSlug: refreshed.orgSlug,
69
+ };
70
+
71
+ await saveCredentials(accountId, nextCredentials);
72
+ cachedCredentials = nextCredentials;
73
+ forceRefresh = false;
74
+ return nextCredentials;
75
+ }
76
+
77
+ return {
78
+ async getAccessToken(): Promise<string> {
79
+ const credentials = await refreshIfNeeded();
80
+ return credentials.accessToken;
81
+ },
82
+ async onAuthError(): Promise<void> {
83
+ forceRefresh = true;
84
+ },
85
+ };
86
+ }
@@ -0,0 +1,118 @@
1
+ import { Command } from "commander";
2
+
3
+ import { loadConfig, loadCredentials } from "./credentials-store.js";
4
+ import { DEFAULT_BAREKEY_API_URL } from "./constants.js";
5
+ import { loadRuntimeConfig } from "./runtime-config.js";
6
+ import type { CliCredentials } from "./types.js";
7
+
8
+ export type LocalSession = {
9
+ baseUrl: string;
10
+ accountId: string;
11
+ credentials: CliCredentials;
12
+ };
13
+
14
+ export type EnvTargetOptions = {
15
+ project?: string;
16
+ stage?: string;
17
+ org?: string;
18
+ };
19
+
20
+ export function toJsonOutput(enabled: boolean, value: unknown): void {
21
+ if (!enabled) {
22
+ return;
23
+ }
24
+ console.log(JSON.stringify(value, null, 2));
25
+ }
26
+
27
+ export async function resolveBaseUrl(explicit: string | undefined): Promise<string> {
28
+ const explicitUrl = explicit?.trim();
29
+ if (explicitUrl && explicitUrl.length > 0) {
30
+ return explicitUrl.replace(/\/$/, "");
31
+ }
32
+
33
+ const envUrl = process.env.BAREKEY_API_URL?.trim();
34
+ if (envUrl && envUrl.length > 0) {
35
+ return envUrl.replace(/\/$/, "");
36
+ }
37
+
38
+ const config = await loadConfig();
39
+ if (config && config.baseUrl.length > 0) {
40
+ return config.baseUrl.replace(/\/$/, "");
41
+ }
42
+
43
+ return DEFAULT_BAREKEY_API_URL;
44
+ }
45
+
46
+ export async function requireLocalSession(): Promise<LocalSession> {
47
+ const config = await loadConfig();
48
+ if (config === null) {
49
+ throw new Error("Not logged in. Run barekey auth login first.");
50
+ }
51
+
52
+ const credentials = await loadCredentials(config.activeAccountId);
53
+ if (credentials === null) {
54
+ throw new Error("Saved credentials not found. Run barekey auth login again.");
55
+ }
56
+
57
+ return {
58
+ baseUrl: config.baseUrl,
59
+ accountId: config.activeAccountId,
60
+ credentials,
61
+ };
62
+ }
63
+
64
+ export async function resolveTarget(
65
+ options: EnvTargetOptions,
66
+ local: LocalSession,
67
+ ): Promise<{
68
+ projectSlug: string;
69
+ stageSlug: string;
70
+ orgSlug?: string;
71
+ }> {
72
+ const runtime = await loadRuntimeConfig();
73
+
74
+ const projectSlug = options.project?.trim() || runtime?.config.project || "";
75
+ const stageSlug = options.stage?.trim() || runtime?.config.environment || "";
76
+ const orgSlug = options.org?.trim() || runtime?.config.org || local.credentials.orgSlug;
77
+
78
+ if (projectSlug.length === 0 || stageSlug.length === 0) {
79
+ const hint = runtime
80
+ ? `Found ${runtime.path} but project/environment is incomplete.`
81
+ : "No barekey.json found in current directory tree.";
82
+ throw new Error(
83
+ `${hint} Pass --project/--stage, or create barekey.json with {"organization":"...","project":"...","environment":"..."}.`,
84
+ );
85
+ }
86
+
87
+ return {
88
+ projectSlug,
89
+ stageSlug,
90
+ orgSlug: orgSlug.length > 0 ? orgSlug : undefined,
91
+ };
92
+ }
93
+
94
+ export function parseChance(value: string | undefined): number {
95
+ if (value === undefined) {
96
+ throw new Error("--chance is required when using --ab.");
97
+ }
98
+
99
+ const parsed = Number(value);
100
+ if (!Number.isFinite(parsed) || parsed < 0 || parsed > 1) {
101
+ throw new Error("--chance must be a number between 0 and 1.");
102
+ }
103
+ return parsed;
104
+ }
105
+
106
+ export function dotenvEscape(value: string): string {
107
+ if (/^[A-Za-z0-9_./:-]+$/.test(value)) {
108
+ return value;
109
+ }
110
+ return JSON.stringify(value);
111
+ }
112
+
113
+ export function addTargetOptions(command: Command): Command {
114
+ return command
115
+ .option("--project <slug>", "Project slug")
116
+ .option("--stage <slug>", "Stage slug")
117
+ .option("--org <slug>", "Organization slug");
118
+ }
@@ -0,0 +1,247 @@
1
+ import os from "node:os";
2
+ import { setTimeout as wait } from "node:timers/promises";
3
+
4
+ import { intro, outro, spinner } from "@clack/prompts";
5
+ import { Command } from "commander";
6
+ import pc from "picocolors";
7
+ import open from "open";
8
+
9
+ import { createCliAuthProvider } from "../auth-provider.js";
10
+ import {
11
+ clearConfig,
12
+ deleteCredentials,
13
+ saveConfig,
14
+ saveCredentials,
15
+ } from "../credentials-store.js";
16
+ import { getJson, postJson } from "../http.js";
17
+ import { requireLocalSession, resolveBaseUrl, toJsonOutput } from "../command-utils.js";
18
+
19
+ function resolveClientName(): string | undefined {
20
+ const configured =
21
+ process.env.BAREKEY_CLIENT_NAME?.trim() ||
22
+ process.env.COMPUTERNAME?.trim() ||
23
+ process.env.HOSTNAME?.trim() ||
24
+ os.hostname().trim();
25
+ if (configured.length === 0) {
26
+ return undefined;
27
+ }
28
+ return configured.slice(0, 120);
29
+ }
30
+
31
+ function resolveVerificationUri(baseUrl: string, verificationUri: string): string {
32
+ try {
33
+ const resolvedBaseUrl = new URL(baseUrl);
34
+ const resolvedVerificationUrl = new URL(verificationUri);
35
+ const isConvexHost =
36
+ resolvedVerificationUrl.host.endsWith(".convex.site") ||
37
+ resolvedVerificationUrl.host.endsWith(".convex.cloud");
38
+ const usesPublicApiHost = resolvedBaseUrl.host.startsWith("api.");
39
+
40
+ if (isConvexHost && usesPublicApiHost) {
41
+ return new URL(
42
+ `${resolvedVerificationUrl.pathname}${resolvedVerificationUrl.search}`,
43
+ `${resolvedBaseUrl.protocol}//${resolvedBaseUrl.host.replace(/^api\./, "")}`,
44
+ ).toString();
45
+ }
46
+ } catch {
47
+ return verificationUri;
48
+ }
49
+
50
+ return verificationUri;
51
+ }
52
+
53
+ async function runLogin(options: { baseUrl?: string }): Promise<void> {
54
+ const baseUrl = await resolveBaseUrl(options.baseUrl);
55
+ intro("Barekey CLI login");
56
+ const loading = spinner();
57
+ loading.start("Starting device authorization");
58
+ let loadingActive = true;
59
+ let pollSpinner: ReturnType<typeof spinner> | null = null;
60
+ let pollActive = false;
61
+
62
+ try {
63
+ const started = await postJson<{
64
+ deviceCode: string;
65
+ userCode: string;
66
+ verificationUri: string;
67
+ intervalSec: number;
68
+ expiresInSec: number;
69
+ }>({
70
+ baseUrl,
71
+ path: "/v1/cli/device/start",
72
+ payload: {
73
+ clientName: resolveClientName(),
74
+ },
75
+ });
76
+ const verificationUri = resolveVerificationUri(baseUrl, started.verificationUri);
77
+
78
+ loading.stop("Authorization initialized");
79
+ loadingActive = false;
80
+ console.log(`${pc.bold("Open")}: ${verificationUri}`);
81
+ console.log(`${pc.bold("Code")}: ${started.userCode}`);
82
+
83
+ try {
84
+ await open(verificationUri);
85
+ } catch {
86
+ // Browser-open can fail in headless environments.
87
+ }
88
+
89
+ const startedAtMs = Date.now();
90
+ const expiresAtMs = startedAtMs + started.expiresInSec * 1000;
91
+ pollSpinner = spinner();
92
+ pollSpinner.start("Waiting for approval in browser");
93
+ pollActive = true;
94
+
95
+ while (Date.now() < expiresAtMs) {
96
+ const poll = await postJson<
97
+ | {
98
+ status: "pending";
99
+ intervalSec: number;
100
+ }
101
+ | {
102
+ status: "approved";
103
+ accessToken: string;
104
+ refreshToken: string;
105
+ accessTokenExpiresAtMs: number;
106
+ refreshTokenExpiresAtMs: number;
107
+ orgId: string;
108
+ orgSlug: string;
109
+ clerkUserId: string;
110
+ }
111
+ >({
112
+ baseUrl,
113
+ path: "/v1/cli/device/poll",
114
+ payload: {
115
+ deviceCode: started.deviceCode,
116
+ },
117
+ });
118
+
119
+ if (poll.status === "pending") {
120
+ await wait(Math.max(1, poll.intervalSec) * 1000);
121
+ continue;
122
+ }
123
+
124
+ const accountId = `${poll.orgSlug}:${poll.clerkUserId}`;
125
+ await saveCredentials(accountId, {
126
+ accessToken: poll.accessToken,
127
+ refreshToken: poll.refreshToken,
128
+ accessTokenExpiresAtMs: poll.accessTokenExpiresAtMs,
129
+ refreshTokenExpiresAtMs: poll.refreshTokenExpiresAtMs,
130
+ clerkUserId: poll.clerkUserId,
131
+ orgId: poll.orgId,
132
+ orgSlug: poll.orgSlug,
133
+ });
134
+ await saveConfig({
135
+ baseUrl,
136
+ activeAccountId: accountId,
137
+ });
138
+
139
+ pollSpinner.stop("Login approved");
140
+ pollActive = false;
141
+ outro(`Logged in as ${pc.bold(poll.clerkUserId)} in ${pc.bold(poll.orgSlug)}.`);
142
+ return;
143
+ }
144
+
145
+ pollSpinner.stop("Timed out");
146
+ pollActive = false;
147
+ throw new Error("Login timed out before device approval completed.");
148
+ } catch (error: unknown) {
149
+ if (loadingActive) {
150
+ loading.stop("Authorization failed");
151
+ }
152
+ if (pollSpinner && pollActive) {
153
+ pollSpinner.stop("Login failed");
154
+ }
155
+ throw error;
156
+ }
157
+ }
158
+
159
+ async function runLogout(): Promise<void> {
160
+ const local = await requireLocalSession();
161
+ await postJson<{ revoked: boolean }>({
162
+ baseUrl: local.baseUrl,
163
+ path: "/v1/cli/logout",
164
+ payload: {
165
+ refreshToken: local.credentials.refreshToken,
166
+ },
167
+ });
168
+ await deleteCredentials(local.accountId);
169
+ await clearConfig();
170
+ console.log("Logged out.");
171
+ }
172
+
173
+ async function runWhoami(options: { json?: boolean }): Promise<void> {
174
+ const local = await requireLocalSession();
175
+ const authProvider = createCliAuthProvider();
176
+ const accessToken = await authProvider.getAccessToken();
177
+ const session = await getJson<{
178
+ clerkUserId: string;
179
+ orgId: string;
180
+ orgSlug: string;
181
+ source: "clerk" | "cli";
182
+ }>({
183
+ baseUrl: local.baseUrl,
184
+ path: "/v1/cli/session",
185
+ accessToken,
186
+ });
187
+
188
+ if (options.json) {
189
+ toJsonOutput(true, session);
190
+ return;
191
+ }
192
+
193
+ console.log(`${pc.bold("User")}: ${session.clerkUserId}`);
194
+ console.log(`${pc.bold("Org")}: ${session.orgSlug}`);
195
+ console.log(`${pc.bold("Source")}: ${session.source}`);
196
+ }
197
+
198
+ export function registerAuthCommands(program: Command): void {
199
+ const auth = program.command("auth").description("Authentication commands");
200
+
201
+ auth
202
+ .command("login")
203
+ .description("Authenticate this machine using browser device flow")
204
+ .option("--base-url <url>", "Barekey API base URL")
205
+ .action(async (options: { baseUrl?: string }) => {
206
+ await runLogin(options);
207
+ });
208
+
209
+ auth
210
+ .command("logout")
211
+ .description("Revoke local CLI session")
212
+ .action(async () => {
213
+ await runLogout();
214
+ });
215
+
216
+ auth
217
+ .command("whoami")
218
+ .description("Show active CLI auth context")
219
+ .option("--json", "Machine-readable output", false)
220
+ .action(async (options: { json?: boolean }) => {
221
+ await runWhoami(options);
222
+ });
223
+
224
+ // Backward-compatible top-level auth aliases.
225
+ program
226
+ .command("login")
227
+ .description("Alias for barekey auth login")
228
+ .option("--base-url <url>", "Barekey API base URL")
229
+ .action(async (options: { baseUrl?: string }) => {
230
+ await runLogin(options);
231
+ });
232
+
233
+ program
234
+ .command("logout")
235
+ .description("Alias for barekey auth logout")
236
+ .action(async () => {
237
+ await runLogout();
238
+ });
239
+
240
+ program
241
+ .command("whoami")
242
+ .description("Alias for barekey auth whoami")
243
+ .option("--json", "Machine-readable output", false)
244
+ .action(async (options: { json?: boolean }) => {
245
+ await runWhoami(options);
246
+ });
247
+ }