@beignet/core 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (331) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/README.md +288 -0
  3. package/dist/application/index.d.ts +260 -0
  4. package/dist/application/index.d.ts.map +1 -0
  5. package/dist/application/index.js +324 -0
  6. package/dist/application/index.js.map +1 -0
  7. package/dist/client/client.d.ts +241 -0
  8. package/dist/client/client.d.ts.map +1 -0
  9. package/dist/client/client.js +531 -0
  10. package/dist/client/client.js.map +1 -0
  11. package/dist/client/index.d.ts +10 -0
  12. package/dist/client/index.d.ts.map +1 -0
  13. package/dist/client/index.js +8 -0
  14. package/dist/client/index.js.map +1 -0
  15. package/dist/client/types.d.ts +139 -0
  16. package/dist/client/types.d.ts.map +1 -0
  17. package/dist/client/types.js +2 -0
  18. package/dist/client/types.js.map +1 -0
  19. package/dist/config/index.d.ts +122 -0
  20. package/dist/config/index.d.ts.map +1 -0
  21. package/dist/config/index.js +216 -0
  22. package/dist/config/index.js.map +1 -0
  23. package/dist/contracts/contract-builder.d.ts +121 -0
  24. package/dist/contracts/contract-builder.d.ts.map +1 -0
  25. package/dist/contracts/contract-builder.js +346 -0
  26. package/dist/contracts/contract-builder.js.map +1 -0
  27. package/dist/contracts/contract-group.d.ts +106 -0
  28. package/dist/contracts/contract-group.d.ts.map +1 -0
  29. package/dist/contracts/contract-group.js +240 -0
  30. package/dist/contracts/contract-group.js.map +1 -0
  31. package/dist/contracts/contract-like.d.ts +21 -0
  32. package/dist/contracts/contract-like.d.ts.map +1 -0
  33. package/dist/contracts/contract-like.js +9 -0
  34. package/dist/contracts/contract-like.js.map +1 -0
  35. package/dist/contracts/index.d.ts +15 -0
  36. package/dist/contracts/index.d.ts.map +1 -0
  37. package/dist/contracts/index.js +11 -0
  38. package/dist/contracts/index.js.map +1 -0
  39. package/dist/contracts/openapi-meta.d.ts +23 -0
  40. package/dist/contracts/openapi-meta.d.ts.map +1 -0
  41. package/dist/contracts/openapi-meta.js +2 -0
  42. package/dist/contracts/openapi-meta.js.map +1 -0
  43. package/dist/contracts/path-template.d.ts +17 -0
  44. package/dist/contracts/path-template.d.ts.map +1 -0
  45. package/dist/contracts/path-template.js +50 -0
  46. package/dist/contracts/path-template.js.map +1 -0
  47. package/dist/contracts/rate-limit.d.ts +50 -0
  48. package/dist/contracts/rate-limit.d.ts.map +1 -0
  49. package/dist/contracts/rate-limit.js +2 -0
  50. package/dist/contracts/rate-limit.js.map +1 -0
  51. package/dist/contracts/types.d.ts +97 -0
  52. package/dist/contracts/types.d.ts.map +1 -0
  53. package/dist/contracts/types.js +54 -0
  54. package/dist/contracts/types.js.map +1 -0
  55. package/dist/contracts/utils.d.ts +3 -0
  56. package/dist/contracts/utils.d.ts.map +1 -0
  57. package/dist/contracts/utils.js +44 -0
  58. package/dist/contracts/utils.js.map +1 -0
  59. package/dist/domain/entity.d.ts +87 -0
  60. package/dist/domain/entity.d.ts.map +1 -0
  61. package/dist/domain/entity.js +155 -0
  62. package/dist/domain/entity.js.map +1 -0
  63. package/dist/domain/events.d.ts +41 -0
  64. package/dist/domain/events.d.ts.map +1 -0
  65. package/dist/domain/events.js +21 -0
  66. package/dist/domain/events.js.map +1 -0
  67. package/dist/domain/index.d.ts +14 -0
  68. package/dist/domain/index.d.ts.map +1 -0
  69. package/dist/domain/index.js +14 -0
  70. package/dist/domain/index.js.map +1 -0
  71. package/dist/domain/value-object.d.ts +60 -0
  72. package/dist/domain/value-object.d.ts.map +1 -0
  73. package/dist/domain/value-object.js +87 -0
  74. package/dist/domain/value-object.js.map +1 -0
  75. package/dist/errors/catalog.d.ts +71 -0
  76. package/dist/errors/catalog.d.ts.map +1 -0
  77. package/dist/errors/catalog.js +71 -0
  78. package/dist/errors/catalog.js.map +1 -0
  79. package/dist/errors/http.d.ts +77 -0
  80. package/dist/errors/http.d.ts.map +1 -0
  81. package/dist/errors/http.js +74 -0
  82. package/dist/errors/http.js.map +1 -0
  83. package/dist/errors/index.d.ts +10 -0
  84. package/dist/errors/index.d.ts.map +1 -0
  85. package/dist/errors/index.js +14 -0
  86. package/dist/errors/index.js.map +1 -0
  87. package/dist/errors/response.d.ts +26 -0
  88. package/dist/errors/response.d.ts.map +1 -0
  89. package/dist/errors/response.js +34 -0
  90. package/dist/errors/response.js.map +1 -0
  91. package/dist/errors/validation.d.ts +18 -0
  92. package/dist/errors/validation.d.ts.map +1 -0
  93. package/dist/errors/validation.js +21 -0
  94. package/dist/errors/validation.js.map +1 -0
  95. package/dist/events/index.d.ts +58 -0
  96. package/dist/events/index.d.ts.map +1 -0
  97. package/dist/events/index.js +102 -0
  98. package/dist/events/index.js.map +1 -0
  99. package/dist/jobs/index.d.ts +56 -0
  100. package/dist/jobs/index.d.ts.map +1 -0
  101. package/dist/jobs/index.js +89 -0
  102. package/dist/jobs/index.js.map +1 -0
  103. package/dist/mail/index.d.ts +75 -0
  104. package/dist/mail/index.d.ts.map +1 -0
  105. package/dist/mail/index.js +84 -0
  106. package/dist/mail/index.js.map +1 -0
  107. package/dist/openapi/index.d.ts +207 -0
  108. package/dist/openapi/index.d.ts.map +1 -0
  109. package/dist/openapi/index.js +449 -0
  110. package/dist/openapi/index.js.map +1 -0
  111. package/dist/openapi/schema-introspector.d.ts +38 -0
  112. package/dist/openapi/schema-introspector.d.ts.map +1 -0
  113. package/dist/openapi/schema-introspector.js +67 -0
  114. package/dist/openapi/schema-introspector.js.map +1 -0
  115. package/dist/ports/audit.d.ts +58 -0
  116. package/dist/ports/audit.d.ts.map +1 -0
  117. package/dist/ports/audit.js +74 -0
  118. package/dist/ports/audit.js.map +1 -0
  119. package/dist/ports/auth.d.ts +23 -0
  120. package/dist/ports/auth.d.ts.map +1 -0
  121. package/dist/ports/auth.js +31 -0
  122. package/dist/ports/auth.js.map +1 -0
  123. package/dist/ports/builder.d.ts +61 -0
  124. package/dist/ports/builder.d.ts.map +1 -0
  125. package/dist/ports/builder.js +48 -0
  126. package/dist/ports/builder.js.map +1 -0
  127. package/dist/ports/cache.d.ts +15 -0
  128. package/dist/ports/cache.d.ts.map +1 -0
  129. package/dist/ports/cache.js +57 -0
  130. package/dist/ports/cache.js.map +1 -0
  131. package/dist/ports/clock.d.ts +10 -0
  132. package/dist/ports/clock.d.ts.map +1 -0
  133. package/dist/ports/clock.js +21 -0
  134. package/dist/ports/clock.js.map +1 -0
  135. package/dist/ports/events.d.ts +71 -0
  136. package/dist/ports/events.d.ts.map +1 -0
  137. package/dist/ports/events.js +2 -0
  138. package/dist/ports/events.js.map +1 -0
  139. package/dist/ports/id-generator.d.ts +12 -0
  140. package/dist/ports/id-generator.d.ts.map +1 -0
  141. package/dist/ports/id-generator.js +22 -0
  142. package/dist/ports/id-generator.js.map +1 -0
  143. package/dist/ports/index.d.ts +98 -0
  144. package/dist/ports/index.d.ts.map +1 -0
  145. package/dist/ports/index.js +67 -0
  146. package/dist/ports/index.js.map +1 -0
  147. package/dist/ports/logger.d.ts +22 -0
  148. package/dist/ports/logger.d.ts.map +1 -0
  149. package/dist/ports/logger.js +34 -0
  150. package/dist/ports/logger.js.map +1 -0
  151. package/dist/ports/mailer.d.ts +6 -0
  152. package/dist/ports/mailer.d.ts.map +1 -0
  153. package/dist/ports/mailer.js +2 -0
  154. package/dist/ports/mailer.js.map +1 -0
  155. package/dist/ports/policy.d.ts +53 -0
  156. package/dist/ports/policy.d.ts.map +1 -0
  157. package/dist/ports/policy.js +81 -0
  158. package/dist/ports/policy.js.map +1 -0
  159. package/dist/ports/rate-limit.d.ts +41 -0
  160. package/dist/ports/rate-limit.d.ts.map +1 -0
  161. package/dist/ports/rate-limit.js +37 -0
  162. package/dist/ports/rate-limit.js.map +1 -0
  163. package/dist/ports/redaction.d.ts +26 -0
  164. package/dist/ports/redaction.d.ts.map +1 -0
  165. package/dist/ports/redaction.js +126 -0
  166. package/dist/ports/redaction.js.map +1 -0
  167. package/dist/ports/schedules.d.ts +9 -0
  168. package/dist/ports/schedules.d.ts.map +1 -0
  169. package/dist/ports/schedules.js +2 -0
  170. package/dist/ports/schedules.js.map +1 -0
  171. package/dist/ports/storage.d.ts +47 -0
  172. package/dist/ports/storage.d.ts.map +1 -0
  173. package/dist/ports/storage.js +185 -0
  174. package/dist/ports/storage.js.map +1 -0
  175. package/dist/ports/testing.d.ts +73 -0
  176. package/dist/ports/testing.d.ts.map +1 -0
  177. package/dist/ports/testing.js +105 -0
  178. package/dist/ports/testing.js.map +1 -0
  179. package/dist/ports/unit-of-work.d.ts +56 -0
  180. package/dist/ports/unit-of-work.d.ts.map +1 -0
  181. package/dist/ports/unit-of-work.js +64 -0
  182. package/dist/ports/unit-of-work.js.map +1 -0
  183. package/dist/providers/index.d.ts +8 -0
  184. package/dist/providers/index.d.ts.map +1 -0
  185. package/dist/providers/index.js +8 -0
  186. package/dist/providers/index.js.map +1 -0
  187. package/dist/providers/instrumentation.d.ts +91 -0
  188. package/dist/providers/instrumentation.d.ts.map +1 -0
  189. package/dist/providers/instrumentation.js +93 -0
  190. package/dist/providers/instrumentation.js.map +1 -0
  191. package/dist/providers/provider.d.ts +146 -0
  192. package/dist/providers/provider.d.ts.map +1 -0
  193. package/dist/providers/provider.js +31 -0
  194. package/dist/providers/provider.js.map +1 -0
  195. package/dist/schedules/index.d.ts +105 -0
  196. package/dist/schedules/index.d.ts.map +1 -0
  197. package/dist/schedules/index.js +178 -0
  198. package/dist/schedules/index.js.map +1 -0
  199. package/dist/server/contract-like.d.ts +5 -0
  200. package/dist/server/contract-like.d.ts.map +1 -0
  201. package/dist/server/contract-like.js +5 -0
  202. package/dist/server/contract-like.js.map +1 -0
  203. package/dist/server/health.d.ts +41 -0
  204. package/dist/server/health.d.ts.map +1 -0
  205. package/dist/server/health.js +46 -0
  206. package/dist/server/health.js.map +1 -0
  207. package/dist/server/hooks/auth.d.ts +42 -0
  208. package/dist/server/hooks/auth.d.ts.map +1 -0
  209. package/dist/server/hooks/auth.js +61 -0
  210. package/dist/server/hooks/auth.js.map +1 -0
  211. package/dist/server/hooks/cors.d.ts +13 -0
  212. package/dist/server/hooks/cors.d.ts.map +1 -0
  213. package/dist/server/hooks/cors.js +70 -0
  214. package/dist/server/hooks/cors.js.map +1 -0
  215. package/dist/server/hooks/errors.d.ts +66 -0
  216. package/dist/server/hooks/errors.d.ts.map +1 -0
  217. package/dist/server/hooks/errors.js +83 -0
  218. package/dist/server/hooks/errors.js.map +1 -0
  219. package/dist/server/hooks/index.d.ts +12 -0
  220. package/dist/server/hooks/index.d.ts.map +1 -0
  221. package/dist/server/hooks/index.js +12 -0
  222. package/dist/server/hooks/index.js.map +1 -0
  223. package/dist/server/hooks/logging.d.ts +33 -0
  224. package/dist/server/hooks/logging.d.ts.map +1 -0
  225. package/dist/server/hooks/logging.js +90 -0
  226. package/dist/server/hooks/logging.js.map +1 -0
  227. package/dist/server/hooks/rate-limit.d.ts +29 -0
  228. package/dist/server/hooks/rate-limit.d.ts.map +1 -0
  229. package/dist/server/hooks/rate-limit.js +93 -0
  230. package/dist/server/hooks/rate-limit.js.map +1 -0
  231. package/dist/server/hooks/utils.d.ts +9 -0
  232. package/dist/server/hooks/utils.d.ts.map +1 -0
  233. package/dist/server/hooks/utils.js +16 -0
  234. package/dist/server/hooks/utils.js.map +1 -0
  235. package/dist/server/hooks.d.ts +2 -0
  236. package/dist/server/hooks.d.ts.map +1 -0
  237. package/dist/server/hooks.js +2 -0
  238. package/dist/server/hooks.js.map +1 -0
  239. package/dist/server/http.d.ts +124 -0
  240. package/dist/server/http.d.ts.map +1 -0
  241. package/dist/server/http.js +2 -0
  242. package/dist/server/http.js.map +1 -0
  243. package/dist/server/index.d.ts +19 -0
  244. package/dist/server/index.d.ts.map +1 -0
  245. package/dist/server/index.js +15 -0
  246. package/dist/server/index.js.map +1 -0
  247. package/dist/server/openapi.d.ts +32 -0
  248. package/dist/server/openapi.d.ts.map +1 -0
  249. package/dist/server/openapi.js +43 -0
  250. package/dist/server/openapi.js.map +1 -0
  251. package/dist/server/providers/index.d.ts +4 -0
  252. package/dist/server/providers/index.d.ts.map +1 -0
  253. package/dist/server/providers/index.js +4 -0
  254. package/dist/server/providers/index.js.map +1 -0
  255. package/dist/server/providers/loadProviderConfig.d.ts +7 -0
  256. package/dist/server/providers/loadProviderConfig.d.ts.map +1 -0
  257. package/dist/server/providers/loadProviderConfig.js +42 -0
  258. package/dist/server/providers/loadProviderConfig.js.map +1 -0
  259. package/dist/server/server.d.ts +86 -0
  260. package/dist/server/server.d.ts.map +1 -0
  261. package/dist/server/server.js +1031 -0
  262. package/dist/server/server.js.map +1 -0
  263. package/dist/server/types.d.ts +3 -0
  264. package/dist/server/types.d.ts.map +1 -0
  265. package/dist/server/types.js +3 -0
  266. package/dist/server/types.js.map +1 -0
  267. package/package.json +129 -0
  268. package/src/application/index.ts +747 -0
  269. package/src/client/client.ts +1105 -0
  270. package/src/client/index.ts +45 -0
  271. package/src/client/types.ts +305 -0
  272. package/src/config/index.ts +497 -0
  273. package/src/contracts/contract-builder.ts +583 -0
  274. package/src/contracts/contract-group.ts +502 -0
  275. package/src/contracts/contract-like.ts +29 -0
  276. package/src/contracts/index.ts +53 -0
  277. package/src/contracts/openapi-meta.ts +22 -0
  278. package/src/contracts/path-template.ts +91 -0
  279. package/src/contracts/rate-limit.ts +50 -0
  280. package/src/contracts/types.ts +207 -0
  281. package/src/contracts/utils.ts +56 -0
  282. package/src/domain/entity.ts +256 -0
  283. package/src/domain/events.ts +52 -0
  284. package/src/domain/index.ts +18 -0
  285. package/src/domain/value-object.ts +135 -0
  286. package/src/errors/catalog.ts +149 -0
  287. package/src/errors/http.ts +80 -0
  288. package/src/errors/index.ts +28 -0
  289. package/src/errors/response.ts +54 -0
  290. package/src/errors/validation.ts +35 -0
  291. package/src/events/index.ts +246 -0
  292. package/src/jobs/index.ts +211 -0
  293. package/src/mail/index.ts +177 -0
  294. package/src/openapi/index.ts +865 -0
  295. package/src/openapi/schema-introspector.ts +107 -0
  296. package/src/ports/audit.ts +176 -0
  297. package/src/ports/auth.ts +76 -0
  298. package/src/ports/builder.ts +97 -0
  299. package/src/ports/cache.ts +94 -0
  300. package/src/ports/clock.ts +34 -0
  301. package/src/ports/events.ts +100 -0
  302. package/src/ports/id-generator.ts +36 -0
  303. package/src/ports/index.ts +221 -0
  304. package/src/ports/logger.ts +67 -0
  305. package/src/ports/policy.ts +242 -0
  306. package/src/ports/rate-limit.ts +91 -0
  307. package/src/ports/redaction.ts +199 -0
  308. package/src/ports/storage.ts +282 -0
  309. package/src/ports/testing.ts +234 -0
  310. package/src/ports/unit-of-work.ts +134 -0
  311. package/src/providers/index.ts +40 -0
  312. package/src/providers/instrumentation.ts +248 -0
  313. package/src/providers/provider.ts +191 -0
  314. package/src/schedules/index.ts +442 -0
  315. package/src/server/contract-like.ts +8 -0
  316. package/src/server/health.ts +82 -0
  317. package/src/server/hooks/auth.ts +147 -0
  318. package/src/server/hooks/cors.ts +87 -0
  319. package/src/server/hooks/errors.ts +126 -0
  320. package/src/server/hooks/index.ts +43 -0
  321. package/src/server/hooks/logging.ts +121 -0
  322. package/src/server/hooks/rate-limit.ts +171 -0
  323. package/src/server/hooks/utils.ts +16 -0
  324. package/src/server/hooks.ts +1 -0
  325. package/src/server/http.ts +189 -0
  326. package/src/server/index.ts +35 -0
  327. package/src/server/openapi.ts +72 -0
  328. package/src/server/providers/index.ts +3 -0
  329. package/src/server/providers/loadProviderConfig.ts +72 -0
  330. package/src/server/server.ts +1521 -0
  331. package/src/server/types.ts +2 -0
