@henosia/app-next 1.0.5 → 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.
@@ -1,5 +1,6 @@
1
1
  /*! Copyright (c) 2026 Henosia ApS. Licensed under the Henosia Commercial Source License v1.0. See LICENSE */
2
- import { HENOSIA_AUTH_GET_SESSION_PATH_NAME, HENOSIA_AUTH_SIGN_IN_PATH_NAME } from "../shared.mjs";
2
+ import { HENOSIA_AUTH_GET_SESSION_PATH_NAME } from "../shared.mjs";
3
+ import { i as cookiePrefix, l as getRequiredEnvValue, n as HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAME_PREFIXES, s as isPageRequest, t as HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAMES } from "../middleware-shared-CZbIZuBw.mjs";
3
4
  import { APIError, betterAuth } from "better-auth";
4
5
  import { createAuthMiddleware } from "better-auth/api";
5
6
  import { bearer, genericOAuth, jwt } from "better-auth/plugins";
@@ -7,38 +8,8 @@ import { nextCookies } from "better-auth/next-js";
7
8
  import { verifyAccessToken } from "better-auth/oauth2";
8
9
  import { parseSetCookieHeader } from "better-auth/cookies";
9
10
  import { AsyncLocalStorage } from "node:async_hooks";
10
- import { headers } from "next/headers.js";
11
11
  //#region src/auth/server.ts
12
12
  /**
13
- * URL pathname values that should not check for auth in middleware, e.g. the sign-in page.
14
- * Additional pathname values can be added for public paths, but ONLY if they should not be intercepted by the
15
- * middleware auth pre-check and redirect.
16
- */
17
- const HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAMES = new Set([HENOSIA_AUTH_SIGN_IN_PATH_NAME]);
18
- if (HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAMES.has("/")) throw new Error(`[Henosia Auth] Invalid path name '/'. Values must be non-root paths`);
19
- /**
20
- * URL pathname prefixes that should not be blocked by auth in middleware, e.g. the better-auth API routes used to sign in.
21
- * Additional pathname prefixes can be added for public paths, but ONLY if they should not be intercepted by the
22
- * middleware auth pre-check and redirect. Include the relevant `/` ending character to prevent unexpected partial
23
- * route matches.
24
- */
25
- const HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAME_PREFIXES = ["/api/auth/", "/.well-known/"];
26
- if (HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAME_PREFIXES.find((p) => p === "/" || !p.trim() || !p.endsWith("/"))) throw new Error("[Henosia Auth] Invalid path name prefixes. Values must be non-empty sub paths ending with slash");
27
- const requiredEnvProxy = new Proxy(process.env, { get(target, property, receiver) {
28
- const value = Reflect.get(target, property, receiver);
29
- if (!value || typeof value !== "string") throw new Error(`[Henosia Auth] Missing required env var: ${property.toString()}`);
30
- return value;
31
- } });
32
- function getRequiredEnvValue(getter, fallback) {
33
- try {
34
- return { get: () => getter(requiredEnvProxy) };
35
- } catch (e) {
36
- if (fallback) return { get: () => fallback };
37
- console.log(e.message);
38
- throw e;
39
- }
40
- }
41
- /**
42
13
  * Environment-provided configuration for Henosia Auth.
43
14
  * When the consuming app runs in the Henosia preview, the env vars are provided by the runtime environment and should not be
44
15
  * hard-coded or present in the `.env.local` file. The Henosia Publish feature automatically sets production versions of
@@ -56,17 +27,6 @@ const henosiaAuthConfig = {
56
27
  projectId: getRequiredEnvValue((env) => env.HENOSIA_AUTH_PROJECT_ID)
57
28
  };
58
29
  /**
59
- * Gets whether the specified request is for a page (a top level page or iframe page)
60
- */
61
- function isPageRequest(request) {
62
- const { headers } = request;
63
- const dest = headers.get("X-Henosia-Fetch-Dest") ?? headers.get("Sec-Fetch-Dest");
64
- if (dest === "document" || dest === "iframe" || dest === "fencedframe") return true;
65
- const isRsc = request.headers.get("RSC") === "1";
66
- const isPrefetch = request.headers.get("Next-Router-Prefetch") === "1";
67
- return isRsc && !isPrefetch;
68
- }
69
- /**
70
30
  * Gets whether the specified request is allowed to spend the single-use OIDC refresh token to obtain a new
71
31
  * access token.
72
32
  *
@@ -77,7 +37,6 @@ function isPageOrRefreshTokenRequest(request) {
77
37
  }
78
38
  const currentRequestStorage = new AsyncLocalStorage();
79
39
  let henosiaOAuthProviderInitialized = false;
80
- const cookiePrefix = `henosia-auth-${henosiaAuthConfig.projectId.get()}`;
81
40
  const authInstance = betterAuth({
82
41
  baseURL: henosiaAuthConfig.baseURL.get(),
83
42
  secret: henosiaAuthConfig.secret.get(),
@@ -163,44 +122,45 @@ const auth = authInstance;
163
122
  * @throws APIError when the current credentials are missing or invalid
164
123
  */
