feat(api): canonical GET /v1/events (org-wide) + deprecated nested alias — four-surface parity (slice 7)#649
Merged
Merged
Conversation
…ias — four-surface parity
Slice 7 of the org-wide events page: make events.list reachable ORG-WIDE across all
four surfaces, not just per-endpoint, and canonicalize the route.
Contract: `events.list` input `endpointId` is now OPTIONAL — absent = the org-wide
browse (RLS-scoped), present = drill into one endpoint. This propagates to the MCP
tool inputSchema automatically (verbatim shape) and to the CLI/API below.
DB shared read-handler: branch on endpointId — absent → `listOrgEvents` (no existence
gate, no endpoint-scoped headCursor); present → the existing `getEndpoint` NOT_FOUND
gate + `listEvents` + headCursor. An unknown/other-org endpointId STILL 404s; a
missing one no longer does.
HTTP routes: `GET /v1/events?endpointId=` is now CANONICAL (operationId `eventsList`,
org-wide by default); `GET /v1/endpoints/{endpointId}/events` stays as a DEPRECATED
ALIAS (operationId `eventsListByEndpoint`) so published SDKs / hardcoded URLs keep
working. RouteDef gained `alias`/`deprecated`/`operationId`; the generator honors them.
routes.test's strict one-route-per-capability BIJECTION is now over CANONICAL routes
only, plus an alias-integrity invariant (every alias names a capability with a
canonical route, is deprecated, has its own operationId) and a generated-spec
operationId-uniqueness check — strengthening the guard, not weakening it.
CLI: `wbhk events list` endpointId is now an OPTIONAL positional (omit for the whole
org); added the missing `--method` / `--dedup-strategy` / `--event-type` facet flags;
the client now calls the canonical `/v1/events`.
TS SDK: `events.list({ endpointId? })` / `listPage(...)` are canonical (org-wide
capable) with the new facets; `listByEndpoint` / `listPageByEndpoint` are the
deprecated nested-route methods (SDK spec-coverage ratchet updated for both).
Parity guard: a new facet×surface matrix test asserts every contract filter facet is
exposed as an API query param on both events routes — so a facet can't ship web/mcp
only while 404ing on REST. MCP is structural (verbatim shape); CLI has a flag-per-facet
test.
OpenAPI regenerated (byte-match + generated-spec tests green); webhook-go re-vendor
follows in a separate PR. Suites: contract 120, db 1442, openapi 40, api 213, cli 833,
sdk-ts 184, mcp 112; full typecheck across 29 packages.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RybDP88CyucJpg7hCbJVc9
…rity guard
Five findings from the slice-7 review:
- TS SDK: `list`/`listPage` dropped the positional endpointId (now filters-first).
A TypeScript caller gets a compile error, but an UNTYPED JS caller still passing
`list('ep_123')` would have it silently bound to `filters` → a whole-org list, a
silent data-scope widening on upgrade. Now a string first arg throws a TypeError
with the migration (`list({ endpointId })`).
- CLI: the endpointId positional became optional; `events list "$EP"` with an unset
shell var would silently list the whole org. An explicitly-EMPTY endpointId is now
a usage error; omitting it entirely stays the intended org-wide path.
- Parity guard: the openapi matrix test only covered contract↔HTTP-route, but its
comment claimed four-surface coverage. Corrected the comment, and added the CLI
half in packages/cli events.test.ts: a facet→flag sample table asserted COMPLETE
against the contract (a new facet with no entry fails) AND each flag EXERCISED
(an unwired flag is a usage error). MCP stays structural (verbatim input shape).
- browseEvents comment claimed a "7d default makes that rare" — that default is
web-component-only; the shared handler now backs api/cli/mcp too, where the 5s
statement_timeout is the ONLY universal backstop. Comment corrected to say so.
- Stale JSDoc on the CLI api-client eventsList cited the deprecated nested route;
updated to the canonical /v1/events (org-wide / ?endpointId=).
Tests: sdk-ts 185 (+string-arg-rejected), cli 835 (+empty-arg + facet-parity),
openapi 24, full typecheck 29. No behavior change to the org-wide read (RLS-scoped;
security review was clean).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RybDP88CyucJpg7hCbJVc9
Second review round on slice 7. Root cause behind three findings: an explicitly
PRESENT but empty-string scoping value was silently dropped to "no constraint" at
the route's buildInput, before the contract could reject it — for endpointId that is
a silent SCOPE widening (a caller who meant one endpoint gets the whole org).
Fixed at the choke point + defense-in-depth:
- Canonical route buildInput: only an ABSENT `?endpointId` (not in the query at all)
means org-wide. A PRESENT-but-empty `?endpointId=` is passed THROUGH so the
contract's `uuid.optional()` 400s it — the SDK's `{ endpointId: "" }` lands here
as `?endpointId=` too, so this closes the SDK + raw-HTTP + Go paths at once (Go's
empty id now 400s instead of widening, no Go change needed). Contract test pins
`{ endpointId: "" }` / whitespace → VALIDATION_ERROR.
- TS SDK: `list`/`listPage` now also reject `{ endpointId: "" }` fail-fast (a clear
TypeError, no round trip), alongside the existing legacy-string-arg guard.
- CLI `--event-type ""` now errors (mirrors the endpointId positional + --search),
rather than forwarding `?eventType=` which the server drops to no-filter.
- CLI parity guard strengthened: it now asserts each facet flag's value is actually
FORWARDED as `?<facet>=` (via a capturing fetch), not merely parsed — a
registered-but-unwired future flag now fails instead of passing green.
openapi.json is UNCHANGED (buildInput is runtime-only), so the webhook-go re-vendor
(#12) stays valid. Suites: contract 52, openapi 24, sdk-ts 186, cli 835; typecheck 29.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RybDP88CyucJpg7hCbJVc9
…hods, honest test title
Third review round on slice 7 — three findings:
- The TS SDK README's quickstart still called `events.list(endpoint.id)`, which now
throws with the new filters-first signature. Updated to `list({ endpointId })`.
(The docs.webhook.co Mintlify pages + the Python examples are slice-11 docs work —
updated once every SDK republishes to the canonical shape; the currently-published
Python SDK still has the old signature. webhook-go's README was fixed in its PR.)
- Adding `endpointId` to the shared EventsListFilters meant the deprecated
`listByEndpoint`/`listPageByEndpoint` (which take the id as a positional) would
SILENTLY ignore a `filters.endpointId` — scoping the wrong endpoint. They now throw
on an ambiguous `filters.endpointId`, and on an empty positional id (which would
otherwise build `/v1/endpoints//events`).
- routes.test title said "empty is dropped" while the assertion proves the opposite
(present-but-empty is passed through → 400). Corrected so a future reader can't
trust the title over the code and reintroduce the silent-widen bug.
sdk-ts 187, openapi 24. openapi.json unchanged (webhook-go #12 stays valid).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RybDP88CyucJpg7hCbJVc9
… error for JS undefined) Fourth-review DX finding: assertByEndpointArgs called endpointId.trim() without a typeof-string guard, so an untyped JS caller passing undefined/null to listByEndpoint()/listPageByEndpoint() got a raw "cannot read .trim of undefined" instead of the friendly "endpointId must be a non-empty endpoint id" message its sibling validateEventsListArg produces for the equivalent mistake. Guard typeof first. Test covers undefined + null. sdk-ts 187 green. openapi.json unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RybDP88CyucJpg7hCbJVc9
choraria
added a commit
to webhook-co/webhook-go
that referenced
this pull request
Jul 17, 2026
…e-vendor spec (#12) Mirrors the upstream slice-7 change (webhook-co/webhook#649): events.list is now org-wide-capable via the canonical `GET /v1/events?endpointId=`. - `Events.List(params)` / `ListPage(ctx, params)` now hit the canonical `/v1/events`, org-wide by default or drilled to one endpoint via `params.EndpointID`. - `Events.ListByEndpoint(endpointID, params)` / `ListPageByEndpoint(...)` are the DEPRECATED methods that hit the nested `/v1/endpoints/{id}/events` alias. - EventsListParams gains EndpointID + the Method / DedupStrategy / EventType facets. - Re-vendored testdata/openapi.json to match the new upstream spec (adds the canonical route + marks the nested one deprecated); spec-coverage ratchet updated for both routes. BREAKING (on upgrade): `Events.List` / `ListPage` dropped their positional endpointID arg — pass `&EventsListParams{EndpointID: &id}` instead, or use `ListByEndpoint`. Already-deployed callers are unaffected (the nested HTTP route still works). go test ./... green; gofmt clean. Claude-Session: https://claude.ai/code/session_01RybDP88CyucJpg7hCbJVc9 Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…down
The Python SDK is hand-written with its own spec-coverage ratchet (twin of
sdk-ts's), so slice 7's new canonical GET /v1/events route surfaced as a red
`test (python)`: "spec routes with no SDK method and no exemption:
['GET /v1/events']". This mirrors the TS/Go split onto Python.
`events.list` / `list_page` now hit the canonical `/v1/events` with
`endpoint_id` as a keyword-only, optional drill-down (omit = org-wide), and
carry the three new facets (method, dedup_strategy, event_type). The nested
route lives on new deprecated `list_by_endpoint` / `list_page_by_endpoint`
methods so the ratchet's `GET /v1/endpoints/{endpointId}/events` thunk still
resolves. Shared filter query hoisted into `_query`; only endpointId placement
differs by route.
An empty endpoint_id emits `?endpointId=` (with_query drops None, keeps ""),
which the route choke point forwards to contract validation → 400 — the same
fail-fast the route fix gives every surface.
181 tests pass, 99.81% coverage (gate 95%).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RybDP88CyucJpg7hCbJVc9
The format check (black==26.5.1) wraps the >88-col dedupStrategy line in the new _query helper. No behaviour change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RybDP88CyucJpg7hCbJVc9
choraria
marked this pull request as ready for review
July 17, 2026 22:37
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.
Slice 7 of the org-wide events page — four-surface parity
Makes
events.listreachable org-wide across all four surfaces (api/cli/mcp + web already had it), and canonicalizes the route.What changed
events.listinputendpointIdis now optional — absent = org-wide (RLS-scoped), present = drill into one endpoint. Propagates to the MCP tool inputSchema automatically (verbatim shape).endpointId— absent →listOrgEvents(no existence gate, no endpoint-scoped headCursor); present → the existinggetEndpointNOT_FOUND gate +listEvents+ headCursor. Unknown/other-org endpointId still 404s; a missing one no longer does.GET /v1/events?endpointId=is now canonical (operationIdeventsList);GET /v1/endpoints/{endpointId}/eventsis a deprecated alias (operationIdeventsListByEndpoint) so published SDKs / hardcoded URLs keep working.routes.test's bijection is now over canonical routes only, plus alias-integrity + operationId-uniqueness invariants (strengthened, not weakened).wbhk events listendpointId is an optional positional (omit for the whole org); added the missing--method/--dedup-strategy/--event-typeflags; calls the canonical route.events.list({ endpointId? })/listPage(...)are canonical + org-wide-capable with the new facets;listByEndpoint/listPageByEndpointare the deprecated nested-route methods.Founder decision
Canonical route keeps the clean
eventsListSDK method; the deprecated alias iseventsListByEndpoint. Already-shipped apps keep working via the live nested URL; upgraders get a one-line migration.Test plan
webhook-go vendored spec is currentcheck will be red here until that merges).🤖 Generated with Claude Code
https://claude.ai/code/session_01RybDP88CyucJpg7hCbJVc9