@wardby/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (412) hide show
  1. package/.env.example +131 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +2 -0
  4. package/README.md +282 -0
  5. package/bin/wardby.js +3 -0
  6. package/deploy/local/docker-compose.yml +19 -0
  7. package/dist/claude-coding-worker/driver.d.ts +70 -0
  8. package/dist/claude-coding-worker/driver.js +128 -0
  9. package/dist/claude-coding-worker/main.d.ts +2 -0
  10. package/dist/claude-coding-worker/main.js +41 -0
  11. package/dist/claude-coding-worker/sdk.d.ts +4 -0
  12. package/dist/claude-coding-worker/sdk.js +43 -0
  13. package/dist/claude-coding-worker/tool-relay.d.ts +1 -0
  14. package/dist/claude-coding-worker/tool-relay.js +9 -0
  15. package/dist/cli-help.d.ts +1 -0
  16. package/dist/cli-help.js +20 -0
  17. package/dist/cli.d.ts +18 -0
  18. package/dist/cli.js +629 -0
  19. package/dist/coding/observability.d.ts +59 -0
  20. package/dist/coding/observability.js +77 -0
  21. package/dist/coding/profile.d.ts +88 -0
  22. package/dist/coding/profile.js +137 -0
  23. package/dist/coding/protocol.d.ts +345 -0
  24. package/dist/coding/protocol.js +445 -0
  25. package/dist/coding/provider.d.ts +4 -0
  26. package/dist/coding/provider.js +18 -0
  27. package/dist/coding-proxy/main.d.ts +1 -0
  28. package/dist/coding-proxy/main.js +51 -0
  29. package/dist/coding-worker/artifact.d.ts +16 -0
  30. package/dist/coding-worker/artifact.js +37 -0
  31. package/dist/coding-worker/driver.d.ts +46 -0
  32. package/dist/coding-worker/driver.js +158 -0
  33. package/dist/coding-worker/errors.d.ts +1 -0
  34. package/dist/coding-worker/errors.js +23 -0
  35. package/dist/coding-worker/isolation-probe.d.ts +1 -0
  36. package/dist/coding-worker/isolation-probe.js +146 -0
  37. package/dist/coding-worker/keeper.d.ts +4 -0
  38. package/dist/coding-worker/keeper.js +29 -0
  39. package/dist/coding-worker/main.d.ts +2 -0
  40. package/dist/coding-worker/main.js +43 -0
  41. package/dist/coding-worker/sdk.d.ts +26 -0
  42. package/dist/coding-worker/sdk.js +28 -0
  43. package/dist/coding-worker/types.d.ts +64 -0
  44. package/dist/coding-worker/types.js +1 -0
  45. package/dist/config/providers.d.ts +158 -0
  46. package/dist/config/providers.js +162 -0
  47. package/dist/core/budget-groups.d.ts +72 -0
  48. package/dist/core/budget-groups.js +149 -0
  49. package/dist/core/budget.d.ts +63 -0
  50. package/dist/core/budget.js +69 -0
  51. package/dist/core/coding-queue.d.ts +32 -0
  52. package/dist/core/coding-queue.js +54 -0
  53. package/dist/core/cron.d.ts +31 -0
  54. package/dist/core/cron.js +43 -0
  55. package/dist/core/datastores.d.ts +41 -0
  56. package/dist/core/datastores.js +105 -0
  57. package/dist/core/db.d.ts +2 -0
  58. package/dist/core/db.js +2 -0
  59. package/dist/core/dispatch.d.ts +81 -0
  60. package/dist/core/dispatch.js +184 -0
  61. package/dist/core/engine-native.d.ts +29 -0
  62. package/dist/core/engine-native.js +259 -0
  63. package/dist/core/http-runtime.d.ts +7 -0
  64. package/dist/core/http-runtime.js +101 -0
  65. package/dist/core/lease.d.ts +13 -0
  66. package/dist/core/lease.js +22 -0
  67. package/dist/core/logger.d.ts +13 -0
  68. package/dist/core/logger.js +45 -0
  69. package/dist/core/memory-tools.d.ts +11 -0
  70. package/dist/core/memory-tools.js +122 -0
  71. package/dist/core/reconciler.d.ts +45 -0
  72. package/dist/core/reconciler.js +152 -0
  73. package/dist/core/runner.d.ts +87 -0
  74. package/dist/core/runner.js +529 -0
  75. package/dist/core/scheduler.d.ts +47 -0
  76. package/dist/core/scheduler.js +121 -0
  77. package/dist/core/secrets.d.ts +46 -0
  78. package/dist/core/secrets.js +100 -0
  79. package/dist/core/subagent-memory-tools.d.ts +18 -0
  80. package/dist/core/subagent-memory-tools.js +121 -0
  81. package/dist/core/timing.d.ts +7 -0
  82. package/dist/core/timing.js +7 -0
  83. package/dist/core/webhooks.d.ts +23 -0
  84. package/dist/core/webhooks.js +113 -0
  85. package/dist/env.d.ts +11 -0
  86. package/dist/env.js +13 -0
  87. package/dist/import/budgets.d.ts +16 -0
  88. package/dist/import/budgets.js +21 -0
  89. package/dist/import/bundle.d.ts +16 -0
  90. package/dist/import/bundle.js +61 -0
  91. package/dist/import/cli-args.d.ts +2 -0
  92. package/dist/import/cli-args.js +44 -0
  93. package/dist/import/create.d.ts +34 -0
  94. package/dist/import/create.js +363 -0
  95. package/dist/import/index.d.ts +25 -0
  96. package/dist/import/index.js +130 -0
  97. package/dist/import/model-gate.d.ts +1 -0
  98. package/dist/import/model-gate.js +3 -0
  99. package/dist/import/neutral-schema.d.ts +369 -0
  100. package/dist/import/neutral-schema.js +109 -0
  101. package/dist/import/preflight.d.ts +41 -0
  102. package/dist/import/preflight.js +71 -0
  103. package/dist/import/report.d.ts +5 -0
  104. package/dist/import/report.js +98 -0
  105. package/dist/import/tool-scan.d.ts +8 -0
  106. package/dist/import/tool-scan.js +20 -0
  107. package/dist/mcp/auth/ownership.d.ts +113 -0
  108. package/dist/mcp/auth/ownership.js +87 -0
  109. package/dist/mcp/auth/principal.d.ts +9 -0
  110. package/dist/mcp/auth/principal.js +9 -0
  111. package/dist/mcp/auth/resource-server.d.ts +47 -0
  112. package/dist/mcp/auth/resource-server.js +82 -0
  113. package/dist/mcp/auth/self-hosted/browser.d.ts +3 -0
  114. package/dist/mcp/auth/self-hosted/browser.js +273 -0
  115. package/dist/mcp/auth/self-hosted/cli.d.ts +2 -0
  116. package/dist/mcp/auth/self-hosted/cli.js +39 -0
  117. package/dist/mcp/auth/self-hosted/credentials.d.ts +58 -0
  118. package/dist/mcp/auth/self-hosted/credentials.js +106 -0
  119. package/dist/mcp/auth/self-hosted/rate-limit.d.ts +13 -0
  120. package/dist/mcp/auth/self-hosted/rate-limit.js +20 -0
  121. package/dist/mcp/auth/self-hosted/session.d.ts +36 -0
  122. package/dist/mcp/auth/self-hosted/session.js +110 -0
  123. package/dist/mcp/capabilities.d.ts +11 -0
  124. package/dist/mcp/capabilities.js +10 -0
  125. package/dist/mcp/context.d.ts +46 -0
  126. package/dist/mcp/context.js +1 -0
  127. package/dist/mcp/errors.d.ts +13 -0
  128. package/dist/mcp/errors.js +22 -0
  129. package/dist/mcp/index.d.ts +32 -0
  130. package/dist/mcp/index.js +187 -0
  131. package/dist/mcp/jsonrpc.d.ts +8 -0
  132. package/dist/mcp/jsonrpc.js +1 -0
  133. package/dist/mcp/server.d.ts +107 -0
  134. package/dist/mcp/server.js +203 -0
  135. package/dist/mcp/tasks/manager.d.ts +63 -0
  136. package/dist/mcp/tasks/manager.js +103 -0
  137. package/dist/mcp/tools/agents.d.ts +2 -0
  138. package/dist/mcp/tools/agents.js +375 -0
  139. package/dist/mcp/tools/budget-groups.d.ts +2 -0
  140. package/dist/mcp/tools/budget-groups.js +136 -0
  141. package/dist/mcp/tools/datastore.d.ts +2 -0
  142. package/dist/mcp/tools/datastore.js +217 -0
  143. package/dist/mcp/tools/memory.d.ts +3 -0
  144. package/dist/mcp/tools/memory.js +60 -0
  145. package/dist/mcp/tools/models.d.ts +8 -0
  146. package/dist/mcp/tools/models.js +15 -0
  147. package/dist/mcp/tools/runs.d.ts +2 -0
  148. package/dist/mcp/tools/runs.js +61 -0
  149. package/dist/mcp/tools/scheduling.d.ts +2 -0
  150. package/dist/mcp/tools/scheduling.js +48 -0
  151. package/dist/mcp/tools/secret-elicitation-form.d.ts +22 -0
  152. package/dist/mcp/tools/secret-elicitation-form.js +93 -0
  153. package/dist/mcp/tools/secret-elicitation-server.d.ts +5 -0
  154. package/dist/mcp/tools/secret-elicitation-server.js +57 -0
  155. package/dist/mcp/tools/secret-elicitation.d.ts +38 -0
  156. package/dist/mcp/tools/secret-elicitation.js +52 -0
  157. package/dist/mcp/tools/secrets.d.ts +13 -0
  158. package/dist/mcp/tools/secrets.js +124 -0
  159. package/dist/mcp/tools/subagents.d.ts +2 -0
  160. package/dist/mcp/tools/subagents.js +133 -0
  161. package/dist/mcp/tools/text-result.d.ts +6 -0
  162. package/dist/mcp/tools/text-result.js +3 -0
  163. package/dist/mcp/tools/tools.d.ts +2 -0
  164. package/dist/mcp/tools/tools.js +211 -0
  165. package/dist/mcp/tools/trigger.d.ts +2 -0
  166. package/dist/mcp/tools/trigger.js +120 -0
  167. package/dist/mcp/tools/webhooks.d.ts +2 -0
  168. package/dist/mcp/tools/webhooks.js +34 -0
  169. package/dist/mcp/transport/http-limits.d.ts +17 -0
  170. package/dist/mcp/transport/http-limits.js +92 -0
  171. package/dist/mcp/transport/stdio.d.ts +23 -0
  172. package/dist/mcp/transport/stdio.js +6 -0
  173. package/dist/mcp/transport/streamable-http.d.ts +34 -0
  174. package/dist/mcp/transport/streamable-http.js +158 -0
  175. package/dist/mcp/unattended-schedules.d.ts +15 -0
  176. package/dist/mcp/unattended-schedules.js +21 -0
  177. package/dist/mcp/webhooks/ingress.d.ts +17 -0
  178. package/dist/mcp/webhooks/ingress.js +33 -0
  179. package/dist/observability/config.d.ts +6 -0
  180. package/dist/observability/config.js +20 -0
  181. package/dist/observability/metrics-server.d.ts +11 -0
  182. package/dist/observability/metrics-server.js +47 -0
  183. package/dist/observability/metrics.d.ts +36 -0
  184. package/dist/observability/metrics.js +158 -0
  185. package/dist/providers/auth/authorization-server.d.ts +41 -0
  186. package/dist/providers/auth/authorization-server.js +1 -0
  187. package/dist/providers/auth/delegating.d.ts +44 -0
  188. package/dist/providers/auth/delegating.js +105 -0
  189. package/dist/providers/auth/index.d.ts +11 -0
  190. package/dist/providers/auth/index.js +20 -0
  191. package/dist/providers/auth/release-gate.d.ts +1 -0
  192. package/dist/providers/auth/release-gate.js +4 -0
  193. package/dist/providers/auth/self-hosted.d.ts +78 -0
  194. package/dist/providers/auth/self-hosted.js +377 -0
  195. package/dist/providers/auth/subject.d.ts +1 -0
  196. package/dist/providers/auth/subject.js +6 -0
  197. package/dist/providers/auth/types.d.ts +63 -0
  198. package/dist/providers/auth/types.js +22 -0
  199. package/dist/providers/coding-proxy/deny-port.d.ts +5 -0
  200. package/dist/providers/coding-proxy/deny-port.js +45 -0
  201. package/dist/providers/coding-proxy/environment-credentials.d.ts +7 -0
  202. package/dist/providers/coding-proxy/environment-credentials.js +16 -0
  203. package/dist/providers/coding-proxy/index.d.ts +7 -0
  204. package/dist/providers/coding-proxy/index.js +7 -0
  205. package/dist/providers/coding-proxy/memory-ledger.d.ts +17 -0
  206. package/dist/providers/coding-proxy/memory-ledger.js +103 -0
  207. package/dist/providers/coding-proxy/metering.d.ts +20 -0
  208. package/dist/providers/coding-proxy/metering.js +161 -0
  209. package/dist/providers/coding-proxy/prisma-ledger.d.ts +15 -0
  210. package/dist/providers/coding-proxy/prisma-ledger.js +213 -0
  211. package/dist/providers/coding-proxy/proxy.d.ts +75 -0
  212. package/dist/providers/coding-proxy/proxy.js +721 -0
  213. package/dist/providers/coding-proxy/runtime.d.ts +18 -0
  214. package/dist/providers/coding-proxy/runtime.js +39 -0
  215. package/dist/providers/coding-proxy/secure-fetch.d.ts +9 -0
  216. package/dist/providers/coding-proxy/secure-fetch.js +72 -0
  217. package/dist/providers/coding-proxy/server.d.ts +17 -0
  218. package/dist/providers/coding-proxy/server.js +139 -0
  219. package/dist/providers/coding-proxy/types.d.ts +99 -0
  220. package/dist/providers/coding-proxy/types.js +1 -0
  221. package/dist/providers/datastore/index.d.ts +2 -0
  222. package/dist/providers/datastore/index.js +2 -0
  223. package/dist/providers/datastore/postgres.d.ts +20 -0
  224. package/dist/providers/datastore/postgres.js +130 -0
  225. package/dist/providers/datastore/scoped.d.ts +10 -0
  226. package/dist/providers/datastore/scoped.js +46 -0
  227. package/dist/providers/datastore/types.d.ts +31 -0
  228. package/dist/providers/datastore/types.js +7 -0
  229. package/dist/providers/email/types.d.ts +46 -0
  230. package/dist/providers/email/types.js +8 -0
  231. package/dist/providers/engine/index.d.ts +1 -0
  232. package/dist/providers/engine/index.js +1 -0
  233. package/dist/providers/engine/types.d.ts +67 -0
  234. package/dist/providers/engine/types.js +16 -0
  235. package/dist/providers/executor/build.d.ts +6 -0
  236. package/dist/providers/executor/build.js +15 -0
  237. package/dist/providers/executor/composition.d.ts +14 -0
  238. package/dist/providers/executor/composition.js +123 -0
  239. package/dist/providers/executor/container.d.ts +184 -0
  240. package/dist/providers/executor/container.js +936 -0
  241. package/dist/providers/executor/dbos-status.d.ts +50 -0
  242. package/dist/providers/executor/dbos-status.js +64 -0
  243. package/dist/providers/executor/dbos.d.ts +53 -0
  244. package/dist/providers/executor/dbos.js +269 -0
  245. package/dist/providers/executor/in-process.d.ts +18 -0
  246. package/dist/providers/executor/in-process.js +38 -0
  247. package/dist/providers/executor/index.d.ts +8 -0
  248. package/dist/providers/executor/index.js +8 -0
  249. package/dist/providers/executor/routing.d.ts +48 -0
  250. package/dist/providers/executor/routing.js +70 -0
  251. package/dist/providers/executor/types.d.ts +55 -0
  252. package/dist/providers/executor/types.js +1 -0
  253. package/dist/providers/index.d.ts +43 -0
  254. package/dist/providers/index.js +18 -0
  255. package/dist/providers/jobs/docker-isolation.d.ts +166 -0
  256. package/dist/providers/jobs/docker-isolation.js +724 -0
  257. package/dist/providers/jobs/docker.d.ts +127 -0
  258. package/dist/providers/jobs/docker.js +1027 -0
  259. package/dist/providers/jobs/fake-kubernetes-api.d.ts +98 -0
  260. package/dist/providers/jobs/fake-kubernetes-api.js +142 -0
  261. package/dist/providers/jobs/fake.d.ts +23 -0
  262. package/dist/providers/jobs/fake.js +132 -0
  263. package/dist/providers/jobs/kubernetes-api.d.ts +60 -0
  264. package/dist/providers/jobs/kubernetes-api.js +20 -0
  265. package/dist/providers/jobs/kubernetes-client.d.ts +36 -0
  266. package/dist/providers/jobs/kubernetes-client.js +171 -0
  267. package/dist/providers/jobs/kubernetes-dry-run-fixture.d.ts +49 -0
  268. package/dist/providers/jobs/kubernetes-dry-run-fixture.js +141 -0
  269. package/dist/providers/jobs/kubernetes-isolation.d.ts +147 -0
  270. package/dist/providers/jobs/kubernetes-isolation.js +607 -0
  271. package/dist/providers/jobs/kubernetes-platform.d.ts +177 -0
  272. package/dist/providers/jobs/kubernetes-platform.js +280 -0
  273. package/dist/providers/jobs/kubernetes-preflight.d.ts +59 -0
  274. package/dist/providers/jobs/kubernetes-preflight.js +339 -0
  275. package/dist/providers/jobs/kubernetes-witness.d.ts +10 -0
  276. package/dist/providers/jobs/kubernetes-witness.js +193 -0
  277. package/dist/providers/jobs/kubernetes.d.ts +127 -0
  278. package/dist/providers/jobs/kubernetes.js +874 -0
  279. package/dist/providers/jobs/safe-extract.d.ts +9 -0
  280. package/dist/providers/jobs/safe-extract.js +325 -0
  281. package/dist/providers/jobs/types.d.ts +78 -0
  282. package/dist/providers/jobs/types.js +6 -0
  283. package/dist/providers/jobs/workspace-swap.d.ts +2 -0
  284. package/dist/providers/jobs/workspace-swap.js +47 -0
  285. package/dist/providers/llm/anthropic.d.ts +13 -0
  286. package/dist/providers/llm/anthropic.js +21 -0
  287. package/dist/providers/llm/bedrock.d.ts +19 -0
  288. package/dist/providers/llm/bedrock.js +33 -0
  289. package/dist/providers/llm/claude-messages.d.ts +106 -0
  290. package/dist/providers/llm/claude-messages.js +157 -0
  291. package/dist/providers/llm/claude-provider.d.ts +40 -0
  292. package/dist/providers/llm/claude-provider.js +31 -0
  293. package/dist/providers/llm/index.d.ts +10 -0
  294. package/dist/providers/llm/index.js +10 -0
  295. package/dist/providers/llm/openai.d.ts +23 -0
  296. package/dist/providers/llm/openai.js +156 -0
  297. package/dist/providers/llm/pricing-anthropic.d.ts +4 -0
  298. package/dist/providers/llm/pricing-anthropic.js +30 -0
  299. package/dist/providers/llm/pricing-bedrock-claude.d.ts +12 -0
  300. package/dist/providers/llm/pricing-bedrock-claude.js +37 -0
  301. package/dist/providers/llm/pricing-core.d.ts +26 -0
  302. package/dist/providers/llm/pricing-core.js +17 -0
  303. package/dist/providers/llm/pricing.d.ts +30 -0
  304. package/dist/providers/llm/pricing.js +74 -0
  305. package/dist/providers/llm/registration.d.ts +8 -0
  306. package/dist/providers/llm/registration.js +39 -0
  307. package/dist/providers/llm/routing.d.ts +25 -0
  308. package/dist/providers/llm/routing.js +32 -0
  309. package/dist/providers/llm/types.d.ts +85 -0
  310. package/dist/providers/llm/types.js +12 -0
  311. package/dist/providers/memory/index.d.ts +2 -0
  312. package/dist/providers/memory/index.js +2 -0
  313. package/dist/providers/memory/postgres.d.ts +20 -0
  314. package/dist/providers/memory/postgres.js +72 -0
  315. package/dist/providers/memory/types.d.ts +32 -0
  316. package/dist/providers/memory/types.js +17 -0
  317. package/dist/providers/secrets/app-key.d.ts +8 -0
  318. package/dist/providers/secrets/app-key.js +50 -0
  319. package/dist/providers/secrets/index.d.ts +10 -0
  320. package/dist/providers/secrets/index.js +13 -0
  321. package/dist/providers/secrets/transfer-envelope.d.ts +12 -0
  322. package/dist/providers/secrets/transfer-envelope.js +35 -0
  323. package/dist/providers/secrets/types.d.ts +18 -0
  324. package/dist/providers/secrets/types.js +12 -0
  325. package/dist/providers/storage/types.d.ts +18 -0
  326. package/dist/providers/storage/types.js +7 -0
  327. package/dist/providers/vcs/git.d.ts +105 -0
  328. package/dist/providers/vcs/git.js +710 -0
  329. package/dist/providers/vcs/github.d.ts +105 -0
  330. package/dist/providers/vcs/github.js +394 -0
  331. package/dist/providers/vcs/index.d.ts +6 -0
  332. package/dist/providers/vcs/index.js +26 -0
  333. package/dist/providers/vcs/types.d.ts +118 -0
  334. package/dist/providers/vcs/types.js +1 -0
  335. package/dist/sandbox/bounded-json.d.ts +2 -0
  336. package/dist/sandbox/bounded-json.js +36 -0
  337. package/dist/sandbox/bridge.d.ts +15 -0
  338. package/dist/sandbox/bridge.js +78 -0
  339. package/dist/sandbox/eval-core.d.ts +37 -0
  340. package/dist/sandbox/eval-core.js +159 -0
  341. package/dist/sandbox/fetch-policy.d.ts +24 -0
  342. package/dist/sandbox/fetch-policy.js +96 -0
  343. package/dist/sandbox/generated/zod-to-json-schema.bundle.js +5 -0
  344. package/dist/sandbox/generated/zod.bundle.js +1 -0
  345. package/dist/sandbox/host-functions.d.ts +29 -0
  346. package/dist/sandbox/host-functions.js +168 -0
  347. package/dist/sandbox/limits.d.ts +39 -0
  348. package/dist/sandbox/limits.js +39 -0
  349. package/dist/sandbox/parser-worker/__fixtures__/crash.d.ts +1 -0
  350. package/dist/sandbox/parser-worker/__fixtures__/crash.js +6 -0
  351. package/dist/sandbox/parser-worker/__fixtures__/echo.d.ts +1 -0
  352. package/dist/sandbox/parser-worker/__fixtures__/echo.js +5 -0
  353. package/dist/sandbox/parser-worker/__fixtures__/reject.d.ts +1 -0
  354. package/dist/sandbox/parser-worker/__fixtures__/reject.js +5 -0
  355. package/dist/sandbox/parser-worker/__fixtures__/spin-forever.d.ts +1 -0
  356. package/dist/sandbox/parser-worker/__fixtures__/spin-forever.js +11 -0
  357. package/dist/sandbox/parser-worker/pool.d.ts +13 -0
  358. package/dist/sandbox/parser-worker/pool.js +111 -0
  359. package/dist/sandbox/parser-worker/worker.d.ts +14 -0
  360. package/dist/sandbox/parser-worker/worker.js +63 -0
  361. package/dist/sandbox/pii-redaction.d.ts +1 -0
  362. package/dist/sandbox/pii-redaction.js +29 -0
  363. package/dist/sandbox/prelude.d.ts +26 -0
  364. package/dist/sandbox/prelude.js +408 -0
  365. package/dist/sandbox/quickjs-module.d.ts +13 -0
  366. package/dist/sandbox/quickjs-module.js +19 -0
  367. package/dist/sandbox/run-in-sandbox.d.ts +34 -0
  368. package/dist/sandbox/run-in-sandbox.js +53 -0
  369. package/dist/sandbox/safe-fetch.d.ts +26 -0
  370. package/dist/sandbox/safe-fetch.js +140 -0
  371. package/dist/sandbox/tool-capabilities.d.ts +66 -0
  372. package/dist/sandbox/tool-capabilities.js +103 -0
  373. package/dist/sandbox/zod-params.d.ts +22 -0
  374. package/dist/sandbox/zod-params.js +60 -0
  375. package/dist/serve.d.ts +14 -0
  376. package/dist/serve.js +54 -0
  377. package/dist/wardby-bin.d.ts +2 -0
  378. package/dist/wardby-bin.js +20 -0
  379. package/package.json +123 -0
  380. package/prisma/migrations/20260905000000_init/migration.sql +34 -0
  381. package/prisma/migrations/20260905010000_scheduler_durability/migration.sql +31 -0
  382. package/prisma/migrations/20260905020000_tools_multiturn/migration.sql +47 -0
  383. package/prisma/migrations/20260905030000_cache_tool_json_schema/migration.sql +13 -0
  384. package/prisma/migrations/20260906000000_mcp_phase4/migration.sql +115 -0
  385. package/prisma/migrations/20260906010000_run_final_text/migration.sql +7 -0
  386. package/prisma/migrations/20260906020000_run_turns/migration.sql +6 -0
  387. package/prisma/migrations/20260906030000_task_principal_id/migration.sql +5 -0
  388. package/prisma/migrations/20260906040000_secure_self_hosted_oauth/migration.sql +67 -0
  389. package/prisma/migrations/20260906050000_phase5_coding_agents/migration.sql +52 -0
  390. package/prisma/migrations/20260906060000_tool_capability_scoping/migration.sql +22 -0
  391. package/prisma/migrations/20260906070000_datastore_pii_flag/migration.sql +6 -0
  392. package/prisma/migrations/20260906080000_secret_elicitation_outcomes/migration.sql +13 -0
  393. package/prisma/migrations/20260907010000_coding_proxy_ledger/migration.sql +57 -0
  394. package/prisma/migrations/20260907020000_budget_groups/migration.sql +24 -0
  395. package/prisma/migrations/20260907030000_coding_webhook_task_override/migration.sql +4 -0
  396. package/prisma/migrations/20260908010000_run_execution_backend/migration.sql +2 -0
  397. package/prisma/migrations/20260908020000_coding_run_audit_metadata/migration.sql +7 -0
  398. package/prisma/migrations/20260910010000_agent_secret_bound_name/migration.sql +16 -0
  399. package/prisma/migrations/20260911010000_coding_worker_toolchains/migration.sql +5 -0
  400. package/prisma/migrations/20260912010000_coding_provider_contract/migration.sql +24 -0
  401. package/prisma/migrations/20260912020000_coding_proxy_protocol/migration.sql +10 -0
  402. package/prisma/migrations/20260912030000_agent_memory/migration.sql +18 -0
  403. package/prisma/migrations/20260912040000_named_shared_datastores/migration.sql +68 -0
  404. package/prisma/migrations/20260913000000_run_trigger_webhook/migration.sql +2 -0
  405. package/prisma/migrations/20260913010000_agent_sub_agent/migration.sql +32 -0
  406. package/prisma/migrations/20260913020000_run_task_override/migration.sql +5 -0
  407. package/prisma/migrations/20260913030000_coding_run_root/migration.sql +11 -0
  408. package/prisma/migrations/20260922010000_coding_run_queued_at/migration.sql +8 -0
  409. package/prisma/migrations/20260922020000_coding_workspace_disk/migration.sql +8 -0
  410. package/prisma/migrations/migration_lock.toml +3 -0
  411. package/prisma/schema.prisma +723 -0
  412. package/scripts/git-askpass.sh +11 -0
