Implement auth helpers, code generation, and request hooks #3

Open
opened 2026-05-05 10:56:54 +02:00 by marcel · 8 comments
Owner

Task 3 — Plan reference: docs/superpowers/plans/2026-05-05-erbstuecke-wannsee.md

User story: As a developer, I need auth helper functions and SvelteKit hooks so that every request has locals.familyCode and locals.admin populated from cookies, and cookie set/clear helpers are available to all routes.

Acceptance criteria

  • src/lib/auth.ts exports:
    • verifyAdminPassword(name, password) — async, reads bcrypt hash from ADMIN_{NAME}_PASSWORD_HASH env var, returns false for unknown names (no user enumeration)
    • validateFamilyCode(code) — synchronous DB lookup, returns { id, displayName } or null
    • generateCode() — returns 8-character string from [A-Z2-9] using crypto.randomBytes (NFR-SEC-001)
    • setFamilyCodeCookie, setAdminCookie, clearFamilyCodeCookie, clearAdminCookie — all httpOnly, sameSite: 'lax'
  • src/hooks.server.ts populates locals.familyCode and locals.admin on every request
  • Hook redirects GET //galerie if locals.familyCode is valid
  • Hook redirects GET /admin/admin/inventar if locals.admin is set
  • 7 Vitest tests pass (correct/wrong/unknown password, missing hash env var, invalid/valid code lookup, code entropy)

Files to create

  • src/lib/auth.ts
  • src/lib/auth.test.ts
  • src/hooks.server.ts

NFR

  • NFR-SEC-001: generateCode() uses crypto.randomBytes — not Math.random()
  • NFR-SEC-002: bcrypt work factor 12 for production hashes
  • NFR-SEC-004: Admin cookie and family code cookie are separate; hooks keep them strictly separate

Depends on: #2 | Size: S | Spec: system-design §4

## Task 3 — Plan reference: `docs/superpowers/plans/2026-05-05-erbstuecke-wannsee.md` **User story:** As a developer, I need auth helper functions and SvelteKit hooks so that every request has `locals.familyCode` and `locals.admin` populated from cookies, and cookie set/clear helpers are available to all routes. ### Acceptance criteria - [ ] `src/lib/auth.ts` exports: - `verifyAdminPassword(name, password)` — async, reads bcrypt hash from `ADMIN_{NAME}_PASSWORD_HASH` env var, returns `false` for unknown names (no user enumeration) - `validateFamilyCode(code)` — synchronous DB lookup, returns `{ id, displayName }` or `null` - `generateCode()` — returns 8-character string from `[A-Z2-9]` using `crypto.randomBytes` (NFR-SEC-001) - `setFamilyCodeCookie`, `setAdminCookie`, `clearFamilyCodeCookie`, `clearAdminCookie` — all `httpOnly`, `sameSite: 'lax'` - [ ] `src/hooks.server.ts` populates `locals.familyCode` and `locals.admin` on every request - [ ] Hook redirects `GET /` → `/galerie` if `locals.familyCode` is valid - [ ] Hook redirects `GET /admin` → `/admin/inventar` if `locals.admin` is set - [ ] 7 Vitest tests pass (correct/wrong/unknown password, missing hash env var, invalid/valid code lookup, code entropy) ### Files to create - `src/lib/auth.ts` - `src/lib/auth.test.ts` - `src/hooks.server.ts` ### NFR - NFR-SEC-001: `generateCode()` uses `crypto.randomBytes` — not `Math.random()` - NFR-SEC-002: bcrypt work factor 12 for production hashes - NFR-SEC-004: Admin cookie and family code cookie are separate; hooks keep them strictly separate **Depends on:** #2 | **Size:** S | **Spec:** system-design §4
marcel added this to the v1.0 — MVP milestone 2026-05-05 10:56:54 +02:00
Author
Owner

👤 Markus Keller — Application Architect

Observations

  • Cookie separation is partially correct, but admin_session validation is missing. The plan's hooks.server.ts sets event.locals.admin = event.cookies.get('admin_session') ?? null directly. This means any string a user manufactures in that cookie becomes locals.admin. There is no whitelist check against ADMIN_NAMES. The system design spec (§4) explicitly calls out: "Unbekannter Benutzername gibt dieselbe Fehlermeldung wie falsches Passwort" — the same discipline must apply in the hook.
  • The verifyAdminPassword early-return creates a timing oracle. The plan's implementation returns false immediately when the name is not in ADMIN_NAMES, before ever calling bcrypt.compare. An attacker can distinguish unknown names from wrong passwords purely by response latency. The plan's own auth.ts fixes this for the DB path, but the guard clause for unknown names reintroduces it.
  • sameSite: 'lax' is used in the plan, but the security persona spec says 'strict'. The system design doc §4 specifies httpOnly Same-Site-Cookie. The plan implements sameSite: 'lax' for both cookies. For a family app served on a subdomain without cross-site link-click requirements, 'strict' is the safer default and matches the security spec.
  • Redirect uses 302 instead of 303. The hook redirects with redirect(302, ...) for the GET / and GET /admin cases. The pattern throughout the rest of the plan (Form Actions) uses 303 See Other, which is the correct SvelteKit idiom for post-redirect patterns. GET-to-GET redirects can stay as 302, but using 303 consistently avoids accidental method-preservation bugs if the route is ever reached via a form submission.
  • locals.admin accepts raw cookie value — not validated against known admin names. If the admin cookie is present but contains an arbitrary string (e.g. an old/corrupted cookie value), locals.admin will be truthy and the admin layout guard will pass. The hook must check ADMIN_NAMES.includes(adminSessionValue) before setting locals.admin.
  • familyCode field code in App.Locals is populated from the raw cookie, which is correct. The plan correctly stores the raw code string alongside id and displayName so the hook can reconstruct the full struct. This is clean.

Recommendations

  • Add an ADMIN_NAMES whitelist check in hooks.server.ts before setting locals.admin:
    const ADMIN_NAMES = ['Marcel', 'Renate', 'Berit'] as const
    const adminValue = event.cookies.get('admin_session')
    event.locals.admin = adminValue && (ADMIN_NAMES as readonly string[]).includes(adminValue)
      ? adminValue
      : null
    
  • In verifyAdminPassword, perform a dummy bcrypt.compare even for unknown names to eliminate the timing channel:
    const DUMMY_HASH = '$2b$12$invalidhashpadding000000000000000000000000000000000000000'
    const hash = process.env[`ADMIN_${name.toUpperCase()}_PASSWORD_HASH`] ?? DUMMY_HASH
    const valid = (ADMIN_NAMES as readonly string[]).includes(name) && await bcrypt.compare(password, hash)
    return valid
    
  • Change sameSite: 'lax' to sameSite: 'strict' in both setFamilyCodeCookie and setAdminCookie to match the security spec.
  • Export ADMIN_NAMES from auth.ts and import it in hooks.server.ts to avoid duplication.

