heartwood-auth

Heartwood Auth Integration Skill

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "heartwood-auth" with this command: npx skills add autumnsgrove/groveengine/autumnsgrove-groveengine-heartwood-auth

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:

// 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, expired, or network error — silently continue
	}
}

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 '='

  1. 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"]' );

  1. 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

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

POST /api/auth/sign-in/magic-link

Magic link sign-in

POST /api/auth/sign-in/passkey

Passkey 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 :

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:

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:

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:

// In callback handler — map OAuth error param to structured error const authError = getAuthError(errorParam); // e.g. "access_denied" → AUTH_ERRORS.ACCESS_DENIED logAuthError(authError, { path: "/auth/callback", ip }); redirect(302, /login?${buildErrorParams(authError)});

Number ranges: 001-019 infrastructure, 020-039 session/token, 040+ reserved.

See AgentUsage/error_handling.md for the full Signpost reference.

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

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

cloudflare-deployment

No summary provided by upstream source.

Repository SourceNeeds Review
General

rich-terminal-output

No summary provided by upstream source.

Repository SourceNeeds Review
General

api-integration

No summary provided by upstream source.

Repository SourceNeeds Review
General

rust-testing

No summary provided by upstream source.

Repository SourceNeeds Review