@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,85 @@
1
+ //@ts-nocheck (remove when using the template)
2
+ "use client";
3
+
4
+ import Link from "next/link";
5
+ import {Button} from "@/components/ui/button";
6
+ import {Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle,} from "@/components/ui/card";
7
+ import {authClient} from "@/lib/auth-client";
8
+ import {cn} from "@/lib/utils";
9
+ import {useToast} from "@/hooks/use-toast";
10
+ import {useState} from "react";
11
+
12
+ export function SignIn() {
13
+
14
+ const {signIn} = authClient;
15
+ const [loading, setLoading] = useState(false);
16
+
17
+ const {toast} = useToast();
18
+
19
+ return (
20
+ <Card className="max-w-md rounded-none">
21
+ <CardHeader>
22
+ <CardTitle className="text-lg md:text-xl">Sign In</CardTitle>
23
+ <CardDescription className="text-xs md:text-sm">
24
+ Sign in for this app is provided by Henosia Auth.
25
+ </CardDescription>
26
+ </CardHeader>
27
+ <CardContent>
28
+ <div className="grid gap-4">
29
+
30
+ <div
31
+ className={cn(
32
+ "w-full gap-2 flex items-center",
33
+ "justify-between flex-col",
34
+ )}
35
+ >
36
+ <Button
37
+ variant="outline"
38
+ className={cn("w-full gap-2 flex relative data-[loading=true]:cursor-progress")}
39
+ data-henosia-auth-sign-in
40
+ data-loading={loading}
41
+ onClick={async () => {
42
+ if (loading) {
43
+ return;
44
+ }
45
+ try {
46
+ setLoading(true);
47
+ const {error} = await signIn.social({
48
+ provider: "henosia",
49
+ callbackURL: "/",
50
+ });
51
+ if (error) {
52
+ console.error('[Henosia Auth] Sign-in error', error);
53
+ toast({ variant: "destructive", title: "Sign-in failed", description: error.message });
54
+ }
55
+ } finally {
56
+ setLoading(false)
57
+ }
58
+ }}
59
+ >
60
+ <span className={"relative"}>
61
+ Sign in with Henosia
62
+ </span>
63
+ </Button>
64
+ </div>
65
+ </div>
66
+ </CardContent>
67
+ <CardFooter>
68
+ <div className="flex justify-center w-full border-t pt-4">
69
+ <p className="text-center text-xs text-neutral-500">
70
+ Learn more about {" "}
71
+ <Link
72
+ href="https://docs.henosia.com"
73
+ className="underline"
74
+ target="_blank"
75
+ >
76
+ <span className="dark:text-white/70 cursor-pointer">
77
+ Henosia Auth.
78
+ </span>
79
+ </Link>
80
+ </p>
81
+ </div>
82
+ </CardFooter>
83
+ </Card>
84
+ );
85
+ }
@@ -0,0 +1,3 @@
1
+ //@ts-nocheck (remove when using the template)
2
+ "use client"
3
+ export { authClient } from '@henosia/app-next/auth/client'
@@ -0,0 +1,2 @@
1
+ //@ts-nocheck (remove when using the template)
2
+ export { createClient } from '@henosia/app-next/supabase/client'
@@ -0,0 +1,2 @@
1
+ //@ts-nocheck (remove when using the template)
2
+ export { createClient } from '@henosia/app-next/supabase/server'
@@ -0,0 +1,29 @@
1
+ //@ts-nocheck (remove when using the template)
2
+ // Template use guidance: Preserve comments verbatim from the line `// To Henosia assistant regarding this file:` and below
3
+ import { type NextRequest } from 'next/server'
4
+ import { createHenosiaAuthMiddleware } from '@henosia/app-next/auth/middleware'
5
+
6
+ const henosiaAuthMiddleware = createHenosiaAuthMiddleware()
7
+
8
+ export async function middleware(request: NextRequest) {
9
+ // App-specific pre-checks here, if any.
10
+ return await henosiaAuthMiddleware(request)
11
+ }
12
+
13
+ export const config = {
14
+ matcher: [
15
+ /*
16
+ * Match all request paths except for the ones starting with:
17
+ * - _next/static (static files)
18
+ * - _next/image (image optimization files)
19
+ * - favicon.ico (favicon file)
20
+ */
21
+ '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
22
+ ],
23
+ }
24
+
25
+ // To Henosia assistant regarding this file:
26
+ // Henosia Auth is a managed solution that is outside your scope of work.
27
+ // Henosia Auth flows must be preserved at all times.
28
+ // Important: This middleware performs an auth *pre-check*. Authorization MUST still be performed at the relevant pages and route handlers using `requireHenosiaAuth` or `routeWithHenosiaAuth` from `@henosia/app-next`.
29
+ // Any potential issues with Henosia Auth flows must be referred to Henosia technical support: https://docs.henosia.com/support
@@ -0,0 +1,65 @@
1
+ import { randomBytes } from 'node:crypto';
2
+
3
+ /**
4
+ * Makes a webpack OutputFileSystem emit assets atomically.
5
+ * Fixes `Uncaught SyntaxError: Invalid or unexpected token` in the browser when the
6
+ * served layout.js is visibly cut off mid-string due to concurrent non-atomic writes.
7
+ */
8
+ function patchWebpackOutputFileSystem(ofs) {
9
+ if (!ofs || ofs.__atomicEmitPatched) return;
10
+ if (typeof ofs.writeFile !== 'function' || typeof ofs.rename !== 'function') {
11
+ return;
12
+ }
13
+
14
+ const origWriteFile = ofs.writeFile.bind(ofs);
15
+ const origRename = ofs.rename.bind(ofs);
16
+ const origUnlink = typeof ofs.unlink === 'function' ? ofs.unlink.bind(ofs) : null;
17
+
18
+ ofs.writeFile = function patchedWriteFile(filePath, content, optionsOrCb, maybeCb) {
19
+ const hasOptions = typeof optionsOrCb !== 'function';
20
+ const options = hasOptions ? optionsOrCb : undefined;
21
+ const cb = hasOptions ? maybeCb : optionsOrCb;
22
+ const tmp = `${filePath}.${process.pid}.${randomBytes(6).toString('hex')}.tmp`;
23
+
24
+ const done = (err) => {
25
+ if (err) {
26
+ if (origUnlink) origUnlink(tmp, () => cb(err));
27
+ else cb(err);
28
+ return;
29
+ }
30
+ origRename(tmp, filePath, cb);
31
+ };
32
+
33
+ if (hasOptions && options !== undefined) {
34
+ origWriteFile(tmp, content, options, done);
35
+ } else {
36
+ origWriteFile(tmp, content, done);
37
+ }
38
+ };
39
+
40
+ Object.defineProperty(ofs, '__atomicEmitPatched', { value: true, enumerable: false });
41
+ }
42
+
43
+ class AtomicEmitPlugin {
44
+ apply(compiler) {
45
+ // `outputFileSystem` may be assigned/replaced after `apply` runs (e.g. by
46
+ // Next.js or webpack-dev-middleware), so patch lazily right before each
47
+ // build's emit phase. This is idempotent thanks to `__atomicEmitPatched`.
48
+ const patch = () => patchWebpackOutputFileSystem(compiler.outputFileSystem);
49
+ compiler.hooks.beforeRun.tap('AtomicEmitPlugin', patch);
50
+ compiler.hooks.watchRun.tap('AtomicEmitPlugin', patch);
51
+ compiler.hooks.beforeCompile.tap('AtomicEmitPlugin', patch);
52
+ }
53
+ }
54
+
55
+ /** @type {import('next').NextConfig} */
56
+ const nextConfig = {
57
+ webpack: (config, { dev }) => {
58
+ if (dev) {
59
+ config.plugins.push(new AtomicEmitPlugin());
60
+ }
61
+ return config;
62
+ },
63
+ };
64
+
65
+ export default nextConfig;
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "nextjs-shadcn-henosia-auth",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "dependencies": {
6
+ "__add_these__:": "",
7
+ "@henosia/app-next": "^1.0.3",
8
+ "@tanstack/react-query": "^5.100.11",
9
+ "better-auth": "1.6.11",
10
+ "__others_omitted_for_brevity__": ""
11
+ },
12
+ "devDependencies": {
13
+ "__others_omitted_for_brevity__": ""
14
+ }
15
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "compilerOptions": {
3
+ "lib": ["dom", "dom.iterable", "esnext"],
4
+ "allowJs": true,
5
+ "skipLibCheck": true,
6
+ "strict": true,
7
+ "noEmit": true,
8
+ "esModuleInterop": true,
9
+ "module": "esnext",
10
+ "moduleResolution": "bundler",
11
+ "resolveJsonModule": true,
12
+ "isolatedModules": true,
13
+ "jsx": "preserve",
14
+ "incremental": true,
15
+ "plugins": [
16
+ {
17
+ "name": "next"
18
+ }
19
+ ],
20
+ "paths": {
21
+ "@/*": ["./*"]
22
+ }
23
+ },
24
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
25
+ "exclude": ["node_modules"]
26
+ }
@@ -0,0 +1,121 @@
1
+ ---
2
+ name: henosia-auth-guards
3
+ description: >-
4
+ Enforce @henosia/app-next's server authorization guards on Next.js App Router
5
+ code before any per-user data is read or returned. Use this skill WHENEVER you
6
+ create, edit, or review a server-side handler: any app/**/page.tsx or
7
+ layout.tsx, any file with a "use server" directive or Server Action, and any
8
+ Route Handler (route.ts/route.js) exporting GET/POST/PUT/PATCH/DELETE — or any
9
+ file importing from @henosia/app-next. Trigger even when the user never mentions
10
+ auth, and EVEN IF Henosia auth middleware is present, since the middleware is a
11
+ pre-check only and does NOT satisfy a handler's own authorization requirement.
12
+ Do NOT use for middleware (guards are invalid there), Client Components ("use
13
+ client"), or purely presentational components with no server data access.
14
+ ---
15
+
16
+ # Henosia Auth Guards
17
+
18
+ Authorization guards for Next.js apps using `@henosia/app-next`. **Every
19
+ protected page, layout, Server Action, and Route Handler MUST call one of these
20
+ guards.** The Henosia Auth middleware (`createHenosiaAuthMiddleware`) is a fast
21
+ *pre-check* only — it does not satisfy the authorization requirement of the
22
+ underlying request handler.
23
+
24
+ ## Import
25
+
26
+ ```ts
27
+ import {
28
+ requireHenosiaAuth,
29
+ getHenosiaAuth,
30
+ routeWithHenosiaAuth,
31
+ type HenosiaAuthContext,
32
+ } from '@henosia/app-next/auth/server-guards'
33
+ ```
34
+
35
+ ## Pick the right guard
36
+
37
+ | Context | Use | Behavior on unauthorized |
38
+ |-------------------------------------------------------------------------|------------------------------|-------------------------------------------------------|
39
+ | Server Component / Page / Server Action / protected layout | `requireHenosiaAuth()` | `redirect()` to `/sign-in` (`HENOSIA_AUTH_SIGN_IN_PATH_NAME`) |
40
+ | Route Handler (`app/api/**/route.ts`) | `routeWithHenosiaAuth(handler)` | `401 Unauthorized` JSON (RFC 7235) — never a redirect |
41
+
42
+ ## Examples
43
+
44
+ ### Page / Server Component / Server Action
45
+
46
+ ```tsx
47
+ // app/dashboard/page.tsx
48
+ import { requireHenosiaAuth } from '@henosia/app-next/auth/server-guards'
49
+
50
+ export default async function Page() {
51
+ const claims = await requireHenosiaAuth()
52
+ return <Dashboard org={claims['https://henosia.com/organization']} />
53
+ }
54
+ ```
55
+
56
+ ```ts
57
+ // Server Action
58
+ 'use server'
59
+ import { requireHenosiaAuth } from '@henosia/app-next/auth/server-guards'
60
+
61
+ export async function createWidget(formData: FormData) {
62
+ await requireHenosiaAuth() // throws-redirects to sign-in if unauthorized
63
+ // ...mutate
64
+ }
65
+ ```
66
+
67
+ ### Route Handler
68
+
69
+ ```ts
70
+ // app/api/me/route.ts
71
+ import { routeWithHenosiaAuth } from '@henosia/app-next/auth/server-guards'
72
+
73
+ export const GET = routeWithHenosiaAuth((_request, { payload }) => ({
74
+ organization: payload['https://henosia.com/organization'],
75
+ preview: payload['https://henosia.com/preview'],
76
+ }))
77
+ ```
78
+
79
+ ## `routeWithHenosiaAuth` handler return values
80
+
81
+ The handler may return any of:
82
+ - A `Response` — returned verbatim (security headers applied automatically).
83
+ - A JSON-serializable value — auto-wrapped with `NextResponse.json` (with security headers).
84
+ - `undefined` / `void` — translated to a `204 No Content` response.
85
+
86
+ Every response (success, 401, 500) gets these headers applied if not already set:
87
+ - `Cache-Control: private, no-store`
88
+ - `Vary: Cookie`
89
+
90
+ Do not override these unless you have a specific reason; they keep auth-protected,
91
+ per-user data out of shared and browser caches.
92
+
93
+ ## Security rules — must follow
94
+
95
+ 1. **`accessToken` is a bearer credential.** Treat it like a password.
96
+ - Do **not** log it, echo it back in a response body, embed it in error
97
+ messages, persist it, or send it to untrusted services.
98
+ - Forward over TLS to trusted downstream services only.
99
+ 2. **Never call guards from middleware.** The middleware path uses
100
+ `createHenosiaAuthMiddleware`, which is the lightweight pre-check.
101
+ `requireHenosiaAuth` / `getHenosiaAuth` rely on `next/headers` and
102
+ `next/navigation` and are valid only inside Server Components, layouts,
103
+ Server Actions, and Route Handlers.
104
+ 3. **Every protected handler must call a guard.** Don't rely on the middleware.
105
+ Even paths matched by the middleware can be reached without it (e.g. when
106
+ `matcher` excludes them, when running in tests, or when a future change
107
+ relaxes the matcher).
108
+
109
+ ## Common pitfalls
110
+
111
+ - ❌ `const claims = requireHenosiaAuth()` — missing `await`. The guard returns a Promise.
112
+ - ❌ Calling `requireHenosiaAuth()` from a Route Handler. It will redirect, which
113
+ is the wrong UX for an API. Use `routeWithHenosiaAuth(handler)` instead.
114
+ - ❌ Using `getHenosiaAuth()` to "protect" a page. It returns `null` on
115
+ unauthorized — the page will render without auth. Use `requireHenosiaAuth()`
116
+ for actual protection; reserve `getHenosiaAuth()` for layouts that render
117
+ different UI for signed-in vs signed-out users.
118
+ - ❌ Manually calling `verifyHenosiaAuthToken` and reinventing the
119
+ redirect/401/500 flow. Use the guards; they encode the canonical contract.
120
+ - ❌ Setting `Cache-Control: public` or otherwise loosening cache headers on a
121
+ guard-wrapped response. Per-user JSON must remain `private, no-store`.
package/README.md CHANGED
@@ -120,7 +120,19 @@ import {
120
120
  } from '@henosia/app-next'
121
121
  ```
122
122
 
123
- `verifyHenosiaAuthToken(request)` MUST be called from any protected page/route handler/server action — the
123
+ ## Server-side auth guards
124
+
125
+ Methods to add authorization guards around pages, router handlers, and server actions.
126
+
127
+ ```ts
128
+ import {
129
+ requireHenosiaAuth,
130
+ routeWithHenosiaAuth,
131
+ getHenosiaAuth
132
+ } from '@henosia/app-next/auth/server-guards'
133
+ ```
134
+
135
+ `requireHenosiaAuth` or `routeWithHenosiaAuth` MUST be called from any protected page/route handler/server action — the
124
136
  middleware only performs a fast pre-check.
125
137
 
126
138
  ## App-switcher hook
@@ -159,15 +171,16 @@ export function MyAppSwitcher() {
159
171
 
160
172
  ## Subpath exports
161
173
 
162
- | Subpath | Purpose |
163
- |------------------------------------------------|------------------------------------------------------------|
164
- | `@henosia/app-next` | Server-side helpers (`auth`, `verifyHenosiaAuthToken`, …) |
165
- | `@henosia/app-next/shared` | Shared constants & types (no runtime deps) |
166
- | `@henosia/app-next/auth/middleware` | `createHenosiaAuthMiddleware` |
167
- | `@henosia/app-next/auth/server` | Lower-level server-side auth surface (re-exported by root) |
168
- | `@henosia/app-next/auth/client` | Better-auth `authClient` for React |
169
- | `@henosia/app-next/api/auth` | `GET`, `POST` for `/api/auth/[...all]` |
170
- | `@henosia/app-next/api/platform` | `GET` for `/api/henosia-platform/[...all]` |
171
- | `@henosia/app-next/platform/app-switcher` | `useAppSwitcher` React hook |
172
- | `@henosia/app-next/supabase/server` | Server-side Supabase client factory |
173
- | `@henosia/app-next/supabase/client` | Browser Supabase client factory |
174
+ | Subpath | Purpose |
175
+ |-------------------------------------------|----------------------------------------------------------------------------|
176
+ | `@henosia/app-next` | Server-side helpers (`auth`, `verifyHenosiaAuthToken`, …) |
177
+ | `@henosia/app-next/shared` | Shared constants & types (no runtime deps) |
178
+ | `@henosia/app-next/auth/middleware` | `createHenosiaAuthMiddleware` |
179
+ | `@henosia/app-next/auth/server` | Lower-level server-side auth surface (re-exported by root) |
180
+ | `@henosia/app-next/auth/server-guards` | Page and route auth guards (`requireHenosiaAuth`, `routeWithHenosiaAuth`, ...) |
181
+ | `@henosia/app-next/auth/client` | Better-auth `authClient` for React |
182
+ | `@henosia/app-next/api/auth` | `GET`, `POST` for `/api/auth/[...all]` |
183
+ | `@henosia/app-next/api/platform` | `GET` for `/api/henosia-platform/[...all]` |
184
+ | `@henosia/app-next/platform/app-switcher` | `useAppSwitcher` React hook |
185
+ | `@henosia/app-next/supabase/server` | Server-side Supabase client factory |
186
+ | `@henosia/app-next/supabase/client` | Browser Supabase client factory |
@@ -1,5 +1,6 @@
1
1
  /*! Copyright (c) 2026 Henosia ApS. Licensed under the Henosia Commercial Source License v1.0. See LICENSE */
2
2
  import { henosiaAuthConfig, isUnauthorizedException, verifyHenosiaAuthToken } from "../auth/server.mjs";
3
+ import { applySecurityHeaders } from "../auth/server-guards.mjs";
3
4
  //#region src/api/platform/v1/app-switcher.ts
4
5
  /**
5
6
  * Handler for the Henosia Platform `app-switcher` endpoint.
@@ -15,10 +16,10 @@ async function handleAppSwitcher(request) {
15
16
  redirect: "error",
16
17
  headers: { authorization: `Bearer ${accessTokenResponse.idToken}` }
17
18
  });
18
- return Response.json(await response.json(), {
19
+ return applySecurityHeaders(Response.json(await response.json(), {
19
20
  status: response.status,
20
21
  statusText: response.statusText
21
- });
22
+ }));
22
23
  } catch (e) {
23
24
  if (isUnauthorizedException(e)) return Response.json({ error: "Your session is invalid or expired. Please reload the page to sign in again." }, { status: 401 });
24
25
  console.error("[Henosia Platform] App Switcher API error", e);
@@ -25,6 +25,7 @@ function delayWithCancel(delayMs) {
25
25
  }
26
26
  //#endregion
27
27
  //#region src/auth/middleware.ts
28
+ const INVALID_SESSION_BODY = { error: "Your session is invalid or expired. Please reload the page to sign in again." };
28
29
  /**
29
30
  * Creates a Next.js middleware function that performs the Henosia Auth pre-check.
30
31
  *
@@ -67,14 +68,17 @@ function createHenosiaAuthMiddleware(options = {}) {
67
68
  transferBetterAuthCookiesToNext(request, response);
68
69
  } catch (e) {
69
70
  if (!isUnauthorizedException(e)) {
70
- console.error("[Middleware]", e);
71
+ console.error("[Henosia Auth] Middleware error", e);
71
72
  throw e;
72
73
  }
73
74
  }
74
75
  return response;
75
76
  }
76
77
  if (unauthenticatedPathNames.has(pathname) || !!unauthenticatedPathNamePrefixes.find((p) => pathname.startsWith(p))) return response;
77
- if (!getSessionCookie(request, { cookiePrefix })) return NextResponse.redirect(new URL(HENOSIA_AUTH_SIGN_IN_PATH_NAME, request.url));
78
+ if (!getSessionCookie(request, { cookiePrefix })) {
79
+ if (request.method !== "GET") return NextResponse.json(INVALID_SESSION_BODY, { status: 401 });
80
+ return NextResponse.redirect(new URL(HENOSIA_AUTH_SIGN_IN_PATH_NAME, request.url));
81
+ }
78
82
  try {
79
83
  const { accessTokenResponse, payload, transferBetterAuthCookiesToNext } = await verifyHenosiaAuthToken(request);
80
84
  transferBetterAuthCookiesToNext(request, response);
@@ -84,12 +88,12 @@ function createHenosiaAuthMiddleware(options = {}) {
84
88
  if (process.env.NEXT_PUBLIC_SUPABASE_URL) await exchangeHenosiaTokenForSupabaseSession(request, response, accessTokenResponse.idToken);
85
89
  } catch (e) {
86
90
  if (isUnauthorizedException(e)) {
87
- if (!isPageRequest(request)) return NextResponse.json({ error: "Your session is invalid or expired. Please reload the page to sign in again." }, { status: e.statusCode ?? 401 });
91
+ if (!isPageRequest(request)) return NextResponse.json(INVALID_SESSION_BODY, { status: e.statusCode ?? 401 });
88
92
  const redirectResponse = NextResponse.redirect(new URL(HENOSIA_AUTH_SIGN_IN_PATH_NAME, request.url));
89
93
  removeCachedAccountData(redirectResponse);
90
94
  return redirectResponse;
91
95
  } else {
92
- console.error("[Middleware]", e);
96
+ console.error("[Henosia Auth] Middleware error", e);
93
97
  throw e;
94
98
  }
95
99
  }
@@ -0,0 +1,116 @@
1
+
2
+ import { HenosiaAuthTokenClaims } from "./server.mjs";
3
+ import { NextRequest, NextResponse } from "next/server.js";
4
+
5
+ //#region src/auth/server-guards.d.ts
6
+ /**
7
+ * For Server Components, layouts, and Server Actions.
8
+ *
9
+ * Verifies the current user's Henosia Auth token and returns the verified
10
+ * claims. On unauthorized, redirects to the Henosia sign-in page (the canonical
11
+ * page-level UX); other errors propagate so Next.js' error boundaries / 500
12
+ * page take over.
13
+ *
14
+ * Wrapped in `React.cache` so multiple components in the same render share a
15
+ * single verification.
16
+ *
17
+ * @example
18
+ * ```tsx
19
+ * // app/dashboard/page.tsx
20
+ * import { requireHenosiaAuth } from '@/lib/henosia-auth'
21
+ *
22
+ * export default async function Page() {
23
+ * const claims = await requireHenosiaAuth()
24
+ * return <Dashboard org={claims['https://henosia.com/organization']} />
25
+ * }
26
+ * ```
27
+ */
28
+ declare const requireHenosiaAuth: () => Promise<HenosiaAuthTokenClaims>;
29
+ /**
30
+ * Non-redirecting variant of {@link requireHenosiaAuth} for layouts and
31
+ * conditional UI (e.g., navbars that need to render either a "Sign in" button
32
+ * or a user menu). Returns `null` when the user is not authenticated; other
33
+ * errors propagate.
34
+ *
35
+ * @example
36
+ * ```tsx
37
+ * // app/layout.tsx
38
+ * import { getHenosiaAuth } from '@/lib/henosia-auth'
39
+ *
40
+ * export default async function RootLayout({ children }) {
41
+ * const claims = await getHenosiaAuth()
42
+ * return <Shell user={claims?.['https://henosia.com/organization']}>{children}</Shell>
43
+ * }
44
+ * ```
45
+ */
46
+ declare const getHenosiaAuth: () => Promise<HenosiaAuthTokenClaims | null>;
47
+ /**
48
+ * Context object passed to a `routeWithHenosiaAuth` handler
49
+ */
50
+ interface HenosiaAuthContext {
51
+ /** Verified Henosia Auth JWT claims (project, organization, preview flag). */
52
+ payload: HenosiaAuthTokenClaims;
53
+ /**
54
+ * OAuth 2.0 access token, e.g. for forwarding to a downstream API as a
55
+ * `Authorization: Bearer …` header.
56
+ *
57
+ * **Security:** this is a bearer credential. Do **not** log it, echo it back
58
+ * in a response body, embed it in error messages, or persist it. Forward
59
+ * over TLS to trusted downstream services only. Treat it like a password.
60
+ */
61
+ accessToken: string;
62
+ }
63
+ /**
64
+ * A JSON-serializable value that the wrapper will auto-wrap with `NextResponse.json`.
65
+ */
66
+ type JsonValue = string | number | boolean | null | JsonValue[] | {
67
+ [key: string]: JsonValue | undefined;
68
+ };
69
+ /**
70
+ * Allowed return types from a `routeWithHenosiaAuth` handler:
71
+ * - A `Response` (returned verbatim — caller controls status/headers/body).
72
+ * - A JSON-serializable value (auto-wrapped via `NextResponse.json`).
73
+ * - `void` / `undefined` → translated to a `204 No Content` response.
74
+ */
75
+ type HandlerReturn = Response | JsonValue | void;
76
+ type RouteParams<TParams> = TParams | Promise<TParams>;
77
+ type Handler<TParams> = (request: NextRequest, ctx: HenosiaAuthContext & {
78
+ params: TParams;
79
+ }) => HandlerReturn | Promise<HandlerReturn>;
80
+ declare function applySecurityHeaders(response: Response): Response;
81
+ declare function unauthorizedResponse(): NextResponse;
82
+ /**
83
+ * For Next.js Route Handlers (`app/api/**\/route.ts`).
84
+ *
85
+ * Wraps a handler so that:
86
+ * - The request is verified up front.
87
+ * - On success, the handler receives `{ payload, accessToken, params }` and
88
+ * may return either a `Response`, any JSON-serializable value
89
+ * (auto-wrapped via `NextResponse.json`), or `undefined` (→ `204
90
+ * No Content`).
91
+ * - On unauthorized, returns a canonical HTTP `401 Unauthorized` response
92
+ * (RFC 7235): never a redirect.
93
+ * - On any other error (verifying the token *or* thrown by the handler),
94
+ * it's logged with the request's pathname for traceability and a generic
95
+ * `500 Internal Server Error` JSON response is returned (no stack /
96
+ * internal details leaked) — the same OAuth-shaped contract regardless of
97
+ * where the failure originated.
98
+ *
99
+ * Preserves the standard Next.js `(request, { params })` signature, including
100
+ * Next 15's promise-typed `params`.
101
+ *
102
+ * @example
103
+ * ```ts
104
+ * // app/api/me/route.ts
105
+ * import { routeWithHenosiaAuth } from '@/lib/henosia-auth'
106
+ *
107
+ * export const GET = routeWithHenosiaAuth((_req, { payload }) => ({
108
+ * organization: payload['https://henosia.com/organization'],
109
+ * }))
110
+ * ```
111
+ */
112
+ declare function routeWithHenosiaAuth<TParams = Record<string, string | string[] | undefined>>(handler: Handler<TParams>): (request: NextRequest, routeCtx: {
113
+ params: RouteParams<TParams>;
114
+ }) => Promise<Response>;
115
+ //#endregion
116
+ export { HenosiaAuthContext, applySecurityHeaders, getHenosiaAuth, requireHenosiaAuth, routeWithHenosiaAuth, unauthorizedResponse };