@omega.js/manager 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 (416) hide show
  1. package/.claude-plugin/marketplace.json +14 -0
  2. package/LICENSE +98 -0
  3. package/README.md +165 -0
  4. package/bin/omega +2 -0
  5. package/bin/omg +2 -0
  6. package/claude-plugin/.claude-plugin/plugin.json +11 -0
  7. package/claude-plugin/.mcp.json +9 -0
  8. package/claude-plugin/README.md +139 -0
  9. package/claude-plugin/hooks/gate/mark.sh +58 -0
  10. package/claude-plugin/hooks/gate/run.sh +97 -0
  11. package/claude-plugin/hooks/guard/run.sh +249 -0
  12. package/claude-plugin/hooks/hooks.json +63 -0
  13. package/claude-plugin/hooks/inject/run.sh +127 -0
  14. package/claude-plugin/hooks/lib/omega-gate.sh +20 -0
  15. package/claude-plugin/hooks/lib/omega-scope.sh +41 -0
  16. package/claude-plugin/hooks/lib/omega-skills.sh +209 -0
  17. package/claude-plugin/hooks/quality/run.sh +132 -0
  18. package/claude-plugin/hooks/shape/run.sh +45 -0
  19. package/claude-plugin/mcp-router-launch.js +29 -0
  20. package/claude-plugin/skills/README.md +48 -0
  21. package/claude-plugin/skills/accessibility/SKILL.md +31 -0
  22. package/claude-plugin/skills/analytics/SKILL.md +30 -0
  23. package/claude-plugin/skills/backend/SKILL.md +25 -0
  24. package/claude-plugin/skills/brandcheck/SKILL.md +29 -0
  25. package/claude-plugin/skills/browser/SKILL.md +64 -0
  26. package/claude-plugin/skills/client/SKILL.md +24 -0
  27. package/claude-plugin/skills/desktop/SKILL.md +25 -0
  28. package/claude-plugin/skills/extension/SKILL.md +25 -0
  29. package/claude-plugin/skills/main/SKILL.md +54 -0
  30. package/claude-plugin/skills/manager/SKILL.md +25 -0
  31. package/claude-plugin/skills/seo/SKILL.md +36 -0
  32. package/claude-plugin/skills/theme/SKILL.md +33 -0
  33. package/claude-plugin/skills/web/SKILL.md +29 -0
  34. package/dist/cli-run.js +34 -0
  35. package/dist/cli.js +29 -0
  36. package/dist/commands/build.js +17 -0
  37. package/dist/commands/clean.js +16 -0
  38. package/dist/commands/company.js +46 -0
  39. package/dist/commands/deploy.js +143 -0
  40. package/dist/commands/dev.js +437 -0
  41. package/dist/commands/devlog.js +25 -0
  42. package/dist/commands/help.js +48 -0
  43. package/dist/commands/manage.js +53 -0
  44. package/dist/commands/migrate.js +76 -0
  45. package/dist/commands/onboard.js +31 -0
  46. package/dist/commands/pipeline.js +319 -0
  47. package/dist/commands/test.js +380 -0
  48. package/dist/commands/update.js +128 -0
  49. package/dist/commands/version.js +8 -0
  50. package/dist/company-init.js +167 -0
  51. package/dist/company.js +283 -0
  52. package/dist/config.js +1359 -0
  53. package/dist/devlog/index.js +226 -0
  54. package/dist/devlog/lib/collect-commits.js +136 -0
  55. package/dist/devlog/lib/generate-post.js +201 -0
  56. package/dist/devlog/lib/ghostii.js +122 -0
  57. package/dist/devlog/lib/project-map.js +98 -0
  58. package/dist/devlog/lib/publish-website.js +88 -0
  59. package/dist/index.js +45 -0
  60. package/dist/lib/agents-md.js +201 -0
  61. package/dist/lib/analytics-secret.js +22 -0
  62. package/dist/lib/argv.js +21 -0
  63. package/dist/lib/auth-admin.js +120 -0
  64. package/dist/lib/automation-client.js +210 -0
  65. package/dist/lib/backend-marketing.js +49 -0
  66. package/dist/lib/brand.js +240 -0
  67. package/dist/lib/bundle-id.js +53 -0
  68. package/dist/lib/claude-settings.js +125 -0
  69. package/dist/lib/company-scaffold.js +213 -0
  70. package/dist/lib/company.js +227 -0
  71. package/dist/lib/config-flow.js +350 -0
  72. package/dist/lib/config-write.js +41 -0
  73. package/dist/lib/custom-target.js +75 -0
  74. package/dist/lib/domain-utils.js +19 -0
  75. package/dist/lib/duration.js +39 -0
  76. package/dist/lib/env-order.js +236 -0
  77. package/dist/lib/env-secret.js +78 -0
  78. package/dist/lib/firestore-rest.js +326 -0
  79. package/dist/lib/framework-bin.js +156 -0
  80. package/dist/lib/gitignore.js +63 -0
  81. package/dist/lib/google-auth.js +495 -0
  82. package/dist/lib/google-token.js +66 -0
  83. package/dist/lib/jwt.js +31 -0
  84. package/dist/lib/legacy-oauth.js +59 -0
  85. package/dist/lib/node-version.js +127 -0
  86. package/dist/lib/owner-plan.js +78 -0
  87. package/dist/lib/package-scripts.js +152 -0
  88. package/dist/lib/preflight.js +470 -0
  89. package/dist/lib/product-create.js +144 -0
  90. package/dist/lib/run-command.js +74 -0
  91. package/dist/lib/run-gates.js +70 -0
  92. package/dist/lib/run-output.js +42 -0
  93. package/dist/lib/run-summary.js +384 -0
  94. package/dist/lib/scaffold.js +427 -0
  95. package/dist/lib/service-input.js +207 -0
  96. package/dist/lib/service-runner.js +466 -0
  97. package/dist/lib/stale.js +24 -0
  98. package/dist/lib/target-selection.js +155 -0
  99. package/dist/lib/verb-fanout.js +128 -0
  100. package/dist/lib/verify-live.js +188 -0
  101. package/dist/manage.js +311 -0
  102. package/dist/omega-bin.js +7 -0
  103. package/dist/onboard.js +532 -0
  104. package/dist/services/account/ensure/users.js +259 -0
  105. package/dist/services/account/index.js +97 -0
  106. package/dist/services/account/lib/backend-client.js +107 -0
  107. package/dist/services/account/lib/password.js +36 -0
  108. package/dist/services/account/lib/resolve-password.js +100 -0
  109. package/dist/services/advertising/ensure/sites.js +101 -0
  110. package/dist/services/advertising/index.js +101 -0
  111. package/dist/services/advertising/lib/adsense-api.js +41 -0
  112. package/dist/services/ai/ensure/keys.js +21 -0
  113. package/dist/services/ai/index.js +36 -0
  114. package/dist/services/analytics/ensure/google-firebase-link.js +230 -0
  115. package/dist/services/analytics/ensure/google-streams.js +256 -0
  116. package/dist/services/analytics/ensure/meta-pixel.js +22 -0
  117. package/dist/services/analytics/ensure/tiktok-pixel.js +22 -0
  118. package/dist/services/analytics/index.js +134 -0
  119. package/dist/services/analytics/lib/analytics-api.js +156 -0
  120. package/dist/services/analytics/lib/meta-api.js +83 -0
  121. package/dist/services/analytics/lib/pixel-account.js +78 -0
  122. package/dist/services/analytics/lib/pixel-provision.js +152 -0
  123. package/dist/services/analytics/lib/pixel-specs.js +57 -0
  124. package/dist/services/analytics/lib/pixel-token.js +88 -0
  125. package/dist/services/analytics/lib/property-flow.js +77 -0
  126. package/dist/services/analytics/lib/tiktok-api.js +157 -0
  127. package/dist/services/analytics/lib/tiktok-auth.js +153 -0
  128. package/dist/services/assets/ensure/logo-gen.js +68 -0
  129. package/dist/services/assets/index.js +109 -0
  130. package/dist/services/assets/lib/assets-config.js +168 -0
  131. package/dist/services/assets/lib/brandmark-api.js +160 -0
  132. package/dist/services/assets/lib/derived.js +113 -0
  133. package/dist/services/assets/lib/font-loader.js +67 -0
  134. package/dist/services/assets/lib/reconcile.js +122 -0
  135. package/dist/services/assets/lib/reset.js +118 -0
  136. package/dist/services/assets/lib/svg-logo-generator.js +154 -0
  137. package/dist/services/assets/lib/svg-to-black.js +32 -0
  138. package/dist/services/assets/write/favicons.js +82 -0
  139. package/dist/services/assets/write/icons.js +64 -0
  140. package/dist/services/assets/write/process.js +96 -0
  141. package/dist/services/assets/write/reconcile.js +30 -0
  142. package/dist/services/assets/write/social-icons.js +108 -0
  143. package/dist/services/assets/write/templates.js +345 -0
  144. package/dist/services/bookmark/ensure/sync.js +255 -0
  145. package/dist/services/bookmark/index.js +17 -0
  146. package/dist/services/campaigns/ensure/contact-person.js +40 -0
  147. package/dist/services/campaigns/ensure/custom-fields.js +84 -0
  148. package/dist/services/campaigns/ensure/domain-auth.js +92 -0
  149. package/dist/services/campaigns/ensure/event-webhook.js +89 -0
  150. package/dist/services/campaigns/ensure/link-branding.js +187 -0
  151. package/dist/services/campaigns/ensure/list.js +50 -0
  152. package/dist/services/campaigns/ensure/segments.js +109 -0
  153. package/dist/services/campaigns/ensure/sender-identity.js +153 -0
  154. package/dist/services/campaigns/ensure/unsubscribe-groups.js +116 -0
  155. package/dist/services/campaigns/index.js +79 -0
  156. package/dist/services/campaigns/lib/dns-sync.js +81 -0
  157. package/dist/services/campaigns/lib/segment-query.js +138 -0
  158. package/dist/services/campaigns/lib/sendgrid-api.js +287 -0
  159. package/dist/services/captcha/ensure/site-key.js +83 -0
  160. package/dist/services/captcha/index.js +78 -0
  161. package/dist/services/captcha/lib/console-url.js +45 -0
  162. package/dist/services/captcha/lib/recaptcha-api.js +36 -0
  163. package/dist/services/certificates/ensure/api-key.js +22 -0
  164. package/dist/services/certificates/ensure/bundle-ids.js +115 -0
  165. package/dist/services/certificates/ensure/certificates.js +187 -0
  166. package/dist/services/certificates/ensure/profiles.js +143 -0
  167. package/dist/services/certificates/index.js +225 -0
  168. package/dist/services/certificates/lib/apple-api.js +183 -0
  169. package/dist/services/certificates/lib/certificate-manager.js +252 -0
  170. package/dist/services/certificates/lib/identifier-manager.js +107 -0
  171. package/dist/services/certificates/lib/keychain.js +135 -0
  172. package/dist/services/certificates/lib/manual-walkthrough.js +165 -0
  173. package/dist/services/certificates/lib/profile-manager.js +115 -0
  174. package/dist/services/chat/data/baseline-knowledge.md +96 -0
  175. package/dist/services/chat/ensure/chat.js +106 -0
  176. package/dist/services/chat/ensure/user.js +45 -0
  177. package/dist/services/chat/index.js +139 -0
  178. package/dist/services/chat/lib/baseline-knowledge.js +137 -0
  179. package/dist/services/cloud/ensure/authentication.js +267 -0
  180. package/dist/services/cloud/ensure/billing.js +120 -0
  181. package/dist/services/cloud/ensure/cloud-messaging.js +105 -0
  182. package/dist/services/cloud/ensure/database.js +60 -0
  183. package/dist/services/cloud/ensure/firestore.js +79 -0
  184. package/dist/services/cloud/ensure/functions.js +49 -0
  185. package/dist/services/cloud/ensure/hosting.js +398 -0
  186. package/dist/services/cloud/ensure/oauth-consent.js +248 -0
  187. package/dist/services/cloud/ensure/project-settings.js +72 -0
  188. package/dist/services/cloud/ensure/sdk-config.js +95 -0
  189. package/dist/services/cloud/ensure/service-account.js +152 -0
  190. package/dist/services/cloud/ensure/services.js +103 -0
  191. package/dist/services/cloud/ensure/storage.js +47 -0
  192. package/dist/services/cloud/index.js +130 -0
  193. package/dist/services/cloud/lib/access-heal.js +130 -0
  194. package/dist/services/cloud/lib/firebase-api.js +662 -0
  195. package/dist/services/cloud/lib/project-flow.js +102 -0
  196. package/dist/services/directory/ensure/entry.js +48 -0
  197. package/dist/services/directory/index.js +64 -0
  198. package/dist/services/directory/lib/blocks.js +104 -0
  199. package/dist/services/disperse/index.js +45 -0
  200. package/dist/services/disperse/write/certs.js +117 -0
  201. package/dist/services/domain/ensure/nameservers.js +179 -0
  202. package/dist/services/domain/index.js +69 -0
  203. package/dist/services/domain/lib/namecheap-api.js +167 -0
  204. package/dist/services/domain/lib/registrars.js +46 -0
  205. package/dist/services/domain/lib/whitelist-walkthrough.js +76 -0
  206. package/dist/services/edge/ensure/cache-rules.js +105 -0
  207. package/dist/services/edge/ensure/dns-records.js +304 -0
  208. package/dist/services/edge/ensure/email-routing.js +292 -0
  209. package/dist/services/edge/ensure/rules-configuration.js +98 -0
  210. package/dist/services/edge/ensure/rules-managed-transforms.js +101 -0
  211. package/dist/services/edge/ensure/rules-redirect.js +107 -0
  212. package/dist/services/edge/ensure/rules-response-headers.js +90 -0
  213. package/dist/services/edge/ensure/rules-security.js +142 -0
  214. package/dist/services/edge/ensure/speed-scheduled-tests.js +98 -0
  215. package/dist/services/edge/ensure/workers.js +219 -0
  216. package/dist/services/edge/ensure/zone-settings.js +108 -0
  217. package/dist/services/edge/ensure/zone.js +183 -0
  218. package/dist/services/edge/index.js +73 -0
  219. package/dist/services/edge/lib/cloudflare-api.js +84 -0
  220. package/dist/services/edge/lib/dns-records-helpers.js +518 -0
  221. package/dist/services/edge/lib/read-cache.js +16 -0
  222. package/dist/services/edge/lib/ruleset-helper.js +108 -0
  223. package/dist/services/edge/workers/omega-api-proxy.js +43 -0
  224. package/dist/services/email/data/baseline-filter.md +4 -0
  225. package/dist/services/email/data/baseline-knowledge.md +86 -0
  226. package/dist/services/email/ensure/agent.js +107 -0
  227. package/dist/services/email/ensure/user.js +45 -0
  228. package/dist/services/email/index.js +138 -0
  229. package/dist/services/email/lib/baseline.js +82 -0
  230. package/dist/services/email/lib/knowledge-file.js +53 -0
  231. package/dist/services/forms/ensure/form.js +46 -0
  232. package/dist/services/forms/ensure/user.js +45 -0
  233. package/dist/services/forms/index.js +138 -0
  234. package/dist/services/migrations/ensure/notifications.js +189 -0
  235. package/dist/services/migrations/ensure/orders.js +34 -0
  236. package/dist/services/migrations/ensure/payment-provider.js +132 -0
  237. package/dist/services/migrations/ensure/payments-intents.js +34 -0
  238. package/dist/services/migrations/ensure/state-retirement.js +266 -0
  239. package/dist/services/migrations/ensure/targets-rename.js +124 -0
  240. package/dist/services/migrations/ensure/users.js +1176 -0
  241. package/dist/services/migrations/index.js +101 -0
  242. package/dist/services/migrations/lib/attribution-touch.js +64 -0
  243. package/dist/services/migrations/lib/ensure-metadata.js +144 -0
  244. package/dist/services/migrations/lib/migration-runner.js +473 -0
  245. package/dist/services/migrations/lib/sanitize-strings.js +112 -0
  246. package/dist/services/migrations/lib/schema-validator.js +154 -0
  247. package/dist/services/monitoring/ensure/dsn.js +54 -0
  248. package/dist/services/monitoring/ensure/projects.js +135 -0
  249. package/dist/services/monitoring/index.js +54 -0
  250. package/dist/services/monitoring/lib/sentry-api.js +94 -0
  251. package/dist/services/newsletter/ensure/custom-fields.js +90 -0
  252. package/dist/services/newsletter/ensure/publication.js +94 -0
  253. package/dist/services/newsletter/ensure/segments.js +140 -0
  254. package/dist/services/newsletter/ensure/webhook.js +108 -0
  255. package/dist/services/newsletter/index.js +64 -0
  256. package/dist/services/newsletter/lib/beehiiv-api.js +124 -0
  257. package/dist/services/newsletter/lib/segment-automation.js +577 -0
  258. package/dist/services/payment/ensure/chargebee-account.js +30 -0
  259. package/dist/services/payment/ensure/chargebee-products.js +359 -0
  260. package/dist/services/payment/ensure/chargebee-webhook.js +113 -0
  261. package/dist/services/payment/ensure/paypal-account.js +36 -0
  262. package/dist/services/payment/ensure/paypal-products.js +374 -0
  263. package/dist/services/payment/ensure/paypal-webhook.js +128 -0
  264. package/dist/services/payment/ensure/stripe-account.js +93 -0
  265. package/dist/services/payment/ensure/stripe-disputes.js +56 -0
  266. package/dist/services/payment/ensure/stripe-products.js +245 -0
  267. package/dist/services/payment/ensure/stripe-radar.js +74 -0
  268. package/dist/services/payment/ensure/stripe-webhook.js +130 -0
  269. package/dist/services/payment/index.js +163 -0
  270. package/dist/services/payment/lib/chargebee-api.js +338 -0
  271. package/dist/services/payment/lib/payment-utils.js +116 -0
  272. package/dist/services/payment/lib/paypal-api.js +354 -0
  273. package/dist/services/payment/lib/provider-setup.js +134 -0
  274. package/dist/services/payment/lib/stripe-api.js +152 -0
  275. package/dist/services/repo/ensure/org.js +70 -0
  276. package/dist/services/repo/ensure/pages.js +72 -0
  277. package/dist/services/repo/ensure/repo.js +90 -0
  278. package/dist/services/repo/index.js +52 -0
  279. package/dist/services/repo/lib/github-api.js +196 -0
  280. package/dist/services/search/ensure/ga-link.js +57 -0
  281. package/dist/services/search/ensure/property.js +137 -0
  282. package/dist/services/search/ensure/sitemaps.js +70 -0
  283. package/dist/services/search/index.js +61 -0
  284. package/dist/services/search/lib/search-console-api.js +92 -0
  285. package/dist/services/seo/ensure/github-repos.js +271 -0
  286. package/dist/services/seo/index.js +55 -0
  287. package/dist/services/seo/lib/gh-api.js +161 -0
  288. package/dist/services/seo/templates/developer-tool/.github/workflows/maintenance.yml +25 -0
  289. package/dist/services/seo/templates/developer-tool/.nvmrc +1 -0
  290. package/dist/services/seo/templates/developer-tool/_README.md.js +121 -0
  291. package/dist/services/seo/templates/developer-tool/_package.json.js +33 -0
  292. package/dist/services/seo/templates/developer-tool/src/index.js +120 -0
  293. package/dist/services/seo/templates/index.js +57 -0
  294. package/dist/services/server/ensure/brands.js +63 -0
  295. package/dist/services/server/index.js +42 -0
  296. package/dist/services/testing/ensure/target-checks.js +97 -0
  297. package/dist/services/testing/index.js +11 -0
  298. package/dist/services/testing/lib/checks.js +467 -0
  299. package/dist/services/update/index.js +20 -0
  300. package/dist/services/update/lib/cache.js +59 -0
  301. package/dist/services/update/lib/fingerprint.js +157 -0
  302. package/dist/services/update/write/targets.js +206 -0
  303. package/dist/services/workspace/ensure/agents.js +42 -0
  304. package/dist/services/workspace/ensure/claude-settings.js +34 -0
  305. package/dist/services/workspace/ensure/config.js +54 -0
  306. package/dist/services/workspace/ensure/defaults.js +60 -0
  307. package/dist/services/workspace/ensure/env-keys.js +55 -0
  308. package/dist/services/workspace/ensure/env-order.js +66 -0
  309. package/dist/services/workspace/ensure/env-rules.js +97 -0
  310. package/dist/services/workspace/ensure/gitignore.js +18 -0
  311. package/dist/services/workspace/ensure/scripts.js +126 -0
  312. package/dist/services/workspace/ensure/structure.js +97 -0
  313. package/dist/services/workspace/ensure/translation-sdk.js +148 -0
  314. package/dist/services/workspace/ensure/workflows.js +54 -0
  315. package/dist/services/workspace/index.js +8 -0
  316. package/dist/vendor/account/engine.js +182 -0
  317. package/dist/vendor/account/features.js +220 -0
  318. package/dist/vendor/account/index.js +53 -0
  319. package/dist/vendor/account/schema.js +272 -0
  320. package/dist/vendor/account/subscription.js +38 -0
  321. package/dist/vendor/config/company.js +31 -0
  322. package/dist/vendor/config/defaults.js +173 -0
  323. package/dist/vendor/config/demo.js +18 -0
  324. package/dist/vendor/config/desktop-artifacts.js +110 -0
  325. package/dist/vendor/config/edit.js +769 -0
  326. package/dist/vendor/config/env-delivery.js +145 -0
  327. package/dist/vendor/config/env-rules.js +93 -0
  328. package/dist/vendor/config/env-schema.js +1078 -0
  329. package/dist/vendor/config/env.js +445 -0
  330. package/dist/vendor/config/hooks.js +97 -0
  331. package/dist/vendor/config/index.js +237 -0
  332. package/dist/vendor/config/instances.js +208 -0
  333. package/dist/vendor/config/load.js +490 -0
  334. package/dist/vendor/config/merge.js +68 -0
  335. package/dist/vendor/config/order.js +139 -0
  336. package/dist/vendor/config/ports.js +374 -0
  337. package/dist/vendor/config/providers.js +32 -0
  338. package/dist/vendor/config/repo.js +142 -0
  339. package/dist/vendor/config/retired-keys.js +430 -0
  340. package/dist/vendor/config/schema.js +1610 -0
  341. package/dist/vendor/config/secrets.js +50 -0
  342. package/dist/vendor/config/seed.js +34 -0
  343. package/dist/vendor/config/site-global.js +205 -0
  344. package/dist/vendor/config/validate.js +554 -0
  345. package/dist/vendor/config/winback.js +61 -0
  346. package/dist/vendor/devkit/attach-log-file.js +262 -0
  347. package/dist/vendor/devkit/certs.js +199 -0
  348. package/dist/vendor/devkit/ci-workflows.js +520 -0
  349. package/dist/vendor/devkit/cli-router.js +156 -0
  350. package/dist/vendor/devkit/command-path.js +46 -0
  351. package/dist/vendor/devkit/deploy-record.js +180 -0
  352. package/dist/vendor/devkit/flows.js +317 -0
  353. package/dist/vendor/devkit/local-https.js +360 -0
  354. package/dist/vendor/devkit/local.js +1905 -0
  355. package/dist/vendor/devkit/logger.js +128 -0
  356. package/dist/vendor/devkit/omega-bin.js +345 -0
  357. package/dist/vendor/devkit/prompt.js +187 -0
  358. package/dist/vendor/devkit/safe-install.js +18 -0
  359. package/dist/vendor/devkit/stop-signals.js +28 -0
  360. package/dist/vendor/devkit/test/scope.js +162 -0
  361. package/dist/vendor/devkit/translate/cache.js +84 -0
  362. package/dist/vendor/devkit/translate/engine.js +243 -0
  363. package/dist/vendor/devkit/translate/index.js +49 -0
  364. package/dist/vendor/devkit/translate/languages.js +134 -0
  365. package/dist/vendor/devkit/translate/providers.js +188 -0
  366. package/dist/vendor/devkit/update.js +569 -0
  367. package/docs/AGENTS.md +200 -0
  368. package/docs/account.md +45 -0
  369. package/docs/advertising.md +46 -0
  370. package/docs/ai.md +34 -0
  371. package/docs/analytics.md +69 -0
  372. package/docs/assets.md +57 -0
  373. package/docs/bookmark.md +26 -0
  374. package/docs/brand.md +151 -0
  375. package/docs/campaigns.md +96 -0
  376. package/docs/captcha.md +46 -0
  377. package/docs/certificates.md +68 -0
  378. package/docs/chat.md +47 -0
  379. package/docs/cloud.md +110 -0
  380. package/docs/company.md +64 -0
  381. package/docs/directory.md +140 -0
  382. package/docs/disperse.md +46 -0
  383. package/docs/domain.md +56 -0
  384. package/docs/edge.md +234 -0
  385. package/docs/email.md +42 -0
  386. package/docs/forms.md +48 -0
  387. package/docs/index.md +109 -0
  388. package/docs/migration.md +203 -0
  389. package/docs/migrations.md +56 -0
  390. package/docs/monitoring.md +41 -0
  391. package/docs/newsletter.md +48 -0
  392. package/docs/payment.md +83 -0
  393. package/docs/repo.md +48 -0
  394. package/docs/search.md +44 -0
  395. package/docs/seo.md +34 -0
  396. package/docs/server.md +37 -0
  397. package/docs/shared/agent-docs.md +89 -0
  398. package/docs/shared/analytics.md +612 -0
  399. package/docs/shared/brands.md +51 -0
  400. package/docs/shared/breaking-changes.md +497 -0
  401. package/docs/shared/config.md +1387 -0
  402. package/docs/shared/deploys.md +215 -0
  403. package/docs/shared/icons.md +201 -0
  404. package/docs/shared/local-dev.md +147 -0
  405. package/docs/shared/logging.md +202 -0
  406. package/docs/shared/monitoring.md +153 -0
  407. package/docs/shared/publishing.md +183 -0
  408. package/docs/shared/rulings.md +34 -0
  409. package/docs/shared/testing.md +147 -0
  410. package/docs/shared/theming.md +604 -0
  411. package/docs/shared/translation.md +291 -0
  412. package/docs/shared/updates.md +61 -0
  413. package/docs/testing.md +34 -0
  414. package/docs/update.md +36 -0
  415. package/docs/workspace.md +64 -0
  416. package/package.json +88 -0
