thevoidforge 21.0.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 (328) hide show
  1. package/dist/scripts/vault-read.d.ts +11 -0
  2. package/dist/scripts/vault-read.js +89 -0
  3. package/dist/scripts/voidforge.d.ts +20 -0
  4. package/dist/scripts/voidforge.js +404 -0
  5. package/dist/tsconfig.tsbuildinfo +1 -0
  6. package/dist/wizard/api/auth.d.ts +5 -0
  7. package/dist/wizard/api/auth.js +133 -0
  8. package/dist/wizard/api/blueprint.d.ts +45 -0
  9. package/dist/wizard/api/blueprint.js +184 -0
  10. package/dist/wizard/api/cloud-providers.d.ts +16 -0
  11. package/dist/wizard/api/cloud-providers.js +363 -0
  12. package/dist/wizard/api/credentials.d.ts +1 -0
  13. package/dist/wizard/api/credentials.js +258 -0
  14. package/dist/wizard/api/danger-room.d.ts +18 -0
  15. package/dist/wizard/api/danger-room.js +401 -0
  16. package/dist/wizard/api/deploy.d.ts +4 -0
  17. package/dist/wizard/api/deploy.js +164 -0
  18. package/dist/wizard/api/prd.d.ts +1 -0
  19. package/dist/wizard/api/prd.js +363 -0
  20. package/dist/wizard/api/project.d.ts +1 -0
  21. package/dist/wizard/api/project.js +239 -0
  22. package/dist/wizard/api/projects.d.ts +6 -0
  23. package/dist/wizard/api/projects.js +648 -0
  24. package/dist/wizard/api/provision.d.ts +4 -0
  25. package/dist/wizard/api/provision.js +535 -0
  26. package/dist/wizard/api/terminal.d.ts +25 -0
  27. package/dist/wizard/api/terminal.js +241 -0
  28. package/dist/wizard/api/users.d.ts +6 -0
  29. package/dist/wizard/api/users.js +244 -0
  30. package/dist/wizard/api/war-room.d.ts +14 -0
  31. package/dist/wizard/api/war-room.js +45 -0
  32. package/dist/wizard/lib/ad-platform-core.d.ts +6 -0
  33. package/dist/wizard/lib/ad-platform-core.js +1 -0
  34. package/dist/wizard/lib/adapters/index.d.ts +52 -0
  35. package/dist/wizard/lib/adapters/index.js +38 -0
  36. package/dist/wizard/lib/adapters/sandbox-bank.d.ts +17 -0
  37. package/dist/wizard/lib/adapters/sandbox-bank.js +77 -0
  38. package/dist/wizard/lib/adapters/sandbox.d.ts +39 -0
  39. package/dist/wizard/lib/adapters/sandbox.js +174 -0
  40. package/dist/wizard/lib/adapters/stripe.d.ts +19 -0
  41. package/dist/wizard/lib/adapters/stripe.js +143 -0
  42. package/dist/wizard/lib/adapters/types.d.ts +9 -0
  43. package/dist/wizard/lib/adapters/types.js +10 -0
  44. package/dist/wizard/lib/agent-memory.d.ts +36 -0
  45. package/dist/wizard/lib/agent-memory.js +114 -0
  46. package/dist/wizard/lib/anomaly-detection.d.ts +59 -0
  47. package/dist/wizard/lib/anomaly-detection.js +122 -0
  48. package/dist/wizard/lib/anthropic.d.ts +21 -0
  49. package/dist/wizard/lib/anthropic.js +105 -0
  50. package/dist/wizard/lib/asset-scanner.d.ts +23 -0
  51. package/dist/wizard/lib/asset-scanner.js +107 -0
  52. package/dist/wizard/lib/audit-log.d.ts +23 -0
  53. package/dist/wizard/lib/audit-log.js +70 -0
  54. package/dist/wizard/lib/autonomy-controller.d.ts +76 -0
  55. package/dist/wizard/lib/autonomy-controller.js +183 -0
  56. package/dist/wizard/lib/body-parser.d.ts +2 -0
  57. package/dist/wizard/lib/body-parser.js +36 -0
  58. package/dist/wizard/lib/build-analytics.d.ts +39 -0
  59. package/dist/wizard/lib/build-analytics.js +91 -0
  60. package/dist/wizard/lib/build-step.d.ts +21 -0
  61. package/dist/wizard/lib/build-step.js +104 -0
  62. package/dist/wizard/lib/campaign-proposer.d.ts +39 -0
  63. package/dist/wizard/lib/campaign-proposer.js +180 -0
  64. package/dist/wizard/lib/campaign-state-machine.d.ts +63 -0
  65. package/dist/wizard/lib/campaign-state-machine.js +114 -0
  66. package/dist/wizard/lib/ci-generator.d.ts +14 -0
  67. package/dist/wizard/lib/ci-generator.js +187 -0
  68. package/dist/wizard/lib/claude-merge.d.ts +38 -0
  69. package/dist/wizard/lib/claude-merge.js +115 -0
  70. package/dist/wizard/lib/codegen/erd-gen.d.ts +16 -0
  71. package/dist/wizard/lib/codegen/erd-gen.js +98 -0
  72. package/dist/wizard/lib/codegen/integrations.d.ts +18 -0
  73. package/dist/wizard/lib/codegen/integrations.js +189 -0
  74. package/dist/wizard/lib/codegen/openapi-gen.d.ts +15 -0
  75. package/dist/wizard/lib/codegen/openapi-gen.js +79 -0
  76. package/dist/wizard/lib/codegen/prisma-types.d.ts +15 -0
  77. package/dist/wizard/lib/codegen/prisma-types.js +44 -0
  78. package/dist/wizard/lib/codegen/seed-gen.d.ts +16 -0
  79. package/dist/wizard/lib/codegen/seed-gen.js +128 -0
  80. package/dist/wizard/lib/compliance.d.ts +51 -0
  81. package/dist/wizard/lib/compliance.js +112 -0
  82. package/dist/wizard/lib/correlation-engine.d.ts +59 -0
  83. package/dist/wizard/lib/correlation-engine.js +151 -0
  84. package/dist/wizard/lib/cost-estimator.d.ts +22 -0
  85. package/dist/wizard/lib/cost-estimator.js +72 -0
  86. package/dist/wizard/lib/cost-tracker.d.ts +27 -0
  87. package/dist/wizard/lib/cost-tracker.js +37 -0
  88. package/dist/wizard/lib/daemon-aggregator.d.ts +71 -0
  89. package/dist/wizard/lib/daemon-aggregator.js +204 -0
  90. package/dist/wizard/lib/daemon-core.d.ts +6 -0
  91. package/dist/wizard/lib/daemon-core.js +5 -0
  92. package/dist/wizard/lib/dashboard-data.d.ts +132 -0
  93. package/dist/wizard/lib/dashboard-data.js +336 -0
  94. package/dist/wizard/lib/dashboard-ws.d.ts +25 -0
  95. package/dist/wizard/lib/dashboard-ws.js +91 -0
  96. package/dist/wizard/lib/deep-current.d.ts +77 -0
  97. package/dist/wizard/lib/deep-current.js +234 -0
  98. package/dist/wizard/lib/deploy-coordinator.d.ts +40 -0
  99. package/dist/wizard/lib/deploy-coordinator.js +86 -0
  100. package/dist/wizard/lib/deploy-log.d.ts +28 -0
  101. package/dist/wizard/lib/deploy-log.js +52 -0
  102. package/dist/wizard/lib/desktop-notify.d.ts +27 -0
  103. package/dist/wizard/lib/desktop-notify.js +98 -0
  104. package/dist/wizard/lib/dns/cloudflare-dns.d.ts +35 -0
  105. package/dist/wizard/lib/dns/cloudflare-dns.js +216 -0
  106. package/dist/wizard/lib/dns/cloudflare-registrar.d.ts +31 -0
  107. package/dist/wizard/lib/dns/cloudflare-registrar.js +148 -0
  108. package/dist/wizard/lib/dns/types.d.ts +22 -0
  109. package/dist/wizard/lib/dns/types.js +4 -0
  110. package/dist/wizard/lib/document-discovery.d.ts +33 -0
  111. package/dist/wizard/lib/document-discovery.js +145 -0
  112. package/dist/wizard/lib/env-validator.d.ts +14 -0
  113. package/dist/wizard/lib/env-validator.js +205 -0
  114. package/dist/wizard/lib/env-writer.d.ts +13 -0
  115. package/dist/wizard/lib/env-writer.js +26 -0
  116. package/dist/wizard/lib/exec.d.ts +30 -0
  117. package/dist/wizard/lib/exec.js +52 -0
  118. package/dist/wizard/lib/experiment.d.ts +70 -0
  119. package/dist/wizard/lib/experiment.js +169 -0
  120. package/dist/wizard/lib/extensions.d.ts +20 -0
  121. package/dist/wizard/lib/extensions.js +183 -0
  122. package/dist/wizard/lib/financial/adapter-factory.d.ts +47 -0
  123. package/dist/wizard/lib/financial/adapter-factory.js +225 -0
  124. package/dist/wizard/lib/financial/billing/base.d.ts +6 -0
  125. package/dist/wizard/lib/financial/billing/base.js +1 -0
  126. package/dist/wizard/lib/financial/billing/google-billing.d.ts +56 -0
  127. package/dist/wizard/lib/financial/billing/google-billing.js +298 -0
  128. package/dist/wizard/lib/financial/billing/meta-billing.d.ts +54 -0
  129. package/dist/wizard/lib/financial/billing/meta-billing.js +243 -0
  130. package/dist/wizard/lib/financial/billing/tiktok-billing.d.ts +54 -0
  131. package/dist/wizard/lib/financial/billing/tiktok-billing.js +260 -0
  132. package/dist/wizard/lib/financial/campaign/base.d.ts +13 -0
  133. package/dist/wizard/lib/financial/campaign/base.js +1 -0
  134. package/dist/wizard/lib/financial/campaign/google-campaign.d.ts +42 -0
  135. package/dist/wizard/lib/financial/campaign/google-campaign.js +388 -0
  136. package/dist/wizard/lib/financial/campaign/meta-campaign.d.ts +41 -0
  137. package/dist/wizard/lib/financial/campaign/meta-campaign.js +311 -0
  138. package/dist/wizard/lib/financial/campaign/sandbox-campaign.d.ts +45 -0
  139. package/dist/wizard/lib/financial/campaign/sandbox-campaign.js +261 -0
  140. package/dist/wizard/lib/financial/campaign/tiktok-campaign.d.ts +40 -0
  141. package/dist/wizard/lib/financial/campaign/tiktok-campaign.js +350 -0
  142. package/dist/wizard/lib/financial/funding-auto.d.ts +44 -0
  143. package/dist/wizard/lib/financial/funding-auto.js +52 -0
  144. package/dist/wizard/lib/financial/funding-policy.d.ts +60 -0
  145. package/dist/wizard/lib/financial/funding-policy.js +179 -0
  146. package/dist/wizard/lib/financial/platform-planner.d.ts +47 -0
  147. package/dist/wizard/lib/financial/platform-planner.js +134 -0
  148. package/dist/wizard/lib/financial/reconciliation-engine.d.ts +78 -0
  149. package/dist/wizard/lib/financial/reconciliation-engine.js +193 -0
  150. package/dist/wizard/lib/financial/registry.d.ts +22 -0
  151. package/dist/wizard/lib/financial/registry.js +26 -0
  152. package/dist/wizard/lib/financial/reporting.d.ts +96 -0
  153. package/dist/wizard/lib/financial/reporting.js +198 -0
  154. package/dist/wizard/lib/financial/stablecoin/base.d.ts +6 -0
  155. package/dist/wizard/lib/financial/stablecoin/base.js +1 -0
  156. package/dist/wizard/lib/financial/stablecoin/circle.d.ts +54 -0
  157. package/dist/wizard/lib/financial/stablecoin/circle.js +367 -0
  158. package/dist/wizard/lib/financial/stablecoin/mercury.d.ts +24 -0
  159. package/dist/wizard/lib/financial/stablecoin/mercury.js +171 -0
  160. package/dist/wizard/lib/financial/stablecoin/sandbox-stablecoin.d.ts +47 -0
  161. package/dist/wizard/lib/financial/stablecoin/sandbox-stablecoin.js +202 -0
  162. package/dist/wizard/lib/financial/treasury-planner.d.ts +52 -0
  163. package/dist/wizard/lib/financial/treasury-planner.js +128 -0
  164. package/dist/wizard/lib/financial-core.d.ts +6 -0
  165. package/dist/wizard/lib/financial-core.js +5 -0
  166. package/dist/wizard/lib/financial-vault.d.ts +34 -0
  167. package/dist/wizard/lib/financial-vault.js +199 -0
  168. package/dist/wizard/lib/frontmatter.d.ts +30 -0
  169. package/dist/wizard/lib/frontmatter.js +96 -0
  170. package/dist/wizard/lib/gap-analysis.d.ts +37 -0
  171. package/dist/wizard/lib/gap-analysis.js +218 -0
  172. package/dist/wizard/lib/github.d.ts +22 -0
  173. package/dist/wizard/lib/github.js +261 -0
  174. package/dist/wizard/lib/headless-deploy.d.ts +14 -0
  175. package/dist/wizard/lib/headless-deploy.js +452 -0
  176. package/dist/wizard/lib/health-monitor.d.ts +15 -0
  177. package/dist/wizard/lib/health-monitor.js +91 -0
  178. package/dist/wizard/lib/health-poller.d.ts +9 -0
  179. package/dist/wizard/lib/health-poller.js +123 -0
  180. package/dist/wizard/lib/heartbeat.d.ts +15 -0
  181. package/dist/wizard/lib/heartbeat.js +827 -0
  182. package/dist/wizard/lib/http-helpers.d.ts +9 -0
  183. package/dist/wizard/lib/http-helpers.js +24 -0
  184. package/dist/wizard/lib/image-gen.d.ts +56 -0
  185. package/dist/wizard/lib/image-gen.js +159 -0
  186. package/dist/wizard/lib/instance-sizing.d.ts +26 -0
  187. package/dist/wizard/lib/instance-sizing.js +51 -0
  188. package/dist/wizard/lib/kongo/analytics.d.ts +29 -0
  189. package/dist/wizard/lib/kongo/analytics.js +179 -0
  190. package/dist/wizard/lib/kongo/campaigns.d.ts +52 -0
  191. package/dist/wizard/lib/kongo/campaigns.js +91 -0
  192. package/dist/wizard/lib/kongo/client.d.ts +58 -0
  193. package/dist/wizard/lib/kongo/client.js +221 -0
  194. package/dist/wizard/lib/kongo/jobs.d.ts +57 -0
  195. package/dist/wizard/lib/kongo/jobs.js +122 -0
  196. package/dist/wizard/lib/kongo/pages.d.ts +60 -0
  197. package/dist/wizard/lib/kongo/pages.js +150 -0
  198. package/dist/wizard/lib/kongo/provisioner.d.ts +64 -0
  199. package/dist/wizard/lib/kongo/provisioner.js +116 -0
  200. package/dist/wizard/lib/kongo/seed.d.ts +49 -0
  201. package/dist/wizard/lib/kongo/seed.js +237 -0
  202. package/dist/wizard/lib/kongo/types.d.ts +323 -0
  203. package/dist/wizard/lib/kongo/types.js +11 -0
  204. package/dist/wizard/lib/kongo/variants.d.ts +57 -0
  205. package/dist/wizard/lib/kongo/variants.js +88 -0
  206. package/dist/wizard/lib/kongo/webhooks.d.ts +41 -0
  207. package/dist/wizard/lib/kongo/webhooks.js +112 -0
  208. package/dist/wizard/lib/marker.d.ts +28 -0
  209. package/dist/wizard/lib/marker.js +79 -0
  210. package/dist/wizard/lib/migrator.d.ts +35 -0
  211. package/dist/wizard/lib/migrator.js +190 -0
  212. package/dist/wizard/lib/natural-language-deploy.d.ts +30 -0
  213. package/dist/wizard/lib/natural-language-deploy.js +186 -0
  214. package/dist/wizard/lib/network.d.ts +22 -0
  215. package/dist/wizard/lib/network.js +72 -0
  216. package/dist/wizard/lib/oauth-core.d.ts +6 -0
  217. package/dist/wizard/lib/oauth-core.js +5 -0
  218. package/dist/wizard/lib/open-browser.d.ts +1 -0
  219. package/dist/wizard/lib/open-browser.js +26 -0
  220. package/dist/wizard/lib/patterns/ad-billing-adapter.d.ts +209 -0
  221. package/dist/wizard/lib/patterns/ad-billing-adapter.js +269 -0
  222. package/dist/wizard/lib/patterns/ad-platform-adapter.d.ts +200 -0
  223. package/dist/wizard/lib/patterns/ad-platform-adapter.js +212 -0
  224. package/dist/wizard/lib/patterns/daemon-process.d.ts +88 -0
  225. package/dist/wizard/lib/patterns/daemon-process.js +271 -0
  226. package/dist/wizard/lib/patterns/financial-transaction.d.ts +161 -0
  227. package/dist/wizard/lib/patterns/financial-transaction.js +132 -0
  228. package/dist/wizard/lib/patterns/funding-plan.d.ts +136 -0
  229. package/dist/wizard/lib/patterns/funding-plan.js +200 -0
  230. package/dist/wizard/lib/patterns/oauth-token-lifecycle.d.ts +94 -0
  231. package/dist/wizard/lib/patterns/oauth-token-lifecycle.js +139 -0
  232. package/dist/wizard/lib/patterns/outbound-rate-limiter.d.ts +67 -0
  233. package/dist/wizard/lib/patterns/outbound-rate-limiter.js +216 -0
  234. package/dist/wizard/lib/patterns/revenue-source-adapter.d.ts +96 -0
  235. package/dist/wizard/lib/patterns/revenue-source-adapter.js +182 -0
  236. package/dist/wizard/lib/patterns/stablecoin-adapter.d.ts +218 -0
  237. package/dist/wizard/lib/patterns/stablecoin-adapter.js +264 -0
  238. package/dist/wizard/lib/prd-validator.d.ts +39 -0
  239. package/dist/wizard/lib/prd-validator.js +137 -0
  240. package/dist/wizard/lib/project-init.d.ts +24 -0
  241. package/dist/wizard/lib/project-init.js +193 -0
  242. package/dist/wizard/lib/project-registry.d.ts +86 -0
  243. package/dist/wizard/lib/project-registry.js +359 -0
  244. package/dist/wizard/lib/provision-manifest.d.ts +44 -0
  245. package/dist/wizard/lib/provision-manifest.js +164 -0
  246. package/dist/wizard/lib/provisioner-registry.d.ts +15 -0
  247. package/dist/wizard/lib/provisioner-registry.js +34 -0
  248. package/dist/wizard/lib/provisioners/aws-vps.d.ts +6 -0
  249. package/dist/wizard/lib/provisioners/aws-vps.js +643 -0
  250. package/dist/wizard/lib/provisioners/cloudflare.d.ts +6 -0
  251. package/dist/wizard/lib/provisioners/cloudflare.js +300 -0
  252. package/dist/wizard/lib/provisioners/docker.d.ts +6 -0
  253. package/dist/wizard/lib/provisioners/docker.js +75 -0
  254. package/dist/wizard/lib/provisioners/http-client.d.ts +20 -0
  255. package/dist/wizard/lib/provisioners/http-client.js +79 -0
  256. package/dist/wizard/lib/provisioners/railway.d.ts +6 -0
  257. package/dist/wizard/lib/provisioners/railway.js +413 -0
  258. package/dist/wizard/lib/provisioners/scripts/caddyfile.d.ts +10 -0
  259. package/dist/wizard/lib/provisioners/scripts/caddyfile.js +54 -0
  260. package/dist/wizard/lib/provisioners/scripts/deploy-vps.d.ts +10 -0
  261. package/dist/wizard/lib/provisioners/scripts/deploy-vps.js +112 -0
  262. package/dist/wizard/lib/provisioners/scripts/docker-compose.d.ts +11 -0
  263. package/dist/wizard/lib/provisioners/scripts/docker-compose.js +91 -0
  264. package/dist/wizard/lib/provisioners/scripts/dockerfile.d.ts +5 -0
  265. package/dist/wizard/lib/provisioners/scripts/dockerfile.js +185 -0
  266. package/dist/wizard/lib/provisioners/scripts/ecosystem-config.d.ts +10 -0
  267. package/dist/wizard/lib/provisioners/scripts/ecosystem-config.js +36 -0
  268. package/dist/wizard/lib/provisioners/scripts/provision-vps.d.ts +14 -0
  269. package/dist/wizard/lib/provisioners/scripts/provision-vps.js +202 -0
  270. package/dist/wizard/lib/provisioners/scripts/rollback-vps.d.ts +10 -0
  271. package/dist/wizard/lib/provisioners/scripts/rollback-vps.js +67 -0
  272. package/dist/wizard/lib/provisioners/self-deploy.d.ts +41 -0
  273. package/dist/wizard/lib/provisioners/self-deploy.js +185 -0
  274. package/dist/wizard/lib/provisioners/static-s3.d.ts +6 -0
  275. package/dist/wizard/lib/provisioners/static-s3.js +235 -0
  276. package/dist/wizard/lib/provisioners/types.d.ts +40 -0
  277. package/dist/wizard/lib/provisioners/types.js +4 -0
  278. package/dist/wizard/lib/provisioners/vercel.d.ts +6 -0
  279. package/dist/wizard/lib/provisioners/vercel.js +287 -0
  280. package/dist/wizard/lib/pty-manager.d.ts +42 -0
  281. package/dist/wizard/lib/pty-manager.js +231 -0
  282. package/dist/wizard/lib/rate-limiter-core.d.ts +5 -0
  283. package/dist/wizard/lib/rate-limiter-core.js +5 -0
  284. package/dist/wizard/lib/reconciliation.d.ts +43 -0
  285. package/dist/wizard/lib/reconciliation.js +173 -0
  286. package/dist/wizard/lib/revenue-types.d.ts +5 -0
  287. package/dist/wizard/lib/revenue-types.js +1 -0
  288. package/dist/wizard/lib/route-optimizer.d.ts +28 -0
  289. package/dist/wizard/lib/route-optimizer.js +93 -0
  290. package/dist/wizard/lib/s3-deploy.d.ts +19 -0
  291. package/dist/wizard/lib/s3-deploy.js +156 -0
  292. package/dist/wizard/lib/safety-tiers.d.ts +76 -0
  293. package/dist/wizard/lib/safety-tiers.js +134 -0
  294. package/dist/wizard/lib/sentry-generator.d.ts +15 -0
  295. package/dist/wizard/lib/sentry-generator.js +116 -0
  296. package/dist/wizard/lib/server-config.d.ts +13 -0
  297. package/dist/wizard/lib/server-config.js +23 -0
  298. package/dist/wizard/lib/service-install.d.ts +18 -0
  299. package/dist/wizard/lib/service-install.js +182 -0
  300. package/dist/wizard/lib/site-scanner.d.ts +80 -0
  301. package/dist/wizard/lib/site-scanner.js +262 -0
  302. package/dist/wizard/lib/ssh-deploy.d.ts +25 -0
  303. package/dist/wizard/lib/ssh-deploy.js +225 -0
  304. package/dist/wizard/lib/templates.d.ts +24 -0
  305. package/dist/wizard/lib/templates.js +219 -0
  306. package/dist/wizard/lib/totp.d.ts +35 -0
  307. package/dist/wizard/lib/totp.js +276 -0
  308. package/dist/wizard/lib/tower-auth.d.ts +43 -0
  309. package/dist/wizard/lib/tower-auth.js +352 -0
  310. package/dist/wizard/lib/tower-rate-limit.d.ts +14 -0
  311. package/dist/wizard/lib/tower-rate-limit.js +61 -0
  312. package/dist/wizard/lib/tower-session.d.ts +28 -0
  313. package/dist/wizard/lib/tower-session.js +119 -0
  314. package/dist/wizard/lib/treasury-backup.d.ts +23 -0
  315. package/dist/wizard/lib/treasury-backup.js +126 -0
  316. package/dist/wizard/lib/treasury-heartbeat.d.ts +82 -0
  317. package/dist/wizard/lib/treasury-heartbeat.js +1104 -0
  318. package/dist/wizard/lib/updater.d.ts +29 -0
  319. package/dist/wizard/lib/updater.js +190 -0
  320. package/dist/wizard/lib/user-manager.d.ts +39 -0
  321. package/dist/wizard/lib/user-manager.js +182 -0
  322. package/dist/wizard/lib/vault.d.ts +26 -0
  323. package/dist/wizard/lib/vault.js +161 -0
  324. package/dist/wizard/router.d.ts +5 -0
  325. package/dist/wizard/router.js +15 -0
  326. package/dist/wizard/server.d.ts +18 -0
  327. package/dist/wizard/server.js +436 -0
  328. package/package.json +59 -0
