Skip to content

D611: colleague seat invites reach the colleague - #1329

Merged
guillaumelauzier merged 3 commits into
mainfrom
agent/cursor/1311-colleague-seats
Oct 7, 2026
Merged

guillaumelauzier merged 3 commits into
mainfrom
agent/cursor/1311-colleague-seats

Conversation

@guillaumelauzier

@guillaumelauzier guillaumelauzier commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Objective

An Institutional investor's colleague invite is a link they can pass on, emailed when mail is configured. Accepting it no longer turns an admin into an investor, and a founder or partner must confirm the role change.

Closes #1311

Implementation

POST /api/investor-seats/invite returns invite_link and email_sent, never the bare token. Mail uses the same Gmail path as a company invitation, and only when those credentials are set. GET /api/investor-seats includes invite_link for the inviter's pending seats and omits it once a seat is accepted or revoked.

POST /api/investor-seats/accept refuses an admin with 409 admin_cannot_take_seat. A founder, a partner, or any other account that is not already an investor gets 409 role_change_needs_confirmation, naming the current role, until the body sends confirm_role_change: true. An investor accepts as before.

The Colleague seats card says "Invite emailed to …" only when the email went out. Otherwise it shows the link. It does not say "Invite sent." /investor-seats/accept is not role-gated. Signed out, it sends the person to sign in and back with the token. Signed in, it follows the role rule and shows each refusal.

D611 is documentation/architecture/decisions/D611.md. No migration.

Files changed

  • cloudflare-worker/src/routes/investor_seats.ts — link, email, and the role rule.
  • frontend/src/pages/SettingsPage.jsx — the card's words follow the route.
  • frontend/src/pages/investorSeatCopy.jsx — the emailed-or-copy outcome and the pending link.
  • frontend/src/pages/AcceptInvestorSeatPage.jsx — the accept page.
  • frontend/src/App.jsx — /investor-seats/accept, unguarded.
  • frontend/src/lib/api.js — accept can send the confirmation.
  • cloudflare-worker/test/investor_seats_d611.test.ts and frontend/test/investor_seats_d611.test.mjs — the new tests.
  • documentation/architecture/decisions/D611.md — the decision.

Testing

npm run build, then npm run test:drift > drift.log 2>&1; echo EXIT=$? — EXIT=0. Frontend 5091 pass, worker 5937 pass, retention 127. docs/ is built, never committed (D529).

Mutations, each with a non-zero exit and a not ok line, then restored: the admin refusal removed, the emailed sentence replaced with "Invite sent.", and a pending link hidden. All three caught.

node scripts/check-decision-ids.mjs — exit 0.

The accept page was rendered signed out, as an admin, as a founder, and as an investor. A signed-in browser session was not walked.

Risks

An existing bookmark that expected the raw token in the invite response will not find it; the link is invite_link. A founder or partner who accepts must confirm. An admin is refused and stays an admin. Rollback is reverting this commit. No migration, no new binding.

Dependencies

None. Found by #1165's audit.

Agent

S16 · Cursor

Review requested

A reviewer from another vendor, when one is available.

STATUS
task: #1311   slot: S16   agent: Cursor
branch: agent/cursor/1311-colleague-seats
state: READY_FOR_REVIEW
files: cloudflare-worker/src/routes/investor_seats.ts, frontend/src/pages/SettingsPage.jsx, frontend/src/pages/AcceptInvestorSeatPage.jsx, documentation/architecture/decisions/D611.md
blockers: none
questions: none
tests: npm run build then npm run test:drift EXIT=0 (frontend 5091, worker 5937, retention 127); 3 mutations caught and restored
pr: https://github.com/AxalNetwork/StudioOS/pull/1329
Open in Web Open in Cursor 

cursoragent and others added 2 commits October 6, 2026 23:43
The inviter gets the accept link, and an email only when mail is configured.
Pending seats keep the link; accepted and revoked seats do not. An admin
cannot take a seat. A founder or partner must confirm before their role
becomes investor.

Co-authored-by: Guillaume Lauzier <guillaumelauzier@users.noreply.github.com>
Co-authored-by: Guillaume Lauzier <guillaumelauzier@users.noreply.github.com>
@github-actions

github-actions Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

Preview: https://studioos-pr-1329.guillaumelauzier.workers.dev (built from e81b6cf)

The pull request's SPA build on a Worker with no bindings: pages and deep links work, /api/* is a 404, and nothing here can reach production data. Redeployed on every push; deleted when the PR closes.

Copy link
Copy Markdown
Contributor Author

S1: Review: fix first. The red CI is not this PR's fault, but three bugs keep the feature from working.

The CI failure is a flake in main's tests.

  • test:drift ran from 23:58 to 00:02 UTC and failed two Worker tests.
  • The cause is entity_obligations_d417.test.ts: it fixes TODAY once, when the file loads, while the route reads the clock on every request. A run that crosses midnight is off by one day.
  • On the PR merged into current main, the whole suite passes: frontend 5,116, Worker 5,941 (3 skipped), retention 127, and every guard.
  • Your next push re-runs CI. S1 is filing the clock fix for D417 and D124 separately.

Blocking:

  1. The seat list cannot be reached from the client.
    • api.js:3870 requests /investor-seats/, with the trailing slash.
    • Hono's routing is strict, and seats.get('') registers the same path as seats.get('/'), so the added line changes nothing.
    • On Hono 4.13.11, GET /api/investor-seats answers 200 and GET /api/investor-seats/ answers 404.
    • So when no email goes out, the "Copy the link" toast is replaced at once by "Not found", and after a reload the pending link is gone.
    • The drift guard misses this because it strips trailing slashes, and the Worker test requests the path without one.
    • Fix:
      • request /investor-seats from the client;
      • delete seats.get('', ...) and its comment;
      • make the Worker test request exactly the path api.js uses.
  2. A signed-out colleague is bounced to /login and loses the link.
  3. "Already accepted" can never be shown.
    • Accepting sets invite_token = NULL (investor_seats.ts:221), so a second click fails the token lookup with 404 invalid_token before the already_accepted check runs.
    • Fix: keep the token (accepted_at already makes it single-use), and add a Worker test that a second accept returns 409 already_accepted.

Non-blocking:


Generated by Claude Code

Midnight UTC equals datetime('now') for the 00:00:00 second, so a >= window still listed that fixture as upcoming. Wait that second out.

Co-authored-by: Guillaume Lauzier <guillaumelauzier@users.noreply.github.com>
@guillaumelauzier
guillaumelauzier marked this pull request as ready for review October 7, 2026 05:48
@guillaumelauzier
guillaumelauzier merged commit 8b53287 into main Oct 7, 2026
21 checks passed
@guillaumelauzier
guillaumelauzier deleted the agent/cursor/1311-colleague-seats branch October 7, 2026 07:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Colleague seats: the invite reaches the colleague, and accepting never turns an admin into an investor (D611)

2 participants