@binary1702/auth 0.2.4 → 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.
- package/AGENTS.md +139 -0
- package/CHANGELOG.md +31 -0
- package/INSTALL.md +30 -17
- package/PROMPT.md +325 -37
- package/README.md +293 -56
- package/dist/cli.js +255 -22
- package/dist/core/admin-html.d.ts +26 -0
- package/dist/core/admin-html.js +252 -0
- package/dist/core/admin.d.ts +26 -0
- package/dist/core/admin.js +48 -0
- package/dist/core/config.d.ts +8 -1
- package/dist/core/config.js +20 -6
- package/dist/core/db.d.ts +138 -7
- package/dist/core/db.js +406 -47
- package/dist/core/email.d.ts +49 -0
- package/dist/core/email.js +213 -0
- package/dist/core/guard.d.ts +49 -0
- package/dist/core/guard.js +103 -0
- package/dist/core/html.d.ts +36 -0
- package/dist/core/html.js +106 -25
- package/dist/core/options.d.ts +76 -0
- package/dist/core/options.js +310 -0
- package/dist/core/org.d.ts +34 -0
- package/dist/core/org.js +55 -0
- package/dist/core/password.d.ts +16 -0
- package/dist/core/password.js +41 -12
- package/dist/core/rate.d.ts +26 -0
- package/dist/core/rate.js +70 -0
- package/dist/core/reset.d.ts +9 -0
- package/dist/core/reset.js +31 -0
- package/dist/core/session.d.ts +23 -2
- package/dist/core/session.js +45 -7
- package/dist/core/signup.d.ts +9 -6
- package/dist/core/signup.js +26 -15
- package/dist/core/tokens.d.ts +29 -0
- package/dist/core/tokens.js +64 -0
- package/dist/core/verify.d.ts +8 -0
- package/dist/core/verify.js +33 -0
- package/dist/core/yaml.d.ts +10 -0
- package/dist/core/yaml.js +85 -0
- package/dist/index.d.ts +17 -6
- package/dist/index.js +17 -6
- package/dist/next/index.d.ts +11 -4
- package/dist/next/index.js +455 -51
- package/dist/next/proxy.d.ts +2 -1
- package/dist/next/proxy.js +16 -7
- package/dist/react/index.d.ts +18 -0
- package/dist/react/index.js +55 -0
- package/package.json +16 -3
package/AGENTS.md
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
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, roles → permissions
|
|
13
|
+
.env.local secrets only (DATABASE_URL, AUTH_SECRET, the email key); the yaml references them as ${NAME}
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Three concepts, one JSON shape, everywhere:
|
|
17
|
+
|
|
18
|
+
```json
|
|
19
|
+
{
|
|
20
|
+
"user": { "id": "4f1c…", "email": "ben@example.com" },
|
|
21
|
+
"organization": { "id": "9a2e…", "name": "Acme" },
|
|
22
|
+
"authorization": { "role": "admin", "permissions": ["invoices.*", "reports.read"] }
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`organization` is `null` when the user is in none; then `role` is `null` and `permissions` is `[]`.
|
|
27
|
+
|
|
28
|
+
## Reading the session
|
|
29
|
+
|
|
30
|
+
| Where | Use | Returns |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| route handler, server component, server action | `await getSession()` | the shape above, or `null` |
|
|
33
|
+
| same, when the caller must be signed in | `await requireSession()` | the shape, or throws `AuthError` (401) |
|
|
34
|
+
| same, when a permission is required | `await requirePermission("invoices.create")` | the shape, or throws `AuthError` (401 signed out, 403 lacking it) |
|
|
35
|
+
| only the user | `await getUser()` / `await requireUser()` | `{ id, email }` |
|
|
36
|
+
| client component | `useAuth()` from `@binary1702/auth/react` | `{ user, organization, authorization, loading, refresh }` |
|
|
37
|
+
| browser code without React | `GET /auth/session` | the shape, or `401` |
|
|
38
|
+
|
|
39
|
+
All imports except the hook come from `@binary1702/auth/next`.
|
|
40
|
+
|
|
41
|
+
**Route handler**
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import { requirePermission, unauthorized } from "@binary1702/auth/next"
|
|
45
|
+
|
|
46
|
+
export async function POST(req: Request) {
|
|
47
|
+
try {
|
|
48
|
+
const { user, organization } = await requirePermission("invoices.create")
|
|
49
|
+
// ... scope every query to organization.id and/or user.id ...
|
|
50
|
+
return Response.json({ ok: true })
|
|
51
|
+
} catch (e) {
|
|
52
|
+
return unauthorized(e) // 401 or 403 JSON for AuthError, rethrows anything else
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**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.
|
|
58
|
+
|
|
59
|
+
**Server action**: call `requireSession()` or `requirePermission()` at the top of every action. Actions can be invoked directly; the page having checked means nothing.
|
|
60
|
+
|
|
61
|
+
**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.
|
|
62
|
+
|
|
63
|
+
## Rules
|
|
64
|
+
|
|
65
|
+
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.
|
|
66
|
+
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.
|
|
67
|
+
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.
|
|
68
|
+
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.
|
|
69
|
+
5. **Permission names are the app's vocabulary.** Pick `area.verb` names (`invoices.create`). `all` grants everything, `invoices.*` everything under that prefix. Define them in `b17_auth.yaml` under `permissions:`; `owner` has everything unless the file says otherwise.
|
|
70
|
+
6. **Do not build sign-in, sign-up, forgot-password or verification pages.** They exist under `/auth/`. Link to them.
|
|
71
|
+
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.
|
|
72
|
+
8. **Do not edit anything in `node_modules/@binary1702/auth`.**
|
|
73
|
+
|
|
74
|
+
## b17_auth.yaml
|
|
75
|
+
|
|
76
|
+
```yaml
|
|
77
|
+
name: Acme Portal # page titles, email subjects
|
|
78
|
+
signup: closed # open | closed | invite (needs inviteCode: ${AUTH_INVITE_CODE})
|
|
79
|
+
protected: /app/* /api/* # only these need sign-in; everything else is open
|
|
80
|
+
|
|
81
|
+
database: ${DATABASE_URL}
|
|
82
|
+
secret: ${AUTH_SECRET}
|
|
83
|
+
|
|
84
|
+
email:
|
|
85
|
+
from: NoReply (Acme) <no-reply@acme.com>
|
|
86
|
+
gmail: # or resend: { apiKey: ${AUTH_RESEND_API_KEY} }
|
|
87
|
+
user: ops@acme.com
|
|
88
|
+
clientEmail: ${AUTH_GMAIL_CLIENT_EMAIL}
|
|
89
|
+
privateKey: ${AUTH_GMAIL_PRIVATE_KEY}
|
|
90
|
+
|
|
91
|
+
permissions: # role: what it may do
|
|
92
|
+
owner: all
|
|
93
|
+
admin: invoices.* reports.*
|
|
94
|
+
member: invoices.read
|
|
95
|
+
|
|
96
|
+
organizations: # per-organization overrides, by name
|
|
97
|
+
Acme:
|
|
98
|
+
member: invoices.read reports.read
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
No arrays: a list is words separated by spaces. Changes are picked up on the next request in development; restart in production.
|
|
102
|
+
|
|
103
|
+
## URLs the package serves
|
|
104
|
+
|
|
105
|
+
| Pages (HTML, for a person) | API (JSON, for code) |
|
|
106
|
+
|---|---|
|
|
107
|
+
| `GET /auth/sign-in`, `/auth/sign-up`, `/auth/forgot`, `/auth/reset?token=`, `/auth/verify?token=`, `/auth/admin` | `GET /auth/session` → the shape or 401 |
|
|
108
|
+
| `GET /auth/sign-out` clears the cookie | `POST /auth/session { organization }` switches the active organization (membership checked) |
|
|
109
|
+
| | `POST /auth/sign-in`, `/auth/sign-up`, `/auth/sign-out`, `/auth/forgot`, `/auth/reset`, `/auth/verify/resend` |
|
|
110
|
+
|
|
111
|
+
Every POST refuses cross-site requests with 403.
|
|
112
|
+
|
|
113
|
+
## Managing users and organizations (a page or the terminal, not code)
|
|
114
|
+
|
|
115
|
+
`/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.
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
npx @binary1702/auth add-user [email]
|
|
119
|
+
npx @binary1702/auth set-password [email] # also ends their sessions
|
|
120
|
+
npx @binary1702/auth remove-user [email]
|
|
121
|
+
npx @binary1702/auth sign-out-everywhere [email]
|
|
122
|
+
npx @binary1702/auth list-users
|
|
123
|
+
npx @binary1702/auth add-org <name>
|
|
124
|
+
npx @binary1702/auth add-member <email> <org> [role] # role defaults to "member"
|
|
125
|
+
npx @binary1702/auth remove-member <email> <org>
|
|
126
|
+
npx @binary1702/auth list-orgs
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## When something is wrong
|
|
130
|
+
|
|
131
|
+
| Symptom | Cause | Fix |
|
|
132
|
+
|---|---|---|
|
|
133
|
+
| `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 |
|
|
134
|
+
| 403 from `requirePermission` for an owner | a `b17_auth.yaml` `permissions.owner` line that narrows it | remove the line; owner has everything by default |
|
|
135
|
+
| `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 |
|
|
136
|
+
| `b17_auth.yaml line N: …` at startup | a syntax mistake in the file | the message says which line |
|
|
137
|
+
| 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 |
|
|
138
|
+
| emails print to the terminal instead of sending | no `email.gmail` / `email.resend` in the yaml | configure one; see README "Email" |
|
|
139
|
+
| `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,31 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.3.0 (2026-10-22)
|
|
4
|
+
|
|
5
|
+
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.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- `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.
|
|
9
|
+
- 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.
|
|
10
|
+
- Organizations and roles: `orgs`, `members (org_id, user_id, role)`; CLI `add-org`, `add-member`, `remove-member`, `list-orgs`.
|
|
11
|
+
- `@binary1702/auth/react`: `AuthProvider` and `useAuth()`.
|
|
12
|
+
- Email verification: verify links on sign-up, `/auth/verify`, `POST /auth/verify/resend`, `requireVerified`.
|
|
13
|
+
- Password reset: `/auth/forgot`, `/auth/reset`.
|
|
14
|
+
- Email over `fetch`: the Gmail API (service account with domain-wide delegation, or OAuth refresh token) or Resend; console in development.
|
|
15
|
+
- One-time tokens (`tokens`): only the SHA-256 is stored; a link works once.
|
|
16
|
+
- 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`.
|
|
17
|
+
- `/auth/admin`: users, organizations and roles from the browser, for emails under `admins:`.
|
|
18
|
+
- `AGENTS.md` for agents working inside an app that has the package; `PROMPT.md` updated for the installer.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- 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.
|
|
22
|
+
- `GET /auth/session` answers `401` when signed out (was `200 { user: null }`).
|
|
23
|
+
- scrypt N is 2^17 (was 2^14); rows re-hash at the next sign-in.
|
|
24
|
+
- The session token carries `iss` and `aud`; tokens issued after 2026-10-22 without them are refused.
|
|
25
|
+
- The production cookie is `__Host-auth_session`.
|
|
26
|
+
- 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`.
|
|
27
|
+
- 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`.
|
|
28
|
+
- Schema changes go through `auth_migrations`.
|
|
29
|
+
|
|
30
|
+
## 0.2.5
|
|
31
|
+
- Open redirect, unguarded paths and cross-site POSTs fixed; sessions revoked on user deletion or password change.
|
package/INSTALL.md
CHANGED
|
@@ -6,53 +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
|
-
✓
|
|
18
|
-
✓
|
|
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
|
|
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 {
|
|
30
|
+
import { getSession, requireSession, requirePermission } from "@binary1702/auth/next"
|
|
29
31
|
|
|
30
|
-
const
|
|
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,
|
|
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
|
-
npx @binary1702/auth set-password
|
|
60
|
+
npx @binary1702/auth set-password # also ends their sessions
|
|
61
|
+
npx @binary1702/auth remove-user
|
|
62
|
+
npx @binary1702/auth sign-out-everywhere
|
|
48
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
|
|
49
67
|
```
|
|
50
68
|
|
|
51
|
-
Or let people register themselves
|
|
52
|
-
|
|
53
|
-
```
|
|
54
|
-
AUTH_SIGNUP=open # anyone can create an account
|
|
55
|
-
AUTH_INVITE_CODE=secret # optional: the form then asks for this code
|
|
56
|
-
```
|
|
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.
|
|
57
70
|
|
|
58
|
-
|
|
71
|
+
Working in the app with an AI agent? Point it at `AGENTS.md`. Installing with one? `PROMPT.md`.
|