Skip to content

fix(platform): validate mailbox ports before saving credentials - #4260

Merged
yannickmonney merged 1 commit into
mainfrom
fix/mailbox-port-validation
Oct 9, 2026
Merged

yannickmonney merged 1 commit into
mainfrom
fix/mailbox-port-validation

Conversation

@yannickmonney

@yannickmonney yannickmonney commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

The mailbox form, connector schema/catalog, credential normalizer and native resolver refuse explicitly supplied ports unless they are integers from 1 through 65535. Valid custom ports remain intact; omitted ports use 993/465 and an omitted Sent folder uses Sent. Existing stored invalid ports now fail with CREDENTIAL_UNRESOLVED and must be corrected in Settings → Connectors, rather than silently using a default.

Rebased onto main e31702b9849afaf653150d4c53a0f93d3b10cb8d; current source 8f14994f30a4b54b204dd87c87e0e0180586fa32. The original 13-path change replays unchanged; the follow-up adds six test/manual changes without changing its ten production blobs.

The prior COMMENTED independent review's required gaps are closed:

  • Actual createCredential/updateCredential regression cases require CREDENTIAL_CONFIG_INVALID/status400 before writes, audit or realtime effects. Controls prove numeric/string custom ports, inclusive boundaries and defaults.
  • Actual listConnectorSummaries, reading shipped YAML, carries both port bounds to the UI.
  • The real add wizard sends no create request for abc, 0, 1.5 or 65536. Correcting or clearing a port enables Add; inline error associations are tested. Stable manual boxes CONN-F7/CONN-B2 and the partial automation register describe that behavior.

Verification: 208 server/native/catalog tests, 20 UI cases and 50 locale/domain guards pass (278 distinct selected). Causal layer-only controls fail with the corresponding normalizer enforcement removed (20 failures / 22 passing controls) or catalog bound projection removed (1 failure / 8 controls); restored source passes. Full platform typecheck, configured type-aware lint, source formatting, 19-connector config validation, manual gate, scoped SAST and whitespace/conflict checks pass. A distinct agent reviewed the exact prepared source and accepted closure of both required findings.

Full Knip retains seven unused exports inherited from frozen main; candidate and an exact-main source control produce byte-identical output. The shared CI repair is separate. These are SQL stand-in and jsdom/HTTP-stub checks, not a new PostgreSQL, real mail, browser or screen-reader claim. All seven native checks and the merge queue remain required.

Closes #3673

Current-main rebase

Replayed the previously accepted source 8f14994f30a4b54b204dd87c87e0e0180586fa32 onto main d1373d84cd56972501403f62145ec52e6f65d44a, including the merged shared CI repair in #4625. The accepted feature payload and all current-main changes are preserved in one atomic commit. Configured commit and conflict checks pass; earlier behavioral proof remains recorded above. All seven native required checks and full merge-group validation remain required for this new source.

Maintenance replay: preserves the accepted feature payload on current main 7d178ca. Includes the merged #4649 Knip cleanup and the exact independently accepted one-line shared CLI inventory repair from #4654 (252f0df), which is still pending native merge on main. The fixed suite inventory keeps its discovery and source/compiled phase guards. Existing feature proof is retained; no fresh full-feature/full-workspace test or hosted-green claim. Native required checks remain mandatory.

@yannickmonney yannickmonney left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Independent review of PR #4260 at 09dfa0dd8ed86a6a71ef907f48a36cd2ab67cf28: changes required

Who reviewed what.

  • Reviewer: agent #3 4aec6e61, run ed8ae283 (TALE-595, dispatched by fleet-dispatch d9301e49).
  • Implementation: Codex #2 7b86c233, run 7af5f602 (TALE-211). The two are different agents and different runs. TALE-211 was reassigned from agent #3 before agent #3 ever ran on it.
  • Contract: #3673 and TALE-211.
  • Why this is a COMMENT review: both agents publish through one GitHub account. The review is bound to the head commit, but it is not a formal approval or change request.

The fix holds at this head. All four layers refuse an explicit port that isn't an integer from 1 to 65535: the form, the connector schema and catalog, the credential normalizer, and the native resolver. None of them puts a default in its place. Every control still holds. Two small gaps block acceptance.

