@binary1702/auth 0.2.5 → 0.4.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 (53) hide show
  1. package/AGENTS.md +155 -0
  2. package/CHANGELOG.md +49 -0
  3. package/INSTALL.md +27 -16
  4. package/PROMPT.md +325 -36
  5. package/README.md +338 -53
  6. package/dist/cli.js +248 -17
  7. package/dist/core/admin-html.d.ts +32 -0
  8. package/dist/core/admin-html.js +350 -0
  9. package/dist/core/admin.d.ts +56 -0
  10. package/dist/core/admin.js +90 -0
  11. package/dist/core/config.d.ts +10 -3
  12. package/dist/core/config.js +22 -8
  13. package/dist/core/db.d.ts +155 -10
  14. package/dist/core/db.js +449 -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 +52 -0
  18. package/dist/core/guard.js +106 -0
  19. package/dist/core/html.d.ts +43 -0
  20. package/dist/core/html.js +122 -28
  21. package/dist/core/invite.d.ts +7 -0
  22. package/dist/core/invite.js +30 -0
  23. package/dist/core/options.d.ts +95 -0
  24. package/dist/core/options.js +377 -0
  25. package/dist/core/org.d.ts +43 -0
  26. package/dist/core/org.js +65 -0
  27. package/dist/core/pages.d.ts +34 -0
  28. package/dist/core/pages.js +73 -0
  29. package/dist/core/password.d.ts +16 -0
  30. package/dist/core/password.js +41 -12
  31. package/dist/core/rate.d.ts +26 -0
  32. package/dist/core/rate.js +70 -0
  33. package/dist/core/reset.d.ts +9 -0
  34. package/dist/core/reset.js +31 -0
  35. package/dist/core/session.d.ts +11 -1
  36. package/dist/core/session.js +27 -7
  37. package/dist/core/signup.d.ts +13 -6
  38. package/dist/core/signup.js +30 -15
  39. package/dist/core/tokens.d.ts +29 -0
  40. package/dist/core/tokens.js +64 -0
  41. package/dist/core/verify.d.ts +8 -0
  42. package/dist/core/verify.js +33 -0
  43. package/dist/core/yaml.d.ts +10 -0
  44. package/dist/core/yaml.js +85 -0
  45. package/dist/index.d.ts +18 -6
  46. package/dist/index.js +18 -6
  47. package/dist/next/index.d.ts +10 -3
  48. package/dist/next/index.js +649 -63
  49. package/dist/next/proxy.d.ts +2 -1
  50. package/dist/next/proxy.js +22 -10
  51. package/dist/react/index.d.ts +72 -0
  52. package/dist/react/index.js +107 -0
  53. package/package.json +18 -4
