feat(workflow-executor): validate bearer claims via a zod middleware [PRD-508]#1659
Merged
Scra3 merged 2 commits intoJun 12, 2026
Conversation
|
Coverage Impact This PR will not change total coverage. Modified Files with Diff Coverage (2)
🛟 Help
|
Base automatically changed from
feature/prd-477-refactor-centralize-executor-http-error-mapping-typed-http
to
main
June 12, 2026 13:44
…[PRD-508]
koa-jwt validates a token's signature/expiry but not its payload shape. Add a
BearerClaimsSchema (non-strict z.object({ id: z.number() }) — tolerates iat/exp
and extra Forest claims) and a middleware right after koa-jwt that validates
ctx.state.user once. handleTrigger drops its ad-hoc Number.isFinite check, and
both routes now get a user with a guaranteed numeric id.
Behaviour change: a well-signed token without a valid numeric id now returns 401
(invalid credentials) instead of 400.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…view [PRD-508]
- Narrow hasRunAccess(runId, user) to { id: number } (only the id is used), removing
the unsound `as StepUser` cast on the validated bearer user — both routes now cast
to BearerClaims consistently.
- Log invalid bearer claims (logger.warn, issue paths/codes only — never the payload)
before the 401: a signed-but-malformed token is rare and high-signal, not churn.
- Tighten BearerClaimsSchema id to z.number().int() (rejects NaN/Infinity/floats),
preserving the previous Number.isFinite invariant.
- Tests: GET route 401 on invalid claims (access check not reached), warn assertion,
non-integer id rejection.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
deb01a4 to
bde84af
Compare
forest-bot
added a commit
that referenced
this pull request
Jun 12, 2026
# @forestadmin/workflow-executor [1.2.0](https://github.com/ForestAdmin/agent-nodejs/compare/@forestadmin/workflow-executor@1.1.5...@forestadmin/workflow-executor@1.2.0) (2026-06-12) ### Features * **workflow-executor:** validate bearer claims via a zod middleware [PRD-508] ([#1659](#1659)) ([5a0a63c](5a0a63c))
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.

What
Validates the bearer JWT payload as early as possible via a zod middleware, instead of the ad-hoc
Number.isFinitecheck inhandleTrigger. Follow-up to PRD-477.koa-jwtonly validates a token's signature + expiry — never the payload shape. Soctx.state.user.idwas not guaranteed;hasRunAccessMiddlewareeven didas StepUserwithout validating (latent gap).Changes
src/http/bearer-claims.ts—BearerClaimsSchema = z.object({ id: z.number() }). Non-strict on purpose:jsonwebtokenaddsiat/expto the decoded payload and Forest may send extra claims — a strict schema would reject every real token. Onlyidis consumed downstream (handleTrigger+hasRunAccess→?userId=);StepUserSchema(9 required fields) is a different contract and isn't reusable.koa-jwt— validatesctx.state.useronce; on failure throwsUnauthorizedHttpError. Both routes now receive a user with a guaranteed numericid.handleTrigger— drops theNumber.isFinitebranch.A well-signed token without a valid numeric
idnow returns 401 Unauthorized (invalid credentials, consistent with koa-jwt's own 401s) instead of 400.Out of scope (decided)
pendingDatais not boundary-validatable — its schema is step-type-dependent (patchBodySchemas[execution.type]), known only aftergetAvailableRun. It stays validated downstream inbase-step-executor. A boundaryz.union/z.discriminatedUnionwas considered and rejected (accepts the wrong step type;.transform()footgun; no discriminator in the front-sent payload).Tests
test/http/bearer-claims.test.ts: accepts{ id }and{ id, iat, exp, … }(the non-strict trap), rejects missing/non-numeric id.test/http/executor-http-server.test.ts: invalid claims → 401; a token with extra claims → 200 (end-to-end non-strict proof).Note
Stacked on
feature/prd-477-…(base of this PR) — it builds on the typed HTTP errors / middleware from PRD-477. Retarget tomainonce PRD-477 merges.fixes PRD-508
Note
Validate bearer JWT claims via Zod middleware in workflow-executor HTTP server
executor-http-server.tsthat validatesctx.state.useragainstBearerClaimsSchema(integeridrequired); invalid payloads receive 401 before reaching any route handler.http-errors.tswith typed HTTP error classes and atoHttpErrorutility that maps domain error categories to HTTP statuses: 404 forNotFoundError, 403 forAccessDeniedError, 503 forUnavailableError, 400 for uncategorizedWorkflowExecutorError, and 401 for koa-jwt errors.errors.tsinto abstract category base classes (NotFoundError,AccessDeniedError,UnavailableError) so HTTP translation is driven by error category rather than per-handler branching.UserMismatchErrornow requires bothbearerUserIdandownerUserIdin its constructor, including both in the technical log message while keeping a generic user-facing message.Macroscope summarized bde84af.