@rblez/authly 0.4.0 → 0.4.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rblez/authly",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Local auth dashboard for Next.js + Supabase",
5
5
  "type": "module",
6
6
  "bin": {
@@ -3,6 +3,7 @@ import path from "node:path";
3
3
  import chalk from "chalk";
4
4
  import ora from "ora";
5
5
  import { detectFramework } from "../lib/framework.js";
6
+ import { scanSupabase } from "../integrations/supabase.js";
6
7
  import { generateEnv } from "../generators/env.js";
7
8
 
8
9
  export async function cmdInit() {
@@ -11,56 +12,50 @@ export async function cmdInit() {
11
12
  // Detect framework
12
13
  const framework = detectFramework();
13
14
  if (!framework) {
14
- console.log(chalk.yellow(" No Next.js project detected. Run authly init from your Next.js project root.\n"));
15
+ console.log(chalk.yellow(" × No Next.js project detected. Run authly init from your Next.js project root.\n"));
15
16
  process.exit(1);
16
17
  }
17
18
  console.log(`${chalk.green("✔")} Detected framework: ${chalk.cyan(framework.name)}`);
18
19
 
19
- // Check for existing config
20
- const hasEnv = fs.existsSync(".env.local");
21
- const hasAuthlyConfig = fs.existsSync("authly.config.json");
20
+ // Scan for existing Supabase credentials
21
+ const projectRoot = path.resolve(process.cwd());
22
+ const scan = scanSupabase(projectRoot);
22
23
 
23
- if (hasEnv) {
24
- console.log(`${chalk.yellow("")} .env.local already exists`);
24
+ if (scan.detected) {
25
+ console.log(`${chalk.green("")} Found existing Supabase credentials`);
25
26
  }
26
- if (hasAuthlyConfig) {
27
- console.log(`${chalk.yellow("•")} authly.config.json already exists`);
28
- }
29
-
30
- // Generate .env.local template
31
- const spinner = ora("Generating .env.local").start();
32
- const envPath = path.join(process.cwd(), ".env.local");
33
27
 
34
- if (!hasEnv) {
28
+ // Generate .env.local if missing
29
+ const envPath = path.join(projectRoot, ".env.local");
30
+ if (!fs.existsSync(envPath)) {
31
+ const spinner = ora("Generating .env.local").start();
35
32
  await generateEnv(envPath);
36
- spinner.succeed("Generated .env.local with authly variables");
33
+ spinner.succeed("Generated .env.local");
37
34
  } else {
38
- spinner.info(".env.local already exists, skipping");
35
+ console.log(`${chalk.yellow("·")} .env.local already exists`);
39
36
  }
40
37
 
41
- // Generate authly config
42
- const configSpinner = ora("Creating authly.config.json").start();
43
- if (!hasAuthlyConfig) {
38
+ // Generate authly.config.json if missing
39
+ const configPath = path.join(projectRoot, "authly.config.json");
40
+ if (!fs.existsSync(configPath)) {
41
+ const configSpinner = ora("Creating authly.config.json").start();
44
42
  const config = {
45
43
  $schema: "https://raw.githubusercontent.com/rblez/authly/main/schema/config.json",
46
- framework,
44
+ framework: framework.name,
47
45
  port: 1284,
48
46
  supabase: {
49
- url: "",
50
- anonKey: "",
51
- serviceRoleKey: "",
47
+ url: scan.url || "",
48
+ anonKey: scan.anonKey ? "set" : "",
49
+ serviceRoleKey: scan.serviceKey ? "set" : "",
50
+ projectRef: scan.projectRef || "",
52
51
  },
53
52
  providers: {},
54
53
  roles: ["admin", "user", "guest"],
55
54
  };
56
- fs.writeFileSync(envPath ? ".env.local" : ".env.local", "", { flag: "a" });
57
- fs.writeFileSync(
58
- "authly.config.json",
59
- JSON.stringify(config, null, 2) + "\n",
60
- );
55
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
61
56
  configSpinner.succeed("Created authly.config.json");
62
57
  } else {
63
- configSpinner.info("authly.config.json already exists");
58
+ console.log(`${chalk.yellow("·")} authly.config.json already exists`);
64
59
  }
65
60
 
66
61
  console.log(chalk.dim("\n Next: run `npx @rblez/authly serve` to start the dashboard\n"));
@@ -18,13 +18,10 @@ import {
18
18
  listRoles,
19
19
  } from "../generators/roles.js";
20
20
  import { listMigrations, getMigration, migrations } from "../generators/migrations.js";
21
- import { detectFramework } from "../lib/framework.js";
22
21
  import { scaffoldAuth, previewGenerated } from "../generators/ui.js";
23
- import { generateEnv } from "../generators/env.js";
24
22
  import { mountMcp } from "../mcp/server.js";
25
- import { scanSupabase } from "../integrations/supabase.js";
26
23
  import { generatePKCE, buildSupabaseAuthorizeUrl, exchangeSupabaseToken, saveSupabaseTokens } from "../lib/supabase-oauth.js";
27
- import { ensureValidAccessToken, autoConfigureFromToken, getProjects, getProjectApiKeys } from "../lib/supabase-api.js";
24
+ import { ensureValidAccessToken, getProjects, getProjectApiKeys } from "../lib/supabase-api.js";
28
25
 
29
26
  const PORT = process.env.PORT || process.env.AUTHLY_PORT || 1284;
30
27
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -41,7 +38,7 @@ export async function cmdServe() {
41
38
 
42
39
  const app = new Hono();
43
40
 
44
- // ── CORS — allow localhost to call the hosted API ──
41
+ // ── CORS — allow localhost to call hosted API ──
45
42
  app.use("/api/*", (c) => {
46
43
  c.header("Access-Control-Allow-Origin", "*");
47
44
  c.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
@@ -70,13 +67,6 @@ export async function cmdServe() {
70
67
  // ── Public API ─────────────────────────────────────
71
68
  app.get("/api/health", (c) => c.json({ status: "ok", uptime: process.uptime() }));
72
69
 
73
- /** GET /api/integrations/supabase/scan — scan local project */
74
- app.get("/api/integrations/supabase/scan", (c) => {
75
- const projectRoot = path.resolve(process.cwd());
76
- const scan = scanSupabase(projectRoot);
77
- return c.json({ success: true, ...scan });
78
- });
79
-
80
70
  /** GET /api/users — list all users from Supabase */
81
71
  app.get("/api/users", async (c) => {
82
72
  const { client, errors } = getSupabaseClient();
@@ -101,7 +91,6 @@ export async function cmdServe() {
101
91
  const result = buildAuthorizeUrl({ provider, redirectUri, state: query.state, scope: query.scope });
102
92
  return c.redirect(result.url);
103
93
  } catch (e) {
104
- // Provider not found — redirect to authorize page
105
94
  return c.redirect("/authorize");
106
95
  }
107
96
  });
@@ -115,6 +104,33 @@ export async function cmdServe() {
115
104
  return c.json({ providers: listProviderStatus() });
116
105
  });
117
106
 
107
+ /** POST /api/auth/:provider/authorize — get OAuth URL as JSON */
108
+ app.post("/api/auth/:provider/authorize", async (c) => {
109
+ const { provider } = c.req.param();
110
+ const body = await c.req.json();
111
+ const result = buildAuthorizeUrl({
112
+ provider,
113
+ redirectUri: body.redirectUri,
114
+ state: body.state,
115
+ scope: body.scope,
116
+ });
117
+ if (result.error) return c.json({ error: result.error }, 400);
118
+ return c.json(result);
119
+ });
120
+
121
+ /** POST /api/auth/:provider/callback — exchange code for session */
122
+ app.post("/api/auth/:provider/callback", async (c) => {
123
+ const { provider } = c.req.param();
124
+ const body = await c.req.json();
125
+ const { user, token, error } = await handleOAuthCallback({
126
+ provider,
127
+ code: body.code,
128
+ redirectUri: body.redirectUri,
129
+ });
130
+ if (error) return c.json({ success: false, error }, 400);
131
+ return c.json({ success: true, user, token });
132
+ });
133
+
118
134
  // ── Supabase Platform OAuth with PKCE ───────────────
119
135
 
120
136
  /** GET /api/auth/supabase/authorize — start Supabase OAuth flow */
@@ -122,11 +138,7 @@ export async function cmdServe() {
122
138
  const query = c.req.query();
123
139
  const { verifier, challenge } = generatePKCE();
124
140
  const state = randomBytes(16).toString("hex");
125
-
126
- // Store verifier for later exchange
127
141
  pkceState.set(state, { verifier, challenge });
128
-
129
- // Auto-expire state after 10 min
130
142
  setTimeout(() => pkceState.delete(state), 10 * 60 * 1000);
131
143
 
132
144
  const result = buildSupabaseAuthorizeUrl({
@@ -134,65 +146,85 @@ export async function cmdServe() {
134
146
  codeChallenge: challenge,
135
147
  organizationSlug: query.organization,
136
148
  });
137
-
138
149
  return c.redirect(result.url);
139
150
  });
140
151
 
141
- /** GET /api/auth/supabase/callback — OAuth callback with PKCE */
152
+ /** GET /api/auth/supabase/callback — OAuth callback with PKCE
153
+ * Saves tokens to DB. Shows project + API keys directly to user
154
+ * for manual copy-paste into .env.local. */
142
155
  app.get("/api/auth/supabase/callback", async (c) => {
143
156
  const query = c.req.query();
144
157
  const code = query.code;
145
158
  const state = query.state;
146
159
 
147
- // Validate state and get stored verifier
148
160
  const stored = pkceState.get(state);
149
161
  if (!stored) {
150
- return c.html(`<h1>Invalid or expired state token</h1>
151
- <p>Return to <a href="/">dashboard</a></p>`, 400);
162
+ return c.html(`<h1>Invalid or expired state token</h1><p><a href="/">dashboard</a></p>`, 400);
152
163
  }
153
-
154
164
  pkceState.delete(state);
155
-
156
165
  if (!code) {
157
- return c.html(`<h1>Authorization failed</h1>
158
- <p>Return to <a href="/">dashboard</a></p>`, 400);
166
+ return c.html(`<h1>Authorization failed</h1><p><a href="/">dashboard</a></p>`, 400);
159
167
  }
160
168
 
161
169
  try {
162
- // Exchange code for tokens
163
- const tokens = await exchangeSupabaseToken({
164
- code,
165
- verifier: stored.verifier,
166
- });
167
-
168
- // Save tokens to DB
170
+ const tokens = await exchangeSupabaseToken({ code, verifier: stored.verifier });
169
171
  await saveSupabaseTokens({
170
172
  accessToken: tokens.access_token,
171
173
  refreshToken: tokens.refresh_token,
172
174
  expiresIn: tokens.expires_in,
173
175
  });
174
176
 
175
- // Auto-configure: get project, extract keys, fill .env.local
176
- const config = await autoConfigureFromToken(tokens.access_token);
177
-
178
- if (config) {
179
- return c.html(`<div style="font-family:sans-serif;max-width:500px;margin:60px auto;text-align:center">
180
- <h1 style="color:#22c55e">Connected to Supabase</h1>
181
- <p>Project: <strong>${config.projectName}</strong></p>
182
- <p><code style="background:#eee;padding:2px 8px;border-radius:4px">${config.projectRef}</code></p>
183
- <p style="color:#666">.env.local and authly.config.json have been updated.</p>
184
- <a href="/" style="display:inline-block;margin-top:16px;padding:8px 24px;background:#111;color:#fff;text-decoration:none;border-radius:6px">Go to Dashboard</a>
185
- </div>`);
177
+ // Get project info for success page
178
+ const projects = await getProjects(tokens.access_token);
179
+ const project = projects?.[0];
180
+
181
+ if (project) {
182
+ const keys = await getProjectApiKeys(tokens.access_token, project.id);
183
+ const anon = keys.find(k => k.type === "anon")?.api_key || "";
184
+ const svc = keys.find(k => k.type === "service_role")?.api_key || "";
185
+
186
+ // Save project ref in tokens table
187
+ const { client } = getSupabaseClient();
188
+ if (client) {
189
+ await client
190
+ .from("authly_supabase_tokens")
191
+ .update({ project_ref: project.id, project_name: project.name })
192
+ .eq("user_id", "00000000-0000-0000-0000-000000000000");
193
+ }
194
+
195
+ return c.html(`<!DOCTYPE html><html><head><meta charset="utf-8"><title>Authly — Connected</title>
196
+ <style>
197
+ body{font-family:sans-serif;background:#0a0a0a;color:#fff;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0}
198
+ .card{background:#111;border:1px solid #222;border-radius:12px;padding:40px 32px;max-width:540px;width:100%;text-align:center}
199
+ h1{color:#22c55e;margin:0 0 12px;font-size:1.5rem}
200
+ p{color:#888;font-size:.9rem;margin:0 0 8px}
201
+ .code{background:#0a0a0a;border:1px solid #333;border-radius:8px;padding:16px;text-align:left;font-size:.8rem;font-family:monospace;overflow-x:auto;color:#ccc;margin:12px 0;line-height:1.8;word-break:break-all}
202
+ .key-val{color:#22c55e}
203
+ .key-label{color:#666}
204
+ a{display:inline-block;margin-top:16px;padding:10px 24px;background:#444;color:#fff;text-decoration:none;border-radius:6px;font-size:.9rem}
205
+ a:hover{background:#555}
206
+ </style></head><body><div class="card">
207
+ <h1>Connected to Supabase</h1>
208
+ <p>Project: <strong>${project.name}</strong></p>
209
+ <p><code style="color:#58a6ff">${project.id}</code></p>
210
+ <p style="margin-top:20px;text-align:left;color:#aaa;">Add these to your project's <strong>.env.local</strong>:</p>
211
+ <div class="code"><span class="key-label">SUPABASE_URL=</span>"<span class="key-val">https://${project.id}.supabase.co</span>"<br>
212
+ <span class="key-label">SUPABASE_ANON_KEY=</span>"<span class="key-val">${anon}</span>"<br>
213
+ <span class="key-label">SUPABASE_SERVICE_ROLE_KEY=</span>"<span class="key-val">${svc}</span>"<br>
214
+ <span class="key-label">AUTHLY_SECRET=</span>"<span class="key-val">generate-with-openssl-rand-hex-32</span>"</div>
215
+ <a href="/">Back to Dashboard</a>
216
+ </div></body></html>`);
186
217
  }
187
218
 
188
- return c.html(`<div style="font-family:sans-serif;max-width:500px;margin:60px auto;text-align:center">
189
- <h1 style="color:#22c55e">Authenticated</h1>
190
- <p>Supabase tokens saved successfully.</p>
191
- <p>Return to <a href="/">dashboard</a>.</p>
192
- </div>`);
219
+ return c.html(`<!DOCTYPE html><html><head><meta charset="utf-8"><title>Authly</title>
220
+ <style>body{font-family:sans-serif;background:#0a0a0a;color:#fff;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0}
221
+ .card{background:#111;border:1px solid #222;border-radius:12px;padding:40px 32px;max-width:540px;width:100%;text-align:center}
222
+ h1{color:#22c55e;margin:0 0 12px}
223
+ a{color:#58a6ff}</style></head><body><div class="card">
224
+ <h1>Authenticated</h1><p>Supabase tokens saved, no projects found.</p>
225
+ <a href="/">Back to Dashboard</a></div></body></html>`);
193
226
  } catch (e) {
194
- return c.html(`<h1>Token exchange failed</h1><p>${e.message}</p>
195
- <p>Return to <a href="/">dashboard</a></p>`, 400);
227
+ return c.html(`<h1>Token exchange failed</h1><p>${e.message}</p><p><a href="/">dashboard</a></p>`, 400);
196
228
  }
197
229
  });
198
230
 
@@ -200,7 +232,7 @@ export async function cmdServe() {
200
232
  app.get("/api/supabase/projects", async (c) => {
201
233
  try {
202
234
  const token = await ensureValidAccessToken();
203
- if (!token) return c.json({ error: "Not connected to Supabase — visit /api/auth/supabase/authorize" }, 401);
235
+ if (!token) return c.json({ error: "Not connected to Supabase" }, 401);
204
236
  const projects = await getProjects(token);
205
237
  return c.json({ success: true, projects });
206
238
  } catch (e) {
@@ -215,7 +247,7 @@ export async function cmdServe() {
215
247
  const token = await ensureValidAccessToken();
216
248
  if (!token) return c.json({ error: "Not connected to Supabase" }, 401);
217
249
  const keys = await getProjectApiKeys(token, ref);
218
- return c.json({ success: true, keys });
250
+ return c.json({ success: true, keys: keys.filter(k => ["anon", "service_role"].includes(k.type)) });
219
251
  } catch (e) {
220
252
  return c.json({ error: e.message }, 500);
221
253
  }
@@ -232,57 +264,7 @@ export async function cmdServe() {
232
264
  }
233
265
  });
234
266
 
235
- /** POST /api/auth/:provider/authorize — get OAuth URL as JSON */
236
- app.post("/api/auth/:provider/authorize", async (c) => {
237
- const { provider } = c.req.param();
238
- const body = await c.req.json();
239
- const result = buildAuthorizeUrl({
240
- provider,
241
- redirectUri: body.redirectUri,
242
- state: body.state,
243
- scope: body.scope,
244
- });
245
- if (result.error) return c.json({ error: result.error }, 400);
246
- return c.json(result);
247
- });
248
-
249
- /** POST /api/auth/ :provider/callback — exchange code for session */
250
- app.post("/api/auth/:provider/callback", async (c) => {
251
- const { provider } = c.req.param();
252
- const body = await c.req.json();
253
- const { session, error } = await exchangeOAuthCode({
254
- provider,
255
- code: body.code,
256
- redirectUri: body.redirectUri,
257
- });
258
- if (error) return c.json({ success: false, error }, 400);
259
-
260
- // Create an internal Authly session token too
261
- const token = await createSessionToken({
262
- sub: session.user?.id ?? "unknown",
263
- role: session.user?.user_metadata?.role ?? "user",
264
- });
265
- return c.json({ success: true, session, token });
266
- });
267
-
268
- /** POST /api/auth/session — create a session token from credentials */
269
- app.post("/api/auth/session", async (c) => {
270
- const body = await c.req.json();
271
- if (!body.sub) return c.json({ error: "Missing 'sub' field" }, 400);
272
- const token = await createSessionToken({
273
- sub: body.sub,
274
- role: body.role ?? "user",
275
- });
276
- return c.json({ success: true, token });
277
- });
278
-
279
- /** GET /api/auth/me — verify session token from Authorization header */
280
- app.get("/api/auth/me", authMiddleware(), (c) =>
281
- c.json({ success: true, session: c.get("session") }),
282
- );
283
-
284
267
  // ── Password Auth ──────────────────────────────────
285
- /** POST /api/auth/register — sign up with email + password */
286
268
  app.post("/api/auth/register", async (c) => {
287
269
  const body = await c.req.json();
288
270
  const { user, token, error } = await signUp({
@@ -294,7 +276,6 @@ export async function cmdServe() {
294
276
  return c.json({ success: true, user, token });
295
277
  });
296
278
 
297
- /** POST /api/auth/login — sign in with email + password */
298
279
  app.post("/api/auth/login", async (c) => {
299
280
  const body = await c.req.json();
300
281
  const { user, token, error } = await signIn({
@@ -305,13 +286,25 @@ export async function cmdServe() {
305
286
  return c.json({ success: true, user, token });
306
287
  });
307
288
 
308
- /** GET /api/auth/providers — list available and enabled providers */
309
289
  app.get("/api/auth/providers", (c) => {
310
290
  const providers = getProviders();
311
291
  return c.json({ providers });
312
292
  });
313
293
 
314
- /** POST /api/auth/magic-link/send send magic link email */
294
+ app.post("/api/auth/session", async (c) => {
295
+ const body = await c.req.json();
296
+ if (!body.sub) return c.json({ error: "Missing 'sub' field" }, 400);
297
+ const token = await createSessionToken({
298
+ sub: body.sub,
299
+ role: body.role ?? "user",
300
+ });
301
+ return c.json({ success: true, token });
302
+ });
303
+
304
+ app.get("/api/auth/me", authMiddleware(), (c) =>
305
+ c.json({ success: true, session: c.get("session") }),
306
+ );
307
+
315
308
  app.post("/api/auth/magic-link/send", async (c) => {
316
309
  const body = await c.req.json();
317
310
  const result = await sendMagicLink({ email: body.email, callbackUrl: body.callbackUrl || "/" });
@@ -319,7 +312,6 @@ export async function cmdServe() {
319
312
  return c.json({ success: true, sent: true });
320
313
  });
321
314
 
322
- /** POST /api/auth/magic-link/verify — verify magic link token */
323
315
  app.post("/api/auth/magic-link/verify", async (c) => {
324
316
  const body = await c.req.json();
325
317
  const { user, token, error } = await verifyMagicLink({ token: body.token });
@@ -327,26 +319,13 @@ export async function cmdServe() {
327
319
  return c.json({ success: true, user, token });
328
320
  });
329
321
 
330
- /** GET /api/config non-sensitive project config */
331
- app.get("/api/config", async (c) => {
332
- const fw = detectFramework();
333
- const { roles = [], error } = await listRoles();
334
- return c.json({
335
- framework: fw ?? null,
336
- providers: listProviderStatus(),
337
- roles,
338
- });
339
- });
340
-
341
- // ── Role management (protected) ────────────────────
342
- /** GET /api/roles — list all roles */
322
+ // ── Role management ────────────────────────────────
343
323
  app.get("/api/roles", async (c) => {
344
324
  const { roles, error } = await listRoles();
345
325
  if (error) return c.json({ success: false, error }, 500);
346
326
  return c.json({ success: true, roles });
347
327
  });
348
328
 
349
- /** POST /api/roles — create a new role */
350
329
  app.post("/api/roles", async (c) => {
351
330
  const { name, description } = await c.req.json();
352
331
  if (!name) return c.json({ error: "'name' is required" }, 400);
@@ -355,7 +334,6 @@ export async function cmdServe() {
355
334
  return c.json(result);
356
335
  });
357
336
 
358
- /** POST /api/roles/:roleId/users/:userId/assign — assign role to user */
359
337
  app.post("/api/roles/:roleName/users/:userId/assign", async (c) => {
360
338
  const { roleName, userId } = c.req.param();
361
339
  const result = await assignRoleToUser(userId, roleName);
@@ -363,7 +341,6 @@ export async function cmdServe() {
363
341
  return c.json(result);
364
342
  });
365
343
 
366
- /** DELETE /api/roles/:roleId/users/:userId/revoke — revoke role from user */
367
344
  app.delete("/api/roles/:roleName/users/:userId/revoke", async (c) => {
368
345
  const { roleName, userId } = c.req.param();
369
346
  const result = await revokeRoleFromUser(userId, roleName);
@@ -371,7 +348,6 @@ export async function cmdServe() {
371
348
  return c.json(result);
372
349
  });
373
350
 
374
- /** GET /api/users/:userId/roles — get roles for a user */
375
351
  app.get("/api/users/:userId/roles", async (c) => {
376
352
  const { userId } = c.req.param();
377
353
  const { roles, error } = await getUserRoles(userId);
@@ -380,7 +356,6 @@ export async function cmdServe() {
380
356
  });
381
357
 
382
358
  // ── API Keys ───────────────────────────────────────
383
- /** POST /api/keys — generate a new API key */
384
359
  app.post("/api/keys", async (c) => {
385
360
  const { client, errors } = getSupabaseClient();
386
361
  if (!client) return c.json({ success: false, errors }, 503);
@@ -400,17 +375,14 @@ export async function cmdServe() {
400
375
  });
401
376
  if (error) return c.json({ success: false, error: error.message }, 400);
402
377
 
403
- // Return raw key ONCE — it cannot be retrieved later
404
378
  return c.json({ success: true, key: rawKey, hashesTo: keyHash });
405
379
  });
406
380
 
407
381
  // ── Migrations ─────────────────────────────────────
408
- /** GET /api/migrations — list available migrations */
409
382
  app.get("/api/migrations", (c) =>
410
383
  c.json({ success: true, migrations: listMigrations() }),
411
384
  );
412
385
 
413
- /** GET /api/migrations/:name/sql — get SQL for a migration */
414
386
  app.get("/api/migrations/:name/sql", (c) => {
415
387
  const { name } = c.req.param();
416
388
  const sql = getMigration(name);
@@ -418,7 +390,6 @@ export async function cmdServe() {
418
390
  return c.json({ success: true, name, sql });
419
391
  });
420
392
 
421
- /** POST /api/migrations/:name/run — execute a migration against Supabase */
422
393
  app.post("/api/migrations/:name/run", async (c) => {
423
394
  const { client, errors } = getSupabaseClient();
424
395
  if (!client) return c.json({ success: false, errors }, 503);
@@ -432,8 +403,7 @@ export async function cmdServe() {
432
403
  return c.json({ success: true, migration: name });
433
404
  });
434
405
 
435
- // ── Scaffold ───────────────────────────────────────
436
- /** POST /api/scaffold/preview — preview generated code without writing */
406
+ // ── Scaffold preview only ─────────────────────────
437
407
  app.post("/api/scaffold/preview", async (c) => {
438
408
  const { type } = await c.req.json();
439
409
  if (!["login", "signup", "middleware", "route-login", "route-signup"].includes(type)) {
@@ -442,78 +412,6 @@ export async function cmdServe() {
442
412
  return c.json({ success: true, type, code: previewGenerated(type) });
443
413
  });
444
414
 
445
- /** POST /api/scaffold/generate — write auth files to a project */
446
- app.post("/api/scaffold/generate", async (c) => {
447
- const { targetDir } = await c.req.json();
448
- if (!targetDir) return c.json({ error: "'targetDir' is required" }, 400);
449
- try {
450
- const { files } = await scaffoldAuth(targetDir, { apiRoutes: true });
451
- c.json({ success: true, files });
452
- } catch (e) {
453
- c.json({ success: false, error: e.message }, 500);
454
- }
455
- });
456
-
457
- // ── Init ──────────────────────────────────────────
458
- /** GET /api/init/scan — autodetect Supabase config in current project */
459
- app.get("/api/init/scan", (c) => {
460
- const projectRoot = path.resolve(process.cwd());
461
- const scan = scanSupabase(projectRoot);
462
- return c.json(scan);
463
- });
464
-
465
- /** POST /api/init/connect — connect with autodetected or manual config */
466
- app.post("/api/init/connect", async (c) => {
467
- const body = await c.req.json().catch(() => ({}));
468
-
469
- // Auto-detect first
470
- const projectRoot = path.resolve(process.cwd());
471
- const scan = scanSupabase(projectRoot);
472
-
473
- // Merge scan results with any manually provided values
474
- const url = scan.url || body.supabaseUrl || process.env.SUPABASE_URL || "";
475
- const anonKey = scan.anonKey || body.supabaseAnonKey || process.env.SUPABASE_ANON_KEY || "";
476
- const serviceKey = scan.serviceKey || body.supabaseServiceKey || process.env.SUPABASE_SERVICE_ROLE_KEY || "";
477
-
478
- // Set env vars from detected config
479
- if (url) process.env.SUPABASE_URL = url;
480
- if (anonKey) process.env.SUPABASE_ANON_KEY = anonKey;
481
- if (serviceKey) process.env.SUPABASE_SERVICE_ROLE_KEY = serviceKey;
482
-
483
- const fw = scan.framework || detectFramework();
484
- if (!fw && !body.supabaseUrl) return c.json({ success: false, error: "No Next.js project detected" }, 400);
485
-
486
- // Generate .env.local if needed
487
- if (!fs.existsSync(".env.local")) {
488
- await generateEnv(".env.local");
489
- }
490
-
491
- // Write authly.config.json
492
- const config = {
493
- framework: fw || "unknown",
494
- supabase: {
495
- url,
496
- projectRef: scan.projectRef || "",
497
- anonKey: anonKey ? "set" : "",
498
- serviceKey: serviceKey ? "set" : "",
499
- autoDetected: scan.detected,
500
- sources: scan.sources,
501
- },
502
- };
503
- fs.writeFileSync("authly.config.json", JSON.stringify(config, null, 2) + "\n");
504
-
505
- return c.json({
506
- success: true,
507
- framework: fw,
508
- supabase: {
509
- url,
510
- detected: scan.detected,
511
- canConnect: scan.canConnect,
512
- sources: scan.sources,
513
- },
514
- });
515
- });
516
-
517
415
  // ── MCP (beta) ─────────────────────────────────────
518
416
  mountMcp(app);
519
417
 
@@ -521,13 +419,11 @@ export async function cmdServe() {
521
419
  app.post("/api/audit", async (c) => {
522
420
  const issues = [];
523
421
 
524
- // Env vars
525
422
  for (const key of ["SUPABASE_URL", "SUPABASE_ANON_KEY", "AUTHLY_SECRET"]) {
526
423
  if (!process.env[key]) issues.push({ check: key, status: "fail", detail: "not set" });
527
424
  else issues.push({ check: key, status: "ok" });
528
425
  }
529
426
 
530
- // Supabase connection
531
427
  const { client } = getSupabaseClient();
532
428
  if (client) {
533
429
  const { error } = await client
@@ -553,7 +449,7 @@ export async function cmdServe() {
553
449
  }
554
450
  return c.json({
555
451
  name: "authly",
556
- version: "0.1.0",
452
+ version: "0.4.0",
557
453
  docs: "/api/health",
558
454
  });
559
455
  });