@beignet/core 0.0.2 → 0.0.4

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 (360) hide show
  1. package/CHANGELOG.md +173 -0
  2. package/README.md +821 -30
  3. package/dist/application/index.d.ts +28 -2
  4. package/dist/application/index.d.ts.map +1 -1
  5. package/dist/application/index.js +140 -12
  6. package/dist/application/index.js.map +1 -1
  7. package/dist/client/client.d.ts +2 -2
  8. package/dist/client/client.d.ts.map +1 -1
  9. package/dist/client/client.js +136 -48
  10. package/dist/client/client.js.map +1 -1
  11. package/dist/client/error-messages.d.ts +14 -0
  12. package/dist/client/error-messages.d.ts.map +1 -0
  13. package/dist/client/error-messages.js +23 -0
  14. package/dist/client/error-messages.js.map +1 -0
  15. package/dist/client/index.d.ts +8 -4
  16. package/dist/client/index.d.ts.map +1 -1
  17. package/dist/client/index.js +6 -2
  18. package/dist/client/index.js.map +1 -1
  19. package/dist/client/types.d.ts +35 -5
  20. package/dist/client/types.d.ts.map +1 -1
  21. package/dist/client-only.d.ts +8 -0
  22. package/dist/client-only.d.ts.map +1 -0
  23. package/dist/client-only.js +8 -0
  24. package/dist/client-only.js.map +1 -0
  25. package/dist/config/index.d.ts +5 -5
  26. package/dist/config/index.d.ts.map +1 -1
  27. package/dist/config/index.js +2 -2
  28. package/dist/config/index.js.map +1 -1
  29. package/dist/contracts/catalog-errors.d.ts +27 -0
  30. package/dist/contracts/catalog-errors.d.ts.map +1 -0
  31. package/dist/contracts/catalog-errors.js +69 -0
  32. package/dist/contracts/catalog-errors.js.map +1 -0
  33. package/dist/contracts/contract-builder.d.ts +15 -12
  34. package/dist/contracts/contract-builder.d.ts.map +1 -1
  35. package/dist/contracts/contract-builder.js +15 -41
  36. package/dist/contracts/contract-builder.js.map +1 -1
  37. package/dist/contracts/contract-group.d.ts +11 -8
  38. package/dist/contracts/contract-group.d.ts.map +1 -1
  39. package/dist/contracts/contract-group.js +13 -40
  40. package/dist/contracts/contract-group.js.map +1 -1
  41. package/dist/contracts/contract-like.d.ts +1 -1
  42. package/dist/contracts/contract-like.d.ts.map +1 -1
  43. package/dist/contracts/index.d.ts +13 -9
  44. package/dist/contracts/index.d.ts.map +1 -1
  45. package/dist/contracts/index.js +9 -5
  46. package/dist/contracts/index.js.map +1 -1
  47. package/dist/contracts/openapi-meta.d.ts +48 -0
  48. package/dist/contracts/openapi-meta.d.ts.map +1 -1
  49. package/dist/contracts/openapi-meta.js +3 -0
  50. package/dist/contracts/openapi-meta.js.map +1 -1
  51. package/dist/contracts/path-template.d.ts +1 -1
  52. package/dist/contracts/path-template.js +2 -2
  53. package/dist/contracts/path-template.js.map +1 -1
  54. package/dist/contracts/schema-shape.d.ts +37 -0
  55. package/dist/contracts/schema-shape.d.ts.map +1 -0
  56. package/dist/contracts/schema-shape.js +61 -0
  57. package/dist/contracts/schema-shape.js.map +1 -0
  58. package/dist/contracts/success-status.d.ts +32 -0
  59. package/dist/contracts/success-status.d.ts.map +1 -0
  60. package/dist/contracts/success-status.js +18 -0
  61. package/dist/contracts/success-status.js.map +1 -0
  62. package/dist/contracts/types.d.ts +25 -5
  63. package/dist/contracts/types.d.ts.map +1 -1
  64. package/dist/contracts/types.js.map +1 -1
  65. package/dist/contracts/utils.d.ts +1 -1
  66. package/dist/contracts/utils.d.ts.map +1 -1
  67. package/dist/contracts/utils.js +1 -1
  68. package/dist/contracts/utils.js.map +1 -1
  69. package/dist/domain/events.d.ts +1 -1
  70. package/dist/domain/events.d.ts.map +1 -1
  71. package/dist/domain/events.js +1 -1
  72. package/dist/domain/events.js.map +1 -1
  73. package/dist/domain/index.d.ts +3 -3
  74. package/dist/domain/index.d.ts.map +1 -1
  75. package/dist/domain/index.js +3 -3
  76. package/dist/domain/index.js.map +1 -1
  77. package/dist/errors/catalog.d.ts +9 -1
  78. package/dist/errors/catalog.d.ts.map +1 -1
  79. package/dist/errors/catalog.js +7 -1
  80. package/dist/errors/catalog.js.map +1 -1
  81. package/dist/errors/http.d.ts +10 -0
  82. package/dist/errors/http.d.ts.map +1 -1
  83. package/dist/errors/http.js +11 -1
  84. package/dist/errors/http.js.map +1 -1
  85. package/dist/errors/index.d.ts +4 -4
  86. package/dist/errors/index.d.ts.map +1 -1
  87. package/dist/errors/index.js +4 -4
  88. package/dist/errors/index.js.map +1 -1
  89. package/dist/errors/response.d.ts +4 -1
  90. package/dist/errors/response.d.ts.map +1 -1
  91. package/dist/errors/response.js.map +1 -1
  92. package/dist/events/index.d.ts +10 -12
  93. package/dist/events/index.d.ts.map +1 -1
  94. package/dist/events/index.js +10 -10
  95. package/dist/events/index.js.map +1 -1
  96. package/dist/idempotency/index.d.ts +5 -3
  97. package/dist/idempotency/index.d.ts.map +1 -1
  98. package/dist/idempotency/index.js.map +1 -1
  99. package/dist/jobs/index.d.ts +148 -16
  100. package/dist/jobs/index.d.ts.map +1 -1
  101. package/dist/jobs/index.js +174 -14
  102. package/dist/jobs/index.js.map +1 -1
  103. package/dist/notifications/index.d.ts +14 -16
  104. package/dist/notifications/index.d.ts.map +1 -1
  105. package/dist/notifications/index.js +14 -14
  106. package/dist/notifications/index.js.map +1 -1
  107. package/dist/openapi/index.d.ts +8 -3
  108. package/dist/openapi/index.d.ts.map +1 -1
  109. package/dist/openapi/index.js +41 -29
  110. package/dist/openapi/index.js.map +1 -1
  111. package/dist/openapi/schema-introspector.d.ts +37 -0
  112. package/dist/openapi/schema-introspector.d.ts.map +1 -1
  113. package/dist/openapi/schema-introspector.js +23 -17
  114. package/dist/openapi/schema-introspector.js.map +1 -1
  115. package/dist/outbox/index.d.ts +18 -4
  116. package/dist/outbox/index.d.ts.map +1 -1
  117. package/dist/outbox/index.js +104 -4
  118. package/dist/outbox/index.js.map +1 -1
  119. package/dist/ports/audit.d.ts +56 -10
  120. package/dist/ports/audit.d.ts.map +1 -1
  121. package/dist/ports/audit.js +71 -3
  122. package/dist/ports/audit.js.map +1 -1
  123. package/dist/ports/auth.d.ts +92 -0
  124. package/dist/ports/auth.d.ts.map +1 -1
  125. package/dist/ports/auth.js +92 -0
  126. package/dist/ports/auth.js.map +1 -1
  127. package/dist/ports/events.d.ts +2 -2
  128. package/dist/ports/events.d.ts.map +1 -1
  129. package/dist/ports/index.d.ts +62 -33
  130. package/dist/ports/index.d.ts.map +1 -1
  131. package/dist/ports/index.js +28 -34
  132. package/dist/ports/index.js.map +1 -1
  133. package/dist/ports/policy.d.ts +32 -3
  134. package/dist/ports/policy.d.ts.map +1 -1
  135. package/dist/ports/policy.js +13 -2
  136. package/dist/ports/policy.js.map +1 -1
  137. package/dist/ports/testing.d.ts +1030 -2
  138. package/dist/ports/testing.d.ts.map +1 -1
  139. package/dist/ports/testing.js +1031 -1
  140. package/dist/ports/testing.js.map +1 -1
  141. package/dist/ports/unbound.d.ts +21 -0
  142. package/dist/ports/unbound.d.ts.map +1 -0
  143. package/dist/ports/unbound.js +57 -0
  144. package/dist/ports/unbound.js.map +1 -0
  145. package/dist/ports/unit-of-work.d.ts +1 -1
  146. package/dist/ports/unit-of-work.d.ts.map +1 -1
  147. package/dist/ports/unit-of-work.js +1 -1
  148. package/dist/ports/unit-of-work.js.map +1 -1
  149. package/dist/providers/index.d.ts +3 -2
  150. package/dist/providers/index.d.ts.map +1 -1
  151. package/dist/providers/index.js +3 -2
  152. package/dist/providers/index.js.map +1 -1
  153. package/dist/providers/instrumentation.d.ts +46 -5
  154. package/dist/providers/instrumentation.d.ts.map +1 -1
  155. package/dist/providers/instrumentation.js +25 -6
  156. package/dist/providers/instrumentation.js.map +1 -1
  157. package/dist/providers/metadata.d.ts +39 -0
  158. package/dist/providers/metadata.d.ts.map +1 -0
  159. package/dist/providers/metadata.js +169 -0
  160. package/dist/providers/metadata.js.map +1 -0
  161. package/dist/providers/provider.d.ts +114 -9
  162. package/dist/providers/provider.d.ts.map +1 -1
  163. package/dist/providers/provider.js +3 -20
  164. package/dist/providers/provider.js.map +1 -1
  165. package/dist/schedules/index.d.ts +94 -13
  166. package/dist/schedules/index.d.ts.map +1 -1
  167. package/dist/schedules/index.js +66 -12
  168. package/dist/schedules/index.js.map +1 -1
  169. package/dist/server/audit-context.d.ts +29 -0
  170. package/dist/server/audit-context.d.ts.map +1 -0
  171. package/dist/server/audit-context.js +44 -0
  172. package/dist/server/audit-context.js.map +1 -0
  173. package/dist/server/context.d.ts +141 -0
  174. package/dist/server/context.d.ts.map +1 -0
  175. package/dist/server/context.js +39 -0
  176. package/dist/server/context.js.map +1 -0
  177. package/dist/server/contract-like.d.ts +1 -1
  178. package/dist/server/contract-like.d.ts.map +1 -1
  179. package/dist/server/contract-like.js +1 -1
  180. package/dist/server/contract-like.js.map +1 -1
  181. package/dist/server/health.d.ts +2 -2
  182. package/dist/server/health.d.ts.map +1 -1
  183. package/dist/server/hooks/auth.d.ts +89 -65
  184. package/dist/server/hooks/auth.d.ts.map +1 -1
  185. package/dist/server/hooks/auth.js +84 -55
  186. package/dist/server/hooks/auth.js.map +1 -1
  187. package/dist/server/hooks/cors.d.ts +1 -1
  188. package/dist/server/hooks/cors.d.ts.map +1 -1
  189. package/dist/server/hooks/errors.d.ts +2 -2
  190. package/dist/server/hooks/errors.d.ts.map +1 -1
  191. package/dist/server/hooks/errors.js +2 -2
  192. package/dist/server/hooks/errors.js.map +1 -1
  193. package/dist/server/hooks/idempotency.d.ts +78 -0
  194. package/dist/server/hooks/idempotency.d.ts.map +1 -0
  195. package/dist/server/hooks/idempotency.js +154 -0
  196. package/dist/server/hooks/idempotency.js.map +1 -0
  197. package/dist/server/hooks/index.d.ts +8 -7
  198. package/dist/server/hooks/index.d.ts.map +1 -1
  199. package/dist/server/hooks/index.js +6 -5
  200. package/dist/server/hooks/index.js.map +1 -1
  201. package/dist/server/hooks/logging.d.ts +2 -2
  202. package/dist/server/hooks/logging.d.ts.map +1 -1
  203. package/dist/server/hooks/logging.js +1 -1
  204. package/dist/server/hooks/logging.js.map +1 -1
  205. package/dist/server/hooks/rate-limit.d.ts +25 -7
  206. package/dist/server/hooks/rate-limit.d.ts.map +1 -1
  207. package/dist/server/hooks/rate-limit.js +47 -12
  208. package/dist/server/hooks/rate-limit.js.map +1 -1
  209. package/dist/server/hooks.d.ts +1 -1
  210. package/dist/server/hooks.d.ts.map +1 -1
  211. package/dist/server/hooks.js +1 -1
  212. package/dist/server/hooks.js.map +1 -1
  213. package/dist/server/http.d.ts +84 -6
  214. package/dist/server/http.d.ts.map +1 -1
  215. package/dist/server/index.d.ts +36 -12
  216. package/dist/server/index.d.ts.map +1 -1
  217. package/dist/server/index.js +24 -8
  218. package/dist/server/index.js.map +1 -1
  219. package/dist/server/instrumentation.d.ts +108 -0
  220. package/dist/server/instrumentation.d.ts.map +1 -0
  221. package/dist/server/instrumentation.js +297 -0
  222. package/dist/server/instrumentation.js.map +1 -0
  223. package/dist/server/openapi.d.ts +3 -3
  224. package/dist/server/openapi.d.ts.map +1 -1
  225. package/dist/server/openapi.js +1 -1
  226. package/dist/server/openapi.js.map +1 -1
  227. package/dist/server/providers/index.d.ts +3 -3
  228. package/dist/server/providers/index.d.ts.map +1 -1
  229. package/dist/server/providers/index.js +3 -3
  230. package/dist/server/providers/index.js.map +1 -1
  231. package/dist/server/providers/loadProviderConfig.d.ts +2 -2
  232. package/dist/server/providers/loadProviderConfig.d.ts.map +1 -1
  233. package/dist/server/providers/loadProviderConfig.js +2 -2
  234. package/dist/server/providers/loadProviderConfig.js.map +1 -1
  235. package/dist/server/request-context.d.ts +67 -0
  236. package/dist/server/request-context.d.ts.map +1 -0
  237. package/dist/server/request-context.js +79 -0
  238. package/dist/server/request-context.js.map +1 -0
  239. package/dist/server/server-context.d.ts +38 -0
  240. package/dist/server/server-context.d.ts.map +1 -0
  241. package/dist/server/server-context.js +38 -0
  242. package/dist/server/server-context.js.map +1 -0
  243. package/dist/server/server.d.ts +148 -35
  244. package/dist/server/server.d.ts.map +1 -1
  245. package/dist/server/server.js +482 -145
  246. package/dist/server/server.js.map +1 -1
  247. package/dist/server/types.d.ts +2 -2
  248. package/dist/server/types.d.ts.map +1 -1
  249. package/dist/server/types.js +2 -2
  250. package/dist/server/types.js.map +1 -1
  251. package/dist/server/use-case-route.d.ts +263 -0
  252. package/dist/server/use-case-route.d.ts.map +1 -0
  253. package/dist/server/use-case-route.js +77 -0
  254. package/dist/server/use-case-route.js.map +1 -0
  255. package/dist/server-only.d.ts +8 -0
  256. package/dist/server-only.d.ts.map +1 -0
  257. package/dist/server-only.js +8 -0
  258. package/dist/server-only.js.map +1 -0
  259. package/dist/tasks/index.d.ts +139 -0
  260. package/dist/tasks/index.d.ts.map +1 -0
  261. package/dist/tasks/index.js +98 -0
  262. package/dist/tasks/index.js.map +1 -0
  263. package/dist/testing/index.d.ts +611 -5
  264. package/dist/testing/index.d.ts.map +1 -1
  265. package/dist/testing/index.js +434 -4
  266. package/dist/testing/index.js.map +1 -1
  267. package/dist/tracing/index.d.ts +89 -0
  268. package/dist/tracing/index.d.ts.map +1 -0
  269. package/dist/tracing/index.js +101 -0
  270. package/dist/tracing/index.js.map +1 -0
  271. package/dist/uploads/client.d.ts +278 -0
  272. package/dist/uploads/client.d.ts.map +1 -0
  273. package/dist/uploads/client.js +428 -0
  274. package/dist/uploads/client.js.map +1 -0
  275. package/dist/uploads/index.d.ts +361 -0
  276. package/dist/uploads/index.d.ts.map +1 -0
  277. package/dist/uploads/index.js +543 -0
  278. package/dist/uploads/index.js.map +1 -0
  279. package/package.json +34 -3
  280. package/src/application/index.ts +193 -10
  281. package/src/client/client.ts +148 -150
  282. package/src/client/error-messages.ts +35 -0
  283. package/src/client/index.ts +12 -4
  284. package/src/client/types.ts +44 -5
  285. package/src/client-only.ts +7 -0
  286. package/src/config/index.ts +6 -6
  287. package/src/contracts/catalog-errors.ts +115 -0
  288. package/src/contracts/contract-builder.ts +39 -76
  289. package/src/contracts/contract-group.ts +33 -68
  290. package/src/contracts/contract-like.ts +1 -1
  291. package/src/contracts/index.ts +24 -11
  292. package/src/contracts/openapi-meta.ts +55 -0
  293. package/src/contracts/path-template.ts +2 -2
  294. package/src/contracts/schema-shape.ts +75 -0
  295. package/src/contracts/success-status.ts +68 -0
  296. package/src/contracts/types.ts +32 -5
  297. package/src/contracts/utils.ts +5 -2
  298. package/src/domain/events.ts +6 -2
  299. package/src/domain/index.ts +3 -3
  300. package/src/errors/catalog.ts +9 -1
  301. package/src/errors/http.ts +11 -1
  302. package/src/errors/index.ts +4 -4
  303. package/src/errors/response.ts +4 -1
  304. package/src/events/index.ts +12 -26
  305. package/src/idempotency/index.ts +5 -3
  306. package/src/jobs/index.ts +340 -29
  307. package/src/notifications/index.ts +17 -27
  308. package/src/openapi/index.ts +73 -38
  309. package/src/openapi/schema-introspector.ts +68 -17
  310. package/src/outbox/index.ts +151 -6
  311. package/src/ports/audit.ts +120 -11
  312. package/src/ports/auth.ts +132 -0
  313. package/src/ports/events.ts +2 -2
  314. package/src/ports/index.ts +104 -35
  315. package/src/ports/policy.ts +50 -3
  316. package/src/ports/testing.ts +2220 -33
  317. package/src/ports/unbound.ts +64 -0
  318. package/src/ports/unit-of-work.ts +6 -2
  319. package/src/providers/index.ts +16 -3
  320. package/src/providers/instrumentation.ts +93 -8
  321. package/src/providers/metadata.ts +234 -0
  322. package/src/providers/provider.ts +168 -9
  323. package/src/schedules/index.ts +173 -23
  324. package/src/server/audit-context.ts +45 -0
  325. package/src/server/context.ts +224 -0
  326. package/src/server/contract-like.ts +1 -1
  327. package/src/server/health.ts +2 -2
  328. package/src/server/hooks/auth.ts +175 -158
  329. package/src/server/hooks/cors.ts +1 -1
  330. package/src/server/hooks/errors.ts +7 -4
  331. package/src/server/hooks/idempotency.ts +263 -0
  332. package/src/server/hooks/index.ts +15 -12
  333. package/src/server/hooks/logging.ts +3 -3
  334. package/src/server/hooks/rate-limit.ts +85 -17
  335. package/src/server/hooks.ts +1 -1
  336. package/src/server/http.ts +112 -6
  337. package/src/server/index.ts +63 -12
  338. package/src/server/instrumentation.ts +470 -0
  339. package/src/server/openapi.ts +4 -4
  340. package/src/server/providers/index.ts +6 -3
  341. package/src/server/providers/loadProviderConfig.ts +4 -4
  342. package/src/server/request-context.ts +116 -0
  343. package/src/server/server-context.ts +44 -0
  344. package/src/server/server.ts +1045 -229
  345. package/src/server/types.ts +2 -2
  346. package/src/server/use-case-route.ts +430 -0
  347. package/src/server-only.ts +7 -0
  348. package/src/tasks/index.ts +275 -0
  349. package/src/testing/index.ts +1153 -6
  350. package/src/tracing/index.ts +176 -0
  351. package/src/uploads/client.ts +861 -0
  352. package/src/uploads/index.ts +1071 -0
  353. package/dist/ports/mailer.d.ts +0 -6
  354. package/dist/ports/mailer.d.ts.map +0 -1
  355. package/dist/ports/mailer.js +0 -2
  356. package/dist/ports/mailer.js.map +0 -1
  357. package/dist/ports/schedules.d.ts +0 -9
  358. package/dist/ports/schedules.d.ts.map +0 -1
  359. package/dist/ports/schedules.js +0 -2
  360. package/dist/ports/schedules.js.map +0 -1
