@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 +27 -9
- package/dist/gating-internals.d.ts +27 -0
- package/dist/gating-internals.d.ts.map +1 -0
- package/dist/gating-internals.js +39 -0
- package/dist/gating-internals.js.map +1 -0
- package/dist/middleware.d.ts +6 -0
- package/dist/middleware.d.ts.map +1 -1
- package/dist/middleware.js +47 -13
- package/dist/middleware.js.map +1 -1
- package/dist/server.d.ts +19 -4
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +45 -22
- package/dist/server.js.map +1 -1
- package/package.json +3 -3
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
|
|
21
|
-
| `createAuthMiddleware` (`./middleware`) | loop-safe edge middleware
|
|
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
|
-
|
|
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
|
-
|
|
215
|
-
|
|
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.
|
|
247
|
-
|
|
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"}
|
package/dist/middleware.d.ts
CHANGED
|
@@ -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
|
package/dist/middleware.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":"
|
|
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"}
|
package/dist/middleware.js
CHANGED
|
@@ -14,17 +14,24 @@
|
|
|
14
14
|
*
|
|
15
15
|
* The sign-in path is never the target of an optimistic redirect.
|
|
16
16
|
*
|
|
17
|
-
* It
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
54
|
-
|
|
55
|
-
|
|
82
|
+
const to = request.nextUrl.clone();
|
|
83
|
+
to.pathname = config.signedInRedirect;
|
|
84
|
+
to.search = "";
|
|
85
|
+
return NextResponse.redirect(to);
|
|
56
86
|
}
|
|
57
|
-
|
|
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
|
package/dist/middleware.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"middleware.js","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":"AAAA
|
|
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
|
|
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: (
|
|
21
|
-
requireUser: (
|
|
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
|
package/dist/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"
|
|
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,
|
|
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.
|
|
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
|
|
20
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
47
|
-
*
|
|
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(
|
|
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(
|
|
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
|
package/dist/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA
|
|
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.
|
|
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.
|
|
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": "
|
|
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",
|