@kirill.konshin/auth 0.0.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 ADDED
@@ -0,0 +1,199 @@
1
+ # @kirill.konshin/auth
2
+
3
+ Reusable [iron-session](https://github.com/vvo/iron-session) authentication for Next.js App Router. Consolidates the session module, layout/page guards, login flow, Route Handler guards and `proxy.ts` guard into one factory.
4
+
5
+ iron-session v8 has no official Next helpers (they were removed with the Pages Router) and no trustworthy third-party ones exist — this package is that layer.
6
+
7
+ ```bash
8
+ yarn add @kirill.konshin/auth iron-session server-only
9
+ ```
10
+
11
+ # Rules
12
+
13
+ - ALWAYS use this package for authentication in Next.js apps, NEVER hand-roll `iron-session` wrappers
14
+ - ONE auth instance per app in `src/auth.ts`: `createPasswordAuth` (password-protected admin) or `createAuth` (token/OAuth)
15
+ - Pass Next's `Route` as the routes generic (`createPasswordAuth<Route>` / `createAuth<Session, Route>`) so `urls` and the return-URL helpers are type-checked (`typedRoutes` is set by `defineNextConfig`)
16
+ - Auth module MUST start with `import 'server-only'` and MUST NOT be module-level `'use server'` (that would expose session getters as callable server actions) — export `loginAction`/`logoutAction` from the SAME module as thin wrappers with function-level `'use server'`, no separate `actions.ts`; ENFORCED BY AGENT: no ESLint rule checks this, add on creation, flag on review
17
+ - Client Components MUST NOT import the auth module — Server Components pass `loginAction`/`logoutAction` down as props
18
+ - Cookie name MUST be unique per app (`<app>_session`), NEVER generic `session`
19
+ - Cookie secret `SESSION_SECRET` (≥32 chars) and login password `ADMIN_PASSWORD` are SEPARATE env vars, never reuse one for the other
20
+ - NEVER compare passwords with `===`, use `verifyPassword` / `timingSafeEqualStrings` (timing-safe)
21
+ - Guard pages/layouts with `redirectNonAuthenticated()` (redirects to login), Server Actions and Route Handlers with `throwNonAuthenticated()` or `authenticatedRouteHandler()` (401), the whole `/api` surface with `proxyGuard()` in `proxy.ts`, login page with `redirectIfAuthenticated()`
22
+ - Login page is ONE Server Component file with `<form action={loginAction}>` — errors return via the `?error=` search param; a Client Component (`useActionState` + `loginFormAction`) ONLY for pending/inline-error UI
23
+ - Reuse `auth.urls.*` (resolved, defaults applied) instead of repeating route literals, e.g. `redirect(auth.urls.afterLogin)`
24
+ - Declare API visibility as `urls.public` / `urls.protected` globs and keep them exhaustive — `proxyGuard()` throws on overlap and warns (treating as public) on unknown paths; `allow` is only for request-level tweaks
25
+ - Session lifetime via `ttl` (seconds) ONLY, NEVER set `cookieOptions.maxAge` (iron-session derives it)
26
+ - Logout via `auth.logout()` (destroy + redirect)
27
+ - Preserve the return URL through the `state` search param / hidden form field on login redirects; ALWAYS extract it with `auth.getState()` (accepts `await searchParams`, `URLSearchParams`, `FormData`, `NextRequest`, raw string; allows internal paths only, never the login page, falls back to `urls.afterLogin` so the result is always usable), NEVER hand-parse `state`
28
+ - NEVER store the return URL in the session — concurrent tabs share the cookie and would clash; it is threaded exclusively via search params, form fields and the `x-pathname` request header
29
+
30
+ # Environment Variables
31
+
32
+ | Variable | Purpose | Required |
33
+ | ---------------- | --------------------------------------- | ----------------------------------------- |
34
+ | `SESSION_SECRET` | Cookie sealing secret, min 32 chars | yes (or `secret` config) |
35
+ | `ADMIN_PASSWORD` | Login password for `createPasswordAuth` | password auth only (or `password` config) |
36
+
37
+ # Canonical Patterns
38
+
39
+ Adapt paths (`/login`, `/app`, `@/auth`) to the project; keep the structure.
40
+
41
+ ## Session module — `src/auth.ts`
42
+
43
+ Password-protected app (admin dashboards, single-user tools):
44
+
45
+ ```ts
46
+ import 'server-only';
47
+
48
+ import { createPasswordAuth } from '@kirill.konshin/auth';
49
+ import type { Route } from 'next';
50
+
51
+ // <Route> makes urls and all return-URL helpers type-checked against real routes (typedRoutes)
52
+ export const auth = createPasswordAuth<Route>({
53
+ cookieName: 'myapp_session',
54
+ urls: {
55
+ login: '/login',
56
+ afterLogin: '/app',
57
+ public: ['/api/tiles/**', '/app/**'], // proxyGuard passes these; /app/** is enforced by its layout
58
+ protected: ['/api/admin/**'], // proxyGuard requires a session here
59
+ },
60
+ });
61
+
62
+ // Server actions live HERE, no separate actions.ts: the function-level 'use server' registers them
63
+ // in this app's build (a plain re-export would not — registration cannot happen in library code,
64
+ // and library closures are not serializable)
65
+ export const loginAction: typeof auth.loginAction = async (...args) => {
66
+ 'use server';
67
+ return auth.loginAction(...args); // plain <form action>: errors go back via ?error= search param
68
+ };
69
+
70
+ export const logoutAction: typeof auth.logout = async (...args) => {
71
+ 'use server';
72
+ return auth.logout(...args); // usable directly as <form action>, a FormData argument is ignored
73
+ };
74
+ ```
75
+
76
+ Client Components can NEVER import this module (`server-only`, and function-level actions are not client-importable) — Server Components (pages/layouts) pass `loginAction`/`logoutAction` down as props.
77
+
78
+ Token/OAuth session (custom shape + own `isValid`; the default checks the lib-owned `AUTHENTICATED` marker that `login()` sets — nothing to configure for password auth):
79
+
80
+ ```ts
81
+ import 'server-only';
82
+
83
+ import { createAuth } from '@kirill.konshin/auth';
84
+ import type { Route } from 'next';
85
+
86
+ export type Session = { accessToken?: string; refreshToken?: string; expiresAt?: number };
87
+
88
+ export const auth = createAuth<Session, Route>({
89
+ cookieName: 'myapp_session',
90
+ urls: { login: '/login', afterLogin: '/app' },
91
+ isValid: (session) => Boolean(session.accessToken && (!session.expiresAt || session.expiresAt > Date.now())),
92
+ });
93
+ ```
94
+
95
+ App-specific logic (e.g. OAuth token refresh) composes on top: `auth.setSession(await refreshToken(...))`, `auth.destroySession()` on refresh failure; add the same `logoutAction` wrapper as above (`loginFormAction` is password auth only).
96
+
97
+ ## Protected layout — `app/(auth)/layout.tsx` or `app/app/layout.tsx`
98
+
99
+ ```tsx
100
+ import { auth } from '@/auth';
101
+
102
+ export default async function Layout({ children }: LayoutProps<'/app'>) {
103
+ await auth.redirectNonAuthenticated(); // redirects to urls.login
104
+ return <>{children}</>;
105
+ }
106
+ ```
107
+
108
+ The deep current pathname is recovered automatically from the `x-pathname` header that `proxyGuard` stamps (cover page routes in the proxy matcher); without it login lands on `urls.afterLogin`. The login page MUST live OUTSIDE the guarded layout — the guard would redirect to itself; `urls.afterLogin` pointing at the login page throws at creation.
109
+
110
+ Route Handlers are not protected by layouts — guard them separately (below).
111
+
112
+ ## Login page — `app/login/page.tsx`
113
+
114
+ The whole login flow is ONE Server Component file — the form posts `loginAction` directly, failures come back as the `?error=` search param:
115
+
116
+ ```tsx
117
+ import { auth, loginAction } from '@/auth';
118
+
119
+ export default async function LoginPage({ searchParams }: PageProps<'/login'>) {
120
+ const params = await searchParams;
121
+ const state = auth.getState(params);
122
+ await auth.redirectIfAuthenticated(state);
123
+
124
+ return (
125
+ <form action={loginAction}>
126
+ {typeof params.error === 'string' && <p>{params.error}</p>}
127
+ <input type="hidden" name="state" value={state} />
128
+ <input type="password" name="password" required autoFocus />
129
+ <button type="submit">Login</button>
130
+ </form>
131
+ );
132
+ }
133
+ ```
134
+
135
+ A Client Component is only justified when it adds something — pending spinners, inline validation. Then wrap `auth.loginFormAction` (the `useActionState` variant that RETURNS the error instead of redirecting) and pass it down as a prop:
136
+
137
+ ```tsx
138
+ 'use client';
139
+ // <LoginForm loginAction={loginFormAction} state={state} /> rendered by the page above
140
+ import type { PasswordAuth } from '@kirill.konshin/auth';
141
+ import { useActionState } from 'react';
142
+
143
+ export function LoginForm({ loginAction, state }: { loginAction: PasswordAuth['loginFormAction']; state?: string }) {
144
+ const [error, action, isPending] = useActionState(loginAction, null);
145
+ // same form; disable the button with isPending, render {error} inline
146
+ }
147
+ ```
148
+
149
+ Admin login screens use MUI + `@toolpad/core` `SignInPage` per project rules — pass `loginFormAction` results into its `signIn` callback.
150
+
151
+ ## Route Handler — `app/api/*/route.ts`
152
+
153
+ ```ts
154
+ import { auth } from '@/auth';
155
+
156
+ export const GET = auth.authenticatedRouteHandler(async () => Response.json(await loadData()));
157
+ ```
158
+
159
+ Inside Server Actions use `await auth.throwNonAuthenticated()` (throws `Unauthorized`) instead.
160
+
161
+ ## Proxy guard (whole `/api` surface) — `src/proxy.ts`
162
+
163
+ ```ts
164
+ import { auth } from '@/auth';
165
+
166
+ export const config = { matcher: ['/api/:path*', '/app/:path*'] }; // literal, statically parsed by Next.js
167
+
168
+ export const proxy = auth.proxyGuard({ matcher: config.matcher });
169
+ ```
170
+
171
+ Next.js statically parses `proxy.ts`: `config` MUST be a literal object and the handler a visible function export — neither can be destructured from a runtime value. The matcher may be BROADER than the `urls` globs, but must cover them all: `proxyGuard` verifies the supplied `matcher` at creation and THROWS on uncovered globs (paths outside the matcher never reach the proxy at all). Omit `matcher`/`config` entirely to run the guard on every path.
172
+
173
+ The guard classifies each request by the `urls.public` / `urls.protected` globs: public passes without a session, protected gets the session check (`503` unconfigured / `401` unauthenticated), a path matching BOTH sets throws (config error), a path matching NEITHER logs a warning and passes as public — keep the glob lists exhaustive. Without any globs everything behind the matcher is protected. `allow: (request) => boolean` remains as an extra request-level bypass.
174
+
175
+ Every forwarded request is stamped with the `x-pathname` header (`PATHNAME_HEADER`) so `redirectNonAuthenticated()` in layouts can return users to the page they were on. List page routes in `urls.public` and cover them in the matcher: the session there is enforced by the layout (a `401` JSON would be wrong for pages), the guard only stamps the header.
176
+
177
+ # Migration From Hand-Rolled Modules
178
+
179
+ | Old pattern | Replacement |
180
+ | ------------------------------------------------------------ | --------------------------------------------------------- |
181
+ | `getIronSession(await cookies(), sessionOptions)` wrapper | `auth.getSession()` |
182
+ | `isLoggedIn()` / `isAdminAuthenticated()` / `getToken()` | `auth.isAuthenticated()` |
183
+ | `ensureAuth()` / `requireAdminSession()` (throwing) | `auth.throwNonAuthenticated()` |
184
+ | `authError()` returning a `401 Response` | `auth.authenticatedRouteHandler(handler)` |
185
+ | Layout `if (!token) redirect('/login?state=...')` | `auth.redirectNonAuthenticated()` |
186
+ | Login page `if (loggedIn) redirect(...)` | `auth.redirectIfAuthenticated(state)` |
187
+ | Hand-written `proxy.ts` / `middleware.ts` session check | `auth.proxyGuard({ allow })` |
188
+ | `session.destroy(); redirect('/login')` | `auth.logout()` |
189
+ | `password === candidate` | `auth.verifyPassword(candidate)` |
190
+ | `ttl` / `cookieOptions.maxAge` juggling | `ttl` config in seconds, nothing else |
191
+ | `'use server'` on the session module / separate `actions.ts` | function-level `'use server'` wrappers in the auth module |
192
+ | `SESSION_PASSWORD` / `PASSWORD` env vars | `SESSION_SECRET` + `ADMIN_PASSWORD` |
193
+ | `loginState(state)` / `typeof state === 'string'` fiddling | `auth.getState(...)` |
194
+ | Hand-rolled `x-pathname` proxy + `headers()` in layout | built into `proxyGuard` + `redirectNonAuthenticated()` |
195
+ | `defaultSession` constant (unused everywhere) | delete |
196
+
197
+ Checklist after migration: unique `cookieName`, secret only in `SESSION_SECRET`, no direct `getIronSession` imports left in the app, all four surfaces guarded (layout, login, route handlers, proxy).
198
+
199
+ Existing sessions: keeping the old `cookieName` and secret (both configurable) means old cookies still unseal — a session in an old shape simply fails `isValid`, so users are logged out naturally, never crash; `login()` starts a fresh session so stale fields from old shapes do not linger. A failed unseal (rotated secret) also degrades to logged-out.
package/dist/auth.d.ts ADDED
@@ -0,0 +1,148 @@
1
+ import { IronSession, SessionOptions } from 'iron-session';
2
+ import { NextRequest, NextResponse } from 'next/server';
3
+ import { AUTHENTICATED } from './client/index.js';
4
+ /** `R` is the route type — pass `Route` from `next` when `typedRoutes` is enabled */
5
+ export type AuthUrls<R extends string = string> = {
6
+ /** Login page, default `/login` */
7
+ login?: R;
8
+ /** Where successful login lands when no `state` param is present, default `/` */
9
+ afterLogin?: R;
10
+ /** Where logout lands, default `urls.login` */
11
+ afterLogout?: R;
12
+ /** Path globs `proxyGuard` serves without a session, e.g. `['/api/tiles/**']` — patterns, not routes */
13
+ public?: string[];
14
+ /** Path globs `proxyGuard` requires a session for, e.g. `['/api/admin/**']` — patterns, not routes */
15
+ protected?: string[];
16
+ };
17
+ export type AuthConfig<T extends object, R extends string = string> = {
18
+ /** MUST be unique per app, e.g. `myapp_session`, never a generic `session` */
19
+ cookieName: string;
20
+ /**
21
+ * Decides whether a session counts as authenticated. Defaults to checking the lib-owned
22
+ * `AUTHENTICATED` marker field (set by `createPasswordAuth`'s `login`, nothing to configure) —
23
+ * pass your own for token/OAuth or custom shapes (e.g. access token present and not expired)
24
+ */
25
+ isValid?: (session: T) => boolean;
26
+ /** Cookie sealing secret, min 32 chars, defaults to `process.env.SESSION_SECRET`, NOT the login password */
27
+ secret?: string;
28
+ /** Session lifetime in SECONDS, default 7 days; cookie `maxAge` is derived from it by iron-session, never set it */
29
+ ttl?: number;
30
+ /** Merged over secure defaults: `httpOnly`, `sameSite=lax`, `path=/`, `secure` in production */
31
+ cookieOptions?: SessionOptions['cookieOptions'];
32
+ urls?: AuthUrls<NoInfer<R>>;
33
+ /** Called by `throwNonAuthenticated` before throwing, e.g. `unauthorized` from `next/navigation` */
34
+ onUnauthorized?: () => never;
35
+ };
36
+ export type ProxyGuardOptions = {
37
+ /** Public requests that skip the auth check */
38
+ allow?: (request: NextRequest) => boolean;
39
+ /**
40
+ * The literal `config.matcher` exported from `proxy.ts`. Validated at creation: it may be
41
+ * broader than the `urls.public`/`urls.protected` globs, but every glob must be covered by it —
42
+ * uncovered paths would bypass the proxy entirely, so a gap THROWS (config error)
43
+ */
44
+ matcher?: string | string[];
45
+ };
46
+ /**
47
+ * App Router Route Handler shape — GET/POST/etc all share it (Next.js exports no public type for
48
+ * route handlers, only the app-generated `RouteContext` global)
49
+ */
50
+ export type RouteHandler<Context = {
51
+ params: Promise<Record<string, string | string[] | undefined>>;
52
+ }> = (request: NextRequest, context: Context) => Response | Promise<Response>;
53
+ /** Resolved `AuthUrls` with all defaults applied */
54
+ export type ResolvedAuthUrls<R extends string = string> = {
55
+ login: R;
56
+ afterLogin: R;
57
+ afterLogout: R;
58
+ public: readonly string[];
59
+ protected: readonly string[];
60
+ };
61
+ /** Anything the `state` return URL can be extracted from */
62
+ export type StateSource = string | null | undefined | URLSearchParams | FormData | NextRequest | Record<string, string | string[] | undefined>;
63
+ export type Auth<T extends object, R extends string = string> = {
64
+ /** Resolved URLs with defaults applied — reuse instead of literals, e.g. `redirect(auth.urls.afterLogin)` */
65
+ urls: ResolvedAuthUrls<R>;
66
+ isValid: (session: T) => boolean;
67
+ /** Both secret (and password for password auth) are present and usable, does not throw */
68
+ isConfigured: () => boolean;
69
+ getSessionOptions: () => SessionOptions;
70
+ getSession: () => Promise<IronSession<T>>;
71
+ /** For proxy / middleware where `cookies()` is not available */
72
+ getRequestSession: (request: Request, response: Response) => Promise<IronSession<T>>;
73
+ isAuthenticated: () => Promise<boolean>;
74
+ /** For Server Actions and Route Handlers: throws `Unauthorized` (or calls `onUnauthorized`) */
75
+ throwNonAuthenticated: () => Promise<IronSession<T>>;
76
+ /**
77
+ * For layouts and pages: redirects to `urls.login`, preserving `state` in the URL.
78
+ * Without it the current pathname is recovered from the `x-pathname` header (stamped by
79
+ * `proxyGuard` when its matcher covers page routes), falling back to `urls.afterLogin` where
80
+ * login lands anyway
81
+ */
82
+ redirectNonAuthenticated: (state?: R) => Promise<IronSession<T>>;
83
+ /** For the login page: redirects away when already authenticated */
84
+ redirectIfAuthenticated: (redirectTo?: R) => Promise<void>;
85
+ /**
86
+ * Extracts and sanitizes the `state` return URL from `await searchParams`, `URLSearchParams`,
87
+ * `FormData`, a `NextRequest` or a raw value: internal paths only (no open redirects), never
88
+ * the login page itself (no redirect loops); falls back to `urls.afterLogin` when absent or
89
+ * unsafe, so the result is always a usable target
90
+ */
91
+ getState: (source?: StateSource) => R;
92
+ /** Merges data into the session and saves it */
93
+ setSession: (data: Partial<T>) => Promise<IronSession<T>>;
94
+ destroySession: () => Promise<void>;
95
+ /**
96
+ * Destroys the session and redirects to `redirectTo ?? urls.afterLogout ?? urls.login`. Usable
97
+ * directly as a `<form action>` — a `FormData` argument is ignored. Expose through a
98
+ * function-level `'use server'` wrapper in the auth module (see README): action registration
99
+ * must happen in app code, a plain re-export is not registered
100
+ */
101
+ logout: (redirectTo?: R | FormData) => Promise<void>;
102
+ /** Wraps a Route Handler (any method — GET/POST/etc share one shape), responds `401` JSON when not authenticated */
103
+ authenticatedRouteHandler: <Context = {
104
+ params: Promise<Record<string, string | string[] | undefined>>;
105
+ }>(handler: RouteHandler<Context>) => RouteHandler<Context>;
106
+ /**
107
+ * The `proxy.ts` handler: `export const config = { matcher: [...] };` (a LITERAL — Next.js
108
+ * parses it statically) then `export const proxy = auth.proxyGuard({ matcher: config.matcher });`.
109
+ * The handler classifies requests by `urls.public` / `urls.protected` globs: public passes
110
+ * without a session, protected responds `503` when unconfigured and `401` JSON when not
111
+ * authenticated, a path matching BOTH glob sets throws (config error), a path matching NEITHER
112
+ * logs a warning and passes as public. Without any globs everything is protected. `allow` is an
113
+ * extra request-level bypass checked first. The supplied `matcher` is verified to cover all
114
+ * globs (it may be broader) — an uncovered glob throws at creation (config error)
115
+ */
116
+ proxyGuard: (options?: ProxyGuardOptions) => (request: NextRequest) => Promise<NextResponse>;
117
+ };
118
+ export type PasswordSession = {
119
+ [AUTHENTICATED]?: boolean;
120
+ };
121
+ export type PasswordAuthConfig<R extends string = string> = Omit<AuthConfig<PasswordSession, R>, 'isValid'> & {
122
+ /** Login password, defaults to `process.env.ADMIN_PASSWORD`, NOT the cookie secret */
123
+ password?: string;
124
+ };
125
+ export type PasswordAuth<R extends string = string> = Auth<PasswordSession, R> & {
126
+ /** Timing-safe, never compare passwords with `===` */
127
+ verifyPassword: (candidate: unknown) => boolean;
128
+ /** Verifies the password and marks the session logged in */
129
+ login: (candidate: unknown) => Promise<boolean>;
130
+ /**
131
+ * Plain `<form action>` for a Server-Component-only login page (the recommended pattern):
132
+ * expects `password` (and optional `state` return URL) form fields; on failure redirects back
133
+ * to `urls.login` with `?error=` (+ `state`), on success to `state` or `urls.afterLogin`.
134
+ * Expose through a function-level `'use server'` wrapper in the auth module (see README)
135
+ */
136
+ loginAction: (formData: FormData) => Promise<void>;
137
+ /**
138
+ * `useActionState` variant returning the error message instead of redirecting on failure — only
139
+ * for client components that need pending/error UI (or Toolpad `SignInPage`). Same wrapper rule
140
+ */
141
+ loginFormAction: (prevState: string | null | undefined, formData: FormData) => Promise<string>;
142
+ };
143
+ export declare const MIN_SECRET_LENGTH = 32;
144
+ /** Constant-time string comparison via SHA-256 digests, safe for values of different lengths */
145
+ export declare const timingSafeEqualStrings: (a: string, b: string) => boolean;
146
+ export declare function createAuth<T extends object = PasswordSession, R extends string = string>(config: AuthConfig<T, R>): Auth<T, R>;
147
+ export declare function createPasswordAuth<R extends string = string>(config: PasswordAuthConfig<R>): PasswordAuth<R>;
148
+ //# sourceMappingURL=auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"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"}
package/dist/auth.js ADDED
@@ -0,0 +1,209 @@
1
+ import { AUTHENTICATED, ERROR_PARAM, PATHNAME_HEADER, STATE_PARAM } from "./client/index.js";
2
+ import { createHash, timingSafeEqual } from "node:crypto";
3
+ import { getIronSession } from "iron-session";
4
+ import { cookies, headers } from "next/headers";
5
+ import { redirect } from "next/navigation";
6
+ import { NextResponse } from "next/server";
7
+ import picomatch from "picomatch";
8
+ //#region src/auth.ts
9
+ var MIN_SECRET_LENGTH = 32;
10
+ var DEFAULT_TTL = 3600 * 24 * 7;
11
+ var sha256 = (value) => createHash("sha256").update(value).digest();
12
+ /** Constant-time string comparison via SHA-256 digests, safe for values of different lengths */
13
+ var timingSafeEqualStrings = (a, b) => timingSafeEqual(sha256(a), sha256(b));
14
+ var withParams = (url, params) => {
15
+ const entries = Object.entries(params).filter((entry) => Boolean(entry[1]));
16
+ if (!entries.length) return url;
17
+ return `${url}${url.includes("?") ? "&" : "?"}${new URLSearchParams(entries)}`;
18
+ };
19
+ var withState = (url, state) => withParams(url, { [STATE_PARAM]: state });
20
+ var matcherToGlobs = (entry) => {
21
+ if (entry.endsWith("/:path*")) {
22
+ const prefix = entry.slice(0, -7);
23
+ return [prefix || "/", `${prefix}/**`];
24
+ }
25
+ return [entry.replace(/:\w+\*/g, "**").replace(/:\w+/g, "*")];
26
+ };
27
+ var defaultIsValid = (session) => session[AUTHENTICATED] === true;
28
+ var expandPrefix = (glob) => glob.endsWith("/**") ? [glob, glob.slice(0, -3) || "/"] : [glob];
29
+ function createAuth(config) {
30
+ const { cookieName, isValid = defaultIsValid, urls = {} } = config;
31
+ const loginUrl = urls.login ?? "/login";
32
+ const resolvedUrls = {
33
+ login: loginUrl,
34
+ afterLogin: urls.afterLogin ?? "/",
35
+ afterLogout: urls.afterLogout ?? loginUrl,
36
+ public: urls.public ?? [],
37
+ protected: urls.protected ?? []
38
+ };
39
+ if (loginUrl !== "/" && resolvedUrls.afterLogin.startsWith(loginUrl)) throw new Error("urls.afterLogin must not point at the login page, this would cause a redirect loop", { cause: {
40
+ cookieName,
41
+ ...urls
42
+ } });
43
+ const isPublicPath = urls.public?.length ? picomatch(urls.public.flatMap(expandPrefix)) : void 0;
44
+ const isProtectedPath = urls.protected?.length ? picomatch(urls.protected.flatMap(expandPrefix)) : void 0;
45
+ const resolveSecret = () => {
46
+ const secret = config.secret ?? process.env.SESSION_SECRET;
47
+ if (!secret || secret.length < 32) throw new Error(`Session secret must be at least 32 characters, set SESSION_SECRET or pass "secret"`, { cause: { cookieName } });
48
+ return secret;
49
+ };
50
+ const getSessionOptions = () => ({
51
+ cookieName,
52
+ password: resolveSecret(),
53
+ ttl: config.ttl ?? DEFAULT_TTL,
54
+ cookieOptions: {
55
+ httpOnly: true,
56
+ sameSite: "lax",
57
+ path: "/",
58
+ secure: process.env.NODE_ENV === "production",
59
+ ...config.cookieOptions
60
+ }
61
+ });
62
+ const isConfigured = () => {
63
+ try {
64
+ resolveSecret();
65
+ return true;
66
+ } catch {
67
+ return false;
68
+ }
69
+ };
70
+ const getSession = async () => getIronSession(await cookies(), getSessionOptions());
71
+ const isAuthenticated = async () => isValid(await getSession());
72
+ const throwNonAuthenticated = async () => {
73
+ const session = await getSession();
74
+ if (!isValid(session)) {
75
+ config.onUnauthorized?.();
76
+ throw new Error("Unauthorized");
77
+ }
78
+ return session;
79
+ };
80
+ const destroySession = async () => (await getSession()).destroy();
81
+ const getState = (source) => {
82
+ let value = source;
83
+ if (source instanceof URLSearchParams || source instanceof FormData) value = source.get(STATE_PARAM);
84
+ else if (source && typeof source === "object") value = "nextUrl" in source ? source.nextUrl.searchParams.get(STATE_PARAM) : source[STATE_PARAM];
85
+ if (Array.isArray(value)) value = value[0];
86
+ if (typeof value !== "string" || !value) return resolvedUrls.afterLogin;
87
+ if (!value.startsWith("/") || value.startsWith("//")) return resolvedUrls.afterLogin;
88
+ if (value.startsWith(loginUrl)) return resolvedUrls.afterLogin;
89
+ return value;
90
+ };
91
+ return {
92
+ urls: resolvedUrls,
93
+ isValid,
94
+ isConfigured,
95
+ getSessionOptions,
96
+ getSession,
97
+ getRequestSession: async (request, response) => getIronSession(request, response, getSessionOptions()),
98
+ isAuthenticated,
99
+ throwNonAuthenticated,
100
+ redirectNonAuthenticated: async (state) => {
101
+ const session = await getSession();
102
+ if (!isValid(session)) {
103
+ const target = getState(state ?? (await headers()).get("x-pathname"));
104
+ redirect(withState(loginUrl, target === resolvedUrls.afterLogin ? void 0 : target));
105
+ }
106
+ return session;
107
+ },
108
+ redirectIfAuthenticated: async (redirectTo) => {
109
+ const session = await getSession();
110
+ if (isValid(session)) redirect(redirectTo ?? resolvedUrls.afterLogin);
111
+ },
112
+ getState,
113
+ setSession: async (data) => {
114
+ const session = await getSession();
115
+ Object.assign(session, data);
116
+ await session.save();
117
+ return session;
118
+ },
119
+ destroySession,
120
+ logout: async (redirectTo) => {
121
+ await destroySession();
122
+ redirect((typeof redirectTo === "string" ? redirectTo : void 0) ?? resolvedUrls.afterLogout);
123
+ },
124
+ authenticatedRouteHandler: (handler) => async (request, context) => {
125
+ if (!await isAuthenticated()) return Response.json({ error: "Unauthorized" }, { status: 401 });
126
+ return handler(request, context);
127
+ },
128
+ proxyGuard: ({ allow, matcher } = {}) => {
129
+ if (matcher) {
130
+ const coveredBy = picomatch([matcher].flat().flatMap(matcherToGlobs));
131
+ const uncovered = [...resolvedUrls.public, ...resolvedUrls.protected].filter((glob) => !expandPrefix(glob).every((probe) => coveredBy(probe.replace("/**", "/probe"))));
132
+ if (uncovered.length) throw new Error("proxyGuard matcher does not cover all urls globs, these paths would bypass it", { cause: {
133
+ uncovered,
134
+ matcher,
135
+ cookieName
136
+ } });
137
+ }
138
+ return async (request) => {
139
+ const { pathname } = request.nextUrl;
140
+ const requestHeaders = new Headers(request.headers);
141
+ requestHeaders.set(PATHNAME_HEADER, pathname);
142
+ const next = () => NextResponse.next({ request: { headers: requestHeaders } });
143
+ if (allow?.(request)) return next();
144
+ if (isPublicPath || isProtectedPath) {
145
+ const isPublic = isPublicPath?.(pathname) ?? false;
146
+ const isProtected = isProtectedPath?.(pathname) ?? false;
147
+ if (isPublic && isProtected) throw new Error("Path matches both urls.public and urls.protected globs", { cause: {
148
+ pathname,
149
+ public: urls.public,
150
+ protected: urls.protected
151
+ } });
152
+ if (isPublic) return next();
153
+ if (!isProtected) {
154
+ console.warn("[AUTH] proxyGuard unknown path, treating as public", {
155
+ pathname,
156
+ cookieName
157
+ });
158
+ return next();
159
+ }
160
+ }
161
+ if (!isConfigured()) return NextResponse.json({ error: "Authentication is not configured" }, { status: 503 });
162
+ const response = next();
163
+ const session = await getIronSession(request, response, getSessionOptions());
164
+ if (isValid(session)) return response;
165
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
166
+ };
167
+ }
168
+ };
169
+ }
170
+ function createPasswordAuth(config) {
171
+ const auth = createAuth(config);
172
+ const resolvePassword = () => config.password ?? process.env.ADMIN_PASSWORD;
173
+ const verifyPassword = (candidate) => {
174
+ const password = resolvePassword();
175
+ if (typeof candidate !== "string" || !candidate || !password) return false;
176
+ return timingSafeEqualStrings(candidate, password);
177
+ };
178
+ const login = async (candidate) => {
179
+ if (!verifyPassword(candidate)) return false;
180
+ await auth.destroySession();
181
+ await auth.setSession({ [AUTHENTICATED]: true });
182
+ return true;
183
+ };
184
+ const isConfigured = () => auth.isConfigured() && Boolean(resolvePassword());
185
+ const submitLogin = async (formData) => {
186
+ if (!isConfigured()) return "Authentication is not configured";
187
+ if (!await login(formData.get("password"))) return "Invalid password";
188
+ redirect(auth.getState(formData));
189
+ };
190
+ return {
191
+ ...auth,
192
+ isConfigured,
193
+ verifyPassword,
194
+ login,
195
+ loginAction: async (formData) => {
196
+ const error = await submitLogin(formData);
197
+ const state = auth.getState(formData);
198
+ redirect(withParams(auth.urls.login, {
199
+ [ERROR_PARAM]: error,
200
+ [STATE_PARAM]: state === auth.urls.afterLogin ? void 0 : state
201
+ }));
202
+ },
203
+ loginFormAction: async (_prevState, formData) => submitLogin(formData)
204
+ };
205
+ }
206
+ //#endregion
207
+ export { MIN_SECRET_LENGTH, createAuth, createPasswordAuth, timingSafeEqualStrings };
208
+
209
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.js","names":[],"sources":["../src/auth.ts"],"sourcesContent":["import { createHash, timingSafeEqual } from 'node:crypto';\nimport { getIronSession, type IronSession, type SessionOptions } from 'iron-session';\nimport { cookies, headers } from 'next/headers';\nimport { redirect } from 'next/navigation';\nimport { type NextRequest, NextResponse } from 'next/server';\nimport picomatch from 'picomatch';\n\nimport { AUTHENTICATED, ERROR_PARAM, PATHNAME_HEADER, STATE_PARAM } from './client';\n\n/** `R` is the route type — pass `Route` from `next` when `typedRoutes` is enabled */\nexport type AuthUrls<R extends string = string> = {\n /** Login page, default `/login` */\n login?: R;\n /** Where successful login lands when no `state` param is present, default `/` */\n afterLogin?: R;\n /** Where logout lands, default `urls.login` */\n afterLogout?: R;\n /** Path globs `proxyGuard` serves without a session, e.g. `['/api/tiles/**']` — patterns, not routes */\n public?: string[];\n /** Path globs `proxyGuard` requires a session for, e.g. `['/api/admin/**']` — patterns, not routes */\n protected?: string[];\n};\n\nexport type AuthConfig<T extends object, R extends string = string> = {\n /** MUST be unique per app, e.g. `myapp_session`, never a generic `session` */\n cookieName: string;\n /**\n * Decides whether a session counts as authenticated. Defaults to checking the lib-owned\n * `AUTHENTICATED` marker field (set by `createPasswordAuth`'s `login`, nothing to configure) —\n * pass your own for token/OAuth or custom shapes (e.g. access token present and not expired)\n */\n isValid?: (session: T) => boolean;\n /** Cookie sealing secret, min 32 chars, defaults to `process.env.SESSION_SECRET`, NOT the login password */\n secret?: string;\n /** Session lifetime in SECONDS, default 7 days; cookie `maxAge` is derived from it by iron-session, never set it */\n ttl?: number;\n /** Merged over secure defaults: `httpOnly`, `sameSite=lax`, `path=/`, `secure` in production */\n cookieOptions?: SessionOptions['cookieOptions'];\n // NoInfer: without it TS would narrow R to the literal urls passed here, breaking deeper paths\n urls?: AuthUrls<NoInfer<R>>;\n /** Called by `throwNonAuthenticated` before throwing, e.g. `unauthorized` from `next/navigation` */\n onUnauthorized?: () => never;\n};\n\nexport type ProxyGuardOptions = {\n /** Public requests that skip the auth check */\n allow?: (request: NextRequest) => boolean;\n /**\n * The literal `config.matcher` exported from `proxy.ts`. Validated at creation: it may be\n * broader than the `urls.public`/`urls.protected` globs, but every glob must be covered by it —\n * uncovered paths would bypass the proxy entirely, so a gap THROWS (config error)\n */\n matcher?: string | string[];\n};\n\n/**\n * App Router Route Handler shape — GET/POST/etc all share it (Next.js exports no public type for\n * route handlers, only the app-generated `RouteContext` global)\n */\nexport type RouteHandler<Context = { params: Promise<Record<string, string | string[] | undefined>> }> = (\n request: NextRequest,\n context: Context,\n) => Response | Promise<Response>;\n\n/** Resolved `AuthUrls` with all defaults applied */\nexport type ResolvedAuthUrls<R extends string = string> = {\n login: R;\n afterLogin: R;\n afterLogout: R;\n public: readonly string[];\n protected: readonly string[];\n};\n\n/** Anything the `state` return URL can be extracted from */\nexport type StateSource =\n | string\n | null\n | undefined\n | URLSearchParams\n | FormData\n | NextRequest\n | Record<string, string | string[] | undefined>;\n\nexport type Auth<T extends object, R extends string = string> = {\n /** Resolved URLs with defaults applied — reuse instead of literals, e.g. `redirect(auth.urls.afterLogin)` */\n urls: ResolvedAuthUrls<R>;\n isValid: (session: T) => boolean;\n /** Both secret (and password for password auth) are present and usable, does not throw */\n isConfigured: () => boolean;\n getSessionOptions: () => SessionOptions;\n getSession: () => Promise<IronSession<T>>;\n /** For proxy / middleware where `cookies()` is not available */\n getRequestSession: (request: Request, response: Response) => Promise<IronSession<T>>;\n isAuthenticated: () => Promise<boolean>;\n /** For Server Actions and Route Handlers: throws `Unauthorized` (or calls `onUnauthorized`) */\n throwNonAuthenticated: () => Promise<IronSession<T>>;\n /**\n * For layouts and pages: redirects to `urls.login`, preserving `state` in the URL.\n * Without it the current pathname is recovered from the `x-pathname` header (stamped by\n * `proxyGuard` when its matcher covers page routes), falling back to `urls.afterLogin` where\n * login lands anyway\n */\n redirectNonAuthenticated: (state?: R) => Promise<IronSession<T>>;\n /** For the login page: redirects away when already authenticated */\n redirectIfAuthenticated: (redirectTo?: R) => Promise<void>;\n /**\n * Extracts and sanitizes the `state` return URL from `await searchParams`, `URLSearchParams`,\n * `FormData`, a `NextRequest` or a raw value: internal paths only (no open redirects), never\n * the login page itself (no redirect loops); falls back to `urls.afterLogin` when absent or\n * unsafe, so the result is always a usable target\n */\n getState: (source?: StateSource) => R;\n /** Merges data into the session and saves it */\n setSession: (data: Partial<T>) => Promise<IronSession<T>>;\n destroySession: () => Promise<void>;\n /**\n * Destroys the session and redirects to `redirectTo ?? urls.afterLogout ?? urls.login`. Usable\n * directly as a `<form action>` — a `FormData` argument is ignored. Expose through a\n * function-level `'use server'` wrapper in the auth module (see README): action registration\n * must happen in app code, a plain re-export is not registered\n */\n logout: (redirectTo?: R | FormData) => Promise<void>;\n /** Wraps a Route Handler (any method — GET/POST/etc share one shape), responds `401` JSON when not authenticated */\n authenticatedRouteHandler: <Context = { params: Promise<Record<string, string | string[] | undefined>> }>(\n handler: RouteHandler<Context>,\n ) => RouteHandler<Context>;\n /**\n * The `proxy.ts` handler: `export const config = { matcher: [...] };` (a LITERAL — Next.js\n * parses it statically) then `export const proxy = auth.proxyGuard({ matcher: config.matcher });`.\n * The handler classifies requests by `urls.public` / `urls.protected` globs: public passes\n * without a session, protected responds `503` when unconfigured and `401` JSON when not\n * authenticated, a path matching BOTH glob sets throws (config error), a path matching NEITHER\n * logs a warning and passes as public. Without any globs everything is protected. `allow` is an\n * extra request-level bypass checked first. The supplied `matcher` is verified to cover all\n * globs (it may be broader) — an uncovered glob throws at creation (config error)\n */\n proxyGuard: (options?: ProxyGuardOptions) => (request: NextRequest) => Promise<NextResponse>;\n};\n\nexport type PasswordSession = { [AUTHENTICATED]?: boolean };\n\nexport type PasswordAuthConfig<R extends string = string> = Omit<AuthConfig<PasswordSession, R>, 'isValid'> & {\n /** Login password, defaults to `process.env.ADMIN_PASSWORD`, NOT the cookie secret */\n password?: string;\n};\n\nexport type PasswordAuth<R extends string = string> = Auth<PasswordSession, R> & {\n /** Timing-safe, never compare passwords with `===` */\n verifyPassword: (candidate: unknown) => boolean;\n /** Verifies the password and marks the session logged in */\n login: (candidate: unknown) => Promise<boolean>;\n /**\n * Plain `<form action>` for a Server-Component-only login page (the recommended pattern):\n * expects `password` (and optional `state` return URL) form fields; on failure redirects back\n * to `urls.login` with `?error=` (+ `state`), on success to `state` or `urls.afterLogin`.\n * Expose through a function-level `'use server'` wrapper in the auth module (see README)\n */\n loginAction: (formData: FormData) => Promise<void>;\n /**\n * `useActionState` variant returning the error message instead of redirecting on failure — only\n * for client components that need pending/error UI (or Toolpad `SignInPage`). Same wrapper rule\n */\n loginFormAction: (prevState: string | null | undefined, formData: FormData) => Promise<string>;\n};\n\nexport const MIN_SECRET_LENGTH = 32;\n\nconst DEFAULT_TTL = 60 * 60 * 24 * 7; // seconds, 7 days\n\nconst sha256 = (value: string): Buffer => createHash('sha256').update(value).digest();\n\n/** Constant-time string comparison via SHA-256 digests, safe for values of different lengths */\nexport const timingSafeEqualStrings = (a: string, b: string): boolean => timingSafeEqual(sha256(a), sha256(b));\n\nconst withParams = (url: string, params: Record<string, string | undefined>): string => {\n const entries = Object.entries(params).filter((entry): entry is [string, string] => Boolean(entry[1]));\n if (!entries.length) return url;\n return `${url}${url.includes('?') ? '&' : '?'}${new URLSearchParams(entries)}`;\n};\n\nconst withState = (url: string, state?: string): string => withParams(url, { [STATE_PARAM]: state });\n\n// path-to-regexp matcher entry → equivalent globs, for verifying matcher coverage of urls globs\nconst matcherToGlobs = (entry: string): string[] => {\n if (entry.endsWith('/:path*')) {\n const prefix = entry.slice(0, -'/:path*'.length);\n return [prefix || '/', `${prefix}/**`];\n }\n return [entry.replace(/:\\w+\\*/g, '**').replace(/:\\w+/g, '*')];\n};\n\n// A boolean flag inside the sealed cookie, under the lib-owned AUTHENTICATED key\nconst defaultIsValid = (session: PasswordSession): boolean => session[AUTHENTICATED] === true;\n\n// `/x/**` should behave like Next's `/x/:path*`, which also matches the bare `/x` (for the root\n// glob `/**` the bare prefix is `/`)\nconst expandPrefix = (glob: string): string[] => (glob.endsWith('/**') ? [glob, glob.slice(0, -3) || '/'] : [glob]);\n\nexport function createAuth<T extends object = PasswordSession, R extends string = string>(\n config: AuthConfig<T, R>,\n): Auth<T, R> {\n const { cookieName, isValid = defaultIsValid as unknown as (session: T) => boolean, urls = {} } = config;\n const loginUrl: R = urls.login ?? ('/login' as R);\n\n const resolvedUrls: ResolvedAuthUrls<R> = {\n login: loginUrl,\n afterLogin: urls.afterLogin ?? ('/' as R),\n afterLogout: urls.afterLogout ?? loginUrl,\n public: urls.public ?? [],\n protected: urls.protected ?? [],\n };\n\n // redirectIfAuthenticated(afterLogin) on the login page would otherwise redirect to itself forever\n if (loginUrl !== '/' && resolvedUrls.afterLogin.startsWith(loginUrl)) {\n throw new Error('urls.afterLogin must not point at the login page, this would cause a redirect loop', {\n cause: { cookieName, ...urls },\n });\n }\n\n const isPublicPath = urls.public?.length ? picomatch(urls.public.flatMap(expandPrefix)) : undefined;\n const isProtectedPath = urls.protected?.length ? picomatch(urls.protected.flatMap(expandPrefix)) : undefined;\n\n const resolveSecret = (): string => {\n const secret = config.secret ?? process.env.SESSION_SECRET;\n if (!secret || secret.length < MIN_SECRET_LENGTH) {\n throw new Error(\n `Session secret must be at least ${MIN_SECRET_LENGTH} characters, set SESSION_SECRET or pass \"secret\"`,\n { cause: { cookieName } },\n );\n }\n return secret;\n };\n\n const getSessionOptions = (): SessionOptions => ({\n cookieName,\n password: resolveSecret(),\n ttl: config.ttl ?? DEFAULT_TTL,\n cookieOptions: {\n httpOnly: true,\n sameSite: 'lax',\n path: '/',\n secure: process.env.NODE_ENV === 'production',\n ...config.cookieOptions,\n },\n });\n\n const isConfigured = (): boolean => {\n try {\n resolveSecret();\n return true;\n } catch {\n return false;\n }\n };\n\n const getSession = async (): Promise<IronSession<T>> => getIronSession<T>(await cookies(), getSessionOptions());\n\n const isAuthenticated = async (): Promise<boolean> => isValid(await getSession());\n\n const throwNonAuthenticated = async (): Promise<IronSession<T>> => {\n const session = await getSession();\n if (!isValid(session)) {\n config.onUnauthorized?.();\n throw new Error('Unauthorized');\n }\n return session;\n };\n\n const destroySession = async (): Promise<void> => (await getSession()).destroy();\n\n const getState = (source?: StateSource): R => {\n let value: unknown = source;\n if (source instanceof URLSearchParams || source instanceof FormData) value = source.get(STATE_PARAM);\n else if (source && typeof source === 'object') {\n value =\n 'nextUrl' in source\n ? (source as NextRequest).nextUrl.searchParams.get(STATE_PARAM)\n : (source as Record<string, string | string[] | undefined>)[STATE_PARAM];\n }\n if (Array.isArray(value)) value = value[0];\n if (typeof value !== 'string' || !value) return resolvedUrls.afterLogin;\n if (!value.startsWith('/') || value.startsWith('//')) return resolvedUrls.afterLogin; // internal paths only\n if (value.startsWith(loginUrl)) return resolvedUrls.afterLogin; // never back to the login page\n return value as R;\n };\n\n return {\n urls: resolvedUrls,\n isValid,\n isConfigured,\n getSessionOptions,\n getSession,\n getRequestSession: async (request, response) => getIronSession<T>(request, response, getSessionOptions()),\n isAuthenticated,\n throwNonAuthenticated,\n redirectNonAuthenticated: async (state) => {\n const session = await getSession();\n if (!isValid(session)) {\n // explicit state > pathname stamped by proxyGuard > afterLogin; getState maps\n // unsafe values (the header is spoofable when no proxy overwrites it) to afterLogin,\n // where login lands anyway, so the state param is dropped\n const target = getState(state ?? (await headers()).get(PATHNAME_HEADER));\n redirect(withState(loginUrl, target === resolvedUrls.afterLogin ? undefined : target));\n }\n return session;\n },\n redirectIfAuthenticated: async (redirectTo) => {\n const session = await getSession();\n if (isValid(session)) redirect(redirectTo ?? resolvedUrls.afterLogin);\n },\n getState,\n setSession: async (data) => {\n const session = await getSession();\n Object.assign(session, data);\n await session.save();\n return session;\n },\n destroySession,\n logout: async (redirectTo) => {\n await destroySession();\n redirect((typeof redirectTo === 'string' ? redirectTo : undefined) ?? resolvedUrls.afterLogout);\n },\n authenticatedRouteHandler: (handler) => async (request, context) => {\n if (!(await isAuthenticated())) return Response.json({ error: 'Unauthorized' }, { status: 401 });\n return handler(request, context);\n },\n proxyGuard: ({ allow, matcher }: ProxyGuardOptions = {}) => {\n if (matcher) {\n // every public/protected glob must be covered by the supplied matcher (which may be\n // broader) — paths outside it never reach the proxy at all, so this is a config error\n const coveredBy = picomatch([matcher].flat().flatMap(matcherToGlobs));\n const uncovered = [...resolvedUrls.public, ...resolvedUrls.protected].filter(\n (glob) => !expandPrefix(glob).every((probe) => coveredBy(probe.replace('/**', '/probe'))),\n );\n if (uncovered.length) {\n throw new Error('proxyGuard matcher does not cover all urls globs, these paths would bypass it', {\n cause: { uncovered, matcher, cookieName },\n });\n }\n }\n\n return async (request: NextRequest): Promise<NextResponse> => {\n const { pathname } = request.nextUrl;\n\n // stamp the pathname so redirectNonAuthenticated can recover it in layouts\n const requestHeaders = new Headers(request.headers);\n requestHeaders.set(PATHNAME_HEADER, pathname);\n const next = () => NextResponse.next({ request: { headers: requestHeaders } });\n\n if (allow?.(request)) return next();\n\n if (isPublicPath || isProtectedPath) {\n const isPublic = isPublicPath?.(pathname) ?? false;\n const isProtected = isProtectedPath?.(pathname) ?? false;\n\n if (isPublic && isProtected) {\n throw new Error('Path matches both urls.public and urls.protected globs', {\n cause: { pathname, public: urls.public, protected: urls.protected },\n });\n }\n if (isPublic) return next();\n if (!isProtected) {\n console.warn('[AUTH] proxyGuard unknown path, treating as public', { pathname, cookieName });\n return next();\n }\n }\n // protected path, or no globs configured at all — everything requires a session\n\n if (!isConfigured()) {\n return NextResponse.json({ error: 'Authentication is not configured' }, { status: 503 });\n }\n const response = next();\n const session = await getIronSession<T>(request, response, getSessionOptions());\n if (isValid(session)) return response;\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });\n };\n },\n };\n}\n\nexport function createPasswordAuth<R extends string = string>(config: PasswordAuthConfig<R>): PasswordAuth<R> {\n const auth = createAuth<PasswordSession, R>(config);\n\n const resolvePassword = (): string | undefined => config.password ?? process.env.ADMIN_PASSWORD;\n\n const verifyPassword = (candidate: unknown): boolean => {\n const password = resolvePassword();\n if (typeof candidate !== 'string' || !candidate || !password) return false;\n return timingSafeEqualStrings(candidate, password);\n };\n\n const login = async (candidate: unknown): Promise<boolean> => {\n if (!verifyPassword(candidate)) return false;\n // fresh session: stale fields from previous session shapes (or another login) never linger\n await auth.destroySession();\n await auth.setSession({ [AUTHENTICATED]: true });\n return true;\n };\n\n const isConfigured = (): boolean => auth.isConfigured() && Boolean(resolvePassword());\n\n // returns the error message, redirects away on success\n const submitLogin = async (formData: FormData): Promise<string> => {\n if (!isConfigured()) return 'Authentication is not configured';\n if (!(await login(formData.get('password')))) return 'Invalid password';\n redirect(auth.getState(formData));\n };\n\n return {\n ...auth,\n isConfigured,\n verifyPassword,\n login,\n loginAction: async (formData) => {\n const error = await submitLogin(formData);\n // back to the login page, error and return URL threaded via search params only; the\n // state param is dropped when it adds nothing over the afterLogin default\n const state = auth.getState(formData);\n redirect(\n withParams(auth.urls.login, {\n [ERROR_PARAM]: error,\n [STATE_PARAM]: state === auth.urls.afterLogin ? undefined : state,\n }),\n );\n },\n loginFormAction: async (_prevState, formData) => submitLogin(formData),\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=auth.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.test.d.ts","sourceRoot":"","sources":["../src/auth.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,16 @@
1
+ /** Name of the search param / form field carrying the return URL through the login flow */
2
+ export declare const STATE_PARAM = "state";
3
+ /** Name of the search param carrying the login error message (`loginAction` failure redirects) */
4
+ export declare const ERROR_PARAM = "error";
5
+ /**
6
+ * Request header `proxyGuard` stamps with the current pathname — layouts cannot see the URL, so
7
+ * `redirectNonAuthenticated()` reads it back to return users to the page they were on after login
8
+ */
9
+ export declare const PATHNAME_HEADER = "x-pathname";
10
+ /**
11
+ * Session field marking a password-authenticated session — lib-owned, collision-safe, nothing to
12
+ * configure. A string constant and NOT a JS `Symbol`: iron-session JSON-serializes the session
13
+ * into the sealed cookie, symbol-keyed fields would be silently dropped
14
+ */
15
+ export declare const AUTHENTICATED = "__authenticated";
16
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAGA,2FAA2F;AAC3F,eAAO,MAAM,WAAW,UAAU,CAAC;AAEnC,kGAAkG;AAClG,eAAO,MAAM,WAAW,UAAU,CAAC;AAEnC;;;GAGG;AACH,eAAO,MAAM,eAAe,eAAe,CAAC;AAE5C;;;;GAIG;AACH,eAAO,MAAM,aAAa,oBAAoB,CAAC"}
@@ -0,0 +1,20 @@
1
+ //#region src/client/index.ts
2
+ /** Name of the search param / form field carrying the return URL through the login flow */
3
+ var STATE_PARAM = "state";
4
+ /** Name of the search param carrying the login error message (`loginAction` failure redirects) */
5
+ var ERROR_PARAM = "error";
6
+ /**
7
+ * Request header `proxyGuard` stamps with the current pathname — layouts cannot see the URL, so
8
+ * `redirectNonAuthenticated()` reads it back to return users to the page they were on after login
9
+ */
10
+ var PATHNAME_HEADER = "x-pathname";
11
+ /**
12
+ * Session field marking a password-authenticated session — lib-owned, collision-safe, nothing to
13
+ * configure. A string constant and NOT a JS `Symbol`: iron-session JSON-serializes the session
14
+ * into the sealed cookie, symbol-keyed fields would be silently dropped
15
+ */
16
+ var AUTHENTICATED = "__authenticated";
17
+ //#endregion
18
+ export { AUTHENTICATED, ERROR_PARAM, PATHNAME_HEADER, STATE_PARAM };
19
+
20
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/client/index.ts"],"sourcesContent":["// Client-safe constants — this subpath (`@kirill.konshin/auth/client`) is importable from Client\n// Components; the main entry pulls in next/headers and is server-only\n\n/** Name of the search param / form field carrying the return URL through the login flow */\nexport const STATE_PARAM = 'state';\n\n/** Name of the search param carrying the login error message (`loginAction` failure redirects) */\nexport const ERROR_PARAM = 'error';\n\n/**\n * Request header `proxyGuard` stamps with the current pathname — layouts cannot see the URL, so\n * `redirectNonAuthenticated()` reads it back to return users to the page they were on after login\n */\nexport const PATHNAME_HEADER = 'x-pathname';\n\n/**\n * Session field marking a password-authenticated session — lib-owned, collision-safe, nothing to\n * configure. A string constant and NOT a JS `Symbol`: iron-session JSON-serializes the session\n * into the sealed cookie, symbol-keyed fields would be silently dropped\n */\nexport const AUTHENTICATED = '__authenticated';\n"],"mappings":";;AAIA,IAAa,cAAc;;AAG3B,IAAa,cAAc;;;;;AAM3B,IAAa,kBAAkB;;;;;;AAO/B,IAAa,gBAAgB"}
@@ -0,0 +1,3 @@
1
+ export * from './auth.js';
2
+ export * from './client/index.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,QAAQ,CAAC;AACvB,cAAc,UAAU,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ import { AUTHENTICATED, ERROR_PARAM, PATHNAME_HEADER, STATE_PARAM } from "./client/index.js";
2
+ import { MIN_SECRET_LENGTH, createAuth, createPasswordAuth, timingSafeEqualStrings } from "./auth.js";
3
+ export { AUTHENTICATED, ERROR_PARAM, MIN_SECRET_LENGTH, PATHNAME_HEADER, STATE_PARAM, createAuth, createPasswordAuth, timingSafeEqualStrings };
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "@kirill.konshin/auth",
3
+ "description": "Reusable iron-session authentication for Next.js App Router: session module, layout/page guards, route handler guards, proxy guard, password login",
4
+ "version": "0.0.1",
5
+ "type": "module",
6
+ "scripts": {
7
+ "----- BUILD -----": "",
8
+ "clean": "rm -rf dist .tscache tsconfig.tsbuildinfo",
9
+ "build": "vite build",
10
+ "build:index": "cti create ./src",
11
+ "build:check-types": "attw --pack .",
12
+ "start": "yarn build --watch",
13
+ "wait": "wait-on ./dist/index.js",
14
+ "----- TEST -----": "",
15
+ "test": "vitest run --coverage",
16
+ "test:watch": "vitest watch --coverage",
17
+ "----- STORYBOOK -----": "",
18
+ "storybook:start": "storybook dev -p 6006",
19
+ "storybook:build": "storybook build"
20
+ },
21
+ "dependencies": {
22
+ "iron-session": "^8.0.4",
23
+ "picomatch": "4.0.5"
24
+ },
25
+ "devDependencies": {
26
+ "@kirill.konshin/utils-private": "*",
27
+ "@types/picomatch": "^4.0.3",
28
+ "next": "^16.2.12"
29
+ },
30
+ "peerDependencies": {
31
+ "next": "^16"
32
+ },
33
+ "peerDependenciesMeta": {
34
+ "next": {
35
+ "optional": true
36
+ }
37
+ },
38
+ "publishConfig": {
39
+ "access": "public"
40
+ },
41
+ "author": "Kirill Konshin <kirill@konshin.org> (https://konshin.org)",
42
+ "license": "MIT",
43
+ "exports": {
44
+ "./client": {
45
+ "import": {
46
+ "import": "./dist/client/index.js",
47
+ "types": "./dist/client/index.d.ts"
48
+ }
49
+ },
50
+ ".": {
51
+ "import": {
52
+ "import": "./dist/index.js",
53
+ "types": "./dist/index.d.ts"
54
+ }
55
+ }
56
+ },
57
+ "main": "./dist/index.js",
58
+ "module": "./dist/index.js",
59
+ "types": "./dist/index.d.ts",
60
+ "repository": {
61
+ "type": "git",
62
+ "url": "https://github.com/kirill-konshin/utils.git",
63
+ "directory": "packages/auth"
64
+ },
65
+ "nx": {
66
+ "tags": [
67
+ "platform:next"
68
+ ]
69
+ },
70
+ "files": [
71
+ "dist"
72
+ ]
73
+ }