@kindgi/api 0.1.4-rc.5 → 0.1.5-rc.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 (507) hide show
  1. package/dist/agent-binding.d.ts +13 -0
  2. package/dist/agent-binding.d.ts.map +1 -1
  3. package/dist/agent-pins.d.ts.map +1 -1
  4. package/dist/agent-pins.js +18 -2
  5. package/dist/agent-pins.js.map +1 -1
  6. package/dist/app.d.ts +110 -17
  7. package/dist/app.d.ts.map +1 -1
  8. package/dist/app.js +163 -35
  9. package/dist/app.js.map +1 -1
  10. package/dist/block-publish.d.ts +56 -0
  11. package/dist/block-publish.d.ts.map +1 -0
  12. package/dist/block-publish.js +132 -0
  13. package/dist/block-publish.js.map +1 -0
  14. package/dist/caller.d.ts +24 -0
  15. package/dist/caller.d.ts.map +1 -0
  16. package/dist/caller.js +47 -0
  17. package/dist/caller.js.map +1 -0
  18. package/dist/capability-binding.d.ts +11 -0
  19. package/dist/capability-binding.d.ts.map +1 -1
  20. package/dist/derive-agent-version.d.ts +7 -0
  21. package/dist/derive-agent-version.d.ts.map +1 -1
  22. package/dist/derive-agent-version.js +4 -0
  23. package/dist/derive-agent-version.js.map +1 -1
  24. package/dist/errors.d.ts.map +1 -1
  25. package/dist/errors.js +73 -7
  26. package/dist/errors.js.map +1 -1
  27. package/dist/eval-case-binding.d.ts +16 -4
  28. package/dist/eval-case-binding.d.ts.map +1 -1
  29. package/dist/eval-run-binding.d.ts +31 -0
  30. package/dist/eval-run-binding.d.ts.map +1 -1
  31. package/dist/eval-run-dispatcher.d.ts +7 -0
  32. package/dist/eval-run-dispatcher.d.ts.map +1 -1
  33. package/dist/eval-run-dispatcher.js.map +1 -1
  34. package/dist/eval-sample.d.ts +11 -0
  35. package/dist/eval-sample.d.ts.map +1 -0
  36. package/dist/eval-sample.js +34 -0
  37. package/dist/eval-sample.js.map +1 -0
  38. package/dist/eval-suite-binding.d.ts +13 -0
  39. package/dist/eval-suite-binding.d.ts.map +1 -1
  40. package/dist/eval-suite-binding.js.map +1 -1
  41. package/dist/flow-binding.d.ts +18 -0
  42. package/dist/flow-binding.d.ts.map +1 -1
  43. package/dist/flow-pins.d.ts.map +1 -1
  44. package/dist/flow-pins.js +18 -2
  45. package/dist/flow-pins.js.map +1 -1
  46. package/dist/gate.d.ts.map +1 -1
  47. package/dist/gate.js +25 -0
  48. package/dist/gate.js.map +1 -1
  49. package/dist/handler-binding.d.ts +32 -0
  50. package/dist/handler-binding.d.ts.map +1 -1
  51. package/dist/hitl-binding.d.ts +23 -0
  52. package/dist/hitl-binding.d.ts.map +1 -1
  53. package/dist/identity-directory-binding.d.ts +82 -4
  54. package/dist/identity-directory-binding.d.ts.map +1 -1
  55. package/dist/identity-provider-binding.d.ts +197 -17
  56. package/dist/identity-provider-binding.d.ts.map +1 -1
  57. package/dist/improvement-pass-binding.d.ts +196 -0
  58. package/dist/improvement-pass-binding.d.ts.map +1 -0
  59. package/dist/improvement-pass-binding.js +9 -0
  60. package/dist/improvement-pass-binding.js.map +1 -0
  61. package/dist/index.d.ts +35 -17
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js +11 -3
  64. package/dist/index.js.map +1 -1
  65. package/dist/judged-dispatcher.d.ts +25 -4
  66. package/dist/judged-dispatcher.d.ts.map +1 -1
  67. package/dist/judged-dispatcher.js +43 -6
  68. package/dist/judged-dispatcher.js.map +1 -1
  69. package/dist/judgment-binding.d.ts +25 -1
  70. package/dist/judgment-binding.d.ts.map +1 -1
  71. package/dist/mcp-endpoint-binding.d.ts +7 -0
  72. package/dist/mcp-endpoint-binding.d.ts.map +1 -1
  73. package/dist/memory-binding.d.ts +124 -17
  74. package/dist/memory-binding.d.ts.map +1 -1
  75. package/dist/memory-erasure-binding.d.ts +145 -0
  76. package/dist/memory-erasure-binding.d.ts.map +1 -0
  77. package/dist/memory-erasure-binding.js +4 -0
  78. package/dist/memory-erasure-binding.js.map +1 -0
  79. package/dist/middleware/auth.d.ts +66 -13
  80. package/dist/middleware/auth.d.ts.map +1 -1
  81. package/dist/middleware/auth.js +150 -50
  82. package/dist/middleware/auth.js.map +1 -1
  83. package/dist/middleware/authorize.d.ts +6 -0
  84. package/dist/middleware/authorize.d.ts.map +1 -1
  85. package/dist/middleware/authorize.js +113 -4
  86. package/dist/middleware/authorize.js.map +1 -1
  87. package/dist/middleware/error-mapper.d.ts +5 -0
  88. package/dist/middleware/error-mapper.d.ts.map +1 -1
  89. package/dist/middleware/error-mapper.js +14 -1
  90. package/dist/middleware/error-mapper.js.map +1 -1
  91. package/dist/middleware/idempotency.d.ts +70 -5
  92. package/dist/middleware/idempotency.d.ts.map +1 -1
  93. package/dist/middleware/idempotency.js +174 -41
  94. package/dist/middleware/idempotency.js.map +1 -1
  95. package/dist/middleware/key-project.d.ts +17 -0
  96. package/dist/middleware/key-project.d.ts.map +1 -0
  97. package/dist/middleware/key-project.js +75 -0
  98. package/dist/middleware/key-project.js.map +1 -0
  99. package/dist/middleware/principal.d.ts.map +1 -1
  100. package/dist/middleware/principal.js +9 -5
  101. package/dist/middleware/principal.js.map +1 -1
  102. package/dist/middleware/project-ref.d.ts.map +1 -1
  103. package/dist/middleware/project-ref.js +3 -0
  104. package/dist/middleware/project-ref.js.map +1 -1
  105. package/dist/openapi/generate.d.ts.map +1 -1
  106. package/dist/openapi/generate.js +5 -2
  107. package/dist/openapi/generate.js.map +1 -1
  108. package/dist/openapi/operations.d.ts.map +1 -1
  109. package/dist/openapi/operations.js +918 -139
  110. package/dist/openapi/operations.js.map +1 -1
  111. package/dist/openapi/schemas.d.ts +92 -13
  112. package/dist/openapi/schemas.d.ts.map +1 -1
  113. package/dist/openapi/schemas.js +2122 -370
  114. package/dist/openapi/schemas.js.map +1 -1
  115. package/dist/person-grants-binding.d.ts +77 -0
  116. package/dist/person-grants-binding.d.ts.map +1 -0
  117. package/dist/person-grants-binding.js +4 -0
  118. package/dist/person-grants-binding.js.map +1 -0
  119. package/dist/proposal-service.d.ts +121 -0
  120. package/dist/proposal-service.d.ts.map +1 -0
  121. package/dist/proposal-service.js +447 -0
  122. package/dist/proposal-service.js.map +1 -0
  123. package/dist/proposal-status.d.ts +60 -0
  124. package/dist/proposal-status.d.ts.map +1 -0
  125. package/dist/proposal-status.js +109 -0
  126. package/dist/proposal-status.js.map +1 -0
  127. package/dist/provider-binding.d.ts +10 -0
  128. package/dist/provider-binding.d.ts.map +1 -1
  129. package/dist/publish-refused.d.ts +18 -3
  130. package/dist/publish-refused.d.ts.map +1 -1
  131. package/dist/publish-refused.js +18 -3
  132. package/dist/publish-refused.js.map +1 -1
  133. package/dist/routes/adapters.d.ts +2 -1
  134. package/dist/routes/adapters.d.ts.map +1 -1
  135. package/dist/routes/adapters.js +3 -1
  136. package/dist/routes/adapters.js.map +1 -1
  137. package/dist/routes/agent-releases.d.ts +55 -3
  138. package/dist/routes/agent-releases.d.ts.map +1 -1
  139. package/dist/routes/agent-releases.js +85 -55
  140. package/dist/routes/agent-releases.js.map +1 -1
  141. package/dist/routes/agents.d.ts +12 -1
  142. package/dist/routes/agents.d.ts.map +1 -1
  143. package/dist/routes/agents.js +100 -2
  144. package/dist/routes/agents.js.map +1 -1
  145. package/dist/routes/approvals.d.ts +15 -3
  146. package/dist/routes/approvals.d.ts.map +1 -1
  147. package/dist/routes/approvals.js +133 -122
  148. package/dist/routes/approvals.js.map +1 -1
  149. package/dist/routes/artifacts.d.ts +25 -3
  150. package/dist/routes/artifacts.d.ts.map +1 -1
  151. package/dist/routes/artifacts.js +116 -7
  152. package/dist/routes/artifacts.js.map +1 -1
  153. package/dist/routes/auth.d.ts +25 -1
  154. package/dist/routes/auth.d.ts.map +1 -1
  155. package/dist/routes/auth.js +655 -281
  156. package/dist/routes/auth.js.map +1 -1
  157. package/dist/routes/blocks.d.ts.map +1 -1
  158. package/dist/routes/blocks.js +2 -42
  159. package/dist/routes/blocks.js.map +1 -1
  160. package/dist/routes/capabilities.d.ts +2 -1
  161. package/dist/routes/capabilities.d.ts.map +1 -1
  162. package/dist/routes/capabilities.js +6 -1
  163. package/dist/routes/capabilities.js.map +1 -1
  164. package/dist/routes/compliance.d.ts +9 -10
  165. package/dist/routes/compliance.d.ts.map +1 -1
  166. package/dist/routes/compliance.js +42 -61
  167. package/dist/routes/compliance.js.map +1 -1
  168. package/dist/routes/conversations.d.ts +8 -1
  169. package/dist/routes/conversations.d.ts.map +1 -1
  170. package/dist/routes/conversations.js +87 -8
  171. package/dist/routes/conversations.js.map +1 -1
  172. package/dist/routes/cost.d.ts +7 -1
  173. package/dist/routes/cost.d.ts.map +1 -1
  174. package/dist/routes/cost.js +22 -3
  175. package/dist/routes/cost.js.map +1 -1
  176. package/dist/routes/denied.d.ts +11 -0
  177. package/dist/routes/denied.d.ts.map +1 -0
  178. package/dist/routes/denied.js +17 -0
  179. package/dist/routes/denied.js.map +1 -0
  180. package/dist/routes/deployments.d.ts +2 -1
  181. package/dist/routes/deployments.d.ts.map +1 -1
  182. package/dist/routes/deployments.js +44 -12
  183. package/dist/routes/deployments.js.map +1 -1
  184. package/dist/routes/eval-comparison.d.ts.map +1 -1
  185. package/dist/routes/eval-comparison.js +69 -1
  186. package/dist/routes/eval-comparison.js.map +1 -1
  187. package/dist/routes/eval-overrides.d.ts +18 -0
  188. package/dist/routes/eval-overrides.d.ts.map +1 -0
  189. package/dist/routes/eval-overrides.js +81 -0
  190. package/dist/routes/eval-overrides.js.map +1 -0
  191. package/dist/routes/eval-runs.d.ts +10 -1
  192. package/dist/routes/eval-runs.d.ts.map +1 -1
  193. package/dist/routes/eval-runs.js +63 -7
  194. package/dist/routes/eval-runs.js.map +1 -1
  195. package/dist/routes/eval-suites.d.ts.map +1 -1
  196. package/dist/routes/eval-suites.js +20 -1
  197. package/dist/routes/eval-suites.js.map +1 -1
  198. package/dist/routes/event-triggers.d.ts +5 -1
  199. package/dist/routes/event-triggers.d.ts.map +1 -1
  200. package/dist/routes/event-triggers.js +26 -3
  201. package/dist/routes/event-triggers.js.map +1 -1
  202. package/dist/routes/export-signing-keys.d.ts +12 -0
  203. package/dist/routes/export-signing-keys.d.ts.map +1 -0
  204. package/dist/routes/export-signing-keys.js +24 -0
  205. package/dist/routes/export-signing-keys.js.map +1 -0
  206. package/dist/routes/flows.d.ts.map +1 -1
  207. package/dist/routes/flows.js +9 -1
  208. package/dist/routes/flows.js.map +1 -1
  209. package/dist/routes/gate-policies.d.ts +4 -1
  210. package/dist/routes/gate-policies.d.ts.map +1 -1
  211. package/dist/routes/gate-policies.js +29 -5
  212. package/dist/routes/gate-policies.js.map +1 -1
  213. package/dist/routes/guardrails.d.ts +16 -14
  214. package/dist/routes/guardrails.d.ts.map +1 -1
  215. package/dist/routes/guardrails.js +39 -3
  216. package/dist/routes/guardrails.js.map +1 -1
  217. package/dist/routes/identity.d.ts +20 -4
  218. package/dist/routes/identity.d.ts.map +1 -1
  219. package/dist/routes/identity.js +246 -6
  220. package/dist/routes/identity.js.map +1 -1
  221. package/dist/routes/improvement-passes.d.ts +52 -0
  222. package/dist/routes/improvement-passes.d.ts.map +1 -0
  223. package/dist/routes/improvement-passes.js +449 -0
  224. package/dist/routes/improvement-passes.js.map +1 -0
  225. package/dist/routes/judged-suites.d.ts +50 -0
  226. package/dist/routes/judged-suites.d.ts.map +1 -1
  227. package/dist/routes/judged-suites.js +66 -27
  228. package/dist/routes/judged-suites.js.map +1 -1
  229. package/dist/routes/judgment-context.d.ts +18 -2
  230. package/dist/routes/judgment-context.d.ts.map +1 -1
  231. package/dist/routes/judgment-context.js +63 -5
  232. package/dist/routes/judgment-context.js.map +1 -1
  233. package/dist/routes/judgment-flow-context.d.ts.map +1 -1
  234. package/dist/routes/judgment-flow-context.js +14 -2
  235. package/dist/routes/judgment-flow-context.js.map +1 -1
  236. package/dist/routes/judgments.d.ts.map +1 -1
  237. package/dist/routes/judgments.js +8 -1
  238. package/dist/routes/judgments.js.map +1 -1
  239. package/dist/routes/mcp.d.ts.map +1 -1
  240. package/dist/routes/mcp.js +26 -1
  241. package/dist/routes/mcp.js.map +1 -1
  242. package/dist/routes/memory-access.d.ts +43 -0
  243. package/dist/routes/memory-access.d.ts.map +1 -0
  244. package/dist/routes/memory-access.js +129 -0
  245. package/dist/routes/memory-access.js.map +1 -0
  246. package/dist/routes/memory-erasures.d.ts +25 -0
  247. package/dist/routes/memory-erasures.d.ts.map +1 -0
  248. package/dist/routes/memory-erasures.js +214 -0
  249. package/dist/routes/memory-erasures.js.map +1 -0
  250. package/dist/routes/memory-parse.d.ts +38 -0
  251. package/dist/routes/memory-parse.d.ts.map +1 -0
  252. package/dist/routes/memory-parse.js +199 -0
  253. package/dist/routes/memory-parse.js.map +1 -0
  254. package/dist/routes/memory.d.ts +9 -4
  255. package/dist/routes/memory.d.ts.map +1 -1
  256. package/dist/routes/memory.js +226 -234
  257. package/dist/routes/memory.js.map +1 -1
  258. package/dist/routes/observations.d.ts +4 -1
  259. package/dist/routes/observations.d.ts.map +1 -1
  260. package/dist/routes/observations.js +8 -2
  261. package/dist/routes/observations.js.map +1 -1
  262. package/dist/routes/pagination.d.ts +7 -0
  263. package/dist/routes/pagination.d.ts.map +1 -1
  264. package/dist/routes/pagination.js +10 -0
  265. package/dist/routes/pagination.js.map +1 -1
  266. package/dist/routes/policies.d.ts +2 -1
  267. package/dist/routes/policies.d.ts.map +1 -1
  268. package/dist/routes/policies.js +3 -1
  269. package/dist/routes/policies.js.map +1 -1
  270. package/dist/routes/project-mismatch.d.ts +22 -0
  271. package/dist/routes/project-mismatch.d.ts.map +1 -0
  272. package/dist/routes/project-mismatch.js +34 -0
  273. package/dist/routes/project-mismatch.js.map +1 -0
  274. package/dist/routes/projects.d.ts +15 -4
  275. package/dist/routes/projects.d.ts.map +1 -1
  276. package/dist/routes/projects.js +100 -9
  277. package/dist/routes/projects.js.map +1 -1
  278. package/dist/routes/proposals.d.ts +25 -17
  279. package/dist/routes/proposals.d.ts.map +1 -1
  280. package/dist/routes/proposals.js +340 -507
  281. package/dist/routes/proposals.js.map +1 -1
  282. package/dist/routes/provenance.d.ts +17 -3
  283. package/dist/routes/provenance.d.ts.map +1 -1
  284. package/dist/routes/provenance.js +79 -103
  285. package/dist/routes/provenance.js.map +1 -1
  286. package/dist/routes/providers.d.ts +17 -1
  287. package/dist/routes/providers.d.ts.map +1 -1
  288. package/dist/routes/providers.js +188 -8
  289. package/dist/routes/providers.js.map +1 -1
  290. package/dist/routes/public-run-tokens.d.ts.map +1 -1
  291. package/dist/routes/public-run-tokens.js +3 -0
  292. package/dist/routes/public-run-tokens.js.map +1 -1
  293. package/dist/routes/runs.d.ts +10 -0
  294. package/dist/routes/runs.d.ts.map +1 -1
  295. package/dist/routes/runs.js +92 -42
  296. package/dist/routes/runs.js.map +1 -1
  297. package/dist/routes/schedules.d.ts +21 -13
  298. package/dist/routes/schedules.d.ts.map +1 -1
  299. package/dist/routes/schedules.js +416 -55
  300. package/dist/routes/schedules.js.map +1 -1
  301. package/dist/routes/segments.d.ts +2 -0
  302. package/dist/routes/segments.d.ts.map +1 -1
  303. package/dist/routes/segments.js +5 -0
  304. package/dist/routes/segments.js.map +1 -1
  305. package/dist/routes/service-accounts.d.ts +17 -0
  306. package/dist/routes/service-accounts.d.ts.map +1 -0
  307. package/dist/routes/service-accounts.js +212 -0
  308. package/dist/routes/service-accounts.js.map +1 -0
  309. package/dist/routes/sign-in-options.d.ts +50 -0
  310. package/dist/routes/sign-in-options.d.ts.map +1 -0
  311. package/dist/routes/sign-in-options.js +98 -0
  312. package/dist/routes/sign-in-options.js.map +1 -0
  313. package/dist/routes/signing-keys.d.ts +2 -1
  314. package/dist/routes/signing-keys.d.ts.map +1 -1
  315. package/dist/routes/signing-keys.js +3 -1
  316. package/dist/routes/signing-keys.js.map +1 -1
  317. package/dist/routes/tenant-access.d.ts +13 -0
  318. package/dist/routes/tenant-access.d.ts.map +1 -0
  319. package/dist/routes/tenant-access.js +26 -0
  320. package/dist/routes/tenant-access.js.map +1 -0
  321. package/dist/routes/tenant.d.ts.map +1 -1
  322. package/dist/routes/tenant.js +4 -0
  323. package/dist/routes/tenant.js.map +1 -1
  324. package/dist/routes/token-sign-in.d.ts +36 -0
  325. package/dist/routes/token-sign-in.d.ts.map +1 -0
  326. package/dist/routes/token-sign-in.js +101 -0
  327. package/dist/routes/token-sign-in.js.map +1 -0
  328. package/dist/routes/tokens.d.ts +15 -5
  329. package/dist/routes/tokens.d.ts.map +1 -1
  330. package/dist/routes/tokens.js +162 -43
  331. package/dist/routes/tokens.js.map +1 -1
  332. package/dist/routes/tools.d.ts.map +1 -1
  333. package/dist/routes/tools.js +9 -1
  334. package/dist/routes/tools.js.map +1 -1
  335. package/dist/routes/trigger-access.d.ts +29 -0
  336. package/dist/routes/trigger-access.d.ts.map +1 -0
  337. package/dist/routes/trigger-access.js +33 -0
  338. package/dist/routes/trigger-access.js.map +1 -0
  339. package/dist/routes/webhook-endpoints.d.ts +2 -1
  340. package/dist/routes/webhook-endpoints.d.ts.map +1 -1
  341. package/dist/routes/webhook-endpoints.js +11 -3
  342. package/dist/routes/webhook-endpoints.js.map +1 -1
  343. package/dist/routes/webhooks.d.ts +5 -1
  344. package/dist/routes/webhooks.d.ts.map +1 -1
  345. package/dist/routes/webhooks.js +25 -2
  346. package/dist/routes/webhooks.js.map +1 -1
  347. package/dist/run-failure.d.ts +21 -0
  348. package/dist/run-failure.d.ts.map +1 -0
  349. package/dist/run-failure.js +29 -0
  350. package/dist/run-failure.js.map +1 -0
  351. package/dist/service-account-binding.d.ts +108 -0
  352. package/dist/service-account-binding.d.ts.map +1 -0
  353. package/dist/service-account-binding.js +4 -0
  354. package/dist/service-account-binding.js.map +1 -0
  355. package/dist/session-store-binding.d.ts +61 -13
  356. package/dist/session-store-binding.d.ts.map +1 -1
  357. package/dist/signed-export.d.ts +89 -0
  358. package/dist/signed-export.d.ts.map +1 -0
  359. package/dist/signed-export.js +149 -0
  360. package/dist/signed-export.js.map +1 -0
  361. package/dist/supervisor-binding.d.ts +186 -413
  362. package/dist/supervisor-binding.d.ts.map +1 -1
  363. package/dist/testing/app-bindings.d.ts +33 -0
  364. package/dist/testing/app-bindings.d.ts.map +1 -0
  365. package/dist/testing/app-bindings.js +112 -0
  366. package/dist/testing/app-bindings.js.map +1 -0
  367. package/dist/testing/index.d.ts +11 -0
  368. package/dist/testing/index.d.ts.map +1 -0
  369. package/dist/testing/index.js +13 -0
  370. package/dist/testing/index.js.map +1 -0
  371. package/dist/testing/stub-binding.d.ts +34 -0
  372. package/dist/testing/stub-binding.d.ts.map +1 -0
  373. package/dist/testing/stub-binding.js +42 -0
  374. package/dist/testing/stub-binding.js.map +1 -0
  375. package/dist/testing/trigger-registry.d.ts +21 -0
  376. package/dist/testing/trigger-registry.d.ts.map +1 -0
  377. package/dist/testing/trigger-registry.js +206 -0
  378. package/dist/testing/trigger-registry.js.map +1 -0
  379. package/dist/token-admin.d.ts +46 -8
  380. package/dist/token-admin.d.ts.map +1 -1
  381. package/dist/token-admin.js.map +1 -1
  382. package/dist/tool-binding.d.ts +13 -0
  383. package/dist/tool-binding.d.ts.map +1 -1
  384. package/dist/trigger-binding.d.ts +2 -2
  385. package/dist/trigger-binding.d.ts.map +1 -1
  386. package/dist/trigger-binding.js +1 -1
  387. package/dist/trigger-binding.js.map +1 -1
  388. package/dist/types.d.ts +16 -0
  389. package/dist/types.d.ts.map +1 -1
  390. package/dist/webhook-endpoint-binding.d.ts +19 -3
  391. package/dist/webhook-endpoint-binding.d.ts.map +1 -1
  392. package/dist/webhook-endpoint-binding.js +1 -1
  393. package/dist/webhook-endpoint-binding.js.map +1 -1
  394. package/openapi.json +15286 -8653
  395. package/package.json +26 -22
  396. package/src/agent-binding.ts +14 -0
  397. package/src/agent-pins.ts +18 -2
  398. package/src/app.ts +366 -80
  399. package/src/block-publish.ts +181 -0
  400. package/src/caller.ts +62 -0
  401. package/src/capability-binding.ts +12 -0
  402. package/src/derive-agent-version.ts +9 -1
  403. package/src/errors.ts +73 -7
  404. package/src/eval-case-binding.ts +17 -1
  405. package/src/eval-run-binding.ts +31 -0
  406. package/src/eval-run-dispatcher.ts +7 -0
  407. package/src/eval-sample.ts +42 -0
  408. package/src/eval-suite-binding.ts +14 -0
  409. package/src/flow-binding.ts +19 -0
  410. package/src/flow-pins.ts +18 -2
  411. package/src/gate.ts +26 -0
  412. package/src/handler-binding.ts +36 -1
  413. package/src/hitl-binding.ts +24 -0
  414. package/src/identity-directory-binding.ts +85 -4
  415. package/src/identity-provider-binding.ts +216 -21
  416. package/src/improvement-pass-binding.ts +193 -0
  417. package/src/index.ts +153 -31
  418. package/src/judged-dispatcher.ts +78 -11
  419. package/src/judgment-binding.ts +25 -1
  420. package/src/mcp-endpoint-binding.ts +7 -0
  421. package/src/memory-binding.ts +121 -17
  422. package/src/memory-erasure-binding.ts +156 -0
  423. package/src/middleware/auth.ts +228 -62
  424. package/src/middleware/authorize.ts +139 -3
  425. package/src/middleware/error-mapper.ts +15 -1
  426. package/src/middleware/idempotency.ts +230 -43
  427. package/src/middleware/key-project.ts +86 -0
  428. package/src/middleware/principal.ts +9 -5
  429. package/src/middleware/project-ref.ts +3 -0
  430. package/src/openapi/generate.ts +11 -2
  431. package/src/openapi/operations.ts +1059 -142
  432. package/src/openapi/schemas.ts +2438 -485
  433. package/src/person-grants-binding.ts +76 -0
  434. package/src/proposal-service.ts +640 -0
  435. package/src/proposal-status.ts +176 -0
  436. package/src/provider-binding.ts +10 -0
  437. package/src/publish-refused.ts +38 -9
  438. package/src/routes/adapters.ts +4 -0
  439. package/src/routes/agent-releases.ts +121 -69
  440. package/src/routes/agents.ts +128 -1
  441. package/src/routes/approvals.ts +164 -154
  442. package/src/routes/artifacts.ts +181 -11
  443. package/src/routes/auth.ts +804 -336
  444. package/src/routes/blocks.ts +2 -53
  445. package/src/routes/capabilities.ts +10 -1
  446. package/src/routes/compliance.ts +60 -99
  447. package/src/routes/conversations.ts +101 -7
  448. package/src/routes/cost.ts +32 -3
  449. package/src/routes/denied.ts +28 -0
  450. package/src/routes/deployments.ts +62 -16
  451. package/src/routes/eval-comparison.ts +81 -2
  452. package/src/routes/eval-overrides.ts +108 -0
  453. package/src/routes/eval-runs.ts +86 -6
  454. package/src/routes/eval-suites.ts +25 -1
  455. package/src/routes/event-triggers.ts +27 -3
  456. package/src/routes/export-signing-keys.ts +31 -0
  457. package/src/routes/flows.ts +12 -1
  458. package/src/routes/gate-policies.ts +37 -6
  459. package/src/routes/guardrails.ts +61 -2
  460. package/src/routes/identity.ts +328 -10
  461. package/src/routes/improvement-passes.ts +541 -0
  462. package/src/routes/judged-suites.ts +113 -43
  463. package/src/routes/judgment-context.ts +76 -5
  464. package/src/routes/judgment-flow-context.ts +16 -2
  465. package/src/routes/judgments.ts +17 -3
  466. package/src/routes/mcp.ts +29 -1
  467. package/src/routes/memory-access.ts +179 -0
  468. package/src/routes/memory-erasures.ts +240 -0
  469. package/src/routes/memory-parse.ts +250 -0
  470. package/src/routes/memory.ts +244 -286
  471. package/src/routes/observations.ts +15 -2
  472. package/src/routes/pagination.ts +12 -0
  473. package/src/routes/policies.ts +7 -1
  474. package/src/routes/project-mismatch.ts +56 -0
  475. package/src/routes/projects.ts +107 -13
  476. package/src/routes/proposals.ts +370 -611
  477. package/src/routes/provenance.ts +99 -133
  478. package/src/routes/providers.ts +223 -8
  479. package/src/routes/public-run-tokens.ts +3 -0
  480. package/src/routes/runs.ts +119 -39
  481. package/src/routes/schedules.ts +472 -60
  482. package/src/routes/segments.ts +11 -0
  483. package/src/routes/service-accounts.ts +262 -0
  484. package/src/routes/sign-in-options.ts +168 -0
  485. package/src/routes/signing-keys.ts +7 -1
  486. package/src/routes/tenant-access.ts +37 -0
  487. package/src/routes/tenant.ts +6 -0
  488. package/src/routes/token-sign-in.ts +161 -0
  489. package/src/routes/tokens.ts +172 -46
  490. package/src/routes/tools.ts +12 -1
  491. package/src/routes/trigger-access.ts +72 -0
  492. package/src/routes/webhook-endpoints.ts +15 -3
  493. package/src/routes/webhooks.ts +26 -2
  494. package/src/run-failure.ts +42 -0
  495. package/src/service-account-binding.ts +113 -0
  496. package/src/session-store-binding.ts +65 -13
  497. package/src/signed-export.ts +250 -0
  498. package/src/supervisor-binding.ts +169 -409
  499. package/src/testing/app-bindings.ts +142 -0
  500. package/src/testing/index.ts +21 -0
  501. package/src/testing/stub-binding.ts +53 -0
  502. package/src/testing/trigger-registry.ts +259 -0
  503. package/src/token-admin.ts +40 -8
  504. package/src/tool-binding.ts +14 -0
  505. package/src/trigger-binding.ts +8 -1
  506. package/src/types.ts +16 -0
  507. package/src/webhook-endpoint-binding.ts +18 -3
