@helloleo/plugins 0.2.0-beta.5 → 0.2.0-beta.7

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/dist/index.js CHANGED
@@ -24944,6 +24944,7 @@ var scaffold = tool({
24944
24944
  "- Always fetch data with react-query (useQuery and useMutation).",
24945
24945
  "",
24946
24946
  "Next: read src/routes/index.tsx and continue with the user's request.",
24947
+ "For authentication (password and/or OAuth), call setup_authentication.",
24947
24948
  "For NetSuite projects, call setup_netsuite."
24948
24949
  ].join(`
24949
24950
  `);
@@ -24954,9 +24955,530 @@ var ScaffoldPlugin = async () => {
24954
24955
  tool: { scaffold }
24955
24956
  };
24956
24957
  };
24958
+ // src/tools/setup-authentication/index.ts
24959
+ import path6 from "path";
24960
+
24961
+ // src/tools/setup-authentication/auth-files.ts
24962
+ function authLibTs(providers) {
24963
+ const hasPassword = providers.includes("password");
24964
+ const oauth = providers.filter((p) => p !== "password");
24965
+ const envFields = [
24966
+ "BETTER_AUTH_SECRET?: string",
24967
+ "BETTER_AUTH_URL?: string",
24968
+ ...oauth.flatMap((p) => {
24969
+ const prefix = p.toUpperCase();
24970
+ return [
24971
+ `${prefix}_CLIENT_ID?: string`,
24972
+ `${prefix}_CLIENT_SECRET?: string`
24973
+ ];
24974
+ })
24975
+ ].join(`
24976
+ `);
24977
+ const socialProvidersBlock = oauth.length ? `
24978
+ socialProviders: {
24979
+ ${oauth.map((p) => {
24980
+ const prefix = p.toUpperCase();
24981
+ return ` ${p}: { clientId: requireEnv(e.${prefix}_CLIENT_ID, '${prefix}_CLIENT_ID'), clientSecret: requireEnv(e.${prefix}_CLIENT_SECRET, '${prefix}_CLIENT_SECRET') },`;
24982
+ }).join(`
24983
+ `)}
24984
+ },` : "";
24985
+ const requireEnvFn = oauth.length ? `
24986
+ function requireEnv(value: string | undefined, name: string): string {
24987
+ if (!value) {
24988
+ throw new Error(\`\${name} is not set. Add it to .env (dev) or run \\\`wrangler secret put \${name}\\\` (prod).\`)
24989
+ }
24990
+ return value
24991
+ }
24992
+ ` : "";
24993
+ return `import { betterAuth } from 'better-auth'
24994
+ import { drizzleAdapter } from '@better-auth/drizzle-adapter'
24995
+ import { tanstackStartCookies } from 'better-auth/tanstack-start'
24996
+ import { db } from '#/db'
24997
+ import * as schema from '#/db/schema'
24998
+ import { env } from '@helloleo/runtime'
24999
+
25000
+ /*
25001
+ * buildAuth() only runs from INSIDE a request (see getAuth() below) --
25002
+ * never call it at module scope. \`db\` is a lazy Proxy over the D1 binding
25003
+ * and env.* Worker secrets don't exist until a request is in flight;
25004
+ * touching either while workerd boots the isolate throws.
25005
+ */
25006
+ ${requireEnvFn}
25007
+ function buildAuth() {
25008
+ const e = env as {
25009
+ ${envFields}
25010
+ }
25011
+
25012
+ return betterAuth({
25013
+ secret: e.BETTER_AUTH_SECRET,
25014
+ baseURL: e.BETTER_AUTH_URL,
25015
+ database: drizzleAdapter(db, { provider: 'sqlite', schema }),
25016
+ emailAndPassword: {
25017
+ enabled: ${hasPassword},
25018
+ },${socialProvidersBlock}
25019
+ plugins: [tanstackStartCookies()],
25020
+ })
25021
+ }
25022
+
25023
+ let instance: ReturnType<typeof buildAuth> | undefined
25024
+
25025
+ export function getAuth() {
25026
+ if (!instance) instance = buildAuth()
25027
+ return instance
25028
+ }
25029
+ `;
25030
+ }
25031
+ function authClientTs() {
25032
+ return `import { createAuthClient } from 'better-auth/react'
25033
+
25034
+ export const authClient = createAuthClient()
25035
+ `;
25036
+ }
25037
+ function authServerTs() {
25038
+ return `import { createServerFn } from '@tanstack/react-start'
25039
+ import { getRequestHeaders } from '@tanstack/react-start/server'
25040
+ import { getAuth } from '#/lib/auth'
25041
+
25042
+ export const getSession = createServerFn({ method: 'GET' }).handler(async () => {
25043
+ const headers = getRequestHeaders()
25044
+ return getAuth().api.getSession({ headers })
25045
+ })
25046
+
25047
+ export const ensureSession = createServerFn({ method: 'GET' }).handler(async () => {
25048
+ const headers = getRequestHeaders()
25049
+ const session = await getAuth().api.getSession({ headers })
25050
+ if (!session) throw new Error('Unauthorized')
25051
+ return session
25052
+ })
25053
+ `;
25054
+ }
25055
+ function authRouteTs() {
25056
+ return `import { createFileRoute } from '@tanstack/react-router'
25057
+ import { getAuth } from '#/lib/auth'
25058
+
25059
+ export const Route = createFileRoute('/api/auth/$')({
25060
+ server: {
25061
+ handlers: {
25062
+ GET: ({ request }: { request: Request }) => getAuth().handler(request),
25063
+ POST: ({ request }: { request: Request }) => getAuth().handler(request),
25064
+ },
25065
+ },
25066
+ })
25067
+ `;
25068
+ }
25069
+
25070
+ // src/tools/setup-authentication/constants.ts
25071
+ var PROVIDERS = [
25072
+ "password",
25073
+ "google",
25074
+ "github",
25075
+ "discord",
25076
+ "microsoft"
25077
+ ];
25078
+ var PROVIDER_LABELS = {
25079
+ google: "Google",
25080
+ github: "GitHub",
25081
+ discord: "Discord",
25082
+ microsoft: "Microsoft"
25083
+ };
25084
+ var BETTER_AUTH_VERSION = "^1.6.23";
25085
+
25086
+ // src/tools/setup-authentication/db-schema.ts
25087
+ import { readFile } from "fs/promises";
25088
+ import path4 from "path";
25089
+ var SCHEMA_MARKER = "sqliteTable('user'";
25090
+ function authTablesBlock() {
25091
+ return `
25092
+ export const user = sqliteTable('user', {
25093
+ id: text('id').primaryKey(),
25094
+ name: text('name').notNull(),
25095
+ email: text('email').notNull().unique(),
25096
+ emailVerified: integer('email_verified', { mode: 'boolean' }).default(false).notNull(),
25097
+ image: text('image'),
25098
+ createdAt: integer('created_at', { mode: 'timestamp_ms' })
25099
+ .default(sql\`(cast(unixepoch('subsecond') * 1000 as integer))\`)
25100
+ .notNull(),
25101
+ updatedAt: integer('updated_at', { mode: 'timestamp_ms' })
25102
+ .default(sql\`(cast(unixepoch('subsecond') * 1000 as integer))\`)
25103
+ .$onUpdate(() => new Date())
25104
+ .notNull(),
25105
+ })
25106
+
25107
+ export const session = sqliteTable('session', {
25108
+ id: text('id').primaryKey(),
25109
+ expiresAt: integer('expires_at', { mode: 'timestamp_ms' }).notNull(),
25110
+ token: text('token').notNull().unique(),
25111
+ createdAt: integer('created_at', { mode: 'timestamp_ms' })
25112
+ .default(sql\`(cast(unixepoch('subsecond') * 1000 as integer))\`)
25113
+ .notNull(),
25114
+ updatedAt: integer('updated_at', { mode: 'timestamp_ms' })
25115
+ .$onUpdate(() => new Date())
25116
+ .notNull(),
25117
+ ipAddress: text('ip_address'),
25118
+ userAgent: text('user_agent'),
25119
+ userId: text('user_id')
25120
+ .notNull()
25121
+ .references(() => user.id, { onDelete: 'cascade' }),
25122
+ })
25123
+
25124
+ export const account = sqliteTable('account', {
25125
+ id: text('id').primaryKey(),
25126
+ accountId: text('account_id').notNull(),
25127
+ providerId: text('provider_id').notNull(),
25128
+ userId: text('user_id')
25129
+ .notNull()
25130
+ .references(() => user.id, { onDelete: 'cascade' }),
25131
+ accessToken: text('access_token'),
25132
+ refreshToken: text('refresh_token'),
25133
+ idToken: text('id_token'),
25134
+ accessTokenExpiresAt: integer('access_token_expires_at', { mode: 'timestamp_ms' }),
25135
+ refreshTokenExpiresAt: integer('refresh_token_expires_at', { mode: 'timestamp_ms' }),
25136
+ scope: text('scope'),
25137
+ password: text('password'),
25138
+ createdAt: integer('created_at', { mode: 'timestamp_ms' })
25139
+ .default(sql\`(cast(unixepoch('subsecond') * 1000 as integer))\`)
25140
+ .notNull(),
25141
+ updatedAt: integer('updated_at', { mode: 'timestamp_ms' })
25142
+ .$onUpdate(() => new Date())
25143
+ .notNull(),
25144
+ })
25145
+
25146
+ export const verification = sqliteTable('verification', {
25147
+ id: text('id').primaryKey(),
25148
+ identifier: text('identifier').notNull(),
25149
+ value: text('value').notNull(),
25150
+ expiresAt: integer('expires_at', { mode: 'timestamp_ms' }).notNull(),
25151
+ createdAt: integer('created_at', { mode: 'timestamp_ms' })
25152
+ .default(sql\`(cast(unixepoch('subsecond') * 1000 as integer))\`)
25153
+ .notNull(),
25154
+ updatedAt: integer('updated_at', { mode: 'timestamp_ms' })
25155
+ .default(sql\`(cast(unixepoch('subsecond') * 1000 as integer))\`)
25156
+ .$onUpdate(() => new Date())
25157
+ .notNull(),
25158
+ })
25159
+ `;
25160
+ }
25161
+ async function appendAuthTables(dir) {
25162
+ const schemaPath = path4.join(dir, "src", "db", "schema.ts");
25163
+ const current = await readFile(schemaPath, "utf8").catch(() => null);
25164
+ if (current === null) {
25165
+ throw new Error("src/db/schema.ts not found -- run scaffold first.");
25166
+ }
25167
+ if (current.includes(SCHEMA_MARKER))
25168
+ return false;
25169
+ await Bun.write(schemaPath, `${current.trimEnd()}
25170
+ ${authTablesBlock()}`);
25171
+ return true;
25172
+ }
25173
+
25174
+ // src/tools/setup-authentication/package-json.ts
25175
+ import { readFile as readFile2 } from "fs/promises";
25176
+ import path5 from "path";
25177
+ async function addAuthDependencies(dir) {
25178
+ const pkgPath = path5.join(dir, "package.json");
25179
+ const pkg = JSON.parse(await readFile2(pkgPath, "utf8"));
25180
+ pkg.dependencies ??= {};
25181
+ if (pkg.dependencies["better-auth"])
25182
+ return false;
25183
+ pkg.dependencies["better-auth"] = BETTER_AUTH_VERSION;
25184
+ pkg.dependencies["@better-auth/drizzle-adapter"] = BETTER_AUTH_VERSION;
25185
+ await Bun.write(pkgPath, `${JSON.stringify(pkg, null, 2)}
25186
+ `);
25187
+ return true;
25188
+ }
25189
+
25190
+ // src/tools/setup-authentication/pages.ts
25191
+ function socialButtonsJsx(oauth) {
25192
+ return oauth.map((p) => ` <Button
25193
+ type="button"
25194
+ variant="outline"
25195
+ className="w-full"
25196
+ onClick={() => authClient.signIn.social({ provider: '${p}', callbackURL: '/' })}
25197
+ >
25198
+ Continue with ${PROVIDER_LABELS[p]}
25199
+ </Button>`).join(`
25200
+ `);
25201
+ }
25202
+ function authedLayoutTsx() {
25203
+ return `import { createFileRoute, redirect } from '@tanstack/react-router'
25204
+ import { getSession } from '#/lib/auth-server'
25205
+
25206
+ // Pathless guard -- no URL segment of its own. Put protected pages under
25207
+ // src/routes/_authed/ (e.g. _authed/dashboard.tsx -> /dashboard) and they
25208
+ // inherit this beforeLoad. See src/routes/README.md, "Authenticated routes".
25209
+ export const Route = createFileRoute('/_authed')({
25210
+ beforeLoad: async () => {
25211
+ const session = await getSession()
25212
+ if (!session) throw redirect({ to: '/login' })
25213
+ return { user: session.user }
25214
+ },
25215
+ })
25216
+ `;
25217
+ }
25218
+ function loginPageTsx(hasPassword, oauth) {
25219
+ const social = oauth.length ? `
25220
+ ${socialButtonsJsx(oauth)}
25221
+ ` : "";
25222
+ const divider = oauth.length && hasPassword ? `
25223
+ <div className="relative">
25224
+ <div className="absolute inset-0 flex items-center">
25225
+ <span className="w-full border-t" />
25226
+ </div>
25227
+ <div className="relative flex justify-center text-xs uppercase">
25228
+ <span className="bg-card px-2 text-muted-foreground">Or continue with email</span>
25229
+ </div>
25230
+ </div>
25231
+ ` : "";
25232
+ const passwordForm = hasPassword ? `
25233
+ <form onSubmit={handleSubmit} className="space-y-4">
25234
+ <div className="space-y-2">
25235
+ <Label htmlFor="email">Email</Label>
25236
+ <Input id="email" type="email" value={email} onChange={e => setEmail(e.target.value)} required autoComplete="email" />
25237
+ </div>
25238
+ <div className="space-y-2">
25239
+ <Label htmlFor="password">Password</Label>
25240
+ <Input id="password" type="password" value={password} onChange={e => setPassword(e.target.value)} required autoComplete="current-password" />
25241
+ </div>
25242
+ {error && <p className="text-sm text-destructive">{error}</p>}
25243
+ <Button type="submit" className="w-full" disabled={loading}>
25244
+ {loading ? 'Logging in\u2026' : 'Log in'}
25245
+ </Button>
25246
+ <p className="text-sm text-center text-muted-foreground">
25247
+ No account? <Link to="/signup" className="underline">Sign up</Link>
25248
+ </p>
25249
+ </form>
25250
+ ` : "";
25251
+ const stateAndHandler = hasPassword ? `
25252
+ const router = useRouter()
25253
+ const [email, setEmail] = useState('')
25254
+ const [password, setPassword] = useState('')
25255
+ const [error, setError] = useState('')
25256
+ const [loading, setLoading] = useState(false)
25257
+
25258
+ async function handleSubmit(e: React.FormEvent) {
25259
+ e.preventDefault()
25260
+ setError('')
25261
+ setLoading(true)
25262
+ const { error } = await authClient.signIn.email({ email, password })
25263
+ setLoading(false)
25264
+ if (error) {
25265
+ setError(error.message ?? 'Something went wrong.')
25266
+ return
25267
+ }
25268
+ router.navigate({ to: '/' })
25269
+ }
25270
+ ` : "";
25271
+ const imports = [
25272
+ hasPassword ? "import { createFileRoute, Link, useRouter } from '@tanstack/react-router'" : "import { createFileRoute } from '@tanstack/react-router'",
25273
+ hasPassword ? "import { useState } from 'react'" : "",
25274
+ "import { authClient } from '#/lib/auth-client'",
25275
+ "import { Button } from '#/components/ui/button'",
25276
+ hasPassword ? "import { Input } from '#/components/ui/input'" : "",
25277
+ hasPassword ? "import { Label } from '#/components/ui/label'" : "",
25278
+ "import { Card, CardContent, CardHeader, CardTitle } from '#/components/ui/card'"
25279
+ ].filter(Boolean).join(`
25280
+ `);
25281
+ return `${imports}
25282
+
25283
+ export const Route = createFileRoute('/login')({
25284
+ component: LoginPage,
25285
+ })
25286
+
25287
+ function LoginPage() {${stateAndHandler}
25288
+ return (
25289
+ <div className="min-h-screen flex items-center justify-center bg-background p-4">
25290
+ <Card className="w-full max-w-sm">
25291
+ <CardHeader>
25292
+ <CardTitle>Log in</CardTitle>
25293
+ </CardHeader>
25294
+ <CardContent className="space-y-4">${social}${divider}${passwordForm}
25295
+ </CardContent>
25296
+ </Card>
25297
+ </div>
25298
+ )
25299
+ }
25300
+ `;
25301
+ }
25302
+ function signupPageTsx() {
25303
+ return `import { createFileRoute, Link, useRouter } from '@tanstack/react-router'
25304
+ import { useState } from 'react'
25305
+ import { authClient } from '#/lib/auth-client'
25306
+ import { Button } from '#/components/ui/button'
25307
+ import { Input } from '#/components/ui/input'
25308
+ import { Label } from '#/components/ui/label'
25309
+ import { Card, CardContent, CardHeader, CardTitle } from '#/components/ui/card'
25310
+
25311
+ export const Route = createFileRoute('/signup')({
25312
+ component: SignupPage,
25313
+ })
25314
+
25315
+ function SignupPage() {
25316
+ const router = useRouter()
25317
+ const [name, setName] = useState('')
25318
+ const [email, setEmail] = useState('')
25319
+ const [password, setPassword] = useState('')
25320
+ const [error, setError] = useState('')
25321
+ const [loading, setLoading] = useState(false)
25322
+
25323
+ async function handleSubmit(e: React.FormEvent) {
25324
+ e.preventDefault()
25325
+ setError('')
25326
+ setLoading(true)
25327
+ const { error } = await authClient.signUp.email({ name, email, password })
25328
+ setLoading(false)
25329
+ if (error) {
25330
+ setError(error.message ?? 'Something went wrong.')
25331
+ return
25332
+ }
25333
+ router.navigate({ to: '/' })
25334
+ }
25335
+
25336
+ return (
25337
+ <div className="min-h-screen flex items-center justify-center bg-background p-4">
25338
+ <Card className="w-full max-w-sm">
25339
+ <CardHeader>
25340
+ <CardTitle>Sign up</CardTitle>
25341
+ </CardHeader>
25342
+ <CardContent>
25343
+ <form onSubmit={handleSubmit} className="space-y-4">
25344
+ <div className="space-y-2">
25345
+ <Label htmlFor="name">Name</Label>
25346
+ <Input id="name" type="text" value={name} onChange={e => setName(e.target.value)} required autoComplete="name" />
25347
+ </div>
25348
+ <div className="space-y-2">
25349
+ <Label htmlFor="email">Email</Label>
25350
+ <Input id="email" type="email" value={email} onChange={e => setEmail(e.target.value)} required autoComplete="email" />
25351
+ </div>
25352
+ <div className="space-y-2">
25353
+ <Label htmlFor="password">Password</Label>
25354
+ <Input id="password" type="password" value={password} onChange={e => setPassword(e.target.value)} required autoComplete="new-password" minLength={8} />
25355
+ </div>
25356
+ {error && <p className="text-sm text-destructive">{error}</p>}
25357
+ <Button type="submit" className="w-full" disabled={loading}>
25358
+ {loading ? 'Signing up\u2026' : 'Sign up'}
25359
+ </Button>
25360
+ <p className="text-sm text-center text-muted-foreground">
25361
+ Already have an account? <Link to="/login" className="underline">Log in</Link>
25362
+ </p>
25363
+ </form>
25364
+ </CardContent>
25365
+ </Card>
25366
+ </div>
25367
+ )
25368
+ }
25369
+ `;
25370
+ }
25371
+ function dashboardPageTsx() {
25372
+ return `import { createFileRoute, useRouter } from '@tanstack/react-router'
25373
+ import { authClient } from '#/lib/auth-client'
25374
+ import { Button } from '#/components/ui/button'
25375
+
25376
+ // Example protected page -- _authed.tsx needs at least one child route or
25377
+ // it collides with index.tsx on path "/" and \`tsr generate\` fails. Replace
25378
+ // this, or add more pages alongside it under src/routes/_authed/.
25379
+ export const Route = createFileRoute('/_authed/dashboard')({
25380
+ component: DashboardPage,
25381
+ })
25382
+
25383
+ function DashboardPage() {
25384
+ const router = useRouter()
25385
+ const { data: session } = authClient.useSession()
25386
+
25387
+ return (
25388
+ <div className="min-h-screen flex items-center justify-center bg-background p-4">
25389
+ <div className="text-center space-y-4">
25390
+ <p className="text-lg">Signed in as {session?.user.email}</p>
25391
+ <Button
25392
+ onClick={() =>
25393
+ authClient.signOut({
25394
+ fetchOptions: { onSuccess: () => router.navigate({ to: '/login' }) },
25395
+ })
25396
+ }
25397
+ >
25398
+ Sign out
25399
+ </Button>
25400
+ </div>
25401
+ </div>
25402
+ )
25403
+ }
25404
+ `;
25405
+ }
25406
+
25407
+ // src/tools/setup-authentication/index.ts
25408
+ var setupAuthentication = tool({
25409
+ description: "Add authentication to an existing project via better-auth. Call scaffold first, " + "then this. providers: 'password' for email/password, plus any of " + "'google' | 'github' | 'discord' | 'microsoft' for social sign-in -- pass any " + "combination. Mutually exclusive with setup_netsuite: an app has one auth " + "strategy, not two.",
25410
+ args: {
25411
+ providers: exports_external2.array(exports_external2.enum(PROVIDERS)).min(1).describe("Auth methods to enable, e.g. ['password', 'google']")
25412
+ },
25413
+ async execute({ providers }, context) {
25414
+ const dir = context.directory;
25415
+ const hasPassword = providers.includes("password");
25416
+ const oauth = providers.filter((p) => p !== "password");
25417
+ const files = [
25418
+ [path6.join("src", "lib", "auth.ts"), authLibTs(providers)],
25419
+ [path6.join("src", "lib", "auth-client.ts"), authClientTs()],
25420
+ [path6.join("src", "lib", "auth-server.ts"), authServerTs()],
25421
+ [path6.join("src", "routes", "api", "auth", "$.ts"), authRouteTs()],
25422
+ [path6.join("src", "routes", "_authed.tsx"), authedLayoutTsx()],
25423
+ [
25424
+ path6.join("src", "routes", "_authed", "dashboard.tsx"),
25425
+ dashboardPageTsx()
25426
+ ],
25427
+ [
25428
+ path6.join("src", "routes", "login.tsx"),
25429
+ loginPageTsx(hasPassword, oauth)
25430
+ ]
25431
+ ];
25432
+ if (hasPassword) {
25433
+ files.push([path6.join("src", "routes", "signup.tsx"), signupPageTsx()]);
25434
+ }
25435
+ for (const [filePath, content] of files) {
25436
+ await Bun.write(path6.join(dir, filePath), content);
25437
+ }
25438
+ const schemaAppended = await appendAuthTables(dir);
25439
+ const depsAdded = await addAuthDependencies(dir);
25440
+ const envLines = [
25441
+ " - dev (.env): BETTER_AUTH_SECRET=<random>, BETTER_AUTH_URL=http://localhost:3000",
25442
+ " - prod: `wrangler secret put BETTER_AUTH_SECRET`, and BETTER_AUTH_URL as a var in wrangler.jsonc"
25443
+ ];
25444
+ for (const p of oauth) {
25445
+ const prefix = p.toUpperCase();
25446
+ envLines.push(` - ${PROVIDER_LABELS[p]}: ${prefix}_CLIENT_ID / ${prefix}_CLIENT_SECRET (dev: .env, prod: wrangler secret put)`);
25447
+ }
25448
+ return [
25449
+ `Wrote authentication (better-auth, providers: ${providers.join(", ")}):`,
25450
+ " - src/lib/auth.ts (lazy betterAuth instance -- read the comment on why it's not a module-scope export)",
25451
+ " - src/lib/auth-client.ts (React client -- authClient.signIn.email/social, signUp.email, signOut)",
25452
+ " - src/lib/auth-server.ts (getSession/ensureSession server functions, for SSR route guards)",
25453
+ " - src/routes/api/auth/$.ts (catch-all handler)",
25454
+ " - src/routes/_authed.tsx (pathless guard for protected routes)",
25455
+ " - src/routes/_authed/dashboard.tsx (example protected page -- _authed needs at least one child route or it collides with index.tsx on '/')",
25456
+ ` - src/routes/login.tsx${hasPassword ? ", src/routes/signup.tsx" : ""}`,
25457
+ ` - src/db/schema.ts ${schemaAppended ? "(appended user/session/account/verification tables)" : "(already had a `user` table, left as-is)"}`,
25458
+ ` - package.json ${depsAdded ? "(added better-auth, @better-auth/drizzle-adapter)" : "(dependencies already present)"}`,
25459
+ "",
25460
+ "Run `bun install`, then `bun run db:push` (or just `bun dev`, which runs it first) to create the new tables.",
25461
+ "",
25462
+ "Required env vars/secrets:",
25463
+ ...envLines,
25464
+ "",
25465
+ "Add more protected pages under src/routes/_authed/ (e.g. _authed/settings.tsx -> /settings); they inherit the guard.",
25466
+ "",
25467
+ "Verify before shipping: password hashing defaults to scrypt via Node's crypto, exposed through the " + "nodejs_compat flag (already on for this template) -- confirm a real sign-up/login round-trip works in `bun dev`.",
25468
+ "",
25469
+ "Mutually exclusive with setup_netsuite -- an app has one auth strategy, not two."
25470
+ ].join(`
25471
+ `);
25472
+ }
25473
+ });
25474
+ var SetupAuthenticationPlugin = async () => {
25475
+ return {
25476
+ tool: { setup_authentication: setupAuthentication }
25477
+ };
25478
+ };
24957
25479
  // src/tools/setup-netsuite.ts
