auth.ts193 lines · main
1import { mkdir, writeFile } from 'node:fs/promises';
2import { dirname, resolve } from 'node:path';
3
4import { error as printError, banner, step, success, blankLine } from '../output.js';
5import { 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 */
11const MIDDLEWARE_TS = `import { NextResponse, type NextRequest } from 'next/server';
12
13const BRIVEN_API_ORIGIN = process.env.NEXT_PUBLIC_BRIVEN_API_ORIGIN ?? 'https://api.briven.tech';
14const 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.
17const 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 */
25export 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
57export const config = {
58 matcher: ['/api/auth/:path*'],
59};
60`;
61
62function envLocalTemplate(projectId: string): string {
63 return `# Briven Auth — fill public key from dashboard → Auth → API keys
64# https://docs.briven.tech/auth
65NEXT_PUBLIC_BRIVEN_API_ORIGIN=https://api.briven.tech
66NEXT_PUBLIC_BRIVEN_PROJECT_ID=${projectId}
67# Browser-safe key (pk_briven_auth_…). Never put a brk_ server key here.
68NEXT_PUBLIC_BRIVEN_AUTH_KEY=pk_briven_auth_xxxxxxxxxxxxxxxx
69# Optional alias for middleware (same value as NEXT_PUBLIC_BRIVEN_AUTH_KEY)
70BRIVEN_AUTH_PUBLIC_KEY=pk_briven_auth_xxxxxxxxxxxxxxxx
71`;
72}
73
74const 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 */
80export const auth = createBrivenAuth({
81 projectId: process.env.NEXT_PUBLIC_BRIVEN_PROJECT_ID!,
82 publicKey: process.env.NEXT_PUBLIC_BRIVEN_AUTH_KEY!,
83});
84`;
85
86const 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
109export 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
125async 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
191function link(url: string): void {
192 step(` docs: ${url}`);
193}