@@ -1,12 +1,13 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  // Copyright (C) 2026 Kindgi Inc.
3
3
 
4
- import { createHash, randomBytes } from 'node:crypto';
4
+ import { createHash, randomBytes, randomUUID } from 'node:crypto';
5
5
 
6
6
  import { Hono } from 'hono';
7
7
  import type { Context } from 'hono';
8
8
 
9
- import type { SessionId, TenantId, UserId } from '@kindgi/types';
9
+ import type { AuditEventBinding } from '@kindgi/audit-events';
10
+ import type { SessionId, TenantId, Timestamp, UserId } from '@kindgi/types';
10
11
 
11
12
  import { statusFor, toWireError } from '../errors.js';
12
13
  import type {
@@ -16,11 +17,19 @@ import type {
16
17
  IdentityProviderBinding,
17
18
  ProviderConfig,
18
19
  RefreshTokenFn,
20
+ SamlAttributeMapping,
19
21
  } from '../identity-provider-binding.js';
20
22
  import { encodeSessionToken } from '../middleware/auth.js';
21
- import type { Session, SessionStoreBinding } from '../session-store-binding.js';
23
+ import type { Authorizer } from '../middleware/authorize.js';
24
+ import { withholdFromReplay } from '../middleware/idempotency.js';
25
+ import type {
26
+ Session,
27
+ SessionCreateOutput,
28
+ SessionStoreBinding,
29
+ } from '../session-store-binding.js';
22
30
  import type { OauthStateStore } from '../state-store-binding.js';
23
31
  import type { AppEnv } from '../types.js';
32
+ import { tenantResourceAccess } from './tenant-access.js';
24
33
 
25
34
  /**
26
35
  * Auth routes. Layer OAuth 2.0 / OIDC on top of the static
@@ -40,7 +49,13 @@ export interface AuthRouterOptions {
40
49
  readonly sessionStore: SessionStoreBinding;
41
50
  readonly identityProvider: IdentityProviderBinding;
42
51
  readonly stateStore: OauthStateStore;
43
- readonly exchangeCode: ExchangeCodeFn;
52
+ /**
53
+ * The deployment's own code exchange. With it, `POST /login/:providerId`
54
+ * and the callback mount (the OAuth flow run by this package); without
55
+ * it (sign-in runs elsewhere, e.g. a browser flow in the deployment),
56
+ * only the provider catalog, refresh and logout do.
57
+ */
58
+ readonly exchangeCode?: ExchangeCodeFn;
44
59
  readonly refreshToken?: RefreshTokenFn;
45
60
  /**
46
61
  * Default TTL for the CSRF/PKCE state cache entries. 10 minutes covers
@@ -48,6 +63,15 @@ export interface AuthRouterOptions {
48
63
  * without keeping a lost row around forever.
49
64
  */
50
65
  readonly stateTtlMs?: number;
66
+ /**
67
+ * With one (T243 A): the provider catalog is tenant-wide, so reading it
68
+ * needs `read` on the tenant and changing it `admin`, as for every
69
+ * tenant-wide resource (`tenantResourceAccess`). Logging in, refreshing
70
+ * and logging out are the caller's own, and stay unchecked.
71
+ */
72
+ readonly authorizer?: Authorizer;
73
+ /** `signed-out` events, best effort. */
74
+ readonly auditEvents?: AuditEventBinding;
51
75
  }
