exploration: support legacy Matrix user IDs#14
Conversation
| * grammar. Homeservers must continue accepting these IDs even though they may not | ||
| * create new IDs using that grammar. | ||
| */ | ||
| const schema = Schema.TemplateLiteral(['@', opaqueId, ':', ServerName.schema]).pipe( |
There was a problem hiding this comment.
[P1] Accept the historical empty localpart
The Matrix Historical User IDs rules explicitly require clients to accept an empty localpart, but opaqueId includes isMinLength(1), so Schema.decodeUnknownEffect(UserId.schema) rejects @:matrix.org. UserId.schema is used by inbound event and account response schemas, so one such legacy identifier can still reject the server payload that issue #9 is intended to handle. Please use a user-ID-specific historical localpart which allows empty, and cover @:matrix.org with a regression test. See https://spec.matrix.org/unstable/appendices/#historical-user-ids.
There was a problem hiding this comment.
Fixed in 362e6fa. UserId.schema now uses a dedicated historical localpart that permits the required empty localpart, while UserId.make still uses the strict modern non-empty grammar. Added inbound coverage for @:matrix.org; the existing outbound rejection remains covered.
| * create new IDs using that grammar. | ||
| */ | ||
| const schema = Schema.TemplateLiteral(['@', opaqueId, ':', ServerName.schema]).pipe( | ||
| Schema.check(Schema.isMaxLength(255)), |
There was a problem hiding this comment.
[P2] Enforce the MXID limit in UTF-8 bytes
Matrix limits the complete user ID to 255 bytes, whereas Effect Schema.isMaxLength checks JavaScript input.length, which counts UTF-16 code units. This becomes observable now that schema accepts Unicode: @ followed by 130 copies of é and :matrix.org is 272 UTF-8 bytes but only 142 code units, and UserId.schema accepts it. Please use a UTF-8 byte-length check for the historical schema and add a multibyte boundary test. See https://spec.matrix.org/unstable/appendices/#user-identifiers.
There was a problem hiding this comment.
Fixed in 362e6fa. The complete MXID is now checked with TextEncoder and capped at 255 UTF-8 bytes instead of UTF-16 code units. Regression coverage accepts an exact 255-byte multibyte ID and rejects the corresponding 256-byte ID.
| * grammar. Homeservers must continue accepting these IDs even though they may not | ||
| * create new IDs using that grammar. | ||
| */ | ||
| const schema = Schema.TemplateLiteral(['@', opaqueId, ':', ServerName.schema]).pipe( |
There was a problem hiding this comment.
[P2] Reject lone UTF-16 surrogates
The historical grammar permits legal non-surrogate Unicode code points, but the opaqueId regex only excludes NUL and colon. A JavaScript string containing a lone U+D800 therefore passes UserId.schema; I reproduced this locally. JSON parsing can produce such a string, and UTF-8 encoding later replaces it with U+FFFD, so the branded value is not stable. Please add an explicit well-formed-Unicode check to the historical user localpart and a regression test. See https://spec.matrix.org/unstable/appendices/#historical-user-ids.
There was a problem hiding this comment.
Fixed in 362e6fa. The historical localpart now rejects isolated UTF-16 surrogate code units while accepting valid paired astral code points. Added regressions for lone high/low surrogates, an accepted emoji, and preserved NUL rejection.
wouter173
left a comment
There was a problem hiding this comment.
Review summary: the inbound/outbound split is directionally sound, UserId.make remains strict, and the existing focused tests and typecheck pass. Before merging, UserId.schema should cover the full historical acceptance grammar and preserve the protocol invariants: accept an empty localpart, reject lone surrogates, and enforce the 255-byte UTF-8 limit. I left three inline comments with reproductions and suggested regression cases. No files or commits were changed during review.
Closes #9
Summary
Verification
Caveat
The repository-wide pnpm lint command currently fails before linting because .oxlintrc.json enables type-aware checking but the tsgolint executable was removed on main. The changed files pass the non-type-aware oxlint rules, and TypeScript typechecking passes.