docs(07): capture phase context

This commit is contained in:
Jakub Zych
2026-09-22 00:24:21 +02:00
parent 2fe5c1e920
commit 1979c45083
2 changed files with 321 additions and 0 deletions

View File

@@ -0,0 +1,154 @@
# Phase 7: User plugin and authentication - Discussion Log
> **Audit trail only.** Do not use as input to planning, research, or execution agents.
> Decisions are captured in CONTEXT.md — this log preserves the alternatives considered.
**Date:** 2026-09-22
**Phase:** 7-User plugin and authentication
**Areas discussed:** Route surface cut, JWT lifecycle, Parity evidence, Security vs parity
---
## Route surface cut
### Core routes
| Option | Description | Selected |
|--------|-------------|----------|
| Nuxt-used + criterion 1 | login, logout, fetch, refresh, register, update, change-password, marketing-consent + forgot/reset/activate/activate-by-code | ✓ |
| Nuxt-used only | Drop forgot/reset/activate; conflicts with criterion 1 | |
| Everything live in routes.php | Also `api/user/batch` and `/_user/activate/{id}` | |
**User's choice:** Asked first which endpoints option 1 skips versus option 3. Answer given: exactly two — `GET /api/user/batch` (presence, no caller) and `GET /_user/activate/{id}` (signed-URL legacy theme flow). Then: "OK option 1 then, we'll build presence in future milestones, legacy cms theme flow is legacy."
### Social login
| Option | Description | Selected |
|--------|-------------|----------|
| oauth-providers only, rest deferred | Login page renders; provider flow deferred | ✓ |
| Port the full social flow now | Redirect, callback, complete endpoints, bootstrap OTP | |
| Defer all of it | oauth-providers 404s | |
### Avatar
| Option | Description | Selected |
|--------|-------------|----------|
| Port both now | Multipart upload on Phase 5 attachments + Thumb(128) | ✓ |
| Payload fields only | Endpoints wait for Phase 12 | |
### Dead routes (PIN, device, 2FA)
| Option | Description | Selected |
|--------|-------------|----------|
| Skip entirely, document | No 501 shells; login never returns two_factor_required | ✓ |
| Skip, but port the 2FA login branch | Enrolled accounts not downgraded at cutover | |
---
## JWT lifecycle
### Blacklist storage
| Option | Description | Selected |
|--------|-------------|----------|
| Postgres table | Survives restarts, works across replicas | ✓ |
| In-process map | Restart resurrects logged-out tokens | |
| Postgres + in-process read cache | More moving parts | |
### Cutover compatibility
| Option | Description | Selected |
|--------|-------------|----------|
| Yes, wire-compatible tokens | Same secret and claims; PHP blacklist not migrated | ✓ |
| Compatible claims, but no promise | Cutover may force re-login | |
| Yes, and migrate the PHP blacklist | Phase 15 import | |
### Extraction
| Option | Description | Selected |
|--------|-------------|----------|
| Port the split exactly | Incl. `?jwt_token=` and jwt-auth default parsers | |
| One permissive extractor everywhere | Cookies on /_user too | |
| Header + cookies only, everywhere (follow-up) | Drop all URL/body/route-param sources | ✓ |
| Drop URL sources, keep POST-body jwt_token (follow-up) | | |
| Drop, but answer explicitly with 400 (follow-up) | | |
**User's choice:** Free text — "jwt_token GET param should be depreciated by now per Golem15 Stack security upgrade, we don't want tokens in access logs. But we need to port the split for Bearer / Cookies as it's used trough all projects."
**Notes:** Verified neither the Nuxt app nor fonoteka-mcp sends `jwt_token`/`?token=`; PHP's `$request->get('jwt_token')` also reads the POST body. Follow-up confirmed "Header + cookies only, everywhere".
### TTLs
| Option | Description | Selected |
|--------|-------------|----------|
| Config keys, PHP-env defaults in the app | Library defaults in plugin, Płytarium values in fonoteka.go | ✓ |
| Hardcode Płytarium's values in the plugin | | |
---
## Parity evidence
| Question | Options | Selected |
|----------|---------|----------|
| How is the /_user contract proven? | Record new tide fixtures + extend manifest / Hand-written contract tests / Both | Record new tide fixtures, extend manifest |
| Mail-dependent codes | Seed hook reads code from DB / Capture from mail sink / Pre-seeded fixed codes | Seed hook reads the code from the DB |
| Error-path coverage | Every distinct status + body per route / Happy path + one failure | Every distinct status + body per route |
| Nuxt client flow | Add a nuxt-auth flow fixture / Route fixtures are enough | Add a nuxt-auth flow fixture |
---
## Security vs parity
### Reset/activation codes
| Option | Description | Selected |
|--------|-------------|----------|
| Add expiry, keep format and columns | TTL via added timestamp column, constant-time compare, same wire bodies | ✓ |
| Exact PHP behaviour | No expiry | |
| Expiry + hash at rest | Breaks PHP-issued codes and the DB-reading seed hook | |
**User's choice:** "Is your research correct? You sure PHP codes do not expire? Option 1 tho, quite sure php codes do expire"
**Notes:** Re-verified in source: Winter Storm `User.php:258-292` is a plain `===` with no timestamp, the plugin does not override it, the users table has no expiry column, and `passwords.expire=60` belongs to the unused Laravel broker. Things that do expire: bootstrap OTP (30 min), signed invite URL (72 h). Possible that another Golem15 project has a newer plugin version. Assumption stated: null issued-at codes get one full TTL from cutover; not objected to.
### Failed-login throttle
| Option | Description | Selected |
|--------|-------------|----------|
| Port Winter Throttle onto user_throttle | Durable, ban/suspend states, PHP bodies | ✓ |
| Limiter bucket only | In-process, no ban state | |
### Login/register quirks
| Option | Description | Selected |
|--------|-------------|----------|
| Port restore-on-login, drop guest conversion | | ✓ |
| Port both exactly | | |
| Drop both | | |
### Mail + hashing
| Option | Description | Selected |
|--------|-------------|----------|
| Send inline now; bcrypt from config, rehash on login | Seam for River in Phase 11 | ✓ |
| Inline mail; bcrypt 10 fixed, no rehash | | |
| Goroutine fire-and-forget mail | | |
### Revoke tokens on password change/reset
| Option | Description | Selected |
|--------|-------------|----------|
| Invalidate all older tokens | Per-user "valid after" timestamp checked by the jwt guard | ✓ |
| Exact PHP behaviour | Old tokens live up to 30 days | |
| Invalidate on reset only | | |
### Setting must_change_password
| Option | Description | Selected |
|--------|-------------|----------|
| CLI command now, admin field in Phase 9 | | ✓ |
| Tests and seed hooks only | | |
---
## Claude's Discretion
- Locale-resolution stage placement for I18N-02
- Package homes for minting/blacklist, Store interface, sweep interval
- `getApiArray` event type and merge order
- Minimal read models for payload fields backed by unported tables (permissions, groups, role)
- 423-exempt grouping asserted over the route table
- Column/config/command names; plan count and split
## Deferred Ideas
- Presence (`api/user/batch`) — future milestone
- Full social login flow and password-bootstrap OTP
- PIN login, device auth, 2FA
- Signed admin-invite activation link (legacy)
- Guest conversion (keios.eu), account deletion/GDPR routes
- River-queued mail (Phase 11), PHP blacklist migration (not done), admin lock/ban UI (Phase 9)