52
76
 
53
77
  const DEFAULT_STATE_TTL_MS = 10 * 60 * 1000;
@@ -72,6 +96,12 @@ export function authRouters(options: AuthRouterOptions): {
72
96
 
73
97
  const authed = new Hono<AppEnv>();
74
98
 
99
+ // The provider catalog is tenant-wide: any GET (the list, one provider,
100
+ // its sign-in URLs) needs `read` on the tenant, any change `admin`.
101
+ const providerAccess = tenantResourceAccess(options.authorizer);
102
+ authed.use('/providers', providerAccess);
103
+ authed.use('/providers/*', providerAccess);
104
+
75
105
  // ---------- GET /providers ----------
76
106
  authed.get('/providers', async (c) => {
77
107
  const tenantId = c.get('tenantId') as TenantId;
@@ -111,10 +141,122 @@ export function authRouters(options: AuthRouterOptions): {
111
141
  }
112
142
 
113
143
  const outcome = await identityProvider.register({ tenantId, config: parsed.value });
144
+ if (outcome.kind === 'invalid') {
145
+ c.status(statusFor('identity-provider-invalid') as never);
146
+ return c.json(
147
+ toWireError(
148
+ {
149
+ code: 'identity-provider-invalid',
150
+ message: outcome.message,
151
+ providerId: parsed.value.providerId,
152
+ },
153
+ requestId,
154
+ ),
155
+ );
156
+ }
114
157
  c.status(201);
115
- return c.json({ providerId: outcome.providerId });
158
+ return c.json({
159
+ providerId: outcome.providerId,
160
+ ...(outcome.provider !== undefined && {
161
+ provider: serializeProviderConfig(outcome.provider),
162
+ }),
163
+ });
164
+ });
165
+
166
+ // ---------- GET /providers/:providerId ----------
167
+ authed.get('/providers/:providerId', async (c) => {
168
+ const tenantId = c.get('tenantId') as TenantId;
169
+ const providerId = c.req.param('providerId');
170
+ const found = await identityProvider.get({ tenantId, providerId });
171
+ if (found === null) return providerNotFound(c, providerId);
172
+ return c.json(serializeProviderConfig(found));
116
173
  });
117
174
 
175
+ // ---------- GET /providers/:providerId/sign-in ----------
176
+ const signInUrls = identityProvider.signInUrls;
177
+ if (signInUrls !== undefined) {
178
+ authed.get('/providers/:providerId/sign-in', async (c) => {
179
+ const requestId = c.get('requestId');
180
+ const tenantId = c.get('tenantId') as TenantId;
181
+ const providerId = c.req.param('providerId');
182
+ const asked = c.req.query('kind');
183
+ if (asked !== undefined && asked !== 'oidc' && asked !== 'saml' && asked !== 'oauth2') {
184
+ c.status(statusFor('bad-input') as never);
185
+ return c.json(
186
+ toWireError(
187
+ { code: 'bad-input', message: '`kind` must be `oidc`, `saml` or `oauth2`' },
188
+ requestId,
189
+ ),
190
+ );
191
+ }
192
+ const registered = await identityProvider.get({ tenantId, providerId });
193
+ const kind = asked ?? registered?.kind;
194
+ if (kind === undefined) {
195
+ c.status(statusFor('bad-input') as never);
196
+ return c.json(
197
+ toWireError(
198
+ {
199
+ code: 'bad-input',
200
+ message: `No identity provider "${providerId}" is registered yet: say which \`kind\` it will be (\`?kind=oidc\` or \`?kind=saml\`)`,
201
+ },
202
+ requestId,
203
+ ),
204
+ );
205
+ }
206
+ const signIn = await signInUrls({ tenantId, providerId, kind });
207
+ if (signIn === undefined) {
208
+ c.status(statusFor('bad-input') as never);
209
+ return c.json(
210
+ toWireError(
211
+ {
212
+ code: 'bad-input',
213
+ message: `This deployment doesn't sign in with \`${kind}\` providers`,
214
+ },
215
+ requestId,
216
+ ),
217
+ );
218
+ }
219
+ return c.json({ providerId, kind, signIn, registered: registered !== null });
220
+ });
221
+ }
222
+
223
+ // ---------- PATCH /providers/:providerId ----------
224
+ const update = identityProvider.update;
225
+ if (update !== undefined) {
226
+ authed.patch('/providers/:providerId', async (c) => {
227
+ const requestId = c.get('requestId');
228
+ const tenantId = c.get('tenantId') as TenantId;
229
+ const providerId = c.req.param('providerId');
230
+
231
+ const parsedJson = await parseJsonBody(c);
232
+ if (parsedJson.kind === 'err') {
233
+ c.status(statusFor('bad-input') as never);
234
+ return c.json(toWireError(parsedJson.error, requestId));
235
+ }
236
+ const existing = await identityProvider.get({ tenantId, providerId });
237
+ if (existing === null) return providerNotFound(c, providerId);
238
+ const merged = mergeProviderChanges(existing, parsedJson.value);
239
+ const parsed = merged.kind === 'err' ? merged : parseProviderConfig(merged.value);
240
+ if (parsed.kind === 'err') {
241
+ c.status(statusFor(parsed.error.code) as never);
242
+ return c.json(toWireError(parsed.error, requestId));
243
+ }
244
+
245
+ const outcome = await update({ tenantId, config: parsed.value });
246
+ if (outcome.kind === 'not-found') return providerNotFound(c, providerId);
247
+ if (outcome.kind === 'invalid') {
248
+ c.status(statusFor('identity-provider-invalid') as never);
249
+ return c.json(
250
+ toWireError(
251
+ { code: 'identity-provider-invalid', message: outcome.message, providerId },
252
+ requestId,
253
+ ),
254
+ );
255
+ }
256
+ return c.json({ providerId, provider: serializeProviderConfig(outcome.provider) });
257
+ });
258
+ }
259
+
118
260
  // ---------- POST /providers/:providerId/unregister ----------
119
261
  authed.post('/providers/:providerId/unregister', async (c) => {
120
262
  const requestId = c.get('requestId');
@@ -138,119 +280,135 @@ export function authRouters(options: AuthRouterOptions): {
138
280
  return c.json({ providerId, unregistered: true });
139
281
  });
140
282
 
141
- // ---------- POST /login/:providerId ----------
142
- authed.post('/login/:providerId', async (c) => {
143
- const requestId = c.get('requestId');
144
- const tenantId = c.get('tenantId') as TenantId;
145
- const providerId = c.req.param('providerId');
283
+ if (exchangeCode !== undefined) {
284
+ // ---------- POST /login/:providerId ----------
285
+ authed.post('/login/:providerId', async (c) => {
286
+ const requestId = c.get('requestId');
287
+ const tenantId = c.get('tenantId') as TenantId;
288
+ const providerId = c.req.param('providerId');
146
289
 
147
- const config = await identityProvider.get({ tenantId, providerId });
148
- if (config === null) {
149
- c.status(statusFor('identity-provider-not-found') as never);
150
- return c.json(
151
- toWireError(
152
- {
153
- code: 'identity-provider-not-found',
154
- message: `No identity provider registered with id "${providerId}"`,
155
- providerId,
156
- },
157
- requestId,
158
- ),
159
- );
160
- }
290
+ const found = await identityProvider.get({ tenantId, providerId });
291
+ if (found === null) {
292
+ c.status(statusFor('identity-provider-not-found') as never);
293
+ return c.json(
294
+ toWireError(
295
+ {
296
+ code: 'identity-provider-not-found',
297
+ message: `No identity provider registered with id "${providerId}"`,
298
+ providerId,
299
+ },
300
+ requestId,
301
+ ),
302
+ );
303
+ }
304
+ const config = oauthFlowOf(found);
305
+ if (config === null) {
306
+ c.status(statusFor('invalid-provider-config') as never);
307
+ return c.json(
308
+ toWireError(
309
+ {
310
+ code: 'invalid-provider-config',
311
+ message: `Identity provider "${providerId}" (${found.kind}) signs in through the deployment's own sign-in, not this endpoint`,
312
+ providerId,
313
+ },
314
+ requestId,
315
+ ),
316
+ );
317
+ }
161
318
 
162
- let redirectUri: string | undefined;
163
- const parsedBody = await parseOptionalJsonBody(c);
164
- if (parsedBody.kind === 'err') {
165
- c.status(statusFor('bad-input') as never);
166
- return c.json(toWireError(parsedBody.error, requestId));
167
- }
168
- if (parsedBody.value !== undefined) {
169
- const raw = (parsedBody.value as { redirectUri?: unknown }).redirectUri;
170
- if (raw !== undefined) {
171
- if (typeof raw !== 'string' || raw.length === 0) {
172
- c.status(statusFor('bad-input') as never);
173
- return c.json(
174
- toWireError(
175
- { code: 'bad-input', message: '`redirectUri` must be a non-empty string' },
176
- requestId,
177
- ),
178
- );
319
+ let redirectUri: string | undefined;
320
+ const parsedBody = await parseOptionalJsonBody(c);
321
+ if (parsedBody.kind === 'err') {
322
+ c.status(statusFor('bad-input') as never);
323
+ return c.json(toWireError(parsedBody.error, requestId));
324
+ }
325
+ if (parsedBody.value !== undefined) {
326
+ const raw = (parsedBody.value as { redirectUri?: unknown }).redirectUri;
327
+ if (raw !== undefined) {
328
+ if (typeof raw !== 'string' || raw.length === 0) {
329
+ c.status(statusFor('bad-input') as never);
330
+ return c.json(
331
+ toWireError(
332
+ { code: 'bad-input', message: '`redirectUri` must be a non-empty string' },
333
+ requestId,
334
+ ),
335
+ );
336
+ }
337
+ redirectUri = raw;
179
338
  }
180
- redirectUri = raw;
181
339
  }
182
- }
183
340
 
184
- const state = base64Url(randomBytes(32));
185
- const codeVerifier = base64Url(randomBytes(64));
186
- const codeChallenge = base64Url(createHash('sha256').update(codeVerifier).digest());
187
- const effectiveRedirect =
188
- redirectUri ??
189
- (typeof config.metadata?.defaultRedirectUri === 'string'
190
- ? String(config.metadata?.defaultRedirectUri)
191
- : '');
192
- if (effectiveRedirect.length === 0) {
193
- c.status(statusFor('bad-input') as never);
194
- return c.json(
195
- toWireError(
196
- {
197
- code: 'bad-input',
198
- message:
199
- '`redirectUri` must be supplied in the body or via `metadata.defaultRedirectUri` on the provider config',
200
- },
201
- requestId,
202
- ),
203
- );
204
- }
341
+ const state = base64Url(randomBytes(32));
342
+ const codeVerifier = base64Url(randomBytes(64));
343
+ const codeChallenge = base64Url(createHash('sha256').update(codeVerifier).digest());
344
+ const effectiveRedirect =
345
+ redirectUri ??
346
+ (typeof config.metadata?.defaultRedirectUri === 'string'
347
+ ? String(config.metadata?.defaultRedirectUri)
348
+ : '');
349
+ if (effectiveRedirect.length === 0) {
350
+ c.status(statusFor('bad-input') as never);
351
+ return c.json(
352
+ toWireError(
353
+ {
354
+ code: 'bad-input',
355
+ message:
356
+ '`redirectUri` must be supplied in the body or via `metadata.defaultRedirectUri` on the provider config',
357
+ },
358
+ requestId,
359
+ ),
360
+ );
361
+ }
205
362
 
206
- // OAuth 2.1 BCP redirect-URI allowlist enforcement. Absent /
207
- // empty list means pass-through (no allowlist check); populated
208
- // list requires an exact-string match against the effective
209
- // redirect URI (either the body-supplied value or the resolved
210
- // `metadata.defaultRedirectUri`). This is the primary gate — the
211
- // callback route re-verifies against the same list for
212
- // belt-and-suspenders defense in case the allowlist tightened
213
- // between login and callback.
214
- if (
215
- config.allowedRedirectUris !== undefined &&
216
- config.allowedRedirectUris.length > 0 &&
217
- !config.allowedRedirectUris.includes(effectiveRedirect)
218
- ) {
219
- c.status(statusFor('redirect-uri-not-allowed') as never);
220
- return c.json(
221
- toWireError(
222
- {
223
- code: 'redirect-uri-not-allowed',
224
- message: `redirect_uri "${effectiveRedirect}" is not in the provider's allowedRedirectUris list`,
225
- providerId,
226
- },
227
- requestId,
228
- ),
229
- );
230
- }
363
+ // OAuth 2.1 BCP redirect-URI allowlist enforcement. Absent /
364
+ // empty list means pass-through (no allowlist check); populated
365
+ // list requires an exact-string match against the effective
366
+ // redirect URI (either the body-supplied value or the resolved
367
+ // `metadata.defaultRedirectUri`). This is the primary gate — the
368
+ // callback route re-verifies against the same list for
369
+ // belt-and-suspenders defense in case the allowlist tightened
370
+ // between login and callback.
371
+ if (
372
+ config.allowedRedirectUris !== undefined &&
373
+ config.allowedRedirectUris.length > 0 &&
374
+ !config.allowedRedirectUris.includes(effectiveRedirect)
375
+ ) {
376
+ c.status(statusFor('redirect-uri-not-allowed') as never);
377
+ return c.json(
378
+ toWireError(
379
+ {
380
+ code: 'redirect-uri-not-allowed',
381
+ message: `redirect_uri "${effectiveRedirect}" is not in the provider's allowedRedirectUris list`,
382
+ providerId,
383
+ },
384
+ requestId,
385
+ ),
386
+ );
387
+ }
231
388
 
232
- await stateStore.put({
233
- state,
234
- tenantId,
235
- providerId,
236
- codeVerifier,
237
- redirectUri: effectiveRedirect,
238
- expiresAt: Date.now() + stateTtlMs,
239
- });
389
+ await stateStore.put({
390
+ state,
391
+ tenantId,
392
+ providerId,
393
+ codeVerifier,
394
+ redirectUri: effectiveRedirect,
395
+ expiresAt: Date.now() + stateTtlMs,
396
+ });
240
397
 
241
- const authorizationUrl = buildAuthorizationUrl({
242
- config,
243
- state,
244
- codeChallenge,
245
- redirectUri: effectiveRedirect,
246
- });
247
- return c.json({
248
- authorizationUrl,
249
- state,
250
- codeChallenge,
251
- codeChallengeMethod: 'S256',
398
+ const authorizationUrl = buildAuthorizationUrl({
399
+ config,
400
+ state,
401
+ codeChallenge,
402
+ redirectUri: effectiveRedirect,
403
+ });
404
+ return c.json({
405
+ authorizationUrl,
406
+ state,
407
+ codeChallenge,
408
+ codeChallengeMethod: 'S256',
409
+ });
252
410
  });
253
- });
411
+ }
254
412
 
255
413
  // ---------- POST /refresh ----------
256
414
  authed.post('/refresh', async (c) => {
@@ -270,6 +428,20 @@ export function authRouters(options: AuthRouterOptions): {
270
428
  ),
271
429
  );
272
430
  }
431
+ if (c.get('sessionCookieName') !== undefined) {
432
+ // Refresh answers with the new token in its body, which a browser
433
+ // session must never hand to page scripts: it ends at its TTL.
434
+ c.status(statusFor('cookie-session-not-refreshable') as never);
435
+ return c.json(
436
+ toWireError(
437
+ {
438
+ code: 'cookie-session-not-refreshable',
439
+ message: 'A browser session (cookie) is not refreshed: sign in again when it ends',
440
+ },
441
+ requestId,
442
+ ),
443
+ );
444
+ }
273
445
  const current = await sessionStore.get({ tenantId, sessionId });
274
446
  if (current === null || current.revokedAt !== undefined) {
275
447
  c.status(statusFor('session-not-found') as never);
@@ -314,14 +486,14 @@ export function authRouters(options: AuthRouterOptions): {
314
486
  });
315
487
  const fresh = await sessionStore.get({ tenantId, sessionId: createdSession.sessionId });
316
488
  if (fresh === null) throw new Error('session vanished immediately after create');
317
- created = { session: fresh, rawToken: encodeSessionToken(createdSession.sessionId) };
489
+ created = { session: fresh, rawToken: sessionTokenOf(createdSession) };
318
490
  } else {
319
491
  // Rotate the framework token only; keep provider tokens as-is.
320
492
  const createdSession = await sessionStore.create({
321
493
  tenantId,
322
494
  userId: current.userId,
323
495
  providerId: current.providerId,
324
- accessToken: current.accessToken,
496
+ ...(current.accessToken !== undefined && { accessToken: current.accessToken }),
325
497
  ...(current.refreshToken !== undefined && { refreshToken: current.refreshToken }),
326
498
  expiresAt: current.expiresAt,
327
499
  scopes: current.scopes,
@@ -329,7 +501,7 @@ export function authRouters(options: AuthRouterOptions): {
329
501
  });
330
502
  const fresh = await sessionStore.get({ tenantId, sessionId: createdSession.sessionId });
331
503
  if (fresh === null) throw new Error('session vanished immediately after create');
332
- created = { session: fresh, rawToken: encodeSessionToken(createdSession.sessionId) };
504
+ created = { session: fresh, rawToken: sessionTokenOf(createdSession) };
333
505
  }
334
506
 
335
507
  // OAuth 2.1 BCP: mark the old session as ROTATED (not just revoked)
@@ -339,6 +511,8 @@ export function authRouters(options: AuthRouterOptions): {
339
511
  // a full re-auth.
340
512
  await sessionStore.revoke({ tenantId, sessionId, reason: 'rotate' });
341
513
 
514
+ // A session token: an Idempotency-Key repeat doesn't get it.
515
+ withholdFromReplay(c);
342
516
  return c.json({
343
517
  sessionToken: created.rawToken,
344
518
  sessionId: created.session.id,
@@ -347,147 +521,168 @@ export function authRouters(options: AuthRouterOptions): {
347
521
  });
348
522
 
349
523
  // ---------- POST /logout ----------
350
- authed.post('/logout', async (c) => {
351
- const requestId = c.get('requestId');
352
- const tenantId = c.get('tenantId') as TenantId;
353
- const sessionId = c.get('sessionId') as SessionId | undefined;
354
- if (sessionId === undefined) {
355
- c.status(statusFor('auth-not-session-token') as never);
356
- return c.json(
357
- toWireError(
358
- {
359
- code: 'auth-not-session-token',
360
- message:
361
- 'Logout is only valid for session tokens (`kgi_sk_*`); bearer tokens are managed via `/v1/tokens`',
362
- },
363
- requestId,
364
- ),
365
- );
366
- }
367
- const outcome = await sessionStore.revoke({ tenantId, sessionId });
368
- return c.json({ sessionId, revoked: outcome.revoked });
369
- });
524
+ authed.post('/logout', logoutHandler(sessionStore, options.auditEvents));
370
525
 
371
526
  const callback = new Hono<AppEnv>();
372
527
 
373
528
  // ---------- POST /callback/:providerId (mounted outside the bearer chain) ----------
374
- callback.post('/:providerId', async (c) => {
375
- const requestId = c.get('requestId');
376
- const providerId = c.req.param('providerId');
529
+ if (exchangeCode !== undefined) {
530
+ callback.post('/:providerId', async (c) => {
531
+ const requestId = c.get('requestId');
532
+ const providerId = c.req.param('providerId');
377
533
 
378
- const parsedJson = await parseJsonBody(c);
379
- if (parsedJson.kind === 'err') {
380
- c.status(statusFor('bad-input') as never);
381
- return c.json(toWireError(parsedJson.error, requestId));
382
- }
383
- const parsedCallback = parseCallbackBody(parsedJson.value);
384
- if (parsedCallback.kind === 'err') {
385
- c.status(statusFor(parsedCallback.error.code) as never);
386
- return c.json(toWireError(parsedCallback.error, requestId));
387
- }
388
- const { code, state } = parsedCallback.value;
389
-
390
- // `state` alone is uniquely identifying (256 bits of entropy). The
391
- // store returns the row's `tenantId` so we know which tenant the
392
- // callback belongs to — cross-checking `providerId` guards against
393
- // a stolen `state` being replayed against the wrong provider mount.
394
- const entry = await stateStore.take({ state, providerId });
395
- if (entry === null) {
396
- c.status(statusFor('oauth-state-invalid') as never);
397
- return c.json(
398
- toWireError(
399
- {
400
- code: 'oauth-state-invalid',
401
- message: '`state` is unknown, expired, or already consumed',
402
- },
403
- requestId,
404
- ),
405
- );
406
- }
534
+ const parsedJson = await parseJsonBody(c);
535
+ if (parsedJson.kind === 'err') {
536
+ c.status(statusFor('bad-input') as never);
537
+ return c.json(toWireError(parsedJson.error, requestId));
538
+ }
539
+ const parsedCallback = parseCallbackBody(parsedJson.value);
540
+ if (parsedCallback.kind === 'err') {
541
+ c.status(statusFor(parsedCallback.error.code) as never);
542
+ return c.json(toWireError(parsedCallback.error, requestId));
543
+ }
544
+ const { code, state } = parsedCallback.value;
407
545
 
408
- // Belt-and-suspenders redirect-URI check. Login already validated
409
- // the stored redirect_uri against `allowedRedirectUris` when the
410
- // row was written, but the allowlist may have tightened between
411
- // login and callback — if so, refuse the exchange rather than
412
- // handing the caller a session under a redirect the tenant no
413
- // longer trusts. Also runs when the provider config went missing
414
- // (unregister mid-flight) so we don't silently proceed.
415
- const currentConfig = await identityProvider.get({
416
- tenantId: entry.tenantId,
417
- providerId,
418
- });
419
- if (
420
- currentConfig !== null &&
421
- currentConfig.allowedRedirectUris !== undefined &&
422
- currentConfig.allowedRedirectUris.length > 0 &&
423
- !currentConfig.allowedRedirectUris.includes(entry.redirectUri)
424
- ) {
425
- c.status(statusFor('redirect-uri-mismatch') as never);
426
- return c.json(
427
- toWireError(
428
- {
429
- code: 'redirect-uri-mismatch',
430
- message:
431
- "redirect_uri from the login state row is not in the provider's current allowedRedirectUris list",
432
- providerId,
433
- },
434
- requestId,
435
- ),
436
- );
437
- }
546
+ // `state` alone is uniquely identifying (256 bits of entropy). The
547
+ // store returns the row's `tenantId` so we know which tenant the
548
+ // callback belongs to — cross-checking `providerId` guards against
549
+ // a stolen `state` being replayed against the wrong provider mount.
550
+ const entry = await stateStore.take({ state, providerId });
551
+ if (entry === null) {
552
+ c.status(statusFor('oauth-state-invalid') as never);
553
+ return c.json(
554
+ toWireError(
555
+ {
556
+ code: 'oauth-state-invalid',
557
+ message: '`state` is unknown, expired, or already consumed',
558
+ },
559
+ requestId,
560
+ ),
561
+ );
562
+ }
438
563
 
439
- let outcome: Awaited<ReturnType<ExchangeCodeFn>>;
440
- try {
441
- outcome = await exchangeCode({
564
+ // Belt-and-suspenders redirect-URI check. Login already validated
565
+ // the stored redirect_uri against `allowedRedirectUris` when the
566
+ // row was written, but the allowlist may have tightened between
567
+ // login and callback — if so, refuse the exchange rather than
568
+ // handing the caller a session under a redirect the tenant no
569
+ // longer trusts. Also runs when the provider config went missing
570
+ // (unregister mid-flight) so we don't silently proceed.
571
+ const currentConfig = await identityProvider.get({
442
572
  tenantId: entry.tenantId,
443
573
  providerId,
444
- code,
445
- codeVerifier: entry.codeVerifier,
446
- redirectUri: entry.redirectUri,
447
574
  });
448
- } catch (err) {
449
- c.status(statusFor('oauth-code-exchange-failed') as never);
450
- return c.json(
451
- toWireError(
452
- {
453
- code: 'oauth-code-exchange-failed',
454
- message: err instanceof Error ? err.message : 'code exchange failed',
455
- },
456
- requestId,
457
- ),
458
- );
459
- }
575
+ const currentAllowlist =
576
+ currentConfig === null ? undefined : oauthFlowOf(currentConfig)?.allowedRedirectUris;
577
+ if (
578
+ currentAllowlist !== undefined &&
579
+ currentAllowlist.length > 0 &&
580
+ !currentAllowlist.includes(entry.redirectUri)
581
+ ) {
582
+ c.status(statusFor('redirect-uri-mismatch') as never);
583
+ return c.json(
584
+ toWireError(
585
+ {
586
+ code: 'redirect-uri-mismatch',
587
+ message:
588
+ "redirect_uri from the login state row is not in the provider's current allowedRedirectUris list",
589
+ providerId,
590
+ },
591
+ requestId,
592
+ ),
593
+ );
594
+ }
460
595
 
461
- const created = await sessionStore.create({
462
- tenantId: entry.tenantId,
463
- userId: outcome.userId as unknown as UserId,
464
- providerId,
465
- accessToken: outcome.accessToken,
466
- ...(outcome.refreshToken !== undefined && { refreshToken: outcome.refreshToken }),
467
- expiresAt: outcome.expiresAt.toISOString() as never,
468
- scopes: outcome.scopes,
469
- ...(outcome.claims !== undefined && { metadata: outcome.claims }),
470
- });
596
+ let outcome: Awaited<ReturnType<ExchangeCodeFn>>;
597
+ try {
598
+ outcome = await exchangeCode({
599
+ tenantId: entry.tenantId,
600
+ providerId,
601
+ code,
602
+ codeVerifier: entry.codeVerifier,
603
+ redirectUri: entry.redirectUri,
604
+ });
605
+ } catch (err) {
606
+ c.status(statusFor('oauth-code-exchange-failed') as never);
607
+ return c.json(
608
+ toWireError(
609
+ {
610
+ code: 'oauth-code-exchange-failed',
611
+ message: err instanceof Error ? err.message : 'code exchange failed',
612
+ },
613
+ requestId,
614
+ ),
615
+ );
616
+ }
471
617
 
472
- c.status(201);
473
- return c.json({
474
- sessionToken: encodeSessionToken(created.sessionId),
475
- sessionId: created.sessionId,
476
- expiresAt: created.expiresAt,
618
+ const created = await sessionStore.create({
619
+ tenantId: entry.tenantId,
620
+ userId: outcome.userId as unknown as UserId,
621
+ providerId,
622
+ accessToken: outcome.accessToken,
623
+ ...(outcome.refreshToken !== undefined && { refreshToken: outcome.refreshToken }),
624
+ expiresAt: outcome.expiresAt.toISOString() as never,
625
+ scopes: outcome.scopes,
626
+ ...(outcome.claims !== undefined && { metadata: outcome.claims }),
627
+ });
628
+
629
+ // A session token: an Idempotency-Key repeat doesn't get it.
630
+ withholdFromReplay(c);
631
+ c.status(201);
632
+ return c.json({
633
+ sessionToken: sessionTokenOf(created),
634
+ sessionId: created.sessionId,
635
+ expiresAt: created.expiresAt,
636
+ });
477
637
  });
478
- });
638
+ }
479
639
 
480
640
  return { authed, callback };
481
641
  }
482
642
 
483
643
  // ---------- helpers ----------
484
644
 
645
+ /** The token a store minted, or the older `kgi_sk_<sessionId>` for a store that mints none. */
646
+ function sessionTokenOf(created: SessionCreateOutput): string {
647
+ return created.token ?? encodeSessionToken(created.sessionId);
648
+ }
649
+
485
650
  function base64Url(buf: Buffer): string {
486
651
  return buf.toString('base64url');
487
652
  }
488
653
 
654
+ /** What this package's own OAuth flow (login + callback) needs from a provider. */
655
+ interface OAuthFlowConfig {
656
+ readonly clientId: string;
657
+ readonly authorizationEndpoint: string;
658
+ readonly scopes: readonly string[];
659
+ readonly allowedRedirectUris?: readonly string[];
660
+ readonly metadata?: Record<string, unknown>;
661
+ }
662
+
663
+ const DEFAULT_OIDC_SCOPES: readonly string[] = ['openid', 'email', 'profile'];
664
+
665
+ /**
666
+ * The provider's OAuth flow settings, or `null` when it has none: a SAML
667
+ * provider, or an OIDC one whose endpoints the deployment hasn't
668
+ * discovered (it signs in through the deployment's own browser flow).
669
+ */
670
+ function oauthFlowOf(config: ProviderConfig): OAuthFlowConfig | null {
671
+ if (config.kind === 'saml') return null;
672
+ if (config.kind === 'oidc' && config.authorizationEndpoint === undefined) return null;
673
+ return {
674
+ clientId: config.clientId,
675
+ authorizationEndpoint: config.authorizationEndpoint as string,
676
+ scopes: config.scopes ?? DEFAULT_OIDC_SCOPES,
677
+ ...(config.allowedRedirectUris !== undefined && {
678
+ allowedRedirectUris: config.allowedRedirectUris,
679
+ }),
680
+ ...(config.metadata !== undefined && { metadata: config.metadata }),
681
+ };
682
+ }
683
+
489
684
  function buildAuthorizationUrl(input: {
490
- readonly config: ProviderConfig;
685
+ readonly config: OAuthFlowConfig;
491
686
  readonly state: string;
492
687
  readonly codeChallenge: string;
493
688
  readonly redirectUri: string;
@@ -504,25 +699,129 @@ function buildAuthorizationUrl(input: {
504
699
  return url.toString();
505
700
  }
506
701
 
702
+ function providerNotFound(c: Context<AppEnv>, providerId: string): Response {
703
+ c.status(statusFor('identity-provider-not-found') as never);
704
+ return c.json(
705
+ toWireError(
706
+ {
707
+ code: 'identity-provider-not-found',
708
+ message: `No identity provider registered with id "${providerId}"`,
709
+ providerId,
710
+ },
711
+ c.get('requestId'),
712
+ ),
713
+ );
714
+ }
715
+
716
+ /** What the issuer's discovery gave: stale once the issuer changes. */
717
+ const DISCOVERED_OIDC_FIELDS = [
718
+ 'authorizationEndpoint',
719
+ 'tokenEndpoint',
720
+ 'userinfoEndpoint',
721
+ 'jwksEndpoint',
722
+ ] as const;
723
+
724
+ /**
725
+ * A PATCH body merged into the stored provider: a field given replaces
726
+ * it, `null` removes it, `providerId` and `kind` can't change. `signIn`
727
+ * is the deployment's, so it never comes from the stored copy. A new
728
+ * `issuer` drops the endpoints discovered from the old one.
729
+ */
730
+ function mergeProviderChanges(
731
+ existing: ProviderConfig,
732
+ body: unknown,
733
+ ): ParsedOk<Record<string, unknown>> | ParsedErr {
734
+ if (body === null || typeof body !== 'object' || Array.isArray(body)) {
735
+ return { kind: 'err', error: { code: 'bad-input', message: 'Request body must be an object' } };
736
+ }
737
+ const changes = body as Record<string, unknown>;
738
+ if (changes.providerId !== undefined && changes.providerId !== existing.providerId) {
739
+ return invalid(
740
+ "`providerId` can't change: register the provider under the new id, then unregister this one",
741
+ );
742
+ }
743
+ if (changes.kind !== undefined && changes.kind !== existing.kind) {
744
+ return invalid(
745
+ "`kind` can't change: register a new provider of that kind, then unregister this one",
746
+ );
747
+ }
748
+ const { signIn: _deployments, ...stored } = serializeProviderConfig(existing);
749
+ const merged: Record<string, unknown> = stored;
750
+ if (
751
+ existing.kind === 'oidc' &&
752
+ changes.issuer !== undefined &&
753
+ changes.issuer !== existing.issuer
754
+ ) {
755
+ for (const field of DISCOVERED_OIDC_FIELDS) delete merged[field];
756
+ }
757
+ for (const [field, value] of Object.entries(changes)) {
758
+ if (value === null) delete merged[field];
759
+ else merged[field] = value;
760
+ }
761
+ return { kind: 'ok', value: merged };
762
+ }
763
+
507
764
  function serializeProviderConfig(c: ProviderConfig): Record<string, unknown> {
508
- // Every field except clientSecretRef (which is a REFERENCE, not the
509
- // secret itself) is safe to return. Clients need enough to render an
510
- // "sign in with X" button and know what scopes they're granting.
511
- return {
765
+ // References to secrets (`clientSecretRef`, `spSigningKeyRef`…) are safe
766
+ // to return: they aren't the secrets. Clients need enough to render a
767
+ // "Sign in with X" button and to configure the identity provider.
768
+ const base = {
512
769
  providerId: c.providerId,
513
770
  kind: c.kind,
514
- clientId: c.clientId,
515
- clientSecretRef: c.clientSecretRef,
516
- authorizationEndpoint: c.authorizationEndpoint,
517
- tokenEndpoint: c.tokenEndpoint,
518
- ...(c.userinfoEndpoint !== undefined && { userinfoEndpoint: c.userinfoEndpoint }),
519
- scopes: c.scopes,
520
- ...(c.allowedRedirectUris !== undefined && {
521
- allowedRedirectUris: c.allowedRedirectUris,
522
- }),
523
- ...(c.claimMapping !== undefined && { claimMapping: c.claimMapping }),
524
- ...(c.metadata !== undefined && { metadata: c.metadata }),
771
+ ...definedOf(c, ['displayName', 'domains', 'join', 'signIn', 'metadata']),
525
772
  };
773
+ switch (c.kind) {
774
+ case 'oauth2':
775
+ return {
776
+ ...base,
777
+ clientId: c.clientId,
778
+ clientSecretRef: c.clientSecretRef,
779
+ authorizationEndpoint: c.authorizationEndpoint,
780
+ tokenEndpoint: c.tokenEndpoint,
781
+ scopes: c.scopes,
782
+ ...definedOf(c, ['userinfoEndpoint', 'allowedRedirectUris', 'claimMapping']),
783
+ };
784
+ case 'oidc':
785
+ return {
786
+ ...base,
787
+ issuer: c.issuer,
788
+ clientId: c.clientId,
789
+ clientSecretRef: c.clientSecretRef,
790
+ ...definedOf(c, [
791
+ 'scopes',
792
+ 'authorizationEndpoint',
793
+ 'tokenEndpoint',
794
+ 'userinfoEndpoint',
795
+ 'jwksEndpoint',
796
+ 'allowedRedirectUris',
797
+ 'claimMapping',
798
+ ]),
799
+ };
800
+ case 'saml':
801
+ return {
802
+ ...base,
803
+ ...definedOf(c, [
804
+ 'idpMetadataXml',
805
+ 'idpEntityId',
806
+ 'idpSsoUrl',
807
+ 'idpCertificates',
808
+ 'spSigningKeyRef',
809
+ 'spDecryptionKeyRef',
810
+ 'wantAssertionsSigned',
811
+ 'attributeMapping',
812
+ ]),
813
+ };
814
+ }
815
+ }
816
+
817
+ /** The named fields of `obj` that are set. */
818
+ function definedOf<T extends object, K extends keyof T>(
819
+ obj: T,
820
+ keys: readonly K[],
821
+ ): Partial<Pick<T, K>> {
822
+ const out: Partial<Pick<T, K>> = {};
823
+ for (const k of keys) if (obj[k] !== undefined) out[k] = obj[k];
824
+ return out;
526
825
  }
527
826
 
528
827
  type ParsedOk<T> = { readonly kind: 'ok'; readonly value: T };
@@ -561,124 +860,234 @@ async function parseOptionalJsonBody(
561
860
  }
562
861
  }
563
862
 
564
- function parseProviderConfig(body: unknown): ParsedOk<ProviderConfig> | ParsedErr {
565
- if (body === null || typeof body !== 'object' || Array.isArray(body)) {
566
- return { kind: 'err', error: { code: 'bad-input', message: 'Request body must be an object' } };
863
+ /**
864
+ * Fields that would carry a secret itself rather than a reference to one:
865
+ * refused outright, so a secret sent by mistake is never stored.
866
+ */
867
+ const PLAINTEXT_SECRET_FIELDS: Readonly<Record<string, string>> = {
868
+ clientSecret: 'clientSecretRef',
869
+ spSigningKey: 'spSigningKeyRef',
870
+ spDecryptionKey: 'spDecryptionKeyRef',
871
+ privateKey: 'spSigningKeyRef',
872
+ };
873
+
874
+ const DOMAIN_RE = /^(?=.{1,253}$)([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/;
875
+
876
+ function invalid(message: string): ParsedErr {
877
+ return { kind: 'err', error: { code: 'invalid-provider-config', message } };
878
+ }
879
+
880
+ /** Reads typed fields off a request body, collecting the first problem. */
881
+ class FieldReader {
882
+ error: ParsedErr | undefined;
883
+ constructor(private readonly b: Record<string, unknown>) {}
884
+
885
+ str(k: string): string {
886
+ const v = this.b[k];
887
+ if (typeof v !== 'string' || v.length === 0) {
888
+ this.error ??= invalid(`Field \`${k}\` must be a non-empty string`);
889
+ return '';
890
+ }
891
+ return v;
567
892
  }
568
- const b = body as Record<string, unknown>;
569
- const requiredStr = (k: string): string | ParsedErr => {
570
- const v = b[k];
893
+
894
+ optStr(k: string): string | undefined {
895
+ const v = this.b[k];
896
+ if (v === undefined) return undefined;
571
897
  if (typeof v !== 'string' || v.length === 0) {
572
- return {
573
- kind: 'err',
574
- error: {
575
- code: 'invalid-provider-config',
576
- message: `Field \`${k}\` must be a non-empty string`,
577
- },
578
- };
898
+ this.error ??= invalid(`\`${k}\` must be a non-empty string when supplied`);
899
+ return undefined;
579
900
  }
580
901
  return v;
581
- };
582
- const providerId = requiredStr('providerId');
583
- if (typeof providerId !== 'string') return providerId;
584
- const clientId = requiredStr('clientId');
585
- if (typeof clientId !== 'string') return clientId;
586
- const clientSecretRef = requiredStr('clientSecretRef');
587
- if (typeof clientSecretRef !== 'string') return clientSecretRef;
588
- const authorizationEndpoint = requiredStr('authorizationEndpoint');
589
- if (typeof authorizationEndpoint !== 'string') return authorizationEndpoint;
590
- const tokenEndpoint = requiredStr('tokenEndpoint');
591
- if (typeof tokenEndpoint !== 'string') return tokenEndpoint;
592
-
593
- const kindRaw = b.kind;
594
- if (kindRaw !== 'oauth2' && kindRaw !== 'oidc') {
595
- return {
596
- kind: 'err',
597
- error: {
598
- code: 'invalid-provider-config',
599
- message: '`kind` must be `oauth2` or `oidc`',
600
- },
601
- };
602
902
  }
603
903
 
604
- const scopesRaw = b.scopes;
605
- if (!Array.isArray(scopesRaw) || scopesRaw.some((s) => typeof s !== 'string')) {
606
- return {
607
- kind: 'err',
608
- error: {
609
- code: 'invalid-provider-config',
610
- message: '`scopes` must be an array of strings',
611
- },
612
- };
904
+ url(k: string, required: boolean): string | undefined {
905
+ const v = required ? this.str(k) : this.optStr(k);
906
+ if (v === undefined || v === '') return v;
907
+ try {
908
+ new URL(v);
909
+ } catch {
910
+ this.error ??= invalid(`\`${k}\` must be an absolute URL`);
911
+ }
912
+ return v;
613
913
  }
614
- const scopes = scopesRaw as string[];
615
914
 
616
- const userinfoEndpointRaw = b.userinfoEndpoint;
617
- if (userinfoEndpointRaw !== undefined && typeof userinfoEndpointRaw !== 'string') {
618
- return {
619
- kind: 'err',
620
- error: {
621
- code: 'invalid-provider-config',
622
- message: '`userinfoEndpoint` must be a string when supplied',
623
- },
624
- };
915
+ strings(k: string, required: boolean): readonly string[] | undefined {
916
+ const v = this.b[k];
917
+ if (v === undefined) {
918
+ if (required) this.error ??= invalid(`\`${k}\` must be an array of strings`);
919
+ return undefined;
920
+ }
921
+ if (!Array.isArray(v) || v.some((x) => typeof x !== 'string' || x.length === 0)) {
922
+ this.error ??= invalid(`\`${k}\` must be an array of non-empty strings`);
923
+ return undefined;
924
+ }
925
+ return v as string[];
625
926
  }
626
- const metadataRaw = b.metadata;
627
- if (
628
- metadataRaw !== undefined &&
629
- (metadataRaw === null || typeof metadataRaw !== 'object' || Array.isArray(metadataRaw))
630
- ) {
631
- return {
632
- kind: 'err',
633
- error: {
634
- code: 'invalid-provider-config',
635
- message: '`metadata` must be an object when supplied',
636
- },
637
- };
927
+
928
+ bool(k: string): boolean | undefined {
929
+ const v = this.b[k];
930
+ if (v === undefined) return undefined;
931
+ if (typeof v !== 'boolean') {
932
+ this.error ??= invalid(`\`${k}\` must be a boolean when supplied`);
933
+ return undefined;
934
+ }
935
+ return v;
638
936
  }
639
937
 
640
- const allowedRedirectUrisRaw = b.allowedRedirectUris;
641
- let allowedRedirectUris: readonly string[] | undefined;
642
- if (allowedRedirectUrisRaw !== undefined) {
643
- if (
644
- !Array.isArray(allowedRedirectUrisRaw) ||
645
- allowedRedirectUrisRaw.some((s) => typeof s !== 'string' || s.length === 0)
646
- ) {
647
- return {
648
- kind: 'err',
649
- error: {
650
- code: 'invalid-provider-config',
651
- message: '`allowedRedirectUris` must be an array of non-empty strings when supplied',
652
- },
653
- };
938
+ object(k: string): Record<string, unknown> | undefined {
939
+ const v = this.b[k];
940
+ if (v === undefined) return undefined;
941
+ if (v === null || typeof v !== 'object' || Array.isArray(v)) {
942
+ this.error ??= invalid(`\`${k}\` must be an object when supplied`);
943
+ return undefined;
944
+ }
945
+ return v as Record<string, unknown>;
946
+ }
947
+ }
948
+
949
+ function parseProviderConfig(body: unknown): ParsedOk<ProviderConfig> | ParsedErr {
950
+ if (body === null || typeof body !== 'object' || Array.isArray(body)) {
951
+ return { kind: 'err', error: { code: 'bad-input', message: 'Request body must be an object' } };
952
+ }
953
+ const b = body as Record<string, unknown>;
954
+ for (const [field, instead] of Object.entries(PLAINTEXT_SECRET_FIELDS)) {
955
+ if (b[field] !== undefined) {
956
+ return invalid(
957
+ `\`${field}\` is never accepted: register \`${instead}\`, a reference the deployment resolves`,
958
+ );
654
959
  }
655
- allowedRedirectUris = allowedRedirectUrisRaw as string[];
656
960
  }
961
+ const kind = b.kind;
962
+ if (kind !== 'oauth2' && kind !== 'oidc' && kind !== 'saml') {
963
+ return invalid('`kind` must be `oidc`, `saml` or `oauth2`');
964
+ }
965
+
966
+ const r = new FieldReader(b);
967
+ const providerId = r.str('providerId');
968
+ const displayName = r.optStr('displayName');
969
+ const domainsRaw = r.strings('domains', false);
970
+ const domains = domainsRaw?.map((d) => d.toLowerCase());
971
+ if (domains?.some((d) => !DOMAIN_RE.test(d))) {
972
+ r.error ??= invalid('`domains` must be email domains, e.g. `acme.com`');
973
+ }
974
+ const join = b.join;
975
+ if (join !== undefined && join !== 'invite' && join !== 'domain') {
976
+ r.error ??= invalid('`join` must be `invite` or `domain`');
977
+ }
978
+ if (join === 'domain' && (domains === undefined || domains.length === 0)) {
979
+ r.error ??= invalid('`join: domain` needs `domains`');
980
+ }
981
+ const metadata = r.object('metadata');
982
+ const base = {
983
+ providerId,
984
+ ...(displayName !== undefined && { displayName }),
985
+ ...(domains !== undefined && { domains }),
986
+ ...(join !== undefined && { join: join as 'invite' | 'domain' }),
987
+ ...(metadata !== undefined && { metadata }),
988
+ };
657
989
 
658
- const claimMappingRaw = b.claimMapping;
659
990
  let claimMapping: ClaimMappingSpec | undefined;
660
- if (claimMappingRaw !== undefined) {
661
- const parsed = parseClaimMapping(claimMappingRaw);
991
+ if (kind !== 'saml' && b.claimMapping !== undefined) {
992
+ const parsed = parseClaimMapping(b.claimMapping);
662
993
  if (parsed.kind === 'err') return parsed;
663
994
  claimMapping = parsed.value;
664
995
  }
665
996
 
666
- return {
667
- kind: 'ok',
668
- value: {
669
- providerId,
670
- kind: kindRaw,
997
+ let value: ProviderConfig;
998
+ if (kind === 'oauth2') {
999
+ const clientId = r.str('clientId');
1000
+ const clientSecretRef = r.str('clientSecretRef');
1001
+ const authorizationEndpoint = r.str('authorizationEndpoint');
1002
+ const tokenEndpoint = r.str('tokenEndpoint');
1003
+ const userinfoEndpoint = r.optStr('userinfoEndpoint');
1004
+ const scopes = r.strings('scopes', true) ?? [];
1005
+ const allowedRedirectUris = r.strings('allowedRedirectUris', false);
1006
+ value = {
1007
+ ...base,
1008
+ kind,
671
1009
  clientId,
672
1010
  clientSecretRef,
673
1011
  authorizationEndpoint,
674
1012
  tokenEndpoint,
675
- ...(typeof userinfoEndpointRaw === 'string' && { userinfoEndpoint: userinfoEndpointRaw }),
676
1013
  scopes,
1014
+ ...(userinfoEndpoint !== undefined && { userinfoEndpoint }),
677
1015
  ...(allowedRedirectUris !== undefined && { allowedRedirectUris }),
678
1016
  ...(claimMapping !== undefined && { claimMapping }),
679
- ...(metadataRaw !== undefined && { metadata: metadataRaw as Record<string, unknown> }),
680
- },
681
- };
1017
+ };
1018
+ } else if (kind === 'oidc') {
1019
+ const issuer = r.url('issuer', true) ?? '';
1020
+ const clientId = r.str('clientId');
1021
+ const clientSecretRef = r.str('clientSecretRef');
1022
+ const scopes = r.strings('scopes', false);
1023
+ const authorizationEndpoint = r.url('authorizationEndpoint', false);
1024
+ const tokenEndpoint = r.url('tokenEndpoint', false);
1025
+ const userinfoEndpoint = r.url('userinfoEndpoint', false);
1026
+ const jwksEndpoint = r.url('jwksEndpoint', false);
1027
+ const allowedRedirectUris = r.strings('allowedRedirectUris', false);
1028
+ value = {
1029
+ ...base,
1030
+ kind,
1031
+ issuer,
1032
+ clientId,
1033
+ clientSecretRef,
1034
+ ...(scopes !== undefined && { scopes }),
1035
+ ...(authorizationEndpoint !== undefined && { authorizationEndpoint }),
1036
+ ...(tokenEndpoint !== undefined && { tokenEndpoint }),
1037
+ ...(userinfoEndpoint !== undefined && { userinfoEndpoint }),
1038
+ ...(jwksEndpoint !== undefined && { jwksEndpoint }),
1039
+ ...(allowedRedirectUris !== undefined && { allowedRedirectUris }),
1040
+ ...(claimMapping !== undefined && { claimMapping }),
1041
+ };
1042
+ } else {
1043
+ const idpMetadataXml = r.optStr('idpMetadataXml');
1044
+ const idpEntityId = r.optStr('idpEntityId');
1045
+ const idpSsoUrl = r.url('idpSsoUrl', false);
1046
+ const idpCertificates = r.strings('idpCertificates', false);
1047
+ if (
1048
+ idpMetadataXml === undefined &&
1049
+ (idpEntityId === undefined ||
1050
+ idpSsoUrl === undefined ||
1051
+ idpCertificates === undefined ||
1052
+ idpCertificates.length === 0)
1053
+ ) {
1054
+ r.error ??= invalid(
1055
+ 'A SAML provider needs `idpMetadataXml`, or `idpEntityId` + `idpSsoUrl` + `idpCertificates`',
1056
+ );
1057
+ }
1058
+ const spSigningKeyRef = r.optStr('spSigningKeyRef');
1059
+ const spDecryptionKeyRef = r.optStr('spDecryptionKeyRef');
1060
+ const wantAssertionsSigned = r.bool('wantAssertionsSigned');
1061
+ const mappingRaw = r.object('attributeMapping');
1062
+ let attributeMapping: SamlAttributeMapping | undefined;
1063
+ if (mappingRaw !== undefined) {
1064
+ const m = new FieldReader(mappingRaw);
1065
+ const userId = m.optStr('userId');
1066
+ const email = m.optStr('email');
1067
+ const displayName = m.optStr('displayName');
1068
+ if (m.error !== undefined)
1069
+ r.error ??= invalid(`\`attributeMapping\`: ${m.error.error.message}`);
1070
+ attributeMapping = {
1071
+ ...(userId !== undefined && { userId }),
1072
+ ...(email !== undefined && { email }),
1073
+ ...(displayName !== undefined && { displayName }),
1074
+ };
1075
+ }
1076
+ value = {
1077
+ ...base,
1078
+ kind,
1079
+ ...(idpMetadataXml !== undefined && { idpMetadataXml }),
1080
+ ...(idpEntityId !== undefined && { idpEntityId }),
1081
+ ...(idpSsoUrl !== undefined && { idpSsoUrl }),
1082
+ ...(idpCertificates !== undefined && { idpCertificates }),
1083
+ ...(spSigningKeyRef !== undefined && { spSigningKeyRef }),
1084
+ ...(spDecryptionKeyRef !== undefined && { spDecryptionKeyRef }),
1085
+ ...(wantAssertionsSigned !== undefined && { wantAssertionsSigned }),
1086
+ ...(attributeMapping !== undefined && { attributeMapping }),
1087
+ };
1088
+ }
1089
+ if (r.error !== undefined) return r.error;
1090
+ return { kind: 'ok', value };
682
1091
  }
683
1092
 
684
1093
  function parseClaimMapping(raw: unknown): ParsedOk<ClaimMappingSpec> | ParsedErr {
@@ -804,3 +1213,62 @@ function parseCallbackBody(
804
1213
  }
805
1214
  return { kind: 'ok', value: { code, state } };
806
1215
  }
1216
+
1217
+ /**
1218
+ * `POST /v1/auth/logout`: revokes the caller's session; a browser session
1219
+ * loses its cookie too. Also mounted on its own with cookie sessions and
1220
+ * no identity providers (console token sign-in).
1221
+ */
1222
+ export function logoutHandler(
1223
+ sessionStore: SessionStoreBinding,
1224
+ auditEvents?: AuditEventBinding,
1225
+ ): (c: Context<AppEnv>) => Promise<Response> {
1226
+ return async (c) => {
1227
+ const requestId = c.get('requestId');
1228
+ const tenantId = c.get('tenantId') as TenantId;
1229
+ const sessionId = c.get('sessionId') as SessionId | undefined;
1230
+ if (sessionId === undefined) {
1231
+ c.status(statusFor('auth-not-session-token') as never);
1232
+ return c.json(
1233
+ toWireError(
1234
+ {
1235
+ code: 'auth-not-session-token',
1236
+ message:
1237
+ 'Logout is only valid for session tokens (`kgi_sk_*`); bearer tokens are managed via `/v1/tokens`',
1238
+ },
1239
+ requestId,
1240
+ ),
1241
+ );
1242
+ }
1243
+ const outcome = await sessionStore.revoke({ tenantId, sessionId });
1244
+ const userId = c.get('userId');
1245
+ if (auditEvents !== undefined && outcome.revoked) {
1246
+ try {
1247
+ const appended = await auditEvents.append([
1248
+ {
1249
+ id: randomUUID(),
1250
+ tenantId,
1251
+ kind: 'signed-out',
1252
+ timestamp: new Date().toISOString() as Timestamp,
1253
+ actor: userId !== undefined ? `user:${userId}` : 'system',
1254
+ outcome: 'succeeded',
1255
+ payload: { v: 1, doc: { sessionId } },
1256
+ },
1257
+ ]);
1258
+ if (appended.kind === 'err') {
1259
+ c.get('log').warn(`signed-out audit event failed: ${appended.error.message}`);
1260
+ }
1261
+ } catch (cause) {
1262
+ c.get('log').warn(
1263
+ `signed-out audit event failed: ${cause instanceof Error ? cause.message : String(cause)}`,
1264
+ );
1265
+ }
1266
+ }
1267
+ const cookieName = c.get('sessionCookieName');
1268
+ if (cookieName !== undefined) {
1269
+ // A browser session: the cookie goes with it.
1270
+ c.header('Set-Cookie', `${cookieName}=; Path=/; Max-Age=0; HttpOnly; Secure; SameSite=Lax`);
1271
+ }
1272
+ return c.json({ sessionId, revoked: outcome.revoked });
1273
+ };
1274
+ }