WS-3 PR-B2b: A13-3 + single-cookie + single-host (incl. flatpickr precursor) #6

Merged
bert.hausmans merged 8 commits from feat/ws-3-pr-b2b-single-cookie-deploy into main 2026-05-06 01:16:07 +02:00

Summary

Final consolidation step in WS-3 single-SPA migration. Three primary deliverables plus three bonus catch-up commits surfaced during build verification.

Primary scope:

  • A13-3 full open-redirect validation in postLoginRedirect (closes A13-3)
  • Single-cookie consolidation: crewli_portal_token + Origin-resolution machinery purged server-side
  • Single-host deploy config: apps/portal build dropped from deploy.sh, portal.crewli.app 301-redirect template added

Bonus catch-up (surfaced during build verification, applied on this branch):

  • flatpickr SCSS → JS-side import migration (was originally going to be a separate precursor PR; pre-existing main-branch build regression)
  • auto-imports.d.ts and components.d.ts regen — pre-existing PR-B2a drift surfaced by flatpickr's pnpm install

Commits

# SHA Message
1 96cb151 feat(security): full A13-3 open-redirect validation in postLoginRedirect
2 2e94a10 refactor(auth): consolidate to single cookie post single-SPA
3 a748c9e chore(deploy): single-host deploy config — drop apps/portal build, retire portal.crewli.app
4 812cc17 docs(auth): reflect single-cookie architecture; close A13-3
5 7a69b03 chore(docs): drop apps/portal references from load-bearing files
6 ad23847 fix(deps): import flatpickr CSS via JS, add flatpickr direct dep
7 eb48557 chore(types): regenerate auto-imports.d.ts to sync with PR-B2a additions
8 289e735 chore(types): regenerate components.d.ts to sync with PR-B2a additions

Acceptance gates (all green)

Gate Result
php artisan test 1487 passed (was 1491; −4 dual-cookie tests removed)
composer analyse (Larastan) no errors above baseline
composer rector --dry-run no new findings (net deletion)
pnpm test 223 passed (was 213; +10 A13-3 cases)
pnpm typecheck 0 errors
pnpm lint 0 errors (pre-existing v5/v6 boundaries deprecation warnings unchanged)
pnpm build 1172 modules transformed; fixed pre-existing regression
bash -n deploy.sh syntax clean

Locked design decisions

  • Cookie name kept as crewli_app_token — no session breakage on deploy
  • frontend_portal_url config key retained (Phase A Q1 — three non-auth consumers in email controllers; refactor tracked as TECH-FRONTEND-URL-CONSOLIDATE)
  • Doc purge limited to load-bearing files: README.md, Makefile, CLAUDE.md (Phase A Q2 — 9 other files deferred as TECH-DOCS-APPS-PORTAL-PURGE)
  • A13-1 finding actualised + flipped to RESOLVED (Phase A Q4) since the underlying localStorage→httpOnly migration shipped earlier in the consolidation arc
  • portal.crewli.app DNS retirement is operational, not code (deferred)

Notable deviations from original plan

  • Branch base: prompt referenced 68f1e6f; actual main was 5380722 post-WS-TOOLING-001. No conflicts — WS-TOOLING-001 was guard-rail tooling, didn't touch auth/deploy paths.
  • Commit 2 sweep caught one extra caller: InvitationController.php also used the removed resolveCookieName($request) / makeAuthCookie($cookieName, $token) signature. Original prompt's caller list missed this; same one-line removal pattern applied in the same commit.
  • Original plan had flatpickr as a separate precursor PR. In execution the fix landed on the B2b branch directly (commits 6-8). Main was red regardless of B2b — flatpickr was a pre-existing regression — so the merge order doesn't matter. Net effect: this PR ships green; main goes from red to green.

Out of scope (tracked in BACKLOG)

  • TECH-FRONTEND-URL-CONSOLIDATE — refactor 3 email controllers to drop per-app URL map (low priority, code cleanliness)
  • TECH-DOCS-APPS-PORTAL-PURGE — sweep 9 other doc files (.cursor/, MASTER_PROMPT_, SETUP, dev-guide, CLAUDE_CODE_TOOLING) — single chore(docs) PR, low priority
  • OPS — Retire portal.crewli.app DNS record — operational task, no deadline

Files changed

git diff main --stat across all 8 commits: ~23 files, net deletions exceed insertions despite gaining 10 frontend test cases.

