Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 7 additions & 10 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# choreography Development Guidelines
# choreography Development Guidelines

Auto-generated from all feature plans. Last updated: 2026-04-17
Auto-generated from all feature plans. Last updated: 2026-04-25

## UI System (Constitution Principle VI — enforced)
All new components, interactive elements, and UX patterns MUST use **Skeleton v4** (`@skeletonlabs/skeleton-svelte`) + **Tailwind CSS v4** first. Custom HTML/CSS is only acceptable when Skeleton provides no equivalent. Do NOT use Flowbite, DaisyUI, or similar alongside Skeleton.
Expand All @@ -15,6 +15,8 @@ All new components, interactive elements, and UX patterns MUST use **Skeleton v4
- TypeScript 5.x / Node.js 20 LTS + SvelteKit 2, Svelte 5 Runes, Drizzle ORM, `@skeletonlabs/skeleton-svelte` v4, Tailwind CSS v4, `@lucide/svelte` (004-add-redemption-dashboard)
- TypeScript 5.x / Node.js 20 LTS + SvelteKit 2, Svelte 5 Runes, Drizzle ORM, `@skeletonlabs/skeleton-svelte` v4, Tailwind CSS v4, `@lucide/svelte`, `better-auth` (004-add-redemption-dashboard)
- SQLite via Drizzle ORM + libsql (`drizzle-orm/libsql`) using existing `DATABASE_URL` (004-add-redemption-dashboard)
- TypeScript 5.x on Node.js 20+ with SvelteKit 2 / Svelte 5 + `@sveltejs/kit`, `better-auth`, `drizzle-orm`, `@libsql/client`, `@skeletonlabs/skeleton(-svelte)`, `pino` (copilot/break-out-user-signup-family-creation)
- SQLite/libsql via Drizzle schema (`user`, `families`, `family_members`, related domain tables) (copilot/break-out-user-signup-family-creation)

- TypeScript 5.x / Node.js 20 LTS (001-choreography-mvp)

Expand All @@ -35,15 +37,10 @@ npm test; npm run lint
TypeScript 5.x / Node.js 20 LTS: Follow standard conventions

## Recent Changes
<<<<<<< HEAD
- 005-auth-system-refactor: Added TypeScript 5.x on Node.js 20+ with SvelteKit 2 / Svelte 5 + `@sveltejs/kit`, `better-auth`, `drizzle-orm`, `@libsql/client`, `@skeletonlabs/skeleton(-svelte)`, `pino`
- 004-add-redemption-dashboard: Added TypeScript 5.x / Node.js 20 LTS + SvelteKit 2, Svelte 5 Runes, Drizzle ORM, `@skeletonlabs/skeleton-svelte` v4, Tailwind CSS v4, `@lucide/svelte`, `better-auth`
- 004-add-redemption-dashboard: Added TypeScript 5.x / Node.js 20 LTS + SvelteKit 2, Svelte 5 Runes, Drizzle ORM, `@skeletonlabs/skeleton-svelte` v4, Tailwind CSS v4, `@lucide/svelte`
- 003-family-unification: Added TypeScript 5.x / Node.js 20 LTS + SvelteKit 2, Svelte 5 Runes, Drizzle ORM, @skeletonlabs/skeleton-svelte v4, Tailwind CSS v4
=======
- 004-add-redemption-dashboard: Added TypeScript 5.x / Node.js 20 LTS + SvelteKit 2, Svelte 5 Runes, Drizzle ORM, `@skeletonlabs/skeleton-svelte` v4, Tailwind CSS v4, `@lucide/svelte`
- 003-family-unification: Added TypeScript 5.x / Node.js 20 LTS + SvelteKit 2, Svelte 5 Runes, Drizzle ORM, @skeletonlabs/skeleton-svelte v4, Tailwind CSS v4
- 002-skeleton-ui-migration: Added TypeScript 5.x / Node.js 20 LTS + SvelteKit 2, Svelte 5 Runes, Tailwind CSS v4, @skeletonlabs/skeleton v4, @skeletonlabs/skeleton-svelte v4, @tailwindcss/forms, @lucide/svelte
>>>>>>> 0387394a03b5892b571414cd8d2e86e1b7297175
- 003-family-unification: Added TypeScript 5.x / Node.js 20 LTS + SvelteKit 2, Svelte 5 Runes, Drizzle ORM, `@skeletonlabs/skeleton-svelte` v4, Tailwind CSS v4
- 002-skeleton-ui-migration: Added TypeScript 5.x / Node.js 20 LTS + SvelteKit 2, Svelte 5 Runes, Tailwind CSS v4, Skeleton v4


