@fluixi/cli 0.1.0-alpha.81 → 0.1.0-alpha.83

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.
Files changed (57) hide show
  1. package/README.md +21 -3
  2. package/dist/add.cjs +590 -0
  3. package/dist/add.d.ts +10 -0
  4. package/dist/add.d.ts.map +1 -0
  5. package/dist/add.js +107 -0
  6. package/dist/add.mjs +562 -0
  7. package/dist/check.cjs +102 -0
  8. package/dist/check.d.ts +13 -0
  9. package/dist/check.d.ts.map +1 -0
  10. package/dist/check.js +88 -0
  11. package/dist/check.mjs +76 -0
  12. package/dist/cli.cjs +977 -78
  13. package/dist/cli.js +44 -2
  14. package/dist/cli.mjs +976 -77
  15. package/dist/doctor.cjs +119 -0
  16. package/dist/doctor.d.ts +13 -0
  17. package/dist/doctor.d.ts.map +1 -0
  18. package/dist/doctor.js +114 -0
  19. package/dist/doctor.mjs +92 -0
  20. package/dist/generate.cjs +614 -65
  21. package/dist/generate.d.ts.map +1 -1
  22. package/dist/generate.js +118 -48
  23. package/dist/generate.mjs +619 -64
  24. package/dist/index.cjs +1 -1
  25. package/dist/index.mjs +1 -1
  26. package/dist/report.cjs +54 -0
  27. package/dist/report.d.ts +5 -0
  28. package/dist/report.d.ts.map +1 -0
  29. package/dist/report.js +31 -0
  30. package/dist/report.mjs +28 -0
  31. package/dist/scaffold/auth.cjs +422 -0
  32. package/dist/scaffold/auth.d.ts +39 -0
  33. package/dist/scaffold/auth.d.ts.map +1 -0
  34. package/dist/scaffold/auth.js +137 -0
  35. package/dist/scaffold/auth.mjs +389 -0
  36. package/dist/scaffold/edit.cjs +153 -0
  37. package/dist/scaffold/edit.d.ts +35 -0
  38. package/dist/scaffold/edit.d.ts.map +1 -0
  39. package/dist/scaffold/edit.js +140 -0
  40. package/dist/scaffold/edit.mjs +124 -0
  41. package/dist/scaffold/index.cjs +610 -0
  42. package/dist/scaffold/index.d.ts +10 -0
  43. package/dist/scaffold/index.d.ts.map +1 -0
  44. package/dist/scaffold/index.js +9 -0
  45. package/dist/scaffold/index.mjs +569 -0
  46. package/dist/scaffold/prompt.cjs +108 -0
  47. package/dist/scaffold/prompt.d.ts +9 -0
  48. package/dist/scaffold/prompt.d.ts.map +1 -0
  49. package/dist/scaffold/prompt.js +95 -0
  50. package/dist/scaffold/prompt.mjs +81 -0
  51. package/dist/scaffold/templates.cjs +274 -0
  52. package/dist/scaffold/templates.d.ts +26 -0
  53. package/dist/scaffold/templates.d.ts.map +1 -0
  54. package/dist/scaffold/templates.js +216 -0
  55. package/dist/scaffold/templates.mjs +229 -0
  56. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  57. package/package.json +19 -3
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Authentication wired into an app: the files, the registrations and the variables.
3
+ *
4
+ * One implementation for `fluixi g auth`, `fluixi add auth` and create-fluixi, so a new app and
5
+ * an existing one get the same code. It works from a project root, writes only files that do
6
+ * not exist yet, and returns what it did for the caller to report.
7
+ */
8
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
9
+ import { dirname, join, relative } from 'node:path';
10
+ import { addEnvExample, addEnvPublic, addImport, register } from './edit.js';
11
+ import * as t from './templates.js';
12
+ /** A refusal the caller reports as it reports any bad input. */
13
+ export class ScaffoldError extends Error {
14
+ }
15
+ /** `jsx` when the project's tsconfig configures it, `html` for an html``-authored one. */
16
+ export function projectFormat(root) {
17
+ try {
18
+ const ts = JSON.parse(readFileSync(join(root, 'tsconfig.json'), 'utf8'));
19
+ if (ts?.compilerOptions && !ts.compilerOptions.jsx)
20
+ return 'html';
21
+ }
22
+ catch {
23
+ // No tsconfig, or one with comments: JSX, the default.
24
+ }
25
+ return 'jsx';
26
+ }
27
+ /** A path for an import between two files, with the .js extension ESM needs. */
28
+ export function importPath(from, to) {
29
+ const p = relative(dirname(from), to).replace(/\\/g, '/').replace(/\.tsx?$/, '.js');
30
+ return p.startsWith('.') ? p : `./${p}`;
31
+ }
32
+ /** Write a file the app may already have, keeping theirs. Returns whether it wrote. */
33
+ export function writeOnce(log, file, content) {
34
+ if (existsSync(file)) {
35
+ log.push({ kind: 'kept', file });
36
+ return false;
37
+ }
38
+ mkdirSync(dirname(file), { recursive: true });
39
+ writeFileSync(file, content);
40
+ log.push({ kind: 'created', file });
41
+ return true;
42
+ }
43
+ const APP_HEADER = '// Registered by @fluixi/start ahead of both entries.\n';
44
+ const APP_SERVER_HEADER = '// Only the server build imports this file, so what it lists never reaches the browser.\n';
45
+ /** List a module in `src/app.config.ts`, or `app.config.server.ts` for a server one. */
46
+ export function registerModule(log, root, server, file, name, entry = name) {
47
+ const config = join(root, server ? 'src/app.config.server.ts' : 'src/app.config.ts');
48
+ register(log, config, server ? APP_SERVER_HEADER : APP_HEADER, {
49
+ importLine: `import { ${name} } from '${importPath(config, file)}';`,
50
+ entry,
51
+ });
52
+ }
53
+ /** `src/config/env.ts` with the `auth` reader: created when missing, appended when not there. */
54
+ export function ensureAuthEnv(log, root) {
55
+ const file = join(root, 'src/config/env.ts');
56
+ if (writeOnce(log, file, t.authEnvConfig()))
57
+ return;
58
+ let text = readFileSync(file, 'utf8');
59
+ if (/export\s+const\s+auth\b/.test(text))
60
+ return;
61
+ if (!/\bpublicEnv\b/.test(text))
62
+ text = addImport(text, `import { publicEnv } from '@fluixi/start/env';`);
63
+ text = addImport(text, `import { authFromEnv } from '@fluixi/oauth2/env';`);
64
+ writeFileSync(file, `${text.replace(/\n*$/, '\n')}\n${t.AUTH_READER}`);
65
+ log.push({ kind: 'updated', file, detail: 'auth' });
66
+ }
67
+ /** The token mode for a provider: what was asked, checked, or the provider's only option. */
68
+ export function resolveTokens(provider, tokens) {
69
+ const refusal = t.SERVER_ONLY[provider];
70
+ if (refusal && tokens === 'browser')
71
+ throw new ScaffoldError(`${provider} needs server tokens: ${refusal}`);
72
+ return { tokens: tokens ?? (refusal ? 'server' : 'browser'), forced: !!refusal && !tokens };
73
+ }
74
+ export function setupOAuth2(root, options, dependencySpec = 'alpha') {
75
+ const provider = options.provider ?? 'oidc';
76
+ if (!t.PROVIDERS.includes(provider))
77
+ throw new ScaffoldError(`unknown provider ${provider}: ${t.PROVIDERS.join(', ')}`);
78
+ const { tokens, forced } = resolveTokens(provider, options.tokens);
79
+ const html = options.format === 'html';
80
+ const ext = html ? 'ts' : 'tsx';
81
+ const steps = [];
82
+ const at = (rel) => join(root, rel);
83
+ ensureAuthEnv(steps, root);
84
+ const service = at('src/services/auth.ts');
85
+ if (writeOnce(steps, service, t.authServiceFile(tokens)))
86
+ registerModule(steps, root, false, service, 'AuthModule');
87
+ if (tokens === 'browser')
88
+ writeOnce(steps, at(`src/routes/callback.${ext}`), t.callbackRoute(html));
89
+ writeOnce(steps, at(`src/routes/login.${ext}`), t.loginRoute(html));
90
+ if (tokens === 'server') {
91
+ const server = at('src/services/auth.server.ts');
92
+ if (writeOnce(steps, server, t.authServerFile()))
93
+ registerModule(steps, root, true, server, 'authServer', 'authServer()');
94
+ }
95
+ const vars = {
96
+ AUTH_PROVIDER: provider,
97
+ AUTH_CLIENT_ID: 'my-app',
98
+ AUTH_TOKENS: tokens,
99
+ ...t.PROVIDER_VARS[provider],
100
+ APP_URL: 'http://localhost:3000',
101
+ };
102
+ addEnvPublic(steps, at('fluixi.config.ts'), Object.keys(vars));
103
+ addEnvExample(steps, at('.env.example'), `@fluixi/oauth2 (${provider}). Listed in env.public: the browser sees them.`, vars);
104
+ if (tokens === 'server') {
105
+ addEnvExample(steps, at('.env.example'), 'Server only, never in env.public. SESSION_SECRET: 32 bytes or more.', {
106
+ OIDC_CLIENT_SECRET: '',
107
+ SESSION_SECRET: '',
108
+ });
109
+ }
110
+ const redirect = tokens === 'browser' ? '/callback' : '/auth/callback';
111
+ return {
112
+ steps,
113
+ dependencies: { '@fluixi/oauth2': dependencySpec },
114
+ next: ['copy .env.example to .env and fill it in', `register <APP_URL>${redirect} as the redirect URI at the provider`],
115
+ notes: forced ? [`${provider} refuses a public client, so the tokens stay on the server`] : [],
116
+ };
117
+ }
118
+ export function setupBetterAuth(root, dependencySpec = 'alpha') {
119
+ const steps = [];
120
+ const at = (rel) => join(root, rel);
121
+ writeOnce(steps, at('src/auth.config.ts'), t.betterAuthConfig());
122
+ writeOnce(steps, at('src/auth.client.ts'), t.betterAuthClient());
123
+ if (!writeOnce(steps, at('src/middleware.ts'), t.betterAuthMiddleware())) {
124
+ steps.push({
125
+ kind: 'hint',
126
+ file: at('src/middleware.ts'),
127
+ lines: [`import { authMiddleware } from '@fluixi/auth/server';`, `import './auth.config.js';`, 'authMiddleware() in the middleware list'],
128
+ });
129
+ }
130
+ writeOnce(steps, at('src/api/auth/[...all].ts'), t.betterAuthRoute());
131
+ return {
132
+ steps,
133
+ dependencies: { '@fluixi/auth': dependencySpec, 'better-auth': '^1' },
134
+ next: ['set your database adapter in src/auth.config.ts'],
135
+ notes: [],
136
+ };
137
+ }
@@ -0,0 +1,389 @@
1
+ // src/scaffold/auth.ts
2
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
3
+ import { dirname as dirname2, join, relative } from "node:path";
4
+
5
+ // src/scaffold/edit.ts
6
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
7
+ import { dirname } from "node:path";
8
+ function closing(text, open) {
9
+ let depth = 0;
10
+ for (let i = open; i < text.length; i++) {
11
+ const ch = text[i];
12
+ if (ch === "/" && text[i + 1] === "/") i = text.indexOf("\n", i) === -1 ? text.length : text.indexOf("\n", i);
13
+ else if (ch === "/" && text[i + 1] === "*") i = text.indexOf("*/", i + 2) + 1;
14
+ else if (ch === "'" || ch === '"' || ch === "`") {
15
+ for (i++; i < text.length && text[i] !== ch; i++) if (text[i] === "\\") i++;
16
+ } else if ("([{".includes(ch)) depth++;
17
+ else if (")]}".includes(ch) && --depth === 0) return i;
18
+ }
19
+ return -1;
20
+ }
21
+ function appendToArray(text, open, items) {
22
+ const close = closing(text, open);
23
+ const inner = text.slice(open + 1, close);
24
+ if (!inner.trim()) return text.slice(0, open + 1) + items.join(", ") + text.slice(close);
25
+ if (inner.includes("\n")) {
26
+ const last = inner.trimEnd();
27
+ const indent = last.slice(last.lastIndexOf("\n") + 1).match(/^\s*/)[0];
28
+ const comma = last.endsWith(",") ? "" : ",";
29
+ const tail = inner.slice(last.length);
30
+ return text.slice(0, open + 1) + last + comma + items.map((i) => `
31
+ ${indent}${i},`).join("") + tail + text.slice(close);
32
+ }
33
+ const body = inner.replace(/,?\s*$/, "");
34
+ const trailing = inner.slice(body.length).replace(/^,/, "");
35
+ return text.slice(0, open + 1) + body + ", " + items.join(", ") + trailing + text.slice(close);
36
+ }
37
+ function addImport(text, line) {
38
+ if (text.includes(line)) return text;
39
+ const imports = [...text.matchAll(/^import[\s\S]*?from\s+['"][^'"]+['"];?[^\n]*\n/gm)];
40
+ const at = imports.length ? imports[imports.length - 1].index + imports[imports.length - 1][0].length : 0;
41
+ return text.slice(0, at) + line + "\n" + text.slice(at);
42
+ }
43
+ function register(log, file, header, r) {
44
+ const imports = [...r.extraImports ?? [], r.importLine];
45
+ if (!existsSync(file)) {
46
+ mkdirSync(dirname(file), { recursive: true });
47
+ writeFileSync(
48
+ file,
49
+ `${header}import { defineApp } from '@fluixi/start/app';
50
+ ${imports.join("\n")}
51
+
52
+ export default defineApp({ imports: [${r.entry}] });
53
+ `
54
+ );
55
+ log.push({ kind: "created", file });
56
+ return true;
57
+ }
58
+ let text = readFileSync(file, "utf8");
59
+ if (text.includes(r.importLine)) {
60
+ log.push({ kind: "listed", file, detail: r.entry });
61
+ return true;
62
+ }
63
+ const m = text.match(/\bimports\s*:\s*\[/);
64
+ if (!m) {
65
+ log.push({ kind: "hint", file, lines: [...imports, `imports: [${r.entry}]`] });
66
+ return false;
67
+ }
68
+ text = appendToArray(text, m.index + m[0].length - 1, [r.entry]);
69
+ for (const line of imports) text = addImport(text, line);
70
+ writeFileSync(file, text);
71
+ log.push({ kind: "updated", file, detail: `imports ${r.entry}` });
72
+ return true;
73
+ }
74
+ function envPublicNames(configFile) {
75
+ if (!existsSync(configFile)) return null;
76
+ const text = readFileSync(configFile, "utf8");
77
+ const m = text.match(/\bpublic\s*:\s*\[/);
78
+ if (!m) return null;
79
+ const open = m.index + m[0].length - 1;
80
+ const list = text.slice(open, closing(text, open) + 1);
81
+ return [...list.matchAll(/['"]([A-Za-z_][A-Za-z0-9_]*)['"]/g)].map((x) => x[1]);
82
+ }
83
+ function addEnvPublic(log, configFile, names) {
84
+ const list = (indent) => `${indent}public: [
85
+ ${names.map((n) => `${indent} '${n}',
86
+ `).join("")}${indent}],
87
+ `;
88
+ const have = envPublicNames(configFile);
89
+ if (have === null) {
90
+ const text2 = existsSync(configFile) ? readFileSync(configFile, "utf8") : "";
91
+ const call = text2.match(/defineConfig\(\{\n/);
92
+ if (call && !/\benv\s*:/.test(text2)) {
93
+ const at = call.index + call[0].length;
94
+ writeFileSync(configFile, text2.slice(0, at) + ` env: {
95
+ ${list(" ")} },
96
+ ` + text2.slice(at));
97
+ log.push({ kind: "updated", file: configFile, detail: `env.public ${names.join(", ")}` });
98
+ return;
99
+ }
100
+ log.push({ kind: "hint", file: configFile, lines: [`env: { public: [${names.map((n) => `'${n}'`).join(", ")}] },`] });
101
+ return;
102
+ }
103
+ const missing = names.filter((n) => !have.includes(n));
104
+ if (!missing.length) return;
105
+ const text = readFileSync(configFile, "utf8");
106
+ const m = text.match(/\bpublic\s*:\s*\[/);
107
+ writeFileSync(configFile, appendToArray(text, m.index + m[0].length - 1, missing.map((n) => `'${n}'`)));
108
+ log.push({ kind: "updated", file: configFile, detail: `env.public + ${missing.join(", ")}` });
109
+ }
110
+ function addEnvExample(log, file, comment, vars) {
111
+ const text = existsSync(file) ? readFileSync(file, "utf8") : "";
112
+ const have = new Set([...text.matchAll(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/gm)].map((m) => m[1]));
113
+ const add = Object.entries(vars).filter(([k]) => !have.has(k) && !have.has(`VITE_${k}`));
114
+ if (!add.length) return;
115
+ const block = `${comment.split("\n").map((l) => `# ${l}`).join("\n")}
116
+ ${add.map(([k, v]) => `${k}=${v}`).join("\n")}
117
+ `;
118
+ writeFileSync(file, text ? `${text.replace(/\n*$/, "\n")}
119
+ ${block}` : block);
120
+ log.push({ kind: text ? "updated" : "created", file, detail: add.map(([k]) => k).join(", ") });
121
+ }
122
+
123
+ // src/scaffold/templates.ts
124
+ var PROVIDERS = ["oidc", "keycloak", "auth0", "okta", "google", "dex", "oauth2"];
125
+ var SERVER_ONLY = {
126
+ google: "Google answers a token request without a client secret with `client_secret is missing`",
127
+ dex: "Dex sends no CORS headers on discovery or the token endpoint"
128
+ };
129
+ var PROVIDER_VARS = {
130
+ oidc: { AUTH_ISSUER: "https://id.example.com" },
131
+ keycloak: { AUTH_BASE_URL: "http://localhost:8080", AUTH_REALM: "master" },
132
+ auth0: { AUTH_DOMAIN: "example.eu.auth0.com", AUTH_AUDIENCE: "https://api.example.com" },
133
+ okta: { AUTH_DOMAIN: "example.okta.com" },
134
+ google: {},
135
+ dex: { AUTH_BASE_URL: "http://localhost:5556/dex" },
136
+ oauth2: {
137
+ AUTH_ISSUER: "https://id.example.com",
138
+ AUTH_AUTHORIZATION_ENDPOINT: "https://id.example.com/authorize",
139
+ AUTH_TOKEN_ENDPOINT: "https://id.example.com/token"
140
+ }
141
+ };
142
+ var AUTH_READER = `// Provider, issuer, client and token mode from the AUTH_* variables in env.public. Moving to
143
+ // another provider is a change to .env.
144
+ export const auth = authFromEnv(publicEnv(), { clientPrefix: '' });
145
+ `;
146
+ var authEnvConfig = () => `import { publicEnv } from '@fluixi/start/env';
147
+ import { authFromEnv } from '@fluixi/oauth2/env';
148
+
149
+ ${AUTH_READER}`;
150
+ var authServiceFile = (tokens) => `import { createAuthService, authModule } from '@fluixi/oauth2';
151
+ import { env } from '@fluixi/start/env';
152
+ ${tokens === "browser" ? `import { auth as authEnv } from '../config/env.js';
153
+ ` : ""}
154
+ export interface User {
155
+ sub: string;
156
+ email?: string;
157
+ name?: string;
158
+ }
159
+
160
+ const appUrl = env('APP_URL') ?? 'http://localhost:3000';
161
+
162
+ export const auth = createAuthService<User>({
163
+ ${tokens === "browser" ? ` config: {
164
+ ...authEnv.client,
165
+ tokens: 'browser',
166
+ redirectUri: \`\${appUrl}/callback\`,
167
+ postLogoutRedirectUri: \`\${appUrl}/login\`,
168
+ },` : ` // The tokens stay on the server behind an httpOnly cookie. The browser calls the routes
169
+ // auth.server.ts mounts under basePath and never sees a token.
170
+ config: {
171
+ tokens: 'server',
172
+ redirectUri: \`\${appUrl}/auth/callback\`,
173
+ postLogoutRedirectUri: \`\${appUrl}/login\`,
174
+ basePath: '/auth',
175
+ },`}
176
+ });
177
+
178
+ export const AuthModule = authModule(auth);
179
+ export const { protectedRoute, Protect, Can, useUser } = auth;
180
+ `;
181
+ var authServerFile = () => `import { authServerModule } from '@fluixi/oauth2/server';
182
+ import { publicEnv, serverEnv } from '@fluixi/start/env';
183
+ import { auth } from '../config/env.js';
184
+
185
+ // A function: serverEnv with required throws when a secret is unset, and it is only read
186
+ // when app.config.server.ts registers this.
187
+ export function authServer() {
188
+ const appUrl = publicEnv().APP_URL ?? 'http://localhost:3000';
189
+ return authServerModule({
190
+ ...auth.client,
191
+ clientSecret: serverEnv('OIDC_CLIENT_SECRET', { required: true }),
192
+ cookieSecret: serverEnv('SESSION_SECRET', { required: true }),
193
+ redirectUri: \`\${appUrl}/auth/callback\`,
194
+ defaultReturnTo: '/',
195
+ secureCookie: appUrl.startsWith('https://'),
196
+ });
197
+ }
198
+ `;
199
+ var callbackRoute = (html) => html ? `import { html } from '@fluixi/core';
200
+ import { OAuthCallback } from '@fluixi/oauth2/callback';
201
+
202
+ export default OAuthCallback({
203
+ pending: () => html\`<p>Signing you in</p>\`,
204
+ failed: (reason) => html\`<p>\${reason}</p><a href="/login">Try again</a>\`,
205
+ });
206
+ ` : `import { OAuthCallback } from '@fluixi/oauth2/callback';
207
+
208
+ export default OAuthCallback({
209
+ pending: () => <p>Signing you in</p>,
210
+ failed: (reason) => (
211
+ <>
212
+ <p>{reason}</p>
213
+ <a href="/login">Try again</a>
214
+ </>
215
+ ),
216
+ });
217
+ `;
218
+ var loginRoute = (html) => html ? `import { html } from '@fluixi/core';
219
+ import { auth } from '../services/auth.js';
220
+
221
+ export default function Login() {
222
+ return html\`<button @click=\${() => auth.signIn({ returnTo: '/' })}>Sign in</button>\`;
223
+ }
224
+ ` : `import { auth } from '../services/auth.js';
225
+
226
+ export default function Login() {
227
+ return <button onClick={() => auth.signIn({ returnTo: '/' })}>Sign in</button>;
228
+ }
229
+ `;
230
+ var betterAuthConfig = () => `import { betterAuth } from 'better-auth';
231
+ import { defineAuth } from '@fluixi/auth/server';
232
+
233
+ export type AppUser = { id: string; email: string; name?: string; role?: string };
234
+
235
+ // Better Auth owns the audited primitives; defineAuth registers it with @fluixi/auth.
236
+ export const auth = defineAuth<AppUser>(
237
+ betterAuth({
238
+ // database: <your adapter>,
239
+ emailAndPassword: { enabled: true },
240
+ // socialProviders: { google: { … }, github: { … } },
241
+ } as Parameters<typeof betterAuth>[0]),
242
+ );
243
+ `;
244
+ var betterAuthClient = () => `import { createAuthClient } from 'better-auth/client';
245
+ import { createAuthHooks } from '@fluixi/auth/client';
246
+ import type { AppUser } from './auth.config.js';
247
+
248
+ const client = createAuthClient({ baseURL: '/api/auth' });
249
+
250
+ // Or, to reuse an existing reactive session: createAuthHooks({ session: yourSessionStore }).
251
+ export const { useUser, useSession, useAuth, useSignIn, useSignOut, protectedRoute } =
252
+ createAuthHooks<AppUser>({ client, loginPath: '/login' });
253
+ `;
254
+ var betterAuthMiddleware = () => `import { defineMiddleware } from '@fluixi/start';
255
+ import { authMiddleware } from '@fluixi/auth/server';
256
+ import './auth.config.js';
257
+
258
+ export default defineMiddleware([authMiddleware()]);
259
+ `;
260
+ var betterAuthRoute = () => `import { mountAuthRoutes } from '@fluixi/auth/server';
261
+ import '../../auth.config.js';
262
+
263
+ const handler = mountAuthRoutes();
264
+ export const GET = handler;
265
+ export const POST = handler;
266
+ export default handler;
267
+ `;
268
+
269
+ // src/scaffold/auth.ts
270
+ var ScaffoldError = class extends Error {
271
+ };
272
+ function projectFormat(root) {
273
+ try {
274
+ const ts = JSON.parse(readFileSync2(join(root, "tsconfig.json"), "utf8"));
275
+ if (ts?.compilerOptions && !ts.compilerOptions.jsx) return "html";
276
+ } catch {
277
+ }
278
+ return "jsx";
279
+ }
280
+ function importPath(from, to) {
281
+ const p = relative(dirname2(from), to).replace(/\\/g, "/").replace(/\.tsx?$/, ".js");
282
+ return p.startsWith(".") ? p : `./${p}`;
283
+ }
284
+ function writeOnce(log, file, content) {
285
+ if (existsSync2(file)) {
286
+ log.push({ kind: "kept", file });
287
+ return false;
288
+ }
289
+ mkdirSync2(dirname2(file), { recursive: true });
290
+ writeFileSync2(file, content);
291
+ log.push({ kind: "created", file });
292
+ return true;
293
+ }
294
+ var APP_HEADER = "// Registered by @fluixi/start ahead of both entries.\n";
295
+ var APP_SERVER_HEADER = "// Only the server build imports this file, so what it lists never reaches the browser.\n";
296
+ function registerModule(log, root, server, file, name, entry = name) {
297
+ const config = join(root, server ? "src/app.config.server.ts" : "src/app.config.ts");
298
+ register(log, config, server ? APP_SERVER_HEADER : APP_HEADER, {
299
+ importLine: `import { ${name} } from '${importPath(config, file)}';`,
300
+ entry
301
+ });
302
+ }
303
+ function ensureAuthEnv(log, root) {
304
+ const file = join(root, "src/config/env.ts");
305
+ if (writeOnce(log, file, authEnvConfig())) return;
306
+ let text = readFileSync2(file, "utf8");
307
+ if (/export\s+const\s+auth\b/.test(text)) return;
308
+ if (!/\bpublicEnv\b/.test(text)) text = addImport(text, `import { publicEnv } from '@fluixi/start/env';`);
309
+ text = addImport(text, `import { authFromEnv } from '@fluixi/oauth2/env';`);
310
+ writeFileSync2(file, `${text.replace(/\n*$/, "\n")}
311
+ ${AUTH_READER}`);
312
+ log.push({ kind: "updated", file, detail: "auth" });
313
+ }
314
+ function resolveTokens(provider, tokens) {
315
+ const refusal = SERVER_ONLY[provider];
316
+ if (refusal && tokens === "browser") throw new ScaffoldError(`${provider} needs server tokens: ${refusal}`);
317
+ return { tokens: tokens ?? (refusal ? "server" : "browser"), forced: !!refusal && !tokens };
318
+ }
319
+ function setupOAuth2(root, options, dependencySpec = "alpha") {
320
+ const provider = options.provider ?? "oidc";
321
+ if (!PROVIDERS.includes(provider)) throw new ScaffoldError(`unknown provider ${provider}: ${PROVIDERS.join(", ")}`);
322
+ const { tokens, forced } = resolveTokens(provider, options.tokens);
323
+ const html = options.format === "html";
324
+ const ext = html ? "ts" : "tsx";
325
+ const steps = [];
326
+ const at = (rel) => join(root, rel);
327
+ ensureAuthEnv(steps, root);
328
+ const service = at("src/services/auth.ts");
329
+ if (writeOnce(steps, service, authServiceFile(tokens))) registerModule(steps, root, false, service, "AuthModule");
330
+ if (tokens === "browser") writeOnce(steps, at(`src/routes/callback.${ext}`), callbackRoute(html));
331
+ writeOnce(steps, at(`src/routes/login.${ext}`), loginRoute(html));
332
+ if (tokens === "server") {
333
+ const server = at("src/services/auth.server.ts");
334
+ if (writeOnce(steps, server, authServerFile())) registerModule(steps, root, true, server, "authServer", "authServer()");
335
+ }
336
+ const vars = {
337
+ AUTH_PROVIDER: provider,
338
+ AUTH_CLIENT_ID: "my-app",
339
+ AUTH_TOKENS: tokens,
340
+ ...PROVIDER_VARS[provider],
341
+ APP_URL: "http://localhost:3000"
342
+ };
343
+ addEnvPublic(steps, at("fluixi.config.ts"), Object.keys(vars));
344
+ addEnvExample(steps, at(".env.example"), `@fluixi/oauth2 (${provider}). Listed in env.public: the browser sees them.`, vars);
345
+ if (tokens === "server") {
346
+ addEnvExample(steps, at(".env.example"), "Server only, never in env.public. SESSION_SECRET: 32 bytes or more.", {
347
+ OIDC_CLIENT_SECRET: "",
348
+ SESSION_SECRET: ""
349
+ });
350
+ }
351
+ const redirect = tokens === "browser" ? "/callback" : "/auth/callback";
352
+ return {
353
+ steps,
354
+ dependencies: { "@fluixi/oauth2": dependencySpec },
355
+ next: ["copy .env.example to .env and fill it in", `register <APP_URL>${redirect} as the redirect URI at the provider`],
356
+ notes: forced ? [`${provider} refuses a public client, so the tokens stay on the server`] : []
357
+ };
358
+ }
359
+ function setupBetterAuth(root, dependencySpec = "alpha") {
360
+ const steps = [];
361
+ const at = (rel) => join(root, rel);
362
+ writeOnce(steps, at("src/auth.config.ts"), betterAuthConfig());
363
+ writeOnce(steps, at("src/auth.client.ts"), betterAuthClient());
364
+ if (!writeOnce(steps, at("src/middleware.ts"), betterAuthMiddleware())) {
365
+ steps.push({
366
+ kind: "hint",
367
+ file: at("src/middleware.ts"),
368
+ lines: [`import { authMiddleware } from '@fluixi/auth/server';`, `import './auth.config.js';`, "authMiddleware() in the middleware list"]
369
+ });
370
+ }
371
+ writeOnce(steps, at("src/api/auth/[...all].ts"), betterAuthRoute());
372
+ return {
373
+ steps,
374
+ dependencies: { "@fluixi/auth": dependencySpec, "better-auth": "^1" },
375
+ next: ["set your database adapter in src/auth.config.ts"],
376
+ notes: []
377
+ };
378
+ }
379
+ export {
380
+ ScaffoldError,
381
+ ensureAuthEnv,
382
+ importPath,
383
+ projectFormat,
384
+ registerModule,
385
+ resolveTokens,
386
+ setupBetterAuth,
387
+ setupOAuth2,
388
+ writeOnce
389
+ };
@@ -0,0 +1,153 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/scaffold/edit.ts
21
+ var edit_exports = {};
22
+ __export(edit_exports, {
23
+ addEnvExample: () => addEnvExample,
24
+ addEnvPublic: () => addEnvPublic,
25
+ addImport: () => addImport,
26
+ envPublicNames: () => envPublicNames,
27
+ register: () => register
28
+ });
29
+ module.exports = __toCommonJS(edit_exports);
30
+ var import_node_fs = require("node:fs");
31
+ var import_node_path = require("node:path");
32
+ function closing(text, open) {
33
+ let depth = 0;
34
+ for (let i = open; i < text.length; i++) {
35
+ const ch = text[i];
36
+ if (ch === "/" && text[i + 1] === "/") i = text.indexOf("\n", i) === -1 ? text.length : text.indexOf("\n", i);
37
+ else if (ch === "/" && text[i + 1] === "*") i = text.indexOf("*/", i + 2) + 1;
38
+ else if (ch === "'" || ch === '"' || ch === "`") {
39
+ for (i++; i < text.length && text[i] !== ch; i++) if (text[i] === "\\") i++;
40
+ } else if ("([{".includes(ch)) depth++;
41
+ else if (")]}".includes(ch) && --depth === 0) return i;
42
+ }
43
+ return -1;
44
+ }
45
+ function appendToArray(text, open, items) {
46
+ const close = closing(text, open);
47
+ const inner = text.slice(open + 1, close);
48
+ if (!inner.trim()) return text.slice(0, open + 1) + items.join(", ") + text.slice(close);
49
+ if (inner.includes("\n")) {
50
+ const last = inner.trimEnd();
51
+ const indent = last.slice(last.lastIndexOf("\n") + 1).match(/^\s*/)[0];
52
+ const comma = last.endsWith(",") ? "" : ",";
53
+ const tail = inner.slice(last.length);
54
+ return text.slice(0, open + 1) + last + comma + items.map((i) => `
55
+ ${indent}${i},`).join("") + tail + text.slice(close);
56
+ }
57
+ const body = inner.replace(/,?\s*$/, "");
58
+ const trailing = inner.slice(body.length).replace(/^,/, "");
59
+ return text.slice(0, open + 1) + body + ", " + items.join(", ") + trailing + text.slice(close);
60
+ }
61
+ function addImport(text, line) {
62
+ if (text.includes(line)) return text;
63
+ const imports = [...text.matchAll(/^import[\s\S]*?from\s+['"][^'"]+['"];?[^\n]*\n/gm)];
64
+ const at = imports.length ? imports[imports.length - 1].index + imports[imports.length - 1][0].length : 0;
65
+ return text.slice(0, at) + line + "\n" + text.slice(at);
66
+ }
67
+ function register(log, file, header, r) {
68
+ const imports = [...r.extraImports ?? [], r.importLine];
69
+ if (!(0, import_node_fs.existsSync)(file)) {
70
+ (0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(file), { recursive: true });
71
+ (0, import_node_fs.writeFileSync)(
72
+ file,
73
+ `${header}import { defineApp } from '@fluixi/start/app';
74
+ ${imports.join("\n")}
75
+
76
+ export default defineApp({ imports: [${r.entry}] });
77
+ `
78
+ );
79
+ log.push({ kind: "created", file });
80
+ return true;
81
+ }
82
+ let text = (0, import_node_fs.readFileSync)(file, "utf8");
83
+ if (text.includes(r.importLine)) {
84
+ log.push({ kind: "listed", file, detail: r.entry });
85
+ return true;
86
+ }
87
+ const m = text.match(/\bimports\s*:\s*\[/);
88
+ if (!m) {
89
+ log.push({ kind: "hint", file, lines: [...imports, `imports: [${r.entry}]`] });
90
+ return false;
91
+ }
92
+ text = appendToArray(text, m.index + m[0].length - 1, [r.entry]);
93
+ for (const line of imports) text = addImport(text, line);
94
+ (0, import_node_fs.writeFileSync)(file, text);
95
+ log.push({ kind: "updated", file, detail: `imports ${r.entry}` });
96
+ return true;
97
+ }
98
+ function envPublicNames(configFile) {
99
+ if (!(0, import_node_fs.existsSync)(configFile)) return null;
100
+ const text = (0, import_node_fs.readFileSync)(configFile, "utf8");
101
+ const m = text.match(/\bpublic\s*:\s*\[/);
102
+ if (!m) return null;
103
+ const open = m.index + m[0].length - 1;
104
+ const list = text.slice(open, closing(text, open) + 1);
105
+ return [...list.matchAll(/['"]([A-Za-z_][A-Za-z0-9_]*)['"]/g)].map((x) => x[1]);
106
+ }
107
+ function addEnvPublic(log, configFile, names) {
108
+ const list = (indent) => `${indent}public: [
109
+ ${names.map((n) => `${indent} '${n}',
110
+ `).join("")}${indent}],
111
+ `;
112
+ const have = envPublicNames(configFile);
113
+ if (have === null) {
114
+ const text2 = (0, import_node_fs.existsSync)(configFile) ? (0, import_node_fs.readFileSync)(configFile, "utf8") : "";
115
+ const call = text2.match(/defineConfig\(\{\n/);
116
+ if (call && !/\benv\s*:/.test(text2)) {
117
+ const at = call.index + call[0].length;
118
+ (0, import_node_fs.writeFileSync)(configFile, text2.slice(0, at) + ` env: {
119
+ ${list(" ")} },
120
+ ` + text2.slice(at));
121
+ log.push({ kind: "updated", file: configFile, detail: `env.public ${names.join(", ")}` });
122
+ return;
123
+ }
124
+ log.push({ kind: "hint", file: configFile, lines: [`env: { public: [${names.map((n) => `'${n}'`).join(", ")}] },`] });
125
+ return;
126
+ }
127
+ const missing = names.filter((n) => !have.includes(n));
128
+ if (!missing.length) return;
129
+ const text = (0, import_node_fs.readFileSync)(configFile, "utf8");
130
+ const m = text.match(/\bpublic\s*:\s*\[/);
131
+ (0, import_node_fs.writeFileSync)(configFile, appendToArray(text, m.index + m[0].length - 1, missing.map((n) => `'${n}'`)));
132
+ log.push({ kind: "updated", file: configFile, detail: `env.public + ${missing.join(", ")}` });
133
+ }
134
+ function addEnvExample(log, file, comment, vars) {
135
+ const text = (0, import_node_fs.existsSync)(file) ? (0, import_node_fs.readFileSync)(file, "utf8") : "";
136
+ const have = new Set([...text.matchAll(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/gm)].map((m) => m[1]));
137
+ const add = Object.entries(vars).filter(([k]) => !have.has(k) && !have.has(`VITE_${k}`));
138
+ if (!add.length) return;
139
+ const block = `${comment.split("\n").map((l) => `# ${l}`).join("\n")}
140
+ ${add.map(([k, v]) => `${k}=${v}`).join("\n")}
141
+ `;
142
+ (0, import_node_fs.writeFileSync)(file, text ? `${text.replace(/\n*$/, "\n")}
143
+ ${block}` : block);
144
+ log.push({ kind: text ? "updated" : "created", file, detail: add.map(([k]) => k).join(", ") });
145
+ }
146
+ // Annotate the CommonJS export names for ESM import in node:
147
+ 0 && (module.exports = {
148
+ addEnvExample,
149
+ addEnvPublic,
150
+ addImport,
151
+ envPublicNames,
152
+ register
153
+ });