Claude-skill-registry heartwood-auth
Integrate Heartwood (GroveAuth) authentication into Grove applications. Use when adding sign-in, protecting routes, or validating sessions in any Grove property.
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/heartwood-auth" ~/.claude/skills/majiayu000-claude-skill-registry-heartwood-auth-1f28d5 && rm -rf "$T"
manifest:
skills/data/heartwood-auth/SKILL.mdsource content
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 |
|---|---|
| Frontend (login UI) |
| 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:
// 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:
// 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 or expired } } return resolve(event); };
Option C: Legacy Token Flow (Backwards Compatible)
For existing integrations using the legacy OAuth flow:
// 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
// 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
// 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
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
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
# 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
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
# Set the client secret on your app wrangler secret put HEARTWOOD_CLIENT_SECRET # Paste: YKzJChC3RPjZvd1f/OD5zUGAvcouOTXG7maQP1ernCg=
Environment Variables
| Variable | Description |
|---|---|
| Your registered client ID |
| Your client secret (never commit!) |
API Endpoints Reference
Better Auth Endpoints (Recommended)
| Method | Endpoint | Purpose |
|---|---|---|
| POST | | OAuth sign-in |
| POST | | Magic link sign-in |
| POST | | Passkey sign-in |
| GET | | Get current session |
| POST | | Sign out |
Legacy Endpoints
| Method | Endpoint | Purpose |
|---|---|---|
| GET | | Login page |
| POST | | Exchange code for tokens |
| GET | | Validate access token |
| GET | | Get user info |
Best Practices
DO
- Use Better Auth client for new integrations
- Validate sessions on every protected request
- Use
cookies for token storagehttpOnly - 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
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)
Related Resources
- Heartwood Spec:
/Users/autumn/Documents/Projects/GroveAuth/GROVEAUTH_SPEC.md - Better Auth Docs: https://better-auth.com
- Client Setup Guide:
/Users/autumn/Documents/Projects/GroveAuth/docs/OAUTH_CLIENT_SETUP.md