@@ -0,0 +1,865 @@
1
+ /**
2
+ * @beignet/core/openapi
3
+ *
4
+ * OpenAPI 3.1 generation from Beignet contracts
5
+ */
6
+
7
+ import { type ZodTypeAny, z } from "zod";
8
+ import {
9
+ type AnyContract,
10
+ type ContractLike,
11
+ getContractHeaderSchemas,
12
+ methodSupportsRequestBody,
13
+ parsePathTemplate,
14
+ resolveContract,
15
+ STANDARD_ERROR_RESPONSE_SCHEMA,
16
+ } from "../contracts";
17
+ import {
18
+ createZodIntrospector,
19
+ type SchemaIntrospector,
20
+ } from "./schema-introspector";
21
+
22
+ // Re-export the introspector types for consumers who want custom implementations
23
+ export {
24
+ createZodIntrospector,
25
+ type SchemaIntrospector,
26
+ } from "./schema-introspector";
27
+
28
+ /**
29
+ * OpenAPI 3.1 Info Object
30
+ */
31
+ export interface OpenAPIInfo {
32
+ title: string;
33
+ version: string;
34
+ description?: string;
35
+ termsOfService?: string;
36
+ contact?: {
37
+ name?: string;
38
+ url?: string;
39
+ email?: string;
40
+ };
41
+ license?: {
42
+ name: string;
43
+ url?: string;
44
+ };
45
+ }
46
+
47
+ /**
48
+ * OpenAPI 3.1 Server Object
49
+ */
50
+ export interface OpenAPIServer {
51
+ url: string;
52
+ description?: string;
53
+ variables?: Record<
54
+ string,
55
+ {
56
+ default: string;
57
+ enum?: string[];
58
+ description?: string;
59
+ }
60
+ >;
61
+ }
62
+
63
+ /**
64
+ * OpenAPI 3.1 Security Scheme Object
65
+ */
66
+ export type OpenAPISecurityScheme =
67
+ | {
68
+ type: "apiKey";
69
+ name: string;
70
+ in: "query" | "header" | "cookie";
71
+ description?: string;
72
+ }
73
+ | {
74
+ type: "http";
75
+ scheme: string;
76
+ bearerFormat?: string;
77
+ description?: string;
78
+ }
79
+ | {
80
+ type: "oauth2";
81
+ flows: Record<string, unknown>;
82
+ description?: string;
83
+ }
84
+ | {
85
+ type: "openIdConnect";
86
+ openIdConnectUrl: string;
87
+ description?: string;
88
+ };
89
+
90
+ /**
91
+ * Schema or Reference object for OpenAPI
92
+ */
93
+ export type SchemaObject = Record<string, unknown>;
94
+ export type ReferenceObject = { $ref: string };
95
+
96
+ /**
97
+ * OpenAPI Parameter Object
98
+ */
99
+ export interface ParameterObject {
100
+ name: string;
101
+ in: "path" | "query" | "header" | "cookie";
102
+ required?: boolean;
103
+ schema?: SchemaObject | ReferenceObject;
104
+ description?: string;
105
+ deprecated?: boolean;
106
+ }
107
+
108
+ /**
109
+ * OpenAPI Request Body Object
110
+ */
111
+ export interface RequestBodyObject {
112
+ required?: boolean;
113
+ description?: string;
114
+ content: Record<
115
+ string,
116
+ {
117
+ schema?: SchemaObject | ReferenceObject;
118
+ }
119
+ >;
120
+ }
121
+
122
+ /**
123
+ * OpenAPI Response Object
124
+ */
125
+ export interface ResponseObject {
126
+ description: string;
127
+ content?: Record<
128
+ string,
129
+ {
130
+ schema?: SchemaObject | ReferenceObject;
131
+ examples?: Record<
132
+ string,
133
+ {
134
+ summary?: string;
135
+ value?: unknown;
136
+ }
137
+ >;
138
+ }
139
+ >;
140
+ }
141
+
142
+ /**
143
+ * OpenAPI Operation Object
144
+ */
145
+ export interface OperationObject {
146
+ operationId?: string;
147
+ summary?: string;
148
+ description?: string;
149
+ tags?: string[];
150
+ deprecated?: boolean;
151
+ externalDocs?: {
152
+ description?: string;
153
+ url: string;
154
+ };
155
+ security?: Array<Record<string, string[]>>;
156
+ parameters?: ParameterObject[];
157
+ requestBody?: RequestBodyObject;
158
+ responses: Record<string, ResponseObject>;
159
+ }
160
+
161
+ /**
162
+ * OpenAPI Path Item Object
163
+ */
164
+ export interface PathItemObject {
165
+ get?: OperationObject;
166
+ post?: OperationObject;
167
+ put?: OperationObject;
168
+ patch?: OperationObject;
169
+ delete?: OperationObject;
170
+ head?: OperationObject;
171
+ options?: OperationObject;
172
+ }
173
+
174
+ /**
175
+ * OpenAPI Paths Object
176
+ */
177
+ export type PathsObject = Record<string, PathItemObject>;
178
+
179
+ /**
180
+ * OpenAPI Components Object
181
+ */
182
+ export interface ComponentsObject {
183
+ schemas?: Record<string, SchemaObject>;
184
+ securitySchemes?: Record<string, OpenAPISecurityScheme>;
185
+ }
186
+
187
+ /**
188
+ * OpenAPI 3.1 Document
189
+ */
190
+ export interface OpenAPIObject {
191
+ openapi: "3.1.0";
192
+ info: OpenAPIInfo;
193
+ servers?: OpenAPIServer[];
194
+ paths: PathsObject;
195
+ components?: ComponentsObject;
196
+ security?: Array<Record<string, string[]>>;
197
+ }
198
+
199
+ /**
200
+ * Options for generating OpenAPI spec
201
+ */
202
+ export interface OpenAPIGeneratorOptions {
203
+ /** API title */
204
+ title: string;
205
+ /** API version */
206
+ version: string;
207
+ /** API description */
208
+ description?: string;
209
+ /** Server configurations */
210
+ servers?: OpenAPIServer[];
211
+ /** Media type for JSON content (default: application/json) */
212
+ jsonMediaType?: string;
213
+ /** Security schemes for authentication */
214
+ securitySchemes?: Record<string, OpenAPISecurityScheme>;
215
+ /** Global security requirements */
216
+ security?: Array<Record<string, string[]>>;
217
+ /**
218
+ * Schema introspector for reading metadata from schema objects.
219
+ * Defaults to a Zod introspector. Provide a custom implementation
220
+ * to support other schema libraries.
221
+ */
222
+ schemaIntrospector?: SchemaIntrospector;
223
+ }
224
+
225
+ /**
226
+ * Internal state for generator
227
+ */
228
+ type GeneratorState = {
229
+ components: ComponentsObject;
230
+ jsonMediaType: string;
231
+ introspector: SchemaIntrospector;
232
+ };
233
+
234
+ type SchemaIO = "input" | "output";
235
+
236
+ /**
237
+ * Type for contracts that can be passed to the generator.
238
+ * Accepts both ContractBuilder instances and plain HttpContractConfig objects.
239
+ */
240
+ export type ContractInput = ContractLike;
241
+
242
+ /**
243
+ * Convert an array of contracts to an OpenAPI 3.1 document
244
+ *
245
+ * @param contracts - Array of HTTP contracts (ContractBuilder instances or configs)
246
+ * @param options - OpenAPI generation options
247
+ * @returns OpenAPI 3.1 document object
248
+ *
249
+ * @example
250
+ * ```ts
251
+ * import { contractsToOpenAPI } from "@beignet/core/openapi";
252
+ * import { getTodo, listTodos } from "./contracts";
253
+ *
254
+ * const spec = contractsToOpenAPI(
255
+ * [getTodo, listTodos],
256
+ * {
257
+ * title: "Todo API",
258
+ * version: "1.0.0",
259
+ * servers: [{ url: "https://api.example.com" }],
260
+ * }
261
+ * );
262
+ * ```
263
+ */
264
+ export function contractsToOpenAPI(
265
+ contracts: readonly ContractInput[],
266
+ options: OpenAPIGeneratorOptions,
267
+ ): OpenAPIObject {
268
+ const paths: PathsObject = {};
269
+ const components: ComponentsObject = {
270
+ schemas: {},
271
+ securitySchemes: options.securitySchemes ?? {},
272
+ };
273
+
274
+ const state: GeneratorState = {
275
+ components,
276
+ jsonMediaType: options.jsonMediaType ?? "application/json",
277
+ introspector: options.schemaIntrospector ?? createZodIntrospector(),
278
+ };
279
+
280
+ for (const contract of contracts) {
281
+ const config = resolveContract(contract);
282
+ addContractToPaths(config, paths, state);
283
+ }
284
+
285
+ const openapi: OpenAPIObject = {
286
+ openapi: "3.1.0",
287
+ info: {
288
+ title: options.title,
289
+ version: options.version,
290
+ description: options.description,
291
+ },
292
+ servers: options.servers,
293
+ paths,
294
+ components:
295
+ Object.keys(components.schemas ?? {}).length > 0 ||
296
+ Object.keys(components.securitySchemes ?? {}).length > 0
297
+ ? components
298
+ : undefined,
299
+ security: options.security,
300
+ };
301
+
302
+ return openapi;
303
+ }
304
+
305
+ // =============================================================================
306
+ // OpenAPI Generation Helpers
307
+ // =============================================================================
308
+
309
+ /**
310
+ * Add a single contract to the paths object
311
+ */
312
+ function addContractToPaths(
313
+ contract: AnyContract,
314
+ paths: PathsObject,
315
+ state: GeneratorState,
316
+ ): void {
317
+ const pathKey = parsePathTemplate(contract.path).openApiPath;
318
+ if (!paths[pathKey]) {
319
+ paths[pathKey] = {};
320
+ }
321
+ const pathItem = paths[pathKey];
322
+
323
+ const meta = contract.metadata?.openapi;
324
+
325
+ const operation: OperationObject = {
326
+ operationId: meta?.operationId ?? contract.name,
327
+ summary: meta?.summary,
328
+ description: meta?.description,
329
+ tags: meta?.tags,
330
+ deprecated: meta?.deprecated,
331
+ externalDocs: meta?.externalDocs,
332
+ security: meta?.security,
333
+ parameters: [],
334
+ responses: {},
335
+ };
336
+
337
+ addPathParams(contract, operation, state);
338
+ addQueryParams(contract, operation, state);
339
+ addHeaderParams(contract, operation, state);
340
+ addRequestBody(contract, operation, state);
341
+ addResponses(contract, operation, state);
342
+
343
+ // Clean up empty parameters array
344
+ if (operation.parameters?.length === 0) {
345
+ delete operation.parameters;
346
+ }
347
+
348
+ const methodKey = contract.method.toLowerCase() as
349
+ | "get"
350
+ | "post"
351
+ | "put"
352
+ | "patch"
353
+ | "delete"
354
+ | "head"
355
+ | "options";
356
+
357
+ pathItem[methodKey] = operation;
358
+ }
359
+
360
+ function addParameter(
361
+ operation: OperationObject,
362
+ parameter: ParameterObject,
363
+ ): void {
364
+ if (!operation.parameters) {
365
+ operation.parameters = [];
366
+ }
367
+
368
+ const existingIndex = operation.parameters.findIndex(
369
+ (existing) =>
370
+ existing.in === parameter.in && existing.name === parameter.name,
371
+ );
372
+ if (existingIndex >= 0) {
373
+ operation.parameters[existingIndex] = parameter;
374
+ return;
375
+ }
376
+
377
+ operation.parameters.push(parameter);
378
+ }
379
+
380
+ /**
381
+ * Add path parameters from contract to operation.
382
+ */
383
+ function addPathParams(
384
+ contract: AnyContract,
385
+ operation: OperationObject,
386
+ state: GeneratorState,
387
+ ): void {
388
+ const pathKeys = parsePathTemplate(contract.path).keys;
389
+ if (!contract.pathParams) {
390
+ for (const key of pathKeys) {
391
+ addParameter(operation, {
392
+ name: key,
393
+ in: "path",
394
+ required: true,
395
+ schema: { type: "string" },
396
+ });
397
+ }
398
+ return;
399
+ }
400
+
401
+ const shape = state.introspector.getShape(contract.pathParams);
402
+ if (!shape) {
403
+ if (pathKeys.length > 0) {
404
+ throw new Error(
405
+ `Unable to introspect pathParams for contract "${contract.name}" at "${contract.path}". OpenAPI path parameters must match the path template.`,
406
+ );
407
+ }
408
+ return;
409
+ }
410
+
411
+ const shapeKeys = Object.keys(shape);
412
+ const missingKeys = pathKeys.filter((key) => !shapeKeys.includes(key));
413
+ const extraKeys = shapeKeys.filter((key) => !pathKeys.includes(key));
414
+ if (missingKeys.length > 0 || extraKeys.length > 0) {
415
+ const details = [
416
+ missingKeys.length > 0
417
+ ? `missing pathParams keys: ${missingKeys.join(", ")}`
418
+ : undefined,
419
+ extraKeys.length > 0
420
+ ? `extra pathParams keys: ${extraKeys.join(", ")}`
421
+ : undefined,
422
+ ]
423
+ .filter(Boolean)
424
+ .join("; ");
425
+ throw new Error(
426
+ `Path parameters for contract "${contract.name}" must match "${contract.path}" (${details}).`,
427
+ );
428
+ }
429
+
430
+ for (const key of pathKeys) {
431
+ const field = shape[key];
432
+ const paramSchemaRef = zodToSchemaRef(
433
+ field as ZodTypeAny,
434
+ `${contract.name}_path_${key}`,
435
+ state,
436
+ "input",
437
+ );
438
+
439
+ const description = state.introspector.getDescription(field);
440
+
441
+ const param: ParameterObject = {
442
+ name: key,
443
+ in: "path",
444
+ required: true,
445
+ schema: paramSchemaRef,
446
+ description,
447
+ };
448
+
449
+ addParameter(operation, param);
450
+ }
451
+ }
452
+
453
+ /**
454
+ * Add query parameters from contract to operation.
455
+ */
456
+ function addQueryParams(
457
+ contract: AnyContract,
458
+ operation: OperationObject,
459
+ state: GeneratorState,
460
+ ): void {
461
+ if (!contract.query) return;
462
+
463
+ const shape = state.introspector.getShape(contract.query);
464
+ if (!shape) return;
465
+
466
+ for (const key of Object.keys(shape)) {
467
+ const originalField = shape[key];
468
+ const optional = state.introspector.isOptional(originalField);
469
+
470
+ let description = state.introspector.getDescription(originalField);
471
+
472
+ const field = originalField;
473
+
474
+ // If outer optional didn't have description, check inner type
475
+ if (!description && optional) {
476
+ description = state.introspector.getDescription(field);
477
+ }
478
+
479
+ const paramSchemaRef = zodToSchemaRef(
480
+ field as ZodTypeAny,
481
+ `${contract.name}_query_${key}`,
482
+ state,
483
+ "input",
484
+ );
485
+
486
+ const param: ParameterObject = {
487
+ name: key,
488
+ in: "query",
489
+ required: !optional,
490
+ schema: paramSchemaRef,
491
+ description,
492
+ };
493
+
494
+ addParameter(operation, param);
495
+ }
496
+ }
497
+
498
+ /**
499
+ * Add header parameters from contract to operation.
500
+ */
501
+ function addHeaderParams(
502
+ contract: AnyContract,
503
+ operation: OperationObject,
504
+ state: GeneratorState,
505
+ ): void {
506
+ const headerSchemas = getContractHeaderSchemas(contract.headers);
507
+ for (const headerSchema of headerSchemas) {
508
+ const shape = state.introspector.getShape(headerSchema);
509
+ if (!shape) continue;
510
+
511
+ for (const key of Object.keys(shape)) {
512
+ const originalField = shape[key];
513
+ const optional = state.introspector.isOptional(originalField);
514
+ const description = state.introspector.getDescription(originalField);
515
+
516
+ const paramSchemaRef = zodToSchemaRef(
517
+ originalField as ZodTypeAny,
518
+ `${contract.name}_header_${key}`,
519
+ state,
520
+ "input",
521
+ );
522
+
523
+ addParameter(operation, {
524
+ name: key,
525
+ in: "header",
526
+ required: !optional,
527
+ schema: paramSchemaRef,
528
+ description,
529
+ });
530
+ }
531
+ }
532
+ }
533
+
534
+ /**
535
+ * Add request body from contract to operation
536
+ */
537
+ function addRequestBody(
538
+ contract: AnyContract,
539
+ operation: OperationObject,
540
+ state: GeneratorState,
541
+ ): void {
542
+ if (!contract.body) return;
543
+ if (!methodSupportsRequestBody(contract.method)) {
544
+ throw new Error(
545
+ `Request bodies are not supported for ${contract.method} contracts. Use POST, PUT, or PATCH for contract request bodies.`,
546
+ );
547
+ }
548
+
549
+ const schemaRef = zodToSchemaRef(
550
+ contract.body as ZodTypeAny,
551
+ `${contract.name}_body`,
552
+ state,
553
+ "input",
554
+ );
555
+
556
+ const description = state.introspector.getDescription(contract.body);
557
+
558
+ operation.requestBody = {
559
+ required: true,
560
+ description,
561
+ content: {
562
+ [state.jsonMediaType]: {
563
+ schema: schemaRef,
564
+ },
565
+ },
566
+ };
567
+ }
568
+
569
+ /**
570
+ * Add responses from contract to operation
571
+ */
572
+ function addResponses(
573
+ contract: AnyContract,
574
+ operation: OperationObject,
575
+ state: GeneratorState,
576
+ ): void {
577
+ // Process all responses (both success and error status codes)
578
+ for (const [statusKey, zodSchema] of Object.entries(contract.responses)) {
579
+ const status = statusKey; // Keep as string
580
+ const catalogErrors = getCatalogErrorsForStatus(contract, Number(status));
581
+
582
+ // null schema means void/empty response (e.g. .responses({ 204: null }))
583
+ const hasSchema = zodSchema != null;
584
+
585
+ const schemaRef = hasSchema
586
+ ? catalogErrors.length > 0 && zodSchema === STANDARD_ERROR_RESPONSE_SCHEMA
587
+ ? catalogErrorResponseSchemaRef(
588
+ catalogErrors,
589
+ `${contract.name}_response_${status}`,
590
+ state,
591
+ )
592
+ : schemaToSchemaRef(
593
+ zodSchema,
594
+ `${contract.name}_response_${status}`,
595
+ state,
596
+ "output",
597
+ )
598
+ : undefined;
599
+
600
+ const described = hasSchema
601
+ ? state.introspector.getDescription(zodSchema)
602
+ : undefined;
603
+
604
+ let description: string;
605
+
606
+ if (described) {
607
+ description = described;
608
+ } else if (catalogErrors.length === 1) {
609
+ description = catalogErrors[0].message;
610
+ } else if (catalogErrors.length > 1) {
611
+ description = catalogErrors.map((error) => error.message).join("; ");
612
+ } else if (status === "200") {
613
+ description = "OK";
614
+ } else if (status === "201") {
615
+ description = "Created";
616
+ } else if (status === "204") {
617
+ description = "No Content";
618
+ } else if (status === "400") {
619
+ description = "Bad Request";
620
+ } else if (status === "401") {
621
+ description = "Unauthorized";
622
+ } else if (status === "403") {
623
+ description = "Forbidden";
624
+ } else if (status === "404") {
625
+ description = "Not Found";
626
+ } else if (status === "500") {
627
+ description = "Internal Server Error";
628
+ } else {
629
+ description = `HTTP ${status}`;
630
+ }
631
+
632
+ const examples = examplesFromCatalogErrors(catalogErrors);
633
+ operation.responses[status] = {
634
+ description,
635
+ content:
636
+ !hasSchema || status === "204"
637
+ ? undefined
638
+ : {
639
+ [state.jsonMediaType]: {
640
+ schema: schemaRef,
641
+ ...(examples ? { examples } : {}),
642
+ },
643
+ },
644
+ };
645
+ }
646
+ }
647
+
648
+ type CatalogErrorForOpenAPI = {
649
+ key: string;
650
+ code: string;
651
+ status: number;
652
+ message: string;
653
+ details?: unknown;
654
+ };
655
+
656
+ function getCatalogErrorsForStatus(
657
+ contract: AnyContract,
658
+ status: number,
659
+ ): CatalogErrorForOpenAPI[] {
660
+ const errors = contract.metadata?.errors;
661
+ if (typeof errors !== "object" || errors === null) return [];
662
+
663
+ return Object.entries(errors)
664
+ .map(([key, error]) => {
665
+ if (
666
+ typeof error !== "object" ||
667
+ error === null ||
668
+ typeof (error as { code?: unknown }).code !== "string" ||
669
+ typeof (error as { status?: unknown }).status !== "number" ||
670
+ typeof (error as { message?: unknown }).message !== "string"
671
+ ) {
672
+ return undefined;
673
+ }
674
+
675
+ const catalogError: CatalogErrorForOpenAPI = {
676
+ key,
677
+ code: (error as { code: string }).code,
678
+ status: (error as { status: number }).status,
679
+ message: (error as { message: string }).message,
680
+ };
681
+ const details = (error as { details?: unknown }).details;
682
+ if (details !== undefined) {
683
+ catalogError.details = details;
684
+ }
685
+
686
+ return catalogError;
687
+ })
688
+ .filter(
689
+ (error): error is CatalogErrorForOpenAPI =>
690
+ error !== undefined && error.status === status,
691
+ );
692
+ }
693
+
694
+ function catalogErrorResponseSchemaRef(
695
+ errors: CatalogErrorForOpenAPI[],
696
+ nameHint: string,
697
+ state: GeneratorState,
698
+ ): SchemaObject | ReferenceObject {
699
+ const schema =
700
+ errors.length === 1
701
+ ? catalogErrorResponseSchema(errors[0], nameHint, state)
702
+ : {
703
+ oneOf: errors.map((error) =>
704
+ catalogErrorResponseSchema(
705
+ error,
706
+ `${nameHint}_${error.key}`,
707
+ state,
708
+ ),
709
+ ),
710
+ };
711
+
712
+ if (!state.components.schemas) {
713
+ state.components.schemas = {};
714
+ }
715
+
716
+ const schemaName = normalizeSchemaName(nameHint, state);
717
+ if (!state.components.schemas[schemaName]) {
718
+ state.components.schemas[schemaName] = schema;
719
+ }
720
+
721
+ return { $ref: `#/components/schemas/${schemaName}` };
722
+ }
723
+
724
+ function catalogErrorResponseSchema(
725
+ error: CatalogErrorForOpenAPI,
726
+ nameHint: string,
727
+ state: GeneratorState,
728
+ ): SchemaObject {
729
+ return {
730
+ type: "object",
731
+ properties: {
732
+ code: { type: "string", const: error.code },
733
+ message: { type: "string" },
734
+ ...(error.details
735
+ ? {
736
+ details: schemaToSchemaRef(
737
+ error.details,
738
+ `${nameHint}_details`,
739
+ state,
740
+ "output",
741
+ ),
742
+ }
743
+ : { details: {} }),
744
+ requestId: { type: "string" },
745
+ },
746
+ required: ["code", "message"],
747
+ };
748
+ }
749
+
750
+ function examplesFromCatalogErrors(
751
+ errors: CatalogErrorForOpenAPI[],
752
+ ): Record<string, { summary?: string; value?: unknown }> | undefined {
753
+ if (errors.length === 0) return undefined;
754
+
755
+ return Object.fromEntries(
756
+ errors.map((error) => [
757
+ normalizeExampleKey(error.key),
758
+ {
759
+ summary: error.message,
760
+ value: {
761
+ code: error.code,
762
+ message: error.message,
763
+ },
764
+ },
765
+ ]),
766
+ );
767
+ }
768
+
769
+ function normalizeExampleKey(key: string): string {
770
+ const normalized = key.replace(/[^A-Za-z0-9._-]/g, "_");
771
+ return normalized || "error";
772
+ }
773
+
774
+ /**
775
+ * Convert a Zod schema to a JSON Schema reference, registering it in components
776
+ */
777
+ function zodToSchemaRef(
778
+ schema: ZodTypeAny,
779
+ nameHint: string,
780
+ state: GeneratorState,
781
+ io: SchemaIO,
782
+ ): SchemaObject | ReferenceObject {
783
+ // Ensure schemas object exists
784
+ if (!state.components.schemas) {
785
+ state.components.schemas = {};
786
+ }
787
+
788
+ const schemaName = normalizeSchemaName(nameHint, state);
789
+
790
+ if (!state.components.schemas[schemaName]) {
791
+ const jsonSchema = z.toJSONSchema(schema, {
792
+ target: "draft-2020-12",
793
+ unrepresentable: "any",
794
+ io,
795
+ }) as SchemaObject;
796
+
797
+ // Remove $schema as it's not needed in OpenAPI
798
+ delete jsonSchema.$schema;
799
+
800
+ state.components.schemas[schemaName] = jsonSchema;
801
+ }
802
+
803
+ return { $ref: `#/components/schemas/${schemaName}` };
804
+ }
805
+
806
+ function schemaToSchemaRef(
807
+ schema: unknown,
808
+ nameHint: string,
809
+ state: GeneratorState,
810
+ io: SchemaIO,
811
+ ): SchemaObject | ReferenceObject {
812
+ if (schema === STANDARD_ERROR_RESPONSE_SCHEMA) {
813
+ return standardErrorResponseSchemaRef(nameHint, state);
814
+ }
815
+
816
+ return zodToSchemaRef(schema as ZodTypeAny, nameHint, state, io);
817
+ }
818
+
819
+ function standardErrorResponseSchemaRef(
820
+ nameHint: string,
821
+ state: GeneratorState,
822
+ ): ReferenceObject {
823
+ if (!state.components.schemas) {
824
+ state.components.schemas = {};
825
+ }
826
+
827
+ const schemaName = normalizeSchemaName(nameHint, state);
828
+ if (!state.components.schemas[schemaName]) {
829
+ state.components.schemas[schemaName] = {
830
+ type: "object",
831
+ properties: {
832
+ code: { type: "string" },
833
+ message: { type: "string" },
834
+ details: {},
835
+ requestId: { type: "string" },
836
+ },
837
+ required: ["code", "message"],
838
+ };
839
+ }
840
+
841
+ return { $ref: `#/components/schemas/${schemaName}` };
842
+ }
843
+
844
+ /**
845
+ * Normalize schema name for use in components.
846
+ * Appends a counter suffix only when the base name already exists.
847
+ */
848
+ function normalizeSchemaName(name: string, state: GeneratorState): string {
849
+ const base = name.replace(/[^A-Za-z0-9]/g, "_");
850
+
851
+ // Check if base name is already used
852
+ if (!state.components.schemas?.[base]) {
853
+ return base;
854
+ }
855
+
856
+ // Find a unique name by appending a counter
857
+ let counter = 1;
858
+ let uniqueName = `${base}_${counter}`;
859
+ while (state.components.schemas[uniqueName]) {
860
+ counter++;
861
+ uniqueName = `${base}_${counter}`;
862
+ }
863
+
864
+ return uniqueName;
865
+ }