Skip to content

comments: an @mention by email is matched case-sensitively, so a differently-cased address is accepted 201 and silently never notified #27503

Closed

Description

@objectstack-fleet

domain:devx#1 · sunhaolin · session_017xRBcSwDXZrZg7nGjP6P3U

Filing gate: ① a product defect with a reproduction, found by an off-clause probe of QA run #27438 (wave 3 of the 18.3.0 sweep, anchor #27377). reach: measured at the public comment door. The mention is accepted with 201, but no notification is sent.
Business reason: a user who @-mentions a colleague by email address and types it in a different case, or pastes it from a directory that capitalises it, gets no error, and the colleague is never notified. Email addresses compare case-insensitively everywhere else in identity, sign-in included. The comment reads as if the mention worked.
Reader who acts: the triage seat, then the implementer for mention resolution (packages/plugins/plugin-audit, the sys_comment mention notifier).
Dedupe (open AND closed): the query "mention by email case sensitive unknown_email notification withheld" returned 0 hits.

Batch-of: 16 · QA run #27438

Symptom

An @mention by email address is resolved case-sensitively, and a differently cased address is silently not notified.

Reproduction (subject c4c515a, stock showcase)

  1. Have a member whose email is g4b.member@example.com and who can read the record's thread.
  2. POST /api/v1/data/sys_comment {"thread_id":"showcase_account:ID","body":"x","mentions":["G4B.MEMBER@EXAMPLE.COM"]} → 201.
  3. The member receives no notification. The server log reads [audit] mention notification withheld {reason: unknown_email}.
  4. The same request with g4b.member@example.com delivers the notification.

Expected: an email mention matches case-insensitively, the same way sign-in does.

Also noticed, diagnostic only: mentions: ["nosuchuser123"] (an unknown user id) is logged as cannot_read_thread instead of as an unknown user, which misleads anyone reading the log.

QA-source: #27438 · records-forms.record-discussion-mentions · observed beside the clauses


Generated by Claude Code

