@beignet/core 0.0.3 → 0.0.5

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 +159 -0
  2. package/README.md +792 -50
  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 +12 -14
  100. package/dist/jobs/index.d.ts.map +1 -1
  101. package/dist/jobs/index.js +13 -13
  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 +15 -6
  116. package/dist/outbox/index.d.ts.map +1 -1
  117. package/dist/outbox/index.js +60 -16
  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 +45 -4
  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 +49 -10
  184. package/dist/server/hooks/auth.d.ts.map +1 -1
  185. package/dist/server/hooks/auth.js +77 -37
  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 +61 -35
  214. package/dist/server/http.d.ts.map +1 -1
  215. package/dist/server/http.js +1 -20
  216. package/dist/server/http.js.map +1 -1
  217. package/dist/server/index.d.ts +36 -12
  218. package/dist/server/index.d.ts.map +1 -1
  219. package/dist/server/index.js +24 -8
  220. package/dist/server/index.js.map +1 -1
  221. package/dist/server/instrumentation.d.ts +108 -0
  222. package/dist/server/instrumentation.d.ts.map +1 -0
  223. package/dist/server/instrumentation.js +297 -0
  224. package/dist/server/instrumentation.js.map +1 -0
  225. package/dist/server/openapi.d.ts +3 -3
  226. package/dist/server/openapi.d.ts.map +1 -1
  227. package/dist/server/openapi.js +1 -1
  228. package/dist/server/openapi.js.map +1 -1
  229. package/dist/server/providers/index.d.ts +3 -3
  230. package/dist/server/providers/index.d.ts.map +1 -1
  231. package/dist/server/providers/index.js +3 -3
  232. package/dist/server/providers/index.js.map +1 -1
  233. package/dist/server/providers/loadProviderConfig.d.ts +2 -2
  234. package/dist/server/providers/loadProviderConfig.d.ts.map +1 -1
  235. package/dist/server/providers/loadProviderConfig.js +2 -2
  236. package/dist/server/providers/loadProviderConfig.js.map +1 -1
  237. package/dist/server/request-context.d.ts +67 -0
  238. package/dist/server/request-context.d.ts.map +1 -0
  239. package/dist/server/request-context.js +79 -0
  240. package/dist/server/request-context.js.map +1 -0
  241. package/dist/server/server-context.d.ts +38 -0
  242. package/dist/server/server-context.d.ts.map +1 -0
  243. package/dist/server/server-context.js +38 -0
  244. package/dist/server/server-context.js.map +1 -0
  245. package/dist/server/server.d.ts +105 -33
  246. package/dist/server/server.d.ts.map +1 -1
  247. package/dist/server/server.js +434 -118
  248. package/dist/server/server.js.map +1 -1
  249. package/dist/server/types.d.ts +2 -2
  250. package/dist/server/types.d.ts.map +1 -1
  251. package/dist/server/types.js +2 -2
  252. package/dist/server/types.js.map +1 -1
  253. package/dist/server/use-case-route.d.ts +263 -0
  254. package/dist/server/use-case-route.d.ts.map +1 -0
  255. package/dist/server/use-case-route.js +77 -0
  256. package/dist/server/use-case-route.js.map +1 -0
  257. package/dist/server-only.d.ts +8 -0
  258. package/dist/server-only.d.ts.map +1 -0
  259. package/dist/server-only.js +8 -0
  260. package/dist/server-only.js.map +1 -0
  261. package/dist/tasks/index.d.ts +139 -0
  262. package/dist/tasks/index.d.ts.map +1 -0
  263. package/dist/tasks/index.js +98 -0
  264. package/dist/tasks/index.js.map +1 -0
  265. package/dist/testing/index.d.ts +607 -5
  266. package/dist/testing/index.d.ts.map +1 -1
  267. package/dist/testing/index.js +426 -4
  268. package/dist/testing/index.js.map +1 -1
  269. package/dist/tracing/index.d.ts +89 -0
  270. package/dist/tracing/index.d.ts.map +1 -0
  271. package/dist/tracing/index.js +101 -0
  272. package/dist/tracing/index.js.map +1 -0
  273. package/dist/uploads/client.d.ts +1 -1
  274. package/dist/uploads/client.d.ts.map +1 -1
  275. package/dist/uploads/index.d.ts +2 -2
  276. package/dist/uploads/index.d.ts.map +1 -1
  277. package/dist/uploads/index.js +1 -1
  278. package/dist/uploads/index.js.map +1 -1
  279. package/package.json +24 -2
  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 +14 -24
  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 +84 -19
  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 +86 -7
  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 +141 -51
  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 +14 -7
  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 +78 -51
  337. package/src/server/index.ts +62 -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 +886 -238
  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 +1142 -6
  350. package/src/tracing/index.ts +176 -0
  351. package/src/uploads/client.ts +1 -1
  352. package/src/uploads/index.ts +7 -3
  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,6 +2,10 @@
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
11
  idempotency, outbox, mail, notifications, schedules, uploads, pagination helpers,
