Skip to content

feat(embed): add the TypeScript router + conformance suite - #14

Open
mattj-monad wants to merge 2 commits into
feature/pro-401from
feature/pro-402
Open

feat(embed): add the TypeScript router + conformance suite#14
mattj-monad wants to merge 2 commits into
feature/pro-401from
feature/pro-402

Conversation

@mattj-monad

Copy link
Copy Markdown
Contributor

Layer 3 of the Monad Embed SDK staged rollout (PRO-402). Stacked on #13 (PRO-401) — targets feature/pro-401; review/merge the stack bottom-up.

What this adds

  • @monad-inc/embed-server — a zero-runtime-dependency /embed router that mounts into Express or bare Node http via createEmbedRouter (core logic + node adapter + Monad API adapter). 13 unit tests.
  • conformance/ — the language-agnostic suite that proves any backend satisfies the contract over HTTP: Schemathesis response-conformance across all 9 routes + scripted lifecycle scenarios (mint → build → status → disable → remove, and egress), driven against a stateful in-memory mock Monad. It boots the router under test as a subprocess (ROUTER=ts|go|python); only the ts server ships here — the go/python servers land with their router layers.
  • CI "Conformance (ts)" job: builds the router, sets up the Python harness (uv + schemathesis), runs ROUTER=ts. From here, conformance is the required gate every later router PR must pass.

Conformance runner stays Python (Schemathesis)

We considered moving it to Node to keep the shared gate on one runtime, but kept Schemathesis: it's the canonical tool for cross-language OpenAPI conformance, there's no mature 3.1 Node equivalent (Dredd/jest-openapi are 3.0-oriented), so Node would mean owning ~100 lines of validation glue — the opposite of low-maintenance. Python is already a shipped router (PRO-404), and conformance is a CI gate, so contributors rarely need Python locally.

Also

  • Ignore the harness .venv in Prettier (mirrors the eslint ignore) and formatted two source files that weren't Prettier-clean on the source branch.

Verification (local)

  • TS router 13/13 vitest; conformance 14 checks green (ROUTER=ts); full typecheck / lint / format / build clean.

Stack

400 → 401 → 402 → 403 (Go) → 404 (Python) → 405 (docs/CI). Go and Python will generalize the conformance CI job to a matrix.

🤖 Generated with Claude Code

https://claude.ai/code/session_011ASKA2VrufrNLGGrcFjY7n

@mattj-monad

Copy link
Copy Markdown
Contributor Author

Force-pushed review fixes (TS router + conformance):

  • Path injection → cross-tenant IDOR: every browser-supplied id (and org) is now encodeURIComponent-escaped before it reaches a Monad URL, so a connectorId like in_1/../../<otherOrg>/inputs/<id> or x?force=true can no longer escape the tenant scope.
  • Upstream body leak: the raw Monad error body is logged server-side only; the browser gets a generic 502 upstream_error message. The generic 500 no longer echoes e.message.
  • Conformance: invalid kind is now asserted across all four kind-routes (was only /catalog); with the new code enum in feat(embed): add /embed OpenAPI contract + Spectral CI gate #12, error-model drift is now caught. ROUTER=ts 17/17.

@mattj-monad

Copy link
Copy Markdown
Contributor Author

