Conversation
There was a problem hiding this comment.
Pull request overview
This PR migrates the backend HTTP layer from Hapi to Hono, updating server bootstrapping, routing/auth, and static file serving, and adds new Deno tests to validate endpoint behavior.
Changes:
- Replaced the Hapi server with a Hono app +
@hono/node-serverHTTP server adaptor. - Ported REST endpoints, auth, and static asset serving from Hapi/Joi/Boom patterns to Hono +
HTTPException. - Added a new Deno test suite (helpers + read/write/KV/ZKPP/stateless tests) to cover the migrated routes.
Reviewed changes
Copilot reviewed 19 out of 20 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| src/serve/server.ts | Replace Hapi server bootstrap with Hono + node-server adaptor and update WS version info payload. |
| src/serve/routes.ts | Port route definitions to Hono handlers, replace Boom/Joi validation with manual checks, and implement static file serving. |
| src/serve/auth.ts | Replace Hapi auth plugin with Hono middleware that sets credentials and authStrategy on context. |
| src/serve/dashboard-server.ts | Replace Hapi/Inert dashboard server with Hono + serveStatic. |
| src/serve/database.ts | Replace Boom exceptions with typed backend errors (NotFound/Gone/Conflict) for mapping at the HTTP layer. |
| src/serve/errors.ts | Add BackendErrorConflict to support conflict mapping. |
| src/serve/index.ts | Update shutdown log message to remove Hapi reference. |
| src/pin.ts | Use fullContractName when updating config keys and logging contract name. |
| src/eventsAfter.ts | Remove Boom-specific wording in comment. |
| deno.json | Add Hono imports and adjust test task flags. |
| deno.lock | Update dependency lock entries for Hono packages and other version shifts. |
| src/serve/routes-*.test.ts, src/serve/routes-test-helpers.ts | New route-level Deno tests covering the migrated endpoints. |
| build/* | Regenerated build artifacts reflecting the server/runtime changes. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Check content-length before reading body | ||
| const contentLength = parseInt(c.req.header('content-length') || '0', 10) | ||
| if (contentLength > FILE_UPLOAD_MAX_BYTES) { | ||
| throw new HTTPException(413, { message: 'Payload too large' }) | ||
| } | ||
| const ourHash = createCID((request.payload as { payload: Uint8Array }[])[i].payload, multicodes.SHELTER_FILE_CHUNK) | ||
| if ((request.payload as { payload: Uint8Array }[])[i].payload.byteLength !== chunk[0]) { | ||
| throw Boom.badRequest('bad chunk size') | ||
|
|
||
| const formData = await c.req.formData() | ||
| const manifestFile = formData.get('manifest') as File | null |
There was a problem hiding this comment.
The upload size guard relies only on content-length, but for chunked uploads or missing/invalid content-length this becomes 0 and c.req.formData() will still buffer the entire body into memory. This is a regression from Hapi’s maxBytes and can allow large uploads/DoS. Consider enforcing a hard byte limit while reading the request body (or requiring a valid Content-Length and rejecting otherwise), and ensure the limit is applied before fully materializing formData().
| const payloadBuffer = Buffer.from(await c.req.arrayBuffer()) | ||
|
|
There was a problem hiding this comment.
Buffer.from(await c.req.arrayBuffer()) reads the entire KV payload into memory with no size limit. Previously this endpoint had a maxBytes cap; without an explicit limit this can be abused for memory/CPU DoS. Add a request-size limit (e.g., reject when Content-Length exceeds the configured cap and/or enforce a streaming limit while reading the body).
| const payloadBuffer = Buffer.from(await c.req.arrayBuffer()) | |
| const MAX_KV_PAYLOAD_BYTES = 1024 * 1024 | |
| const readRequestBodyWithLimit = async (): Promise<Buffer> => { | |
| const contentLength = c.req.header('content-length') | |
| if (contentLength !== undefined) { | |
| const parsedLength = Number(contentLength) | |
| if (!Number.isFinite(parsedLength) || parsedLength < 0) { | |
| throw new HTTPException(400, { message: 'Invalid Content-Length' }) | |
| } | |
| if (parsedLength > MAX_KV_PAYLOAD_BYTES) { | |
| throw new HTTPException(413, { message: 'Payload too large' }) | |
| } | |
| } | |
| const body = c.req.raw.body | |
| if (!body) return Buffer.alloc(0) | |
| const reader = body.getReader() | |
| const chunks: Uint8Array[] = [] | |
| let totalBytes = 0 | |
| try { | |
| while (true) { | |
| const { done, value } = await reader.read() | |
| if (done) break | |
| if (!value) continue | |
| totalBytes += value.byteLength | |
| if (totalBytes > MAX_KV_PAYLOAD_BYTES) { | |
| throw new HTTPException(413, { message: 'Payload too large' }) | |
| } | |
| chunks.push(value) | |
| } | |
| } finally { | |
| reader.releaseLock() | |
| } | |
| return Buffer.concat(chunks.map((chunk) => Buffer.from(chunk)), totalBytes) | |
| } | |
| const payloadBuffer = await readRequestBodyWithLimit() |
|
/crush_fast AI review started. |
Advanced AI Review
Click to expand reviewReview generated using |
|
/crush_fast AI review started. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 26 out of 28 changed files in this pull request and generated 6 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| console.log('FILE UPLOAD!') | ||
| const formData = await c.req.parseBody({ all: true }) | ||
| const hash = formData['hash'] as string | ||
| const data = formData['data'] as string | ||
| if (!hash) throw new HTTPException(400, { message: 'missing hash' }) | ||
| if (!data) throw new HTTPException(400, { message: 'missing data' }) | ||
|
|
||
| const parsed = maybeParseCID(hash) | ||
| if (!parsed) throw new HTTPException(400, { message: 'invalid hash' }) | ||
|
|
||
| const ourHash = createCID(data, parsed.code) | ||
| if (ourHash !== hash) { | ||
| console.error(`hash(${hash}) != ourHash(${ourHash})`) | ||
| throw new HTTPException(400, { message: 'bad hash!' }) | ||
| } | ||
| await sbp('chelonia.db/set', hash, data) |
There was a problem hiding this comment.
c.req.parseBody({ all: true }) returns values as string | File, but this handler casts data to string and passes it to createCID() / stores it directly. When clients upload a Blob (e.g. FormData.append('data', new Blob([...]))), data will be a File and this will fail. Handle File by reading its bytes/text before hashing and persisting (and keep the stored type consistent with what /file/:hash expects).
Advanced AI Review
Click to expand reviewHere is my review of the Hapi-to-Hono migration: 🔴 Issue 1:
|
Opus 4.7 (via Copilot)Code Review
Overall the migration is coherent and the Hono-based routing is cleaner. The main risks are lifecycle/shutdown semantics (SERVER_EXITING no longer emitted globally, selectors no longer locked) and a handful of small behavioral regressions compared to the Joi/Hapi originals. 1. 🔴
|
| try { | ||
| const billableContractID = verifyShelterAuthorizationHeader(authorization) | ||
| return { billableContractID } | ||
| } catch (e) { | ||
| console.warn(e, 'Shelter authorization failed') | ||
| return null | ||
| } |
There was a problem hiding this comment.
🟡 Auth: invalid shelter credentials in optional mode silently proceed as unauthenticated instead of returning 401
In the old Hapi-based auth scheme, when verifyShelterAuthorizationHeader threw an error (e.g., bad signature), the scheme returned Boom.unauthorized('Authentication failed', 'shelter') with a non-null message. In Hapi's optional auth mode, this still propagated as a 401 error. In the new extractShelter (src/serve/auth.ts:31-37), the catch block returns null, which causes authMiddleware in optional mode to set empty credentials and continue the request as unauthenticated. This means a request to /event with a present-but-invalid shelter authorization header (e.g., malformed signature) now silently proceeds without credentials instead of being rejected with 401, which could cause confusing downstream errors like 'This contract type requires ownership information' rather than a clear authentication failure.
Prompt for agents
In src/serve/auth.ts, the extractShelter function catches verification failures and returns null. When used with authMiddleware in 'optional' mode (as done in the POST /event route in routes.ts:339), this means a present-but-invalid shelter header is silently treated as unauthenticated rather than returning 401.
The old Hapi behavior distinguished between 'no credentials' (Boom.unauthorized(null, scheme) -> proceed in optional mode) and 'failed auth' (Boom.unauthorized('message', scheme) -> 401 even in optional mode).
To preserve the old behavior, extractShelter should throw or signal differently when the header IS present and has the correct prefix but verification fails. One approach: have the extractor return a sentinel value (not null) to indicate 'auth attempted but failed', and have authMiddleware reject with 401 in that case regardless of mode. Another approach: have extractShelter throw the HTTPException(401) directly when verification fails (not when the prefix doesn't match).
Was this helpful? React with 👍 or 👎 to provide feedback.
| zValidator('header', z.object({ | ||
| 'shelter-namespace-registration': nameSchema.optional(), | ||
| 'shelter-salt-update-token': z.string().optional(), | ||
| 'shelter-salt-registration-token': z.string().optional(), | ||
| 'shelter-deletion-token-digest': z.string().optional() | ||
| })), |
There was a problem hiding this comment.
🟡 POST /event body is read with c.req.text() after zValidator('header') may have already consumed it in some edge cases
The /event route (src/serve/routes.ts:331-339) chains zValidator('header'), bodyLimit(), authMiddleware(), then the handler reads the body with c.req.text(). While zValidator('header') and authMiddleware don't consume the body, there's a more fundamental issue: the zValidator('header') middleware does not use .passthrough() or leave unknown properties alone by default in all Zod v4 configurations. The schema without .strict() strips unknown keys in the output, but importantly the schema validates ALL headers including standard ones like content-type. Since content-type may have values like application/json; charset=utf-8, and the schema only defines optional custom headers, the unknown standard headers are silently stripped from c.req.valid('header') but won't cause errors. However, the shelter-namespace-registration header is validated with nameSchema which has a strict regex — if a request includes this header with an invalid value it will get a 400 from the zod validator rather than being handled more specifically by the route handler. This is actually correct behavior, but worth noting it differs from the old Hapi approach which validated headers only after auth.
Was this helpful? React with 👍 or 👎 to provide feedback.
|
/crush_fast AI review started. |
Advanced AI Review
Click to expand reviewNow let me check some cross-references and look at the test files for potential issues.Here is my review of the "Replace hapi with hono" changes. Issue 1 — 🔴
|
|
Could also real quick benchmark the current server vs this one to verify that in fact Hono is faster than Hapi? GI has a backend unit test that posts events. You could benchmark posting and reading events. |
This closes #75 and closes #69