commit b89690a9f2e9aaa61e9f81d33cf030defea7a99a Author: ginnoir Date: Wed May 6 00:05:50 2026 -0500 Initial scaffold: tooling, plan, task briefs - pnpm 10 workspace + TypeScript strict + ESLint flat + Prettier - CLAUDE.md as canonical brief - docs/tasks/ — 22 task briefs broken out by phase for sub-sessions - docs/decisions/ — ADR scaffold Implements task 01 (repo-init). diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..1014ba7 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..1714092 --- /dev/null +++ b/.env.example @@ -0,0 +1,25 @@ +# Public app URL (used for OIDC redirect URIs, share links, etc.) +NEXT_PUBLIC_APP_URL=https://fam.ginnoir.com + +# Postgres +DATABASE_URL=postgres://famapp:famapp@localhost:5432/famapp + +# Auth.js +AUTH_SECRET=replace-with-openssl-rand-base64-32 + +# OIDC (Authentik) +AUTH_OIDC_ISSUER=https://auth.ginnoir.com/application/o/famapp/ +AUTH_OIDC_CLIENT_ID=replace-me +AUTH_OIDC_CLIENT_SECRET=replace-me + +# Web Push (generate with: pnpm vapid:generate) +VAPID_PUBLIC_KEY= +VAPID_PRIVATE_KEY= +VAPID_SUBJECT=mailto:you@example.com + +# ntfy (optional fallback channel; leave blank to disable) +NTFY_URL= +NTFY_TOPIC= + +# Logging +LOG_LEVEL=info diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..ef264ac --- /dev/null +++ b/.gitattributes @@ -0,0 +1,9 @@ +* text=auto eol=lf +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.ico binary +*.webp binary +*.woff binary +*.woff2 binary diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4960f0b --- /dev/null +++ b/.gitignore @@ -0,0 +1,43 @@ +# Dependencies +node_modules/ +.pnpm-store/ + +# Build output +.next/ +out/ +dist/ +build/ +*.tsbuildinfo + +# Env +.env +.env.local +.env.*.local +.env.production + +# Logs +*.log +npm-debug.log* +pnpm-debug.log* + +# OS +.DS_Store +Thumbs.db +desktop.ini + +# Editors +.vscode/* +!.vscode/extensions.json +!.vscode/settings.json.example +.idea/ + +# Coverage / test artifacts +coverage/ +playwright-report/ +test-results/ + +# Drizzle generated artifacts (migrations themselves are committed) +drizzle/meta/ + +# Backups +deploy/backups/data/ diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..2bd5a0a --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +22 diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..5fe82c0 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,5 @@ +node_modules +.next +dist +drizzle +pnpm-lock.yaml diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..672d96f --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,9 @@ +{ + "semi": true, + "singleQuote": false, + "trailingComma": "all", + "printWidth": 100, + "tabWidth": 2, + "arrowParens": "always", + "endOfLine": "lf" +} diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..fc94068 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,165 @@ +# famapp + +Self-hosted family coordination web app for Matt and his wife. Replaces a commercial family app with a shared calendar, shopping/task lists, and notes/reminders. Designed to be **deliberately extensible** — Matt expects to bolt on niche features over time, so the architecture treats every feature as a module. + +This file is the canonical brief. Read it at the start of every session before making changes. Sub-task briefs in [`docs/tasks/`](docs/tasks/) reference this file; do not duplicate its contents there. + +--- + +## Goals + +- Replace current family app: shared calendar, shopping list, task list, notes/reminders. +- Self-hosted on Matt's home server, accessible from outside the house with auth. +- Wife-friendly: installable PWA, passkey/SSO login, no extra apps to install for notifications. +- "Share anything" — most entities can produce a temporary public link for outsiders. +- Architecture optimized for adding many small features later without core changes. + +--- + +## Stack (decided) + +| Layer | Choice | Notes | +| ------------------ | ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Frontend + backend | Next.js 15 App Router + TypeScript | Single Node process, server actions for mutations | +| UI | Tailwind + shadcn/ui | | +| DB | Postgres 16 | Dedicated container per app (Matt's rule) | +| ORM | Drizzle | Lightweight, SQL-friendly, good for evolving schema | +| Auth/SSO | **Authentik** at `auth.ginnoir.com` | Picked over Pocket-ID because it ships forward-auth (Proxy Provider) for the rest of Matt's stack (Sonarr/Radarr/Prowlarr/NZBGet/qBittorrent/Tautulli/Overseerr/FreshRSS/ntfy) which has no native OIDC. famapp itself uses OIDC. | +| Push | Web Push (VAPID) | Works in installed PWAs incl. iOS Safari 16.4+. ntfy stays optional. | +| Realtime | Postgres `LISTEN/NOTIFY` → SSE | Simpler than WebSockets, fine for two users | +| Reverse proxy | Caddy (existing) | `fam.ginnoir.com` → famapp:3000, `auth.ginnoir.com` → authentik | +| Package manager | pnpm | Fast, content-addressed store, strict deps | +| Runtime | Node 22 LTS | In `node:22-alpine` container | +| Host | Ubuntu Server 24.04 x64, Docker + Compose | Existing self-host stack | + +Domain: `fam.ginnoir.com`. SSO: `auth.ginnoir.com`. + +--- + +## Architecture: extensibility-first + +Every feature is a self-contained **module** under `src/modules//`. A module declares its tables, routes, dashboard contributions, quick-add actions, and supported share/reminder/search behaviors via a manifest. Core services (sharing, push, activity log, search, reminders) operate generically against any registered entity — adding a new module should not require touching core code. + +``` +src/ + modules/ + _core/ # auth, household, sharing, push, activity log, reminders + calendar/ + schema.ts # drizzle tables + server/ # server actions + queries + components/ + routes.ts # registers /calendar pages + API + dashboard.tsx # widget contributed to dashboard + manifest.ts # name, icon, nav, entity types, share/reminder handlers + lists/ + notes/ + app/ # Next.js routes — thin, mounts modules + lib/ +``` + +### Core primitives every module gets + +- **Entity registry.** Modules declare entity types; share-link, activity log, search, reminders all work against any registered entity. +- **Dashboard contribution API.** Each module exports a widget + priority. Dashboard composes whatever's installed. +- **Quick-add registry.** Modules register quick actions for the dashboard's `+` menu. +- **Share-link service.** `createShareLink(entityType, entityId, { expiresAt, capabilities })` → `fam.ginnoir.com/s/`. Generic. +- **Notification bus.** `notify(userId, { title, body, url })` fans out to web push + in-app + (optional) ntfy. +- **Permissions.** Household-scoped by default. Share-tokens grant scoped read/write per entity. +- **Feature flags.** Env/db-driven for staging experiments. + +**Rule for contributors (including future Sonnet sessions):** if a feature requires a change to `_core` to support a new entity type, that's a smell — extend the registry instead. + +--- + +## Data model (v1) + +- `users`, `households`, `household_members` +- `calendar_events` — title, start, end, all_day, location, notes, color, owner. `rrule` text column reserved (no recurrence in v1). `external_source`/`external_id` nullable for future Google/Apple sync. +- `lists` (type enum: `shopping` | `task` | future), `list_items` +- `notes` — title, body, pinned, remind_at +- `share_links` — entity_type, entity_id, token, capabilities jsonb, expires_at +- `activity_log` — entity_type, entity_id, actor, action, payload jsonb +- `push_subscriptions` +- `reminders` — entity_type, entity_id, fire_at, channel. Generic; used by notes/events/anything. + +All entity tables include `household_id`. All mutations are scoped to the caller's household. + +--- + +## Pages (v1) + +- `/` Dashboard — today + next 3 days events, top of shopping list, open tasks, pinned notes, recent activity, quick-add `+` +- `/calendar` — month/week/day, drag-create +- `/lists/shopping`, `/lists/tasks` — defaults; create more +- `/notes` +- `/s/` — public share viewer +- `/settings` — household, push toggles, share-link management + +--- + +## Deploy shape + +```yaml +# compose.yaml (sketch — final lives in /deploy) +services: + famapp: # node:22-alpine, Next.js + famapp-db: # postgres:16 + authentik-server: + authentik-worker: + authentik-db: # postgres:16 (separate per Matt's rule) + authentik-redis: +``` + +Caddy: `fam.ginnoir.com` → `famapp:3000`. `auth.ginnoir.com` → `authentik-server:9000`. + +Backups: nightly `pg_dump` to a host volume. Out of scope for phase 1 but reserve the cron slot. + +--- + +## Build phases + +1. **Scaffold** — repo init, Next.js + Drizzle + Tailwind + shadcn, module loader, compose stack, Caddy snippet, Authentik wired up, seeded household with both users. +2. **Core modules** — calendar, lists, notes (CRUD only). +3. **Dashboard + quick-add + activity log.** +4. **Sharing** — share-link service + `/s/` viewer. +5. **Push notifications + reminders.** +6. **PWA polish** — manifest, icons, offline shell, install prompt. +7. **Hardening** — backups (pg_dump cron), rate limiting on share links, logging. + +Tasks for each phase live in [`docs/tasks/`](docs/tasks/). Sub-sessions should pick up a task file, follow its scope, and stop at its acceptance criteria. + +--- + +## Conventions + +- **TypeScript strict.** No `any` without a written reason. +- **Server actions** for mutations; route handlers only for webhooks/SSE/share-link viewer. +- **Drizzle migrations** committed under `drizzle/`. Never edit a shipped migration — add a new one. +- **No comments** unless the _why_ is non-obvious. Names should carry intent. +- **Module isolation.** A module imports from `_core` and `lib/` only — never from a sibling module. +- **One module = one PR/commit boundary** when possible. +- **Tests:** Vitest for units (where it pays off), Playwright for one happy-path E2E per module. Don't write tests for trivial CRUD. +- **Secrets** via `.env` (gitignored) and `.env.example` (committed, no values). + +--- + +## Out of scope for v1 (do not build) + +- ICS export / Google / Apple / Outlook calendar sync (data model leaves room) +- Recurrence editor (column reserved, no UI) +- Native iOS/Android app (PWA only; architecture must not preclude it) +- Migration from any existing app (starting fresh) +- Forward-auth wiring for the rest of Matt's stack (Authentik is installed; wiring the \*arr apps is a separate later task) +- Multi-tenant / multi-household (schema has `household_id` but UI is single-household) + +--- + +## Where things live + +- **This brief:** `CLAUDE.md` +- **Task briefs for sub-sessions:** `docs/tasks/NN-name.md` +- **Architecture decisions worth preserving:** `docs/decisions/NNNN-title.md` (lightweight ADR — only when a non-obvious choice is made) +- **App code:** `src/` +- **Drizzle migrations:** `drizzle/` +- **Deploy:** `deploy/` (compose.yaml, Caddyfile snippet, Authentik bootstrap) +- **Memory (cross-session):** `C:\Users\MattC\.claude\projects\C--Users-MattC-Documents-famapp\memory\` diff --git a/README.md b/README.md new file mode 100644 index 0000000..5d264ec --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# famapp + +Self-hosted family coordination: shared calendar, lists, notes. + +- **Plan & architecture:** [`CLAUDE.md`](CLAUDE.md) +- **Task briefs (for sub-sessions):** [`docs/tasks/`](docs/tasks/) +- **Architecture decisions:** [`docs/decisions/`](docs/decisions/) diff --git a/docs/decisions/README.md b/docs/decisions/README.md new file mode 100644 index 0000000..70c8589 --- /dev/null +++ b/docs/decisions/README.md @@ -0,0 +1,25 @@ +# Architecture decisions + +Lightweight ADRs. Add one only when a non-obvious choice is made and the reasoning would be hard to reconstruct later. Keep each under ~300 words. + +## Format + +``` +# NNNN — Title + +Date: YYYY-MM-DD +Status: accepted | superseded by NNNN + +## Context +What problem we faced. + +## Decision +What we chose. + +## Consequences +What this costs us, what it buys us. +``` + +## Index + +- (none yet — add as decisions are made) diff --git a/docs/tasks/01-repo-init.md b/docs/tasks/01-repo-init.md new file mode 100644 index 0000000..002a45c --- /dev/null +++ b/docs/tasks/01-repo-init.md @@ -0,0 +1,42 @@ +# 01 — Repo init & tooling + +## Goal + +Initialize the famapp repo with pnpm, TypeScript strict, ESLint/Prettier, and a clean baseline commit. + +## Why + +Every later task assumes the repo, lockfile, and lint config exist. + +## Depends on + +None. + +## Scope + +- `git init` in `C:\Users\MattC\Documents\famapp` with `main` as default branch. +- `package.json` (`"packageManager": "pnpm@"`, `"engines": { "node": ">=22" }`). +- `pnpm-workspace.yaml` (single root for now; leave room for workspaces). +- `tsconfig.json` — strict, `moduleResolution: "bundler"`, paths alias `@/* → src/*`. +- ESLint (flat config) + Prettier with sensible defaults; `eslint-config-next` deferred to task 02. +- `.gitignore` (Node, Next.js, env files, OS junk). +- `.env.example` with placeholders for: `DATABASE_URL`, `AUTH_OIDC_ISSUER`, `AUTH_OIDC_CLIENT_ID`, `AUTH_OIDC_CLIENT_SECRET`, `AUTH_SECRET`, `VAPID_PUBLIC_KEY`, `VAPID_PRIVATE_KEY`, `NEXT_PUBLIC_APP_URL`. +- `README.md` — short pointer to `CLAUDE.md` and `docs/tasks/`. +- Editor: `.editorconfig`, `.nvmrc` (`22`). + +## Out of scope + +- Installing Next.js, Drizzle, Tailwind (those are in 02 and 03). +- Husky / lint-staged (defer; small two-person project doesn't need it yet). + +## Acceptance criteria + +- [ ] `pnpm install` succeeds on a clean clone. +- [ ] `pnpm lint` and `pnpm format:check` run (even if no files yet) and exit 0. +- [ ] `tsc --noEmit` exits 0. +- [ ] `git log` shows exactly one initial commit. + +## Notes + +- Use the latest pnpm 10.x. +- Flat ESLint config (`eslint.config.mjs`) — don't use the legacy `.eslintrc`. diff --git a/docs/tasks/02-nextjs-skeleton.md b/docs/tasks/02-nextjs-skeleton.md new file mode 100644 index 0000000..ec86441 --- /dev/null +++ b/docs/tasks/02-nextjs-skeleton.md @@ -0,0 +1,38 @@ +# 02 — Next.js app skeleton + +## Goal + +Stand up a minimal Next.js 15 (App Router) app with Tailwind and shadcn/ui, rendering a placeholder dashboard. + +## Why + +Provides the shell the modules will plug into. + +## Depends on + +- 01 (repo init) + +## Scope + +- Install Next.js 15 + React 19 into the existing repo (do **not** use `create-next-app` to overwrite — add deps manually or with `--use-pnpm` into the existing dir). +- App Router under `src/app/` with a root layout and a `/` page that renders "famapp" plus a stub "Dashboard coming soon". +- Tailwind v4 (or latest stable) configured against `src/**`. +- shadcn/ui initialized; install `button`, `card`, `input`, `dialog` to start. +- Path alias `@/*` working at runtime + in the editor. +- `next.config.ts` with `reactStrictMode: true` and `output: "standalone"` (required for the Docker image in task 05). + +## Out of scope + +- Auth, DB, modules. Just the shell. +- Custom theming beyond shadcn defaults. + +## Acceptance criteria + +- [ ] `pnpm dev` serves `http://localhost:3000` showing the placeholder page. +- [ ] `pnpm build` succeeds with `output: "standalone"` producing `.next/standalone/`. +- [ ] `pnpm lint` (now extending `next/core-web-vitals`) passes. +- [ ] No `any` introduced. + +## Notes + +- shadcn CLI writes to `src/components/ui/`. Keep it there; modules will import from it. diff --git a/docs/tasks/03-drizzle-postgres.md b/docs/tasks/03-drizzle-postgres.md new file mode 100644 index 0000000..4fc6716 --- /dev/null +++ b/docs/tasks/03-drizzle-postgres.md @@ -0,0 +1,38 @@ +# 03 — Drizzle + Postgres setup + +## Goal + +Wire Drizzle ORM to a Postgres 16 instance, with migrations and a working `db` client. + +## Depends on + +- 02 (Next.js skeleton) + +## Scope + +- Install `drizzle-orm`, `drizzle-kit`, `postgres` (the `postgres` driver, not `pg`). +- `drizzle.config.ts` pointing at `src/modules/**/schema.ts` and writing to `./drizzle/`. +- `src/lib/db.ts` exporting a singleton `db` client built from `DATABASE_URL`. +- Initial schema in `src/modules/_core/schema.ts`: + - `users` (id uuid pk, email unique, display_name, created_at) + - `households` (id uuid pk, name, created_at) + - `household_members` (household_id, user_id, role enum: `owner` | `member`, pk on both) +- pnpm scripts: `db:generate`, `db:migrate`, `db:studio`. +- `docker-compose.dev.yaml` at repo root with a `famapp-db` service for local dev (Postgres 16, exposed on 5432, named volume). + +## Out of scope + +- Module-specific tables (calendar/lists/notes ship in their own tasks). +- Seed data (in task 07). + +## Acceptance criteria + +- [ ] `docker compose -f docker-compose.dev.yaml up -d` starts Postgres locally. +- [ ] `pnpm db:generate && pnpm db:migrate` produces `drizzle/0000_*.sql` and applies it. +- [ ] A throwaway script can `import { db } from "@/lib/db"` and `select` from `users` (returning empty). +- [ ] `tsc --noEmit` passes. + +## Notes + +- Use the `postgres` driver — works in both Node and edge-light contexts and is what Drizzle docs prefer for serverless-shaped apps. +- Keep the dev compose file separate from the production `deploy/compose.yaml` (task 05). diff --git a/docs/tasks/04-module-loader.md b/docs/tasks/04-module-loader.md new file mode 100644 index 0000000..6d976cc --- /dev/null +++ b/docs/tasks/04-module-loader.md @@ -0,0 +1,72 @@ +# 04 — Module loader & registry + +## Goal + +Implement the module system that everything else hangs off: each module declares a manifest, the loader composes them at startup, and core services iterate the registry instead of hardcoding entity types. + +## Why + +This is the load-bearing extensibility piece. Get this wrong and every later module needs core changes. + +## Depends on + +- 03 (DB) + +## Scope + +### Module manifest type (`src/modules/_core/module.ts`) + +```ts +export type ModuleManifest = { + id: string; // "calendar", "lists", "notes" + name: string; // human-readable + nav?: { href: string; label: string; icon?: string }; + entities: EntityTypeRegistration[]; // see below + dashboardWidgets?: DashboardWidget[]; + quickAdds?: QuickAddAction[]; +}; + +export type EntityTypeRegistration = { + type: string; // "calendar.event", "lists.item", "notes.note" + label: { singular: string; plural: string }; + share?: ShareCapabilities; // null = not shareable + reminder?: ReminderCapabilities; // null = not remindable + search?: SearchAdapter; // null = not searchable + resolveUrl: (id: string) => string; // canonical app URL + loadForShare?: (id: string) => Promise; // payload for /s/ +}; +``` + +### Registry (`src/modules/_core/registry.ts`) + +- `registerModule(manifest)` and `getRegistry()` returning frozen views. +- `getEntityType(type)` lookup. +- Import-time side-effect free: modules are listed and registered explicitly in `src/modules/index.ts`. + +### Loader (`src/modules/index.ts`) + +- Imports each module's manifest and calls `registerModule`. Order is deterministic. +- Stub modules for now: `calendar`, `lists`, `notes` each export an empty-but-valid manifest so the loader has something to register. + +### Wiring + +- Root layout reads the registry to render nav. +- A throwaway `/debug/registry` page (dev-only) dumps the loaded registry as JSON. + +## Out of scope + +- Real implementations of share / reminder / search adapters (later phases). +- Dynamic plugin loading from disk. Modules are statically imported. + +## Acceptance criteria + +- [ ] `src/modules/index.ts` registers three stub modules. +- [ ] Nav renders entries from the registry, not from a hardcoded list. +- [ ] `/debug/registry` shows all three module manifests in dev. +- [ ] Adding a fourth stub module only requires creating its folder + adding one line to `src/modules/index.ts`. +- [ ] All types exported from `_core` so other modules can import without circulars. + +## Notes + +- Frozen objects (`Object.freeze`) on registry exposure are cheap insurance against accidental mutation. +- Resist the urge to make this a fancy DI container. A plain map is enough. diff --git a/docs/tasks/05-compose-caddy.md b/docs/tasks/05-compose-caddy.md new file mode 100644 index 0000000..651c685 --- /dev/null +++ b/docs/tasks/05-compose-caddy.md @@ -0,0 +1,63 @@ +# 05 — Compose stack & Caddy + +## Goal + +Produce the production deploy artifacts: Dockerfile, `deploy/compose.yaml`, Caddy snippet, `.env.production.example`. + +## Depends on + +- 02 (so `output: "standalone"` exists) +- 03 (so DB service is defined) + +## Scope + +### Dockerfile (multi-stage) + +- Stage 1: `node:22-alpine` + pnpm, install deps, build (`pnpm build`). +- Stage 2: `node:22-alpine` runtime, copy `.next/standalone`, `.next/static`, `public/`. Run as non-root. `CMD ["node", "server.js"]`. + +### `deploy/compose.yaml` + +Services: + +- `famapp` (built from Dockerfile) +- `famapp-db` (postgres:16, volume `famapp_db_data`) +- `authentik-server`, `authentik-worker`, `authentik-db` (postgres:16, volume `authentik_db_data`), `authentik-redis` — leave fully configured but task 06 will tune env + +All services on a `famapp_net` network. famapp depends on famapp-db. No host port exposure for the DBs/redis. + +### Caddy snippet (`deploy/Caddyfile.snippet`) + +``` +fam.ginnoir.com { + reverse_proxy famapp:3000 +} + +auth.ginnoir.com { + reverse_proxy authentik-server:9000 +} +``` + +Comment at the top: "Include from main Caddyfile or paste into the existing one." + +### `.env.production.example` + +All vars needed by the compose stack, with comments explaining each. + +## Out of scope + +- Actually running the stack against ginnoir.com (Matt does that). +- Backups, log shipping (later tasks). + +## Acceptance criteria + +- [ ] `docker build -t famapp .` succeeds. +- [ ] `docker compose -f deploy/compose.yaml config` validates without errors. +- [ ] famapp container starts against famapp-db when given a populated `.env`. +- [ ] Image size under ~300 MB. +- [ ] No secrets committed; only `.env.production.example`. + +## Notes + +- Use `pnpm fetch` + `pnpm install --offline` in the build stage for cache locality. +- `output: "standalone"` (set in task 02) means we copy `.next/standalone/server.js`, not run `next start`. diff --git a/docs/tasks/06-authentik-oidc.md b/docs/tasks/06-authentik-oidc.md new file mode 100644 index 0000000..e851062 --- /dev/null +++ b/docs/tasks/06-authentik-oidc.md @@ -0,0 +1,53 @@ +# 06 — Authentik install + OIDC integration + +## Goal + +Bring up Authentik in the compose stack and integrate famapp as an OIDC client. Logging in at `fam.ginnoir.com` redirects to `auth.ginnoir.com`, returns, and creates a session. + +## Depends on + +- 04 (registry, so we have a `users` table to map into) +- 05 (compose stack) + +## Scope + +### Authentik bootstrap + +- Tune env in `deploy/compose.yaml` for authentik-server/worker/db/redis per Authentik's official docs. +- `deploy/authentik/README.md` — manual one-time steps Matt runs after first boot: + 1. Visit `auth.ginnoir.com/if/flow/initial-setup/`, set the akadmin password. + 2. Create an OIDC Provider for famapp (RS256, redirect URI `https://fam.ginnoir.com/api/auth/callback/authentik`). + 3. Create an Application bound to it; copy client ID + secret into famapp's `.env`. + 4. Create the two user accounts (Matt + wife) with passkeys. + +### famapp OIDC client + +- Install `next-auth@beta` (v5) — Auth.js. +- `src/lib/auth.ts` configures Auth.js with a generic OIDC provider pointed at `AUTH_OIDC_ISSUER`. +- On first login, upsert into `users` table by email; create-or-attach to the single seeded household (task 07 owns the seeding; this task assumes it). +- Middleware (`src/middleware.ts`) protects everything except `/s/*` (share viewer), `/api/auth/*`, and static assets. +- A signed-in user is available via a `getCurrentUser()` server helper. + +### Login UX + +- `/login` page with a single "Sign in with SSO" button. +- After login, redirect to `/`. + +## Out of scope + +- Forward-auth / Outpost wiring for the rest of Matt's stack (separate later task). +- Account self-service in famapp (Authentik owns identity). +- Automating Authentik provider creation via Terraform/blueprints — manual is fine for now; document it well. + +## Acceptance criteria + +- [ ] Hitting `/` while signed out redirects to `/login`. +- [ ] Signing in with an Authentik account creates/updates a row in `users` and returns to `/`. +- [ ] `getCurrentUser()` works in server components and server actions. +- [ ] `/s/` is reachable signed-out (placeholder is fine — real viewer is task 31). +- [ ] Auth secret comes from `AUTH_SECRET` env, not committed. + +## Notes + +- Authentik's OIDC issuer URL takes the form `https://auth.ginnoir.com/application/o//`. Document this in the README so Matt copies it correctly. +- Use Auth.js's database session strategy with the Drizzle adapter so the `users` row is the source of truth. diff --git a/docs/tasks/07-household-seed.md b/docs/tasks/07-household-seed.md new file mode 100644 index 0000000..fd06670 --- /dev/null +++ b/docs/tasks/07-household-seed.md @@ -0,0 +1,29 @@ +# 07 — Household seeding & session + +## Goal + +On first boot, create the single household and attach Matt + wife to it. Every authenticated request resolves a `householdId` cheaply. + +## Depends on + +- 06 (auth) + +## Scope + +- Migration / seed script (`pnpm db:seed`) that inserts one household named "Home" if none exists. +- On first OIDC login, attach the user to the seeded household as `owner` (Matt) or `member` (everyone else). Idempotent. +- `getCurrentSession()` server helper returns `{ user, household, role }`. Throws if no household membership. +- All later modules' queries take `householdId` as the scope key. +- `/settings/household` page showing household name, member list, and a "rename household" form (owner only). + +## Out of scope + +- Multi-household UI (schema supports it; UI is single-household only). +- Invite flows (Authentik provisions accounts; we just attach them). + +## Acceptance criteria + +- [ ] `pnpm db:seed` is idempotent — running twice does not create duplicate households. +- [ ] First user to sign in becomes `owner`; subsequent users become `member`. +- [ ] `getCurrentSession()` returns a typed object with `household.id`. +- [ ] `/settings/household` renders for both roles; rename only works for owner. diff --git a/docs/tasks/10-calendar-module.md b/docs/tasks/10-calendar-module.md new file mode 100644 index 0000000..10f33bb --- /dev/null +++ b/docs/tasks/10-calendar-module.md @@ -0,0 +1,57 @@ +# 10 — Calendar module + +## Goal + +Implement the `calendar` module: shared events with month/week/day views and CRUD. + +## Depends on + +- 04 (module loader), 07 (household) + +## Scope + +### Schema (`src/modules/calendar/schema.ts`) + +- `calendar_events`: + - `id` uuid pk + - `household_id` fk + - `title` text + - `start_at` timestamptz, `end_at` timestamptz + - `all_day` boolean + - `location` text nullable + - `notes` text nullable + - `color` text nullable + - `owner_id` fk users + - `rrule` text nullable (reserved; no UI) + - `external_source` text nullable, `external_id` text nullable (reserved) + - `created_at`, `updated_at` + +### Server (`src/modules/calendar/server/`) + +- `listEvents({ from, to })` — household-scoped, range query. +- `createEvent`, `updateEvent`, `deleteEvent` — server actions, validate with Zod. + +### UI (`src/modules/calendar/components/`) + +- `/calendar` page with month / week / day toggle. Recommended lib: **FullCalendar** (`@fullcalendar/react` + day/week/month plugins) — handles the heavy lifting; we own only the data layer. +- Click a day cell → create-event dialog. Click an event → edit dialog. Drag-resize updates `end_at`. + +### Manifest + +- Registers entity type `calendar.event` with `share` enabled (read-only by default), `reminder` enabled, `search` enabled (title + notes + location). +- Registers nav entry `/calendar`. +- Dashboard widget: next 3 days (built in task 20, but expose the data fetcher here). +- Quick-add: "New event" → opens create dialog. + +## Out of scope + +- Recurrence UI (rrule column reserved, ignored on read). +- External calendar sync. +- Free/busy aggregation across users. + +## Acceptance criteria + +- [ ] CRUD works end-to-end with optimistic updates. +- [ ] All queries scoped to `household_id`; cross-household leakage is impossible (verify with a second seeded household in a test). +- [ ] Manifest registers all four capabilities (entity, nav, widget data, quick-add). +- [ ] One Playwright happy-path test: create → see on calendar → edit → delete. diff --git a/docs/tasks/11-lists-module.md b/docs/tasks/11-lists-module.md new file mode 100644 index 0000000..ee933f6 --- /dev/null +++ b/docs/tasks/11-lists-module.md @@ -0,0 +1,48 @@ +# 11 — Lists module (shopping + task) + +## Goal + +Implement the `lists` module supporting two list types out of the box (`shopping`, `task`), with the schema designed so additional types can be added later without migration. + +## Depends on + +- 04, 07 + +## Scope + +### Schema + +- `lists`: `id`, `household_id`, `type` (text — not an enum, to allow extension), `name`, `archived`, `created_at`. +- `list_items`: `id`, `list_id`, `text`, `done` boolean, `qty` text nullable, `notes` text nullable, `due_at` timestamptz nullable, `assignee_id` fk users nullable, `position` int (for ordering), `created_at`, `updated_at`. + +Seed: one default list of each type per household on first access (idempotent). + +### Server + +- `listLists({ type? })`, `getList(id)`, `createList`, `renameList`, `archiveList`. +- `addItem`, `toggleItem`, `updateItem`, `deleteItem`, `reorderItems`. + +### UI + +- `/lists` index showing all lists grouped by type. +- `/lists/[id]` — fast keyboard-driven entry: focus stays in input, Enter adds, checkbox toggles, swipe-left (mobile) deletes. +- Realtime: SSE subscription on the list id; updates from the other user appear without refresh. (Implementation detail: Postgres `LISTEN/NOTIFY` channel `list:`; thin SSE route handler bridges it. Document the pattern in `docs/decisions/`.) + +### Manifest + +- Entity type `lists.list` (shareable read+write via share token), `lists.item` (not directly shareable; inherits via list). +- Quick-adds: "Add to shopping", "Add to tasks" (each adds a single item to the default list of that type). +- Dashboard widget: top 5 unchecked shopping items + open tasks assigned to me. + +## Out of scope + +- Reordering across lists. +- Per-item images. +- Recurring tasks (use reminders module later if needed). + +## Acceptance criteria + +- [ ] Both default lists are auto-seeded. +- [ ] Adding an item on one device appears on the other within ~1s via SSE. +- [ ] `type` is `text` not `enum` — adding a third type works without a migration. +- [ ] Playwright: add item → check it off → archive list. diff --git a/docs/tasks/12-notes-module.md b/docs/tasks/12-notes-module.md new file mode 100644 index 0000000..29daa25 --- /dev/null +++ b/docs/tasks/12-notes-module.md @@ -0,0 +1,30 @@ +# 12 — Notes module + +## Goal + +Lightweight shared notes with optional reminders. + +## Depends on + +- 04, 07 + +## Scope + +- Schema: `notes` (`id`, `household_id`, `author_id`, `title`, `body` markdown, `pinned` bool, `remind_at` timestamptz nullable, `created_at`, `updated_at`). +- `/notes` index: pinned first, then by updated_at desc. +- `/notes/[id]` editor — minimal markdown (use `@uiw/react-md-editor` or similar; live preview optional). +- `remind_at` writes a row to `reminders` (table comes from `_core`; reminder firing comes from task 41). +- Manifest: shareable, remindable, searchable (title + body). +- Dashboard widget: pinned notes. + +## Out of scope + +- Rich text / WYSIWYG. +- Attachments (separate later module). +- Collaborative editing. + +## Acceptance criteria + +- [ ] Create / edit / pin / delete works. +- [ ] Setting `remind_at` creates a `reminders` row scoped to entity `notes.note`. +- [ ] Markdown renders safely (no raw HTML injection). diff --git a/docs/tasks/20-dashboard.md b/docs/tasks/20-dashboard.md new file mode 100644 index 0000000..7186c00 --- /dev/null +++ b/docs/tasks/20-dashboard.md @@ -0,0 +1,33 @@ +# 20 — Dashboard composition + +## Goal + +The `/` landing page composes widgets contributed by installed modules. No hardcoded widget list. + +## Depends on + +- 10, 11, 12 + +## Scope + +- `DashboardWidget` type already in `_core` (from task 04). Each widget exports `{ id, priority, render }`. +- `/` reads `getRegistry().dashboardWidgets`, sorts by priority desc, renders them in a responsive grid (1 col mobile, 2-3 cols desktop). +- Widgets implemented: + - **calendar.upcoming** — next 3 days of events + - **lists.shopping** — top 5 unchecked items in default shopping list + - **lists.tasks** — my open tasks (assigned to me, due soon first) + - **notes.pinned** — pinned notes + - **core.activity** — recent activity feed (depends on task 22; until then render a stub) +- Skeleton loaders while widgets load (each widget is a server component with `loading.tsx`-equivalent Suspense boundary). +- "+" floating action button opens the quick-add menu (task 21). + +## Out of scope + +- User-configurable widget order or hide/show (defer; pin a sane default order). +- Drag-rearrange. + +## Acceptance criteria + +- [ ] Removing a module from `src/modules/index.ts` cleanly removes its widgets — no errors, no blank slot. +- [ ] Widgets load in parallel (verify in network tab — no waterfall). +- [ ] Mobile layout (375px) is usable without horizontal scroll. diff --git a/docs/tasks/21-quick-add.md b/docs/tasks/21-quick-add.md new file mode 100644 index 0000000..701cd3d --- /dev/null +++ b/docs/tasks/21-quick-add.md @@ -0,0 +1,26 @@ +# 21 — Quick-add registry + +## Goal + +A single global `+` button on the dashboard (and a `cmd+k` palette anywhere) that lists actions contributed by modules. + +## Depends on + +- 20 + +## Scope + +- `QuickAddAction` type: `{ id, label, icon?, shortcut?, run: () => void | Promise }`. +- Modules register via manifest. Registry exposes a sorted list. +- UI: floating `+` on dashboard opens a sheet of actions; `cmd/ctrl+k` opens a command-palette-style modal (use `cmdk` package). +- Default actions: New event, Add to shopping, Add to tasks, New note. + +## Out of scope + +- Fuzzy search across data (search adapter is a later concern). +- Action history / recents. + +## Acceptance criteria + +- [ ] Adding a new module that registers a quick-add appears in both the FAB sheet and the cmd-k palette without touching core. +- [ ] Keyboard navigation works in the palette (arrows + enter + escape). diff --git a/docs/tasks/22-activity-log.md b/docs/tasks/22-activity-log.md new file mode 100644 index 0000000..d6b60f7 --- /dev/null +++ b/docs/tasks/22-activity-log.md @@ -0,0 +1,26 @@ +# 22 — Activity log + +## Goal + +Generic activity feed every module writes to and the dashboard reads from. + +## Depends on + +- 10, 11, 12 + +## Scope + +- Schema: `activity_log` (`id`, `household_id`, `entity_type`, `entity_id`, `actor_id`, `action`, `payload` jsonb, `created_at`). Index on `(household_id, created_at desc)`. +- `_core` exports `logActivity({ entityType, entityId, action, payload })`. Reads `householdId` and `actorId` from current session. +- Calendar/lists/notes modules call it on create/update/delete. +- Dashboard widget `core.activity` shows last 20 entries with human-readable rendering. Each entity type's manifest provides a `renderActivity(entry)` function. + +## Out of scope + +- Per-user activity filters. +- Retention / pruning (no growth concerns at two-user scale). + +## Acceptance criteria + +- [ ] Every CRUD across the three core modules writes a row. +- [ ] Widget renders entries via the registry — no `if (entityType === ...)` branches in the widget itself. diff --git a/docs/tasks/30-share-links.md b/docs/tasks/30-share-links.md new file mode 100644 index 0000000..7cec60e --- /dev/null +++ b/docs/tasks/30-share-links.md @@ -0,0 +1,32 @@ +# 30 — Share-link service + +## Goal + +A generic service that issues temporary, scoped, revocable share tokens for any registered entity. + +## Depends on + +- 04 (registry), 07 (household) + +## Scope + +- Schema: `share_links` (`id`, `household_id`, `entity_type`, `entity_id`, `token` unique, `capabilities` jsonb (e.g. `{ read: true, write: false }`), `created_by`, `expires_at` nullable, `revoked_at` nullable, `created_at`). +- `_core/share.ts`: + - `createShareLink(entityType, entityId, opts)` → `{ url, token, expiresAt }`. + - `resolveShareToken(token)` → `{ entityType, entityId, capabilities } | null` (rejects expired/revoked). + - `revokeShareLink(id)`. +- Verifies the entity type is registered and shareable per its manifest's `share` capability declaration. +- Token format: 32-byte URL-safe base64. Stored hashed (sha-256) — only the URL contains the raw token. + +## Out of scope + +- The viewer page (task 31). +- Rate limiting (task 61). +- Per-recipient access logs. + +## Acceptance criteria + +- [ ] Creating a link for an entity type with `share: undefined` throws. +- [ ] Tokens are stored hashed; raw token only returned at creation. +- [ ] Expired/revoked tokens return null. +- [ ] `/settings` shows active share links per entity with revoke buttons. diff --git a/docs/tasks/31-share-viewer.md b/docs/tasks/31-share-viewer.md new file mode 100644 index 0000000..56b147f --- /dev/null +++ b/docs/tasks/31-share-viewer.md @@ -0,0 +1,27 @@ +# 31 — Public share viewer (`/s/`) + +## Goal + +Unauthenticated route that resolves a share token and renders the entity using its manifest's `loadForShare` adapter. + +## Depends on + +- 30 + +## Scope + +- `/s/[token]/page.tsx` — server component. Calls `resolveShareToken`. If valid, calls the entity's `loadForShare(id)` and renders a per-type viewer component (each module exports a ``). +- If the token grants write capability, render an editable view (initially only relevant for lists — recipient can check items off). +- Friendly error page for invalid/expired/revoked tokens. +- Add `noindex` headers on the route. + +## Out of scope + +- Auth on the share page (deliberately public; security is the unguessable token). +- Comments / reactions from anonymous viewers. + +## Acceptance criteria + +- [ ] Calendar event share renders title/time/location/notes — read-only. +- [ ] Shopping list share with `write: true` lets a recipient toggle items; toggles round-trip back into the household (with `actor_id = null` in activity log, action `share.toggle`). +- [ ] Middleware does not gate `/s/*`. diff --git a/docs/tasks/40-web-push.md b/docs/tasks/40-web-push.md new file mode 100644 index 0000000..9d6786d --- /dev/null +++ b/docs/tasks/40-web-push.md @@ -0,0 +1,28 @@ +# 40 — Web Push (VAPID) + +## Goal + +Subscribe browsers to web push and deliver notifications to them. + +## Depends on + +- 06 (auth), 50 (PWA shell — needed for iOS to allow push). If 50 isn't done, this task can land but iOS won't fire until then. + +## Scope + +- `pnpm add web-push` for server, use built-in `PushManager` on client. +- VAPID keys generated once (script: `pnpm vapid:generate`), stored in `.env` as `VAPID_PUBLIC_KEY` / `VAPID_PRIVATE_KEY`. Document in README. +- Schema: `push_subscriptions` (`id`, `user_id`, `endpoint` unique, `p256dh`, `auth`, `user_agent`, `created_at`). +- Service worker registers on app load; on user opt-in (button in `/settings`), prompts permission and stores subscription via server action. +- Server helper `sendPush(userId, { title, body, url })` iterates subscriptions, removes any returning 404/410. + +## Out of scope + +- Notification preferences per category (defer). +- ntfy fallback (task 42). + +## Acceptance criteria + +- [ ] Opt-in flow works in Chrome desktop and mobile, plus iOS Safari (when PWA installed). +- [ ] Stale subscriptions are pruned on send failure. +- [ ] Sending a test notification from `/settings` reaches all of the current user's subscribed devices. diff --git a/docs/tasks/41-reminders.md b/docs/tasks/41-reminders.md new file mode 100644 index 0000000..b323c52 --- /dev/null +++ b/docs/tasks/41-reminders.md @@ -0,0 +1,28 @@ +# 41 — Reminders engine + +## Goal + +A generic, entity-agnostic reminder system that fires at `fire_at` and routes through the notification bus. + +## Depends on + +- 40 (push), 42 (bus) + +## Scope + +- Schema: `reminders` (`id`, `household_id`, `entity_type`, `entity_id`, `fire_at` timestamptz, `channel` text default `'auto'`, `fired_at` timestamptz nullable, `created_by`). +- Worker: a long-running tick (every 30s) that selects due un-fired reminders, calls `notify(...)`, marks `fired_at`. Runs in the same Node process via `setInterval` guarded by a Postgres advisory lock to allow future horizontal scaling. +- `_core/reminders.ts` exposes `scheduleReminder(...)`, `cancelReminder(id)`, `listReminders(entityType, entityId)`. +- Notes module's `remind_at` field calls `scheduleReminder` on save and `cancelReminder` when cleared. +- Calendar module: optional "remind me" on event create (default 30 min before). + +## Out of scope + +- Recurring reminders (use rrule later). +- Snooze. + +## Acceptance criteria + +- [ ] A note with `remind_at = now + 1min` fires within 90s and triggers a push notification. +- [ ] Editing the note's `remind_at` updates the existing reminder, not creates duplicates. +- [ ] Worker survives DB blips (caught + logged, not crashed). diff --git a/docs/tasks/42-notification-bus.md b/docs/tasks/42-notification-bus.md new file mode 100644 index 0000000..92b448c --- /dev/null +++ b/docs/tasks/42-notification-bus.md @@ -0,0 +1,28 @@ +# 42 — Notification bus + ntfy adapter + +## Goal + +Single `notify()` entrypoint that fans out to web push, in-app inbox, and (optionally) ntfy. + +## Depends on + +- 40 + +## Scope + +- `_core/notify.ts` — `notify(userId, { title, body, url, channels? })`. +- Channels: `push`, `inapp`, `ntfy`. Default: `['push', 'inapp']`. +- In-app inbox: `notifications` table (`id`, `user_id`, `title`, `body`, `url`, `read_at`, `created_at`); bell icon in header with unread count and dropdown. +- ntfy adapter: if `NTFY_URL` and `NTFY_TOPIC` env are set, also POSTs there. Off by default; Matt opts in. +- Per-user toggles in `/settings` for each channel. + +## Out of scope + +- Email notifications. +- Per-event-type routing rules (Matt can add later as a module). + +## Acceptance criteria + +- [ ] `notify()` with default channels delivers push + in-app and ignores ntfy. +- [ ] Setting `NTFY_URL`+`NTFY_TOPIC` and toggling on in settings adds ntfy delivery. +- [ ] Unread count is accurate and clears on read. diff --git a/docs/tasks/50-pwa-shell.md b/docs/tasks/50-pwa-shell.md new file mode 100644 index 0000000..8596c38 --- /dev/null +++ b/docs/tasks/50-pwa-shell.md @@ -0,0 +1,28 @@ +# 50 — Manifest, icons, install prompt + +## Goal + +Make famapp installable as a PWA on iOS and Android. + +## Depends on + +- 02 + +## Scope + +- `public/manifest.webmanifest` with name, short_name, theme_color, background_color, display: `standalone`, start_url `/`. +- App icons (192, 384, 512, maskable). Source SVG committed; export script in `scripts/`. +- `apple-touch-icon` link in root layout. +- Service worker registration (next-pwa or hand-rolled — prefer hand-rolled to avoid coupling to a library that lags Next.js releases). +- Install prompt: a small banner on first visit on supported browsers, dismissible. + +## Out of scope + +- Offline caching (task 51). +- Background sync. + +## Acceptance criteria + +- [ ] Lighthouse "Installable" check passes. +- [ ] Installing on iOS 16.4+ home screen produces a standalone window with no Safari chrome. +- [ ] App icon shows correctly on Android home screen with maskable variant. diff --git a/docs/tasks/51-offline.md b/docs/tasks/51-offline.md new file mode 100644 index 0000000..b02ad17 --- /dev/null +++ b/docs/tasks/51-offline.md @@ -0,0 +1,28 @@ +# 51 — Offline shell + service worker caching + +## Goal + +The app shell loads without network; cached data is shown with a "stale" indicator while fresh data loads. + +## Depends on + +- 50 + +## Scope + +- Service worker uses Workbox-style strategies (hand-rolled, see task 50 rationale): + - **App shell** (HTML, JS, CSS): stale-while-revalidate. + - **API GETs**: network-first with 2s timeout, fall back to cache. + - **Mutations**: never cache; if offline, show a clear "you're offline" toast. +- Offline page for navigations that have no cache. +- Version SW on every build; show a "new version available, refresh" toast when a new SW takes control. + +## Out of scope + +- Background sync of queued mutations (defer; usually more trouble than worth at this scale). + +## Acceptance criteria + +- [ ] In Chrome DevTools "Offline", reloading the app shows the cached dashboard with a stale-indicator. +- [ ] Attempting to mutate while offline shows a clear error, doesn't silently drop. +- [ ] Deploying a new build prompts active sessions to refresh. diff --git a/docs/tasks/60-backups.md b/docs/tasks/60-backups.md new file mode 100644 index 0000000..5d3dc41 --- /dev/null +++ b/docs/tasks/60-backups.md @@ -0,0 +1,27 @@ +# 60 — Postgres backups (pg_dump cron) + +## Goal + +Nightly compressed pg_dump of `famapp-db` (and `authentik-db`) to a host volume, with a retention policy. + +## Depends on + +- 05 + +## Scope + +- `deploy/backups/` directory committed. +- Add a `famapp-backup` service to compose: small Alpine container with `postgresql-client` + `cron` + a script that runs `pg_dump -Fc` against both DBs into `/backups//.dump`. +- Retention: keep last 14 daily, last 8 weekly, last 6 monthly. Pure shell, no fancy tooling. +- Document restore procedure in `deploy/backups/README.md`. + +## Out of scope + +- Off-site replication (Matt's call; he can rsync `/backups` elsewhere). +- Encryption at rest (handled at the disk layer). + +## Acceptance criteria + +- [ ] After a day, dump files exist for both DBs. +- [ ] `restore.sh ` script exists and works against a fresh DB. +- [ ] Retention script keeps the right counts. diff --git a/docs/tasks/61-rate-limit.md b/docs/tasks/61-rate-limit.md new file mode 100644 index 0000000..f7908e5 --- /dev/null +++ b/docs/tasks/61-rate-limit.md @@ -0,0 +1,24 @@ +# 61 — Rate limiting on share links + +## Goal + +Prevent token brute-forcing on `/s/`. + +## Depends on + +- 31 + +## Scope + +- IP + token-prefix bucket. Reject after N failed lookups per minute per IP. In-memory LRU is fine for one Node process; document switch to Redis if multi-process arrives. +- Failed `resolveShareToken` calls increment the bucket; successful resolves do not. +- Generic `lib/rate-limit.ts` so other endpoints can use the same primitive. + +## Out of scope + +- Captcha. + +## Acceptance criteria + +- [ ] 50 bad tokens from one IP in a minute returns 429 thereafter. +- [ ] Legitimate access from another IP unaffected. diff --git a/docs/tasks/62-logging.md b/docs/tasks/62-logging.md new file mode 100644 index 0000000..29790ca --- /dev/null +++ b/docs/tasks/62-logging.md @@ -0,0 +1,27 @@ +# 62 — Structured logging + +## Goal + +JSON logs for the app, easy to grep and ship later. + +## Depends on + +- 02 + +## Scope + +- `pino` for app logs; pretty-print only in dev. +- Log levels via `LOG_LEVEL` env (default `info`). +- Request log middleware (method, path, status, duration, userId if present). +- Errors: capture stack + cause, never log secrets. + +## Out of scope + +- Log shipping (Loki/etc). +- Sentry/error tracking. + +## Acceptance criteria + +- [ ] Production logs are single-line JSON. +- [ ] No secret values appear anywhere in the log stream during a typical session. +- [ ] Request logs include duration and userId when authenticated. diff --git a/docs/tasks/README.md b/docs/tasks/README.md new file mode 100644 index 0000000..8b7d4a8 --- /dev/null +++ b/docs/tasks/README.md @@ -0,0 +1,68 @@ +# Task briefs + +Each file here is a self-contained brief for a sub-session (typically Sonnet) to execute one chunk of work. Briefs assume the executor has read [`/CLAUDE.md`](../../CLAUDE.md) — do not restate the stack or architecture there. + +## How to use a brief + +1. Read `/CLAUDE.md` first. +2. Read the task file end-to-end. +3. Stop when **all** acceptance criteria pass. Do not bolt on extra scope. +4. If a brief turns out to be wrong, update the brief in the same commit as the code. + +## Brief format + +Every task file has these sections: + +- **Goal** — one sentence. +- **Why** — what this unlocks; how it fits the larger plan. +- **Depends on** — task numbers that must be done first. +- **Scope** — bullet list of what to build. +- **Out of scope** — explicit non-goals to prevent drift. +- **Acceptance criteria** — checkable list. Done = all checked. +- **Notes** — gotchas, recommended libs, sketches. + +## Phase index + +### Phase 1 — Scaffold + +- [01 — Repo init & tooling](01-repo-init.md) +- [02 — Next.js app skeleton](02-nextjs-skeleton.md) +- [03 — Drizzle + Postgres setup](03-drizzle-postgres.md) +- [04 — Module loader & registry](04-module-loader.md) +- [05 — Compose stack & Caddy](05-compose-caddy.md) +- [06 — Authentik install + OIDC integration](06-authentik-oidc.md) +- [07 — Household seeding & session](07-household-seed.md) + +### Phase 2 — Core modules + +- [10 — Calendar module](10-calendar-module.md) +- [11 — Lists module (shopping + task)](11-lists-module.md) +- [12 — Notes module](12-notes-module.md) + +### Phase 3 — Dashboard & UX + +- [20 — Dashboard composition](20-dashboard.md) +- [21 — Quick-add registry](21-quick-add.md) +- [22 — Activity log](22-activity-log.md) + +### Phase 4 — Sharing + +- [30 — Share-link service](30-share-links.md) +- [31 — Public share viewer (`/s/`)](31-share-viewer.md) + +### Phase 5 — Notifications & reminders + +- [40 — Web Push (VAPID)](40-web-push.md) +- [41 — Reminders engine](41-reminders.md) +- [42 — Notification bus + ntfy adapter](42-notification-bus.md) + +### Phase 6 — PWA polish + +- [50 — Manifest, icons, install prompt](50-pwa-shell.md) +- [51 — Offline shell + service worker caching](51-offline.md) + +### Phase 7 — Hardening + +- [60 — Postgres backups (pg_dump cron)](60-backups.md) +- [61 — Rate limiting on share links](61-rate-limit.md) +- [62 — Structured logging](62-logging.md) diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..6c6b2c2 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,33 @@ +import js from "@eslint/js"; +import tseslint from "typescript-eslint"; +import globals from "globals"; + +export default tseslint.config( + { + ignores: [ + "node_modules/**", + ".next/**", + "dist/**", + "drizzle/**", + "*.config.js", + "*.config.mjs", + "*.config.cjs", + ], + }, + js.configs.recommended, + ...tseslint.configs.recommended, + { + languageOptions: { + globals: { + ...globals.node, + ...globals.browser, + }, + }, + rules: { + "@typescript-eslint/no-unused-vars": [ + "error", + { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }, + ], + }, + }, +); diff --git a/package.json b/package.json new file mode 100644 index 0000000..2d03717 --- /dev/null +++ b/package.json @@ -0,0 +1,26 @@ +{ + "name": "famapp", + "version": "0.0.0", + "private": true, + "type": "module", + "packageManager": "pnpm@10.33.3", + "engines": { + "node": ">=22" + }, + "scripts": { + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "format": "prettier --write .", + "format:check": "prettier --check .", + "typecheck": "tsc --noEmit" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@types/node": "^22.9.0", + "eslint": "^9.15.0", + "globals": "^15.12.0", + "prettier": "^3.3.3", + "typescript": "^5.6.3", + "typescript-eslint": "^8.15.0" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..5cfe107 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,990 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@eslint/js': + specifier: ^10.0.1 + version: 10.0.1(eslint@9.39.4) + '@types/node': + specifier: ^22.9.0 + version: 22.19.17 + eslint: + specifier: ^9.15.0 + version: 9.39.4 + globals: + specifier: ^15.12.0 + version: 15.15.0 + prettier: + specifier: ^3.3.3 + version: 3.8.3 + typescript: + specifier: ^5.6.3 + version: 5.9.3 + typescript-eslint: + specifier: ^8.15.0 + version: 8.59.2(eslint@9.39.4)(typescript@5.9.3) + +packages: + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.2': + resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.5': + resolution: {integrity: sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@10.0.1': + resolution: {integrity: sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + peerDependencies: + eslint: ^10.0.0 + peerDependenciesMeta: + eslint: + optional: true + + '@eslint/js@9.39.4': + resolution: {integrity: sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/node@22.19.17': + resolution: {integrity: sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==} + + '@typescript-eslint/eslint-plugin@8.59.2': + resolution: {integrity: sha512-j/bwmkBvHUtPNxzuWe5z6BEk3q54YRyGlBXkSsmfoih7zNrBvl5A9A98anlp/7JbyZcWIJ8KXo/3Tq/DjFLtuQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.59.2 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.59.2': + resolution: {integrity: sha512-plR3pp6D+SSUn1HM7xvSkx12/DhoHInI2YF35KAcVFNZvlC0gtrWqx7Qq1oH2Ssgi0vlFRCTbP+DZc7B9+TtsQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.59.2': + resolution: {integrity: sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.59.2': + resolution: {integrity: sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.59.2': + resolution: {integrity: sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.59.2': + resolution: {integrity: sha512-nhqaj1nmTdVVl/BP5omXNRGO38jn5iosis2vbdmupF2txCf8ylWT8lx+JlvMYYVqzGVKtjojUFoQ3JRWK+mfzQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.59.2': + resolution: {integrity: sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.59.2': + resolution: {integrity: sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.59.2': + resolution: {integrity: sha512-Juw3EinkXqjaffxz6roowvV7GZT/kET5vSKKZT6upl5TXdWkLkYmNPXwDDL2Vkt2DPn0nODIS4egC/0AGxKo/Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.59.2': + resolution: {integrity: sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + brace-expansion@1.1.14: + resolution: {integrity: sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==} + + brace-expansion@5.0.5: + resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} + engines: {node: 18 || 20 || >=22} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@9.39.4: + resolution: {integrity: sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@15.15.0: + resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} + engines: {node: '>=18'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier@3.8.3: + resolution: {integrity: sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==} + engines: {node: '>=14'} + hasBin: true + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} + + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + typescript-eslint@8.59.2: + resolution: {integrity: sha512-pJw051uomb3ZeCzGTpRb8RbEqB5Y4WWet8gl/GcTlU35BSx0PVdZ86/bqkQCyKKuraVQEK7r6kBHQXF+fBhkoQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + +snapshots: + + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4)': + dependencies: + eslint: 9.39.4 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.21.2': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.5': + dependencies: + ajv: 6.15.0 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@10.0.1(eslint@9.39.4)': + optionalDependencies: + eslint: 9.39.4 + + '@eslint/js@9.39.4': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 + + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@types/estree@1.0.8': {} + + '@types/json-schema@7.0.15': {} + + '@types/node@22.19.17': + dependencies: + undici-types: 6.21.0 + + '@typescript-eslint/eslint-plugin@8.59.2(@typescript-eslint/parser@8.59.2(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.59.2(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.59.2 + '@typescript-eslint/type-utils': 8.59.2(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/utils': 8.59.2(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.59.2 + eslint: 9.39.4 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.59.2(eslint@9.39.4)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.59.2 + '@typescript-eslint/types': 8.59.2 + '@typescript-eslint/typescript-estree': 8.59.2(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.59.2 + debug: 4.4.3 + eslint: 9.39.4 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.59.2(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.59.2(typescript@5.9.3) + '@typescript-eslint/types': 8.59.2 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.59.2': + dependencies: + '@typescript-eslint/types': 8.59.2 + '@typescript-eslint/visitor-keys': 8.59.2 + + '@typescript-eslint/tsconfig-utils@8.59.2(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.59.2(eslint@9.39.4)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.59.2 + '@typescript-eslint/typescript-estree': 8.59.2(typescript@5.9.3) + '@typescript-eslint/utils': 8.59.2(eslint@9.39.4)(typescript@5.9.3) + debug: 4.4.3 + eslint: 9.39.4 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.59.2': {} + + '@typescript-eslint/typescript-estree@8.59.2(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.59.2(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.59.2(typescript@5.9.3) + '@typescript-eslint/types': 8.59.2 + '@typescript-eslint/visitor-keys': 8.59.2 + debug: 4.4.3 + minimatch: 10.2.5 + semver: 7.7.4 + tinyglobby: 0.2.16 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.59.2(eslint@9.39.4)(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4) + '@typescript-eslint/scope-manager': 8.59.2 + '@typescript-eslint/types': 8.59.2 + '@typescript-eslint/typescript-estree': 8.59.2(typescript@5.9.3) + eslint: 9.39.4 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.59.2': + dependencies: + '@typescript-eslint/types': 8.59.2 + eslint-visitor-keys: 5.0.1 + + acorn-jsx@5.3.2(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + + acorn@8.16.0: {} + + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + argparse@2.0.1: {} + + balanced-match@1.0.2: {} + + balanced-match@4.0.4: {} + + brace-expansion@1.1.14: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@5.0.5: + dependencies: + balanced-match: 4.0.4 + + callsites@3.1.0: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + concat-map@0.0.1: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-is@0.1.4: {} + + escape-string-regexp@4.0.0: {} + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@9.39.4: + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.2 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.5 + '@eslint/js': 9.39.4 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + ajv: 6.15.0 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 4.2.1 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + fast-deep-equal@3.1.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.4.2 + keyv: 4.5.4 + + flatted@3.4.2: {} + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@14.0.0: {} + + globals@15.15.0: {} + + has-flag@4.0.0: {} + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + isexe@2.0.0: {} + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.5 + + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.14 + + ms@2.1.3: {} + + natural-compare@1.4.0: {} + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + picomatch@4.0.4: {} + + prelude-ls@1.2.1: {} + + prettier@3.8.3: {} + + punycode@2.3.1: {} + + resolve-from@4.0.0: {} + + semver@7.7.4: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + strip-json-comments@3.1.1: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + tinyglobby@0.2.16: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + ts-api-utils@2.5.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + typescript-eslint@8.59.2(eslint@9.39.4)(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.59.2(@typescript-eslint/parser@8.59.2(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/parser': 8.59.2(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.59.2(typescript@5.9.3) + '@typescript-eslint/utils': 8.59.2(eslint@9.39.4)(typescript@5.9.3) + eslint: 9.39.4 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + typescript@5.9.3: {} + + undici-types@6.21.0: {} + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + yocto-queue@0.1.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..ccbac80 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +packages: + - "." diff --git a/src/placeholder.ts b/src/placeholder.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/src/placeholder.ts @@ -0,0 +1 @@ +export {}; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..301a281 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2023", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "isolatedModules": true, + "verbatimModuleSyntax": true, + "strict": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "allowJs": false, + "noEmit": true, + "jsx": "preserve", + "incremental": true, + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + }, + "include": ["src/**/*", "*.ts", "*.mts", "*.cts"], + "exclude": ["node_modules", ".next", "dist", "drizzle"] +}