@paperclipai/server 2026.817.0 → 2026.818.0-beta.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 (548) hide show
  1. package/dist/adapters/hermes-gateway-doc.d.ts +1 -1
  2. package/dist/adapters/hermes-gateway-doc.d.ts.map +1 -1
  3. package/dist/adapters/hermes-gateway-doc.js +1 -1
  4. package/dist/adapters/plugin-loader.d.ts.map +1 -1
  5. package/dist/adapters/plugin-loader.js +3 -2
  6. package/dist/adapters/plugin-loader.js.map +1 -1
  7. package/dist/agent-auth-jwt.d.ts.map +1 -1
  8. package/dist/agent-auth-jwt.js +8 -2
  9. package/dist/agent-auth-jwt.js.map +1 -1
  10. package/dist/app.d.ts +5 -1
  11. package/dist/app.d.ts.map +1 -1
  12. package/dist/app.js +202 -15
  13. package/dist/app.js.map +1 -1
  14. package/dist/built-ins/agents/summarizer/AGENTS.md +2 -2
  15. package/dist/built-ins/agents/summarizer/routines/refresh-stale-summaries.md +1 -0
  16. package/dist/db-errors.d.ts +9 -0
  17. package/dist/db-errors.d.ts.map +1 -1
  18. package/dist/db-errors.js +19 -0
  19. package/dist/db-errors.js.map +1 -1
  20. package/dist/errors.d.ts +1 -1
  21. package/dist/errors.d.ts.map +1 -1
  22. package/dist/errors.js +2 -2
  23. package/dist/errors.js.map +1 -1
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +162 -38
  26. package/dist/index.js.map +1 -1
  27. package/dist/lib/request-promise-memo.d.ts +6 -0
  28. package/dist/lib/request-promise-memo.d.ts.map +1 -0
  29. package/dist/lib/request-promise-memo.js +25 -0
  30. package/dist/lib/request-promise-memo.js.map +1 -0
  31. package/dist/lib/request-promise-memo.test.d.ts +2 -0
  32. package/dist/lib/request-promise-memo.test.d.ts.map +1 -0
  33. package/dist/lib/request-promise-memo.test.js +74 -0
  34. package/dist/lib/request-promise-memo.test.js.map +1 -0
  35. package/dist/middleware/auth.d.ts +13 -1
  36. package/dist/middleware/auth.d.ts.map +1 -1
  37. package/dist/middleware/auth.js +92 -41
  38. package/dist/middleware/auth.js.map +1 -1
  39. package/dist/middleware/cloud-tenant-actor.test.js +26 -1
  40. package/dist/middleware/cloud-tenant-actor.test.js.map +1 -1
  41. package/dist/middleware/error-handler.d.ts.map +1 -1
  42. package/dist/middleware/error-handler.js +11 -2
  43. package/dist/middleware/error-handler.js.map +1 -1
  44. package/dist/middleware/logger.d.ts.map +1 -1
  45. package/dist/middleware/logger.js +7 -33
  46. package/dist/middleware/logger.js.map +1 -1
  47. package/dist/middleware/private-hostname-guard.d.ts.map +1 -1
  48. package/dist/middleware/private-hostname-guard.js +11 -6
  49. package/dist/middleware/private-hostname-guard.js.map +1 -1
  50. package/dist/middleware/private-json-etag.d.ts +3 -0
  51. package/dist/middleware/private-json-etag.d.ts.map +1 -0
  52. package/dist/middleware/private-json-etag.js +37 -0
  53. package/dist/middleware/private-json-etag.js.map +1 -0
  54. package/dist/middleware/redact-sensitive.d.ts.map +1 -1
  55. package/dist/middleware/redact-sensitive.js +11 -0
  56. package/dist/middleware/redact-sensitive.js.map +1 -1
  57. package/dist/realtime/live-events-ws.d.ts +12 -0
  58. package/dist/realtime/live-events-ws.d.ts.map +1 -1
  59. package/dist/realtime/live-events-ws.js +20 -0
  60. package/dist/realtime/live-events-ws.js.map +1 -1
  61. package/dist/redaction.js +1 -1
  62. package/dist/redaction.js.map +1 -1
  63. package/dist/routes/access.d.ts +2 -0
  64. package/dist/routes/access.d.ts.map +1 -1
  65. package/dist/routes/access.js +21 -10
  66. package/dist/routes/access.js.map +1 -1
  67. package/dist/routes/agents.d.ts +42 -0
  68. package/dist/routes/agents.d.ts.map +1 -1
  69. package/dist/routes/agents.js +875 -8
  70. package/dist/routes/agents.js.map +1 -1
  71. package/dist/routes/companies.d.ts +5 -1
  72. package/dist/routes/companies.d.ts.map +1 -1
  73. package/dist/routes/companies.js +467 -30
  74. package/dist/routes/companies.js.map +1 -1
  75. package/dist/routes/environments.d.ts +12 -3
  76. package/dist/routes/environments.d.ts.map +1 -1
  77. package/dist/routes/environments.js +139 -23
  78. package/dist/routes/environments.js.map +1 -1
  79. package/dist/routes/execution-workspaces.d.ts.map +1 -1
  80. package/dist/routes/execution-workspaces.js +240 -70
  81. package/dist/routes/execution-workspaces.js.map +1 -1
  82. package/dist/routes/file-resources.d.ts +14 -1
  83. package/dist/routes/file-resources.d.ts.map +1 -1
  84. package/dist/routes/file-resources.js +182 -3
  85. package/dist/routes/file-resources.js.map +1 -1
  86. package/dist/routes/index.d.ts +2 -1
  87. package/dist/routes/index.d.ts.map +1 -1
  88. package/dist/routes/index.js +2 -1
  89. package/dist/routes/index.js.map +1 -1
  90. package/dist/routes/instance-settings.d.ts.map +1 -1
  91. package/dist/routes/instance-settings.js +20 -1
  92. package/dist/routes/instance-settings.js.map +1 -1
  93. package/dist/routes/issues.d.ts.map +1 -1
  94. package/dist/routes/issues.js +750 -235
  95. package/dist/routes/issues.js.map +1 -1
  96. package/dist/routes/onboarding-seed.d.ts +27 -0
  97. package/dist/routes/onboarding-seed.d.ts.map +1 -0
  98. package/dist/routes/onboarding-seed.js +56 -0
  99. package/dist/routes/onboarding-seed.js.map +1 -0
  100. package/dist/routes/openapi.d.ts.map +1 -1
  101. package/dist/routes/openapi.js +316 -2
  102. package/dist/routes/openapi.js.map +1 -1
  103. package/dist/routes/secrets.d.ts.map +1 -1
  104. package/dist/routes/secrets.js +11 -1
  105. package/dist/routes/secrets.js.map +1 -1
  106. package/dist/routes/setup-token-route.test.d.ts +2 -0
  107. package/dist/routes/setup-token-route.test.d.ts.map +1 -0
  108. package/dist/routes/setup-token-route.test.js +896 -0
  109. package/dist/routes/setup-token-route.test.js.map +1 -0
  110. package/dist/routes/tool-access.d.ts.map +1 -1
  111. package/dist/routes/tool-access.js +87 -52
  112. package/dist/routes/tool-access.js.map +1 -1
  113. package/dist/routes/workspace-runtime-service-authz.d.ts +11 -1
  114. package/dist/routes/workspace-runtime-service-authz.d.ts.map +1 -1
  115. package/dist/routes/workspace-runtime-service-authz.js +18 -16
  116. package/dist/routes/workspace-runtime-service-authz.js.map +1 -1
  117. package/dist/services/access.d.ts.map +1 -1
  118. package/dist/services/activity.d.ts +1 -1
  119. package/dist/services/agents.d.ts +21 -0
  120. package/dist/services/agents.d.ts.map +1 -1
  121. package/dist/services/agents.js +121 -1
  122. package/dist/services/agents.js.map +1 -1
  123. package/dist/services/attention-resolver-audience.test.d.ts +2 -0
  124. package/dist/services/attention-resolver-audience.test.d.ts.map +1 -0
  125. package/dist/services/attention-resolver-audience.test.js +81 -0
  126. package/dist/services/attention-resolver-audience.test.js.map +1 -0
  127. package/dist/services/attention.d.ts +20 -1
  128. package/dist/services/attention.d.ts.map +1 -1
  129. package/dist/services/attention.js +45 -1
  130. package/dist/services/attention.js.map +1 -1
  131. package/dist/services/authorization.d.ts +2 -2
  132. package/dist/services/authorization.d.ts.map +1 -1
  133. package/dist/services/authorization.js +89 -46
  134. package/dist/services/authorization.js.map +1 -1
  135. package/dist/services/board-auth.d.ts.map +1 -1
  136. package/dist/services/board-auth.js +40 -1
  137. package/dist/services/board-auth.js.map +1 -1
  138. package/dist/services/board-auth.test.d.ts +2 -0
  139. package/dist/services/board-auth.test.d.ts.map +1 -0
  140. package/dist/services/board-auth.test.js +34 -0
  141. package/dist/services/board-auth.test.js.map +1 -0
  142. package/dist/services/built-in-agents.d.ts.map +1 -1
  143. package/dist/services/built-in-agents.js +20 -4
  144. package/dist/services/built-in-agents.js.map +1 -1
  145. package/dist/services/bundled-plugins.d.ts +11 -1
  146. package/dist/services/bundled-plugins.d.ts.map +1 -1
  147. package/dist/services/bundled-plugins.js +46 -1
  148. package/dist/services/bundled-plugins.js.map +1 -1
  149. package/dist/services/codex-device-login-reaper.d.ts +61 -0
  150. package/dist/services/codex-device-login-reaper.d.ts.map +1 -0
  151. package/dist/services/codex-device-login-reaper.js +219 -0
  152. package/dist/services/codex-device-login-reaper.js.map +1 -0
  153. package/dist/services/codex-device-login-service.d.ts +355 -0
  154. package/dist/services/codex-device-login-service.d.ts.map +1 -0
  155. package/dist/services/codex-device-login-service.js +775 -0
  156. package/dist/services/codex-device-login-service.js.map +1 -0
  157. package/dist/services/companies.d.ts.map +1 -1
  158. package/dist/services/companies.js +7 -0
  159. package/dist/services/companies.js.map +1 -1
  160. package/dist/services/company-export-readme.js +1 -1
  161. package/dist/services/company-export-readme.js.map +1 -1
  162. package/dist/services/company-import-transfers.d.ts +55 -0
  163. package/dist/services/company-import-transfers.d.ts.map +1 -0
  164. package/dist/services/company-import-transfers.js +151 -0
  165. package/dist/services/company-import-transfers.js.map +1 -0
  166. package/dist/services/company-portability.d.ts.map +1 -1
  167. package/dist/services/company-portability.js +128 -3
  168. package/dist/services/company-portability.js.map +1 -1
  169. package/dist/services/company-transfer-runs.d.ts +136 -0
  170. package/dist/services/company-transfer-runs.d.ts.map +1 -0
  171. package/dist/services/company-transfer-runs.js +243 -0
  172. package/dist/services/company-transfer-runs.js.map +1 -0
  173. package/dist/services/cross-issue-influence-limit.d.ts +7 -1
  174. package/dist/services/cross-issue-influence-limit.d.ts.map +1 -1
  175. package/dist/services/cross-issue-influence-limit.js.map +1 -1
  176. package/dist/services/environment-config.d.ts +15 -0
  177. package/dist/services/environment-config.d.ts.map +1 -1
  178. package/dist/services/environment-config.js +56 -10
  179. package/dist/services/environment-config.js.map +1 -1
  180. package/dist/services/environment-execution-target.d.ts.map +1 -1
  181. package/dist/services/environment-execution-target.js +61 -11
  182. package/dist/services/environment-execution-target.js.map +1 -1
  183. package/dist/services/environment-run-orchestrator.d.ts +16 -1
  184. package/dist/services/environment-run-orchestrator.d.ts.map +1 -1
  185. package/dist/services/environment-run-orchestrator.js +1 -1
  186. package/dist/services/environment-run-orchestrator.js.map +1 -1
  187. package/dist/services/environment-runtime.d.ts +178 -2
  188. package/dist/services/environment-runtime.d.ts.map +1 -1
  189. package/dist/services/environment-runtime.js +1035 -96
  190. package/dist/services/environment-runtime.js.map +1 -1
  191. package/dist/services/environments.d.ts +75 -0
  192. package/dist/services/environments.d.ts.map +1 -1
  193. package/dist/services/environments.js +199 -8
  194. package/dist/services/environments.js.map +1 -1
  195. package/dist/services/execution-allowlist.d.ts +16 -4
  196. package/dist/services/execution-allowlist.d.ts.map +1 -1
  197. package/dist/services/execution-allowlist.js +35 -16
  198. package/dist/services/execution-allowlist.js.map +1 -1
  199. package/dist/services/execution-allowlist.test.js +33 -0
  200. package/dist/services/execution-allowlist.test.js.map +1 -1
  201. package/dist/services/execution-workspace-policy.d.ts +14 -1
  202. package/dist/services/execution-workspace-policy.d.ts.map +1 -1
  203. package/dist/services/execution-workspace-policy.js +29 -12
  204. package/dist/services/execution-workspace-policy.js.map +1 -1
  205. package/dist/services/execution-workspaces.d.ts +89 -0
  206. package/dist/services/execution-workspaces.d.ts.map +1 -1
  207. package/dist/services/execution-workspaces.js +997 -125
  208. package/dist/services/execution-workspaces.js.map +1 -1
  209. package/dist/services/heartbeat-run-summary.d.ts +1 -0
  210. package/dist/services/heartbeat-run-summary.d.ts.map +1 -1
  211. package/dist/services/heartbeat-run-summary.js +19 -3
  212. package/dist/services/heartbeat-run-summary.js.map +1 -1
  213. package/dist/services/heartbeat.d.ts +18 -2
  214. package/dist/services/heartbeat.d.ts.map +1 -1
  215. package/dist/services/heartbeat.js +369 -16
  216. package/dist/services/heartbeat.js.map +1 -1
  217. package/dist/services/import-write-types.d.ts +12 -0
  218. package/dist/services/import-write-types.d.ts.map +1 -1
  219. package/dist/services/index.d.ts +2 -0
  220. package/dist/services/index.d.ts.map +1 -1
  221. package/dist/services/index.js +2 -0
  222. package/dist/services/index.js.map +1 -1
  223. package/dist/services/instance-settings.d.ts +10 -1
  224. package/dist/services/instance-settings.d.ts.map +1 -1
  225. package/dist/services/instance-settings.js +16 -9
  226. package/dist/services/instance-settings.js.map +1 -1
  227. package/dist/services/issue-review-policy.d.ts +4 -0
  228. package/dist/services/issue-review-policy.d.ts.map +1 -1
  229. package/dist/services/issue-review-policy.js +18 -7
  230. package/dist/services/issue-review-policy.js.map +1 -1
  231. package/dist/services/issue-thread-interaction-resolution.d.ts +92 -0
  232. package/dist/services/issue-thread-interaction-resolution.d.ts.map +1 -0
  233. package/dist/services/issue-thread-interaction-resolution.js +198 -0
  234. package/dist/services/issue-thread-interaction-resolution.js.map +1 -0
  235. package/dist/services/issue-thread-interaction-resolution.test.d.ts +2 -0
  236. package/dist/services/issue-thread-interaction-resolution.test.d.ts.map +1 -0
  237. package/dist/services/issue-thread-interaction-resolution.test.js +152 -0
  238. package/dist/services/issue-thread-interaction-resolution.test.js.map +1 -0
  239. package/dist/services/issue-thread-interactions.d.ts +24 -4
  240. package/dist/services/issue-thread-interactions.d.ts.map +1 -1
  241. package/dist/services/issue-thread-interactions.js +360 -145
  242. package/dist/services/issue-thread-interactions.js.map +1 -1
  243. package/dist/services/issue-thread-interactions.test.js +18 -11
  244. package/dist/services/issue-thread-interactions.test.js.map +1 -1
  245. package/dist/services/issues.d.ts +3 -0
  246. package/dist/services/issues.d.ts.map +1 -1
  247. package/dist/services/issues.js +157 -33
  248. package/dist/services/issues.js.map +1 -1
  249. package/dist/services/local-service-supervisor.d.ts +2 -0
  250. package/dist/services/local-service-supervisor.d.ts.map +1 -1
  251. package/dist/services/local-service-supervisor.js +53 -3
  252. package/dist/services/local-service-supervisor.js.map +1 -1
  253. package/dist/services/managed-environments.d.ts +4 -1
  254. package/dist/services/managed-environments.d.ts.map +1 -1
  255. package/dist/services/managed-environments.js +86 -2
  256. package/dist/services/managed-environments.js.map +1 -1
  257. package/dist/services/managed-environments.test.js +164 -0
  258. package/dist/services/managed-environments.test.js.map +1 -1
  259. package/dist/services/onboarding-greeting.d.ts +7 -0
  260. package/dist/services/onboarding-greeting.d.ts.map +1 -0
  261. package/dist/services/onboarding-greeting.js +27 -0
  262. package/dist/services/onboarding-greeting.js.map +1 -0
  263. package/dist/services/onboarding-greeting.test.d.ts +2 -0
  264. package/dist/services/onboarding-greeting.test.d.ts.map +1 -0
  265. package/dist/services/onboarding-greeting.test.js +32 -0
  266. package/dist/services/onboarding-greeting.test.js.map +1 -0
  267. package/dist/services/onboarding-seed.d.ts +57 -0
  268. package/dist/services/onboarding-seed.d.ts.map +1 -0
  269. package/dist/services/onboarding-seed.js +344 -0
  270. package/dist/services/onboarding-seed.js.map +1 -0
  271. package/dist/services/plan-review-context.d.ts +9 -1
  272. package/dist/services/plan-review-context.d.ts.map +1 -1
  273. package/dist/services/plan-review-context.js +192 -0
  274. package/dist/services/plan-review-context.js.map +1 -1
  275. package/dist/services/plugin-environment-driver.d.ts +46 -0
  276. package/dist/services/plugin-environment-driver.d.ts.map +1 -1
  277. package/dist/services/plugin-environment-driver.js +50 -0
  278. package/dist/services/plugin-environment-driver.js.map +1 -1
  279. package/dist/services/plugin-host-services.d.ts +1 -0
  280. package/dist/services/plugin-host-services.d.ts.map +1 -1
  281. package/dist/services/plugin-host-services.js +51 -28
  282. package/dist/services/plugin-host-services.js.map +1 -1
  283. package/dist/services/plugin-loader.d.ts +10 -1
  284. package/dist/services/plugin-loader.d.ts.map +1 -1
  285. package/dist/services/plugin-loader.js +43 -2
  286. package/dist/services/plugin-loader.js.map +1 -1
  287. package/dist/services/plugin-worker-manager.d.ts +61 -0
  288. package/dist/services/plugin-worker-manager.d.ts.map +1 -1
  289. package/dist/services/plugin-worker-manager.js +248 -1
  290. package/dist/services/plugin-worker-manager.js.map +1 -1
  291. package/dist/services/productivity-review.d.ts.map +1 -1
  292. package/dist/services/productivity-review.js +5 -0
  293. package/dist/services/productivity-review.js.map +1 -1
  294. package/dist/services/recovery/service.d.ts.map +1 -1
  295. package/dist/services/recovery/service.js +5 -1
  296. package/dist/services/recovery/service.js.map +1 -1
  297. package/dist/services/recovery/successful-run-handoff.d.ts +16 -1
  298. package/dist/services/recovery/successful-run-handoff.d.ts.map +1 -1
  299. package/dist/services/recovery/successful-run-handoff.js +32 -0
  300. package/dist/services/recovery/successful-run-handoff.js.map +1 -1
  301. package/dist/services/recovery/successful-run-handoff.test.js +51 -1
  302. package/dist/services/recovery/successful-run-handoff.test.js.map +1 -1
  303. package/dist/services/routines.d.ts.map +1 -1
  304. package/dist/services/routines.js +48 -4
  305. package/dist/services/routines.js.map +1 -1
  306. package/dist/services/runtime-exposure/broker-client.d.ts +88 -0
  307. package/dist/services/runtime-exposure/broker-client.d.ts.map +1 -0
  308. package/dist/services/runtime-exposure/broker-client.js +217 -0
  309. package/dist/services/runtime-exposure/broker-client.js.map +1 -0
  310. package/dist/services/runtime-exposure/broker-client.test.d.ts +2 -0
  311. package/dist/services/runtime-exposure/broker-client.test.d.ts.map +1 -0
  312. package/dist/services/runtime-exposure/broker-client.test.js +185 -0
  313. package/dist/services/runtime-exposure/broker-client.test.js.map +1 -0
  314. package/dist/services/runtime-exposure/exposure-manager.d.ts +135 -0
  315. package/dist/services/runtime-exposure/exposure-manager.d.ts.map +1 -0
  316. package/dist/services/runtime-exposure/exposure-manager.js +247 -0
  317. package/dist/services/runtime-exposure/exposure-manager.js.map +1 -0
  318. package/dist/services/runtime-exposure/exposure-manager.test.d.ts +2 -0
  319. package/dist/services/runtime-exposure/exposure-manager.test.d.ts.map +1 -0
  320. package/dist/services/runtime-exposure/exposure-manager.test.js +384 -0
  321. package/dist/services/runtime-exposure/exposure-manager.test.js.map +1 -0
  322. package/dist/services/runtime-exposure/loopback-listener.d.ts +33 -0
  323. package/dist/services/runtime-exposure/loopback-listener.d.ts.map +1 -0
  324. package/dist/services/runtime-exposure/loopback-listener.js +146 -0
  325. package/dist/services/runtime-exposure/loopback-listener.js.map +1 -0
  326. package/dist/services/runtime-exposure/loopback-listener.test.d.ts +2 -0
  327. package/dist/services/runtime-exposure/loopback-listener.test.d.ts.map +1 -0
  328. package/dist/services/runtime-exposure/loopback-listener.test.js +116 -0
  329. package/dist/services/runtime-exposure/loopback-listener.test.js.map +1 -0
  330. package/dist/services/runtime-exposure/port-pair.d.ts +47 -0
  331. package/dist/services/runtime-exposure/port-pair.d.ts.map +1 -0
  332. package/dist/services/runtime-exposure/port-pair.js +57 -0
  333. package/dist/services/runtime-exposure/port-pair.js.map +1 -0
  334. package/dist/services/runtime-exposure/port-pair.test.d.ts +2 -0
  335. package/dist/services/runtime-exposure/port-pair.test.d.ts.map +1 -0
  336. package/dist/services/runtime-exposure/port-pair.test.js +92 -0
  337. package/dist/services/runtime-exposure/port-pair.test.js.map +1 -0
  338. package/dist/services/runtime-exposure/port-reservation.d.ts +211 -0
  339. package/dist/services/runtime-exposure/port-reservation.d.ts.map +1 -0
  340. package/dist/services/runtime-exposure/port-reservation.js +380 -0
  341. package/dist/services/runtime-exposure/port-reservation.js.map +1 -0
  342. package/dist/services/runtime-exposure/port-reservation.test.d.ts +2 -0
  343. package/dist/services/runtime-exposure/port-reservation.test.d.ts.map +1 -0
  344. package/dist/services/runtime-exposure/port-reservation.test.js +456 -0
  345. package/dist/services/runtime-exposure/port-reservation.test.js.map +1 -0
  346. package/dist/services/runtime-exposure/tailscale-hostname.d.ts +11 -0
  347. package/dist/services/runtime-exposure/tailscale-hostname.d.ts.map +1 -0
  348. package/dist/services/runtime-exposure/tailscale-hostname.js +36 -0
  349. package/dist/services/runtime-exposure/tailscale-hostname.js.map +1 -0
  350. package/dist/services/runtime-exposure/tailscale-hostname.test.d.ts +2 -0
  351. package/dist/services/runtime-exposure/tailscale-hostname.test.d.ts.map +1 -0
  352. package/dist/services/runtime-exposure/tailscale-hostname.test.js +14 -0
  353. package/dist/services/runtime-exposure/tailscale-hostname.test.js.map +1 -0
  354. package/dist/services/sandbox-orphan-cleanup-spool.d.ts +64 -0
  355. package/dist/services/sandbox-orphan-cleanup-spool.d.ts.map +1 -0
  356. package/dist/services/sandbox-orphan-cleanup-spool.js +134 -0
  357. package/dist/services/sandbox-orphan-cleanup-spool.js.map +1 -0
  358. package/dist/services/sandbox-provider-runtime.d.ts +15 -0
  359. package/dist/services/sandbox-provider-runtime.d.ts.map +1 -1
  360. package/dist/services/sandbox-provider-runtime.js +1 -0
  361. package/dist/services/sandbox-provider-runtime.js.map +1 -1
  362. package/dist/services/secrets.d.ts +103 -0
  363. package/dist/services/secrets.d.ts.map +1 -1
  364. package/dist/services/secrets.js +432 -10
  365. package/dist/services/secrets.js.map +1 -1
  366. package/dist/services/setup-token-session.d.ts +630 -0
  367. package/dist/services/setup-token-session.d.ts.map +1 -0
  368. package/dist/services/setup-token-session.js +1005 -0
  369. package/dist/services/setup-token-session.js.map +1 -0
  370. package/dist/services/setup-token-session.test.d.ts +2 -0
  371. package/dist/services/setup-token-session.test.d.ts.map +1 -0
  372. package/dist/services/setup-token-session.test.js +1143 -0
  373. package/dist/services/setup-token-session.test.js.map +1 -0
  374. package/dist/services/setup-token-transport-binding.d.ts +202 -0
  375. package/dist/services/setup-token-transport-binding.d.ts.map +1 -0
  376. package/dist/services/setup-token-transport-binding.js +386 -0
  377. package/dist/services/setup-token-transport-binding.js.map +1 -0
  378. package/dist/services/setup-token-transport-binding.test.d.ts +2 -0
  379. package/dist/services/setup-token-transport-binding.test.d.ts.map +1 -0
  380. package/dist/services/setup-token-transport-binding.test.js +761 -0
  381. package/dist/services/setup-token-transport-binding.test.js.map +1 -0
  382. package/dist/services/summary-slots.d.ts.map +1 -1
  383. package/dist/services/summary-slots.js +40 -6
  384. package/dist/services/summary-slots.js.map +1 -1
  385. package/dist/services/task-watchdogs.js +2 -2
  386. package/dist/services/task-watchdogs.js.map +1 -1
  387. package/dist/services/tool-access-policy.d.ts +1 -1
  388. package/dist/services/tool-access.d.ts.map +1 -1
  389. package/dist/services/tool-access.js +29 -11
  390. package/dist/services/tool-access.js.map +1 -1
  391. package/dist/services/tool-gateway.d.ts +1 -1
  392. package/dist/services/tool-gateway.d.ts.map +1 -1
  393. package/dist/services/tool-gateway.js +56 -5
  394. package/dist/services/tool-gateway.js.map +1 -1
  395. package/dist/services/workspace-file-resources.d.ts +10 -1
  396. package/dist/services/workspace-file-resources.d.ts.map +1 -1
  397. package/dist/services/workspace-file-resources.js +258 -6
  398. package/dist/services/workspace-file-resources.js.map +1 -1
  399. package/dist/services/workspace-git-operation-scheduler.d.ts +135 -0
  400. package/dist/services/workspace-git-operation-scheduler.d.ts.map +1 -0
  401. package/dist/services/workspace-git-operation-scheduler.js +655 -0
  402. package/dist/services/workspace-git-operation-scheduler.js.map +1 -0
  403. package/dist/services/workspace-git-operation-scheduler.test.d.ts +2 -0
  404. package/dist/services/workspace-git-operation-scheduler.test.d.ts.map +1 -0
  405. package/dist/services/workspace-git-operation-scheduler.test.js +367 -0
  406. package/dist/services/workspace-git-operation-scheduler.test.js.map +1 -0
  407. package/dist/services/workspace-operations.d.ts +60 -0
  408. package/dist/services/workspace-operations.d.ts.map +1 -1
  409. package/dist/services/workspace-operations.js +384 -19
  410. package/dist/services/workspace-operations.js.map +1 -1
  411. package/dist/services/workspace-runtime-exposure-backfill.test.d.ts +2 -0
  412. package/dist/services/workspace-runtime-exposure-backfill.test.d.ts.map +1 -0
  413. package/dist/services/workspace-runtime-exposure-backfill.test.js +149 -0
  414. package/dist/services/workspace-runtime-exposure-backfill.test.js.map +1 -0
  415. package/dist/services/workspace-runtime-exposure.test.d.ts +2 -0
  416. package/dist/services/workspace-runtime-exposure.test.d.ts.map +1 -0
  417. package/dist/services/workspace-runtime-exposure.test.js +517 -0
  418. package/dist/services/workspace-runtime-exposure.test.js.map +1 -0
  419. package/dist/services/workspace-runtime-leases.d.ts +90 -0
  420. package/dist/services/workspace-runtime-leases.d.ts.map +1 -0
  421. package/dist/services/workspace-runtime-leases.js +252 -0
  422. package/dist/services/workspace-runtime-leases.js.map +1 -0
  423. package/dist/services/workspace-runtime-read-model.d.ts +12 -0
  424. package/dist/services/workspace-runtime-read-model.d.ts.map +1 -1
  425. package/dist/services/workspace-runtime-read-model.test.js +3 -0
  426. package/dist/services/workspace-runtime-read-model.test.js.map +1 -1
  427. package/dist/services/workspace-runtime-ready-comment.test.js +1 -0
  428. package/dist/services/workspace-runtime-ready-comment.test.js.map +1 -1
  429. package/dist/services/workspace-runtime.d.ts +160 -2
  430. package/dist/services/workspace-runtime.d.ts.map +1 -1
  431. package/dist/services/workspace-runtime.js +1693 -141
  432. package/dist/services/workspace-runtime.js.map +1 -1
  433. package/dist/shutdown.d.ts +24 -0
  434. package/dist/shutdown.d.ts.map +1 -1
  435. package/dist/shutdown.js +37 -0
  436. package/dist/shutdown.js.map +1 -1
  437. package/dist/shutdown.test.js +113 -1
  438. package/dist/shutdown.test.js.map +1 -1
  439. package/dist/startup-banner.js +1 -1
  440. package/dist/startup-banner.js.map +1 -1
  441. package/dist/url-utils.d.ts +26 -0
  442. package/dist/url-utils.d.ts.map +1 -0
  443. package/dist/url-utils.js +59 -0
  444. package/dist/url-utils.js.map +1 -0
  445. package/dist/url-utils.test.d.ts +2 -0
  446. package/dist/url-utils.test.d.ts.map +1 -0
  447. package/dist/url-utils.test.js +55 -0
  448. package/dist/url-utils.test.js.map +1 -0
  449. package/dist/worktree-config.d.ts.map +1 -1
  450. package/dist/worktree-config.js +3 -17
  451. package/dist/worktree-config.js.map +1 -1
  452. package/package.json +20 -20
  453. package/skills/paperclip/SKILL.md +16 -11
  454. package/skills/paperclip/references/api-reference.md +8 -5
  455. package/skills/paperclip-board/SKILL.md +1 -1
  456. package/ui-dist/assets/{abnfDiagram-VRR7QNED-wvSnvD6-.js → abnfDiagram-N423BO3Z-BvgoV-u5.js} +1 -1
  457. package/ui-dist/assets/{arc-Bx0V1JsN.js → arc-BInnUBUU.js} +1 -1
  458. package/ui-dist/assets/architectureDiagram-T3A2C74G-Dg4MziOm.js +36 -0
  459. package/ui-dist/assets/{blockDiagram-677ZJIJ3-BROcLaUk.js → blockDiagram-VBNYF7ZC-D9kzAkLN.js} +1 -1
  460. package/ui-dist/assets/{c4Diagram-LMCZKHZV-D-V3kgju.js → c4Diagram-5PPSVZJV-BwzsgV_0.js} +1 -1
  461. package/ui-dist/assets/channel-qoSYbCn0.js +1 -0
  462. package/ui-dist/assets/{chunk-32BRIVSS-qillH3H6.js → chunk-2GRJ4B5K-D2EQ9vrl.js} +1 -1
  463. package/ui-dist/assets/{chunk-2Q5K7J3B-D19cVnii.js → chunk-2Q5K7J3B-DOA_8fSd.js} +1 -1
  464. package/ui-dist/assets/{chunk-EX3LRPZG-CLUswrZg.js → chunk-5RXB4S5H-CT5ukRiT.js} +1 -1
  465. package/ui-dist/assets/{chunk-5VM5RSS4-Cwlp02Lx.js → chunk-5VM5RSS4-D2gOlkyB.js} +1 -1
  466. package/ui-dist/assets/{chunk-MOJQB5TN-yKciyq6E.js → chunk-6Q2QTUOP-DUb6aG8c.js} +1 -1
  467. package/ui-dist/assets/{chunk-V7JOEXUC-DjJ75g0_.js → chunk-GF5L2VYU-BPabG3Cx.js} +1 -1
  468. package/ui-dist/assets/{chunk-JWPE2WC7-BjepyzEb.js → chunk-JWPE2WC7-scNfhlZz.js} +1 -1
  469. package/ui-dist/assets/{chunk-VR4S4FIN-CiEmy8-R.js → chunk-KBJHAD2P-BcRAzRpq.js} +1 -1
  470. package/ui-dist/assets/{chunk-RYQCIY6F-ed7mQM9j.js → chunk-RYQCIY6F-BiZ0-EGh.js} +1 -1
  471. package/ui-dist/assets/{chunk-XXDRQBXY-BiI83zoO.js → chunk-XXDRQBXY-lJULxFIE.js} +1 -1
  472. package/ui-dist/assets/classDiagram-JCYQIIEL-B24u1REs.js +1 -0
  473. package/ui-dist/assets/classDiagram-v2-OCEON4UE-B24u1REs.js +1 -0
  474. package/ui-dist/assets/{cose-bilkent-JH36ORCC-DIdQf5ta.js → cose-bilkent-JH36ORCC-Do8pe8JB.js} +1 -1
  475. package/ui-dist/assets/{cynefin-VYW2F7L2-B2m9xxh8.js → cynefin-VYW2F7L2-B34jJY1x.js} +1 -1
  476. package/ui-dist/assets/{cynefinDiagram-TSTJHNR4-CGVdB0kX.js → cynefinDiagram-MW4NZA55-DQ8QK8JK.js} +1 -1
  477. package/ui-dist/assets/cytoscape.esm-DShqj__B.js +321 -0
  478. package/ui-dist/assets/{dagre-VKFMJZFB-CskKiZwJ.js → dagre-VZM6K2ZE-B7lSKgxH.js} +1 -1
  479. package/ui-dist/assets/{diagram-OA4YK3LP-D2cn5eIN.js → diagram-7IWD3JNH-B3hLAFGE.js} +1 -1
  480. package/ui-dist/assets/{diagram-FQU43EPY-oa5Zu0XG.js → diagram-B4RE2ZJO-CH9Ro9e0.js} +1 -1
  481. package/ui-dist/assets/{diagram-NH7WQ7WH-fymti5Js.js → diagram-LBJQPF4R-DdPVMii7.js} +1 -1
  482. package/ui-dist/assets/{diagram-G47NLZAW-BWF2ZCsw.js → diagram-Q27KOJAE-f5VXswjc.js} +1 -1
  483. package/ui-dist/assets/diagram-UB23O5K3-DGqxixsR.js +41 -0
  484. package/ui-dist/assets/{ebnfDiagram-CCIWWBDH-CJ1d3D5q.js → ebnfDiagram-BXEA7PRR-C-1klg_w.js} +1 -1
  485. package/ui-dist/assets/{erDiagram-Q63AITRT-BJCPa7kK.js → erDiagram-JOGREHBK-Dt4qK4gv.js} +1 -1
  486. package/ui-dist/assets/{flowDiagram-23GEKE2U-BWjWuz6r.js → flowDiagram-UKHOOZJN-9ZlrTuET.js} +5 -5
  487. package/ui-dist/assets/{ganttDiagram-NO4QXBWP-Dv1KKjoM.js → ganttDiagram-PKOTCBZU-CE3lyvFq.js} +1 -1
  488. package/ui-dist/assets/{gitGraphDiagram-IHSO6WYX-9X7vgL0A.js → gitGraphDiagram-DS77QQ5N-HH2fSgad.js} +1 -1
  489. package/ui-dist/assets/{index-CpaK48ok.js → index-B61ORkIb.js} +1 -1
  490. package/ui-dist/assets/index-BHiUiToL.css +1 -0
  491. package/ui-dist/assets/{index-D2YCrA-D.js → index-BY5ITKmG.js} +1 -1
  492. package/ui-dist/assets/{index-9JMDmXc5.js → index-BbAhzCq8.js} +1 -1
  493. package/ui-dist/assets/{index-C5qHatpp.js → index-Bc9g6q63.js} +1 -1
  494. package/ui-dist/assets/{index-DuI6e5mP.js → index-Bf7tyGxE.js} +1 -1
  495. package/ui-dist/assets/{index-BahRsj0s.js → index-BvFx-4KW.js} +1 -1
  496. package/ui-dist/assets/{index-DKlsEDV7.js → index-C1mpWCcn.js} +1 -1
  497. package/ui-dist/assets/{index-DIXaXZrW.js → index-CHXG4GPL.js} +1 -1
  498. package/ui-dist/assets/{index-DcfO6NoY.js → index-CLu3Tfpv.js} +1 -1
  499. package/ui-dist/assets/{index-Cfz6P0Qq.js → index-CQhaM6qd.js} +1 -1
  500. package/ui-dist/assets/{index-D9d3sl6K.js → index-CVTL3aAL.js} +1 -1
  501. package/ui-dist/assets/{index-BexA3NAl.js → index-CcRTJ93J.js} +1 -1
  502. package/ui-dist/assets/{index-CnBrDwGT.js → index-Cm6iEYPt.js} +247 -223
  503. package/ui-dist/assets/{index-ByGcu-MB.js → index-DQIrrn2L.js} +1 -1
  504. package/ui-dist/assets/{index-DKEzHO7c.js → index-DU4U1fuv.js} +1 -1
  505. package/ui-dist/assets/{index-pqJO8FsV.js → index-DV2FOgyD.js} +1 -1
  506. package/ui-dist/assets/{index-tDuieU3u.js → index-DceuCrr8.js} +1 -1
  507. package/ui-dist/assets/{index-CjN2lm_w.js → index-DlVBBF8o.js} +1 -1
  508. package/ui-dist/assets/{index-0yJtUsFh.js → index-DsdVneqe.js} +1 -1
  509. package/ui-dist/assets/{index-C2a4_IKU.js → index-DvFUXI-Q.js} +1 -1
  510. package/ui-dist/assets/{index-BTMiDMYv.js → index-FHzrJ7iQ.js} +1 -1
  511. package/ui-dist/assets/{index-cUQXJtLK.js → index-diFs0CNC.js} +1 -1
  512. package/ui-dist/assets/{index-C96LHV1A.js → index-hkanj4t6.js} +1 -1
  513. package/ui-dist/assets/{index-rDI1Q4r9.js → index-yRJnh4vx.js} +1 -1
  514. package/ui-dist/assets/{infoDiagram-FWYZ7A6U-CHIhutSb.js → infoDiagram-6WML65LV-2jai2fw-.js} +1 -1
  515. package/ui-dist/assets/{ishikawaDiagram-FXEZZL3T-ChaD7-5_.js → ishikawaDiagram-WSZJBQD7-CUmGhmEb.js} +1 -1
  516. package/ui-dist/assets/{journeyDiagram-5HDEW3XC-M5T2sMgn.js → journeyDiagram-NVQOT4AX-CGQuPDO1.js} +1 -1
  517. package/ui-dist/assets/{kanban-definition-HUTT4EX6-B8H6R_Wc.js → kanban-definition-27J2QSJJ-zgEPcbHl.js} +1 -1
  518. package/ui-dist/assets/{linear-cpPKlVCq.js → linear-BVf5VtLS.js} +1 -1
  519. package/ui-dist/assets/mermaid.core-D2TFkGfI.js +308 -0
  520. package/ui-dist/assets/{mindmap-definition-LN4V7U3C-Bf6POzvm.js → mindmap-definition-FAOFIHXS-xoAQ0Lb_.js} +1 -1
  521. package/ui-dist/assets/{pegDiagram-2B236MQR-BJbGktAV.js → pegDiagram-VL7TDLO6-CBR1PWLr.js} +1 -1
  522. package/ui-dist/assets/{pieDiagram-ENE6RG2P-BmSN9Bio.js → pieDiagram-7S7Q4E2Y-BMPnzK_e.js} +1 -1
  523. package/ui-dist/assets/{quadrantDiagram-ABIIQ3AL-JUpqQCx_.js → quadrantDiagram-CIZ2JOQS-CPtVUPkR.js} +1 -1
  524. package/ui-dist/assets/{railroadDiagram-RFXS5EU6-B8k8T4-2.js → railroadDiagram-AXF67PYL-DC0lsZV1.js} +1 -1
  525. package/ui-dist/assets/{requirementDiagram-TGXJPOKE-b32heq7Q.js → requirementDiagram-LRYGKXZP-BF6k05SB.js} +1 -1
  526. package/ui-dist/assets/{sankeyDiagram-HTMAVEWB-DC3s-xgf.js → sankeyDiagram-W5VNT64P-CRN0ibiP.js} +1 -1
  527. package/ui-dist/assets/{sequenceDiagram-DBY2YBRQ-BsCFyv_E.js → sequenceDiagram-SI44F4Z6-CQtTiPWm.js} +1 -1
  528. package/ui-dist/assets/{sizeCapture-X5ZJPWSS-S7xeCud3.js → sizeCapture-X5ZJPWSS-BdjuGZd1.js} +1 -1
  529. package/ui-dist/assets/{stateDiagram-2N3HPSRC-58O3ofoS.js → stateDiagram-OKZ733FA-B99onxyh.js} +1 -1
  530. package/ui-dist/assets/stateDiagram-v2-UEYNNEHI-bzFlYBZ1.js +1 -0
  531. package/ui-dist/assets/{swimlanes-5IMT3BWC-DFCVG0iF.js → swimlanes-SLNWSIFB-BIByPbrL.js} +2 -2
  532. package/ui-dist/assets/swimlanesDiagram-ULZ7WXOC-DgdkBnuK.js +8 -0
  533. package/ui-dist/assets/{timeline-definition-FHXFAJF6-C0c0SkQu.js → timeline-definition-Z64GVDOM-I1NgOcn9.js} +1 -1
  534. package/ui-dist/assets/{vennDiagram-L72KCM5P-B1cEx8GQ.js → vennDiagram-T6HMQDX7-D6t2Njcl.js} +1 -1
  535. package/ui-dist/assets/{wardleyDiagram-EHGQE667-BQhDkOx9.js → wardleyDiagram-T6FBY63Y-BiYKm9gh.js} +1 -1
  536. package/ui-dist/assets/{xychartDiagram-FW5EYKEG-YSQp1N0i.js → xychartDiagram-ELKLHX3M-BhwXKWIa.js} +4 -4
  537. package/ui-dist/index.html +3 -3
  538. package/ui-dist/sw.js +8 -3
  539. package/ui-dist/assets/architectureDiagram-ZJ3FMSHR-BcJt8Ia1.js +0 -36
  540. package/ui-dist/assets/channel-ZRpjyNiZ.js +0 -1
  541. package/ui-dist/assets/classDiagram-OUVF2IWQ-CCzzzCgk.js +0 -1
  542. package/ui-dist/assets/classDiagram-v2-EOCWNBFH-CCzzzCgk.js +0 -1
  543. package/ui-dist/assets/cytoscape.esm-D8joxN9f.js +0 -321
  544. package/ui-dist/assets/diagram-WEI45ONY-M3WOC2tK.js +0 -41
  545. package/ui-dist/assets/index-BqfqfoQX.css +0 -1
  546. package/ui-dist/assets/mermaid.core-MCazc_od.js +0 -308
  547. package/ui-dist/assets/stateDiagram-v2-6OUMAXLB-_VY5dpDG.js +0 -1
  548. package/ui-dist/assets/swimlanesDiagram-G3AALYLV-BJhUjfns.js +0 -8
