Agents Builders

Adopt Studio::Link in turf-monster: /l magic-link + referral cutover + managed email image

Archived
task-a54f8e18a68f

Created

Jun 21, 14:11

Started

Jun 22, 01:55

Completed

Jun 22, 03:01

DevOps handoff

Type

Feature

Shape

ui+db

Worktree Slug

standard-link-turf

Repositories

turf-monster

Release Train

Branch

feat/standard-link-turf

QA URL

Production URL

auth email referral migration

Acceptance Criteria

  • turf-monster magic-link backed by Studio::Link (store=:database), emails short /l/<token>; preserves contest landing + age-gate behavior
  • Referral links cut over from ?ref=<slug> to tokenized /l/<token>: per-user referral link + share UI + attribution; legacy ?ref= kept as back-compat capture
  • Magic-link banner becomes admin-managed (Studio::EmailImage) with the static asset as fallback; Email images link in admin hub

Expected Test Plan

  • unit
  • component
  • integration
  • e2e

Checks Run

  • [unit] Studio::Link token/consume/referral logic via suite + engine LinkToken 8/23
  • [component] magic_links_controller_test (interstitial/consume/EXPIRED) + InvitesController referral-only + /admin/email_images render
  • [integration] full minitest 1343 runs/6230 assertions/0 failures — /i referral-only (magic token rejected, POST /i 404) + expired-link rejection + email_banner + reference_attribution
  • [e2e] bin/e2e-parallel magic_link.spec.js + referrals.spec.js — 10 passed on isolated test stack

Stage Timeline

Who handled each stage, the time it took (measured), and the model / tokens / cost reported (best-effort) — plus who's on it right now. means the agent didn't report that metric.

No stage changes recorded yet.

Conversation

QA review feedback, agent handoffs, and follow-up notes for this task.

Handoff 6 days ago

PR #160 (turf-monster). Adopts Studio::Link: shared model + /i referral cutover (legacy ?ref= kept as back-compat) + admin-managed magic-link banner. Magic link stays /magic_link (already short; /l is TM's landing namespace). Full minitest 1340 green + 10 e2e (magic_link + referrals) green + live-smoked. DEPENDS on engine PR #7 gem publish — Gemfile points at the engine branch (github); flip to ~> 0.8 + bundle on release.

QA Feedback 6 days ago

QA BLOCK (rework). Strong adoption (draw_link_routes=false correctly set; in-change migrations; idempotent prefetch-safe referral, no double-credit) but 2 blockers + the engine-publish gate. (1) SECURITY — POST /i/:token (config/routes.rb:131-132) wires both /i verbs to the engine Studio::LinksController, whose #consume burns+signs-in ANY magic_link-kind token via the engine sign_up_new (bare User.new+save!): NO legal-age attestation, no reset_prior_session!, no reference cap. turf's compliance gate lives ONLY in MagicLinksController#sign_up_new:126-128. A user can request a magic link with the age box unchecked, then POST that token to /i/<token> and create an account bypassing the age gate — a second live login path the task forbids, on a mainnet-live underwriting-bound app. POST /i serves NO referral purpose (referral is GET-only by engine design). FIX: drop POST /i; constrain GET /i to referral-kind tokens only (404/redirect other kinds). (2) Carry-forward gate: expiry coverage was DELETED not replaced (old magic_link_test.rb held the only travel-past-TTL rejection; engine harness cannot host one). Add an integration test: mint token, travel past Studio.magic_link_ttl (15m), assert consume POST rejected. (3) Release-ordering hard gate: Gemfile pins studio-engine to unpublished branch feat/standard-link-model — cannot merge to main until engine #7 (task-f2ca3e97a730) merges + publishes 0.8.0 + re-pin to ~> 0.8. Sequence: fix 1+2 -> engine publish -> re-pin -> CI -> merge. Green CI only proves the engine BRANCH works.

Handoff 6 days ago

QA rework done (re #160). (1) SECURITY FIXED — dropped POST /i; GET /i now -> InvitesController (referral-kind ONLY: cookie+redirect). Engine Studio::LinksController#consume no longer reachable at /i, so a magic-link token cannot create an account there bypassing the legal-age gate. Non-referral token at /i rejected; POST /i unrouted (404). Regression tests added. (2) Expiry coverage restored (mint -> travel past Studio.magic_link_ttl -> consume rejected). (3) Engine-publish gate acknowledged. Full minitest 1343/0F + e2e 10 green. Commit cca5161.

QA Feedback 5 days ago

QA RE-REVIEW: APPROVE-PENDING-ENGINE-PUBLISH. SECURITY blocker RESOLVED — GET /i now routes to app-owned InvitesController#show (referral-kind + live only; magic_link tokens fall through to root redirect; NO sign-in, NO token burn, NO engine-consume delegation; draw_link_routes=false so no POST consume reachable outside MagicLinksController; POST /i returns 404). Tests prove every claim. Expiry test added (test/controllers/magic_links_controller_test.rb:266 — travel past Studio.magic_link_ttl -> rejected). NOT rework — held on DEPENDENCY: studio-engine 0.8.0 publish (Gemfile pins branch feat/standard-link-model). UNBLOCK RECIPE (one final commit once the gem ships): (1) operator publishes studio-engine 0.8.0; (2) repin Gemfile 'studio-engine','~> 0.8' + refresh lock; (3) FIX two stale comments still describing the REJECTED engine-controller-handles-/i design: config/initializers/studio.rb (draw_link_routes block) + test/integration/referral_link_test.rb header; (4) merge. Non-blocking follow-ups: orphaned magic_links table (no drop migration); referral_for lacks unique index on (linkable,kind,target).

Comment 5 days ago

Repin landed: Gemfile -> studio-engine ~> 0.8, lockfile resolves 0.8.0, 2 stale /i comments fixed (now describe InvitesController#show), full CI GREEN against the PUBLISHED 0.8.0 gem (lint/scan/test + 3 playwright shards). Avi pre-approved the logic; engine shipped -> reviewed. NOTE: turf-monster deploys via its OWN pipeline (turf-monster/bin/deploy), not mcritchie-studio bin/release.

Comment 5 days ago

PR #160 squash-merged to turf-monster main (2026-06-22) — code landed, repinned studio-engine ~> 0.8 (0.8.0). NOT deployed: turf-monster deploys via its own pipeline (turf-monster/bin/deploy → turf-monster-mainnet / app.turfmonster.media), which is a MAINNET prod deploy the operator runs explicitly. Task stays reviewed pending that deploy. (turf is NOT shippable via the mcritchie-studio bin/release conductor — single-app, mcr-only.)

Comment 5 days ago

SHIPPED to mainnet: turf-monster-mainnet v104 (app.turfmonster.media), 2026-06-22. PR #160 merged to turf main, deployed via turf-monster/bin/deploy. Preflight green (IDL hash already pinned — no on-chain change), migrations CreateStudioLinks + AllowNullImageCacheOwner ran in release phase, smoke /up 200. Studio::Link /l magic-link + referral + managed email image now LIVE on mainnet; studio-engine 0.8.0 in prod.

Sealed-bid sizing

Edit →

Alex (PM)

Avi (PO)

Dev

Actual