@@ -35,6 +39,7 @@ 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 |
@@ -50,12 +55,180 @@ 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 |
56
64
  | `@beignet/core/uploads` | Upload definitions, router, signer port, and test signer |
57
65
  | `@beignet/core/uploads/client` | Browser upload client for server and direct uploads |
58
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.
231
+
59
232
  ## Key concepts
60
233
 
61
234
  ### Contract
@@ -77,10 +250,10 @@ A **contract group** allows you to share configuration across related endpoints,
77
250
 
78
251
  ```ts
79
252
  import { z } from "zod";
80
- import { createContractGroup } from "@beignet/core/contracts";
253
+ import { defineContractGroup } from "@beignet/core/contracts";
81
254
 
82
255
  // Create a contract group for related endpoints
83
- const todos = createContractGroup()
256
+ const todos = defineContractGroup()
84
257
  .namespace("todos")
85
258
  .prefix("/api/todos")
86
259
  .meta({ auth: "required" })
@@ -132,6 +305,22 @@ Clients and OpenAPI generation infer required path argument keys from literal
132
305
  path templates. Use `.pathParams(...)` when you want runtime validation,
133
306
  coercion, richer OpenAPI schemas, or parameter descriptions.
134
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
+
135
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.
136
325
 
137
326
  Request bodies are supported for `POST`, `PUT`, and `PATCH` contracts only.
@@ -139,10 +328,10 @@ Request bodies are supported for `POST`, `PUT`, and `PATCH` contracts only.
139
328
  If you do not pass `name`, Beignet generates one from the HTTP method and full path:
140
329
 
141
330
  ```ts
142
- createContract({ method: "GET", path: "/users/:id" }).name;
331
+ defineContract({ method: "GET", path: "/users/:id" }).name;
143
332
  // "getUsersById"
144
333
 
145
- createContract({ method: "POST", path: "/api/todos" }).name;
334
+ defineContract({ method: "POST", path: "/api/todos" }).name;
146
335
  // "createTodos"
147
336
  ```
148
337
 
@@ -153,7 +342,7 @@ Auto-generated names ignore a leading `/api` segment, include path parameters as
153
342
  Use `.prefix(...)` on a contract group to compose shared URL path segments without repeating them on every route:
154
343
 
155
344
  ```ts
156
- const api = createContractGroup().prefix("/api/v1");
345
+ const api = defineContractGroup().prefix("/api/v1");
157
346
 
158
347
  const todos = api
159
348
  .namespace("todos")
@@ -170,21 +359,198 @@ Prefixes compose immutably and normalize boundary slashes. `namespace()` control
170
359
  resource identity for contract names, OpenAPI tags, and client cache grouping;
171
360
  `prefix()` only controls URL paths.
172
361
 
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
+ ```
445
+
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:
456
+
457
+ ```ts
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
511
+
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
+
173
538
  ### Test factories and seeds
174
539
 
175
- Use `@beignet/core/testing` to keep feature tests and demo seed data
176
- port-based. Factories build app-owned records, and optional `persist` functions
177
- write through the context you pass in:
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:
178
543
 
