Skip to content

feat(embed): add the Go router (net/http) - #15

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

feat(embed): add the Go router (net/http)#15
mattj-monad wants to merge 2 commits into
feature/pro-402from
feature/pro-403

Conversation

@mattj-monad

Copy link
Copy Markdown
Contributor

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

What this adds

  • routers/go — the Go port of the /embed router: embed.Router(embed.Config{…}) on the standard library net/http, zero third-party dependencies (module github.com/monad-inc/embed/routers/go). Idiomatic Go; same contract.
  • Generalizes the CI conformance job into a ts + go matrix. The Go leg boots go run ./cmd/conformance and runs the same suite with ROUTER=go.

Verification (local)

  • go vet + go test pass
  • conformance: ROUTER=go 14/14, ROUTER=ts 14/14 (regression)

Reviewers judge idiomatic Go; conformance proves contract-parity with the TS router.

Stack

400 → 401 → 402 → 403 → 404 (Python) → 405 (docs/CI).

🤖 Generated with Claude Code

https://claude.ai/code/session_011ASKA2VrufrNLGGrcFjY7n

@mattj-monad

Copy link
Copy Markdown
Contributor Author

Force-pushed review fixes (Go router):

  • Path injection: url.PathEscape on every id + org (client.go and the embed.go delete paths).
  • Upstream body leak: upstream detail is logged server-side; the browser gets a generic 502.
  • Construction guard: Router() now fails fast with a clear panic if GetCustomerOrgID is nil, instead of a per-request nil-deref that kills the connection.
    go vet/go test pass; ROUTER=go conformance 17/17.

@mattj-monad

Copy link
Copy Markdown
Contributor Author

Force-pushed: 404/409 handling for the Go router — added a typed upstreamError{status, detail} (client.go) and upstream() now maps 404 → not_found / 409 → conflict / else 502 via errors.As. ROUTER=go conformance 19/19.

Comment thread routers/go/client.go

// esc URL-encodes a single path segment so browser-supplied ids can't inject
// query params or traverse the path (`/`, `?`, `#` are neutralised).
var esc = url.PathEscape

@Credgate Credgate Jul 31, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🤔 is this really needed?

Comment thread routers/go/client.go
}

func (c *client) listCatalog(ctx context.Context, kind string, allow []string) ([]CatalogType, error) {
data, err := c.do(ctx, "GET", "/v1/"+kind+"s", nil)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

why add s to every kind? why not rely on them passing the correct kindS? Should we make an enum for this so they know what kinds are valid?

Comment thread routers/go/client.go
}

func (c *client) listConnectors(ctx context.Context, org, kind string) ([]ConfiguredConnector, error) {
data, err := c.do(ctx, "GET", "/v1/"+esc(org)+"/"+kind+"s?limit=1000&offset=0", nil)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

same here kind[s]

@Credgate Credgate left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

List endpoints should support pagination with an ease of use Next() method or something so the caller can actually list all of the underlying resources. i believe list pipelines today returns a max of 10 by default since you're not passing any limit params. you can wrap all models in {rows, Pagination{total,limit,offset}}.

You have a couple methods around getting a pipelines status by a component but getting the component itself should already be returning that info in the component_of key.

Comment thread routers/go/client.go
}

func (c *client) listPipelines(ctx context.Context, org string) ([]pipeSummary, error) {
data, err := c.do(ctx, "GET", "/v2/"+esc(org)+"/pipelines/", nil)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This is only ever going to return 10 pipelines without proper pagination. All of our pagination is handled with a simple struct. you could implement a pagination response with a next funct or something to make pagination through returned results simple.

Comment thread routers/go/client.go
}

// statusByInput resolves the pipeline an input feeds, knowing only the input id.
func (c *client) statusByInput(ctx context.Context, org, inputID string) (PipelineStatus, error) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

GET /v1/{org}/outputs/{id}
GET /v1/{org}/inputs/{id}
already returns a component_of value so you shouldn't need to do this at all to get the status of an input.