@@ -0,0 +1,8 @@
1
+ export declare const SPEC_UNSUPPORTED_HOST_APIS: readonly string[];
2
+ export declare function supportedGlobalsFromPrelude(preludeSource: string): Set<string>;
3
+ export declare function scanToolCode(code: string, supported: ReadonlySet<string>): {
4
+ ok: true;
5
+ } | {
6
+ ok: false;
7
+ rejectedApis: string[];
8
+ };
@@ -0,0 +1,20 @@
1
+ // Host APIs a migration bundle's tool may reference that wardby will NOT
2
+ // provide. sendEmail/getInboundEmail are intentionally absent here: they
3
+ // exist as throwing placeholders in SANDBOX_PRELUDE (they import fine and
4
+ // fail at runtime). npmLockUpdate is roadmap-excluded (spec §3.3), so a tool
5
+ // referencing it is rejected at import. Cross-checked against the live
6
+ // prelude so the list self-heals off anything the prelude later defines.
7
+ export const SPEC_UNSUPPORTED_HOST_APIS = ["npmLockUpdate"];
8
+ export function supportedGlobalsFromPrelude(preludeSource) {
9
+ const names = new Set();
10
+ const re = /globalThis\.([A-Za-z_$][\w$]*)\s*=/g;
11
+ let m;
12
+ while ((m = re.exec(preludeSource)) !== null)
13
+ names.add(m[1]);
14
+ return names;
15
+ }
16
+ export function scanToolCode(code, supported) {
17
+ const denied = SPEC_UNSUPPORTED_HOST_APIS.filter((api) => !supported.has(api));
18
+ const hit = denied.filter((api) => new RegExp(`\\b${api}\\b`).test(code));
19
+ return hit.length === 0 ? { ok: true } : { ok: false, rejectedApis: hit };
20
+ }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Single seam for ownership decisions across Agent/BudgetGroup/Tool/Secret/
3
+ * Webhook/Task (and anything else that gains a nullable owner column
4
+ * later). A null owner means "public": readable AND mutable by anyone —
5
+ * there is no gatekeeper, so no principal is turned away. `isOwner` stays
6
+ * available (strict, no null-passthrough) for checks that must stay
7
+ * owner-only regardless of publicness — e.g. a future claim-on-edit
8
+ * feature would use it to tell "already owned" apart from "public" before
9
+ * deciding whether to stamp a new owner. Every tool file routes through
10
+ * this module instead of re-deriving the null-handling itself, so changing
11
+ * the policy again later is a change here, not an N-file sweep.
12
+ */
13
+ import type { PrismaClient, Tool } from "@prisma/client";
14
+ export declare function canRead(ownerId: string | null, principalId: string): boolean;
15
+ export declare function isOwner(ownerId: string | null, principalId: string): boolean;
16
+ /** Same rule as canRead: a null owner is a mutation target for anyone, same as it's a read target for anyone. */
17
+ export declare function canMutate(ownerId: string | null, principalId: string): boolean;
18
+ export declare function assertCanMutate(ownerId: string | null, principalId: string, notFoundMessage: string): void;
19
+ /** Prisma where-clause fragment: rows owned by the caller, plus public (null-owner) rows. */
20
+ export declare function visibleToPrincipal(principalId: string): {
21
+ OR: ({
22
+ ownerId: string;
23
+ } | {
24
+ ownerId: null;
25
+ })[];
26
+ };
27
+ export declare function requireOwnedAgent(db: PrismaClient, id: string, principalId: string): Promise<{
28
+ name: string;
29
+ model: string;
30
+ budgetUsd: import("@prisma/client/runtime/library").Decimal;
31
+ id: string;
32
+ systemPrompt: string;
33
+ createdAt: Date;
34
+ updatedAt: Date;
35
+ kind: import("@prisma/client").$Enums.AgentKind;
36
+ schedule: string | null;
37
+ timezone: string;
38
+ scheduleEnabled: boolean;
39
+ lastScheduledAt: Date | null;
40
+ maxTurns: number;
41
+ ownerId: string | null;
42
+ budgetGroupId: string | null;
43
+ memoryEnabled: boolean;
44
+ }>;
45
+ export declare function requireReadableAgent(db: PrismaClient, id: string, principalId: string): Promise<{
46
+ name: string;
47
+ model: string;
48
+ budgetUsd: import("@prisma/client/runtime/library").Decimal;
49
+ id: string;
50
+ systemPrompt: string;
51
+ createdAt: Date;
52
+ updatedAt: Date;
53
+ kind: import("@prisma/client").$Enums.AgentKind;
54
+ schedule: string | null;
55
+ timezone: string;
56
+ scheduleEnabled: boolean;
57
+ lastScheduledAt: Date | null;
58
+ maxTurns: number;
59
+ ownerId: string | null;
60
+ budgetGroupId: string | null;
61
+ memoryEnabled: boolean;
62
+ }>;
63
+ export declare function requireOwnedBudgetGroup(db: PrismaClient, id: string, principalId: string): Promise<{
64
+ name: string;
65
+ id: string;
66
+ createdAt: Date;
67
+ updatedAt: Date;
68
+ ownerId: string | null;
69
+ dailyBudgetUsd: import("@prisma/client/runtime/library").Decimal | null;
70
+ weeklyBudgetUsd: import("@prisma/client/runtime/library").Decimal | null;
71
+ monthlyBudgetUsd: import("@prisma/client/runtime/library").Decimal | null;
72
+ warnThresholdRatio: import("@prisma/client/runtime/library").Decimal;
73
+ }>;
74
+ export declare function requireReadableBudgetGroup(db: PrismaClient, id: string, principalId: string): Promise<{
75
+ name: string;
76
+ id: string;
77
+ createdAt: Date;
78
+ updatedAt: Date;
79
+ ownerId: string | null;
80
+ dailyBudgetUsd: import("@prisma/client/runtime/library").Decimal | null;
81
+ weeklyBudgetUsd: import("@prisma/client/runtime/library").Decimal | null;
82
+ monthlyBudgetUsd: import("@prisma/client/runtime/library").Decimal | null;
83
+ warnThresholdRatio: import("@prisma/client/runtime/library").Decimal;
84
+ }>;
85
+ export declare function requireOwnedSecret(db: PrismaClient, id: string, principalId: string): Promise<void>;
86
+ export declare function requireOwnedWebhook(db: PrismaClient, id: string, principalId: string): Promise<void>;
87
+ export declare function requireOwnedDatastore(db: PrismaClient, id: string, principalId: string): Promise<{
88
+ name: string;
89
+ id: string;
90
+ createdAt: Date;
91
+ updatedAt: Date;
92
+ ownerId: string | null;
93
+ }>;
94
+ export declare function requireOwnedTool(db: Pick<PrismaClient, "tool">, id: string, principalId: string): Promise<Tool>;
95
+ /**
96
+ * A Task's principalId is the caller who triggered it, not the underlying
97
+ * agent's owner — a public agent's runs are still private to whoever
98
+ * triggered them. No canRead/null=public path exists here on purpose.
99
+ */
100
+ export declare function requireOwnedTask(db: PrismaClient, taskId: string, principalId: string): Promise<{
101
+ status: import("@prisma/client").$Enums.TaskStatus;
102
+ runId: string | null;
103
+ error: import("@prisma/client/runtime/library").JsonValue | null;
104
+ result: import("@prisma/client/runtime/library").JsonValue | null;
105
+ id: string;
106
+ createdAt: Date;
107
+ updatedAt: Date;
108
+ kind: import("@prisma/client").$Enums.TaskKind;
109
+ principalId: string | null;
110
+ inputRequests: import("@prisma/client/runtime/library").JsonValue | null;
111
+ inputResponses: import("@prisma/client/runtime/library").JsonValue | null;
112
+ ttlAt: Date;
113
+ }>;
@@ -0,0 +1,87 @@
1
+ import { McpError } from "../errors.js";
2
+ export function canRead(ownerId, principalId) {
3
+ return ownerId === null || ownerId === principalId;
4
+ }
5
+ export function isOwner(ownerId, principalId) {
6
+ return ownerId === principalId;
7
+ }
8
+ /** Same rule as canRead: a null owner is a mutation target for anyone, same as it's a read target for anyone. */
9
+ export function canMutate(ownerId, principalId) {
10
+ return ownerId === null || ownerId === principalId;
11
+ }
12
+ export function assertCanMutate(ownerId, principalId, notFoundMessage) {
13
+ if (!canMutate(ownerId, principalId))
14
+ throw new McpError(403, notFoundMessage);
15
+ }
16
+ /** Prisma where-clause fragment: rows owned by the caller, plus public (null-owner) rows. */
17
+ export function visibleToPrincipal(principalId) {
18
+ return { OR: [{ ownerId: principalId }, { ownerId: null }] };
19
+ }
20
+ export async function requireOwnedAgent(db, id, principalId) {
21
+ const agent = await db.agent.findUnique({ where: { id } });
22
+ if (!agent)
23
+ throw new McpError(404, `Agent "${id}" not found.`);
24
+ assertCanMutate(agent.ownerId, principalId, `Agent "${id}" is not owned by the caller.`);
25
+ return agent;
26
+ }
27
+ export async function requireReadableAgent(db, id, principalId) {
28
+ const agent = await db.agent.findUnique({ where: { id } });
29
+ if (!agent || !canRead(agent.ownerId, principalId)) {
30
+ throw new McpError(404, `Agent "${id}" not found.`);
31
+ }
32
+ return agent;
33
+ }
34
+ export async function requireOwnedBudgetGroup(db, id, principalId) {
35
+ const group = await db.budgetGroup.findUnique({ where: { id } });
36
+ if (!group)
37
+ throw new McpError(404, `Budget group "${id}" not found.`);
38
+ assertCanMutate(group.ownerId, principalId, `Budget group "${id}" is not owned by the caller.`);
39
+ return group;
40
+ }
41
+ export async function requireReadableBudgetGroup(db, id, principalId) {
42
+ const group = await db.budgetGroup.findUnique({ where: { id } });
43
+ if (!group || !canRead(group.ownerId, principalId)) {
44
+ throw new McpError(404, `Budget group "${id}" not found.`);
45
+ }
46
+ return group;
47
+ }
48
+ export async function requireOwnedSecret(db, id, principalId) {
49
+ const secret = await db.secret.findUnique({ where: { id } });
50
+ if (!secret)
51
+ throw new McpError(403, `Secret "${id}" is not owned by the caller.`);
52
+ assertCanMutate(secret.ownerId, principalId, `Secret "${id}" is not owned by the caller.`);
53
+ }
54
+ export async function requireOwnedWebhook(db, id, principalId) {
55
+ const webhook = await db.webhook.findUnique({ where: { id } });
56
+ if (!webhook)
57
+ throw new McpError(403, `Webhook "${id}" is not owned by the caller.`);
58
+ assertCanMutate(webhook.ownerId, principalId, `Webhook "${id}" is not owned by the caller.`);
59
+ }
60
+ export async function requireOwnedDatastore(db, id, principalId) {
61
+ const datastore = await db.datastore.findUnique({ where: { id } });
62
+ if (!datastore)
63
+ throw new McpError(403, `Datastore "${id}" is not owned by the caller.`);
64
+ assertCanMutate(datastore.ownerId, principalId, `Datastore "${id}" is not owned by the caller.`);
65
+ return datastore;
66
+ }
67
+ export async function requireOwnedTool(db, id, principalId) {
68
+ const tool = await db.tool.findUnique({ where: { id } });
69
+ if (!tool)
70
+ throw new McpError(404, `Tool "${id}" not found.`);
71
+ assertCanMutate(tool.ownerId, principalId, `Tool "${id}" is not owned by the caller.`);
72
+ return tool;
73
+ }
74
+ /**
75
+ * A Task's principalId is the caller who triggered it, not the underlying
76
+ * agent's owner — a public agent's runs are still private to whoever
77
+ * triggered them. No canRead/null=public path exists here on purpose.
78
+ */
79
+ export async function requireOwnedTask(db, taskId, principalId) {
80
+ const task = await db.task.findUnique({ where: { id: taskId } });
81
+ // 404, not 403: confirming a taskId exists at all to a non-owner is
82
+ // itself a small information leak — same reasoning as agent reads.
83
+ if (!task || !isOwner(task.principalId, principalId)) {
84
+ throw new McpError(404, `Task "${taskId}" not found.`);
85
+ }
86
+ return task;
87
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Find-or-create a Principal by subject. `subject` is either the IdP's
3
+ * subject claim (delegating/self-hosted, from a validated AuthProfile) or
4
+ * the configured LOCAL_PRINCIPAL (stdio, no token) — either way, the caller
5
+ * always carries an owner, so a deployment that later moves from stdio to
6
+ * HTTP+OAuth doesn't orphan locally-authored agents.
7
+ */
8
+ import type { PrismaClient, Principal } from "@prisma/client";
9
+ export declare function resolvePrincipal(subject: string, db: PrismaClient): Promise<Principal>;
@@ -0,0 +1,9 @@
1
+ import { requireSubject } from "../../providers/auth/subject.js";
2
+ export async function resolvePrincipal(subject, db) {
3
+ requireSubject(subject);
4
+ return db.principal.upsert({
5
+ where: { subject },
6
+ create: { subject },
7
+ update: {},
8
+ });
9
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * OAuth 2.1 resource-server surface: PRM (.well-known) discovery, bearer
3
+ * validation, and scope enforcement. Works identically regardless of which
4
+ * AuthProvider (delegating | self-hosted) is active — both implement
5
+ * verifyBearer, so this file never branches on auth mode.
6
+ */
7
+ import type { PrismaClient } from "@prisma/client";
8
+ import type { AuthProvider } from "../../providers/auth/types.js";
9
+ import { McpError } from "../errors.js";
10
+ import type { McpRequestContext, McpProviders } from "../context.js";
11
+ /** The initial capability tiers (§Scopes → capability tiers in the design doc). */
12
+ export declare const SCOPES_SUPPORTED: string[];
13
+ export interface ProtectedResourceMetadata {
14
+ resource: string;
15
+ authorization_servers: string[];
16
+ scopes_supported: string[];
17
+ bearer_methods_supported: string[];
18
+ }
19
+ /** RFC 9728 §3.1: insert `/.well-known/oauth-protected-resource` before the resource's path. */
20
+ export declare function protectedResourceMetadataUrl(canonicalUri: string): string;
21
+ export declare function protectedResourceMetadata(config: {
22
+ canonicalUri: string;
23
+ authorizationServers: string[];
24
+ }): ProtectedResourceMetadata;
25
+ export interface AuthenticateHeaders {
26
+ authorization?: string;
27
+ }
28
+ export interface AuthenticateDeps {
29
+ authProvider: AuthProvider;
30
+ db: PrismaClient;
31
+ providers: McpProviders;
32
+ canonicalUri: string;
33
+ }
34
+ /**
35
+ * Validates the bearer token in the `Authorization` header only — never a
36
+ * query string, so a caller passing a token any other way is structurally
37
+ * invisible here and falls through to the missing-header 401.
38
+ */
39
+ export declare function authenticate(headers: AuthenticateHeaders, deps: AuthenticateDeps): Promise<McpRequestContext>;
40
+ /**
41
+ * Throws a 403 insufficient_scope McpError listing every scope this
42
+ * operation needs (not just the ones the caller is missing) — so a client
43
+ * can re-auth once with the full required set instead of round-tripping
44
+ * per missing scope.
45
+ */
46
+ export declare function requireScope(ctx: McpRequestContext, canonicalUri: string, ...scopes: string[]): void;
47
+ export { McpError };
@@ -0,0 +1,82 @@
1
+ import { resolvePrincipal } from "./principal.js";
2
+ import { McpError, unauthorized, insufficientScope } from "../errors.js";
3
+ /** The initial capability tiers (§Scopes → capability tiers in the design doc). */
4
+ export const SCOPES_SUPPORTED = [
5
+ "agents:read",
6
+ "agents:write",
7
+ "tools:write",
8
+ "runs:trigger",
9
+ "datastore:write",
10
+ "secrets:write",
11
+ "webhooks:write",
12
+ "budget_groups:write",
13
+ // Reassigns an agent's owner regardless of who currently owns it (or
14
+ // whether it's public) — a step above agents:write, which only ever lets
15
+ // a caller act on agents they already own or that are unowned.
16
+ "agents:admin",
17
+ ];
18
+ /** RFC 9728 §3.1: insert `/.well-known/oauth-protected-resource` before the resource's path. */
19
+ export function protectedResourceMetadataUrl(canonicalUri) {
20
+ const url = new URL(canonicalUri);
21
+ const path = url.pathname === "/" ? "" : url.pathname;
22
+ return `${url.origin}/.well-known/oauth-protected-resource${path}`;
23
+ }
24
+ export function protectedResourceMetadata(config) {
25
+ return {
26
+ resource: config.canonicalUri,
27
+ authorization_servers: config.authorizationServers,
28
+ scopes_supported: SCOPES_SUPPORTED,
29
+ bearer_methods_supported: ["header"],
30
+ };
31
+ }
32
+ /**
33
+ * Validates the bearer token in the `Authorization` header only — never a
34
+ * query string, so a caller passing a token any other way is structurally
35
+ * invisible here and falls through to the missing-header 401.
36
+ */
37
+ export async function authenticate(headers, deps) {
38
+ const resourceMetadataUrl = protectedResourceMetadataUrl(deps.canonicalUri);
39
+ const header = headers.authorization;
40
+ if (!header || !header.startsWith("Bearer ")) {
41
+ throw unauthorized("Missing or malformed Authorization header (expected: Bearer <token>).", resourceMetadataUrl);
42
+ }
43
+ const token = header.slice("Bearer ".length).trim();
44
+ if (!token) {
45
+ throw unauthorized("Missing or malformed Authorization header (expected: Bearer <token>).", resourceMetadataUrl);
46
+ }
47
+ let verified;
48
+ try {
49
+ verified = await deps.authProvider.verifyBearer(token);
50
+ }
51
+ catch {
52
+ throw unauthorized("Invalid token.", resourceMetadataUrl);
53
+ }
54
+ const principal = await resolvePrincipal(verified.subject, deps.db);
55
+ return {
56
+ principal,
57
+ scopes: new Set(verified.scopes),
58
+ canonicalUri: deps.canonicalUri,
59
+ providers: deps.providers,
60
+ db: deps.db,
61
+ // authenticate() only sees the HTTP Authorization header, before the
62
+ // JSON-RPC body (and its _meta envelope) is even parsed — the real
63
+ // per-call value is computed later, in server.ts's resolveCtx, once the
64
+ // envelope is available. Callers using ONLY this function's return
65
+ // value (streamable-http.ts's auth gate) never read this field or mcpReq.
66
+ clientSupportsTasks: false,
67
+ mcpReq: { requestState: () => undefined },
68
+ };
69
+ }
70
+ /**
71
+ * Throws a 403 insufficient_scope McpError listing every scope this
72
+ * operation needs (not just the ones the caller is missing) — so a client
73
+ * can re-auth once with the full required set instead of round-tripping
74
+ * per missing scope.
75
+ */
76
+ export function requireScope(ctx, canonicalUri, ...scopes) {
77
+ const held = scopes.every((s) => ctx.scopes.has(s));
78
+ if (held)
79
+ return;
80
+ throw insufficientScope(scopes, protectedResourceMetadataUrl(canonicalUri));
81
+ }
82
+ export { McpError };
@@ -0,0 +1,3 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import type { SelfHostedAuthProvider } from "../../../providers/auth/self-hosted.js";
3
+ export declare function browserHandler(provider: SelfHostedAuthProvider): (req: IncomingMessage, res: ServerResponse, url: URL, body: unknown) => Promise<boolean>;
@@ -0,0 +1,273 @@
1
+ import { randomUUID, randomBytes } from "node:crypto";
2
+ import { PostgresRateLimiter, RateLimitError } from "./rate-limit.js";
3
+ import { logger } from "../../../core/logger.js";
4
+ const authLog = logger.child({ module: "self-hosted-oauth" });
5
+ const paths = new Set([
6
+ "/login",
7
+ "/consent",
8
+ "/logout",
9
+ "/authorize",
10
+ "/register",
11
+ "/token",
12
+ "/revoke",
13
+ "/.well-known/oauth-authorization-server",
14
+ ]);
15
+ function escape(value) {
16
+ return value.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
17
+ }
18
+ // CORS-mode same-origin fetch retains Origin under no-referrer, unlike native form POSTs.
19
+ const FORM_SCRIPT = `document.addEventListener('submit', async (event) => {
20
+ const form = event.target;
21
+ event.preventDefault();
22
+ const body = new URLSearchParams(new FormData(form, event.submitter));
23
+ try {
24
+ const response = await fetch(form.action, { method: 'POST', mode: 'cors', credentials: 'same-origin', headers: { 'accept': 'application/json' }, body });
25
+ const data = await response.json();
26
+ if (response.ok && typeof data.redirect === 'string') { location.assign(data.redirect); return; }
27
+ } catch {}
28
+ document.getElementById('auth-error').textContent = 'Unable to complete the request. Reload this page and try again.';
29
+ });`;
30
+ function html(res, content) {
31
+ const nonce = randomBytes(18).toString("base64");
32
+ res.setHeader("content-security-policy", `default-src 'none'; script-src 'nonce-${nonce}'; connect-src 'self'; form-action 'self'; frame-ancestors 'none'; base-uri 'none'`);
33
+ res
34
+ .writeHead(200, { "content-type": "text/html; charset=utf-8" })
35
+ .end('<!doctype html><html lang="en"><meta charset="utf-8"><title>wardby authorization</title><body>' +
36
+ content +
37
+ '<p id="auth-error" role="alert"></p><noscript>JavaScript is required for secure form submission.</noscript><script nonce="' +
38
+ nonce +
39
+ '">' +
40
+ FORM_SCRIPT +
41
+ "</script></body></html>");
42
+ }
43
+ function json(res, status, body) {
44
+ res.writeHead(status, { "content-type": "application/json" }).end(JSON.stringify(body));
45
+ }
46
+ function redirect(res, location, req) {
47
+ if (req?.method === "POST" && req.headers.accept === "application/json") {
48
+ json(res, 200, { redirect: location });
49
+ return;
50
+ }
51
+ res.writeHead(303, { location }).end();
52
+ }
53
+ function hidden(name, value) {
54
+ return `<input type="hidden" name="${name}" value="${escape(value)}">`;
55
+ }
56
+ export function browserHandler(provider) {
57
+ const canonical = new URL(provider.config.canonicalUri);
58
+ const secure = canonical.protocol === "https:";
59
+ const sessionName = secure ? "__Host-wardby-session" : "wardby-dev-session";
60
+ const loginName = secure ? "__Host-wardby-login" : "wardby-dev-login";
61
+ const limiter = new PostgresRateLimiter(provider.db, provider.credentials);
62
+ const cookie = (name, value, sameSite, age) => `${name}=${value}; Path=/; HttpOnly; SameSite=${sameSite}; Max-Age=${age}${secure ? "; Secure" : ""}`;
63
+ return async (req, res, url, body) => {
64
+ if (!paths.has(url.pathname))
65
+ return false;
66
+ res.setHeader("cache-control", "no-store");
67
+ res.setHeader("content-security-policy", "default-src 'none'; form-action 'self'; frame-ancestors 'none'; base-uri 'none'");
68
+ res.setHeader("x-content-type-options", "nosniff");
69
+ res.setHeader("referrer-policy", "no-referrer");
70
+ try {
71
+ const cookies = new Map();
72
+ for (const part of (req.headers.cookie ?? "").split(";")) {
73
+ const [key, ...value] = part.trim().split("=");
74
+ if (cookies.has(key))
75
+ throw new Error("duplicate cookie");
76
+ cookies.set(key, value.join("="));
77
+ }
78
+ for (const key of url.searchParams.keys())
79
+ if (url.searchParams.getAll(key).length !== 1)
80
+ throw new Error("duplicate query parameter");
81
+ const ip = req.socket.remoteAddress ?? "unknown";
82
+ await limiter.check("auth-ip", ip, 120);
83
+ if (req.method === "GET" && url.pathname === "/.well-known/oauth-authorization-server") {
84
+ json(res, 200, provider.asMetadata());
85
+ return true;
86
+ }
87
+ if (req.method === "POST" && url.pathname === "/register") {
88
+ await limiter.check("registration", ip, 10);
89
+ if (!body || typeof body !== "object" || body instanceof URLSearchParams || Array.isArray(body))
90
+ throw new Error("JSON object body required");
91
+ const p = body;
92
+ if (Object.keys(p).some((k) => ![
93
+ "redirect_uris",
94
+ "client_name",
95
+ "grant_types",
96
+ "token_endpoint_auth_method",
97
+ "response_types",
98
+ "scope",
99
+ "application_type",
100
+ ].includes(k)))
101
+ throw new Error("unsupported registration field");
102
+ if (p.response_types && JSON.stringify(p.response_types) !== '["code"]')
103
+ throw new Error("unsupported response_types");
104
+ // Claude Code sends both of the following. Neither is stored.
105
+ // RFC 7591 `scope`: scopes are requested and consented per
106
+ // authorization at /authorize, never fixed at registration.
107
+ if (p.scope !== undefined && (typeof p.scope !== "string" || p.scope.length > 512))
108
+ throw new Error("invalid scope");
109
+ // OIDC Dynamic Client Registration `application_type`: every client
110
+ // here is already a public PKCE client with validated redirect URIs.
111
+ if (p.application_type !== undefined && p.application_type !== "native" && p.application_type !== "web")
112
+ throw new Error("unsupported application_type");
113
+ const registered = await provider.registerClient({
114
+ redirectUris: p.redirect_uris,
115
+ clientName: p.client_name,
116
+ grantTypes: p.grant_types,
117
+ tokenEndpointAuthMethod: p.token_endpoint_auth_method,
118
+ });
119
+ json(res, 201, {
120
+ client_id: registered.clientId,
121
+ token_endpoint_auth_method: "none",
122
+ redirect_uris: p.redirect_uris,
123
+ });
124
+ return true;
125
+ }
126
+ if (req.method === "GET" && url.pathname === "/authorize") {
127
+ const p = url.searchParams;
128
+ if (p.has("subject") || p.get("response_type") !== "code")
129
+ throw new Error("invalid authorize parameters");
130
+ const result = await provider.handleAuthorize({
131
+ clientId: p.get("client_id") ?? "",
132
+ redirectUri: p.get("redirect_uri") ?? "",
133
+ codeChallenge: p.get("code_challenge") ?? "",
134
+ codeChallengeMethod: p.get("code_challenge_method") ?? "",
135
+ resource: p.get("resource") ?? provider.config.canonicalUri,
136
+ scope: p.get("scope") ?? "",
137
+ state: p.get("state") ?? undefined,
138
+ });
139
+ let target = "/login";
140
+ try {
141
+ await provider.sessions.get(cookies.get(sessionName) ?? "");
142
+ target = "/consent";
143
+ }
144
+ catch {
145
+ /* No valid browser session. */
146
+ }
147
+ redirect(res, target + "?interaction=" + encodeURIComponent(result.interactionId));
148
+ return true;
149
+ }
150
+ if (req.method === "GET" && url.pathname === "/login") {
151
+ const interaction = url.searchParams.get("interaction") ?? "";
152
+ const nonce = provider.credentials.create("rvb").token;
153
+ const challenge = await provider.sessions.challenge("login", null, nonce + ":" + interaction);
154
+ res.setHeader("set-cookie", cookie(loginName, nonce, "Strict", 600));
155
+ html(res, '<h1>Sign in to wardby</h1><form method="post" action="/login">' +
156
+ hidden("interaction", interaction) +
157
+ hidden("csrf", challenge) +
158
+ '<label>Login key <input type="password" name="login_key" required autocomplete="off"></label><button>Sign in</button></form>');
159
+ return true;
160
+ }
161
+ if (req.method === "POST" &&
162
+ ["/login", "/consent", "/logout"].includes(url.pathname) &&
163
+ req.headers.origin !== canonical.origin) {
164
+ json(res, 403, { error: "invalid_origin" });
165
+ return true;
166
+ }
167
+ if (req.method === "POST" && !(body instanceof URLSearchParams))
168
+ throw new Error("form body required");
169
+ const p = body instanceof URLSearchParams ? body : new URLSearchParams();
170
+ if (req.method === "POST" && url.pathname === "/login") {
171
+ if (p.has("subject"))
172
+ throw new Error("subject parameter not allowed");
173
+ const key = p.get("login_key") ?? "";
174
+ await limiter.check("login-ip", ip, 10);
175
+ await limiter.check("login-key", provider.credentials.id(key, "rvk") ?? "invalid", 5);
176
+ const interaction = p.get("interaction") ?? "";
177
+ const nonce = cookies.get(loginName);
178
+ if (!nonce)
179
+ throw new Error("missing login nonce");
180
+ const session = await provider.sessions.login(key, p.get("csrf") ?? "", nonce + ":" + interaction, cookies.get(sessionName));
181
+ res.setHeader("set-cookie", [
182
+ cookie(sessionName, session, "Lax", 30 * 86400),
183
+ cookie(loginName, "", "Strict", 0),
184
+ ]);
185
+ redirect(res, interaction ? "/consent?interaction=" + encodeURIComponent(interaction) : "/logout", req);
186
+ return true;
187
+ }
188
+ if (req.method === "GET" && url.pathname === "/consent") {
189
+ const id = url.searchParams.get("interaction") ?? "";
190
+ const { interaction, challenge } = await provider.consentPage(cookies.get(sessionName) ?? "", id);
191
+ const name = interaction.client.metadata.client_name;
192
+ html(res, "<h1>Authorize " +
193
+ escape(name) +
194
+ "</h1><p>Resource: " +
195
+ escape(interaction.resource) +
196
+ "</p><p>Scopes: " +
197
+ escape(interaction.requestedScope) +
198
+ '</p><form action="/consent" method="post">' +
199
+ hidden("interaction", id) +
200
+ hidden("csrf", challenge) +
201
+ '<button name="decision" value="approve">Approve</button><button name="decision" value="deny">Deny</button></form>');
202
+ return true;
203
+ }
204
+ if (req.method === "POST" && url.pathname === "/consent") {
205
+ if (!["approve", "deny"].includes(p.get("decision") ?? ""))
206
+ throw new Error("invalid consent decision");
207
+ redirect(res, await provider.consent(cookies.get(sessionName) ?? "", p.get("interaction") ?? "", p.get("csrf") ?? "", p.get("decision") === "approve"), req);
208
+ return true;
209
+ }
210
+ if (req.method === "GET" && url.pathname === "/logout") {
211
+ const session = await provider.sessions.get(cookies.get(sessionName) ?? "");
212
+ const challenge = await provider.sessions.challenge("logout", session.sessionId, "logout");
213
+ html(res, '<form action="/logout" method="post">' + hidden("csrf", challenge) + "<button>Sign out</button></form>");
214
+ return true;
215
+ }
216
+ if (req.method === "POST" && url.pathname === "/logout") {
217
+ await provider.sessions.logout(cookies.get(sessionName) ?? "", p.get("csrf") ?? "");
218
+ res.setHeader("set-cookie", cookie(sessionName, "", "Lax", 0));
219
+ redirect(res, "/login", req);
220
+ return true;
221
+ }
222
+ if (req.method === "POST" && ["/token", "/revoke"].includes(url.pathname)) {
223
+ if (req.headers.authorization)
224
+ throw new Error("Authorization header not allowed (public clients only)");
225
+ if (p.has("client_secret") || p.has("client_assertion"))
226
+ throw new Error("client credentials not allowed (public clients only)");
227
+ const clientId = p.get("client_id") ?? "";
228
+ if (url.pathname === "/revoke") {
229
+ await provider.revoke(p.get("token") ?? "", clientId);
230
+ json(res, 200, {});
231
+ return true;
232
+ }
233
+ const grantType = p.get("grant_type");
234
+ if (grantType !== "authorization_code" && grantType !== "refresh_token")
235
+ throw new Error("unsupported grant_type");
236
+ const common = { clientId, resource: p.get("resource") ?? undefined };
237
+ const token = await provider.handleToken(grantType === "refresh_token"
238
+ ? { ...common, grantType, refreshToken: p.get("refresh_token") ?? "" }
239
+ : {
240
+ ...common,
241
+ grantType,
242
+ code: p.get("code") ?? "",
243
+ codeVerifier: p.get("code_verifier") ?? "",
244
+ redirectUri: p.get("redirect_uri") ?? "",
245
+ });
246
+ json(res, 200, {
247
+ access_token: token.accessToken,
248
+ token_type: token.tokenType,
249
+ expires_in: token.expiresIn,
250
+ refresh_token: token.refreshToken,
251
+ scope: token.scope,
252
+ });
253
+ return true;
254
+ }
255
+ json(res, 405, { error: "method_not_allowed" });
256
+ }
257
+ catch (err) {
258
+ if (err instanceof RateLimitError) {
259
+ res.setHeader("retry-after", "60");
260
+ json(res, 429, { error: "rate_limited" });
261
+ }
262
+ else {
263
+ // The id is returned to the client and logged with the reason, so a
264
+ // rejection reported by a client can be traced. Only the reason is
265
+ // logged - never request values, which include codes and tokens.
266
+ const id = randomUUID();
267
+ authLog.warn({ id, path: url.pathname, reason: err instanceof Error ? err.message || "unspecified" : "unspecified" }, "self-hosted OAuth request rejected");
268
+ json(res, 400, { error: url.pathname === "/token" ? "invalid_grant" : "invalid_request", id });
269
+ }
270
+ }
271
+ return true;
272
+ };
273
+ }