179
544
  ```ts
180
545
  import {
181
- defineFactory,
546
+ createDatabaseTestHarness,
547
+ createFactory,
182
548
  defineSeed,
183
549
  resetFactories,
184
550
  runSeeds,
185
551
  } from "@beignet/core/testing";
186
552
 
187
- const postFactory = defineFactory("post", {
553
+ const postFactory = createFactory("post", {
188
554
  defaults: ({ sequence }) => ({
189
555
  title: `Post ${sequence}`,
190
556
  content: "Created in a test.",
@@ -207,9 +573,173 @@ export function resetPostFactories() {
207
573
  }
208
574
  ```
209
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" });
595
+ ```
596
+
210
597
  Keep factories and seeds app-owned. They should not import database clients,
211
598
  ORM table objects, or provider SDKs directly.
212
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
+
213
743
  ### Pagination
214
744
 
215
745
  Use `@beignet/core/pagination` to keep list use cases and repository ports
@@ -233,10 +763,65 @@ return ctx.ports.posts.findMany({
233
763
  Beignet's convention is `items` for list contents and `page` for pagination
234
764
  metadata. Keep filters and sort options app-owned plain objects.
235
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
+
236
794
  ### Idempotency
237
795
 
238
- Use `@beignet/core/idempotency` when a command, webhook, or job may be retried
239
- 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:
240
825
 
241
826
  ```ts
