Skip to content

org: invite an email address to an organization - #2561

Merged
gtrrz-victor merged 9 commits into
mainfrom
alex/cor-1744-cli-invite-commands
Sep 25, 2026
Merged

gtrrz-victor merged 9 commits into
mainfrom
alex/cor-1744-cli-invite-commands

Conversation

@khaong

@khaong khaong commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

https://entire.io/gh/entireio/cli/trails/1401

Why

COR-1744. An org can only grant membership to someone the control plane can
already name: entire org grant add takes a provider handle, so it cannot
reach a colleague who has never signed in. Invitations close that gap — an
email address is enough — and this adds the CLI commands for managing them.

What

entire org invite send <org> --email <email> [--role owner|admin|member] [--json]
entire org invite list <org> [--status open|accepted|revoked|expired|all] [--json]
entire org invite revoke <org> --email <email>

invite is its own subtree under entire org, beside grant: send
creates an invitation, and list and revoke manage the ones already sent.
The org is addressed by name; an unknown
name reports no org named "x" (run `entire org list` to see org names).
The commands reuse the org resolution, paging, revoke and --json plumbing
the grant subtree already has, and validateRole is generalized to
validateChoice so --role and --status share one check.

The invitee accepts through the link in the invitation email; there is no
CLI accept command.

Notes on decisions a reviewer may want to check:

  • send and revoke both take the invitee as --email. The org is the
    one positional on every invite verb. The revoke route addresses an
    invitation by ULID, so revoke resolves the address through the listing.
    It searches only the open invitations, so re-revoking an accepted one is
    never mistaken for the request.
  • Revoking needs the org. The API's only revoke route is
    DELETE /orgs/{orgId}/invitations/{id}; there is no org-less invitation
    lookup, so revoke cannot drop <org> without a server change.
  • The invitation ID is shown but not taken. invite list leads with an
    ID column and invite send prints the new invitation's ID, as the org
    commands do for org IDs.
  • No client-side role gate. Who may invite with which role stays the
    server's decision, so a 403 reaches the user in the server's own words. The
    CLI checks only that --role spells a value the API declares.
  • revoke has no --json. It revokes with a 204 and has no object to
    render. TestControlPlaneJSONFlag_OnlyOnHonoringCommands pins the rule that
    the flag exists only where it is honored; grant remove is the same shape.
  • AdditionalProps is blanked on every Invitation. An invitation is an
    object with an accept token. Its modeled fields carry none, but ogen
    round-trips any undeclared response property verbatim into --json, so the
    create path and every item of the list path clear the bag.

No jurisdiction handling is added. crossjuris.Transport already sits under
every coreapi constructor and follows a 421 once, so these verbs inherit it.

The vendored spec refresh these commands need has landed on main
separately, so this diff is handwritten code only: 258 production lines and
378 test lines.

Verification

  • mise run check — pass (format, lint, unit, integration, canary)
  • mise run lint — 0 issues

Tests covering the invite behavior:

behavior test
AdditionalProps blanking on create / list TestOrgInviteSend_JSONDropsUnmodeledResponseProperties, TestOrgInviteList_JSONDropsUnmodeledResponsePropertiesOnEveryItem
reporting the stored role on resend TestOrgInviteSend_ResendReportsTheStoredRole
the default --role value TestOrgInviteSend_SendsTheDefaultRoleWhenFlagOmitted
the ID in the list table and send output TestOrgInviteList_ListsAndFiltersByStatus, TestOrgInviteSend_CreatesAndReportsTheRole, TestOrgInviteSend_ResendReportsTheStoredRole
--email is required on send and revoke TestOrgInvite_SendAndRevokeRequireEmail
server-side role policy (403) TestOrgInviteSend_ForbiddenRoleSurfacesTheServerMessage
--status validation TestOrgInviteList_RejectsAnUnknownStatus
the open-only filter in the email lookup TestOrgInviteRevoke_ResolvesAnEmailThroughTheOpenListing
name-only unknown-org message TestOrgInvite_UnknownOrgNameHintsAtNamesOnly
Invitation/Membership enum loosening TestListOrgInvitations_UnknownEnumValuesPassThrough, TestListOrgMembers_UnknownEnumValuesPassThrough