165
124
  async function verifyHenosiaAuthToken(request) {
166
- currentRequestStorage.enterWith(request);
167
- const { response: accessTokenResponse, headers: accessTokenResponseHeaders } = await authInstance.api.getAccessToken({
168
- body: { providerId: henosiaAuthConfig.provider },
169
- headers: await headers(),
170
- returnHeaders: true
171
- });
172
- if (!accessTokenResponse.idToken) throw new APIError("UNAUTHORIZED", { message: "No id_token returned for openid scope" });
173
- const payload = await verifyAccessToken(accessTokenResponse.idToken, {
174
- jwksUrl: `${henosiaAuthConfig.henosiaAuthPlatformServiceBaseUrl.get()}/api/auth/jwks`,
175
- verifyOptions: {
176
- audience: henosiaAuthConfig.clientId.get(),
177
- issuer: `${henosiaAuthConfig.henosiaAuthPlatformServiceBaseUrl.get()}/api/auth`
178
- }
179
- });
180
- const { "https://henosia.com/project": projectId } = payload;
181
- if (!projectId) throw new APIError("UNAUTHORIZED", { message: "token does not grant access" });
182
- if (!henosiaAuthConfig.projectId) throw new APIError("INTERNAL_SERVER_ERROR", { message: "HENOSIA_AUTH_PROJECT_ID env var has not been set" });
183
- if (henosiaAuthConfig.projectId.get() !== projectId) throw new APIError("UNAUTHORIZED", { message: "token has invalid projectId" });
184
- const setCookieHeaders = accessTokenResponseHeaders.getSetCookie();
185
- const transferBetterAuthCookiesToNext = (nextRequest, response) => {
186
- for (const cookie of setCookieHeaders) parseSetCookieHeader(cookie).forEach((attributes, name) => {
187
- const { value, ...betterOptions } = attributes;
188
- const options = {
189
- ...betterOptions,
190
- maxAge: betterOptions["max-age"],
191
- httpOnly: betterOptions.httponly,
192
- sameSite: betterOptions.samesite
193
- };
194
- nextRequest.cookies.set(name, value);
195
- response.cookies.set(name, value, options);
125
+ return await currentRequestStorage.run(request, async () => {
126
+ const { response: accessTokenResponse, headers: accessTokenResponseHeaders } = await authInstance.api.getAccessToken({
127
+ body: { providerId: henosiaAuthConfig.provider },
128
+ headers: request.headers,
129
+ returnHeaders: true
196
130
  });
197
- };
198
- return {
199
- accessTokenResponse,
200
- payload,
201
- setCookieHeaders,
202
- transferBetterAuthCookiesToNext
203
- };
131
+ if (!accessTokenResponse.idToken) throw new APIError("UNAUTHORIZED", { message: "No id_token returned for openid scope" });
132
+ const payload = await verifyAccessToken(accessTokenResponse.idToken, {
133
+ jwksUrl: `${henosiaAuthConfig.henosiaAuthPlatformServiceBaseUrl.get()}/api/auth/jwks`,
134
+ verifyOptions: {
135
+ audience: henosiaAuthConfig.clientId.get(),
136
+ issuer: `${henosiaAuthConfig.henosiaAuthPlatformServiceBaseUrl.get()}/api/auth`
137
+ }
138
+ });
139
+ const { "https://henosia.com/project": projectId } = payload;
140
+ if (!projectId) throw new APIError("UNAUTHORIZED", { message: "token does not grant access" });
141
+ if (!henosiaAuthConfig.projectId) throw new APIError("INTERNAL_SERVER_ERROR", { message: "HENOSIA_AUTH_PROJECT_ID env var has not been set" });
142
+ if (henosiaAuthConfig.projectId.get() !== projectId) throw new APIError("UNAUTHORIZED", { message: "token has invalid projectId" });
143
+ const setCookieHeaders = accessTokenResponseHeaders.getSetCookie();
144
+ const transferBetterAuthCookiesToNext = (nextRequest, response) => {
145
+ for (const cookie of setCookieHeaders) parseSetCookieHeader(cookie).forEach((attributes, name) => {
146
+ const { value, ...betterOptions } = attributes;
147
+ const options = {
148
+ ...betterOptions,
149
+ maxAge: betterOptions["max-age"],
150
+ httpOnly: betterOptions.httponly,
151
+ sameSite: betterOptions.samesite
152
+ };
153
+ nextRequest.cookies.set(name, value);
154
+ response.cookies.set(name, value, options);
155
+ });
156
+ };
157
+ return {
158
+ accessTokenResponse,
159
+ payload,
160
+ setCookieHeaders,
161
+ transferBetterAuthCookiesToNext
162
+ };
163
+ });
204
164
  }
205
165
  const unauthorizedExceptionCodes = new Set(["ACCOUNT_NOT_FOUND", "FAILED_TO_GET_ACCESS_TOKEN"]);
206
166
  /**
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
 
2
- import { i as HenosiaOrganizationContext, n as HENOSIA_AUTH_SIGN_IN_PATH_NAME, r as HENOSIA_ORGANIZATION_CTX_COOKIE, t as HENOSIA_AUTH_GET_SESSION_PATH_NAME } from "./shared-BWt7Sysv.mjs";
3
- import { HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAMES, HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAME_PREFIXES, HenosiaAuthConfig, HenosiaAuthTokenClaims, auth, cookiePrefix, henosiaAuthConfig, isPageOrRefreshTokenRequest, isPageRequest, isUnauthorizedException, verifyHenosiaAuthToken } from "./auth/server.mjs";
2
+ import { c as HENOSIA_ORGANIZATION_CTX_COOKIE, l as HenosiaOrganizationContext, n as HENOSIA_AUTH_INVALID_SESSION_BODY, o as HENOSIA_AUTH_SESSION_SYNC_PATH_NAME, s as HENOSIA_AUTH_SIGN_IN_PATH_NAME, t as HENOSIA_AUTH_GET_SESSION_PATH_NAME } from "./shared-BWPBaubT.mjs";
3
+ 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";
4
4
  import { HenosiaAuthContext, getHenosiaAuth, requireHenosiaAuth, routeWithHenosiaAuth } from "./auth/server-guards.mjs";
5
- export { HENOSIA_AUTH_GET_SESSION_PATH_NAME, HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAMES, HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAME_PREFIXES, HENOSIA_AUTH_SIGN_IN_PATH_NAME, HENOSIA_ORGANIZATION_CTX_COOKIE, type HenosiaAuthConfig, type HenosiaAuthContext, type HenosiaAuthTokenClaims, type HenosiaOrganizationContext, auth, cookiePrefix, getHenosiaAuth, henosiaAuthConfig, isPageOrRefreshTokenRequest, isPageRequest, isUnauthorizedException, requireHenosiaAuth, routeWithHenosiaAuth, verifyHenosiaAuthToken };
5
+ export { HENOSIA_AUTH_GET_SESSION_PATH_NAME, HENOSIA_AUTH_INVALID_SESSION_BODY, HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAMES, HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAME_PREFIXES, HENOSIA_AUTH_SESSION_SYNC_PATH_NAME, HENOSIA_AUTH_SIGN_IN_PATH_NAME, HENOSIA_ORGANIZATION_CTX_COOKIE, type HenosiaAuthConfig, type HenosiaAuthContext, type HenosiaAuthTokenClaims, type HenosiaOrganizationContext, auth, cookiePrefix, getHenosiaAuth, henosiaAuthConfig, isPageOrRefreshTokenRequest, isPageRequest, isUnauthorizedException, requireHenosiaAuth, routeWithHenosiaAuth, verifyHenosiaAuthToken };
package/dist/index.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  /*! Copyright (c) 2026 Henosia ApS. Licensed under the Henosia Commercial Source License v1.0. See LICENSE */
2
- import { HENOSIA_AUTH_GET_SESSION_PATH_NAME, HENOSIA_AUTH_SIGN_IN_PATH_NAME, HENOSIA_ORGANIZATION_CTX_COOKIE } from "./shared.mjs";
3
- import { HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAMES, HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAME_PREFIXES, auth, cookiePrefix, henosiaAuthConfig, isPageOrRefreshTokenRequest, isPageRequest, isUnauthorizedException, verifyHenosiaAuthToken } from "./auth/server.mjs";
2
+ import { HENOSIA_AUTH_GET_SESSION_PATH_NAME, HENOSIA_AUTH_INVALID_SESSION_BODY, HENOSIA_AUTH_SESSION_SYNC_PATH_NAME, HENOSIA_AUTH_SIGN_IN_PATH_NAME, HENOSIA_ORGANIZATION_CTX_COOKIE } from "./shared.mjs";
3
+ import { i as cookiePrefix, n as HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAME_PREFIXES, s as isPageRequest, t as HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAMES } from "./middleware-shared-CZbIZuBw.mjs";
4
+ import { auth, henosiaAuthConfig, isPageOrRefreshTokenRequest, isUnauthorizedException, verifyHenosiaAuthToken } from "./auth/server.mjs";
4
5
  import { getHenosiaAuth, requireHenosiaAuth, routeWithHenosiaAuth } from "./auth/server-guards.mjs";
5
- export { HENOSIA_AUTH_GET_SESSION_PATH_NAME, HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAMES, HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAME_PREFIXES, HENOSIA_AUTH_SIGN_IN_PATH_NAME, HENOSIA_ORGANIZATION_CTX_COOKIE, auth, cookiePrefix, getHenosiaAuth, henosiaAuthConfig, isPageOrRefreshTokenRequest, isPageRequest, isUnauthorizedException, requireHenosiaAuth, routeWithHenosiaAuth, verifyHenosiaAuthToken };
6
+ export { HENOSIA_AUTH_GET_SESSION_PATH_NAME, HENOSIA_AUTH_INVALID_SESSION_BODY, HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAMES, HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAME_PREFIXES, HENOSIA_AUTH_SESSION_SYNC_PATH_NAME, HENOSIA_AUTH_SIGN_IN_PATH_NAME, HENOSIA_ORGANIZATION_CTX_COOKIE, auth, cookiePrefix, getHenosiaAuth, henosiaAuthConfig, isPageOrRefreshTokenRequest, isPageRequest, isUnauthorizedException, requireHenosiaAuth, routeWithHenosiaAuth, verifyHenosiaAuthToken };
@@ -0,0 +1,140 @@
1
+ /*! Copyright (c) 2026 Henosia ApS. Licensed under the Henosia Commercial Source License v1.0. See LICENSE */
2
+ import { HENOSIA_AUTH_SIGN_IN_PATH_NAME } from "./shared.mjs";
3
+ import "next/server.js";
4
+ //#region src/auth/env.ts
5
+ const requiredEnvProxy = new Proxy(process.env, { get(target, property, receiver) {
6
+ const value = Reflect.get(target, property, receiver);
7
+ if (!value || typeof value !== "string") throw new Error(`[Henosia Auth] Missing required env var: ${property.toString()}`);
8
+ return value;
9
+ } });
10
+ function getRequiredEnvValue(getter, fallback) {
11
+ return { get: () => {
12
+ try {
13
+ return getter(requiredEnvProxy);
14
+ } catch (e) {
15
+ if (fallback !== void 0) return fallback;
16
+ console.log(e.message);
17
+ throw e;
18
+ }
19
+ } };
20
+ }
21
+ //#endregion
22
+ //#region src/auth/middleware-shared.ts
23
+ /**
24
+ * URL pathname values that should not check for auth in middleware, e.g. the sign-in page.
25
+ * Additional pathname values can be added for public paths, but ONLY if they should not be intercepted by the
26
+ * middleware auth pre-check and redirect.
27
+ */
28
+ const HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAMES = new Set([HENOSIA_AUTH_SIGN_IN_PATH_NAME]);
29
+ if (HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAMES.has("/")) throw new Error(`[Henosia Auth] Invalid path name '/'. Values must be non-root paths`);
30
+ /**
31
+ * URL pathname prefixes that should not be blocked by auth in middleware, e.g. the better-auth API routes used to sign in.
32
+ * Additional pathname prefixes can be added for public paths, but ONLY if they should not be intercepted by the
33
+ * middleware auth pre-check and redirect. Include the relevant `/` ending character to prevent unexpected partial
34
+ * route matches.
35
+ */
36
+ const HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAME_PREFIXES = ["/api/auth/", "/.well-known/"];
37
+ if (HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAME_PREFIXES.find((p) => p === "/" || !p.trim() || !p.endsWith("/"))) throw new Error("[Henosia Auth] Invalid path name prefixes. Values must be non-empty sub paths ending with slash");
38
+ const middlewareConfig = {
39
+ baseURL: getRequiredEnvValue((env) => env.BETTER_AUTH_URL),
40
+ projectId: getRequiredEnvValue((env) => env.HENOSIA_AUTH_PROJECT_ID)
41
+ };
42
+ const cookiePrefix = `henosia-auth-${middlewareConfig.projectId.get()}`;
43
+ function parseCookies(cookieHeader) {
44
+ const cookieMap = /* @__PURE__ */ new Map();
45
+ for (const cookie of cookieHeader.split(/;\s*/)) {
46
+ const [name, value] = cookie.split(/=(.*)/s);
47
+ if (name) cookieMap.set(name, value);
48
+ }
49
+ return cookieMap;
50
+ }
51
+ /**
52
+ * Lightweight Better Auth session-cookie lookup for Next.js middleware.
53
+ *
54
+ * This intentionally mirrors Better Auth's `getSessionCookie` naming rules without importing
55
+ * `better-auth/cookies`, because that module imports JWT helpers at module scope and pulls the
56
+ * server auth graph into Edge middleware bundles.
57
+ */
58
+ function getHenosiaSessionCookie(request) {
59
+ const cookies = request.headers.get("cookie");
60
+ if (!cookies) return null;
61
+ const parsedCookie = parseCookies(cookies);
62
+ const getCookie = (name) => parsedCookie.get(name) || parsedCookie.get(`__Secure-${name}`);
63
+ return getCookie(`${cookiePrefix}.session_token`) || getCookie(`${cookiePrefix}-session_token`) || null;
64
+ }
65
+ /**
66
+ * Gets whether the specified request is for a page (a top level page or iframe page).
67
+ */
68
+ function isPageRequest(request) {
69
+ const { headers } = request;
70
+ const dest = headers.get("X-Henosia-Fetch-Dest") ?? headers.get("Sec-Fetch-Dest");
71
+ if (dest === "document" || dest === "iframe" || dest === "fencedframe") return true;
72
+ const isRsc = request.headers.get("RSC") === "1";
73
+ const isPrefetch = request.headers.get("Next-Router-Prefetch") === "1";
74
+ return isRsc && !isPrefetch;
75
+ }
76
+ function getSetCookieHeaders(headers) {
77
+ const getSetCookie = headers.getSetCookie?.();
78
+ if (getSetCookie?.length) return getSetCookie;
79
+ const setCookie = headers.get("set-cookie");
80
+ return setCookie ? splitSetCookieHeader(setCookie) : [];
81
+ }
82
+ function splitSetCookieHeader(setCookieHeader) {
83
+ const cookies = [];
84
+ let start = 0;
85
+ for (let i = 0; i < setCookieHeader.length; i += 1) {
86
+ if (setCookieHeader[i] !== ",") continue;
87
+ let cursor = i + 1;
88
+ while (cursor < setCookieHeader.length && setCookieHeader[cursor] === " ") cursor += 1;
89
+ while (cursor < setCookieHeader.length && setCookieHeader[cursor] !== "=" && setCookieHeader[cursor] !== ";" && setCookieHeader[cursor] !== ",") cursor += 1;
90
+ if (setCookieHeader[cursor] === "=") {
91
+ cookies.push(setCookieHeader.slice(start, i).trim());
92
+ start = i + 1;
93
+ }
94
+ }
95
+ const lastCookie = setCookieHeader.slice(start).trim();
96
+ if (lastCookie) cookies.push(lastCookie);
97
+ return cookies;
98
+ }
99
+ function applySetCookieHeadersToRequestHeaders(requestHeaders, setCookieHeaders) {
100
+ if (!setCookieHeaders.length) return;
101
+ const cookies = parseCookies(requestHeaders.get("cookie") ?? "");
102
+ for (const setCookie of setCookieHeaders) {
103
+ const parsed = parseSetCookie(setCookie);
104
+ if (!parsed) continue;
105
+ if (parsed.deleted) cookies.delete(parsed.name);
106
+ else cookies.set(parsed.name, parsed.value);
107
+ }
108
+ requestHeaders.set("cookie", Array.from(cookies.entries()).map(([name, value]) => `${name}=${value}`).join("; "));
109
+ }
110
+ function parseSetCookie(setCookie) {
111
+ const [cookiePair, ...attributes] = setCookie.split(";");
112
+ if (!cookiePair) return null;
113
+ const [name, value] = cookiePair.trim().split(/=(.*)/s);
114
+ if (!name) return null;
115
+ const deleted = attributes.some((attribute) => {
116
+ const normalized = attribute.trim().toLowerCase();
117
+ return normalized === "max-age=0" || normalized.startsWith("expires=thu, 01 jan 1970");
118
+ });
119
+ return {
120
+ name,
121
+ value: value ?? "",
122
+ deleted
123
+ };
124
+ }
125
+ /**
126
+ * Removes potentially invalid cached account data from cookies before the next better-auth sign in attempt.
127
+ */
128
+ function removeCachedAccountData(response) {
129
+ const baseURL = middlewareConfig.baseURL.get();
130
+ const secure = baseURL.startsWith("https");
131
+ const name = `${secure ? "__Secure-" : ""}${cookiePrefix}.account_data`;
132
+ response.cookies.delete({
133
+ name,
134
+ secure,
135
+ httpOnly: true,
136
+ domain: new URL(baseURL).hostname
137
+ });
138
+ }
139
+ //#endregion
140
+ export { getHenosiaSessionCookie as a, removeCachedAccountData as c, cookiePrefix as i, getRequiredEnvValue as l, HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAME_PREFIXES as n, getSetCookieHeaders as o, applySetCookieHeadersToRequestHeaders as r, isPageRequest as s, HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAMES as t };
@@ -1,5 +1,5 @@
1
1
 
2
- import { i as HenosiaOrganizationContext } from "../shared-BWt7Sysv.mjs";
2
+ import { l as HenosiaOrganizationContext } from "../shared-BWPBaubT.mjs";
3
3
 
4
4
  //#region src/platform/app-switcher.d.ts
5
5
  /**
@@ -23,10 +23,7 @@ interface AppSwitcherAppGroup {
23
23
  */
24
24
  interface AppSwitcherSuccessResponse {
25
25
  groupedApps: AppSwitcherAppGroup[];
26
- organization: {
27
- id: string;
28
- name: string;
29
- };
26
+ organization: HenosiaOrganizationContext;
30
27
  }
31
28
  /**
32
29
  * Error response shape returned by `/api/henosia-platform/v1/app-switcher`.
@@ -51,8 +48,8 @@ interface UseAppSwitcherOptions {
51
48
  */
52
49
  interface UseAppSwitcherResult {
53
50
  /**
54
- * The current organization context, derived from the {@link HENOSIA_ORGANIZATION_CTX_COOKIE} cookie set by the
55
- * Henosia Auth middleware. `null` until the cookie has been read (or if no organization is in context).
51
+ * The current organization context, derived from the app-switcher response or the cached
52
+ * {@link HENOSIA_ORGANIZATION_CTX_COOKIE} cookie. `null` until either source is available.
56
53
  */
57
54
  organization: HenosiaOrganizationContext;
58
55
  /**
@@ -71,9 +68,9 @@ interface UseAppSwitcherResult {
71
68
  /**
72
69
  * React hook backing the Henosia app-switcher UI.
73
70
  *
74
- * Reads the current organization context from the {@link HENOSIA_ORGANIZATION_CTX_COOKIE} cookie and queries
75
- * the Henosia Platform `app-switcher` endpoint mounted at `/api/henosia-platform/v1/app-switcher` (provided by
76
- * `@henosia/app-next/api/platform`).
71
+ * Reads the cached organization context from the {@link HENOSIA_ORGANIZATION_CTX_COOKIE} cookie and queries the
72
+ * Henosia Platform `app-switcher` endpoint mounted at `/api/henosia-platform/v1/app-switcher` (provided by
73
+ * `@henosia/app-next/api/platform`). Once loaded, the verified API response is preferred over the cached cookie.
77
74
  *
78
75
  *
79
76
  * @example
@@ -24,9 +24,9 @@ async function fetchAppSwitcher() {
24
24
  /**
25
25
  * React hook backing the Henosia app-switcher UI.
26
26
  *
27
- * Reads the current organization context from the {@link HENOSIA_ORGANIZATION_CTX_COOKIE} cookie and queries
28
- * the Henosia Platform `app-switcher` endpoint mounted at `/api/henosia-platform/v1/app-switcher` (provided by
29
- * `@henosia/app-next/api/platform`).
27
+ * Reads the cached organization context from the {@link HENOSIA_ORGANIZATION_CTX_COOKIE} cookie and queries the
28
+ * Henosia Platform `app-switcher` endpoint mounted at `/api/henosia-platform/v1/app-switcher` (provided by
29
+ * `@henosia/app-next/api/platform`). Once loaded, the verified API response is preferred over the cached cookie.
30
30
  *
31
31
  *
32
32
  * @example
@@ -64,7 +64,7 @@ function useAppSwitcher(options) {
64
64
  enabled
65
65
  });
66
66
  return {
67
- organization,
67
+ organization: data?.organization ?? organization,
68
68
  groupedApps: data?.groupedApps ?? [],
69
69
  error,
70
70
  isLoading
@@ -0,0 +1,112 @@
1
+
2
+ import { l as HenosiaOrganizationContext } from "./shared-BWPBaubT.mjs";
3
+ import { Auth } from "better-auth";
4
+ import { NextRequest, NextResponse } from "next/server.js";
5
+
6
+ //#region src/auth/env.d.ts
7
+ type EnvLiveGetString = {
8
+ get(): string;
9
+ };
10
+ //#endregion
11
+ //#region src/auth/middleware-shared.d.ts
12
+ /**
13
+ * URL pathname values that should not check for auth in middleware, e.g. the sign-in page.
14
+ * Additional pathname values can be added for public paths, but ONLY if they should not be intercepted by the
15
+ * middleware auth pre-check and redirect.
16
+ */
17
+ declare const HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAMES: Set<string>;
18
+ /**
19
+ * URL pathname prefixes that should not be blocked by auth in middleware, e.g. the better-auth API routes used to sign in.
20
+ * Additional pathname prefixes can be added for public paths, but ONLY if they should not be intercepted by the
21
+ * middleware auth pre-check and redirect. Include the relevant `/` ending character to prevent unexpected partial
22
+ * route matches.
23
+ */
24
+ declare const HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAME_PREFIXES: string[];
25
+ declare const cookiePrefix: `henosia-auth-${string}`;
26
+ /**
27
+ * Gets whether the specified request is for a page (a top level page or iframe page).
28
+ */
29
+ declare function isPageRequest(request: Request): boolean;
30
+ //#endregion
31
+ //#region src/auth/server.d.ts
32
+ interface HenosiaAuthConfig {
33
+ /** Name of the Henosia Auth OAuth2 provider */
34
+ provider: 'henosia';
35
+ /** The consuming app's own deployment or preview browser URL. Used as the better-auth base url */
36
+ baseURL: EnvLiveGetString;
37
+ /** The consuming app's own deployment or preview auth secret. Used as the better-auth secret */
38
+ secret: EnvLiveGetString;
39
+ /** OAuth client ID from HenosiaAuthClientService */
40
+ clientId: EnvLiveGetString;
41
+ /** OAuth client secret from HenosiaAuthClientService */
42
+ clientSecret: EnvLiveGetString;
43
+ /** The Henosia auth and platform service base URL, under which the OAuth client is registered */
44
+ henosiaAuthPlatformServiceBaseUrl: EnvLiveGetString;
45
+ /** The provider identifier that Henosia Auth is registered with as a OIDC-based Custom Provider in Supabase Auth */
46
+ henosiaAuthSupabaseProvider: EnvLiveGetString;
47
+ /**
48
+ * The project id that the JWT project claim should match in HenosiaAuthTokenClaims.
49
+ * Ensures that signed-in Henosia users cannot access projects that they have not been granted access to,
50
+ * for example that User A from Org A cannot access User B from Org B's project.
51
+ * */
52
+ projectId: EnvLiveGetString;
53
+ }
54
+ /**
55
+ * Environment-provided configuration for Henosia Auth.
56
+ * When the consuming app runs in the Henosia preview, the env vars are provided by the runtime environment and should not be
57
+ * hard-coded or present in the `.env.local` file. The Henosia Publish feature automatically sets production versions of
58
+ * the values on the deployed app. For self-publish flows or local development, see the `Environment settings`
59
+ * in the Henosia builder navbar for the relevant values. The preview values cannot be used for production deployments.
60
+ */
61
+ declare const henosiaAuthConfig: HenosiaAuthConfig;
62
+ interface HenosiaAuthTokenClaims {
63
+ /** The project id that the claim is associated with. A `null` value indicates no project access was granted. */
64
+ 'https://henosia.com/project': string | null;
65
+ /** Whether the claim is associated with Henosia's preview browser in the builder */
66
+ 'https://henosia.com/preview': boolean;
67
+ /** The organization that the app belongs to */
68
+ 'https://henosia.com/organization': HenosiaOrganizationContext;
69
+ }
70
+ /**
71
+ * Gets whether the specified request is allowed to spend the single-use OIDC refresh token to obtain a new
72
+ * access token.
73
+ *
74
+ */
75
+ declare function isPageOrRefreshTokenRequest(request: Request): boolean;
76
+ /**
77
+ * The Henosia Auth instance.
78
+ *
79
+ * Typed as the minimal {@link Auth} type from better-auth (which is parameterised on the default
80
+ * {@link BetterAuthOptions}) rather than the deeply-inferred concrete type returned by {@link betterAuth}.
81
+ * The deeply-inferred graph references non-portable internals from `better-auth` and `zod` that cannot be
82
+ * named in published declarations (TS2883).
83
+ */
84
+ declare const auth: Auth;
85
+ /**
86
+ * Verifies the Henosia Auth JWT on the current request, or throws in case it's missing or invalid.
87
+ * This method must be called before allowing access to any protected pages or routes,
88
+ * and before accessing data in server side page methods or actions.
89
+ * @param request the current request which provides auth headers
90
+ * @return the better-auth `getAccessToken` response, the verified token `payload` (a {@link HenosiaAuthTokenClaims}),
91
+ * the `Set-Cookie` headers produced while obtaining/refreshing the token, and a
92
+ * `transferBetterAuthCookiesToNext` helper that forwards those cookies onto a Next.js request/response pair
93
+ * @throws APIError when the current credentials are missing or invalid
94
+ */
95
+ declare function verifyHenosiaAuthToken(request: Request): Promise<{
96
+ accessTokenResponse: {
97
+ accessToken: string;
98
+ accessTokenExpiresAt: Date | undefined;
99
+ scopes: string[];
100
+ idToken: string | undefined;
101
+ };
102
+ payload: HenosiaAuthTokenClaims;
103
+ setCookieHeaders: string[];
104
+ transferBetterAuthCookiesToNext: (nextRequest: NextRequest, response: NextResponse) => void;
105
+ }>;
106
+ /**
107
+ * Determines whether the specified exception should start the sign-in flow to authenticate the user
108
+ * @param error the error thrown by `verifyHenosiaAuthToken`
109
+ */
110
+ declare function isUnauthorizedException(error: unknown): boolean;
111
+ //#endregion
112
+ export { isPageOrRefreshTokenRequest as a, HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAMES as c, isPageRequest as d, henosiaAuthConfig as i, HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAME_PREFIXES as l, HenosiaAuthTokenClaims as n, isUnauthorizedException as o, auth as r, verifyHenosiaAuthToken as s, HenosiaAuthConfig as t, cookiePrefix as u };
@@ -0,0 +1,114 @@
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_ORGANIZATION_CTX_COOKIE } from "./shared.mjs";
3
+ import { henosiaAuthConfig, isUnauthorizedException, verifyHenosiaAuthToken } from "./auth/server.mjs";
4
+ import { NextResponse } from "next/server.js";
5
+ //#region src/auth/utils.ts
6
+ function delayWithCancel(delayMs) {
7
+ let timer;
8
+ let reject = null;
9
+ const promise = new Promise((resolve, _reject) => {
10
+ reject = _reject;
11
+ timer = setTimeout(() => resolve(), delayMs);
12
+ });
13
+ const cancel = () => {
14
+ clearTimeout(timer);
15
+ queueMicrotask(() => {
16
+ reject();
17
+ reject = null;
18
+ });
19
+ };
20
+ return {
21
+ promise,
22
+ cancel
23
+ };
24
+ }
25
+ //#endregion
26
+ //#region src/auth/session-sync.ts
27
+ function syncOrganizationContextCookie(request, response, payload) {
28
+ const cachedOrg = request.cookies.get("henosia.org")?.value ?? null;
29
+ const org = JSON.stringify(payload["https://henosia.com/organization"] ?? null);
30
+ if (cachedOrg !== org) response.cookies.set(HENOSIA_ORGANIZATION_CTX_COOKIE, org);
31
+ }
32
+ /**
33
+ * Transfers cookies and downstream sessions that require the server auth graph.
34
+ *
35
+ * This runs from Node route handlers directly or through the middleware's internal sync route so
36
+ * `@henosia/app-next/auth/middleware` remains safe to bundle for Edge runtimes.
37
+ */
38
+ async function syncHenosiaAuthSession(request, response, verified, options = {}) {
39
+ if (options.transferBetterAuthCookies ?? true) verified.transferBetterAuthCookiesToNext(request, response);
40
+ syncOrganizationContextCookie(request, response, verified.payload);
41
+ if (process.env.NEXT_PUBLIC_SUPABASE_URL) await exchangeHenosiaTokenForSupabaseSession(request, response, verified.accessTokenResponse.idToken);
42
+ }
43
+ /**
44
+ * Internal route handler used by middleware to perform server-only session sync before the matched request renders.
45
+ */
46
+ async function handleHenosiaAuthSessionSync(request) {
47
+ try {
48
+ const verified = await verifyHenosiaAuthToken(buildOriginalRequestForSessionSync(request));
49
+ const response = new NextResponse(null, { status: 204 });
50
+ await syncHenosiaAuthSession(request, response, verified);
51
+ return applySessionSyncResponseHeaders(response);
52
+ } catch (e) {
53
+ if (isUnauthorizedException(e)) return applySessionSyncResponseHeaders(NextResponse.json(HENOSIA_AUTH_INVALID_SESSION_BODY, { status: 401 }));
54
+ console.error("[Henosia Auth] session sync error", e);
55
+ return applySessionSyncResponseHeaders(NextResponse.json({ error: "Server error" }, { status: 500 }));
56
+ }
57
+ }
58
+ function applySessionSyncResponseHeaders(response) {
59
+ response.headers.set("Cache-Control", "private, no-store");
60
+ response.headers.set("Vary", "Cookie");
61
+ return response;
62
+ }
63
+ function buildOriginalRequestForSessionSync(request) {
64
+ const originalUrl = request.headers.get("x-henosia-auth-original-url") ?? request.url;
65
+ const originalMethod = request.headers.get("x-henosia-auth-original-method") ?? "GET";
66
+ const originalHeaders = new Headers(request.headers);
67
+ const originalFetchDest = request.headers.get(HENOSIA_AUTH_SESSION_SYNC_ORIGINAL_FETCH_DEST_HEADER);
68
+ if (originalFetchDest) originalHeaders.set("X-Henosia-Fetch-Dest", originalFetchDest);
69
+ return new Request(originalUrl, {
70
+ headers: originalHeaders,
71
+ method: originalMethod
72
+ });
73
+ }
74
+ /**
75
+ * Lazy-load `@supabase/ssr` so apps that don't use Supabase don't have to install it.
76
+ */
77
+ async function exchangeHenosiaTokenForSupabaseSession(request, response, idToken) {
78
+ let createServerClient;
79
+ try {
80
+ ({createServerClient} = await import("@supabase/ssr"));
81
+ } catch (e) {
82
+ 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);
83
+ return;
84
+ }
85
+ const supabase = createServerClient(process.env.NEXT_PUBLIC_SUPABASE_URL, process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY, { cookies: {
86
+ getAll() {
87
+ return request.cookies.getAll();
88
+ },
89
+ setAll(cookiesToSet) {
90
+ cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value));
91
+ try {
92
+ cookiesToSet.forEach(({ name, value, options }) => response.cookies.set(name, value, options));
93
+ } catch {}
94
+ }
95
+ } });
96
+ const { promise: supabaseTimeout, cancel } = delayWithCancel(5e3);
97
+ await Promise.race([supabaseTimeout.then(() => {
98
+ console.error(`[Supabase] Timed out getting a valid Supabase session: ${process.env.NEXT_PUBLIC_SUPABASE_URL} may be paused or down.`);
99
+ }), (async () => {
100
+ try {
101
+ const { data } = await supabase.auth.getSession();
102
+ if (!data.session) await supabase.auth.signInWithIdToken({
103
+ provider: henosiaAuthConfig.henosiaAuthSupabaseProvider.get(),
104
+ token: idToken
105
+ });
106
+ } catch (error) {
107
+ console.error("[Supabase] Unable to get valid Supabase session", error);
108
+ } finally {
109
+ cancel();
110
+ }
111
+ })()]);
112
+ }
113
+ //#endregion
114
+ export { syncHenosiaAuthSession as n, syncOrganizationContextCookie as r, handleHenosiaAuthSessionSync as t };
@@ -0,0 +1,30 @@
1
+
2
+ //#region src/shared.d.ts
3
+ declare const HENOSIA_AUTH_SIGN_IN_PATH_NAME = "/sign-in";
4
+ /**
5
+ * The pathname for better-auth's `/get-session` endpoint, used by the React `useSession` hook.
6
+ *
7
+ * `useSession` automatically refetches this endpoint on browser tab focus (and optionally on a polling
8
+ * interval), which we leverage as a built-in keep-alive for the OIDC access/refresh tokens — see
9
+ * `isPageOrRefreshTokenRequest` and the middleware-backed session sync route.
10
+ */
11
+ declare const HENOSIA_AUTH_GET_SESSION_PATH_NAME = "/api/auth/get-session";
12
+ /**
13
+ * Internal route used by the Edge middleware to delegate server-only token refresh and downstream session sync
14
+ * without importing the Better Auth server graph into the middleware bundle.
15
+ */
16
+ declare const HENOSIA_AUTH_SESSION_SYNC_PATH_NAME = "/api/auth/henosia-session-sync";
17
+ declare const HENOSIA_AUTH_SESSION_SYNC_ORIGINAL_URL_HEADER = "x-henosia-auth-original-url";
18
+ declare const HENOSIA_AUTH_SESSION_SYNC_ORIGINAL_METHOD_HEADER = "x-henosia-auth-original-method";
19
+ declare const HENOSIA_AUTH_SESSION_SYNC_ORIGINAL_FETCH_DEST_HEADER = "x-henosia-auth-original-fetch-dest";
20
+ type HenosiaOrganizationContext = {
21
+ id: string;
22
+ name: string;
23
+ logoUrl: string | null;
24
+ } | null;
25
+ declare const HENOSIA_ORGANIZATION_CTX_COOKIE = "henosia.org";
26
+ declare const HENOSIA_AUTH_INVALID_SESSION_BODY: {
27
+ error: string;
28
+ };
29
+ //#endregion
30
+ export { HENOSIA_AUTH_SESSION_SYNC_ORIGINAL_URL_HEADER as a, HENOSIA_ORGANIZATION_CTX_COOKIE as c, HENOSIA_AUTH_SESSION_SYNC_ORIGINAL_METHOD_HEADER as i, HenosiaOrganizationContext as l, HENOSIA_AUTH_INVALID_SESSION_BODY as n, HENOSIA_AUTH_SESSION_SYNC_PATH_NAME as o, HENOSIA_AUTH_SESSION_SYNC_ORIGINAL_FETCH_DEST_HEADER as r, HENOSIA_AUTH_SIGN_IN_PATH_NAME as s, HENOSIA_AUTH_GET_SESSION_PATH_NAME as t };
package/dist/shared.d.mts CHANGED
@@ -1,3 +1,3 @@
1
1
 