## Summary Final consolidation step in WS-3 single-SPA migration. Three primary deliverables plus three bonus catch-up commits surfaced during build verification. **Primary scope:** - A13-3 full open-redirect validation in `postLoginRedirect` (closes A13-3) - Single-cookie consolidation: `crewli_portal_token` + Origin-resolution machinery purged server-side - Single-host deploy config: `apps/portal` build dropped from `deploy.sh`, `portal.crewli.app` 301-redirect template added **Bonus catch-up (surfaced during build verification, applied on this branch):** - flatpickr SCSS → JS-side import migration (was originally going to be a separate precursor PR; pre-existing main-branch build regression) - `auto-imports.d.ts` and `components.d.ts` regen — pre-existing PR-B2a drift surfaced by flatpickr's pnpm install ## Commits | # | SHA | Message | | - | --- | ------- | | 1 | `96cb151` | feat(security): full A13-3 open-redirect validation in postLoginRedirect | | 2 | `2e94a10` | refactor(auth): consolidate to single cookie post single-SPA | | 3 | `a748c9e` | chore(deploy): single-host deploy config — drop apps/portal build, retire portal.crewli.app | | 4 | `812cc17` | docs(auth): reflect single-cookie architecture; close A13-3 | | 5 | `7a69b03` | chore(docs): drop apps/portal references from load-bearing files | | 6 | `ad23847` | fix(deps): import flatpickr CSS via JS, add flatpickr direct dep | | 7 | `eb48557` | chore(types): regenerate auto-imports.d.ts to sync with PR-B2a additions | | 8 | `289e735` | chore(types): regenerate components.d.ts to sync with PR-B2a additions | ## Acceptance gates (all green) | Gate | Result | | ---- | ------ | | `php artisan test` | 1487 passed (was 1491; −4 dual-cookie tests removed) | | `composer analyse` (Larastan) | no errors above baseline | | `composer rector --dry-run` | no new findings (net deletion) | | `pnpm test` | 223 passed (was 213; +10 A13-3 cases) | | `pnpm typecheck` | 0 errors | | `pnpm lint` | 0 errors (pre-existing v5/v6 boundaries deprecation warnings unchanged) | | `pnpm build` | 1172 modules transformed; fixed pre-existing regression | | `bash -n deploy.sh` | syntax clean | ## Locked design decisions - Cookie name kept as `crewli_app_token` — no session breakage on deploy - `frontend_portal_url` config key retained (Phase A Q1 — three non-auth consumers in email controllers; refactor tracked as `TECH-FRONTEND-URL-CONSOLIDATE`) - Doc purge limited to load-bearing files: `README.md`, `Makefile`, `CLAUDE.md` (Phase A Q2 — 9 other files deferred as `TECH-DOCS-APPS-PORTAL-PURGE`) - `A13-1` finding actualised + flipped to `RESOLVED` (Phase A Q4) since the underlying localStorage→httpOnly migration shipped earlier in the consolidation arc - `portal.crewli.app` DNS retirement is operational, not code (deferred) ## Notable deviations from original plan - **Branch base:** prompt referenced `68f1e6f`; actual main was `5380722` post-WS-TOOLING-001. No conflicts — WS-TOOLING-001 was guard-rail tooling, didn't touch auth/deploy paths. - **Commit 2 sweep caught one extra caller:** `InvitationController.php` also used the removed `resolveCookieName($request)` / `makeAuthCookie($cookieName, $token)` signature. Original prompt's caller list missed this; same one-line removal pattern applied in the same commit. - **Original plan had flatpickr as a separate precursor PR.** In execution the fix landed on the B2b branch directly (commits 6-8). Main was red regardless of B2b — flatpickr was a pre-existing regression — so the merge order doesn't matter. Net effect: this PR ships green; main goes from red to green. ## Out of scope (tracked in BACKLOG) - `TECH-FRONTEND-URL-CONSOLIDATE` — refactor 3 email controllers to drop per-app URL map (low priority, code cleanliness) - `TECH-DOCS-APPS-PORTAL-PURGE` — sweep 9 other doc files (.cursor/*, MASTER_PROMPT_*, SETUP, dev-guide, CLAUDE_CODE_TOOLING) — single `chore(docs)` PR, low priority - `OPS — Retire portal.crewli.app DNS record` — operational task, no deadline ## Files changed `git diff main --stat` across all 8 commits: ~23 files, net deletions exceed insertions despite gaining 10 frontend test cases.
bert.hausmans added 8 commits 2026-05-06 01:15:09 +02:00
Replaces the WS-3 PR-B2a minimum precaution (`startsWith('/') &&
!startsWith('//')`) with a layered validator that rejects every input
that is not a strict relative path.