Follow-ups

  • The issue spells these entire grant org invite. They ship as
    entire org invite …, alongside entire org grant ….
  • entire org invite list walks every page. The grant listings do the same,
    so the --all/--limit/--page-size treatment the repo listings have is
    worth giving them all at once rather than this one verb.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PLWcdMjoLpAaiATYzY3v74


Note

Medium Risk
Touches org membership and bearer invitation tokens; mitigations are explicit but credential handling and authorization still depend on correct CLI and API behavior.

Overview
Adds org invitation support to the CLI so membership can be granted by email before the invitee has a provider account, plus an invitee path to accept.

Under entire org grant, new verbs invite, invites, and uninvite call the control-plane invitation APIs (create/resend, paginated list with --status, revoke by ULID or open invitation email). They hang off the shared grant subtree via a new extraCmds hook on grantTarget, reuse org ref resolution and listing helpers, and validateChoice (renamed from validateRole) for --role and --status. uninvite has no --json; invite, invites, and org join do, per the existing --json registry test.

entire org join <token> accepts an invitation for the logged-in user. Invitation tokens are treated as secrets: errors are redactToken-scrubbed, dash-prefixed tokens get a custom flag-parse message, and AdditionalProps are cleared on invitation/join responses so accept tokens cannot leak through --json.

Broad integration tests cover API wiring, resend messaging, server-side role policy (403), email→ULID uninvite, and token non-disclosure. UPSTREAM.md notes invitation list fields in the read-model enum loosening story.

Reviewed by Cursor Bugbot for commit 206c9f4. Configure here.

@khaong
khaong requested a review from a team as a code owner September 23, 2026 04:18
Copilot AI lite review requested due to automatic review settings September 23, 2026 04:18

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Critical token-handling findings remain unresolved in org join.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 2 High severity · 1 Low severity

Open (3)
What changed in this PR

Adds organization invitation management and token-based invitation acceptance to the CLI, alongside a refreshed Core API client.

Changes:

  • Adds organization invite, list, revoke, and join workflows.
  • Regenerates API models and adapts related CLI integrations.
  • Adds compatibility, pagination, and token-safety tests.