package/README.md CHANGED
@@ -2,9 +2,13 @@
2
2
 
3
3
  > Core framework primitives for Beignet
4
4
 
5
+ > [!CAUTION]
6
+ > Beignet is experimental alpha software. The `0.0.x` package line is for early
7
+ > evaluation, and APIs may change between releases while the framework settles.
8
+
5
9
  This package provides Beignet's framework primitives: contracts, server runtime,
6
10
  typed client, use cases, ports, domain helpers, app errors, config, events,
7
- idempotency, outbox, mail, notifications, schedules, pagination helpers,
11
+ idempotency, outbox, mail, notifications, schedules, uploads, pagination helpers,
8
12
  testing helpers, and OpenAPI generation.
9
13
 
10
14
  ## Installation
@@ -35,13 +39,14 @@ name the framework area they depend on.
35
39
  | --- | --- |
36
40
  | `@beignet/core/application` | Use case builder and test helpers |
37
41
  | `@beignet/core/client` | Typed HTTP client |
42
+ | `@beignet/core/client-only` | Static lint marker for modules intended for client-side imports |
38
43
  | `@beignet/core/config` | Environment config validation |
39
44
  | `@beignet/core/contracts` | HTTP contract builders, types, path helpers, and contract metadata |
40
45
  | `@beignet/core/domain` | Entities, value objects, and domain events |