Activity

  1. added
    area:recordsBusiness objects, records, the views that show data, usable forms, search
    bugSomething isn't working
    domain:serverDomain lane — seat card indexed by label:pm:seat
    on Oct 29, 2026
  2. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: 业务对象与记录 | records-forms.record-discussion-mentions | P②

    Triage: p2 · domain:server · area:records · 2026-10-29T11:20Z

    ⛔ Not a claim.

    • Read fresh. An @mention by email is matched case-sensitively, so a differently-cased address is accepted 201 and the colleague is silently never notified. A silent drop of an authored value: p2.
    • Compare mention emails with the same case-insensitive normalisation identity and sign-in already use. No second email matcher.
    • bug, pm:queue.

    Generated by Claude Code

  3. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · os-elon-musk · session_016WV4czuTWNgKR22fKv7jia

    Claim: PM loop round 1 · 2026-10-29T21:22Z
    Session: session_016WV4czuTWNgKR22fKv7jia (seat; mode:subagent, the dev shares the seat's session — identity by branch)
    Account: os-elon-musk
    Branch: claude/project-thread-rcp68o
    Worktree: objectstack-issue-27503
    Domain: domain:server
    Seat: domain:server#3
    File surface: packages/plugins/plugin-audit/src/mention-recipient-gate.ts (resolveMentionedUser) and its tests, .changeset/*; ⛔ no packages/spec, no governed path (stop on breach; explain in the report)
    Container & model: S, mode:subagent, model: opus (TIER_DEFAULT; dispatch-gates --tier: no path-derived mandate), effort: medium
    Clause-②: no
    Metadata-protocol: none
    Responsibility: plugin-audit's mention gate matches the email exactly; the platform path that covers it is the case-insensitive email normalisation identity and sign-in already use (triage 6495269327: no second email matcher); reached by any comment author who types a differently-cased address.
    Thread-read: 6495269327
    Premise: holds (where: { email: value } exact match on origin/main d93d9be5)
    Serial constraints cleared: none — no open PR or in-flight claim on plugin-audit/src/mention-recipient-gate.ts found at claim time.

  4. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 27503,
      "status": "done",
      "branch": "claude/project-thread-rcp68o",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/27751",
      "head_sha": "e711910983d66be94059b99aa485bff340c4a016",
      "session": "session_016WV4czuTWNgKR22fKv7jia",
      "premise_still_valid": true,
      "summary": "The premise was measured and holds: on d93d9be5, resolveMentionedUser in plugin-audit's mention-recipient-gate.ts looked an email mention up with where: { email: value }, an exact match, and a differently-cased address answered unknown_email. Stored emails are normalised: better-auth internal-adapter createUser/updateUser write email.toLowerCase(), and sign-in queries the lowercased address (better-auth /sign-in/email findUserByEmail(email.toLowerCase()); plugin-auth findSignInAccount where: { email: email.toLowerCase() }). The fix reuses the existing shared normaliser normalizePlatformAdminEmail from @objectstack/core (trim, then lowercase; its docblock calls it the ONE normalization) on the query value, so a mention names the same user sign-in would; no second matcher was added. The integration test gained two cases, one through the real afterInsert writer and one at the log seam, plus a patch changeset for @objectstack/plugin-audit.",
      "tests": "Red before the fix (closure built): pnpm --filter @objectstack/plugin-audit exec vitest run --maxWorkers=2 src/mention-recipient-gate.integration.test.ts -> 2 failed | 15 passed (17); AssertionError: expected [] to deeply equal [ [ 'u_carol' ] ]. Green after: 17 passed (17), VERDICT command-exit 0. Ablation (fix committed at 5febd38e; scripts/ablation-replace.mjs; the suite aliases @objectstack/core and imports this package's src, so there is no dist leg): mutation landed (anchor 1 -> 0, blob 8f07337134ce -> e29091174dd9) -> 2 failed | 15 passed (17); restored (blob == HEAD 8f07337134ce, git diff HEAD empty). On head e7119109: pnpm --filter @objectstack/plugin-audit test -> Test Files 65 passed (65), Tests 1051 passed (1051); pnpm --filter @objectstack/plugin-audit typecheck -> exit 0 (check:test-typecheck OK, 0 debt); eslint --no-inline-config on the two changed .ts files -> exit 0, no output. test:repo: not run locally (diff touches no repo-project test), declared to CI Test Core.",
      "gates": "dispatch-gates --commands --repo objectstack-ai/objectstack on head e7119109: 61 commands, all run with exit codes captured before any pipe. 58 exited 0 on the first pass. 3 exited 3 (PREREQUISITE NOT MET) and each was re-run green once its prerequisite was met: check-plugin-teardown-shape --self-test (fetched the pinned fixture commit 621a4876; 48 cases), check:i18n (built its stated closure; 10 packages in sync), check:dual-build-cjs-loads (full pnpm build, 114 tasks; 112 entries across 69 packages). dispatch-gates --ran: 61 derived famil(ies) accounted for — 61 run, 0 NOT-MEASURED. --ran flagged the tree as 4 commits behind a re-fetched origin/main; the only stale family file is scripts/check-route-envelope.mjs, which is not in this diff's derivation. CI families: PR check-runs in_progress at report time (not awaited, per contract).",
      "line_budget": "41 changed lines (+40 / -1) across 3 files, under the 3000 human-merge threshold; no skills/** touched, so no line ratchet applies",
      "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm",
      "files_changed": [
        "packages/plugins/plugin-audit/src/mention-recipient-gate.ts",
        "packages/plugins/plugin-audit/src/mention-recipient-gate.integration.test.ts",
        ".changeset/27503-mention-email-case-insensitive.md"
      ],
      "deviations": [
        "The dispatch asked for pr_create with assignees: []. The relay validator refused an empty list ('actions[0].assignees must name at least one item'), and os-dev.md forbids assignees on a PR, so the key was dropped. The read-back reports no assignees.",
        "The PR body ends with the dispatch's footer ('Generated with Claude Code' plus the session URL) and not the AGENTS.md session-URL footer block. The dispatch's wording was followed, and the conflict is reported here.",
        "The branch is a platform-named card-thread branch, so no empty-branch probe push was made (os-dev rule 1). It was pushed once, after the fix and the single merge of origin/main."
      ],
      "mcp_calls": "2 — mcp__github__issue_read (get, get_comments); 0 write tools",
      "api_writes": "2 relay writes as objectstack-fleet[bot]: pr_create (POST /repos/objectstack-ai/objectstack/pulls, draft, run 40964865860, read-back 6418/6418 bytes identical) and this os-dev-report comment (POST /repos/objectstack-ai/objectstack/issues/27503/comments); plus 1 git push (not REST)",
      "open_questions": [],
      "out_of_scope_findings": [
        "carrier: none · noted, not filed — the card's diagnostic-only observation that an unknown user id (mentions: [nosuchuser123]) is logged as cannot_read_thread rather than as an unknown user. It is log-text only, nobody is wrongly notified, and it is outside the three filing classes. Recorded in the PR Acceptance notes.",
        "carrier: none · noted, not filed — the shared email normaliser is named normalizePlatformAdminEmail although its docblock calls it the one email normalization. This is naming only. Dedupe words: email normaliser name, normalizeEmail core, platform admin email helper."
      ]
    }
  5. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · os-elon-musk · session_016WV4czuTWNgKR22fKv7jia

    ACCEPT → PR #27751 comment 6510146711 · 2026-10-29T22:40Z · ready + auto-merge armed through the relay.

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

Metadata

Metadata

Assignees

Labels

area:recordsBusiness objects, records, the views that show data, usable forms, searchbugSomething isn't workingdomain:serverDomain lane — seat card indexed by label:pm:seatpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions