@nifrajs/authjs 0.0.0-bootstrap.0 → 4.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nifra Authors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,36 @@
1
+ # `@nifrajs/authjs`
2
+
3
+ Official [Auth.js](https://authjs.dev) integration for nifra - backend mount, session helpers,
4
+ route guards, a framework-agnostic client, and React bindings.
5
+
6
+ ```ts
7
+ import { authjs, getSession, requireAuthUser } from "@nifrajs/authjs"
8
+ import GitHub from "@auth/core/providers/github"
9
+
10
+ const authConfig = {
11
+ providers: [GitHub({ clientId: process.env.GITHUB_ID! })],
12
+ secret: process.env.AUTH_SECRET!,
13
+ trustHost: true,
14
+ }
15
+
16
+ export const app = server()
17
+ .use(authjs(authConfig)) // serves /api/auth/* (sign-in, callbacks, session, sign-out)
18
+ .get("/me", async (c) => ({ user: (await getSession(c.req, authConfig))?.user ?? null }))
19
+ ```
20
+
21
+ - `authjs(config, options?)` - identity plugin mounting Auth.js at `/api/auth/*` (GET + POST).
22
+ Secrets resolve per request (explicit → platform binding → `process.env`); missing fails loud.
23
+ - `getSession(req, config, options?)` - `Session | null`, usable in handlers and loaders. Pass the
24
+ mount's `authUrl`, `trustProxy` and `basePath` so the session is read where the mount serves it.
25
+ - `requireAuthUser(req, config, { redirectTo? })` - returns the user or throws a 401/redirect.
26
+ - `@nifrajs/authjs/client` - `createAuthClient()` (session, sign-in, sign-out) for any frontend.
27
+ - `@nifrajs/web-react/auth` - `<AuthSessionProvider>` + `useAuthSession()`.
28
+
29
+ Auth.js itself does the security-critical work (PKCE, state, token verification); this package
30
+ is the nifra-shaped surface over `@auth/core`. See `/docs/auth` (Auth.js section) for the full
31
+ guide, including proxies (`authUrl`) and edge secret bindings.
32
+
33
+ ## For AI agents
34
+
35
+ Start with [`LLM.md`](./LLM.md) - this package's contract card. For the wider framework, see the
36
+ repository [`llms-full.txt`](../../llms-full.txt) corpus.
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Framework-agnostic Auth.js client - `fetch` against the mounted Auth.js endpoints, no DOM
3
+ * framework involved. Every UI adapter builds on this: pass it a base path (default
4
+ * `/api/auth`, matching the backend mount default) and an optional `fetch` override for tests
5
+ * or loaders that must forward headers.
6
+ *
7
+ * ```ts
8
+ * import { createAuthClient } from "@nifrajs/authjs/client"
9
+ *
10
+ * const auth = createAuthClient()
11
+ * await auth.signIn("github") // posts sign-in, then navigates to the provider (browser only)
12
+ * await auth.signOut() // posts signout, then navigates home
13
+ * const session = await auth.getSession() // works anywhere fetch does
14
+ * ```
15
+ */
16
+ import type { Session } from "@auth/core/types";
17
+ export type { Session } from "@auth/core/types";
18
+ export interface AuthClientOptions {
19
+ /** Auth.js mount path. Default `"/api/auth"`. */
20
+ readonly basePath?: string;
21
+ /** `fetch` implementation. Default global `fetch` - override in tests or to forward headers. */
22
+ readonly fetch?: typeof fetch;
23
+ }
24
+ export interface SignInOptions {
25
+ /** Where the provider returns afterwards. Default the current page. */
26
+ readonly callbackUrl?: string;
27
+ }
28
+ export interface SignOutOptions {
29
+ /** Where to land after sign-out. Default `"/"`. Set `redirect: false` to stay put. */
30
+ readonly callbackUrl?: string;
31
+ readonly redirect?: boolean;
32
+ }
33
+ /**
34
+ * Build `/api/auth/signin/:provider?callbackUrl=…` without touching `window` (testable).
35
+ *
36
+ * @deprecated Auth.js v5 accepts only a CSRF-carrying POST at this path and answers a GET - a link
37
+ * or a navigation here - with an error. Call `signIn()`, which submits that POST.
38
+ */
39
+ export declare function signInUrl(providerId: string, options: {
40
+ readonly basePath?: string;
41
+ readonly callbackUrl: string;
42
+ }): string;
43
+ export interface AuthClient {
44
+ /** The Auth.js session, or `null` when unauthenticated. Usable anywhere `fetch` runs. */
45
+ getSession(init?: RequestInit): Promise<Session | null>;
46
+ /** Start an OAuth/OIDC sign-in (browser only): posts the CSRF-protected sign-in to Auth.js, then
47
+ * navigates to the provider page it answers with. */
48
+ signIn(providerId: string, options?: SignInOptions): Promise<void>;
49
+ /** Sign out via the CSRF-protected endpoint, then navigate (unless `redirect: false`). */
50
+ signOut(options?: SignOutOptions): Promise<void>;
51
+ }
52
+ /** Create the client. Stateless - every call hits the endpoints, so servers stay authoritative. */
53
+ export declare function createAuthClient(options?: AuthClientOptions): AuthClient;
54
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAA;AAE/C,YAAY,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAA;AAE/C,MAAM,WAAW,iBAAiB;IAChC,iDAAiD;IACjD,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;IAC1B,gGAAgG;IAChG,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,KAAK,CAAA;CAC9B;AAED,MAAM,WAAW,aAAa;IAC5B,uEAAuE;IACvE,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAC9B;AAED,MAAM,WAAW,cAAc;IAC7B,sFAAsF;IACtF,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAC5B;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CACvB,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE;IAAE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GACpE,MAAM,CAGR;AAwDD,MAAM,WAAW,UAAU;IACzB,yFAAyF;IACzF,UAAU,CAAC,IAAI,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAAA;IACvD;yDACqD;IACrD,MAAM,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAClE,0FAA0F;IAC1F,OAAO,CAAC,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACjD;AAED,mGAAmG;AACnG,wBAAgB,gBAAgB,CAAC,OAAO,GAAE,iBAAsB,GAAG,UAAU,CAoD5E"}
package/dist/client.js ADDED
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Build `/api/auth/signin/:provider?callbackUrl=…` without touching `window` (testable).
3
+ *
4
+ * @deprecated Auth.js v5 accepts only a CSRF-carrying POST at this path and answers a GET - a link
5
+ * or a navigation here - with an error. Call `signIn()`, which submits that POST.
6
+ */
7
+ export function signInUrl(providerId, options) {
8
+ const base = options.basePath ?? "/api/auth";
9
+ return `${base}/signin/${encodeURIComponent(providerId)}?callbackUrl=${encodeURIComponent(options.callbackUrl)}`;
10
+ }
11
+ const currentUrl = () => {
12
+ if (typeof window === "undefined" || typeof window.location?.href !== "string") {
13
+ throw new Error("[nifra/authjs] signIn needs a browser page (no window.location.href)");
14
+ }
15
+ return window.location.href;
16
+ };
17
+ function isSafeRelativePath(value) {
18
+ if (!value.startsWith("/") || value.startsWith("//"))
19
+ return false;
20
+ for (let i = 1; i < value.length; i++) {
21
+ const code = value.charCodeAt(i);
22
+ if (code <= 0x1f || code === 0x7f || code === 0x5c)
23
+ return false;
24
+ }
25
+ return true;
26
+ }
27
+ /** Where Auth.js sent a sign-in: the provider's authorization page, usually on another origin. It
28
+ * came from the auth endpoint, not the caller, so only a non-web scheme (`javascript:`) is refused. */
29
+ function providerNavigationTarget(value) {
30
+ try {
31
+ const target = new URL(value, window.location.href);
32
+ if (target.protocol === "http:" || target.protocol === "https:")
33
+ return target.href;
34
+ }
35
+ catch {
36
+ // An unparseable answer falls back to the home path, as an unsafe callback does.
37
+ }
38
+ return "/";
39
+ }
40
+ /** Keep post-auth navigation on this origin; an unsafe callback is never an external redirect. */
41
+ function safeNavigationTarget(value) {
42
+ if (isSafeRelativePath(value))
43
+ return value;
44
+ if (typeof window !== "undefined" && typeof window.location?.href === "string") {
45
+ try {
46
+ const target = new URL(value, window.location.href);
47
+ if ((target.protocol === "http:" || target.protocol === "https:") &&
48
+ target.origin === window.location.origin) {
49
+ return target.href;
50
+ }
51
+ }
52
+ catch {
53
+ // Invalid callback URLs fall back to the home path rather than becoming a navigation sink.
54
+ }
55
+ }
56
+ return "/";
57
+ }
58
+ const navigate = (url, target = safeNavigationTarget) => {
59
+ if (typeof window === "undefined" || typeof window.location?.assign !== "function") {
60
+ throw new Error("[nifra/authjs] navigation needs a browser page");
61
+ }
62
+ window.location.assign(target(url));
63
+ };
64
+ /** Create the client. Stateless - every call hits the endpoints, so servers stay authoritative. */
65
+ export function createAuthClient(options = {}) {
66
+ const base = options.basePath ?? "/api/auth";
67
+ const impl = options.fetch ?? fetch;
68
+ const csrfToken = async (action) => {
69
+ const csrf = (await (await impl(`${base}/csrf`)).json());
70
+ if (typeof csrf.csrfToken !== "string" || csrf.csrfToken === "") {
71
+ throw new Error(`[nifra/authjs] ${action}: CSRF token endpoint did not answer`);
72
+ }
73
+ return csrf.csrfToken;
74
+ };
75
+ return {
76
+ async getSession(init) {
77
+ const response = await impl(`${base}/session`, init);
78
+ if (!response.ok)
79
+ return null;
80
+ const session = (await response.json());
81
+ return session?.user ? session : null;
82
+ },
83
+ async signIn(providerId, signInOptions = {}) {
84
+ const callbackUrl = signInOptions.callbackUrl ?? currentUrl();
85
+ const token = await csrfToken("signIn");
86
+ // `X-Auth-Return-Redirect` makes Auth.js answer `{ url }` instead of a 302 that `fetch` would
87
+ // follow on its own, so the browser navigates to the provider itself.
88
+ const response = await impl(`${base}/signin/${encodeURIComponent(providerId)}`, {
89
+ method: "POST",
90
+ headers: {
91
+ "content-type": "application/x-www-form-urlencoded",
92
+ "x-auth-return-redirect": "1",
93
+ },
94
+ body: new URLSearchParams({ csrfToken: token, callbackUrl }),
95
+ });
96
+ const answer = response.ok ? await response.json().catch(() => null) : null;
97
+ const url = typeof answer === "object" && answer !== null ? Reflect.get(answer, "url") : undefined;
98
+ if (typeof url !== "string" || url === "") {
99
+ throw new Error(`[nifra/authjs] signIn failed (HTTP ${response.status})`);
100
+ }
101
+ navigate(url, providerNavigationTarget);
102
+ },
103
+ async signOut(signOutOptions = {}) {
104
+ const token = await csrfToken("signOut");
105
+ const callbackUrl = signOutOptions.callbackUrl ?? "/";
106
+ const response = await impl(`${base}/signout`, {
107
+ method: "POST",
108
+ headers: { "content-type": "application/x-www-form-urlencoded" },
109
+ body: new URLSearchParams({ csrfToken: token, callbackUrl }),
110
+ });
111
+ if (!response.ok) {
112
+ throw new Error(`[nifra/authjs] signOut failed (HTTP ${response.status})`);
113
+ }
114
+ if (signOutOptions.redirect !== false)
115
+ navigate(callbackUrl);
116
+ },
117
+ };
118
+ }
119
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Official Auth.js integration for nifra - backend half. Mount the full Auth.js surface
3
+ * (OAuth/OIDC sign-in, callbacks, session, sign-out, CSRF) into a nifra app, read the session in
4
+ * handlers and loaders, and guard routes - with Auth.js, not a reimplementation, doing the
5
+ * security-critical work (PKCE, state, token verification, session cookies).
6
+ *
7
+ * ```ts
8
+ * import { authjs, requireAuthUser } from "@nifrajs/authjs"
9
+ * import GitHub from "@auth/core/providers/github"
10
+ *
11
+ * const auth = {
12
+ * providers: [GitHub({ clientId: process.env.GITHUB_ID! })],
13
+ * secret: process.env.AUTH_SECRET,
14
+ * trustHost: true, // or a canonical origin: AuthJSOptions.authUrl / AUTH_URL
15
+ * }
16
+ *
17
+ * export const app = server()
18
+ * .use(authjs(auth))
19
+ * .get("/me", async (c) => ({ user: await requireAuthUser(c.req, auth) }))
20
+ * ```
21
+ *
22
+ * Frontend half: `createAuthClient` in `@nifrajs/authjs/client`, plus `<AuthSessionProvider>` /
23
+ * `useAuthSession()` in `@nifrajs/web-react/auth` (other adapters wrap the agnostic client the
24
+ * same way `@nifrajs/web-react/i18n` wraps `@nifrajs/i18n`).
25
+ */
26
+ import type { AuthConfig } from "@auth/core";
27
+ import type { Session } from "@auth/core/types";
28
+ export type { Session } from "@auth/core/types";
29
+ /** The Auth.js config this integration drives - everything `@auth/core` accepts except `raw`. */
30
+ export type AuthJSConfig = Omit<AuthConfig, "raw">;
31
+ export interface AuthJSOptions {
32
+ /** Mount path for Auth.js routes. Defaults to `config.basePath`, then `"/api/auth"`. */
33
+ readonly basePath?: string;
34
+ /** Public origin (e.g. `"https://app.example.com"`); falls back to the `AUTH_URL` variable.
35
+ * Every request is rewritten onto it, so Auth.js trusts it and neither the Host header nor
36
+ * forwarded headers can replace it. Without one, Auth.js trusts the Host header only when
37
+ * `trustHost` (or `AUTH_TRUST_HOST`) says so, or outside production. */
38
+ readonly authUrl?: string;
39
+ /** Trust proxy-provided `x-forwarded-*` headers when `authUrl` is absent. Default `false`.
40
+ * Enable only when the deployment strips and replaces those headers at a trusted proxy. */
41
+ readonly trustProxy?: boolean;
42
+ /** Session secret. Falls back to the `secretEnv` platform binding, then `process.env`. Missing
43
+ * everywhere fails loud (500) on first use - Auth.js cannot sign cookies without it. */
44
+ readonly secret?: string;
45
+ /** Platform-binding name carrying the secret on edge runtimes (no `process.env` there).
46
+ * Default `"AUTH_SECRET"`. */
47
+ readonly secretEnv?: string;
48
+ }
49
+ /**
50
+ * Mount an Auth.js instance into a nifra app: serves `${basePath}/*` (default `/api/auth/*`) for
51
+ * `GET` + `POST`, so sign-in, OAuth callbacks, session, sign-out, and CSRF all run on your server
52
+ * through `@auth/core` itself. A type-IDENTITY plugin (see `defineIdentityPlugin`): mounting never
53
+ * changes the route registry's type, so the typed client keeps working below the mount.
54
+ *
55
+ * Idempotent (named `"authjs"` - applying twice mounts once).
56
+ */
57
+ export declare function authjs(config: AuthJSConfig, options?: AuthJSOptions): import("@nifrajs/core").IdentityPlugin<never>;
58
+ /** What a guard does when the check fails: 302 to `redirectTo` (same-origin path), else 401 JSON.
59
+ * Pass the `basePath`, `authUrl` and `trustProxy` the mount was given, so the session is read on
60
+ * the origin and path the mount serves it from. */
61
+ export interface AuthGuardOptions extends Pick<AuthJSOptions, "basePath" | "authUrl" | "trustProxy"> {
62
+ readonly redirectTo?: string;
63
+ /** Secret override for this call. */
64
+ readonly secret?: string | string[];
65
+ /** Platform binding object used when `config.secret` is not present. */
66
+ readonly env?: unknown;
67
+ /** Secret key in `env`; defaults to `AUTH_SECRET`. */
68
+ readonly secretEnv?: string;
69
+ }
70
+ /**
71
+ * Resolve the Auth.js session for a request - `null` when unauthenticated. Takes the raw `Request`
72
+ * so it works in handlers (`c.req`), loaders/actions (`request`), and middleware. Mirrors the
73
+ * session-callback interception `@hono/auth-js` uses: the full `{ session, user }` flows through
74
+ * your configured `callbacks.session` first.
75
+ */
76
+ export declare function getSession(req: Request, config: AuthJSConfig, options?: AuthGuardOptions): Promise<Session | null>;
77
+ /**
78
+ * Require an authenticated user. Returns `session.user`; otherwise throws a `status(...)` render
79
+ * (302/401) - control flow, caught by nifra's plain-data lane like `@nifrajs/auth` guards. Call at
80
+ * the top of a protected handler/loader/action.
81
+ */
82
+ export declare function requireAuthUser(req: Request, config: AuthJSConfig, options?: AuthGuardOptions): Promise<NonNullable<Session["user"]>>;
83
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAkB,MAAM,YAAY,CAAA;AAC5D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAA;AAI/C,YAAY,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAA;AAE/C,iGAAiG;AACjG,MAAM,MAAM,YAAY,GAAG,IAAI,CAAC,UAAU,EAAE,KAAK,CAAC,CAAA;AAElD,MAAM,WAAW,aAAa;IAC5B,wFAAwF;IACxF,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;IAC1B;;;4EAGwE;IACxE,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;IACzB;+FAC2F;IAC3F,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAA;IAC7B;4FACwF;IACxF,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB;kCAC8B;IAC9B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAC5B;AA6JD;;;;;;;GAOG;AACH,wBAAgB,MAAM,CAAC,MAAM,EAAE,YAAY,EAAE,OAAO,GAAE,aAAkB,iDAqCvE;AAED;;mDAEmD;AACnD,MAAM,WAAW,gBACf,SAAQ,IAAI,CAAC,aAAa,EAAE,UAAU,GAAG,SAAS,GAAG,YAAY,CAAC;IAClE,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;IAC5B,qCAAqC;IACrC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;IACnC,wEAAwE;IACxE,QAAQ,CAAC,GAAG,CAAC,EAAE,OAAO,CAAA;IACtB,sDAAsD;IACtD,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAC5B;AAaD;;;;;GAKG;AACH,wBAAsB,UAAU,CAC9B,GAAG,EAAE,OAAO,EACZ,MAAM,EAAE,YAAY,EACpB,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAoBzB;AAED;;;;GAIG;AACH,wBAAsB,eAAe,CACnC,GAAG,EAAE,OAAO,EACZ,MAAM,EAAE,YAAY,EACpB,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,WAAW,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAIvC"}
package/dist/index.js ADDED
@@ -0,0 +1,253 @@
1
+ /**
2
+ * Official Auth.js integration for nifra - backend half. Mount the full Auth.js surface
3
+ * (OAuth/OIDC sign-in, callbacks, session, sign-out, CSRF) into a nifra app, read the session in
4
+ * handlers and loaders, and guard routes - with Auth.js, not a reimplementation, doing the
5
+ * security-critical work (PKCE, state, token verification, session cookies).
6
+ *
7
+ * ```ts
8
+ * import { authjs, requireAuthUser } from "@nifrajs/authjs"
9
+ * import GitHub from "@auth/core/providers/github"
10
+ *
11
+ * const auth = {
12
+ * providers: [GitHub({ clientId: process.env.GITHUB_ID! })],
13
+ * secret: process.env.AUTH_SECRET,
14
+ * trustHost: true, // or a canonical origin: AuthJSOptions.authUrl / AUTH_URL
15
+ * }
16
+ *
17
+ * export const app = server()
18
+ * .use(authjs(auth))
19
+ * .get("/me", async (c) => ({ user: await requireAuthUser(c.req, auth) }))
20
+ * ```
21
+ *
22
+ * Frontend half: `createAuthClient` in `@nifrajs/authjs/client`, plus `<AuthSessionProvider>` /
23
+ * `useAuthSession()` in `@nifrajs/web-react/auth` (other adapters wrap the agnostic client the
24
+ * same way `@nifrajs/web-react/i18n` wraps `@nifrajs/i18n`).
25
+ */
26
+ import { defineIdentityPlugin, isSameOriginPath, status } from "@nifrajs/core/server";
27
+ const HANDLED_METHODS = ["GET", "POST"];
28
+ /** Read a secret from the process environment where one exists (never on edge workers). */
29
+ function readEnvSecret(name) {
30
+ if (typeof process === "undefined")
31
+ return undefined;
32
+ const value = process.env?.[name];
33
+ return typeof value === "string" && value !== "" ? value : undefined;
34
+ }
35
+ /** Resolve the Auth.js secret: explicit config (a rotation list passes through untouched -
36
+ * Auth.js itself tries each entry), platform binding, then the process environment. */
37
+ function resolveSecret(env, secretEnv, configured) {
38
+ if (configured !== undefined)
39
+ return typeof configured === "string" ? configured : [...configured];
40
+ const name = secretEnv ?? "AUTH_SECRET";
41
+ const fromEnv = typeof env === "object" && env !== null ? env[name] : undefined;
42
+ if (typeof fromEnv === "string" && fromEnv !== "")
43
+ return fromEnv;
44
+ return readEnvSecret(name);
45
+ }
46
+ /** Validate the configured public origin once; request headers must never redefine it. */
47
+ function canonicalPublicOrigin(value) {
48
+ let url;
49
+ try {
50
+ url = new URL(value);
51
+ }
52
+ catch {
53
+ throw new Error("[nifra/authjs] authUrl must be an absolute http(s) origin");
54
+ }
55
+ if ((url.protocol !== "http:" && url.protocol !== "https:") ||
56
+ url.username !== "" ||
57
+ url.password !== "" ||
58
+ url.pathname !== "/" ||
59
+ url.search !== "" ||
60
+ url.hash !== "") {
61
+ throw new Error("[nifra/authjs] authUrl must be an absolute http(s) origin");
62
+ }
63
+ return url.origin;
64
+ }
65
+ /** What Auth.js reads its documented variables from (`AUTH_URL`, `AUTH_TRUST_HOST`, `NODE_ENV`,
66
+ * `AUTH_<PROVIDER>_ID`, ...): the platform bindings, then the process environment. */
67
+ function authEnv(bindings) {
68
+ const bound = typeof bindings === "object" && bindings !== null ? bindings : undefined;
69
+ const processEnv = typeof process === "undefined" ? undefined : process.env;
70
+ return new Proxy({}, {
71
+ get: (_target, key) => {
72
+ if (typeof key !== "string")
73
+ return undefined;
74
+ if (bound !== undefined && Object.hasOwn(bound, key))
75
+ return Reflect.get(bound, key);
76
+ return processEnv?.[key];
77
+ },
78
+ });
79
+ }
80
+ /** The origin of the documented `AUTH_URL` (or `NEXTAUTH_URL`), which may carry a path. */
81
+ function envOrigin(env) {
82
+ const value = env.AUTH_URL ?? env.NEXTAUTH_URL;
83
+ if (value === undefined || value === "")
84
+ return undefined;
85
+ let url;
86
+ try {
87
+ url = new URL(String(value));
88
+ }
89
+ catch {
90
+ throw new Error("[nifra/authjs] AUTH_URL must be an absolute http(s) URL");
91
+ }
92
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
93
+ throw new Error("[nifra/authjs] AUTH_URL must be an absolute http(s) URL");
94
+ }
95
+ return url.origin;
96
+ }
97
+ function normalizeBasePath(value) {
98
+ let base = value ?? "/api/auth";
99
+ while (base.endsWith("/"))
100
+ base = base.slice(0, -1);
101
+ return base === "" ? "/api/auth" : base;
102
+ }
103
+ /**
104
+ * The config Auth.js runs with - never the caller's object, which `setEnvDefaults` would mutate.
105
+ * `basePath` is always explicit: core applies no default of its own here, and an unset basePath
106
+ * answers every action (even `/csrf`) with a flat 400.
107
+ */
108
+ function activeConfig(config, basePath, secret, origin, env, applyEnvDefaults) {
109
+ const active = {
110
+ ...config,
111
+ basePath,
112
+ secret: typeof secret === "string" ? secret : [...secret],
113
+ };
114
+ // A request is rewritten onto a configured origin, so its Host header names nothing. Without
115
+ // one, trusting the Host header is Auth.js's own rule: `trustHost`, `AUTH_TRUST_HOST`, or a
116
+ // `NODE_ENV` other than production.
117
+ if (origin !== undefined)
118
+ active.trustHost ??= true;
119
+ applyEnvDefaults(env, active, true);
120
+ return active;
121
+ }
122
+ /** Remove proxy routing metadata so Auth.js cannot derive a URL from attacker-controlled headers. */
123
+ function sanitizePublicRequest(req, url, host) {
124
+ const rewritten = new Request(url, req);
125
+ rewritten.headers.delete("forwarded");
126
+ rewritten.headers.delete("x-forwarded-host");
127
+ rewritten.headers.delete("x-forwarded-proto");
128
+ rewritten.headers.delete("host");
129
+ rewritten.headers.set("host", host);
130
+ return rewritten;
131
+ }
132
+ /** Derive the public request URL: the configured origin, else the request's own, with forwarded
133
+ * headers trusted only on an explicit opt-in. */
134
+ function publicUrl(req, origin, trustProxy) {
135
+ const url = new URL(req.url);
136
+ if (origin !== undefined) {
137
+ const target = new URL(origin);
138
+ url.protocol = target.protocol;
139
+ url.host = target.host;
140
+ return url;
141
+ }
142
+ if (!trustProxy)
143
+ return url;
144
+ const proto = req.headers.get("x-forwarded-proto");
145
+ const host = req.headers.get("x-forwarded-host") ?? req.headers.get("host");
146
+ if (proto === null && host === null)
147
+ return url;
148
+ const forwarded = new URL(req.url);
149
+ try {
150
+ if (proto !== null) {
151
+ const protocol = proto.endsWith(":") ? proto : `${proto}:`;
152
+ if (protocol !== "http:" && protocol !== "https:")
153
+ throw new Error("invalid proxy protocol");
154
+ forwarded.protocol = protocol;
155
+ }
156
+ if (host !== null)
157
+ forwarded.host = host;
158
+ }
159
+ catch {
160
+ return url;
161
+ }
162
+ return forwarded;
163
+ }
164
+ /** Rewrite a request onto its public URL, preserving method/headers/body. */
165
+ function publicRequest(req, origin, trustProxy) {
166
+ const url = publicUrl(req, origin, trustProxy);
167
+ return sanitizePublicRequest(req, url.href, url.host);
168
+ }
169
+ /**
170
+ * Mount an Auth.js instance into a nifra app: serves `${basePath}/*` (default `/api/auth/*`) for
171
+ * `GET` + `POST`, so sign-in, OAuth callbacks, session, sign-out, and CSRF all run on your server
172
+ * through `@auth/core` itself. A type-IDENTITY plugin (see `defineIdentityPlugin`): mounting never
173
+ * changes the route registry's type, so the typed client keeps working below the mount.
174
+ *
175
+ * Idempotent (named `"authjs"` - applying twice mounts once).
176
+ */
177
+ export function authjs(config, options = {}) {
178
+ const base = normalizeBasePath(options.basePath ?? config.basePath);
179
+ const pattern = `${base}/*`;
180
+ const publicOrigin = options.authUrl === undefined ? undefined : canonicalPublicOrigin(options.authUrl);
181
+ return defineIdentityPlugin("authjs", (app) => {
182
+ for (const method of HANDLED_METHODS) {
183
+ // `register`'s handler is typed `(context: never) => unknown`; the framework invokes it with
184
+ // the real Context, so reading `c.req`/`c.env` is sound. Auth.js's Response passes through.
185
+ app.register(method, pattern, undefined, async (c) => {
186
+ const secret = resolveSecret(c.env, options.secretEnv, options.secret ?? config.secret);
187
+ if (secret === undefined) {
188
+ return status(500, { ok: false, error: "auth_misconfigured" });
189
+ }
190
+ const env = authEnv(c.env);
191
+ let origin;
192
+ try {
193
+ origin = publicOrigin ?? envOrigin(env);
194
+ }
195
+ catch {
196
+ return status(500, { ok: false, error: "auth_misconfigured" });
197
+ }
198
+ const { Auth, setEnvDefaults } = await import("@auth/core");
199
+ const active = activeConfig(config, base, secret, origin, env, setEnvDefaults);
200
+ return Auth(publicRequest(c.req, origin, options.trustProxy === true), active);
201
+ });
202
+ }
203
+ return app;
204
+ });
205
+ }
206
+ const guardRejection = (options) => {
207
+ const to = options.redirectTo;
208
+ if (to === undefined)
209
+ return status(401, { ok: false, error: "unauthorized" });
210
+ if (!isSameOriginPath(to)) {
211
+ throw new Error(`[nifra/authjs] guard redirectTo must be a same-origin path beginning with "/" (got ${JSON.stringify(to)})`);
212
+ }
213
+ return status(302, undefined, { headers: { location: to } });
214
+ };
215
+ /**
216
+ * Resolve the Auth.js session for a request - `null` when unauthenticated. Takes the raw `Request`
217
+ * so it works in handlers (`c.req`), loaders/actions (`request`), and middleware. Mirrors the
218
+ * session-callback interception `@hono/auth-js` uses: the full `{ session, user }` flows through
219
+ * your configured `callbacks.session` first.
220
+ */
221
+ export async function getSession(req, config, options = {}) {
222
+ const secret = resolveSecret(options.env, options.secretEnv, options.secret ?? config.secret);
223
+ if (secret === undefined) {
224
+ throw new Error("[nifra/authjs] getSession needs config.secret, options.secret, or AUTH_SECRET");
225
+ }
226
+ const env = authEnv(options.env);
227
+ const origin = options.authUrl === undefined ? envOrigin(env) : canonicalPublicOrigin(options.authUrl);
228
+ const base = normalizeBasePath(options.basePath ?? config.basePath);
229
+ const { Auth, setEnvDefaults } = await import("@auth/core");
230
+ const active = activeConfig(config, base, secret, origin, env, setEnvDefaults);
231
+ // Read where the mount serves it: the public origin's scheme picks the `__Secure-` cookie names.
232
+ const url = publicUrl(req, origin, options.trustProxy === true);
233
+ const sessionReq = new Request(`${url.origin}${base}/session`, {
234
+ headers: { cookie: req.headers.get("cookie") ?? "" },
235
+ });
236
+ const response = await Auth(sessionReq, active);
237
+ if (!response.ok)
238
+ return null;
239
+ const session = (await response.json());
240
+ return session?.user ? session : null;
241
+ }
242
+ /**
243
+ * Require an authenticated user. Returns `session.user`; otherwise throws a `status(...)` render
244
+ * (302/401) - control flow, caught by nifra's plain-data lane like `@nifrajs/auth` guards. Call at
245
+ * the top of a protected handler/loader/action.
246
+ */
247
+ export async function requireAuthUser(req, config, options = {}) {
248
+ const session = await getSession(req, config, options);
249
+ if (session?.user)
250
+ return session.user;
251
+ throw guardRejection(options);
252
+ }
253
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAKH,OAAO,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAA;AA0BrF,MAAM,eAAe,GAAG,CAAC,KAAK,EAAE,MAAM,CAAU,CAAA;AAEhD,2FAA2F;AAC3F,SAAS,aAAa,CAAC,IAAY;IACjC,IAAI,OAAO,OAAO,KAAK,WAAW;QAAE,OAAO,SAAS,CAAA;IACpD,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAA;IACjC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAA;AACtE,CAAC;AAED;uFACuF;AACvF,SAAS,aAAa,CACpB,GAAY,EACZ,SAA6B,EAC7B,UAAkD;IAElD,IAAI,UAAU,KAAK,SAAS;QAAE,OAAO,OAAO,UAAU,KAAK,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,GAAG,UAAU,CAAC,CAAA;IAClG,MAAM,IAAI,GAAG,SAAS,IAAI,aAAa,CAAA;IACvC,MAAM,OAAO,GACX,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,CAAC,CAAC,CAAE,GAA+B,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IAC9F,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,KAAK,EAAE;QAAE,OAAO,OAAO,CAAA;IACjE,OAAO,aAAa,CAAC,IAAI,CAAC,CAAA;AAC5B,CAAC;AAED,0FAA0F;AAC1F,SAAS,qBAAqB,CAAC,KAAa;IAC1C,IAAI,GAAQ,CAAA;IACZ,IAAI,CAAC;QACH,GAAG,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAA;IACtB,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CAAC,2DAA2D,CAAC,CAAA;IAC9E,CAAC;IACD,IACE,CAAC,GAAG,CAAC,QAAQ,KAAK,OAAO,IAAI,GAAG,CAAC,QAAQ,KAAK,QAAQ,CAAC;QACvD,GAAG,CAAC,QAAQ,KAAK,EAAE;QACnB,GAAG,CAAC,QAAQ,KAAK,EAAE;QACnB,GAAG,CAAC,QAAQ,KAAK,GAAG;QACpB,GAAG,CAAC,MAAM,KAAK,EAAE;QACjB,GAAG,CAAC,IAAI,KAAK,EAAE,EACf,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,2DAA2D,CAAC,CAAA;IAC9E,CAAC;IACD,OAAO,GAAG,CAAC,MAAM,CAAA;AACnB,CAAC;AAED;sFACsF;AACtF,SAAS,OAAO,CAAC,QAAiB;IAChC,MAAM,KAAK,GAAG,OAAO,QAAQ,KAAK,QAAQ,IAAI,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAA;IACtF,MAAM,UAAU,GAAG,OAAO,OAAO,KAAK,WAAW,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAA;IAC3E,OAAO,IAAI,KAAK,CACd,EAAE,EACF;QACE,GAAG,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,EAAE;YACpB,IAAI,OAAO,GAAG,KAAK,QAAQ;gBAAE,OAAO,SAAS,CAAA;YAC7C,IAAI,KAAK,KAAK,SAAS,IAAI,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,GAAG,CAAC;gBAAE,OAAO,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC,CAAA;YACpF,OAAO,UAAU,EAAE,CAAC,GAAG,CAAC,CAAA;QAC1B,CAAC;KACF,CACF,CAAA;AACH,CAAC;AAED,2FAA2F;AAC3F,SAAS,SAAS,CAAC,GAA4B;IAC7C,MAAM,KAAK,GAAG,GAAG,CAAC,QAAQ,IAAI,GAAG,CAAC,YAAY,CAAA;IAC9C,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,EAAE;QAAE,OAAO,SAAS,CAAA;IACzD,IAAI,GAAQ,CAAA;IACZ,IAAI,CAAC;QACH,GAAG,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAA;IAC9B,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CAAC,yDAAyD,CAAC,CAAA;IAC5E,CAAC;IACD,IAAI,GAAG,CAAC,QAAQ,KAAK,OAAO,IAAI,GAAG,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;QAC1D,MAAM,IAAI,KAAK,CAAC,yDAAyD,CAAC,CAAA;IAC5E,CAAC;IACD,OAAO,GAAG,CAAC,MAAM,CAAA;AACnB,CAAC;AAED,SAAS,iBAAiB,CAAC,KAAyB;IAClD,IAAI,IAAI,GAAG,KAAK,IAAI,WAAW,CAAA;IAC/B,OAAO,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA;IACnD,OAAO,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAA;AACzC,CAAC;AAED;;;;GAIG;AACH,SAAS,YAAY,CACnB,MAAoB,EACpB,QAAgB,EAChB,MAAyB,EACzB,MAA0B,EAC1B,GAA4B,EAC5B,gBAAuC;IAEvC,MAAM,MAAM,GAAe;QACzB,GAAG,MAAM;QACT,QAAQ;QACR,MAAM,EAAE,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC;KAC1D,CAAA;IACD,6FAA6F;IAC7F,4FAA4F;IAC5F,oCAAoC;IACpC,IAAI,MAAM,KAAK,SAAS;QAAE,MAAM,CAAC,SAAS,KAAK,IAAI,CAAA;IACnD,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,CAAA;IACnC,OAAO,MAAM,CAAA;AACf,CAAC;AAED,qGAAqG;AACrG,SAAS,qBAAqB,CAAC,GAAY,EAAE,GAAW,EAAE,IAAY;IACpE,MAAM,SAAS,GAAG,IAAI,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAA;IACvC,SAAS,CAAC,OAAO,CAAC,MAAM,CAAC,WAAW,CAAC,CAAA;IACrC,SAAS,CAAC,OAAO,CAAC,MAAM,CAAC,kBAAkB,CAAC,CAAA;IAC5C,SAAS,CAAC,OAAO,CAAC,MAAM,CAAC,mBAAmB,CAAC,CAAA;IAC7C,SAAS,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;IAChC,SAAS,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAA;IACnC,OAAO,SAAS,CAAA;AAClB,CAAC;AAED;iDACiD;AACjD,SAAS,SAAS,CAAC,GAAY,EAAE,MAA0B,EAAE,UAAmB;IAC9E,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;IAC5B,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,CAAA;QAC9B,GAAG,CAAC,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAA;QAC9B,GAAG,CAAC,IAAI,GAAG,MAAM,CAAC,IAAI,CAAA;QACtB,OAAO,GAAG,CAAA;IACZ,CAAC;IACD,IAAI,CAAC,UAAU;QAAE,OAAO,GAAG,CAAA;IAC3B,MAAM,KAAK,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAA;IAClD,MAAM,IAAI,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;IAC3E,IAAI,KAAK,KAAK,IAAI,IAAI,IAAI,KAAK,IAAI;QAAE,OAAO,GAAG,CAAA;IAC/C,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;IAClC,IAAI,CAAC;QACH,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;YACnB,MAAM,QAAQ,GAAG,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,KAAK,GAAG,CAAA;YAC1D,IAAI,QAAQ,KAAK,OAAO,IAAI,QAAQ,KAAK,QAAQ;gBAAE,MAAM,IAAI,KAAK,CAAC,wBAAwB,CAAC,CAAA;YAC5F,SAAS,CAAC,QAAQ,GAAG,QAAQ,CAAA;QAC/B,CAAC;QACD,IAAI,IAAI,KAAK,IAAI;YAAE,SAAS,CAAC,IAAI,GAAG,IAAI,CAAA;IAC1C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,GAAG,CAAA;IACZ,CAAC;IACD,OAAO,SAAS,CAAA;AAClB,CAAC;AAED,6EAA6E;AAC7E,SAAS,aAAa,CAAC,GAAY,EAAE,MAA0B,EAAE,UAAmB;IAClF,MAAM,GAAG,GAAG,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,CAAC,CAAA;IAC9C,OAAO,qBAAqB,CAAC,GAAG,EAAE,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,IAAI,CAAC,CAAA;AACvD,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,MAAM,CAAC,MAAoB,EAAE,UAAyB,EAAE;IACtE,MAAM,IAAI,GAAG,iBAAiB,CAAC,OAAO,CAAC,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,CAAA;IACnE,MAAM,OAAO,GAAG,GAAG,IAAI,IAAI,CAAA;IAC3B,MAAM,YAAY,GAChB,OAAO,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,qBAAqB,CAAC,OAAO,CAAC,OAAO,CAAC,CAAA;IACpF,OAAO,oBAAoB,CAAC,QAAQ,EAAE,CAAsB,GAAM,EAAK,EAAE;QACvE,KAAK,MAAM,MAAM,IAAI,eAAe,EAAE,CAAC;YACrC,6FAA6F;YAC7F,4FAA4F;YAC5F,GAAG,CAAC,QAAQ,CACV,MAAM,EACN,OAAO,EACP,SAAS,EACT,KAAK,EAAE,CAIN,EAAE,EAAE;gBACH,MAAM,MAAM,GAAG,aAAa,CAAC,CAAC,CAAC,GAAG,EAAE,OAAO,CAAC,SAAS,EAAE,OAAO,CAAC,MAAM,IAAI,MAAM,CAAC,MAAM,CAAC,CAAA;gBACvF,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;oBACzB,OAAO,MAAM,CAAC,GAAG,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,oBAAoB,EAAE,CAAC,CAAA;gBAChE,CAAC;gBACD,MAAM,GAAG,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;gBAC1B,IAAI,MAA0B,CAAA;gBAC9B,IAAI,CAAC;oBACH,MAAM,GAAG,YAAY,IAAI,SAAS,CAAC,GAAG,CAAC,CAAA;gBACzC,CAAC;gBAAC,MAAM,CAAC;oBACP,OAAO,MAAM,CAAC,GAAG,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,oBAAoB,EAAE,CAAC,CAAA;gBAChE,CAAC;gBACD,MAAM,EAAE,IAAI,EAAE,cAAc,EAAE,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,CAAA;gBAC3D,MAAM,MAAM,GAAG,YAAY,CAAC,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,cAAc,CAAC,CAAA;gBAC9E,OAAO,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,UAAU,KAAK,IAAI,CAAC,EAAE,MAAM,CAAC,CAAA;YAChF,CAAC,CACF,CAAA;QACH,CAAC;QACD,OAAO,GAAG,CAAA;IACZ,CAAC,CAAC,CAAA;AACJ,CAAC;AAgBD,MAAM,cAAc,GAAG,CAAC,OAAyB,EAAkB,EAAE;IACnE,MAAM,EAAE,GAAG,OAAO,CAAC,UAAU,CAAA;IAC7B,IAAI,EAAE,KAAK,SAAS;QAAE,OAAO,MAAM,CAAC,GAAG,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,cAAc,EAAE,CAAC,CAAA;IAC9E,IAAI,CAAC,gBAAgB,CAAC,EAAE,CAAC,EAAE,CAAC;QAC1B,MAAM,IAAI,KAAK,CACb,sFAAsF,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,GAAG,CAC5G,CAAA;IACH,CAAC;IACD,OAAO,MAAM,CAAC,GAAG,EAAE,SAAS,EAAE,EAAE,OAAO,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,EAAE,CAAC,CAAA;AAC9D,CAAC,CAAA;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,GAAY,EACZ,MAAoB,EACpB,UAA4B,EAAE;IAE9B,MAAM,MAAM,GAAG,aAAa,CAAC,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,SAAS,EAAE,OAAO,CAAC,MAAM,IAAI,MAAM,CAAC,MAAM,CAAC,CAAA;IAC7F,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CAAC,+EAA+E,CAAC,CAAA;IAClG,CAAC;IACD,MAAM,GAAG,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;IAChC,MAAM,MAAM,GACV,OAAO,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,qBAAqB,CAAC,OAAO,CAAC,OAAO,CAAC,CAAA;IACzF,MAAM,IAAI,GAAG,iBAAiB,CAAC,OAAO,CAAC,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,CAAA;IACnE,MAAM,EAAE,IAAI,EAAE,cAAc,EAAE,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,CAAA;IAC3D,MAAM,MAAM,GAAG,YAAY,CAAC,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,cAAc,CAAC,CAAA;IAC9E,iGAAiG;IACjG,MAAM,GAAG,GAAG,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,UAAU,KAAK,IAAI,CAAC,CAAA;IAC/D,MAAM,UAAU,GAAG,IAAI,OAAO,CAAC,GAAG,GAAG,CAAC,MAAM,GAAG,IAAI,UAAU,EAAE;QAC7D,OAAO,EAAE,EAAE,MAAM,EAAE,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE;KACrD,CAAC,CAAA;IACF,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,UAAU,EAAE,MAAM,CAAC,CAAA;IAC/C,IAAI,CAAC,QAAQ,CAAC,EAAE;QAAE,OAAO,IAAI,CAAA;IAC7B,MAAM,OAAO,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAmB,CAAA;IACzD,OAAO,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAA;AACvC,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,GAAY,EACZ,MAAoB,EACpB,UAA4B,EAAE;IAE9B,MAAM,OAAO,GAAG,MAAM,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,CAAA;IACtD,IAAI,OAAO,EAAE,IAAI;QAAE,OAAO,OAAO,CAAC,IAAI,CAAA;IACtC,MAAM,cAAc,CAAC,OAAO,CAAC,CAAA;AAC/B,CAAC"}
package/package.json CHANGED
@@ -1 +1,64 @@
1
- {"name":"@nifrajs/authjs","version":"0.0.0-bootstrap.0","description":"Placeholder; the real package publishes from github.com/nifrajs/nifra","license":"MIT","repository":{"type":"git","url":"git+https://github.com/nifrajs/nifra.git","directory":"packages/authjs"}}
1
+ {
2
+ "name": "@nifrajs/authjs",
3
+ "version": "4.0.2",
4
+ "description": "Official Auth.js integration for nifra - mount Auth.js (OAuth, OIDC, credentials, WebAuthn-ready) into a nifra app, read sessions in handlers and loaders, guard routes, and drive it all from any frontend.",
5
+ "keywords": [
6
+ "bun",
7
+ "typescript",
8
+ "nifra",
9
+ "auth",
10
+ "authjs",
11
+ "oauth",
12
+ "oidc",
13
+ "session"
14
+ ],
15
+ "type": "module",
16
+ "nifra": {
17
+ "environment": "library"
18
+ },
19
+ "main": "./dist/index.js",
20
+ "module": "./dist/index.js",
21
+ "types": "./dist/index.d.ts",
22
+ "exports": {
23
+ ".": {
24
+ "bun": "./src/index.ts",
25
+ "types": "./dist/index.d.ts",
26
+ "default": "./dist/index.js"
27
+ },
28
+ "./client": {
29
+ "bun": "./src/client.ts",
30
+ "types": "./dist/client.d.ts",
31
+ "default": "./dist/client.js"
32
+ }
33
+ },
34
+ "files": [
35
+ "dist",
36
+ "src"
37
+ ],
38
+ "sideEffects": false,
39
+ "scripts": {
40
+ "build": "tsc -p tsconfig.build.json && bun run ../../scripts/fix-dts.ts dist"
41
+ },
42
+ "license": "MIT",
43
+ "publishConfig": {
44
+ "access": "public"
45
+ },
46
+ "dependencies": {
47
+ "@auth/core": "^0.41.3"
48
+ },
49
+ "peerDependencies": {
50
+ "@nifrajs/core": "^4.0.2"
51
+ },
52
+ "devDependencies": {
53
+ "@nifrajs/core": "workspace:*"
54
+ },
55
+ "repository": {
56
+ "type": "git",
57
+ "url": "git+https://github.com/nifrajs/nifra.git",
58
+ "directory": "packages/authjs"
59
+ },
60
+ "bugs": {
61
+ "url": "https://github.com/nifrajs/nifra/issues"
62
+ },
63
+ "homepage": "https://github.com/nifrajs/nifra#readme"
64
+ }
package/src/client.ts ADDED
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Framework-agnostic Auth.js client - `fetch` against the mounted Auth.js endpoints, no DOM
3
+ * framework involved. Every UI adapter builds on this: pass it a base path (default
4
+ * `/api/auth`, matching the backend mount default) and an optional `fetch` override for tests
5
+ * or loaders that must forward headers.
6
+ *
7
+ * ```ts
8
+ * import { createAuthClient } from "@nifrajs/authjs/client"
9
+ *
10
+ * const auth = createAuthClient()
11
+ * await auth.signIn("github") // posts sign-in, then navigates to the provider (browser only)
12
+ * await auth.signOut() // posts signout, then navigates home
13
+ * const session = await auth.getSession() // works anywhere fetch does
14
+ * ```
15
+ */
16
+ import type { Session } from "@auth/core/types"
17
+
18
+ export type { Session } from "@auth/core/types"
19
+
20
+ export interface AuthClientOptions {
21
+ /** Auth.js mount path. Default `"/api/auth"`. */
22
+ readonly basePath?: string
23
+ /** `fetch` implementation. Default global `fetch` - override in tests or to forward headers. */
24
+ readonly fetch?: typeof fetch
25
+ }
26
+
27
+ export interface SignInOptions {
28
+ /** Where the provider returns afterwards. Default the current page. */
29
+ readonly callbackUrl?: string
30
+ }
31
+
32
+ export interface SignOutOptions {
33
+ /** Where to land after sign-out. Default `"/"`. Set `redirect: false` to stay put. */
34
+ readonly callbackUrl?: string
35
+ readonly redirect?: boolean
36
+ }
37
+
38
+ /**
39
+ * Build `/api/auth/signin/:provider?callbackUrl=…` without touching `window` (testable).
40
+ *
41
+ * @deprecated Auth.js v5 accepts only a CSRF-carrying POST at this path and answers a GET - a link
42
+ * or a navigation here - with an error. Call `signIn()`, which submits that POST.
43
+ */
44
+ export function signInUrl(
45
+ providerId: string,
46
+ options: { readonly basePath?: string; readonly callbackUrl: string },
47
+ ): string {
48
+ const base = options.basePath ?? "/api/auth"
49
+ return `${base}/signin/${encodeURIComponent(providerId)}?callbackUrl=${encodeURIComponent(options.callbackUrl)}`
50
+ }
51
+
52
+ const currentUrl = (): string => {
53
+ if (typeof window === "undefined" || typeof window.location?.href !== "string") {
54
+ throw new Error("[nifra/authjs] signIn needs a browser page (no window.location.href)")
55
+ }
56
+ return window.location.href
57
+ }
58
+
59
+ function isSafeRelativePath(value: string): boolean {
60
+ if (!value.startsWith("/") || value.startsWith("//")) return false
61
+ for (let i = 1; i < value.length; i++) {
62
+ const code = value.charCodeAt(i)
63
+ if (code <= 0x1f || code === 0x7f || code === 0x5c) return false
64
+ }
65
+ return true
66
+ }
67
+
68
+ /** Where Auth.js sent a sign-in: the provider's authorization page, usually on another origin. It
69
+ * came from the auth endpoint, not the caller, so only a non-web scheme (`javascript:`) is refused. */
70
+ function providerNavigationTarget(value: string): string {
71
+ try {
72
+ const target = new URL(value, window.location.href)
73
+ if (target.protocol === "http:" || target.protocol === "https:") return target.href
74
+ } catch {
75
+ // An unparseable answer falls back to the home path, as an unsafe callback does.
76
+ }
77
+ return "/"
78
+ }
79
+
80
+ /** Keep post-auth navigation on this origin; an unsafe callback is never an external redirect. */
81
+ function safeNavigationTarget(value: string): string {
82
+ if (isSafeRelativePath(value)) return value
83
+ if (typeof window !== "undefined" && typeof window.location?.href === "string") {
84
+ try {
85
+ const target = new URL(value, window.location.href)
86
+ if (
87
+ (target.protocol === "http:" || target.protocol === "https:") &&
88
+ target.origin === window.location.origin
89
+ ) {
90
+ return target.href
91
+ }
92
+ } catch {
93
+ // Invalid callback URLs fall back to the home path rather than becoming a navigation sink.
94
+ }
95
+ }
96
+ return "/"
97
+ }
98
+
99
+ const navigate = (url: string, target: (url: string) => string = safeNavigationTarget): void => {
100
+ if (typeof window === "undefined" || typeof window.location?.assign !== "function") {
101
+ throw new Error("[nifra/authjs] navigation needs a browser page")
102
+ }
103
+ window.location.assign(target(url))
104
+ }
105
+
106
+ export interface AuthClient {
107
+ /** The Auth.js session, or `null` when unauthenticated. Usable anywhere `fetch` runs. */
108
+ getSession(init?: RequestInit): Promise<Session | null>
109
+ /** Start an OAuth/OIDC sign-in (browser only): posts the CSRF-protected sign-in to Auth.js, then
110
+ * navigates to the provider page it answers with. */
111
+ signIn(providerId: string, options?: SignInOptions): Promise<void>
112
+ /** Sign out via the CSRF-protected endpoint, then navigate (unless `redirect: false`). */
113
+ signOut(options?: SignOutOptions): Promise<void>
114
+ }
115
+
116
+ /** Create the client. Stateless - every call hits the endpoints, so servers stay authoritative. */
117
+ export function createAuthClient(options: AuthClientOptions = {}): AuthClient {
118
+ const base = options.basePath ?? "/api/auth"
119
+ const impl = options.fetch ?? fetch
120
+ const csrfToken = async (action: string): Promise<string> => {
121
+ const csrf = (await (await impl(`${base}/csrf`)).json()) as { csrfToken?: unknown }
122
+ if (typeof csrf.csrfToken !== "string" || csrf.csrfToken === "") {
123
+ throw new Error(`[nifra/authjs] ${action}: CSRF token endpoint did not answer`)
124
+ }
125
+ return csrf.csrfToken
126
+ }
127
+ return {
128
+ async getSession(init?: RequestInit): Promise<Session | null> {
129
+ const response = await impl(`${base}/session`, init)
130
+ if (!response.ok) return null
131
+ const session = (await response.json()) as Session | null
132
+ return session?.user ? session : null
133
+ },
134
+ async signIn(providerId: string, signInOptions: SignInOptions = {}): Promise<void> {
135
+ const callbackUrl = signInOptions.callbackUrl ?? currentUrl()
136
+ const token = await csrfToken("signIn")
137
+ // `X-Auth-Return-Redirect` makes Auth.js answer `{ url }` instead of a 302 that `fetch` would
138
+ // follow on its own, so the browser navigates to the provider itself.
139
+ const response = await impl(`${base}/signin/${encodeURIComponent(providerId)}`, {
140
+ method: "POST",
141
+ headers: {
142
+ "content-type": "application/x-www-form-urlencoded",
143
+ "x-auth-return-redirect": "1",
144
+ },
145
+ body: new URLSearchParams({ csrfToken: token, callbackUrl }),
146
+ })
147
+ const answer: unknown = response.ok ? await response.json().catch(() => null) : null
148
+ const url =
149
+ typeof answer === "object" && answer !== null ? Reflect.get(answer, "url") : undefined
150
+ if (typeof url !== "string" || url === "") {
151
+ throw new Error(`[nifra/authjs] signIn failed (HTTP ${response.status})`)
152
+ }
153
+ navigate(url, providerNavigationTarget)
154
+ },
155
+ async signOut(signOutOptions: SignOutOptions = {}): Promise<void> {
156
+ const token = await csrfToken("signOut")
157
+ const callbackUrl = signOutOptions.callbackUrl ?? "/"
158
+ const response = await impl(`${base}/signout`, {
159
+ method: "POST",
160
+ headers: { "content-type": "application/x-www-form-urlencoded" },
161
+ body: new URLSearchParams({ csrfToken: token, callbackUrl }),
162
+ })
163
+ if (!response.ok) {
164
+ throw new Error(`[nifra/authjs] signOut failed (HTTP ${response.status})`)
165
+ }
166
+ if (signOutOptions.redirect !== false) navigate(callbackUrl)
167
+ },
168
+ }
169
+ }
package/src/index.ts ADDED
@@ -0,0 +1,328 @@
1
+ /**
2
+ * Official Auth.js integration for nifra - backend half. Mount the full Auth.js surface
3
+ * (OAuth/OIDC sign-in, callbacks, session, sign-out, CSRF) into a nifra app, read the session in
4
+ * handlers and loaders, and guard routes - with Auth.js, not a reimplementation, doing the
5
+ * security-critical work (PKCE, state, token verification, session cookies).
6
+ *
7
+ * ```ts
8
+ * import { authjs, requireAuthUser } from "@nifrajs/authjs"
9
+ * import GitHub from "@auth/core/providers/github"
10
+ *
11
+ * const auth = {
12
+ * providers: [GitHub({ clientId: process.env.GITHUB_ID! })],
13
+ * secret: process.env.AUTH_SECRET,
14
+ * trustHost: true, // or a canonical origin: AuthJSOptions.authUrl / AUTH_URL
15
+ * }
16
+ *
17
+ * export const app = server()
18
+ * .use(authjs(auth))
19
+ * .get("/me", async (c) => ({ user: await requireAuthUser(c.req, auth) }))
20
+ * ```
21
+ *
22
+ * Frontend half: `createAuthClient` in `@nifrajs/authjs/client`, plus `<AuthSessionProvider>` /
23
+ * `useAuthSession()` in `@nifrajs/web-react/auth` (other adapters wrap the agnostic client the
24
+ * same way `@nifrajs/web-react/i18n` wraps `@nifrajs/i18n`).
25
+ */
26
+
27
+ import type { AuthConfig, setEnvDefaults } from "@auth/core"
28
+ import type { Session } from "@auth/core/types"
29
+ import type { AnyServer, Platform, ResponseResult } from "@nifrajs/core/server"
30
+ import { defineIdentityPlugin, isSameOriginPath, status } from "@nifrajs/core/server"
31
+
32
+ export type { Session } from "@auth/core/types"
33
+
34
+ /** The Auth.js config this integration drives - everything `@auth/core` accepts except `raw`. */
35
+ export type AuthJSConfig = Omit<AuthConfig, "raw">
36
+
37
+ export interface AuthJSOptions {
38
+ /** Mount path for Auth.js routes. Defaults to `config.basePath`, then `"/api/auth"`. */
39
+ readonly basePath?: string
40
+ /** Public origin (e.g. `"https://app.example.com"`); falls back to the `AUTH_URL` variable.
41
+ * Every request is rewritten onto it, so Auth.js trusts it and neither the Host header nor
42
+ * forwarded headers can replace it. Without one, Auth.js trusts the Host header only when
43
+ * `trustHost` (or `AUTH_TRUST_HOST`) says so, or outside production. */
44
+ readonly authUrl?: string
45
+ /** Trust proxy-provided `x-forwarded-*` headers when `authUrl` is absent. Default `false`.
46
+ * Enable only when the deployment strips and replaces those headers at a trusted proxy. */
47
+ readonly trustProxy?: boolean
48
+ /** Session secret. Falls back to the `secretEnv` platform binding, then `process.env`. Missing
49
+ * everywhere fails loud (500) on first use - Auth.js cannot sign cookies without it. */
50
+ readonly secret?: string
51
+ /** Platform-binding name carrying the secret on edge runtimes (no `process.env` there).
52
+ * Default `"AUTH_SECRET"`. */
53
+ readonly secretEnv?: string
54
+ }
55
+
56
+ const HANDLED_METHODS = ["GET", "POST"] as const
57
+
58
+ /** Read a secret from the process environment where one exists (never on edge workers). */
59
+ function readEnvSecret(name: string): string | undefined {
60
+ if (typeof process === "undefined") return undefined
61
+ const value = process.env?.[name]
62
+ return typeof value === "string" && value !== "" ? value : undefined
63
+ }
64
+
65
+ /** Resolve the Auth.js secret: explicit config (a rotation list passes through untouched -
66
+ * Auth.js itself tries each entry), platform binding, then the process environment. */
67
+ function resolveSecret(
68
+ env: unknown,
69
+ secretEnv: string | undefined,
70
+ configured: string | readonly string[] | undefined,
71
+ ): string | string[] | undefined {
72
+ if (configured !== undefined) return typeof configured === "string" ? configured : [...configured]
73
+ const name = secretEnv ?? "AUTH_SECRET"
74
+ const fromEnv =
75
+ typeof env === "object" && env !== null ? (env as Record<string, unknown>)[name] : undefined
76
+ if (typeof fromEnv === "string" && fromEnv !== "") return fromEnv
77
+ return readEnvSecret(name)
78
+ }
79
+
80
+ /** Validate the configured public origin once; request headers must never redefine it. */
81
+ function canonicalPublicOrigin(value: string): string {
82
+ let url: URL
83
+ try {
84
+ url = new URL(value)
85
+ } catch {
86
+ throw new Error("[nifra/authjs] authUrl must be an absolute http(s) origin")
87
+ }
88
+ if (
89
+ (url.protocol !== "http:" && url.protocol !== "https:") ||
90
+ url.username !== "" ||
91
+ url.password !== "" ||
92
+ url.pathname !== "/" ||
93
+ url.search !== "" ||
94
+ url.hash !== ""
95
+ ) {
96
+ throw new Error("[nifra/authjs] authUrl must be an absolute http(s) origin")
97
+ }
98
+ return url.origin
99
+ }
100
+
101
+ /** What Auth.js reads its documented variables from (`AUTH_URL`, `AUTH_TRUST_HOST`, `NODE_ENV`,
102
+ * `AUTH_<PROVIDER>_ID`, ...): the platform bindings, then the process environment. */
103
+ function authEnv(bindings: unknown): Record<string, unknown> {
104
+ const bound = typeof bindings === "object" && bindings !== null ? bindings : undefined
105
+ const processEnv = typeof process === "undefined" ? undefined : process.env
106
+ return new Proxy(
107
+ {},
108
+ {
109
+ get: (_target, key) => {
110
+ if (typeof key !== "string") return undefined
111
+ if (bound !== undefined && Object.hasOwn(bound, key)) return Reflect.get(bound, key)
112
+ return processEnv?.[key]
113
+ },
114
+ },
115
+ )
116
+ }
117
+
118
+ /** The origin of the documented `AUTH_URL` (or `NEXTAUTH_URL`), which may carry a path. */
119
+ function envOrigin(env: Record<string, unknown>): string | undefined {
120
+ const value = env.AUTH_URL ?? env.NEXTAUTH_URL
121
+ if (value === undefined || value === "") return undefined
122
+ let url: URL
123
+ try {
124
+ url = new URL(String(value))
125
+ } catch {
126
+ throw new Error("[nifra/authjs] AUTH_URL must be an absolute http(s) URL")
127
+ }
128
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
129
+ throw new Error("[nifra/authjs] AUTH_URL must be an absolute http(s) URL")
130
+ }
131
+ return url.origin
132
+ }
133
+
134
+ function normalizeBasePath(value: string | undefined): string {
135
+ let base = value ?? "/api/auth"
136
+ while (base.endsWith("/")) base = base.slice(0, -1)
137
+ return base === "" ? "/api/auth" : base
138
+ }
139
+
140
+ /**
141
+ * The config Auth.js runs with - never the caller's object, which `setEnvDefaults` would mutate.
142
+ * `basePath` is always explicit: core applies no default of its own here, and an unset basePath
143
+ * answers every action (even `/csrf`) with a flat 400.
144
+ */
145
+ function activeConfig(
146
+ config: AuthJSConfig,
147
+ basePath: string,
148
+ secret: string | string[],
149
+ origin: string | undefined,
150
+ env: Record<string, unknown>,
151
+ applyEnvDefaults: typeof setEnvDefaults,
152
+ ): AuthConfig {
153
+ const active: AuthConfig = {
154
+ ...config,
155
+ basePath,
156
+ secret: typeof secret === "string" ? secret : [...secret],
157
+ }
158
+ // A request is rewritten onto a configured origin, so its Host header names nothing. Without
159
+ // one, trusting the Host header is Auth.js's own rule: `trustHost`, `AUTH_TRUST_HOST`, or a
160
+ // `NODE_ENV` other than production.
161
+ if (origin !== undefined) active.trustHost ??= true
162
+ applyEnvDefaults(env, active, true)
163
+ return active
164
+ }
165
+
166
+ /** Remove proxy routing metadata so Auth.js cannot derive a URL from attacker-controlled headers. */
167
+ function sanitizePublicRequest(req: Request, url: string, host: string): Request {
168
+ const rewritten = new Request(url, req)
169
+ rewritten.headers.delete("forwarded")
170
+ rewritten.headers.delete("x-forwarded-host")
171
+ rewritten.headers.delete("x-forwarded-proto")
172
+ rewritten.headers.delete("host")
173
+ rewritten.headers.set("host", host)
174
+ return rewritten
175
+ }
176
+
177
+ /** Derive the public request URL: the configured origin, else the request's own, with forwarded
178
+ * headers trusted only on an explicit opt-in. */
179
+ function publicUrl(req: Request, origin: string | undefined, trustProxy: boolean): URL {
180
+ const url = new URL(req.url)
181
+ if (origin !== undefined) {
182
+ const target = new URL(origin)
183
+ url.protocol = target.protocol
184
+ url.host = target.host
185
+ return url
186
+ }
187
+ if (!trustProxy) return url
188
+ const proto = req.headers.get("x-forwarded-proto")
189
+ const host = req.headers.get("x-forwarded-host") ?? req.headers.get("host")
190
+ if (proto === null && host === null) return url
191
+ const forwarded = new URL(req.url)
192
+ try {
193
+ if (proto !== null) {
194
+ const protocol = proto.endsWith(":") ? proto : `${proto}:`
195
+ if (protocol !== "http:" && protocol !== "https:") throw new Error("invalid proxy protocol")
196
+ forwarded.protocol = protocol
197
+ }
198
+ if (host !== null) forwarded.host = host
199
+ } catch {
200
+ return url
201
+ }
202
+ return forwarded
203
+ }
204
+
205
+ /** Rewrite a request onto its public URL, preserving method/headers/body. */
206
+ function publicRequest(req: Request, origin: string | undefined, trustProxy: boolean): Request {
207
+ const url = publicUrl(req, origin, trustProxy)
208
+ return sanitizePublicRequest(req, url.href, url.host)
209
+ }
210
+
211
+ /**
212
+ * Mount an Auth.js instance into a nifra app: serves `${basePath}/*` (default `/api/auth/*`) for
213
+ * `GET` + `POST`, so sign-in, OAuth callbacks, session, sign-out, and CSRF all run on your server
214
+ * through `@auth/core` itself. A type-IDENTITY plugin (see `defineIdentityPlugin`): mounting never
215
+ * changes the route registry's type, so the typed client keeps working below the mount.
216
+ *
217
+ * Idempotent (named `"authjs"` - applying twice mounts once).
218
+ */
219
+ export function authjs(config: AuthJSConfig, options: AuthJSOptions = {}) {
220
+ const base = normalizeBasePath(options.basePath ?? config.basePath)
221
+ const pattern = `${base}/*`
222
+ const publicOrigin =
223
+ options.authUrl === undefined ? undefined : canonicalPublicOrigin(options.authUrl)
224
+ return defineIdentityPlugin("authjs", <S extends AnyServer>(app: S): S => {
225
+ for (const method of HANDLED_METHODS) {
226
+ // `register`'s handler is typed `(context: never) => unknown`; the framework invokes it with
227
+ // the real Context, so reading `c.req`/`c.env` is sound. Auth.js's Response passes through.
228
+ app.register(
229
+ method,
230
+ pattern,
231
+ undefined,
232
+ async (c: {
233
+ readonly req: Request
234
+ readonly env: unknown
235
+ readonly platform?: Platform
236
+ }) => {
237
+ const secret = resolveSecret(c.env, options.secretEnv, options.secret ?? config.secret)
238
+ if (secret === undefined) {
239
+ return status(500, { ok: false, error: "auth_misconfigured" })
240
+ }
241
+ const env = authEnv(c.env)
242
+ let origin: string | undefined
243
+ try {
244
+ origin = publicOrigin ?? envOrigin(env)
245
+ } catch {
246
+ return status(500, { ok: false, error: "auth_misconfigured" })
247
+ }
248
+ const { Auth, setEnvDefaults } = await import("@auth/core")
249
+ const active = activeConfig(config, base, secret, origin, env, setEnvDefaults)
250
+ return Auth(publicRequest(c.req, origin, options.trustProxy === true), active)
251
+ },
252
+ )
253
+ }
254
+ return app
255
+ })
256
+ }
257
+
258
+ /** What a guard does when the check fails: 302 to `redirectTo` (same-origin path), else 401 JSON.
259
+ * Pass the `basePath`, `authUrl` and `trustProxy` the mount was given, so the session is read on
260
+ * the origin and path the mount serves it from. */
261
+ export interface AuthGuardOptions
262
+ extends Pick<AuthJSOptions, "basePath" | "authUrl" | "trustProxy"> {
263
+ readonly redirectTo?: string
264
+ /** Secret override for this call. */
265
+ readonly secret?: string | string[]
266
+ /** Platform binding object used when `config.secret` is not present. */
267
+ readonly env?: unknown
268
+ /** Secret key in `env`; defaults to `AUTH_SECRET`. */
269
+ readonly secretEnv?: string
270
+ }
271
+
272
+ const guardRejection = (options: AuthGuardOptions): ResponseResult => {
273
+ const to = options.redirectTo
274
+ if (to === undefined) return status(401, { ok: false, error: "unauthorized" })
275
+ if (!isSameOriginPath(to)) {
276
+ throw new Error(
277
+ `[nifra/authjs] guard redirectTo must be a same-origin path beginning with "/" (got ${JSON.stringify(to)})`,
278
+ )
279
+ }
280
+ return status(302, undefined, { headers: { location: to } })
281
+ }
282
+
283
+ /**
284
+ * Resolve the Auth.js session for a request - `null` when unauthenticated. Takes the raw `Request`
285
+ * so it works in handlers (`c.req`), loaders/actions (`request`), and middleware. Mirrors the
286
+ * session-callback interception `@hono/auth-js` uses: the full `{ session, user }` flows through
287
+ * your configured `callbacks.session` first.
288
+ */
289
+ export async function getSession(
290
+ req: Request,
291
+ config: AuthJSConfig,
292
+ options: AuthGuardOptions = {},
293
+ ): Promise<Session | null> {
294
+ const secret = resolveSecret(options.env, options.secretEnv, options.secret ?? config.secret)
295
+ if (secret === undefined) {
296
+ throw new Error("[nifra/authjs] getSession needs config.secret, options.secret, or AUTH_SECRET")
297
+ }
298
+ const env = authEnv(options.env)
299
+ const origin =
300
+ options.authUrl === undefined ? envOrigin(env) : canonicalPublicOrigin(options.authUrl)
301
+ const base = normalizeBasePath(options.basePath ?? config.basePath)
302
+ const { Auth, setEnvDefaults } = await import("@auth/core")
303
+ const active = activeConfig(config, base, secret, origin, env, setEnvDefaults)
304
+ // Read where the mount serves it: the public origin's scheme picks the `__Secure-` cookie names.
305
+ const url = publicUrl(req, origin, options.trustProxy === true)
306
+ const sessionReq = new Request(`${url.origin}${base}/session`, {
307
+ headers: { cookie: req.headers.get("cookie") ?? "" },
308
+ })
309
+ const response = await Auth(sessionReq, active)
310
+ if (!response.ok) return null
311
+ const session = (await response.json()) as Session | null
312
+ return session?.user ? session : null
313
+ }
314
+
315
+ /**
316
+ * Require an authenticated user. Returns `session.user`; otherwise throws a `status(...)` render
317
+ * (302/401) - control flow, caught by nifra's plain-data lane like `@nifrajs/auth` guards. Call at
318
+ * the top of a protected handler/loader/action.
319
+ */
320
+ export async function requireAuthUser(
321
+ req: Request,
322
+ config: AuthJSConfig,
323
+ options: AuthGuardOptions = {},
324
+ ): Promise<NonNullable<Session["user"]>> {
325
+ const session = await getSession(req, config, options)
326
+ if (session?.user) return session.user
327
+ throw guardRejection(options)
328
+ }