style(17): apply prettier formatting to satisfy CI format:check
CI / changes (pull_request) Successful in 6s
CI / api (pull_request) Successful in 2m9s
CI / fast-checks (pull_request) Successful in 2m30s
CI / security (pull_request) Successful in 59s
CI / harness (pull_request) Failing after 12m0s
CI / gate (pull_request) Failing after 2s
CI / changes (pull_request) Successful in 6s
CI / api (pull_request) Successful in 2m9s
CI / fast-checks (pull_request) Successful in 2m30s
CI / security (pull_request) Successful in 59s
CI / harness (pull_request) Failing after 12m0s
CI / gate (pull_request) Failing after 2s
Reformats 4 phase-17 files (SettingsSheet.tsx, tokens.css, vite.config.ts, pwa-assets.config.ts) plus 11 pre-existing non-conformant docs/READMEs that the repo-wide format:check also flags. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
6c2c6f24a9
commit
24bc8d2c32
@@ -260,7 +260,7 @@ Do not make direct repo edits outside a GSD workflow unless the user explicitly
|
|||||||
> Generated by GSD from session_analysis. Run `/gsd-profile-user` to update.
|
> Generated by GSD from session_analysis. Run `/gsd-profile-user` to update.
|
||||||
|
|
||||||
| Dimension | Rating | Confidence |
|
| Dimension | Rating | Confidence |
|
||||||
|-----------|--------|------------|
|
| -------------- | --------------------- | ---------- |
|
||||||
| Communication | conversational | MEDIUM |
|
| Communication | conversational | MEDIUM |
|
||||||
| Decisions | fast-intuitive | MEDIUM |
|
| Decisions | fast-intuitive | MEDIUM |
|
||||||
| Explanations | concise | MEDIUM |
|
| Explanations | concise | MEDIUM |
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ See [`docs/deployment.md`](docs/deployment.md) for Unraid/Docker Compose deploym
|
|||||||
Every PR to `main` must pass four jobs before it can merge:
|
Every PR to `main` must pass four jobs before it can merge:
|
||||||
|
|
||||||
| Job | What it runs |
|
| Job | What it runs |
|
||||||
| -------------------- | -------------------------------------------------------------------------- |
|
| ------------------ | ---------------------------------------------------------------------------------- |
|
||||||
| `CI / fast-checks` | `pnpm lint`, `pnpm format:check`, `pnpm md:lint`, `pnpm typecheck`, PWA unit tests |
|
| `CI / fast-checks` | `pnpm lint`, `pnpm format:check`, `pnpm md:lint`, `pnpm typecheck`, PWA unit tests |
|
||||||
| `CI / api` | DB migrations + API test suite against a real MariaDB 11 service container |
|
| `CI / api` | DB migrations + API test suite against a real MariaDB 11 service container |
|
||||||
| `CI / harness` | Playwright end-to-end harness (WebKit iPhone + Chromium Pixel) |
|
| `CI / harness` | Playwright end-to-end harness (WebKit iPhone + Chromium Pixel) |
|
||||||
|
|||||||
+2
-2
@@ -126,7 +126,7 @@ Migration files are written to `src/db/migrations/` and checked into source cont
|
|||||||
## Environment variables
|
## Environment variables
|
||||||
|
|
||||||
| Variable | Required | Description |
|
| Variable | Required | Description |
|
||||||
| ----------------------------- | ------------------- | ---------------------------------------------------------------------------------- |
|
| ----------------------------- | -------------------- | ---------------------------------------------------------------------------------- |
|
||||||
| `DB_HOST` | Yes | MariaDB host |
|
| `DB_HOST` | Yes | MariaDB host |
|
||||||
| `DB_USER` | Yes | MariaDB user |
|
| `DB_USER` | Yes | MariaDB user |
|
||||||
| `DB_PASSWORD` | Yes | MariaDB password |
|
| `DB_PASSWORD` | Yes | MariaDB password |
|
||||||
@@ -156,7 +156,7 @@ See [../../docs/CONFIGURATION.md](../../docs/CONFIGURATION.md) for the full refe
|
|||||||
The API supports two non-exclusive auth modes, determined at startup:
|
The API supports two non-exclusive auth modes, determined at startup:
|
||||||
|
|
||||||
| Mode | When active | How it works |
|
| Mode | When active | How it works |
|
||||||
| ---- | ----------- | ------------ |
|
| -------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| **Local** | Always (default) | `POST /api/auth/local/login` with username + password; issues an HS256 JWT `local-session` cookie. Requires `LOCAL_SESSION_SECRET`. |
|
| **Local** | Always (default) | `POST /api/auth/local/login` with username + password; issues an HS256 JWT `local-session` cookie. Requires `LOCAL_SESSION_SECRET`. |
|
||||||
| **OIDC** | When `OIDC_ISSUER` + `OIDC_CLIENT_ID` are set (env or app_config) | `@hono/oidc-auth` authorization-code + PKCE against Authelia. Local users can upgrade to OIDC via `POST /api/me/link-oidc`. |
|
| **OIDC** | When `OIDC_ISSUER` + `OIDC_CLIENT_ID` are set (env or app_config) | `@hono/oidc-auth` authorization-code + PKCE against Authelia. Local users can upgrade to OIDC via `POST /api/me/link-oidc`. |
|
||||||
| **Dev bypass** | `DEV_AUTH_BYPASS=true` in non-production | Skips both guards and injects a synthetic dev user. Blocked in `NODE_ENV=production` by boot guard. |
|
| **Dev bypass** | `DEV_AUTH_BYPASS=true` in non-production | Skips both guards and injects a synthetic dev user. Blocked in `NODE_ENV=production` by boot guard. |
|
||||||
|
|||||||
+1
-1
@@ -33,7 +33,7 @@ The API backend must also be running for most features. See [GETTING-STARTED.md]
|
|||||||
## Scripts
|
## Scripts
|
||||||
|
|
||||||
| Command | What it does |
|
| Command | What it does |
|
||||||
| ------------------------------------------------ | ------------------------------------------------------------------------- |
|
| ----------------------------------------------- | ------------------------------------------------------------- |
|
||||||
| `pnpm --filter @familysync/pwa dev` | Start Vite dev server (HMR) |
|
| `pnpm --filter @familysync/pwa dev` | Start Vite dev server (HMR) |
|
||||||
| `pnpm --filter @familysync/pwa build` | Type-check then build production bundle (`tsc && vite build`) |
|
| `pnpm --filter @familysync/pwa build` | Type-check then build production bundle (`tsc && vite build`) |
|
||||||
| `pnpm --filter @familysync/pwa preview` | Serve the production build locally |
|
| `pnpm --filter @familysync/pwa preview` | Serve the production build locally |
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { defineConfig, minimal2023Preset } from '@vite-pwa/assets-generator/config'
|
import { defineConfig, minimal2023Preset } from '@vite-pwa/assets-generator/config';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
preset: {
|
preset: {
|
||||||
@@ -7,4 +7,4 @@ export default defineConfig({
|
|||||||
images: ['public/logo.svg'],
|
images: ['public/logo.svg'],
|
||||||
// Do NOT set overrideManifestIcons: true — the manifest is maintained by hand
|
// Do NOT set overrideManifestIcons: true — the manifest is maintained by hand
|
||||||
// in vite.config.ts (plan 17-04); auto-override would stomp the explicit entries.
|
// in vite.config.ts (plan 17-04); auto-override would stomp the explicit entries.
|
||||||
})
|
});
|
||||||
|
|||||||
@@ -29,7 +29,13 @@ import { usePushSubscription } from '../hooks/usePushSubscription.js';
|
|||||||
import { useIsPhone } from '../hooks/useIsPhone.js';
|
import { useIsPhone } from '../hooks/useIsPhone.js';
|
||||||
import { useFocusTrap } from '../hooks/useFocusTrap.js';
|
import { useFocusTrap } from '../hooks/useFocusTrap.js';
|
||||||
import { InstructionSheet } from './InstructionSheet.js';
|
import { InstructionSheet } from './InstructionSheet.js';
|
||||||
import { fetchMe, fetchAuthMode, fetchChangePassword, fetchLinkOidc, fetchLocalLogout } from '../api/client.js';
|
import {
|
||||||
|
fetchMe,
|
||||||
|
fetchAuthMode,
|
||||||
|
fetchChangePassword,
|
||||||
|
fetchLinkOidc,
|
||||||
|
fetchLocalLogout,
|
||||||
|
} from '../api/client.js';
|
||||||
|
|
||||||
// CR-04: fetch VAPID key (from sessionStorage cache if available) for the
|
// CR-04: fetch VAPID key (from sessionStorage cache if available) for the
|
||||||
// tap-gated subscribe() path. Same logic as PushPermissionPrompt.
|
// tap-gated subscribe() path. Same logic as PushPermissionPrompt.
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
:root,
|
:root,
|
||||||
[data-theme="light"] {
|
[data-theme='light'] {
|
||||||
/* ─────────────────────────────────────────────────────────────────────────
|
/* ─────────────────────────────────────────────────────────────────────────
|
||||||
* BASE SURFACE / BORDER / TEXT PALETTE
|
* BASE SURFACE / BORDER / TEXT PALETTE
|
||||||
* ───────────────────────────────────────────────────────────────────────── */
|
* ───────────────────────────────────────────────────────────────────────── */
|
||||||
|
|||||||
@@ -42,7 +42,12 @@ export default defineConfig({
|
|||||||
icons: [
|
icons: [
|
||||||
{ src: '/icon-192.png', sizes: '192x192', type: 'image/png' },
|
{ src: '/icon-192.png', sizes: '192x192', type: 'image/png' },
|
||||||
{ src: '/icon-512.png', sizes: '512x512', type: 'image/png' },
|
{ src: '/icon-512.png', sizes: '512x512', type: 'image/png' },
|
||||||
{ src: '/icon-maskable-512.png', sizes: '512x512', type: 'image/png', purpose: 'maskable' },
|
{
|
||||||
|
src: '/icon-maskable-512.png',
|
||||||
|
sizes: '512x512',
|
||||||
|
type: 'image/png',
|
||||||
|
purpose: 'maskable',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|||||||
+12
-11
@@ -26,7 +26,7 @@ No API key or `Authorization` header is used. Credentials are never included in
|
|||||||
## Endpoints Overview
|
## Endpoints Overview
|
||||||
|
|
||||||
| Method | Path | Auth | Description |
|
| Method | Path | Auth | Description |
|
||||||
| ------ | ------------------------------------- | --------- | -------------------------------------------------- |
|
| ------ | --------------------------------- | ------- | --------------------------------------------------- |
|
||||||
| GET | `/health` | None | DB liveness check |
|
| GET | `/health` | None | DB liveness check |
|
||||||
| GET | `/callback` | None | OIDC authorization-code exchange |
|
| GET | `/callback` | None | OIDC authorization-code exchange |
|
||||||
| GET | `/api/setup/status` | None | Setup wizard completion status |
|
| GET | `/api/setup/status` | None | Setup wizard completion status |
|
||||||
@@ -130,7 +130,7 @@ Stores non-secret OIDC and VAPID configuration into `app_config`. Returns `423`
|
|||||||
```
|
```
|
||||||
|
|
||||||
| Field | Type | Required | Constraints |
|
| Field | Type | Required | Constraints |
|
||||||
| ---------------- | ------ | -------- | ------------------------------ |
|
| ---------------- | ------ | -------- | ----------------------------- |
|
||||||
| `oidcIssuer` | string | Yes | HTTPS URL |
|
| `oidcIssuer` | string | Yes | HTTPS URL |
|
||||||
| `oidcClientId` | string | Yes | 1–256 characters |
|
| `oidcClientId` | string | Yes | 1–256 characters |
|
||||||
| `vapidPublicKey` | string | Yes | 1–512 characters |
|
| `vapidPublicKey` | string | Yes | 1–512 characters |
|
||||||
@@ -188,7 +188,7 @@ Creates the first admin user (no OIDC identity yet, `claimed: false`) and valida
|
|||||||
```
|
```
|
||||||
|
|
||||||
| Field | Type | Required | Constraints |
|
| Field | Type | Required | Constraints |
|
||||||
| --------------- | ------ | -------- | ---------------- |
|
| --------------- | ------ | -------- | ------------------------------- |
|
||||||
| `fastmailEmail` | string | Yes | Valid email, max 256 characters |
|
| `fastmailEmail` | string | Yes | Valid email, max 256 characters |
|
||||||
| `appPassword` | string | Yes | 1–500 characters |
|
| `appPassword` | string | Yes | 1–500 characters |
|
||||||
|
|
||||||
@@ -233,6 +233,7 @@ Pre-auth endpoint. Returns which authentication methods are currently enabled. U
|
|||||||
Validates a username + password against `local_credentials` and issues a signed `local-session` JWT cookie. Pre-auth — reachable without a session.
|
Validates a username + password against `local_credentials` and issues a signed `local-session` JWT cookie. Pre-auth — reachable without a session.
|
||||||
|
|
||||||
Rate limiting is per-username (not per-IP):
|
Rate limiting is per-username (not per-IP):
|
||||||
|
|
||||||
- 5 failures within 60 seconds → `429 Too Many Requests`
|
- 5 failures within 60 seconds → `429 Too Many Requests`
|
||||||
- 10 cumulative failures → `423 Account Locked` (auto-expires after 15 minutes or on admin password reset)
|
- 10 cumulative failures → `423 Account Locked` (auto-expires after 15 minutes or on admin password reset)
|
||||||
|
|
||||||
@@ -245,7 +246,7 @@ Timing-oracle defense: `verifyPassword` (scrypt) is always called, even for unkn
|
|||||||
```
|
```
|
||||||
|
|
||||||
| Field | Type | Required | Constraints |
|
| Field | Type | Required | Constraints |
|
||||||
| ---------- | ------ | -------- | ---------------- |
|
| ---------- | ------ | -------- | -------------------------- |
|
||||||
| `username` | string | Yes | 1–128 characters (trimmed) |
|
| `username` | string | Yes | 1–128 characters (trimmed) |
|
||||||
| `password` | string | Yes | 1–1000 characters |
|
| `password` | string | Yes | 1–1000 characters |
|
||||||
|
|
||||||
@@ -297,7 +298,7 @@ Display name is derived from OIDC claims in priority order: `name` → `preferre
|
|||||||
```
|
```
|
||||||
|
|
||||||
| Field | Type | Description |
|
| Field | Type | Description |
|
||||||
| -------------------- | ------- | ---------------------------------------------------------------- |
|
| -------------------- | ------- | ---------------------------------------------------------- |
|
||||||
| `id` | integer | Stable member ID |
|
| `id` | integer | Stable member ID |
|
||||||
| `displayName` | string | Derived from OIDC claims or set by admin |
|
| `displayName` | string | Derived from OIDC claims or set by admin |
|
||||||
| `color` | string | Member's assigned color (hex) |
|
| `color` | string | Member's assigned color (hex) |
|
||||||
@@ -324,7 +325,7 @@ Member self-service endpoint to set or rotate their own Fastmail CalDAV app pass
|
|||||||
```
|
```
|
||||||
|
|
||||||
| Field | Type | Required | Constraints |
|
| Field | Type | Required | Constraints |
|
||||||
| --------------- | ------ | -------- | ---------------- |
|
| --------------- | ------ | -------- | ------------------------------- |
|
||||||
| `providerType` | string | Yes | Must be `"caldav"` |
|
| `providerType` | string | Yes | Must be `"caldav"` |
|
||||||
| `fastmailEmail` | string | Yes | Valid email, max 256 characters |
|
| `fastmailEmail` | string | Yes | Valid email, max 256 characters |
|
||||||
| `appPassword` | string | Yes | 1–500 characters |
|
| `appPassword` | string | Yes | 1–500 characters |
|
||||||
@@ -349,7 +350,7 @@ Self-service password change for local-auth members. Requires the current passwo
|
|||||||
```
|
```
|
||||||
|
|
||||||
| Field | Type | Required | Constraints |
|
| Field | Type | Required | Constraints |
|
||||||
| ----------------- | ------ | -------- | ---------------- |
|
| ----------------- | ------ | -------- | -------------------- |
|
||||||
| `currentPassword` | string | Yes | 1+ characters |
|
| `currentPassword` | string | Yes | 1+ characters |
|
||||||
| `newPassword` | string | Yes | Minimum 8 characters |
|
| `newPassword` | string | Yes | Minimum 8 characters |
|
||||||
|
|
||||||
@@ -943,7 +944,7 @@ Creates a new local-auth member: inserts a `users` row and a `local_credentials`
|
|||||||
```
|
```
|
||||||
|
|
||||||
| Field | Type | Required | Constraints |
|
| Field | Type | Required | Constraints |
|
||||||
| ----------------- | ------ | -------- | ---------------- |
|
| ----------------- | ------ | -------- | -------------------- |
|
||||||
| `displayName` | string | Yes | 1–256 characters |
|
| `displayName` | string | Yes | 1–256 characters |
|
||||||
| `username` | string | Yes | 1–128 characters |
|
| `username` | string | Yes | 1–128 characters |
|
||||||
| `initialPassword` | string | Yes | Minimum 8 characters |
|
| `initialPassword` | string | Yes | Minimum 8 characters |
|
||||||
@@ -998,7 +999,7 @@ Validates and stores a Fastmail CalDAV app password for any household member. Pe
|
|||||||
```
|
```
|
||||||
|
|
||||||
| Field | Type | Required | Constraints |
|
| Field | Type | Required | Constraints |
|
||||||
| --------------- | ------- | -------- | ---------------- |
|
| --------------- | ------- | -------- | ------------------------------- |
|
||||||
| `userId` | integer | Yes | Positive integer |
|
| `userId` | integer | Yes | Positive integer |
|
||||||
| `providerType` | string | Yes | Must be `"caldav"` |
|
| `providerType` | string | Yes | Must be `"caldav"` |
|
||||||
| `fastmailEmail` | string | Yes | Valid email, max 256 characters |
|
| `fastmailEmail` | string | Yes | Valid email, max 256 characters |
|
||||||
@@ -1066,7 +1067,7 @@ Validates and upserts the household IANA timezone into `app_config`.
|
|||||||
```
|
```
|
||||||
|
|
||||||
| Field | Type | Required | Constraints |
|
| Field | Type | Required | Constraints |
|
||||||
| ---------- | ------ | -------- | -------------------------- |
|
| ---------- | ------ | -------- | ------------------------------------ |
|
||||||
| `timezone` | string | Yes | Valid IANA timezone, 1–64 characters |
|
| `timezone` | string | Yes | Valid IANA timezone, 1–64 characters |
|
||||||
|
|
||||||
**Response 200** — `{ "ok": true }`
|
**Response 200** — `{ "ok": true }`
|
||||||
@@ -1106,7 +1107,7 @@ All error responses use a consistent JSON envelope.
|
|||||||
```
|
```
|
||||||
|
|
||||||
| HTTP Status | Meaning |
|
| HTTP Status | Meaning |
|
||||||
| ----------- | ------------------------------------------------------------------------------ |
|
| ----------- | -------------------------------------------------------------------------------------------------------- |
|
||||||
| `400` | Invalid request parameters (e.g., malformed date window) |
|
| `400` | Invalid request parameters (e.g., malformed date window) |
|
||||||
| `401` | Session missing or invalid |
|
| `401` | Session missing or invalid |
|
||||||
| `403` | Authenticated but not authorized (wrong owner, sharee attempted owner-only op, non-admin on admin route) |
|
| `403` | Authenticated but not authorized (wrong owner, sharee attempted owner-only op, non-admin on admin route) |
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ familysync/
|
|||||||
### Directory Rationale
|
### Directory Rationale
|
||||||
|
|
||||||
| Directory | Purpose |
|
| Directory | Purpose |
|
||||||
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||||
| `apps/api/src/routes/` | One file per resource — `events.ts`, `lists.ts`, `me.ts`, `push.ts`, `sse.ts`, `health.ts`, `admin.ts`, `setup.ts`, `authMode.ts`, `localAuth.ts` |
|
| `apps/api/src/routes/` | One file per resource — `events.ts`, `lists.ts`, `me.ts`, `push.ts`, `sse.ts`, `health.ts`, `admin.ts`, `setup.ts`, `authMode.ts`, `localAuth.ts` |
|
||||||
| `apps/api/src/broker/` | All CalDAV I/O: `client.ts` (tsdav factory), `sync.ts` (REPORT→DB), `poller.ts` (5-min ctag check), `outboxWorker.ts` (async write-back), `expand.ts` (RRULE expansion), `write.ts` (PUT/DELETE), `vevent.ts` (ICS builder), `crypto.ts` (AES-256-GCM for app passwords) |
|
| `apps/api/src/broker/` | All CalDAV I/O: `client.ts` (tsdav factory), `sync.ts` (REPORT→DB), `poller.ts` (5-min ctag check), `outboxWorker.ts` (async write-back), `expand.ts` (RRULE expansion), `write.ts` (PUT/DELETE), `vevent.ts` (ICS builder), `crypto.ts` (AES-256-GCM for app passwords) |
|
||||||
| `apps/api/src/auth/` | `middleware.ts` (re-exports `@hono/oidc-auth`), `devBypass.ts` (DEV_AUTH_BYPASS inject), `localAuthMiddleware.ts` (local-session cookie → user), `localCredentials.ts` (scrypt hash/verify), `localSession.ts` (JWT cookie issue/verify/clear), `oidcConfig.ts` (env+DB fallback for OIDC config), `linkNonceStore.ts` (single-use OIDC-link nonces), `linkOidc.ts` (bind OIDC identity to local user), `persistSessionCookie.ts` (session lifetime extension), `user.ts` (upsert on first OIDC login) |
|
| `apps/api/src/auth/` | `middleware.ts` (re-exports `@hono/oidc-auth`), `devBypass.ts` (DEV_AUTH_BYPASS inject), `localAuthMiddleware.ts` (local-session cookie → user), `localCredentials.ts` (scrypt hash/verify), `localSession.ts` (JWT cookie issue/verify/clear), `oidcConfig.ts` (env+DB fallback for OIDC config), `linkNonceStore.ts` (single-use OIDC-link nonces), `linkOidc.ts` (bind OIDC identity to local user), `persistSessionCookie.ts` (session lifetime extension), `user.ts` (upsert on first OIDC login) |
|
||||||
@@ -104,7 +104,7 @@ familysync/
|
|||||||
## Key Abstractions
|
## Key Abstractions
|
||||||
|
|
||||||
| Abstraction | File | Description |
|
| Abstraction | File | Description |
|
||||||
| ------------------------------------------ | ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ------------------------------------------------------ | ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `app` (Hono) | `apps/api/src/index.ts` | Root Hono app; mounts all routes and serves the PWA static build |
|
| `app` (Hono) | `apps/api/src/index.ts` | Root Hono app; mounts all routes and serves the PWA static build |
|
||||||
| Drizzle schema | `apps/api/src/db/schema.ts` | Single source of truth for all table definitions (`users`, `memberCredentials`, `localCredentials`, `calendars`, `calendarEvents`, `calendarOutbox`, `lists`, `listShares`, `listItems`, `pushSubscriptions`, `appConfig`) |
|
| Drizzle schema | `apps/api/src/db/schema.ts` | Single source of truth for all table definitions (`users`, `memberCredentials`, `localCredentials`, `calendars`, `calendarEvents`, `calendarOutbox`, `lists`, `listShares`, `listItems`, `pushSubscriptions`, `appConfig`) |
|
||||||
| `syncCalendar` | `apps/api/src/broker/sync.ts` | REPORT → ical.js parse → `onDuplicateKeyUpdate` upsert into MariaDB |
|
| `syncCalendar` | `apps/api/src/broker/sync.ts` | REPORT → ical.js parse → `onDuplicateKeyUpdate` upsert into MariaDB |
|
||||||
@@ -235,7 +235,7 @@ routes/setup.ts ──→ db (app_config)
|
|||||||
## Infrastructure
|
## Infrastructure
|
||||||
|
|
||||||
| Component | Technology |
|
| Component | Technology |
|
||||||
| -------------------- | ------------------------------------------------------------------------------------------------------------------- |
|
| -------------------- | --------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| Runtime | Node.js 22 LTS |
|
| Runtime | Node.js 22 LTS |
|
||||||
| HTTP framework | Hono 4.x (`@hono/node-server`) |
|
| HTTP framework | Hono 4.x (`@hono/node-server`) |
|
||||||
| Database | MariaDB 11 (Docker volume) |
|
| Database | MariaDB 11 (Docker volume) |
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ All runtime configuration is supplied via environment variables. There are no JS
|
|||||||
### Database
|
### Database
|
||||||
|
|
||||||
| Variable | Required | Default | Description |
|
| Variable | Required | Default | Description |
|
||||||
| ------------------ | ------------ | ------------ | ------------------------------------------------------------------------------------------------------------------------ |
|
| ------------------ | ------------ | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `DB_HOST` | Yes | `localhost` | MariaDB hostname. Use `mariadb` inside Docker Compose; use `localhost` (or `127.0.0.1`) for host-side dev runs. |
|
| `DB_HOST` | Yes | `localhost` | MariaDB hostname. Use `mariadb` inside Docker Compose; use `localhost` (or `127.0.0.1`) for host-side dev runs. |
|
||||||
| `DB_PORT` | No | `3306` | MariaDB port. |
|
| `DB_PORT` | No | `3306` | MariaDB port. |
|
||||||
| `DB_USER` | No | `familysync` | Database user. |
|
| `DB_USER` | No | `familysync` | Database user. |
|
||||||
@@ -56,7 +56,7 @@ Five of these variables — `DB_HOST`, `DB_USER`, `DB_PASSWORD`, `DB_NAME`, `DB_
|
|||||||
These variables govern the stateless local-auth path introduced in Phase 19. Local auth issues a separate `local-session` JWT cookie (distinct from `oidc-auth`) signed with `LOCAL_SESSION_SECRET`.
|
These variables govern the stateless local-auth path introduced in Phase 19. Local auth issues a separate `local-session` JWT cookie (distinct from `oidc-auth`) signed with `LOCAL_SESSION_SECRET`.
|
||||||
|
|
||||||
| Variable | Required | Default | Description |
|
| Variable | Required | Default | Description |
|
||||||
| ----------------------- | ------------------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ----------------------- | ------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||||
| `LOCAL_SESSION_SECRET` | **Required** (non-bypass) | _(none)_ | 32+ character secret used to sign and verify `local-session` JWT cookies (HS256). Generate with `openssl rand -base64 32`. The API refuses to start with a fatal error if this is absent or shorter than 32 characters, unless `DEV_AUTH_BYPASS=true`. |
|
| `LOCAL_SESSION_SECRET` | **Required** (non-bypass) | _(none)_ | 32+ character secret used to sign and verify `local-session` JWT cookies (HS256). Generate with `openssl rand -base64 32`. The API refuses to start with a fatal error if this is absent or shorter than 32 characters, unless `DEV_AUTH_BYPASS=true`. |
|
||||||
| `LOCAL_SESSION_EXPIRES` | No | `86400` | `local-session` cookie `Max-Age` in seconds (default 1 day). Mirrors `OIDC_AUTH_EXPIRES` but applies to the local-auth cookie. Malformed (non-numeric) values silently fall back to the default. Source: `apps/api/src/auth/localSession.ts`. |
|
| `LOCAL_SESSION_EXPIRES` | No | `86400` | `local-session` cookie `Max-Age` in seconds (default 1 day). Mirrors `OIDC_AUTH_EXPIRES` but applies to the local-auth cookie. Malformed (non-numeric) values silently fall back to the default. Source: `apps/api/src/auth/localSession.ts`. |
|
||||||
|
|
||||||
@@ -91,7 +91,7 @@ npx web-push generate-vapid-keys --json
|
|||||||
### Runtime Mode
|
### Runtime Mode
|
||||||
|
|
||||||
| Variable | Required | Default | Description |
|
| Variable | Required | Default | Description |
|
||||||
| ----------------- | -------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ----------------- | -------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||||
| `NODE_ENV` | No | _(not set)_ | Set to `production` in the production Docker Compose. When `production`, the dev-auth bypass is unconditionally disabled regardless of `DEV_AUTH_BYPASS`. |
|
| `NODE_ENV` | No | _(not set)_ | Set to `production` in the production Docker Compose. When `production`, the dev-auth bypass is unconditionally disabled regardless of `DEV_AUTH_BYPASS`. |
|
||||||
| `DEV_AUTH_BYPASS` | No | _(not set)_ | Set to `true` to bypass OIDC authentication for local development without a live Authelia instance. **Only active when `NODE_ENV !== 'production'`.** The production `docker-compose.yml` must never include this variable. |
|
| `DEV_AUTH_BYPASS` | No | _(not set)_ | Set to `true` to bypass OIDC authentication for local development without a live Authelia instance. **Only active when `NODE_ENV !== 'production'`.** The production `docker-compose.yml` must never include this variable. |
|
||||||
| `TZ` | No | _(not set)_ | IANA timezone identifier (e.g. `America/Toronto`) used as the server-side fallback for the household timezone when no value is stored in `app_config`. The full fallback chain is: stored DB value → `TZ` env → `Intl.DateTimeFormat().resolvedOptions().timeZone`. Empty or whitespace values are ignored. Source: `apps/api/src/lib/householdTimezone.ts`. |
|
| `TZ` | No | _(not set)_ | IANA timezone identifier (e.g. `America/Toronto`) used as the server-side fallback for the household timezone when no value is stored in `app_config`. The full fallback chain is: stored DB value → `TZ` env → `Intl.DateTimeFormat().resolvedOptions().timeZone`. Empty or whitespace values are ignored. Source: `apps/api/src/lib/householdTimezone.ts`. |
|
||||||
@@ -103,7 +103,7 @@ npx web-push generate-vapid-keys --json
|
|||||||
These variables are never needed in production and should not appear in the production `.env`.
|
These variables are never needed in production and should not appear in the production `.env`.
|
||||||
|
|
||||||
| Variable | Scope | Default | Description |
|
| Variable | Scope | Default | Description |
|
||||||
| ----------------------- | ---------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
| ----------------------- | --------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `FASTMAIL_EMAIL` | Dev spike script only | _(none)_ | Fastmail account email. Read only by `apps/api/src/broker/spike.ts`, a standalone dev script for enumerating CalDAV collections. Not imported by the API or Docker image. |
|
| `FASTMAIL_EMAIL` | Dev spike script only | _(none)_ | Fastmail account email. Read only by `apps/api/src/broker/spike.ts`, a standalone dev script for enumerating CalDAV collections. Not imported by the API or Docker image. |
|
||||||
| `FASTMAIL_APP_PASSWORD` | Dev spike script only | _(none)_ | Fastmail app password. Read only by `apps/api/src/broker/spike.ts`. **Never logged.** Not used by the API in any environment. |
|
| `FASTMAIL_APP_PASSWORD` | Dev spike script only | _(none)_ | Fastmail app password. Read only by `apps/api/src/broker/spike.ts`. **Never logged.** Not used by the API in any environment. |
|
||||||
| `PLAYWRIGHT_BASE_URL` | E2E tests only | `http://localhost:5173` | Base URL for Playwright e2e tests. Overridden to `http://127.0.0.1:5173` in CI to avoid IPv6 resolution failures. Source: `apps/pwa/playwright.config.ts`. |
|
| `PLAYWRIGHT_BASE_URL` | E2E tests only | `http://localhost:5173` | Base URL for Playwright e2e tests. Overridden to `http://127.0.0.1:5173` in CI to avoid IPv6 resolution failures. Source: `apps/pwa/playwright.config.ts`. |
|
||||||
|
|||||||
+5
-5
@@ -101,7 +101,7 @@ Vite serves the PWA with HMR on the configured dev port. The PWA's API calls tar
|
|||||||
### Root workspace scripts
|
### Root workspace scripts
|
||||||
|
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
| ------------------------ | ------------------------------------------------------------- |
|
| ----------------------- | --------------------------------------------------------------------------- |
|
||||||
| `pnpm dev:api` | Start API dev watcher (`node --watch dist/index.js`) |
|
| `pnpm dev:api` | Start API dev watcher (`node --watch dist/index.js`) |
|
||||||
| `pnpm dev:pwa` | Start Vite dev server for the PWA |
|
| `pnpm dev:pwa` | Start Vite dev server for the PWA |
|
||||||
| `pnpm build` | Build both `apps/api` (tsc) and `apps/pwa` (tsc + vite build) |
|
| `pnpm build` | Build both `apps/api` (tsc) and `apps/pwa` (tsc + vite build) |
|
||||||
@@ -157,9 +157,9 @@ pnpm md:lint # Markdown lint (also runs in CI fast-checks)
|
|||||||
|
|
||||||
Config: `eslint.config.js` (root, flat ESLint 9 format). The config covers:
|
Config: `eslint.config.js` (root, flat ESLint 9 format). The config covers:
|
||||||
|
|
||||||
- **All `apps/**/*.{ts,tsx}`** — `js.configs.recommended` + `tseslint.configs.recommendedTypeChecked` with `projectService: true` (type-aware rules, auto-discovers all `tsconfig.json` files)
|
- **All `apps/**/\*.{ts,tsx}`** — `js.configs.recommended`+`tseslint.configs.recommendedTypeChecked`with`projectService: true`(type-aware rules, auto-discovers all`tsconfig.json` files)
|
||||||
- **`apps/pwa/**/*.{ts,tsx}` additionally** — `eslint-plugin-react` + `eslint-plugin-react-hooks` (React 19 flat config; React Compiler rules disabled — this codebase does not use the Compiler)
|
- **`apps/pwa/**/\*.{ts,tsx}`additionally** —`eslint-plugin-react`+`eslint-plugin-react-hooks` (React 19 flat config; React Compiler rules disabled — this codebase does not use the Compiler)
|
||||||
- **All `apps/**/*.{ts,tsx}`** — `eslint-plugin-security` (14 of 15 rules at error; `detect-object-injection` disabled due to high false-positive rate on schema-derived numeric keys)
|
- **All `apps/**/\*.{ts,tsx}`** — `eslint-plugin-security`(14 of 15 rules at error;`detect-object-injection` disabled due to high false-positive rate on schema-derived numeric keys)
|
||||||
- **Tool configs + test dirs** (`drizzle.config.ts`, `vitest.config.ts`, `apps/api/tests/**`, `apps/pwa/e2e/**`) — type-aware rules disabled via `disableTypeChecked` (these files are outside the main tsconfig projects)
|
- **Tool configs + test dirs** (`drizzle.config.ts`, `vitest.config.ts`, `apps/api/tests/**`, `apps/pwa/e2e/**`) — type-aware rules disabled via `disableTypeChecked` (these files are outside the main tsconfig projects)
|
||||||
- **Prettier integration** — `eslint-config-prettier` last in the config disables all formatting rules that conflict with Prettier
|
- **Prettier integration** — `eslint-config-prettier` last in the config disables all formatting rules that conflict with Prettier
|
||||||
|
|
||||||
@@ -196,7 +196,7 @@ Run `pnpm typecheck` before opening a PR to catch errors that vitest and Vite bu
|
|||||||
Every PR to `main` runs through `.gitea/workflows/ci.yml`. A `changes` path-filter job determines whether code files changed; the `api` and `harness` jobs are skipped entirely for doc-only PRs (changes only to `.planning/**`, `.gitea/**`, or `*.md` files).
|
Every PR to `main` runs through `.gitea/workflows/ci.yml`. A `changes` path-filter job determines whether code files changed; the `api` and `harness` jobs are skipped entirely for doc-only PRs (changes only to `.planning/**`, `.gitea/**`, or `*.md` files).
|
||||||
|
|
||||||
| Job | Runs on | Checks |
|
| Job | Runs on | Checks |
|
||||||
| ------------- | ------------------- | ----------------------------------------------------------------------------------------------------------- |
|
| ------------- | -------------------- | ------------------------------------------------------------------------------------------------------------ |
|
||||||
| `fast-checks` | Every PR | `pnpm lint` → `pnpm format:check` → `pnpm md:lint` → `pnpm typecheck` → `pnpm --filter @familysync/pwa test` |
|
| `fast-checks` | Every PR | `pnpm lint` → `pnpm format:check` → `pnpm md:lint` → `pnpm typecheck` → `pnpm --filter @familysync/pwa test` |
|
||||||
| `api` | Code-change PRs only | DB migrations + `pnpm --filter @familysync/api test` (vitest against a MariaDB 11 service container) |
|
| `api` | Code-change PRs only | DB migrations + `pnpm --filter @familysync/api test` (vitest against a MariaDB 11 service container) |
|
||||||
| `harness` | Code-change PRs only | DB migrations + seed dev user + API build + Playwright e2e (WebKit + Chromium) with `DEV_AUTH_BYPASS=true` |
|
| `harness` | Code-change PRs only | DB migrations + seed dev user + API build + Playwright e2e (WebKit + Chromium) with `DEV_AUTH_BYPASS=true` |
|
||||||
|
|||||||
@@ -134,6 +134,7 @@ Or set `DB_HOST=localhost` directly in your `.env` for host-side dev.
|
|||||||
|
|
||||||
**`[FATAL] LOCAL_SESSION_SECRET is not set or is shorter than 32 characters`**
|
**`[FATAL] LOCAL_SESSION_SECRET is not set or is shorter than 32 characters`**
|
||||||
The API refuses to start in non-bypass mode without a valid `LOCAL_SESSION_SECRET`. Either:
|
The API refuses to start in non-bypass mode without a valid `LOCAL_SESSION_SECRET`. Either:
|
||||||
|
|
||||||
- Set `DEV_AUTH_BYPASS=true` in `.env` for local dev (bypass mode exempts the requirement), or
|
- Set `DEV_AUTH_BYPASS=true` in `.env` for local dev (bypass mode exempts the requirement), or
|
||||||
- Run `pnpm generate-secrets` and add the generated `LOCAL_SESSION_SECRET` value to `.env`.
|
- Run `pnpm generate-secrets` and add the generated `LOCAL_SESSION_SECRET` value to `.env`.
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -7,7 +7,7 @@
|
|||||||
Both apps use **Vitest** (`^4.1.8`).
|
Both apps use **Vitest** (`^4.1.8`).
|
||||||
|
|
||||||
| App | Environment | Global setup | Per-file setup |
|
| App | Environment | Global setup | Per-file setup |
|
||||||
| ---------- | ----------- | ----------------------------------- | ---------------------------- |
|
| ---------- | ----------- | ------------------------------- | ---------------------------- |
|
||||||
| `apps/api` | `node` | `apps/api/test/global-setup.ts` | `apps/api/test/setup.ts` |
|
| `apps/api` | `node` | `apps/api/test/global-setup.ts` | `apps/api/test/setup.ts` |
|
||||||
| `apps/pwa` | `jsdom` | — | `apps/pwa/src/test-setup.ts` |
|
| `apps/pwa` | `jsdom` | — | `apps/pwa/src/test-setup.ts` |
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ pnpm --filter @familysync/api exec vitest run tests/routes/lists.test.ts
|
|||||||
The PWA has a Playwright harness configured in `apps/pwa/playwright.config.ts` with three device profiles:
|
The PWA has a Playwright harness configured in `apps/pwa/playwright.config.ts` with three device profiles:
|
||||||
|
|
||||||
| Profile | Viewport | Engine | User-Agent |
|
| Profile | Viewport | Engine | User-Agent |
|
||||||
| --------- | --------- | -------- | ------------------------- |
|
| --------- | -------- | -------- | ------------------------- |
|
||||||
| `iphone` | 390×844 | WebKit | Mobile Safari (iPhone 14) |
|
| `iphone` | 390×844 | WebKit | Mobile Safari (iPhone 14) |
|
||||||
| `pixel` | 412×915 | Chromium | Chrome Android (Pixel 7) |
|
| `pixel` | 412×915 | Chromium | Chrome Android (Pixel 7) |
|
||||||
| `desktop` | 1280×720 | Chromium | Desktop Chrome |
|
| `desktop` | 1280×720 | Chromium | Desktop Chrome |
|
||||||
@@ -233,7 +233,7 @@ The throwaway credentials (`DB_USER=familysync`, `DB_PASSWORD=testpass`) are sco
|
|||||||
Runs the Playwright mobile and desktop e2e harness (iphone + pixel + desktop) against a runner-hosted dev stack. Skipped for doc-only PRs.
|
Runs the Playwright mobile and desktop e2e harness (iphone + pixel + desktop) against a runner-hosted dev stack. Skipped for doc-only PRs.
|
||||||
|
|
||||||
| Step | Detail |
|
| Step | Detail |
|
||||||
| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||||
| MariaDB service | Same `mariadb:11` setup as the `api` job |
|
| MariaDB service | Same `mariadb:11` setup as the `api` job |
|
||||||
| Schema migrations | `pnpm --filter @familysync/api db:migrate` |
|
| Schema migrations | `pnpm --filter @familysync/api db:migrate` |
|
||||||
| Dev user seed | Inserts `users` row id=1 (`INSERT IGNORE`) for `DEV_AUTH_BYPASS` |
|
| Dev user seed | Inserts `users` row id=1 (`INSERT IGNORE`) for `DEV_AUTH_BYPASS` |
|
||||||
@@ -251,7 +251,7 @@ The API process is started and the Playwright suite invoked within a single CI s
|
|||||||
Runs secret scanning and dependency audits. Always runs regardless of the `changes` filter (secrets can appear in doc-only commits). Dependency audit and outdated checks run only when code changes are detected.
|
Runs secret scanning and dependency audits. Always runs regardless of the `changes` filter (secrets can appear in doc-only commits). Dependency audit and outdated checks run only when code changes are detected.
|
||||||
|
|
||||||
| Step | Tool/Command | Detail |
|
| Step | Tool/Command | Detail |
|
||||||
| ------------------- | ------------------------------- | -------------------------------------------------------------- |
|
| ---------------- | --------------------------------- | ------------------------------------------------------ |
|
||||||
| Secret scan | `gitleaks` (v8.30.1) | Scans the PR diff range; blocks on any finding |
|
| Secret scan | `gitleaks` (v8.30.1) | Scans the PR diff range; blocks on any finding |
|
||||||
| Dependency audit | `node scripts/check-audit.mjs` | Blocks on High or Critical severity vulnerabilities |
|
| Dependency audit | `node scripts/check-audit.mjs` | Blocks on High or Critical severity vulnerabilities |
|
||||||
| Outdated report | `node scripts/check-outdated.mjs` | Advisory only — always exits 0, logged but never gates |
|
| Outdated report | `node scripts/check-outdated.mjs` | Advisory only — always exits 0, logged but never gates |
|
||||||
|
|||||||
+2
-1
@@ -33,7 +33,7 @@ FamilySync uses a self-hosted Gitea Actions runner. Two workflows govern the rel
|
|||||||
Triggered on every pull request targeting `main`. The workflow runs a `changes` filter job first, then launches the following jobs in parallel:
|
Triggered on every pull request targeting `main`. The workflow runs a `changes` filter job first, then launches the following jobs in parallel:
|
||||||
|
|
||||||
| Job | Runs when | What it checks |
|
| Job | Runs when | What it checks |
|
||||||
| ------------- | -------------------- | --------------------------------------------------------------------------------------------------------- |
|
| ------------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------- |
|
||||||
| `fast-checks` | Always | Lint (`pnpm lint`), format check (`pnpm format:check`), markdown lint (`pnpm md:lint`), typecheck, PWA unit tests |
|
| `fast-checks` | Always | Lint (`pnpm lint`), format check (`pnpm format:check`), markdown lint (`pnpm md:lint`), typecheck, PWA unit tests |
|
||||||
| `api` | Code-changing PRs only | DB migrations + API integration tests against a live MariaDB service container |
|
| `api` | Code-changing PRs only | DB migrations + API integration tests against a live MariaDB service container |
|
||||||
| `harness` | Code-changing PRs only | Full Playwright E2E suite (iPhone + Pixel + desktop profiles) against the compiled API |
|
| `harness` | Code-changing PRs only | Full Playwright E2E suite (iPhone + Pixel + desktop profiles) against the compiled API |
|
||||||
@@ -62,6 +62,7 @@ The current milestone prefix (`v1.1`) is set in the `MILESTONE` env var at the t
|
|||||||
The immutable `:<milestone>-<sha>` tag is pushed first. `:latest` is only moved after the immutable tag has landed, so a failed second push can never leave `:latest` advanced without a corresponding rollback tag.
|
The immutable `:<milestone>-<sha>` tag is pushed first. `:latest` is only moved after the immutable tag has landed, so a failed second push can never leave `:latest` advanced without a corresponding rollback tag.
|
||||||
|
|
||||||
Before pushing, the workflow runs two image hygiene assertions:
|
Before pushing, the workflow runs two image hygiene assertions:
|
||||||
|
|
||||||
1. **Static assertions** — verifies `.dockerignore` contains all required exclusion patterns and that the build targets `--target production`.
|
1. **Static assertions** — verifies `.dockerignore` contains all required exclusion patterns and that the build targets `--target production`.
|
||||||
2. **Boot-smoke** — starts the image with `NODE_ENV=production` and `DEV_AUTH_BYPASS=true` and asserts that it refuses to start (confirming the D-08 guard fires in the shipped image).
|
2. **Boot-smoke** — starts the image with `NODE_ENV=production` and `DEV_AUTH_BYPASS=true` and asserts that it refuses to start (confirming the D-08 guard fires in the shipped image).
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user