@aws-blocks/core 0.1.0

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 (394) hide show
  1. package/LICENSE +174 -0
  2. package/dist/api.d.ts +147 -0
  3. package/dist/api.d.ts.map +1 -0
  4. package/dist/api.js +10 -0
  5. package/dist/api.test.d.ts +2 -0
  6. package/dist/api.test.d.ts.map +1 -0
  7. package/dist/api.test.js +19 -0
  8. package/dist/bb-utils.d.ts +11 -0
  9. package/dist/bb-utils.d.ts.map +1 -0
  10. package/dist/bb-utils.js +12 -0
  11. package/dist/builtin-routes.d.ts +6 -0
  12. package/dist/builtin-routes.d.ts.map +1 -0
  13. package/dist/builtin-routes.js +57 -0
  14. package/dist/cdk/__fixtures__/factory-backend.d.ts +3 -0
  15. package/dist/cdk/__fixtures__/factory-backend.d.ts.map +1 -0
  16. package/dist/cdk/__fixtures__/factory-backend.js +6 -0
  17. package/dist/cdk/__fixtures__/fullid-construct-backend.d.ts +2 -0
  18. package/dist/cdk/__fixtures__/fullid-construct-backend.d.ts.map +1 -0
  19. package/dist/cdk/__fixtures__/fullid-construct-backend.js +37 -0
  20. package/dist/cdk/__fixtures__/handler.d.ts +5 -0
  21. package/dist/cdk/__fixtures__/handler.d.ts.map +1 -0
  22. package/dist/cdk/__fixtures__/handler.js +3 -0
  23. package/dist/cdk/__fixtures__/side-effect-backend.d.ts +2 -0
  24. package/dist/cdk/__fixtures__/side-effect-backend.d.ts.map +1 -0
  25. package/dist/cdk/__fixtures__/side-effect-backend.js +5 -0
  26. package/dist/cdk/blocks-backend.d.ts +68 -0
  27. package/dist/cdk/blocks-backend.d.ts.map +1 -0
  28. package/dist/cdk/blocks-backend.js +214 -0
  29. package/dist/cdk/blocks-backend.test.d.ts +2 -0
  30. package/dist/cdk/blocks-backend.test.d.ts.map +1 -0
  31. package/dist/cdk/blocks-backend.test.js +151 -0
  32. package/dist/cdk/blocks-stack.test.d.ts +2 -0
  33. package/dist/cdk/blocks-stack.test.d.ts.map +1 -0
  34. package/dist/cdk/blocks-stack.test.js +78 -0
  35. package/dist/cdk/config-registry.d.ts +26 -0
  36. package/dist/cdk/config-registry.d.ts.map +1 -0
  37. package/dist/cdk/config-registry.js +80 -0
  38. package/dist/cdk/index.d.ts +35 -0
  39. package/dist/cdk/index.d.ts.map +1 -0
  40. package/dist/cdk/index.js +90 -0
  41. package/dist/cdk/mixins.d.ts +28 -0
  42. package/dist/cdk/mixins.d.ts.map +1 -0
  43. package/dist/cdk/mixins.js +37 -0
  44. package/dist/cdk/mixins.test.d.ts +2 -0
  45. package/dist/cdk/mixins.test.d.ts.map +1 -0
  46. package/dist/cdk/mixins.test.js +108 -0
  47. package/dist/cdk/node-version.d.ts +14 -0
  48. package/dist/cdk/node-version.d.ts.map +1 -0
  49. package/dist/cdk/node-version.js +15 -0
  50. package/dist/cdk/stack-metadata.d.ts +12 -0
  51. package/dist/cdk/stack-metadata.d.ts.map +1 -0
  52. package/dist/cdk/stack-metadata.js +22 -0
  53. package/dist/cdk/stack-metadata.test.d.ts +2 -0
  54. package/dist/cdk/stack-metadata.test.d.ts.map +1 -0
  55. package/dist/cdk/stack-metadata.test.js +56 -0
  56. package/dist/cdk/synth-guard.d.ts +14 -0
  57. package/dist/cdk/synth-guard.d.ts.map +1 -0
  58. package/dist/cdk/synth-guard.js +20 -0
  59. package/dist/cdk/synth-guard.test.d.ts +2 -0
  60. package/dist/cdk/synth-guard.test.d.ts.map +1 -0
  61. package/dist/cdk/synth-guard.test.js +15 -0
  62. package/dist/client/index.d.ts +97 -0
  63. package/dist/client/index.d.ts.map +1 -0
  64. package/dist/client/index.js +238 -0
  65. package/dist/client/index.test.d.ts +2 -0
  66. package/dist/client/index.test.d.ts.map +1 -0
  67. package/dist/client/index.test.js +113 -0
  68. package/dist/common/config.d.ts +45 -0
  69. package/dist/common/config.d.ts.map +1 -0
  70. package/dist/common/config.js +154 -0
  71. package/dist/common/config.test.d.ts +2 -0
  72. package/dist/common/config.test.d.ts.map +1 -0
  73. package/dist/common/config.test.js +173 -0
  74. package/dist/common/constants.d.ts +11 -0
  75. package/dist/common/constants.d.ts.map +1 -0
  76. package/dist/common/constants.js +12 -0
  77. package/dist/common/crypto.d.ts +6 -0
  78. package/dist/common/crypto.d.ts.map +1 -0
  79. package/dist/common/crypto.js +14 -0
  80. package/dist/common/index.d.ts +220 -0
  81. package/dist/common/index.d.ts.map +1 -0
  82. package/dist/common/index.js +180 -0
  83. package/dist/common/index.test.d.ts +2 -0
  84. package/dist/common/index.test.d.ts.map +1 -0
  85. package/dist/common/index.test.js +195 -0
  86. package/dist/common/mock-data.d.ts +40 -0
  87. package/dist/common/mock-data.d.ts.map +1 -0
  88. package/dist/common/mock-data.js +39 -0
  89. package/dist/common/official-bb-names.generated.d.ts +7 -0
  90. package/dist/common/official-bb-names.generated.d.ts.map +1 -0
  91. package/dist/common/official-bb-names.generated.js +28 -0
  92. package/dist/common/sdk-registry.d.ts +136 -0
  93. package/dist/common/sdk-registry.d.ts.map +1 -0
  94. package/dist/common/sdk-registry.js +125 -0
  95. package/dist/constants.d.ts +27 -0
  96. package/dist/constants.d.ts.map +1 -0
  97. package/dist/constants.js +28 -0
  98. package/dist/cors.d.ts +39 -0
  99. package/dist/cors.d.ts.map +1 -0
  100. package/dist/cors.js +85 -0
  101. package/dist/cors.test.d.ts +2 -0
  102. package/dist/cors.test.d.ts.map +1 -0
  103. package/dist/cors.test.js +241 -0
  104. package/dist/db-naming.d.ts +21 -0
  105. package/dist/db-naming.d.ts.map +1 -0
  106. package/dist/db-naming.js +38 -0
  107. package/dist/db-naming.test.d.ts +2 -0
  108. package/dist/db-naming.test.d.ts.map +1 -0
  109. package/dist/db-naming.test.js +29 -0
  110. package/dist/errors.d.ts +67 -0
  111. package/dist/errors.d.ts.map +1 -0
  112. package/dist/errors.js +69 -0
  113. package/dist/hosting.d.ts +258 -0
  114. package/dist/hosting.d.ts.map +1 -0
  115. package/dist/hosting.js +340 -0
  116. package/dist/hosting.test.d.ts +2 -0
  117. package/dist/hosting.test.d.ts.map +1 -0
  118. package/dist/hosting.test.js +1077 -0
  119. package/dist/index.cdk.d.ts +13 -0
  120. package/dist/index.cdk.d.ts.map +1 -0
  121. package/dist/index.cdk.js +13 -0
  122. package/dist/index.d.ts +9 -0
  123. package/dist/index.d.ts.map +1 -0
  124. package/dist/index.js +10 -0
  125. package/dist/lambda-handler.d.ts +131 -0
  126. package/dist/lambda-handler.d.ts.map +1 -0
  127. package/dist/lambda-handler.js +610 -0
  128. package/dist/lambda-handler.test.d.ts +2 -0
  129. package/dist/lambda-handler.test.d.ts.map +1 -0
  130. package/dist/lambda-handler.test.js +835 -0
  131. package/dist/pipeline/index.d.ts +4 -0
  132. package/dist/pipeline/index.d.ts.map +1 -0
  133. package/dist/pipeline/index.js +3 -0
  134. package/dist/pipeline/pipeline-construct.d.ts +130 -0
  135. package/dist/pipeline/pipeline-construct.d.ts.map +1 -0
  136. package/dist/pipeline/pipeline-construct.js +484 -0
  137. package/dist/pipeline/pipeline.test.d.ts +2 -0
  138. package/dist/pipeline/pipeline.test.d.ts.map +1 -0
  139. package/dist/pipeline/pipeline.test.js +1231 -0
  140. package/dist/pipeline/types.d.ts +351 -0
  141. package/dist/pipeline/types.d.ts.map +1 -0
  142. package/dist/pipeline/types.js +3 -0
  143. package/dist/raw-route.cdk.d.ts +47 -0
  144. package/dist/raw-route.cdk.d.ts.map +1 -0
  145. package/dist/raw-route.cdk.js +51 -0
  146. package/dist/raw-route.d.ts +119 -0
  147. package/dist/raw-route.d.ts.map +1 -0
  148. package/dist/raw-route.js +239 -0
  149. package/dist/raw-route.mock.d.ts +47 -0
  150. package/dist/raw-route.mock.d.ts.map +1 -0
  151. package/dist/raw-route.mock.js +51 -0
  152. package/dist/raw-route.test.d.ts +2 -0
  153. package/dist/raw-route.test.d.ts.map +1 -0
  154. package/dist/raw-route.test.js +597 -0
  155. package/dist/redact.d.ts +51 -0
  156. package/dist/redact.d.ts.map +1 -0
  157. package/dist/redact.js +134 -0
  158. package/dist/redact.test.d.ts +2 -0
  159. package/dist/redact.test.d.ts.map +1 -0
  160. package/dist/redact.test.js +199 -0
  161. package/dist/rpc.d.ts +54 -0
  162. package/dist/rpc.d.ts.map +1 -0
  163. package/dist/rpc.js +127 -0
  164. package/dist/rpc.test.d.ts +2 -0
  165. package/dist/rpc.test.d.ts.map +1 -0
  166. package/dist/rpc.test.js +60 -0
  167. package/dist/scripts/cdk-telemetry-env.d.ts +13 -0
  168. package/dist/scripts/cdk-telemetry-env.d.ts.map +1 -0
  169. package/dist/scripts/cdk-telemetry-env.js +21 -0
  170. package/dist/scripts/cdk-telemetry-env.test.d.ts +2 -0
  171. package/dist/scripts/cdk-telemetry-env.test.d.ts.map +1 -0
  172. package/dist/scripts/cdk-telemetry-env.test.js +56 -0
  173. package/dist/scripts/cleanup.d.ts +3 -0
  174. package/dist/scripts/cleanup.d.ts.map +1 -0
  175. package/dist/scripts/cleanup.js +35 -0
  176. package/dist/scripts/console.d.ts +6 -0
  177. package/dist/scripts/console.d.ts.map +1 -0
  178. package/dist/scripts/console.js +25 -0
  179. package/dist/scripts/deploy.d.ts +6 -0
  180. package/dist/scripts/deploy.d.ts.map +1 -0
  181. package/dist/scripts/deploy.js +82 -0
  182. package/dist/scripts/destroy.d.ts +6 -0
  183. package/dist/scripts/destroy.d.ts.map +1 -0
  184. package/dist/scripts/destroy.js +30 -0
  185. package/dist/scripts/dev-server-cors.test.d.ts +2 -0
  186. package/dist/scripts/dev-server-cors.test.d.ts.map +1 -0
  187. package/dist/scripts/dev-server-cors.test.js +23 -0
  188. package/dist/scripts/dev-server-telemetry.test.d.ts +2 -0
  189. package/dist/scripts/dev-server-telemetry.test.d.ts.map +1 -0
  190. package/dist/scripts/dev-server-telemetry.test.js +113 -0
  191. package/dist/scripts/dev-server.d.ts +21 -0
  192. package/dist/scripts/dev-server.d.ts.map +1 -0
  193. package/dist/scripts/dev-server.js +456 -0
  194. package/dist/scripts/ensure-secrets.d.ts +31 -0
  195. package/dist/scripts/ensure-secrets.d.ts.map +1 -0
  196. package/dist/scripts/ensure-secrets.js +100 -0
  197. package/dist/scripts/ensure-secrets.test.d.ts +2 -0
  198. package/dist/scripts/ensure-secrets.test.d.ts.map +1 -0
  199. package/dist/scripts/ensure-secrets.test.js +44 -0
  200. package/dist/scripts/external-migrations-step.d.ts +62 -0
  201. package/dist/scripts/external-migrations-step.d.ts.map +1 -0
  202. package/dist/scripts/external-migrations-step.js +243 -0
  203. package/dist/scripts/external-migrations-step.test.d.ts +2 -0
  204. package/dist/scripts/external-migrations-step.test.d.ts.map +1 -0
  205. package/dist/scripts/external-migrations-step.test.js +47 -0
  206. package/dist/scripts/extract-ts-types.d.ts +65 -0
  207. package/dist/scripts/extract-ts-types.d.ts.map +1 -0
  208. package/dist/scripts/extract-ts-types.js +664 -0
  209. package/dist/scripts/extract-ts-types.test.d.ts +2 -0
  210. package/dist/scripts/extract-ts-types.test.d.ts.map +1 -0
  211. package/dist/scripts/extract-ts-types.test.js +497 -0
  212. package/dist/scripts/generate-client-worker.d.ts +2 -0
  213. package/dist/scripts/generate-client-worker.d.ts.map +1 -0
  214. package/dist/scripts/generate-client-worker.js +13 -0
  215. package/dist/scripts/generate-client.d.ts +14 -0
  216. package/dist/scripts/generate-client.d.ts.map +1 -0
  217. package/dist/scripts/generate-client.js +73 -0
  218. package/dist/scripts/generate-client.test.d.ts +2 -0
  219. package/dist/scripts/generate-client.test.d.ts.map +1 -0
  220. package/dist/scripts/generate-client.test.js +48 -0
  221. package/dist/scripts/generate-spec-cli.d.ts +3 -0
  222. package/dist/scripts/generate-spec-cli.d.ts.map +1 -0
  223. package/dist/scripts/generate-spec-cli.js +77 -0
  224. package/dist/scripts/generate-spec-cli.test.d.ts +14 -0
  225. package/dist/scripts/generate-spec-cli.test.d.ts.map +1 -0
  226. package/dist/scripts/generate-spec-cli.test.js +90 -0
  227. package/dist/scripts/generate-spec.d.ts +61 -0
  228. package/dist/scripts/generate-spec.d.ts.map +1 -0
  229. package/dist/scripts/generate-spec.js +746 -0
  230. package/dist/scripts/generate-spec.test.d.ts +12 -0
  231. package/dist/scripts/generate-spec.test.d.ts.map +1 -0
  232. package/dist/scripts/generate-spec.test.js +88 -0
  233. package/dist/scripts/index.d.ts +12 -0
  234. package/dist/scripts/index.d.ts.map +1 -0
  235. package/dist/scripts/index.js +13 -0
  236. package/dist/scripts/sandbox.d.ts +11 -0
  237. package/dist/scripts/sandbox.d.ts.map +1 -0
  238. package/dist/scripts/sandbox.js +200 -0
  239. package/dist/scripts/telemetry-cli.d.ts +3 -0
  240. package/dist/scripts/telemetry-cli.d.ts.map +1 -0
  241. package/dist/scripts/telemetry-cli.js +8 -0
  242. package/dist/scripts/telemetry.d.ts +17 -0
  243. package/dist/scripts/telemetry.d.ts.map +1 -0
  244. package/dist/scripts/telemetry.js +150 -0
  245. package/dist/scripts/telemetry.test.d.ts +2 -0
  246. package/dist/scripts/telemetry.test.d.ts.map +1 -0
  247. package/dist/scripts/telemetry.test.js +203 -0
  248. package/dist/scripts/validate-spec.d.ts +22 -0
  249. package/dist/scripts/validate-spec.d.ts.map +1 -0
  250. package/dist/scripts/validate-spec.js +159 -0
  251. package/dist/server/index.d.ts +3 -0
  252. package/dist/server/index.d.ts.map +1 -0
  253. package/dist/server/index.js +3 -0
  254. package/dist/server/withAuth.d.ts +142 -0
  255. package/dist/server/withAuth.d.ts.map +1 -0
  256. package/dist/server/withAuth.js +261 -0
  257. package/dist/server/withAuth.test.d.ts +2 -0
  258. package/dist/server/withAuth.test.d.ts.map +1 -0
  259. package/dist/server/withAuth.test.js +278 -0
  260. package/dist/telemetry/client.d.ts +59 -0
  261. package/dist/telemetry/client.d.ts.map +1 -0
  262. package/dist/telemetry/client.js +215 -0
  263. package/dist/telemetry/config-writer.d.ts +16 -0
  264. package/dist/telemetry/config-writer.d.ts.map +1 -0
  265. package/dist/telemetry/config-writer.js +36 -0
  266. package/dist/telemetry/consent-commands.test.d.ts +2 -0
  267. package/dist/telemetry/consent-commands.test.d.ts.map +1 -0
  268. package/dist/telemetry/consent-commands.test.js +161 -0
  269. package/dist/telemetry/consent.d.ts +60 -0
  270. package/dist/telemetry/consent.d.ts.map +1 -0
  271. package/dist/telemetry/consent.js +122 -0
  272. package/dist/telemetry/environment.d.ts +33 -0
  273. package/dist/telemetry/environment.d.ts.map +1 -0
  274. package/dist/telemetry/environment.js +87 -0
  275. package/dist/telemetry/identifiers.d.ts +20 -0
  276. package/dist/telemetry/identifiers.d.ts.map +1 -0
  277. package/dist/telemetry/identifiers.js +86 -0
  278. package/dist/telemetry/index.d.ts +6 -0
  279. package/dist/telemetry/index.d.ts.map +1 -0
  280. package/dist/telemetry/index.js +4 -0
  281. package/dist/telemetry/telemetry.test.d.ts +2 -0
  282. package/dist/telemetry/telemetry.test.d.ts.map +1 -0
  283. package/dist/telemetry/telemetry.test.js +1131 -0
  284. package/dist/telemetry/trackCommand.d.ts +32 -0
  285. package/dist/telemetry/trackCommand.d.ts.map +1 -0
  286. package/dist/telemetry/trackCommand.js +102 -0
  287. package/dist/telemetry/types.d.ts +92 -0
  288. package/dist/telemetry/types.d.ts.map +1 -0
  289. package/dist/telemetry/types.js +1 -0
  290. package/dist/types.d.ts +24 -0
  291. package/dist/types.d.ts.map +1 -0
  292. package/dist/types.js +3 -0
  293. package/dist/version.d.ts +2 -0
  294. package/dist/version.d.ts.map +1 -0
  295. package/dist/version.js +2 -0
  296. package/package.json +83 -0
  297. package/src/api.test.ts +25 -0
  298. package/src/api.ts +163 -0
  299. package/src/bb-utils.ts +13 -0
  300. package/src/builtin-routes.ts +62 -0
  301. package/src/cdk/__fixtures__/factory-backend.ts +8 -0
  302. package/src/cdk/__fixtures__/fullid-construct-backend.ts +42 -0
  303. package/src/cdk/__fixtures__/handler.ts +4 -0
  304. package/src/cdk/__fixtures__/side-effect-backend.ts +7 -0
  305. package/src/cdk/blocks-backend.test.ts +207 -0
  306. package/src/cdk/blocks-backend.ts +249 -0
  307. package/src/cdk/blocks-stack.test.ts +106 -0
  308. package/src/cdk/config-registry.ts +106 -0
  309. package/src/cdk/index.ts +115 -0
  310. package/src/cdk/mixins.test.ts +129 -0
  311. package/src/cdk/mixins.ts +41 -0
  312. package/src/cdk/node-version.ts +17 -0
  313. package/src/cdk/stack-metadata.test.ts +71 -0
  314. package/src/cdk/stack-metadata.ts +29 -0
  315. package/src/cdk/synth-guard.test.ts +21 -0
  316. package/src/cdk/synth-guard.ts +23 -0
  317. package/src/client/index.test.ts +118 -0
  318. package/src/client/index.ts +320 -0
  319. package/src/common/config.test.ts +236 -0
  320. package/src/common/config.ts +169 -0
  321. package/src/common/constants.ts +15 -0
  322. package/src/common/crypto.ts +15 -0
  323. package/src/common/index.test.ts +242 -0
  324. package/src/common/index.ts +335 -0
  325. package/src/common/mock-data.ts +52 -0
  326. package/src/common/official-bb-names.generated.ts +29 -0
  327. package/src/common/sdk-registry.ts +150 -0
  328. package/src/constants.ts +31 -0
  329. package/src/cors.test.ts +279 -0
  330. package/src/cors.ts +91 -0
  331. package/src/db-naming.test.ts +48 -0
  332. package/src/db-naming.ts +41 -0
  333. package/src/errors.ts +72 -0
  334. package/src/hosting.test.ts +1431 -0
  335. package/src/hosting.ts +626 -0
  336. package/src/index.cdk.ts +45 -0
  337. package/src/index.ts +22 -0
  338. package/src/lambda-handler.test.ts +1005 -0
  339. package/src/lambda-handler.ts +706 -0
  340. package/src/pipeline/index.ts +12 -0
  341. package/src/pipeline/pipeline-construct.ts +662 -0
  342. package/src/pipeline/pipeline.test.ts +1565 -0
  343. package/src/pipeline/types.ts +384 -0
  344. package/src/raw-route.cdk.ts +56 -0
  345. package/src/raw-route.mock.ts +56 -0
  346. package/src/raw-route.test.ts +819 -0
  347. package/src/raw-route.ts +316 -0
  348. package/src/redact.test.ts +222 -0
  349. package/src/redact.ts +143 -0
  350. package/src/rpc.test.ts +73 -0
  351. package/src/rpc.ts +174 -0
  352. package/src/scripts/cdk-telemetry-env.test.ts +75 -0
  353. package/src/scripts/cdk-telemetry-env.ts +23 -0
  354. package/src/scripts/cleanup.ts +39 -0
  355. package/src/scripts/console.ts +34 -0
  356. package/src/scripts/deploy.ts +106 -0
  357. package/src/scripts/destroy.ts +42 -0
  358. package/src/scripts/dev-server-cors.test.ts +28 -0
  359. package/src/scripts/dev-server-telemetry.test.ts +127 -0
  360. package/src/scripts/dev-server.ts +515 -0
  361. package/src/scripts/ensure-secrets.test.ts +50 -0
  362. package/src/scripts/ensure-secrets.ts +113 -0
  363. package/src/scripts/external-migrations-step.test.ts +64 -0
  364. package/src/scripts/external-migrations-step.ts +269 -0
  365. package/src/scripts/extract-ts-types.test.ts +516 -0
  366. package/src/scripts/extract-ts-types.ts +773 -0
  367. package/src/scripts/generate-client-worker.ts +15 -0
  368. package/src/scripts/generate-client.test.ts +69 -0
  369. package/src/scripts/generate-client.ts +86 -0
  370. package/src/scripts/generate-spec-cli.test.ts +106 -0
  371. package/src/scripts/generate-spec-cli.ts +92 -0
  372. package/src/scripts/generate-spec.test.ts +99 -0
  373. package/src/scripts/generate-spec.ts +833 -0
  374. package/src/scripts/index.ts +21 -0
  375. package/src/scripts/sandbox.ts +230 -0
  376. package/src/scripts/telemetry-cli.ts +10 -0
  377. package/src/scripts/telemetry.test.ts +239 -0
  378. package/src/scripts/telemetry.ts +173 -0
  379. package/src/scripts/validate-spec.ts +199 -0
  380. package/src/server/index.ts +5 -0
  381. package/src/server/withAuth.test.ts +348 -0
  382. package/src/server/withAuth.ts +323 -0
  383. package/src/telemetry/client.ts +226 -0
  384. package/src/telemetry/config-writer.ts +52 -0
  385. package/src/telemetry/consent-commands.test.ts +196 -0
  386. package/src/telemetry/consent.ts +154 -0
  387. package/src/telemetry/environment.ts +100 -0
  388. package/src/telemetry/identifiers.ts +94 -0
  389. package/src/telemetry/index.ts +11 -0
  390. package/src/telemetry/telemetry.test.ts +1343 -0
  391. package/src/telemetry/trackCommand.ts +110 -0
  392. package/src/telemetry/types.ts +92 -0
  393. package/src/types.ts +33 -0
  394. package/src/version.ts +2 -0