File Summary
internal/​coreapi/​UPSTREAM.md Documents API compatibility workarounds; command path needs correction.
internal/​coreapi/​spec/​normalize.go Preserves forward-compatible read-model values.
internal/​coreapi/​repo_readiness_test.go Updates repository response-wrapper tests.
internal/​coreapi/​oas_validators_gen.go Regenerated API validators.
internal/​coreapi/​oas_security_gen.go Regenerated security mappings.
internal/​coreapi/​oas_request_encoders_gen.go Regenerated request encoders.
internal/​coreapi/​oas_parameters_gen.go Regenerated request parameters.
internal/​coreapi/​oas_operations_gen.go Regenerated operation definitions.
internal/​coreapi/​oas_interfaces_gen.go Adds regenerated response interfaces.
internal/​coreapi/​oas_defaults_gen.go Regenerated request defaults.
internal/​coreapi/​oas_cfg_gen.go Regenerated configuration.
internal/​coreapi/​client_test.go Tests unknown enum compatibility.
cmd/​entire/​cli/​repo.go Handles wrapped repository responses.
cmd/​entire/​cli/​repo_readiness.go Converts created repositories for readiness polling.
cmd/​entire/​cli/​repo_protection.go Uses updated branch-rule models.
cmd/​entire/​cli/​repo_protection_test.go Updates branch-protection fixtures.
cmd/​entire/​cli/​repo_mirror.go Uses refreshed mirror models.
cmd/​entire/​cli/​repo_mirror_test.go Updates mirror assertions.
cmd/​entire/​cli/​repo_mirror_request_test.go Updates asynchronous mirror tests.
cmd/​entire/​cli/​repo_mirror_probe.go Handles refreshed mirror results.
cmd/​entire/​cli/​project.go Handles wrapped project responses.
cmd/​entire/​cli/​org.go Registers joining and adapts organization responses.
cmd/​entire/​cli/​org_join.go Accepts invitations; critical findings remain for JSON token leakage and redaction edge cases.
cmd/​entire/​cli/​org_join_test.go Tests token handling; wording cleanup is needed.
cmd/​entire/​cli/​org_invite.go Implements invitation management commands.
cmd/​entire/​cli/​org_invite_test.go Tests invitation workflows.
cmd/​entire/​cli/​grant.go Adds invitation commands and shared validation.
cmd/​entire/​cli/​grant_test.go Tests grant validation changes.
cmd/​entire/​cli/​corecmd_json_flag_test.go Verifies JSON flag coverage.
Files not reviewed (7)
  • internal/coreapi/oas_cfg_gen.go: Generated file
  • internal/coreapi/oas_defaults_gen.go: Generated file
  • internal/coreapi/oas_interfaces_gen.go: Generated file
  • internal/coreapi/oas_operations_gen.go: Generated file
  • internal/coreapi/oas_parameters_gen.go: Generated file
  • internal/coreapi/oas_request_encoders_gen.go: Generated file
  • internal/coreapi/oas_security_gen.go: Generated file

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread cmd/entire/cli/org_join.go Outdated
Comment thread cmd/entire/cli/org_join.go Outdated
Comment thread internal/coreapi/UPSTREAM.md Outdated
@khaong

khaong commented Sep 23, 2026

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

@khaong
khaong force-pushed the alex/cor-1744-cli-invite-commands branch from 69b6a7e to 992a300 Compare September 23, 2026 05:50
@khaong

khaong commented Sep 23, 2026

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

@khaong

khaong commented Sep 23, 2026

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

khaong and others added 3 commits September 24, 2026 15:19
…tion

Add the four verbs that let an org grant membership to someone the control
plane cannot name yet. `grant add` needs an existing provider account; an
invitation needs only an email address.

  entire org grant invite <org> <email> [--role owner|admin|member] [--json]
  entire org grant invites <org> [--status open|accepted|revoked|expired|all]
  entire org grant uninvite <org> <email|id>
  entire org join <token>

The first three sit in the existing `grant` subtree, beside add/list/remove,
and reuse its resolution, listing, revoke and --json plumbing. `join` hangs
off `entire org` instead, because it acts on the caller's own account: the
caller is the invitee, not a manager addressing someone else.

`uninvite` accepts an email address as well as an invitation ULID, resolving
it through the open listing, because the revoke route addresses an invitation
by ULID and nobody reads a ULID out of an email. It searches only the open
invitations, so re-revoking an accepted one is not mistaken for the request.

Who may invite with which role stays the server's decision. The CLI checks
only that --role spells a value the API declares, so one place decides that
an admin may not mint owners, and a 403 reaches the user with the server's
own words.

The invitation token is a bearer credential, so no stream this code writes
may carry it. The accept response holds no token, so success and --json are
safe by construction. Two paths could reintroduce it, and both are closed and
tested: a flag parse error, which quotes the argument cobra could not read,
and a server problem detail that names what it rejected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PLWcdMjoLpAaiATYzY3v74
AcceptedInvitation/AcceptInvitationOK (and the nested Membership) carry
an ogen additionalProperties bag that round-trips any response
property the schema doesn't declare, verbatim, into --json output.
Blank it before rendering rather than trust the endpoint's contract
never grows one.

redactToken substituted a fixed "<redacted>" placeholder. The token
schema only requires a non-empty string, so a caller can supply that
exact placeholder text as their token; replacing a token with text
equal to itself is a no-op and left it sitting in the message. Delete
the token instead of substituting a lookalike.

