@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,79 @@
1
+ /**
2
+ * Campaigns service (SendGrid provider) — the brand's email-marketing infrastructure: domain
3
+ * authentication (DKIM/SPF CNAMEs written via Cloudflare), link branding for
4
+ * the emailurl host (validated, then the CNAME flips proxied), a verified sender
5
+ * for Single Sends, the brand's marketing list, the unsubscribe groups
6
+ * @omega.js/backend sends through, @omega.js/backend's custom fields and
7
+ * segments (from @omega.js/backend's SSOT), and the account-global Event
8
+ * Webhook pointed at the parent @omega.js/backend's forwarder.
9
+ *
10
+ * Runs after cloudflare (the zone must exist for the DKIM records). The
11
+ * whole service reconciles one SendGrid account per brand-or-company — the
12
+ * groups/fields/segments/webhook operations converge to the same result when
13
+ * sibling brands share the account.
14
+ *
15
+ * Auth: SENDGRID_API_KEY in the brand .env; the event-webhook operation
16
+ * additionally needs OMEGA_WEBHOOK_KEY, which the setup MINTS through the
17
+ * shared contract (it is OMEGA's own key, #635). No API key → clean skip.
18
+ */
19
+ const { chosenProvider } = require('../../vendor/config/index.js');
20
+
21
+ const { serviceInputSpec } = require('../../config.js');
22
+ const { createServiceRunner } = require('../../lib/service-runner.js');
23
+ const { requestServiceInput } = require('../../lib/service-input.js');
24
+ const { CloudflareAPI } = require('../edge/lib/cloudflare-api.js');
25
+ const { getApexDomain } = require('../../lib/domain-utils.js');
26
+ const { SendGridAPI } = require('./lib/sendgrid-api.js');
27
+
28
+ module.exports.run = createServiceRunner({
29
+ serviceDir: __dirname,
30
+ setup: async (context) => {
31
+ const campaigns = context.brandConfig.marketing?.campaigns || {};
32
+
33
+ if (campaigns.enabled === false) {
34
+ return { skip: true, reason: 'marketing.campaigns.enabled = false' };
35
+ }
36
+
37
+ // The vendor is a KEY under marketing.campaigns.providers (#425) — no
38
+ // entry means none chosen and this service skips.
39
+ const provider = chosenProvider(campaigns.providers);
40
+ if (!provider) {
41
+ return { skip: true, reason: 'no marketing.campaigns.providers entry' };
42
+ }
43
+ if (provider !== 'sendgrid') {
44
+ return { skip: true, reason: `marketing.campaigns.providers.${provider} is not a known email-marketing provider` };
45
+ }
46
+
47
+ const domain = (context.brandConfig.brand?.url || '').replace(/^https?:\/\//, '').replace(/\/$/, '');
48
+ if (!domain) {
49
+ return { skip: true, reason: 'no brand.url configured' };
50
+ }
51
+
52
+ if (!context.sendgridApi) {
53
+ const gate = await requestServiceInput(context, serviceInputSpec('campaigns', { names: ['SENDGRID_API_KEY'] }));
54
+ if (gate) return gate;
55
+ }
56
+
57
+ // The webhook key is OMEGA's OWN (#635): the shared helper mints it in
58
+ // place when this service runs before the workspace one did, so the
59
+ // webhook operation can read it unconditionally.
60
+ const webhookKeyGate = await requestServiceInput(context, serviceInputSpec('campaigns', { names: ['OMEGA_WEBHOOK_KEY'] }));
61
+ if (webhookKeyGate) return webhookKeyGate;
62
+
63
+ // Tests inject fake clients via context.sendgridApi / context.cloudflareApi.
64
+ // Cloudflare is only needed while the domain still has to be DNS-verified
65
+ // (and for the link-branding proxy flip); without a token the domain-auth
66
+ // operation prints the records to add manually and link-branding prints the
67
+ // manual flip.
68
+ return {
69
+ sendgridApi: context.sendgridApi || new SendGridAPI(),
70
+ cloudflareApi: context.cloudflareApi
71
+ || (process.env.CLOUDFLARE_TOKEN ? new CloudflareAPI() : null),
72
+ domain,
73
+ apexDomain: getApexDomain(domain),
74
+ // A subdomain project's apex records belong to the PARENT brand's walk
75
+ // (the same line the edge service draws) — link-branding skips on it.
76
+ isSubdomainProject: domain !== getApexDomain(domain),
77
+ };
78
+ },
79
+ });
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Diff-sync a SendGrid record set into the brand's Cloudflare apex zone — the
3
+ * ONE writer both whitelabel operations share (domain authentication's DKIM
4
+ * CNAMEs, link branding's two link CNAMEs).
5
+ *
6
+ * Exact match = untouched, wrong content = patched, missing = created. Every
7
+ * record lands UNPROXIED: SendGrid validates each of them by resolving the
8
+ * host as a plain CNAME, and a proxied record answers with Cloudflare's own
9
+ * addresses instead. No Cloudflare token (or no zone yet) prints the records
10
+ * to add by hand, so a manual fix converges on the rerun.
11
+ */
12
+ const chalk = require('chalk').default;
13
+
14
+ /**
15
+ * @param {object|null} cloudflareApi - The Cloudflare client (null without a token)
16
+ * @param {string} apexDomain - The zone the records belong to
17
+ * @param {Array<{ key: string, record: object }>} records - SendGrid's own
18
+ * record set, each entry keyed by the name SendGrid gives it
19
+ * @returns {Promise<boolean>} - true when records were written via Cloudflare,
20
+ * false when they were printed for manual addition
21
+ */
22
+ async function syncDnsRecords(cloudflareApi, apexDomain, records) {
23
+ if (!cloudflareApi) {
24
+ console.log(` ${chalk.yellow('⚠')} No CLOUDFLARE_TOKEN — add these records manually, then rerun:`);
25
+ logManualRecords(records);
26
+ return false;
27
+ }
28
+
29
+ const zone = await cloudflareApi.getZoneByName(apexDomain);
30
+ if (!zone) {
31
+ console.log(` ${chalk.yellow('⚠')} No Cloudflare zone for ${chalk.cyan(apexDomain)} — add these records manually (or rerun once the cloudflare service creates the zone):`);
32
+ logManualRecords(records);
33
+ return false;
34
+ }
35
+
36
+ const existing = await cloudflareApi.makeRequest(`/zones/${zone.id}/dns_records?type=CNAME&per_page=100`, { method: 'GET' });
37
+ const existingRecords = existing.result || [];
38
+
39
+ for (const { key, record } of records) {
40
+ const type = record.type.toUpperCase();
41
+ const name = record.host;
42
+ const content = record.data;
43
+
44
+ const match = existingRecords.find((r) => r.type === type && r.name === name);
45
+
46
+ if (match && match.content === content) {
47
+ console.log(` ${chalk.dim('→')} ${chalk.dim(key)}: ${chalk.cyan(name)} already in place`);
48
+ continue;
49
+ }
50
+
51
+ if (match) {
52
+ await cloudflareApi.makeRequest(`/zones/${zone.id}/dns_records/${match.id}`, {
53
+ method: 'PATCH',
54
+ body: JSON.stringify({ type, name, content, ttl: 1, proxied: false }),
55
+ });
56
+ console.log(` ${chalk.yellow('↻')} ${chalk.dim(key)}: updated ${chalk.cyan(name)}`);
57
+ continue;
58
+ }
59
+
60
+ await cloudflareApi.makeRequest(`/zones/${zone.id}/dns_records`, {
61
+ method: 'POST',
62
+ body: JSON.stringify({ type, name, content, ttl: 1, proxied: false, comment: `SendGrid ${key}` }),
63
+ });
64
+ console.log(` ${chalk.green('✓')} ${chalk.dim(key)}: created ${chalk.cyan(name)}`);
65
+ }
66
+
67
+ return true;
68
+ }
69
+
70
+ function logManualRecords(records) {
71
+ for (const { record } of records) {
72
+ console.log(` ${chalk.cyan(record.type.toUpperCase())} ${record.host} → ${record.data}`);
73
+ }
74
+ }
75
+
76
+ /** The manualRecords shape a warned return carries when nothing was written. */
77
+ function manualRecordList(records) {
78
+ return records.map(({ record }) => ({ type: record.type.toUpperCase(), name: record.host, content: record.data }));
79
+ }
80
+
81
+ module.exports = { syncDnsRecords, manualRecordList };
@@ -0,0 +1,138 @@
1
+ /**
2
+ * @omega.js/backend segment conditions → SendGrid query_dsl (the SQL-like segment query
3
+ * language). Pure functions, ported behavior-for-behavior from omega-manager
4
+ * — the generated SQL is compared against live segments for staleness, so
5
+ * any change here re-syncs every segment on the next run.
6
+ */
7
+ const { BACKEND_FIELDS_MAP } = require('../../../lib/backend-marketing.js');
8
+
9
+ /**
10
+ * Convert a @omega.js/backend field condition to a WHERE clause fragment. Custom fields
11
+ * are referenced by name (double-quoted) in query_dsl, not by internal ID.
12
+ */
13
+ function fieldConditionToWhere(condition) {
14
+ const fieldDef = BACKEND_FIELDS_MAP[condition.field];
15
+ if (!fieldDef) {
16
+ throw new Error(`Unknown field "${condition.field}" in segment condition`);
17
+ }
18
+
19
+ const col = `"${condition.field}"`;
20
+
21
+ switch (condition.op) {
22
+ case '==':
23
+ return `${col} = '${condition.value}'`;
24
+ case '!=':
25
+ return `${col} != '${condition.value}'`;
26
+ case 'within': {
27
+ const days = parseInt(condition.value, 10);
28
+ return `${col} > timestampadd(day, -${days}, CURRENT_TIMESTAMP)`;
29
+ }
30
+ case 'not_within': {
31
+ const days = parseInt(condition.value, 10);
32
+ return `${col} <= timestampadd(day, -${days}, CURRENT_TIMESTAMP)`;
33
+ }
34
+ default:
35
+ throw new Error(`Unsupported field condition op "${condition.op}"`);
36
+ }
37
+ }
38
+
39
+ /** Contact conditions use built-in contact_data columns (email, first_name, …). */
40
+ function contactConditionToWhere(condition) {
41
+ switch (condition.op) {
42
+ case 'email_is':
43
+ return `email = '${condition.value}'`;
44
+ case 'email_like':
45
+ return `email LIKE '${condition.value}'`;
46
+ default:
47
+ throw new Error(`Unsupported contact condition op "${condition.op}"`);
48
+ }
49
+ }
50
+
51
+ function conditionToWhere(condition) {
52
+ if (condition.type === 'engagement') {
53
+ return null; // Handled separately in buildQueryDsl
54
+ }
55
+ if (condition.type === 'contact') {
56
+ return contactConditionToWhere(condition);
57
+ }
58
+ return fieldConditionToWhere(condition);
59
+ }
60
+
61
+ /**
62
+ * Build a complete query_dsl from @omega.js/backend segment conditions. Engagement
63
+ * conditions need event_data (JOIN or subqueries); field/contact conditions
64
+ * query contact_data directly.
65
+ */
66
+ function buildQueryDsl(conditions, logic) {
67
+ const engagementConditions = conditions.filter((c) => c.type === 'engagement');
68
+ const nonEngagementConditions = conditions.filter((c) => c.type !== 'engagement');
69
+ const joiner = logic === 'or' ? ' OR ' : ' AND ';
70
+
71
+ const fieldWhereParts = nonEngagementConditions.map((c) => conditionToWhere(c)).filter(Boolean);
72
+
73
+ // No engagement — simple contact_data query
74
+ if (engagementConditions.length === 0) {
75
+ const whereClause = fieldWhereParts.length > 0
76
+ ? ` WHERE ${fieldWhereParts.join(joiner)}`
77
+ : '';
78
+ return `SELECT contact_id, updated_at FROM contact_data${whereClause}`;
79
+ }
80
+
81
+ // Positive engagement (opened_or_clicked) uses a JOIN
82
+ const positiveEngagement = engagementConditions.find((c) => c.op === 'opened_or_clicked');
83
+ if (positiveEngagement) {
84
+ const days = parseInt(positiveEngagement.value, 10);
85
+ const threshold = `timestampadd(day, -${days}, CURRENT_TIMESTAMP)`;
86
+ const engWhere = `(e.event_type = 'open' OR e.event_type = 'click') AND e.timestamp > ${threshold}`;
87
+ const allWhere = [...fieldWhereParts, engWhere];
88
+ return `SELECT c.contact_id, c.updated_at FROM contact_data c`
89
+ + ` JOIN event_data e ON c.contact_id = e.contact_id`
90
+ + ` WHERE ${allWhere.join(joiner)}`;
91
+ }
92
+
93
+ // Negative/count engagement conditions use subqueries (combinable)
94
+ const engagementWhereParts = engagementConditions.map((engagement) => {
95
+ switch (engagement.op) {
96
+ case 'not_opened': {
97
+ const days = parseInt(engagement.value, 10);
98
+ const threshold = `timestampadd(day, -${days}, CURRENT_TIMESTAMP)`;
99
+ return `contact_id NOT IN (`
100
+ + `SELECT contact_id FROM event_data WHERE event_type = 'open' AND timestamp > ${threshold}`
101
+ + `)`;
102
+ }
103
+ case 'not_opened_or_clicked': {
104
+ const days = parseInt(engagement.value, 10);
105
+ const threshold = `timestampadd(day, -${days}, CURRENT_TIMESTAMP)`;
106
+ return `contact_id NOT IN (`
107
+ + `SELECT contact_id FROM event_data WHERE (event_type = 'open' OR event_type = 'click') AND timestamp > ${threshold}`
108
+ + `)`;
109
+ }
110
+ case 'received_gte': {
111
+ // SendGrid has no GROUP BY / HAVING — approximate >=N delivered events
112
+ // by requiring one in each of N distinct time windows. The window size
113
+ // comes from the sibling time-based engagement condition's period.
114
+ const minCount = parseInt(engagement.value, 10);
115
+ const timeEng = engagementConditions.find((c) => c.op !== 'received_gte');
116
+ const totalDays = timeEng ? parseInt(timeEng.value, 10) : 180;
117
+ const windowSize = Math.floor(totalDays / minCount);
118
+
119
+ return '(' + Array.from({ length: minCount }, (_, i) => {
120
+ const start = i * windowSize;
121
+ const end = (i + 1) * windowSize;
122
+ return `contact_id IN (`
123
+ + `SELECT contact_id FROM event_data WHERE event_type = 'delivered'`
124
+ + ` AND timestamp > timestampadd(day, -${end}, CURRENT_TIMESTAMP)`
125
+ + ` AND timestamp <= timestampadd(day, -${start}, CURRENT_TIMESTAMP)`
126
+ + `)`;
127
+ }).join(' AND ') + ')';
128
+ }
129
+ default:
130
+ throw new Error(`Unsupported engagement op "${engagement.op}"`);
131
+ }
132
+ });
133
+
134
+ const allWhere = [...fieldWhereParts, ...engagementWhereParts];
135
+ return `SELECT contact_id, updated_at FROM contact_data WHERE ${allWhere.join(joiner)}`;
136
+ }
137
+
138
+ module.exports = { buildQueryDsl };
@@ -0,0 +1,287 @@
1
+ /**
2
+ * SendGrid v3 API client — marketing (lists, custom fields, segments),
3
+ * unsubscribe groups, verified senders, domain authentication, and the
4
+ * account-global Event Webhook. Named-method surface so tests can fake it
5
+ * method-for-method.
6
+ *
7
+ * Auth: SENDGRID_API_KEY in the brand .env.
8
+ */
9
+ const SENDGRID_API_BASE = 'https://api.sendgrid.com/v3';
10
+
11
+ class SendGridAPI {
12
+ constructor(options = {}) {
13
+ this.apiKey = options.apiKey || process.env.SENDGRID_API_KEY;
14
+ }
15
+
16
+ async makeRequest(endpoint, options = {}) {
17
+ const response = await fetch(`${SENDGRID_API_BASE}${endpoint}`, {
18
+ ...options,
19
+ headers: {
20
+ 'Authorization': `Bearer ${this.apiKey}`,
21
+ 'Content-Type': 'application/json',
22
+ ...options.headers,
23
+ },
24
+ });
25
+
26
+ // 204 No Content / 202 Accepted come back bodyless
27
+ if (response.status === 204 || response.status === 202) {
28
+ return null;
29
+ }
30
+
31
+ const text = await response.text();
32
+ if (!text) {
33
+ if (!response.ok) {
34
+ throw new Error(`SendGrid API error (${response.status}): ${response.statusText}`);
35
+ }
36
+ return null;
37
+ }
38
+
39
+ const data = JSON.parse(text);
40
+
41
+ if (!response.ok) {
42
+ const message = data.errors?.map((e) => e.message).join(', ')
43
+ || data.message
44
+ || data.error
45
+ || response.statusText;
46
+ throw new Error(`SendGrid API error (${response.status}): ${message}`);
47
+ }
48
+
49
+ return data;
50
+ }
51
+
52
+ // ========== Marketing lists ==========
53
+
54
+ /** All marketing lists (paginated). */
55
+ async getLists() {
56
+ const allLists = [];
57
+ let pageToken = null;
58
+
59
+ do {
60
+ const params = new URLSearchParams({ page_size: '100' });
61
+ if (pageToken) {
62
+ params.set('page_token', pageToken);
63
+ }
64
+
65
+ const data = await this.makeRequest(`/marketing/lists?${params}`);
66
+ allLists.push(...(data.result || []));
67
+ pageToken = data._metadata?.next;
68
+ } while (pageToken);
69
+
70
+ return allLists;
71
+ }
72
+
73
+ async getListByName(name) {
74
+ const lists = await this.getLists();
75
+ return lists.find((list) => list.name === name);
76
+ }
77
+
78
+ async getList(listId) {
79
+ try {
80
+ return await this.makeRequest(`/marketing/lists/${listId}`);
81
+ } catch (error) {
82
+ if (error.message.includes('404')) {
83
+ return null;
84
+ }
85
+ throw error;
86
+ }
87
+ }
88
+
89
+ async createList(name) {
90
+ return this.makeRequest('/marketing/lists', {
91
+ method: 'POST',
92
+ body: JSON.stringify({ name }),
93
+ });
94
+ }
95
+
96
+ // ========== Custom fields ==========
97
+
98
+ async getCustomFields() {
99
+ const data = await this.makeRequest('/marketing/field_definitions');
100
+ return data.custom_fields || [];
101
+ }
102
+
103
+ /** fieldType: 'Text' | 'Number' | 'Date' */
104
+ async createCustomField(name, fieldType) {
105
+ return this.makeRequest('/marketing/field_definitions', {
106
+ method: 'POST',
107
+ body: JSON.stringify({ name, field_type: fieldType }),
108
+ });
109
+ }
110
+
111
+ async deleteCustomField(fieldId) {
112
+ await this.makeRequest(`/marketing/field_definitions/${fieldId}`, {
113
+ method: 'DELETE',
114
+ });
115
+ }
116
+
117
+ // ========== Segments (v2 API) ==========
118
+
119
+ /** All segments (paginated). The list endpoint omits query_dsl — use getSegment for detail. */
120
+ async getSegments() {
121
+ const allSegments = [];
122
+ let pageToken = null;
123
+
124
+ do {
125
+ const params = new URLSearchParams({ page_size: '50' });
126
+ if (pageToken) {
127
+ params.set('page_token', pageToken);
128
+ }
129
+
130
+ const data = await this.makeRequest(`/marketing/segments/2.0?${params}`);
131
+ allSegments.push(...(data.results || []));
132
+ pageToken = data._metadata?.next;
133
+ } while (pageToken);
134
+
135
+ return allSegments;
136
+ }
137
+
138
+ async getSegment(segmentId) {
139
+ return this.makeRequest(`/marketing/segments/2.0/${segmentId}`);
140
+ }
141
+
142
+ async createSegment(name, queryDsl) {
143
+ return this.makeRequest('/marketing/segments/2.0', {
144
+ method: 'POST',
145
+ body: JSON.stringify({ name, query_dsl: queryDsl }),
146
+ });
147
+ }
148
+
149
+ async updateSegment(segmentId, name, queryDsl) {
150
+ return this.makeRequest(`/marketing/segments/2.0/${segmentId}`, {
151
+ method: 'PATCH',
152
+ body: JSON.stringify({ name, query_dsl: queryDsl }),
153
+ });
154
+ }
155
+
156
+ async deleteSegment(segmentId) {
157
+ await this.makeRequest(`/marketing/segments/2.0/${segmentId}`, {
158
+ method: 'DELETE',
159
+ });
160
+ }
161
+
162
+ // ========== Unsubscribe groups (ASM) ==========
163
+
164
+ /** Every unsubscribe group on the ACCOUNT (ids are account-scoped, not brand-scoped). */
165
+ async getUnsubscribeGroups() {
166
+ return this.makeRequest('/asm/groups');
167
+ }
168
+
169
+ /** description is recipient-facing — it renders on the preferences page. */
170
+ async createUnsubscribeGroup(name, description) {
171
+ return this.makeRequest('/asm/groups', {
172
+ method: 'POST',
173
+ body: JSON.stringify({ name, description, is_default: false }),
174
+ });
175
+ }
176
+
177
+ // ========== Verified senders ==========
178
+
179
+ async getVerifiedSenders() {
180
+ const data = await this.makeRequest('/verified_senders');
181
+ return data?.results || [];
182
+ }
183
+
184
+ /**
185
+ * Create a verified sender. Auto-verifies when domain authentication is
186
+ * valid (the domain-auth operation runs first for exactly that reason).
187
+ *
188
+ * @param {Object} sender - { nickname, fromEmail, fromName, replyToEmail, replyToName, address }
189
+ * address: { street, street2, city, state, zip, country } (CAN-SPAM requires it)
190
+ */
191
+ async createVerifiedSender({ nickname, fromEmail, fromName, replyToEmail, replyToName, address }) {
192
+ return this.makeRequest('/verified_senders', {
193
+ method: 'POST',
194
+ body: JSON.stringify({
195
+ nickname,
196
+ from_email: fromEmail,
197
+ from_name: fromName,
198
+ reply_to: replyToEmail,
199
+ reply_to_name: replyToName,
200
+ address: address.street,
201
+ address2: address.street2 || '',
202
+ city: address.city,
203
+ state: address.state,
204
+ zip: address.zip,
205
+ country: address.country,
206
+ }),
207
+ });
208
+ }
209
+
210
+ async deleteVerifiedSender(senderId) {
211
+ await this.makeRequest(`/verified_senders/${senderId}`, {
212
+ method: 'DELETE',
213
+ });
214
+ }
215
+
216
+ // ========== Domain authentication ==========
217
+
218
+ async getAuthenticatedDomains() {
219
+ return this.makeRequest('/whitelabel/domains');
220
+ }
221
+
222
+ /** Creates the DKIM/SPF record set (automatic_security → 3 CNAMEs). */
223
+ async authenticateDomain(domain, subdomain = 'emailauth') {
224
+ return this.makeRequest('/whitelabel/domains', {
225
+ method: 'POST',
226
+ body: JSON.stringify({
227
+ domain,
228
+ subdomain,
229
+ automatic_security: true,
230
+ custom_spf: false,
231
+ }),
232
+ });
233
+ }
234
+
235
+ // ========== Link branding ==========
236
+
237
+ /**
238
+ * Every branded LINK host on the account — the `emailurl.<domain>` CNAME
239
+ * SendGrid rewrites transactional links through. Each entry carries its own
240
+ * `valid`, which is SendGrid's word on whether it has resolved the record
241
+ * (`{ domain, subdomain, valid, dns: { domain_cname, owner_cname } }`).
242
+ */
243
+ async getBrandedLinks() {
244
+ return this.makeRequest('/whitelabel/links');
245
+ }
246
+
247
+ /** Creates the branded link record set (two CNAMEs under `<subdomain>.`). */
248
+ async createBrandedLink(domain, subdomain = 'emailurl') {
249
+ return this.makeRequest('/whitelabel/links', {
250
+ method: 'POST',
251
+ body: JSON.stringify({
252
+ domain,
253
+ subdomain,
254
+ default: false,
255
+ }),
256
+ });
257
+ }
258
+
259
+ /** Asks SendGrid to check the DNS records for a branded link. */
260
+ async validateBrandedLink(linkId) {
261
+ return this.makeRequest(`/whitelabel/links/${linkId}/validate`, {
262
+ method: 'POST',
263
+ });
264
+ }
265
+
266
+ /** Asks SendGrid to check the DNS records for a domain authentication. */
267
+ async validateDomain(domainId) {
268
+ return this.makeRequest(`/whitelabel/domains/${domainId}/validate`, {
269
+ method: 'POST',
270
+ });
271
+ }
272
+
273
+ // ========== Event Webhook (account-global) ==========
274
+
275
+ async getEventWebhookSettings() {
276
+ return this.makeRequest('/user/webhooks/event/settings');
277
+ }
278
+
279
+ async updateEventWebhookSettings(settings) {
280
+ return this.makeRequest('/user/webhooks/event/settings', {
281
+ method: 'PATCH',
282
+ body: JSON.stringify(settings),
283
+ });
284
+ }
285
+ }
286
+
287
+ module.exports = { SendGridAPI };
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Validate the brand's own classic reCAPTCHA keys.
3
+ *
4
+ * omega-manager only printed a checkmark outside onboarding; this port
5
+ * actually proves the secret: siteverify with a throwaway token answers
6
+ * invalid-input-response when the secret is valid and invalid-input-secret
7
+ * when it isn't. The key's domain list has no read API (classic reCAPTCHA),
8
+ * so domain membership can't be proven — interactive runs open the console
9
+ * and confirm once, stamping config (a confirm nothing can re-check is the
10
+ * one reconcile flag config keeps, #434 — same pattern as the ga-link
11
+ * confirm); non-interactive runs keep the printed guidance.
12
+ *
13
+ * The probe is a pure read (no assessment is created), so dry-run behaves
14
+ * identically to a normal run.
15
+ */
16
+ const chalk = require('chalk').default;
17
+ const { openBrowserAndPoll } = require('../../../vendor/devkit/flows.js');
18
+ const { writeBrandConfig } = require('../../../lib/config-write.js');
19
+ const { canPrompt } = require('../../../lib/run-gates.js');
20
+ const { recaptchaConsoleUrl } = require('../lib/console-url.js');
21
+
22
+ const CONFIRMED_PATH = 'captcha.providers.recaptcha.domainsConfirmed';
23
+
24
+ // Throwaway token for the secret-validity probe — never a real assessment
25
+ const PROBE_TOKEN = 'omega-manager-secret-validation-probe';
26
+
27
+ module.exports = async function ensureSiteKey(context) {
28
+ const { recaptchaApi, siteKey, domain, brandConfig, options = {} } = context;
29
+
30
+ const consoleUrl = recaptchaConsoleUrl(brandConfig, siteKey);
31
+
32
+ console.log(` ${chalk.dim('→')} Validating reCAPTCHA secret key...`);
33
+ const verification = await recaptchaApi.verify(PROBE_TOKEN);
34
+ const errorCodes = verification['error-codes'] || [];
35
+
36
+ if (errorCodes.includes('invalid-input-secret')) {
37
+ console.log(` ${chalk.red('✗')} RECAPTCHA_SECRET_KEY in the brand .env is not a valid secret key`);
38
+ console.log(` ${chalk.dim('→')} Key settings: ${chalk.cyan(consoleUrl)}`);
39
+ throw new Error('RECAPTCHA_SECRET_KEY is invalid (siteverify answered invalid-input-secret) — fix it in the brand .env');
40
+ }
41
+
42
+ // The bare domain only — the brand contract dropped the www twin, so
43
+ // recommending it would ask for a listing nothing serves
44
+ const domains = [domain];
45
+ const output = { siteKey: { secretValid: true, domains, consoleUrl } };
46
+
47
+ console.log(` ${chalk.green('✓')} reCAPTCHA secret key is valid`);
48
+
49
+ // The domain list has no read API — a one-time interactive confirm stamps
50
+ // config (#434); a changed brand domain naturally re-prompts. A pre-existing
51
+ // stamp that also carries the www twin still covers the bare domain, so it
52
+ // confirms without churn and the stamp narrows to what's recommended now
53
+ const confirmed = brandConfig.captcha?.providers?.recaptcha?.domainsConfirmed || [];
54
+ if (domains.every((entry) => confirmed.includes(entry))) {
55
+ // Narrow a stamp that carries extras (the retired www twin); an exact
56
+ // match stays disk-free
57
+ if (confirmed.length !== domains.length) {
58
+ writeBrandConfig(context, { [CONFIRMED_PATH]: domains });
59
+ }
60
+ console.log(` ${chalk.green('✓')} Domain list confirmed (${domains.join(' + ')})`);
61
+ return { output };
62
+ }
63
+
64
+ if (canPrompt(options)) {
65
+ const result = await openBrowserAndPoll({
66
+ url: consoleUrl,
67
+ label: 'the reCAPTCHA console',
68
+ promptMessage: `Add ${chalk.cyan(domains.join(' + '))} to the reCAPTCHA key's domain list.`,
69
+ waitMessage: 'Waiting for the domains to be saved',
70
+ manualOnly: true,
71
+ indent: ' ',
72
+ });
73
+
74
+ if (result.success) {
75
+ writeBrandConfig(context, { [CONFIRMED_PATH]: domains });
76
+ console.log(` ${chalk.green('✓')} Domain list confirmed`);
77
+ return { output };
78
+ }
79
+ }
80
+
81
+ console.log(` ${chalk.dim(`→ The key's domain list has no read API — ensure ${domains.join(' + ')} is listed: ${consoleUrl}`)}`);
82
+ return { output };
83
+ };