Filed unassigned and ungraded by the #12993 dev seat while repairing the SMTP port drift surface. Not the same defect class as #12993 (that card is duplication; this is the accept set), so it was deliberately NOT repaired there — narrowing the accept set inside a refactor whose whole claim is behaviour preservation would have been a behaviour change wearing a cleanup's clothes.
Measured
SmtpTransport's port guard is a finite-and-in-range check with no integrality test, so a fractional port passes construction and is refused three layers later by the kernel:
SmtpTransport({ host: 'smtp.x', port: 587.5 }) -> ACCEPTED at construction
net.connect({ port: 587.5 }) -> ERR_SOCKET_BAD_PORT
"Port should be >= 0 and < 65536.
Received type number (587.5)."
Measured on origin/main@dc729f358 through the built dist/index.js, both lines above are real output.
Why it is worth a card
The refusal this transport does emit says (expected 1-65535). 587.5is in 1-65535 on any reading of that sentence, so the transport states a rule it does not enforce, and the value it waves through cannot ever connect. What the operator finally sees is ERR_SOCKET_BAD_PORT ... options.port ... at send time — an error naming an internal option, thrown from a code path with no connection to the Settings field they typed in.
That is the same defect shape #12673 was opened for and PR #12898 repaired for the CLI listen port, one layer down: a door that refuses late, under the name of the channel rather than the spelling the operator used.
Not established here
- Whether the fix is
Number.isInteger at the guard, or a shared reader that normalises. The CLI's port-contract.ts deliberately keeps a WIDE accept set (parseInt tolerance: 3e3, 0x0BB8, 3000.0 all boot), and its module header is explicit that a door may not narrow it — so "make SMTP strict" is a real decision with a precedent pointing the other way, not an obvious repair. - Severity not judged. Reaching it requires an operator to type a fractional port.
Re-check
node -e "const m=require('./packages/plugins/plugin-email/dist/index.js'); new m.SmtpTransport({host:'smtp.x',port:587.5}); console.log('accepted')"
⚠️ The current accept set is pinned deliberately in packages/plugins/plugin-email/src/transports/smtp-port-contract.test.ts (refactors the enforcement without narrowing what it accepts), where 587.5 sits in the table as MEASURED, not endorsed. A fix here must update that table in the same edit — the pin exists to make a behaviour change visible, not to prevent one.
Refs
Filed unassigned and ungraded by the #12993 dev seat while repairing the SMTP port drift surface. Not the same defect class as #12993 (that card is duplication; this is the accept set), so it was deliberately NOT repaired there — narrowing the accept set inside a refactor whose whole claim is behaviour preservation would have been a behaviour change wearing a cleanup's clothes.
Measured
SmtpTransport's port guard is a finite-and-in-range check with no integrality test, so a fractional port passes construction and is refused three layers later by the kernel:Measured on
origin/main@dc729f358through the builtdist/index.js, both lines above are real output.Why it is worth a card
The refusal this transport does emit says
(expected 1-65535).587.5is in 1-65535 on any reading of that sentence, so the transport states a rule it does not enforce, and the value it waves through cannot ever connect. What the operator finally sees isERR_SOCKET_BAD_PORT ... options.port ...at send time — an error naming an internal option, thrown from a code path with no connection to the Settings field they typed in.That is the same defect shape #12673 was opened for and PR #12898 repaired for the CLI listen port, one layer down: a door that refuses late, under the name of the channel rather than the spelling the operator used.
Not established here
Number.isIntegerat the guard, or a shared reader that normalises. The CLI'sport-contract.tsdeliberately keeps a WIDE accept set (parseInttolerance:3e3,0x0BB8,3000.0all boot), and its module header is explicit that a door may not narrow it — so "make SMTP strict" is a real decision with a precedent pointing the other way, not an obvious repair.Re-check
packages/plugins/plugin-email/src/transports/smtp-port-contract.test.ts(refactors the enforcement without narrowing what it accepts), where587.5sits in the table as MEASURED, not endorsed. A fix here must update that table in the same edit — the pin exists to make a behaviour change visible, not to prevent one.Refs
os dev --portis unvalidated andos start --portis unbounded — both forward to theservechild on a channel that renames the operator's input #12673 / PR fix(cli): one port contract, three doors — dev/start/serve refuse in the operator's own spelling (#12673) #12898 — the same "refuses late, under the wrong name" shape, repaired for the CLI listen port