<!-- MANUAL ADDITIONS START -->
Expand Down
129 changes: 55 additions & 74 deletions specs/005-auth-system-refactor/contracts/auth-contracts.md
Original file line number Diff line number Diff line change
@@ -1,108 +1,89 @@
# Contracts: Authentication Interfaces
# Contracts: Signup / Onboarding / Family-Creation Interfaces

## Scope

Defines runtime interface expectations for login UX, auth endpoints, session behavior, and failure handling for the better-auth migration.
Defines contract behavior for decoupled account signup, onboarding routing, and independent family creation after signup.

## 1. Environment Configuration Contract
## 1. Signup Contract (`/signup`)

### Inputs
### Request (form POST)

- `AUTH_MODE`: `local` | `oidc` | `both` (default `local`)
- `OIDC_ISSUER`: URL string
- `OIDC_ACCOUNT_CLAIM`: claim key string (default `email`)
- `OIDC_CLIENT_ID`: string
- `OIDC_CLIENT_SECRET`: string
- `OIDC_ISSUER_LABEL`: string (default `Single Sign-On`)
- `OIDC_ZERO_MATCH_POLICY`: `deny` | `provision` (default `deny`)
- `BETTER_AUTH_SECRET`: 32+ character secret
- Required inputs:
- `email`
- `password`
- `displayName`
- Optional inputs:
- `avatarEmoji`
- `pin` (if current kiosk/member flow still uses it for admin profile)
- Not accepted in decoupled path:
- `familyName` as required field during account creation

### Required combinations
### Success behavior

- `AUTH_MODE=local`: only `BETTER_AUTH_SECRET` required for auth engine operation.
- `AUTH_MODE=oidc`: all OIDC vars required.
- `AUTH_MODE=both`: local remains available if OIDC vars are invalid/missing.
- `OIDC_ZERO_MATCH_POLICY=deny`: no local match on first OIDC sign-in denies login with guidance.
- `OIDC_ZERO_MATCH_POLICY=provision`: no local match on first OIDC sign-in provisions auth user/account and allows login.
- Creates auth user + credential account.
- Creates authenticated session.
- Does **not** create `families` or `family_members` records.
- Redirects to `/onboarding` for first-run family setup.

## 2. Login UI Contract (`/login`)
### Error behavior

### Local mode (`AUTH_MODE=local`)
- Duplicate email -> `409` with user-facing duplicate-account message.
- Validation failures -> `400` with field-level/summary guidance.

- Must render local form fields (email + password).
- Must not render OIDC sign-in button.
## 2. Onboarding Routing Contract

### OIDC mode (`AUTH_MODE=oidc`)
### Guard rules

- Must render single OIDC sign-in button labeled with `OIDC_ISSUER_LABEL`.
- Must not render local password form.
- If OIDC config invalid, sign-in must be blocked with guidance message.
- If unauthenticated user accesses onboarding-required pages -> redirect to `/login`.
- If authenticated user has zero memberships -> redirect to `/onboarding` when attempting app routes requiring family context.
- If authenticated user has at least one membership and visits `/onboarding` root -> redirect to default app destination.

### Both mode (`AUTH_MODE=both`)
## 3. Family Creation Contract (Onboarding Action/API)

- Must render OIDC button first, then divider, then local form.
- If OIDC config invalid/missing, OIDC entry is hidden/disabled per UX decision and local form remains usable.
### Request

## 3. Auth API Route Contract
- Authenticated user submits:
- `familyName` (required)
- optional family setup metadata if later needed

### Route
### Transactional behavior

- `GET/POST /api/auth/[...all]`
- Create `families` row.
- Create `family_members` row linking current user as `admin`.
- Commit atomically (no partial family without membership).

### Behavior
### Response

- All auth lifecycle traffic (signin/signup/callback/signout/session) is handled by `better-auth` route handler.
- Existing app routes must rely on resolved auth session state from this single source.
- Success -> redirect to `/admin/family?new=1` (or equivalent first-admin landing page).
- Failure -> remain on onboarding with actionable error.

## 4. OIDC Account Linking Contract
## 4. Session Shape and Compatibility Contract

### Inputs
- Session identity remains sourced from `better-auth`.
- Family context fields must be treated as nullable/derivable until membership exists.
- Any route requiring `familyId` must resolve membership first and redirect if absent.

- OIDC callback payload including configured `OIDC_ACCOUNT_CLAIM`.
## 5. UI Contract

### Normalization

- Claim comparison for local linking must apply:
- trim surrounding whitespace
- case-insensitive comparison

### Outcomes

- Exactly one local match: link identity to existing user.
- Zero local matches: apply `OIDC_ZERO_MATCH_POLICY` (`deny` -> block with guidance, `provision` -> create auth user/account mapping).
- No claim present: deny login with missing-claim configuration error.
- Multiple local matches: deny login with admin-action-required error.
- In all deny cases above: no account creation and no linking side effects.

## 5. Session and Guard Contract

### Session source

- Authenticated session is provided by `better-auth` only.
- Legacy cookie-token table/session validation logic is removed.

### Route protection

- Authenticated route groups continue to redirect unauthenticated users to `/login`.
- Admin-only routes continue to enforce role checks.
- `/signup` presents account-creation-only copy and controls.
- `/onboarding` presents explicit next step to create/join family.
- All components remain Skeleton v4 + Tailwind based.

## 6. Observability Contract

### Required structured log events
### Required structured events

- `auth_oidc_config_invalid`
- `auth_oidc_claim_missing`
- `auth_oidc_link_ambiguous`
- `onboarding_required`
- `family_created_from_onboarding`
- `family_create_failed`

### Required fields (non-secret)
### Required metadata

- `authMode`
- `issuer` (if configured)
- `claimKey`
- `userId`
- `path`
- `requestId` (or equivalent correlation key if available)
- `reason`
- `requestId` (or equivalent)
- `reason` (for failures)

### Security rules

- Never log client secret, access tokens, refresh tokens, or raw ID tokens.
- Never log passwords, secrets, or token material.
135 changes: 62 additions & 73 deletions specs/005-auth-system-refactor/data-model.md
Original file line number Diff line number Diff line change
@@ -1,109 +1,98 @@
# Data Model: Authentication System Refactor
# Data Model: Decoupled Signup, Onboarding, and Family Creation

## Overview

This feature introduces a dedicated authentication layer using `better-auth` while preserving existing family-domain entities. The model adds canonical auth entities and defines how they relate to existing household members.
This model keeps identity/auth entities in `better-auth` and shifts family creation to an explicit onboarding step. A user can exist in the system without a family membership immediately after signup.

## Entities

### 1. Auth User
### 1. Auth User (`user`)

- Purpose: Canonical identity record managed by `better-auth` for an application user.
- Purpose: Canonical signed-in identity.
- Key fields:
- `id` (string, primary key)
- `email` (string, nullable for non-email identities depending on provider)
- `emailVerified` (boolean)
- `name` (string, optional display label)
- `image` (string, optional)
- `createdAt`, `updatedAt` (timestamps)
- `id` (PK)
- `email` (normalized, unique within credential provider behavior)
- `name`
- `avatarEmoji` (existing schema field for emoji/avatar representation)
- `isActive`
- `createdAt`, `updatedAt`
- Validation rules:
- Email, when present, should be normalized to lowercase and trimmed.
- IDs are immutable.
- Email must be trimmed + lowercase before persistence checks.
- `name` required for local signup flow.
- Relationships:
- One-to-many with Auth Account
- One-to-many with Auth Session
- One-to-one/optional mapping to existing Member for continuity in current app domain.
- 1:N Auth Account
- 1:N Auth Session
- 0:N Family Membership

### 2. Auth Session
### 2. Family (`families`)

- Purpose: Persistent authenticated session managed by `better-auth`.
- Purpose: Household container for shared chores/prizes/settings.
- Key fields:
- `id` (string, primary key)
- `userId` (foreign key -> Auth User)
- `expiresAt` (timestamp)
- `ipAddress`, `userAgent` (optional metadata)
- `createdAt`, `updatedAt` (timestamps)
- `id` (ULID)
- `name`
- `leaderboardResetDay`
- `createdAt`
- Validation rules:
- Session must reference an existing Auth User.
- Expired sessions are not accepted for authorization.
- Name required and non-empty.
- Relationships:
- Many-to-one with Auth User.
- 1:N Family Membership
- 1:N Chores/Prizes/Activity domain data

