ts-procedures 10.1.2 → 10.2.1

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 (272) hide show
  1. package/CHANGELOG.md +33 -86
  2. package/README.md +11 -6
  3. package/agent_config/bin/postinstall.mjs +20 -89
  4. package/agent_config/bin/setup.mjs +51 -264
  5. package/agent_config/lib/install-skills.mjs +108 -0
  6. package/agent_config/{claude-code/skills → skills}/ts-procedures/anti-patterns.md +8 -0
  7. package/agent_config/{claude-code/skills → skills}/ts-procedures/api-reference.md +25 -22
  8. package/agent_config/{claude-code/skills → skills}/ts-procedures/patterns.md +24 -13
  9. package/build/adapters/hono/envelope-parity.test.js +6 -17
  10. package/build/adapters/hono/envelope-parity.test.js.map +1 -1
  11. package/build/adapters/hono/handlers/http-stream.d.ts +2 -1
  12. package/build/adapters/hono/handlers/http-stream.js +9 -2
  13. package/build/adapters/hono/handlers/http-stream.js.map +1 -1
  14. package/build/adapters/hono/handlers/http.d.ts +2 -1
  15. package/build/adapters/hono/handlers/http.js +9 -2
  16. package/build/adapters/hono/handlers/http.js.map +1 -1
  17. package/build/adapters/hono/handlers/rpc.d.ts +2 -1
  18. package/build/adapters/hono/handlers/rpc.js +9 -2
  19. package/build/adapters/hono/handlers/rpc.js.map +1 -1
  20. package/build/adapters/hono/handlers/stream.d.ts +2 -1
  21. package/build/adapters/hono/handlers/stream.js +9 -2
  22. package/build/adapters/hono/handlers/stream.js.map +1 -1
  23. package/build/adapters/hono/index.d.ts +14 -7
  24. package/build/adapters/hono/index.js +45 -5
  25. package/build/adapters/hono/index.js.map +1 -1
  26. package/build/adapters/hono/index.test.js +55 -0
  27. package/build/adapters/hono/index.test.js.map +1 -1
  28. package/build/adapters/hono/request.d.ts +7 -0
  29. package/build/adapters/hono/request.js +15 -0
  30. package/build/adapters/hono/request.js.map +1 -1
  31. package/build/adapters/hono/types.d.ts +31 -0
  32. package/build/adapters/hono/types.js.map +1 -1
  33. package/build/codegen/bin/cli.js +1 -1
  34. package/build/codegen/bin/cli.js.map +1 -1
  35. package/build/codegen/constants.js +2 -2
  36. package/build/codegen/constants.js.map +1 -1
  37. package/build/codegen/emit/api-route.js +1 -1
  38. package/build/codegen/emit/api-route.js.map +1 -1
  39. package/build/codegen/emit/scope-file.js +1 -1
  40. package/build/codegen/emit/scope-file.js.map +1 -1
  41. package/build/codegen/emit-errors.integration.test.js +2 -2
  42. package/build/codegen/emit-errors.integration.test.js.map +1 -1
  43. package/build/codegen/resolve-envelope.js +1 -1
  44. package/build/codegen/resolve-envelope.js.map +1 -1
  45. package/build/codegen/test-helpers/run-tsc.js +1 -1
  46. package/build/codegen/test-helpers/run-tsc.js.map +1 -1
  47. package/build/core/create-http-stream.d.ts +3 -14
  48. package/build/core/create-http-stream.js +4 -2
  49. package/build/core/create-http-stream.js.map +1 -1
  50. package/build/core/create-http.d.ts +2 -9
  51. package/build/core/create-http.js +3 -2
  52. package/build/core/create-http.js.map +1 -1
  53. package/build/core/create-http.test-d.js +104 -0
  54. package/build/core/create-http.test-d.js.map +1 -0
  55. package/build/core/create-stream.js +6 -2
  56. package/build/core/create-stream.js.map +1 -1
  57. package/build/core/create.js +5 -4
  58. package/build/core/create.js.map +1 -1
  59. package/build/core/create.test.js +63 -0
  60. package/build/core/create.test.js.map +1 -1
  61. package/build/core/internal.d.ts +16 -0
  62. package/build/core/internal.js +22 -0
  63. package/build/core/internal.js.map +1 -1
  64. package/build/core/procedures.d.ts +9 -16
  65. package/build/core/procedures.js +4 -0
  66. package/build/core/procedures.js.map +1 -1
  67. package/build/core/types.d.ts +85 -4
  68. package/build/errors.d.ts +33 -0
  69. package/build/errors.js +91 -0
  70. package/build/errors.js.map +1 -0
  71. package/build/errors.test.d.ts +1 -0
  72. package/build/errors.test.js +122 -0
  73. package/build/errors.test.js.map +1 -0
  74. package/build/exports.d.ts +1 -2
  75. package/build/exports.js +0 -4
  76. package/build/exports.js.map +1 -1
  77. package/build/implementations/http/doc-registry.d.ts +28 -0
  78. package/build/implementations/http/doc-registry.js +96 -0
  79. package/build/implementations/http/doc-registry.js.map +1 -0
  80. package/build/implementations/http/doc-registry.test.d.ts +1 -0
  81. package/build/implementations/http/doc-registry.test.js +516 -0
  82. package/build/implementations/http/doc-registry.test.js.map +1 -0
  83. package/build/implementations/http/error-taxonomy.d.ts +249 -0
  84. package/build/implementations/http/error-taxonomy.js +252 -0
  85. package/build/implementations/http/error-taxonomy.js.map +1 -0
  86. package/build/implementations/http/error-taxonomy.test.d.ts +1 -0
  87. package/build/implementations/http/error-taxonomy.test.js +399 -0
  88. package/build/implementations/http/error-taxonomy.test.js.map +1 -0
  89. package/build/implementations/http/express-rpc/error-taxonomy.test.d.ts +1 -0
  90. package/build/implementations/http/express-rpc/error-taxonomy.test.js +83 -0
  91. package/build/implementations/http/express-rpc/error-taxonomy.test.js.map +1 -0
  92. package/build/implementations/http/express-rpc/index.d.ts +125 -0
  93. package/build/implementations/http/express-rpc/index.js +216 -0
  94. package/build/implementations/http/express-rpc/index.js.map +1 -0
  95. package/build/implementations/http/express-rpc/index.test.d.ts +1 -0
  96. package/build/implementations/http/express-rpc/index.test.js +684 -0
  97. package/build/implementations/http/express-rpc/index.test.js.map +1 -0
  98. package/build/implementations/http/express-rpc/types.d.ts +11 -0
  99. package/build/implementations/http/express-rpc/types.js +2 -0
  100. package/build/implementations/http/express-rpc/types.js.map +1 -0
  101. package/build/implementations/http/hono-api/error-taxonomy.test.d.ts +1 -0
  102. package/build/implementations/http/hono-api/error-taxonomy.test.js +137 -0
  103. package/build/implementations/http/hono-api/error-taxonomy.test.js.map +1 -0
  104. package/build/implementations/http/hono-api/index.d.ts +151 -0
  105. package/build/implementations/http/hono-api/index.js +344 -0
  106. package/build/implementations/http/hono-api/index.js.map +1 -0
  107. package/build/implementations/http/hono-api/index.test.d.ts +1 -0
  108. package/build/implementations/http/hono-api/index.test.js +992 -0
  109. package/build/implementations/http/hono-api/index.test.js.map +1 -0
  110. package/build/implementations/http/hono-api/types.d.ts +13 -0
  111. package/build/implementations/http/hono-api/types.js +2 -0
  112. package/build/implementations/http/hono-api/types.js.map +1 -0
  113. package/build/implementations/http/hono-rpc/error-taxonomy.test.d.ts +1 -0
  114. package/build/implementations/http/hono-rpc/error-taxonomy.test.js +64 -0
  115. package/build/implementations/http/hono-rpc/error-taxonomy.test.js.map +1 -0
  116. package/build/implementations/http/hono-rpc/index.d.ts +130 -0
  117. package/build/implementations/http/hono-rpc/index.js +209 -0
  118. package/build/implementations/http/hono-rpc/index.js.map +1 -0
  119. package/build/implementations/http/hono-rpc/index.test.d.ts +1 -0
  120. package/build/implementations/http/hono-rpc/index.test.js +828 -0
  121. package/build/implementations/http/hono-rpc/index.test.js.map +1 -0
  122. package/build/implementations/http/hono-rpc/types.d.ts +11 -0
  123. package/build/implementations/http/hono-rpc/types.js +2 -0
  124. package/build/implementations/http/hono-rpc/types.js.map +1 -0
  125. package/build/implementations/http/hono-stream/error-taxonomy.test.d.ts +1 -0
  126. package/build/implementations/http/hono-stream/error-taxonomy.test.js +159 -0
  127. package/build/implementations/http/hono-stream/error-taxonomy.test.js.map +1 -0
  128. package/build/implementations/http/hono-stream/index.d.ts +171 -0
  129. package/build/implementations/http/hono-stream/index.js +415 -0
  130. package/build/implementations/http/hono-stream/index.js.map +1 -0
  131. package/build/implementations/http/hono-stream/index.test.d.ts +1 -0
  132. package/build/implementations/http/hono-stream/index.test.js +1383 -0
  133. package/build/implementations/http/hono-stream/index.test.js.map +1 -0
  134. package/build/implementations/http/hono-stream/types.d.ts +15 -0
  135. package/build/implementations/http/hono-stream/types.js +2 -0
  136. package/build/implementations/http/hono-stream/types.js.map +1 -0
  137. package/build/implementations/http/on-request-error.test.d.ts +1 -0
  138. package/build/implementations/http/on-request-error.test.js +173 -0
  139. package/build/implementations/http/on-request-error.test.js.map +1 -0
  140. package/build/implementations/http/route-errors.test.d.ts +1 -0
  141. package/build/implementations/http/route-errors.test.js +139 -0
  142. package/build/implementations/http/route-errors.test.js.map +1 -0
  143. package/build/implementations/types.d.ts +192 -0
  144. package/build/implementations/types.js +2 -0
  145. package/build/implementations/types.js.map +1 -0
  146. package/build/index.d.ts +165 -0
  147. package/build/index.js +258 -0
  148. package/build/index.js.map +1 -0
  149. package/build/index.test.d.ts +1 -0
  150. package/build/index.test.js +920 -0
  151. package/build/index.test.js.map +1 -0
  152. package/build/schema/compute-schema.js +1 -5
  153. package/build/schema/compute-schema.js.map +1 -1
  154. package/build/schema/compute-schema.test.js +0 -13
  155. package/build/schema/compute-schema.test.js.map +1 -1
  156. package/build/schema/extract-json-schema.d.ts +2 -0
  157. package/build/schema/extract-json-schema.js +12 -0
  158. package/build/schema/extract-json-schema.js.map +1 -0
  159. package/build/schema/extract-json-schema.test.d.ts +1 -0
  160. package/build/schema/extract-json-schema.test.js +23 -0
  161. package/build/schema/extract-json-schema.test.js.map +1 -0
  162. package/build/schema/parser.d.ts +28 -0
  163. package/build/schema/parser.js +170 -0
  164. package/build/schema/parser.js.map +1 -0
  165. package/build/schema/parser.test.d.ts +1 -0
  166. package/build/schema/parser.test.js +120 -0
  167. package/build/schema/parser.test.js.map +1 -0
  168. package/build/schema/resolve-schema-lib.d.ts +12 -0
  169. package/build/schema/resolve-schema-lib.js +11 -0
  170. package/build/schema/resolve-schema-lib.js.map +1 -0
  171. package/build/schema/resolve-schema-lib.test.d.ts +1 -0
  172. package/build/schema/resolve-schema-lib.test.js +17 -0
  173. package/build/schema/resolve-schema-lib.test.js.map +1 -0
  174. package/build/schema/typebox.d.ts +1 -1
  175. package/build/schema/typebox.js +2 -20
  176. package/build/schema/typebox.js.map +1 -1
  177. package/build/schema/typebox.test.js +0 -10
  178. package/build/schema/typebox.test.js.map +1 -1
  179. package/build/schema/types.d.ts +8 -0
  180. package/build/schema/types.js +2 -0
  181. package/build/schema/types.js.map +1 -0
  182. package/build/server/context.d.ts +12 -2
  183. package/build/server/context.js +49 -4
  184. package/build/server/context.js.map +1 -1
  185. package/build/server/context.test.js +32 -1
  186. package/build/server/context.test.js.map +1 -1
  187. package/build/server/doc-registry.js +0 -3
  188. package/build/server/doc-registry.js.map +1 -1
  189. package/build/server/doc-registry.test.js +0 -31
  190. package/build/server/doc-registry.test.js.map +1 -1
  191. package/build/server/index.d.ts +0 -1
  192. package/build/server/index.js +0 -1
  193. package/build/server/index.js.map +1 -1
  194. package/build/server/types.d.ts +6 -27
  195. package/build/stack-utils.d.ts +25 -0
  196. package/build/stack-utils.js +95 -0
  197. package/build/stack-utils.js.map +1 -0
  198. package/build/stack-utils.test.d.ts +1 -0
  199. package/build/stack-utils.test.js +80 -0
  200. package/build/stack-utils.test.js.map +1 -0
  201. package/docs/ai-agent-setup.md +19 -27
  202. package/docs/client-error-handling.md +1 -1
  203. package/docs/core.md +8 -0
  204. package/docs/http-integrations.md +42 -28
  205. package/package.json +8 -8
  206. package/src/adapters/astro/README.md +2 -0
  207. package/src/adapters/hono/envelope-parity.test.ts +6 -18
  208. package/src/adapters/hono/handlers/http-stream.ts +12 -3
  209. package/src/adapters/hono/handlers/http.ts +12 -3
  210. package/src/adapters/hono/handlers/rpc.ts +12 -3
  211. package/src/adapters/hono/handlers/stream.ts +12 -3
  212. package/src/adapters/hono/index.test.ts +70 -0
  213. package/src/adapters/hono/index.ts +53 -11
  214. package/src/adapters/hono/request.ts +18 -0
  215. package/src/adapters/hono/types.ts +37 -0
  216. package/src/codegen/bin/cli.ts +1 -1
  217. package/src/codegen/constants.ts +2 -2
  218. package/src/codegen/emit/api-route.ts +1 -1
  219. package/src/codegen/emit/scope-file.ts +1 -2
  220. package/src/codegen/emit-errors.integration.test.ts +2 -2
  221. package/src/codegen/resolve-envelope.ts +1 -1
  222. package/src/codegen/test-helpers/run-tsc.ts +0 -1
  223. package/src/core/create-http-stream.ts +20 -13
  224. package/src/core/create-http.test-d.ts +154 -0
  225. package/src/core/create-http.ts +19 -8
  226. package/src/core/create-stream.ts +12 -4
  227. package/src/core/create.test.ts +74 -0
  228. package/src/core/create.ts +10 -5
  229. package/src/core/internal.ts +35 -0
  230. package/src/core/procedures.ts +4 -0
  231. package/src/core/types.ts +99 -4
  232. package/src/exports.ts +6 -4
  233. package/src/schema/compute-schema.test.ts +0 -16
  234. package/src/schema/compute-schema.ts +1 -5
  235. package/src/schema/typebox.test.ts +0 -12
  236. package/src/schema/typebox.ts +2 -21
  237. package/src/server/context.test.ts +39 -1
  238. package/src/server/context.ts +54 -4
  239. package/src/server/doc-registry.test.ts +0 -34
  240. package/src/server/doc-registry.ts +0 -3
  241. package/src/server/index.ts +0 -1
  242. package/src/server/types.ts +8 -27
  243. package/agent_config/claude-code/.claude-plugin/plugin.json +0 -5
  244. package/agent_config/claude-code/agents/ts-procedures-architect.md +0 -138
  245. package/agent_config/copilot/copilot-instructions.md +0 -521
  246. package/agent_config/cursor/cursorrules +0 -521
  247. package/agent_config/lib/install-claude.mjs +0 -57
  248. package/build/schema/strip-internal-keywords.d.ts +0 -18
  249. package/build/schema/strip-internal-keywords.js +0 -33
  250. package/build/schema/strip-internal-keywords.js.map +0 -1
  251. package/build/schema/strip-internal-keywords.test.js +0 -38
  252. package/build/schema/strip-internal-keywords.test.js.map +0 -1
  253. package/build/server/spec-version.d.ts +0 -24
  254. package/build/server/spec-version.js +0 -26
  255. package/build/server/spec-version.js.map +0 -1
  256. package/build/version.d.ts +0 -9
  257. package/build/version.js +0 -11
  258. package/build/version.js.map +0 -1
  259. package/docs/doc-envelope-spec-v1.md +0 -296
  260. package/docs/doc-envelope-v1.schema.json +0 -253
  261. package/src/schema/strip-internal-keywords.test.ts +0 -43
  262. package/src/schema/strip-internal-keywords.ts +0 -36
  263. package/src/server/spec-version.ts +0 -27
  264. package/src/version.ts +0 -11
  265. /package/agent_config/{claude-code/skills → skills}/ts-procedures/SKILL.md +0 -0
  266. /package/agent_config/{claude-code/skills → skills}/ts-procedures/checklist.md +0 -0
  267. /package/agent_config/{claude-code/skills → skills}/ts-procedures/templates/astro-catchall.md +0 -0
  268. /package/agent_config/{claude-code/skills → skills}/ts-procedures/templates/client.md +0 -0
  269. /package/agent_config/{claude-code/skills → skills}/ts-procedures/templates/hono.md +0 -0
  270. /package/agent_config/{claude-code/skills → skills}/ts-procedures/templates/procedure.md +0 -0
  271. /package/agent_config/{claude-code/skills → skills}/ts-procedures/templates/stream-procedure.md +0 -0
  272. /package/build/{schema/strip-internal-keywords.test.d.ts → core/create-http.test-d.d.ts} +0 -0