Required

  1. No PR test pins the credential normalizer or the catalog pass-through.
    • Revert backend/domains/connector_credentials/service.ts alone to main, and every PR test stays green (58 + 99 + 13). The same happens for backend/core/connector_credentials/connector_catalog.ts alone.
    • So two layers have no regression test:
      • the API's refusal before save, which is the "responds 201 and stores 65536 / 1.5" half of #3673;
      • the summary fields that carry the bounds to the form in production. config-fields.test.tsx uses a hand-written summary, so it can't catch this.
    • My probe A turns red under those mutants: 10 cases for the first, 1 for the second.
    • Please add:
      • (a) createCredential and updateCredential refuse 65536, 1.5 and 0 with CREDENTIAL_CONFIG_INVALID, before any INSERT or UPDATE. They store 1993/1587, and they apply the 993/465/Sent defaults. The fakeSql stand-in in service.hints.test.ts makes this a few lines.
      • (b) listConnectorSummaries() serves integer: true, min: 1, max: 65535 for both ports, for example next to catalog_docs.test.ts.
    • The probe source is attached to TALE-595 if you want a starting point.
  2. Manual box CONN-B2 is now false.
    • services/platform/tests/manual/suites/connectors.md:267-271 expects that typing abc into IMAP port and submitting gets the server's "IMAP port" must be a number.
    • At this head, that never happens. Add credential is disabled, the inline settings.connectors.invalidPort error shows, and nothing is sent (probe C, "CONN-B2 path"). The server's wording has also become … must be a number between 1 and 65535.
    • Please:
      • reword CONN-B2 to the new behaviour, keeping its ID;
      • say in CONN-F7 (:136-142) that the ports now gate Add as well;
      • add CONN-B2 to the config-fields.test.tsx row in reference/automation.md (:331) if that spec owns it.

The brief, item by item

Brief item At 09dfa0dd8 Evidence
Ports outside the integers 1–65535 are refused before save ✅ in the form, the schema, the normalizer and the resolver Form (probe C): Add is disabled for 65536, 1.5, 0 and abc, and createCredential is never called.
Schema to catalog (probe A): integer, min and max reach listConnectorSummaries().
Normalizer (probe A): 65536, 1.5, 0, -1, "65536", "1.5" and "abc" each give CREDENTIAL_CONFIG_INVALID with no INSERT. Edit behaves the same, with no UPDATE.
Resolver (probe B): stored 65536, 1.5, -1, 70000, "65536", "abc" and "" each give CREDENTIAL_UNRESOLVED with its hint.
An explicit invalid value is never silently replaced by a default ✅ On main, each of the seven stored values above resolves silently to imap 993 / smtp 465, and the issue's pair of ports (IMAP 65536, SMTP 1.5) doesn't throw (probe B, main log). At head, all of them throw.
A row saved before the fix with 65536/1.5 shows both errors in Edit. Save stays disabled until both are corrected, and then it saves 1993/1587 (probe C).
Valid ports 1993/1587, the missing-host control and the Sent default ✅ 1993/1587 are stored, and they resolve unchanged under both tls and starttls, so the saved port is the port used (probes A and B). Ports 1 and 65535 are accepted.
A missing host still disables Add, gets CREDENTIAL_CONFIG_REQUIRED, and is refused by the resolver.
Omitted ports and Sent folder store 993/465/Sent, and an edit that clears Sent stores Sent.
A blank port is not an error: it is left out, so the default applies (probe C).
The inline error is accessible, in EN, DE and FR ✅ The input gets aria-invalid="true". Its aria-describedby and aria-errormessage point at a role="alert" (polite) message, which is also part of its accessible description.
The repo's axe rule set (tests/utils/a11y.ts) finds no violations on the open dialog with both errors showing.
EN: Enter an integer port from 1 to 65535.
DE: Gib eine ganze Zahl zwischen 1 und 65535 ein. (de-CH inherits it; it contains no ß.)
FR: Saisis un port entier compris entre 1 et 65535.
lib/i18n/messages.test.ts and e2e-keys.test.ts: 26/26.
Only loopback fixtures are used ✅ in substance No new test opens a socket. The resolver and isComplete are pure, and the native file injects its transport.
The new hosts are imap.example.com/smtp.example.com, not loopback. They are RFC 2606 names, like the 83 existing uses in the same file.
My probes use 127.0.0.1 only.
The regression fails on main ✅ for the form, resolver and YAML; ❌ for the normalizer and catalog (Required 1) With the 10 non-test files at merge base d29a3c8fe and the tests at head:
imap-smtp.test.ts: 6 failed / 52 passed.
shipped-connectors.test.ts: 1 / 98.
config-fields.test.tsx: 1 / 12.
They fail for the right reasons: "expected [Function] to throw", and isComplete returning true. With head restored, all pass.

