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
@@ -259,16 +259,16 @@ 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.
|
||||
|
||||
| Dimension | Rating | Confidence |
|
||||
|-----------|--------|------------|
|
||||
| Communication | conversational | MEDIUM |
|
||||
| Decisions | fast-intuitive | MEDIUM |
|
||||
| Explanations | concise | MEDIUM |
|
||||
| Debugging | diagnostic | MEDIUM |
|
||||
| UX Philosophy | design-conscious | MEDIUM |
|
||||
| Vendor Choices | opinionated | LOW |
|
||||
| Frustrations | instruction-adherence | MEDIUM |
|
||||
| Learning | self-directed | MEDIUM |
|
||||
| Dimension | Rating | Confidence |
|
||||
| -------------- | --------------------- | ---------- |
|
||||
| Communication | conversational | MEDIUM |
|
||||
| Decisions | fast-intuitive | MEDIUM |
|
||||
| Explanations | concise | MEDIUM |
|
||||
| Debugging | diagnostic | MEDIUM |
|
||||
| UX Philosophy | design-conscious | MEDIUM |
|
||||
| Vendor Choices | opinionated | LOW |
|
||||
| Frustrations | instruction-adherence | MEDIUM |
|
||||
| Learning | self-directed | MEDIUM |
|
||||
|
||||
**Directives:**
|
||||
|
||||
|
||||
@@ -132,13 +132,13 @@ See [`docs/deployment.md`](docs/deployment.md) for Unraid/Docker Compose deploym
|
||||
|
||||
Every PR to `main` must pass four jobs before it can merge:
|
||||
|
||||
| Job | What it runs |
|
||||
| -------------------- | -------------------------------------------------------------------------- |
|
||||
| `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 / harness` | Playwright end-to-end harness (WebKit iPhone + Chromium Pixel) |
|
||||
| `CI / security` | Gitleaks secret scan (all PRs) + `pnpm audit` + outdated report (code PRs) |
|
||||
| `CI / gate` | Aggregate: asserts all jobs above passed or were legitimately skipped |
|
||||
| Job | What it runs |
|
||||
| ------------------ | ---------------------------------------------------------------------------------- |
|
||||
| `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 / harness` | Playwright end-to-end harness (WebKit iPhone + Chromium Pixel) |
|
||||
| `CI / security` | Gitleaks secret scan (all PRs) + `pnpm audit` + outdated report (code PRs) |
|
||||
| `CI / gate` | Aggregate: asserts all jobs above passed or were legitimately skipped |
|
||||
|
||||
`fast-checks` and `security` always run. `api` and `harness` are skipped for doc-only PRs (no changes outside `.gitea/`, `.planning/`, or `*.md`). The `gate` job is the single required check for merge. Defined in `.gitea/workflows/ci.yml`.
|
||||
|
||||
|
||||
+26
-26
@@ -125,27 +125,27 @@ Migration files are written to `src/db/migrations/` and checked into source cont
|
||||
|
||||
## Environment variables
|
||||
|
||||
| Variable | Required | Description |
|
||||
| ----------------------------- | ------------------- | ---------------------------------------------------------------------------------- |
|
||||
| `DB_HOST` | Yes | MariaDB host |
|
||||
| `DB_USER` | Yes | MariaDB user |
|
||||
| `DB_PASSWORD` | Yes | MariaDB password |
|
||||
| `DB_NAME` | Yes | MariaDB database name |
|
||||
| `DB_PORT` | No (default `3306`) | MariaDB port |
|
||||
| `OIDC_ISSUER` | Yes (production) | Authelia issuer URL |
|
||||
| `OIDC_CLIENT_ID` | Yes (production) | OIDC client ID |
|
||||
| `OIDC_CLIENT_SECRET` | Yes (production) | OIDC client secret |
|
||||
| `OIDC_AUTH_EXTERNAL_URL` | Yes (production) | External-facing URL for redirect_uri behind Pangolin tunnel |
|
||||
| `OIDC_REDIRECT_URI` | No | Explicit redirect URI (overrides the `${OIDC_AUTH_EXTERNAL_URL}/callback` default) |
|
||||
| `VAPID_SUBJECT` | Yes (push) | `mailto:` or `https:` operator identifier |
|
||||
| `VAPID_PUBLIC_KEY` | Yes (push) | VAPID public key |
|
||||
| `VAPID_PRIVATE_KEY` | Yes (push) | VAPID private key |
|
||||
| `APP_PASSWORD_ENCRYPTION_KEY` | Yes | AES-256-GCM key (64-char hex) for stored Fastmail app passwords |
|
||||
| `LOCAL_SESSION_SECRET` | Yes (local auth) | HS256 signing key for local-session JWT cookies (min 32 chars) |
|
||||
| `LOCAL_SESSION_EXPIRES` | No (default `86400`)| Local session lifetime in seconds |
|
||||
| `DEV_AUTH_BYPASS` | No | Set to `true` (non-production only) to skip OIDC and inject a dev user |
|
||||
| `NODE_ENV` | No | Set to `production` to enforce OIDC unconditionally |
|
||||
| `TZ` | No | IANA timezone fallback when household_timezone is not set in app_config |
|
||||
| Variable | Required | Description |
|
||||
| ----------------------------- | -------------------- | ---------------------------------------------------------------------------------- |
|
||||
| `DB_HOST` | Yes | MariaDB host |
|
||||
| `DB_USER` | Yes | MariaDB user |
|
||||
| `DB_PASSWORD` | Yes | MariaDB password |
|
||||
| `DB_NAME` | Yes | MariaDB database name |
|
||||
| `DB_PORT` | No (default `3306`) | MariaDB port |
|
||||
| `OIDC_ISSUER` | Yes (production) | Authelia issuer URL |
|
||||
| `OIDC_CLIENT_ID` | Yes (production) | OIDC client ID |
|
||||
| `OIDC_CLIENT_SECRET` | Yes (production) | OIDC client secret |
|
||||
| `OIDC_AUTH_EXTERNAL_URL` | Yes (production) | External-facing URL for redirect_uri behind Pangolin tunnel |
|
||||
| `OIDC_REDIRECT_URI` | No | Explicit redirect URI (overrides the `${OIDC_AUTH_EXTERNAL_URL}/callback` default) |
|
||||
| `VAPID_SUBJECT` | Yes (push) | `mailto:` or `https:` operator identifier |
|
||||
| `VAPID_PUBLIC_KEY` | Yes (push) | VAPID public key |
|
||||
| `VAPID_PRIVATE_KEY` | Yes (push) | VAPID private key |
|
||||
| `APP_PASSWORD_ENCRYPTION_KEY` | Yes | AES-256-GCM key (64-char hex) for stored Fastmail app passwords |
|
||||
| `LOCAL_SESSION_SECRET` | Yes (local auth) | HS256 signing key for local-session JWT cookies (min 32 chars) |
|
||||
| `LOCAL_SESSION_EXPIRES` | No (default `86400`) | Local session lifetime in seconds |
|
||||
| `DEV_AUTH_BYPASS` | No | Set to `true` (non-production only) to skip OIDC and inject a dev user |
|
||||
| `NODE_ENV` | No | Set to `production` to enforce OIDC unconditionally |
|
||||
| `TZ` | No | IANA timezone fallback when household_timezone is not set in app_config |
|
||||
|
||||
> **Note:** `CREDENTIAL_ENCRYPTION_KEY` was renamed to `APP_PASSWORD_ENCRYPTION_KEY`. Update any existing `.env` files if upgrading from an earlier phase.
|
||||
|
||||
@@ -155,11 +155,11 @@ See [../../docs/CONFIGURATION.md](../../docs/CONFIGURATION.md) for the full refe
|
||||
|
||||
The API supports two non-exclusive auth modes, determined at startup:
|
||||
|
||||
| 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`. |
|
||||
| **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. |
|
||||
| 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`. |
|
||||
| **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. |
|
||||
|
||||
`GET /api/auth/mode` returns `{ localEnabled, oidcEnabled }` before authentication — the PWA uses this to decide which login form to show.
|
||||
|
||||
|
||||
+11
-11
@@ -32,17 +32,17 @@ The API backend must also be running for most features. See [GETTING-STARTED.md]
|
||||
|
||||
## Scripts
|
||||
|
||||
| Command | What it does |
|
||||
| ------------------------------------------------ | ------------------------------------------------------------------------- |
|
||||
| `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 preview` | Serve the production build locally |
|
||||
| `pnpm --filter @familysync/pwa lint` | Run ESLint over `src/` and `e2e/` with zero warnings allowed |
|
||||
| `pnpm --filter @familysync/pwa typecheck` | Run `tsc --noEmit` for both `src/` and `e2e/` tsconfigs |
|
||||
| `pnpm --filter @familysync/pwa test` | Run Vitest unit/integration suite once (`vitest run`) |
|
||||
| `pnpm --filter @familysync/pwa test:e2e` | Run Playwright end-to-end tests headlessly |
|
||||
| `pnpm --filter @familysync/pwa test:e2e:ui` | Open the Playwright UI runner |
|
||||
| `pnpm --filter @familysync/pwa test:e2e:headed` | Run Playwright tests in a headed browser |
|
||||
| Command | What it does |
|
||||
| ----------------------------------------------- | ------------------------------------------------------------- |
|
||||
| `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 preview` | Serve the production build locally |
|
||||
| `pnpm --filter @familysync/pwa lint` | Run ESLint over `src/` and `e2e/` with zero warnings allowed |
|
||||
| `pnpm --filter @familysync/pwa typecheck` | Run `tsc --noEmit` for both `src/` and `e2e/` tsconfigs |
|
||||
| `pnpm --filter @familysync/pwa test` | Run Vitest unit/integration suite once (`vitest run`) |
|
||||
| `pnpm --filter @familysync/pwa test:e2e` | Run Playwright end-to-end tests headlessly |
|
||||
| `pnpm --filter @familysync/pwa test:e2e:ui` | Open the Playwright UI runner |
|
||||
| `pnpm --filter @familysync/pwa test:e2e:headed` | Run Playwright tests in a headed browser |
|
||||
|
||||
## Source layout
|
||||
|
||||
|
||||
@@ -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({
|
||||
preset: {
|
||||
@@ -7,4 +7,4 @@ export default defineConfig({
|
||||
images: ['public/logo.svg'],
|
||||
// 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.
|
||||
})
|
||||
});
|
||||
|
||||
@@ -29,7 +29,13 @@ import { usePushSubscription } from '../hooks/usePushSubscription.js';
|
||||
import { useIsPhone } from '../hooks/useIsPhone.js';
|
||||
import { useFocusTrap } from '../hooks/useFocusTrap.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
|
||||
// tap-gated subscribe() path. Same logic as PushPermissionPrompt.
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
*/
|
||||
|
||||
:root,
|
||||
[data-theme="light"] {
|
||||
[data-theme='light'] {
|
||||
/* ─────────────────────────────────────────────────────────────────────────
|
||||
* BASE SURFACE / BORDER / TEXT PALETTE
|
||||
* ───────────────────────────────────────────────────────────────────────── */
|
||||
|
||||
@@ -42,7 +42,12 @@ export default defineConfig({
|
||||
icons: [
|
||||
{ src: '/icon-192.png', sizes: '192x192', 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',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
|
||||
+98
-97
@@ -25,54 +25,54 @@ No API key or `Authorization` header is used. Credentials are never included in
|
||||
|
||||
## Endpoints Overview
|
||||
|
||||
| Method | Path | Auth | Description |
|
||||
| ------ | ------------------------------------- | --------- | -------------------------------------------------- |
|
||||
| GET | `/health` | None | DB liveness check |
|
||||
| GET | `/callback` | None | OIDC authorization-code exchange |
|
||||
| GET | `/api/setup/status` | None | Setup wizard completion status |
|
||||
| POST | `/api/setup/config` | None | Store OIDC and VAPID config (wizard step 1) |
|
||||
| POST | `/api/setup/validate/db` | None | Validate DB connectivity (wizard step) |
|
||||
| POST | `/api/setup/validate/oidc` | None | Validate OIDC issuer discovery (wizard step) |
|
||||
| POST | `/api/setup/validate/vapid` | None | Validate VAPID key pair (wizard step) |
|
||||
| POST | `/api/setup/credential` | None | Store first admin's Fastmail credential (wizard) |
|
||||
| POST | `/api/setup/complete` | None | Lock the setup wizard |
|
||||
| GET | `/api/auth/mode` | None | Auth mode discovery (local vs OIDC enabled) |
|
||||
| POST | `/api/auth/local/login` | None | Local username+password login |
|
||||
| POST | `/api/auth/local/logout` | None | Clear local session cookie |
|
||||
| GET | `/api/auth/local/logout` | None | Clear local session cookie (browser redirect alias)|
|
||||
| GET | `/api/login` | OIDC | OIDC login entry point, redirects to `/` |
|
||||
| GET | `/api/me` | Session | Current user identity, role, and setup status |
|
||||
| POST | `/api/me/credential` | Session | Member self-service Fastmail credential update |
|
||||
| POST | `/api/me/password` | Session | Member self-change local password |
|
||||
| POST | `/api/me/link-oidc` | Session | Initiate OIDC identity link for a local user |
|
||||
| GET | `/api/events` | Session | Windowed calendar occurrences |
|
||||
| POST | `/api/events/create` | Session | Enqueue a new event write |
|
||||
| PATCH | `/api/events/:uid/edit` | Session | Enqueue an event update |
|
||||
| DELETE | `/api/events/:uid` | Session | Enqueue an event delete |
|
||||
| GET | `/api/events/sync-status` | Session | Outbox status for a UID |
|
||||
| GET | `/api/events/writable-calendars` | Session | Calendars the member can write to |
|
||||
| GET | `/api/lists` | Session | All lists accessible to the member |
|
||||
| POST | `/api/lists` | Session | Create a list |
|
||||
| PATCH | `/api/lists/:id` | Session | Update list name or sharing |
|
||||
| DELETE | `/api/lists/:id` | Session | Delete a list (owner only) |
|
||||
| GET | `/api/lists/:id/items` | Session | All items in a list |
|
||||
| POST | `/api/lists/:id/items` | Session | Add an item to a list |
|
||||
| PATCH | `/api/list-items/:itemId` | Session | Update a single list item field |
|
||||
| DELETE | `/api/list-items/:itemId` | Session | Delete a list item |
|
||||
| GET | `/api/sse/heartbeat` | Session | SSE heartbeat stream |
|
||||
| GET | `/api/sse/lists` | Session | Scoped live-list SSE stream |
|
||||
| GET | `/api/push/vapid-public-key` | Session | VAPID public key for push subscribe |
|
||||
| POST | `/api/push/subscription` | Session | Register a push subscription |
|
||||
| DELETE | `/api/push/subscription` | Session | Remove push subscriptions for caller |
|
||||
| GET | `/api/admin/members` | Admin | List members with credential status |
|
||||
| POST | `/api/admin/members` | Admin | Create a new local member |
|
||||
| POST | `/api/admin/members/:id/password` | Admin | Reset a local member's password |
|
||||
| POST | `/api/admin/credentials` | Admin | Validate and store a member's Fastmail credential |
|
||||
| GET | `/api/admin/calendars` | Admin | List synced calendars |
|
||||
| PUT | `/api/admin/calendars/:id/shared` | Admin | Designate the shared family calendar |
|
||||
| GET | `/api/admin/config/timezone` | Admin | Get household timezone |
|
||||
| PUT | `/api/admin/config/timezone` | Admin | Set household timezone |
|
||||
| POST | `/api/admin/config/timezone/seed` | Admin | Seed household timezone if not yet set |
|
||||
| Method | Path | Auth | Description |
|
||||
| ------ | --------------------------------- | ------- | --------------------------------------------------- |
|
||||
| GET | `/health` | None | DB liveness check |
|
||||
| GET | `/callback` | None | OIDC authorization-code exchange |
|
||||
| GET | `/api/setup/status` | None | Setup wizard completion status |
|
||||
| POST | `/api/setup/config` | None | Store OIDC and VAPID config (wizard step 1) |
|
||||
| POST | `/api/setup/validate/db` | None | Validate DB connectivity (wizard step) |
|
||||
| POST | `/api/setup/validate/oidc` | None | Validate OIDC issuer discovery (wizard step) |
|
||||
| POST | `/api/setup/validate/vapid` | None | Validate VAPID key pair (wizard step) |
|
||||
| POST | `/api/setup/credential` | None | Store first admin's Fastmail credential (wizard) |
|
||||
| POST | `/api/setup/complete` | None | Lock the setup wizard |
|
||||
| GET | `/api/auth/mode` | None | Auth mode discovery (local vs OIDC enabled) |
|
||||
| POST | `/api/auth/local/login` | None | Local username+password login |
|
||||
| POST | `/api/auth/local/logout` | None | Clear local session cookie |
|
||||
| GET | `/api/auth/local/logout` | None | Clear local session cookie (browser redirect alias) |
|
||||
| GET | `/api/login` | OIDC | OIDC login entry point, redirects to `/` |
|
||||
| GET | `/api/me` | Session | Current user identity, role, and setup status |
|
||||
| POST | `/api/me/credential` | Session | Member self-service Fastmail credential update |
|
||||
| POST | `/api/me/password` | Session | Member self-change local password |
|
||||
| POST | `/api/me/link-oidc` | Session | Initiate OIDC identity link for a local user |
|
||||
| GET | `/api/events` | Session | Windowed calendar occurrences |
|
||||
| POST | `/api/events/create` | Session | Enqueue a new event write |
|
||||
| PATCH | `/api/events/:uid/edit` | Session | Enqueue an event update |
|
||||
| DELETE | `/api/events/:uid` | Session | Enqueue an event delete |
|
||||
| GET | `/api/events/sync-status` | Session | Outbox status for a UID |
|
||||
| GET | `/api/events/writable-calendars` | Session | Calendars the member can write to |
|
||||
| GET | `/api/lists` | Session | All lists accessible to the member |
|
||||
| POST | `/api/lists` | Session | Create a list |
|
||||
| PATCH | `/api/lists/:id` | Session | Update list name or sharing |
|
||||
| DELETE | `/api/lists/:id` | Session | Delete a list (owner only) |
|
||||
| GET | `/api/lists/:id/items` | Session | All items in a list |
|
||||
| POST | `/api/lists/:id/items` | Session | Add an item to a list |
|
||||
| PATCH | `/api/list-items/:itemId` | Session | Update a single list item field |
|
||||
| DELETE | `/api/list-items/:itemId` | Session | Delete a list item |
|
||||
| GET | `/api/sse/heartbeat` | Session | SSE heartbeat stream |
|
||||
| GET | `/api/sse/lists` | Session | Scoped live-list SSE stream |
|
||||
| GET | `/api/push/vapid-public-key` | Session | VAPID public key for push subscribe |
|
||||
| POST | `/api/push/subscription` | Session | Register a push subscription |
|
||||
| DELETE | `/api/push/subscription` | Session | Remove push subscriptions for caller |
|
||||
| GET | `/api/admin/members` | Admin | List members with credential status |
|
||||
| POST | `/api/admin/members` | Admin | Create a new local member |
|
||||
| POST | `/api/admin/members/:id/password` | Admin | Reset a local member's password |
|
||||
| POST | `/api/admin/credentials` | Admin | Validate and store a member's Fastmail credential |
|
||||
| GET | `/api/admin/calendars` | Admin | List synced calendars |
|
||||
| PUT | `/api/admin/calendars/:id/shared` | Admin | Designate the shared family calendar |
|
||||
| GET | `/api/admin/config/timezone` | Admin | Get household timezone |
|
||||
| PUT | `/api/admin/config/timezone` | Admin | Set household timezone |
|
||||
| POST | `/api/admin/config/timezone/seed` | Admin | Seed household timezone if not yet set |
|
||||
|
||||
---
|
||||
|
||||
@@ -129,12 +129,12 @@ Stores non-secret OIDC and VAPID configuration into `app_config`. Returns `423`
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Required | Constraints |
|
||||
| ---------------- | ------ | -------- | ------------------------------ |
|
||||
| `oidcIssuer` | string | Yes | HTTPS URL |
|
||||
| `oidcClientId` | string | Yes | 1–256 characters |
|
||||
| `vapidPublicKey` | string | Yes | 1–512 characters |
|
||||
| `appExternalUrl` | string | Yes | HTTPS URL, max 512 characters |
|
||||
| Field | Type | Required | Constraints |
|
||||
| ---------------- | ------ | -------- | ----------------------------- |
|
||||
| `oidcIssuer` | string | Yes | HTTPS URL |
|
||||
| `oidcClientId` | string | Yes | 1–256 characters |
|
||||
| `vapidPublicKey` | string | Yes | 1–512 characters |
|
||||
| `appExternalUrl` | string | Yes | HTTPS URL, max 512 characters |
|
||||
|
||||
**Response 200**
|
||||
|
||||
@@ -187,10 +187,10 @@ 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 |
|
||||
| `appPassword` | string | Yes | 1–500 characters |
|
||||
| `appPassword` | string | Yes | 1–500 characters |
|
||||
|
||||
Zod validation errors for this route never echo received values (the app password is never included in error responses).
|
||||
|
||||
@@ -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.
|
||||
|
||||
Rate limiting is per-username (not per-IP):
|
||||
|
||||
- 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)
|
||||
|
||||
@@ -244,10 +245,10 @@ Timing-oracle defense: `verifyPassword` (scrypt) is always called, even for unkn
|
||||
{ "username": "alice", "password": "hunter2" }
|
||||
```
|
||||
|
||||
| Field | Type | Required | Constraints |
|
||||
| ---------- | ------ | -------- | ---------------- |
|
||||
| Field | Type | Required | Constraints |
|
||||
| ---------- | ------ | -------- | -------------------------- |
|
||||
| `username` | string | Yes | 1–128 characters (trimmed) |
|
||||
| `password` | string | Yes | 1–1000 characters |
|
||||
| `password` | string | Yes | 1–1000 characters |
|
||||
|
||||
Zod validation errors never echo received values.
|
||||
|
||||
@@ -296,14 +297,14 @@ Display name is derived from OIDC claims in priority order: `name` → `preferre
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
| -------------------- | ------- | ---------------------------------------------------------------- |
|
||||
| `id` | integer | Stable member ID |
|
||||
| `displayName` | string | Derived from OIDC claims or set by admin |
|
||||
| `color` | string | Member's assigned color (hex) |
|
||||
| `isAdmin` | boolean | Whether the member has the admin role |
|
||||
| `needsProviderSetup` | boolean | True when no Fastmail credential is stored for this member |
|
||||
| `hasLocalCredential` | boolean | True when a local username/password credential exists |
|
||||
| Field | Type | Description |
|
||||
| -------------------- | ------- | ---------------------------------------------------------- |
|
||||
| `id` | integer | Stable member ID |
|
||||
| `displayName` | string | Derived from OIDC claims or set by admin |
|
||||
| `color` | string | Member's assigned color (hex) |
|
||||
| `isAdmin` | boolean | Whether the member has the admin role |
|
||||
| `needsProviderSetup` | boolean | True when no Fastmail credential is stored for this member |
|
||||
| `hasLocalCredential` | boolean | True when a local username/password credential exists |
|
||||
|
||||
**Error responses:** `401` if the session is invalid.
|
||||
|
||||
@@ -323,11 +324,11 @@ Member self-service endpoint to set or rotate their own Fastmail CalDAV app pass
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Required | Constraints |
|
||||
| --------------- | ------ | -------- | ---------------- |
|
||||
| `providerType` | string | Yes | Must be `"caldav"` |
|
||||
| Field | Type | Required | Constraints |
|
||||
| --------------- | ------ | -------- | ------------------------------- |
|
||||
| `providerType` | string | Yes | Must be `"caldav"` |
|
||||
| `fastmailEmail` | string | Yes | Valid email, max 256 characters |
|
||||
| `appPassword` | string | Yes | 1–500 characters |
|
||||
| `appPassword` | string | Yes | 1–500 characters |
|
||||
|
||||
**Response 200** — `{ "ok": true }`
|
||||
|
||||
@@ -348,9 +349,9 @@ Self-service password change for local-auth members. Requires the current passwo
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Required | Constraints |
|
||||
| ----------------- | ------ | -------- | ---------------- |
|
||||
| `currentPassword` | string | Yes | 1+ characters |
|
||||
| Field | Type | Required | Constraints |
|
||||
| ----------------- | ------ | -------- | -------------------- |
|
||||
| `currentPassword` | string | Yes | 1+ characters |
|
||||
| `newPassword` | string | Yes | Minimum 8 characters |
|
||||
|
||||
**Response 200** — `{ "ok": true }`
|
||||
@@ -942,10 +943,10 @@ Creates a new local-auth member: inserts a `users` row and a `local_credentials`
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Required | Constraints |
|
||||
| ----------------- | ------ | -------- | ---------------- |
|
||||
| `displayName` | string | Yes | 1–256 characters |
|
||||
| `username` | string | Yes | 1–128 characters |
|
||||
| Field | Type | Required | Constraints |
|
||||
| ----------------- | ------ | -------- | -------------------- |
|
||||
| `displayName` | string | Yes | 1–256 characters |
|
||||
| `username` | string | Yes | 1–128 characters |
|
||||
| `initialPassword` | string | Yes | Minimum 8 characters |
|
||||
|
||||
Zod validation errors never echo received values (the initial password is never included in error responses).
|
||||
@@ -997,12 +998,12 @@ Validates and stores a Fastmail CalDAV app password for any household member. Pe
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Required | Constraints |
|
||||
| --------------- | ------- | -------- | ---------------- |
|
||||
| `userId` | integer | Yes | Positive integer |
|
||||
| `providerType` | string | Yes | Must be `"caldav"` |
|
||||
| Field | Type | Required | Constraints |
|
||||
| --------------- | ------- | -------- | ------------------------------- |
|
||||
| `userId` | integer | Yes | Positive integer |
|
||||
| `providerType` | string | Yes | Must be `"caldav"` |
|
||||
| `fastmailEmail` | string | Yes | Valid email, max 256 characters |
|
||||
| `appPassword` | string | Yes | 1–500 characters |
|
||||
| `appPassword` | string | Yes | 1–500 characters |
|
||||
|
||||
Zod validation errors and CalDAV validation failures return `400` with `{ "error": "Invalid request" }` — the app password is never echoed.
|
||||
|
||||
@@ -1022,7 +1023,7 @@ Lists all synced calendars with their shared-calendar designation.
|
||||
{
|
||||
"calendars": [
|
||||
{ "id": 1, "displayName": "Personal", "isShared": false },
|
||||
{ "id": 2, "displayName": "Family", "isShared": true }
|
||||
{ "id": 2, "displayName": "Family", "isShared": true }
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -1065,8 +1066,8 @@ Validates and upserts the household IANA timezone into `app_config`.
|
||||
{ "timezone": "America/Toronto" }
|
||||
```
|
||||
|
||||
| Field | Type | Required | Constraints |
|
||||
| ---------- | ------ | -------- | -------------------------- |
|
||||
| Field | Type | Required | Constraints |
|
||||
| ---------- | ------ | -------- | ------------------------------------ |
|
||||
| `timezone` | string | Yes | Valid IANA timezone, 1–64 characters |
|
||||
|
||||
**Response 200** — `{ "ok": true }`
|
||||
@@ -1105,17 +1106,17 @@ All error responses use a consistent JSON envelope.
|
||||
{ "error": "Human-readable message" }
|
||||
```
|
||||
|
||||
| HTTP Status | Meaning |
|
||||
| ----------- | ------------------------------------------------------------------------------ |
|
||||
| `400` | Invalid request parameters (e.g., malformed date window) |
|
||||
| `401` | Session missing or invalid |
|
||||
| HTTP Status | Meaning |
|
||||
| ----------- | -------------------------------------------------------------------------------------------------------- |
|
||||
| `400` | Invalid request parameters (e.g., malformed date window) |
|
||||
| `401` | Session missing or invalid |
|
||||
| `403` | Authenticated but not authorized (wrong owner, sharee attempted owner-only op, non-admin on admin route) |
|
||||
| `404` | Resource not found |
|
||||
| `409` | Conflict (e.g., duplicate username) |
|
||||
| `422` | Valid request but cannot be fulfilled (e.g., user has no calendar configured) |
|
||||
| `423` | Locked (setup already complete, or account locked after too many failed logins)|
|
||||
| `429` | Too many requests (login rate limit exceeded for this username) |
|
||||
| `503` | DB or downstream service unavailable |
|
||||
| `404` | Resource not found |
|
||||
| `409` | Conflict (e.g., duplicate username) |
|
||||
| `422` | Valid request but cannot be fulfilled (e.g., user has no calendar configured) |
|
||||
| `423` | Locked (setup already complete, or account locked after too many failed logins) |
|
||||
| `429` | Too many requests (login rate limit exceeded for this username) |
|
||||
| `503` | DB or downstream service unavailable |
|
||||
|
||||
Zod validation failures return `400` with a structured body from `@hono/zod-validator` rather than the `{ "error": "..." }` envelope. Exception: credential and password routes use a `noEchoHook` that always returns `{ "error": "Invalid request" }` to prevent echoing submitted secrets in error details.
|
||||
|
||||
|
||||
+45
-45
@@ -88,40 +88,40 @@ familysync/
|
||||
|
||||
### Directory Rationale
|
||||
|
||||
| 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/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) |
|
||||
| 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/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/db/` | `schema.ts` (Drizzle `mysqlTable` definitions), `client.ts` (mysql2 pool), `migrations/` (drizzle-kit output) |
|
||||
| `apps/api/src/lib/` | Stateless helpers: `listEmitter.ts` (EventEmitter fan-out), `listChangeDispatcher.ts`, `eventChangeDispatcher.ts`, `pushDispatcher.ts` (VAPID send), `pushCoalescer.ts`, `listAccess.ts`, `rank.ts` (fractional indexing), `bootGuards.ts` (startup safety assertions), `requireAdmin.ts` (admin-role guard), `setupGuard.ts` (isSetupLocked check) |
|
||||
| `apps/pwa/src/api/` | Thin typed fetch wrappers — `client.ts` (events, me, sync-status, auth-mode, local login/logout), `listsClient.ts` (lists and items) |
|
||||
| `apps/pwa/src/store/` | `calendarStore.ts` and `listsStore.ts` — Zustand UI-only state (no server data) |
|
||||
| `apps/pwa/src/hooks/` | `useListSSE.ts` (bounded-backoff EventSource), `usePushSubscription.ts` (VAPID subscribe) |
|
||||
| `apps/api/src/db/` | `schema.ts` (Drizzle `mysqlTable` definitions), `client.ts` (mysql2 pool), `migrations/` (drizzle-kit output) |
|
||||
| `apps/api/src/lib/` | Stateless helpers: `listEmitter.ts` (EventEmitter fan-out), `listChangeDispatcher.ts`, `eventChangeDispatcher.ts`, `pushDispatcher.ts` (VAPID send), `pushCoalescer.ts`, `listAccess.ts`, `rank.ts` (fractional indexing), `bootGuards.ts` (startup safety assertions), `requireAdmin.ts` (admin-role guard), `setupGuard.ts` (isSetupLocked check) |
|
||||
| `apps/pwa/src/api/` | Thin typed fetch wrappers — `client.ts` (events, me, sync-status, auth-mode, local login/logout), `listsClient.ts` (lists and items) |
|
||||
| `apps/pwa/src/store/` | `calendarStore.ts` and `listsStore.ts` — Zustand UI-only state (no server data) |
|
||||
| `apps/pwa/src/hooks/` | `useListSSE.ts` (bounded-backoff EventSource), `usePushSubscription.ts` (VAPID subscribe) |
|
||||
|
||||
---
|
||||
|
||||
## Key Abstractions
|
||||
|
||||
| Abstraction | File | Description |
|
||||
| ------------------------------------------ | ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `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`) |
|
||||
| `syncCalendar` | `apps/api/src/broker/sync.ts` | REPORT → ical.js parse → `onDuplicateKeyUpdate` upsert into MariaDB |
|
||||
| `expandOccurrences` | `apps/api/src/broker/expand.ts` | Server-side RRULE expansion using `ical.js` + `rrule`; never runs in the browser |
|
||||
| `CalendarOccurrence` | `apps/api/src/broker/expand.ts` | Wire type for a single concrete event occurrence; mirrored in the PWA's `api/client.ts` |
|
||||
| `calendarOutbox` table | `apps/api/src/db/schema.ts` | Transactional outbox pattern — CalDAV writes are enqueued here and drained asynchronously |
|
||||
| `runOutboxDrain` | `apps/api/src/broker/outboxWorker.ts` | Drains pending outbox rows every 15s; handles retry backoff, 412 conflict, dead-lettering, and edit-as-move ordering |
|
||||
| `publishListEvent` / `subscribeListEvents` | `apps/api/src/lib/listEmitter.ts` | In-process EventEmitter fan-out keyed per list; SSE route subscribes on open and unsubscribes on disconnect |
|
||||
| `dispatchPush` | `apps/api/src/lib/pushDispatcher.ts` | Centralised VAPID-signed push sender; handles 410/404 subscription pruning |
|
||||
| `issueLocalSessionCookie` / `verifyLocalSessionCookie` | `apps/api/src/auth/localSession.ts` | Issues and verifies the `local-session` JWT cookie used by local username/password auth |
|
||||
| `localAuthMiddleware` | `apps/api/src/auth/localAuthMiddleware.ts` | Reads `local-session` cookie → populates `c.get('user')`; no-op passthrough when cookie absent (OIDC guard fires for unauthenticated requests) |
|
||||
| `linkOidcToUser` / `OidcLinkConflictError` | `apps/api/src/auth/linkOidc.ts` | Binds an OIDC iss+sub to an existing local user; throws `OidcLinkConflictError` on identity collision |
|
||||
| `localCredentials` table | `apps/api/src/db/schema.ts` | Per-member local login credentials (scrypt PHC hash); a row exists iff the member can log in with username/password |
|
||||
| `appConfig` table | `apps/api/src/db/schema.ts` | Key/value store for setup wizard output (OIDC config, VAPID public key, setup_complete flag) |
|
||||
| `isSetupLocked` | `apps/api/src/lib/setupGuard.ts` | Returns true when the first-run wizard is complete; setup mutation routes call this as their first guard |
|
||||
| `SessionExpiredError` | `apps/pwa/src/api/client.ts` | Typed error thrown by all fetch wrappers on 401/opaqueredirect; global `QueryCache` handler arms the session-expiry interstitial |
|
||||
| Zustand stores | `apps/pwa/src/store/` | UI-only ephemeral state (open panels, selected date, active tab); server state always in TanStack Query |
|
||||
| Abstraction | File | Description |
|
||||
| ------------------------------------------------------ | ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `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`) |
|
||||
| `syncCalendar` | `apps/api/src/broker/sync.ts` | REPORT → ical.js parse → `onDuplicateKeyUpdate` upsert into MariaDB |
|
||||
| `expandOccurrences` | `apps/api/src/broker/expand.ts` | Server-side RRULE expansion using `ical.js` + `rrule`; never runs in the browser |
|
||||
| `CalendarOccurrence` | `apps/api/src/broker/expand.ts` | Wire type for a single concrete event occurrence; mirrored in the PWA's `api/client.ts` |
|
||||
| `calendarOutbox` table | `apps/api/src/db/schema.ts` | Transactional outbox pattern — CalDAV writes are enqueued here and drained asynchronously |
|
||||
| `runOutboxDrain` | `apps/api/src/broker/outboxWorker.ts` | Drains pending outbox rows every 15s; handles retry backoff, 412 conflict, dead-lettering, and edit-as-move ordering |
|
||||
| `publishListEvent` / `subscribeListEvents` | `apps/api/src/lib/listEmitter.ts` | In-process EventEmitter fan-out keyed per list; SSE route subscribes on open and unsubscribes on disconnect |
|
||||
| `dispatchPush` | `apps/api/src/lib/pushDispatcher.ts` | Centralised VAPID-signed push sender; handles 410/404 subscription pruning |
|
||||
| `issueLocalSessionCookie` / `verifyLocalSessionCookie` | `apps/api/src/auth/localSession.ts` | Issues and verifies the `local-session` JWT cookie used by local username/password auth |
|
||||
| `localAuthMiddleware` | `apps/api/src/auth/localAuthMiddleware.ts` | Reads `local-session` cookie → populates `c.get('user')`; no-op passthrough when cookie absent (OIDC guard fires for unauthenticated requests) |
|
||||
| `linkOidcToUser` / `OidcLinkConflictError` | `apps/api/src/auth/linkOidc.ts` | Binds an OIDC iss+sub to an existing local user; throws `OidcLinkConflictError` on identity collision |
|
||||
| `localCredentials` table | `apps/api/src/db/schema.ts` | Per-member local login credentials (scrypt PHC hash); a row exists iff the member can log in with username/password |
|
||||
| `appConfig` table | `apps/api/src/db/schema.ts` | Key/value store for setup wizard output (OIDC config, VAPID public key, setup_complete flag) |
|
||||
| `isSetupLocked` | `apps/api/src/lib/setupGuard.ts` | Returns true when the first-run wizard is complete; setup mutation routes call this as their first guard |
|
||||
| `SessionExpiredError` | `apps/pwa/src/api/client.ts` | Typed error thrown by all fetch wrappers on 401/opaqueredirect; global `QueryCache` handler arms the session-expiry interstitial |
|
||||
| Zustand stores | `apps/pwa/src/store/` | UI-only ephemeral state (open panels, selected date, active tab); server state always in TanStack Query |
|
||||
|
||||
---
|
||||
|
||||
@@ -234,21 +234,21 @@ routes/setup.ts ──→ db (app_config)
|
||||
|
||||
## Infrastructure
|
||||
|
||||
| Component | Technology |
|
||||
| -------------------- | ------------------------------------------------------------------------------------------------------------------- |
|
||||
| Runtime | Node.js 22 LTS |
|
||||
| HTTP framework | Hono 4.x (`@hono/node-server`) |
|
||||
| Database | MariaDB 11 (Docker volume) |
|
||||
| ORM | Drizzle ORM 0.45.x (`mysql2` dialect) |
|
||||
| Auth IdP | Authelia (pre-deployed, external) — OIDC authorization code + PKCE; optional when local auth is enabled |
|
||||
| Component | Technology |
|
||||
| -------------------- | --------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Runtime | Node.js 22 LTS |
|
||||
| HTTP framework | Hono 4.x (`@hono/node-server`) |
|
||||
| Database | MariaDB 11 (Docker volume) |
|
||||
| ORM | Drizzle ORM 0.45.x (`mysql2` dialect) |
|
||||
| Auth IdP | Authelia (pre-deployed, external) — OIDC authorization code + PKCE; optional when local auth is enabled |
|
||||
| Session middleware | `@hono/oidc-auth` (OIDC session — storage-less signed JWT cookies) + custom `localSession.ts` (local-auth HS256 JWT cookie) |
|
||||
| Calendar source | Fastmail CalDAV (`caldav.fastmail.com`) — read via `tsdav`, write via transactional outbox |
|
||||
| Calendar parsing | `ical.js` (VCALENDAR/VEVENT parse) + `rrule` (RRULE expansion) |
|
||||
| App password storage | AES-256-GCM encrypted in `member_credentials.encrypted_password` |
|
||||
| Local auth storage | scrypt PHC hash in `local_credentials.password_hash`; session signed with `LOCAL_SESSION_SECRET` env var |
|
||||
| Push notifications | `web-push` (VAPID) → APNs (iOS) / FCM (Android) |
|
||||
| Live list sync | In-process Node.js `EventEmitter` → SSE (`text/event-stream`) |
|
||||
| Redis | Present in stack (image: `redis:7-alpine`); not used in current runtime (reserved for future multi-process pub/sub) |
|
||||
| PWA | React 19 + Vite 8 + `vite-plugin-pwa` (Workbox `injectManifest` mode) |
|
||||
| Networking | Pangolin/Newt tunnel — no open ports; split-DNS internal domain |
|
||||
| Deployment | Docker Compose on Unraid; single `api` container serves both the API and the PWA static build |
|
||||
| Calendar source | Fastmail CalDAV (`caldav.fastmail.com`) — read via `tsdav`, write via transactional outbox |
|
||||
| Calendar parsing | `ical.js` (VCALENDAR/VEVENT parse) + `rrule` (RRULE expansion) |
|
||||
| App password storage | AES-256-GCM encrypted in `member_credentials.encrypted_password` |
|
||||
| Local auth storage | scrypt PHC hash in `local_credentials.password_hash`; session signed with `LOCAL_SESSION_SECRET` env var |
|
||||
| Push notifications | `web-push` (VAPID) → APNs (iOS) / FCM (Android) |
|
||||
| Live list sync | In-process Node.js `EventEmitter` → SSE (`text/event-stream`) |
|
||||
| Redis | Present in stack (image: `redis:7-alpine`); not used in current runtime (reserved for future multi-process pub/sub) |
|
||||
| PWA | React 19 + Vite 8 + `vite-plugin-pwa` (Workbox `injectManifest` mode) |
|
||||
| Networking | Pangolin/Newt tunnel — no open ports; split-DNS internal domain |
|
||||
| Deployment | Docker Compose on Unraid; single `api` container serves both the API and the PWA static build |
|
||||
|
||||
+20
-20
@@ -10,14 +10,14 @@ All runtime configuration is supplied via environment variables. There are no JS
|
||||
|
||||
### Database
|
||||
|
||||
| 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_PORT` | No | `3306` | MariaDB port. |
|
||||
| `DB_USER` | No | `familysync` | Database user. |
|
||||
| `DB_PASSWORD` | **Required** | _(none)_ | Database password. Also used by the `mariadb` service as `MARIADB_PASSWORD`. |
|
||||
| `DB_NAME` | No | `familysync` | Database name. |
|
||||
| `DB_ROOT_PASSWORD` | **Required** | _(none)_ | MariaDB root password. Used only by the `mariadb` Docker service (`MARIADB_ROOT_PASSWORD`). Not read by the API process. |
|
||||
| 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_PORT` | No | `3306` | MariaDB port. |
|
||||
| `DB_USER` | No | `familysync` | Database user. |
|
||||
| `DB_PASSWORD` | **Required** | _(none)_ | Database password. Also used by the `mariadb` service as `MARIADB_PASSWORD`. |
|
||||
| `DB_NAME` | No | `familysync` | Database name. |
|
||||
| `DB_ROOT_PASSWORD` | **Required** | _(none)_ | MariaDB root password. Used only by the `mariadb` Docker service (`MARIADB_ROOT_PASSWORD`). Not read by the API process. |
|
||||
| `DB_ROOT_USER` | No | `root` | MariaDB root username. Read only by `apps/api/test/global-setup.ts` during local test provisioning. Never used by the API or Docker Compose in production. |
|
||||
|
||||
Five of these variables — `DB_HOST`, `DB_USER`, `DB_PASSWORD`, `DB_NAME`, `DB_PORT` — are read by `drizzle.config.ts` when running migrations (`db:generate` / `db:migrate`) and by the API process to build its connection pool. `DB_ROOT_PASSWORD` is **not** read by either; it is consumed only by the `mariadb` Docker service. `DB_ROOT_USER` is only used by the local Vitest global setup to create and grant the `familysync_test` database.
|
||||
@@ -55,10 +55,10 @@ 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`.
|
||||
|
||||
| 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_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`. |
|
||||
|
||||
**Security note:** `LOCAL_SESSION_SECRET` must be a distinct value from `OIDC_AUTH_SECRET`. Both are JWT signing keys, but they govern different cookies and must not be shared.
|
||||
|
||||
@@ -90,10 +90,10 @@ npx web-push generate-vapid-keys --json
|
||||
|
||||
### Runtime Mode
|
||||
|
||||
| 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`. |
|
||||
| `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. |
|
||||
| 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`. |
|
||||
| `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`. |
|
||||
|
||||
---
|
||||
@@ -102,11 +102,11 @@ npx web-push generate-vapid-keys --json
|
||||
|
||||
These variables are never needed in production and should not appear in the production `.env`.
|
||||
|
||||
| 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_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`. |
|
||||
| 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_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`. |
|
||||
|
||||
---
|
||||
|
||||
|
||||
+23
-23
@@ -100,19 +100,19 @@ Vite serves the PWA with HMR on the configured dev port. The PWA's API calls tar
|
||||
|
||||
### Root workspace scripts
|
||||
|
||||
| Command | Description |
|
||||
| ------------------------ | ------------------------------------------------------------- |
|
||||
| `pnpm dev:api` | Start API dev watcher (`node --watch dist/index.js`) |
|
||||
| `pnpm dev:pwa` | Start Vite dev server for the PWA |
|
||||
| `pnpm build` | Build both `apps/api` (tsc) and `apps/pwa` (tsc + vite build) |
|
||||
| `pnpm test` | Run API test suite (`vitest run` in `apps/api`) |
|
||||
| `pnpm test:e2e` | Run Playwright e2e harness (`apps/pwa`) |
|
||||
| `pnpm lint` | ESLint across all workspaces (`pnpm -r --if-present lint`) |
|
||||
| `pnpm format` | Reformat all files with Prettier (`prettier --write .`) |
|
||||
| `pnpm format:check` | Check formatting without writing (`prettier --check .`) |
|
||||
| `pnpm typecheck` | `tsc --noEmit` in all workspaces |
|
||||
| `pnpm md:lint` | Markdown lint (`markdownlint-cli2`) across the repo |
|
||||
| `pnpm generate-secrets` | Generate VAPID and session secret values via `scripts/generate-secrets.mjs` |
|
||||
| Command | Description |
|
||||
| ----------------------- | --------------------------------------------------------------------------- |
|
||||
| `pnpm dev:api` | Start API dev watcher (`node --watch dist/index.js`) |
|
||||
| `pnpm dev:pwa` | Start Vite dev server for the PWA |
|
||||
| `pnpm build` | Build both `apps/api` (tsc) and `apps/pwa` (tsc + vite build) |
|
||||
| `pnpm test` | Run API test suite (`vitest run` in `apps/api`) |
|
||||
| `pnpm test:e2e` | Run Playwright e2e harness (`apps/pwa`) |
|
||||
| `pnpm lint` | ESLint across all workspaces (`pnpm -r --if-present lint`) |
|
||||
| `pnpm format` | Reformat all files with Prettier (`prettier --write .`) |
|
||||
| `pnpm format:check` | Check formatting without writing (`prettier --check .`) |
|
||||
| `pnpm typecheck` | `tsc --noEmit` in all workspaces |
|
||||
| `pnpm md:lint` | Markdown lint (`markdownlint-cli2`) across the repo |
|
||||
| `pnpm generate-secrets` | Generate VAPID and session secret values via `scripts/generate-secrets.mjs` |
|
||||
|
||||
### `apps/api` scripts
|
||||
|
||||
@@ -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:
|
||||
|
||||
- **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)
|
||||
- **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}`** — `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)
|
||||
- **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)
|
||||
- **Prettier integration** — `eslint-config-prettier` last in the config disables all formatting rules that conflict with Prettier
|
||||
|
||||
@@ -195,13 +195,13 @@ 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).
|
||||
|
||||
| Job | Runs on | Checks |
|
||||
| ------------- | ------------------- | ----------------------------------------------------------------------------------------------------------- |
|
||||
| `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) |
|
||||
| `harness` | Code-change PRs only | DB migrations + seed dev user + API build + Playwright e2e (WebKit + Chromium) with `DEV_AUTH_BYPASS=true` |
|
||||
| `security` | Every PR | Gitleaks secret scan (PR diff); `pnpm audit` (High+Critical blocking) + outdated report on code-change PRs |
|
||||
| `gate` | Always | Final aggregator — requires `fast-checks` and `security` to succeed; `api` and `harness` may be skipped |
|
||||
| Job | Runs on | Checks |
|
||||
| ------------- | -------------------- | ------------------------------------------------------------------------------------------------------------ |
|
||||
| `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) |
|
||||
| `harness` | Code-change PRs only | DB migrations + seed dev user + API build + Playwright e2e (WebKit + Chromium) with `DEV_AUTH_BYPASS=true` |
|
||||
| `security` | Every PR | Gitleaks secret scan (PR diff); `pnpm audit` (High+Critical blocking) + outdated report on code-change PRs |
|
||||
| `gate` | Always | Final aggregator — requires `fast-checks` and `security` to succeed; `api` and `harness` may be skipped |
|
||||
|
||||
All five jobs must pass (or be legitimately skipped) before a PR can merge. See [docs/TESTING.md](TESTING.md) for test suite details.
|
||||
|
||||
|
||||
@@ -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`**
|
||||
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
|
||||
- Run `pnpm generate-secrets` and add the generated `LOCAL_SESSION_SECRET` value to `.env`.
|
||||
|
||||
|
||||
+25
-25
@@ -6,10 +6,10 @@
|
||||
|
||||
Both apps use **Vitest** (`^4.1.8`).
|
||||
|
||||
| App | Environment | Global setup | Per-file setup |
|
||||
| ---------- | ----------- | ----------------------------------- | ---------------------------- |
|
||||
| `apps/api` | `node` | `apps/api/test/global-setup.ts` | `apps/api/test/setup.ts` |
|
||||
| `apps/pwa` | `jsdom` | — | `apps/pwa/src/test-setup.ts` |
|
||||
| App | Environment | Global setup | Per-file setup |
|
||||
| ---------- | ----------- | ------------------------------- | ---------------------------- |
|
||||
| `apps/api` | `node` | `apps/api/test/global-setup.ts` | `apps/api/test/setup.ts` |
|
||||
| `apps/pwa` | `jsdom` | — | `apps/pwa/src/test-setup.ts` |
|
||||
|
||||
**apps/api global setup** (`test/global-setup.ts`) runs once before any test file. Locally it provisions an isolated `familysync_test` database (root connection → `CREATE DATABASE IF NOT EXISTS familysync_test` → GRANT → `drizzle migrate`) and then truncates every table to give each run a clean slate. Under CI (`process.env.CI` truthy) it returns immediately — the CI `api` job provisions its own `familysync` service container via `db:migrate`.
|
||||
|
||||
@@ -53,11 +53,11 @@ 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:
|
||||
|
||||
| Profile | Viewport | Engine | User-Agent |
|
||||
| --------- | --------- | -------- | ------------------------- |
|
||||
| `iphone` | 390×844 | WebKit | Mobile Safari (iPhone 14) |
|
||||
| `pixel` | 412×915 | Chromium | Chrome Android (Pixel 7) |
|
||||
| `desktop` | 1280×720 | Chromium | Desktop Chrome |
|
||||
| Profile | Viewport | Engine | User-Agent |
|
||||
| --------- | -------- | -------- | ------------------------- |
|
||||
| `iphone` | 390×844 | WebKit | Mobile Safari (iPhone 14) |
|
||||
| `pixel` | 412×915 | Chromium | Chrome Android (Pixel 7) |
|
||||
| `desktop` | 1280×720 | Chromium | Desktop Chrome |
|
||||
|
||||
All profiles block the service worker (`serviceWorkers: 'block'`) so the Workbox SW does not intercept requests during tests. Auth is handled via `DEV_AUTH_BYPASS=true` on the API — never via stored browser state.
|
||||
|
||||
@@ -232,17 +232,17 @@ 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.
|
||||
|
||||
| Step | Detail |
|
||||
| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| MariaDB service | Same `mariadb:11` setup as the `api` job |
|
||||
| Schema migrations | `pnpm --filter @familysync/api db:migrate` |
|
||||
| Dev user seed | Inserts `users` row id=1 (`INSERT IGNORE`) for `DEV_AUTH_BYPASS` |
|
||||
| Local credentials seed | Inserts `local_credentials` row for dev user (username: `devuser`, password: `devpass`) via inline scrypt hash — Phase 19 requirement |
|
||||
| API build | `pnpm --filter @familysync/api build` (dist/ is gitignored) |
|
||||
| Playwright install | `npx playwright install --with-deps webkit chromium` (no cache) |
|
||||
| API start + tests | API started as a background process in the same step as `playwright test` to survive the step boundary; `DEV_AUTH_BYPASS=true`, `NODE_ENV=development` |
|
||||
| Base URL | `http://127.0.0.1:5173` (not `localhost` — runner resolves `localhost` to `::1` but Vite binds IPv4-only) |
|
||||
| Artifacts on fail | Traces, screenshots, videos, and HTML report uploaded via `ChristopherHX/gitea-upload-artifact@v4` (standard `upload-artifact` aborts on Gitea) |
|
||||
| Step | Detail |
|
||||
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| MariaDB service | Same `mariadb:11` setup as the `api` job |
|
||||
| Schema migrations | `pnpm --filter @familysync/api db:migrate` |
|
||||
| Dev user seed | Inserts `users` row id=1 (`INSERT IGNORE`) for `DEV_AUTH_BYPASS` |
|
||||
| Local credentials seed | Inserts `local_credentials` row for dev user (username: `devuser`, password: `devpass`) via inline scrypt hash — Phase 19 requirement |
|
||||
| API build | `pnpm --filter @familysync/api build` (dist/ is gitignored) |
|
||||
| Playwright install | `npx playwright install --with-deps webkit chromium` (no cache) |
|
||||
| API start + tests | API started as a background process in the same step as `playwright test` to survive the step boundary; `DEV_AUTH_BYPASS=true`, `NODE_ENV=development` |
|
||||
| Base URL | `http://127.0.0.1:5173` (not `localhost` — runner resolves `localhost` to `::1` but Vite binds IPv4-only) |
|
||||
| Artifacts on fail | Traces, screenshots, videos, and HTML report uploaded via `ChristopherHX/gitea-upload-artifact@v4` (standard `upload-artifact` aborts on Gitea) |
|
||||
|
||||
The API process is started and the Playwright suite invoked within a single CI step. Starting the API in an earlier step causes it to be reaped at the step boundary before Playwright runs.
|
||||
|
||||
@@ -250,11 +250,11 @@ 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.
|
||||
|
||||
| Step | Tool/Command | Detail |
|
||||
| ------------------- | ------------------------------- | -------------------------------------------------------------- |
|
||||
| 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 |
|
||||
| Outdated report | `node scripts/check-outdated.mjs` | Advisory only — always exits 0, logged but never gates |
|
||||
| Step | Tool/Command | Detail |
|
||||
| ---------------- | --------------------------------- | ------------------------------------------------------ |
|
||||
| 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 |
|
||||
| Outdated report | `node scripts/check-outdated.mjs` | Advisory only — always exits 0, logged but never gates |
|
||||
|
||||
### `gate`
|
||||
|
||||
|
||||
+8
-7
@@ -32,13 +32,13 @@ 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:
|
||||
|
||||
| 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 |
|
||||
| `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 |
|
||||
| `security` | Always | Secret scan (gitleaks, PR diff); dependency audit and outdated report on code-changing PRs |
|
||||
| `gate` | Always | Aggregates results — fails if any non-skipped required job did not succeed |
|
||||
| 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 |
|
||||
| `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 |
|
||||
| `security` | Always | Secret scan (gitleaks, PR diff); dependency audit and outdated report on code-changing PRs |
|
||||
| `gate` | Always | Aggregates results — fails if any non-skipped required job did not succeed |
|
||||
|
||||
The `api` and `harness` jobs are **skipped on doc-only PRs** (changes confined to `.gitea/**`, `.planning/**`, or `*.md` files). A doc-only PR must pass `fast-checks` and `security`; the heavy jobs are not required.
|
||||
|
||||
@@ -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.
|
||||
|
||||
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`.
|
||||
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