@henosia/app-next 1.0.4 → 1.0.6
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/.agents/skills/henosia-app-next/SKILL.md +9 -8
- package/README.md +9 -4
- package/dist/api/auth.d.mts +3 -1
- package/dist/api/auth.mjs +32 -2
- package/dist/api/platform.mjs +42 -6
- package/dist/auth/middleware.d.mts +6 -21
- package/dist/auth/middleware.mjs +43 -128
- package/dist/auth/server-guards.d.mts +1 -1
- package/dist/auth/server-guards.mjs +2 -2
- package/dist/auth/server.d.mts +1 -106
- package/dist/auth/server.mjs +40 -79
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +4 -3
- package/dist/middleware-shared-CZbIZuBw.mjs +140 -0
- package/dist/platform/app-switcher.d.mts +7 -10
- package/dist/platform/app-switcher.mjs +4 -4
- package/dist/server-DfD6Dc91.d.mts +112 -0
- package/dist/session-sync-Cva_oreV.mjs +114 -0
- package/dist/shared-BWPBaubT.d.mts +30 -0
- package/dist/shared.d.mts +2 -2
- package/dist/shared.mjs +11 -2
- package/package.json +1 -1
- package/dist/shared-BWt7Sysv.d.mts +0 -19
|
@@ -16,8 +16,8 @@ enabled: false # flip to true to enable during Henosia Auth integration in proje
|
|
|
16
16
|
|
|
17
17
|
`@henosia/app-next` is the Henosia integration for Next.js apps. It provides:
|
|
18
18
|
|
|
19
|
-
- Henosia Auth middleware (pre-check
|
|
20
|
-
- better-auth and Henosia Platform catch-all Route Handlers.
|
|
19
|
+
- Henosia Auth middleware (edge-safe session-cookie pre-check that delegates server-only session sync to the auth route).
|
|
20
|
+
- better-auth and Henosia Platform catch-all Route Handlers, including token refresh, organization-context sync, and optional Supabase session exchange.
|
|
21
21
|
- A React `authClient` (better-auth) and a `useAppSwitcher` hook.
|
|
22
22
|
- Optional Supabase server/browser client factories.
|
|
23
23
|
- Server-side auth guards — see the **`henosia-auth-guards` skill** for those
|
|
@@ -27,7 +27,7 @@ enabled: false # flip to true to enable during Henosia Auth integration in proje
|
|
|
27
27
|
## Authoritative reference
|
|
28
28
|
|
|
29
29
|
For complete and up-to-date package usage details (peer dependencies, subpath
|
|
30
|
-
exports, Supabase exchange behavior, app-switcher hook options), read:
|
|
30
|
+
exports, middleware-backed route-handler session sync, Supabase exchange behavior, app-switcher hook options), read:
|
|
31
31
|
|
|
32
32
|
- `node_modules/@henosia/app-next/README.md`
|
|
33
33
|
|
|
@@ -171,13 +171,14 @@ README section.
|
|
|
171
171
|
12. **(Optional) Add Supabase helpers.** When `NEXT_PUBLIC_SUPABASE_URL` is
|
|
172
172
|
set in the Henosia builder env, install the Supabase peer deps and copy
|
|
173
173
|
`lib/supabase/server.ts` and `lib/supabase/client.ts` from the template.
|
|
174
|
-
The middleware
|
|
175
|
-
|
|
176
|
-
|
|
174
|
+
The Henosia Auth middleware delegates to the auth route handler, which
|
|
175
|
+
exchanges the Henosia Auth token for a Supabase session before protected
|
|
176
|
+
requests render and during session refresh — no extra wiring required. See
|
|
177
|
+
README "5. (Optional) Supabase helpers".
|
|
177
178
|
13. **Protect every non-public page, layout, server action, and route
|
|
178
179
|
handler.** Use the **`henosia-auth-guards` skill**. The middleware is a
|
|
179
|
-
pre-check only — it does **not** satisfy the
|
|
180
|
-
requirement.
|
|
180
|
+
pre-check and session-sync entrypoint only — it does **not** satisfy the
|
|
181
|
+
per-route authorization requirement.
|
|
181
182
|
14. Carefully remove the old auth flow components and routes that Henosia Auth replaces
|
|
182
183
|
|
|
183
184
|
## Subpath imports cheat sheet
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @henosia/app-next
|
|
2
2
|
|
|
3
|
-
Henosia integration for Next.js apps: Provides middleware
|
|
3
|
+
Henosia integration for Next.js apps: Provides edge-safe middleware, better-auth setup for Henosia Auth, Henosia Platform API route handlers, and (optionally) automatic Supabase sign in.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -133,13 +133,18 @@ import {
|
|
|
133
133
|
```
|
|
134
134
|
|
|
135
135
|
`requireHenosiaAuth` or `routeWithHenosiaAuth` MUST be called from any protected page/route handler/server action — the
|
|
136
|
-
middleware only
|
|
136
|
+
middleware is only the request pre-check and session-sync entrypoint. It keeps the middleware bundle Edge-safe by
|
|
137
|
+
delegating token refresh, organization-context cookie updates, and optional Supabase session exchange to the
|
|
138
|
+
`/api/auth/henosia-session-sync` route handled by the auth catch-all route.
|
|
139
|
+
|
|
140
|
+
The sync route is mounted automatically when you re-export `GET, POST` from `@henosia/app-next/api/auth`; no extra app
|
|
141
|
+
route is required.
|
|
137
142
|
|
|
138
143
|
## App-switcher hook
|
|
139
144
|
|
|
140
145
|
`@henosia/app-next/platform/app-switcher` exposes the `useAppSwitcher` React hook used to build a custom
|
|
141
|
-
app-switcher UI. It reads the current organization context from the
|
|
142
|
-
|
|
146
|
+
app-switcher UI. It reads the current organization context from the catch-all platform route mounted in
|
|
147
|
+
[step 3](#3-henosia-platform-catch-all-route), with the cached organization cookie as an immediate fallback.
|
|
143
148
|
|
|
144
149
|
Requires a `@tanstack/react-query` `QueryClientProvider` higher up in the tree.
|
|
145
150
|
|
package/dist/api/auth.d.mts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
|
|
2
|
+
import { NextRequest } from "next/server.js";
|
|
3
|
+
|
|
2
4
|
//#region src/api/auth.d.ts
|
|
3
|
-
declare const GET: (request:
|
|
5
|
+
declare const GET: (request: NextRequest) => Promise<Response>;
|
|
4
6
|
declare const POST: (request: Request) => Promise<Response>;
|
|
5
7
|
//#endregion
|
|
6
8
|
export { GET, POST };
|
package/dist/api/auth.mjs
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
/*! Copyright (c) 2026 Henosia ApS. Licensed under the Henosia Commercial Source License v1.0. See LICENSE */
|
|
2
|
-
import
|
|
2
|
+
import "../shared.mjs";
|
|
3
|
+
import { o as getSetCookieHeaders } from "../middleware-shared-CZbIZuBw.mjs";
|
|
4
|
+
import { auth, isUnauthorizedException, verifyHenosiaAuthToken } from "../auth/server.mjs";
|
|
5
|
+
import { n as syncHenosiaAuthSession, t as handleHenosiaAuthSessionSync } from "../session-sync-Cva_oreV.mjs";
|
|
3
6
|
import { toNextJsHandler } from "better-auth/next-js";
|
|
7
|
+
import { NextResponse } from "next/server.js";
|
|
4
8
|
//#region src/api/auth.ts
|
|
5
9
|
/**
|
|
6
10
|
* Catch-all Next.js route handler for better-auth.
|
|
@@ -12,7 +16,33 @@ import { toNextJsHandler } from "better-auth/next-js";
|
|
|
12
16
|
* ```
|
|
13
17
|
*/
|
|
14
18
|
const handlers = toNextJsHandler(auth);
|
|
15
|
-
const GET =
|
|
19
|
+
const GET = async (request) => {
|
|
20
|
+
const { pathname } = new URL(request.url);
|
|
21
|
+
if (pathname === "/api/auth/henosia-session-sync") return handleHenosiaAuthSessionSync(request);
|
|
22
|
+
if (pathname !== "/api/auth/get-session") return handlers.GET(request);
|
|
23
|
+
let verified = null;
|
|
24
|
+
const refreshedCookieResponse = new NextResponse(null);
|
|
25
|
+
try {
|
|
26
|
+
verified = await verifyHenosiaAuthToken(request);
|
|
27
|
+
verified.transferBetterAuthCookiesToNext(request, refreshedCookieResponse);
|
|
28
|
+
} catch (e) {
|
|
29
|
+
if (!isUnauthorizedException(e)) {
|
|
30
|
+
console.error("[Henosia Auth] get-session refresh error", e);
|
|
31
|
+
throw e;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const betterAuthResponse = await handlers.GET(request);
|
|
35
|
+
const response = new NextResponse(betterAuthResponse.body, {
|
|
36
|
+
headers: betterAuthResponse.headers,
|
|
37
|
+
status: betterAuthResponse.status,
|
|
38
|
+
statusText: betterAuthResponse.statusText
|
|
39
|
+
});
|
|
40
|
+
if (verified) {
|
|
41
|
+
for (const setCookie of getSetCookieHeaders(refreshedCookieResponse.headers)) response.headers.append("set-cookie", setCookie);
|
|
42
|
+
await syncHenosiaAuthSession(request, response, verified, { transferBetterAuthCookies: false });
|
|
43
|
+
}
|
|
44
|
+
return response;
|
|
45
|
+
};
|
|
16
46
|
const POST = handlers.POST;
|
|
17
47
|
//#endregion
|
|
18
48
|
export { GET, POST };
|
package/dist/api/platform.mjs
CHANGED
|
@@ -1,31 +1,67 @@
|
|
|
1
1
|
/*! Copyright (c) 2026 Henosia ApS. Licensed under the Henosia Commercial Source License v1.0. See LICENSE */
|
|
2
|
+
import { HENOSIA_AUTH_INVALID_SESSION_BODY } from "../shared.mjs";
|
|
2
3
|
import { henosiaAuthConfig, isUnauthorizedException, verifyHenosiaAuthToken } from "../auth/server.mjs";
|
|
3
4
|
import { applySecurityHeaders } from "../auth/server-guards.mjs";
|
|
5
|
+
import { r as syncOrganizationContextCookie } from "../session-sync-Cva_oreV.mjs";
|
|
6
|
+
import { NextResponse } from "next/server.js";
|
|
4
7
|
//#region src/api/platform/v1/app-switcher.ts
|
|
5
8
|
/**
|
|
6
9
|
* Handler for the Henosia Platform `app-switcher` endpoint.
|
|
7
10
|
* Proxies the authenticated request to the Henosia Auth platform service using the verified id token.
|
|
8
11
|
*/
|
|
9
12
|
async function handleAppSwitcher(request) {
|
|
13
|
+
let verified = null;
|
|
10
14
|
try {
|
|
11
|
-
|
|
15
|
+
verified = await verifyHenosiaAuthToken(request);
|
|
12
16
|
const fetchUrl = new URL(henosiaAuthConfig.henosiaAuthPlatformServiceBaseUrl.get());
|
|
13
17
|
fetchUrl.pathname = request.nextUrl.pathname;
|
|
14
18
|
const response = await fetch(fetchUrl, {
|
|
15
19
|
method: "GET",
|
|
16
20
|
redirect: "error",
|
|
17
|
-
headers: { authorization: `Bearer ${accessTokenResponse.idToken}` }
|
|
21
|
+
headers: { authorization: `Bearer ${verified.accessTokenResponse.idToken}` }
|
|
18
22
|
});
|
|
19
|
-
|
|
23
|
+
if (!response.ok) {
|
|
24
|
+
const nextResponse = createUpstreamResponse(response);
|
|
25
|
+
applySecurityHeaders(nextResponse);
|
|
26
|
+
return applyVerifiedAuthCookies(request, nextResponse, verified);
|
|
27
|
+
}
|
|
28
|
+
const body = await response.json();
|
|
29
|
+
const responseBody = isObjectRecord(body) ? body : {};
|
|
30
|
+
const nextResponse = NextResponse.json({
|
|
31
|
+
...responseBody,
|
|
32
|
+
organization: verified.payload["https://henosia.com/organization"] ?? null
|
|
33
|
+
}, {
|
|
20
34
|
status: response.status,
|
|
21
35
|
statusText: response.statusText
|
|
22
|
-
})
|
|
36
|
+
});
|
|
37
|
+
applySecurityHeaders(nextResponse);
|
|
38
|
+
return applyVerifiedAuthCookies(request, nextResponse, verified);
|
|
23
39
|
} catch (e) {
|
|
24
|
-
if (isUnauthorizedException(e)) return
|
|
40
|
+
if (isUnauthorizedException(e)) return applySecurityHeaders(NextResponse.json(HENOSIA_AUTH_INVALID_SESSION_BODY, { status: 401 }));
|
|
25
41
|
console.error("[Henosia Platform] App Switcher API error", e);
|
|
26
|
-
|
|
42
|
+
const response = NextResponse.json({ error: "Server error" }, { status: 500 });
|
|
43
|
+
applySecurityHeaders(response);
|
|
44
|
+
return verified ? applyVerifiedAuthCookies(request, response, verified) : response;
|
|
27
45
|
}
|
|
28
46
|
}
|
|
47
|
+
function applyVerifiedAuthCookies(request, response, verified) {
|
|
48
|
+
verified.transferBetterAuthCookiesToNext(request, response);
|
|
49
|
+
syncOrganizationContextCookie(request, response, verified.payload);
|
|
50
|
+
return response;
|
|
51
|
+
}
|
|
52
|
+
function createUpstreamResponse(response) {
|
|
53
|
+
const headers = new Headers();
|
|
54
|
+
const contentType = response.headers.get("content-type");
|
|
55
|
+
if (contentType) headers.set("content-type", contentType);
|
|
56
|
+
return new NextResponse(response.body, {
|
|
57
|
+
headers,
|
|
58
|
+
status: response.status,
|
|
59
|
+
statusText: response.statusText
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function isObjectRecord(value) {
|
|
63
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
64
|
+
}
|
|
29
65
|
//#endregion
|
|
30
66
|
//#region src/api/platform.ts
|
|
31
67
|
/**
|
|
@@ -23,31 +23,16 @@ interface HenosiaAuthMiddlewareOptions {
|
|
|
23
23
|
additionalUnauthenticatedPathNamePrefixes?: ReadonlyArray<string>;
|
|
24
24
|
}
|
|
25
25
|
/**
|
|
26
|
-
* Creates a Next.js middleware function that performs the Henosia Auth pre-check.
|
|
26
|
+
* Creates a Next.js middleware function that performs the Henosia Auth edge pre-check.
|
|
27
27
|
*
|
|
28
28
|
* The returned function:
|
|
29
|
-
* - Returns a redirect to the sign-in page when no
|
|
30
|
-
* -
|
|
31
|
-
* -
|
|
32
|
-
* -
|
|
33
|
-
* 401 JSON response otherwise) so the caller can compose additional logic on top.
|
|
29
|
+
* - Returns a redirect to the sign-in page when no session cookie is present.
|
|
30
|
+
* - Returns a 401 JSON response for non-GET requests without a session cookie.
|
|
31
|
+
* - Delegates token refresh, organization-context sync, and optional Supabase exchange to the server auth route.
|
|
32
|
+
* - Avoids importing the server auth graph so the middleware can be bundled for Edge runtimes.
|
|
34
33
|
*
|
|
35
34
|
* **Important**: this middleware performs an auth *pre-check*. Authorization MUST still be performed at the
|
|
36
|
-
* relevant pages and route handlers using
|
|
37
|
-
*
|
|
38
|
-
* @example Composing with custom logic
|
|
39
|
-
* ```ts
|
|
40
|
-
* import { type NextRequest, NextResponse } from 'next/server'
|
|
41
|
-
* import { createHenosiaAuthMiddleware } from '@henosia/app-next/middleware'
|
|
42
|
-
*
|
|
43
|
-
* const henosiaAuth = createHenosiaAuthMiddleware()
|
|
44
|
-
*
|
|
45
|
-
* export async function middleware(request: NextRequest) {
|
|
46
|
-
* // Add app-specific logic here ...
|
|
47
|
-
* return await henosiaAuth(request)
|
|
48
|
-
* }
|
|
49
|
-
*
|
|
50
|
-
* ```
|
|
35
|
+
* relevant pages and route handlers using `requireHenosiaAuth` or `routeWithHenosiaAuth` from `@henosia/app-next`.
|
|
51
36
|
*/
|
|
52
37
|
declare function createHenosiaAuthMiddleware(options?: HenosiaAuthMiddlewareOptions): (request: NextRequest) => Promise<NextResponse>;
|
|
53
38
|
//#endregion
|
package/dist/auth/middleware.mjs
CHANGED
|
@@ -1,57 +1,19 @@
|
|
|
1
1
|
/*! Copyright (c) 2026 Henosia ApS. Licensed under the Henosia Commercial Source License v1.0. See LICENSE */
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { getSessionCookie } from "better-auth/cookies";
|
|
2
|
+
import { HENOSIA_AUTH_INVALID_SESSION_BODY, HENOSIA_AUTH_SESSION_SYNC_ORIGINAL_FETCH_DEST_HEADER, HENOSIA_AUTH_SESSION_SYNC_ORIGINAL_METHOD_HEADER, HENOSIA_AUTH_SESSION_SYNC_ORIGINAL_URL_HEADER, HENOSIA_AUTH_SESSION_SYNC_PATH_NAME, HENOSIA_AUTH_SIGN_IN_PATH_NAME } from "../shared.mjs";
|
|
3
|
+
import { a as getHenosiaSessionCookie, c as removeCachedAccountData, n as HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAME_PREFIXES, o as getSetCookieHeaders, r as applySetCookieHeadersToRequestHeaders, s as isPageRequest, t as HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAMES } from "../middleware-shared-CZbIZuBw.mjs";
|
|
5
4
|
import { NextResponse } from "next/server.js";
|
|
6
|
-
//#region src/auth/utils.ts
|
|
7
|
-
function delayWithCancel(delayMs) {
|
|
8
|
-
let timer;
|
|
9
|
-
let reject = null;
|
|
10
|
-
const promise = new Promise((resolve, _reject) => {
|
|
11
|
-
reject = _reject;
|
|
12
|
-
timer = setTimeout(() => resolve(), delayMs);
|
|
13
|
-
});
|
|
14
|
-
const cancel = () => {
|
|
15
|
-
clearTimeout(timer);
|
|
16
|
-
queueMicrotask(() => {
|
|
17
|
-
reject();
|
|
18
|
-
reject = null;
|
|
19
|
-
});
|
|
20
|
-
};
|
|
21
|
-
return {
|
|
22
|
-
promise,
|
|
23
|
-
cancel
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
//#endregion
|
|
27
5
|
//#region src/auth/middleware.ts
|
|
28
|
-
const INVALID_SESSION_BODY = { error: "Your session is invalid or expired. Please reload the page to sign in again." };
|
|
29
6
|
/**
|
|
30
|
-
* Creates a Next.js middleware function that performs the Henosia Auth pre-check.
|
|
7
|
+
* Creates a Next.js middleware function that performs the Henosia Auth edge pre-check.
|
|
31
8
|
*
|
|
32
9
|
* The returned function:
|
|
33
|
-
* - Returns a redirect to the sign-in page when no
|
|
34
|
-
* -
|
|
35
|
-
* -
|
|
36
|
-
* -
|
|
37
|
-
* 401 JSON response otherwise) so the caller can compose additional logic on top.
|
|
10
|
+
* - Returns a redirect to the sign-in page when no session cookie is present.
|
|
11
|
+
* - Returns a 401 JSON response for non-GET requests without a session cookie.
|
|
12
|
+
* - Delegates token refresh, organization-context sync, and optional Supabase exchange to the server auth route.
|
|
13
|
+
* - Avoids importing the server auth graph so the middleware can be bundled for Edge runtimes.
|
|
38
14
|
*
|
|
39
15
|
* **Important**: this middleware performs an auth *pre-check*. Authorization MUST still be performed at the
|
|
40
|
-
* relevant pages and route handlers using
|
|
41
|
-
*
|
|
42
|
-
* @example Composing with custom logic
|
|
43
|
-
* ```ts
|
|
44
|
-
* import { type NextRequest, NextResponse } from 'next/server'
|
|
45
|
-
* import { createHenosiaAuthMiddleware } from '@henosia/app-next/middleware'
|
|
46
|
-
*
|
|
47
|
-
* const henosiaAuth = createHenosiaAuthMiddleware()
|
|
48
|
-
*
|
|
49
|
-
* export async function middleware(request: NextRequest) {
|
|
50
|
-
* // Add app-specific logic here ...
|
|
51
|
-
* return await henosiaAuth(request)
|
|
52
|
-
* }
|
|
53
|
-
*
|
|
54
|
-
* ```
|
|
16
|
+
* relevant pages and route handlers using `requireHenosiaAuth` or `routeWithHenosiaAuth` from `@henosia/app-next`.
|
|
55
17
|
*/
|
|
56
18
|
function createHenosiaAuthMiddleware(options = {}) {
|
|
57
19
|
const unauthenticatedPathNames = new Set([...HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAMES, ...options.additionalUnauthenticatedPathNames ?? []]);
|
|
@@ -59,98 +21,51 @@ function createHenosiaAuthMiddleware(options = {}) {
|
|
|
59
21
|
if (unauthenticatedPathNamePrefixes.find((p) => p === "/" || !p.trim() || !p.endsWith("/"))) throw new Error("[Henosia Auth] Invalid path name prefixes in additionalUnauthenticatedPathNamePrefixes. Values must be non-empty sub paths ending with slash");
|
|
60
22
|
if (unauthenticatedPathNames.has("/")) throw new Error(`[Henosia Auth] Invalid path name '/' in additionalUnauthenticatedPathNames. Values must be non-root paths`);
|
|
61
23
|
return async function henosiaAuthMiddleware(request) {
|
|
62
|
-
const response = NextResponse.next({ request });
|
|
63
24
|
const { pathname } = request.nextUrl;
|
|
64
|
-
if (pathname === "/sign-in")
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const { transferBetterAuthCookiesToNext } = await verifyHenosiaAuthToken(request);
|
|
68
|
-
transferBetterAuthCookiesToNext(request, response);
|
|
69
|
-
} catch (e) {
|
|
70
|
-
if (!isUnauthorizedException(e)) {
|
|
71
|
-
console.error("[Henosia Auth] Middleware error", e);
|
|
72
|
-
throw e;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
25
|
+
if (pathname === "/sign-in") {
|
|
26
|
+
const response = NextResponse.next({ request });
|
|
27
|
+
removeCachedAccountData(response);
|
|
75
28
|
return response;
|
|
76
29
|
}
|
|
77
|
-
if (unauthenticatedPathNames.has(pathname) || !!unauthenticatedPathNamePrefixes.find((p) => pathname.startsWith(p))) return
|
|
78
|
-
if (!
|
|
79
|
-
if (request.method !== "GET") return NextResponse.json(
|
|
30
|
+
if (unauthenticatedPathNames.has(pathname) || !!unauthenticatedPathNamePrefixes.find((p) => pathname.startsWith(p))) return NextResponse.next({ request });
|
|
31
|
+
if (!getHenosiaSessionCookie(request)) {
|
|
32
|
+
if (request.method !== "GET") return NextResponse.json(HENOSIA_AUTH_INVALID_SESSION_BODY, { status: 401 });
|
|
80
33
|
return NextResponse.redirect(new URL(HENOSIA_AUTH_SIGN_IN_PATH_NAME, request.url));
|
|
81
34
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if (
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
return redirectResponse;
|
|
95
|
-
} else {
|
|
96
|
-
console.error("[Henosia Auth] Middleware error", e);
|
|
97
|
-
throw e;
|
|
98
|
-
}
|
|
35
|
+
const requestHeaders = new Headers(request.headers);
|
|
36
|
+
const syncResponse = await fetch(new URL(HENOSIA_AUTH_SESSION_SYNC_PATH_NAME, request.url), {
|
|
37
|
+
method: "GET",
|
|
38
|
+
headers: createSessionSyncHeaders(request),
|
|
39
|
+
cache: "no-store",
|
|
40
|
+
redirect: "manual"
|
|
41
|
+
});
|
|
42
|
+
if (syncResponse.status === 401) {
|
|
43
|
+
if (!isPageRequest(request)) return NextResponse.json(HENOSIA_AUTH_INVALID_SESSION_BODY, { status: 401 });
|
|
44
|
+
const redirectResponse = NextResponse.redirect(new URL(HENOSIA_AUTH_SIGN_IN_PATH_NAME, request.url));
|
|
45
|
+
removeCachedAccountData(redirectResponse);
|
|
46
|
+
return redirectResponse;
|
|
99
47
|
}
|
|
48
|
+
if (!syncResponse.ok) throw new Error(`[Henosia Auth] Session sync failed: ${syncResponse.status} ${syncResponse.statusText}`);
|
|
49
|
+
const setCookieHeaders = getSetCookieHeaders(syncResponse.headers);
|
|
50
|
+
applySetCookieHeadersToRequestHeaders(requestHeaders, setCookieHeaders);
|
|
51
|
+
const response = NextResponse.next({ request: { headers: requestHeaders } });
|
|
52
|
+
for (const setCookie of setCookieHeaders) response.headers.append("set-cookie", setCookie);
|
|
100
53
|
return response;
|
|
101
54
|
};
|
|
102
55
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
56
|
+
function createSessionSyncHeaders(request) {
|
|
57
|
+
const headers = new Headers();
|
|
58
|
+
const cookie = request.headers.get("cookie");
|
|
59
|
+
if (cookie) headers.set("cookie", cookie);
|
|
60
|
+
headers.set(HENOSIA_AUTH_SESSION_SYNC_ORIGINAL_URL_HEADER, request.url);
|
|
61
|
+
headers.set(HENOSIA_AUTH_SESSION_SYNC_ORIGINAL_METHOD_HEADER, request.method);
|
|
62
|
+
const fetchDest = request.headers.get("X-Henosia-Fetch-Dest") ?? request.headers.get("Sec-Fetch-Dest");
|
|
63
|
+
if (fetchDest) headers.set(HENOSIA_AUTH_SESSION_SYNC_ORIGINAL_FETCH_DEST_HEADER, fetchDest);
|
|
64
|
+
for (const header of ["RSC", "Next-Router-Prefetch"]) {
|
|
65
|
+
const value = request.headers.get(header);
|
|
66
|
+
if (value) headers.set(header, value);
|
|
113
67
|
}
|
|
114
|
-
|
|
115
|
-
getAll() {
|
|
116
|
-
return request.cookies.getAll();
|
|
117
|
-
},
|
|
118
|
-
setAll(cookiesToSet) {
|
|
119
|
-
cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value));
|
|
120
|
-
try {
|
|
121
|
-
cookiesToSet.forEach(({ name, value, options }) => response.cookies.set(name, value, options));
|
|
122
|
-
} catch {}
|
|
123
|
-
}
|
|
124
|
-
} });
|
|
125
|
-
const { promise: supabaseTimeout, cancel } = delayWithCancel(5e3);
|
|
126
|
-
await Promise.race([supabaseTimeout.then(() => {
|
|
127
|
-
console.error(`[Supabase] Timed out getting a valid Supabase session: ${process.env.NEXT_PUBLIC_SUPABASE_URL} may be paused or down.`);
|
|
128
|
-
}), (async () => {
|
|
129
|
-
try {
|
|
130
|
-
const { data } = await supabase.auth.getSession();
|
|
131
|
-
if (!data.session) await supabase.auth.signInWithIdToken({
|
|
132
|
-
provider: henosiaAuthConfig.henosiaAuthSupabaseProvider.get(),
|
|
133
|
-
token: idToken
|
|
134
|
-
});
|
|
135
|
-
} catch (error) {
|
|
136
|
-
console.error("[Supabase] Unable to get valid Supabase session", error);
|
|
137
|
-
} finally {
|
|
138
|
-
cancel();
|
|
139
|
-
}
|
|
140
|
-
})()]);
|
|
141
|
-
}
|
|
142
|
-
/**
|
|
143
|
-
* Removes potentially invalid cached account data from cookies before the next better-auth sign in attempt
|
|
144
|
-
*/
|
|
145
|
-
function removeCachedAccountData(response) {
|
|
146
|
-
const secure = henosiaAuthConfig.baseURL.get().startsWith("https");
|
|
147
|
-
const name = `${secure ? "__Secure-" : ""}${cookiePrefix}.account_data`;
|
|
148
|
-
response.cookies.delete({
|
|
149
|
-
name,
|
|
150
|
-
secure,
|
|
151
|
-
httpOnly: true,
|
|
152
|
-
domain: new URL(henosiaAuthConfig.baseURL.get()).hostname
|
|
153
|
-
});
|
|
68
|
+
return headers;
|
|
154
69
|
}
|
|
155
70
|
//#endregion
|
|
156
71
|
export { createHenosiaAuthMiddleware };
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/*! Copyright (c) 2026 Henosia ApS. Licensed under the Henosia Commercial Source License v1.0. See LICENSE */
|
|
2
2
|
import { HENOSIA_AUTH_SIGN_IN_PATH_NAME } from "../shared.mjs";
|
|
3
3
|
import { isUnauthorizedException, verifyHenosiaAuthToken } from "./server.mjs";
|
|
4
|
-
import {
|
|
4
|
+
import { NextResponse } from "next/server.js";
|
|
5
5
|
import { cache } from "react";
|
|
6
|
+
import { headers } from "next/headers.js";
|
|
6
7
|
import { redirect } from "next/navigation.js";
|
|
7
|
-
import { NextResponse } from "next/server.js";
|
|
8
8
|
//#region src/auth/server-guards.ts
|
|
9
9
|
async function buildRequestFromHeaders() {
|
|
10
10
|
const h = await headers();
|
package/dist/auth/server.d.mts
CHANGED
|
@@ -1,108 +1,3 @@
|
|
|
1
1
|
|
|
2
|
-
import { i as
|
|
3
|
-
import { Auth } from "better-auth";
|
|
4
|
-
import { NextRequest, NextResponse } from "next/server.js";
|
|
5
|
-
|
|
6
|
-
//#region src/auth/server.d.ts
|
|
7
|
-
/**
|
|
8
|
-
* URL pathname values that should not check for auth in middleware, e.g. the sign-in page.
|
|
9
|
-
* Additional pathname values can be added for public paths, but ONLY if they should not be intercepted by the
|
|
10
|
-
* middleware auth pre-check and redirect.
|
|
11
|
-
*/
|
|
12
|
-
declare const HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAMES: Set<string>;
|
|
13
|
-
/**
|
|
14
|
-
* URL pathname prefixes that should not be blocked by auth in middleware, e.g. the better-auth API routes used to sign in.
|
|
15
|
-
* Additional pathname prefixes can be added for public paths, but ONLY if they should not be intercepted by the
|
|
16
|
-
* middleware auth pre-check and redirect. Include the relevant `/` ending character to prevent unexpected partial
|
|
17
|
-
* route matches.
|
|
18
|
-
*/
|
|
19
|
-
declare const HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAME_PREFIXES: string[];
|
|
20
|
-
type EnvLiveGetString = {
|
|
21
|
-
get(): string;
|
|
22
|
-
};
|
|
23
|
-
interface HenosiaAuthConfig {
|
|
24
|
-
/** Name of the Henosia Auth OAuth2 provider */
|
|
25
|
-
provider: 'henosia';
|
|
26
|
-
/** The consuming app's own deployment or preview browser URL. Used as the better-auth base url */
|
|
27
|
-
baseURL: EnvLiveGetString;
|
|
28
|
-
/** The consuming app's own deployment or preview auth secret. Used as the better-auth secret */
|
|
29
|
-
secret: EnvLiveGetString;
|
|
30
|
-
/** OAuth client ID from HenosiaAuthClientService */
|
|
31
|
-
clientId: EnvLiveGetString;
|
|
32
|
-
/** OAuth client secret from HenosiaAuthClientService */
|
|
33
|
-
clientSecret: EnvLiveGetString;
|
|
34
|
-
/** The Henosia auth and platform service base URL, under which the OAuth client is registered */
|
|
35
|
-
henosiaAuthPlatformServiceBaseUrl: EnvLiveGetString;
|
|
36
|
-
/** The provider identifier that Henosia Auth is registered with as a OIDC-based Custom Provider in Supabase Auth */
|
|
37
|
-
henosiaAuthSupabaseProvider: EnvLiveGetString;
|
|
38
|
-
/**
|
|
39
|
-
* The project id that the JWT project claim should match in HenosiaAuthTokenClaims.
|
|
40
|
-
* Ensures that signed-in Henosia users cannot access projects that they have not been granted access to,
|
|
41
|
-
* for example that User A from Org A cannot access User B from Org B's project.
|
|
42
|
-
* */
|
|
43
|
-
projectId: EnvLiveGetString;
|
|
44
|
-
}
|
|
45
|
-
/**
|
|
46
|
-
* Environment-provided configuration for Henosia Auth.
|
|
47
|
-
* When the consuming app runs in the Henosia preview, the env vars are provided by the runtime environment and should not be
|
|
48
|
-
* hard-coded or present in the `.env.local` file. The Henosia Publish feature automatically sets production versions of
|
|
49
|
-
* the values on the deployed app. For self-publish flows or local development, see the `Environment settings`
|
|
50
|
-
* in the Henosia builder navbar for the relevant values. The preview values cannot be used for production deployments.
|
|
51
|
-
*/
|
|
52
|
-
declare const henosiaAuthConfig: HenosiaAuthConfig;
|
|
53
|
-
interface HenosiaAuthTokenClaims {
|
|
54
|
-
/** The project id that the claim is associated with. A `null` value indicates no project access was granted. */
|
|
55
|
-
'https://henosia.com/project': string | null;
|
|
56
|
-
/** Whether the claim is associated with Henosia's preview browser in the builder */
|
|
57
|
-
'https://henosia.com/preview': boolean;
|
|
58
|
-
/** The organization that the app belongs to */
|
|
59
|
-
'https://henosia.com/organization': HenosiaOrganizationContext;
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
62
|
-
* Gets whether the specified request is for a page (a top level page or iframe page)
|
|
63
|
-
*/
|
|
64
|
-
declare function isPageRequest(request: Request): boolean;
|
|
65
|
-
/**
|
|
66
|
-
* Gets whether the specified request is allowed to spend the single-use OIDC refresh token to obtain a new
|
|
67
|
-
* access token.
|
|
68
|
-
*
|
|
69
|
-
*/
|
|
70
|
-
declare function isPageOrRefreshTokenRequest(request: Request): boolean;
|
|
71
|
-
declare const cookiePrefix: `henosia-auth-${string}`;
|
|
72
|
-
/**
|
|
73
|
-
* The Henosia Auth instance.
|
|
74
|
-
*
|
|
75
|
-
* Typed as the minimal {@link Auth} type from better-auth (which is parameterised on the default
|
|
76
|
-
* {@link BetterAuthOptions}) rather than the deeply-inferred concrete type returned by {@link betterAuth}.
|
|
77
|
-
* The deeply-inferred graph references non-portable internals from `better-auth` and `zod` that cannot be
|
|
78
|
-
* named in published declarations (TS2883).
|
|
79
|
-
*/
|
|
80
|
-
declare const auth: Auth;
|
|
81
|
-
/**
|
|
82
|
-
* Verifies the Henosia Auth JWT on the current request, or throws in case it's missing or invalid.
|
|
83
|
-
* This method must be called before allowing access to any protected pages or routes,
|
|
84
|
-
* and before accessing data in server side page methods or actions.
|
|
85
|
-
* @param request the current request which provides auth headers
|
|
86
|
-
* @return the better-auth `getAccessToken` response, the verified token `payload` (a {@link HenosiaAuthTokenClaims}),
|
|
87
|
-
* the `Set-Cookie` headers produced while obtaining/refreshing the token, and a
|
|
88
|
-
* `transferBetterAuthCookiesToNext` helper that forwards those cookies onto a Next.js request/response pair
|
|
89
|
-
* @throws APIError when the current credentials are missing or invalid
|
|
90
|
-
*/
|
|
91
|
-
declare function verifyHenosiaAuthToken(request: Request): Promise<{
|
|
92
|
-
accessTokenResponse: {
|
|
93
|
-
accessToken: string;
|
|
94
|
-
accessTokenExpiresAt: Date | undefined;
|
|
95
|
-
scopes: string[];
|
|
96
|
-
idToken: string | undefined;
|
|
97
|
-
};
|
|
98
|
-
payload: HenosiaAuthTokenClaims;
|
|
99
|
-
setCookieHeaders: string[];
|
|
100
|
-
transferBetterAuthCookiesToNext: (nextRequest: NextRequest, response: NextResponse) => void;
|
|
101
|
-
}>;
|
|
102
|
-
/**
|
|
103
|
-
* Determines whether the specified exception should start the sign-in flow to authenticate the user
|
|
104
|
-
* @param error the error thrown by `verifyHenosiaAuthToken`
|
|
105
|
-
*/
|
|
106
|
-
declare function isUnauthorizedException(error: unknown): boolean;
|
|
107
|
-
//#endregion
|
|
2
|
+
import { a as isPageOrRefreshTokenRequest, c as HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAMES, d as isPageRequest, i as henosiaAuthConfig, l as HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAME_PREFIXES, n as HenosiaAuthTokenClaims, o as isUnauthorizedException, r as auth, s as verifyHenosiaAuthToken, t as HenosiaAuthConfig, u as cookiePrefix } from "../server-DfD6Dc91.mjs";
|
|
108
3
|
export { HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAMES, HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAME_PREFIXES, HenosiaAuthConfig, HenosiaAuthTokenClaims, auth, cookiePrefix, henosiaAuthConfig, isPageOrRefreshTokenRequest, isPageRequest, isUnauthorizedException, verifyHenosiaAuthToken };
|