Also fixes a doc typo: internal/coreapi/UPSTREAM.md named the
nonexistent `entire grant org invites`; the shipped verb is
`entire org grant invites`.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PLWcdMjoLpAaiATYzY3v74
Invitation (and so CreateOrgInvitationCreated/CreateOrgInvitationOK)
carries the same ogen additionalProperties bag blanked on
AcceptedInvitation/AcceptInvitationOK in org_join.go. An invitation is
the one other object in this PR with an accept token, so the same
defense applies to the whole family rather than a new one: the create
path blanked it on the single returned Invitation, and the list path
blanks it on every item, since it loops.

TestOrgInvite_JSONDropsUnmodeledResponseProperties and
TestOrgInvites_JSONDropsUnmodeledResponsePropertiesOnEveryItem mirror
TestOrgJoin_JSONDropsUnmodeledResponseProperties's shape; both were
checked to fail against the pre-fix code.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PLWcdMjoLpAaiATYzY3v74
@khaong
khaong force-pushed the alex/cor-1744-cli-invite-commands branch from 44b961d to 2bfba08 Compare September 24, 2026 05:26
@khaong
khaong changed the base branch from main to alex/cli-vendored-spec-split September 24, 2026 05:26
@khaong

khaong commented Sep 24, 2026

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

@khaong

khaong commented Sep 24, 2026

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 206c9f4. Configure here.

gtrrz-victor added a commit that referenced this pull request Sep 24, 2026
coreapi: land vendored Core spec refresh before #2561
Base automatically changed from alex/cli-vendored-spec-split to main September 24, 2026 12:19
Soph
Soph previously approved these changes Sep 24, 2026
The invitation verbs lived under the grant subtree as `org grant invite`,
`org grant invites` and `org grant uninvite`. They now form their own
subtree: `entire org invite <org> <email>` sends an invitation, with
`invite list` and `invite revoke` managing sent ones. The grant subtree's
extraCmds hook, added only for these verbs, is removed.

Drop `entire org join`; accepting an invitation is not a CLI flow.

The invite commands address the org by name, so their help and their
unknown-org error no longer mention ULIDs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Entire-Checkpoint: 01M3C16S05RRS6XVCNB110S4HA
@gtrrz-victor gtrrz-victor changed the title org: invite an email address to an organization, and accept an invitation org: invite an email address to an organization Sep 25, 2026
gtrrz-victor and others added 2 commits September 25, 2026 15:04
`entire org invite` becomes a plain group, and creating an invitation
moves to `entire org invite send <org> --email <email>` beside `list`
and `revoke`. With no verb on the group, an org named `list` or `revoke`
no longer collides with a subcommand, so the --json test's group-as-verb
walk is dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Entire-Checkpoint: 01M3C90EN60B0VC3FVAB81C6SR
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Entire-Checkpoint: 01M3C9S5WD81VYSG834QWHD43M
@gtrrz-victor
gtrrz-victor force-pushed the alex/cor-1744-cli-invite-commands branch from 819ad24 to f6e7953 Compare September 25, 2026 13:10
gtrrz-victor and others added 2 commits September 25, 2026 15:30
`invite revoke` accepts an invitation's ID as well as its email, but the
table hid it, so it was only reachable through --json. The list table
now leads with an ID column, and send's success line ends with the ID.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Entire-Checkpoint: 01M3CC5YPSMKWQWAH435FH2N4A
`entire org invite revoke <org> --email <email>` matches `invite send`:
the org is the one positional and the invitee is a required flag. The
invitation ID is no longer accepted; the CLI still resolves the email to
the open invitation's ID, which the revoke route needs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Entire-Checkpoint: 01M3CGMWY935HXKGKECZ7KTRH9
@gtrrz-victor
gtrrz-victor merged commit 320447b into main Sep 25, 2026
18 checks passed
@gtrrz-victor
gtrrz-victor deleted the alex/cor-1744-cli-invite-commands branch September 25, 2026 15:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

4 participants