callman-core 1.38.0 → 1.40.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 (309) hide show
  1. package/dist/host/connections.d.ts +7 -0
  2. package/dist/host/connections.d.ts.map +1 -1
  3. package/dist/host/connections.js +8 -0
  4. package/dist/host/connections.js.map +1 -1
  5. package/dist/host/db/connectionMapping.d.ts +3 -0
  6. package/dist/host/db/connectionMapping.d.ts.map +1 -1
  7. package/dist/host/db/connectionMapping.js +3 -0
  8. package/dist/host/db/connectionMapping.js.map +1 -1
  9. package/dist/host/db/dbErrors.d.ts +9 -0
  10. package/dist/host/db/dbErrors.d.ts.map +1 -0
  11. package/dist/host/db/dbErrors.js +126 -0
  12. package/dist/host/db/dbErrors.js.map +1 -0
  13. package/dist/host/db/mongoAdapter.d.ts.map +1 -1
  14. package/dist/host/db/mongoAdapter.js +12 -2
  15. package/dist/host/db/mongoAdapter.js.map +1 -1
  16. package/dist/host/db/mysqlAdapter.d.ts.map +1 -1
  17. package/dist/host/db/mysqlAdapter.js +2 -1
  18. package/dist/host/db/mysqlAdapter.js.map +1 -1
  19. package/dist/host/db/oracleAdapter.d.ts.map +1 -1
  20. package/dist/host/db/oracleAdapter.js +12 -1
  21. package/dist/host/db/oracleAdapter.js.map +1 -1
  22. package/dist/host/db/oracleConnectString.d.ts.map +1 -1
  23. package/dist/host/db/oracleConnectString.js +13 -4
  24. package/dist/host/db/oracleConnectString.js.map +1 -1
  25. package/dist/host/db/queryExecutor.d.ts.map +1 -1
  26. package/dist/host/db/queryExecutor.js +6 -0
  27. package/dist/host/db/queryExecutor.js.map +1 -1
  28. package/dist/host/db/types.d.ts +16 -0
  29. package/dist/host/db/types.d.ts.map +1 -1
  30. package/dist/host/index.d.ts +1 -0
  31. package/dist/host/index.d.ts.map +1 -1
  32. package/dist/host/index.js +2 -0
  33. package/dist/host/index.js.map +1 -1
  34. package/dist/http/transportError.d.ts +15 -0
  35. package/dist/http/transportError.d.ts.map +1 -0
  36. package/dist/http/transportError.js +70 -0
  37. package/dist/http/transportError.js.map +1 -0
  38. package/dist/index.d.ts +1 -0
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +1 -0
  41. package/dist/index.js.map +1 -1
  42. package/dist/mock-engine/auth.d.ts +29 -0
  43. package/dist/mock-engine/auth.d.ts.map +1 -0
  44. package/dist/mock-engine/auth.js +86 -0
  45. package/dist/mock-engine/auth.js.map +1 -0
  46. package/dist/mock-engine/conditions.d.ts +31 -0
  47. package/dist/mock-engine/conditions.d.ts.map +1 -0
  48. package/dist/mock-engine/conditions.js +210 -0
  49. package/dist/mock-engine/conditions.js.map +1 -0
  50. package/dist/mock-engine/constants.d.ts +28 -0
  51. package/dist/mock-engine/constants.d.ts.map +1 -0
  52. package/dist/mock-engine/constants.js +113 -0
  53. package/dist/mock-engine/constants.js.map +1 -0
  54. package/dist/mock-engine/context.d.ts +17 -0
  55. package/dist/mock-engine/context.d.ts.map +1 -0
  56. package/dist/mock-engine/context.js +92 -0
  57. package/dist/mock-engine/context.js.map +1 -0
  58. package/dist/mock-engine/data/actions.d.ts +4 -0
  59. package/dist/mock-engine/data/actions.d.ts.map +1 -0
  60. package/dist/mock-engine/data/actions.js +149 -0
  61. package/dist/mock-engine/data/actions.js.map +1 -0
  62. package/dist/mock-engine/data/query.d.ts +28 -0
  63. package/dist/mock-engine/data/query.d.ts.map +1 -0
  64. package/dist/mock-engine/data/query.js +402 -0
  65. package/dist/mock-engine/data/query.js.map +1 -0
  66. package/dist/mock-engine/data/record-codec.d.ts +28 -0
  67. package/dist/mock-engine/data/record-codec.d.ts.map +1 -0
  68. package/dist/mock-engine/data/record-codec.js +155 -0
  69. package/dist/mock-engine/data/record-codec.js.map +1 -0
  70. package/dist/mock-engine/data/resource-ops.d.ts +53 -0
  71. package/dist/mock-engine/data/resource-ops.d.ts.map +1 -0
  72. package/dist/mock-engine/data/resource-ops.js +461 -0
  73. package/dist/mock-engine/data/resource-ops.js.map +1 -0
  74. package/dist/mock-engine/data/seed.d.ts +21 -0
  75. package/dist/mock-engine/data/seed.d.ts.map +1 -0
  76. package/dist/mock-engine/data/seed.js +239 -0
  77. package/dist/mock-engine/data/seed.js.map +1 -0
  78. package/dist/mock-engine/data/validate-record.d.ts +30 -0
  79. package/dist/mock-engine/data/validate-record.d.ts.map +1 -0
  80. package/dist/mock-engine/data/validate-record.js +165 -0
  81. package/dist/mock-engine/data/validate-record.js.map +1 -0
  82. package/dist/mock-engine/definition.d.ts +31 -0
  83. package/dist/mock-engine/definition.d.ts.map +1 -0
  84. package/dist/mock-engine/definition.js +440 -0
  85. package/dist/mock-engine/definition.js.map +1 -0
  86. package/dist/mock-engine/engine.d.ts +75 -0
  87. package/dist/mock-engine/engine.d.ts.map +1 -0
  88. package/dist/mock-engine/engine.js +828 -0
  89. package/dist/mock-engine/engine.js.map +1 -0
  90. package/dist/mock-engine/faults.d.ts +38 -0
  91. package/dist/mock-engine/faults.d.ts.map +1 -0
  92. package/dist/mock-engine/faults.js +113 -0
  93. package/dist/mock-engine/faults.js.map +1 -0
  94. package/dist/mock-engine/helpers.d.ts +68 -0
  95. package/dist/mock-engine/helpers.d.ts.map +1 -0
  96. package/dist/mock-engine/helpers.js +388 -0
  97. package/dist/mock-engine/helpers.js.map +1 -0
  98. package/dist/mock-engine/import/build.d.ts +16 -0
  99. package/dist/mock-engine/import/build.d.ts.map +1 -0
  100. package/dist/mock-engine/import/build.js +435 -0
  101. package/dist/mock-engine/import/build.js.map +1 -0
  102. package/dist/mock-engine/import/index.d.ts +15 -0
  103. package/dist/mock-engine/import/index.d.ts.map +1 -0
  104. package/dist/mock-engine/import/index.js +92 -0
  105. package/dist/mock-engine/import/index.js.map +1 -0
  106. package/dist/mock-engine/import/openapi.d.ts +10 -0
  107. package/dist/mock-engine/import/openapi.d.ts.map +1 -0
  108. package/dist/mock-engine/import/openapi.js +199 -0
  109. package/dist/mock-engine/import/openapi.js.map +1 -0
  110. package/dist/mock-engine/import/paths.d.ts +16 -0
  111. package/dist/mock-engine/import/paths.d.ts.map +1 -0
  112. package/dist/mock-engine/import/paths.js +64 -0
  113. package/dist/mock-engine/import/paths.js.map +1 -0
  114. package/dist/mock-engine/import/postman.d.ts +21 -0
  115. package/dist/mock-engine/import/postman.d.ts.map +1 -0
  116. package/dist/mock-engine/import/postman.js +241 -0
  117. package/dist/mock-engine/import/postman.js.map +1 -0
  118. package/dist/mock-engine/import/report.d.ts +12 -0
  119. package/dist/mock-engine/import/report.d.ts.map +1 -0
  120. package/dist/mock-engine/import/report.js +39 -0
  121. package/dist/mock-engine/import/report.js.map +1 -0
  122. package/dist/mock-engine/import/types.d.ts +94 -0
  123. package/dist/mock-engine/import/types.d.ts.map +1 -0
  124. package/dist/mock-engine/import/types.js +6 -0
  125. package/dist/mock-engine/import/types.js.map +1 -0
  126. package/dist/mock-engine/index.d.ts +24 -0
  127. package/dist/mock-engine/index.d.ts.map +1 -0
  128. package/dist/mock-engine/index.js +27 -0
  129. package/dist/mock-engine/index.js.map +1 -0
  130. package/dist/mock-engine/legacy-builder.d.ts +27 -0
  131. package/dist/mock-engine/legacy-builder.d.ts.map +1 -0
  132. package/dist/mock-engine/legacy-builder.js +322 -0
  133. package/dist/mock-engine/legacy-builder.js.map +1 -0
  134. package/dist/mock-engine/memory-store.d.ts +73 -0
  135. package/dist/mock-engine/memory-store.d.ts.map +1 -0
  136. package/dist/mock-engine/memory-store.js +314 -0
  137. package/dist/mock-engine/memory-store.js.map +1 -0
  138. package/dist/mock-engine/node/ajv-body-validator.d.ts +13 -0
  139. package/dist/mock-engine/node/ajv-body-validator.d.ts.map +1 -0
  140. package/dist/mock-engine/node/ajv-body-validator.js +158 -0
  141. package/dist/mock-engine/node/ajv-body-validator.js.map +1 -0
  142. package/dist/mock-engine/node/file-store.d.ts +30 -0
  143. package/dist/mock-engine/node/file-store.d.ts.map +1 -0
  144. package/dist/mock-engine/node/file-store.js +116 -0
  145. package/dist/mock-engine/node/file-store.js.map +1 -0
  146. package/dist/mock-engine/node/index.d.ts +5 -0
  147. package/dist/mock-engine/node/index.d.ts.map +1 -0
  148. package/dist/mock-engine/node/index.js +8 -0
  149. package/dist/mock-engine/node/index.js.map +1 -0
  150. package/dist/mock-engine/node/node-http-adapter.d.ts +38 -0
  151. package/dist/mock-engine/node/node-http-adapter.d.ts.map +1 -0
  152. package/dist/mock-engine/node/node-http-adapter.js +215 -0
  153. package/dist/mock-engine/node/node-http-adapter.js.map +1 -0
  154. package/dist/mock-engine/node/vm-regex-runner.d.ts +14 -0
  155. package/dist/mock-engine/node/vm-regex-runner.d.ts.map +1 -0
  156. package/dist/mock-engine/node/vm-regex-runner.js +90 -0
  157. package/dist/mock-engine/node/vm-regex-runner.js.map +1 -0
  158. package/dist/mock-engine/render.d.ts +19 -0
  159. package/dist/mock-engine/render.d.ts.map +1 -0
  160. package/dist/mock-engine/render.js +57 -0
  161. package/dist/mock-engine/render.js.map +1 -0
  162. package/dist/mock-engine/routing.d.ts +48 -0
  163. package/dist/mock-engine/routing.d.ts.map +1 -0
  164. package/dist/mock-engine/routing.js +122 -0
  165. package/dist/mock-engine/routing.js.map +1 -0
  166. package/dist/mock-engine/templates.d.ts +58 -0
  167. package/dist/mock-engine/templates.d.ts.map +1 -0
  168. package/dist/mock-engine/templates.js +300 -0
  169. package/dist/mock-engine/templates.js.map +1 -0
  170. package/dist/mock-engine/testing/index.d.ts +13 -0
  171. package/dist/mock-engine/testing/index.d.ts.map +1 -0
  172. package/dist/mock-engine/testing/index.js +69 -0
  173. package/dist/mock-engine/testing/index.js.map +1 -0
  174. package/dist/mock-engine/types.d.ts +684 -0
  175. package/dist/mock-engine/types.d.ts.map +1 -0
  176. package/dist/mock-engine/types.js +7 -0
  177. package/dist/mock-engine/types.js.map +1 -0
  178. package/dist/mock-engine/util.d.ts +25 -0
  179. package/dist/mock-engine/util.d.ts.map +1 -0
  180. package/dist/mock-engine/util.js +143 -0
  181. package/dist/mock-engine/util.js.map +1 -0
  182. package/dist/openapi/index.d.ts +34 -0
  183. package/dist/openapi/index.d.ts.map +1 -0
  184. package/dist/openapi/index.js +246 -0
  185. package/dist/openapi/index.js.map +1 -0
  186. package/dist/runner/requestRunner.d.ts.map +1 -1
  187. package/dist/runner/requestRunner.js +6 -1
  188. package/dist/runner/requestRunner.js.map +1 -1
  189. package/dist/script/features/random.d.ts.map +1 -1
  190. package/dist/script/features/random.js +3 -1
  191. package/dist/script/features/random.js.map +1 -1
  192. package/dist/script/types.d.ts +5 -0
  193. package/dist/script/types.d.ts.map +1 -1
  194. package/dist/templating/postman-generators.d.ts +5 -0
  195. package/dist/templating/postman-generators.d.ts.map +1 -1
  196. package/dist/templating/postman-generators.js +19 -14
  197. package/dist/templating/postman-generators.js.map +1 -1
  198. package/dist/templating/random-generators.d.ts.map +1 -1
  199. package/dist/templating/random-generators.js +3 -1
  200. package/dist/templating/random-generators.js.map +1 -1
  201. package/dist/templating/random-source.d.ts +12 -0
  202. package/dist/templating/random-source.d.ts.map +1 -0
  203. package/dist/templating/random-source.js +38 -0
  204. package/dist/templating/random-source.js.map +1 -0
  205. package/dist-cjs/host/connections.js +8 -0
  206. package/dist-cjs/host/connections.js.map +1 -1
  207. package/dist-cjs/host/db/connectionMapping.js +3 -0
  208. package/dist-cjs/host/db/connectionMapping.js.map +1 -1
  209. package/dist-cjs/host/db/dbErrors.js +130 -0
  210. package/dist-cjs/host/db/dbErrors.js.map +1 -0
  211. package/dist-cjs/host/db/mongoAdapter.js +12 -2
  212. package/dist-cjs/host/db/mongoAdapter.js.map +1 -1
  213. package/dist-cjs/host/db/mysqlAdapter.js +2 -1
  214. package/dist-cjs/host/db/mysqlAdapter.js.map +1 -1
  215. package/dist-cjs/host/db/oracleAdapter.js +12 -1
  216. package/dist-cjs/host/db/oracleAdapter.js.map +1 -1
  217. package/dist-cjs/host/db/oracleConnectString.js +13 -4
  218. package/dist-cjs/host/db/oracleConnectString.js.map +1 -1
  219. package/dist-cjs/host/db/queryExecutor.js +6 -0
  220. package/dist-cjs/host/db/queryExecutor.js.map +1 -1
  221. package/dist-cjs/host/index.js +5 -2
  222. package/dist-cjs/host/index.js.map +1 -1
  223. package/dist-cjs/http/transportError.js +75 -0
  224. package/dist-cjs/http/transportError.js.map +1 -0
  225. package/dist-cjs/index.js +4 -1
  226. package/dist-cjs/index.js.map +1 -1
  227. package/dist-cjs/mock-engine/auth.js +96 -0
  228. package/dist-cjs/mock-engine/auth.js.map +1 -0
  229. package/dist-cjs/mock-engine/conditions.js +216 -0
  230. package/dist-cjs/mock-engine/conditions.js.map +1 -0
  231. package/dist-cjs/mock-engine/constants.js +116 -0
  232. package/dist-cjs/mock-engine/constants.js.map +1 -0
  233. package/dist-cjs/mock-engine/context.js +99 -0
  234. package/dist-cjs/mock-engine/context.js.map +1 -0
  235. package/dist-cjs/mock-engine/data/actions.js +153 -0
  236. package/dist-cjs/mock-engine/data/actions.js.map +1 -0
  237. package/dist-cjs/mock-engine/data/query.js +413 -0
  238. package/dist-cjs/mock-engine/data/query.js.map +1 -0
  239. package/dist-cjs/mock-engine/data/record-codec.js +165 -0
  240. package/dist-cjs/mock-engine/data/record-codec.js.map +1 -0
  241. package/dist-cjs/mock-engine/data/resource-ops.js +476 -0
  242. package/dist-cjs/mock-engine/data/resource-ops.js.map +1 -0
  243. package/dist-cjs/mock-engine/data/seed.js +246 -0
  244. package/dist-cjs/mock-engine/data/seed.js.map +1 -0
  245. package/dist-cjs/mock-engine/data/validate-record.js +170 -0
  246. package/dist-cjs/mock-engine/data/validate-record.js.map +1 -0
  247. package/dist-cjs/mock-engine/definition.js +454 -0
  248. package/dist-cjs/mock-engine/definition.js.map +1 -0
  249. package/dist-cjs/mock-engine/engine.js +837 -0
  250. package/dist-cjs/mock-engine/engine.js.map +1 -0
  251. package/dist-cjs/mock-engine/faults.js +117 -0
  252. package/dist-cjs/mock-engine/faults.js.map +1 -0
  253. package/dist-cjs/mock-engine/helpers.js +399 -0
  254. package/dist-cjs/mock-engine/helpers.js.map +1 -0
  255. package/dist-cjs/mock-engine/import/build.js +439 -0
  256. package/dist-cjs/mock-engine/import/build.js.map +1 -0
  257. package/dist-cjs/mock-engine/import/index.js +120 -0
  258. package/dist-cjs/mock-engine/import/index.js.map +1 -0
  259. package/dist-cjs/mock-engine/import/openapi.js +203 -0
  260. package/dist-cjs/mock-engine/import/openapi.js.map +1 -0
  261. package/dist-cjs/mock-engine/import/paths.js +72 -0
  262. package/dist-cjs/mock-engine/import/paths.js.map +1 -0
  263. package/dist-cjs/mock-engine/import/postman.js +247 -0
  264. package/dist-cjs/mock-engine/import/postman.js.map +1 -0
  265. package/dist-cjs/mock-engine/import/report.js +43 -0
  266. package/dist-cjs/mock-engine/import/report.js.map +1 -0
  267. package/dist-cjs/mock-engine/import/types.js +7 -0
  268. package/dist-cjs/mock-engine/import/types.js.map +1 -0
  269. package/dist-cjs/mock-engine/index.js +81 -0
  270. package/dist-cjs/mock-engine/index.js.map +1 -0
  271. package/dist-cjs/mock-engine/legacy-builder.js +327 -0
  272. package/dist-cjs/mock-engine/legacy-builder.js.map +1 -0
  273. package/dist-cjs/mock-engine/memory-store.js +318 -0
  274. package/dist-cjs/mock-engine/memory-store.js.map +1 -0
  275. package/dist-cjs/mock-engine/node/ajv-body-validator.js +166 -0
  276. package/dist-cjs/mock-engine/node/ajv-body-validator.js.map +1 -0
  277. package/dist-cjs/mock-engine/node/file-store.js +123 -0
  278. package/dist-cjs/mock-engine/node/file-store.js.map +1 -0
  279. package/dist-cjs/mock-engine/node/index.js +16 -0
  280. package/dist-cjs/mock-engine/node/index.js.map +1 -0
  281. package/dist-cjs/mock-engine/node/node-http-adapter.js +219 -0
  282. package/dist-cjs/mock-engine/node/node-http-adapter.js.map +1 -0
  283. package/dist-cjs/mock-engine/node/vm-regex-runner.js +97 -0
  284. package/dist-cjs/mock-engine/node/vm-regex-runner.js.map +1 -0
  285. package/dist-cjs/mock-engine/render.js +64 -0
  286. package/dist-cjs/mock-engine/render.js.map +1 -0
  287. package/dist-cjs/mock-engine/routing.js +130 -0
  288. package/dist-cjs/mock-engine/routing.js.map +1 -0
  289. package/dist-cjs/mock-engine/templates.js +308 -0
  290. package/dist-cjs/mock-engine/templates.js.map +1 -0
  291. package/dist-cjs/mock-engine/testing/index.js +72 -0
  292. package/dist-cjs/mock-engine/testing/index.js.map +1 -0
  293. package/dist-cjs/mock-engine/types.js +8 -0
  294. package/dist-cjs/mock-engine/types.js.map +1 -0
  295. package/dist-cjs/mock-engine/util.js +166 -0
  296. package/dist-cjs/mock-engine/util.js.map +1 -0
  297. package/dist-cjs/openapi/index.js +257 -0
  298. package/dist-cjs/openapi/index.js.map +1 -0
  299. package/dist-cjs/runner/requestRunner.js +6 -1
  300. package/dist-cjs/runner/requestRunner.js.map +1 -1
  301. package/dist-cjs/script/features/random.js +3 -1
  302. package/dist-cjs/script/features/random.js.map +1 -1
  303. package/dist-cjs/templating/postman-generators.js +21 -15
  304. package/dist-cjs/templating/postman-generators.js.map +1 -1
  305. package/dist-cjs/templating/random-generators.js +3 -1
  306. package/dist-cjs/templating/random-generators.js.map +1 -1
  307. package/dist-cjs/templating/random-source.js +44 -0
  308. package/dist-cjs/templating/random-source.js.map +1 -0
  309. package/package.json +29 -1
