@henosia/app-next 1.0.6 → 1.0.7

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.
@@ -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 (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.
19
+ - Henosia Auth middleware (pre-check + token refresh + optional Supabase session exchange).
20
+ - better-auth and Henosia Platform catch-all Route Handlers.
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, middleware-backed route-handler session sync, Supabase exchange behavior, app-switcher hook options), read:
30
+ exports, Supabase exchange behavior, app-switcher hook options), read:
31
31
 
32
32
  - `node_modules/@henosia/app-next/README.md`
33
33
 
@@ -171,21 +171,20 @@ 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 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".
174
+ The middleware automatically exchanges the Henosia Auth token for a
175
+ Supabase session no extra wiring required. See README "5. (Optional)
176
+ Supabase helpers".
178
177
  13. **Protect every non-public page, layout, server action, and route
179
178
  handler.** Use the **`henosia-auth-guards` skill**. The middleware is a
180
- pre-check and session-sync entrypoint only — it does **not** satisfy the
181
- per-route authorization requirement.
179
+ pre-check only — it does **not** satisfy the per-route authorization
180
+ requirement.
182
181
  14. Carefully remove the old auth flow components and routes that Henosia Auth replaces
183
182
 
184
183
  ## Subpath imports cheat sheet
185
184
 
186
185
  ```ts
187
186
  // Server-side helpers (advanced; prefer the auth guards)
188
- import { auth, verifyHenosiaAuthToken, isUnauthorizedException, isPageRequest, HENOSIA_AUTH_SIGN_IN_PATH_NAME } from '@henosia/app-next'
187
+ import { getAuth, verifyHenosiaAuthToken, isUnauthorizedException, isPageRequest, HENOSIA_AUTH_SIGN_IN_PATH_NAME } from '@henosia/app-next'
189
188
 
190
189
  // Shared constants & types (no runtime deps; safe to import from server or client)
191
190
  import { HENOSIA_AUTH_SIGN_IN_PATH_NAME, HENOSIA_AUTH_GET_SESSION_PATH_NAME, HENOSIA_ORGANIZATION_CTX_COOKIE } from '@henosia/app-next/shared'
@@ -214,7 +213,7 @@ import { createClient } from '@henosia/app-next/supabase/client'
214
213
 
215
214
  ## Lower-level helpers — usually not needed
216
215
 
217
- `auth`, `verifyHenosiaAuthToken`, `isUnauthorizedException`, `isPageRequest` are
216
+ `getAuth`, `verifyHenosiaAuthToken`, `isUnauthorizedException`, `isPageRequest` are
218
217
  exported from the package root for advanced/edge cases. For protecting pages
219
218
  and routes, **always prefer the guards** documented in the
220
219
  `henosia-auth-guards` skill. They encode the canonical redirect / 401 / 500
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @henosia/app-next
2
2
 
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.
3
+ Henosia integration for Next.js apps: Provides middleware and better-auth setup for Henosia Auth, Henosia Platform API route handlers, and (optionally) automatic Supabase sign in.
4
4
 
5
5
  ## Installation
6
6
 
@@ -112,7 +112,7 @@ import { createClient } from '@henosia/app-next/supabase/client'
112
112
 
113
113
  ```ts
