@rblez/authly 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.
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@rblez/authly",
3
+ "version": "0.1.0",
4
+ "description": "Local auth dashboard for Next.js + Supabase",
5
+ "type": "module",
6
+ "bin": {
7
+ "authly": "./bin/authly.js"
8
+ },
9
+ "scripts": {
10
+ "dev": "node bin/authly.js serve",
11
+ "lint": "prettier --check src/",
12
+ "format": "prettier --write src/"
13
+ },
14
+ "engines": {
15
+ "node": ">=18.0.0"
16
+ },
17
+ "files": [
18
+ "bin/",
19
+ "src/",
20
+ "dist/"
21
+ ],
22
+ "dependencies": {
23
+ "@hono/node-server": "^1.19.12",
24
+ "@modelcontextprotocol/sdk": "^1.29.0",
25
+ "@supabase/supabase-js": "^2.101.1",
26
+ "bcryptjs": "^2.4.3",
27
+ "chalk": "^5.6.2",
28
+ "hono": "^4.12.10",
29
+ "jose": "^6.2.2",
30
+ "ora": "^9.3.0",
31
+ "zod": "^4.3.6"
32
+ },
33
+ "devDependencies": {
34
+ "prettier": "^3.8.1"
35
+ },
36
+ "keywords": [
37
+ "auth",
38
+ "supabase",
39
+ "nextjs",
40
+ "dashboard"
41
+ ],
42
+ "license": "MIT",
43
+ "author": "rblez <rblez@proton.me>",
44
+ "repository": {
45
+ "type": "git",
46
+ "url": "git+https://github.com/rblez/authly.git"
47
+ }
48
+ }
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Authly SDK — unified auth interface.
3
+ *
4
+ * signIn / signUp / signOut / getProviders / getDashboardConfig
5
+ */
6
+
7
+ import bcrypt from "bcryptjs";
8
+ import { getSupabaseClient } from "../lib/supabase.js";
9
+ import { createSessionToken, verifySessionToken } from "../lib/jwt.js";
10
+ import { buildAuthorizeUrl, exchangeTokens, upsertUser, authWithProvider, listProviderStatus } from "../lib/oauth.js";
11
+
12
+ // ── Password auth ────────────────────────────────────
13
+
14
+ /**
15
+ * Register a new user with email + password.
16
+ * Creates user in authly_users, hashes password with bcrypt.
17
+ */
18
+ export async function signUp({ email, password, name = "" }) {
19
+ const { client, errors } = getSupabaseClient();
20
+ if (!client) return { user: null, error: errors.join(", ") };
21
+
22
+ if (!email || !password || password.length < 8) {
23
+ return { user: null, error: "Email and password (min 8 chars) are required" };
24
+ }
25
+
26
+ // Check if user exists
27
+ const { data: existing } = await client
28
+ .from("authly_users")
29
+ .select("id")
30
+ .eq("email", email.toLowerCase())
31
+ .single();
32
+
33
+ if (existing) {
34
+ return { user: null, error: "An account with this email already exists" };
35
+ }
36
+
37
+ const passwordHash = await bcrypt.hash(password, 12);
38
+
39
+ const { data, error } = await client
40
+ .from("authly_users")
41
+ .insert({ email: email.toLowerCase(), password_hash: passwordHash, name })
42
+ .select("id, email, name, created_at")
43
+ .single();
44
+
45
+ if (error) return { user: null, error: error.message };
46
+
47
+ const token = await createSessionToken({ sub: data.id, role: "user" });
48
+ return { user: data, token, error: null };
49
+ }
50
+
51
+ /**
52
+ * Sign in with email + password.
53
+ */
54
+ export async function signIn({ email, password }) {
55
+ const { client, errors } = getSupabaseClient();
56
+ if (!client) return { user: null, error: errors.join(", ") };
57
+
58
+ const { data, error } = await client
59
+ .from("authly_users")
60
+ .select("id, email, name, password_hash, email_verified")
61
+ .eq("email", email.toLowerCase())
62
+ .single();
63
+
64
+ if (error || !data) {
65
+ return { user: null, error: "Invalid email or password" };
66
+ }
67
+
68
+ if (!data.password_hash) {
69
+ return { user: null, error: "This account uses OAuth login. Sign in with your provider." };
70
+ }
71
+
72
+ const valid = await bcrypt.compare(password, data.password_hash);
73
+ if (!valid) {
74
+ return { user: null, error: "Invalid email or password" };
75
+ }
76
+
77
+ const token = await createSessionToken({ sub: data.id, role: "user" });
78
+ const { id, email: userEmail, name, email_verified: emailVerified } = data;
79
+ return {
80
+ user: { id, email: userEmail, name, emailVerified },
81
+ token,
82
+ error: null,
83
+ };
84
+ }
85
+
86
+ /**
87
+ * Sign out (invalidate session — token-based so no-op server-side).
88
+ */
89
+ export function signOut() {
90
+ return { success: true };
91
+ }
92
+
93
+ /**
94
+ * Verify an existing session token.
95
+ */
96
+ export async function getSession(token) {
97
+ if (!token) return { session: null, error: "No token provided" };
98
+ const session = await verifySessionToken(token);
99
+ if (!session) return { session: null, error: "Invalid or expired token" };
100
+ return { session, error: null };
101
+ }
102
+
103
+ // ── Provider management ──────────────────────────────
104
+
105
+ /**
106
+ * Get list of all available auth providers and their enabled status.
107
+ */
108
+ export function getProviders() {
109
+ return listProviderStatus();
110
+ }
111
+
112
+ /**
113
+ * Build authorization URL for a provider.
114
+ */
115
+ export function getProviderUrl({ provider, redirectUri, state, scope }) {
116
+ try {
117
+ return buildAuthorizeUrl({ provider, redirectUri, state, scope });
118
+ } catch (e) {
119
+ return { error: e.message };
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Handle OAuth callback — exchange code, upsert user, create session.
125
+ */
126
+ export async function handleOAuthCallback({ provider, code, redirectUri }) {
127
+ try {
128
+ const user = await authWithProvider({ provider, code, redirectUri });
129
+ const token = await createSessionToken({ sub: user.userId, role: "user" });
130
+ return { user, token, error: null };
131
+ } catch (e) {
132
+ return { user: null, token: null, error: e.message };
133
+ }
134
+ }
@@ -0,0 +1,82 @@
1
+ import fs from "node:fs";
2
+ import chalk from "chalk";
3
+ import ora from "ora";
4
+ import { detectFramework } from "../lib/framework.js";
5
+
6
+ export async function cmdAudit() {
7
+ console.log(chalk.bold("\n Authly Audit\n"));
8
+
9
+ let issues = [];
10
+
11
+ // Check 1: Project detection
12
+ const fw = detectFramework();
13
+ if (fw) {
14
+ console.log(`${chalk.green("✔")} Project detected: ${chalk.cyan(fw.name)}`);
15
+ } else {
16
+ issues.push("No recognizable project framework found");
17
+ console.log(`${chalk.red("✘")} No project framework detected`);
18
+ }
19
+
20
+ // Check 2: .env.local
21
+ if (fs.existsSync(".env.local")) {
22
+ console.log(`${chalk.green("✔")} .env.local exists`);
23
+ const envContent = fs.readFileSync(".env.local", "utf-8");
24
+ const vars = [
25
+ "SUPABASE_URL",
26
+ "SUPABASE_ANON_KEY",
27
+ "SUPABASE_SERVICE_ROLE_KEY",
28
+ "AUTHLY_SECRET",
29
+ ];
30
+ for (const v of vars) {
31
+ if (envContent.includes(v)) {
32
+ const val = envContent.split(v)[1]?.split("=")[1]?.trim();
33
+ if (val && val !== '""' && val !== "''") {
34
+ console.log(` ${chalk.green("✔")} ${v} is set`);
35
+ } else {
36
+ issues.push(`${v} is empty`);
37
+ console.log(` ${chalk.red("✘")} ${v} is empty`);
38
+ }
39
+ }
40
+ }
41
+ } else {
42
+ issues.push(".env.local not found — run `npx @rblez/authly init`");
43
+ console.log(`${chalk.red("✘")} .env.local not found`);
44
+ }
45
+
46
+ // Check 3: authly.config.json
47
+ if (fs.existsSync("authly.config.json")) {
48
+ console.log(`${chalk.green("✔")} authly.config.json exists`);
49
+ try {
50
+ JSON.parse(fs.readFileSync("authly.config.json", "utf-8"));
51
+ console.log(`${chalk.green("✔")} authly.config.json is valid JSON`);
52
+ } catch {
53
+ issues.push("authly.config.json contains invalid JSON");
54
+ console.log(`${chalk.red("✘")} authly.config.json is invalid JSON`);
55
+ }
56
+ } else {
57
+ issues.push("authly.config.json not found");
58
+ console.log(`${chalk.red("✘")} authly.config.json not found`);
59
+ }
60
+
61
+ // Check 4: Middleware
62
+ const middlewarePath = "middleware.ts";
63
+ const middlewarePath2 = "middleware.tsx";
64
+ if (fs.existsSync(middlewarePath) || fs.existsSync(middlewarePath2)) {
65
+ console.log(`${chalk.green("✔")} Next.js middleware exists`);
66
+ } else {
67
+ issues.push("Next.js middleware not found");
68
+ console.log(`${chalk.yellow("•")} Next.js middleware not found (optional)`);
69
+ }
70
+
71
+ // Summary
72
+ console.log();
73
+ if (issues.length === 0) {
74
+ console.log(chalk.green.bold(" All checks passed — your auth configuration looks good\n"));
75
+ } else {
76
+ console.log(chalk.red.bold(` ${issues.length} issue(s) found:\n`));
77
+ for (const issue of issues) {
78
+ console.log(` ${chalk.red("•")} ${issue}`);
79
+ }
80
+ console.log();
81
+ }
82
+ }
@@ -0,0 +1,67 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import chalk from "chalk";
4
+ import ora from "ora";
5
+ import { detectFramework } from "../lib/framework.js";
6
+ import { generateEnv } from "../generators/env.js";
7
+
8
+ export async function cmdInit() {
9
+ console.log(chalk.bold("\n Authly Init\n"));
10
+
11
+ // Detect framework
12
+ const framework = detectFramework();
13
+ if (!framework) {
14
+ console.log(chalk.yellow(" No Next.js project detected. Run authly init from your Next.js project root.\n"));
15
+ process.exit(1);
16
+ }
17
+ console.log(`${chalk.green("✔")} Detected framework: ${chalk.cyan(framework.name)}`);
18
+
19
+ // Check for existing config
20
+ const hasEnv = fs.existsSync(".env.local");
21
+ const hasAuthlyConfig = fs.existsSync("authly.config.json");
22
+
23
+ if (hasEnv) {
24
+ console.log(`${chalk.yellow("•")} .env.local already exists`);
25
+ }
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
+
34
+ if (!hasEnv) {
35
+ await generateEnv(envPath);
36
+ spinner.succeed("Generated .env.local with authly variables");
37
+ } else {
38
+ spinner.info(".env.local already exists, skipping");
39
+ }
40
+
41
+ // Generate authly config
42
+ const configSpinner = ora("Creating authly.config.json").start();
43
+ if (!hasAuthlyConfig) {
44
+ const config = {
45
+ $schema: "https://raw.githubusercontent.com/rblez/authly/main/schema/config.json",
46
+ framework,
47
+ port: 1284,
48
+ supabase: {
49
+ url: "",
50
+ anonKey: "",
51
+ serviceRoleKey: "",
52
+ },
53
+ providers: {},
54
+ roles: ["admin", "user", "guest"],
55
+ };
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
+ );
61
+ configSpinner.succeed("Created authly.config.json");
62
+ } else {
63
+ configSpinner.info("authly.config.json already exists");
64
+ }
65
+
66
+ console.log(chalk.dim("\n Next: run `npx @rblez/authly serve` to start the dashboard\n"));
67
+ }