@@ -0,0 +1,304 @@
1
+ /**
2
+ * Ensure DNS records match `edge.providers.cloudflare.dns` plus the platform record set
3
+ * (GitHub Pages, www, email-provider MX/SPF, DMARC; BIMI only when
4
+ * configured — see lib/dns-records-helpers.js).
5
+ *
6
+ * Subdomain projects only touch records belonging to the subdomain (apex
7
+ * records are owned by the parent brand).
8
+ *
9
+ * The whole SendGrid set is not config at all — this handler asks SendGrid
10
+ * before it diffs: `GET /v3/whitelabel/domains` for the `u<id>.<whitelabel>`
11
+ * host the domain-auth records are built from ([#692]), and
12
+ * `GET /v3/whitelabel/links` for whether `emailurl.<domain>` may be proxied
13
+ * yet ([#646]).
14
+ */
15
+ const chalk = require('chalk').default;
16
+ const { pollWithSpinner } = require('../../../vendor/devkit/flows.js');
17
+ const { cacheRead } = require('../lib/read-cache.js');
18
+ const { getZoneId, zoneGate } = require('../lib/ruleset-helper.js');
19
+ const { diffRecords } = require('../lib/dns-records-helpers.js');
20
+ const { SendGridAPI } = require('../../campaigns/lib/sendgrid-api.js');
21
+ const { canPrompt, dryRunPlan } = require('../../../lib/run-gates.js');
22
+
23
+ const LINK_BRANDING_PENDING_REASON = 'SendGrid has not validated the branded link — the emailurl CNAME stays unproxied';
24
+
25
+ module.exports = async function ensureDnsRecords(context) {
26
+ const { cloudflareApi: api, brandRoot, brandConfig, domain, isSubdomainProject, options = {} } = context;
27
+ const zoneId = getZoneId(context);
28
+ const gated = zoneGate(context, 'dnsRecords');
29
+ if (gated) return gated;
30
+
31
+ // === READ ===
32
+ const data = await api.makeRequest(`/zones/${zoneId}/dns_records`);
33
+ const records = data.result;
34
+ console.log(` ${chalk.green('✓')} Read ${chalk.dim(`(${records.length} items)`)}`);
35
+ cacheRead(brandRoot, 'dns-records', { count: records.length, records });
36
+
37
+ // === DIFF ===
38
+ // Tests inject a fake client via context.sendgridApi
39
+ const sendgridApi = context.sendgridApi || (process.env.SENDGRID_API_KEY ? new SendGridAPI() : null);
40
+ const sendgrid = await resolveDomainAuth(context, sendgridApi);
41
+ const { valid: linkBrandingValid, pending: linkBrandingPending } = await resolveLinkBranding(context, sendgridApi, sendgrid);
42
+ const diff = diffRecords({ records, brandConfig, domain, isSubdomainProject, sendgrid, linkBrandingValid });
43
+ if (!diff) {
44
+ console.log(` ${chalk.dim('⊘ No changes needed')}`);
45
+ if (linkBrandingPending) {
46
+ return { status: 'warned', reason: LINK_BRANDING_PENDING_REASON };
47
+ }
48
+ return;
49
+ }
50
+
51
+ if (options.dryRun) {
52
+ // The SendGrid host is READ, never written, so a dry run reads it too and
53
+ // names what it found — the plan covers the same records a real run does
54
+ // ([#692](https://github.com/Omega-JS-Stack/omega/issues/692)).
55
+ const sendgridPlan = sendgrid ? ` (SendGrid domain auth read live: u${sendgrid.id}.${sendgrid.whitelabel}.sendgrid.net)` : '';
56
+ dryRunPlan(`create ${diff.toCreate.length}, update ${diff.toUpdate.length}, delete ${diff.toDelete.length}${sendgridPlan}`);
57
+ return {
58
+ status: 'success',
59
+ output: { dns: { planned: { create: diff.toCreate.length, update: diff.toUpdate.length, delete: diff.toDelete.length } } },
60
+ };
61
+ }
62
+
63
+ // === WRITE ===
64
+ const output = { created: [], updated: [], deleted: [], errors: [] };
65
+
66
+ for (const record of diff.toCreate) {
67
+ try {
68
+ await api.makeRequest(`/zones/${zoneId}/dns_records`, {
69
+ method: 'POST',
70
+ body: JSON.stringify({
71
+ type: record.type,
72
+ name: record.name,
73
+ content: record.content,
74
+ ttl: record.ttl || 1,
75
+ proxied: record.proxied ?? false,
76
+ priority: record.priority,
77
+ comment: record.comment,
78
+ }),
79
+ });
80
+ output.created.push({ name: record.name, type: record.type });
81
+ console.log(` ${chalk.green('✓')} Created ${chalk.cyan(`${record.type} ${record.name}`)}`);
82
+ } catch (error) {
83
+ output.errors.push({ name: record.name, type: record.type, error: error.message });
84
+ console.error(` ${chalk.red('✗')} Failed to create ${chalk.cyan(`${record.type} ${record.name}`)}${chalk.dim(`: ${error.message}`)}`);
85
+ }
86
+ }
87
+
88
+ for (const record of diff.toUpdate) {
89
+ try {
90
+ await api.makeRequest(`/zones/${zoneId}/dns_records/${record.id}`, {
91
+ method: 'PATCH',
92
+ body: JSON.stringify({
93
+ type: record.type,
94
+ name: record.name,
95
+ content: record.content,
96
+ ttl: record.ttl,
97
+ proxied: record.proxied,
98
+ comment: record.comment,
99
+ priority: record.priority,
100
+ }),
101
+ });
102
+ output.updated.push({ name: record.name, type: record.type });
103
+ console.log(` ${chalk.green('✓')} Updated ${chalk.cyan(`${record.type} ${record.name}`)}`);
104
+ } catch (error) {
105
+ output.errors.push({ name: record.name, type: record.type, error: error.message });
106
+ console.error(` ${chalk.red('✗')} Failed to update ${chalk.cyan(`${record.type} ${record.name}`)}${chalk.dim(`: ${error.message}`)}`);
107
+ }
108
+ }
109
+
110
+ for (const record of diff.toDelete) {
111
+ try {
112
+ await api.makeRequest(`/zones/${zoneId}/dns_records/${record.id}`, { method: 'DELETE' });
113
+ output.deleted.push({ name: record.name, type: record.type, content: record.content });
114
+ console.log(` ${chalk.green('✓')} Deleted ${chalk.cyan(`${record.type} ${record.name}`)} (${chalk.dim(record.content)})`);
115
+ } catch (error) {
116
+ output.errors.push({ name: record.name, type: record.type, error: error.message });
117
+ console.error(` ${chalk.red('✗')} Failed to delete ${chalk.cyan(`${record.type} ${record.name}`)}${chalk.dim(`: ${error.message}`)}`);
118
+ }
119
+ }
120
+
121
+ const parts = [];
122
+ if (output.created.length > 0) parts.push(`${chalk.bold(output.created.length)} created`);
123
+ if (output.updated.length > 0) parts.push(`${chalk.bold(output.updated.length)} updated`);
124
+ if (output.deleted.length > 0) parts.push(`${chalk.bold(output.deleted.length)} deleted`);
125
+ if (output.errors.length > 0) parts.push(`${chalk.bold(output.errors.length)} errors`);
126
+ if (parts.length > 0) {
127
+ console.log(` Summary: ${parts.join(', ')}`);
128
+ }
129
+
130
+ const reasons = [];
131
+ if (output.errors.length > 0) {
132
+ reasons.push(`${output.errors.length} record(s) failed: ${output.errors.map((e) => e.name).join(', ')}`);
133
+ }
134
+ if (linkBrandingPending) {
135
+ reasons.push(LINK_BRANDING_PENDING_REASON);
136
+ }
137
+
138
+ return {
139
+ status: reasons.length > 0 ? 'warned' : 'success',
140
+ ...(reasons.length > 0 ? { reason: reasons.join('; ') } : {}),
141
+ output: { dns: output },
142
+ };
143
+ };
144
+
145
+ /**
146
+ * The `u<id>.<whitelabel>` host every SendGrid domain-auth record is built
147
+ * from, read LIVE off the account.
148
+ *
149
+ * These are SendGrid's OWN observed facts about the domain, not choices a
150
+ * brand makes, so they are referenced at the source instead of copied into
151
+ * config — a `dns.sendgrid` block drifts the moment the account
152
+ * re-authenticates the domain, and nothing ever wrote it back
153
+ * ([#692](https://github.com/Omega-JS-Stack/omega/issues/692)).
154
+ * `GET /v3/whitelabel/domains` answers with the authentication's own record
155
+ * set, whose `mail_cname` points at exactly the host the record builder
156
+ * rebuilds (`u<id>.<whitelabel>.sendgrid.net`).
157
+ *
158
+ * Every no-answer SKIPS the SendGrid records and says why: no key, no
159
+ * authentication for this domain (the campaigns service creates it), an
160
+ * unreachable API, or a host shape this parser does not know. A subdomain
161
+ * project has no SendGrid records to build at all — the apex record set
162
+ * belongs to the parent brand — so it never asks.
163
+ *
164
+ * @param {object} context - The service context
165
+ * @param {object|null} api - The SendGrid client (null without an API key)
166
+ * @returns {Promise<{ id: string, whitelabel: string }|null>}
167
+ */
168
+ async function resolveDomainAuth({ domain, isSubdomainProject }, api) {
169
+ if (isSubdomainProject) return null;
170
+
171
+ if (!api) {
172
+ console.log(` ${chalk.yellow('⚠')} SendGrid records skipped — no ${chalk.cyan('SENDGRID_API_KEY')} to read the domain authentication with`);
173
+ return null;
174
+ }
175
+
176
+ let domains;
177
+ try {
178
+ domains = await api.getAuthenticatedDomains();
179
+ } catch (error) {
180
+ console.log(` ${chalk.yellow('⚠')} SendGrid records skipped — could not read the domain authentication${chalk.dim(`: ${error.message}`)}`);
181
+ return null;
182
+ }
183
+
184
+ const auth = (domains || []).find((entry) => (entry.domain || '').toLowerCase() === domain.toLowerCase());
185
+ if (!auth) {
186
+ console.log(` ${chalk.yellow('⚠')} SendGrid records skipped — no authenticated domain for ${chalk.cyan(domain)} in SendGrid (the campaigns service creates it)`);
187
+ return null;
188
+ }
189
+
190
+ const host = auth.dns?.mail_cname?.data;
191
+ const parsed = /^u([^.]+)\.([^.]+)\.sendgrid\.net$/i.exec(host || '');
192
+ if (!parsed) {
193
+ console.log(` ${chalk.yellow('⚠')} SendGrid records skipped — domain-auth host ${chalk.cyan(host || '(none)')} is not ${chalk.dim('u<id>.<whitelabel>.sendgrid.net')}`);
194
+ return null;
195
+ }
196
+
197
+ return { id: parsed[1], whitelabel: parsed[2] };
198
+ }
199
+
200
+ /**
201
+ * Has SendGrid VALIDATED the branded link host for this brand's domain?
202
+ *
203
+ * SendGrid validates `emailurl.<domain>` by resolving it as a CNAME to
204
+ * sendgrid.net, and a Cloudflare-proxied record answers with the edge's own
205
+ * addresses instead — so proxying it BEFORE validation locks the branding out
206
+ * of ever validating, and every emailed link stays broken. This is the read
207
+ * that keeps the flip in order: grey-cloud until `GET /v3/whitelabel/links`
208
+ * reports `valid: true` for the host
209
+ * ([#646](https://github.com/Omega-JS-Stack/omega/issues/646)).
210
+ *
211
+ * An interactive run WAITS for that answer rather than leaving the flip to a
212
+ * later walk ([#662](https://github.com/Omega-JS-Stack/omega/issues/662)):
213
+ * `pollWithSpinner` re-asks SendGrid until the branding validates and the
214
+ * diff below proxies the CNAME in this same walk. Skipping the wait (or a
215
+ * non-interactive run) keeps the record grey and the rerun message.
216
+ *
217
+ * An unreachable SendGrid answers NO: the unproxied record is the safe half of
218
+ * the pair (a plain-HTTP hop, which is where every brand already is), while a
219
+ * wrong YES is unrecoverable without a hand edit. No API key never reaches
220
+ * here — the domain-auth read skipped the whole set first.
221
+ *
222
+ * NO ENTRY is the one NO that is not pending: the campaigns service creates
223
+ * the branding later in this same walk
224
+ * ([#693](https://github.com/Omega-JS-Stack/omega/issues/693)), so waiting on
225
+ * it here would never end. That case says so and moves on.
226
+ *
227
+ * @param {object} context - The service context
228
+ * @param {object|null} api - The SendGrid client (null without an API key)
229
+ * @param {object|null} sendgrid - The live domain-auth values, when there are
230
+ * SendGrid records to gate at all
231
+ * @returns {Promise<{ valid: boolean, pending: boolean }>} - `pending` is a
232
+ * live branding that did not validate: the warned reason's condition.
233
+ */
234
+ async function resolveLinkBranding(context, api, sendgrid) {
235
+ const { domain, options = {} } = context;
236
+
237
+ // No SendGrid records to gate: the domain-auth read found none (and already
238
+ // said why), or a subdomain project, whose record set stops at the GitHub
239
+ // Pages pair (the apex belongs to the parent).
240
+ if (!sendgrid) {
241
+ return { valid: false, pending: false };
242
+ }
243
+
244
+ const host = `emailurl.${domain}`;
245
+
246
+ const branding = await readBrandedLink(api, host, true);
247
+
248
+ // Nothing to WAIT for HERE: the account has no link branding for this host
249
+ // yet, and the campaigns service creates it LATER IN THIS SAME WALK
250
+ // ([#693](https://github.com/Omega-JS-Stack/omega/issues/693)) — waiting on
251
+ // it before that would never end. The record still lands (grey), which is
252
+ // exactly the state SendGrid validates against.
253
+ if (branding === null) {
254
+ console.log(` ${chalk.yellow('⚠')} ${chalk.cyan(`CNAME ${host}`)} stays unproxied — SendGrid has no link branding for that host yet. The campaigns service creates and validates it later in this run (when campaigns is enabled for this brand), then flips ${chalk.cyan(`CNAME ${host}`)} to proxied.`);
255
+ return { valid: false, pending: false };
256
+ }
257
+
258
+ let valid = branding?.valid === true;
259
+
260
+ // Wait it out: SendGrid validates minutes after the CNAME lands, and the
261
+ // proxy flip belongs to THIS walk (#662). Ticks stay quiet — the first read
262
+ // already reported an unreachable API.
263
+ if (!valid && canPrompt(options)) {
264
+ const result = await pollWithSpinner({
265
+ check: async () => ((await readBrandedLink(api, host))?.valid === true ? { done: true } : { done: false }),
266
+ intervalMs: 10000,
267
+ message: `Waiting for SendGrid to validate ${host}`,
268
+ indent: ' ',
269
+ });
270
+ valid = result.success;
271
+ }
272
+
273
+ if (!valid) {
274
+ console.log(` ${chalk.yellow('⚠')} ${chalk.cyan(`CNAME ${host}`)} stays unproxied — SendGrid has not validated the branded link. Rerun ${chalk.cyan('omega manage')} once it has, and the record flips to proxied so emailed links land on HTTPS.`);
275
+ }
276
+
277
+ return { valid, pending: !valid };
278
+ }
279
+
280
+ /**
281
+ * One read of SendGrid's branded-link list: this host's entry.
282
+ *
283
+ * The caller needs all three answers apart, because only ONE of them is worth
284
+ * waiting on: an entry that exists and is not valid yet is minutes away, while
285
+ * a missing entry arrives from the campaigns service later in the walk, not
286
+ * from any wait here.
287
+ *
288
+ * @param {object} api - The SendGrid client
289
+ * @param {string} host - The branded link host (`emailurl.<domain>`)
290
+ * @param {boolean} [logError] - Report an unreachable API (the first read only)
291
+ * @returns {Promise<object|null|undefined>} The branding entry; `null` when the
292
+ * account has none for this host; `undefined` when the read itself failed.
293
+ */
294
+ async function readBrandedLink(api, host, logError = false) {
295
+ try {
296
+ const links = await api.getBrandedLinks();
297
+ return (links || []).find((link) => `${link.subdomain}.${link.domain}`.toLowerCase() === host.toLowerCase()) || null;
298
+ } catch (error) {
299
+ if (logError) {
300
+ console.log(` ${chalk.yellow('⚠')} Could not read SendGrid link branding${chalk.dim(`: ${error.message}`)}`);
301
+ }
302
+ return undefined;
303
+ }
304
+ }
@@ -0,0 +1,292 @@
1
+ /**
2
+ * Ensure Cloudflare Email Routing is enabled and forwarding rules are synced.
3
+ *
4
+ * Only runs when domain.email.providers names cloudflare; rules come from
5
+ * domain.email.forwarding [{ from: 'support' | '*', to: 'inbox@…' }].
6
+ *
7
+ * Unverified destination addresses: a verification email is sent; interactive
8
+ * runs open the dashboard and retry the rule write until the address verifies,
9
+ * non-interactive/dry runs return warned with the dashboard URL — rerun after
10
+ * verifying.
11
+ */
12
+ const chalk = require('chalk').default;
13
+ const { openBrowserAndPoll } = require('../../../vendor/devkit/flows.js');
14
+ const { cacheRead } = require('../lib/read-cache.js');
15
+ const { getZoneId, zoneGate } = require('../lib/ruleset-helper.js');
16
+ const { resolveEmailProvider } = require('../../domain/lib/registrars.js');
17
+ const { canPrompt, dryRunPlan } = require('../../../lib/run-gates.js');
18
+
19
+ function isUnverifiedError(error) {
20
+ return error.message.includes('2054') || error.message.toLowerCase().includes('not verified');
21
+ }
22
+
23
+ async function getAccountId(api, zoneId) {
24
+ const zone = await api.makeRequest(`/zones/${zoneId}`);
25
+ return zone.result.account.id;
26
+ }
27
+
28
+ /**
29
+ * Send a verification email for the destination; interactive runs then open
30
+ * the dashboard and retry the rule write until the address verifies.
31
+ *
32
+ * @returns {boolean} - true when the rule landed after verification
33
+ */
34
+ async function handleUnverified(api, zoneId, destination, retryWrite, options = {}) {
35
+ console.log(` ${chalk.yellow('⚠')} Destination ${chalk.cyan(destination)} is not verified in Cloudflare`);
36
+
37
+ const accountId = await getAccountId(api, zoneId);
38
+ try {
39
+ await api.makeRequest(`/accounts/${accountId}/email/routing/addresses`, {
40
+ method: 'POST',
41
+ body: JSON.stringify({ email: destination }),
42
+ });
43
+ console.log(` ${chalk.dim('→')} Verification email sent to ${chalk.cyan(destination)}`);
44
+ } catch (verifyError) {
45
+ if (verifyError.message.includes('already exists')) {
46
+ console.log(` ${chalk.dim('→')} Verification already pending for ${chalk.cyan(destination)}`);
47
+ } else {
48
+ console.log(` ${chalk.yellow('⚠')} Could not send verification email${chalk.dim(`: ${verifyError.message}`)}`);
49
+ }
50
+ }
51
+
52
+ const dashboardUrl = `https://dash.cloudflare.com/${accountId}/email-routing/destination-addresses`;
53
+
54
+ if (canPrompt(options)) {
55
+ const result = await openBrowserAndPoll({
56
+ url: dashboardUrl,
57
+ label: 'the Cloudflare destination-addresses page',
58
+ promptMessage: `Verify ${chalk.cyan(destination)} in Cloudflare Email Routing (check the inbox).`,
59
+ waitMessage: 'Checking verification',
60
+ indent: ' ',
61
+ check: async () => {
62
+ try {
63
+ await retryWrite();
64
+ return { done: true };
65
+ } catch (error) {
66
+ if (isUnverifiedError(error)) {
67
+ return { done: false };
68
+ }
69
+ return { done: true, error: error.message };
70
+ }
71
+ },
72
+ });
73
+
74
+ if (result.success) {
75
+ console.log(` ${chalk.green('✓')} Verified — rule created`);
76
+ return true;
77
+ }
78
+ if (result.error) {
79
+ console.log(` ${chalk.yellow('⚠')} Rule write failed${chalk.dim(`: ${result.error}`)}`);
80
+ }
81
+ }
82
+
83
+ console.log(` ${chalk.dim(`Verify at: ${dashboardUrl} — then rerun`)}`);
84
+ return false;
85
+ }
86
+
87
+ module.exports = async function ensureEmailRouting(context) {
88
+ const { cloudflareApi: api, brandRoot, domain, brandConfig, options = {} } = context;
89
+ const zoneId = getZoneId(context);
90
+ const gated = zoneGate(context, 'emailRouting');
91
+ if (gated) return gated;
92
+ const emailConfig = brandConfig.domain?.email;
93
+
94
+ if (resolveEmailProvider(brandConfig) !== 'cloudflare') {
95
+ console.log(` ${chalk.dim('⊘ Email provider is not cloudflare — nothing to route')}`);
96
+ return;
97
+ }
98
+
99
+ // === READ: routing status ===
100
+ let routing = null;
101
+ try {
102
+ const resp = await api.makeRequest(`/zones/${zoneId}/email/routing`);
103
+ routing = resp.result;
104
+ } catch {
105
+ if (options.dryRun) {
106
+ return dryRunPlan('enable email routing', { status: 'success', output: { emailRouting: { planned: 'enable' } } });
107
+ }
108
+ console.log(` ${chalk.dim('→')} Enabling email routing...`);
109
+ await api.makeRequest(`/zones/${zoneId}/email/routing/enable`, { method: 'POST' });
110
+ const resp = await api.makeRequest(`/zones/${zoneId}/email/routing`);
111
+ routing = resp.result;
112
+ }
113
+
114
+ if (routing?.enabled) {
115
+ console.log(` ${chalk.green('✓')} Email routing enabled`);
116
+ } else {
117
+ console.log(` ${chalk.yellow('⚠')} Email routing not yet active (may need DNS propagation)`);
118
+ }
119
+
120
+ const desiredRules = emailConfig.forwarding || [];
121
+ if (desiredRules.length === 0) {
122
+ console.log(` ${chalk.dim('→')} No forwarding rules configured`);
123
+ return { output: { emailRouting: { enabled: true, rules: 0 } } };
124
+ }
125
+
126
+ // Split catch-all from regular rules — Cloudflare uses a separate endpoint for catch-all
127
+ const desiredCatchAll = desiredRules.find((r) => r.from === '*');
128
+ const desiredRegular = desiredRules.filter((r) => r.from !== '*');
129
+
130
+ const existingResp = await api.makeRequest(`/zones/${zoneId}/email/routing/rules`);
131
+ const existingRules = existingResp.result || [];
132
+ cacheRead(brandRoot, 'email-routing', { routing, rules: existingRules });
133
+
134
+ let created = 0;
135
+ let updated = 0;
136
+ let skipped = 0;
137
+ let deleted = 0;
138
+ let unverified = 0;
139
+ let planned = 0;
140
+
141
+ // --- Catch-all rule (separate API endpoint) ---
142
+ if (desiredCatchAll) {
143
+ const catchAllResp = await api.makeRequest(`/zones/${zoneId}/email/routing/rules/catch_all`);
144
+ const currentCatchAll = catchAllResp.result;
145
+ const currentAction = currentCatchAll?.actions?.[0];
146
+ const alreadyCorrect = currentCatchAll?.enabled
147
+ && currentAction?.type === 'forward'
148
+ && currentAction?.value?.[0] === desiredCatchAll.to;
149
+
150
+ if (alreadyCorrect) {
151
+ console.log(` ${chalk.green('✓')} ${chalk.cyan('*')} → ${chalk.dim(desiredCatchAll.to)}`);
152
+ skipped++;
153
+ } else if (options.dryRun) {
154
+ dryRunPlan(`set catch-all → ${desiredCatchAll.to}`);
155
+ planned++;
156
+ } else {
157
+ console.log(` ${chalk.dim('→')} Updating catch-all: ${chalk.cyan('*')} → ${chalk.dim(desiredCatchAll.to)}`);
158
+ const putCatchAll = () => api.makeRequest(`/zones/${zoneId}/email/routing/rules/catch_all`, {
159
+ method: 'PUT',
160
+ body: JSON.stringify({
161
+ matchers: [{ type: 'all' }],
162
+ actions: [{ type: 'forward', value: [desiredCatchAll.to] }],
163
+ enabled: true,
164
+ name: 'Catch-all',
165
+ }),
166
+ });
167
+ try {
168
+ await putCatchAll();
169
+ console.log(` ${chalk.green('✓')} Updated`);
170
+ updated++;
171
+ } catch (error) {
172
+ if (!isUnverifiedError(error)) throw error;
173
+ const resolved = await handleUnverified(api, zoneId, desiredCatchAll.to, putCatchAll, options);
174
+ if (resolved) {
175
+ updated++;
176
+ } else {
177
+ unverified++;
178
+ }
179
+ }
180
+ }
181
+ }
182
+
183
+ // --- Regular rules ---
184
+ for (const desired of desiredRegular) {
185
+ const matcherValue = `${desired.from}@${domain}`;
186
+
187
+ const exists = existingRules.find((rule) => {
188
+ if (!rule.matchers || !rule.actions) return false;
189
+ const matcher = rule.matchers[0];
190
+ const action = rule.actions[0];
191
+ return matcher.type === 'literal'
192
+ && matcher.value === matcherValue
193
+ && action.value?.[0] === desired.to;
194
+ });
195
+
196
+ if (exists) {
197
+ console.log(` ${chalk.green('✓')} ${chalk.cyan(matcherValue)} → ${chalk.dim(desired.to)}`);
198
+ skipped++;
199
+ continue;
200
+ }
201
+
202
+ if (options.dryRun) {
203
+ dryRunPlan(`create ${matcherValue} → ${desired.to}`);
204
+ planned++;
205
+ continue;
206
+ }
207
+
208
+ console.log(` ${chalk.dim('→')} Creating rule: ${chalk.cyan(matcherValue)} → ${chalk.dim(desired.to)}`);
209
+
210
+ const postRule = () => api.makeRequest(`/zones/${zoneId}/email/routing/rules`, {
211
+ method: 'POST',
212
+ body: JSON.stringify({
213
+ matchers: [{ type: 'literal', field: 'to', value: matcherValue }],
214
+ actions: [{ type: 'forward', value: [desired.to] }],
215
+ enabled: true,
216
+ name: `Forward ${matcherValue}`,
217
+ }),
218
+ });
219
+
220
+ try {
221
+ await postRule();
222
+ console.log(` ${chalk.green('✓')} Created`);
223
+ created++;
224
+ } catch (error) {
225
+ if (!isUnverifiedError(error)) throw error;
226
+ const resolved = await handleUnverified(api, zoneId, desired.to, postRule, options);
227
+ if (resolved) {
228
+ created++;
229
+ } else {
230
+ unverified++;
231
+ }
232
+ }
233
+ }
234
+
235
+ // Delete regular rules in Cloudflare that aren't in config (skip catch-all — managed separately)
236
+ for (const existing of existingRules) {
237
+ if (!existing.matchers || !existing.actions || !existing.enabled) continue;
238
+
239
+ const matcher = existing.matchers[0];
240
+ const action = existing.actions[0];
241
+ if (action?.type !== 'forward') continue;
242
+ if (matcher.type === 'all') continue;
243
+
244
+ const isDesired = desiredRegular.some((desired) =>
245
+ matcher.type === 'literal'
246
+ && matcher.value === `${desired.from}@${domain}`
247
+ && action.value?.[0] === desired.to,
248
+ );
249
+
250
+ if (!isDesired) {
251
+ if (options.dryRun) {
252
+ dryRunPlan(`remove ${matcher.value} → ${action.value?.[0]}`);
253
+ planned++;
254
+ continue;
255
+ }
256
+ console.log(` ${chalk.dim('→')} Removing rule: ${chalk.cyan(matcher.value)} → ${chalk.dim(action.value?.[0])}`);
257
+ await api.makeRequest(`/zones/${zoneId}/email/routing/rules/${existing.tag}`, { method: 'DELETE' });
258
+ console.log(` ${chalk.green('✓')} Removed`);
259
+ deleted++;
260
+ }
261
+ }
262
+
263
+ // Disable catch-all if not in config but currently active
264
+ if (!desiredCatchAll) {
265
+ const catchAllResp = await api.makeRequest(`/zones/${zoneId}/email/routing/rules/catch_all`);
266
+ const currentCatchAll = catchAllResp.result;
267
+ if (currentCatchAll?.enabled && currentCatchAll?.actions?.[0]?.type === 'forward') {
268
+ if (options.dryRun) {
269
+ dryRunPlan('disable catch-all (not in config)');
270
+ planned++;
271
+ } else {
272
+ console.log(` ${chalk.dim('→')} Disabling catch-all (not in config)`);
273
+ await api.makeRequest(`/zones/${zoneId}/email/routing/rules/catch_all`, {
274
+ method: 'PUT',
275
+ body: JSON.stringify({
276
+ matchers: [{ type: 'all' }],
277
+ actions: [{ type: 'drop' }],
278
+ enabled: false,
279
+ }),
280
+ });
281
+ console.log(` ${chalk.green('✓')} Disabled`);
282
+ deleted++;
283
+ }
284
+ }
285
+ }
286
+
287
+ return {
288
+ status: unverified > 0 ? 'warned' : 'success',
289
+ ...(unverified > 0 ? { reason: `${unverified} destination address(es) unverified` } : {}),
290
+ output: { emailRouting: { enabled: true, created, updated, skipped, deleted, unverified, ...(planned > 0 && { planned }) } },
291
+ };
292
+ };