Skip to main content

heartwood-auth

Integrate Heartwood (GroveAuth) authentication into Grove applications. Use when adding sign-in, protecting routes, or validating sessions in any Grove property.

インストールへ移動

ソース情報

リポジトリ
AutumnsGrove/Lattice
ソースの最終更新活動
2026年5月19日 23:12
検出された SKILL.md の言語
英語
スター
6
フォーク
0

インストール方法

デフォルトでは、最初にソースを確認する Prompt が選択されています。直接コマンドに切り替えるか、ローカルコピーをダウンロードすることもできます。

ソースファイルを確認

インストールを決める前に、SKILL.md と SkillsMP に表示されている付属ファイルをお読みください。

SKILL.md を表示中

SKILL.md
ソースの指示 · 読み取り専用プレビュー
name
heartwood-auth
description
Integrate Heartwood (GroveAuth) authentication into Grove applications. Use when adding sign-in, protecting routes, or validating sessions in any Grove property.
# Heartwood Auth Integration Skill ## When to Activate Activate this skill when: - Adding authentication to a Grove application - Protecting admin routes - Validating user sessions - Setting up OAuth sign-in - Integrating with Heartwood (GroveAuth) ## Overview **Heartwood** is Grove's centralized authentication service powered by Better Auth. | Domain | Purpose | | ----------------------- | ------------------- | | `heartwood.grove.place` | Frontend (login UI) | | `auth-api.grove.place` | Backend API | ### Key Features - **OAuth Providers**: Google - **Magic Links**: Click-to-login emails via Resend - **Passkeys**: WebAuthn passwordless authentication - **KV-Cached Sessions**: Sub-100ms validation - **Cross-Subdomain SSO**: Single session across all .grove.place ## Integration Approaches ### Option A: Better Auth Client (Recommended) For new integrations, use Better Auth's client library: ```typescript // src/lib/auth/client.ts import { createAuthClient } from "better-auth/client"; export const auth = createAuthClient({ baseURL: "https://auth-api.grove.place", }); // Sign in with Google await auth.signIn.social({ provider: "google" }); // Get current session const session = await auth.getSession(); // Sign out await auth.signOut(); ``` ### Option B: Cookie-Based SSO (\*.grove.place apps) For apps on `.grove.place` subdomains, sessions work automatically via cookies: ```typescript // src/hooks.server.ts import type { Handle } from "@sveltejs/kit"; export const handle: Handle = async ({ event, resolve }) => { // Check session via Heartwood API const sessionCookie = event.cookies.get("better-auth.session_token"); if (sessionCookie) { try { const response = await fetch("https://auth-api.grove.place/api/auth/session", { headers: { Cookie: `better-auth.session_token=${sessionCookie}`, }, }); if (response.ok) { const data = await response.json(); event.locals.user = data.user; event.locals.session = data.session; } } catch { // Session invalid, expired, or network error — silently continue } } return resolve(event); }; ``` ### Option C: Legacy Token Flow (Backwards Compatible) For existing integrations using the legacy OAuth flow: ```typescript // 1. Redirect to Heartwood login const params = new URLSearchParams({ client_id: "your-client-id", redirect_uri: "https://yourapp.grove.place/auth/callback", state: crypto.randomUUID(), code_challenge: await generateCodeChallenge(verifier), code_challenge_method: "S256", }); redirect(302, `https://auth-api.grove.place/login?${params}`); // 2. Exchange code for tokens (in callback route) const tokens = await fetch("https://auth-api.grove.place/token", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ grant_type: "authorization_code", code: code, redirect_uri: "https://yourapp.grove.place/auth/callback", client_id: "your-client-id", client_secret: env.HEARTWOOD_CLIENT_SECRET, code_verifier: verifier, }), }).then((r) => r.json()); // 3. Verify token on protected routes const user = await fetch("https://auth-api.grove.place/verify", { headers: { Authorization: `Bearer ${tokens.access_token}` }, }).then((r) => r.json()); ``` ## Protected Routes Pattern ### SvelteKit Layout Protection ```typescript // src/routes/admin/+layout.server.ts import { redirect } from "@sveltejs/kit"; import type { LayoutServerLoad } from "./$types"; export const load: LayoutServerLoad = async ({ locals }) => { if (!locals.user) { throw redirect(302, "/auth/login"); } return { user: locals.user, }; }; ``` ### API Route Protection ```typescript // src/routes/api/protected/+server.ts import { json, error } from "@sveltejs/kit"; import type { RequestHandler } from "./$types"; export const GET: RequestHandler = async ({ locals }) => { if (!locals.user) { throw error(401, "Unauthorized"); } return json({ message: "Protected data", user: locals.user }); }; ``` ## Session Validation ### Via Better Auth Session Endpoint ```typescript async function validateSession(sessionToken: string) { const response = await fetch("https://auth-api.grove.place/api/auth/session", { headers: { Cookie: `better-auth.session_token=${sessionToken}`, }, }); if (!response.ok) return null; const data = await response.json(); return data.session ? data : null; } ``` ### Via Legacy Verify Endpoint ```typescript async function validateToken(accessToken: string) { const response = await fetch("https://auth-api.grove.place/verify", { headers: { Authorization: `Bearer ${accessToken}`, }, }); const data = await response.json(); return data.active ? data : null; } ``` ## Client Registration To integrate a new app with Heartwood, you need to register it as a client. ### 1. Generate Client Credentials ```bash # Generate a secure client secret openssl rand -base64 32 # Example: YKzJChC3RPjZvd1f/OD5zUGAvcouOTXG7maQP1ernCg= # Hash it for storage (base64url encoding) echo -n "YOUR_SECRET" | openssl dgst -sha256 -binary | base64 | tr '+/' '-_' | tr -d '=' ``` ### 2. Register in Heartwood Database ```sql INSERT INTO clients (id, name, client_id, client_secret_hash, redirect_uris, allowed_origins) VALUES ( lower(hex(randomblob(16))), 'Your App Name', 'your-app-id', 'BASE64URL_HASHED_SECRET', '["https://yourapp.grove.place/auth/callback"]', '["https://yourapp.grove.place"]' ); ``` ### 3. Set Secrets on Your App ```bash # Set the client secret on your app wrangler secret put HEARTWOOD_CLIENT_SECRET # Paste: YKzJChC3RPjZvd1f/OD5zUGAvcouOTXG7maQP1ernCg= ``` ## Environment Variables | Variable | Description | | ------------------------- | ---------------------------------- | | `HEARTWOOD_CLIENT_ID` | Your registered client ID | | `HEARTWOOD_CLIENT_SECRET` | Your client secret (never commit!) | ## API Endpoints Reference ### Better Auth Endpoints (Recommended) | Method | Endpoint | Purpose | | ------ | ------------------------------ | ------------------- | | POST | `/api/auth/sign-in/social` | OAuth sign-in | | GET | `/api/auth/session` | Get current session | | POST | `/api/auth/sign-out` | Sign out | ### Legacy Endpoints | Method | Endpoint | Purpose | | ------ | ----------- | ------------------------ | | GET | `/login` | Login page | | POST | `/token` | Exchange code for tokens | | GET | `/verify` | Validate access token | | GET | `/userinfo` | Get user info | ## Best Practices ### DO - Use Better Auth client for new integrations - Validate sessions on every protected request - Use `httpOnly` cookies for token storage - Implement proper error handling for auth failures - Log out users gracefully when sessions expire ### DON'T - Store tokens in localStorage (XSS vulnerable) - Skip session validation on API routes - Hardcode client secrets - Ignore token expiration ## Type-Safe Error Handling **Use Rootwork type guards** in catch blocks instead of manual error type narrowing. Import from `@autumnsgrove/lattice/server`: ```typescript import { isRedirect, isHttpError } from "@autumnsgrove/lattice/server"; try { // ... auth flow } catch (err) { if (isRedirect(err)) throw err; // Re-throw SvelteKit redirects if (isHttpError(err)) { // Handle HTTP errors with proper status code console.error(`Auth failed: ${err.status} ${err.body}`); } // Fallback error handling } ``` **Reading session data from KV/cache:** Use `safeJsonParse()` for type-safe deserialization: ```typescript import { safeJsonParse } from "@autumnsgrove/lattice/server"; import { z } from "zod"; const sessionSchema = z.object({ userId: z.string(), email: z.string().email(), }); const rawSession = await kv.get("session:123"); const session = safeJsonParse(rawSession, sessionSchema); if (session) { event.locals.user = { id: session.userId, email: session.email }; } ``` ## Cross-Subdomain SSO All `.grove.place` apps share the same session cookie automatically: ``` better-auth.session_token (domain=.grove.place) ``` Once a user signs in on any Grove property, they're signed in everywhere. ## Troubleshooting ### "Session not found" errors - Check cookie domain is `.grove.place` - Verify SESSION_KV namespace is accessible - Check session hasn't expired ### OAuth callback errors - Verify redirect_uri matches registered client - Check client_id is correct - Ensure client_secret_hash uses base64url encoding ### Slow authentication - Ensure KV caching is enabled (SESSION_KV binding) - Check for cold start issues (Workers may sleep) ## Error Codes (HW-AUTH Catalog) Heartwood has its own Signpost error catalog with 16 codes: ```typescript import { AUTH_ERRORS, getAuthError, logAuthError, buildErrorParams, } from "@autumnsgrove/lattice/heartwood"; ``` **Key error codes:** | Code | Key | When | | ------------- | ----------------------- | -------------------------------------------- | | `HW-AUTH-001` | `ACCESS_DENIED` | User lacks permission | | `HW-AUTH-002` | `PROVIDER_ERROR` | OAuth provider failed | | `HW-AUTH-004` | `REDIRECT_URI_MISMATCH` | Callback URL doesn't match registered client | | `HW-AUTH-020` | `NO_SESSION` | No session cookie found | | `HW-AUTH-021` | `SESSION_EXPIRED` | Session timed out | | `HW-AUTH-022` | `INVALID_TOKEN` | Token verification failed | | `HW-AUTH-023` | `TOKEN_EXCHANGE_FAILED` | Code-for-token exchange failed | **Mapping OAuth errors to Signpost codes:** ```typescript // In callback handler — map OAuth error param to structured error const authError = getAuthError(errorParam); // e.g. "access_denied" → AUTH_ERRORS.ACCESS_DENIED
GitHubで見る
この SKILL.md は非常に大きいため、SkillsMP では最初のセクションだけを表示しています。 GitHubで見る