Repository navigation
fix(plugin-auth, platform-objects): resending an expired invitation replaces it, leaving one pending invitation - #28605
Conversation
…nd on an expired row (red)
…ng invitation better-auth's findPendingInvitation skips a row past its expiresAt, so a resend (or an invite) on an expired invitation issues a new one and the expired row kept reading pending. The global after hook now moves the person's other expired pending rows in that organization to expired after a successful invite-member, so the Pending list holds one row per person. A refusal retires nothing; a resend that renews an unexpired row keeps it. Claude-Session: https://claude.ai/code/session_01FEkQ5mL9b8QSVNXp9PcY6T Co-authored-by: Claude <noreply@anthropic.com>
…n expired invitation too Claude-Session: https://claude.ai/code/session_01FEkQ5mL9b8QSVNXp9PcY6T Co-authored-by: Claude <noreply@anthropic.com>
…send Invitation's confirmation; changeset Claude-Session: https://claude.ai/code/session_01FEkQ5mL9b8QSVNXp9PcY6T Co-authored-by: Claude <noreply@anthropic.com>
…tire step calls Claude-Session: https://claude.ai/code/session_01FEkQ5mL9b8QSVNXp9PcY6T Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 15 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 6 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 23 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin e55c40c4d3ec563e4e5499218a2a50dc0d0e4e27 && git checkout e55c40c4d3ec563e4e5499218a2a50dc0d0e4e27
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ce001f97abbe460d9a6cc9a4bde6c6e38c9d37f8 df411420a635f079f8ee2c2ae50b9df734b1e37f && git checkout -B drift-repro ce001f97abbe460d9a6cc9a4bde6c6e38c9d37f8 && git merge --no-ff df411420a635f079f8ee2c2ae50b9df734b1e37f
node scripts/docs-audit/affected-docs.mjs --json ce001f97abbe460d9a6cc9a4bde6c6e38c9d37f8
|
|
domain:server#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo 席位核对ACCEPT · The maintainer's standing authorisation for landing reviewed PRs, verbatim: 「按照 pm-dispatch 规则复核过的都授权」.
Generated by Claude Code |
Requested by Leehom · project thread
Fixes #28540
Clause-②: no
Metadata-protocol: none
维护者速读
packages/plugins/plugin-auth/src/invitation-expired-supersede.ts(新的退役步骤);packages/plugins/plugin-auth/src/auth-manager-after-hook.ts(接入点);packages/plugins/plugin-auth/src/invitation-resend-expired.test.ts。pnpm --filter @objectstack/plugin-auth exec vitest run --maxWorkers=2 src/invitation-resend-expired.test.ts→ 15/15 通过(其中 5 条是本卡的)。What changes
The retire step (
packages/plugins/plugin-auth/src/invitation-expired-supersede.ts, new). better-auth 1.7.12'sinvite-memberlooks up the person's pending invitation withfindPendingInvitation, which drops every row whoseexpiresAthas passed (organization/adapter.mjs). So a resend (resend: true, theresend_invitationrow action) on an invitation that has already expired finds nothing to renew and issues a new invitation, and the expired row kept readingpendingforever.retireExpiredInvitationsOnReissue(ctx)runs in the globalafterhook, right after the delivery-outcome stamp, and only on a successful invite-member answer. It reads the answered person's pending rows in that organization through the vendor's own adapter (ctx.context.adapter.findManyon modelinvitation), skips the answered row by id and every row still before its expiry, and moves the rest toexpiredwith a write that namesstatus: 'pending'beside the id (a row that was accepted, declined or canceled meanwhile is not touched).expired, notcanceled: the row did expire,expiredis the ObjectStack half ofInvitationStatus(better-auth has no such value), and the row now shows in the Expired / Canceled list.isBeforeExpiry(invitation-link-action.ts), now exported for reuse; the gate itself is unchanged. An expiry that does not read as a date counts as expired, as the vendor and the gate read it.warn, and never turns the issued invitation into an error.findMany/updatewith keyword objects), socheck:tenant-audit-censusreads the receiver as the better-auth adapter rather than an unplaced engine write; no ledger row is added.The confirmation (
packages/platform-objects/src/identity/sys-invitation.object.ts).resend_invitation'sdescriptionnow reads "Resend the invitation to (the record.email hole)? The expiry date starts over, and an invitation that has already expired is replaced by a new one." It is true on both rows Resend is offered on. Itsai.descriptionsays the same. Theenbundle is regenerated bynode scripts/check-i18n-bundles.mjs --write; the zh-CN, ja-JP and es-ES leaves are re-translated by hand (a stale translation is not a gap, so the merge keeps it otherwise).Not changed: the copy-link gate from #28529 (its
visibleand handler), Resend'svisible(still offered on an expired row: resend is the remedy), the invite-member answer's shape, and the vendor's renewal of an unexpired row.Tests
packages/plugins/plugin-auth/src/invitation-resend-expired.test.ts(new): a realAuthManagerover the installed better-auth and the memory engine, every invitation issued and resent through the vendor's own route (the harness ofinvitation-delivery-outcome.test.ts, cut to two people).expired.pending.Readings:
origin/maina52191f, test at head): 2 failed / 13 passed, the two expired cases; every control green on base.node scripts/ablation-replace.mjs, retire step only (anchorif (!row || row.id === answeredId || isBeforeExpiry(row.expiresAt)) continue;prefixed with an always-true["ABL28540"].length > 0 ||): anchor 1 → 0, blob25bc42e6c78c→78d9bfb5c76e; 2 failed / 13 passed, the two expired cases only; restore blob equals HEAD,git diff HEADempty. The suite importssrc/directly, so no build sits between the mutation and the run.invitation-delivery-outcome.test.ts47/47.pnpm --filter @objectstack/plugin-auth testTest Files 208 passed (208), Tests 3958 passed, 10 skipped;pnpm --filter @objectstack/platform-objects testTest Files 106 passed (106), Tests 1488 passed; bothtypecheckexit 0.Acceptance notes
auth-manager-after-hook.ts,invitation-link-action.ts(oneexportkeyword) or the new module; the seat may amend it.invitation-delivery-outcome.test.ts, whose tests already take about 10 s of wall. Like 33 other plugin-auth test files, the new file importscreateMemoryEnginefromimpersonation-bearer-rotation.test.ts, which also runs that file's 10 cases inside it (observed, not filed).pr_create: none touchesauth-manager-before-hook.ts,auth-manager-after-hook.ts,invitation-link-action.tsorsys-invitation.object.ts. PR fix(platform-objects): the Users identity source reads "Local Account" instead of "Env-Native" #28543 edits the same four*.objects.generated.tsbundles at line 108 (env_native), this PR at line 851: separate hunks.Verification
All readings on head df41142 (1a454b7 merged with
origin/maincd065d3, which brought in PR #28543's edits to the same bundles; the merge was clean). Nothing landed on this branch after these runs.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack→ 63 families, each run with its exit code captured before any pipe, all exit 0 on df41142.--ranreconciliation:✓ dispatch-gates --ran: 63 derived famil(ies) accounted for — 63 run, 0 NOT-MEASURED (a DERIVED zero — all 63 recorded an exit code and none of them is 3).First-run reds on an earlier head and their fixes:check-tenant-audit-censusexit 1 (an untypedadapter.update()receiver) → the adapter typed, exit 0;check-plugin-teardown-shape.mjs --self-testexit 3 on the shallow clone →git fetch --depth=1of its control commit, exit 0;check:dual-build-cjs-loadsexit 3 (nodist/yet) → exit 0 after the i18n gate's build closure (115 entry points across 70 packages load).node scripts/check-platform-checklist.mjs --changed-from origin/main,node scripts/check-commit-card-trailers.mjs --range origin/main..HEAD,node scripts/check-durability-degradation-log-level.mjs, andpnpm exec eslint --no-inline-configover the five touched source files (neither package has alintscript; the repo-widepnpm lintis CI's).!in the title, no narrowing arm).Generated by Claude Code