41
46
  | `@beignet/core/errors` | Error catalogs and response helpers |
42
47
  | `@beignet/core/events` | Events and listeners |
43
48
  | `@beignet/core/idempotency` | Retry-safe command, webhook, and job primitives |
44
- | `@beignet/core/jobs` | Job definitions and inline job dispatch |
49
+ | `@beignet/core/jobs` | Job definitions, retry policies, and inline job dispatch |
45
50
  | `@beignet/core/mail` | Mail port and memory mailer |
46
51
  | `@beignet/core/notifications` | Notification definitions, dispatchers, mail channels, and test adapters |
47
52
  | `@beignet/core/openapi` | OpenAPI generation |
@@ -50,9 +55,179 @@ name the framework area they depend on.
50
55
  | `@beignet/core/ports` | App-facing ports, auth, audit, policies, cache, storage, logging, and redaction |
51
56
  | `@beignet/core/ports/testing` | Port and policy test helpers |
52
57
  | `@beignet/core/providers` | Provider lifecycle and instrumentation primitives |
53
- | `@beignet/core/schedules` | Scheduled task primitives |
58
+ | `@beignet/core/schedules` | Schedule primitives |
54
59
  | `@beignet/core/server` | Framework-agnostic server runtime and hook helpers |
55
- | `@beignet/core/testing` | Test factories, seed definitions, and seed runners |
60
+ | `@beignet/core/server-only` | Static lint marker for modules that must stay out of client bundles |
61
+ | `@beignet/core/tasks` | Operational task definitions and inline task execution |
62
+ | `@beignet/core/testing` | Test context factories, memory port fixtures, provider install helper, factories, seeds, and database harnesses |
63
+ | `@beignet/core/tracing` | Dependency-free W3C trace context primitives |
64
+ | `@beignet/core/uploads` | Upload definitions, router, signer port, and test signer |
65
+ | `@beignet/core/uploads/client` | Browser upload client for server and direct uploads |
66
+
67
+ ## Durable failure language
68
+
69
+ Jobs, outbox delivery, and schedule runners use the same terms:
70
+
71
+ - `attempt` is the one-based execution or delivery attempt currently being
72
+ handled.
73
+ - `attempts` in a retry policy is the maximum total attempts, including the
74
+ first try.
75
+ - `backoff` is the delay before the next retry.
76
+ - `terminal failure` means the work should not be retried automatically.
77
+ - `dead letter` is a durable terminal delivery state, currently owned by the
78
+ outbox.
79
+
80
+ Schedules do not own retry policies. They can carry provider attempt metadata
81
+ through `ScheduleRunContext.attempt`, then dispatch jobs or outbox messages when
82
+ the work needs Beignet-managed retry and dead-letter behavior.
83
+
84
+ Outbox drains emit first-class provider instrumentation for delivered, retried,
85
+ and dead-lettered messages when you pass a devtools or instrumentation port to
86
+ `drainOutbox(...)`. Pass `instrumentationContext` when the worker has request or
87
+ trace IDs that should connect the drain to devtools rows.
88
+
89
+ ## Provider-contributed ports
90
+
91
+ Apps bind app-owned ports directly and defer the rest to providers with the
92
+ curried `definePorts<AppPorts>()({ bound, deferred })` form. Deferred keys boot
93
+ as throwing placeholders, and `createServer(...)` fails startup with the
94
+ unbound key list unless `onUnboundPorts` is set to `"warn"` or `"ignore"`.
95
+
96
+ ```typescript
97
+ import { definePorts } from "@beignet/core/ports";
98
+ import type { AppPorts } from "@/ports";
99
+
100
+ export const appPorts = definePorts<AppPorts>()({
101
+ bound: { gate },
102
+ deferred: ["db", "logger", "mailer", "storage"],
103
+ });
104
+ ```
105
+
106
+ Use `InferProviderPorts` with an `as const` provider list to type the runtime
107
+ ports without casts:
108
+
109
+ ```typescript
110
+ import type { InferProviderPorts } from "@beignet/core/providers";
111
+ import type { AppPorts } from "@/ports";
112
+ import type { providers } from "@/server/providers";
113
+
114
+ export type AppRuntimePorts = AppPorts & InferProviderPorts<typeof providers>;
115
+ ```
116
+
117
+ App-local providers can declare required ports, app context, and
118
+ service-context input through the curried `createProvider()` form. `setup`
119
+ then receives typed `ports` and a `createServiceContext` factory that returns
120
+ the app context:
121
+
122
+ ```typescript
123
+ import { createProvider } from "@beignet/core/providers";
124
+
125
+ export const appDatabaseProvider = createProvider<
126
+ { db: DbPort<typeof schema>; devtools?: DevtoolsPort },
127
+ AppContext,
128
+ AppServiceContextInput
129
+ >()({
130
+ name: "app-database",
131
+ async setup({ ports, createServiceContext }) {
132
+ const repositories = createRepositories(ports.db.db);
133
+ return { ports: repositories };
134
+ },
135
+ });
136
+ ```
137
+
138
+ Lifecycle hooks returned from `setup` should close over setup locals; a
139
+ `start(ctx)` hook with an unannotated parameter keeps TypeScript from inferring
140
+ the provided ports from the returned `ports` object.
141
+
142
+ ## Provider metadata
143
+
144
+ Reusable provider packages should declare static metadata in `package.json`
145
+ under `beignet.provider`. That manifest metadata is package-owned and
146
+ side-effect-free, so CLI diagnostics can inspect installed provider packages
147
+ without importing provider implementation code.
148
+
149
+ ```json
150
+ {
151
+ "beignet": {
152
+ "provider": {
153
+ "displayName": "Cache provider",
154
+ "ports": ["cache"],
155
+ "appPorts": [{ "name": "cache", "type": "CachePort" }],
156
+ "env": ["CACHE_URL", "CACHE_REGION"],
157
+ "requiredEnv": ["CACHE_URL"],
158
+ "registration": {
159
+ "required": true,
160
+ "tokens": ["cacheProvider", "createCacheProvider"]
161
+ },
162
+ "watchers": ["cache"]
163
+ }
164
+ }
165
+ }
166
+ ```
167
+
168
+ `env` lists all variables the provider may read. `requiredEnv` is the subset
169
+ that `beignet doctor --strict` should require in app config.
170
+
171
+ `registration.required: true` marks providers that apps must register in
172
+ `server/providers.ts`; doctor reports a missing registration as a warning,
173
+ which fails `beignet doctor --strict`. Optional-by-design providers such as
174
+ `@beignet/devtools` can declare `registration.severity: "hint"` instead, so an
175
+ installed-but-unregistered package is reported as an informational hint that
176
+ never fails doctor, even in strict mode. Use
177
+ `parseProviderPackageMetadata(...)` to validate manifest metadata before
178
+ publishing a provider package.
179
+
180
+ Provider objects can also declare optional runtime-inert metadata for app-local
181
+ tooling and documentation. It does not change runtime setup; it describes the
182
+ package, contributed ports, required prior ports, env vars, and devtools
183
+ watchers owned by the provider.
184
+
185
+ ```typescript
186
+ import { createProvider } from "@beignet/core/providers";
187
+
188
+ export const cacheProvider = createProvider({
189
+ name: "cache",
190
+ metadata: {
191
+ packageName: "@acme/beignet-provider-cache",
192
+ ports: ["cache"],
193
+ env: ["CACHE_URL"],
194
+ watchers: ["cache"],
195
+ },
196
+ setup() {
197
+ return { ports: { cache: createCachePort() } };
198
+ },
199
+ });
200
+ ```
201
+
202
+ ## Tasks
203
+
204
+ Use `@beignet/core/tasks` for app-owned operational entrypoints such as
205
+ backfills, maintenance work, and one-off repair scripts. Tasks are not HTTP
206
+ routes and are not background jobs; they are explicit functions a CLI or worker
207
+ can run with parsed input and an application context. Run them with
208
+ `runTask(...)` or `beignet task run`, and collect them with `defineTasks(...)`.
209
+
210
+ ```typescript
211
+ import { createTasks } from "@beignet/core/tasks";
212
+ import { z } from "zod";
213
+ import type { AppContext } from "@/app-context";
214
+
215
+ const { defineTask } = createTasks<AppContext>();
216
+
217
+ export const backfillSearchTask = defineTask("posts.backfill-search", {
218
+ input: z.object({
219
+ dryRun: z.boolean().default(true),
220
+ }),
221
+ async handle({ input, ctx }) {
222
+ ctx.ports.logger.info("Backfill started", {
223
+ dryRun: input.dryRun,
224
+ });
225
+ },
226
+ });
227
+ ```
228
+
229
+ Feature-owned task files should usually call use cases, repositories, or
230
+ ports rather than hiding business rules inside a script.
56
231
 
