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

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
@@ -12332,11 +12332,17 @@ function tool(input) {
12332
12332
  tool.schema = exports_external;
12333
12333
  // src/tools/inspect-http.ts
12334
12334
  var z = tool.schema;
12335
- var ALLOWED_BASE_HOSTS = [
12336
- "helloleo.dev",
12337
- "helloleo.app",
12338
- "augustin-d60.workers.dev"
12339
- ];
12335
+ var hosts = {
12336
+ "helloleo.dev": {
12337
+ private: true
12338
+ },
12339
+ "helloleo.app": {
12340
+ private: false
12341
+ },
12342
+ "augustin-d60.workers.dev": {
12343
+ private: false
12344
+ }
12345
+ };
12340
12346
  var DEFAULT_TIMEOUT_MS = 15000;
12341
12347
  var MAX_TIMEOUT_MS = 30000;
12342
12348
  var MAX_RESPONSE_BYTES = 1e6;
@@ -12370,9 +12376,11 @@ var inspectHttp = tool({
12370
12376
  }
12371
12377
  });
12372
12378
  const timeoutMs = Math.max(1, Math.min(args.timeout, MAX_TIMEOUT_MS));
12379
+ const previewGateSecret = process.env.PREVIEW_INSPECT_SECRET;
12380
+ const defaultUserAgent = isPrivateHost(url2.hostname) && previewGateSecret ? `LeoInspect/${previewGateSecret}` : "leoInspect/0.1";
12373
12381
  const requestHeaders = {
12374
12382
  Accept: "application/json, text/plain, */*",
12375
- "User-Agent": "helloleo-plugins-inspect-http/0.1",
12383
+ "User-Agent": defaultUserAgent,
12376
12384
  Origin: "https://app.helloleo.dev",
12377
12385
  ...args.headers
12378
12386
  };
@@ -12436,16 +12444,23 @@ function parseTargetUrl(raw) {
12436
12444
  }
12437
12445
  return url2;
12438
12446
  }
12439
- function allowedHostsHuman() {
12440
- return ALLOWED_BASE_HOSTS.map((base) => `**.${base}`).join(", ");
12441
- }
12442
- function isAllowedHost(hostname3) {
12447
+ function findHostBase(hostname3) {
12443
12448
  const host = hostname3.toLowerCase().replace(/\.$/, "");
12444
- for (const base of ALLOWED_BASE_HOSTS) {
12449
+ for (const base of Object.keys(hosts)) {
12445
12450
  if (host === base || host.endsWith(`.${base}`))
12446
- return true;
12451
+ return base;
12447
12452
  }
12448
- return false;
12453
+ return null;
12454
+ }
12455
+ function allowedHostsHuman() {
12456
+ return Object.keys(hosts).map((base) => `**.${base}`).join(", ");
12457
+ }
12458
+ function isAllowedHost(hostname3) {
12459
+ return findHostBase(hostname3) !== null;
12460
+ }
12461
+ function isPrivateHost(hostname3) {
12462
+ const base = findHostBase(hostname3);
12463
+ return base !== null && hosts[base].private;
12449
12464
  }
12450
12465
  function hasHeader(headers, name) {
12451
12466
  const target = name.toLowerCase();
@@ -25318,7 +25333,546 @@ var SetupNetsuitePlugin = async () => {
25318
25333
  tool: { setup_netsuite: setupNetsuite }
25319
25334
  };
25320
25335
  };
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
+ };
25321
25874
  export {
25875
+ SetupPasswordAuthPlugin,
25322
25876
  SetupNetsuitePlugin,
25323
25877
  ScaffoldPlugin,
25324
25878
  RemovePlugin,