Force-pushed: 404/409 are now real implementation, not deferred. The router translates Monad's upstream status into the contract's error model — 404 → not_found, 409 → conflict, everything else → 502 upstream_error — using the UpstreamError.status it already carries. The conformance mock now emulates the two real Monad constraints (PATCH an unknown pipeline → 404; create a duplicate connection, matched on the pipeline's component-id set → 409), and two new scenarios assert them. ROUTER=ts 19/19.

@mattj-monad

Copy link
Copy Markdown
Contributor Author

Force-pushed: hardened the conformance mock. The connectors-list mock now returns Monad's real envelope shape — { <kind>s: [...], pagination: {…} } — instead of a bare { <kind>s: [...] }. This catches a router that decodes the whole envelope as an array instead of reading just the <kind>s key (exactly the Go bug fixed in #15, found running the demo against live staging). Conformance still 19/19 across ts/go/python.

@mattj-monad

Copy link
Copy Markdown
Contributor Author

Force-pushed: extended the mock hardening. The connectors mock now also returns the list as null for an empty tenant (real Monad returns null, not []) alongside the pagination sibling — exercising both shapes (a populated inputs, an empty/null outputs). This catches routers that don't coalesce null[] (the Python bug fixed in #16, found running the demo live). Conformance 19/19 across ts/go/python.

Adds @monad-inc/embed-server — a zero-runtime-dependency /embed router that
mounts into Express or bare Node http via createEmbedRouter (core logic + node
adapter + Monad API adapter) — and the language-agnostic conformance suite that
proves any backend satisfies the contract over HTTP:

- conformance/ — Schemathesis response-conformance across all 9 routes plus
  scripted lifecycle scenarios (mint -> build -> status -> disable -> remove,
  and egress), driven against a stateful in-memory mock Monad. The harness boots
  the router under test as a subprocess (ROUTER=ts|go|python); only the ts server
  ships here — go/python servers land with their router layers.
- A CI "Conformance (ts)" job builds the router and runs ROUTER=ts. From here,
  conformance is the required gate every later router PR must pass.

Also ignores the Python harness .venv in prettier and formats two source files
that weren't prettier-clean.

Local: TS router 13 tests + conformance 14 checks green (ROUTER=ts); full
typecheck / lint / format / build clean.

Layer 3 (PRO-402), stacked on the browser client (PRO-401).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_011ASKA2VrufrNLGGrcFjY7n
@mattj-monad

Copy link
Copy Markdown
Contributor Author

Force-pushed: fully aligned the conformance mock with the real Monad API (read from its OpenAPI in monad/api) — this closes the PRO-423 audit. Beyond the pagination + null fixes, the mock now mirrors real response shapes for every endpoint the routers call:

  • POST /v2/{org}/pipelines201 + the full pipeline record (was 200 + {id})
  • GET /v2/{org}/pipelines{ pipelines: [...], pagination } (was a bare array)
  • pipeline detail → the record at top level — nodes/edges/enabled, no config wrapper
  • richer catalog / connector / output / status objects; /v3/sessions unchanged

Finding: the routers are mostly defensively written (they already handle both the old simplified shapes and the real ones — bare-array-or-{pipelines}, {config}-or-flat, etc.), so conformance stays 19/19 across ts/go/python. The only two real gaps were the Go pagination-sibling and Python null-list bugs (fixed in #15/#16) — which the previously-too-simple mock had hidden. The mock now catches any future non-defensive parsing.

…idelity

Adds a live conformance mode (MONAD_LIVE=1) that boots the router against the
real Monad API instead of the mock, driven by env-configurable credentials/org/
provisioned ids (all defaulting to the mock fixtures, so the hermetic run is
unchanged). Schemathesis is restricted to read-only GET operations in live mode.

Adds test_mock_fidelity.py + monad_schemas.py, which validate every mock
response against the Monad response shapes the routers consume — pinning the
mock to the documented upstream contract (needs jsonschema).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_011ASKA2VrufrNLGGrcFjY7n

@andrewc-monad andrewc-monad left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a few questions & comments about this new stuff:

  • If the conformance stuff is to be run only as validation, like during a CI run for each router implementation, can we move the folder into a test folder and make it more explicit that this part of the repo isn't something that a consumer of the embed package would need to worry about?
  • The root of the embed repo is the "workspace" for all packages in this repo, including @monad-inc/embed which is the existing embedded UI frontend with createConnectorFrame.
    • I think it makes more sense to have the new @monad-inc/embed-server moved to the packages/ folder in this repo as its own package, and then it can be exported under a subpath just like the pieces of packages/embed. I think this makes it so both client-side (createConnectorFrame, etc) and the server-side (the TS router), are both exported and available in a single package.
    • Doing it this way I think makes it more explicit that we want our customers who use this package to build their embedded UIs to use the server-side pieces we provide.

For the python & go versions, is it OK to push those to later? having those go out increases the surface area of what we have to support, and especially for python, even though we don't use it a lot at Monad, we'd be expected to be able to help end-users using embed to get it integrated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants