@donebear/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,46 @@
1
+ //#region src/types.d.ts
2
+ type OAuthProvider = "google" | "github";
3
+ type OutputFormat = "text" | "json" | "csv" | "tsv";
4
+ interface CliContext {
5
+ json: boolean;
6
+ color: boolean;
7
+ debug: boolean;
8
+ tokenOverride: string | null;
9
+ apiUrlOverride: string | null;
10
+ format: OutputFormat;
11
+ copy: boolean;
12
+ total: boolean;
13
+ }
14
+ interface SupabaseConfig {
15
+ url: string;
16
+ publishableKey: string;
17
+ }
18
+ interface StoredSessionUser {
19
+ id: string;
20
+ email: string | null;
21
+ }
22
+ interface StoredAuthSession {
23
+ accessToken: string;
24
+ refreshToken: string | null;
25
+ tokenType: string | null;
26
+ expiresAt: string | null;
27
+ provider: OAuthProvider;
28
+ user: StoredSessionUser | null;
29
+ createdAt: string;
30
+ }
31
+ type AuthSource = "flag" | "environment" | "stored";
32
+ interface ResolvedAuthToken {
33
+ token: string;
34
+ source: AuthSource;
35
+ expiresAt: string | null;
36
+ user: StoredSessionUser | null;
37
+ }
38
+ //#endregion
39
+ //#region src/auth-session.d.ts
40
+ declare function resolveAuthToken(context: CliContext, config: SupabaseConfig | null): Promise<ResolvedAuthToken | null>;
41
+ //#endregion
42
+ //#region src/supabase.d.ts
43
+ declare function resolveSupabaseConfig(): SupabaseConfig;
44
+ //#endregion
45
+ export { type AuthSource, type CliContext, type OAuthProvider, type ResolvedAuthToken, type StoredAuthSession, type SupabaseConfig, resolveAuthToken, resolveSupabaseConfig };
46
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/auth-session.ts","../src/supabase.ts"],"mappings":";KAAY,aAAA;AAAA,KAEA,YAAA;AAAA,UAEK,UAAA;EACf,IAAA;EACA,KAAA;EACA,KAAA;EACA,aAAA;EACA,cAAA;EACA,MAAA,EAAQ,YAAA;EACR,IAAA;EACA,KAAA;AAAA;AAAA,UAGe,cAAA;EACf,GAAA;EACA,cAAA;AAAA;AAAA,UAGe,iBAAA;EACf,EAAA;EACA,KAAA;AAAA;AAAA,UAGe,iBAAA;EACf,WAAA;EACA,YAAA;EACA,SAAA;EACA,SAAA;EACA,QAAA,EAAU,aAAA;EACV,IAAA,EAAM,iBAAA;EACN,SAAA;AAAA;AAAA,KAQU,UAAA;AAAA,UAEK,iBAAA;EACf,KAAA;EACA,MAAA,EAAQ,UAAA;EACR,SAAA;EACA,IAAA,EAAM,iBAAA;AAAA;;;iBCyEc,gBAAA,CACpB,OAAA,EAAS,UAAA,EACT,MAAA,EAAQ,cAAA,UACP,OAAA,CAAQ,iBAAA;;;iBCzEK,qBAAA,CAAA,GAAyB,cAAA"}
package/dist/index.mjs ADDED
@@ -0,0 +1,263 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
4
+ import { createClient } from "@supabase/supabase-js";
5
+ import "dotenv";
6
+
7
+ //#region src/constants.ts
8
+ const CLI_NAME = "donebear";
9
+ const DONEBEAR_TOKEN_ENV = "DONEBEAR_TOKEN";
10
+ const DONEBEAR_CONFIG_DIR_ENV = "DONEBEAR_CONFIG_DIR";
11
+ const DONEBEAR_SUPABASE_URL_ENV = "DONEBEAR_SUPABASE_URL";
12
+ const DONEBEAR_SUPABASE_KEY_ENV = "DONEBEAR_SUPABASE_PUBLISHABLE_KEY";
13
+ const FALLBACK_SUPABASE_URL_ENV_KEYS = ["NEXT_PUBLIC_SUPABASE_URL", "SUPABASE_URL"];
14
+ const FALLBACK_SUPABASE_KEY_ENV_KEYS = ["NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY", "SUPABASE_ANON_KEY"];
15
+ function getDefaultConfigBaseDir() {
16
+ if (process.platform === "win32") return process.env.APPDATA ?? join(homedir(), "AppData", "Roaming");
17
+ return process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
18
+ }
19
+ const configBaseDir = getDefaultConfigBaseDir();
20
+ const CONFIG_DIR = process.env[DONEBEAR_CONFIG_DIR_ENV] ?? join(configBaseDir, CLI_NAME);
21
+ const AUTH_FILE_PATH = join(CONFIG_DIR, "auth.json");
22
+ const CONTEXT_FILE_PATH = join(CONFIG_DIR, "context.json");
23
+
24
+ //#endregion
25
+ //#region src/errors.ts
26
+ const EXIT_CODES = {
27
+ SUCCESS: 0,
28
+ ERROR: 1,
29
+ CANCELLED: 2,
30
+ AUTH_REQUIRED: 4
31
+ };
32
+ var CliError = class extends Error {
33
+ exitCode;
34
+ constructor(message, exitCode = EXIT_CODES.ERROR) {
35
+ super(message);
36
+ this.name = "CliError";
37
+ this.exitCode = exitCode;
38
+ }
39
+ };
40
+
41
+ //#endregion
42
+ //#region src/storage.ts
43
+ function isRecord(value) {
44
+ return typeof value === "object" && value !== null;
45
+ }
46
+ function isOAuthProvider(value) {
47
+ return value === "google" || value === "github";
48
+ }
49
+ function parseStoredAuthSession(value) {
50
+ if (!isRecord(value)) return null;
51
+ if (typeof value.accessToken !== "string") return null;
52
+ if (value.refreshToken !== null && typeof value.refreshToken !== "string") return null;
53
+ if (value.tokenType !== null && typeof value.tokenType !== "string") return null;
54
+ if (value.expiresAt !== null && typeof value.expiresAt !== "string") return null;
55
+ if (!isOAuthProvider(value.provider)) return null;
56
+ const user = value.user;
57
+ if (user !== null && (!isRecord(user) || typeof user.id !== "string" || user.email !== null && typeof user.email !== "string")) return null;
58
+ if (typeof value.createdAt !== "string") return null;
59
+ const parsedUser = user === null ? null : {
60
+ id: user.id,
61
+ email: user.email ?? null
62
+ };
63
+ return {
64
+ accessToken: value.accessToken,
65
+ refreshToken: value.refreshToken,
66
+ tokenType: value.tokenType,
67
+ expiresAt: value.expiresAt,
68
+ provider: value.provider,
69
+ user: parsedUser,
70
+ createdAt: value.createdAt
71
+ };
72
+ }
73
+ function parseAuthFile(payload) {
74
+ const parsed = JSON.parse(payload);
75
+ if (!isRecord(parsed) || parsed.version !== 1) throw new CliError(`Invalid auth cache format in ${AUTH_FILE_PATH}`, EXIT_CODES.ERROR);
76
+ const session = parseStoredAuthSession(parsed.session);
77
+ if (!session) throw new CliError(`Invalid auth session in ${AUTH_FILE_PATH}`, EXIT_CODES.ERROR);
78
+ return {
79
+ version: 1,
80
+ session
81
+ };
82
+ }
83
+ async function readStoredAuthSession() {
84
+ try {
85
+ return parseAuthFile(await readFile(AUTH_FILE_PATH, "utf8")).session;
86
+ } catch (error) {
87
+ if (isRecord(error) && error.code === "ENOENT") return null;
88
+ if (error instanceof CliError) throw error;
89
+ throw new CliError(`Failed to read credentials from ${AUTH_FILE_PATH}`, EXIT_CODES.ERROR);
90
+ }
91
+ }
92
+ async function writeStoredAuthSession(session) {
93
+ const payload = {
94
+ version: 1,
95
+ session
96
+ };
97
+ await mkdir(CONFIG_DIR, {
98
+ recursive: true,
99
+ mode: 448
100
+ });
101
+ await writeFile(AUTH_FILE_PATH, `${JSON.stringify(payload, null, 2)}\n`, {
102
+ encoding: "utf8",
103
+ mode: 384
104
+ });
105
+ }
106
+ async function clearStoredAuthSession() {
107
+ await rm(AUTH_FILE_PATH, { force: true });
108
+ }
109
+
110
+ //#endregion
111
+ //#region src/env.ts
112
+ function readEnvValue(key) {
113
+ const value = process.env[key];
114
+ if (typeof value !== "string") return;
115
+ const trimmed = value.trim();
116
+ return trimmed.length > 0 ? trimmed : void 0;
117
+ }
118
+
119
+ //#endregion
120
+ //#region src/supabase.ts
121
+ function firstDefined(keys) {
122
+ for (const key of keys) {
123
+ const value = readEnvValue(key);
124
+ if (value) return value;
125
+ }
126
+ }
127
+ function resolveSupabaseConfig() {
128
+ const url = readEnvValue(DONEBEAR_SUPABASE_URL_ENV) ?? firstDefined(FALLBACK_SUPABASE_URL_ENV_KEYS);
129
+ const publishableKey = readEnvValue(DONEBEAR_SUPABASE_KEY_ENV) ?? firstDefined(FALLBACK_SUPABASE_KEY_ENV_KEYS);
130
+ if (!(url && publishableKey)) throw new CliError([
131
+ "Supabase configuration is missing.",
132
+ `Set ${DONEBEAR_SUPABASE_URL_ENV} and ${DONEBEAR_SUPABASE_KEY_ENV}.`,
133
+ `Fallback keys are also supported: ${FALLBACK_SUPABASE_URL_ENV_KEYS.join(", ")} and ${FALLBACK_SUPABASE_KEY_ENV_KEYS.join(", ")}.`
134
+ ].join(" "), EXIT_CODES.ERROR);
135
+ return {
136
+ url,
137
+ publishableKey
138
+ };
139
+ }
140
+ function createSupabaseClient(config, options) {
141
+ return createClient(config.url, config.publishableKey, { auth: {
142
+ flowType: "pkce",
143
+ autoRefreshToken: false,
144
+ detectSessionInUrl: false,
145
+ persistSession: options?.persistSession ?? false,
146
+ storage: options?.storage
147
+ } });
148
+ }
149
+ function toStoredAuthSession(session, provider) {
150
+ const expiresAt = typeof session.expires_at === "number" ? (/* @__PURE__ */ new Date(session.expires_at * 1e3)).toISOString() : null;
151
+ return {
152
+ accessToken: session.access_token,
153
+ refreshToken: session.refresh_token ?? null,
154
+ tokenType: session.token_type ?? null,
155
+ expiresAt,
156
+ provider,
157
+ user: session.user ? {
158
+ id: session.user.id,
159
+ email: session.user.email ?? null
160
+ } : null,
161
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
162
+ };
163
+ }
164
+ async function refreshSupabaseSession(config, refreshToken) {
165
+ const { data, error } = await createSupabaseClient(config).auth.refreshSession({ refresh_token: refreshToken });
166
+ if (error) throw new CliError(`Failed to refresh session: ${error.message}`, EXIT_CODES.AUTH_REQUIRED);
167
+ if (!data.session) throw new CliError("No session returned during refresh", EXIT_CODES.AUTH_REQUIRED);
168
+ return data.session;
169
+ }
170
+
171
+ //#endregion
172
+ //#region src/auth-session.ts
173
+ function parseJwtBase64Url(input) {
174
+ const normalized = input.replace(/-/g, "+").replace(/_/g, "/");
175
+ const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "=");
176
+ return Buffer.from(padded, "base64").toString("utf8");
177
+ }
178
+ function parseJwtClaims(token) {
179
+ const payloadPart = token.split(".").at(1);
180
+ if (!payloadPart) return null;
181
+ try {
182
+ const payload = parseJwtBase64Url(payloadPart);
183
+ const parsed = JSON.parse(payload);
184
+ if (typeof parsed !== "object" || parsed === null) return null;
185
+ const claims = parsed;
186
+ return {
187
+ exp: typeof claims.exp === "number" ? claims.exp : void 0,
188
+ email: typeof claims.email === "string" ? claims.email : void 0,
189
+ sub: typeof claims.sub === "string" ? claims.sub : void 0
190
+ };
191
+ } catch {
192
+ return null;
193
+ }
194
+ }
195
+ function expiryFromClaims(claims) {
196
+ if (!claims || typeof claims.exp !== "number") return null;
197
+ return (/* @__PURE__ */ new Date(claims.exp * 1e3)).toISOString();
198
+ }
199
+ function userFromClaims(claims) {
200
+ if (!(claims?.sub || claims?.email)) return null;
201
+ return {
202
+ id: claims.sub ?? "unknown",
203
+ email: claims.email ?? null
204
+ };
205
+ }
206
+ function isExpired(session) {
207
+ if (!session.expiresAt) return false;
208
+ const expiresAtMs = Date.parse(session.expiresAt);
209
+ if (Number.isNaN(expiresAtMs)) return false;
210
+ return expiresAtMs <= Date.now();
211
+ }
212
+ function shouldRefresh(session) {
213
+ if (!session.expiresAt) return false;
214
+ const expiresAtMs = Date.parse(session.expiresAt);
215
+ if (Number.isNaN(expiresAtMs)) return false;
216
+ return expiresAtMs - Date.now() <= 6e4;
217
+ }
218
+ function tokenFromRaw(token, source) {
219
+ const claims = parseJwtClaims(token);
220
+ return {
221
+ token,
222
+ source,
223
+ expiresAt: expiryFromClaims(claims),
224
+ user: userFromClaims(claims)
225
+ };
226
+ }
227
+ async function resolveAuthToken(context, config) {
228
+ if (context.tokenOverride) return tokenFromRaw(context.tokenOverride, "flag");
229
+ const envToken = process.env[DONEBEAR_TOKEN_ENV]?.trim();
230
+ if (envToken) return tokenFromRaw(envToken, "environment");
231
+ const session = await readStoredAuthSession();
232
+ if (!session) return null;
233
+ if (!(shouldRefresh(session) || isExpired(session))) return {
234
+ token: session.accessToken,
235
+ source: "stored",
236
+ expiresAt: session.expiresAt,
237
+ user: session.user
238
+ };
239
+ if (!(session.refreshToken && config)) {
240
+ if (isExpired(session)) {
241
+ await clearStoredAuthSession();
242
+ return null;
243
+ }
244
+ return {
245
+ token: session.accessToken,
246
+ source: "stored",
247
+ expiresAt: session.expiresAt,
248
+ user: session.user
249
+ };
250
+ }
251
+ const updatedSession = toStoredAuthSession(await refreshSupabaseSession(config, session.refreshToken), session.provider);
252
+ await writeStoredAuthSession(updatedSession);
253
+ return {
254
+ token: updatedSession.accessToken,
255
+ source: "stored",
256
+ expiresAt: updatedSession.expiresAt,
257
+ user: updatedSession.user
258
+ };
259
+ }
260
+
261
+ //#endregion
262
+ export { resolveAuthToken, resolveSupabaseConfig };
263
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/constants.ts","../src/errors.ts","../src/storage.ts","../src/env.ts","../src/supabase.ts","../src/auth-session.ts"],"sourcesContent":["import { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\nexport const CLI_NAME = \"donebear\";\n\nexport const DONEBEAR_TOKEN_ENV = \"DONEBEAR_TOKEN\";\nexport const DONEBEAR_DEBUG_ENV = \"DONEBEAR_DEBUG\";\nexport const DONEBEAR_CONFIG_DIR_ENV = \"DONEBEAR_CONFIG_DIR\";\nexport const DONEBEAR_SUPABASE_URL_ENV = \"DONEBEAR_SUPABASE_URL\";\nexport const DONEBEAR_SUPABASE_KEY_ENV = \"DONEBEAR_SUPABASE_PUBLISHABLE_KEY\";\nexport const DONEBEAR_API_URL_ENV = \"DONEBEAR_API_URL\";\n\nexport const FALLBACK_SUPABASE_URL_ENV_KEYS = [\n \"NEXT_PUBLIC_SUPABASE_URL\",\n \"SUPABASE_URL\",\n] as const;\n\nexport const FALLBACK_SUPABASE_KEY_ENV_KEYS = [\n \"NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY\",\n \"SUPABASE_ANON_KEY\",\n] as const;\nexport const FALLBACK_API_URL_ENV_KEYS = [\n \"NEXT_PUBLIC_RESERVE_MANAGE_API\",\n] as const;\nexport const DEFAULT_API_URL = \"http://127.0.0.1:3001\";\n\nexport const DEFAULT_OAUTH_PROVIDER = \"google\";\nexport const DEFAULT_OAUTH_CALLBACK_PORT = 8787;\nexport const DEFAULT_OAUTH_CALLBACK_PATH = \"/auth/callback\";\nexport const DEFAULT_OAUTH_TIMEOUT_SECONDS = 180;\n\nfunction getDefaultConfigBaseDir(): string {\n if (process.platform === \"win32\") {\n return process.env.APPDATA ?? join(homedir(), \"AppData\", \"Roaming\");\n }\n\n return process.env.XDG_CONFIG_HOME ?? join(homedir(), \".config\");\n}\n\nconst configBaseDir = getDefaultConfigBaseDir();\n\nexport const CONFIG_DIR =\n process.env[DONEBEAR_CONFIG_DIR_ENV] ?? join(configBaseDir, CLI_NAME);\n\nexport const AUTH_FILE_PATH = join(CONFIG_DIR, \"auth.json\");\nexport const CONTEXT_FILE_PATH = join(CONFIG_DIR, \"context.json\");\n","export const EXIT_CODES = {\n SUCCESS: 0,\n ERROR: 1,\n CANCELLED: 2,\n AUTH_REQUIRED: 4,\n} as const;\n\nexport type ExitCode = (typeof EXIT_CODES)[keyof typeof EXIT_CODES];\n\nexport class CliError extends Error {\n readonly exitCode: ExitCode;\n\n constructor(message: string, exitCode: ExitCode = EXIT_CODES.ERROR) {\n super(message);\n this.name = \"CliError\";\n this.exitCode = exitCode;\n }\n}\n\nexport function toCliError(error: unknown): CliError {\n if (error instanceof CliError) {\n return error;\n }\n\n if (error instanceof Error) {\n return new CliError(error.message, EXIT_CODES.ERROR);\n }\n\n return new CliError(\"Unknown error\", EXIT_CODES.ERROR);\n}\n","import { mkdir, readFile, rm, writeFile } from \"node:fs/promises\";\nimport { AUTH_FILE_PATH, CONFIG_DIR } from \"./constants.js\";\nimport { CliError, EXIT_CODES } from \"./errors.js\";\nimport type {\n AuthFileData,\n OAuthProvider,\n StoredAuthSession,\n} from \"./types.js\";\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null;\n}\n\nfunction isOAuthProvider(value: unknown): value is OAuthProvider {\n return value === \"google\" || value === \"github\";\n}\n\nfunction parseStoredAuthSession(value: unknown): StoredAuthSession | null {\n if (!isRecord(value)) {\n return null;\n }\n\n if (typeof value.accessToken !== \"string\") {\n return null;\n }\n\n if (value.refreshToken !== null && typeof value.refreshToken !== \"string\") {\n return null;\n }\n\n if (value.tokenType !== null && typeof value.tokenType !== \"string\") {\n return null;\n }\n\n if (value.expiresAt !== null && typeof value.expiresAt !== \"string\") {\n return null;\n }\n\n if (!isOAuthProvider(value.provider)) {\n return null;\n }\n\n const user = value.user;\n if (\n user !== null &&\n (!isRecord(user) ||\n typeof user.id !== \"string\" ||\n (user.email !== null && typeof user.email !== \"string\"))\n ) {\n return null;\n }\n\n if (typeof value.createdAt !== \"string\") {\n return null;\n }\n\n const parsedUser =\n user === null\n ? null\n : {\n id: user.id as string,\n email: (user.email as string | null) ?? null,\n };\n\n return {\n accessToken: value.accessToken,\n refreshToken: value.refreshToken,\n tokenType: value.tokenType,\n expiresAt: value.expiresAt,\n provider: value.provider,\n user: parsedUser,\n createdAt: value.createdAt,\n };\n}\n\nfunction parseAuthFile(payload: string): AuthFileData {\n const parsed = JSON.parse(payload) as unknown;\n\n if (!isRecord(parsed) || parsed.version !== 1) {\n throw new CliError(\n `Invalid auth cache format in ${AUTH_FILE_PATH}`,\n EXIT_CODES.ERROR\n );\n }\n\n const session = parseStoredAuthSession(parsed.session);\n\n if (!session) {\n throw new CliError(\n `Invalid auth session in ${AUTH_FILE_PATH}`,\n EXIT_CODES.ERROR\n );\n }\n\n return {\n version: 1,\n session,\n };\n}\n\nexport async function readStoredAuthSession(): Promise<StoredAuthSession | null> {\n try {\n const raw = await readFile(AUTH_FILE_PATH, \"utf8\");\n const parsed = parseAuthFile(raw);\n return parsed.session;\n } catch (error) {\n if (isRecord(error) && error.code === \"ENOENT\") {\n return null;\n }\n\n if (error instanceof CliError) {\n throw error;\n }\n\n throw new CliError(\n `Failed to read credentials from ${AUTH_FILE_PATH}`,\n EXIT_CODES.ERROR\n );\n }\n}\n\nexport async function writeStoredAuthSession(\n session: StoredAuthSession\n): Promise<void> {\n const payload: AuthFileData = {\n version: 1,\n session,\n };\n\n await mkdir(CONFIG_DIR, {\n recursive: true,\n mode: 0o700,\n });\n\n await writeFile(AUTH_FILE_PATH, `${JSON.stringify(payload, null, 2)}\\n`, {\n encoding: \"utf8\",\n mode: 0o600,\n });\n}\n\nexport async function clearStoredAuthSession(): Promise<void> {\n await rm(AUTH_FILE_PATH, {\n force: true,\n });\n}\n","import { existsSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport { config as loadDotEnvFile } from \"dotenv\";\n\nconst DEFAULT_ENV_FILES = [\n \".env.local\",\n \".env\",\n \"apps/manage-frontend/.env.local\",\n \"apps/manage-api/.env\",\n] as const;\n\nlet loaded = false;\n\nexport function loadCliEnvironmentFiles(): void {\n if (loaded) {\n return;\n }\n\n for (const relativePath of DEFAULT_ENV_FILES) {\n const absolutePath = resolve(process.cwd(), relativePath);\n\n if (!existsSync(absolutePath)) {\n continue;\n }\n\n loadDotEnvFile({\n path: absolutePath,\n override: false,\n quiet: true,\n });\n }\n\n loaded = true;\n}\n\nexport function readEnvValue(key: string): string | undefined {\n const value = process.env[key];\n\n if (typeof value !== \"string\") {\n return undefined;\n }\n\n const trimmed = value.trim();\n return trimmed.length > 0 ? trimmed : undefined;\n}\n","import {\n createClient,\n type Session,\n type SupabaseClient,\n type SupportedStorage,\n type User,\n} from \"@supabase/supabase-js\";\nimport {\n DONEBEAR_SUPABASE_KEY_ENV,\n DONEBEAR_SUPABASE_URL_ENV,\n FALLBACK_SUPABASE_KEY_ENV_KEYS,\n FALLBACK_SUPABASE_URL_ENV_KEYS,\n} from \"./constants.js\";\nimport { readEnvValue } from \"./env.js\";\nimport { CliError, EXIT_CODES } from \"./errors.js\";\nimport type {\n OAuthProvider,\n StoredAuthSession,\n SupabaseConfig,\n} from \"./types.js\";\n\nclass MemoryStorage implements SupportedStorage {\n private readonly values = new Map<string, string>();\n\n getItem(key: string): string | null {\n return this.values.get(key) ?? null;\n }\n\n setItem(key: string, value: string): void {\n this.values.set(key, value);\n }\n\n removeItem(key: string): void {\n this.values.delete(key);\n }\n}\n\nfunction firstDefined(keys: readonly string[]): string | undefined {\n for (const key of keys) {\n const value = readEnvValue(key);\n\n if (value) {\n return value;\n }\n }\n\n return undefined;\n}\n\nexport function resolveSupabaseConfig(): SupabaseConfig {\n const url =\n readEnvValue(DONEBEAR_SUPABASE_URL_ENV) ??\n firstDefined(FALLBACK_SUPABASE_URL_ENV_KEYS);\n\n const publishableKey =\n readEnvValue(DONEBEAR_SUPABASE_KEY_ENV) ??\n firstDefined(FALLBACK_SUPABASE_KEY_ENV_KEYS);\n\n if (!(url && publishableKey)) {\n throw new CliError(\n [\n \"Supabase configuration is missing.\",\n `Set ${DONEBEAR_SUPABASE_URL_ENV} and ${DONEBEAR_SUPABASE_KEY_ENV}.`,\n `Fallback keys are also supported: ${FALLBACK_SUPABASE_URL_ENV_KEYS.join(\n \", \"\n )} and ${FALLBACK_SUPABASE_KEY_ENV_KEYS.join(\", \")}.`,\n ].join(\" \"),\n EXIT_CODES.ERROR\n );\n }\n\n return {\n url,\n publishableKey,\n };\n}\n\nexport function createSupabaseClient(\n config: SupabaseConfig,\n options?: {\n storage?: SupportedStorage;\n persistSession?: boolean;\n }\n): SupabaseClient {\n return createClient(config.url, config.publishableKey, {\n auth: {\n flowType: \"pkce\",\n autoRefreshToken: false,\n detectSessionInUrl: false,\n persistSession: options?.persistSession ?? false,\n storage: options?.storage,\n },\n });\n}\n\nexport function createPkceSupabaseClient(\n config: SupabaseConfig\n): SupabaseClient {\n return createSupabaseClient(config, {\n storage: new MemoryStorage(),\n persistSession: true,\n });\n}\n\nexport function toStoredAuthSession(\n session: Session,\n provider: OAuthProvider\n): StoredAuthSession {\n const expiresAt =\n typeof session.expires_at === \"number\"\n ? new Date(session.expires_at * 1000).toISOString()\n : null;\n\n return {\n accessToken: session.access_token,\n refreshToken: session.refresh_token ?? null,\n tokenType: session.token_type ?? null,\n expiresAt,\n provider,\n user: session.user\n ? {\n id: session.user.id,\n email: session.user.email ?? null,\n }\n : null,\n createdAt: new Date().toISOString(),\n };\n}\n\nexport async function refreshSupabaseSession(\n config: SupabaseConfig,\n refreshToken: string\n): Promise<Session> {\n const client = createSupabaseClient(config);\n const { data, error } = await client.auth.refreshSession({\n refresh_token: refreshToken,\n });\n\n if (error) {\n throw new CliError(\n `Failed to refresh session: ${error.message}`,\n EXIT_CODES.AUTH_REQUIRED\n );\n }\n\n if (!data.session) {\n throw new CliError(\n \"No session returned during refresh\",\n EXIT_CODES.AUTH_REQUIRED\n );\n }\n\n return data.session;\n}\n\nexport async function getSupabaseUser(\n config: SupabaseConfig,\n accessToken: string\n): Promise<User> {\n const client = createSupabaseClient(config);\n const { data, error } = await client.auth.getUser(accessToken);\n\n if (error) {\n throw new CliError(\n `Failed to load user: ${error.message}`,\n EXIT_CODES.ERROR\n );\n }\n\n if (!data.user) {\n throw new CliError(\"No user returned for current token\", EXIT_CODES.ERROR);\n }\n\n return data.user;\n}\n","import { DONEBEAR_TOKEN_ENV } from \"./constants.js\";\nimport {\n clearStoredAuthSession,\n readStoredAuthSession,\n writeStoredAuthSession,\n} from \"./storage.js\";\nimport { refreshSupabaseSession, toStoredAuthSession } from \"./supabase.js\";\nimport type {\n CliContext,\n ResolvedAuthToken,\n StoredAuthSession,\n SupabaseConfig,\n} from \"./types.js\";\n\ninterface JwtClaims {\n exp?: number;\n email?: string;\n sub?: string;\n}\n\nfunction parseJwtBase64Url(input: string): string {\n const normalized = input.replace(/-/g, \"+\").replace(/_/g, \"/\");\n const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, \"=\");\n return Buffer.from(padded, \"base64\").toString(\"utf8\");\n}\n\nexport function parseJwtClaims(token: string): JwtClaims | null {\n const parts = token.split(\".\");\n const payloadPart = parts.at(1);\n\n if (!payloadPart) {\n return null;\n }\n\n try {\n const payload = parseJwtBase64Url(payloadPart);\n const parsed = JSON.parse(payload) as unknown;\n\n if (typeof parsed !== \"object\" || parsed === null) {\n return null;\n }\n\n const claims = parsed as Record<string, unknown>;\n\n return {\n exp: typeof claims.exp === \"number\" ? claims.exp : undefined,\n email: typeof claims.email === \"string\" ? claims.email : undefined,\n sub: typeof claims.sub === \"string\" ? claims.sub : undefined,\n };\n } catch {\n return null;\n }\n}\n\nfunction expiryFromClaims(claims: JwtClaims | null): string | null {\n if (!claims || typeof claims.exp !== \"number\") {\n return null;\n }\n\n return new Date(claims.exp * 1000).toISOString();\n}\n\nfunction userFromClaims(claims: JwtClaims | null): ResolvedAuthToken[\"user\"] {\n if (!(claims?.sub || claims?.email)) {\n return null;\n }\n\n return {\n id: claims.sub ?? \"unknown\",\n email: claims.email ?? null,\n };\n}\n\nfunction nowUnixSeconds(): number {\n return Math.floor(Date.now() / 1000);\n}\n\nfunction isExpired(session: StoredAuthSession): boolean {\n if (!session.expiresAt) {\n return false;\n }\n\n const expiresAtMs = Date.parse(session.expiresAt);\n\n if (Number.isNaN(expiresAtMs)) {\n return false;\n }\n\n return expiresAtMs <= Date.now();\n}\n\nfunction shouldRefresh(session: StoredAuthSession): boolean {\n if (!session.expiresAt) {\n return false;\n }\n\n const expiresAtMs = Date.parse(session.expiresAt);\n\n if (Number.isNaN(expiresAtMs)) {\n return false;\n }\n\n return expiresAtMs - Date.now() <= 60_000;\n}\n\nfunction tokenFromRaw(\n token: string,\n source: ResolvedAuthToken[\"source\"]\n): ResolvedAuthToken {\n const claims = parseJwtClaims(token);\n\n return {\n token,\n source,\n expiresAt: expiryFromClaims(claims),\n user: userFromClaims(claims),\n };\n}\n\nexport async function resolveAuthToken(\n context: CliContext,\n config: SupabaseConfig | null\n): Promise<ResolvedAuthToken | null> {\n if (context.tokenOverride) {\n return tokenFromRaw(context.tokenOverride, \"flag\");\n }\n\n const envToken = process.env[DONEBEAR_TOKEN_ENV]?.trim();\n\n if (envToken) {\n return tokenFromRaw(envToken, \"environment\");\n }\n\n const session = await readStoredAuthSession();\n\n if (!session) {\n return null;\n }\n\n if (!(shouldRefresh(session) || isExpired(session))) {\n return {\n token: session.accessToken,\n source: \"stored\",\n expiresAt: session.expiresAt,\n user: session.user,\n };\n }\n\n if (!(session.refreshToken && config)) {\n if (isExpired(session)) {\n await clearStoredAuthSession();\n return null;\n }\n\n return {\n token: session.accessToken,\n source: \"stored\",\n expiresAt: session.expiresAt,\n user: session.user,\n };\n }\n\n const refreshedSession = await refreshSupabaseSession(\n config,\n session.refreshToken\n );\n const updatedSession = toStoredAuthSession(\n refreshedSession,\n session.provider\n );\n await writeStoredAuthSession(updatedSession);\n\n return {\n token: updatedSession.accessToken,\n source: \"stored\",\n expiresAt: updatedSession.expiresAt,\n user: updatedSession.user,\n };\n}\n\nexport function resolveTokenStatus(token: ResolvedAuthToken): {\n expiresInSeconds: number | null;\n expired: boolean;\n} {\n const claims = parseJwtClaims(token.token);\n\n if (!claims?.exp) {\n return {\n expiresInSeconds: null,\n expired: false,\n };\n }\n\n const expiresInSeconds = claims.exp - nowUnixSeconds();\n\n return {\n expiresInSeconds,\n expired: expiresInSeconds <= 0,\n };\n}\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@donebear/cli",
3
+ "version": "0.1.0",
4
+ "description": "Done Bear CLI with OAuth authentication",
5
+ "type": "module",
6
+ "main": "./dist/index.mjs",
7
+ "types": "./dist/index.d.mts",
8
+ "bin": {
9
+ "donebear": "./dist/cli.mjs"
10
+ },
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.mts",
14
+ "import": "./dist/index.mjs"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "README.md",
20
+ "AGENTS.md"
21
+ ],
22
+ "scripts": {
23
+ "build": "tsdown",
24
+ "dev": "tsdown --watch",
25
+ "start": "node dist/cli.mjs",
26
+ "lint": "biome lint .",
27
+ "check-types": "tsc --noEmit",
28
+ "test": "vitest run"
29
+ },
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "engines": {
34
+ "node": ">=18"
35
+ },
36
+ "dependencies": {
37
+ "@supabase/supabase-js": "^2.93.3",
38
+ "commander": "^14.0.2",
39
+ "dotenv": "^17.2.3"
40
+ },
41
+ "devDependencies": {
42
+ "@biomejs/biome": "^2.3.13",
43
+ "@types/node": "^25.1.0",
44
+ "tsdown": "^0.20.3",
45
+ "typescript": "^5.9.3",
46
+ "vitest": "^4.0.18"
47
+ }
48
+ }