2
- import { i as HenosiaOrganizationContext, n as HENOSIA_AUTH_SIGN_IN_PATH_NAME, r as HENOSIA_ORGANIZATION_CTX_COOKIE, t as HENOSIA_AUTH_GET_SESSION_PATH_NAME } from "./shared-BWt7Sysv.mjs";
3
- export { HENOSIA_AUTH_GET_SESSION_PATH_NAME, HENOSIA_AUTH_SIGN_IN_PATH_NAME, HENOSIA_ORGANIZATION_CTX_COOKIE, HenosiaOrganizationContext };
2
+ import { a as HENOSIA_AUTH_SESSION_SYNC_ORIGINAL_URL_HEADER, c as HENOSIA_ORGANIZATION_CTX_COOKIE, i as HENOSIA_AUTH_SESSION_SYNC_ORIGINAL_METHOD_HEADER, l as HenosiaOrganizationContext, n as HENOSIA_AUTH_INVALID_SESSION_BODY, o as HENOSIA_AUTH_SESSION_SYNC_PATH_NAME, r as HENOSIA_AUTH_SESSION_SYNC_ORIGINAL_FETCH_DEST_HEADER, s as HENOSIA_AUTH_SIGN_IN_PATH_NAME, t as HENOSIA_AUTH_GET_SESSION_PATH_NAME } from "./shared-BWPBaubT.mjs";
3
+ export { HENOSIA_AUTH_GET_SESSION_PATH_NAME, 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, HENOSIA_ORGANIZATION_CTX_COOKIE, HenosiaOrganizationContext };