@xzibit/app-kit
v0.2.0
Published
Shared server/auth layer for the Xzibit Apps portfolio: launcher-JWT verification, requireAuth + redirect-to-launcher, same-origin feedback proxy factories, and a fail-loud env manifest. The server-side companion to @xzibit/ui (client/UI).
Maintainers
Readme
@xzibit/app-kit
Shared server/auth layer for the Xzibit Apps portfolio. The companion to
@xzibit/ui:
| Package | Layer |
| --- | --- |
| @xzibit/ui | client / UI — TopBar, FeedbackButton, BuildBadge |
| @xzibit/app-kit | server / auth / proxy — launcher-JWT verify, requireAuth, feedback proxy, env manifest |
Improvements propagate by version bump — no per-app copy-paste, no drift.
Install
npm install @xzibit/app-kit josenext is a peer dependency (provided by the app).
Two entry points
@xzibit/app-kit— server (route handlers / server components). Usesnext/headers.@xzibit/app-kit/middleware— edge-safe (Next.js middleware).next/server+ jose only.
Never import the main entry from middleware.
Wiring (what the template does for you)
1. Middleware — middleware.ts
import { createAuthMiddleware } from '@xzibit/app-kit/middleware';
export const middleware = createAuthMiddleware();
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico|api/).*)'],
};Captures the ?access_token= launcher SSO handoff into the auth_token HttpOnly
cookie, requires a valid launcher JWT, and redirects to <launcher>/login?redirect=…
when unauthenticated.
2. Feedback proxies — two lines each
// app/api/feedback/route.ts
import { createFeedbackProxy } from '@xzibit/app-kit';
export const POST = createFeedbackProxy();
// app/api/feedback/clarify/route.ts
import { createClarifyProxy } from '@xzibit/app-kit';
export const POST = createClarifyProxy();Reads the launcher JWT cookie server-side and forwards to the launcher with
Authorization: Bearer, preserving the single shared submissions table.
3. Env manifest — fail loud — instrumentation.ts
import { assertServerEnv, verifyJwtSecretFingerprint } from '@xzibit/app-kit';
export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
assertServerEnv(); // absent/malformed secret → throw at boot
await verifyJwtSecretFingerprint(); // present-but-STALE secret → fail loud (prod)
}
}This is the fix for the wrong/missing-SUPABASE_JWT_SECRET login-loop bug
(LED / Org Chart / X-Mark): a bad secret becomes a loud startup error, not a
silent relogin loop. Pass { requireServiceRole: true } if the app uses the
Supabase admin client.
verifyJwtSecretFingerprint() closes the present-but-stale gap assertServerEnv
can't: it fetches the launcher's published fingerprint
(GET /api/auth/jwt-secret-fingerprint — a one-way SHA-256 hash; no secret/token ever
leaves the launcher) and compares it to sha256(SUPABASE_JWT_SECRET). A definite
mismatch throws in production (console.error in dev) — "stale SUPABASE_JWT_SECRET
— does not match launcher signer"; an unreachable launcher only warns (can't-verify
is not stale, so it never blocks boot).
4. Server helpers
import { requireAuth, getOptionalUser, getAuthenticatedUser, requireRole } from '@xzibit/app-kit';
// server component / page — redirects to the launcher if unauthenticated
const user = await requireAuth();
// API route — returns { user } | { error, status }
const res = await getAuthenticatedUser();5. BuildBadge SHA
import { getBuildInfo } from '@xzibit/app-kit';
const { sha, time } = getBuildInfo(); // reads NEXT_PUBLIC_BUILD_SHA / _TIMEEnv manifest
| Var | Required | Notes |
| --- | --- | --- |
| SUPABASE_JWT_SECRET (or JWT_SECRET) | ✅ | launcher JWT signing secret — validated for placeholder/length |
| NEXT_PUBLIC_SUPABASE_URL | ✅ | must be a valid URL |
| NEXT_PUBLIC_SUPABASE_ANON_KEY | ✅ | |
| SUPABASE_SERVICE_ROLE_KEY | only if requireServiceRole | server-only |
| NEXT_PUBLIC_XZIBIT_APPS_URL | optional | launcher base; defaults to https://xzibit-apps.vercel.app |
API
verifyLauncherToken · createAuthMiddleware · requireAuth · getOptionalUser
· getAuthenticatedUser · requireRole · createFeedbackProxy · createClarifyProxy
· assertServerEnv · verifyJwtSecretFingerprint · getJwtSecret · getLauncherUrl
· getBuildInfo · EnvError
Releasing
Versioned like @xzibit/ui: bump version, push a vX.Y.Z git tag → GitHub
Actions runs npm publish --provenance.
