Skip to content

feat(plugin-auth, plugin-email): under open and email_domain, a sign-up for an existing address answers like a new one and mails that address, while mail is delivered - #25145

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-25060-signup-existing-address
Oct 22, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-25060-signup-existing-address

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Fixes #25060

Clause-②: yes
Metadata-protocol: none

This carries out ruling 1B on #25040 (comment 6308462370), with the seat's calls Q1–Q3 on this card (comment 6309679111). Under the open and email_domain audience postures, a sign-up for an address that already has an account now answers the same shape as a successful sign-up, and that address's holder gets a mail saying it already has an account, pointing to sign-in and password reset. This happens only while the mail transport really delivers. Otherwise the explicit refusal stays, so a success with no mail behind it cannot come back. invite_only is unchanged.

Disclosure: the #9387 ruling (5323310017) applies. This body describes behaviour only. It carries no request shape, response body, status or code sample, and no probe.

What changed

packages/plugins/plugin-auth/src/auth-manager.ts

  • The decision stays in the /sign-up/email before-hook, after the audience gate and after hasExistingUserFor finds the row. The existing-address refusal is raised unless all three of these hold (existingAddressSignUpAnswersSuccess):
    1. The posture permits self-registration (open, email_domain).
    2. The vendor's anti-enumeration shield is on. This is read from the options the vendor reads for it: verification required, or auto sign-in off.
    3. Mail is delivered (isEmailDeliveryLive).
  • One reader of IEmailService.isConfigured() (isEmailDeliveryLive). It is read at sign-up time and never cached, so a transport swapped from the mail settings applies to the next request. A service without the member, or a member that throws, reads as not live, so the request falls back to the refusal. hasEmailTransport and isEmailServiceAvailable are untouched.
  • The mail rides the vendor's emailAndPassword.onExistingUserSignUp hook (sendExistingAccountSignUpNotice). The vendor hands it the existing row. The hook checks posture and liveness again, because it also fires on the one path that never reached the decision: an unanswerable uniqueness probe falls through to the vendor. So invite_only never mails on that path either. The hook skips placeholder addresses, sends auth.existing_account_sign_up as identity mail, and never throws. A failed send is logged once at error level.
    • Locale ladder: the recipient's stored sys_user.locale, then the deployment default. There is no request rung, because the requester is not the recipient (Q2).
    • Links: the Console's /login and /forgot-password pages, built through getConsolePageUrl. No token is minted.
  • One line in the sign-up after-hook, outside the claim's listed regions. The vendor's success answer for an existing address carries a user id that belongs to no row. The after-hook stamped password_changed_at on that id, and every such sign-up logged the "password_changed_at / must_change_password were NOT written" durability warning about a user that was never created. Pin ① measured this: it records warn-level lines, and it went red before the fix. The before-hook now marks the request (__osExistingAddressSignUp, the same ctx.context channel as the neighbouring __osDisableSignUpOrig), and the after-hook skips the stamp for it. Without this line, the landed behaviour produces a false lost-write report on every existing-address sign-up, so it stays in this PR.

packages/plugins/plugin-email/src/templates/auth-templates.ts (Q2)

  • Adds auth.existing_account_sign_up in en-US, zh-CN, ja-JP and es-ES, and adds it to BUILTIN_AUTH_TEMPLATES (28 rows become 32).
  • Variables: user.name (optional), user.email, signInUrl (url), resetPasswordUrl (url), appName (optional). The two URL holes are triple-braced only inside href attributes, as the outlet pin requires.
  • The default footer stays: "if this wasn't you, you can safely ignore this message" is true here.

Tests

  • New: plugin-auth/src/signup-existing-address-success-shape.test.ts (13 cases on a real ObjectQL and SQLite engine).
  • New block in plugin-email/src/auth-templates-locales.test.ts.
  • Count and digest updates: builtin-auth-render-identity.test.ts (32 rows; the four new rows are captured from the registry render, and a comment says so) and email-plugin.template-runtime-write.test.ts (32).
  • A header note in signup-existing-address-refusal.test.ts. Its cases wire no email service, which is exactly the deployment that keeps the refusal, so they stand unchanged.

content/docs/deployment/self-hosting.mdx (patch round 1, under the amended claim 6329490109; cross-lane declaration on #6023 6329495818). The section "Who may sign up — and what each posture tells a stranger" is corrected to the behaviour as shipped, written abstractly under the #9387 discipline. Every other sentence of the page is byte-identical. Four parts changed:

  • The open row of the table. It said a caller learns whether an address is registered. It now says a caller learns nothing while the mail transport is live: a registered address gets the same answer as a new sign-up, nothing is written, and the holder is mailed. Without a live transport, or with verification declared off and auto sign-in left on, the explicit refusal stays and does tell the caller.
  • The two disclosure-by-design paragraphs ("On open, that disclosure is by design" and "That silence was never a real secret, either"). They are replaced by "What decides it on open and email_domain". It lists the three conditions: a self-registration posture, verification on or auto sign-in off, and a live transport per IEmailService.isConfigured(), asked at every sign-up, with plugin-email's log transport never live and an absent member counting as not live. It then describes the mail: the template, its language and its links.
  • The paragraph saying onExistingUserSignUp is deliberately not wired ("If you need open without the disclosure, ask for it"). It is replaced by "Otherwise the refusal stays explicit". That paragraph names the three refusal cases, keeps the enumeration-oracle warning for open without a live transport, and keeps the black-hole reason the refusal exists. It ends with how to close the oracle: keep verification on and connect a mail transport.
  • The email_domain remedy bullet ("Opening the audience posture is not enough on its own", under "Two remedies that look like they work"). A seeded person's registration writes nothing and buys no login either way: refused explicitly, or, with a live transport, answered like a new registration and mailed. The bullet's sentences on fresh addresses and verification are unchanged.

.changeset/25060-signup-existing-address.md: plugin-auth and plugin-email, patch. See the first acceptance note for why this is patch and not the card's minor.

The partition-2 assumptions, measured on better-auth 1.7.12

  1. Vendor readings M4 and M5 were re-measured on 1.7.12.
    • (a) Read from dist/api/routes/sign-up.mjs lines 155 and 192–204, and context/create-context.mjs lines 215–224. onExistingUserSignUp fires only when findUserByEmail returns a row AND the shield is on. It runs after the timing-equalizing hash, through runInBackgroundOrAwait. plugin-auth configures no backgroundTasks handler, so the hook is awaited inline, and its rejection is caught and logged by the vendor. This is unchanged from 1.7.3.
    • (b) Measured by pin ①, which does not write the shape down. On the same deployment, the existing-address answer equals a new sign-up's answer in status, header names, top-level members, user members and their types, a null token, and no session cookie. This holds under email_domain, under open with verification on, and under open with verification off and auto sign-in off. It is unchanged from the 1.7.3 reading.
  2. isConfigured() as landed. EmailService.isConfigured() (plugin-email/src/email-service.ts) answers !(transport instanceof LogTransport), read from the live transport on every call. Its tests (email-service.test.ts and email-plugin.mail-settings.test.ts) cover the log transport, a live transport, and swaps in both directions through the mail settings. An absent member reads as not live here (pin ②).
  3. The before-hook and vendor-hook split holds as assumed. No change of route was needed.

Verification at 629addbf9

  • pnpm --filter @objectstack/plugin-auth exec vitest run --maxWorkers=2 (full suite): lock VERDICT command-exit 0. Test Files 182 passed (182); Tests 3451 passed, 10 skipped (3461).
  • pnpm --filter @objectstack/plugin-email exec vitest run --maxWorkers=2 (full suite): Test Files 36 passed (36); Tests 668 passed (668).
  • pnpm --filter @objectstack/plugin-email typecheck: exit 0, test layer 0 ledgered errors.
  • pnpm --filter @objectstack/plugin-auth typecheck, after building both packages: lock VERDICT command-exit 0. The test layer holds its ledger unchanged (10 files, 88 errors, 23 signatures), and the new test file compiles clean.

Ablations

Each row is one mutation of auth-manager.ts through scripts/ablation-replace.mjs: an anchor that must hit, the landing proven by anchor count and blob change, a restore proven against the HEAD blob with an empty git diff HEAD. Each row ran the new pin file. The suites resolve the source directly through a relative import, so no build is needed.

Leg Forbidden behaviour put back Result
A1 a wired service reads as live (the option the seat refused) red: 4 failed. The three not-live variants of pin ② and the runtime-swap case.
A2 the posture conjunct is dropped red: 1 failed. Pin ③, the invited holder under invite_only with the shield on and a live transport.
A3 the shield conjunct is dropped green, 13 passed. Pin ④ holds because the vendor itself refuses with the same code and message when the shield is off. Our conjunct keeps the refusal at our own seam (see the acceptance notes).
A4 the vendor hook is unwired red: 7 failed. Pin ① in all three configurations, plus the swap, locale and failed-send cases.
A5 the decision always refuses (the old behaviour) red: 7 failed, the same cases.
A6 the stored-locale rung is dropped red: 1 failed. Pin ⑤, stored locale.
A7 the after-hook stamps the rowless id again red: 2 failed. Pin ①, both postures (the warn-level lost-write report).
A8 the en-US subject is pasted under the zh-CN template row red: 2 failed. Both the zh-CN render and the "no en-US text" negative pin.

Every leg restored cleanly (restored=yes on all eight).

Gates

  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 70 commands at 629addbf9. All 70 were run, with exit codes captured before any pipe.
  • 69 exited 0. pnpm check:dual-build-cjs-loads exited 3 (PREREQUISITE NOT MET: it needs every package's dist/, so a whole-repo build). That one is NOT MEASURED locally and left to CI.
  • --ran reconciliation: "70 derived famil(ies) accounted for — 69 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)".
  • Not run locally, and owned by CI: the repo-wide scans, pnpm lint first among them.
  • Patch round 1, at c8b1dce63 (adds the docs page). The derivation grew to 100 commands, adding the docs families. All 100 were run, with exit codes captured before any pipe.
    • On the first pass, nine exited non-zero for missing build prerequisites in the recreated worktree: eight exited 3, and check:docs exited 1 because its generated tree had not been built.
    • After a closure build of plugin-auth, plugin-email and lint with their dependencies (lock VERDICT command-exit 0), all nine exited 0. That includes check:dual-build-cjs-loads, which was NOT MEASURED in the first round.
    • --ran reconciliation: "100 derived famil(ies) accounted for — 100 run, 0 NOT-MEASURED (a DERIVED zero — all 100 recorded an exit code and none of them is 3)".
    • The docs change touches no code, so the test runs above stand.

Acceptance notes

  • The changeset level is patch, not the card's minor. The card was filed before maintainer ruling A on changeset levels: minor only when the metadata protocol changes (added / narrowed / semantic), patch otherwise; release notes grouped by what the reader must do (maintainer ruling A, 2026-10-21) #25105 (2026-10-21). Ruling A makes the level follow the Metadata-protocol: declaration alone, and names REST and services as none, which takes patch. This diff adds no authorable key, value or metadata type. A built-in template name is a free string, not a new accepted value. With none, check-changeset-no-major refuses a minor (none-minor). Declaring added to reach minor would be a false declaration. The door change is described in the changeset either way.
  • Two sentences of self-hosting.mdx were left byte-identical, as the patch round ordered. Both still describe the refusal without the live-transport case. The recovery section's "Invite an address the directory does not already hold" says a sign-up for an address the directory holds is refused "invitation or not". That holds under invite_only, the posture that recovery runs on, and the advice itself stays right. The email_domain table row says a caller learns something only inside an allowlisted domain. That is still true, but it is now conditional on there being no live transport.
  • The four new template constants are not re-exported from plugin-email's src/index.ts, which every sibling family is. index.ts is outside the claim's fence. The template is fully reachable through BUILTIN_AUTH_TEMPLATES and the registry.
  • The shield conjunct is defence in depth, not the only guard (ablation A3). Where the shield is off, the vendor refuses an existing address on its own, with the same code and message. Our conjunct keeps the refusal at the platform's seam, so it does not depend on that vendor branch. Pin ④ therefore reds only if both guards go.
  • Accepted residual (Partition 3). A send that fails after the decision leaves a success answer with no mail, the same gap a failed verification send leaves for a new address. It is logged once at error level, and pin ⑥ holds that.
  • Probe-failure path. When the uniqueness probe cannot be answered, the request falls through to the vendor exactly as before. With a live transport under open or email_domain, that path now also mails the holder. The after-hook marker is not set on that path, so the pre-existing stamp warning remains there, next to the probe's own error line.
  • Pin ⑤'s second case is not ablated. The second case checks that a requester's Accept-Language never chooses the mail's language. The vendor hook is given the request, but this code does not read it, so there is no request rung to put back. The pin holds the line against a future change.
  • Timing. Both answers now send one mail inline: verification for a new address, this notice for an existing one. The existing-address path no longer skips the send, which narrows the timing gap the vendor's hash only partly levels.
  • origin/main moved by two commits since the base (459afea1f, 4c7dcb451). Neither touches plugin-auth, plugin-email or the email contract. No merge was taken; CI and the queue validate the merge.

Generated by Claude Code

…uccess and mails its holder under open and email_domain, while mail is delivered

Ruling 1B on #25040. The /sign-up/email before-hook keeps the explicit
refusal unless the posture permits self-registration, the vendor's
anti-enumeration shield is on and IEmailService.isConfigured() answers
true; the vendor's onExistingUserSignUp hook sends the new
auth.existing_account_sign_up template (plugin-email, four locales) in the
recipient's stored locale, then the deployment default.

Claude-Session: https://claude.ai/code/session_01UnHcTADjeg3VwxF21QHj7W
Co-authored-by: Claude <noreply@anthropic.com>
…after-hook stamps nothing on the answer's rowless user id

Claude-Session: https://claude.ai/code/session_01UnHcTADjeg3VwxF21QHj7W
Co-authored-by: Claude <noreply@anthropic.com>
… 28 seeded-row renders

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

github-actions Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/plugin-auth, @objectstack/plugin-email, touching 21 documentable anchor(s).

9 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/automation/email-templates.mdx (via bodyHtml (symbol, a field of const object AUTH_EXISTING_ACCOUNT_SIGN_UP_TEMPLATE; a field of const object AUTH_EXISTING_ACCOUNT_SIGN_UP_TEMPLATE_ES_ES; a field of const object AUTH_EXISTING_ACCOUNT_SIGN_UP_TEMPLATE_JA_JP; a field of const object AUTH_EXISTING_ACCOUNT_SIGN_UP_TEMPLATE_ZH_CN))
  • content/docs/deployment/environment-variables.mdx (via appName (literal, a string literal in EXISTING_ACCOUNT_SIGN_UP_VARIABLES))
  • content/docs/getting-started/quick-start.mdx (via appName (literal, a string literal in EXISTING_ACCOUNT_SIGN_UP_VARIABLES))
  • content/docs/kernel/contracts/auth-service.mdx (via AuthManager (symbol, a top-level class), user.name (literal, a string literal in EXISTING_ACCOUNT_SIGN_UP_VARIABLES))
  • content/docs/kernel/contracts/cache-service.mdx (via user.name (literal, a string literal in EXISTING_ACCOUNT_SIGN_UP_VARIABLES))
  • content/docs/kernel/services-checklist.mdx (via AuthManager (symbol, a top-level class))
  • content/docs/permissions/authentication.mdx (via AuthManager (symbol, a top-level class), appName (literal, a string literal in EXISTING_ACCOUNT_SIGN_UP_VARIABLES))
  • content/docs/ui/setup-app.mdx (via appName (literal, a string literal in EXISTING_ACCOUNT_SIGN_UP_VARIABLES))
  • content/docs/ui/translations.mdx (via user.email (literal, a string literal in EXISTING_ACCOUNT_SIGN_UP_VARIABLES))
What this run could not see
  • 1 anchor(s) matched too much of the corpus to be a work list: sys_user (literal, 42 pages)
  • 3 name(s) were too generic to anchor anything (single lowercase words)
  • 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 — 21 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 459afea1f69ff69422de2a7a2c7674b6f7ea1aca → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 7d8df8e6fe4e442cfe5def08f41efec4e8069eb8 — the merge of head c8b1dce6365cf61c7d8c95bb44dc08a0edc3fed0 into base 459afea1f69ff69422de2a7a2c7674b6f7ea1aca, 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 7d8df8e6fe4e442cfe5def08f41efec4e8069eb8 && git checkout 7d8df8e6fe4e442cfe5def08f41efec4e8069eb8
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 459afea1f69ff69422de2a7a2c7674b6f7ea1aca c8b1dce6365cf61c7d8c95bb44dc08a0edc3fed0 && git checkout -B drift-repro 459afea1f69ff69422de2a7a2c7674b6f7ea1aca && git merge --no-ff c8b1dce6365cf61c7d8c95bb44dc08a0edc3fed0

node scripts/docs-audit/affected-docs.mjs --json 459afea1f69ff69422de2a7a2c7674b6f7ea1aca

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 459afea1f69ff69422de2a7a2c7674b6f7ea1aca → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: TIER_DEFAULT
Head-sha: 629addbf97035ec1330d7fa4c016e5aa556a62c6
Local-runs: none

The review is owed by the Clause-②: yes declaration alone (the claim 6327912966, the PR body and the changeset). No published schema and no governed path are in the committed tree (8 files: plugin-auth, plugin-email, .changeset/). It was read from the diff, the card, ruling 1B (6308462370 on #25040) and the seat calls (6309679111). ⛔ Disclosure (#9387, 5323310017): behaviour only.

① Derived judgments

  • Accept-set change: the public email-and-password sign-up answers an existing address differently in one configuration. When all three of these hold, the explicit refusal becomes the vendor's generic success, plus a mail to the holder:

    • the posture is open or email_domain;
    • the vendor's anti-enumeration shield is on (verification required, or auto sign-in off), read from the options the vendor itself reads;
    • IEmailService.isConfigured() answers true, read per request.

    This matches ruling 1B. Correct.

  • The precondition: a missing member, a throwing member and a false answer all keep the refusal (isEmailDeliveryLive). hasEmailTransport / isEmailServiceAvailable are not repurposed. This matches the ruling's "not live ⇒ refusal" and Q1. Correct.

  • invite_only is unchanged. open with the shield off keeps the refusal (Q3, no session for someone else's account). Correct.

  • The mail: the vendor hook re-asks posture and liveness, so the unanswerable-probe fall-through cannot mail under invite_only.

    • It skips placeholder addresses.
    • It uses the stored locale, then the deployment default, with no request rung (Q2).
    • It links to the Console's sign-in and forgot-password pages and mints no token.
    • It never throws, and logs a failed send at error level once.

    Correct.

  • The after-hook line: it skips password_changed_at for the rowless id of the success answer. The need is measured (pin ①, ablation A7), and the amended claim 6329490109 accepts it. Correct.

  • Public surface: no export is added (src/index.ts is untouched). The template is one more row in the existing built-in registry. No widening beyond the declared door change.

② Semver level

patch for @objectstack/plugin-auth and @objectstack/plugin-email, with Metadata-protocol: none. This follows #25105's ruling A (closed: minor only when the metadata protocol changes). No authorable key, value or metadata type changes. The card's "minor" predates that ruling. Consistent.

③ Boundary flags

  • The dev's open question 1 (the docs page made false) is answered A by the amended claim 6329490109: a patch round corrects that one section of content/docs/deployment/self-hosting.mdx, a prose face this record does not gate.
  • The missing src/index.ts re-export stays an Acceptance note: nothing this PR touched becomes false.
  • The out-of-scope reading (the pre-existing stamp warning on the probe-failure path) is carried as an Acceptance note: carrier none, noted, not filed.
  • Ablation A3 (the shield conjunct is defence in depth, backed by the vendor's identical refusal) is recorded in the PR.

Implemented-by: claude/issue-25060-signup-existing-address
Reviewed-by: session_01UnHcTADjeg3VwxF21QHj7W

VERDICT: PASS


Generated by Claude Code

…ing address, now that open and email_domain answer like a new sign-up while mail is delivered

Claude-Session: https://claude.ai/code/session_01UnHcTADjeg3VwxF21QHj7W
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

domain:services seat 2 (seat post #21118) · os-bill · session_01UnHcTADjeg3VwxF21QHj7W · 2026-10-22T01:16Z · ACCEPT at head c8b1dce6365cf61c7d8c95bb44dc08a0edc3fed0 · dev reports 6329444466 (round 0) and 6330264572 (patch round 1) on #25060

Regen-provenance: 6329519413 · 629addbf97035ec1330d7fa4c016e5aa556a62c6 → c8b1dce6365cf61c7d8c95bb44dc08a0edc3fed0 · git diff --name-only outside the prose faces → (empty); the one changed path is content/docs/deployment/self-hosting.mdx

  1. Shape: a draft PR to main whose first line is Fixes #25060, assigned os-bill.
    • Clause-②: yes and Metadata-protocol: none are at line start in the body and the changeset.
    • The seat wrote the patch round's body update (15330 bytes, read back identical), because the role file reserves body edits for the seat.
  2. Premises measured:
    • The vendor readings were re-measured on better-auth 1.7.12, and are unchanged from 1.7.3.
    • isConfigured() was read as landed: false for the log transport, following runtime swaps.
    • The seat's contract review read the diff against ruling 1B and the seat calls Q1–Q3.
  3. Gates (check-runs pinned to this head):
    • 36 success, including Lint & Repo Gates, TypeScript Type Check, Governed Surface Queue Guard, Check Changeset, Build Core and Test Core 1–6.
    • 6 skipped (Console Pin Gate, Temporal Conformance, Dogfood Verify CLI, the Dogfood Regression Gate matrix template, both opt-in harnesses), each on the check-expected-skips.mjs roster. The roster was read from the script; the tool needs yaml, which the shared checkout does not have.
    • check-governed-merges.mjs --pr 25145: NOT governed, +986 / −59, under 3000.
    • Contract review PASS at TIER_DEFAULT: 6329519413 on 629addbf9. It carries to this head by the provenance line above: the hop is prose-only.
  4. Accepted deviations:
  5. Out of scope (each dev finding, one line):
    • the pre-existing stamp warning on the probe-failure path → Acceptance notes;
    • the four template constants not re-exported from plugin-email's src/index.ts → Acceptance notes.
    • Two self-hosting.mdx sentences outside the four named parts stay true as written: the invite_only recovery advice, and the email_domain row, whose disclosure is now conditional. → Acceptance notes.
    • The docs-drift bot's nine listed pages were read: none enumerates the auth templates or states the sign-up answer. They are matched on generic identifiers, so none is made false.
  6. Landing: ordinary queue landing (not governed). The seat sets ready and auto-merge through the relay in this act. After the merge, plugin-auth: under the open and email_domain postures, a sign-up for an address that already has an account answers the success shape and mails that address (ruling 1B on #25040); without a live mail transport the explicit refusal stays #25060 closes by Fixes; the seat removes pm:dispatched and records the landing.

Prose faces, checked sentence by sentence against the diff:

  • The changeset: the answer change under the three conditions; the refusal cases (not live, invite_only, open with the shield off); the per-request read; the template, its four locales and its locale ladder; the client note; the failed-send residual; "Unchanged". All hold.

  • self-hosting.mdx:

    • the remedy bullet: either answer writes nothing, and sign-in still fails;
    • the open row: nothing while live, else an explicit refusal;
    • "What decides it": the three conditions as coded;
    • the mail paragraph: no token, stored language then deployment default, the hook wired after the platform decides;
    • "Otherwise the refusal stays explicit": the three cases, the oracle warning, the remedy.

    All hold, and no status or code sample was added.


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/xl tests tooling

Projects

None yet

2 participants