rastrillo / idear Public

Spec: the reconciliation route needs a token, so a claim-race loser must be invited first

Paul Campbell pushed by paul@keymail.dev 656234ac0338fbd021683c44be9b39d3df85bbf1 parent c26b405
2 files changed, +10 −2
  • .superpowers/sdd/2026-08-23-idear/progress.md +4 −0
  • docs/superpowers/specs/2026-08-23-idear-design.md +6 −2
diff --git a/.superpowers/sdd/2026-08-23-idear/progress.md b/.superpowers/sdd/2026-08-23-idear/progress.md
index 2bb4d3e..2b5a324 100644
--- a/.superpowers/sdd/2026-08-23-idear/progress.md
+++ b/.superpowers/sdd/2026-08-23-idear/progress.md
@@ -80,3 +80,7 @@ Task 4: dispatched (sonnet), BASE 5707e39 — middleware + the two identity adap
Task 4: implemented DONE (7fc0ee0). Controller re-verified: suite green, -race green.
Task 4: Ruling (spec/brief conflict on CarryToken): the METHOD form stands, against the spec's literal `idear.CarryToken(...)` package-function snippet. Three reasons: the spec snippet is internally inconsistent (the same section binds `r` to the roster), the method needs Config.Logger to report a form-parse failure and a package function cannot reach it, and Task 5 mounts whatever the plan's Interfaces block names. idear's copy of the spec corrected so the record matches what shipped; rastrillo's merged copy carries the same typo and is not worth a PR on its own — SKILL.md is what users read. Cost if wrong: a one-line signature change and one mount site.
Task 4: fix round 0 — none needed yet; reviewer dispatched.
+Task 4: review — spec ❌, quality Needs work: 1 CRITICAL, 2 Important, 5 Minor. Reviewer REPRODUCED the critical rather than arguing it.
+Task 4: Ruling (CRITICAL): canonicalise the Subject on BOTH sides, not one. Authorize normalises the address before writing Subject; rastrillo/auth mints the session as id.Address, and keymaildev/signin@v0.1.1 deliberately lowercases only the DOMAIN, preserving local-part case ("some ordinary mail systems are case-sensitive there"). So a visitor typing Alice@Corp.Test — which iOS does by default without autocapitalize="none" — gets a successful claim writing Subject "alice@corp.test", then a session with Subject "Alice@Corp.Test" that 404s on every guarded route forever, /members included. The instance is then permanently unusable and needs DB surgery. Fix: one normalizeSubject used at every Subject WRITE and in BySubject. It is a no-op on password's decimal ids and it makes the unique index on Subject actually mean one row per person. Writing the raw address (the literal spec text) is less catastrophic but still lets one human hold two rows. Cost if wrong: two case-variant addresses are treated as one member, which for a roster is the desired reading anyway.
+Task 4: Ruling (spec clarification, done by me): the spec claimed the reconciliation route "resolves the Claim-race loser". Too terse rather than false — the loser holds no token, so the route cannot help until someone invites them; then they sign in (their app user row exists) and redeem there rather than through sign-up, which would fail on the duplicate email. Spec corrected so Task 5 does not implement a promise it cannot keep.
+Task 4: fix round 1 dispatched (resume original implementer, opus), FIX_BASE 7fc0ee0
diff --git a/docs/superpowers/specs/2026-08-23-idear-design.md b/docs/superpowers/specs/2026-08-23-idear-design.md
index 2562726..1da7e80 100644
--- a/docs/superpowers/specs/2026-08-23-idear-design.md
+++ b/docs/superpowers/specs/2026-08-23-idear-design.md
@@ -296,8 +296,12 @@ public invitation routes are *for*:
- `POST /invitations/{token}` — **for a signed-in viewer with no Member
row**: given a valid token matching their session, or a roster with zero
rows, it writes the Member from the live session `Subject`. This heals
- the orphan, resolves the `Claim`-race loser, and is the only path by
- which a Member row is created from an already-live session.
+ the orphan and is the only path by which a Member row is created from
+ an already-live session. It resolves the `Claim`-race loser too, but
+ only *after* someone invites them: the route needs a valid token, and
+ a claim-race loser holds none. They can still sign in — their app user
+ row exists — so once invited they redeem here rather than through
+ sign-up, which would fail on the duplicate email.
Both public routes are rate-limited; `GET` is an unauthenticated lookup
of a secret and must not be a free oracle.