242
827
  import {
@@ -245,17 +830,17 @@ import {
245
830
  } from "@beignet/core/idempotency";
246
831
 
247
832
  const result = await runIdempotently(ctx.ports.idempotency, {
248
- namespace: "todos.create",
249
- key: input.idempotencyKey,
833
+ namespace: "todos.import",
834
+ key: input.importId,
250
835
  scope: {
251
836
  tenantId: ctx.tenant?.id,
252
837
  actorId: ctx.actor?.id,
253
838
  },
254
839
  fingerprint: await createIdempotencyFingerprint(input, {
255
- omit: ["idempotencyKey"],
840
+ omit: ["importId"],
256
841
  }),
257
842
  ttlSec: 60 * 60 * 24,
258
- run: () => ctx.ports.uow.transaction((tx) => tx.todos.create(input)),
843
+ run: () => ctx.ports.uow.transaction((tx) => tx.todos.importBatch(input)),
259
844
  });
260
845
  ```
261
846
 
@@ -268,9 +853,11 @@ const idempotency = createMemoryIdempotencyStore();
268
853
  ```
269
854
 
270
855
  Production apps should back `IdempotencyPort` with atomic SQL or Redis storage.
271
- For high-integrity workflows, prefer exposing a transaction-scoped
272
- `tx.idempotency` port from the app Unit of Work so reservation, business writes,
273
- 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.
274
861
 
275
862
  ### Outbox
276
863
 
@@ -317,6 +904,33 @@ await drainOutbox({
317
904
  The outbox is at-least-once delivery. Use idempotent listeners or jobs when a
318
905
  duplicate delivery would be harmful.
319
906
 
907
+ ### Schedules
908
+
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
+
320
934
  ### Contract metadata and route hooks
321
935
 
322
936
  Use metadata to describe cross-cutting concerns for OpenAPI, clients, docs, and
@@ -343,41 +957,159 @@ const sendMessage = messages
343
957
  });
344
958
  ```
345
959
 
346
- Use route hooks for runtime enforcement where the route is wired:
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:
347
964
 
348
965
  ```ts
349
- import {
350
- createAuthHooks,
351
- defineRoute,
352
- defineRouteGroup,
353
- } from "@beignet/core/server";
354
-
355
- const auth = createAuthHooks<AppContext, { user: CurrentUser }>({
356
- resolve: async ({ ctx, req }) => {
357
- const session = await ctx.ports.auth.getSession(req);
966
+ import { createAuthHooks, defineRouteGroup } from "@beignet/core/server";
967
+ import type { AppContext } from "@/app-context";
358
968
 
359
- return session ? { user: session.user } : null;
969
+ const auth = createAuthHooks<AppContext>()({
970
+ resolve: ({ ctx }) => {
971
+ return ctx.auth ? { user: ctx.auth.user } : null;
360
972
  },
361
973
  });
362
974
 
363
- const route = defineRoute<AppContext>();
364
-
365
975
  export const messageRoutes = defineRouteGroup<AppContext>()({
366
976
  name: "messages",
367
977
  routes: [
368
- route({
978
+ {
369
979
  contract: sendMessage,
370
980
  hooks: [auth.required()],
371
- handle: async ({ ctx, body }) => {
372
- ctx.user.id;
981
+ useCase: sendMessageUseCase,
982
+ },
983
+ ],
984
+ });
985
+ ```
373
986
 
374
- return sendMessageUseCase.run({ ctx, input: body });
375
- },
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,
376
1055
  }),
377
- ],
1056
+ },
378
1057
  });
379
1058
  ```
380
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
+
381
1113
  ### OpenAPI metadata
382
1114
 
383
1115
  Add OpenAPI-specific metadata for documentation using the `.openapi()` method:
@@ -401,6 +1133,12 @@ export const getTodo = todos
401
1133
  });
402
1134
  ```
403
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
+
404
1142
  ### Schema introspection
405
1143
 
406
1144
  Contracts expose their schemas for runtime introspection:
@@ -418,12 +1156,12 @@ getTodo.metadata; // { auth: "required", ... }
418
1156
 
419
1157
  ## API reference
420
1158
 
421
- ### `createContractGroup()`
1159
+ ### `defineContractGroup()`
422
1160
 
423
1161
  Creates a new contract group for defining related endpoints.
424
1162
 
425
1163
  ```ts
426
- const group = createContractGroup()
1164
+ const group = defineContractGroup()
427
1165
  .namespace("myNamespace") // Optional resource namespace
428
1166
  .prefix("/api/v1") // Optional URL path prefix
429
1167
  .meta({ auth: "required" }) // Shared metadata
@@ -433,6 +1171,10 @@ const group = createContractGroup()
433
1171
  });
434
1172
  ```
435
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
+
436
1178
  Any non-empty response map is treated as a response contract. Include
437
1179
  successful statuses such as `200` or `201` alongside custom error statuses; use
438
1180
  `responses: {}` only when you want to skip response validation. Prefer
@@ -453,7 +1195,7 @@ standard error envelope.
453
1195
  | `.headers(schema)` | Define request header schema |
454
1196
  | `.body(schema)` | Define request body schema |
455
1197
  | `.responses({ ... })` | Define or merge response schemas by status code |
456
- | `.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 |
457
1199
  | `.meta(metadata)` | Add custom metadata |
458
1200
  | `.openapi(options)` | Add OpenAPI metadata (summary, tags, etc.) |
459
1201
 
@@ -469,13 +1211,13 @@ OpenAPI generation currently requires Zod schemas, even though core contracts ca
469
1211
 
470
1212
  ## Related packages
471
1213
 
472
- - [`@beignet/web`](https://beignet.dev/server) - Web Fetch server adapter
473
- - [`@beignet/next`](https://beignet.dev/server) - Next.js server adapter
474
- - [`@beignet/react-query`](https://beignet.dev/react-query) - TanStack Query integration
475
- - [`@beignet/react-hook-form`](https://beignet.dev/react-hook-form) - React Hook Form integration
476
- - [`@beignet/react-uploads`](https://beignet.dev/react-uploads) - React upload state and progress hooks
477
- - [`@beignet/nuqs`](https://beignet.dev/nuqs) - URL query state integration with nuqs
478
- - [`@beignet/devtools`](https://beignet.dev/devtools) - Local request, provider, and audit timeline
1214
+ - [`@beignet/web`](https://beignetjs.com/server) - Web Fetch server adapter
1215
+ - [`@beignet/next`](https://beignetjs.com/server) - Next.js server adapter
1216
+ - [`@beignet/react-query`](https://beignetjs.com/react-query) - TanStack Query integration
1217
+ - [`@beignet/react-hook-form`](https://beignetjs.com/react-hook-form) - React Hook Form integration
1218
+ - [`@beignet/react-uploads`](https://beignetjs.com/react-uploads) - React upload state and progress hooks
1219
+ - [`@beignet/nuqs`](https://beignetjs.com/nuqs) - URL query state integration with nuqs
1220
+ - [`@beignet/devtools`](https://beignetjs.com/devtools) - Local request, provider, and audit timeline
479
1221
 
480
1222
  ## License
481
1223