Runs

All runs are targeted and use one worker (--maxWorkers 1).

Set Head 09dfa0dd8 main source, head tests Head restored
PR: imap-smtp.test.ts, shipped-connectors.test.ts 58 + 99 pass 7 fail / 150 pass all pass
PR: config-fields.test.tsx 13 pass 1 fail / 12 pass all pass
Probe A: normalizer via createCredential/updateCredential (fakeSql), plus catalog 16 pass 11 fail / 5 pass (the controls and abc) all pass
Probe B: native resolver 15 pass 8 fail / 7 pass (the controls) all pass
Probe C: the real add and edit dialogs, axe, locales 8 pass 8 fail all pass

Other checks at head:

  • oxlint --type-aware on the 9 changed TS files is clean.
  • oxfmt --check is clean.
  • A scoped tsc gives 0 errors. It covers the 9 changed TS files plus the ambient types, 2,610 program files in all.
  • configs:validate is OK: 19 connectors.
  • The native suite collects and runs normally here. The zod/v4 interop failure described in the PR body didn't reproduce.

Non-blocking

  • invalidPort hard-codes "1 to 65535", and it shows for any invalid number field.
    • That's fine today: the two mailbox ports are the only shipped number config fields.
    • Before another one ships, interpolate min/max or limit the message to ports.
  • For 1.5, the server says "SMTP port" must be a number between 1 and 65535. When integer is set, it should say "whole number".
  • The schema doesn't check that these settings make sense together.
    • It accepts integer, min and max on non-number fields, min > max, and a default outside the bounds.
    • If a connector shipped a default outside its bounds, the server would refuse it, and every credential that omits the port would fail.
    • The rule that a default matches its type already goes unenforced.
  • min, max and step land on a text input (role textbox, no type), and HTML doesn't apply them there.
  • config-fields.tsx:48-49 still says "0 is a real port".
  • Two test nits:
    • The it.each titles leave out the value, so three tests share each name.
    • /port/i also matches "unsupported". Asserting code: 'CREDENTIAL_UNRESOLVED' and the key would be tighter.
  • Rows saved with invalid ports before this fix will now fail at use time with CREDENTIAL_UNRESOLVED ("correct the mailbox port in Settings → Connectors"). Before, they silently used 993/465. That is what the contract asks for, but the PR body should say so.

Not run

  • No real browser, stack, PostgreSQL or mail transport. The SQL is a stand-in.
  • No screen reader, and no contrast check (jsdom can't measure it).
  • No whole-platform tsc or lint, no Knip, no opengrep, no E2E.
  • CI: at 07:59Z all 6 Candidate source / Resolve source runs were queued, and none had completed. I didn't wait for CI, rerun it or cancel it.
  • I didn't push, merge or move any card. A new head needs a fresh scoped review.

@yannickmonney
yannickmonney force-pushed the fix/mailbox-port-validation branch from 09dfa0d to 1df2ce7 Compare October 9, 2026 02:14
@yannickmonney
yannickmonney disabled auto-merge October 9, 2026 02:55
@yannickmonney
yannickmonney force-pushed the fix/mailbox-port-validation branch from 1df2ce7 to 8f14994 Compare October 9, 2026 10:29
@yannickmonney yannickmonney changed the title fix(platform): reject invalid mailbox ports fix(platform): validate mailbox ports before saving credentials Oct 9, 2026
@yannickmonney
yannickmonney force-pushed the fix/mailbox-port-validation branch from 8f14994 to efdb555 Compare October 9, 2026 14:06
@yannickmonney
yannickmonney force-pushed the fix/mailbox-port-validation branch from efdb555 to 350ef72 Compare October 9, 2026 15:00
@yannickmonney
yannickmonney added this pull request to the merge queue Oct 9, 2026
@yannickmonney
yannickmonney merged commit 080f87a into main Oct 9, 2026
64 checks passed
@yannickmonney
yannickmonney deleted the fix/mailbox-port-validation branch October 9, 2026 22:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(platform): mailbox connector saves invalid ports but silently uses different default ports

1 participant