114
114
  import {
115
- auth,
115
+ getAuth,
116
116
  verifyHenosiaAuthToken,
117
117
  isUnauthorizedException,
118
118
  isPageRequest,
@@ -133,18 +133,13 @@ import {
133
133
  ```
134
134
 
135
135
  `requireHenosiaAuth` or `routeWithHenosiaAuth` MUST be called from any protected page/route handler/server action — the
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.
136
+ middleware only performs a fast pre-check.
142
137
 
143
138
  ## App-switcher hook
144
139
 
145
140
  `@henosia/app-next/platform/app-switcher` exposes the `useAppSwitcher` React hook used to build a custom
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.
141
+ app-switcher UI. It reads the current organization context from the cookie set by the middleware and queries
142
+ the catch-all platform route mounted in [step 3](#3-henosia-platform-catch-all-route).
148
143
 
149
144
  Requires a `@tanstack/react-query` `QueryClientProvider` higher up in the tree.
150
145
 
@@ -178,7 +173,7 @@ export function MyAppSwitcher() {
178
173
 
179
174
  | Subpath | Purpose |
180
175
  |-------------------------------------------|----------------------------------------------------------------------------|
181
- | `@henosia/app-next` | Server-side helpers (`auth`, `verifyHenosiaAuthToken`, …) |
176
+ | `@henosia/app-next` | Server-side helpers (`getAuth`, `verifyHenosiaAuthToken`, …) |
182
177
  | `@henosia/app-next/shared` | Shared constants & types (no runtime deps) |
183
178
  | `@henosia/app-next/auth/middleware` | `createHenosiaAuthMiddleware` |
184
179
  | `@henosia/app-next/auth/server` | Lower-level server-side auth surface (re-exported by root) |
@@ -1,8 +1,6 @@
1
1
 
2
- import { NextRequest } from "next/server.js";
3
-
4
2
  //#region src/api/auth.d.ts
5
- declare const GET: (request: NextRequest) => Promise<Response>;
3
+ declare const GET: (request: Request) => Promise<Response>;
6
4
  declare const POST: (request: Request) => Promise<Response>;
7
5
  //#endregion
8
6
  export { GET, POST };
package/dist/api/auth.mjs CHANGED
@@ -1,10 +1,6 @@
1
1
  /*! Copyright (c) 2026 Henosia ApS. Licensed under the Henosia Commercial Source License v1.0. See LICENSE */
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";
2
+ import { r as getAuth } from "../server-DvvkIB4j.mjs";
6
3
  import { toNextJsHandler } from "better-auth/next-js";
7
- import { NextResponse } from "next/server.js";
8
4
  //#region src/api/auth.ts
9
5
  /**
10
6
  * Catch-all Next.js route handler for better-auth.
@@ -15,34 +11,8 @@ import { NextResponse } from "next/server.js";
15
11
  * export { GET, POST } from '@henosia/app-next/api/auth'
16
12
  * ```
17
13
  */
18
- const handlers = toNextJsHandler(auth);
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
- };
14
+ const handlers = toNextJsHandler(getAuth());
15
+ const GET = handlers.GET;
46
16
  const POST = handlers.POST;
47
17
  //#endregion
48
18
  export { GET, POST };
@@ -1,67 +1,31 @@
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";
3
- import { henosiaAuthConfig, isUnauthorizedException, verifyHenosiaAuthToken } from "../auth/server.mjs";
2
+ import { l as henosiaAuthConfig, o as isUnauthorizedException, s as verifyHenosiaAuthToken } from "../server-DvvkIB4j.mjs";
4
3
  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";
7
4
  //#region src/api/platform/v1/app-switcher.ts
8
5
  /**
9
6
  * Handler for the Henosia Platform `app-switcher` endpoint.
10
7
  * Proxies the authenticated request to the Henosia Auth platform service using the verified id token.
11
8
  */
12
9
  async function handleAppSwitcher(request) {
13
- let verified = null;
14
10
  try {
15
- verified = await verifyHenosiaAuthToken(request);
11
+ const { accessTokenResponse } = await verifyHenosiaAuthToken(request);
16
12
  const fetchUrl = new URL(henosiaAuthConfig.henosiaAuthPlatformServiceBaseUrl.get());
17
13
  fetchUrl.pathname = request.nextUrl.pathname;
18
14
  const response = await fetch(fetchUrl, {
19
15
  method: "GET",
20
16
  redirect: "error",
21
- headers: { authorization: `Bearer ${verified.accessTokenResponse.idToken}` }
17
+ headers: { authorization: `Bearer ${accessTokenResponse.idToken}` }
22
18
  });
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
- }, {
19
+ return applySecurityHeaders(Response.json(await response.json(), {
34
20
  status: response.status,
35
21
  statusText: response.statusText
36
- });
37
- applySecurityHeaders(nextResponse);
38
- return applyVerifiedAuthCookies(request, nextResponse, verified);
22
+ }));
39
23
  } catch (e) {
40
- if (isUnauthorizedException(e)) return applySecurityHeaders(NextResponse.json(HENOSIA_AUTH_INVALID_SESSION_BODY, { status: 401 }));
24
+ if (isUnauthorizedException(e)) return Response.json({ error: "Your session is invalid or expired. Please reload the page to sign in again." }, { status: 401 });
41
25
  console.error("[Henosia Platform] App Switcher API error", e);
42
- const response = NextResponse.json({ error: "Server error" }, { status: 500 });
43
- applySecurityHeaders(response);
44
- return verified ? applyVerifiedAuthCookies(request, response, verified) : response;
26
+ return Response.json({ error: "Server error" }, { status: 500 });
45
27
  }
46
28
  }
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
- }
65
29
  //#endregion
66
30
  //#region src/api/platform.ts
67
31
  /**
@@ -23,16 +23,31 @@ interface HenosiaAuthMiddlewareOptions {
23
23
  additionalUnauthenticatedPathNamePrefixes?: ReadonlyArray<string>;
24
24
  }
25
25
  /**
26
- * Creates a Next.js middleware function that performs the Henosia Auth edge pre-check.
26
+ * Creates a Next.js middleware function that performs the Henosia Auth pre-check.
27
27
  *
28
28
  * The returned function:
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.
29
+ * - Returns a redirect to the sign-in page when no valid session cookie is present.
30
+ * - Verifies the Henosia Auth JWT, transferring any refreshed cookies onto the response.
31
+ * - Optionally exchanges the Henosia Auth token for a Supabase session if `NEXT_PUBLIC_SUPABASE_URL` is set.
32
+ * - Returns the resulting `NextResponse` (a `NextResponse.next({ request })` when authenticated, a redirect or
33
+ * 401 JSON response otherwise) so the caller can compose additional logic on top.
33
34
  *
34
35
  * **Important**: this middleware performs an auth *pre-check*. Authorization MUST still be performed at the
35
- * relevant pages and route handlers using `requireHenosiaAuth` or `routeWithHenosiaAuth` from `@henosia/app-next`.
36
+ * relevant pages and route handlers using {@link verifyHenosiaAuthToken}.
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
+ * ```
36
51
  */
37
52
  declare function createHenosiaAuthMiddleware(options?: HenosiaAuthMiddlewareOptions): (request: NextRequest) => Promise<NextResponse>;
38
53
  //#endregion
@@ -1,19 +1,57 @@
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, 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";
2
+ import { HENOSIA_AUTH_SIGN_IN_PATH_NAME, HENOSIA_ORGANIZATION_CTX_COOKIE } from "../shared.mjs";
3
+ import { a as isPageRequest, c as cookiePrefix, l as henosiaAuthConfig, n as HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAME_PREFIXES, o as isUnauthorizedException, s as verifyHenosiaAuthToken, t as HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAMES } from "../server-DvvkIB4j.mjs";
4
+ import { getSessionCookie } from "better-auth/cookies";
4
5
  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
5
27
  //#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." };
6
29
  /**
7
- * Creates a Next.js middleware function that performs the Henosia Auth edge pre-check.
30
+ * Creates a Next.js middleware function that performs the Henosia Auth pre-check.
8
31
  *
9
32
  * The returned function:
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.
33
+ * - Returns a redirect to the sign-in page when no valid session cookie is present.
34
+ * - Verifies the Henosia Auth JWT, transferring any refreshed cookies onto the response.
35
+ * - Optionally exchanges the Henosia Auth token for a Supabase session if `NEXT_PUBLIC_SUPABASE_URL` is set.
36
+ * - Returns the resulting `NextResponse` (a `NextResponse.next({ request })` when authenticated, a redirect or
37
+ * 401 JSON response otherwise) so the caller can compose additional logic on top.
14
38
  *
15
39
  * **Important**: this middleware performs an auth *pre-check*. Authorization MUST still be performed at the
16
- * relevant pages and route handlers using `requireHenosiaAuth` or `routeWithHenosiaAuth` from `@henosia/app-next`.
40
+ * relevant pages and route handlers using {@link verifyHenosiaAuthToken}.
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
+ * ```
17
55
  */
