auth.ts193 lines · main
| 1 | import { mkdir, writeFile } from 'node:fs/promises'; |
| 2 | import { dirname, resolve } from 'node:path'; |
| 3 | |
| 4 | import { error as printError, banner, step, success, blankLine } from '../output.js'; |
| 5 | import { readProjectConfig } from '../project-config.js'; |
| 6 | |
| 7 | /** |
| 8 | * Next.js middleware that proxies `/api/auth/*` to Briven's auth-tenant bridge |
| 9 | * with the project id + browser-safe public key. Body streaming needs duplex. |
| 10 | */ |
| 11 | const MIDDLEWARE_TS = `import { NextResponse, type NextRequest } from 'next/server'; |
| 12 | |
| 13 | const BRIVEN_API_ORIGIN = process.env.NEXT_PUBLIC_BRIVEN_API_ORIGIN ?? 'https://api.briven.tech'; |
| 14 | const BRIVEN_PROJECT_ID = process.env.NEXT_PUBLIC_BRIVEN_PROJECT_ID!; |
| 15 | // Prefer the public Next env (browser-safe pk_briven_auth_…); fall back to the |
| 16 | // server-only alias used by older scaffolds. |
| 17 | const BRIVEN_AUTH_KEY = |
| 18 | process.env.BRIVEN_AUTH_PUBLIC_KEY ?? process.env.NEXT_PUBLIC_BRIVEN_AUTH_KEY!; |
| 19 | |
| 20 | /** |
| 21 | * First-party proxy: browser + magic-link emails hit YOUR host, not a bare |
| 22 | * api.briven.tech page. Magic links look like: |
| 23 | * https://your.app/api/auth/v1/auth-tenant/magic-link/verify?token=… |
| 24 | */ |
| 25 | export async function middleware(req: NextRequest) { |
| 26 | if (!req.nextUrl.pathname.startsWith('/api/auth/')) return NextResponse.next(); |
| 27 | |
| 28 | let path = req.nextUrl.pathname; |
| 29 | // Canonical (emails + SDK): /api/auth/v1/auth-tenant/foo → /v1/auth-tenant/foo |
| 30 | if (path.startsWith('/api/auth/v1/auth-tenant')) { |
| 31 | path = path.slice('/api/auth'.length); |
| 32 | } else { |
| 33 | // Short form: /api/auth/foo → /v1/auth-tenant/foo |
| 34 | path = path.replace(/^\\/api\\/auth/, '/v1/auth-tenant'); |
| 35 | } |
| 36 | |
| 37 | const url = new URL(path, BRIVEN_API_ORIGIN); |
| 38 | url.search = req.nextUrl.search; |
| 39 | |
| 40 | const headers = new Headers(req.headers); |
| 41 | headers.set('x-briven-project-id', BRIVEN_PROJECT_ID); |
| 42 | headers.set('authorization', \`Bearer \${BRIVEN_AUTH_KEY}\`); |
| 43 | // Pass project id in query for bare GET email clicks (no Authorization header). |
| 44 | if (!url.searchParams.has('briven_project_id')) { |
| 45 | url.searchParams.set('briven_project_id', BRIVEN_PROJECT_ID); |
| 46 | } |
| 47 | |
| 48 | return fetch(url, { |
| 49 | method: req.method, |
| 50 | headers, |
| 51 | body: req.body, |
| 52 | // @ts-expect-error — duplex is required for streaming request bodies in Node 18+ |
| 53 | duplex: 'half', |
| 54 | }); |
| 55 | } |
| 56 | |
| 57 | export const config = { |
| 58 | matcher: ['/api/auth/:path*'], |
| 59 | }; |
| 60 | `; |
| 61 | |
| 62 | function envLocalTemplate(projectId: string): string { |
| 63 | return `# Briven Auth — fill public key from dashboard → Auth → API keys |
| 64 | # https://docs.briven.tech/auth |
| 65 | NEXT_PUBLIC_BRIVEN_API_ORIGIN=https://api.briven.tech |
| 66 | NEXT_PUBLIC_BRIVEN_PROJECT_ID=${projectId} |
| 67 | # Browser-safe key (pk_briven_auth_…). Never put a brk_ server key here. |
| 68 | NEXT_PUBLIC_BRIVEN_AUTH_KEY=pk_briven_auth_xxxxxxxxxxxxxxxx |
| 69 | # Optional alias for middleware (same value as NEXT_PUBLIC_BRIVEN_AUTH_KEY) |
| 70 | BRIVEN_AUTH_PUBLIC_KEY=pk_briven_auth_xxxxxxxxxxxxxxxx |
| 71 | `; |
| 72 | } |
| 73 | |
| 74 | const AUTH_TS = `import { createBrivenAuth } from '@briven/auth'; |
| 75 | |
| 76 | /** |
| 77 | * Stateless client. Session lives in the httpOnly cookie set by Briven. |
| 78 | * https://docs.briven.tech/auth |
| 79 | */ |
| 80 | export const auth = createBrivenAuth({ |
| 81 | projectId: process.env.NEXT_PUBLIC_BRIVEN_PROJECT_ID!, |
| 82 | publicKey: process.env.NEXT_PUBLIC_BRIVEN_AUTH_KEY!, |
| 83 | }); |
| 84 | `; |
| 85 | |
| 86 | const SIGN_IN_HINT = `// Example sign-in page (paste into app/sign-in/page.tsx or similar) |
| 87 | // |
| 88 | // Option A — hosted Briven pages (fastest pilot): |
| 89 | // 'use client'; |
| 90 | // import { auth } from '@/lib/auth'; |
| 91 | // export default function SignIn() { |
| 92 | // return ( |
| 93 | // <button type="button" onClick={() => { |
| 94 | // window.location.assign(auth.hostedPageURL('sign-in', '/dashboard')); |
| 95 | // }}> |
| 96 | // Sign in |
| 97 | // </button> |
| 98 | // ); |
| 99 | // } |
| 100 | // |
| 101 | // Option B — embedded panel: |
| 102 | // 'use client'; |
| 103 | // import { BrivenSignIn } from '@briven/auth/react'; |
| 104 | // export default function SignIn() { |
| 105 | // return <BrivenSignIn redirectTo="/dashboard" showEmailPassword showMagicLink />; |
| 106 | // } |
| 107 | `; |
| 108 | |
| 109 | export async function runAuth(argv: readonly string[]): Promise<number> { |
| 110 | const cmd = argv[0]; |
| 111 | |
| 112 | if (cmd === 'scaffold') { |
| 113 | return runScaffold(); |
| 114 | } |
| 115 | |
| 116 | banner('auth'); |
| 117 | step('usage:'); |
| 118 | step(' briven auth scaffold — middleware.ts + lib/auth.ts + .env.local seeds'); |
| 119 | blankLine(); |
| 120 | step('docs: https://docs.briven.tech/auth'); |
| 121 | step('human checklist: AUTH-GO-LIVE-CHECKLIST.md (in the Briven repo)'); |
| 122 | return 0; |
| 123 | } |
| 124 | |
| 125 | async function runScaffold(): Promise<number> { |
| 126 | const cwd = process.cwd(); |
| 127 | const config = await readProjectConfig(cwd); |
| 128 | if (!config) { |
| 129 | printError('no briven.json found — run `briven link` first.'); |
| 130 | return 1; |
| 131 | } |
| 132 | |
| 133 | const projectId = config.projectId?.trim() || 'p_xxxxxxxxxxxxxxxx'; |
| 134 | if (!config.projectId) { |
| 135 | step('warning: briven.json has no projectId yet — run `briven link` and re-scaffold,'); |
| 136 | step(' or paste your real p_… id into .env.local yourself.'); |
| 137 | } |
| 138 | |
| 139 | banner('auth scaffold'); |
| 140 | |
| 141 | const middlewarePath = resolve(cwd, 'middleware.ts'); |
| 142 | await writeFile(middlewarePath, MIDDLEWARE_TS); |
| 143 | step('created middleware.ts (proxies /api/auth/* → Briven)'); |
| 144 | |
| 145 | const authPath = resolve(cwd, 'lib/auth.ts'); |
| 146 | try { |
| 147 | await mkdir(dirname(authPath), { recursive: true }); |
| 148 | await writeFile(authPath, AUTH_TS, { flag: 'wx' }); |
| 149 | step('created lib/auth.ts (createBrivenAuth client)'); |
| 150 | } catch { |
| 151 | step('lib/auth.ts already exists — skipped'); |
| 152 | } |
| 153 | |
| 154 | const hintPath = resolve(cwd, 'lib/auth.sign-in.example.tsx.txt'); |
| 155 | try { |
| 156 | await writeFile(hintPath, SIGN_IN_HINT, { flag: 'wx' }); |
| 157 | step('created lib/auth.sign-in.example.tsx.txt (copy into a page)'); |
| 158 | } catch { |
| 159 | step('sign-in example already exists — skipped'); |
| 160 | } |
| 161 | |
| 162 | const envPath = resolve(cwd, '.env.local'); |
| 163 | try { |
| 164 | // Only write .env.local if it doesn't exist — never overwrite secrets. |
| 165 | await writeFile(envPath, envLocalTemplate(projectId), { flag: 'wx' }); |
| 166 | step(`created .env.local (project id prefilled: ${projectId})`); |
| 167 | } catch { |
| 168 | step('.env.local already exists — skipped (add the vars manually if missing)'); |
| 169 | } |
| 170 | |
| 171 | blankLine(); |
| 172 | success('scaffolded Briven Auth:'); |
| 173 | step(' middleware.ts'); |
| 174 | step(' lib/auth.ts'); |
| 175 | step(' lib/auth.sign-in.example.tsx.txt'); |
| 176 | step(' .env.local (if it was missing)'); |
| 177 | blankLine(); |
| 178 | step('Clerk-simple next steps (do in order):'); |
| 179 | step(' 1. Dashboard → Auth → Enable (once). Starter pack turns magic+OTP+passkey ON.'); |
| 180 | step(' 2. Auth → API keys → create pk_briven_auth_… (read-write) — paste into .env.local'); |
| 181 | step(' 3. Auth → Allowed Domains → add your real site (localhost:3000 often pre-seeded)'); |
| 182 | step(' 4. pnpm add @briven/auth (or npm i @briven/auth)'); |
| 183 | step(' 5. Copy lib/auth.sign-in.example into a real page (or hostedPageURL sign-in)'); |
| 184 | step(' 6. Deploy THIS app after any auth code change — GitHub green ≠ live site'); |
| 185 | step(' 7. Prove: email code or magic link works on the Allowed Domain'); |
| 186 | step(' Tip: if agents say "providers OFF", re-check THIS project — not another MCP binding.'); |
| 187 | link('https://docs.briven.tech/auth'); |
| 188 | return 0; |
| 189 | } |
| 190 | |
| 191 | function link(url: string): void { |
| 192 | step(` docs: ${url}`); |
| 193 | } |