24958
25480
  import { mkdir as mkdir2 } from "fs/promises";
24959
- import path4 from "path";
25481
+ import path7 from "path";
24960
25482
  function netsuiteTs() {
24961
25483
  return `/*
24962
25484
  * NetSuite OAuth client helper (bearer-token-in-fragment).
@@ -25281,27 +25803,27 @@ var setupNetsuite = tool({
25281
25803
  args: {},
25282
25804
  async execute(_args, context) {
25283
25805
  const dir = context.directory;
25284
- await mkdir2(path4.join(dir, "src", "components", "auth"), {
25806
+ await mkdir2(path7.join(dir, "src", "components", "auth"), {
25285
25807
  recursive: true
25286
25808
  });
25287
25809
  const nsFiles = [
25288
- [path4.join("src", "lib", "netsuite.ts"), netsuiteTs()],
25289
- [path4.join("src", "lib", "auth.tsx"), authTsx()],
25810
+ [path7.join("src", "lib", "netsuite.ts"), netsuiteTs()],
25811
+ [path7.join("src", "lib", "auth.tsx"), authTsx()],
25290
25812
  [
25291
- path4.join("src", "components", "auth", "AuthLayout.tsx"),
25813
+ path7.join("src", "components", "auth", "AuthLayout.tsx"),
25292
25814
  authLayoutTsx()
25293
25815
  ],
25294
25816
  [
25295
- path4.join("src", "components", "auth", "LoginPage.tsx"),
25817
+ path7.join("src", "components", "auth", "LoginPage.tsx"),
25296
25818
  netsuiteLoginPageTsx()
25297
25819
  ],
25298
25820
  [
25299
- path4.join("src", "components", "auth", "SignInWithNetSuite.tsx"),
25821
+ path7.join("src", "components", "auth", "SignInWithNetSuite.tsx"),
25300
25822
  signInButtonTsx()
25301
25823
  ]
25302
25824
  ];
25303
25825
  for (const [filePath, content] of nsFiles) {
25304
- await Bun.write(path4.join(dir, filePath), content);
25826
+ await Bun.write(path7.join(dir, filePath), content);
25305
25827
  }
25306
25828
  return [
25307
25829
  `Wrote ${nsFiles.length} NetSuite OAuth files (all additive, nothing overwritten):`,
@@ -25333,547 +25855,9 @@ var SetupNetsuitePlugin = async () => {
25333
25855
  tool: { setup_netsuite: setupNetsuite }
25334
25856
  };
25335
25857
  };
25336
- // src/tools/setup-password-auth.ts
25337
- import { readFile } from "fs/promises";
25338
- import path5 from "path";
25339
- var SCHEMA_MARKER = "sqliteTable('users'";
25340
- function usersTableBlock() {
25341
- return `
25342
- export const users = sqliteTable('users', {
25343
- id: integer('id').primaryKey({ autoIncrement: true }),
25344
- email: text('email').notNull().unique(),
25345
- passwordHash: text('password_hash').notNull(),
25346
- createdAt: integer('created_at')
25347
- .notNull()
25348
- .default(sql\`(unixepoch())\`),
25349
- })
25350
-
25351
- export type User = typeof users.$inferSelect
25352
- `;
25353
- }
25354
- function authLibTs() {
25355
- return `import { db } from '#/db'
25356
- import { users } from '#/db/schema'
25357
- import { env } from '@helloleo/runtime'
25358
- import { eq } from 'drizzle-orm'
25359
-
25360
- /*
25361
- * Password hashing, session JWTs, and user lookup. Used ONLY from
25362
- * src/routes/api/auth/*.ts server routes -- never imported by a client
25363
- * route file. Sessions are a stateless HMAC-signed JWT in an httpOnly
25364
- * cookie, not a server-side session store: verifying a session costs one
25365
- * signature check, no database or KV round-trip.
25366
- *
25367
- * AUTH_JWT_SECRET must be set for signing/verifying to work:
25368
- * - dev: add \`AUTH_JWT_SECRET=<random value>\` to .env (gitignored;
25369
- * the cf-shim exposes process.env vars as-is in dev).
25370
- * - prod: \`wrangler secret put AUTH_JWT_SECRET\`.
25371
- * Generate a value with: openssl rand -hex 32
25372
- */
25373
-
25374
- const encoder = new TextEncoder()
25375
- const SESSION_COOKIE = 'session'
25376
- const SESSION_TTL_SECONDS = 60 * 60 * 24 * 7
25377
-
25378
- function getAuthSecret(): string {
25379
- const secret = (env as { AUTH_JWT_SECRET?: string }).AUTH_JWT_SECRET
25380
- if (!secret) {
25381
- throw new Error(
25382
- 'AUTH_JWT_SECRET is not set. Add it to .env (dev) or run ' +
25383
- '\`wrangler secret put AUTH_JWT_SECRET\` (prod).',
25384
- )
25385
- }
25386
- return secret
25387
- }
25388
-
25389
- function toHex(bytes: Uint8Array): string {
25390
- return Array.from(bytes)
25391
- .map(b => b.toString(16).padStart(2, '0'))
25392
- .join('')
25393
- }
25394
-
25395
- function timingSafeEqual(a: string, b: string): boolean {
25396
- if (a.length !== b.length) return false
25397
- let diff = 0
25398
- for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i)
25399
- return diff === 0
25400
- }
25401
-
25402
- // ---- password hashing (salted SHA-256; sufficient for a single-tenant
25403
- // demo app -- swap for a WASM bcrypt/argon2 if this ever guards real data) ----
25404
-
25405
- export function generateSalt(): string {
25406
- return toHex(crypto.getRandomValues(new Uint8Array(16)))
25407
- }
25408
-
25409
- async function sha256Hex(input: string): Promise<string> {
25410
- const digest = await crypto.subtle.digest('SHA-256', encoder.encode(input))
25411
- return toHex(new Uint8Array(digest))
25412
- }
25413
-
25414
- export async function hashPassword(password: string, salt: string): Promise<string> {
25415
- return \`\${salt}:\${await sha256Hex(salt + password)}\`
25416
- }
25417
-
25418
- export async function verifyPassword(password: string, stored: string): Promise<boolean> {
25419
- const [salt, hash] = stored.split(':')
25420
- if (!salt || !hash) return false
25421
- return timingSafeEqual(await sha256Hex(salt + password), hash)
25422
- }
25423
-
25424
- // ---- session JWT (HS256, hand-rolled -- no dependency needed for one HMAC) ----
25425
-
25426
- function base64url(bytes: Uint8Array): string {
25427
- let binary = ''
25428
- for (const b of bytes) binary += String.fromCharCode(b)
25429
- return btoa(binary).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '')
25430
- }
25431
-
25432
- function base64urlDecode(value: string): string {
25433
- const padded = value.replace(/-/g, '+').replace(/_/g, '/')
25434
- return atob(padded)
25435
- }
25436
-
25437
- async function hmacKey(secret: string) {
25438
- return crypto.subtle.importKey(
25439
- 'raw',
25440
- encoder.encode(secret),
25441
- { name: 'HMAC', hash: 'SHA-256' },
25442
- false,
25443
- ['sign'],
25444
- )
25445
- }
25446
-
25447
- export async function signSession(userId: number): Promise<string> {
25448
- const header = base64url(encoder.encode(JSON.stringify({ alg: 'HS256', typ: 'JWT' })))
25449
- const payload = base64url(
25450
- encoder.encode(
25451
- JSON.stringify({ sub: userId, exp: Math.floor(Date.now() / 1000) + SESSION_TTL_SECONDS }),
25452
- ),
25453
- )
25454
- const key = await hmacKey(getAuthSecret())
25455
- const sig = await crypto.subtle.sign('HMAC', key, encoder.encode(\`\${header}.\${payload}\`))
25456
- return \`\${header}.\${payload}.\${base64url(new Uint8Array(sig))}\`
25457
- }
25458
-
25459
- export async function verifySession(token: string): Promise<number | null> {
25460
- const parts = token.split('.')
25461
- if (parts.length !== 3) return null
25462
- const [header, payload, sig] = parts
25463
- const key = await hmacKey(getAuthSecret())
25464
- const expectedSig = base64url(
25465
- new Uint8Array(await crypto.subtle.sign('HMAC', key, encoder.encode(\`\${header}.\${payload}\`))),
25466
- )
25467
- if (!timingSafeEqual(expectedSig, sig)) return null
25468
- try {
25469
- const claims = JSON.parse(base64urlDecode(payload)) as { sub: number; exp: number }
25470
- if (claims.exp < Math.floor(Date.now() / 1000)) return null
25471
- return claims.sub
25472
- } catch {
25473
- return null
25474
- }
25475
- }
25476
-
25477
- export function sessionCookieHeader(token: string): string {
25478
- return \`\${SESSION_COOKIE}=\${token}; Path=/; Max-Age=\${SESSION_TTL_SECONDS}; HttpOnly; SameSite=Lax\`
25479
- }
25480
-
25481
- export function clearSessionCookieHeader(): string {
25482
- return \`\${SESSION_COOKIE}=; Path=/; Max-Age=0; HttpOnly; SameSite=Lax\`
25483
- }
25484
-
25485
- export function readSessionToken(request: Request): string | null {
25486
- const cookieHeader = request.headers.get('cookie') ?? ''
25487
- for (const part of cookieHeader.split(/;\\s*/)) {
25488
- const eq = part.indexOf('=')
25489
- if (eq === -1) continue
25490
- if (part.slice(0, eq) === SESSION_COOKIE) return part.slice(eq + 1)
25491
- }
25492
- return null
25493
- }
25494
-
25495
- export async function getUserFromRequest(request: Request) {
25496
- const token = readSessionToken(request)
25497
- if (!token) return null
25498
- const userId = await verifySession(token)
25499
- if (userId === null) return null
25500
- const [user] = await db
25501
- .select({ id: users.id, email: users.email })
25502
- .from(users)
25503
- .where(eq(users.id, userId))
25504
- .limit(1)
25505
- return user ?? null
25506
- }
25507
- `;
25508
- }
25509
- function signupRouteTs() {
25510
- return `import { createFileRoute } from '@tanstack/react-router'
25511
- import { db } from '#/db'
25512
- import { users } from '#/db/schema'
25513
- import { generateSalt, hashPassword, sessionCookieHeader, signSession } from '#/lib/auth'
25514
- import { eq } from 'drizzle-orm'
25515
- import { z } from 'zod'
25516
-
25517
- const bodySchema = z.object({ email: z.string().email(), password: z.string().min(8) })
25518
-
25519
- export const Route = createFileRoute('/api/auth/signup')({
25520
- server: {
25521
- handlers: {
25522
- POST: async ({ request }) => {
25523
- const parsed = bodySchema.safeParse(await request.json().catch(() => null))
25524
- if (!parsed.success) {
25525
- return Response.json({ error: 'Invalid email or password (min 8 characters).' }, { status: 400 })
25526
- }
25527
- const email = parsed.data.email.toLowerCase()
25528
-
25529
- const [existing] = await db.select({ id: users.id }).from(users).where(eq(users.email, email)).limit(1)
25530
- if (existing) {
25531
- return Response.json({ error: 'An account with this email already exists.' }, { status: 409 })
25532
- }
25533
-
25534
- const salt = generateSalt()
25535
- const passwordHash = await hashPassword(parsed.data.password, salt)
25536
- const [user] = await db
25537
- .insert(users)
25538
- .values({ email, passwordHash })
25539
- .returning({ id: users.id, email: users.email })
25540
-
25541
- const token = await signSession(user.id)
25542
- return Response.json(
25543
- { user },
25544
- { status: 201, headers: { 'Set-Cookie': sessionCookieHeader(token) } },
25545
- )
25546
- },
25547
- },
25548
- },
25549
- })
25550
- `;
25551
- }
25552
- function loginRouteTs() {
25553
- return `import { createFileRoute } from '@tanstack/react-router'
25554
- import { db } from '#/db'
25555
- import { users } from '#/db/schema'
25556
- import { sessionCookieHeader, signSession, verifyPassword } from '#/lib/auth'
25557
- import { eq } from 'drizzle-orm'
25558
- import { z } from 'zod'
25559
-
25560
- const bodySchema = z.object({ email: z.string().email(), password: z.string() })
25561
-
25562
- export const Route = createFileRoute('/api/auth/login')({
25563
- server: {
25564
- handlers: {
25565
- POST: async ({ request }) => {
25566
- const parsed = bodySchema.safeParse(await request.json().catch(() => null))
25567
- if (!parsed.success) {
25568
- return Response.json({ error: 'Invalid email or password.' }, { status: 400 })
25569
- }
25570
- const email = parsed.data.email.toLowerCase()
25571
-
25572
- const [user] = await db.select().from(users).where(eq(users.email, email)).limit(1)
25573
- if (!user || !(await verifyPassword(parsed.data.password, user.passwordHash))) {
25574
- return Response.json({ error: 'Invalid email or password.' }, { status: 401 })
25575
- }
25576
-
25577
- const token = await signSession(user.id)
25578
- return Response.json(
25579
- { user: { id: user.id, email: user.email } },
25580
- { status: 200, headers: { 'Set-Cookie': sessionCookieHeader(token) } },
25581
- )
25582
- },
25583
- },
25584
- },
25585
- })
25586
- `;
25587
- }
25588
- function logoutRouteTs() {
25589
- return `import { createFileRoute } from '@tanstack/react-router'
25590
- import { clearSessionCookieHeader } from '#/lib/auth'
25591
-
25592
- export const Route = createFileRoute('/api/auth/logout')({
25593
- server: {
25594
- handlers: {
25595
- POST: async () => {
25596
- return Response.json({ ok: true }, { headers: { 'Set-Cookie': clearSessionCookieHeader() } })
25597
- },
25598
- },
25599
- },
25600
- })
25601
- `;
25602
- }
25603
- function meRouteTs() {
25604
- return `import { createFileRoute } from '@tanstack/react-router'
25605
- import { getUserFromRequest } from '#/lib/auth'
25606
-
25607
- export const Route = createFileRoute('/api/auth/me')({
25608
- server: {
25609
- handlers: {
25610
- GET: async ({ request }) => {
25611
- const user = await getUserFromRequest(request)
25612
- return Response.json({ user })
25613
- },
25614
- },
25615
- },
25616
- })
25617
- `;
25618
- }
25619
- function sessionClientTs() {
25620
- return `/*
25621
- * Client-side session helpers. getUser() is the getUser() referenced by the
25622
- * "Authenticated routes" pattern in src/routes/README.md -- wire it into
25623
- * __root.tsx's beforeLoad (see that file's manual step below).
25624
- */
25625
-
25626
- export interface SessionUser {
25627
- id: number
25628
- email: string
25629
- }
25630
-
25631
- export async function getUser(): Promise<SessionUser | null> {
25632
- const res = await fetch('/api/auth/me')
25633
- if (!res.ok) return null
25634
- const data = (await res.json()) as { user: SessionUser | null }
25635
- return data.user
25636
- }
25637
-
25638
- export async function login(email: string, password: string): Promise<{ user: SessionUser } | { error: string }> {
25639
- const res = await fetch('/api/auth/login', {
25640
- method: 'POST',
25641
- headers: { 'Content-Type': 'application/json' },
25642
- body: JSON.stringify({ email, password }),
25643
- })
25644
- const data = await res.json()
25645
- return res.ok ? { user: data.user } : { error: data.error ?? 'Something went wrong.' }
25646
- }
25647
-
25648
- export async function signup(email: string, password: string): Promise<{ user: SessionUser } | { error: string }> {
25649
- const res = await fetch('/api/auth/signup', {
25650
- method: 'POST',
25651
- headers: { 'Content-Type': 'application/json' },
25652
- body: JSON.stringify({ email, password }),
25653
- })
25654
- const data = await res.json()
25655
- return res.ok ? { user: data.user } : { error: data.error ?? 'Something went wrong.' }
25656
- }
25657
-
25658
- export async function logout(): Promise<void> {
25659
- await fetch('/api/auth/logout', { method: 'POST' })
25660
- }
25661
- `;
25662
- }
25663
- function authedLayoutTsx() {
25664
- return `import { createFileRoute, redirect } from '@tanstack/react-router'
25665
-
25666
- // Pathless guard -- no URL segment of its own. Put protected pages under
25667
- // src/routes/_authed/ (e.g. _authed/dashboard.tsx -> /dashboard) and they
25668
- // inherit this beforeLoad. See src/routes/README.md, "Authenticated routes".
25669
- export const Route = createFileRoute('/_authed')({
25670
- beforeLoad: ({ context }) => {
25671
- if (!context.user) {
25672
- throw redirect({ to: '/login' })
25673
- }
25674
- },
25675
- })
25676
- `;
25677
- }
25678
- function loginPageTsx() {
25679
- return `import { createFileRoute, Link, redirect, useRouter } from '@tanstack/react-router'
25680
- import { useState } from 'react'
25681
- import { login } from '#/lib/session-client'
25682
- import { Button } from '#/components/ui/button'
25683
- import { Input } from '#/components/ui/input'
25684
- import { Label } from '#/components/ui/label'
25685
- import { Card, CardContent, CardHeader, CardTitle } from '#/components/ui/card'
25686
-
25687
- export const Route = createFileRoute('/login')({
25688
- beforeLoad: ({ context }) => {
25689
- if (context.user) throw redirect({ to: '/' })
25690
- },
25691
- component: LoginPage,
25692
- })
25693
-
25694
- function LoginPage() {
25695
- const router = useRouter()
25696
- const [email, setEmail] = useState('')
25697
- const [password, setPassword] = useState('')
25698
- const [error, setError] = useState('')
25699
- const [loading, setLoading] = useState(false)
25700
-
25701
- async function handleSubmit(e: React.FormEvent) {
25702
- e.preventDefault()
25703
- setError('')
25704
- setLoading(true)
25705
- const result = await login(email, password)
25706
- setLoading(false)
25707
- if ('error' in result) {
25708
- setError(result.error)
25709
- return
25710
- }
25711
- await router.invalidate()
25712
- router.navigate({ to: '/' })
25713
- }
25714
-
25715
- return (
25716
- <div className="min-h-screen flex items-center justify-center bg-background p-4">
25717
- <Card className="w-full max-w-sm">
25718
- <CardHeader>
25719
- <CardTitle>Log in</CardTitle>
25720
- </CardHeader>
25721
- <CardContent>
25722
- <form onSubmit={handleSubmit} className="space-y-4">
25723
- <div className="space-y-2">
25724
- <Label htmlFor="email">Email</Label>
25725
- <Input id="email" type="email" value={email} onChange={e => setEmail(e.target.value)} required autoComplete="email" />
25726
- </div>
25727
- <div className="space-y-2">
25728
- <Label htmlFor="password">Password</Label>
25729
- <Input id="password" type="password" value={password} onChange={e => setPassword(e.target.value)} required autoComplete="current-password" />
25730
- </div>
25731
- {error && <p className="text-sm text-destructive">{error}</p>}
25732
- <Button type="submit" className="w-full" disabled={loading}>
25733
- {loading ? 'Logging in\u2026' : 'Log in'}
25734
- </Button>
25735
- <p className="text-sm text-center text-muted-foreground">
25736
- No account? <Link to="/signup" className="underline">Sign up</Link>
25737
- </p>
25738
- </form>
25739
- </CardContent>
25740
- </Card>
25741
- </div>
25742
- )
25743
- }
25744
- `;
25745
- }
25746
- function signupPageTsx() {
25747
- return `import { createFileRoute, Link, redirect, useRouter } from '@tanstack/react-router'
25748
- import { useState } from 'react'
25749
- import { signup } from '#/lib/session-client'
25750
- import { Button } from '#/components/ui/button'
25751
- import { Input } from '#/components/ui/input'
25752
- import { Label } from '#/components/ui/label'
25753
- import { Card, CardContent, CardHeader, CardTitle } from '#/components/ui/card'
25754
-
25755
- export const Route = createFileRoute('/signup')({
25756
- beforeLoad: ({ context }) => {
25757
- if (context.user) throw redirect({ to: '/' })
25758
- },
25759
- component: SignupPage,
25760
- })
25761
-
25762
- function SignupPage() {
25763
- const router = useRouter()
25764
- const [email, setEmail] = useState('')
25765
- const [password, setPassword] = useState('')
25766
- const [error, setError] = useState('')
25767
- const [loading, setLoading] = useState(false)
25768
-
25769
- async function handleSubmit(e: React.FormEvent) {
25770
- e.preventDefault()
25771
- setError('')
25772
- setLoading(true)
25773
- const result = await signup(email, password)
25774
- setLoading(false)
25775
- if ('error' in result) {
25776
- setError(result.error)
25777
- return
25778
- }
25779
- await router.invalidate()
25780
- router.navigate({ to: '/' })
25781
- }
25782
-
25783
- return (
25784
- <div className="min-h-screen flex items-center justify-center bg-background p-4">
25785
- <Card className="w-full max-w-sm">
25786
- <CardHeader>
25787
- <CardTitle>Sign up</CardTitle>
25788
- </CardHeader>
25789
- <CardContent>
25790
- <form onSubmit={handleSubmit} className="space-y-4">
25791
- <div className="space-y-2">
25792
- <Label htmlFor="email">Email</Label>
25793
- <Input id="email" type="email" value={email} onChange={e => setEmail(e.target.value)} required autoComplete="email" />
25794
- </div>
25795
- <div className="space-y-2">
25796
- <Label htmlFor="password">Password</Label>
25797
- <Input id="password" type="password" value={password} onChange={e => setPassword(e.target.value)} required autoComplete="new-password" minLength={8} />
25798
- </div>
25799
- {error && <p className="text-sm text-destructive">{error}</p>}
25800
- <Button type="submit" className="w-full" disabled={loading}>
25801
- {loading ? 'Signing up\u2026' : 'Sign up'}
25802
- </Button>
25803
- <p className="text-sm text-center text-muted-foreground">
25804
- Already have an account? <Link to="/login" className="underline">Log in</Link>
25805
- </p>
25806
- </form>
25807
- </CardContent>
25808
- </Card>
25809
- </div>
25810
- )
25811
- }
25812
- `;
25813
- }
25814
- async function appendUsersTable(dir) {
25815
- const schemaPath = path5.join(dir, "src", "db", "schema.ts");
25816
- const current = await readFile(schemaPath, "utf8").catch(() => null);
25817
- if (current === null) {
25818
- throw new Error(`src/db/schema.ts not found -- run scaffold first.`);
25819
- }
25820
- if (current.includes(SCHEMA_MARKER))
25821
- return false;
25822
- await Bun.write(schemaPath, `${current.trimEnd()}
25823
- ${usersTableBlock()}`);
25824
- return true;
25825
- }
25826
- var setupPasswordAuth = tool({
25827
- description: "Add email/password auth to an existing project. Call scaffold first, " + "then this. Writes a users table, stateless-JWT session helpers, and " + "signup/login/logout/me API routes.",
25828
- args: {},
25829
- async execute(_args, context) {
25830
- const dir = context.directory;
25831
- await Promise.all([
25832
- Bun.write(path5.join(dir, "src", "lib", "auth.ts"), authLibTs()),
25833
- Bun.write(path5.join(dir, "src", "lib", "session-client.ts"), sessionClientTs()),
25834
- Bun.write(path5.join(dir, "src", "routes", "api", "auth", "signup.ts"), signupRouteTs()),
25835
- Bun.write(path5.join(dir, "src", "routes", "api", "auth", "login.ts"), loginRouteTs()),
25836
- Bun.write(path5.join(dir, "src", "routes", "api", "auth", "logout.ts"), logoutRouteTs()),
25837
- Bun.write(path5.join(dir, "src", "routes", "api", "auth", "me.ts"), meRouteTs()),
25838
- Bun.write(path5.join(dir, "src", "routes", "_authed.tsx"), authedLayoutTsx()),
25839
- Bun.write(path5.join(dir, "src", "routes", "login.tsx"), loginPageTsx()),
25840
- Bun.write(path5.join(dir, "src", "routes", "signup.tsx"), signupPageTsx())
25841
- ]);
25842
- const schemaAppended = await appendUsersTable(dir);
25843
- return [
25844
- "Wrote password auth (users table, JWT session, API routes):",
25845
- ` - src/db/schema.ts ${schemaAppended ? "(appended `users` table)" : "(already had a `users` table, left as-is)"}`,
25846
- " - src/lib/auth.ts (hashing, JWT sign/verify, getUserFromRequest -- server-only, never import from a client route)",
25847
- " - src/lib/session-client.ts (getUser, login, signup, logout -- client fetch helpers)",
25848
- " - src/routes/api/auth/{signup,login,logout,me}.ts (server routes -- never createServerFn)",
25849
- " - src/routes/_authed.tsx (pathless guard for protected routes)",
25850
- " - src/routes/login.tsx, src/routes/signup.tsx",
25851
- "",
25852
- "Session is a stateless HMAC-signed JWT in an httpOnly cookie (no localStorage, no server-side session store).",
25853
- "",
25854
- "Required: set AUTH_JWT_SECRET or every request 500s.",
25855
- " - dev: add `AUTH_JWT_SECRET=<value>` to .env",
25856
- " - prod: `wrangler secret put AUTH_JWT_SECRET`.",
25857
- "",
25858
- "Next (manual, one edit): in src/routes/__root.tsx, load the user into router context, per src/routes/README.md's Authenticated routes section:",
25859
- "",
25860
- " import { getUser } from '#/lib/session-client'",
25861
- " // in createRootRouteWithContext(...)({ ... }):",
25862
- " beforeLoad: async () => ({ user: await getUser() }),",
25863
- "",
25864
- "Then put protected pages under src/routes/_authed/ (e.g. _authed/dashboard.tsx -> /dashboard); they inherit the guard."
25865
- ].join(`
25866
- `);
25867
- }
25868
- });
25869
- var SetupPasswordAuthPlugin = async () => {
25870
- return {
25871
- tool: { setup_password_auth: setupPasswordAuth }
25872
- };
25873
- };
25874
25858
  export {
25875
- SetupPasswordAuthPlugin,
25876
25859
  SetupNetsuitePlugin,
25860
+ SetupAuthenticationPlugin,
25877
25861
  ScaffoldPlugin,
25878
25862
  RemovePlugin,
25879
25863
  InspectHttpPlugin