@henosia/app-next 1.0.3 → 1.0.5

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 (20) 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/dist/auth/server.mjs +2 -1
  20. package/package.json +2 -1
@@ -0,0 +1,228 @@
1
+ ---
2
+ name: henosia-app-next
3
+ description: |
4
+ Integrate the `@henosia/app-next` package into a Next.js app: install,
5
+ configure middleware, mount the better-auth and Henosia Platform catch-all
6
+ routes, wire up the sign-in page, and (optionally) add the app switcher and
7
+ Supabase helpers. Load this skill when adding `@henosia/app-next` to a
8
+ project, scaffolding a new Henosia-Auth-backed Next.js app, or troubleshooting
9
+ the integration scaffolding (middleware, sign-in route, providers,
10
+ re-exports). For per-page / per-route authorization once integration is in
11
+ place, use the `henosia-auth-guards` skill instead.
12
+ enabled: false # flip to true to enable during Henosia Auth integration in projects with legacy auth
13
+ ---
14
+
15
+ # Integrating `@henosia/app-next`
16
+
17
+ `@henosia/app-next` is the Henosia integration for Next.js apps. It provides:
18
+
19
+ - Henosia Auth middleware (pre-check + token refresh + optional Supabase session exchange).
20
+ - better-auth and Henosia Platform catch-all Route Handlers.
21
+ - A React `authClient` (better-auth) and a `useAppSwitcher` hook.
22
+ - Optional Supabase server/browser client factories.
23
+ - Server-side auth guards — see the **`henosia-auth-guards` skill** for those
24
+ (this skill covers the one-time integration; that skill covers the per-route
25
+ authorization work an agent does on every protected page/route/server action).
26
+
27
+ ## Authoritative reference
28
+
29
+ For complete and up-to-date package usage details (peer dependencies, subpath
30
+ exports, Supabase exchange behavior, app-switcher hook options), read:
31
+
32
+ - `node_modules/@henosia/app-next/README.md`
33
+
34
+ The README is the source of truth. This skill summarizes the integration
35
+ workflow and explains how to use the bundled reference template.
36
+
37
+ ## Environment variables — do not edit
38
+
39
+ `@henosia/app-next` requires several env vars (`BETTER_AUTH_URL`,
40
+ `BETTER_AUTH_SECRET`, `HENOSIA_AUTH_*`, optional `NEXT_PUBLIC_SUPABASE_*`). In
41
+ the consuming app, **these are provided automatically by the Henosia builder
42
+ environment** which is where this skill is invoked from.
43
+
44
+ - ❌ Do **not** add Henosia env vars to `.env.local`, deployment
45
+ config, or CI scripts.
46
+ - ❌ Do **not** read or echo their values back to the user.
47
+ - ✅ Treat the env as a managed contract. If a value is missing at runtime, the
48
+ package surfaces a `[Henosia Auth] Missing required env var: …` error — that
49
+ is a Henosia environment issue, not a code change.
50
+ - See the README "Required environment variables" section for the full list and
51
+ what each one is for, but you should not need to set any of them yourself.
52
+
53
+ ## Reference template (use this as the starting point)
54
+
55
+ The package ships a working reference template at:
56
+
57
+ ```
58
+ node_modules/@henosia/app-next/.agents/skills/henosia-app-next/assets/template/
59
+ ```
60
+
61
+ Use it as the source for all integration files. **Always copy or merge from the
62
+ template — do not re-derive these files from scratch.**
63
+
64
+ ### File-to-target-path map
65
+
66
+ | Template file | Target in consuming app | Required? |
67
+ |----------------------------------------------|----------------------------------------------|--------------------------------------------|
68
+ | `middleware.ts` | `middleware.ts` (app root) | Required |
69
+ | `app/api/auth/[...all]/route.ts` | Same path | Required |
70
+ | `app/api/henosia-platform/[...all]/route.ts` | Same path | Required |
71
+ | `lib/auth-client.ts` | `lib/auth-client.ts` (or app's equivalent) | Required |
72
+ | `app/(auth)/sign-in/page.tsx` | A route that resolves to `/sign-in` | Required |
73
+ | `app/(auth)/sign-in/loading.tsx` | Same folder as the sign-in page | Recommended |
74
+ | `components/sign-in.tsx` | `components/sign-in.tsx` | Required (used by the sign-in page) |
75
+ | `app/layout.tsx` | Merge into existing root layout | Required (partial merge) |
76
+ | `components/query-provider.tsx` | `components/query-provider.tsx` | Required if using the app switcher |
77
+ | `components/app-switcher.tsx` | `components/app-switcher.tsx` | Optional (only if surfacing the switcher) |
78
+ | `components/nav-user.tsx` | `components/nav-user.tsx` | Optional (only if surfacing user identity) |
79
+ | `lib/supabase/server.ts` | `lib/supabase/server.ts` | Optional (only if using Supabase) |
80
+ | `lib/supabase/client.ts` | `lib/supabase/client.ts` | Optional (only if using Supabase) |
81
+ | `package.json` | Additive merge into existing `package.json` | Required (additive) |
82
+ | `tsconfig.json` | Additive merge into existing `tsconfig.json` | Required (additive) |
83
+ | `next.config.mjs` | Additive merge into existing `next.config.*` | Required (additive) |
84
+
85
+ The sign-in page lives under the `(auth)` Next.js route group in the template.
86
+ The route group itself is an organization choice; what matters is that the
87
+ URL path resolves to `/sign-in` (the value of `HENOSIA_AUTH_SIGN_IN_PATH_NAME`).
88
+
89
+ ### Template conventions — follow exactly
90
+
91
+ When copying a template file into the consuming app, apply these rules:
92
+
93
+ 1. **Strip `//@ts-nocheck (remove when using the template)`** from the first
94
+ line of every copied file. It exists only so the template files type-check
95
+ in isolation here.
96
+ 2. **Strip `// Template use guidance: …`** lines. They are instructions for the
97
+ integrating agent (you), not for the consuming app.
98
+ 3. **Preserve verbatim** every comment from `// To Henosia assistant regarding
99
+ this file:` to end-of-file. These currently appear in `middleware.ts` and
100
+ `app/api/auth/[...all]/route.ts`. They protect the Henosia Auth flows from
101
+ well-intentioned future edits and **must remain in the consuming app's
102
+ files**, byte-for-byte.
103
+ 4. **`package.json` is an additive merge.** The template uses
104
+ `"__add_these__:"` and `"__others_omitted_for_brevity__"` sentinel keys to
105
+ mark merge boundaries. Add the listed deps to the consuming app's existing
106
+ `dependencies` (preserve everything already there). Use the template's
107
+ pinned versions for `@henosia/app-next`, `@tanstack/react-query`, and
108
+ `better-auth` unless the consuming app already has a newer compatible
109
+ version. Drop the sentinel keys when merging.
110
+ 5. **`tsconfig.json` is an additive merge.** Ensure the consuming app has the
111
+ `@/*` path alias under `compilerOptions.paths` and the `next` plugin under
112
+ `compilerOptions.plugins`; the template's other settings are typical Next.js
113
+ defaults — only fold them in if missing.
114
+ 6. **`app/layout.tsx` is a partial merge.** Preserve the host app's existing
115
+ layout structure (fonts, metadata, theming, providers, slots, etc.). Fold
116
+ in only:
117
+ - The `usePathname()` import + `path !== HENOSIA_AUTH_SIGN_IN_PATH_NAME`
118
+ branching, so the sign-in page renders without the host app's chrome.
119
+ - The `<QueryProvider>` wrap (required for the app switcher and any other
120
+ `@tanstack/react-query` consumer).
121
+ The template comment `// Preserve the existing layout elements as it stands
122
+ today when using this template` is the directive — follow it.
123
+ 7. **`components/sign-in.tsx`, `app-switcher.tsx`, `nav-user.tsx` are
124
+ reference UIs.** Adapt the visuals to the host app's design system, but
125
+ **preserve the Henosia-specific behavior**:
126
+ - `sign-in.tsx`: keep `signIn.social({ provider: "henosia", callbackURL: "/" })`,
127
+ the `data-henosia-auth-sign-in` attribute on the trigger, and the
128
+ `loading` UX around the call.
129
+ - `app-switcher.tsx`: keep the `useAppSwitcher({ enabled: open })` hook
130
+ usage, the `groupedApps` rendering, and the `error` / `isLoading` paths.
131
+ - `nav-user.tsx`: keep `authClient.useSession()`, and on sign-out keep the
132
+ `router.push(HENOSIA_AUTH_SIGN_IN_PATH_NAME)` + `router.refresh()`
133
+ sequence inside `authClient.signOut`'s `onSuccess`.
134
+ 8. **The thin re-export files** (`lib/auth-client.ts`, `lib/supabase/server.ts`,
135
+ `lib/supabase/client.ts`) exist so the rest of the consuming app imports
136
+ from `@/lib/...` and the `@henosia/app-next` subpaths are referenced from
137
+ exactly one place. Keep them as one-line re-exports.
138
+
139
+ ## Integration checklist
140
+
141
+ Work through these in order. Most steps map directly to a template file or a
142
+ README section.
143
+
144
+ 1. **Install the package and peer dependencies.** See README "Installation".
145
+ Use `pnpm`/`npm`/`yarn` per the host app's convention. Install
146
+ `@supabase/ssr` and `@supabase/supabase-js` only if the app uses Supabase.
147
+ 2. **Do not configure env vars.** They are provided by the Henosia builder
148
+ environment (see "Environment variables — do not edit" above).
149
+ 3. **Add `middleware.ts`** at the app root. Copy from the template, applying
150
+ the template-conventions rules. Keep the `// To Henosia assistant…` block.
151
+ 4. **Mount `app/api/auth/[...all]/route.ts`** by re-exporting `GET, POST` from
152
+ `@henosia/app-next/api/auth`. Copy from the template; keep the `// To
153
+ Henosia assistant…` block.
154
+ 5. **Mount `app/api/henosia-platform/[...all]/route.ts`** by re-exporting `GET`
155
+ from `@henosia/app-next/api/platform`. Copy from the template.
156
+ 6. **Add the React auth client re-export** at `lib/auth-client.ts`.
157
+ 7. **Create the sign-in route at `/sign-in`.** Copy `app/(auth)/sign-in/page.tsx`,
158
+ `app/(auth)/sign-in/loading.tsx`, and `components/sign-in.tsx`. The
159
+ route group is optional; the resolved URL must be `/sign-in`.
160
+ 8. **Merge `app/layout.tsx`** per the partial-merge rule. Wrap the tree with
161
+ `<QueryProvider>` and skip the host app's chrome on
162
+ `HENOSIA_AUTH_SIGN_IN_PATH_NAME`.
163
+ 9. **Add `<QueryProvider>`** (`components/query-provider.tsx`) if you don't
164
+ already have a `QueryClientProvider` higher in the tree. Required for
165
+ `useAppSwitcher` and any other `@tanstack/react-query` consumer.
166
+ 10. **(Optional) Add the app switcher.** Copy `components/app-switcher.tsx`
167
+ where you want it in your shell (e.g. inside a `<Sidebar>`). It depends on
168
+ shadcn/ui primitives — install the ones it imports if missing.
169
+ 11. **(Optional) Add the user nav / sign-out menu.** Copy
170
+ `components/nav-user.tsx`. Adapt visuals; keep the Henosia behavior.
171
+ 12. **(Optional) Add Supabase helpers.** When `NEXT_PUBLIC_SUPABASE_URL` is
172
+ set in the Henosia builder env, install the Supabase peer deps and copy
173
+ `lib/supabase/server.ts` and `lib/supabase/client.ts` from the template.
174
+ The middleware automatically exchanges the Henosia Auth token for a
175
+ Supabase session — no extra wiring required. See README "5. (Optional)
176
+ Supabase helpers".
177
+ 13. **Protect every non-public page, layout, server action, and route
178
+ handler.** Use the **`henosia-auth-guards` skill**. The middleware is a
179
+ pre-check only — it does **not** satisfy the per-route authorization
180
+ requirement.
181
+ 14. Carefully remove the old auth flow components and routes that Henosia Auth replaces
182
+
183
+ ## Subpath imports cheat sheet
184
+
185
+ ```ts
186
+ // Server-side helpers (advanced; prefer the auth guards)
187
+ import { auth, verifyHenosiaAuthToken, isUnauthorizedException, isPageRequest, HENOSIA_AUTH_SIGN_IN_PATH_NAME } from '@henosia/app-next'
188
+
189
+ // Shared constants & types (no runtime deps; safe to import from server or client)
190
+ import { HENOSIA_AUTH_SIGN_IN_PATH_NAME, HENOSIA_AUTH_GET_SESSION_PATH_NAME, HENOSIA_ORGANIZATION_CTX_COOKIE } from '@henosia/app-next/shared'
191
+ import type { HenosiaOrganizationContext } from '@henosia/app-next/shared'
192
+
193
+ // Middleware
194
+ import { createHenosiaAuthMiddleware } from '@henosia/app-next/auth/middleware'
195
+
196
+ // Server-side auth guards (see henosia-auth-guards skill)
197
+ import { requireHenosiaAuth, getHenosiaAuth, routeWithHenosiaAuth } from '@henosia/app-next/auth/server-guards'
198
+
199
+ // React auth client
200
+ import { authClient } from '@henosia/app-next/auth/client'
201
+
202
+ // Catch-all route re-exports
203
+ export { GET, POST } from '@henosia/app-next/api/auth'
204
+ export { GET } from '@henosia/app-next/api/platform'
205
+
206
+ // App-switcher hook
207
+ import { useAppSwitcher } from '@henosia/app-next/platform/app-switcher'
208
+
209
+ // Supabase helpers (peer deps required)
210
+ import { createClient } from '@henosia/app-next/supabase/server'
211
+ import { createClient } from '@henosia/app-next/supabase/client'
212
+ ```
213
+
214
+ ## Lower-level helpers — usually not needed
215
+
216
+ `auth`, `verifyHenosiaAuthToken`, `isUnauthorizedException`, `isPageRequest` are
217
+ exported from the package root for advanced/edge cases. For protecting pages
218
+ and routes, **always prefer the guards** documented in the
219
+ `henosia-auth-guards` skill. They encode the canonical redirect / 401 / 500
220
+ contract, apply the correct security headers, and integrate with React's
221
+ `cache()` for per-render deduplication.
222
+
223
+ ## See also
224
+
225
+ - `henosia-auth-guards` skill — the per-page/per-route authorization workflow.
226
+ You will need this on virtually every Henosia-Auth-backed Next.js project.
227
+ - `@henosia/app-next` README at `node_modules/@henosia/app-next/README.md`.
228
+ - Reference template at `node_modules/@henosia/app-next/.agents/skills/henosia-app-next/assets/template/`.
@@ -0,0 +1,10 @@
1
+ export default function SignInLoading() {
2
+ return (
3
+ <div className="flex min-h-screen items-center justify-center">
4
+ <div className="text-center">
5
+ <div className="h-8 w-8 animate-spin rounded-full border-4 border-gray-300 border-t-gray-900 mx-auto"></div>
6
+ <p className="mt-4 text-gray-600">Loading...</p>
7
+ </div>
8
+ </div>
9
+ );
10
+ }
@@ -0,0 +1,15 @@
1
+ "use client";
2
+ import {SignIn} from "@/components/sign-in";
3
+
4
+ export default function Page() {
5
+
6
+ return (
7
+ <div className="w-full">
8
+ <div className="flex items-center flex-col justify-center w-full md:py-10">
9
+ <div className="md:w-[400px]">
10
+ <SignIn />
11
+ </div>
12
+ </div>
13
+ </div>
14
+ );
15
+ }
@@ -0,0 +1,9 @@
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
+ export { GET, POST } from '@henosia/app-next/api/auth'
4
+
5
+ // To Henosia assistant regarding this file:
6
+ // Henosia Auth is a managed solution that is outside your scope of work.
7
+ // Henosia Auth flows must be preserved at all times.
8
+ // All non-public pages and routes must be protected using `requireHenosiaAuth` or `routeWithHenosiaAuth` from `@henosia/app-next`.
9
+ // Any potential issues with Henosia Auth flows must be referred to Henosia technical support: https://docs.henosia.com/support
@@ -0,0 +1,2 @@
1
+ //@ts-nocheck (remove when using the template)
2
+ export { GET } from '@henosia/app-next/api/platform'
@@ -0,0 +1,35 @@
1
+ //@ts-nocheck (remove when using the template)
2
+ // this partial layout file illustrates how to modify the layout based on whether the path is the sign in page
3
+ // it should be merged with the existing layout's structure and dependencies
4
+ "use client";
5
+ import type * as React from "react";
6
+ import { usePathname } from "next/navigation";
7
+ import {HENOSIA_AUTH_SIGN_IN_PATH_NAME} from "@henosia/app-next/shared";
8
+ import { QueryProvider } from "@/components/query-provider";
9
+
10
+ // ... other required imports and setup, e.g. fonts
11
+
12
+ export default function RootLayout({
13
+ children
14
+ }: Readonly<{
15
+ children: React.ReactNode;
16
+ breadcrumb: React.ReactNode;
17
+ }>) {
18
+ const path = usePathname();
19
+ return (
20
+ <html lang="en" suppressHydrationWarning>
21
+ <body>
22
+ <QueryProvider>
23
+ {path !== HENOSIA_AUTH_SIGN_IN_PATH_NAME ? (
24
+ <>
25
+ {/* Preserve the existing layout elements as it stands today when using this template */}
26
+ <main>{children}</main>
27
+ </>
28
+ ) : (
29
+ children
30
+ )}
31
+ </QueryProvider>
32
+ </body>
33
+ </html>
34
+ );
35
+ }
@@ -0,0 +1,148 @@
1
+ //@ts-nocheck (remove when using the template)
2
+ "use client"
3
+
4
+ import * as React from "react"
5
+ import {useState} from "react";
6
+ import {ChevronsUpDown, GalleryVerticalEnd, Plus, PanelTop} from "lucide-react"
7
+
8
+ import {
9
+ Popover,
10
+ PopoverContent,
11
+ PopoverTrigger,
12
+ } from "@/components/ui/popover"
13
+ import {
14
+ Command,
15
+ CommandEmpty,
16
+ CommandGroup,
17
+ CommandInput,
18
+ CommandItem,
19
+ CommandList,
20
+ CommandSeparator,
21
+ } from "@/components/ui/command"
22
+ import {
23
+ SidebarMenu,
24
+ SidebarMenuButton,
25
+ SidebarMenuItem,
26
+ useSidebar,
27
+ } from "@/components/ui/sidebar"
28
+ import {Skeleton} from "@/components/ui/skeleton";
29
+ import {AppSwitcherApp, useAppSwitcher} from "@henosia/app-next/platform/app-switcher";
30
+
31
+ export function AppSwitcher() {
32
+ const { isMobile } = useSidebar();
33
+ const [open, setOpen] = useState(false);
34
+
35
+ const {organization, groupedApps, error, isLoading} = useAppSwitcher({
36
+ enabled: open
37
+ });
38
+
39
+ const handleSelectApp = (app: AppSwitcherApp) => {
40
+ window.open(app.url);
41
+ setOpen(false);
42
+ };
43
+
44
+ return (
45
+ <SidebarMenu>
46
+ <SidebarMenuItem>
47
+ <Popover open={open} onOpenChange={setOpen}>
48
+ <PopoverTrigger asChild>
49
+ <SidebarMenuButton
50
+ size="lg"
51
+ className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
52
+ >
53
+ <div className="flex aspect-square size-8 items-center justify-center rounded-lg text-sidebar-primary-foreground">
54
+ {organization ? (
55
+ <>
56
+ {organization.logoUrl ? (
57
+ <>
58
+ {/* eslint-disable-next-line @next/next/no-img-element */}
59
+ <img alt={`Logo for ${organization.name}`} src={organization.logoUrl} className={"size-6"} />
60
+ </>
61
+ ) : (
62
+ <GalleryVerticalEnd className={"size-4 text-primary"} />
63
+ )}
64
+ </>
65
+ ) : (
66
+ <Skeleton className={"size-6 opacity-40 rounded-lg"} />
67
+ )}
68
+ </div>
69
+ <div className="grid flex-1 text-left text-sm leading-tight">
70
+ <span className="truncate font-semibold">
71
+ {organization?.name ?? <Skeleton className={"w-16 h-[1em] opacity-40"} />}
72
+ </span>
73
+ </div>
74
+ <ChevronsUpDown className="ml-auto" />
75
+ </SidebarMenuButton>
76
+ </PopoverTrigger>
77
+ <PopoverContent
78
+ className="w-[22rem] rounded-lg p-0"
79
+ align="start"
80
+ side={isMobile ? "bottom" : "right"}
81
+ sideOffset={4}
82
+ >
83
+ <Command>
84
+ <CommandInput placeholder="Search apps…" />
85
+ <CommandList className="max-h-[80vh]">
86
+ {isLoading ? (
87
+ <div className="flex flex-col gap-1 p-2" role="status" aria-label="Loading apps">
88
+ {Array.from({ length: 3 }).map((_, i) => (
89
+ <React.Fragment key={i}>
90
+ {i === 0 && (<div className={"p-2"}><Skeleton className="h-2 w-24" /></div>)}
91
+ <div key={i} className="flex items-center gap-2 p-2">
92
+ <Skeleton className="size-6 rounded-sm" />
93
+ <Skeleton className="h-3 flex-1" />
94
+ </div>
95
+ </React.Fragment>
96
+ ))}
97
+ </div>
98
+ ) : error ? (
99
+ <div className="p-4 text-sm text-destructive">
100
+ {error.message ?? 'Failed to load apps.'}
101
+ </div>
102
+ ) : (
103
+ <>
104
+ <CommandEmpty>No apps found.</CommandEmpty>
105
+ {groupedApps.map(({ group, apps }) => (
106
+ <CommandGroup key={group} heading={group}>
107
+ {apps.map((app) => (
108
+ <CommandItem
109
+ key={app.id}
110
+ value={`${group} ${app.name}`}
111
+ onSelect={() => handleSelectApp(app)}
112
+ className="gap-2 p-2"
113
+ title={`${app.name} – ${app.url}`}
114
+ >
115
+ <div className="flex size-6 items-center justify-center rounded-sm border bg-background">
116
+ <PanelTop className="size-4 shrink-0" />
117
+ </div>
118
+ <span className="truncate">{app.name}</span>
119
+ </CommandItem>
120
+ ))}
121
+ </CommandGroup>
122
+ ))}
123
+ </>
124
+ )}
125
+ <CommandSeparator />
126
+ <CommandGroup>
127
+ <CommandItem
128
+ disabled={isLoading}
129
+ onSelect={() => {
130
+ window.open('https://app.henosia.com/app');
131
+ setOpen(false)
132
+ }}
133
+ className="gap-2 p-2"
134
+ >
135
+ <div className="flex size-6 items-center justify-center rounded-md border bg-background">
136
+ <Plus className="size-4" />
137
+ </div>
138
+ <div className="font-medium text-muted-foreground">Build new app</div>
139
+ </CommandItem>
140
+ </CommandGroup>
141
+ </CommandList>
142
+ </Command>
143
+ </PopoverContent>
144
+ </Popover>
145
+ </SidebarMenuItem>
146
+ </SidebarMenu>
147
+ )
148
+ }
@@ -0,0 +1,143 @@
1
+ //@ts-nocheck (remove when using the template)
2
+ "use client"
3
+
4
+ import {
5
+ BadgeCheck,
6
+ ChevronsUpDown,
7
+ LogOut,
8
+ } from "lucide-react"
9
+
10
+ import {
11
+ Avatar,
12
+ AvatarFallback,
13
+ AvatarImage,
14
+ } from "@/components/ui/avatar"
15
+ import {
16
+ DropdownMenu,
17
+ DropdownMenuContent,
18
+ DropdownMenuGroup,
19
+ DropdownMenuItem,
20
+ DropdownMenuLabel,
21
+ DropdownMenuSeparator,
22
+ DropdownMenuTrigger,
23
+ } from "@/components/ui/dropdown-menu"
24
+ import {
25
+ SidebarMenu,
26
+ SidebarMenuButton,
27
+ SidebarMenuItem,
28
+ useSidebar,
29
+ } from "@/components/ui/sidebar"
30
+ import { Skeleton } from "@/components/ui/skeleton"
31
+ import Link from "next/link"
32
+ import { useRouter } from "next/navigation"
33
+ import { authClient } from "@/lib/auth-client";
34
+ import { HENOSIA_AUTH_SIGN_IN_PATH_NAME } from "@henosia/app-next/shared"
35
+
36
+ function getInitials(name?: string | null): string {
37
+ if (!name) return ""
38
+ const parts = name.trim().split(/\s+/).filter(Boolean)
39
+ if (parts.length === 0) return ""
40
+ if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase()
41
+ return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase()
42
+ }
43
+
44
+ export function NavUser() {
45
+ const { isMobile } = useSidebar()
46
+ const router = useRouter()
47
+ const { data: session, isPending } = authClient.useSession()
48
+
49
+ if (isPending) {
50
+ return (
51
+ <SidebarMenu>
52
+ <SidebarMenuItem>
53
+ <SidebarMenuButton size="lg" disabled>
54
+ <Skeleton className="h-8 w-8 rounded-lg" />
55
+ <div className="grid flex-1 gap-1 text-left text-sm leading-tight">
56
+ <Skeleton className="h-3 w-24" />
57
+ <Skeleton className="h-3 w-32" />
58
+ </div>
59
+ </SidebarMenuButton>
60
+ </SidebarMenuItem>
61
+ </SidebarMenu>
62
+ )
63
+ }
64
+
65
+ const sessionUser = session?.user
66
+ if (!sessionUser) {
67
+ return null
68
+ }
69
+
70
+ const name = sessionUser.name ?? ""
71
+ const email = sessionUser.email ?? ""
72
+ const avatar = sessionUser.image ?? undefined
73
+ const initials = getInitials(name || email)
74
+
75
+ const handleSignOut = async () => {
76
+ await authClient.signOut({
77
+ fetchOptions: {
78
+ onSuccess: () => {
79
+ router.push(HENOSIA_AUTH_SIGN_IN_PATH_NAME)
80
+ router.refresh()
81
+ },
82
+ },
83
+ })
84
+ }
85
+
86
+ return (
87
+ <SidebarMenu>
88
+ <SidebarMenuItem>
89
+ <DropdownMenu>
90
+ <DropdownMenuTrigger asChild>
91
+ <SidebarMenuButton
92
+ size="lg"
93
+ className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
94
+ >
95
+ <Avatar className="h-8 w-8 rounded-lg">
96
+ {avatar ? <AvatarImage src={avatar} alt={name} /> : null}
97
+ <AvatarFallback className="rounded-lg">{initials}</AvatarFallback>
98
+ </Avatar>
99
+ <div className="grid flex-1 text-left text-sm leading-tight">
100
+ <span className="truncate font-semibold">{name}</span>
101
+ <span className="truncate text-xs">{email}</span>
102
+ </div>
103
+ <ChevronsUpDown className="ml-auto size-4" />
104
+ </SidebarMenuButton>
105
+ </DropdownMenuTrigger>
106
+ <DropdownMenuContent
107
+ className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
108
+ side={isMobile ? "bottom" : "right"}
109
+ align="end"
110
+ sideOffset={4}
111
+ >
112
+ <DropdownMenuLabel className="p-0 font-normal">
113
+ <div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
114
+ <Avatar className="h-8 w-8 rounded-lg">
115
+ {avatar ? <AvatarImage src={avatar} alt={name} /> : null}
116
+ <AvatarFallback className="rounded-lg">{initials}</AvatarFallback>
117
+ </Avatar>
118
+ <div className="grid flex-1 text-left text-sm leading-tight">
119
+ <span className="truncate font-semibold">{name}</span>
120
+ <span className="truncate text-xs">{email}</span>
121
+ </div>
122
+ </div>
123
+ </DropdownMenuLabel>
124
+ <DropdownMenuSeparator />
125
+ <DropdownMenuGroup>
126
+ <DropdownMenuItem asChild={true}>
127
+ <Link target={'_blank'} href={"https://app.henosia.com/settings/profile"}>
128
+ <BadgeCheck />
129
+ Profile
130
+ </Link>
131
+ </DropdownMenuItem>
132
+ </DropdownMenuGroup>
133
+ <DropdownMenuSeparator />
134
+ <DropdownMenuItem onSelect={handleSignOut}>
135
+ <LogOut />
136
+ Log out
137
+ </DropdownMenuItem>
138
+ </DropdownMenuContent>
139
+ </DropdownMenu>
140
+ </SidebarMenuItem>
141
+ </SidebarMenu>
142
+ )
143
+ }
@@ -0,0 +1,23 @@
1
+ //@ts-nocheck (remove when using the template)
2
+ "use client";
3
+
4
+ import * as React from "react";
5
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
6
+
7
+ export function QueryProvider({ children }: { children: React.ReactNode }) {
8
+ const [queryClient] = React.useState(
9
+ () =>
10
+ new QueryClient({
11
+ defaultOptions: {
12
+ queries: {
13
+ staleTime: 60 * 1000,
14
+ refetchOnWindowFocus: false,
15
+ },
16
+ },
17
+ })
18
+ );
19
+
20
+ return (
21
+ <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
22
+ );
23
+ }
@@ -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`.
@@ -59,7 +59,8 @@ const henosiaAuthConfig = {
59
59
  * Gets whether the specified request is for a page (a top level page or iframe page)
60
60
  */
61
61
  function isPageRequest(request) {
62
- const dest = request.headers.get("Sec-Fetch-Dest");
62
+ const { headers } = request;
63
+ const dest = headers.get("X-Henosia-Fetch-Dest") ?? headers.get("Sec-Fetch-Dest");
63
64
  if (dest === "document" || dest === "iframe" || dest === "fencedframe") return true;
64
65
  const isRsc = request.headers.get("RSC") === "1";
65
66
  const isPrefetch = request.headers.get("Next-Router-Prefetch") === "1";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@henosia/app-next",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
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": {