@ingram-tech/nk-auth 0.6.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -17,8 +17,8 @@ only what you need from focused subpaths.
17
17
  | `createAuthPool` (`./pool`) | `pg` Pool with optional SSL CA verification |
18
18
  | `bcryptPassword`, `makeEmailSenders`, `makePasskeyOptions`, `uuidGenerateId` (`./`) | password migration, email hooks, passkeys, UUID ids |
19
19
  | `createServerSupabase` (`./`) | RLS-aware supabase-js client (attaches the session JWT) |
20
- | `createAuthHelpers` (`./server`) | validated App Router session helpers: `getSession` / `getUser` / `requireSession` / `requireUser` / `redirectIfAuthenticated` |
21
- | `createAuthMiddleware` (`./middleware`) | loop-safe edge middleware that only gates *unauthenticated* users off protected paths |
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 |
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
22
 
23
23
  > **Supabase Auth → Better Auth + RLS migration?** Read
24
24
  > [`docs/better-auth-migration.md`](../../docs/better-auth-migration.md) — the
@@ -203,16 +203,25 @@ export const {
203
203
  // app/dashboard/page.tsx — gate a protected page (validated, DB-backed).
204
204
  import { requireUser } from "@/lib/auth/session";
205
205
  export default async function Dashboard() {
206
- const user = await requireUser(); // -> /login when signed out
206
+ // -> /login?next=/dashboard when signed out (and ?stale=1 when the cookie is
207
+ // present-but-invalid, so middleware clears it). next/stale are automatic.
208
+ const user = await requireUser();
207
209
  return <main>Hi {user.email}</main>;
208
210
  }
209
211
 
210
- // app/login/page.tsx — gate the sign-in page HERE, never in middleware.
212
+ // app/login/page.tsx — gate the sign-in page HERE, never in middleware. Honor
213
+ // `next` so sign-in returns the user to where they were headed.
211
214
  import { redirectIfAuthenticated } from "@/lib/auth/session";
215
+ import { safeNext } from "@ingram-tech/nk-auth/server";
212
216
  import { LoginForm } from "./login-form";
213
- export default async function Login() {
214
- await redirectIfAuthenticated("/dashboard"); // validated: a stale cookie
215
- return <LoginForm />; // resolves to "signed out" and falls through to the form
217
+ export default async function Login({
218
+ searchParams,
219
+ }: {
220
+ searchParams: Promise<{ next?: string }>;
221
+ }) {
222
+ const next = safeNext((await searchParams).next) ?? "/dashboard";
223
+ await redirectIfAuthenticated(next); // validated: a stale cookie resolves to
224
+ return <LoginForm next={next} />; // "signed out" and falls through to the form
216
225
  }
217
226
  ```
218
227
 
@@ -243,8 +252,17 @@ can only trust the cookie *exists*. The server helpers hit `auth.api.getSession`
243
252
  and check the session *contents*. When those disagree (revoked session, rotated
244
253
  secret, wiped DB) the validated layer wins and parks the user on `/login` — and
245
254
  because middleware refuses to bounce `/login`, the form renders instead of
246
- ping-ponging. Middleware is optional; sites that prefer one source of truth can
247
- use the server helpers alone.
255
+ ping-ponging.
256
+
257
+ **Self-heal.** A bad session doesn't strand the user. The guard redirects to
258
+ `/login?next=<where they were>&stale=1`; middleware (the only pre-render place
259
+ that can touch cookies) deletes the dead Better Auth cookies on the `stale`
260
+ marker and bounces to a clean `/login?next=…`; signing in returns them to
261
+ `next`. `next` for the cookie-less case is filled in by middleware directly; for
262
+ the cookie-present case the guard reads it from the `x-nk-auth-path` header
263
+ middleware injects — so the self-heal (next + clearing) needs the middleware.
264
+ Sites that skip middleware still get validated gating from the server helpers,
265
+ just without automatic `next`/clearing.
248
266
 
249
267
  ## RLS bridge (the important part)
250
268
 
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Internal helpers shared by the server helpers and the middleware. Kept in a
3
+ * zero-`next` module so neither subpath imports the other (server pulls
4
+ * next/navigation, middleware pulls next/server — mixing them breaks the
5
+ * respective runtimes). Not a public export.
6
+ */
7
+ /**
8
+ * Request header the middleware sets to the originally-requested path so a
9
+ * server-component guard — which Next.js otherwise gives no way to learn its own
10
+ * URL — can preserve it as a `next` param when it redirects to sign-in.
11
+ */
12
+ export declare const NK_AUTH_PATH_HEADER = "x-nk-auth-path";
13
+ /**
14
+ * Accept only an internal, non-protocol-relative path as a post-login redirect,
15
+ * so `next` can never be turned into an open redirect to another origin.
16
+ */
17
+ export declare function safeNextParam(value: string | null | undefined): string | null;
18
+ /**
19
+ * Build a sign-in URL: `signInPath`, plus `next` (when safe) so the user returns
20
+ * to where they were headed, plus a `stale=1` marker when a session cookie is
21
+ * present but invalid — the signal the middleware uses to clear the dead cookie.
22
+ */
23
+ export declare function signInUrl(signInPath: string, opts: {
24
+ next?: string | null;
25
+ stale?: boolean;
26
+ }): string;
27
+ //# sourceMappingURL=gating-internals.d.ts.map
@@ -0,0 +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;;;GAGG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAI7E;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"}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Internal helpers shared by the server helpers and the middleware. Kept in a
3
+ * zero-`next` module so neither subpath imports the other (server pulls
4
+ * next/navigation, middleware pulls next/server — mixing them breaks the
5
+ * respective runtimes). Not a public export.
6
+ */
7
+ /**
8
+ * Request header the middleware sets to the originally-requested path so a
9
+ * server-component guard — which Next.js otherwise gives no way to learn its own
10
+ * URL — can preserve it as a `next` param when it redirects to sign-in.
11
+ */
12
+ export const NK_AUTH_PATH_HEADER = "x-nk-auth-path";
13
+ /**
14
+ * Accept only an internal, non-protocol-relative path as a post-login redirect,
15
+ * so `next` can never be turned into an open redirect to another origin.
16
+ */
17
+ export function safeNextParam(value) {
18
+ if (!value)
19
+ return null;
20
+ if (!value.startsWith("/") || value.startsWith("//"))
21
+ return null;
22
+ return value;
23
+ }
24
+ /**
25
+ * Build a sign-in URL: `signInPath`, plus `next` (when safe) so the user returns
26
+ * to where they were headed, plus a `stale=1` marker when a session cookie is
27
+ * present but invalid — the signal the middleware uses to clear the dead cookie.
28
+ */
29
+ export function signInUrl(signInPath, opts) {
30
+ const params = new URLSearchParams();
31
+ const next = safeNextParam(opts.next);
32
+ if (next)
33
+ params.set("next", next);
34
+ if (opts.stale)
35
+ params.set("stale", "1");
36
+ const qs = params.toString();
37
+ return qs ? `${signInPath}?${qs}` : signInPath;
38
+ }
39
+ //# sourceMappingURL=gating-internals.js.map
@@ -0,0 +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;;;GAGG;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,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"}
@@ -15,6 +15,12 @@ export interface AuthMiddlewareConfig {
15
15
  frontDoorPaths?: string[];
16
16
  /** Destination for the front-door redirect. Required when `frontDoorPaths` is set. */
17
17
  signedInRedirect?: string;
18
+ /**
19
+ * Cookie-name fragment for the cookies cleared on a stale session. Default
20
+ * `better-auth`, which matches `better-auth.session_token` and the
21
+ * `__Secure-…` production variant.
22
+ */
23
+ sessionCookiePrefix?: string;
18
24
  }
19
25
  export declare function createAuthMiddleware(config: AuthMiddlewareConfig): (request: NextRequest) => NextResponse;
20
26
  //# sourceMappingURL=middleware.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":"AAuBA,OAAO,EAAE,KAAK,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE7D,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;CAC1B;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,oBAAoB,IAwBrC,SAAS,WAAW,KAAG,YAAY,CAyB9D"}
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,CAoD9D"}
@@ -14,17 +14,24 @@
14
14
  *
15
15
  * The sign-in path is never the target of an optimistic redirect.
16
16
  *
17
- * It will only ever (a) push *cookie-less* requests off `protectedPaths`, and
18
- * optionally (b) push *cookie-bearing* requests off a front door to the app. It
19
- * refuses to protect or front-door the sign-in path, because either would
20
- * reintroduce the loop. Sending a *signed-in* user away from /login is the job
21
- * of `redirectIfAuthenticated` in the server helpers, which validates first.
17
+ * It also owns the two things only middleware can do here:
18
+ *
19
+ * - **Preserve the destination.** When it sends an unauthenticated user to
20
+ * sign-in, it appends `?next=<requested path>`; and it injects an
21
+ * `x-nk-auth-path` request header so the server guards can do the same for
22
+ * the cookie-present-but-invalid case (an RSC can't otherwise learn its URL).
23
+ * - **Clear a stale cookie.** The guard parks an invalid session at
24
+ * `${signInPath}?stale=1`; middleware (the only pre-render place that can set
25
+ * cookies) deletes the dead Better Auth cookies and bounces to a clean
26
+ * sign-in URL, so a bad session self-heals instead of failing every request.
22
27
  */
23
28
  import { getSessionCookie } from "better-auth/cookies";
24
29
  import { NextResponse } from "next/server";
30
+ import { NK_AUTH_PATH_HEADER, safeNextParam } from "./gating-internals";
25
31
  export function createAuthMiddleware(config) {
26
32
  const signInPath = config.signInPath ?? "/login";
27
33
  const frontDoorPaths = config.frontDoorPaths ?? [];
34
+ const cookiePrefix = config.sessionCookiePrefix ?? "better-auth";
28
35
  // Loop-safety, enforced once at construction rather than hoped-for per
29
36
  // request. The sign-in path is where the validated guard parks an
30
37
  // invalid-but-present session; it must never be the target of an optimistic
@@ -39,22 +46,49 @@ export function createAuthMiddleware(config) {
39
46
  throw new Error("@ingram-tech/nk-auth: signedInRedirect is required when frontDoorPaths is set.");
40
47
  }
41
48
  return function middleware(request) {
42
- const hasSessionCookie = !!getSessionCookie(request);
43
49
  const path = request.nextUrl.pathname;
50
+ // 1. Stale-session handshake. The validated guard sends an invalid session
51
+ // to `${signInPath}?stale=1&next=…`. Clear the dead Better Auth cookies
52
+ // (only middleware can, pre-render) and bounce to a clean sign-in URL
53
+ // that keeps `next`. After this the cookie is gone, so it can't re-arm.
54
+ if (path === signInPath && request.nextUrl.searchParams.get("stale") === "1") {
55
+ const to = request.nextUrl.clone();
56
+ to.searchParams.delete("stale");
57
+ const res = NextResponse.redirect(to);
58
+ for (const cookie of request.cookies.getAll()) {
59
+ if (cookie.name.includes(cookiePrefix))
60
+ res.cookies.delete(cookie.name);
61
+ }
62
+ return res;
63
+ }
64
+ const hasSessionCookie = !!getSessionCookie(request);
65
+ // 2. Unauthenticated (no cookie) on a protected path -> sign in, and
66
+ // remember where they were going.
44
67
  if (!hasSessionCookie &&
45
68
  config.protectedPaths.some((p) => path.startsWith(p))) {
46
- const url = request.nextUrl.clone();
47
- url.pathname = signInPath;
48
- return NextResponse.redirect(url);
69
+ const original = request.nextUrl.pathname + request.nextUrl.search;
70
+ const to = request.nextUrl.clone();
71
+ to.pathname = signInPath;
72
+ to.search = "";
73
+ const next = safeNextParam(original);
74
+ if (next)
75
+ to.searchParams.set("next", next);
76
+ return NextResponse.redirect(to);
49
77
  }
78
+ // 3. Front door: a cookie-bearing visit to "/" (or configured) -> the app.
50
79
  if (hasSessionCookie &&
51
80
  config.signedInRedirect &&
52
81
  frontDoorPaths.includes(path)) {
53
- const url = request.nextUrl.clone();
54
- url.pathname = config.signedInRedirect;
55
- return NextResponse.redirect(url);
82
+ const to = request.nextUrl.clone();
83
+ to.pathname = config.signedInRedirect;
84
+ to.search = "";
85
+ return NextResponse.redirect(to);
56
86
  }
57
- return NextResponse.next();
87
+ // 4. Pass through, injecting the requested path so server guards can build
88
+ // `next` for the cookie-present-but-invalid case.
89
+ const requestHeaders = new Headers(request.headers);
90
+ requestHeaders.set(NK_AUTH_PATH_HEADER, path + request.nextUrl.search);
91
+ return NextResponse.next({ request: { headers: requestHeaders } });
58
92
  };
59
93
  }
60
94
  //# sourceMappingURL=middleware.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"middleware.js","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,EAAoB,YAAY,EAAE,MAAM,aAAa,CAAC;AAoB7D,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;IAEnD,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,gBAAgB,GAAG,CAAC,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC;QACrD,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC;QAEtC,IACC,CAAC,gBAAgB;YACjB,MAAM,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,EACpD,CAAC;YACF,MAAM,GAAG,GAAG,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;YACpC,GAAG,CAAC,QAAQ,GAAG,UAAU,CAAC;YAC1B,OAAO,YAAY,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;QACnC,CAAC;QAED,IACC,gBAAgB;YAChB,MAAM,CAAC,gBAAgB;YACvB,cAAc,CAAC,QAAQ,CAAC,IAAI,CAAC,EAC5B,CAAC;YACF,MAAM,GAAG,GAAG,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;YACpC,GAAG,CAAC,QAAQ,GAAG,MAAM,CAAC,gBAAgB,CAAC;YACvC,OAAO,YAAY,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;QACnC,CAAC;QAED,OAAO,YAAY,CAAC,IAAI,EAAE,CAAC;IAC5B,CAAC,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"middleware.js","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":"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"}
package/dist/server.d.ts CHANGED
@@ -1,3 +1,9 @@
1
+ /**
2
+ * Validate a `next` redirect param: returns it only if it's an internal,
3
+ * non-protocol-relative path, else null. Use on the login page before honoring
4
+ * `?next=` so it can't become an open redirect.
5
+ */
6
+ export { safeNextParam as safeNext } from "./gating-internals";
1
7
  /** The shape we need from a session: anything carrying a `user`. */
2
8
  interface SessionLike {
3
9
  user: unknown;
@@ -14,12 +20,21 @@ interface AuthLike<S extends SessionLike> {
14
20
  }) => Promise<S | null>;
15
21
  };
16
22
  }
17
- export declare function createAuthHelpers<S extends SessionLike>(auth: AuthLike<S>): {
23
+ export interface AuthHelpersOptions {
24
+ /** Where guards send unauthenticated users. Default `/login`. */
25
+ signInPath?: string;
26
+ /**
27
+ * Cookie-name fragment that identifies the Better Auth session cookie, used
28
+ * to detect a present-but-invalid (stale) cookie. Default `better-auth`,
29
+ * which matches `better-auth.session_token` and `__Secure-…` in production.
30
+ */
31
+ sessionCookiePrefix?: string;
32
+ }
33
+ export declare function createAuthHelpers<S extends SessionLike>(auth: AuthLike<S>, options?: AuthHelpersOptions): {
18
34
  getSession: () => Promise<S | null>;
19
35
  getUser: () => Promise<S["user"] | null>;
20
- requireSession: (redirectTo?: string) => Promise<S>;
21
- requireUser: (redirectTo?: string) => Promise<S["user"]>;
36
+ requireSession: () => Promise<S>;
37
+ requireUser: () => Promise<S["user"]>;
22
38
  redirectIfAuthenticated: (to: string) => Promise<void>;
23
39
  };
24
- export {};
25
40
  //# sourceMappingURL=server.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAwBA,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;KAC/D,CAAC;CACF;AAED,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,WAAW,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC;sBAE5C,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;mBAgBpB,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC;6CARG,OAAO,CAAC,CAAC,CAAC;0CAkBb,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;kCAa1B,MAAM,KAAG,OAAO,CAAC,IAAI,CAAC;EAWjE"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AA4BA;;;;GAIG;AACH,OAAO,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAE/D,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;KAC/D,CAAC;CACF;AAED,MAAM,WAAW,kBAAkB;IAClC,iEAAiE;IACjE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,WAAW,EACtD,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC,EACjB,OAAO,GAAE,kBAAuB;sBAMH,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;mBAKpB,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;EAWjE"}
package/dist/server.js CHANGED
@@ -7,53 +7,76 @@
7
7
  * import { createAuthHelpers } from "@ingram-tech/nk-auth/server";
8
8
  * import { auth } from "@/lib/auth";
9
9
  *
10
- * export const { getSession, getUser, requireUser, redirectIfAuthenticated } =
11
- * createAuthHelpers(auth);
10
+ * export const { getSession, getUser, requireSession, requireUser,
11
+ * redirectIfAuthenticated } = createAuthHelpers(auth);
12
12
  *
13
13
  * These are the **validated** authority: every call hits `auth.api.getSession`,
14
14
  * which checks the session against the database — not merely the presence of a
15
- * cookie. That distinction is the whole point. The optimistic, cookie-presence
15
+ * cookie. When a guard finds no valid session it redirects to the sign-in page,
16
+ * preserving the requested path as `next` (read from the header the middleware
17
+ * injects) and, when a session cookie is present-but-invalid, adding `stale=1`
18
+ * so the middleware clears the dead cookie. The optimistic, cookie-presence
16
19
  * check belongs in middleware (see "@ingram-tech/nk-auth/middleware"); it may
17
20
  * only ever push *unauthenticated* users off protected routes. The decision to
18
21
  * send a signed-in user *away* from the sign-in page must run through
19
- * `redirectIfAuthenticated` here, so a stale or revoked cookie resolves to "no
20
- * session" and falls through to the form instead of ping-ponging forever.
22
+ * `redirectIfAuthenticated` here, so a stale cookie resolves to "no session"
23
+ * and falls through to the form instead of ping-ponging forever.
21
24
  */
22
- import { headers } from "next/headers";
25
+ import { cookies, headers } from "next/headers";
23
26
  import { redirect } from "next/navigation";
24
- export function createAuthHelpers(auth) {
27
+ import { NK_AUTH_PATH_HEADER, signInUrl } from "./gating-internals";
28
+ /**
29
+ * Validate a `next` redirect param: returns it only if it's an internal,
30
+ * non-protocol-relative path, else null. Use on the login page before honoring
31
+ * `?next=` so it can't become an open redirect.
32
+ */
33
+ export { safeNextParam as safeNext } from "./gating-internals";
34
+ export function createAuthHelpers(auth, options = {}) {
35
+ const signInPath = options.signInPath ?? "/login";
36
+ const cookiePrefix = options.sessionCookiePrefix ?? "better-auth";
25
37
  /** The validated session ({ user, session, … }) or null. */
26
38
  async function getSession() {
27
39
  return auth.api.getSession({ headers: await headers() });
28
40
  }
29
- /**
30
- * Require a session or redirect, returning the full validated session (use
31
- * when the caller needs more than the user — session id, active org, …).
32
- */
33
- async function requireSession(redirectTo = "/login") {
34
- const session = await getSession();
35
- // redirect() is typed `never`, so `session` narrows to non-null below.
36
- if (!session)
37
- redirect(redirectTo);
38
- return session;
39
- }
40
41
  /** The authenticated user, or null. */
41
42
  async function getUser() {
42
43
  const session = await getSession();
43
44
  return session?.user ?? null;
44
45
  }
45
46
  /**
46
- * Require a signed-in user or redirect. Returns the user (non-null) so
47
- * callers keep their `const user = await requireUser()` shape. `redirect()`
47
+ * The sign-in redirect for a request with no valid session: keep the
48
+ * requested path as `next` (from the middleware-injected header), and flag
49
+ * `stale=1` when a session cookie is present-but-invalid so the middleware
50
+ * clears it. The two reads are request-scoped and cached by Next.
51
+ */
52
+ async function signInTarget() {
53
+ const [h, c] = await Promise.all([headers(), cookies()]);
54
+ const next = h.get(NK_AUTH_PATH_HEADER);
55
+ const stale = c.getAll().some((ck) => ck.name.includes(cookiePrefix));
56
+ return signInUrl(signInPath, { next, stale });
57
+ }
58
+ /**
59
+ * Require a signed-in user or redirect to sign-in. Returns the user (non-null)
60
+ * so callers keep their `const user = await requireUser()` shape. `redirect()`
48
61
  * throws, so control never returns when signed out.
49
62
  */
50
- async function requireUser(redirectTo = "/login") {
63
+ async function requireUser() {
51
64
  const user = await getUser();
52
65
  // redirect() is typed `never`, so `user` narrows to non-null below.
53
66
  if (!user)
54
- redirect(redirectTo);
67
+ redirect(await signInTarget());
55
68
  return user;
56
69
  }
70
+ /**
71
+ * Require a session or redirect, returning the full validated session (use
72
+ * when the caller needs more than the user — session id, active org, …).
73
+ */
74
+ async function requireSession() {
75
+ const session = await getSession();
76
+ if (!session)
77
+ redirect(await signInTarget());
78
+ return session;
79
+ }
57
80
  /**
58
81
  * Redirect an already-signed-in user away (e.g. /login -> /dashboard). This
59
82
  * is the *correct* place to gate the sign-in page: because it validates the
@@ -1 +1 @@
1
- {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAkB3C,MAAM,UAAU,iBAAiB,CAAwB,IAAiB;IACzE,4DAA4D;IAC5D,KAAK,UAAU,UAAU;QACxB,OAAO,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,OAAO,EAAE,MAAM,OAAO,EAAE,EAAE,CAAC,CAAC;IAC1D,CAAC;IAED;;;OAGG;IACH,KAAK,UAAU,cAAc,CAAC,UAAU,GAAG,QAAQ;QAClD,MAAM,OAAO,GAAG,MAAM,UAAU,EAAE,CAAC;QACnC,uEAAuE;QACvE,IAAI,CAAC,OAAO;YAAE,QAAQ,CAAC,UAAU,CAAC,CAAC;QACnC,OAAO,OAAO,CAAC;IAChB,CAAC;IAED,uCAAuC;IACvC,KAAK,UAAU,OAAO;QACrB,MAAM,OAAO,GAAG,MAAM,UAAU,EAAE,CAAC;QACnC,OAAO,OAAO,EAAE,IAAI,IAAI,IAAI,CAAC;IAC9B,CAAC;IAED;;;;OAIG;IACH,KAAK,UAAU,WAAW,CAAC,UAAU,GAAG,QAAQ;QAC/C,MAAM,IAAI,GAAG,MAAM,OAAO,EAAE,CAAC;QAC7B,oEAAoE;QACpE,IAAI,CAAC,IAAI;YAAE,QAAQ,CAAC,UAAU,CAAC,CAAC;QAChC,OAAO,IAAI,CAAC;IACb,CAAC;IAED;;;;;OAKG;IACH,KAAK,UAAU,uBAAuB,CAAC,EAAU;QAChD,IAAI,MAAM,UAAU,EAAE;YAAE,QAAQ,CAAC,EAAE,CAAC,CAAC;IACtC,CAAC;IAED,OAAO;QACN,UAAU;QACV,OAAO;QACP,cAAc;QACd,WAAW;QACX,uBAAuB;KACvB,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAC3C,OAAO,EAAE,mBAAmB,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAEpE;;;;GAIG;AACH,OAAO,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AA6B/D,MAAM,UAAU,iBAAiB,CAChC,IAAiB,EACjB,UAA8B,EAAE;IAEhC,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,QAAQ,CAAC;IAClD,MAAM,YAAY,GAAG,OAAO,CAAC,mBAAmB,IAAI,aAAa,CAAC;IAElE,4DAA4D;IAC5D,KAAK,UAAU,UAAU;QACxB,OAAO,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,OAAO,EAAE,MAAM,OAAO,EAAE,EAAE,CAAC,CAAC;IAC1D,CAAC;IAED,uCAAuC;IACvC,KAAK,UAAU,OAAO;QACrB,MAAM,OAAO,GAAG,MAAM,UAAU,EAAE,CAAC;QACnC,OAAO,OAAO,EAAE,IAAI,IAAI,IAAI,CAAC;IAC9B,CAAC;IAED;;;;;OAKG;IACH,KAAK,UAAU,YAAY;QAC1B,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC;QACzD,MAAM,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC;QACxC,MAAM,KAAK,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC,CAAC;QACtE,OAAO,SAAS,CAAC,UAAU,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IAC/C,CAAC;IAED;;;;OAIG;IACH,KAAK,UAAU,WAAW;QACzB,MAAM,IAAI,GAAG,MAAM,OAAO,EAAE,CAAC;QAC7B,oEAAoE;QACpE,IAAI,CAAC,IAAI;YAAE,QAAQ,CAAC,MAAM,YAAY,EAAE,CAAC,CAAC;QAC1C,OAAO,IAAI,CAAC;IACb,CAAC;IAED;;;OAGG;IACH,KAAK,UAAU,cAAc;QAC5B,MAAM,OAAO,GAAG,MAAM,UAAU,EAAE,CAAC;QACnC,IAAI,CAAC,OAAO;YAAE,QAAQ,CAAC,MAAM,YAAY,EAAE,CAAC,CAAC;QAC7C,OAAO,OAAO,CAAC;IAChB,CAAC;IAED;;;;;OAKG;IACH,KAAK,UAAU,uBAAuB,CAAC,EAAU;QAChD,IAAI,MAAM,UAAU,EAAE;YAAE,QAAQ,CAAC,EAAE,CAAC,CAAC;IACtC,CAAC;IAED,OAAO;QACN,UAAU;QACV,OAAO;QACP,cAAc;QACd,WAAW;QACX,uBAAuB;KACvB,CAAC;AACH,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ingram-tech/nk-auth",
3
- "version": "0.6.0",
3
+ "version": "0.7.1",
4
4
  "description": "The Ingram Better Auth foundation: composable presets (org, dual-shape JWT, Supabase RLS bridge, active-org hooks, pg pool) for Next.js sites.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -61,7 +61,7 @@
61
61
  "test": "vitest run"
62
62
  },
63
63
  "dependencies": {
64
- "@ingram-tech/nk-db": "^0.3.0",
64
+ "@ingram-tech/nk-db": "^0.4.0",
65
65
  "bcrypt": "^6.0.0",
66
66
  "jose": "^6.0.0",
67
67
  "zod": "^4.0.0"
@@ -93,7 +93,7 @@
93
93
  },
94
94
  "devDependencies": {
95
95
  "@better-auth/passkey": "^1.6.0",
96
- "@ingram-tech/typescript-config": "workspace:*",
96
+ "@ingram-tech/typescript-config": "0.1.0",
97
97
  "@supabase/supabase-js": "^2.45.0",
98
98
  "@types/bcrypt": "^6.0.0",
99
99
  "@types/node": "^25.0.0",