Repository navigation
Description
Part of #11586 (epic — ruling and V1 scope live there; read it first).
Deliverable
The backend half of the universal org join link, V1:
Data — new platform object (suggested name sys_join_link; naming per sibling conventions in packages/platform-objects/src/identity/): organization_id (lookup sys_organization), token (opaque, ≥128-bit random, URL-safe), role semantics PINNED to member (not authorable in V1 — a constant, not a field, unless the schema layer makes a fixed field cheaper to audit), expires_at (default: creation +7d), revoked_at (nullable), max_uses (nullable), use_count, created_by. Exactly ONE active (non-revoked, non-expired) link per org — enforce at the write endpoint AND with a partial unique index if the driver supports it. Generic CRUD suppressed (managed via endpoints only, like sys_invitation).
Endpoints (mounted beside the better-auth organization surface; exact mount point is implementation detail):
- create / rotate — org owner/admin only (same permission predicate family as invite-member); rotate = revoke current + mint new, atomic.
- revoke — org owner/admin only.
- token info (GET by token) — unauthenticated-safe: returns org display name + validity state ONLY; tokens must not be enumerable; rate-limit this and join (a join link is a bearer credential — probing resistance matters).
- join (POST, by token) — requires an authenticated session with verified email (posture per better-auth GHSA-fmh4-wcc4-5jm3 / PR check-engine-double-contract cannot read three mock-initializer spellings, and one of them hides a live unguarded engine delete double #9877; the epic records this as ruled). Validates live: not expired, not revoked, uses remaining. Membership write goes through better-auth's server-side addMember API (
auth.api.addMember— the server-only endpoint measured in the [finding]sys_member"Add Member" action targetsPOST /organization/add-member, which better-auth 1.7.0-rc.2 never mounts (server-only) — on multi-org there is NO remaining UI path to attach an existing user to an org #9941 thread) so org hooks, audit, and limits fire — NEVER hand-insertsys_memberrows. Already-a-member ⇒ idempotent success. Distinct error codes for expired / revoked / exhausted / invalid.
Contract bookkeeping — Clause-② YES (this widens the public API surface): book any new feature-gated inputs in PUBLIC_AUTH_FEATURES (packages/spec/src/kernel/public-auth-features.ts, organization entry) per the bidirectional feature-gate guard; new error codes go wherever the error-code ledger requires. Expect the needs:contract-review chain regardless of dispatch tier (subagent tier is not self-provable).
Tests — authz matrix (stranger / plain member / admin / owner) on all mutating endpoints; expiry / revocation / rotation / exhaustion; verified-email gate on join; an assertion that join goes through the addMember path (not a raw insert); idempotent re-join; token-info leak surface (no org internals for invalid tokens).
Token storage note (PM suggestion, dev judges): retrievable-by-admin plaintext in the org-scoped row is acceptable for V1 (member-grade, expiring, revocable — unlike an OAuth client_secret); if hashed-at-rest with show-once is trivially compatible with the console's "copy link anytime" UX, flag the option in the report rather than deciding silently.
Non-goals (V1, ruled on the epic)
Domain allowlists, approval queue, per-link roles, multiple concurrent links, SCIM interplay.
Consumers to unblock
objectui (console dialog + /join/:token route) and cloud (control-plane org-scoping + E2E) sub-issues carry Blocked-by: pointing here — their cards state what they need: stable endpoint paths + error codes in this card's PR body.
Activity
os-zhuang commentedon Aug 24, 2026
Claim: PM loop round 1 (epic #11586, maintainer direct-dispatch channel)
Session:
session_71836b57-5db6-459d-ae4d-c6d9d7dd2cc6Branch:
claude/issue-11587-org-join-link-v1Worktree:
objectstack-issue-11587Domain:
domain:servicesFile surface:
packages/plugins/plugin-auth/src/**(endpoints + wiring),packages/platform-objects/src/identity/**(new object + tests),packages/spec/src/kernel/public-auth-features.ts(gate booking; error-code ledger if new codes require it) (stop on breach; explain in the report)Container & model: L,
mode:subagent,model: claude-fable-5— Clause-② mandatory tier (widens the public API surface);dispatch-gates --tieris a floor, the content limb mandates the ceiling hereClause-②: yes
Serial constraints cleared: open PRs scanned — #11560 / #11406 touch
packages/specparse layer (different files than the ledger; no overlap), no open PR touches plugin-auth or platform-objects identity (#11530 and #11562 both MERGED — same-day churn onsys-member.object.ts/public-auth-features.ts, so base on fresh origin/main and re-read those files). Sibling sub-issues objectui#5961 / cloud#1633 arepm:blockedon this card — never same batch.Note:
mode:subagenttier is not self-provable ⇒needs:contract-reviewchain applies at ACCEPT regardless of the dispatched tier.Audit (direct-dispatch): maintainer instruction, 2026-08-24 chat, quoted verbatim: 「可以,你开一个epic任务,用objectstack项目经理技能统一派发处理」. Epic delegation recorded on the parent; the spec-adjacent booking is dispatched by the epic PM under that instruction rather than transferred to the spec seat (deviation recorded on the parent).
os-zhuang commentedon Aug 24, 2026
Container & model correction:
model: claude-fable-5→model: opus, availability exemption.Reading: three consecutive dispatch attempts (one fresh + two SendMessage revivals, 5-minute backoff between) each terminated mid-run by API
529 Overloaded, with ZERO durable output (no worktree, no remote branch — verified before each revival). fable is measurably unavailable right now; per the maintainer's standing exemption (2026-08-13, verbatim): 「fable 如果用完了,可以用 opus」. Not downgrading further.Clause-② remains
yesand is compensated as designed: theneeds:contract-reviewchain at ACCEPT runs atCONTRACT_REVIEW_TIERregardless of the implementation tier (the review exists precisely to compensate a below-floor dispatch). Re-dispatching a fresh agent now; branch/worktree names unchanged from the claim.os-zhuang commentedon Aug 24, 2026
Dispatch paused — upstream provider incident. Four consecutive dev runs (3× fable, 1× opus after the recorded downgrade) were killed mid-run by API 529 Overloaded with zero durable output. status.claude.com has an active incident: "elevated errors on requests to Claude Mythos 5, Claude Fable 5, Claude Opus 5, and other Claude models" (Investigating 05:06 UTC, Identified 05:27 UTC, 2026-08-24). This is infra-wide, not tier-specific — the earlier fable→opus downgrade reading is superseded by this one; the model choice will be re-evaluated at resume (fable if available, per the original Clause-② mandate). Claim stands; a recovery timer is set to resume dispatch when the incident clears.
os-zhuang commentedon Aug 24, 2026
Resuming dispatch on a live probe reading (not on the status page, which still shows the 05:27 UTC "Identified" entry): a minimal fable-tier subagent round-trip just succeeded, so the channel is usable — the incident is an elevated error RATE, not an outage. Model returns to
claude-fable-5per the Clause-② mandate, as promised in the pause comment. The dev is instructed to push early and commit/push after each unit so progress survives any further intermittent kill.feat(platform-objects,spec): sys_join_link object + feature-gate book…
feat(plugin-auth): org join-link endpoints — create/rotate/revoke/get…
test(plugin-auth): join-link authz matrix, lifecycle, verified-email …
fix(platform-objects,plugin-auth): param-less rotate/revoke actions v…
os-zhuang commentedon Aug 24, 2026
PAUSED by the maintainer — the audience assumption behind this epic was corrected.
Maintainer, 2026-08-24 chat, quoted verbatim: 「协作者不会多,我在云端手工加入都可以,主要是 业务用户怎么方便的注册进来,你先和我讨论方案。」
This epic builds the cloud-organization join link — membership in the control plane, i.e. the COLLABORATOR layer (env list, development, billing visibility). The maintainer states that layer is low-volume and manual invitation is acceptable, so the epic's justification is withdrawn for now. The real need is the business-user layer: convenient self-registration into a tenant ENVIRONMENT (e.g.
steedos.objectos.ai), which is a different runtime, different tables, different auth surface — the code here does not transfer.Dispatch stopped mid-run; branch
claude/issue-11587-org-join-link-v1exists with partial work and is left in place. No PR was opened.Restart-when: the business-user onboarding design (under discussion with the maintainer 2026-08-24) concludes and the maintainer rules that a cloud-org join link is still wanted.
Live readings that informed the correction (measured on
steedos.objectos.ai/api/v1/auth/config, 2026-08-24):emailPassword.disableSignUp=false,requireEmailVerification=false,socialProviders=[objectstack-cloud],features.ssoEnforced=false,tenancyPosture=single. Env-side self-registration is open and unverified today, andreconcileMembership(policy defaultauto) auto-binds each new user into the env's single org — that is the surface the business-user design must address.os-zhuang commentedon Aug 24, 2026
Closing not planned — the hold's
Restart-when:condition has fired, and the answer is "not wanted now".That condition was: the business-user onboarding design concludes and the maintainer rules whether a cloud-org join link is still wanted. The design concluded today as #11723 (audience posture, ruled and dispatched). The maintainer's ruling on the collaborator layer, quoted verbatim from the 2026-08-24 chat, is:
Manual invitation covers the collaborator (cloud-organization) layer at current volume, so this epic has no pull. Per the maintainer's standing guidance (2026-08-16, verbatim: 「我建议有些暂时不准备开发的应该直接关闭。」), work with no firable restart condition is closed rather than held — the card IS the record, and reopening is free.
Preserved for whoever revives this: the V1 token shape reasoned out here (one active link per org, role pinned to
member, default 7-day expiry, revoke/rotate, optional max-uses, membership written through the governedaddMemberpath, verified-email posture per better-auth GHSA-fmh4-wcc4-5jm3) is directly reusable if alinkvalue is ever added to the audience-posture vocabulary in #11723. Branchclaude/issue-11587-org-join-link-v1holds partial work; no PR was opened.