isSafeRelativePath rejects:
- Empty / null / undefined input
- Non-`/`-prefixed paths (including leading whitespace)
- Protocol-relative URLs (`//evil.com`)
- Backslash anywhere (browsers normalise `\` → `/` in some contexts;
  `/\evil.com` parses as `//evil.com`)
- ASCII control characters `\x00`–`\x1F` and `\x7F` (NUL, tab, LF, CR,
  DEL, etc. — header-injection vectors)
- Anything the URL constructor parses to a different origin than the
  synthetic invalid origin used as the resolution base

The URL-constructor check is the authoritative guard; the prefix and
character checks are fast pre-filters that short-circuit common
attack shapes without paying the URL allocation.

Test coverage expands from 6 → 16 cases. New cases pin the
backslash, control-character, leading-whitespace, and positive-
character-set contracts. The URL-encoded-slash-in-query case
documents that we don't false-positive on `%2F` in query strings.

Closes A13-3 (open-redirect on post-login).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The dual-cookie machinery (crewli_app_token + crewli_portal_token,
Origin-based resolution) was load-bearing only when the second SPA
existed. apps/portal/ was deleted in WS-3 PR-B1; the resolver code
has been carrying dead branches since then. Collapse to one cookie.

Cookie name retained as crewli_app_token — no session breakage on
deploy. crewli_portal_token is fully purged from the server-side.

CookieBearerToken middleware:
- COOKIE_NAMES array → single COOKIE_NAME constant
- resolveCookieName method (Origin/Referer parsing, host+port
  matching against frontend_app_url/frontend_portal_url) → removed
- Body collapses to: skip if Authorization header present; else
  read crewli_app_token cookie and inject Bearer header

SetAuthCookie trait:
- COOKIE_MAP / resolveCookieName / originMatches → removed
- makeAuthCookie / forgetAuthCookie now take only $token; the
  cookie name is the trait's private constant

Five callers updated to drop the resolveCookieName($request) line
and the cookie-name argument: LoginController (3 sites),
MfaVerifyController (1 site), AuthRefreshController (1 site),
LogoutController (1 site), InvitationController (1 site — caller
list in the prompt missed this one but the same pattern applies).

frontend_portal_url config key retained (per Phase A directive Q1):
EmailChangeController, PasswordResetController, PersonController are
non-auth consumers that build per-app URL maps for outbound emails.
The map structure is now functionally redundant (production resolves
all FRONTEND_* env vars to the same host) but stays structurally
intact. Refactor tracked as TECH-FRONTEND-URL-CONSOLIDATE in the
upcoming docs commit.

HttpOnlyCookieAuthTest:
- Removed 4 dual-cookie tests (login_sets_portal_cookie_for_portal_origin,
  app_cookie_does_not_authenticate_portal_requests,
  portal_cookie_does_not_authenticate_app_requests,
  correct_cookie_authenticates_with_matching_origin)
- Renamed login_sets_app_cookie_for_unknown_origin →
  login_sets_app_cookie_regardless_of_origin; expanded to four
  Origin variants (none, app, unknown, foreign) — pins the new
  origin-agnostic contract
- Removed Origin headers from request calls in remaining tests
  (now meaningless)

Backend test count: 1491 → 1487 (-4 deleted, dual-cookie tests
encoding the obsolete contract). Pint clean. Larastan clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
deploy.sh referenced apps/portal which was deleted in WS-3 PR-B1; the
script has been broken in main since that merge (npm run build
-w apps/portal would fail). Collapse to a single-app build.

Changes:
- deploy.sh: replace dual-build block (build app + portal, verify both
  dist/) with single-app build (build app, verify dist/index.html)
- deploy/nginx/csp-portal.conf: deleted (content was identical to
  csp-spa.conf — verified before removal)
- deploy/README.md: replace "Portal (portal.crewli.app)" server-block
  section with "Legacy portal redirect" — a 301 server block
  template that redirects portal.crewli.app → crewli.app preserving
  the request URI. Notes that DNS retirement is a separate ops task

Out of scope: actually retiring the portal.crewli.app DNS record
(operational, tracked separately).

bash -n deploy.sh: clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
dev-docs/AUTH_ARCHITECTURE.md (v1.0 → v2.0):
- Title section updated to single-SPA / single-cookie reality
- Client Applications table collapsed to one row
- Cookie Specification table collapsed to one row (crewli_app_token)
- Token Lifecycle / Validation section: Origin-based resolution
  language removed; middleware described as origin-agnostic
- Cross-app isolation paragraph removed (no second app)
- Configuration Reference table marks FRONTEND_PORTAL_URL as legacy,
  pointing at TECH-FRONTEND-URL-CONSOLIDATE
- New §11 "History" preserves the pre-WS-3 dual-cookie context for
  future readers, mentions PR-B2a + PR-B2b roles in the unwind

dev-docs/BACKLOG.md — three new entries:
- TECH-FRONTEND-URL-CONSOLIDATE: refactor email controllers to drop
  per-app URL map (EmailChangeController, PasswordResetController,
  PersonController) — low priority, code-cleanliness only
- TECH-DOCS-APPS-PORTAL-PURGE: sweep apps/portal references from
  briefing/tooling docs (.cursor/, MASTER_PROMPT_*, SETUP, dev-guide,
  CLAUDE_CODE_TOOLING) — single chore(docs) PR, low priority
- OPS — DNS retirement of portal.crewli.app — operational task,
  deferred until traffic monitoring confirms zero usage

dev-docs/SECURITY_AUDIT.md:
- A13-1 narrative actualised: pre-WS-3 dual-cookie context kept as
  history, status flipped to RESOLVED (the localStorage→httpOnly
  migration shipped earlier in the consolidation arc)
- A13-3: status flipped to RESOLVED by WS-3 PR-B2b; description
  rewritten to reflect the new postLoginRedirect.ts validator and
  the 16 spec coverage
- Priority remediation table item 8 strikes through A13-3

Backend test suite: 1487 passed (unchanged from Commit 2 baseline).
Frontend: 223 passed (unchanged from Commit 1 baseline).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three load-bearing files still described the pre-WS-3 dual-SPA
reality. Surgical edits to reflect the single-SPA architecture
shipped in WS-3 PR-B (B1: portal moves; B2a: auth+routing
consolidation; B2b: server-side cookie consolidation).

CLAUDE.md:
- Quality-gates ts-reset bullet (line 27): "both SPAs" → "the SPA"
- Quality-gates Vitest bullet (lines 30-32): rewrite from "apps/portal
  has 113+ tests; apps/app currently has no Vitest setup (TECH-APP-VITEST)"
  to current truth: apps/app has Vitest with 213 tests as of PR-B2a.
  TECH-APP-VITEST is implicitly closed.
- Repository layout (line 44): drop apps/portal/ bullet; rephrase
  apps/app/ as the single workspace
- "Apps and portal architecture" → "App architecture": rewrite for
  single-workspace + two access modes. Login-based covers
  organizers + volunteers + crew + super_admin (context-routed
  in-app via useAuthStore.availableContexts); token-based covers
  artists, suppliers, press
- CORS subsection: collapse two-origin config to single origin
  (localhost:5174 dev, https://crewli.app prod). Preserve the
  existing crewli.nl marketing-only note

WS-TOOLING-001 sections (Larastan, Rector, Telescope tooling
configuration) verified untouched via `git diff CLAUDE.md`.

README.md (line 25): collapse the Applications table from two rows
(Organizer + Portal) to one (SPA). Adjust trailing sentence accordingly.

Makefile:
- .PHONY list: drop `portal`
- help echo: drop "make portal" line
- portal target: removed (the underlying `cd apps/portal && pnpm dev`
  would fail since the directory was removed in PR-B1)

Out of scope (deferred to TECH-DOCS-APPS-PORTAL-PURGE backlog item):
.cursor/ instructions, MASTER_PROMPT_*, dev-docs/SETUP, dev-docs/dev-guide,
dev-docs/CLAUDE_CODE_TOOLING. WS-3-SESSION-1C-AUDIT.md skipped (frozen
historical doc).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bert.hausmans merged commit 808ec212eb into main 2026-05-06 01:16:07 +02:00
bert.hausmans deleted branch feat/ws-3-pr-b2b-single-cookie-deploy 2026-05-06 01:16:07 +02:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: bert.hausmans/crewli#6