Open Decisions (omit if none)

  • sameSite: 'lax' vs 'strict' — The plan uses 'lax'; the security persona spec and Nora's guidelines say 'strict'. 'strict' breaks the ?code= link-click flow if the link is shared from an external app (WhatsApp, email) — the cookie won't be sent on the first navigation, so the redirect from gate to /galerie would fail on the first tap. If family members share invite links via messaging apps, 'lax' may be required for the gate-screen ?code= auto-login to work. (Raised by: Markus Keller)
## 👤 Markus Keller — Application Architect ### Observations - **Cookie separation is partially correct, but `admin_session` validation is missing.** The plan's `hooks.server.ts` sets `event.locals.admin = event.cookies.get('admin_session') ?? null` directly. This means any string a user manufactures in that cookie becomes `locals.admin`. There is no whitelist check against `ADMIN_NAMES`. The system design spec (§4) explicitly calls out: "Unbekannter Benutzername gibt dieselbe Fehlermeldung wie falsches Passwort" — the same discipline must apply in the hook. - **The `verifyAdminPassword` early-return creates a timing oracle.** The plan's implementation returns `false` immediately when the name is not in `ADMIN_NAMES`, before ever calling `bcrypt.compare`. An attacker can distinguish unknown names from wrong passwords purely by response latency. The plan's own `auth.ts` fixes this for the DB path, but the guard clause for unknown names reintroduces it. - **`sameSite: 'lax'` is used in the plan, but the security persona spec says `'strict'`.** The system design doc §4 specifies `httpOnly Same-Site-Cookie`. The plan implements `sameSite: 'lax'` for both cookies. For a family app served on a subdomain without cross-site link-click requirements, `'strict'` is the safer default and matches the security spec. - **Redirect uses `302` instead of `303`.** The hook redirects with `redirect(302, ...)` for the `GET /` and `GET /admin` cases. The pattern throughout the rest of the plan (Form Actions) uses `303 See Other`, which is the correct SvelteKit idiom for post-redirect patterns. GET-to-GET redirects can stay as `302`, but using `303` consistently avoids accidental method-preservation bugs if the route is ever reached via a form submission. - **`locals.admin` accepts raw cookie value — not validated against known admin names.** If the admin cookie is present but contains an arbitrary string (e.g. an old/corrupted cookie value), `locals.admin` will be truthy and the admin layout guard will pass. The hook must check `ADMIN_NAMES.includes(adminSessionValue)` before setting `locals.admin`. - **`familyCode` field `code` in `App.Locals` is populated from the raw cookie, which is correct.** The plan correctly stores the raw code string alongside `id` and `displayName` so the hook can reconstruct the full struct. This is clean. ### Recommendations - Add an `ADMIN_NAMES` whitelist check in `hooks.server.ts` before setting `locals.admin`: ```typescript const ADMIN_NAMES = ['Marcel', 'Renate', 'Berit'] as const const adminValue = event.cookies.get('admin_session') event.locals.admin = adminValue && (ADMIN_NAMES as readonly string[]).includes(adminValue) ? adminValue : null ``` - In `verifyAdminPassword`, perform a dummy `bcrypt.compare` even for unknown names to eliminate the timing channel: ```typescript const DUMMY_HASH = '$2b$12$invalidhashpadding000000000000000000000000000000000000000' const hash = process.env[`ADMIN_${name.toUpperCase()}_PASSWORD_HASH`] ?? DUMMY_HASH const valid = (ADMIN_NAMES as readonly string[]).includes(name) && await bcrypt.compare(password, hash) return valid ``` - Change `sameSite: 'lax'` to `sameSite: 'strict'` in both `setFamilyCodeCookie` and `setAdminCookie` to match the security spec. - Export `ADMIN_NAMES` from `auth.ts` and import it in `hooks.server.ts` to avoid duplication. ### Open Decisions _(omit if none)_ - **`sameSite: 'lax'` vs `'strict'`** — The plan uses `'lax'`; the security persona spec and Nora's guidelines say `'strict'`. `'strict'` breaks the `?code=` link-click flow if the link is shared from an external app (WhatsApp, email) — the cookie won't be sent on the first navigation, so the redirect from gate to `/galerie` would fail on the first tap. If family members share invite links via messaging apps, `'lax'` may be required for the gate-screen `?code=` auto-login to work. _(Raised by: Markus Keller)_
Author
Owner

👤 Felix Brandt — Fullstack Developer

Observations

  • TDD structure is sound. The plan correctly prescribes writing the failing test first (Step 1), verifying it fails (Step 2), implementing (Step 3), then verifying it passes (Step 4). This is exactly the red-green cycle.
  • validateFamilyCode test relies on getDb() directly. The test calls getDb().prepare(...).run() to seed data. This is correct — it uses the same :memory: singleton that validateFamilyCode will call internally via getDb(). The _resetForTesting() in beforeEach ensures a fresh DB per test.
  • generateCode test regex is too broad. The plan's test asserts /^[A-Z0-9]{8}$/ but the implementation's charset CODE_CHARS = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789' deliberately excludes I, O, 0, and 1 to avoid visual confusion. The test should reflect the actual intended charset, not a superset. A code containing I, O, 0, or 1 would pass the test but violate the NFR-SEC-001 intent (see security spec comment in persona files).
  • No test for the entropy/uniqueness property of generateCode beyond collision avoidance. The plan tests that 20 calls produce >15 unique values. This is weak — it could pass even with a bad PRNG. The security spec calls for 32^8 ≈ 10^12 combinations; a charset-length test would be more meaningful.
  • Cookie helpers are untested. setFamilyCodeCookie, setAdminCookie, clearFamilyCodeCookie, clearAdminCookie have no unit tests in the plan. The acceptance criteria list them as required exports; they should have at least a smoke test verifying they call cookies.set / cookies.delete with the correct options.
  • hooks.server.ts has no tests in the plan. The hook's redirect logic (GET //galerie, GET /admin/admin/inventar) and the locals population are untested. These are critical behaviors. SvelteKit hooks can be tested by importing handle and invoking it with a mock event.
  • Missing test: hook clears locals.familyCode when cookie exists but code is invalid. If an old/expired code cookie is present, validateFamilyCode returns null and locals.familyCode must be null. The plan's implementation handles this correctly, but there is no test asserting it.
  • display_namedisplayName mapping is done inline in validateFamilyCode. This is clean and correct — the snake_case DB column is camelCased at the boundary.

Recommendations

  • Tighten the generateCode test regex to match the actual charset (no I, O, 0, 1):
    expect(code).toMatch(/^[ABCDEFGHJKLMNPQRSTUVWXYZ23456789]{8}$/)
    
  • Add a test for the stale-cookie case in hooks:
    it('sets locals.familyCode to null when cookie value is not in the database', () => {
      // seed no codes, set cookie to 'BADVALUE1'
      // invoke handle — expect event.locals.familyCode === null
    })
    
  • Add minimal tests for the cookie helpers using a mock Cookies object:
    it('setFamilyCodeCookie sets httpOnly cookie with 1-year maxAge', () => {
      const set = vi.fn()
      setFamilyCodeCookie({ set } as any, 'AB3K7MN2')
      expect(set).toHaveBeenCalledWith('family_code', 'AB3K7MN2', expect.objectContaining({ httpOnly: true }))
    })
    
  • Export ADMIN_NAMES from auth.ts so hooks.server.ts can import rather than redeclare it.
## 👤 Felix Brandt — Fullstack Developer ### Observations - **TDD structure is sound.** The plan correctly prescribes writing the failing test first (Step 1), verifying it fails (Step 2), implementing (Step 3), then verifying it passes (Step 4). This is exactly the red-green cycle. - **`validateFamilyCode` test relies on `getDb()` directly.** The test calls `getDb().prepare(...).run()` to seed data. This is correct — it uses the same `:memory:` singleton that `validateFamilyCode` will call internally via `getDb()`. The `_resetForTesting()` in `beforeEach` ensures a fresh DB per test. - **`generateCode` test regex is too broad.** The plan's test asserts `/^[A-Z0-9]{8}$/` but the implementation's charset `CODE_CHARS = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'` deliberately excludes `I`, `O`, `0`, and `1` to avoid visual confusion. The test should reflect the actual intended charset, not a superset. A code containing `I`, `O`, `0`, or `1` would pass the test but violate the NFR-SEC-001 intent (see security spec comment in persona files). - **No test for the entropy/uniqueness property of `generateCode` beyond collision avoidance.** The plan tests that 20 calls produce >15 unique values. This is weak — it could pass even with a bad PRNG. The security spec calls for 32^8 ≈ 10^12 combinations; a charset-length test would be more meaningful. - **Cookie helpers are untested.** `setFamilyCodeCookie`, `setAdminCookie`, `clearFamilyCodeCookie`, `clearAdminCookie` have no unit tests in the plan. The acceptance criteria list them as required exports; they should have at least a smoke test verifying they call `cookies.set` / `cookies.delete` with the correct options. - **`hooks.server.ts` has no tests in the plan.** The hook's redirect logic (`GET /` → `/galerie`, `GET /admin` → `/admin/inventar`) and the `locals` population are untested. These are critical behaviors. SvelteKit hooks can be tested by importing `handle` and invoking it with a mock event. - **Missing test: hook clears `locals.familyCode` when cookie exists but code is invalid.** If an old/expired code cookie is present, `validateFamilyCode` returns `null` and `locals.familyCode` must be `null`. The plan's implementation handles this correctly, but there is no test asserting it. - **`display_name` → `displayName` mapping is done inline in `validateFamilyCode`.** This is clean and correct — the snake_case DB column is camelCased at the boundary. ### Recommendations - Tighten the `generateCode` test regex to match the actual charset (no `I`, `O`, `0`, `1`): ```typescript expect(code).toMatch(/^[ABCDEFGHJKLMNPQRSTUVWXYZ23456789]{8}$/) ``` - Add a test for the stale-cookie case in hooks: ```typescript it('sets locals.familyCode to null when cookie value is not in the database', () => { // seed no codes, set cookie to 'BADVALUE1' // invoke handle — expect event.locals.familyCode === null }) ``` - Add minimal tests for the cookie helpers using a mock `Cookies` object: ```typescript it('setFamilyCodeCookie sets httpOnly cookie with 1-year maxAge', () => { const set = vi.fn() setFamilyCodeCookie({ set } as any, 'AB3K7MN2') expect(set).toHaveBeenCalledWith('family_code', 'AB3K7MN2', expect.objectContaining({ httpOnly: true })) }) ``` - Export `ADMIN_NAMES` from `auth.ts` so `hooks.server.ts` can import rather than redeclare it.
Author
Owner

👤 Nora "NullX" Steiner — Application Security Engineer

Observations

  • CWE-203 Timing Oracle — verifyAdminPassword leaks username existence via early return. The plan's implementation guards with if (!(ADMIN_NAMES as readonly string[]).includes(name)) return false before calling bcrypt.compare. An attacker sending login attempts for unknown usernames vs. known-but-wrong-password usernames will observe a measurable latency difference (~0 ms vs. ~250 ms for WF12). This is a textbook user-enumeration timing channel. The system design spec (§4) explicitly prohibits user enumeration.

  • admin_session cookie value is trusted without server-side validation in hooks. The plan's hooks.server.ts sets event.locals.admin = event.cookies.get('admin_session') ?? null. Any value in that cookie — including attacker-forged strings — becomes locals.admin. The hook must validate the value against the known admin name whitelist before trusting it. This is the same mistake flagged in Nora's persona spec as the "Wrong: trusting the cookie value directly" antipattern.

  • sameSite: 'lax' instead of 'strict' on both cookies. The plan's cookie helpers use sameSite: 'lax'. The security persona spec requires 'strict' for both cookies. 'lax' permits cookies to be sent on top-level navigations from external origins (e.g., clicking a link from WhatsApp). While this has a use-case for the ?code= gate flow, it also means the admin session cookie is sent on external-link navigation — a small but unnecessary exposure for the admin session specifically. At minimum, the admin cookie must be 'strict'.

  • No secure: !dev flag on cookies. The plan's cookie helpers set neither secure: true nor the SvelteKit dev-conditional. In production (HTTPS behind Caddy), cookies without Secure can be sent over HTTP if a downgrade occurs. SvelteKit's dev import resolves this cleanly:

    import { dev } from '$app/environment'
    cookies.set('family_code', code, { ..., secure: !dev })
    
  • generateCode charset excludes confusable characters — correct. CODE_CHARS = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789' omits I, O, 0, 1. This is the right implementation per NFR-SEC-001. The 32-character alphabet over 8 positions gives 32^8 ≈ 1.1 × 10^12 combinations — brute-force infeasible at normal web request rates without rate limiting.

  • No rate limiting on the gate-screen code submission. The issue spec and plan don't mention rate limiting on the ?code= validation or the gate-screen form action. With 10^12 combinations and no rate limiting, the attack window is wide enough that a determined attacker could make meaningful progress over months. For a short-lived family app this is acceptable, but it should be a documented decision, not an oversight.

  • No SESSION_SECRET startup guard. The system design and Markus's architecture persona both require a startup crash if SESSION_SECRET is missing. This is not implemented in Task 3 (it may belong in db.ts or hooks.server.ts). It should be placed in hooks.server.ts since that is the first server-side module loaded per request.

