najm-auth 3.3.2 → 4.0.0
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 +99 -26
- package/dist/{NajmAuthClient-BBl-GRis.d.ts → NajmAuthClient-BygfBCpF.d.ts} +2 -1
- package/dist/client/edge.d.ts +15 -2
- package/dist/client/edge.js +25 -7
- package/dist/client/index.d.ts +3 -3
- package/dist/client/index.js +3 -0
- package/dist/client/react/index.d.ts +23 -3
- package/dist/client/react/index.js +63 -6
- package/dist/client/server/index.d.ts +8 -8
- package/dist/client/server/index.js +28 -7
- package/dist/client/server/react.d.ts +2 -2
- package/dist/{getSession-BthP85UA.d.ts → getSession-DBX8XpsR.d.ts} +1 -1
- package/dist/index.d.ts +323 -88
- package/dist/index.js +1535 -818
- package/dist/{types-BaSfgxqE.d.ts → types-CI2t8wpJ.d.ts} +1 -1
- package/package.json +6 -5
package/README.md
CHANGED
|
@@ -98,6 +98,10 @@ GOOGLE_CLIENT_ID=<google-web-client-id>
|
|
|
98
98
|
GOOGLE_CLIENT_SECRET=<google-web-client-secret>
|
|
99
99
|
# Optional for a split frontend/API deployment. Otherwise FRONTEND_URL is used.
|
|
100
100
|
GOOGLE_CALLBACK_URL=https://app.example.com/api/auth/oauth/google/callback
|
|
101
|
+
# Optional GitHub sign-in
|
|
102
|
+
GITHUB_CLIENT_ID=<github-oauth-app-client-id>
|
|
103
|
+
GITHUB_CLIENT_SECRET=<github-oauth-app-client-secret>
|
|
104
|
+
GITHUB_CALLBACK_URL=https://app.example.com/api/auth/oauth/github/callback
|
|
101
105
|
```
|
|
102
106
|
|
|
103
107
|
> ⚠️ **Security:** Generate secrets with `openssl rand -base64 32`
|
|
@@ -129,10 +133,10 @@ auth({
|
|
|
129
133
|
database?: string // Default: 'default'
|
|
130
134
|
blacklistPrefix?: string // Default: 'auth:blacklist:'
|
|
131
135
|
|
|
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)
|
|
136
|
+
// Registration
|
|
137
|
+
defaultRole?: string | null // Auto-assign role to new users
|
|
138
|
+
publicRegistration?: boolean // Default: true; mounts POST /auth/register
|
|
139
|
+
bcryptRounds?: number // Default: 10 (valid: 4-31)
|
|
136
140
|
|
|
137
141
|
// Frontend
|
|
138
142
|
frontendUrl?: string // Password reset link base URL
|
|
@@ -152,7 +156,7 @@ auth({
|
|
|
152
156
|
}
|
|
153
157
|
}
|
|
154
158
|
|
|
155
|
-
// Optional
|
|
159
|
+
// Optional external identity providers
|
|
156
160
|
oauth?: {
|
|
157
161
|
google?: true | {
|
|
158
162
|
clientId?: string // Or GOOGLE_CLIENT_ID
|
|
@@ -164,6 +168,15 @@ auth({
|
|
|
164
168
|
autoLinkVerifiedEmail?: boolean // Default: false
|
|
165
169
|
allowedHostedDomains?: string[] // Validates the Google hd claim
|
|
166
170
|
}
|
|
171
|
+
github?: true | {
|
|
172
|
+
clientId?: string // Or GITHUB_CLIENT_ID
|
|
173
|
+
clientSecret?: string // Or GITHUB_CLIENT_SECRET
|
|
174
|
+
callbackUrl?: string // Or GITHUB_CALLBACK_URL
|
|
175
|
+
frontendCallbackPath?: string // Default: /auth/oauth/callback
|
|
176
|
+
errorRedirectPath?: string // Default: /login
|
|
177
|
+
allowSignup?: boolean // Default: true
|
|
178
|
+
autoLinkVerifiedEmail?: boolean // Default: false
|
|
179
|
+
}
|
|
167
180
|
}
|
|
168
181
|
|
|
169
182
|
// Dependencies (forwarded to plugins)
|
|
@@ -182,7 +195,7 @@ All routes are prefixed with `/auth` and auto-registered by the plugin.
|
|
|
182
195
|
|
|
183
196
|
| Method | Path | Description | Auth |
|
|
184
197
|
|--------|------|-------------|------|
|
|
185
|
-
| `POST` | `/auth/register` | Register new user (omitted when `publicRegistration: false`) | None |
|
|
198
|
+
| `POST` | `/auth/register` | Register new user (omitted when `publicRegistration: false`) | None |
|
|
186
199
|
| `POST` | `/auth/login` | Login with email/password | None |
|
|
187
200
|
| `POST` | `/auth/refresh` | Refresh access token (cookie) | None (uses refresh cookie) |
|
|
188
201
|
| `POST` | `/auth/session/recover` | Reissue signed session without token rotation | Refresh cookie + recovery header |
|
|
@@ -195,12 +208,12 @@ All routes are prefixed with `/auth` and auto-registered by the plugin.
|
|
|
195
208
|
| `POST` | `/auth/oauth/google/link` | Link Google to the current user | ✅ Required |
|
|
196
209
|
| `GET` | `/auth/credential-setup/setup` | Read the pending setup session | Setup cookie |
|
|
197
210
|
| `POST` | `/auth/credential-setup/change` | Replace the temporary credential | 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.
|
|
211
|
+
| `POST` | `/auth/credential-setup/cancel` | Abandon the setup session | Setup cookie |
|
|
212
|
+
|
|
213
|
+
Applications with an approval-owned onboarding flow should set
|
|
214
|
+
`publicRegistration: false`. This removes the unauthenticated route while
|
|
215
|
+
retaining `AuthService.registerUser()`, `provisionUser()`, and other internal
|
|
216
|
+
account-management APIs for trusted application services.
|
|
204
217
|
|
|
205
218
|
### Identity presets
|
|
206
219
|
|
|
@@ -336,6 +349,20 @@ user has the same email but is not linked, sign-in fails with
|
|
|
336
349
|
`client.linkOAuthAccount('google')` to prove control of both accounts. Setting
|
|
337
350
|
`autoLinkVerifiedEmail: true` opts into verified-email linking.
|
|
338
351
|
|
|
352
|
+
### GitHub Sign-In
|
|
353
|
+
|
|
354
|
+
Enable GitHub with `oauth: { github: true }`. Credentials come from
|
|
355
|
+
`GITHUB_CLIENT_ID` and `GITHUB_CLIENT_SECRET`; the callback defaults to
|
|
356
|
+
`${FRONTEND_URL}/api/auth/oauth/github/callback`. Register that exact callback
|
|
357
|
+
on the GitHub OAuth App, and set `GITHUB_CALLBACK_URL` only for a split-origin
|
|
358
|
+
deployment. GitHub login uses authorization code plus PKCE, requests
|
|
359
|
+
`user:email`, requires a verified primary email, and keys the durable provider
|
|
360
|
+
link by GitHub's numeric user ID.
|
|
361
|
+
|
|
362
|
+
The client exposes `loginWithGitHub()`, `useGitHubLogin()`, and the headless
|
|
363
|
+
`GitHubLoginButton`; the generic `linkOAuthAccount('github')` method links an
|
|
364
|
+
authenticated Najm user.
|
|
365
|
+
|
|
339
366
|
### Admin Routes (all require `@isAdmin()`)
|
|
340
367
|
|
|
341
368
|
| Method | Path | Description |
|
|
@@ -637,7 +664,7 @@ credential_setup_requirements
|
|
|
637
664
|
Existing databases must generate and run a migration after upgrading so the
|
|
638
665
|
new `oauth_accounts`, `credential_setup_sessions`, and
|
|
639
666
|
`credential_setup_requirements` tables exist. Custom `AuthSchema` objects may
|
|
640
|
-
omit `oauthAccounts` while OAuth is disabled, but
|
|
667
|
+
omit `oauthAccounts` while OAuth is disabled, but provider configuration fails
|
|
641
668
|
fast unless the custom schema supplies it. Both credential-setup tables are
|
|
642
669
|
required of a custom schema, because the setup flow is always mounted.
|
|
643
670
|
|
|
@@ -822,6 +849,31 @@ Next.js 16 requires the exported Proxy `config` to be a statically analyzable
|
|
|
822
849
|
object literal. Turbopack rejects `export const config = auth.config`, so the
|
|
823
850
|
matcher is the one integration value that cannot be composed at runtime.
|
|
824
851
|
|
|
852
|
+
When a Proxy generates request-scoped headers for the downstream render, pass
|
|
853
|
+
only the overrides as the optional second argument. Najm merges them over the
|
|
854
|
+
incoming request and preserves them when session recovery replaces the cookie.
|
|
855
|
+
The application still owns any matching response header:
|
|
856
|
+
|
|
857
|
+
```typescript
|
|
858
|
+
export default async function proxy(request: Request) {
|
|
859
|
+
const nonce = btoa(globalThis.crypto.randomUUID());
|
|
860
|
+
const policy = `script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`;
|
|
861
|
+
const response = await auth.proxy(request, {
|
|
862
|
+
requestHeaders: {
|
|
863
|
+
'content-security-policy': policy,
|
|
864
|
+
'x-nonce': nonce,
|
|
865
|
+
},
|
|
866
|
+
});
|
|
867
|
+
response.headers.set('Content-Security-Policy', policy);
|
|
868
|
+
return response;
|
|
869
|
+
}
|
|
870
|
+
```
|
|
871
|
+
|
|
872
|
+
Authentication reads the original request. `requestHeaders` controls only what
|
|
873
|
+
the successful Next.js render receives. Attempts to override `cookie` or
|
|
874
|
+
`authorization` fail closed; only Najm's validated recovery path may replace
|
|
875
|
+
the cookie after it authorizes the recovered session.
|
|
876
|
+
|
|
825
877
|
```typescript
|
|
826
878
|
// src/app/api/[...route]/route.ts
|
|
827
879
|
import { handle } from 'najm-core';
|
|
@@ -1025,8 +1077,9 @@ throw new HttpError(403, 'Insufficient permissions for this action');
|
|
|
1025
1077
|
- Login uses a dummy password hash for missing users to reduce timing leaks.
|
|
1026
1078
|
- Forgot-password responses avoid email enumeration.
|
|
1027
1079
|
- Auth routes register `najm-rate` and ship route-level brute-force limits.
|
|
1028
|
-
- Session cookies are signed
|
|
1029
|
-
|
|
1080
|
+
- Session cookies are signed, short-lived, and bound to their refresh-token
|
|
1081
|
+
family; server auth resolution checks both the session version and positive
|
|
1082
|
+
family liveness.
|
|
1030
1083
|
- Expired signed sessions recover through authoritative, non-rotating refresh
|
|
1031
1084
|
validation; middleware verifies the reissued HMAC before using its claims.
|
|
1032
1085
|
- Server-side recovery sends only the configured refresh cookie and accepts
|
|
@@ -1065,17 +1118,20 @@ errors cannot change the authentication result.
|
|
|
1065
1118
|
|
|
1066
1119
|
### Password Reset Tokens
|
|
1067
1120
|
|
|
1068
|
-
|
|
1121
|
+
Reset and invite links are signed JWTs whose `jti` is stored in the configured
|
|
1122
|
+
cache with the same expiry. `verifyResetToken()` atomically compares and
|
|
1123
|
+
deletes that value, so exactly one concurrent caller can consume a link and a
|
|
1124
|
+
stale link cannot delete the value for a newer one.
|
|
1069
1125
|
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1126
|
+
`AuthService.resetPassword()` validates the replacement password before
|
|
1127
|
+
consumption. Once consumed, a token stays consumed even if the later user
|
|
1128
|
+
mutation fails; restoring it would make the link replayable, so the user must
|
|
1129
|
+
request a new one.
|
|
1130
|
+
|
|
1131
|
+
The built-in memory and Redis drivers implement the required atomic primitive.
|
|
1132
|
+
A custom cache driver may omit `compareAndDelete()` for compatibility with
|
|
1133
|
+
unrelated cache usage, but reset and invite consumption then fails closed. Do
|
|
1134
|
+
not emulate this operation with separate `get()` and `del()` calls.
|
|
1079
1135
|
|
|
1080
1136
|
### Purpose-Bound Credential Setup
|
|
1081
1137
|
|
|
@@ -1125,8 +1181,9 @@ consuming it; `cancel()` revokes it and clears the cookie.
|
|
|
1125
1181
|
### Session Management
|
|
1126
1182
|
|
|
1127
1183
|
- Sessions are multi-device: the token table stores one refresh row per login session (keyed by a unique `tokenFamily`), so a user can stay logged in on several devices at once. Logout and rotation are scoped to the current session; password change/reset revoke every session
|
|
1184
|
+
- Revocation changes the refresh row to a durable `revoked` tombstone until its original expiry. Active-session reads and rotations require `status = active`, so losing Redis cannot revive a logged-out database session; expired tombstones are removed by normal session cleanup
|
|
1128
1185
|
- A stale refresh token presented after the 120-second rotation grace window revokes only that session's family as reuse protection
|
|
1129
|
-
- The signed session cookie is accepted
|
|
1186
|
+
- The signed session cookie is accepted on the fast path only while Redis positively identifies its family as live and owned by the same user; an unknown cache state falls back to authoritative refresh-row recovery
|
|
1130
1187
|
- Use `@RateLimit` on logout for DDoS protection
|
|
1131
1188
|
|
|
1132
1189
|
### Token Blacklist
|
|
@@ -1150,8 +1207,24 @@ consuming it; `cancel()` revokes it and clears the cookie.
|
|
|
1150
1207
|
```bash
|
|
1151
1208
|
bun run test # Run all tests
|
|
1152
1209
|
bun run test:auth # Run auth tests only
|
|
1210
|
+
bun run --cwd packages/najm-auth test:real-infra # Opt-in PostgreSQL + Redis races
|
|
1211
|
+
bun packages/najm-auth/integration/mailpit-forgot-password/run.ts # Loopback Redis + Mailpit HTTP acceptance
|
|
1153
1212
|
```
|
|
1154
1213
|
|
|
1214
|
+
The real-infrastructure suite runs only with `NAJM_AUTH_REAL_INFRA=1`. Supply
|
|
1215
|
+
loopback-only `NAJM_AUTH_REAL_POSTGRES_URL` and `NAJM_AUTH_REAL_REDIS_URL` (or
|
|
1216
|
+
the conventional `DATABASE_URL` and `REDIS_URL`). It creates and drops its own
|
|
1217
|
+
randomly named PostgreSQL database and cleans only its unique Redis key prefix;
|
|
1218
|
+
remote endpoints fail before either service is touched.
|
|
1219
|
+
|
|
1220
|
+
The Mailpit acceptance runner requires Redis on `127.0.0.1:6399`, Mailpit SMTP
|
|
1221
|
+
on `127.0.0.1:1025`, and the Mailpit API on `127.0.0.1:8025` by default. It
|
|
1222
|
+
boots the real auth plugin over HTTP with an ephemeral SQLite fixture, proves
|
|
1223
|
+
ignored fields and spoofed forwarding headers cannot buy more reset emails,
|
|
1224
|
+
and removes only its run-specific messages and Redis keys. The three endpoints
|
|
1225
|
+
can be changed with the `NAJM_AUTH_MAILPIT_*` variables, but non-loopback
|
|
1226
|
+
values fail before the fixture is created.
|
|
1227
|
+
|
|
1155
1228
|
Test files include:
|
|
1156
1229
|
- `schema.test.ts` — Schema exports validation
|
|
1157
1230
|
- `auth.test.ts` — Authentication flow
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { R as RetryConfig, a as RequestOptions, A as AuthUser, b as AuthClientConfig, L as LoginCredentials, c as LoginResult, O as OAuthProvider, d as OAuthLoginOptions, e as AuthState, f as AuthEvent, g as AuthEventHandler } from './types-
|
|
1
|
+
import { R as RetryConfig, a as RequestOptions, A as AuthUser, b as AuthClientConfig, L as LoginCredentials, c as LoginResult, O as OAuthProvider, d as OAuthLoginOptions, e as AuthState, f as AuthEvent, g as AuthEventHandler } from './types-CI2t8wpJ.js';
|
|
2
2
|
|
|
3
3
|
interface FetchClientConfig {
|
|
4
4
|
baseURL: string;
|
|
@@ -63,6 +63,7 @@ declare class NajmAuthClient {
|
|
|
63
63
|
getOAuthLoginUrl(provider: OAuthProvider, options?: OAuthLoginOptions): string;
|
|
64
64
|
loginWithOAuth(provider: OAuthProvider, options?: OAuthLoginOptions): void;
|
|
65
65
|
loginWithGoogle(options?: OAuthLoginOptions): void;
|
|
66
|
+
loginWithGitHub(options?: OAuthLoginOptions): void;
|
|
66
67
|
linkOAuthAccount(provider: OAuthProvider, options?: OAuthLoginOptions): Promise<void>;
|
|
67
68
|
completeOAuthLogin(): Promise<AuthUser>;
|
|
68
69
|
logout(): Promise<void>;
|
package/dist/client/edge.d.ts
CHANGED
|
@@ -3,6 +3,19 @@ import { S as SessionRecoveryFailure } from '../sessionRecovery-D5Fa0yZ1.js';
|
|
|
3
3
|
export { a as SessionRecoveryErrorDetails, b as SessionRecoveryFailureReason } from '../sessionRecovery-D5Fa0yZ1.js';
|
|
4
4
|
|
|
5
5
|
type ProxySessionMode = 'optimistic' | 'authoritative';
|
|
6
|
+
/**
|
|
7
|
+
* Per-request headers an application proxy needs to expose to the downstream
|
|
8
|
+
* Next.js render. This is primarily useful for request-scoped values such as a
|
|
9
|
+
* Content Security Policy nonce.
|
|
10
|
+
*
|
|
11
|
+
* Values are merged over the incoming request headers. Authentication always
|
|
12
|
+
* evaluates the original request, so these overrides cannot change which
|
|
13
|
+
* principal or cookie Najm authorizes. `authorization` and `cookie` are
|
|
14
|
+
* rejected here; only Najm's validated recovery path may replace the cookie.
|
|
15
|
+
*/
|
|
16
|
+
interface AuthProxyOptions {
|
|
17
|
+
requestHeaders?: HeadersInit;
|
|
18
|
+
}
|
|
6
19
|
interface AuthMiddlewareConfig {
|
|
7
20
|
/** Routes that require authentication (glob patterns) */
|
|
8
21
|
protectedRoutes?: string[];
|
|
@@ -73,6 +86,6 @@ interface AuthMiddlewareConfig {
|
|
|
73
86
|
* };
|
|
74
87
|
* ```
|
|
75
88
|
*/
|
|
76
|
-
declare function withAuthMiddleware(config: AuthMiddlewareConfig): (request: Request) => Promise<next_server.NextResponse<unknown>>;
|
|
89
|
+
declare function withAuthMiddleware(config: AuthMiddlewareConfig): (request: Request, options?: AuthProxyOptions) => Promise<next_server.NextResponse<unknown>>;
|
|
77
90
|
|
|
78
|
-
export { type AuthMiddlewareConfig, type ProxySessionMode, SessionRecoveryFailure, withAuthMiddleware };
|
|
91
|
+
export { type AuthMiddlewareConfig, type AuthProxyOptions, type ProxySessionMode, SessionRecoveryFailure, withAuthMiddleware };
|
package/dist/client/edge.js
CHANGED
|
@@ -19,6 +19,7 @@ function parseSessionCookiePayload(payload, maxAgeSeconds = DEFAULT_SESSION_MAX_
|
|
|
19
19
|
if (!isRecord(data) || !isValidUser(data.user)) return null;
|
|
20
20
|
if (!isStringArray(data.roles) || !isStringArray(data.permissions)) return null;
|
|
21
21
|
if (!Number.isInteger(data.sessionVersion) || data.sessionVersion < 0) return null;
|
|
22
|
+
if (typeof data.tokenFamily !== "string" || !data.tokenFamily) return null;
|
|
22
23
|
if (!Number.isFinite(data.iat) || !Number.isInteger(data.iat) || data.iat <= 0) return null;
|
|
23
24
|
const issuedAt = data.iat;
|
|
24
25
|
if (issuedAt > now + MAX_CLOCK_SKEW_MS) return null;
|
|
@@ -28,6 +29,7 @@ function parseSessionCookiePayload(payload, maxAgeSeconds = DEFAULT_SESSION_MAX_
|
|
|
28
29
|
roles: [...data.roles],
|
|
29
30
|
permissions: [...data.permissions],
|
|
30
31
|
sessionVersion: data.sessionVersion,
|
|
32
|
+
tokenFamily: data.tokenFamily,
|
|
31
33
|
iat: issuedAt
|
|
32
34
|
};
|
|
33
35
|
} catch {
|
|
@@ -353,8 +355,25 @@ function withAuthMiddleware(config) {
|
|
|
353
355
|
} = config;
|
|
354
356
|
const verifyAlways = proxySessionMode === void 0 ? legacyVerifyAlways : proxySessionMode === "authoritative";
|
|
355
357
|
const resolvedInternalRecoveryURL = resolveInternalRecoveryURL(internalRecoveryURL);
|
|
356
|
-
return /* @__PURE__ */ __name(async function middleware(request) {
|
|
358
|
+
return /* @__PURE__ */ __name(async function middleware(request, options = {}) {
|
|
357
359
|
const { NextResponse } = await import("next/server");
|
|
360
|
+
const downstreamHeaders = new Headers(request.headers);
|
|
361
|
+
if (options.requestHeaders) {
|
|
362
|
+
const overrides = new Headers(options.requestHeaders);
|
|
363
|
+
for (const identityHeader of ["authorization", "cookie"]) {
|
|
364
|
+
if (overrides.has(identityHeader)) {
|
|
365
|
+
throw new TypeError(
|
|
366
|
+
`Auth proxy requestHeaders cannot override ${identityHeader}`
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
overrides.forEach((value, key) => {
|
|
371
|
+
downstreamHeaders.set(key, value);
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
const continueRequest = /* @__PURE__ */ __name(() => NextResponse.next({
|
|
375
|
+
request: { headers: downstreamHeaders }
|
|
376
|
+
}), "continueRequest");
|
|
358
377
|
const redirectToLogin = /* @__PURE__ */ __name((returnPath2, clearCookies) => {
|
|
359
378
|
const loginUrl = new URL(loginRoute, request.url);
|
|
360
379
|
loginUrl.searchParams.set("from", returnPath2);
|
|
@@ -371,10 +390,10 @@ function withAuthMiddleware(config) {
|
|
|
371
390
|
const pathname = url.pathname;
|
|
372
391
|
const returnPath = `${url.pathname}${url.search}`;
|
|
373
392
|
if (matchesAny(pathname, publicRoutes)) {
|
|
374
|
-
return
|
|
393
|
+
return continueRequest();
|
|
375
394
|
}
|
|
376
395
|
const isProtected = protectedRoutes.length === 0 || matchesAny(pathname, protectedRoutes);
|
|
377
|
-
if (!isProtected) return
|
|
396
|
+
if (!isProtected) return continueRequest();
|
|
378
397
|
const cookie = request.headers.get("cookie") ?? "";
|
|
379
398
|
const sessionCookie = readCookieValue(cookie, sessionCookieName);
|
|
380
399
|
const secret = resolveSessionSecret(sessionSecret);
|
|
@@ -420,16 +439,15 @@ function withAuthMiddleware(config) {
|
|
|
420
439
|
return forbidden;
|
|
421
440
|
}
|
|
422
441
|
if (recovery?.status === "recovered") {
|
|
423
|
-
|
|
424
|
-
requestHeaders.set(
|
|
442
|
+
downstreamHeaders.set(
|
|
425
443
|
"cookie",
|
|
426
444
|
replaceCookieValue(cookie, sessionCookieName, recovery.sessionCookieValue)
|
|
427
445
|
);
|
|
428
|
-
const response =
|
|
446
|
+
const response = continueRequest();
|
|
429
447
|
response.headers.append("Set-Cookie", recovery.setCookie);
|
|
430
448
|
return response;
|
|
431
449
|
}
|
|
432
|
-
return
|
|
450
|
+
return continueRequest();
|
|
433
451
|
}, "middleware");
|
|
434
452
|
}
|
|
435
453
|
__name(withAuthMiddleware, "withAuthMiddleware");
|
package/dist/client/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export { F as FetchClient, H as HydrateSession, N as NajmAuthClient, c as createAuthClient } from '../NajmAuthClient-
|
|
2
|
-
import { D as DecodedToken, T as TabSyncMessage, S as SyncPayload } from '../types-
|
|
3
|
-
export { b as AuthClientConfig, h as AuthError, f as AuthEvent, g as AuthEventHandler, i as AuthEventMap, e as AuthState, A as AuthUser, j as AuthenticatedLogin, C as CredentialSetupPending, L as LoginCredentials, c as LoginResult, d as OAuthLoginOptions, O as OAuthProvider, a as RequestOptions, R as RetryConfig, k as ServerResponse, l as TokenPair } from '../types-
|
|
1
|
+
export { F as FetchClient, H as HydrateSession, N as NajmAuthClient, c as createAuthClient } from '../NajmAuthClient-BygfBCpF.js';
|
|
2
|
+
import { D as DecodedToken, T as TabSyncMessage, S as SyncPayload } from '../types-CI2t8wpJ.js';
|
|
3
|
+
export { b as AuthClientConfig, h as AuthError, f as AuthEvent, g as AuthEventHandler, i as AuthEventMap, e as AuthState, A as AuthUser, j as AuthenticatedLogin, C as CredentialSetupPending, L as LoginCredentials, c as LoginResult, d as OAuthLoginOptions, O as OAuthProvider, a as RequestOptions, R as RetryConfig, k as ServerResponse, l as TokenPair } from '../types-CI2t8wpJ.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Decode a JWT token payload without verification.
|
package/dist/client/index.js
CHANGED
|
@@ -334,6 +334,9 @@ var NajmAuthClient = class _NajmAuthClient {
|
|
|
334
334
|
loginWithGoogle(options) {
|
|
335
335
|
this.loginWithOAuth("google", options);
|
|
336
336
|
}
|
|
337
|
+
loginWithGitHub(options) {
|
|
338
|
+
this.loginWithOAuth("github", options);
|
|
339
|
+
}
|
|
337
340
|
async linkOAuthAccount(provider, options = {}) {
|
|
338
341
|
const query = options.returnTo ? `?${new URLSearchParams({ returnTo: this.validateReturnTo(options.returnTo) })}` : "";
|
|
339
342
|
const res = await this.api.post(
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { ReactNode, CSSProperties, ReactElement } from 'react';
|
|
3
|
-
import { N as NajmAuthClient, H as HydrateSession } from '../../NajmAuthClient-
|
|
4
|
-
import { e as AuthState, A as AuthUser, c as LoginResult, h as AuthError, L as LoginCredentials, d as OAuthLoginOptions, f as AuthEvent, i as AuthEventMap } from '../../types-
|
|
3
|
+
import { N as NajmAuthClient, H as HydrateSession } from '../../NajmAuthClient-BygfBCpF.js';
|
|
4
|
+
import { e as AuthState, A as AuthUser, c as LoginResult, h as AuthError, L as LoginCredentials, d as OAuthLoginOptions, f as AuthEvent, i as AuthEventMap } from '../../types-CI2t8wpJ.js';
|
|
5
5
|
|
|
6
6
|
interface AuthProviderProps {
|
|
7
7
|
client: NajmAuthClient;
|
|
@@ -177,6 +177,17 @@ interface UseGoogleLoginReturn {
|
|
|
177
177
|
}
|
|
178
178
|
declare function useGoogleLogin(options?: UseGoogleLoginOptions): UseGoogleLoginReturn;
|
|
179
179
|
|
|
180
|
+
interface UseGitHubLoginOptions {
|
|
181
|
+
onError?: (error: AuthError | Error) => void;
|
|
182
|
+
}
|
|
183
|
+
interface UseGitHubLoginReturn {
|
|
184
|
+
loginWithGitHub: (options?: OAuthLoginOptions) => void;
|
|
185
|
+
linkGitHub: (options?: OAuthLoginOptions) => Promise<void>;
|
|
186
|
+
isRedirecting: boolean;
|
|
187
|
+
error: AuthError | Error | null;
|
|
188
|
+
}
|
|
189
|
+
declare function useGitHubLogin(options?: UseGitHubLoginOptions): UseGitHubLoginReturn;
|
|
190
|
+
|
|
180
191
|
interface UseOAuthCallbackOptions {
|
|
181
192
|
onSuccess?: (user: AuthUser) => void;
|
|
182
193
|
onError?: (error: AuthError | Error) => void;
|
|
@@ -571,6 +582,15 @@ declare function GoogleLoginButton({ children, returnTo, onError }: GoogleLoginB
|
|
|
571
582
|
disabled?: boolean;
|
|
572
583
|
}, string | react.JSXElementConstructor<any>>;
|
|
573
584
|
|
|
585
|
+
interface GitHubLoginButtonProps extends OAuthLoginOptions {
|
|
586
|
+
children: ReactNode;
|
|
587
|
+
onError?: (error: Error) => void;
|
|
588
|
+
}
|
|
589
|
+
declare function GitHubLoginButton({ children, returnTo, onError }: GitHubLoginButtonProps): ReactElement<{
|
|
590
|
+
onClick?: (event: unknown) => void;
|
|
591
|
+
disabled?: boolean;
|
|
592
|
+
}, string | react.JSXElementConstructor<any>>;
|
|
593
|
+
|
|
574
594
|
interface OAuthCallbackProps {
|
|
575
595
|
fallback?: ReactNode;
|
|
576
596
|
errorFallback?: ReactNode | ((props: {
|
|
@@ -580,4 +600,4 @@ interface OAuthCallbackProps {
|
|
|
580
600
|
}
|
|
581
601
|
declare function OAuthCallback({ fallback, errorFallback, defaultRedirect, }: OAuthCallbackProps): react.JSX.Element;
|
|
582
602
|
|
|
583
|
-
export { AuthBoundary, type AuthEventEntry, AuthGate, AuthLoading, AuthProvider, Can, GoogleLoginButton, IfAuth, LoginButton, OAuthCallback, PermissionList, Protected, RedirectToLogin, Role, type SessionStatus, SignOutButton, SignedIn, SignedOut, UserAvatar, UserEmail, UserName, UserRole, useAuth, useAuthClient, useAuthEvent, useAuthEvents, useChangePassword, useForgotPassword, useGoogleLogin, useLogin, useLogout, useOAuthCallback, usePermissions, useRegister, useResetPassword, useSession, useUser };
|
|
603
|
+
export { AuthBoundary, type AuthEventEntry, AuthGate, AuthLoading, AuthProvider, Can, GitHubLoginButton, GoogleLoginButton, IfAuth, LoginButton, OAuthCallback, PermissionList, Protected, RedirectToLogin, Role, type SessionStatus, SignOutButton, SignedIn, SignedOut, UserAvatar, UserEmail, UserName, UserRole, useAuth, useAuthClient, useAuthEvent, useAuthEvents, useChangePassword, useForgotPassword, useGitHubLogin, useGoogleLogin, useLogin, useLogout, useOAuthCallback, usePermissions, useRegister, useResetPassword, useSession, useUser };
|
|
@@ -354,14 +354,48 @@ function useGoogleLogin(options) {
|
|
|
354
354
|
}
|
|
355
355
|
__name(useGoogleLogin, "useGoogleLogin");
|
|
356
356
|
|
|
357
|
+
// src/client/react/useGitHubLogin.ts
|
|
358
|
+
import { useCallback as useCallback8, useState as useState10 } from "react";
|
|
359
|
+
function useGitHubLogin(options) {
|
|
360
|
+
const client = useAuthClient();
|
|
361
|
+
const [isRedirecting, setIsRedirecting] = useState10(false);
|
|
362
|
+
const [error, setError] = useState10(null);
|
|
363
|
+
const fail = useCallback8((value) => {
|
|
364
|
+
const next = value instanceof Error ? value : new Error(String(value));
|
|
365
|
+
setIsRedirecting(false);
|
|
366
|
+
setError(next);
|
|
367
|
+
options?.onError?.(next);
|
|
368
|
+
}, [options?.onError]);
|
|
369
|
+
const loginWithGitHub = useCallback8((loginOptions) => {
|
|
370
|
+
setError(null);
|
|
371
|
+
setIsRedirecting(true);
|
|
372
|
+
try {
|
|
373
|
+
client.loginWithGitHub(loginOptions);
|
|
374
|
+
} catch (value) {
|
|
375
|
+
fail(value);
|
|
376
|
+
}
|
|
377
|
+
}, [client, fail]);
|
|
378
|
+
const linkGitHub = useCallback8(async (loginOptions) => {
|
|
379
|
+
setError(null);
|
|
380
|
+
setIsRedirecting(true);
|
|
381
|
+
try {
|
|
382
|
+
await client.linkOAuthAccount("github", loginOptions);
|
|
383
|
+
} catch (value) {
|
|
384
|
+
fail(value);
|
|
385
|
+
}
|
|
386
|
+
}, [client, fail]);
|
|
387
|
+
return { loginWithGitHub, linkGitHub, isRedirecting, error };
|
|
388
|
+
}
|
|
389
|
+
__name(useGitHubLogin, "useGitHubLogin");
|
|
390
|
+
|
|
357
391
|
// src/client/react/useOAuthCallback.ts
|
|
358
|
-
import { useCallback as
|
|
392
|
+
import { useCallback as useCallback9, useRef as useRef3, useState as useState11 } from "react";
|
|
359
393
|
function useOAuthCallback(options) {
|
|
360
394
|
const client = useAuthClient();
|
|
361
395
|
const promise = useRef3(null);
|
|
362
|
-
const [isLoading, setIsLoading] =
|
|
363
|
-
const [error, setError] =
|
|
364
|
-
const complete =
|
|
396
|
+
const [isLoading, setIsLoading] = useState11(false);
|
|
397
|
+
const [error, setError] = useState11(null);
|
|
398
|
+
const complete = useCallback9(async () => {
|
|
365
399
|
if (promise.current) return promise.current;
|
|
366
400
|
setIsLoading(true);
|
|
367
401
|
setError(null);
|
|
@@ -398,7 +432,7 @@ function useAuthEvent(event, handler) {
|
|
|
398
432
|
__name(useAuthEvent, "useAuthEvent");
|
|
399
433
|
|
|
400
434
|
// src/client/react/useAuthEvents.ts
|
|
401
|
-
import { useEffect as useEffect4, useRef as useRef5, useState as
|
|
435
|
+
import { useEffect as useEffect4, useRef as useRef5, useState as useState12 } from "react";
|
|
402
436
|
var ALL_EVENTS = [
|
|
403
437
|
"login",
|
|
404
438
|
"logout",
|
|
@@ -411,7 +445,7 @@ var ALL_EVENTS = [
|
|
|
411
445
|
function useAuthEvents(opts) {
|
|
412
446
|
const client = useAuthClient();
|
|
413
447
|
const max = opts?.maxEntries ?? 100;
|
|
414
|
-
const [entries, setEntries] =
|
|
448
|
+
const [entries, setEntries] = useState12([]);
|
|
415
449
|
const maxRef = useRef5(max);
|
|
416
450
|
maxRef.current = max;
|
|
417
451
|
useEffect4(() => {
|
|
@@ -714,6 +748,27 @@ function GoogleLoginButton({ children, returnTo, onError }) {
|
|
|
714
748
|
}
|
|
715
749
|
__name(GoogleLoginButton, "GoogleLoginButton");
|
|
716
750
|
|
|
751
|
+
// src/client/react/GitHubLoginButton.tsx
|
|
752
|
+
import { Children as Children4, cloneElement as cloneElement4, isValidElement as isValidElement4 } from "react";
|
|
753
|
+
function GitHubLoginButton({ children, returnTo, onError }) {
|
|
754
|
+
const { loginWithGitHub, isRedirecting } = useGitHubLogin({ onError });
|
|
755
|
+
const child = Children4.only(children);
|
|
756
|
+
if (!isValidElement4(child)) {
|
|
757
|
+
throw new Error("<GitHubLoginButton> expects a single React element as its child");
|
|
758
|
+
}
|
|
759
|
+
const element = child;
|
|
760
|
+
return cloneElement4(element, {
|
|
761
|
+
onClick: /* @__PURE__ */ __name((event) => {
|
|
762
|
+
element.props.onClick?.(event);
|
|
763
|
+
if (!event?.defaultPrevented) {
|
|
764
|
+
loginWithGitHub({ returnTo });
|
|
765
|
+
}
|
|
766
|
+
}, "onClick"),
|
|
767
|
+
disabled: Boolean(element.props.disabled || isRedirecting)
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
__name(GitHubLoginButton, "GitHubLoginButton");
|
|
771
|
+
|
|
717
772
|
// src/client/react/OAuthCallback.tsx
|
|
718
773
|
import { useEffect as useEffect7 } from "react";
|
|
719
774
|
import { Fragment as Fragment14, jsx as jsx16 } from "react/jsx-runtime";
|
|
@@ -755,6 +810,7 @@ export {
|
|
|
755
810
|
AuthLoading,
|
|
756
811
|
AuthProvider,
|
|
757
812
|
Can,
|
|
813
|
+
GitHubLoginButton,
|
|
758
814
|
GoogleLoginButton,
|
|
759
815
|
IfAuth,
|
|
760
816
|
LoginButton,
|
|
@@ -776,6 +832,7 @@ export {
|
|
|
776
832
|
useAuthEvents,
|
|
777
833
|
useChangePassword,
|
|
778
834
|
useForgotPassword,
|
|
835
|
+
useGitHubLogin,
|
|
779
836
|
useGoogleLogin,
|
|
780
837
|
useLogin,
|
|
781
838
|
useLogout,
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { A as AuthUser, R as RetryConfig } from '../../types-
|
|
2
|
-
import { F as FetchClient, N as NajmAuthClient } from '../../NajmAuthClient-
|
|
3
|
-
import { ProxySessionMode } from '../edge.js';
|
|
1
|
+
import { A as AuthUser, R as RetryConfig } from '../../types-CI2t8wpJ.js';
|
|
2
|
+
import { F as FetchClient, N as NajmAuthClient } from '../../NajmAuthClient-BygfBCpF.js';
|
|
3
|
+
import { AuthProxyOptions, ProxySessionMode } from '../edge.js';
|
|
4
4
|
export { withAuthMiddleware } from '../edge.js';
|
|
5
|
-
import { G as GetSessionConfig, S as ServerSession$1 } from '../../getSession-
|
|
6
|
-
export { A as AuthConfigError, a as AuthTransportError, N as NoSessionError, g as getSession } from '../../getSession-
|
|
5
|
+
import { G as GetSessionConfig, S as ServerSession$1 } from '../../getSession-DBX8XpsR.js';
|
|
6
|
+
export { A as AuthConfigError, a as AuthTransportError, N as NoSessionError, g as getSession } from '../../getSession-DBX8XpsR.js';
|
|
7
7
|
import { S as SessionRecoveryFailure } from '../../sessionRecovery-D5Fa0yZ1.js';
|
|
8
8
|
export { a as SessionRecoveryErrorDetails, b as SessionRecoveryFailureReason } from '../../sessionRecovery-D5Fa0yZ1.js';
|
|
9
9
|
import 'next/server';
|
|
@@ -244,9 +244,9 @@ interface AuthKit {
|
|
|
244
244
|
*/
|
|
245
245
|
requireRole: (roles: string[]) => Promise<ServerSession$1>;
|
|
246
246
|
/** Generated Next.js 16 Proxy function. */
|
|
247
|
-
proxy: (request: Request) => Promise<Response>;
|
|
247
|
+
proxy: (request: Request, options?: AuthProxyOptions) => Promise<Response>;
|
|
248
248
|
/** @deprecated Next.js 16 renamed Middleware to Proxy. Use `proxy`. */
|
|
249
|
-
middleware: (request: Request) => Promise<Response>;
|
|
249
|
+
middleware: (request: Request, options?: AuthProxyOptions) => Promise<Response>;
|
|
250
250
|
/** @deprecated Next.js 16 requires a static config literal in `proxy.ts`. */
|
|
251
251
|
config: {
|
|
252
252
|
matcher: string[];
|
|
@@ -298,4 +298,4 @@ interface SafeRedirectOptions {
|
|
|
298
298
|
*/
|
|
299
299
|
declare function getSafeRedirectPath(value: string | string[] | undefined | null, options?: SafeRedirectOptions | string): string;
|
|
300
300
|
|
|
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 };
|
|
301
|
+
export { type AuthCookiePersistenceOptions, type AuthKit, AuthProxyOptions, 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 };
|
|
@@ -22,6 +22,7 @@ function parseSessionCookiePayload(payload, maxAgeSeconds = DEFAULT_SESSION_MAX_
|
|
|
22
22
|
if (!isRecord(data) || !isValidUser(data.user)) return null;
|
|
23
23
|
if (!isStringArray(data.roles) || !isStringArray(data.permissions)) return null;
|
|
24
24
|
if (!Number.isInteger(data.sessionVersion) || data.sessionVersion < 0) return null;
|
|
25
|
+
if (typeof data.tokenFamily !== "string" || !data.tokenFamily) return null;
|
|
25
26
|
if (!Number.isFinite(data.iat) || !Number.isInteger(data.iat) || data.iat <= 0) return null;
|
|
26
27
|
const issuedAt = data.iat;
|
|
27
28
|
if (issuedAt > now + MAX_CLOCK_SKEW_MS) return null;
|
|
@@ -31,6 +32,7 @@ function parseSessionCookiePayload(payload, maxAgeSeconds = DEFAULT_SESSION_MAX_
|
|
|
31
32
|
roles: [...data.roles],
|
|
32
33
|
permissions: [...data.permissions],
|
|
33
34
|
sessionVersion: data.sessionVersion,
|
|
35
|
+
tokenFamily: data.tokenFamily,
|
|
34
36
|
iat: issuedAt
|
|
35
37
|
};
|
|
36
38
|
} catch {
|
|
@@ -764,8 +766,25 @@ function withAuthMiddleware(config) {
|
|
|
764
766
|
} = config;
|
|
765
767
|
const verifyAlways = proxySessionMode === void 0 ? legacyVerifyAlways : proxySessionMode === "authoritative";
|
|
766
768
|
const resolvedInternalRecoveryURL = resolveInternalRecoveryURL(internalRecoveryURL);
|
|
767
|
-
return /* @__PURE__ */ __name(async function middleware(request) {
|
|
769
|
+
return /* @__PURE__ */ __name(async function middleware(request, options = {}) {
|
|
768
770
|
const { NextResponse } = await import("next/server");
|
|
771
|
+
const downstreamHeaders = new Headers(request.headers);
|
|
772
|
+
if (options.requestHeaders) {
|
|
773
|
+
const overrides = new Headers(options.requestHeaders);
|
|
774
|
+
for (const identityHeader of ["authorization", "cookie"]) {
|
|
775
|
+
if (overrides.has(identityHeader)) {
|
|
776
|
+
throw new TypeError(
|
|
777
|
+
`Auth proxy requestHeaders cannot override ${identityHeader}`
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
overrides.forEach((value, key) => {
|
|
782
|
+
downstreamHeaders.set(key, value);
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
const continueRequest = /* @__PURE__ */ __name(() => NextResponse.next({
|
|
786
|
+
request: { headers: downstreamHeaders }
|
|
787
|
+
}), "continueRequest");
|
|
769
788
|
const redirectToLogin = /* @__PURE__ */ __name((returnPath2, clearCookies) => {
|
|
770
789
|
const loginUrl = new URL(loginRoute, request.url);
|
|
771
790
|
loginUrl.searchParams.set("from", returnPath2);
|
|
@@ -782,10 +801,10 @@ function withAuthMiddleware(config) {
|
|
|
782
801
|
const pathname = url.pathname;
|
|
783
802
|
const returnPath = `${url.pathname}${url.search}`;
|
|
784
803
|
if (matchesAny(pathname, publicRoutes)) {
|
|
785
|
-
return
|
|
804
|
+
return continueRequest();
|
|
786
805
|
}
|
|
787
806
|
const isProtected = protectedRoutes.length === 0 || matchesAny(pathname, protectedRoutes);
|
|
788
|
-
if (!isProtected) return
|
|
807
|
+
if (!isProtected) return continueRequest();
|
|
789
808
|
const cookie = request.headers.get("cookie") ?? "";
|
|
790
809
|
const sessionCookie = readCookieValue(cookie, sessionCookieName);
|
|
791
810
|
const secret = resolveSessionSecret(sessionSecret);
|
|
@@ -831,16 +850,15 @@ function withAuthMiddleware(config) {
|
|
|
831
850
|
return forbidden;
|
|
832
851
|
}
|
|
833
852
|
if (recovery?.status === "recovered") {
|
|
834
|
-
|
|
835
|
-
requestHeaders.set(
|
|
853
|
+
downstreamHeaders.set(
|
|
836
854
|
"cookie",
|
|
837
855
|
replaceCookieValue(cookie, sessionCookieName, recovery.sessionCookieValue)
|
|
838
856
|
);
|
|
839
|
-
const response =
|
|
857
|
+
const response = continueRequest();
|
|
840
858
|
response.headers.append("Set-Cookie", recovery.setCookie);
|
|
841
859
|
return response;
|
|
842
860
|
}
|
|
843
|
-
return
|
|
861
|
+
return continueRequest();
|
|
844
862
|
}, "middleware");
|
|
845
863
|
}
|
|
846
864
|
__name(withAuthMiddleware, "withAuthMiddleware");
|
|
@@ -1045,6 +1063,9 @@ var NajmAuthClient = class _NajmAuthClient {
|
|
|
1045
1063
|
loginWithGoogle(options) {
|
|
1046
1064
|
this.loginWithOAuth("google", options);
|
|
1047
1065
|
}
|
|
1066
|
+
loginWithGitHub(options) {
|
|
1067
|
+
this.loginWithOAuth("github", options);
|
|
1068
|
+
}
|
|
1048
1069
|
async linkOAuthAccount(provider, options = {}) {
|
|
1049
1070
|
const query = options.returnTo ? `?${new URLSearchParams({ returnTo: this.validateReturnTo(options.returnTo) })}` : "";
|
|
1050
1071
|
const res = await this.api.post(
|