@henosia/app-next 1.0.2 → 1.0.4
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 +228 -0
- package/.agents/skills/henosia-app-next/assets/template/app/(auth)/sign-in/loading.tsx +10 -0
- package/.agents/skills/henosia-app-next/assets/template/app/(auth)/sign-in/page.tsx +15 -0
- package/.agents/skills/henosia-app-next/assets/template/app/api/auth/[...all]/route.ts +9 -0
- package/.agents/skills/henosia-app-next/assets/template/app/api/henosia-platform/[...all]/route.ts +2 -0
- package/.agents/skills/henosia-app-next/assets/template/app/layout.tsx +35 -0
- package/.agents/skills/henosia-app-next/assets/template/components/app-switcher.tsx +148 -0
- package/.agents/skills/henosia-app-next/assets/template/components/nav-user.tsx +143 -0
- package/.agents/skills/henosia-app-next/assets/template/components/query-provider.tsx +23 -0
- package/.agents/skills/henosia-app-next/assets/template/components/sign-in.tsx +85 -0
- package/.agents/skills/henosia-app-next/assets/template/lib/auth-client.ts +3 -0
- package/.agents/skills/henosia-app-next/assets/template/lib/supabase/client.ts +2 -0
- package/.agents/skills/henosia-app-next/assets/template/lib/supabase/server.ts +2 -0
- package/.agents/skills/henosia-app-next/assets/template/middleware.ts +29 -0
- package/.agents/skills/henosia-app-next/assets/template/next.config.mjs +65 -0
- package/.agents/skills/henosia-app-next/assets/template/package.json +15 -0
- package/.agents/skills/henosia-app-next/assets/template/tsconfig.json +26 -0
- package/.agents/skills/henosia-auth-guards/SKILL.md +121 -0
- package/README.md +26 -13
- package/dist/api/platform.mjs +3 -2
- package/dist/auth/middleware.mjs +8 -4
- package/dist/auth/server-guards.d.mts +116 -0
- package/dist/auth/server-guards.mjs +167 -0
- package/dist/auth/server.d.mts +1 -1
- package/dist/index.d.mts +3 -2
- package/dist/index.mjs +2 -1
- package/dist/platform/app-switcher.d.mts +1 -1
- package/dist/shared-BWt7Sysv.d.mts +19 -0
- package/dist/shared.d.mts +1 -17
- package/package.json +3 -1
|
@@ -0,0 +1,167 @@
|
|
|
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 { isUnauthorizedException, verifyHenosiaAuthToken } from "./server.mjs";
|
|
4
|
+
import { headers } from "next/headers.js";
|
|
5
|
+
import { cache } from "react";
|
|
6
|
+
import { redirect } from "next/navigation.js";
|
|
7
|
+
import { NextResponse } from "next/server.js";
|
|
8
|
+
//#region src/auth/server-guards.ts
|
|
9
|
+
async function buildRequestFromHeaders() {
|
|
10
|
+
const h = await headers();
|
|
11
|
+
const url = `${h.get("x-forwarded-proto") ?? "http"}://${h.get("x-forwarded-host") ?? h.get("host") ?? "localhost"}/`;
|
|
12
|
+
return new Request(url, { headers: h });
|
|
13
|
+
}
|
|
14
|
+
async function verifyClaims() {
|
|
15
|
+
const { payload } = await verifyHenosiaAuthToken(await buildRequestFromHeaders());
|
|
16
|
+
return payload;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* For Server Components, layouts, and Server Actions.
|
|
20
|
+
*
|
|
21
|
+
* Verifies the current user's Henosia Auth token and returns the verified
|
|
22
|
+
* claims. On unauthorized, redirects to the Henosia sign-in page (the canonical
|
|
23
|
+
* page-level UX); other errors propagate so Next.js' error boundaries / 500
|
|
24
|
+
* page take over.
|
|
25
|
+
*
|
|
26
|
+
* Wrapped in `React.cache` so multiple components in the same render share a
|
|
27
|
+
* single verification.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```tsx
|
|
31
|
+
* // app/dashboard/page.tsx
|
|
32
|
+
* import { requireHenosiaAuth } from '@/lib/henosia-auth'
|
|
33
|
+
*
|
|
34
|
+
* export default async function Page() {
|
|
35
|
+
* const claims = await requireHenosiaAuth()
|
|
36
|
+
* return <Dashboard org={claims['https://henosia.com/organization']} />
|
|
37
|
+
* }
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
const requireHenosiaAuth = cache(async () => {
|
|
41
|
+
try {
|
|
42
|
+
return await verifyClaims();
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if (isUnauthorizedException(error)) redirect(HENOSIA_AUTH_SIGN_IN_PATH_NAME);
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
/**
|
|
49
|
+
* Non-redirecting variant of {@link requireHenosiaAuth} for layouts and
|
|
50
|
+
* conditional UI (e.g., navbars that need to render either a "Sign in" button
|
|
51
|
+
* or a user menu). Returns `null` when the user is not authenticated; other
|
|
52
|
+
* errors propagate.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```tsx
|
|
56
|
+
* // app/layout.tsx
|
|
57
|
+
* import { getHenosiaAuth } from '@/lib/henosia-auth'
|
|
58
|
+
*
|
|
59
|
+
* export default async function RootLayout({ children }) {
|
|
60
|
+
* const claims = await getHenosiaAuth()
|
|
61
|
+
* return <Shell user={claims?.['https://henosia.com/organization']}>{children}</Shell>
|
|
62
|
+
* }
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
const getHenosiaAuth = cache(async () => {
|
|
66
|
+
try {
|
|
67
|
+
return await verifyClaims();
|
|
68
|
+
} catch (error) {
|
|
69
|
+
if (isUnauthorizedException(error)) return null;
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
/**
|
|
74
|
+
* Headers we attach to *every* response produced by `routeWithHenosiaAuth`
|
|
75
|
+
* (success, 401, 500). Auth-protected JSON is per-user and must not be cached
|
|
76
|
+
* by shared caches (RFC 9111 §5.2.2.7 — `private`) and must not be reused
|
|
77
|
+
* even by the user's own cache (`no-store`). `Vary: Cookie` is the formally
|
|
78
|
+
* correct signal that the response varies on the auth cookie (RFC 9110 §12.5.5).
|
|
79
|
+
*/
|
|
80
|
+
const SECURITY_HEADERS = {
|
|
81
|
+
"Cache-Control": "private, no-store",
|
|
82
|
+
"Vary": "Cookie"
|
|
83
|
+
};
|
|
84
|
+
function applySecurityHeaders(response) {
|
|
85
|
+
for (const [name, value] of Object.entries(SECURITY_HEADERS)) if (!response.headers.has(name)) response.headers.set(name, value);
|
|
86
|
+
return response;
|
|
87
|
+
}
|
|
88
|
+
function unauthorizedResponse() {
|
|
89
|
+
return NextResponse.json({
|
|
90
|
+
error: "invalid_token",
|
|
91
|
+
error_description: "The access token is missing, invalid, or expired."
|
|
92
|
+
}, {
|
|
93
|
+
status: 401,
|
|
94
|
+
headers: SECURITY_HEADERS
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
function internalServerErrorResponse() {
|
|
98
|
+
return NextResponse.json({ error: "internal_server_error" }, {
|
|
99
|
+
status: 500,
|
|
100
|
+
headers: SECURITY_HEADERS
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* For Next.js Route Handlers (`app/api/**\/route.ts`).
|
|
105
|
+
*
|
|
106
|
+
* Wraps a handler so that:
|
|
107
|
+
* - The request is verified up front.
|
|
108
|
+
* - On success, the handler receives `{ payload, accessToken, params }` and
|
|
109
|
+
* may return either a `Response`, any JSON-serializable value
|
|
110
|
+
* (auto-wrapped via `NextResponse.json`), or `undefined` (→ `204
|
|
111
|
+
* No Content`).
|
|
112
|
+
* - On unauthorized, returns a canonical HTTP `401 Unauthorized` response
|
|
113
|
+
* (RFC 7235): never a redirect.
|
|
114
|
+
* - On any other error (verifying the token *or* thrown by the handler),
|
|
115
|
+
* it's logged with the request's pathname for traceability and a generic
|
|
116
|
+
* `500 Internal Server Error` JSON response is returned (no stack /
|
|
117
|
+
* internal details leaked) — the same OAuth-shaped contract regardless of
|
|
118
|
+
* where the failure originated.
|
|
119
|
+
*
|
|
120
|
+
* Preserves the standard Next.js `(request, { params })` signature, including
|
|
121
|
+
* Next 15's promise-typed `params`.
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* ```ts
|
|
125
|
+
* // app/api/me/route.ts
|
|
126
|
+
* import { routeWithHenosiaAuth } from '@/lib/henosia-auth'
|
|
127
|
+
*
|
|
128
|
+
* export const GET = routeWithHenosiaAuth((_req, { payload }) => ({
|
|
129
|
+
* organization: payload['https://henosia.com/organization'],
|
|
130
|
+
* }))
|
|
131
|
+
* ```
|
|
132
|
+
*/
|
|
133
|
+
function routeWithHenosiaAuth(handler) {
|
|
134
|
+
return async function protectedHandler(request, routeCtx) {
|
|
135
|
+
let payload;
|
|
136
|
+
let accessToken;
|
|
137
|
+
try {
|
|
138
|
+
const verified = await verifyHenosiaAuthToken(request);
|
|
139
|
+
payload = verified.payload;
|
|
140
|
+
accessToken = verified.accessTokenResponse.accessToken;
|
|
141
|
+
} catch (error) {
|
|
142
|
+
if (isUnauthorizedException(error)) return unauthorizedResponse();
|
|
143
|
+
console.error(`[Henosia Auth] Unexpected error verifying token for ${request.method} ${request.nextUrl.pathname}`, error);
|
|
144
|
+
return internalServerErrorResponse();
|
|
145
|
+
}
|
|
146
|
+
let result;
|
|
147
|
+
try {
|
|
148
|
+
const params = await routeCtx.params;
|
|
149
|
+
result = await handler(request, {
|
|
150
|
+
payload,
|
|
151
|
+
accessToken,
|
|
152
|
+
params
|
|
153
|
+
});
|
|
154
|
+
} catch (error) {
|
|
155
|
+
console.error(`[Henosia Auth] Unhandled error in handler for ${request.method} ${request.nextUrl.pathname}`, error);
|
|
156
|
+
return internalServerErrorResponse();
|
|
157
|
+
}
|
|
158
|
+
if (result instanceof Response) return applySecurityHeaders(result);
|
|
159
|
+
if (result === void 0) return new Response(null, {
|
|
160
|
+
status: 204,
|
|
161
|
+
headers: SECURITY_HEADERS
|
|
162
|
+
});
|
|
163
|
+
return NextResponse.json(result, { headers: SECURITY_HEADERS });
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
//#endregion
|
|
167
|
+
export { applySecurityHeaders, getHenosiaAuth, requireHenosiaAuth, routeWithHenosiaAuth, unauthorizedResponse };
|
package/dist/auth/server.d.mts
CHANGED
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
import {
|
|
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
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";
|
|
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 };
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +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
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";
|
|
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, auth, cookiePrefix, getHenosiaAuth, henosiaAuthConfig, isPageOrRefreshTokenRequest, isPageRequest, isUnauthorizedException, requireHenosiaAuth, routeWithHenosiaAuth, verifyHenosiaAuthToken };
|
|
@@ -0,0 +1,19 @@
|
|
|
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 special-case for this path.
|
|
10
|
+
*/
|
|
11
|
+
declare const HENOSIA_AUTH_GET_SESSION_PATH_NAME = "/api/auth/get-session";
|
|
12
|
+
type HenosiaOrganizationContext = {
|
|
13
|
+
id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
logoUrl: string | null;
|
|
16
|
+
} | null;
|
|
17
|
+
declare const HENOSIA_ORGANIZATION_CTX_COOKIE = "henosia.org";
|
|
18
|
+
//#endregion
|
|
19
|
+
export { HenosiaOrganizationContext as i, HENOSIA_AUTH_SIGN_IN_PATH_NAME as n, HENOSIA_ORGANIZATION_CTX_COOKIE as r, HENOSIA_AUTH_GET_SESSION_PATH_NAME as t };
|
package/dist/shared.d.mts
CHANGED
|
@@ -1,19 +1,3 @@
|
|
|
1
1
|
|
|
2
|
-
|
|
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 special-case for this path.
|
|
10
|
-
*/
|
|
11
|
-
declare const HENOSIA_AUTH_GET_SESSION_PATH_NAME = "/api/auth/get-session";
|
|
12
|
-
type HenosiaOrganizationContext = {
|
|
13
|
-
id: string;
|
|
14
|
-
name: string;
|
|
15
|
-
logoUrl: string | null;
|
|
16
|
-
} | null;
|
|
17
|
-
declare const HENOSIA_ORGANIZATION_CTX_COOKIE = "henosia.org";
|
|
18
|
-
//#endregion
|
|
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";
|
|
19
3
|
export { HENOSIA_AUTH_GET_SESSION_PATH_NAME, HENOSIA_AUTH_SIGN_IN_PATH_NAME, HENOSIA_ORGANIZATION_CTX_COOKIE, HenosiaOrganizationContext };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@henosia/app-next",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Henosia integration for Next.js apps: Provides Henosia Auth and Henosia Platform features like app switcher",
|
|
5
5
|
"author": "Henosia",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"type": "module",
|
|
16
16
|
"files": [
|
|
17
17
|
"dist",
|
|
18
|
+
".agents/skills",
|
|
18
19
|
"LICENSE"
|
|
19
20
|
],
|
|
20
21
|
"scripts": {
|
|
@@ -67,6 +68,7 @@
|
|
|
67
68
|
"./auth/client": "./dist/auth/client.mjs",
|
|
68
69
|
"./auth/middleware": "./dist/auth/middleware.mjs",
|
|
69
70
|
"./auth/server": "./dist/auth/server.mjs",
|
|
71
|
+
"./auth/server-guards": "./dist/auth/server-guards.mjs",
|
|
70
72
|
"./platform/app-switcher": "./dist/platform/app-switcher.mjs",
|
|
71
73
|
"./shared": "./dist/shared.mjs",
|
|
72
74
|
"./supabase/client": "./dist/supabase/client.mjs",
|