@binary1702/auth 0.2.5 → 0.3.0

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.
Files changed (49) hide show
  1. package/AGENTS.md +139 -0
  2. package/CHANGELOG.md +31 -0
  3. package/INSTALL.md +27 -16
  4. package/PROMPT.md +324 -36
  5. package/README.md +279 -51
  6. package/dist/cli.js +232 -17
  7. package/dist/core/admin-html.d.ts +26 -0
  8. package/dist/core/admin-html.js +252 -0
  9. package/dist/core/admin.d.ts +26 -0
  10. package/dist/core/admin.js +48 -0
  11. package/dist/core/config.d.ts +8 -1
  12. package/dist/core/config.js +20 -6
  13. package/dist/core/db.d.ts +131 -10
  14. package/dist/core/db.js +388 -69
  15. package/dist/core/email.d.ts +49 -0
  16. package/dist/core/email.js +213 -0
  17. package/dist/core/guard.d.ts +49 -0
  18. package/dist/core/guard.js +103 -0
  19. package/dist/core/html.d.ts +36 -0
  20. package/dist/core/html.js +106 -25
  21. package/dist/core/options.d.ts +76 -0
  22. package/dist/core/options.js +310 -0
  23. package/dist/core/org.d.ts +34 -0
  24. package/dist/core/org.js +55 -0
  25. package/dist/core/password.d.ts +16 -0
  26. package/dist/core/password.js +41 -12
  27. package/dist/core/rate.d.ts +26 -0
  28. package/dist/core/rate.js +70 -0
  29. package/dist/core/reset.d.ts +9 -0
  30. package/dist/core/reset.js +31 -0
  31. package/dist/core/session.d.ts +11 -1
  32. package/dist/core/session.js +27 -7
  33. package/dist/core/signup.d.ts +9 -6
  34. package/dist/core/signup.js +26 -15
  35. package/dist/core/tokens.d.ts +29 -0
  36. package/dist/core/tokens.js +64 -0
  37. package/dist/core/verify.d.ts +8 -0
  38. package/dist/core/verify.js +33 -0
  39. package/dist/core/yaml.d.ts +10 -0
  40. package/dist/core/yaml.js +85 -0
  41. package/dist/index.d.ts +17 -6
  42. package/dist/index.js +17 -6
  43. package/dist/next/index.d.ts +10 -3
  44. package/dist/next/index.js +444 -52
  45. package/dist/next/proxy.d.ts +2 -1
  46. package/dist/next/proxy.js +11 -7
  47. package/dist/react/index.d.ts +18 -0
  48. package/dist/react/index.js +55 -0
  49. package/package.json +16 -3
package/PROMPT.md CHANGED
@@ -4,77 +4,365 @@ Copy everything below the line into an AI coding agent running inside the Next.j
4
4
 
5
5
  ---
6
6
 
7
- Install `@binary1702/auth` in this project. It adds email + password auth to a Next.js App Router app. Follow these steps exactly and report the result of each.
7
+ Install `@binary1702/auth` in this project. It adds email + password sign-in to a Next.js App Router app: a sign-in page, a session cookie, and a guard in front of every page and API route. Follow the steps in order. After each step, note what happened; you will report it at the end.
8
8
 
9
- ## Before you start
9
+ ## How the package works (read first)
10
10
 
11
- Check these and stop if any fails:
11
+ Knowing the moving parts lets you fix problems instead of guessing.
12
12
 
13
- - `app/` or `src/app/` exists (App Router). Pages Router is not supported.
14
- - A Postgres connection string is available. Look for `DATABASE_URL` or `NEON_DATABASE_URL` in `.env.local` or the environment. If none exists, ask me for one.
15
- - If the project already has auth (a `proxy.ts` or `middleware.ts` that checks sessions, or packages like next-auth, better-auth, clerk, `@neondatabase/auth`), tell me before continuing. This package will put its own sign-in in front of everything.
13
+ ```
14
+ users, orgs, members, the package's tables, created by migrations on first start. Do not create tables with
15
+ tokens, auth_migrations these names in the app; the installer refuses a database whose users table is not its own
16
+ AUTH_SECRET signs the session cookie; anyone with it can forge sessions
17
+ auth_session cookie set on sign-in, httpOnly, 7 days; says WHO, nothing else
18
+ proxy.ts runs before every request: valid cookie or public path? continue : sign in
19
+ getSession() in your server code: cookie valid AND user still allowed? → { user, organization, authorization }
20
+ b17_auth.yaml the app's settings: name, sign-up policy, which paths need sign-in, email, roles → permissions
21
+ .env.local secrets only; the yaml references them as ${NAME}
22
+ /auth/[...path] route serves sign-in, sign-up, sign-out, session, verify, forgot, reset
23
+ ```
24
+
25
+ Two gates:
26
+
27
+ - **The proxy** checks only that the cookie is genuine. No database. It protects pages from being opened by signed-out visitors.
28
+ - **`getSession()` / `requireSession()` / `requirePermission()`** also check the database, so a deleted user, a changed password or (when required) an unverified email is refused. They protect data. Anything that reads or writes data for a user must call one of them; the proxy alone is not enough. `getUser()` / `requireUser()` are the same check returning only the user.
29
+
30
+ What the proxy lets through without a session:
31
+
32
+ | Path | Why |
33
+ |---|---|
34
+ | `/auth/*` | the sign-in pages themselves |
35
+ | `/_next/*` | Next.js internals |
36
+ | files ending in `.png .jpg .jpeg .gif .svg .ico .webp .avif .css .js .map .woff .woff2 .ttf .otf .txt .xml .webmanifest` | static assets from `public/` |
37
+ | everything not under `protected:` in `b17_auth.yaml`, when that key is set | the app decides what needs sign-in |
38
+
39
+ Everything else needs a session, including every path under `/api/` (even `/api/report.csv`). Signed-out page requests are redirected to `/auth/sign-in?next=<path>`; signed-out `/api/*` requests get `401` JSON.
40
+
41
+ POSTs to `/auth/*` that a browser marks as coming from another website are refused with `403`. Requests with no browser headers (curl, server code) are allowed.
42
+
43
+ ## Step 1: Check before you start
44
+
45
+ Stop and tell me if any check fails. Do not work around a failed check on your own.
46
+
47
+ 1. **App Router.** `app/` or `src/app/` exists. Pages Router (`pages/` only) is not supported.
48
+ 2. **Next.js version.** Read `next` in `package.json`. Write it down: 16+ uses `proxy.ts`, 15 uses `middleware.ts` (Step 4).
49
+ 3. **Node 20 or newer.** Run `node --version`.
50
+ 4. **Database.** Look for `DATABASE_URL` or `NEON_DATABASE_URL` in `.env.local`, `.env`, or the environment. It must be a **Neon** connection string (host ends in `neon.tech`). The package talks to Postgres through Neon's HTTP driver, so plain Postgres (local Docker, RDS, Supabase) will not work. If there is none, ask me for one. Never invent one.
51
+ 5. **Existing auth.** If the project already has `proxy.ts` / `middleware.ts` that checks sessions, or packages like `next-auth`, `better-auth`, `@clerk/nextjs`, `@supabase/auth-helpers-nextjs`, `@neondatabase/auth`, tell me before continuing. This package puts its own sign-in in front of everything.
52
+ 6. **Pages that must stay public.** Ask me whether any page should be visible without signing in (landing page, pricing, a webhook under `/api/`). By default nothing is. If yes, you will add them in Step 4.
53
+ 7. **Package manager.** Look for `pnpm-lock.yaml`, `yarn.lock` or `bun.lock*`. If one exists, install with that tool first (`pnpm add @binary1702/auth`, `yarn add …`, `bun add …`). The CLI uses npm otherwise, which would create a second lockfile.
54
+ 8. **.gitignore.** Confirm `.env.local` (or `.env*`) is ignored. The CLI writes the secret there. If it is not ignored, add it before Step 2.
16
55
 
17
- ## Install
56
+ ## Step 2: Run the installer
18
57
 
19
- Run, in the project root:
58
+ In the project root:
20
59
 
21
60
  ```bash
22
61
  npx @binary1702/auth
23
62
  ```
24
63
 
25
- It is interactive. It asks for the Postgres connection string, then an email and a password for the first user. If you cannot answer prompts, run it and hand control to me.
64
+ It asks, in this order, only for what is missing:
26
65
 
27
- It does the following, skipping anything already done:
66
+ | Prompt | When it appears | Answer |
67
+ |---|---|---|
68
+ | `Postgres connection string (DATABASE_URL)` | no `DATABASE_URL` in `.env.local` | the Neon URL from Step 1; a suggestion in parentheses is used if you press Enter |
69
+ | `Email transport (gmail / resend)` | first install, no `b17_auth.yaml` yet | press Enter to skip unless I gave you a Gmail service-account key file or a Resend key. Skipped: emails print to the dev console. |
70
+ | `Email` | the user table is empty | the first user's email, from me |
71
+ | `Password` / `Password again` | same | at least 8 characters, from me |
72
+
73
+ **Passwords come from me.** Never make one up, never write it to a file, never print it in your report. The simplest path: run the command and hand the terminal to me for the prompts. If you cannot run interactive commands, the CLI also reads answers from piped input, one per line, in the order above (an empty line skips the email prompt; skip lines for prompts that will not appear). Only do that with values I gave you, and tell me it may be stored in shell history.
74
+
75
+ What it does, skipping anything already done (safe to re-run):
28
76
 
29
77
  ```
30
78
  ✓ @binary1702/auth installed → package.json
31
79
  ✓ DATABASE_URL → .env.local
32
- ✓ AUTH_SECRET generated → .env.local
80
+ ✓ AUTH_SECRET generated → .env.local → 43 random characters
81
+ · email not configured → or ✓ AUTH_GMAIL_… / AUTH_RESEND_… → .env.local
33
82
  ✓ app/auth/[...path]/route.ts → export { GET, POST } from "@binary1702/auth/next"
34
- ✓ proxy.ts → guard in front of every route
35
- ✓ auth_users table ready → in the database
83
+ ✓ proxy.ts → the guard
84
+ ✓ b17_auth.yaml written → the app's settings, name taken from package.json
85
+ ✓ database schema ready (N migrations) → tables created in the database
36
86
  ✓ user "<email>" created
37
87
  ```
38
88
 
39
- If `proxy.ts` or `middleware.ts` already existed, the CLI does not touch it and prints a snippet instead. Merge the snippet into the existing file: call the package's `proxy` for the paths you want protected and keep the matcher inline. On Next 15 the file is `middleware.ts`, same content.
89
+ With `src/app/`, the files go to `src/app/auth/[...path]/route.ts` and `src/proxy.ts`.
40
90
 
41
- ## Verify
91
+ If it fails:
42
92
 
43
- Start the dev server and check with real HTTP requests:
93
+ | Message | Cause | Fix |
94
+ |---|---|---|
95
+ | `No package.json here` | wrong directory | `cd` to the project root |
96
+ | `No app/ directory` | Pages Router | stop, tell me |
97
+ | `That does not look like a postgres:// URL` | wrong value | re-run with the real URL |
98
+ | `Could not reach the database: …` | bad URL, wrong password, not Neon | check the URL with me; quote the message |
99
+ | `Input ended before all prompts were answered` | piped input too short | give one line per prompt |
44
100
 
45
- | Request | Expect |
46
- |---|---|
47
- | `GET /` with no cookie | redirect to `/auth/sign-in` |
48
- | `GET /auth/sign-in` | 200, HTML form |
49
- | `GET /api/<any>` with no cookie | 401 JSON |
50
- | `POST /auth/sign-in` JSON `{ "email", "password" }` | 200 `{ user }` and `Set-Cookie: auth_session=…; HttpOnly` |
51
- | `GET /auth/session` with that cookie | `{ user: { id, email } }` |
52
- | `GET /auth/sign-out` | cookie cleared, redirect to sign-in |
101
+ ## Step 3: Check the two files it wrote
102
+
103
+ **`app/auth/[...path]/route.ts`** must be exactly:
104
+
105
+ ```ts
106
+ export { GET, POST } from "@binary1702/auth/next"
107
+ ```
108
+
109
+ If `app/auth/` already had other files, keep them; a specific route like `app/auth/welcome/page.tsx` takes priority over the catch-all and is public.
110
+
111
+ **`proxy.ts`** (or `src/proxy.ts`) must be:
112
+
113
+ ```ts
114
+ import { proxy } from "@binary1702/auth/next/proxy"
115
+
116
+ export default proxy
117
+
118
+ // Everything except Next's build output. The proxy itself lets the
119
+ // auth pages and static files through.
120
+ export const config = {
121
+ matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
122
+ }
123
+ ```
124
+
125
+ The matcher must be written inline. Next reads it from the file at build time and ignores an imported one.
126
+
127
+ ## Step 4: Adjust the guard for this project
128
+
129
+ Pick the case that applies:
130
+
131
+ ```
132
+ Next 15?
133
+ └─ rename proxy.ts → middleware.ts (same folder, same content) and add runtime: "nodejs"
134
+ to its config export (plus experimental.nodeMiddleware: true in next.config). Next 15
135
+ ignores proxy.ts, so without this nothing is protected.
136
+
137
+ A proxy.ts / middleware.ts already existed?
138
+ ├─ CLI printed "has the old matcher" → an older version of this package; replace only the
139
+ │ matcher line with the one in Step 3
140
+ └─ CLI printed "exists, not touched" → merge by hand, see "Merging" below
141
+
142
+ Some pages must stay public (Step 1.6)?
143
+ └─ list what DOES need sign-in under protected: in b17_auth.yaml. No code. See "Protected paths" below.
144
+
145
+ Otherwise → nothing to do
146
+ ```
147
+
148
+ **Merging into an existing proxy.** Keep the existing logic, call the package's proxy for the paths that need sign-in, and keep one inline matcher that covers both:
149
+
150
+ ```ts
151
+ import { NextResponse, type NextRequest } from "next/server"
152
+ import { proxy as auth } from "@binary1702/auth/next/proxy"
153
+
154
+ export default async function proxy(request: NextRequest) {
155
+ // ...existing logic that must run first (redirects, headers, i18n)...
156
+ return auth(request)
157
+ }
158
+
159
+ export const config = {
160
+ matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
161
+ }
162
+ ```
163
+
164
+ If the existing logic builds its own response (adds headers, rewrites), run the auth check first and return its answer when it refuses; it lets a request through with status `200`:
165
+
166
+ ```ts
167
+ export default async function proxy(request: NextRequest) {
168
+ const gate = await auth(request)
169
+ if (gate.status !== 200) return gate // redirect to sign-in, or 401
170
+ // ...existing logic, returning its own response...
171
+ }
172
+ ```
53
173
 
54
- Then open `/auth/sign-in` in the browser, sign in, confirm you land on `/`.
174
+ Tell me what you merged.
55
175
 
56
- ## Wire it into the code
176
+ **Protected paths.** One line in `b17_auth.yaml` listing what needs sign-in, exact paths or `prefix/*`; everything else is open. `/app/*` covers `/app` and everything below it and nothing beside it (`/apps` stays open).
57
177
 
58
- Wherever the app needs the current user on the server:
178
+ ```yaml
179
+ protected: /app/* /account /api/*
180
+ ```
181
+
182
+ Without the key, everything needs sign-in (the default); then `public:` lists the exceptions instead. Never set both.
183
+
184
+ With `protected:`, static files are open unless their folder is listed. In the default mode, files with an extension outside the list in "How the package works" (for example `.pdf`, `.mp4`) need a session; add their folder to `public:` if signed-out visitors must load them.
185
+
186
+ ## Step 5: Use the session in the code
187
+
188
+ The proxy only protects pages from opening. Every place that reads or changes data for a user must check the session itself. The session is one JSON shape everywhere:
189
+
190
+ ```json
191
+ { "user": { "id", "email" }, "organization": { "id", "name" } | null, "authorization": { "role", "permissions": [] } }
192
+ ```
193
+
194
+ **Route handlers (`app/api/**/route.ts`):**
59
195
 
60
196
  ```ts
61
- import { requireUser } from "@binary1702/auth/next"
197
+ import { requireSession, requirePermission, unauthorized } from "@binary1702/auth/next"
62
198
 
63
- const user = await requireUser() // { id, email } or throws AuthError (401)
199
+ export async function GET() {
200
+ try {
201
+ const { user, organization } = await requireSession() // or requirePermission("invoices.read")
202
+ // ...query data WHERE org_id = organization.id (or owner_id = user.id)...
203
+ return Response.json(data)
204
+ } catch (e) {
205
+ return unauthorized(e) // 401 / 403 for auth errors, rethrows anything else
206
+ }
207
+ }
64
208
  ```
