@rblez/authly 0.1.0 → 0.3.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.
- package/bin/authly.js +3 -1
- package/dist/dashboard/app.js +182 -32
- package/dist/dashboard/authorize.html +117 -0
- package/dist/dashboard/index.html +192 -15
- package/dist/dashboard/styles.css +10 -0
- package/package.json +3 -2
- package/src/auth/index.js +98 -0
- package/src/commands/ext.js +107 -0
- package/src/commands/serve.js +218 -15
- package/src/generators/env.js +3 -2
- package/src/generators/migrations.js +33 -0
- package/src/integrations/supabase.js +156 -0
- package/src/lib/oauth.js +23 -1
- package/src/lib/supabase-api.js +152 -0
- package/src/lib/supabase-oauth.js +200 -0
package/src/commands/serve.js
CHANGED
|
@@ -8,7 +8,7 @@ import chalk from "chalk";
|
|
|
8
8
|
import ora from "ora";
|
|
9
9
|
import { getSupabaseClient, fetchUsers } from "../lib/supabase.js";
|
|
10
10
|
import { createSessionToken, verifySessionToken, authMiddleware, requireRole } from "../lib/jwt.js";
|
|
11
|
-
import { signUp, signIn, signOut, getSession, getProviders, handleOAuthCallback } from "../auth/index.js";
|
|
11
|
+
import { signUp, signIn, signOut, getSession, getProviders, handleOAuthCallback, sendMagicLink, verifyMagicLink } from "../auth/index.js";
|
|
12
12
|
import { buildAuthorizeUrl, exchangeTokens, listProviderStatus } from "../lib/oauth.js";
|
|
13
13
|
import {
|
|
14
14
|
createRole,
|
|
@@ -22,10 +22,20 @@ import { detectFramework } from "../lib/framework.js";
|
|
|
22
22
|
import { scaffoldAuth, previewGenerated } from "../generators/ui.js";
|
|
23
23
|
import { generateEnv } from "../generators/env.js";
|
|
24
24
|
import { mountMcp } from "../mcp/server.js";
|
|
25
|
+
import { scanSupabase } from "../integrations/supabase.js";
|
|
26
|
+
import { generatePKCE, buildSupabaseAuthorizeUrl, exchangeSupabaseToken, saveSupabaseTokens } from "../lib/supabase-oauth.js";
|
|
27
|
+
import { ensureValidAccessToken, autoConfigureFromToken, getProjects, getProjectApiKeys } from "../lib/supabase-api.js";
|
|
25
28
|
|
|
26
|
-
const PORT = process.env.AUTHLY_PORT || 1284;
|
|
29
|
+
const PORT = process.env.PORT || process.env.AUTHLY_PORT || 1284;
|
|
27
30
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
28
31
|
|
|
32
|
+
/**
|
|
33
|
+
* In-memory PKCE state store (server-side, dev-only).
|
|
34
|
+
* Key: state token, Value: { verifier, codeChallenge }
|
|
35
|
+
* Survives no restart — fine for local dev.
|
|
36
|
+
*/
|
|
37
|
+
const pkceState = new Map();
|
|
38
|
+
|
|
29
39
|
export async function cmdServe() {
|
|
30
40
|
const spinner = ora("Starting authly dashboard…").start();
|
|
31
41
|
|
|
@@ -49,6 +59,13 @@ export async function cmdServe() {
|
|
|
49
59
|
// ── Public API ─────────────────────────────────────
|
|
50
60
|
app.get("/api/health", (c) => c.json({ status: "ok", uptime: process.uptime() }));
|
|
51
61
|
|
|
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
|
+
|
|
52
69
|
/** GET /api/users — list all users from Supabase */
|
|
53
70
|
app.get("/api/users", async (c) => {
|
|
54
71
|
const { client, errors } = getSupabaseClient();
|
|
@@ -63,7 +80,148 @@ export async function cmdServe() {
|
|
|
63
80
|
c.json({ providers: listProviderStatus() }),
|
|
64
81
|
);
|
|
65
82
|
|
|
66
|
-
/**
|
|
83
|
+
/** GET /api/auth/:provider/authorize — redirect to provider */
|
|
84
|
+
app.get("/api/auth/:provider/authorize", async (c) => {
|
|
85
|
+
const { provider } = c.req.param();
|
|
86
|
+
const query = c.req.query();
|
|
87
|
+
const redirectUri = query.redirectUri || `${c.req.url.split("/api/")[0]}/api/auth/${provider}/callback`;
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const result = buildAuthorizeUrl({ provider, redirectUri, state: query.state, scope: query.scope });
|
|
91
|
+
return c.redirect(result.url);
|
|
92
|
+
} catch (e) {
|
|
93
|
+
// Provider not found — redirect to authorize page
|
|
94
|
+
return c.redirect("/authorize");
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
/** GET /authorize — sign-in page listing available providers */
|
|
99
|
+
app.get("/authorize", (c) => {
|
|
100
|
+
if (hasDashboard) {
|
|
101
|
+
const html = fs.readFileSync(path.join(dashboardPath, "authorize.html"), "utf-8");
|
|
102
|
+
return c.html(html);
|
|
103
|
+
}
|
|
104
|
+
return c.json({ providers: listProviderStatus() });
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// ── Supabase Platform OAuth with PKCE ───────────────
|
|
108
|
+
|
|
109
|
+
/** GET /api/auth/supabase/authorize — start Supabase OAuth flow */
|
|
110
|
+
app.get("/api/auth/supabase/authorize", async (c) => {
|
|
111
|
+
const query = c.req.query();
|
|
112
|
+
const { verifier, challenge } = generatePKCE();
|
|
113
|
+
const state = randomBytes(16).toString("hex");
|
|
114
|
+
|
|
115
|
+
// Store verifier for later exchange
|
|
116
|
+
pkceState.set(state, { verifier, challenge });
|
|
117
|
+
|
|
118
|
+
// Auto-expire state after 10 min
|
|
119
|
+
setTimeout(() => pkceState.delete(state), 10 * 60 * 1000);
|
|
120
|
+
|
|
121
|
+
const result = buildSupabaseAuthorizeUrl({
|
|
122
|
+
state,
|
|
123
|
+
codeChallenge: challenge,
|
|
124
|
+
organizationSlug: query.organization,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
return c.redirect(result.url);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
/** GET /api/auth/supabase/callback — OAuth callback with PKCE */
|
|
131
|
+
app.get("/api/auth/supabase/callback", async (c) => {
|
|
132
|
+
const query = c.req.query();
|
|
133
|
+
const code = query.code;
|
|
134
|
+
const state = query.state;
|
|
135
|
+
|
|
136
|
+
// Validate state and get stored verifier
|
|
137
|
+
const stored = pkceState.get(state);
|
|
138
|
+
if (!stored) {
|
|
139
|
+
return c.html(`<h1>Invalid or expired state token</h1>
|
|
140
|
+
<p>Return to <a href="/">dashboard</a></p>`, 400);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
pkceState.delete(state);
|
|
144
|
+
|
|
145
|
+
if (!code) {
|
|
146
|
+
return c.html(`<h1>Authorization failed</h1>
|
|
147
|
+
<p>Return to <a href="/">dashboard</a></p>`, 400);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
// Exchange code for tokens
|
|
152
|
+
const tokens = await exchangeSupabaseToken({
|
|
153
|
+
code,
|
|
154
|
+
verifier: stored.verifier,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// Save tokens to DB
|
|
158
|
+
await saveSupabaseTokens({
|
|
159
|
+
accessToken: tokens.access_token,
|
|
160
|
+
refreshToken: tokens.refresh_token,
|
|
161
|
+
expiresIn: tokens.expires_in,
|
|
162
|
+
});
|
|
163
|
+
|
|
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>`);
|
|
175
|
+
}
|
|
176
|
+
|
|
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>`);
|
|
182
|
+
} 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);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
/** GET /api/supabase/projects — list Supabase projects */
|
|
189
|
+
app.get("/api/supabase/projects", async (c) => {
|
|
190
|
+
try {
|
|
191
|
+
const token = await ensureValidAccessToken();
|
|
192
|
+
if (!token) return c.json({ error: "Not connected to Supabase — visit /api/auth/supabase/authorize" }, 401);
|
|
193
|
+
const projects = await getProjects(token);
|
|
194
|
+
return c.json({ success: true, projects });
|
|
195
|
+
} catch (e) {
|
|
196
|
+
return c.json({ error: e.message }, 500);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
/** GET /api/supabase/projects/:ref/keys — get API keys for a project */
|
|
201
|
+
app.get("/api/supabase/projects/:ref/keys", async (c) => {
|
|
202
|
+
const { ref } = c.req.param();
|
|
203
|
+
try {
|
|
204
|
+
const token = await ensureValidAccessToken();
|
|
205
|
+
if (!token) return c.json({ error: "Not connected to Supabase" }, 401);
|
|
206
|
+
const keys = await getProjectApiKeys(token, ref);
|
|
207
|
+
return c.json({ success: true, keys });
|
|
208
|
+
} catch (e) {
|
|
209
|
+
return c.json({ error: e.message }, 500);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
/** POST /api/auth/supabase/refresh — refresh expired token */
|
|
214
|
+
app.post("/api/auth/supabase/refresh", async (c) => {
|
|
215
|
+
try {
|
|
216
|
+
const token = await ensureValidAccessToken();
|
|
217
|
+
if (!token) return c.json({ error: "Not connected to Supabase" }, 401);
|
|
218
|
+
return c.json({ success: true });
|
|
219
|
+
} catch (e) {
|
|
220
|
+
return c.json({ error: e.message }, 500);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
/** POST /api/auth/:provider/authorize — get OAuth URL as JSON */
|
|
67
225
|
app.post("/api/auth/:provider/authorize", async (c) => {
|
|
68
226
|
const { provider } = c.req.param();
|
|
69
227
|
const body = await c.req.json();
|
|
@@ -142,6 +300,22 @@ export async function cmdServe() {
|
|
|
142
300
|
return c.json({ providers });
|
|
143
301
|
});
|
|
144
302
|
|
|
303
|
+
/** POST /api/auth/magic-link/send — send magic link email */
|
|
304
|
+
app.post("/api/auth/magic-link/send", async (c) => {
|
|
305
|
+
const body = await c.req.json();
|
|
306
|
+
const result = await sendMagicLink({ email: body.email, callbackUrl: body.callbackUrl || "/" });
|
|
307
|
+
if (result.error) return c.json({ success: false, error: result.error }, 400);
|
|
308
|
+
return c.json({ success: true, sent: true });
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
/** POST /api/auth/magic-link/verify — verify magic link token */
|
|
312
|
+
app.post("/api/auth/magic-link/verify", async (c) => {
|
|
313
|
+
const body = await c.req.json();
|
|
314
|
+
const { user, token, error } = await verifyMagicLink({ token: body.token });
|
|
315
|
+
if (error) return c.json({ success: false, error }, 401);
|
|
316
|
+
return c.json({ success: true, user, token });
|
|
317
|
+
});
|
|
318
|
+
|
|
145
319
|
/** GET /api/config — non-sensitive project config */
|
|
146
320
|
app.get("/api/config", async (c) => {
|
|
147
321
|
const fw = detectFramework();
|
|
@@ -270,16 +444,33 @@ export async function cmdServe() {
|
|
|
270
444
|
});
|
|
271
445
|
|
|
272
446
|
// ── Init ──────────────────────────────────────────
|
|
273
|
-
/**
|
|
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 */
|
|
274
455
|
app.post("/api/init/connect", async (c) => {
|
|
275
456
|
const body = await c.req.json().catch(() => ({}));
|
|
276
|
-
const fw = detectFramework();
|
|
277
|
-
if (!fw) return c.json({ success: false, error: "No Next.js project detected" }, 400);
|
|
278
457
|
|
|
279
|
-
//
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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);
|
|
283
474
|
|
|
284
475
|
// Generate .env.local if needed
|
|
285
476
|
if (!fs.existsSync(".env.local")) {
|
|
@@ -288,16 +479,28 @@ export async function cmdServe() {
|
|
|
288
479
|
|
|
289
480
|
// Write authly.config.json
|
|
290
481
|
const config = {
|
|
291
|
-
framework: fw,
|
|
482
|
+
framework: fw || "unknown",
|
|
292
483
|
supabase: {
|
|
293
|
-
url
|
|
294
|
-
|
|
295
|
-
|
|
484
|
+
url,
|
|
485
|
+
projectRef: scan.projectRef || "",
|
|
486
|
+
anonKey: anonKey ? "set" : "",
|
|
487
|
+
serviceKey: serviceKey ? "set" : "",
|
|
488
|
+
autoDetected: scan.detected,
|
|
489
|
+
sources: scan.sources,
|
|
296
490
|
},
|
|
297
491
|
};
|
|
298
492
|
fs.writeFileSync("authly.config.json", JSON.stringify(config, null, 2) + "\n");
|
|
299
493
|
|
|
300
|
-
return c.json({
|
|
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
|
+
});
|
|
301
504
|
});
|
|
302
505
|
|
|
303
506
|
// ── MCP (beta) ─────────────────────────────────────
|
package/src/generators/env.js
CHANGED
|
@@ -27,8 +27,9 @@ GOOGLE_CLIENT_SECRET=""
|
|
|
27
27
|
GITHUB_CLIENT_ID=""
|
|
28
28
|
GITHUB_CLIENT_SECRET=""
|
|
29
29
|
|
|
30
|
-
# Magic Link (optional)
|
|
31
|
-
|
|
30
|
+
# Magic Link via Resend (optional)
|
|
31
|
+
RESEND_API_KEY=""
|
|
32
|
+
RESEND_FROM="noreply@authly.dev"
|
|
32
33
|
|
|
33
34
|
# Dashboard (optional overrides)
|
|
34
35
|
# AUTHLY_PORT=1284
|
|
@@ -125,6 +125,39 @@ CREATE TABLE IF NOT EXISTS public.authly_sessions (
|
|
|
125
125
|
|
|
126
126
|
CREATE INDEX idx_sessions_user ON public.authly_sessions(user_id);
|
|
127
127
|
CREATE INDEX idx_sessions_token ON public.authly_sessions(token_hash);
|
|
128
|
+
`,
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
name: "007_create_magic_links_table",
|
|
132
|
+
description: "Magic Link auth via Resend — one-time-use tokens",
|
|
133
|
+
sql: `
|
|
134
|
+
CREATE TABLE IF NOT EXISTS public.authly_magic_links (
|
|
135
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
136
|
+
user_id uuid NOT NULL REFERENCES public.authly_users(id) ON DELETE CASCADE,
|
|
137
|
+
token_hash text UNIQUE NOT NULL,
|
|
138
|
+
expires_at timestamptz NOT NULL,
|
|
139
|
+
used boolean DEFAULT false,
|
|
140
|
+
created_at timestamptz DEFAULT now()
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
CREATE INDEX idx_magic_links_token ON public.authly_magic_links(token_hash);
|
|
144
|
+
CREATE INDEX idx_magic_links_user ON public.authly_magic_links(user_id);
|
|
145
|
+
`,
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
name: "008_create_supabase_tokens_table",
|
|
149
|
+
description: "Supabase Platform OAuth tokens — manage user's Supabase projects",
|
|
150
|
+
sql: `
|
|
151
|
+
CREATE TABLE IF NOT EXISTS public.authly_supabase_tokens (
|
|
152
|
+
user_id uuid PRIMARY KEY REFERENCES public.authly_users(id) ON DELETE CASCADE,
|
|
153
|
+
access_token text NOT NULL,
|
|
154
|
+
refresh_token text NOT NULL,
|
|
155
|
+
expires_at timestamptz NOT NULL,
|
|
156
|
+
project_ref text,
|
|
157
|
+
project_name text,
|
|
158
|
+
created_at timestamptz DEFAULT now(),
|
|
159
|
+
updated_at timestamptz DEFAULT now()
|
|
160
|
+
);
|
|
128
161
|
`,
|
|
129
162
|
},
|
|
130
163
|
];
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Supabase auto-detection integration.
|
|
3
|
+
*
|
|
4
|
+
* Scans the local Next.js project for Supabase credentials.
|
|
5
|
+
* No PAT or manual input needed — Authly finds them in env files.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Read a .env file format and return key-value pairs.
|
|
13
|
+
* @param {string} filePath
|
|
14
|
+
* @returns {Record<string, string>}
|
|
15
|
+
*/
|
|
16
|
+
function _parseEnv(filePath) {
|
|
17
|
+
const result = {};
|
|
18
|
+
if (!fs.existsSync(filePath)) return result;
|
|
19
|
+
|
|
20
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
21
|
+
for (const line of content.split("\n")) {
|
|
22
|
+
const trimmed = line.trim();
|
|
23
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
24
|
+
const idx = trimmed.indexOf("=");
|
|
25
|
+
if (idx === -1) continue;
|
|
26
|
+
const key = trimmed.slice(0, idx).trim();
|
|
27
|
+
let value = trimmed.slice(idx + 1).trim();
|
|
28
|
+
// Remove surrounding quotes
|
|
29
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
30
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
31
|
+
value = value.slice(1, -1);
|
|
32
|
+
}
|
|
33
|
+
result[key] = value;
|
|
34
|
+
}
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Parse supabase/config.toml if it exists.
|
|
40
|
+
* @param {string} projectRoot
|
|
41
|
+
* @returns {{ projectRef?: string; poolerUrl?: string }}
|
|
42
|
+
*/
|
|
43
|
+
function _parseSupabaseToml(projectRoot) {
|
|
44
|
+
const tomlPath = path.join(projectRoot, "supabase", "config.toml");
|
|
45
|
+
if (!fs.existsSync(tomlPath)) return {};
|
|
46
|
+
|
|
47
|
+
const content = fs.readFileSync(tomlPath, "utf-8");
|
|
48
|
+
const refMatch = content.match(/project_id\s*=\s*"?([a-zA-Z0-9]{20})"?/);
|
|
49
|
+
return refMatch ? { projectRef: refMatch[1] } : {};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Try to find a Supabase URL in a local project.
|
|
54
|
+
* Checks: supabase/.env, .env.local, .env, supabase/config.toml
|
|
55
|
+
* @param {string} cwd
|
|
56
|
+
* @returns {string|null}
|
|
57
|
+
*/
|
|
58
|
+
function _findSupabaseUrl(cwd) {
|
|
59
|
+
// Check supabase/.env
|
|
60
|
+
const supabaseEnv = _parseEnv(path.join(cwd, "supabase", ".env"));
|
|
61
|
+
if (supabaseEnv.SUPABASE_URL) return supabaseEnv.SUPABASE_URL;
|
|
62
|
+
|
|
63
|
+
// Check common env files in order of preference
|
|
64
|
+
for (const envFile of [".env.local", ".env.development.local", ".env.development", ".env"]) {
|
|
65
|
+
const env = _parseEnv(path.join(cwd, envFile));
|
|
66
|
+
if (env.NEXT_PUBLIC_SUPABASE_URL) return env.NEXT_PUBLIC_SUPABASE_URL;
|
|
67
|
+
if (env.SUPABASE_URL) return env.SUPABASE_URL;
|
|
68
|
+
if (env.NEXT_PUBLIC_SUPABASE_ANON_KEY) {
|
|
69
|
+
// Some projects set the ref as NEXT_PUBLIC_SUPABASE_URL
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Try to find Supabase keys in a local project.
|
|
78
|
+
* @param {string} cwd
|
|
79
|
+
* @returns {{ anonKey?: string; serviceKey?: string }}
|
|
80
|
+
*/
|
|
81
|
+
function _findSupabaseKeys(cwd) {
|
|
82
|
+
const result = {};
|
|
83
|
+
|
|
84
|
+
for (const envFile of [".env.local", ".env", ".env.development.local", ".env.development"]) {
|
|
85
|
+
const env = _parseEnv(path.join(cwd, envFile));
|
|
86
|
+
if (env.NEXT_PUBLIC_SUPABASE_ANON_KEY) result.anonKey = env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
|
87
|
+
if (env.SUPABASE_ANON_KEY) result.anonKey = env.SUPABASE_ANON_KEY;
|
|
88
|
+
if (env.SUPABASE_SERVICE_ROLE_KEY) result.serviceKey = env.SUPABASE_SERVICE_ROLE_KEY;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Scan the given project directory for Supabase configuration.
|
|
96
|
+
* Returns everything found — may be partial if not all vars are configured.
|
|
97
|
+
*
|
|
98
|
+
* @param {string} cwd — Project root (where package.json lives)
|
|
99
|
+
* @returns {{
|
|
100
|
+
* detected: boolean;
|
|
101
|
+
* url?: string;
|
|
102
|
+
* anonKey?: string;
|
|
103
|
+
* serviceKey?: string;
|
|
104
|
+
* projectRef?: string;
|
|
105
|
+
* framework?: string;
|
|
106
|
+
* sources: string[];
|
|
107
|
+
* canConnect: boolean;
|
|
108
|
+
* }}
|
|
109
|
+
*/
|
|
110
|
+
export function scanSupabase(cwd) {
|
|
111
|
+
const sources = [];
|
|
112
|
+
|
|
113
|
+
const url = _findSupabaseUrl(cwd);
|
|
114
|
+
if (url) sources.push("env files");
|
|
115
|
+
|
|
116
|
+
const keys = _findSupabaseKeys(cwd);
|
|
117
|
+
if (keys.anonKey) sources.push("env files");
|
|
118
|
+
if (keys.serviceKey) sources.push("env files");
|
|
119
|
+
|
|
120
|
+
const { projectRef } = _parseSupabaseToml(cwd);
|
|
121
|
+
if (projectRef) sources.push("supabase/config.toml");
|
|
122
|
+
|
|
123
|
+
const canConnect = !!(url && keys.anonKey && keys.serviceKey);
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
detected: !!url || !!keys.anonKey,
|
|
127
|
+
url,
|
|
128
|
+
anonKey: keys.anonKey,
|
|
129
|
+
serviceKey: keys.serviceKey,
|
|
130
|
+
projectRef,
|
|
131
|
+
framework: _detectFramework(cwd),
|
|
132
|
+
sources: [...new Set(sources)],
|
|
133
|
+
canConnect,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Detect if the project is Next.js, Remit, etc.
|
|
139
|
+
* @param {string} cwd
|
|
140
|
+
* @returns {string|null}
|
|
141
|
+
*/
|
|
142
|
+
function _detectFramework(cwd) {
|
|
143
|
+
const pkgPath = path.join(cwd, "package.json");
|
|
144
|
+
if (!fs.existsSync(pkgPath)) return null;
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
148
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
149
|
+
if (deps.next) return "nextjs";
|
|
150
|
+
if (deps.remix) return "remix";
|
|
151
|
+
if (deps.sveltekit || deps["@sveltejs/kit"]) return "sveltekit";
|
|
152
|
+
if (deps.vite) return "vite";
|
|
153
|
+
} catch {}
|
|
154
|
+
|
|
155
|
+
return null;
|
|
156
|
+
}
|
package/src/lib/oauth.js
CHANGED
|
@@ -255,13 +255,32 @@ export async function authWithProvider(opts) {
|
|
|
255
255
|
};
|
|
256
256
|
}
|
|
257
257
|
|
|
258
|
+
/**
|
|
259
|
+
* List all configured providers and their status.
|
|
260
|
+
*
|
|
261
|
+
* @returns {{ name: string; enabled: boolean; scopes: string }[]}
|
|
262
|
+
*/
|
|
263
|
+
/**
|
|
264
|
+
* Include additional "providers" that are not in the PROVIDERS
|
|
265
|
+
* object (e.g. magic-link, password).
|
|
266
|
+
*
|
|
267
|
+
* @returns {{ name: string; enabled: boolean; scopes: string }[]}
|
|
268
|
+
*/
|
|
269
|
+
function _listAdditionalProviders() {
|
|
270
|
+
const extras = [];
|
|
271
|
+
if (process.env.RESEND_API_KEY) {
|
|
272
|
+
extras.push({ name: "magiclink", enabled: true, scopes: "email" });
|
|
273
|
+
}
|
|
274
|
+
return extras;
|
|
275
|
+
}
|
|
276
|
+
|
|
258
277
|
/**
|
|
259
278
|
* List all configured providers and their status.
|
|
260
279
|
*
|
|
261
280
|
* @returns {{ name: string; enabled: boolean; scopes: string }[]}
|
|
262
281
|
*/
|
|
263
282
|
export function listProviderStatus() {
|
|
264
|
-
|
|
283
|
+
const oauthProviders = Object.keys(PROVIDERS).map((name) => {
|
|
265
284
|
const upper = name.toUpperCase();
|
|
266
285
|
const clientId = process.env[`${upper}_CLIENT_ID`] || "";
|
|
267
286
|
const clientSecret = process.env[`${upper}_CLIENT_SECRET`] || "";
|
|
@@ -271,6 +290,9 @@ export function listProviderStatus() {
|
|
|
271
290
|
scopes: PROVIDERS[name].scopeDefault,
|
|
272
291
|
};
|
|
273
292
|
});
|
|
293
|
+
|
|
294
|
+
const extras = _listAdditionalProviders();
|
|
295
|
+
return [...oauthProviders, ...extras];
|
|
274
296
|
}
|
|
275
297
|
|
|
276
298
|
/**
|