Skip to content

Commit 8558668

Browse files
fix(runtime): an exact POST /webhooks/redeliver dispatcher domain for kernels with no raw app (#22842)
Fixes #22755 Clause-②: no Segment 2 of the webhooks half of #22564's stage 2, under the maintainer ruling recorded verbatim as 「A + 扫类」 on #22438 (director record `6079645593`, item 2). A hosted tenant kernel owns no socket, so it has no raw Hono app for `plugin-webhooks` to mount the redeliver door on: on that shape `POST /api/v1/webhooks/redeliver` answered `404 ROUTE_NOT_FOUND`, even though the plugin had registered the `webhooks` slot whose `handleRedeliver` member (segment 1's contract, #22756's member) serves it. This PR gives the dispatcher that route. Parent #22564 remains open. ## What changed **`@objectstack/runtime`** (the one published change; `patch` changeset) - New dispatcher domain `packages/runtime/src/domains/webhooks.ts`, registered in `HttpDispatcher` beside `/approvals/act` (ADR-0076 D11: the dispatcher owns registration, one route bridges to one service slot). It claims exactly `/webhooks/redeliver` (`match: 'exact'`) for `POST` only, resolves the request kernel's `webhooks` slot **per request**, and forwards `context.request` unread to `IWebhookService.handleRedeliver`, returning that `Response` untouched. - Absent slot, or a service without `handleRedeliver`: `501 NOT_IMPLEMENTED` (an existing standard code), with a message naming which. Never `ROUTE_NOT_FOUND`. - A member that throws: sanitised `500 INTERNAL_ERROR`, original error logged. A request whose body a transport already consumed: also `500`, logged, and the member is not asked (it would otherwise answer "Request body must be JSON." for a well-formed request). - No preamble-gate change. The domain adds no credential rule of its own: the member judges the caller from the request through the `auth` service, as the self-hosted mount does. Unlike `/approvals/act`, this path is NOT exempted from either dispatcher gate (no ADR-0069 allow-list entry, no membership skip), so the hosted door is stricter than the mount, never looser. `packages/core` is untouched. - `route-ledger.ts`: one `server-only` row, `POST /webhooks/redeliver` (no SDK method exists and none is declared); census 85 to 86. **Pins and bookkeeping (no published surface)** - `scripts/check-route-envelope.mjs`: declares `webhooks.ts` with `handBuilt: 0`. - `packages/qa/http-conformance`: devDependencies on `@objectstack/plugin-webhooks` and `@objectstack/service-messaging`, both aliased to source (anchored) in its vitest config, so one suite boots the real plugin on both doors; `pnpm-lock.yaml` gains the 6-line importer link. `scripts/engine-double-contract.pinned.json` records that suite's pinned fake-engine `findOne`. - `packages/qa/dogfood`: a new `enforced` authz row, `webhook-redeliver-session`, classifies the new dispatcher-domain key (the baseline is untouched, still 30); the probe census and both matrix headers move 85 to 86 rows, 22 to 23 domains, 21 to 22 `covers` keys; the showcase live-mount parity gate pins the row as served by plugin-webhooks' raw-app mount on a self-hosted boot (the `/approvals/act` reason). ## The PM's four mechanism assumptions, as measured 1. **Premise: TRUE.** Measured on `main` `b7cd1af9d` before any change, with the real `WebhookOutboxPlugin` over the real `MessagingService` on a `LiteKernel` with no `http.server`, under the real `createHonoApp`: the `webhooks` slot held `handleRedeliver` (a function), and `POST /api/v1/webhooks/redeliver` answered `404 ROUTE_NOT_FOUND` ("Route Not Found: /webhooks/redeliver") for an authenticated and an anonymous caller alike. 2. **Slot timing: held.** plugin-webhooks registers the slot at `kernel:ready`, only once its veto installs (`bootRedeliverDoor`). The domain reads it per request and records nothing; a pin registers the slot after a request was answered `501` and the next request reaches the member. 3. **Anonymous: both doors answer the same bytes.** Self-hosted mount: `401 UNAUTHENTICATED`, body `{"success":false,"error":{"code":"UNAUTHENTICATED","message":"Sign in to redeliver webhook deliveries."}}`. Hosted door after this change: the same status, headers and body. Neither dispatcher gate decides anything for an anonymous caller (membership returns early with no user; the auth gate needs a session), so the member's own refusal is the answer. Nothing needed an allow-list or gate change, and none was made. 4. **Method.** Measured on both doors: the self-hosted door answers every non-`POST` (`GET`, `PUT`, `PATCH`, `DELETE`, `HEAD`) with plugin-hono-server's `404 ENDPOINT_NOT_FOUND`; the hosted door answers the dispatcher's `404 ROUTE_NOT_FOUND`. The hosted answer is pinned, and the member never runs for a non-`POST` (spied on the real service; the outbox row stays `dead`). ## Pins - `packages/runtime/src/domains/webhooks-redeliver.test.ts` (13): the real `HttpDispatcher` on a kernel with no `http.server` and a stub member that reads its own JSON body. Forwarding with the body unread; the member's refusal passed through; the exact `POST` claim (no other method, sub-path or sibling); `501` for an absent slot and for an absent member; per-request slot resolution; the sanitised `500`s; and the gates: a signed-in non-member refused `403 PROJECT_MEMBERSHIP_REQUIRED` on both spellings, a gated session refused `403` by ADR-0069, while a member of the environment and an anonymous caller reach the member. - `packages/qa/http-conformance/src/hono-webhooks-redeliver.conformance.test.ts` (7), the end-to-end pin this card owes (#22756 landed first): the real plugin on both doors. Through `createHonoApp`, an authenticated `POST` replays a finished delivery and the row moves `dead` to `pending` in the real outbox (so the JSON body crossed the catch-all unread); ten refusals (anonymous `401`, not-JSON and empty `400`, missing and blank `400`, unknown and cross-organization `404`, not-finished, never-sent and veto `409`) answer the same status, `code`, headers and body bytes at both doors (row ids masked); non-`POST` as above; the typed absence across the segment boundary with the member removed from the real service, with the slot left empty by the plugin's own boot (a messaging service that cannot take the veto), and with no plugin at all, each `501 NOT_IMPLEMENTED`, with `/webhooks/redeliverx` as the route-miss control; and the CONTROL below. - CONTROL, kernel with `http.server`: the route is mounted once, as `POST`, on the raw app, and the mount answers with its own bytes (no dispatcher `httpStatus` / `route` / `hint` keys), equal to the bytes measured on the base commit. This PR adds no mount there: `dispatcher-plugin.ts` mounts explicit routes and is untouched, and the showcase parity gate (a real self-hosted composition) still sees no port mount for the path. ## Ablations (one-off, from committed state, through `scripts/ablation-replace.mjs`, restore proven by blob hash equal to HEAD, empty `git diff HEAD` and a clean `git status --porcelain`) - **Registration removed** (the `register(createWebhooksRedeliverDomain(...))` call replaced by `void createWebhooksRedeliverDomain;` so the DTS build still passes `noUnusedLocals`), runtime `dist/` rebuilt, `ablation-dist-preflight --absent` green (marker absent from all 6 built files; tree mutate leg confirmed). Unit pins: 10 of 13 red, every one on `ROUTE_NOT_FOUND` / "Route Not Found: /webhooks/redeliver"; the 3 that stay green are the three gate pins (both gates run before route resolution, so they refuse with or without the domain). Wire pins: 5 of 7 red (`404` where `200` or `501` was expected, and the anonymous parity row); the 2 that stay green are the non-`POST` pin and the self-hosted CONTROL, neither of which depends on the domain. Restore leg: blob equal to HEAD, tree clean, rebuilt, marker present in 2 built files, 13 of 13 and 7 of 7 green. - **Typed-absence guards, each disabled on purpose** (unit level; the runtime suite resolves the domain from source). Slot guard off: exactly the two slot-absence pins red (`TypeError: Cannot read properties of undefined (reading 'handleRedeliver')`). Member guard off: exactly the member-absence pin red (`500 INTERNAL_ERROR` instead of `501 NOT_IMPLEMENTED`). Restored, 13 of 13 green. The wire suite's three absence cases remove the member and the slot on the real composition instead (see Pins). - **Showcase parity pin renamed**: the gate reports `POST /webhooks/redeliver — LEDGERED BUT NOT MOUNTED` plus the stale pin (2 of 8 red), and the boot log shows `[webhook-outbox] redeliver endpoint mounted at POST /api/v1/webhooks/redeliver`, so the pin is load-bearing and its reason is the measured one. Restored, 8 of 8. ## Verification Measured on `be4051ae2` (this branch with `main` `efcbac73c` merged; after the merge `pnpm install --frozen-lockfile`, a full `turbo run build` of the workspace, 73 of 73 tasks, and `rm -rf packages/runtime/.objectstack`). - `@objectstack/runtime`: `local` project in three chunks, 104 + 123 + 123 files, 1985 + 1520 + 1546 tests passed (19 skipped); `repo` project 3 files, 751 passed; `typecheck` (src + test layer) 0, the new test file counted in the test-layer program with `--listFiles`. - `@objectstack/http-conformance`: 11 files, 117 passed; `typecheck` 0. - `@objectstack/dogfood`: `authz-conformance.test.ts`, `authz-probe-blind-spot.test.ts` and `route-ledger-live-mount-parity.dogfood.test.ts`, 3 files, 100 passed; `typecheck` 0. The rest of that suite is CI's. - Gates: `dispatch-gates --commands` derives 100 families for this diff (the dispatch list's 53 without `pnpm lint`, plus 47 the diff adds); all 100 exit 0 on `be4051ae2`, and `dispatch-gates --ran` with every exit recorded reads 100 derived, 100 run, 0 NOT-MEASURED, 0 UNRUN. Two were red on the way and fixed in this diff: `check:route-ledger-census` (85 to 86) and `check:engine-double-contract` (the new suite's fake `findOne` now opens with `assertEngineFindOnePredicate`; the pinned ledger records it). `check:dual-build-cjs-loads` first refused with its prerequisite (8 packages without `dist/`); after the full build it measured 107 entries across 66 packages. - Lint, narrowed and proven: the population is `eslint.config.mjs`'s own `files` globs minus `NEVER_LINTED`, which takes 12 of this diff's 16 files (the changeset, two JSON files and the lockfile are outside it); `eslint --no-inline-config --format json` over those 12 reports 12 files, 0 errors, 0 warnings, 0 fatal, none ignored. Invariance: the config enables no type-aware linting (stated in the config itself) and the diff touches none of its inputs, so it cannot move the verdict of an untouched file. The full `pnpm lint` is CI's. ## Acceptance notes - Two doors, two 404 codes for a non-`POST`: `ROUTE_NOT_FOUND` (hosted) and `ENDPOINT_NOT_FOUND` (self-hosted). Pre-existing on the self-hosted side and the same split `/approvals/act` recorded; out of this card's scope. - The hosted door is stricter than the mount in two measured ways, as the contract states: a signed-in caller who is not a member of the request's environment gets `403 PROJECT_MEMBERSHIP_REQUIRED`, and a session owing an authentication step gets the ADR-0069 `403`. The self-hosted mount has neither gate. - The scoped spelling `/api/v1/environments/:id/webhooks/redeliver` reaches the domain through the dispatcher's generic scope strip (membership-checked like the unscoped one); the self-hosted mount has no such spelling. - AGENTS.md route rule 5 (closed query-parameter set) does not apply: this is a `POST` whose input is its JSON body, and neither the domain nor the member reads `req.query`. - Other raw-app mounts seen while reading (metadata HMR, cloud-connection's marketplace proxy, local install and runtime config, the CLI console, trigger-api's inbound hooks) are the sweep's to classify under the same ruling; trigger-api's is a sibling card on this lane (#22773 is held behind this one). Named here, not acted on. Carrier: the #22564 sweep owner. - The error-code ledger's provenance note for plugin-webhooks' `INVALID_REQUEST` (`packages/spec/src/api/error-code-ledger.zod.ts`) describes the raw-app mount and stays true; the same code now also leaves through the dispatcher door, which is the member's own envelope passed through. No spec edit (that is the `domain:spec` seat's surface); noted only. - The branch merged `main` once (`efcbac73c`) before this PR; the three incoming commits touch none of these files. --- _Generated by [Claude Code](https://claude.ai/code/session_01B5CHJNXuuqzChM4w6hkTN4)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent d489318 commit 8558668

16 files changed

Lines changed: 980 additions & 24 deletions
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@objectstack/runtime': patch
3+
---
4+
5+
fix(runtime): the webhook redeliver button works on a kernel with no raw HTTP app — the dispatcher serves `POST /webhooks/redeliver`
6+
7+
Clause-②: no
8+
9+
On a self-hosted server, `@objectstack/plugin-webhooks` serves the operator's redeliver button by mounting `POST /api/v1/webhooks/redeliver` on the raw Hono app. A hosted tenant kernel has no raw app, so on that shape the path answered `404 ROUTE_NOT_FOUND`, even though the plugin had registered the `webhooks` service that can serve it.
10+
11+
**The door that changes: `HttpDispatcher`, for a kernel with no raw app.** It now serves exactly `POST /webhooks/redeliver` (no sub-paths, no other method). It forwards the request, body unread, to the request kernel's `webhooks` service (`IWebhookService.handleRedeliver`), read on every request, and returns that `Response` as it is. So the hosted door answers what the self-hosted mount answers: `200` with the replayed row, and the same `401`, `400`, `404` and `409` refusals with the same bodies. An anonymous call is refused `401 UNAUTHENTICATED` there exactly as on the mount.
12+
13+
- No `webhooks` service registered (plugin-webhooks absent, or present without its redelivery veto), or one without `handleRedeliver`: `501 NOT_IMPLEMENTED`, naming which. Never `ROUTE_NOT_FOUND`.
14+
- A member that throws: a sanitised `500 INTERNAL_ERROR`, with the original error in the server log. The same answer, logged, when a transport hands over a request whose body was already read, instead of a "not JSON" refusal for a well-formed request.
15+
- Any method other than `POST` is not claimed: it answers the dispatcher's ordinary `404 ROUTE_NOT_FOUND` and never reaches the service.
16+
- The dispatcher's two gates apply to this path unchanged: a session that still owes an authentication step, or a signed-in caller who is not a member of the request's environment, is refused `403` before the service runs. This door is therefore stricter than the self-hosted mount, never looser.
17+
18+
Nothing changes on a self-hosted server: the plugin's raw-app mount still serves the path there and never reaches the dispatcher.

‎packages/qa/dogfood/test/authz-conformance.matrix.ts‎

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
// dispatcher domain files.
2626
//
2727
// The population comes from `packages/rest/src/rest-route-ledger.ts` (81 rows
28-
// / 18 families) and `packages/runtime/src/route-ledger.ts` (85 rows / 22
28+
// / 18 families) and `packages/runtime/src/route-ledger.ts` (86 rows / 23
2929
// domains) because those two are enumerated from a RUNNING server and guarded
3030
// in both directions by their own conformance tests — so a new family or
3131
// domain cannot be silently absent from them, and therefore cannot be silently
@@ -67,9 +67,10 @@
6767
// The ledgers supply the POPULATION; the classification stays a reviewed row
6868
// here.
6969
//
70-
// Of the 40 ledger keys, 10 are classified by rows below (re-derived when the
71-
// `/approvals/act` domain arrived classified, #22576, after `/i18n` left the
72-
// baseline, #22432); the other 30 are enumerated one by one,
70+
// Of the 41 ledger keys, 11 are classified by rows below (re-derived when the
71+
// `/webhooks/redeliver` domain arrived classified, #22755, after
72+
// `/approvals/act` did, #22576, and `/i18n` left the baseline, #22432); the
73+
// other 30 are enumerated one by one,
7374
// dated, and pinned SHRINK-ONLY in `authz-ledger-population.baseline.ts`,
7475
// whose MAX note dates every step down from the 34 of 40 it held on the
7576
// 2026-08-31 adoption day. That list can only get shorter: growth, staleness,
@@ -99,12 +100,12 @@
99100
// [commit 2ce1eb41b] That completeness is over ROUTES, not over primitives: a primitive
100101
// enforced by a predicate inside an existing resolver adds no entry point, so
101102
// it can be neither UNCLASSIFIED nor STALE. Measured against the rows below:
102-
// 44 of 55 carry no `covers` key at all (11 rows, 21 keys, every one an
103-
// HTTP/transport pin), and 38 of the file's 48 `enforced` rows are exactly
103+
// 44 of 56 carry no `covers` key at all (12 rows, 22 keys, every one an
104+
// HTTP/transport pin), and 38 of the file's 49 `enforced` rows are exactly
104105
// that in-resolver shape — the ADR-0049/#8613 `active` rows and the ADR-0091
105106
// grant-validity-window row among them (see their own blocks further down)
106107
// are the normal case, not an exception. Of the
107-
// 21 `covers` keys that DO exist, 7 are GATE pins tied to the enforcement call
108+
// 22 `covers` keys that DO exist, 7 are GATE pins tied to the enforcement call
108109
// itself, not merely a function name — delete `shouldDenyAnonymous` from
109110
// `/actions`, `/automation`, `/packages`, `/analytics` or `/i18n`, or drop the MCP
110111
// context-threading / stdio principal binding, and the pinned key vanishes from source, its row
@@ -330,6 +331,25 @@ export const AUTHZ_CONFORMANCE: AuthzPrimitive[] = [
330331
covers: ['dispatcher-domain:route-ledger.ts:/approvals/act'],
331332
note: 'The DISPATCHER domain only: on a self-hosted kernel the same path is served from the raw Hono app and never reaches the dispatcher, which is why the route-ledger rows are pinned unobservable on the showcase boot. Unit- and wire-proven, not dogfood-proven: plugin-approvals action-page-member.integration.test.ts drives the member on a real kernel and store (GET never decides; POST decides as the bound approver, audited as that approver; unknown, expired and consumed tokens told on the page with 200; no cookie or Authorization read); runtime domains/approvals-act.test.ts pins the domain on the real HttpDispatcher (the request forwarded unread, HEAD answered as a bodiless GET, 501 for an absent slot or member, a SIGNED-IN non-member reaching the member exactly as an anonymous caller does while /approvals/act/x, /approvals/actx, /approvals, /approvals/requests and /data/task stay membership-checked); core/security/auth-gate.test.ts pins the exact allow-list entry; http-conformance hono-approvals-act.conformance.test.ts drives the real plugin through createHonoApp and the self-hosted mount, where a form POST redeems a live token and HEAD answers identically at both doors.' },
332333

334+
// #22755 — the `/webhooks/redeliver` dispatcher domain (segment 2 of #22564's
335+
// stage 2; ruling A on #22438). Unlike `approval-action-token` above, the
336+
// credential here IS the session: the member judges the caller from the
337+
// request's own credentials through the `auth` service, so the honest class
338+
// is "a signed-in caller, scoped to their active organization", and the
339+
// dispatcher's two preamble gates apply to the path unexempted. ⛔ Ids stay
340+
// in this comment, out of the row's strings.
341+
//
342+
// Not HIGH_RISK, for the list's own reason: it marks a primitive guarding
343+
// object DATA through a sibling HTTP entry point, and this door serves no
344+
// record read or query — it replays one outbox row the caller's organization
345+
// owns. Its proofs are the plugin's member suite and the two-door wire suite,
346+
// neither of which lives in this directory; a dogfood proof driving the route
347+
// at its literal wire path is the honest upgrade, and would claim this row.
348+
{ id: 'webhook-redeliver-session', summary: 'Webhook redeliver door (`POST /webhooks/redeliver`, the operator button that replays a finished delivery) — any signed-in caller may press it, and only for a delivery row of their own active organization; an anonymous caller is refused by the member itself, exactly as the self-hosted mount refuses it', state: 'enforced',
349+
enforcement: 'plugin-webhooks/webhook-outbox-plugin.ts serveRedeliver — the ONE body both faces run: the caller resolved from the request\'s own credentials through the auth service (no signed-in user: 401 UNAUTHENTICATED), the caller\'s active organization threaded into messaging.redeliverHttp as the tenant (a row outside it: 404 RESOURCE_NOT_FOUND), and the redelivery veto installed with messaging before the slot is registered (a row whose subscription or signing secret is gone: 409 DELIVERY_NOT_ELIGIBLE); reached through IWebhookService.handleRedeliver by runtime/domains/webhooks.ts, which forwards the transport\'s request UNREAD, resolves the slot per request and adds no credential of its own (an absent slot or member is a typed 501, never a route miss). The two dispatcher preamble gates apply to the exact path with NO exemption: core/security/auth-gate.ts ALLOW_ROUTES does not list it (a session owing an authentication step is refused 403 first) and runtime/http-dispatcher.ts enforceProjectMembership does not skip it (a signed-in non-member of the environment is refused 403 PROJECT_MEMBERSHIP_REQUIRED first). The self-hosted raw-app mount (plugin-webhooks registerAdminRoutes) runs the same body on the same outbox',
350+
covers: ['dispatcher-domain:route-ledger.ts:/webhooks/redeliver'],
351+
note: 'The DISPATCHER domain only: on a self-hosted kernel the same path is served from the raw Hono app and never reaches the dispatcher, which is why the route-ledger row is pinned unobservable on the showcase boot. Unit- and wire-proven, not dogfood-proven: plugin-webhooks webhook-redeliver-member.test.ts drives the member on a real kernel, messaging service and outbox (every refusal the contract declares, the tenant scope, the veto, byte parity with the mount); runtime domains/webhooks-redeliver.test.ts pins the domain on the real HttpDispatcher (the request forwarded unread, POST alone claimed, 501 for an absent slot or member, the slot read per request, a signed-in non-member and a gated session stopped before the member while a member of the environment and an anonymous caller reach it); http-conformance hono-webhooks-redeliver.conformance.test.ts drives the real plugin through createHonoApp and the self-hosted mount, where a replay lands in the outbox and every refusal, anonymous first, answers the same bytes at both doors.' },
352+
333353
// ── #2992 / ADR-0096 D4 — latent execution surfaces (pre-wiring identity
334354
// admission). Neither surface is reachable by a client today; these rows
335355
// register their identity posture NOW so the ratchet (see the probes +

‎packages/qa/dogfood/test/authz-conformance.test.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -990,6 +990,8 @@ describe('the ledger-sourced population and its baseline bite', () => {
990990
'dispatcher-domain:route-ledger.ts:/mcp',
991991
'dispatcher-domain:route-ledger.ts:/meta',
992992
'dispatcher-domain:route-ledger.ts:/packages',
993+
// [#22755] classified by `webhook-redeliver-session`.
994+
'dispatcher-domain:route-ledger.ts:/webhooks/redeliver',
993995
'rest-family:rest-route-ledger.ts:metadata',
994996
// [#22430] classified by `anonymous-deny-api-description`.
995997
'rest-family:rest-route-ledger.ts:openapi',

‎packages/qa/dogfood/test/authz-probe-blind-spot.census.ts‎

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -106,11 +106,11 @@
106106
// `RestServer.getRoutes()` on a booted server and guarded per route by
107107
// `rest-route-ledger.conformance.test.ts`. It reaches all 17 registrars;
108108
// this table reaches 1.
109-
// `packages/runtime/src/route-ledger.ts`: 85 rows over 22 domains. Its
109+
// `packages/runtime/src/route-ledger.ts`: 86 rows over 23 domains. Its
110110
// machine contract is DOMAIN-level, by live registry introspection
111111
// (`domainRegistry.list()`), the per-route rows being documentation. It
112112
// covers all 15 `async handle*(` methods in `http-dispatcher.ts` and all
113-
// 17 `DomainRoute` prefixes declared by the 16 domain files.
113+
// 18 `DomainRoute` prefixes declared by the 17 domain files.
114114
// Nine more ledgers exist repo-wide (290 rows in total).
115115
//
116116
// ⭐ On FILE SELECTION the ledgers are simply the right answer, and that is
@@ -298,7 +298,11 @@ export interface ProbeTableReading {
298298
// `approval-action-token` row covers one more key, the `/approvals/act`
299299
// dispatcher-domain key, which arrived classified rather than through the
300300
// baseline (the baseline still holds 30 and its ceiling did not move).
301-
export const PROBE_TABLE: ProbeTableReading = { entries: 20, files: 15, keys: 21 };
301+
// [#22755] 20 / 15 / 21 -> 20 / 15 / 22: no probe and no file joined; the
302+
// `webhook-redeliver-session` row covers one more key, the
303+
// `/webhooks/redeliver` dispatcher-domain key, which arrived classified rather
304+
// than through the baseline (the baseline still holds 30, its ceiling unmoved).
305+
export const PROBE_TABLE: ProbeTableReading = { entries: 20, files: 15, keys: 22 };
302306

303307
/**
304308
* The probe count `authz-conformance.matrix.ts`'s header states.
@@ -387,7 +391,7 @@ export const PROBE_FILE_CENSUS: readonly ProbeFileReading[] = [
387391
file: 'packages/runtime/src/route-ledger.ts',
388392
kinds: ['ROUTE_ENUMERATION'],
389393
probes: 1,
390-
keys: 22,
394+
keys: 23,
391395
// [#13953] 80 -> 82: the two operator run-lifecycle rows
392396
// (`POST /automation/:name/runs/:runId/cancel` and `.../restore-suspension`).
393397
// Both carry `domain: '/automation'`, an EXISTING key, so `reachable` moves
@@ -418,11 +422,19 @@ export const PROBE_FILE_CENSUS: readonly ProbeFileReading[] = [
418422
// `/security` domain dispatches to it — so the domain-file counts in the
419423
// note below are unchanged. Controls moved with the row ("route: '" and
420424
// "domain: '" 84 -> 85, RouteLedgerEntry unchanged at 2).
421-
population: 85,
422-
reachable: 85,
425+
// [#22755] 85 -> 86: the `POST /webhooks/redeliver` row arrived with the
426+
// dispatcher's `/webhooks/redeliver` domain (the webhook redeliver door for
427+
// a kernel with no raw app). It carries `domain: '/webhooks/redeliver'`, so
428+
// it is REACHABLE — `reachable` moves with `population`, `blindSpot` stays
429+
// 0 — and it carries a NEW distinct domain: `keys` 22 -> 23, one new key,
430+
// classified by `webhook-redeliver-session` rather than added to the
431+
// baseline. Controls moved with the row ("route: '" and "domain: '"
432+
// 85 -> 86, RouteLedgerEntry unchanged at 2).
433+
population: 86,
434+
reachable: 86,
423435
blindSpot: 0,
424436
populationRule: 'ledger rows inside ROUTE_LEDGER; reachable = rows carrying a `domain` (each distinct value mints a key)',
425-
controls: { "route: '": 85, "domain: '": 85, RouteLedgerEntry: 2 },
437+
controls: { "route: '": 86, "domain: '": 86, RouteLedgerEntry: 2 },
426438
// [#22432] Re-derived on the merged ref: the note's two counts had stood at
427439
// 11 files / 5 classified / 16 baselined since before the `/analytics`
428440
// domain was classified (#21061), which named one more file and moved one
@@ -433,15 +445,18 @@ export const PROBE_FILE_CENSUS: readonly ProbeFileReading[] = [
433445
// [#22576] `domains/approvals.ts` is a sixteenth domain file with a
434446
// seventeenth plain prefix, named by no probe, and its key is the eighth
435447
// classified: 22 domains, 14 still in the baseline.
448+
// [#22755] `domains/webhooks.ts` is a seventeenth domain file with an
449+
// eighteenth plain prefix, named by no probe, and its key is the ninth
450+
// classified: 23 domains, 14 still in the baseline.
436451
note:
437452
'The dispatcher half. Its machine contract is DOMAIN-level by live registry introspection ' +
438453
'(domainRegistry.list()), guarded in BOTH directions by route-ledger.conformance.test.ts: every ' +
439454
'registered domain needs a row, and every ledger domain must be a live prefix or a pinned legacy / ' +
440455
'non-dispatch branch. That two-way guard is what settles the FILE-SELECTION layer by ' +
441-
'construction — all 17 DomainRoute prefixes declared across the 16 domain files that declare one are ' +
442-
'ledger domains today, including the 10 files no probe names. 22 domains; 8 classified ' +
443-
'(/meta, /actions, /automation, /packages, /mcp, /analytics, /i18n, /approvals/act), 14 in the ' +
444-
'shrink-only baseline.',
456+
'construction — all 18 DomainRoute prefixes declared across the 17 domain files that declare one are ' +
457+
'ledger domains today, including the 11 files no probe names. 23 domains; 9 classified ' +
458+
'(/meta, /actions, /automation, /packages, /mcp, /analytics, /i18n, /approvals/act, ' +
459+
'/webhooks/redeliver), 14 in the shrink-only baseline.',
445460
},
446461
{
447462
file: 'packages/rest/src/rest-server.ts',

‎packages/qa/dogfood/test/authz-probe-blind-spot.test.ts‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,22 +43,24 @@ describe('authz probe blind-spot census (#13260)', () => {
4343
expect(derived.table).toEqual(PROBE_TABLE);
4444
});
4545

46-
it('every classified key is accounted for — 21 `covers` keys, no more', () => {
47-
// The matrix's `covers` keys number 21 today: the 9 probe-minted keys this
46+
it('every classified key is accounted for — 22 `covers` keys, no more', () => {
47+
// The matrix's `covers` keys number 22 today: the 9 probe-minted keys this
4848
// census was first measured against, plus the 6 ledger family/domain keys
4949
// classified when the population moved (2026-08-31), plus the `/analytics`
5050
// domain's gate-pin key and its dispatcher-domain key (#21061), plus the
5151
// `openapi` REST family key (#22430), plus the `/i18n` domain's gate-pin
5252
// key and its dispatcher-domain key (#22432), plus the `/approvals/act`
53-
// dispatcher-domain key, classified by `approval-action-token` (#22576). If a probe
53+
// dispatcher-domain key, classified by `approval-action-token` (#22576),
54+
// plus the `/webhooks/redeliver` dispatcher-domain key, classified by
55+
// `webhook-redeliver-session` (#22755). If a probe
5456
// starts minting a key no row covers, the ratchet itself goes red as
5557
// UNCLASSIFIED — that is its job, and this pin does not duplicate it. What
5658
// this asserts is only that the census's key count is current.
5759
//
58-
// ⚠️ This is NOT the size of the population. The ledgers mint 40 keys; 10
60+
// ⚠️ This is NOT the size of the population. The ledgers mint 41 keys; 11
5961
// are classified here and 30 are enumerated in the shrink-only baseline,
6062
// which `authz-conformance.test.ts` holds to its own four rules.
61-
expect(PROBE_TABLE.keys).toBe(21);
63+
expect(PROBE_TABLE.keys).toBe(22);
6264
});
6365

6466
it.each(PROBE_FILE_CENSUS.map((r) => [r.file, r] as const))(

‎packages/qa/dogfood/test/route-ledger-live-mount-parity.dogfood.test.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,13 @@ const UNEXERCISED_BY_THIS_BOOT: Record<string, string> = {
149149
'self-hosted, plugin-approvals mounts the ADR-0043 action page with rawApp.get() directly on Hono, so it never passes through the IHttpServer port; the dispatcher domain this row ledgers serves only a kernel with no raw app. Both doors are driven, on the real plugin, by packages/qa/http-conformance/src/hono-approvals-act.conformance.test.ts',
150150
'POST /api/v1/approvals/act':
151151
'self-hosted, plugin-approvals mounts the ADR-0043 redemption with rawApp.post() directly on Hono, so it never passes through the IHttpServer port; the dispatcher domain this row ledgers serves only a kernel with no raw app. Both doors are driven, on the real plugin, by packages/qa/http-conformance/src/hono-approvals-act.conformance.test.ts',
152+
// [#22755] Two doors, one path, again. This row ledgers the DISPATCHER's
153+
// `/webhooks/redeliver` domain, the door for a kernel with NO raw app. The
154+
// showcase `requires` the `webhooks` capability, so this self-hosted boot
155+
// composes plugin-webhooks, which serves the same path itself from
156+
// `rawApp.post()` on the Hono app — never through the port.
157+
'POST /api/v1/webhooks/redeliver':
158+
'self-hosted, plugin-webhooks mounts the redeliver door with rawApp.post() directly on Hono, so it never passes through the IHttpServer port; the dispatcher domain this row ledgers serves only a kernel with no raw app. Both doors are driven, on the real plugin, by packages/qa/http-conformance/src/hono-webhooks-redeliver.conformance.test.ts',
152159
};
153160

154161
/** Segments a probe path uses for `:params` — must match no literal segment. */

‎packages/qa/http-conformance/package.json‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@
2020
"@objectstack/objectql": "workspace:*",
2121
"@objectstack/plugin-approvals": "workspace:*",
2222
"@objectstack/plugin-hono-server": "workspace:*",
23+
"@objectstack/plugin-webhooks": "workspace:*",
2324
"@objectstack/runtime": "workspace:*",
25+
"@objectstack/service-messaging": "workspace:*",
2426
"@objectstack/spec": "workspace:*",
2527
"@types/node": "^26.6.3",
2628
"typescript": "^6.0.3",

0 commit comments

Comments
 (0)