@usehyper/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +31 -0
  3. package/package.json +40 -0
  4. package/registry-sources/agent-rules/README.md +12 -0
  5. package/registry-sources/agent-rules/files/.cursor/rules/hyper.md +178 -0
  6. package/registry-sources/agent-rules/files/AGENTS.md +64 -0
  7. package/registry-sources/agent-rules/manifest.json +15 -0
  8. package/src/__tests__/add.test.ts +125 -0
  9. package/src/__tests__/cli.test.ts +50 -0
  10. package/src/__tests__/security.test.ts +101 -0
  11. package/src/args.ts +38 -0
  12. package/src/bin.ts +77 -0
  13. package/src/commands/add.ts +232 -0
  14. package/src/commands/bench.ts +185 -0
  15. package/src/commands/build.ts +146 -0
  16. package/src/commands/client.ts +78 -0
  17. package/src/commands/dev.ts +53 -0
  18. package/src/commands/diff.ts +80 -0
  19. package/src/commands/env.ts +92 -0
  20. package/src/commands/help.ts +42 -0
  21. package/src/commands/init.ts +119 -0
  22. package/src/commands/list.ts +46 -0
  23. package/src/commands/mcp.ts +51 -0
  24. package/src/commands/openapi.ts +50 -0
  25. package/src/commands/routes.ts +45 -0
  26. package/src/commands/security.ts +233 -0
  27. package/src/commands/test.ts +191 -0
  28. package/src/commands/typecheck.ts +19 -0
  29. package/src/commands/update.ts +91 -0
  30. package/src/commands/version.ts +16 -0
  31. package/src/config/index.ts +30 -0
  32. package/src/config/io.ts +112 -0
  33. package/src/config/tsconfig.ts +138 -0
  34. package/src/config/types.ts +63 -0
  35. package/src/entry.ts +42 -0
  36. package/src/index.ts +57 -0
  37. package/src/load-app.ts +89 -0
  38. package/src/registry/__tests__/env-writer.test.ts +83 -0
  39. package/src/registry/apply.ts +268 -0
  40. package/src/registry/client.ts +127 -0
  41. package/src/registry/env-writer.ts +135 -0
  42. package/src/registry/index.ts +18 -0
  43. package/src/registry/rewrite.ts +177 -0
  44. package/src/registry/snapshot.ts +1018 -0
  45. package/src/registry/types.ts +62 -0
  46. package/src/templates.ts +141 -0
@@ -0,0 +1,1018 @@
1
+ /**
2
+ * Auto-generated by tools/build-snapshots.ts. Do not edit.
3
+ *
4
+ * Bundled snapshot of the registry, regenerated on every install + deploy.
5
+ */
6
+
7
+ import type { RegistryComponent, RegistryIndex } from "./types.ts"
8
+
9
+ export const SNAPSHOT_INDEX: RegistryIndex = {
10
+ "schema": 1,
11
+ "generatedAt": "2026-05-24T12:15:05.341Z",
12
+ "source": "https://github.com/usehyper/hyper",
13
+ "components": [
14
+ {
15
+ "name": "agent-rules",
16
+ "version": "0.1.0",
17
+ "description": "Cursor / Claude Code rules teaching AI assistants how to compose Hyper routes, plugins, and middleware correctly.",
18
+ "registryDeps": [],
19
+ "fileCount": 2,
20
+ "title": "Agent rules"
21
+ },
22
+ {
23
+ "name": "auth-jwt",
24
+ "version": "0.1.0",
25
+ "description": "JWT auth middleware + .auth() route builder sugar for Hyper.",
26
+ "registryDeps": [
27
+ "core"
28
+ ],
29
+ "fileCount": 2
30
+ },
31
+ {
32
+ "name": "cache",
33
+ "version": "0.1.0",
34
+ "description": "SWR + ETag + stampede protection for Hyper routes.",
35
+ "registryDeps": [
36
+ "core"
37
+ ],
38
+ "fileCount": 2
39
+ },
40
+ {
41
+ "name": "client",
42
+ "version": "0.1.0",
43
+ "description": "Typed RPC client + codegen for Hyper.",
44
+ "registryDeps": [
45
+ "core"
46
+ ],
47
+ "fileCount": 7
48
+ },
49
+ {
50
+ "name": "compress",
51
+ "version": "0.1.0",
52
+ "description": "Content-negotiated gzip/brotli compression plugin for Hyper.",
53
+ "registryDeps": [
54
+ "core"
55
+ ],
56
+ "fileCount": 1
57
+ },
58
+ {
59
+ "name": "core",
60
+ "version": "0.1.0",
61
+ "description": "Hyper core — Bun-first, AI-native API framework runtime.",
62
+ "registryDeps": [],
63
+ "fileCount": 22
64
+ },
65
+ {
66
+ "name": "cors",
67
+ "version": "0.1.0",
68
+ "description": "Minimal, strict CORS middleware for Hyper.",
69
+ "registryDeps": [
70
+ "core"
71
+ ],
72
+ "fileCount": 1
73
+ },
74
+ {
75
+ "name": "csp",
76
+ "version": "0.1.0",
77
+ "description": "Content-Security-Policy + sibling headers (CSP, CORP, COEP, COOP, Report-To) for Hyper.",
78
+ "registryDeps": [
79
+ "core"
80
+ ],
81
+ "fileCount": 1
82
+ },
83
+ {
84
+ "name": "dev-mcp",
85
+ "version": "0.1.0",
86
+ "description": "Dev-mode app-as-MCP server — expose /.hyper/mcp with introspection + replay tools.",
87
+ "registryDeps": [
88
+ "core"
89
+ ],
90
+ "fileCount": 4
91
+ },
92
+ {
93
+ "name": "idempotency",
94
+ "version": "0.1.0",
95
+ "description": "Idempotency-Key middleware — one-shot result caching for mutating requests.",
96
+ "registryDeps": [
97
+ "core"
98
+ ],
99
+ "fileCount": 3
100
+ },
101
+ {
102
+ "name": "log",
103
+ "version": "0.1.0",
104
+ "description": "Wide-event structured logger for Hyper — the reference plugin.",
105
+ "registryDeps": [
106
+ "core"
107
+ ],
108
+ "fileCount": 11
109
+ },
110
+ {
111
+ "name": "mcp",
112
+ "version": "0.1.0",
113
+ "description": "Model Context Protocol (MCP) adapter for Hyper.",
114
+ "registryDeps": [
115
+ "core"
116
+ ],
117
+ "fileCount": 3
118
+ },
119
+ {
120
+ "name": "msgpack",
121
+ "version": "0.1.0",
122
+ "description": "MessagePack wire format for Hyper — content-negotiated encode/decode.",
123
+ "registryDeps": [
124
+ "core"
125
+ ],
126
+ "fileCount": 2
127
+ },
128
+ {
129
+ "name": "openapi",
130
+ "version": "0.1.0",
131
+ "description": "OpenAPI 3.1 serializer + Swagger UI for Hyper. Pluggable SchemaConverter.",
132
+ "registryDeps": [
133
+ "core"
134
+ ],
135
+ "fileCount": 5
136
+ },
137
+ {
138
+ "name": "openapi-arktype",
139
+ "version": "0.1.0",
140
+ "description": "SchemaConverter for ArkType to @hyper/openapi.",
141
+ "registryDeps": [
142
+ "openapi"
143
+ ],
144
+ "fileCount": 1
145
+ },
146
+ {
147
+ "name": "openapi-valibot",
148
+ "version": "0.1.0",
149
+ "description": "SchemaConverter for Valibot to @hyper/openapi.",
150
+ "registryDeps": [
151
+ "openapi"
152
+ ],
153
+ "fileCount": 1
154
+ },
155
+ {
156
+ "name": "openapi-zod",
157
+ "version": "0.1.0",
158
+ "description": "SchemaConverter for Zod (v3 + v4) to @hyper/openapi.",
159
+ "registryDeps": [
160
+ "openapi"
161
+ ],
162
+ "fileCount": 1
163
+ },
164
+ {
165
+ "name": "otel",
166
+ "version": "0.1.0",
167
+ "description": "OpenTelemetry tracing + SLO histograms for Hyper.",
168
+ "registryDeps": [
169
+ "core"
170
+ ],
171
+ "fileCount": 2
172
+ },
173
+ {
174
+ "name": "rate-limit",
175
+ "version": "0.1.0",
176
+ "description": "Token-bucket rate limiting for Hyper. In-memory + pluggable stores.",
177
+ "registryDeps": [
178
+ "core"
179
+ ],
180
+ "fileCount": 2
181
+ },
182
+ {
183
+ "name": "session",
184
+ "version": "0.1.0",
185
+ "description": "Signed-cookie session middleware for Hyper. Pluggable stores.",
186
+ "registryDeps": [
187
+ "core"
188
+ ],
189
+ "fileCount": 3
190
+ },
191
+ {
192
+ "name": "subscribe",
193
+ "version": "0.1.0",
194
+ "description": "route.subscribe() primitive — projects to SSE, MCP resource notifications, tRPC subscriptions.",
195
+ "registryDeps": [
196
+ "core"
197
+ ],
198
+ "fileCount": 1
199
+ },
200
+ {
201
+ "name": "testing",
202
+ "version": "0.1.0",
203
+ "description": "Testing helpers for Hyper apps — app.test, fakeRequest, matchers, memory stores, fuzz.",
204
+ "registryDeps": [
205
+ "core"
206
+ ],
207
+ "fileCount": 13
208
+ },
209
+ {
210
+ "name": "trpc",
211
+ "version": "0.1.0",
212
+ "description": "tRPC bridge — mount tRPC into Hyper, or convert a tRPC router to Hyper routes.",
213
+ "registryDeps": [
214
+ "core"
215
+ ],
216
+ "fileCount": 4
217
+ }
218
+ ]
219
+ } as const
220
+
221
+ export const SNAPSHOT_COMPONENTS: Readonly<Record<string, RegistryComponent>> = {
222
+ "agent-rules": {
223
+ "name": "agent-rules",
224
+ "version": "0.1.0",
225
+ "description": "Cursor / Claude Code rules teaching AI assistants how to compose Hyper routes, plugins, and middleware correctly.",
226
+ "readme": "# agent-rules\n\nDrops `.cursor/rules/hyper.md` and `AGENTS.md` into your repo so Cursor, Claude\nCode, and other AI assistants understand how Hyper apps are structured: the\nchain API on `Hyper`, the explicit `route.<verb>().handle()` builder, the\n`decorate()` typing flow, plugin authoring, and the secure-by-default posture.\n\n```bash\nhyper add agent-rules\n```\n\nAfter install, AI agents get the rules automatically — no extra setup.\n",
227
+ "registryDeps": [],
228
+ "peerDeps": {},
229
+ "optionalPeerDeps": {},
230
+ "files": [
231
+ {
232
+ "path": ".cursor/rules/hyper.md",
233
+ "contents": "---\ndescription: How to write Hyper routes, plugins, and middleware in this repo\nglobs:\n - src/hyper/**/*.ts\n - src/**/*.ts\nalwaysApply: true\n---\n\n# Hyper rules\n\nThis project uses [Hyper](https://hyperjs.ai), a Bun-first API framework whose\nsource code lives in this repository at `src/hyper/<component>/`. Imports use\nthe `@hyper/*` alias (mapped via `tsconfig.json` `paths`).\n\n## Authoring routes\n\nTwo equivalent styles. Prefer the chain API for app composition; prefer the\nbuilder for routes you want to attach middleware/decorators/types to.\n\n### Chain API (preferred for top-level apps)\n\n```ts\nimport { Hyper, ok } from \"@hyper/core\"\nimport { z } from \"zod\"\n\nexport default new Hyper()\n .get(\"/health\", \"OK\")\n .post(\n \"/users\",\n { body: z.object({ name: z.string(), email: z.email() }) },\n ({ body }) => ok({ id: crypto.randomUUID(), ...body }),\n )\n .listen(3000)\n```\n\n### Route builder (preferred when you need typed responses + middleware)\n\n```ts\nimport { ok, route, notFound } from \"@hyper/core\"\nimport { z } from \"zod\"\n\nconst UserParams = z.object({ id: z.string() })\n\nexport const getUser = route\n .get(\"/users/:id\")\n .params(UserParams)\n .handle(async ({ params, ctx }) => {\n const u = await ctx.store.get(params.id)\n if (!u) return notFound({ code: \"user_not_found\" })\n return ok(u)\n })\n```\n\n## Composing apps with `.use()`\n\n`.use()` is polymorphic. It accepts:\n\n- A sub-`Hyper` instance — its prefix is honored\n- A `HyperApp` from `app({...})`\n- A raw `Route` value or array of routes\n- A plugin returned by a plugin factory (`hyperLog(...)`, `cors(...)`, etc.)\n- A plain middleware (object with `start`/`success`/`error`/`finish`)\n\n```ts\nimport { Hyper } from \"@hyper/core\"\nimport { hyperLog } from \"@hyper/log\"\nimport { cors } from \"@hyper/cors\"\nimport users from \"./routes/users.ts\"\nimport posts from \"./routes/posts.ts\"\n\nexport default new Hyper()\n .use(hyperLog({ service: \"api\" }))\n .use(cors({ origin: [\"https://example.com\"] }))\n .use(users) // honors `users`'s own prefix\n .use(\"/v1\", posts) // re-prefix\n .listen(3000)\n```\n\n## Decorating context\n\nUse `.decorate()` (or `decorate: [...]` in `app({...})`) to attach typed\nservices to `ctx`. Always extend `AppContext` via module augmentation so\nhandlers see the right types.\n\n```ts\nimport { Hyper } from \"@hyper/core\"\nimport { db } from \"./db.ts\"\n\ndeclare module \"@hyper/core\" {\n interface AppContext {\n readonly db: typeof db\n }\n}\n\nexport default new Hyper().decorate(() => ({ db }))\n```\n\n## Response helpers\n\nAlways return through helpers — they project to OpenAPI/MCP/client-types\ncorrectly. Never `new Response()` directly.\n\n```ts\nimport {\n ok, created, accepted, noContent,\n badRequest, unauthorized, forbidden, notFound, conflict, unprocessable, tooManyRequests,\n redirect, html, text, sse, stream, file,\n} from \"@hyper/core\"\n```\n\n## Errors\n\nThrow `HyperError` for typed errors — they project to the route's `errors`\nunion and serialize consistently.\n\n```ts\nimport { createError } from \"@hyper/core\"\n\nthrow createError({ status: 409, code: \"duplicate_email\", message: \"Email already in use\" })\n```\n\n## Validation\n\nBody / params / query schemas are Standard Schema-compatible: Zod, Valibot,\nArkType all work. Schemas declared on a route project to OpenAPI input\nschemas automatically.\n\n## Secure-by-default — do NOT disable lightly\n\nHyper sets these for every response unless explicitly turned off:\n\n- `x-content-type-options: nosniff`\n- `x-frame-options: DENY`\n- `referrer-policy: strict-origin-when-cross-origin`\n- `strict-transport-security` (production only)\n- 1MB body cap\n- prototype-pollution guards on JSON bodies\n- per-route timeouts\n\nAuth endpoints default to rate-limiting via `@hyper/rate-limit`. JWT secrets\nmust be ≥32 bytes (`@hyper/auth-jwt` will refuse to start otherwise).\n\n## Testing\n\nUse `@hyper/testing` — `app.test()`, `call()`, memory stores, deterministic\nclocks. Tests should run against `app.fetch(new Request(...))` directly,\nno network.\n\n```ts\nimport { describe, expect, test } from \"bun:test\"\nimport app from \"../src/app.ts\"\n\ndescribe(\"users\", () => {\n test(\"GET /users/:id\", async () => {\n const res = await app.fetch(new Request(\"http://localhost/users/u1\"))\n expect(res.status).toBe(200)\n })\n})\n```\n\n## File layout convention\n\n```\nsrc/\n hyper/ # Hyper framework source (managed by `hyper` CLI)\n core/ # @hyper/core\n log/ # @hyper/log\n cors/ # @hyper/cors\n ...\n app.ts # entrypoint; default-exports a Hyper instance or HyperApp\n routes/ # sub-app modules (preferred over inline routes)\n schemas/ # Zod / Valibot schemas\n```\n\n`src/hyper/` is owned by the registry — do not edit by hand unless you intend\nto fork that component (and accept that `hyper update` will conflict). Run\n`hyper diff` to inspect drift between your local copy and the upstream\nregistry.\n",
234
+ "sha256": "10d849a3ab7b53790b7e021d5a941f4224f3c679f9da5f1f076474fe2b9a5d24",
235
+ "target": "@root/.cursor/rules/hyper.md"
236
+ },
237
+ {
238
+ "path": "AGENTS.md",
239
+ "contents": "# AGENTS.md\n\nGuidance for AI coding agents working in this repository.\n\nThis project uses [Hyper](https://hyperjs.ai), a Bun-first API framework. The\nframework source is **vendored into this repo** under `src/hyper/<component>/`\nand managed by the `hyper` CLI — components are installed from a registry and\ncopied directly into the project, not pulled in as npm dependencies. You own\nthe code; you can edit it freely; `hyper update` will pull upstream changes\nwhen you ask for them.\n\n## Quick orientation\n\n- `src/app.ts` — entrypoint. Default-exports a `Hyper` instance or `HyperApp`.\n- `src/hyper/core/` — framework runtime. Imports as `@hyper/core`.\n- `src/hyper/<plugin>/` — installable plugins (log, cors, auth-jwt, …). Each is\n imported as `@hyper/<plugin>`.\n- `package.json` has **no `@usehyper/*` deps** — everything ships via the\n registry.\n- `hyper.config.json` — the registry config (URL, baseDir, alias).\n- `hyper.lock.json` — pins each installed component's version + per-file hash.\n\n## When you write code\n\n1. Import from `@hyper/<component>` — never `@usehyper/<component>`.\n2. Always return through Hyper's response helpers (`ok`, `created`, `notFound`, …).\n3. Always declare schemas (`body`, `params`, `query`) — they project to\n OpenAPI/MCP/client types automatically.\n4. Use `.decorate()` for typed services on `ctx`. Augment `AppContext` via\n `declare module \"@hyper/core\"`.\n5. For protected routes, chain `.auth()` from `@hyper/auth-jwt`.\n6. Never weaken the secure-by-default headers without an explicit reason.\n\n## When you install components\n\nUse the CLI, not edits:\n\n```bash\nhyper add cors\nhyper add auth-jwt\nhyper add openapi openapi-zod\nhyper diff log # inspect drift\nhyper update log # bump to latest registry version\nhyper add --info session # show readme/files/deps without installing\n```\n\nThe CLI rewrites `@hyper/*` imports to whatever alias is configured in\n`hyper.config.json`. Do not hand-edit the `paths` mapping.\n\n## When you debug or extend\n\n- `hyper routes` — print the route graph\n- `hyper openapi` — emit OpenAPI 3.1\n- `hyper client out.ts` — emit a typed RPC client\n- `hyper mcp` — serve the app's routes over MCP for AI introspection\n- `hyper bench --tests` — measure per-route latency\n\n## Style\n\n- Imports use `.ts` extensions (`from \"./schemas.ts\"`) — `verbatimModuleSyntax`\n is on.\n- Prefer the chain API (`new Hyper().get(...)`) for top-level apps; prefer\n the builder (`route.get(...).body(...).handle(...)`) for sub-app modules.\n- Tests run via `bun test` against `app.fetch(new Request(...))` — no network.\n",
240
+ "sha256": "8b6b968951c70d27234e5642b2636be927bdff7e611fa86a2b1cb6a8f67e9446",
241
+ "target": "@root/AGENTS.md"
242
+ }
243
+ ],
244
+ "subpaths": {},
245
+ "title": "Agent rules",
246
+ "docs": "Files installed:\n - AGENTS.md (project root)\n - .cursor/rules/hyper.md\n\nKeep these committed so AI assistants pick up Hyper's conventions automatically. Re-run `hyper add agent-rules` after upgrading Hyper to refresh."
247
+ },
248
+ "auth-jwt": {
249
+ "name": "auth-jwt",
250
+ "version": "0.1.0",
251
+ "description": "JWT auth middleware + .auth() route builder sugar for Hyper.",
252
+ "readme": "# @hyper/auth-jwt\n\nJWT auth plugin for Hyper — bearer-token verification, typed `ctx.user`, role/scope guards.\n\n## Install\n\n```bash\nbun add @hyper/auth-jwt\n```\n\n## Usage\n\n```ts\nimport { Hyper, ok } from \"@hyper/core\"\nimport { authJwtPlugin } from \"@hyper/auth-jwt\"\n\nexport default new Hyper()\n .use(authJwtPlugin({ secretEnv: \"JWT_SECRET\" }))\n .get(\"/me\", ({ ctx }) => ok({ user: ctx.user }))\n .listen(3000)\n```\n\n## Docs\n\nSee the [main README](../../README.md) and [docs/](../../docs) for guides and integration recipes.\n\n## License\n\nMIT\n",
253
+ "registryDeps": [
254
+ "core"
255
+ ],
256
+ "peerDeps": {},
257
+ "optionalPeerDeps": {},
258
+ "files": [
259
+ {
260
+ "path": "auth-jwt/index.ts",
261
+ "contents": "/**\n * @hyper/auth-jwt — JWT authentication middleware + `.auth()` route sugar.\n *\n * app({ plugins: [authJwtPlugin({ secret: env.JWT_SECRET })] })\n * route.get(\"/me\").auth().handle((c) => ok({ user: c.ctx.user }))\n *\n * `route.auth()` is a thin wrapper around `route.meta({ auth: true })`\n * plus a pre-chained middleware that enforces presence of `ctx.user`.\n */\n\nimport { RouteBuilder } from \"@hyper/core\"\nimport type { HyperPlugin, Middleware } from \"@hyper/core\"\nimport { JwtError, type JwtPayload, type VerifyOptions, verifyJwt } from \"./jwt.ts\"\n\nexport { JwtError, verifyJwt } from \"./jwt.ts\"\nexport type { JwtAlgorithm, JwtHeader, JwtPayload, VerifyOptions } from \"./jwt.ts\"\n\n/**\n * Default `ctx.user` shape when no `loadUser` is supplied. Mirrors the\n * populated fields in the middleware below.\n */\nexport interface AuthUser {\n readonly sub: string\n readonly scope?: string | readonly string[]\n}\n\nexport interface AuthJwtConfig extends VerifyOptions {\n /** Optional: map verified payload → ctx.user. */\n readonly loadUser?: (payload: JwtPayload) => unknown | Promise<unknown>\n /** Optional: where to look for the token. Default: Authorization: Bearer … */\n readonly extract?: (req: Request) => string | null\n /**\n * Opt out of the 32-byte secret length check. Off by default — a secret\n * shorter than 32 bytes is a bootable footgun and Hyper fails fast.\n */\n readonly allowShortSecret?: boolean\n}\n\n/** Minimum secret length we enforce at boot. 32 bytes = 256 bits. */\nexport const MIN_JWT_SECRET_BYTES = 32\n\n/** Validates a JWT secret against the minimum-length rule. Throws with why/fix. */\nexport function validateJwtSecret(\n secret: string,\n opts: { readonly allowShort?: boolean } = {},\n): void {\n if (opts.allowShort) return\n const bytes = new TextEncoder().encode(secret).byteLength\n if (bytes < MIN_JWT_SECRET_BYTES) {\n throw new Error(\n `@hyper/auth-jwt: secret is ${bytes} bytes; minimum is ${MIN_JWT_SECRET_BYTES}. Why: short HS256 secrets are brute-forceable in hours on commodity hardware. Fix: generate a 32+ byte secret (e.g., \\`openssl rand -base64 48\\`) or pass \\`allowShortSecret: true\\` at your own risk.`,\n )\n }\n}\n\ndeclare module \"@hyper/core\" {\n interface AppContext {\n /**\n * The authenticated user. Defaults to the `AuthUser` shape\n * populated by the middleware when no `loadUser` is supplied.\n *\n * To type a custom shape, augment this interface in your app:\n * declare module \"@hyper/core\" {\n * interface AppContext { user?: MyUser }\n * }\n */\n readonly user?: AuthUser\n readonly jwt?: JwtPayload\n }\n}\n\nexport function authJwt(config: AuthJwtConfig): Middleware {\n validateJwtSecret(config.secret, { allowShort: config.allowShortSecret ?? false })\n const extract = config.extract ?? defaultExtract\n return async ({ ctx, req, next }) => {\n const token = extract(req)\n if (!token) return unauthorized(\"missing_token\")\n try {\n const { payload } = await verifyJwt(token, config)\n ;(ctx as { jwt?: JwtPayload }).jwt = payload\n if (config.loadUser) {\n ;(ctx as { user?: unknown }).user = await config.loadUser(payload)\n } else {\n ;(ctx as { user?: unknown }).user = { sub: payload.sub, scope: payload.scope }\n }\n return next()\n } catch (e) {\n if (e instanceof JwtError) return unauthorized(e.code)\n throw e\n }\n }\n}\n\nfunction defaultExtract(req: Request): string | null {\n const h = req.headers.get(\"authorization\")\n if (!h) return null\n const [type, value] = h.split(\" \")\n return type?.toLowerCase() === \"bearer\" ? (value ?? null) : null\n}\n\nfunction unauthorized(code: string) {\n return new Response(JSON.stringify({ error: \"unauthorized\", code }), {\n status: 401,\n headers: {\n \"content-type\": \"application/json\",\n \"www-authenticate\": 'Bearer realm=\"hyper\", error=\"invalid_token\"',\n },\n })\n}\n\n/**\n * Plugin — installs the `.auth()` method on the route builder prototype.\n * Consumers only need to chain `use(authJwt(...))` on protected routes,\n * or call `.auth()` as sugar.\n */\nexport function authJwtPlugin(config: AuthJwtConfig): HyperPlugin {\n validateJwtSecret(config.secret, { allowShort: config.allowShortSecret ?? false })\n installAuthMethod(authJwt(config))\n return {\n name: \"@hyper/auth-jwt\",\n }\n}\n\n/**\n * Install `.auth()` on the RouteBuilder prototype. Safe to call many times —\n * idempotent. Exported so tests and userland can pre-install without a plugin.\n */\nexport function installAuthMethod(mw: Middleware): void {\n const proto = (\n RouteBuilder as unknown as {\n prototype: {\n auth?: () => unknown\n use: (m: Middleware) => unknown\n meta: (m: Record<string, unknown>) => { use: (m: Middleware) => unknown }\n }\n }\n ).prototype\n if (proto.auth) return\n proto.auth = function auth(this: {\n meta: (m: Record<string, unknown>) => { use: (m: Middleware) => unknown }\n }) {\n return this.meta({ auth: true }).use(mw)\n }\n}\n",
262
+ "sha256": "96b122083a7cd37615963b08e411eadfee44b8cd3b8ca971015b92e6a7d98628"
263
+ },
264
+ {
265
+ "path": "auth-jwt/jwt.ts",
266
+ "contents": "/**\n * Minimal HS256/HS384/HS512 verify-only JWT implementation.\n *\n * We explicitly do NOT sign — in 2026 all first-party auth flows issue\n * tokens through the OAuth provider; server-side we just verify.\n *\n * For RS/ES we expose a verify hook — consumers plug in their JWKS\n * resolver via `jwks(url)` utility.\n */\n\nimport { timingSafeEqual } from \"node:crypto\"\n\nexport type JwtAlgorithm = \"HS256\" | \"HS384\" | \"HS512\"\n\nexport interface JwtHeader {\n readonly alg: string\n readonly kid?: string\n readonly typ?: string\n}\n\nexport interface JwtPayload {\n readonly iss?: string\n readonly aud?: string | readonly string[]\n readonly sub?: string\n readonly exp?: number\n readonly nbf?: number\n readonly iat?: number\n readonly scope?: string\n readonly [k: string]: unknown\n}\n\nexport interface VerifyOptions {\n readonly secret?: string | Uint8Array\n readonly algorithms?: readonly JwtAlgorithm[]\n readonly issuer?: string\n readonly audience?: string\n readonly clockToleranceSec?: number\n}\n\nexport class JwtError extends Error {\n constructor(\n readonly code: string,\n msg: string,\n ) {\n super(msg)\n }\n}\n\nconst SUPPORTED: Record<JwtAlgorithm, \"SHA-256\" | \"SHA-384\" | \"SHA-512\"> = {\n HS256: \"SHA-256\",\n HS384: \"SHA-384\",\n HS512: \"SHA-512\",\n}\n\nexport async function verifyJwt(\n token: string,\n options: VerifyOptions,\n): Promise<{ header: JwtHeader; payload: JwtPayload }> {\n const parts = token.split(\".\")\n if (parts.length !== 3) throw new JwtError(\"invalid_token\", \"malformed jwt\")\n const [h, p, sig] = parts\n const header = JSON.parse(b64urlToUtf8(h!)) as JwtHeader\n const payload = JSON.parse(b64urlToUtf8(p!)) as JwtPayload\n const alg = header.alg as JwtAlgorithm\n const allowed = new Set(options.algorithms ?? [\"HS256\"])\n if (!allowed.has(alg)) throw new JwtError(\"alg_not_allowed\", `disallowed alg: ${alg}`)\n\n if (alg.startsWith(\"HS\")) {\n const digest = SUPPORTED[alg]\n if (!digest) throw new JwtError(\"alg_unsupported\", `unsupported alg: ${alg}`)\n if (!options.secret) throw new JwtError(\"no_secret\", \"secret required for HMAC\")\n const key = await crypto.subtle.importKey(\n \"raw\",\n typeof options.secret === \"string\"\n ? new TextEncoder().encode(options.secret)\n : options.secret,\n { name: \"HMAC\", hash: digest },\n false,\n [\"sign\"],\n )\n const signed = await crypto.subtle.sign(\"HMAC\", key, new TextEncoder().encode(`${h}.${p}`))\n const expected = new Uint8Array(signed)\n const actual = b64urlToBytes(sig!)\n if (\n expected.length !== actual.length ||\n !timingSafeEqual(Buffer.from(expected), Buffer.from(actual))\n ) {\n throw new JwtError(\"bad_signature\", \"jwt signature mismatch\")\n }\n } else {\n throw new JwtError(\"alg_unsupported\", `unsupported alg: ${alg}`)\n }\n\n const now = Math.floor(Date.now() / 1000)\n const skew = options.clockToleranceSec ?? 30\n if (typeof payload.exp === \"number\" && now > payload.exp + skew) {\n throw new JwtError(\"expired\", \"jwt expired\")\n }\n if (typeof payload.nbf === \"number\" && now + skew < payload.nbf) {\n throw new JwtError(\"not_yet_valid\", \"jwt not yet valid\")\n }\n if (options.issuer && payload.iss !== options.issuer) {\n throw new JwtError(\"bad_issuer\", `issuer ${payload.iss ?? \"\"} != ${options.issuer}`)\n }\n if (options.audience) {\n const aud = payload.aud\n const ok = Array.isArray(aud) ? aud.includes(options.audience) : aud === options.audience\n if (!ok) throw new JwtError(\"bad_audience\", \"aud mismatch\")\n }\n return { header, payload }\n}\n\nfunction b64urlToUtf8(s: string): string {\n return new TextDecoder().decode(b64urlToBytes(s))\n}\nfunction b64urlToBytes(s: string): Uint8Array {\n const pad = s.length % 4 === 0 ? 0 : 4 - (s.length % 4)\n const b64 = (s + \"====\".slice(0, pad)).replace(/-/g, \"+\").replace(/_/g, \"/\")\n const bin = atob(b64)\n const out = new Uint8Array(bin.length)\n for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i)\n return out\n}\n",
267
+ "sha256": "4b6868d1eeea3479e60a49ed1487ad9b6eee3b5d07491c375c4d9f0b44615360"
268
+ }
269
+ ],
270
+ "subpaths": {}
271
+ },
272
+ "cache": {
273
+ "name": "cache",
274
+ "version": "0.1.0",
275
+ "description": "SWR + ETag + stampede protection for Hyper routes.",
276
+ "readme": "# @hyper/cache\n\nSWR + ETag + stampede protection for Hyper routes.\n\n## Install\n\n```bash\nbun add @hyper/cache\n```\n\n## Usage\n\n```ts\nimport { Hyper, ok } from \"@hyper/core\"\nimport { cache } from \"@hyper/cache\"\n\nexport default new Hyper()\n .use(cache({ maxAgeMs: 60_000 }))\n .get(\"/feed\", async () => ok(await loadFeed()))\n .listen(3000)\n```\n\n## Docs\n\nSee the [main README](../../README.md) and [docs/](../../docs) for guides and integration recipes.\n\n## License\n\nMIT\n",
277
+ "registryDeps": [
278
+ "core"
279
+ ],
280
+ "peerDeps": {},
281
+ "optionalPeerDeps": {},
282
+ "files": [
283
+ {
284
+ "path": "cache/index.ts",
285
+ "contents": "/**\n * @hyper/cache — stale-while-revalidate + ETag + stampede protection.\n *\n * Keyed by method + URL. Only caches GET/HEAD.\n *\n * fresh (age <= maxAge): serve from cache (no revalidation)\n * stale (age <= maxAge+swr): serve stale + kick off background refresh\n * dead (older than stale): synchronous refresh, single-flight locked\n *\n * ETag:\n * - We auto-generate a weak ETag from the response body (xxhash3 if\n * available, SHA-1 fallback).\n * - If `If-None-Match` matches, we return 304.\n */\n\nimport { type Middleware, coerce } from \"@hyper/core\"\n\nexport interface CacheEntry {\n readonly status: number\n readonly headers: Record<string, string>\n readonly body: Uint8Array\n readonly etag: string\n readonly createdAt: number\n}\n\nexport interface CacheStore {\n get(key: string): Promise<CacheEntry | undefined>\n set(key: string, value: CacheEntry): Promise<void>\n}\n\nexport function memoryCache(): CacheStore {\n const map = new Map<string, CacheEntry>()\n return {\n async get(k) {\n return map.get(k)\n },\n async set(k, v) {\n map.set(k, v)\n },\n }\n}\n\nexport interface CacheConfig {\n readonly store?: CacheStore\n /** Seconds. */\n readonly maxAge: number\n /** Seconds of stale grace for SWR. */\n readonly staleWhileRevalidate?: number\n readonly methods?: readonly string[]\n}\n\nexport function cache(config: CacheConfig): Middleware {\n const store = config.store ?? memoryCache()\n const maxAgeMs = config.maxAge * 1000\n const swrMs = (config.staleWhileRevalidate ?? 0) * 1000\n const methods = new Set(config.methods ?? [\"GET\", \"HEAD\"])\n const inFlight = new Map<string, Promise<CacheEntry>>()\n\n return async ({ req, next }) => {\n if (!methods.has(req.method)) return next()\n const key = `${req.method} ${req.url}`\n const now = Date.now()\n const existing = await store.get(key)\n const ifNoneMatch = req.headers.get(\"if-none-match\")\n\n if (existing) {\n const age = now - existing.createdAt\n const ageSec = Math.floor(age / 1000)\n if (age <= maxAgeMs) {\n if (ifNoneMatch && ifNoneMatch === existing.etag) {\n return notModified(existing.etag)\n }\n return materialize(existing, \"fresh\", ageSec, maxAgeMs / 1000, swrMs / 1000)\n }\n if (age <= maxAgeMs + swrMs) {\n if (!inFlight.has(key)) {\n inFlight.set(\n key,\n refresh({ next, store, key })\n .catch(() => existing)\n .finally(() => inFlight.delete(key)),\n )\n }\n if (ifNoneMatch && ifNoneMatch === existing.etag) {\n return notModified(existing.etag)\n }\n return materialize(existing, \"stale\", ageSec, maxAgeMs / 1000, swrMs / 1000)\n }\n }\n\n let p = inFlight.get(key)\n if (!p) {\n p = refresh({ next, store, key })\n inFlight.set(key, p)\n p.finally(() => inFlight.delete(key))\n }\n const entry = await p\n if (ifNoneMatch && ifNoneMatch === entry.etag) return notModified(entry.etag)\n return materialize(entry, \"miss\", 0, maxAgeMs / 1000, swrMs / 1000)\n }\n}\n\nasync function refresh(args: {\n next: () => Promise<unknown>\n store: CacheStore\n key: string\n}): Promise<CacheEntry> {\n const out = await args.next()\n const res = out instanceof Response ? out : coerce(out)\n const body = new Uint8Array(await res.clone().arrayBuffer())\n const headers: Record<string, string> = {}\n for (const [k, v] of res.headers) headers[k] = v\n const etag = `W/\"${await etagOf(body)}\"`\n const entry: CacheEntry = {\n status: res.status,\n headers,\n body,\n etag,\n createdAt: Date.now(),\n }\n if (res.status >= 200 && res.status < 300) await args.store.set(args.key, entry)\n return entry\n}\n\nfunction materialize(\n entry: CacheEntry,\n mode: \"fresh\" | \"stale\" | \"miss\",\n age: number,\n maxAge: number,\n swr: number,\n): Response {\n const h = new Headers(entry.headers)\n h.set(\"etag\", entry.etag)\n h.set(\"age\", age.toString())\n h.set(\"x-cache\", mode)\n if (maxAge > 0) {\n h.set(\n \"cache-control\",\n swr > 0\n ? `public, max-age=${maxAge}, stale-while-revalidate=${swr}`\n : `public, max-age=${maxAge}`,\n )\n }\n return new Response(entry.body, { status: entry.status, headers: h })\n}\n\nfunction notModified(etag: string): Response {\n return new Response(null, { status: 304, headers: { etag } })\n}\n\nasync function etagOf(buf: Uint8Array): Promise<string> {\n const xx = (Bun as unknown as { hash?: { xxHash3?: (b: Uint8Array) => bigint } }).hash?.xxHash3\n if (xx) return xx(buf).toString(16)\n const d = await crypto.subtle.digest(\"SHA-1\", buf)\n return Array.from(new Uint8Array(d))\n .map((b) => b.toString(16).padStart(2, \"0\"))\n .join(\"\")\n}\n",
286
+ "sha256": "0edc6e62868a82c1aceeb0c541b20c41e93eaf6b374c0b5cba955a171209861a"
287
+ },
288
+ {
289
+ "path": "cache/sqlite.ts",
290
+ "contents": "/**\n * bun:sqlite-backed CacheStore. Persistent, zero-dependency, ~10k rps.\n *\n * import { sqliteCache } from \"@hyper/cache/sqlite\"\n * const store = sqliteCache({ path: \"./cache.sqlite\" })\n * use(cache({ maxAge: 60, store }))\n *\n * The schema is WAL-mode, synchronous=NORMAL, with a size/entry cap.\n * Sweeps happen on read (lazy) and on `.sweep()` (manual).\n */\n\nimport { Database } from \"bun:sqlite\"\nimport type { CacheEntry, CacheStore } from \"./index.ts\"\n\nexport interface SqliteCacheOptions {\n /** File path. `\":memory:\"` for RAM-only. Default: `./.hyper/cache.sqlite`. */\n readonly path?: string\n /** Max entries; oldest evicted first. Default: 100_000. */\n readonly maxEntries?: number\n}\n\nexport function sqliteCache(opts: SqliteCacheOptions = {}): CacheStore & {\n readonly sweep: () => void\n readonly close: () => void\n} {\n const path = opts.path ?? \"./.hyper/cache.sqlite\"\n const maxEntries = opts.maxEntries ?? 100_000\n const db = new Database(path, { create: true })\n db.exec(\"PRAGMA journal_mode = WAL; PRAGMA synchronous = NORMAL;\")\n db.exec(`\n CREATE TABLE IF NOT EXISTS hyper_cache (\n k TEXT PRIMARY KEY,\n status INTEGER NOT NULL,\n headers TEXT NOT NULL,\n body BLOB NOT NULL,\n etag TEXT NOT NULL,\n created_at INTEGER NOT NULL\n );\n CREATE INDEX IF NOT EXISTS hyper_cache_created_at ON hyper_cache (created_at);\n `)\n const stmtGet = db.prepare(\n \"SELECT status, headers, body, etag, created_at AS createdAt FROM hyper_cache WHERE k = ?\",\n )\n const stmtSet = db.prepare(\n \"INSERT OR REPLACE INTO hyper_cache (k, status, headers, body, etag, created_at) VALUES (?,?,?,?,?,?)\",\n )\n const stmtCount = db.prepare(\"SELECT COUNT(*) AS c FROM hyper_cache\")\n const stmtPrune = db.prepare(\n \"DELETE FROM hyper_cache WHERE rowid IN (SELECT rowid FROM hyper_cache ORDER BY created_at ASC LIMIT ?)\",\n )\n\n const sweep = () => {\n const row = stmtCount.get() as { c: number } | undefined\n if (row && row.c > maxEntries) {\n stmtPrune.run(row.c - maxEntries)\n }\n }\n\n return {\n async get(key) {\n const row = stmtGet.get(key) as\n | { status: number; headers: string; body: Uint8Array; etag: string; createdAt: number }\n | undefined\n if (!row) return undefined\n return {\n status: row.status,\n headers: JSON.parse(row.headers) as Record<string, string>,\n body: row.body,\n etag: row.etag,\n createdAt: row.createdAt,\n } satisfies CacheEntry\n },\n async set(key, value) {\n stmtSet.run(\n key,\n value.status,\n JSON.stringify(value.headers),\n value.body,\n value.etag,\n value.createdAt,\n )\n sweep()\n },\n sweep,\n close: () => db.close(),\n }\n}\n",
291
+ "sha256": "a31497d6bb54b3dbdf0286c0a5824ba7115a28c1f8e68e0ea889999363787c22"
292
+ }
293
+ ],
294
+ "subpaths": {}
295
+ },
296
+ "client": {
297
+ "name": "client",
298
+ "version": "0.1.0",
299
+ "description": "Typed RPC client + codegen for Hyper.",
300
+ "readme": "# @hyper/client\n\nTyped RPC client + codegen for Hyper apps. Optional TanStack Query bindings at `@hyper/client/tanstack-query`.\n\n## Install\n\n```bash\nbun add @hyper/client\n```\n\n## Usage\n\nRuntime client:\n\n```ts\nimport { createClient, fetchTransport } from \"@hyper/client\"\n\nconst c = createClient(fetchTransport({ baseUrl: \"https://api.example.com\" }))\n\nconst res = await c.call({ method: \"GET\", path: \"/users/:id\", params: { id: \"abc\" } })\n```\n\nType-safe client via codegen:\n\n```bash\nbun x hyper client ./src/generated/client.ts\n```\n\n```ts\nimport { client } from \"./generated/client.ts\"\nconst me = await client.users.show({ id: \"abc\" })\n```\n\n## Docs\n\nSee the [main README](../../README.md) and [docs/](../../docs) for guides and integration recipes.\n\n## License\n\nMIT\n",
301
+ "registryDeps": [
302
+ "core"
303
+ ],
304
+ "peerDeps": {},
305
+ "optionalPeerDeps": {},
306
+ "files": [
307
+ {
308
+ "path": "client/client.ts",
309
+ "contents": "/**\n * createClient — a typed-ish RPC client. The generics are threaded so that\n * if you hand a `PlainRouter` shape to `createClient`, you get dot-paths.\n *\n * Example:\n * const api = createClient<typeof router>(fetchTransport({ baseUrl }))\n * await api.users.list()\n * await api.users.create({ title: \"x\" })\n */\n\nimport type { Transport } from \"./types.ts\"\n\n// Stand-alone clone of the core PlainRouter shape to avoid a type dep.\ntype RouterLike = {\n // biome-ignore lint/suspicious/noExplicitAny: structural router\n [key: string]: any\n}\n\ntype CallableRouteLike = {\n readonly method: string\n readonly path: string\n readonly meta?: unknown\n}\n\n/**\n * We expose the transport directly. Codegen emits a thin wrapper with\n * the proper TypeScript shape for the user; the runtime is identical.\n */\nexport interface ClientContract {\n call<T = unknown>(input: {\n method: string\n path: string\n params?: Record<string, string>\n query?: Record<string, unknown>\n body?: unknown\n headers?: Record<string, string>\n signal?: AbortSignal\n }): Promise<T>\n}\n\nexport function createClient(transport: Transport): ClientContract {\n return {\n async call(input) {\n const path = applyPathParams(input.path, input.params)\n const url = input.query ? `${path}?${new URLSearchParams(stringifyQuery(input.query))}` : path\n const res = await transport.request({\n method: input.method,\n url,\n ...(input.headers ? { headers: input.headers } : {}),\n ...(input.body !== undefined ? { body: input.body } : {}),\n ...(input.signal ? { signal: input.signal } : {}),\n })\n if (res.status >= 400) {\n const err = extractError(res.data, res.status)\n throw Object.assign(new Error(err.message), err)\n }\n return res.data as never\n },\n }\n}\n\nexport function applyPathParams(path: string, params: Record<string, string> | undefined): string {\n if (!params) return path\n return path.replace(/:([A-Za-z0-9_]+)/g, (_, k: string) => {\n const v = params[k]\n if (v === undefined) throw new Error(`missing path param :${k}`)\n return encodeURIComponent(v)\n })\n}\n\nfunction stringifyQuery(q: Record<string, unknown>): Record<string, string> {\n const out: Record<string, string> = {}\n for (const [k, v] of Object.entries(q)) {\n if (v === undefined || v === null) continue\n out[k] = String(v)\n }\n return out\n}\n\nfunction extractError(\n data: unknown,\n status: number,\n): { status: number; code: string; message: string } {\n if (data && typeof data === \"object\" && \"error\" in data) {\n const e = (data as { error: Record<string, unknown> }).error\n return {\n status,\n code: typeof e.code === \"string\" ? e.code : \"unknown\",\n message: typeof e.message === \"string\" ? e.message : `HTTP ${status}`,\n }\n }\n return { status, code: \"unknown\", message: `HTTP ${status}` }\n}\n\n/**\n * routerToClient — mirrors a plain-object router tree at runtime, replacing\n * each CallableRoute leaf with a function that invokes the transport.\n * Codegen emits a static `.d.ts` equivalent; this is the runtime twin.\n */\nexport function routerToClient<R extends RouterLike>(router: R, transport: Transport): RouterLike {\n const client = createClient(transport)\n const walk = (node: unknown): unknown => {\n if (!node || typeof node !== \"object\") return node\n if (isRoute(node)) {\n const r = node as CallableRouteLike\n return async (input?: {\n params?: Record<string, string>\n query?: Record<string, unknown>\n body?: unknown\n headers?: Record<string, string>\n signal?: AbortSignal\n }) =>\n client.call({\n method: r.method,\n path: r.path,\n ...(input?.params && { params: input.params }),\n ...(input?.query && { query: input.query }),\n ...(input?.body !== undefined && { body: input.body }),\n ...(input?.headers && { headers: input.headers }),\n ...(input?.signal && { signal: input.signal }),\n })\n }\n const out: Record<string, unknown> = {}\n for (const [k, v] of Object.entries(node as Record<string, unknown>)) out[k] = walk(v)\n return out\n }\n return walk(router) as RouterLike\n}\n\nfunction isRoute(x: unknown): boolean {\n return Boolean(\n x &&\n typeof x === \"object\" &&\n typeof (x as { method?: unknown }).method === \"string\" &&\n typeof (x as { path?: unknown }).path === \"string\" &&\n typeof (x as { handler?: unknown }).handler === \"function\",\n )\n}\n",
310
+ "sha256": "718e43d48228b4a29c4e00d01ab4b02c31330968e6d466c753a2554d76f0f04d"
311
+ },
312
+ {
313
+ "path": "client/codegen.ts",
314
+ "contents": "/**\n * Codegen — consumes a `ClientManifest` (from @hyper/core) and emits a\n * pair of files:\n *\n * client.ts — a runtime proxy wrapping fetchTransport\n * client.d.ts — compile-time types for every route\n *\n * This is the moral equivalent of tRPC's router typings, minus reflection.\n *\n * Live-watch integration (hyper dev) is added in the CLI; this module stays\n * a pure function so it can be called from tests and from build pipelines.\n */\n\nimport type { ClientManifest } from \"@hyper/core\"\n\nexport interface CodegenOptions {\n readonly manifest: ClientManifest\n /** Default baseUrl embedded in the emitted file (overridable at runtime). */\n readonly baseUrl?: string\n /** Name for the generated root client (defaults to \"api\"). */\n readonly rootName?: string\n /**\n * Emit `Result<T, Errors>` tagged-union leaf signatures alongside the\n * exception-throwing leaves. Off by default for minimum-diff ergonomics;\n * apps that prefer explicit error handling should opt in.\n */\n readonly resultTypes?: boolean\n}\n\nexport function generateClient(opts: CodegenOptions): {\n runtime: string\n declaration: string\n} {\n const root = opts.rootName ?? \"api\"\n const resultTypes = opts.resultTypes ?? false\n const tree = buildTree(opts.manifest.routes)\n const dts = emitDts(tree, root, resultTypes)\n const js = emitRuntime(tree, root, opts.baseUrl, resultTypes)\n return { runtime: js, declaration: dts }\n}\n\n// --- path-to-namespace tree --------------------------------------------------\n\ninterface Leaf {\n readonly kind: \"leaf\"\n readonly method: string\n readonly path: string\n readonly name: string\n readonly errors?: readonly string[]\n readonly throws?: readonly number[]\n}\ninterface Branch {\n readonly kind: \"branch\"\n readonly children: Map<string, Leaf | Branch>\n}\n\ntype Node = Leaf | Branch\n\nfunction buildTree(routes: readonly ClientManifest[\"routes\"][number][]): Branch {\n const root: Branch = { kind: \"branch\", children: new Map() }\n for (const r of routes) {\n const name = r.name ?? defaultName(r.method, r.path)\n const segments = splitName(name)\n let cur: Branch = root\n for (let i = 0; i < segments.length - 1; i++) {\n const seg = segments[i]!\n let next = cur.children.get(seg)\n if (!next || next.kind !== \"branch\") {\n next = { kind: \"branch\", children: new Map() }\n cur.children.set(seg, next)\n }\n cur = next\n }\n cur.children.set(segments[segments.length - 1]!, {\n kind: \"leaf\",\n method: r.method,\n path: r.path,\n name,\n ...(r.errors && r.errors.length > 0 && { errors: r.errors }),\n ...(r.throws && r.throws.length > 0 && { throws: r.throws }),\n })\n }\n return root\n}\n\nfunction splitName(name: string): string[] {\n return name\n .replace(/[^A-Za-z0-9._]+/g, \"_\")\n .split(/[._]/)\n .filter((s) => s.length > 0)\n}\n\nfunction defaultName(method: string, path: string): string {\n return `${method.toLowerCase()}_${path.replace(/[^A-Za-z0-9]+/g, \"_\")}`\n}\n\n// --- emitters ---------------------------------------------------------------\n\nfunction emitRuntime(\n root: Branch,\n rootName: string,\n baseUrl: string | undefined,\n resultTypes: boolean,\n): string {\n const leafHelper = resultTypes\n ? `const leaf = (method, path) => async (input = {}) => {\\n try {\\n return { ok: true, value: await client.call({ method, path, ...input }) }\\n } catch (e) {\\n return { ok: false, error: { code: e.code ?? \"unknown\", status: e.status ?? 500, message: e.message ?? String(e) } }\\n }\\n }`\n : \"const leaf = (method, path) => (input = {}) => client.call({ method, path, ...input })\"\n const header = `/* eslint-disable */\\n/* Generated by @hyper/client/codegen. Do not edit. */\\nimport { createClient } from \"@hyper/client\"\\nimport { fetchTransport } from \"@hyper/client\"\\n\\nexport function build(opts = {}) {\\n const transport = opts.transport ?? fetchTransport({ baseUrl: opts.baseUrl ?? ${JSON.stringify(baseUrl ?? \"\")}, headers: opts.headers })\\n const client = createClient(transport)\\n ${leafHelper}\\n return ${runtimeLiteral(root)}\\n}\\n\\nexport const ${rootName} = build()\\n`\n return header\n}\n\nfunction runtimeLiteral(node: Node): string {\n if (node.kind === \"leaf\")\n return `leaf(${JSON.stringify(node.method)}, ${JSON.stringify(node.path)})`\n const entries: string[] = []\n for (const [k, child] of node.children) {\n entries.push(` ${safeKey(k)}: ${runtimeLiteral(child)}`)\n }\n return `{\\n${entries.join(\",\\n\")}\\n}`\n}\n\nfunction emitDts(root: Branch, rootName: string, resultTypes: boolean): string {\n const preamble = resultTypes\n ? `export type Ok<T> = { readonly ok: true; readonly value: T }\\nexport type Err<C extends string> = { readonly ok: false; readonly error: { readonly code: C; readonly status: number; readonly message: string } }\\nexport type Result<T, C extends string> = Ok<T> | Err<C>\\nexport type Leaf<T = unknown, C extends string = \"unknown\"> = (input?: Input) => Promise<Result<T, C>>\\n`\n : \"export type Leaf<T = unknown> = (input?: Input) => Promise<T>\\n\"\n const header = `/* Generated by @hyper/client/codegen. Do not edit. */\\nimport type { Transport } from \"@hyper/client\"\\n\\nexport type Input = {\\n params?: Record<string, string>\\n query?: Record<string, unknown>\\n body?: unknown\\n headers?: Record<string, string>\\n signal?: AbortSignal\\n}\\n\\n${preamble}\\nexport type Client = ${dtsLiteral(root, 0, resultTypes)}\\n\\nexport const ${rootName}: Client\\nexport function build(opts?: { baseUrl?: string; headers?: Record<string, string>; transport?: Transport }): Client\\n`\n return header\n}\n\nfunction dtsLiteral(node: Node, indent: number, resultTypes: boolean): string {\n if (node.kind === \"leaf\") {\n if (!resultTypes) return \"Leaf\"\n const codes = [...(node.errors ?? []), ...(node.throws ?? []).map((n) => `http_${n}`)]\n if (codes.length === 0) return \"Leaf\"\n const union = codes.map((c) => JSON.stringify(c)).join(\" | \")\n return `Leaf<unknown, ${union}>`\n }\n const pad = \" \".repeat(indent + 1)\n const entries: string[] = []\n for (const [k, child] of node.children) {\n entries.push(`${pad}${safeKey(k)}: ${dtsLiteral(child, indent + 1, resultTypes)}`)\n }\n return `{\\n${entries.join(\"\\n\")}\\n${\" \".repeat(indent)}}`\n}\n\nfunction safeKey(k: string): string {\n return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(k) ? k : JSON.stringify(k)\n}\n",
315
+ "sha256": "9de687b7462628bfb5727289445d85ea720798073baf5ed2d79a8312d26e24bc"
316
+ },
317
+ {
318
+ "path": "client/index.ts",
319
+ "contents": "/**\n * @hyper/client — typed RPC client + codegen for Hyper apps.\n *\n * Runtime: createClient(transport) gives a `.call({ method, path, ... })` primitive.\n * Typed: `hyper client <out>` emits `client.ts` + `client.d.ts` from the\n * running app's `toClientManifest()`.\n * TanStack: `@hyper/client/tanstack-query` ships queryOptions / mutationOptions.\n *\n * Exports are ergonomic re-exports; see individual files for detail.\n */\n\nexport { applyPathParams, createClient, routerToClient } from \"./client.ts\"\nexport type { ClientContract } from \"./client.ts\"\nexport { generateClient } from \"./codegen.ts\"\nexport type { CodegenOptions } from \"./codegen.ts\"\nexport { subscribe } from \"./sse.ts\"\nexport type { SubscribeOptions } from \"./sse.ts\"\nexport { fetchTransport } from \"./transport.ts\"\nexport type { FetchTransportConfig } from \"./transport.ts\"\nexport type {\n HyperRpcError,\n Result,\n Transport,\n TransportRequest,\n TransportResponse,\n} from \"./types.ts\"\n\n// Re-export core typing utilities for convenience\nexport type {\n InferRouterCtx,\n InferRouterInputs,\n InferRouterOutputs,\n} from \"@hyper/core\"\n",
320
+ "sha256": "1ba23c2698106352d179cc209fe1c263cd7956663ca38cbb83a93fc9a3ee2fbe"
321
+ },
322
+ {
323
+ "path": "client/sse.ts",
324
+ "contents": "/**\n * SSE subscribe helper.\n *\n * for await (const event of subscribe(\"/events\", { baseUrl })) { ... }\n */\n\nexport interface SubscribeOptions {\n readonly baseUrl?: string\n readonly headers?: Record<string, string>\n readonly signal?: AbortSignal\n readonly fetch?: typeof fetch\n}\n\nexport async function* subscribe(\n path: string,\n opts: SubscribeOptions = {},\n): AsyncGenerator<{ type: string; data: string; id?: string }> {\n const url = opts.baseUrl\n ? `${opts.baseUrl.replace(/\\/+$/, \"\")}${path.startsWith(\"/\") ? path : `/${path}`}`\n : path\n const fetchImpl = opts.fetch ?? globalThis.fetch.bind(globalThis)\n const res = await fetchImpl(url, {\n headers: { accept: \"text/event-stream\", ...opts.headers },\n ...(opts.signal ? { signal: opts.signal } : {}),\n })\n if (!res.body) throw new Error(\"sse: empty response body\")\n\n const decoder = new TextDecoder()\n const reader = res.body.getReader()\n let buf = \"\"\n try {\n while (true) {\n const { value, done } = await reader.read()\n if (done) break\n buf += decoder.decode(value, { stream: true })\n let idx = buf.indexOf(\"\\n\\n\")\n while (idx >= 0) {\n const raw = buf.slice(0, idx)\n buf = buf.slice(idx + 2)\n yield parseSseEvent(raw)\n idx = buf.indexOf(\"\\n\\n\")\n }\n }\n } finally {\n reader.releaseLock()\n }\n}\n\nfunction parseSseEvent(raw: string): { type: string; data: string; id?: string } {\n let type = \"message\"\n const dataLines: string[] = []\n let id: string | undefined\n for (const line of raw.split(\"\\n\")) {\n if (!line || line.startsWith(\":\")) continue\n const colon = line.indexOf(\":\")\n const k = colon < 0 ? line : line.slice(0, colon)\n const v = colon < 0 ? \"\" : line.slice(colon + 1).replace(/^ /, \"\")\n if (k === \"event\") type = v\n else if (k === \"data\") dataLines.push(v)\n else if (k === \"id\") id = v\n }\n return { type, data: dataLines.join(\"\\n\"), ...(id !== undefined && { id }) }\n}\n",
325
+ "sha256": "695238d750814c2400552b29fc993cff16ad4b7732ea89f302d9528164aefe2d"
326
+ },
327
+ {
328
+ "path": "client/tanstack-query.ts",
329
+ "contents": "/**\n * TanStack Query helpers — tiny wrappers that build queryKey/queryFn or\n * mutationFn pairs from a generated client function.\n *\n * These do not depend on @tanstack/react-query at compile time; the user\n * provides the types via the helper's generics.\n */\n\ntype Leaf<T = unknown> = (input?: {\n params?: Record<string, string>\n query?: Record<string, unknown>\n body?: unknown\n headers?: Record<string, string>\n signal?: AbortSignal\n}) => Promise<T>\n\nexport function queryOptions<T>(\n leaf: Leaf<T>,\n input?: Parameters<Leaf<T>>[0],\n): { queryKey: readonly unknown[]; queryFn: () => Promise<T> } {\n return {\n queryKey: [leaf.name || \"hyper\", input],\n queryFn: () => leaf(input),\n }\n}\n\nexport function mutationOptions<T, I extends Parameters<Leaf<T>>[0] | undefined>(\n leaf: Leaf<T>,\n): { mutationFn: (input: I) => Promise<T> } {\n return {\n mutationFn: (input: I) => leaf(input),\n }\n}\n",
330
+ "sha256": "53d024578c4d0fa88084230f63d5a732bafc7b48ff4f61c02b45005649c43e1d"
331
+ },
332
+ {
333
+ "path": "client/transport.ts",
334
+ "contents": "/**\n * Default fetch-backed transport.\n *\n * Supports JSON by default; a wire-format hook (MessagePack, etc.) can\n * be added by passing a custom `encode`/`decode` pair.\n */\n\nimport type { Transport, TransportRequest, TransportResponse } from \"./types.ts\"\n\nexport interface FetchTransportConfig {\n readonly baseUrl: string\n readonly headers?: Record<string, string>\n readonly fetch?: typeof fetch\n readonly encode?: (body: unknown) => BodyInit | undefined\n readonly decode?: (res: Response) => Promise<unknown>\n}\n\nconst defaultEncode = (body: unknown): BodyInit | undefined => {\n if (body === undefined || body === null) return undefined\n if (typeof body === \"string\" || body instanceof ArrayBuffer || body instanceof Uint8Array) {\n return body as BodyInit\n }\n return JSON.stringify(body)\n}\n\nconst defaultDecode = async (res: Response): Promise<unknown> => {\n const ct = res.headers.get(\"content-type\") ?? \"\"\n if (ct.includes(\"application/json\")) return res.json()\n if (ct.startsWith(\"text/\")) return res.text()\n if (res.status === 204) return undefined\n return res.arrayBuffer()\n}\n\nexport function fetchTransport(cfg: FetchTransportConfig): Transport {\n const encode = cfg.encode ?? defaultEncode\n const decode = cfg.decode ?? defaultDecode\n const fetchImpl = cfg.fetch ?? globalThis.fetch.bind(globalThis)\n return {\n async request(input: TransportRequest): Promise<TransportResponse> {\n const headers: Record<string, string> = {\n \"content-type\": \"application/json\",\n ...cfg.headers,\n ...input.headers,\n }\n const body = encode(input.body)\n const res = await fetchImpl(joinUrl(cfg.baseUrl, input.url), {\n method: input.method,\n headers,\n ...(body !== undefined && { body }),\n ...(input.signal ? { signal: input.signal } : {}),\n })\n const data = await decode(res)\n return { status: res.status, data, headers: res.headers }\n },\n }\n}\n\nfunction joinUrl(base: string, path: string): string {\n if (/^https?:/.test(path)) return path\n const cleanBase = base.replace(/\\/+$/, \"\")\n const cleanPath = path.startsWith(\"/\") ? path : `/${path}`\n return `${cleanBase}${cleanPath}`\n}\n",
335
+ "sha256": "ebc67de8a01e3a166e817f615c05d28f5f6a4d979e792b0c37b94f4e08323c65"
336
+ },
337
+ {
338
+ "path": "client/types.ts",
339
+ "contents": "/**\n * Client-side types.\n */\n\n/** Transport adapter — can swap fetch for something else (MessagePack, etc). */\nexport interface Transport {\n readonly request: (input: TransportRequest) => Promise<TransportResponse>\n}\n\nexport interface TransportRequest {\n readonly method: string\n readonly url: string\n readonly headers?: Record<string, string>\n readonly body?: unknown\n readonly signal?: AbortSignal\n}\n\nexport interface TransportResponse {\n readonly status: number\n readonly data: unknown\n readonly headers: Headers\n}\n\n/** Standard error shape emitted by Hyper's HTTP layer. */\nexport interface HyperRpcError {\n readonly status: number\n readonly code: string\n readonly message: string\n readonly why?: string\n readonly fix?: string\n readonly details?: unknown\n}\n\n/** Result union for `.throws()`/`.errors()`. */\nexport type Result<T, E = HyperRpcError> =\n | { readonly ok: true; readonly data: T }\n | { readonly ok: false; readonly error: E }\n",
340
+ "sha256": "b2f5bc51bddd81c066621f7cd24fea7a8a5d724542b05b868022dfa6a164db57"
341
+ }
342
+ ],
343
+ "subpaths": {}
344
+ },
345
+ "compress": {
346
+ "name": "compress",
347
+ "version": "0.1.0",
348
+ "description": "Content-negotiated gzip/brotli compression plugin for Hyper.",
349
+ "readme": "# @hyper/compress\n\nContent-negotiated gzip/brotli compression plugin for Hyper.\n\n## Install\n\n```bash\nbun add @hyper/compress\n```\n\n## Usage\n\n```ts\nimport { Hyper } from \"@hyper/core\"\nimport { compress } from \"@hyper/compress\"\n\nexport default new Hyper()\n .use(compress())\n .listen(3000)\n```\n\n## Docs\n\nSee the [main README](../../README.md) and [docs/](../../docs) for guides and integration recipes.\n\n## License\n\nMIT\n",
350
+ "registryDeps": [
351
+ "core"
352
+ ],
353
+ "peerDeps": {},
354
+ "optionalPeerDeps": {},
355
+ "files": [
356
+ {
357
+ "path": "compress/index.ts",
358
+ "contents": "/**\n * @hyper/compress — content-negotiated gzip/brotli compression.\n *\n * Wired as middleware (not a plugin) because we need to return a new\n * Response. Inspect the handler's response, negotiate the encoding,\n * compress in a single sync pass with Bun.gzipSync / node:zlib brotli.\n *\n * Defaults follow the \"safe-by-default\" philosophy:\n * - Only compress text/*, application/json, javascript, xml, svg, wasm.\n * No images/video/audio.\n * - Skip responses smaller than 1 KB (overhead dominates).\n * - Always set `Vary: Accept-Encoding` when we might compress.\n */\n\nimport { brotliCompressSync, gzipSync, constants as zlibConstants } from \"node:zlib\"\nimport { type Middleware, coerce } from \"@hyper/core\"\n\nexport interface CompressConfig {\n readonly threshold?: number\n readonly brotli?: boolean\n readonly types?: readonly string[]\n readonly level?: { readonly gzip?: number; readonly brotli?: number }\n}\n\nconst DEFAULT_TYPES: readonly string[] = Object.freeze([\n \"text/\",\n \"application/json\",\n \"application/ld+json\",\n \"application/javascript\",\n \"application/xml\",\n \"image/svg+xml\",\n \"application/wasm\",\n \"application/manifest+json\",\n])\n\nexport function compress(config: CompressConfig = {}): Middleware {\n const threshold = config.threshold ?? 1024\n const preferBrotli = config.brotli ?? true\n const types = [...DEFAULT_TYPES, ...(config.types ?? [])]\n const gzipLevel = config.level?.gzip ?? 6\n const brotliLevel = config.level?.brotli ?? 5\n\n return async ({ req, next }) => {\n const out = await next()\n const res = out instanceof Response ? out : coerce(out)\n if (res.headers.has(\"content-encoding\")) return res\n\n const ae = req.headers.get(\"accept-encoding\")?.toLowerCase() ?? \"\"\n const acceptsBr = preferBrotli && ae.includes(\"br\")\n const acceptsGz = ae.includes(\"gzip\")\n if (!acceptsBr && !acceptsGz) return res\n\n const ct = res.headers.get(\"content-type\")?.toLowerCase() ?? \"\"\n if (!ct || !types.some((t) => ct.startsWith(t))) return res\n\n const body = new Uint8Array(await res.arrayBuffer())\n // Always signal negotiation even if we don't compress.\n if (body.byteLength < threshold) {\n const hh = new Headers(res.headers)\n hh.append(\"vary\", \"Accept-Encoding\")\n return new Response(body, { status: res.status, statusText: res.statusText, headers: hh })\n }\n\n let encoded: Uint8Array\n let enc: string\n if (acceptsBr) {\n encoded = brotliCompressSync(body, {\n params: { [zlibConstants.BROTLI_PARAM_QUALITY]: brotliLevel },\n })\n enc = \"br\"\n } else {\n encoded = gzipSync(body, { level: gzipLevel })\n enc = \"gzip\"\n }\n\n const headers = new Headers(res.headers)\n headers.set(\"content-encoding\", enc)\n headers.set(\"content-length\", encoded.byteLength.toString())\n headers.append(\"vary\", \"Accept-Encoding\")\n return new Response(encoded, { status: res.status, statusText: res.statusText, headers })\n }\n}\n",
359
+ "sha256": "fe73d81049a3ab3e5ac2b61e82fdfd9eaf82b8ef2b4794c30d0e6ac20709cb90"
360
+ }
361
+ ],
362
+ "subpaths": {}
363
+ },
364
+ "core": {
365
+ "name": "core",
366
+ "version": "0.1.0",
367
+ "description": "Hyper core — Bun-first, AI-native API framework runtime.",
368
+ "readme": "# @hyper/core\n\nHyper core — the only hard dependency across the Hyper ecosystem. Zero runtime\ndependencies. Bun-native.\n\n## Install\n\n```bash\nbun add @hyper/core\n```\n\n## Usage\n\n```ts\nimport { Hyper, ok } from \"@hyper/core\"\n\nexport default new Hyper()\n .get(\"/\", () => ok({ hello: \"world\" }))\n .listen(3000)\n```\n\nCompose sub-apps, plugins, middleware, or raw `Route` values through a single\npolymorphic `.use()`:\n\n```ts\nimport { Hyper } from \"@hyper/core\"\nimport users from \"./routes/users.ts\"\n\nexport default new Hyper()\n .use(users) // honors sub-app's own prefix\n .use(\"/v1\", users) // or re-prefix explicitly\n .listen(3000)\n```\n\nCLI tools (`hyper openapi`, `hyper routes`, `hyper bench`, `hyper dev`) set\n`HYPER_SKIP_LISTEN=1` before importing, so the same file serves as both\nserver entrypoint and introspection manifest.\n\n## Docs\n\nSee the [main README](../../README.md) and [docs/](../../docs) for guides,\nsecurity defaults, and integration recipes.\n\n## License\n\nMIT\n",
369
+ "registryDeps": [],
370
+ "peerDeps": {},
371
+ "optionalPeerDeps": {},
372
+ "files": [
373
+ {
374
+ "path": "core/adapters/bun.ts",
375
+ "contents": "/**\n * Bun adapter helpers.\n *\n * Thin wrappers around `Bun.serve` that use the native `routes` map\n * emitted by the app + fall through to `fetch` for anything the map\n * cannot express (e.g. catch-alls, middleware-only paths).\n */\n\nimport type { HyperApp } from \"../types.ts\"\n\nexport interface ServeOptions {\n readonly port?: number\n readonly hostname?: string\n readonly idleTimeout?: number\n readonly tls?: import(\"bun\").TLSOptions\n readonly development?: boolean\n}\n\n/** Convenience wrapper. Callers may always prefer `Bun.serve` directly. */\nexport function serve(app: HyperApp, opts: ServeOptions = {}): ReturnType<typeof Bun.serve> {\n const serveOpts: Record<string, unknown> = {\n routes: app.routes,\n fetch: app.fetch,\n idleTimeout: opts.idleTimeout ?? 10,\n }\n if (opts.port !== undefined) serveOpts.port = opts.port\n if (opts.hostname !== undefined) serveOpts.hostname = opts.hostname\n if (opts.tls !== undefined) serveOpts.tls = opts.tls\n if (opts.development !== undefined) serveOpts.development = opts.development\n // Cast: Bun.serve's Options union is too narrow for our generic shape;\n // the runtime accepts every key we set.\n return Bun.serve(serveOpts as unknown as Parameters<typeof Bun.serve>[0])\n}\n",
376
+ "sha256": "4f8eb0e6ee1e02aab9cfff7389179c3fdb5b33e64cff1e7448ddfaefb3b9f1e7"
377
+ },
378
+ {
379
+ "path": "core/app.ts",
380
+ "contents": "/**\n * app() — builds a HyperApp from routes, groups, plugins, env, decorate.\n *\n * Boot order:\n * 1. Merge env layers → typed env (throws on bad input with why/fix).\n * 2. Run every decorate(env) → static ctx (singletons like db).\n * 3. Run plugin.build(app) / plugin.context(env) → merge into static ctx.\n * 4. Wire request pipeline: derive(ctx) per-request, plugins.before/after.\n *\n * The app is opaque for end users — everything goes through `.fetch`.\n */\n\nimport { type ContextBlueprint, applyDerive, resolveStaticContext } from \"./decorate.ts\"\nimport { parseEnv, withEnv } from \"./env.ts\"\nimport { HyperError, asHyperError } from \"./error.ts\"\nimport { GroupBuilder, fromPlainRouter } from \"./group.ts\"\nimport {\n type ClientManifest,\n type MCPManifest,\n type OpenAPIManifest,\n type OpenAPIManifestConfig,\n toClientManifest,\n toMCPManifest,\n toOpenAPI,\n} from \"./projection.ts\"\nimport { parseBodyAuto } from \"./request.ts\"\nimport { coerce, errorResponse } from \"./response.ts\"\nimport { Router } from \"./router.ts\"\nimport {\n DEFAULT_SECURITY,\n METHOD_OVERRIDE_HEADERS,\n METHOD_OVERRIDE_QUERY_KEYS,\n applyDefaultHeaders,\n} from \"./security.ts\"\nimport { SchemaValidationError, type StandardSchemaV1, parseStandard } from \"./standard-schema.ts\"\nimport type {\n AppConfig,\n AppContext,\n BunRoutes,\n HyperApp,\n HyperPlugin,\n InternalHandlerCtx,\n InvokeInput,\n InvokeResult,\n Route,\n SecurityDefaults,\n} from \"./types.ts\"\n\nexport function app(config: AppConfig = {}): HyperApp {\n const security: SecurityDefaults = { ...DEFAULT_SECURITY, ...config.security }\n\n // 1. Collect routes -------------------------------------------------------\n const allRoutes: Route[] = []\n if (config.routes) allRoutes.push(...config.routes)\n if (config.groups) {\n for (const g of config.groups) {\n // Accept either a GroupBuilder (has .build()) or a RouteGroup literal.\n const built: import(\"./types.ts\").RouteGroup =\n typeof (g as { build?: unknown }).build === \"function\"\n ? (g as import(\"./types.ts\").GroupConfigEntry).build()\n : (g as import(\"./types.ts\").RouteGroup)\n for (const r of built.routes) allRoutes.push(r)\n }\n }\n if (config.router) {\n const built = fromPlainRouter(config.router).build()\n for (const r of built.routes) allRoutes.push(r)\n }\n\n const router = new Router()\n for (const r of allRoutes) router.add(r)\n\n // 2. Env (lazy — the first request triggers boot). In a real app, boot\n // should be eager; but keeping it lazy makes the library usable in\n // edge/test environments where no process.env is available.\n //\n // Once boot resolves, we cache the state directly and bypass the\n // promise on subsequent requests — the hot path becomes sync.\n let bootedPromise: Promise<BootedState> | undefined\n let bootedCache: BootedState | undefined\n let bootedError: unknown\n const plugins: readonly HyperPlugin[] = config.plugins ?? []\n\n // Precompute per-hook plugin arrays so the request pipeline skips\n // any hook category that has zero installed callbacks — no empty\n // `for (const p of plugins)` loops on the hot path.\n const pluginsPreRoute: readonly HyperPlugin[] = plugins.filter((p) => p.request?.preRoute)\n const pluginsBefore: readonly HyperPlugin[] = plugins.filter((p) => p.request?.before)\n const pluginsAfter: readonly HyperPlugin[] = plugins.filter((p) => p.request?.after)\n const pluginsOnError: readonly HyperPlugin[] = plugins.filter((p) => p.request?.onError)\n\n // Whether the app declares any env schema. When false we skip the\n // AsyncLocalStorage (`withEnv`) wrapping per request — saves a Map\n // alloc + ALS snapshot per fetch on plaintext-style routes.\n const envRequired = config.env?.schema !== undefined\n\n const boot = async (): Promise<BootedState> => {\n const envLayers: StandardSchemaV1[] = []\n if (config.env?.schema) envLayers.push(config.env.schema as StandardSchemaV1)\n const env = await parseEnv(envLayers, config.env?.source)\n\n const blueprint: ContextBlueprint = {\n decorators: config.decorate ?? [],\n derives: config.derive ?? [],\n }\n const { ctx: staticCtx, dispose } = await resolveStaticContext(blueprint, env)\n\n // Plugin-installed context\n for (const p of plugins) {\n if (p.context) {\n const added = await p.context(env)\n Object.assign(staticCtx, added)\n }\n if (p.build) await p.build(instance)\n }\n\n return { env, staticCtx, dispose, blueprint }\n }\n\n const fetch = async (req: Request): Promise<Response> => {\n let booted = bootedCache\n if (!booted) {\n if (bootedError) {\n return finalize(errorResponse(asHyperError(bootedError)), isHttps(req), security)\n }\n if (!bootedPromise) {\n bootedPromise = boot().then(\n (s) => {\n bootedCache = s\n return s\n },\n (e) => {\n bootedError = e\n throw e\n },\n )\n }\n try {\n booted = await bootedPromise\n } catch (e) {\n return finalize(errorResponse(asHyperError(e)), isHttps(req), security)\n }\n }\n const hooks: HookPlugins = {\n preRoute: pluginsPreRoute,\n before: pluginsBefore,\n after: pluginsAfter,\n onError: pluginsOnError,\n }\n // Skip the AsyncLocalStorage wrap when no env schema is declared —\n // `useEnv()` is opt-in so the cost is unjustified on plaintext\n // throughput benchmarks that never call it.\n if (!envRequired) return handleRequest(req, booted, router, security, hooks)\n return withEnv(booted.env, () => handleRequest(req, booted!, router, security, hooks))\n }\n\n const routes = buildBunRoutes(allRoutes, fetch)\n\n const invoke = async (input: InvokeInput): Promise<InvokeResult> => {\n const rawPath = input.path.startsWith(\"/\") ? input.path : `/${input.path}`\n const resolvedPath = input.params\n ? rawPath.replace(/:([A-Za-z0-9_]+)/g, (_, k: string) => {\n const v = input.params?.[k]\n if (v === undefined) throw new Error(`invoke: missing path param :${k}`)\n return encodeURIComponent(v)\n })\n : rawPath\n const url = new URL(`http://local${resolvedPath}`)\n if (input.query) {\n for (const [k, v] of Object.entries(input.query)) {\n if (v !== undefined) url.searchParams.set(k, String(v))\n }\n }\n const init: RequestInit = {\n method: input.method,\n ...(input.headers ? { headers: input.headers } : {}),\n ...(input.body !== undefined && hasBody(input.method)\n ? {\n body: typeof input.body === \"string\" ? input.body : JSON.stringify(input.body),\n headers: {\n \"content-type\": \"application/json\",\n ...(input.headers ?? {}),\n },\n }\n : {}),\n }\n const req = new Request(url, init)\n const res = await fetch(req)\n const ct = res.headers.get(\"content-type\") ?? \"\"\n const data = ct.includes(\"application/json\") ? await res.json() : await res.text()\n return { status: res.status, data, headers: res.headers }\n }\n\n const toOpenAPIFn = (cfg: OpenAPIManifestConfig = {}): OpenAPIManifest =>\n toOpenAPI(allRoutes, cfg)\n const toMCPFn = (): MCPManifest => toMCPManifest(allRoutes)\n const toClientFn = (): ClientManifest => toClientManifest(allRoutes)\n\n const instance: HyperApp = {\n fetch,\n routes,\n routeList: Object.freeze([...allRoutes]),\n invoke,\n toOpenAPI: toOpenAPIFn,\n toMCPManifest: toMCPFn,\n toClientManifest: toClientFn,\n __config: config,\n test: (overrides = {}) => makeTestApp(config, overrides),\n }\n return instance\n}\n\nfunction makeTestApp(base: AppConfig, overrides: import(\"./types.ts\").TestOverrides): HyperApp {\n // Merge env: original source + overrides.env (overrides win).\n const env: AppConfig[\"env\"] | undefined = base.env\n ? {\n ...base.env,\n source: { ...(base.env.source ?? {}), ...(overrides.env ?? {}) },\n }\n : overrides.env !== undefined\n ? { source: { ...overrides.env } }\n : base.env\n\n const addDecorators = toArray(overrides.decorate)\n const decorate =\n addDecorators.length > 0 ? [...(base.decorate ?? []), ...addDecorators] : base.decorate\n\n const addDerives = toArray(overrides.derive)\n const derive = addDerives.length > 0 ? [...(base.derive ?? []), ...addDerives] : base.derive\n\n let plugins = base.plugins ?? []\n if (overrides.plugins) {\n const { skip = [], replace = {}, add = [] } = overrides.plugins\n plugins = plugins\n .filter((p) => !skip.includes(p.name))\n .map((p) => replace[p.name] ?? p)\n .concat(add)\n }\n\n return app({\n ...base,\n ...(env !== undefined && { env }),\n ...(decorate !== undefined && { decorate }),\n ...(derive !== undefined && { derive }),\n plugins,\n })\n}\n\nfunction toArray<T>(x: T | readonly T[] | undefined): readonly T[] {\n if (x === undefined) return []\n return Array.isArray(x) ? (x as readonly T[]) : [x as T]\n}\n\ninterface BootedState {\n readonly env: Record<string, unknown>\n readonly staticCtx: Record<string, unknown>\n readonly dispose: () => Promise<void>\n readonly blueprint: ContextBlueprint\n}\n\ninterface HookPlugins {\n readonly preRoute: readonly HyperPlugin[]\n readonly before: readonly HyperPlugin[]\n readonly after: readonly HyperPlugin[]\n readonly onError: readonly HyperPlugin[]\n}\n\nasync function handleRequest(\n req: Request,\n booted: BootedState,\n router: Router,\n security: SecurityDefaults,\n hooks: HookPlugins,\n): Promise<Response> {\n // Pathname is extracted via indexOf — we never allocate a URL on the\n // routing hot path. URL is only built on-demand if the handler reads\n // `ctx.url` (via a lazy prototype getter on the handler ctx).\n const rawUrl = req.url\n const pathname = extractPathname(rawUrl)\n const https = isHttps(req)\n\n // Method-override guard — refuse to reinterpret the verb from headers\n // or query string. CSRF attackers love these; Hyper never honors them.\n if (security.rejectMethodOverride) {\n const headers = req.headers\n for (let i = 0; i < METHOD_OVERRIDE_HEADERS.length; i++) {\n const h = METHOD_OVERRIDE_HEADERS[i]!\n if (headers.has(h)) return finalize(methodOverrideRejected(h), https, security)\n }\n for (let i = 0; i < METHOD_OVERRIDE_QUERY_KEYS.length; i++) {\n const q = METHOD_OVERRIDE_QUERY_KEYS[i]!\n if (urlHasQueryKey(rawUrl, q)) return finalize(methodOverrideRejected(q), https, security)\n }\n }\n\n // Plugin pre-route hooks may short-circuit (CORS preflight, etc.)\n // before routing, so OPTIONS on unregistered paths still works.\n if (hooks.preRoute.length > 0) {\n for (const p of hooks.preRoute) {\n const r = await p.request!.preRoute!({ req })\n if (r instanceof Response) return finalize(r, https, security)\n }\n }\n\n const match = router.find(req.method as \"GET\", pathname)\n if (!match) {\n return finalize(\n new Response(\n JSON.stringify({\n error: { status: 404, message: `No route for ${req.method} ${pathname}` },\n }),\n { status: 404, headers: { \"content-type\": \"application/json; charset=utf-8\" } },\n ),\n https,\n security,\n )\n }\n\n // Per-request ctx — skips the spread when there are no derivers.\n const ctx = (await applyDerive(booted.blueprint, booted.staticCtx, booted.env, req)) as AppContext\n\n try {\n if (hooks.before.length > 0) {\n for (const p of hooks.before) {\n await p.request!.before!({ req, ctx, route: match.route })\n }\n }\n const timeoutMs =\n (match.route.meta.timeoutMs as number | undefined) ?? security.requestTimeoutMs\n const res =\n timeoutMs > 0\n ? await withTimeout(runPipeline(match.route, match.params, req, ctx), timeoutMs)\n : await runPipeline(match.route, match.params, req, ctx)\n if (hooks.after.length > 0) {\n for (const p of hooks.after) {\n await p.request!.after!({ req, ctx, res, route: match.route })\n }\n }\n return finalize(res, https, security, match.route.meta.headers)\n } catch (e) {\n if (hooks.onError.length > 0) {\n for (const p of hooks.onError) {\n await p.request!.onError!({ req, ctx, error: e, route: match.route })\n }\n }\n const err = e instanceof SchemaValidationError ? schemaToHyperError(e) : asHyperError(e)\n return finalize(errorResponse(err), https, security)\n }\n}\n\nfunction schemaToHyperError(e: SchemaValidationError): HyperError {\n return new HyperError({\n status: 400,\n code: \"validation_failed\",\n message: \"Request failed validation.\",\n why: \"One or more inputs did not match the declared schema.\",\n fix: \"Check the `details` field for per-field issues and correct the payload.\",\n details: {\n issues: e.issues.map((i) => ({\n path: i.path?.map(String) ?? [],\n message: i.message,\n })),\n },\n })\n}\n\n/**\n * Shared prototype for the per-request handler ctx. Every field that\n * isn't strictly needed up-front is declared as a lazy getter —\n * `ctx.url`, `ctx.query`, `ctx.headers`, `ctx.responseHeaders`. When\n * the route declares a schema we set the parsed value as an own\n * property on the instance, which shadows the getter. The handler\n * pays the cost of allocating a URL / URLSearchParams / Headers only\n * when it actually touches them.\n *\n * Using a shared prototype (not a per-request defineProperty) means\n * every ictx shares one hidden class — JSC specializes it cleanly.\n */\ninterface LazyCtxState {\n req: Request\n _url?: URL\n _query?: unknown\n _headers?: unknown\n _rh?: Headers\n _cookies?: import(\"bun\").CookieMap\n}\n\n/**\n * Each lazy accessor has both a getter (materialize-on-first-read)\n * and a setter (cache override). The setter lets the pipeline write\n * parsed schema output through `ictx.query = parsed` without\n * tripping strict-mode's \"assign to readonly property\" error, while\n * also giving us the hidden-class benefit of a single shared layout.\n */\nconst ICTX_PROTO: PropertyDescriptorMap = {\n url: {\n get(this: LazyCtxState): URL {\n let u = this._url\n if (u === undefined) {\n u = new URL(this.req.url)\n this._url = u\n }\n return u\n },\n set(this: LazyCtxState, v: URL) {\n this._url = v\n },\n enumerable: true,\n configurable: true,\n },\n query: {\n get(this: LazyCtxState): unknown {\n let q = this._query\n if (q === undefined) {\n const out: Record<string, string> = {}\n const url = this.req.url\n const qi = url.indexOf(\"?\")\n if (qi >= 0) {\n const hash = url.indexOf(\"#\", qi)\n const end = hash < 0 ? url.length : hash\n const sp = new URLSearchParams(url.slice(qi + 1, end))\n sp.forEach((v, k) => {\n out[k] = v\n })\n }\n q = out\n this._query = q\n }\n return q\n },\n set(this: LazyCtxState, v: unknown) {\n this._query = v\n },\n enumerable: true,\n configurable: true,\n },\n headers: {\n get(this: LazyCtxState): unknown {\n let h = this._headers\n if (h === undefined) {\n const out: Record<string, string> = {}\n ;(this.req as Request).headers.forEach((v, k) => {\n out[k] = v\n })\n h = out\n this._headers = h\n }\n return h\n },\n set(this: LazyCtxState, v: unknown) {\n this._headers = v\n },\n enumerable: true,\n configurable: true,\n },\n responseHeaders: {\n get(this: LazyCtxState): Headers {\n let rh = this._rh\n if (rh === undefined) {\n rh = new Headers()\n this._rh = rh\n }\n return rh\n },\n set(this: LazyCtxState, v: Headers) {\n this._rh = v\n },\n enumerable: true,\n configurable: true,\n },\n cookies: {\n value(this: LazyCtxState): import(\"bun\").CookieMap {\n let c = this._cookies\n if (c === undefined) {\n c = new Bun.CookieMap(this.req.headers.get(\"cookie\") ?? \"\")\n this._cookies = c\n }\n return c\n },\n enumerable: true,\n writable: false,\n configurable: false,\n },\n}\n\n// All `ictx` objects share this prototype → one hidden class.\nconst ICTX_PROTOTYPE: object = Object.create(null, ICTX_PROTO)\n\nasync function runPipeline(\n route: Route,\n params: Record<string, string>,\n req: Request,\n ctx: AppContext,\n): Promise<Response> {\n const parsedParams = route.params ? await parseStandard(route.params, params) : params\n\n // The ictx object is laid out with a fixed shape so V8/JSC can\n // specialize it. Own-property writes for schema-declared inputs\n // shadow the lazy getters on the prototype.\n const ictx = Object.create(ICTX_PROTOTYPE) as InternalHandlerCtx & LazyCtxState\n ictx.req = req\n ;(ictx as unknown as { params: unknown }).params = parsedParams\n ;(ictx as unknown as { body: unknown }).body = undefined\n ;(ictx as unknown as { ctx: AppContext }).ctx = ctx\n\n if (route.query) {\n // Schema-declared query → allocate URLSearchParams once, extract\n // into a plain object, then run Standard Schema over it.\n const rawUrl = req.url\n const qi = rawUrl.indexOf(\"?\")\n const queryInput: Record<string, string> = {}\n if (qi >= 0) {\n const hash = rawUrl.indexOf(\"#\", qi)\n const end = hash < 0 ? rawUrl.length : hash\n const sp = new URLSearchParams(rawUrl.slice(qi + 1, end))\n sp.forEach((v, k) => {\n queryInput[k] = v\n })\n }\n const parsed = await parseStandard(route.query, queryInput)\n // Writes through the prototype's setter → caches as own state,\n // shadowing the lazy getter on subsequent reads.\n ;(ictx as unknown as { query: unknown }).query = parsed\n }\n\n if (hasBody(req.method)) {\n const raw = await parseBodyAuto(req)\n const parsedBody = route.body ? await parseStandard(route.body, raw) : raw\n ;(ictx as unknown as { body: unknown }).body = parsedBody\n }\n\n if (route.headers) {\n const headerInput: Record<string, string> = {}\n req.headers.forEach((v, k) => {\n headerInput[k] = v\n })\n const parsed = await parseStandard(route.headers, headerInput)\n ;(ictx as unknown as { headers: unknown }).headers = parsed\n }\n\n const result = await route.handler(ictx)\n return coerce(result)\n}\n\nfunction hasBody(method: string): boolean {\n return method !== \"GET\" && method !== \"HEAD\" && method !== \"OPTIONS\"\n}\n\nfunction finalize(\n res: Response,\n https: boolean,\n security: SecurityDefaults,\n routeOverrides?: Record<string, string>,\n): Response {\n if (!security.headers) return res\n const emitHsts =\n security.hstsEnv === false\n ? false\n : (process.env.NODE_ENV ?? \"development\") === security.hstsEnv\n\n // Fast path: response helpers pre-bake the secure defaults, so when\n // there are no route overrides AND we don't need HSTS, we can return\n // the response unchanged. Probe via `x-content-type-options` — this\n // is the sentinel that every Hyper helper emits.\n const needsHsts = https && emitHsts !== false\n if (\n !routeOverrides &&\n !needsHsts &&\n !res.headers.has(\"server\") &&\n res.headers.has(\"x-content-type-options\")\n ) {\n return res\n }\n\n return applyDefaultHeaders(res, {\n https,\n emitHsts,\n ...(routeOverrides ? { overrides: routeOverrides } : {}),\n })\n}\n\nfunction methodOverrideRejected(which: string): Response {\n return new Response(\n JSON.stringify({\n error: {\n status: 400,\n code: \"method_override_rejected\",\n message: `Hyper refuses to honor method override via '${which}'.`,\n why: \"Method overrides are a CSRF/verb-smuggling vector and are disabled by default.\",\n fix: \"Call the endpoint with the real HTTP verb. If you really need overrides, set `app({ security: { rejectMethodOverride: false } })`.\",\n },\n }),\n { status: 400, headers: { \"content-type\": \"application/json; charset=utf-8\" } },\n )\n}\n\n/**\n * Cheap https detection from `req.url` — avoids allocating a full URL\n * on the boot-error path. `req.url` is always absolute for Bun Request\n * objects produced by `Bun.serve`.\n */\nfunction isHttps(req: Request): boolean {\n const u = req.url\n return u.length > 5 && u.charCodeAt(4) === 115 /* 's' */ && u.startsWith(\"https:\")\n}\n\n/**\n * Extract pathname from a fully-qualified request URL without\n * constructing a `URL` object. Returns `/` for inputs without a path.\n * The single allocation is the final `slice()`.\n *\n * extractPathname(\"http://host:3000/foo/bar?x=1\") === \"/foo/bar\"\n */\nfunction extractPathname(url: string): string {\n const schemeEnd = url.indexOf(\"://\")\n if (schemeEnd < 0) return \"/\"\n const pathStart = url.indexOf(\"/\", schemeEnd + 3)\n if (pathStart < 0) return \"/\"\n const q = url.indexOf(\"?\", pathStart)\n const h = url.indexOf(\"#\", pathStart)\n const end = q < 0 ? h : h < 0 ? q : Math.min(q, h)\n return end < 0 ? url.slice(pathStart) : url.slice(pathStart, end)\n}\n\n/**\n * Direct string scan for a query parameter key. Avoids URL /\n * URLSearchParams construction on the method-override guard's hot path.\n */\nfunction urlHasQueryKey(url: string, key: string): boolean {\n const qStart = url.indexOf(\"?\")\n if (qStart < 0) return false\n const hash = url.indexOf(\"#\", qStart)\n const qEnd = hash < 0 ? url.length : hash\n const keyLen = key.length\n let i = qStart + 1\n while (i < qEnd) {\n const delim = url.indexOf(\"&\", i)\n const segEnd = delim < 0 || delim >= qEnd ? qEnd : delim\n // A key is a match when either \"key=\" starts at i, or the raw key\n // appears as a flag (no `=`) and runs to segEnd.\n if (\n i + keyLen <= segEnd &&\n url.startsWith(key, i) &&\n (i + keyLen === segEnd || url.charCodeAt(i + keyLen) === 61) /* '=' */\n ) {\n return true\n }\n if (delim < 0) return false\n i = delim + 1\n }\n return false\n}\n\nasync function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {\n let timer: ReturnType<typeof setTimeout> | undefined\n const timeout = new Promise<never>((_, reject) => {\n timer = setTimeout(() => {\n reject(\n new HyperError({\n status: 504,\n code: \"request_timeout\",\n message: `Handler exceeded ${ms}ms timeout.`,\n why: \"The handler did not produce a response in time.\",\n fix: \"Make the handler faster, raise `security.requestTimeoutMs`, or set `.meta({ timeoutMs })` per-route.\",\n }),\n )\n }, ms)\n if (typeof (timer as { unref?: () => void }).unref === \"function\") {\n ;(timer as { unref: () => void }).unref()\n }\n })\n try {\n return await Promise.race([promise, timeout])\n } finally {\n if (timer) clearTimeout(timer)\n }\n}\n\nfunction buildBunRoutes(\n routes: readonly Route[],\n fetch: (req: Request) => Promise<Response>,\n): BunRoutes {\n const map: BunRoutes = {}\n // Index routes by path so we can detect fully-static paths (every\n // method on that path is a `.staticResponse()`). Static paths let\n // Bun.serve's native router short-circuit without calling a fn.\n const byPath = new Map<string, Route[]>()\n for (const r of routes) {\n const list = byPath.get(r.path)\n if (list) list.push(r)\n else byPath.set(r.path, [r])\n }\n for (const [path, list] of byPath) {\n const allStatic = list.length > 0 && list.every((r) => r.kind === \"static\")\n if (allStatic && list.length === 1 && list[0]!.staticResponse) {\n // Single-method static response → native static route (Response)\n map[path] = list[0]!.staticResponse\n } else if (allStatic) {\n // Method-keyed static responses.\n const methodMap: Record<string, Response> = {}\n for (const r of list) {\n if (r.staticResponse) methodMap[r.method] = r.staticResponse\n }\n map[path] = methodMap\n } else {\n map[path] = (req: Request) => fetch(req)\n }\n }\n return map\n}\n",
381
+ "sha256": "febb44d5431ee0dfa984f85f2cd16453720ea22b0004456c91ced99114c260e4"
382
+ },
383
+ {
384
+ "path": "core/decorate.ts",
385
+ "contents": "/**\n * Context decoration — three-tier dependency injection.\n *\n * 1. `decorate(env => ({ db, redis }))` at app / group / route level\n * — static singletons constructed once at boot. Disposed in reverse\n * order on shutdown via `Symbol.asyncDispose`.\n * 2. `derive(fn)` — runs per-request; computes values from ctx/req\n * (e.g., `ctx.user` from a JWT claim).\n * 3. Plugin-installed context via `plugin.context`.\n *\n * Types flow via `declare module \"@hyper/core\" { interface AppContext { ... } }`.\n *\n * Recipe (cross-file typing):\n * // src/ctx.d.ts\n * import type { Db } from \"./db\"\n * declare module \"@hyper/core\" { interface AppContext { db: Db } }\n */\n\nimport type { AppContext } from \"./types.ts\"\n\nexport type DecorateFactory<Env = unknown, Added = unknown> = (env: Env) => Added | Promise<Added>\n\nexport type DeriveFactory<\n Env = unknown,\n CtxIn extends AppContext = AppContext,\n Added = unknown,\n> = (args: { ctx: CtxIn; env: Env; req: Request }) => Added | Promise<Added>\n\n/** Registry built at app() time; applied to each request's ctx. */\nexport interface ContextBlueprint<Env = unknown> {\n readonly decorators: readonly DecorateFactory<Env>[]\n readonly derives: readonly DeriveFactory<Env>[]\n}\n\n/**\n * Resolve all `decorate()` entries once at boot. Returns the merged\n * static context plus a disposer (async) that runs in reverse order.\n */\nexport async function resolveStaticContext<Env>(\n bp: ContextBlueprint<Env>,\n env: Env,\n): Promise<{ ctx: Record<string, unknown>; dispose: () => Promise<void> }> {\n const merged: Record<string, unknown> = {}\n const disposers: Array<() => Promise<void>> = []\n for (const f of bp.decorators) {\n const added = await f(env)\n if (added && typeof added === \"object\") {\n for (const [k, v] of Object.entries(added as Record<string, unknown>)) {\n merged[k] = v\n if (isAsyncDisposable(v)) {\n disposers.push(async () => {\n await (v as { [Symbol.asyncDispose]: () => PromiseLike<void> })[Symbol.asyncDispose]()\n })\n } else if (isDisposable(v)) {\n disposers.push(async () => {\n ;(v as { [Symbol.dispose]: () => void })[Symbol.dispose]()\n })\n }\n }\n }\n }\n return {\n ctx: merged,\n async dispose() {\n for (let i = disposers.length - 1; i >= 0; i--) {\n try {\n await disposers[i]?.()\n } catch (err) {\n console.error(\"hyper: disposer failed:\", err)\n }\n }\n },\n }\n}\n\n/**\n * Apply per-request `derive()` to an already-static-decorated ctx.\n *\n * Fast path: when there are zero derive functions we return the static\n * ctx as-is — avoiding a per-request shallow clone. Plugins/consumers\n * must treat the ctx as read-only (which the `AppContext` type already\n * implies via declaration-merged readonly surfaces).\n */\nexport async function applyDerive<Env>(\n bp: ContextBlueprint<Env>,\n staticCtx: Record<string, unknown>,\n env: Env,\n req: Request,\n): Promise<Record<string, unknown>> {\n if (bp.derives.length === 0) return staticCtx\n const ctx = { ...staticCtx }\n for (const f of bp.derives) {\n const added = await f({ ctx: ctx as AppContext, env, req })\n if (added && typeof added === \"object\") Object.assign(ctx, added)\n }\n return ctx\n}\n\nfunction isAsyncDisposable(v: unknown): boolean {\n return (\n typeof v === \"object\" &&\n v !== null &&\n typeof (v as Record<PropertyKey, unknown>)[Symbol.asyncDispose] === \"function\"\n )\n}\n\nfunction isDisposable(v: unknown): boolean {\n return (\n typeof v === \"object\" &&\n v !== null &&\n typeof (v as Record<PropertyKey, unknown>)[Symbol.dispose] === \"function\"\n )\n}\n",
386
+ "sha256": "0fae17db271ab3fe0748651d3060c76b085065a99e9b9c8aa8893721264b8cbd"
387
+ },
388
+ {
389
+ "path": "core/env.ts",
390
+ "contents": "/**\n * Environment configuration — layered & parsed once at boot.\n *\n * - `app({ env: schema })` for global, plus `.env(schema)` on group/route.\n * - Layers merge by intersection: the handler sees the union of all\n * declared fields with narrowed types.\n * - Parse errors throw at boot with a `why`/`fix` shape listing every\n * field that failed (agents fix all of them in one edit).\n * - Secret marking: paths matching the provided `secret` paths are\n * redacted by `@hyper/log` and never echoed to error responses.\n * - `useEnv()` via AsyncLocalStorage for deep code.\n */\n\nimport { AsyncLocalStorage } from \"node:async_hooks\"\nimport { HyperError } from \"./error.ts\"\nimport { SchemaValidationError, parseStandard } from \"./standard-schema.ts\"\nimport type { StandardSchemaV1 } from \"./standard-schema.ts\"\n\nexport interface EnvConfig {\n readonly schema?: StandardSchemaV1\n /** Dot-paths that should be treated as secret (auto-redacted). */\n readonly secrets?: readonly string[]\n /** Source env (defaults to process.env). */\n readonly source?: Record<string, string | undefined>\n}\n\nconst envStorage = new AsyncLocalStorage<Record<string, unknown>>()\n\n/** Retrieve the current request's env (runs inside an async scope). */\nexport function useEnv<T = Record<string, unknown>>(): T {\n const env = envStorage.getStore()\n if (!env) {\n throw new HyperError({\n status: 500,\n message: \"useEnv() called outside a request scope.\",\n why: \"AsyncLocalStorage has no env; the app is likely not initialized.\",\n fix: \"Ensure code paths using useEnv() run inside the app.fetch() pipeline.\",\n })\n }\n return env as T\n}\n\nexport function withEnv<T>(env: Record<string, unknown>, fn: () => T): T {\n return envStorage.run(env, fn)\n}\n\n/**\n * Parse a collection of layer-schemas against `source` once at boot.\n * Returns the merged typed env. Throws `EnvParseError` with a per-field\n * breakdown on failure.\n */\nexport async function parseEnv(\n layers: readonly StandardSchemaV1[],\n source: Record<string, string | undefined> = process.env,\n): Promise<Record<string, unknown>> {\n if (layers.length === 0) return { ...source }\n const out: Record<string, unknown> = {}\n const allIssues: Array<{ layer: number; path: string; message: string }> = []\n for (let i = 0; i < layers.length; i++) {\n const schema = layers[i]!\n try {\n const parsed = (await parseStandard(schema, source)) as Record<string, unknown>\n Object.assign(out, parsed)\n } catch (e) {\n if (e instanceof SchemaValidationError) {\n for (const issue of e.issues) {\n allIssues.push({\n layer: i,\n path: (issue.path ?? []).map(String).join(\".\") || \"(root)\",\n message: issue.message,\n })\n }\n } else {\n throw e\n }\n }\n }\n if (allIssues.length > 0) throw new EnvParseError(allIssues)\n return out\n}\n\nexport class EnvParseError extends Error {\n readonly issues: ReadonlyArray<{ layer: number; path: string; message: string }>\n constructor(issues: ReadonlyArray<{ layer: number; path: string; message: string }>) {\n const lines = issues.map((i) => ` layer ${i.layer} ${i.path}: ${i.message}`).join(\"\\n\")\n super(`Environment did not match declared schema:\\n${lines}`)\n this.name = \"EnvParseError\"\n this.issues = issues\n }\n}\n\n/**\n * Mark secret paths on an env object in-place for @hyper/log consumers.\n * A non-enumerable symbol keyed off the env carries the list.\n */\nexport const SECRET_PATHS: unique symbol = Symbol.for(\"@hyper/core/secret-paths\")\n\nexport function markSecrets<T extends object>(env: T, paths: readonly string[]): T {\n Object.defineProperty(env, SECRET_PATHS, {\n value: Object.freeze([...paths]),\n enumerable: false,\n })\n return env\n}\n\nexport function getSecretPaths(env: object): readonly string[] | undefined {\n const paths = (env as Record<PropertyKey, unknown>)[SECRET_PATHS]\n if (!paths) return undefined\n return paths as readonly string[]\n}\n\n/** Helper: wrap a Standard Schema field with a marker string. */\nexport function secret<T>(schema: T): T & { __hyperSecret: true } {\n return schema as T & { __hyperSecret: true }\n}\n",
391
+ "sha256": "51a24721cd91e60b83dbc64d63c61165c5752c4511c6e88949e6ff8c9647567c"
392
+ },
393
+ {
394
+ "path": "core/error.ts",
395
+ "contents": "/**\n * Structured errors.\n *\n * Hyper distinguishes thrown errors (unexpected) from returned errors\n * (contract-defined). `createError` produces the thrown shape with\n * `why`/`fix` fields that surface in logs, error responses, and the\n * MCP error payload — making failures actionable for both humans\n * and agents.\n */\n\nexport interface HyperErrorInit {\n /** HTTP status to project (defaults to 500). */\n status?: number\n /** Short machine code (e.g., \"email_exists\"). */\n code?: string\n /** Human message. */\n message: string\n /** Why this happened — explained to the caller (not internal details). */\n why?: string\n /** How to fix it — agent-actionable. */\n fix?: string\n /** Documentation or recovery links. */\n links?: readonly string[]\n /** Arbitrary structured detail (redacted in logs if matching secret paths). */\n details?: Record<string, unknown>\n /** Underlying cause (stripped from wire response; kept in logs). */\n cause?: unknown\n}\n\nexport class HyperError extends Error {\n readonly status: number\n readonly code?: string\n readonly why?: string\n readonly fix?: string\n readonly links?: readonly string[]\n readonly details?: Record<string, unknown>\n\n constructor(init: HyperErrorInit) {\n super(init.message, { cause: init.cause })\n this.name = \"HyperError\"\n this.status = init.status ?? 500\n if (init.code !== undefined) this.code = init.code\n if (init.why !== undefined) this.why = init.why\n if (init.fix !== undefined) this.fix = init.fix\n if (init.links !== undefined) this.links = init.links\n if (init.details !== undefined) this.details = init.details\n }\n\n /** Wire shape — safe to serialize to clients and agents. */\n toJSON(): Record<string, unknown> {\n const base: Record<string, unknown> = {\n error: {\n status: this.status,\n message: this.message,\n },\n }\n const err = base.error as Record<string, unknown>\n if (this.code) err.code = this.code\n if (this.why) err.why = this.why\n if (this.fix) err.fix = this.fix\n if (this.links) err.links = this.links\n if (this.details) err.details = this.details\n return base\n }\n}\n\n/** Factory — preferred API. */\nexport function createError(init: HyperErrorInit): HyperError {\n return new HyperError(init)\n}\n\n/** Project unknown errors into a HyperError at the boundary. */\nexport function asHyperError(e: unknown): HyperError {\n if (e instanceof HyperError) return e\n if (e instanceof Error) {\n return new HyperError({\n status: 500,\n message: e.message || \"Internal Server Error\",\n why: \"Handler threw an unhandled error.\",\n fix: \"Check server logs for the stack trace.\",\n cause: e,\n })\n }\n return new HyperError({\n status: 500,\n message: \"Internal Server Error\",\n why: \"Handler threw a non-Error value.\",\n cause: e,\n })\n}\n",
396
+ "sha256": "9885ff59ffa0a40c58604eecbf204d6d56185a9cb65080bb306f4552f291be3a"
397
+ },
398
+ {
399
+ "path": "core/example.ts",
400
+ "contents": "/**\n * runExamples(app) — walks every route's `meta.examples` and executes each\n * example against the in-process app.invoke() path. Used by `hyper test`\n * and directly inside consumer test files (see @hyper/testing).\n */\n\nimport type { HttpMethod, HyperApp, RouteExample } from \"./types.ts\"\n\nexport interface ExampleResult {\n readonly route: string\n readonly method: string\n readonly example: string\n readonly ok: boolean\n readonly status: number\n readonly expected?: number\n readonly actual?: unknown\n readonly error?: string\n}\n\nexport async function runExamples(app: HyperApp): Promise<readonly ExampleResult[]> {\n const out: ExampleResult[] = []\n for (const route of app.routeList) {\n const examples = route.meta.examples as readonly RouteExample[] | undefined\n if (!examples || examples.length === 0) continue\n for (const ex of examples) {\n const expected = ex.output?.status\n try {\n const result = await app.invoke({\n method: route.method as HttpMethod,\n path: route.path,\n ...(ex.input?.params && { params: ex.input.params as Record<string, string> }),\n ...(ex.input?.query && { query: ex.input.query }),\n ...(ex.input?.body !== undefined && { body: ex.input.body }),\n ...(ex.input?.headers && {\n headers: Object.fromEntries(\n Object.entries(ex.input.headers).map(([k, v]) => [k, String(v)]),\n ),\n }),\n })\n const statusOk = expected === undefined ? result.status < 400 : result.status === expected\n const bodyOk =\n ex.output?.body === undefined\n ? true\n : JSON.stringify(result.data) === JSON.stringify(ex.output.body)\n out.push({\n route: route.path,\n method: route.method,\n example: ex.name,\n ok: statusOk && bodyOk,\n status: result.status,\n ...(expected !== undefined && { expected }),\n ...(ex.output?.body !== undefined && { actual: result.data }),\n })\n } catch (e) {\n out.push({\n route: route.path,\n method: route.method,\n example: ex.name,\n ok: false,\n status: 0,\n error: (e as Error).message,\n })\n }\n }\n }\n return out\n}\n",
401
+ "sha256": "a2eb72f9af0d96bdeece06b35b2ee9eba3afede8c70e8d1b8cae1b46333ff3df"
402
+ },
403
+ {
404
+ "path": "core/file.ts",
405
+ "contents": "/**\n * file() — serve a file from disk via `Bun.file(path)`.\n *\n * Refuses `..` segments by default to prevent path traversal. Users\n * can opt in explicitly when serving from a safely-sandboxed path.\n */\n\nimport { HyperError } from \"./error.ts\"\n\nexport interface FileOptions {\n /** Only set this for a path you fully control and sanitize upstream. */\n readonly allowTraversal?: boolean\n /** Optional content-type override; otherwise Bun sniffs from extension. */\n readonly type?: string\n}\n\n/**\n * Return a `Bun.file(path)` response helper. The response layer\n * detects the BunFile shape and passes through via `sendfile`.\n */\nexport function file(path: string, opts: FileOptions = {}): import(\"bun\").BunFile {\n if (!opts.allowTraversal && hasTraversal(path)) {\n throw new HyperError({\n status: 400,\n code: \"path_traversal\",\n message: \"Refusing to serve a path containing '..' segments.\",\n why: \"Path traversal is a common attack; Hyper rejects it at the file helper.\",\n fix: \"Pass `allowTraversal: true` only when serving from a sandboxed prefix you control.\",\n })\n }\n return Bun.file(path, opts.type ? { type: opts.type } : undefined)\n}\n\nfunction hasTraversal(p: string): boolean {\n // Normalize forward+backslashes.\n const normalized = p.replace(/\\\\/g, \"/\")\n for (const seg of normalized.split(\"/\")) {\n if (seg === \"..\") return true\n }\n return false\n}\n",
406
+ "sha256": "d4bb32c62d9778df1651dc9ed3b9f1a3538c81ed14704302c1369ad822b3c810"
407
+ },
408
+ {
409
+ "path": "core/group.ts",
410
+ "contents": "/**\n * group(prefix) — full composition API.\n *\n * - `.use(middleware)` — prepended to each route's chain\n * - `.meta(obj)` — merged into each route's meta\n * - `.add(...routes)` — register routes (paths rewritten with prefix)\n * - `.merge(otherGroup)` — absorb another group's routes+middleware\n * - `.prefix(more)` — return a new group rooted deeper\n * - `.lazy(() => import(...))` — code-splitting; resolved on first match\n *\n * Plain-object router shape:\n * app({ router: { users: { create, get } } })\n * is equivalent to a group tree — the nested-object layout maps 1:1\n * into `api.users.create()` at the client.\n */\n\nimport { type Middleware, compileChain } from \"./middleware.ts\"\nimport type { Route, RouteGroup, RouteMeta } from \"./types.ts\"\n\nexport type LazyGroup = () => Promise<{ default: GroupBuilder } | GroupBuilder>\n\nexport class GroupBuilder {\n readonly #prefix: string\n readonly #routes: Route[] = []\n readonly #middleware: Middleware[] = []\n readonly #meta: RouteMeta = {}\n readonly #lazyLoaders: LazyGroup[] = []\n\n constructor(prefix = \"\") {\n this.#prefix = normalizePrefix(prefix)\n }\n\n add(...routes: Route[]): GroupBuilder {\n for (const r of routes) {\n this.#routes.push(this.#decorate(r))\n }\n return this\n }\n\n use(mw: Middleware): GroupBuilder {\n this.#middleware.push(mw)\n // Re-decorate: existing routes need the new middleware prepended.\n for (let i = 0; i < this.#routes.length; i++) {\n const r = this.#routes[i]!\n this.#routes[i] = {\n ...r,\n handler: wrapWithMiddleware(r.path, r.handler, [mw]),\n }\n }\n return this\n }\n\n meta(meta: RouteMeta): GroupBuilder {\n Object.assign(this.#meta, meta)\n for (let i = 0; i < this.#routes.length; i++) {\n const r = this.#routes[i]!\n this.#routes[i] = { ...r, meta: { ...meta, ...r.meta } }\n }\n return this\n }\n\n prefix(more: string): GroupBuilder {\n return new GroupBuilder(joinPath(this.#prefix, more))\n }\n\n merge(other: GroupBuilder): GroupBuilder {\n const built = other.build()\n // Merged routes already carry their full path; don't re-prefix.\n for (const r of built.routes) {\n const handler =\n this.#middleware.length > 0\n ? wrapWithMiddleware(r.path, r.handler, this.#middleware)\n : r.handler\n this.#routes.push({ ...r, meta: { ...this.#meta, ...r.meta }, handler })\n }\n return this\n }\n\n lazy(loader: LazyGroup): GroupBuilder {\n this.#lazyLoaders.push(loader)\n return this\n }\n\n /** Resolve lazy groups (called by app() at construction). */\n async resolve(): Promise<void> {\n for (const loader of this.#lazyLoaders) {\n const mod = await loader()\n const g = mod instanceof GroupBuilder ? mod : mod.default\n await g.resolve()\n this.merge(g)\n }\n }\n\n build(): RouteGroup {\n return { prefix: this.#prefix, routes: [...this.#routes] }\n }\n\n /**\n * Invoke a route as a function (integration tests, projections).\n * Walks the group's routes and dispatches to .callable() when present.\n */\n async call<T = unknown>(\n method: string,\n path: string,\n input: {\n params?: Record<string, unknown>\n query?: Record<string, unknown>\n body?: unknown\n headers?: Record<string, string>\n req?: Request\n } = {},\n ): Promise<T> {\n const routes = [...this.#routes]\n const full = joinPath(\"\", path)\n const match = routes.find((r) => r.method === method.toUpperCase() && r.path === full)\n if (!match) throw new Error(`group.call: no route ${method} ${full}`)\n // Prefer attached .callable() if present.\n const callable = (match as { callable?: (i: unknown) => Promise<unknown> }).callable\n if (callable) return callable(input) as Promise<T>\n // Fallback: run via internal handler.\n const req = input.req ?? new Request(`http://local${full}`, { method })\n const result = await match.handler({\n req,\n url: new URL(req.url),\n params: (input.params ?? {}) as Record<string, string>,\n query: new URLSearchParams((input.query as Record<string, string> | undefined) ?? {}),\n headers: new Headers((input.headers ?? {}) as Record<string, string>),\n body: input.body,\n ctx: {},\n cookies: () => new Bun.CookieMap(req.headers.get(\"cookie\") ?? \"\"),\n responseHeaders: new Headers(),\n })\n return result as T\n }\n\n #decorate(r: Route): Route {\n const path = joinPath(this.#prefix, r.path)\n const meta = { ...this.#meta, ...r.meta }\n const handler =\n this.#middleware.length > 0\n ? wrapWithMiddleware(path, r.handler, this.#middleware)\n : r.handler\n return { ...r, path, meta, handler }\n }\n}\n\nfunction wrapWithMiddleware(\n path: string,\n handler: Route[\"handler\"],\n mws: readonly Middleware[],\n): Route[\"handler\"] {\n const runner = compileChain(mws)\n return (ictx) =>\n runner(\n {\n ctx: ictx.ctx,\n input: { params: ictx.params, query: ictx.query, body: ictx.body, headers: ictx.headers },\n req: ictx.req,\n path,\n params: ictx.params,\n },\n () => handler(ictx),\n ) as ReturnType<Route[\"handler\"]>\n}\n\nexport function group(prefix = \"\"): GroupBuilder {\n return new GroupBuilder(prefix)\n}\n\n/** Create a lazy group placeholder. */\nexport function lazy(loader: LazyGroup): GroupBuilder {\n const g = new GroupBuilder()\n g.lazy(loader)\n return g\n}\n\nfunction normalizePrefix(p: string): string {\n if (p === \"\" || p === \"/\") return \"\"\n let out = p.startsWith(\"/\") ? p : `/${p}`\n if (out.endsWith(\"/\")) out = out.slice(0, -1)\n return out\n}\n\nfunction joinPath(prefix: string, rest: string): string {\n const r = rest.startsWith(\"/\") ? rest : `/${rest}`\n if (prefix === \"\") return r\n return `${prefix}${r}`\n}\n\n// ------------------------------------------------------------------\n// Plain-object router → GroupBuilder\n// ------------------------------------------------------------------\n\n/**\n * A plain-object router. Nested records of routes or\n * sub-routers become a group tree. Paths come from the route's own\n * `.path`; object keys provide the typed-client namespace only.\n */\nexport interface PlainRouter {\n [key: string]: Route | PlainRouter\n}\n\nexport function fromPlainRouter(router: PlainRouter, prefix = \"\"): GroupBuilder {\n const g = new GroupBuilder(prefix)\n walk(router, g)\n return g\n}\n\nfunction walk(router: PlainRouter, g: GroupBuilder): void {\n for (const v of Object.values(router)) {\n if (isRoute(v)) g.add(v)\n else walk(v, g)\n }\n}\n\nfunction isRoute(x: unknown): x is Route {\n return (\n typeof x === \"object\" &&\n x !== null &&\n typeof (x as { handler?: unknown }).handler === \"function\" &&\n typeof (x as { method?: unknown }).method === \"string\" &&\n typeof (x as { path?: unknown }).path === \"string\"\n )\n}\n",
411
+ "sha256": "2ecc2a8c85b38a5c1051e4338d7dc2db94a0f6ede3ca21f7e7f0d86a7560266c"
412
+ },
413
+ {
414
+ "path": "core/hash.ts",
415
+ "contents": "/**\n * Hashing helpers — always `Bun.hash.xxHash3` for cache keys, ETags,\n * idempotency keys. Never `crypto.createHash('md5')` or similar.\n *\n * Constant-time compare uses `node:crypto.timingSafeEqual` (Bun's\n * zero-cost polyfill; no userland substitute).\n */\n\nimport { timingSafeEqual as nodeTimingSafeEqual } from \"node:crypto\"\n\nexport function xxh3(input: string | ArrayBufferView | ArrayBufferLike): string {\n const buf = typeof input === \"string\" ? new TextEncoder().encode(input) : input\n // Bun.hash.xxHash3 accepts ArrayBuffer/TypedArray/string.\n return Bun.hash.xxHash3(buf as Parameters<typeof Bun.hash.xxHash3>[0]).toString(16)\n}\n\n/** Build an ETag for a response body. Strong by default. */\nexport function etag(body: string | ArrayBufferView | ArrayBufferLike): string {\n return `\"${xxh3(body)}\"`\n}\n\n/**\n * Constant-time string equality. Inputs must be the same length;\n * otherwise returns false without any comparison (short-circuit is\n * safe — leaking length is not a secret-material leak).\n */\nexport function timingSafeEqualStr(a: string, b: string): boolean {\n if (a.length !== b.length) return false\n return nodeTimingSafeEqual(Buffer.from(a), Buffer.from(b))\n}\n",
416
+ "sha256": "6aca4a7885e1846f66550601ba62755fd995d3045ab0e98534830b05d9586b15"
417
+ },
418
+ {
419
+ "path": "core/hyper.ts",
420
+ "contents": "/**\n * Hyper — the top-level chain API.\n *\n * A thin, ergonomic wrapper around `app({...})` + the `route.<verb>`\n * builder. Construct a server with `new Hyper()`, add routes via verb\n * shortcuts, compose sub-apps / plugins / middleware / namespaces via\n * the polymorphic `.use()`, and boot with `.listen()`.\n *\n * export default new Hyper()\n * .get(\"/\", () => \"Hello Hyper\")\n * .listen(3000)\n *\n * The chain class is additive — everything still lowers to the existing\n * `Route`/`HyperApp` primitives, so plugins, openapi projection,\n * testing, and CLI tooling keep working unchanged.\n */\n\nimport type { Server } from \"bun\"\nimport { app } from \"./app.ts\"\nimport { GroupBuilder, fromPlainRouter } from \"./group.ts\"\nimport { type ChainRunner, type Middleware, compileChain } from \"./middleware.ts\"\nimport { type RouteBuilder, route } from \"./route.ts\"\nimport type { HandlerCtx } from \"./route.ts\"\nimport type { StandardSchemaV1 } from \"./standard-schema.ts\"\nimport type {\n AppConfig,\n AppContext,\n BunRoutes,\n DecorateFactory,\n DeriveFactory,\n EnvConfigLike,\n HandlerReturn,\n HttpMethod,\n HyperApp,\n HyperPlugin,\n InternalHandlerCtx,\n InvokeInput,\n InvokeResult,\n PlainRouterConfig,\n Route,\n RouteGroup,\n RouteHandler,\n RouteMeta,\n SecurityDefaults,\n TestOverrides,\n} from \"./types.ts\"\n\n/** Version string for the banner line. Stays in sync with `@hyper/core`. */\nconst HYPER_VERSION = \"0.1.0\"\n\n/** Constructor-time options. */\nexport interface HyperOptions {\n /** Mount all routes added on this instance under this prefix. */\n readonly prefix?: string\n /** Security baseline overrides — merged with secure-by-default. */\n readonly security?: Partial<SecurityDefaults>\n /** Env schema + secrets + source. Parsed at boot. */\n readonly env?: EnvConfigLike\n /** Optional name — surfaces in banner, logs, and error messages. */\n readonly name?: string\n}\n\n/** Options for `.listen()`. */\nexport interface ListenOptions {\n readonly port?: number\n readonly hostname?: string\n /** Bun.serve idleTimeout (seconds). Default: 10. */\n readonly idleTimeout?: number\n /** Bun.serve development flag. Default: true when NODE_ENV !== \"production\". */\n readonly development?: boolean\n /** Print the startup banner. Default: true outside of production. */\n readonly banner?: boolean\n /** Wire SIGTERM/SIGINT → drain. Default: true. */\n readonly drain?: boolean\n}\n\n// ---------------------------------------------------------------------\n// Type helpers\n// ---------------------------------------------------------------------\n\n/**\n * Extract the `:param` tokens from a path literal as a `{ [name]: string }`\n * record. Mirrors the runtime grammar exactly:\n *\n * - Param names match `[A-Za-z_][A-Za-z0-9_]*`.\n * - Anything else (including `.`, `@`, `-`, slashes) is a literal that ends\n * the param name. So `\"/r/:name@:version.json\"` infers `{ name; version }`,\n * not `{ \"name@:version.json\"; ... }`.\n *\n * Falls back to an empty record when the path has no params — destructuring\n * `({ params })` stays valid on schema-less static paths.\n */\ntype _IdentChar =\n | \"_\"\n | \"a\"\n | \"b\"\n | \"c\"\n | \"d\"\n | \"e\"\n | \"f\"\n | \"g\"\n | \"h\"\n | \"i\"\n | \"j\"\n | \"k\"\n | \"l\"\n | \"m\"\n | \"n\"\n | \"o\"\n | \"p\"\n | \"q\"\n | \"r\"\n | \"s\"\n | \"t\"\n | \"u\"\n | \"v\"\n | \"w\"\n | \"x\"\n | \"y\"\n | \"z\"\n | \"A\"\n | \"B\"\n | \"C\"\n | \"D\"\n | \"E\"\n | \"F\"\n | \"G\"\n | \"H\"\n | \"I\"\n | \"J\"\n | \"K\"\n | \"L\"\n | \"M\"\n | \"N\"\n | \"O\"\n | \"P\"\n | \"Q\"\n | \"R\"\n | \"S\"\n | \"T\"\n | \"U\"\n | \"V\"\n | \"W\"\n | \"X\"\n | \"Y\"\n | \"Z\"\n | \"0\"\n | \"1\"\n | \"2\"\n | \"3\"\n | \"4\"\n | \"5\"\n | \"6\"\n | \"7\"\n | \"8\"\n | \"9\"\n\n/** Walk forward from a `:`, accumulating identifier chars into the name. */\ntype _ReadName<P extends string, Acc extends string = \"\"> = P extends `${infer C}${infer Rest}`\n ? C extends _IdentChar\n ? _ReadName<Rest, `${Acc}${C}`>\n : { name: Acc; rest: P }\n : { name: Acc; rest: \"\" }\n\n/**\n * Walk the path character-by-character, accumulating each `:name` we hit\n * into a union. We can't use `${string}:${infer After}` because TS\n * template-literal inference is greedy on the prefix and would skip over\n * intermediate params (e.g. dropping `name` from `:name@:version`).\n */\ntype _Walk<P extends string, Acc extends string = never> = P extends `:${infer After}`\n ? _ReadName<After> extends { name: infer N; rest: infer R }\n ? N extends \"\"\n ? // \":\" not followed by an identifier char — treat as literal, advance one char.\n After extends `${string}${infer Tail}`\n ? _Walk<Tail, Acc>\n : Acc\n : N extends string\n ? R extends string\n ? _Walk<R, Acc | N>\n : Acc | N\n : Acc\n : Acc\n : P extends `${infer _C}${infer Rest}`\n ? _Walk<Rest, Acc>\n : Acc\n\nexport type PathParams<P extends string> = [_Walk<P>] extends [never]\n ? Record<string, never>\n : { [K in _Walk<P>]: string }\n\n/**\n * Narrow the output of a `StandardSchemaV1` or fall back to `Fallback`\n * when the schema is absent (undefined in the options bag).\n */\nexport type InferSchema<S, Fallback> = S extends StandardSchemaV1<unknown, infer O> ? O : Fallback\n\n/** Per-route options accepted by the verb shortcuts. */\nexport interface RouteOpts<\n P extends StandardSchemaV1 | undefined = StandardSchemaV1 | undefined,\n Q extends StandardSchemaV1 | undefined = StandardSchemaV1 | undefined,\n B extends StandardSchemaV1 | undefined = StandardSchemaV1 | undefined,\n H extends StandardSchemaV1 | undefined = StandardSchemaV1 | undefined,\n> {\n readonly params?: P\n readonly query?: Q\n readonly body?: B\n readonly headers?: H\n readonly meta?: RouteMeta\n readonly use?: readonly Middleware[]\n /** Declared thrown-error shapes keyed by HTTP status. */\n readonly throws?: Record<number, StandardSchemaV1>\n /** Named error-code catalog. */\n readonly errors?: Record<string, StandardSchemaV1>\n}\n\n/** The typed handler signature for `new Hyper<Ctx>().<verb>(path, [opts,] handler)`. */\nexport type VerbHandler<\n Path extends string = string,\n Opts extends RouteOpts | undefined = undefined,\n Ctx extends AppContext = AppContext,\n> = (\n ctx: HandlerCtx<\n Opts extends { params: infer P } ? InferSchema<P, PathParams<Path>> : PathParams<Path>,\n Opts extends { query: infer Q } ? InferSchema<Q, unknown> : unknown,\n Opts extends { body: infer B } ? InferSchema<B, unknown> : unknown,\n Opts extends { headers: infer H } ? InferSchema<H, unknown> : unknown,\n Ctx\n >,\n) => HandlerReturn | Promise<HandlerReturn>\n\n/**\n * Polymorphic dispatch — `.use()` accepts any of these shapes.\n *\n * Order of discrimination (see {@link Hyper.use}):\n * 1. `Hyper` — sub-app composition (its own prefix honored).\n * 2. `GroupBuilder` — flatten to RouteGroup, apply parent prefix.\n * 3. `RouteGroup` — same as above.\n * 4. `Route` | `Route[]` — register directly.\n * 5. `HyperPlugin` — install (must have `name: string`).\n * 6. `Middleware` — `typeof fn === \"function\"`, appended to the stack.\n * 7. Plain object — walked as an ESM namespace / PlainRouter.\n */\nexport type UseArg =\n // biome-ignore lint/suspicious/noExplicitAny: variance hole for heterogeneous Hyper<Ctx>\n | Hyper<any>\n | GroupBuilder\n | RouteGroup\n | Route\n | readonly Route[]\n | HyperPlugin\n | Middleware\n | Record<string, unknown>\n\n/** Brand used for duck-typed recognition across package boundaries. */\nexport const HYPER_BUILDER_BRAND = \"__hyperBuilder__\" as const\n\n// Shared state for graceful-shutdown handler. One process-level\n// listener per signal no matter how many `Hyper` instances `.listen()`.\n// biome-ignore lint/suspicious/noExplicitAny: heterogeneous by design\nconst activeServers: Set<Hyper<any>> = new Set()\nlet drainInstalled = false\n\nfunction installDrainHandlersOnce(): void {\n if (drainInstalled) return\n drainInstalled = true\n const shutdown = (signal: string): void => {\n // Drain every live server; do not exit until all have resolved.\n const pending: Promise<void>[] = []\n for (const inst of activeServers) {\n const s = inst.server\n if (!s) continue\n pending.push(\n Promise.resolve(s.stop(false))\n .then(() => undefined)\n .catch(() => undefined),\n )\n }\n void Promise.all(pending).then(() => {\n // Respect the Unix convention: 128 + signal number.\n const code = signal === \"SIGTERM\" ? 128 + 15 : signal === \"SIGINT\" ? 128 + 2 : 0\n process.exit(code)\n })\n }\n process.on(\"SIGTERM\", () => shutdown(\"SIGTERM\"))\n process.on(\"SIGINT\", () => shutdown(\"SIGINT\"))\n}\n\n/**\n * The top-level chain class.\n *\n * Mutable by design — every method returns `this`. Once `.build()` or\n * `.listen()` has produced a `HyperApp`, subsequent mutations transparently\n * invalidate the cache and rebuild on next access.\n */\nexport class Hyper<Ctx extends AppContext = AppContext> {\n /** Duck-typed brand so CLI tooling can recognize a `Hyper` across package boundaries. */\n readonly __hyperBuilder__ = true\n\n readonly #prefix: string\n readonly #options: HyperOptions\n readonly #routes: Route[] = []\n readonly #middleware: Middleware[] = []\n readonly #decorators: DecorateFactory[] = []\n readonly #derives: DeriveFactory[] = []\n readonly #plugins: HyperPlugin[] = []\n #routerConfig?: PlainRouterConfig\n #envConfig?: EnvConfigLike\n #securityOverrides: Partial<SecurityDefaults> = {}\n #built: HyperApp | undefined\n #server: Server<unknown> | undefined\n\n constructor(opts: HyperOptions = {}) {\n this.#prefix = normalizePrefix(opts.prefix ?? \"\")\n this.#options = opts\n if (opts.security) this.#securityOverrides = { ...opts.security }\n if (opts.env !== undefined) this.#envConfig = opts.env\n }\n\n // -----------------------------------------------------------------\n // Introspection\n // -----------------------------------------------------------------\n\n /** The normalized prefix (e.g. \"/users\"). Empty string when unset. */\n get prefix(): string {\n return this.#prefix\n }\n\n /** The live `Bun.Server` — populated after `.listen()`. */\n get server(): Server<unknown> | undefined {\n return this.#server\n }\n\n /** Display name (for banners / diagnostics). */\n get name(): string {\n return this.#options.name ?? \"hyper\"\n }\n\n /** Raw route list — forwards to the built app. */\n get routeList(): readonly Route[] {\n return this.build().routeList\n }\n\n /** `Bun.serve({ routes })` compatible map — forwards to the built app. */\n get routes(): BunRoutes {\n return this.build().routes\n }\n\n /** fetch-compatible entry point for any Bun/edge/workers adapter. */\n get fetch(): (req: Request) => Promise<Response> {\n return this.build().fetch\n }\n\n // -----------------------------------------------------------------\n // Verb shortcuts\n // -----------------------------------------------------------------\n\n get<Path extends string>(path: Path, handler: VerbHandler<Path, undefined, Ctx>): this\n get<Path extends string, const O extends RouteOpts>(\n path: Path,\n opts: O,\n handler: VerbHandler<Path, O, Ctx>,\n ): this\n get(path: string, body: string): this\n get(path: string, a: unknown, b?: unknown): this {\n return this.#addRoute(\"GET\", path, a, b)\n }\n post<Path extends string>(path: Path, handler: VerbHandler<Path, undefined, Ctx>): this\n post<Path extends string, const O extends RouteOpts>(\n path: Path,\n opts: O,\n handler: VerbHandler<Path, O, Ctx>,\n ): this\n post(path: string, body: string): this\n post(path: string, a: unknown, b?: unknown): this {\n return this.#addRoute(\"POST\", path, a, b)\n }\n put<Path extends string>(path: Path, handler: VerbHandler<Path, undefined, Ctx>): this\n put<Path extends string, const O extends RouteOpts>(\n path: Path,\n opts: O,\n handler: VerbHandler<Path, O, Ctx>,\n ): this\n put(path: string, body: string): this\n put(path: string, a: unknown, b?: unknown): this {\n return this.#addRoute(\"PUT\", path, a, b)\n }\n patch<Path extends string>(path: Path, handler: VerbHandler<Path, undefined, Ctx>): this\n patch<Path extends string, const O extends RouteOpts>(\n path: Path,\n opts: O,\n handler: VerbHandler<Path, O, Ctx>,\n ): this\n patch(path: string, body: string): this\n patch(path: string, a: unknown, b?: unknown): this {\n return this.#addRoute(\"PATCH\", path, a, b)\n }\n delete<Path extends string>(path: Path, handler: VerbHandler<Path, undefined, Ctx>): this\n delete<Path extends string, const O extends RouteOpts>(\n path: Path,\n opts: O,\n handler: VerbHandler<Path, O, Ctx>,\n ): this\n delete(path: string, body: string): this\n delete(path: string, a: unknown, b?: unknown): this {\n return this.#addRoute(\"DELETE\", path, a, b)\n }\n head<Path extends string>(path: Path, handler: VerbHandler<Path, undefined, Ctx>): this\n head<Path extends string, const O extends RouteOpts>(\n path: Path,\n opts: O,\n handler: VerbHandler<Path, O, Ctx>,\n ): this\n head(path: string, body: string): this\n head(path: string, a: unknown, b?: unknown): this {\n return this.#addRoute(\"HEAD\", path, a, b)\n }\n options<Path extends string>(path: Path, handler: VerbHandler<Path, undefined, Ctx>): this\n options<Path extends string, const O extends RouteOpts>(\n path: Path,\n opts: O,\n handler: VerbHandler<Path, O, Ctx>,\n ): this\n options(path: string, body: string): this\n options(path: string, a: unknown, b?: unknown): this {\n return this.#addRoute(\"OPTIONS\", path, a, b)\n }\n\n // -----------------------------------------------------------------\n // Composition\n // -----------------------------------------------------------------\n\n /** Register a plain-object router. Nested keys become a group tree. */\n router(cfg: PlainRouterConfig): this {\n this.#invalidate()\n // Delegate to fromPlainRouter; the prefix flow mirrors sub-app use.\n const g = fromPlainRouter(cfg as never, \"\")\n for (const r of g.build().routes) {\n this.#routes.push(this.#prefixAndWrap(r, \"\"))\n }\n // Keep a reference so the raw router config is still available to\n // tooling that inspects `app.__config.router` (e.g. typed clients).\n this.#routerConfig = cfg\n return this\n }\n\n /** Polymorphic `.use()` — see {@link UseArg}. */\n // biome-ignore lint/suspicious/noExplicitAny: sub-app Ctx is opaque at this boundary\n use(prefix: string, sub: Hyper<any>): this\n use(arg: UseArg): this\n use(arg1: unknown, arg2?: unknown): this {\n this.#invalidate()\n\n // Two-arg form: (prefix, sub-app)\n if (typeof arg1 === \"string\") {\n if (!(arg2 instanceof Hyper)) {\n throw new Error(\"Hyper.use(prefix, sub): the second argument must be a Hyper instance.\")\n }\n return this.#useSubApp(arg2, normalizePrefix(arg1))\n }\n\n const arg = arg1\n if (arg instanceof Hyper) return this.#useSubApp(arg, \"\")\n if (arg instanceof GroupBuilder) return this.#useGroup(arg.build())\n if (isRouteGroup(arg)) return this.#useGroup(arg)\n if (isRoute(arg)) return this.#useRoutes([arg])\n if (Array.isArray(arg) && arg.every(isRoute)) return this.#useRoutes(arg as readonly Route[])\n if (isHyperPlugin(arg)) {\n this.#plugins.push(arg)\n return this\n }\n if (typeof arg === \"function\") {\n this.#middleware.push(arg as Middleware)\n return this\n }\n // ESM namespace / plain object → walk for Route-shaped values.\n if (typeof arg === \"object\" && arg !== null) {\n return this.#useNamespace(arg as Record<string, unknown>)\n }\n\n throw new Error(\n \"Hyper.use: unsupported argument. Accepts Hyper sub-app, GroupBuilder, RouteGroup, Route(s), HyperPlugin, Middleware, or ESM namespace.\",\n )\n }\n\n /** Mount a single plugin by name. Identical to `.use(plugin)`. */\n plugin(p: HyperPlugin): this {\n this.#invalidate()\n this.#plugins.push(p)\n return this\n }\n\n /**\n * Static context decoration (db, redis, caches) — constructed once at boot.\n * Returns a `Hyper` with the widened `Ctx` so downstream handlers see the\n * added shape without casting.\n */\n decorate<A extends object>(factory: (env?: unknown) => A | Promise<A>): Hyper<Ctx & Readonly<A>> {\n this.#invalidate()\n this.#decorators.push(factory as DecorateFactory)\n return this as unknown as Hyper<Ctx & Readonly<A>>\n }\n\n /**\n * Per-request context derivation. Runs once per request, after decorators\n * and before the handler. Returned fields merge into `ctx` and are visible\n * to the handler with full type inference.\n */\n derive<A extends object>(\n factory: (args: { ctx: Ctx; env?: unknown; req: Request }) => A | Promise<A>,\n ): Hyper<Ctx & Readonly<A>> {\n this.#invalidate()\n this.#derives.push(factory as unknown as DeriveFactory)\n return this as unknown as Hyper<Ctx & Readonly<A>>\n }\n\n /** Declare env schema. Parsed at boot; `parseEnv` throws on bad input. */\n env(cfg: EnvConfigLike): this {\n this.#invalidate()\n this.#envConfig = cfg\n return this\n }\n\n /** Partial overrides over the secure-by-default baseline. */\n security(overrides: Partial<SecurityDefaults>): this {\n this.#invalidate()\n this.#securityOverrides = { ...this.#securityOverrides, ...overrides }\n return this\n }\n\n // -----------------------------------------------------------------\n // Build / listen\n // -----------------------------------------------------------------\n\n /**\n * Construct (and memoize) the underlying `HyperApp`. Safe to call\n * many times; re-runs only when chain state has been mutated since\n * the last call.\n */\n build(): HyperApp {\n if (this.#built) return this.#built\n const config: AppConfig = {\n routes: this.#routes,\n plugins: this.#plugins,\n decorate: this.#decorators,\n derive: this.#derives,\n ...(this.#envConfig && { env: this.#envConfig }),\n security: this.#securityOverrides,\n ...(this.#routerConfig && { router: this.#routerConfig }),\n }\n this.#built = app(config)\n return this.#built\n }\n\n /**\n * Boot a real `Bun.serve` unless `process.env.HYPER_SKIP_LISTEN` is\n * set. Returns `this` so the chain can still be exported cleanly:\n *\n * export default new Hyper().get(\"/\", () => \"hi\").listen(3000)\n */\n listen(portOrOpts?: number | ListenOptions): this {\n const built = this.build()\n\n if (process.env.HYPER_SKIP_LISTEN) return this\n\n const opts: ListenOptions =\n typeof portOrOpts === \"number\" ? { port: portOrOpts } : (portOrOpts ?? {})\n const isProd = process.env.NODE_ENV === \"production\"\n const port = opts.port ?? Number(process.env.PORT ?? 3000)\n const hostname = opts.hostname ?? (isProd ? \"0.0.0.0\" : \"localhost\")\n const idleTimeout = opts.idleTimeout ?? 10\n const development = opts.development ?? !isProd\n const bannerOn = opts.banner ?? !isProd\n const drainOn = opts.drain !== false\n\n this.#server = Bun.serve({\n port,\n hostname,\n routes: built.routes,\n fetch: built.fetch,\n idleTimeout,\n development,\n })\n\n if (bannerOn) {\n const n = built.routeList.length\n const plural = n === 1 ? \"route\" : \"routes\"\n console.log(\n `${this.name} ${HYPER_VERSION} listening on http://${this.#server.hostname}:${this.#server.port} (${n} ${plural})`,\n )\n }\n\n if (drainOn) {\n activeServers.add(this)\n installDrainHandlersOnce()\n }\n\n return this\n }\n\n /**\n * Stop the live server. `drain` (default true) waits for in-flight\n * requests; pass `false` to immediately force-close.\n */\n async stop(drain = true): Promise<void> {\n const s = this.#server\n if (!s) return\n activeServers.delete(this)\n this.#server = undefined\n await s.stop(!drain)\n }\n\n // -----------------------------------------------------------------\n // HyperApp proxies\n // -----------------------------------------------------------------\n\n invoke(input: InvokeInput): Promise<InvokeResult> {\n return this.build().invoke(input)\n }\n toOpenAPI(cfg?: Parameters<HyperApp[\"toOpenAPI\"]>[0]): ReturnType<HyperApp[\"toOpenAPI\"]> {\n return this.build().toOpenAPI(cfg)\n }\n toMCPManifest(): ReturnType<HyperApp[\"toMCPManifest\"]> {\n return this.build().toMCPManifest()\n }\n toClientManifest(): ReturnType<HyperApp[\"toClientManifest\"]> {\n return this.build().toClientManifest()\n }\n test(overrides?: TestOverrides): HyperApp {\n return this.build().test(overrides)\n }\n\n // -----------------------------------------------------------------\n // Internal\n // -----------------------------------------------------------------\n\n #invalidate(): void {\n this.#built = undefined\n }\n\n #addRoute(\n method: HttpMethod,\n path: string,\n optsOrHandler: unknown,\n maybeHandler?: unknown,\n ): this {\n this.#invalidate()\n\n // String shortcut: `.get(\"/\", \"Hello\")` → .staticResponse(new Response(\"Hello\"))\n if (typeof optsOrHandler === \"string\" && maybeHandler === undefined) {\n const fullPath = joinPaths(this.#prefix, path)\n const r = this.#verbBuilder(method, fullPath).staticResponse(\n new Response(optsOrHandler),\n ) as Route\n this.#routes.push(r)\n return this\n }\n\n let opts: RouteOpts | undefined\n let handler: (ctx: HandlerCtx) => HandlerReturn | Promise<HandlerReturn>\n if (maybeHandler !== undefined) {\n opts = optsOrHandler as RouteOpts\n handler = maybeHandler as (ctx: HandlerCtx) => HandlerReturn | Promise<HandlerReturn>\n } else {\n handler = optsOrHandler as (ctx: HandlerCtx) => HandlerReturn | Promise<HandlerReturn>\n }\n\n const fullPath = joinPaths(this.#prefix, path)\n let builder: RouteBuilder = this.#verbBuilder(method, fullPath)\n\n if (opts) {\n if (opts.params) builder = builder.params(opts.params)\n if (opts.query) builder = builder.query(opts.query)\n if (opts.body) builder = builder.body(opts.body)\n if (opts.headers) builder = builder.headers(opts.headers)\n if (opts.meta) builder = builder.meta(opts.meta)\n if (opts.throws) builder = builder.throws(opts.throws)\n if (opts.errors) builder = builder.errors(opts.errors)\n if (opts.use) for (const mw of opts.use) builder = builder.use(mw)\n }\n\n // Instance-level middleware is applied AFTER per-route opts, so the\n // chain order is: instance-mw → route-opts-mw → handler. Same\n // intuition as Express: parent-scoped mw wraps inner.\n for (const mw of this.#middleware) builder = builder.use(mw)\n\n const r = builder.handle(handler)\n this.#routes.push(r)\n return this\n }\n\n #verbBuilder(method: HttpMethod, path: string): RouteBuilder {\n switch (method) {\n case \"GET\":\n return route.get(path)\n case \"POST\":\n return route.post(path)\n case \"PUT\":\n return route.put(path)\n case \"PATCH\":\n return route.patch(path)\n case \"DELETE\":\n return route.delete(path)\n case \"HEAD\":\n return route.head(path)\n case \"OPTIONS\":\n return route.options(path)\n }\n }\n\n // biome-ignore lint/suspicious/noExplicitAny: heterogeneous sub-app Ctx\n #useSubApp(sub: Hyper<any>, extraPrefix: string): this {\n // Use the sub-app's already-built route list — its own prefix\n // (from `new Hyper({ prefix })`) is already baked into each path.\n const built = sub.build()\n for (const r of built.routeList) {\n this.#routes.push(this.#prefixAndWrap(r, extraPrefix))\n }\n return this\n }\n\n #useGroup(g: RouteGroup): this {\n // A RouteGroup has its prefix already baked into each route's path.\n for (const r of g.routes) {\n this.#routes.push(this.#prefixAndWrap(r, \"\"))\n }\n return this\n }\n\n #useRoutes(routes: readonly Route[]): this {\n for (const r of routes) {\n this.#routes.push(this.#prefixAndWrap(r, \"\"))\n }\n return this\n }\n\n #useNamespace(ns: Record<string, unknown>): this {\n // Walk the namespace for Route-shaped values, applying this prefix\n // + current middleware stack to each. Nested objects recurse.\n for (const value of Object.values(ns)) {\n if (isRoute(value)) {\n this.#routes.push(this.#prefixAndWrap(value, \"\"))\n } else if (value && typeof value === \"object\" && !Array.isArray(value)) {\n // Guard against walking primitives or circular structures. A\n // typical ESM namespace only contains a single level of Routes.\n this.#useNamespace(value as Record<string, unknown>)\n }\n }\n return this\n }\n\n #prefixAndWrap(r: Route, extra: string): Route {\n const combined = joinPaths(joinPaths(this.#prefix, extra), r.path)\n if (combined === r.path && this.#middleware.length === 0) return r\n if (this.#middleware.length === 0) return { ...r, path: combined }\n const chain: ChainRunner = compileChain(this.#middleware.slice())\n const wrapped: RouteHandler = (ictx: InternalHandlerCtx) =>\n chain(\n {\n ctx: ictx.ctx,\n input: {\n params: ictx.params,\n query: ictx.query,\n body: ictx.body,\n headers: ictx.headers,\n },\n req: ictx.req,\n path: combined,\n params: ictx.params,\n },\n () => r.handler(ictx),\n ) as ReturnType<RouteHandler>\n return { ...r, path: combined, handler: wrapped }\n }\n}\n\n/**\n * `hyper(opts?)` — factory alias for `new Hyper(opts)`. Returns the\n * same class instance, so `instanceof Hyper` continues to work.\n */\nexport function hyper(opts?: HyperOptions): Hyper {\n return new Hyper(opts)\n}\n\n// ---------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------\n\n/**\n * Combine two path segments, matching Elysia's \"prefix = /users, path\n * = /\" → \"/users\" intuition. Whichever segment is empty is dropped; a\n * path of \"/\" at the tail collapses to the prefix.\n */\nexport function joinPaths(prefix: string, rest: string): string {\n const p = prefix === \"\" || prefix === \"/\" ? \"\" : prefix\n const r = rest === \"\" || rest === \"/\" ? \"\" : rest.startsWith(\"/\") ? rest : `/${rest}`\n if (p === \"\" && r === \"\") return \"/\"\n if (p === \"\") return r\n if (r === \"\") return p\n return `${p}${r}`\n}\n\nfunction normalizePrefix(p: string): string {\n if (p === \"\" || p === \"/\") return \"\"\n let out = p.startsWith(\"/\") ? p : `/${p}`\n if (out.endsWith(\"/\")) out = out.slice(0, -1)\n return out\n}\n\nfunction isRoute(x: unknown): x is Route {\n return (\n typeof x === \"object\" &&\n x !== null &&\n typeof (x as Route).method === \"string\" &&\n typeof (x as Route).path === \"string\" &&\n typeof (x as Route).handler === \"function\" &&\n typeof (x as Route).kind === \"string\"\n )\n}\n\nfunction isRouteGroup(x: unknown): x is RouteGroup {\n return (\n typeof x === \"object\" &&\n x !== null &&\n typeof (x as RouteGroup).prefix === \"string\" &&\n Array.isArray((x as RouteGroup).routes)\n )\n}\n\nfunction isHyperPlugin(x: unknown): x is HyperPlugin {\n if (typeof x !== \"object\" || x === null) return false\n if (typeof (x as HyperPlugin).name !== \"string\") return false\n // A plugin isn't a route (would collide on `.handler`/`.method`).\n if (isRoute(x)) return false\n return true\n}\n",
421
+ "sha256": "bdb9dd67d460432c1f7b4ad5dda0a9bfde6d035109cb9a985f613bd517d9c38b"
422
+ },
423
+ {
424
+ "path": "core/index.ts",
425
+ "contents": "/**\n * @hyper/core — public entry.\n */\n\nexport const VERSION: string = \"0.1.0\"\n\n// Core types\nexport type {\n AppConfig,\n AppContext,\n BunFileLike,\n BunRoutes,\n EnvConfigLike,\n ErrorRegistry,\n HandlerReturn,\n HttpMethod,\n HyperApp,\n HyperPlugin,\n Infer,\n InternalHandlerCtx,\n InvokeInput,\n InvokeResult,\n Route,\n RouteExample,\n RouteGroup,\n RouteHandler,\n RouteMeta,\n SecurityDefaults,\n} from \"./types.ts\"\n\n// Standard Schema\nexport type {\n StandardSchemaV1,\n StandardSchemaV1Issue,\n StandardSchemaV1Props,\n StandardSchemaV1Result,\n} from \"./standard-schema.ts\"\nexport { isStandardSchema, parseStandard, SchemaValidationError } from \"./standard-schema.ts\"\n\n// Builder\nexport { route, RouteBuilder } from \"./route.ts\"\nexport type { BuilderState, CallableRoute, HandlerCtx, InferIn } from \"./route.ts\"\n\n// Middleware\nexport { compileChain, onError, onFinish, onStart, onSuccess } from \"./middleware.ts\"\nexport type { ChainRunner } from \"./middleware.ts\"\nexport type { Middleware, MiddlewareArgs } from \"./middleware.ts\"\n\n// Composition\nexport { fromPlainRouter, group, GroupBuilder, lazy } from \"./group.ts\"\nexport type { LazyGroup, PlainRouter } from \"./group.ts\"\n\n// App\nexport { app } from \"./app.ts\"\n\n// Chain API — `new Hyper()` / `hyper()`\nexport { HYPER_BUILDER_BRAND, Hyper, hyper, joinPaths } from \"./hyper.ts\"\nexport type { HyperOptions, ListenOptions, RouteOpts, UseArg, VerbHandler } from \"./hyper.ts\"\n\n// Errors\nexport { asHyperError, createError, HyperError } from \"./error.ts\"\nexport type { HyperErrorInit } from \"./error.ts\"\n\n// Response helpers\nexport {\n accepted,\n badRequest,\n coerce,\n conflict,\n created,\n errorResponse,\n forbidden,\n html,\n jsonResponse,\n noContent,\n notFound,\n ok,\n redirect,\n sse,\n stream,\n text,\n tooManyRequests,\n unauthorized,\n unprocessable,\n} from \"./response.ts\"\nexport type { TypedResponse } from \"./response.ts\"\n\n// File\nexport { file } from \"./file.ts\"\nexport type { FileOptions } from \"./file.ts\"\n\n// Hash / timing-safe\nexport { etag, timingSafeEqualStr, xxh3 } from \"./hash.ts\"\n\n// Security\nexport {\n applyDefaultHeaders,\n assertNoProtoKeys,\n DEFAULT_BODY_LIMIT_BYTES,\n DEFAULT_RESPONSE_HEADERS,\n DEFAULT_SECURITY,\n FORBIDDEN_JSON_KEYS,\n PrototypePollutionError,\n SUPPRESSED_HEADERS,\n} from \"./security.ts\"\n\n// Request\nexport { parseBodyAuto, parseJsonBody, readTextBody } from \"./request.ts\"\n\n// Decorate / derive\nexport { applyDerive, resolveStaticContext } from \"./decorate.ts\"\nexport type { ContextBlueprint, DecorateFactory, DeriveFactory } from \"./decorate.ts\"\n\n// Env\nexport {\n EnvParseError,\n getSecretPaths,\n markSecrets,\n parseEnv,\n SECRET_PATHS,\n secret,\n useEnv,\n withEnv,\n} from \"./env.ts\"\nexport type { EnvConfig } from \"./env.ts\"\n\n// Type utilities\nexport type { InferRouterCtx, InferRouterInputs, InferRouterOutputs } from \"./infer.ts\"\n\n// Resource bundles + examples\nexport { resource } from \"./resource.ts\"\nexport type { ResourceHandlers, ResourceMethod, ResourceOptions } from \"./resource.ts\"\nexport { runExamples } from \"./example.ts\"\nexport type { ExampleResult } from \"./example.ts\"\n\n// Projection (OpenAPI / MCP / client manifests)\nexport {\n projectRoute,\n projectRoutes,\n toClientManifest,\n toMCPManifest,\n toOpenAPI,\n} from \"./projection.ts\"\nexport type {\n ClientManifest,\n MCPManifest,\n MCPTool,\n OpenAPIManifest,\n OpenAPIManifestConfig,\n ProjectedRoute,\n SchemaDescriptor,\n} from \"./projection.ts\"\n",
426
+ "sha256": "63e5a349effd109c74ab852807e6ba77ce676b40747ff70fff2680d0234a22b3"
427
+ },
428
+ {
429
+ "path": "core/infer.ts",
430
+ "contents": "/**\n * Type utilities for downstream consumers.\n *\n * These let `@hyper/client` and user code derive input/output/context\n * types from a router tree without reflection.\n *\n * Usage:\n * import { type InferRouterInputs } from \"@hyper/core\"\n * type Inputs = InferRouterInputs<typeof router>\n * type CreateUser = Inputs[\"users\"][\"create\"]\n */\n\nimport type { CallableRoute } from \"./route.ts\"\nimport type { AppContext, Route } from \"./types.ts\"\n\n/** Anything that looks like a plain-object router branch. */\nexport type RouterLike = { [key: string]: Route | RouterLike }\n\n/** Extract inputs from a router tree, preserving namespace structure. */\nexport type InferRouterInputs<R> = {\n [K in keyof R]: R[K] extends CallableRoute<infer _M, infer P, infer Q, infer B, infer H, infer _O>\n ? { params: P; query: Q; body: B; headers: H }\n : R[K] extends Route\n ? unknown\n : R[K] extends RouterLike\n ? InferRouterInputs<R[K]>\n : never\n}\n\n/** Extract outputs — the handler's resolved return type. */\nexport type InferRouterOutputs<R> = {\n [K in keyof R]: R[K] extends CallableRoute<\n infer _M,\n infer _P,\n infer _Q,\n infer _B,\n infer _H,\n infer O\n >\n ? Awaited<O>\n : R[K] extends Route\n ? unknown\n : R[K] extends RouterLike\n ? InferRouterOutputs<R[K]>\n : never\n}\n\n/** The shared context type used by every route in the tree. */\nexport type InferRouterCtx<_R> = AppContext\n",
431
+ "sha256": "f00baa5e87627474c9941607a7b2428ffbe48404b5f55e93d724ad4a9a3be07a"
432
+ },
433
+ {
434
+ "path": "core/middleware.ts",
435
+ "contents": "/**\n * Middleware — with input, output access, and mapInput.\n *\n * Signature:\n * ({ ctx, input, next, mapInput }) => Promise<Response>\n *\n * `next(transformedInput?)` runs the rest of the chain (or the handler)\n * and returns the output. Middleware can inspect / transform / short-\n * circuit the response.\n *\n * Lifecycle factories (`onStart`, `onSuccess`, `onError`, `onFinish`)\n * are sugar over this primitive. They always produce a regular\n * middleware under the hood — meta is sugar.\n */\n\nimport type { AppContext, HandlerReturn } from \"./types.ts\"\n\nexport interface MiddlewareArgs<C = AppContext, I = unknown> {\n readonly ctx: C\n /** Input resolved from `params`/`query`/`body`/`headers`. */\n readonly input: I\n /** The actual Request. */\n readonly req: Request\n /** Current route path (inc. matched params). */\n readonly path: string\n /** Invoke the rest of the chain + handler; optionally mapInput. */\n readonly next: (mapped?: I) => Promise<HandlerReturn> | HandlerReturn\n /** Matched params for mapping convenience. */\n readonly params: Record<string, string>\n}\n\nexport type Middleware<C = AppContext, I = unknown> = (\n args: MiddlewareArgs<C, I>,\n) => Promise<HandlerReturn> | HandlerReturn\n\n// Lifecycle factories --------------------------------------------------------\n\nexport function onStart<C = AppContext>(\n fn: (\n args: Pick<MiddlewareArgs<C, unknown>, \"ctx\" | \"input\" | \"req\" | \"path\" | \"params\">,\n ) => void | Promise<void>,\n): Middleware<C, unknown> {\n return async ({ ctx, input, next, req, path, params }) => {\n await fn({ ctx, input, req, path, params })\n return next()\n }\n}\n\nexport function onSuccess<C = AppContext>(\n fn: (args: {\n ctx: C\n output: HandlerReturn\n req: Request\n }) => void | Promise<void>,\n): Middleware<C, unknown> {\n return async ({ ctx, next, req }) => {\n const output = await next()\n await fn({ ctx, output, req })\n return output\n }\n}\n\nexport function onError<C = AppContext>(\n fn: (args: { ctx: C; error: unknown; req: Request }) => void | Promise<void>,\n): Middleware<C, unknown> {\n return async ({ ctx, next, req }) => {\n try {\n return await next()\n } catch (error) {\n await fn({ ctx, error, req })\n throw error\n }\n }\n}\n\nexport function onFinish<C = AppContext>(\n fn: (args: {\n ctx: C\n output?: HandlerReturn\n error?: unknown\n req: Request\n }) => void | Promise<void>,\n): Middleware<C, unknown> {\n return async ({ ctx, next, req }) => {\n try {\n const output = await next()\n await fn({ ctx, output, req })\n return output\n } catch (error) {\n await fn({ ctx, error, req })\n throw error\n }\n }\n}\n\n/**\n * Runner produced by `compileChain` — invokes the precompiled pipeline.\n *\n * `next` is captured per-request so each middleware's `next()` call is a\n * single function reference, not a closure rebuilt on every dispatch.\n */\nexport type ChainRunner = (\n args: Omit<MiddlewareArgs, \"next\">,\n base: () => Promise<HandlerReturn> | HandlerReturn,\n) => Promise<HandlerReturn> | HandlerReturn\n\n/**\n * Precompile a middleware chain into a single function, once, at\n * route-build time. Eliminates per-request closure allocations that a\n * naive composition would pay. Zero-middleware routes get the fast\n * path — the compiled runner delegates directly to `base`.\n */\nexport function compileChain(middleware: readonly Middleware[]): ChainRunner {\n if (middleware.length === 0) {\n return (_args, base) => base()\n }\n return (args, base) => {\n let i = 0\n const dispatch = (mapped?: unknown): Promise<HandlerReturn> | HandlerReturn => {\n if (i >= middleware.length) return base()\n const mw = middleware[i++]!\n const mwArgs: MiddlewareArgs = {\n ctx: args.ctx,\n input: mapped !== undefined ? mapped : args.input,\n req: args.req,\n path: args.path,\n params: args.params,\n next: dispatch,\n }\n return mw(mwArgs)\n }\n return dispatch()\n }\n}\n",
436
+ "sha256": "44b54fb5c7b53901b9ecc660c4bf6f3de305c90d9d0c819accdca177ed9ecfa7"
437
+ },
438
+ {
439
+ "path": "core/projection.ts",
440
+ "contents": "/**\n * Multi-protocol projection infrastructure.\n *\n * One route definition projects to many transports:\n * HTTP — always on (the route is HTTP-first).\n * typed RPC client — always on (shape is inferred from the route graph).\n * MCP tool — opt-in via `meta.mcp`.\n * server action — opt-in via `meta.action` or `.actionable()`.\n * websocket/SSE — opt-in via the handler return type.\n *\n * The functions in this file walk a route graph and produce serializable\n * manifests. The `app.invoke()` path is shared across protocols so\n * business logic runs exactly once.\n */\n\nimport type { Route, RouteMeta } from \"./types.ts\"\n\n/** Minimal serializable schema descriptor — the full converter lives in @hyper/openapi. */\nexport interface SchemaDescriptor {\n readonly kind: \"unknown\" | \"object\" | \"string\" | \"number\" | \"boolean\" | \"array\"\n readonly properties?: Record<string, SchemaDescriptor>\n}\n\n/** A raw route as projected into any manifest. */\nexport interface ProjectedRoute {\n readonly method: string\n readonly path: string\n readonly name?: string\n readonly tags: readonly string[]\n readonly deprecated?: boolean\n readonly version?: string\n readonly mcp?: RouteMeta[\"mcp\"]\n readonly action?: boolean\n readonly internal?: boolean\n readonly params?: SchemaDescriptor\n readonly query?: SchemaDescriptor\n readonly body?: SchemaDescriptor\n /** Thrown HTTP status codes declared via `.throws(status, schema)`. */\n readonly throws?: readonly number[]\n /** Named error codes declared via `.errors({ code: schema })`. */\n readonly errors?: readonly string[]\n}\n\nfunction descriptorOf(x: unknown): SchemaDescriptor | undefined {\n if (!x) return undefined\n return { kind: \"unknown\" }\n}\n\nexport function projectRoute(r: Route): ProjectedRoute {\n const meta = r.meta\n const params = descriptorOf(r.params)\n const query = descriptorOf(r.query)\n const body = descriptorOf(r.body)\n const deprecated = meta.deprecated ? true : undefined\n const throws = r.throws\n ? Object.keys(r.throws)\n .map((n) => Number(n))\n .filter((n) => Number.isFinite(n))\n : undefined\n const errors = r.errors ? Object.keys(r.errors) : undefined\n const base: ProjectedRoute = {\n method: r.method,\n path: r.path,\n tags: meta.tags ?? [],\n ...(meta.name !== undefined && { name: meta.name }),\n ...(meta.mcp !== undefined && { mcp: meta.mcp }),\n ...(meta.action !== undefined && { action: Boolean(meta.action) }),\n ...(meta.internal !== undefined && { internal: meta.internal }),\n ...(deprecated !== undefined && { deprecated }),\n ...(meta.version !== undefined && { version: meta.version }),\n ...(params && { params }),\n ...(query && { query }),\n ...(body && { body }),\n ...(throws && throws.length > 0 && { throws }),\n ...(errors && errors.length > 0 && { errors }),\n }\n return base\n}\n\nexport function projectRoutes(routes: readonly Route[]): readonly ProjectedRoute[] {\n return routes.filter((r) => !r.meta.internal).map(projectRoute)\n}\n\n/** Minimal OpenAPI 3.1 manifest. @hyper/openapi adds schema conversion later. */\nexport interface OpenAPIManifest {\n readonly openapi: \"3.1.0\"\n readonly info: { title: string; version: string; description?: string }\n readonly paths: Record<string, Record<string, OpenAPIOperation>>\n}\n\ninterface OpenAPIOperation {\n readonly operationId?: string\n readonly tags?: readonly string[]\n readonly deprecated?: boolean\n readonly parameters?: readonly OpenAPIParam[]\n readonly requestBody?: { readonly content: Record<string, unknown> }\n readonly responses: Record<string, { description: string }>\n}\n\ninterface OpenAPIParam {\n readonly name: string\n readonly in: \"path\" | \"query\" | \"header\"\n readonly required: boolean\n}\n\nexport interface OpenAPIManifestConfig {\n readonly title?: string\n readonly version?: string\n readonly description?: string\n}\n\nfunction openApiPath(path: string): string {\n // Convert Bun `:param` to OpenAPI `{param}`\n return path.replace(/:([A-Za-z0-9_]+)/g, \"{$1}\")\n}\n\nexport function toOpenAPI(\n routes: readonly Route[],\n cfg: OpenAPIManifestConfig = {},\n): OpenAPIManifest {\n const paths: Record<string, Record<string, OpenAPIOperation>> = {}\n for (const r of routes) {\n if (r.meta.internal) continue\n const p = openApiPath(r.path)\n const operation: OpenAPIOperation = {\n ...(r.meta.name !== undefined && { operationId: r.meta.name }),\n ...(r.meta.tags !== undefined && { tags: r.meta.tags }),\n ...(r.meta.deprecated && { deprecated: true }),\n ...(r.body !== undefined && {\n requestBody: {\n content: { \"application/json\": { schema: { $ref: \"#/components/schemas/Body\" } } },\n },\n }),\n responses: {\n \"200\": { description: \"success\" },\n },\n }\n if (!paths[p]) paths[p] = {}\n paths[p][r.method.toLowerCase()] = operation\n }\n return {\n openapi: \"3.1.0\",\n info: {\n title: cfg.title ?? \"Hyper API\",\n version: cfg.version ?? \"0.0.0\",\n ...(cfg.description !== undefined && { description: cfg.description }),\n },\n paths,\n }\n}\n\n/** MCP manifest (JSON-RPC shaped). @hyper/mcp produces the transport. */\nexport interface MCPManifest {\n readonly version: \"1.0\"\n readonly tools: readonly MCPTool[]\n}\n\nexport interface MCPTool {\n readonly name: string\n readonly description: string\n readonly method: string\n readonly path: string\n readonly inputSchema: {\n readonly type: \"object\"\n readonly properties: Record<string, unknown>\n }\n}\n\nexport function toMCPManifest(routes: readonly Route[]): MCPManifest {\n const tools: MCPTool[] = []\n for (const r of routes) {\n if (r.meta.internal) continue\n if (!r.meta.mcp) continue\n const cfg = r.meta.mcp as { description: string }\n tools.push({\n name: r.meta.name ?? `${r.method.toLowerCase()}_${r.path.replace(/[^a-z0-9]+/gi, \"_\")}`,\n description: cfg.description,\n method: r.method,\n path: r.path,\n inputSchema: {\n type: \"object\",\n properties: {\n ...(r.params ? { params: { type: \"object\" } } : {}),\n ...(r.query ? { query: { type: \"object\" } } : {}),\n ...(r.body ? { body: { type: \"object\" } } : {}),\n },\n },\n })\n }\n return { version: \"1.0\", tools }\n}\n\n/** Typed-client manifest — the serializable contract @hyper/client consumes. */\nexport interface ClientManifest {\n readonly version: \"1.0\"\n readonly routes: readonly ProjectedRoute[]\n}\n\nexport function toClientManifest(routes: readonly Route[]): ClientManifest {\n return { version: \"1.0\", routes: projectRoutes(routes) }\n}\n",
441
+ "sha256": "d4073186cd677ece4b6e5e266214e4594148d71f9e5a5058a235607c3829004a"
442
+ },
443
+ {
444
+ "path": "core/request.ts",
445
+ "contents": "/**\n * Request parsing — lazy, size-capped, prototype-safe.\n *\n * Body parsing only runs when a route declares a `.body(schema)` or a\n * middleware reads `ctx.body`. We count bytes against the body limit\n * before calling `JSON.parse` so oversized payloads fail fast with 413.\n */\n\nimport { HyperError } from \"./error.ts\"\nimport {\n DEFAULT_BODY_LIMIT_BYTES,\n FORBIDDEN_JSON_KEYS,\n PrototypePollutionError,\n assertNoProtoKeys,\n} from \"./security.ts\"\n\nfunction jsonSafeReviver(key: string, value: unknown): unknown {\n if (FORBIDDEN_JSON_KEYS.includes(key)) {\n throw new HyperError({\n status: 400,\n code: \"proto_pollution\",\n message: `Refusing body containing dangerous key \"${key}\".`,\n why: \"The body contains a prototype-pollution vector.\",\n fix: \"Remove the __proto__ / constructor / prototype key from the payload.\",\n cause: new PrototypePollutionError(key, []),\n })\n }\n return value\n}\n\nexport interface ReadBodyOptions {\n readonly maxBytes?: number\n}\n\n/**\n * Read a request body as text honoring the byte limit. We avoid\n * `req.text()` so we can bail fast before buffering more than allowed.\n */\nexport async function readTextBody(req: Request, opts: ReadBodyOptions = {}): Promise<string> {\n const max = opts.maxBytes ?? DEFAULT_BODY_LIMIT_BYTES\n if (!req.body) return \"\"\n const contentLength = req.headers.get(\"content-length\")\n if (contentLength !== null) {\n const declared = Number(contentLength)\n if (Number.isFinite(declared) && declared > max) {\n throw payloadTooLarge(declared, max)\n }\n }\n\n const reader = req.body.getReader()\n const decoder = new TextDecoder()\n let total = 0\n let out = \"\"\n try {\n while (true) {\n const { value, done } = await reader.read()\n if (done) break\n if (value) {\n total += value.byteLength\n if (total > max) throw payloadTooLarge(total, max)\n out += decoder.decode(value, { stream: true })\n }\n }\n out += decoder.decode()\n } finally {\n reader.releaseLock?.()\n }\n return out\n}\n\n/**\n * Parse request body as JSON with prototype-pollution guard and size cap.\n * Returns `undefined` for no-body requests.\n */\nexport async function parseJsonBody(req: Request, opts: ReadBodyOptions = {}): Promise<unknown> {\n const text = await readTextBody(req, opts)\n if (text === \"\") return undefined\n let parsed: unknown\n try {\n // Reviver catches __proto__ / constructor / prototype keys before\n // they can pollute the prototype chain. JSON.parse's own __proto__\n // behavior is to assign to [[Prototype]] directly — so a post-hoc\n // Object.keys check never sees it. We reject at read time instead.\n parsed = JSON.parse(text, jsonSafeReviver)\n } catch (e) {\n if (e instanceof HyperError) throw e\n throw new HyperError({\n status: 400,\n code: \"invalid_json\",\n message: \"Request body is not valid JSON.\",\n why: \"The body failed JSON.parse.\",\n fix: \"Send a JSON payload matching the declared schema.\",\n cause: e,\n })\n }\n assertNoProtoKeys(parsed)\n return parsed\n}\n\n/** Auto-detect the right body parser from content-type. */\nexport async function parseBodyAuto(req: Request, opts: ReadBodyOptions = {}): Promise<unknown> {\n const ct = (req.headers.get(\"content-type\") ?? \"\").toLowerCase()\n if (!ct || ct.startsWith(\"application/json\") || ct.startsWith(\"application/ld+json\")) {\n return parseJsonBody(req, opts)\n }\n if (ct.startsWith(\"text/\")) {\n return readTextBody(req, opts)\n }\n if (ct.startsWith(\"application/x-www-form-urlencoded\")) {\n const text = await readTextBody(req, opts)\n const out: Record<string, string> = {}\n const params = new URLSearchParams(text)\n params.forEach((v, k) => {\n if (k === \"__proto__\" || k === \"constructor\" || k === \"prototype\") return\n out[k] = v\n })\n return out\n }\n if (ct.startsWith(\"multipart/form-data\")) {\n // Buffer the form; Bun's Request.formData respects body stream.\n return req.formData()\n }\n // Binary passthrough (rare for handlers — they should opt in explicitly).\n return req.arrayBuffer()\n}\n\nfunction payloadTooLarge(got: number, max: number): HyperError {\n return new HyperError({\n status: 413,\n code: \"payload_too_large\",\n message: `Request body is ${got} bytes, limit is ${max}.`,\n why: \"Bodies exceed Hyper's configured maxBytes.\",\n fix: \"Increase the limit on the route via `.body(schema, { maxBytes })` or trim the payload.\",\n })\n}\n",
446
+ "sha256": "c4669691fb6589f65a1442518f26ca6695ce3160a0a038165456a63596b4e79a"
447
+ },
448
+ {
449
+ "path": "core/resource.ts",
450
+ "contents": "/**\n * route.resource() — emit a standard CRUD bundle for a collection.\n *\n * Example:\n * const users = resource(\"/users\", {\n * list: () => store.list(),\n * get: ({ params }) => store.get(params.id),\n * create: ({ body }) => store.create(body),\n * update: ({ params, body }) => store.update(params.id, body),\n * remove: ({ params }) => store.remove(params.id),\n * })\n *\n * Returns an array of routes ready to add to `app({ routes })`.\n */\n\nimport { type CallableRoute, route } from \"./route.ts\"\nimport type { HandlerReturn, HttpMethod } from \"./types.ts\"\n\ntype HandlerFn<P, B> = (args: {\n params: P\n body: B\n req: Request\n url: URL\n // biome-ignore lint/suspicious/noExplicitAny: ctx is user-augmented\n ctx: any\n}) => Promise<HandlerReturn> | HandlerReturn\n\nexport interface ResourceHandlers<T, U = Partial<T>> {\n readonly list?: HandlerFn<Record<string, string>, never>\n readonly get?: HandlerFn<{ id: string }, never>\n readonly create?: HandlerFn<Record<string, string>, T>\n readonly update?: HandlerFn<{ id: string }, U>\n readonly remove?: HandlerFn<{ id: string }, never>\n}\n\nexport interface ResourceOptions {\n /** Human-readable resource name (for metadata). */\n readonly name?: string\n /** Expose CRUD as MCP tools. */\n readonly mcp?: boolean\n}\n\nexport function resource<T, U = Partial<T>>(\n basePath: string,\n handlers: ResourceHandlers<T, U>,\n opts: ResourceOptions = {},\n): readonly CallableRoute[] {\n const name = opts.name ?? basePath.replace(/\\//g, \"\")\n const mcpMeta = (op: string, desc: string): { mcp: { description: string } } | object =>\n opts.mcp ? { mcp: { description: `${desc} (${name})` } } : {}\n const out: CallableRoute[] = []\n\n if (handlers.list) {\n out.push(\n route\n .get(basePath)\n .meta({ name: `${name}.list`, tags: [name], ...mcpMeta(\"list\", \"List\") })\n .handle((c) =>\n handlers.list!({\n params: c.params as Record<string, string>,\n body: undefined as never,\n req: c.req,\n url: c.url,\n ctx: c.ctx,\n }),\n ) as CallableRoute,\n )\n }\n if (handlers.get) {\n out.push(\n route\n .get(`${basePath}/:id`)\n .meta({ name: `${name}.get`, tags: [name], ...mcpMeta(\"get\", \"Get\") })\n .handle((c) =>\n handlers.get!({\n params: c.params as { id: string },\n body: undefined as never,\n req: c.req,\n url: c.url,\n ctx: c.ctx,\n }),\n ) as CallableRoute,\n )\n }\n if (handlers.create) {\n out.push(\n route\n .post(basePath)\n .meta({ name: `${name}.create`, tags: [name], ...mcpMeta(\"create\", \"Create\") })\n .handle((c) =>\n handlers.create!({\n params: c.params as Record<string, string>,\n body: c.body as T,\n req: c.req,\n url: c.url,\n ctx: c.ctx,\n }),\n ) as CallableRoute,\n )\n }\n if (handlers.update) {\n out.push(\n route\n .patch(`${basePath}/:id`)\n .meta({ name: `${name}.update`, tags: [name], ...mcpMeta(\"update\", \"Update\") })\n .handle((c) =>\n handlers.update!({\n params: c.params as { id: string },\n body: c.body as U,\n req: c.req,\n url: c.url,\n ctx: c.ctx,\n }),\n ) as CallableRoute,\n )\n }\n if (handlers.remove) {\n out.push(\n route\n .delete(`${basePath}/:id`)\n .meta({ name: `${name}.remove`, tags: [name], ...mcpMeta(\"remove\", \"Remove\") })\n .handle((c) =>\n handlers.remove!({\n params: c.params as { id: string },\n body: undefined as never,\n req: c.req,\n url: c.url,\n ctx: c.ctx,\n }),\n ) as CallableRoute,\n )\n }\n\n return out\n}\n\n/** Convenience mapping to avoid explicit HttpMethod unions in docs. */\nexport type ResourceMethod = Extract<HttpMethod, \"GET\" | \"POST\" | \"PATCH\" | \"DELETE\">\n",
451
+ "sha256": "4b4e4f5fd0412fc350fc8f200d075c840ae8deb4dfa7c552dcbd7864e0522c17"
452
+ },
453
+ {
454
+ "path": "core/response.ts",
455
+ "contents": "/**\n * Return helpers + response coercion.\n *\n * The philosophy: handlers return values. The framework converts those\n * into `Response` objects with correct status codes. Helpers make the\n * status explicit at the return site and keep TypeScript inference clean.\n *\n * Perf note: helpers pre-merge the secure-by-default response headers so\n * `finalize()` in app.ts can skip the Headers-clone + new-Response cost\n * on the common path (only paying for HSTS / route overrides when those\n * features are actually in play).\n */\n\nimport type { HyperError } from \"./error.ts\"\nimport type { BunFileLike, HandlerReturn } from \"./types.ts\"\n\n/**\n * Bake Hyper's secure-by-default headers directly into the Response\n * constructed by the helpers. The pipeline detects these via a single\n * `headers.has(\"x-content-type-options\")` probe and skips the Headers\n * clone when no other mutation is needed.\n */\nconst DEFAULT_SECURITY_HEADERS: Readonly<Record<string, string>> = Object.freeze({\n \"x-content-type-options\": \"nosniff\",\n \"x-frame-options\": \"DENY\",\n \"referrer-policy\": \"strict-origin-when-cross-origin\",\n \"cross-origin-opener-policy\": \"same-origin\",\n \"cross-origin-resource-policy\": \"same-origin\",\n \"permissions-policy\": \"camera=(), microphone=(), geolocation=(), interest-cohort=()\",\n})\n\nconst JSON_HEADERS_PREBAKED: Readonly<Record<string, string>> = Object.freeze({\n \"content-type\": \"application/json; charset=utf-8\",\n ...DEFAULT_SECURITY_HEADERS,\n})\n\nconst TEXT_HEADERS_PREBAKED: Readonly<Record<string, string>> = Object.freeze({\n \"content-type\": \"text/plain; charset=utf-8\",\n ...DEFAULT_SECURITY_HEADERS,\n})\n\nconst HTML_HEADERS_PREBAKED: Readonly<Record<string, string>> = Object.freeze({\n \"content-type\": \"text/html; charset=utf-8\",\n ...DEFAULT_SECURITY_HEADERS,\n})\n\n/** Branded helper result — carries status so TS can infer response types. */\nexport interface TypedResponse<S extends number, B> extends Response {\n readonly __hyper?: { status: S; body: B }\n}\n\n// --- 2xx ------------------------------------------------------------------\n\nexport function ok<B>(body: B, init?: ResponseInit): TypedResponse<200, B> {\n return jsonResponse(200, body, init) as TypedResponse<200, B>\n}\n\nexport function created<B>(body: B, init?: ResponseInit): TypedResponse<201, B> {\n return jsonResponse(201, body, init) as TypedResponse<201, B>\n}\n\nexport function accepted<B>(body: B, init?: ResponseInit): TypedResponse<202, B> {\n return jsonResponse(202, body, init) as TypedResponse<202, B>\n}\n\nexport function noContent(init?: ResponseInit): TypedResponse<204, null> {\n if (!init) {\n return new Response(null, {\n status: 204,\n headers: DEFAULT_SECURITY_HEADERS,\n }) as TypedResponse<204, null>\n }\n const headers = mergeHeaders(init.headers, DEFAULT_SECURITY_HEADERS)\n return new Response(null, { ...init, status: 204, headers }) as TypedResponse<204, null>\n}\n\n// --- 3xx ------------------------------------------------------------------\n\nexport function redirect(\n location: string,\n status: 301 | 302 | 303 | 307 | 308 = 302,\n): TypedResponse<301 | 302 | 303 | 307 | 308, null> {\n return new Response(null, {\n status,\n headers: { location },\n }) as TypedResponse<301 | 302 | 303 | 307 | 308, null>\n}\n\n// --- 4xx error helpers (returned, not thrown) ----------------------------\n\nexport function badRequest<B extends { code?: string } | undefined = undefined>(\n body?: B,\n init?: ResponseInit,\n): TypedResponse<400, B> {\n return jsonResponse(400, body ?? null, init) as unknown as TypedResponse<400, B>\n}\n\nexport function unauthorized<B extends { code?: string } | undefined = undefined>(\n body?: B,\n init?: ResponseInit,\n): TypedResponse<401, B> {\n return jsonResponse(401, body ?? null, init) as unknown as TypedResponse<401, B>\n}\n\nexport function forbidden<B extends { code?: string } | undefined = undefined>(\n body?: B,\n init?: ResponseInit,\n): TypedResponse<403, B> {\n return jsonResponse(403, body ?? null, init) as unknown as TypedResponse<403, B>\n}\n\nexport function notFound<B extends { code?: string } | undefined = undefined>(\n body?: B,\n init?: ResponseInit,\n): TypedResponse<404, B> {\n return jsonResponse(404, body ?? null, init) as unknown as TypedResponse<404, B>\n}\n\nexport function conflict<B extends { code?: string } | undefined = undefined>(\n body?: B,\n init?: ResponseInit,\n): TypedResponse<409, B> {\n return jsonResponse(409, body ?? null, init) as unknown as TypedResponse<409, B>\n}\n\nexport function unprocessable<B extends { code?: string } | undefined = undefined>(\n body?: B,\n init?: ResponseInit,\n): TypedResponse<422, B> {\n return jsonResponse(422, body ?? null, init) as unknown as TypedResponse<422, B>\n}\n\nexport function tooManyRequests<B extends { code?: string } | undefined = undefined>(\n body?: B,\n init?: ResponseInit,\n): TypedResponse<429, B> {\n return jsonResponse(429, body ?? null, init) as unknown as TypedResponse<429, B>\n}\n\n// --- Body helpers ---------------------------------------------------------\n\nexport function text(body: string, init?: ResponseInit): Response {\n if (!init) {\n return new Response(body, { status: 200, headers: TEXT_HEADERS_PREBAKED })\n }\n const headers = mergeHeaders(init.headers, TEXT_HEADERS_PREBAKED)\n return new Response(body, { ...init, status: init.status ?? 200, headers })\n}\n\nexport function html(body: string, init?: ResponseInit): Response {\n if (!init) {\n return new Response(body, { status: 200, headers: HTML_HEADERS_PREBAKED })\n }\n const headers = mergeHeaders(init.headers, HTML_HEADERS_PREBAKED)\n return new Response(body, { ...init, status: init.status ?? 200, headers })\n}\n\n/** Server-Sent Events response. Pass an AsyncIterable of {data, event?, id?}. */\nexport function sse(\n source: AsyncIterable<{ data: string; event?: string; id?: string }>,\n init?: ResponseInit,\n): Response {\n const stream = new ReadableStream<Uint8Array>({\n async start(controller) {\n const enc = new TextEncoder()\n try {\n for await (const ev of source) {\n let chunk = \"\"\n if (ev.id) chunk += `id: ${ev.id}\\n`\n if (ev.event) chunk += `event: ${ev.event}\\n`\n for (const line of ev.data.split(\"\\n\")) chunk += `data: ${line}\\n`\n chunk += \"\\n\"\n controller.enqueue(enc.encode(chunk))\n }\n } catch (err) {\n controller.error(err)\n return\n }\n controller.close()\n },\n })\n const headers = mergeHeaders(init?.headers, {\n \"content-type\": \"text/event-stream\",\n \"cache-control\": \"no-cache, no-transform\",\n \"x-accel-buffering\": \"no\",\n })\n return new Response(stream, { ...init, status: init?.status ?? 200, headers })\n}\n\n/** Generic streaming response (AsyncIterable of strings or bytes). */\nexport function stream(source: AsyncIterable<string | Uint8Array>, init?: ResponseInit): Response {\n const readable = new ReadableStream<Uint8Array>({\n async start(controller) {\n const enc = new TextEncoder()\n try {\n for await (const chunk of source) {\n controller.enqueue(typeof chunk === \"string\" ? enc.encode(chunk) : chunk)\n }\n } catch (err) {\n controller.error(err)\n return\n }\n controller.close()\n },\n })\n return new Response(readable, { ...init, status: init?.status ?? 200 })\n}\n\n// --- Internal -------------------------------------------------------------\n\nexport function jsonResponse(status: number, body: unknown, init?: ResponseInit): Response {\n const payload = body === null || body === undefined ? null : JSON.stringify(body)\n if (!init) {\n return new Response(payload, { status, headers: JSON_HEADERS_PREBAKED })\n }\n const headers = mergeHeaders(init.headers, JSON_HEADERS_PREBAKED)\n return new Response(payload, { ...init, status, headers })\n}\n\nfunction mergeHeaders(input: HeadersInit | undefined, defaults?: Record<string, string>): Headers {\n const h = new Headers(input)\n if (defaults) {\n for (const [k, v] of Object.entries(defaults)) {\n if (!h.has(k)) h.set(k, v)\n }\n }\n return h\n}\n\n/**\n * Coerce a handler return into a Response. Rules:\n * - `Response` → passthrough\n * - `Bun.file`-like (has `.stream`) → streamed passthrough with content-type\n * - `ReadableStream` → 200 with stream body\n * - `string` → 200 text/plain\n * - everything else → 200 JSON\n */\nexport function coerce(value: HandlerReturn): Response {\n if (value instanceof Response) return value\n if (value === undefined || value === null) return new Response(null, { status: 204 })\n if (isBunFile(value)) return bunFileToResponse(value)\n if (value instanceof ReadableStream) return new Response(value, { status: 200 })\n if (typeof value === \"string\") return text(value)\n if (typeof value === \"object\" && value !== null && Symbol.asyncIterator in (value as object)) {\n return stream(value as AsyncIterable<string | Uint8Array>)\n }\n return ok(value)\n}\n\nfunction isBunFile(v: unknown): v is BunFileLike {\n return (\n typeof v === \"object\" && v !== null && typeof (v as { stream?: unknown }).stream === \"function\"\n )\n}\n\nfunction bunFileToResponse(file: BunFileLike): Response {\n const headers = new Headers()\n if (file.type) headers.set(\"content-type\", file.type)\n if (typeof file.size === \"number\") headers.set(\"content-length\", String(file.size))\n return new Response(file.stream(), { status: 200, headers })\n}\n\n// Error response projection ------------------------------------------------\n\nexport function errorResponse(err: HyperError): Response {\n return jsonResponse(err.status, err.toJSON())\n}\n",
456
+ "sha256": "8e183f4aadb4139206e27e2105250896dcddad9d700fd6448a87943d2b2e32df"
457
+ },
458
+ {
459
+ "path": "core/route.ts",
460
+ "contents": "/**\n * Route builder — the fluent, immutable DX surface.\n *\n * Every chain step returns a new builder with an updated type state.\n * `.handle(fn)` closes the builder and produces a `Route` value.\n *\n * Surface:\n * route.<verb>(path)\n * .params(schema)\n * .query(schema)\n * .body(schema)\n * .headers(schema)\n * .meta({...})\n * .use(middleware) // output access + mapInput\n * .errors({ CODE: schema }) // named-code catalog\n * .throws({ 404: schema }) // declared thrown shapes\n * .handle(fn) // closes the builder into a Route\n *\n * Returned Route carries an attached `.callable(ctx, input)` for in-\n * process invocation (testing, Server Actions, projections).\n */\n\nimport type { ChainRunner, Middleware } from \"./middleware.ts\"\nimport { compileChain } from \"./middleware.ts\"\nimport type { StandardSchemaV1 } from \"./standard-schema.ts\"\nimport type {\n HandlerReturn,\n HttpMethod,\n InternalHandlerCtx,\n Route,\n RouteHandler,\n RouteMeta,\n} from \"./types.ts\"\n\nexport interface BuilderState {\n params?: StandardSchemaV1\n query?: StandardSchemaV1\n body?: StandardSchemaV1\n headers?: StandardSchemaV1\n meta: RouteMeta\n middleware: readonly Middleware[]\n errors?: Record<string, StandardSchemaV1>\n throws?: Record<number, StandardSchemaV1>\n}\n\nexport type InferIn<S> = S extends StandardSchemaV1<infer _I, infer O> ? O : unknown\n\nexport interface HandlerCtx<\n Params = unknown,\n Query = unknown,\n Body = unknown,\n HeadersT = unknown,\n Ctx extends import(\"./types.ts\").AppContext = import(\"./types.ts\").AppContext,\n> {\n readonly req: Request\n readonly url: URL\n readonly params: Params\n readonly query: Query\n readonly body: Body\n readonly headers: HeadersT\n readonly cookies: () => import(\"bun\").CookieMap\n /** Decorated app context — `ctx.log`, `ctx.db`, etc. */\n readonly ctx: Ctx\n}\n\n/** A Route that can also be invoked as a plain async function. */\nexport interface CallableRoute<\n M extends HttpMethod = HttpMethod,\n Params = unknown,\n Query = unknown,\n Body = unknown,\n HeadersT = unknown,\n Output = unknown,\n> extends Route<M> {\n readonly callable: (input: {\n params?: Params\n query?: Query\n body?: Body\n headers?: HeadersT\n req?: Request\n ctx?: import(\"./types.ts\").AppContext\n }) => Promise<Output>\n}\n\nexport class RouteBuilder<\n M extends HttpMethod = HttpMethod,\n Params = unknown,\n Query = unknown,\n Body = unknown,\n HeadersT = unknown,\n> {\n readonly #method: M\n readonly #path: string\n readonly #state: BuilderState\n\n constructor(method: M, path: string, state: BuilderState = { meta: {}, middleware: [] }) {\n this.#method = method\n this.#path = path\n this.#state = state\n }\n\n params<S extends StandardSchemaV1>(\n schema: S,\n ): RouteBuilder<M, InferIn<S>, Query, Body, HeadersT> {\n return new RouteBuilder(this.#method, this.#path, { ...this.#state, params: schema })\n }\n\n query<S extends StandardSchemaV1>(\n schema: S,\n ): RouteBuilder<M, Params, InferIn<S>, Body, HeadersT> {\n return new RouteBuilder(this.#method, this.#path, { ...this.#state, query: schema })\n }\n\n body<S extends StandardSchemaV1>(\n schema: S,\n ): RouteBuilder<M, Params, Query, InferIn<S>, HeadersT> {\n return new RouteBuilder(this.#method, this.#path, { ...this.#state, body: schema })\n }\n\n headers<S extends StandardSchemaV1>(schema: S): RouteBuilder<M, Params, Query, Body, InferIn<S>> {\n return new RouteBuilder(this.#method, this.#path, { ...this.#state, headers: schema })\n }\n\n meta(meta: RouteMeta): RouteBuilder<M, Params, Query, Body, HeadersT> {\n return new RouteBuilder(this.#method, this.#path, {\n ...this.#state,\n meta: { ...this.#state.meta, ...meta },\n })\n }\n\n /** Mark deprecated — emits Sunset header + OpenAPI + client JSDoc. */\n deprecated(\n opts: boolean | { reason?: string; sunset?: Date | string } = true,\n ): RouteBuilder<M, Params, Query, Body, HeadersT> {\n let value: RouteMeta[\"deprecated\"]\n if (typeof opts === \"boolean\") {\n value = opts\n } else {\n const out: { reason?: string; sunset?: string } = {}\n if (opts.reason !== undefined) out.reason = opts.reason\n if (opts.sunset !== undefined) {\n out.sunset = opts.sunset instanceof Date ? opts.sunset.toUTCString() : opts.sunset\n }\n value = out\n }\n const nextHeaders: Record<string, string> = { ...(this.#state.meta.headers ?? {}) }\n if (typeof value === \"object\" && value?.sunset) {\n nextHeaders.Sunset = value.sunset\n } else if (value === true) {\n nextHeaders.Deprecation = \"true\"\n }\n return new RouteBuilder(this.#method, this.#path, {\n ...this.#state,\n meta: { ...this.#state.meta, deprecated: value, headers: nextHeaders },\n })\n }\n\n /** Tag this route with a version string (header or URL prefix routed). */\n version(v: string): RouteBuilder<M, Params, Query, Body, HeadersT> {\n return new RouteBuilder(this.#method, this.#path, {\n ...this.#state,\n meta: { ...this.#state.meta, version: v },\n })\n }\n\n /** Add an example — surfaced in OpenAPI, MCP few-shot, client JSDoc, tests. */\n example(ex: import(\"./types.ts\").RouteExample): RouteBuilder<M, Params, Query, Body, HeadersT> {\n return new RouteBuilder(this.#method, this.#path, {\n ...this.#state,\n meta: {\n ...this.#state.meta,\n examples: [...(this.#state.meta.examples ?? []), ex],\n },\n })\n }\n\n /** Per-route timeout in milliseconds (overrides `security.requestTimeoutMs`). */\n timeout(ms: number): RouteBuilder<M, Params, Query, Body, HeadersT> {\n if (!Number.isFinite(ms) || ms < 0) {\n throw new Error(`route.timeout(${ms}): must be a non-negative finite number of milliseconds`)\n }\n return new RouteBuilder(this.#method, this.#path, {\n ...this.#state,\n meta: { ...this.#state.meta, timeoutMs: ms },\n })\n }\n\n /**\n * Short-circuit to a constant Response. Eligible for Bun.serve's static\n * routes path — no handler invocation, no middleware, near-zero latency.\n *\n * route.get(\"/health\").static(Response.json({ ok: true }))\n *\n * Use cases: health checks, robots.txt, static configuration, feature\n * flags served from a CDN origin, etc. If you need middleware (logging,\n * auth), fall back to `.handle(() => ...)`.\n */\n staticResponse(res: Response): Route<M> {\n const method = this.#method\n const path = this.#path\n const state = this.#state\n // The hot path is Bun.serve's native static routes, which consume\n // `staticResponse` directly and never invoke `handler`. The dev\n // router falls back to `res.clone()` — Bun's clone is O(body) for\n // string bodies and doesn't materialize a buffer.\n return {\n method,\n path,\n meta: state.meta,\n handler: () => res.clone(),\n kind: \"static\",\n staticResponse: res,\n }\n }\n\n /** Mark the route as a React Server Action. */\n actionable(): RouteBuilder<M, Params, Query, Body, HeadersT> {\n return new RouteBuilder(this.#method, this.#path, {\n ...this.#state,\n meta: { ...this.#state.meta, action: true },\n })\n }\n\n use(mw: Middleware): RouteBuilder<M, Params, Query, Body, HeadersT> {\n return new RouteBuilder(this.#method, this.#path, {\n ...this.#state,\n middleware: [...this.#state.middleware, mw],\n })\n }\n\n /** Declare thrown error shapes per HTTP status (declared-throws contract). */\n throws(map: Record<number, StandardSchemaV1>): RouteBuilder<M, Params, Query, Body, HeadersT> {\n return new RouteBuilder(this.#method, this.#path, {\n ...this.#state,\n throws: { ...(this.#state.throws ?? {}), ...map },\n })\n }\n\n /** Declare named-code error catalog. */\n errors(map: Record<string, StandardSchemaV1>): RouteBuilder<M, Params, Query, Body, HeadersT> {\n return new RouteBuilder(this.#method, this.#path, {\n ...this.#state,\n errors: { ...(this.#state.errors ?? {}), ...map },\n })\n }\n\n handle(\n fn: (ctx: HandlerCtx<Params, Query, Body, HeadersT>) => Promise<HandlerReturn> | HandlerReturn,\n ): CallableRoute<M, Params, Query, Body, HeadersT, HandlerReturn> {\n const state = this.#state\n const method = this.#method\n const path = this.#path\n\n // Precompile the middleware chain at build time — zero-middleware\n // routes get a direct-call fast path with no extra allocations.\n const chain: ChainRunner = compileChain(state.middleware)\n const hasMiddleware = state.middleware.length > 0\n\n const handler: RouteHandler = (ictx: InternalHandlerCtx) => {\n // ictx is shaped by the pipeline to exactly match HandlerCtx —\n // url / query / headers are either the parsed schema output\n // (set as own properties by runPipeline) or lazy prototype\n // getters that materialize on first access. We skip the typed-\n // copy allocation that the old implementation paid per request.\n const typed = ictx as unknown as HandlerCtx<Params, Query, Body, HeadersT>\n if (!hasMiddleware) return fn(typed)\n return chain(\n {\n ctx: ictx.ctx,\n input: {\n params: typed.params,\n query: typed.query,\n body: typed.body,\n headers: typed.headers,\n },\n req: ictx.req,\n path,\n params: ictx.params,\n },\n () => fn(typed),\n )\n }\n\n const middlewareTags: string[] = []\n for (const mw of state.middleware) {\n const tag = (mw as unknown as { __hyperTag?: string }).__hyperTag\n if (typeof tag === \"string\") middlewareTags.push(tag)\n }\n\n const r: CallableRoute<M, Params, Query, Body, HeadersT, HandlerReturn> = {\n method,\n path,\n ...(state.params !== undefined && { params: state.params }),\n ...(state.query !== undefined && { query: state.query }),\n ...(state.body !== undefined && { body: state.body }),\n ...(state.headers !== undefined && { headers: state.headers }),\n meta: state.meta,\n handler,\n ...(state.throws !== undefined && { throws: state.throws }),\n ...(state.errors !== undefined && { errors: state.errors }),\n ...(middlewareTags.length > 0 && { middlewareTags }),\n kind: \"fn\",\n callable: async (input) => {\n const req = input.req ?? new Request(`http://local${path}`, { method })\n return fn({\n req,\n url: new URL(req.url),\n params: (input.params ?? {}) as Params,\n query: (input.query ?? {}) as Query,\n body: (input.body ?? undefined) as Body,\n headers: (input.headers ?? {}) as HeadersT,\n cookies: () => new Bun.CookieMap(req.headers.get(\"cookie\") ?? \"\"),\n ctx: (input.ctx ?? {}) as import(\"./types.ts\").AppContext,\n })\n },\n }\n return r\n }\n}\n\nexport const route: {\n get: (path: string) => RouteBuilder<\"GET\">\n post: (path: string) => RouteBuilder<\"POST\">\n put: (path: string) => RouteBuilder<\"PUT\">\n patch: (path: string) => RouteBuilder<\"PATCH\">\n delete: (path: string) => RouteBuilder<\"DELETE\">\n head: (path: string) => RouteBuilder<\"HEAD\">\n options: (path: string) => RouteBuilder<\"OPTIONS\">\n lazy: <R extends Route>(loader: () => Promise<{ default: R } | R>) => Promise<R>\n} = {\n get: (path) => new RouteBuilder(\"GET\", path),\n post: (path) => new RouteBuilder(\"POST\", path),\n put: (path) => new RouteBuilder(\"PUT\", path),\n patch: (path) => new RouteBuilder(\"PATCH\", path),\n delete: (path) => new RouteBuilder(\"DELETE\", path),\n head: (path) => new RouteBuilder(\"HEAD\", path),\n options: (path) => new RouteBuilder(\"OPTIONS\", path),\n lazy: async <R extends Route>(loader: () => Promise<{ default: R } | R>): Promise<R> => {\n const mod = await loader()\n if (mod && typeof mod === \"object\" && \"default\" in mod) {\n return (mod as { default: R }).default\n }\n return mod as R\n },\n}\n",
461
+ "sha256": "aeede5c10b4bdf6d19bb342a110b776803b3a1bffa99f8fd63ad0f2a2c913b0a"
462
+ },
463
+ {
464
+ "path": "core/router.ts",
465
+ "contents": "/**\n * Dev router — zero-dep trie that mirrors `Bun.serve({ routes })`\n * semantics so dev and prod behave identically.\n *\n * Supported patterns:\n * - Static: \"/users\"\n * - Param: \"/users/:id\"\n * - Mixed-segment: \"/r/:slug.json\", \"/r/:name@:version.json\", \"/posts/:y-:m-:d\", \"/v:version/users\"\n * - Wildcard: \"/api/*\"\n *\n * A \"mixed\" segment is one with literal characters around or between params.\n * It compiles to a regex once at route-add time; the runtime walker uses\n * `pattern.exec(segment)` only for those nodes. Pure-segment params keep the\n * zero-allocation static fast path unchanged.\n *\n * Multiple mixed patterns may share a parent node — they're tried in\n * descending order of specificity (more literal characters wins), so\n * `/r/:name@:version.json` is preferred over `/r/:name.json` when both\n * match. Within the same specificity, registration order wins.\n *\n * Param names are `[A-Za-z_][A-Za-z0-9_]*`. Anything else in the segment is a\n * literal byte that must match the request path verbatim.\n *\n * Method-keyed dispatch is handled at the Route level (one compiled handler\n * per verb); the router only matches paths.\n */\n\nimport type { HttpMethod, Route } from \"./types.ts\"\n\nexport interface MatchResult {\n readonly route: Route\n readonly params: Record<string, string>\n}\n\ninterface PureParam {\n readonly name: string\n readonly node: Node\n}\n\ninterface MixedParam {\n /** Original pattern segment, used for diagnostics. */\n readonly pattern: string\n /** Compiled regex anchored to the full segment. */\n readonly regex: RegExp\n /** Capture-group names in order. */\n readonly names: readonly string[]\n /** Count of literal characters — higher wins ties at match time. */\n readonly literalChars: number\n readonly node: Node\n}\n\ninterface Node {\n /** Static children: \"users\" -> Node */\n statics: Map<string, Node>\n /** Single pure-segment param child (`:id`). */\n pureParam?: PureParam\n /**\n * Mixed-segment children (`:slug.json`, `:a@:b`, ...). Multiple may\n * coexist at the same depth — tried in descending specificity order at\n * match time.\n */\n mixedParams?: MixedParam[]\n /** Wildcard child: \"*\" */\n wildcard?: Node\n /** Routes terminating at this node, keyed by method. */\n handlers?: Partial<Record<HttpMethod, Route>>\n}\n\nexport class Router {\n readonly #root: Node = newNode()\n\n add(route: Route): void {\n const segments = splitPath(route.path)\n let cur = this.#root\n for (const seg of segments) {\n if (seg === \"*\" || seg.startsWith(\"*\")) {\n if (!cur.wildcard) cur.wildcard = newNode()\n cur = cur.wildcard\n break\n }\n const parsed = parsePatternSegment(seg)\n if (parsed === \"static\") {\n let child = cur.statics.get(seg)\n if (!child) {\n child = newNode()\n cur.statics.set(seg, child)\n }\n cur = child\n continue\n }\n if (parsed.kind === \"pure\") {\n if (!cur.pureParam) {\n cur.pureParam = { name: parsed.name, node: newNode() }\n } else if (cur.pureParam.name !== parsed.name) {\n throw new Error(\n `Route conflict: ${route.path} has param :${parsed.name} but trie already uses :${cur.pureParam.name}`,\n )\n }\n cur = cur.pureParam.node\n continue\n }\n // Mixed pattern — multiple may coexist at the same depth.\n if (!cur.mixedParams) cur.mixedParams = []\n const existing = cur.mixedParams.find((m) => m.pattern === seg)\n if (existing) {\n cur = existing.node\n continue\n }\n const slot: MixedParam = {\n pattern: seg,\n regex: parsed.regex,\n names: parsed.names,\n literalChars: parsed.literalChars,\n node: newNode(),\n }\n cur.mixedParams.push(slot)\n // Most specific (highest literal count) first; ties keep registration order.\n cur.mixedParams.sort((a, b) => b.literalChars - a.literalChars)\n cur = slot.node\n }\n if (!cur.handlers) cur.handlers = {}\n if (cur.handlers[route.method]) {\n throw new Error(`Duplicate route: ${route.method} ${route.path}`)\n }\n cur.handlers[route.method] = route\n }\n\n find(method: HttpMethod, pathname: string): MatchResult | null {\n const matched = walkInline(this.#root, pathname)\n if (!matched) return null\n const route = matched.node.handlers?.[method]\n if (!route) {\n // Fallback: HEAD uses GET; OPTIONS handled by caller.\n if (method === \"HEAD\") {\n const getRoute = matched.node.handlers?.GET\n if (getRoute) return { route: getRoute, params: matched.params ?? EMPTY_PARAMS }\n }\n return null\n }\n return { route, params: matched.params ?? EMPTY_PARAMS }\n }\n\n /** Enumerate all routes for introspection. */\n *all(): Generator<Route> {\n yield* enumerate(this.#root)\n }\n}\n\nfunction newNode(): Node {\n return { statics: new Map() }\n}\n\nconst EMPTY_PARAMS: Record<string, string> = Object.freeze(\n Object.create(null) as Record<string, string>,\n) as Record<string, string>\n\nfunction splitPath(path: string): string[] {\n const trimmed = path.startsWith(\"/\") ? path.slice(1) : path\n if (trimmed === \"\") return []\n return trimmed.split(\"/\")\n}\n\ninterface WalkHit {\n readonly node: Node\n /** Lazily allocated — `null` means \"no params were matched\". */\n readonly params: Record<string, string> | null\n}\n\n/**\n * Zero-allocation walker for the static fast path.\n *\n * Iterates the pathname by slicing between `/` delimiters directly on the\n * string — no segments array, no params object, no closures. When a `:param`,\n * mixed pattern, or `*` node is encountered we switch to the `walkWithParams`\n * helper which handles backtracking.\n */\nfunction walkInline(root: Node, pathname: string): WalkHit | null {\n let i = pathname.charCodeAt(0) === 47 /* '/' */ ? 1 : 0\n const len = pathname.length\n let node: Node = root\n\n // Empty path (`/` or ``) matches the root.\n if (i >= len) return { node, params: null }\n\n while (i < len) {\n let j = i\n while (j < len && pathname.charCodeAt(j) !== 47) j++\n const seg = pathname.slice(i, j)\n\n const stat = node.statics.get(seg)\n if (stat && !node.pureParam && !node.mixedParams && !node.wildcard) {\n // Unambiguous static step — no backtracking possible.\n node = stat\n i = j + 1\n continue\n }\n return walkWithParams(node, pathname, i)\n }\n return { node, params: null }\n}\n\nfunction walkWithParams(startNode: Node, pathname: string, startIndex: number): WalkHit | null {\n const params: Record<string, string> = {}\n const hit = walkRecur(startNode, pathname, startIndex, params)\n if (!hit) return null\n for (const _k in params) return { node: hit, params }\n return { node: hit, params: null }\n}\n\nfunction walkRecur(\n node: Node,\n pathname: string,\n i: number,\n params: Record<string, string>,\n): Node | null {\n const len = pathname.length\n if (i >= len) return node\n let j = i\n while (j < len && pathname.charCodeAt(j) !== 47) j++\n const seg = pathname.slice(i, j)\n const nextIndex = j + 1\n\n // 1) Static (most specific) wins first.\n const stat = node.statics.get(seg)\n if (stat) {\n if (nextIndex > len) return stat\n const r = walkRecur(stat, pathname, nextIndex, params)\n if (r) return r\n }\n\n // 2a) Mixed-segment params first (most specific to least specific).\n if (node.mixedParams) {\n for (const mp of node.mixedParams) {\n const m = mp.regex.exec(seg)\n if (!m) continue\n const captured = mp.names\n for (let k = 0; k < captured.length; k++) {\n const name = captured[k]\n if (name !== undefined) {\n const value = m[k + 1]\n params[name] = value === undefined ? \"\" : decodeURIComponent(value)\n }\n }\n if (nextIndex > len) return mp.node\n const r = walkRecur(mp.node, pathname, nextIndex, params)\n if (r) return r\n for (const name of captured) delete params[name]\n }\n }\n\n // 2b) Pure-segment param.\n if (node.pureParam) {\n const name = node.pureParam.name\n params[name] = decodeURIComponent(seg)\n if (nextIndex > len) return node.pureParam.node\n const r = walkRecur(node.pureParam.node, pathname, nextIndex, params)\n if (r) return r\n delete params[name]\n }\n\n // 3) Wildcard catch-all.\n if (node.wildcard) {\n params[\"*\"] = decodeURIComponent(pathname.slice(i))\n return node.wildcard\n }\n return null\n}\n\nfunction* enumerate(node: Node): Generator<Route> {\n if (node.handlers) {\n for (const v of Object.values(node.handlers)) if (v) yield v\n }\n for (const child of node.statics.values()) yield* enumerate(child)\n if (node.pureParam) yield* enumerate(node.pureParam.node)\n if (node.mixedParams) for (const mp of node.mixedParams) yield* enumerate(mp.node)\n if (node.wildcard) yield* enumerate(node.wildcard)\n}\n\n// ---------------------------------------------------------------------------\n// Pattern parsing\n// ---------------------------------------------------------------------------\n\ntype ParsedSegment =\n | \"static\"\n | { readonly kind: \"pure\"; readonly name: string }\n | {\n readonly kind: \"mixed\"\n readonly regex: RegExp\n readonly names: readonly string[]\n readonly literalChars: number\n }\n\nconst IDENT_HEAD = /[A-Za-z_]/\nconst IDENT_REST = /[A-Za-z0-9_]/\n\nfunction parsePatternSegment(seg: string): ParsedSegment {\n // Fast path: no `:` means pure-static.\n if (seg.indexOf(\":\") === -1) return \"static\"\n\n // Single `:name` covering the whole segment? -> pure param (fast path).\n if (seg.length > 1 && seg.charCodeAt(0) === 58 /* ':' */) {\n let k = 1\n if (k < seg.length && IDENT_HEAD.test(seg.charAt(k))) {\n k++\n while (k < seg.length && IDENT_REST.test(seg.charAt(k))) k++\n if (k === seg.length) return { kind: \"pure\", name: seg.slice(1) }\n }\n }\n\n // Mixed segment — scan and build a regex, capturing each `:name`.\n const names: string[] = []\n let pattern = \"^\"\n let literalChars = 0\n let i = 0\n while (i < seg.length) {\n const ch = seg.charAt(i)\n if (ch === \":\" && i + 1 < seg.length && IDENT_HEAD.test(seg.charAt(i + 1))) {\n let k = i + 1\n while (k < seg.length && IDENT_REST.test(seg.charAt(k))) k++\n const name = seg.slice(i + 1, k)\n if (names.includes(name)) {\n throw new Error(`Duplicate param :${name} in segment \"${seg}\"`)\n }\n names.push(name)\n pattern += \"([^/]+?)\"\n i = k\n continue\n }\n pattern += escapeRegexChar(ch)\n literalChars += 1\n i++\n }\n pattern += \"$\"\n return { kind: \"mixed\", regex: new RegExp(pattern), names, literalChars }\n}\n\nconst REGEX_META = new Set(\".*+?^${}()|[]\\\\\".split(\"\"))\nfunction escapeRegexChar(ch: string): string {\n return REGEX_META.has(ch) ? `\\\\${ch}` : ch\n}\n",
466
+ "sha256": "f81563c103792e1b30d75349453368bf4cb5d6c92b5443c4b661e2ab2526784e"
467
+ },
468
+ {
469
+ "path": "core/security.ts",
470
+ "contents": "/**\n * Secure-by-default baseline.\n *\n * Applied by the app's fetch pipeline. Opt-out only — users can\n * override per-route via `.meta({ headers: {...} })` or disable\n * globally in `app({ security: {...} })`.\n */\n\nimport type { SecurityDefaults } from \"./types.ts\"\n\n/** Default 1 MB body size limit. */\nexport const DEFAULT_BODY_LIMIT_BYTES: number = 1_048_576\n\n/** Default response headers (sans HSTS; HSTS only added on HTTPS in prod). */\nexport const DEFAULT_RESPONSE_HEADERS: Readonly<Record<string, string>> = Object.freeze({\n \"x-content-type-options\": \"nosniff\",\n \"x-frame-options\": \"DENY\",\n \"referrer-policy\": \"strict-origin-when-cross-origin\",\n \"cross-origin-opener-policy\": \"same-origin\",\n \"cross-origin-resource-policy\": \"same-origin\",\n \"permissions-policy\": \"camera=(), microphone=(), geolocation=(), interest-cohort=()\",\n})\n\n/** Headers that must never be emitted by Hyper itself. */\nexport const SUPPRESSED_HEADERS: readonly string[] = Object.freeze([\"server\"])\n\n/**\n * Keys that are refused by our JSON parser at the boundary to prevent\n * prototype pollution. Rejection raises a 400 HyperError.\n */\nexport const FORBIDDEN_JSON_KEYS: readonly string[] = Object.freeze([\n \"__proto__\",\n \"constructor\",\n \"prototype\",\n])\n\nexport const DEFAULT_SECURITY: SecurityDefaults = {\n headers: true,\n bodyLimitBytes: DEFAULT_BODY_LIMIT_BYTES,\n rejectProtoKeys: true,\n serverHeader: false,\n rejectMethodOverride: true,\n requestTimeoutMs: 30_000,\n hstsEnv: \"production\",\n}\n\n/** Headers/body keys used to smuggle verbs via override. */\nexport const METHOD_OVERRIDE_HEADERS: readonly string[] = Object.freeze([\n \"x-http-method-override\",\n \"x-method-override\",\n \"x-http-method\",\n])\nexport const METHOD_OVERRIDE_QUERY_KEYS: readonly string[] = Object.freeze([\"_method\"])\n\n// Precomputed entries of the default response headers — hoisted out of\n// the hot path so we don't pay an Object.entries allocation per request.\nconst DEFAULT_HEADER_ENTRIES: ReadonlyArray<readonly [string, string]> = Object.freeze(\n Object.entries(DEFAULT_RESPONSE_HEADERS),\n)\n\n/**\n * Apply default headers to a Response. Always returns a new Response\n * with a fresh Headers bag — the previous version tried to short-circuit\n * via `headersEqual`, which was strictly more expensive than cloning\n * (two Array allocations + two sorts every request).\n *\n * HSTS is added only when `https && emitHsts !== false`.\n */\nexport function applyDefaultHeaders(\n res: Response,\n opts: {\n https: boolean\n overrides?: Readonly<Record<string, string>>\n /** Emit HSTS. Typically set by the app only in production + HTTPS. */\n emitHsts?: boolean\n },\n): Response {\n const { https, emitHsts, overrides } = opts\n const headers = new Headers(res.headers)\n for (let i = 0; i < DEFAULT_HEADER_ENTRIES.length; i++) {\n const entry = DEFAULT_HEADER_ENTRIES[i]!\n if (!headers.has(entry[0])) headers.set(entry[0], entry[1])\n }\n if (https && emitHsts !== false && !headers.has(\"strict-transport-security\")) {\n headers.set(\"strict-transport-security\", \"max-age=15552000; includeSubDomains\")\n }\n for (let i = 0; i < SUPPRESSED_HEADERS.length; i++) headers.delete(SUPPRESSED_HEADERS[i]!)\n if (overrides) {\n for (const k in overrides) {\n const v = overrides[k]\n if (v !== undefined) headers.set(k.toLowerCase(), v)\n }\n }\n return new Response(res.body, { status: res.status, statusText: res.statusText, headers })\n}\n\n/**\n * Deep-walk and reject forbidden keys (prototype pollution guard).\n * Throws `PrototypePollutionError` on hit.\n */\nexport function assertNoProtoKeys(value: unknown, path: string[] = []): void {\n if (value === null || typeof value !== \"object\") return\n if (Array.isArray(value)) {\n for (let i = 0; i < value.length; i++) {\n assertNoProtoKeys(value[i], [...path, String(i)])\n }\n return\n }\n for (const key of Object.keys(value)) {\n if (FORBIDDEN_JSON_KEYS.includes(key)) {\n throw new PrototypePollutionError(key, path)\n }\n assertNoProtoKeys((value as Record<string, unknown>)[key], [...path, key])\n }\n}\n\nexport class PrototypePollutionError extends Error {\n readonly key: string\n readonly path: readonly string[]\n constructor(key: string, path: readonly string[]) {\n super(`Refusing body containing dangerous key \"${key}\" at ${path.join(\".\") || \"(root)\"}`)\n this.name = \"PrototypePollutionError\"\n this.key = key\n this.path = path\n }\n}\n",
471
+ "sha256": "ce26fbfbb77fe2ba9df90900935bc079df4dc91e96d777983a3ad0c00b7502b7"
472
+ },
473
+ {
474
+ "path": "core/standard-schema.ts",
475
+ "contents": "/**\n * Standard Schema adapter.\n *\n * Hyper does not depend on any specific validation library; we accept\n * anything implementing the `~standard` contract.\n *\n * Spec: https://github.com/standard-schema/standard-schema\n */\n\nexport interface StandardSchemaV1<Input = unknown, Output = Input> {\n readonly \"~standard\": StandardSchemaV1Props<Input, Output>\n}\n\nexport interface StandardSchemaV1Props<Input, Output> {\n readonly version: 1\n readonly vendor: string\n readonly validate: (\n value: unknown,\n ) => StandardSchemaV1Result<Output> | Promise<StandardSchemaV1Result<Output>>\n readonly types?: StandardSchemaV1Types<Input, Output> | undefined\n}\n\nexport interface StandardSchemaV1Types<Input, Output> {\n readonly input: Input\n readonly output: Output\n}\n\nexport type StandardSchemaV1Result<Output> =\n | StandardSchemaV1SuccessResult<Output>\n | StandardSchemaV1FailureResult\n\nexport interface StandardSchemaV1SuccessResult<Output> {\n readonly value: Output\n readonly issues?: undefined\n}\n\nexport interface StandardSchemaV1FailureResult {\n readonly issues: readonly StandardSchemaV1Issue[]\n}\n\nexport interface StandardSchemaV1Issue {\n readonly message: string\n readonly path?: readonly (PropertyKey | StandardSchemaV1PathSegment)[] | undefined\n}\n\nexport interface StandardSchemaV1PathSegment {\n readonly key: PropertyKey\n}\n\n/**\n * Run a Standard Schema against `value`. Returns the parsed value or\n * throws a `SchemaValidationError` with the issues attached so the\n * error mapper can project to a 400 with why/fix.\n */\nexport async function parseStandard<I, O>(\n schema: StandardSchemaV1<I, O>,\n value: unknown,\n): Promise<O> {\n const result = await schema[\"~standard\"].validate(value)\n if (result.issues && result.issues.length > 0) {\n throw new SchemaValidationError(result.issues)\n }\n\n return (result as StandardSchemaV1SuccessResult<O>).value\n}\n\nexport class SchemaValidationError extends Error {\n readonly issues: readonly StandardSchemaV1Issue[]\n constructor(issues: readonly StandardSchemaV1Issue[]) {\n super(\n issues\n .map((i) => `${(i.path ?? []).map(String).join(\".\") || \"(root)\"}: ${i.message}`)\n .join(\"; \"),\n )\n this.name = \"SchemaValidationError\"\n this.issues = issues\n }\n}\n\n/** Narrowing guard. */\nexport function isStandardSchema(x: unknown): x is StandardSchemaV1 {\n return (\n typeof x === \"object\" &&\n x !== null &&\n \"~standard\" in x &&\n typeof (x as { \"~standard\": unknown })[\"~standard\"] === \"object\"\n )\n}\n",
476
+ "sha256": "6283de86db2508ac522d3ea59585d0ac0b1ab4e83b99c3a70947e679e2691ac9"
477
+ },
478
+ {
479
+ "path": "core/types.ts",
480
+ "contents": "/**\n * Public types for @hyper/core.\n *\n * Kept in a single file so declaration merging surfaces (AppContext,\n * RouteMeta, ErrorRegistry) are easy to locate and re-export.\n */\n\nimport type { StandardSchemaV1 } from \"./standard-schema.ts\"\n\n/** HTTP verbs supported by the builder. */\nexport type HttpMethod = \"GET\" | \"POST\" | \"PUT\" | \"PATCH\" | \"DELETE\" | \"HEAD\" | \"OPTIONS\"\n\n/**\n * Consumer-augmentable app context. Decorate/derive/plugins populate\n * this via `declare module \"@hyper/core\" { interface AppContext { ... } }`.\n *\n * Declared as an empty `interface` (not `type = {}`) so that TypeScript\n * honors declaration-merging: every `declare module` contribution adds\n * fields to this shape. Unlike `type = {}`, which matches any non-null\n * value and silently accepts garbage, `interface AppContext {}` enforces\n * the augmented shape in consumer code.\n */\n// biome-ignore lint/suspicious/noEmptyInterface: augmentable declaration-merging surface\n// biome-ignore lint/complexity/noBannedTypes: interface is the correct primitive here\nexport interface AppContext {}\n\n/** A per-route example — surfaced to OpenAPI, MCP few-shot, client JSDoc, tests. */\nexport interface RouteExample {\n readonly name: string\n readonly input?: {\n params?: Record<string, unknown>\n query?: Record<string, unknown>\n body?: unknown\n headers?: Record<string, unknown>\n }\n readonly output?: {\n status?: number\n body?: unknown\n }\n}\n\n/** Per-route metadata (OpenAPI, MCP, auth tags, etc.). Augmentable. */\nexport interface RouteMeta {\n /** Human-readable name. */\n name?: string\n /** Free-form tags; plugins may filter on these. */\n tags?: readonly string[]\n /** Set by `@hyper/mcp`; if absent, the route is not MCP-exposed. */\n mcp?: false | { description: string; [k: string]: unknown }\n /** Reserved for internal tooling (dev MCP etc.). Never projected. */\n internal?: boolean\n /** CSRF on/off for cookie-auth routes. Default: on. */\n csrf?: boolean\n /** Marks auth-endpoint routes for the default rate-limit recipe. */\n authEndpoint?: boolean\n /** Caller-defined overrides to response headers. */\n headers?: Record<string, string>\n /** Marks the route as deprecated — surfaces in OpenAPI + Sunset header. */\n deprecated?: boolean | { readonly reason?: string; readonly sunset?: string }\n /** API version for header/prefix-based routing. */\n version?: string\n /** Server action marker (`.actionable()`). */\n action?: boolean\n /** Per-route hard timeout in milliseconds. Overrides `security.requestTimeoutMs`. */\n timeoutMs?: number\n /** Examples — OpenAPI, MCP few-shot, client JSDoc, contract tests. */\n examples?: readonly RouteExample[]\n [k: string]: unknown\n}\n\n/**\n * Named error codes catalog per route. Augmentable via declaration\n * merging (same pattern as {@link AppContext}).\n */\n// biome-ignore lint/suspicious/noEmptyInterface: augmentable declaration-merging surface\n// biome-ignore lint/complexity/noBannedTypes: interface is the correct primitive here\nexport interface ErrorRegistry {}\n\n/** The shape returned from every parsing step — Standard Schema aligned. */\nexport type Infer<S> = S extends StandardSchemaV1<unknown, infer O> ? O : unknown\n\n/** A Hyper response can be a real `Response`, a `Bun.file`, or bare data. */\nexport type HandlerReturn =\n | Response\n | BunFileLike\n | object\n | string\n | number\n | boolean\n | null\n | ReadableStream\n | AsyncIterable<string | Uint8Array>\n | undefined\n\n/** Marker for anything coercible by our response layer (Bun.file(...)). */\nexport interface BunFileLike {\n readonly stream: () => ReadableStream\n readonly type?: string\n readonly size?: number\n readonly name?: string\n}\n\n/** A compiled route — the normalized shape the app and router consume. */\nexport interface Route<M extends HttpMethod = HttpMethod> {\n readonly method: M\n readonly path: string\n readonly params?: StandardSchemaV1\n readonly query?: StandardSchemaV1\n readonly body?: StandardSchemaV1\n readonly headers?: StandardSchemaV1\n readonly meta: RouteMeta\n readonly handler: RouteHandler\n /** Declared thrown-error shapes keyed by HTTP status (projection surface). */\n readonly throws?: Record<number, StandardSchemaV1>\n /** Named error-code catalog (projection surface). */\n readonly errors?: Record<string, StandardSchemaV1>\n /** True when the handler is a function (not a pre-built Response). */\n readonly kind: \"fn\" | \"static\"\n /** Optional compile-time-static Response for fast path. */\n readonly staticResponse?: Response\n /**\n * Tags for every middleware attached to this route, in order. Middleware\n * opts in by setting `fn.__hyperTag = \"<name>\"`. Consumed by\n * `hyper security --check` and other introspection tools.\n */\n readonly middlewareTags?: readonly string[]\n}\n\n/** The internal handler shape after builder normalization. */\nexport type RouteHandler = (ctx: InternalHandlerCtx) => Promise<HandlerReturn> | HandlerReturn\n\n/**\n * Context passed into the handler — a superset the framework builds.\n *\n * `url`, `query`, `headers`, and `responseHeaders` are lazy: they're\n * materialized only when the handler reads them. When a route\n * declares a `.query()` / `.headers()` schema the pipeline sets the\n * parsed plain object as an own property (shadowing the lazy getter).\n * `query` / `headers` therefore hold whatever the handler expects —\n * typically a parsed record or `Record<string, string>` for the\n * schema-less case.\n */\nexport interface InternalHandlerCtx {\n readonly req: Request\n readonly url: URL\n readonly params: Record<string, string>\n readonly query: unknown\n readonly headers: unknown\n readonly body: unknown\n /** Populated by plugin.context / decorate / derive. */\n readonly ctx: AppContext\n /** Lazy Bun.CookieMap accessor — parse on first touch. */\n readonly cookies: () => import(\"bun\").CookieMap\n /** Mutable response header bag; flushed into the final Response. */\n readonly responseHeaders: Headers\n}\n\n/** A decorator factory — produces static context from the parsed env. */\nexport type DecorateFactory<Env = unknown, Added extends object = object> = (\n env: Env,\n) => Added | Promise<Added>\n\n/** A derive factory — produces per-request context from ctx + env + req. */\nexport type DeriveFactory<\n Env = unknown,\n CtxIn extends AppContext = AppContext,\n Added extends object = object,\n> = (args: { ctx: CtxIn; env: Env; req: Request }) => Added | Promise<Added>\n\n/** Input accepted for `AppConfig.groups` — matches the GroupBuilder shape. */\nexport interface GroupConfigEntry {\n /** The flattened build output consumed by `app()`. */\n build(): RouteGroup\n}\n\n/** A plain-object router; nested records of routes or sub-routers. */\nexport interface PlainRouterConfig {\n readonly [key: string]: Route | PlainRouterConfig\n}\n\n/** App-level config. */\nexport interface AppConfig {\n /** Collected top-level routes. */\n readonly routes?: readonly Route[]\n /** Collected groups (flattened at app()). Accepts `GroupBuilder`s or `RouteGroup` literals. */\n readonly groups?: readonly (GroupConfigEntry | RouteGroup)[]\n /** Plain-object router (gives the typed-client tree). */\n readonly router?: PlainRouterConfig\n /** Feature flags for security defaults. On by default. */\n readonly security?: Partial<SecurityDefaults>\n /** Env schema + secrets + source. */\n readonly env?: EnvConfigLike\n /** Static context decoration (db, redis, etc.) constructed at boot. */\n readonly decorate?: readonly DecorateFactory[]\n /** Per-request derived context. */\n readonly derive?: readonly DeriveFactory[]\n /** Plugins installed in priority order. */\n readonly plugins?: readonly HyperPlugin[]\n}\n\n/** Plugin surface for extending `app()` with lifecycle hooks and ctx decoration. */\nexport interface HyperPlugin {\n readonly name: string\n readonly build?: (app: HyperApp) => void | Promise<void>\n readonly request?: {\n /**\n * Fires BEFORE route matching. Returning a Response short-circuits\n * the rest of the pipeline — ideal for CORS preflight and auth gates.\n */\n readonly preRoute?: (args: {\n req: Request\n }) => Response | undefined | Promise<Response | undefined>\n readonly before?: (args: {\n req: Request\n ctx: AppContext\n route?: Route\n }) => void | Promise<void>\n readonly after?: (args: {\n req: Request\n ctx: AppContext\n res: Response\n route?: Route\n }) => void | Promise<void>\n readonly onError?: (args: {\n req: Request\n ctx: AppContext\n error: unknown\n route?: Route\n }) => void | Promise<void>\n }\n readonly context?: (env: unknown) => object | Promise<object>\n}\n\nexport interface EnvConfigLike {\n readonly schema?: unknown\n readonly secrets?: readonly string[]\n readonly source?: Record<string, string | undefined>\n}\n\n/** A single invocation — the shared path between HTTP/MCP/RPC/actions. */\nexport interface InvokeInput {\n readonly method: HttpMethod\n readonly path: string\n readonly params?: Record<string, string>\n readonly query?: Record<string, unknown>\n readonly body?: unknown\n readonly headers?: Record<string, string>\n /** Optional pre-set AppContext (bypasses decorate/derive). Useful for tests. */\n readonly ctx?: AppContext\n}\n\nexport interface InvokeResult {\n readonly status: number\n readonly data: unknown\n readonly headers: Headers\n}\n\n/** The built app surface. */\nexport interface HyperApp {\n /** fetch-compatible entry point for any Bun/edge/workers adapter. */\n readonly fetch: (req: Request) => Promise<Response>\n /** Bun.serve({ routes }) shape — static + dynamic routes mounted natively. */\n readonly routes: BunRoutes\n /** Raw route list for introspection. */\n readonly routeList: readonly Route[]\n /** Shared invoke path — HTTP/MCP/RPC/actions all funnel here. */\n readonly invoke: (input: InvokeInput) => Promise<InvokeResult>\n /** OpenAPI 3.1 serializer (schema conversion provided by @hyper/openapi). */\n readonly toOpenAPI: (cfg?: {\n title?: string\n version?: string\n description?: string\n }) => import(\"./projection.ts\").OpenAPIManifest\n /** MCP manifest. @hyper/mcp adds the transport. */\n readonly toMCPManifest: () => import(\"./projection.ts\").MCPManifest\n /** Client manifest. @hyper/client consumes this. */\n readonly toClientManifest: () => import(\"./projection.ts\").ClientManifest\n /** Original AppConfig — used by `app.test()` to produce scoped clones. */\n readonly __config: AppConfig\n /**\n * Create a test-scoped clone. Replaces env/decorate/derive and can skip\n * or swap plugins by name. Returns a fresh immutable app.\n */\n readonly test: (overrides?: TestOverrides) => HyperApp\n}\n\n/** Overrides accepted by `app.test()`. */\nexport interface TestOverrides {\n /** Replace env source values (merged into config.env.source). */\n readonly env?: Record<string, string | undefined>\n /** Additional decorators appended to config.decorate. */\n readonly decorate?: DecorateFactory | readonly DecorateFactory[]\n /** Additional derive functions appended to config.derive. */\n readonly derive?: DeriveFactory | readonly DeriveFactory[]\n /** Plugins to skip (by name) or replace (by name → new plugin). */\n readonly plugins?: {\n readonly skip?: readonly string[]\n readonly replace?: Record<string, HyperPlugin>\n readonly add?: readonly HyperPlugin[]\n }\n}\n\n/** One mounted-route value in `Bun.serve({ routes })`. */\nexport type BunRouteValue =\n | Response\n | Record<string, Response>\n | ((req: Request) => Response | Promise<Response>)\n\n/** The Bun.serve routes map shape. See https://bun.sh/docs/api/http#routing. */\nexport type BunRoutes = Record<string, BunRouteValue>\n\n/** A RouteGroup is a collection of routes with a shared prefix. */\nexport interface RouteGroup {\n readonly prefix: string\n readonly routes: readonly Route[]\n}\n\n/** Security defaults — see ./security.ts for wire values. */\nexport interface SecurityDefaults {\n readonly headers: boolean\n readonly bodyLimitBytes: number\n readonly rejectProtoKeys: boolean\n readonly serverHeader: false\n /**\n * When true, a request carrying `X-HTTP-Method-Override` or\n * `_method` is rejected with 400. Default: true. Prevents a class of\n * CSRF/verb-smuggling bugs where attackers bypass safe-method checks.\n */\n readonly rejectMethodOverride: boolean\n /**\n * Hard request timeout in ms. The framework aborts the handler and\n * returns 504 if a response isn't produced in time. 0 disables.\n * Default: 30_000 (30s).\n */\n readonly requestTimeoutMs: number\n /**\n * Explicit env that allows Hyper to emit HSTS. Default: \"production\".\n * HSTS is never emitted for HTTP, and only emitted for HTTPS when the\n * current NODE_ENV (or provided `env`) matches. Prevents accidental\n * HSTS pinning on dev domains.\n */\n readonly hstsEnv: string | false\n}\n",
481
+ "sha256": "b37b2ecf54186938e449559cf4d80d772ef3a4f2a856bc140776ecb8a3f0c288"
482
+ }
483
+ ],
484
+ "subpaths": {
485
+ "bun": "adapters/bun"
486
+ }
487
+ },
488
+ "cors": {
489
+ "name": "cors",
490
+ "version": "0.1.0",
491
+ "description": "Minimal, strict CORS middleware for Hyper.",
492
+ "readme": "# @hyper/cors\n\nMinimal, strict CORS middleware for Hyper.\n\n## Install\n\n```bash\nbun add @hyper/cors\n```\n\n## Usage\n\n```ts\nimport { Hyper } from \"@hyper/core\"\nimport { corsPlugin } from \"@hyper/cors\"\n\nexport default new Hyper()\n .use(corsPlugin({ origin: [\"https://example.com\"] }))\n .get(\"/\", () => ({ hello: \"world\" }))\n .listen(3000)\n```\n\n## Docs\n\nSee the [main README](../../README.md) and [docs/](../../docs) for guides and integration recipes.\n\n## License\n\nMIT\n",
493
+ "registryDeps": [
494
+ "core"
495
+ ],
496
+ "peerDeps": {},
497
+ "optionalPeerDeps": {},
498
+ "files": [
499
+ {
500
+ "path": "cors/index.ts",
501
+ "contents": "/**\n * @hyper/cors — strict, zero-config-wins CORS for Hyper.\n *\n * Secure by default:\n * - origins MUST be a list or a callback; wildcard \"*\" is refused when\n * credentials are on.\n * - vary: Origin is always set.\n * - Preflight answered via plugin pre-hook (short-circuits route match).\n *\n * Usage:\n * app({ plugins: [corsPlugin({ origin: [\"https://app.example.com\"] })] })\n */\n\nimport type { HyperPlugin } from \"@hyper/core\"\n\nexport interface CorsConfig {\n readonly origin: readonly string[] | ((origin: string) => boolean) | \"*\"\n readonly methods?: readonly string[]\n readonly headers?: readonly string[]\n readonly credentials?: boolean\n readonly exposeHeaders?: readonly string[]\n readonly maxAge?: number\n /**\n * Opt out of the wildcard-origin hard error. Off by default.\n *\n * Passing `origin: \"*\"` without this flag is rejected at boot because\n * wildcard CORS effectively disables the Same-Origin Policy for your\n * API — any site on the internet can read it. If you really want that,\n * set `allowAnyOrigin: true` to acknowledge the risk.\n */\n readonly allowAnyOrigin?: boolean\n}\n\nconst DEFAULT_METHODS = [\"GET\", \"HEAD\", \"POST\", \"PUT\", \"PATCH\", \"DELETE\", \"OPTIONS\"] as const\nconst DEFAULT_HEADERS = [\"content-type\", \"authorization\", \"x-request-id\"] as const\n\nfunction assertValid(config: CorsConfig): void {\n if (config.credentials && config.origin === \"*\") {\n throw new Error(\n \"cors: credentials=true is incompatible with origin='*'. \" +\n \"Why: browsers refuse to send credentials to wildcard origins; this configuration never works. \" +\n \"Fix: list the exact origins you trust in `origin: [...]`.\",\n )\n }\n if (config.origin === \"*\" && !config.allowAnyOrigin) {\n throw new Error(\n \"cors: origin='*' is refused by default. \" +\n \"Why: wildcard CORS lets every site on the internet read responses from your API. \" +\n \"Fix: pass an explicit allow-list (`origin: ['https://app.example.com']`) or a predicate \" +\n \"(`origin: (o) => o.endsWith('.example.com')`). If you really want public read access \" +\n \"(public docs, OSS metrics, etc.), set `allowAnyOrigin: true` to acknowledge the risk.\",\n )\n }\n}\n\nfunction resolveOrigin(config: CorsConfig, origin: string): string | null {\n if (config.origin === \"*\") return \"*\"\n if (typeof config.origin === \"function\") return config.origin(origin) ? origin : null\n return config.origin.includes(origin) ? origin : null\n}\n\nexport function corsPlugin(config: CorsConfig): HyperPlugin {\n assertValid(config)\n const allowMethods = (config.methods ?? DEFAULT_METHODS).join(\", \")\n const allowHeaders = (config.headers ?? DEFAULT_HEADERS).join(\", \")\n const exposeHeaders = config.exposeHeaders?.join(\", \")\n const maxAge = (config.maxAge ?? 600).toString()\n return {\n name: \"@hyper/cors\",\n request: {\n preRoute({ req }) {\n if (req.method !== \"OPTIONS\" || !req.headers.has(\"access-control-request-method\")) {\n return\n }\n const origin = req.headers.get(\"origin\") ?? \"\"\n const allowed = origin ? resolveOrigin(config, origin) : null\n const headers = new Headers({ vary: \"Origin, Access-Control-Request-Headers\" })\n if (allowed) {\n headers.set(\"access-control-allow-origin\", allowed)\n headers.set(\"access-control-allow-methods\", allowMethods)\n headers.set(\"access-control-allow-headers\", allowHeaders)\n headers.set(\"access-control-max-age\", maxAge)\n if (config.credentials) headers.set(\"access-control-allow-credentials\", \"true\")\n }\n return new Response(null, { status: 204, headers })\n },\n after({ req, res }) {\n const origin = req.headers.get(\"origin\")\n if (!origin) return\n const allowed = resolveOrigin(config, origin)\n if (!allowed) return\n res.headers.append(\"vary\", \"Origin\")\n res.headers.set(\"access-control-allow-origin\", allowed)\n if (config.credentials) res.headers.set(\"access-control-allow-credentials\", \"true\")\n if (exposeHeaders) res.headers.set(\"access-control-expose-headers\", exposeHeaders)\n },\n },\n }\n}\n",
502
+ "sha256": "2e899bd6938273e082d8741be0ecad1d20dfca98962b594a54ccc777c7c29f8c"
503
+ }
504
+ ],
505
+ "subpaths": {}
506
+ },
507
+ "csp": {
508
+ "name": "csp",
509
+ "version": "0.1.0",
510
+ "description": "Content-Security-Policy + sibling headers (CSP, CORP, COEP, COOP, Report-To) for Hyper.",
511
+ "readme": "# @hyper/csp\n\nContent-Security-Policy + sibling headers (CSP, CORP, COEP, COOP, Report-To) for Hyper.\n\n## Install\n\n```bash\nbun add @hyper/csp\n```\n\n## Usage\n\n```ts\nimport { Hyper } from \"@hyper/core\"\nimport { cspPlugin } from \"@hyper/csp\"\n\nexport default new Hyper()\n .use(cspPlugin({ strictApi: true }))\n .listen(3000)\n```\n\n## Docs\n\nSee the [main README](../../README.md) and [docs/](../../docs) for guides and integration recipes.\n\n## License\n\nMIT\n",
512
+ "registryDeps": [
513
+ "core"
514
+ ],
515
+ "peerDeps": {},
516
+ "optionalPeerDeps": {},
517
+ "files": [
518
+ {
519
+ "path": "csp/index.ts",
520
+ "contents": "/**\n * @hyper/csp — Content-Security-Policy + strict-by-default siblings.\n *\n * Most Hyper deployments serve JSON APIs and never render HTML, so the\n * default policy is extremely restrictive:\n *\n * default-src 'none'; frame-ancestors 'none'; base-uri 'none';\n * form-action 'none'; object-src 'none'; upgrade-insecure-requests\n *\n * Consumers that serve HTML can provide a tighter HTML-shaped policy\n * using `cspPlugin({ directives: {...} })`. Per-response nonces are\n * minted when `noncePlaceholder` is set — the handler can read the\n * nonce from `ctx.cspNonce` and inject it into inline `<script>` tags.\n */\n\nimport type { HyperPlugin } from \"@hyper/core\"\n\nexport interface CspConfig {\n /** Emit `Content-Security-Policy-Report-Only` instead of the enforcing header. */\n readonly reportOnly?: boolean\n /** Whether to add a fresh per-response nonce to script-src/style-src. */\n readonly nonce?: boolean\n /** Key-value directive map. Values are arrays of allowed sources. */\n readonly directives?: Partial<Record<CspDirective, readonly string[]>>\n /** Report endpoint; if set, emits `report-to` + a Report-To header stub. */\n readonly reportUri?: string\n /** Extra response headers. */\n readonly headers?: {\n readonly permissionsPolicy?: string\n readonly referrerPolicy?: string\n readonly crossOriginEmbedderPolicy?: \"require-corp\" | \"unsafe-none\" | \"credentialless\"\n readonly crossOriginOpenerPolicy?: \"same-origin\" | \"same-origin-allow-popups\" | \"unsafe-none\"\n readonly crossOriginResourcePolicy?: \"same-site\" | \"same-origin\" | \"cross-origin\"\n }\n}\n\nexport type CspDirective =\n | \"default-src\"\n | \"base-uri\"\n | \"connect-src\"\n | \"font-src\"\n | \"form-action\"\n | \"frame-ancestors\"\n | \"frame-src\"\n | \"img-src\"\n | \"manifest-src\"\n | \"media-src\"\n | \"object-src\"\n | \"script-src\"\n | \"script-src-elem\"\n | \"script-src-attr\"\n | \"style-src\"\n | \"style-src-elem\"\n | \"style-src-attr\"\n | \"worker-src\"\n | \"upgrade-insecure-requests\"\n | \"block-all-mixed-content\"\n\nconst API_DEFAULT: Partial<Record<CspDirective, readonly string[]>> = {\n \"default-src\": [\"'none'\"],\n \"frame-ancestors\": [\"'none'\"],\n \"base-uri\": [\"'none'\"],\n \"form-action\": [\"'none'\"],\n \"object-src\": [\"'none'\"],\n \"upgrade-insecure-requests\": [],\n}\n\ndeclare module \"@hyper/core\" {\n interface AppContext {\n readonly cspNonce?: string\n }\n}\n\nexport function cspPlugin(config: CspConfig = {}): HyperPlugin {\n const directives: Record<string, readonly string[]> = {\n ...API_DEFAULT,\n ...(config.directives as Record<string, readonly string[]> | undefined),\n }\n const header = config.reportOnly\n ? \"content-security-policy-report-only\"\n : \"content-security-policy\"\n\n return {\n name: \"@hyper/csp\",\n request: {\n before({ ctx }) {\n if (config.nonce) {\n const nonce = randomNonce()\n ;(ctx as { cspNonce?: string }).cspNonce = nonce\n }\n },\n after({ ctx, res }) {\n const merged: Record<string, readonly string[]> = { ...directives }\n if (config.nonce) {\n const n = (ctx as { cspNonce?: string }).cspNonce\n if (n) {\n merged[\"script-src\"] = dedupe([...(merged[\"script-src\"] ?? []), `'nonce-${n}'`])\n merged[\"style-src\"] = dedupe([...(merged[\"style-src\"] ?? []), `'nonce-${n}'`])\n }\n }\n if (config.reportUri) {\n merged[\"report-uri\"] = [config.reportUri]\n }\n const value = serialize(merged)\n if (value) res.headers.set(header, value)\n if (config.reportUri && !res.headers.has(\"report-to\")) {\n res.headers.set(\n \"report-to\",\n `{\"group\":\"csp\",\"max_age\":10886400,\"endpoints\":[{\"url\":\"${config.reportUri}\"}]}`,\n )\n }\n const h = config.headers\n if (h?.permissionsPolicy) res.headers.set(\"permissions-policy\", h.permissionsPolicy)\n if (h?.referrerPolicy) res.headers.set(\"referrer-policy\", h.referrerPolicy)\n if (h?.crossOriginEmbedderPolicy) {\n res.headers.set(\"cross-origin-embedder-policy\", h.crossOriginEmbedderPolicy)\n }\n if (h?.crossOriginOpenerPolicy) {\n res.headers.set(\"cross-origin-opener-policy\", h.crossOriginOpenerPolicy)\n }\n if (h?.crossOriginResourcePolicy) {\n res.headers.set(\"cross-origin-resource-policy\", h.crossOriginResourcePolicy)\n }\n },\n },\n }\n}\n\nfunction serialize(directives: Record<string, readonly string[]>): string {\n const parts: string[] = []\n for (const [name, values] of Object.entries(directives)) {\n if (values.length === 0) {\n parts.push(name)\n } else {\n parts.push(`${name} ${values.join(\" \")}`)\n }\n }\n return parts.join(\"; \")\n}\n\nfunction dedupe(values: readonly string[]): readonly string[] {\n return Array.from(new Set(values))\n}\n\nfunction randomNonce(): string {\n const buf = new Uint8Array(16)\n crypto.getRandomValues(buf)\n let s = \"\"\n for (let i = 0; i < buf.length; i++) s += String.fromCharCode(buf[i]!)\n return btoa(s).replace(/=+$/, \"\")\n}\n",
521
+ "sha256": "7a372546a274b87d0031d88155fed8932ac8943745fd90011c6493c6f322db95"
522
+ }
523
+ ],
524
+ "subpaths": {}
525
+ },
526
+ "dev-mcp": {
527
+ "name": "dev-mcp",
528
+ "version": "0.1.0",
529
+ "description": "Dev-mode app-as-MCP server — expose /.hyper/mcp with introspection + replay tools.",
530
+ "readme": "# @hyper/dev-mcp\n\nDev-mode app-as-MCP server — exposes `/.hyper/mcp` with introspection + replay tools.\n\n## Install\n\n```bash\nbun add @hyper/dev-mcp\n```\n\n## Usage\n\n```ts\nimport { Hyper } from \"@hyper/core\"\nimport { devMcp } from \"@hyper/dev-mcp\"\n\nconst app = new Hyper()\nif (process.env.NODE_ENV !== \"production\") app.use(devMcp())\nexport default app.listen(3000)\n```\n\n## Docs\n\nSee the [main README](../../README.md) and [docs/](../../docs) for guides and integration recipes.\n\n## License\n\nMIT\n",
531
+ "registryDeps": [
532
+ "core"
533
+ ],
534
+ "peerDeps": {},
535
+ "optionalPeerDeps": {},
536
+ "files": [
537
+ {
538
+ "path": "dev-mcp/index.ts",
539
+ "contents": "/**\n * @hyper/dev-mcp — dev-time MCP surface mounted at /.hyper/mcp.\n *\n * Zero prod exposure: the plugin becomes a no-op unless `enabled: true`\n * or `NODE_ENV !== \"production\"`. `hyper build` strips it automatically\n * because the plugin short-circuits to an empty object in production\n * envs (see `devMcpPlugin`).\n */\n\nexport { buildTools, devMcpPlugin, DevRecorder } from \"./plugin.ts\"\nexport type { DevMcpConfig, DevTool, RecordedError, RecordedRequest } from \"./plugin.ts\"\n",
540
+ "sha256": "14bc3f2bea70c600399bfc96afaf99ddd0f8c16b023542806f9100eff1b88e44"
541
+ },
542
+ {
543
+ "path": "dev-mcp/plugin.ts",
544
+ "contents": "/**\n * @hyper/dev-mcp — localhost-only MCP server embedded under /.hyper/mcp.\n *\n * app({ plugins: [devMcpPlugin({ enabled: process.env.NODE_ENV !== \"production\" })] })\n *\n * Safety:\n * - Hard-disabled unless enabled=true (or NODE_ENV !== \"production\").\n * - Denies requests not coming from loopback.\n * - Never projects routes tagged `meta.internal: true`.\n */\n\nimport type { HyperApp, HyperPlugin } from \"@hyper/core\"\nimport { DevRecorder, type RecordedError, type RecordedRequest } from \"./recorder.ts\"\nimport { type DevTool, buildTools } from \"./tools.ts\"\n\nexport { DevRecorder } from \"./recorder.ts\"\nexport type { RecordedError, RecordedRequest } from \"./recorder.ts\"\nexport { buildTools } from \"./tools.ts\"\nexport type { DevTool } from \"./tools.ts\"\n\nexport interface DevMcpConfig {\n readonly enabled?: boolean\n /** URL path the dev MCP server lives at. Default: /.hyper/mcp */\n readonly path?: string\n /** Extra IP prefixes permitted in addition to 127.* and ::1. */\n readonly allowedHosts?: readonly string[]\n readonly recorder?: DevRecorder\n}\n\nconst LOOPBACK = new Set([\"127.0.0.1\", \"::1\", \"localhost\"])\n\nexport function devMcpPlugin(config: DevMcpConfig = {}): HyperPlugin {\n const enabled = config.enabled ?? process.env.NODE_ENV !== \"production\"\n const base = config.path ?? \"/.hyper/mcp\"\n const recorder = config.recorder ?? new DevRecorder()\n const allowed = new Set([...LOOPBACK, ...(config.allowedHosts ?? [])])\n let tools: readonly DevTool[] = []\n let appRef: HyperApp | undefined\n\n const requestIds = new WeakMap<Request, string>()\n const startTimes = new WeakMap<Request, number>()\n\n return {\n name: \"@hyper/dev-mcp\",\n build(app) {\n if (!enabled) return\n appRef = app\n tools = buildTools(app, recorder)\n },\n request: {\n async preRoute({ req }) {\n if (!enabled) return\n const url = new URL(req.url)\n if (url.pathname !== base) return\n if (!isLocal(req, allowed)) {\n return new Response(JSON.stringify({ error: \"forbidden\" }), {\n status: 403,\n headers: { \"content-type\": \"application/json\" },\n })\n }\n if (req.method !== \"POST\") {\n return new Response(JSON.stringify({ error: \"method_not_allowed\" }), {\n status: 405,\n headers: { \"content-type\": \"application/json\" },\n })\n }\n const body = (await req.json().catch(() => null)) as JsonRpc | null\n if (!body || body.jsonrpc !== \"2.0\" || typeof body.method !== \"string\") {\n return json({ jsonrpc: \"2.0\", id: null, error: { code: -32600, message: \"bad request\" } })\n }\n return handle(body, tools)\n },\n before({ req }) {\n if (!enabled) return\n requestIds.set(req, crypto.randomUUID())\n startTimes.set(req, performance.now())\n },\n async after({ req, res, route }) {\n if (!enabled || !appRef) return\n const url = new URL(req.url)\n if (url.pathname === base) return\n if (route?.meta.internal) return\n recorder.push({\n id: requestIds.get(req) ?? crypto.randomUUID(),\n method: req.method,\n path: url.pathname,\n route: route?.path,\n status: res.status,\n durationMs: performance.now() - (startTimes.get(req) ?? performance.now()),\n startedAt: Date.now(),\n headers: Object.fromEntries(req.headers.entries()),\n query: Object.fromEntries(url.searchParams.entries()),\n ...(req.body\n ? {\n body: await req\n .clone()\n .text()\n .catch(() => \"\"),\n }\n : {}),\n })\n },\n onError({ req, error, route }) {\n if (!enabled) return\n const url = new URL(req.url)\n recorder.pushError({\n id: requestIds.get(req) ?? crypto.randomUUID(),\n method: req.method,\n path: url.pathname,\n route: route?.path,\n at: Date.now(),\n message: error instanceof Error ? error.message : String(error),\n ...(error instanceof Error && error.stack ? { stack: error.stack } : {}),\n })\n },\n },\n }\n}\n\ninterface JsonRpc {\n readonly jsonrpc: \"2.0\"\n readonly id?: string | number | null\n readonly method: string\n readonly params?: unknown\n}\n\nasync function handle(rpc: JsonRpc, tools: readonly DevTool[]): Promise<Response> {\n switch (rpc.method) {\n case \"initialize\":\n return json({\n jsonrpc: \"2.0\",\n id: rpc.id ?? null,\n result: {\n protocolVersion: \"2024-11-05\",\n capabilities: { tools: {} },\n serverInfo: { name: \"hyper-dev\", version: \"0.0.0\" },\n },\n })\n case \"tools/list\":\n return json({\n jsonrpc: \"2.0\",\n id: rpc.id ?? null,\n result: {\n tools: tools.map((t) => ({\n name: t.name,\n description: t.description,\n inputSchema: t.input,\n })),\n },\n })\n case \"tools/call\": {\n const p = rpc.params as { name: string; arguments?: Record<string, unknown> } | undefined\n if (!p?.name) {\n return json({\n jsonrpc: \"2.0\",\n id: rpc.id ?? null,\n error: { code: -32602, message: \"missing tool name\" },\n })\n }\n const tool = tools.find((t) => t.name === p.name)\n if (!tool) {\n return json({\n jsonrpc: \"2.0\",\n id: rpc.id ?? null,\n error: { code: -32601, message: `unknown tool: ${p.name}` },\n })\n }\n try {\n const value = await tool.call(p.arguments ?? {})\n return json({\n jsonrpc: \"2.0\",\n id: rpc.id ?? null,\n result: {\n content: [{ type: \"text\", text: JSON.stringify(value) }],\n structuredContent: value,\n },\n })\n } catch (e) {\n return json({\n jsonrpc: \"2.0\",\n id: rpc.id ?? null,\n error: {\n code: -32000,\n message: e instanceof Error ? e.message : String(e),\n },\n })\n }\n }\n default:\n return json({\n jsonrpc: \"2.0\",\n id: rpc.id ?? null,\n error: { code: -32601, message: `unknown method: ${rpc.method}` },\n })\n }\n}\n\nfunction json(o: unknown): Response {\n return new Response(JSON.stringify(o), {\n headers: { \"content-type\": \"application/json; charset=utf-8\" },\n })\n}\n\nfunction isLocal(req: Request, allowed: Set<string>): boolean {\n const host = req.headers.get(\"host\")?.split(\":\")[0] ?? \"\"\n if (allowed.has(host)) return true\n // When behind a proxy / inside Bun.serve, fall back to x-forwarded-for.\n const xff = req.headers.get(\"x-forwarded-for\")?.split(\",\")[0]?.trim()\n if (xff && allowed.has(xff)) return true\n return !host || host === \"local\"\n}\n",
545
+ "sha256": "e46356dd9cce05f886cb310a79f5a4d112094dc604bd699d69a3db172862df50"
546
+ },
547
+ {
548
+ "path": "dev-mcp/recorder.ts",
549
+ "contents": "/**\n * Rolling in-memory recorder for recent requests + errors.\n *\n * Used by the dev MCP tools (recent_requests, recent_errors, replay_request).\n * Bounded by a max size so memory stays flat across long dev sessions.\n */\n\nexport interface RecordedRequest {\n readonly id: string\n readonly method: string\n readonly path: string\n readonly status: number\n readonly durationMs: number\n readonly startedAt: number\n readonly route?: string\n readonly headers: Record<string, string>\n readonly query: Record<string, string>\n readonly body?: string\n}\n\nexport interface RecordedError {\n readonly id: string\n readonly method: string\n readonly path: string\n readonly at: number\n readonly message: string\n readonly stack?: string\n readonly route?: string\n}\n\nconst MAX = 200\n\nexport class DevRecorder {\n #requests: RecordedRequest[] = []\n #errors: RecordedError[] = []\n\n push(r: RecordedRequest): void {\n this.#requests.push(r)\n if (this.#requests.length > MAX) this.#requests.shift()\n }\n pushError(e: RecordedError): void {\n this.#errors.push(e)\n if (this.#errors.length > MAX) this.#errors.shift()\n }\n requests(limit = 50): readonly RecordedRequest[] {\n return this.#requests.slice(-limit).reverse()\n }\n errors(limit = 50): readonly RecordedError[] {\n return this.#errors.slice(-limit).reverse()\n }\n find(id: string): RecordedRequest | undefined {\n return this.#requests.find((r) => r.id === id)\n }\n clear(): void {\n this.#requests.length = 0\n this.#errors.length = 0\n }\n}\n",
550
+ "sha256": "4bdaf4390550eb2e14a0af9f2a44ed62f021910eb69b8cfb3573dc61ba64d52c"
551
+ },
552
+ {
553
+ "path": "dev-mcp/tools.ts",
554
+ "contents": "/**\n * Dev MCP tool implementations.\n *\n * Tools expose localhost-only introspection + replay helpers. Internal\n * routes (meta.internal) are never surfaced here.\n */\n\nimport type { HyperApp, Route } from \"@hyper/core\"\nimport type { DevRecorder } from \"./recorder.ts\"\n\nexport interface DevTool {\n readonly name: string\n readonly description: string\n readonly input: Record<string, unknown>\n readonly call: (args: Record<string, unknown>) => Promise<unknown> | unknown\n}\n\nexport function buildTools(app: HyperApp, rec: DevRecorder): readonly DevTool[] {\n return [\n {\n name: \"list_routes\",\n description: \"List every non-internal route in the running app.\",\n input: { type: \"object\", additionalProperties: false },\n call: () =>\n publicRoutes(app).map((r) => ({\n method: r.method,\n path: r.path,\n name: r.meta.name,\n tags: r.meta.tags ?? [],\n deprecated: !!r.meta.deprecated,\n mcp: !!r.meta.mcp,\n })),\n },\n {\n name: \"get_route\",\n description: \"Fetch detailed metadata (params, query, body, examples) for a route.\",\n input: {\n type: \"object\",\n properties: {\n method: { type: \"string\" },\n path: { type: \"string\" },\n },\n required: [\"method\", \"path\"],\n },\n call: (args) => {\n const { method, path } = args as { method: string; path: string }\n const r = publicRoutes(app).find(\n (x) => x.method.toUpperCase() === method.toUpperCase() && x.path === path,\n )\n if (!r) return { error: \"route_not_found\" }\n return {\n method: r.method,\n path: r.path,\n meta: r.meta,\n hasParams: !!r.params,\n hasQuery: !!r.query,\n hasBody: !!r.body,\n hasHeaders: !!r.headers,\n throws: r.throws ? Object.keys(r.throws).map(Number) : [],\n errors: r.errors ? Object.keys(r.errors) : [],\n }\n },\n },\n {\n name: \"recent_requests\",\n description: \"Return the last N handled HTTP requests (default 50).\",\n input: {\n type: \"object\",\n properties: { limit: { type: \"integer\", minimum: 1, maximum: 200 } },\n },\n call: (args) => rec.requests((args?.limit as number) ?? 50),\n },\n {\n name: \"recent_errors\",\n description: \"Return the last N errors captured while handling requests.\",\n input: {\n type: \"object\",\n properties: { limit: { type: \"integer\", minimum: 1, maximum: 200 } },\n },\n call: (args) => rec.errors((args?.limit as number) ?? 50),\n },\n {\n name: \"invoke_route\",\n description:\n \"Invoke a route in-process (no network). Same path as HTTP — runs middleware, validators, handler.\",\n input: {\n type: \"object\",\n properties: {\n method: { type: \"string\" },\n path: { type: \"string\" },\n params: { type: \"object\", additionalProperties: true },\n query: { type: \"object\", additionalProperties: true },\n body: {},\n headers: { type: \"object\", additionalProperties: { type: \"string\" } },\n },\n required: [\"method\", \"path\"],\n },\n call: async (args) => {\n const { method, path, params, query, body, headers } = args as {\n method: string\n path: string\n params?: Record<string, string>\n query?: Record<string, unknown>\n body?: unknown\n headers?: Record<string, string>\n }\n return app.invoke({\n method: method.toUpperCase() as \"GET\",\n path,\n ...(params && { params }),\n ...(query && { query }),\n ...(body !== undefined && { body }),\n ...(headers && { headers }),\n })\n },\n },\n {\n name: \"replay_request\",\n description: \"Replay a previously recorded request by id (dev-only).\",\n input: {\n type: \"object\",\n properties: { id: { type: \"string\" } },\n required: [\"id\"],\n },\n call: async (args) => {\n const { id } = args as { id: string }\n const r = rec.find(id)\n if (!r) return { error: \"request_not_found\" }\n return app.invoke({\n method: r.method as \"GET\",\n path: r.path,\n query: Object.fromEntries(Object.entries(r.query)),\n headers: r.headers,\n ...(r.body !== undefined && {\n body: safeParseJson(r.body),\n }),\n })\n },\n },\n ]\n}\n\nfunction publicRoutes(app: HyperApp): readonly Route[] {\n return app.routeList.filter((r) => !r.meta.internal)\n}\n\nfunction safeParseJson(s: string): unknown {\n try {\n return JSON.parse(s)\n } catch {\n return s\n }\n}\n",
555
+ "sha256": "d196b3706d12526022f26805cb959627eb88c0a5f201b0f2a20f71f94de107a8"
556
+ }
557
+ ],
558
+ "subpaths": {}
559
+ },
560
+ "idempotency": {
561
+ "name": "idempotency",
562
+ "version": "0.1.0",
563
+ "description": "Idempotency-Key middleware — one-shot result caching for mutating requests.",
564
+ "readme": "# @hyper/idempotency\n\n`Idempotency-Key` middleware — one-shot result caching for mutating requests.\n\n## Install\n\n```bash\nbun add @hyper/idempotency\n```\n\n## Usage\n\n```ts\nimport { Hyper } from \"@hyper/core\"\nimport { idempotency } from \"@hyper/idempotency\"\n\nexport default new Hyper()\n .use(idempotency())\n .listen(3000)\n```\n\n## Docs\n\nSee the [main README](../../README.md) and [docs/](../../docs) for guides and integration recipes.\n\n## License\n\nMIT\n",
565
+ "registryDeps": [
566
+ "core"
567
+ ],
568
+ "peerDeps": {},
569
+ "optionalPeerDeps": {},
570
+ "files": [
571
+ {
572
+ "path": "idempotency/index.ts",
573
+ "contents": "/**\n * @hyper/idempotency — Idempotency-Key middleware.\n *\n * RFC-aligned behavior:\n * - If request has `Idempotency-Key`, we hash (key + method + path + body)\n * and cache the response for `ttlMs`.\n * - Replays within the TTL return the cached response (with\n * `Idempotent-Replayed: true` header).\n * - Concurrent requests for the same key serialize via a short lock.\n *\n * This keeps consumers safe from retried PUT/POSTs at the edge.\n */\n\nimport { type Middleware, coerce } from \"@hyper/core\"\nimport { type IdempotencyStore, memoryStore } from \"./store.ts\"\n\nexport { memoryStore } from \"./store.ts\"\nexport type { CachedResponse, IdempotencyStore } from \"./store.ts\"\n\nexport interface IdempotencyConfig {\n readonly store?: IdempotencyStore\n /** Default: 24h. */\n readonly ttlMs?: number\n /** Default: [\"POST\", \"PUT\", \"PATCH\", \"DELETE\"]. */\n readonly methods?: readonly string[]\n}\n\nconst DEFAULT_METHODS = new Set([\"POST\", \"PUT\", \"PATCH\", \"DELETE\"])\nconst DAY = 24 * 60 * 60 * 1000\n\nexport function idempotency(config: IdempotencyConfig = {}): Middleware {\n const store = config.store ?? memoryStore()\n const ttl = config.ttlMs ?? DAY\n const methods = new Set(config.methods ?? [...DEFAULT_METHODS])\n\n return async ({ req, path, next }) => {\n if (!methods.has(req.method)) return next()\n const key = req.headers.get(\"idempotency-key\")\n if (!key) return next()\n const cacheKey = await hash(`${key}|${req.method}|${path}|${await peekBody(req)}`)\n\n const cached = await store.get(cacheKey)\n if (cached) return replay(cached)\n\n const locked = await store.lock(cacheKey, 30_000)\n if (!locked) {\n // Another in-flight request holds this key — conservative: 409.\n return new Response(JSON.stringify({ error: \"idempotency_in_flight\", idempotencyKey: key }), {\n status: 409,\n headers: { \"content-type\": \"application/json\" },\n })\n }\n\n try {\n const out = await next()\n const res = out instanceof Response ? out : coerce(out)\n const body = await res.clone().text()\n const headers: Record<string, string> = {}\n for (const [k, v] of res.headers) headers[k] = v\n await store.set(cacheKey, { status: res.status, headers, body, createdAt: Date.now() }, ttl)\n return res\n } finally {\n await store.unlock(cacheKey)\n }\n }\n}\n\nfunction replay(c: {\n readonly status: number\n readonly headers: Record<string, string>\n readonly body: string\n}) {\n const h = new Headers(c.headers)\n h.set(\"idempotent-replayed\", \"true\")\n return new Response(c.body, { status: c.status, headers: h })\n}\n\nasync function peekBody(req: Request): Promise<string> {\n if (!req.body) return \"\"\n // Hash-only peek — we deliberately consume into a cloned Request so\n // downstream handlers still see the original stream.\n try {\n return await req.clone().text()\n } catch {\n return \"\"\n }\n}\n\nasync function hash(s: string): Promise<string> {\n const buf = new TextEncoder().encode(s)\n const digest = await crypto.subtle.digest(\"SHA-256\", buf)\n return Array.from(new Uint8Array(digest))\n .map((b) => b.toString(16).padStart(2, \"0\"))\n .join(\"\")\n}\n",
574
+ "sha256": "2dacbe196c26c16ec748ac6b07396257e89ac5840f98506480f5a10acc2a5380"
575
+ },
576
+ {
577
+ "path": "idempotency/sqlite.ts",
578
+ "contents": "/**\n * bun:sqlite-backed IdempotencyStore — persistent across process restarts\n * and suitable for single-node production. For multi-node, use Redis.\n */\n\nimport { Database } from \"bun:sqlite\"\nimport type { CachedResponse, IdempotencyStore } from \"./store.ts\"\n\nexport interface SqliteIdempotencyOptions {\n readonly path?: string\n}\n\nexport function sqliteIdempotency(opts: SqliteIdempotencyOptions = {}): IdempotencyStore & {\n readonly sweep: () => void\n readonly close: () => void\n} {\n const path = opts.path ?? \"./.hyper/idempotency.sqlite\"\n const db = new Database(path, { create: true })\n db.exec(\"PRAGMA journal_mode = WAL; PRAGMA synchronous = NORMAL;\")\n db.exec(`\n CREATE TABLE IF NOT EXISTS hyper_idempotency (\n k TEXT PRIMARY KEY,\n status INTEGER NOT NULL,\n headers TEXT NOT NULL,\n body TEXT NOT NULL,\n created_at INTEGER NOT NULL,\n expires_at INTEGER NOT NULL\n );\n CREATE TABLE IF NOT EXISTS hyper_idempotency_locks (\n k TEXT PRIMARY KEY,\n expires_at INTEGER NOT NULL\n );\n `)\n const now = () => Date.now()\n const stmtGet = db.prepare(\n \"SELECT status, headers, body, created_at AS createdAt, expires_at AS expiresAt FROM hyper_idempotency WHERE k = ?\",\n )\n const stmtSet = db.prepare(\n \"INSERT OR REPLACE INTO hyper_idempotency (k, status, headers, body, created_at, expires_at) VALUES (?,?,?,?,?,?)\",\n )\n const stmtLockGet = db.prepare(\n \"SELECT expires_at AS expiresAt FROM hyper_idempotency_locks WHERE k = ?\",\n )\n const stmtLockSet = db.prepare(\n \"INSERT OR REPLACE INTO hyper_idempotency_locks (k, expires_at) VALUES (?, ?)\",\n )\n const stmtLockDel = db.prepare(\"DELETE FROM hyper_idempotency_locks WHERE k = ?\")\n const stmtSweep = db.prepare(\"DELETE FROM hyper_idempotency WHERE expires_at < ?\")\n const stmtSweepLocks = db.prepare(\"DELETE FROM hyper_idempotency_locks WHERE expires_at < ?\")\n\n const sweep = () => {\n const t = now()\n stmtSweep.run(t)\n stmtSweepLocks.run(t)\n }\n\n return {\n async get(key) {\n const row = stmtGet.get(key) as\n | { status: number; headers: string; body: string; createdAt: number; expiresAt: number }\n | undefined\n if (!row) return undefined\n if (row.expiresAt < now()) return undefined\n return {\n status: row.status,\n headers: JSON.parse(row.headers) as Record<string, string>,\n body: row.body,\n createdAt: row.createdAt,\n } satisfies CachedResponse\n },\n async set(key, value, ttlMs) {\n stmtSet.run(\n key,\n value.status,\n JSON.stringify(value.headers),\n value.body,\n value.createdAt,\n now() + ttlMs,\n )\n },\n async lock(key, ttlMs) {\n const row = stmtLockGet.get(key) as { expiresAt: number } | undefined\n if (row && row.expiresAt >= now()) return false\n stmtLockSet.run(key, now() + ttlMs)\n return true\n },\n async unlock(key) {\n stmtLockDel.run(key)\n },\n sweep,\n close: () => db.close(),\n }\n}\n",
579
+ "sha256": "b87abeaf293f180269aecdde1e1bebae166fe2ec806eee1cd8e6fb4fe25cd1e7"
580
+ },
581
+ {
582
+ "path": "idempotency/store.ts",
583
+ "contents": "/**\n * IdempotencyStore — pluggable cache backend.\n *\n * We ship an in-memory TTL store by default. Production deployments\n * bind a Redis/KeyDB adapter via `.store(...)`.\n */\n\nexport interface CachedResponse {\n readonly status: number\n readonly headers: Record<string, string>\n readonly body: string\n readonly createdAt: number\n}\n\nexport interface IdempotencyStore {\n get(key: string): Promise<CachedResponse | undefined>\n set(key: string, value: CachedResponse, ttlMs: number): Promise<void>\n /** Returns true if the key was *newly* locked. Used for request-in-flight races. */\n lock(key: string, ttlMs: number): Promise<boolean>\n unlock(key: string): Promise<void>\n}\n\nexport function memoryStore(): IdempotencyStore {\n const data = new Map<string, { value: CachedResponse; expires: number }>()\n const locks = new Map<string, number>()\n const now = () => Date.now()\n return {\n async get(key) {\n const v = data.get(key)\n if (!v) return undefined\n if (v.expires < now()) {\n data.delete(key)\n return undefined\n }\n return v.value\n },\n async set(key, value, ttlMs) {\n data.set(key, { value, expires: now() + ttlMs })\n },\n async lock(key, ttlMs) {\n const existing = locks.get(key)\n if (existing && existing >= now()) return false\n locks.set(key, now() + ttlMs)\n return true\n },\n async unlock(key) {\n locks.delete(key)\n },\n }\n}\n",
584
+ "sha256": "acc224c4c2d63185e1d1762e5da222f8c094ad22d2e12d1b3a1f0d5ca3408e75"
585
+ }
586
+ ],
587
+ "subpaths": {}
588
+ },
589
+ "log": {
590
+ "name": "log",
591
+ "version": "0.1.0",
592
+ "description": "Wide-event structured logger for Hyper — the reference plugin.",
593
+ "readme": "# @hyper/log\n\nWide-event structured logger for Hyper — the reference plugin.\n\nOne log event per request, attached to `ctx.log`. Pluggable drains (stdout, file, Axiom, memory, BYO). Secrets redacted by default.\n\n## Install\n\n```bash\nbun add @hyper/log\n```\n\n## Usage\n\n```ts\nimport { Hyper } from \"@hyper/core\"\nimport { hyperLog } from \"@hyper/log\"\n\nexport default new Hyper()\n .use(hyperLog({ service: \"orders\" }))\n .get(\"/health\", ({ ctx }) => {\n ctx.log.event(\"health.check\", { ok: true })\n return { ok: true }\n })\n .listen(3000)\n```\n\n## Docs\n\nSee the [main README](../../README.md) and [docs/](../../docs) for guides and integration recipes.\n\n## License\n\nMIT\n",
594
+ "registryDeps": [
595
+ "core"
596
+ ],
597
+ "peerDeps": {},
598
+ "optionalPeerDeps": {},
599
+ "files": [
600
+ {
601
+ "path": "log/ai.ts",
602
+ "contents": "/**\n * AI SDK wrapper — emits one event per generateText/streamText call with\n * token counts, model, latency, and a hash of the prompt (not the content).\n *\n * Usage:\n * const wrappedModel = wrapAiModel(openai(\"gpt-4o-mini\"), () => ctx.log)\n *\n * We stay deliberately structural: we don't import `ai` to keep peer deps\n * optional. Users pass their model-like object; we proxy the `doGenerate`\n * / `doStream` calls.\n */\n\nimport type { LogBuilder } from \"./types.ts\"\n\ntype GetLog = () => LogBuilder | undefined\n\ninterface AiModelLike {\n readonly modelId?: string\n readonly provider?: string\n // biome-ignore lint/suspicious/noExplicitAny: AI SDK provider types vary\n doGenerate?: (...args: any[]) => Promise<any>\n // biome-ignore lint/suspicious/noExplicitAny: AI SDK provider types vary\n doStream?: (...args: any[]) => Promise<any>\n}\n\nexport function wrapAiModel<M extends AiModelLike>(model: M, getLog: GetLog): M {\n const base = {\n provider: model.provider,\n modelId: model.modelId,\n }\n\n // biome-ignore lint/suspicious/noExplicitAny: see above\n const wrap = (fn: (...a: any[]) => Promise<any>, kind: \"generate\" | \"stream\") => {\n // biome-ignore lint/suspicious/noExplicitAny: dynamic dispatch\n return async (...args: any[]) => {\n const start = performance.now()\n const log = getLog()\n try {\n const out = await fn.apply(model, args)\n const usage = (out as { usage?: { promptTokens?: number; completionTokens?: number } })\n ?.usage\n log\n ?.child(\"ai\")\n .set({\n ...base,\n kind,\n took_ms: performance.now() - start,\n prompt_tokens: usage?.promptTokens,\n completion_tokens: usage?.completionTokens,\n })\n .finish()\n return out\n } catch (e) {\n log\n ?.child(\"ai\")\n .set({\n ...base,\n kind,\n took_ms: performance.now() - start,\n err: String(e),\n })\n .level(\"error\")\n .finish()\n throw e\n }\n }\n }\n\n return new Proxy(model, {\n get(target, prop, receiver) {\n const v = Reflect.get(target, prop, receiver)\n if (prop === \"doGenerate\" && typeof v === \"function\") {\n // biome-ignore lint/suspicious/noExplicitAny: see file header\n return wrap(v as (...a: any[]) => Promise<any>, \"generate\")\n }\n if (prop === \"doStream\" && typeof v === \"function\") {\n // biome-ignore lint/suspicious/noExplicitAny: see file header\n return wrap(v as (...a: any[]) => Promise<any>, \"stream\")\n }\n return v\n },\n })\n}\n",
603
+ "sha256": "e950952c86184e6215a5efeac63c6a7445761440893cb0164ad3af9f762ec320"
604
+ },
605
+ {
606
+ "path": "log/builder.ts",
607
+ "contents": "/**\n * LogBuilder — one per request by default. Collects wide-event fields\n * and emits exactly one event when `finish()` is called.\n *\n * Child builders emit their own events but inherit parent fields.\n */\n\nimport type { LogBuilder, LogDrain, LogEvent, LogLevel } from \"./types.ts\"\n\nconst LEVEL_ORDER: Record<LogLevel, number> = {\n trace: 10,\n debug: 20,\n info: 30,\n warn: 40,\n error: 50,\n fatal: 60,\n}\n\nexport interface BuilderOptions {\n readonly drains: readonly LogDrain[]\n readonly minLevel: LogLevel\n readonly clock: () => number\n readonly render: (event: LogEvent) => LogEvent\n readonly sample: (event: LogEvent) => boolean\n readonly parentFields?: Record<string, unknown>\n readonly scope?: string\n}\n\nexport function createLogBuilder(opts: BuilderOptions): LogBuilder {\n const fields: Record<string, unknown> = { ...(opts.parentFields ?? {}) }\n let hintLevel: LogLevel = \"info\"\n let finished = false\n if (opts.scope) fields.scope = opts.scope\n\n const emit = (msg: string | undefined): void => {\n if (finished) return\n finished = true\n const level = hintLevel\n if (LEVEL_ORDER[level] < LEVEL_ORDER[opts.minLevel]) return\n const event: LogEvent = opts.render({\n ts: new Date(opts.clock()).toISOString(),\n level,\n ...(msg !== undefined ? { msg } : {}),\n ...fields,\n })\n if (!opts.sample(event)) return\n for (const d of opts.drains) {\n try {\n const p = d.write(event)\n if (p && typeof (p as Promise<void>).catch === \"function\") {\n ;(p as Promise<void>).catch(() => {\n // Swallow drain errors to protect the request path.\n })\n }\n } catch {\n // Drains must not break the request pipeline.\n }\n }\n }\n\n const builder: LogBuilder = {\n set(f) {\n Object.assign(fields, f)\n return builder\n },\n level(l) {\n hintLevel = l\n return builder\n },\n child(scope) {\n return createLogBuilder({\n drains: opts.drains,\n minLevel: opts.minLevel,\n clock: opts.clock,\n render: opts.render,\n sample: opts.sample,\n parentFields: { ...fields },\n scope,\n })\n },\n finish(msg) {\n emit(msg)\n },\n info(msg, f) {\n if (f) Object.assign(fields, f)\n hintLevel = \"info\"\n emit(msg)\n },\n warn(msg, f) {\n if (f) Object.assign(fields, f)\n hintLevel = \"warn\"\n emit(msg)\n },\n error(msg, f) {\n if (f) Object.assign(fields, f)\n hintLevel = \"error\"\n emit(msg)\n },\n }\n return builder\n}\n",
608
+ "sha256": "0997403fe6a7196a37ccaf409593b00038d0d365c4369752f8dfd24fd6563cbe"
609
+ },
610
+ {
611
+ "path": "log/bun-sql.ts",
612
+ "contents": "/**\n * Bun.sql template-tag wrapper that emits query events.\n *\n * Usage:\n * import { sql as raw } from \"bun\"\n * import { wrapBunSql } from \"@hyper/log/bun-sql\"\n * const sql = wrapBunSql(raw, () => ctx.log)\n */\n\nimport type { LogBuilder } from \"./types.ts\"\n\ntype GetLog = () => LogBuilder | undefined\n// biome-ignore lint/suspicious/noExplicitAny: Bun.sql is a template tag with many shapes\ntype BunSql = (strings: TemplateStringsArray, ...values: any[]) => Promise<unknown>\n\nexport function wrapBunSql(sql: BunSql, getLog: GetLog): BunSql {\n // biome-ignore lint/suspicious/noExplicitAny: see above\n return ((strings: TemplateStringsArray, ...values: any[]): Promise<unknown> => {\n const start = performance.now()\n const log = getLog()\n return sql(strings, ...values).then(\n (out) => {\n log\n ?.child(\"db.query\")\n .set({ took_ms: performance.now() - start })\n .finish()\n return out\n },\n (err) => {\n log\n ?.child(\"db.query\")\n .set({ took_ms: performance.now() - start, err: String(err) })\n .level(\"error\")\n .finish()\n throw err\n },\n )\n }) as BunSql\n}\n",
613
+ "sha256": "276bf4a3471fb8b7e55f65e66e0d3826f8686f42a3bee0c6650ae30d1f479e58"
614
+ },
615
+ {
616
+ "path": "log/drains.ts",
617
+ "contents": "/**\n * Built-in drains.\n *\n * - `stdoutDrain()` — NDJSON to stdout (zero-dep, default).\n * - `memoryDrain()` — collects events for tests.\n * - `fileDrain(path)` — appends NDJSON lines to a file via Bun.file.\n * - `axiomDrain({ dataset, token })` — batched HTTP POST to Axiom.\n *\n * Drains are fire-and-forget. Errors are swallowed by the builder.\n */\n\nimport type { LogDrain, LogEvent } from \"./types.ts\"\n\nexport function stdoutDrain(): LogDrain {\n const encoder = new TextEncoder()\n return {\n name: \"stdout\",\n write(event) {\n const line = `${JSON.stringify(event)}\\n`\n if (typeof Bun !== \"undefined\" && typeof Bun.write === \"function\") {\n Bun.write(Bun.stdout, line).catch(() => {})\n } else {\n process.stdout.write(encoder.encode(line))\n }\n },\n }\n}\n\nexport function memoryDrain(): LogDrain & { readonly events: LogEvent[] } {\n const events: LogEvent[] = []\n return {\n name: \"memory\",\n events,\n write(event) {\n events.push(event)\n },\n }\n}\n\nexport function fileDrain(path: string): LogDrain {\n return {\n name: `file:${path}`,\n async write(event) {\n if (typeof Bun === \"undefined\") return\n const file = Bun.file(path)\n const writer = file.writer()\n writer.write(`${JSON.stringify(event)}\\n`)\n await writer.end()\n },\n }\n}\n\nexport interface AxiomDrainConfig {\n readonly dataset: string\n readonly token: string\n readonly endpoint?: string\n readonly batchSize?: number\n readonly flushIntervalMs?: number\n}\n\nexport function axiomDrain(cfg: AxiomDrainConfig): LogDrain {\n const endpoint = cfg.endpoint ?? `https://api.axiom.co/v1/datasets/${cfg.dataset}/ingest`\n const batchSize = cfg.batchSize ?? 100\n const flushMs = cfg.flushIntervalMs ?? 1000\n let buf: LogEvent[] = []\n let timer: ReturnType<typeof setTimeout> | null = null\n\n const flush = async (): Promise<void> => {\n if (buf.length === 0) return\n const batch = buf\n buf = []\n if (timer) {\n clearTimeout(timer)\n timer = null\n }\n try {\n await fetch(endpoint, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/x-ndjson\",\n authorization: `Bearer ${cfg.token}`,\n },\n body: batch.map((e) => JSON.stringify(e)).join(\"\\n\"),\n })\n } catch {\n // Drop on network error; logs are best-effort.\n }\n }\n\n return {\n name: `axiom:${cfg.dataset}`,\n write(event) {\n buf.push(event)\n if (buf.length >= batchSize) {\n flush()\n return\n }\n if (!timer) timer = setTimeout(flush, flushMs)\n },\n flush,\n close: flush,\n }\n}\n",
618
+ "sha256": "d5455d3c7223b927eeb6e1514493297df31801c710a3d75cf75c8ccecf3180aa"
619
+ },
620
+ {
621
+ "path": "log/drizzle.ts",
622
+ "contents": "/**\n * Query-event helper for Drizzle. Not a hard dependency — users wire it\n * themselves with their own drizzle instance.\n *\n * Usage:\n * import { drizzle } from \"drizzle-orm/bun-sqlite\"\n * import { wrapDrizzle } from \"@hyper/log/drizzle\"\n * const db = wrapDrizzle(drizzle(...), () => ctx.log)\n */\n\nimport type { LogBuilder } from \"./types.ts\"\n\ntype GetLog = () => LogBuilder | undefined\n\n/**\n * Minimal interface so we don't depend on drizzle-orm types.\n * Works against any object exposing an `execute(query)` or similar.\n */\ninterface ExecutableDb {\n // biome-ignore lint/suspicious/noExplicitAny: user's drizzle instance\n execute?: (q: unknown, ...rest: any[]) => Promise<unknown>\n // biome-ignore lint/suspicious/noExplicitAny: user's drizzle instance\n run?: (q: unknown, ...rest: any[]) => Promise<unknown>\n}\n\nexport function wrapDrizzle<Db extends ExecutableDb>(db: Db, getLog: GetLog): Db {\n const hook = async (method: \"execute\" | \"run\", q: unknown, rest: unknown[]) => {\n const start = performance.now()\n const log = getLog()\n try {\n // biome-ignore lint/suspicious/noExplicitAny: dynamic dispatch\n const out = await (db[method] as any).call(db, q, ...rest)\n log\n ?.child(\"db.query\")\n .set({ method, took_ms: performance.now() - start })\n .finish()\n return out\n } catch (e) {\n log\n ?.child(\"db.query\")\n .set({ method, took_ms: performance.now() - start, err: String(e) })\n .level(\"error\")\n .finish()\n throw e\n }\n }\n const patched = { ...db }\n if (typeof db.execute === \"function\") {\n // biome-ignore lint/suspicious/noExplicitAny: preserving user's types\n ;(patched as any).execute = (q: unknown, ...rest: any[]) => hook(\"execute\", q, rest)\n }\n if (typeof db.run === \"function\") {\n // biome-ignore lint/suspicious/noExplicitAny: preserving user's types\n ;(patched as any).run = (q: unknown, ...rest: any[]) => hook(\"run\", q, rest)\n }\n return patched as Db\n}\n",
623
+ "sha256": "ae1289c26d1634589837770fcb62a790279aa9aa152c098a0c6a43538a7d685b"
624
+ },
625
+ {
626
+ "path": "log/index.ts",
627
+ "contents": "/**\n * @hyper/log — wide-event structured logger.\n *\n * Straight to the point:\n * - One log event per request (wide events), attached to `ctx.log`.\n * - Drains: stdout (default), file, axiom, memory (tests). BYO is easy.\n * - Redacts secrets by default; `secret()` marks env fields for auto-redaction.\n * - No runtime deps besides @hyper/core peer.\n */\n\nexport { createLogBuilder } from \"./builder.ts\"\nexport { wrapQueries } from \"./wrap-queries.ts\"\nexport { axiomDrain, fileDrain, memoryDrain, stdoutDrain } from \"./drains.ts\"\nexport type { AxiomDrainConfig } from \"./drains.ts\"\nexport { hyperLog } from \"./plugin.ts\"\nexport type { HyperLogPluginConfig } from \"./plugin.ts\"\nexport { DEFAULT_REDACT, redact } from \"./redact.ts\"\nexport type { LogBuilder, LogConfig, LogDrain, LogEvent, LogLevel } from \"./types.ts\"\n",
628
+ "sha256": "89a89ee726231db8fc3861eae468ddecd1907c0a055083a00c2de3e9a17e94e2"
629
+ },
630
+ {
631
+ "path": "log/plugin.ts",
632
+ "contents": "/**\n * hyperLog({ ... }) — the reference plugin.\n *\n * Wires a per-request LogBuilder into `ctx.log` via the plugin protocol:\n *\n * - `context()` — nothing (we want per-request state, not a singleton)\n * - `request.before` — constructs a LogBuilder and attaches it to ctx.log\n * - `request.after` — finishes the event with status/duration/route\n * - `request.onError` — finishes the event at `error` level with why/fix\n *\n * Request correlation: we hook `ctx.log` directly on the AppContext object.\n * Because plugins run against the same `ctx` for before/after/onError, this\n * is safe without AsyncLocalStorage (which is reserved for useEnv()).\n */\n\nimport type { HyperPlugin } from \"@hyper/core\"\nimport { createLogBuilder } from \"./builder.ts\"\nimport { stdoutDrain } from \"./drains.ts\"\nimport { DEFAULT_REDACT, redact } from \"./redact.ts\"\nimport type { LogBuilder, LogConfig, LogEvent } from \"./types.ts\"\n\nconst REQUEST_ID_HEADER = \"x-request-id\"\n\nfunction makeId(): string {\n if (typeof crypto !== \"undefined\" && \"randomUUID\" in crypto) return crypto.randomUUID()\n return `r_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`\n}\n\nexport interface HyperLogPluginConfig extends LogConfig {\n /** Tag every event with this service name. */\n service?: string\n /** If true, include `req.headers` (with redaction). Default: false. */\n includeHeaders?: boolean\n}\n\nexport function hyperLog(config: HyperLogPluginConfig = {}): HyperPlugin {\n const drains = config.drains ?? [stdoutDrain()]\n const minLevel = config.level ?? \"info\"\n const clock = config.clock ?? Date.now\n const redactPaths = config.redact ?? DEFAULT_REDACT\n const sampleRate = config.sampleRate ?? 1\n const keep = config.keep\n\n const render = (event: LogEvent): LogEvent => {\n const masked = redact(event, redactPaths) as LogEvent\n if (config.service !== undefined && masked.service === undefined) {\n return { ...masked, service: config.service } as LogEvent\n }\n return masked\n }\n const sample = (event: LogEvent): boolean => {\n if (sampleRate >= 1) return true\n if (keep?.(event)) return true\n return Math.random() < sampleRate\n }\n\n return {\n name: \"@hyper/log\",\n request: {\n before({ req, ctx }) {\n const startedAt = clock()\n const requestId = req.headers.get(REQUEST_ID_HEADER) ?? makeId()\n const log = createLogBuilder({\n drains,\n minLevel,\n clock,\n render,\n sample,\n parentFields: {\n request_id: requestId,\n method: req.method,\n path: new URL(req.url).pathname,\n ...(config.includeHeaders\n ? { headers: Object.fromEntries(req.headers.entries()) }\n : {}),\n },\n })\n const ctxMut = ctx as unknown as {\n log: LogBuilder\n _logStartedAt: number\n _logRequestId: string\n }\n ctxMut.log = log\n ctxMut._logStartedAt = startedAt\n ctxMut._logRequestId = requestId\n },\n after({ ctx, res, route }) {\n const ctxAny = ctx as unknown as {\n log?: LogBuilder\n _logStartedAt?: number\n }\n const log = ctxAny.log\n if (!log) return\n const durMs = ctxAny._logStartedAt ? clock() - ctxAny._logStartedAt : undefined\n log.set({\n status: res.status,\n ...(durMs !== undefined ? { duration_ms: durMs } : {}),\n ...(route ? { route: route.path, method: route.method } : {}),\n })\n const level = res.status >= 500 ? \"error\" : res.status >= 400 ? \"warn\" : \"info\"\n log.level(level).finish(\"request\")\n },\n onError({ ctx, error, route }) {\n const ctxAny = ctx as unknown as {\n log?: LogBuilder\n _logStartedAt?: number\n }\n const log = ctxAny.log\n if (!log) return\n const durMs = ctxAny._logStartedAt ? clock() - ctxAny._logStartedAt : undefined\n const err = error as { message?: string; code?: string; why?: string; fix?: string }\n log.set({\n ...(durMs !== undefined ? { duration_ms: durMs } : {}),\n ...(route ? { route: route.path, method: route.method } : {}),\n err: {\n message: err?.message,\n code: err?.code,\n why: err?.why,\n fix: err?.fix,\n },\n })\n log.level(\"error\").finish(\"request_error\")\n },\n },\n }\n}\n",
633
+ "sha256": "a910daa221daa2b59a6d992307ebb2c901de94866c190a3c64be5976a2716047"
634
+ },
635
+ {
636
+ "path": "log/prisma.ts",
637
+ "contents": "/**\n * Prisma $extends() compatible helper — adds per-query timing events.\n *\n * Usage:\n * const prisma = new PrismaClient().$extends(prismaLogExtension(() => ctx.log))\n */\n\nimport type { LogBuilder } from \"./types.ts\"\n\ntype GetLog = () => LogBuilder | undefined\n\n// biome-ignore lint/suspicious/noExplicitAny: Prisma extension is dynamic by design\nexport function prismaLogExtension(getLog: GetLog): any {\n return {\n name: \"@hyper/log\",\n query: {\n $allOperations: async ({\n model,\n operation,\n query,\n args,\n }: {\n model?: string\n operation: string\n // biome-ignore lint/suspicious/noExplicitAny: Prisma next\n query: (args: any) => Promise<unknown>\n // biome-ignore lint/suspicious/noExplicitAny: Prisma args\n args: any\n }): Promise<unknown> => {\n const start = performance.now()\n const log = getLog()\n try {\n const out = await query(args)\n log\n ?.child(\"db.query\")\n .set({ model, operation, took_ms: performance.now() - start })\n .finish()\n return out\n } catch (e) {\n log\n ?.child(\"db.query\")\n .set({ model, operation, took_ms: performance.now() - start, err: String(e) })\n .level(\"error\")\n .finish()\n throw e\n }\n },\n },\n }\n}\n",
638
+ "sha256": "8ad242c7c1ec9a1637ca8aea3f3098ae5442be96c4a55211526a5da6e9911674"
639
+ },
640
+ {
641
+ "path": "log/redact.ts",
642
+ "contents": "/**\n * Redaction — path-based masking for log events.\n *\n * Performance note: we pre-compile the redact path list into a small set\n * and short-circuit when the set is empty.\n */\n\nconst MASK = \"[REDACTED]\"\n\n/** Default keys that are always masked when encountered. */\nexport const DEFAULT_REDACT: readonly string[] = [\n \"password\",\n \"pass\",\n \"token\",\n \"authorization\",\n \"cookie\",\n \"secret\",\n \"api_key\",\n \"apiKey\",\n \"access_token\",\n \"refresh_token\",\n \"ssn\",\n] as const\n\nexport function redact(value: unknown, paths: readonly string[] = DEFAULT_REDACT): unknown {\n if (value == null) return value\n const set = new Set(paths.map((p) => p.toLowerCase()))\n if (set.size === 0) return value\n return walk(value, set, \"\")\n}\n\nfunction walk(v: unknown, set: Set<string>, prefix: string): unknown {\n if (v == null || typeof v !== \"object\") return v\n if (Array.isArray(v)) return v.map((x, i) => walk(x, set, `${prefix}[${i}]`))\n const out: Record<string, unknown> = {}\n for (const [k, val] of Object.entries(v as Record<string, unknown>)) {\n const path = prefix ? `${prefix}.${k}` : k\n if (set.has(k.toLowerCase()) || set.has(path.toLowerCase())) {\n out[k] = MASK\n } else {\n out[k] = walk(val, set, path)\n }\n }\n return out\n}\n",
643
+ "sha256": "76b3773a3630a34580d508004fffa6af55408d17808f967bc3936dfcd70cf666"
644
+ },
645
+ {
646
+ "path": "log/types.ts",
647
+ "contents": "/**\n * @hyper/log types — the wide-event surface.\n *\n * One log per request is the default. Handlers call `ctx.log.set({...})`\n * to accumulate fields; the request-level event is flushed on response\n * (or error) with all fields rolled up.\n */\n\nexport type LogLevel = \"trace\" | \"debug\" | \"info\" | \"warn\" | \"error\" | \"fatal\"\n\n/** A single log record (one request = one event by default). */\nexport interface LogEvent {\n readonly ts: string\n readonly level: LogLevel\n readonly msg?: string\n readonly [key: string]: unknown\n}\n\n/** A log builder that accumulates fields and emits one event. */\nexport interface LogBuilder {\n /** Merge fields into the current event. Last wins. */\n set(fields: Record<string, unknown>): LogBuilder\n /** Scoped child — emits its own event with a shared prefix. */\n child(scope: string): LogBuilder\n /** Level hint; final level chosen at drain time. */\n level(l: LogLevel): LogBuilder\n /** Manually finish; normally called by the plugin on response. */\n finish(msg?: string): void\n /** Convenience shortcuts. Avoid in hot path — prefer `.set()`. */\n info(msg: string, fields?: Record<string, unknown>): void\n warn(msg: string, fields?: Record<string, unknown>): void\n error(msg: string, fields?: Record<string, unknown>): void\n}\n\n/** Drain — receives rendered events. */\nexport interface LogDrain {\n readonly name: string\n write(event: LogEvent): void | Promise<void>\n flush?(): void | Promise<void>\n close?(): void | Promise<void>\n}\n\nexport interface LogConfig {\n /** One or more drains. Defaults to stdout NDJSON. */\n drains?: readonly LogDrain[]\n /** Minimum level emitted. */\n level?: LogLevel\n /** Paths (dot-path) that are masked in serialized output. */\n redact?: readonly string[]\n /** Sampling: 0..1. Events below the cutoff drop unless `keep()` hits. */\n sampleRate?: number\n /** Predicate: if it returns true, the event is always kept. */\n keep?: (event: LogEvent) => boolean\n /** Injected clock for tests; defaults to Date.now(). */\n clock?: () => number\n}\n",
648
+ "sha256": "cb98e6dc97de8718009d7194eaa776511639d793a096cc3181d7eacb83ec631f"
649
+ },
650
+ {
651
+ "path": "log/wrap-queries.ts",
652
+ "contents": "/**\n * wrapQueries(db, getLog) — a generic helper that records query timing\n * on any object exposing async methods. Works out-of-the-box with Drizzle,\n * Prisma, Bun.sql wrappers, and hand-rolled repositories.\n *\n * The contract is minimal: every method is wrapped; synchronous methods\n * are passed through. Errors are logged at `error` level.\n */\n\nimport type { LogBuilder } from \"./types.ts\"\n\ntype GetLog = () => LogBuilder | undefined\n\n// biome-ignore lint/suspicious/noExplicitAny: user's repo/orm shape is opaque\nexport function wrapQueries<T extends Record<string, any>>(db: T, getLog: GetLog): T {\n return new Proxy(db, {\n get(target, prop, receiver) {\n const v = Reflect.get(target, prop, receiver)\n if (typeof v !== \"function\") return v\n return (...args: unknown[]) => {\n const start = performance.now()\n const label = String(prop)\n try {\n const result = (v as (...a: unknown[]) => unknown).apply(target, args)\n if (result && typeof (result as Promise<unknown>).then === \"function\") {\n return (result as Promise<unknown>).then(\n (out) => {\n getLog()\n ?.child(\"db.query\")\n .set({ method: label, took_ms: performance.now() - start })\n .finish()\n return out\n },\n (err: unknown) => {\n getLog()\n ?.child(\"db.query\")\n .set({ method: label, took_ms: performance.now() - start, err: String(err) })\n .level(\"error\")\n .finish()\n throw err\n },\n )\n }\n getLog()\n ?.child(\"db.query\")\n .set({ method: label, took_ms: performance.now() - start })\n .finish()\n return result\n } catch (err) {\n getLog()\n ?.child(\"db.query\")\n .set({ method: label, took_ms: performance.now() - start, err: String(err) })\n .level(\"error\")\n .finish()\n throw err\n }\n }\n },\n }) as T\n}\n",
653
+ "sha256": "be693b07a8ddaa6fc97fb6a67342f071339bdd58cbbc055574dc46d63056b4b3"
654
+ }
655
+ ],
656
+ "subpaths": {}
657
+ },
658
+ "mcp": {
659
+ "name": "mcp",
660
+ "version": "0.1.0",
661
+ "description": "Model Context Protocol (MCP) adapter for Hyper.",
662
+ "readme": "# @hyper/mcp\n\nModel Context Protocol (MCP) adapter for Hyper — turn any Hyper app into an MCP server.\n\n## Install\n\n```bash\nbun add @hyper/mcp\n```\n\n## Usage\n\n```ts\nimport { Hyper, ok } from \"@hyper/core\"\nimport { mcpServer } from \"@hyper/mcp\"\n\nconst app = new Hyper().get(\"/ping\", () => ok({ pong: true }))\n\nconst server = mcpServer(app)\nBun.serve({ port: 5174, fetch: server.handle })\n```\n\n## Docs\n\nSee the [main README](../../README.md) and [docs/](../../docs) for guides and integration recipes.\n\n## License\n\nMIT\n",
663
+ "registryDeps": [
664
+ "core"
665
+ ],
666
+ "peerDeps": {},
667
+ "optionalPeerDeps": {},
668
+ "files": [
669
+ {
670
+ "path": "mcp/audit.ts",
671
+ "contents": "/**\n * Audit — pretty-print or JSON-dump the MCP-exposed surface, including\n * auth requirements inferred from route meta.\n */\n\nimport type { HyperApp } from \"@hyper/core\"\n\nexport interface AuditReport {\n readonly exposedCount: number\n readonly total: number\n readonly tools: readonly {\n readonly name: string\n readonly description: string\n readonly method: string\n readonly path: string\n readonly requiresAuth: boolean\n }[]\n}\n\nexport function auditMcp(app: HyperApp): AuditReport {\n const manifest = app.toMCPManifest()\n const byPath = new Map(app.routeList.map((r) => [`${r.method} ${r.path}`, r]))\n const tools = manifest.tools.map((t) => {\n const route = byPath.get(`${t.method} ${t.path}`)\n const requiresAuth = Boolean(\n route && (route.meta.authEndpoint || route.meta.tags?.includes(\"auth\")),\n )\n return {\n name: t.name,\n description: t.description,\n method: t.method,\n path: t.path,\n requiresAuth,\n }\n })\n return {\n exposedCount: tools.length,\n total: app.routeList.filter((r) => !r.meta.internal).length,\n tools,\n }\n}\n\nexport function formatAuditHuman(report: AuditReport): string {\n const lines: string[] = []\n lines.push(`MCP surface: ${report.exposedCount}/${report.total} routes exposed\\n`)\n for (const t of report.tools) {\n const auth = t.requiresAuth ? \" [auth]\" : \"\"\n lines.push(` ${t.method.padEnd(6)} ${t.path}${auth}`)\n lines.push(` ${t.description}`)\n }\n return lines.join(\"\\n\")\n}\n",
672
+ "sha256": "6b13a9c2e22e10693248b643646a14f6d7500417e3ce5d797185ac645f1598b7"
673
+ },
674
+ {
675
+ "path": "mcp/index.ts",
676
+ "contents": "/**\n * @hyper/mcp — exposes declared routes over the Model Context Protocol.\n *\n * Usage:\n * const mcp = mcpServer(app)\n * Bun.serve({ port: 5174, fetch: mcp.handle })\n *\n * Routes annotated with `meta.mcp = { description }` are exposed as tools.\n * `hyper mcp --audit` prints the surface before it ships.\n */\n\nexport { auditMcp, formatAuditHuman } from \"./audit.ts\"\nexport type { AuditReport } from \"./audit.ts\"\nexport { mcpServer } from \"./server.ts\"\nexport type { McpServer, McpServerConfig } from \"./server.ts\"\n",
677
+ "sha256": "35444e98257b9d6d2639d1af143f16b75bebe491f69d0f21182cc5debe3334af"
678
+ },
679
+ {
680
+ "path": "mcp/server.ts",
681
+ "contents": "/**\n * Minimal MCP server — JSON-RPC 2.0 over HTTP POST / stdio pipes.\n *\n * We implement the subset needed for tools:\n * - initialize\n * - tools/list\n * - tools/call\n *\n * Anything declared with `meta.mcp = { description }` becomes a tool.\n * Tool invocation funnels through the shared `app.invoke()` path so\n * middleware, logging, and validation run exactly once.\n */\n\nimport type { HttpMethod, HyperApp, MCPManifest } from \"@hyper/core\"\n\nexport interface McpServer {\n readonly handle: (req: Request) => Promise<Response>\n readonly manifest: MCPManifest\n readonly listTools: () => readonly { name: string; description: string }[]\n readonly callTool: (name: string, args: unknown) => Promise<unknown>\n}\n\ninterface JsonRpcRequest {\n readonly jsonrpc: \"2.0\"\n readonly id?: number | string | null\n readonly method: string\n readonly params?: unknown\n}\n\ninterface JsonRpcResponse {\n readonly jsonrpc: \"2.0\"\n readonly id: number | string | null\n readonly result?: unknown\n readonly error?: { code: number; message: string; data?: unknown }\n}\n\nexport interface McpServerConfig {\n /** Override the manifest (usually omitted; taken from app). */\n readonly manifest?: MCPManifest\n /** Require auth check on every tool call. Defaults to always-allow. */\n readonly authorize?: (args: { toolName: string; req: Request }) => boolean | Promise<boolean>\n /** Server identity (surfaced on initialize). */\n readonly info?: { name: string; version: string }\n}\n\nexport function mcpServer(app: HyperApp, cfg: McpServerConfig = {}): McpServer {\n const manifest = cfg.manifest ?? app.toMCPManifest()\n const byName = new Map(manifest.tools.map((t) => [t.name, t]))\n\n const callTool = async (name: string, args: unknown): Promise<unknown> => {\n const tool = byName.get(name)\n if (!tool) throw rpcError(-32601, `unknown tool: ${name}`)\n const input = (args ?? {}) as {\n params?: Record<string, string>\n query?: Record<string, unknown>\n body?: unknown\n }\n const result = await app.invoke({\n method: tool.method as HttpMethod,\n path: tool.path,\n ...(input.params && { params: input.params }),\n ...(input.query && { query: input.query }),\n ...(input.body !== undefined && { body: input.body }),\n })\n if (result.status >= 400) {\n throw rpcError(-32000, `tool failed with ${result.status}`, result.data)\n }\n return result.data\n }\n\n const handle = async (req: Request): Promise<Response> => {\n if (req.method !== \"POST\") {\n return json(\n { jsonrpc: \"2.0\", id: null, error: { code: -32600, message: \"expected POST\" } },\n 405,\n )\n }\n let msg: JsonRpcRequest\n try {\n msg = (await req.json()) as JsonRpcRequest\n } catch {\n return json(\n { jsonrpc: \"2.0\", id: null, error: { code: -32700, message: \"parse error\" } },\n 400,\n )\n }\n try {\n switch (msg.method) {\n case \"initialize\":\n return rpcOk(msg.id ?? null, {\n serverInfo: cfg.info ?? { name: \"hyper-mcp\", version: \"0.0.0\" },\n capabilities: { tools: {} },\n })\n case \"tools/list\":\n return rpcOk(msg.id ?? null, {\n tools: manifest.tools.map((t) => ({\n name: t.name,\n description: t.description,\n inputSchema: t.inputSchema,\n })),\n })\n case \"tools/call\": {\n const params = (msg.params ?? {}) as { name: string; arguments?: unknown }\n if (cfg.authorize) {\n const ok = await cfg.authorize({ toolName: params.name, req })\n if (!ok) {\n return rpcErr(msg.id ?? null, -32001, `unauthorized: ${params.name}`)\n }\n }\n const output = await callTool(params.name, params.arguments)\n return rpcOk(msg.id ?? null, {\n content: [{ type: \"text\", text: JSON.stringify(output) }],\n })\n }\n default:\n return rpcErr(msg.id ?? null, -32601, `method not found: ${msg.method}`)\n }\n } catch (e) {\n const err = e as { code?: number; message?: string; data?: unknown }\n return rpcErr(msg.id ?? null, err.code ?? -32000, err.message ?? \"server error\", err.data)\n }\n }\n\n return {\n handle,\n manifest,\n listTools: () => manifest.tools.map((t) => ({ name: t.name, description: t.description })),\n callTool,\n }\n}\n\nfunction rpcError(\n code: number,\n message: string,\n data?: unknown,\n): Error & {\n code: number\n data?: unknown\n} {\n return Object.assign(new Error(message), { code, data })\n}\n\nfunction rpcOk(id: number | string | null, result: unknown): Response {\n const body: JsonRpcResponse = { jsonrpc: \"2.0\", id, result }\n return json(body, 200)\n}\n\nfunction rpcErr(\n id: number | string | null,\n code: number,\n message: string,\n data?: unknown,\n): Response {\n const body: JsonRpcResponse = {\n jsonrpc: \"2.0\",\n id,\n error: { code, message, ...(data !== undefined && { data }) },\n }\n return json(body, 200)\n}\n\nfunction json(body: unknown, status: number): Response {\n return new Response(JSON.stringify(body), {\n status,\n headers: { \"content-type\": \"application/json\" },\n })\n}\n",
682
+ "sha256": "3008a688528e4b0eab27c5b9ed03225711c5638457a00bfc2343420a45a7eeab"
683
+ }
684
+ ],
685
+ "subpaths": {}
686
+ },
687
+ "msgpack": {
688
+ "name": "msgpack",
689
+ "version": "0.1.0",
690
+ "description": "MessagePack wire format for Hyper — content-negotiated encode/decode.",
691
+ "readme": "# @hyper/msgpack\n\nMessagePack wire format for Hyper — content-negotiated encode/decode.\n\n## Install\n\n```bash\nbun add @hyper/msgpack\n```\n\n## Usage\n\n```ts\nimport { Hyper } from \"@hyper/core\"\nimport { msgpack } from \"@hyper/msgpack\"\n\nexport default new Hyper()\n .use(msgpack())\n .listen(3000)\n```\n\n## Docs\n\nSee the [main README](../../README.md) and [docs/](../../docs) for guides and integration recipes.\n\n## License\n\nMIT\n",
692
+ "registryDeps": [
693
+ "core"
694
+ ],
695
+ "peerDeps": {},
696
+ "optionalPeerDeps": {},
697
+ "files": [
698
+ {
699
+ "path": "msgpack/codec.ts",
700
+ "contents": "/**\n * Minimal MessagePack encoder/decoder.\n *\n * Covers the subset Hyper traffic uses in practice:\n * nil, bool, int (8/16/32/64 signed), float64, string, bin, array, map.\n *\n * Not a drop-in for `@msgpack/msgpack` — no extensions, timestamps, or\n * BigInts. If your payload needs more than this, plug in the full codec\n * via `app({ encoders: { \"application/msgpack\": custom } })`.\n */\n\nexport function encode(value: unknown): Uint8Array {\n const buf: number[] = []\n write(buf, value)\n return Uint8Array.from(buf)\n}\n\nexport function decode(bytes: Uint8Array): unknown {\n const cursor = { i: 0 }\n const v = read(bytes, cursor)\n return v\n}\n\nfunction write(buf: number[], v: unknown): void {\n if (v === null || v === undefined) {\n buf.push(0xc0)\n return\n }\n if (typeof v === \"boolean\") {\n buf.push(v ? 0xc3 : 0xc2)\n return\n }\n if (typeof v === \"number\") {\n if (Number.isInteger(v)) {\n writeInt(buf, v)\n } else {\n writeFloat64(buf, v)\n }\n return\n }\n if (typeof v === \"string\") {\n writeStr(buf, v)\n return\n }\n if (v instanceof Uint8Array) {\n writeBin(buf, v)\n return\n }\n if (Array.isArray(v)) {\n writeArray(buf, v)\n return\n }\n if (typeof v === \"object\") {\n writeMap(buf, v as Record<string, unknown>)\n return\n }\n throw new Error(`msgpack: unsupported type: ${typeof v}`)\n}\n\nfunction writeInt(buf: number[], n: number): void {\n if (n >= 0 && n <= 0x7f) {\n buf.push(n)\n } else if (n < 0 && n >= -0x20) {\n buf.push(n & 0xff)\n } else if (n >= 0 && n <= 0xff) {\n buf.push(0xcc, n)\n } else if (n >= 0 && n <= 0xffff) {\n buf.push(0xcd, (n >> 8) & 0xff, n & 0xff)\n } else if (n >= 0 && n <= 0xffffffff) {\n buf.push(0xce, (n >>> 24) & 0xff, (n >>> 16) & 0xff, (n >>> 8) & 0xff, n & 0xff)\n } else if (n >= -0x80 && n <= 0x7f) {\n buf.push(0xd0, n & 0xff)\n } else if (n >= -0x8000 && n <= 0x7fff) {\n buf.push(0xd1, (n >> 8) & 0xff, n & 0xff)\n } else if (n >= -0x80000000 && n <= 0x7fffffff) {\n buf.push(0xd2, (n >>> 24) & 0xff, (n >>> 16) & 0xff, (n >>> 8) & 0xff, n & 0xff)\n } else {\n // Fallback: encode as float64 — preserves precision up to 2^53.\n writeFloat64(buf, n)\n }\n}\n\nfunction writeFloat64(buf: number[], n: number): void {\n buf.push(0xcb)\n const b = new ArrayBuffer(8)\n new DataView(b).setFloat64(0, n, false)\n const view = new Uint8Array(b)\n for (let i = 0; i < 8; i++) buf.push(view[i]!)\n}\n\nfunction writeStr(buf: number[], s: string): void {\n const bytes = new TextEncoder().encode(s)\n const n = bytes.length\n if (n <= 31) buf.push(0xa0 | n)\n else if (n <= 0xff) buf.push(0xd9, n)\n else if (n <= 0xffff) buf.push(0xda, (n >> 8) & 0xff, n & 0xff)\n else buf.push(0xdb, (n >>> 24) & 0xff, (n >>> 16) & 0xff, (n >>> 8) & 0xff, n & 0xff)\n for (let i = 0; i < bytes.length; i++) buf.push(bytes[i]!)\n}\n\nfunction writeBin(buf: number[], b: Uint8Array): void {\n const n = b.length\n if (n <= 0xff) buf.push(0xc4, n)\n else if (n <= 0xffff) buf.push(0xc5, (n >> 8) & 0xff, n & 0xff)\n else buf.push(0xc6, (n >>> 24) & 0xff, (n >>> 16) & 0xff, (n >>> 8) & 0xff, n & 0xff)\n for (let i = 0; i < b.length; i++) buf.push(b[i]!)\n}\n\nfunction writeArray(buf: number[], arr: readonly unknown[]): void {\n const n = arr.length\n if (n <= 15) buf.push(0x90 | n)\n else if (n <= 0xffff) buf.push(0xdc, (n >> 8) & 0xff, n & 0xff)\n else buf.push(0xdd, (n >>> 24) & 0xff, (n >>> 16) & 0xff, (n >>> 8) & 0xff, n & 0xff)\n for (const v of arr) write(buf, v)\n}\n\nfunction writeMap(buf: number[], o: Record<string, unknown>): void {\n const keys = Object.keys(o)\n const n = keys.length\n if (n <= 15) buf.push(0x80 | n)\n else if (n <= 0xffff) buf.push(0xde, (n >> 8) & 0xff, n & 0xff)\n else buf.push(0xdf, (n >>> 24) & 0xff, (n >>> 16) & 0xff, (n >>> 8) & 0xff, n & 0xff)\n for (const k of keys) {\n writeStr(buf, k)\n write(buf, o[k])\n }\n}\n\nfunction read(bytes: Uint8Array, c: { i: number }): unknown {\n const b = bytes[c.i++]!\n if (b <= 0x7f) return b\n if (b >= 0xe0) return b - 0x100\n if (b === 0xc0) return null\n if (b === 0xc2) return false\n if (b === 0xc3) return true\n if (b === 0xcc) return bytes[c.i++]!\n if (b === 0xcd) {\n const v = (bytes[c.i]! << 8) | bytes[c.i + 1]!\n c.i += 2\n return v\n }\n if (b === 0xce) {\n const v =\n bytes[c.i]! * 0x1000000 + ((bytes[c.i + 1]! << 16) | (bytes[c.i + 2]! << 8) | bytes[c.i + 3]!)\n c.i += 4\n return v\n }\n if (b === 0xd0) {\n const v = bytes[c.i++]!\n return v > 0x7f ? v - 0x100 : v\n }\n if (b === 0xd1) {\n const v = (bytes[c.i]! << 8) | bytes[c.i + 1]!\n c.i += 2\n return v > 0x7fff ? v - 0x10000 : v\n }\n if (b === 0xd2) {\n const dv = new DataView(bytes.buffer, bytes.byteOffset + c.i, 4)\n const v = dv.getInt32(0, false)\n c.i += 4\n return v\n }\n if (b === 0xcb) {\n const dv = new DataView(bytes.buffer, bytes.byteOffset + c.i, 8)\n const v = dv.getFloat64(0, false)\n c.i += 8\n return v\n }\n if (b >= 0xa0 && b <= 0xbf) return readStr(bytes, c, b & 0x1f)\n if (b === 0xd9) return readStr(bytes, c, bytes[c.i++]!)\n if (b === 0xda) {\n const n = (bytes[c.i]! << 8) | bytes[c.i + 1]!\n c.i += 2\n return readStr(bytes, c, n)\n }\n if (b === 0xdb) {\n const dv = new DataView(bytes.buffer, bytes.byteOffset + c.i, 4)\n const n = dv.getUint32(0, false)\n c.i += 4\n return readStr(bytes, c, n)\n }\n if (b === 0xc4) return readBin(bytes, c, bytes[c.i++]!)\n if (b >= 0x90 && b <= 0x9f) return readArr(bytes, c, b & 0x0f)\n if (b === 0xdc) {\n const n = (bytes[c.i]! << 8) | bytes[c.i + 1]!\n c.i += 2\n return readArr(bytes, c, n)\n }\n if (b >= 0x80 && b <= 0x8f) return readMap(bytes, c, b & 0x0f)\n if (b === 0xde) {\n const n = (bytes[c.i]! << 8) | bytes[c.i + 1]!\n c.i += 2\n return readMap(bytes, c, n)\n }\n throw new Error(`msgpack: unexpected byte 0x${b.toString(16)}`)\n}\n\nfunction readStr(bytes: Uint8Array, c: { i: number }, n: number): string {\n const s = new TextDecoder().decode(bytes.subarray(c.i, c.i + n))\n c.i += n\n return s\n}\nfunction readBin(bytes: Uint8Array, c: { i: number }, n: number): Uint8Array {\n const v = bytes.slice(c.i, c.i + n)\n c.i += n\n return v\n}\nfunction readArr(bytes: Uint8Array, c: { i: number }, n: number): unknown[] {\n const out: unknown[] = []\n for (let i = 0; i < n; i++) out.push(read(bytes, c))\n return out\n}\nfunction readMap(bytes: Uint8Array, c: { i: number }, n: number): Record<string, unknown> {\n const out: Record<string, unknown> = {}\n for (let i = 0; i < n; i++) {\n const k = read(bytes, c) as string\n out[k] = read(bytes, c)\n }\n return out\n}\n",
701
+ "sha256": "bd0e918d130dfc037a6337fa6c6d9e74ba96aad84d6ad53692b120df00ec9d21"
702
+ },
703
+ {
704
+ "path": "msgpack/index.ts",
705
+ "contents": "/**\n * @hyper/msgpack — content-negotiated MessagePack middleware + transport.\n *\n * use(msgpack())\n *\n * When the client sends `Accept: application/msgpack` AND the handler\n * returns a serializable object, we encode with msgpack. When the client\n * posts `Content-Type: application/msgpack`, we decode the body first.\n *\n * HTTP semantics are preserved; only the wire format changes.\n */\n\nimport type { Middleware } from \"@hyper/core\"\nimport { decode, encode } from \"./codec.ts\"\n\nexport { decode, encode } from \"./codec.ts\"\n\nexport const CONTENT_TYPE = \"application/msgpack\"\n\nexport function msgpack(): Middleware {\n return async ({ req, next }) => {\n // Decode inbound body if it's msgpack. We rewrite the Request body\n // as JSON so the rest of the stack (validators, handlers) sees the\n // same shape regardless of wire format.\n let inbound = req\n if (req.body && (req.headers.get(\"content-type\") ?? \"\").startsWith(CONTENT_TYPE)) {\n const bytes = new Uint8Array(await req.arrayBuffer())\n const decoded = decode(bytes)\n const newHeaders = new Headers(req.headers)\n newHeaders.set(\"content-type\", \"application/json\")\n inbound = new Request(req.url, {\n method: req.method,\n headers: newHeaders,\n body: JSON.stringify(decoded),\n })\n }\n\n const wantsMsgpack = (req.headers.get(\"accept\") ?? \"\").includes(CONTENT_TYPE)\n // Use `inbound` if we rewrote the Request — the rest of the chain\n // pulls from the original handler scope so this swap only affects\n // our own cloned-body reads; we just forward the headers.\n void inbound\n const out = await next()\n if (!wantsMsgpack) return out\n const res =\n out instanceof Response\n ? out\n : new Response(JSON.stringify(out), {\n headers: { \"content-type\": \"application/json\" },\n })\n if ((res.headers.get(\"content-type\") ?? \"\").includes(\"application/json\")) {\n const body = await res.clone().json()\n const bytes = encode(body)\n const headers = new Headers(res.headers)\n headers.set(\"content-type\", CONTENT_TYPE)\n headers.set(\"content-length\", bytes.byteLength.toString())\n return new Response(bytes, { status: res.status, headers })\n }\n return res\n }\n}\n",
706
+ "sha256": "81f92241243fd660d23bb1e207616adfdd01c90009b7b3561f1511acf8706d95"
707
+ }
708
+ ],
709
+ "subpaths": {}
710
+ },
711
+ "openapi": {
712
+ "name": "openapi",
713
+ "version": "0.1.0",
714
+ "description": "OpenAPI 3.1 serializer + Swagger UI for Hyper. Pluggable SchemaConverter.",
715
+ "readme": "# @hyper/openapi\n\nOpenAPI 3.1 serializer + Swagger UI for Hyper. Pluggable `SchemaConverter`.\n\n## Install\n\n```bash\nbun add @hyper/openapi @hyper/openapi-zod\n```\n\n## Usage\n\n```ts\nimport { Hyper } from \"@hyper/core\"\nimport { openapiPlugin } from \"@hyper/openapi\"\nimport { zodConverter } from \"@hyper/openapi-zod\"\n\nexport default new Hyper()\n .use(openapiPlugin({ converter: zodConverter }))\n .listen(3000)\n```\n\n## Docs\n\nSee the [main README](../../README.md) and [docs/](../../docs) for guides and integration recipes.\n\n## License\n\nMIT\n",
716
+ "registryDeps": [
717
+ "core"
718
+ ],
719
+ "peerDeps": {},
720
+ "optionalPeerDeps": {},
721
+ "files": [
722
+ {
723
+ "path": "openapi/converter.ts",
724
+ "contents": "/**\n * SchemaConverter — the pluggable boundary.\n *\n * A converter inspects a Standard Schema value and returns an OpenAPI\n * JSON Schema fragment. The base @hyper/openapi package ships a\n * `fallbackConverter` that just emits `type: object`. Integrations like\n * `@hyper/openapi-zod` / `-valibot` / `-arktype` extend this.\n */\n\nimport type { StandardSchemaV1 } from \"@hyper/core\"\n\nexport type JsonSchema = Record<string, unknown>\n\nexport interface SchemaConverter {\n readonly name: string\n readonly canHandle: (s: unknown) => boolean\n readonly toJsonSchema: (s: unknown) => JsonSchema\n}\n\nexport const fallbackConverter: SchemaConverter = {\n name: \"fallback\",\n canHandle: () => true,\n toJsonSchema: () => ({ type: \"object\" }),\n}\n\nexport function firstConverter(\n converters: readonly SchemaConverter[],\n schema: unknown,\n): SchemaConverter {\n for (const c of converters) if (c.canHandle(schema)) return c\n return fallbackConverter\n}\n\n/**\n * Detect a Standard Schema value — gives us a baseline fall-through.\n */\nexport function isStandardSchema(x: unknown): x is StandardSchemaV1 {\n return Boolean(x && typeof x === \"object\" && \"~standard\" in (x as Record<string, unknown>))\n}\n",
725
+ "sha256": "698bac9a0aeafc78a6e658f58fab4f100f4d1e90ee76304330048e61a3f09e02"
726
+ },
727
+ {
728
+ "path": "openapi/generate.ts",
729
+ "contents": "/**\n * Convert a Hyper app's route list into an OpenAPI 3.1 document,\n * threading schemas through the registered `SchemaConverter`s.\n */\n\nimport type { HyperApp, Route, RouteExample } from \"@hyper/core\"\nimport { type JsonSchema, type SchemaConverter, firstConverter } from \"./converter.ts\"\n\nexport interface GenerateConfig {\n readonly title?: string\n readonly version?: string\n readonly description?: string\n readonly servers?: readonly { url: string; description?: string }[]\n readonly converters?: readonly SchemaConverter[]\n}\n\nexport interface OpenAPIDoc {\n readonly openapi: \"3.1.0\"\n readonly info: { title: string; version: string; description?: string }\n readonly servers?: readonly { url: string; description?: string }[]\n readonly paths: Record<string, Record<string, OpenAPIOperation>>\n readonly components?: { schemas?: Record<string, JsonSchema> }\n}\n\ninterface OpenAPIOperation {\n readonly operationId?: string\n readonly summary?: string\n readonly tags?: readonly string[]\n readonly deprecated?: boolean\n readonly parameters?: readonly OpenAPIParam[]\n readonly requestBody?: {\n readonly content: Record<string, { schema: JsonSchema; examples?: Record<string, unknown> }>\n }\n readonly responses: Record<\n string,\n { description: string; content?: Record<string, { schema?: JsonSchema; example?: unknown }> }\n >\n readonly \"x-sunset\"?: string\n readonly \"x-version\"?: string\n}\n\ninterface OpenAPIParam {\n readonly name: string\n readonly in: \"path\" | \"query\" | \"header\"\n readonly required: boolean\n readonly schema?: JsonSchema\n}\n\nconst PATH_PARAM = /:([A-Za-z0-9_]+)/g\n\nexport function generate(app: HyperApp, cfg: GenerateConfig = {}): OpenAPIDoc {\n const converters = cfg.converters ?? []\n const paths: Record<string, Record<string, OpenAPIOperation>> = {}\n for (const r of app.routeList) {\n if (r.meta.internal) continue\n const p = toOpenApiPath(r.path)\n const operation = buildOperation(r, converters)\n if (!paths[p]) paths[p] = {}\n paths[p][r.method.toLowerCase()] = operation\n }\n return {\n openapi: \"3.1.0\",\n info: {\n title: cfg.title ?? \"Hyper API\",\n version: cfg.version ?? \"0.0.0\",\n ...(cfg.description !== undefined && { description: cfg.description }),\n },\n ...(cfg.servers && { servers: cfg.servers }),\n paths,\n }\n}\n\nfunction toOpenApiPath(path: string): string {\n return path.replace(PATH_PARAM, \"{$1}\")\n}\n\nfunction buildOperation(r: Route, converters: readonly SchemaConverter[]): OpenAPIOperation {\n const parameters: OpenAPIParam[] = []\n for (const match of r.path.matchAll(PATH_PARAM)) {\n parameters.push({ name: match[1]!, in: \"path\", required: true })\n }\n if (r.query) {\n const conv = firstConverter(converters, r.query)\n const js = conv.toJsonSchema(r.query)\n if (js.type === \"object\" && typeof js.properties === \"object\" && js.properties) {\n const required = new Set<string>((js.required as string[]) ?? [])\n for (const [name, sub] of Object.entries(js.properties as Record<string, JsonSchema>)) {\n parameters.push({\n name,\n in: \"query\",\n required: required.has(name),\n schema: sub,\n })\n }\n }\n }\n\n let requestBody: OpenAPIOperation[\"requestBody\"]\n if (r.body) {\n const conv = firstConverter(converters, r.body)\n const schema = conv.toJsonSchema(r.body)\n const examples = buildBodyExamples(r.meta.examples as readonly RouteExample[] | undefined)\n requestBody = {\n content: {\n \"application/json\": {\n schema,\n ...(examples && { examples }),\n },\n },\n }\n }\n\n const responseExamples = buildResponseExamples(\n r.meta.examples as readonly RouteExample[] | undefined,\n )\n const responses: OpenAPIOperation[\"responses\"] = {\n \"200\": {\n description: \"success\",\n ...(responseExamples && {\n content: { \"application/json\": { example: responseExamples } },\n }),\n },\n }\n\n if (r.throws) {\n for (const [status, schema] of Object.entries(r.throws)) {\n const conv = firstConverter(converters, schema)\n responses[status] = {\n description: \"declared error\",\n content: { \"application/json\": { schema: conv.toJsonSchema(schema) } },\n }\n }\n }\n\n const meta = r.meta\n const deprecated = !!meta.deprecated\n const sunset =\n typeof meta.deprecated === \"object\" && meta.deprecated?.sunset\n ? meta.deprecated.sunset\n : undefined\n return {\n ...(meta.name !== undefined && { operationId: meta.name }),\n ...(meta.tags !== undefined && { tags: meta.tags }),\n ...(deprecated && { deprecated: true }),\n ...(parameters.length > 0 && { parameters }),\n ...(requestBody && { requestBody }),\n responses,\n ...(sunset && { \"x-sunset\": sunset }),\n ...(meta.version !== undefined && { \"x-version\": meta.version }),\n }\n}\n\nfunction buildBodyExamples(\n examples: readonly RouteExample[] | undefined,\n): Record<string, { value: unknown }> | undefined {\n if (!examples) return undefined\n const out: Record<string, { value: unknown }> = {}\n for (const ex of examples) {\n if (ex.input?.body !== undefined) out[ex.name] = { value: ex.input.body }\n }\n return Object.keys(out).length > 0 ? out : undefined\n}\n\nfunction buildResponseExamples(examples: readonly RouteExample[] | undefined): unknown | undefined {\n if (!examples) return undefined\n const ex = examples.find((e) => e.output?.body !== undefined)\n return ex?.output?.body\n}\n",
730
+ "sha256": "3d1a43081b5a814d0e99130b3d31e2bfb1e0c706cdf66d256923cd97a145bb89"
731
+ },
732
+ {
733
+ "path": "openapi/index.ts",
734
+ "contents": "/**\n * @hyper/openapi — OpenAPI 3.1 serializer + Swagger UI for Hyper apps.\n *\n * import { openapiHandlers } from \"@hyper/openapi\"\n * const oa = openapiHandlers(app, { title: \"My API\", converters: [...] })\n * // Then mount oa.spec at /openapi.json and oa.docs at /docs.\n *\n * SchemaConverter is pluggable — see @hyper/openapi-zod / -valibot / -arktype.\n */\n\nexport { fallbackConverter, firstConverter, isStandardSchema } from \"./converter.ts\"\nexport type { JsonSchema, SchemaConverter } from \"./converter.ts\"\nexport { generate } from \"./generate.ts\"\nexport type { GenerateConfig, OpenAPIDoc } from \"./generate.ts\"\nexport { openapiHandlers, openapiPlugin } from \"./plugin.ts\"\nexport type { OpenApiPluginConfig } from \"./plugin.ts\"\nexport { swaggerHtml } from \"./swagger.ts\"\nexport type { SwaggerHtmlOptions } from \"./swagger.ts\"\n",
735
+ "sha256": "044532288c945e2b9c9e6cbcdf74561007540706ceae7afebdc5d421fe5536af"
736
+ },
737
+ {
738
+ "path": "openapi/plugin.ts",
739
+ "contents": "/**\n * openapiPlugin — exposes /openapi.json and /docs.\n *\n * Plugins don't add routes directly; the consumer mounts our two handlers\n * explicitly (`openapiHandlers(...)`) and the plugin wires default-on\n * cache headers for the spec URL.\n */\n\nimport type { HyperApp, HyperPlugin, InvokeInput, Route } from \"@hyper/core\"\nimport type { SchemaConverter } from \"./converter.ts\"\nimport { type GenerateConfig, type OpenAPIDoc, generate } from \"./generate.ts\"\nimport { type SwaggerHtmlOptions, swaggerHtml } from \"./swagger.ts\"\n\nexport interface OpenApiPluginConfig extends GenerateConfig, SwaggerHtmlOptions {}\n\nexport function openapiPlugin(config: OpenApiPluginConfig = {}): HyperPlugin {\n return {\n name: \"@hyper/openapi\",\n build() {\n // Reserved for future dynamic-route registration.\n },\n }\n}\n\n/** Standalone handler pair users mount on their app. */\nexport function openapiHandlers(\n app: HyperApp,\n config: OpenApiPluginConfig = {},\n): {\n spec: (req: Request) => Response\n docs: (req: Request) => Response\n doc: OpenAPIDoc\n} {\n const doc = generate(app, config)\n const docJson = JSON.stringify(doc)\n const html = swaggerHtml({\n ...(config.specUrl !== undefined && { specUrl: config.specUrl }),\n ...(config.title !== undefined && { title: config.title }),\n })\n return {\n doc,\n spec: () =>\n new Response(docJson, {\n headers: {\n \"content-type\": \"application/json; charset=utf-8\",\n \"cache-control\": \"public, max-age=60\",\n },\n }),\n docs: () =>\n new Response(html, {\n headers: {\n \"content-type\": \"text/html; charset=utf-8\",\n \"cache-control\": \"public, max-age=60\",\n },\n }),\n }\n}\n\n// Unused but exported for TypeScript — keeps the type dep alive.\nexport type _InvokeInput = InvokeInput\nexport type _Route = Route\nexport type _SchemaConverter = SchemaConverter\n",
740
+ "sha256": "b0c3ea65ec3b71cb72b681d53845368653f63b8fcf83aef83eea43b3b805cae4"
741
+ },
742
+ {
743
+ "path": "openapi/swagger.ts",
744
+ "contents": "/**\n * Swagger UI handler — self-contained HTML + redirect to /openapi.json.\n * CDN-loaded; no bundling required.\n */\n\nexport interface SwaggerHtmlOptions {\n readonly specUrl?: string\n readonly title?: string\n}\n\nexport function swaggerHtml(opts: SwaggerHtmlOptions = {}): string {\n const spec = opts.specUrl ?? \"/openapi.json\"\n const title = opts.title ?? \"API Docs\"\n return `<!doctype html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <title>${escapeHtml(title)}</title>\n <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css\">\n <style>body { margin: 0 }</style>\n</head>\n<body>\n <div id=\"ui\"></div>\n <script src=\"https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js\"></script>\n <script>\n window.ui = SwaggerUIBundle({\n url: ${JSON.stringify(spec)},\n dom_id: \"#ui\",\n deepLinking: true,\n docExpansion: \"list\",\n })\n </script>\n</body>\n</html>`\n}\n\nfunction escapeHtml(s: string): string {\n return s.replace(\n /[&<>\"']/g,\n (c) =>\n ({\n \"&\": \"&amp;\",\n \"<\": \"&lt;\",\n \">\": \"&gt;\",\n '\"': \"&quot;\",\n \"'\": \"&#39;\",\n })[c] as string,\n )\n}\n",
745
+ "sha256": "50a2379f4f0dc2cb8749bc252d353b395daf2aad9723ed68acfc8925e500d2c8"
746
+ }
747
+ ],
748
+ "subpaths": {}
749
+ },
750
+ "openapi-arktype": {
751
+ "name": "openapi-arktype",
752
+ "version": "0.1.0",
753
+ "description": "SchemaConverter for ArkType to @hyper/openapi.",
754
+ "readme": "# @hyper/openapi-arktype\n\nArkType `SchemaConverter` for `@hyper/openapi`.\n\n## Install\n\n```bash\nbun add @hyper/openapi @hyper/openapi-arktype arktype\n```\n\n## Usage\n\n```ts\nimport { Hyper } from \"@hyper/core\"\nimport { openapiPlugin } from \"@hyper/openapi\"\nimport { arktypeConverter } from \"@hyper/openapi-arktype\"\n\nexport default new Hyper()\n .use(openapiPlugin({ converter: arktypeConverter }))\n .listen(3000)\n```\n\n## Docs\n\nSee the [main README](../../README.md) and [docs/](../../docs) for guides and integration recipes.\n\n## License\n\nMIT\n",
755
+ "registryDeps": [
756
+ "openapi"
757
+ ],
758
+ "peerDeps": {},
759
+ "optionalPeerDeps": {},
760
+ "files": [
761
+ {
762
+ "path": "openapi-arktype/index.ts",
763
+ "contents": "/**\n * @hyper/openapi-arktype — SchemaConverter for ArkType.\n *\n * ArkType types expose a `toJsonSchema()` method (v2+). We thin-wrap that\n * so users get proper JSON Schema without pulling ArkType at runtime.\n */\n\nimport type { JsonSchema, SchemaConverter } from \"@hyper/openapi\"\n\ninterface ArkType {\n readonly toJsonSchema?: () => JsonSchema\n readonly infer?: unknown\n readonly definition?: unknown\n}\n\nfunction isArkType(s: unknown): s is ArkType {\n if (!s || typeof s !== \"object\") return false\n const x = s as Record<string, unknown>\n return typeof x.toJsonSchema === \"function\" && (\"infer\" in x || \"definition\" in x)\n}\n\nexport const arktypeConverter: SchemaConverter = {\n name: \"arktype\",\n canHandle: isArkType,\n toJsonSchema: (s) => {\n const t = s as ArkType\n try {\n return t.toJsonSchema?.() ?? { type: \"object\" }\n } catch {\n return { type: \"object\" }\n }\n },\n}\n",
764
+ "sha256": "0204827d71f8ee4830290effe4e980c717bff83544cea4bfb50b255855e7cfeb"
765
+ }
766
+ ],
767
+ "subpaths": {}
768
+ },
769
+ "openapi-valibot": {
770
+ "name": "openapi-valibot",
771
+ "version": "0.1.0",
772
+ "description": "SchemaConverter for Valibot to @hyper/openapi.",
773
+ "readme": "# @hyper/openapi-valibot\n\nValibot `SchemaConverter` for `@hyper/openapi`.\n\n## Install\n\n```bash\nbun add @hyper/openapi @hyper/openapi-valibot valibot\n```\n\n## Usage\n\n```ts\nimport { Hyper } from \"@hyper/core\"\nimport { openapiPlugin } from \"@hyper/openapi\"\nimport { valibotConverter } from \"@hyper/openapi-valibot\"\n\nexport default new Hyper()\n .use(openapiPlugin({ converter: valibotConverter }))\n .listen(3000)\n```\n\n## Docs\n\nSee the [main README](../../README.md) and [docs/](../../docs) for guides and integration recipes.\n\n## License\n\nMIT\n",
774
+ "registryDeps": [
775
+ "openapi"
776
+ ],
777
+ "peerDeps": {},
778
+ "optionalPeerDeps": {},
779
+ "files": [
780
+ {
781
+ "path": "openapi-valibot/index.ts",
782
+ "contents": "/**\n * @hyper/openapi-valibot — SchemaConverter for Valibot schemas.\n *\n * Valibot schemas are plain objects with a `kind: \"schema\"` marker plus\n * `type` field (e.g. \"object\", \"string\", \"array\", \"union\", \"optional\").\n */\n\nimport type { JsonSchema, SchemaConverter } from \"@hyper/openapi\"\n\ninterface ValibotSchema {\n readonly kind: \"schema\"\n readonly type: string\n readonly expects?: string\n readonly [k: string]: unknown\n}\n\nfunction isValibot(s: unknown): s is ValibotSchema {\n if (!s || typeof s !== \"object\") return false\n const x = s as Record<string, unknown>\n return x.kind === \"schema\" && typeof x.type === \"string\"\n}\n\nfunction toJson(v: ValibotSchema): JsonSchema {\n switch (v.type) {\n case \"string\":\n return { type: \"string\" }\n case \"number\":\n return { type: \"number\" }\n case \"boolean\":\n return { type: \"boolean\" }\n case \"literal\":\n return { const: (v as { literal: unknown }).literal }\n case \"picklist\":\n case \"enum\":\n return { enum: (v as { options: readonly unknown[] }).options }\n case \"array\": {\n const item = (v as { item: ValibotSchema }).item\n return { type: \"array\", items: toJson(item) }\n }\n case \"object\": {\n const entries = (v as { entries: Record<string, ValibotSchema> }).entries\n const properties: Record<string, JsonSchema> = {}\n const required: string[] = []\n for (const [k, sub] of Object.entries(entries)) {\n properties[k] = toJson(sub)\n if (sub.type !== \"optional\" && sub.type !== \"nullish\") required.push(k)\n }\n return {\n type: \"object\",\n properties,\n ...(required.length > 0 && { required }),\n }\n }\n case \"optional\":\n case \"nullish\":\n return toJson((v as { wrapped: ValibotSchema }).wrapped)\n case \"nullable\": {\n const j = toJson((v as { wrapped: ValibotSchema }).wrapped)\n const t = j.type\n return { ...j, type: Array.isArray(t) ? [...t, \"null\"] : t ? [t as string, \"null\"] : \"null\" }\n }\n case \"union\": {\n const opts = (v as { options: readonly ValibotSchema[] }).options\n return { anyOf: opts.map(toJson) }\n }\n default:\n return {}\n }\n}\n\nexport const valibotConverter: SchemaConverter = {\n name: \"valibot\",\n canHandle: isValibot,\n toJsonSchema: (s) => toJson(s as ValibotSchema),\n}\n",
783
+ "sha256": "fdf64d870827b059368772bf9fad0f5d3406886c6a2e65e1b85fe86db5dfccf9"
784
+ }
785
+ ],
786
+ "subpaths": {}
787
+ },
788
+ "openapi-zod": {
789
+ "name": "openapi-zod",
790
+ "version": "0.1.0",
791
+ "description": "SchemaConverter for Zod (v3 + v4) to @hyper/openapi.",
792
+ "readme": "# @hyper/openapi-zod\n\nZod (v3 + v4) `SchemaConverter` for `@hyper/openapi`.\n\n## Install\n\n```bash\nbun add @hyper/openapi @hyper/openapi-zod zod\n```\n\n## Usage\n\n```ts\nimport { Hyper } from \"@hyper/core\"\nimport { openapiPlugin } from \"@hyper/openapi\"\nimport { zodConverter } from \"@hyper/openapi-zod\"\n\nexport default new Hyper()\n .use(openapiPlugin({ converter: zodConverter }))\n .listen(3000)\n```\n\n## Docs\n\nSee the [main README](../../README.md) and [docs/](../../docs) for guides and integration recipes.\n\n## License\n\nMIT\n",
793
+ "registryDeps": [
794
+ "openapi"
795
+ ],
796
+ "peerDeps": {},
797
+ "optionalPeerDeps": {},
798
+ "files": [
799
+ {
800
+ "path": "openapi-zod/index.ts",
801
+ "contents": "/**\n * @hyper/openapi-zod — SchemaConverter that understands Zod v3 and v4.\n *\n * import { zodConverter } from \"@hyper/openapi-zod\"\n * openapiHandlers(app, { converters: [zodConverter] })\n *\n * Detection:\n * - Zod schemas expose `_def.typeName` (v3) or `_def.type` (v4).\n * - We sniff structurally so Zod isn't required at import-time.\n *\n * This converter does not rely on `zod-to-json-schema`; it walks `_def`\n * directly for the subset of types we care about (object / array / string\n * / number / boolean / enum / union / optional / nullable / default).\n */\n\nimport type { JsonSchema, SchemaConverter } from \"@hyper/openapi\"\n\ninterface ZodLike {\n readonly _def: ZodDef\n readonly parse?: (...a: unknown[]) => unknown\n readonly safeParse?: (...a: unknown[]) => unknown\n}\n\ninterface ZodDef {\n readonly typeName?: string\n readonly type?: string\n readonly [k: string]: unknown\n}\n\nfunction isZod(s: unknown): s is ZodLike {\n if (!s || typeof s !== \"object\") return false\n const x = s as { _def?: unknown; parse?: unknown; safeParse?: unknown }\n if (!x._def || typeof x._def !== \"object\") return false\n return typeof x.parse === \"function\" || typeof x.safeParse === \"function\"\n}\n\nfunction defName(def: ZodDef): string | undefined {\n return (def.typeName ?? def.type) as string | undefined\n}\n\nfunction toJson(schema: ZodLike): JsonSchema {\n const def = schema._def\n const name = defName(def)\n switch (name) {\n case \"ZodString\":\n case \"string\":\n return { type: \"string\" }\n case \"ZodNumber\":\n case \"number\":\n return { type: \"number\" }\n case \"ZodBoolean\":\n case \"boolean\":\n return { type: \"boolean\" }\n case \"ZodLiteral\":\n case \"literal\":\n return { const: (def as { value: unknown }).value }\n case \"ZodEnum\":\n case \"enum\": {\n const v = def as { values?: readonly unknown[]; entries?: Record<string, unknown> }\n const values = v.values ?? (v.entries ? Object.values(v.entries) : [])\n return { enum: values }\n }\n case \"ZodArray\":\n case \"array\": {\n const v = def as { type?: ZodLike; element?: ZodLike }\n const inner = v.type ?? v.element\n return { type: \"array\", ...(inner && { items: toJson(inner) }) }\n }\n case \"ZodObject\":\n case \"object\": {\n const shapeFn = (def as { shape?: () => Record<string, ZodLike> }).shape\n const shape =\n typeof shapeFn === \"function\"\n ? shapeFn()\n : ((def as { shape?: Record<string, ZodLike> }).shape ?? {})\n const properties: Record<string, JsonSchema> = {}\n const required: string[] = []\n for (const [k, v] of Object.entries(shape)) {\n const inner = toJson(v)\n properties[k] = inner\n const innerName = defName(v._def)\n if (innerName !== \"ZodOptional\" && innerName !== \"optional\") required.push(k)\n }\n return {\n type: \"object\",\n properties,\n ...(required.length > 0 && { required }),\n }\n }\n case \"ZodOptional\":\n case \"optional\": {\n const inner = (def as { innerType: ZodLike }).innerType\n return toJson(inner)\n }\n case \"ZodNullable\":\n case \"nullable\": {\n const inner = (def as { innerType: ZodLike }).innerType\n const j = toJson(inner)\n const t = j.type\n return { ...j, type: Array.isArray(t) ? [...t, \"null\"] : t ? [t as string, \"null\"] : \"null\" }\n }\n case \"ZodDefault\":\n case \"default\": {\n const inner = (def as { innerType: ZodLike }).innerType\n return { ...toJson(inner), default: (def as { defaultValue?: unknown }).defaultValue }\n }\n case \"ZodUnion\":\n case \"union\": {\n const options = (def as { options: readonly ZodLike[] }).options\n return { anyOf: options.map(toJson) }\n }\n default:\n return {}\n }\n}\n\nexport const zodConverter: SchemaConverter = {\n name: \"zod\",\n canHandle: isZod,\n toJsonSchema: (s) => toJson(s as ZodLike),\n}\n",
802
+ "sha256": "fc3d5db756dd6ca5d1827b63657b956d043e5df1fbac7bdaac7d8888f1863c18"
803
+ }
804
+ ],
805
+ "subpaths": {}
806
+ },
807
+ "otel": {
808
+ "name": "otel",
809
+ "version": "0.1.0",
810
+ "description": "OpenTelemetry tracing + SLO histograms for Hyper.",
811
+ "readme": "# @hyper/otel\n\nOpenTelemetry tracing + SLO histograms for Hyper.\n\n## Install\n\n```bash\nbun add @hyper/otel\n```\n\n## Usage\n\n```ts\nimport { Hyper } from \"@hyper/core\"\nimport { otelMiddleware } from \"@hyper/otel\"\n\nexport default new Hyper()\n .use(otelMiddleware())\n .listen(3000)\n```\n\n## Docs\n\nSee the [main README](../../README.md) and [docs/](../../docs) for guides and integration recipes.\n\n## License\n\nMIT\n",
812
+ "registryDeps": [
813
+ "core"
814
+ ],
815
+ "peerDeps": {},
816
+ "optionalPeerDeps": {},
817
+ "files": [
818
+ {
819
+ "path": "otel/index.ts",
820
+ "contents": "/**\n * @hyper/otel — OpenTelemetry-flavored request spans + SLO histograms.\n *\n * The full OTLP exporter is wired via the user's `@opentelemetry/sdk-node`\n * setup (we reuse the global provider). This middleware just emits spans\n * when an `otel.Tracer` is provided, and always pushes duration samples\n * into our built-in SLO recorder.\n */\n\nimport type { Middleware } from \"@hyper/core\"\nimport { SloRecorder } from \"./slo.ts\"\n\nexport { SloRecorder } from \"./slo.ts\"\nexport type { SloTarget } from \"./slo.ts\"\n\nexport interface OtelConfig {\n readonly tracer?: TracerLike\n readonly recorder?: SloRecorder\n}\n\ninterface SpanLike {\n setAttribute(key: string, value: unknown): void\n setStatus(status: { code: number; message?: string }): void\n end(): void\n recordException?(e: unknown): void\n}\n\ninterface TracerLike {\n startSpan(name: string, options?: { attributes?: Record<string, unknown> }): SpanLike\n}\n\nexport function otel(config: OtelConfig = {}): Middleware {\n const recorder = config.recorder ?? new SloRecorder()\n return async ({ req, path, next }) => {\n const span = config.tracer?.startSpan(`${req.method} ${path}`, {\n attributes: {\n \"http.method\": req.method,\n \"http.route\": path,\n \"http.url\": req.url,\n },\n })\n const t0 = performance.now()\n try {\n const out = await next()\n const dt = performance.now() - t0\n recorder.record(path, dt)\n if (span) {\n span.setAttribute(\"http.duration_ms\", dt)\n span.setStatus({ code: 1 })\n span.end()\n }\n return out\n } catch (error) {\n const dt = performance.now() - t0\n recorder.record(path, dt)\n if (span) {\n span.setAttribute(\"http.duration_ms\", dt)\n span.setStatus({ code: 2, message: String(error) })\n span.recordException?.(error)\n span.end()\n }\n throw error\n }\n }\n}\n",
821
+ "sha256": "4e1006ec68ad3aa283cc2defb73d29cb42ceb70bb94e120ee899dd84d6a6719a"
822
+ },
823
+ {
824
+ "path": "otel/slo.ts",
825
+ "contents": "/**\n * SLO histogram recorder + `.slo()` route builder sugar.\n *\n * `.slo({ p99: 200 })` attaches `{ slo: { p99: 200 } }` to `meta`; the\n * `otelPlugin` middleware uses it to produce a per-route histogram and\n * tags spans with `slo.target` / `slo.violation=true`.\n */\n\nexport interface SloTarget {\n readonly p50?: number\n readonly p95?: number\n readonly p99?: number\n}\n\ninterface Sample {\n readonly durationMs: number\n readonly route: string\n}\n\nexport class SloRecorder {\n #samples: Sample[] = []\n\n record(route: string, durationMs: number): void {\n this.#samples.push({ route, durationMs })\n if (this.#samples.length > 10_000) this.#samples.shift()\n }\n\n percentile(route: string, p: number): number {\n const xs = this.#samples\n .filter((s) => s.route === route)\n .map((s) => s.durationMs)\n .sort((a, b) => a - b)\n if (xs.length === 0) return 0\n const idx = Math.min(xs.length - 1, Math.floor((xs.length * p) / 100))\n return xs[idx] ?? 0\n }\n\n snapshot(): Record<string, { p50: number; p95: number; p99: number; count: number }> {\n const byRoute = new Map<string, number[]>()\n for (const s of this.#samples) {\n const arr = byRoute.get(s.route) ?? []\n arr.push(s.durationMs)\n byRoute.set(s.route, arr)\n }\n const out: Record<string, { p50: number; p95: number; p99: number; count: number }> = {}\n for (const [route, xs] of byRoute) {\n xs.sort((a, b) => a - b)\n const at = (p: number) => xs[Math.min(xs.length - 1, Math.floor((xs.length * p) / 100))] ?? 0\n out[route] = { p50: at(50), p95: at(95), p99: at(99), count: xs.length }\n }\n return out\n }\n}\n",
826
+ "sha256": "43d88404b1c0dd90fd5bb7193c364075dc1c66cf1e99d682c9e58658f3876f39"
827
+ }
828
+ ],
829
+ "subpaths": {}
830
+ },
831
+ "rate-limit": {
832
+ "name": "rate-limit",
833
+ "version": "0.1.0",
834
+ "description": "Token-bucket rate limiting for Hyper. In-memory + pluggable stores.",
835
+ "readme": "# @hyper/rate-limit\n\nToken-bucket rate limiting for Hyper. In-memory + pluggable stores.\n\n## Install\n\n```bash\nbun add @hyper/rate-limit\n```\n\n## Usage\n\n```ts\nimport { Hyper } from \"@hyper/core\"\nimport { authRateLimitPlugin, rateLimit } from \"@hyper/rate-limit\"\n\nexport default new Hyper()\n .use(rateLimit({ max: 100, windowMs: 60_000 }))\n .use(authRateLimitPlugin())\n .listen(3000)\n```\n\n## Docs\n\nSee the [main README](../../README.md) and [docs/](../../docs) for guides and integration recipes.\n\n## License\n\nMIT\n",
836
+ "registryDeps": [
837
+ "core"
838
+ ],
839
+ "peerDeps": {},
840
+ "optionalPeerDeps": {},
841
+ "files": [
842
+ {
843
+ "path": "rate-limit/index.ts",
844
+ "contents": "/**\n * @hyper/rate-limit — token-bucket rate limiting.\n *\n * Pluggable store (default: in-memory). Default key extractor uses\n * the X-Forwarded-For / client IP; consumers can override (session id,\n * api key, etc.).\n *\n * use(rateLimit({ window: \"1m\", limit: 60 }))\n *\n * Adds standard headers: `RateLimit-Limit`, `RateLimit-Remaining`,\n * `RateLimit-Reset`. Responds 429 with `Retry-After` on exhaustion.\n */\n\nimport { HyperError, type HyperPlugin, type Middleware, coerce } from \"@hyper/core\"\n\nexport interface RateLimitStore {\n take(key: string, limit: number, windowMs: number): Promise<RateLimitResult>\n}\n\nexport interface RateLimitResult {\n readonly allowed: boolean\n readonly remaining: number\n readonly resetMs: number\n}\n\ninterface Bucket {\n count: number\n reset: number\n}\n\nexport function memoryLimiter(): RateLimitStore {\n const buckets = new Map<string, Bucket>()\n return {\n async take(key, limit, windowMs) {\n const now = Date.now()\n let b = buckets.get(key)\n if (!b || b.reset <= now) {\n b = { count: 0, reset: now + windowMs }\n buckets.set(key, b)\n }\n b.count += 1\n const allowed = b.count <= limit\n const remaining = Math.max(0, limit - b.count)\n return { allowed, remaining, resetMs: b.reset - now }\n },\n }\n}\n\nexport interface RateLimitConfig {\n readonly store?: RateLimitStore\n readonly limit: number\n /** ms or short string (\"1m\", \"10s\", \"1h\"). */\n readonly window: number | string\n readonly key?: (req: Request) => string\n}\n\nexport function rateLimit(config: RateLimitConfig): Middleware {\n const store = config.store ?? memoryLimiter()\n const windowMs = typeof config.window === \"string\" ? parseDuration(config.window) : config.window\n const keyFn = config.key ?? defaultKey\n\n return async ({ req, next }) => {\n const key = keyFn(req)\n const r = await store.take(key, config.limit, windowMs)\n if (!r.allowed) {\n const retryAfter = Math.ceil(r.resetMs / 1000)\n return new Response(JSON.stringify({ error: \"rate_limit_exceeded\", retryAfter }), {\n status: 429,\n headers: {\n \"content-type\": \"application/json\",\n \"retry-after\": retryAfter.toString(),\n \"ratelimit-limit\": config.limit.toString(),\n \"ratelimit-remaining\": \"0\",\n \"ratelimit-reset\": retryAfter.toString(),\n },\n })\n }\n const out = await next()\n const res = out instanceof Response ? out : coerce(out)\n res.headers.set(\"ratelimit-limit\", config.limit.toString())\n res.headers.set(\"ratelimit-remaining\", r.remaining.toString())\n res.headers.set(\"ratelimit-reset\", Math.ceil(r.resetMs / 1000).toString())\n return res\n }\n}\n\nfunction defaultKey(req: Request): string {\n return (\n req.headers.get(\"x-forwarded-for\")?.split(\",\")[0]?.trim() ??\n req.headers.get(\"x-real-ip\") ??\n \"anonymous\"\n )\n}\n\n/**\n * Auto-rate-limit plugin for auth endpoints.\n *\n * Any route carrying `meta.authEndpoint === true` gets a default rate\n * limit applied without the author having to chain `.use(rateLimit(...))`\n * on every login/reset/verify handler. Route-level limits still win:\n * if the route declares its own rateLimit middleware, this plugin no-ops.\n *\n * app({\n * routes: [loginRoute, resetPasswordRoute],\n * plugins: [authRateLimitPlugin({ limit: 10, window: \"1m\" })],\n * })\n *\n * Default key: caller IP + route path. Credential-stuffing attackers\n * spraying hundreds of accounts from one IP hit the limit immediately.\n */\nexport interface AuthRateLimitConfig {\n readonly limit?: number\n readonly window?: number | string\n readonly store?: RateLimitStore\n readonly key?: (req: Request) => string\n}\n\nconst AUTH_STATE = new WeakMap<\n Request,\n { path: string; limit: number; remaining: number; resetMs: number }\n>()\n\nexport function authRateLimitPlugin(config: AuthRateLimitConfig = {}): HyperPlugin {\n const limit = config.limit ?? 10\n const window = config.window ?? \"1m\"\n const windowMs = typeof window === \"string\" ? parseDuration(window) : window\n const store = config.store ?? memoryLimiter()\n const keyFn = config.key ?? defaultKey\n\n return {\n name: \"@hyper/rate-limit:auth\",\n request: {\n async before({ req, route }) {\n if (!route?.meta?.authEndpoint) return\n const key = `auth:${route.path}:${keyFn(req)}`\n const r = await store.take(key, limit, windowMs)\n AUTH_STATE.set(req, { path: route.path, limit, remaining: r.remaining, resetMs: r.resetMs })\n if (!r.allowed) {\n const retryAfter = Math.ceil(r.resetMs / 1000)\n throw new HyperError({\n status: 429,\n code: \"rate_limit_exceeded\",\n message: \"Too many auth attempts. Back off and retry.\",\n why: `More than ${limit} auth attempts in the current window.`,\n fix: `Wait ${retryAfter} seconds and retry. Consider adding CAPTCHA or progressive delays on the client.`,\n details: { retryAfter, limit },\n })\n }\n },\n after({ req, res, route }) {\n if (!route?.meta?.authEndpoint) return\n const s = AUTH_STATE.get(req)\n if (!s) return\n AUTH_STATE.delete(req)\n res.headers.set(\"ratelimit-limit\", s.limit.toString())\n res.headers.set(\"ratelimit-remaining\", s.remaining.toString())\n res.headers.set(\"ratelimit-reset\", Math.ceil(s.resetMs / 1000).toString())\n },\n },\n }\n}\n\nfunction parseDuration(s: string): number {\n const m = /^(\\d+)\\s*(ms|s|m|h|d)$/.exec(s)\n if (!m) throw new Error(`rate-limit: invalid duration \"${s}\"`)\n const n = Number.parseInt(m[1]!, 10)\n switch (m[2]) {\n case \"ms\":\n return n\n case \"s\":\n return n * 1000\n case \"m\":\n return n * 60 * 1000\n case \"h\":\n return n * 60 * 60 * 1000\n case \"d\":\n return n * 24 * 60 * 60 * 1000\n default:\n throw new Error(`rate-limit: invalid unit \"${m[2]}\"`)\n }\n}\n",
845
+ "sha256": "29e7982259e47e287c6d420726e53eaca974b546023b2a1ff963f612879027f3"
846
+ },
847
+ {
848
+ "path": "rate-limit/sqlite.ts",
849
+ "contents": "/**\n * bun:sqlite-backed RateLimitStore — survives process restart; well-\n * suited for single-node deployments. For multi-node, use Redis.\n */\n\nimport { Database } from \"bun:sqlite\"\nimport type { RateLimitResult, RateLimitStore } from \"./index.ts\"\n\nexport interface SqliteRateLimitOptions {\n readonly path?: string\n}\n\nexport function sqliteRateLimit(opts: SqliteRateLimitOptions = {}): RateLimitStore & {\n readonly sweep: () => void\n readonly close: () => void\n} {\n const path = opts.path ?? \"./.hyper/rate-limit.sqlite\"\n const db = new Database(path, { create: true })\n db.exec(\"PRAGMA journal_mode = WAL; PRAGMA synchronous = NORMAL;\")\n db.exec(`\n CREATE TABLE IF NOT EXISTS hyper_rl (\n k TEXT PRIMARY KEY,\n count INTEGER NOT NULL,\n reset_at INTEGER NOT NULL\n );\n `)\n const stmtGet = db.prepare(\"SELECT count, reset_at AS resetAt FROM hyper_rl WHERE k = ?\")\n const stmtInit = db.prepare(\n \"INSERT OR REPLACE INTO hyper_rl (k, count, reset_at) VALUES (?, 1, ?)\",\n )\n const stmtBump = db.prepare(\"UPDATE hyper_rl SET count = count + 1 WHERE k = ?\")\n const stmtSweep = db.prepare(\"DELETE FROM hyper_rl WHERE reset_at < ?\")\n\n return {\n async take(key, limit, windowMs): Promise<RateLimitResult> {\n const now = Date.now()\n const row = stmtGet.get(key) as { count: number; resetAt: number } | undefined\n if (!row || row.resetAt <= now) {\n stmtInit.run(key, now + windowMs)\n return { allowed: true, remaining: Math.max(0, limit - 1), resetMs: windowMs }\n }\n stmtBump.run(key)\n const count = row.count + 1\n const allowed = count <= limit\n return { allowed, remaining: Math.max(0, limit - count), resetMs: row.resetAt - now }\n },\n sweep: () => {\n stmtSweep.run(Date.now())\n },\n close: () => db.close(),\n }\n}\n",
850
+ "sha256": "9254dfa14d9959b31e2420880b2cf3797d4efd4beaf38355ba0d176194b61c1a"
851
+ }
852
+ ],
853
+ "subpaths": {}
854
+ },
855
+ "session": {
856
+ "name": "session",
857
+ "version": "0.1.0",
858
+ "description": "Signed-cookie session middleware for Hyper. Pluggable stores.",
859
+ "readme": "# @hyper/session\n\nSigned-cookie session middleware for Hyper. Pluggable stores.\n\n## Install\n\n```bash\nbun add @hyper/session\n```\n\n## Usage\n\n```ts\nimport { Hyper } from \"@hyper/core\"\nimport { csrfGuard, session } from \"@hyper/session\"\n\nexport default new Hyper()\n .use(session({ secret: process.env.SESSION_SECRET! }))\n .use(csrfGuard())\n .get(\"/me\", ({ ctx }) => ({ session: ctx.session }))\n .listen(3000)\n```\n\n## Docs\n\nSee the [main README](../../README.md) and [docs/](../../docs) for guides and integration recipes.\n\n## License\n\nMIT\n",
860
+ "registryDeps": [
861
+ "core"
862
+ ],
863
+ "peerDeps": {},
864
+ "optionalPeerDeps": {},
865
+ "files": [
866
+ {
867
+ "path": "session/csrf.ts",
868
+ "contents": "/**\n * CSRF double-submit protection for cookie-authenticated routes.\n *\n * Strategy: on every response where a session is active, we issue a\n * non-HttpOnly `csrf` cookie containing a random token. For mutating\n * methods (POST/PUT/PATCH/DELETE), we verify that the client echoed\n * the token back in the `X-CSRF-Token` header (constant-time compare).\n *\n * Usage:\n *\n * const sess = session({ secret: env.SESSION_SECRET })\n * const guard = csrfGuard()\n * route.post(\"/mutate\").use(sess).use(guard).handle(...)\n *\n * Or pre-bind the pair:\n *\n * const { session: sess, csrf: guard } = sessionWithCsrf({ secret })\n *\n * Pure-bearer endpoints that deliberately ignore cookies can omit\n * `csrfGuard()` — it only acts when `ctx.session` is present.\n */\n\nimport { timingSafeEqual } from \"node:crypto\"\nimport { type Middleware, coerce, createError } from \"@hyper/core\"\n\nexport interface CsrfConfig {\n readonly cookieName?: string\n readonly headerName?: string\n readonly sameSite?: \"Strict\" | \"Lax\" | \"None\"\n readonly secure?: boolean\n /** Methods that never require a CSRF check. Default: safe verbs. */\n readonly exemptMethods?: readonly string[]\n}\n\nconst DEFAULT_EXEMPT = [\"GET\", \"HEAD\", \"OPTIONS\"] as const\n\nexport function csrfGuard(config: CsrfConfig = {}): Middleware {\n const cookieName = config.cookieName ?? \"csrf\"\n const headerName = (config.headerName ?? \"x-csrf-token\").toLowerCase()\n const sameSite = config.sameSite ?? \"Lax\"\n const secure = config.secure ?? true\n const exempt = new Set((config.exemptMethods ?? DEFAULT_EXEMPT).map((m) => m.toUpperCase()))\n\n const mw: Middleware = async ({ ctx, req, next }) => {\n const sess = (ctx as { session?: { id?: string } }).session\n // Only protect requests whose session was *loaded from* an incoming\n // cookie. Freshly-minted sessions (first-time login) don't have a\n // csrf cookie to echo yet — that would create a chicken-and-egg block.\n const isEstablished = !!sess?.id\n const method = req.method.toUpperCase()\n const cookieToken = readCookie(req, cookieName)\n const hasSession = !!sess\n\n if (isEstablished && !exempt.has(method)) {\n const headerToken = req.headers.get(headerName)\n if (!cookieToken || !headerToken || !constantTimeEq(cookieToken, headerToken)) {\n throw createError({\n status: 403,\n code: \"csrf_token_mismatch\",\n message: \"Missing or invalid CSRF token.\",\n why: \"Mutating request from a cookie-authenticated session without a matching CSRF token.\",\n fix: `Include the '${headerName}' header with the value from the '${cookieName}' cookie. Non-browser clients should use bearer auth instead of cookies.`,\n })\n }\n }\n\n const out = await next()\n const res = out instanceof Response ? out : coerce(out)\n\n if (hasSession && !cookieToken) {\n const tok = newToken()\n res.headers.append(\n \"set-cookie\",\n `${cookieName}=${tok}; Path=/; SameSite=${sameSite}${secure ? \"; Secure\" : \"\"}`,\n )\n }\n return res\n }\n ;(mw as unknown as { __hyperTag: string }).__hyperTag = \"@hyper/session:csrf\"\n return mw\n}\n\nfunction readCookie(req: Request, name: string): string | null {\n const header = req.headers.get(\"cookie\")\n if (!header) return null\n for (const part of header.split(/;\\s*/)) {\n const [k, ...rest] = part.split(\"=\")\n if (k === name) return rest.join(\"=\")\n }\n return null\n}\n\nfunction constantTimeEq(a: string, b: string): boolean {\n const ab = new TextEncoder().encode(a)\n const bb = new TextEncoder().encode(b)\n if (ab.length !== bb.length) return false\n return timingSafeEqual(Buffer.from(ab), Buffer.from(bb))\n}\n\nfunction newToken(): string {\n const buf = new Uint8Array(24)\n crypto.getRandomValues(buf)\n let s = \"\"\n for (let i = 0; i < buf.length; i++) s += String.fromCharCode(buf[i]!)\n return btoa(s).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\")\n}\n",
869
+ "sha256": "bf0abfe2a8e8ccf2f38371e5cb4d0a9986f738b27ef169c75d412aec6655f0ca"
870
+ },
871
+ {
872
+ "path": "session/index.ts",
873
+ "contents": "/**\n * @hyper/session — encrypted, signed-cookie session middleware.\n *\n * - Cookie stores a short opaque id; payload lives in the pluggable\n * `SessionStore` (in-memory by default).\n * - Session id is HMAC-signed; mismatch = session discarded, no 500.\n * - Rolling expiration by default; consumer can opt into absolute.\n * - `ctx.session.set/get/destroy/regenerate` for handler use.\n */\n\nimport { timingSafeEqual } from \"node:crypto\"\nimport { type Middleware, coerce } from \"@hyper/core\"\n\nexport { csrfGuard, type CsrfConfig } from \"./csrf.ts\"\n\nexport interface SessionStore {\n get(id: string): Promise<Record<string, unknown> | undefined>\n set(id: string, data: Record<string, unknown>, ttlMs: number): Promise<void>\n destroy(id: string): Promise<void>\n}\n\nexport function memorySessions(): SessionStore {\n const m = new Map<string, { data: Record<string, unknown>; expires: number }>()\n return {\n async get(id) {\n const v = m.get(id)\n if (!v) return undefined\n if (v.expires < Date.now()) {\n m.delete(id)\n return undefined\n }\n return v.data\n },\n async set(id, data, ttlMs) {\n m.set(id, { data, expires: Date.now() + ttlMs })\n },\n async destroy(id) {\n m.delete(id)\n },\n }\n}\n\nexport interface SessionConfig {\n readonly secret: string\n readonly store?: SessionStore\n readonly cookieName?: string\n /** ms. Default: 7 days. */\n readonly ttlMs?: number\n /** Renew cookie on every request. Default: true. */\n readonly rolling?: boolean\n /** Secure cookie flag. Default: true. */\n readonly secure?: boolean\n readonly sameSite?: \"Strict\" | \"Lax\" | \"None\"\n /** Opt out of the 32-byte secret check. Off by default. */\n readonly allowShortSecret?: boolean\n}\n\nexport const MIN_SESSION_SECRET_BYTES = 32\n\nexport function validateSessionSecret(\n secret: string,\n opts: { readonly allowShort?: boolean } = {},\n): void {\n if (opts.allowShort) return\n const bytes = new TextEncoder().encode(secret).byteLength\n if (bytes < MIN_SESSION_SECRET_BYTES) {\n throw new Error(\n `@hyper/session: secret is ${bytes} bytes; minimum is ${MIN_SESSION_SECRET_BYTES}. Why: short HMAC secrets let an attacker forge session ids with modest compute. Fix: generate a 32+ byte secret (e.g., \\`openssl rand -base64 48\\`) or pass \\`allowShortSecret: true\\` to opt out.`,\n )\n }\n}\n\nexport interface SessionHandle {\n readonly id: string\n get<T = unknown>(key: string): T | undefined\n set(key: string, value: unknown): void\n destroy(): void\n regenerate(): void\n readonly dirty: boolean\n}\n\ndeclare module \"@hyper/core\" {\n interface AppContext {\n readonly session?: SessionHandle\n }\n}\n\nconst WEEK = 7 * 24 * 60 * 60 * 1000\n\nexport function session(config: SessionConfig): Middleware {\n validateSessionSecret(config.secret, { allowShort: config.allowShortSecret ?? false })\n const store = config.store ?? memorySessions()\n const name = config.cookieName ?? \"hyper.sid\"\n const ttl = config.ttlMs ?? WEEK\n const rolling = config.rolling ?? true\n const secure = config.secure ?? true\n const sameSite = config.sameSite ?? \"Lax\"\n\n const mw: Middleware = async ({ ctx, req, next }) => {\n const existingId = await readSignedCookie(req, name, config.secret)\n let id = existingId ?? \"\"\n let data: Record<string, unknown> = {}\n if (id) {\n data = (await store.get(id)) ?? {}\n }\n let dirty = false\n let destroyed = false\n let regenerated = false\n\n const handle: SessionHandle = Object.freeze({\n get id() {\n return id\n },\n get<T>(k: string): T | undefined {\n return data[k] as T | undefined\n },\n set(k, v) {\n data[k] = v\n dirty = true\n },\n destroy() {\n destroyed = true\n dirty = true\n },\n regenerate() {\n regenerated = true\n dirty = true\n },\n get dirty() {\n return dirty\n },\n })\n ;(ctx as { session?: SessionHandle }).session = handle\n\n const out = await next()\n const res = out instanceof Response ? out : coerce(out)\n if (destroyed && id) {\n await store.destroy(id)\n res.headers.append(\n \"set-cookie\",\n `${name}=; Path=/; Max-Age=0; HttpOnly; SameSite=${sameSite}${secure ? \"; Secure\" : \"\"}`,\n )\n return res\n }\n if (regenerated || (!id && Object.keys(data).length > 0)) {\n if (id) await store.destroy(id)\n id = newId()\n }\n if (dirty || (rolling && id)) {\n if (!id) id = newId()\n await store.set(id, data, ttl)\n const signed = await signCookie(id, config.secret)\n res.headers.append(\n \"set-cookie\",\n `${name}=${signed}; Path=/; Max-Age=${Math.floor(ttl / 1000)}; HttpOnly; SameSite=${sameSite}${\n secure ? \"; Secure\" : \"\"\n }`,\n )\n }\n return res\n }\n ;(mw as unknown as { __hyperTag: string }).__hyperTag = \"@hyper/session\"\n return mw\n}\n\nasync function readSignedCookie(\n req: Request,\n name: string,\n secret: string,\n): Promise<string | null> {\n const header = req.headers.get(\"cookie\")\n if (!header) return null\n for (const part of header.split(/;\\s*/)) {\n const [k, ...rest] = part.split(\"=\")\n if (k === name && rest.length) {\n const value = rest.join(\"=\")\n const id = await verifyCookie(value, secret)\n if (id) return id\n }\n }\n return null\n}\n\nfunction newId(): string {\n return crypto.randomUUID().replace(/-/g, \"\")\n}\n\nasync function signCookie(id: string, secret: string): Promise<string> {\n const key = await importHmac(secret)\n const sig = await crypto.subtle.sign(\"HMAC\", key, new TextEncoder().encode(id))\n const b64 = b64url(new Uint8Array(sig))\n return `${id}.${b64}`\n}\n\nasync function verifyCookie(value: string, secret: string): Promise<string | null> {\n const dot = value.lastIndexOf(\".\")\n if (dot <= 0) return null\n const id = value.slice(0, dot)\n const sigB64 = value.slice(dot + 1)\n const key = await importHmac(secret)\n const expected = new Uint8Array(\n await crypto.subtle.sign(\"HMAC\", key, new TextEncoder().encode(id)),\n )\n const actual = fromB64url(sigB64)\n if (expected.length !== actual.length) return null\n if (!timingSafeEqual(Buffer.from(expected), Buffer.from(actual))) return null\n return id\n}\n\nasync function importHmac(secret: string): Promise<CryptoKey> {\n return crypto.subtle.importKey(\n \"raw\",\n new TextEncoder().encode(secret),\n { name: \"HMAC\", hash: \"SHA-256\" },\n false,\n [\"sign\"],\n )\n}\n\nfunction b64url(b: Uint8Array): string {\n let s = \"\"\n for (let i = 0; i < b.length; i++) s += String.fromCharCode(b[i]!)\n return btoa(s).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\")\n}\nfunction fromB64url(s: string): Uint8Array {\n const pad = s.length % 4 === 0 ? 0 : 4 - (s.length % 4)\n const b64 = (s + \"====\".slice(0, pad)).replace(/-/g, \"+\").replace(/_/g, \"/\")\n const bin = atob(b64)\n const out = new Uint8Array(bin.length)\n for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i)\n return out\n}\n",
874
+ "sha256": "24334ad83d54e7cc091c50cc1e4edf65855a3ff66d68c245eb60dc4472da6ee4"
875
+ },
876
+ {
877
+ "path": "session/sqlite.ts",
878
+ "contents": "/**\n * bun:sqlite-backed SessionStore — persistent, single-node production.\n * For multi-node, use Redis.\n */\n\nimport { Database } from \"bun:sqlite\"\nimport type { SessionStore } from \"./index.ts\"\n\nexport interface SqliteSessionOptions {\n readonly path?: string\n}\n\nexport function sqliteSessions(opts: SqliteSessionOptions = {}): SessionStore & {\n readonly sweep: () => void\n readonly close: () => void\n} {\n const path = opts.path ?? \"./.hyper/sessions.sqlite\"\n const db = new Database(path, { create: true })\n db.exec(\"PRAGMA journal_mode = WAL; PRAGMA synchronous = NORMAL;\")\n db.exec(`\n CREATE TABLE IF NOT EXISTS hyper_sessions (\n id TEXT PRIMARY KEY,\n data TEXT NOT NULL,\n expires INTEGER NOT NULL\n );\n CREATE INDEX IF NOT EXISTS hyper_sessions_expires ON hyper_sessions (expires);\n `)\n const stmtGet = db.prepare(\"SELECT data, expires FROM hyper_sessions WHERE id = ?\")\n const stmtSet = db.prepare(\n \"INSERT OR REPLACE INTO hyper_sessions (id, data, expires) VALUES (?, ?, ?)\",\n )\n const stmtDel = db.prepare(\"DELETE FROM hyper_sessions WHERE id = ?\")\n const stmtSweep = db.prepare(\"DELETE FROM hyper_sessions WHERE expires < ?\")\n\n return {\n async get(id) {\n const row = stmtGet.get(id) as { data: string; expires: number } | undefined\n if (!row) return undefined\n if (row.expires < Date.now()) {\n stmtDel.run(id)\n return undefined\n }\n return JSON.parse(row.data) as Record<string, unknown>\n },\n async set(id, data, ttlMs) {\n stmtSet.run(id, JSON.stringify(data), Date.now() + ttlMs)\n },\n async destroy(id) {\n stmtDel.run(id)\n },\n sweep: () => {\n stmtSweep.run(Date.now())\n },\n close: () => db.close(),\n }\n}\n",
879
+ "sha256": "e721a9469d938493d7602ed827e7fe7622ae189d945b333f6d94cffd0421caef"
880
+ }
881
+ ],
882
+ "subpaths": {}
883
+ },
884
+ "subscribe": {
885
+ "name": "subscribe",
886
+ "version": "0.1.0",
887
+ "description": "route.subscribe() primitive — projects to SSE, MCP resource notifications, tRPC subscriptions.",
888
+ "readme": "# @hyper/subscribe\n\n`subscribe()` primitive — projects to SSE, MCP resource notifications, tRPC subscriptions.\n\n## Install\n\n```bash\nbun add @hyper/subscribe\n```\n\n## Usage\n\n```ts\nimport { Hyper } from \"@hyper/core\"\nimport { subscribe } from \"@hyper/subscribe\"\n\nexport default new Hyper()\n .use(\n subscribe(\"/events\", async function* () {\n yield { data: { type: \"tick\", at: Date.now() } }\n }),\n )\n .listen(3000)\n```\n\n## Docs\n\nSee the [main README](../../README.md) and [docs/](../../docs) for guides and integration recipes.\n\n## License\n\nMIT\n",
889
+ "registryDeps": [
890
+ "core"
891
+ ],
892
+ "peerDeps": {},
893
+ "optionalPeerDeps": {},
894
+ "files": [
895
+ {
896
+ "path": "subscribe/index.ts",
897
+ "contents": "/**\n * @hyper/subscribe — `route.subscribe()` primitive.\n *\n * A single subscription definition projects to:\n * - HTTP: Server-Sent Events on GET <path>\n * - MCP: resource notifications (via resources/subscribe)\n * - tRPC: subscription procedure\n *\n * The user writes an async generator producing events; we serialize to\n * each protocol. This v0 ships the HTTP→SSE projection; the MCP + tRPC\n * adapters read the same iterator factory so the shape stays uniform.\n */\n\nimport { route, sse } from \"@hyper/core\"\nimport type { CallableRoute, RouteMeta } from \"@hyper/core\"\n\nexport interface SubscribeEvent<T = unknown> {\n readonly event?: string\n readonly data: T\n readonly id?: string\n}\n\nexport type SubscribeHandler<T> = (args: {\n req: Request\n signal: AbortSignal\n}) => AsyncIterable<SubscribeEvent<T>>\n\nexport interface SubscribeOptions {\n readonly name?: string\n readonly description?: string\n readonly meta?: RouteMeta\n}\n\n/**\n * Build a subscription route. Returns a `CallableRoute` so callers can\n * call it in tests or via MCP/tRPC without a server.\n */\nexport function subscribe<T>(\n path: string,\n handler: SubscribeHandler<T>,\n opts: SubscribeOptions = {},\n): CallableRoute {\n const meta: RouteMeta = {\n ...(opts.name && { name: opts.name }),\n ...(opts.description && { description: opts.description }),\n ...(opts.meta ?? {}),\n subscription: true,\n }\n return route\n .get(path)\n .meta(meta)\n .handle(async (c) => {\n const controller = new AbortController()\n const signal = controller.signal\n c.req.signal.addEventListener(\"abort\", () => controller.abort(), { once: true })\n const source = handler({ req: c.req, signal })\n const stringified = (async function* () {\n for await (const ev of source) {\n yield {\n data: typeof ev.data === \"string\" ? ev.data : JSON.stringify(ev.data),\n ...(ev.event && { event: ev.event }),\n ...(ev.id && { id: ev.id }),\n }\n }\n })()\n return sse(stringified, { signal })\n })\n}\n\n/**\n * Collect a finite number of events from a subscription — useful for\n * MCP `resources/read` snapshots and for tests.\n */\nexport async function collect<T>(\n handler: SubscribeHandler<T>,\n n: number,\n req: Request = new Request(\"http://local/\"),\n): Promise<readonly SubscribeEvent<T>[]> {\n const out: SubscribeEvent<T>[] = []\n const ctrl = new AbortController()\n for await (const ev of handler({ req, signal: ctrl.signal })) {\n out.push(ev)\n if (out.length >= n) break\n }\n ctrl.abort()\n return out\n}\n",
898
+ "sha256": "e096aff51c6d3f5b1cd6fc94964b2b77fc101b4ea0f2c7f4e484acd1cb665783"
899
+ }
900
+ ],
901
+ "subpaths": {}
902
+ },
903
+ "testing": {
904
+ "name": "testing",
905
+ "version": "0.1.0",
906
+ "description": "Testing helpers for Hyper apps — app.test, fakeRequest, matchers, memory stores, fuzz.",
907
+ "readme": "# @hyper/testing\n\nTesting helpers for Hyper apps — `call`, matchers, memory stores, fuzz.\n\n## Install\n\n```bash\nbun add -d @hyper/testing\n```\n\n## Usage\n\n```ts\nimport { Hyper, ok } from \"@hyper/core\"\nimport { assertResponse, call } from \"@hyper/testing\"\n\nconst app = new Hyper().get(\"/\", () => ok({ hello: \"world\" }))\n\nconst res = await call(app, \"GET\", \"/\")\nassertResponse(res).isOk()\n```\n\n`call` accepts both `Hyper` instances and built `HyperApp` values, so the same helper works for unit and integration tests.\n\n## Docs\n\nSee the [main README](../../README.md) and [docs/](../../docs) for guides and integration recipes.\n\n## License\n\nMIT\n",
908
+ "registryDeps": [
909
+ "core"
910
+ ],
911
+ "peerDeps": {},
912
+ "optionalPeerDeps": {},
913
+ "files": [
914
+ {
915
+ "path": "testing/assert.ts",
916
+ "contents": "/**\n * `assertResponse(res)` — fluent matcher that integrates with bun:test\n * expect failures.\n *\n * Each matcher returns `this` for chaining. On mismatch we throw with a\n * descriptive message; bun:test surfaces the throw as a failed expect.\n */\n\n/** Minimal subset of match semantics we need — deep partial equality. */\nfunction matches(actual: unknown, expected: unknown): boolean {\n if (expected instanceof RegExp) return typeof actual === \"string\" && expected.test(actual)\n if (Array.isArray(expected)) {\n if (!Array.isArray(actual) || actual.length < expected.length) return false\n return expected.every((e, i) => matches(actual[i], e))\n }\n if (expected && typeof expected === \"object\") {\n if (!actual || typeof actual !== \"object\") return false\n for (const [k, v] of Object.entries(expected)) {\n if (!matches((actual as Record<string, unknown>)[k], v)) return false\n }\n return true\n }\n return Object.is(actual, expected)\n}\n\nexport interface Assertion {\n readonly raw: Response\n hasStatus(code: number): Assertion\n hasHeader(name: string, matcher?: string | RegExp): Assertion\n hasCookie(name: string): Assertion\n hasJson(partial: unknown): Promise<Assertion>\n hasText(text: string | RegExp): Promise<Assertion>\n isError(shape?: { code?: string; status?: number; message?: string | RegExp }): Promise<Assertion>\n json<T = unknown>(): Promise<T>\n}\n\nexport function assertResponse(res: Response): Assertion {\n const self: Assertion = {\n raw: res,\n hasStatus(code) {\n if (res.status !== code) throw new Error(`expected status ${code}, got ${res.status}`)\n return self\n },\n hasHeader(name, matcher) {\n const v = res.headers.get(name)\n if (v === null) throw new Error(`expected header ${name} to be set`)\n if (matcher !== undefined && !matches(v, matcher)) {\n throw new Error(`header ${name}=${v} did not match ${String(matcher)}`)\n }\n return self\n },\n hasCookie(name) {\n const set = res.headers.getSetCookie\n ? res.headers.getSetCookie()\n : [res.headers.get(\"set-cookie\") ?? \"\"]\n const found = set.some((c) => c?.startsWith(`${name}=`))\n if (!found) throw new Error(`expected Set-Cookie for ${name}`)\n return self\n },\n async hasJson(partial) {\n const ct = res.headers.get(\"content-type\") ?? \"\"\n if (!ct.includes(\"application/json\"))\n throw new Error(`expected JSON response, got ${ct || \"(none)\"}`)\n const body = await res.clone().json()\n if (!matches(body, partial)) {\n throw new Error(\n `response body did not match.\\n expected: ${JSON.stringify(partial)}\\n actual: ${JSON.stringify(body)}`,\n )\n }\n return self\n },\n async hasText(text) {\n const body = await res.clone().text()\n if (!matches(body, text)) throw new Error(`response text did not match ${String(text)}`)\n return self\n },\n async isError(shape = {}) {\n const body = (await res\n .clone()\n .json()\n .catch(() => null)) as { error?: Record<string, unknown> } | null\n const err = body?.error ?? null\n if (!err) throw new Error(\"expected Hyper error envelope\")\n if (shape.code !== undefined && err.code !== shape.code) {\n throw new Error(`expected error code ${shape.code}, got ${String(err.code)}`)\n }\n if (shape.status !== undefined && res.status !== shape.status) {\n throw new Error(`expected error status ${shape.status}, got ${res.status}`)\n }\n if (shape.message !== undefined && !matches(err.message, shape.message)) {\n throw new Error(`error.message did not match ${String(shape.message)}`)\n }\n return self\n },\n async json<T>() {\n return (await res.clone().json()) as T\n },\n }\n return self\n}\n",
917
+ "sha256": "f6ff3c616f6c33f691d185a989becf36ebb93fb42d5e3faf0a85163efa08013d"
918
+ },
919
+ {
920
+ "path": "testing/auth.ts",
921
+ "contents": "/**\n * Auth test helpers.\n *\n * `signJwtHS256({...})` — produces a real signed HS256 JWT you can put\n * in the `authorization: Bearer <token>` header. Bun.CryptoHasher is the\n * same primitive `@hyper/auth-jwt` uses, so end-to-end tests exercise\n * the production verify path.\n */\n\nimport { createHmac } from \"node:crypto\"\n\nexport interface SignJwtOptions {\n readonly secret: string\n readonly payload: Record<string, unknown>\n readonly expiresInMs?: number\n readonly now?: () => number\n}\n\nexport function signJwtHS256(opts: SignJwtOptions): string {\n const now = (opts.now ?? Date.now)()\n const header = { alg: \"HS256\", typ: \"JWT\" }\n const payload: Record<string, unknown> = {\n iat: Math.floor(now / 1000),\n ...(opts.expiresInMs ? { exp: Math.floor((now + opts.expiresInMs) / 1000) } : {}),\n ...opts.payload,\n }\n const h = b64url(JSON.stringify(header))\n const p = b64url(JSON.stringify(payload))\n const sig = createHmac(\"sha256\", opts.secret).update(`${h}.${p}`).digest()\n return `${h}.${p}.${b64urlBuf(sig)}`\n}\n\n/** Convenience — `asUser({ id })` + a real bearer header in one call. */\nexport function bearerAsUser(opts: {\n readonly secret: string\n readonly id: string\n readonly roles?: readonly string[]\n readonly expiresInMs?: number\n}): { authorization: string } {\n const token = signJwtHS256({\n secret: opts.secret,\n payload: { sub: opts.id, ...(opts.roles && { roles: opts.roles }) },\n ...(opts.expiresInMs !== undefined && { expiresInMs: opts.expiresInMs }),\n })\n return { authorization: `Bearer ${token}` }\n}\n\nfunction b64url(s: string): string {\n return b64urlBuf(Buffer.from(s, \"utf8\"))\n}\nfunction b64urlBuf(buf: Buffer): string {\n return buf.toString(\"base64\").replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\")\n}\n",
922
+ "sha256": "af50e6bd96c175ccf03e6c18b516605b59163831a3ebf5e11cfdee08fa15402b"
923
+ },
924
+ {
925
+ "path": "testing/call.ts",
926
+ "contents": "/**\n * `call(app, method, path, init?)` — runs the full pipeline in-process,\n * returns a real Response. The recommended integration-test primitive.\n *\n * Accepts either a `HyperApp` (from `app({...})`) or a `Hyper`\n * instance (from `new Hyper()` / `hyper()`). The latter is lowered via\n * `.build()` so tests never need to call it manually.\n */\n\nimport { type HttpMethod, Hyper, type HyperApp } from \"@hyper/core\"\nimport { type FakeRequestInit, fakeRequest } from \"./request.ts\"\n\nexport function call(\n app: HyperApp | Hyper,\n method: HttpMethod,\n path: string,\n init: FakeRequestInit = {},\n): Promise<Response> {\n const built = app instanceof Hyper ? app.build() : app\n return built.fetch(fakeRequest(method, path, init))\n}\n",
927
+ "sha256": "2cc5d6ead4d3687cab3ba274cee244524bbc4634223ca0a0f14bb39fe2f552d8"
928
+ },
929
+ {
930
+ "path": "testing/capture.ts",
931
+ "contents": "/**\n * `captureEvents(app)` — attaches a plugin that siphons every wide log\n * event into an in-memory array, so tests can assert on observability\n * contracts (what routes emit, what redaction shape, etc.).\n *\n * We don't import @hyper/log here to keep the peer-dep boundary clean;\n * instead we install a plugin with a request.after hook that drains\n * whatever structured log the ctx exposes. Users can additionally plug\n * @hyper/log's `captureDrain` for full fidelity.\n */\n\nimport type { HyperApp, HyperPlugin, TestOverrides } from \"@hyper/core\"\n\nexport interface CapturedEvent {\n readonly method: string\n readonly path: string\n readonly status: number\n readonly durationMs: number\n readonly [k: string]: unknown\n}\n\nexport interface EventCapture {\n readonly events: readonly CapturedEvent[]\n /** Convenience: find the first event matching a partial shape. */\n find(match: Partial<CapturedEvent>): CapturedEvent | undefined\n /** True if at least one event matches. */\n has(match: Partial<CapturedEvent>): boolean\n /** Clear recorded events — useful between test cases. */\n clear(): void\n /** Stop recording and detach. */\n stop(): void\n}\n\n/**\n * Install the capture plugin on a test-scoped app clone. Returns the\n * capture handle plus a new HyperApp with capture active.\n */\nexport function captureEvents(\n base: HyperApp,\n opts: TestOverrides = {},\n): {\n readonly app: HyperApp\n readonly capture: EventCapture\n} {\n const events: CapturedEvent[] = []\n let stopped = false\n const plugin: HyperPlugin = {\n name: \"@hyper/testing:capture\",\n request: {\n after({ req, res, route }) {\n if (stopped) return\n const url = new URL(req.url)\n events.push({\n method: req.method,\n path: route?.path ?? url.pathname,\n status: res.status,\n durationMs: 0,\n })\n },\n },\n }\n const existing = opts.plugins ?? {}\n const app = base.test({\n ...opts,\n plugins: { ...existing, add: [...(existing.add ?? []), plugin] },\n })\n const capture: EventCapture = {\n get events() {\n return events\n },\n find: (m) => events.find((e) => matchesPartial(e, m)),\n has: (m) => events.some((e) => matchesPartial(e, m)),\n clear: () => {\n events.length = 0\n },\n stop: () => {\n stopped = true\n },\n }\n return { app, capture }\n}\n\nfunction matchesPartial(ev: CapturedEvent, m: Partial<CapturedEvent>): boolean {\n for (const [k, v] of Object.entries(m)) {\n if ((ev as Record<string, unknown>)[k] !== v) return false\n }\n return true\n}\n",
932
+ "sha256": "f2ece1c2ca6b6669ca3d28ca03a66fde8c1e27f81e1c20a0355218e7b6b7ff23"
933
+ },
934
+ {
935
+ "path": "testing/clock.ts",
936
+ "contents": "/**\n * Test clock — a single abstraction plugins can consume instead of\n * `Date.now()`. Plugins accept `clock?: Clock` in their config; at test\n * time you pass a fake clock and call `advanceTime(ms)`.\n *\n * We deliberately do NOT monkey-patch global `Date`. Explicit clock\n * injection is the contract.\n */\n\nexport interface Clock {\n readonly now: () => number\n}\n\nexport interface TestClock extends Clock {\n /** Move the clock forward by `ms`. Pending timers are not drained. */\n readonly advance: (ms: number) => void\n /** Reset the clock to `t0`. */\n readonly reset: (t0?: number) => void\n}\n\nexport function testClock(t0 = 1_700_000_000_000): TestClock {\n let t = t0\n return {\n now: () => t,\n advance: (ms: number) => {\n t += ms\n },\n reset: (r = t0) => {\n t = r\n },\n }\n}\n\nexport const systemClock: Clock = { now: () => Date.now() }\n\n/** Ambient helper — tests call this to advance a shared clock. */\nlet ambient: TestClock | undefined\nexport function useTestClock(clock: TestClock): TestClock {\n ambient = clock\n return clock\n}\nexport function advanceTime(ms: number): void {\n if (!ambient)\n throw new Error(\"advanceTime: no ambient test clock — call useTestClock(testClock()) first\")\n ambient.advance(ms)\n}\n",
937
+ "sha256": "97998c629b1329357201c77520115bb987f098aa669d7dfc40918536915e73db"
938
+ },
939
+ {
940
+ "path": "testing/fuzz.ts",
941
+ "contents": "/**\n * @hyper/testing/fuzz — request-boundary attack corpus.\n *\n * `fuzzRoute(app, \"POST /users\")` hammers the given route with a\n * baseline set of nasty inputs. Each entry expects the framework to\n * answer with a 4xx (never a 500, never a hang, never silent corruption).\n *\n * Consumers get parity coverage with the framework's own fuzz suite.\n */\n\nimport type { HttpMethod, HyperApp } from \"@hyper/core\"\nimport { fakeRequest } from \"./request.ts\"\n\nexport interface FuzzCase {\n readonly name: string\n /** Expected status range. Default: 4xx. */\n readonly expectStatus?: (status: number) => boolean\n /** Builds a Request for the given route target. */\n readonly build: (method: HttpMethod, path: string) => Request\n}\n\nconst OVERSIZED_BODY = \"x\".repeat(2 * 1024 * 1024) // 2 MB (> 1 MB default)\n\nconst ATTACK_CASES: readonly FuzzCase[] = [\n {\n name: \"proto-pollution via __proto__\",\n build: (m, p) =>\n fakeRequest(m, p, { json: JSON.parse('{\"__proto__\": {\"polluted\": true}, \"ok\": 1}') }),\n },\n {\n name: \"proto-pollution via constructor.prototype\",\n build: (m, p) =>\n fakeRequest(m, p, {\n json: JSON.parse('{\"constructor\": {\"prototype\": {\"p\": 1}}}'),\n }),\n },\n {\n name: \"oversized body (>1MB)\",\n build: (m, p) => fakeRequest(m, p, { text: OVERSIZED_BODY }),\n expectStatus: (s) => s === 413 || (s >= 400 && s < 500),\n },\n {\n name: \"malformed JSON\",\n expectStatus: (s) => s === 400,\n build: (m, p) =>\n new Request(new URL(`http://local${p}`), {\n method: m,\n headers: { \"content-type\": \"application/json\" },\n body: \"{oops\",\n }),\n },\n {\n name: \"path traversal in path segment\",\n build: (m, _p) => fakeRequest(m, \"/../../../etc/passwd\"),\n expectStatus: (s) => s >= 400 && s < 500,\n },\n {\n name: \"smuggled method via X-HTTP-Method-Override\",\n build: (m, p) => fakeRequest(m, p, { headers: { \"x-http-method-override\": \"DELETE\" } }),\n // The framework must NOT coerce the method — 200/404 acceptable, but\n // never a 'DELETE' being honored. We only check the request succeeds\n // or fails without silently swapping verbs.\n expectStatus: (s) => s < 500,\n },\n {\n name: \"overlong header\",\n build: (m, p) => fakeRequest(m, p, { headers: { \"x-big\": \"y\".repeat(65_000) } }),\n expectStatus: (s) => s < 500,\n },\n {\n name: \"XSS-ish cookie\",\n build: (m, p) => fakeRequest(m, p, { cookie: { sid: '\"><script>alert(1)</script>' } }),\n expectStatus: (s) => s < 500,\n },\n {\n name: \"null byte in path\",\n build: (m, _p) => fakeRequest(m, \"/users/\\x00id\"),\n expectStatus: (s) => s >= 400 && s < 500,\n },\n {\n name: \"empty JSON body\",\n build: (m, p) =>\n fakeRequest(m, p, { text: \"\", headers: { \"content-type\": \"application/json\" } }),\n expectStatus: (s) => s < 500,\n },\n {\n name: \"duplicate content-length\",\n build: (m, p) =>\n new Request(new URL(`http://local${p}`), {\n method: m,\n headers: { \"content-type\": \"application/json\", \"content-length\": \"100\" },\n body: \"{}\",\n }),\n expectStatus: (s) => s < 500,\n },\n]\n\nexport interface FuzzReport {\n readonly method: HttpMethod\n readonly path: string\n readonly passed: readonly FuzzResult[]\n readonly failed: readonly FuzzResult[]\n readonly ok: boolean\n}\n\nexport interface FuzzResult {\n readonly case: string\n readonly status: number\n readonly accepted: boolean\n readonly error?: string\n}\n\n/**\n * Run every case in the corpus against `METHOD PATH`. Returns a report\n * describing which cases were handled correctly (non-500, matching the\n * expected status predicate).\n */\nexport async function fuzzRoute(\n app: HyperApp,\n entry: `${HttpMethod} ${string}`,\n opts: { readonly rounds?: number; readonly extraCases?: readonly FuzzCase[] } = {},\n): Promise<FuzzReport> {\n const [method, path] = entry.split(\" \") as [HttpMethod, string]\n const cases = [...ATTACK_CASES, ...(opts.extraCases ?? [])]\n const rounds = Math.max(1, opts.rounds ?? 1)\n const passed: FuzzResult[] = []\n const failed: FuzzResult[] = []\n for (const c of cases) {\n const expect = c.expectStatus ?? defaultExpect\n for (let i = 0; i < rounds; i++) {\n let status = 0\n let error: string | undefined\n try {\n const res = await app.fetch(c.build(method, path))\n status = res.status\n } catch (e) {\n error = e instanceof Error ? e.message : String(e)\n }\n const accepted = !error && expect(status)\n const result: FuzzResult = {\n case: c.name,\n status,\n accepted,\n ...(error !== undefined && { error }),\n }\n ;(accepted ? passed : failed).push(result)\n }\n }\n return { method, path, passed, failed, ok: failed.length === 0 }\n}\n\nfunction defaultExpect(status: number): boolean {\n return status >= 400 && status < 500\n}\n\n/** Re-exported so framework-internal tests can use the same corpus. */\nexport { ATTACK_CASES }\n",
942
+ "sha256": "7f4e25aa453748ed9a300426e2ef19517a0a38b2f65a9574881352a02cad5e30"
943
+ },
944
+ {
945
+ "path": "testing/index.ts",
946
+ "contents": "/**\n * @hyper/testing — ergonomic primitives for testing Hyper apps.\n *\n * Philosophy: testing a Hyper route should feel like testing a plain\n * async function. No supertest, no dev-server juggling, no mock server.\n */\n\nexport { assertResponse, type Assertion } from \"./assert.ts\"\nexport { call } from \"./call.ts\"\nexport {\n captureEvents,\n type CapturedEvent,\n type EventCapture,\n} from \"./capture.ts\"\nexport {\n advanceTime,\n type Clock,\n systemClock,\n testClock,\n type TestClock,\n useTestClock,\n} from \"./clock.ts\"\nexport {\n type KvEntry,\n type KvStore,\n type MemoryDb,\n memoryDb,\n memoryKv,\n memoryRateLimiter,\n type MemoryRateLimiterOptions,\n type MemoryTable,\n memoryTable,\n type RateLimitResult,\n} from \"./memory-stores.ts\"\nexport { mockCtx } from \"./mock-ctx.ts\"\nexport { mockPlugin } from \"./mock-plugin.ts\"\nexport { asUser, fakeRequest, type FakeRequestInit, type FakeUser } from \"./request.ts\"\nexport { type ManifestSnapshot, snapshotManifest } from \"./snapshot.ts\"\n",
947
+ "sha256": "db07b29f69d579f5a05dd64120e28cff18d6e7b2d743d9aa30a5360129874bce"
948
+ },
949
+ {
950
+ "path": "testing/memory-stores.ts",
951
+ "contents": "/**\n * Memory stores — drop-in replacements for the Store shapes that\n * @hyper/cache, @hyper/idempotency, @hyper/rate-limit, @hyper/session\n * accept. Identical surface, zero persistence, deterministic for tests.\n *\n * These shapes intentionally don't import from the consumer packages —\n * they duplicate the tiny interfaces so `@hyper/testing` can serve any\n * of them without cyclic deps.\n */\n\nimport type { Clock } from \"./clock.ts\"\nimport { systemClock } from \"./clock.ts\"\n\n// Generic KV ------------------------------------------------------------\n\nexport interface KvEntry<V> {\n readonly value: V\n readonly expiresAt: number | null\n}\n\nexport interface KvStore<V> {\n get(key: string): Promise<V | undefined>\n set(key: string, value: V, ttlMs?: number): Promise<void>\n delete(key: string): Promise<void>\n}\n\nexport function memoryKv<V>(clock: Clock = systemClock): KvStore<V> {\n const map = new Map<string, KvEntry<V>>()\n return {\n async get(key) {\n const e = map.get(key)\n if (!e) return undefined\n if (e.expiresAt !== null && e.expiresAt <= clock.now()) {\n map.delete(key)\n return undefined\n }\n return e.value\n },\n async set(key, value, ttlMs) {\n map.set(key, { value, expiresAt: ttlMs ? clock.now() + ttlMs : null })\n },\n async delete(key) {\n map.delete(key)\n },\n }\n}\n\n// Rate limiter ----------------------------------------------------------\n\nexport interface RateLimitResult {\n readonly allowed: boolean\n readonly remaining: number\n readonly resetMs: number\n}\n\nexport interface MemoryRateLimiterOptions {\n readonly limit: number\n readonly windowMs: number\n readonly clock?: Clock\n}\n\nexport function memoryRateLimiter(opts: MemoryRateLimiterOptions): {\n check: (key: string) => Promise<RateLimitResult>\n reset: (key?: string) => void\n} {\n const clock = opts.clock ?? systemClock\n const buckets = new Map<string, { tokens: number; resetAt: number }>()\n return {\n async check(key: string) {\n const now = clock.now()\n let b = buckets.get(key)\n if (!b || b.resetAt <= now) {\n b = { tokens: opts.limit, resetAt: now + opts.windowMs }\n buckets.set(key, b)\n }\n if (b.tokens <= 0) {\n return { allowed: false, remaining: 0, resetMs: b.resetAt - now }\n }\n b.tokens -= 1\n return { allowed: true, remaining: b.tokens, resetMs: b.resetAt - now }\n },\n reset(key) {\n if (key === undefined) buckets.clear()\n else buckets.delete(key)\n },\n }\n}\n\n// Tiny in-memory SQL-ish \"db\" ------------------------------------------\n\nexport interface MemoryTable<Row> {\n readonly name: string\n readonly rows: Row[]\n insert(row: Row): Row\n find(predicate: (r: Row) => boolean): Row | undefined\n filter(predicate: (r: Row) => boolean): Row[]\n update(predicate: (r: Row) => boolean, patch: Partial<Row>): Row | undefined\n delete(predicate: (r: Row) => boolean): number\n clear(): void\n}\n\nexport function memoryTable<Row>(name: string): MemoryTable<Row> {\n const rows: Row[] = []\n return {\n name,\n rows,\n insert(r) {\n rows.push(r)\n return r\n },\n find: (p) => rows.find(p),\n filter: (p) => rows.filter(p),\n update: (p, patch) => {\n const i = rows.findIndex(p)\n if (i < 0) return undefined\n rows[i] = { ...(rows[i] as object), ...(patch as object) } as Row\n return rows[i]\n },\n delete: (p) => {\n const before = rows.length\n for (let i = rows.length - 1; i >= 0; i--) {\n if (p(rows[i] as Row)) rows.splice(i, 1)\n }\n return before - rows.length\n },\n clear() {\n rows.length = 0\n },\n }\n}\n\n/**\n * `memoryDb()` — a bag of named tables with helpers. Covers the ~20\n * queries that `apps/examples/todo` exercises. Grows organically.\n */\nexport interface MemoryDb {\n table<Row>(name: string): MemoryTable<Row>\n reset(): void\n}\n\nexport function memoryDb(): MemoryDb {\n const tables = new Map<string, MemoryTable<unknown>>()\n return {\n table<Row>(name: string): MemoryTable<Row> {\n let t = tables.get(name) as MemoryTable<Row> | undefined\n if (!t) {\n t = memoryTable<Row>(name)\n tables.set(name, t as MemoryTable<unknown>)\n }\n return t\n },\n reset() {\n for (const t of tables.values()) t.clear()\n },\n }\n}\n",
952
+ "sha256": "dce325e9bf13406884d4b541ff734c58448da00cd397aeac524195752e5b5420"
953
+ },
954
+ {
955
+ "path": "testing/mock-ctx.ts",
956
+ "contents": "/**\n * `mockCtx(overrides)` — build a typed AppContext stub for calling a\n * route as a plain async function via `route.callable({ ctx })`.\n *\n * The type-level augmentation on `AppContext` is respected: if your app\n * declares `interface AppContext { db: Db; user?: User }`, then\n * `mockCtx({ db: fakeDb })` returns `AppContext` with those fields set.\n */\n\nimport type { AppContext } from \"@hyper/core\"\n\nexport function mockCtx<T extends Partial<AppContext> = Partial<AppContext>>(\n overrides: T = {} as T,\n): AppContext {\n return overrides as unknown as AppContext\n}\n",
957
+ "sha256": "83ac8641be49b823d4247bbd57a4698f124659b51befa858dbf50eb4294b16e2"
958
+ },
959
+ {
960
+ "path": "testing/mock-plugin.ts",
961
+ "contents": "/**\n * `mockPlugin({...})` — one-liner plugin for inserting arbitrary test\n * behavior without writing a full plugin file.\n *\n * app.test({ plugins: { add: [mockPlugin({\n * name: \"stub-metrics\",\n * request: { after: ({ res }) => counts.push(res.status) },\n * })] } })\n */\n\nimport type { HyperPlugin } from \"@hyper/core\"\n\nexport function mockPlugin(plugin: HyperPlugin): HyperPlugin {\n return plugin\n}\n",
962
+ "sha256": "86b3febf8af4bf7e1e5720cb9e90da085e20894dee37f2f41f1db4aff19c1b4c"
963
+ },
964
+ {
965
+ "path": "testing/request.ts",
966
+ "contents": "/**\n * Request builders — `fakeRequest` and `asUser`.\n *\n * `fakeRequest` is a thin ergonomic wrapper around `new Request()` that\n * accepts the fields tests actually want (json, form, auth, cookie, ip)\n * without the ceremony of building headers by hand.\n */\n\nimport type { HttpMethod } from \"@hyper/core\"\n\nexport interface FakeRequestInit {\n readonly query?: Record<string, string | number | boolean>\n readonly json?: unknown\n readonly form?: FormData | Record<string, string>\n readonly text?: string\n readonly auth?: string\n readonly cookie?: Record<string, string> | string\n readonly ip?: string\n readonly headers?: Record<string, string>\n readonly origin?: string\n}\n\nconst DEFAULT_ORIGIN = \"http://local\"\n\nexport function fakeRequest(method: HttpMethod, path: string, init: FakeRequestInit = {}): Request {\n const origin = init.origin ?? DEFAULT_ORIGIN\n const url = new URL(path.startsWith(\"/\") ? `${origin}${path}` : path)\n if (init.query) {\n for (const [k, v] of Object.entries(init.query)) url.searchParams.set(k, String(v))\n }\n const headers = new Headers(init.headers ?? {})\n if (init.auth) headers.set(\"authorization\", init.auth)\n if (init.cookie !== undefined) {\n const cookie = typeof init.cookie === \"string\" ? init.cookie : toCookieHeader(init.cookie)\n headers.set(\"cookie\", cookie)\n }\n if (init.ip) headers.set(\"x-forwarded-for\", init.ip)\n\n let body: BodyInit | undefined\n if (init.json !== undefined) {\n body = JSON.stringify(init.json)\n if (!headers.has(\"content-type\")) headers.set(\"content-type\", \"application/json\")\n } else if (init.form !== undefined) {\n body = init.form instanceof FormData ? init.form : toFormData(init.form)\n } else if (init.text !== undefined) {\n body = init.text\n if (!headers.has(\"content-type\")) headers.set(\"content-type\", \"text/plain; charset=utf-8\")\n }\n\n return new Request(url, { method, headers, ...(body !== undefined && { body }) })\n}\n\nfunction toCookieHeader(jar: Record<string, string>): string {\n return Object.entries(jar)\n .map(([k, v]) => `${k}=${encodeURIComponent(v)}`)\n .join(\"; \")\n}\n\nfunction toFormData(obj: Record<string, string>): FormData {\n const fd = new FormData()\n for (const [k, v] of Object.entries(obj)) fd.set(k, v)\n return fd\n}\n\n/**\n * Partial-ctx helper — produces a `user`-shaped stub for routes guarded\n * by `@hyper/auth-jwt`. Bypasses JWT verify in tests that don't need a\n * signed token; use `@hyper/testing/auth` for real-signed tokens.\n */\nexport interface FakeUser {\n readonly id: string\n readonly roles?: readonly string[]\n readonly claims?: Record<string, unknown>\n}\n\nexport function asUser(id: string | FakeUser): { readonly user: FakeUser } {\n const u: FakeUser = typeof id === \"string\" ? { id } : id\n return { user: u }\n}\n",
967
+ "sha256": "f556e16beb614a03eacb81d17a9d564e01040a1ae8ca61a159d515801629b826"
968
+ },
969
+ {
970
+ "path": "testing/snapshot.ts",
971
+ "contents": "/**\n * `snapshotManifest(app)` — guards the public contract in a single\n * assertion. Snapshots OpenAPI + MCP + client manifests together with a\n * stable structure. A breaking change to any surface fails the snapshot.\n */\n\nimport type { HyperApp } from \"@hyper/core\"\n\nexport interface ManifestSnapshot {\n readonly openapi: unknown\n readonly mcp: unknown\n readonly client: unknown\n}\n\nexport function snapshotManifest(app: HyperApp): ManifestSnapshot {\n return {\n openapi: app.toOpenAPI({ title: \"snapshot\", version: \"0.0.0\" }),\n mcp: app.toMCPManifest(),\n client: app.toClientManifest(),\n }\n}\n",
972
+ "sha256": "12133ab8c2c558cf0d0d4da63fb2c21a38b3c492fdc93c6d8fb38d4afd45341d"
973
+ },
974
+ {
975
+ "path": "testing/types.ts",
976
+ "contents": "/**\n * Type-level test helpers. Re-exports `expectTypeOf` from expect-type\n * plus Hyper-shaped narrowing helpers.\n */\n\nimport type { HttpMethod, HyperApp, Route } from \"@hyper/core\"\n\nexport { expectTypeOf } from \"expect-type\"\n\nexport interface RouteAssertion<R> {\n readonly input: {\n toEqualTypeOf<T>(): R extends { __input: infer I } ? ([I] extends [T] ? true : never) : never\n }\n readonly output: {\n toEqualTypeOf<T>(): R extends { __output: infer O } ? ([O] extends [T] ? true : never) : never\n }\n}\n\nexport function expectRoute<R>(_route: R): RouteAssertion<R> {\n return {\n input: { toEqualTypeOf: () => true as never },\n output: { toEqualTypeOf: () => true as never },\n }\n}\n\n/** Runtime helper used in compile-time-shaped tests — always true. */\nexport function expectApp(app: HyperApp): {\n hasRoute(entry: `${HttpMethod} ${string}`): boolean\n} {\n return {\n hasRoute(entry) {\n const [method, path] = entry.split(\" \") as [HttpMethod, string]\n return app.routeList.some((r: Route) => r.method === method && r.path === path)\n },\n }\n}\n",
977
+ "sha256": "747f2349a2aadc8c7c44a63008e671283e3748dc0dae7502192d4b3df89bbc7f"
978
+ }
979
+ ],
980
+ "subpaths": {}
981
+ },
982
+ "trpc": {
983
+ "name": "trpc",
984
+ "version": "0.1.0",
985
+ "description": "tRPC bridge — mount tRPC into Hyper, or convert a tRPC router to Hyper routes.",
986
+ "readme": "# @hyper/trpc\n\ntRPC bridge — mount tRPC into Hyper, or convert a tRPC router into Hyper routes.\n\n## Install\n\n```bash\nbun add @hyper/trpc\n```\n\n## Usage\n\nMount a tRPC router inside a Hyper app:\n\n```ts\nimport { Hyper } from \"@hyper/core\"\nimport { trpcPlugin } from \"@hyper/trpc\"\nimport { appRouter } from \"./trpc/router.ts\"\n\nexport default new Hyper()\n .use(trpcPlugin({ router: appRouter, prefix: \"/trpc\" }))\n .listen(3000)\n```\n\nOr convert a Hyper app into a tRPC router:\n\n```ts\nimport { toTrpcRouter } from \"@hyper/trpc\"\nimport { initTRPC } from \"@trpc/server\"\nimport app from \"./app.ts\"\n\nconst t = initTRPC.create()\nexport const trpcRouter = toTrpcRouter(app, { t })\n```\n\n## Docs\n\nSee the [main README](../../README.md) and [docs/](../../docs) for guides and integration recipes.\n\n## License\n\nMIT\n",
987
+ "registryDeps": [
988
+ "core"
989
+ ],
990
+ "peerDeps": {},
991
+ "optionalPeerDeps": {
992
+ "@trpc/server": ">=11.0.0 <12.0.0"
993
+ },
994
+ "files": [
995
+ {
996
+ "path": "trpc/bridge.ts",
997
+ "contents": "/**\n * Mount a tRPC router at a path prefix inside a Hyper app.\n *\n * Strategy: we use tRPC's `callTRPCProcedure` API if available; otherwise\n * fall back to a minimal POST-JSON protocol we know how to drive:\n *\n * POST /trpc/<procPath> body: { input?: ... }\n * response: { result: { data: T } } or { error: { code, message, data } }\n *\n * The tRPC server package itself provides `fetchRequestHandler` — users who\n * want full batch/stream semantics should use that directly and simply add\n * an explicit Hyper route that forwards the request.\n */\n\nimport type { HyperPlugin, Middleware } from \"@hyper/core\"\nimport type { TrpcBridgeOptions, TrpcRouterLike } from \"./types.ts\"\n\nexport function trpcPlugin<Ctx = unknown>(\n router: TrpcRouterLike,\n opts: TrpcBridgeOptions<Ctx> = {},\n): HyperPlugin {\n const prefix = (opts.prefix ?? \"/trpc\").replace(/\\/+$/, \"\")\n const trpcAny = router as { _def?: { procedures?: Record<string, unknown> } }\n const procedures = trpcAny?._def?.procedures ?? {}\n\n return {\n name: \"@hyper/trpc\",\n async build(app) {\n // The bridge is delivered as an HTTP handler mounted via\n // `trpcHandler(path)` returned below. This `build` hook is a no-op\n // but reserved for future dynamic route registration.\n void app\n },\n }\n}\n\n/**\n * Build a fetch-compatible handler you can mount as a Hyper route:\n *\n * app({ routes: [\n * route.post(\"/trpc/:proc\").handle(trpcHandler(router)),\n * route.post(\"/trpc/:proc/*\").handle(trpcHandler(router)),\n * ]})\n *\n * Or as a catch-all via your framework of choice.\n */\nexport function trpcHandler<Ctx>(\n router: TrpcRouterLike,\n opts: TrpcBridgeOptions<Ctx> = {},\n): (c: {\n req: Request\n params: { proc?: string }\n ctx: Ctx\n body: unknown\n}) => Promise<Response> {\n const trpcAny = router as {\n _def?: { procedures?: Record<string, { _def?: { type?: string } }> }\n }\n const procedures = trpcAny._def?.procedures ?? {}\n return async (c) => {\n const procName = c.params.proc\n if (!procName) {\n return json({ error: { code: \"BAD_REQUEST\", message: \"missing procedure\" } }, 400)\n }\n const proc = procedures[procName]\n if (!proc) {\n return json({ error: { code: \"NOT_FOUND\", message: `unknown procedure: ${procName}` } }, 404)\n }\n const rawInput = (c.body as { input?: unknown } | undefined)?.input\n try {\n const callable = proc as unknown as (args: {\n ctx: unknown\n input: unknown\n rawInput: unknown\n type: string\n }) => Promise<unknown>\n const ctx = opts.createContext ? await opts.createContext({ req: c.req, ctx: c.ctx }) : c.ctx\n const type = proc._def?.type ?? (c.req.method === \"GET\" ? \"query\" : \"mutation\")\n const data = await callable({ ctx, input: rawInput, rawInput, type })\n return json({ result: { data } }, 200)\n } catch (e) {\n opts.onError?.({ path: procName, error: e })\n const err = e as { code?: string; message?: string; cause?: unknown }\n return json(\n {\n error: {\n code: err.code ?? \"INTERNAL_SERVER_ERROR\",\n message: err.message ?? \"tRPC procedure failed\",\n },\n },\n 500,\n )\n }\n }\n}\n\n/**\n * sharedCtxMiddleware — exposes Hyper's AppContext as tRPC's ctx, so the\n * same ctx singletons (db, log, session) power both transports.\n */\nexport function sharedCtxMiddleware(): Middleware {\n return async (args) => args.next()\n}\n\nfunction json(body: unknown, status: number): Response {\n return new Response(JSON.stringify(body), {\n status,\n headers: { \"content-type\": \"application/json\" },\n })\n}\n",
998
+ "sha256": "b4e9cfd7f45971496911fbbb8bca1e8712d8a061a6bb9ba73d4f97e2e7b1da79"
999
+ },
1000
+ {
1001
+ "path": "trpc/index.ts",
1002
+ "contents": "/**\n * @hyper/trpc — two-way bridge.\n *\n * - `trpcHandler(router)` — mount a tRPC router into a Hyper route handler.\n * - `trpcToHyper(router)` — convert a tRPC router to an array of Hyper routes.\n * - `trpcPlugin(router)` — register the bridge as a Hyper plugin (reserved for\n * future dynamic-route registration).\n *\n * The bridge is structurally typed so @hyper/trpc loads without @trpc/server\n * installed. Users pass their real router and we narrow at the call site.\n */\n\nexport { sharedCtxMiddleware, trpcHandler, trpcPlugin } from \"./bridge.ts\"\nexport { trpcToHyper } from \"./to-hyper.ts\"\nexport type { TrpcBridgeOptions, TrpcRouterLike, TrpcToHyperOptions } from \"./types.ts\"\n",
1003
+ "sha256": "e8e1ea2d4a4f212b34cdae02fe0214903195c47e44004f522ac9faee290dbd95"
1004
+ },
1005
+ {
1006
+ "path": "trpc/to-hyper.ts",
1007
+ "contents": "/**\n * trpcToHyper — walks a tRPC router's procedure map and emits Hyper routes.\n * Used for migration: drop a tRPC router into a Hyper app with one call.\n *\n * We emit one POST route per procedure at `${prefix}/${procPath}`, body =\n * the raw tRPC input. Query-vs-mutation distinction is preserved via\n * `meta.trpc.type`.\n */\n\nimport { type CallableRoute, route } from \"@hyper/core\"\nimport type { TrpcRouterLike, TrpcToHyperOptions } from \"./types.ts\"\n\nexport function trpcToHyper(\n router: TrpcRouterLike,\n opts: TrpcToHyperOptions = {},\n): readonly CallableRoute[] {\n const prefix = (opts.prefix ?? \"/trpc\").replace(/\\/+$/, \"\")\n const procedures =\n (\n router as {\n _def?: { procedures?: Record<string, { _def?: { type?: string } }> }\n }\n )._def?.procedures ?? {}\n\n const routes: CallableRoute[] = []\n for (const [procName, proc] of Object.entries(procedures)) {\n const type = proc._def?.type ?? \"mutation\"\n const meta = {\n name: procName,\n trpc: { type },\n ...(opts.mapMeta ? opts.mapMeta(procName, proc) : {}),\n }\n const r = route\n .post(`${prefix}/${procName}`)\n .meta(meta)\n .handle(async (c) => {\n const callable = proc as unknown as (args: {\n ctx: unknown\n input: unknown\n rawInput: unknown\n type: string\n }) => Promise<unknown>\n const input = (c.body as { input?: unknown })?.input\n const data = await callable({ ctx: c.ctx, input, rawInput: input, type })\n return { result: { data } }\n }) as CallableRoute\n routes.push(r)\n }\n return routes\n}\n",
1008
+ "sha256": "afc025df796f2ab8a45f05a9e420e16383b46b5a4c753e10ea149c7b3e39619d"
1009
+ },
1010
+ {
1011
+ "path": "trpc/types.ts",
1012
+ "contents": "/**\n * Structural tRPC types — we avoid importing @trpc/server so the package\n * loads without the peer installed. Users pass their real router and we\n * type-narrow at the call site.\n */\n\n// biome-ignore lint/suspicious/noExplicitAny: structural tRPC router\nexport type TrpcRouterLike = any\n\nexport interface TrpcBridgeOptions<Ctx = unknown> {\n /** Path prefix for the HTTP POST JSON handler. Defaults to /trpc. */\n readonly prefix?: string\n /** Build a tRPC context from the Hyper AppContext. */\n readonly createContext?: (args: {\n readonly req: Request\n readonly ctx: Ctx\n }) => Promise<unknown> | unknown\n /** Error-bridge hook — called on tRPC error before response. */\n readonly onError?: (args: { readonly path?: string; readonly error: unknown }) => void\n}\n\nexport interface TrpcToHyperOptions {\n readonly prefix?: string\n /** Convert a tRPC procedure shape to Hyper `meta` (auth, mcp, etc.). */\n readonly mapMeta?: (procName: string, procedure: unknown) => Record<string, unknown>\n}\n",
1013
+ "sha256": "f855779991397c774988f48de0e7d032b7913db90b9dc482a67f8c10e4c9022c"
1014
+ }
1015
+ ],
1016
+ "subpaths": {}
1017
+ }
1018
+ } as const