57
232
  ## Key concepts
58
233
 
@@ -67,7 +242,7 @@ A **contract** is the single source of truth for an API endpoint. It describes:
67
242
 
68
243
  ### Contract group
69
244
 
70
- A **contract group** allows you to share configuration across related endpoints, such as a common namespace, authentication requirements, and shared response schemas.
245
+ A **contract group** allows you to share configuration across related endpoints, such as a common namespace, route metadata, headers, and shared response schemas.
71
246
 
72
247
  ## Usage
73
248
 
@@ -75,10 +250,10 @@ A **contract group** allows you to share configuration across related endpoints,
75
250
 
76
251
  ```ts
77
252
  import { z } from "zod";
78
- import { createContractGroup } from "@beignet/core/contracts";
253
+ import { defineContractGroup } from "@beignet/core/contracts";
79
254
 
80
255
  // Create a contract group for related endpoints
81
- const todos = createContractGroup()
256
+ const todos = defineContractGroup()
82
257
  .namespace("todos")
83
258
  .prefix("/api/todos")
84
259
  .meta({ auth: "required" })
@@ -130,6 +305,22 @@ Clients and OpenAPI generation infer required path argument keys from literal
130
305
  path templates. Use `.pathParams(...)` when you want runtime validation,
131
306
  coercion, richer OpenAPI schemas, or parameter descriptions.
132
307
 
308
+ `createServer(...)` enforces registration-time guarantees: each method + path
309
+ may only be registered once, contract names must be unique across the route
310
+ registry because typed clients, OpenAPI operations, and devtools key on them,
311
+ and an introspectable `.pathParams(...)` object schema must declare exactly the
312
+ `:param` keys from the path template. Mismatches fail server startup with the
313
+ contract name and path. At dispatch time, a request that matches a registered
314
+ path with an unregistered method receives a framework-owned `405
315
+ METHOD_NOT_ALLOWED` response with an `Allow` header listing the registered
316
+ methods; `HEAD` is intentionally not served by `GET` handlers.
317
+
318
+ Contract path templates intentionally support concrete segments and
319
+ single-segment params such as `:id` and `[id]`. Framework or platform
320
+ catch-all route files can expose a central Beignet handler, but individual
321
+ contracts should stay on explicit paths; catch-all contract patterns such as
322
+ `/files/[...path]` are rejected.
323
+
133
324
  Use `.headers(...)` for request headers that are part of the endpoint contract. Declare header keys in lowercase; server and client runtime matching is case-insensitive.
134
325
 
135
326
  Request bodies are supported for `POST`, `PUT`, and `PATCH` contracts only.
@@ -137,10 +328,10 @@ Request bodies are supported for `POST`, `PUT`, and `PATCH` contracts only.
137
328
  If you do not pass `name`, Beignet generates one from the HTTP method and full path:
138
329
 
139
330
  ```ts
