Skip to content

feat(api): canonical GET /v1/events (org-wide) + deprecated nested alias — four-surface parity (slice 7)#649

Merged
choraria merged 7 commits into
mainfrom
events-parity-canonical
Jul 17, 2026
Merged

feat(api): canonical GET /v1/events (org-wide) + deprecated nested alias — four-surface parity (slice 7)#649
choraria merged 7 commits into
mainfrom
events-parity-canonical

Conversation

@choraria

Copy link
Copy Markdown
Contributor

Slice 7 of the org-wide events page — four-surface parity

Makes events.list reachable org-wide across all four surfaces (api/cli/mcp + web already had it), and canonicalizes the route.

What changed

  • Contract: events.list input endpointId is now optional — absent = org-wide (RLS-scoped), present = drill into one endpoint. Propagates to the MCP tool inputSchema automatically (verbatim shape).
  • DB shared read-handler: branches on endpointId — absent → listOrgEvents (no existence gate, no endpoint-scoped headCursor); present → the existing getEndpoint NOT_FOUND gate + listEvents + headCursor. Unknown/other-org endpointId still 404s; a missing one no longer does.
  • HTTP routes: GET /v1/events?endpointId= is now canonical (operationId eventsList); GET /v1/endpoints/{endpointId}/events is a deprecated alias (operationId eventsListByEndpoint) 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).
  • CLI: wbhk events list endpointId is an optional positional (omit for the whole org); added the missing --method / --dedup-strategy / --event-type flags; calls the canonical route.
  • TS SDK: events.list({ endpointId? }) / listPage(...) are canonical + org-wide-capable with the new facets; listByEndpoint / listPageByEndpoint are the deprecated nested-route methods.
  • Parity guard: a new facet×surface matrix test asserts every contract filter facet is exposed on both events routes (so a facet can't ship web/mcp-only while 404ing on REST).

Founder decision

Canonical route keeps the clean eventsList SDK method; the deprecated alias is eventsListByEndpoint. Already-shipped apps keep working via the live nested URL; upgraders get a one-line migration.

Test plan

  • contract 120, db 1442, openapi 40, api 213, cli 833, sdk-ts 184, mcp 112; full typecheck across 29 packages. OpenAPI regenerated (byte-match green).
  • webhook-go re-vendor follows in a separate PR (the webhook-go vendored spec is current check will be red here until that merges).

🤖 Generated with Claude Code

https://claude.ai/code/session_01RybDP88CyucJpg7hCbJVc9

…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
choraria and others added 4 commits July 17, 2026 22:41
…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>
choraria and others added 2 commits July 17, 2026 23:27
…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
choraria marked this pull request as ready for review July 17, 2026 22:37
@choraria
choraria merged commit 0dc3b27 into main Jul 17, 2026
32 checks passed
@choraria
choraria deleted the events-parity-canonical branch July 17, 2026 22:37
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.

1 participant