@spfn/auth 0.3.0-beta.23 → 0.3.0-beta.24
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 +165 -2
- package/dist/client-proof.js +13 -5
- package/dist/client-proof.js.map +1 -1
- package/dist/client.d.ts +52 -1
- package/dist/client.js +40 -0
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +82 -0
- package/dist/config.js +37 -0
- package/dist/config.js.map +1 -1
- package/dist/crypto.d.ts +1 -1
- package/dist/errors.d.ts +113 -3
- package/dist/errors.js +68 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +38 -5
- package/dist/index.js +73 -2
- package/dist/index.js.map +1 -1
- package/dist/{machine-principals-CaEFq61K.d.ts → machine-principals-ZJd9anVT.d.ts} +1667 -900
- package/dist/nextjs/api.js +156 -6
- package/dist/nextjs/api.js.map +1 -1
- package/dist/nextjs/server.d.ts +42 -24
- package/dist/nextjs/server.js +72 -5
- package/dist/nextjs/server.js.map +1 -1
- package/dist/server.d.ts +448 -525
- package/dist/server.js +1389 -653
- package/dist/server.js.map +1 -1
- package/dist/{session-Dfwu5g2W.d.ts → session-BbhAGZtA.d.ts} +57 -1
- package/dist/{types-DYyhze28.d.ts → types-CTdoTOxM.d.ts} +24 -1
- package/migrations/20260918184037_happy_mordo/migration.sql +4 -0
- package/migrations/20260918184037_happy_mordo/snapshot.json +6000 -0
- package/migrations/20260918184152_dear_rictor/migration.sql +3 -0
- package/migrations/20260918184152_dear_rictor/snapshot.json +6039 -0
- package/package.json +1 -1
package/dist/nextjs/server.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import { ReactNode } from 'react';
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
import { authApi } from '@spfn/auth';
|
|
4
|
+
import { S as SessionData } from '../session-BbhAGZtA.js';
|
|
5
|
+
export { i as isSafeReturnPath } from '../session-BbhAGZtA.js';
|
|
6
|
+
import { K as KeyAlgorithmType } from '../types-CTdoTOxM.js';
|
|
6
7
|
import { NextResponse, NextRequest } from 'next/server';
|
|
7
8
|
|
|
8
9
|
interface RequireAuthProps {
|
|
@@ -19,6 +20,17 @@ interface RequireAuthProps {
|
|
|
19
20
|
* Fallback UI to show instead of redirecting
|
|
20
21
|
*/
|
|
21
22
|
fallback?: ReactNode;
|
|
23
|
+
/**
|
|
24
|
+
* Path to send a bound session whose key has run out (#97)
|
|
25
|
+
*
|
|
26
|
+
* Not the sign-in page: the person is still signed in and one WebAuthn
|
|
27
|
+
* ceremony puts a live key back in the cookie. The page this names is the
|
|
28
|
+
* app's own, and all it has to do is render a client component that calls
|
|
29
|
+
* `renewSession(api)` and returns them to where they were.
|
|
30
|
+
*
|
|
31
|
+
* @default env SPFN_AUTH_SESSION_RENEW_PATH, or '/auth/renew'
|
|
32
|
+
*/
|
|
33
|
+
renewalPath?: string;
|
|
22
34
|
}
|
|
23
35
|
/**
|
|
24
36
|
* Require Authentication Guard
|
|
@@ -38,8 +50,17 @@ interface RequireAuthProps {
|
|
|
38
50
|
* <PrivateContent />
|
|
39
51
|
* </RequireAuth>
|
|
40
52
|
* ```
|
|
53
|
+
*
|
|
54
|
+
* @example A bound session whose key ran out
|
|
55
|
+
* ```tsx
|
|
56
|
+
* // Sent to /account/renew instead of the sign-in page. That page renders a
|
|
57
|
+
* // client component calling renewSession(api).
|
|
58
|
+
* <RequireAuth renewalPath="/account/renew">
|
|
59
|
+
* <DashboardContent />
|
|
60
|
+
* </RequireAuth>
|
|
61
|
+
* ```
|
|
41
62
|
*/
|
|
42
|
-
declare function RequireAuth({ children, redirectTo, fallback, }: RequireAuthProps): Promise<react_jsx_runtime.JSX.Element>;
|
|
63
|
+
declare function RequireAuth({ children, redirectTo, renewalPath, fallback, }: RequireAuthProps): Promise<react_jsx_runtime.JSX.Element>;
|
|
43
64
|
|
|
44
65
|
interface RequireRoleProps {
|
|
45
66
|
/**
|
|
@@ -140,29 +161,26 @@ declare function RequirePermission({ permissions, children, redirectTo, fallback
|
|
|
140
161
|
*
|
|
141
162
|
* Uses authApi to check permissions in real-time
|
|
142
163
|
*/
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* A bound session whose key has run out, seen from a server component.
|
|
167
|
+
*
|
|
168
|
+
* The third state `getAuthSessionData` answers with, and the reason it is a
|
|
169
|
+
* sentinel rather than `null`: a server component's `api.` call goes through the
|
|
170
|
+
* same proxy a browser's does, so it meets the same renewal-required refusal —
|
|
171
|
+
* and `null` there would read as "not signed in" and send the person to the
|
|
172
|
+
* sign-in page, which is precisely the thing renewal exists to avoid. A server
|
|
173
|
+
* component cannot run a WebAuthn ceremony, so the guard hands the work to a page
|
|
174
|
+
* that can.
|
|
175
|
+
*/
|
|
176
|
+
declare const RENEWAL_REQUIRED = "renewal-required";
|
|
177
|
+
type AuthSessionData = Awaited<ReturnType<typeof authApi.getAuthSession.call>>;
|
|
178
|
+
/** What a guard gets back: the session, the renewal sentinel, or nothing. */
|
|
179
|
+
type AuthSessionState = AuthSessionData | typeof RENEWAL_REQUIRED | null;
|
|
143
180
|
/**
|
|
144
181
|
* Get current auth session with roles and permissions via API
|
|
145
182
|
*/
|
|
146
|
-
declare function getAuthSessionData(): Promise<
|
|
147
|
-
role: {
|
|
148
|
-
id: number;
|
|
149
|
-
name: string;
|
|
150
|
-
displayName: string;
|
|
151
|
-
priority: number;
|
|
152
|
-
};
|
|
153
|
-
permissions: {
|
|
154
|
-
id: number;
|
|
155
|
-
name: string;
|
|
156
|
-
displayName: string;
|
|
157
|
-
category: "auth" | "custom" | "user" | "rbac" | "system" | undefined;
|
|
158
|
-
}[];
|
|
159
|
-
userId: number;
|
|
160
|
-
publicId: string;
|
|
161
|
-
email: string | null;
|
|
162
|
-
emailVerified: boolean;
|
|
163
|
-
phoneVerified: boolean;
|
|
164
|
-
hasPassword: boolean;
|
|
165
|
-
} | null>;
|
|
183
|
+
declare function getAuthSessionData(): Promise<AuthSessionState>;
|
|
166
184
|
/**
|
|
167
185
|
* Get user role
|
|
168
186
|
*/
|
package/dist/nextjs/server.js
CHANGED
|
@@ -215,6 +215,10 @@ function getSessionTtl(override) {
|
|
|
215
215
|
}
|
|
216
216
|
return 7 * 24 * 60 * 60;
|
|
217
217
|
}
|
|
218
|
+
function getSessionRenewPath() {
|
|
219
|
+
return env3.SPFN_AUTH_SESSION_RENEW_PATH?.trim() || DEFAULT_SESSION_RENEW_PATH;
|
|
220
|
+
}
|
|
221
|
+
var DEFAULT_SESSION_RENEW_PATH = "/auth/renew";
|
|
218
222
|
|
|
219
223
|
// src/nextjs/session-helpers.ts
|
|
220
224
|
import { env as env4 } from "@spfn/auth/config";
|
|
@@ -309,36 +313,48 @@ async function clearPendingSession() {
|
|
|
309
313
|
|
|
310
314
|
// src/nextjs/guards/auth-utils.ts
|
|
311
315
|
import { authApi } from "@spfn/auth";
|
|
316
|
+
import { SessionRenewalRequiredError } from "@spfn/auth/errors";
|
|
317
|
+
var RENEWAL_REQUIRED = "renewal-required";
|
|
312
318
|
async function getAuthSessionData() {
|
|
313
319
|
try {
|
|
314
320
|
const session = await authApi.getAuthSession.call();
|
|
315
321
|
authLogger.middleware.debug("Auth session retrieved", { name: session.role?.name });
|
|
316
322
|
return session;
|
|
317
323
|
} catch (error) {
|
|
324
|
+
if (isRenewalRequired(error)) {
|
|
325
|
+
authLogger.middleware.debug("Auth session needs renewing");
|
|
326
|
+
return RENEWAL_REQUIRED;
|
|
327
|
+
}
|
|
318
328
|
authLogger.middleware.error("Failed to get auth session", { error });
|
|
319
329
|
return null;
|
|
320
330
|
}
|
|
321
331
|
}
|
|
332
|
+
function isRenewalRequired(error) {
|
|
333
|
+
return error instanceof SessionRenewalRequiredError || error?.name === "SessionRenewalRequiredError";
|
|
334
|
+
}
|
|
335
|
+
function resolvedSession(state) {
|
|
336
|
+
return state && state !== RENEWAL_REQUIRED ? state : null;
|
|
337
|
+
}
|
|
322
338
|
async function getUserRole() {
|
|
323
|
-
const session = await getAuthSessionData();
|
|
339
|
+
const session = resolvedSession(await getAuthSessionData());
|
|
324
340
|
return session?.role?.name || null;
|
|
325
341
|
}
|
|
326
342
|
async function getUserPermissions() {
|
|
327
|
-
const session = await getAuthSessionData();
|
|
343
|
+
const session = resolvedSession(await getAuthSessionData());
|
|
328
344
|
if (!session) {
|
|
329
345
|
return [];
|
|
330
346
|
}
|
|
331
347
|
return session.permissions?.map((p) => p.name) || [];
|
|
332
348
|
}
|
|
333
349
|
async function hasAnyRole(requiredRoles) {
|
|
334
|
-
const session = await getAuthSessionData();
|
|
350
|
+
const session = resolvedSession(await getAuthSessionData());
|
|
335
351
|
if (!session) {
|
|
336
352
|
return false;
|
|
337
353
|
}
|
|
338
354
|
return requiredRoles.includes(session.role?.name);
|
|
339
355
|
}
|
|
340
356
|
async function hasAnyPermission(requiredPermissions) {
|
|
341
|
-
const session = await getAuthSessionData();
|
|
357
|
+
const session = resolvedSession(await getAuthSessionData());
|
|
342
358
|
if (!session) {
|
|
343
359
|
return false;
|
|
344
360
|
}
|
|
@@ -351,6 +367,7 @@ import { Fragment, jsx } from "react/jsx-runtime";
|
|
|
351
367
|
async function RequireAuth({
|
|
352
368
|
children,
|
|
353
369
|
redirectTo = "/auth/login",
|
|
370
|
+
renewalPath,
|
|
354
371
|
fallback
|
|
355
372
|
}) {
|
|
356
373
|
const session = await getSession();
|
|
@@ -361,6 +378,9 @@ async function RequireAuth({
|
|
|
361
378
|
redirect(redirectTo);
|
|
362
379
|
}
|
|
363
380
|
const serverSession = await getAuthSessionData();
|
|
381
|
+
if (serverSession === RENEWAL_REQUIRED) {
|
|
382
|
+
redirect(renewalPath ?? getSessionRenewPath());
|
|
383
|
+
}
|
|
364
384
|
if (!serverSession) {
|
|
365
385
|
redirect(redirectTo);
|
|
366
386
|
}
|
|
@@ -458,10 +478,56 @@ function isSafeReturnPath(returnPath) {
|
|
|
458
478
|
return !/^\/[^/?#]*:/.test(returnPath);
|
|
459
479
|
}
|
|
460
480
|
|
|
481
|
+
// src/nextjs/interceptors/session-binding.ts
|
|
482
|
+
import { SessionResealFailedError } from "@spfn/auth/errors";
|
|
483
|
+
|
|
484
|
+
// src/server/lib/ua-family.ts
|
|
485
|
+
var FAMILY_MARKERS = [
|
|
486
|
+
{ family: "edge", marker: /\bEdg(?:A|iOS)?\// },
|
|
487
|
+
{ family: "chrome", marker: /\b(?:Chrome|CriOS)\// },
|
|
488
|
+
{ family: "firefox", marker: /\b(?:Firefox|FxiOS)\// },
|
|
489
|
+
{ family: "safari", marker: /\bSafari\// }
|
|
490
|
+
];
|
|
491
|
+
function uaFamily(userAgent) {
|
|
492
|
+
if (!userAgent) {
|
|
493
|
+
return "other";
|
|
494
|
+
}
|
|
495
|
+
return FAMILY_MARKERS.find((entry) => entry.marker.test(userAgent))?.family ?? "other";
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// src/nextjs/interceptors/cookie-options.ts
|
|
499
|
+
function resolveSecure() {
|
|
500
|
+
const override = process.env.SPFN_AUTH_COOKIE_SECURE;
|
|
501
|
+
if (override !== void 0) {
|
|
502
|
+
return override === "true";
|
|
503
|
+
}
|
|
504
|
+
return process.env.NODE_ENV === "production";
|
|
505
|
+
}
|
|
506
|
+
var cookieSecure = resolveSecure();
|
|
507
|
+
|
|
508
|
+
// src/nextjs/interceptors/session-binding.ts
|
|
509
|
+
function bindingSessionFields(body, userAgent) {
|
|
510
|
+
if (body?.sessionBinding !== "passkey" || typeof body.keyExpiresAtMillis !== "number") {
|
|
511
|
+
return {};
|
|
512
|
+
}
|
|
513
|
+
return {
|
|
514
|
+
binding: "passkey",
|
|
515
|
+
keyExpiresAt: body.keyExpiresAtMillis,
|
|
516
|
+
...userAgent ? { uaFamily: uaFamily(userAgent) } : {}
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
|
|
461
520
|
// src/nextjs/oauth-handlers.ts
|
|
462
521
|
function safeReturnUrl(requested, defaultRedirect) {
|
|
463
522
|
return requested && isSafeReturnPath(requested) ? requested : defaultRedirect;
|
|
464
523
|
}
|
|
524
|
+
function bindingFromQuery(searchParams) {
|
|
525
|
+
const expiresAt = Number(searchParams.get("keyExpiresAtMillis"));
|
|
526
|
+
if (searchParams.get("sessionBinding") !== "passkey" || !Number.isFinite(expiresAt)) {
|
|
527
|
+
return {};
|
|
528
|
+
}
|
|
529
|
+
return { sessionBinding: "passkey", keyExpiresAtMillis: expiresAt };
|
|
530
|
+
}
|
|
465
531
|
function createOAuthCallbackHandler(options) {
|
|
466
532
|
const defaultRedirect = options?.defaultRedirectUrl || "/";
|
|
467
533
|
const errorRedirect = options?.errorRedirectUrl || "/auth/error";
|
|
@@ -497,7 +563,8 @@ function createOAuthCallbackHandler(options) {
|
|
|
497
563
|
userId,
|
|
498
564
|
privateKey: pendingSession.privateKey,
|
|
499
565
|
keyId: pendingSession.keyId,
|
|
500
|
-
algorithm: pendingSession.algorithm
|
|
566
|
+
algorithm: pendingSession.algorithm,
|
|
567
|
+
...bindingSessionFields(bindingFromQuery(searchParams), request.headers.get("user-agent"))
|
|
501
568
|
}, ttl);
|
|
502
569
|
const redirectUrl = new URL(returnUrl, request.url);
|
|
503
570
|
const response = NextResponse.redirect(redirectUrl);
|