@@ -7,9 +7,9 @@ import { dispatchMidStreamError, dispatchPreStreamError } from '../../../server/
7
7
  import { buildStreamRouteDoc } from '../../../server/docs/stream-doc.js'
8
8
  import { resolveFactoryContext } from '../../../server/context.js'
9
9
  import { SseEventSequencer } from '../../../server/sse.js'
10
- import { respondToDispatch } from '../request.js'
10
+ import { respondToDispatch, runMiddlewareList } from '../request.js'
11
11
  import { midStreamErrorConfig, preStreamErrorConfig } from '../types.js'
12
- import type { DocAccumulator, HonoAppBuilderConfig, HonoFactoryItem } from '../types.js'
12
+ import type { DocAccumulator, HonoAppBuilderConfig, HonoFactoryItem, HonoRequestMiddleware } from '../types.js'
13
13
 
14
14
  export function installRpcStreamRoute(params: {
15
15
  app: Hono
@@ -18,8 +18,9 @@ export function installRpcStreamRoute(params: {
18
18
  cfg: HonoAppBuilderConfig
19
19
  docs: DocAccumulator
20
20
  streamMode: StreamMode
21
+ builderMiddleware?: HonoRequestMiddleware[]
21
22
  }): void {
22
- const { app, procedure, factoryItem, cfg, docs, streamMode } = params
23
+ const { app, procedure, factoryItem, cfg, docs, streamMode, builderMiddleware } = params
23
24
 
24
25
  const route = buildStreamRouteDoc(
25
26
  procedure,
@@ -29,8 +30,16 @@ export function installRpcStreamRoute(params: {
29
30
  )
30
31
  docs.push(route)
31
32
 
33
+ const combinedMw = [
34
+ ...(builderMiddleware ?? []),
35
+ ...(factoryItem.middleware ?? []),
36
+ ]
37
+
32
38
  const handler = async (c: Context) => {
33
39
  try {
40
+ const early = await runMiddlewareList(c, combinedMw)
41
+ if (early) return early
42
+
34
43
  const context = await resolveFactoryContext(factoryItem.factoryContext, c)
35
44
 
36
45
  const reqParams =
@@ -325,3 +325,73 @@ describe('HonoAppBuilder — per-factory streamMode override', () => {
325
325
  await res.text()
326
326
  })
327
327
  })
328
+
329
+ describe('HonoAppBuilder — middleware (new)', () => {
330
+ test('builder config.middleware and per-register middleware both run; register ones after builder ones', async () => {
331
+ const order: string[] = []
332
+ const builderMw = vi.fn(async (_c: any, next: any) => { order.push('builder'); await next() })
333
+ const regMw = vi.fn(async (_c: any, next: any) => { order.push('register'); await next() })
334
+
335
+ const P = Procedures<object, RPCConfig>()
336
+ P.Create('Ping', { scope: 'p', version: 1 }, async () => ({ ok: true }))
337
+
338
+ const app = new HonoAppBuilder({ middleware: [builderMw] })
339
+ .register(P, () => ({}), { middleware: [regMw] })
340
+ .build()
341
+
342
+ const res = await app.request('/p/ping/1', { method: 'POST', body: '{}', headers: { 'Content-Type': 'application/json' } })
343
+ expect(res.status).toBe(200)
344
+ expect(order).toEqual(['builder', 'register'])
345
+ })
346
+
347
+ test('middleware can short-circuit by throwing (flows to dispatch + onRequestError)', async () => {
348
+ const onRequestError = vi.fn()
349
+ const P = Procedures<object, RPCConfig>()
350
+ P.Create('Secret', { scope: 's', version: 1 }, async () => ({ secret: true }))
351
+
352
+ const app = new HonoAppBuilder({ onRequestError })
353
+ .register(P, () => ({}), {
354
+ middleware: [async () => { throw Object.assign(new Error('nope'), { status: 401 }) }],
355
+ })
356
+ .build()
357
+
358
+ const res = await app.request('/s/secret/1', { method: 'POST', body: '{}', headers: { 'Content-Type': 'application/json' } })
359
+ expect(res.status).toBe(500) // default path since no taxonomy matched the thrown Error
360
+ expect(onRequestError).toHaveBeenCalledTimes(1)
361
+ })
362
+
363
+ test('register options.context layers are composed with main factoryContext (later wins)', async () => {
364
+ const P = Procedures<{ tenant?: string; uid: string }, RPCConfig>()
365
+ P.Create('WhoAmI', { scope: 'w', version: 1 }, async (ctx) => ({ tenant: ctx.tenant, uid: ctx.uid }))
366
+
367
+ const base = (c: any) => ({ uid: c.req.header('x-uid') || 'u0' })
368
+ const tenantLayer = { tenant: 't-from-layer' }
369
+
370
+ const app = new HonoAppBuilder()
371
+ .register(P, base, { context: tenantLayer as any })
372
+ .build()
373
+
374
+ const res = await app.request('/w/who-am-i/1', {
375
+ method: 'POST',
376
+ headers: { 'Content-Type': 'application/json', 'x-uid': 'user-xyz' },
377
+ body: '{}',
378
+ })
379
+ expect(await res.json()).toEqual({ tenant: 't-from-layer', uid: 'user-xyz' })
380
+ })
381
+
382
+ test('chainable .use() installs and runs for procedures', async () => {
383
+ const calls: string[] = []
384
+ const mw = async (_c: any, next: any) => { calls.push('used'); await next() }
385
+
386
+ const P = Procedures<object, RPCConfig>()
387
+ P.Create('X', { scope: 'x', version: 1 }, async () => ({}))
388
+
389
+ const b = new HonoAppBuilder()
390
+ b.use(mw)
391
+ b.register(P, () => ({}))
392
+ const app = b.build()
393
+
394
+ await app.request('/x/x/1', { method: 'POST', body: '{}', headers: { 'Content-Type': 'application/json' } })
395
+ expect(calls).toContain('used')
396
+ })
397
+ })
@@ -21,10 +21,11 @@ import { installRpcRoute } from './handlers/rpc.js'
21
21
  import { installRpcStreamRoute } from './handlers/stream.js'
22
22
  import { installHttpRoute } from './handlers/http.js'
23
23
  import { installHttpStreamRoute } from './handlers/http-stream.js'
24
+ import { runMiddlewareList } from './request.js'
24
25
  import type { Context } from 'hono'
25
- import type { HonoAppBuilderConfig, HonoFactoryItem, ExtendProcedureDoc } from './types.js'
26
+ import type { HonoAppBuilderConfig, HonoFactoryItem, ExtendProcedureDoc, HonoRequestMiddleware, RegisterOptions } from './types.js'
26
27
 
27
- export type { HonoAppBuilderConfig, ExtendProcedureDoc, QueryParser } from './types.js'
28
+ export type { HonoAppBuilderConfig, ExtendProcedureDoc, QueryParser, HonoRequestMiddleware, RegisterOptions } from './types.js'
28
29
  export type { OnRequestErrorContext } from './types.js'
29
30
  export { sse } from '../../server/sse.js'
30
31
  export type { SSEOptions } from '../../server/sse.js'
@@ -46,6 +47,7 @@ export { DocRegistry } from '../../server/doc-registry.js'
46
47
  export class HonoAppBuilder<TStreamErrorData = unknown> {
47
48
  private readonly _app: Hono
48
49
  private readonly factories: HonoFactoryItem<any>[] = []
50
+ private readonly builderMiddleware: HonoRequestMiddleware[] = []
49
51
  private _docs: AnyHttpRouteDoc[] | null = null
50
52
  private _skipped: { name: string; reason: string }[] = []
51
53
  private _built = false
@@ -53,6 +55,10 @@ export class HonoAppBuilder<TStreamErrorData = unknown> {
53
55
  constructor(readonly config?: HonoAppBuilderConfig<TStreamErrorData>) {
54
56
  this._app = config?.app ?? new Hono()
55
57
 
58
+ if (config?.middleware?.length) {
59
+ this.builderMiddleware.push(...config.middleware)
60
+ }
61
+
56
62
  if (config?.onRequestStart) {
57
63
  this._app.use('*', async (c, next) => {
58
64
  config.onRequestStart!(c)
@@ -67,6 +73,36 @@ export class HonoAppBuilder<TStreamErrorData = unknown> {
67
73
  }
68
74
  }
69
75
 
76
+ /**
77
+ * Register Hono middleware to run for routes on this builder.
78
+ * If no pattern is provided, uses the builder's pathPrefix (or '*').
79
+ *
80
+ * The middleware is installed on the underlying Hono app immediately
81
+ * (classic Hono behavior) and is also available for per-procedure
82
+ * sequencing inside handlers.
83
+ */
84
+ use(patternOrMw?: string | HonoRequestMiddleware, maybeMw?: HonoRequestMiddleware): this {
85
+ let pattern: string | undefined
86
+ let mw: HonoRequestMiddleware
87
+ if (typeof patternOrMw === 'function') {
88
+ mw = patternOrMw
89
+ } else {
90
+ pattern = patternOrMw
91
+ mw = maybeMw!
92
+ }
93
+ if (!mw) return this
94
+
95
+ this.builderMiddleware.push(mw)
96
+
97
+ const effective = pattern ?? (this.config?.pathPrefix ? `${this.config.pathPrefix.replace(/\/$/, '')}/*` : '*')
98
+ this._app.use(effective as any, async (c, next) => {
99
+ const res = await mw(c, async () => { await next() })
100
+ if (res instanceof Response) return res
101
+ })
102
+
103
+ return this
104
+ }
105
+
70
106
  static makeRoutePath = _makeRoutePath
71
107
 
72
108
  get app(): Hono {
@@ -89,16 +125,20 @@ export class HonoAppBuilder<TStreamErrorData = unknown> {
89
125
  register<TFactory extends ProceduresFactory>(
90
126
  factory: TFactory,
91
127
  factoryContext: FactoryContextInit<ExtractContext<TFactory>, Context>,
92
- options?: {
93
- streamMode?: StreamMode
94
- extendProcedureDoc?: ExtendProcedureDoc
95
- },
128
+ options?: RegisterOptions<TFactory>,
96
129
  ): this {
130
+ // Compose contexts if options.context provided: [base, extra]
131
+ const effectiveContext: FactoryContextInit<ExtractContext<TFactory>, Context> =
132
+ options?.context
133
+ ? [factoryContext, options.context] as any
134
+ : factoryContext
135
+
97
136
  this.factories.push({
98
137
  factory,
99
- factoryContext,
138
+ factoryContext: effectiveContext,
100
139
  streamMode: options?.streamMode,
101
140
  extendProcedureDoc: options?.extendProcedureDoc,
141
+ middleware: options?.middleware,
102
142
  } as HonoFactoryItem<any>)
103
143
  // Invalidate docs cache so the next read recomputes.
104
144
  this._docs = null
@@ -154,22 +194,24 @@ export class HonoAppBuilder<TStreamErrorData = unknown> {
154
194
  const skipped: { name: string; reason: string }[] = []
155
195
  const cfg = this.config ?? {}
156
196
 
197
+ const builderMw = this.builderMiddleware
198
+
157
199
  for (const item of this.factories) {
158
200
  for (const procedure of item.factory.getProcedures().values() as Iterable<AnyProcedureRegistration<any, any>>) {
159
201
  switch (procedure.kind) {
160
202
  case 'rpc':
161
- installRpcRoute({ app: this._app, procedure: procedure as any, factoryItem: item, cfg, docs })
203
+ installRpcRoute({ app: this._app, procedure: procedure as any, factoryItem: item, cfg, docs, builderMiddleware: builderMw })
162
204
  break
163
205
  case 'rpc-stream': {
164
206
  const streamMode = item.streamMode ?? cfg.stream?.defaultStreamMode ?? 'sse'
165
- installRpcStreamRoute({ app: this._app, procedure: procedure as any, factoryItem: item, cfg, docs, streamMode })
207
+ installRpcStreamRoute({ app: this._app, procedure: procedure as any, factoryItem: item, cfg, docs, streamMode, builderMiddleware: builderMw })
166
208
  break
167
209
  }
168
210
  case 'http':
169
- installHttpRoute({ app: this._app, procedure: procedure as any, factoryItem: item, cfg, docs })
211
+ installHttpRoute({ app: this._app, procedure: procedure as any, factoryItem: item, cfg, docs, builderMiddleware: builderMw })
170
212
  break
171
213
  case 'http-stream':
172
- installHttpStreamRoute({ app: this._app, procedure: procedure as any, factoryItem: item, cfg, docs })
214
+ installHttpStreamRoute({ app: this._app, procedure: procedure as any, factoryItem: item, cfg, docs, builderMiddleware: builderMw })
173
215
  break
174
216
  default: {
175
217
  const reason = `Unknown procedure kind "${(procedure as any).kind}"`
@@ -1,6 +1,7 @@
1
1
  import type { Context } from 'hono'
2
2
  import type { RequestSource } from '../../server/request/params.js'
3
3
  import type { PreStreamDispatchResult } from '../../server/errors/dispatch.js'
4
+ import type { HonoRequestMiddleware } from './types.js'
4
5
 
5
6
  /** Adapts a Hono `Context` to the server layer's {@link RequestSource}. */
6
7
  export function honoRequestSource(c: Context): RequestSource {
@@ -26,3 +27,20 @@ export function respondToDispatch(
26
27
  if (result.type === 'response') return result.response
27
28
  return c.json(result.body, result.statusCode as never)
28
29
  }
30
+
31
+ /**
32
+ * Run a list of middleware (builder + per-item) for the current request.
33
+ * Returns a Response if a middleware short-circuited (or threw — caller should treat throw as error).
34
+ * Otherwise returns undefined to continue to normal procedure handling.
35
+ */
36
+ export async function runMiddlewareList(
37
+ c: Context,
38
+ middleware: HonoRequestMiddleware[] | undefined,
39
+ ): Promise<Response | undefined> {
40
+ if (!middleware || middleware.length === 0) return undefined
41
+ for (const mw of middleware) {
42
+ const maybe = await mw(c, async () => { /* continue to next in list */ })
43
+ if (maybe instanceof Response) return maybe
44
+ }
45
+ return undefined
46
+ }
@@ -40,11 +40,41 @@ export type ExtendProcedureDoc = (params: {
40
40
  procedure: AnyProcedureRegistration<any, any>
41
41
  }) => Record<string, unknown>
42
42
 
43
+ /**
44
+ * Middleware that can run before procedure handlers on a HonoAppBuilder.
45
+ * Similar to Hono middleware but integrated with ts-procedures dispatch.
46
+ *
47
+ * - Throw an error → routes through normal error dispatch (taxonomy, onError, 500).
48
+ * - Return a Response → sent verbatim (early exit, e.g. redirect or custom 401).
49
+ * - Call next() to continue to the procedure (or next middleware).
50
+ *
51
+ * Use for auth, tenancy, rate limiting, request ID injection, etc.
52
+ */
53
+ export type HonoRequestMiddleware = (
54
+ c: Context,
55
+ next: () => Promise<void> | void,
56
+ ) => Response | Promise<Response | void> | void
57
+
58
+ /**
59
+ * Optional per-registration middleware and context overrides.
60
+ * `middleware` runs before the factoryContext for these procedures.
61
+ * `factoryContext` (when provided here) is composed with the register arg via array semantics.
62
+ */
63
+ export type RegisterOptions<TFactory extends ProceduresFactory = ProceduresFactory> = {
64
+ streamMode?: StreamMode
65
+ extendProcedureDoc?: ExtendProcedureDoc
66
+ /** Per-slice middleware (run for routes from this factory only). */
67
+ middleware?: HonoRequestMiddleware[]
68
+ /** Additional context layer composed (left-to-right) with the main factoryContext. */
69
+ context?: FactoryContextInit<ExtractContext<TFactory>, Context>
70
+ }
71
+
43
72
  export type HonoFactoryItem<TFactory extends ProceduresFactory = ProceduresFactory> = {
44
73
  factory: TFactory
45
74
  factoryContext: FactoryContextInit<ExtractContext<TFactory>, Context>
46
75
  streamMode?: StreamMode
47
76
  extendProcedureDoc?: ExtendProcedureDoc
77
+ middleware?: HonoRequestMiddleware[]
48
78
  }
49
79
 
50
80
  export type HonoAppBuilderConfig<TStreamErrorData = unknown> = {
@@ -64,6 +94,13 @@ export type HonoAppBuilderConfig<TStreamErrorData = unknown> = {
64
94
  onRequestStart?: (c: Context) => void
65
95
  onRequestEnd?: (c: Context) => void
66
96
 
97
+ /**
98
+ * Builder-wide middleware run (via internal wrapping) for every procedure
99
+ * registered on this builder. Runs before per-registration middleware and
100
+ * before the factoryContext.
101
+ */
102
+ middleware?: HonoRequestMiddleware[]
103
+
67
104
  // Kind-specific blocks
68
105
  rpc?: {
69
106
  onSuccess?: (procedure: TProcedureRegistration<any, any>, c: Context) => void
@@ -79,7 +79,7 @@ export async function loadConfigFile(configPath?: string): Promise<CodegenConfig
79
79
  } catch (err) {
80
80
  if (configPath !== undefined) {
81
81
  // Explicit path — always throw
82
- throw new Error(`[ts-procedures-codegen] Failed to load config from ${filePath}: ${err instanceof Error ? err.message : err}`, { cause: err })
82
+ throw new Error(`[ts-procedures-codegen] Failed to load config from ${filePath}: ${err instanceof Error ? err.message : err}`)
83
83
  }
84
84
  // Default path — silently ignore if not found
85
85
  return undefined
@@ -1,4 +1,4 @@
1
- import { PACKAGE_VERSION } from '../version.js'
1
+ import pkg from '../../package.json' with { type: 'json' }
2
2
 
3
3
  /**
4
4
  * Stable, version-agnostic token embedded in every generated file (across all
@@ -7,4 +7,4 @@ import { PACKAGE_VERSION } from '../version.js'
7
7
  * the package version (an orphan left by an older release must still match).
8
8
  */
9
9
  export const CODEGEN_SIGNATURE = 'ts-procedures-codegen'
10
- export const CODEGEN_HEADER = `// Auto-generated by ${CODEGEN_SIGNATURE} (v${PACKAGE_VERSION}) — do not edit`
10
+ export const CODEGEN_HEADER = `// Auto-generated by ${CODEGEN_SIGNATURE} (v${pkg.version}) — do not edit`
@@ -62,7 +62,7 @@ export async function emitApiRoute(route: APIHttpRouteDoc, ctx: EmitRouteContext
62
62
 
63
63
  const declarations: string[] = []
64
64
  let paramsTypeName = 'void'
65
- let returnTypeName: string
65
+ let returnTypeName = 'void'
66
66
 
67
67
  // Track reserved names across all sub-namespaces. Model names are reserved
68
68
  // so an ajsc-extracted sub-type can't silently merge with a referenced model.
@@ -132,8 +132,7 @@ export async function emitScopeFile(
132
132
  } catch (err) {
133
133
  const msg = err instanceof Error ? err.message : String(err)
134
134
  throw new Error(
135
- `[ts-procedures-codegen] Failed to emit route "${route.name}" (kind: ${kind}, scope: ${group.scopeKey}): ${msg}`,
136
- { cause: err }
135
+ `[ts-procedures-codegen] Failed to emit route "${route.name}" (kind: ${kind}, scope: ${group.scopeKey}): ${msg}`
137
136
  )
138
137
  }
139
138
 
@@ -91,7 +91,7 @@ describe('generated _errors.ts — runtime behavior', () => {
91
91
  const err = e as { stdout?: Buffer; stderr?: Buffer }
92
92
  const stdout = err.stdout?.toString() ?? ''
93
93
  const stderr = err.stderr?.toString() ?? ''
94
- throw new Error(`tsc failed in ${outDir}:\nSTDOUT:\n${stdout}\nSTDERR:\n${stderr}`, { cause: e })
94
+ throw new Error(`tsc failed in ${outDir}:\nSTDOUT:\n${stdout}\nSTDERR:\n${stderr}`)
95
95
  }
96
96
 
97
97
  // Dynamic import of the compiled output.
@@ -164,7 +164,7 @@ describe('generated _errors.ts — runtime behavior', () => {
164
164
  const err = e as { stdout?: Buffer; stderr?: Buffer }
165
165
  const stdout = err.stdout?.toString() ?? ''
166
166
  const stderr = err.stderr?.toString() ?? ''
167
- throw new Error(`tsc failed in ${outDir}:\nSTDOUT:\n${stdout}\nSTDERR:\n${stderr}`, { cause: e })
167
+ throw new Error(`tsc failed in ${outDir}:\nSTDOUT:\n${stdout}\nSTDERR:\n${stderr}`)
168
168
  }
169
169
 
170
170
  const errorsUrl = `file://${join(outDir, 'out', '_errors.js')}`
@@ -21,7 +21,7 @@ export async function resolveEnvelope(input: ResolveInput): Promise<DocEnvelope>
21
21
  response = await fetch(input.url)
22
22
  } catch (err) {
23
23
  const msg = err instanceof Error ? err.message : String(err)
24
- throw new Error(`[ts-procedures-codegen] Failed to connect to ${input.url}: ${msg}`, { cause: err })
24
+ throw new Error(`[ts-procedures-codegen] Failed to connect to ${input.url}: ${msg}`)
25
25
  }
26
26
  if (!response.ok) {
27
27
  throw new Error(`[ts-procedures-codegen] ${input.url} returned HTTP ${response.status} ${response.statusText}`)
@@ -51,7 +51,6 @@ export function runTsc(args: {
51
51
  const combined = [stdout, stderr].filter(Boolean).join('\n').trim()
52
52
  throw new Error(
53
53
  `[runTsc] tsc failed for ${tsconfigPath}:\n${combined || (e.message ?? '(no output)')}`,
54
- { cause: err },
55
54
  )
56
55
  }
57
56
  }
@@ -7,15 +7,20 @@ import {
7
7
  assertNotDuplicate,
8
8
  combineSignals,
9
9
  iterateWithGuards,
10
+ runWithMiddlewares,
10
11
  shouldSkipValidation,
11
12
  validateReqChannels,
12
13
  } from './internal.js'
13
14
  import type { FactoryRuntime } from './internal.js'
14
- import type { HttpMethod, ProcedureResult, TStreamContext, THttpStreamProcedureRegistration } from './types.js'
15
-
16
- type HttpReq<TReq> = TReq extends Record<string, unknown>
17
- ? Prettify<{ [K in keyof TReq]: Infer<TReq[K]> }>
18
- : undefined
15
+ import type {
16
+ HttpMethod,
17
+ HttpReq,
18
+ ProcedureResult,
19
+ THttpReqInput,
20
+ THttpStreamSchema,
21
+ TStreamContext,
22
+ THttpStreamProcedureRegistration,
23
+ } from './types.js'
19
24
 
20
25
  export function makeCreateHttpStream<TContext>(runtime: FactoryRuntime<TContext, any>) {
21
26
  /**
@@ -29,7 +34,7 @@ export function makeCreateHttpStream<TContext>(runtime: FactoryRuntime<TContext,
29
34
  */
30
35
  return function CreateHttpStream<
31
36
  TName extends string,
32
- TReq extends Record<string, unknown> | undefined,
37
+ TReq extends THttpReqInput | undefined,
33
38
  TYieldType,
34
39
  TReturnType = void,
35
40
  TResHeaders = undefined,
@@ -42,12 +47,7 @@ export function makeCreateHttpStream<TContext>(runtime: FactoryRuntime<TContext,
42
47
  scope?: string
43
48
  errors?: TErrorKey[]
44
49
  description?: string
45
- schema: {
46
- req?: TReq
47
- yield?: TYieldType
48
- returnType?: TReturnType
49
- res?: TResHeaders extends undefined ? undefined : { headers: TResHeaders }
50
- }
50
+ schema: THttpStreamSchema<TReq, TYieldType, TReturnType, TResHeaders>
51
51
  validateYields?: boolean
52
52
  },
53
53
  handler: TResHeaders extends undefined
@@ -116,7 +116,14 @@ export function makeCreateHttpStream<TContext>(runtime: FactoryRuntime<TContext,
116
116
  signal: combineSignals(incomingSignal, abortController.signal),
117
117
  }
118
118
 
119
- const handlerResult: any = (handler as any)({ ...ctx, ...streamCtx }, req)
119
+ const fullCtx = { ...ctx, ...streamCtx }
120
+ // Factory middleware wraps obtaining the stream result (generator or {stream, headers}).
121
+ const handlerResult: any = runWithMiddlewares(
122
+ runtime.middlewares,
123
+ fullCtx,
124
+ req,
125
+ (c, r) => (handler as any)(c, r)
126
+ )
120
127
  const guardOptions = {
121
128
  name,
122
129
  validateYields,
@@ -0,0 +1,154 @@
1
+ import { Type } from 'typebox'
2
+ import { describe, expectTypeOf, test } from 'vitest'
3
+
4
+ import { Procedures } from './procedures.js'
5
+ import type { HttpReturn } from './create-http.js'
6
+ import type {
7
+ HttpReq,
8
+ HttpSchemaInput,
9
+ TCreateHttpConfig,
10
+ THttpReqInput,
11
+ THttpResInput,
12
+ } from './types.js'
13
+ import type { APIInput } from '../server/types.js'
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Type tests for HTTP input shape autocomplete / DX contract
17
+ // These guard the shapes that power IDE autocomplete for schema.req / schema.res.
18
+ // ---------------------------------------------------------------------------
19
+
20
+ describe('THttpReqInput / THttpResInput / APIInput', () => {
21
+ test('APIInput accepts only the four known channels', () => {
22
+ // Valid
23
+ const good: APIInput = {
24
+ pathParams: Type.Object({ id: Type.String() }),
25
+ query: Type.Object({}),
26
+ body: Type.Object({}),
27
+ headers: Type.Object({}),
28
+ }
29
+ expectTypeOf(good).toEqualTypeOf<APIInput>()
30
+
31
+ // Invalid channel should not be assignable
32
+ // @ts-expect-error - unknown channel name
33
+ const _bad: APIInput = { wrong: Type.Object({}) as any }
34
+ })
35
+
36
+ test('THttpResInput shape', () => {
37
+ const resShape: THttpResInput = {
38
+ body: Type.Object({ ok: Type.Boolean() }),
39
+ headers: Type.Object({ 'x-foo': Type.String() }),
40
+ }
41
+ expectTypeOf(resShape).toEqualTypeOf<THttpResInput>()
42
+ })
43
+ })
44
+
45
+ describe('TCreateHttpConfig shapes for literals', () => {
46
+ test('config literal gets the channel keys from the shape (autocomplete contract)', () => {
47
+ const cfg: TCreateHttpConfig = {
48
+ path: '/x',
49
+ method: 'get',
50
+ schema: {
51
+ req: {
52
+ // These keys should be suggested by the IDE
53
+ pathParams: Type.Object({ id: Type.String() }),
54
+ query: undefined,
55
+ },
56
+ res: {
57
+ body: Type.Object({}),
58
+ },
59
+ },
60
+ }
61
+ expectTypeOf(cfg).toEqualTypeOf<TCreateHttpConfig>()
62
+ })
63
+
64
+ test('using satisfies APIInput on req is valid and preserves specific types', () => {
65
+ const req = {
66
+ pathParams: Type.Object({ id: Type.String() }),
67
+ query: Type.Object({ q: Type.Optional(Type.String()) }),
68
+ } satisfies APIInput
69
+
70
+ const cfg: TCreateHttpConfig<typeof req> = {
71
+ path: '/y',
72
+ method: 'get',
73
+ schema: { req },
74
+ }
75
+
76
+ // The assignment succeeded, proving the shape accepted the keys
77
+ // (specific inference of TReq from literal is a bonus)
78
+ })
79
+ })
80
+
81
+ describe('handler inference via HttpReq / HttpReturn', () => {
82
+ const procs = Procedures()
83
+
84
+ test('full req shape produces correctly typed handler param', () => {
85
+ const { Get } = procs.CreateHttp(
86
+ 'Get',
87
+ {
88
+ path: '/u/:id',
89
+ method: 'get',
90
+ schema: {
91
+ req: {
92
+ pathParams: Type.Object({ id: Type.String() }),
93
+ query: Type.Object({ verbose: Type.Optional(Type.Boolean()) }),
94
+ },
95
+ },
96
+ },
97
+ async (_ctx, req) => {
98
+ expectTypeOf(req).toEqualTypeOf<{
99
+ pathParams: { id: string }
100
+ query: { verbose?: boolean }
101
+ }>()
102
+ // no res schema => void
103
+ return undefined
104
+ }
105
+ )
106
+
107
+ type Handler = typeof Get
108
+ expectTypeOf<Parameters<Handler>[1]>().toEqualTypeOf<{
109
+ pathParams: { id: string }
110
+ query: { verbose?: boolean }
111
+ }>()
112
+ })
113
+
114
+ test('res with headers produces { body, headers } return shape', () => {
115
+ const { WithHeaders } = procs.CreateHttp(
116
+ 'WithHeaders',
117
+ {
118
+ path: '/h',
119
+ method: 'get',
120
+ schema: {
121
+ res: {
122
+ body: Type.Object({ data: Type.String() }),
123
+ headers: Type.Object({ 'x-trace': Type.String() }),
124
+ },
125
+ },
126
+ },
127
+ async () => ({ body: { data: 'hi' }, headers: { 'x-trace': 'abc' } })
128
+ )
129
+
130
+ type R = Awaited<ReturnType<typeof WithHeaders>>
131
+ expectTypeOf<R>().toEqualTypeOf<{ body: { data: string }; headers: { 'x-trace': string } }>()
132
+ })
133
+
134
+ test('no schema.res yields void (204-style)', () => {
135
+ const { Logout } = procs.CreateHttp(
136
+ 'Logout',
137
+ { path: '/logout', method: 'post' },
138
+ async () => undefined
139
+ )
140
+
141
+ type R = Awaited<ReturnType<typeof Logout>>
142
+ expectTypeOf<R>().toEqualTypeOf<void>()
143
+ })
144
+ })
145
+
146
+ // Smoke test that HttpReq and HttpSchemaInput are exported and usable
147
+ test('HttpReq and HttpSchemaInput are exported and usable', () => {
148
+ type ExampleReq = HttpSchemaInput<THttpReqInput, { pathParams: { id: string } }>
149
+ expectTypeOf<ExampleReq>().toHaveProperty('pathParams')
150
+
151
+ // HttpReq resolves through Infer; we just check it doesn't error and has shape
152
+ type H = HttpReq<{ query: { q?: string } }>
153
+ expectTypeOf<H>().toHaveProperty('query')
154
+ })