Skip to content

Commit 75b6d6a

Browse files
feat(trigger-api): the trigger-api service serves the inbound hook from a Request, and the self-hosted route is read from http.server first (trigger-api segment 3 of ruling A on #22757) (#22873)
Fixes #22774 Clause-②: yes Dispatched by the `domain:services` seat 2 PM (claim `6109841597`, branch `claude/issue-22774-inbound-hook-member`), as triage scoped the card in `6109754072`: trigger-api segment 3 of ruling A on #22757 (director record `6105447950`). Disclosure: the card is `security`, so this body names classes and positions only. ## What changes - **The member.** `ApiTriggerPlugin` registers the `trigger-api` service and implements the optional `ITriggerApiService.handleInboundHook(request, hook)` declared by #22772 (`packages/triggers/trigger-api/src/plugin.ts`). It takes a web-standard `Request` and returns a `Response`, with no Hono context, no raw app and no `http.server`. - **One door, two faces.** The self-hosted raw-app mount and the member both call one private body, `serveInboundHook`. It does one body read, reads one pair of header names, makes one call into `ApiTrigger.handleRequest` and answers one way. `handleRequest` holds the hook match, the per-hook secret read, the constant-time verifier (both signature forms, the 300 s window) and every status, and verification still runs before anything is enqueued. The mount hands the body its real Hono context, so it makes exactly the calls it made before (`c.req.text()`, `c.req.param` twice, `c.req.header` twice, then `c.json(body, status)`). The member hands it a `Request` adapter that answers the way Hono's `c.json` answers. The mount is not a caller of the member, and the path is mounted once. This follows the landed `webhooks` precedent (PR #22797). - **The slot is registered wherever the trigger is installed**, in the same `kernel:ready` step that registers the `api` trigger. Without an automation service there is no trigger, so there is no slot and no mount. The boot `warn` says so. - **`http.server` is read before the `http-server` alias.** Each name is read in its own `try`, and the first one present answers. A single server registered under both names is mounted once. With two servers under the two names, the mount goes on the `http.server` one. - **Comment text the change made false**, kept true: `api-trigger.ts` (the `handleRequest` docblock and the class docblock name both faces) and `trigger-api-route-ledger.ts` (its header now says the server is read as `http.server` first). The ledger row is unchanged. - **The route census learns the canonical key.** `HOST_APP_REACH` in `trigger-api-route-ledger.conformance.test.ts` now matches the canonical service key `http.server` as well as the alias. `plugin.ts` reads the canonical name now, so a census that knew only the alias would miss a second module reaching the same server that way. A case for each direction is added. This is an on-the-spot extension of a guard in this package, in the same class as the card. It is declared here and in the report. - `hono` is added as a **devDependency** (`package.json` + 3 lockfile lines). It is test-only, so the parity table reads the mount's real wire answer, as in PR #22797. - Changeset `.changeset/22774-inbound-hook-member.md`: `@objectstack/trigger-api` minor, `Clause-②: yes`. Zero `packages/spec` edits, no `ServiceSlotContracts` entry (#22848 remains open for it), no runtime dispatcher domain or allow-list row (#22773 remains open; whichever of the pair lands second runs the end-to-end pin), no replay-protection change. ## The PM's mechanism assumptions, measured 1. **Holds.** The member is declared on `origin/main` as `handleInboundHook?(request, hook)` in `packages/spec/src/contracts/trigger-api-service.ts`. Its docblock's status list is the member's spec: `401 INVALID_SIGNATURE` for every unverified post, `503 SERVICE_UNAVAILABLE` (secret unreadable, no queue), `400 INVALID_REQUEST`, `503 ENQUEUE_FAILED`, `202 { accepted, messageId }`. 2. **One sentence is stale.** One docblock bullet names only the body-only signature form ("`sha256=` and the hex HMAC-SHA256 of the raw body under that secret"). Since PR #22803, the raw mount also verifies the timestamped form within its 300 s window. The member answers what the mount answers, both forms included (pinned). The sentence is listed under Acceptance notes, and `packages/spec` is not edited here. 3. **Holds.** The mount delegates to the shared body with its wire answers unchanged. That is pinned by the existing 42 tests, which pass unmodified, and by the member/mount parity table below. 4. **Holds.** `plugin.ts` read only the `http-server` alias. All four registrations are pinned: canonical alone, alias alone, both on one server, and two servers. 5. **Already done by PR #22822.** `git grep -n "no oracle" d489318 -- packages/triggers/trigger-api/src` returns 0 hits, against control `HOOKS_PATH` in `plugin.ts` (3 hits). PR #22822 restated that comment, so nothing is changed for it here. ## Evidence (head `f44f31a18`) - `pnpm --filter @objectstack/trigger-api exec vitest run` passes `Test Files 3 passed (3)` and `Tests 54 passed (54)`. That is 42 existing tests plus 12 new ones in `inbound-hook-member.test.ts`. `pnpm --filter @objectstack/trigger-api run typecheck` is clean, and `tsc --listFiles` reaches all 7 `src/` files, the new test among them. - **What the new pins cover.** The tests boot a real `LiteKernel` with the real plugin, plus a real Hono app for the mount. - The hosted shape (no HTTP server): the member answers 17 arms, each with its `code` and `status`. They cover every unverified-post class, the 503 residual, the 400s, both signature forms, the empty body, and a hand-written body verified as its own bytes. Nothing is enqueued for a refusal. It also answers the two queue faults, passes `x-idempotency-key` through, and takes the flow and hook id from `hook`, never from the URL. - Parity: for every arm, the member and the mount on `http.server` give equal status, headers and body bytes. - Composition: four server shapes, plus a run with no automation service. - **Ablations.** Every negative pin was run with the forbidden behaviour put back. Each mutation went through `scripts/ablation-replace.mjs`, with the anchor hit and the write verified on disk. Each restore was proven by the blob hash equal to `HEAD`'s and an empty `git diff HEAD`. The subject resolves to `src` (relative imports, and `@objectstack/core` aliased to source), so no `dist/` rebuild applies. | Behaviour put back | Pins that went red | |---|---| | main's alias-only read | 4: both parity tests, canonical alone, two servers | | the alias read first | 1: two servers | | the path mounted twice | 4: every mounted-once pin | | the slot filled where no trigger is installed | 1: no automation | | the verifier skipped | 2: the hosted-shape arm table, parity | | path parameters re-parsed from the URL | 1: hook, not URL | | a member header the mount does not send | 3: the arm table, both parity tests | | a re-serialised body verified instead of the sender's bytes | 2: the arm table, parity | | the census forgetting the canonical key | 2: both census probes | The first double-mount attempt was a no-op. Its replacement contained its own anchor, so the anchor count rose from 1 to 2. The tool refused the write, restored the file and ran no test. It was re-run on a different anchor, with the result above. - **Gates.** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at this head derives 75 commands (no stale-tree warning). 74 were run, all exit 0. `check:lean-entry-closure` was re-run after building `@objectstack/objectql` and measured green. **NOT MEASURED: `check:dual-build-cjs-loads`**, reason: `PREREQUISITE NOT MET`. It reads every package's `dist/` (82 absent locally), so it would need a full `pnpm build`. It is declared to CI. Reconciled with `--ran` and exit codes recorded: `75 derived, 74 run, 1 NOT-MEASURED, 0 UNRUN`. - **Lint, a proven narrowing** (the repo-wide `pnpm lint` run is CI's). `pnpm exec eslint --no-inline-config --format json` over the 5 changed TypeScript files gives 0 errors and 0 warnings. ESLint's own JSON reports `files 5`. The population comes from `eslint.config.mjs`: its `packages/**/*.{ts,tsx,mts,cts}` objects cover all five, and `isPathIgnored` is false for each. Invariance: the config never enables type-aware linting (no `parserOptions.project`, no typed rules), so this diff cannot move any verdict on an untouched file. ## Acceptance notes - **Stale spec docblock sentence** (not edited, zero-`packages/spec` scope). In `packages/spec/src/contracts/trigger-api-service.ts`, the third bullet under "What it serves" names only the `sha256=` body-only form. Today's mount, and so the member, also verifies the timestamped form within its tolerance window. The docblock's own governing line ("exactly as the plugin's self-hosted mount serves it today") stays true. No open PR is known to touch this file. - **For the dispatcher-domain author (#22773).** If the forwarded `Request`'s body was already read, the member rejects with the platform's body-already-used `TypeError` rather than answering. That matches the contract, which says the caller answers that case as a server fault, so the caller should check before forwarding. --- _Generated by [Claude Code](https://claude.ai/code/session_013LbZ9MhPp1iriZEtgJqioA)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 51f36e7 commit 75b6d6a

8 files changed

Lines changed: 524 additions & 23 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@objectstack/trigger-api': minor
3+
---
4+
5+
feat(trigger-api): the `trigger-api` service serves the inbound hook from a web-standard `Request` (`ITriggerApiService.handleInboundHook`), and the self-hosted route is found under `http.server` too (#22774)
6+
7+
Clause-②: yes
8+
9+
- **What is new.** Wherever `ApiTriggerPlugin` installs its `api` trigger, it now registers the `trigger-api` service and implements its optional `ITriggerApiService.handleInboundHook(request, hook)` member. That member is the inbound hook, `POST /api/v1/automation/hooks/:flowName/:hookId`, taking a `Request` and returning a `Response`, with no raw app, no Hono context and no `http.server`. A hosted tenant kernel owns no socket, so this is how the door reaches it. Its one caller is the runtime HTTP dispatcher's exact `POST /automation/hooks/:flowName/:hookId` domain, which lands separately. The flow name and the hook id come from `hook`, the route the caller matched, and are never parsed again from the URL. The body is read once, from the request itself, and verified as the sender's own bytes.
10+
- **One door, two faces.** The member and the self-hosted route run the same code: the same hook match, the same per-hook secret, read at that moment, the same constant-time verification of `x-objectstack-signature` (the timestamped form and the deprecated body-only form alike), done before anything is enqueued, and the same answer for every outcome. For the same request they answer the same status, the same `Content-Type: application/json` and the same bytes: `401 INVALID_SIGNATURE` for every post that does not verify, `503 SERVICE_UNAVAILABLE` when the hook's secret cannot be read or no queue service is registered, `400 INVALID_REQUEST` for a body that is not a JSON object, `503 ENQUEUE_FAILED`, or `202` with `{ accepted: true, messageId }`.
11+
- **No trigger, no service.** Where the automation service is absent, no trigger is installed, so the `trigger-api` slot stays empty (as no route is mounted). The member's caller then answers its typed absence. The boot `warn` line says so.
12+
- **The route is found under `http.server` too.** The plugin reads `http.server` first, then the deprecated `http-server` alias. A host that registers its server only as `http.server` (the runtime's `config.server` path) now gets the self-hosted route. One that registers a single server under both names still gets it once. Where the two names hold two servers, the route goes on the `http.server` one.
13+
- **The self-hosted route is otherwise unchanged.** Where it was mounted before, it answers the same status, headers and bytes for the same request, and the path is never mounted twice.

‎packages/triggers/trigger-api/package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
},
2424
"devDependencies": {
2525
"@types/node": "^26.6.3",
26+
"hono": "^4.13.9",
2627
"typescript": "^6.0.3",
2728
"vitest": "^4.1.11"
2829
},

‎packages/triggers/trigger-api/src/api-trigger.ts‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,8 @@ export function verifySignature(secret: string, rawBody: string, header: string
161161
* hook (URL path + the required HMAC secret from the start node's `config`)
162162
* and subscribes a queue consumer that runs the flow. The HTTP side
163163
* ({@link handleRequest}) validates and **enqueues** — it never executes the
164-
* flow in-band:
164+
* flow in-band. It answers the same through the self-hosted mount and through
165+
* the `trigger-api` service's `handleInboundHook` (#22774):
165166
*
166167
* POST /api/v1/automation/hooks/:flowName/:hookId
167168
* → 401 INVALID_SIGNATURE — every post that does not verify, one answer:
@@ -271,7 +272,10 @@ export class ApiTrigger implements FlowTrigger {
271272

272273
/**
273274
* Handle one inbound request. Transport-agnostic: the plugin adapts this
274-
* to the host HTTP server. Returns the response to send.
275+
* to both faces of the door, the self-hosted raw-app mount and the
276+
* `trigger-api` service's `handleInboundHook` (#22774), through one
277+
* adapter, so both answer exactly what this returns. Returns the response
278+
* to send.
275279
*/
276280
async handleRequest(input: {
277281
flowName: string;

‎packages/triggers/trigger-api/src/inbound-hook-member.test.ts‎

Lines changed: 371 additions & 0 deletions
Large diffs are not rendered by default.

‎packages/triggers/trigger-api/src/plugin.ts‎

Lines changed: 115 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
22

33
import type { Plugin, PluginContext } from '@objectstack/core';
4+
import type { ITriggerApiService } from '@objectstack/spec/contracts';
45
import { ApiTrigger } from './api-trigger.js';
56
import type { FlowTrigger, QueueServiceSurface } from './api-trigger.js';
67

@@ -24,14 +25,95 @@ interface HttpServerSurface {
2425
/** Mount point for inbound hooks on the host HTTP server. */
2526
export const HOOKS_PATH = '/api/v1/automation/hooks/:flowName/:hookId';
2627

28+
/**
29+
* [#22774] The service slot this plugin fills: `ITriggerApiService`
30+
* (`@objectstack/spec/contracts`). The key is the package's own name, the
31+
* name ADR-0041 §2 gives the trigger family, as the contract's docblock
32+
* declares it.
33+
*/
34+
const TRIGGER_API_SLOT = 'trigger-api';
35+
36+
/**
37+
* [#22774] The slice of a Hono context the inbound-hook door reads and answers
38+
* through, and nothing more: the raw body as text, two request headers, and
39+
* `json(body, status)`.
40+
*
41+
* Both faces of the door hand {@link serveInboundHook} one of these. The
42+
* self-hosted mount hands it the real Hono context, so its answers are the ones
43+
* it always gave. The `trigger-api` service's `handleInboundHook` hands it
44+
* {@link requestDoorContext} over a web-standard `Request`. So both faces run
45+
* ONE copy of the door: one body read, one pair of header names, one
46+
* {@link ApiTrigger.handleRequest} (the hook match, the secret read, the
47+
* verifier and every status), and one way of answering.
48+
*/
49+
interface InboundHookDoorContext<R> {
50+
req: { text(): Promise<string>; header(name: string): string | undefined };
51+
json(body: unknown, status: number): R;
52+
}
53+
54+
/**
55+
* [#22774] A web-standard `Request` as the door's context, answering the way
56+
* Hono's `c.json` answers: the body is `JSON.stringify(body)` and the one
57+
* header is `Content-Type: application/json`.
58+
*
59+
* The body is read with `request.text()`, which is what Hono's `c.req.text()`
60+
* reads (the raw request's own `text()`), so the verifier sees the bytes the
61+
* sender signed, read once and never re-serialised. A header is read as Hono's
62+
* `c.req.header(name)` reads it: `headers.get(name)`, absent as `undefined`.
63+
*/
64+
function requestDoorContext(request: Request): InboundHookDoorContext<Response> {
65+
return {
66+
req: {
67+
text: () => request.text(),
68+
header: (name) => request.headers.get(name) ?? undefined,
69+
},
70+
json: (body, status) => new Response(JSON.stringify(body), {
71+
status,
72+
headers: { 'Content-Type': 'application/json' },
73+
}),
74+
};
75+
}
76+
77+
/**
78+
* [#22774] The inbound-hook door, `POST /api/v1/automation/hooks/:flowName/:hookId`,
79+
* ONE body behind two faces: the self-hosted raw-app mount, and the
80+
* `trigger-api` service's transport-neutral `handleInboundHook`. Neither face
81+
* carries a rule of its own, so they cannot drift: the same hook match, the
82+
* same per-hook secret, the same constant-time verifier before anything is
83+
* enqueued, and the same answer for every outcome.
84+
*
85+
* The path parameters arrive as `hook`, from the route each face matched, and
86+
* are never re-parsed from a URL. The mount is not a caller of the member, and
87+
* nothing here mounts anything.
88+
*/
89+
async function serveInboundHook<R>(
90+
trigger: ApiTrigger,
91+
c: InboundHookDoorContext<R>,
92+
hook: { readonly flowName: string; readonly hookId: string },
93+
): Promise<R> {
94+
const rawBody = await c.req.text();
95+
const out = await trigger.handleRequest({
96+
flowName: hook.flowName,
97+
hookId: hook.hookId,
98+
rawBody,
99+
signatureHeader: c.req.header('x-objectstack-signature'),
100+
idempotencyKey: c.req.header('x-idempotency-key') || undefined,
101+
});
102+
return c.json(out.body, out.status);
103+
}
104+
27105
/**
28106
* ApiTriggerPlugin (ADR-0041 Tier 1)
29107
*
30108
* Makes `type: 'api'` flows actually fire. The automation engine derives an
31109
* `api` binding from the flow; this plugin provides the concrete trigger:
32110
*
33111
* - mounts `POST /api/v1/automation/hooks/:flowName/:hookId` on the host
34-
* Hono app (resolved via the `http-server` service);
112+
* Hono app (resolved via the `http.server` service, then its deprecated
113+
* `http-server` alias);
114+
* - [#22774] registers the `trigger-api` service, whose
115+
* `handleInboundHook(request, hook)` serves the same door from a
116+
* web-standard `Request`, for a kernel with no raw app;
35117
* - validates hookId + HMAC signature (`x-objectstack-signature`,
36118
* GitHub/Stripe style, constant-time) against the flow's start-node
37119
* config;
@@ -61,7 +143,10 @@ export class ApiTriggerPlugin implements Plugin {
61143
ctx.hook('kernel:ready', async () => {
62144
const automation = this.resolveService<AutomationTriggerRegistry>(ctx, 'automation');
63145
if (!automation || typeof automation.registerTrigger !== 'function') {
64-
ctx.logger.warn('ApiTriggerPlugin: automation service not available — api trigger NOT installed');
146+
ctx.logger.warn(
147+
'ApiTriggerPlugin: automation service not available — api trigger NOT installed, '
148+
+ 'and the trigger-api service not registered',
149+
);
65150
return;
66151
}
67152
if (!this.resolveService<QueueServiceSurface>(ctx, 'queue')) {
@@ -74,23 +159,38 @@ export class ApiTriggerPlugin implements Plugin {
74159
);
75160
automation.registerTrigger(trigger);
76161

77-
const http = this.resolveService<HttpServerSurface>(ctx, 'http-server');
162+
// [#22774] The `trigger-api` service, registered wherever the
163+
// trigger is: where no trigger is installed the slot stays empty,
164+
// and the member's caller answers its typed absence (the
165+
// contract's rule), just as no route is mounted here then.
166+
const service: ITriggerApiService = {
167+
handleInboundHook: (request, hook) => serveInboundHook(trigger, requestDoorContext(request), hook),
168+
};
169+
ctx.registerService(TRIGGER_API_SLOT, service);
170+
171+
// [#22774] The server is read as `http.server` first, then the
172+
// deprecated `http-server` alias, as `plugin-webhooks` and
173+
// `plugin-approvals` read it. The canonical name is the only one
174+
// present on every provider path: `runtime.ts`'s `config.server`
175+
// path registers no alias, so an alias-only read never mounted the
176+
// door there. `resolveService` asks each name in its own `try`,
177+
// because `getService` throws on an empty slot. Where both names
178+
// are registered (`plugin-hono-server` registers one server under
179+
// both), the first answers, so the path is mounted once.
180+
const http = this.resolveService<HttpServerSurface>(ctx, 'http.server')
181+
?? this.resolveService<HttpServerSurface>(ctx, 'http-server');
78182
const rawApp = http && typeof http.getRawApp === 'function' ? http.getRawApp() : null;
79183
if (!rawApp) {
80-
ctx.logger.warn('ApiTriggerPlugin: HTTP server not available — hooks endpoint not mounted');
184+
ctx.logger.warn(
185+
'ApiTriggerPlugin: HTTP server not available — hooks endpoint not mounted '
186+
+ '(the trigger-api service still serves it to a caller that forwards the request)',
187+
);
81188
return;
82189
}
83-
rawApp.post(HOOKS_PATH, async (c: any) => {
84-
const rawBody = await c.req.text();
85-
const out = await trigger.handleRequest({
86-
flowName: c.req.param('flowName'),
87-
hookId: c.req.param('hookId'),
88-
rawBody,
89-
signatureHeader: c.req.header('x-objectstack-signature'),
90-
idempotencyKey: c.req.header('x-idempotency-key') || undefined,
91-
});
92-
return c.json(out.body, out.status);
93-
});
190+
rawApp.post(HOOKS_PATH, async (c: any) => serveInboundHook(trigger, c, {
191+
flowName: c.req.param('flowName'),
192+
hookId: c.req.param('hookId'),
193+
}));
94194
ctx.logger.info(`ApiTriggerPlugin: api trigger registered, hooks mounted at POST ${HOOKS_PATH}`);
95195
});
96196
}

‎packages/triggers/trigger-api/src/trigger-api-route-ledger.conformance.test.ts‎

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -242,8 +242,14 @@ function pathLiteralsIn(file: string): string[] {
242242
/** Absolute-path-shaped string literals, in any of the three quote styles. */
243243
const ABSOLUTE_PATH_LITERAL = /(['"`])(\/[A-Za-z0-9._~:@-][^'"`\s]*)\1/g;
244244

245-
/** The two spellings by which a module in this package reaches the HOST app. */
246-
const HOST_APP_REACH = /getRawApp|['"`]http-server['"`]/;
245+
/**
246+
* The spellings by which a module in this package reaches the HOST app: the
247+
* raw-app accessor, and the server's service key under its canonical name
248+
* `http.server` and its deprecated alias `http-server` (#22774: `plugin.ts`
249+
* reads the canonical name first, so a census that knew only the alias would
250+
* miss a second module reaching the same server the way the plugin now does).
251+
*/
252+
const HOST_APP_REACH = /getRawApp|['"`]http[.-]server['"`]/;
247253

248254
/**
249255
* Does `source` reach for the host app IN CODE?
@@ -381,6 +387,7 @@ describe('comment-stripper (the scan machinery, pinned in both directions)', ()
381387
expect(reachesHostApp("const s = ctx.getService('http-server');\n")).toBe(true);
382388
expect(reachesHostApp('const s = ctx.getService("http-server");\n')).toBe(true);
383389
expect(reachesHostApp('const s = ctx.getService(`http-server`);\n')).toBe(true);
390+
expect(reachesHostApp("const s = ctx.getService('http.server');\n")).toBe(true);
384391
expect(reachesHostApp('const app = server.getRawApp();\n')).toBe(true);
385392
});
386393

@@ -391,10 +398,11 @@ describe('comment-stripper (the scan machinery, pinned in both directions)', ()
391398
'plugin.ts': 'const app = http.getRawApp();\n',
392399
'prose-only.ts': '// getRawApp() is reached in plugin.ts, never here\n',
393400
'zzz-second-reacher.ts': "const s = ctx.getService('http-server');\n",
401+
'zzz-third-reacher.ts': "const s = ctx.getService('http.server');\n",
394402
};
395403
expect(
396404
filesReachingHostApp(Object.keys(fake).sort(), (f) => fake[f]),
397-
).toEqual(['plugin.ts', 'zzz-second-reacher.ts']);
405+
).toEqual(['plugin.ts', 'zzz-second-reacher.ts', 'zzz-third-reacher.ts']);
398406
});
399407

400408
it('drops paths that only appear in prose, and keeps every path in code', () => {

‎packages/triggers/trigger-api/src/trigger-api-route-ledger.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@
66
* (#11863, in the #3636 pattern).
77
*
88
* WHY THIS EXISTS. This plugin mounts its inbound-hooks endpoint on the HOST
9-
* Hono app: it resolves the `http-server` service at `kernel:ready`, takes the
10-
* framework-native handle through `getRawApp()`, and registers `POST` straight
11-
* on it. That mount is outside every ledger the platform had:
9+
* Hono app: it resolves the `http.server` service (then its deprecated
10+
* `http-server` alias, #22774) at `kernel:ready`, takes the framework-native
11+
* handle through `getRawApp()`, and registers `POST` straight on it. That
12+
* mount is outside every ledger the platform had:
1213
*
1314
* - the dispatcher ledger (`packages/runtime/src/route-ledger.ts`) sees
1415
* `RouteManager` branches; this is not one, and

‎pnpm-lock.yaml‎

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)