@chainpatrol/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,287 @@
1
+ import {
2
+ getConfig,
3
+ getConfigDir
4
+ } from "./chunk-U73SABXK.js";
5
+
6
+ // src/lib/auth.ts
7
+ import {
8
+ appendFileSync,
9
+ chmodSync,
10
+ closeSync,
11
+ existsSync,
12
+ mkdirSync,
13
+ openSync,
14
+ readFileSync,
15
+ unlinkSync,
16
+ writeFileSync
17
+ } from "fs";
18
+ import { join } from "path";
19
+ import { z } from "zod";
20
+ var CLIENT_ID = "chainpatrol-cli";
21
+ var AUTH_TIMEOUT_MS = 3e4;
22
+ var credentialsSchema = z.object({
23
+ accessToken: z.string().min(1),
24
+ expiresAt: z.string().datetime({ offset: true }),
25
+ email: z.string().email().optional()
26
+ });
27
+ var getSessionResponseSchema = z.object({
28
+ user: z.object({
29
+ email: z.string().email().optional()
30
+ }).optional()
31
+ });
32
+ var deviceCodeResponseSchema = z.object({
33
+ device_code: z.string().min(1),
34
+ user_code: z.string().min(1),
35
+ verification_uri: z.string().url(),
36
+ verification_uri_complete: z.string().url().optional(),
37
+ expires_in: z.number().int().positive(),
38
+ interval: z.number().int().positive()
39
+ });
40
+ var pollSuccessResponseSchema = z.object({
41
+ access_token: z.string().min(1),
42
+ token_type: z.string().min(1),
43
+ expires_in: z.number().int().positive(),
44
+ scope: z.string()
45
+ });
46
+ var pollErrorResponseSchema = z.object({
47
+ error: z.string().optional()
48
+ });
49
+ function credsFile() {
50
+ return join(getConfigDir(), "credentials.json");
51
+ }
52
+ function debugFile() {
53
+ return join(getConfigDir(), "auth-debug.log");
54
+ }
55
+ function sanitizeDebugValue(value) {
56
+ if (Array.isArray(value)) {
57
+ return value.map((item) => sanitizeDebugValue(item));
58
+ }
59
+ if (value && typeof value === "object") {
60
+ const output = {};
61
+ for (const [key, entry] of Object.entries(value)) {
62
+ const isSensitive = /token|secret|password|authorization|key|session/i.test(key);
63
+ output[key] = isSensitive ? "[redacted]" : sanitizeDebugValue(entry);
64
+ }
65
+ return output;
66
+ }
67
+ return value;
68
+ }
69
+ function writeDebugLog(context, payload) {
70
+ try {
71
+ mkdirSync(getConfigDir(), { recursive: true });
72
+ const fileDescriptor = openSync(debugFile(), "a", 384);
73
+ chmodSync(debugFile(), 384);
74
+ try {
75
+ appendFileSync(
76
+ fileDescriptor,
77
+ `${(/* @__PURE__ */ new Date()).toISOString()} ${context}
78
+ ${JSON.stringify(sanitizeDebugValue(payload), null, 2)}
79
+
80
+ `
81
+ );
82
+ } finally {
83
+ closeSync(fileDescriptor);
84
+ }
85
+ chmodSync(debugFile(), 384);
86
+ } catch {
87
+ }
88
+ }
89
+ var AuthNotLoggedInError = class extends Error {
90
+ constructor() {
91
+ super("Not logged in. Run `chainpatrol login` first.");
92
+ }
93
+ };
94
+ var AuthExpiredError = class extends Error {
95
+ constructor() {
96
+ super("Session expired. Run `chainpatrol login` to re-authenticate.");
97
+ }
98
+ };
99
+ var AuthCorruptedError = class extends Error {
100
+ constructor() {
101
+ super("Stored credentials are invalid. Run `chainpatrol login` to re-authenticate.");
102
+ }
103
+ };
104
+ function getCredentials() {
105
+ if (!existsSync(credsFile())) {
106
+ throw new AuthNotLoggedInError();
107
+ }
108
+ try {
109
+ const raw = readFileSync(credsFile(), "utf-8");
110
+ const parsed = JSON.parse(raw);
111
+ const validation = credentialsSchema.safeParse(parsed);
112
+ if (!validation.success) {
113
+ writeDebugLog("credentials-validation-failed", {
114
+ path: credsFile(),
115
+ errors: validation.error.issues,
116
+ parsed
117
+ });
118
+ throw new AuthCorruptedError();
119
+ }
120
+ return validation.data;
121
+ } catch (error) {
122
+ if (error instanceof AuthCorruptedError) {
123
+ throw error;
124
+ }
125
+ writeDebugLog("credentials-read-failed", {
126
+ path: credsFile(),
127
+ message: error instanceof Error ? error.message : String(error)
128
+ });
129
+ throw new AuthCorruptedError();
130
+ }
131
+ }
132
+ function storeCredentials(creds) {
133
+ mkdirSync(getConfigDir(), { recursive: true });
134
+ writeFileSync(credsFile(), JSON.stringify(creds, null, 2), { mode: 384 });
135
+ }
136
+ function clearCredentials() {
137
+ if (existsSync(credsFile())) {
138
+ unlinkSync(credsFile());
139
+ }
140
+ }
141
+ function isLoggedIn() {
142
+ try {
143
+ getValidCredentials();
144
+ return true;
145
+ } catch {
146
+ return false;
147
+ }
148
+ }
149
+ function getValidCredentials() {
150
+ const creds = getCredentials();
151
+ const expiresMs = new Date(creds.expiresAt).getTime();
152
+ if (Number.isNaN(expiresMs) || expiresMs < Date.now()) {
153
+ clearCredentials();
154
+ throw new AuthExpiredError();
155
+ }
156
+ return creds;
157
+ }
158
+ async function fetchUserEmail(accessToken) {
159
+ const config = getConfig();
160
+ try {
161
+ const res = await fetch(`${config.apiUrl}/api/better-auth/get-session`, {
162
+ headers: { Authorization: `Bearer ${accessToken}` },
163
+ signal: AbortSignal.timeout(AUTH_TIMEOUT_MS)
164
+ });
165
+ if (!res.ok) return null;
166
+ const rawData = await res.json();
167
+ const data = getSessionResponseSchema.safeParse(rawData);
168
+ if (!data.success) {
169
+ writeDebugLog("fetch-user-email-response-invalid", {
170
+ status: res.status,
171
+ errors: data.error.issues,
172
+ rawData
173
+ });
174
+ return null;
175
+ }
176
+ return data.data.user?.email ?? null;
177
+ } catch {
178
+ return null;
179
+ }
180
+ }
181
+ async function authFetch(url, body) {
182
+ try {
183
+ return await fetch(url, {
184
+ method: "POST",
185
+ headers: { "Content-Type": "application/json" },
186
+ body: JSON.stringify(body),
187
+ signal: AbortSignal.timeout(AUTH_TIMEOUT_MS)
188
+ });
189
+ } catch (err) {
190
+ if (err instanceof DOMException && err.name === "TimeoutError") {
191
+ throw new Error("Request timed out. Check your network connection and try again.");
192
+ }
193
+ throw new Error("Network error. Check your internet connection and try again.");
194
+ }
195
+ }
196
+ async function requestDeviceCode() {
197
+ const config = getConfig();
198
+ const res = await authFetch(`${config.apiUrl}/api/better-auth/device/code`, {
199
+ client_id: CLIENT_ID
200
+ });
201
+ if (!res.ok) {
202
+ if (res.status >= 500) {
203
+ throw new Error(
204
+ "ChainPatrol API is temporarily unavailable. Please try again later."
205
+ );
206
+ }
207
+ throw new Error(`Authentication failed (${res.status}). Please try again.`);
208
+ }
209
+ const rawData = await res.json();
210
+ const data = deviceCodeResponseSchema.safeParse(rawData);
211
+ if (!data.success) {
212
+ writeDebugLog("request-device-code-response-invalid", {
213
+ status: res.status,
214
+ errors: data.error.issues,
215
+ rawData
216
+ });
217
+ throw new Error("Received an invalid authentication response. Please try again.");
218
+ }
219
+ return data.data;
220
+ }
221
+ async function pollForToken(deviceCode) {
222
+ const config = getConfig();
223
+ const res = await authFetch(`${config.apiUrl}/api/better-auth/device/token`, {
224
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
225
+ device_code: deviceCode,
226
+ client_id: CLIENT_ID
227
+ });
228
+ if (res.ok) {
229
+ const rawData = await res.json();
230
+ const data = pollSuccessResponseSchema.safeParse(rawData);
231
+ if (!data.success) {
232
+ writeDebugLog("poll-for-token-success-response-invalid", {
233
+ status: res.status,
234
+ errors: data.error.issues,
235
+ rawData
236
+ });
237
+ return {
238
+ status: "error",
239
+ message: "Received an invalid authentication response. Please run `chainpatrol login` again."
240
+ };
241
+ }
242
+ return {
243
+ status: "success",
244
+ accessToken: data.data.access_token,
245
+ expiresIn: data.data.expires_in
246
+ };
247
+ }
248
+ const rawError = await res.json();
249
+ const error = pollErrorResponseSchema.safeParse(rawError);
250
+ if (!error.success) {
251
+ writeDebugLog("poll-for-token-error-response-invalid", {
252
+ status: res.status,
253
+ errors: error.error.issues,
254
+ rawError
255
+ });
256
+ return {
257
+ status: "error",
258
+ message: "Received an invalid authentication response. Please run `chainpatrol login` again."
259
+ };
260
+ }
261
+ switch (error.data.error) {
262
+ case "authorization_pending":
263
+ return { status: "pending" };
264
+ case "slow_down":
265
+ return { status: "slow_down", addSeconds: 5 };
266
+ case "expired_token":
267
+ return { status: "expired" };
268
+ case "access_denied":
269
+ return { status: "denied" };
270
+ default:
271
+ return { status: "error", message: error.data.error ?? "Unknown error" };
272
+ }
273
+ }
274
+
275
+ export {
276
+ AuthNotLoggedInError,
277
+ AuthExpiredError,
278
+ AuthCorruptedError,
279
+ getCredentials,
280
+ storeCredentials,
281
+ clearCredentials,
282
+ isLoggedIn,
283
+ getValidCredentials,
284
+ fetchUserEmail,
285
+ requestDeviceCode,
286
+ pollForToken
287
+ };