65
209
 
66
- `getUser()` returns `null` instead of throwing. Both work in route handlers, server components and server actions.
210
+ Permissions are names the app chooses (`invoices.create`), mapped from roles in `b17_auth.yaml` under `permissions:`. `owner` has everything by default. Memberships come from the CLI (`add-org`, `add-member`). Do not invent a permission system in the app; use `requirePermission()`.
211
+
212
+ **Server components (pages, layouts):**
213
+
214
+ ```ts
215
+ import { redirect } from "next/navigation"
216
+ import { getUser } from "@binary1702/auth/next"
217
+
218
+ export default async function Page() {
219
+ const user = await getUser()
220
+ if (!user) redirect("/auth/sign-out") // clears a revoked cookie, lands on sign-in
221
+ // ...
222
+ }
223
+ ```
224
+
225
+ Use `getUser()` + `redirect` in pages rather than `requireUser()`: `requireUser()` throws, which shows the error page instead of the sign-in page.
226
+
227
+ **Server actions:** call `requireUser()` at the top of every action. Actions can be called directly, so do not rely on the page having checked.
228
+
229
+ **Client components:** wrap the root layout once, then read the session anywhere without a request:
230
+
231
+ ```tsx
232
+ // app/layout.tsx (server)
233
+ import { getSession } from "@binary1702/auth/next"
234
+ import { AuthProvider } from "@binary1702/auth/react"
235
+ export default async function Layout({ children }) {
236
+ return <AuthProvider session={await getSession()}>{children}</AuthProvider>
237
+ }
238
+
239
+ // any client component
240
+ import { useAuth } from "@binary1702/auth/react"
241
+ const { user, organization, authorization } = useAuth() // for rendering only; the server still checks
242
+ ```
243
+
244
+ Without React: `fetch("/auth/session")` returns the same shape, or `401`.
245
+
246
+ Sign-out: a link to `/auth/sign-out`, or `fetch("/auth/sign-out", { method: "POST" })`.
247
+
248
+ **What the ids are:** `user.id` and `organization.id` are UUIDs and never change; use them as foreign keys in the app's own tables. The package authenticates and tells you the role. What rows a role may touch is the app's query: always filter by `organization.id` and/or `user.id`.
249
+
250
+ **Do not:**
251
+ - build a sign-in, sign-up, forgot-password or verification page (they exist under `/auth/`)
252
+ - read or parse the `auth_session` cookie yourself; use `getSession()`
253
+ - query or alter the package's tables (`users`, `orgs`, `members`, `tokens`); use the CLI commands below
254
+ - put a secret in `b17_auth.yaml`; secrets go in `.env.local` and the yaml references them as `${NAME}`
255
+ - edit anything in `node_modules/@binary1702/auth`
256
+
257
+ After the install, `node_modules/@binary1702/auth/AGENTS.md` is the reference for working in the app day to day.
258
+
259
+ **Custom sign-in form (only if I ask for one):** a page under `/auth/` with a form that POSTs `email`, `password` and optionally `next` to `/auth/sign-in`. It must be served from the same site; cross-site POSTs get `403`.
260
+
261
+ ## Step 6: Settings
262
+
263
+ Only if I ask for them. All go in `b17_auth.yaml` (the CLI wrote it with comments showing every key). In development changes apply on the next request; in production restart.
264
+
265
+ | Key | Effect |
266
+ |---|---|
267
+ | `name: Acme` | page titles and email subjects |
268
+ | `signup: open` | anyone can create an account at `/auth/sign-up`; the sign-in page gets a "Create one" link |
269
+ | `signup: invite` + `inviteCode: ${AUTH_INVITE_CODE}` | the sign-up form also requires the code from `.env.local` |
270
+ | `emailDomain: example.com` | sign-up only accepts `@example.com` emails |
271
+ | `requireVerified: true` | no session until the emailed link is clicked (needs email configured) |
272
+ | `protected: /app/* /api/*` | only these need sign-in; everything else is open (default without it: everything needs sign-in, `public:` lists exceptions) |
273
+ | `email:` | Gmail API or Resend; see the README's "Email" section. Keys stay in `.env.local`. |
274
+ | `permissions:` / `organizations:` | role → permissions, and per-organization overrides |
275
+
276
+ Without `signup`, sign-up is closed, except while the user table is empty: then `/auth/sign-in` sends visitors to create the first account.
277
+
278
+ `admins: <email>` in `b17_auth.yaml` lets that user open `/auth/admin`, which does everything below from the browser.
279
+
280
+ User and organization management from the terminal (each asks for anything not given):
281
+
282
+ ```bash
283
+ npx @binary1702/auth add-user [email]
284
+ npx @binary1702/auth set-password [email] # also signs them out everywhere
285
+ npx @binary1702/auth remove-user [email] # asks to type the email again
286
+ npx @binary1702/auth sign-out-everywhere [email]
287
+ npx @binary1702/auth list-users
288
+ npx @binary1702/auth add-org <name>
289
+ npx @binary1702/auth add-member <email> <org> [role] # role defaults to "member"
290
+ npx @binary1702/auth remove-member <email> <org>
291
+ npx @binary1702/auth list-orgs
292
+ ```
293
+
294
+ ## Step 7: Verify
295
+
296
+ Start the dev server (`npm run dev`, or the project's script). Restart it if it was already running, so it loads `.env.local`. Then check with real HTTP requests. Replace `3000` with the actual port. Use the email and password I gave you; do not print the password in your report.
297
+
298
+ ```bash
299
+ B=http://localhost:3000
300
+ curl -s -o /dev/null -w "%{http_code} %{redirect_url}\n" $B/
301
+ curl -s -o /dev/null -w "%{http_code}\n" $B/auth/sign-in
302
+ curl -s -w " %{http_code}\n" $B/api/anything
303
+ curl -s -c jar.txt -w " %{http_code}\n" -H "content-type: application/json" \
304
+ -d '{"email":"<email>","password":"<password>"}' $B/auth/sign-in
305
+ curl -s -b jar.txt $B/auth/session
306
+ curl -s -w " %{http_code}\n" $B/auth/session
307
+ curl -s -b jar.txt -o /dev/null -w "%{http_code}\n" $B/
308
+ curl -s -w " %{http_code}\n" -H "sec-fetch-site: cross-site" -H "content-type: application/json" \
309
+ -d '{"email":"x@x.com","password":"x"}' $B/auth/sign-in
310
+ curl -s -o /dev/null -w "%{http_code} %{redirect_url}\n" "$B/auth/sign-in?next=/%5Cevil.com" -b jar.txt
311
+ curl -s -b jar.txt -o /dev/null -w "%{http_code} %{redirect_url}\n" $B/auth/sign-out
312
+ rm jar.txt
313
+ ```
314
+
315
+ | # | Request | Expect |
316
+ |---|---|---|
317
+ | 1 | `GET /` no cookie | `307` to `/auth/sign-in` (or `200` if `/` is in your public list) |
318
+ | 2 | `GET /auth/sign-in` | `200` (or `307` to `/auth/sign-up` if there are no users yet) |
319
+ | 3 | `GET /api/anything` no cookie | `401` `{"error":"Unauthorized"}` |
320
+ | 4 | `POST /auth/sign-in` JSON | `200` `{"user":{...}}`; `jar.txt` contains `auth_session` |
321
+ | 5 | `GET /auth/session` with cookie | `{"user":{"id":"…","email":"…"},"organization":null,"authorization":{"role":null,"permissions":[]}}` (organization and role filled in once the user is in one) |
322
+ | 5b | `GET /auth/session` no cookie | `401` `{"error":"Unauthorized"}` |
323
+ | 6 | `GET /` with cookie | the page's normal status, not a redirect to sign-in |
324
+ | 7 | cross-site `POST /auth/sign-in` | `403` `{"error":"Cross-site request refused"}` |
325
+ | 8 | sign-in page with `next=/\evil.com`, signed in | redirect to `/` on this site, never to `evil.com` |
326
+ | 9 | `GET /auth/sign-out` | `303` to `/auth/sign-in` |
327
+
328
+ Also check, by request or in the browser:
329
+ - each public page from Step 4 opens signed out; a page next to it that is not public redirects
330
+ - an image from `public/` loads on the sign-in page and on public pages
331
+ - open `/auth/sign-in` in a browser, sign in, confirm you land on `/` (or on the `next` page)
332
+
333
+ If something does not match:
334
+
335
+ | Symptom | Likely cause | Fix |
336
+ |---|---|---|
337
+ | everything opens without signing in | Next 15 with `proxy.ts`; or proxy file in the wrong folder | Step 4; the file must be `src/proxy.ts` when the app is in `src/app/`, root `proxy.ts` when it is in `app/` |
338
+ | `500` with `AUTH_SECRET is missing` | dev server started before `.env.local` was written | restart the dev server |
339
+ | `500` with a database error | wrong `DATABASE_URL`, not a Neon URL | check the URL with me |
340
+ | `403 Cross-site request refused` from the app's own form | form or fetch served from a different origin/port than the app | serve it from the same origin |
341
+ | images or fonts redirect to sign-in | default mode and the extension is not in the static list | add the folder to `public:`, or switch to `protected:` |
342
+ | page shows the error screen when signed out | page uses `requireUser()` / `requireSession()` | use `getUser()` / `getSession()` + `redirect` (Step 5) |
343
+ | signed in, but pages still say signed out | user was removed or password changed after sign-in; or `requireVerified` is on and the email is unverified | sign out and in again; or follow the verification link |
344
+ | `b17_auth.yaml line N: …` on start | a typo in the settings file | fix that line; the message names the key |
345
+ | `… refers to ${NAME}, which is not set` | the yaml references a variable missing from `.env.local` | add it, restart |
346
+
347
+ ## Step 8: Deploy
67
348
 
68
- For sign-out, link to `/auth/sign-out`. For client code, `fetch("/auth/session")`.
349
+ Copy every variable in `.env.local` (`DATABASE_URL`, `AUTH_SECRET`, the email variables such as `AUTH_GMAIL_CLIENT_EMAIL` + `AUTH_GMAIL_PRIVATE_KEY` or `AUTH_RESEND_API_KEY`, `AUTH_INVITE_CODE` if used) into the host's environment variables (Vercel: Project Settings → Environment Variables). `b17_auth.yaml` is committed with the code and needs nothing. Do not paste the values into your report or a commit.
69
350
 
70
- Do not build a sign-up page. One exists at `/auth/sign-up`. It is closed by default (except for the very first user on an empty install). To let people register, add `AUTH_SIGNUP=open` to `.env.local`, optionally with `AUTH_INVITE_CODE=<code>` to require a code on the form. Users can also be created with `npx @binary1702/auth add-user`.
351
+ - Use the same `AUTH_SECRET` for every instance of the app; changing it signs everyone out.
352
+ - If production uses a different database than development, the table is created on the first request but has no users, and while it is empty the first visitor can create the first account. Tell me right after the deploy, so I open `/auth/sign-in` on the live site and create my account before anyone else does. (Running `add-user` locally does not help: the CLI uses the `DATABASE_URL` in `.env.local`, which is the development database.)
353
+ - In production the cookie is `__Host-auth_session`: `Secure`, path `/`, no domain. The site must be served over HTTPS, and the cookie cannot be shared with a subdomain (that is the point).
71
354
 
72
- ## Deploy
355
+ ## Step 9: Report
73
356
 
74
- Copy `DATABASE_URL` and `AUTH_SECRET` from `.env.local` into the host's environment. Nothing else.
357
+ Tell me, in order:
75
358
 
76
- ## Report
359
+ 1. each check from Step 1 and its result
360
+ 2. each installer line from Step 2: done, skipped, or failed (error text verbatim)
361
+ 3. what you changed in Step 4 and why (Next 15 rename, merge, public paths)
362
+ 4. every file you changed in Step 5
363
+ 5. each row of the Step 7 table with the actual status code, and the extra checks
364
+ 6. anything that took more steps than this guide says, or did not match it
77
365
 
78
- Tell me, in order: each install step and whether it was done or skipped, each verify row with the actual status code, anything that failed with the error text verbatim, and anything that took more steps than it should. Do not modify the package itself, only this project.
366
+ Never include `AUTH_SECRET`, `DATABASE_URL` or any password in the report. Do not modify the package itself, only this project.
79
367
 
80
368
  Docs: https://www.npmjs.com/package/@binary1702/auth