diff --git a/.changeset/dispatchit-express-adapter.md b/.changeset/dispatchit-express-adapter.md new file mode 100644 index 0000000..1bc6d2f --- /dev/null +++ b/.changeset/dispatchit-express-adapter.md @@ -0,0 +1,19 @@ +--- +'dispatchit': minor +--- + +Adds an Express adapter. Import `dispatchitExpress` from `dispatchit/adapters/express` and mount it on any Express 4 or 5 app. + +Because Express parses the request body before your middleware runs, the adapter needs the raw body to verify signatures. Capture it with a verify hook when you configure `express.json`, then mount the adapter after it: + +```ts +app.use( + '/api/webhooks/dispatchit', + express.json({ + verify: (req, res, buf) => { + ;(req as DispatchitRequest).rawBody = buf + }, + }), + dispatchitExpress(config) +) +``` diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..f745d09 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,82 @@ +name: Deploy hono-api + +on: + push: + branches: [main] + paths: + - 'packages/demos/hono-api/**' + - 'packages/dispatchit/**' + - '.github/workflows/deploy.yml' + pull_request: + types: [opened, synchronize, reopened] + paths: + - 'packages/demos/hono-api/**' + - 'packages/dispatchit/**' + - '.github/workflows/deploy.yml' + +permissions: + contents: read + pull-requests: write + +# One deploy at a time per PR/branch. +concurrency: + group: deploy-hono-api-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + deploy: + name: Deploy hono-api + runs-on: ubuntu-24.04-arm + # Manual approval gate for production, if wanted: create the "production" + # environment in repo settings and add required reviewers. + environment: ${{ github.event_name == 'push' && 'production' || '' }} + steps: + - uses: actions/checkout@v7 + + - uses: jdx/mise-action@v4 + + - run: pnpm install --frozen-lockfile + + # wrangler re-runs build.command from wrangler.jsonc before bundling; this + # step keeps the mandatory SDK build visible and fails fast if it breaks. + - name: Build dispatchit SDK + run: pnpm --filter dispatchit build + + - name: Skip notice for fork PRs + if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork + run: 'echo "::notice::Preview deploy skipped: fork PRs have no access to CLOUDFLARE secrets."' + + # `versions upload` never activates the production version, so previews on + # PRs cannot affect production traffic. Exposes the version preview URL. + - name: Upload preview version + id: preview + if: github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork + shell: bash + run: | + set -o pipefail + pnpm --filter hono-api exec wrangler versions upload \ + --message "preview: ${{ github.head_ref }} @ ${{ github.event.pull_request.head.sha }}" \ + | tee "$RUNNER_TEMP/wrangler-preview.log" + preview_url=$(grep -oE 'https://[a-zA-Z0-9.-]+\.workers\.dev[^[:space:]]*' "$RUNNER_TEMP/wrangler-preview.log" | head -n 1 || true) + echo "preview_url=$preview_url" >> "$GITHUB_OUTPUT" + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + + - name: Comment preview URL on the PR + if: github.event_name == 'pull_request' && steps.preview.outputs.preview_url != '' + env: + GH_TOKEN: ${{ github.token }} + PREVIEW_URL: ${{ steps.preview.outputs.preview_url }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: 'gh pr comment "$PR_NUMBER" --body "๐Ÿงช Preview deploy for `$HEAD_SHA`: $PREVIEW_URL"' + + # Production deploys only from main; uploads above never touch production + # traffic, this does. + - name: Deploy to production + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + run: pnpm --filter hono-api exec wrangler deploy + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} diff --git a/README.md b/README.md new file mode 100644 index 0000000..c45f5a6 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# Dispatchit SDK + +This repository contains the Dispatchit SDK source & demo apps. Currently the SDK only supports Node.js, other languages & frameworks might be added in the future. + +## Releasing + +Releases are managed with [Changesets](https://github.com/changesets/changesets) โ€” see +[`.changeset/README.md`](.changeset/README.md) in the repository root for the full flow: + +1. Add a changeset on your PR (`pnpm changeset`). +2. Merging to `main` opens a _Version Packages_ PR that bumps the version and writes `CHANGELOG.md`. +3. Merging that PR publishes to npm and tags the release. diff --git a/packages/demos/express-api/README.md b/packages/demos/express-api/README.md new file mode 100644 index 0000000..594ebf9 --- /dev/null +++ b/packages/demos/express-api/README.md @@ -0,0 +1,36 @@ +# express-api + +Demo webhook endpoint for Dispatchit (Express on Node.js) built on [`dispatchit`](../../dispatchit) and Express. It exposes mocked delivery/logistics capabilities the Dispatchit assistant can invoke during a conversation. It has no database, deliveries are seeded in memory per phone number. + +## Running + +```bash +pnpm dev # node --watch, from the repo root or the package +pnpm --filter express-api dev # or on its own +``` + +## Connecting Dispatchit + +1. Start this package locally (it listens on `http://127.0.0.1:8787`). +2. In the Dispatchit app (Settings โ†’ Webhooks), configure: + - URL: `http://127.0.0.1:8787/api/webhooks/dispatchit` + - Signing key: the same secret this package verifies with (`DEMO_WEBHOOK_SECRET`, defaults to `dev-webhook-secret`). +3. Press **Test** โ€” Dispatchit sends a signed ping and the webhook flips to ready. + +## Environment + +| Variable | Default | Purpose | +| --------------------- | -------------------- | --------------------------------------------------- | +| `DEMO_WEBHOOK_SECRET` | `dev-webhook-secret` | HMAC secret used to verify requests from Dispatchit | +| `PORT` | `8787` | Port the demo server listens on | + +## Capabilities + +| Capability | Purpose | Failure behavior | +| -------------------- | -------------------------------------------------- | --------------------------------------- | +| `listOrders` | List the customer's deliveries with status and ETA | Tells the assistant to try again later | +| `getDeliveryStatus` | Look up one delivery by order id | Tells the assistant the id wasn't found | +| `rescheduleDelivery` | Reschedule a confirmed delivery to a new date | Reroutes the conversation to a human | + +Seeded deliveries per phone number: `DLV-1001` (out for delivery, today), `DLV-1002` (confirmed, in 3 days), +`DLV-1003` (delivered, 2 days ago). Only `confirmed` deliveries can be rescheduled. diff --git a/packages/demos/express-api/package.json b/packages/demos/express-api/package.json new file mode 100644 index 0000000..59aa5f0 --- /dev/null +++ b/packages/demos/express-api/package.json @@ -0,0 +1,24 @@ +{ + "name": "express-api", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "node --watch --env-file-if-exists=.env src/index.ts", + "start": "node src/index.ts", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "dispatchit": "workspace:*", + "express": "^5.1.0", + "zod": "catalog:" + }, + "devDependencies": { + "@types/express": "catalog:", + "@types/node": "catalog:", + "typescript": "catalog:" + }, + "engines": { + "node": ">=22.9" + } +} diff --git a/packages/demos/express-api/src/config.ts b/packages/demos/express-api/src/config.ts new file mode 100644 index 0000000..c67a8c0 --- /dev/null +++ b/packages/demos/express-api/src/config.ts @@ -0,0 +1,114 @@ +import {loadEnvFile} from 'node:process' +try { + loadEnvFile() +} catch {} + +import {defineCapability, dispatchit} from 'dispatchit' +import * as z from 'zod' + +const deliveryStatuses = ['confirmed', 'preparing', 'out_for_delivery', 'delivered'] as const + +interface MockDelivery { + orderId: string + status: (typeof deliveryStatuses)[number] + /** ISO date (YYYY-MM-DD) the delivery is scheduled for. */ + deliveryDate: string +} + +/** In-memory mock store keyed by phone number โ€” the demo has no database. */ +const deliveriesByPhone = new Map>() + +export const config = dispatchit({ + webhookSecret: process.env.DEMO_WEBHOOK_SECRET ?? 'dev-webhook-secret', + // The demo authorizes every WhatsApp number. + onAuth: () => true, + capabilities: { + listOrders: defineCapability({ + description: "List the customer's deliveries with their current status.", + access: 'public', + input: z.object({}), + onSuccess: z.object({ + orders: z.array( + z.object({orderId: z.string().trim(), status: z.enum(deliveryStatuses), eta: z.string().trim()}) + ), + }), + onFail: {message: 'I could not load the deliveries right now. Ask the customer to try again shortly.'}, + handler: async ({context}) => ({ + orders: deliveriesFor(context.phoneNumber).map(delivery => ({ + orderId: delivery.orderId, + status: delivery.status, + eta: etaFor(delivery), + })), + }), + }), + + getDeliveryStatus: defineCapability({ + description: 'Look up the status and delivery window of one delivery by its order id.', + access: 'public', + input: z.object({ + orderId: z.string().trim().min(1), + }), + onSuccess: z.object({ + orderId: z.string().trim(), + status: z.enum(deliveryStatuses), + eta: z.string().trim(), + }), + onFail: { + message: 'I could not find a delivery with that order id. Ask the customer to double-check it.', + }, + handler: async ({input, context}) => { + const delivery = deliveriesFor(context.phoneNumber).find(order => order.orderId === input.orderId) + if (!delivery) throw new Error('Unknown order id') + + return {orderId: delivery.orderId, status: delivery.status, eta: etaFor(delivery)} + }, + }), + + rescheduleDelivery: defineCapability({ + description: + 'Reschedule a delivery to a new date (YYYY-MM-DD). Deliveries that are out for delivery or already delivered cannot be rescheduled.', + input: z.object({ + orderId: z.string().trim().min(1), + newDate: z.iso.date(), + }), + onSuccess: z.object({ + orderId: z.string().trim(), + status: z.enum(deliveryStatuses), + eta: z.string().trim(), + }), + onFail: { + action: 'reroute-to-human', + }, + handler: async ({input, context}) => { + const delivery = deliveriesFor(context.phoneNumber).find(order => order.orderId === input.orderId) + if (!delivery || delivery.status !== 'confirmed') throw new Error('Delivery cannot be rescheduled') + + delivery.deliveryDate = input.newDate + return {orderId: delivery.orderId, status: delivery.status, eta: etaFor(delivery)} + }, + }), + }, +}) + +function deliveriesFor(phoneNumber: string): Array { + let deliveries = deliveriesByPhone.get(phoneNumber) + if (!deliveries) { + deliveries = [ + {orderId: 'DLV-1001', status: 'out_for_delivery', deliveryDate: isoDateOffset(0)}, + {orderId: 'DLV-1002', status: 'confirmed', deliveryDate: isoDateOffset(3)}, + {orderId: 'DLV-1003', status: 'delivered', deliveryDate: isoDateOffset(-2)}, + ] + deliveriesByPhone.set(phoneNumber, deliveries) + } + return deliveries +} + +function etaFor(delivery: MockDelivery): string { + return delivery.status === 'delivered' ? 'already delivered' : `expected on ${delivery.deliveryDate}` +} + +function isoDateOffset(days: number): string { + const date = new Date() + date.setDate(date.getDate() + days) + return date.toISOString().slice(0, 10) +} diff --git a/packages/demos/express-api/src/index.ts b/packages/demos/express-api/src/index.ts new file mode 100644 index 0000000..7e8bf70 --- /dev/null +++ b/packages/demos/express-api/src/index.ts @@ -0,0 +1,35 @@ +import {loadEnvFile} from 'node:process' +try { + loadEnvFile() +} catch {} + +import {dispatchitExpress} from 'dispatchit/adapters/express' +import type {DispatchitRequest} from 'dispatchit/adapters/express' +import express from 'express' + +import {config} from './config.ts' + +const port = Number(process.env.PORT ?? 8787) + +const app = express() + +app.get('/', (_request, response) => { + response.json({status: 'ok'}) +}) + +app.post( + '/api/webhooks/dispatchit', + // Capture the raw body before parsing โ€” Dispatchit verifies HMAC signatures against it. + express.json({ + // body-parser's verify hook inherently receives (req, res, buf). + // oxlint-disable-next-line max-params + verify: (request, _response, buffer) => { + ;(request as DispatchitRequest).rawBody = buffer + }, + }), + dispatchitExpress(config) +) + +app.listen(port, () => { + console.log(`express-api listening on http://127.0.0.1:${port}`) +}) diff --git a/packages/demos/express-api/tsconfig.json b/packages/demos/express-api/tsconfig.json new file mode 100644 index 0000000..3df9f36 --- /dev/null +++ b/packages/demos/express-api/tsconfig.json @@ -0,0 +1,25 @@ +{ + "include": ["**/*.ts"], + "compilerOptions": { + "target": "esnext", + "module": "preserve", + "lib": ["ESNext"], + "types": ["node"], + + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "erasableSyntaxOnly": true, + "allowJs": true, + "verbatimModuleSyntax": true, + + "noEmit": true, + + /* Linting */ + "skipLibCheck": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + } +} diff --git a/packages/demos/hono-api/package.json b/packages/demos/hono-api/package.json index c27b1c9..ea976bc 100644 --- a/packages/demos/hono-api/package.json +++ b/packages/demos/hono-api/package.json @@ -4,6 +4,7 @@ "private": true, "type": "module", "scripts": { + "build": "pnpm --filter dispatchit build", "dev": "wrangler dev", "deploy": "wrangler deploy", "typecheck": "tsc --noEmit" diff --git a/packages/demos/hono-api/wrangler.jsonc b/packages/demos/hono-api/wrangler.jsonc index 18d8506..d38f2c6 100644 --- a/packages/demos/hono-api/wrangler.jsonc +++ b/packages/demos/hono-api/wrangler.jsonc @@ -5,7 +5,13 @@ "main": "src/index.ts", "compatibility_date": "2025-09-01", "compatibility_flags": ["nodejs_compat"], + // Exposes per-version preview URLs on `wrangler versions upload` (used for PR previews). + "preview_urls": true, "observability": { "enabled": true, }, + // Builds the dispatchit SDK (dist/) on fresh clones before wrangler bundles the worker. + "build": { + "command": "pnpm --filter dispatchit build", + }, } diff --git a/packages/dispatchit/README.md b/packages/dispatchit/README.md index 3055b87..efa9293 100644 --- a/packages/dispatchit/README.md +++ b/packages/dispatchit/README.md @@ -1,6 +1,6 @@ # dispatchit -Framework-agnostic SDK for building Dispatchit webhook endpoints. It lets you expose typed **capabilities** โ€” small, schema-validated operations โ€” that the Dispatchit assistant can invoke on your backend during a conversation, along with an auth hook to decide which users are allowed in. +Framework-agnostic SDK for integrating your backends with the Dispatchit agents. The SDK provides utilities to make it easier for you to expose _capabilities_ that the Dispatchit assistant can invoke on your backend during a conversation, along with an auth hook to decide which users are allowed in. The SDK provides a fully type-safe API for adding capabilities easily. ## Installation @@ -8,7 +8,7 @@ Framework-agnostic SDK for building Dispatchit webhook endpoints. It lets you ex pnpm add dispatchit ``` -The SDK has zero runtime dependencies. Schemas are provided through the [Standard Schema](https://standardschema.dev) interface, so any compliant validation library works โ€” Zod 4, Valibot and ArkType are supported out of the box. +The SDK has zero runtime dependencies. Schemas are provided through the [Standard Schema](https://standardschema.dev) interface, so any compliant validation library works (e.g. Zod, Valibot, etc.) ## Quick start @@ -21,7 +21,7 @@ import * as z from 'zod' import {applyToJob, searchApplicants} from './your-db' export const config = dispatchit({ - webhookSecret: process.env.ANSWR_WEBHOOK_SECRET!, + webhookSecret: process.env.DISPATCHIT_WEBHOOK_SECRET!, onAuth: async ({phoneNumber}) => { // Return whether this user is allowed to use the assistant. return userExists(phoneNumber) @@ -48,10 +48,35 @@ export const config = dispatchit({ }, }) +// hono const app = new Hono() app.use('/api/webhooks/dispatchit', dispatchitHono(config)) ``` +### Express + +The Express setup is simple, but our SDK will need the raw body instead of the already JSON parsed body from Express to correctly validate the signature. + +```ts +import {dispatchitExpress} from 'dispatchit/adapters/express' +import type {DispatchitRequest} from 'dispatchit/adapters/express' +import express from 'express' + +const app = express() + +app.post( + '/api/webhooks/dispatchit', + express.json({ + verify: (req, res, buf) => { + ;(req as DispatchitRequest).rawBody = buf + }, + }), + dispatchitExpress(config) +) +``` + +Works with Express 4 and 5. Requests that reach the adapter without a captured `rawBody` fail. + ## Concepts ### Capabilities @@ -81,6 +106,8 @@ Every failure path โ€” invalid input, a thrown handler, a result that doesn't ma ## Protocol +In case we don't support your framework, or your backend is built in a completely different language, the following details can help you implement an adapter. + All requests are `POST` with a JSON body, signed with the webhook secret: ``` @@ -111,15 +138,6 @@ pnpm typecheck # tsc --noEmit The package ships two entry points, both ESM: - `dispatchit` โ€” core -- `dispatchit/adapters/*` โ€” framework adapters (`hono` today) - -`hono` is an optional peer dependency โ€” only install it if you use the Hono adapter. - -## Releasing - -Releases are managed with [Changesets](https://github.com/changesets/changesets) โ€” see -[`.changeset/README.md`](../.changeset/README.md) in the repository root for the full flow: +- `dispatchit/adapters/*` โ€” framework adapters (`express` and `hono` today) -1. Add a changeset on your PR (`pnpm changeset`). -2. Merging to `main` opens a _Version Packages_ PR that bumps the version and writes `CHANGELOG.md`. -3. Merging that PR publishes to npm and tags the release. +`express` and `hono` are optional peer dependencies โ€” only install the one(s) matching the adapter you use. diff --git a/packages/dispatchit/package.json b/packages/dispatchit/package.json index f1dd9a9..aa62136 100644 --- a/packages/dispatchit/package.json +++ b/packages/dispatchit/package.json @@ -14,15 +14,25 @@ "type": "module", "exports": { ".": { - "import": "./dist/index.js", - "types": "./dist/index.d.ts" + "types": "./src/index.ts", + "import": "./dist/index.js" }, "./adapters/*": { - "import": "./dist/adapters/*.js", - "types": "./dist/adapters/*.d.ts" + "types": "./src/adapters/*.ts", + "import": "./dist/adapters/*.js" } }, "publishConfig": { + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./adapters/*": { + "types": "./dist/adapters/*.d.ts", + "import": "./dist/adapters/*.js" + } + }, "access": "public", "registry": "https://registry.npmjs.org/" }, @@ -37,6 +47,8 @@ }, "devDependencies": { "@changesets/cli": "3.0.3", + "@types/express": "catalog:", + "@types/express-serve-static-core": "^5.1.3", "@types/node": "catalog:", "hono": "4.13.7", "rolldown": "1.2.8", @@ -46,9 +58,13 @@ "zod": "catalog:" }, "peerDependencies": { + "express": ">=4.0.0", "hono": "^4.0.0" }, "peerDependenciesMeta": { + "express": { + "optional": true + }, "hono": { "optional": true } diff --git a/packages/dispatchit/rolldown.config.ts b/packages/dispatchit/rolldown.config.ts index df1d440..c4a6fd4 100644 --- a/packages/dispatchit/rolldown.config.ts +++ b/packages/dispatchit/rolldown.config.ts @@ -4,6 +4,7 @@ import {dts} from 'rolldown-plugin-dts' export default defineConfig({ input: { index: 'src/index.ts', + 'adapters/express': 'src/adapters/express.ts', 'adapters/hono': 'src/adapters/hono.ts', }, output: { diff --git a/packages/dispatchit/src/adapters/express.test.ts b/packages/dispatchit/src/adapters/express.test.ts new file mode 100644 index 0000000..d1f6e11 --- /dev/null +++ b/packages/dispatchit/src/adapters/express.test.ts @@ -0,0 +1,169 @@ +import {createHmac} from 'node:crypto' + +import {describe, expect, test, vi} from 'vitest' +import * as z from 'zod' + +import {defineCapability, dispatchit, DispatchitConfigError} from '../config' +import type {Dispatchit} from '../config' +import {dispatchitExpress} from './express' + +const webhookSecret = 'test-secret' + +const instance: Dispatchit = dispatchit({ + webhookSecret, + onAuth: async () => true, + capabilities: { + applyToJob: defineCapability({ + description: 'Apply to a job', + input: z.object({jobId: z.string().trim()}), + onSuccess: z.object({status: z.string().trim()}), + onFail: {message: 'Try again later.'}, + handler: async ({input}) => ({status: `applied-${input.jobId}`}), + }), + }, +}) + +interface FakeRequest { + method: string + headers: Record + rawBody?: string +} + +interface FakeResponse { + statusCode: number + headers: Record + body: string | undefined + finished: boolean +} + +function createFakeResponse(): FakeResponse & { + status(code: number): {send(body: string): void} + set(name: string, value: string): void + send(body: string): void +} { + const internal: FakeResponse = {statusCode: 200, headers: {}, body: undefined, finished: false} + return { + get statusCode() { + return internal.statusCode + }, + get headers() { + return internal.headers + }, + get body() { + return internal.body + }, + get finished() { + return internal.finished + }, + status(code) { + internal.statusCode = code + return { + send(body) { + internal.body = body + internal.finished = true + }, + } + }, + set(name, value) { + internal.headers[name] = value + }, + send(body) { + internal.body = body + internal.finished = true + }, + } +} + +/** Minimal req/res stand-ins matching how the adapter touches the Express objects. */ +function createHandlerArgs(request: FakeRequest) { + const response = createFakeResponse() + const next = vi.fn() + // Fake handler type mirrors the 3-parameter Express middleware signature. + // oxlint-disable-next-line max-params + const handler = dispatchitExpress(instance) as unknown as ( + req: FakeRequest, + res: ReturnType, + next: () => void + ) => Promise + return { + next, + run: () => handler(request, response, next), + response, + } +} + +function signedBody(payload: unknown, secret = webhookSecret): FakeRequest { + const rawBody = JSON.stringify(payload) + return { + method: 'POST', + headers: { + 'content-type': 'application/json', + 'x-dispatchit-signature': `sha256=${createHmac('sha256', secret).update(rawBody, 'utf8').digest('hex')}`, + }, + rawBody, + } +} + +describe('dispatchitExpress', () => { + test('responds to a signed ping', async () => { + const {run, response} = createHandlerArgs(signedBody({request: 'ping'})) + await run() + expect(response.statusCode).toBe(200) + expect(response.body).toBe('{}') + expect(response.headers['content-type']).toContain('application/json') + }) + + test('runs a capability and returns the result', async () => { + const {run, response} = createHandlerArgs( + signedBody({ + request: 'invoke', + name: 'applyToJob', + input: {jobId: 'job-1'}, + context: {phoneNumber: '+15550001'}, + }) + ) + await run() + expect(response.statusCode).toBe(200) + expect(JSON.parse(response.body!)).toEqual({success: true, result: {status: 'applied-job-1'}}) + }) + + test('returns the protocol status for non-POST requests', async () => { + const {run, response} = createHandlerArgs({method: 'GET', headers: {}, rawBody: ''}) + await run() + expect(response.statusCode).toBe(405) + }) + + test('rejects an unsigned request with the protocol status', async () => { + const {run, response} = createHandlerArgs({method: 'POST', headers: {}, rawBody: '{"request":"ping"}'}) + await run() + expect(response.statusCode).toBe(401) + }) + + test('fails with a helpful error when rawBody was not captured', async () => { + const {next, run} = createHandlerArgs({method: 'POST', headers: {}, rawBody: undefined}) + await run() + expect(next).toHaveBeenCalledTimes(1) + const error = next.mock.calls[0]![0] as DispatchitConfigError + expect(error).toBeInstanceOf(DispatchitConfigError) + expect(error.message).toContain('express.json({verify') + }) + + test('forwards protocol errors to next', async () => { + const failing: Dispatchit = { + handleRequest: async () => { + throw new Error('boom') + }, + } + const next = vi.fn() + // Fake handler type mirrors the 3-parameter Express middleware signature. + // oxlint-disable-next-line max-params + const handler = dispatchitExpress(failing) as unknown as ( + req: FakeRequest, + res: ReturnType, + next: () => void + ) => Promise + await handler({method: 'POST', headers: {}, rawBody: '{}'}, createFakeResponse(), next) + expect(next).toHaveBeenCalledTimes(1) + expect((next.mock.calls[0]![0] as Error).message).toBe('boom') + }) +}) diff --git a/packages/dispatchit/src/adapters/express.ts b/packages/dispatchit/src/adapters/express.ts new file mode 100644 index 0000000..d3402a9 --- /dev/null +++ b/packages/dispatchit/src/adapters/express.ts @@ -0,0 +1,53 @@ +import type {IncomingHttpHeaders} from 'node:http' + +import type {NextFunction, Request, RequestHandler, Response} from 'express' + +import type {Dispatchit} from '../config' +import {DispatchitConfigError} from '../config' + +declare module 'express-serve-static-core' { + interface Request { + /** Raw request body captured before body parsing โ€” required for HMAC signature verification. */ + rawBody?: string | Buffer + } +} + +/** Express request with the raw body captured by a parser-level verify hook (see the module augmentation). */ +export type DispatchitRequest = Request + +const missingRawBodyMessage = + 'Missing req.rawBody. Dispatchit verifies signatures against the raw request body, which body parsers consume. ' + + 'Capture it with a parser-level verify hook: express.json({verify: (req, res, buf) => {(req as DispatchitRequest).rawBody = buf}}).' + +export function dispatchitExpress(instance: Dispatchit): RequestHandler { + // Express middleware inherently receives (req, res, next). + // oxlint-disable-next-line max-params + return async (request, response: Response, next: NextFunction) => { + try { + const rawBody = (request as DispatchitRequest).rawBody + if (rawBody === undefined) throw new DispatchitConfigError(missingRawBodyMessage) + + const result = await instance.handleRequest({ + method: request.method, + headers: toHeaders(request.headers), + rawBody: typeof rawBody === 'string' ? rawBody : rawBody.toString('utf8'), + }) + + response.status(result.status) + result.headers.forEach((value, name) => response.set(name, value)) + response.send(await result.text()) + } catch (error) { + next(error) + } + } +} + +/** Converts Node's incoming headers (multi-valued, possibly undefined) into a fetch `Headers`. */ +function toHeaders(incoming: IncomingHttpHeaders): Headers { + const headers = new Headers() + for (const [name, value] of Object.entries(incoming)) { + if (value === undefined) continue + for (const item of Array.isArray(value) ? value : [value]) headers.append(name, item) + } + return headers +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fbf9c1b..ece284a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -164,6 +164,9 @@ settings: catalogs: default: + '@types/express': + specifier: 5.0.5 + version: 5.0.5 '@types/node': specifier: 26.5.1 version: 26.5.1 @@ -203,6 +206,28 @@ importers: specifier: ^7.0.2 version: 7.0.2 + packages/demos/express-api: + dependencies: + dispatchit: + specifier: workspace:* + version: link:../../dispatchit + express: + specifier: ^5.1.0 + version: 5.3.0(supports-color@10.2.2) + zod: + specifier: 'catalog:' + version: 4.6.5 + devDependencies: + '@types/express': + specifier: 'catalog:' + version: 5.0.5 + '@types/node': + specifier: 'catalog:' + version: 26.5.1 + typescript: + specifier: 'catalog:' + version: 7.0.2 + packages/demos/hono-api: dependencies: dispatchit: @@ -230,10 +255,19 @@ importers: '@standard-schema/spec': specifier: 1.1.0 version: 1.1.0 + express: + specifier: '>=4.0.0' + version: 5.3.0(supports-color@10.2.2) devDependencies: '@changesets/cli': specifier: 3.0.3 version: 3.0.3 + '@types/express': + specifier: 'catalog:' + version: 5.0.5 + '@types/express-serve-static-core': + specifier: ^5.1.3 + version: 5.1.3 '@types/node': specifier: 'catalog:' version: 26.5.1 @@ -1350,9 +1384,15 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@types/body-parser@1.19.6': + resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} @@ -1362,12 +1402,39 @@ packages: '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@types/express-serve-static-core@5.1.3': + resolution: {integrity: sha512-dPfW8NFiOF4wOHc7+N/QSxlY9cfSsenewGbAz8C8U/MULPd/YZ27LvJUIlzaXie7e6Ove9YunJGgC9tbHD2cKw==} + + '@types/express@5.0.5': + resolution: {integrity: sha512-LuIQOcb6UmnF7C1PCFmEU1u2hmiHL43fgFQX67sN3H4Z+0Yk0Neo++mFsBjhOAuLzvlQeqAAkeDOZrJs9rzumQ==} + + '@types/http-errors@2.0.5': + resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/mime@1.3.5': + resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + '@types/node@26.5.1': resolution: {integrity: sha512-CzNm2FezW4VR/LjG6yUdiEgLE/rAQ9Slj5gCu/C2VrdcW7I0ahNZ8DRbHT7zOZ6r3ONgd/bsQIeSaoDGrd1C6g==} + '@types/qs@6.15.1': + resolution: {integrity: sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==} + + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + + '@types/send@0.17.6': + resolution: {integrity: sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==} + + '@types/send@1.2.1': + resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} + + '@types/serve-static@1.15.10': + resolution: {integrity: sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==} + '@typescript-eslint/project-service@8.70.1': resolution: {integrity: sha512-62xOgboPfwc3/IgPSX/W6oQR3ZbF04194FPGUGH8HL8iLFHbt/456/8Ph1wLNUgVF+s94FlHoipBsz+v7+LMnA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1674,6 +1741,10 @@ packages: '@yuku-toolchain/types@0.9.5': resolution: {integrity: sha512-KiuLNNgX9uNealaWAR+G3/cMXnRk9x4TY2EkYe/KIag+UPdwiA0RRf1hr1WAxzTP8KGzCTkqUdLPvqh32sEO3w==} + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -1698,10 +1769,18 @@ packages: blake3-wasm@2.1.5: resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} + body-parser@2.3.0: + resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==} + engines: {node: '>=18'} + brace-expansion@5.0.12: resolution: {integrity: sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==} engines: {node: 20 || >=22} + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + cac@7.0.0: resolution: {integrity: sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==} engines: {node: '>=20.19.0'} @@ -1709,10 +1788,34 @@ packages: cacheable@2.5.0: resolution: {integrity: sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==} + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + chai@6.2.2: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} + content-disposition@2.0.1: + resolution: {integrity: sha512-e+H0ZXHSWYrENhQzw1LPuP4oF5MzVKmDU6d3hxlvaPEYLLg62MxtQNPRx4SYSuYJSBUgnQIG4HIN2tEtNv7Dog==} + engines: {node: '>=18'} + + content-type@2.1.0: + resolution: {integrity: sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==} + engines: {node: '>=18'} + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + cookie@1.1.1: resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} engines: {node: '>=18'} @@ -1733,6 +1836,10 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -1746,12 +1853,35 @@ packages: oxc-resolver: optional: true + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + error-stack-parser-es@1.0.5: resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + es-module-lexer@2.3.2: resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} + esbuild@0.28.1: resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} engines: {node: '>=18'} @@ -1762,6 +1892,9 @@ packages: engines: {node: '>=18'} hasBin: true + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} @@ -1826,10 +1959,18 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + expect-type@1.4.0: resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} engines: {node: '>=12.0.0'} + express@5.3.0: + resolution: {integrity: sha512-qCqy1xSoaugxOD4IVHb1ZWdkvCF2W3aE2okKouWj8VwJ+ztskSiXoBnCSJqfKT+cVPJhuJqe9YLbY425+Zh00g==} + engines: {node: '>= 18'} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1860,6 +2001,10 @@ packages: file-entry-cache@11.1.5: resolution: {integrity: sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==} + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -1870,11 +2015,30 @@ packages: flatted@3.4.4: resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==} + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + get-tsconfig@5.0.0-beta.6: resolution: {integrity: sha512-X6fBC0pmImC70gvX2zm56go9hx0MyoGVdG0tUCkg/D+Xnh5TJsOZ7iDbOdI3PvmtrDxnu1YdDufpK2QJX1Meqw==} engines: {node: '>=20.20.0'} @@ -1883,10 +2047,22 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + hashery@1.5.1: resolution: {integrity: sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==} engines: {node: '>=20'} + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + hono@4.13.7: resolution: {integrity: sha512-c8/gF9ac8Y78/agExVocyLevgR+JlpNB444Py0FSX8pJoPdYUfUzRcXtYEYGwt6l19qIlVZPN5Mfsw9jFShmQQ==} engines: {node: '>=16.9.0'} @@ -1897,10 +2073,18 @@ packages: hookified@2.2.0: resolution: {integrity: sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==} + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + human-id@4.2.1: resolution: {integrity: sha512-zPGsiS+dWoTZtZ4AtpA9Y+BdSFSNWvnouNlWNoUFyAM6xHOHmdCvqO3k8AIbdamCOv4gUFUVNPf6rJFfc4UiJw==} hasBin: true + iconv-lite@0.7.3: + resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==} + engines: {node: '>=0.10.0'} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -1912,6 +2096,13 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -1920,6 +2111,9 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -2030,6 +2224,26 @@ packages: magic-string@1.4.1: resolution: {integrity: sha512-8lyCu36ErXR0J9uaGKlKQoiLZKmtI63YGLE8G2o9jyRPdr4X47LusSOwgOJOzcVtp81fTAAjxR7BwKz682Jhow==} + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + media-typer@1.1.1: + resolution: {integrity: sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==} + engines: {node: '>= 0.8'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + miniflare@5.20260921.0-alpha: resolution: {integrity: sha512-vHH/unOYvV2jA1Q9SdkmzrQhhMoksdwg5jegu6ZeKaaRzgxZhVbt1NdTpQjHF2VTgiBjgP8SiUlUMfruB3N3SQ==} engines: {node: '>=22.0.0'} @@ -2049,10 +2263,25 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + negotiator@1.1.0: + resolution: {integrity: sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==} + engines: {node: '>=18'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + obug@2.2.1: resolution: {integrity: sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==} engines: {node: '>=12.20.0'} + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -2098,6 +2327,10 @@ packages: package-manager-detector@1.8.0: resolution: {integrity: sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==} + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -2109,6 +2342,9 @@ packages: path-to-regexp@6.3.0: resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + path-to-regexp@8.4.2: + resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -2127,6 +2363,10 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + proxy-addr@2.0.8: + resolution: {integrity: sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==} + engines: {node: '>= 0.10'} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -2135,6 +2375,18 @@ packages: resolution: {integrity: sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==} engines: {node: '>=20'} + qs@6.16.0: + resolution: {integrity: sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==} + engines: {node: '>=0.6'} + + range-parser@1.3.0: + resolution: {integrity: sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==} + engines: {node: '>= 0.6'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} @@ -2162,11 +2414,29 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + semver@7.8.5: resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} engines: {node: '>=10'} hasBin: true + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + sharp@0.35.4: resolution: {integrity: sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==} engines: {node: '>=20.9.0'} @@ -2188,6 +2458,22 @@ packages: resolution: {integrity: sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==} engines: {node: '>= 0.4'} + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.1: + resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} + engines: {node: '>= 0.4'} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -2201,6 +2487,10 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + std-env@4.2.0: resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} @@ -2228,6 +2518,10 @@ packages: resolution: {integrity: sha512-9YodfrxS9g9IbFr/KOjE5bAeJ0p61n3bW6mqvy0jtoeKd1kTW1Cxm0oulm6KX2lyM9Gl6WIe8nEbY7LWv5ZJww==} engines: {node: ^20.0.0 || >=22.0.0} + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + ts-api-utils@2.5.0: resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} engines: {node: '>=18.12'} @@ -2246,6 +2540,10 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} + type-is@2.1.0: + resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} + engines: {node: '>= 18'} + typescript@7.0.2: resolution: {integrity: sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==} engines: {node: '>=16.20.0'} @@ -2261,9 +2559,17 @@ packages: unenv@2.0.0-rc.24: resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + vite@8.3.0: resolution: {integrity: sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2377,6 +2683,9 @@ packages: '@cloudflare/workers-types': optional: true + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + ws@8.21.0: resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} engines: {node: '>=10.0.0'} @@ -3135,23 +3444,68 @@ snapshots: '@standard-schema/spec@1.1.0': {} + '@types/body-parser@1.19.6': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 26.5.1 + '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 assertion-error: 2.0.1 + '@types/connect@3.4.38': + dependencies: + '@types/node': 26.5.1 + '@types/deep-eql@4.0.2': {} '@types/esrecurse@4.3.1': {} '@types/estree@1.0.9': {} + '@types/express-serve-static-core@5.1.3': + dependencies: + '@types/node': 26.5.1 + '@types/qs': 6.15.1 + '@types/range-parser': 1.2.7 + '@types/send': 1.2.1 + + '@types/express@5.0.5': + dependencies: + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 5.1.3 + '@types/serve-static': 1.15.10 + + '@types/http-errors@2.0.5': {} + '@types/json-schema@7.0.15': {} + '@types/mime@1.3.5': {} + '@types/node@26.5.1': dependencies: undici-types: 8.9.0 + '@types/qs@6.15.1': {} + + '@types/range-parser@1.2.7': {} + + '@types/send@0.17.6': + dependencies: + '@types/mime': 1.3.5 + '@types/node': 26.5.1 + + '@types/send@1.2.1': + dependencies: + '@types/node': 26.5.1 + + '@types/serve-static@1.15.10': + dependencies: + '@types/http-errors': 2.0.5 + '@types/node': 26.5.1 + '@types/send': 0.17.6 + '@typescript-eslint/project-service@8.70.1(supports-color@10.2.2)(typescript@7.0.2)': dependencies: '@typescript-eslint/tsconfig-utils': 8.70.1(typescript@7.0.2) @@ -3348,6 +3702,11 @@ snapshots: '@yuku-toolchain/types@0.9.5': {} + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.1.0 + acorn-jsx@5.3.2(acorn@8.18.0): dependencies: acorn: 8.18.0 @@ -3367,10 +3726,26 @@ snapshots: blake3-wasm@2.1.5: {} + body-parser@2.3.0(supports-color@10.2.2): + dependencies: + bytes: 3.1.2 + content-type: 2.1.0 + debug: 4.4.3(supports-color@10.2.2) + http-errors: 2.0.1 + iconv-lite: 0.7.3 + on-finished: 2.4.1 + qs: 6.16.0 + raw-body: 3.0.2 + type-is: 2.1.0 + transitivePeerDependencies: + - supports-color + brace-expansion@5.0.12: dependencies: balanced-match: 4.0.4 + bytes@3.1.2: {} + cac@7.0.0: {} cacheable@2.5.0: @@ -3381,8 +3756,26 @@ snapshots: keyv: 5.6.0 qified: 0.10.1 + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + chai@6.2.2: {} + content-disposition@2.0.1: {} + + content-type@2.1.0: {} + + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + cookie@1.1.1: {} cross-spawn@7.0.6: @@ -3399,14 +3792,34 @@ snapshots: deep-is@0.1.4: {} + depd@2.0.0: {} + detect-libc@2.1.2: {} dts-resolver@3.0.0: {} + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ee-first@1.1.1: {} + + encodeurl@2.0.0: {} + error-stack-parser-es@1.0.5: {} + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + es-module-lexer@2.3.2: {} + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + esbuild@0.28.1: optionalDependencies: '@esbuild/aix-ppc64': 0.28.1 @@ -3466,6 +3879,8 @@ snapshots: '@esbuild/win32-x64': 0.28.2 optional: true + escape-html@1.0.3: {} + escape-string-regexp@4.0.0: {} eslint-plugin-zod@4.14.2(eslint@10.11.0(supports-color@10.2.2))(oxlint@1.85.0(oxlint-tsgolint@7.0.2002))(supports-color@10.2.2)(typescript@7.0.2)(zod@4.6.5): @@ -3548,8 +3963,43 @@ snapshots: esutils@2.0.3: {} + etag@1.8.1: {} + expect-type@1.4.0: {} + express@5.3.0(supports-color@10.2.2): + dependencies: + accepts: 2.0.0 + body-parser: 2.3.0(supports-color@10.2.2) + content-disposition: 2.0.1 + content-type: 2.1.0 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3(supports-color@10.2.2) + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1(supports-color@10.2.2) + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.8 + qs: 6.16.0 + range-parser: 1.3.0 + router: 2.2.0(supports-color@10.2.2) + send: 1.2.1(supports-color@10.2.2) + serve-static: 2.2.1(supports-color@10.2.2) + statuses: 2.0.2 + type-is: 2.1.0 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + fast-deep-equal@3.1.3: {} fast-json-stable-stringify@2.1.0: {} @@ -3574,6 +4024,17 @@ snapshots: dependencies: flat-cache: 6.1.23 + finalhandler@2.1.1(supports-color@10.2.2): + dependencies: + debug: 4.4.3(supports-color@10.2.2) + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + find-up@5.0.0: dependencies: locate-path: 6.0.0 @@ -3587,9 +4048,33 @@ snapshots: flatted@3.4.4: {} + forwarded@0.2.0: {} + + fresh@2.0.0: {} + fsevents@2.3.3: optional: true + function-bind@1.1.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.2 + get-tsconfig@5.0.0-beta.6: dependencies: resolve-pkg-maps: 1.0.0 @@ -3598,30 +4083,56 @@ snapshots: dependencies: is-glob: 4.0.3 + gopd@1.2.0: {} + + has-symbols@1.1.0: {} + hashery@1.5.1: dependencies: hookified: 1.15.1 + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + hono@4.13.7: {} hookified@1.15.1: {} hookified@2.2.0: {} + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + human-id@4.2.1: {} + iconv-lite@0.7.3: + dependencies: + safer-buffer: 2.1.2 + ignore@5.3.2: {} import-meta-resolve@4.2.0: {} imurmurhash@0.1.4: {} + inherits@2.0.4: {} + + ipaddr.js@1.9.1: {} + is-extglob@2.1.1: {} is-glob@4.0.3: dependencies: is-extglob: 2.1.1 + is-promise@4.0.0: {} + isexe@2.0.0: {} jju@1.4.0: {} @@ -3705,6 +4216,18 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.6.0 + math-intrinsics@1.1.0: {} + + media-typer@1.1.1: {} + + merge-descriptors@2.0.0: {} + + mime-db@1.54.0: {} + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + miniflare@5.20260921.0-alpha(@types/node@26.5.1): dependencies: '@cspotcode/source-map-support': 0.8.1 @@ -3728,8 +4251,22 @@ snapshots: natural-compare@1.4.0: {} + negotiator@1.1.0: + dependencies: + content-type: 2.1.0 + + object-inspect@1.13.4: {} + obug@2.2.1: {} + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -3805,12 +4342,16 @@ snapshots: package-manager-detector@1.8.0: {} + parseurl@1.3.3: {} + path-exists@4.0.0: {} path-key@3.1.1: {} path-to-regexp@6.3.0: {} + path-to-regexp@8.4.2: {} + pathe@2.0.3: {} picocolors@1.1.1: {} @@ -3825,12 +4366,31 @@ snapshots: prelude-ls@1.2.1: {} + proxy-addr@2.0.8: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + punycode@2.3.1: {} qified@0.10.1: dependencies: hookified: 2.2.0 + qs@6.16.0: + dependencies: + es-define-property: 1.0.1 + side-channel: 1.1.1 + + range-parser@1.3.0: {} + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.3 + unpipe: 1.0.0 + resolve-pkg-maps@1.0.0: {} rolldown-plugin-dts@0.28.5(rolldown@1.2.8)(typescript@7.0.2): @@ -3868,8 +4428,47 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.2.8 '@rolldown/binding-win32-x64-msvc': 1.2.8 + router@2.2.0(supports-color@10.2.2): + dependencies: + debug: 4.4.3(supports-color@10.2.2) + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.4.2 + transitivePeerDependencies: + - supports-color + + safer-buffer@2.1.2: {} + semver@7.8.5: {} + send@1.2.1(supports-color@10.2.2): + dependencies: + debug: 4.4.3(supports-color@10.2.2) + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.3.0 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.1(supports-color@10.2.2): + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1(supports-color@10.2.2) + transitivePeerDependencies: + - supports-color + + setprototypeof@1.2.0: {} + sharp@0.35.4(@types/node@26.5.1): dependencies: '@img/colour': 1.1.0 @@ -3911,6 +4510,34 @@ snapshots: shell-quote@1.10.0: {} + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} sisteransi@1.0.5: {} @@ -3919,6 +4546,8 @@ snapshots: stackback@0.0.2: {} + statuses@2.0.2: {} + std-env@4.2.0: {} supports-color@10.2.2: {} @@ -3936,6 +4565,8 @@ snapshots: tinypool@2.1.2: {} + toidentifier@1.0.1: {} + ts-api-utils@2.5.0(typescript@7.0.2): dependencies: typescript: 7.0.2 @@ -3954,6 +4585,12 @@ snapshots: dependencies: prelude-ls: 1.2.1 + type-is@2.1.0: + dependencies: + content-type: 2.1.0 + media-typer: 1.1.1 + mime-types: 3.0.2 + typescript@7.0.2: optionalDependencies: '@typescript/typescript-aix-ppc64': 7.0.2 @@ -3985,10 +4622,14 @@ snapshots: dependencies: pathe: 2.0.3 + unpipe@1.0.0: {} + uri-js@4.4.1: dependencies: punycode: 2.3.1 + vary@1.1.2: {} + vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(tsx@4.23.13)(yaml@2.9.1): dependencies: lightningcss: 1.33.0 @@ -4060,6 +4701,8 @@ snapshots: - bufferutil - utf-8-validate + wrappy@1.0.2: {} + ws@8.21.0: {} yaml@2.9.1: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 69f4c40..4b11f83 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -3,6 +3,7 @@ packages: - packages/demos/* catalog: + '@types/express': 5.0.5 '@types/node': 26.5.1 typescript: 7.0.2 vitest: 5.0.1