140
- createContract({ method: "GET", path: "/users/:id" }).name;
331
+ defineContract({ method: "GET", path: "/users/:id" }).name;
141
332
  // "getUsersById"
142
333
 
143
- createContract({ method: "POST", path: "/api/todos" }).name;
334
+ defineContract({ method: "POST", path: "/api/todos" }).name;
144
335
  // "createTodos"
145
336
  ```
146
337
 
@@ -151,7 +342,7 @@ Auto-generated names ignore a leading `/api` segment, include path parameters as
151
342
  Use `.prefix(...)` on a contract group to compose shared URL path segments without repeating them on every route:
152
343
 
153
344
  ```ts
154
- const api = createContractGroup().prefix("/api/v1");
345
+ const api = defineContractGroup().prefix("/api/v1");
155
346
 
156
347
  const todos = api
157
348
  .namespace("todos")
@@ -168,16 +359,198 @@ Prefixes compose immutably and normalize boundary slashes. `namespace()` control
168
359
  resource identity for contract names, OpenAPI tags, and client cache grouping;
169
360
  `prefix()` only controls URL paths.
170
361
 
171
- ### Test factories and seeds
362
+ ### Test app fixtures
363
+
364
+ Use `@beignet/core/testing` to build app contexts and common memory ports
365
+ without hand-rolling audit, event, job, mail, notification, outbox, storage,
366
+ idempotency, logger, clock, and UOW setup in every test:
367
+
368
+ ```ts
369
+ import { createUseCaseTester } from "@beignet/core/application";
370
+ import { createTestContextFactory, createTestPorts } from "@beignet/core/testing";
371
+ import {
372
+ createTestTenant,
373
+ createTestUserActor,
374
+ } from "@beignet/core/ports/testing";
375
+
376
+ const fixture = createTestPorts<AppContext["ports"]>({
377
+ base: appPorts,
378
+ overrides: {
379
+ gate: appPorts.gate,
380
+ posts: { findById: async (id) => postRecord(id) },
381
+ },
382
+ });
383
+ const createContext = createTestContextFactory<AppContext, AppContext["ports"]>({
384
+ ports: fixture.ports,
385
+ actor: createTestUserActor("user_test"),
386
+ auth: { user: { id: "user_test" } },
387
+ tenant: createTestTenant("tenant_example"),
388
+ });
389
+ const tester = createUseCaseTester<AppContext>(createContext);
390
+ ```
391
+
392
+ The returned fixture exposes captured side effects such as `events`,
393
+ `dispatchedJobs`, `audit.entries`, `mailer.deliveries`,
394
+ `notifications.deliveries`, `outbox.messages`, and memory storage for
395
+ assertions.
396
+
397
+ `overrides` is typed as `TestPortsOverrides<Ports>`, which accepts typed
398
+ partial ports without casts. The partial rule is one level deep: an
399
+ object-valued port may supply only the members the test needs, and any missing
400
+ member becomes a named throwing function (`Test port "posts.update" was called
401
+ but not provided.`). Function-valued ports, class instances, and other exotic
402
+ objects are supplied whole — nested config objects are not partial.
403
+
404
+ The default `audit` port is wrapped with `createAmbientAuditLog(...)`, so
405
+ entries recorded inside an active request context inherit actor, tenant,
406
+ request ID, and trace ID exactly like production. `fixture.audit` still
407
+ exposes the underlying memory port for `entries` assertions.
408
+
409
+ #### One-call test contexts
410
+
411
+ Use `createTestContext(...)` when a job, listener, schedule, notification, or
412
+ task test needs a full app context instead of a repeated factory:
413
+
414
+ ```ts
415
+ import { createTestContext } from "@beignet/core/testing";
416
+
417
+ const makeContext = createTestContext<AppContext>();
418
+
419
+ it("audits handled jobs", async () => {
420
+ using fixture = makeContext({
421
+ ports: { issues: { findById: async (id) => issueRecord(id) } },
422
+ });
423
+
424
+ await IndexIssueJob.handle({ job: IndexIssueJob, payload, ctx: fixture.ctx });
425
+
426
+ expect(fixture.audit.entries).toMatchObject([
427
+ { action: "jobs.issues.index", requestId: "test-request" },
428
+ ]);
429
+ });
430
+ ```
431
+
432
+ The fixture assembles `ctx` with actor (default
433
+ `createTestSystemActor("test-system")`), tenant, request ID, trace ID, `auth`,
434
+ ports, and a live bound `ctx.gate`. It also enters the ambient request context
435
+ so ambient enrichment (such as the default audit port) behaves like the
436
+ server; `using` (or an explicit `dispose()`) clears it:
437
+
438
+ ```ts
439
+ let fixture: ReturnType<ReturnType<typeof createTestContext<AppContext>>>;
440
+
441
+ afterEach(() => {
442
+ fixture.dispose();
443
+ });
444
+ ```
172
445
 
173
- Use `@beignet/core/testing` to keep feature tests and demo seed data
174
- port-based. Factories build app-owned records, and optional `persist` functions
175
- write through the context you pass in:
446
+ Pass `ambient: false` to skip ambient entry. Reading an app port that is
447
+ neither a kit default nor supplied throws a named error
448
+ (`App port "tweets" is not bound in this test context.`), so partial port
449
+ wiring fails on use instead of failing silently.
450
+
451
+ #### Transactional domain events
452
+
453
+ When a use case records domain events through a buffered recorder on the
454
+ transaction ports, pass `transaction.outbox: true` to flush `tx.events` to
455
+ `ports.eventBus` after commit and clear it after rollback:
176
456
 
177
457
  ```ts
178
- import { defineFactory, defineSeed, runSeeds } from "@beignet/core/testing";
458
+ import { createDomainEventRecorder } from "@beignet/core/ports";
459
+
460
+ const fixture = createTestPorts<AppContext["ports"], AppTransactionPorts>({
461
+ transaction: {
462
+ ports: (ports) => ({ ...ports, events: createDomainEventRecorder() }),
463
+ outbox: true,
464
+ },
465
+ });
466
+ ```
467
+
468
+ `transaction.outbox` requires `transaction.ports` to include an `events`
469
+ recorder such as `createDomainEventRecorder()` or
470
+ `createOutboxEventRecorder(...)`; the kit throws a named error otherwise.
471
+
472
+ #### Sharing the server context blueprint
473
+
474
+ Declare the `context` blueprint once with `defineServerContext(...)` from
475
+ `@beignet/core/server` and keep it in a canonical `server/context.ts` file.
476
+ The same value round-trips through `createServer(...)` adapters and
477
+ `createTestApp(...)` with full inference:
478
+
479
+ ```ts
480
+ // server/context.ts
481
+ import { defineServerContext } from "@beignet/core/server";
482
+
483
+ export const appContext = defineServerContext<AppContext, AppPorts>()({
484
+ gate: (ports) => ports.gate,
485
+ request: async ({ req, ports, requestId, trace }) => ({
486
+ actor: await resolveActor(req),
487
+ auth: null,
488
+ requestId,
489
+ ...trace,
490
+ ports,
491
+ }),
492
+ service: ({ ports, requestId, trace }) => ({
493
+ actor: createServiceActor("app-service"),
494
+ auth: null,
495
+ requestId,
496
+ ...trace,
497
+ ports,
498
+ }),
499
+ });
500
+ ```
501
+
502
+ ```ts
503
+ // server/index.ts
504
+ const server = await createNextServer({ ports, routes, context: appContext });
505
+
506
+ // features/<feature>/tests/routes.test.ts
507
+ const app = await createTestApp({ ports, routes, context: appContext });
508
+ ```
509
+
510
+ ### Testing providers
179
511
 
