@binary1702/auth 0.0.0-stage → 0.1.1

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/INSTALL.md ADDED
@@ -0,0 +1,47 @@
1
+ # Install
2
+
3
+ Inside a Next.js project (App Router):
4
+
5
+ ```bash
6
+ npx @binary1702/auth
7
+ ```
8
+
9
+ That is the whole install. It asks for your Postgres connection string once, then does everything else:
10
+
11
+ ```
12
+ ✓ @binary1702/auth installed
13
+ ✓ DATABASE_URL → .env.local
14
+ ✓ AUTH_SECRET generated → .env.local
15
+ ✓ app/auth/[...path]/route.ts
16
+ ✓ proxy.ts
17
+ ✓ auth_users table ready
18
+ ✓ user "ben" created
19
+ ```
20
+
21
+ Safe to run again. It only does what is left.
22
+
23
+ ## Use it
24
+
25
+ Every page and API route is now behind sign-in. Get the user anywhere on the server:
26
+
27
+ ```ts
28
+ import { requireUser } from "@binary1702/auth/next"
29
+
30
+ const user = await requireUser() // { id, username } or throws 401
31
+ ```
32
+
33
+ ## URLs you get
34
+
35
+ | URL | |
36
+ |---|---|
37
+ | `/auth/sign-in` | sign-in page |
38
+ | `/auth/sign-out` | clears the session |
39
+ | `/auth/session` | `{ user }` as JSON, for client code |
40
+
41
+ ## More users
42
+
43
+ ```bash
44
+ npx @binary1702/auth add-user
45
+ npx @binary1702/auth set-password
46
+ npx @binary1702/auth list-users
47
+ ```
package/PROMPT.md ADDED
@@ -0,0 +1,80 @@
1
+ # Agent prompt: install @binary1702/auth
2
+
3
+ Copy everything below the line into an AI coding agent running inside the Next.js project.
4
+
5
+ ---
6
+
7
+ Install `@binary1702/auth` in this project. It adds username + password auth to a Next.js App Router app. Follow these steps exactly and report the result of each.
8
+
9
+ ## Before you start
10
+
11
+ Check these and stop if any fails:
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.
16
+
17
+ ## Install
18
+
19
+ Run, in the project root:
20
+
21
+ ```bash
22
+ npx @binary1702/auth
23
+ ```
24
+
25
+ It is interactive. It asks for the Postgres connection string, then a username and a password for the first user. If you cannot answer prompts, run it and hand control to me.
26
+
27
+ It does the following, skipping anything already done:
28
+
29
+ ```
30
+ ✓ @binary1702/auth installed → package.json
31
+ ✓ DATABASE_URL → .env.local
32
+ ✓ AUTH_SECRET generated → .env.local
33
+ ✓ 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
36
+ ✓ user "<name>" created
37
+ ```
38
+
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.
40
+
41
+ ## Verify
42
+
43
+ Start the dev server and check with real HTTP requests:
44
+
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 `{ "username", "password" }` | 200 `{ user }` and `Set-Cookie: auth_session=…; HttpOnly` |
51
+ | `GET /auth/session` with that cookie | `{ user: { id, username } }` |
52
+ | `GET /auth/sign-out` | cookie cleared, redirect to sign-in |
53
+
54
+ Then open `/auth/sign-in` in the browser, sign in, confirm you land on `/`.
55
+
56
+ ## Wire it into the code
57
+
58
+ Wherever the app needs the current user on the server:
59
+
60
+ ```ts
61
+ import { requireUser } from "@binary1702/auth/next"
62
+
63
+ const user = await requireUser() // { id, username } or throws AuthError (401)
64
+ ```
65
+
66
+ `getUser()` returns `null` instead of throwing. Both work in route handlers, server components and server actions.
67
+
68
+ For sign-out, link to `/auth/sign-out`. For client code, `fetch("/auth/session")`.
69
+
70
+ Do not build a sign-up page. Users are created with `npx @binary1702/auth add-user`.
71
+
72
+ ## Deploy
73
+
74
+ Copy `DATABASE_URL` and `AUTH_SECRET` from `.env.local` into the host's environment. Nothing else.
75
+
76
+ ## Report
77
+
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.
79
+
80
+ Docs: https://www.npmjs.com/package/@binary1702/auth
package/README.md CHANGED
@@ -1,3 +1,244 @@
1
- # Temporary Holding Version
1
+ # @binary1702/auth
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Username + password auth for Next.js. Plug and play.
4
+
5
+ ```bash
6
+ npx @binary1702/auth
7
+ ```
8
+
9
+ One command. It installs itself, writes two env vars, writes two files, creates the table, creates your first user. Then you move on.
10
+
11
+ Letting an AI agent do the install? Paste [PROMPT.md](./PROMPT.md) into it. Same steps, written for an agent, with the checks to run and what to report back. [INSTALL.md](./INSTALL.md) is the short human version.
12
+
13
+ ---
14
+
15
+ ## Quick start (new project)
16
+
17
+ ```bash
18
+ npx create-next-app my-app
19
+ cd my-app
20
+ npx @binary1702/auth
21
+ ```
22
+
23
+ You will be asked for one thing: your Postgres connection string (Neon). Everything else is automatic.
24
+
25
+ ```
26
+ @binary1702/auth
27
+
28
+ ✓ @binary1702/auth installed
29
+ ✓ DATABASE_URL → .env.local
30
+ ✓ AUTH_SECRET generated → .env.local
31
+ ✓ app/auth/[...path]/route.ts
32
+ ✓ proxy.ts
33
+ ✓ auth_users table ready
34
+
35
+ Create the first user:
36
+ Username: ben
37
+ Password:
38
+ Password again:
39
+ ✓ user "ben" created
40
+
41
+ Done. Start the dev server and open /auth/sign-in.
42
+ ```
43
+
44
+ Run `npm run dev`, open the site, you are redirected to the sign-in page. Sign in. Done.
45
+
46
+ **Safe to re-run.** Each step is skipped if it is already done. Running it again on a finished project just offers to add another user.
47
+
48
+ ---
49
+
50
+ ## Use it in your code
51
+
52
+ Every page and API route is now behind sign-in. To know who the user is:
53
+
54
+ ```ts
55
+ import { requireUser } from "@binary1702/auth/next"
56
+
57
+ export async function GET() {
58
+ const user = await requireUser() // { id, username }
59
+ // ...
60
+ }
61
+ ```
62
+
63
+ That is the only line you ever write by hand.
64
+
65
+ ```ts
66
+ import { getUser } from "@binary1702/auth/next"
67
+
68
+ const user = await getUser() // { id, username } or null, never throws
69
+ ```
70
+
71
+ Works in route handlers, server components, and server actions.
72
+
73
+ ---
74
+
75
+ ## What you get
76
+
77
+ | URL | What it does |
78
+ |---|---|
79
+ | `/auth/sign-in` | The sign-in page. Served by the package, no React, no styling work. |
80
+ | `/auth/sign-out` | Clears the session and sends you back to sign-in. Link to it. |
81
+ | `/auth/session` | `{ user }` or `{ user: null }` as JSON, for client code. |
82
+
83
+ Everything else on the site requires a session. Unauthenticated page requests redirect to `/auth/sign-in?next=<where they were going>`. Unauthenticated `/api/*` requests get a `401` JSON response.
84
+
85
+ ---
86
+
87
+ ## Managing users
88
+
89
+ There is no sign-up page. Users are created from the terminal.
90
+
91
+ ```bash
92
+ npx @binary1702/auth add-user # prompts for username and password
93
+ npx @binary1702/auth add-user gladys # username given
94
+ npx @binary1702/auth set-password ben
95
+ npx @binary1702/auth list-users
96
+ ```
97
+
98
+ ---
99
+
100
+ ## The two files it wrote
101
+
102
+ You do not need to touch these. Here is what they are, for when you are curious.
103
+
104
+ **`app/auth/[...path]/route.ts`** mounts the handlers.
105
+
106
+ ```ts
107
+ export { GET, POST } from "@binary1702/auth/next"
108
+ ```
109
+
110
+ **`proxy.ts`** guards every request. (On Next 15 this file is called `middleware.ts`. Same content.)
111
+
112
+ ```ts
113
+ import { proxy } from "@binary1702/auth/next/proxy"
114
+
115
+ export default proxy
116
+
117
+ export const config = {
118
+ matcher: ["/((?!auth|_next/static|_next/image|favicon.ico|.*\\..*).*)"],
119
+ }
120
+ ```
121
+
122
+ The matcher is inline because Next reads it statically at build time and ignores a re-exported one.
123
+
124
+ If you already had a `proxy.ts`, the CLI leaves it alone and prints the snippet for you to merge.
125
+
126
+ ---
127
+
128
+ ## Env vars
129
+
130
+ | Var | What | Who sets it |
131
+ |---|---|---|
132
+ | `DATABASE_URL` | Postgres connection string | You, once, when the CLI asks |
133
+ | `AUTH_SECRET` | Signs the session cookie | The CLI generates it |
134
+
135
+ Both live in `.env.local`. Copy both to your host (Vercel, etc.) when you deploy.
136
+
137
+ Rotating `AUTH_SECRET` signs everyone out. That is the revoke-all button.
138
+
139
+ ---
140
+
141
+ ## How it works
142
+
143
+ Five primitives around one cookie. Nothing else.
144
+
145
+ ```
146
+ auth_users (id, username, password_hash, created_at) identity one table, owned by the package
147
+ scrypt proof node:crypto, no dependency
148
+ signed JWT in an httpOnly cookie, 7 days session stateless, zero DB reads per request
149
+ proxy.ts guard verifies the cookie signature only
150
+ /auth/sign-in /auth/sign-out /auth/session lifecycle one route file
151
+ ```
152
+
153
+ ```
154
+ Request
155
+ ├─ proxy.ts cookie valid? → continue else → redirect (page) or 401 (api)
156
+ ├─ /auth/sign-in POST username+password → verify hash → set cookie → redirect
157
+ └─ your route requireUser() → reads cookie → { id, username }
158
+ ```
159
+
160
+ **Authentication only.** The package tells you `user.id`. What that user may see or edit is your app's job. Keeping that boundary is what keeps this package small enough to never break.
161
+
162
+ **Trade-off you should know.** The session is a signed token, not a database row. That is why the proxy costs nothing per request. The cost is that one session cannot be revoked before it expires. Change `AUTH_SECRET` to revoke all of them.
163
+
164
+ ---
165
+
166
+ ## API reference
167
+
168
+ ### `@binary1702/auth/next`
169
+
170
+ | Export | |
171
+ |---|---|
172
+ | `GET`, `POST` | Route handlers. Mount at `app/auth/[...path]/route.ts`. |
173
+ | `requireUser()` | `Promise<{ id, username }>`. Throws `AuthError` (status 401) if signed out. |
174
+ | `getUser()` | `Promise<{ id, username } \| null>`. Never throws. |
175
+ | `unauthorized(err)` | Turns an `AuthError` into a 401 JSON response. Rethrows anything else. |
176
+ | `AuthError` | The error class. Has `.status = 401`. |
177
+
178
+ ### `@binary1702/auth/next/proxy`
179
+
180
+ | Export | |
181
+ |---|---|
182
+ | `proxy` | The guard function. Default export too. |
183
+ | `MATCHER` | The matcher array, for reference. Next needs it written inline in your file. |
184
+
185
+ ### `@binary1702/auth` (framework-free core)
186
+
187
+ For building your own handlers or using the primitives outside Next.
188
+
189
+ | Export | |
190
+ |---|---|
191
+ | `hashPassword(pw)`, `verifyPassword(pw, hash)` | scrypt |
192
+ | `signSession(user, secret)`, `verifySession(token, secret)` | JWT HS256 |
193
+ | `ensureSchema()`, `findUser()`, `createUser()`, `countUsers()`, `listUsers()`, `setPassword()` | Database |
194
+ | `signInPage({ error?, next?, title? })` | The HTML form as a string |
195
+ | `COOKIE_NAME`, `SESSION_DAYS`, `SIGN_IN_PATH`, `SIGN_OUT_PATH` | Constants |
196
+
197
+ ---
198
+
199
+ ## Client-side usage
200
+
201
+ No client library. Plain HTTP.
202
+
203
+ ```ts
204
+ // who am I
205
+ const { user } = await fetch("/auth/session").then((r) => r.json())
206
+
207
+ // sign in from a custom form
208
+ await fetch("/auth/sign-in", {
209
+ method: "POST",
210
+ headers: { "content-type": "application/json" },
211
+ body: JSON.stringify({ username, password }),
212
+ }) // 200 { user } or 401 { error }
213
+
214
+ // sign out
215
+ await fetch("/auth/sign-out", { method: "POST" })
216
+ ```
217
+
218
+ Or just link to `/auth/sign-out`.
219
+
220
+ ---
221
+
222
+ ## Custom sign-in page
223
+
224
+ The built-in page is plain HTML with light and dark mode. If you want your own, make any page with a form that POSTs `username` and `password` to `/auth/sign-in`, and put it anywhere under `/auth/` so the proxy lets it through. Optional hidden field `next` for where to go after.
225
+
226
+ ---
227
+
228
+ ## Requirements
229
+
230
+ - Next.js 15 or 16, App Router
231
+ - Node 20+
232
+ - A Postgres database (built for Neon, works with any Postgres that the Neon serverless driver can reach)
233
+
234
+ ---
235
+
236
+ ## Development
237
+
238
+ ```bash
239
+ npm install
240
+ npm test # core primitives, no database needed
241
+ npm run build # tsc → dist/
242
+ ```
243
+
244
+ Release: `npm version <patch|minor|major> && npm publish && git push --follow-tags`.
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,285 @@
1
+ #!/usr/bin/env node
2
+ // ─────────────────────────────────────────────────────────────
3
+ // npx @binary1702/auth
4
+ //
5
+ // One command. Each step is skipped if already done, so it is
6
+ // safe to run again at any time.
7
+ //
8
+ // 1. install the package into the project
9
+ // 2. DATABASE_URL → prompt, write .env.local
10
+ // 3. AUTH_SECRET → generate, write .env.local
11
+ // 4. app/auth/[...path]/route.ts
12
+ // 5. proxy.ts
13
+ // 6. create auth_users table
14
+ // 7. create the first user
15
+ //
16
+ // Subcommands:
17
+ // add-user [name] create another user
18
+ // set-password [name] change a password
19
+ // list-users
20
+ // ─────────────────────────────────────────────────────────────
21
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, appendFileSync } from "node:fs";
22
+ import { join, dirname } from "node:path";
23
+ import { randomBytes } from "node:crypto";
24
+ import { spawnSync } from "node:child_process";
25
+ import { createInterface } from "node:readline";
26
+ import { stdin, stdout } from "node:process";
27
+ const PKG = "@binary1702/auth";
28
+ const cwd = process.cwd();
29
+ // ── output ────────────────────────────────────────────────────
30
+ const c = {
31
+ dim: (s) => `\x1b[2m${s}\x1b[0m`,
32
+ bold: (s) => `\x1b[1m${s}\x1b[0m`,
33
+ green: (s) => `\x1b[32m${s}\x1b[0m`,
34
+ red: (s) => `\x1b[31m${s}\x1b[0m`,
35
+ };
36
+ const done = (msg) => console.log(` ${c.green("✓")} ${msg}`);
37
+ const skip = (msg) => console.log(` ${c.dim("·")} ${c.dim(msg)}`);
38
+ const fail = (msg) => {
39
+ console.error(`\n ${c.red("✗")} ${msg}\n`);
40
+ process.exit(1);
41
+ };
42
+ // ── prompts ───────────────────────────────────────────────────
43
+ // One readline for the whole run, with every incoming line queued.
44
+ // readline emits all buffered lines at once (piped stdin, pasted text),
45
+ // so a question registered after that burst would otherwise see nothing.
46
+ let _rl = null;
47
+ let muted = false;
48
+ let inputClosed = false;
49
+ const lines = [];
50
+ const waiters = [];
51
+ function rl() {
52
+ if (_rl)
53
+ return _rl;
54
+ _rl = createInterface({ input: stdin, output: stdout, terminal: stdin.isTTY ?? false });
55
+ // Echo nothing while a password is being typed (terminal mode only).
56
+ const anyRl = _rl;
57
+ anyRl._writeToOutput = (s) => {
58
+ if (!muted)
59
+ stdout.write(s);
60
+ };
61
+ _rl.on("line", (line) => {
62
+ const w = waiters.shift();
63
+ w ? w(line) : lines.push(line);
64
+ });
65
+ _rl.on("close", () => {
66
+ inputClosed = true;
67
+ while (waiters.length)
68
+ waiters.shift()(null);
69
+ });
70
+ return _rl;
71
+ }
72
+ function closePrompts() {
73
+ _rl?.close();
74
+ _rl = null;
75
+ }
76
+ async function ask(question, opts = {}) {
77
+ rl();
78
+ const suffix = opts.default ? c.dim(` (${opts.default})`) : "";
79
+ stdout.write(` ${question}${suffix}: `);
80
+ if (opts.hidden)
81
+ muted = true;
82
+ const line = lines.length > 0
83
+ ? lines.shift()
84
+ : inputClosed
85
+ ? null
86
+ : await new Promise((res) => waiters.push(res));
87
+ if (opts.hidden) {
88
+ muted = false;
89
+ stdout.write("\n");
90
+ }
91
+ if (line === null)
92
+ return fail("Input ended before all prompts were answered.");
93
+ return opts.hidden ? line : line.trim() || opts.default || "";
94
+ }
95
+ // ── env file ──────────────────────────────────────────────────
96
+ const ENV_FILE = join(cwd, ".env.local");
97
+ function readEnv() {
98
+ const out = {};
99
+ if (!existsSync(ENV_FILE))
100
+ return out;
101
+ for (const line of readFileSync(ENV_FILE, "utf8").split("\n")) {
102
+ const m = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.*)\s*$/);
103
+ if (m)
104
+ out[m[1]] = m[2].replace(/^["']|["']$/g, "");
105
+ }
106
+ return out;
107
+ }
108
+ function writeEnv(key, value) {
109
+ const prefix = existsSync(ENV_FILE) && !readFileSync(ENV_FILE, "utf8").endsWith("\n") && readFileSync(ENV_FILE, "utf8").length > 0 ? "\n" : "";
110
+ appendFileSync(ENV_FILE, `${prefix}${key}=${value}\n`);
111
+ process.env[key] = value;
112
+ }
113
+ // ── files the consumer needs ──────────────────────────────────
114
+ const ROUTE_FILE = `export { GET, POST } from "${PKG}/next"\n`;
115
+ const PROXY_FILE = `import { proxy } from "${PKG}/next/proxy"
116
+
117
+ export default proxy
118
+
119
+ // Everything except the auth pages, Next internals, and static files.
120
+ export const config = {
121
+ matcher: ["/((?!auth|_next/static|_next/image|favicon.ico|.*\\\\..*).*)"],
122
+ }
123
+ `;
124
+ function writeIfMissing(rel, content) {
125
+ const abs = join(cwd, rel);
126
+ if (existsSync(abs)) {
127
+ skip(`${rel} exists`);
128
+ return false;
129
+ }
130
+ mkdirSync(dirname(abs), { recursive: true });
131
+ writeFileSync(abs, content);
132
+ done(rel);
133
+ return true;
134
+ }
135
+ // ── steps ─────────────────────────────────────────────────────
136
+ function stepProject() {
137
+ if (!existsSync(join(cwd, "package.json"))) {
138
+ fail(`No package.json here. Run this inside your Next.js project:\n npx create-next-app my-app && cd my-app && npx ${PKG}`);
139
+ }
140
+ if (existsSync(join(cwd, "src", "app")))
141
+ return { appDir: "src/app", src: true };
142
+ if (existsSync(join(cwd, "app")))
143
+ return { appDir: "app", src: false };
144
+ return fail(`No app/ directory. ${PKG} needs the Next.js App Router.`);
145
+ }
146
+ function stepInstall() {
147
+ if (existsSync(join(cwd, "node_modules", ...PKG.split("/")))) {
148
+ skip(`${PKG} installed`);
149
+ return;
150
+ }
151
+ console.log(` ${c.dim(`npm install ${PKG}`)}`);
152
+ const r = spawnSync("npm", ["install", PKG], { cwd, stdio: "inherit" });
153
+ if (r.status !== 0)
154
+ fail(`npm install ${PKG} failed`);
155
+ done(`${PKG} installed`);
156
+ }
157
+ async function stepEnv() {
158
+ const env = readEnv();
159
+ if (env.DATABASE_URL) {
160
+ process.env.DATABASE_URL = env.DATABASE_URL;
161
+ skip("DATABASE_URL set");
162
+ }
163
+ else {
164
+ const guess = process.env.DATABASE_URL ?? env.NEON_DATABASE_URL ?? process.env.NEON_DATABASE_URL;
165
+ const url = await ask("Postgres connection string (DATABASE_URL)", { default: guess });
166
+ if (!url.startsWith("postgres"))
167
+ fail("That does not look like a postgres:// URL.");
168
+ writeEnv("DATABASE_URL", url);
169
+ done("DATABASE_URL → .env.local");
170
+ }
171
+ if (env.AUTH_SECRET) {
172
+ process.env.AUTH_SECRET = env.AUTH_SECRET;
173
+ skip("AUTH_SECRET set");
174
+ }
175
+ else {
176
+ writeEnv("AUTH_SECRET", randomBytes(32).toString("base64url"));
177
+ done("AUTH_SECRET generated → .env.local");
178
+ }
179
+ }
180
+ function stepFiles(appDir, src) {
181
+ writeIfMissing(`${appDir}/auth/[...path]/route.ts`, ROUTE_FILE);
182
+ const proxyPath = src ? "src/proxy.ts" : "proxy.ts";
183
+ const legacy = [src ? "src/middleware.ts" : "middleware.ts", proxyPath].filter((p) => existsSync(join(cwd, p)));
184
+ if (legacy.length) {
185
+ skip(`${legacy[0]} exists, not touched. Add to it:`);
186
+ console.log(c.dim(PROXY_FILE.replace(/^/gm, " ")));
187
+ return;
188
+ }
189
+ writeIfMissing(proxyPath, PROXY_FILE);
190
+ }
191
+ async function db() {
192
+ // Imported lazily: needs DATABASE_URL in process.env first.
193
+ return import("./core/db.js");
194
+ }
195
+ async function stepSchema() {
196
+ const { ensureSchema } = await db();
197
+ await ensureSchema().catch((e) => fail(`Could not reach the database: ${e.message}`));
198
+ done("auth_users table ready");
199
+ }
200
+ async function createUserInteractive(presetName) {
201
+ const { createUser, findUser } = await db();
202
+ const { hashPassword } = await import("./core/password.js");
203
+ const username = presetName ?? (await ask("Username"));
204
+ if (!username)
205
+ fail("Username is required.");
206
+ if (await findUser(username))
207
+ fail(`User "${username}" already exists.`);
208
+ const password = await ask("Password", { hidden: true });
209
+ if (password.length < 8)
210
+ fail("Password must be at least 8 characters.");
211
+ const again = await ask("Password again", { hidden: true });
212
+ if (password !== again)
213
+ fail("Passwords do not match.");
214
+ await createUser(username, await hashPassword(password));
215
+ done(`user "${username}" created`);
216
+ }
217
+ async function stepFirstUser() {
218
+ const { countUsers } = await db();
219
+ const n = await countUsers();
220
+ if (n > 0) {
221
+ skip(`${n} user${n === 1 ? "" : "s"} exist`);
222
+ return;
223
+ }
224
+ console.log("\n Create the first user:");
225
+ await createUserInteractive();
226
+ }
227
+ function finish() {
228
+ console.log(`
229
+ ${c.bold("Done.")} Start the dev server and open ${c.bold("/auth/sign-in")}.
230
+
231
+ Get the user in any route or server component:
232
+
233
+ import { requireUser } from "${PKG}/next"
234
+ const user = await requireUser() // { id, username }
235
+
236
+ Add users later: npx ${PKG} add-user
237
+ `);
238
+ }
239
+ // ── main ──────────────────────────────────────────────────────
240
+ async function main() {
241
+ const [cmd, arg] = process.argv.slice(2);
242
+ console.log(`\n${c.bold(PKG)}\n`);
243
+ if (cmd === "help" || cmd === "--help" || cmd === "-h") {
244
+ console.log(` npx ${PKG} set up auth in this project (safe to re-run)
245
+ npx ${PKG} add-user [name] create a user
246
+ npx ${PKG} set-password [name]
247
+ npx ${PKG} list-users
248
+ `);
249
+ return;
250
+ }
251
+ stepProject();
252
+ if (cmd === "add-user" || cmd === "set-password" || cmd === "list-users") {
253
+ await stepEnv();
254
+ const d = await db();
255
+ if (cmd === "add-user")
256
+ await createUserInteractive(arg);
257
+ if (cmd === "list-users") {
258
+ for (const u of await d.listUsers())
259
+ console.log(` ${u.username} ${c.dim(u.id)}`);
260
+ }
261
+ if (cmd === "set-password") {
262
+ const username = arg ?? (await ask("Username"));
263
+ const password = await ask("New password", { hidden: true });
264
+ if (password.length < 8)
265
+ fail("Password must be at least 8 characters.");
266
+ const { hashPassword } = await import("./core/password.js");
267
+ const ok = await d.setPassword(username, await hashPassword(password));
268
+ ok ? done(`password updated for "${username}"`) : fail(`No user "${username}".`);
269
+ }
270
+ console.log();
271
+ return;
272
+ }
273
+ if (cmd)
274
+ fail(`Unknown command "${cmd}". Try: npx ${PKG} help`);
275
+ const { appDir, src } = stepProject();
276
+ stepInstall();
277
+ await stepEnv();
278
+ stepFiles(appDir, src);
279
+ await stepSchema();
280
+ await stepFirstUser();
281
+ finish();
282
+ }
283
+ main()
284
+ .catch((e) => fail(e instanceof Error ? e.message : String(e)))
285
+ .finally(closePrompts);
@@ -0,0 +1,7 @@
1
+ export declare const COOKIE_NAME = "auth_session";
2
+ export declare const SESSION_DAYS = 7;
3
+ export declare const SIGN_IN_PATH = "/auth/sign-in";
4
+ export declare const SIGN_OUT_PATH = "/auth/sign-out";
5
+ export declare const TABLE = "auth_users";
6
+ export declare function getSecret(): string;
7
+ export declare function getDatabaseUrl(): string;
@@ -0,0 +1,25 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // Configuration. Two env vars, nothing else.
3
+ //
4
+ // DATABASE_URL Postgres connection string (Neon)
5
+ // AUTH_SECRET signs the session cookie; generated by the CLI
6
+ // ─────────────────────────────────────────────────────────────
7
+ export const COOKIE_NAME = "auth_session";
8
+ export const SESSION_DAYS = 7;
9
+ export const SIGN_IN_PATH = "/auth/sign-in";
10
+ export const SIGN_OUT_PATH = "/auth/sign-out";
11
+ export const TABLE = "auth_users";
12
+ export function getSecret() {
13
+ const secret = process.env.AUTH_SECRET;
14
+ if (!secret || secret.length < 16) {
15
+ throw new Error("AUTH_SECRET is missing. Run: npx @binary1702/auth");
16
+ }
17
+ return secret;
18
+ }
19
+ export function getDatabaseUrl() {
20
+ const url = process.env.DATABASE_URL;
21
+ if (!url) {
22
+ throw new Error("DATABASE_URL is missing. Run: npx @binary1702/auth");
23
+ }
24
+ return url;
25
+ }
@@ -0,0 +1,16 @@
1
+ import { TABLE } from "./config.ts";
2
+ import type { User } from "./session.ts";
3
+ export declare function ensureSchema(): Promise<void>;
4
+ type Row = {
5
+ id: string;
6
+ username: string;
7
+ password_hash: string;
8
+ };
9
+ export declare function findUser(username: string): Promise<Row | null>;
10
+ export declare function createUser(username: string, passwordHash: string): Promise<User>;
11
+ export declare function countUsers(): Promise<number>;
12
+ export declare function listUsers(): Promise<(User & {
13
+ created_at: string;
14
+ })[]>;
15
+ export declare function setPassword(username: string, passwordHash: string): Promise<boolean>;
16
+ export { TABLE };
@@ -0,0 +1,65 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // Storage. One table, owned by this package.
3
+ //
4
+ // auth_users (id, username, password_hash, created_at)
5
+ //
6
+ // ensureSchema() is idempotent and memoized per process, so route
7
+ // handlers can call it on every request for free after the first.
8
+ // ─────────────────────────────────────────────────────────────
9
+ import { neon } from "@neondatabase/serverless";
10
+ import { getDatabaseUrl, TABLE } from "./config.js";
11
+ let _sql = null;
12
+ function sql() {
13
+ return (_sql ??= neon(getDatabaseUrl()));
14
+ }
15
+ let _schema = null;
16
+ export function ensureSchema() {
17
+ return (_schema ??= (async () => {
18
+ await sql() `
19
+ CREATE TABLE IF NOT EXISTS auth_users (
20
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
21
+ username TEXT NOT NULL UNIQUE,
22
+ password_hash TEXT NOT NULL,
23
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
24
+ )
25
+ `;
26
+ })().catch((err) => {
27
+ _schema = null; // let the next call retry
28
+ throw err;
29
+ }));
30
+ }
31
+ export async function findUser(username) {
32
+ await ensureSchema();
33
+ const rows = (await sql() `
34
+ SELECT id, username, password_hash FROM auth_users WHERE username = ${username}
35
+ `);
36
+ return rows[0] ?? null;
37
+ }
38
+ export async function createUser(username, passwordHash) {
39
+ await ensureSchema();
40
+ const [row] = (await sql() `
41
+ INSERT INTO auth_users (username, password_hash)
42
+ VALUES (${username}, ${passwordHash})
43
+ RETURNING id, username
44
+ `);
45
+ return row;
46
+ }
47
+ export async function countUsers() {
48
+ await ensureSchema();
49
+ const [row] = (await sql() `SELECT COUNT(*)::int AS n FROM auth_users`);
50
+ return row.n;
51
+ }
52
+ export async function listUsers() {
53
+ await ensureSchema();
54
+ return (await sql() `
55
+ SELECT id, username, created_at FROM auth_users ORDER BY created_at
56
+ `);
57
+ }
58
+ export async function setPassword(username, passwordHash) {
59
+ await ensureSchema();
60
+ const rows = (await sql() `
61
+ UPDATE auth_users SET password_hash = ${passwordHash} WHERE username = ${username} RETURNING id
62
+ `);
63
+ return rows.length > 0;
64
+ }
65
+ export { TABLE };
@@ -0,0 +1,5 @@
1
+ export declare function signInPage(opts?: {
2
+ error?: string;
3
+ next?: string;
4
+ title?: string;
5
+ }): string;
@@ -0,0 +1,43 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // The sign-in page. Plain HTML served by the route handler,
3
+ // so the consumer never builds a form or a page.
4
+ // ─────────────────────────────────────────────────────────────
5
+ import { SIGN_IN_PATH } from "./config.js";
6
+ const escape = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
7
+ export function signInPage(opts = {}) {
8
+ const title = escape(opts.title ?? "Sign in");
9
+ const next = escape(opts.next ?? "/");
10
+ const error = opts.error ? `<p class="error">${escape(opts.error)}</p>` : "";
11
+ return `<!doctype html>
12
+ <html lang="en">
13
+ <head>
14
+ <meta charset="utf-8">
15
+ <meta name="viewport" content="width=device-width, initial-scale=1">
16
+ <title>${title}</title>
17
+ <style>
18
+ :root { color-scheme: light dark; --fg: #111; --bg: #fff; --line: #ddd; --muted: #666; --err: #c0392b; }
19
+ @media (prefers-color-scheme: dark) { :root { --fg: #eee; --bg: #111; --line: #333; --muted: #999; --err: #ff6b6b; } }
20
+ * { box-sizing: border-box; }
21
+ body { margin: 0; min-height: 100vh; display: grid; place-items: center; background: var(--bg); color: var(--fg);
22
+ font: 15px/1.4 system-ui, -apple-system, sans-serif; }
23
+ form { width: 100%; max-width: 320px; padding: 16px; display: grid; gap: 12px; }
24
+ h1 { font-size: 18px; font-weight: 600; margin: 0 0 4px; }
25
+ label { display: grid; gap: 4px; font-size: 13px; color: var(--muted); }
26
+ input { font: inherit; padding: 8px 10px; border: 1px solid var(--line); border-radius: 6px; background: var(--bg); color: var(--fg); }
27
+ input:focus { outline: 2px solid var(--fg); outline-offset: 1px; }
28
+ button { font: inherit; font-weight: 500; padding: 9px; border: 0; border-radius: 6px; background: var(--fg); color: var(--bg); cursor: pointer; }
29
+ .error { margin: 0; font-size: 13px; color: var(--err); }
30
+ </style>
31
+ </head>
32
+ <body>
33
+ <form method="post" action="${SIGN_IN_PATH}">
34
+ <h1>${title}</h1>
35
+ ${error}
36
+ <label>Username <input name="username" autocomplete="username" autofocus required></label>
37
+ <label>Password <input name="password" type="password" autocomplete="current-password" required></label>
38
+ <input type="hidden" name="next" value="${next}">
39
+ <button type="submit">Sign in</button>
40
+ </form>
41
+ </body>
42
+ </html>`;
43
+ }
@@ -0,0 +1,2 @@
1
+ export declare function hashPassword(password: string): Promise<string>;
2
+ export declare function verifyPassword(password: string, stored: string): Promise<boolean>;
@@ -0,0 +1,32 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // Password hashing. scrypt from node:crypto, no dependency.
3
+ //
4
+ // Stored format: scrypt$N$r$p$<salt b64url>$<hash b64url>
5
+ // Parameters live in the string, so they can be raised later
6
+ // without invalidating existing rows.
7
+ // ─────────────────────────────────────────────────────────────
8
+ import { randomBytes, scrypt as scryptCb, timingSafeEqual } from "node:crypto";
9
+ import { promisify } from "node:util";
10
+ const scrypt = promisify(scryptCb);
11
+ const N = 16384;
12
+ const R = 8;
13
+ const P = 1;
14
+ const KEYLEN = 64;
15
+ function run(password, salt, n, r, p) {
16
+ return scrypt(password, salt, KEYLEN, { N: n, r, p, maxmem: 256 * n * r });
17
+ }
18
+ export async function hashPassword(password) {
19
+ const salt = randomBytes(16);
20
+ const hash = await run(password, salt, N, R, P);
21
+ return ["scrypt", N, R, P, salt.toString("base64url"), hash.toString("base64url")].join("$");
22
+ }
23
+ export async function verifyPassword(password, stored) {
24
+ const parts = stored.split("$");
25
+ if (parts.length !== 6 || parts[0] !== "scrypt")
26
+ return false;
27
+ const [, n, r, p, saltB64, hashB64] = parts;
28
+ const salt = Buffer.from(saltB64, "base64url");
29
+ const expected = Buffer.from(hashB64, "base64url");
30
+ const actual = await run(password, salt, Number(n), Number(r), Number(p));
31
+ return actual.length === expected.length && timingSafeEqual(actual, expected);
32
+ }
@@ -0,0 +1,6 @@
1
+ export type User = {
2
+ id: string;
3
+ username: string;
4
+ };
5
+ export declare function signSession(user: User, secret: string): Promise<string>;
6
+ export declare function verifySession(token: string, secret: string): Promise<User | null>;
@@ -0,0 +1,29 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // Session token. A signed JWT (HS256) carried in an httpOnly cookie.
3
+ //
4
+ // Stateless: verifying needs the secret only, never the database.
5
+ // That is what lets the proxy run on every request at zero cost.
6
+ // Trade-off: a token cannot be revoked before it expires (7 days).
7
+ // ─────────────────────────────────────────────────────────────
8
+ import { SignJWT, jwtVerify } from "jose";
9
+ import { SESSION_DAYS } from "./config.js";
10
+ const key = (secret) => new TextEncoder().encode(secret);
11
+ export async function signSession(user, secret) {
12
+ return new SignJWT({ username: user.username })
13
+ .setProtectedHeader({ alg: "HS256" })
14
+ .setSubject(user.id)
15
+ .setIssuedAt()
16
+ .setExpirationTime(`${SESSION_DAYS}d`)
17
+ .sign(key(secret));
18
+ }
19
+ export async function verifySession(token, secret) {
20
+ try {
21
+ const { payload } = await jwtVerify(token, key(secret), { algorithms: ["HS256"] });
22
+ if (typeof payload.sub !== "string" || typeof payload.username !== "string")
23
+ return null;
24
+ return { id: payload.sub, username: payload.username };
25
+ }
26
+ catch {
27
+ return null;
28
+ }
29
+ }
@@ -0,0 +1,5 @@
1
+ export { hashPassword, verifyPassword } from "./core/password.ts";
2
+ export { signSession, verifySession, type User } from "./core/session.ts";
3
+ export { ensureSchema, findUser, createUser, countUsers, listUsers, setPassword } from "./core/db.ts";
4
+ export { signInPage } from "./core/html.ts";
5
+ export { COOKIE_NAME, SESSION_DAYS, SIGN_IN_PATH, SIGN_OUT_PATH, getSecret, getDatabaseUrl } from "./core/config.ts";
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ // Core primitives. Framework-free. Use these if you are not on Next.js
2
+ // or want to build your own handlers.
3
+ export { hashPassword, verifyPassword } from "./core/password.js";
4
+ export { signSession, verifySession } from "./core/session.js";
5
+ export { ensureSchema, findUser, createUser, countUsers, listUsers, setPassword } from "./core/db.js";
6
+ export { signInPage } from "./core/html.js";
7
+ export { COOKIE_NAME, SESSION_DAYS, SIGN_IN_PATH, SIGN_OUT_PATH, getSecret, getDatabaseUrl } from "./core/config.js";
@@ -0,0 +1,20 @@
1
+ import { NextResponse, type NextRequest } from "next/server.js";
2
+ import { type User } from "../core/session.ts";
3
+ export type { User };
4
+ export declare class AuthError extends Error {
5
+ status: number;
6
+ constructor(message?: string);
7
+ }
8
+ /** Current user from the session cookie, or null. Never touches the database. */
9
+ export declare function getUser(): Promise<User | null>;
10
+ /** Current user, or throw AuthError (status 401). */
11
+ export declare function requireUser(): Promise<User>;
12
+ /** A 401 JSON response for catch blocks: `catch (e) { return unauthorized(e) }` */
13
+ export declare function unauthorized(err?: unknown): NextResponse;
14
+ type Ctx = {
15
+ params: Promise<{
16
+ path: string[];
17
+ }>;
18
+ };
19
+ export declare function GET(req: NextRequest, ctx: Ctx): Promise<NextResponse>;
20
+ export declare function POST(req: NextRequest, ctx: Ctx): Promise<NextResponse>;
@@ -0,0 +1,135 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // Next.js route handlers + helpers.
3
+ //
4
+ // Mount once: app/auth/[...path]/route.ts
5
+ // export { GET, POST } from "@binary1702/auth/next"
6
+ //
7
+ // Serves:
8
+ // GET /auth/sign-in the sign-in page
9
+ // POST /auth/sign-in verify, set cookie, redirect to ?next or /
10
+ // GET /auth/sign-out clear cookie, redirect to sign-in
11
+ // POST /auth/sign-out same (for fetch callers)
12
+ // GET /auth/session { user } or { user: null }
13
+ //
14
+ // Use anywhere on the server:
15
+ // const user = await requireUser() // { id, username } or throws AuthError(401)
16
+ // const user = await getUser() // { id, username } or null
17
+ // ─────────────────────────────────────────────────────────────
18
+ import { cookies } from "next/headers.js";
19
+ import { NextResponse } from "next/server.js";
20
+ import { COOKIE_NAME, SESSION_DAYS, SIGN_IN_PATH, getSecret } from "../core/config.js";
21
+ import { signSession, verifySession } from "../core/session.js";
22
+ import { verifyPassword } from "../core/password.js";
23
+ import { findUser } from "../core/db.js";
24
+ import { signInPage } from "../core/html.js";
25
+ export class AuthError extends Error {
26
+ status = 401;
27
+ constructor(message = "Unauthorized") {
28
+ super(message);
29
+ this.name = "AuthError";
30
+ }
31
+ }
32
+ /** Current user from the session cookie, or null. Never touches the database. */
33
+ export async function getUser() {
34
+ const token = (await cookies()).get(COOKIE_NAME)?.value;
35
+ if (!token)
36
+ return null;
37
+ return verifySession(token, getSecret());
38
+ }
39
+ /** Current user, or throw AuthError (status 401). */
40
+ export async function requireUser() {
41
+ const user = await getUser();
42
+ if (!user)
43
+ throw new AuthError();
44
+ return user;
45
+ }
46
+ /** A 401 JSON response for catch blocks: `catch (e) { return unauthorized(e) }` */
47
+ export function unauthorized(err) {
48
+ if (err && !(err instanceof AuthError))
49
+ throw err;
50
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
51
+ }
52
+ const cookieOptions = () => ({
53
+ httpOnly: true,
54
+ sameSite: "lax",
55
+ secure: process.env.NODE_ENV === "production",
56
+ path: "/",
57
+ maxAge: SESSION_DAYS * 24 * 60 * 60,
58
+ });
59
+ /** Only allow same-origin relative paths as a post-login destination. */
60
+ function safeNext(value) {
61
+ if (!value || !value.startsWith("/") || value.startsWith("//"))
62
+ return "/";
63
+ return value;
64
+ }
65
+ function html(body, status = 200) {
66
+ return new NextResponse(body, { status, headers: { "content-type": "text/html; charset=utf-8" } });
67
+ }
68
+ async function readBody(req) {
69
+ const type = req.headers.get("content-type") ?? "";
70
+ if (type.includes("application/json")) {
71
+ const b = (await req.json().catch(() => ({})));
72
+ return { username: String(b.username ?? ""), password: String(b.password ?? ""), next: safeNext(String(b.next ?? "")), json: true };
73
+ }
74
+ const f = await req.formData();
75
+ return {
76
+ username: String(f.get("username") ?? ""),
77
+ password: String(f.get("password") ?? ""),
78
+ next: safeNext(String(f.get("next") ?? "")),
79
+ json: false,
80
+ };
81
+ }
82
+ function signOutResponse(req, json) {
83
+ const res = json
84
+ ? NextResponse.json({ ok: true })
85
+ : NextResponse.redirect(new URL(SIGN_IN_PATH, req.url), 303);
86
+ res.cookies.set(COOKIE_NAME, "", { ...cookieOptions(), maxAge: 0 });
87
+ return res;
88
+ }
89
+ // ── handlers ──────────────────────────────────────────────────
90
+ export async function GET(req, ctx) {
91
+ const { path } = await ctx.params;
92
+ const route = path.join("/");
93
+ if (route === "sign-in") {
94
+ const next = safeNext(req.nextUrl.searchParams.get("next"));
95
+ const existing = req.cookies.get(COOKIE_NAME)?.value;
96
+ if (existing && (await verifySession(existing, getSecret()))) {
97
+ return NextResponse.redirect(new URL(next, req.url));
98
+ }
99
+ const error = req.nextUrl.searchParams.get("error") ? "Wrong username or password." : undefined;
100
+ return html(signInPage({ next, error }));
101
+ }
102
+ if (route === "sign-out")
103
+ return signOutResponse(req, false);
104
+ if (route === "session") {
105
+ const token = req.cookies.get(COOKIE_NAME)?.value;
106
+ const user = token ? await verifySession(token, getSecret()) : null;
107
+ return NextResponse.json({ user });
108
+ }
109
+ return NextResponse.json({ error: "Not found" }, { status: 404 });
110
+ }
111
+ export async function POST(req, ctx) {
112
+ const { path } = await ctx.params;
113
+ const route = path.join("/");
114
+ if (route === "sign-out")
115
+ return signOutResponse(req, true);
116
+ if (route !== "sign-in")
117
+ return NextResponse.json({ error: "Not found" }, { status: 404 });
118
+ const { username, password, next, json } = await readBody(req);
119
+ const row = username && password ? await findUser(username.trim()) : null;
120
+ const ok = row ? await verifyPassword(password, row.password_hash) : false;
121
+ if (!row || !ok) {
122
+ if (json)
123
+ return NextResponse.json({ error: "Wrong username or password" }, { status: 401 });
124
+ const url = new URL(SIGN_IN_PATH, req.url);
125
+ url.searchParams.set("error", "1");
126
+ if (next !== "/")
127
+ url.searchParams.set("next", next);
128
+ return NextResponse.redirect(url, 303);
129
+ }
130
+ const user = { id: row.id, username: row.username };
131
+ const token = await signSession(user, getSecret());
132
+ const res = json ? NextResponse.json({ user }) : NextResponse.redirect(new URL(next, req.url), 303);
133
+ res.cookies.set(COOKIE_NAME, token, cookieOptions());
134
+ return res;
135
+ }
@@ -0,0 +1,5 @@
1
+ import { NextResponse, type NextRequest } from "next/server.js";
2
+ /** Paste this into proxy.ts as `export const config = { matcher: MATCHER }` */
3
+ export declare const MATCHER: string[];
4
+ export declare function proxy(request: NextRequest): Promise<NextResponse>;
5
+ export default proxy;
@@ -0,0 +1,35 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // Proxy (Next 16) / middleware (Next 15). Runs before every route.
3
+ //
4
+ // Verifies the cookie signature only. No database, no network.
5
+ // no/invalid cookie + page request → 302 to /auth/sign-in?next=<path>
6
+ // no/invalid cookie + /api request → 401 JSON
7
+ // valid cookie → continue
8
+ //
9
+ // The CLI writes proxy.ts with an inline `config.matcher`, because
10
+ // Next reads the matcher statically and ignores a re-exported one.
11
+ // This function also guards those paths itself, so a missing matcher
12
+ // degrades to "slightly slower", never to "static assets blocked".
13
+ // ─────────────────────────────────────────────────────────────
14
+ import { NextResponse } from "next/server.js";
15
+ import { COOKIE_NAME, SIGN_IN_PATH, getSecret } from "../core/config.js";
16
+ import { verifySession } from "../core/session.js";
17
+ /** Paste this into proxy.ts as `export const config = { matcher: MATCHER }` */
18
+ export const MATCHER = ["/((?!auth|_next/static|_next/image|favicon.ico|.*\\..*).*)"];
19
+ const PUBLIC = /^\/(auth(\/|$)|_next\/|favicon\.ico$)|\.[a-z0-9]+$/i;
20
+ export async function proxy(request) {
21
+ const { pathname, search } = request.nextUrl;
22
+ if (PUBLIC.test(pathname))
23
+ return NextResponse.next();
24
+ const token = request.cookies.get(COOKIE_NAME)?.value;
25
+ if (token && (await verifySession(token, getSecret())))
26
+ return NextResponse.next();
27
+ if (pathname.startsWith("/api/")) {
28
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
29
+ }
30
+ const url = new URL(SIGN_IN_PATH, request.url);
31
+ if (pathname !== "/")
32
+ url.searchParams.set("next", pathname + search);
33
+ return NextResponse.redirect(url);
34
+ }
35
+ export default proxy;
package/package.json CHANGED
@@ -1,6 +1,67 @@
1
1
  {
2
2
  "name": "@binary1702/auth",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
4
+ "description": "Username + password auth for Next.js. One command: npx @binary1702/auth",
5
+ "type": "module",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/binary1702/auth.git"
9
+ },
10
+ "homepage": "https://github.com/binary1702/auth#readme",
11
+ "license": "MIT",
12
+ "bin": {
13
+ "binary1702-auth": "dist/cli.js"
14
+ },
15
+ "main": "dist/index.js",
16
+ "types": "dist/index.d.ts",
17
+ "exports": {
18
+ ".": {
19
+ "types": "./dist/index.d.ts",
20
+ "import": "./dist/index.js"
21
+ },
22
+ "./next": {
23
+ "types": "./dist/next/index.d.ts",
24
+ "import": "./dist/next/index.js"
25
+ },
26
+ "./next/proxy": {
27
+ "types": "./dist/next/proxy.d.ts",
28
+ "import": "./dist/next/proxy.js"
29
+ }
30
+ },
31
+ "files": [
32
+ "dist",
33
+ "INSTALL.md",
34
+ "PROMPT.md"
35
+ ],
36
+ "scripts": {
37
+ "build": "tsc",
38
+ "prepublishOnly": "npm run build",
39
+ "test": "node --test tests/*.test.ts"
40
+ },
41
+ "dependencies": {
42
+ "@neondatabase/serverless": "^1.1.0",
43
+ "jose": "^6.2.5"
44
+ },
45
+ "peerDependencies": {
46
+ "next": ">=15.0.0"
47
+ },
48
+ "devDependencies": {
49
+ "@types/node": "^20",
50
+ "next": "^16.3.2",
51
+ "typescript": "^5.9.3"
52
+ },
53
+ "engines": {
54
+ "node": ">=20"
55
+ },
56
+ "keywords": [
57
+ "auth",
58
+ "authentication",
59
+ "nextjs",
60
+ "password",
61
+ "neon",
62
+ "postgres"
63
+ ],
64
+ "publishConfig": {
65
+ "access": "public"
66
+ }
67
+ }