@pikku/core 0.11.2 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (436) hide show
  1. package/CHANGELOG.md +57 -2
  2. package/dist/crypto-utils.d.ts +2 -0
  3. package/dist/crypto-utils.js +78 -0
  4. package/dist/errors/errors.d.ts +14 -0
  5. package/dist/errors/errors.js +26 -0
  6. package/dist/errors/index.d.ts +1 -1
  7. package/dist/errors/index.js +1 -1
  8. package/dist/function/function-runner.d.ts +10 -4
  9. package/dist/function/function-runner.js +126 -45
  10. package/dist/function/functions.types.d.ts +32 -87
  11. package/dist/function/functions.types.js +16 -46
  12. package/dist/function/index.d.ts +2 -2
  13. package/dist/function/index.js +1 -2
  14. package/dist/handle-error.d.ts +2 -2
  15. package/dist/handle-error.js +3 -12
  16. package/dist/index.d.ts +30 -26
  17. package/dist/index.js +19 -29
  18. package/dist/internal.d.ts +3 -0
  19. package/dist/internal.js +2 -0
  20. package/dist/middleware/auth-apikey.d.ts +2 -2
  21. package/dist/middleware/auth-apikey.js +3 -3
  22. package/dist/middleware/auth-bearer.d.ts +3 -3
  23. package/dist/middleware/auth-bearer.js +4 -7
  24. package/dist/middleware/auth-cookie.d.ts +4 -4
  25. package/dist/middleware/auth-cookie.js +7 -9
  26. package/dist/middleware/cors.d.ts +46 -0
  27. package/dist/middleware/cors.js +81 -0
  28. package/dist/middleware/index.d.ts +5 -3
  29. package/dist/middleware/index.js +5 -3
  30. package/dist/middleware/remote-auth.d.ts +5 -0
  31. package/dist/middleware/remote-auth.js +55 -0
  32. package/dist/middleware/timeout.d.ts +2 -2
  33. package/dist/middleware-runner.d.ts +1 -1
  34. package/dist/middleware-runner.js +8 -10
  35. package/dist/permissions.d.ts +2 -2
  36. package/dist/permissions.js +11 -15
  37. package/dist/pikku-state.d.ts +10 -8
  38. package/dist/pikku-state.js +48 -25
  39. package/dist/schema.d.ts +4 -4
  40. package/dist/schema.js +20 -15
  41. package/dist/services/ai-agent-runner-service.d.ts +43 -0
  42. package/dist/services/ai-agent-runner-service.js +1 -0
  43. package/dist/services/ai-run-state-service.d.ts +13 -0
  44. package/dist/services/ai-run-state-service.js +1 -0
  45. package/dist/services/ai-storage-service.d.ts +18 -0
  46. package/dist/services/ai-storage-service.js +1 -0
  47. package/dist/services/content-service.d.ts +6 -0
  48. package/dist/services/deployment-service.d.ts +19 -0
  49. package/dist/services/deployment-service.js +1 -0
  50. package/dist/services/gopass-secrets.d.ts +47 -0
  51. package/dist/services/gopass-secrets.js +97 -0
  52. package/dist/services/in-memory-ai-run-state-service.d.ts +15 -0
  53. package/dist/services/in-memory-ai-run-state-service.js +44 -0
  54. package/dist/services/in-memory-trigger-service.d.ts +22 -0
  55. package/dist/services/in-memory-trigger-service.js +48 -0
  56. package/dist/services/in-memory-workflow-service.d.ts +59 -0
  57. package/dist/services/in-memory-workflow-service.js +287 -0
  58. package/dist/services/index.d.ts +26 -11
  59. package/dist/services/index.js +11 -13
  60. package/dist/services/local-content.d.ts +2 -1
  61. package/dist/services/local-content.js +6 -1
  62. package/dist/services/local-secrets.d.ts +25 -2
  63. package/dist/services/local-secrets.js +45 -0
  64. package/dist/services/local-variables.d.ts +6 -1
  65. package/dist/services/local-variables.js +18 -0
  66. package/dist/services/logger-console.d.ts +2 -1
  67. package/dist/services/scheduler-service.d.ts +9 -1
  68. package/dist/services/scheduler-service.js +8 -0
  69. package/dist/services/schema-service.d.ts +6 -0
  70. package/dist/services/scoped-secret-service.d.ts +16 -0
  71. package/dist/services/scoped-secret-service.js +35 -0
  72. package/dist/services/secret-service.d.ts +25 -4
  73. package/dist/services/trigger-service.d.ts +16 -0
  74. package/dist/services/trigger-service.js +1 -0
  75. package/dist/services/typed-secret-service.d.ts +30 -0
  76. package/dist/services/typed-secret-service.js +40 -0
  77. package/dist/services/typed-variables-service.d.ts +27 -0
  78. package/dist/services/typed-variables-service.js +46 -0
  79. package/dist/services/user-session-service.d.ts +15 -2
  80. package/dist/services/user-session-service.js +17 -0
  81. package/dist/services/variables-service.d.ts +5 -0
  82. package/dist/services/workflow-service.d.ts +9 -5
  83. package/dist/types/core.types.d.ts +67 -25
  84. package/dist/types/core.types.js +8 -1
  85. package/dist/types/state.types.d.ts +53 -59
  86. package/dist/utils.d.ts +7 -6
  87. package/dist/utils.js +27 -12
  88. package/dist/version.d.ts +6 -0
  89. package/dist/version.js +19 -0
  90. package/dist/wirings/ai-agent/ai-agent-assistant-ui.d.ts +5 -0
  91. package/dist/wirings/ai-agent/ai-agent-assistant-ui.js +146 -0
  92. package/dist/wirings/ai-agent/ai-agent-helpers.d.ts +28 -0
  93. package/dist/wirings/ai-agent/ai-agent-helpers.js +40 -0
  94. package/dist/wirings/ai-agent/ai-agent-memory.d.ts +29 -0
  95. package/dist/wirings/ai-agent/ai-agent-memory.js +199 -0
  96. package/dist/wirings/ai-agent/ai-agent-model-config.d.ts +9 -0
  97. package/dist/wirings/ai-agent/ai-agent-model-config.js +36 -0
  98. package/dist/wirings/ai-agent/ai-agent-prepare.d.ts +59 -0
  99. package/dist/wirings/ai-agent/ai-agent-prepare.js +348 -0
  100. package/dist/wirings/ai-agent/ai-agent-registry.d.ts +15 -0
  101. package/dist/wirings/ai-agent/ai-agent-registry.js +47 -0
  102. package/dist/wirings/ai-agent/ai-agent-runner.d.ts +3 -0
  103. package/dist/wirings/ai-agent/ai-agent-runner.js +195 -0
  104. package/dist/wirings/ai-agent/ai-agent-stream.d.ts +12 -0
  105. package/dist/wirings/ai-agent/ai-agent-stream.js +662 -0
  106. package/dist/wirings/ai-agent/ai-agent.types.d.ts +313 -0
  107. package/dist/wirings/ai-agent/ai-agent.types.js +1 -0
  108. package/dist/wirings/ai-agent/index.d.ts +7 -0
  109. package/dist/wirings/ai-agent/index.js +6 -0
  110. package/dist/wirings/channel/channel-common.d.ts +8 -5
  111. package/dist/wirings/channel/channel-common.js +15 -8
  112. package/dist/wirings/channel/channel-handler.d.ts +3 -3
  113. package/dist/wirings/channel/channel-handler.js +15 -6
  114. package/dist/wirings/channel/channel-middleware-runner.d.ts +9 -0
  115. package/dist/wirings/channel/channel-middleware-runner.js +73 -0
  116. package/dist/wirings/channel/channel-runner.d.ts +5 -5
  117. package/dist/wirings/channel/channel-runner.js +10 -9
  118. package/dist/wirings/channel/channel-store.d.ts +1 -1
  119. package/dist/wirings/channel/channel.types.d.ts +9 -7
  120. package/dist/wirings/channel/define-channel-routes.d.ts +21 -0
  121. package/dist/wirings/channel/define-channel-routes.js +23 -0
  122. package/dist/wirings/channel/index.d.ts +10 -8
  123. package/dist/wirings/channel/index.js +7 -8
  124. package/dist/wirings/channel/local/index.d.ts +3 -3
  125. package/dist/wirings/channel/local/index.js +3 -3
  126. package/dist/wirings/channel/local/local-channel-runner.d.ts +7 -2
  127. package/dist/wirings/channel/local/local-channel-runner.js +22 -9
  128. package/dist/wirings/channel/local/local-eventhub-service.d.ts +2 -2
  129. package/dist/wirings/channel/log-channels.d.ts +1 -1
  130. package/dist/wirings/channel/pikku-abstract-channel-handler.d.ts +1 -1
  131. package/dist/wirings/channel/serverless/index.d.ts +1 -1
  132. package/dist/wirings/channel/serverless/index.js +1 -1
  133. package/dist/wirings/channel/serverless/serverless-channel-runner.d.ts +4 -4
  134. package/dist/wirings/channel/serverless/serverless-channel-runner.js +40 -23
  135. package/dist/wirings/cli/channel/cli-channel-runner.d.ts +1 -1
  136. package/dist/wirings/cli/channel/index.d.ts +1 -1
  137. package/dist/wirings/cli/channel/index.js +1 -1
  138. package/dist/wirings/cli/cli-runner.d.ts +3 -3
  139. package/dist/wirings/cli/cli-runner.js +12 -8
  140. package/dist/wirings/cli/cli.types.d.ts +9 -9
  141. package/dist/wirings/cli/command-parser.d.ts +1 -1
  142. package/dist/wirings/cli/define-cli-commands.d.ts +19 -0
  143. package/dist/wirings/cli/define-cli-commands.js +20 -0
  144. package/dist/wirings/cli/index.d.ts +2 -3
  145. package/dist/wirings/cli/index.js +1 -3
  146. package/dist/wirings/http/http-routes.d.ts +69 -0
  147. package/dist/wirings/http/http-routes.js +139 -0
  148. package/dist/wirings/http/http-runner.d.ts +9 -9
  149. package/dist/wirings/http/http-runner.js +79 -34
  150. package/dist/wirings/http/http.types.d.ts +77 -41
  151. package/dist/wirings/http/index.d.ts +5 -4
  152. package/dist/wirings/http/index.js +4 -3
  153. package/dist/wirings/http/log-http-routes.d.ts +1 -1
  154. package/dist/wirings/http/pikku-fetch-http-request.d.ts +1 -1
  155. package/dist/wirings/http/pikku-fetch-http-response.d.ts +2 -2
  156. package/dist/wirings/http/pikku-fetch-http-response.js +6 -1
  157. package/dist/wirings/http/routers/http-router.d.ts +1 -1
  158. package/dist/wirings/http/routers/path-to-regex.d.ts +2 -2
  159. package/dist/wirings/mcp/index.d.ts +4 -3
  160. package/dist/wirings/mcp/index.js +3 -3
  161. package/dist/wirings/mcp/mcp-endpoint-registry.d.ts +1 -1
  162. package/dist/wirings/mcp/mcp-runner.d.ts +1 -6
  163. package/dist/wirings/mcp/mcp-runner.js +28 -40
  164. package/dist/wirings/mcp/mcp.types.d.ts +5 -5
  165. package/dist/wirings/node/index.d.ts +2 -0
  166. package/dist/wirings/node/index.js +1 -0
  167. package/dist/wirings/node/node.types.d.ts +20 -0
  168. package/dist/wirings/node/node.types.js +1 -0
  169. package/dist/wirings/oauth2/index.d.ts +3 -0
  170. package/dist/wirings/oauth2/index.js +2 -0
  171. package/dist/wirings/oauth2/oauth2-client.d.ts +47 -0
  172. package/dist/wirings/oauth2/oauth2-client.js +267 -0
  173. package/dist/wirings/oauth2/oauth2.types.d.ts +65 -0
  174. package/dist/wirings/oauth2/oauth2.types.js +4 -0
  175. package/dist/wirings/oauth2/wire-oauth2-credential.d.ts +20 -0
  176. package/dist/wirings/oauth2/wire-oauth2-credential.js +21 -0
  177. package/dist/wirings/queue/index.d.ts +1 -1
  178. package/dist/wirings/queue/queue-runner.d.ts +1 -5
  179. package/dist/wirings/queue/queue-runner.js +11 -8
  180. package/dist/wirings/queue/queue.types.d.ts +5 -5
  181. package/dist/wirings/queue/register-queue-helper.d.ts +2 -2
  182. package/dist/wirings/queue/register-queue-helper.js +1 -1
  183. package/dist/wirings/queue/validate-worker-config.d.ts +1 -1
  184. package/dist/wirings/rpc/index.d.ts +3 -1
  185. package/dist/wirings/rpc/index.js +2 -1
  186. package/dist/wirings/rpc/rpc-runner.d.ts +58 -8
  187. package/dist/wirings/rpc/rpc-runner.js +177 -31
  188. package/dist/wirings/rpc/rpc-types.d.ts +20 -4
  189. package/dist/wirings/rpc/wire-addon.d.ts +10 -0
  190. package/dist/wirings/rpc/wire-addon.js +9 -0
  191. package/dist/wirings/scheduler/index.d.ts +2 -2
  192. package/dist/wirings/scheduler/index.js +1 -2
  193. package/dist/wirings/scheduler/log-schedulers.d.ts +1 -1
  194. package/dist/wirings/scheduler/scheduler-runner.d.ts +3 -5
  195. package/dist/wirings/scheduler/scheduler-runner.js +11 -14
  196. package/dist/wirings/scheduler/scheduler.types.d.ts +2 -2
  197. package/dist/wirings/secret/index.d.ts +3 -0
  198. package/dist/wirings/secret/index.js +2 -0
  199. package/dist/wirings/secret/secret.types.d.ts +27 -0
  200. package/dist/wirings/secret/secret.types.js +1 -0
  201. package/dist/wirings/secret/validate-secret-definitions.d.ts +6 -0
  202. package/dist/wirings/secret/validate-secret-definitions.js +55 -0
  203. package/dist/wirings/trigger/index.d.ts +3 -2
  204. package/dist/wirings/trigger/index.js +2 -2
  205. package/dist/wirings/trigger/pikku-trigger-service.d.ts +22 -0
  206. package/dist/wirings/trigger/pikku-trigger-service.js +70 -0
  207. package/dist/wirings/trigger/trigger-runner.d.ts +18 -9
  208. package/dist/wirings/trigger/trigger-runner.js +48 -19
  209. package/dist/wirings/trigger/trigger.types.d.ts +99 -11
  210. package/dist/wirings/trigger/trigger.types.js +40 -1
  211. package/dist/wirings/variable/index.d.ts +3 -0
  212. package/dist/wirings/variable/index.js +2 -0
  213. package/dist/wirings/variable/validate-variable-definitions.d.ts +6 -0
  214. package/dist/wirings/variable/validate-variable-definitions.js +45 -0
  215. package/dist/wirings/variable/variable.types.d.ts +18 -0
  216. package/dist/wirings/variable/variable.types.js +1 -0
  217. package/dist/wirings/workflow/dsl/index.d.ts +1 -1
  218. package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +7 -1
  219. package/dist/wirings/workflow/dsl/workflow-runner.js +3 -2
  220. package/dist/wirings/workflow/graph/graph-node.d.ts +1 -6
  221. package/dist/wirings/workflow/graph/graph-runner.d.ts +5 -31
  222. package/dist/wirings/workflow/graph/graph-runner.js +384 -296
  223. package/dist/wirings/workflow/graph/index.d.ts +4 -3
  224. package/dist/wirings/workflow/graph/index.js +3 -3
  225. package/dist/wirings/workflow/graph/template.d.ts +15 -0
  226. package/dist/wirings/workflow/graph/template.js +27 -0
  227. package/dist/wirings/workflow/graph/wire-workflow-graph.d.ts +15 -0
  228. package/dist/wirings/workflow/graph/wire-workflow-graph.js +8 -0
  229. package/dist/wirings/workflow/graph/workflow-graph.types.d.ts +0 -13
  230. package/dist/wirings/workflow/index.d.ts +6 -6
  231. package/dist/wirings/workflow/index.js +6 -4
  232. package/dist/wirings/workflow/pikku-workflow-service.d.ts +29 -19
  233. package/dist/wirings/workflow/pikku-workflow-service.js +217 -82
  234. package/dist/wirings/workflow/workflow-helpers.d.ts +36 -0
  235. package/dist/wirings/workflow/workflow-helpers.js +38 -0
  236. package/dist/wirings/workflow/workflow.types.d.ts +42 -95
  237. package/package.json +14 -15
  238. package/run-tests.sh +4 -1
  239. package/src/crypto-utils.test.ts +116 -0
  240. package/src/crypto-utils.ts +99 -0
  241. package/src/errors/error.test.ts +143 -2
  242. package/src/errors/errors.ts +27 -0
  243. package/src/errors/index.ts +1 -1
  244. package/src/factory-functions.test.ts +42 -2
  245. package/src/function/function-runner.test.ts +6 -13
  246. package/src/function/function-runner.ts +161 -53
  247. package/src/function/functions.types.ts +63 -132
  248. package/src/function/index.ts +5 -2
  249. package/src/handle-error.test.ts +361 -0
  250. package/src/handle-error.ts +6 -14
  251. package/src/index.ts +97 -30
  252. package/src/internal.ts +6 -0
  253. package/src/middleware/auth-apikey.test.ts +16 -13
  254. package/src/middleware/auth-apikey.ts +3 -3
  255. package/src/middleware/auth-bearer.test.ts +18 -15
  256. package/src/middleware/auth-bearer.ts +27 -32
  257. package/src/middleware/auth-cookie.test.ts +17 -14
  258. package/src/middleware/auth-cookie.ts +9 -13
  259. package/src/middleware/cors.test.ts +424 -0
  260. package/src/middleware/cors.ts +104 -0
  261. package/src/middleware/index.ts +5 -3
  262. package/src/middleware/remote-auth.test.ts +488 -0
  263. package/src/middleware/remote-auth.ts +68 -0
  264. package/src/middleware-runner.test.ts +123 -1
  265. package/src/middleware-runner.ts +13 -13
  266. package/src/permissions.test.ts +83 -9
  267. package/src/permissions.ts +17 -19
  268. package/src/pikku-state.test.ts +224 -0
  269. package/src/pikku-state.ts +59 -35
  270. package/src/run-tests-script.test.ts +49 -0
  271. package/src/schema.test.ts +198 -6
  272. package/src/schema.ts +28 -21
  273. package/src/services/ai-agent-runner-service.ts +41 -0
  274. package/src/services/ai-run-state-service.ts +20 -0
  275. package/src/services/ai-storage-service.ts +39 -0
  276. package/src/services/content-service.ts +7 -0
  277. package/src/services/deployment-service.ts +22 -0
  278. package/src/services/gopass-secrets.ts +98 -0
  279. package/src/services/in-memory-ai-run-state-service.ts +73 -0
  280. package/src/services/in-memory-trigger-service.ts +64 -0
  281. package/src/services/in-memory-workflow-service.test.ts +351 -0
  282. package/src/services/in-memory-workflow-service.ts +395 -0
  283. package/src/services/index.ts +50 -14
  284. package/src/services/local-content.ts +9 -3
  285. package/src/services/local-secrets.test.ts +80 -0
  286. package/src/services/local-secrets.ts +53 -2
  287. package/src/services/local-variables.test.ts +61 -0
  288. package/src/services/local-variables.ts +23 -1
  289. package/src/services/logger-console.test.ts +118 -0
  290. package/src/services/logger-console.ts +2 -1
  291. package/src/services/scheduler-service.ts +11 -1
  292. package/src/services/schema-service.ts +7 -0
  293. package/src/services/scoped-secret-service.test.ts +74 -0
  294. package/src/services/scoped-secret-service.ts +41 -0
  295. package/src/services/secret-service.ts +25 -4
  296. package/src/services/trigger-service.ts +17 -0
  297. package/src/services/typed-secret-service.test.ts +93 -0
  298. package/src/services/typed-secret-service.ts +70 -0
  299. package/src/services/typed-variables-service.test.ts +73 -0
  300. package/src/services/typed-variables-service.ts +81 -0
  301. package/src/services/user-session-service.test.ts +113 -0
  302. package/src/services/user-session-service.ts +25 -2
  303. package/src/services/variables-service.ts +5 -0
  304. package/src/services/workflow-service.ts +23 -13
  305. package/src/time-utils.test.ts +1 -1
  306. package/src/types/core.types.ts +95 -24
  307. package/src/types/state.types.ts +56 -73
  308. package/src/utils.test.ts +350 -0
  309. package/src/utils.ts +33 -18
  310. package/src/version.test.ts +80 -0
  311. package/src/version.ts +25 -0
  312. package/src/wirings/ai-agent/ai-agent-assistant-ui.test.ts +363 -0
  313. package/src/wirings/ai-agent/ai-agent-assistant-ui.ts +214 -0
  314. package/src/wirings/ai-agent/ai-agent-helpers.test.ts +152 -0
  315. package/src/wirings/ai-agent/ai-agent-helpers.ts +76 -0
  316. package/src/wirings/ai-agent/ai-agent-memory.ts +300 -0
  317. package/src/wirings/ai-agent/ai-agent-model-config.test.ts +115 -0
  318. package/src/wirings/ai-agent/ai-agent-model-config.ts +43 -0
  319. package/src/wirings/ai-agent/ai-agent-prepare.ts +504 -0
  320. package/src/wirings/ai-agent/ai-agent-registry.test.ts +37 -0
  321. package/src/wirings/ai-agent/ai-agent-registry.ts +82 -0
  322. package/src/wirings/ai-agent/ai-agent-runner.test.ts +317 -0
  323. package/src/wirings/ai-agent/ai-agent-runner.ts +251 -0
  324. package/src/wirings/ai-agent/ai-agent-stream.test.ts +652 -0
  325. package/src/wirings/ai-agent/ai-agent-stream.ts +1049 -0
  326. package/src/wirings/ai-agent/ai-agent.types.ts +328 -0
  327. package/src/wirings/ai-agent/index.ts +39 -0
  328. package/src/wirings/channel/channel-common.ts +33 -20
  329. package/src/wirings/channel/channel-handler.ts +26 -9
  330. package/src/wirings/channel/channel-middleware-runner.ts +119 -0
  331. package/src/wirings/channel/channel-runner.ts +15 -16
  332. package/src/wirings/channel/channel-store.ts +1 -1
  333. package/src/wirings/channel/channel.types.ts +21 -9
  334. package/src/wirings/channel/define-channel-routes.ts +25 -0
  335. package/src/wirings/channel/index.ts +23 -8
  336. package/src/wirings/channel/local/index.ts +3 -3
  337. package/src/wirings/channel/local/local-channel-runner.test.ts +57 -7
  338. package/src/wirings/channel/local/local-channel-runner.ts +40 -19
  339. package/src/wirings/channel/local/local-eventhub-service.test.ts +2 -2
  340. package/src/wirings/channel/local/local-eventhub-service.ts +2 -2
  341. package/src/wirings/channel/log-channels.ts +1 -1
  342. package/src/wirings/channel/pikku-abstract-channel-handler.ts +1 -1
  343. package/src/wirings/channel/serverless/index.ts +5 -1
  344. package/src/wirings/channel/serverless/serverless-channel-runner.ts +57 -36
  345. package/src/wirings/cli/channel/cli-channel-runner.ts +1 -1
  346. package/src/wirings/cli/channel/index.ts +1 -1
  347. package/src/wirings/cli/cli-runner.test.ts +10 -10
  348. package/src/wirings/cli/cli-runner.ts +28 -14
  349. package/src/wirings/cli/cli.types.ts +14 -14
  350. package/src/wirings/cli/command-parser.test.ts +9 -9
  351. package/src/wirings/cli/command-parser.ts +1 -1
  352. package/src/wirings/cli/define-cli-commands.ts +24 -0
  353. package/src/wirings/cli/index.ts +10 -4
  354. package/src/wirings/http/http-routes.test.ts +322 -0
  355. package/src/wirings/http/http-routes.ts +198 -0
  356. package/src/wirings/http/http-runner.test.ts +12 -20
  357. package/src/wirings/http/http-runner.ts +101 -51
  358. package/src/wirings/http/http.types.ts +119 -48
  359. package/src/wirings/http/index.ts +16 -4
  360. package/src/wirings/http/log-http-routes.ts +1 -1
  361. package/src/wirings/http/pikku-fetch-http-request.ts +1 -1
  362. package/src/wirings/http/pikku-fetch-http-response.ts +12 -5
  363. package/src/wirings/http/routers/http-router.ts +1 -1
  364. package/src/wirings/http/routers/path-to-regex.test.ts +1 -1
  365. package/src/wirings/http/routers/path-to-regex.ts +4 -3
  366. package/src/wirings/mcp/index.ts +27 -3
  367. package/src/wirings/mcp/mcp-endpoint-registry.test.ts +3 -3
  368. package/src/wirings/mcp/mcp-endpoint-registry.ts +5 -1
  369. package/src/wirings/mcp/mcp-runner.ts +48 -65
  370. package/src/wirings/mcp/mcp.types.ts +5 -5
  371. package/src/wirings/node/index.ts +2 -0
  372. package/src/wirings/node/node.types.ts +23 -0
  373. package/src/wirings/oauth2/index.ts +3 -0
  374. package/src/wirings/oauth2/oauth2-client.test.ts +929 -0
  375. package/src/wirings/oauth2/oauth2-client.ts +335 -0
  376. package/src/wirings/oauth2/oauth2.types.ts +69 -0
  377. package/src/wirings/oauth2/wire-oauth2-credential.ts +23 -0
  378. package/src/wirings/queue/index.ts +13 -2
  379. package/src/wirings/queue/queue-runner.test.ts +142 -73
  380. package/src/wirings/queue/queue-runner.ts +18 -14
  381. package/src/wirings/queue/queue.types.ts +5 -5
  382. package/src/wirings/queue/register-queue-helper.ts +3 -5
  383. package/src/wirings/queue/validate-worker-config.test.ts +108 -0
  384. package/src/wirings/queue/validate-worker-config.ts +4 -1
  385. package/src/wirings/rpc/index.ts +3 -1
  386. package/src/wirings/rpc/rpc-runner.ts +254 -41
  387. package/src/wirings/rpc/rpc-types.ts +28 -4
  388. package/src/wirings/rpc/wire-addon.ts +20 -0
  389. package/src/wirings/scheduler/index.ts +6 -4
  390. package/src/wirings/scheduler/log-schedulers.ts +1 -1
  391. package/src/wirings/scheduler/scheduler-runner.test.ts +78 -45
  392. package/src/wirings/scheduler/scheduler-runner.ts +20 -27
  393. package/src/wirings/scheduler/scheduler.types.ts +2 -2
  394. package/src/wirings/secret/index.ts +9 -0
  395. package/src/wirings/secret/secret.types.ts +32 -0
  396. package/src/wirings/secret/validate-secret-definitions.test.ts +140 -0
  397. package/src/wirings/secret/validate-secret-definitions.ts +82 -0
  398. package/src/wirings/trigger/index.ts +10 -2
  399. package/src/wirings/trigger/pikku-trigger-service.ts +112 -0
  400. package/src/wirings/trigger/trigger-runner.test.ts +79 -0
  401. package/src/wirings/trigger/trigger-runner.ts +80 -41
  402. package/src/wirings/trigger/trigger.types.ts +144 -22
  403. package/src/wirings/variable/index.ts +8 -0
  404. package/src/wirings/variable/validate-variable-definitions.test.ts +91 -0
  405. package/src/wirings/variable/validate-variable-definitions.ts +69 -0
  406. package/src/wirings/variable/variable.types.ts +22 -0
  407. package/src/wirings/workflow/dsl/index.ts +1 -0
  408. package/src/wirings/workflow/dsl/workflow-dsl.types.ts +9 -1
  409. package/src/wirings/workflow/dsl/workflow-runner.ts +4 -3
  410. package/src/wirings/workflow/graph/graph-node.ts +1 -6
  411. package/src/wirings/workflow/graph/graph-runner.test.ts +487 -0
  412. package/src/wirings/workflow/graph/graph-runner.ts +496 -374
  413. package/src/wirings/workflow/graph/index.ts +14 -3
  414. package/src/wirings/workflow/graph/template.test.ts +49 -0
  415. package/src/wirings/workflow/graph/template.ts +42 -0
  416. package/src/wirings/workflow/graph/wire-workflow-graph.ts +29 -0
  417. package/src/wirings/workflow/graph/workflow-graph.types.ts +0 -22
  418. package/src/wirings/workflow/index.ts +21 -10
  419. package/src/wirings/workflow/pikku-workflow-service.test.ts +200 -0
  420. package/src/wirings/workflow/pikku-workflow-service.ts +308 -105
  421. package/src/wirings/workflow/workflow-helpers.test.ts +129 -0
  422. package/src/wirings/workflow/workflow-helpers.ts +79 -0
  423. package/src/wirings/workflow/workflow.types.ts +47 -92
  424. package/tsconfig.tsbuildinfo +1 -1
  425. package/dist/wirings/forge-node/forge-node.types.d.ts +0 -120
  426. package/dist/wirings/forge-node/forge-node.types.js +0 -38
  427. package/dist/wirings/forge-node/index.d.ts +0 -1
  428. package/dist/wirings/forge-node/index.js +0 -1
  429. package/dist/wirings/workflow/wire-workflow.d.ts +0 -42
  430. package/dist/wirings/workflow/wire-workflow.js +0 -53
  431. package/dist/wirings/workflow/workflow-utils.d.ts +0 -23
  432. package/dist/wirings/workflow/workflow-utils.js +0 -66
  433. package/src/wirings/forge-node/forge-node.types.ts +0 -135
  434. package/src/wirings/forge-node/index.ts +0 -1
  435. package/src/wirings/workflow/wire-workflow.ts +0 -94
  436. package/src/wirings/workflow/workflow-utils.ts +0 -120
@@ -0,0 +1,929 @@
1
+ import { describe, test, afterEach, mock } from 'node:test'
2
+ import * as assert from 'node:assert/strict'
3
+ import { OAuth2Client } from './oauth2-client.js'
4
+ import type { OAuth2Token, OAuth2AppCredential } from './oauth2.types.js'
5
+ import type { OAuth2CredentialConfig } from '../secret/secret.types.js'
6
+ import type { SecretService } from '../../services/secret-service.js'
7
+
8
+ // Store the original fetch for restoration
9
+ const originalFetch = globalThis.fetch
10
+
11
+ // Mock SecretService with in-memory store
12
+ function createMockSecretService(
13
+ initialSecrets: Record<string, unknown> = {}
14
+ ): SecretService & { getStoredSecrets: () => Map<string, unknown> } {
15
+ const secrets = new Map<string, unknown>(Object.entries(initialSecrets))
16
+ return {
17
+ getSecret: async (key: string) => secrets.get(key) as string,
18
+ getSecretJSON: async <T>(key: string) => secrets.get(key) as T,
19
+ setSecretJSON: async (key: string, value: unknown) => {
20
+ secrets.set(key, value)
21
+ },
22
+ deleteSecret: async (_key: string) => {
23
+ // Not used in tests
24
+ },
25
+ hasSecret: async (key: string) => secrets.has(key),
26
+ // Helper for tests to inspect stored values
27
+ getStoredSecrets: () => secrets,
28
+ }
29
+ }
30
+
31
+ // Helper to create a mock fetch response
32
+ function mockFetchResponse(data: unknown, status = 200, ok = true): Response {
33
+ return {
34
+ ok,
35
+ status,
36
+ json: async () => data,
37
+ text: async () => JSON.stringify(data),
38
+ } as Response
39
+ }
40
+
41
+ // Default test configuration
42
+ const defaultConfig: OAuth2CredentialConfig = {
43
+ tokenSecretId: 'OAUTH_TOKENS',
44
+ authorizationUrl: 'https://example.com/oauth/authorize',
45
+ tokenUrl: 'https://example.com/oauth/token',
46
+ scopes: ['read', 'write'],
47
+ }
48
+
49
+ const defaultAppCredential: OAuth2AppCredential = {
50
+ clientId: 'test-client-id',
51
+ clientSecret: 'test-client-secret',
52
+ }
53
+
54
+ describe('OAuth2Client', () => {
55
+ afterEach(() => {
56
+ // Restore original fetch after each test
57
+ globalThis.fetch = originalFetch
58
+ })
59
+
60
+ describe('Token Management', () => {
61
+ test('returns cached valid token', async () => {
62
+ const validToken: OAuth2Token = {
63
+ accessToken: 'valid-access-token',
64
+ refreshToken: 'refresh-token',
65
+ expiresAt: Date.now() + 3600000, // 1 hour from now
66
+ tokenType: 'Bearer',
67
+ }
68
+
69
+ const secrets = createMockSecretService({
70
+ OAUTH_TOKENS: validToken,
71
+ APP_CREDS: defaultAppCredential,
72
+ })
73
+
74
+ const client = new OAuth2Client(defaultConfig, 'APP_CREDS', secrets)
75
+
76
+ // First call loads from secrets
77
+ const token1 = await client.getAccessToken()
78
+ assert.equal(token1, 'valid-access-token')
79
+
80
+ // Second call should return cached token without hitting secrets
81
+ const token2 = await client.getAccessToken()
82
+ assert.equal(token2, 'valid-access-token')
83
+ })
84
+
85
+ test('refreshes expired token', async () => {
86
+ const expiredToken: OAuth2Token = {
87
+ accessToken: 'expired-access-token',
88
+ refreshToken: 'refresh-token',
89
+ expiresAt: Date.now() - 1000, // Expired
90
+ tokenType: 'Bearer',
91
+ }
92
+
93
+ const secrets = createMockSecretService({
94
+ OAUTH_TOKENS: expiredToken,
95
+ APP_CREDS: defaultAppCredential,
96
+ })
97
+
98
+ globalThis.fetch = mock.fn(async () =>
99
+ mockFetchResponse({
100
+ access_token: 'new-access-token',
101
+ refresh_token: 'new-refresh-token',
102
+ expires_in: 3600,
103
+ token_type: 'Bearer',
104
+ })
105
+ )
106
+
107
+ const client = new OAuth2Client(defaultConfig, 'APP_CREDS', secrets)
108
+ const token = await client.getAccessToken()
109
+
110
+ assert.equal(token, 'new-access-token')
111
+ })
112
+
113
+ test('loads from secrets on first call', async () => {
114
+ const storedToken: OAuth2Token = {
115
+ accessToken: 'stored-access-token',
116
+ refreshToken: 'stored-refresh-token',
117
+ expiresAt: Date.now() + 3600000,
118
+ tokenType: 'Bearer',
119
+ }
120
+
121
+ const secrets = createMockSecretService({
122
+ OAUTH_TOKENS: storedToken,
123
+ APP_CREDS: defaultAppCredential,
124
+ })
125
+
126
+ const client = new OAuth2Client(defaultConfig, 'APP_CREDS', secrets)
127
+ const token = await client.getAccessToken()
128
+
129
+ assert.equal(token, 'stored-access-token')
130
+ })
131
+
132
+ test('refreshes expired token from secrets', async () => {
133
+ const expiredToken: OAuth2Token = {
134
+ accessToken: 'expired-token',
135
+ refreshToken: 'valid-refresh-token',
136
+ expiresAt: Date.now() - 1000, // Expired
137
+ tokenType: 'Bearer',
138
+ }
139
+
140
+ const secrets = createMockSecretService({
141
+ OAUTH_TOKENS: expiredToken,
142
+ APP_CREDS: defaultAppCredential,
143
+ })
144
+
145
+ globalThis.fetch = mock.fn(async () =>
146
+ mockFetchResponse({
147
+ access_token: 'refreshed-token',
148
+ expires_in: 3600,
149
+ token_type: 'Bearer',
150
+ })
151
+ )
152
+
153
+ const client = new OAuth2Client(defaultConfig, 'APP_CREDS', secrets)
154
+ const token = await client.getAccessToken()
155
+
156
+ assert.equal(token, 'refreshed-token')
157
+ })
158
+
159
+ test('throws when no token available', async () => {
160
+ const secrets = createMockSecretService({
161
+ APP_CREDS: defaultAppCredential,
162
+ // No OAUTH_TOKENS
163
+ })
164
+
165
+ const client = new OAuth2Client(defaultConfig, 'APP_CREDS', secrets)
166
+
167
+ await assert.rejects(
168
+ async () => await client.getAccessToken(),
169
+ /undefined/
170
+ )
171
+ })
172
+
173
+ test('uses 60-second expiry buffer', async () => {
174
+ // Token expires in 30 seconds (within the 60-second buffer)
175
+ const almostExpiredToken: OAuth2Token = {
176
+ accessToken: 'almost-expired-token',
177
+ refreshToken: 'refresh-token',
178
+ expiresAt: Date.now() + 30000, // 30 seconds from now
179
+ tokenType: 'Bearer',
180
+ }
181
+
182
+ const secrets = createMockSecretService({
183
+ OAUTH_TOKENS: almostExpiredToken,
184
+ APP_CREDS: defaultAppCredential,
185
+ })
186
+
187
+ globalThis.fetch = mock.fn(async () =>
188
+ mockFetchResponse({
189
+ access_token: 'refreshed-due-to-buffer',
190
+ expires_in: 3600,
191
+ token_type: 'Bearer',
192
+ })
193
+ )
194
+
195
+ const client = new OAuth2Client(defaultConfig, 'APP_CREDS', secrets)
196
+ const token = await client.getAccessToken()
197
+
198
+ // Should refresh because within 60-second buffer
199
+ assert.equal(token, 'refreshed-due-to-buffer')
200
+ })
201
+ })
202
+
203
+ describe('Token Refresh', () => {
204
+ test('makes correct HTTP request', async () => {
205
+ const tokenWithRefresh: OAuth2Token = {
206
+ accessToken: 'old-token',
207
+ refreshToken: 'my-refresh-token',
208
+ expiresAt: Date.now() - 1000,
209
+ tokenType: 'Bearer',
210
+ }
211
+
212
+ const secrets = createMockSecretService({
213
+ OAUTH_TOKENS: tokenWithRefresh,
214
+ APP_CREDS: defaultAppCredential,
215
+ })
216
+
217
+ let capturedRequest: { url: string; options: RequestInit } | null = null
218
+
219
+ globalThis.fetch = mock.fn(
220
+ async (url: RequestInfo | URL, options?: RequestInit) => {
221
+ capturedRequest = { url: url.toString(), options: options! }
222
+ return mockFetchResponse({
223
+ access_token: 'new-token',
224
+ expires_in: 3600,
225
+ token_type: 'Bearer',
226
+ })
227
+ }
228
+ ) as typeof fetch
229
+
230
+ const client = new OAuth2Client(defaultConfig, 'APP_CREDS', secrets)
231
+ await client.getAccessToken()
232
+
233
+ assert.ok(capturedRequest, 'Expected capturedRequest to be set')
234
+ const req = capturedRequest as { url: string; options: RequestInit }
235
+ assert.equal(req.url, 'https://example.com/oauth/token')
236
+ assert.equal(req.options.method, 'POST')
237
+ assert.equal(
238
+ (req.options.headers as Record<string, string>)?.['Content-Type'],
239
+ 'application/x-www-form-urlencoded'
240
+ )
241
+
242
+ const body = req.options.body as string
243
+ assert.ok(body.includes('grant_type=refresh_token'))
244
+ assert.ok(body.includes('refresh_token=my-refresh-token'))
245
+ assert.ok(body.includes('client_id=test-client-id'))
246
+ assert.ok(body.includes('client_secret=test-client-secret'))
247
+ })
248
+
249
+ test('updates cached token', async () => {
250
+ const expiredToken: OAuth2Token = {
251
+ accessToken: 'expired-token',
252
+ refreshToken: 'refresh-token',
253
+ expiresAt: Date.now() - 1000,
254
+ tokenType: 'Bearer',
255
+ }
256
+
257
+ const secrets = createMockSecretService({
258
+ OAUTH_TOKENS: expiredToken,
259
+ APP_CREDS: defaultAppCredential,
260
+ })
261
+
262
+ let fetchCallCount = 0
263
+ globalThis.fetch = mock.fn(async () => {
264
+ fetchCallCount++
265
+ return mockFetchResponse({
266
+ access_token: 'new-cached-token',
267
+ expires_in: 3600,
268
+ token_type: 'Bearer',
269
+ })
270
+ })
271
+
272
+ const client = new OAuth2Client(defaultConfig, 'APP_CREDS', secrets)
273
+
274
+ // First call - triggers refresh
275
+ const token1 = await client.getAccessToken()
276
+ assert.equal(token1, 'new-cached-token')
277
+
278
+ // Second call - should use cached token, no refresh
279
+ const token2 = await client.getAccessToken()
280
+ assert.equal(token2, 'new-cached-token')
281
+ assert.equal(fetchCallCount, 1) // Only one refresh call
282
+ })
283
+
284
+ test('throws on HTTP error', async () => {
285
+ const expiredToken: OAuth2Token = {
286
+ accessToken: 'expired-token',
287
+ refreshToken: 'refresh-token',
288
+ expiresAt: Date.now() - 1000,
289
+ tokenType: 'Bearer',
290
+ }
291
+
292
+ const secrets = createMockSecretService({
293
+ OAUTH_TOKENS: expiredToken,
294
+ APP_CREDS: defaultAppCredential,
295
+ })
296
+
297
+ globalThis.fetch = mock.fn(async () =>
298
+ mockFetchResponse({ error: 'invalid_grant' }, 400, false)
299
+ )
300
+
301
+ const client = new OAuth2Client(defaultConfig, 'APP_CREDS', secrets)
302
+
303
+ await assert.rejects(
304
+ async () => await client.getAccessToken(),
305
+ /Token refresh failed: 400/
306
+ )
307
+ })
308
+
309
+ test('throws when no refresh token', async () => {
310
+ const tokenWithoutRefresh: OAuth2Token = {
311
+ accessToken: 'expired-token',
312
+ // No refreshToken
313
+ expiresAt: Date.now() - 1000,
314
+ tokenType: 'Bearer',
315
+ }
316
+
317
+ const secrets = createMockSecretService({
318
+ OAUTH_TOKENS: tokenWithoutRefresh,
319
+ APP_CREDS: defaultAppCredential,
320
+ })
321
+
322
+ const client = new OAuth2Client(defaultConfig, 'APP_CREDS', secrets)
323
+
324
+ await assert.rejects(
325
+ async () => await client.getAccessToken(),
326
+ /OAuth2 token expired and no refresh token available/
327
+ )
328
+ })
329
+
330
+ test('persists refreshed token to secrets', async () => {
331
+ const expiredToken: OAuth2Token = {
332
+ accessToken: 'expired-token',
333
+ refreshToken: 'refresh-token',
334
+ expiresAt: Date.now() - 1000,
335
+ tokenType: 'Bearer',
336
+ }
337
+
338
+ const secrets = createMockSecretService({
339
+ OAUTH_TOKENS: expiredToken,
340
+ APP_CREDS: defaultAppCredential,
341
+ })
342
+
343
+ globalThis.fetch = mock.fn(async () =>
344
+ mockFetchResponse({
345
+ access_token: 'new-persisted-token',
346
+ refresh_token: 'new-refresh-token',
347
+ expires_in: 3600,
348
+ token_type: 'Bearer',
349
+ })
350
+ )
351
+
352
+ const client = new OAuth2Client(defaultConfig, 'APP_CREDS', secrets)
353
+ await client.getAccessToken()
354
+
355
+ // Verify the token was persisted to secrets
356
+ const storedToken = secrets
357
+ .getStoredSecrets()
358
+ .get('OAUTH_TOKENS') as OAuth2Token
359
+ assert.equal(storedToken.accessToken, 'new-persisted-token')
360
+ assert.equal(storedToken.refreshToken, 'new-refresh-token')
361
+ assert.ok(storedToken.expiresAt && storedToken.expiresAt > Date.now())
362
+ })
363
+
364
+ test('promise lock prevents concurrent refreshes', async () => {
365
+ const expiredToken: OAuth2Token = {
366
+ accessToken: 'expired-token',
367
+ refreshToken: 'refresh-token',
368
+ expiresAt: Date.now() - 1000,
369
+ tokenType: 'Bearer',
370
+ }
371
+
372
+ const secrets = createMockSecretService({
373
+ OAUTH_TOKENS: expiredToken,
374
+ APP_CREDS: defaultAppCredential,
375
+ })
376
+
377
+ let fetchCallCount = 0
378
+ globalThis.fetch = mock.fn(async () => {
379
+ fetchCallCount++
380
+ // Simulate network delay
381
+ await new Promise((resolve) => setTimeout(resolve, 50))
382
+ return mockFetchResponse({
383
+ access_token: 'concurrent-token',
384
+ expires_in: 3600,
385
+ token_type: 'Bearer',
386
+ })
387
+ })
388
+
389
+ const client = new OAuth2Client(defaultConfig, 'APP_CREDS', secrets)
390
+
391
+ // Trigger concurrent refresh calls
392
+ const [token1, token2, token3] = await Promise.all([
393
+ client.getAccessToken(),
394
+ client.getAccessToken(),
395
+ client.getAccessToken(),
396
+ ])
397
+
398
+ // All should get the same token
399
+ assert.equal(token1, 'concurrent-token')
400
+ assert.equal(token2, 'concurrent-token')
401
+ assert.equal(token3, 'concurrent-token')
402
+
403
+ // Only one actual refresh should have happened
404
+ assert.equal(fetchCallCount, 1)
405
+ })
406
+
407
+ test('preserves refresh token if not returned', async () => {
408
+ const expiredToken: OAuth2Token = {
409
+ accessToken: 'expired-token',
410
+ refreshToken: 'original-refresh-token',
411
+ expiresAt: Date.now() - 1000,
412
+ tokenType: 'Bearer',
413
+ }
414
+
415
+ const secrets = createMockSecretService({
416
+ OAUTH_TOKENS: expiredToken,
417
+ APP_CREDS: defaultAppCredential,
418
+ })
419
+
420
+ // Response doesn't include refresh_token
421
+ globalThis.fetch = mock.fn(async () =>
422
+ mockFetchResponse({
423
+ access_token: 'new-token',
424
+ expires_in: 3600,
425
+ token_type: 'Bearer',
426
+ // No refresh_token in response
427
+ })
428
+ )
429
+
430
+ const client = new OAuth2Client(defaultConfig, 'APP_CREDS', secrets)
431
+ await client.getAccessToken()
432
+
433
+ // Expire the new token to trigger another refresh
434
+ // The client should still have the original refresh token
435
+ // To verify this, we'd need to access internal state or trigger another refresh
436
+ // For now, we verify the first refresh worked
437
+ const token = await client.getAccessToken()
438
+ assert.equal(token, 'new-token')
439
+ })
440
+ })
441
+
442
+ describe('Request Method', () => {
443
+ test('adds Authorization header', async () => {
444
+ const validToken: OAuth2Token = {
445
+ accessToken: 'bearer-token',
446
+ expiresAt: Date.now() + 3600000,
447
+ tokenType: 'Bearer',
448
+ }
449
+
450
+ const secrets = createMockSecretService({
451
+ OAUTH_TOKENS: validToken,
452
+ APP_CREDS: defaultAppCredential,
453
+ })
454
+
455
+ let capturedHeaders: Record<string, string> | null = null
456
+
457
+ globalThis.fetch = mock.fn(
458
+ async (_url: RequestInfo | URL, options?: RequestInit) => {
459
+ capturedHeaders = options?.headers as Record<string, string>
460
+ return mockFetchResponse({ data: 'test' })
461
+ }
462
+ ) as typeof fetch
463
+
464
+ const client = new OAuth2Client(defaultConfig, 'APP_CREDS', secrets)
465
+ await client.request('https://api.example.com/resource')
466
+
467
+ assert.ok(capturedHeaders, 'Expected capturedHeaders to be set')
468
+ const headers = capturedHeaders as Record<string, string>
469
+ assert.equal(headers.Authorization, 'Bearer bearer-token')
470
+ })
471
+
472
+ test('retries on 401 with fresh token', async () => {
473
+ const validToken: OAuth2Token = {
474
+ accessToken: 'initial-token',
475
+ refreshToken: 'refresh-token',
476
+ expiresAt: Date.now() + 3600000,
477
+ tokenType: 'Bearer',
478
+ }
479
+
480
+ const secrets = createMockSecretService({
481
+ OAUTH_TOKENS: validToken,
482
+ APP_CREDS: defaultAppCredential,
483
+ })
484
+
485
+ let requestCount = 0
486
+ globalThis.fetch = mock.fn(
487
+ async (url: RequestInfo | URL, options?: RequestInit) => {
488
+ requestCount++
489
+ const urlStr = url.toString()
490
+
491
+ // Token refresh endpoint
492
+ if (urlStr === 'https://example.com/oauth/token') {
493
+ return mockFetchResponse({
494
+ access_token: 'refreshed-token',
495
+ expires_in: 3600,
496
+ token_type: 'Bearer',
497
+ })
498
+ }
499
+
500
+ // API endpoint
501
+ const headers = options?.headers as Record<string, string>
502
+ if (headers?.Authorization === 'Bearer initial-token') {
503
+ // First request returns 401
504
+ return mockFetchResponse({}, 401, false)
505
+ }
506
+
507
+ // Retry with refreshed token succeeds
508
+ return mockFetchResponse({ success: true })
509
+ }
510
+ ) as typeof fetch
511
+
512
+ const client = new OAuth2Client(defaultConfig, 'APP_CREDS', secrets)
513
+ const response = await client.request('https://api.example.com/resource')
514
+
515
+ assert.equal(response.status, 200)
516
+ assert.equal(requestCount, 3) // Initial request + refresh + retry
517
+ })
518
+
519
+ test('no double retry on 401', async () => {
520
+ const validToken: OAuth2Token = {
521
+ accessToken: 'token',
522
+ refreshToken: 'refresh-token',
523
+ expiresAt: Date.now() + 3600000,
524
+ tokenType: 'Bearer',
525
+ }
526
+
527
+ const secrets = createMockSecretService({
528
+ OAUTH_TOKENS: validToken,
529
+ APP_CREDS: defaultAppCredential,
530
+ })
531
+
532
+ let apiRequestCount = 0
533
+ globalThis.fetch = mock.fn(async (url: RequestInfo | URL) => {
534
+ const urlStr = url.toString()
535
+
536
+ if (urlStr === 'https://example.com/oauth/token') {
537
+ return mockFetchResponse({
538
+ access_token: 'new-token',
539
+ expires_in: 3600,
540
+ token_type: 'Bearer',
541
+ })
542
+ }
543
+
544
+ apiRequestCount++
545
+ // Always return 401
546
+ return mockFetchResponse({}, 401, false)
547
+ }) as typeof fetch
548
+
549
+ const client = new OAuth2Client(defaultConfig, 'APP_CREDS', secrets)
550
+ const response = await client.request('https://api.example.com/resource')
551
+
552
+ assert.equal(response.status, 401)
553
+ // Should only be 2 API requests (initial + one retry)
554
+ assert.equal(apiRequestCount, 2)
555
+ })
556
+
557
+ test('passes through other status codes', async () => {
558
+ const validToken: OAuth2Token = {
559
+ accessToken: 'token',
560
+ expiresAt: Date.now() + 3600000,
561
+ tokenType: 'Bearer',
562
+ }
563
+
564
+ const secrets = createMockSecretService({
565
+ OAUTH_TOKENS: validToken,
566
+ APP_CREDS: defaultAppCredential,
567
+ })
568
+
569
+ globalThis.fetch = mock.fn(async () =>
570
+ mockFetchResponse({ error: 'forbidden' }, 403, false)
571
+ )
572
+
573
+ const client = new OAuth2Client(defaultConfig, 'APP_CREDS', secrets)
574
+ const response = await client.request('https://api.example.com/resource')
575
+
576
+ assert.equal(response.status, 403)
577
+ })
578
+
579
+ test('preserves user headers', async () => {
580
+ const validToken: OAuth2Token = {
581
+ accessToken: 'token',
582
+ expiresAt: Date.now() + 3600000,
583
+ tokenType: 'Bearer',
584
+ }
585
+
586
+ const secrets = createMockSecretService({
587
+ OAUTH_TOKENS: validToken,
588
+ APP_CREDS: defaultAppCredential,
589
+ })
590
+
591
+ let capturedHeaders: Record<string, string> | null = null
592
+
593
+ globalThis.fetch = mock.fn(
594
+ async (_url: RequestInfo | URL, options?: RequestInit) => {
595
+ capturedHeaders = options?.headers as Record<string, string>
596
+ return mockFetchResponse({ data: 'test' })
597
+ }
598
+ ) as typeof fetch
599
+
600
+ const client = new OAuth2Client(defaultConfig, 'APP_CREDS', secrets)
601
+ await client.request('https://api.example.com/resource', {
602
+ headers: {
603
+ 'Content-Type': 'application/json',
604
+ 'X-Custom-Header': 'custom-value',
605
+ },
606
+ })
607
+
608
+ assert.ok(capturedHeaders, 'Expected capturedHeaders to be set')
609
+ const headers = capturedHeaders as Record<string, string>
610
+ assert.equal(headers['Content-Type'], 'application/json')
611
+ assert.equal(headers['X-Custom-Header'], 'custom-value')
612
+ assert.equal(headers.Authorization, 'Bearer token')
613
+ })
614
+ })
615
+
616
+ describe('Authorization URL', () => {
617
+ test('includes required params', async () => {
618
+ const secrets = createMockSecretService({
619
+ APP_CREDS: defaultAppCredential,
620
+ })
621
+
622
+ const client = new OAuth2Client(defaultConfig, 'APP_CREDS', secrets)
623
+ const url = await client.getAuthorizationUrl(
624
+ 'test-state',
625
+ 'http://localhost:3000/callback'
626
+ )
627
+
628
+ const parsed = new URL(url)
629
+ assert.equal(
630
+ parsed.origin + parsed.pathname,
631
+ 'https://example.com/oauth/authorize'
632
+ )
633
+ assert.equal(parsed.searchParams.get('response_type'), 'code')
634
+ assert.equal(parsed.searchParams.get('client_id'), 'test-client-id')
635
+ assert.equal(
636
+ parsed.searchParams.get('redirect_uri'),
637
+ 'http://localhost:3000/callback'
638
+ )
639
+ assert.equal(parsed.searchParams.get('scope'), 'read write')
640
+ assert.equal(parsed.searchParams.get('state'), 'test-state')
641
+ })
642
+
643
+ test('includes additional params', async () => {
644
+ const configWithParams: OAuth2CredentialConfig = {
645
+ ...defaultConfig,
646
+ additionalParams: {
647
+ access_type: 'offline',
648
+ prompt: 'consent',
649
+ },
650
+ }
651
+
652
+ const secrets = createMockSecretService({
653
+ APP_CREDS: defaultAppCredential,
654
+ })
655
+
656
+ const client = new OAuth2Client(configWithParams, 'APP_CREDS', secrets)
657
+ const url = await client.getAuthorizationUrl(
658
+ 'state',
659
+ 'http://localhost/callback'
660
+ )
661
+
662
+ const parsed = new URL(url)
663
+ assert.equal(parsed.searchParams.get('access_type'), 'offline')
664
+ assert.equal(parsed.searchParams.get('prompt'), 'consent')
665
+ })
666
+
667
+ test('URL-encodes values', async () => {
668
+ const configWithSpecialScopes: OAuth2CredentialConfig = {
669
+ ...defaultConfig,
670
+ scopes: ['read:user', 'write:data'],
671
+ }
672
+
673
+ const secrets = createMockSecretService({
674
+ APP_CREDS: defaultAppCredential,
675
+ })
676
+
677
+ const client = new OAuth2Client(
678
+ configWithSpecialScopes,
679
+ 'APP_CREDS',
680
+ secrets
681
+ )
682
+ const url = await client.getAuthorizationUrl(
683
+ 'state with spaces',
684
+ 'http://localhost/callback?param=value'
685
+ )
686
+
687
+ // URL should be properly encoded
688
+ assert.ok(
689
+ url.includes('state+with+spaces') ||
690
+ url.includes('state%20with%20spaces')
691
+ )
692
+ assert.ok(
693
+ url.includes(
694
+ 'redirect_uri=http%3A%2F%2Flocalhost%2Fcallback%3Fparam%3Dvalue'
695
+ )
696
+ )
697
+ })
698
+ })
699
+
700
+ describe('Code Exchange', () => {
701
+ test('makes correct HTTP request', async () => {
702
+ const secrets = createMockSecretService({
703
+ APP_CREDS: defaultAppCredential,
704
+ })
705
+
706
+ let capturedRequest: { url: string; options: RequestInit } | null = null
707
+
708
+ globalThis.fetch = mock.fn(
709
+ async (url: RequestInfo | URL, options?: RequestInit) => {
710
+ capturedRequest = { url: url.toString(), options: options! }
711
+ return mockFetchResponse({
712
+ access_token: 'exchanged-token',
713
+ refresh_token: 'refresh-token',
714
+ expires_in: 3600,
715
+ token_type: 'Bearer',
716
+ })
717
+ }
718
+ ) as typeof fetch
719
+
720
+ const client = new OAuth2Client(defaultConfig, 'APP_CREDS', secrets)
721
+ await client.exchangeCode('auth-code', 'http://localhost/callback')
722
+
723
+ assert.ok(capturedRequest, 'Expected capturedRequest to be set')
724
+ const req = capturedRequest as { url: string; options: RequestInit }
725
+ assert.equal(req.url, 'https://example.com/oauth/token')
726
+ assert.equal(req.options.method, 'POST')
727
+
728
+ const body = req.options.body as string
729
+ assert.ok(body.includes('grant_type=authorization_code'))
730
+ assert.ok(body.includes('code=auth-code'))
731
+ assert.ok(body.includes('redirect_uri=http%3A%2F%2Flocalhost%2Fcallback'))
732
+ assert.ok(body.includes('client_id=test-client-id'))
733
+ assert.ok(body.includes('client_secret=test-client-secret'))
734
+ })
735
+
736
+ test('caches resulting token', async () => {
737
+ const secrets = createMockSecretService({
738
+ APP_CREDS: defaultAppCredential,
739
+ })
740
+
741
+ let fetchCallCount = 0
742
+ globalThis.fetch = mock.fn(async () => {
743
+ fetchCallCount++
744
+ return mockFetchResponse({
745
+ access_token: 'new-token',
746
+ refresh_token: 'new-refresh',
747
+ expires_in: 3600,
748
+ token_type: 'Bearer',
749
+ })
750
+ })
751
+
752
+ const client = new OAuth2Client(defaultConfig, 'APP_CREDS', secrets)
753
+ const token = await client.exchangeCode(
754
+ 'code',
755
+ 'http://localhost/callback'
756
+ )
757
+
758
+ assert.equal(token.accessToken, 'new-token')
759
+
760
+ // Subsequent getAccessToken should use cached token
761
+ const accessToken = await client.getAccessToken()
762
+ assert.equal(accessToken, 'new-token')
763
+ assert.equal(fetchCallCount, 1) // No additional fetch
764
+ })
765
+
766
+ test('throws on HTTP error', async () => {
767
+ const secrets = createMockSecretService({
768
+ APP_CREDS: defaultAppCredential,
769
+ })
770
+
771
+ globalThis.fetch = mock.fn(async () =>
772
+ mockFetchResponse({ error: 'invalid_code' }, 400, false)
773
+ )
774
+
775
+ const client = new OAuth2Client(defaultConfig, 'APP_CREDS', secrets)
776
+
777
+ await assert.rejects(
778
+ async () =>
779
+ await client.exchangeCode('bad-code', 'http://localhost/callback'),
780
+ /Token exchange failed: 400/
781
+ )
782
+ })
783
+
784
+ test('handles missing optional fields', async () => {
785
+ const secrets = createMockSecretService({
786
+ APP_CREDS: defaultAppCredential,
787
+ })
788
+
789
+ globalThis.fetch = mock.fn(async () =>
790
+ mockFetchResponse({
791
+ access_token: 'token-only',
792
+ // No refresh_token, expires_in, or scope
793
+ })
794
+ )
795
+
796
+ const client = new OAuth2Client(defaultConfig, 'APP_CREDS', secrets)
797
+ const token = await client.exchangeCode(
798
+ 'code',
799
+ 'http://localhost/callback'
800
+ )
801
+
802
+ assert.equal(token.accessToken, 'token-only')
803
+ assert.equal(token.refreshToken, undefined)
804
+ assert.equal(token.expiresAt, undefined)
805
+ assert.equal(token.tokenType, 'Bearer') // Default
806
+ assert.equal(token.scope, undefined)
807
+ })
808
+ })
809
+
810
+ describe('Security', () => {
811
+ test('validates token response', async () => {
812
+ const secrets = createMockSecretService({
813
+ APP_CREDS: defaultAppCredential,
814
+ })
815
+
816
+ // Response without access_token
817
+ globalThis.fetch = mock.fn(async () =>
818
+ mockFetchResponse({
819
+ refresh_token: 'refresh-only',
820
+ expires_in: 3600,
821
+ })
822
+ )
823
+
824
+ const client = new OAuth2Client(defaultConfig, 'APP_CREDS', secrets)
825
+
826
+ await assert.rejects(
827
+ async () =>
828
+ await client.exchangeCode('code', 'http://localhost/callback'),
829
+ /Invalid token response: missing access_token/
830
+ )
831
+ })
832
+
833
+ test('validates access_token is a string', async () => {
834
+ const secrets = createMockSecretService({
835
+ APP_CREDS: defaultAppCredential,
836
+ })
837
+
838
+ // Response with non-string access_token
839
+ globalThis.fetch = mock.fn(async () =>
840
+ mockFetchResponse({
841
+ access_token: 12345, // Number instead of string
842
+ expires_in: 3600,
843
+ })
844
+ )
845
+
846
+ const client = new OAuth2Client(defaultConfig, 'APP_CREDS', secrets)
847
+
848
+ await assert.rejects(
849
+ async () =>
850
+ await client.exchangeCode('code', 'http://localhost/callback'),
851
+ /Invalid token response: missing access_token/
852
+ )
853
+ })
854
+
855
+ test('sanitizes error messages', async () => {
856
+ const expiredToken: OAuth2Token = {
857
+ accessToken: 'expired',
858
+ refreshToken: 'refresh',
859
+ expiresAt: Date.now() - 1000,
860
+ tokenType: 'Bearer',
861
+ }
862
+
863
+ const secrets = createMockSecretService({
864
+ OAUTH_TOKENS: expiredToken,
865
+ APP_CREDS: defaultAppCredential,
866
+ })
867
+
868
+ // Error response with sensitive info
869
+ globalThis.fetch = mock.fn(async () =>
870
+ mockFetchResponse(
871
+ {
872
+ error: 'invalid_grant',
873
+ error_description: 'Refresh token expired for user@email.com',
874
+ },
875
+ 400,
876
+ false
877
+ )
878
+ )
879
+
880
+ const client = new OAuth2Client(defaultConfig, 'APP_CREDS', secrets)
881
+
882
+ await assert.rejects(
883
+ async () => await client.getAccessToken(),
884
+ (error: Error) => {
885
+ // Should NOT contain the email or detailed error
886
+ assert.ok(!error.message.includes('user@email.com'))
887
+ assert.ok(!error.message.includes('Refresh token expired'))
888
+ // Should contain generic error info
889
+ assert.ok(error.message.includes('400'))
890
+ return true
891
+ }
892
+ )
893
+ })
894
+
895
+ test('timeout works', async () => {
896
+ const expiredToken: OAuth2Token = {
897
+ accessToken: 'expired',
898
+ refreshToken: 'refresh',
899
+ expiresAt: Date.now() - 1000,
900
+ tokenType: 'Bearer',
901
+ }
902
+
903
+ const secrets = createMockSecretService({
904
+ OAUTH_TOKENS: expiredToken,
905
+ APP_CREDS: defaultAppCredential,
906
+ })
907
+
908
+ const client = new OAuth2Client(defaultConfig, 'APP_CREDS', secrets)
909
+
910
+ // The timeout is 30 seconds, which is too long for a test
911
+ // Instead, we verify that the signal is being passed
912
+ // by checking that an abort signal is present in the fetch call
913
+ let signalPassed = false
914
+ globalThis.fetch = mock.fn(
915
+ async (_url: RequestInfo | URL, options?: RequestInit) => {
916
+ signalPassed = options?.signal instanceof AbortSignal
917
+ return mockFetchResponse({
918
+ access_token: 'token',
919
+ expires_in: 3600,
920
+ token_type: 'Bearer',
921
+ })
922
+ }
923
+ ) as typeof fetch
924
+
925
+ await client.getAccessToken()
926
+ assert.equal(signalPassed, true)
927
+ })
928
+ })
929
+ })