feat(teams): reusable email-invitation module#139
Merged
Conversation
The pure leaf of the email-invitation feature, shipped under the existing
`@tangle-network/agent-app/teams` subpath (no drizzle/env/react/IO):
- Token/expiry/validation helpers lifted from creative-agent's validated flow:
`generateInvitationToken` (`inv_`-prefixed, base64url(32B), self-identifying so
it never collides with members-api tokens), `getInvitationExpiresAt` (7d),
`normalizeInvitationEmail`, `parseInvitationPermission` (assignable roles only),
`inviteUrlForToken`, and the status/email-status types.
- `renderInvitationEmail(input, { fromAddress })` — a transport-free template
returning `{ from, subject, html, text }` with html-escaping. The secret + the
Resend/SES call stay in the consuming app's seam; agent-app ships only the
deterministic body, so the template is reused fleet-wide without drift.
`InvitationPermission` aliases `AssignableWorkspaceRole` to keep one role vocab.
`createWorkspaceInvitationTable({ userTable, workspaceTable, organizationTable })`
under `@tangle-network/agent-app/teams/drizzle`. A SEPARATE, opt-in factory — NOT
folded into `createTeamTables` — so existing teams adopters (creative/legal/tax)
get no surprise `workspace_invitation` diff on their next drizzle-kit generate, and
an app already running that table adopts the factory with no row rewrite.
- Columns/enums/4 indexes mirror creative-agent's validated hand-rolled table.
- FKs wire into the product's user + workspace tables and the `organizations`
table from createTeamTables; call after createTeamTables so it's in scope (lazy
`.references()` closures make the ordering safe).
- Exports `WorkspaceInvitationRow`.
`createInvitationsApi(...)` under the new subpath `@tangle-network/agent-app/teams/invitations-api` (wired into tsup entries + package.json exports). The rich email-invitation lifecycle — create / list / resend / revoke / preview / accept over the dedicated `workspace_invitation` table — lifted out of any one app's route file, returning a discriminated `InvitationOutcome` the route adapter maps to its framework's response. Mirrors the members-api factory shape; app-specific I/O is injected as seams: - `sendInvitationEmail` (required) — the app's mail transport, returning a typed outcome. A failed send never blocks creation (emailStatus = 'failed', link still returned). - `enforceSeat` (optional, reused from members-api) — billing gate fired at create time only for a genuinely NEW seat (skips existing org members), so pending invites are counted at invite time, not surfaced as a wrong-time 402 on accept. - `memberSyncSeam.add` (optional) — fire-and-forget propagation on accept (creative had no sandbox sync; gtm needs it). Fires only when a new workspaceMembers row is inserted. Accept materializes organizationMembers + workspaceMembers (so members-api.listMembers still surfaces accepted members), keeps email-verified + same-email-collision guards, and keeps the originating token on the materialized row (inv_ tokens never collide).
…surface The React surface for the rich invitation flow, callback-driven and styled with the shipped `var(--*)` tokens (no app router/fetch/toast/UI-lib import): - `InvitationsPanel` — invite by email + role and an invitation history (pending/accepted/expired/revoked) with email-delivery status; pending rows expose copy-link / resend / revoke. Backed by `./teams/invitations-api` over fetch. - `InviteAcceptPage` extended for `expired` / `revoked` states, an optional verify-email branch (gated by `needsEmailVerification` / `onResendVerification` seams — apps without email verification omit them), and expiry display. - `MembersPanel` gains `showInviteForm` (default true) so an app driving invites through InvitationsPanel mounts the members panel list-only, avoiding two competing invite UIs. Contracts widened additively, so existing MembersPanel/InviteAcceptPage consumers are unaffected. Lazy entry added for InvitationsPanel.
- `invitations.test.ts` — pure helpers (token shape/uniqueness, expiry, email normalize, permission parsing) and `renderInvitationEmail` (brand from-address, subject, html-escaping). - `invitations-api.test.ts` — full lifecycle over the real in-memory SQLite harness: create (normalize, invalid-role 400, non-admin 404, duplicate 409, fail-soft email), enforceSeat (402 for a new seat, skipped for an existing org member), list (admin-only), resend/revoke (+ resend-after-revoke 409), preview, and accept (email-mismatch 403, unverified 403, happy path materializes membership + fires add sync + reachable via access, idempotent re-accept, and post-expiry 409). - Adds `emailVerified` to the shared teams db-helper users table (additive, defaulted; the other teams suites are unaffected). 82 teams tests green.
tangletools
approved these changes
Jun 23, 2026
tangletools
left a comment
There was a problem hiding this comment.
✅ Auto-approved PR — da3207e4
Blanket team auto-approval is enabled for this reviewer service.
The full PR reviewer audit still runs separately and will publish findings if it detects issues.
tangletools · auto-approval · reason: blanket_auto_approve · 2026-06-23T07:09:30Z
|
| State | Detail |
|---|---|
| Interrupted | webhook restarted |
No review verdict was produced for this run. Trigger a fresh review on the current PR head if the PR is still open.
tangletools · #139 · model: kimi-for-coding · updated 2026-06-23T07:25:02Z
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Extracts creative-agent's validated email-invitation system into agent-app as a reusable, seam-driven module under the existing teams capability, so creative/gtm/future apps plug it in fast. Server lifecycle + schema factory + pure email renderer + React UI. Additive only — no breaking changes to existing exports.
Closes #138.
Why
Invitations were diverged across apps: creative-agent had a production-ready dedicated
workspace_invitationlifecycle (status/expiry/resend/revoke/preview + email), while agent-app'smembers-apionly models an invite as a pendingworkspaceMemberrow and never sends email. This lifts the rich, validated flow into the shared lib behind injected seams, mirroring themembers-apifactory shape.What's in it
teams/invitations.ts(pure leaf) — token/expiry/normalize/parse helpers + transport-freerenderInvitationEmail(input, { fromAddress }). Ships under@tangle-network/agent-app/teams.teams/drizzle/invitations-schema.ts—createWorkspaceInvitationTable(...), a separate opt-in factory (not folded intocreateTeamTables, so other teams adopters get no surprise migration). Under…/teams/drizzle.teams/invitations-api.ts—createInvitationsApi(...)(create/list/resend/revoke/preview/accept) behind the new subpath…/teams/invitations-api. Seams:sendInvitationEmail(required, typed outcome, fail-soft),enforceSeat(optional, reused from members-api — fires only for a genuinely new seat),memberSyncSeam.add(optional, fired on accept).teams-react— newInvitationsPanel(invite form + history with resend/revoke/copy) +InviteAcceptPageextended forexpired/revokedand an optional verify-email seam;MembersPanelgainsshowInviteForm. Contracts widened additively.Seam design
The only external-boundary call (mail send) stays in the consuming app's
sendInvitationEmailseam and returns a typed{ succeeded }outcome; agent-app ships only the deterministic template (renderInvitationEmail). Secrets/branding (RESEND_API_KEY, from-address) never enter the library. Billing and sandbox sync are optional seams, so an app wires only what it needs.Verification
tsc --noEmit: clean across all new files (pre-existing, unrelatedeval-campaign/sandboxpeer-drift errors are untouched by this PR).teams/invitations-apientry builds todist.emailVerifiedcolumn in the shared db-helper changes nothing for them).Follow-ups (out of scope)
workspace_invitationrows toward billable seats.