Repository navigation
fix(mp): type CommunicationType as MP's real enum — 'Text' and 'Letter' 500 - #1
Closed
jonnydcakes wants to merge 1 commit into
Closed
jonnydcakes wants to merge 1 commit into
jonnydcakes wants to merge 1 commit into
Conversation
…r' 500
`CommunicationInfo.CommunicationType` was typed `'Email' | 'Text' | 'Letter'`.
Ministry Platform's `Platform.Messaging.CommunicationType` is
`Unknown | Email | SMS | RssFeed | GlobalMFA`, so two of the three values the
type offered were never members, and MP rejects either with an opaque HTTP 500
rather than a 400:
{"Message":"Error converting value \"Text\" to type
'Platform.Messaging.CommunicationType'. Path 'CommunicationType' ...
Requested value 'Text' was not found."}
The only value the type offered for texting was one that always fails, and the
failure reads as a server fault rather than a bad payload. `'Email'` happens to
be a real member and is the only value anyone had passed, which is why the
other two went unnoticed.
Once `'SMS'` is used, MP additionally requires `TextPhoneNumberId`
(`dp_SMS_Numbers.SMS_Number_ID` of the outbound number) and answers its absence
with a second 500 — `Property 'TextPhoneNumberId' is required and must be
populated`. MP's own Swagger marks the field optional, so the Swagger cannot be
trusted here.
Three changes:
- `COMMUNICATION_TYPES` as a `const [...] as const` with `CommunicationType`
derived from it, so the enum has one source of truth.
- `CommunicationInfo` becomes a discriminated union, so the compiler requires
`TextPhoneNumberId` exactly when the type is `'SMS'`. A runtime 500 becomes a
build error.
- A runtime guard in `CommunicationService.createCommunication`, placed above
`ensureValidToken()` so a doomed payload costs neither a token refresh nor a
round trip. It catches callers arriving through an `as` cast or untyped JSON,
and names the accepted values so nobody needs the Swagger to fix their call.
Verified against a live MP tenant on 2026-09-08 rather than from the Swagger
alone: `dp_Communication_Types` holds `1 Email`, `2 SMS Text`, `3 RSS Feed`,
`4 GlobalMFA` (`Unknown` is the zero value and has no row), and an email plus an
SMS were both delivered end to end — the SMS only after switching `'Text'` to
`'SMS'` and supplying `TextPhoneNumberId`.
Tests: eight cases added to `communication.service.test.ts`. Each rejection test
asserts that *nothing was sent* — not merely that an error came back — since
asserting on the message alone would still pass if the guard were moved below
the POST. The type half is covered by `@ts-expect-error` cases that fail
`tsc --noEmit`, which `next build` also performs.
jonnydcakes
force-pushed
the
fix/communication-type-enum
branch
from
September 19, 2026 15:17
9956589 to
66b157e
Compare
Owner
Author
|
Superseded by the upstream PR: MinistryPlatform-Community#92. Branch kept — that PR is built from it. |
chriskehayias
added a commit
that referenced
this pull request
Sep 29, 2026
…okies, sign-out Close auth-test-gaps #1-MinistryPlatform-Community#5 and MinistryPlatform-Community#8 (security review 2026-09-28, wave 2). - src/test-utils/mock-oidc.ts: shared mock MP OIDC server (discovery with issuer + jwks_uri, local RS256 JWKS, token, userinfo); fetch throws on any URL it does not serve. Cookie jar and code-flow helpers. - auth.origin-check: the real instance refuses https://evil.example, "/\t/evil", "//evil.example" and "/\evil.example" callbackURLs (403) and a cross-site Origin on a cookie-bearing POST; negative controls show disableOriginCheck: true and trustedOrigins ["*"] would let them through. - auth.code-flow: GET /callback/ministry-platform through auth.handler: missing, mismatched, cookie-less, tampered and expired state refused before the code is redeemed; code exchange without a PKCE verifier; id_token refused when signed by a foreign key, wrong iss/aud or expired; sub binding; session minted. Sign-out expires both cookies, and a replayed session_token is dead at once (the full pair once the 1 h cache lapses). pkce: false and disableIdTokenNonceBinding: true pinned in config and behaviourally, with negative controls. - auth.session-config: cookie names, prefix, HttpOnly/SameSite=Lax/Path/ Max-Age, __Secure- + Secure over https, no account_data cookie; the decrypted session_data holds no customSession output; expiresAt = +12 h. - auth.rate-limit: AUTH_IP_ADDRESS_HEADERS / AUTH_TRUSTED_PROXIES through the real options into the sign-in limiter, with a shared-bucket control. - auth.test, auth.ip-address, auth.user-id-cache: install the mock OIDC server so the instance under test is built on the verified id_token path instead of a failed discovery fetch. Mutation-checked against src/lib/auth.ts (reverted): disableOriginCheck true/removed, trustedOrigins ["*"], strategy jwt/compact, expiresIn raised, refreshCache removed, sub binding removed, nonce binding removed, pkce removed and storeAccountCookie on all turn the suite red. Secret/baseURL validation tests are tracked with security-auth-url-env-not-validated. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
src/lib/providers/ministry-platform/types/provider.types.ts:41MP's
Platform.Messaging.CommunicationTypeisUnknown | Email | SMS | RssFeed | GlobalMFA.'Text'and'Letter'were never members, and MP rejects either with an opaque HTTP 500,not a 400:
So the only value the type offered for texting is one that always fails, and the failure reads
as a server fault rather than a bad payload.
'Email'happens to be a real member — and it isthe only value anyone has ever passed, which is why a two-thirds-wrong union sat in the type
layer unnoticed.
Also: SMS requires
TextPhoneNumberIdOnce
CommunicationType: 'SMS'is used, MP additionally requires it:TextPhoneNumberIdis the outbound number'sdp_SMS_Numbers.SMS_Number_ID. The field alreadyexists on
CommunicationInfobut is optional — it is effectively required whenCommunicationType === 'SMS'. MP's own Swagger marks it optional, so the Swagger cannot betrusted here.
Verified against a live tenant
Not taken from the Swagger alone — re-checked on a production MP instance on 2026-09-08:
dp_Communication_Types1 Email,2 SMS Text,3 RSS Feed,4 GlobalMFA(Unknownis the zero value, no row)dp_SMS_NumbersOriginally found on 2026-06-11 building a passwordless sign-in spike that reuses this
/communicationspattern: the email delivered; the SMS succeeded only after switching'Text'→
'SMS'and addingTextPhoneNumberId.Impact
Latent, not a live outage. Nothing in the template calls
createCommunication— thereferences are the plumbing (
helper.ts,provider.ts,communication.service.ts) and thetests, which use
'Email'. This is a wrong type surface waiting for the first person who triesto send a text, who gets a 500 with no indication their payload was at fault. Every downstream
fork of the template inherits it.
The change
COMMUNICATION_TYPESas aconst [...] as constwithCommunicationTypederived fromit, so the enum has one source of truth and a comment naming what verified it.
CommunicationInfobecomes a discriminated union, so the compiler requiresTextPhoneNumberIdexactly when the type is'SMS'. A runtime 500 becomes a build error.CommunicationService.createCommunication, placed aboveensureValidToken()so a doomed payload costs neither a token refresh nor a round trip. Itcatches callers arriving through an
ascast or untyped JSON, and names the accepted valuesso nobody needs the Swagger to fix their call.
Plus a paragraph in the provider
docs/README.mdCommunications section.provider.types.tsis hand-maintained (generate-types.tsonly writes intomodels/), so thiswill not be undone by
npm run mp:generate:models.Tests
Eight cases added to the existing
communication.service.test.ts(21 in that file now), plus acompile-time block.
Each rejection test asserts that nothing was sent — no
post, nopostFormData, and noensureValidToken— not merely that an error came back. Asserting on the message alone wouldstill pass if the guard were later moved below the POST, which is the failure mode the guard
exists to prevent.
All four mutations were run and each fails the suite it should:
assertSendable(...)callensureValidToken()'Text'back inCOMMUNICATION_TYPEStscTS2578 — unused@ts-expect-errorTextPhoneNumberIdoptional for'SMS'tscTS2578 — unused@ts-expect-errorVerification on this branch:
npx vitest run --coverage→ 59 files / 1023 tests pass, coveragegates green (99.74% statements);
npx tsc --noEmitclean;npx eslint .clean;node scripts/check-lockfile.mjsreports no drift (no dependency changes).