@ingram-tech/nk-auth 0.11.0 → 0.12.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/README.md +40 -54
- package/dist/gating-internals.d.ts +5 -0
- package/dist/gating-internals.d.ts.map +1 -1
- package/dist/gating-internals.js +8 -0
- package/dist/gating-internals.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/jwt.d.ts.map +1 -1
- package/dist/jwt.js +17 -1
- package/dist/jwt.js.map +1 -1
- package/dist/keys.d.ts +15 -0
- package/dist/keys.d.ts.map +1 -1
- package/dist/keys.js +36 -4
- package/dist/keys.js.map +1 -1
- package/dist/middleware.d.ts.map +1 -1
- package/dist/middleware.js +15 -3
- package/dist/middleware.js.map +1 -1
- package/migrations/0001_better_auth.sql +0 -2
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -11,9 +11,10 @@ exactly one Better Auth copy in the app.
|
|
|
11
11
|
|
|
12
12
|
| Export (subpath) | For |
|
|
13
13
|
| --- | --- |
|
|
14
|
+
| `authEnv`, `authSecret`, `isConfigured` (`./`) | env validation — `authEnv()` returns the full `{ secret, baseURL, databaseUrl }` bundle; `authSecret()` resolves just the secret (with the prod/dev rule) for sites that derive their own `baseURL` / DB connection |
|
|
14
15
|
| `backendJwtOptions` / `verifyBackendJwt` (`./jwt`) | a JWT for the site's own backend API (custom `audience`) |
|
|
15
16
|
| `nkOrganizationDefaults`, `lastActiveOrganizationHooks`, `lastActiveOrganizationUserField` (`./organization`) | org-plugin defaults + active-org restore/persist |
|
|
16
|
-
| `createAuthPool` (`./pool`) | `
|
|
17
|
+
| `createAuthPool` (`./pool`) | **deprecated** — alias of `createPool` from [`@ingram-tech/nk-db`](../nk-db); inject the site's shared pool instead |
|
|
17
18
|
| `makeEmailSenders`, `makePasskeyOptions`, `passkeyOptionsForBaseUrl`, `uuidGenerateId` (`./`) | email hooks, passkeys (`passkeyOptionsForBaseUrl` derives `rpID`/`origin` from a single base URL), UUID ids |
|
|
18
19
|
| `bcryptPassword` (`./`) | **legacy only** — bcrypt verifier for sites with pre-existing bcrypt hashes. New sites omit it (Better Auth defaults to scrypt). See [Migrating bcrypt passwords to scrypt](#migrating-bcrypt-passwords-to-scrypt) |
|
|
19
20
|
| `createAuthHelpers`, `safeNext` (`./server`) | validated App Router session helpers (`getSession` / `getUser` / `requireSession` / `requireUser` / `redirectIfAuthenticated`) with automatic `next` + stale-cookie signalling; `safeNext` validates a `?next=` param |
|
|
@@ -24,15 +25,11 @@ exactly one Better Auth copy in the app.
|
|
|
24
25
|
> `withRls` / `withRlsTransaction` (claims taken from the Better Auth session — no
|
|
25
26
|
> JWT minting, no REST proxy).
|
|
26
27
|
>
|
|
27
|
-
> **Backend-JWT + org sites** (a backend API plus the org plugin): compose
|
|
28
|
-
> `backendJwtOptions({ audience })`,
|
|
29
|
-
> `lastActiveOrganizationHooks(pool)` in your
|
|
30
|
-
> tokens with `verifyBackendJwt`. Keep
|
|
31
|
-
> permissions/roles, connectors) in the app.
|
|
32
|
-
>
|
|
33
|
-
> **Note:** pin `kysely@0.28.x` in the consuming app (0.29 moved
|
|
34
|
-
> `DEFAULT_MIGRATION_TABLE` out of its barrel, breaking the adapter + the
|
|
35
|
-
> Turbopack build).
|
|
28
|
+
> **Backend-JWT + org sites** (a backend API plus the org plugin): compose the
|
|
29
|
+
> site's shared nk-db pool, `backendJwtOptions({ audience })`,
|
|
30
|
+
> `nkOrganizationDefaults`, and `lastActiveOrganizationHooks(pool)` in your
|
|
31
|
+
> `betterAuth()`; verify backend tokens with `verifyBackendJwt`. Keep
|
|
32
|
+
> app-specific bits (SSO restrictions, permissions/roles, connectors) in the app.
|
|
36
33
|
|
|
37
34
|
## Install
|
|
38
35
|
|
|
@@ -52,6 +49,25 @@ Outside production, `BETTER_AUTH_SECRET` falls back to a well-known insecure
|
|
|
52
49
|
placeholder, so local dev and tests run without setting it (a warning is logged).
|
|
53
50
|
In production it stays required — a missing secret throws at startup.
|
|
54
51
|
|
|
52
|
+
If your site derives its own `baseURL` and opens its own database connection, it
|
|
53
|
+
may not want `authEnv()`'s all-or-nothing bundle (which also requires
|
|
54
|
+
`BETTER_AUTH_URL` and `DATABASE_URL`). Take just the secret — same prod/dev rule —
|
|
55
|
+
with `authSecret()`:
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import { authSecret } from "@ingram-tech/nk-auth";
|
|
59
|
+
|
|
60
|
+
export const auth = betterAuth({
|
|
61
|
+
database: myOwnDirectPool,
|
|
62
|
+
secret: authSecret(), // required in prod, dev placeholder otherwise — owned here
|
|
63
|
+
baseURL: myOwnBaseUrl,
|
|
64
|
+
// ...
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Do this instead of re-implementing the prod-required / dev-placeholder rule in
|
|
69
|
+
the app: the security-sensitive default then lives in exactly one place.
|
|
70
|
+
|
|
55
71
|
## 1. Apply the schema
|
|
56
72
|
|
|
57
73
|
```bash
|
|
@@ -84,7 +100,7 @@ import {
|
|
|
84
100
|
uuidGenerateId,
|
|
85
101
|
} from "@ingram-tech/nk-auth";
|
|
86
102
|
import { betterAuth } from "better-auth";
|
|
87
|
-
import {
|
|
103
|
+
import { pool } from "@/lib/db"; // the ONE shared createPool() from @ingram-tech/nk-db
|
|
88
104
|
|
|
89
105
|
const env = authEnv();
|
|
90
106
|
const email = makeEmailSenders(({ to, subject, url }) =>
|
|
@@ -92,7 +108,7 @@ const email = makeEmailSenders(({ to, subject, url }) =>
|
|
|
92
108
|
);
|
|
93
109
|
|
|
94
110
|
export const auth = betterAuth({
|
|
95
|
-
database:
|
|
111
|
+
database: pool, // inject the shared pool — exactly one pool per process
|
|
96
112
|
secret: env.secret,
|
|
97
113
|
baseURL: env.baseURL,
|
|
98
114
|
basePath: authBasePath, // mount at /auth, not the framework default /api/auth
|
|
@@ -128,8 +144,9 @@ export const auth = betterAuth({
|
|
|
128
144
|
// app/auth/[...all]/route.ts — a standard Next.js route handler.
|
|
129
145
|
// Lives at /auth (set via `basePath: authBasePath`), NOT /api/auth: auth is a
|
|
130
146
|
// user-facing surface (sign-in, OAuth callbacks), not an internal machine API.
|
|
147
|
+
import { toNextJsHandler } from "better-auth/next-js";
|
|
131
148
|
import { auth } from "@/lib/auth";
|
|
132
|
-
export const { GET, POST } = auth
|
|
149
|
+
export const { GET, POST } = toNextJsHandler(auth);
|
|
133
150
|
```
|
|
134
151
|
|
|
135
152
|
## 3. Query data with RLS intact
|
|
@@ -331,45 +348,14 @@ async redirects() {
|
|
|
331
348
|
## Migrating bcrypt passwords to scrypt
|
|
332
349
|
|
|
333
350
|
`bcryptPassword` is **legacy support only** (see its `@deprecated` note). It
|
|
334
|
-
exists so sites whose `account.password` hashes are bcrypt keep verifying.
|
|
335
|
-
Auth's default hasher is **scrypt** (`<salt-hex>:<key-hex>`), and bcrypt
|
|
336
|
-
are trivially distinguishable (they start with `$2a
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
- The full **password-reset flow** — `requestPasswordReset` → email →
|
|
345
|
-
`resetPassword`, which re-hashes with the configured (scrypt) hasher. We
|
|
346
|
-
already wire `sendResetPassword` via `makeEmailSenders`.
|
|
347
|
-
- Admin `setUserPassword` (admin plugin) for out-of-band overrides.
|
|
348
|
-
|
|
349
|
-
**What it does NOT have (we'd build it):**
|
|
350
|
-
|
|
351
|
-
- **Rehash-on-login.** Better Auth never re-hashes a password on successful
|
|
352
|
-
sign-in. There is no `needsRehash`.
|
|
353
|
-
- **A "must reset password" gate.** No native flag blocks sign-in until a user
|
|
354
|
-
resets; that's an extra `user` field + a `before` sign-in hook if you want it.
|
|
355
|
-
|
|
356
|
-
**The plan.** Replace `bcryptPassword` with a **dual-format verifier** (override
|
|
357
|
-
only `verify`, leaving `hash` as the scrypt default): branch on
|
|
358
|
-
`hash.startsWith("$2")` → bcrypt compare, else fall through to Better Auth's
|
|
359
|
-
scrypt verify. Old hashes keep working; every new signup, password change, and
|
|
360
|
-
reset is written as scrypt. Then upgrade existing hashes by one of:
|
|
361
|
-
|
|
362
|
-
1. **Lazy (preferred):** wrap the sign-in route (an nk-auth plugin endpoint or a
|
|
363
|
-
thin site route) so that, after a successful bcrypt verify, it re-hashes the
|
|
364
|
-
submitted plaintext with scrypt and persists it via
|
|
365
|
-
`internalAdapter.updatePassword(userId, newHash)`. This reconstructs the
|
|
366
|
-
rehash-on-login that core lacks, using only supported adapter calls — the
|
|
367
|
-
plaintext is only available here, at the sign-in request.
|
|
368
|
-
2. **Eager:** run a `requestPasswordReset` campaign for all bcrypt users; their
|
|
369
|
-
next reset writes scrypt. Pair with the dual-format verifier as the bridge
|
|
370
|
-
(there's no native "must reset" gate, so un-migrated users still log in via
|
|
371
|
-
bcrypt until they reset).
|
|
372
|
-
|
|
373
|
-
Either way the dual-format verifier is the one piece nk-auth should standardize;
|
|
374
|
-
the forced-reset gate is only worth building if a site needs a hard cutover.
|
|
351
|
+
exists so sites whose `account.password` hashes are bcrypt keep verifying.
|
|
352
|
+
Better Auth's default hasher is **scrypt** (`<salt-hex>:<key-hex>`), and bcrypt
|
|
353
|
+
hashes are trivially distinguishable (they start with `$2a$`/`$2b$`/`$2y$`), so
|
|
354
|
+
the migration path is a **dual-format verifier**: override only
|
|
355
|
+
`emailAndPassword.password.verify` to branch on `hash.startsWith("$2")` →
|
|
356
|
+
bcrypt compare, else Better Auth's scrypt verify. Old hashes keep working;
|
|
357
|
+
every new signup, password change, and reset writes scrypt. (Better Auth has no
|
|
358
|
+
rehash-on-login and no "must reset" gate, so bcrypt hashes only upgrade when
|
|
359
|
+
the user resets — or via a sign-in wrapper that persists a re-hash with
|
|
360
|
+
`internalAdapter.updatePassword`.)
|
|
375
361
|
**Status: proposed** — not yet shipped; `bcryptPassword` remains the stopgap.
|
|
@@ -13,6 +13,11 @@ export declare const NK_AUTH_PATH_HEADER = "x-nk-auth-path";
|
|
|
13
13
|
/**
|
|
14
14
|
* Accept only an internal, non-protocol-relative path as a post-login redirect,
|
|
15
15
|
* so `next` can never be turned into an open redirect to another origin.
|
|
16
|
+
*
|
|
17
|
+
* Beyond the `//` check, backslashes and ASCII controls must also be rejected:
|
|
18
|
+
* browsers treat `\` as `/` in http(s) URLs (so `/\evil.com` resolves to
|
|
19
|
+
* `https://evil.com/`) and the URL parser strips tab/newline (so an encoded
|
|
20
|
+
* `/\t/evil.com` collapses to `//evil.com`).
|
|
16
21
|
*/
|
|
17
22
|
export declare function safeNextParam(value: string | null | undefined): string | null;
|
|
18
23
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gating-internals.d.ts","sourceRoot":"","sources":["../src/gating-internals.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,mBAAmB,CAAC;AAEpD
|
|
1
|
+
{"version":3,"file":"gating-internals.d.ts","sourceRoot":"","sources":["../src/gating-internals.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,mBAAmB,CAAC;AAEpD;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAM7E;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CACxB,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,GAC7C,MAAM,CAOR"}
|
package/dist/gating-internals.js
CHANGED
|
@@ -13,12 +13,20 @@ export const NK_AUTH_PATH_HEADER = "x-nk-auth-path";
|
|
|
13
13
|
/**
|
|
14
14
|
* Accept only an internal, non-protocol-relative path as a post-login redirect,
|
|
15
15
|
* so `next` can never be turned into an open redirect to another origin.
|
|
16
|
+
*
|
|
17
|
+
* Beyond the `//` check, backslashes and ASCII controls must also be rejected:
|
|
18
|
+
* browsers treat `\` as `/` in http(s) URLs (so `/\evil.com` resolves to
|
|
19
|
+
* `https://evil.com/`) and the URL parser strips tab/newline (so an encoded
|
|
20
|
+
* `/\t/evil.com` collapses to `//evil.com`).
|
|
16
21
|
*/
|
|
17
22
|
export function safeNextParam(value) {
|
|
18
23
|
if (!value)
|
|
19
24
|
return null;
|
|
20
25
|
if (!value.startsWith("/") || value.startsWith("//"))
|
|
21
26
|
return null;
|
|
27
|
+
// oxlint-disable-next-line no-control-regex -- rejecting control chars is the point
|
|
28
|
+
if (/[\\\u0000-\u001f\u007f]/.test(value))
|
|
29
|
+
return null;
|
|
22
30
|
return value;
|
|
23
31
|
}
|
|
24
32
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gating-internals.js","sourceRoot":"","sources":["../src/gating-internals.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;;;GAIG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,gBAAgB,CAAC;AAEpD
|
|
1
|
+
{"version":3,"file":"gating-internals.js","sourceRoot":"","sources":["../src/gating-internals.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;;;GAIG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,gBAAgB,CAAC;AAEpD;;;;;;;;GAQG;AACH,MAAM,UAAU,aAAa,CAAC,KAAgC;IAC7D,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IACxB,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IAClE,oFAAoF;IACpF,IAAI,yBAAyB,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACvD,OAAO,KAAK,CAAC;AACd,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,SAAS,CACxB,UAAkB,EAClB,IAA+C;IAE/C,MAAM,MAAM,GAAG,IAAI,eAAe,EAAE,CAAC;IACrC,MAAM,IAAI,GAAG,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACtC,IAAI,IAAI;QAAE,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IACnC,IAAI,IAAI,CAAC,KAAK;QAAE,MAAM,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;IACzC,MAAM,EAAE,GAAG,MAAM,CAAC,QAAQ,EAAE,CAAC;IAC7B,OAAO,EAAE,CAAC,CAAC,CAAC,GAAG,UAAU,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC;AAChD,CAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { type BackendJwtConfig, backendJwtOptions, verifyBackendJwt } from "./jwt.js";
|
|
2
2
|
export { base58Id, fromPrefixedId, toPrefixedId, uuidGenerateId } from "./id.js";
|
|
3
|
-
export { type AuthEnv, authEnv, isConfigured } from "./keys.js";
|
|
3
|
+
export { type AuthEnv, authEnv, authSecret, isConfigured } from "./keys.js";
|
|
4
4
|
export { bcryptPassword, makeEmailSenders, makePasskeyOptions, type PasskeyConfig, passkeyOptionsForBaseUrl, type SendEmail, } from "./options.js";
|
|
5
5
|
export { lastActiveOrganizationHooks, lastActiveOrganizationUserField, nkOrganizationDefaults, } from "./organization.js";
|
|
6
6
|
export { CREDENTIAL_PROVIDER_ID, DEFAULT_MAX_PASSWORD_LENGTH, DEFAULT_MIN_PASSWORD_LENGTH, type PasswordPolicy, passwordSchema, type ResetPasswordError, type ResetPasswordErrorCode, validateNewPassword, } from "./password.js";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,gBAAgB,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AACtF,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AACjF,OAAO,EAAE,KAAK,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,gBAAgB,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AACtF,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AACjF,OAAO,EAAE,KAAK,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC5E,OAAO,EACN,cAAc,EACd,gBAAgB,EAChB,kBAAkB,EAClB,KAAK,aAAa,EAClB,wBAAwB,EACxB,KAAK,SAAS,GACd,MAAM,cAAc,CAAC;AACtB,OAAO,EACN,2BAA2B,EAC3B,+BAA+B,EAC/B,sBAAsB,GACtB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACN,sBAAsB,EACtB,2BAA2B,EAC3B,2BAA2B,EAC3B,KAAK,cAAc,EACnB,cAAc,EACd,KAAK,kBAAkB,EACvB,KAAK,sBAAsB,EAC3B,mBAAmB,GACnB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC1C,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// only what it needs (e.g. avoid bcrypt when it doesn't use passwords).
|
|
5
5
|
export { backendJwtOptions, verifyBackendJwt } from "./jwt.js";
|
|
6
6
|
export { base58Id, fromPrefixedId, toPrefixedId, uuidGenerateId } from "./id.js";
|
|
7
|
-
export { authEnv, isConfigured } from "./keys.js";
|
|
7
|
+
export { authEnv, authSecret, isConfigured } from "./keys.js";
|
|
8
8
|
export { bcryptPassword, makeEmailSenders, makePasskeyOptions, passkeyOptionsForBaseUrl, } from "./options.js";
|
|
9
9
|
export { lastActiveOrganizationHooks, lastActiveOrganizationUserField, nkOrganizationDefaults, } from "./organization.js";
|
|
10
10
|
export { CREDENTIAL_PROVIDER_ID, DEFAULT_MAX_PASSWORD_LENGTH, DEFAULT_MIN_PASSWORD_LENGTH, passwordSchema, validateNewPassword, } from "./password.js";
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,+EAA+E;AAC/E,4EAA4E;AAC5E,wEAAwE;AAExE,OAAO,EAAyB,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AACtF,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AACjF,OAAO,EAAgB,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,+EAA+E;AAC/E,4EAA4E;AAC5E,wEAAwE;AAExE,OAAO,EAAyB,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AACtF,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AACjF,OAAO,EAAgB,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC5E,OAAO,EACN,cAAc,EACd,gBAAgB,EAChB,kBAAkB,EAElB,wBAAwB,GAExB,MAAM,cAAc,CAAC;AACtB,OAAO,EACN,2BAA2B,EAC3B,+BAA+B,EAC/B,sBAAsB,GACtB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACN,sBAAsB,EACtB,2BAA2B,EAC3B,2BAA2B,EAE3B,cAAc,EAGd,mBAAmB,GACnB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC1C,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC"}
|
package/dist/jwt.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jwt.d.ts","sourceRoot":"","sources":["../src/jwt.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAGN,KAAK,UAAU,EAEf,MAAM,MAAM,CAAC;AAEd;;;;;GAKG;AAEH,MAAM,WAAW,gBAAgB;IAChC,sEAAsE;IACtE,QAAQ,EAAE,MAAM,CAAC;IACjB,kCAAkC;IAClC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,uBAAuB,CAAC,EAAE,OAAO,CAAC;CAClC;AAED,sEAAsE;AACtE,eAAO,MAAM,iBAAiB,GAAI,QAAQ,gBAAgB,KAAG,UAM3D,CAAC;
|
|
1
|
+
{"version":3,"file":"jwt.d.ts","sourceRoot":"","sources":["../src/jwt.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAGN,KAAK,UAAU,EAEf,MAAM,MAAM,CAAC;AAEd;;;;;GAKG;AAEH,MAAM,WAAW,gBAAgB;IAChC,sEAAsE;IACtE,QAAQ,EAAE,MAAM,CAAC;IACjB,kCAAkC;IAClC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,uBAAuB,CAAC,EAAE,OAAO,CAAC;CAClC;AAED,sEAAsE;AACtE,eAAO,MAAM,iBAAiB,GAAI,QAAQ,gBAAgB,KAAG,UAM3D,CAAC;AAWH;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,gBAAgB,GAAU,QAAQ;IAC9C,KAAK,EAAE,MAAM,CAAC;IACd,2EAA2E;IAC3E,OAAO,EAAE,MAAM,GAAG,GAAG,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CACf,KAAG,OAAO,CAAC,UAAU,CAmCrB,CAAC"}
|
package/dist/jwt.js
CHANGED
|
@@ -8,6 +8,12 @@ export const backendJwtOptions = (config) => ({
|
|
|
8
8
|
},
|
|
9
9
|
});
|
|
10
10
|
const jwksCache = new Map();
|
|
11
|
+
// Forced-reload throttle (per JWKS URL). The no-matching-key path is reachable
|
|
12
|
+
// by anyone who can send a request with a made-up `kid`, and `.reload()`
|
|
13
|
+
// deliberately bypasses jose's cooldown — unthrottled, garbage tokens would
|
|
14
|
+
// each cost a fresh fetch against the auth origin.
|
|
15
|
+
const RELOAD_COOLDOWN_MS = 30_000;
|
|
16
|
+
const lastForcedReload = new Map();
|
|
11
17
|
/**
|
|
12
18
|
* Verify a Better-Auth-minted backend JWT (EdDSA) against the issuer's JWKS.
|
|
13
19
|
* For use by the backend that consumes `backendJwtOptions` tokens. Throws on an
|
|
@@ -33,6 +39,9 @@ export const verifyBackendJwt = async (params) => {
|
|
|
33
39
|
audience: params.audience,
|
|
34
40
|
issuer: params.issuer,
|
|
35
41
|
algorithms: ["EdDSA"],
|
|
42
|
+
// Tolerate a few seconds of clock skew between the minting and verifying
|
|
43
|
+
// hosts; jose's default of 0 fails legitimate tokens at exp/nbf boundaries.
|
|
44
|
+
clockTolerance: 5,
|
|
36
45
|
};
|
|
37
46
|
try {
|
|
38
47
|
const { payload } = await jwtVerify(params.token, jwks, options);
|
|
@@ -42,7 +51,14 @@ export const verifyBackendJwt = async (params) => {
|
|
|
42
51
|
if (!(err instanceof joseErrors.JWKSNoMatchingKey))
|
|
43
52
|
throw err;
|
|
44
53
|
// `kid` absent from the cached JWKS — almost always a just-rotated signing
|
|
45
|
-
// key. Force a refresh past the cooldown and retry exactly once
|
|
54
|
+
// key. Force a refresh past the cooldown and retry exactly once, at most
|
|
55
|
+
// once per cooldown window so unknown-`kid` garbage can't hammer the
|
|
56
|
+
// issuer.
|
|
57
|
+
const now = Date.now();
|
|
58
|
+
if (now - (lastForcedReload.get(cacheKey) ?? 0) < RELOAD_COOLDOWN_MS) {
|
|
59
|
+
throw err;
|
|
60
|
+
}
|
|
61
|
+
lastForcedReload.set(cacheKey, now);
|
|
46
62
|
await jwks.reload();
|
|
47
63
|
const { payload } = await jwtVerify(params.token, jwks, options);
|
|
48
64
|
return payload;
|
package/dist/jwt.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jwt.js","sourceRoot":"","sources":["../src/jwt.ts"],"names":[],"mappings":"AACA,OAAO,EACN,kBAAkB,EAClB,MAAM,IAAI,UAAU,EAEpB,SAAS,GACT,MAAM,MAAM,CAAC;AAqBd,sEAAsE;AACtE,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,MAAwB,EAAc,EAAE,CAAC,CAAC;IAC3E,uBAAuB,EAAE,MAAM,CAAC,uBAAuB;IACvD,GAAG,EAAE;QACJ,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,cAAc,EAAE,MAAM,CAAC,cAAc;KACrC;CACD,CAAC,CAAC;AAEH,MAAM,SAAS,GAAG,IAAI,GAAG,EAAiD,CAAC;AAE3E;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,KAAK,EAAE,MAMtC,EAAuB,EAAE;IACzB,MAAM,GAAG,GACR,OAAO,MAAM,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC;IAC/E,MAAM,QAAQ,GAAG,GAAG,CAAC,QAAQ,EAAE,CAAC;IAChC,IAAI,IAAI,GAAG,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IACnC,IAAI,CAAC,IAAI,EAAE,CAAC;QACX,IAAI,GAAG,kBAAkB,CAAC,GAAG,CAAC,CAAC;QAC/B,SAAS,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAC/B,CAAC;IACD,MAAM,OAAO,GAAG;QACf,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,UAAU,EAAE,CAAC,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"jwt.js","sourceRoot":"","sources":["../src/jwt.ts"],"names":[],"mappings":"AACA,OAAO,EACN,kBAAkB,EAClB,MAAM,IAAI,UAAU,EAEpB,SAAS,GACT,MAAM,MAAM,CAAC;AAqBd,sEAAsE;AACtE,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,MAAwB,EAAc,EAAE,CAAC,CAAC;IAC3E,uBAAuB,EAAE,MAAM,CAAC,uBAAuB;IACvD,GAAG,EAAE;QACJ,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,cAAc,EAAE,MAAM,CAAC,cAAc;KACrC;CACD,CAAC,CAAC;AAEH,MAAM,SAAS,GAAG,IAAI,GAAG,EAAiD,CAAC;AAE3E,+EAA+E;AAC/E,yEAAyE;AACzE,4EAA4E;AAC5E,mDAAmD;AACnD,MAAM,kBAAkB,GAAG,MAAM,CAAC;AAClC,MAAM,gBAAgB,GAAG,IAAI,GAAG,EAAkB,CAAC;AAEnD;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,KAAK,EAAE,MAMtC,EAAuB,EAAE;IACzB,MAAM,GAAG,GACR,OAAO,MAAM,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC;IAC/E,MAAM,QAAQ,GAAG,GAAG,CAAC,QAAQ,EAAE,CAAC;IAChC,IAAI,IAAI,GAAG,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IACnC,IAAI,CAAC,IAAI,EAAE,CAAC;QACX,IAAI,GAAG,kBAAkB,CAAC,GAAG,CAAC,CAAC;QAC/B,SAAS,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAC/B,CAAC;IACD,MAAM,OAAO,GAAG;QACf,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,UAAU,EAAE,CAAC,OAAO,CAAC;QACrB,yEAAyE;QACzE,4EAA4E;QAC5E,cAAc,EAAE,CAAC;KACjB,CAAC;IACF,IAAI,CAAC;QACJ,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,SAAS,CAAC,MAAM,CAAC,KAAK,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;QACjE,OAAO,OAAO,CAAC;IAChB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACd,IAAI,CAAC,CAAC,GAAG,YAAY,UAAU,CAAC,iBAAiB,CAAC;YAAE,MAAM,GAAG,CAAC;QAC9D,2EAA2E;QAC3E,yEAAyE;QACzE,qEAAqE;QACrE,UAAU;QACV,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,IAAI,GAAG,GAAG,CAAC,gBAAgB,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,GAAG,kBAAkB,EAAE,CAAC;YACtE,MAAM,GAAG,CAAC;QACX,CAAC;QACD,gBAAgB,CAAC,GAAG,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;QACpC,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC;QACpB,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,SAAS,CAAC,MAAM,CAAC,KAAK,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;QACjE,OAAO,OAAO,CAAC;IAChB,CAAC;AACF,CAAC,CAAC"}
|
package/dist/keys.d.ts
CHANGED
|
@@ -16,7 +16,22 @@
|
|
|
16
16
|
* plain `next dev` alike). In production it stays strictly required — a missing
|
|
17
17
|
* secret throws at startup rather than silently signing sessions with a value
|
|
18
18
|
* anyone could guess.
|
|
19
|
+
*
|
|
20
|
+
* Sites that derive their own `baseURL` / database connection can take just the
|
|
21
|
+
* secret's prod/dev rule via `authSecret()` instead of the whole `authEnv()`
|
|
22
|
+
* bundle — so that security-sensitive default is never re-implemented app-side.
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* Resolve just the session-signing secret, applying the same rule `authEnv()`
|
|
26
|
+
* does: strictly required in production, an insecure dev placeholder otherwise.
|
|
27
|
+
*
|
|
28
|
+
* For sites that derive their own `baseURL` and open their own database
|
|
29
|
+
* connection (so they don't want `authEnv()`'s all-or-nothing bundle) but still
|
|
30
|
+
* want nk-auth to own the secret's prod/dev fallback. Reaching for this keeps
|
|
31
|
+
* the security-sensitive default in one place instead of hand-copied into the
|
|
32
|
+
* app. Backed by the same `secretField()` schema as `authEnv()`.
|
|
19
33
|
*/
|
|
34
|
+
export declare const authSecret: () => string;
|
|
20
35
|
export interface AuthEnv {
|
|
21
36
|
secret: string;
|
|
22
37
|
baseURL: string;
|
package/dist/keys.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"keys.d.ts","sourceRoot":"","sources":["../src/keys.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"keys.d.ts","sourceRoot":"","sources":["../src/keys.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AA2CH;;;;;;;;;GASG;AACH,eAAO,MAAM,UAAU,QAAO,MAW7B,CAAC;AAEF,MAAM,WAAW,OAAO;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;CACpB;AAED;;;;GAIG;AACH,eAAO,MAAM,OAAO,QAAO,OAe1B,CAAC;AAEF,+EAA+E;AAC/E,eAAO,MAAM,YAAY,QAAO,OAAuD,CAAC"}
|
package/dist/keys.js
CHANGED
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
* plain `next dev` alike). In production it stays strictly required — a missing
|
|
17
17
|
* secret throws at startup rather than silently signing sessions with a value
|
|
18
18
|
* anyone could guess.
|
|
19
|
+
*
|
|
20
|
+
* Sites that derive their own `baseURL` / database connection can take just the
|
|
21
|
+
* secret's prod/dev rule via `authSecret()` instead of the whole `authEnv()`
|
|
22
|
+
* bundle — so that security-sensitive default is never re-implemented app-side.
|
|
19
23
|
*/
|
|
20
24
|
import { z } from "zod";
|
|
21
25
|
/**
|
|
@@ -37,6 +41,37 @@ const buildSchema = () => z.object({
|
|
|
37
41
|
DATABASE_URL: z.string().min(1),
|
|
38
42
|
});
|
|
39
43
|
let warnedPlaceholder = false;
|
|
44
|
+
/**
|
|
45
|
+
* Warn once when the insecure dev placeholder is in use, so a missing secret is
|
|
46
|
+
* visible in local logs without nagging on every call. Never fires in
|
|
47
|
+
* production, where `secretField()` makes the secret required.
|
|
48
|
+
*/
|
|
49
|
+
const warnIfPlaceholder = (secret) => {
|
|
50
|
+
if (secret === DEV_SECRET_PLACEHOLDER && !warnedPlaceholder) {
|
|
51
|
+
warnedPlaceholder = true;
|
|
52
|
+
console.warn("@ingram-tech/nk-auth: BETTER_AUTH_SECRET is unset — using an insecure dev placeholder. Set it before deploying.");
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Resolve just the session-signing secret, applying the same rule `authEnv()`
|
|
57
|
+
* does: strictly required in production, an insecure dev placeholder otherwise.
|
|
58
|
+
*
|
|
59
|
+
* For sites that derive their own `baseURL` and open their own database
|
|
60
|
+
* connection (so they don't want `authEnv()`'s all-or-nothing bundle) but still
|
|
61
|
+
* want nk-auth to own the secret's prod/dev fallback. Reaching for this keeps
|
|
62
|
+
* the security-sensitive default in one place instead of hand-copied into the
|
|
63
|
+
* app. Backed by the same `secretField()` schema as `authEnv()`.
|
|
64
|
+
*/
|
|
65
|
+
export const authSecret = () => {
|
|
66
|
+
const result = secretField().safeParse(process.env.BETTER_AUTH_SECRET);
|
|
67
|
+
if (!result.success) {
|
|
68
|
+
// secretField() only rejects a missing/empty secret in production (dev
|
|
69
|
+
// supplies the placeholder default), so that is the sole failure here.
|
|
70
|
+
throw new Error("@ingram-tech/nk-auth: BETTER_AUTH_SECRET is required (an unset secret is only allowed outside production)");
|
|
71
|
+
}
|
|
72
|
+
warnIfPlaceholder(result.data);
|
|
73
|
+
return result.data;
|
|
74
|
+
};
|
|
40
75
|
/**
|
|
41
76
|
* Read and validate all nk-auth env vars at once. Throws a single error listing
|
|
42
77
|
* everything missing/invalid, so a misconfigured site fails fast at startup
|
|
@@ -51,10 +86,7 @@ export const authEnv = () => {
|
|
|
51
86
|
throw new Error(`@ingram-tech/nk-auth: invalid environment — ${issues}`);
|
|
52
87
|
}
|
|
53
88
|
const env = result.data;
|
|
54
|
-
|
|
55
|
-
warnedPlaceholder = true;
|
|
56
|
-
console.warn("@ingram-tech/nk-auth: BETTER_AUTH_SECRET is unset — using an insecure dev placeholder. Set it before deploying.");
|
|
57
|
-
}
|
|
89
|
+
warnIfPlaceholder(env.BETTER_AUTH_SECRET);
|
|
58
90
|
return {
|
|
59
91
|
secret: env.BETTER_AUTH_SECRET,
|
|
60
92
|
baseURL: env.BETTER_AUTH_URL,
|
package/dist/keys.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"keys.js","sourceRoot":"","sources":["../src/keys.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"keys.js","sourceRoot":"","sources":["../src/keys.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;GAGG;AACH,MAAM,sBAAsB,GAAG,gDAAgD,CAAC;AAEhF;;;;GAIG;AACH,MAAM,WAAW,GAAG,GAAG,EAAE,CACxB,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,YAAY;IACpC,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACnB,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,sBAAsB,CAAC,CAAC;AAEtD,MAAM,WAAW,GAAG,GAAG,EAAE,CACxB,CAAC,CAAC,MAAM,CAAC;IACR,kBAAkB,EAAE,WAAW,EAAE;IACjC,eAAe,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE;IACjC,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;CAC/B,CAAC,CAAC;AAEJ,IAAI,iBAAiB,GAAG,KAAK,CAAC;AAE9B;;;;GAIG;AACH,MAAM,iBAAiB,GAAG,CAAC,MAAc,EAAQ,EAAE;IAClD,IAAI,MAAM,KAAK,sBAAsB,IAAI,CAAC,iBAAiB,EAAE,CAAC;QAC7D,iBAAiB,GAAG,IAAI,CAAC;QACzB,OAAO,CAAC,IAAI,CACX,iHAAiH,CACjH,CAAC;IACH,CAAC;AACF,CAAC,CAAC;AAEF;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,GAAW,EAAE;IACtC,MAAM,MAAM,GAAG,WAAW,EAAE,CAAC,SAAS,CAAC,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC;IACvE,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACrB,uEAAuE;QACvE,uEAAuE;QACvE,MAAM,IAAI,KAAK,CACd,2GAA2G,CAC3G,CAAC;IACH,CAAC;IACD,iBAAiB,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAC/B,OAAO,MAAM,CAAC,IAAI,CAAC;AACpB,CAAC,CAAC;AAQF;;;;GAIG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,GAAY,EAAE;IACpC,MAAM,MAAM,GAAG,WAAW,EAAE,CAAC,SAAS,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACpD,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACrB,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM;aAChC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,KAAK,CAAC,OAAO,EAAE,CAAC;aAC3D,IAAI,CAAC,IAAI,CAAC,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,+CAA+C,MAAM,EAAE,CAAC,CAAC;IAC1E,CAAC;IACD,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC;IACxB,iBAAiB,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC;IAC1C,OAAO;QACN,MAAM,EAAE,GAAG,CAAC,kBAAkB;QAC9B,OAAO,EAAE,GAAG,CAAC,eAAe;QAC5B,WAAW,EAAE,GAAG,CAAC,YAAY;KAC7B,CAAC;AACH,CAAC,CAAC;AAEF,+EAA+E;AAC/E,MAAM,CAAC,MAAM,YAAY,GAAG,GAAY,EAAE,CAAC,WAAW,EAAE,CAAC,SAAS,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC"}
|
package/dist/middleware.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":"AA4BA,OAAO,EAAE,KAAK,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAG7D,MAAM,WAAW,oBAAoB;IACpC;;;OAGG;IACH,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,8DAA8D;IAC9D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,sFAAsF;IACtF,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,oBAAoB,IAyBrC,SAAS,WAAW,KAAG,YAAY,
|
|
1
|
+
{"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":"AA4BA,OAAO,EAAE,KAAK,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAG7D,MAAM,WAAW,oBAAoB;IACpC;;;OAGG;IACH,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,8DAA8D;IAC9D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,sFAAsF;IACtF,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,oBAAoB,IAyBrC,SAAS,WAAW,KAAG,YAAY,CAkE9D"}
|
package/dist/middleware.js
CHANGED
|
@@ -56,8 +56,18 @@ export function createAuthMiddleware(config) {
|
|
|
56
56
|
to.searchParams.delete("stale");
|
|
57
57
|
const res = NextResponse.redirect(to);
|
|
58
58
|
for (const cookie of request.cookies.getAll()) {
|
|
59
|
-
if (cookie.name.includes(cookiePrefix))
|
|
60
|
-
|
|
59
|
+
if (cookie.name.includes(cookiePrefix)) {
|
|
60
|
+
// `__Secure-`/`__Host-` cookies: browsers reject the deletion
|
|
61
|
+
// Set-Cookie unless it carries the Secure attribute itself, so a
|
|
62
|
+
// bare delete() would silently leave the dead cookie in place on
|
|
63
|
+
// HTTPS and re-run this handshake on every visit.
|
|
64
|
+
res.cookies.delete({
|
|
65
|
+
name: cookie.name,
|
|
66
|
+
path: "/",
|
|
67
|
+
secure: cookie.name.startsWith("__Secure-") ||
|
|
68
|
+
cookie.name.startsWith("__Host-"),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
61
71
|
}
|
|
62
72
|
return res;
|
|
63
73
|
}
|
|
@@ -65,7 +75,9 @@ export function createAuthMiddleware(config) {
|
|
|
65
75
|
// 2. Unauthenticated (no cookie) on a protected path -> sign in, and
|
|
66
76
|
// remember where they were going.
|
|
67
77
|
if (!hasSessionCookie &&
|
|
68
|
-
|
|
78
|
+
// Segment-boundary match: "/app" must gate "/app" and "/app/x" but not
|
|
79
|
+
// "/application".
|
|
80
|
+
config.protectedPaths.some((p) => path === p || path.startsWith(`${p}/`))) {
|
|
69
81
|
const original = request.nextUrl.pathname + request.nextUrl.search;
|
|
70
82
|
const to = request.nextUrl.clone();
|
|
71
83
|
to.pathname = signInPath;
|
package/dist/middleware.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"middleware.js","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,EAAoB,YAAY,EAAE,MAAM,aAAa,CAAC;AAC7D,OAAO,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AA0B3E,MAAM,UAAU,oBAAoB,CAAC,MAA4B;IAChE,MAAM,UAAU,GAAG,MAAM,CAAC,UAAU,IAAI,QAAQ,CAAC;IACjD,MAAM,cAAc,GAAG,MAAM,CAAC,cAAc,IAAI,EAAE,CAAC;IACnD,MAAM,YAAY,GAAG,MAAM,CAAC,mBAAmB,IAAI,aAAa,CAAC;IAEjE,uEAAuE;IACvE,kEAAkE;IAClE,4EAA4E;IAC5E,qCAAqC;IACrC,IAAI,MAAM,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QACjE,MAAM,IAAI,KAAK,CACd,qCAAqC,UAAU,8FAA8F,CAC7I,CAAC;IACH,CAAC;IACD,IAAI,cAAc,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC;QACzC,MAAM,IAAI,KAAK,CACd,qCAAqC,UAAU,sFAAsF,CACrI,CAAC;IACH,CAAC;IACD,IAAI,cAAc,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,gBAAgB,EAAE,CAAC;QAC3D,MAAM,IAAI,KAAK,CACd,gFAAgF,CAChF,CAAC;IACH,CAAC;IAED,OAAO,SAAS,UAAU,CAAC,OAAoB;QAC9C,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC;QAEtC,2EAA2E;QAC3E,2EAA2E;QAC3E,yEAAyE;QACzE,2EAA2E;QAC3E,IAAI,IAAI,KAAK,UAAU,IAAI,OAAO,CAAC,OAAO,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,KAAK,GAAG,EAAE,CAAC;YAC9E,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;YACnC,EAAE,CAAC,YAAY,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;YAChC,MAAM,GAAG,GAAG,YAAY,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;YACtC,KAAK,MAAM,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC;gBAC/C,IAAI,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"middleware.js","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":"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"}
|
|
@@ -16,8 +16,6 @@
|
|
|
16
16
|
-- which bypasses RLS, so denying the app's RLS role any access here costs
|
|
17
17
|
-- nothing and keeps the auth tables off-limits to user-facing queries.
|
|
18
18
|
|
|
19
|
-
create extension if not exists "pgcrypto" with schema "extensions";
|
|
20
|
-
|
|
21
19
|
create table if not exists "public"."user" (
|
|
22
20
|
"id" text primary key default gen_random_uuid()::text, -- hardening (1)
|
|
23
21
|
"name" text not null,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ingram-tech/nk-auth",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "The Ingram Better Auth foundation: composable presets (org, dual-shape JWT, active-org hooks, pg pool) for Next.js sites.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -60,19 +60,19 @@
|
|
|
60
60
|
"./migrations/*": "./migrations/*"
|
|
61
61
|
},
|
|
62
62
|
"scripts": {
|
|
63
|
-
"build": "tsc -p tsconfig.json",
|
|
63
|
+
"build": "rm -rf dist tsconfig.tsbuildinfo && tsc -p tsconfig.json",
|
|
64
64
|
"type-check": "tsc -p tsconfig.json --noEmit",
|
|
65
65
|
"test": "vitest run"
|
|
66
66
|
},
|
|
67
67
|
"dependencies": {
|
|
68
|
-
"@ingram-tech/nk-db": "^1.
|
|
68
|
+
"@ingram-tech/nk-db": "^1.2.0",
|
|
69
69
|
"bcrypt": "^6.0.0",
|
|
70
70
|
"jose": "^6.2.3",
|
|
71
71
|
"zod": "^4.4.3"
|
|
72
72
|
},
|
|
73
73
|
"peerDependencies": {
|
|
74
|
-
"@better-auth/passkey": "^1.6.
|
|
75
|
-
"better-auth": "^1.6.
|
|
74
|
+
"@better-auth/passkey": "^1.6.15",
|
|
75
|
+
"better-auth": "^1.6.15",
|
|
76
76
|
"next": "^15.0.0 || ^16.0.0",
|
|
77
77
|
"pg": "^8.13.0",
|
|
78
78
|
"react": "^18.0.0 || ^19.0.0"
|
|
@@ -93,7 +93,7 @@
|
|
|
93
93
|
},
|
|
94
94
|
"devDependencies": {
|
|
95
95
|
"@better-auth/passkey": "^1.6.22",
|
|
96
|
-
"@ingram-tech/nk-dev": "0.2.
|
|
96
|
+
"@ingram-tech/nk-dev": "0.2.5",
|
|
97
97
|
"@types/bcrypt": "^6.0.0",
|
|
98
98
|
"@types/node": "^26.0.1",
|
|
99
99
|
"@types/pg": "^8.20.0",
|