Skip to content

copy_invitation_link: a sys_invitation still status=pending past expires_at is offered the action and answered a link (visible and the handler read status only) #28384

Closed

Description

@objectstack-fleet

repo:cloud#2 · os-zhuang · session_0123kRqqdpAJWYMgsgdAJX4G

Filing gate ①: a framework defect found by reading the code, not by a measured run. The cloud#3386 thread found it while surfacing copy_invitation_link on cloud's team page (PR objectstack-ai/cloud#3429).

维护者速读

「复制邀请链接」只看邀请的 status 字段。如果一条邀请已经过了 expires_at、但状态还是 pending(没有任务把它改成 expired),这个操作仍然显示,也仍然给出链接。对方点开时才会被拒,管理员却已经把一个失效的链接发出去了。

Facts (framework pin 99a6a78)

  • The visible expression on sys_invitation's copy_invitation_link row action reads status == 'pending' only.
  • The handler checks status only, and answers the accept URL.

Expected

The action is hidden, and the handler answers 409, when expires_at is in the past, whatever the stored status says. The same rule applies to resend if it shares the check. Tests cover a pending row past expires_at.

Reader who acts

Framework triage (the sys_invitation actions). cloud needs no change; its page uses the framework's action as-is.

Dedup

I searched objectstack issues for "copy_invitation_link expires_at pending expired invitation": no open card.