@@ -0,0 +1,833 @@
1
+ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ /**
5
+ * Generate an OpenRPC 1.3.2 specification document from backend ApiNamespace exports.
6
+ *
7
+ * Phase 1: Introspect each `ApiNamespace`, discover methods, emit base types.
8
+ * Phase 2: Detect Zod schemas referenced in method bodies and emit precise
9
+ * JSON Schema types via Zod 4's built-in `toJSONSchema()`.
10
+ *
11
+ * The mechanism mirrors `generate-client.ts`: dynamically import the backend
12
+ * via `pathToFileURL`, discover `ApiNamespace` exports via `API_NAMESPACE_MARKER`,
13
+ * and reflect on handler methods.
14
+ *
15
+ * @see docs/native-clients/codegen-design.md — IR design decision and authoring progression
16
+ * @see docs/native-clients/schema-generation-guide-for-devs.md — customer-facing guide and `x-blocks-*` reference
17
+ */
18
+
19
+ import { writeFileSync, mkdirSync, readFileSync, existsSync } from 'fs';
20
+ import { dirname, join } from 'path';
21
+ import { pathToFileURL } from 'url';
22
+ import { API_NAMESPACE_MARKER } from '../api.js';
23
+ import { extractMethodTypes, extractSkipCodegenMethods, type MethodTypeInfo } from './extract-ts-types.js';
24
+ import { validateSpec, type SpecValidationError } from './validate-spec.js';
25
+
26
+ // ── OpenRPC types (subset) ──────────────────────────────────────────────────
27
+
28
+ interface OpenRpcDocument {
29
+ openrpc: string;
30
+ info: { title: string; version: string };
31
+ servers?: { name: string; url: string }[];
32
+ methods: OpenRpcMethod[];
33
+ components?: { schemas?: Record<string, JsonSchema> };
34
+ }
35
+
36
+ interface OpenRpcMethod {
37
+ name: string;
38
+ params: OpenRpcParam[];
39
+ result: OpenRpcResult;
40
+ }
41
+
42
+ interface OpenRpcParam {
43
+ name: string;
44
+ required: boolean;
45
+ schema: JsonSchema;
46
+ }
47
+
48
+ interface OpenRpcResult {
49
+ name: string;
50
+ schema: JsonSchema;
51
+ }
52
+
53
+ interface JsonSchema {
54
+ type?: string;
55
+ $ref?: string;
56
+ [key: string]: unknown;
57
+ }
58
+
59
+ // ── Zod detection ───────────────────────────────────────────────────────────
60
+
61
+ /**
62
+ * Check if a value is a Zod schema (Zod 4+).
63
+ * Zod 4 schemas have a `_zod` property with `def`, `parse`, etc.
64
+ */
65
+ function isZodSchema(val: unknown): boolean {
66
+ return val != null && typeof val === 'object' && (val as any)._zod != null && typeof (val as any)._zod === 'object';
67
+ }
68
+
69
+ /**
70
+ * Try to convert a Zod schema to JSON Schema using Zod 4's built-in `toJSONSchema()`.
71
+ * Returns null if the conversion fails or Zod is not available.
72
+ */
73
+ function zodToJsonSchema(zodModule: any, schema: unknown): JsonSchema | null {
74
+ try {
75
+ if (typeof zodModule?.toJSONSchema !== 'function') return null;
76
+ const raw = zodModule.toJSONSchema(schema);
77
+ // Strip the $schema field — OpenRPC components don't need it
78
+ const { $schema, ...rest } = raw;
79
+ return rest as JsonSchema;
80
+ } catch {
81
+ return null;
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Extract the meta `id` from a Zod schema's JSON Schema output.
87
+ * Zod 4's `.meta({ id: 'Post' })` emits `"id": "Post"` at the top level.
88
+ */
89
+ function getSchemaMetaId(jsonSchema: JsonSchema): string | undefined {
90
+ const id = jsonSchema.id;
91
+ if (typeof id === 'string' && id.length > 0) return id;
92
+ return undefined;
93
+ }
94
+
95
+ /** Capitalize first letter: "kvGet" → "KvGet" */
96
+ function capitalize(s: string): string {
97
+ return s.charAt(0).toUpperCase() + s.slice(1);
98
+ }
99
+
100
+ /**
101
+ * Collect all Zod schemas reachable from the backend module.
102
+ *
103
+ * Walks exported values looking for anything with `_zod`. Also walks
104
+ * one level into plain objects (e.g., an exported `schemas` map).
105
+ * Returns a map of variable-name → Zod schema instance.
106
+ */
107
+ function collectZodSchemas(backend: Record<string, any>): Map<string, unknown> {
108
+ const schemas = new Map<string, unknown>();
109
+ for (const [name, value] of Object.entries(backend)) {
110
+ if (name.startsWith('_')) continue;
111
+ if (isZodSchema(value)) {
112
+ schemas.set(name, value);
113
+ } else if (value != null && typeof value === 'object' && !Array.isArray(value)) {
114
+ // Walk one level into plain objects (e.g., exported schema maps)
115
+ for (const [subName, subValue] of Object.entries(value)) {
116
+ if (isZodSchema(subValue)) {
117
+ schemas.set(subName, subValue);
118
+ }
119
+ }
120
+ }
121
+ }
122
+ return schemas;
123
+ }
124
+
125
+ /**
126
+ * Scan a function's source text for `.parse(` calls that reference known
127
+ * Zod schema variable names. Returns the first matching schema, or null.
128
+ *
129
+ * Matches patterns like:
130
+ * - `GetPostInput.parse(rawInput)`
131
+ * - `GetPostInput.safeParse(rawInput)`
132
+ * - `schema.parse(input)`
133
+ */
134
+ function findZodParseSchema(
135
+ fnSource: string,
136
+ knownSchemas: Map<string, unknown>,
137
+ ): unknown | null {
138
+ for (const [name] of knownSchemas) {
139
+ // Match: schemaName.parse( or schemaName.safeParse(
140
+ const pattern = new RegExp(`\\b${escapeRegex(name)}\\s*\\.\\s*(?:safe)?[Pp]arse\\s*\\(`);
141
+ if (pattern.test(fnSource)) {
142
+ return knownSchemas.get(name) ?? null;
143
+ }
144
+ }
145
+ return null;
146
+ }
147
+
148
+ function escapeRegex(s: string): string {
149
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
150
+ }
151
+
152
+ // ── Param name extraction ───────────────────────────────────────────────────
153
+
154
+ /**
155
+ * Extract parameter names from a function using `Function.toString()`.
156
+ *
157
+ * Handles:
158
+ * - `async foo(a, b, c) { ... }`
159
+ * - `async (a, b) => { ... }`
160
+ * - `function foo(a, b) { ... }`
161
+ * - Destructured params: `{ key, value }` → single param named `param0`
162
+ * - Default values: `a = 5` → `a`
163
+ * - Rest params: `...args` → `args`
164
+ * - Optional params: `a?` → `a` (TS syntax erased, but sometimes visible)
165
+ */
166
+ function extractParamNames(fn: Function): string[] {
167
+ const src = fn.toString();
168
+
169
+ // Match the parameter list between the first ( and its matching )
170
+ const openParen = src.indexOf('(');
171
+ if (openParen === -1) return [];
172
+
173
+ // Find matching close paren, accounting for nested parens
174
+ let depth = 0;
175
+ let closeParen = -1;
176
+ for (let i = openParen; i < src.length; i++) {
177
+ if (src[i] === '(') depth++;
178
+ else if (src[i] === ')') {
179
+ depth--;
180
+ if (depth === 0) { closeParen = i; break; }
181
+ }
182
+ }
183
+ if (closeParen === -1) return [];
184
+
185
+ const paramStr = src.substring(openParen + 1, closeParen).trim();
186
+ if (!paramStr) return [];
187
+
188
+ // Split on commas, but respect nested braces/brackets/parens
189
+ const params: string[] = [];
190
+ let current = '';
191
+ let nestDepth = 0;
192
+ for (const ch of paramStr) {
193
+ if (ch === '(' || ch === '{' || ch === '[') nestDepth++;
194
+ else if (ch === ')' || ch === '}' || ch === ']') nestDepth--;
195
+ else if (ch === ',' && nestDepth === 0) {
196
+ params.push(current.trim());
197
+ current = '';
198
+ continue;
199
+ }
200
+ current += ch;
201
+ }
202
+ if (current.trim()) params.push(current.trim());
203
+
204
+ return params.map((raw, i) => {
205
+ // Destructured: { key, value } → param{i}
206
+ if (raw.startsWith('{') || raw.startsWith('[')) return `param${i}`;
207
+ // Rest: ...args → args
208
+ if (raw.startsWith('...')) raw = raw.slice(3);
209
+ // Strip default value: a = 5 → a
210
+ const eqIdx = raw.indexOf('=');
211
+ if (eqIdx !== -1) raw = raw.substring(0, eqIdx);
212
+ // Strip TS type annotation: a: string → a (visible in some runtimes)
213
+ const colonIdx = raw.indexOf(':');
214
+ if (colonIdx !== -1) raw = raw.substring(0, colonIdx);
215
+ // Strip optional marker: a? → a
216
+ raw = raw.replace(/\?$/, '');
217
+ return raw.trim();
218
+ });
219
+ }
220
+
221
+ // ── Spec generation ─────────────────────────────────────────────────────────
222
+
223
+ /**
224
+ * Discover all `ApiNamespace` exports from the backend module and build
225
+ * an OpenRPC document describing every method.
226
+ *
227
+ * For methods that reference Zod schemas (detected via `.parse()` calls in
228
+ * the function body), emits precise JSON Schema types. For plain TS methods,
229
+ * falls back to `{ "type": "unknown" }`.
230
+ *
231
+ * Named schemas (via `.meta({ id: 'Post' })`) are placed in
232
+ * `components.schemas` with `$ref` pointers. Unnamed schemas are inlined
233
+ * or given synthetic names like `KvGetInput`.
234
+ */
235
+ /**
236
+ * Loader for the foundation module. Defaults to plain dynamic `import()` so JS
237
+ * entry points work with no extra deps. The CLI overrides this with a tsx-based
238
+ * loader when the entry point ends in `.ts` / `.tsx`, so customers can point the
239
+ * spec emitter at their TypeScript source without an extra build step.
240
+ */
241
+ export type FoundationLoader = (fileUrl: string) => Promise<Record<string, unknown>>;
242
+
243
+ const defaultLoader: FoundationLoader = (url) => import(url);
244
+
245
+ export async function generateSpec(
246
+ foundationPath: string,
247
+ loader: FoundationLoader = defaultLoader,
248
+ ): Promise<OpenRpcDocument> {
249
+ // Reuse the same global-collector pattern as generate-client.ts so that
250
+ // Building Block constructors don't fail during import.
251
+ const collectorAlreadyActive = Array.isArray((globalThis as any).__BLOCKS_CLIENT_MIDDLEWARE__);
252
+ if (!collectorAlreadyActive) {
253
+ (globalThis as any).__BLOCKS_CLIENT_MIDDLEWARE__ = [];
254
+ }
255
+
256
+ const backend = await loader(pathToFileURL(foundationPath).href);
257
+
258
+ if (!collectorAlreadyActive) {
259
+ delete (globalThis as any).__BLOCKS_CLIENT_MIDDLEWARE__;
260
+ }
261
+
262
+ // Try to import Zod from the backend's dependency tree.
263
+ // If Zod isn't installed, Phase 2 features are skipped gracefully.
264
+ let zodModule: any = null;
265
+ try {
266
+ zodModule = await import('zod');
267
+ // Zod 4 re-exports from a nested module; normalize
268
+ if (zodModule.z && typeof zodModule.z.toJSONSchema === 'function') {
269
+ zodModule = zodModule.z;
270
+ }
271
+ } catch {
272
+ // Zod not available — all schemas will be { type: "unknown" }
273
+ }
274
+
275
+ // Collect Zod schemas from module exports
276
+ const knownSchemas = collectZodSchemas(backend);
277
+
278
+ // Extract TypeScript type info from the source file (AST-based).
279
+ // Used as fallback for methods without Zod schemas — gives base types
280
+ // (string, number, boolean, object shapes) instead of "unknown".
281
+ let tsTypes = new Map<string, MethodTypeInfo>();
282
+ try {
283
+ tsTypes = extractMethodTypes(foundationPath);
284
+ } catch {
285
+ // TS parsing failed — fall back to unknown types
286
+ }
287
+
288
+ // Pure-AST scan for `@blocksSkipCodegen`-tagged methods. Runs even when
289
+ // `extractMethodTypes` blows up on a real-world TS program (a common
290
+ // scenario: missing tsconfig paths, third-party `.d.ts` mismatches).
291
+ let skipCodegenNames = new Set<string>();
292
+ try {
293
+ skipCodegenNames = extractSkipCodegenMethods(foundationPath);
294
+ } catch {
295
+ // AST parse failed — nothing to skip.
296
+ }
297
+
298
+ const methods: OpenRpcMethod[] = [];
299
+ const componentSchemas: Record<string, JsonSchema> = {};
300
+
301
+ for (const [exportName, exportValue] of Object.entries(backend)) {
302
+ if (exportName.startsWith('_')) continue;
303
+
304
+ const namespaceName = (exportValue as any)?.[API_NAMESPACE_MARKER];
305
+ if (typeof namespaceName !== 'string') continue;
306
+
307
+ // Use the export name for the qualified method name — this matches
308
+ // how the dev server and Lambda handler register APIs (by export name,
309
+ // not the ApiNamespace() first argument).
310
+ const routingName = exportName;
311
+
312
+ // ApiNamespace is a function (context) => { method1, method2, ... }
313
+ // Call it with a stub context to get the method map.
314
+ const stubContext = createStubContext();
315
+ let methodMap: Record<string, Function>;
316
+ try {
317
+ const result = typeof exportValue === 'function' ? exportValue(stubContext) : exportValue;
318
+ methodMap = result && typeof result === 'object' ? result : {};
319
+ } catch {
320
+ // If the handler throws during stub invocation, skip this namespace
321
+ continue;
322
+ }
323
+
324
+ for (const [methodName, methodFn] of Object.entries(methodMap)) {
325
+ if (typeof methodFn !== 'function') continue;
326
+
327
+ // `@blocksSkipCodegen` is a JSDoc tag the customer puts on a method
328
+ // they want callable from the generated TS client (Proxy-based,
329
+ // doesn't enumerate) but absent from the OpenRPC spec — so native
330
+ // generators (Swift / Kotlin) never emit a binding for it.
331
+ // Mock-only / dev-only helpers like `getLastCode` use this.
332
+ //
333
+ // Two sources because `extractMethodTypes` can fail-soft on
334
+ // type-resolution errors and silently empty its Map — we always
335
+ // want the JSDoc tag to be honored, so the AST-only scan acts as
336
+ // a safety net.
337
+ if (tsTypes.get(methodName)?.skipCodegen || skipCodegenNames.has(methodName)) continue;
338
+
339
+ const qualifiedName = `${routingName}.${methodName}`;
340
+ const resultName = capitalize(methodName) + 'Result';
341
+ const paramNames = extractParamNames(methodFn);
342
+ const fnSource = methodFn.toString();
343
+
344
+ // Phase 2: try to find a Zod input schema referenced in the method body
345
+ const inputSchema = zodModule ? findZodParseSchema(fnSource, knownSchemas) : null;
346
+ let inputJsonSchema: JsonSchema | null = null;
347
+
348
+ if (inputSchema && zodModule) {
349
+ inputJsonSchema = zodToJsonSchema(zodModule, inputSchema);
350
+ }
351
+
352
+ let params: OpenRpcParam[];
353
+
354
+ if (inputJsonSchema && inputJsonSchema.type === 'object' && inputJsonSchema.properties) {
355
+ // Zod object schema detected — emit one param per property
356
+ const required = new Set<string>(
357
+ Array.isArray(inputJsonSchema.required) ? inputJsonSchema.required as string[] : []
358
+ );
359
+ const metaId = getSchemaMetaId(inputJsonSchema);
360
+
361
+ if (metaId) {
362
+ // Named schema → place in components.schemas, reference via $ref
363
+ const { id: _id, additionalProperties: _ap, ...cleanSchema } = inputJsonSchema;
364
+ componentSchemas[metaId] = cleanSchema;
365
+ }
366
+
367
+ params = Object.entries(inputJsonSchema.properties as Record<string, JsonSchema>).map(
368
+ ([propName, propSchema]) => ({
369
+ name: propName,
370
+ required: required.has(propName),
371
+ schema: cleanJsonSchema(propSchema),
372
+ })
373
+ );
374
+ } else if (inputJsonSchema) {
375
+ // Non-object Zod schema (e.g., z.string()) — single param
376
+ params = paramNames.map((name) => ({
377
+ name,
378
+ required: true,
379
+ schema: cleanJsonSchema(inputJsonSchema!),
380
+ }));
381
+ } else {
382
+ // No Zod schema — fall back to TypeScript AST types
383
+ const tsInfo = tsTypes.get(methodName);
384
+ if (tsInfo) {
385
+ params = tsInfo.params.map((p) => ({
386
+ name: p.name,
387
+ required: p.required,
388
+ schema: p.schema.type === 'unknown' ? { type: 'unknown' } : p.schema,
389
+ }));
390
+ } else {
391
+ // No TS type info either — emit unknown
392
+ params = paramNames.map((name) => ({
393
+ name,
394
+ required: true,
395
+ schema: { type: 'unknown' },
396
+ }));
397
+ }
398
+ }
399
+
400
+ // Return type: Zod doesn't cover this yet, so use TS types
401
+ const tsInfo = tsTypes.get(methodName);
402
+ const rawResultSchema = tsInfo?.returnType && tsInfo.returnType.type !== 'unknown'
403
+ ? tsInfo.returnType
404
+ : { type: 'unknown' };
405
+
406
+ // Determine result schema — transferable types get semantic annotations
407
+ let resultSchema: JsonSchema;
408
+
409
+ if (tsInfo?.transferable) {
410
+ // Emit x-blocks-transferable instead of raw object schema
411
+ const transferableSchema: JsonSchema = {
412
+ 'x-blocks-transferable': tsInfo.transferable.blocksTag,
413
+ };
414
+ if (tsInfo.transferable.typeArgs.length > 0) {
415
+ // Hoist named type args to components.schemas, build the type-args array
416
+ const typeArgsSchemas: JsonSchema[] = [];
417
+ for (const argSchema of tsInfo.transferable.typeArgs) {
418
+ const argTitle = argSchema.title as string | undefined;
419
+ if (argTitle && argSchema.type === 'object' && argSchema.properties) {
420
+ const { title: _t, ...cleanArg } = argSchema;
421
+ if (!componentSchemas[argTitle]) {
422
+ componentSchemas[argTitle] = cleanArg;
423
+ }
424
+ typeArgsSchemas.push({ $ref: `#/components/schemas/${argTitle}` });
425
+ } else {
426
+ typeArgsSchemas.push(argSchema);
427
+ }
428
+ }
429
+ transferableSchema['x-blocks-type-args'] = typeArgsSchemas;
430
+ }
431
+ resultSchema = transferableSchema;
432
+ } else if (rawResultSchema.title && rawResultSchema.type === 'object' && rawResultSchema.properties) {
433
+ // Named object type → extract to components.schemas
434
+ const returnTitle = rawResultSchema.title as string;
435
+ const { title: _t, ...cleanReturn } = rawResultSchema;
436
+ if (!componentSchemas[returnTitle]) {
437
+ componentSchemas[returnTitle] = cleanReturn;
438
+ }
439
+ resultSchema = { $ref: `#/components/schemas/${returnTitle}` };
440
+ } else if (rawResultSchema.type === 'array' && rawResultSchema.items) {
441
+ // Check if array items have a named type (e.g., Todo[] → extract Todo)
442
+ const items = rawResultSchema.items as JsonSchema;
443
+ const itemsTitle = items.title as string | undefined;
444
+ if (itemsTitle && items.type === 'object' && items.properties) {
445
+ const { title: _t, ...cleanItems } = items;
446
+ if (!componentSchemas[itemsTitle]) {
447
+ componentSchemas[itemsTitle] = cleanItems;
448
+ }
449
+ resultSchema = { type: 'array', items: { $ref: `#/components/schemas/${itemsTitle}` } };
450
+ } else {
451
+ resultSchema = rawResultSchema;
452
+ }
453
+ } else {
454
+ resultSchema = rawResultSchema;
455
+ }
456
+
457
+ methods.push({
458
+ name: qualifiedName,
459
+ params,
460
+ result: {
461
+ name: resultName,
462
+ schema: resultSchema,
463
+ },
464
+ });
465
+ }
466
+ }
467
+
468
+ // Sort methods alphabetically for stable output
469
+ methods.sort((a, b) => a.name.localeCompare(b.name));
470
+
471
+ // Post-process step 1: regroup nested-union arms that share a primary
472
+ // discriminator. Run BEFORE hoisting so the regrouped wrapper picks up
473
+ // the named title (if any) in one go and codegen sees a single arm per
474
+ // action name.
475
+ for (const method of methods) {
476
+ for (const param of method.params) {
477
+ param.schema = regroupSharedDiscriminator(param.schema);
478
+ }
479
+ method.result.schema = regroupSharedDiscriminator(method.result.schema);
480
+ }
481
+ for (const [name, schema] of Object.entries(componentSchemas)) {
482
+ componentSchemas[name] = regroupSharedDiscriminator(schema);
483
+ }
484
+
485
+ // Post-process step 2: walk all schemas and hoist named types (objects with `title`)
486
+ // to components.schemas with $ref pointers. This catches named types nested
487
+ // inside other schemas (e.g., AuthUser inside AuthState.user oneOf).
488
+ for (const method of methods) {
489
+ for (const param of method.params) {
490
+ param.schema = hoistNamedSchemas(param.schema, componentSchemas);
491
+ }
492
+ method.result.schema = hoistNamedSchemas(method.result.schema, componentSchemas);
493
+ }
494
+ // Also walk component schemas themselves (e.g., AuthState contains AuthAction contains AuthField)
495
+ for (const [name, schema] of Object.entries(componentSchemas)) {
496
+ componentSchemas[name] = hoistNamedSchemas(schema, componentSchemas, name);
497
+ }
498
+
499
+ const doc: OpenRpcDocument = {
500
+ openrpc: '1.3.2',
501
+ info: { title: 'api', version: '1.0.0' },
502
+ methods,
503
+ };
504
+
505
+ if (Object.keys(componentSchemas).length > 0) {
506
+ doc.components = { schemas: componentSchemas };
507
+ }
508
+
509
+ return doc;
510
+ }
511
+
512
+ /**
513
+ * Recursively walk a JSON Schema tree and hoist any named object types
514
+ * (those with a `title` property) to `componentSchemas` with `$ref` pointers.
515
+ *
516
+ * This ensures types like `AuthUser` (nested inside `AuthState.user`) get
517
+ * extracted to `components.schemas` so native codegen produces named types
518
+ * instead of anonymous inline structs.
519
+ *
520
+ * @param skipTitle - Don't hoist the schema itself if its title matches this
521
+ * (prevents a component schema from replacing itself with a self-$ref).
522
+ */
523
+ function hoistNamedSchemas(
524
+ schema: JsonSchema,
525
+ componentSchemas: Record<string, JsonSchema>,
526
+ skipTitle?: string,
527
+ ): JsonSchema {
528
+ if (!schema || typeof schema !== 'object') return schema;
529
+
530
+ // If this is a $ref, nothing to do
531
+ if (schema.$ref) return schema;
532
+
533
+ // If this is a named object type, hoist it (unless it's the schema we're currently inside
534
+ // or it's a built-in JS type that shouldn't become a component schema)
535
+ const BUILTIN_TYPES = new Set(['Date', 'RegExp', 'Error', 'Map', 'Set', 'Promise', 'ArrayBuffer', 'URL']);
536
+ const title = schema.title as string | undefined;
537
+ if (title && title !== skipTitle && !BUILTIN_TYPES.has(title) && schema.type === 'object' && schema.properties) {
538
+ const { title: _t, ...cleanSchema } = schema as any;
539
+ if (!componentSchemas[title]) {
540
+ componentSchemas[title] = cleanSchema;
541
+ // Recursively hoist within the newly added schema
542
+ componentSchemas[title] = hoistNamedSchemas(cleanSchema, componentSchemas, title);
543
+ }
544
+ return { $ref: `#/components/schemas/${title}` };
545
+ }
546
+
547
+ // Walk properties
548
+ if (schema.properties) {
549
+ const newProps: Record<string, JsonSchema> = {};
550
+ for (const [key, val] of Object.entries(schema.properties as Record<string, JsonSchema>)) {
551
+ newProps[key] = hoistNamedSchemas(val, componentSchemas, skipTitle);
552
+ }
553
+ schema = { ...schema, properties: newProps };
554
+ }
555
+
556
+ // Walk array items
557
+ if (schema.items) {
558
+ if (Array.isArray(schema.items)) {
559
+ schema = { ...schema, items: schema.items.map((i: JsonSchema) => hoistNamedSchemas(i, componentSchemas, skipTitle)) };
560
+ } else {
561
+ schema = { ...schema, items: hoistNamedSchemas(schema.items as JsonSchema, componentSchemas, skipTitle) };
562
+ }
563
+ }
564
+
565
+ // Walk oneOf
566
+ if (Array.isArray(schema.oneOf)) {
567
+ schema = { ...schema, oneOf: schema.oneOf.map((s: JsonSchema) => hoistNamedSchemas(s, componentSchemas, skipTitle)) };
568
+ }
569
+
570
+ // Walk additionalProperties
571
+ if (schema.additionalProperties && typeof schema.additionalProperties === 'object') {
572
+ schema = { ...schema, additionalProperties: hoistNamedSchemas(schema.additionalProperties as JsonSchema, componentSchemas, skipTitle) };
573
+ }
574
+
575
+ return schema;
576
+ }
577
+
578
+ /**
579
+ * Strip Zod-generated noise from a schema for cleaner OpenRPC output.
580
+ * Removes `$schema` and `id` (meta) fields. Preserves
581
+ * `additionalProperties` because it carries open-shape semantics for
582
+ * intersections like `T & Record<string, V>` (e.g. Cognito sign-up's
583
+ * custom-attribute payload) that consumer codegen needs to honor.
584
+ */
585
+ function cleanJsonSchema(schema: JsonSchema): JsonSchema {
586
+ const { $schema, id, ...rest } = schema;
587
+ return rest;
588
+ }
589
+
590
+ /**
591
+ * If a `oneOf` arose from the TS pattern `{ action: K } & U[K]` where `U[K]`
592
+ * is itself a union (e.g. Cognito's `confirmSignIn` challenge variants),
593
+ * the intersection distributes — producing N flat sibling arms that all
594
+ * share a primary discriminator (`action: 'confirmSignIn'`) plus a few
595
+ * shared base fields. Native codegen sees N look-alike structs and falls
596
+ * back to numeric suffixes (`ConfirmSignIn_1`, `ConfirmSignIn_2`, …).
597
+ *
598
+ * This pass walks each `oneOf` and, for any group of arms whose primary
599
+ * discriminator collapses to the same single literal value, regroups them
600
+ * under one outer arm with a nested `oneOf` over the remaining
601
+ * (truly-distinguishing) per-arm fields. A secondary discriminator field —
602
+ * one whose literal value differs across the group — is kept per-arm so
603
+ * the inner `oneOf` stays a discriminated union.
604
+ */
605
+ function regroupSharedDiscriminator(schema: JsonSchema): JsonSchema {
606
+ if (!schema || typeof schema !== 'object') return schema;
607
+
608
+ // Recurse first so nested oneOfs are normalized bottom-up.
609
+ if (schema.properties) {
610
+ const newProps: Record<string, JsonSchema> = {};
611
+ for (const [k, v] of Object.entries(schema.properties as Record<string, JsonSchema>)) {
612
+ newProps[k] = regroupSharedDiscriminator(v);
613
+ }
614
+ schema = { ...schema, properties: newProps };
615
+ }
616
+ if (schema.items) {
617
+ if (Array.isArray(schema.items)) {
618
+ schema = { ...schema, items: schema.items.map((s: JsonSchema) => regroupSharedDiscriminator(s)) };
619
+ } else {
620
+ schema = { ...schema, items: regroupSharedDiscriminator(schema.items as JsonSchema) };
621
+ }
622
+ }
623
+ if (schema.additionalProperties && typeof schema.additionalProperties === 'object') {
624
+ schema = { ...schema, additionalProperties: regroupSharedDiscriminator(schema.additionalProperties as JsonSchema) };
625
+ }
626
+
627
+ if (!Array.isArray(schema.oneOf)) return schema;
628
+
629
+ const arms = (schema.oneOf as JsonSchema[]).map((a) => regroupSharedDiscriminator(a));
630
+
631
+ // Group arms by their primary discriminator literal value. We pick the
632
+ // discriminator as the field that has a single-literal `const`/`enum`
633
+ // value AND appears with the same literal across every member of a
634
+ // candidate group. Fall back: most-common-literal field name.
635
+ const primary = findPrimaryDiscriminator(arms);
636
+ if (!primary) return { ...schema, oneOf: arms };
637
+
638
+ const groups = new Map<string, JsonSchema[]>();
639
+ const ungrouped: JsonSchema[] = [];
640
+ for (const arm of arms) {
641
+ const lit = singleLiteralValue(arm, primary);
642
+ if (lit === undefined) {
643
+ ungrouped.push(arm);
644
+ continue;
645
+ }
646
+ const list = groups.get(lit) ?? [];
647
+ list.push(arm);
648
+ groups.set(lit, list);
649
+ }
650
+
651
+ const out: JsonSchema[] = [...ungrouped];
652
+ for (const [literal, members] of groups) {
653
+ if (members.length < 2) {
654
+ out.push(...members);
655
+ continue;
656
+ }
657
+ // Compute fields shared across every member with the same shape
658
+ // (same single-literal value or same schema). Any field whose
659
+ // single-literal value DIFFERS across members must stay per-arm.
660
+ const sharedFieldNames = new Set<string>(sharedRequiredProperties(members));
661
+ // Drop fields that have differing single-literals — those are the
662
+ // secondary discriminator and must remain on each inner arm.
663
+ for (const name of [...sharedFieldNames]) {
664
+ const valuesAcrossGroup = new Set<string>();
665
+ let hasNonLiteral = false;
666
+ for (const m of members) {
667
+ const v = singleLiteralValue(m, name);
668
+ if (v === undefined) {
669
+ hasNonLiteral = true;
670
+ break;
671
+ }
672
+ valuesAcrossGroup.add(v);
673
+ }
674
+ if (hasNonLiteral) continue;
675
+ if (valuesAcrossGroup.size > 1) sharedFieldNames.delete(name);
676
+ }
677
+
678
+ const sharedProps: Record<string, JsonSchema> = {};
679
+ const sharedRequired: string[] = [];
680
+ const memberProps0 = (members[0]!.properties as Record<string, JsonSchema> | undefined) ?? {};
681
+ const memberRequired0 = (members[0]!.required as string[] | undefined) ?? [];
682
+ for (const name of sharedFieldNames) {
683
+ if (memberProps0[name]) sharedProps[name] = memberProps0[name]!;
684
+ if (memberRequired0.includes(name)) sharedRequired.push(name);
685
+ }
686
+
687
+ // Each inner arm gets the per-arm subset (everything not in shared).
688
+ const innerArms: JsonSchema[] = members.map((m) => {
689
+ const props = (m.properties as Record<string, JsonSchema> | undefined) ?? {};
690
+ const req = (m.required as string[] | undefined) ?? [];
691
+ const innerProps: Record<string, JsonSchema> = {};
692
+ const innerReq: string[] = [];
693
+ for (const [k, v] of Object.entries(props)) {
694
+ if (sharedFieldNames.has(k)) continue;
695
+ innerProps[k] = v;
696
+ if (req.includes(k)) innerReq.push(k);
697
+ }
698
+ const inner: JsonSchema = { type: 'object', properties: innerProps };
699
+ if (innerReq.length > 0) inner.required = innerReq;
700
+ return inner;
701
+ });
702
+
703
+ const grouped: JsonSchema = {
704
+ type: 'object',
705
+ properties: sharedProps,
706
+ ...(sharedRequired.length > 0 ? { required: sharedRequired } : {}),
707
+ oneOf: innerArms,
708
+ };
709
+ // preserve any x-blocks-* annotations that were on each arm by
710
+ // promoting the first member's (they all matched on the
711
+ // primary discriminator literal anyway)
712
+ const m0 = members[0]!;
713
+ const annotationKeys = Object.keys(m0).filter((k) => k.startsWith('x-blocks-'));
714
+ for (const ak of annotationKeys) (grouped as any)[ak] = (m0 as any)[ak];
715
+ out.push(grouped);
716
+ }
717
+
718
+ const next: JsonSchema = { ...schema, oneOf: out };
719
+ return next;
720
+ }
721
+
722
+ function findPrimaryDiscriminator(arms: JsonSchema[]): string | null {
723
+ if (arms.length === 0) return null;
724
+ const counts = new Map<string, Map<string, number>>();
725
+ for (const arm of arms) {
726
+ const props = (arm.properties as Record<string, JsonSchema> | undefined) ?? {};
727
+ for (const [name, sub] of Object.entries(props)) {
728
+ const lit = singleLiteralValueFromSchema(sub);
729
+ if (lit === undefined) continue;
730
+ let m = counts.get(name);
731
+ if (!m) { m = new Map(); counts.set(name, m); }
732
+ m.set(lit, (m.get(lit) ?? 0) + 1);
733
+ }
734
+ }
735
+ let best: string | null = null;
736
+ let bestScore = 0;
737
+ for (const [name, valueCounts] of counts) {
738
+ // score = max group size for this field (bigger collapses first)
739
+ let maxGroup = 0;
740
+ for (const c of valueCounts.values()) if (c > maxGroup) maxGroup = c;
741
+ if (maxGroup > bestScore) { bestScore = maxGroup; best = name; }
742
+ }
743
+ return bestScore >= 2 ? best : null;
744
+ }
745
+
746
+ function singleLiteralValue(arm: JsonSchema, field: string): string | undefined {
747
+ const props = (arm.properties as Record<string, JsonSchema> | undefined) ?? {};
748
+ const sub = props[field];
749
+ if (!sub) return undefined;
750
+ return singleLiteralValueFromSchema(sub);
751
+ }
752
+
753
+ function singleLiteralValueFromSchema(sub: JsonSchema): string | undefined {
754
+ if (typeof sub.const === 'string') return sub.const;
755
+ if (Array.isArray(sub.enum) && sub.enum.length === 1 && typeof sub.enum[0] === 'string') {
756
+ return sub.enum[0];
757
+ }
758
+ return undefined;
759
+ }
760
+
761
+ function sharedRequiredProperties(members: JsonSchema[]): string[] {
762
+ if (members.length === 0) return [];
763
+ const first = members[0]!;
764
+ const firstProps = (first.properties as Record<string, JsonSchema> | undefined) ?? {};
765
+ const candidate = new Set<string>(Object.keys(firstProps));
766
+ for (let i = 1; i < members.length; i++) {
767
+ const props = (members[i]!.properties as Record<string, JsonSchema> | undefined) ?? {};
768
+ for (const name of [...candidate]) {
769
+ if (!(name in props)) candidate.delete(name);
770
+ }
771
+ }
772
+ return [...candidate];
773
+ }
774
+
775
+ /**
776
+ * Generate the OpenRPC spec, validate it, and write it to disk as `blocks.spec.json`.
777
+ * Logs validation warnings but does not fail — the spec is still written.
778
+ */
779
+ export async function writeSpec(
780
+ foundationPath: string,
781
+ outputPath: string,
782
+ loader?: FoundationLoader,
783
+ ): Promise<void> {
784
+ const doc = await generateSpec(foundationPath, loader);
785
+
786
+ // Populate servers array from deployed config if available
787
+ const projectRoot = dirname(dirname(foundationPath));
788
+ const configPath = join(projectRoot, '.blocks-sandbox', 'config.json');
789
+ if (existsSync(configPath)) {
790
+ try {
791
+ const config = JSON.parse(readFileSync(configPath, 'utf-8'));
792
+ if (config.apiUrl) {
793
+ doc.servers = [{ name: config.environment || 'default', url: config.apiUrl }];
794
+ }
795
+ } catch {}
796
+ }
797
+
798
+ // Validate conformance
799
+ const errors = validateSpec(doc);
800
+ if (errors.length > 0) {
801
+ console.warn(`⚠️ OpenRPC spec has ${errors.length} conformance issue(s):`);
802
+ for (const err of errors) {
803
+ console.warn(` ${err.path}: ${err.message}`);
804
+ }
805
+ }
806
+
807
+ mkdirSync(dirname(outputPath), { recursive: true });
808
+ writeFileSync(outputPath, JSON.stringify(doc, null, '\t'));
809
+ }
810
+
811
+ // ── Stub context ────────────────────────────────────────────────────────────
812
+
813
+ /**
814
+ * Create a minimal stub `BlocksContext` so that `ApiNamespace` handler functions
815
+ * can be invoked for method discovery without a real HTTP request.
816
+ */
817
+ function createStubContext() {
818
+ const headers = new Headers();
819
+ return {
820
+ request: {
821
+ headers,
822
+ body: null,
823
+ json: async () => ({}),
824
+ text: async () => '',
825
+ params: {},
826
+ },
827
+ response: {
828
+ headers: new Headers(),
829
+ status: 200,
830
+ send: () => {},
831
+ },
832
+ };
833
+ }