@@ -0,0 +1,1104 @@
1
+ /**
2
+ * Treasury Heartbeat — heartbeat jobs, socket handlers, and circuit breakers
3
+ * for stablecoin funding operations.
4
+ *
5
+ * Wires the pure-logic modules (treasury-planner, funding-policy, reconciliation-engine)
6
+ * into the heartbeat daemon's job scheduler and socket API.
7
+ *
8
+ * Only activates when stablecoin funding is configured (funding-config.json.enc exists).
9
+ *
10
+ * PRD Reference: S10.4, S12, S13.2, S15, S16
11
+ * Agents: Dockson (treasury), Heartbeat daemon
12
+ */
13
+ import { join } from 'node:path';
14
+ import { existsSync } from 'node:fs';
15
+ import { readFile, appendFile, mkdir, stat, rename } from 'node:fs/promises';
16
+ import { randomUUID } from 'node:crypto';
17
+ import { TREASURY_DIR, atomicWrite } from './financial-core.js';
18
+ import { forecastRunway, generateFundingPlan, } from './financial/treasury-planner.js';
19
+ import { evaluatePolicy, aggregateDecisions } from './financial/funding-policy.js';
20
+ import { evaluateAutoFunding } from './financial/funding-auto.js';
21
+ import { reconcileThreeWay, shouldFreeze } from './financial/reconciliation-engine.js';
22
+ // ── Types ────────────────────────────────────────────
23
+ /** Funding config marker — the encrypted config file in treasury dir. */
24
+ const FUNDING_CONFIG_PATH = join(TREASURY_DIR, 'funding-config.json.enc');
25
+ const FUNDING_PLANS_LOG = join(TREASURY_DIR, 'funding-plans.jsonl');
26
+ const TRANSFERS_LOG = join(TREASURY_DIR, 'transfers.jsonl');
27
+ const RECONCILIATION_LOG = join(TREASURY_DIR, 'reconciliation.jsonl');
28
+ const PENDING_TRANSFERS_FILE = join(TREASURY_DIR, 'pending-transfers.json');
29
+ function defaultTreasuryState() {
30
+ return {
31
+ stablecoinBalanceCents: 0,
32
+ bankBalanceCents: 0,
33
+ pendingTransferCount: 0,
34
+ lastOfframpAt: null,
35
+ lastReconciliationAt: null,
36
+ runwayDays: 0,
37
+ fundingFrozen: false,
38
+ freezeReason: null,
39
+ consecutiveMismatches: 0,
40
+ consecutiveProviderFailures: 0,
41
+ lastCircuitBreakerCheck: null,
42
+ dailyMovementCents: 0,
43
+ dailyMovementDate: new Date().toISOString().slice(0, 10),
44
+ pendingObligationsCents: 0,
45
+ googleInvoiceDueSoon: false,
46
+ googleInvoiceCents: 0,
47
+ metaDebitFailed: false,
48
+ metaPaymentRisk: false,
49
+ metaForecast7DayCents: 0,
50
+ };
51
+ }
52
+ // ── State File Persistence ───────────────────────────
53
+ const TREASURY_STATE_FILE = join(TREASURY_DIR, 'treasury-state.json');
54
+ let treasuryState = defaultTreasuryState();
55
+ async function loadTreasuryState() {
56
+ try {
57
+ if (existsSync(TREASURY_STATE_FILE)) {
58
+ const raw = await readFile(TREASURY_STATE_FILE, 'utf-8');
59
+ treasuryState = { ...defaultTreasuryState(), ...JSON.parse(raw) };
60
+ }
61
+ }
62
+ catch {
63
+ treasuryState = defaultTreasuryState();
64
+ }
65
+ }
66
+ async function saveTreasuryState() {
67
+ await mkdir(TREASURY_DIR, { recursive: true });
68
+ await atomicWrite(TREASURY_STATE_FILE, JSON.stringify(treasuryState, null, 2));
69
+ }
70
+ // ── Configuration Check ──────────────────────────────
71
+ export function isStablecoinConfigured() {
72
+ return existsSync(FUNDING_CONFIG_PATH);
73
+ }
74
+ async function readPendingTransfers() {
75
+ try {
76
+ if (!existsSync(PENDING_TRANSFERS_FILE))
77
+ return [];
78
+ const raw = await readFile(PENDING_TRANSFERS_FILE, 'utf-8');
79
+ return JSON.parse(raw);
80
+ }
81
+ catch {
82
+ return [];
83
+ }
84
+ }
85
+ async function writePendingTransfers(transfers) {
86
+ await mkdir(TREASURY_DIR, { recursive: true });
87
+ await atomicWrite(PENDING_TRANSFERS_FILE, JSON.stringify(transfers, null, 2));
88
+ }
89
+ const WAL_FILE = join(TREASURY_DIR, 'pending-ops.jsonl');
90
+ const WAL_MAX_SIZE_BYTES = 10 * 1024 * 1024; // 10MB rotation threshold
91
+ const WAL_MAX_ROTATIONS = 7;
92
+ /** Rotate WAL file using 7-file rotation (same as audit-log pattern). */
93
+ async function rotateWalIfNeeded() {
94
+ try {
95
+ const stats = await stat(WAL_FILE);
96
+ if (stats.size >= WAL_MAX_SIZE_BYTES) {
97
+ for (let i = WAL_MAX_ROTATIONS - 1; i >= 1; i--) {
98
+ try {
99
+ await rename(WAL_FILE + '.' + i, WAL_FILE + '.' + (i + 1));
100
+ }
101
+ catch { /* file doesn't exist at this slot — skip */ }
102
+ }
103
+ await rename(WAL_FILE, WAL_FILE + '.1');
104
+ }
105
+ }
106
+ catch {
107
+ // File doesn't exist yet — that's fine
108
+ }
109
+ }
110
+ async function writeWalEntry(entry) {
111
+ await mkdir(TREASURY_DIR, { recursive: true });
112
+ await rotateWalIfNeeded();
113
+ await appendFile(WAL_FILE, JSON.stringify(entry) + '\n', 'utf-8');
114
+ }
115
+ /** Read all pending WAL entries for recovery. */
116
+ async function readPendingWalEntries() {
117
+ try {
118
+ if (!existsSync(WAL_FILE))
119
+ return [];
120
+ const content = await readFile(WAL_FILE, 'utf-8');
121
+ const lines = content.trim().split('\n').filter(Boolean);
122
+ const entries = [];
123
+ for (const line of lines) {
124
+ try {
125
+ const entry = JSON.parse(line);
126
+ if (entry.status === 'pending') {
127
+ entries.push(entry);
128
+ }
129
+ }
130
+ catch { /* skip malformed */ }
131
+ }
132
+ return entries;
133
+ }
134
+ catch {
135
+ return [];
136
+ }
137
+ }
138
+ /** Complete a WAL entry by writing a completion record. */
139
+ async function completeWalEntry(intentId, operation, result, error) {
140
+ await writeWalEntry({
141
+ intentId,
142
+ operation,
143
+ params: {},
144
+ status: result,
145
+ createdAt: new Date().toISOString(),
146
+ completedAt: new Date().toISOString(),
147
+ error,
148
+ });
149
+ }
150
+ /** WAL recovery: check pending ops against adapter and resolve them. */
151
+ async function recoverPendingOps(vaultKey, logger) {
152
+ const pendingOps = await readPendingWalEntries();
153
+ if (pendingOps.length === 0)
154
+ return;
155
+ logger.log(`WAL recovery: found ${pendingOps.length} pending operation(s)`);
156
+ for (const op of pendingOps) {
157
+ try {
158
+ if (op.operation === 'offramp') {
159
+ const params = op.params;
160
+ const providerTransferId = params?.providerTransferId;
161
+ if (!providerTransferId) {
162
+ // No provider transfer ID means the offramp never initiated — mark failed
163
+ await completeWalEntry(op.intentId, op.operation, 'failed', 'No provider transfer ID — offramp never initiated');
164
+ logger.log(`WAL recovery: ${op.intentId} marked failed (no provider transfer ID)`);
165
+ continue;
166
+ }
167
+ const { getStablecoinAdapter } = await import('./financial/adapter-factory.js');
168
+ const adapter = await getStablecoinAdapter(vaultKey, logger);
169
+ const status = await adapter.getTransferStatus(providerTransferId);
170
+ if (status.status === 'completed') {
171
+ await completeWalEntry(op.intentId, op.operation, 'completed');
172
+ logger.log(`WAL recovery: ${op.intentId} confirmed completed`);
173
+ }
174
+ else if (status.status === 'failed') {
175
+ await completeWalEntry(op.intentId, op.operation, 'failed', 'Transfer failed at provider');
176
+ logger.log(`WAL recovery: ${op.intentId} confirmed failed`);
177
+ }
178
+ else {
179
+ logger.log(`WAL recovery: ${op.intentId} still ${status.status} — will retry next startup`);
180
+ }
181
+ }
182
+ else if (op.operation === 'auto-funding-execute') {
183
+ const params = op.params;
184
+ const providerTransferId = params?.providerTransferId;
185
+ if (!providerTransferId) {
186
+ await completeWalEntry(op.intentId, op.operation, 'failed', 'No provider transfer ID');
187
+ logger.log(`WAL recovery: ${op.intentId} marked failed (no provider transfer ID)`);
188
+ continue;
189
+ }
190
+ const { getStablecoinAdapter } = await import('./financial/adapter-factory.js');
191
+ const adapter = await getStablecoinAdapter(vaultKey, logger);
192
+ const status = await adapter.getTransferStatus(providerTransferId);
193
+ if (status.status === 'completed') {
194
+ await completeWalEntry(op.intentId, op.operation, 'completed');
195
+ logger.log(`WAL recovery: ${op.intentId} confirmed completed`);
196
+ }
197
+ else if (status.status === 'failed') {
198
+ await completeWalEntry(op.intentId, op.operation, 'failed', 'Transfer failed at provider');
199
+ logger.log(`WAL recovery: ${op.intentId} confirmed failed`);
200
+ }
201
+ else {
202
+ logger.log(`WAL recovery: ${op.intentId} still ${status.status} — will retry next startup`);
203
+ }
204
+ }
205
+ else {
206
+ // Unknown op type — mark failed to avoid infinite recovery loop
207
+ await completeWalEntry(op.intentId, op.operation, 'failed', `Unknown operation type: ${op.operation}`);
208
+ logger.log(`WAL recovery: ${op.intentId} marked failed (unknown operation: ${op.operation})`);
209
+ }
210
+ }
211
+ catch (err) {
212
+ const msg = err instanceof Error ? err.message : 'Unknown error';
213
+ logger.log(`WAL recovery: ${op.intentId} check failed: ${msg}`);
214
+ }
215
+ }
216
+ }
217
+ export function evaluateCircuitBreakers(state) {
218
+ const reasons = [];
219
+ // CB-1: Provider unavailable for 3 consecutive polls
220
+ if (state.consecutiveProviderFailures >= 3) {
221
+ reasons.push(`Stablecoin provider unavailable for ${state.consecutiveProviderFailures} consecutive polls — freeze funding`);
222
+ }
223
+ // CB-2: Off-ramp pending beyond SLA window (24 hours)
224
+ // Checked via pending transfers — caller handles this with transfer data
225
+ // CB-3: Reconciliation mismatch for 2 consecutive closes
226
+ if (state.consecutiveMismatches >= 2) {
227
+ reasons.push(`Reconciliation mismatch for ${state.consecutiveMismatches} consecutive closes — freeze funding`);
228
+ }
229
+ // CB-6: Max daily treasury movement exceeded ($50,000 default)
230
+ const MAX_DAILY_MOVEMENT_CENTS = 5_000_000; // $50,000
231
+ if (state.dailyMovementCents > MAX_DAILY_MOVEMENT_CENTS) {
232
+ reasons.push(`Daily treasury movement $${(state.dailyMovementCents / 100).toFixed(2)} exceeds max $${(MAX_DAILY_MOVEMENT_CENTS / 100).toFixed(2)} — freeze funding`);
233
+ }
234
+ return {
235
+ shouldFreeze: reasons.length > 0,
236
+ reasons,
237
+ };
238
+ }
239
+ /** Evaluate CB-2 specifically for pending transfers beyond SLA. */
240
+ export function evaluateTransferSlaBreaker(transfers, slaHours = 24) {
241
+ const reasons = [];
242
+ const slaMs = slaHours * 60 * 60 * 1000;
243
+ const now = Date.now();
244
+ for (const t of transfers) {
245
+ if (t.status !== 'pending' && t.status !== 'processing')
246
+ continue;
247
+ const age = now - new Date(t.initiatedAt).getTime();
248
+ if (age > slaMs) {
249
+ reasons.push(`Transfer ${t.id} pending for ${Math.round(age / (60 * 60 * 1000))}h — exceeds ${slaHours}h SLA — freeze funding`);
250
+ }
251
+ }
252
+ return {
253
+ shouldFreeze: reasons.length > 0,
254
+ reasons,
255
+ };
256
+ }
257
+ /** Evaluate CB-4 and CB-5 from billing adapter state. */
258
+ export function evaluateBillingBreakers(opts) {
259
+ const reasons = [];
260
+ // CB-4: Google invoice due soon + insufficient fiat
261
+ if (opts.googleInvoiceDueSoon) {
262
+ const availableForInvoice = opts.bankBalanceCents - opts.minimumBufferCents;
263
+ if (availableForInvoice < opts.googleInvoiceCents) {
264
+ reasons.push(`Google invoice $${(opts.googleInvoiceCents / 100).toFixed(2)} due soon but only ` +
265
+ `$${(availableForInvoice / 100).toFixed(2)} available — freeze non-essential funding`);
266
+ }
267
+ }
268
+ // CB-5: Meta debit fails or payment-risk state
269
+ if (opts.metaDebitFailed) {
270
+ reasons.push('Meta direct debit failed — freeze funding until bank balance confirmed');
271
+ }
272
+ if (opts.metaPaymentRisk) {
273
+ reasons.push('Meta account in payment-risk state — freeze funding');
274
+ }
275
+ return {
276
+ shouldFreeze: reasons.length > 0,
277
+ reasons,
278
+ };
279
+ }
280
+ /** Read all funding plans from the JSONL log. */
281
+ async function readFundingPlans() {
282
+ try {
283
+ if (!existsSync(FUNDING_PLANS_LOG))
284
+ return [];
285
+ const content = await readFile(FUNDING_PLANS_LOG, 'utf-8');
286
+ const lines = content.trim().split('\n').filter(Boolean);
287
+ const plans = [];
288
+ for (const line of lines) {
289
+ try {
290
+ plans.push(JSON.parse(line));
291
+ }
292
+ catch { /* skip malformed */ }
293
+ }
294
+ return plans;
295
+ }
296
+ catch {
297
+ return [];
298
+ }
299
+ }
300
+ /** Rewrite the full funding plans log (after status transitions). */
301
+ async function writeFundingPlans(plans) {
302
+ await mkdir(TREASURY_DIR, { recursive: true });
303
+ const content = plans.map(p => JSON.stringify(p)).join('\n') + '\n';
304
+ await atomicWrite(FUNDING_PLANS_LOG, content);
305
+ }
306
+ // ── Auto-Funding Plan Executor ───────────────────────
307
+ /** Execute approved funding plans: initiate offramp, track pending, write WAL. */
308
+ async function executeApprovedPlans(vaultKey, logger, triggerFreeze) {
309
+ const plans = await readFundingPlans();
310
+ const approvedPlans = plans.filter(p => p.status === 'APPROVED');
311
+ if (approvedPlans.length === 0)
312
+ return;
313
+ logger.log(`Executing ${approvedPlans.length} approved funding plan(s)`);
314
+ const { getStablecoinAdapter } = await import('./financial/adapter-factory.js');
315
+ const adapter = await getStablecoinAdapter(vaultKey, logger);
316
+ for (const plan of approvedPlans) {
317
+ const intentId = randomUUID();
318
+ // Write WAL entry before execution (ADR-3)
319
+ await writeWalEntry({
320
+ intentId,
321
+ operation: 'auto-funding-execute',
322
+ params: { planId: plan.id, requiredCents: plan.requiredCents },
323
+ status: 'pending',
324
+ createdAt: new Date().toISOString(),
325
+ });
326
+ try {
327
+ const planRef = {
328
+ id: plan.id ?? randomUUID(),
329
+ sourceFundingId: plan.sourceFundingId ?? 'default-source',
330
+ destinationBankId: plan.destinationBankId ?? 'default-bank',
331
+ requiredCents: (plan.requiredCents ?? 0),
332
+ idempotencyKey: plan.idempotencyKey ?? randomUUID(),
333
+ };
334
+ const transfer = await adapter.initiateOfframp(planRef, plan.hash ?? '');
335
+ // Transition plan to PENDING_SETTLEMENT
336
+ plan.status = 'PENDING_SETTLEMENT';
337
+ plan.updatedAt = new Date().toISOString();
338
+ await writeFundingPlans(plans);
339
+ // Log the transfer
340
+ await mkdir(TREASURY_DIR, { recursive: true });
341
+ await appendFile(TRANSFERS_LOG, JSON.stringify(transfer) + '\n', 'utf-8');
342
+ // Track pending transfer
343
+ const pending = await readPendingTransfers();
344
+ pending.push({
345
+ id: transfer.id,
346
+ fundingPlanId: plan.id,
347
+ providerTransferId: transfer.providerTransferId,
348
+ amountCents: transfer.amountCents,
349
+ status: 'pending',
350
+ initiatedAt: transfer.initiatedAt,
351
+ lastPolledAt: transfer.initiatedAt,
352
+ });
353
+ await writePendingTransfers(pending);
354
+ // Update treasury state
355
+ treasuryState.pendingTransferCount += 1;
356
+ treasuryState.lastOfframpAt = new Date().toISOString();
357
+ // Track daily movement for CB-6
358
+ const today = new Date().toISOString().slice(0, 10);
359
+ if (treasuryState.dailyMovementDate !== today) {
360
+ treasuryState.dailyMovementCents = 0;
361
+ treasuryState.dailyMovementDate = today;
362
+ }
363
+ treasuryState.dailyMovementCents += (plan.requiredCents ?? 0);
364
+ // Complete WAL
365
+ await writeWalEntry({
366
+ intentId,
367
+ operation: 'auto-funding-execute',
368
+ params: { planId: plan.id, transferId: transfer.id, providerTransferId: transfer.providerTransferId },
369
+ status: 'completed',
370
+ createdAt: new Date().toISOString(),
371
+ completedAt: new Date().toISOString(),
372
+ });
373
+ logger.log(`Auto-funding executed: plan ${plan.id}, ` +
374
+ `$${((plan.requiredCents ?? 0) / 100).toFixed(2)} ` +
375
+ `via ${transfer.provider} (transfer ${transfer.id})`);
376
+ // CB-6: Check daily movement limits
377
+ const cb = evaluateCircuitBreakers(treasuryState);
378
+ if (cb.shouldFreeze && !treasuryState.fundingFrozen) {
379
+ await triggerFreeze(cb.reasons.join('; '));
380
+ }
381
+ }
382
+ catch (err) {
383
+ const msg = err instanceof Error ? err.message : 'Unknown error';
384
+ logger.log(`Auto-funding execution failed for plan ${plan.id}: ${msg}`);
385
+ // Mark plan as FAILED
386
+ plan.status = 'FAILED';
387
+ plan.updatedAt = new Date().toISOString();
388
+ await writeFundingPlans(plans);
389
+ // Mark WAL as failed
390
+ await writeWalEntry({
391
+ intentId,
392
+ operation: 'auto-funding-execute',
393
+ params: { planId: plan.id },
394
+ status: 'failed',
395
+ createdAt: new Date().toISOString(),
396
+ error: msg,
397
+ });
398
+ }
399
+ }
400
+ await saveTreasuryState();
401
+ }
402
+ // ── Treasury Heartbeat Jobs (PRD S10.4) ──────────────
403
+ export function registerTreasuryJobs(scheduler, logger, writeCurrentState, triggerFreeze, vaultKey) {
404
+ if (!isStablecoinConfigured()) {
405
+ logger.log('Treasury jobs skipped — stablecoin funding not configured');
406
+ return;
407
+ }
408
+ // Load persisted treasury state on registration
409
+ void loadTreasuryState().catch(() => {
410
+ /* state will use defaults */
411
+ });
412
+ // WAL recovery: resolve pending operations from prior crash/restart
413
+ void recoverPendingOps(vaultKey, logger).catch((err) => {
414
+ const msg = err instanceof Error ? err.message : 'Unknown error';
415
+ logger.log(`WAL recovery failed: ${msg}`);
416
+ });
417
+ // Job 1: stablecoin-balance-check (hourly)
418
+ scheduler.add('stablecoin-balance-check', 3_600_000, async () => {
419
+ logger.log('Stablecoin balance check starting');
420
+ try {
421
+ const { getStablecoinAdapter } = await import('./financial/adapter-factory.js');
422
+ const adapter = await getStablecoinAdapter(vaultKey, logger);
423
+ const balances = await adapter.getBalances();
424
+ treasuryState.stablecoinBalanceCents = balances.totalStablecoinCents;
425
+ treasuryState.consecutiveProviderFailures = 0;
426
+ logger.log(`Stablecoin balance: $${(balances.totalStablecoinCents / 100).toFixed(2)} ` +
427
+ `(${balances.stablecoin.length} wallets)`);
428
+ await saveTreasuryState();
429
+ await writeCurrentState();
430
+ }
431
+ catch (err) {
432
+ treasuryState.consecutiveProviderFailures += 1;
433
+ const msg = err instanceof Error ? err.message : 'Unknown error';
434
+ logger.log(`Stablecoin balance check failed (${treasuryState.consecutiveProviderFailures} consecutive): ${msg}`);
435
+ // CB-1: Provider unavailable for 3 consecutive polls
436
+ const cb = evaluateCircuitBreakers(treasuryState);
437
+ if (cb.shouldFreeze && !treasuryState.fundingFrozen) {
438
+ await triggerFreeze(cb.reasons.join('; '));
439
+ }
440
+ await saveTreasuryState();
441
+ }
442
+ });
443
+ // Job 2: offramp-status-poll (15 min)
444
+ scheduler.add('offramp-status-poll', 900_000, async () => {
445
+ const pending = await readPendingTransfers();
446
+ const activePending = pending.filter(t => t.status === 'pending' || t.status === 'processing');
447
+ if (activePending.length === 0)
448
+ return;
449
+ logger.log(`Polling ${activePending.length} pending off-ramp transfers`);
450
+ try {
451
+ const { getStablecoinAdapter } = await import('./financial/adapter-factory.js');
452
+ const adapter = await getStablecoinAdapter(vaultKey, logger);
453
+ let updated = false;
454
+ for (const transfer of activePending) {
455
+ try {
456
+ const status = await adapter.getTransferStatus(transfer.providerTransferId);
457
+ if (status.status !== transfer.status) {
458
+ transfer.status = status.status;
459
+ transfer.lastPolledAt = new Date().toISOString();
460
+ updated = true;
461
+ logger.log(`Transfer ${transfer.id} status: ${status.status} ` +
462
+ `($${(status.amountCents / 100).toFixed(2)})`);
463
+ if (status.status === 'completed') {
464
+ treasuryState.pendingTransferCount = Math.max(0, treasuryState.pendingTransferCount - 1);
465
+ // Mark matching funding plan as SETTLED
466
+ if (transfer.fundingPlanId) {
467
+ try {
468
+ const plans = await readFundingPlans();
469
+ const plan = plans.find(p => p.id === transfer.fundingPlanId);
470
+ if (plan && (plan.status === 'PENDING_SETTLEMENT' || plan.status === 'APPROVED')) {
471
+ plan.status = 'SETTLED';
472
+ plan.updatedAt = new Date().toISOString();
473
+ await writeFundingPlans(plans);
474
+ logger.log(`Funding plan ${plan.id} marked SETTLED`);
475
+ }
476
+ }
477
+ catch (planErr) {
478
+ const planMsg = planErr instanceof Error ? planErr.message : 'Unknown error';
479
+ logger.log(`Failed to update funding plan status: ${planMsg}`);
480
+ }
481
+ }
482
+ }
483
+ }
484
+ }
485
+ catch (err) {
486
+ const msg = err instanceof Error ? err.message : 'Unknown error';
487
+ logger.log(`Transfer ${transfer.id} poll failed: ${msg}`);
488
+ }
489
+ }
490
+ if (updated) {
491
+ await writePendingTransfers(pending);
492
+ await saveTreasuryState();
493
+ }
494
+ // CB-2: Check for SLA breach on pending transfers
495
+ const slaCheck = evaluateTransferSlaBreaker(pending);
496
+ if (slaCheck.shouldFreeze && !treasuryState.fundingFrozen) {
497
+ await triggerFreeze(slaCheck.reasons.join('; '));
498
+ }
499
+ }
500
+ catch (err) {
501
+ const msg = err instanceof Error ? err.message : 'Unknown error';
502
+ logger.log(`Off-ramp status poll error: ${msg}`);
503
+ }
504
+ });
505
+ // Job 3: bank-settlement-monitor (hourly)
506
+ scheduler.add('bank-settlement-monitor', 3_600_000, async () => {
507
+ logger.log('Bank settlement monitor starting');
508
+ try {
509
+ const { getBankAdapter } = await import('./financial/adapter-factory.js');
510
+ const bankAdapter = await getBankAdapter(vaultKey, logger);
511
+ if (!bankAdapter.getBalance) {
512
+ logger.log('Bank adapter does not support getBalance — skipping');
513
+ await writeCurrentState();
514
+ return;
515
+ }
516
+ const balance = await bankAdapter.getBalance();
517
+ treasuryState.bankBalanceCents = balance.available;
518
+ logger.log(`Bank balance: $${(balance.available / 100).toFixed(2)} available, ` +
519
+ `$${(balance.pending / 100).toFixed(2)} pending`);
520
+ // Check for newly settled transfers
521
+ const pending = await readPendingTransfers();
522
+ const recentlyCompleted = pending.filter(t => t.status === 'completed');
523
+ if (recentlyCompleted.length > 0) {
524
+ logger.log(`${recentlyCompleted.length} transfer(s) settled since last check`);
525
+ }
526
+ await saveTreasuryState();
527
+ await writeCurrentState();
528
+ }
529
+ catch (err) {
530
+ const msg = err instanceof Error ? err.message : 'Unknown error';
531
+ logger.log(`Bank settlement monitor error: ${msg}`);
532
+ }
533
+ });
534
+ // Job 4: google-invoice-scan (daily)
535
+ scheduler.add('google-invoice-scan', 86_400_000, async () => {
536
+ logger.log('Google invoice scan starting');
537
+ try {
538
+ const { getBillingAdapter } = await import('./financial/adapter-factory.js');
539
+ const billingAdapter = await getBillingAdapter('google', vaultKey, logger);
540
+ if (!billingAdapter) {
541
+ logger.log('Google invoice scan: no billing adapter available — skipping');
542
+ await writeCurrentState();
543
+ return;
544
+ }
545
+ const now = new Date();
546
+ const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
547
+ const today = now.toISOString().slice(0, 10);
548
+ const start = thirtyDaysAgo.toISOString().slice(0, 10);
549
+ const invoices = await billingAdapter.readInvoices('google', { start, end: today });
550
+ // Identify pending/overdue invoices for obligation tracking
551
+ const pendingInvoices = invoices.filter(i => i.status === 'pending' || i.status === 'overdue');
552
+ const totalInvoiceCents = pendingInvoices.reduce((sum, i) => sum + i.amountCents, 0);
553
+ // Check for invoices due within 7 days
554
+ const sevenDaysFromNow = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
555
+ const dueSoon = pendingInvoices.some(i => {
556
+ const dueDate = new Date(i.dueDate);
557
+ return dueDate.getTime() <= sevenDaysFromNow.getTime();
558
+ });
559
+ // Update treasury state with invoice data
560
+ treasuryState.googleInvoiceDueSoon = dueSoon;
561
+ treasuryState.googleInvoiceCents = totalInvoiceCents;
562
+ // Update pending obligations (add google invoices portion)
563
+ // Recalculate: google invoices + meta debits
564
+ treasuryState.pendingObligationsCents =
565
+ totalInvoiceCents + treasuryState.metaForecast7DayCents;
566
+ logger.log(`Google invoice scan: ${invoices.length} invoice(s), ` +
567
+ `${pendingInvoices.length} pending/overdue ` +
568
+ `($${(totalInvoiceCents / 100).toFixed(2)}), ` +
569
+ `due soon: ${dueSoon}`);
570
+ // CB-4: Evaluate billing breakers with invoice data
571
+ const cbResult = evaluateBillingBreakers({
572
+ googleInvoiceDueSoon: dueSoon,
573
+ googleInvoiceCents: totalInvoiceCents,
574
+ bankBalanceCents: treasuryState.bankBalanceCents,
575
+ minimumBufferCents: 50_000, // $500 minimum buffer
576
+ metaDebitFailed: treasuryState.metaDebitFailed,
577
+ metaPaymentRisk: treasuryState.metaPaymentRisk,
578
+ });
579
+ if (cbResult.shouldFreeze && !treasuryState.fundingFrozen) {
580
+ await triggerFreeze(cbResult.reasons.join('; '));
581
+ }
582
+ await saveTreasuryState();
583
+ await writeCurrentState();
584
+ }
585
+ catch (err) {
586
+ const msg = err instanceof Error ? err.message : 'Unknown error';
587
+ logger.log(`Google invoice scan error: ${msg}`);
588
+ }
589
+ });
590
+ // Job 5: meta-debit-monitor (daily)
591
+ scheduler.add('meta-debit-monitor', 86_400_000, async () => {
592
+ logger.log('Meta debit monitor starting');
593
+ try {
594
+ const { getBillingAdapter } = await import('./financial/adapter-factory.js');
595
+ const billingAdapter = await getBillingAdapter('meta', vaultKey, logger);
596
+ if (!billingAdapter) {
597
+ logger.log('Meta debit monitor: no billing adapter available — skipping');
598
+ await writeCurrentState();
599
+ return;
600
+ }
601
+ const now = new Date();
602
+ const today = now.toISOString().slice(0, 10);
603
+ const sevenDaysFromNow = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
604
+ const end = sevenDaysFromNow.toISOString().slice(0, 10);
605
+ let debits = [];
606
+ let metaDebitFailed = false;
607
+ try {
608
+ debits = await billingAdapter.readExpectedDebits('meta', { start: today, end });
609
+ }
610
+ catch (debitErr) {
611
+ // If we can't read debits, Meta may be in a bad state
612
+ metaDebitFailed = true;
613
+ const msg = debitErr instanceof Error ? debitErr.message : 'Unknown error';
614
+ logger.log(`Meta debit read failed: ${msg}`);
615
+ debits = [];
616
+ }
617
+ // Sum expected debits for obligation tracking
618
+ const totalDebitCents = debits.reduce((sum, d) => sum + d.estimatedAmountCents, 0);
619
+ // Check for failed or risky debits
620
+ const hasFailedDebit = debits.some(d => d.status === 'failed');
621
+ // Meta "payment risk" = debit failed or adapter call failed
622
+ const paymentRisk = metaDebitFailed || hasFailedDebit;
623
+ // Update treasury state with debit data
624
+ treasuryState.metaDebitFailed = metaDebitFailed || hasFailedDebit;
625
+ treasuryState.metaPaymentRisk = paymentRisk;
626
+ treasuryState.metaForecast7DayCents = totalDebitCents;
627
+ // Update pending obligations (google invoices + meta debits)
628
+ treasuryState.pendingObligationsCents =
629
+ treasuryState.googleInvoiceCents + totalDebitCents;
630
+ logger.log(`Meta debit monitor: ${debits.length} expected debit(s) ` +
631
+ `($${(totalDebitCents / 100).toFixed(2)} next 7 days), ` +
632
+ `failed: ${hasFailedDebit}, risk: ${paymentRisk}`);
633
+ // CB-5: Evaluate billing breakers with debit data
634
+ const cbResult = evaluateBillingBreakers({
635
+ googleInvoiceDueSoon: treasuryState.googleInvoiceDueSoon,
636
+ googleInvoiceCents: treasuryState.googleInvoiceCents,
637
+ bankBalanceCents: treasuryState.bankBalanceCents,
638
+ minimumBufferCents: 50_000, // $500 minimum buffer
639
+ metaDebitFailed: treasuryState.metaDebitFailed,
640
+ metaPaymentRisk: paymentRisk,
641
+ });
642
+ if (cbResult.shouldFreeze && !treasuryState.fundingFrozen) {
643
+ await triggerFreeze(cbResult.reasons.join('; '));
644
+ }
645
+ await saveTreasuryState();
646
+ await writeCurrentState();
647
+ }
648
+ catch (err) {
649
+ const msg = err instanceof Error ? err.message : 'Unknown error';
650
+ logger.log(`Meta debit monitor error: ${msg}`);
651
+ }
652
+ });
653
+ // Job 6: runway-forecast (every 6 hours)
654
+ scheduler.add('runway-forecast', 21_600_000, async () => {
655
+ logger.log('Runway forecast starting');
656
+ try {
657
+ // Read campaign data for spend projection
658
+ const campaignsDir = join(TREASURY_DIR, 'campaigns');
659
+ const campaigns = [];
660
+ if (existsSync(campaignsDir)) {
661
+ const { readdir } = await import('node:fs/promises');
662
+ const files = await readdir(campaignsDir);
663
+ for (const file of files) {
664
+ if (!file.endsWith('.json'))
665
+ continue;
666
+ try {
667
+ const content = await readFile(join(campaignsDir, file), 'utf-8');
668
+ const c = JSON.parse(content);
669
+ campaigns.push({
670
+ campaignId: c.id ?? file,
671
+ platform: c.platform ?? 'google',
672
+ dailyBudgetCents: c.dailyBudgetCents ?? 0,
673
+ status: c.status ?? 'paused',
674
+ });
675
+ }
676
+ catch { /* skip malformed */ }
677
+ }
678
+ }
679
+ const bankBalance = treasuryState.bankBalanceCents;
680
+ const pendingObligations = treasuryState.pendingObligationsCents;
681
+ const forecast = forecastRunway(bankBalance, campaigns, pendingObligations);
682
+ treasuryState.runwayDays = forecast.runwayDays;
683
+ logger.log(`Runway forecast: ${forecast.runwayDays} days ` +
684
+ `(bank $${(bankBalance / 100).toFixed(2)}, ` +
685
+ `daily spend $${(forecast.dailySpendCents / 100).toFixed(2)})`);
686
+ // Auto-funding evaluation: if runway is low, check if we should auto-off-ramp
687
+ if (!treasuryState.fundingFrozen) {
688
+ const autoConfig = {
689
+ source: {
690
+ id: 'default-source',
691
+ provider: 'circle',
692
+ asset: 'USDC',
693
+ network: 'ETH',
694
+ sourceAccountId: 'default',
695
+ whitelistedDestinationBankId: 'default-bank',
696
+ status: 'active',
697
+ },
698
+ bank: {
699
+ id: 'default-bank',
700
+ provider: 'mercury',
701
+ accountId: 'default',
702
+ currency: 'USD',
703
+ availableBalanceCents: bankBalance,
704
+ reservedBalanceCents: 0,
705
+ minimumBufferCents: 50_000, // $500
706
+ },
707
+ planConfig: {
708
+ minimumOfframpCents: 10_000, // $100
709
+ bufferTargetCents: 100_000, // $1,000
710
+ maxDailyOfframpCents: 5_000_000, // $50,000
711
+ targetRunwayDays: 30,
712
+ },
713
+ pendingSpendCents: pendingObligations,
714
+ obligations: [],
715
+ googleInvoiceDueSoon: treasuryState.googleInvoiceDueSoon,
716
+ googleInvoiceCents: treasuryState.googleInvoiceCents,
717
+ metaUsesDirectDebit: treasuryState.metaForecast7DayCents > 0,
718
+ metaForecast7DayCents: treasuryState.metaForecast7DayCents,
719
+ debitProtectionBufferCents: 0,
720
+ discrepancyExists: treasuryState.consecutiveMismatches > 0,
721
+ previousHash: '',
722
+ };
723
+ const autoResult = evaluateAutoFunding(autoConfig);
724
+ if (autoResult) {
725
+ logger.log(`Auto-funding approved: $${(autoResult.plan.requiredCents / 100).toFixed(2)} ` +
726
+ `(reason: ${autoResult.plan.reason}) — queuing for execution`);
727
+ // Log the approved plan
728
+ await mkdir(TREASURY_DIR, { recursive: true });
729
+ await appendFile(FUNDING_PLANS_LOG, JSON.stringify(autoResult.plan) + '\n', 'utf-8');
730
+ }
731
+ else {
732
+ logger.log('Auto-funding: no action needed or policy blocked');
733
+ }
734
+ // Execute approved funding plans
735
+ await executeApprovedPlans(vaultKey, logger, triggerFreeze);
736
+ }
737
+ await saveTreasuryState();
738
+ await writeCurrentState();
739
+ }
740
+ catch (err) {
741
+ const msg = err instanceof Error ? err.message : 'Unknown error';
742
+ logger.log(`Runway forecast error: ${msg}`);
743
+ }
744
+ });
745
+ // Job 7: funding-reconciliation (extends existing reconciliation at midnight+06:00)
746
+ scheduler.add('funding-reconciliation', 3_600_000, async () => {
747
+ const hour = new Date().getUTCHours();
748
+ if (hour !== 0 && hour !== 6)
749
+ return;
750
+ logger.log(`Funding reconciliation (${hour === 0 ? 'preliminary' : 'authoritative'}) starting`);
751
+ try {
752
+ // Read provider transfers from transfers log
753
+ const providerTransfers = [];
754
+ if (existsSync(TRANSFERS_LOG)) {
755
+ const content = await readFile(TRANSFERS_LOG, 'utf-8');
756
+ const lines = content.trim().split('\n').filter(Boolean);
757
+ for (const line of lines) {
758
+ try {
759
+ const t = JSON.parse(line);
760
+ providerTransfers.push(t);
761
+ }
762
+ catch { /* skip malformed */ }
763
+ }
764
+ }
765
+ // Bank transactions and platform spend are empty until bank adapter
766
+ // is wired with real credentials
767
+ const bankTransactions = [];
768
+ const platformSpend = [];
769
+ const report = reconcileThreeWay(providerTransfers, bankTransactions, platformSpend);
770
+ // Write report to reconciliation log
771
+ await mkdir(TREASURY_DIR, { recursive: true });
772
+ await appendFile(RECONCILIATION_LOG, JSON.stringify({ ...report, type: hour === 0 ? 'preliminary' : 'authoritative' }) + '\n', 'utf-8');
773
+ treasuryState.lastReconciliationAt = new Date().toISOString();
774
+ // Track consecutive mismatches for CB-3
775
+ if (report.mismatchCount > 0) {
776
+ treasuryState.consecutiveMismatches += 1;
777
+ logger.log(`Reconciliation: ${report.mismatchCount} mismatch(es), ` +
778
+ `${treasuryState.consecutiveMismatches} consecutive`);
779
+ }
780
+ else {
781
+ treasuryState.consecutiveMismatches = 0;
782
+ logger.log(`Reconciliation: clean — ${report.transferMatches.length} transfers matched, ` +
783
+ `variance $${(report.overallVarianceCents / 100).toFixed(2)}`);
784
+ }
785
+ // CB-3: Reconciliation mismatch for 2 consecutive closes
786
+ if (shouldFreeze(report.mismatchCount, treasuryState.consecutiveMismatches, 2)) {
787
+ if (!treasuryState.fundingFrozen) {
788
+ await triggerFreeze(`Reconciliation mismatch for ${treasuryState.consecutiveMismatches} consecutive closes`);
789
+ }
790
+ }
791
+ await saveTreasuryState();
792
+ await writeCurrentState();
793
+ }
794
+ catch (err) {
795
+ const msg = err instanceof Error ? err.message : 'Unknown error';
796
+ logger.log(`Funding reconciliation error: ${msg}`);
797
+ }
798
+ });
799
+ // Job 8: stale-plan-detector (hourly)
800
+ scheduler.add('stale-plan-detector', 3_600_000, async () => {
801
+ try {
802
+ if (!existsSync(FUNDING_PLANS_LOG))
803
+ return;
804
+ const content = await readFile(FUNDING_PLANS_LOG, 'utf-8');
805
+ const lines = content.trim().split('\n').filter(Boolean);
806
+ const STALE_THRESHOLD_MS = 24 * 60 * 60 * 1000; // 24 hours
807
+ const now = Date.now();
808
+ let staleCount = 0;
809
+ for (const line of lines) {
810
+ try {
811
+ const plan = JSON.parse(line);
812
+ if (plan.status !== 'DRAFT' && plan.status !== 'APPROVED')
813
+ continue;
814
+ const age = now - new Date(plan.createdAt ?? '').getTime();
815
+ if (age > STALE_THRESHOLD_MS) {
816
+ staleCount += 1;
817
+ logger.log(`Stale funding plan: ${plan.id} (${plan.status}, ${Math.round(age / (60 * 60 * 1000))}h old)`);
818
+ }
819
+ }
820
+ catch { /* skip malformed */ }
821
+ }
822
+ if (staleCount > 0) {
823
+ logger.log(`${staleCount} stale funding plan(s) detected — plans stuck in PENDING >24h`);
824
+ }
825
+ }
826
+ catch (err) {
827
+ const msg = err instanceof Error ? err.message : 'Unknown error';
828
+ logger.log(`Stale plan detector error: ${msg}`);
829
+ }
830
+ });
831
+ logger.log('Treasury heartbeat jobs registered (8 jobs, WAL recovery active)');
832
+ }
833
+ // ── Treasury Socket Handlers ─────────────────────────
834
+ export async function handleTreasuryRequest(method, path, _body, auth, logger, triggerFreeze, vaultKey) {
835
+ // POST /treasury/offramp — vault+TOTP required
836
+ if (method === 'POST' && path === '/treasury/offramp') {
837
+ if (!auth.vaultVerified || !auth.totpVerified) {
838
+ return {
839
+ status: 403,
840
+ body: { ok: false, error: 'Off-ramp requires valid vault password + TOTP code' },
841
+ };
842
+ }
843
+ if (!isStablecoinConfigured()) {
844
+ return {
845
+ status: 400,
846
+ body: { ok: false, error: 'Stablecoin funding not configured' },
847
+ };
848
+ }
849
+ if (treasuryState.fundingFrozen) {
850
+ return {
851
+ status: 423,
852
+ body: { ok: false, error: `Funding frozen: ${treasuryState.freezeReason ?? 'unknown reason'}` },
853
+ };
854
+ }
855
+ logger.log('Off-ramp requested via treasury API');
856
+ // Write WAL entry first (ADR-3)
857
+ const intentId = randomUUID();
858
+ await writeWalEntry({
859
+ intentId,
860
+ operation: 'offramp',
861
+ params: _body,
862
+ status: 'pending',
863
+ createdAt: new Date().toISOString(),
864
+ });
865
+ try {
866
+ const { getStablecoinAdapter } = await import('./financial/adapter-factory.js');
867
+ const adapter = await getStablecoinAdapter(vaultKey, logger);
868
+ // Generate a funding plan using treasury planner
869
+ const bankBalance = treasuryState.bankBalanceCents;
870
+ const source = {
871
+ id: 'default-source',
872
+ provider: 'circle',
873
+ asset: 'USDC',
874
+ network: 'ETH',
875
+ sourceAccountId: 'default',
876
+ whitelistedDestinationBankId: 'default-bank',
877
+ status: 'active',
878
+ };
879
+ const bank = {
880
+ id: 'default-bank',
881
+ provider: 'mercury',
882
+ accountId: 'default',
883
+ currency: 'USD',
884
+ availableBalanceCents: bankBalance,
885
+ reservedBalanceCents: 0,
886
+ minimumBufferCents: 50_000, // $500
887
+ };
888
+ const config = {
889
+ minimumOfframpCents: 10_000, // $100
890
+ bufferTargetCents: 100_000, // $1,000
891
+ maxDailyOfframpCents: 5_000_000, // $50,000
892
+ targetRunwayDays: 30,
893
+ };
894
+ const obligations = [];
895
+ const plan = generateFundingPlan(source, bank, obligations, config, '');
896
+ if (!plan) {
897
+ return {
898
+ status: 200,
899
+ body: { ok: true, message: 'No funding needed — balance sufficient' },
900
+ };
901
+ }
902
+ // Evaluate policy before executing
903
+ const policyState = {
904
+ bankBalanceCents: bank.availableBalanceCents,
905
+ minimumBufferCents: bank.minimumBufferCents,
906
+ reservedCents: bank.reservedBalanceCents,
907
+ proposedOfframpCents: plan.requiredCents,
908
+ maxDailyMovementCents: config.maxDailyOfframpCents,
909
+ googleInvoiceDueSoon: false,
910
+ googleInvoiceCents: 0,
911
+ metaUsesDirectDebit: false,
912
+ metaForecast7DayCents: 0,
913
+ debitProtectionBufferCents: 0,
914
+ discrepancyExists: treasuryState.consecutiveMismatches > 0,
915
+ proposingBudgetRaise: false,
916
+ platformCapability: 'FULLY_FUNDABLE',
917
+ claimingAutonomousFunding: false,
918
+ };
919
+ const decisions = evaluatePolicy(policyState);
920
+ const aggregate = aggregateDecisions(decisions);
921
+ if (aggregate.action === 'freeze') {
922
+ await triggerFreeze(aggregate.blockingRules.map(r => r.reason).join('; '));
923
+ return {
924
+ status: 423,
925
+ body: { ok: false, error: 'Policy freeze triggered', rules: aggregate.blockingRules },
926
+ };
927
+ }
928
+ if (aggregate.action === 'deny') {
929
+ return {
930
+ status: 403,
931
+ body: {
932
+ ok: false,
933
+ error: 'Policy denied off-ramp',
934
+ rules: aggregate.blockingRules,
935
+ },
936
+ };
937
+ }
938
+ // Initiate the off-ramp
939
+ const planRef = {
940
+ id: plan.id,
941
+ sourceFundingId: plan.sourceFundingId,
942
+ destinationBankId: plan.destinationBankId,
943
+ requiredCents: plan.requiredCents,
944
+ idempotencyKey: plan.idempotencyKey,
945
+ };
946
+ const transfer = await adapter.initiateOfframp(planRef, plan.hash);
947
+ // Log the plan and transfer
948
+ await mkdir(TREASURY_DIR, { recursive: true });
949
+ await appendFile(FUNDING_PLANS_LOG, JSON.stringify(plan) + '\n', 'utf-8');
950
+ await appendFile(TRANSFERS_LOG, JSON.stringify(transfer) + '\n', 'utf-8');
951
+ // Track pending transfer
952
+ const pending = await readPendingTransfers();
953
+ pending.push({
954
+ id: transfer.id,
955
+ fundingPlanId: plan.id,
956
+ providerTransferId: transfer.providerTransferId,
957
+ amountCents: transfer.amountCents,
958
+ status: 'pending',
959
+ initiatedAt: transfer.initiatedAt,
960
+ lastPolledAt: transfer.initiatedAt,
961
+ });
962
+ await writePendingTransfers(pending);
963
+ // Update treasury state
964
+ treasuryState.pendingTransferCount += 1;
965
+ treasuryState.lastOfframpAt = new Date().toISOString();
966
+ // Track daily movement for CB-6
967
+ const today = new Date().toISOString().slice(0, 10);
968
+ if (treasuryState.dailyMovementDate !== today) {
969
+ treasuryState.dailyMovementCents = 0;
970
+ treasuryState.dailyMovementDate = today;
971
+ }
972
+ treasuryState.dailyMovementCents += plan.requiredCents;
973
+ // Complete WAL
974
+ await writeWalEntry({
975
+ intentId,
976
+ operation: 'offramp',
977
+ params: { planId: plan.id, transferId: transfer.id },
978
+ status: 'completed',
979
+ createdAt: new Date().toISOString(),
980
+ completedAt: new Date().toISOString(),
981
+ });
982
+ await saveTreasuryState();
983
+ logger.log(`Off-ramp initiated: $${(plan.requiredCents / 100).toFixed(2)} ` +
984
+ `via ${transfer.provider} (transfer ${transfer.id})`);
985
+ return {
986
+ status: 200,
987
+ body: {
988
+ ok: true,
989
+ message: 'Off-ramp initiated',
990
+ plan: { id: plan.id, amountCents: plan.requiredCents, reason: plan.reason },
991
+ transfer: { id: transfer.id, status: transfer.status, provider: transfer.provider },
992
+ },
993
+ };
994
+ }
995
+ catch (err) {
996
+ const msg = err instanceof Error ? err.message : 'Off-ramp failed';
997
+ logger.log(`Off-ramp error: ${msg}`);
998
+ await writeWalEntry({
999
+ intentId,
1000
+ operation: 'offramp',
1001
+ params: _body,
1002
+ status: 'failed',
1003
+ createdAt: new Date().toISOString(),
1004
+ error: msg,
1005
+ });
1006
+ return { status: 500, body: { ok: false, error: `Off-ramp failed: ${msg}` } };
1007
+ }
1008
+ }
1009
+ // POST /treasury/freeze — session token only (protective)
1010
+ if (method === 'POST' && path === '/treasury/freeze') {
1011
+ logger.log('Treasury FREEZE command received');
1012
+ treasuryState.fundingFrozen = true;
1013
+ treasuryState.freezeReason = 'Manual freeze via treasury API';
1014
+ await saveTreasuryState();
1015
+ return { status: 200, body: { ok: true, message: 'Funding frozen' } };
1016
+ }
1017
+ // POST /treasury/unfreeze — vault+TOTP required
1018
+ if (method === 'POST' && path === '/treasury/unfreeze') {
1019
+ if (!auth.vaultVerified || !auth.totpVerified) {
1020
+ return {
1021
+ status: 403,
1022
+ body: { ok: false, error: 'Unfreeze requires valid vault password + TOTP code' },
1023
+ };
1024
+ }
1025
+ logger.log('Treasury UNFREEZE command received');
1026
+ treasuryState.fundingFrozen = false;
1027
+ treasuryState.freezeReason = null;
1028
+ // Reset circuit breaker counters on unfreeze
1029
+ treasuryState.consecutiveMismatches = 0;
1030
+ treasuryState.consecutiveProviderFailures = 0;
1031
+ await saveTreasuryState();
1032
+ return { status: 200, body: { ok: true, message: 'Funding unfrozen' } };
1033
+ }
1034
+ // GET /treasury/balances — session token only
1035
+ if (method === 'GET' && path === '/treasury/balances') {
1036
+ return {
1037
+ status: 200,
1038
+ body: {
1039
+ ok: true,
1040
+ data: {
1041
+ stablecoinBalanceCents: treasuryState.stablecoinBalanceCents,
1042
+ bankBalanceCents: treasuryState.bankBalanceCents,
1043
+ totalAvailableCents: treasuryState.stablecoinBalanceCents + treasuryState.bankBalanceCents,
1044
+ pendingTransferCount: treasuryState.pendingTransferCount,
1045
+ fundingFrozen: treasuryState.fundingFrozen,
1046
+ },
1047
+ },
1048
+ };
1049
+ }
1050
+ // GET /treasury/funding-status — session token only
1051
+ if (method === 'GET' && path === '/treasury/funding-status') {
1052
+ const pending = await readPendingTransfers();
1053
+ const activePending = pending.filter(t => t.status === 'pending' || t.status === 'processing');
1054
+ return {
1055
+ status: 200,
1056
+ body: {
1057
+ ok: true,
1058
+ data: {
1059
+ pendingPlans: activePending.length,
1060
+ pendingTransfers: activePending.map(t => ({
1061
+ id: t.id,
1062
+ amountCents: t.amountCents,
1063
+ status: t.status,
1064
+ initiatedAt: t.initiatedAt,
1065
+ })),
1066
+ runwayDays: treasuryState.runwayDays,
1067
+ fundingFrozen: treasuryState.fundingFrozen,
1068
+ freezeReason: treasuryState.freezeReason,
1069
+ lastOfframpAt: treasuryState.lastOfframpAt,
1070
+ lastReconciliationAt: treasuryState.lastReconciliationAt,
1071
+ consecutiveMismatches: treasuryState.consecutiveMismatches,
1072
+ },
1073
+ },
1074
+ };
1075
+ }
1076
+ // GET /treasury/runway — session token only
1077
+ if (method === 'GET' && path === '/treasury/runway') {
1078
+ return {
1079
+ status: 200,
1080
+ body: {
1081
+ ok: true,
1082
+ data: {
1083
+ runwayDays: treasuryState.runwayDays,
1084
+ bankBalanceCents: treasuryState.bankBalanceCents,
1085
+ stablecoinBalanceCents: treasuryState.stablecoinBalanceCents,
1086
+ fundingFrozen: treasuryState.fundingFrozen,
1087
+ },
1088
+ },
1089
+ };
1090
+ }
1091
+ // Not a treasury route — return null so the caller falls through
1092
+ return null;
1093
+ }
1094
+ // ── Freeze Helper for Circuit Breakers ───────────────
1095
+ export async function executeTreasuryFreeze(reason, logger) {
1096
+ treasuryState.fundingFrozen = true;
1097
+ treasuryState.freezeReason = reason;
1098
+ logger.log(`TREASURY FREEZE: ${reason}`);
1099
+ await saveTreasuryState();
1100
+ }
1101
+ // ── Exported State Accessors (for heartbeat state snapshot) ──
1102
+ export function getTreasuryStateSnapshot() {
1103
+ return { ...treasuryState };
1104
+ }