Skip to content

invite-member answers outcome by mail delivery and sys_invitation gains a server copy_invitation_link action that answers the accept URL (ruling E on #27797) #27843

Closed

Description

@objectstack-fleet

director#1 · os-bill · session_01Utp6Cb7FDwjs2e5wWuRbRq

Ruling-ref: #27797 (comment) · letter E · 2026-10-30T01:41Z

Filing gate: ③ ruling-directed split. The maintainer ruled letter E on #27797 in the project decision thread 「决定无邮件时邀请如何分享链接」: 「同意你的建议,但是你应该等车道开发,着急的标记高优先」. The ruling comment on #27797 carries the full record; this card is its server half. ⛔ Not a claim. Lane domain:server; this card waits for a server seat (the maintainer's word: wait for the lane, no cross-lane exception). Grade p2 as the parent split (#27521 row 6); raise only on the maintainer's word.

Blocks: #27797 (domain:ui), whose remaining half is a live re-measure and a pin once this card is on main.

Business reason

An administrator who invites someone on a deployment with no live mail transport (the stock log transport, or no email service at all) is told "Invitation sent to …". Nothing left the server, and Setup → Invitations offers no way to hand the accept link over by hand, so the invitee never arrives and nobody knows why. The workspace Invitations page already shows the link after creation, but the governed invite action (role-cap option gate, cloudManaged gate, membership reach) is the metadata one, and that one dead-ends.

What was measured (origin/main 561b7642, source read)

  • The toast is invite_user.successMessage: 'Invitation sent to {{ result.email }}' in packages/platform-objects/src/identity/sys-invitation.object.ts, mirrored in sys-user, sys-member and sys-organization; resend_invitation says "Invitation resent" the same way. The renderer shows it as declared.
  • The row menu and the detail drawer render the object's list_item actions. No action answers the accept link.
  • The one reader of mail deliverability is AuthManager.isEmailDeliveryLive() (private, packages/plugins/plugin-auth/src/auth-manager.ts): IEmailService.isConfigured() === true, so the log transport is NOT live — the same rule sign-up uses. Nothing carries that fact to the client.
  • The accept link already has one server-side constructor: getConsolePageUrl('/accept-invitation/<id>') in sendInvitationEmail (auth-manager-organization-plugin.ts), the link the mail carries.
  • The spec already has what both halves need, so no spec change: ActionSchema.outcomeMessages (success copy per server outcome, for type: 'api' and type: 'script'), and resultDialog with format: 'text' ("plain text with copy"; precedent sys_oauth_application.rotate_client_secret). In the console a type: 'script' action dispatches to the platform action route POST /api/v1/actions/<object>/<action>, whose handlers are registered with engine.registerAction(object, action, handler).

Wanted (the ruling's execution parameters, fixed)

  1. Delivery outcome on the answer. A successful POST /api/v1/auth/organization/invite-member (invite and resend) answers with a top-level outcome: 'sent' | 'link_only', decided by isEmailDeliveryLive() at call time — ⛔ not hasEmailTransport / isEmailServiceAvailable (a wired log transport is not delivery). The mechanism (better-auth after-hook rewriting the success body, a wrap of the route in the auth plugin, or the action door) is the server seat's to choose; the contract is the one field name, beside the invitation row the answer already carries ({{ result.email }} keeps reading).
  2. A server action that answers the link. sys_invitation.copy_invitation_link registered by plugin-auth through engine.registerAction('sys_invitation', 'copy_invitation_link', …); it answers { acceptUrl } built by the existing getConsolePageUrl('/accept-invitation/<id>') — the same single-source link the mail carries, so no second link builder exists. Reach: the same gates as resend_invitation (requiresFeature: 'organization', requiresMembershipReach: 'invite_member', hidden while features.cloudManaged), and the record's status == 'pending'. ⛔ The URL is a bearer credential: never logged outside dev (the existing sendInvitationEmail rule), answered only to a caller the gates admit.
  3. Metadata, in sys_invitation and the three mirrors. invite_user declares outcomeMessages: { sent: 'Invitation sent to ${result.email}', link_only: 'Invitation created for ${result.email}. Mail is not configured on this server — use Copy invite link to share it.' } (wording is the seat's; it must never say "sent" on link_only); resend_invitation the same pair. A new action copy_invitation_link (label "Copy invite link", locations: ['list_item', 'record_header'], type: 'script', resultDialog: { title, description, format: 'text', fields: [{ path: 'acceptUrl', label }], acknowledge }). Generated translation bundles follow their existing path.
  4. Pins. (a) log transport → invite answers outcome: 'link_only' and the declared copy for it contains no "sent"; a configured transport → outcome: 'sent'. (b) The action's acceptUrl equals the URL sendInvitationEmail would put in the mail for the same invitation (mount uiBasePath and canonical origin included). (c) The action is refused for a caller outside the invite reach and for a non-pending row. (d) Hidden under cloudManaged.

Clause-②: no (no packages/spec change expected; the seat measures from the diff). Metadata-protocol: none expected. Review tier: by the touched paths (packages/plugins/plugin-auth, packages/platform-objects).

Dedupe

Read the open domain:server titles for invitation, invite-member, accept URL, outcome. #25335 (closed: Setup → Email states the live transport) and #8090 (closed, not_planned: workspace copied-link URL) do not cover this. New.

Links back: #27797 (ui half, blocked by this card), #27521 (row 6), #25335, #8090.
Size: S/M.

Activity

  1. added
    bugSomething isn't working
    area:identityLogin and identity — sign-up, sessions, organization membership, SSO
    domain:serverDomain lane — seat card indexed by label:pm:seat
    and removed on Oct 30, 2026
  2. objectstack-fleet commented on Oct 30, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · os-elon-musk · session_016WV4czuTWNgKR22fKv7jia

    Claim: PM loop round 2 · 2026-10-30T01:51Z
    Session: session_016WV4d1FQ4kRKSX6tuBop8i (card thread; seat session_016WV4czuTWNgKR22fKv7jia)
    Account: os-elon-musk
    Branch: claude/project-thread-7h7pzh (the card thread's platform-assigned branch — the recorded platform exception; amended by the seat when the thread reported it)
    Worktree: objectstack-issue-27843
    Domain: domain:server
    Seat: domain:server#3
    File surface: packages/plugins/plugin-auth/src/** (invite answer outcome, the copy_invitation_link handler), packages/platform-objects/src/identity/sys-invitation.object.ts and the three mirror objects (sys-user, sys-member, sys-organization) for outcomeMessages and the new action, generated translation bundles on their existing path, tests, .changeset/*; plus packages/spec/src/kernel/public-auth-features.ts, two gatedInputs booking lines for the new action under organization and cloudManaged and the artifacts the spec build derives from them (amended by the seat: the #2874 reverse-completeness guard requires every features.* gate to be booked; no schema, key or shape change); ⛔ no other packages/spec path (stop on breach; explain in the report)
    Container & model: M, mode:thread, model: opus (TIER_DEFAULT unless dispatch-gates --tier derives a mandate from the touched paths), effort: high
    Clause-②: yes (additive: a new answer field and a new action; the dev measures from the diff)
    Metadata-protocol: none
    Responsibility: the invite answer of POST /api/v1/auth/organization/invite-member and the sys_invitation actions; reached by an administrator inviting on a deployment without live mail.
    Thread-read: none
    Premise: holds (director-filed under the body's Ruling-ref: line, #27797 comment 6512831254, letter E, from the maintainer's ruling; the body's measurements are on 561b7642)
    Serial constraints cleared: no open PR touches plugin-auth or the four identity objects at claim time. Blocks #27797 (domain:ui), which re-measures once this is on main.

  3. objectstack-fleet commented on Oct 30, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 27843,
      "status": "done",
      "branch": "claude/project-thread-7h7pzh",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/27902",
      "session": "session_016WV4d1FQ4kRKSX6tuBop8i",
      "premise_still_valid": true,
      "summary": "invite-member (invite and resend) now answers a top-level outcome 'sent' | 'link_only' beside the invitation row, decided at call time by AuthManager.isEmailDeliveryLive() from the global after hook; refusals are untouched. sys_invitation.copy_invitation_link is registered by AuthPlugin.start() via engine.registerAction under com.objectstack.plugin-auth and answers { acceptUrl } from getConsoleUrl(invitationAcceptPath(id)), the path the invitation mail now composes from too; the handler enforces cloud-managed (409), session (401), invite reach on the invitation's organization via MEMBERSHIP_REACH.invite_member (403, before status) and pending status (409), and never logs the link. The four invite_user mirrors and resend_invitation declare outcomeMessages (link_only copy never says sent), the new action declares a text resultDialog, and zh-CN/ja-JP/es-ES leaves are hand-translated.",
      "tests": "At e4a4c0a6: plugin-auth `pnpm test` 204 files, 3885 passed / 10 skipped, typecheck exit 0; platform-objects `pnpm test` 100 files, 1432/1432, typecheck exit 0; spec public-auth-features.test.ts + ui/action.test.ts 177/177; spec check:generated 14/14 up to date. New pins: plugin-auth src/invitation-delivery-outcome.test.ts (27 tests in the run incl. the imported memory-engine file's; (a) link_only for no service / isConfigured false / member absent, sent for live, resend follows transport at call time, refusal carries no outcome; (b) acceptUrl equals the mail's data.acceptUrl incl. origin https://crm.acme.test + uiBasePath /console, never logged; (c) owner + delegated_admin admitted, plain member 403, other-org owner 403, no session 401, canceled/accepted 409, reach before status, unknown 404; (d) OS_CLOUD_MANAGED=1 409 invitationLink sentence; start() registers once under AUTH_PLUGIN_ID) and platform-objects src/identity/invitation-delivery-copy.test.ts (outcome copy on 4 mirrors + resend, link_only has no (re)sent, visible evaluated through @objectstack/formula for grades/status/sparse face/cloudManaged/organization, translated leaves). Ablations via scripts/ablation-replace.mjs (src resolution, no dist): outcome forced 'sent' -> 4 (a) red; reach forced open -> 3 (c) red; both restored, blob == HEAD, git diff HEAD empty.",
      "gates": "dispatch-gates --commands at e4a4c0a6: 87 families, 86 exit 0, `pnpm check:dual-build-cjs-loads` exit 3 = NOT MEASURED (needs whole-tree build; CI runs it). --ran: 87 accounted (86 run, 1 NOT-MEASURED, 0 UNRUN). pnpm lint exit 0. check-commit-card-trailers --range origin/main..HEAD exit 0 (4 commits). --tier: no path-derived mandate; Clause-2 suspect path packages/spec/src/kernel/public-auth-features.ts (two gatedInputs rows). PR check-runs: in_progress at report time.",
      "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm",
      "mcp_calls": "3 — mcp__github__issue_read x2 (card body, comments), mcp__github__pull_request_read x1 (precedent PR #27748 body); no write tool",
      "api_writes": "3 relay writes — pr_create (#27902), pr_request_reviewers (os-elon-musk), this comment; plus 4 git pushes (not REST)",
      "line_budget": "+1135 / -10 over 21 files (generated bundles included), under the 3000 human-merge threshold",
      "deviations": [
        "File surface: packages/spec/src/kernel/public-auth-features.ts touched (two gatedInputs booking rows), amended into the claim by the seat (option A) after platform-objects feature-gate-guard.test.ts red 2 cells.",
        "Clause-2 copied from the claim's `yes (additive …)` as the closed arm `yes (widening)`.",
        "Label spelled 'Copy Invite Link' (siblings' Title Case) where the card wrote 'Copy invite link'.",
        "requiresFeature organization is not re-checked server-side by the handler (UI gate; invitations exist only under the always-mounted organization plugin)."
      ],
      "files_changed": [
        ".changeset/27843-invite-link-only-outcome.md",
        "packages/platform-objects/src/apps/translations/{en,zh-CN,ja-JP,es-ES}.objects.generated.ts",
        "packages/platform-objects/src/identity/invite-outcome-messages.ts",
        "packages/platform-objects/src/identity/invitation-delivery-copy.test.ts",
        "packages/platform-objects/src/identity/sys-{invitation,member,organization,user}.object.ts",
        "packages/plugins/plugin-auth/src/auth-manager-after-hook.ts",
        "packages/plugins/plugin-auth/src/auth-manager-auth-instance.ts",
        "packages/plugins/plugin-auth/src/auth-manager-organization-plugin.ts",
        "packages/plugins/plugin-auth/src/auth-manager.ts",
        "packages/plugins/plugin-auth/src/auth-plugin.ts",
        "packages/plugins/plugin-auth/src/cloud-managed.ts",
        "packages/plugins/plugin-auth/src/invitation-delivery-outcome.ts",
        "packages/plugins/plugin-auth/src/invitation-delivery-outcome.test.ts",
        "packages/plugins/plugin-auth/src/invitation-link-action.ts",
        "packages/spec/src/kernel/public-auth-features.ts"
      ],
      "open_questions": [],
      "out_of_scope_findings": [
        "carrier: none · noted, not filed — a status 'pending' row past expires_at is answered a link the accept page refuses (ruling gate is status == 'pending'); PR Acceptance notes",
        "carrier: #27797 · the console workspace Invitations page builds its own accept link client-side; the ui half re-measures against this action",
        "carrier: none · noted, not filed — a composition mounting createIdentityObjectsPlugin without AuthPlugin gets one ADR-0110 boot warn (unbound script declaration sys_invitation:copy_invitation_link) and a 404 from the door"
      ]
    }

    Generated by Claude Code

  4. objectstack-fleet commented on Oct 30, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · os-elon-musk · session_016WV4czuTWNgKR22fKv7jia

    ACCEPT → PR #27902 comment 6514272085 · 2026-10-30T03:32Z · ready + auto-merge armed through the relay.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:identityLogin and identity — sign-up, sessions, organization membership, SSObugSomething isn't workingdomain:serverDomain lane — seat card indexed by label:pm:seatpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions