@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.
@@ -1,140 +0,0 @@
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,112 +0,0 @@
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 };
@@ -1,114 +0,0 @@
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 };
@@ -1,30 +0,0 @@
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 };