Recommendations

  • Fix the timing oracle with a dummy hash approach:
    // CWE-203 prevention: always run bcrypt.compare to equalize response time
    const DUMMY_HASH = '$2b$12$00000000000000000000000000000000000000000000000000000000'
    export async function verifyAdminPassword(name: string, password: string): Promise<boolean> {
      const hash = process.env[`ADMIN_${name.toUpperCase()}_PASSWORD_HASH`] ?? DUMMY_HASH
      const match = await bcrypt.compare(password, hash)
      return (ADMIN_NAMES as readonly string[]).includes(name) && match
    }
    
  • Add secure: !dev to both cookie helpers. Import dev from '$app/environment'.
  • Change admin_session cookie to sameSite: 'strict'. Family code cookie may stay 'lax' if the ?code= link-click flow requires it (see Open Decisions).
  • Add admin cookie whitelist validation in hooks.server.ts:
    const adminValue = event.cookies.get('admin_session')
    event.locals.admin = adminValue && ADMIN_NAMES.includes(adminValue as any) ? adminValue : null
    
  • Add a SESSION_SECRET startup guard in hooks.server.ts:
    if (!process.env.SESSION_SECRET) throw new Error('SESSION_SECRET environment variable is required')
    
  • Add a test that verifies verifyAdminPassword response time for unknown names is not significantly lower than for wrong-password cases (or document the dummy-hash approach in a code comment explaining the threat model).

Open Decisions (omit if none)

  • Rate limiting on code submission — No mechanism exists to limit attempts on the ?code= gate. 10^12 combinations with no throttle is acceptable for a short-lived family app, but it is an undocumented assumption. Options: (a) accept the risk and document it; (b) add SvelteKit middleware that counts failed attempts per IP; (c) rely on Caddy rate-limiting at the proxy level. (Raised by: Nora Steiner)
## 👤 Nora "NullX" Steiner — Application Security Engineer ### Observations - **CWE-203 Timing Oracle — `verifyAdminPassword` leaks username existence via early return.** The plan's implementation guards with `if (!(ADMIN_NAMES as readonly string[]).includes(name)) return false` before calling `bcrypt.compare`. An attacker sending login attempts for unknown usernames vs. known-but-wrong-password usernames will observe a measurable latency difference (~0 ms vs. ~250 ms for WF12). This is a textbook user-enumeration timing channel. The system design spec (§4) explicitly prohibits user enumeration. - **`admin_session` cookie value is trusted without server-side validation in hooks.** The plan's `hooks.server.ts` sets `event.locals.admin = event.cookies.get('admin_session') ?? null`. Any value in that cookie — including attacker-forged strings — becomes `locals.admin`. The hook must validate the value against the known admin name whitelist before trusting it. This is the same mistake flagged in Nora's persona spec as the "Wrong: trusting the cookie value directly" antipattern. - **`sameSite: 'lax'` instead of `'strict'` on both cookies.** The plan's cookie helpers use `sameSite: 'lax'`. The security persona spec requires `'strict'` for both cookies. `'lax'` permits cookies to be sent on top-level navigations from external origins (e.g., clicking a link from WhatsApp). While this has a use-case for the `?code=` gate flow, it also means the admin session cookie is sent on external-link navigation — a small but unnecessary exposure for the admin session specifically. At minimum, the admin cookie must be `'strict'`. - **No `secure: !dev` flag on cookies.** The plan's cookie helpers set neither `secure: true` nor the SvelteKit `dev`-conditional. In production (HTTPS behind Caddy), cookies without `Secure` can be sent over HTTP if a downgrade occurs. SvelteKit's `dev` import resolves this cleanly: ```typescript import { dev } from '$app/environment' cookies.set('family_code', code, { ..., secure: !dev }) ``` - **`generateCode` charset excludes confusable characters — correct.** `CODE_CHARS = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'` omits `I`, `O`, `0`, `1`. This is the right implementation per NFR-SEC-001. The 32-character alphabet over 8 positions gives 32^8 ≈ 1.1 × 10^12 combinations — brute-force infeasible at normal web request rates without rate limiting. - **No rate limiting on the gate-screen code submission.** The issue spec and plan don't mention rate limiting on the `?code=` validation or the gate-screen form action. With 10^12 combinations and no rate limiting, the attack window is wide enough that a determined attacker could make meaningful progress over months. For a short-lived family app this is acceptable, but it should be a documented decision, not an oversight. - **No `SESSION_SECRET` startup guard.** The system design and Markus's architecture persona both require a startup crash if `SESSION_SECRET` is missing. This is not implemented in Task 3 (it may belong in `db.ts` or `hooks.server.ts`). It should be placed in `hooks.server.ts` since that is the first server-side module loaded per request. ### Recommendations - Fix the timing oracle with a dummy hash approach: ```typescript // CWE-203 prevention: always run bcrypt.compare to equalize response time const DUMMY_HASH = '$2b$12$00000000000000000000000000000000000000000000000000000000' export async function verifyAdminPassword(name: string, password: string): Promise<boolean> { const hash = process.env[`ADMIN_${name.toUpperCase()}_PASSWORD_HASH`] ?? DUMMY_HASH const match = await bcrypt.compare(password, hash) return (ADMIN_NAMES as readonly string[]).includes(name) && match } ``` - Add `secure: !dev` to both cookie helpers. Import `dev` from `'$app/environment'`. - Change `admin_session` cookie to `sameSite: 'strict'`. Family code cookie may stay `'lax'` if the `?code=` link-click flow requires it (see Open Decisions). - Add admin cookie whitelist validation in `hooks.server.ts`: ```typescript const adminValue = event.cookies.get('admin_session') event.locals.admin = adminValue && ADMIN_NAMES.includes(adminValue as any) ? adminValue : null ``` - Add a `SESSION_SECRET` startup guard in `hooks.server.ts`: ```typescript if (!process.env.SESSION_SECRET) throw new Error('SESSION_SECRET environment variable is required') ``` - Add a test that verifies `verifyAdminPassword` response time for unknown names is not significantly lower than for wrong-password cases (or document the dummy-hash approach in a code comment explaining the threat model). ### Open Decisions _(omit if none)_ - **Rate limiting on code submission** — No mechanism exists to limit attempts on the `?code=` gate. 10^12 combinations with no throttle is acceptable for a short-lived family app, but it is an undocumented assumption. Options: (a) accept the risk and document it; (b) add SvelteKit middleware that counts failed attempts per IP; (c) rely on Caddy rate-limiting at the proxy level. _(Raised by: Nora Steiner)_
Author
Owner

👤 Sara Holt — QA Engineer & Test Strategist