Comment thread routers/go/client.go
"disabled": dis, "conditions": e.Conditions,
})
}
_, err = c.do(ctx, "PATCH", "/v2/"+esc(org)+"/pipelines/"+esc(pipelineID), map[string]any{

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This endpoint should work like a true patch now so you can safely pass just enabled bool and the other field states should be preserved.

Comment thread routers/go/client.go
// buildDevNull creates a throwaway dev/null output, then wires the input to it.
func (c *client) buildDevNull(ctx context.Context, org, inputID, name string) (BuiltPipeline, error) {
data, err := c.do(ctx, "POST", "/v2/"+esc(org)+"/outputs", map[string]any{
"output_type": "dev-null",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

you can just pass type now :P

Comment thread routers/go/client.go Outdated
Comment on lines +140 to +147
var page map[string][]struct {
ID string `json:"id"`
Type string `json:"type"`
Name string `json:"name"`
}
if err := json.Unmarshal(data, &page); err != nil {
return nil, err
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I have a hard time believing this unmarhsall will actually work

Comment thread routers/go/client.go
Comment on lines +276 to +290
var arr []pipeSummary
if json.Unmarshal(data, &arr) == nil && len(arr) > 0 {
return arr, nil
}
var obj struct {
Pipelines []pipeSummary `json:"pipelines"`
Data []pipeSummary `json:"data"`
}
if err := json.Unmarshal(data, &obj); err != nil {
return nil, err
}
if len(obj.Pipelines) > 0 {
return obj.Pipelines, nil
}
return obj.Data, nil

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This response model doesn't look correct to me either and what is the point of data vs pipelines key they are essentially the same thing. maybe claude was guessing what the key was or something? hah

Comment thread routers/go/README.md
| `APIKey` | Long-lived Monad API key (server-side only). |
| `APIBase` | Monad API base. Optional — defaults to `https://app.monad.com/api` (production). |
| `FrameOrigin` | Iframe origin returned by `GET /embed/config`. Optional — defaults to production. |
| `GetCustomerOrgID` | Map the authenticated request → the tenant's Monad team id. Return `("", nil)` or an error to reject (→ 401). |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Okay i see what i was missing - this is actually a security boundary. The guide may want to point to that fact. This should convert their bearer token -> monad organization id.

@mattj-monad

Copy link
Copy Markdown
Contributor Author

Force-pushed: fixed a real listConnectors bug found running the demo against live Monad staging. Real Monad returns { outputs: [...], pagination: {…} }; the Go decode used map[string][]struct{…}, which forces every top-level key (including pagination, an object) to be an array → json: cannot unmarshal object into []struct. TS/Python only read the <kind>s key, so they were unaffected. Now decodes map[string]json.RawMessage and unmarshals just the <kind>s array. Paired with the mock hardening in #14, conformance now covers this class. ROUTER=go 19/19.

@mattj-monad
mattj-monad force-pushed the feature/pro-403 branch 2 times, most recently from 25d2e86 to 3315bdb Compare July 31, 2026 21:12
mattj-monad and others added 2 commits July 31, 2026 15:42
Adds the Go port of the /embed router — embed.Router(embed.Config{...}) on the
standard library net/http, zero third-party dependencies (module
github.com/monad-inc/embed/routers/go). Same contract, idiomatic Go.

Generalizes the conformance CI job to a ts + go matrix; the Go leg boots
`go run ./cmd/conformance` and runs ROUTER=go against the same suite, proving
contract-parity with the TypeScript router.

Local: go vet + go test pass; conformance green for ROUTER=go and ROUTER=ts.

Layer 4 (PRO-403), stacked on the TS router + conformance (PRO-402).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_011ASKA2VrufrNLGGrcFjY7n
Parameterizes the Go conformance server (cmd/conformance) from env with the
same knobs as the TS/Python servers, defaulting to the mock fixtures.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_011ASKA2VrufrNLGGrcFjY7n
Credgate added a commit that referenced this pull request Aug 4, 2026
…[PRO-455]

Review of the Go port (#15) surfaced defects the TypeScript reference has too.
Validated against the API source rather than the published spec, which is stale.

- resolve a connector's pipeline via `GET /v1/{org}/{kind}s/{id}`'s
  `component_of` instead of scanning every pipeline. The scan sent no `limit`,
  so past the tenant's tenth pipeline it reported "not connected" for pipelines
  that exist.
- `setEnabled` now PATCHes `{enabled}` alone. PATCH became a true partial update
  in api#2163; the read-modify-write it replaces silently dropped node
  `config_overrides` and edge `schema_detection_spec` on every toggle.
- drain the connector list instead of one `limit=1000` request
- send `type` on output create; `output_type` is deprecated (api#2156)
- drop the speculative `data` / `config` response-shape fallbacks — Monad
  returns neither
- map `kind` to its path segment explicitly rather than appending "s"
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