@@ -0,0 +1,1143 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
3
+ import { eq, sql } from "drizzle-orm";
4
+ import { claudeSetupTokenSessions, companies, createDb, environments, getEmbeddedPostgresTestSupport, startEmbeddedPostgresTestDatabase, } from "@paperclipai/db";
5
+ import { createDbSetupTokenCleanupStore, SetupTokenSessionService, SetupTokenSessionError, assessConfidentialStartup, evaluateConfidentialTransport, isTerminalSessionState, toSanitizedLoginUrl, SETUP_TOKEN_SESSION_NOT_FOUND, SETUP_TOKEN_SUBMIT_CONFLICT, SETUP_TOKEN_RATE_LIMITED, SETUP_TOKEN_CAP_EXCEEDED, SETUP_TOKEN_TOKEN_UNAVAILABLE, SETUP_TOKEN_STORAGE_FAILED, } from "./setup-token-session.js";
6
+ import { redactSensitive } from "../middleware/redact-sensitive.js";
7
+ import { sanitizeRecord } from "../redaction.js";
8
+ const FULL_LOGIN_URL = "https://claude.com/cai/oauth/authorize?client_id=abc&code=SECRETCODE123&code_challenge=xyz&code_challenge_method=S256&redirect_uri=http%3A%2F%2Flocalhost&response_type=code&scope=org&state=STATEVALUE";
9
+ // A synthetic token. The session passes the token through in memory; it does not
10
+ // parse it. No real token is present.
11
+ const SYNTH_TOKEN = "sk-ant-oat01-SYNTHETICSYNTHETICSYNTHETIC01";
12
+ const OWNER_SCOPE = {
13
+ companyId: "company-1",
14
+ ownerUserId: "user-1",
15
+ targetAgentId: "agent-1",
16
+ adapterType: "claude_local",
17
+ environmentId: "env-1",
18
+ };
19
+ /** A controllable fake login process. It records the call order into `events`. */
20
+ class FakeProcess {
21
+ id;
22
+ onPrompt;
23
+ onCredential;
24
+ events;
25
+ done;
26
+ resolveDone;
27
+ submittedCode = null;
28
+ submitCalls = 0;
29
+ stopCalls = 0;
30
+ constructor(id, onPrompt, onCredential, events) {
31
+ this.id = id;
32
+ this.onPrompt = onPrompt;
33
+ this.onCredential = onCredential;
34
+ this.events = events;
35
+ this.done = new Promise((resolve) => {
36
+ this.resolveDone = resolve;
37
+ });
38
+ }
39
+ surfacePrompt(url) {
40
+ this.onPrompt({ url });
41
+ }
42
+ // The credential sink is asynchronous: it awaits the owner-bound secret write.
43
+ // The test awaits this call so the write settles before it asserts.
44
+ async surfaceCredential(token) {
45
+ await this.onCredential(token);
46
+ }
47
+ finish(outcome) {
48
+ this.resolveDone(outcome);
49
+ }
50
+ submitCode(code) {
51
+ this.submitCalls += 1;
52
+ this.submittedCode = code;
53
+ this.events.push(`submit:${this.id}`);
54
+ }
55
+ stop() {
56
+ this.stopCalls += 1;
57
+ this.events.push(`stop:${this.id}`);
58
+ }
59
+ }
60
+ class FakeLeaseManager {
61
+ events;
62
+ acquired = [];
63
+ released = [];
64
+ releaseByIdCalls = [];
65
+ failReleaseOnce = false;
66
+ counter = 0;
67
+ constructor(events = []) {
68
+ this.events = events;
69
+ }
70
+ async acquire(input) {
71
+ this.counter += 1;
72
+ const id = `lease-${this.counter}`;
73
+ this.acquired.push(id);
74
+ return { id };
75
+ }
76
+ async release(lease) {
77
+ if (this.failReleaseOnce) {
78
+ this.failReleaseOnce = false;
79
+ throw new Error("release failed");
80
+ }
81
+ this.released.push(lease.id);
82
+ this.events.push(`release:${lease.id}`);
83
+ }
84
+ async releaseById(leaseId) {
85
+ this.releaseByIdCalls.push(leaseId);
86
+ this.released.push(leaseId);
87
+ }
88
+ }
89
+ // A lease manager whose `acquire` stays in flight until the test resolves it.
90
+ // The test uses it to hold the first start suspended on the lease acquire, then
91
+ // prove a concurrent start for the same owner fails closed before it reaches the
92
+ // provider. It can also fail the next acquire, to drive the acquire-failure
93
+ // rollback path.
94
+ class DeferredLeaseManager {
95
+ acquireCalls = 0;
96
+ released = [];
97
+ releaseByIdCalls = [];
98
+ failNextAcquire = false;
99
+ counter = 0;
100
+ pending = [];
101
+ async acquire() {
102
+ this.acquireCalls += 1;
103
+ if (this.failNextAcquire) {
104
+ this.failNextAcquire = false;
105
+ throw new Error("lease acquire failed");
106
+ }
107
+ const id = `lease-${(this.counter += 1)}`;
108
+ return new Promise((resolve) => {
109
+ this.pending.push({ resolve, id });
110
+ });
111
+ }
112
+ /** Resolves the oldest in-flight acquire with its lease. */
113
+ resolveNextAcquire() {
114
+ const next = this.pending.shift();
115
+ if (next)
116
+ next.resolve({ id: next.id });
117
+ }
118
+ /** The count of acquires that are in flight. */
119
+ pendingCount() {
120
+ return this.pending.length;
121
+ }
122
+ async release(lease) {
123
+ this.released.push(lease.id);
124
+ }
125
+ async releaseById(leaseId) {
126
+ this.releaseByIdCalls.push(leaseId);
127
+ this.released.push(leaseId);
128
+ }
129
+ }
130
+ // Builds the durable-record identity from a session scope and the session id. The
131
+ // fake writers use it to simulate the atomic durable `stored` transition that the
132
+ // production writer performs inside the credential transaction.
133
+ function identityFromScope(scope, sessionId) {
134
+ return {
135
+ sessionId,
136
+ companyId: scope.companyId,
137
+ ownerUserId: scope.ownerUserId,
138
+ adapterType: scope.adapterType,
139
+ environmentId: scope.environmentId,
140
+ };
141
+ }
142
+ function identityMatchesRow(row, identity) {
143
+ return (row.companyId === identity.companyId &&
144
+ row.ownerUserId === identity.ownerUserId &&
145
+ row.adapterType === identity.adapterType &&
146
+ row.environmentId === identity.environmentId);
147
+ }
148
+ class FakeStore {
149
+ rows = new Map();
150
+ async record(record) {
151
+ this.rows.set(record.sessionId, { ...record });
152
+ }
153
+ async markState(identity, state) {
154
+ const row = this.rows.get(identity.sessionId);
155
+ // The write matches the full owner scope, so it never updates a row by the
156
+ // session id alone.
157
+ if (row && identityMatchesRow(row, identity))
158
+ row.state = state;
159
+ }
160
+ async remove(identity) {
161
+ // The delete matches the full owner scope, so it never removes a row by the
162
+ // session id alone.
163
+ const row = this.rows.get(identity.sessionId);
164
+ if (row && identityMatchesRow(row, identity))
165
+ this.rows.delete(identity.sessionId);
166
+ }
167
+ async listReapable(now) {
168
+ return [...this.rows.values()].filter((row) => isTerminalSessionState(row.state) || row.deadline <= now || row.boundAt !== null);
169
+ }
170
+ async consumeStoredClaim(identity) {
171
+ const row = this.rows.get(identity.sessionId);
172
+ if (!row ||
173
+ !identityMatchesRow(row, identity) ||
174
+ row.state !== "stored" ||
175
+ row.boundAt !== null ||
176
+ row.deadline <= Date.now()) {
177
+ return null;
178
+ }
179
+ row.boundAt = Date.now();
180
+ return { ...row };
181
+ }
182
+ }
183
+ function allowAllRateLimiter() {
184
+ return { consume: () => ({ allowed: true, retryAfterSeconds: 0 }) };
185
+ }
186
+ function buildService(overrides = {}) {
187
+ const events = overrides.events ?? [];
188
+ const processes = [];
189
+ let processCounter = 0;
190
+ const factory = overrides.factory ??
191
+ (({ onPrompt, onCredential }) => {
192
+ processCounter += 1;
193
+ const process = new FakeProcess(`p${processCounter}`, onPrompt, onCredential, events);
194
+ processes.push(process);
195
+ return process;
196
+ });
197
+ const leases = overrides.leases ?? new FakeLeaseManager(events);
198
+ const store = overrides.store ?? new FakeStore();
199
+ const secretWrites = [];
200
+ const completeCredential = overrides.completeCredential ??
201
+ (async (input) => {
202
+ secretWrites.push({ scope: input.scope, sessionId: input.sessionId, token: input.token });
203
+ // Simulate the atomic durable `stored` transition the production writer runs
204
+ // inside the credential transaction, so the store reflects the claim.
205
+ await store.markState(identityFromScope(input.scope, input.sessionId), "stored");
206
+ });
207
+ const logs = [];
208
+ const service = new SetupTokenSessionService({
209
+ factory,
210
+ leases,
211
+ store,
212
+ completeCredential,
213
+ rateLimiter: overrides.rateLimiter ?? allowAllRateLimiter(),
214
+ caps: overrides.caps ?? { perOwner: 5, perAgent: 5, perCompany: 5 },
215
+ ttlMs: overrides.ttlMs ?? 60_000,
216
+ tokenRetentionMs: overrides.tokenRetentionMs,
217
+ now: overrides.now,
218
+ log: (line) => logs.push(line),
219
+ });
220
+ return { service, processes, leases, store, events, secretWrites, logs };
221
+ }
222
+ describe("SetupTokenSessionService.start", () => {
223
+ it("returns a session id and no token, and holds one live process and lease", async () => {
224
+ const { service, processes, leases } = buildService();
225
+ const result = await service.start(OWNER_SCOPE);
226
+ expect(result.sessionId).toBeTruthy();
227
+ expect(result).not.toHaveProperty("token");
228
+ expect(processes).toHaveLength(1);
229
+ expect(leases.acquired).toEqual(["lease-1"]);
230
+ expect(service.activeSessionCount()).toBe(1);
231
+ });
232
+ it("returns an opaque, high-entropy session id", async () => {
233
+ const { service } = buildService();
234
+ const a = await service.start(OWNER_SCOPE);
235
+ const b = await service.start({ ...OWNER_SCOPE, ownerUserId: "user-2" });
236
+ expect(a.sessionId).not.toEqual(b.sessionId);
237
+ expect(a.sessionId.length).toBeGreaterThanOrEqual(32);
238
+ });
239
+ it("rejects a caller over the rate limit with 429", async () => {
240
+ let allowed = true;
241
+ const rateLimiter = {
242
+ consume: () => {
243
+ const decision = { allowed, retryAfterSeconds: allowed ? 0 : 30 };
244
+ allowed = false;
245
+ return decision;
246
+ },
247
+ };
248
+ const { service } = buildService({ rateLimiter });
249
+ await service.start(OWNER_SCOPE);
250
+ await expect(service.start(OWNER_SCOPE)).rejects.toMatchObject({
251
+ status: 429,
252
+ message: SETUP_TOKEN_RATE_LIMITED,
253
+ });
254
+ });
255
+ it("rejects a caller over the per-owner cap with 429", async () => {
256
+ const { service } = buildService({ caps: { perOwner: 1, perAgent: 5, perCompany: 5 } });
257
+ await service.start(OWNER_SCOPE);
258
+ await expect(service.start({ ...OWNER_SCOPE, targetAgentId: "agent-2" })).rejects.toMatchObject({
259
+ status: 429,
260
+ message: SETUP_TOKEN_CAP_EXCEEDED,
261
+ });
262
+ });
263
+ });
264
+ describe("SetupTokenSessionService.start cap reservation (concurrency)", () => {
265
+ const capsPerOwnerOne = { perOwner: 1, perAgent: 5, perCompany: 5 };
266
+ it("reserves the owner slot synchronously, so a concurrent start fails closed with 429", async () => {
267
+ const leases = new DeferredLeaseManager();
268
+ const { service } = buildService({ leases, caps: capsPerOwnerOne });
269
+ // Start the first session. It reserves the owner slot synchronously, then it
270
+ // suspends on the deferred lease acquire. The acquire reached the provider and
271
+ // has not resolved yet.
272
+ const firstStart = service.start(OWNER_SCOPE);
273
+ await flush();
274
+ expect(leases.acquireCalls).toBe(1);
275
+ expect(leases.pendingCount()).toBe(1);
276
+ // The second start for the same owner runs while the first acquire is in
277
+ // flight. The synchronous reservation already holds the one owner slot, so the
278
+ // second start fails closed with the fixed 429 cap error before it reaches the
279
+ // provider. Only one acquire ever reaches the provider.
280
+ await expect(service.start(OWNER_SCOPE)).rejects.toMatchObject({
281
+ status: 429,
282
+ message: SETUP_TOKEN_CAP_EXCEEDED,
283
+ });
284
+ expect(leases.acquireCalls).toBe(1);
285
+ // Let the first start finish. It holds the one live session.
286
+ leases.resolveNextAcquire();
287
+ const first = await firstStart;
288
+ expect(first.sessionId).toBeTruthy();
289
+ expect(service.activeSessionCount()).toBe(1);
290
+ });
291
+ it("releases the owner reservation after cleanup, so the owner can start again", async () => {
292
+ const leases = new DeferredLeaseManager();
293
+ const { service } = buildService({ leases, caps: capsPerOwnerOne });
294
+ const firstStart = service.start(OWNER_SCOPE);
295
+ await flush();
296
+ leases.resolveNextAcquire();
297
+ const first = await firstStart;
298
+ // Cancel the live session. The cleanup releases the owner reservation.
299
+ await service.cancel(first.sessionId, OWNER_SCOPE);
300
+ expect(service.activeSessionCount()).toBe(0);
301
+ // A new start for the same owner reserves the slot again and starts, so the
302
+ // cleanup released the reservation exactly once and left no slot held.
303
+ const secondStart = service.start(OWNER_SCOPE);
304
+ await flush();
305
+ leases.resolveNextAcquire();
306
+ const second = await secondStart;
307
+ expect(second.sessionId).toBeTruthy();
308
+ expect(second.sessionId).not.toBe(first.sessionId);
309
+ expect(service.activeSessionCount()).toBe(1);
310
+ });
311
+ it("releases the owner reservation when the lease acquire fails, so a retry can start", async () => {
312
+ const leases = new DeferredLeaseManager();
313
+ leases.failNextAcquire = true;
314
+ const { service } = buildService({ leases, caps: capsPerOwnerOne });
315
+ // The first start reserves the owner slot, then the lease acquire rejects. The
316
+ // failure path rolls back the reservation and holds no durable state.
317
+ await expect(service.start(OWNER_SCOPE)).rejects.toThrow("lease acquire failed");
318
+ expect(service.activeSessionCount()).toBe(0);
319
+ expect(leases.released).toEqual([]);
320
+ // A retry for the same owner reserves the slot again and starts. The failed
321
+ // start left no reservation behind.
322
+ const retry = service.start(OWNER_SCOPE);
323
+ await flush();
324
+ expect(leases.acquireCalls).toBe(2);
325
+ leases.resolveNextAcquire();
326
+ const result = await retry;
327
+ expect(result.sessionId).toBeTruthy();
328
+ expect(service.activeSessionCount()).toBe(1);
329
+ });
330
+ it("releases the reservation and the lease when the durable record write fails", async () => {
331
+ const leases = new FakeLeaseManager();
332
+ const store = new FakeStore();
333
+ let failRecord = true;
334
+ const recordOriginal = store.record.bind(store);
335
+ store.record = async (record) => {
336
+ if (failRecord) {
337
+ failRecord = false;
338
+ throw new Error("durable write failed");
339
+ }
340
+ return recordOriginal(record);
341
+ };
342
+ const { service } = buildService({ leases, store, caps: capsPerOwnerOne });
343
+ // The durable write fails. The failure path releases the acquired lease and
344
+ // rolls back the reservation.
345
+ await expect(service.start(OWNER_SCOPE)).rejects.toThrow("durable write failed");
346
+ expect(leases.released).toEqual(["lease-1"]);
347
+ expect(service.activeSessionCount()).toBe(0);
348
+ // The reservation rolled back, so a retry for the same owner starts.
349
+ const retry = await service.start(OWNER_SCOPE);
350
+ expect(retry.sessionId).toBeTruthy();
351
+ expect(service.activeSessionCount()).toBe(1);
352
+ });
353
+ it("releases the reservation and the lease when the factory throws, so a retry can start", async () => {
354
+ const leases = new FakeLeaseManager();
355
+ let failFactory = true;
356
+ const factory = () => {
357
+ if (failFactory) {
358
+ failFactory = false;
359
+ throw new Error("factory failed");
360
+ }
361
+ // A minimal live process for the retry. It never ends on its own.
362
+ return {
363
+ done: new Promise(() => { }),
364
+ submitCode: () => { },
365
+ stop: () => { },
366
+ };
367
+ };
368
+ const { service } = buildService({ leases, factory, caps: capsPerOwnerOne });
369
+ // The factory throws. The start releases the lease, drops the durable record,
370
+ // rolls back the reservation, and returns the fixed 503 start error.
371
+ await expect(service.start(OWNER_SCOPE)).rejects.toMatchObject({ status: 503 });
372
+ expect(leases.released).toEqual(["lease-1"]);
373
+ expect(service.activeSessionCount()).toBe(0);
374
+ // The reservation rolled back, so a retry for the same owner starts.
375
+ const retry = await service.start(OWNER_SCOPE);
376
+ expect(retry.sessionId).toBeTruthy();
377
+ expect(service.activeSessionCount()).toBe(1);
378
+ });
379
+ });
380
+ describe("SetupTokenSessionService.readPrompt", () => {
381
+ it("returns the full login URL to the authorized owner once the prompt surfaces", async () => {
382
+ const { service, processes } = buildService();
383
+ const { sessionId } = await service.start(OWNER_SCOPE);
384
+ expect(service.readPrompt(sessionId, OWNER_SCOPE).loginUrl).toBeNull();
385
+ processes[0].surfacePrompt(FULL_LOGIN_URL);
386
+ const view = service.readPrompt(sessionId, OWNER_SCOPE);
387
+ expect(view.state).toBe("awaiting_code");
388
+ expect(view.loginUrl).toBe(FULL_LOGIN_URL);
389
+ });
390
+ });
391
+ describe("SetupTokenSessionService.submitCode", () => {
392
+ it("accepts one browser code, advances the live session once, and rejects every later submit", async () => {
393
+ const { service, processes } = buildService();
394
+ const { sessionId } = await service.start(OWNER_SCOPE);
395
+ processes[0].surfacePrompt(FULL_LOGIN_URL);
396
+ const first = service.submitCode(sessionId, OWNER_SCOPE, "browsercode-1");
397
+ expect(first.state).toBe("submitting");
398
+ expect(processes[0].submitCalls).toBe(1);
399
+ expect(processes[0].submittedCode).toBe("browsercode-1");
400
+ expect(() => service.submitCode(sessionId, OWNER_SCOPE, "browsercode-2")).toThrow(SETUP_TOKEN_SUBMIT_CONFLICT);
401
+ // A later submit, including one after an invalid-code retry, never reaches
402
+ // the live process.
403
+ expect(processes[0].submitCalls).toBe(1);
404
+ });
405
+ it("rejects a submit before the prompt surfaces", async () => {
406
+ const { service } = buildService();
407
+ const { sessionId } = await service.start(OWNER_SCOPE);
408
+ expect(() => service.submitCode(sessionId, OWNER_SCOPE, "code")).toThrow(SetupTokenSessionError);
409
+ });
410
+ });
411
+ describe("SetupTokenSessionService authorization boundary", () => {
412
+ const crossCompany = { ...OWNER_SCOPE, companyId: "company-2" };
413
+ const sameCompanyOtherUser = { ...OWNER_SCOPE, ownerUserId: "user-9" };
414
+ const otherAgent = { ...OWNER_SCOPE, targetAgentId: "agent-9" };
415
+ const otherAdapter = { ...OWNER_SCOPE, adapterType: "codex_local" };
416
+ const otherEnvironment = { ...OWNER_SCOPE, environmentId: "env-9" };
417
+ it("returns the same not-found for a cross-scope caller on every operation", async () => {
418
+ const { service, processes } = buildService();
419
+ const { sessionId } = await service.start(OWNER_SCOPE);
420
+ processes[0].surfacePrompt(FULL_LOGIN_URL);
421
+ // The full-scope match rejects a mismatch in any of the five scope fields:
422
+ // the company, the owner, the agent, the adapter, and the environment.
423
+ for (const scope of [
424
+ crossCompany,
425
+ sameCompanyOtherUser,
426
+ otherAgent,
427
+ otherAdapter,
428
+ otherEnvironment,
429
+ ]) {
430
+ expect(() => service.readPrompt(sessionId, scope)).toThrow(SETUP_TOKEN_SESSION_NOT_FOUND);
431
+ expect(() => service.submitCode(sessionId, scope, "code")).toThrow(SETUP_TOKEN_SESSION_NOT_FOUND);
432
+ expect(() => service.completeSession(sessionId, scope)).toThrow(SETUP_TOKEN_SESSION_NOT_FOUND);
433
+ await expect(service.cancel(sessionId, scope)).rejects.toThrow(SETUP_TOKEN_SESSION_NOT_FOUND);
434
+ await expect(service.expire(sessionId, scope)).rejects.toThrow(SETUP_TOKEN_SESSION_NOT_FOUND);
435
+ }
436
+ });
437
+ it("returns the same not-found for a missing session", async () => {
438
+ const { service } = buildService();
439
+ expect(() => service.readPrompt("missing", OWNER_SCOPE)).toThrow(SETUP_TOKEN_SESSION_NOT_FOUND);
440
+ });
441
+ });
442
+ describe("SetupTokenSessionService company-and-environment scope", () => {
443
+ // The agentless company scope. It carries a null agent id.
444
+ const COMPANY_SCOPE = { ...OWNER_SCOPE, targetAgentId: null };
445
+ const companyKey = {
446
+ companyId: COMPANY_SCOPE.companyId,
447
+ ownerUserId: COMPANY_SCOPE.ownerUserId,
448
+ adapterType: COMPANY_SCOPE.adapterType,
449
+ };
450
+ it("resolves an agentless session by the company key and returns the intrinsic environment", async () => {
451
+ const { service, processes } = buildService();
452
+ const { sessionId } = await service.start(COMPANY_SCOPE);
453
+ processes[0].surfacePrompt(FULL_LOGIN_URL);
454
+ const scope = service.resolveCompanyScope(sessionId, companyKey);
455
+ expect(scope.targetAgentId).toBeNull();
456
+ expect(scope.environmentId).toBe(COMPANY_SCOPE.environmentId);
457
+ const descriptor = service.describeOwned(sessionId, scope);
458
+ expect(descriptor.sessionId).toBe(sessionId);
459
+ expect(descriptor.environmentId).toBe(COMPANY_SCOPE.environmentId);
460
+ expect(descriptor.loginUrl).toBe(FULL_LOGIN_URL);
461
+ });
462
+ it("rejects an agent-scoped session through the company key", async () => {
463
+ const { service } = buildService();
464
+ // The session carries a non-null agent id. The company key requires the
465
+ // agentless marker, so a company route cannot reach an agent session.
466
+ const { sessionId } = await service.start(OWNER_SCOPE);
467
+ expect(() => service.resolveCompanyScope(sessionId, companyKey)).toThrow(SETUP_TOKEN_SESSION_NOT_FOUND);
468
+ });
469
+ it("returns the same not-found for a foreign company key", async () => {
470
+ const { service } = buildService();
471
+ const { sessionId } = await service.start(COMPANY_SCOPE);
472
+ // A cross-company, a cross-owner, and a cross-adapter key each return the
473
+ // same not-found error as a missing session.
474
+ for (const key of [
475
+ { ...companyKey, companyId: "company-2" },
476
+ { ...companyKey, ownerUserId: "user-9" },
477
+ { ...companyKey, adapterType: "codex_local" },
478
+ ]) {
479
+ expect(() => service.resolveCompanyScope(sessionId, key)).toThrow(SETUP_TOKEN_SESSION_NOT_FOUND);
480
+ }
481
+ });
482
+ });
483
+ describe("SetupTokenSessionService cleanup order", () => {
484
+ it("stops the direct child before the lease release on cancel", async () => {
485
+ const events = [];
486
+ const { service } = buildService({ events });
487
+ const { sessionId } = await service.start(OWNER_SCOPE);
488
+ await service.cancel(sessionId, OWNER_SCOPE);
489
+ const stopIndex = events.indexOf("stop:p1");
490
+ const releaseIndex = events.indexOf("release:lease-1");
491
+ expect(stopIndex).toBeGreaterThanOrEqual(0);
492
+ expect(releaseIndex).toBeGreaterThanOrEqual(0);
493
+ expect(stopIndex).toBeLessThan(releaseIndex);
494
+ expect(service.activeSessionCount()).toBe(0);
495
+ });
496
+ it("stops the direct child before the lease release on expire", async () => {
497
+ const events = [];
498
+ const { service } = buildService({ events });
499
+ const { sessionId } = await service.start(OWNER_SCOPE);
500
+ const result = await service.expire(sessionId, OWNER_SCOPE);
501
+ expect(result.state).toBe("timed_out");
502
+ expect(events.indexOf("stop:p1")).toBeLessThan(events.indexOf("release:lease-1"));
503
+ });
504
+ it("releases the lease when the process ends on its own", async () => {
505
+ const { service, processes, leases } = buildService();
506
+ const { sessionId } = await service.start(OWNER_SCOPE);
507
+ processes[0].surfacePrompt(FULL_LOGIN_URL);
508
+ service.submitCode(sessionId, OWNER_SCOPE, "code");
509
+ processes[0].finish("success");
510
+ await new Promise((resolve) => setImmediate(resolve));
511
+ expect(leases.released).toEqual(["lease-1"]);
512
+ expect(service.activeSessionCount()).toBe(0);
513
+ });
514
+ });
515
+ describe("SetupTokenSessionService durable reaper", () => {
516
+ it("replays the durable record and releases the orphaned lease after a restart", async () => {
517
+ const store = new FakeStore();
518
+ const leases = new FakeLeaseManager();
519
+ // Simulate a prior process that crashed with a live record and a lease.
520
+ await store.record({
521
+ sessionId: "orphan-1",
522
+ companyId: "company-1",
523
+ ownerUserId: "user-1",
524
+ adapterType: "claude_local",
525
+ environmentId: "env-1",
526
+ leaseId: "lease-orphan",
527
+ deadline: 1_000,
528
+ state: "awaiting_code",
529
+ boundAt: null,
530
+ });
531
+ const { service } = buildService({ store, leases, now: () => 5_000 });
532
+ const summary = await service.reap(5_000);
533
+ expect(summary.released).toBe(1);
534
+ expect(leases.releaseByIdCalls).toEqual(["lease-orphan"]);
535
+ expect(store.rows.size).toBe(0);
536
+ });
537
+ it("keeps the record when the reaper lease release fails, so it stays retryable", async () => {
538
+ const store = new FakeStore();
539
+ await store.record({
540
+ sessionId: "orphan-2",
541
+ companyId: "company-1",
542
+ ownerUserId: "user-1",
543
+ adapterType: "claude_local",
544
+ environmentId: "env-1",
545
+ leaseId: "lease-orphan-2",
546
+ deadline: 1_000,
547
+ state: "failed",
548
+ boundAt: null,
549
+ });
550
+ const leases = new FakeLeaseManager();
551
+ leases.releaseById = async () => {
552
+ throw new Error("provider down");
553
+ };
554
+ const { service } = buildService({ store, leases, now: () => 5_000 });
555
+ const summary = await service.reap(5_000);
556
+ expect(summary.failed).toBe(1);
557
+ expect(store.rows.has("orphan-2")).toBe(true);
558
+ });
559
+ });
560
+ describe("SetupTokenSessionService.completeSession", () => {
561
+ it("returns the non-secret storedSessionId after a successful secret write, and no token", async () => {
562
+ const { service, processes, leases, store, secretWrites, logs } = buildService();
563
+ const { sessionId } = await service.start(OWNER_SCOPE);
564
+ processes[0].surfacePrompt(FULL_LOGIN_URL);
565
+ service.submitCode(sessionId, OWNER_SCOPE, "code");
566
+ // The session awaits the owner-bound secret write before it completes.
567
+ await processes[0].surfaceCredential(SYNTH_TOKEN);
568
+ processes[0].finish("success");
569
+ await new Promise((resolve) => setImmediate(resolve));
570
+ // The token reached the owner-bound writer, once, with the owner scope.
571
+ expect(secretWrites).toEqual([{ scope: OWNER_SCOPE, sessionId, token: SYNTH_TOKEN }]);
572
+ // The service releases the sandbox lease at once.
573
+ expect(leases.released).toEqual(["lease-1"]);
574
+ // The durable row stays as the stored-session claim, not removed.
575
+ expect(store.rows.get(sessionId)?.state).toBe("stored");
576
+ // The completion returns the non-secret storedSessionId and carries no token.
577
+ const completion = service.completeSession(sessionId, OWNER_SCOPE);
578
+ expect(completion.storedSessionId).toBe(sessionId);
579
+ expect(completion).not.toHaveProperty("token");
580
+ // No response, log, or durable record contains the token after success.
581
+ const haystack = `${logs.join("\n")}\n${JSON.stringify(completion)}\n${JSON.stringify([
582
+ ...store.rows.values(),
583
+ ])}`;
584
+ expect(haystack).not.toContain(SYNTH_TOKEN);
585
+ });
586
+ it("returns the fixed unavailable error before the secret write completes", async () => {
587
+ const { service, processes } = buildService();
588
+ const { sessionId } = await service.start(OWNER_SCOPE);
589
+ processes[0].surfacePrompt(FULL_LOGIN_URL);
590
+ service.submitCode(sessionId, OWNER_SCOPE, "code");
591
+ // The secret write has not run, so the completion is unavailable.
592
+ expect(() => service.completeSession(sessionId, OWNER_SCOPE)).toThrow(SETUP_TOKEN_TOKEN_UNAVAILABLE);
593
+ });
594
+ it("reaches failed with no binding and no completion when the secret write errors", async () => {
595
+ const store = new FakeStore();
596
+ const completeCredential = async () => {
597
+ throw new Error("storage down");
598
+ };
599
+ const { service, processes, leases, logs } = buildService({ store, completeCredential });
600
+ const { sessionId } = await service.start(OWNER_SCOPE);
601
+ processes[0].surfacePrompt(FULL_LOGIN_URL);
602
+ service.submitCode(sessionId, OWNER_SCOPE, "code");
603
+ // The sink rejects with the fixed, non-secret storage error. It does not
604
+ // swallow the failure.
605
+ await expect(processes[0].surfaceCredential(SYNTH_TOKEN)).rejects.toMatchObject({
606
+ message: SETUP_TOKEN_STORAGE_FAILED,
607
+ });
608
+ // Let the terminal-state handler run.
609
+ processes[0].finish("failure");
610
+ await new Promise((resolve) => setImmediate(resolve));
611
+ // The session failed: it holds no live session and released the lease.
612
+ expect(service.activeSessionCount()).toBe(0);
613
+ expect(leases.released).toEqual(["lease-1"]);
614
+ // No stored-session claim exists: the durable row never reached stored.
615
+ expect(store.rows.has(sessionId)).toBe(false);
616
+ // The completion is gone, so a read returns the same not-found error.
617
+ expect(() => service.completeSession(sessionId, OWNER_SCOPE)).toThrow(SETUP_TOKEN_SESSION_NOT_FOUND);
618
+ // No log line contains the token after the failure.
619
+ expect(logs.join("\n")).not.toContain(SYNTH_TOKEN);
620
+ });
621
+ it("purges the retained completion when the retention window ends", async () => {
622
+ const { service, processes } = buildService({ tokenRetentionMs: 5 });
623
+ const { sessionId } = await service.start(OWNER_SCOPE);
624
+ processes[0].surfacePrompt(FULL_LOGIN_URL);
625
+ service.submitCode(sessionId, OWNER_SCOPE, "code");
626
+ await processes[0].surfaceCredential(SYNTH_TOKEN);
627
+ processes[0].finish("success");
628
+ await new Promise((resolve) => setTimeout(resolve, 20));
629
+ // The retention timer dropped the in-memory session, so a read is not found.
630
+ expect(() => service.completeSession(sessionId, OWNER_SCOPE)).toThrow(SETUP_TOKEN_SESSION_NOT_FOUND);
631
+ });
632
+ });
633
+ // A deferred owner-bound secret writer. The test controls when the write
634
+ // settles, so it can hold the write in flight and drive a cancel or an expiry
635
+ // into the exact window that used to race the write. It records only the
636
+ // non-secret ids and the token, and it counts how many times the writer started.
637
+ function buildDeferredWriter(store) {
638
+ const calls = [];
639
+ let started = 0;
640
+ let pendingInput = null;
641
+ let resolveWrite = null;
642
+ let rejectWrite = null;
643
+ const writer = (input) => {
644
+ started += 1;
645
+ pendingInput = { scope: input.scope, sessionId: input.sessionId };
646
+ calls.push({ scope: input.scope, sessionId: input.sessionId, token: input.token });
647
+ return new Promise((resolve, reject) => {
648
+ resolveWrite = resolve;
649
+ rejectWrite = reject;
650
+ });
651
+ };
652
+ return {
653
+ writer,
654
+ calls,
655
+ startedCount: () => started,
656
+ // The commit resolves the write. It first simulates the atomic durable
657
+ // `stored` transition the production writer runs inside the same transaction.
658
+ resolve: async () => {
659
+ if (pendingInput) {
660
+ await store.markState(identityFromScope(pendingInput.scope, pendingInput.sessionId), "stored");
661
+ }
662
+ resolveWrite?.();
663
+ },
664
+ reject: () => rejectWrite?.(new Error("storage down")),
665
+ };
666
+ }
667
+ // Yields to the microtask queue, so a scheduled sink or lock step can run.
668
+ const flush = () => new Promise((resolve) => setImmediate(resolve));
669
+ describe("SetupTokenSessionService terminal-race credential write", () => {
670
+ for (const variant of [
671
+ { label: "cancel", run: (service, sessionId) => service.cancel(sessionId, OWNER_SCOPE), terminalState: "cancelled" },
672
+ { label: "expiry", run: (service, sessionId) => service.expire(sessionId, OWNER_SCOPE), terminalState: "timed_out" },
673
+ ]) {
674
+ it(`writes no secret when a ${variant.label} wins the race, and leaves no stored claim`, async () => {
675
+ const store = new FakeStore();
676
+ const deferred = buildDeferredWriter(store);
677
+ const { service, processes, leases } = buildService({ store, completeCredential: deferred.writer });
678
+ const { sessionId } = await service.start(OWNER_SCOPE);
679
+ processes[0].surfacePrompt(FULL_LOGIN_URL);
680
+ service.submitCode(sessionId, OWNER_SCOPE, "code");
681
+ // The terminal transition wins: it reaches the terminal state before the
682
+ // credential arrives. The cleanup removes the durable row.
683
+ await variant.run(service, sessionId);
684
+ expect(store.rows.has(sessionId)).toBe(false);
685
+ // The credential arrives after the terminal transition. The sink fails
686
+ // closed: it never calls the writer and it rejects with the fixed,
687
+ // non-secret error.
688
+ await expect(processes[0].surfaceCredential(SYNTH_TOKEN)).rejects.toMatchObject({
689
+ message: SETUP_TOKEN_STORAGE_FAILED,
690
+ });
691
+ // The writer never committed, so no unintended secret write happened.
692
+ expect(deferred.startedCount()).toBe(0);
693
+ expect(deferred.calls).toHaveLength(0);
694
+ // The durable row holds no stored claim.
695
+ expect(store.rows.has(sessionId)).toBe(false);
696
+ // The completion has no success: a read returns the same not-found error.
697
+ expect(() => service.completeSession(sessionId, OWNER_SCOPE)).toThrow(SETUP_TOKEN_SESSION_NOT_FOUND);
698
+ // The session released its lease and holds no live session.
699
+ expect(leases.released).toEqual(["lease-1"]);
700
+ expect(service.activeSessionCount()).toBe(0);
701
+ });
702
+ it(`does not erase the stored claim when the write wins the race, then a ${variant.label} arrives`, async () => {
703
+ const store = new FakeStore();
704
+ const deferred = buildDeferredWriter(store);
705
+ const { service, processes, leases } = buildService({ store, completeCredential: deferred.writer });
706
+ const { sessionId } = await service.start(OWNER_SCOPE);
707
+ processes[0].surfacePrompt(FULL_LOGIN_URL);
708
+ service.submitCode(sessionId, OWNER_SCOPE, "code");
709
+ // Start the credential write. It is in flight and holds the session lock, so
710
+ // a terminal transition that arrives now must wait for the write to settle.
711
+ const credentialSettled = processes[0].surfaceCredential(SYNTH_TOKEN);
712
+ await flush();
713
+ expect(deferred.startedCount()).toBe(1);
714
+ // The terminal transition arrives while the write is in flight. It blocks on
715
+ // the lock; it does not interleave with the write.
716
+ const terminalSettled = variant.run(service, sessionId);
717
+ await flush();
718
+ // The write wins the serialized ordering: it commits, then the service
719
+ // records the non-secret markers and releases the lock.
720
+ await deferred.resolve();
721
+ await credentialSettled;
722
+ await terminalSettled;
723
+ // The writer committed exactly once. No duplicate write is possible.
724
+ expect(deferred.startedCount()).toBe(1);
725
+ expect(deferred.calls).toEqual([{ scope: OWNER_SCOPE, sessionId, token: SYNTH_TOKEN }]);
726
+ // The terminal transition did not erase the claim: the durable row stays
727
+ // `stored` and the terminal API reports the completed state.
728
+ expect(store.rows.get(sessionId)?.state).toBe("stored");
729
+ const completion = service.completeSession(sessionId, OWNER_SCOPE);
730
+ expect(completion.storedSessionId).toBe(sessionId);
731
+ // The service released the sandbox lease at once.
732
+ expect(leases.released).toEqual(["lease-1"]);
733
+ });
734
+ }
735
+ it("does not erase the claim when the process reports success after the write wins a cancel race", async () => {
736
+ // This proves the natural success path stays intact when a cancel loses the
737
+ // race: the write commits, the cancel completes the session, and the later
738
+ // process-done success transition is an idempotent no-op.
739
+ const store = new FakeStore();
740
+ const deferred = buildDeferredWriter(store);
741
+ const { service, processes } = buildService({ store, completeCredential: deferred.writer });
742
+ const { sessionId } = await service.start(OWNER_SCOPE);
743
+ processes[0].surfacePrompt(FULL_LOGIN_URL);
744
+ service.submitCode(sessionId, OWNER_SCOPE, "code");
745
+ const credentialSettled = processes[0].surfaceCredential(SYNTH_TOKEN);
746
+ await flush();
747
+ const cancelSettled = service.cancel(sessionId, OWNER_SCOPE);
748
+ await flush();
749
+ await deferred.resolve();
750
+ await credentialSettled;
751
+ await cancelSettled;
752
+ // The process reports success after the sink resolved. The transition finds a
753
+ // completed, cleaned-up session and does nothing.
754
+ processes[0].finish("success");
755
+ await flush();
756
+ expect(deferred.startedCount()).toBe(1);
757
+ expect(store.rows.get(sessionId)?.state).toBe("stored");
758
+ expect(service.completeSession(sessionId, OWNER_SCOPE).storedSessionId).toBe(sessionId);
759
+ });
760
+ });
761
+ describe("no secret reaches a sink (SR-1, SR-5)", () => {
762
+ it("keeps the full login URL, the code, and the token out of the durable record", async () => {
763
+ const store = new FakeStore();
764
+ const { service, processes } = buildService({ store });
765
+ const { sessionId } = await service.start(OWNER_SCOPE);
766
+ processes[0].surfacePrompt(FULL_LOGIN_URL);
767
+ service.submitCode(sessionId, OWNER_SCOPE, "SECRETCODE123");
768
+ await processes[0].surfaceCredential(SYNTH_TOKEN);
769
+ const serialized = JSON.stringify([...store.rows.values()]);
770
+ expect(serialized).not.toContain("SECRETCODE123");
771
+ expect(serialized).not.toContain("STATEVALUE");
772
+ expect(serialized).not.toContain("cai/oauth/authorize");
773
+ expect(serialized).not.toContain(SYNTH_TOKEN);
774
+ });
775
+ it("sanitizes the login URL to origin and path only", () => {
776
+ expect(toSanitizedLoginUrl(FULL_LOGIN_URL)).toBe("https://claude.com/cai/oauth/authorize");
777
+ expect(toSanitizedLoginUrl("not a url")).toBe("[unparsable-login-url]");
778
+ });
779
+ it("redacts the browserCode and authorization_code in the HTTP-log sanitizer", () => {
780
+ const redacted = redactSensitive({
781
+ browserCode: "SECRETCODE123",
782
+ authorization_code: "AUTHCODE",
783
+ loginUrl: FULL_LOGIN_URL,
784
+ nested: { browserCode: "SECRETCODE123" },
785
+ });
786
+ expect(redacted.browserCode).toBe("[REDACTED]");
787
+ expect(redacted.authorization_code).toBe("[REDACTED]");
788
+ expect(redacted.loginUrl).toBe("https://claude.com/cai/oauth/authorize");
789
+ expect(redacted.nested.browserCode).toBe("[REDACTED]");
790
+ });
791
+ it("redacts the browserCode and authorization_code in the activity sanitizer", () => {
792
+ const redacted = sanitizeRecord({
793
+ browserCode: "SECRETCODE123",
794
+ authorization_code: "AUTHCODE",
795
+ loginUrl: FULL_LOGIN_URL,
796
+ });
797
+ const serialized = JSON.stringify(redacted);
798
+ expect(serialized).not.toContain("SECRETCODE123");
799
+ expect(serialized).not.toContain("AUTHCODE");
800
+ expect(serialized).not.toContain("STATEVALUE");
801
+ });
802
+ });
803
+ describe("confidential transport guard (SR-6, SR-7)", () => {
804
+ const authenticatedNoProxy = {
805
+ deploymentMode: "authenticated",
806
+ trustedProxies: [],
807
+ };
808
+ const authenticatedWithProxy = {
809
+ deploymentMode: "authenticated",
810
+ trustedProxies: ["10.0.0.5"],
811
+ };
812
+ const localTrusted = { deploymentMode: "local_trusted", trustedProxies: [] };
813
+ it("allows a direct TLS request", () => {
814
+ expect(evaluateConfidentialTransport(authenticatedNoProxy, {
815
+ socketEncrypted: true,
816
+ remoteAddress: "203.0.113.7",
817
+ forwardedProto: undefined,
818
+ }).allowed).toBe(true);
819
+ });
820
+ it("denies a direct non-loopback HTTP request (SR-6)", () => {
821
+ const decision = evaluateConfidentialTransport(authenticatedNoProxy, {
822
+ socketEncrypted: false,
823
+ remoteAddress: "203.0.113.7",
824
+ forwardedProto: undefined,
825
+ });
826
+ expect(decision.allowed).toBe(false);
827
+ });
828
+ it("allows a local_trusted loopback request as the only local exception (SR-6)", () => {
829
+ expect(evaluateConfidentialTransport(localTrusted, {
830
+ socketEncrypted: false,
831
+ remoteAddress: "127.0.0.1",
832
+ forwardedProto: undefined,
833
+ }).allowed).toBe(true);
834
+ // The same loopback request under authenticated mode fails closed.
835
+ expect(evaluateConfidentialTransport(authenticatedNoProxy, {
836
+ socketEncrypted: false,
837
+ remoteAddress: "127.0.0.1",
838
+ forwardedProto: undefined,
839
+ }).allowed).toBe(false);
840
+ });
841
+ it("denies a spoofed X-Forwarded-Proto when no trusted proxy is configured (SR-6)", () => {
842
+ const decision = evaluateConfidentialTransport(authenticatedNoProxy, {
843
+ socketEncrypted: false,
844
+ remoteAddress: "203.0.113.7",
845
+ forwardedProto: "https",
846
+ });
847
+ expect(decision.allowed).toBe(false);
848
+ });
849
+ it("allows a forwarded HTTPS request from the configured proxy allowlist (SR-6, SR-7)", () => {
850
+ const decision = evaluateConfidentialTransport(authenticatedWithProxy, {
851
+ socketEncrypted: false,
852
+ remoteAddress: "10.0.0.5",
853
+ forwardedProto: "https",
854
+ });
855
+ expect(decision.allowed).toBe(true);
856
+ });
857
+ it("allows a forwarded HTTPS request from a proxy inside a configured CIDR (SR-7)", () => {
858
+ const decision = evaluateConfidentialTransport({ deploymentMode: "authenticated", trustedProxies: ["10.0.0.0/24"] }, { socketEncrypted: false, remoteAddress: "10.0.0.200", forwardedProto: "https" });
859
+ expect(decision.allowed).toBe(true);
860
+ });
861
+ it("denies a forwarded HTTPS request from a peer outside the allowlist (SR-7)", () => {
862
+ const decision = evaluateConfidentialTransport(authenticatedWithProxy, {
863
+ socketEncrypted: false,
864
+ remoteAddress: "10.0.0.6",
865
+ forwardedProto: "https",
866
+ });
867
+ expect(decision.allowed).toBe(false);
868
+ });
869
+ it("fails closed at startup when no proxy allowlist is configured (SR-7)", () => {
870
+ expect(assessConfidentialStartup(authenticatedNoProxy).proxyForwardingEnabled).toBe(false);
871
+ expect(assessConfidentialStartup(authenticatedWithProxy).proxyForwardingEnabled).toBe(true);
872
+ });
873
+ it("allows a forwarded request under the operator edge-TLS declaration (SR-7)", () => {
874
+ const declared = { ...authenticatedNoProxy, edgeTlsTerminated: true };
875
+ // The platform edge labels the client hop https.
876
+ expect(evaluateConfidentialTransport(declared, {
877
+ socketEncrypted: false,
878
+ remoteAddress: "203.0.113.7",
879
+ forwardedProto: "https",
880
+ }).allowed).toBe(true);
881
+ // A platform edge that strips or never sets the header still counts: the
882
+ // declaration asserts TLS for every request the platform admits.
883
+ expect(evaluateConfidentialTransport(declared, {
884
+ socketEncrypted: false,
885
+ remoteAddress: "203.0.113.7",
886
+ forwardedProto: undefined,
887
+ }).allowed).toBe(true);
888
+ });
889
+ it("still denies a request the edge itself labels plain http under the declaration", () => {
890
+ const declared = { ...authenticatedNoProxy, edgeTlsTerminated: true };
891
+ const decision = evaluateConfidentialTransport(declared, {
892
+ socketEncrypted: false,
893
+ remoteAddress: "203.0.113.7",
894
+ forwardedProto: "http",
895
+ });
896
+ expect(decision.allowed).toBe(false);
897
+ expect(decision.reason).toBe("edge_labeled_plain_http");
898
+ });
899
+ it("keeps failing closed when the declaration is absent, so the default is unchanged", () => {
900
+ // The same request that the declaration admits fails closed without it —
901
+ // this pins that adding the option does not loosen the default posture.
902
+ expect(evaluateConfidentialTransport(authenticatedNoProxy, {
903
+ socketEncrypted: false,
904
+ remoteAddress: "203.0.113.7",
905
+ forwardedProto: "https",
906
+ }).allowed).toBe(false);
907
+ });
908
+ it("reports the edge-TLS declaration in the startup assessment", () => {
909
+ const assessment = assessConfidentialStartup({
910
+ ...authenticatedNoProxy,
911
+ edgeTlsTerminated: true,
912
+ });
913
+ expect(assessment.proxyForwardingEnabled).toBe(true);
914
+ expect(assessment.reason).toBe("edge_tls_termination_declared");
915
+ });
916
+ it("denies a direct non-loopback HTTP receive-token request, so it delivers no token (SR-6)", () => {
917
+ // The route calls this guard before receive-token. A denied decision makes
918
+ // the route return the fixed no-secret error and never read the token.
919
+ const decision = evaluateConfidentialTransport(authenticatedNoProxy, {
920
+ socketEncrypted: false,
921
+ remoteAddress: "203.0.113.7",
922
+ forwardedProto: undefined,
923
+ });
924
+ expect(decision.allowed).toBe(false);
925
+ });
926
+ it("denies a spoofed forwarded HTTPS receive-token request under a broad proxy setting (SR-7)", () => {
927
+ // A broad `TRUST_PROXY=true` setting never populates the dedicated allowlist,
928
+ // so the guard reads an empty allowlist and fails closed on the forwarded
929
+ // protocol. The route returns the fixed no-secret error and delivers no
930
+ // token.
931
+ const decision = evaluateConfidentialTransport(authenticatedNoProxy, {
932
+ socketEncrypted: false,
933
+ remoteAddress: "203.0.113.7",
934
+ forwardedProto: "https",
935
+ });
936
+ expect(decision.allowed).toBe(false);
937
+ });
938
+ });
939
+ describe("SetupTokenSessionService durable state scope", () => {
940
+ it("marks the durable record state by the full owner scope", async () => {
941
+ const store = new FakeStore();
942
+ const { service, processes } = buildService({ store });
943
+ const { sessionId } = await service.start(OWNER_SCOPE);
944
+ processes[0].surfacePrompt(FULL_LOGIN_URL);
945
+ await new Promise((resolve) => setImmediate(resolve));
946
+ expect(store.rows.get(sessionId)?.state).toBe("awaiting_code");
947
+ // A foreign-scope mark leaves the row unchanged, so a write never updates a
948
+ // row by the session id alone.
949
+ await store.markState({
950
+ sessionId,
951
+ companyId: OWNER_SCOPE.companyId,
952
+ ownerUserId: "intruder",
953
+ adapterType: OWNER_SCOPE.adapterType,
954
+ environmentId: OWNER_SCOPE.environmentId,
955
+ }, "submitting");
956
+ expect(store.rows.get(sessionId)?.state).toBe("awaiting_code");
957
+ });
958
+ });
959
+ // --- The durable, database-backed cleanup store ------------------------------
960
+ const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
961
+ const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport();
962
+ const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip;
963
+ if (!embeddedPostgresSupport.supported) {
964
+ console.warn(`Skipping durable setup-token cleanup store tests on this host: ${embeddedPostgresSupport.reason ?? "unsupported environment"}`);
965
+ }
966
+ describeEmbeddedPostgres("durable setup-token cleanup store (embedded postgres)", () => {
967
+ let stopDb;
968
+ let connectionString;
969
+ let db;
970
+ beforeAll(async () => {
971
+ const started = await startEmbeddedPostgresTestDatabase("claude-setup-token-store");
972
+ stopDb = started.cleanup;
973
+ connectionString = started.connectionString;
974
+ db = createDb(connectionString);
975
+ });
976
+ afterEach(async () => {
977
+ await db.delete(claudeSetupTokenSessions);
978
+ await db.delete(environments);
979
+ await db.delete(companies);
980
+ });
981
+ afterAll(async () => {
982
+ await stopDb?.();
983
+ });
984
+ // Seed a company and an environment, then return one fresh record identity.
985
+ async function seedScope() {
986
+ const companyId = randomUUID();
987
+ const environmentId = randomUUID();
988
+ await db.insert(companies).values({
989
+ id: companyId,
990
+ name: "Acme",
991
+ status: "active",
992
+ // A unique issue prefix per company; the column has a unique index.
993
+ issuePrefix: companyId.slice(0, 8),
994
+ createdAt: new Date(),
995
+ updatedAt: new Date(),
996
+ });
997
+ await db.insert(environments).values({
998
+ id: environmentId,
999
+ name: `sandbox-${environmentId.slice(0, 8)}`,
1000
+ driver: "sandbox",
1001
+ status: "active",
1002
+ config: { provider: "fake" },
1003
+ createdAt: new Date(),
1004
+ updatedAt: new Date(),
1005
+ });
1006
+ return {
1007
+ sessionId: randomUUID(),
1008
+ companyId,
1009
+ ownerUserId: `user-${randomUUID().slice(0, 8)}`,
1010
+ adapterType: "claude_local",
1011
+ environmentId,
1012
+ };
1013
+ }
1014
+ async function insertRecord(identity, state, deadlineMs) {
1015
+ const store = createDbSetupTokenCleanupStore(db);
1016
+ await store.record({
1017
+ ...identity,
1018
+ leaseId: "lease-1",
1019
+ deadline: deadlineMs,
1020
+ state,
1021
+ boundAt: null,
1022
+ });
1023
+ }
1024
+ async function readRow(sessionId) {
1025
+ const rows = await db
1026
+ .select()
1027
+ .from(claudeSetupTokenSessions)
1028
+ .where(eq(claudeSetupTokenSessions.sessionId, sessionId));
1029
+ return rows[0];
1030
+ }
1031
+ it("records the non-secret cleanup record and reads it back", async () => {
1032
+ const identity = await seedScope();
1033
+ await insertRecord(identity, "starting", Date.now() + 60_000);
1034
+ const row = await readRow(identity.sessionId);
1035
+ expect(row?.companyId).toBe(identity.companyId);
1036
+ expect(row?.ownerUserId).toBe(identity.ownerUserId);
1037
+ expect(row?.adapterType).toBe(identity.adapterType);
1038
+ expect(row?.environmentId).toBe(identity.environmentId);
1039
+ expect(row?.state).toBe("starting");
1040
+ expect(row?.boundAt).toBeNull();
1041
+ });
1042
+ it("consumes a stored claim once with one conditional write", async () => {
1043
+ const identity = await seedScope();
1044
+ await insertRecord(identity, "stored", Date.now() + 60_000);
1045
+ const store = createDbSetupTokenCleanupStore(db);
1046
+ const first = await store.consumeStoredClaim(identity);
1047
+ expect(first).not.toBeNull();
1048
+ expect(first?.sessionId).toBe(identity.sessionId);
1049
+ expect(first?.boundAt).not.toBeNull();
1050
+ expect((await readRow(identity.sessionId))?.boundAt).not.toBeNull();
1051
+ // A second consume finds the claim already consumed and returns no row.
1052
+ const second = await store.consumeStoredClaim(identity);
1053
+ expect(second).toBeNull();
1054
+ });
1055
+ it("returns no row for a foreign-scope consume and leaves the claim unconsumed", async () => {
1056
+ const identity = await seedScope();
1057
+ await insertRecord(identity, "stored", Date.now() + 60_000);
1058
+ const store = createDbSetupTokenCleanupStore(db);
1059
+ const foreign = await store.consumeStoredClaim({ ...identity, ownerUserId: "intruder" });
1060
+ expect(foreign).toBeNull();
1061
+ expect((await readRow(identity.sessionId))?.boundAt).toBeNull();
1062
+ });
1063
+ it("returns no row for an expired claim", async () => {
1064
+ const identity = await seedScope();
1065
+ await insertRecord(identity, "stored", Date.now() - 1_000);
1066
+ const store = createDbSetupTokenCleanupStore(db);
1067
+ const consumed = await store.consumeStoredClaim(identity);
1068
+ expect(consumed).toBeNull();
1069
+ expect((await readRow(identity.sessionId))?.boundAt).toBeNull();
1070
+ });
1071
+ it("returns no row for a non-stored claim", async () => {
1072
+ const identity = await seedScope();
1073
+ await insertRecord(identity, "submitting", Date.now() + 60_000);
1074
+ const store = createDbSetupTokenCleanupStore(db);
1075
+ const consumed = await store.consumeStoredClaim(identity);
1076
+ expect(consumed).toBeNull();
1077
+ expect((await readRow(identity.sessionId))?.boundAt).toBeNull();
1078
+ });
1079
+ it("lists terminal, expired, and consumed records but not a live stored claim", async () => {
1080
+ const store = createDbSetupTokenCleanupStore(db);
1081
+ const now = Date.now();
1082
+ const terminal = await seedScope();
1083
+ await insertRecord(terminal, "failed", now + 60_000);
1084
+ const expired = await seedScope();
1085
+ await insertRecord(expired, "awaiting_code", now - 1_000);
1086
+ const consumed = await seedScope();
1087
+ await insertRecord(consumed, "stored", now + 60_000);
1088
+ await store.consumeStoredClaim(consumed);
1089
+ const liveStored = await seedScope();
1090
+ await insertRecord(liveStored, "stored", now + 60_000);
1091
+ const reapable = await store.listReapable(now);
1092
+ const ids = new Set(reapable.map((record) => record.sessionId));
1093
+ expect(ids.has(terminal.sessionId)).toBe(true);
1094
+ expect(ids.has(expired.sessionId)).toBe(true);
1095
+ expect(ids.has(consumed.sessionId)).toBe(true);
1096
+ // A live, unexpired, unconsumed stored claim is not reapable.
1097
+ expect(ids.has(liveStored.sessionId)).toBe(false);
1098
+ });
1099
+ it("returns no row when the claim row lock holds until after the deadline", async () => {
1100
+ // This test proves the consume uses `clock_timestamp()`, not
1101
+ // `transaction_timestamp()`. The row starts with a far-future deadline, so
1102
+ // the consume treats it as a valid candidate and waits for the row lock. A
1103
+ // second connection holds the lock, sets a near deadline inside the locked
1104
+ // transaction, and commits after that deadline passes. The consume then
1105
+ // re-checks the predicate against the committed row with the current
1106
+ // database time, so the expired claim returns no row. A
1107
+ // `transaction_timestamp()` predicate would use the stale start time and
1108
+ // wrongly consume the claim.
1109
+ const identity = await seedScope();
1110
+ await insertRecord(identity, "stored", Date.now() + 3_600_000);
1111
+ const store = createDbSetupTokenCleanupStore(db);
1112
+ const lockDb = createDb(connectionString);
1113
+ let signalLocked;
1114
+ const locked = new Promise((resolve) => {
1115
+ signalLocked = resolve;
1116
+ });
1117
+ let releaseGate;
1118
+ const gate = new Promise((resolve) => {
1119
+ releaseGate = resolve;
1120
+ });
1121
+ const lockHeld = lockDb.transaction(async (tx) => {
1122
+ await tx.execute(sql `SELECT bound_at FROM claude_setup_token_sessions WHERE session_id = ${identity.sessionId} FOR UPDATE`);
1123
+ // Set a near deadline inside the locked transaction. The consume cannot see
1124
+ // this value until the transaction commits.
1125
+ await tx.execute(sql `UPDATE claude_setup_token_sessions SET deadline_at = clock_timestamp() + interval '300 milliseconds' WHERE session_id = ${identity.sessionId}`);
1126
+ signalLocked();
1127
+ await gate;
1128
+ });
1129
+ // Wait until the lock is truly held, then fire the consume. The consume
1130
+ // blocks on the row lock; its initial scan sees the far-future deadline.
1131
+ await locked;
1132
+ const consumePromise = store.consumeStoredClaim(identity);
1133
+ // Hold the lock until the near deadline passes, then commit.
1134
+ await delay(600);
1135
+ releaseGate();
1136
+ await lockHeld;
1137
+ const consumed = await consumePromise;
1138
+ expect(consumed).toBeNull();
1139
+ expect((await readRow(identity.sessionId))?.boundAt).toBeNull();
1140
+ await lockDb.$client.end();
1141
+ });
1142
+ });
1143
+ //# sourceMappingURL=setup-token-session.test.js.map