Observations

  • 7 tests are specified in the acceptance criteria; the plan delivers exactly 7. Count: verifyAdminPassword (4 tests) + validateFamilyCode (2 tests) + generateCode (2 tests from plan, but one is a uniqueness check not in the original 7-count). The count checks out against the AC, but several critical behaviors are untested at the unit level.

  • No test for the stale-cookie path in hooks. The hook must set locals.familyCode = null when the cookie value exists but is not in the database. This is a correctness requirement (expired/deleted codes must not grant access) with no test coverage.

  • No tests for the redirect behaviors specified in the AC. The AC states: "Hook redirects GET //galerie if locals.familyCode is valid" and "Hook redirects GET /admin/admin/inventar if locals.admin is set." These behaviors have zero test coverage in the plan.

  • The generateCode uniqueness test is statistically weak. Asserting that 20 calls produce >15 unique values proves almost nothing about entropy — a broken PRNG producing sequential outputs would still pass. A charset-membership assertion per character is more meaningful.

  • beforeEach cleanup is incomplete. The test cleans delete process.env.ADMIN_MARCEL_PASSWORD_HASH but does not clean ADMIN_RENATE_PASSWORD_HASH or ADMIN_BERIT_PASSWORD_HASH. If a test sets those env vars and they bleed into the next test, false positives are possible.

  • No test for validateFamilyCode with a code that was deleted. The DB is fresh per test, but there is no test that inserts a code, deletes it, then verifies validateFamilyCode returns null. This edge case is important for the "code invalidation" workflow in the admin.

  • Cookie helper tests are absent. setFamilyCodeCookie, setAdminCookie, clearFamilyCodeCookie, clearAdminCookie are listed in the AC as required exports but have no tests. At minimum, each should verify the correct cookie name and options are passed.

  • Test names are acceptable but could be more specific. "returns true for correct password" is readable. "returns 8-character string of A-Z and 0-9" is slightly wrong now (the charset excludes some digits and letters) — the name should reflect the actual charset.

Recommendations

  • Expand beforeEach to delete all three admin env vars:
    beforeEach(() => {
      process.env.DATABASE_PATH = ':memory:'
      _resetForTesting()
      delete process.env.ADMIN_MARCEL_PASSWORD_HASH
      delete process.env.ADMIN_RENATE_PASSWORD_HASH
      delete process.env.ADMIN_BERIT_PASSWORD_HASH
    })
    
  • Add a hook test suite (src/hooks.server.test.ts) covering at minimum:
    1. Unauthenticated request → locals.familyCode === null, locals.admin === null
    2. Valid family code cookie → locals.familyCode populated with id and displayName
    3. Invalid/expired family code cookie → locals.familyCode === null
    4. GET / with valid family code → redirects to /galerie
    5. GET /admin with valid admin cookie → redirects to /admin/inventar
  • Replace the weak uniqueness test with a charset-membership assertion:
    it('uses only characters from the defined charset (no I, O, 0, 1)', () => {
      const CHARSET = new Set('ABCDEFGHJKLMNPQRSTUVWXYZ23456789')
      for (let i = 0; i < 50; i++) {
        const code = generateCode()
        for (const char of code) {
          expect(CHARSET.has(char)).toBe(true)
        }
      }
    })
    
  • Add cookie helper tests using vi.fn() mock for the Cookies interface to verify correct options (httpOnly, sameSite, path, maxAge) per cookie type.

Open Decisions (omit if none)

  • Hook test placementhooks.server.ts logic (redirect behaviors, locals population) is currently untested by any file in the plan. Options: (a) add src/hooks.server.test.ts as a Vitest unit test that mocks the SvelteKit event; (b) cover these behaviors only via Playwright E2E in a later task. Unit-level is faster and catches regressions earlier, but requires a mocking strategy for event.cookies. (Raised by: Sara Holt)
## 👤 Sara Holt — QA Engineer & Test Strategist ### Observations - **7 tests are specified in the acceptance criteria; the plan delivers exactly 7.** Count: `verifyAdminPassword` (4 tests) + `validateFamilyCode` (2 tests) + `generateCode` (2 tests from plan, but one is a uniqueness check not in the original 7-count). The count checks out against the AC, but several critical behaviors are untested at the unit level. - **No test for the stale-cookie path in hooks.** The hook must set `locals.familyCode = null` when the cookie value exists but is not in the database. This is a correctness requirement (expired/deleted codes must not grant access) with no test coverage. - **No tests for the redirect behaviors specified in the AC.** The AC states: "Hook redirects `GET /` → `/galerie` if `locals.familyCode` is valid" and "Hook redirects `GET /admin` → `/admin/inventar` if `locals.admin` is set." These behaviors have zero test coverage in the plan. - **The `generateCode` uniqueness test is statistically weak.** Asserting that 20 calls produce >15 unique values proves almost nothing about entropy — a broken PRNG producing sequential outputs would still pass. A charset-membership assertion per character is more meaningful. - **`beforeEach` cleanup is incomplete.** The test cleans `delete process.env.ADMIN_MARCEL_PASSWORD_HASH` but does not clean `ADMIN_RENATE_PASSWORD_HASH` or `ADMIN_BERIT_PASSWORD_HASH`. If a test sets those env vars and they bleed into the next test, false positives are possible. - **No test for `validateFamilyCode` with a code that was deleted.** The DB is fresh per test, but there is no test that inserts a code, deletes it, then verifies `validateFamilyCode` returns `null`. This edge case is important for the "code invalidation" workflow in the admin. - **Cookie helper tests are absent.** `setFamilyCodeCookie`, `setAdminCookie`, `clearFamilyCodeCookie`, `clearAdminCookie` are listed in the AC as required exports but have no tests. At minimum, each should verify the correct cookie name and options are passed. - **Test names are acceptable but could be more specific.** "returns true for correct password" is readable. "returns 8-character string of A-Z and 0-9" is slightly wrong now (the charset excludes some digits and letters) — the name should reflect the actual charset. ### Recommendations - Expand `beforeEach` to delete all three admin env vars: ```typescript beforeEach(() => { process.env.DATABASE_PATH = ':memory:' _resetForTesting() delete process.env.ADMIN_MARCEL_PASSWORD_HASH delete process.env.ADMIN_RENATE_PASSWORD_HASH delete process.env.ADMIN_BERIT_PASSWORD_HASH }) ``` - Add a hook test suite (`src/hooks.server.test.ts`) covering at minimum: 1. Unauthenticated request → `locals.familyCode === null`, `locals.admin === null` 2. Valid family code cookie → `locals.familyCode` populated with `id` and `displayName` 3. Invalid/expired family code cookie → `locals.familyCode === null` 4. `GET /` with valid family code → redirects to `/galerie` 5. `GET /admin` with valid admin cookie → redirects to `/admin/inventar` - Replace the weak uniqueness test with a charset-membership assertion: ```typescript it('uses only characters from the defined charset (no I, O, 0, 1)', () => { const CHARSET = new Set('ABCDEFGHJKLMNPQRSTUVWXYZ23456789') for (let i = 0; i < 50; i++) { const code = generateCode() for (const char of code) { expect(CHARSET.has(char)).toBe(true) } } }) ``` - Add cookie helper tests using `vi.fn()` mock for the `Cookies` interface to verify correct options (httpOnly, sameSite, path, maxAge) per cookie type. ### Open Decisions _(omit if none)_ - **Hook test placement** — `hooks.server.ts` logic (redirect behaviors, `locals` population) is currently untested by any file in the plan. Options: (a) add `src/hooks.server.test.ts` as a Vitest unit test that mocks the SvelteKit event; (b) cover these behaviors only via Playwright E2E in a later task. Unit-level is faster and catches regressions earlier, but requires a mocking strategy for `event.cookies`. _(Raised by: Sara Holt)_
Author
Owner

