@henosia/app-next 1.0.5 → 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.
- package/.agents/skills/henosia-app-next/SKILL.md +2 -2
- package/README.md +2 -2
- package/dist/api/auth.mjs +2 -2
- package/dist/api/platform.mjs +1 -1
- package/dist/auth/middleware.mjs +3 -3
- package/dist/auth/server-guards.mjs +1 -1
- package/dist/auth/server.d.mts +14 -40
- package/dist/auth/server.mjs +2 -214
- package/dist/index.d.mts +49 -3
- package/dist/index.mjs +2 -2
- package/dist/platform/app-switcher.d.mts +1 -1
- package/dist/server-DvvkIB4j.mjs +232 -0
- package/dist/shared.d.mts +1 -1
- package/package.json +1 -1
- /package/dist/{shared-BWt7Sysv.d.mts → shared-Cddy8ptu.d.mts} +0 -0
|
@@ -184,7 +184,7 @@ README section.
|
|
|
184
184
|
|
|
185
185
|
```ts
|
|
186
186
|
// Server-side helpers (advanced; prefer the auth guards)
|
|
187
|
-
import {
|
|
187
|
+
import { getAuth, verifyHenosiaAuthToken, isUnauthorizedException, isPageRequest, HENOSIA_AUTH_SIGN_IN_PATH_NAME } from '@henosia/app-next'
|
|
188
188
|
|
|
189
189
|
// Shared constants & types (no runtime deps; safe to import from server or client)
|
|
190
190
|
import { HENOSIA_AUTH_SIGN_IN_PATH_NAME, HENOSIA_AUTH_GET_SESSION_PATH_NAME, HENOSIA_ORGANIZATION_CTX_COOKIE } from '@henosia/app-next/shared'
|
|
@@ -213,7 +213,7 @@ import { createClient } from '@henosia/app-next/supabase/client'
|
|
|
213
213
|
|
|
214
214
|
## Lower-level helpers — usually not needed
|
|
215
215
|
|
|
216
|
-
`
|
|
216
|
+
`getAuth`, `verifyHenosiaAuthToken`, `isUnauthorizedException`, `isPageRequest` are
|
|
217
217
|
exported from the package root for advanced/edge cases. For protecting pages
|
|
218
218
|
and routes, **always prefer the guards** documented in the
|
|
219
219
|
`henosia-auth-guards` skill. They encode the canonical redirect / 401 / 500
|
package/README.md
CHANGED
|
@@ -112,7 +112,7 @@ import { createClient } from '@henosia/app-next/supabase/client'
|
|
|
112
112
|
|
|
113
113
|
```ts
|
|
114
114
|
import {
|
|
115
|
-
|
|
115
|
+
getAuth,
|
|
116
116
|
verifyHenosiaAuthToken,
|
|
117
117
|
isUnauthorizedException,
|
|
118
118
|
isPageRequest,
|
|
@@ -173,7 +173,7 @@ export function MyAppSwitcher() {
|
|
|
173
173
|
|
|
174
174
|
| Subpath | Purpose |
|
|
175
175
|
|-------------------------------------------|----------------------------------------------------------------------------|
|
|
176
|
-
| `@henosia/app-next` | Server-side helpers (`
|
|
176
|
+
| `@henosia/app-next` | Server-side helpers (`getAuth`, `verifyHenosiaAuthToken`, …) |
|
|
177
177
|
| `@henosia/app-next/shared` | Shared constants & types (no runtime deps) |
|
|
178
178
|
| `@henosia/app-next/auth/middleware` | `createHenosiaAuthMiddleware` |
|
|
179
179
|
| `@henosia/app-next/auth/server` | Lower-level server-side auth surface (re-exported by root) |
|
package/dist/api/auth.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*! Copyright (c) 2026 Henosia ApS. Licensed under the Henosia Commercial Source License v1.0. See LICENSE */
|
|
2
|
-
import {
|
|
2
|
+
import { r as getAuth } from "../server-DvvkIB4j.mjs";
|
|
3
3
|
import { toNextJsHandler } from "better-auth/next-js";
|
|
4
4
|
//#region src/api/auth.ts
|
|
5
5
|
/**
|
|
@@ -11,7 +11,7 @@ import { toNextJsHandler } from "better-auth/next-js";
|
|
|
11
11
|
* export { GET, POST } from '@henosia/app-next/api/auth'
|
|
12
12
|
* ```
|
|
13
13
|
*/
|
|
14
|
-
const handlers = toNextJsHandler(
|
|
14
|
+
const handlers = toNextJsHandler(getAuth());
|
|
15
15
|
const GET = handlers.GET;
|
|
16
16
|
const POST = handlers.POST;
|
|
17
17
|
//#endregion
|
package/dist/api/platform.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*! Copyright (c) 2026 Henosia ApS. Licensed under the Henosia Commercial Source License v1.0. See LICENSE */
|
|
2
|
-
import { henosiaAuthConfig, isUnauthorizedException, verifyHenosiaAuthToken } from "../
|
|
2
|
+
import { l as henosiaAuthConfig, o as isUnauthorizedException, s as verifyHenosiaAuthToken } from "../server-DvvkIB4j.mjs";
|
|
3
3
|
import { applySecurityHeaders } from "../auth/server-guards.mjs";
|
|
4
4
|
//#region src/api/platform/v1/app-switcher.ts
|
|
5
5
|
/**
|
package/dist/auth/middleware.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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, HENOSIA_ORGANIZATION_CTX_COOKIE } from "../shared.mjs";
|
|
3
|
-
import {
|
|
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
4
|
import { getSessionCookie } from "better-auth/cookies";
|
|
5
5
|
import { NextResponse } from "next/server.js";
|
|
6
6
|
//#region src/auth/utils.ts
|
|
@@ -75,7 +75,7 @@ function createHenosiaAuthMiddleware(options = {}) {
|
|
|
75
75
|
return response;
|
|
76
76
|
}
|
|
77
77
|
if (unauthenticatedPathNames.has(pathname) || !!unauthenticatedPathNamePrefixes.find((p) => pathname.startsWith(p))) return response;
|
|
78
|
-
if (!getSessionCookie(request, { cookiePrefix })) {
|
|
78
|
+
if (!getSessionCookie(request, { cookiePrefix: cookiePrefix.get() })) {
|
|
79
79
|
if (request.method !== "GET") return NextResponse.json(INVALID_SESSION_BODY, { status: 401 });
|
|
80
80
|
return NextResponse.redirect(new URL(HENOSIA_AUTH_SIGN_IN_PATH_NAME, request.url));
|
|
81
81
|
}
|
|
@@ -144,7 +144,7 @@ async function exchangeHenosiaTokenForSupabaseSession(request, response, idToken
|
|
|
144
144
|
*/
|
|
145
145
|
function removeCachedAccountData(response) {
|
|
146
146
|
const secure = henosiaAuthConfig.baseURL.get().startsWith("https");
|
|
147
|
-
const name = `${secure ? "__Secure-" : ""}${cookiePrefix}.account_data`;
|
|
147
|
+
const name = `${secure ? "__Secure-" : ""}${cookiePrefix.get()}.account_data`;
|
|
148
148
|
response.cookies.delete({
|
|
149
149
|
name,
|
|
150
150
|
secure,
|
|
@@ -1,6 +1,6 @@
|
|
|
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 "
|
|
3
|
+
import { o as isUnauthorizedException, s as verifyHenosiaAuthToken } from "../server-DvvkIB4j.mjs";
|
|
4
4
|
import { headers } from "next/headers.js";
|
|
5
5
|
import { cache } from "react";
|
|
6
6
|
import { redirect } from "next/navigation.js";
|
package/dist/auth/server.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
import { i as HenosiaOrganizationContext } from "../shared-
|
|
2
|
+
import { i as HenosiaOrganizationContext } from "../shared-Cddy8ptu.mjs";
|
|
3
3
|
import { Auth } from "better-auth";
|
|
4
4
|
import { NextRequest, NextResponse } from "next/server.js";
|
|
5
5
|
|
|
@@ -17,39 +17,6 @@ declare const HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAMES: Set<string>;
|
|
|
17
17
|
* route matches.
|
|
18
18
|
*/
|
|
19
19
|
declare const HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAME_PREFIXES: string[];
|
|
20
|
-
type EnvLiveGetString = {
|
|
21
|
-
get(): string;
|
|
22
|
-
};
|
|
23
|
-
interface HenosiaAuthConfig {
|
|
24
|
-
/** Name of the Henosia Auth OAuth2 provider */
|
|
25
|
-
provider: 'henosia';
|
|
26
|
-
/** The consuming app's own deployment or preview browser URL. Used as the better-auth base url */
|
|
27
|
-
baseURL: EnvLiveGetString;
|
|
28
|
-
/** The consuming app's own deployment or preview auth secret. Used as the better-auth secret */
|
|
29
|
-
secret: EnvLiveGetString;
|
|
30
|
-
/** OAuth client ID from HenosiaAuthClientService */
|
|
31
|
-
clientId: EnvLiveGetString;
|
|
32
|
-
/** OAuth client secret from HenosiaAuthClientService */
|
|
33
|
-
clientSecret: EnvLiveGetString;
|
|
34
|
-
/** The Henosia auth and platform service base URL, under which the OAuth client is registered */
|
|
35
|
-
henosiaAuthPlatformServiceBaseUrl: EnvLiveGetString;
|
|
36
|
-
/** The provider identifier that Henosia Auth is registered with as a OIDC-based Custom Provider in Supabase Auth */
|
|
37
|
-
henosiaAuthSupabaseProvider: EnvLiveGetString;
|
|
38
|
-
/**
|
|
39
|
-
* The project id that the JWT project claim should match in HenosiaAuthTokenClaims.
|
|
40
|
-
* Ensures that signed-in Henosia users cannot access projects that they have not been granted access to,
|
|
41
|
-
* for example that User A from Org A cannot access User B from Org B's project.
|
|
42
|
-
* */
|
|
43
|
-
projectId: EnvLiveGetString;
|
|
44
|
-
}
|
|
45
|
-
/**
|
|
46
|
-
* Environment-provided configuration for Henosia Auth.
|
|
47
|
-
* When the consuming app runs in the Henosia preview, the env vars are provided by the runtime environment and should not be
|
|
48
|
-
* hard-coded or present in the `.env.local` file. The Henosia Publish feature automatically sets production versions of
|
|
49
|
-
* the values on the deployed app. For self-publish flows or local development, see the `Environment settings`
|
|
50
|
-
* in the Henosia builder navbar for the relevant values. The preview values cannot be used for production deployments.
|
|
51
|
-
*/
|
|
52
|
-
declare const henosiaAuthConfig: HenosiaAuthConfig;
|
|
53
20
|
interface HenosiaAuthTokenClaims {
|
|
54
21
|
/** The project id that the claim is associated with. A `null` value indicates no project access was granted. */
|
|
55
22
|
'https://henosia.com/project': string | null;
|
|
@@ -68,16 +35,23 @@ declare function isPageRequest(request: Request): boolean;
|
|
|
68
35
|
*
|
|
69
36
|
*/
|
|
70
37
|
declare function isPageOrRefreshTokenRequest(request: Request): boolean;
|
|
71
|
-
declare const cookiePrefix: `henosia-auth-${string}`;
|
|
72
38
|
/**
|
|
73
|
-
*
|
|
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.
|
|
74
48
|
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
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}.
|
|
77
51
|
* The deeply-inferred graph references non-portable internals from `better-auth` and `zod` that cannot be
|
|
78
52
|
* named in published declarations (TS2883).
|
|
79
53
|
*/
|
|
80
|
-
declare const
|
|
54
|
+
declare const getAuth: () => Auth;
|
|
81
55
|
/**
|
|
82
56
|
* Verifies the Henosia Auth JWT on the current request, or throws in case it's missing or invalid.
|
|
83
57
|
* This method must be called before allowing access to any protected pages or routes,
|
|
@@ -105,4 +79,4 @@ declare function verifyHenosiaAuthToken(request: Request): Promise<{
|
|
|
105
79
|
*/
|
|
106
80
|
declare function isUnauthorizedException(error: unknown): boolean;
|
|
107
81
|
//#endregion
|
|
108
|
-
export { HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAMES, HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAME_PREFIXES,
|
|
82
|
+
export { HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAMES, HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAME_PREFIXES, HenosiaAuthTokenClaims, getAuth, isPageOrRefreshTokenRequest, isPageRequest, isUnauthorizedException, verifyHenosiaAuthToken };
|
package/dist/auth/server.mjs
CHANGED
|
@@ -1,215 +1,3 @@
|
|
|
1
1
|
/*! Copyright (c) 2026 Henosia ApS. Licensed under the Henosia Commercial Source License v1.0. See LICENSE */
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
import { createAuthMiddleware } from "better-auth/api";
|
|
5
|
-
import { bearer, genericOAuth, jwt } from "better-auth/plugins";
|
|
6
|
-
import { nextCookies } from "better-auth/next-js";
|
|
7
|
-
import { verifyAccessToken } from "better-auth/oauth2";
|
|
8
|
-
import { parseSetCookieHeader } from "better-auth/cookies";
|
|
9
|
-
import { AsyncLocalStorage } from "node:async_hooks";
|
|
10
|
-
import { headers } from "next/headers.js";
|
|
11
|
-
//#region src/auth/server.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
|
-
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
|
-
* Environment-provided configuration for Henosia Auth.
|
|
43
|
-
* When the consuming app runs in the Henosia preview, the env vars are provided by the runtime environment and should not be
|
|
44
|
-
* hard-coded or present in the `.env.local` file. The Henosia Publish feature automatically sets production versions of
|
|
45
|
-
* the values on the deployed app. For self-publish flows or local development, see the `Environment settings`
|
|
46
|
-
* in the Henosia builder navbar for the relevant values. The preview values cannot be used for production deployments.
|
|
47
|
-
*/
|
|
48
|
-
const henosiaAuthConfig = {
|
|
49
|
-
provider: "henosia",
|
|
50
|
-
baseURL: getRequiredEnvValue((env) => env.BETTER_AUTH_URL),
|
|
51
|
-
secret: getRequiredEnvValue((env) => env.BETTER_AUTH_SECRET),
|
|
52
|
-
henosiaAuthPlatformServiceBaseUrl: getRequiredEnvValue((env) => env.HENOSIA_AUTH_SERVICE_BASE_URL),
|
|
53
|
-
henosiaAuthSupabaseProvider: getRequiredEnvValue((env) => env.HENOSIA_AUTH_SUPABASE_PROVIDER, "custom:henosia-auth:platform"),
|
|
54
|
-
clientSecret: getRequiredEnvValue((env) => env.HENOSIA_AUTH_CLIENT_SECRET),
|
|
55
|
-
clientId: getRequiredEnvValue((env) => env.HENOSIA_AUTH_CLIENT_ID),
|
|
56
|
-
projectId: getRequiredEnvValue((env) => env.HENOSIA_AUTH_PROJECT_ID)
|
|
57
|
-
};
|
|
58
|
-
/**
|
|
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
|
-
* Gets whether the specified request is allowed to spend the single-use OIDC refresh token to obtain a new
|
|
71
|
-
* access token.
|
|
72
|
-
*
|
|
73
|
-
*/
|
|
74
|
-
function isPageOrRefreshTokenRequest(request) {
|
|
75
|
-
if (isPageRequest(request)) return true;
|
|
76
|
-
return new URL(request.url).pathname === HENOSIA_AUTH_GET_SESSION_PATH_NAME;
|
|
77
|
-
}
|
|
78
|
-
const currentRequestStorage = new AsyncLocalStorage();
|
|
79
|
-
let henosiaOAuthProviderInitialized = false;
|
|
80
|
-
const cookiePrefix = `henosia-auth-${henosiaAuthConfig.projectId.get()}`;
|
|
81
|
-
const authInstance = betterAuth({
|
|
82
|
-
baseURL: henosiaAuthConfig.baseURL.get(),
|
|
83
|
-
secret: henosiaAuthConfig.secret.get(),
|
|
84
|
-
advanced: { cookiePrefix },
|
|
85
|
-
plugins: [
|
|
86
|
-
bearer(),
|
|
87
|
-
jwt(),
|
|
88
|
-
genericOAuth({ config: [{
|
|
89
|
-
providerId: henosiaAuthConfig.provider,
|
|
90
|
-
discoveryUrl: `${henosiaAuthConfig.henosiaAuthPlatformServiceBaseUrl.get()}/.well-known/openid-configuration`,
|
|
91
|
-
clientId: henosiaAuthConfig.clientId.get(),
|
|
92
|
-
clientSecret: henosiaAuthConfig.clientSecret.get(),
|
|
93
|
-
scopes: [
|
|
94
|
-
"openid",
|
|
95
|
-
"profile",
|
|
96
|
-
"email",
|
|
97
|
-
"offline_access"
|
|
98
|
-
],
|
|
99
|
-
pkce: true,
|
|
100
|
-
mapProfileToUser: (profile) => {
|
|
101
|
-
if (!profile.name) profile.name = profile.email;
|
|
102
|
-
return profile;
|
|
103
|
-
}
|
|
104
|
-
}] }),
|
|
105
|
-
nextCookies()
|
|
106
|
-
],
|
|
107
|
-
hooks: { before: createAuthMiddleware(async (ctx) => {
|
|
108
|
-
if (henosiaOAuthProviderInitialized) return;
|
|
109
|
-
for (const provider of ctx.context.socialProviders) {
|
|
110
|
-
const { refreshAccessToken } = provider;
|
|
111
|
-
if (!refreshAccessToken || provider.id !== henosiaAuthConfig.provider) continue;
|
|
112
|
-
provider.refreshAccessToken = async (refreshToken) => {
|
|
113
|
-
const request = currentRequestStorage.getStore();
|
|
114
|
-
if (!request) throw new APIError("INTERNAL_SERVER_ERROR", { message: `Expected a current request during refreshAccessToken` });
|
|
115
|
-
if (!isPageOrRefreshTokenRequest(request)) throw new APIError("UNAUTHORIZED", {
|
|
116
|
-
statusCode: 401,
|
|
117
|
-
message: "Fetching a refresh access token is only allowed for page or refresh token requests"
|
|
118
|
-
});
|
|
119
|
-
return await refreshAccessToken(refreshToken);
|
|
120
|
-
};
|
|
121
|
-
henosiaOAuthProviderInitialized = true;
|
|
122
|
-
break;
|
|
123
|
-
}
|
|
124
|
-
}) },
|
|
125
|
-
session: { cookieCache: {
|
|
126
|
-
enabled: true,
|
|
127
|
-
version: "1",
|
|
128
|
-
maxAge: 720 * 60 * 60,
|
|
129
|
-
strategy: "jwe",
|
|
130
|
-
refreshCache: true
|
|
131
|
-
} },
|
|
132
|
-
account: {
|
|
133
|
-
storeStateStrategy: "cookie",
|
|
134
|
-
storeAccountCookie: true
|
|
135
|
-
},
|
|
136
|
-
databaseHooks: { user: { create: { before: async (data) => {
|
|
137
|
-
const stableId = data.sub;
|
|
138
|
-
if (!stableId) throw new APIError("INTERNAL_SERVER_ERROR", { message: `Expected '.sub' to be defined but got '${stableId}' for '${data.email}'` });
|
|
139
|
-
return { data: {
|
|
140
|
-
...data,
|
|
141
|
-
id: stableId
|
|
142
|
-
} };
|
|
143
|
-
} } } },
|
|
144
|
-
trustedOrigins: [henosiaAuthConfig.henosiaAuthPlatformServiceBaseUrl.get()]
|
|
145
|
-
});
|
|
146
|
-
/**
|
|
147
|
-
* The Henosia Auth instance.
|
|
148
|
-
*
|
|
149
|
-
* Typed as the minimal {@link Auth} type from better-auth (which is parameterised on the default
|
|
150
|
-
* {@link BetterAuthOptions}) rather than the deeply-inferred concrete type returned by {@link betterAuth}.
|
|
151
|
-
* The deeply-inferred graph references non-portable internals from `better-auth` and `zod` that cannot be
|
|
152
|
-
* named in published declarations (TS2883).
|
|
153
|
-
*/
|
|
154
|
-
const auth = authInstance;
|
|
155
|
-
/**
|
|
156
|
-
* Verifies the Henosia Auth JWT on the current request, or throws in case it's missing or invalid.
|
|
157
|
-
* This method must be called before allowing access to any protected pages or routes,
|
|
158
|
-
* and before accessing data in server side page methods or actions.
|
|
159
|
-
* @param request the current request which provides auth headers
|
|
160
|
-
* @return the better-auth `getAccessToken` response, the verified token `payload` (a {@link HenosiaAuthTokenClaims}),
|
|
161
|
-
* the `Set-Cookie` headers produced while obtaining/refreshing the token, and a
|
|
162
|
-
* `transferBetterAuthCookiesToNext` helper that forwards those cookies onto a Next.js request/response pair
|
|
163
|
-
* @throws APIError when the current credentials are missing or invalid
|
|
164
|
-
*/
|
|
165
|
-
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);
|
|
196
|
-
});
|
|
197
|
-
};
|
|
198
|
-
return {
|
|
199
|
-
accessTokenResponse,
|
|
200
|
-
payload,
|
|
201
|
-
setCookieHeaders,
|
|
202
|
-
transferBetterAuthCookiesToNext
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
const unauthorizedExceptionCodes = new Set(["ACCOUNT_NOT_FOUND", "FAILED_TO_GET_ACCESS_TOKEN"]);
|
|
206
|
-
/**
|
|
207
|
-
* Determines whether the specified exception should start the sign-in flow to authenticate the user
|
|
208
|
-
* @param error the error thrown by `verifyHenosiaAuthToken`
|
|
209
|
-
*/
|
|
210
|
-
function isUnauthorizedException(error) {
|
|
211
|
-
const apiError = error ?? {};
|
|
212
|
-
return unauthorizedExceptionCodes.has(apiError.body?.code ?? "") || apiError.status === "UNAUTHORIZED" || apiError.statusCode === 401;
|
|
213
|
-
}
|
|
214
|
-
//#endregion
|
|
215
|
-
export { HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAMES, HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAME_PREFIXES, auth, cookiePrefix, henosiaAuthConfig, isPageOrRefreshTokenRequest, isPageRequest, isUnauthorizedException, verifyHenosiaAuthToken };
|
|
2
|
+
import { a as isPageRequest, i as isPageOrRefreshTokenRequest, n as HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAME_PREFIXES, o as isUnauthorizedException, r as getAuth, s as verifyHenosiaAuthToken, t as HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAMES } from "../server-DvvkIB4j.mjs";
|
|
3
|
+
export { HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAMES, HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAME_PREFIXES, getAuth, isPageOrRefreshTokenRequest, isPageRequest, isUnauthorizedException, verifyHenosiaAuthToken };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,51 @@
|
|
|
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-
|
|
3
|
-
import { HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAMES, HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAME_PREFIXES,
|
|
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-Cddy8ptu.mjs";
|
|
3
|
+
import { HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAMES, HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAME_PREFIXES, HenosiaAuthTokenClaims, getAuth, isPageOrRefreshTokenRequest, isPageRequest, isUnauthorizedException, verifyHenosiaAuthToken } from "./auth/server.mjs";
|
|
4
4
|
import { HenosiaAuthContext, getHenosiaAuth, requireHenosiaAuth, routeWithHenosiaAuth } from "./auth/server-guards.mjs";
|
|
5
|
-
|
|
5
|
+
|
|
6
|
+
//#region src/auth/configuration.d.ts
|
|
7
|
+
/**
|
|
8
|
+
* A lazy accessor for an environment-derived string value. Reading is deferred until {@link get} is invoked,
|
|
9
|
+
* so the underlying `process.env` lookup only happens at call time rather than at module load.
|
|
10
|
+
*/
|
|
11
|
+
type EnvLiveGetString = {
|
|
12
|
+
get(): string;
|
|
13
|
+
};
|
|
14
|
+
interface HenosiaAuthConfig {
|
|
15
|
+
/** Name of the Henosia Auth OAuth2 provider */
|
|
16
|
+
provider: 'henosia';
|
|
17
|
+
/** The consuming app's own deployment or preview browser URL. Used as the better-auth base url */
|
|
18
|
+
baseURL: EnvLiveGetString;
|
|
19
|
+
/** The consuming app's own deployment or preview auth secret. Used as the better-auth secret */
|
|
20
|
+
secret: EnvLiveGetString;
|
|
21
|
+
/** OAuth client ID from HenosiaAuthClientService */
|
|
22
|
+
clientId: EnvLiveGetString;
|
|
23
|
+
/** OAuth client secret from HenosiaAuthClientService */
|
|
24
|
+
clientSecret: EnvLiveGetString;
|
|
25
|
+
/** The Henosia auth and platform service base URL, under which the OAuth client is registered */
|
|
26
|
+
henosiaAuthPlatformServiceBaseUrl: EnvLiveGetString;
|
|
27
|
+
/** The provider identifier that Henosia Auth is registered with as a OIDC-based Custom Provider in Supabase Auth */
|
|
28
|
+
henosiaAuthSupabaseProvider: EnvLiveGetString;
|
|
29
|
+
/**
|
|
30
|
+
* The project id that the JWT project claim should match in HenosiaAuthTokenClaims.
|
|
31
|
+
* Ensures that signed-in Henosia users cannot access projects that they have not been granted access to,
|
|
32
|
+
* for example that User A from Org A cannot access User B from Org B's project.
|
|
33
|
+
* */
|
|
34
|
+
projectId: EnvLiveGetString;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Environment-provided configuration for Henosia Auth.
|
|
38
|
+
* When the consuming app runs in the Henosia preview, the env vars are provided by the runtime environment and should not be
|
|
39
|
+
* hard-coded or present in the `.env.local` file. The Henosia Publish feature automatically sets production versions of
|
|
40
|
+
* the values on the deployed app. For self-publish flows or local development, see the `Environment settings`
|
|
41
|
+
* in the Henosia builder navbar for the relevant values. The preview values cannot be used for production deployments.
|
|
42
|
+
*/
|
|
43
|
+
declare const henosiaAuthConfig: HenosiaAuthConfig;
|
|
44
|
+
/**
|
|
45
|
+
* Project-scoped cookie prefix used by Henosia Auth.
|
|
46
|
+
*/
|
|
47
|
+
declare const cookiePrefix: {
|
|
48
|
+
get: () => `henosia-auth-${string}`;
|
|
49
|
+
};
|
|
50
|
+
//#endregion
|
|
51
|
+
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, cookiePrefix, getAuth, getHenosiaAuth, henosiaAuthConfig, isPageOrRefreshTokenRequest, isPageRequest, isUnauthorizedException, requireHenosiaAuth, routeWithHenosiaAuth, verifyHenosiaAuthToken };
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*! Copyright (c) 2026 Henosia ApS. Licensed under the Henosia Commercial Source License v1.0. See LICENSE */
|
|
2
2
|
import { HENOSIA_AUTH_GET_SESSION_PATH_NAME, HENOSIA_AUTH_SIGN_IN_PATH_NAME, HENOSIA_ORGANIZATION_CTX_COOKIE } from "./shared.mjs";
|
|
3
|
-
import {
|
|
3
|
+
import { a as isPageRequest, c as cookiePrefix, i as isPageOrRefreshTokenRequest, l as henosiaAuthConfig, n as HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAME_PREFIXES, o as isUnauthorizedException, r as getAuth, s as verifyHenosiaAuthToken, t as HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAMES } from "./server-DvvkIB4j.mjs";
|
|
4
4
|
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,
|
|
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, cookiePrefix, getAuth, getHenosiaAuth, henosiaAuthConfig, isPageOrRefreshTokenRequest, isPageRequest, isUnauthorizedException, requireHenosiaAuth, routeWithHenosiaAuth, verifyHenosiaAuthToken };
|
|
@@ -0,0 +1,232 @@
|
|
|
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";
|
|
3
|
+
import { APIError, betterAuth } from "better-auth";
|
|
4
|
+
import { createAuthMiddleware } from "better-auth/api";
|
|
5
|
+
import { bearer, genericOAuth, jwt } from "better-auth/plugins";
|
|
6
|
+
import { nextCookies } from "better-auth/next-js";
|
|
7
|
+
import { verifyAccessToken } from "better-auth/oauth2";
|
|
8
|
+
import { parseSetCookieHeader } from "better-auth/cookies";
|
|
9
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
10
|
+
import { headers } from "next/headers.js";
|
|
11
|
+
//#region src/auth/configuration.ts
|
|
12
|
+
const requiredEnvProxy = new Proxy(process.env, { get(target, property, receiver) {
|
|
13
|
+
const value = Reflect.get(target, property, receiver);
|
|
14
|
+
if (!value || typeof value !== "string") throw new Error(`[Henosia Auth] Missing required env var: ${property.toString()}`);
|
|
15
|
+
return value;
|
|
16
|
+
} });
|
|
17
|
+
function getRequiredEnvValue(getter, fallback) {
|
|
18
|
+
try {
|
|
19
|
+
return { get: () => getter(requiredEnvProxy) };
|
|
20
|
+
} catch (e) {
|
|
21
|
+
if (fallback) return { get: () => fallback };
|
|
22
|
+
console.log(e.message);
|
|
23
|
+
throw e;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Environment-provided configuration for Henosia Auth.
|
|
28
|
+
* When the consuming app runs in the Henosia preview, the env vars are provided by the runtime environment and should not be
|
|
29
|
+
* hard-coded or present in the `.env.local` file. The Henosia Publish feature automatically sets production versions of
|
|
30
|
+
* the values on the deployed app. For self-publish flows or local development, see the `Environment settings`
|
|
31
|
+
* in the Henosia builder navbar for the relevant values. The preview values cannot be used for production deployments.
|
|
32
|
+
*/
|
|
33
|
+
const henosiaAuthConfig = {
|
|
34
|
+
provider: "henosia",
|
|
35
|
+
baseURL: getRequiredEnvValue((env) => env.BETTER_AUTH_URL),
|
|
36
|
+
secret: getRequiredEnvValue((env) => env.BETTER_AUTH_SECRET),
|
|
37
|
+
henosiaAuthPlatformServiceBaseUrl: getRequiredEnvValue((env) => env.HENOSIA_AUTH_SERVICE_BASE_URL),
|
|
38
|
+
henosiaAuthSupabaseProvider: getRequiredEnvValue((env) => env.HENOSIA_AUTH_SUPABASE_PROVIDER, "custom:henosia-auth:platform"),
|
|
39
|
+
clientSecret: getRequiredEnvValue((env) => env.HENOSIA_AUTH_CLIENT_SECRET),
|
|
40
|
+
clientId: getRequiredEnvValue((env) => env.HENOSIA_AUTH_CLIENT_ID),
|
|
41
|
+
projectId: getRequiredEnvValue((env) => env.HENOSIA_AUTH_PROJECT_ID)
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Project-scoped cookie prefix used by Henosia Auth.
|
|
45
|
+
*/
|
|
46
|
+
const cookiePrefix = { get: () => `henosia-auth-${henosiaAuthConfig.projectId.get()}` };
|
|
47
|
+
//#endregion
|
|
48
|
+
//#region src/auth/server.ts
|
|
49
|
+
/**
|
|
50
|
+
* URL pathname values that should not check for auth in middleware, e.g. the sign-in page.
|
|
51
|
+
* Additional pathname values can be added for public paths, but ONLY if they should not be intercepted by the
|
|
52
|
+
* middleware auth pre-check and redirect.
|
|
53
|
+
*/
|
|
54
|
+
const HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAMES = new Set([HENOSIA_AUTH_SIGN_IN_PATH_NAME]);
|
|
55
|
+
if (HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAMES.has("/")) throw new Error(`[Henosia Auth] Invalid path name '/'. Values must be non-root paths`);
|
|
56
|
+
/**
|
|
57
|
+
* URL pathname prefixes that should not be blocked by auth in middleware, e.g. the better-auth API routes used to sign in.
|
|
58
|
+
* Additional pathname prefixes can be added for public paths, but ONLY if they should not be intercepted by the
|
|
59
|
+
* middleware auth pre-check and redirect. Include the relevant `/` ending character to prevent unexpected partial
|
|
60
|
+
* route matches.
|
|
61
|
+
*/
|
|
62
|
+
const HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAME_PREFIXES = ["/api/auth/", "/.well-known/"];
|
|
63
|
+
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");
|
|
64
|
+
/**
|
|
65
|
+
* Gets whether the specified request is for a page (a top level page or iframe page)
|
|
66
|
+
*/
|
|
67
|
+
function isPageRequest(request) {
|
|
68
|
+
const { headers } = request;
|
|
69
|
+
const dest = headers.get("X-Henosia-Fetch-Dest") ?? headers.get("Sec-Fetch-Dest");
|
|
70
|
+
if (dest === "document" || dest === "iframe" || dest === "fencedframe") return true;
|
|
71
|
+
const isRsc = request.headers.get("RSC") === "1";
|
|
72
|
+
const isPrefetch = request.headers.get("Next-Router-Prefetch") === "1";
|
|
73
|
+
return isRsc && !isPrefetch;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Gets whether the specified request is allowed to spend the single-use OIDC refresh token to obtain a new
|
|
77
|
+
* access token.
|
|
78
|
+
*
|
|
79
|
+
*/
|
|
80
|
+
function isPageOrRefreshTokenRequest(request) {
|
|
81
|
+
if (isPageRequest(request)) return true;
|
|
82
|
+
return new URL(request.url).pathname === HENOSIA_AUTH_GET_SESSION_PATH_NAME;
|
|
83
|
+
}
|
|
84
|
+
const currentRequestStorage = new AsyncLocalStorage();
|
|
85
|
+
let henosiaOAuthProviderInitialized = false;
|
|
86
|
+
const createAuthInstance = () => betterAuth({
|
|
87
|
+
baseURL: henosiaAuthConfig.baseURL.get(),
|
|
88
|
+
secret: henosiaAuthConfig.secret.get(),
|
|
89
|
+
advanced: { cookiePrefix: cookiePrefix.get() },
|
|
90
|
+
plugins: [
|
|
91
|
+
bearer(),
|
|
92
|
+
jwt(),
|
|
93
|
+
genericOAuth({ config: [{
|
|
94
|
+
providerId: henosiaAuthConfig.provider,
|
|
95
|
+
discoveryUrl: `${henosiaAuthConfig.henosiaAuthPlatformServiceBaseUrl.get()}/.well-known/openid-configuration`,
|
|
96
|
+
clientId: henosiaAuthConfig.clientId.get(),
|
|
97
|
+
clientSecret: henosiaAuthConfig.clientSecret.get(),
|
|
98
|
+
scopes: [
|
|
99
|
+
"openid",
|
|
100
|
+
"profile",
|
|
101
|
+
"email",
|
|
102
|
+
"offline_access"
|
|
103
|
+
],
|
|
104
|
+
pkce: true,
|
|
105
|
+
mapProfileToUser: (profile) => {
|
|
106
|
+
if (!profile.name) profile.name = profile.email;
|
|
107
|
+
return profile;
|
|
108
|
+
}
|
|
109
|
+
}] }),
|
|
110
|
+
nextCookies()
|
|
111
|
+
],
|
|
112
|
+
hooks: { before: createAuthMiddleware(async (ctx) => {
|
|
113
|
+
if (henosiaOAuthProviderInitialized) return;
|
|
114
|
+
for (const provider of ctx.context.socialProviders) {
|
|
115
|
+
const { refreshAccessToken } = provider;
|
|
116
|
+
if (!refreshAccessToken || provider.id !== henosiaAuthConfig.provider) continue;
|
|
117
|
+
provider.refreshAccessToken = async (refreshToken) => {
|
|
118
|
+
const request = currentRequestStorage.getStore();
|
|
119
|
+
if (!request) throw new APIError("INTERNAL_SERVER_ERROR", { message: `Expected a current request during refreshAccessToken` });
|
|
120
|
+
if (!isPageOrRefreshTokenRequest(request)) throw new APIError("UNAUTHORIZED", {
|
|
121
|
+
statusCode: 401,
|
|
122
|
+
message: "Fetching a refresh access token is only allowed for page or refresh token requests"
|
|
123
|
+
});
|
|
124
|
+
return await refreshAccessToken(refreshToken);
|
|
125
|
+
};
|
|
126
|
+
henosiaOAuthProviderInitialized = true;
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
}) },
|
|
130
|
+
session: { cookieCache: {
|
|
131
|
+
enabled: true,
|
|
132
|
+
version: "1",
|
|
133
|
+
maxAge: 720 * 60 * 60,
|
|
134
|
+
strategy: "jwe",
|
|
135
|
+
refreshCache: true
|
|
136
|
+
} },
|
|
137
|
+
account: {
|
|
138
|
+
storeStateStrategy: "cookie",
|
|
139
|
+
storeAccountCookie: true
|
|
140
|
+
},
|
|
141
|
+
databaseHooks: { user: { create: { before: async (data) => {
|
|
142
|
+
const stableId = data.sub;
|
|
143
|
+
if (!stableId) throw new APIError("INTERNAL_SERVER_ERROR", { message: `Expected '.sub' to be defined but got '${stableId}' for '${data.email}'` });
|
|
144
|
+
return { data: {
|
|
145
|
+
...data,
|
|
146
|
+
id: stableId
|
|
147
|
+
} };
|
|
148
|
+
} } } },
|
|
149
|
+
trustedOrigins: [henosiaAuthConfig.henosiaAuthPlatformServiceBaseUrl.get()]
|
|
150
|
+
});
|
|
151
|
+
let authInstance = null;
|
|
152
|
+
/**
|
|
153
|
+
* Returns the singleton Henosia Auth instance, creating it on first call.
|
|
154
|
+
*
|
|
155
|
+
* The instance is created lazily so that importing this module does not eagerly read the required
|
|
156
|
+
* environment variables (see {@link henosiaAuthConfig}). This means modules that only need the
|
|
157
|
+
* configuration or shared helpers can be imported in environments where the full set of `BETTER_AUTH_*` /
|
|
158
|
+
* `HENOSIA_AUTH_*` env vars is not yet populated, and only call sites that actually invoke `getAuth()`
|
|
159
|
+
* will require them to be set.
|
|
160
|
+
*
|
|
161
|
+
* Subsequent calls return the same underlying better-auth instance.
|
|
162
|
+
*
|
|
163
|
+
* The return type is the minimal {@link Auth} type from better-auth (which is parameterised on the default
|
|
164
|
+
* `BetterAuthOptions`) rather than the deeply-inferred concrete type returned by {@link betterAuth}.
|
|
165
|
+
* The deeply-inferred graph references non-portable internals from `better-auth` and `zod` that cannot be
|
|
166
|
+
* named in published declarations (TS2883).
|
|
167
|
+
*/
|
|
168
|
+
const getAuth = () => {
|
|
169
|
+
if (!authInstance) authInstance = createAuthInstance();
|
|
170
|
+
return authInstance;
|
|
171
|
+
};
|
|
172
|
+
/**
|
|
173
|
+
* Verifies the Henosia Auth JWT on the current request, or throws in case it's missing or invalid.
|
|
174
|
+
* This method must be called before allowing access to any protected pages or routes,
|
|
175
|
+
* and before accessing data in server side page methods or actions.
|
|
176
|
+
* @param request the current request which provides auth headers
|
|
177
|
+
* @return the better-auth `getAccessToken` response, the verified token `payload` (a {@link HenosiaAuthTokenClaims}),
|
|
178
|
+
* the `Set-Cookie` headers produced while obtaining/refreshing the token, and a
|
|
179
|
+
* `transferBetterAuthCookiesToNext` helper that forwards those cookies onto a Next.js request/response pair
|
|
180
|
+
* @throws APIError when the current credentials are missing or invalid
|
|
181
|
+
*/
|
|
182
|
+
async function verifyHenosiaAuthToken(request) {
|
|
183
|
+
currentRequestStorage.enterWith(request);
|
|
184
|
+
const { response: accessTokenResponse, headers: accessTokenResponseHeaders } = await getAuth().api.getAccessToken({
|
|
185
|
+
body: { providerId: henosiaAuthConfig.provider },
|
|
186
|
+
headers: await headers(),
|
|
187
|
+
returnHeaders: true
|
|
188
|
+
});
|
|
189
|
+
if (!accessTokenResponse.idToken) throw new APIError("UNAUTHORIZED", { message: "No id_token returned for openid scope" });
|
|
190
|
+
const payload = await verifyAccessToken(accessTokenResponse.idToken, {
|
|
191
|
+
jwksUrl: `${henosiaAuthConfig.henosiaAuthPlatformServiceBaseUrl.get()}/api/auth/jwks`,
|
|
192
|
+
verifyOptions: {
|
|
193
|
+
audience: henosiaAuthConfig.clientId.get(),
|
|
194
|
+
issuer: `${henosiaAuthConfig.henosiaAuthPlatformServiceBaseUrl.get()}/api/auth`
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
const { "https://henosia.com/project": projectId } = payload;
|
|
198
|
+
if (!projectId) throw new APIError("UNAUTHORIZED", { message: "token does not grant access" });
|
|
199
|
+
if (!henosiaAuthConfig.projectId) throw new APIError("INTERNAL_SERVER_ERROR", { message: "HENOSIA_AUTH_PROJECT_ID env var has not been set" });
|
|
200
|
+
if (henosiaAuthConfig.projectId.get() !== projectId) throw new APIError("UNAUTHORIZED", { message: "token has invalid projectId" });
|
|
201
|
+
const setCookieHeaders = accessTokenResponseHeaders.getSetCookie();
|
|
202
|
+
const transferBetterAuthCookiesToNext = (nextRequest, response) => {
|
|
203
|
+
for (const cookie of setCookieHeaders) parseSetCookieHeader(cookie).forEach((attributes, name) => {
|
|
204
|
+
const { value, ...betterOptions } = attributes;
|
|
205
|
+
const options = {
|
|
206
|
+
...betterOptions,
|
|
207
|
+
maxAge: betterOptions["max-age"],
|
|
208
|
+
httpOnly: betterOptions.httponly,
|
|
209
|
+
sameSite: betterOptions.samesite
|
|
210
|
+
};
|
|
211
|
+
nextRequest.cookies.set(name, value);
|
|
212
|
+
response.cookies.set(name, value, options);
|
|
213
|
+
});
|
|
214
|
+
};
|
|
215
|
+
return {
|
|
216
|
+
accessTokenResponse,
|
|
217
|
+
payload,
|
|
218
|
+
setCookieHeaders,
|
|
219
|
+
transferBetterAuthCookiesToNext
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
const unauthorizedExceptionCodes = new Set(["ACCOUNT_NOT_FOUND", "FAILED_TO_GET_ACCESS_TOKEN"]);
|
|
223
|
+
/**
|
|
224
|
+
* Determines whether the specified exception should start the sign-in flow to authenticate the user
|
|
225
|
+
* @param error the error thrown by `verifyHenosiaAuthToken`
|
|
226
|
+
*/
|
|
227
|
+
function isUnauthorizedException(error) {
|
|
228
|
+
const apiError = error ?? {};
|
|
229
|
+
return unauthorizedExceptionCodes.has(apiError.body?.code ?? "") || apiError.status === "UNAUTHORIZED" || apiError.statusCode === 401;
|
|
230
|
+
}
|
|
231
|
+
//#endregion
|
|
232
|
+
export { isPageRequest as a, cookiePrefix as c, isPageOrRefreshTokenRequest as i, henosiaAuthConfig as l, HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAME_PREFIXES as n, isUnauthorizedException as o, getAuth as r, verifyHenosiaAuthToken as s, HENOSIA_AUTH_MIDDLEWARE_UNAUTHENTICATED_PATH_NAMES 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-
|
|
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-Cddy8ptu.mjs";
|
|
3
3
|
export { HENOSIA_AUTH_GET_SESSION_PATH_NAME, HENOSIA_AUTH_SIGN_IN_PATH_NAME, HENOSIA_ORGANIZATION_CTX_COOKIE, HenosiaOrganizationContext };
|
package/package.json
CHANGED
|
File without changes
|