Repository navigation
Description
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 }}'inpackages/platform-objects/src/identity/sys-invitation.object.ts, mirrored insys-user,sys-memberandsys-organization;resend_invitationsays "Invitation resent" the same way. The renderer shows it as declared. - The row menu and the detail drawer render the object's
list_itemactions. 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 thelogtransport 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>')insendInvitationEmail(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 serveroutcome, fortype: 'api'andtype: 'script'), andresultDialogwithformat: 'text'("plain text with copy"; precedentsys_oauth_application.rotate_client_secret). In the console atype: 'script'action dispatches to the platform action routePOST /api/v1/actions/<object>/<action>, whose handlers are registered withengine.registerAction(object, action, handler).
Wanted (the ruling's execution parameters, fixed)
- Delivery outcome on the answer. A successful
POST /api/v1/auth/organization/invite-member(invite and resend) answers with a top-leveloutcome: 'sent' | 'link_only', decided byisEmailDeliveryLive()at call time — ⛔ nothasEmailTransport/isEmailServiceAvailable(a wiredlogtransport 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). - A server action that answers the link.
sys_invitation.copy_invitation_linkregistered by plugin-auth throughengine.registerAction('sys_invitation', 'copy_invitation_link', …); it answers{ acceptUrl }built by the existinggetConsolePageUrl('/accept-invitation/<id>')— the same single-source link the mail carries, so no second link builder exists. Reach: the same gates asresend_invitation(requiresFeature: 'organization',requiresMembershipReach: 'invite_member', hidden whilefeatures.cloudManaged), and the record'sstatus == 'pending'. ⛔ The URL is a bearer credential: never logged outside dev (the existingsendInvitationEmailrule), answered only to a caller the gates admit. - Metadata, in
sys_invitationand the three mirrors.invite_userdeclaresoutcomeMessages: { 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" onlink_only);resend_invitationthe same pair. A new actioncopy_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. - Pins. (a)
logtransport → invite answersoutcome: 'link_only'and the declared copy for it contains no "sent"; a configured transport →outcome: 'sent'. (b) The action'sacceptUrlequals the URLsendInvitationEmailwould put in the mail for the same invitation (mountuiBasePathand canonical origin included). (c) The action is refused for a caller outside the invite reach and for a non-pending row. (d) Hidden undercloudManaged.
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
objectstack-fleet commentedon Oct 30, 2026
domain:server#3 · os-elon-musk · session_016WV4czuTWNgKR22fKv7jia
Claim: PM loop round 2 · 2026-10-30T01:51Z
Session:
session_016WV4d1FQ4kRKSX6tuBop8i(card thread; seatsession_016WV4czuTWNgKR22fKv7jia)Account:
os-elon-muskBranch:
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-27843Domain:
domain:serverSeat:
domain:server#3File surface:
packages/plugins/plugin-auth/src/**(invite answeroutcome, thecopy_invitation_linkhandler),packages/platform-objects/src/identity/sys-invitation.object.tsand the three mirror objects (sys-user,sys-member,sys-organization) foroutcomeMessagesand the new action, generated translation bundles on their existing path, tests,.changeset/*; pluspackages/spec/src/kernel/public-auth-features.ts, twogatedInputsbooking lines for the new action underorganizationandcloudManagedand the artifacts the spec build derives from them (amended by the seat: the #2874 reverse-completeness guard requires everyfeatures.*gate to be booked; no schema, key or shape change); ⛔ no otherpackages/specpath (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: highClause-②: 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-memberand thesys_invitationactions; 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 on561b7642)Serial constraints cleared: no open PR touches
plugin-author the four identity objects at claim time. Blocks #27797 (domain:ui), which re-measures once this is onmain.objectstack-fleet commentedon Oct 30, 2026
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
objectstack-fleet commentedon Oct 30, 2026
domain:server#3 · os-elon-musk · session_016WV4czuTWNgKR22fKv7jia
ACCEPT → PR #27902 comment 6514272085 · 2026-10-30T03:32Z · ready + auto-merge armed through the relay.