@rblez/authly 0.3.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/bin/authly.js CHANGED
@@ -11,7 +11,7 @@ const COMMANDS = {
11
11
  init: { description: "Initialize authly in your project", handler: cmdInit },
12
12
  ext: { description: "Manage extensions (add, remove)", handler: cmdExt },
13
13
  audit: { description: "Check auth configuration for issues", handler: cmdAudit },
14
- version: { description: "Show version", handler: () => console.log("0.3.0") },
14
+ version: { description: "Show version", handler: () => console.log("0.4.0") },
15
15
  };
16
16
 
17
17
  async function main() {
@@ -7,11 +7,14 @@ document.addEventListener("DOMContentLoaded", () => {
7
7
  const statusText = document.getElementById("statusText");
8
8
  const statusDot = document.querySelector(".header__dot");
9
9
 
10
+ // All API calls go to the hosted authly instance
11
+ const API_URL = "https://authly.rblez.com/api";
12
+
10
13
  checkHealth();
11
14
 
12
15
  // ── Helpers ─────────────────────────────────────────
13
16
  async function api(endpoint, opts = {}) {
14
- const res = await fetch(`/api${endpoint}`, {
17
+ const res = await fetch(`${API_URL}${endpoint}`, {
15
18
  headers: { "Content-Type": "application/json" },
16
19
  ...opts,
17
20
  });
@@ -40,7 +43,7 @@ document.addEventListener("DOMContentLoaded", () => {
40
43
  // ── Health ──────────────────────────────────────────
41
44
  async function checkHealth() {
42
45
  try {
43
- const res = await fetch("/api/health");
46
+ const res = await fetch(`${API_URL}/health`);
44
47
  if (res.ok) {
45
48
  statusText.textContent = "Connected";
46
49
  statusDot.style.background = "#22c55e";
@@ -54,7 +54,7 @@
54
54
 
55
55
  <!-- Platform: Connect Supabase (OAuth to Supabase API) -->
56
56
  <div class="section-label">Platform connection</div>
57
- <a href="/api/auth/supabase/authorize" id="supabasePlatformBtn" class="platform-connect">
57
+ <a href="https://authly.rblez.com/api/auth/supabase/authorize" id="supabasePlatformBtn" class="platform-connect">
58
58
  <img src="https://cdn.simpleicons.org/supabase/fff" width="20" height="20" alt="Supabase" />
59
59
  <span class="label">Connect Supabase</span>
60
60
  <span class="status" id="sbStatus">Not connected</span>
@@ -68,10 +68,12 @@
68
68
  </div>
69
69
 
70
70
  <script>
71
+ const API_URL = "https://authly.rblez.com/api";
72
+
71
73
  async function loadProviders() {
72
74
  const container = document.getElementById("providerList");
73
75
  try {
74
- const res = await fetch("/api/providers");
76
+ const res = await fetch(`${API_URL}/providers`);
75
77
  const data = await res.json();
76
78
  if (!data.providers) { container.innerHTML = "<p style='color:#555'>&mdash;</p>"; return; }
77
79
 
@@ -86,11 +88,10 @@
86
88
  </div>`;
87
89
  }).join("");
88
90
 
89
- // Attach click handlers
90
91
  container.querySelectorAll(".provider-btn[data-enabled='true']").forEach(el => {
91
92
  el.addEventListener("click", () => {
92
93
  const provider = el.dataset.provider;
93
- window.location.href = `/api/auth/${provider}/authorize`;
94
+ window.location.href = `${API_URL}/auth/${provider}/authorize`;
94
95
  });
95
96
  });
96
97
  } catch {
@@ -98,14 +99,12 @@
98
99
  }
99
100
  }
100
101
 
101
- // Check if Supabase is already connected
102
102
  async function checkSupabaseConnected() {
103
103
  try {
104
- const res = await fetch("/api/health");
104
+ const res = await fetch(`${API_URL}/health`);
105
105
  if (res.ok) {
106
106
  document.getElementById("sbStatus").textContent = "Connected";
107
- const btn = document.getElementById("supabasePlatformBtn");
108
- btn.classList.add("connected");
107
+ document.getElementById("supabasePlatformBtn").classList.add("connected");
109
108
  }
110
109
  } catch {}
111
110
  }
@@ -187,7 +187,7 @@
187
187
  <div id="integrationDetail" class="hidden" style="margin-top:12px"></div>
188
188
  <div style="margin-top:16px;display:flex;gap:8px;flex-wrap:wrap">
189
189
  <button class="btn btn--primary btn--sm" id="reconnectBtn"><i class="ri-refresh-line"></i> Re-scan &amp; reconnect</button>
190
- <a href="/api/auth/supabase/authorize" class="btn btn--sm" style="background:#0d1b3e;border:1px solid #1d355e;color:#58a6ff;text-decoration:none" id="connectSbPlatformBtn"><i class="ri-plug-line"></i> Connect via OAuth</a>
190
+ <a href="https://authly.rblez.com/api/auth/supabase/authorize" target="_blank" class="btn btn--sm" style="background:#0d1b3e;border:1px solid #1d355e;color:#58a6ff;text-decoration:none" id="connectSbPlatformBtn"><i class="ri-plug-line"></i> Connect via OAuth</a>
191
191
  </div>
192
192
  </div>
193
193
  </section>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rblez/authly",
3
- "version": "0.3.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,6 +38,17 @@ export async function cmdServe() {
41
38
 
42
39
  const app = new Hono();
43
40
 
41
+ // ── CORS — allow localhost to call hosted API ──
42
+ app.use("/api/*", (c) => {
43
+ c.header("Access-Control-Allow-Origin", "*");
44
+ c.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
45
+ c.header("Access-Control-Allow-Headers", "Content-Type, Authorization");
46
+ if (c.req.method === "OPTIONS") {
47
+ return new Response(null, { status: 204 });
48
+ }
49
+ return c.next();
50
+ });
51
+
44
52
  // ── Static file serving ────────────────────────────
45
53
  const dashboardPath = path.join(__dirname, "../../dist/dashboard");
46
54
  const hasDashboard = fs.existsSync(dashboardPath);
@@ -59,13 +67,6 @@ export async function cmdServe() {
59
67
  // ── Public API ─────────────────────────────────────
60
68
  app.get("/api/health", (c) => c.json({ status: "ok", uptime: process.uptime() }));
61
69
 
62
- /** GET /api/integrations/supabase/scan — scan local project */
63
- app.get("/api/integrations/supabase/scan", (c) => {
64
- const projectRoot = path.resolve(process.cwd());
65
- const scan = scanSupabase(projectRoot);
66
- return c.json({ success: true, ...scan });
67
- });
68
-
69
70
  /** GET /api/users — list all users from Supabase */
70
71
  app.get("/api/users", async (c) => {
71
72
  const { client, errors } = getSupabaseClient();
@@ -90,7 +91,6 @@ export async function cmdServe() {
90
91
  const result = buildAuthorizeUrl({ provider, redirectUri, state: query.state, scope: query.scope });
91
92
  return c.redirect(result.url);
92
93
  } catch (e) {
93
- // Provider not found — redirect to authorize page
94
94
  return c.redirect("/authorize");
95
95
  }
96
96
  });
@@ -104,6 +104,33 @@ export async function cmdServe() {
104
104
  return c.json({ providers: listProviderStatus() });
105
105
  });
106
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
+
107
134
  // ── Supabase Platform OAuth with PKCE ───────────────
108
135
 
109
136
  /** GET /api/auth/supabase/authorize — start Supabase OAuth flow */
@@ -111,11 +138,7 @@ export async function cmdServe() {
111
138
  const query = c.req.query();
112
139
  const { verifier, challenge } = generatePKCE();
113
140
  const state = randomBytes(16).toString("hex");
114
-
115
- // Store verifier for later exchange
116
141
  pkceState.set(state, { verifier, challenge });
117
-
118
- // Auto-expire state after 10 min
119
142
  setTimeout(() => pkceState.delete(state), 10 * 60 * 1000);
120
143
 
121
144
  const result = buildSupabaseAuthorizeUrl({
@@ -123,65 +146,85 @@ export async function cmdServe() {
123
146
  codeChallenge: challenge,
124
147
  organizationSlug: query.organization,
125
148
  });
126
-
127
149
  return c.redirect(result.url);
128
150
  });
129
151
 
130
- /** 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. */
131
155
  app.get("/api/auth/supabase/callback", async (c) => {
132
156
  const query = c.req.query();
133
157
  const code = query.code;
134
158
  const state = query.state;
135
159
 
136
- // Validate state and get stored verifier
137
160
  const stored = pkceState.get(state);
138
161
  if (!stored) {
139
- return c.html(`<h1>Invalid or expired state token</h1>
140
- <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);
141
163
  }
142
-
143
164
  pkceState.delete(state);
144
-
145
165
  if (!code) {
146
- return c.html(`<h1>Authorization failed</h1>
147
- <p>Return to <a href="/">dashboard</a></p>`, 400);
166
+ return c.html(`<h1>Authorization failed</h1><p><a href="/">dashboard</a></p>`, 400);
148
167
  }
149
168
 
150
169
  try {
151
- // Exchange code for tokens
152
- const tokens = await exchangeSupabaseToken({
153
- code,
154
- verifier: stored.verifier,
155
- });
156
-
157
- // Save tokens to DB
170
+ const tokens = await exchangeSupabaseToken({ code, verifier: stored.verifier });
158
171
  await saveSupabaseTokens({
159
172
  accessToken: tokens.access_token,
160
173
  refreshToken: tokens.refresh_token,
161
174
  expiresIn: tokens.expires_in,
162
175
  });
163
176
 
164
- // Auto-configure: get project, extract keys, fill .env.local
165
- const config = await autoConfigureFromToken(tokens.access_token);
166
-
167
- if (config) {
168
- return c.html(`<div style="font-family:sans-serif;max-width:500px;margin:60px auto;text-align:center">
169
- <h1 style="color:#22c55e">Connected to Supabase</h1>
170
- <p>Project: <strong>${config.projectName}</strong></p>
171
- <p><code style="background:#eee;padding:2px 8px;border-radius:4px">${config.projectRef}</code></p>
172
- <p style="color:#666">.env.local and authly.config.json have been updated.</p>
173
- <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>
174
- </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>`);
175
217
  }
176
218
 
177
- return c.html(`<div style="font-family:sans-serif;max-width:500px;margin:60px auto;text-align:center">
178
- <h1 style="color:#22c55e">Authenticated</h1>
179
- <p>Supabase tokens saved successfully.</p>
180
- <p>Return to <a href="/">dashboard</a>.</p>
181
- </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>`);
182
226
  } catch (e) {
183
- return c.html(`<h1>Token exchange failed</h1><p>${e.message}</p>
184
- <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);
185
228
  }
186
229
  });
187
230
 
@@ -189,7 +232,7 @@ export async function cmdServe() {
189
232
  app.get("/api/supabase/projects", async (c) => {
190
233
  try {
191
234
  const token = await ensureValidAccessToken();
192
- 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);
193
236
  const projects = await getProjects(token);
194
237
  return c.json({ success: true, projects });
195
238
  } catch (e) {
@@ -204,7 +247,7 @@ export async function cmdServe() {
204
247
  const token = await ensureValidAccessToken();
205
248
  if (!token) return c.json({ error: "Not connected to Supabase" }, 401);
206
249
  const keys = await getProjectApiKeys(token, ref);
207
- return c.json({ success: true, keys });
250
+ return c.json({ success: true, keys: keys.filter(k => ["anon", "service_role"].includes(k.type)) });
208
251
  } catch (e) {
209
252
  return c.json({ error: e.message }, 500);
210
253
  }
@@ -221,57 +264,7 @@ export async function cmdServe() {
221
264
  }
222
265
  });
223
266
 
224
- /** POST /api/auth/:provider/authorize — get OAuth URL as JSON */
225
- app.post("/api/auth/:provider/authorize", async (c) => {
226
- const { provider } = c.req.param();
227
- const body = await c.req.json();
228
- const result = buildAuthorizeUrl({
229
- provider,
230
- redirectUri: body.redirectUri,
231
- state: body.state,
232
- scope: body.scope,
233
- });
234
- if (result.error) return c.json({ error: result.error }, 400);
235
- return c.json(result);
236
- });
237
-
238
- /** POST /api/auth/ :provider/callback — exchange code for session */
239
- app.post("/api/auth/:provider/callback", async (c) => {
240
- const { provider } = c.req.param();
241
- const body = await c.req.json();
242
- const { session, error } = await exchangeOAuthCode({
243
- provider,
244
- code: body.code,
245
- redirectUri: body.redirectUri,
246
- });
247
- if (error) return c.json({ success: false, error }, 400);
248
-
249
- // Create an internal Authly session token too
250
- const token = await createSessionToken({
251
- sub: session.user?.id ?? "unknown",
252
- role: session.user?.user_metadata?.role ?? "user",
253
- });
254
- return c.json({ success: true, session, token });
255
- });
256
-
257
- /** POST /api/auth/session — create a session token from credentials */
258
- app.post("/api/auth/session", async (c) => {
259
- const body = await c.req.json();
260
- if (!body.sub) return c.json({ error: "Missing 'sub' field" }, 400);
261
- const token = await createSessionToken({
262
- sub: body.sub,
263
- role: body.role ?? "user",
264
- });
265
- return c.json({ success: true, token });
266
- });
267
-
268
- /** GET /api/auth/me — verify session token from Authorization header */
269
- app.get("/api/auth/me", authMiddleware(), (c) =>
270
- c.json({ success: true, session: c.get("session") }),
271
- );
272
-
273
267
  // ── Password Auth ──────────────────────────────────
274
- /** POST /api/auth/register — sign up with email + password */
275
268
  app.post("/api/auth/register", async (c) => {
276
269
  const body = await c.req.json();
277
270
  const { user, token, error } = await signUp({
@@ -283,7 +276,6 @@ export async function cmdServe() {
283
276
  return c.json({ success: true, user, token });
284
277
  });
285
278
 
286
- /** POST /api/auth/login — sign in with email + password */
287
279
  app.post("/api/auth/login", async (c) => {
288
280
  const body = await c.req.json();
289
281
  const { user, token, error } = await signIn({
@@ -294,13 +286,25 @@ export async function cmdServe() {
294
286
  return c.json({ success: true, user, token });
295
287
  });
296
288
 
297
- /** GET /api/auth/providers — list available and enabled providers */
298
289
  app.get("/api/auth/providers", (c) => {
299
290
  const providers = getProviders();
300
291
  return c.json({ providers });
301
292
  });
302
293
 
303
- /** 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
+
304
308
  app.post("/api/auth/magic-link/send", async (c) => {
305
309
  const body = await c.req.json();
306
310
  const result = await sendMagicLink({ email: body.email, callbackUrl: body.callbackUrl || "/" });
@@ -308,7 +312,6 @@ export async function cmdServe() {
308
312
  return c.json({ success: true, sent: true });
309
313
  });
310
314
 
311
- /** POST /api/auth/magic-link/verify — verify magic link token */
312
315
  app.post("/api/auth/magic-link/verify", async (c) => {
313
316
  const body = await c.req.json();
314
317
  const { user, token, error } = await verifyMagicLink({ token: body.token });
@@ -316,26 +319,13 @@ export async function cmdServe() {
316
319
  return c.json({ success: true, user, token });
317
320
  });
318
321
 
319
- /** GET /api/config non-sensitive project config */
320
- app.get("/api/config", async (c) => {
321
- const fw = detectFramework();
322
- const { roles = [], error } = await listRoles();
323
- return c.json({
324
- framework: fw ?? null,
325
- providers: listProviderStatus(),
326
- roles,
327
- });
328
- });
329
-
330
- // ── Role management (protected) ────────────────────
331
- /** GET /api/roles — list all roles */
322
+ // ── Role management ────────────────────────────────
332
323
  app.get("/api/roles", async (c) => {
333
324
  const { roles, error } = await listRoles();
334
325
  if (error) return c.json({ success: false, error }, 500);
335
326
  return c.json({ success: true, roles });
336
327
  });
337
328
 
338
- /** POST /api/roles — create a new role */
339
329
  app.post("/api/roles", async (c) => {
340
330
  const { name, description } = await c.req.json();
341
331
  if (!name) return c.json({ error: "'name' is required" }, 400);
@@ -344,7 +334,6 @@ export async function cmdServe() {
344
334
  return c.json(result);
345
335
  });
346
336
 
347
- /** POST /api/roles/:roleId/users/:userId/assign — assign role to user */
348
337
  app.post("/api/roles/:roleName/users/:userId/assign", async (c) => {
349
338
  const { roleName, userId } = c.req.param();
350
339
  const result = await assignRoleToUser(userId, roleName);
@@ -352,7 +341,6 @@ export async function cmdServe() {
352
341
  return c.json(result);
353
342
  });
354
343
 
355
- /** DELETE /api/roles/:roleId/users/:userId/revoke — revoke role from user */
356
344
  app.delete("/api/roles/:roleName/users/:userId/revoke", async (c) => {
357
345
  const { roleName, userId } = c.req.param();
358
346
  const result = await revokeRoleFromUser(userId, roleName);
@@ -360,7 +348,6 @@ export async function cmdServe() {
360
348
  return c.json(result);
361
349
  });
362
350
 
363
- /** GET /api/users/:userId/roles — get roles for a user */
364
351
  app.get("/api/users/:userId/roles", async (c) => {
365
352
  const { userId } = c.req.param();
366
353
  const { roles, error } = await getUserRoles(userId);
@@ -369,7 +356,6 @@ export async function cmdServe() {
369
356
  });
370
357
 
371
358
  // ── API Keys ───────────────────────────────────────
372
- /** POST /api/keys — generate a new API key */
373
359
  app.post("/api/keys", async (c) => {
374
360
  const { client, errors } = getSupabaseClient();
375
361
  if (!client) return c.json({ success: false, errors }, 503);
@@ -389,17 +375,14 @@ export async function cmdServe() {
389
375
  });
390
376
  if (error) return c.json({ success: false, error: error.message }, 400);
391
377
 
392
- // Return raw key ONCE — it cannot be retrieved later
393
378
  return c.json({ success: true, key: rawKey, hashesTo: keyHash });
394
379
  });
395
380
 
396
381
  // ── Migrations ─────────────────────────────────────
397
- /** GET /api/migrations — list available migrations */
398
382
  app.get("/api/migrations", (c) =>
399
383
  c.json({ success: true, migrations: listMigrations() }),
400
384
  );
401
385
 
402
- /** GET /api/migrations/:name/sql — get SQL for a migration */
403
386
  app.get("/api/migrations/:name/sql", (c) => {
404
387
  const { name } = c.req.param();
405
388
  const sql = getMigration(name);
@@ -407,7 +390,6 @@ export async function cmdServe() {
407
390
  return c.json({ success: true, name, sql });
408
391
  });
409
392
 
410
- /** POST /api/migrations/:name/run — execute a migration against Supabase */
411
393
  app.post("/api/migrations/:name/run", async (c) => {
412
394
  const { client, errors } = getSupabaseClient();
413
395
  if (!client) return c.json({ success: false, errors }, 503);
@@ -421,8 +403,7 @@ export async function cmdServe() {
421
403
  return c.json({ success: true, migration: name });
422
404
  });
423
405
 
424
- // ── Scaffold ───────────────────────────────────────
425
- /** POST /api/scaffold/preview — preview generated code without writing */
406
+ // ── Scaffold preview only ─────────────────────────
426
407
  app.post("/api/scaffold/preview", async (c) => {
427
408
  const { type } = await c.req.json();
428
409
  if (!["login", "signup", "middleware", "route-login", "route-signup"].includes(type)) {
@@ -431,78 +412,6 @@ export async function cmdServe() {
431
412
  return c.json({ success: true, type, code: previewGenerated(type) });
432
413
  });
433
414
 
434
- /** POST /api/scaffold/generate — write auth files to a project */
435
- app.post("/api/scaffold/generate", async (c) => {
436
- const { targetDir } = await c.req.json();
437
- if (!targetDir) return c.json({ error: "'targetDir' is required" }, 400);
438
- try {
439
- const { files } = await scaffoldAuth(targetDir, { apiRoutes: true });
440
- c.json({ success: true, files });
441
- } catch (e) {
442
- c.json({ success: false, error: e.message }, 500);
443
- }
444
- });
445
-
446
- // ── Init ──────────────────────────────────────────
447
- /** GET /api/init/scan — autodetect Supabase config in current project */
448
- app.get("/api/init/scan", (c) => {
449
- const projectRoot = path.resolve(process.cwd());
450
- const scan = scanSupabase(projectRoot);
451
- return c.json(scan);
452
- });
453
-
454
- /** POST /api/init/connect — connect with autodetected or manual config */
455
- app.post("/api/init/connect", async (c) => {
456
- const body = await c.req.json().catch(() => ({}));
457
-
458
- // Auto-detect first
459
- const projectRoot = path.resolve(process.cwd());
460
- const scan = scanSupabase(projectRoot);
461
-
462
- // Merge scan results with any manually provided values
463
- const url = scan.url || body.supabaseUrl || process.env.SUPABASE_URL || "";
464
- const anonKey = scan.anonKey || body.supabaseAnonKey || process.env.SUPABASE_ANON_KEY || "";
465
- const serviceKey = scan.serviceKey || body.supabaseServiceKey || process.env.SUPABASE_SERVICE_ROLE_KEY || "";
466
-
467
- // Set env vars from detected config
468
- if (url) process.env.SUPABASE_URL = url;
469
- if (anonKey) process.env.SUPABASE_ANON_KEY = anonKey;
470
- if (serviceKey) process.env.SUPABASE_SERVICE_ROLE_KEY = serviceKey;
471
-
472
- const fw = scan.framework || detectFramework();
473
- if (!fw && !body.supabaseUrl) return c.json({ success: false, error: "No Next.js project detected" }, 400);
474
-
475
- // Generate .env.local if needed
476
- if (!fs.existsSync(".env.local")) {
477
- await generateEnv(".env.local");
478
- }
479
-
480
- // Write authly.config.json
481
- const config = {
482
- framework: fw || "unknown",
483
- supabase: {
484
- url,
485
- projectRef: scan.projectRef || "",
486
- anonKey: anonKey ? "set" : "",
487
- serviceKey: serviceKey ? "set" : "",
488
- autoDetected: scan.detected,
489
- sources: scan.sources,
490
- },
491
- };
492
- fs.writeFileSync("authly.config.json", JSON.stringify(config, null, 2) + "\n");
493
-
494
- return c.json({
495
- success: true,
496
- framework: fw,
497
- supabase: {
498
- url,
499
- detected: scan.detected,
500
- canConnect: scan.canConnect,
501
- sources: scan.sources,
502
- },
503
- });
504
- });
505
-
506
415
  // ── MCP (beta) ─────────────────────────────────────
507
416
  mountMcp(app);
508
417
 
@@ -510,13 +419,11 @@ export async function cmdServe() {
510
419
  app.post("/api/audit", async (c) => {
511
420
  const issues = [];
512
421
 
513
- // Env vars
514
422
  for (const key of ["SUPABASE_URL", "SUPABASE_ANON_KEY", "AUTHLY_SECRET"]) {
515
423
  if (!process.env[key]) issues.push({ check: key, status: "fail", detail: "not set" });
516
424
  else issues.push({ check: key, status: "ok" });
517
425
  }
518
426
 
519
- // Supabase connection
520
427
  const { client } = getSupabaseClient();
521
428
  if (client) {
522
429
  const { error } = await client
@@ -542,7 +449,7 @@ export async function cmdServe() {
542
449
  }
543
450
  return c.json({
544
451
  name: "authly",
545
- version: "0.1.0",
452
+ version: "0.4.0",
546
453
  docs: "/api/health",
547
454
  });
548
455
  });