Skip to main content

grove-auth-integration

Integrate Heartwood authentication into a new or existing Grove property. Covers client registration, PKCE OAuth flow, SvelteKit route setup, session validation, and wrangler configuration. Use when adding auth to any Grove site.

Zur Installation springen

Quellinformationen

Repository
AutumnsGrove/Lattice
Letzte Quellaktivität
25. Februar 2026 um 14:16
Erkannte Sprache von SKILL.md
Englisch
Sterne
6
Forks
0

Installationsoptionen

Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.

Quelldateien prüfen

Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.

SKILL.md wird angezeigt

SKILL.md
Quellanweisungen · Schreibgeschützte Vorschau
name
grove-auth-integration
description
Integrate Heartwood authentication into a new or existing Grove property. Covers client registration, PKCE OAuth flow, SvelteKit route setup, session validation, and wrangler configuration. Use when adding auth to any Grove site.
# Grove Auth Integration Add Heartwood authentication to a Grove property — from client registration through production deployment. ## When to Activate - User says "add auth to this project" or "wire up Heartwood" - User is building a new Grove property that needs login - User needs to register a new OAuth client with Heartwood - User explicitly calls `/grove-auth-integration` - User mentions needing sign-in, protected routes, or session validation - User says "integrate GroveAuth" or "add login" --- ## Key URLs | Service | URL | Purpose | | --------------- | ------------------------------- | -------------------------------- | | **Login UI** | `https://heartwood.grove.place` | Where users authenticate | | **API** | `https://auth-api.grove.place` | Token exchange, verify, sessions | | **D1 Database** | `groveauth` (via wrangler) | Client registration | --- ## The Pipeline ``` Identify → Register Client → Configure Secrets → Write Code → Wire Wrangler → Test ``` **Error Handling in Auth Flows:** Auth errors MUST use the `AUTH_ERRORS` Signpost catalog — never bare redirect with ad-hoc error strings. ```typescript import { AUTH_ERRORS, getAuthError, logAuthError, buildErrorParams, } from "@autumnsgrove/lattice/heartwood"; // In callback — map OAuth error to structured code if (errorParam) { const authError = getAuthError(errorParam); logAuthError(authError, { path: "/auth/callback" }); redirect(302, `/login?${buildErrorParams(authError)}`); } ``` See `AgentUsage/error_handling.md` for the full Signpost reference. **Type-Safe Error Handling in Catch Blocks:** Always use Rootwork type guards in catch blocks instead of manual property checks. Import `isRedirect()` and `isHttpError()` from `@autumnsgrove/lattice/server`: ```typescript import { isRedirect, isHttpError } from "@autumnsgrove/lattice/server"; try { // ... auth flow code } catch (err) { if (isRedirect(err)) throw err; // Re-throw SvelteKit redirects if (isHttpError(err)) { // Handle HTTP errors with proper status and message } redirect(302, "/?error=auth_failed"); } ``` --- ### Step 1: Identify the Integration Ask the user (or determine from context): 1. **Project name** — The Cloudflare Pages/Workers project name 2. **Client ID** — A simple slug (e.g., `grove-plant`, `grove-domains`, `arbor-admin`) 3. **Site URL** — Production URL (e.g., `https://plant.grove.place`) 4. **Callback path** — Usually `/auth/callback` 5. **Project type** — SvelteKit Pages (most common), Workers, or other 6. **Session approach** — OAuth tokens (standard) or SessionDO (faster, same-account only) --- ### Step 2: Register the OAuth Client #### 2a. Generate client secret ```bash CLIENT_SECRET=$(openssl rand -base64 32) echo "Client Secret: $CLIENT_SECRET" ``` Save this value — you'll need it for both the client secrets AND the database hash. #### 2b. Generate base64url hash **CRITICAL**: Heartwood uses **base64url encoding** — dashes (`-`), underscores (`_`), NO padding (`=`). ```bash CLIENT_SECRET_HASH=$(echo -n "$CLIENT_SECRET" | openssl dgst -sha256 -binary | base64 | tr '+/' '-_' | tr -d '=') echo "Secret Hash: $CLIENT_SECRET_HASH" ``` | Format | Example | Correct? | | ------------- | ---------------------------------- | -------- | | **base64url** | `Sdgtaokie8-H7GKw-tn0S_6XNSh1rdv` | YES | | base64 | `Sdgtaokie8+H7GKw+tn0S/6XNSh1rdv=` | NO | | hex | `49d82d6a89227bcf87ec62b0...` | NO | #### 2c. Insert into Heartwood database ```bash wrangler d1 execute groveauth --remote --command=" INSERT INTO clients (id, name, client_id, client_secret_hash, redirect_uris, allowed_origins) VALUES ( '$(uuidgen | tr '[:upper:]' '[:lower:]')', 'DISPLAY_NAME', 'CLIENT_ID', 'BASE64URL_HASH', '[\"https://SITE_URL/auth/callback\", \"http://localhost:5173/auth/callback\"]', '[\"https://SITE_URL\", \"http://localhost:5173\"]' ) ON CONFLICT(client_id) DO UPDATE SET client_secret_hash = excluded.client_secret_hash, redirect_uris = excluded.redirect_uris, allowed_origins = excluded.allowed_origins; " ``` **Always include localhost** in redirect_uris and allowed_origins for development. --- ### Step 3: Configure Secrets on the Client #### For Pages projects (SvelteKit): ```bash echo "CLIENT_ID" | wrangler pages secret put GROVEAUTH_CLIENT_ID --project PROJECT_NAME echo "CLIENT_SECRET" | wrangler pages secret put GROVEAUTH_CLIENT_SECRET --project PROJECT_NAME echo "https://SITE_URL/auth/callback" | wrangler pages secret put GROVEAUTH_REDIRECT_URI --project PROJECT_NAME echo "https://auth-api.grove.place" | wrangler pages secret put GROVEAUTH_URL --project PROJECT_NAME ``` #### For Workers projects: ```bash cd worker-directory echo "CLIENT_ID" | wrangler secret put GROVEAUTH_CLIENT_ID echo "CLIENT_SECRET" | wrangler secret put GROVEAUTH_CLIENT_SECRET ``` --- ### Step 4: Write the Auth Code (SvelteKit) Create these files in the SvelteKit project: #### 4a. Login initiation route: `src/routes/auth/+server.ts` ```typescript /** * OAuth Initiation - Start Heartwood OAuth flow * Redirects to GroveAuth with PKCE parameters. */ import { redirect } from "@sveltejs/kit"; import type { RequestHandler } from "./$types"; function generateRandomString(length: number): string { const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"; const randomValues = crypto.getRandomValues(new Uint8Array(length)); return Array.from(randomValues, (v) => charset[v % charset.length]).join(""); } async function generatePKCE(): Promise<{ verifier: string; challenge: string; }> { const verifier = generateRandomString(64); const encoder = new TextEncoder(); const data = encoder.encode(verifier); const hash = await crypto.subtle.digest("SHA-256", data); const challenge = btoa(String.fromCharCode(...new Uint8Array(hash))) .replace(/\+/g, "-") .replace(/\//g, "_") .replace(/=/g, ""); return { verifier, challenge }; } export const GET: RequestHandler = async ({ url, cookies, platform }) => { const env = platform?.env as Record<string, string> | undefined; const authBaseUrl = env?.GROVEAUTH_URL || "https://auth-api.grove.place"; const clientId = env?.GROVEAUTH_CLIENT_ID || "YOUR_CLIENT_ID"; const appBaseUrl = env?.PUBLIC_APP_URL || "https://YOUR_SITE_URL"; const redirectUri = `${appBaseUrl}/auth/callback`; const { verifier, challenge } = await generatePKCE(); const state = generateRandomString(32); const isProduction = url.hostname !== "localhost" && url.hostname !== "127.0.0.1"; const cookieOptions = { path: "/", httpOnly: true, secure: isProduction, sameSite: "lax" as const, maxAge: 60 * 10, // 10 minutes }; cookies.set("auth_state", state, cookieOptions); cookies.set("auth_code_verifier", verifier, cookieOptions); const authUrl = new URL(`${authBaseUrl}/login`); authUrl.searchParams.set("client_id", clientId); authUrl.searchParams.set("redirect_uri", redirectUri); authUrl.searchParams.set("response_type", "code"); authUrl.searchParams.set("scope", "openid profile email"); authUrl.searchParams.set("state", state); authUrl.searchParams.set("code_challenge", challenge); authUrl.searchParams.set("code_challenge_method", "S256"); redirect(302, authUrl.toString()); }; ``` #### 4b. Callback handler: `src/routes/auth/callback/+server.ts` ```typescript /** * OAuth Callback - Handle Heartwood OAuth response * Exchanges authorization code for tokens and creates session. */ import { redirect } from "@sveltejs/kit"; import { isRedirect } from "@autumnsgrove/lattice/server"; import type { RequestHandler } from "./$types"; export const GET: RequestHandler = async ({ url, cookies, platform }) => { const code = url.searchParams.get("code"); const state = url.searchParams.get("state"); const errorParam = url.searchParams.get("error"); if (errorParam) { redirect(302, `/?error=${encodeURIComponent(errorParam)}`); } // Validate state (CSRF protection) const savedState = cookies.get("auth_state"); if (!state || state !== savedState) { redirect(302, "/?error=invalid_state"); } // Get PKCE verifier const codeVerifier = cookies.get("auth_code_verifier"); if (!codeVerifier || !code) { redirect(302, "/?error=missing_credentials"); } // Clear auth cookies immediately cookies.delete("auth_state", { path: "/" }); cookies.delete("auth_code_verifier", { path: "/" }); const env = platform?.env as Record<string, string> | undefined; const authBaseUrl = env?.GROVEAUTH_URL || "https://auth-api.grove.place"; const clientId = env?.GROVEAUTH_CLIENT_ID || "YOUR_CLIENT_ID"; const clientSecret = env?.GROVEAUTH_CLIENT_SECRET || ""; const appBaseUrl = env?.PUBLIC_APP_URL || "https://YOUR_SITE_URL"; const redirectUri = `${appBaseUrl}/auth/callback`; try { // Exchange code for tokens const tokenResponse = await fetch(`${authBaseUrl}/token`, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ grant_type: "authorization_code", code, redirect_uri: redirectUri, client_id: clientId, client_secret: clientSecret, code_verifier: codeVerifier, }), }); if (!tokenResponse.ok) { redirect(302, "/?error=token_exchange_failed"); } const tokens = (await tokenResponse.json()) as { access_token: string; refresh_token?: string; expires_in?: number; }; // Fetch user info const userinfoResponse = await fetch(`${authBaseUrl}/userinfo`, { headers: { Authorization: `Bearer ${tokens.access_token}` }, }); if (!userinfoResponse.ok) { redirect(302, "/?error=userinfo_failed"); } const userinfo = (await userinfoResponse.json()) as { sub?: string; id?: string; email: string; name?: string; email_verified?: boolean; }; const userId = userinfo.sub || userinfo.id; const email = userinfo.email; if (!userId || !email) { redirect(302, "/?error=incomplete_profile"); } // Set session cookies
Auf GitHub ansehen
Diese SKILL.md ist sehr gross, daher zeigt SkillsMP hier nur den ersten Abschnitt. Auf GitHub ansehen