@fluixi/cli 0.1.0-alpha.82 → 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,216 @@
1
+ /** What the env, module, service, api, server-fn and oauth2 generators write. */
2
+ export const camel = (name) => name.toLowerCase().replace(/_+([a-z0-9])/g, (_, ch) => ch.toUpperCase());
3
+ export const envConfig = (names) => {
4
+ const fields = names.length
5
+ ? names.map((n) => ` ${camel(n)}: '${n}',`).join('\n')
6
+ : ` // apiUrl: 'API_URL',`;
7
+ return `import { pickEnv } from '@fluixi/start/env';
8
+
9
+ // Every missing variable is reported at once, when this module loads. A field that may be
10
+ // unset takes { key, optional: true } or { key, default }; parse: asNumber and friends convert.
11
+ export const config = pickEnv({
12
+ ${fields}
13
+ });
14
+ `;
15
+ };
16
+ export const moduleFile = (Name, name) => `import { defineModule } from '@fluixi/start/app';
17
+
18
+ export const ${Name}Module = defineModule({
19
+ name: '${name}',
20
+ imports: [],
21
+ providers: [],
22
+ });
23
+ `;
24
+ export const serviceFile = (Name, name, server) => `${server ? `// Server only: the browser build refuses a page that imports this file.\n` : ''}import { injectionToken } from '@fluixi/start/di';
25
+ import { defineModule } from '@fluixi/start/app';
26
+
27
+ export class ${Name}Service {}
28
+
29
+ export const ${Name}Token = injectionToken<${Name}Service>('${Name}');
30
+
31
+ export const ${Name}Module = defineModule({
32
+ name: '${name}',
33
+ providers: [{ provide: ${Name}Token, useFactory: () => new ${Name}Service() }],
34
+ });
35
+ `;
36
+ export const apiRoute = () => `export function GET(request: Request) {
37
+ return Response.json({ path: new URL(request.url).pathname });
38
+ }
39
+ `;
40
+ export const guardedApiRoute = (guardImport, role) => `import { guard } from '${guardImport}';
41
+
42
+ export const GET = guard(
43
+ async (request, claims) => {
44
+ return Response.json({ sub: claims.sub });
45
+ },
46
+ { role: '${role}' },
47
+ );
48
+ `;
49
+ export const verifyServer = () => `import { createTokenVerifier, createApiGuard } from '@fluixi/oauth2/verify';
50
+ import { auth } from '../config/env.js';
51
+
52
+ // Checks a bearer token's signature, issuer, audience and expiry against the provider.
53
+ export const verify = createTokenVerifier(auth.provider.verifier(auth.clientId));
54
+
55
+ // Answers 401 without a valid token and 403 without the role.
56
+ export const guard = createApiGuard({ verify, roles: auth.provider.roles });
57
+ `;
58
+ export const serverFn = (name) => `export async function ${name}(input: { id: string }) {
59
+ 'use server';
60
+ // Runs on the server only. The browser calls it over RPC, so input and result are plain data.
61
+ return { id: input.id };
62
+ }
63
+ `;
64
+ // ── oauth2 ──────────────────────────────────────────────────────────────────
65
+ export const PROVIDERS = ['oidc', 'keycloak', 'auth0', 'okta', 'google', 'dex', 'oauth2'];
66
+ /** Providers that refuse a public client, so tokens must stay on the server. */
67
+ export const SERVER_ONLY = {
68
+ google: 'Google answers a token request without a client secret with `client_secret is missing`',
69
+ dex: 'Dex sends no CORS headers on discovery or the token endpoint',
70
+ };
71
+ /** The variables each preset needs beyond the common ones, with a placeholder. */
72
+ export const PROVIDER_VARS = {
73
+ oidc: { AUTH_ISSUER: 'https://id.example.com' },
74
+ keycloak: { AUTH_BASE_URL: 'http://localhost:8080', AUTH_REALM: 'master' },
75
+ auth0: { AUTH_DOMAIN: 'example.eu.auth0.com', AUTH_AUDIENCE: 'https://api.example.com' },
76
+ okta: { AUTH_DOMAIN: 'example.okta.com' },
77
+ google: {},
78
+ dex: { AUTH_BASE_URL: 'http://localhost:5556/dex' },
79
+ oauth2: {
80
+ AUTH_ISSUER: 'https://id.example.com',
81
+ AUTH_AUTHORIZATION_ENDPOINT: 'https://id.example.com/authorize',
82
+ AUTH_TOKEN_ENDPOINT: 'https://id.example.com/token',
83
+ },
84
+ };
85
+ export const AUTH_READER = `// Provider, issuer, client and token mode from the AUTH_* variables in env.public. Moving to
86
+ // another provider is a change to .env.
87
+ export const auth = authFromEnv(publicEnv(), { clientPrefix: '' });
88
+ `;
89
+ export const authEnvConfig = () => `import { publicEnv } from '@fluixi/start/env';
90
+ import { authFromEnv } from '@fluixi/oauth2/env';
91
+
92
+ ${AUTH_READER}`;
93
+ export const authServiceFile = (tokens) => `import { createAuthService, authModule } from '@fluixi/oauth2';
94
+ import { env } from '@fluixi/start/env';
95
+ ${tokens === 'browser' ? `import { auth as authEnv } from '../config/env.js';\n` : ''}
96
+ export interface User {
97
+ sub: string;
98
+ email?: string;
99
+ name?: string;
100
+ }
101
+
102
+ const appUrl = env('APP_URL') ?? 'http://localhost:3000';
103
+
104
+ export const auth = createAuthService<User>({
105
+ ${tokens === 'browser'
106
+ ? ` config: {
107
+ ...authEnv.client,
108
+ tokens: 'browser',
109
+ redirectUri: \`\${appUrl}/callback\`,
110
+ postLogoutRedirectUri: \`\${appUrl}/login\`,
111
+ },`
112
+ : ` // The tokens stay on the server behind an httpOnly cookie. The browser calls the routes
113
+ // auth.server.ts mounts under basePath and never sees a token.
114
+ config: {
115
+ tokens: 'server',
116
+ redirectUri: \`\${appUrl}/auth/callback\`,
117
+ postLogoutRedirectUri: \`\${appUrl}/login\`,
118
+ basePath: '/auth',
119
+ },`}
120
+ });
121
+
122
+ export const AuthModule = authModule(auth);
123
+ export const { protectedRoute, Protect, Can, useUser } = auth;
124
+ `;
125
+ export const authServerFile = () => `import { authServerModule } from '@fluixi/oauth2/server';
126
+ import { publicEnv, serverEnv } from '@fluixi/start/env';
127
+ import { auth } from '../config/env.js';
128
+
129
+ // A function: serverEnv with required throws when a secret is unset, and it is only read
130
+ // when app.config.server.ts registers this.
131
+ export function authServer() {
132
+ const appUrl = publicEnv().APP_URL ?? 'http://localhost:3000';
133
+ return authServerModule({
134
+ ...auth.client,
135
+ clientSecret: serverEnv('OIDC_CLIENT_SECRET', { required: true }),
136
+ cookieSecret: serverEnv('SESSION_SECRET', { required: true }),
137
+ redirectUri: \`\${appUrl}/auth/callback\`,
138
+ defaultReturnTo: '/',
139
+ secureCookie: appUrl.startsWith('https://'),
140
+ });
141
+ }
142
+ `;
143
+ export const callbackRoute = (html) => html
144
+ ? `import { html } from '@fluixi/core';
145
+ import { OAuthCallback } from '@fluixi/oauth2/callback';
146
+
147
+ export default OAuthCallback({
148
+ pending: () => html\`<p>Signing you in</p>\`,
149
+ failed: (reason) => html\`<p>\${reason}</p><a href="/login">Try again</a>\`,
150
+ });
151
+ `
152
+ : `import { OAuthCallback } from '@fluixi/oauth2/callback';
153
+
154
+ export default OAuthCallback({
155
+ pending: () => <p>Signing you in</p>,
156
+ failed: (reason) => (
157
+ <>
158
+ <p>{reason}</p>
159
+ <a href="/login">Try again</a>
160
+ </>
161
+ ),
162
+ });
163
+ `;
164
+ export const loginRoute = (html) => html
165
+ ? `import { html } from '@fluixi/core';
166
+ import { auth } from '../services/auth.js';
167
+
168
+ export default function Login() {
169
+ return html\`<button @click=\${() => auth.signIn({ returnTo: '/' })}>Sign in</button>\`;
170
+ }
171
+ `
172
+ : `import { auth } from '../services/auth.js';
173
+
174
+ export default function Login() {
175
+ return <button onClick={() => auth.signIn({ returnTo: '/' })}>Sign in</button>;
176
+ }
177
+ `;
178
+ // ── better-auth ─────────────────────────────────────────────────────────────
179
+ export const betterAuthConfig = () => `import { betterAuth } from 'better-auth';
180
+ import { defineAuth } from '@fluixi/auth/server';
181
+
182
+ export type AppUser = { id: string; email: string; name?: string; role?: string };
183
+
184
+ // Better Auth owns the audited primitives; defineAuth registers it with @fluixi/auth.
185
+ export const auth = defineAuth<AppUser>(
186
+ betterAuth({
187
+ // database: <your adapter>,
188
+ emailAndPassword: { enabled: true },
189
+ // socialProviders: { google: { … }, github: { … } },
190
+ } as Parameters<typeof betterAuth>[0]),
191
+ );
192
+ `;
193
+ export const betterAuthClient = () => `import { createAuthClient } from 'better-auth/client';
194
+ import { createAuthHooks } from '@fluixi/auth/client';
195
+ import type { AppUser } from './auth.config.js';
196
+
197
+ const client = createAuthClient({ baseURL: '/api/auth' });
198
+
199
+ // Or, to reuse an existing reactive session: createAuthHooks({ session: yourSessionStore }).
200
+ export const { useUser, useSession, useAuth, useSignIn, useSignOut, protectedRoute } =
201
+ createAuthHooks<AppUser>({ client, loginPath: '/login' });
202
+ `;
203
+ export const betterAuthMiddleware = () => `import { defineMiddleware } from '@fluixi/start';
204
+ import { authMiddleware } from '@fluixi/auth/server';
205
+ import './auth.config.js';
206
+
207
+ export default defineMiddleware([authMiddleware()]);
208
+ `;
209
+ export const betterAuthRoute = () => `import { mountAuthRoutes } from '@fluixi/auth/server';
210
+ import '../../auth.config.js';
211
+
212
+ const handler = mountAuthRoutes();
213
+ export const GET = handler;
214
+ export const POST = handler;
215
+ export default handler;
216
+ `;
@@ -0,0 +1,229 @@
1
+ // src/scaffold/templates.ts
2
+ var camel = (name) => name.toLowerCase().replace(/_+([a-z0-9])/g, (_, ch) => ch.toUpperCase());
3
+ var envConfig = (names) => {
4
+ const fields = names.length ? names.map((n) => ` ${camel(n)}: '${n}',`).join("\n") : ` // apiUrl: 'API_URL',`;
5
+ return `import { pickEnv } from '@fluixi/start/env';
6
+
7
+ // Every missing variable is reported at once, when this module loads. A field that may be
8
+ // unset takes { key, optional: true } or { key, default }; parse: asNumber and friends convert.
9
+ export const config = pickEnv({
10
+ ${fields}
11
+ });
12
+ `;
13
+ };
14
+ var moduleFile = (Name, name) => `import { defineModule } from '@fluixi/start/app';
15
+
16
+ export const ${Name}Module = defineModule({
17
+ name: '${name}',
18
+ imports: [],
19
+ providers: [],
20
+ });
21
+ `;
22
+ var serviceFile = (Name, name, server) => `${server ? `// Server only: the browser build refuses a page that imports this file.
23
+ ` : ""}import { injectionToken } from '@fluixi/start/di';
24
+ import { defineModule } from '@fluixi/start/app';
25
+
26
+ export class ${Name}Service {}
27
+
28
+ export const ${Name}Token = injectionToken<${Name}Service>('${Name}');
29
+
30
+ export const ${Name}Module = defineModule({
31
+ name: '${name}',
32
+ providers: [{ provide: ${Name}Token, useFactory: () => new ${Name}Service() }],
33
+ });
34
+ `;
35
+ var apiRoute = () => `export function GET(request: Request) {
36
+ return Response.json({ path: new URL(request.url).pathname });
37
+ }
38
+ `;
39
+ var guardedApiRoute = (guardImport, role) => `import { guard } from '${guardImport}';
40
+
41
+ export const GET = guard(
42
+ async (request, claims) => {
43
+ return Response.json({ sub: claims.sub });
44
+ },
45
+ { role: '${role}' },
46
+ );
47
+ `;
48
+ var verifyServer = () => `import { createTokenVerifier, createApiGuard } from '@fluixi/oauth2/verify';
49
+ import { auth } from '../config/env.js';
50
+
51
+ // Checks a bearer token's signature, issuer, audience and expiry against the provider.
52
+ export const verify = createTokenVerifier(auth.provider.verifier(auth.clientId));
53
+
54
+ // Answers 401 without a valid token and 403 without the role.
55
+ export const guard = createApiGuard({ verify, roles: auth.provider.roles });
56
+ `;
57
+ var serverFn = (name) => `export async function ${name}(input: { id: string }) {
58
+ 'use server';
59
+ // Runs on the server only. The browser calls it over RPC, so input and result are plain data.
60
+ return { id: input.id };
61
+ }
62
+ `;
63
+ var PROVIDERS = ["oidc", "keycloak", "auth0", "okta", "google", "dex", "oauth2"];
64
+ var SERVER_ONLY = {
65
+ google: "Google answers a token request without a client secret with `client_secret is missing`",
66
+ dex: "Dex sends no CORS headers on discovery or the token endpoint"
67
+ };
68
+ var PROVIDER_VARS = {
69
+ oidc: { AUTH_ISSUER: "https://id.example.com" },
70
+ keycloak: { AUTH_BASE_URL: "http://localhost:8080", AUTH_REALM: "master" },
71
+ auth0: { AUTH_DOMAIN: "example.eu.auth0.com", AUTH_AUDIENCE: "https://api.example.com" },
72
+ okta: { AUTH_DOMAIN: "example.okta.com" },
73
+ google: {},
74
+ dex: { AUTH_BASE_URL: "http://localhost:5556/dex" },
75
+ oauth2: {
76
+ AUTH_ISSUER: "https://id.example.com",
77
+ AUTH_AUTHORIZATION_ENDPOINT: "https://id.example.com/authorize",
78
+ AUTH_TOKEN_ENDPOINT: "https://id.example.com/token"
79
+ }
80
+ };
81
+ var AUTH_READER = `// Provider, issuer, client and token mode from the AUTH_* variables in env.public. Moving to
82
+ // another provider is a change to .env.
83
+ export const auth = authFromEnv(publicEnv(), { clientPrefix: '' });
84
+ `;
85
+ var authEnvConfig = () => `import { publicEnv } from '@fluixi/start/env';
86
+ import { authFromEnv } from '@fluixi/oauth2/env';
87
+
88
+ ${AUTH_READER}`;
89
+ var authServiceFile = (tokens) => `import { createAuthService, authModule } from '@fluixi/oauth2';
90
+ import { env } from '@fluixi/start/env';
91
+ ${tokens === "browser" ? `import { auth as authEnv } from '../config/env.js';
92
+ ` : ""}
93
+ export interface User {
94
+ sub: string;
95
+ email?: string;
96
+ name?: string;
97
+ }
98
+
99
+ const appUrl = env('APP_URL') ?? 'http://localhost:3000';
100
+
101
+ export const auth = createAuthService<User>({
102
+ ${tokens === "browser" ? ` config: {
103
+ ...authEnv.client,
104
+ tokens: 'browser',
105
+ redirectUri: \`\${appUrl}/callback\`,
106
+ postLogoutRedirectUri: \`\${appUrl}/login\`,
107
+ },` : ` // The tokens stay on the server behind an httpOnly cookie. The browser calls the routes
108
+ // auth.server.ts mounts under basePath and never sees a token.
109
+ config: {
110
+ tokens: 'server',
111
+ redirectUri: \`\${appUrl}/auth/callback\`,
112
+ postLogoutRedirectUri: \`\${appUrl}/login\`,
113
+ basePath: '/auth',
114
+ },`}
115
+ });
116
+
117
+ export const AuthModule = authModule(auth);
118
+ export const { protectedRoute, Protect, Can, useUser } = auth;
119
+ `;
120
+ var authServerFile = () => `import { authServerModule } from '@fluixi/oauth2/server';
121
+ import { publicEnv, serverEnv } from '@fluixi/start/env';
122
+ import { auth } from '../config/env.js';
123
+
124
+ // A function: serverEnv with required throws when a secret is unset, and it is only read
125
+ // when app.config.server.ts registers this.
126
+ export function authServer() {
127
+ const appUrl = publicEnv().APP_URL ?? 'http://localhost:3000';
128
+ return authServerModule({
129
+ ...auth.client,
130
+ clientSecret: serverEnv('OIDC_CLIENT_SECRET', { required: true }),
131
+ cookieSecret: serverEnv('SESSION_SECRET', { required: true }),
132
+ redirectUri: \`\${appUrl}/auth/callback\`,
133
+ defaultReturnTo: '/',
134
+ secureCookie: appUrl.startsWith('https://'),
135
+ });
136
+ }
137
+ `;
138
+ var callbackRoute = (html) => html ? `import { html } from '@fluixi/core';
139
+ import { OAuthCallback } from '@fluixi/oauth2/callback';
140
+
141
+ export default OAuthCallback({
142
+ pending: () => html\`<p>Signing you in</p>\`,
143
+ failed: (reason) => html\`<p>\${reason}</p><a href="/login">Try again</a>\`,
144
+ });
145
+ ` : `import { OAuthCallback } from '@fluixi/oauth2/callback';
146
+
147
+ export default OAuthCallback({
148
+ pending: () => <p>Signing you in</p>,
149
+ failed: (reason) => (
150
+ <>
151
+ <p>{reason}</p>
152
+ <a href="/login">Try again</a>
153
+ </>
154
+ ),
155
+ });
156
+ `;
157
+ var loginRoute = (html) => html ? `import { html } from '@fluixi/core';
158
+ import { auth } from '../services/auth.js';
159
+
160
+ export default function Login() {
161
+ return html\`<button @click=\${() => auth.signIn({ returnTo: '/' })}>Sign in</button>\`;
162
+ }
163
+ ` : `import { auth } from '../services/auth.js';
164
+
165
+ export default function Login() {
166
+ return <button onClick={() => auth.signIn({ returnTo: '/' })}>Sign in</button>;
167
+ }
168
+ `;
169
+ var betterAuthConfig = () => `import { betterAuth } from 'better-auth';
170
+ import { defineAuth } from '@fluixi/auth/server';
171
+
172
+ export type AppUser = { id: string; email: string; name?: string; role?: string };
173
+
174
+ // Better Auth owns the audited primitives; defineAuth registers it with @fluixi/auth.
175
+ export const auth = defineAuth<AppUser>(
176
+ betterAuth({
177
+ // database: <your adapter>,
178
+ emailAndPassword: { enabled: true },
179
+ // socialProviders: { google: { … }, github: { … } },
180
+ } as Parameters<typeof betterAuth>[0]),
181
+ );
182
+ `;
183
+ var betterAuthClient = () => `import { createAuthClient } from 'better-auth/client';
184
+ import { createAuthHooks } from '@fluixi/auth/client';
185
+ import type { AppUser } from './auth.config.js';
186
+
187
+ const client = createAuthClient({ baseURL: '/api/auth' });
188
+
189
+ // Or, to reuse an existing reactive session: createAuthHooks({ session: yourSessionStore }).
190
+ export const { useUser, useSession, useAuth, useSignIn, useSignOut, protectedRoute } =
191
+ createAuthHooks<AppUser>({ client, loginPath: '/login' });
192
+ `;
193
+ var betterAuthMiddleware = () => `import { defineMiddleware } from '@fluixi/start';
194
+ import { authMiddleware } from '@fluixi/auth/server';
195
+ import './auth.config.js';
196
+
197
+ export default defineMiddleware([authMiddleware()]);
198
+ `;
199
+ var betterAuthRoute = () => `import { mountAuthRoutes } from '@fluixi/auth/server';
200
+ import '../../auth.config.js';
201
+
202
+ const handler = mountAuthRoutes();
203
+ export const GET = handler;
204
+ export const POST = handler;
205
+ export default handler;
206
+ `;
207
+ export {
208
+ AUTH_READER,
209
+ PROVIDERS,
210
+ PROVIDER_VARS,
211
+ SERVER_ONLY,
212
+ apiRoute,
213
+ authEnvConfig,
214
+ authServerFile,
215
+ authServiceFile,
216
+ betterAuthClient,
217
+ betterAuthConfig,
218
+ betterAuthMiddleware,
219
+ betterAuthRoute,
220
+ callbackRoute,
221
+ camel,
222
+ envConfig,
223
+ guardedApiRoute,
224
+ loginRoute,
225
+ moduleFile,
226
+ serverFn,
227
+ serviceFile,
228
+ verifyServer
229
+ };