18
56
  function createHenosiaAuthMiddleware(options = {}) {
19
57
  const unauthenticatedPathNames = new Set([...HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAMES, ...options.additionalUnauthenticatedPathNames ?? []]);
@@ -21,51 +59,98 @@ function createHenosiaAuthMiddleware(options = {}) {
21
59
  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");
22
60
  if (unauthenticatedPathNames.has("/")) throw new Error(`[Henosia Auth] Invalid path name '/' in additionalUnauthenticatedPathNames. Values must be non-root paths`);
23
61
  return async function henosiaAuthMiddleware(request) {
62
+ const response = NextResponse.next({ request });
24
63
  const { pathname } = request.nextUrl;
25
- if (pathname === "/sign-in") {
26
- const response = NextResponse.next({ request });
27
- removeCachedAccountData(response);
64
+ if (pathname === "/sign-in") removeCachedAccountData(response);
65
+ if (pathname === "/api/auth/get-session") {
66
+ try {
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
+ }
28
75
  return response;
29
76
  }
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 });
77
+ if (unauthenticatedPathNames.has(pathname) || !!unauthenticatedPathNamePrefixes.find((p) => pathname.startsWith(p))) return response;
78
+ if (!getSessionCookie(request, { cookiePrefix: cookiePrefix.get() })) {
79
+ if (request.method !== "GET") return NextResponse.json(INVALID_SESSION_BODY, { status: 401 });
33
80
  return NextResponse.redirect(new URL(HENOSIA_AUTH_SIGN_IN_PATH_NAME, request.url));
34
81
  }
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;
82
+ try {
83
+ const { accessTokenResponse, payload, transferBetterAuthCookiesToNext } = await verifyHenosiaAuthToken(request);
84
+ transferBetterAuthCookiesToNext(request, response);
85
+ const cachedOrg = JSON.stringify(request.cookies.get("henosia.org")?.value ?? null);
86
+ const org = JSON.stringify(payload["https://henosia.com/organization"]);
87
+ if (cachedOrg !== org) response.cookies.set(HENOSIA_ORGANIZATION_CTX_COOKIE, org);
88
+ if (process.env.NEXT_PUBLIC_SUPABASE_URL) await exchangeHenosiaTokenForSupabaseSession(request, response, accessTokenResponse.idToken);
89
+ } catch (e) {
90
+ if (isUnauthorizedException(e)) {
91
+ if (!isPageRequest(request)) return NextResponse.json(INVALID_SESSION_BODY, { status: e.statusCode ?? 401 });
92
+ const redirectResponse = NextResponse.redirect(new URL(HENOSIA_AUTH_SIGN_IN_PATH_NAME, request.url));
93
+ removeCachedAccountData(redirectResponse);
94
+ return redirectResponse;
95
+ } else {
96
+ console.error("[Henosia Auth] Middleware error", e);
97
+ throw e;
98
+ }
47
99
  }
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);
53
100
  return response;
54
101
  };