package/AGENTS.md ADDED
@@ -0,0 +1,155 @@
1
+ # For agents working in an app that uses @binary1702/auth
2
+
3
+ You are editing an application that already has `@binary1702/auth` installed (there is an `app/auth/[...path]/route.ts`, a `proxy.ts` and a `b17_auth.yaml`). This file tells you what the package gives you and the rules for using it. Installing it is a different document: `PROMPT.md`.
4
+
5
+ ## The model in one screen
6
+
7
+ ```
8
+ cookie auth_session: a signed token saying WHO (user id). Nothing else. Never read it yourself.
9
+ proxy.ts runs on every request: open path or valid cookie → continue, else → /auth/sign-in (401 for /api/*)
10
+ getSession() the real check, one query: user still exists, session not revoked, email verified if required
11
+ → { user, organization, authorization } or null
12
+ b17_auth.yaml the app's choices: name, sign-up policy, which paths need sign-in, email, the permissions the app
13
+ gates (permissions:) and the starting roles (roles:, written into the database once)
14
+ roles table what each role may do, as set on /auth/admin (Roles, or inside an organization). The database decides.
15
+ .env.local secrets only (DATABASE_URL, AUTH_SECRET, the email key); the yaml references them as ${NAME}
16
+ ```
17
+
18
+ Three concepts, one JSON shape, everywhere:
19
+
20
+ ```json
21
+ {
22
+ "user": { "id": "4f1c…", "email": "ben@example.com" },
23
+ "organization": { "id": "9a2e…", "name": "Acme" },
24
+ "authorization": { "role": "admin", "permissions": ["invoices.*", "reports.read"] }
25
+ }
26
+ ```
27
+
28
+ `organization` is `null` when the user is in none; then `role` is `null` and `permissions` is `[]`.
29
+
30
+ ## Reading the session
31
+
32
+ | Where | Use | Returns |
33
+ |---|---|---|
34
+ | route handler, server component, server action | `await getSession()` | the shape above, or `null` |
35
+ | same, when the caller must be signed in | `await requireSession()` | the shape, or throws `AuthError` (401) |
36
+ | same, when a permission is required | `await requirePermission("invoices.create")` | the shape, or throws `AuthError` (401 signed out, 403 lacking it) |
37
+ | only the user | `await getUser()` / `await requireUser()` | `{ id, email }` |
38
+ | client component | `useAuth()` from `@binary1702/auth/react` | `{ user, organization, authorization, loading, refresh }` |
39
+ | browser code without React | `GET /auth/session` | the shape, or `401` |
40
+
41
+ All imports except the hook come from `@binary1702/auth/next`.
42
+
43
+ **Route handler**
44
+
45
+ ```ts
46
+ import { requirePermission, unauthorized } from "@binary1702/auth/next"
47
+
48
+ export async function POST(req: Request) {
49
+ try {
50
+ const { user, organization } = await requirePermission("invoices.create")
51
+ // ... scope every query to organization.id and/or user.id ...
52
+ return Response.json({ ok: true })
53
+ } catch (e) {
54
+ return unauthorized(e) // 401 or 403 JSON for AuthError, rethrows anything else
55
+ }
56
+ }
57
+ ```
58
+
59
+ **Page or layout** (server component): use `getSession()` and `redirect("/auth/sign-out")` when it is null. Do not use `requireSession()` in a page: it throws, which shows the error page instead of the sign-in page.
60
+
61
+ **Server action**: call `requireSession()` or `requirePermission()` at the top of every action. Actions can be invoked directly; the page having checked means nothing.
62
+
63
+ **Client component**: `useAuth()` needs an `<AuthProvider>` above it. The root layout normally has `<AuthProvider session={await getSession()}>`, so no request is made from the browser. Use it to show or hide UI only.
64
+
65
+ ## Rules
66
+
67
+ 1. **The server decides.** A greyed-out button is a hint. Every route handler and server action that reads or changes data calls `requireSession()` or `requirePermission()` itself.
68
+ 2. **Scope data by `organization.id`** (and `user.id` where it is personal). The package authenticates and tells you the role; what rows that role may touch is the app's query.
69
+ 3. **Never read or parse the `auth_session` cookie.** Never query the package's tables (`users`, `orgs`, `members`, `tokens`, `auth_migrations`) from app code. Users and memberships are managed with the CLI (below). Your own tables should use other names.
70
+ 4. **Never put a secret in `b17_auth.yaml`.** Secrets go in `.env.local`; the yaml references them as `${NAME}`. The CLI refuses a literal secret in the file.
71
+ 5. **Permission names are the app's vocabulary.** Pick `area.verb` names (`invoices.create`) and declare each with a label in `b17_auth.yaml` under `permissions:`; those are the rows on `/auth/admin › Roles`. Who holds which is decided there and saved in the database, never in code or in the yaml (`roles:` only seeds the database once). `owner` and the emails under `admins:` always hold everything. In the UI use `useAuth().can("invoices.create")` to show or hide; on the server `requirePermission()` is the check.
72
+ 6. **Do not build forgot-password or verification pages.** They exist under `/auth/`. Link to them. Sign-in and sign-up exist there too; build your own only when the app wants its own design, and then through `pages:` in `b17_auth.yaml` plus `signIn()` / `signUp()` / `fetchPolicy()` from `@binary1702/auth/react` (see "Your own sign-in page" below). Never a page that reads the cookie or checks a password itself.
73
+ 7. **Do not edit `proxy.ts`'s matcher.** Which paths need sign-in is `protected:` in `b17_auth.yaml` (`/x/*` covers `/x` and everything below); everything not listed is open. Without the key, everything is guarded, and `public:` lists exceptions instead. Never both.
74
+ 8. **Do not edit anything in `node_modules/@binary1702/auth`.**
75
+
76
+ ## b17_auth.yaml
77
+
78
+ ```yaml
79
+ name: Acme Portal # page titles, email subjects
80
+ signup: closed # open | closed | invite (needs inviteCode: ${AUTH_INVITE_CODE})
81
+ protected: /app/* /api/* # only these need sign-in; everything else is open
82
+ home: /app # the signed-in home: sign-in lands here, a signed-in visitor to / is sent here
83
+
84
+ database: ${DATABASE_URL}
85
+ secret: ${AUTH_SECRET}
86
+
87
+ email:
88
+ from: NoReply (Acme) <no-reply@acme.com>
89
+ gmail: # or resend: { apiKey: ${AUTH_RESEND_API_KEY} }
90
+ user: ops@acme.com
91
+ clientEmail: ${AUTH_GMAIL_CLIENT_EMAIL}
92
+ privateKey: ${AUTH_GMAIL_PRIVATE_KEY}
93
+
94
+ permissions: # what the app gates: id: label (the rows on /auth/admin › Roles)
95
+ invoices.read: See invoices
96
+ invoices.create: Create invoices
97
+ reports.read: See reports
98
+
99
+ roles: # the starting point, written into the database once; /auth/admin decides after
100
+ owner: all # owner always holds everything
101
+ admin: invoices.* reports.*
102
+ member: invoices.read
103
+ ```
104
+
105
+ No arrays: a list is words separated by spaces. Changes are picked up on the next request in development; restart in production.
106
+
107
+ ## Your own sign-in page
108
+
109
+ ```yaml
110
+ pages:
111
+ signIn: /sign-in # the app's own page (either, or both)
112
+ signUp: /sign-up
113
+ ```
114
+
115
+ With that, every redirect the package makes (the proxy, sign-out, a verified email, "create the first account") lands on the app's page, `GET /auth/sign-in` redirects there with the same query, and the page is always open. The protocol stays with the package: the page calls `signIn({ email, password, next })` / `signUp({ email, password, invite?, next })` from `@binary1702/auth/react` (JSON POSTs to `/auth/sign-in` and `/auth/sign-up`) and navigates to `r.next` on `ok`, or shows `r.error`. `fetchPolicy()` (`GET /auth/policy`) says whether sign-up is allowed, whether an invite code is needed, and whether this is the first account (`signup.reason === "first"`: the sign-in page should send the visitor to sign-up). The query the page may receive: `next`, `error` (`1` = wrong email or password, otherwise the message), `verify=1`, `verified=1`.
116
+
117
+ ## URLs the package serves
118
+
119
+ | Pages (HTML, for a person) | API (JSON, for code) |
120
+ |---|---|
121
+ | `GET /auth/sign-in`, `/auth/sign-up`, `/auth/forgot`, `/auth/reset?token=`, `/auth/invite?token=`, `/auth/verify?token=`, `/auth/admin` | `GET /auth/session` → the shape or 401 |
122
+ | `GET /auth/sign-out` clears the cookie, lands on `?next=` (same origin) or sign-in | `POST /auth/session { organization }` switches the active organization (membership checked) |
123
+ | | `GET /auth/policy` → `{ signup, pages, name }` for an app's own sign-in / sign-up page |
124
+ | | `POST /auth/sign-in`, `/auth/sign-up` (answer `{ user, next }` + cookie), `/auth/sign-out`, `/auth/forgot`, `/auth/reset`, `/auth/verify/resend` |
125
+
126
+ Every POST refuses cross-site requests with 403.
127
+
128
+ ## Managing users and organizations (a page or the terminal, not code)
129
+
130
+ `/auth/admin` does all of the below from the browser, for emails listed under `admins:` in `b17_auth.yaml`. It assigns roles; it never edits the yaml, where roles get their meaning.
131
+
132
+ ```bash
133
+ npx @binary1702/auth add-user [email]
134
+ npx @binary1702/auth set-password [email] # also ends their sessions
135
+ npx @binary1702/auth remove-user [email]
136
+ npx @binary1702/auth sign-out-everywhere [email]
137
+ npx @binary1702/auth list-users
138
+ npx @binary1702/auth add-org <name>
139
+ npx @binary1702/auth add-member <email> <org> [role] # role defaults to "member"
140
+ npx @binary1702/auth remove-member <email> <org>
141
+ npx @binary1702/auth list-orgs
142
+ ```
143
+
144
+ ## When something is wrong
145
+
146
+ | Symptom | Cause | Fix |
147
+ |---|---|---|
148
+ | `getSession()` is null but the user just signed in | password changed, `sign-out-everywhere`, or `requireVerified` is on and the email is not verified | sign out and in; or verify the email |
149
+ | 403 from `requirePermission` for a role that should have it | the role's row in the database (Roles on /auth/admin, or the organization's own rule) does not tick that permission; a permission added to the yaml later starts off | tick it on /auth/admin; owner and admins always have everything |
150
+ | `permissions: now lists what the app gates…` at startup | the yaml still has the pre-0.4.0 role map under `permissions:` | rename that section to `roles:`; declare the catalogue under `permissions:` |
151
+ | `AUTH_SECRET is missing` / `DATABASE_URL is missing` | server started before `.env.local`, or the yaml references a name that is not set | check `.env.local`, restart |
152
+ | `b17_auth.yaml line N: …` at startup | a syntax mistake in the file | the message says which line |
153
+ | a page redirects to sign-in that should not | it matches `protected:` (or is not in `public:`); or Next 15 edge middleware cannot read the file | adjust the list; on Next 15 set `runtime: "nodejs"` in the middleware config |
154
+ | emails print to the terminal instead of sending | no `email.gmail` / `email.resend` in the yaml | configure one; see README "Email" |
155
+ | `429 Too many attempts` while testing sign-in | the rate limit: 10 wrong tries per email or 20 per IP in 15 minutes | wait for `Retry-After`, or test with another address |
package/CHANGELOG.md ADDED
@@ -0,0 +1,49 @@
1
+ # Changelog
2
+
3
+ ## Unreleased (0.4.0)
4
+
5
+ ### Added
6
+ - `pages:` in `b17_auth.yaml` (`signIn: /sign-in`, `signUp: /sign-up`; env `AUTH_SIGN_IN_PAGE` / `AUTH_SIGN_UP_PAGE`): the app serves its own sign-in / sign-up page in its own design. Every redirect the package makes lands there, `GET /auth/sign-in` and `/auth/sign-up` redirect there with the same query, and the page is always open to the proxy. The POST protocol does not move.
7
+ - `@binary1702/auth/react`: `signIn()`, `signUp()`, `signOut()`, `fetchPolicy()`, the protocol as functions for such a page, with one result shape (`AuthResult`).
8
+ - `GET /auth/policy`: `{ signup: { allowed, inviteRequired, reason }, pages, name }`, what a sign-in / sign-up page needs before it renders.
9
+ - `POST /auth/sign-in` and `/auth/sign-up` JSON answers carry `next`, the server-checked same-origin destination.
10
+ - `home:` in `b17_auth.yaml` (env `AUTH_HOME`): the signed-in home. Sign-in lands there when nothing says otherwise, the admin page's "app" link points there, and the proxy sends a signed-in visitor who opens `/` there, so `/` can stay the public site. `GET /auth/policy` returns it as `home`.
11
+ - `/auth/sign-out?next=<path>`: where to land after signing out (same origin), e.g. `?next=/` for the public front.
12
+ - **Permissions are decided in the database, on /auth/admin.** A `roles` table (migration 8) holds what each role may do: for every organization (scope `""`) or for one organization (its id); the most specific rule wins, and nothing saved means nothing allowed. The session query joins it, so a change is live on the next request. `/auth/admin › Roles` is a grid: one row per permission the app declares, one checkbox per role, Save per role; each organization's page has the same grid for its own rule ("follows Roles" until saved). The `owner` role is fixed at everything and cannot be edited. An organization's owner may open their organization on /auth/admin and manage its roles and members; everything else stays with `admins:`.
13
+ - `b17_auth.yaml`: `permissions:` now declares what the app gates (`id: label`, the grid's rows); `roles:` is the starting point, written into the database once (at install, or on first use) and restorable per role with "defaults". `organizations:` is gone (set an organization's own rule on its page). A file still holding the old role map under `permissions:` fails at startup with the fix spelled out. Env: `AUTH_ROLES` (JSON) replaces `AUTH_PERMISSIONS`, which still works.
14
+ - `@binary1702/auth/react`: `useAuth().can(permission)` for showing and hiding UI.
15
+ - Two ways to add a user on `/auth/admin`: **Register** (email + password: ready and verified) and **Invite** (email only: the account gets a random password and one link, `/auth/invite?token=…`, good once for seven days; the person chooses a password, which verifies the address, and is signed in). "invite" on an unverified user sends the link again. `invite` is a token kind; `POST /auth/invite` is the reset flow for it.
16
+ - The admin page is honest about email: a banner on every tab when no transport is configured, and "email" / "invite" say the link was printed to the dev terminal rather than claiming a send. Either way the one-time link just issued is shown on the page with a copy button, so an admin can hand it over directly (only this site's own /auth/ links are ever shown; the query is not trusted).
17
+
18
+ ### Changed
19
+ - `permissionsFor(role, stored)` and `buildSession(row, { admins })`: the yaml no longer takes part in the decision. An email under `admins:` holds every permission.
20
+
21
+ ## 0.3.0 (2026-10-22)
22
+
23
+ Nothing to change by hand when upgrading from 0.2.x: migrations run on first start, the old cookie name is still read, tokens from before the cutoff are accepted, and password hashes upgrade themselves at sign-in. Run `npx @binary1702/auth` once to get `b17_auth.yaml` written.
24
+
25
+ ### Added
26
+ - `b17_auth.yaml`: one settings file (name, sign-up policy, protected paths, admins, email, roles → permissions, per-organization overrides) with `${NAME}` references into `.env.local`. A dependency-free YAML subset. Secrets must be references; the CLI refuses a literal.
27
+ - The session contract: `GET /auth/session` and `getSession()` return `{ user, organization, authorization }`; `requireSession()`, `requirePermission()`; `POST /auth/session` switches the active organization after a membership check.
28
+ - Organizations and roles: `orgs`, `members (org_id, user_id, role)`; CLI `add-org`, `add-member`, `remove-member`, `list-orgs`.
29
+ - `@binary1702/auth/react`: `AuthProvider` and `useAuth()`.
30
+ - Email verification: verify links on sign-up, `/auth/verify`, `POST /auth/verify/resend`, `requireVerified`.
31
+ - Password reset: `/auth/forgot`, `/auth/reset`.
32
+ - Email over `fetch`: the Gmail API (service account with domain-wide delegation, or OAuth refresh token) or Resend; console in development.
33
+ - One-time tokens (`tokens`): only the SHA-256 is stored; a link works once.
34
+ - Rate limiting (`rate_limits`): sign-in 20 per IP and 10 per email per 15 min, sign-up 10 per IP an hour, reset and verification emails 3 per address an hour; `429` with `Retry-After`.
35
+ - `/auth/admin`: users, organizations and roles from the browser, for emails under `admins:`.
36
+ - `AGENTS.md` for agents working inside an app that has the package; `PROMPT.md` updated for the installer.
37
+
38
+ ### Changed
39
+ - Tables are `users`, `orgs`, `members`, `tokens`, `rate_limits` and `auth_migrations`; a migration renames the old `auth_*` tables. A fresh install refuses a database whose `users` table is not the package's.
40
+ - `GET /auth/session` answers `401` when signed out (was `200 { user: null }`).
41
+ - scrypt N is 2^17 (was 2^14); rows re-hash at the next sign-in.
42
+ - The session token carries `iss` and `aud`; tokens issued after 2026-10-22 without them are refused.
43
+ - The production cookie is `__Host-auth_session`.
44
+ - Every auth page is served with `Cache-Control: no-store`, `X-Frame-Options: DENY`, `Referrer-Policy: same-origin` and a no-script `Content-Security-Policy`.
45
+ - Passwords are capped at 256 characters; the invite code is compared in constant time; emails are unique case-insensitively; a sign-up race ends in `409`.
46
+ - Schema changes go through `auth_migrations`.
47
+
48
+ ## 0.2.5
49
+ - Open redirect, unguarded paths and cross-site POSTs fixed; sessions revoked on user deletion or password change.
package/INSTALL.md CHANGED
@@ -6,55 +6,66 @@ Inside a Next.js project (App Router):
6
6
  npx @binary1702/auth
7
7
  ```
8
8
 
9
- That is the whole install. It asks for your Postgres connection string once, then does everything else:
9
+ That is the whole install. It asks for your Postgres connection string once, offers to set up email (Enter to skip), then does everything else:
10
10
 
11
11
  ```
12
12
  ✓ @binary1702/auth installed
13
13
  ✓ DATABASE_URL → .env.local
14
14
  ✓ AUTH_SECRET generated → .env.local
15
+ · email not configured: dev prints emails to the console
15
16
  ✓ app/auth/[...path]/route.ts
16
17
  ✓ proxy.ts
17
- ✓ auth_users table ready
18
- ✓ user "ben" created
18
+ ✓ b17_auth.yaml written: app name, sign-up policy, email, permissions
19
+ ✓ database schema ready (4 migrations)
20
+ ✓ user "ben@example.com" created
19
21
  ```
20
22
 
21
23
  Safe to run again. It only does what is left.
22
24
 
23
25
  ## Use it
24
26
 
25
- Every page and API route is now behind sign-in. Get the user anywhere on the server:
27
+ Every page and API route is now behind sign-in. Get the session anywhere on the server:
26
28
 
27
29
  ```ts
28
- import { requireUser } from "@binary1702/auth/next"
30
+ import { getSession, requireSession, requirePermission } from "@binary1702/auth/next"
29
31
 
30
- const user = await requireUser() // { id, email } or throws 401
32
+ const session = await getSession() // { user, organization, authorization } or null
33
+ const session = await requireSession() // same, or throws 401
34
+ const session = await requirePermission("invoices.create") // same, or throws 401 / 403
31
35
  ```
32
36
 
37
+ In React: `<AuthProvider session={await getSession()}>` in the root layout, then `useAuth()` in any client component.
38
+
39
+ ## The two files you edit
40
+
41
+ **`b17_auth.yaml`** in the project root: app name, who may sign up, which paths need sign-in, email, roles → permissions. No secrets.
42
+
43
+ **`.env.local`**: `DATABASE_URL`, `AUTH_SECRET`, and the email key. The yaml references them as `${NAME}`.
44
+
33
45
  ## URLs you get
34
46
 
35
47
  | URL | |
36
48
  |---|---|
37
49
  | `/auth/sign-in` | sign-in page |
50
+ | `/auth/sign-up` | registration, when `signup:` allows it |
51
+ | `/auth/forgot` | password reset by email |
38
52
  | `/auth/sign-out` | clears the session |
39
- | `/auth/session` | `{ user }` as JSON, for client code |
53
+ | `/auth/session` | `{ user, organization, authorization }` as JSON, or 401 |
54
+ | `/auth/admin` | users, organizations, roles, for emails under `admins:` in `b17_auth.yaml` |
40
55
 
41
56
  ## More users
42
57
 
43
- From the terminal:
44
-
45
58
  ```bash
46
59
  npx @binary1702/auth add-user
47
60
  npx @binary1702/auth set-password # also ends their sessions
48
61
  npx @binary1702/auth remove-user
49
62
  npx @binary1702/auth sign-out-everywhere
50
63
  npx @binary1702/auth list-users
64
+ npx @binary1702/auth add-org <name>
65
+ npx @binary1702/auth add-member <email> <org> [role]
66
+ npx @binary1702/auth list-orgs
51
67
  ```
52
68
 
53
- Or let people register themselves at `/auth/sign-up`. Add to `.env.local`:
54
-
55
- ```
56
- AUTH_SIGNUP=open # anyone can create an account
57
- AUTH_INVITE_CODE=secret # optional: the form then asks for this code
58
- ```
69
+ Or let people register themselves: `signup: open` (or `invite`) in `b17_auth.yaml`. Without it, sign-up is closed, except on a fresh install with zero users, where `/auth/sign-in` takes you to create the first account.
59
70
 
60
- Without `AUTH_SIGNUP`, sign-up is closed. The one exception is a fresh install with zero users, where `/auth/sign-in` takes you to create the first account.
71
+ Working in the app with an AI agent? Point it at `AGENTS.md`. Installing with one? `PROMPT.md`.