Skip to content

fix(plugin-auth): the Magic Link mail states the lifetime the link really has, five minutes - #25238

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-25112-magic-link-ttl
Oct 22, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-25112-magic-link-ttl

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Sales · project thread

Fixes #25112

Clause-②: no

Metadata-protocol: none

What changed

The auth.magic_link mail said "This link expires in 10 minutes" while the link it carries lived 300 s: magicLink() was mounted without expiresIn (so better-auth's own default applied) and sendMagicLink passed a separate hard-coded expiresInMinutes: 10.

  • packages/plugins/plugin-auth/src/auth-manager.ts: one local constant, magicLinkExpiresInSec = 5 * 60, beside the mount. The mount now passes expiresIn: magicLinkExpiresInSec, and the mail's expiresInMinutes is Math.round(magicLinkExpiresInSec / 60). The copy and the link read one source.
  • The link's lifetime does not change. It was 300 s, measured in the installed better-auth 1.7.12 (dist/plugins/magic-link/index.mjs, expiresAt: new Date(Date.now() + (opts.expiresIn || 300) * 1e3)), and it stays 300 s, now stated rather than inherited, so a dependency bump cannot move it under the mail.
  • The seat's ruling on the card's open value (claim comment 6341542393), executed as written: keep the measured 300 s, state it from one constant, derive the mail from it. Lengthening the lifetime would relax a sign-in credential and stays with the maintainer.

Where the fix lives, and why not in plugin-email

All four locale rows of auth.magic_link in packages/plugins/plugin-email/src/templates/auth-templates.ts (en-US, zh-CN, ja-JP, es-ES), HTML and text faces, render the lifetime only from {{expiresInMinutes}}. None hard-codes a number. The false value was produced entirely by the producer in plugin-auth, so plugin-email does not move and takes no changeset.

Tests

  • auth-email-locale.test.ts: the existing payload pin asserted the false 10; it now asserts 5. A new pin asserts the mount states expiresIn: 300 and that the mail's expiresInMinutes * 60 equals the mount's expiresIn.
  • Ablation, on head 145974256 with the fix committed first, through scripts/ablation-replace.mjs (anchor hit 1, then 0; blob restored to the HEAD blob c3dd34d7bf34 both times):
    1. expiresInMinutes set back to the literal 10: 2 failed / 67 passed (expected 10 to be 5, expected 600 to be 300).
    2. expiresIn deleted from the mount: 1 failed / 68 passed (expected undefined to be 300).
  • pnpm --filter @objectstack/plugin-auth build && typecheck && test: 185 files passed, 3487 passed and 10 skipped.

Gates

dispatch-gates --commands derived 70 commands for this diff, and all 70 exited 0. Reconciliation: 70 derived, 70 run, 0 NOT-MEASURED, 0 UNRUN. Two of them first answered PREREQUISITE NOT MET (exit 3): check-plugin-teardown-shape --self-test on a shallow clone, and check:dual-build-cjs-loads without a full build. Both were re-run after git fetch --unshallow and pnpm build, and both exited 0.

Acceptance notes

  • plugin-email's template tests feed auth.magic_link the sample value expiresInMinutes: 10 (plain-text-faces.test.ts, auth-templates-locales.test.ts). It is only render data and makes no claim about the link's lifetime, so it is left as is.
  • check:auth-mount-ledger tracks route mounts, not plugin options, so it does not read this change.

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_016g7nYP9wdNx1GDa5XDr7Uy

…ally has, five minutes

The magicLink() mount states expiresIn from one constant (300 s, the
lifetime the vendor default already gave the link) and the mail's
expiresInMinutes is derived from it, instead of a hard-coded 10.

Claude-Session: https://claude.ai/code/session_016g7nYP9wdNx1GDa5XDr7Uy
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/s documentation Improvements or additions to documentation tests tooling labels Oct 22, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 18 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 03de83bfe145eea8f495bcbc9886105715721b3f → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 6fe880d22a28e17e3a3fe97c423539fcda4c90ec — the merge of head 1459742566eb4d1b2271aa01d4c622f231ab345b into base 03de83bfe145eea8f495bcbc9886105715721b3f, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 6fe880d22a28e17e3a3fe97c423539fcda4c90ec && git checkout 6fe880d22a28e17e3a3fe97c423539fcda4c90ec
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 03de83bfe145eea8f495bcbc9886105715721b3f 1459742566eb4d1b2271aa01d4c622f231ab345b && git checkout -B drift-repro 03de83bfe145eea8f495bcbc9886105715721b3f && git merge --no-ff 1459742566eb4d1b2271aa01d4c622f231ab345b

node scripts/docs-audit/affected-docs.mjs --json 03de83bfe145eea8f495bcbc9886105715721b3f

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · 2026-10-22T14:04Z

domain:services seat 1 (seat post #6021) · os-sales · session_016g7nYP9wdNx1GDa5XDr7Uy · card #25112 (claim 6341542393, with the seat's lifetime ruling) · reviewed head 1459742566eb4d1b2271aa01d4c622f231ab345b

  • Shape: producer-side only. magicLink() is mounted with expiresIn from one local constant (300 s), and the mail's expiresInMinutes is derived from it. The link's effective lifetime is unchanged, as ruled; no lengthening. 3 files, +36 / −2, inside the claimed surface. plugin-email does not move: its four auth.magic_link rows render only {{expiresInMinutes}} (dev reading; no hard-coded number).
  • Premise, measured by the dev on 123ff6f4e: better-auth 1.7.12's magic-link plugin defaults expiresIn to 300 s, so the mount change states the existing value.
  • Pins and ablation: the payload pin moves 10 → 5; a new pin asserts the mount's expiresIn is 300 and expiresInMinutes * 60 equals it. Both ablations, fix committed first, turned red as expected (literal 10 back: 2 failed; expiresIn removed: 1 failed) and were restored blob-identical.
  • Gates: dispatch-gates --ran: 70 derived, 70 run, 0 NOT MEASURED, 0 UNRUN. plugin-auth 185 files / 3,487 tests green under the verify lock. CI on this head still running; landing waits on every check green or an expected skip.
  • Accepted deviations: none.
  • Out of scope: plugin-email template tests render with the sample value expiresInMinutes: 10; render data claiming no lifetime, Acceptance notes only — dropped — not a defect.
  • Landing: not governed (check-governed-merges --pr 25238). Clause-②: no (no accept set or public surface moves). Ready + auto-merge through the relay once CI is green.

The seat prepended the project attribution block to the PR body; the dev's text is unchanged below it.


Generated by Claude Code

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

Labels

documentation Improvements or additions to documentation size/s tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

plugin-auth: the Magic Link mail says the link expires in 10 minutes, while the link it sends lives 300 seconds (measured)

2 participants