@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.
Files changed (30) hide show
  1. package/.agents/skills/henosia-app-next/SKILL.md +228 -0
  2. package/.agents/skills/henosia-app-next/assets/template/app/(auth)/sign-in/loading.tsx +10 -0
  3. package/.agents/skills/henosia-app-next/assets/template/app/(auth)/sign-in/page.tsx +15 -0
  4. package/.agents/skills/henosia-app-next/assets/template/app/api/auth/[...all]/route.ts +9 -0
  5. package/.agents/skills/henosia-app-next/assets/template/app/api/henosia-platform/[...all]/route.ts +2 -0
  6. package/.agents/skills/henosia-app-next/assets/template/app/layout.tsx +35 -0
  7. package/.agents/skills/henosia-app-next/assets/template/components/app-switcher.tsx +148 -0
  8. package/.agents/skills/henosia-app-next/assets/template/components/nav-user.tsx +143 -0
  9. package/.agents/skills/henosia-app-next/assets/template/components/query-provider.tsx +23 -0
  10. package/.agents/skills/henosia-app-next/assets/template/components/sign-in.tsx +85 -0
  11. package/.agents/skills/henosia-app-next/assets/template/lib/auth-client.ts +3 -0
  12. package/.agents/skills/henosia-app-next/assets/template/lib/supabase/client.ts +2 -0
  13. package/.agents/skills/henosia-app-next/assets/template/lib/supabase/server.ts +2 -0
  14. package/.agents/skills/henosia-app-next/assets/template/middleware.ts +29 -0
  15. package/.agents/skills/henosia-app-next/assets/template/next.config.mjs +65 -0
  16. package/.agents/skills/henosia-app-next/assets/template/package.json +15 -0
  17. package/.agents/skills/henosia-app-next/assets/template/tsconfig.json +26 -0
  18. package/.agents/skills/henosia-auth-guards/SKILL.md +121 -0
  19. package/README.md +26 -13
  20. package/dist/api/platform.mjs +3 -2
  21. package/dist/auth/middleware.mjs +8 -4
  22. package/dist/auth/server-guards.d.mts +116 -0
  23. package/dist/auth/server-guards.mjs +167 -0
  24. package/dist/auth/server.d.mts +1 -1
  25. package/dist/index.d.mts +3 -2
  26. package/dist/index.mjs +2 -1
  27. package/dist/platform/app-switcher.d.mts +1 -1
  28. package/dist/shared-BWt7Sysv.d.mts +19 -0
  29. package/dist/shared.d.mts +1 -17
  30. 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 };
@@ -1,5 +1,5 @@
1
1
 
2
- import { HenosiaOrganizationContext } from "../shared.mjs";
2
+ import { i as HenosiaOrganizationContext } from "../shared-BWt7Sysv.mjs";
3
3
  import { Auth } from "better-auth";
4
4
  import { NextRequest, NextResponse } from "next/server.js";
5
5
 
package/dist/index.d.mts CHANGED
@@ -1,4 +1,5 @@
1
1
 
2
- import { HENOSIA_AUTH_GET_SESSION_PATH_NAME, HENOSIA_AUTH_SIGN_IN_PATH_NAME, HENOSIA_ORGANIZATION_CTX_COOKIE, HenosiaOrganizationContext } from "./shared.mjs";
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
- 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 HenosiaAuthTokenClaims, type HenosiaOrganizationContext, auth, cookiePrefix, henosiaAuthConfig, isPageOrRefreshTokenRequest, isPageRequest, isUnauthorizedException, verifyHenosiaAuthToken };
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
- 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, henosiaAuthConfig, isPageOrRefreshTokenRequest, isPageRequest, isUnauthorizedException, verifyHenosiaAuthToken };
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 };
@@ -1,5 +1,5 @@
1
1
 
2
- import { HenosiaOrganizationContext } from "../shared.mjs";
2
+ import { i as HenosiaOrganizationContext } from "../shared-BWt7Sysv.mjs";
3
3
 
4
4
  //#region src/platform/app-switcher.d.ts
5
5
  /**
@@ -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
- //#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
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.2",
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",