@ingram-tech/nk-auth 0.14.0 → 0.15.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 CHANGED
@@ -1,32 +1,28 @@
1
1
  # @ingram-tech/nk-auth
2
2
 
3
- The Ingram **[Better Auth](https://better-auth.com) foundation**: a toolkit of
4
- composable presets each Ingram Next.js site spreads into its *own*
5
- `betterAuth()` call. It is **not** a `betterAuth()` wrapper — the site stays
6
- plain Better Auth (prime directive), keeping full plugin type inference. Import
7
- only what you need from focused subpaths.
3
+ Composable [Better Auth](https://better-auth.com) presets for Next.js apps, spread
4
+ into your *own* `betterAuth()` call. This is not a `betterAuth()` wrapper: your app
5
+ stays plain Better Auth, so plugin type inference survives. Import only what you
6
+ need from focused subpaths.
8
7
 
9
- `better-auth`, `pg`, `@better-auth/passkey` are **peer dependencies** so there's
8
+ `better-auth`, `pg`, `@better-auth/passkey` are peer dependencies, so there is
10
9
  exactly one Better Auth copy in the app.
11
10
 
12
11
  | Export (subpath) | For |
13
12
  | --- | --- |
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 |
15
- | `backendJwtOptions` / `verifyBackendJwt` (`./jwt`) | a JWT for the site's own backend API (custom `audience`; `ids` mints `sub` as the public user id) |
13
+ | `authEnv`, `authSecret`, `isConfigured` (`./`) | env validation — `authEnv()` returns the full `{ secret, baseURL, databaseUrl }` bundle; `authSecret()` resolves just the secret (with the prod/dev rule) for apps that derive their own `baseURL` / DB connection |
14
+ | `backendJwtOptions` / `verifyBackendJwt` (`./jwt`) | a JWT for your own backend API (custom `audience`; `ids` mints `sub` as the public user id) |
16
15
  | `nkOrganizationDefaults`, `lastActiveOrganizationHooks`, `lastActiveOrganizationUserField` (`./organization`) | org-plugin defaults + active-org restore/persist |
17
- | `createAuthPool` (`./pool`) | **deprecated** — alias of `createPool` from [`@ingram-tech/nk-db`](../nk-db); inject the site's shared pool instead |
16
+ | `createAuthPool` (`./pool`) | **deprecated** — alias of `createPool` from [`@ingram-tech/nk-db`](../nk-db); inject your app's shared pool instead |
18
17
  | `makeEmailSenders`, `makePasskeyOptions`, `passkeyOptionsForBaseUrl`, `uuidGenerateId` (`./`) | email hooks, passkeys (`passkeyOptionsForBaseUrl` derives `rpID`/`origin` from a single base URL), UUID ids |
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) |
20
- | `createAuthHelpers`, `safeNext` (`./server`) | validated App Router session helpers (`getSession` / `getUser` / `requireSession` / `requireUser` / `redirectIfAuthenticated`), request-memoized via React `cache()`, with automatic `next` + stale-cookie signalling; `safeNext` validates a `?next=` param |
21
- | `createAuthMiddleware` (`./middleware`) | loop-safe edge middleware: gates unauthenticated users off protected paths, preserves `next`, and clears a stale session cookie so a bad session self-heals |
22
-
23
- > **Data access + RLS** is owned by [`@ingram-tech/nk-db`](../nk-db): query over a
24
- > direct `pg` connection and enforce per-request Row Level Security with
25
- > `withRls` / `withRlsTransaction` (claims taken from the Better Auth session — no
26
- > JWT minting, no REST proxy).
18
+ | `bcryptPassword` (`./`) | **legacy only** — bcrypt verifier for apps with pre-existing bcrypt hashes. New apps omit it (Better Auth defaults to scrypt). See [Migrating bcrypt passwords to scrypt](#migrating-bcrypt-passwords-to-scrypt) |
19
+ | `createAuthHelpers`, `safeNext` (`./server`) | validated App Router session helpers (`getSession` / `getUser` / `requireSession` / `requireUser` / `signInTarget` / `redirectIfAuthenticated`), request-memoized via React `cache()`, with automatic `next` + stale-cookie signalling; `safeNext` validates a `?next=` param |
20
+ | `createAuthMiddleware`, `withAuthPathHeader`, `clearStaleSession` (`./middleware`) | loop-safe edge middleware: gates unauthenticated users off protected paths, preserves `next`, and clears a stale session cookie so a bad session self-heals; the last two are its `next`-header and stale-cookie halves, for a site with its own proxy |
21
+
22
+ > **Data access + RLS** is owned by [`@ingram-tech/nk-db`](../nk-db); see §3.
27
23
  >
28
- > **Backend-JWT + org sites** (a backend API plus the org plugin): compose the
29
- > site's shared nk-db pool, `backendJwtOptions({ audience })`,
24
+ > **Backend-JWT + org sites** (a backend API plus the org plugin): compose your
25
+ > app's shared nk-db pool, `backendJwtOptions({ audience })`,
30
26
  > `nkOrganizationDefaults`, and `lastActiveOrganizationHooks(pool)` in your
31
27
  > `betterAuth()`; verify backend tokens with `verifyBackendJwt`. Keep
32
28
  > app-specific bits (SSO restrictions, permissions/roles, connectors) in the app.
@@ -49,7 +45,7 @@ Outside production, `BETTER_AUTH_SECRET` falls back to a well-known insecure
49
45
  placeholder, so local dev and tests run without setting it (a warning is logged).
50
46
  In production it stays required — a missing secret throws at startup.
51
47
 
52
- If your site derives its own `baseURL` and opens its own database connection, it
48
+ If your app derives its own `baseURL` and opens its own database connection, it
53
49
  may not want `authEnv()`'s all-or-nothing bundle (which also requires
54
50
  `BETTER_AUTH_URL` and `DATABASE_URL`). Take just the secret — same prod/dev rule —
55
51
  with `authSecret()`:
@@ -116,14 +112,14 @@ const db = await createTestDb({
116
112
  });
117
113
  ```
118
114
 
119
- **Upgrading better-auth** never means hand-writing a migration in your site: a
115
+ **Upgrading better-auth** never means hand-writing a migration in your app: a
120
116
  schema-changing upgrade ships as a new file in nk-auth's chain, so you bump the
121
117
  dependency and your next migrate applies it. Reconcile the shipped schema against
122
- a pinned `better-auth` with `npx @better-auth/cli generate` (a nextkit-maintainer
123
- task, done once here — not per site).
118
+ a pinned `better-auth` with `npx @better-auth/cli generate` (a maintainer task,
119
+ done once in this package).
124
120
 
125
- > **Adopting from the old copy-in model?** Earlier docs told sites to `cp` the
126
- > baseline into their own `drizzle/` chain. If you already did, keep that file
121
+ > **Adopting from the old copy-in model?** Earlier versions told you to `cp` the
122
+ > baseline into your own `drizzle/` chain. If you already did, keep that file
127
123
  > (deleting an applied migration causes journal drift) and just add the auth-chain
128
124
  > invocation above: its DDL is all `… if not exists`, so it no-ops against your
129
125
  > existing tables and simply records the nk-auth journal. New auth migrations then
@@ -131,10 +127,8 @@ task, done once here — not per site).
131
127
 
132
128
  ## 2. Configure the server
133
129
 
134
- This package is **not** a `betterAuth()` wrapper — your site calls `betterAuth`
135
- itself and spreads in our presets. That keeps full Better Auth type inference at
136
- the call site (so `auth.api.*` stays typed) and keeps the site plain Better Auth,
137
- per the prime directive.
130
+ Your app calls `betterAuth` itself and spreads in the presets, which keeps full
131
+ Better Auth type inference at the call site, so `auth.api.*` stays typed.
138
132
 
139
133
  ```ts
140
134
  // lib/auth.ts
@@ -166,13 +160,13 @@ export const auth = betterAuth({
166
160
  basePath: authBasePath, // mount at /auth, not the framework default /api/auth
167
161
  advanced: { database: { generateId: uuidGenerateId } }, // UUIDv7 ids
168
162
  // ^ stored as hyphenated UUIDv7 (uuid columns / RLS policies stay valid).
169
- // To show those same ids as prefixed base58 on the wire/UI — `team_…`,
170
- // matching the Ingram Cloud API's `agt_`/`smt_` ids — skin them with
163
+ // To show those same ids as prefixed base58 on the wire/UI — `team_…`, the
164
+ // way an API exposes prefixed ids like `agt_`/`smt_` — skin them with
171
165
  // `toPrefixedId(uuid, "team")` / recover with `fromPrefixedId`. `base58Id`
172
- // mints a fresh one directly for text-id sites. All from `@ingram-tech/nk-auth`.
166
+ // mints a fresh one directly for text-id apps. All from `@ingram-tech/nk-auth`.
173
167
  emailAndPassword: {
174
168
  enabled: true,
175
- // Better Auth hashes with scrypt by default. Only sites carrying
169
+ // Better Auth hashes with scrypt by default. Only apps carrying
176
170
  // pre-existing bcrypt hashes set `password: bcryptPassword` (legacy).
177
171
  sendResetPassword: email.sendResetPassword,
178
172
  },
@@ -232,11 +226,11 @@ has any stored preference.
232
226
  mail a user ever gets from you; a bare `<a href=url>url</a>` reads as phishing
233
227
  and trains people to distrust your domain. Take the
234
228
  [`registry`](https://github.com/ingram-technologies/registry) email components
235
- (`shadcn add email-verification email-password-reset`) — they accept `heading` /
236
- `body` / `ctaLabel` / `preview` overrides precisely so you can pass translated
237
- copy — or write your own. Send auth links from the default `notifications` local
238
- part (`fromAddress("Example")`) — **not** `no-reply`: this is the mail users are
239
- most likely to reply to, and dropping that reply is hostile. See
229
+ (`shadcn add email-verification email-password-reset`), which accept `heading` /
230
+ `body` / `ctaLabel` / `preview` overrides so you can pass translated copy, or
231
+ write your own. Send auth links from the default `notifications` local part
232
+ (`fromAddress("Example")`), **not** `no-reply`: this is the mail users are most
233
+ likely to reply to, and dropping that reply is hostile. See
240
234
  [transactional-email.md](../../docs/transactional-email.md).
241
235
 
242
236
  **Spread all three in; do not hand-write them.** `betterAuth()` takes its
@@ -274,8 +268,8 @@ claims GUC, so a policy's `auth.uid()` still compares to a `uuid` column.
274
268
 
275
269
  ## 4. Client
276
270
 
277
- Assemble the client in a `"use client"` module (full plugin inference is
278
- preserved here too):
271
+ Assemble the client in a `"use client"` module (plugin inference is preserved
272
+ here too):
279
273
 
280
274
  ```tsx
281
275
  "use client";
@@ -393,14 +387,57 @@ that can touch cookies) deletes the dead Better Auth cookies on the `stale`
393
387
  marker and bounces to a clean `/login?next=…`; signing in returns them to
394
388
  `next`. `next` for the cookie-less case is filled in by middleware directly; for
395
389
  the cookie-present case the guard reads it from the `x-nk-auth-path` header
396
- middleware injects — so the self-heal (next + clearing) needs the middleware.
397
- Sites that skip middleware still get validated gating from the server helpers,
398
- just without automatic `next`/clearing.
390
+ middleware injects.
391
+
392
+ **Your own proxy.** An app that composes its own proxy (locale routing, tenant
393
+ pinning, …) keeps validated gating from the server helpers, but the guards can
394
+ only preserve `next` if something sets that header. Both halves of the
395
+ middleware are exported on their own so a custom proxy adopts them without the
396
+ optimistic gate; outside production the guards warn once when the header is
397
+ missing, and `nk doctor` flags the shape.
398
+
399
+ ```ts
400
+ // proxy.ts — a site with its own proxy
401
+ import { clearStaleSession, withAuthPathHeader } from "@ingram-tech/nk-auth/middleware";
402
+ import { localeProxy } from "@ingram-tech/nk-i18n/next";
403
+
404
+ export function proxy(request: NextRequest) {
405
+ const stale = clearStaleSession(request, { signInPath: "/login" });
406
+ if (stale) return stale; // dead cookie cleared, `next` kept
407
+ const requestHeaders = new Headers(request.headers);
408
+ withAuthPathHeader(request, requestHeaders); // guards can now build `next`
409
+ return localeProxy(routing, request, { requestHeaders });
410
+ }
411
+ ```
412
+
413
+ (`createAuthMiddleware`'s middleware takes the same `{ requestHeaders }` option
414
+ when you want the gate too.)
415
+
416
+ **Your own guard.** A wrapper that layers something over `getUser()` (a profile
417
+ lookup, say) redirects to `await signInTarget()`, the same URL `requireUser`
418
+ would have used, with `next` and `stale` filled in:
419
+
420
+ ```ts
421
+ const { getUser, signInTarget } = createAuthHelpers(auth);
422
+ export async function requireProfile() {
423
+ const user = await getUser();
424
+ if (!user) redirect(await signInTarget());
425
+ return loadProfile(user.id);
426
+ }
427
+ ```
428
+
429
+ **Your own param.** Both `createAuthHelpers` and `createAuthMiddleware` take
430
+ `nextParam` (default `next`) and `isSafeNext` (default `safeNext`, internal
431
+ paths only). A site that already ships `?redirectTo=` with a trusted-origin
432
+ allow-list passes `{ nextParam: "redirectTo", isSafeNext: isTrustedRedirect }`
433
+ to both, and its sign-in page keeps validating with the same function. Whatever
434
+ `isSafeNext` admits is where sign-in will send the user, so keep it as strict as
435
+ `safeNext` on everything except the origins you own.
399
436
 
400
437
  ## 6. Passwords: change, set, and reset
401
438
 
402
439
  nk-auth owns the reset **sender** (`makeEmailSenders.sendResetPassword`, §2) and
403
- now closes the loop so a site never touches Better Auth's `account` table or
440
+ closes the loop so your app never touches Better Auth's `account` table or
404
441
  endpoint names directly. Three pieces:
405
442
 
406
443
  **Detect what login the user has.** A social-only account (Google, …) has no
@@ -478,11 +515,11 @@ async redirects() {
478
515
  ## Migrating bcrypt passwords to scrypt
479
516
 
480
517
  `bcryptPassword` is **legacy support only** (see its `@deprecated` note). It
481
- exists so sites whose `account.password` hashes are bcrypt keep verifying —
518
+ exists so apps whose `account.password` hashes are bcrypt keep verifying:
482
519
  that is all nk-auth ships for the bcrypt case.
483
520
 
484
- The path to move a site fully onto scrypt is a **dual-format verifier**, wired
485
- in the site (nk-auth does not ship it). Better Auth's default hasher is scrypt
521
+ The path to move fully onto scrypt is a **dual-format verifier**, wired in your
522
+ app (nk-auth does not ship it). Better Auth's default hasher is scrypt
486
523
  (`<salt-hex>:<key-hex>`) and bcrypt hashes are trivially distinguishable (they
487
524
  start with `$2a$`/`$2b$`/`$2y$`), so override only
488
525
  `emailAndPassword.password.verify` to branch on `hash.startsWith("$2")` →
@@ -21,12 +21,32 @@ export declare const NK_AUTH_PATH_HEADER = "x-nk-auth-path";
21
21
  */
22
22
  export declare function safeNextParam(value: string | null | undefined): string | null;
23
23
  /**
24
- * Build a sign-in URL: `signInPath`, plus `next` (when safe) so the user returns
25
- * to where they were headed, plus a `stale=1` marker when a session cookie is
26
- * present but invalid — the signal the middleware uses to clear the dead cookie.
24
+ * How a site names and validates its post-login redirect param. Shared by the
25
+ * middleware (which writes it for the cookie-less case) and the server helpers
26
+ * (which write it for the cookie-present-but-invalid case), so both halves agree
27
+ * on the one contract the sign-in page reads.
28
+ */
29
+ export interface NextParamOptions {
30
+ /** Query param carrying the post-login destination. Default `next`. */
31
+ nextParam?: string;
32
+ /**
33
+ * Validate a candidate destination: return the value to redirect to, or null
34
+ * to drop it. Default {@link safeNextParam}, which admits only internal
35
+ * paths. A site with an existing trusted-origin allow-list plugs its own
36
+ * validator in here rather than forking the URL builder; whatever it admits
37
+ * is what its sign-in page will redirect to, so keep it as strict as
38
+ * `safeNextParam` on everything but the origins you own.
39
+ */
40
+ isSafeNext?: (value: string) => string | null;
41
+ }
42
+ /**
43
+ * Build a sign-in URL: `signInPath`, plus the next param (when safe) so the user
44
+ * returns to where they were headed, plus a `stale=1` marker when a session
45
+ * cookie is present but invalid — the signal the middleware uses to clear the
46
+ * dead cookie.
27
47
  */
28
48
  export declare function signInUrl(signInPath: string, opts: {
29
49
  next?: string | null;
30
50
  stale?: boolean;
31
- }): string;
51
+ } & NextParamOptions): string;
32
52
  //# sourceMappingURL=gating-internals.d.ts.map
@@ -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;;;;;;;;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"}
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;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAChC,uEAAuE;IACvE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;CAC9C;AAED;;;;;GAKG;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,GAAG,gBAAgB,GAChE,MAAM,CAQR"}
@@ -30,15 +30,17 @@ export function safeNextParam(value) {
30
30
  return value;
31
31
  }
32
32
  /**
33
- * Build a sign-in URL: `signInPath`, plus `next` (when safe) so the user returns
34
- * to where they were headed, plus a `stale=1` marker when a session cookie is
35
- * present but invalid — the signal the middleware uses to clear the dead cookie.
33
+ * Build a sign-in URL: `signInPath`, plus the next param (when safe) so the user
34
+ * returns to where they were headed, plus a `stale=1` marker when a session
35
+ * cookie is present but invalid — the signal the middleware uses to clear the
36
+ * dead cookie.
36
37
  */
37
38
  export function signInUrl(signInPath, opts) {
38
39
  const params = new URLSearchParams();
39
- const next = safeNextParam(opts.next);
40
+ const validate = opts.isSafeNext ?? safeNextParam;
41
+ const next = opts.next ? validate(opts.next) : null;
40
42
  if (next)
41
- params.set("next", next);
43
+ params.set(opts.nextParam ?? "next", next);
42
44
  if (opts.stale)
43
45
  params.set("stale", "1");
44
46
  const qs = params.toString();
@@ -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;;;;;;;;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"}
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;AAsBD;;;;;GAKG;AACH,MAAM,UAAU,SAAS,CACxB,UAAkB,EAClB,IAAkE;IAElE,MAAM,MAAM,GAAG,IAAI,eAAe,EAAE,CAAC;IACrC,MAAM,QAAQ,GAAG,IAAI,CAAC,UAAU,IAAI,aAAa,CAAC;IAClD,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACpD,IAAI,IAAI;QAAE,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,IAAI,MAAM,EAAE,IAAI,CAAC,CAAC;IACrD,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"}
@@ -1,5 +1,47 @@
1
1
  import { type NextRequest, NextResponse } from "next/server";
2
- export interface AuthMiddlewareConfig {
2
+ import { type NextParamOptions } from "./gating-internals.js";
3
+ export type { NextParamOptions } from "./gating-internals.js";
4
+ /**
5
+ * Forward the requested path + query to the app as the `x-nk-auth-path` request
6
+ * header, so a server guard can build `next` when it redirects to sign-in. The
7
+ * one line a custom proxy needs for `next` preservation:
8
+ *
9
+ * const requestHeaders = new Headers(request.headers);
10
+ * withAuthPathHeader(request, requestHeaders);
11
+ * return localeProxy(routing, request, { requestHeaders });
12
+ *
13
+ * Set, never passed through: the header is ours to mint, so a client that sends
14
+ * one of its own can't choose where sign-in returns to.
15
+ */
16
+ export declare function withAuthPathHeader(request: NextRequest, requestHeaders: Headers): void;
17
+ export interface StaleSessionConfig {
18
+ /** The sign-in path the server guards redirect to. Default `/login`. */
19
+ signInPath?: string;
20
+ /** Cookie-name fragment of the cookies to clear. Default `better-auth`. */
21
+ sessionCookiePrefix?: string;
22
+ }
23
+ /**
24
+ * The stale-session handshake, on its own for a custom proxy. The validated
25
+ * guard sends a present-but-invalid session to `${signInPath}?stale=1&next=…`;
26
+ * this clears the dead Better Auth cookies (only middleware can, pre-render) and
27
+ * returns a redirect to the same URL minus `stale`, which keeps `next`. Returns
28
+ * null on every other request, so a proxy can short-circuit on it:
29
+ *
30
+ * const stale = clearStaleSession(request, { signInPath: "/login" });
31
+ * if (stale) return stale;
32
+ *
33
+ * After the redirect the cookie is gone, so the handshake can't re-arm.
34
+ */
35
+ export declare function clearStaleSession(request: NextRequest, config?: StaleSessionConfig): NextResponse | null;
36
+ export interface AuthMiddlewareOptions {
37
+ /**
38
+ * Headers to forward to the app, if the middleware has its own to add. A
39
+ * fresh copy of the request's headers is used when omitted; pass your own
40
+ * when you also set things like a tenant or locale header.
41
+ */
42
+ requestHeaders?: Headers;
43
+ }
44
+ export interface AuthMiddlewareConfig extends NextParamOptions {
3
45
  /**
4
46
  * Path prefixes that require a session. A request without a session cookie
5
47
  * to any of these is redirected to `signInPath`. Matched with `startsWith`.
@@ -22,5 +64,5 @@ export interface AuthMiddlewareConfig {
22
64
  */
23
65
  sessionCookiePrefix?: string;
24
66
  }
25
- export declare function createAuthMiddleware(config: AuthMiddlewareConfig): (request: NextRequest) => NextResponse;
67
+ export declare function createAuthMiddleware(config: AuthMiddlewareConfig): (request: NextRequest, options?: AuthMiddlewareOptions) => NextResponse;
26
68
  //# sourceMappingURL=middleware.d.ts.map
@@ -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,aAiC5B,WAAW,KAAG,YAAY,CA6D9D"}
1
+ {"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":"AAmCA,OAAO,EAAE,KAAK,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC7D,OAAO,EAEN,KAAK,gBAAgB,EAErB,MAAM,uBAAuB,CAAC;AAE/B,YAAY,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAE9D;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CACjC,OAAO,EAAE,WAAW,EACpB,cAAc,EAAE,OAAO,GACrB,IAAI,CAKN;AAED,MAAM,WAAW,kBAAkB;IAClC,wEAAwE;IACxE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,2EAA2E;IAC3E,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,CAChC,OAAO,EAAE,WAAW,EACpB,MAAM,GAAE,kBAAuB,GAC7B,YAAY,GAAG,IAAI,CA4BrB;AAED,MAAM,WAAW,qBAAqB;IACrC;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,oBAAqB,SAAQ,gBAAgB;IAC7D;;;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,aAkCtD,WAAW,YACX,qBAAqB,KAC5B,YAAY,CAgDf"}
@@ -24,10 +24,70 @@
24
24
  * `${signInPath}?stale=1`; middleware (the only pre-render place that can set
25
25
  * cookies) deletes the dead Better Auth cookies and bounces to a clean
26
26
  * sign-in URL, so a bad session self-heals instead of failing every request.
27
+ *
28
+ * Both halves are also exported on their own — {@link withAuthPathHeader} and
29
+ * {@link clearStaleSession} — for a site whose proxy composes several concerns
30
+ * (locale routing, tenant pinning, …) and does not want the optimistic gate.
31
+ * Without the header the server guards cannot preserve `next` at all, and they
32
+ * say so (a one-time warning outside production); wiring one line into your own
33
+ * proxy is enough to fix that.
27
34
  */
28
35
  import { getSessionCookie } from "better-auth/cookies";
29
36
  import { NextResponse } from "next/server";
30
- import { NK_AUTH_PATH_HEADER, safeNextParam } from "./gating-internals.js";
37
+ import { NK_AUTH_PATH_HEADER, signInUrl, } from "./gating-internals.js";
38
+ /**
39
+ * Forward the requested path + query to the app as the `x-nk-auth-path` request
40
+ * header, so a server guard can build `next` when it redirects to sign-in. The
41
+ * one line a custom proxy needs for `next` preservation:
42
+ *
43
+ * const requestHeaders = new Headers(request.headers);
44
+ * withAuthPathHeader(request, requestHeaders);
45
+ * return localeProxy(routing, request, { requestHeaders });
46
+ *
47
+ * Set, never passed through: the header is ours to mint, so a client that sends
48
+ * one of its own can't choose where sign-in returns to.
49
+ */
50
+ export function withAuthPathHeader(request, requestHeaders) {
51
+ requestHeaders.set(NK_AUTH_PATH_HEADER, request.nextUrl.pathname + request.nextUrl.search);
52
+ }
53
+ /**
54
+ * The stale-session handshake, on its own for a custom proxy. The validated
55
+ * guard sends a present-but-invalid session to `${signInPath}?stale=1&next=…`;
56
+ * this clears the dead Better Auth cookies (only middleware can, pre-render) and
57
+ * returns a redirect to the same URL minus `stale`, which keeps `next`. Returns
58
+ * null on every other request, so a proxy can short-circuit on it:
59
+ *
60
+ * const stale = clearStaleSession(request, { signInPath: "/login" });
61
+ * if (stale) return stale;
62
+ *
63
+ * After the redirect the cookie is gone, so the handshake can't re-arm.
64
+ */
65
+ export function clearStaleSession(request, config = {}) {
66
+ const signInPath = config.signInPath ?? "/login";
67
+ const cookiePrefix = config.sessionCookiePrefix ?? "better-auth";
68
+ if (request.nextUrl.pathname !== signInPath ||
69
+ request.nextUrl.searchParams.get("stale") !== "1") {
70
+ return null;
71
+ }
72
+ const to = request.nextUrl.clone();
73
+ to.searchParams.delete("stale");
74
+ const res = NextResponse.redirect(to);
75
+ for (const cookie of request.cookies.getAll()) {
76
+ if (cookie.name.includes(cookiePrefix)) {
77
+ // `__Secure-`/`__Host-` cookies: browsers reject the deletion
78
+ // Set-Cookie unless it carries the Secure attribute itself, so a
79
+ // bare delete() would silently leave the dead cookie in place on
80
+ // HTTPS and re-run this handshake on every visit.
81
+ res.cookies.delete({
82
+ name: cookie.name,
83
+ path: "/",
84
+ secure: cookie.name.startsWith("__Secure-") ||
85
+ cookie.name.startsWith("__Host-"),
86
+ });
87
+ }
88
+ }
89
+ return res;
90
+ }
31
91
  export function createAuthMiddleware(config) {
32
92
  const signInPath = config.signInPath ?? "/login";
33
93
  const frontDoorPaths = config.frontDoorPaths ?? [];
@@ -51,32 +111,16 @@ export function createAuthMiddleware(config) {
51
111
  if (frontDoorPaths.length > 0 && !config.signedInRedirect) {
52
112
  throw new Error("@ingram-tech/nk-auth: signedInRedirect is required when frontDoorPaths is set.");
53
113
  }
54
- return function middleware(request) {
114
+ return function middleware(request, options = {}) {
55
115
  const path = request.nextUrl.pathname;
56
- // 1. Stale-session handshake. The validated guard sends an invalid session
57
- // to `${signInPath}?stale=1&next=…`. Clear the dead Better Auth cookies
58
- // (only middleware can, pre-render) and bounce to a clean sign-in URL
59
- // that keeps `next`. After this the cookie is gone, so it can't re-arm.
60
- if (path === signInPath && request.nextUrl.searchParams.get("stale") === "1") {
61
- const to = request.nextUrl.clone();
62
- to.searchParams.delete("stale");
63
- const res = NextResponse.redirect(to);
64
- for (const cookie of request.cookies.getAll()) {
65
- if (cookie.name.includes(cookiePrefix)) {
66
- // `__Secure-`/`__Host-` cookies: browsers reject the deletion
67
- // Set-Cookie unless it carries the Secure attribute itself, so a
68
- // bare delete() would silently leave the dead cookie in place on
69
- // HTTPS and re-run this handshake on every visit.
70
- res.cookies.delete({
71
- name: cookie.name,
72
- path: "/",
73
- secure: cookie.name.startsWith("__Secure-") ||
74
- cookie.name.startsWith("__Host-"),
75
- });
76
- }
77
- }
78
- return res;
79
- }
116
+ // 1. Stale-session handshake: clear the dead cookies and bounce to a clean
117
+ // sign-in URL that keeps `next`.
118
+ const stale = clearStaleSession(request, {
119
+ signInPath,
120
+ sessionCookiePrefix: cookiePrefix,
121
+ });
122
+ if (stale)
123
+ return stale;
80
124
  const hasSessionCookie = !!getSessionCookie(request);
81
125
  // 2. Unauthenticated (no cookie) on a protected path -> sign in, and
82
126
  // remember where they were going.
@@ -84,10 +128,11 @@ export function createAuthMiddleware(config) {
84
128
  const original = request.nextUrl.pathname + request.nextUrl.search;
85
129
  const to = request.nextUrl.clone();
86
130
  to.pathname = signInPath;
87
- to.search = "";
88
- const next = safeNextParam(original);
89
- if (next)
90
- to.searchParams.set("next", next);
131
+ to.search = new URL(signInUrl(signInPath, {
132
+ next: original,
133
+ nextParam: config.nextParam,
134
+ isSafeNext: config.isSafeNext,
135
+ }), request.nextUrl).search;
91
136
  return NextResponse.redirect(to);
92
137
  }
93
138
  // 3. Front door: a cookie-bearing visit to "/" (or configured) -> the app.
@@ -101,8 +146,8 @@ export function createAuthMiddleware(config) {
101
146
  }
102
147
  // 4. Pass through, injecting the requested path so server guards can build
103
148
  // `next` for the cookie-present-but-invalid case.
104
- const requestHeaders = new Headers(request.headers);
105
- requestHeaders.set(NK_AUTH_PATH_HEADER, path + request.nextUrl.search);
149
+ const requestHeaders = options.requestHeaders ?? new Headers(request.headers);
150
+ withAuthPathHeader(request, requestHeaders);
106
151
  return NextResponse.next({ request: { headers: requestHeaders } });
107
152
  };
108
153
  }
@@ -1 +1 @@
1
- {"version":3,"file":"middleware.js","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"middleware.js","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":"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"}
package/dist/server.d.ts CHANGED
@@ -25,12 +25,14 @@
25
25
  * and falls through to the form instead of ping-ponging forever.
26
26
  */
27
27
  import type { IdHelper } from "@ingram-tech/nk-db/id";
28
+ import { type NextParamOptions } from "./gating-internals.js";
28
29
  /**
29
30
  * Validate a `next` redirect param: returns it only if it's an internal,
30
31
  * non-protocol-relative path, else null. Use on the login page before honoring
31
32
  * `?next=` so it can't become an open redirect.
32
33
  */
33
34
  export { safeNextParam as safeNext } from "./gating-internals.js";
35
+ export type { NextParamOptions } from "./gating-internals.js";
34
36
  /** The shape we need from a session: anything carrying a `user`. */
35
37
  interface SessionLike {
36
38
  user: unknown;
@@ -69,7 +71,7 @@ export interface SessionIds {
69
71
  user: IdHelper;
70
72
  organization?: IdHelper;
71
73
  }
72
- export interface AuthHelpersOptions {
74
+ export interface AuthHelpersOptions extends NextParamOptions {
73
75
  /** Where guards send unauthenticated users. Default `/login`. */
74
76
  signInPath?: string;
75
77
  /**
@@ -97,6 +99,7 @@ export declare function createAuthHelpers<S extends SessionLike>(auth: AuthLike<
97
99
  getUser: () => Promise<S["user"] | null>;
98
100
  requireSession: () => Promise<S>;
99
101
  requireUser: () => Promise<S["user"]>;
102
+ signInTarget: () => Promise<string>;
100
103
  redirectIfAuthenticated: (to: string) => Promise<void>;
101
104
  getLinkedProviders: () => Promise<string[]>;
102
105
  hasCredentialAccount: () => Promise<boolean>;
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAOtD;;;;GAIG;AACH,OAAO,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAElE,oEAAoE;AACpE,UAAU,WAAW;IACpB,IAAI,EAAE,OAAO,CAAC;CACd;AAED;;;;GAIG;AACH,UAAU,QAAQ,CAAC,CAAC,SAAS,WAAW;IACvC,GAAG,EAAE;QACJ,UAAU,EAAE,CAAC,KAAK,EAAE;YAAE,OAAO,EAAE,OAAO,CAAA;SAAE,KAAK,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;QAC/D;;;;;WAKG;QACH,gBAAgB,EAAE,CAAC,KAAK,EAAE;YACzB,OAAO,EAAE,OAAO,CAAC;SACjB,KAAK,OAAO,CAAC,KAAK,CAAC;YAAE,UAAU,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC,CAAC;KAC7C,CAAC;CACF;AAED;;;;;;GAMG;AACH,MAAM,WAAW,UAAU;IAC1B,IAAI,EAAE,QAAQ,CAAC;IACf,YAAY,CAAC,EAAE,QAAQ,CAAC;CACxB;AAED,MAAM,WAAW,kBAAkB;IAClC,iEAAiE;IACjE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,GAAG,CAAC,EAAE,UAAU,CAAC;IACjB;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAKD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,WAAW,EACrD,OAAO,EAAE,CAAC,EACV,GAAG,EAAE,UAAU,GACb,CAAC,CAyBH;AAED,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,WAAW,EACtD,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC,EACjB,OAAO,GAAE,kBAAuB;sBAiBG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;mBAQ1B,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC;0BAkClB,OAAO,CAAC,CAAC,CAAC;uBAXb,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;kCAuBL,MAAM,KAAG,OAAO,CAAC,IAAI,CAAC;8BAU5B,OAAO,CAAC,MAAM,EAAE,CAAC;gCAcf,OAAO,CAAC,OAAO,CAAC;EAcvD"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAItD,OAAO,EAEN,KAAK,gBAAgB,EAErB,MAAM,uBAAuB,CAAC;AAG/B;;;;GAIG;AACH,OAAO,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAClE,YAAY,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAE9D,oEAAoE;AACpE,UAAU,WAAW;IACpB,IAAI,EAAE,OAAO,CAAC;CACd;AAED;;;;GAIG;AACH,UAAU,QAAQ,CAAC,CAAC,SAAS,WAAW;IACvC,GAAG,EAAE;QACJ,UAAU,EAAE,CAAC,KAAK,EAAE;YAAE,OAAO,EAAE,OAAO,CAAA;SAAE,KAAK,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;QAC/D;;;;;WAKG;QACH,gBAAgB,EAAE,CAAC,KAAK,EAAE;YACzB,OAAO,EAAE,OAAO,CAAC;SACjB,KAAK,OAAO,CAAC,KAAK,CAAC;YAAE,UAAU,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC,CAAC;KAC7C,CAAC;CACF;AAED;;;;;;GAMG;AACH,MAAM,WAAW,UAAU;IAC1B,IAAI,EAAE,QAAQ,CAAC;IACf,YAAY,CAAC,EAAE,QAAQ,CAAC;CACxB;AAED,MAAM,WAAW,kBAAmB,SAAQ,gBAAgB;IAC3D,iEAAiE;IACjE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,GAAG,CAAC,EAAE,UAAU,CAAC;IACjB;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAKD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,WAAW,EACrD,OAAO,EAAE,CAAC,EACV,GAAG,EAAE,UAAU,GACb,CAAC,CAyBH;AAED,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,WAAW,EACtD,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC,EACjB,OAAO,GAAE,kBAAuB;sBAiBG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;mBAQ1B,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC;0BA+DlB,OAAO,CAAC,CAAC,CAAC;uBAXb,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;wBA3BjB,OAAO,CAAC,MAAM,CAAC;kCAkDH,MAAM,KAAG,OAAO,CAAC,IAAI,CAAC;8BAU5B,OAAO,CAAC,MAAM,EAAE,CAAC;gCAcf,OAAO,CAAC,OAAO,CAAC;EAevD"}
package/dist/server.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { cookies, headers } from "next/headers";
2
2
  import { redirect } from "next/navigation";
3
3
  import { cache } from "react";
4
- import { NK_AUTH_PATH_HEADER, signInUrl } from "./gating-internals.js";
4
+ import { NK_AUTH_PATH_HEADER, signInUrl, } from "./gating-internals.js";
5
5
  import { CREDENTIAL_PROVIDER_ID } from "./password.js";
6
6
  /**
7
7
  * Validate a `next` redirect param: returns it only if it's an internal,
@@ -65,17 +65,41 @@ export function createAuthHelpers(auth, options = {}) {
65
65
  const session = await getSession();
66
66
  return session?.user ?? null;
67
67
  }
68
+ // The missing-header warning fires once per helpers instance, not once per
69
+ // request: it is a wiring mistake, and a wiring mistake does not get less
70
+ // wrong the second time.
71
+ let warnedMissingHeader = false;
68
72
  /**
69
73
  * The sign-in redirect for a request with no valid session: keep the
70
74
  * requested path as `next` (from the middleware-injected header), and flag
71
75
  * `stale=1` when a session cookie is present-but-invalid so the middleware
72
76
  * clears it. The two reads are request-scoped and cached by Next.
77
+ *
78
+ * Exported for a site that wraps the guards in its own (a profile lookup on
79
+ * top of `getUser()`, say): `redirect(await signInTarget())` gets `next` and
80
+ * `stale` right without re-deriving either.
81
+ *
82
+ * `next` depends on the `x-nk-auth-path` header, which only middleware can
83
+ * set. When it is absent the destination is silently lost — the user signs in
84
+ * and lands on the default page instead of the one they asked for — so
85
+ * outside production this says so once, naming the two ways to wire it.
73
86
  */
74
87
  async function signInTarget() {
75
88
  const [h, c] = await Promise.all([headers(), cookies()]);
76
89
  const next = h.get(NK_AUTH_PATH_HEADER);
90
+ if (next === null &&
91
+ !warnedMissingHeader &&
92
+ process.env.NODE_ENV !== "production") {
93
+ warnedMissingHeader = true;
94
+ console.warn(`@ingram-tech/nk-auth: \`next\` not preserved — no ${NK_AUTH_PATH_HEADER} request header. Mount createAuthMiddleware, or call withAuthPathHeader(request, requestHeaders) from "@ingram-tech/nk-auth/middleware" in your own proxy.`);
95
+ }
77
96
  const stale = c.getAll().some((ck) => ck.name.includes(cookiePrefix));
78
- return signInUrl(signInPath, { next, stale });
97
+ return signInUrl(signInPath, {
98
+ next,
99
+ stale,
100
+ nextParam: options.nextParam,
101
+ isSafeNext: options.isSafeNext,
102
+ });
79
103
  }
80
104
  /**
81
105
  * Require a signed-in user or redirect to sign-in. Returns the user (non-null)
@@ -137,6 +161,7 @@ export function createAuthHelpers(auth, options = {}) {
137
161
  getUser,
138
162
  requireSession,
139
163
  requireUser,
164
+ signInTarget,
140
165
  redirectIfAuthenticated,
141
166
  getLinkedProviders,
142
167
  hasCredentialAccount,
@@ -1 +1 @@
1
- {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ingram-tech/nk-auth",
3
- "version": "0.14.0",
3
+ "version": "0.15.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",
@@ -68,7 +68,7 @@
68
68
  "test": "vitest run"
69
69
  },
70
70
  "dependencies": {
71
- "@ingram-tech/nk-db": "^2.1.0",
71
+ "@ingram-tech/nk-db": "^2.1.1",
72
72
  "bcrypt": "^6.0.0",
73
73
  "jose": "^6.2.9",
74
74
  "zod": "^4.4.3"
@@ -96,7 +96,7 @@
96
96
  },
97
97
  "devDependencies": {
98
98
  "@better-auth/passkey": "^1.7.0",
99
- "@ingram-tech/nk-dev": "0.12.2",
99
+ "@ingram-tech/nk-dev": "0.14.0",
100
100
  "@types/bcrypt": "^6.0.0",
101
101
  "@types/node": "^26.2.0",
102
102
  "@types/pg": "^8.23.1",
@@ -30,17 +30,39 @@ export function safeNextParam(value: string | null | undefined): string | null {
30
30
  }
31
31
 
32
32
  /**
33
- * Build a sign-in URL: `signInPath`, plus `next` (when safe) so the user returns
34
- * to where they were headed, plus a `stale=1` marker when a session cookie is
35
- * present but invalid — the signal the middleware uses to clear the dead cookie.
33
+ * How a site names and validates its post-login redirect param. Shared by the
34
+ * middleware (which writes it for the cookie-less case) and the server helpers
35
+ * (which write it for the cookie-present-but-invalid case), so both halves agree
36
+ * on the one contract the sign-in page reads.
37
+ */
38
+ export interface NextParamOptions {
39
+ /** Query param carrying the post-login destination. Default `next`. */
40
+ nextParam?: string;
41
+ /**
42
+ * Validate a candidate destination: return the value to redirect to, or null
43
+ * to drop it. Default {@link safeNextParam}, which admits only internal
44
+ * paths. A site with an existing trusted-origin allow-list plugs its own
45
+ * validator in here rather than forking the URL builder; whatever it admits
46
+ * is what its sign-in page will redirect to, so keep it as strict as
47
+ * `safeNextParam` on everything but the origins you own.
48
+ */
49
+ isSafeNext?: (value: string) => string | null;
50
+ }
51
+
52
+ /**
53
+ * Build a sign-in URL: `signInPath`, plus the next param (when safe) so the user
54
+ * returns to where they were headed, plus a `stale=1` marker when a session
55
+ * cookie is present but invalid — the signal the middleware uses to clear the
56
+ * dead cookie.
36
57
  */
37
58
  export function signInUrl(
38
59
  signInPath: string,
39
- opts: { next?: string | null; stale?: boolean },
60
+ opts: { next?: string | null; stale?: boolean } & NextParamOptions,
40
61
  ): string {
41
62
  const params = new URLSearchParams();
42
- const next = safeNextParam(opts.next);
43
- if (next) params.set("next", next);
63
+ const validate = opts.isSafeNext ?? safeNextParam;
64
+ const next = opts.next ? validate(opts.next) : null;
65
+ if (next) params.set(opts.nextParam ?? "next", next);
44
66
  if (opts.stale) params.set("stale", "1");
45
67
  const qs = params.toString();
46
68
  return qs ? `${signInPath}?${qs}` : signInPath;
package/src/middleware.ts CHANGED
@@ -24,12 +24,108 @@
24
24
  * `${signInPath}?stale=1`; middleware (the only pre-render place that can set
25
25
  * cookies) deletes the dead Better Auth cookies and bounces to a clean
26
26
  * sign-in URL, so a bad session self-heals instead of failing every request.
27
+ *
28
+ * Both halves are also exported on their own — {@link withAuthPathHeader} and
29
+ * {@link clearStaleSession} — for a site whose proxy composes several concerns
30
+ * (locale routing, tenant pinning, …) and does not want the optimistic gate.
31
+ * Without the header the server guards cannot preserve `next` at all, and they
32
+ * say so (a one-time warning outside production); wiring one line into your own
33
+ * proxy is enough to fix that.
27
34
  */
28
35
  import { getSessionCookie } from "better-auth/cookies";
29
36
  import { type NextRequest, NextResponse } from "next/server";
30
- import { NK_AUTH_PATH_HEADER, safeNextParam } from "./gating-internals.js";
37
+ import {
38
+ NK_AUTH_PATH_HEADER,
39
+ type NextParamOptions,
40
+ signInUrl,
41
+ } from "./gating-internals.js";
42
+
43
+ export type { NextParamOptions } from "./gating-internals.js";
44
+
45
+ /**
46
+ * Forward the requested path + query to the app as the `x-nk-auth-path` request
47
+ * header, so a server guard can build `next` when it redirects to sign-in. The
48
+ * one line a custom proxy needs for `next` preservation:
49
+ *
50
+ * const requestHeaders = new Headers(request.headers);
51
+ * withAuthPathHeader(request, requestHeaders);
52
+ * return localeProxy(routing, request, { requestHeaders });
53
+ *
54
+ * Set, never passed through: the header is ours to mint, so a client that sends
55
+ * one of its own can't choose where sign-in returns to.
56
+ */
57
+ export function withAuthPathHeader(
58
+ request: NextRequest,
59
+ requestHeaders: Headers,
60
+ ): void {
61
+ requestHeaders.set(
62
+ NK_AUTH_PATH_HEADER,
63
+ request.nextUrl.pathname + request.nextUrl.search,
64
+ );
65
+ }
66
+
67
+ export interface StaleSessionConfig {
68
+ /** The sign-in path the server guards redirect to. Default `/login`. */
69
+ signInPath?: string;
70
+ /** Cookie-name fragment of the cookies to clear. Default `better-auth`. */
71
+ sessionCookiePrefix?: string;
72
+ }
73
+
74
+ /**
75
+ * The stale-session handshake, on its own for a custom proxy. The validated
76
+ * guard sends a present-but-invalid session to `${signInPath}?stale=1&next=…`;
77
+ * this clears the dead Better Auth cookies (only middleware can, pre-render) and
78
+ * returns a redirect to the same URL minus `stale`, which keeps `next`. Returns
79
+ * null on every other request, so a proxy can short-circuit on it:
80
+ *
81
+ * const stale = clearStaleSession(request, { signInPath: "/login" });
82
+ * if (stale) return stale;
83
+ *
84
+ * After the redirect the cookie is gone, so the handshake can't re-arm.
85
+ */
86
+ export function clearStaleSession(
87
+ request: NextRequest,
88
+ config: StaleSessionConfig = {},
89
+ ): NextResponse | null {
90
+ const signInPath = config.signInPath ?? "/login";
91
+ const cookiePrefix = config.sessionCookiePrefix ?? "better-auth";
92
+ if (
93
+ request.nextUrl.pathname !== signInPath ||
94
+ request.nextUrl.searchParams.get("stale") !== "1"
95
+ ) {
96
+ return null;
97
+ }
98
+ const to = request.nextUrl.clone();
99
+ to.searchParams.delete("stale");
100
+ const res = NextResponse.redirect(to);
101
+ for (const cookie of request.cookies.getAll()) {
102
+ if (cookie.name.includes(cookiePrefix)) {
103
+ // `__Secure-`/`__Host-` cookies: browsers reject the deletion
104
+ // Set-Cookie unless it carries the Secure attribute itself, so a
105
+ // bare delete() would silently leave the dead cookie in place on
106
+ // HTTPS and re-run this handshake on every visit.
107
+ res.cookies.delete({
108
+ name: cookie.name,
109
+ path: "/",
110
+ secure:
111
+ cookie.name.startsWith("__Secure-") ||
112
+ cookie.name.startsWith("__Host-"),
113
+ });
114
+ }
115
+ }
116
+ return res;
117
+ }
31
118
 
32
- export interface AuthMiddlewareConfig {
119
+ export interface AuthMiddlewareOptions {
120
+ /**
121
+ * Headers to forward to the app, if the middleware has its own to add. A
122
+ * fresh copy of the request's headers is used when omitted; pass your own
123
+ * when you also set things like a tenant or locale header.
124
+ */
125
+ requestHeaders?: Headers;
126
+ }
127
+
128
+ export interface AuthMiddlewareConfig extends NextParamOptions {
33
129
  /**
34
130
  * Path prefixes that require a session. A request without a session cookie
35
131
  * to any of these is redirected to `signInPath`. Matched with `startsWith`.
@@ -86,34 +182,19 @@ export function createAuthMiddleware(config: AuthMiddlewareConfig) {
86
182
  );
87
183
  }
88
184
 
89
- return function middleware(request: NextRequest): NextResponse {
185
+ return function middleware(
186
+ request: NextRequest,
187
+ options: AuthMiddlewareOptions = {},
188
+ ): NextResponse {
90
189
  const path = request.nextUrl.pathname;
91
190
 
92
- // 1. Stale-session handshake. The validated guard sends an invalid session
93
- // to `${signInPath}?stale=1&next=…`. Clear the dead Better Auth cookies
94
- // (only middleware can, pre-render) and bounce to a clean sign-in URL
95
- // that keeps `next`. After this the cookie is gone, so it can't re-arm.
96
- if (path === signInPath && request.nextUrl.searchParams.get("stale") === "1") {
97
- const to = request.nextUrl.clone();
98
- to.searchParams.delete("stale");
99
- const res = NextResponse.redirect(to);
100
- for (const cookie of request.cookies.getAll()) {
101
- if (cookie.name.includes(cookiePrefix)) {
102
- // `__Secure-`/`__Host-` cookies: browsers reject the deletion
103
- // Set-Cookie unless it carries the Secure attribute itself, so a
104
- // bare delete() would silently leave the dead cookie in place on
105
- // HTTPS and re-run this handshake on every visit.
106
- res.cookies.delete({
107
- name: cookie.name,
108
- path: "/",
109
- secure:
110
- cookie.name.startsWith("__Secure-") ||
111
- cookie.name.startsWith("__Host-"),
112
- });
113
- }
114
- }
115
- return res;
116
- }
191
+ // 1. Stale-session handshake: clear the dead cookies and bounce to a clean
192
+ // sign-in URL that keeps `next`.
193
+ const stale = clearStaleSession(request, {
194
+ signInPath,
195
+ sessionCookiePrefix: cookiePrefix,
196
+ });
197
+ if (stale) return stale;
117
198
 
118
199
  const hasSessionCookie = !!getSessionCookie(request);
119
200
 
@@ -123,9 +204,14 @@ export function createAuthMiddleware(config: AuthMiddlewareConfig) {
123
204
  const original = request.nextUrl.pathname + request.nextUrl.search;
124
205
  const to = request.nextUrl.clone();
125
206
  to.pathname = signInPath;
126
- to.search = "";
127
- const next = safeNextParam(original);
128
- if (next) to.searchParams.set("next", next);
207
+ to.search = new URL(
208
+ signInUrl(signInPath, {
209
+ next: original,
210
+ nextParam: config.nextParam,
211
+ isSafeNext: config.isSafeNext,
212
+ }),
213
+ request.nextUrl,
214
+ ).search;
129
215
  return NextResponse.redirect(to);
130
216
  }
131
217
 
@@ -143,8 +229,8 @@ export function createAuthMiddleware(config: AuthMiddlewareConfig) {
143
229
 
144
230
  // 4. Pass through, injecting the requested path so server guards can build
145
231
  // `next` for the cookie-present-but-invalid case.
146
- const requestHeaders = new Headers(request.headers);
147
- requestHeaders.set(NK_AUTH_PATH_HEADER, path + request.nextUrl.search);
232
+ const requestHeaders = options.requestHeaders ?? new Headers(request.headers);
233
+ withAuthPathHeader(request, requestHeaders);
148
234
  return NextResponse.next({ request: { headers: requestHeaders } });
149
235
  };
150
236
  }
package/src/server.ts CHANGED
@@ -28,7 +28,11 @@ import type { IdHelper } from "@ingram-tech/nk-db/id";
28
28
  import { cookies, headers } from "next/headers";
29
29
  import { redirect } from "next/navigation";
30
30
  import { cache } from "react";
31
- import { NK_AUTH_PATH_HEADER, signInUrl } from "./gating-internals.js";
31
+ import {
32
+ NK_AUTH_PATH_HEADER,
33
+ type NextParamOptions,
34
+ signInUrl,
35
+ } from "./gating-internals.js";
32
36
  import { CREDENTIAL_PROVIDER_ID } from "./password.js";
33
37
 
34
38
  /**
@@ -37,6 +41,7 @@ import { CREDENTIAL_PROVIDER_ID } from "./password.js";
37
41
  * `?next=` so it can't become an open redirect.
38
42
  */
39
43
  export { safeNextParam as safeNext } from "./gating-internals.js";
44
+ export type { NextParamOptions } from "./gating-internals.js";
40
45
 
41
46
  /** The shape we need from a session: anything carrying a `user`. */
42
47
  interface SessionLike {
@@ -75,7 +80,7 @@ export interface SessionIds {
75
80
  organization?: IdHelper;
76
81
  }
77
82
 
78
- export interface AuthHelpersOptions {
83
+ export interface AuthHelpersOptions extends NextParamOptions {
79
84
  /** Where guards send unauthenticated users. Default `/login`. */
80
85
  signInPath?: string;
81
86
  /**
@@ -163,17 +168,46 @@ export function createAuthHelpers<S extends SessionLike>(
163
168
  return session?.user ?? null;
164
169
  }
165
170
 
171
+ // The missing-header warning fires once per helpers instance, not once per
172
+ // request: it is a wiring mistake, and a wiring mistake does not get less
173
+ // wrong the second time.
174
+ let warnedMissingHeader = false;
175
+
166
176
  /**
167
177
  * The sign-in redirect for a request with no valid session: keep the
168
178
  * requested path as `next` (from the middleware-injected header), and flag
169
179
  * `stale=1` when a session cookie is present-but-invalid so the middleware
170
180
  * clears it. The two reads are request-scoped and cached by Next.
181
+ *
182
+ * Exported for a site that wraps the guards in its own (a profile lookup on
183
+ * top of `getUser()`, say): `redirect(await signInTarget())` gets `next` and
184
+ * `stale` right without re-deriving either.
185
+ *
186
+ * `next` depends on the `x-nk-auth-path` header, which only middleware can
187
+ * set. When it is absent the destination is silently lost — the user signs in
188
+ * and lands on the default page instead of the one they asked for — so
189
+ * outside production this says so once, naming the two ways to wire it.
171
190
  */
172
191
  async function signInTarget(): Promise<string> {
173
192
  const [h, c] = await Promise.all([headers(), cookies()]);
174
193
  const next = h.get(NK_AUTH_PATH_HEADER);
194
+ if (
195
+ next === null &&
196
+ !warnedMissingHeader &&
197
+ process.env.NODE_ENV !== "production"
198
+ ) {
199
+ warnedMissingHeader = true;
200
+ console.warn(
201
+ `@ingram-tech/nk-auth: \`next\` not preserved — no ${NK_AUTH_PATH_HEADER} request header. Mount createAuthMiddleware, or call withAuthPathHeader(request, requestHeaders) from "@ingram-tech/nk-auth/middleware" in your own proxy.`,
202
+ );
203
+ }
175
204
  const stale = c.getAll().some((ck) => ck.name.includes(cookiePrefix));
176
- return signInUrl(signInPath, { next, stale });
205
+ return signInUrl(signInPath, {
206
+ next,
207
+ stale,
208
+ nextParam: options.nextParam,
209
+ isSafeNext: options.isSafeNext,
210
+ });
177
211
  }
178
212
 
179
213
  /**
@@ -238,6 +272,7 @@ export function createAuthHelpers<S extends SessionLike>(
238
272
  getUser,
239
273
  requireSession,
240
274
  requireUser,
275
+ signInTarget,
241
276
  redirectIfAuthenticated,
242
277
  getLinkedProviders,
243
278
  hasCredentialAccount,