55
102
  }
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);
103
+ /**
104
+ * Lazy-load `@supabase/ssr` so apps that don't use Supabase don't have to install it.
105
+ */
106
+ async function exchangeHenosiaTokenForSupabaseSession(request, response, idToken) {
107
+ let createServerClient;
108
+ try {
109
+ ({createServerClient} = await import("@supabase/ssr"));
110
+ } catch (e) {
111
+ console.error("[Henosia Auth] NEXT_PUBLIC_SUPABASE_URL is set but `@supabase/ssr` is not installed. Install it as a peer dependency to enable Supabase session exchange.", e);
112
+ return;
67
113
  }
68
- return headers;
114
+ const supabase = createServerClient(process.env.NEXT_PUBLIC_SUPABASE_URL, process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY, { cookies: {
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.get()}.account_data`;
148
+ response.cookies.delete({
149
+ name,
150
+ secure,
151
+ httpOnly: true,
152
+ domain: new URL(henosiaAuthConfig.baseURL.get()).hostname
153
+ });
69
154
  }
70
155
  //#endregion
71
156
  export { createHenosiaAuthMiddleware };
@@ -1,5 +1,5 @@
1
1
 
2
- import { n as HenosiaAuthTokenClaims } from "../server-DfD6Dc91.mjs";
2
+ import { HenosiaAuthTokenClaims } from "./server.mjs";
3
3
  import { NextRequest, NextResponse } from "next/server.js";
4
4
 
5
5
  //#region src/auth/server-guards.d.ts
@@ -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
- import { isUnauthorizedException, verifyHenosiaAuthToken } from "./server.mjs";
4
- import { NextResponse } from "next/server.js";
5
- import { cache } from "react";
3
+ import { o as isUnauthorizedException, s as verifyHenosiaAuthToken } from "../server-DvvkIB4j.mjs";
6
4
  import { headers } from "next/headers.js";
5
+ import { cache } from "react";
7
6
  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();
@@ -1,3 +1,82 @@
1
1
 
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";
3
- export { HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAMES, HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAME_PREFIXES, HenosiaAuthConfig, HenosiaAuthTokenClaims, auth, cookiePrefix, henosiaAuthConfig, isPageOrRefreshTokenRequest, isPageRequest, isUnauthorizedException, verifyHenosiaAuthToken };
2
+ import { i as HenosiaOrganizationContext } from "../shared-Cddy8ptu.mjs";
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
+ interface HenosiaAuthTokenClaims {
21
+ /** The project id that the claim is associated with. A `null` value indicates no project access was granted. */
22
+ 'https://henosia.com/project': string | null;
23
+ /** Whether the claim is associated with Henosia's preview browser in the builder */
24
+ 'https://henosia.com/preview': boolean;
25
+ /** The organization that the app belongs to */
26
+ 'https://henosia.com/organization': HenosiaOrganizationContext;
27
+ }
28
+ /**
29
+ * Gets whether the specified request is for a page (a top level page or iframe page)
30
+ */
31
+ declare function isPageRequest(request: Request): boolean;
32
+ /**
33
+ * Gets whether the specified request is allowed to spend the single-use OIDC refresh token to obtain a new
34
+ * access token.
35
+ *
36
+ */
37
+ declare function isPageOrRefreshTokenRequest(request: Request): boolean;
38
+ /**
39
+ * Returns the singleton Henosia Auth instance, creating it on first call.
40
+ *
41
+ * The instance is created lazily so that importing this module does not eagerly read the required
42
+ * environment variables (see {@link henosiaAuthConfig}). This means modules that only need the
43
+ * configuration or shared helpers can be imported in environments where the full set of `BETTER_AUTH_*` /
44
+ * `HENOSIA_AUTH_*` env vars is not yet populated, and only call sites that actually invoke `getAuth()`
45
+ * will require them to be set.
46
+ *
47
+ * Subsequent calls return the same underlying better-auth instance.
48
+ *
49
+ * The return type is the minimal {@link Auth} type from better-auth (which is parameterised on the default
50
+ * `BetterAuthOptions`) rather than the deeply-inferred concrete type returned by {@link betterAuth}.
51
+ * The deeply-inferred graph references non-portable internals from `better-auth` and `zod` that cannot be
52
+ * named in published declarations (TS2883).
53
+ */
54
+ declare const getAuth: () => Auth;
55
+ /**
56
+ * Verifies the Henosia Auth JWT on the current request, or throws in case it's missing or invalid.
57
+ * This method must be called before allowing access to any protected pages or routes,
58
+ * and before accessing data in server side page methods or actions.
59
+ * @param request the current request which provides auth headers
60
+ * @return the better-auth `getAccessToken` response, the verified token `payload` (a {@link HenosiaAuthTokenClaims}),
61
+ * the `Set-Cookie` headers produced while obtaining/refreshing the token, and a
62
+ * `transferBetterAuthCookiesToNext` helper that forwards those cookies onto a Next.js request/response pair
63
+ * @throws APIError when the current credentials are missing or invalid
64
+ */
65
+ declare function verifyHenosiaAuthToken(request: Request): Promise<{
66
+ accessTokenResponse: {
67
+ accessToken: string;
68
+ accessTokenExpiresAt: Date | undefined;
69
+ scopes: string[];
70
+ idToken: string | undefined;
71
+ };
72
+ payload: HenosiaAuthTokenClaims;
73
+ setCookieHeaders: string[];
74
+ transferBetterAuthCookiesToNext: (nextRequest: NextRequest, response: NextResponse) => void;
75
+ }>;
76
+ /**
77
+ * Determines whether the specified exception should start the sign-in flow to authenticate the user
78
+ * @param error the error thrown by `verifyHenosiaAuthToken`
79
+ */
80
+ declare function isUnauthorizedException(error: unknown): boolean;
81
+ //#endregion
82
+ export { HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAMES, HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAME_PREFIXES, HenosiaAuthTokenClaims, getAuth, isPageOrRefreshTokenRequest, isPageRequest, isUnauthorizedException, verifyHenosiaAuthToken };