@kirill.konshin/auth 0.0.1 → 0.0.2
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 +12 -1
- package/dist/auth.d.ts +119 -117
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +14 -11
- package/dist/auth.js.map +1 -1
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +4 -4
- package/dist/client/index.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/package.json +13 -15
- package/dist/auth.test.d.ts +0 -2
- package/dist/auth.test.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -19,6 +19,7 @@ yarn add @kirill.konshin/auth iron-session server-only
|
|
|
19
19
|
- Cookie secret `SESSION_SECRET` (≥32 chars) and login password `ADMIN_PASSWORD` are SEPARATE env vars, never reuse one for the other
|
|
20
20
|
- NEVER compare passwords with `===`, use `verifyPassword` / `timingSafeEqualStrings` (timing-safe)
|
|
21
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
|
+
- Call page/layout guards inside a `<Suspense>` boundary of that page or layout: the session check is request-time (`connection()`), so under `cacheComponents` it blocks prerendering and instant navigation outside one
|
|
22
23
|
- 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
24
|
- Reuse `auth.urls.*` (resolved, defaults applied) instead of repeating route literals, e.g. `redirect(auth.urls.afterLogin)`
|
|
24
25
|
- 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
|
|
@@ -97,9 +98,19 @@ App-specific logic (e.g. OAuth token refresh) composes on top: `auth.setSession(
|
|
|
97
98
|
## Protected layout — `app/(auth)/layout.tsx` or `app/app/layout.tsx`
|
|
98
99
|
|
|
99
100
|
```tsx
|
|
101
|
+
import { type ReactNode, Suspense } from 'react';
|
|
102
|
+
|
|
100
103
|
import { auth } from '@/auth';
|
|
101
104
|
|
|
102
|
-
export default
|
|
105
|
+
export default function Layout({ children }: LayoutProps<'/app'>) {
|
|
106
|
+
return (
|
|
107
|
+
<Suspense>
|
|
108
|
+
<Protected>{children}</Protected>
|
|
109
|
+
</Suspense>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function Protected({ children }: { children: ReactNode }) {
|
|
103
114
|
await auth.redirectNonAuthenticated(); // redirects to urls.login
|
|
104
115
|
return <>{children}</>;
|
|
105
116
|
}
|
package/dist/auth.d.ts
CHANGED
|
@@ -1,148 +1,150 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { AUTHENTICATED } from "./client/index.js";
|
|
2
|
+
import { IronSession, SessionOptions } from "iron-session";
|
|
3
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
4
|
+
//#region src/auth.d.ts
|
|
4
5
|
/** `R` is the route type — pass `Route` from `next` when `typedRoutes` is enabled */
|
|
5
6
|
export type AuthUrls<R extends string = string> = {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
7
|
+
/** Login page, default `/login` */
|
|
8
|
+
login?: R;
|
|
9
|
+
/** Where successful login lands when no `state` param is present, default `/` */
|
|
10
|
+
afterLogin?: R;
|
|
11
|
+
/** Where logout lands, default `urls.login` */
|
|
12
|
+
afterLogout?: R;
|
|
13
|
+
/** Path globs `proxyGuard` serves without a session, e.g. `['/api/tiles/**']` — patterns, not routes */
|
|
14
|
+
public?: string[];
|
|
15
|
+
/** Path globs `proxyGuard` requires a session for, e.g. `['/api/admin/**']` — patterns, not routes */
|
|
16
|
+
protected?: string[];
|
|
16
17
|
};
|
|
17
18
|
export type AuthConfig<T extends object, R extends string = string> = {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
19
|
+
/** MUST be unique per app, e.g. `myapp_session`, never a generic `session` */
|
|
20
|
+
cookieName: string;
|
|
21
|
+
/**
|
|
22
|
+
* Decides whether a session counts as authenticated. Defaults to checking the lib-owned
|
|
23
|
+
* `AUTHENTICATED` marker field (set by `createPasswordAuth`'s `login`, nothing to configure) —
|
|
24
|
+
* pass your own for token/OAuth or custom shapes (e.g. access token present and not expired)
|
|
25
|
+
*/
|
|
26
|
+
isValid?: (session: T) => boolean;
|
|
27
|
+
/** Cookie sealing secret, min 32 chars, defaults to `process.env.SESSION_SECRET`, NOT the login password */
|
|
28
|
+
secret?: string;
|
|
29
|
+
/** Session lifetime in SECONDS, default 7 days; cookie `maxAge` is derived from it by iron-session, never set it */
|
|
30
|
+
ttl?: number;
|
|
31
|
+
/** Merged over secure defaults: `httpOnly`, `sameSite=lax`, `path=/`, `secure` in production */
|
|
32
|
+
cookieOptions?: SessionOptions["cookieOptions"];
|
|
33
|
+
urls?: AuthUrls<NoInfer<R>>;
|
|
34
|
+
/** Called by `throwNonAuthenticated` before throwing, e.g. `unauthorized` from `next/navigation` */
|
|
35
|
+
onUnauthorized?: () => never;
|
|
35
36
|
};
|
|
36
37
|
export type ProxyGuardOptions = {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
38
|
+
/** Public requests that skip the auth check */
|
|
39
|
+
allow?: (request: NextRequest) => boolean;
|
|
40
|
+
/**
|
|
41
|
+
* The literal `config.matcher` exported from `proxy.ts`. Validated at creation: it may be
|
|
42
|
+
* broader than the `urls.public`/`urls.protected` globs, but every glob must be covered by it —
|
|
43
|
+
* uncovered paths would bypass the proxy entirely, so a gap THROWS (config error)
|
|
44
|
+
*/
|
|
45
|
+
matcher?: string | string[];
|
|
45
46
|
};
|
|
46
47
|
/**
|
|
47
48
|
* App Router Route Handler shape — GET/POST/etc all share it (Next.js exports no public type for
|
|
48
49
|
* route handlers, only the app-generated `RouteContext` global)
|
|
49
50
|
*/
|
|
50
51
|
export type RouteHandler<Context = {
|
|
51
|
-
|
|
52
|
+
params: Promise<Record<string, string | string[] | undefined>>;
|
|
52
53
|
}> = (request: NextRequest, context: Context) => Response | Promise<Response>;
|
|
53
54
|
/** Resolved `AuthUrls` with all defaults applied */
|
|
54
55
|
export type ResolvedAuthUrls<R extends string = string> = {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
56
|
+
login: R;
|
|
57
|
+
afterLogin: R;
|
|
58
|
+
afterLogout: R;
|
|
59
|
+
public: readonly string[];
|
|
60
|
+
protected: readonly string[];
|
|
60
61
|
};
|
|
61
62
|
/** Anything the `state` return URL can be extracted from */
|
|
62
63
|
export type StateSource = string | null | undefined | URLSearchParams | FormData | NextRequest | Record<string, string | string[] | undefined>;
|
|
63
64
|
export type Auth<T extends object, R extends string = string> = {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
65
|
+
/** Resolved URLs with defaults applied — reuse instead of literals, e.g. `redirect(auth.urls.afterLogin)` */
|
|
66
|
+
urls: ResolvedAuthUrls<R>;
|
|
67
|
+
isValid: (session: T) => boolean;
|
|
68
|
+
/** Both secret (and password for password auth) are present and usable, does not throw */
|
|
69
|
+
isConfigured: () => boolean;
|
|
70
|
+
getSessionOptions: () => SessionOptions;
|
|
71
|
+
getSession: () => Promise<IronSession<T>>;
|
|
72
|
+
/** For proxy / middleware where `cookies()` is not available */
|
|
73
|
+
getRequestSession: (request: Request, response: Response) => Promise<IronSession<T>>;
|
|
74
|
+
isAuthenticated: () => Promise<boolean>;
|
|
75
|
+
/** For Server Actions and Route Handlers: throws `Unauthorized` (or calls `onUnauthorized`) */
|
|
76
|
+
throwNonAuthenticated: () => Promise<IronSession<T>>;
|
|
77
|
+
/**
|
|
78
|
+
* For layouts and pages: redirects to `urls.login`, preserving `state` in the URL.
|
|
79
|
+
* Without it the current pathname is recovered from the `x-pathname` header (stamped by
|
|
80
|
+
* `proxyGuard` when its matcher covers page routes), falling back to `urls.afterLogin` where
|
|
81
|
+
* login lands anyway
|
|
82
|
+
*/
|
|
83
|
+
redirectNonAuthenticated: (state?: R) => Promise<IronSession<T>>;
|
|
84
|
+
/** For the login page: redirects away when already authenticated */
|
|
85
|
+
redirectIfAuthenticated: (redirectTo?: R) => Promise<void>;
|
|
86
|
+
/**
|
|
87
|
+
* Extracts and sanitizes the `state` return URL from `await searchParams`, `URLSearchParams`,
|
|
88
|
+
* `FormData`, a `NextRequest` or a raw value: internal paths only (no open redirects), never
|
|
89
|
+
* the login page itself (no redirect loops); falls back to `urls.afterLogin` when absent or
|
|
90
|
+
* unsafe, so the result is always a usable target
|
|
91
|
+
*/
|
|
92
|
+
getState: (source?: StateSource) => R;
|
|
93
|
+
/** Merges data into the session and saves it */
|
|
94
|
+
setSession: (data: Partial<T>) => Promise<IronSession<T>>;
|
|
95
|
+
destroySession: () => Promise<void>;
|
|
96
|
+
/**
|
|
97
|
+
* Destroys the session and redirects to `redirectTo ?? urls.afterLogout ?? urls.login`. Usable
|
|
98
|
+
* directly as a `<form action>` — a `FormData` argument is ignored. Expose through a
|
|
99
|
+
* function-level `'use server'` wrapper in the auth module (see README): action registration
|
|
100
|
+
* must happen in app code, a plain re-export is not registered
|
|
101
|
+
*/
|
|
102
|
+
logout: (redirectTo?: R | FormData) => Promise<void>;
|
|
103
|
+
/** Wraps a Route Handler (any method — GET/POST/etc share one shape), responds `401` JSON when not authenticated */
|
|
104
|
+
authenticatedRouteHandler: <Context = {
|
|
105
|
+
params: Promise<Record<string, string | string[] | undefined>>;
|
|
106
|
+
}>(handler: RouteHandler<Context>) => RouteHandler<Context>;
|
|
107
|
+
/**
|
|
108
|
+
* The `proxy.ts` handler: `export const config = { matcher: [...] };` (a LITERAL — Next.js
|
|
109
|
+
* parses it statically) then `export const proxy = auth.proxyGuard({ matcher: config.matcher });`.
|
|
110
|
+
* The handler classifies requests by `urls.public` / `urls.protected` globs: public passes
|
|
111
|
+
* without a session, protected responds `503` when unconfigured and `401` JSON when not
|
|
112
|
+
* authenticated, a path matching BOTH glob sets throws (config error), a path matching NEITHER
|
|
113
|
+
* logs a warning and passes as public. Without any globs everything is protected. `allow` is an
|
|
114
|
+
* extra request-level bypass checked first. The supplied `matcher` is verified to cover all
|
|
115
|
+
* globs (it may be broader) — an uncovered glob throws at creation (config error)
|
|
116
|
+
*/
|
|
117
|
+
proxyGuard: (options?: ProxyGuardOptions) => (request: NextRequest) => Promise<NextResponse>;
|
|
117
118
|
};
|
|
118
119
|
export type PasswordSession = {
|
|
119
|
-
|
|
120
|
+
[AUTHENTICATED]?: boolean;
|
|
120
121
|
};
|
|
121
|
-
export type PasswordAuthConfig<R extends string = string> = Omit<AuthConfig<PasswordSession, R>,
|
|
122
|
-
|
|
123
|
-
|
|
122
|
+
export type PasswordAuthConfig<R extends string = string> = Omit<AuthConfig<PasswordSession, R>, "isValid"> & {
|
|
123
|
+
/** Login password, defaults to `process.env.ADMIN_PASSWORD`, NOT the cookie secret */
|
|
124
|
+
password?: string;
|
|
124
125
|
};
|
|
125
126
|
export type PasswordAuth<R extends string = string> = Auth<PasswordSession, R> & {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
127
|
+
/** Timing-safe, never compare passwords with `===` */
|
|
128
|
+
verifyPassword: (candidate: unknown) => boolean;
|
|
129
|
+
/** Verifies the password and marks the session logged in */
|
|
130
|
+
login: (candidate: unknown) => Promise<boolean>;
|
|
131
|
+
/**
|
|
132
|
+
* Plain `<form action>` for a Server-Component-only login page (the recommended pattern):
|
|
133
|
+
* expects `password` (and optional `state` return URL) form fields; on failure redirects back
|
|
134
|
+
* to `urls.login` with `?error=` (+ `state`), on success to `state` or `urls.afterLogin`.
|
|
135
|
+
* Expose through a function-level `'use server'` wrapper in the auth module (see README)
|
|
136
|
+
*/
|
|
137
|
+
loginAction: (formData: FormData) => Promise<void>;
|
|
138
|
+
/**
|
|
139
|
+
* `useActionState` variant returning the error message instead of redirecting on failure — only
|
|
140
|
+
* for client components that need pending/error UI (or Toolpad `SignInPage`). Same wrapper rule
|
|
141
|
+
*/
|
|
142
|
+
loginFormAction: (prevState: string | null | undefined, formData: FormData) => Promise<string>;
|
|
142
143
|
};
|
|
143
144
|
export declare const MIN_SECRET_LENGTH = 32;
|
|
144
145
|
/** Constant-time string comparison via SHA-256 digests, safe for values of different lengths */
|
|
145
146
|
export declare const timingSafeEqualStrings: (a: string, b: string) => boolean;
|
|
146
147
|
export declare function createAuth<T extends object = PasswordSession, R extends string = string>(config: AuthConfig<T, R>): Auth<T, R>;
|
|
147
148
|
export declare function createPasswordAuth<R extends string = string>(config: PasswordAuthConfig<R>): PasswordAuth<R>;
|
|
149
|
+
//#endregion
|
|
148
150
|
//# sourceMappingURL=auth.d.ts.map
|
package/dist/auth.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","
|
|
1
|
+
{"version":3,"file":"auth.d.ts","names":[],"sources":["../src/auth.ts"],"mappings":";;;;;YAUY,SAAS;;EAEjB,QAAQ;;EAER,aAAa;;EAEb,cAAc;;EAEd;;EAEA;;YAGQ,WAAW,kBAAkB;;EAErC;;;;;;EAMA,WAAW,SAAS;;EAEpB;;EAEA;;EAEA,gBAAgB;EAEhB,OAAO,SAAS,QAAQ;;EAExB;;YAGQ;;EAER,SAAS,SAAS;;;;;;EAMlB;;;;;;YAOQ,aAAa;EAAY,QAAQ,QAAQ;MACjD,SAAS,aACT,SAAS,YACR,WAAW,QAAQ;;YAGZ,iBAAiB;EACzB,OAAO;EACP,YAAY;EACZ,aAAa;EACb;EACA;;;YAIQ,0CAIN,kBACA,WACA,cACA;YAEM,KAAK,kBAAkB;;EAE/B,MAAM,iBAAiB;EACvB,UAAU,SAAS;;EAEnB;EACA,yBAAyB;EACzB,kBAAkB,QAAQ,YAAY;;EAEtC,oBAAoB,SAAS,SAAS,UAAU,aAAa,QAAQ,YAAY;EACjF,uBAAuB;;EAEvB,6BAA6B,QAAQ,YAAY;;;;;;;EAOjD,2BAA2B,QAAQ,MAAM,QAAQ,YAAY;;EAE7D,0BAA0B,aAAa,MAAM;;;;;;;EAO7C,WAAW,SAAS,gBAAgB;;EAEpC,aAAa,MAAM,QAAQ,OAAO,QAAQ,YAAY;EACtD,sBAAsB;;;;;;;EAOtB,SAAS,aAAa,IAAI,aAAa;;EAEvC,4BAA4B;IAAY,QAAQ,QAAQ;KACpD,SAAS,aAAa,aACrB,aAAa;;;;;;;;;;;EAWlB,aAAa,UAAU,uBAAuB,SAAS,gBAAgB,QAAQ;;YAGvE;GAAqB;;YAErB,mBAAmB,6BAA6B,KAAK,WAAW,iBAAiB;;EAEzF;;YAGQ,aAAa,6BAA6B,KAAK,iBAAiB;;EAExE,iBAAiB;;EAEjB,QAAQ,uBAAuB;;;;;;;EAO/B,cAAc,UAAU,aAAa;;;;;EAKrC,kBAAkB,sCAAsC,UAAU,aAAa;;qBAGtE;;qBAOA,yBAA0B,WAAW;wBA0BlC,WAAW,mBAAmB,iBAAiB,2BAC3D,QAAQ,WAAW,GAAG,KACvB,KAAK,GAAG;wBAyLK,mBAAmB,2BAA2B,QAAQ,mBAAmB,KAAK,aAAa"}
|
package/dist/auth.js
CHANGED
|
@@ -3,29 +3,29 @@ import { createHash, timingSafeEqual } from "node:crypto";
|
|
|
3
3
|
import { getIronSession } from "iron-session";
|
|
4
4
|
import { cookies, headers } from "next/headers";
|
|
5
5
|
import { redirect } from "next/navigation";
|
|
6
|
-
import { NextResponse } from "next/server";
|
|
6
|
+
import { NextResponse, connection } from "next/server";
|
|
7
7
|
import picomatch from "picomatch";
|
|
8
8
|
//#region src/auth.ts
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
const MIN_SECRET_LENGTH = 32;
|
|
10
|
+
const DEFAULT_TTL = 604800;
|
|
11
|
+
const sha256 = (value) => createHash("sha256").update(value).digest();
|
|
12
12
|
/** Constant-time string comparison via SHA-256 digests, safe for values of different lengths */
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
const timingSafeEqualStrings = (a, b) => timingSafeEqual(sha256(a), sha256(b));
|
|
14
|
+
const withParams = (url, params) => {
|
|
15
15
|
const entries = Object.entries(params).filter((entry) => Boolean(entry[1]));
|
|
16
16
|
if (!entries.length) return url;
|
|
17
17
|
return `${url}${url.includes("?") ? "&" : "?"}${new URLSearchParams(entries)}`;
|
|
18
18
|
};
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
const withState = (url, state) => withParams(url, { [STATE_PARAM]: state });
|
|
20
|
+
const matcherToGlobs = (entry) => {
|
|
21
21
|
if (entry.endsWith("/:path*")) {
|
|
22
22
|
const prefix = entry.slice(0, -7);
|
|
23
23
|
return [prefix || "/", `${prefix}/**`];
|
|
24
24
|
}
|
|
25
25
|
return [entry.replace(/:\w+\*/g, "**").replace(/:\w+/g, "*")];
|
|
26
26
|
};
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
const defaultIsValid = (session) => session[AUTHENTICATED] === true;
|
|
28
|
+
const expandPrefix = (glob) => glob.endsWith("/**") ? [glob, glob.slice(0, -3) || "/"] : [glob];
|
|
29
29
|
function createAuth(config) {
|
|
30
30
|
const { cookieName, isValid = defaultIsValid, urls = {} } = config;
|
|
31
31
|
const loginUrl = urls.login ?? "/login";
|
|
@@ -67,7 +67,10 @@ function createAuth(config) {
|
|
|
67
67
|
return false;
|
|
68
68
|
}
|
|
69
69
|
};
|
|
70
|
-
const getSession = async () =>
|
|
70
|
+
const getSession = async () => {
|
|
71
|
+
await connection();
|
|
72
|
+
return getIronSession(await cookies(), getSessionOptions());
|
|
73
|
+
};
|
|
71
74
|
const isAuthenticated = async () => isValid(await getSession());
|
|
72
75
|
const throwNonAuthenticated = async () => {
|
|
73
76
|
const session = await getSession();
|
package/dist/auth.js.map
CHANGED
|
@@ -1 +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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"}
|
|
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 { connection, 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 // Unsealing checks the TTL with `Date.now()`, which Next.js rejects while prerendering (Partial Prefetching\n // prerenders link prefetches with cookies); `connection()` holds the session read until request time.\n const getSession = async (): Promise<IronSession<T>> => {\n await connection();\n return getIronSession<T>(await cookies(), getSessionOptions());\n };\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":";;;;;;;;AAqKA,MAAa,oBAAoB;AAEjC,MAAM,cAAc;AAEpB,MAAM,UAAU,UAA0B,WAAW,QAAQ,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,OAAO;;AAGpF,MAAa,0BAA0B,GAAW,MAAuB,gBAAgB,OAAO,CAAC,GAAG,OAAO,CAAC,CAAC;AAE7G,MAAM,cAAc,KAAa,WAAuD;CACpF,MAAM,UAAU,OAAO,QAAQ,MAAM,CAAC,CAAC,QAAQ,UAAqC,QAAQ,MAAM,EAAE,CAAC;CACrG,IAAI,CAAC,QAAQ,QAAQ,OAAO;CAC5B,OAAO,GAAG,MAAM,IAAI,SAAS,GAAG,IAAI,MAAM,MAAM,IAAI,gBAAgB,OAAO;AAC/E;AAEA,MAAM,aAAa,KAAa,UAA2B,WAAW,KAAK,GAAG,cAAc,MAAM,CAAC;AAGnG,MAAM,kBAAkB,UAA4B;CAChD,IAAI,MAAM,SAAS,SAAS,GAAG;EAC3B,MAAM,SAAS,MAAM,MAAM,GAAG,EAAiB;EAC/C,OAAO,CAAC,UAAU,KAAK,GAAG,OAAO,IAAI;CACzC;CACA,OAAO,CAAC,MAAM,QAAQ,WAAW,IAAI,CAAC,CAAC,QAAQ,SAAS,GAAG,CAAC;AAChE;AAGA,MAAM,kBAAkB,YAAsC,QAAQ,mBAAmB;AAIzF,MAAM,gBAAgB,SAA4B,KAAK,SAAS,KAAK,IAAI,CAAC,MAAM,KAAK,MAAM,GAAG,EAAE,KAAK,GAAG,IAAI,CAAC,IAAI;AAEjH,SAAgB,WACZ,QACU;CACV,MAAM,EAAE,YAAY,UAAU,gBAAsD,OAAO,CAAC,MAAM;CAClG,MAAM,WAAc,KAAK,SAAU;CAEnC,MAAM,eAAoC;EACtC,OAAO;EACP,YAAY,KAAK,cAAe;EAChC,aAAa,KAAK,eAAe;EACjC,QAAQ,KAAK,UAAU,CAAC;EACxB,WAAW,KAAK,aAAa,CAAC;CAClC;CAGA,IAAI,aAAa,OAAO,aAAa,WAAW,WAAW,QAAQ,GAC/D,MAAM,IAAI,MAAM,sFAAsF,EAClG,OAAO;EAAE;EAAY,GAAG;CAAK,EACjC,CAAC;CAGL,MAAM,eAAe,KAAK,QAAQ,SAAS,UAAU,KAAK,OAAO,QAAQ,YAAY,CAAC,IAAI,KAAA;CAC1F,MAAM,kBAAkB,KAAK,WAAW,SAAS,UAAU,KAAK,UAAU,QAAQ,YAAY,CAAC,IAAI,KAAA;CAEnG,MAAM,sBAA8B;EAChC,MAAM,SAAS,OAAO,UAAU,QAAQ,IAAI;EAC5C,IAAI,CAAC,UAAU,OAAO,SAAA,IAClB,MAAM,IAAI,MACN,sFACA,EAAE,OAAO,EAAE,WAAW,EAAE,CAC5B;EAEJ,OAAO;CACX;CAEA,MAAM,2BAA2C;EAC7C;EACA,UAAU,cAAc;EACxB,KAAK,OAAO,OAAO;EACnB,eAAe;GACX,UAAU;GACV,UAAU;GACV,MAAM;GACN,QAAQ,QAAQ,IAAI,aAAa;GACjC,GAAG,OAAO;EACd;CACJ;CAEA,MAAM,qBAA8B;EAChC,IAAI;GACA,cAAc;GACd,OAAO;EACX,QAAQ;GACJ,OAAO;EACX;CACJ;CAIA,MAAM,aAAa,YAAqC;EACpD,MAAM,WAAW;EACjB,OAAO,eAAkB,MAAM,QAAQ,GAAG,kBAAkB,CAAC;CACjE;CAEA,MAAM,kBAAkB,YAA8B,QAAQ,MAAM,WAAW,CAAC;CAEhF,MAAM,wBAAwB,YAAqC;EAC/D,MAAM,UAAU,MAAM,WAAW;EACjC,IAAI,CAAC,QAAQ,OAAO,GAAG;GACnB,OAAO,iBAAiB;GACxB,MAAM,IAAI,MAAM,cAAc;EAClC;EACA,OAAO;CACX;CAEA,MAAM,iBAAiB,aAA4B,MAAM,WAAW,EAAA,CAAG,QAAQ;CAE/E,MAAM,YAAY,WAA4B;EAC1C,IAAI,QAAiB;EACrB,IAAI,kBAAkB,mBAAmB,kBAAkB,UAAU,QAAQ,OAAO,IAAI,WAAW;OAC9F,IAAI,UAAU,OAAO,WAAW,UACjC,QACI,aAAa,SACN,OAAuB,QAAQ,aAAa,IAAI,WAAW,IAC3D,OAAyD;EAExE,IAAI,MAAM,QAAQ,KAAK,GAAG,QAAQ,MAAM;EACxC,IAAI,OAAO,UAAU,YAAY,CAAC,OAAO,OAAO,aAAa;EAC7D,IAAI,CAAC,MAAM,WAAW,GAAG,KAAK,MAAM,WAAW,IAAI,GAAG,OAAO,aAAa;EAC1E,IAAI,MAAM,WAAW,QAAQ,GAAG,OAAO,aAAa;EACpD,OAAO;CACX;CAEA,OAAO;EACH,MAAM;EACN;EACA;EACA;EACA;EACA,mBAAmB,OAAO,SAAS,aAAa,eAAkB,SAAS,UAAU,kBAAkB,CAAC;EACxG;EACA;EACA,0BAA0B,OAAO,UAAU;GACvC,MAAM,UAAU,MAAM,WAAW;GACjC,IAAI,CAAC,QAAQ,OAAO,GAAG;IAInB,MAAM,SAAS,SAAS,UAAU,MAAM,QAAQ,EAAA,CAAG,IAAA,YAAmB,CAAC;IACvE,SAAS,UAAU,UAAU,WAAW,aAAa,aAAa,KAAA,IAAY,MAAM,CAAC;GACzF;GACA,OAAO;EACX;EACA,yBAAyB,OAAO,eAAe;GAC3C,MAAM,UAAU,MAAM,WAAW;GACjC,IAAI,QAAQ,OAAO,GAAG,SAAS,cAAc,aAAa,UAAU;EACxE;EACA;EACA,YAAY,OAAO,SAAS;GACxB,MAAM,UAAU,MAAM,WAAW;GACjC,OAAO,OAAO,SAAS,IAAI;GAC3B,MAAM,QAAQ,KAAK;GACnB,OAAO;EACX;EACA;EACA,QAAQ,OAAO,eAAe;GAC1B,MAAM,eAAe;GACrB,UAAU,OAAO,eAAe,WAAW,aAAa,KAAA,MAAc,aAAa,WAAW;EAClG;EACA,4BAA4B,YAAY,OAAO,SAAS,YAAY;GAChE,IAAI,CAAE,MAAM,gBAAgB,GAAI,OAAO,SAAS,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;GAC/F,OAAO,QAAQ,SAAS,OAAO;EACnC;EACA,aAAa,EAAE,OAAO,YAA+B,CAAC,MAAM;GACxD,IAAI,SAAS;IAGT,MAAM,YAAY,UAAU,CAAC,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,QAAQ,cAAc,CAAC;IACpE,MAAM,YAAY,CAAC,GAAG,aAAa,QAAQ,GAAG,aAAa,SAAS,CAAC,CAAC,QACjE,SAAS,CAAC,aAAa,IAAI,CAAC,CAAC,OAAO,UAAU,UAAU,MAAM,QAAQ,OAAO,QAAQ,CAAC,CAAC,CAC5F;IACA,IAAI,UAAU,QACV,MAAM,IAAI,MAAM,iFAAiF,EAC7F,OAAO;KAAE;KAAW;KAAS;IAAW,EAC5C,CAAC;GAET;GAEA,OAAO,OAAO,YAAgD;IAC1D,MAAM,EAAE,aAAa,QAAQ;IAG7B,MAAM,iBAAiB,IAAI,QAAQ,QAAQ,OAAO;IAClD,eAAe,IAAI,iBAAiB,QAAQ;IAC5C,MAAM,aAAa,aAAa,KAAK,EAAE,SAAS,EAAE,SAAS,eAAe,EAAE,CAAC;IAE7E,IAAI,QAAQ,OAAO,GAAG,OAAO,KAAK;IAElC,IAAI,gBAAgB,iBAAiB;KACjC,MAAM,WAAW,eAAe,QAAQ,KAAK;KAC7C,MAAM,cAAc,kBAAkB,QAAQ,KAAK;KAEnD,IAAI,YAAY,aACZ,MAAM,IAAI,MAAM,0DAA0D,EACtE,OAAO;MAAE;MAAU,QAAQ,KAAK;MAAQ,WAAW,KAAK;KAAU,EACtE,CAAC;KAEL,IAAI,UAAU,OAAO,KAAK;KAC1B,IAAI,CAAC,aAAa;MACd,QAAQ,KAAK,sDAAsD;OAAE;OAAU;MAAW,CAAC;MAC3F,OAAO,KAAK;KAChB;IACJ;IAGA,IAAI,CAAC,aAAa,GACd,OAAO,aAAa,KAAK,EAAE,OAAO,mCAAmC,GAAG,EAAE,QAAQ,IAAI,CAAC;IAE3F,MAAM,WAAW,KAAK;IACtB,MAAM,UAAU,MAAM,eAAkB,SAAS,UAAU,kBAAkB,CAAC;IAC9E,IAAI,QAAQ,OAAO,GAAG,OAAO;IAC7B,OAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;GACvE;EACJ;CACJ;AACJ;AAEA,SAAgB,mBAA8C,QAAgD;CAC1G,MAAM,OAAO,WAA+B,MAAM;CAElD,MAAM,wBAA4C,OAAO,YAAY,QAAQ,IAAI;CAEjF,MAAM,kBAAkB,cAAgC;EACpD,MAAM,WAAW,gBAAgB;EACjC,IAAI,OAAO,cAAc,YAAY,CAAC,aAAa,CAAC,UAAU,OAAO;EACrE,OAAO,uBAAuB,WAAW,QAAQ;CACrD;CAEA,MAAM,QAAQ,OAAO,cAAyC;EAC1D,IAAI,CAAC,eAAe,SAAS,GAAG,OAAO;EAEvC,MAAM,KAAK,eAAe;EAC1B,MAAM,KAAK,WAAW,GAAG,gBAAgB,KAAK,CAAC;EAC/C,OAAO;CACX;CAEA,MAAM,qBAA8B,KAAK,aAAa,KAAK,QAAQ,gBAAgB,CAAC;CAGpF,MAAM,cAAc,OAAO,aAAwC;EAC/D,IAAI,CAAC,aAAa,GAAG,OAAO;EAC5B,IAAI,CAAE,MAAM,MAAM,SAAS,IAAI,UAAU,CAAC,GAAI,OAAO;EACrD,SAAS,KAAK,SAAS,QAAQ,CAAC;CACpC;CAEA,OAAO;EACH,GAAG;EACH;EACA;EACA;EACA,aAAa,OAAO,aAAa;GAC7B,MAAM,QAAQ,MAAM,YAAY,QAAQ;GAGxC,MAAM,QAAQ,KAAK,SAAS,QAAQ;GACpC,SACI,WAAW,KAAK,KAAK,OAAO;KACvB,cAAc;KACd,cAAc,UAAU,KAAK,KAAK,aAAa,KAAA,IAAY;GAChE,CAAC,CACL;EACJ;EACA,iBAAiB,OAAO,YAAY,aAAa,YAAY,QAAQ;CACzE;AACJ"}
|
package/dist/client/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
//#region src/client/index.d.ts
|
|
1
2
|
/** Name of the search param / form field carrying the return URL through the login flow */
|
|
2
3
|
export declare const STATE_PARAM = "state";
|
|
3
4
|
/** Name of the search param carrying the login error message (`loginAction` failure redirects) */
|
|
@@ -13,4 +14,5 @@ export declare const PATHNAME_HEADER = "x-pathname";
|
|
|
13
14
|
* into the sealed cookie, symbol-keyed fields would be silently dropped
|
|
14
15
|
*/
|
|
15
16
|
export declare const AUTHENTICATED = "__authenticated";
|
|
17
|
+
//#endregion
|
|
16
18
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/client/index.ts"],"mappings":";;qBAIa;;qBAGA;;;;;qBAMA;;;;;;qBAOA"}
|
package/dist/client/index.js
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
//#region src/client/index.ts
|
|
2
2
|
/** Name of the search param / form field carrying the return URL through the login flow */
|
|
3
|
-
|
|
3
|
+
const STATE_PARAM = "state";
|
|
4
4
|
/** Name of the search param carrying the login error message (`loginAction` failure redirects) */
|
|
5
|
-
|
|
5
|
+
const ERROR_PARAM = "error";
|
|
6
6
|
/**
|
|
7
7
|
* Request header `proxyGuard` stamps with the current pathname — layouts cannot see the URL, so
|
|
8
8
|
* `redirectNonAuthenticated()` reads it back to return users to the page they were on after login
|
|
9
9
|
*/
|
|
10
|
-
|
|
10
|
+
const PATHNAME_HEADER = "x-pathname";
|
|
11
11
|
/**
|
|
12
12
|
* Session field marking a password-authenticated session — lib-owned, collision-safe, nothing to
|
|
13
13
|
* configure. A string constant and NOT a JS `Symbol`: iron-session JSON-serializes the session
|
|
14
14
|
* into the sealed cookie, symbol-keyed fields would be silently dropped
|
|
15
15
|
*/
|
|
16
|
-
|
|
16
|
+
const AUTHENTICATED = "__authenticated";
|
|
17
17
|
//#endregion
|
|
18
18
|
export { AUTHENTICATED, ERROR_PARAM, PATHNAME_HEADER, STATE_PARAM };
|
|
19
19
|
|
package/dist/client/index.js.map
CHANGED
|
@@ -1 +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,
|
|
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,MAAa,cAAc;;AAG3B,MAAa,cAAc;;;;;AAM3B,MAAa,kBAAkB;;;;;;AAO/B,MAAa,gBAAgB"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { AUTHENTICATED, ERROR_PARAM, PATHNAME_HEADER, STATE_PARAM } from "./client/index.js";
|
|
2
|
+
import { Auth, AuthConfig, AuthUrls, MIN_SECRET_LENGTH, PasswordAuth, PasswordAuthConfig, PasswordSession, ProxyGuardOptions, ResolvedAuthUrls, RouteHandler, StateSource, createAuth, createPasswordAuth, timingSafeEqualStrings } from "./auth.js";
|
|
3
|
+
export { AUTHENTICATED, Auth, AuthConfig, AuthUrls, ERROR_PARAM, MIN_SECRET_LENGTH, PATHNAME_HEADER, PasswordAuth, PasswordAuthConfig, PasswordSession, ProxyGuardOptions, ResolvedAuthUrls, RouteHandler, STATE_PARAM, StateSource, createAuth, createPasswordAuth, timingSafeEqualStrings };
|
package/package.json
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kirill.konshin/auth",
|
|
3
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.
|
|
4
|
+
"version": "0.0.2",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"----- BUILD -----": "",
|
|
8
8
|
"clean": "rm -rf dist .tscache tsconfig.tsbuildinfo",
|
|
9
|
-
"build": "
|
|
9
|
+
"build": "vp pack",
|
|
10
10
|
"build:index": "cti create ./src",
|
|
11
11
|
"build:check-types": "attw --pack .",
|
|
12
12
|
"start": "yarn build --watch",
|
|
13
13
|
"wait": "wait-on ./dist/index.js",
|
|
14
14
|
"----- TEST -----": "",
|
|
15
|
-
"test": "
|
|
16
|
-
"test:watch": "
|
|
15
|
+
"test": "vp test run --coverage",
|
|
16
|
+
"test:watch": "vp test watch --coverage",
|
|
17
17
|
"----- STORYBOOK -----": "",
|
|
18
18
|
"storybook:start": "storybook dev -p 6006",
|
|
19
19
|
"storybook:build": "storybook build"
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@kirill.konshin/utils-private": "*",
|
|
27
27
|
"@types/picomatch": "^4.0.3",
|
|
28
|
-
"next": "^16.
|
|
28
|
+
"next": "^16.3.0"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
31
|
"next": "^16"
|
|
@@ -41,17 +41,15 @@
|
|
|
41
41
|
"author": "Kirill Konshin <kirill@konshin.org> (https://konshin.org)",
|
|
42
42
|
"license": "MIT",
|
|
43
43
|
"exports": {
|
|
44
|
-
"./client": {
|
|
45
|
-
"import": {
|
|
46
|
-
"import": "./dist/client/index.js",
|
|
47
|
-
"types": "./dist/client/index.d.ts"
|
|
48
|
-
}
|
|
49
|
-
},
|
|
50
44
|
".": {
|
|
51
|
-
"
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
45
|
+
"types": "./dist/index.d.ts",
|
|
46
|
+
"import": "./dist/index.js",
|
|
47
|
+
"default": "./dist/index.js"
|
|
48
|
+
},
|
|
49
|
+
"./client": {
|
|
50
|
+
"types": "./dist/client/index.d.ts",
|
|
51
|
+
"import": "./dist/client/index.js",
|
|
52
|
+
"default": "./dist/client/index.js"
|
|
55
53
|
}
|
|
56
54
|
},
|
|
57
55
|
"main": "./dist/index.js",
|
package/dist/auth.test.d.ts
DELETED
package/dist/auth.test.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"auth.test.d.ts","sourceRoot":"","sources":["../src/auth.test.ts"],"names":[],"mappings":""}
|
package/dist/index.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,QAAQ,CAAC;AACvB,cAAc,UAAU,CAAC"}
|