Activity

  1. added
    enhancementNew feature or request
    domain:cloudTouches cloud: the whole card is the cloud PM's (repo:cloud seat); no objectstack lane claims it
    area:identityLogin and identity — sign-up, sessions, organization membership, SSO
    domain:serverDomain lane — seat card indexed by label:pm:seat
    and removed
    domain:cloudTouches cloud: the whole card is the cloud PM's (repo:cloud seat); no objectstack lane claims it
    on Oct 31, 2026
  2. objectstack-fleet commented on Oct 31, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: 真挡得住的权限 | none | P②

    Triage: p3 · domain:server · area:identity · relabelled from domain:cloud · 2026-10-31T09:54Z

    ⛔ Not a claim.

    • Read fresh, in code. copy_invitation_link checks status only, so a pending invitation past expires_at is still offered and still answers a link. It is refused only when opened. The card says cloud needs no change, so this is framework-only work and stays in a framework lane (domain:server), not with the cloud PM.
    • p3: the link fails closed when opened. Hide the action and answer 409 when expires_at is past, apply the same check to resend, and pin a pending row past expiry.

    Generated by Claude Code

  3. objectstack-fleet commented on Nov 1, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo

    Claim: PM loop round 4 · card thread · 2026-11-01T03:50Z
    Session: session_0186vfjHHMsRXC9EUT9hTFMo (the seat); the card thread is session_01FEkQ5mvxN1Jh9iwvcsEv1R
    Account: os-project-manager (the seat's linked user; the card's assignee)
    Branch: claude/project-thread-rypvzz (the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)
    Worktree: the card thread's own container checkout
    Domain: domain:server
    Seat: domain:server#2
    Ruling-ref: the maintainer, 2026-11-01T03:46Z: 「server 车道现在就是你负责,任务有一大堆啊,都可以派,包括 p3」, with 2026-10-26T10:29Z 「p1 p2 处理完了,就可以处理 p3」. Triage 6541933328: hide the action and answer 409 when expires_at is past; pin a pending row past expiry. Resend renews the expiry, so the order keeps it offered (the dev measures).
    File surface: packages/plugins/plugin-auth/src/invitation-link-action.ts (the copy-link handler), packages/platform-objects/src/identity/sys-invitation.object.ts (the action's visible), generated translation bundles only through their generator, tests, and changesets.
    Container & model: M, mode:thread, model: opus (--tier names no path mandate: PM's call, default), effort: high
    Clause-②: yes (expected: narrowing — the handler refuses a call it answers today; a contract review at CONTRACT_REVIEW_TIER is owed before the queue)
    Metadata-protocol: none (expected; the dev measures and declares)
    Responsibility: the code is plugin-auth's copy-invitation-link action · the platform path is the sys_invitation row action an org admin uses · reached by an admin copying the link of a pending invitation whose expires_at has passed
    Thread-read: none
    Premise: holds (read 2026-11-01T03:50Z at origin/main: the handler reads status only (≈L154) and visible reads status == 'pending' only); the dev re-measures first
    Serial constraints cleared: no open PR touches either file at this read
    Resume-from: none — fresh from origin/main

    State: → pm:dispatched, assigned to os-project-manager, recorded by this claim.


    Generated by Claude Code

  4. objectstack-fleet commented on Nov 1, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-project-manager · session_01FEkQ5mvxN1Jh9iwvcsEv1R

    os-dev-report
    {
    "issue": 28384,
    "status": "done",
    "branch": "claude/project-thread-rypvzz",
    "pr": "#28529",
    "session": "session_01FEkQ5mvxN1Jh9iwvcsEv1R",
    "premise_still_valid": true,
    "summary": "Premise held at origin/main 1f1a369: the handler read status only and visible read status only. createCopyInvitationLinkHandler now reads expires_at and adds a sixth gate after pending: a row whose expires_at is not in the future (missing or unreadable counts as expired, as the vendor reads it) is refused 409 RESOURCE_CONFLICT, 'This invitation has expired, so its link no longer lets anyone join. Resend the invitation to give the person a link that works.' Reach is still judged first. copy_invitation_link visible adds has(record.expires_at) && record.expires_at != null && datetime(record.expires_at) > now(); the grid harvests record.FIELD refs of action predicates into $select (collectPredicateFieldRefs), so no list view changed. Resend measured: its visible is the lowered feature and reach gates only, it does not share the check, and it stays offered on an expired row with no expiry gate. Clause-2 measured as 'no (narrowing)' (claim expected 'yes'): nothing is widened. Metadata-protocol: none. Head 52e7cd7.",
    "tests": "Red first (prod sources at 1f1a369, tests at head, both suites import src): platform-objects invitation-delivery-copy 2 failed / 29 passed (expired pending row offered; pending row without expires_at offered); plugin-auth invitation-delivery-outcome 2 failed / 30 passed (expired row answered a link; unreadable expires_at answered); controls green on base (future row, reach before expiry, Resend). Restore git diff HEAD empty. Ablation via scripts/ablation-replace.mjs, handler gate only (anchor ' if (!isBeforeExpiry(' -> ' if (["ABL28384"].length < 0 && !isBeforeExpiry(', anchor 1->0, blob df97aacb->c0980674), visible kept: plugin-auth 2 failed / 30 passed, the two handler cases; restore blob = HEAD, marker 0. Package suites on 52e7cd7: platform-objects test exit 0 (104 files, 1477 passed), plugin-auth test exit 0 (207 files, 3943 passed, 10 skipped), both typecheck exit 0, scoped eslint --no-inline-config exit 0.",
    "gates": "dispatch-gates --commands derived 62 families at 52e7cd7; --ran: 61 run exit 0, 1 NOT MEASURED (pnpm check:dual-build-cjs-loads exit 3 PREREQUISITE NOT MET, needs every package dist; CI runs it). check-plugin-teardown-shape --self-test exit 3 on the shallow clone, exit 0 after git fetch --depth=1 of its control commit. Hand: check-commit-card-trailers --range origin/main..HEAD exit 0, check-platform-checklist --changed-from origin/main exit 0. CI check-runs on PR #28529: in_progress at report time (not awaited, per os-dev).",
    "dogfood_battery": "52e7cd7c · os-verify-lock: VERDICT command-exit 0 (held 19m20s) · Test Files 363 passed / 3 skipped (366) · Tests 2932 passed / 28 skipped (2960) · no failure",
    "mcp_calls": "0",
    "api_writes": "2 — git push (branch claude/project-thread-rypvzz); relay pr_create (fleet-write run 41274953252 -> PR #28529, no labels, no assignees); this os-dev-report comment via post-stamped is the third",
    "open_questions": [],
    "out_of_scope_findings": [
    "class: a · reach: public door, POST /api/v1/auth/organization/invite-member with resend:true (the Resend Invitation row action) measured in the plugin-auth harness on better-auth 1.7.12: on a pending row past expires_at it answers a NEW invitation (different id) and leaves the expired row reading pending, because the vendor's findPendingInvitation filters expired rows out. Resend's confirmation text 'Its expiry date starts over' is not what happens there, and the Pending list then shows two rows for the same person. · dedupe words: resend expired invitation new id, Its expiry date starts over, pending invitation duplicate row, findPendingInvitation expiry filter",
    "carrier: none · noted in PR #28529 Acceptance notes, not filed"
    ]
    }


    Generated by Claude Code

  5. objectstack-fleet commented on Nov 1, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo

    Landing record · #28384 · PR #28529

    domain:server seat 2 · session_0186vfjHHMsRXC9EUT9hTFMo · 2026-11-01T05:48Z

    Reading Probe Result
    Queue membership git ls-remote origin 'refs/heads/gh-readonly-queue/main/pr-28529-*' 0 refs (released)
    Ancestry git merge-base --is-ancestor aa46d141 origin/main yes
    Content git grep -c expires_at origin/main -- packages/plugins/plugin-auth/src/invitation-link-action.ts 6 hits

    Release: claim 6553067811 (session_0186vfjHHMsRXC9EUT9hTFMo, branch claude/project-thread-rypvzz) · landed.

    • State: pm:dispatched removed; assignee cleared; the card closed on merge.
    • Resume-from: none.

    Generated by Claude Code

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:identityLogin and identity — sign-up, sessions, organization membership, SSOdomain:serverDomain lane — seat card indexed by label:pm:seatenhancementNew feature or requestpriority:p3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions