@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,1105 @@
1
+ import {
2
+ BEIGNET_ERROR_OWNER_HEADER,
3
+ type ContractLike,
4
+ getContractHeaderSchemas,
5
+ type HttpContractConfig,
6
+ methodSupportsRequestBody,
7
+ parsePathTemplate,
8
+ type ResolveContract,
9
+ resolveContract,
10
+ type StandardErrorResponseBody,
11
+ type StandardSchema,
12
+ type StandardSchemaV1,
13
+ } from "../contracts";
14
+ import { isErrorResponseBody, SchemaValidationError } from "../errors";
15
+ import type {
16
+ CallArgs,
17
+ ClientConfig,
18
+ EndpointCallArgs,
19
+ EndpointResult,
20
+ InferEndpointErrorResponse,
21
+ InferEndpointErrorResponseByStatus,
22
+ InferEndpointErrorStatus,
23
+ InferSuccessResponse,
24
+ } from "./types";
25
+
26
+ export type ContractErrorSource = "http" | "client" | "network" | "contract";
27
+
28
+ export type ContractErrorWithSource<
29
+ TError,
30
+ TSource extends ContractErrorSource,
31
+ > = Extract<TError, { readonly source: TSource }>;
32
+
33
+ export type ContractErrorWithStatus<TError, TStatus extends number> = Extract<
34
+ TError,
35
+ { readonly status: TStatus }
36
+ >;
37
+
38
+ export type ContractErrorWithCode<TError, TCode extends string> = Extract<
39
+ TError,
40
+ { readonly code: TCode }
41
+ >;
42
+
43
+ export type HttpContractError<
44
+ TBody = unknown,
45
+ TStatus extends number = number,
46
+ > = ContractError<TBody, TStatus, "http"> & {
47
+ readonly source: "http";
48
+ readonly status: TStatus;
49
+ readonly response: Response;
50
+ };
51
+
52
+ export type ClientContractError = ContractError<
53
+ undefined,
54
+ undefined,
55
+ "client"
56
+ > & {
57
+ readonly source: "client";
58
+ readonly status: undefined;
59
+ readonly response: undefined;
60
+ };
61
+
62
+ export type NetworkContractError = ContractError<
63
+ undefined,
64
+ undefined,
65
+ "network"
66
+ > & {
67
+ readonly source: "network";
68
+ readonly status: undefined;
69
+ readonly response: undefined;
70
+ };
71
+
72
+ export type ResponseContractError<
73
+ TBody = unknown,
74
+ TStatus extends number | undefined = number | undefined,
75
+ > = ContractError<TBody, TStatus, "contract"> & {
76
+ readonly source: "contract";
77
+ readonly status: TStatus;
78
+ };
79
+
80
+ export type AnyContractError =
81
+ | HttpContractError
82
+ | ClientContractError
83
+ | NetworkContractError
84
+ | ResponseContractError;
85
+
86
+ type EndpointCatalogErrorDefinition<TContract extends HttpContractConfig> =
87
+ TContract["metadata"] extends { errors: infer TErrors }
88
+ ? TErrors extends Record<
89
+ string,
90
+ { code: string; status: number; message: string }
91
+ >
92
+ ? TErrors[keyof TErrors]
93
+ : never
94
+ : never;
95
+
96
+ type InferErrorDefinitionDetails<TDef> = TDef extends {
97
+ details: StandardSchemaV1;
98
+ }
99
+ ? StandardSchemaV1.InferOutput<TDef["details"]>
100
+ : unknown;
101
+
102
+ type StandardErrorBodyForDefinition<TDef extends { code: string }> =
103
+ StandardErrorResponseBody & {
104
+ code: TDef["code"];
105
+ details?: InferErrorDefinitionDetails<TDef>;
106
+ };
107
+
108
+ type EndpointCatalogContractError<TContract extends HttpContractConfig> =
109
+ EndpointCatalogErrorDefinition<TContract> extends infer TDef
110
+ ? TDef extends { code: string; status: number }
111
+ ? HttpContractError<
112
+ StandardErrorBodyForDefinition<TDef>,
113
+ TDef["status"]
114
+ > & {
115
+ readonly code: TDef["code"];
116
+ readonly details?: InferErrorDefinitionDetails<TDef>;
117
+ }
118
+ : never
119
+ : never;
120
+
121
+ export type InferEndpointErrorCode<TContract extends HttpContractConfig> =
122
+ EndpointCatalogErrorDefinition<TContract>["code"];
123
+
124
+ export type InferEndpointContractError<TContract extends HttpContractConfig> =
125
+ | EndpointCatalogContractError<TContract>
126
+ | {
127
+ [TStatus in InferEndpointErrorStatus<TContract>]: HttpContractError<
128
+ InferEndpointErrorResponseByStatus<TContract, TStatus>,
129
+ TStatus
130
+ >;
131
+ }[InferEndpointErrorStatus<TContract>]
132
+ | HttpContractError<InferEndpointErrorResponse<TContract>, number>
133
+ | ClientContractError
134
+ | NetworkContractError
135
+ | ResponseContractError<
136
+ InferEndpointErrorResponse<TContract>,
137
+ number | undefined
138
+ >;
139
+
140
+ /**
141
+ * HTTP client error with contract metadata
142
+ */
143
+ export class ContractError<
144
+ TBody = unknown,
145
+ TStatus extends number | undefined = number | undefined,
146
+ TSource extends ContractErrorSource = ContractErrorSource,
147
+ > extends Error {
148
+ readonly source: TSource;
149
+ readonly status: TStatus;
150
+ readonly code?: string;
151
+ readonly body?: TBody;
152
+ readonly details?: unknown;
153
+ readonly response?: Response;
154
+ override cause?: unknown;
155
+
156
+ constructor(args: {
157
+ source: TSource;
158
+ status?: TStatus;
159
+ code?: string;
160
+ message: string;
161
+ body?: TBody;
162
+ details?: unknown;
163
+ response?: Response;
164
+ cause?: unknown;
165
+ }) {
166
+ super(args.message);
167
+ this.name = "ContractError";
168
+ this.source = args.source;
169
+ this.status = args.status as TStatus;
170
+ this.code = args.code;
171
+ this.body = args.body;
172
+ this.details = args.details;
173
+ this.response = args.response;
174
+ this.cause = args.cause;
175
+ }
176
+
177
+ /**
178
+ * Check if this error has a specific HTTP status code
179
+ */
180
+ hasStatus<S extends number>(
181
+ status: S,
182
+ ): this is this & { readonly status: S } {
183
+ return (this.status as number | undefined) === status;
184
+ }
185
+
186
+ /**
187
+ * Check if this error came from a specific source.
188
+ */
189
+ hasSource<S extends ContractErrorSource>(
190
+ source: S,
191
+ ): this is this & { readonly source: S } {
192
+ return (this.source as ContractErrorSource) === source;
193
+ }
194
+
195
+ /**
196
+ * Check if this error has a specific error code
197
+ */
198
+ hasCode<C extends string>(code: C): this is this & { code: C } {
199
+ return this.code === code;
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Type guard to check if an unknown error is a ContractError,
205
+ * optionally narrowing by HTTP status code.
206
+ *
207
+ * @example
208
+ * ```ts
209
+ * try { await endpoint.call(...) }
210
+ * catch (err) {
211
+ * if (isContractError(err, 404)) {
212
+ * // err.status is 404
213
+ * }
214
+ * if (isContractError(err)) {
215
+ * // err is ContractError
216
+ * }
217
+ * }
218
+ * ```
219
+ */
220
+ export function isContractError(err: unknown): err is AnyContractError;
221
+ export function isContractError<S extends number>(
222
+ err: unknown,
223
+ status: S,
224
+ ): err is HttpContractError<unknown, S>;
225
+ export function isContractError<
226
+ TError extends AnyContractError,
227
+ S extends number,
228
+ >(
229
+ err: TError,
230
+ criteria: { status: S },
231
+ ): err is ContractErrorWithStatus<TError, S>;
232
+ export function isContractError<
233
+ TError extends AnyContractError,
234
+ S extends ContractErrorSource,
235
+ >(
236
+ err: TError,
237
+ criteria: { source: S },
238
+ ): err is ContractErrorWithSource<TError, S>;
239
+ export function isContractError<
240
+ TError extends ContractError,
241
+ Status extends number,
242
+ Source extends ContractErrorSource,
243
+ >(
244
+ err: TError,
245
+ criteria: { status: Status; source: Source },
246
+ ): err is ContractErrorWithSource<
247
+ ContractErrorWithStatus<TError, Status>,
248
+ Source
249
+ >;
250
+ export function isContractError<
251
+ TError extends ContractError,
252
+ Code extends string,
253
+ >(
254
+ err: TError,
255
+ criteria: { code: Code },
256
+ ): err is ContractErrorWithCode<TError, Code>;
257
+ export function isContractError<
258
+ TError extends ContractError,
259
+ Status extends number,
260
+ Code extends string,
261
+ >(
262
+ err: TError,
263
+ criteria: { status: Status; code: Code },
264
+ ): err is ContractErrorWithCode<ContractErrorWithStatus<TError, Status>, Code>;
265
+ export function isContractError<
266
+ TError extends ContractError,
267
+ Source extends ContractErrorSource,
268
+ Code extends string,
269
+ >(
270
+ err: TError,
271
+ criteria: { source: Source; code: Code },
272
+ ): err is ContractErrorWithCode<ContractErrorWithSource<TError, Source>, Code>;
273
+ export function isContractError<
274
+ TError extends ContractError,
275
+ Status extends number,
276
+ Source extends ContractErrorSource,
277
+ Code extends string,
278
+ >(
279
+ err: TError,
280
+ criteria: { status: Status; source: Source; code: Code },
281
+ ): err is ContractErrorWithCode<
282
+ ContractErrorWithSource<ContractErrorWithStatus<TError, Status>, Source>,
283
+ Code
284
+ >;
285
+ export function isContractError<S extends number>(
286
+ err: unknown,
287
+ criteria: { status: S },
288
+ ): err is ContractError<unknown, S>;
289
+ export function isContractError<S extends ContractErrorSource>(
290
+ err: unknown,
291
+ criteria: { source: S },
292
+ ): err is ContractError<unknown, number | undefined, S>;
293
+ export function isContractError<
294
+ Status extends number,
295
+ Source extends ContractErrorSource,
296
+ >(
297
+ err: unknown,
298
+ criteria: { status: Status; source: Source },
299
+ ): err is ContractErrorWithSource<HttpContractError<unknown, Status>, Source>;
300
+ export function isContractError<Code extends string>(
301
+ err: unknown,
302
+ criteria: { code: Code },
303
+ ): err is ContractError<unknown, number | undefined> & { readonly code: Code };
304
+ export function isContractError(
305
+ err: unknown,
306
+ criteria?:
307
+ | number
308
+ | { code?: string; source?: ContractErrorSource; status?: number },
309
+ ): err is AnyContractError {
310
+ const status = typeof criteria === "number" ? criteria : criteria?.status;
311
+ const source = typeof criteria === "object" ? criteria.source : undefined;
312
+ const code = typeof criteria === "object" ? criteria.code : undefined;
313
+ return (
314
+ err instanceof ContractError &&
315
+ (status === undefined || err.status === status) &&
316
+ (source === undefined || err.source === source) &&
317
+ (code === undefined || err.code === code)
318
+ );
319
+ }
320
+
321
+ function formatQuotedList(values: string[]): string {
322
+ return values.map((value) => `"${value}"`).join(", ");
323
+ }
324
+
325
+ function createMissingPathParamsMessage(
326
+ path: string,
327
+ missing: string[],
328
+ provided: string[],
329
+ ): string {
330
+ const label = missing.length === 1 ? "parameter" : "parameters";
331
+ const providedSuffix = provided.length
332
+ ? ` (provided: ${provided.join(", ")})`
333
+ : "";
334
+
335
+ return `Missing required path ${label} ${formatQuotedList(missing)} for path "${path}"${providedSuffix}`;
336
+ }
337
+
338
+ /**
339
+ * Validate data using a Standard Schema validator
340
+ * Throws SchemaValidationError if validation fails
341
+ */
342
+ async function validateSchema<T>(
343
+ schema: StandardSchemaV1<unknown, T>,
344
+ data: unknown,
345
+ ): Promise<T> {
346
+ const result = await schema["~standard"].validate(data);
347
+ if (result.issues?.length) {
348
+ throw new SchemaValidationError(result.issues);
349
+ }
350
+ if ("value" in result) {
351
+ return result.value;
352
+ }
353
+ throw new Error("Invalid Standard Schema result: missing value");
354
+ }
355
+
356
+ function normalizeHeaderRecord(
357
+ headers: Record<string, string | undefined>,
358
+ ): Record<string, string> {
359
+ const normalized: Record<string, string> = {};
360
+ for (const [key, value] of Object.entries(headers)) {
361
+ if (value !== undefined) {
362
+ normalized[key.toLowerCase()] = value;
363
+ }
364
+ }
365
+ return normalized;
366
+ }
367
+
368
+ function serializeParsedHeaders(parsed: unknown): Record<string, string> {
369
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
370
+ return {};
371
+ }
372
+
373
+ const headers: Record<string, string> = {};
374
+ for (const [key, value] of Object.entries(parsed)) {
375
+ if (value !== undefined && value !== null) {
376
+ headers[key.toLowerCase()] = String(value);
377
+ }
378
+ }
379
+ return headers;
380
+ }
381
+
382
+ async function validateHeaderSchemas(
383
+ schemas: readonly StandardSchema[],
384
+ headers: Record<string, string>,
385
+ ): Promise<Record<string, string>> {
386
+ let validatedHeaders = headers;
387
+
388
+ for (const schema of schemas) {
389
+ const parsed = await validateSchema(schema, headers);
390
+ validatedHeaders = {
391
+ ...validatedHeaders,
392
+ ...serializeParsedHeaders(parsed),
393
+ };
394
+ }
395
+
396
+ return validatedHeaders;
397
+ }
398
+
399
+ type PrimitiveParam = string | number | boolean;
400
+ type QueryParamValue =
401
+ | PrimitiveParam
402
+ | null
403
+ | undefined
404
+ | Array<PrimitiveParam>;
405
+
406
+ type PathParams = Record<string, PrimitiveParam>;
407
+ type QueryParams = Record<string, QueryParamValue>;
408
+
409
+ /**
410
+ * Endpoint wrapper for a specific contract
411
+ */
412
+ export class Endpoint<
413
+ TContract extends HttpContractConfig,
414
+ TProvidedHeaders extends string = never,
415
+ > {
416
+ constructor(
417
+ private contract: TContract,
418
+ private config: ClientConfig<TProvidedHeaders>,
419
+ ) {}
420
+
421
+ isError(err: unknown): err is InferEndpointContractError<TContract>;
422
+ isError<S extends InferEndpointErrorStatus<TContract>>(
423
+ err: unknown,
424
+ status: S,
425
+ ): err is ContractErrorWithStatus<InferEndpointContractError<TContract>, S>;
426
+ isError<S extends number>(
427
+ err: unknown,
428
+ status: S,
429
+ ): err is HttpContractError<unknown, S>;
430
+ isError<S extends InferEndpointErrorStatus<TContract>>(
431
+ err: unknown,
432
+ criteria: { status: S },
433
+ ): err is ContractErrorWithStatus<InferEndpointContractError<TContract>, S>;
434
+ isError<S extends ContractErrorSource>(
435
+ err: unknown,
436
+ criteria: { source: S },
437
+ ): err is ContractErrorWithSource<InferEndpointContractError<TContract>, S>;
438
+ isError<C extends InferEndpointErrorCode<TContract>>(
439
+ err: unknown,
440
+ criteria: { code: C },
441
+ ): err is ContractErrorWithCode<InferEndpointContractError<TContract>, C>;
442
+ isError<C extends string>(
443
+ err: unknown,
444
+ criteria: { code: C },
445
+ ): err is InferEndpointContractError<TContract> & { readonly code: C };
446
+ isError<
447
+ Status extends InferEndpointErrorStatus<TContract>,
448
+ Source extends ContractErrorSource,
449
+ >(
450
+ err: unknown,
451
+ criteria: { status: Status; source: Source },
452
+ ): err is ContractErrorWithSource<
453
+ ContractErrorWithStatus<InferEndpointContractError<TContract>, Status>,
454
+ Source
455
+ >;
456
+ isError<
457
+ Status extends InferEndpointErrorStatus<TContract>,
458
+ C extends InferEndpointErrorCode<TContract>,
459
+ >(
460
+ err: unknown,
461
+ criteria: { status: Status; code: C },
462
+ ): err is ContractErrorWithCode<
463
+ ContractErrorWithStatus<InferEndpointContractError<TContract>, Status>,
464
+ C
465
+ >;
466
+ isError<
467
+ Source extends ContractErrorSource,
468
+ C extends InferEndpointErrorCode<TContract>,
469
+ >(
470
+ err: unknown,
471
+ criteria: { source: Source; code: C },
472
+ ): err is ContractErrorWithCode<
473
+ ContractErrorWithSource<InferEndpointContractError<TContract>, Source>,
474
+ C
475
+ >;
476
+ isError<
477
+ Status extends InferEndpointErrorStatus<TContract>,
478
+ Source extends ContractErrorSource,
479
+ C extends InferEndpointErrorCode<TContract>,
480
+ >(
481
+ err: unknown,
482
+ criteria: { status: Status; source: Source; code: C },
483
+ ): err is ContractErrorWithCode<
484
+ ContractErrorWithSource<
485
+ ContractErrorWithStatus<InferEndpointContractError<TContract>, Status>,
486
+ Source
487
+ >,
488
+ C
489
+ >;
490
+ isError(
491
+ err: unknown,
492
+ criteria?:
493
+ | number
494
+ | { code?: string; source?: ContractErrorSource; status?: number },
495
+ ): err is InferEndpointContractError<TContract> {
496
+ return isContractError(err, criteria as never);
497
+ }
498
+
499
+ /**
500
+ * Call the endpoint with the given arguments
501
+ */
502
+ async call(
503
+ ...callArgs: CallArgs<TContract, TProvidedHeaders>
504
+ ): Promise<InferSuccessResponse<TContract>> {
505
+ const result = await this.safeCall(...callArgs);
506
+ if (!result.ok) {
507
+ throw result.error;
508
+ }
509
+ return result.data;
510
+ }
511
+
512
+ /**
513
+ * Call the endpoint and return a typed result instead of throwing ContractError.
514
+ */
515
+ async safeCall(
516
+ ...callArgs: CallArgs<TContract, TProvidedHeaders>
517
+ ): Promise<EndpointResult<TContract, InferEndpointContractError<TContract>>> {
518
+ const args = (callArgs[0] ?? {}) as EndpointCallArgs<
519
+ TContract,
520
+ TProvidedHeaders
521
+ >;
522
+
523
+ try {
524
+ if (args.body !== undefined && args.rawBody !== undefined) {
525
+ throw this.createError(
526
+ undefined,
527
+ "INVALID_REQUEST_BODY",
528
+ "Pass either body or rawBody, not both.",
529
+ );
530
+ }
531
+
532
+ const methodSupportsBody = methodSupportsRequestBody(
533
+ this.contract.method,
534
+ );
535
+ if (
536
+ (args.body !== undefined || args.rawBody !== undefined) &&
537
+ !methodSupportsBody
538
+ ) {
539
+ throw this.createError(
540
+ undefined,
541
+ "INVALID_REQUEST_BODY",
542
+ `Request bodies are not supported for ${this.contract.method} contracts. Use POST, PUT, or PATCH for contract request bodies.`,
543
+ );
544
+ }
545
+
546
+ let requestBody: BodyInit | undefined;
547
+ let requestBodyType: "json" | "raw" | undefined;
548
+ if (args.rawBody !== undefined && methodSupportsBody) {
549
+ requestBody = args.rawBody;
550
+ requestBodyType = "raw";
551
+ }
552
+
553
+ if (args.body !== undefined && methodSupportsBody) {
554
+ let bodyToSend = args.body;
555
+ if (this.config.validate && this.contract.body) {
556
+ try {
557
+ bodyToSend = (await validateSchema(
558
+ this.contract.body,
559
+ args.body,
560
+ )) as typeof bodyToSend;
561
+ } catch (err) {
562
+ if (err instanceof SchemaValidationError) {
563
+ throw this.createError(
564
+ 422,
565
+ "VALIDATION_ERROR",
566
+ "Body validation failed",
567
+ err.issues,
568
+ );
569
+ }
570
+ throw err;
571
+ }
572
+ }
573
+ requestBody = JSON.stringify(bodyToSend);
574
+ requestBodyType = "json";
575
+ }
576
+
577
+ const url = await this.buildUrl(
578
+ args.path as PathParams | undefined,
579
+ args.query as QueryParams | undefined,
580
+ );
581
+ let headers = await this.buildHeaders(
582
+ args.headers as Record<string, string> | undefined,
583
+ requestBodyType === "json",
584
+ );
585
+ if (this.config.validate) {
586
+ const headerSchemas = getContractHeaderSchemas(this.contract.headers);
587
+ if (headerSchemas.length > 0) {
588
+ try {
589
+ headers = await validateHeaderSchemas(headerSchemas, headers);
590
+ } catch (err) {
591
+ if (err instanceof SchemaValidationError) {
592
+ throw this.createError(
593
+ 422,
594
+ "VALIDATION_ERROR",
595
+ "Headers validation failed",
596
+ err.issues,
597
+ );
598
+ }
599
+ throw err;
600
+ }
601
+ }
602
+ }
603
+ const fetchFn = this.config.fetch || fetch;
604
+
605
+ const options: RequestInit = {
606
+ method: this.contract.method,
607
+ headers,
608
+ signal: args.signal,
609
+ };
610
+
611
+ if (requestBody !== undefined) {
612
+ options.body = requestBody;
613
+ }
614
+
615
+ const response = await fetchFn(url, options);
616
+
617
+ // Handle non-2xx responses
618
+ if (!response.ok) {
619
+ let errorBody: unknown;
620
+ try {
621
+ errorBody = await parseResponseBody(response);
622
+ } catch (parseErr) {
623
+ // JSON parse failed — still report the HTTP error
624
+ throw this.createError(
625
+ response.status,
626
+ "INVALID_JSON",
627
+ createInvalidJsonMessage("error", response.status),
628
+ undefined,
629
+ parseErr,
630
+ undefined,
631
+ response,
632
+ "contract",
633
+ );
634
+ }
635
+
636
+ const validatedError = await validateErrorBodyOrStandardEnvelope(
637
+ this.contract,
638
+ response,
639
+ errorBody,
640
+ );
641
+
642
+ const errorPayload = getErrorPayload(validatedError);
643
+ throw this.createError(
644
+ response.status,
645
+ errorPayload.code || "HTTP_ERROR",
646
+ errorPayload.message || response.statusText,
647
+ errorPayload.details,
648
+ undefined,
649
+ validatedError,
650
+ response,
651
+ "http",
652
+ );
653
+ }
654
+
655
+ // Parse response
656
+ let data: unknown;
657
+ try {
658
+ data = await parseResponseBody(response);
659
+ } catch (parseErr) {
660
+ throw this.createError(
661
+ response.status,
662
+ "INVALID_JSON",
663
+ createInvalidJsonMessage("success", response.status),
664
+ undefined,
665
+ parseErr,
666
+ undefined,
667
+ response,
668
+ "contract",
669
+ );
670
+ }
671
+
672
+ // Validate response if schema exists for this status
673
+ const statusKey = String(response.status);
674
+ const hasSchema = statusKey in this.contract.responses;
675
+ const hasDeclaredResponses =
676
+ Object.keys(this.contract.responses).length > 0;
677
+ const responseSchema = this.contract.responses[response.status];
678
+ if (hasSchema && responseSchema === null) {
679
+ if (data !== undefined && data !== null) {
680
+ throw this.createError(
681
+ response.status,
682
+ "RESPONSE_VALIDATION_ERROR",
683
+ `Response validation failed for ${this.contract.method} ${this.contract.path} (status ${response.status}, contract: ${this.contract.name})`,
684
+ [
685
+ {
686
+ message:
687
+ "Response body must be empty for a null response schema.",
688
+ },
689
+ ],
690
+ undefined,
691
+ data,
692
+ response,
693
+ "contract",
694
+ );
695
+ }
696
+ } else if (hasSchema && responseSchema) {
697
+ try {
698
+ data = await validateSchema(responseSchema, data);
699
+ } catch (err) {
700
+ if (err instanceof SchemaValidationError) {
701
+ throw this.createError(
702
+ response.status,
703
+ "RESPONSE_VALIDATION_ERROR",
704
+ `Response validation failed for ${this.contract.method} ${this.contract.path} (status ${response.status}, contract: ${this.contract.name})`,
705
+ err.issues,
706
+ err,
707
+ data,
708
+ response,
709
+ "contract",
710
+ );
711
+ }
712
+ throw err;
713
+ }
714
+ } else if (!hasSchema && hasDeclaredResponses) {
715
+ throw this.createError(
716
+ response.status,
717
+ "UNDECLARED_RESPONSE_STATUS",
718
+ `Server returned undeclared status ${response.status} for ${this.contract.method} ${this.contract.path} (contract: ${this.contract.name})`,
719
+ undefined,
720
+ undefined,
721
+ data,
722
+ response,
723
+ "contract",
724
+ );
725
+ }
726
+
727
+ return {
728
+ ok: true,
729
+ status: response.status,
730
+ data: data as InferSuccessResponse<TContract>,
731
+ response,
732
+ };
733
+ } catch (err: unknown) {
734
+ if (err instanceof ContractError) {
735
+ const error = err as InferEndpointContractError<TContract>;
736
+ return {
737
+ ok: false,
738
+ status: error.status,
739
+ error,
740
+ response: error.response,
741
+ };
742
+ }
743
+ const error = this.createError(
744
+ undefined,
745
+ "NETWORK_ERROR",
746
+ err instanceof Error ? err.message : "Network request failed",
747
+ undefined,
748
+ err,
749
+ undefined,
750
+ undefined,
751
+ "network",
752
+ ) as InferEndpointContractError<TContract>;
753
+ return {
754
+ ok: false,
755
+ status: error.status,
756
+ error,
757
+ response: error.response,
758
+ };
759
+ }
760
+ }
761
+
762
+ /**
763
+ * Build the full URL with path and query parameters
764
+ */
765
+ private async buildUrl(
766
+ path?: PathParams,
767
+ query?: QueryParams,
768
+ ): Promise<string> {
769
+ let parsedPath: ReturnType<typeof parsePathTemplate>;
770
+ try {
771
+ parsedPath = parsePathTemplate(this.contract.path);
772
+ } catch (cause) {
773
+ throw new ContractError({
774
+ source: "client",
775
+ code: "INVALID_PATH_TEMPLATE",
776
+ message: cause instanceof Error ? cause.message : String(cause),
777
+ cause,
778
+ });
779
+ }
780
+ let pathToSerialize = path;
781
+
782
+ // Replace path parameters
783
+ if (path && parsedPath.keys.length > 0) {
784
+ // Validate path params if schema exists and validation is enabled
785
+ if (this.config.validate && this.contract.pathParams) {
786
+ try {
787
+ pathToSerialize = (await validateSchema(
788
+ this.contract.pathParams,
789
+ path,
790
+ )) as PathParams;
791
+ } catch (err) {
792
+ if (err instanceof SchemaValidationError) {
793
+ throw this.createError(
794
+ 422,
795
+ "VALIDATION_ERROR",
796
+ "Path params validation failed",
797
+ err.issues,
798
+ );
799
+ }
800
+ throw err;
801
+ }
802
+ }
803
+ }
804
+
805
+ const normalizedPath = pathToSerialize ?? path;
806
+ const missingPathParams = [
807
+ ...new Set(
808
+ parsedPath.keys.filter((key) => normalizedPath?.[key] === undefined),
809
+ ),
810
+ ];
811
+ if (missingPathParams.length) {
812
+ const provided = path ? Object.keys(path) : [];
813
+ throw new ContractError({
814
+ source: "client",
815
+ code: "MISSING_PATH_PARAMS",
816
+ message: createMissingPathParamsMessage(
817
+ this.contract.path,
818
+ missingPathParams,
819
+ provided,
820
+ ),
821
+ });
822
+ }
823
+
824
+ let url = `/${parsedPath.segments
825
+ .map((segment) =>
826
+ segment.kind === "static"
827
+ ? segment.value
828
+ : encodeURIComponent(String(normalizedPath?.[segment.name])),
829
+ )
830
+ .join("/")}`;
831
+
832
+ // Add query parameters
833
+ let queryToSerialize = query;
834
+ if (query) {
835
+ // Validate query params if schema exists and validation is enabled
836
+ if (this.config.validate && this.contract.query) {
837
+ try {
838
+ queryToSerialize = (await validateSchema(
839
+ this.contract.query,
840
+ query,
841
+ )) as QueryParams;
842
+ } catch (err) {
843
+ if (err instanceof SchemaValidationError) {
844
+ throw this.createError(
845
+ 422,
846
+ "VALIDATION_ERROR",
847
+ "Query params validation failed",
848
+ err.issues,
849
+ );
850
+ }
851
+ throw err;
852
+ }
853
+ }
854
+
855
+ const params = new URLSearchParams();
856
+ const normalizedQuery = queryToSerialize ?? query;
857
+ for (const [key, value] of Object.entries(normalizedQuery)) {
858
+ if (value !== undefined && value !== null) {
859
+ if (typeof value === "object" && !Array.isArray(value)) {
860
+ throw new ContractError({
861
+ source: "client",
862
+ code: "INVALID_QUERY_PARAM",
863
+ message: `Query parameter "${key}" contains a non-serializable object. Use primitive values or arrays of primitives.`,
864
+ });
865
+ }
866
+ if (Array.isArray(value)) {
867
+ for (const v of value) {
868
+ if (typeof v === "object" && v !== null) {
869
+ throw new ContractError({
870
+ source: "client",
871
+ code: "INVALID_QUERY_PARAM",
872
+ message: `Query parameter "${key}" contains a non-serializable array element. Use primitive values only.`,
873
+ });
874
+ }
875
+ params.append(key, String(v));
876
+ }
877
+ } else {
878
+ params.append(key, String(value));
879
+ }
880
+ }
881
+ }
882
+ const queryString = params.toString();
883
+ if (queryString) {
884
+ url += `?${queryString}`;
885
+ }
886
+ }
887
+
888
+ // Prepend base URL, normalizing trailing/leading slashes
889
+ const baseUrl = this.config.baseUrl || "";
890
+ if (baseUrl && url.startsWith("/") && baseUrl.endsWith("/")) {
891
+ return baseUrl + url.slice(1);
892
+ }
893
+ return baseUrl + url;
894
+ }
895
+
896
+ /**
897
+ * Build request headers
898
+ */
899
+ private async buildHeaders(
900
+ customHeaders?: Record<string, string>,
901
+ hasJsonBody = false,
902
+ ): Promise<Record<string, string>> {
903
+ const configHeaders =
904
+ typeof this.config.headers === "function"
905
+ ? await this.config.headers()
906
+ : this.config.headers || {};
907
+
908
+ const headers = normalizeHeaderRecord({
909
+ ...configHeaders,
910
+ ...customHeaders,
911
+ });
912
+
913
+ // Only set Content-Type for methods that can have a body
914
+ if (hasJsonBody && methodSupportsRequestBody(this.contract.method)) {
915
+ const hasContentType = Object.keys(headers).some(
916
+ (k) => k.toLowerCase() === "content-type",
917
+ );
918
+ if (!hasContentType) {
919
+ headers["content-type"] = "application/json";
920
+ }
921
+ }
922
+
923
+ return headers;
924
+ }
925
+
926
+ /**
927
+ * Create a contract error
928
+ */
929
+ private createError(
930
+ status: number | undefined,
931
+ code: string,
932
+ message: string,
933
+ details?: unknown,
934
+ cause?: unknown,
935
+ body?: unknown,
936
+ response?: Response,
937
+ source: ContractErrorSource = "client",
938
+ ): AnyContractError {
939
+ return new ContractError({
940
+ source,
941
+ status: source === "client" || source === "network" ? undefined : status,
942
+ code,
943
+ message,
944
+ body: source === "client" || source === "network" ? undefined : body,
945
+ details,
946
+ response:
947
+ source === "client" || source === "network" ? undefined : response,
948
+ cause,
949
+ }) as AnyContractError;
950
+ }
951
+ }
952
+
953
+ /**
954
+ * Client for making contract-based requests
955
+ */
956
+ export class Client<TProvidedHeaders extends string = never> {
957
+ constructor(private config: ClientConfig<TProvidedHeaders>) {}
958
+
959
+ /**
960
+ * Create an endpoint wrapper for a contract
961
+ *
962
+ * Accepts either an HttpContractConfig object or a ContractBuilder
963
+ * (which has a .config property that returns the config)
964
+ */
965
+ endpoint<TContractLike extends ContractLike>(
966
+ contract: TContractLike,
967
+ ): Endpoint<ResolveContract<TContractLike>, TProvidedHeaders> {
968
+ const resolved = resolveContract(contract);
969
+ return new Endpoint(resolved, this.config);
970
+ }
971
+ }
972
+
973
+ /**
974
+ * Create a configured client
975
+ */
976
+ export function createClient<const TProvidedHeaders extends string = never>(
977
+ config: ClientConfig<TProvidedHeaders> = {},
978
+ ): Client<TProvidedHeaders> {
979
+ return new Client(config);
980
+ }
981
+
982
+ async function parseResponseBody(response: Response): Promise<unknown> {
983
+ if (response.status === 204 || response.status === 205) {
984
+ return undefined;
985
+ }
986
+
987
+ const text = await response.text().catch(() => "");
988
+ if (text === "") {
989
+ return undefined;
990
+ }
991
+
992
+ const contentType = response.headers.get("content-type");
993
+ if (contentType?.includes("application/json")) {
994
+ return JSON.parse(text);
995
+ }
996
+
997
+ return text;
998
+ }
999
+
1000
+ function createInvalidJsonMessage(
1001
+ context: "success" | "error",
1002
+ status: number,
1003
+ ): string {
1004
+ return `Failed to parse JSON ${context} response (status ${status})`;
1005
+ }
1006
+
1007
+ function createErrorResponseValidationMessage(
1008
+ contract: HttpContractConfig,
1009
+ status: number,
1010
+ ): string {
1011
+ return `Error response validation failed for ${contract.method} ${contract.path} (status ${status}, contract: ${contract.name})`;
1012
+ }
1013
+
1014
+ function hasFrameworkErrorOwner(response: Response): boolean {
1015
+ return response.headers.get(BEIGNET_ERROR_OWNER_HEADER) === "framework";
1016
+ }
1017
+
1018
+ async function validateErrorBodyOrStandardEnvelope(
1019
+ contract: HttpContractConfig,
1020
+ response: Response,
1021
+ errorBody: unknown,
1022
+ ): Promise<unknown> {
1023
+ const errorSchema = contract.responses[response.status];
1024
+ const hasDeclaredResponses = Object.keys(contract.responses).length > 0;
1025
+
1026
+ if (errorSchema === null) {
1027
+ if (errorBody === undefined || errorBody === null) {
1028
+ return undefined;
1029
+ }
1030
+ if (isErrorResponseBody(errorBody) && hasFrameworkErrorOwner(response)) {
1031
+ return errorBody;
1032
+ }
1033
+
1034
+ throw new ContractError({
1035
+ source: "contract",
1036
+ status: response.status,
1037
+ code: "ERROR_RESPONSE_VALIDATION_ERROR",
1038
+ message: createErrorResponseValidationMessage(contract, response.status),
1039
+ details: [
1040
+ {
1041
+ message: "Response body must be empty for a null response schema.",
1042
+ },
1043
+ ],
1044
+ body: errorBody,
1045
+ response,
1046
+ });
1047
+ }
1048
+
1049
+ if (!errorSchema) {
1050
+ if (isErrorResponseBody(errorBody) && hasFrameworkErrorOwner(response)) {
1051
+ return errorBody;
1052
+ }
1053
+ if (hasDeclaredResponses) {
1054
+ throw new ContractError({
1055
+ source: "contract",
1056
+ status: response.status,
1057
+ code: "UNDECLARED_ERROR_STATUS",
1058
+ message: `Server returned undeclared error status ${response.status} for ${contract.method} ${contract.path} (contract: ${contract.name})`,
1059
+ body: errorBody,
1060
+ response,
1061
+ });
1062
+ }
1063
+ return errorBody;
1064
+ }
1065
+
1066
+ try {
1067
+ return await validateSchema(errorSchema, errorBody);
1068
+ } catch (err) {
1069
+ if (!(err instanceof SchemaValidationError)) {
1070
+ throw err;
1071
+ }
1072
+
1073
+ if (isErrorResponseBody(errorBody) && hasFrameworkErrorOwner(response)) {
1074
+ return errorBody;
1075
+ }
1076
+
1077
+ throw new ContractError({
1078
+ source: "contract",
1079
+ status: response.status,
1080
+ code: "ERROR_RESPONSE_VALIDATION_ERROR",
1081
+ message: createErrorResponseValidationMessage(contract, response.status),
1082
+ details: err.issues,
1083
+ cause: err,
1084
+ body: errorBody,
1085
+ response,
1086
+ });
1087
+ }
1088
+ }
1089
+
1090
+ function getErrorPayload(body: unknown): {
1091
+ code?: string;
1092
+ message?: string;
1093
+ details?: unknown;
1094
+ } {
1095
+ if (typeof body !== "object" || body === null) {
1096
+ return {};
1097
+ }
1098
+
1099
+ const payload = body as Record<string, unknown>;
1100
+ return {
1101
+ code: typeof payload.code === "string" ? payload.code : undefined,
1102
+ message: typeof payload.message === "string" ? payload.message : undefined,
1103
+ details: payload.details,
1104
+ };
1105
+ }