najm-auth 3.1.5 → 3.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -129,9 +129,10 @@ auth({
129
129
  database?: string // Default: 'default'
130
130
  blacklistPrefix?: string // Default: 'auth:blacklist:'
131
131
 
132
- // Registration
133
- defaultRole?: string | null // Auto-assign role to new users
134
- bcryptRounds?: number // Default: 10 (valid: 4-31)
132
+ // Registration
133
+ defaultRole?: string | null // Auto-assign role to new users
134
+ publicRegistration?: boolean // Default: true; mounts POST /auth/register
135
+ bcryptRounds?: number // Default: 10 (valid: 4-31)
135
136
 
136
137
  // Frontend
137
138
  frontendUrl?: string // Password reset link base URL
@@ -181,7 +182,7 @@ All routes are prefixed with `/auth` and auto-registered by the plugin.
181
182
 
182
183
  | Method | Path | Description | Auth |
183
184
  |--------|------|-------------|------|
184
- | `POST` | `/auth/register` | Register new user | None |
185
+ | `POST` | `/auth/register` | Register new user (omitted when `publicRegistration: false`) | None |
185
186
  | `POST` | `/auth/login` | Login with email/password | None |
186
187
  | `POST` | `/auth/refresh` | Refresh access token (cookie) | None (uses refresh cookie) |
187
188
  | `POST` | `/auth/session/recover` | Reissue signed session without token rotation | Refresh cookie + recovery header |
@@ -194,7 +195,12 @@ All routes are prefixed with `/auth` and auto-registered by the plugin.
194
195
  | `POST` | `/auth/oauth/google/link` | Link Google to the current user | ✅ Required |
195
196
  | `GET` | `/auth/credential-setup/setup` | Read the pending setup session | Setup cookie |
196
197
  | `POST` | `/auth/credential-setup/change` | Replace the temporary credential | Setup cookie |
197
- | `POST` | `/auth/credential-setup/cancel` | Abandon the setup session | Setup cookie |
198
+ | `POST` | `/auth/credential-setup/cancel` | Abandon the setup session | Setup cookie |
199
+
200
+ Applications with an approval-owned onboarding flow should set
201
+ `publicRegistration: false`. This removes the unauthenticated route while
202
+ retaining `AuthService.registerUser()`, `provisionUser()`, and other internal
203
+ account-management APIs for trusted application services.
198
204
 
199
205
  ### Identity presets
200
206
 
@@ -766,13 +772,14 @@ auth({
766
772
 
767
773
  ## Next.js App Router Structure
768
774
 
769
- Every App Router application keeps the same three files. Copying more than this
775
+ Every App Router application keeps the same four files. Copying more than this
770
776
  between apps means logic that belongs in the package has leaked into them.
771
777
 
772
778
  ```text
773
779
  src/lib/auth.ts defineAuth() configuration — browser, server, and proxy safe
774
780
  src/lib/session.ts one createReactServerAuth() instance for Server Components
775
- src/proxy.ts imports auth.ts only, and exports auth.middleware
781
+ src/proxy.ts exports auth.proxy plus Next's required static matcher
782
+ src/app/api/[...route]/route.ts binds the server through auth.routeHandlers()
776
783
  ```
777
784
 
778
785
  ```typescript
@@ -786,6 +793,7 @@ export const auth = defineAuth({
786
793
  publicRoutes: ['/', '/login'],
787
794
  protectedRoutes: ['/dashboard/:path*', '/admin/:path*'],
788
795
  roleRoutes: { '/admin/:path*': ['admin'] },
796
+ proxySessionMode: 'optimistic',
789
797
  });
790
798
  ```
791
799
 
@@ -804,10 +812,33 @@ export const serverAuth = createReactServerAuth(auth);
804
812
  // src/proxy.ts
805
813
  import { auth } from './lib/auth';
806
814
 
807
- export default auth.middleware;
808
- export const config = auth.config;
815
+ export default auth.proxy;
816
+ export const config = {
817
+ matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
818
+ };
809
819
  ```
810
820
 
821
+ Next.js 16 requires the exported Proxy `config` to be a statically analyzable
822
+ object literal. Turbopack rejects `export const config = auth.config`, so the
823
+ matcher is the one integration value that cannot be composed at runtime.
824
+
825
+ ```typescript
826
+ // src/app/api/[...route]/route.ts
827
+ import { handle } from 'najm-core';
828
+ import server from '@app/server';
829
+
830
+ import { auth } from '../../../lib/auth';
831
+
832
+ const handlers = auth.routeHandlers(handle(server));
833
+ export const { GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS } = handlers;
834
+ ```
835
+
836
+ `auth.routeHandlers()` applies the remember-me lifecycle to login, refresh,
837
+ credential setup, and logout for every supported Next.js verb. It automatically
838
+ uses the refresh and signed-session cookie names from `defineAuth()`; an app only
839
+ passes an option when it intentionally customizes behavior, such as
840
+ `{ rememberCookieName: 'school.remember' }`.
841
+
811
842
  ### Why `session.ts` exists
812
843
 
813
844
  A Next.js page is not one function. The root layout, each nested layout, and the
@@ -862,7 +893,7 @@ the same reason in mirror image:
862
893
  | the `createReactServerAuth()` module | server only | browser, Edge |
863
894
 
864
895
  `auth.client` and `auth.api` are what Client Components call, and
865
- `auth.middleware` is what the Edge proxy calls, so the `defineAuth()` module is
896
+ `auth.proxy` is what the Edge proxy calls, so the `defineAuth()` module is
866
897
  always in the browser and Edge graphs. The adapter must never be. Putting both
867
898
  in one file puts the adapter everywhere `auth` already is, and the `browser`
868
899
  export condition — which exists precisely to catch this — resolves to a module
@@ -910,13 +941,19 @@ into a silently anonymous page.
910
941
  |---|---|
911
942
  | `loginRoute`, `forbiddenRoute`, route matchers, `roleRoutes` | when to redirect where |
912
943
  | cookie names, `apiBaseURL`, `authPrefix`, recovery URL | request memoization |
913
- | `refreshThreshold`, `tabSync`, `verifyAlways` | strict vs optional semantics |
944
+ | `refreshThreshold`, `tabSync`, `proxySessionMode` | strict vs optional semantics |
914
945
  | — | `session.roles` / `user.role` fallback |
915
946
  | — | error classification |
916
947
 
917
- If a new app has to copy anything beyond the three files above, that logic
948
+ If a new app has to copy anything beyond the four files above, that logic
918
949
  belongs in the package instead.
919
950
 
951
+ `proxySessionMode: 'optimistic'` is the default and locally verifies the signed
952
+ snapshot, matching Next.js guidance that Proxy is an optimistic routing boundary.
953
+ Use `'authoritative'` only when every protected navigation must also validate
954
+ refresh-session state. The older `verifyAlways` option and `auth.middleware`
955
+ property remain as deprecated compatibility aliases.
956
+
920
957
  ### What a new app must prove
921
958
 
922
959
  At its real Next.js production boundary, not with mocks:
@@ -999,8 +1036,10 @@ throw new HttpError(403, 'Insufficient permissions for this action');
999
1036
  when their public reverse-proxy origin is not reachable from the app process.
1000
1037
  - `onRecoveryFailure` exposes structured, secret-free recovery diagnostics
1001
1038
  without logging anything by default.
1002
- - `verifyAlways` forces that authoritative check on every protected request;
1003
- the default bounds cached role/status staleness to `session.maxAge`.
1039
+ - `proxySessionMode: 'authoritative'` forces that check on every protected
1040
+ request; the default `'optimistic'` mode bounds cached role/status staleness
1041
+ to `session.maxAge`. The deprecated `verifyAlways` flag maps to the same
1042
+ behavior for existing applications.
1004
1043
 
1005
1044
  ### Next.js 16 Reverse-Proxy Recovery
1006
1045
 
@@ -2,6 +2,7 @@ import * as next_server from 'next/server';
2
2
  import { S as SessionRecoveryFailure } from '../sessionRecovery-D5Fa0yZ1.js';
3
3
  export { a as SessionRecoveryErrorDetails, b as SessionRecoveryFailureReason } from '../sessionRecovery-D5Fa0yZ1.js';
4
4
 
5
+ type ProxySessionMode = 'optimistic' | 'authoritative';
5
6
  interface AuthMiddlewareConfig {
6
7
  /** Routes that require authentication (glob patterns) */
7
8
  protectedRoutes?: string[];
@@ -28,8 +29,15 @@ interface AuthMiddlewareConfig {
28
29
  /**
29
30
  * Force authoritative refresh-session validation on every protected request.
30
31
  * This reissues the signed session cookie without rotating refresh tokens.
32
+ * @deprecated Use `proxySessionMode: 'authoritative'` instead.
31
33
  */
32
34
  verifyAlways?: boolean;
35
+ /**
36
+ * How Proxy handles an otherwise valid signed session snapshot.
37
+ * `optimistic` verifies it locally; `authoritative` also checks refresh state.
38
+ * Defaults to `optimistic`.
39
+ */
40
+ proxySessionMode?: ProxySessionMode;
33
41
  /**
34
42
  * Session-recovery endpoint. Relative values resolve against the request
35
43
  * origin. Defaults to `${apiBaseURL}${authPrefix}/session/recover`.
@@ -67,4 +75,4 @@ interface AuthMiddlewareConfig {
67
75
  */
68
76
  declare function withAuthMiddleware(config: AuthMiddlewareConfig): (request: Request) => Promise<next_server.NextResponse<unknown>>;
69
77
 
70
- export { type AuthMiddlewareConfig, SessionRecoveryFailure, withAuthMiddleware };
78
+ export { type AuthMiddlewareConfig, type ProxySessionMode, SessionRecoveryFailure, withAuthMiddleware };
@@ -345,11 +345,13 @@ function withAuthMiddleware(config) {
345
345
  sessionCookieName = "najm.session",
346
346
  sessionSecret,
347
347
  sessionMaxAge,
348
- verifyAlways = false,
348
+ verifyAlways: legacyVerifyAlways = false,
349
+ proxySessionMode,
349
350
  recoveryURL,
350
351
  internalRecoveryURL,
351
352
  onRecoveryFailure
352
353
  } = config;
354
+ const verifyAlways = proxySessionMode === void 0 ? legacyVerifyAlways : proxySessionMode === "authoritative";
353
355
  const resolvedInternalRecoveryURL = resolveInternalRecoveryURL(internalRecoveryURL);
354
356
  return /* @__PURE__ */ __name(async function middleware(request) {
355
357
  const { NextResponse } = await import("next/server");
@@ -1,5 +1,6 @@
1
1
  import { A as AuthUser, R as RetryConfig } from '../../types-BaSfgxqE.js';
2
2
  import { F as FetchClient, N as NajmAuthClient } from '../../NajmAuthClient-ZtXTIUSF.js';
3
+ import { ProxySessionMode } from '../edge.js';
3
4
  export { withAuthMiddleware } from '../edge.js';
4
5
  import { G as GetSessionConfig, S as ServerSession$1 } from '../../getSession-BthP85UA.js';
5
6
  export { A as AuthConfigError, a as AuthTransportError, N as NoSessionError, g as getSession } from '../../getSession-BthP85UA.js';
@@ -88,6 +89,68 @@ interface WithAuthProps<P> {
88
89
  */
89
90
  declare function withAuth<P extends Record<string, unknown> = Record<string, unknown>>(Page: (args: WithAuthProps<P>) => Promise<unknown> | unknown, options?: WithAuthOptions): (props: P) => Promise<unknown>;
90
91
 
92
+ type AuthRouteHandler<Args extends unknown[] = []> = (request: Request, ...args: Args) => Response | Promise<Response>;
93
+ interface NextAuthRouteHandlers<Args extends unknown[] = []> {
94
+ GET: AuthRouteHandler<Args>;
95
+ POST: AuthRouteHandler<Args>;
96
+ PUT: AuthRouteHandler<Args>;
97
+ PATCH: AuthRouteHandler<Args>;
98
+ DELETE: AuthRouteHandler<Args>;
99
+ HEAD: AuthRouteHandler<Args>;
100
+ OPTIONS: AuthRouteHandler<Args>;
101
+ }
102
+ interface AuthCookiePersistenceOptions {
103
+ /**
104
+ * Cookies whose lifetime this rewrites. Defaults to the two najm-auth issues.
105
+ * Anything not named here is passed through untouched.
106
+ */
107
+ authCookieNames?: string[];
108
+ /** Where the one-bit choice is stored. Defaults to `najm.remember`. */
109
+ rememberCookieName?: string;
110
+ /** How long a remembered choice lasts. Defaults to 7 days. */
111
+ maxAgeSeconds?: number;
112
+ /** Paths whose JSON body carries `rememberMe`. Defaults to `/api/auth/login`. */
113
+ loginPaths?: string[];
114
+ /** Paths that end a session and clear the choice. Defaults to `/api/auth/logout`. */
115
+ logoutPaths?: string[];
116
+ /** Paths that reissue cookies and must reapply the stored choice. */
117
+ refreshPaths?: string[];
118
+ /**
119
+ * Paths that finish credential setup. The stored choice is cleared there:
120
+ * the login it was recorded for never produced a session.
121
+ */
122
+ setupCompletionPaths?: string[];
123
+ /**
124
+ * Recognizes a response that has *not* issued a usable session because the
125
+ * user must still set up credentials.
126
+ *
127
+ * Najm's own setup response is recognized without this — supply it only to
128
+ * cover an application-specific shape. Such a response may carry auth
129
+ * cookies anyway, and persisting them would leave a half-authenticated
130
+ * browser that skips the setup step on reload. Returning `true` replaces
131
+ * them with deletions and clears the stored choice.
132
+ */
133
+ isSetupResponse?: (payload: unknown) => boolean;
134
+ }
135
+ /**
136
+ * Strips the lifetime attributes so the browser drops the cookie when it closes.
137
+ *
138
+ * Only the named auth cookies are touched — rewriting an unrelated `Set-Cookie`
139
+ * from the same response would be a silent side effect on someone else's state.
140
+ */
141
+ declare function makeSessionCookie(setCookie: string, authCookieNames?: string[]): string;
142
+ /**
143
+ * Wraps a request handler so the auth cookies it issues match the user's
144
+ * "remember me" choice.
145
+ *
146
+ * ```ts
147
+ * // app/api/[...route]/route.ts
148
+ * const handler = withAuthCookiePersistence((req) => server.fetch(req));
149
+ * export { handler as GET, handler as POST };
150
+ * ```
151
+ */
152
+ declare function withAuthCookiePersistence<Args extends unknown[] = []>(handler: AuthRouteHandler<Args>, options?: AuthCookiePersistenceOptions): AuthRouteHandler<Args>;
153
+
91
154
  interface DefineAuthConfig {
92
155
  /** API base URL (default: '/api') */
93
156
  apiBaseURL?: string;
@@ -138,13 +201,22 @@ interface DefineAuthConfig {
138
201
  recoveryURL?: string | false;
139
202
  /** Loopback-only recovery endpoint for self-hosted reverse-proxy setups. */
140
203
  internalRecoveryURL?: string;
141
- /** Next.js middleware matcher (default: exclude _next, favicon, api) */
204
+ /**
205
+ * Next.js middleware matcher (default: exclude _next, favicon, api).
206
+ * @deprecated Next.js 16 requires a static matcher literal in `proxy.ts`.
207
+ */
142
208
  matcher?: string[];
143
209
  /**
144
210
  * Force authoritative refresh-session validation on every protected request.
145
211
  * Recovery reissues the signed cookie without rotating refresh tokens.
212
+ * @deprecated Use `proxySessionMode: 'authoritative'` instead.
146
213
  */
147
214
  verifyAlways?: boolean;
215
+ /**
216
+ * How Proxy handles a valid signed session snapshot. Defaults to `optimistic`.
217
+ * API and server authorization remain authoritative in either mode.
218
+ */
219
+ proxySessionMode?: ProxySessionMode;
148
220
  /** Secret-free diagnostic hook for failed server or proxy recovery. */
149
221
  onRecoveryFailure?: (failure: SessionRecoveryFailure) => void;
150
222
  }
@@ -171,12 +243,19 @@ interface AuthKit {
171
243
  * ```
172
244
  */
173
245
  requireRole: (roles: string[]) => Promise<ServerSession$1>;
174
- /** Generated Next.js middleware function */
246
+ /** Generated Next.js 16 Proxy function. */
247
+ proxy: (request: Request) => Promise<Response>;
248
+ /** @deprecated Next.js 16 renamed Middleware to Proxy. Use `proxy`. */
175
249
  middleware: (request: Request) => Promise<Response>;
176
- /** Next.js middleware config with matcher */
250
+ /** @deprecated Next.js 16 requires a static config literal in `proxy.ts`. */
177
251
  config: {
178
252
  matcher: string[];
179
253
  };
254
+ /**
255
+ * Bind a Web Request handler to every Next.js Route Handler verb and apply
256
+ * Najm's login, refresh, setup, and logout cookie lifecycle consistently.
257
+ */
258
+ routeHandlers: <Args extends unknown[] = []>(handler: AuthRouteHandler<Args>, options?: AuthCookiePersistenceOptions) => NextAuthRouteHandlers<Args>;
180
259
  /**
181
260
  * Protect a server component — redirects to loginRoute if unauthenticated.
182
261
  * Passes session to the wrapped component.
@@ -219,57 +298,4 @@ interface SafeRedirectOptions {
219
298
  */
220
299
  declare function getSafeRedirectPath(value: string | string[] | undefined | null, options?: SafeRedirectOptions | string): string;
221
300
 
222
- type RequestHandler = (request: Request) => Promise<Response>;
223
- interface AuthCookiePersistenceOptions {
224
- /**
225
- * Cookies whose lifetime this rewrites. Defaults to the two najm-auth issues.
226
- * Anything not named here is passed through untouched.
227
- */
228
- authCookieNames?: string[];
229
- /** Where the one-bit choice is stored. Defaults to `najm.remember`. */
230
- rememberCookieName?: string;
231
- /** How long a remembered choice lasts. Defaults to 7 days. */
232
- maxAgeSeconds?: number;
233
- /** Paths whose JSON body carries `rememberMe`. Defaults to `/api/auth/login`. */
234
- loginPaths?: string[];
235
- /** Paths that end a session and clear the choice. Defaults to `/api/auth/logout`. */
236
- logoutPaths?: string[];
237
- /** Paths that reissue cookies and must reapply the stored choice. */
238
- refreshPaths?: string[];
239
- /**
240
- * Paths that finish credential setup. The stored choice is cleared there:
241
- * the login it was recorded for never produced a session.
242
- */
243
- setupCompletionPaths?: string[];
244
- /**
245
- * Recognizes a response that has *not* issued a usable session because the
246
- * user must still set up credentials.
247
- *
248
- * Najm's own setup response is recognized without this — supply it only to
249
- * cover an application-specific shape. Such a response may carry auth
250
- * cookies anyway, and persisting them would leave a half-authenticated
251
- * browser that skips the setup step on reload. Returning `true` replaces
252
- * them with deletions and clears the stored choice.
253
- */
254
- isSetupResponse?: (payload: unknown) => boolean;
255
- }
256
- /**
257
- * Strips the lifetime attributes so the browser drops the cookie when it closes.
258
- *
259
- * Only the named auth cookies are touched — rewriting an unrelated `Set-Cookie`
260
- * from the same response would be a silent side effect on someone else's state.
261
- */
262
- declare function makeSessionCookie(setCookie: string, authCookieNames?: string[]): string;
263
- /**
264
- * Wraps a request handler so the auth cookies it issues match the user's
265
- * "remember me" choice.
266
- *
267
- * ```ts
268
- * // app/api/[...route]/route.ts
269
- * const handler = withAuthCookiePersistence((req) => server.fetch(req));
270
- * export { handler as GET, handler as POST };
271
- * ```
272
- */
273
- declare function withAuthCookiePersistence(handler: RequestHandler, options?: AuthCookiePersistenceOptions): RequestHandler;
274
-
275
- export { type AuthCookiePersistenceOptions, type AuthKit, type DefineAuthConfig, GetSessionConfig, type SafeRedirectOptions, ServerSession$1 as ServerSession, SessionRecoveryFailure, type WithAuthOptions, type WithAuthProps, createServerClient, defineAuth, getSafeRedirectPath, getServerSession, makeSessionCookie, withAuth, withAuthCookiePersistence };
301
+ export { type AuthCookiePersistenceOptions, type AuthKit, type AuthRouteHandler, type DefineAuthConfig, GetSessionConfig, type NextAuthRouteHandlers, ProxySessionMode, type SafeRedirectOptions, ServerSession$1 as ServerSession, SessionRecoveryFailure, type WithAuthOptions, type WithAuthProps, createServerClient, defineAuth, getSafeRedirectPath, getServerSession, makeSessionCookie, withAuth, withAuthCookiePersistence };