feat(apigateway): an api-gateway handler owns its own wire response - #26
Merged
Conversation
`forApiGateway` handlers now return an `ApiGatewayResponse` — any status, any
headers, any body, or none — and the sdk passes it to the wire as written.
the root cause was the wrong type at the output point: `invoke` returned a
*body*, so the wire encode had to invent the rest and invented
`{ statusCode: 200, body: JSON.stringify(x) }`. a body cannot carry a status,
so 204, 308, and xml were inexpressible by construction.
- widen the one contract rather than add a peer export — `PickAny<{ status,
headers, body }>`, so `{}` is a compile error rather than middys silent 500
- `schema` is required, `z.any()` is the explicit opt-out — validation and
introspection stay on one code path
- `asApiGatewayResponseSchema({ body })` lifts a body schema into the envelope
- add `genContentTypeCoherenceMiddleware` — a body-less response reaches the
wire with no `content-type`, which is the twilio 11200 shape retired
- add `HttpStatusCode.PERMANENT_REDIRECT_308`
- fix: `corsInputToCorsConfig` handed the vendor `origins: undefined`, which
clobbered its `[]` default and threw from `after` — every success response of
a `cors: { origins: "*" }` handler was a 500
- 15 wire acceptance cases over real http, both payload versions, whole-response
denylist snapshots
BREAKING CHANGE: `invoke` returns `ApiGatewayResponse` rather than a body.
`({ x })` becomes `({ body: { x } })`. an aws-native `{ statusCode, body }`
return answers 500 with a named error rather than a silent mis-encode.
Co-authored-by: ulad kasach <uladkasach@gmail.com>
an org policy now refuses any action referenced by tag, so every workflow run failed at action lookup before step one. this repo last ran ci on 2026-07-23, before the policy landed. - pins all 45 third-party `uses:` refs across 6 workflow files - shas taken from the verified table in declapract-typescript-ehmpathy route `v2026_08_06.fix-workflow-sha-pins`, which pinned its own 47 refs and shipped a clamp test for them - each ref keeps a `# vN` tail, so the tracked major stays readable - local `./` refs are exempt and untouched verified by count rather than by pattern, per that route: 53 total `uses:` lines = 45 sha-pinned + 8 local. zero tag refs remain. this is a targeted edit, not a declapract apply — the repo sits at 0.49.4 and a full upgrade would pull seven releases of unrelated template churn. Co-authored-by: ulad kasach <uladkasach@gmail.com>
uladkasach
enabled auto-merge (squash)
September 9, 2026 06:18
the `test` workflow died at startup — 0 jobs, 0s — so none of the 8 required `suite/*` checks ever reported and the pr could not merge. .the cause = `.test.yml` and `.publish-npm.yml` each declare `permissions: id-token: write`, and a called workflow may not exceed its caller. the repo default is now `read` (`actions/permissions/workflow` -> default_workflow_permissions: read), and `test.yml`/`publish.yml` declared no permissions at all. control: `.declastruct.yml` requests no elevation, and `provision.yml` calls it with no permissions block and passes. so the default is not the problem — the unmet request is. this worked on 2026-07-23 and broke by 2026-09-09 with no workflow edit, so it is the same org hardening wave as the sha-pin policy. the fix is the block upstream already carries on both callers (declapract-typescript-ehmpathy, cicd-common + cicd-package). `release.yml` is deliberately untouched: it writes via a github app token rather than GITHUB_TOKEN, and upstream declares no permissions on it either. Co-authored-by: ulad kasach <uladkasach@gmail.com>
uladkasach
added a commit
that referenced
this pull request
Sep 9, 2026
…text (#27) a github expression inside a run block is substituted into the command text itself, so a backtick or a dollar-paren in a commit message is executed by bash. the merge of #26 carried backticks, the release workflow died at exit 127, and no tag, no release, and no npm publish were produced. - move commit-message into the step env, where bash never re-parses it - drop the dead REPO_URL assignment, which no line in this step reads matches declapract-typescript-ehmpathy upstream, which already carries this fix. this repo is five releases behind it. Co-authored-by: seaturtle[bot] <seaturtle@ehmpath.com>
Merged
Contributor
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.
feat(apigateway): an api-gateway handler owns its own wire response
forApiGatewayhandlers now return anApiGatewayResponse— any status, anyheaders, any body, or none — and the sdk passes it to the wire as written.
the root cause was the wrong type at the output point:
invokereturned abody, so the wire encode had to invent the rest and invented
{ statusCode: 200, body: JSON.stringify(x) }. a body cannot carry a status,so 204, 308, and xml were inexpressible by construction.
PickAny<{ status, headers, body }>, so{}is a compile error rather than middys silent 500schemais required,z.any()is the explicit opt-out — validation andintrospection stay on one code path
asApiGatewayResponseSchema({ body })lifts a body schema into the envelopegenContentTypeCoherenceMiddleware— a body-less response reaches thewire with no
content-type, which is the twilio 11200 shape retiredHttpStatusCode.PERMANENT_REDIRECT_308corsInputToCorsConfighanded the vendororigins: undefined, whichclobbered its
[]default and threw fromafter— every success response ofa
cors: { origins: "*" }handler was a 500denylist snapshots
BREAKING CHANGE:
invokereturnsApiGatewayResponserather than a body.({ x })becomes({ body: { x } }). an aws-native{ statusCode, body }return answers 500 with a named error rather than a silent mis-encode.
🐢🌊 surfed in by seaturtle[bot]