👤 Tobias Wendt — DevOps & Platform Engineer

Observations

  • SESSION_SECRET is referenced in the system design but not validated at startup. Task 3 is the first task that loads hooks.server.ts, which is the natural place to guard against a missing SESSION_SECRET. If this guard is omitted, the app will start and serve requests without the secret configured — a silent security misconfiguration. Tobias's persona spec explicitly calls for a startup crash on missing SESSION_SECRET.

  • The three admin password hash env vars (ADMIN_MARCEL_PASSWORD_HASH, ADMIN_RENATE_PASSWORD_HASH, ADMIN_BERIT_PASSWORD_HASH) are consumed in auth.ts via process.env. This is correct — they come from environment, not from code. The .env.example mentioned in the DevOps persona spec should be created or updated alongside this task so the deployment documentation stays in sync with the code.

  • No .env.example update is planned in this task. Task 3 introduces the env var consumption pattern for all auth variables. The .env.example file should document ADMIN_MARCEL_PASSWORD_HASH, ADMIN_RENATE_PASSWORD_HASH, ADMIN_BERIT_PASSWORD_HASH, and SESSION_SECRET with generation instructions. This is a documentation gap that will cause friction during the first deployment.

  • DATABASE_PATH env var is also consumed by db.ts (Task 2), not Task 3. The auth module imports getDb() which reads DATABASE_PATH. The env var chain is: hooks.server.tsauth.tsgetDb()process.env.DATABASE_PATH. All of these need to be documented together in .env.example.

  • UPLOAD_DIR is not yet in scope for Task 3 (that's Task 4), but the complete .env.example should be deferred until at least Task 4 lands.

Recommendations

  • Add a SESSION_SECRET startup guard at the top of hooks.server.ts (runs once at module-init time, before any request):
    if (!process.env.SESSION_SECRET) {
      throw new Error('SESSION_SECRET environment variable is required. Set it in .env before starting.')
    }
    
  • Create a .env.example file alongside Task 3 commit with at minimum:
    # .env.example — copy to .env and fill in real values (never commit .env)
    SESSION_SECRET=          # random string, min 32 chars
    DATABASE_PATH=/app/db/erbstuecke.db
    ADMIN_MARCEL_PASSWORD_HASH=   # bcrypt(12) — node -e "require('bcryptjs').hash('pw',12).then(console.log)"
    ADMIN_RENATE_PASSWORD_HASH=
    ADMIN_BERIT_PASSWORD_HASH=
    # UPLOAD_DIR added in Task 4
    
  • Verify .env is in .gitignore before committing this task — the auth credentials env vars make it critical.
## 👤 Tobias Wendt — DevOps & Platform Engineer ### Observations - **`SESSION_SECRET` is referenced in the system design but not validated at startup.** Task 3 is the first task that loads `hooks.server.ts`, which is the natural place to guard against a missing `SESSION_SECRET`. If this guard is omitted, the app will start and serve requests without the secret configured — a silent security misconfiguration. Tobias's persona spec explicitly calls for a startup crash on missing `SESSION_SECRET`. - **The three admin password hash env vars (`ADMIN_MARCEL_PASSWORD_HASH`, `ADMIN_RENATE_PASSWORD_HASH`, `ADMIN_BERIT_PASSWORD_HASH`) are consumed in `auth.ts` via `process.env`.** This is correct — they come from environment, not from code. The `.env.example` mentioned in the DevOps persona spec should be created or updated alongside this task so the deployment documentation stays in sync with the code. - **No `.env.example` update is planned in this task.** Task 3 introduces the env var consumption pattern for all auth variables. The `.env.example` file should document `ADMIN_MARCEL_PASSWORD_HASH`, `ADMIN_RENATE_PASSWORD_HASH`, `ADMIN_BERIT_PASSWORD_HASH`, and `SESSION_SECRET` with generation instructions. This is a documentation gap that will cause friction during the first deployment. - **`DATABASE_PATH` env var is also consumed by `db.ts` (Task 2), not Task 3.** The auth module imports `getDb()` which reads `DATABASE_PATH`. The env var chain is: `hooks.server.ts` → `auth.ts` → `getDb()` → `process.env.DATABASE_PATH`. All of these need to be documented together in `.env.example`. - **`UPLOAD_DIR` is not yet in scope for Task 3** (that's Task 4), but the complete `.env.example` should be deferred until at least Task 4 lands. ### Recommendations - Add a `SESSION_SECRET` startup guard at the top of `hooks.server.ts` (runs once at module-init time, before any request): ```typescript if (!process.env.SESSION_SECRET) { throw new Error('SESSION_SECRET environment variable is required. Set it in .env before starting.') } ``` - Create a `.env.example` file alongside Task 3 commit with at minimum: ```bash # .env.example — copy to .env and fill in real values (never commit .env) SESSION_SECRET= # random string, min 32 chars DATABASE_PATH=/app/db/erbstuecke.db ADMIN_MARCEL_PASSWORD_HASH= # bcrypt(12) — node -e "require('bcryptjs').hash('pw',12).then(console.log)" ADMIN_RENATE_PASSWORD_HASH= ADMIN_BERIT_PASSWORD_HASH= # UPLOAD_DIR added in Task 4 ``` - Verify `.env` is in `.gitignore` before committing this task — the auth credentials env vars make it critical.
Author
Owner

👤 Leonie Voss — UX Design Lead & Accessibility Strategist

Observations

  • This task is backend-only — no UI components, no templates, no rendered output. auth.ts and hooks.server.ts are pure server modules. There are no accessibility or visual concerns to flag in the implementation itself.

  • The redirects defined in the hook directly affect UX flows. Two redirect behaviors are specified:

    1. GET //galerie when locals.familyCode is valid (prevents authenticated family members from seeing the gate screen again)
    2. GET /admin/admin/inventar when locals.admin is set (prevents admins from landing on a dead route)
      Both are correct from a UX standpoint — users should not be asked to re-authenticate if already authenticated.
  • Cookie maxAge values affect the re-authentication UX. The plan sets maxAge: 60 * 60 * 24 * 365 (1 year) for family codes and 60 * 60 * 24 * 7 (7 days) for admin sessions. From a UX perspective, a 1-year family cookie is appropriate — family members should not have to re-enter their code repeatedly across sessions. The 7-day admin session is reasonable for the usage pattern (infrequent admin visits during a short-lived event). Nora's persona spec suggests 8 hours for admin; this is a security vs. UX tradeoff.

  • No feedback mechanism is specified in this task for when a cookie exists but the code has been deleted. If an admin deletes a family code while the member has an active session, the hook will clear locals.familyCode silently. The member will be redirected to the gate screen without explanation. A brief error message on the gate screen ("Dein Zugangscode ist nicht mehr gültig") would improve the experience, but this is a concern for the gate-screen task, not Task 3.

  • The displayName is carried in locals.familyCode and will be available to all downstream route load functions. This enables personalized greetings in the gallery UI ("Hallo, Markus!") without additional DB queries per request. Good architecture for UX.

Recommendations

  • Ensure the gate-screen task (likely Task 5 or the route implementation) handles the case where the family code cookie is present but invalid — show a friendly German-language message rather than silently redirecting to the gate form.
  • Consider whether the admin session maxAge should be 8 hours (Nora's recommendation) or 7 days (plan's value). 8 hours matches the design spec's intent for a shorter-lived admin session; 7 days is more convenient if the admin works across multiple days. This is a human-context decision.

Open Decisions (omit if none)

  • Admin cookie maxAge: 8 hours vs. 7 days — The security spec suggests 8 hours for admin sessions; the plan implements 7 days. If admins (Marcel, Renate, Berit) will work on the inventory across multiple calendar days without re-logging in, 7 days is more convenient. If the session should expire overnight for security, 8 hours is correct. (Raised by: Leonie Voss)
## 👤 Leonie Voss — UX Design Lead & Accessibility Strategist ### Observations - **This task is backend-only — no UI components, no templates, no rendered output.** `auth.ts` and `hooks.server.ts` are pure server modules. There are no accessibility or visual concerns to flag in the implementation itself. - **The redirects defined in the hook directly affect UX flows.** Two redirect behaviors are specified: 1. `GET /` → `/galerie` when `locals.familyCode` is valid (prevents authenticated family members from seeing the gate screen again) 2. `GET /admin` → `/admin/inventar` when `locals.admin` is set (prevents admins from landing on a dead route) Both are correct from a UX standpoint — users should not be asked to re-authenticate if already authenticated. - **Cookie `maxAge` values affect the re-authentication UX.** The plan sets `maxAge: 60 * 60 * 24 * 365` (1 year) for family codes and `60 * 60 * 24 * 7` (7 days) for admin sessions. From a UX perspective, a 1-year family cookie is appropriate — family members should not have to re-enter their code repeatedly across sessions. The 7-day admin session is reasonable for the usage pattern (infrequent admin visits during a short-lived event). Nora's persona spec suggests 8 hours for admin; this is a security vs. UX tradeoff. - **No feedback mechanism is specified in this task for when a cookie exists but the code has been deleted.** If an admin deletes a family code while the member has an active session, the hook will clear `locals.familyCode` silently. The member will be redirected to the gate screen without explanation. A brief error message on the gate screen ("Dein Zugangscode ist nicht mehr gültig") would improve the experience, but this is a concern for the gate-screen task, not Task 3. - **The `displayName` is carried in `locals.familyCode`** and will be available to all downstream route load functions. This enables personalized greetings in the gallery UI ("Hallo, Markus!") without additional DB queries per request. Good architecture for UX. ### Recommendations - Ensure the gate-screen task (likely Task 5 or the route implementation) handles the case where the family code cookie is present but invalid — show a friendly German-language message rather than silently redirecting to the gate form. - Consider whether the admin session `maxAge` should be 8 hours (Nora's recommendation) or 7 days (plan's value). 8 hours matches the design spec's intent for a shorter-lived admin session; 7 days is more convenient if the admin works across multiple days. This is a human-context decision. ### Open Decisions _(omit if none)_ - **Admin cookie `maxAge`: 8 hours vs. 7 days** — The security spec suggests 8 hours for admin sessions; the plan implements 7 days. If admins (Marcel, Renate, Berit) will work on the inventory across multiple calendar days without re-logging in, 7 days is more convenient. If the session should expire overnight for security, 8 hours is correct. _(Raised by: Leonie Voss)_
Author
Owner

👤 Elicit — Requirements Engineer

Observations

  • All 5 acceptance criteria are implementable as specified. The issue is well-written: user story is clear, file list is explicit, NFRs are referenced with IDs, and the dependency on #2 is noted. Definition of Ready score: high.

  • AC gap: the "no user enumeration" requirement appears in the spec (system design §4) and in Nora's domain expertise table, but is not listed as an explicit acceptance criterion. The issue says verifyAdminPassword "returns false for unknown names (no user enumeration)" but does not state the timing requirement. A timing oracle is still user enumeration — the AC should be strengthened:

    verifyAdminPassword(name, password) — async, reads bcrypt hash from ADMIN_{NAME}_PASSWORD_HASH env var, returns false for unknown names without revealing via response latency whether the name is known

  • NFR-SEC-001 references crypto.randomBytes and [A-Z2-9] charset. The issue body says [A-Z2-9] but both the security persona spec and the plan implementation use 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789' — which excludes I, O, 0, 1 (not just limiting to A-Z2-9). The AC charset description is imprecise. The correct description is: "uppercase letters excluding I and O, digits 2–9 (excluding 0 and 1)."

  • The locals.admin validation requirement is implied but not stated. The system design §4 says "hooks.server.ts → locals.admin" but does not explicitly state that the cookie value must be validated against the known admin whitelist. A developer reading only the issue could implement the insecure version (trust cookie directly) and still claim all AC are met.

  • Dependency on #2 is correct. validateFamilyCode calls getDb() which is implemented in Task 2 (db.ts). The dependency is real and correctly noted.

  • Size estimate "S" is reasonable for 3 files with 7 tests, given that the implementation is well-specified in the plan. 1–3 hours of focused work.

Recommendations

  • Strengthen the user-enumeration AC to explicitly include timing:

    verifyAdminPassword must return false for unknown admin names and must take the same wall-clock time as a wrong-password attempt to prevent timing-based user enumeration.

  • Correct the charset description in the issue body from [A-Z2-9] to "uppercase A–Z excluding I and O, digits 2–9":

    generateCode() — returns 8-character string from uppercase letters (A–Z, excluding I and O) and digits (2–9) using crypto.randomBytes

  • Add an explicit AC for hooks.server.ts admin cookie validation:

    Hook sets locals.admin only if the admin_session cookie value matches a known admin name (Marcel, Renate, or Berit); arbitrary cookie values must not set locals.admin.

  • Add an explicit AC for the stale-cookie case:

    Hook sets locals.familyCode = null when the family_code cookie is present but the code no longer exists in the database.

## 👤 Elicit — Requirements Engineer ### Observations - **All 5 acceptance criteria are implementable as specified.** The issue is well-written: user story is clear, file list is explicit, NFRs are referenced with IDs, and the dependency on #2 is noted. Definition of Ready score: high. - **AC gap: the "no user enumeration" requirement appears in the spec (system design §4) and in Nora's domain expertise table, but is not listed as an explicit acceptance criterion.** The issue says `verifyAdminPassword` "returns `false` for unknown names (no user enumeration)" but does not state the timing requirement. A timing oracle is still user enumeration — the AC should be strengthened: > `verifyAdminPassword(name, password)` — async, reads bcrypt hash from `ADMIN_{NAME}_PASSWORD_HASH` env var, returns `false` for unknown names **without revealing via response latency whether the name is known** - **NFR-SEC-001 references `crypto.randomBytes` and `[A-Z2-9]` charset.** The issue body says `[A-Z2-9]` but both the security persona spec and the plan implementation use `'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'` — which excludes `I`, `O`, `0`, `1` (not just limiting to `A-Z2-9`). The AC charset description is imprecise. The correct description is: "uppercase letters excluding I and O, digits 2–9 (excluding 0 and 1)." - **The `locals.admin` validation requirement is implied but not stated.** The system design §4 says "hooks.server.ts → `locals.admin`" but does not explicitly state that the cookie value must be validated against the known admin whitelist. A developer reading only the issue could implement the insecure version (trust cookie directly) and still claim all AC are met. - **Dependency on #2 is correct.** `validateFamilyCode` calls `getDb()` which is implemented in Task 2 (`db.ts`). The dependency is real and correctly noted. - **Size estimate "S" is reasonable** for 3 files with 7 tests, given that the implementation is well-specified in the plan. 1–3 hours of focused work. ### Recommendations - Strengthen the user-enumeration AC to explicitly include timing: > `verifyAdminPassword` must return `false` for unknown admin names **and must take the same wall-clock time as a wrong-password attempt** to prevent timing-based user enumeration. - Correct the charset description in the issue body from `[A-Z2-9]` to "uppercase A–Z excluding I and O, digits 2–9": > `generateCode()` — returns 8-character string from uppercase letters (A–Z, excluding I and O) and digits (2–9) using `crypto.randomBytes` - Add an explicit AC for `hooks.server.ts` admin cookie validation: > Hook sets `locals.admin` **only if the `admin_session` cookie value matches a known admin name** (Marcel, Renate, or Berit); arbitrary cookie values must not set `locals.admin`. - Add an explicit AC for the stale-cookie case: > Hook sets `locals.familyCode = null` when the `family_code` cookie is present but the code no longer exists in the database.
Author
Owner

🗳️ Decision Queue — Action Required

4 decisions need your input before implementation starts.

  • sameSite: 'lax' vs 'strict' for family code cookie — The plan uses 'lax'; the security spec says 'strict'. 'strict' will break the ?code= invite-link flow when family members tap a link from WhatsApp or email — the browser won't send the cookie on that first cross-site navigation, causing the auto-login to fail on the first tap (member lands on gate, sees the code pre-filled but not yet authenticated). 'lax' fixes the UX but is a small security concession. The admin cookie should be 'strict' regardless. (Raised by: Markus Keller, Nora Steiner)

Rate Limiting on Code Submission

  • Accept vs. mitigate brute-force risk on the ?code= gate — 32^8 ≈ 10^12 combinations with no throttle. For a short-lived family app this risk is low, but it is currently undocumented. Options: (a) explicitly accept and document the risk; (b) add application-level IP throttling in hooks.server.ts; (c) add a Caddy rate_limit directive on the host. Option (a) costs nothing and is likely appropriate; options (b)/(c) add complexity. (Raised by: Nora Steiner)

Hook Test Strategy

  • Unit-test hooks.server.ts vs. defer to E2E — The hook's redirect behaviors and locals population are currently untested at the unit level. Options: (a) add src/hooks.server.test.ts as a Vitest unit test that mocks the SvelteKit event object — fast, runs in CI without a browser, requires a small mock harness; (b) cover these behaviors only via Playwright E2E in a later task — less upfront work but slower feedback loop and deferred coverage. (Raised by: Sara Holt)
  • maxAge: 8 hours vs. 7 days for the admin_session cookie — The security persona spec recommends 8 hours (sessions expire overnight); the plan implements 7 days (admins don't re-login across days). If Marcel, Renate, or Berit will work on the inventory across multiple calendar days, 7 days is more convenient. If overnight expiry is acceptable or desired for security hygiene, 8 hours is correct. (Raised by: Leonie Voss)
## 🗳️ Decision Queue — Action Required _4 decisions need your input before implementation starts._ ### Cookie SameSite Policy - **`sameSite: 'lax'` vs `'strict'` for family code cookie** — The plan uses `'lax'`; the security spec says `'strict'`. `'strict'` will break the `?code=` invite-link flow when family members tap a link from WhatsApp or email — the browser won't send the cookie on that first cross-site navigation, causing the auto-login to fail on the first tap (member lands on gate, sees the code pre-filled but not yet authenticated). `'lax'` fixes the UX but is a small security concession. The admin cookie should be `'strict'` regardless. _(Raised by: Markus Keller, Nora Steiner)_ ### Rate Limiting on Code Submission - **Accept vs. mitigate brute-force risk on the `?code=` gate** — 32^8 ≈ 10^12 combinations with no throttle. For a short-lived family app this risk is low, but it is currently undocumented. Options: (a) explicitly accept and document the risk; (b) add application-level IP throttling in `hooks.server.ts`; (c) add a Caddy `rate_limit` directive on the host. Option (a) costs nothing and is likely appropriate; options (b)/(c) add complexity. _(Raised by: Nora Steiner)_ ### Hook Test Strategy - **Unit-test `hooks.server.ts` vs. defer to E2E** — The hook's redirect behaviors and `locals` population are currently untested at the unit level. Options: (a) add `src/hooks.server.test.ts` as a Vitest unit test that mocks the SvelteKit `event` object — fast, runs in CI without a browser, requires a small mock harness; (b) cover these behaviors only via Playwright E2E in a later task — less upfront work but slower feedback loop and deferred coverage. _(Raised by: Sara Holt)_ ### Admin Session Cookie Lifetime - **`maxAge`: 8 hours vs. 7 days for the `admin_session` cookie** — The security persona spec recommends 8 hours (sessions expire overnight); the plan implements 7 days (admins don't re-login across days). If Marcel, Renate, or Berit will work on the inventory across multiple calendar days, 7 days is more convenient. If overnight expiry is acceptable or desired for security hygiene, 8 hours is correct. _(Raised by: Leonie Voss)_
Sign in to join this conversation.