@@ -0,0 +1,828 @@
1
+ // The mock engine pipeline. Hosts (backend Express, desktop node:http,
2
+ // renderer sandbox) handle platform gates first — per-IP fuse, lookup,
3
+ // 404/410, X-Mock-Token, rate limits — then call `executeMockRequest`, and
4
+ // carry out the returned delay/transport (respond, hang, drop).
5
+ //
6
+ // Order: session → /__callman control → route match → faults(before) →
7
+ // sign-in requirement → body validation → variants → execute (resource
8
+ // endpoint | data action | legacy builder | static/template) →
9
+ // faults(after) → delay. Store failures surface as 503, never as a fake 2xx.
10
+ import { bearerToken, generateMockSecret, signMockJwt, verifyMockJwt, verifyPassword } from "./auth.js";
11
+ import { evaluateCondition } from "./conditions.js";
12
+ import { MOCK_CONTROL_PREFIX, MOCK_SESSION_HEADER, MOCK_SESSION_ID_PATTERN } from "./constants.js";
13
+ import { MockOpBudgetExceeded } from "./context.js";
14
+ import { runDataAction } from "./data/actions.js";
15
+ import { andFilters } from "./data/query.js";
16
+ import { canonicalKey, keyFromParam } from "./data/record-codec.js";
17
+ import { createRecord, deleteRecords, findByLookup, getRecord, listRecords, notFoundResult, presentRecord, resourceByName, validationFailure, writeRecord, } from "./data/resource-ops.js";
18
+ import { buildSeedPlan } from "./data/seed.js";
19
+ import { evaluateFaults } from "./faults.js";
20
+ import { applyLegacyMutationsInMemory, renderLegacyBuilder } from "./legacy-builder.js";
21
+ import { buildRendered, jsonResponse, renderOutcome, renderResponseConfig } from "./render.js";
22
+ import { compileRouteTable, matchRouteTable } from "./routing.js";
23
+ import { templateReferences } from "./templates.js";
24
+ import { isPlainObject, lookupHeader, toHex, utf8 } from "./util.js";
25
+ export const compileMockDefinition = (definition) => ({
26
+ definition,
27
+ table: compileRouteTable(definition),
28
+ });
29
+ /** Small LRU for compiled definitions, keyed by the host (e.g. `${mockId}:${rev}`). */
30
+ export const createCompiledMockCache = (max = 500) => {
31
+ const cache = new Map();
32
+ return {
33
+ get(key, factory) {
34
+ const hit = cache.get(key);
35
+ if (hit) {
36
+ cache.delete(key);
37
+ cache.set(key, hit);
38
+ return hit;
39
+ }
40
+ const compiled = compileMockDefinition(factory());
41
+ cache.set(key, compiled);
42
+ if (cache.size > max) {
43
+ const oldest = cache.keys().next().value;
44
+ if (oldest !== undefined)
45
+ cache.delete(oldest);
46
+ }
47
+ return compiled;
48
+ },
49
+ clear() {
50
+ cache.clear();
51
+ },
52
+ };
53
+ };
54
+ // ── Helpers ───────────────────────────────────────────────────────────
55
+ const BODY_METHODS = new Set(["POST", "PUT", "PATCH"]);
56
+ const STATUS_TEXT = {
57
+ 400: "Bad Request", 401: "Unauthorized", 403: "Forbidden", 404: "Not Found", 408: "Request Timeout",
58
+ 409: "Conflict", 418: "I'm a teapot", 422: "Unprocessable Entity", 429: "Too Many Requests",
59
+ 500: "Internal Server Error", 502: "Bad Gateway", 503: "Service Unavailable", 504: "Gateway Timeout",
60
+ };
61
+ const delayMs = (delay, ctx) => {
62
+ if (!delay || delay.mode === "none")
63
+ return 0;
64
+ if (delay.mode === "fixed")
65
+ return Math.max(0, delay.ms);
66
+ const min = Math.max(0, delay.minMs);
67
+ const max = Math.max(min, delay.maxMs);
68
+ return min + Math.floor(ctx.random.next() * (max - min + 1));
69
+ };
70
+ const secretCache = new Map();
71
+ const signingSecret = async (definition, ctx) => {
72
+ if (definition.userAuth?.secret)
73
+ return definition.userAuth.secret;
74
+ // A host should always store a generated secret; this keeps old/partial
75
+ // definitions working with a stable per-mock fallback.
76
+ const cached = secretCache.get(ctx.mockId);
77
+ if (cached)
78
+ return cached;
79
+ const derived = toHex(await ctx.crypto.sha256(utf8(`callman-mock:${ctx.mockId}`)));
80
+ if (secretCache.size > 1000)
81
+ secretCache.clear();
82
+ secretCache.set(ctx.mockId, derived);
83
+ return derived;
84
+ };
85
+ /** Wrap a store so one request cannot run more than `max` operations. */
86
+ const budgetStore = (store, max, trace) => {
87
+ const tick = () => {
88
+ trace.storeOps += 1;
89
+ if (trace.storeOps > max)
90
+ throw new MockOpBudgetExceeded();
91
+ };
92
+ return {
93
+ ensureNamespace: (...args) => (tick(), store.ensureNamespace(...args)),
94
+ resetNamespace: (...args) => (tick(), store.resetNamespace(...args)),
95
+ dropNamespace: (...args) => (tick(), store.dropNamespace(...args)),
96
+ listNamespaces: (...args) => (tick(), store.listNamespaces(...args)),
97
+ insert: (...args) => (tick(), store.insert(...args)),
98
+ get: (...args) => (tick(), store.get(...args)),
99
+ getMany: (...args) => (tick(), store.getMany(...args)),
100
+ find: (...args) => (tick(), store.find(...args)),
101
+ count: (...args) => (tick(), store.count(...args)),
102
+ countNamespace: (...args) => (tick(), store.countNamespace(...args)),
103
+ replace: (...args) => (tick(), store.replace(...args)),
104
+ delete: (...args) => (tick(), store.delete(...args)),
105
+ clearResource: (...args) => (tick(), store.clearResource(...args)),
106
+ nextSequence: (...args) => (tick(), store.nextSequence(...args)),
107
+ bumpSequence: (...args) => (tick(), store.bumpSequence(...args)),
108
+ };
109
+ };
110
+ /** Seed plan for a namespace (deterministic per mock). */
111
+ export const seedPlanFor = (definition, ctx) => buildSeedPlan(definition, { mockId: ctx.mockId, now: ctx.clock.now(), crypto: ctx.crypto, maxGenerate: ctx.limits.maxSeedGenerate });
112
+ const sessionIdleMs = (definition, ctx) => Math.max(5, Math.min(1440, definition.sessions?.idleTtlMinutes ?? Math.round(ctx.limits.sessionIdleTtlMs / 60_000))) * 60_000;
113
+ const resultOf = (response, trace, delay = 0, transport = { kind: "respond" }, maxDelay = 60_000) => ({ response, delayMs: Math.min(maxDelay, Math.max(0, delay)), transport, trace });
114
+ // ── Pipeline ──────────────────────────────────────────────────────────
115
+ export const executeMockRequest = async (compiled, req, ctx) => {
116
+ const trace = { faults: [], storeOps: 0 };
117
+ const limits = ctx.limits;
118
+ const maxBody = limits.maxResponseBodyBytes;
119
+ const definition = compiled.definition;
120
+ const tpl = { random: ctx.random, now: () => ctx.clock.now() };
121
+ const method = req.method.toUpperCase();
122
+ const done = (response, delay = 0, transport = { kind: "respond" }) => {
123
+ if (method === "HEAD")
124
+ response = { ...response, body: "" };
125
+ return resultOf(response, trace, delay, transport, limits.maxDelayMs);
126
+ };
127
+ try {
128
+ // 1. Session namespace
129
+ const sessionsEnabled = definition.sessions?.enabled !== false;
130
+ const rawSession = req.headers[MOCK_SESSION_HEADER];
131
+ let sessionId = null;
132
+ if (sessionsEnabled && rawSession !== undefined && rawSession !== "") {
133
+ if (!MOCK_SESSION_ID_PATTERN.test(rawSession)) {
134
+ trace.failureReason = "bad-session";
135
+ return done(jsonResponse(400, { error: "Invalid X-Mock-Session header", hint: "Use 1-64 letters, digits, '.', '_', ':' or '-'" }, maxBody));
136
+ }
137
+ sessionId = rawSession;
138
+ trace.session = sessionId;
139
+ }
140
+ const scope = { mockId: ctx.mockId, ns: sessionId ? `s:${sessionId}` : "_" };
141
+ const store = ctx.store ? budgetStore(ctx.store, limits.maxDataOpsPerRequest, trace) : undefined;
142
+ let nsReady = null;
143
+ const ensureNamespace = () => {
144
+ if (!store)
145
+ return Promise.resolve("ok");
146
+ if (!nsReady) {
147
+ nsReady = store
148
+ .ensureNamespace(scope, () => seedPlanFor(definition, ctx), {
149
+ isSession: sessionId !== null,
150
+ maxSessions: limits.maxSessions,
151
+ idleTtlMs: sessionIdleMs(definition, ctx),
152
+ now: ctx.clock.now(),
153
+ })
154
+ .then((outcome) => (outcome.ok ? "ok" : "session-limit"));
155
+ }
156
+ return nsReady;
157
+ };
158
+ const sessionLimit = () => {
159
+ trace.failureReason = "session-limit";
160
+ return done(jsonResponse(429, { error: "Too many active test sessions", limit: limits.maxSessions }, maxBody, { "x-mock-limit-reason": "session-limit" }));
161
+ };
162
+ // 2. Control endpoints
163
+ if (req.path === MOCK_CONTROL_PREFIX || req.path.startsWith(`${MOCK_CONTROL_PREFIX}/`)) {
164
+ trace.virtual = { kind: "control" };
165
+ return done(await handleControl(req, definition, ctx, store, scope, sessionId, maxBody));
166
+ }
167
+ // Lazily verified sign-in state.
168
+ let authPromise = null;
169
+ const getAuth = () => {
170
+ if (!authPromise)
171
+ authPromise = resolveAuth(definition, req, ctx);
172
+ return authPromise;
173
+ };
174
+ const baseCondition = (params, auth) => ({
175
+ params,
176
+ query: req.query,
177
+ headers: req.headers,
178
+ body: req.body,
179
+ session: { id: sessionId },
180
+ ...(auth ? { auth: { authenticated: auth.authenticated, sub: auth.sub, claims: auth.claims } } : {}),
181
+ });
182
+ // 3. Route match
183
+ let matched = null;
184
+ for (const candidate of matchRouteTable(compiled.table, method, req.path)) {
185
+ const target = candidate.entry.target;
186
+ if (target.kind === "route" && target.route.match) {
187
+ const auth = conditionNeedsAuth(target.route.match) ? await getAuth() : undefined;
188
+ if (!evaluateCondition(target.route.match, baseCondition(candidate.params, auth), ctx.regex))
189
+ continue;
190
+ }
191
+ matched = candidate;
192
+ break;
193
+ }
194
+ if (!matched) {
195
+ trace.failureReason = "no-route";
196
+ return done(jsonResponse(404, { error: "No matching route", hint: `Configure a route for ${method} ${req.path}` }, maxBody));
197
+ }
198
+ const { entry, params } = matched;
199
+ const target = entry.target;
200
+ // Target-specific settings
201
+ let counterKey = "";
202
+ let faultConfig;
203
+ let delayConfig;
204
+ let requirement = "none";
205
+ let validation;
206
+ const protect = definition.userAuth?.enabled ? definition.userAuth.protect : "none";
207
+ if (target.kind === "route") {
208
+ counterKey = target.route.routeId;
209
+ trace.routeId = target.route.routeId;
210
+ faultConfig = target.route.faults;
211
+ delayConfig = target.route.behavior?.delay;
212
+ requirement = target.route.auth ?? (protect === "all" ? "user" : "none");
213
+ validation = target.route.requestValidation;
214
+ }
215
+ else if (target.kind === "resource" || target.kind === "nested") {
216
+ const resource = target.kind === "resource" ? target.resource : target.child;
217
+ const endpointName = target.kind === "resource" ? target.endpoint : target.endpoint;
218
+ const endpoint = resource.endpoints[endpointName];
219
+ counterKey = `res:${resource.resourceId}:${endpointName}`;
220
+ trace.virtual = { kind: "resource", resource: resource.name, endpoint: endpointName };
221
+ faultConfig = endpoint.faults;
222
+ delayConfig = endpoint.delay;
223
+ requirement = endpoint.auth ?? resource.auth ?? (protect !== "none" || resource.ownerField ? "user" : "none");
224
+ validation = endpoint.requestValidation;
225
+ }
226
+ else {
227
+ counterKey = `auth:${target.op}`;
228
+ trace.virtual = { kind: "auth", endpoint: target.op };
229
+ requirement = target.op === "me" ? "user" : "none";
230
+ }
231
+ const callCount = { value: null, computed: false };
232
+ const authForFaults = faultsNeedAuth(faultConfig, definition.faults) ? await getAuth() : undefined;
233
+ const faultInput = {
234
+ configs: [faultConfig, definition.faults],
235
+ counterKey,
236
+ scope,
237
+ conditionContext: baseCondition(params, authForFaults),
238
+ callCount,
239
+ };
240
+ // 4. Faults (before)
241
+ const before = await evaluateFaults(ctx, { ...faultInput, phase: "before" });
242
+ trace.faults.push(...before.applied);
243
+ const baseDelay = delayMs(delayConfig, ctx);
244
+ if (before.terminal) {
245
+ return applyTerminalFault(before, baseDelay + before.extraDelayMs, req, params, sessionId, ctx, tpl, trace, done, maxBody);
246
+ }
247
+ // 5. Sign-in requirement
248
+ let auth;
249
+ if (requirement !== "none") {
250
+ auth = await getAuth();
251
+ if (!auth.authenticated) {
252
+ trace.failureReason = "auth-required";
253
+ return done(renderOutcome(definition.userAuth?.responses?.unauthorized, {
254
+ statusCode: 401,
255
+ body: { error: "Unauthorized", message: auth.tokenError ? `Bearer token ${auth.tokenError}` : "Sign in and send Authorization: Bearer <token>" },
256
+ headers: { "www-authenticate": 'Bearer realm="mock"' + (auth.tokenError ? ', error="invalid_token"' : "") },
257
+ }, scopeFor(req, params, sessionId, ctx, auth), tpl, maxBody), baseDelay + before.extraDelayMs);
258
+ }
259
+ trace.authSubject = auth.sub;
260
+ if (typeof requirement === "object" && Array.isArray(requirement.roles) && requirement.roles.length > 0) {
261
+ const role = auth.claims?.role;
262
+ if (typeof role !== "string" || !requirement.roles.includes(role)) {
263
+ trace.failureReason = "forbidden";
264
+ return done(renderOutcome(definition.userAuth?.responses?.forbidden, { statusCode: 403, body: { error: "Forbidden" } }, scopeFor(req, params, sessionId, ctx, auth), tpl, maxBody), baseDelay + before.extraDelayMs);
265
+ }
266
+ }
267
+ }
268
+ else if (definition.userAuth?.enabled && lookupHeader(req.headers, "authorization")) {
269
+ auth = await getAuth();
270
+ if (auth.authenticated)
271
+ trace.authSubject = auth.sub;
272
+ }
273
+ // 6. Body validation
274
+ if (validation?.mode === "schema" && validation.schema !== undefined && BODY_METHODS.has(method) && ctx.bodyValidator) {
275
+ const invalidFields = ctx.bodyValidator(validation.schema, req.body);
276
+ if (invalidFields) {
277
+ trace.failureReason = "request-validation-failed";
278
+ return done(jsonResponse(400, { error: "Request body validation failed", invalidFields }, maxBody), baseDelay + before.extraDelayMs);
279
+ }
280
+ }
281
+ const templateScope = scopeFor(req, params, sessionId, ctx, auth);
282
+ if (callCount.computed && callCount.value !== null)
283
+ templateScope.call = { count: callCount.value };
284
+ const dataRuntime = () => ({
285
+ definition,
286
+ scope,
287
+ store: store,
288
+ ctx,
289
+ tpl,
290
+ templateScope,
291
+ mountPath: req.mountPath,
292
+ requestPath: req.path,
293
+ });
294
+ const needsStore = () => store ? null : jsonResponse(500, { error: "This mock has data endpoints but no data store is configured" }, maxBody);
295
+ // Load auth.user lazily when a template asks for it.
296
+ const loadAuthUser = async (templates) => {
297
+ if (!auth?.authenticated || !auth.sub || !store || !definition.userAuth?.enabled)
298
+ return;
299
+ if (!templateReferences(templates, "auth.user"))
300
+ return;
301
+ const users = resourceByName(definition, definition.userAuth.usersResource);
302
+ if (!users)
303
+ return;
304
+ if ((await ensureNamespace()) === "session-limit")
305
+ return;
306
+ const doc = await store.get(scope, users.resourceId, keyFromParam(users, auth.sub));
307
+ const user = doc ? presentRecord(users, doc) : null;
308
+ templateScope.auth.user = user;
309
+ };
310
+ // 7–8. Variants, then execute
311
+ let response;
312
+ if (target.kind === "route") {
313
+ const route = target.route;
314
+ let variantResponse = null;
315
+ if (route.variants && route.variants.length > 0) {
316
+ const needAuth = route.variants.some((variant) => conditionNeedsAuth(variant.condition));
317
+ const condAuth = needAuth ? (auth ?? (await getAuth())) : auth;
318
+ const condition = baseCondition(params, condAuth);
319
+ const ordered = route.variants.slice().sort((a, b) => a.priority - b.priority);
320
+ for (const variant of ordered) {
321
+ if (evaluateCondition(variant.condition, condition, ctx.regex)) {
322
+ variantResponse = variant.response;
323
+ trace.variantId = variant.variantId;
324
+ break;
325
+ }
326
+ }
327
+ }
328
+ if (variantResponse) {
329
+ response = await renderRouteResponse(variantResponse, templateScope, ctx, tpl, trace, maxBody, loadAuthUser);
330
+ }
331
+ else if (route.action) {
332
+ const missing = needsStore();
333
+ if (missing)
334
+ return done(missing);
335
+ if ((await ensureNamespace()) === "session-limit")
336
+ return sessionLimit();
337
+ const overrides = { ...(route.action.responses ?? {}) };
338
+ if (!overrides.success && route.response.type === "template") {
339
+ overrides.success = { statusCode: route.response.statusCode, headers: route.response.headers, body: route.response.body };
340
+ }
341
+ await loadAuthUser(overrides);
342
+ const op = await runDataAction(dataRuntime(), route.action, req.rawQuery);
343
+ trace.action = { operation: route.action.operation, resource: route.action.resource, outcome: op.outcome, affected: op.affected };
344
+ response = renderOp(op, overrides, templateScope, tpl, maxBody, route.action.operation === "find" ? undefined : actionDefaults(route.action.operation, op));
345
+ if (op.status >= 400)
346
+ trace.failureReason = `data-${op.outcome}`;
347
+ }
348
+ else {
349
+ response = await renderRouteResponse(route.response, templateScope, ctx, tpl, trace, maxBody, loadAuthUser);
350
+ }
351
+ }
352
+ else if (target.kind === "resource" || target.kind === "nested") {
353
+ const missing = needsStore();
354
+ if (missing)
355
+ return done(missing);
356
+ if ((await ensureNamespace()) === "session-limit")
357
+ return sessionLimit();
358
+ const rt = dataRuntime();
359
+ const op = await runResourceEndpoint(rt, target, params, req);
360
+ const resource = target.kind === "resource" ? target.resource : target.child;
361
+ const endpointName = target.endpoint;
362
+ const overrides = resource.endpoints[endpointName].responses ?? {};
363
+ await loadAuthUser(overrides);
364
+ trace.action = { operation: endpointName, resource: resource.name, outcome: op.outcome, affected: op.affected };
365
+ response = renderOp(op, overrides, templateScope, tpl, maxBody);
366
+ if (op.status >= 400)
367
+ trace.failureReason = `data-${op.outcome}`;
368
+ }
369
+ else {
370
+ const missing = needsStore();
371
+ if (missing)
372
+ return done(missing);
373
+ if ((await ensureNamespace()) === "session-limit")
374
+ return sessionLimit();
375
+ response = await runAuthEndpoint(target.op, dataRuntime(), req, auth, trace, maxBody);
376
+ }
377
+ // 9. Faults (after) — the handler ran; the answer is lost or delayed.
378
+ const after = await evaluateFaults(ctx, { ...faultInput, phase: "after" });
379
+ trace.faults.push(...after.applied);
380
+ const totalDelay = baseDelay + before.extraDelayMs + after.extraDelayMs;
381
+ if (after.terminal)
382
+ return applyTerminalFault(after, totalDelay, req, params, sessionId, ctx, tpl, trace, done, maxBody);
383
+ return done(response, totalDelay);
384
+ }
385
+ catch (error) {
386
+ if (error instanceof MockOpBudgetExceeded) {
387
+ trace.failureReason = "data-budget";
388
+ return done(jsonResponse(500, { error: "This request needed too many data operations" }, maxBody));
389
+ }
390
+ trace.failureReason = "data-error";
391
+ ctx.logger?.error("Mock engine request failed", {
392
+ mockId: ctx.mockId,
393
+ path: req.path,
394
+ message: error instanceof Error ? error.message : String(error),
395
+ });
396
+ return done(jsonResponse(503, { error: "Mock data store unavailable" }, maxBody));
397
+ }
398
+ };
399
+ // ── Pieces of the pipeline ────────────────────────────────────────────
400
+ const conditionNeedsAuth = (condition) => !!condition && condition.rules.some((rule) => rule.field === "auth" || rule.field.startsWith("auth."));
401
+ const faultsNeedAuth = (...configs) => configs.some((config) => config?.rules.some((rule) => rule.when?.condition && conditionNeedsAuth(rule.when.condition)));
402
+ const scopeFor = (req, params, sessionId, ctx, auth) => ({
403
+ request: {
404
+ method: req.method.toUpperCase(),
405
+ path: req.path,
406
+ url: `${req.mountPath}${req.path}`,
407
+ params,
408
+ query: req.query,
409
+ headers: req.headers,
410
+ body: req.body,
411
+ },
412
+ state: ctx.legacyState?.snapshot() ?? {},
413
+ session: { id: sessionId },
414
+ mock: { id: ctx.mockId, ...(ctx.mockName ? { name: ctx.mockName } : {}) },
415
+ auth: auth?.authenticated
416
+ ? { authenticated: true, sub: auth.sub, claims: auth.claims ?? {} }
417
+ : { authenticated: false },
418
+ });
419
+ const resolveAuth = async (definition, req, ctx) => {
420
+ const config = definition.userAuth;
421
+ if (!config?.enabled)
422
+ return { authenticated: false };
423
+ const token = bearerToken(lookupHeader(req.headers, "authorization"));
424
+ if (!token)
425
+ return { authenticated: false };
426
+ const verified = await verifyMockJwt(ctx.crypto, await signingSecret(definition, ctx), token, {
427
+ nowSeconds: Math.floor(ctx.clock.now() / 1000),
428
+ issuer: config.issuer ?? `callman-mock/${ctx.mockId}`,
429
+ audience: config.audience,
430
+ });
431
+ if (!verified.ok)
432
+ return { authenticated: false, tokenError: verified.reason === "expired" ? "has expired" : "is invalid" };
433
+ return { authenticated: true, sub: verified.claims.sub, claims: verified.claims };
434
+ };
435
+ const renderRouteResponse = async (config, scope, ctx, tpl, trace, maxBody, loadAuthUser) => {
436
+ if (config.type === "builder") {
437
+ const state = ctx.legacyState?.snapshot() ?? {};
438
+ const built = renderLegacyBuilder(config, {
439
+ request: scope.request,
440
+ state,
441
+ random: ctx.random,
442
+ now: () => ctx.clock.now(),
443
+ maxBodyBytes: maxBody,
444
+ });
445
+ if (built.mutations && built.mutations.length > 0) {
446
+ if (!ctx.legacyState)
447
+ return jsonResponse(500, { error: "This mock's state cannot be saved here" }, maxBody);
448
+ try {
449
+ await ctx.legacyState.apply(built.mutations);
450
+ trace.legacyStateChanged = true;
451
+ applyLegacyMutationsInMemory(state, built.mutations);
452
+ }
453
+ catch (error) {
454
+ trace.failureReason = "state-write-failed";
455
+ ctx.logger?.error("Mock state mutation failed", { mockId: ctx.mockId, message: error instanceof Error ? error.message : String(error) });
456
+ return jsonResponse(500, { error: "Mock state could not be saved" }, maxBody);
457
+ }
458
+ }
459
+ return built.rendered;
460
+ }
461
+ if (config.type === "template")
462
+ await loadAuthUser([config.body, config.headers]);
463
+ return renderResponseConfig(config, scope, tpl, maxBody);
464
+ };
465
+ const actionDefaults = (operation, op) => {
466
+ if (op.status >= 400)
467
+ return undefined;
468
+ switch (operation) {
469
+ case "count":
470
+ return { statusCode: 200, body: { count: op.scope.total ?? 0 } };
471
+ case "delete":
472
+ return { statusCode: 204, body: null };
473
+ default:
474
+ return undefined;
475
+ }
476
+ };
477
+ const renderOp = (op, overrides, scope, tpl, maxBody, defaults) => {
478
+ const outcome = op.outcome;
479
+ const override = overrides[outcome] ?? (outcome === "created" ? overrides.success : undefined);
480
+ return renderOutcome(override, { statusCode: defaults?.statusCode ?? op.status, body: defaults?.body ?? op.body, headers: op.headers }, { ...scope, ...op.scope }, tpl, maxBody);
481
+ };
482
+ const coerceParam = (resource, field, raw) => {
483
+ const declared = resource.fields.find((f) => f.name === field);
484
+ if ((declared?.type === "integer" || declared?.type === "number") && /^-?\d+(\.\d+)?$/.test(raw))
485
+ return Number(raw);
486
+ return raw;
487
+ };
488
+ const runResourceEndpoint = async (rt, target, params, req) => {
489
+ const ownerStamp = (resource) => {
490
+ const sub = rt.templateScope.auth?.sub;
491
+ if (!resource.ownerField || sub === undefined)
492
+ return {};
493
+ return { [resource.ownerField]: coerceParam(resource, resource.ownerField, sub) };
494
+ };
495
+ if (target.kind === "nested") {
496
+ const parentValue = params[target.field] ?? "";
497
+ const parent = await findByLookup(rt, target.parent, parentValue);
498
+ if (!parent)
499
+ return notFoundResult(target.parent);
500
+ const parentId = parent.data[target.parent.idField];
501
+ if (target.endpoint === "list") {
502
+ const key = canonicalKey(parentId);
503
+ return listRecords(rt, target.child, req.rawQuery, key ? { ref: target.field, keys: [key] } : undefined);
504
+ }
505
+ return createRecord(rt, target.child, req.body, { ...ownerStamp(target.child), [target.field]: parentId });
506
+ }
507
+ const { resource, endpoint } = target;
508
+ const lookup = resource.lookupField ?? resource.idField;
509
+ const value = params[lookup] ?? "";
510
+ switch (endpoint) {
511
+ case "list":
512
+ return listRecords(rt, resource, req.rawQuery);
513
+ case "get":
514
+ return getRecord(rt, resource, value, req.rawQuery);
515
+ case "create":
516
+ return createRecord(rt, resource, req.body, ownerStamp(resource));
517
+ case "replace":
518
+ case "patch": {
519
+ const doc = await findByLookup(rt, resource, value);
520
+ if (!doc)
521
+ return notFoundResult(resource);
522
+ return writeRecord(rt, resource, doc, req.body, endpoint === "replace" ? "replace" : "patch");
523
+ }
524
+ case "delete": {
525
+ const doc = await findByLookup(rt, resource, value);
526
+ return deleteRecords(rt, resource, doc ? [doc] : []);
527
+ }
528
+ default:
529
+ return notFoundResult(resource);
530
+ }
531
+ };
532
+ const issueToken = async (definition, ctx, users, user) => {
533
+ const config = definition.userAuth;
534
+ const nowSeconds = Math.floor(ctx.clock.now() / 1000);
535
+ const claims = {
536
+ sub: String(user[users.idField]),
537
+ iat: nowSeconds,
538
+ exp: nowSeconds + config.tokenTtlSeconds,
539
+ iss: config.issuer ?? `callman-mock/${ctx.mockId}`,
540
+ ...(config.audience ? { aud: config.audience } : {}),
541
+ };
542
+ if (config.roleClaim && user[config.roleClaim] !== undefined)
543
+ claims.role = user[config.roleClaim];
544
+ for (const [claim, field] of Object.entries(config.claims ?? {})) {
545
+ if (user[field] !== undefined && !["sub", "iat", "exp", "iss", "aud"].includes(claim))
546
+ claims[claim] = user[field];
547
+ }
548
+ const token = await signMockJwt(ctx.crypto, await signingSecret(definition, ctx), claims);
549
+ return { token, expiresIn: config.tokenTtlSeconds };
550
+ };
551
+ const runAuthEndpoint = async (op, rt, req, auth, trace, maxBody) => {
552
+ const definition = rt.definition;
553
+ const config = definition.userAuth;
554
+ const users = resourceByName(definition, config.usersResource);
555
+ if (!users)
556
+ return jsonResponse(500, { error: `Sign-in is configured for an unknown resource "${config.usersResource}"` }, maxBody);
557
+ const scope = rt.templateScope;
558
+ if (op === "me") {
559
+ if (!auth?.sub)
560
+ return jsonResponse(401, { error: "Unauthorized" }, maxBody);
561
+ const doc = await rt.store.get(rt.scope, users.resourceId, keyFromParam(users, auth.sub));
562
+ if (!doc) {
563
+ trace.failureReason = "auth-required";
564
+ return jsonResponse(401, { error: "Unauthorized", message: "The signed-in user no longer exists" }, maxBody);
565
+ }
566
+ const user = presentRecord(users, doc);
567
+ return buildRendered(200, {}, user, maxBody);
568
+ }
569
+ if (op === "register") {
570
+ const created = await createRecord(rt, users, req.body);
571
+ if (created.outcome !== "created") {
572
+ trace.failureReason = `data-${created.outcome}`;
573
+ return renderOutcome(undefined, { statusCode: created.status, body: created.body }, { ...scope, ...created.scope }, rt.tpl, maxBody);
574
+ }
575
+ const user = created.scope.record;
576
+ const stored = await rt.store.get(rt.scope, users.resourceId, keyFromParam(users, String(user[users.idField])));
577
+ const { token, expiresIn } = await issueToken(definition, rt.ctx, users, stored?.data ?? user);
578
+ trace.authSubject = String(user[users.idField]);
579
+ return renderOutcome(config.responses?.loginSuccess, { statusCode: 201, body: { accessToken: token, tokenType: "Bearer", expiresIn, user } }, { ...scope, token, expiresIn, record: user }, rt.tpl, maxBody);
580
+ }
581
+ // login
582
+ const body = isPlainObject(req.body) ? req.body : {};
583
+ const identity = body[config.identityField];
584
+ const password = body[config.passwordField];
585
+ if (typeof identity !== "string" || identity === "" || typeof password !== "string") {
586
+ trace.failureReason = "data-validationError";
587
+ const invalid = [
588
+ ...(typeof identity !== "string" || identity === "" ? [{ field: config.identityField, message: "is required" }] : []),
589
+ ...(typeof password !== "string" ? [{ field: config.passwordField, message: "is required" }] : []),
590
+ ];
591
+ const failed = validationFailure(invalid);
592
+ return buildRendered(400, {}, failed.body, maxBody);
593
+ }
594
+ const identityField = users.fields.find((f) => f.name === config.identityField);
595
+ const filter = identityField?.uniqueCaseInsensitive
596
+ ? { field: config.identityField, op: "like", value: identity }
597
+ : { field: config.identityField, op: "eq", value: identity };
598
+ const { docs } = await rt.store.find(rt.scope, users.resourceId, { filter: andFilters(filter), limit: 5, noTotal: true });
599
+ const candidate = docs.find((doc) => {
600
+ const value = doc.data[config.identityField];
601
+ return typeof value === "string" && (identityField?.uniqueCaseInsensitive ? value.toLowerCase() === identity.toLowerCase() : value === identity);
602
+ });
603
+ const ok = candidate ? await verifyPassword(rt.ctx.crypto, candidate.data[config.passwordField], password) : false;
604
+ if (!candidate || !ok) {
605
+ trace.failureReason = "auth-failed";
606
+ return renderOutcome(config.responses?.loginFailure, { statusCode: 401, body: { error: "Invalid credentials" } }, scope, rt.tpl, maxBody);
607
+ }
608
+ const user = presentRecord(users, candidate);
609
+ const { token, expiresIn } = await issueToken(definition, rt.ctx, users, candidate.data);
610
+ trace.authSubject = String(candidate.data[users.idField]);
611
+ return renderOutcome(config.responses?.loginSuccess, { statusCode: 200, body: { accessToken: token, tokenType: "Bearer", expiresIn, user } }, { ...scope, token, expiresIn, record: user }, rt.tpl, maxBody);
612
+ };
613
+ const applyTerminalFault = (decision, delay, req, params, sessionId, ctx, tpl, trace, done, maxBody) => {
614
+ trace.failureReason = "fault-injected";
615
+ const terminal = decision.terminal;
616
+ if (terminal.kind === "timeout") {
617
+ return done(jsonResponse(504, { error: "Gateway Timeout" }, maxBody), 0, { kind: "hang", ms: terminal.ms });
618
+ }
619
+ if (terminal.kind === "drop") {
620
+ return done(jsonResponse(502, { error: "Connection dropped" }, maxBody), delay, { kind: "drop", mode: terminal.mode });
621
+ }
622
+ const status = terminal.response.statusCode ?? 500;
623
+ return done(renderOutcome(terminal.response, { statusCode: status, body: { error: STATUS_TEXT[status] ?? "Error", injected: true } }, scopeFor(req, params, sessionId, ctx), tpl, maxBody), delay);
624
+ };
625
+ const handleControl = async (req, definition, ctx, store, scope, sessionId, maxBody) => {
626
+ const sub = req.path.slice(MOCK_CONTROL_PREFIX.length);
627
+ const method = req.method.toUpperCase();
628
+ if (sub === "/session" || sub === "/session/reset") {
629
+ if (definition.sessions?.enabled === false)
630
+ return jsonResponse(404, { error: "Test sessions are turned off for this mock" }, maxBody);
631
+ if (!sessionId)
632
+ return jsonResponse(400, { error: "Send the X-Mock-Session header" }, maxBody);
633
+ if (!store)
634
+ return jsonResponse(500, { error: "No data store is configured" }, maxBody);
635
+ if (sub === "/session" && method === "GET") {
636
+ const info = (await store.listNamespaces(ctx.mockId)).find((ns) => ns.ns === scope.ns);
637
+ if (!info)
638
+ return jsonResponse(404, { error: "This session has no data yet" }, maxBody);
639
+ return jsonResponse(200, { id: sessionId, createdAt: info.createdAt, lastSeenAt: info.lastSeenAt, expiresAt: info.idleExpiresAt, recordCount: info.recordCount }, maxBody);
640
+ }
641
+ if (sub === "/session/reset" && method === "POST") {
642
+ if (definition.sessions?.allowPublicReset === false)
643
+ return jsonResponse(403, { error: "Resetting sessions from the mock URL is turned off" }, maxBody);
644
+ const plan = await seedPlanFor(definition, ctx);
645
+ await store.resetNamespace(scope, plan, ctx.clock.now());
646
+ await ctx.counters.reset(scope);
647
+ return jsonResponse(200, { reset: true, session: sessionId }, maxBody);
648
+ }
649
+ }
650
+ return jsonResponse(404, { error: "Unknown control endpoint" }, maxBody);
651
+ };
652
+ export class MockDataError extends Error {
653
+ status;
654
+ code;
655
+ constructor(status, message, code) {
656
+ super(message);
657
+ this.status = status;
658
+ this.code = code;
659
+ this.name = "MockDataError";
660
+ }
661
+ }
662
+ export const createMockDataManager = (compiled, ctx) => {
663
+ const definition = compiled.definition;
664
+ if (!ctx.store)
665
+ throw new MockDataError(500, "No data store is configured", "MOCK_DATA_STORE_MISSING");
666
+ const store = ctx.store;
667
+ const tpl = { random: ctx.random, now: () => ctx.clock.now() };
668
+ const scopeOf = (session) => {
669
+ if (session && !MOCK_SESSION_ID_PATTERN.test(session))
670
+ throw new MockDataError(400, "Invalid session id", "MOCK_SESSION_INVALID");
671
+ return { mockId: ctx.mockId, ns: session ? `s:${session}` : "_" };
672
+ };
673
+ const resourceOf = (name) => {
674
+ const resource = resourceByName(definition, name);
675
+ if (!resource)
676
+ throw new MockDataError(404, `Unknown resource "${name}"`, "MOCK_RESOURCE_NOT_FOUND");
677
+ return resource;
678
+ };
679
+ const runtime = async (session) => {
680
+ const scope = scopeOf(session);
681
+ const ensured = await store.ensureNamespace(scope, () => seedPlanFor(definition, ctx), {
682
+ isSession: Boolean(session),
683
+ maxSessions: ctx.limits.maxSessions,
684
+ idleTtlMs: sessionIdleMs(definition, ctx),
685
+ now: ctx.clock.now(),
686
+ });
687
+ if (!ensured.ok)
688
+ throw new MockDataError(429, "Too many active test sessions", "MOCK_SESSION_LIMIT");
689
+ return {
690
+ definition,
691
+ scope,
692
+ store,
693
+ ctx,
694
+ tpl,
695
+ templateScope: { request: { method: "POST", params: {}, query: {}, headers: {} }, session: { id: session ?? null }, mock: { id: ctx.mockId } },
696
+ management: true,
697
+ mountPath: "",
698
+ requestPath: "",
699
+ };
700
+ };
701
+ return {
702
+ async listRecords(name, options = {}) {
703
+ const resource = resourceOf(name);
704
+ const rt = await runtime(options.session);
705
+ const query = options.rawQuery ?? [];
706
+ const hasLimit = query.some(([key]) => key === "_limit" || key === "_per_page");
707
+ const effective = hasLimit ? query : [...query, ["_limit", "50"]];
708
+ const op = await listRecords(rt, { ...resource, list: { ...resource.list, envelope: "array", filterMode: "any" } }, effective);
709
+ return {
710
+ items: op.scope.records ?? [],
711
+ total: Number(op.scope.total ?? 0),
712
+ page: Number(op.scope.page ?? 1),
713
+ limit: Number(op.scope.limit ?? 50),
714
+ };
715
+ },
716
+ async getRecord(name, id, session) {
717
+ const resource = resourceOf(name);
718
+ const rt = await runtime(session);
719
+ const doc = await rt.store.get(rt.scope, resource.resourceId, keyFromParam(resource, id));
720
+ return doc ? doc.data : null;
721
+ },
722
+ async insertRecords(name, records, session) {
723
+ const resource = resourceOf(name);
724
+ const rt = await runtime(session);
725
+ const inserted = [];
726
+ const errors = [];
727
+ for (let i = 0; i < records.length; i += 1) {
728
+ const op = await createRecord(rt, resource, records[i]);
729
+ if (op.outcome === "created")
730
+ inserted.push(op.body);
731
+ else
732
+ errors.push({ index: i, status: op.status, body: op.body });
733
+ }
734
+ return { inserted, errors };
735
+ },
736
+ async updateRecord(name, id, body, mode, session) {
737
+ const resource = resourceOf(name);
738
+ const rt = await runtime(session);
739
+ const doc = await rt.store.get(rt.scope, resource.resourceId, keyFromParam(resource, id));
740
+ if (!doc)
741
+ return { status: 404, body: { error: "Not found" } };
742
+ const op = await writeRecord(rt, resource, doc, body, mode);
743
+ return { status: op.status, body: op.body };
744
+ },
745
+ async deleteRecords(name, ids, session) {
746
+ const resource = resourceOf(name);
747
+ const rt = await runtime(session);
748
+ if (ids === "all") {
749
+ const deleted = await rt.store.clearResource(rt.scope, resource.resourceId);
750
+ return { status: 200, deleted };
751
+ }
752
+ const docs = await rt.store.getMany(rt.scope, resource.resourceId, ids.map((id) => keyFromParam(resource, id)));
753
+ if (docs.length === 0)
754
+ return { status: 404, deleted: 0, body: { error: "Not found" } };
755
+ const op = await deleteRecords(rt, resource, docs);
756
+ return { status: op.status, deleted: op.affected, ...(op.status >= 400 ? { body: op.body } : {}) };
757
+ },
758
+ async resetData(session) {
759
+ const scope = scopeOf(session);
760
+ const plan = await seedPlanFor(definition, ctx);
761
+ await store.resetNamespace(scope, plan, ctx.clock.now());
762
+ await ctx.counters.reset(scope);
763
+ },
764
+ async prepareData(addedResourceIds = []) {
765
+ if (definition.resources.length === 0)
766
+ return;
767
+ const shared = scopeOf(null);
768
+ const ensured = await store.ensureNamespace(shared, () => seedPlanFor(definition, ctx), {
769
+ isSession: false,
770
+ maxSessions: ctx.limits.maxSessions,
771
+ idleTtlMs: sessionIdleMs(definition, ctx),
772
+ now: ctx.clock.now(),
773
+ });
774
+ const added = addedResourceIds.filter((id) => definition.resources.some((resource) => resource.resourceId === id));
775
+ if ((ensured.ok && ensured.created) || added.length === 0)
776
+ return;
777
+ const plan = await seedPlanFor(definition, ctx);
778
+ const sessions = (await store.listNamespaces(ctx.mockId)).filter((ns) => ns.kind === "session" && ns.sessionId).map((ns) => scopeOf(ns.sessionId));
779
+ for (const scope of [shared, ...sessions]) {
780
+ for (const resourceId of added) {
781
+ if ((await store.count(scope, resourceId, undefined, 1)) > 0)
782
+ continue;
783
+ for (const doc of plan.docs) {
784
+ if (doc.resource === resourceId)
785
+ await store.insert(scope, { ...doc, data: { ...doc.data } });
786
+ }
787
+ const sequence = plan.sequences[resourceId];
788
+ if (sequence)
789
+ await store.bumpSequence(scope, resourceId, sequence);
790
+ }
791
+ }
792
+ },
793
+ async listSessions() {
794
+ const namespaces = await store.listNamespaces(ctx.mockId);
795
+ return namespaces
796
+ .filter((ns) => ns.kind === "session" && ns.sessionId)
797
+ .map((ns) => ({ id: ns.sessionId, createdAt: ns.createdAt, lastSeenAt: ns.lastSeenAt, expiresAt: ns.idleExpiresAt, recordCount: ns.recordCount }));
798
+ },
799
+ async dropSession(session) {
800
+ const scope = scopeOf(session);
801
+ await store.dropNamespace(scope);
802
+ await ctx.counters.reset(scope);
803
+ },
804
+ async exportData(session) {
805
+ const rt = await runtime(session);
806
+ const out = {};
807
+ for (const resource of definition.resources) {
808
+ const { docs } = await rt.store.find(rt.scope, resource.resourceId, { limit: ctx.limits.maxRecordsPerResource, noTotal: true });
809
+ out[resource.name] = docs.map((doc) => doc.data);
810
+ }
811
+ return out;
812
+ },
813
+ async issueUserToken(userId, session) {
814
+ if (!definition.userAuth?.enabled)
815
+ return null;
816
+ const users = resourceByName(definition, definition.userAuth.usersResource);
817
+ if (!users)
818
+ return null;
819
+ const rt = await runtime(session);
820
+ const doc = await rt.store.get(rt.scope, users.resourceId, keyFromParam(users, userId));
821
+ if (!doc)
822
+ return null;
823
+ return issueToken(definition, ctx, users, doc.data);
824
+ },
825
+ };
826
+ };
827
+ export { generateMockSecret };
828
+ //# sourceMappingURL=engine.js.map