@fluixi/cli 0.1.0-alpha.82 β 0.1.0-alpha.84
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/README.md +21 -3
- package/dist/add.cjs +590 -0
- package/dist/add.d.ts +10 -0
- package/dist/add.d.ts.map +1 -0
- package/dist/add.js +107 -0
- package/dist/add.mjs +562 -0
- package/dist/check.cjs +102 -0
- package/dist/check.d.ts +13 -0
- package/dist/check.d.ts.map +1 -0
- package/dist/check.js +88 -0
- package/dist/check.mjs +76 -0
- package/dist/cli.cjs +977 -78
- package/dist/cli.js +44 -2
- package/dist/cli.mjs +976 -77
- package/dist/doctor.cjs +119 -0
- package/dist/doctor.d.ts +13 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +114 -0
- package/dist/doctor.mjs +92 -0
- package/dist/generate.cjs +614 -65
- package/dist/generate.d.ts.map +1 -1
- package/dist/generate.js +118 -48
- package/dist/generate.mjs +619 -64
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/report.cjs +54 -0
- package/dist/report.d.ts +5 -0
- package/dist/report.d.ts.map +1 -0
- package/dist/report.js +31 -0
- package/dist/report.mjs +28 -0
- package/dist/scaffold/auth.cjs +422 -0
- package/dist/scaffold/auth.d.ts +39 -0
- package/dist/scaffold/auth.d.ts.map +1 -0
- package/dist/scaffold/auth.js +137 -0
- package/dist/scaffold/auth.mjs +389 -0
- package/dist/scaffold/edit.cjs +153 -0
- package/dist/scaffold/edit.d.ts +35 -0
- package/dist/scaffold/edit.d.ts.map +1 -0
- package/dist/scaffold/edit.js +140 -0
- package/dist/scaffold/edit.mjs +124 -0
- package/dist/scaffold/index.cjs +610 -0
- package/dist/scaffold/index.d.ts +10 -0
- package/dist/scaffold/index.d.ts.map +1 -0
- package/dist/scaffold/index.js +9 -0
- package/dist/scaffold/index.mjs +569 -0
- package/dist/scaffold/prompt.cjs +108 -0
- package/dist/scaffold/prompt.d.ts +9 -0
- package/dist/scaffold/prompt.d.ts.map +1 -0
- package/dist/scaffold/prompt.js +95 -0
- package/dist/scaffold/prompt.mjs +81 -0
- package/dist/scaffold/templates.cjs +274 -0
- package/dist/scaffold/templates.d.ts +26 -0
- package/dist/scaffold/templates.d.ts.map +1 -0
- package/dist/scaffold/templates.js +216 -0
- package/dist/scaffold/templates.mjs +229 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/package.json +19 -3
package/README.md
CHANGED
|
@@ -23,6 +23,10 @@ fluixi dev # SSR dev server (Vite middleware + HMR)
|
|
|
23
23
|
fluixi build # client + server bundles β dist/client, dist/server
|
|
24
24
|
fluixi start # production SSR server (after build)
|
|
25
25
|
fluixi generate <kind> <name> # scaffold into the current app (alias: g)
|
|
26
|
+
fluixi check [--strict] # env and auth rules over the project; exit 1 on an error
|
|
27
|
+
fluixi env # env.public set or not, .env.example drift, secrets that would ship
|
|
28
|
+
fluixi doctor # duplicate @fluixi/* copies, ts-plugin missing from tsconfig
|
|
29
|
+
fluixi info # versions, adapter, modules, pages, API routes
|
|
26
30
|
```
|
|
27
31
|
|
|
28
32
|
## π¦ Installation
|
|
@@ -38,11 +42,25 @@ fluixi g component Button # β src/components/Button.tsx
|
|
|
38
42
|
fluixi g route about # β src/routes/about.tsx (file route)
|
|
39
43
|
fluixi g route "blog/[id]" # β src/routes/blog/[id].tsx (dynamic segment)
|
|
40
44
|
fluixi g middleware # β src/middleware.ts
|
|
41
|
-
fluixi g
|
|
45
|
+
fluixi g env # β src/config/env.ts (pickEnv over env.public)
|
|
46
|
+
fluixi g module billing # β src/services/billing/billing.module.ts, listed in src/app.config.ts
|
|
47
|
+
fluixi g service Orders # β src/services/orders.ts (class, token, module)
|
|
48
|
+
fluixi g service Mailer --server # β src/services/mailer.server.ts, listed in src/app.config.server.ts
|
|
49
|
+
fluixi g api "users/[id]" # β src/api/users/[id].ts
|
|
50
|
+
fluixi g api admin/stats --role admin # β guarded route + src/services/verify.server.ts
|
|
51
|
+
fluixi g server-fn getUser # β src/server/getUser.ts ("use server")
|
|
52
|
+
fluixi g auth oauth2 --provider keycloak --tokens server
|
|
53
|
+
# β config/env.ts, services/auth.ts, login route, auth.server.ts,
|
|
54
|
+
# env.public and .env.example entries
|
|
55
|
+
fluixi g auth better-auth # β auth.config + auth.client + middleware + src/api/auth route
|
|
42
56
|
```
|
|
43
57
|
|
|
44
|
-
|
|
45
|
-
stops with an error
|
|
58
|
+
New files are written unless a file with the same name already exists, in which case the
|
|
59
|
+
process stops with an error. Edits are limited to `app.config.ts`, `app.config.server.ts`
|
|
60
|
+
(adding an import and entry), `env.public` in `fluixi.config.ts`, and appending to
|
|
61
|
+
`.env.example`; the original layout of each file is preserved, and when imports are not a
|
|
62
|
+
plain list the command prints the lines for manual pasting. Multi-file OAuth2 generation
|
|
63
|
+
keeps any existing files unchanged.
|
|
46
64
|
|
|
47
65
|
### JSX or html``
|
|
48
66
|
|
package/dist/add.cjs
ADDED
|
@@ -0,0 +1,590 @@
|
|
|
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/add.ts
|
|
21
|
+
var add_exports = {};
|
|
22
|
+
__export(add_exports, {
|
|
23
|
+
ADD_USAGE: () => ADD_USAGE,
|
|
24
|
+
add: () => add,
|
|
25
|
+
channelOf: () => channelOf,
|
|
26
|
+
installCommand: () => installCommand
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(add_exports);
|
|
29
|
+
var import_node_child_process = require("node:child_process");
|
|
30
|
+
var import_node_fs3 = require("node:fs");
|
|
31
|
+
var import_node_path4 = require("node:path");
|
|
32
|
+
var import_node_process2 = require("node:process");
|
|
33
|
+
var import_ui2 = require("@fluixi/start/ui");
|
|
34
|
+
|
|
35
|
+
// src/report.ts
|
|
36
|
+
var import_node_path = require("node:path");
|
|
37
|
+
var import_ui = require("@fluixi/start/ui");
|
|
38
|
+
var rel = (file) => (0, import_node_path.relative)(process.cwd(), file) || ".";
|
|
39
|
+
function report(steps) {
|
|
40
|
+
for (const s of steps) {
|
|
41
|
+
if (s.kind === "hint") {
|
|
42
|
+
console.log(` ${import_ui.c.yellow("!")} ${import_ui.c.gray("add to")} ${import_ui.c.cyan(rel(s.file))}${import_ui.c.gray(":")}`);
|
|
43
|
+
for (const l of s.lines) console.log(` ${l}`);
|
|
44
|
+
} else if (s.kind === "kept") {
|
|
45
|
+
console.log(` ${import_ui.c.gray("-")} ${import_ui.c.cyan(rel(s.file))} ${import_ui.c.gray("exists, kept")}`);
|
|
46
|
+
} else if (s.kind === "listed") {
|
|
47
|
+
console.log(` ${import_ui.c.gray("-")} ${import_ui.c.cyan(rel(s.file))} ${import_ui.c.gray("already lists it")}`);
|
|
48
|
+
} else {
|
|
49
|
+
console.log(` ${import_ui.c.green("β")} ${import_ui.c.gray(s.kind)} ${import_ui.c.cyan(rel(s.file))}${s.detail ? import_ui.c.gray(`: ${s.detail}`) : ""}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function reportAuth(result, installHint) {
|
|
54
|
+
for (const n of result.notes) console.log(` ${import_ui.c.gray(n)}`);
|
|
55
|
+
report(result.steps);
|
|
56
|
+
const next = [...installHint ? [installHint] : [], ...result.next];
|
|
57
|
+
if (next.length) console.log(` ${import_ui.c.gray("next:")} ${next.map((n) => import_ui.c.gray(n)).join(import_ui.c.gray("; "))}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// src/scaffold/auth.ts
|
|
61
|
+
var import_node_fs2 = require("node:fs");
|
|
62
|
+
var import_node_path3 = require("node:path");
|
|
63
|
+
|
|
64
|
+
// src/scaffold/edit.ts
|
|
65
|
+
var import_node_fs = require("node:fs");
|
|
66
|
+
var import_node_path2 = require("node:path");
|
|
67
|
+
function closing(text, open) {
|
|
68
|
+
let depth = 0;
|
|
69
|
+
for (let i = open; i < text.length; i++) {
|
|
70
|
+
const ch = text[i];
|
|
71
|
+
if (ch === "/" && text[i + 1] === "/") i = text.indexOf("\n", i) === -1 ? text.length : text.indexOf("\n", i);
|
|
72
|
+
else if (ch === "/" && text[i + 1] === "*") i = text.indexOf("*/", i + 2) + 1;
|
|
73
|
+
else if (ch === "'" || ch === '"' || ch === "`") {
|
|
74
|
+
for (i++; i < text.length && text[i] !== ch; i++) if (text[i] === "\\") i++;
|
|
75
|
+
} else if ("([{".includes(ch)) depth++;
|
|
76
|
+
else if (")]}".includes(ch) && --depth === 0) return i;
|
|
77
|
+
}
|
|
78
|
+
return -1;
|
|
79
|
+
}
|
|
80
|
+
function appendToArray(text, open, items) {
|
|
81
|
+
const close = closing(text, open);
|
|
82
|
+
const inner = text.slice(open + 1, close);
|
|
83
|
+
if (!inner.trim()) return text.slice(0, open + 1) + items.join(", ") + text.slice(close);
|
|
84
|
+
if (inner.includes("\n")) {
|
|
85
|
+
const last = inner.trimEnd();
|
|
86
|
+
const indent = last.slice(last.lastIndexOf("\n") + 1).match(/^\s*/)[0];
|
|
87
|
+
const comma = last.endsWith(",") ? "" : ",";
|
|
88
|
+
const tail = inner.slice(last.length);
|
|
89
|
+
return text.slice(0, open + 1) + last + comma + items.map((i) => `
|
|
90
|
+
${indent}${i},`).join("") + tail + text.slice(close);
|
|
91
|
+
}
|
|
92
|
+
const body = inner.replace(/,?\s*$/, "");
|
|
93
|
+
const trailing = inner.slice(body.length).replace(/^,/, "");
|
|
94
|
+
return text.slice(0, open + 1) + body + ", " + items.join(", ") + trailing + text.slice(close);
|
|
95
|
+
}
|
|
96
|
+
function addImport(text, line) {
|
|
97
|
+
if (text.includes(line)) return text;
|
|
98
|
+
const imports = [...text.matchAll(/^import[\s\S]*?from\s+['"][^'"]+['"];?[^\n]*\n/gm)];
|
|
99
|
+
const at = imports.length ? imports[imports.length - 1].index + imports[imports.length - 1][0].length : 0;
|
|
100
|
+
return text.slice(0, at) + line + "\n" + text.slice(at);
|
|
101
|
+
}
|
|
102
|
+
function register(log, file, header, r) {
|
|
103
|
+
const imports = [...r.extraImports ?? [], r.importLine];
|
|
104
|
+
if (!(0, import_node_fs.existsSync)(file)) {
|
|
105
|
+
(0, import_node_fs.mkdirSync)((0, import_node_path2.dirname)(file), { recursive: true });
|
|
106
|
+
(0, import_node_fs.writeFileSync)(
|
|
107
|
+
file,
|
|
108
|
+
`${header}import { defineApp } from '@fluixi/start/app';
|
|
109
|
+
${imports.join("\n")}
|
|
110
|
+
|
|
111
|
+
export default defineApp({ imports: [${r.entry}] });
|
|
112
|
+
`
|
|
113
|
+
);
|
|
114
|
+
log.push({ kind: "created", file });
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
let text = (0, import_node_fs.readFileSync)(file, "utf8");
|
|
118
|
+
if (text.includes(r.importLine)) {
|
|
119
|
+
log.push({ kind: "listed", file, detail: r.entry });
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
const m = text.match(/\bimports\s*:\s*\[/);
|
|
123
|
+
if (!m) {
|
|
124
|
+
log.push({ kind: "hint", file, lines: [...imports, `imports: [${r.entry}]`] });
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
text = appendToArray(text, m.index + m[0].length - 1, [r.entry]);
|
|
128
|
+
for (const line of imports) text = addImport(text, line);
|
|
129
|
+
(0, import_node_fs.writeFileSync)(file, text);
|
|
130
|
+
log.push({ kind: "updated", file, detail: `imports ${r.entry}` });
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
function envPublicNames(configFile) {
|
|
134
|
+
if (!(0, import_node_fs.existsSync)(configFile)) return null;
|
|
135
|
+
const text = (0, import_node_fs.readFileSync)(configFile, "utf8");
|
|
136
|
+
const m = text.match(/\bpublic\s*:\s*\[/);
|
|
137
|
+
if (!m) return null;
|
|
138
|
+
const open = m.index + m[0].length - 1;
|
|
139
|
+
const list = text.slice(open, closing(text, open) + 1);
|
|
140
|
+
return [...list.matchAll(/['"]([A-Za-z_][A-Za-z0-9_]*)['"]/g)].map((x) => x[1]);
|
|
141
|
+
}
|
|
142
|
+
function addEnvPublic(log, configFile, names) {
|
|
143
|
+
const list = (indent) => `${indent}public: [
|
|
144
|
+
${names.map((n) => `${indent} '${n}',
|
|
145
|
+
`).join("")}${indent}],
|
|
146
|
+
`;
|
|
147
|
+
const have = envPublicNames(configFile);
|
|
148
|
+
if (have === null) {
|
|
149
|
+
const text2 = (0, import_node_fs.existsSync)(configFile) ? (0, import_node_fs.readFileSync)(configFile, "utf8") : "";
|
|
150
|
+
const call = text2.match(/defineConfig\(\{\n/);
|
|
151
|
+
if (call && !/\benv\s*:/.test(text2)) {
|
|
152
|
+
const at = call.index + call[0].length;
|
|
153
|
+
(0, import_node_fs.writeFileSync)(configFile, text2.slice(0, at) + ` env: {
|
|
154
|
+
${list(" ")} },
|
|
155
|
+
` + text2.slice(at));
|
|
156
|
+
log.push({ kind: "updated", file: configFile, detail: `env.public ${names.join(", ")}` });
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
log.push({ kind: "hint", file: configFile, lines: [`env: { public: [${names.map((n) => `'${n}'`).join(", ")}] },`] });
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const missing = names.filter((n) => !have.includes(n));
|
|
163
|
+
if (!missing.length) return;
|
|
164
|
+
const text = (0, import_node_fs.readFileSync)(configFile, "utf8");
|
|
165
|
+
const m = text.match(/\bpublic\s*:\s*\[/);
|
|
166
|
+
(0, import_node_fs.writeFileSync)(configFile, appendToArray(text, m.index + m[0].length - 1, missing.map((n) => `'${n}'`)));
|
|
167
|
+
log.push({ kind: "updated", file: configFile, detail: `env.public + ${missing.join(", ")}` });
|
|
168
|
+
}
|
|
169
|
+
function addEnvExample(log, file, comment, vars) {
|
|
170
|
+
const text = (0, import_node_fs.existsSync)(file) ? (0, import_node_fs.readFileSync)(file, "utf8") : "";
|
|
171
|
+
const have = new Set([...text.matchAll(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/gm)].map((m) => m[1]));
|
|
172
|
+
const add2 = Object.entries(vars).filter(([k]) => !have.has(k) && !have.has(`VITE_${k}`));
|
|
173
|
+
if (!add2.length) return;
|
|
174
|
+
const block = `${comment.split("\n").map((l) => `# ${l}`).join("\n")}
|
|
175
|
+
${add2.map(([k, v]) => `${k}=${v}`).join("\n")}
|
|
176
|
+
`;
|
|
177
|
+
(0, import_node_fs.writeFileSync)(file, text ? `${text.replace(/\n*$/, "\n")}
|
|
178
|
+
${block}` : block);
|
|
179
|
+
log.push({ kind: text ? "updated" : "created", file, detail: add2.map(([k]) => k).join(", ") });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// src/scaffold/templates.ts
|
|
183
|
+
var PROVIDERS = ["oidc", "keycloak", "auth0", "okta", "google", "dex", "oauth2"];
|
|
184
|
+
var SERVER_ONLY = {
|
|
185
|
+
google: "Google answers a token request without a client secret with `client_secret is missing`",
|
|
186
|
+
dex: "Dex sends no CORS headers on discovery or the token endpoint"
|
|
187
|
+
};
|
|
188
|
+
var PROVIDER_VARS = {
|
|
189
|
+
oidc: { AUTH_ISSUER: "https://id.example.com" },
|
|
190
|
+
keycloak: { AUTH_BASE_URL: "http://localhost:8080", AUTH_REALM: "master" },
|
|
191
|
+
auth0: { AUTH_DOMAIN: "example.eu.auth0.com", AUTH_AUDIENCE: "https://api.example.com" },
|
|
192
|
+
okta: { AUTH_DOMAIN: "example.okta.com" },
|
|
193
|
+
google: {},
|
|
194
|
+
dex: { AUTH_BASE_URL: "http://localhost:5556/dex" },
|
|
195
|
+
oauth2: {
|
|
196
|
+
AUTH_ISSUER: "https://id.example.com",
|
|
197
|
+
AUTH_AUTHORIZATION_ENDPOINT: "https://id.example.com/authorize",
|
|
198
|
+
AUTH_TOKEN_ENDPOINT: "https://id.example.com/token"
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
var AUTH_READER = `// Provider, issuer, client and token mode from the AUTH_* variables in env.public. Moving to
|
|
202
|
+
// another provider is a change to .env.
|
|
203
|
+
export const auth = authFromEnv(publicEnv(), { clientPrefix: '' });
|
|
204
|
+
`;
|
|
205
|
+
var authEnvConfig = () => `import { publicEnv } from '@fluixi/start/env';
|
|
206
|
+
import { authFromEnv } from '@fluixi/oauth2/env';
|
|
207
|
+
|
|
208
|
+
${AUTH_READER}`;
|
|
209
|
+
var authServiceFile = (tokens) => `import { createAuthService, authModule } from '@fluixi/oauth2';
|
|
210
|
+
import { env } from '@fluixi/start/env';
|
|
211
|
+
${tokens === "browser" ? `import { auth as authEnv } from '../config/env.js';
|
|
212
|
+
` : ""}
|
|
213
|
+
export interface User {
|
|
214
|
+
sub: string;
|
|
215
|
+
email?: string;
|
|
216
|
+
name?: string;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const appUrl = env('APP_URL') ?? 'http://localhost:3000';
|
|
220
|
+
|
|
221
|
+
export const auth = createAuthService<User>({
|
|
222
|
+
${tokens === "browser" ? ` config: {
|
|
223
|
+
...authEnv.client,
|
|
224
|
+
tokens: 'browser',
|
|
225
|
+
redirectUri: \`\${appUrl}/callback\`,
|
|
226
|
+
postLogoutRedirectUri: \`\${appUrl}/login\`,
|
|
227
|
+
},` : ` // The tokens stay on the server behind an httpOnly cookie. The browser calls the routes
|
|
228
|
+
// auth.server.ts mounts under basePath and never sees a token.
|
|
229
|
+
config: {
|
|
230
|
+
tokens: 'server',
|
|
231
|
+
redirectUri: \`\${appUrl}/auth/callback\`,
|
|
232
|
+
postLogoutRedirectUri: \`\${appUrl}/login\`,
|
|
233
|
+
basePath: '/auth',
|
|
234
|
+
},`}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
export const AuthModule = authModule(auth);
|
|
238
|
+
export const { protectedRoute, Protect, Can, useUser } = auth;
|
|
239
|
+
`;
|
|
240
|
+
var authServerFile = () => `import { authServerModule } from '@fluixi/oauth2/server';
|
|
241
|
+
import { publicEnv, serverEnv } from '@fluixi/start/env';
|
|
242
|
+
import { auth } from '../config/env.js';
|
|
243
|
+
|
|
244
|
+
// A function: serverEnv with required throws when a secret is unset, and it is only read
|
|
245
|
+
// when app.config.server.ts registers this.
|
|
246
|
+
export function authServer() {
|
|
247
|
+
const appUrl = publicEnv().APP_URL ?? 'http://localhost:3000';
|
|
248
|
+
return authServerModule({
|
|
249
|
+
...auth.client,
|
|
250
|
+
clientSecret: serverEnv('OIDC_CLIENT_SECRET', { required: true }),
|
|
251
|
+
cookieSecret: serverEnv('SESSION_SECRET', { required: true }),
|
|
252
|
+
redirectUri: \`\${appUrl}/auth/callback\`,
|
|
253
|
+
defaultReturnTo: '/',
|
|
254
|
+
secureCookie: appUrl.startsWith('https://'),
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
`;
|
|
258
|
+
var callbackRoute = (html) => html ? `import { html } from '@fluixi/core';
|
|
259
|
+
import { OAuthCallback } from '@fluixi/oauth2/callback';
|
|
260
|
+
|
|
261
|
+
export default OAuthCallback({
|
|
262
|
+
pending: () => html\`<p>Signing you in</p>\`,
|
|
263
|
+
failed: (reason) => html\`<p>\${reason}</p><a href="/login">Try again</a>\`,
|
|
264
|
+
});
|
|
265
|
+
` : `import { OAuthCallback } from '@fluixi/oauth2/callback';
|
|
266
|
+
|
|
267
|
+
export default OAuthCallback({
|
|
268
|
+
pending: () => <p>Signing you in</p>,
|
|
269
|
+
failed: (reason) => (
|
|
270
|
+
<>
|
|
271
|
+
<p>{reason}</p>
|
|
272
|
+
<a href="/login">Try again</a>
|
|
273
|
+
</>
|
|
274
|
+
),
|
|
275
|
+
});
|
|
276
|
+
`;
|
|
277
|
+
var loginRoute = (html) => html ? `import { html } from '@fluixi/core';
|
|
278
|
+
import { auth } from '../services/auth.js';
|
|
279
|
+
|
|
280
|
+
export default function Login() {
|
|
281
|
+
return html\`<button @click=\${() => auth.signIn({ returnTo: '/' })}>Sign in</button>\`;
|
|
282
|
+
}
|
|
283
|
+
` : `import { auth } from '../services/auth.js';
|
|
284
|
+
|
|
285
|
+
export default function Login() {
|
|
286
|
+
return <button onClick={() => auth.signIn({ returnTo: '/' })}>Sign in</button>;
|
|
287
|
+
}
|
|
288
|
+
`;
|
|
289
|
+
var betterAuthConfig = () => `import { betterAuth } from 'better-auth';
|
|
290
|
+
import { defineAuth } from '@fluixi/auth/server';
|
|
291
|
+
|
|
292
|
+
export type AppUser = { id: string; email: string; name?: string; role?: string };
|
|
293
|
+
|
|
294
|
+
// Better Auth owns the audited primitives; defineAuth registers it with @fluixi/auth.
|
|
295
|
+
export const auth = defineAuth<AppUser>(
|
|
296
|
+
betterAuth({
|
|
297
|
+
// database: <your adapter>,
|
|
298
|
+
emailAndPassword: { enabled: true },
|
|
299
|
+
// socialProviders: { google: { β¦ }, github: { β¦ } },
|
|
300
|
+
} as Parameters<typeof betterAuth>[0]),
|
|
301
|
+
);
|
|
302
|
+
`;
|
|
303
|
+
var betterAuthClient = () => `import { createAuthClient } from 'better-auth/client';
|
|
304
|
+
import { createAuthHooks } from '@fluixi/auth/client';
|
|
305
|
+
import type { AppUser } from './auth.config.js';
|
|
306
|
+
|
|
307
|
+
const client = createAuthClient({ baseURL: '/api/auth' });
|
|
308
|
+
|
|
309
|
+
// Or, to reuse an existing reactive session: createAuthHooks({ session: yourSessionStore }).
|
|
310
|
+
export const { useUser, useSession, useAuth, useSignIn, useSignOut, protectedRoute } =
|
|
311
|
+
createAuthHooks<AppUser>({ client, loginPath: '/login' });
|
|
312
|
+
`;
|
|
313
|
+
var betterAuthMiddleware = () => `import { defineMiddleware } from '@fluixi/start';
|
|
314
|
+
import { authMiddleware } from '@fluixi/auth/server';
|
|
315
|
+
import './auth.config.js';
|
|
316
|
+
|
|
317
|
+
export default defineMiddleware([authMiddleware()]);
|
|
318
|
+
`;
|
|
319
|
+
var betterAuthRoute = () => `import { mountAuthRoutes } from '@fluixi/auth/server';
|
|
320
|
+
import '../../auth.config.js';
|
|
321
|
+
|
|
322
|
+
const handler = mountAuthRoutes();
|
|
323
|
+
export const GET = handler;
|
|
324
|
+
export const POST = handler;
|
|
325
|
+
export default handler;
|
|
326
|
+
`;
|
|
327
|
+
|
|
328
|
+
// src/scaffold/auth.ts
|
|
329
|
+
var ScaffoldError = class extends Error {
|
|
330
|
+
};
|
|
331
|
+
function projectFormat(root) {
|
|
332
|
+
try {
|
|
333
|
+
const ts = JSON.parse((0, import_node_fs2.readFileSync)((0, import_node_path3.join)(root, "tsconfig.json"), "utf8"));
|
|
334
|
+
if (ts?.compilerOptions && !ts.compilerOptions.jsx) return "html";
|
|
335
|
+
} catch {
|
|
336
|
+
}
|
|
337
|
+
return "jsx";
|
|
338
|
+
}
|
|
339
|
+
function importPath(from, to) {
|
|
340
|
+
const p = (0, import_node_path3.relative)((0, import_node_path3.dirname)(from), to).replace(/\\/g, "/").replace(/\.tsx?$/, ".js");
|
|
341
|
+
return p.startsWith(".") ? p : `./${p}`;
|
|
342
|
+
}
|
|
343
|
+
function writeOnce(log, file, content) {
|
|
344
|
+
if ((0, import_node_fs2.existsSync)(file)) {
|
|
345
|
+
log.push({ kind: "kept", file });
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
(0, import_node_fs2.mkdirSync)((0, import_node_path3.dirname)(file), { recursive: true });
|
|
349
|
+
(0, import_node_fs2.writeFileSync)(file, content);
|
|
350
|
+
log.push({ kind: "created", file });
|
|
351
|
+
return true;
|
|
352
|
+
}
|
|
353
|
+
var APP_HEADER = "// Registered by @fluixi/start ahead of both entries.\n";
|
|
354
|
+
var APP_SERVER_HEADER = "// Only the server build imports this file, so what it lists never reaches the browser.\n";
|
|
355
|
+
function registerModule(log, root, server, file, name, entry = name) {
|
|
356
|
+
const config = (0, import_node_path3.join)(root, server ? "src/app.config.server.ts" : "src/app.config.ts");
|
|
357
|
+
register(log, config, server ? APP_SERVER_HEADER : APP_HEADER, {
|
|
358
|
+
importLine: `import { ${name} } from '${importPath(config, file)}';`,
|
|
359
|
+
entry
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
function ensureAuthEnv(log, root) {
|
|
363
|
+
const file = (0, import_node_path3.join)(root, "src/config/env.ts");
|
|
364
|
+
if (writeOnce(log, file, authEnvConfig())) return;
|
|
365
|
+
let text = (0, import_node_fs2.readFileSync)(file, "utf8");
|
|
366
|
+
if (/export\s+const\s+auth\b/.test(text)) return;
|
|
367
|
+
if (!/\bpublicEnv\b/.test(text)) text = addImport(text, `import { publicEnv } from '@fluixi/start/env';`);
|
|
368
|
+
text = addImport(text, `import { authFromEnv } from '@fluixi/oauth2/env';`);
|
|
369
|
+
(0, import_node_fs2.writeFileSync)(file, `${text.replace(/\n*$/, "\n")}
|
|
370
|
+
${AUTH_READER}`);
|
|
371
|
+
log.push({ kind: "updated", file, detail: "auth" });
|
|
372
|
+
}
|
|
373
|
+
function resolveTokens(provider, tokens) {
|
|
374
|
+
const refusal = SERVER_ONLY[provider];
|
|
375
|
+
if (refusal && tokens === "browser") throw new ScaffoldError(`${provider} needs server tokens: ${refusal}`);
|
|
376
|
+
return { tokens: tokens ?? (refusal ? "server" : "browser"), forced: !!refusal && !tokens };
|
|
377
|
+
}
|
|
378
|
+
function setupOAuth2(root, options, dependencySpec = "alpha") {
|
|
379
|
+
const provider = options.provider ?? "oidc";
|
|
380
|
+
if (!PROVIDERS.includes(provider)) throw new ScaffoldError(`unknown provider ${provider}: ${PROVIDERS.join(", ")}`);
|
|
381
|
+
const { tokens, forced } = resolveTokens(provider, options.tokens);
|
|
382
|
+
const html = options.format === "html";
|
|
383
|
+
const ext = html ? "ts" : "tsx";
|
|
384
|
+
const steps = [];
|
|
385
|
+
const at = (rel2) => (0, import_node_path3.join)(root, rel2);
|
|
386
|
+
ensureAuthEnv(steps, root);
|
|
387
|
+
const service = at("src/services/auth.ts");
|
|
388
|
+
if (writeOnce(steps, service, authServiceFile(tokens))) registerModule(steps, root, false, service, "AuthModule");
|
|
389
|
+
if (tokens === "browser") writeOnce(steps, at(`src/routes/callback.${ext}`), callbackRoute(html));
|
|
390
|
+
writeOnce(steps, at(`src/routes/login.${ext}`), loginRoute(html));
|
|
391
|
+
if (tokens === "server") {
|
|
392
|
+
const server = at("src/services/auth.server.ts");
|
|
393
|
+
if (writeOnce(steps, server, authServerFile())) registerModule(steps, root, true, server, "authServer", "authServer()");
|
|
394
|
+
}
|
|
395
|
+
const vars = {
|
|
396
|
+
AUTH_PROVIDER: provider,
|
|
397
|
+
AUTH_CLIENT_ID: "my-app",
|
|
398
|
+
AUTH_TOKENS: tokens,
|
|
399
|
+
...PROVIDER_VARS[provider],
|
|
400
|
+
APP_URL: "http://localhost:3000"
|
|
401
|
+
};
|
|
402
|
+
addEnvPublic(steps, at("fluixi.config.ts"), Object.keys(vars));
|
|
403
|
+
addEnvExample(steps, at(".env.example"), `@fluixi/oauth2 (${provider}). Listed in env.public: the browser sees them.`, vars);
|
|
404
|
+
if (tokens === "server") {
|
|
405
|
+
addEnvExample(steps, at(".env.example"), "Server only, never in env.public. SESSION_SECRET: 32 bytes or more.", {
|
|
406
|
+
OIDC_CLIENT_SECRET: "",
|
|
407
|
+
SESSION_SECRET: ""
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
const redirect = tokens === "browser" ? "/callback" : "/auth/callback";
|
|
411
|
+
return {
|
|
412
|
+
steps,
|
|
413
|
+
dependencies: { "@fluixi/oauth2": dependencySpec },
|
|
414
|
+
next: ["copy .env.example to .env and fill it in", `register <APP_URL>${redirect} as the redirect URI at the provider`],
|
|
415
|
+
notes: forced ? [`${provider} refuses a public client, so the tokens stay on the server`] : []
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
function setupBetterAuth(root, dependencySpec = "alpha") {
|
|
419
|
+
const steps = [];
|
|
420
|
+
const at = (rel2) => (0, import_node_path3.join)(root, rel2);
|
|
421
|
+
writeOnce(steps, at("src/auth.config.ts"), betterAuthConfig());
|
|
422
|
+
writeOnce(steps, at("src/auth.client.ts"), betterAuthClient());
|
|
423
|
+
if (!writeOnce(steps, at("src/middleware.ts"), betterAuthMiddleware())) {
|
|
424
|
+
steps.push({
|
|
425
|
+
kind: "hint",
|
|
426
|
+
file: at("src/middleware.ts"),
|
|
427
|
+
lines: [`import { authMiddleware } from '@fluixi/auth/server';`, `import './auth.config.js';`, "authMiddleware() in the middleware list"]
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
writeOnce(steps, at("src/api/auth/[...all].ts"), betterAuthRoute());
|
|
431
|
+
return {
|
|
432
|
+
steps,
|
|
433
|
+
dependencies: { "@fluixi/auth": dependencySpec, "better-auth": "^1" },
|
|
434
|
+
next: ["set your database adapter in src/auth.config.ts"],
|
|
435
|
+
notes: []
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// src/scaffold/prompt.ts
|
|
440
|
+
var import_promises = require("node:readline/promises");
|
|
441
|
+
var import_node_readline = require("node:readline");
|
|
442
|
+
var import_node_process = require("node:process");
|
|
443
|
+
var COLOR = import_node_process.env.NO_COLOR || import_node_process.env.FORCE_COLOR === "0" ? false : (!!import_node_process.stdout.isTTY || !!import_node_process.env.FORCE_COLOR) && import_node_process.env.TERM !== "dumb";
|
|
444
|
+
var sgr = (o, x) => (s) => COLOR ? `\x1B[${o}m${s}\x1B[${x}m` : `${s}`;
|
|
445
|
+
var c2 = { bold: sgr(1, 22), green: sgr(32, 39), cyan: sgr(36, 39), gray: sgr(90, 39) };
|
|
446
|
+
var BAR = c2.gray("β");
|
|
447
|
+
function askSelect(label, choices) {
|
|
448
|
+
return new Promise((done) => {
|
|
449
|
+
let idx = 0;
|
|
450
|
+
import_node_process.stdout.write(`${c2.green("β")} ${c2.bold(label)}
|
|
451
|
+
`);
|
|
452
|
+
const draw = (first) => {
|
|
453
|
+
if (!first) import_node_process.stdout.write(`\x1B[${choices.length}A`);
|
|
454
|
+
for (let i = 0; i < choices.length; i++) {
|
|
455
|
+
const on = i === idx;
|
|
456
|
+
const dot = on ? c2.green("β") : c2.gray("β");
|
|
457
|
+
const name = on ? c2.cyan(c2.bold(choices[i].label)) : choices[i].label;
|
|
458
|
+
import_node_process.stdout.write(`\x1B[2K${BAR} ${dot} ${name} ${c2.gray(choices[i].hint)}
|
|
459
|
+
`);
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
draw(true);
|
|
463
|
+
(0, import_node_readline.emitKeypressEvents)(import_node_process.stdin);
|
|
464
|
+
if (import_node_process.stdin.isTTY) import_node_process.stdin.setRawMode(true);
|
|
465
|
+
const onKey = (_s, k) => {
|
|
466
|
+
if (k.name === "up" || k.name === "k") {
|
|
467
|
+
idx = (idx - 1 + choices.length) % choices.length;
|
|
468
|
+
draw(false);
|
|
469
|
+
} else if (k.name === "down" || k.name === "j") {
|
|
470
|
+
idx = (idx + 1) % choices.length;
|
|
471
|
+
draw(false);
|
|
472
|
+
} else if (k.name === "return" || k.name === "enter") {
|
|
473
|
+
finish();
|
|
474
|
+
done(choices[idx].value);
|
|
475
|
+
} else if (k.name === "c" && k.ctrl) {
|
|
476
|
+
finish();
|
|
477
|
+
import_node_process.stdout.write("\n");
|
|
478
|
+
(0, import_node_process.exit)(1);
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
const onEnd = () => {
|
|
482
|
+
finish();
|
|
483
|
+
done(choices[idx].value);
|
|
484
|
+
};
|
|
485
|
+
const finish = () => {
|
|
486
|
+
import_node_process.stdin.off("keypress", onKey);
|
|
487
|
+
import_node_process.stdin.off("end", onEnd);
|
|
488
|
+
import_node_process.stdin.off("close", onEnd);
|
|
489
|
+
if (import_node_process.stdin.isTTY) import_node_process.stdin.setRawMode(false);
|
|
490
|
+
import_node_process.stdin.pause();
|
|
491
|
+
import_node_process.stdout.write(`${BAR}
|
|
492
|
+
`);
|
|
493
|
+
};
|
|
494
|
+
if (import_node_process.stdin.readableEnded) {
|
|
495
|
+
finish();
|
|
496
|
+
done(choices[idx].value);
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
import_node_process.stdin.on("keypress", onKey);
|
|
500
|
+
import_node_process.stdin.on("end", onEnd);
|
|
501
|
+
import_node_process.stdin.on("close", onEnd);
|
|
502
|
+
import_node_process.stdin.resume();
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// src/add.ts
|
|
507
|
+
var ADD_USAGE = `Usage: fluixi add <feature> [options]
|
|
508
|
+
|
|
509
|
+
Features:
|
|
510
|
+
auth [oauth2|better-auth] authentication, asked for when not given
|
|
511
|
+
--provider <p> ${PROVIDERS.join(", ")}
|
|
512
|
+
--tokens browser|server where an OAuth2 app keeps its tokens
|
|
513
|
+
--no-install write the files, leave the packages to you
|
|
514
|
+
`;
|
|
515
|
+
function option(args, name) {
|
|
516
|
+
const i = args.indexOf(`--${name}`);
|
|
517
|
+
if (i !== -1 && args[i + 1] && !args[i + 1].startsWith("-")) return args[i + 1];
|
|
518
|
+
return args.find((a) => a.startsWith(`--${name}=`))?.slice(name.length + 3);
|
|
519
|
+
}
|
|
520
|
+
function installCommand(root, specs) {
|
|
521
|
+
if ((0, import_node_fs3.existsSync)((0, import_node_path4.join)(root, "pnpm-lock.yaml"))) return ["pnpm", ["add", ...specs]];
|
|
522
|
+
if ((0, import_node_fs3.existsSync)((0, import_node_path4.join)(root, "yarn.lock"))) return ["yarn", ["add", ...specs]];
|
|
523
|
+
if ((0, import_node_fs3.existsSync)((0, import_node_path4.join)(root, "bun.lockb")) || (0, import_node_fs3.existsSync)((0, import_node_path4.join)(root, "bun.lock"))) return ["bun", ["add", ...specs]];
|
|
524
|
+
return ["npm", ["install", ...specs]];
|
|
525
|
+
}
|
|
526
|
+
function channelOf(root) {
|
|
527
|
+
try {
|
|
528
|
+
const version = JSON.parse((0, import_node_fs3.readFileSync)((0, import_node_path4.join)(root, "node_modules/@fluixi/start/package.json"), "utf8")).version;
|
|
529
|
+
return version.match(/-([a-z]+)\./)?.[1] ?? "latest";
|
|
530
|
+
} catch {
|
|
531
|
+
return "alpha";
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
async function addAuth(root, args) {
|
|
535
|
+
const ask = import_node_process2.stdin.isTTY;
|
|
536
|
+
let kind = args[1] && !args[1].startsWith("-") ? args[1] : void 0;
|
|
537
|
+
let provider = option(args, "provider");
|
|
538
|
+
let tokens = option(args, "tokens");
|
|
539
|
+
if (kind && kind !== "oauth2" && kind !== "better-auth") throw new ScaffoldError(`unknown auth kind ${kind}: oauth2 or better-auth`);
|
|
540
|
+
if (tokens && tokens !== "browser" && tokens !== "server") throw new ScaffoldError("--tokens is browser or server");
|
|
541
|
+
if (provider && !PROVIDERS.includes(provider)) throw new ScaffoldError(`unknown provider ${provider}: ${PROVIDERS.join(", ")}`);
|
|
542
|
+
if (!kind && ask) {
|
|
543
|
+
kind = await askSelect("Authentication", [
|
|
544
|
+
{ value: "oauth2", label: "OAuth2 / OIDC", hint: "@fluixi/oauth2 Β· Keycloak, Auth0, Okta, Googleβ¦" },
|
|
545
|
+
{ value: "better-auth", label: "Better Auth", hint: "@fluixi/auth Β· your own user table" }
|
|
546
|
+
]);
|
|
547
|
+
}
|
|
548
|
+
kind ??= "oauth2";
|
|
549
|
+
if (kind === "oauth2" && !provider && ask) {
|
|
550
|
+
provider = await askSelect("Identity provider", PROVIDERS.map((p) => ({ value: p, label: p, hint: "" })));
|
|
551
|
+
}
|
|
552
|
+
if (kind === "oauth2" && !tokens && ask && !SERVER_ONLY[provider ?? "oidc"]) {
|
|
553
|
+
tokens = await askSelect("Where the tokens live", [
|
|
554
|
+
{ value: "browser", label: "Browser", hint: "public client Β· PKCE" },
|
|
555
|
+
{ value: "server", label: "Server", hint: "httpOnly cookie Β· client secret" }
|
|
556
|
+
]);
|
|
557
|
+
}
|
|
558
|
+
const channel = channelOf(root);
|
|
559
|
+
const result = kind === "oauth2" ? setupOAuth2(root, { provider, tokens, format: projectFormat(root) }, channel) : setupBetterAuth(root, channel);
|
|
560
|
+
const specs = Object.entries(result.dependencies).map(([name, spec]) => `${name}@${spec}`);
|
|
561
|
+
if (args.includes("--no-install")) {
|
|
562
|
+
reportAuth(result, `install ${specs.join(" ")}`);
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
reportAuth(result);
|
|
566
|
+
const [bin, binArgs] = installCommand(root, specs);
|
|
567
|
+
console.log(` ${import_ui2.c.gray("βΊ")} ${import_ui2.c.cyan([bin, ...binArgs].join(" "))}`);
|
|
568
|
+
(0, import_node_child_process.execFileSync)(bin, binArgs, { cwd: root, stdio: "inherit" });
|
|
569
|
+
}
|
|
570
|
+
async function add(feature, args, root = process.cwd()) {
|
|
571
|
+
if (feature !== "auth") {
|
|
572
|
+
console.error(ADD_USAGE);
|
|
573
|
+
process.exitCode = 1;
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
try {
|
|
577
|
+
await addAuth(root, args);
|
|
578
|
+
} catch (error) {
|
|
579
|
+
if (!(error instanceof ScaffoldError)) throw error;
|
|
580
|
+
console.error(` ${import_ui2.c.red("β")} ${error.message}`);
|
|
581
|
+
process.exitCode = 1;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
585
|
+
0 && (module.exports = {
|
|
586
|
+
ADD_USAGE,
|
|
587
|
+
add,
|
|
588
|
+
channelOf,
|
|
589
|
+
installCommand
|
|
590
|
+
});
|
package/dist/add.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const ADD_USAGE: string;
|
|
2
|
+
/** The install command for the package manager the lockfile names. */
|
|
3
|
+
export declare function installCommand(root: string, specs: string[]): [string, string[]];
|
|
4
|
+
/**
|
|
5
|
+
* The dist-tag the app's framework came from: an app on `alpha` gets the auth package from
|
|
6
|
+
* `alpha` too, so the two agree on the framework they expect.
|
|
7
|
+
*/
|
|
8
|
+
export declare function channelOf(root: string): string;
|
|
9
|
+
export declare function add(feature: string | undefined, args: string[], root?: string): Promise<void>;
|
|
10
|
+
//# sourceMappingURL=add.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"add.d.ts","sourceRoot":"","sources":["../src/add.ts"],"names":[],"mappings":"AAyBA,eAAO,MAAM,SAAS,QAOrB,CAAC;AAQF,sEAAsE;AACtE,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAKhF;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAO9C;AA4CD,wBAAsB,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,IAAI,SAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAa1G"}
|