180
- const postFactory = defineFactory("post", {
512
+ Use `installProviderForTest(...)` to run provider setup against test ports
513
+ without hand-rolling setup, port merge, and lifecycle plumbing:
514
+
515
+ ```ts
516
+ import { installProviderForTest } from "@beignet/core/testing";
517
+
518
+ const { ports, result, start, stop } = await installProviderForTest(
519
+ redisProvider,
520
+ {
521
+ ports: { devtools },
522
+ config: { URL: "redis://localhost:6379" },
523
+ },
524
+ );
525
+
526
+ const cache = ports.cache as CachePort;
527
+ await cache.set("posts:list", "[]");
528
+
529
+ await stop();
530
+ ```
531
+
532
+ `ports` contains the base ports merged with provider-contributed ports, and
533
+ `result` exposes the raw setup result for lifecycle-hook assertions. `config`
534
+ is passed to setup as-is, matching server startup where config is validated
535
+ before setup runs. Pass `createServiceContext` when the provider builds
536
+ service contexts from runtime entrypoints.
537
+
538
+ ### Test factories and seeds
539
+
540
+ Use the same subpath to keep feature tests and demo seed data port-based.
541
+ Factories build app-owned records, and optional `persist` functions write
542
+ through the context you pass in:
543
+
544
+ ```ts
545
+ import {
546
+ createDatabaseTestHarness,
547
+ createFactory,
548
+ defineSeed,
549
+ resetFactories,
550
+ runSeeds,
551
+ } from "@beignet/core/testing";
552
+
553
+ const postFactory = createFactory("post", {
181
554
  defaults: ({ sequence }) => ({
182
555
  title: `Post ${sequence}`,
183
556
  content: "Created in a test.",
@@ -194,11 +567,179 @@ const demoPostsSeed = defineSeed("demo-posts", {
194
567
  export async function seedDemoPosts(ctx: AppContext) {
195
568
  await runSeeds({ ctx, seeds: [demoPostsSeed] });
196
569
  }
570
+
571
+ export function resetPostFactories() {
572
+ resetFactories(postFactory);
573
+ }
574
+ ```
575
+
576
+ For repository and persistence tests, compose the app-owned database fixture
577
+ with the same factories and seeds:
578
+
579
+ ```ts
580
+ const databaseHarness = createDatabaseTestHarness({
581
+ create: createTestDatabase,
582
+ ctx: (database) => ({ ports: database.ports }),
583
+ reset: (database) => database.reset(),
584
+ close: (database) => database.close(),
585
+ factories: [postFactory],
586
+ seeds: [demoPostsSeed],
587
+ });
588
+
589
+ afterEach(async () => {
590
+ await databaseHarness.cleanup();
591
+ });
592
+
593
+ const { ctx } = await databaseHarness.setup({ seed: true });
594
+ const post = await postFactory.create(ctx, { title: "Database conventions" });
197
595
  ```
198
596
 
199
597
  Keep factories and seeds app-owned. They should not import database clients,
200
598
  ORM table objects, or provider SDKs directly.
201
599
 
600
+ ### Port testing helpers
601
+
602
+ Use `@beignet/core/ports/testing` when tests need stable actor, tenant,
603
+ authorization, or audit assertions:
604
+
605
+ ```ts
606
+ import {
607
+ assertAuditEntry,
608
+ createPolicyTester,
609
+ createTestActivityContext,
610
+ createTestTenant,
611
+ createTestUserActor,
612
+ } from "@beignet/core/ports/testing";
613
+
614
+ const activity = createTestActivityContext({
615
+ actor: createTestUserActor("user_1", { role: "admin" }),
616
+ tenant: createTestTenant("tenant_1"),
617
+ });
618
+
619
+ const tester = createPolicyTester({ policies: [postPolicy] });
620
+ await tester.assertMatrix([
621
+ {
622
+ name: "admin can publish",
623
+ ctx: activity,
624
+ ability: "posts.publish",
625
+ subject: post,
626
+ expected: "allow",
627
+ },
628
+ ]);
629
+
630
+ assertAuditEntry(audit.entries, {
631
+ action: "posts.publish",
632
+ actorId: "user_1",
633
+ tenantId: "tenant_1",
634
+ resourceType: "post",
635
+ resourceId: post.id,
636
+ });
637
+ ```
638
+
639
+ `createTestImpersonatedUserActor(...)` is available for tests where an admin or
640
+ support actor is acting as another user and audit metadata should record the
641
+ impersonator ID.
642
+
643
+ The same subpath includes assertion helpers for common provider-backed test
644
+ adapters:
645
+
646
+ ```ts
647
+ import {
648
+ assertDispatchedJob,
649
+ assertIdempotencyCompleted,
650
+ assertMailDelivery,
651
+ assertNotificationDelivery,
652
+ assertOutboxDelivered,
653
+ assertOutboxDrainResult,
654
+ assertOutboxPending,
655
+ assertProviderInstrumentationEvent,
656
+ assertRecordedEvent,
657
+ assertStorageObject,
658
+ createRecordingEventBus,
659
+ createRecordingJobDispatcher,
660
+ createRecordingProviderInstrumentation,
661
+ } from "@beignet/core/ports/testing";
662
+ import { drainOutbox } from "@beignet/core/outbox";
663
+ import { createProviderInstrumentation } from "@beignet/core/providers";
664
+
665
+ const { bus, events } = createRecordingEventBus();
666
+ const { jobs, dispatchedJobs } = createRecordingJobDispatcher();
667
+
668
+ await bus.publish(PostPublished, { postId: post.id });
669
+ await jobs.dispatch(LogPostPublishedJob, { postId: post.id });
670
+
671
+ assertRecordedEvent(events, {
672
+ name: "posts.published",
673
+ payload: { postId: post.id },
674
+ });
675
+
676
+ assertDispatchedJob(dispatchedJobs, {
677
+ name: "posts.log-published",
678
+ payload: { postId: post.id },
679
+ });
680
+
681
+ assertNotificationDelivery(notifications.deliveries, {
682
+ notificationName: "posts.published",
683
+ channels: ["email"],
684
+ });
685
+
686
+ assertMailDelivery(mailer.deliveries, {
687
+ subject: "Post published",
688
+ });
689
+
690
+ await assertStorageObject(storage, {
691
+ key: "posts/post_1/attachment.txt",
692
+ text: "hello",
693
+ });
694
+
695
+ assertIdempotencyCompleted(fixture.idempotency, {
696
+ namespace: "posts.create",
697
+ key: "idem_1",
698
+ result: { id: post.id },
699
+ });
700
+
701
+ assertOutboxPending(outbox, {
702
+ kind: "event",
703
+ name: "posts.published",
704
+ payload: { postId: post.id },
705
+ });
706
+
707
+ const result = await drainOutbox({ outbox, registry, eventBus, jobs });
708
+
709
+ assertOutboxDrainResult(result, {
710
+ claimed: 1,
711
+ delivered: 1,
712
+ });
713
+
714
+ assertOutboxDelivered(outbox.messages, {
715
+ kind: "event",
716
+ name: "posts.published",
717
+ });
718
+
719
+ const { instrumentation, events: providerEvents } =
720
+ createRecordingProviderInstrumentation();
721
+ const providerInstrumentation = createProviderInstrumentation(instrumentation, {
722
+ providerName: "redis",
723
+ watcher: "providers",
724
+ });
725
+
726
+ providerInstrumentation.custom({
727
+ name: "cache.get",
728
+ details: { key: "posts:list", hit: true },
729
+ });
730
+
731
+ assertProviderInstrumentationEvent(providerEvents, {
732
+ type: "custom",
733
+ name: "cache.get",
734
+ providerName: "redis",
735
+ details: { hit: true },
736
+ });
737
+ ```
738
+
739
+ `createProviderInstrumentation(...)` adds `details.providerName` to custom and
740
+ typed provider events, so tests and devtools can group provider work
741
+ consistently.
742
+
202
743
  ### Pagination
203
744
 
204
745
  Use `@beignet/core/pagination` to keep list use cases and repository ports
@@ -222,10 +763,65 @@ return ctx.ports.posts.findMany({
222
763
  Beignet's convention is `items` for list contents and `page` for pagination
223
764
  metadata. Keep filters and sort options app-owned plain objects.
224
765
 
766
+ ### Rate limiting
767
+
768
+ Use `createRateLimitHooks(...)` from `@beignet/core/server` to enforce
769
+ `contract.metadata.rateLimit` at the HTTP boundary:
770
+
771
+ ```ts
772
+ import { createRateLimitHooks } from "@beignet/core/server";
773
+
774
+ const server = await createServer<AppContext, AppPorts>({
775
+ ports: appPorts,
776
+ hooks: [createRateLimitHooks<AppContext>()],
777
+ // ...
778
+ });
779
+ ```
780
+
781
+ - `global` and `ip` scopes run in `onRequest` before parsing and context
782
+ creation; `user` scope runs in `beforeHandle` once `ctx.actor` exists.
783
+ - `ip` buckets default to the last `x-forwarded-for` entry, the address
784
+ appended by the platform's trusted proxy. Use the `ipSource` option to
785
+ switch to `"x-forwarded-for-first"` behind a header-normalizing edge, or
786
+ pass a function for platform headers such as `cf-connecting-ip`.
787
+ - Denials throw the framework `429 Too Many Requests` catalog error with
788
+ `scope`, `retryAfterSeconds`, and `resetAt` details. The bucket key is
789
+ never included in the client-visible response.
790
+ - Each denial emits a `rateLimit.denied` instrumentation event carrying the
791
+ key, scope, limit, and window when the app ports include an
792
+ `instrumentation` or `devtools` sink, so operators keep bucket visibility.
793
+
225
794
  ### Idempotency
226
795
 
227
- Use `@beignet/core/idempotency` when a command, webhook, or job may be retried
228
- and must not perform duplicate work:
796
+ Use `createIdempotencyHooks(...)` from `@beignet/core/server` to enforce
797
+ `contract.metadata.idempotency` at the HTTP boundary, mirroring
798
+ `createRateLimitHooks(...)`:
799
+
800
+ ```ts
801
+ import { createIdempotencyHooks } from "@beignet/core/server";
802
+
803
+ const server = await createServer<AppContext, AppPorts>({
804
+ ports: appPorts,
805
+ hooks: [createIdempotencyHooks<AppContext>()],
806
+ // ...
807
+ });
808
+ ```
809
+
810
+ The hook reads the key from the metadata header (default `idempotency-key`),
811
+ reserves it through `ctx.ports.idempotency` after request parsing, replays
812
+ completed matching responses with an `idempotency-replayed: true` header, and
813
+ maps in-progress and conflicting keys to framework-owned `409` responses using
814
+ the `httpErrors.IdempotencyInProgress` and `httpErrors.IdempotencyConflict`
815
+ catalog entries.
816
+
817
+ Typed clients read the same metadata: `createClient(...)` endpoints attach a
818
+ generated UUID to the metadata header on every call (injected before request
819
+ header validation, so header schemas pass), and the header becomes optional in
820
+ call types. Pass `idempotencyKey` as a call option for retry-with-same-key
821
+ flows; an explicit `headers` value always wins over generation.
822
+
823
+ Use `runIdempotently(...)` from `@beignet/core/idempotency` when a non-HTTP
824
+ command, webhook, or job may be retried and must not perform duplicate work:
229
825
 
230
826
  ```ts
231
827
  import {
@@ -234,17 +830,17 @@ import {
234
830
  } from "@beignet/core/idempotency";
235
831
 
236
832
  const result = await runIdempotently(ctx.ports.idempotency, {
237
- namespace: "todos.create",
238
- key: input.idempotencyKey,
833
+ namespace: "todos.import",
834
+ key: input.importId,
239
835
  scope: {
240
836
  tenantId: ctx.tenant?.id,
241
837
  actorId: ctx.actor?.id,
242
838
  },
243
839
  fingerprint: await createIdempotencyFingerprint(input, {
244
- omit: ["idempotencyKey"],
840
+ omit: ["importId"],
245
841
  }),
246
842
  ttlSec: 60 * 60 * 24,
247
- run: () => ctx.ports.uow.transaction((tx) => tx.todos.create(input)),
843
+ run: () => ctx.ports.uow.transaction((tx) => tx.todos.importBatch(input)),
248
844
  });
249
845
  ```
250
846
 
@@ -257,9 +853,11 @@ const idempotency = createMemoryIdempotencyStore();
257
853
  ```
258
854
 
259
855
  Production apps should back `IdempotencyPort` with atomic SQL or Redis storage.
260
- For high-integrity workflows, prefer exposing a transaction-scoped
261
- `tx.idempotency` port from the app Unit of Work so reservation, business writes,
262
- audit records, domain-event records, and idempotency completion commit together.
856
+ The Drizzle/libSQL path can use `createDrizzleTursoIdempotencyPort(...)` from
857
+ `@beignet/provider-drizzle-turso`. For high-integrity workflows, prefer exposing
858
+ a transaction-scoped `tx.idempotency` port from the app Unit of Work so
859
+ reservation, business writes, audit records, domain-event records, and
860
+ idempotency completion commit together.
263
861
 
264
862
  ### Outbox
265
863
 
@@ -306,9 +904,37 @@ await drainOutbox({
306
904
  The outbox is at-least-once delivery. Use idempotent listeners or jobs when a
307
905
  duplicate delivery would be harmful.
308
906
 
309
- ### Contract metadata
907
+ ### Schedules
310
908
 
311
- Use metadata to drive cross-cutting concerns like authentication, rate limiting, and idempotency:
909
+ Use `@beignet/core/schedules` to define typed schedules and run them
910
+ inline from cron routes, workers, scripts, and tests. Pass a
911
+ devtools-compatible sink as `instrumentation` and the inline runner records
912
+ `schedule` devtools events (`started`, `completed`, `failed`) for each run:
913
+
914
+ ```ts
915
+ import { createInlineScheduleRunner } from "@beignet/core/schedules";
916
+
917
+ const runner = createInlineScheduleRunner<AppContext>({
918
+ ctx,
919
+ instrumentation: ctx.ports.devtools,
920
+ instrumentationContext: {
921
+ requestId: ctx.requestId,
922
+ traceId: ctx.traceId,
923
+ },
924
+ });
925
+
926
+ await runner.run(SendDailyDigestSchedule, { source: "vercel-cron" });
927
+ ```
928
+
929
+ `instrumentationContext` attaches request correlation fields to recorded
930
+ events. Recording failures are isolated from schedule execution and reported
931
+ to `onHookError` when provided. Handler failures still reject `runner.run(...)`
932
+ after `onError` runs so trigger hosts can retry.
933
+
934
+ ### Contract metadata and route hooks
935
+
936
+ Use metadata to describe cross-cutting concerns for OpenAPI, clients, docs, and
937
+ app conventions:
312
938
 
313
939
  ```ts
314
940
  const sendMessage = messages
@@ -331,6 +957,159 @@ const sendMessage = messages
331
957
  });
332
958
  ```
333
959
 
960
+ The built-in server hooks enforce `rateLimit` and `idempotency` metadata:
961
+ install `createRateLimitHooks(...)` and `createIdempotencyHooks(...)` where the
962
+ server is composed. Use route hooks for runtime enforcement of route-specific
963
+ policy where the route is wired:
964
+
965
+ ```ts
966
+ import { createAuthHooks, defineRouteGroup } from "@beignet/core/server";
967
+ import type { AppContext } from "@/app-context";
968
+
969
+ const auth = createAuthHooks<AppContext>()({
970
+ resolve: ({ ctx }) => {
971
+ return ctx.auth ? { user: ctx.auth.user } : null;
972
+ },
973
+ });
974
+
975
+ export const messageRoutes = defineRouteGroup<AppContext>()({
976
+ name: "messages",
977
+ routes: [
978
+ {
979
+ contract: sendMessage,
980
+ hooks: [auth.required()],
981
+ useCase: sendMessageUseCase,
982
+ },
983
+ ],
984
+ });
985
+ ```
986
+
987
+ Ordinary app routes bind `{ contract, useCase }`. The response status is
988
+ inferred when the contract declares exactly one `2xx` response (otherwise
989
+ `status` is required and typed to the declared keys), and the use case input
990
+ defaults to the merged request parts via `defaultBinderInput` — query lowest,
991
+ then body, then path; headers are never merged and need an explicit
992
+ `input: (parts) => ...` mapper. When the use case `.input(...)` schema is the
993
+ contract's sole request schema by reference, the server skips the use case's
994
+ input re-parse — one schema, one parse.
995
+
996
+ Use `{ contract, handle }` as the escape hatch for response headers,
997
+ streaming, and multi-status responses. `defineRoute<AppContext>()` remains
998
+ available for full handlers that read hook-added `ctx` fields.
999
+
1000
+ When credentials live in request headers, declare a `headers` schema on the
1001
+ auth hooks. The hook validates the raw lowercase request header record before
1002
+ `resolve` runs, so `resolve` receives typed header values; on `required()`
1003
+ routes a schema failure returns a framework-owned `401`:
1004
+
1005
+ ```ts
1006
+ const writerAuth = createAuthHooks<AppContext>()({
1007
+ name: "writer",
1008
+ headers: writerHeadersSchema,
1009
+ resolve: ({ headers }) => ({
1010
+ actor: createUserActor(headers["x-user-id"]),
1011
+ }),
1012
+ });
1013
+ ```
1014
+
1015
+ ### HTTP adapter boundary
1016
+
1017
+ `@beignet/core/server` is framework-neutral. It owns route matching, hooks,
1018
+ request validation, response validation, error mapping, and provider lifecycle.
1019
+ Adapters own the platform edge only:
1020
+
1021
+ - Convert the native request into `HttpRequestLike`
1022
+ - Call `server.api(...)` or a single route handler
1023
+ - Convert `HttpResponse` back into the native response type
1024
+
1025
+ The public adapter contract is `HttpAdapter<NativeRequest, NativeResponse>`.
1026
+ Use it when building a runtime package beyond the first-party `@beignet/web`
1027
+ and `@beignet/next` adapters.
1028
+
1029
+ ### Request instrumentation and tracing
1030
+
1031
+ `createServer(...)` owns request instrumentation. For every request it
1032
+ resolves a request ID (from `x-request-id`, or generated) and a W3C trace
1033
+ context (from `traceparent`, or generated) before user hooks and context
1034
+ creation, passes them to context factories as `requestId` and `trace`, writes
1035
+ both response headers, and records `request`/`error` events into the provider
1036
+ instrumentation port resolved from final ports (`ports.instrumentation`, then
1037
+ `ports.devtools`). Without an installed sink, headers are still written and
1038
+ events are a no-op.
1039
+
1040
+ ```ts
1041
+ const server = await createServer({
1042
+ ports,
1043
+ providers,
1044
+ // Defaults shown. Pass `instrumentation: false` to disable entirely.
1045
+ instrumentation: {
1046
+ requestIdHeader: "x-request-id",
1047
+ traceContextHeader: "traceparent",
1048
+ ignorePaths: ["/api/devtools"],
1049
+ },
1050
+ context: {
1051
+ request: ({ ports, requestId, trace }) => ({
1052
+ requestId,
1053
+ ...trace,
1054
+ ports,
1055
+ }),
1056
+ },
1057
+ });
1058
+ ```
1059
+
1060
+ Service contexts created with `server.createServiceContext(...)` receive fresh
1061
+ `requestId` and `trace` values per call. Context values win: when a factory
1062
+ sets its own `requestId`, headers and recorded events use it.
1063
+
1064
+ Trace primitives live in `@beignet/core/tracing` (`TraceContext`,
1065
+ `createTraceContext`, `createChildTraceContext`, `parseTraceparent`,
1066
+ `createTraceparent`, `createTraceId`, `createSpanId`). The module is
1067
+ dependency-free so app context types can be imported from client bundles.
1068
+
1069
+ Use cases created with `createUseCase(...)` are instrumented by default. Each
1070
+ run resolves the instrumentation port from `ctx.ports`, creates a child span
1071
+ from the context's trace fields, and records `usecase` lifecycle events plus
1072
+ correlated `error` events for failures. Pass `instrumentation: false` to opt
1073
+ out.
1074
+
1075
+ `createInstrumentedAuditLog({ audit, instrumentation })` from
1076
+ `@beignet/core/ports` writes durable audit entries first and mirrors sanitized
1077
+ audit activity into the resolved instrumentation sink.
1078
+
1079
+ `createAmbientAuditLog(audit)` from `@beignet/core/server` fills missing
1080
+ `actor`, `tenant`, `requestId`, and `traceId` fields from the ambient request
1081
+ context at record time. The server keeps that context current for requests
1082
+ (including identity elevated by route hooks) and for service contexts created
1083
+ with `server.createServiceContext(...)`, so jobs, listeners, schedules, and
1084
+ tasks are covered. Because enrichment happens at record time, the wrapper also
1085
+ works for audit ports rebuilt per transaction inside a unit of work — wrap
1086
+ both the top-level port and the per-transaction rebuild:
1087
+
1088
+ ```ts
1089
+ import { createAmbientAuditLog } from "@beignet/core/server";
1090
+
1091
+ const audit = createAmbientAuditLog(
1092
+ createInstrumentedAuditLog({ audit: durableAudit, instrumentation: ports }),
1093
+ );
1094
+
1095
+ await audit.record({
1096
+ action: "posts.publish",
1097
+ resource: { type: "post", id: post.id },
1098
+ });
1099
+ ```
1100
+
1101
+ Entry-provided fields always win; on runtimes without `AsyncLocalStorage`
1102
+ the wrapper passes entries through unchanged, and entries without an actor
1103
+ normalize to an anonymous actor.
1104
+
1105
+ Route-owned response validation can be disabled with
1106
+ `validateResponses: false` on `createServer(...)`, mirroring the client option
1107
+ of the same name. Binder routes whose use case `.output(...)` schema is the
1108
+ declared success response schema by reference already skip the redundant
1109
+ success-status parse; if profiling justifies disabling validation entirely,
1110
+ drive `validateResponses` from an environment flag so development and CI keep
1111
+ it on.
1112
+
334
1113
  ### OpenAPI metadata
335
1114
 
336
1115
  Add OpenAPI-specific metadata for documentation using the `.openapi()` method:
@@ -354,6 +1133,12 @@ export const getTodo = todos
354
1133
  });
355
1134
  ```
356
1135
 
1136
+ Use `requestBody`, `responses`, and `parameters` overrides when an operation
1137
+ needs non-JSON media such as multipart uploads, binary downloads, event streams,
1138
+ or cookie parameters. `contractsToOpenAPI(...)` accepts `schemaConverters` for
1139
+ non-Zod Standard Schema libraries; custom converters run before Beignet's
1140
+ default Zod converter.
1141
+
357
1142
  ### Schema introspection
358
1143
 
359
1144
  Contracts expose their schemas for runtime introspection:
@@ -371,12 +1156,12 @@ getTodo.metadata; // { auth: "required", ... }
371
1156
 
372
1157
  ## API reference
373
1158
 
374
- ### `createContractGroup()`
1159
+ ### `defineContractGroup()`
375
1160
 
376
1161
  Creates a new contract group for defining related endpoints.
377
1162
 
378
1163
  ```ts
379
- const group = createContractGroup()
1164
+ const group = defineContractGroup()
380
1165
  .namespace("myNamespace") // Optional resource namespace
381
1166
  .prefix("/api/v1") // Optional URL path prefix
382
1167
  .meta({ auth: "required" }) // Shared metadata
@@ -386,6 +1171,10 @@ const group = createContractGroup()
386
1171
  });
387
1172
  ```
388
1173
 
1174
+ Shared catalog errors merge with route-level `.errors(...)` declarations, so
1175
+ each contract carries the union of group and route errors. Later declarations
1176
+ win when the same catalog key is declared twice.
1177
+
389
1178
  Any non-empty response map is treated as a response contract. Include
390
1179
  successful statuses such as `200` or `201` alongside custom error statuses; use
391
1180
  `responses: {}` only when you want to skip response validation. Prefer
@@ -406,7 +1195,7 @@ standard error envelope.
406
1195
  | `.headers(schema)` | Define request header schema |
407
1196
  | `.body(schema)` | Define request body schema |
408
1197
  | `.responses({ ... })` | Define or merge response schemas by status code |
409
- | `.errors({ ... })` | Declare route-owned catalog errors using Beignet's standard error envelope |
1198
+ | `.errors({ ... })` | Declare route-owned catalog errors using Beignet's standard error envelope; merges with group and earlier declarations |
410
1199
  | `.meta(metadata)` | Add custom metadata |
411
1200
  | `.openapi(options)` | Add OpenAPI metadata (summary, tags, etc.) |
412
1201
 
@@ -422,9 +1211,11 @@ OpenAPI generation currently requires Zod schemas, even though core contracts ca
422
1211
 
423
1212
  ## Related packages
424
1213
 
1214
+ - [`@beignet/web`](https://beignet.dev/server) - Web Fetch server adapter
425
1215
  - [`@beignet/next`](https://beignet.dev/server) - Next.js server adapter
426
1216
  - [`@beignet/react-query`](https://beignet.dev/react-query) - TanStack Query integration
427
1217
  - [`@beignet/react-hook-form`](https://beignet.dev/react-hook-form) - React Hook Form integration
1218
+ - [`@beignet/react-uploads`](https://beignet.dev/react-uploads) - React upload state and progress hooks
428
1219
  - [`@beignet/nuqs`](https://beignet.dev/nuqs) - URL query state integration with nuqs
429
1220
  - [`@beignet/devtools`](https://beignet.dev/devtools) - Local request, provider, and audit timeline
430
1221