### 3. Auth Account
### 3. Family Membership (`family_members`)

- Purpose: External or credential-based login identity linked to an Auth User.
- Purpose: Join table linking users to families with role.
- Key fields:
- `id` (string, primary key)
- `userId` (foreign key -> Auth User)
- `providerId` (string; e.g., `oidc` or local-credential provider id)
- `accountId` (string; provider subject/claim identity)
- `accessToken`, `refreshToken`, `idToken` (optional, if stored by provider flow)
- `createdAt`, `updatedAt` (timestamps)
- `memberId` -> `user.id`
- `familyId` -> `families.id`
- `role` (`admin` | `member`)
- `joinedAt`
- Validation rules:
- (`providerId`, `accountId`) must be unique.
- Account must reference an existing Auth User.
- Unique pair (`memberId`, `familyId`).
- First family creator from onboarding is assigned `admin`.
- Relationships:
- Many-to-one with Auth User.
- N:1 Auth User
- N:1 Family

### 4. Verification/Credential Support Entities (if required by better-auth)
### 4. Onboarding Status (Derived)

- Purpose: Support password/email verification and recovery semantics depending on enabled plugins.
- Expected fields: token/value + expiry + identifier references.
- Purpose: Runtime state used for routing.
- Derived fields:
- `hasFamilyMembership` (boolean)
- `onboardingRequired` = authenticated AND `hasFamilyMembership=false`
- Validation rules:
- Tokens must expire and be single-use where applicable.
- Relationships:
- Reference Auth User or identifier fields based on plugin requirements.

### 5. Member (Existing Domain Entity)

- Purpose: Existing family-domain user profile used by chores, prizes, assignments, and role logic.
- Key fields currently include:
- `id`, `displayName`, `email`, `passwordHash`, `pin`, `isActive`, `createdAt`
- Feature-specific role in migration:
- Remains source of family membership and app-domain behavior.
- Local email identities are matched for first-time OIDC account linking by normalized claim value.
- Relationship updates:
- Add deterministic mapping approach between Auth User and Member (direct FK or stable lookup mapping) during implementation design.
- Determined from `family_members` existence; no separate persistence required.

## Relationship Summary

- Auth User 1 -> N Auth Session
- Auth User 1 -> N Auth Account
- Auth User 1 -> 0..1 Member mapping (implementation detail chosen in code/migration)
- Member N -> N Families (existing `family_members` join remains unchanged)
- Auth User 1 -> N Auth Session
- Auth User 1 -> 0..N Family Membership
- Family 1 -> N Family Membership

## State Transitions

### Sign-In State
### Account and Onboarding Lifecycle

1. Unauthenticated
2. Authenticated session created (local or OIDC)
3. Session refreshed/continued until expiry
4. Signed out or expired -> returns to unauthenticated
1. Anonymous user submits signup form.
2. Auth User (+ auth account/session) is created.
3. Membership check:
- No family membership -> redirect to onboarding.
- Has family membership -> proceed to app routes.
4. Onboarding family creation:
- Create family row.
- Create family_members row with role `admin`.
- Mark onboarding complete via derived membership state.
5. User lands on normal app home/admin destination.

### OIDC First-Login Linking State
### Existing User Sign-In

1. OIDC callback received with configured claim
2. Claim normalized (trim + case-fold)
3. Match local identifier:
- Exactly one match -> create/link Auth Account to existing user mapping
- Zero matches -> create new auth user per policy (if allowed by implementation scope)
- Multiple matches -> deny login and emit admin-action-required error
4. Session creation only after successful link decision
1. User signs in.
2. Membership check runs during route/session guard.
3. If membership missing, redirect to onboarding; otherwise continue normally.

## Data Integrity Rules

- No duplicate (`providerId`, `accountId`) auth accounts.
- No auth session without a valid auth user.
- Missing configured OIDC claim always hard-fails sign-in (no partial account creation).
- Ambiguous local matches always hard-fail sign-in (no auto-linking).
- No family should exist without at least one admin membership after successful onboarding completion transaction.
- No duplicate membership pairs (`memberId`, `familyId`).
- Signup must not create `families` rows directly.
- App routes that require `familyId` must gate or redirect before dereferencing family-dependent data.
Loading