@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,187 @@
1
+ /**
2
+ * Ensure SendGrid link branding for the brand's domain — the `emailurl.`
3
+ * host every transactional link is rewritten through.
4
+ *
5
+ * A valid branding is converged proof. A missing one is created in one pass,
6
+ * exactly like its domain-auth sibling: brand the links in SendGrid (two
7
+ * CNAMEs), diff-sync those records into the Cloudflare apex zone UNPROXIED,
8
+ * then validate — interactive runs poll until SendGrid confirms;
9
+ * non-interactive/dry runs validate ONCE, report warned, and the rerun
10
+ * converges.
11
+ *
12
+ * The records are written HERE and not left to the edge service: on a fresh
13
+ * brand's FIRST walk, edge's live SendGrid read finds no authenticated domain
14
+ * at all, so it writes none of the SendGrid set — and a branding with no CNAME
15
+ * behind it can never validate.
16
+ *
17
+ * Once SendGrid says valid, the record may finally ride Cloudflare's proxy —
18
+ * so this operation patches it to proxied on the spot instead of leaving the
19
+ * flip to a later walk ([#693](https://github.com/Omega-JS-Stack/omega/issues/693)).
20
+ */
21
+ const chalk = require('chalk').default;
22
+ const { pollWithSpinner } = require('../../../vendor/devkit/flows.js');
23
+ const { canPrompt, dryRunPlan } = require('../../../lib/run-gates.js');
24
+ const { syncDnsRecords, manualRecordList } = require('../lib/dns-sync.js');
25
+
26
+ // MUST match the name the edge record set builds (`emailurl.<domain>` in
27
+ // services/edge/lib/dns-records-helpers.js) — the branding SendGrid validates
28
+ // and the CNAME Cloudflare serves are the same host or neither works.
29
+ const SUBDOMAIN = 'emailurl';
30
+
31
+ // The record set SendGrid returns for a branded link
32
+ const DNS_KEYS = ['domain_cname', 'owner_cname'];
33
+
34
+ module.exports = async function ensureLinkBranding(context) {
35
+ const { sendgridApi: api, cloudflareApi, domain, apexDomain, isSubdomainProject, options = {} } = context;
36
+
37
+ // A subdomain project owns no apex records — the parent brand's walk does,
38
+ // which is why the edge service skips the whole SendGrid set for one. A
39
+ // branding created here would wait forever on CNAMEs this brand never writes.
40
+ if (isSubdomainProject) {
41
+ console.log(` ${chalk.dim(`⊘ Subdomain project — the parent brand owns the link branding for ${apexDomain}`)}`);
42
+ return;
43
+ }
44
+
45
+ const host = `${SUBDOMAIN}.${domain}`;
46
+
47
+ // === READ: is the host already branded and valid? ===
48
+ const links = await api.getBrandedLinks();
49
+ let branding = findBranding(links, host);
50
+
51
+ if (branding?.valid) {
52
+ console.log(` ${chalk.green('✓')} Link branding ${chalk.cyan(host)} validated`);
53
+ // Catch-up flip only: a branding THIS run did not validate is the edge
54
+ // service's to reconcile (its live read already desired the record
55
+ // proxied), so a grey record here is a raced walk, not a pending step —
56
+ // patch it when Cloudflare is reachable, and never warn about it.
57
+ if (cloudflareApi) {
58
+ await proxyRecord(cloudflareApi, apexDomain, host);
59
+ }
60
+ return { output: { linkBranding: { id: branding.id, valid: true } } };
61
+ }
62
+
63
+ if (options.dryRun) {
64
+ const planned = branding ? ['dns-records', 'validate', 'proxy-record'] : ['brand-links', 'dns-records', 'validate', 'proxy-record'];
65
+ return dryRunPlan(`${planned.join(' → ')}`, { output: { linkBranding: { planned } } });
66
+ }
67
+
68
+ // === Create the branding when missing ===
69
+ if (!branding) {
70
+ branding = await api.createBrandedLink(domain, SUBDOMAIN);
71
+ console.log(` ${chalk.green('✓')} Created link branding for ${chalk.cyan(host)}`);
72
+ } else {
73
+ console.log(` ${chalk.yellow('↻')} Link branding ${chalk.cyan(host)} exists but is not validated — reconciling DNS`);
74
+ }
75
+
76
+ const records = DNS_KEYS
77
+ .map((key) => ({ key, record: branding.dns?.[key] }))
78
+ .filter(({ record }) => record);
79
+
80
+ if (records.length === 0) {
81
+ throw new Error('SendGrid returned no DNS records for the link branding');
82
+ }
83
+
84
+ // === DNS via Cloudflare (or manual guidance) ===
85
+ // Written UNPROXIED, which is the only state SendGrid can validate: it
86
+ // resolves the host as a plain CNAME to sendgrid.net, and a proxied record
87
+ // answers with Cloudflare's own addresses instead.
88
+ const synced = await syncDnsRecords(cloudflareApi, apexDomain, records);
89
+
90
+ // === Validate — DNS may need a minute; interactive runs wait it out ===
91
+ const isValid = (validation) => validation?.valid === true;
92
+
93
+ let valid = isValid(await api.validateBrandedLink(branding.id));
94
+
95
+ if (!valid && canPrompt(options)) {
96
+ const result = await pollWithSpinner({
97
+ check: async () => {
98
+ try {
99
+ return isValid(await api.validateBrandedLink(branding.id)) ? { done: true } : { done: false };
100
+ } catch {
101
+ return { done: false };
102
+ }
103
+ },
104
+ intervalMs: 10000,
105
+ message: `Validating link branding (waiting for SendGrid to resolve ${host})`,
106
+ indent: ' ',
107
+ });
108
+ valid = result.success;
109
+ }
110
+
111
+ if (!valid) {
112
+ console.log(` ${chalk.yellow('⚠')} Validation pending — DNS is likely still propagating, rerun in a few minutes`);
113
+ return {
114
+ status: 'warned',
115
+ reason: 'link branding validation pending — the emailurl CNAME stays unproxied',
116
+ output: { linkBranding: { id: branding.id, valid: false, ...(synced ? {} : { manualRecords: manualRecordList(records) }) } },
117
+ };
118
+ }
119
+
120
+ console.log(` ${chalk.green('✓')} Link branding ${chalk.cyan(host)} validated`);
121
+
122
+ // === The flip: a validated branding may ride Cloudflare's proxy ===
123
+ // Edge stays the reconciler — its next live read sees `valid: true` and
124
+ // DESIRES the record proxied, so both services agree on the same state and
125
+ // the rerun is a no-op either way.
126
+ const proxied = await proxyRecord(cloudflareApi, apexDomain, host);
127
+
128
+ if (!proxied) {
129
+ return {
130
+ status: 'warned',
131
+ reason: 'link branding validated but the emailurl CNAME could not be proxied',
132
+ output: { linkBranding: { id: branding.id, valid: true, proxied: false } },
133
+ };
134
+ }
135
+
136
+ return { output: { linkBranding: { id: branding.id, valid: true, proxied: true } } };
137
+ };
138
+
139
+ /** The account's branding entry for this host (ids are account-scoped). */
140
+ function findBranding(links, host) {
141
+ return (links || []).find((link) => `${link.subdomain}.${link.domain}`.toLowerCase() === host.toLowerCase()) || null;
142
+ }
143
+
144
+ /**
145
+ * Patch the branded-link CNAME to proxied.
146
+ *
147
+ * @returns {boolean} - true when the record rides the proxy, false when the
148
+ * manual line was printed instead
149
+ */
150
+ async function proxyRecord(cloudflareApi, apexDomain, host) {
151
+ if (!cloudflareApi) {
152
+ logManualFlip('No CLOUDFLARE_TOKEN', host);
153
+ return false;
154
+ }
155
+
156
+ const zone = await cloudflareApi.getZoneByName(apexDomain);
157
+ if (!zone) {
158
+ logManualFlip(`No Cloudflare zone for ${apexDomain}`, host);
159
+ return false;
160
+ }
161
+
162
+ const existing = await cloudflareApi.makeRequest(`/zones/${zone.id}/dns_records?type=CNAME&per_page=100`, { method: 'GET' });
163
+ const match = (existing.result || []).find((r) => r.type === 'CNAME' && r.name === host);
164
+
165
+ // The DNS sync above writes this record, so reaching here means the zone
166
+ // answered without it (a hand-deleted record, or a read that raced a write).
167
+ if (!match) {
168
+ console.log(` ${chalk.yellow('⚠')} No ${chalk.cyan(`CNAME ${host}`)} record in the zone — add ${chalk.cyan(`${host} → sendgrid.net`)} first, then rerun ${chalk.cyan('omega manage')} and it flips to proxied.`);
169
+ return false;
170
+ }
171
+
172
+ if (match.proxied === true) {
173
+ console.log(` ${chalk.dim('→')} ${chalk.cyan(host)} already proxied`);
174
+ return true;
175
+ }
176
+
177
+ await cloudflareApi.makeRequest(`/zones/${zone.id}/dns_records/${match.id}`, {
178
+ method: 'PATCH',
179
+ body: JSON.stringify({ type: 'CNAME', name: host, content: match.content, ttl: 1, proxied: true, comment: match.comment }),
180
+ });
181
+ console.log(` ${chalk.green('✓')} Proxied ${chalk.cyan(host)} — emailed links now land on HTTPS`);
182
+ return true;
183
+ }
184
+
185
+ function logManualFlip(why, host) {
186
+ console.log(` ${chalk.yellow('⚠')} ${why} — turn the proxy ON for ${chalk.cyan(`CNAME ${host}`)} manually (orange cloud), or rerun ${chalk.cyan('omega manage')} once Cloudflare is reachable.`);
187
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Ensure the brand's SendGrid marketing list exists.
3
+ *
4
+ * Resolution order: marketing.campaigns.providers.sendgrid.listId from
5
+ * config → exact-name lookup → create. The resolved id is written back into
6
+ * omega.json5 (marketing.campaigns.providers.sendgrid.listId, its ONE
7
+ * authoritative home — comment-preserving).
8
+ */
9
+ const chalk = require('chalk').default;
10
+ const { writeBrandConfig } = require('../../../lib/config-write.js');
11
+ const { dryRunPlan } = require('../../../lib/run-gates.js');
12
+
13
+ module.exports = async function ensureList(context) {
14
+ const { sendgridApi: api, brandConfig, options = {} } = context;
15
+
16
+ const listName = brandConfig.brand.name;
17
+ const configuredId = brandConfig.marketing?.campaigns?.providers?.sendgrid?.listId;
18
+
19
+ // 1. Configured id — verify it still exists
20
+ if (configuredId) {
21
+ const list = await api.getList(configuredId);
22
+ if (list) {
23
+ console.log(` ${chalk.green('✓')} List ${chalk.cyan(`"${list.name}"`)} ${chalk.dim(`(${list.id})`)}`);
24
+ if (list.id !== configuredId) {
25
+ writeBrandConfig(context, { 'marketing.campaigns.providers.sendgrid.listId': list.id });
26
+ }
27
+ return { state: { listId: list.id, listName: list.name } };
28
+ }
29
+ console.log(` ${chalk.yellow('↻')} Configured listId ${chalk.cyan(configuredId)} no longer exists — falling back to name lookup`);
30
+ }
31
+
32
+ // 2. Exact-name lookup
33
+ const existing = await api.getListByName(listName);
34
+ if (existing) {
35
+ console.log(` ${chalk.green('✓')} Matched list ${chalk.cyan(`"${listName}"`)} ${chalk.dim(`(${existing.id})`)}`);
36
+ writeBrandConfig(context, { 'marketing.campaigns.providers.sendgrid.listId': existing.id });
37
+ return { state: { listId: existing.id, listName } };
38
+ }
39
+
40
+ // 3. Create
41
+ if (options.dryRun) {
42
+ return dryRunPlan(`create list "${listName}"`, { output: { list: { planned: 'create', listName } } });
43
+ }
44
+
45
+ const created = await api.createList(listName);
46
+ console.log(` ${chalk.green('✓')} Created list ${chalk.cyan(`"${listName}"`)} ${chalk.dim(`(${created.id})`)}`);
47
+ writeBrandConfig(context, { 'marketing.campaigns.providers.sendgrid.listId': created.id });
48
+
49
+ return { state: { listId: created.id, listName } };
50
+ };
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Ensure @omega.js/backend's segments exist in SendGrid with the right query_dsl.
3
+ *
4
+ * The segment list comes from @omega.js/backend's marketing SSOT; each one's
5
+ * query_dsl is rebuilt from its conditions and compared against the live
6
+ * segment (the list endpoint omits query_dsl, so present segments cost one
7
+ * detail read each). Stale segments are PATCHed in place, falling back to
8
+ * delete + recreate when SendGrid rejects the PATCH. Orphaned `__temp_`
9
+ * segments (leaked by @omega.js/backend's brand-scoped campaign sends on crash) are swept.
10
+ * Segments @omega.js/backend doesn't own are never touched.
11
+ */
12
+ const chalk = require('chalk').default;
13
+ const { segmentsFor } = require('../../../lib/backend-marketing.js');
14
+ const { buildQueryDsl } = require('../lib/segment-query.js');
15
+ const { dryRunPlan } = require('../../../lib/run-gates.js');
16
+
17
+ const SENDGRID_SEGMENTS = segmentsFor('sendgrid');
18
+
19
+ module.exports = async function ensureSegments(context) {
20
+ const { sendgridApi: api, options = {} } = context;
21
+
22
+ const existing = await api.getSegments();
23
+
24
+ const orphans = existing.filter((s) => s.name.startsWith('__temp_'));
25
+ const existingByName = Object.fromEntries(
26
+ existing.filter((s) => !s.name.startsWith('__temp_')).map((s) => [s.name, s]),
27
+ );
28
+
29
+ // Rebuild every segment's query and diff against the live detail
30
+ const missing = [];
31
+ const stale = [];
32
+
33
+ for (const segment of SENDGRID_SEGMENTS) {
34
+ const queryDsl = buildQueryDsl(segment.conditions, segment.logic);
35
+ const sgSegment = existingByName[segment.name];
36
+
37
+ if (!sgSegment) {
38
+ missing.push({ ...segment, queryDsl });
39
+ continue;
40
+ }
41
+
42
+ const full = await api.getSegment(sgSegment.id);
43
+ if (full.query_dsl !== queryDsl) {
44
+ stale.push({ ...segment, queryDsl, existingId: sgSegment.id });
45
+ }
46
+ }
47
+
48
+ if (orphans.length === 0 && missing.length === 0 && stale.length === 0) {
49
+ console.log(` ${chalk.green('✓')} All ${SENDGRID_SEGMENTS.length} segments exist`);
50
+ return { output: { segments: { total: SENDGRID_SEGMENTS.length, created: 0, updated: 0, orphansSwept: 0 } } };
51
+ }
52
+
53
+ if (options.dryRun) {
54
+ const planned = {
55
+ create: missing.map((s) => s.name),
56
+ update: stale.map((s) => s.name),
57
+ sweepOrphans: orphans.length,
58
+ };
59
+ return dryRunPlan(`create ${planned.create.length}, update ${planned.update.length}, sweep ${planned.sweepOrphans} orphan(s)`, { output: { segments: { planned } } });
60
+ }
61
+
62
+ // Sweep leaked __temp_ segments
63
+ for (const orphan of orphans) {
64
+ await api.deleteSegment(orphan.id);
65
+ }
66
+ if (orphans.length > 0) {
67
+ console.log(` ${chalk.yellow('↻')} Swept ${orphans.length} orphaned __temp_ segment(s)`);
68
+ }
69
+
70
+ // Stale: PATCH in place, fall back to delete + recreate
71
+ let updated = 0;
72
+ for (const segment of stale) {
73
+ try {
74
+ await api.updateSegment(segment.existingId, segment.name, segment.queryDsl);
75
+ console.log(` ${chalk.green('↻')} Updated ${chalk.cyan(segment.display)} in place`);
76
+ updated += 1;
77
+ } catch (patchError) {
78
+ await api.deleteSegment(segment.existingId);
79
+ missing.push(segment);
80
+ console.log(` ${chalk.yellow('↻')} Recreating ${chalk.cyan(segment.display)} ${chalk.dim(`(PATCH rejected: ${patchError.message})`)}`);
81
+ }
82
+ }
83
+
84
+ for (const segment of missing) {
85
+ const created = await api.createSegment(segment.name, segment.queryDsl);
86
+ console.log(` ${chalk.green('✓')} Created ${chalk.cyan(segment.display)} ${chalk.dim(`(${created.id})`)}`);
87
+ }
88
+
89
+ // Verify everything actually persisted
90
+ const verify = await api.getSegments();
91
+ const verifyByName = new Set(verify.map((s) => s.name));
92
+ const stillMissing = SENDGRID_SEGMENTS.filter((s) => !verifyByName.has(s.name));
93
+
94
+ if (stillMissing.length > 0) {
95
+ console.log(` ${chalk.yellow('⚠')} ${stillMissing.length} segment(s) failed to persist: ${chalk.cyan(stillMissing.map((s) => s.display).join(', '))}`);
96
+ return { status: 'warned', reason: `${stillMissing.length} segment(s) failed to persist: ${stillMissing.map((s) => s.name).join(', ')}`, output: { segments: { total: SENDGRID_SEGMENTS.length, failed: stillMissing.map((s) => s.name) } } };
97
+ }
98
+
99
+ return {
100
+ output: {
101
+ segments: {
102
+ total: SENDGRID_SEGMENTS.length,
103
+ created: missing.length,
104
+ updated,
105
+ orphansSwept: orphans.length,
106
+ },
107
+ },
108
+ };
109
+ };
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Ensure a verified SendGrid sender identity for the brand.
3
+ *
4
+ * Single Sends require a registered sender (not an inline from address);
5
+ * each brand gets one: offers@{contact-email domain} with the brand's name.
6
+ * It auto-verifies because domain-auth runs first. @omega.js/backend resolves the sender
7
+ * id at runtime by from_email, so nothing lands in state.
8
+ *
9
+ * CAN-SPAM requires a physical mailing address — brand.address in
10
+ * omega.json5. omega-manager silently stamped the company's address on
11
+ * every brand; the port ASKS for the brand's own (#635) and warns only when
12
+ * nobody can be asked.
13
+ */
14
+ const chalk = require('chalk').default;
15
+ const { input } = require('../../../vendor/devkit/prompt.js');
16
+ const { canPrompt, dryRunPlan } = require('../../../lib/run-gates.js');
17
+ const { confirmSetup, landValue } = require('../../../lib/config-flow.js');
18
+
19
+ // The five fields brand.address carries. Only the three SendGrid cannot
20
+ // create a sender without are required — an address with no region or postal
21
+ // code is a real address in plenty of countries.
22
+ const ADDRESS_FIELDS = [
23
+ { key: 'line1', message: 'Street address:', required: true },
24
+ { key: 'city', message: 'City:', required: true },
25
+ { key: 'region', message: 'State / region:' },
26
+ { key: 'postalCode', message: 'Postal code:' },
27
+ { key: 'country', message: 'Country:', required: true },
28
+ ];
29
+
30
+ /**
31
+ * Ask for the brand's mailing address and land it as ONE brand.address object
32
+ * (#635). Five fields behind ONE gate — asking the Provide / Skip / Disable
33
+ * question per field would be the same question five times. The campaigns
34
+ * service already gated on its API key, so this gate is about the address.
35
+ *
36
+ * @param {Object} context - Handler context.
37
+ * @returns {Promise<Object|null>} The landed address, or null when the user
38
+ * stepped aside or left a required field empty.
39
+ */
40
+ async function askBrandAddress(context) {
41
+ const action = await confirmSetup(context, {
42
+ label: 'Mailing address (CAN-SPAM)',
43
+ instructions: ['Every marketing email must carry a physical mailing address — it lands in brand.address'],
44
+ disablePath: 'marketing.campaigns.enabled',
45
+ });
46
+ if (action !== 'yes') {
47
+ return null;
48
+ }
49
+
50
+ const address = {};
51
+ for (const field of ADDRESS_FIELDS) {
52
+ const value = (await input({ message: field.message }) || '').trim();
53
+ if (!value) {
54
+ if (field.required) {
55
+ return null;
56
+ }
57
+ continue;
58
+ }
59
+ address[field.key] = value;
60
+ }
61
+
62
+ landValue(context, 'brand.address', address);
63
+ return address;
64
+ }
65
+
66
+ module.exports = async function ensureSenderIdentity(context) {
67
+ const { sendgridApi: api, brandConfig, options = {} } = context;
68
+
69
+ const brandName = brandConfig.brand.name;
70
+ const contactEmail = brandConfig.brand?.contact?.email;
71
+ const contactDomain = contactEmail?.split('@')[1];
72
+
73
+ if (!contactDomain) {
74
+ console.log(chalk.dim(' ⊘ No brand.contact.email configured — nothing to register'));
75
+ return {};
76
+ }
77
+
78
+ const fromEmail = `offers@${contactDomain}`;
79
+
80
+ const senders = await api.getVerifiedSenders();
81
+ const match = senders.find((s) => s.from_email === fromEmail);
82
+ // The nickname is the managed handle (unique per SendGrid account) — a
83
+ // sender with this brand's nickname but another address is a stale
84
+ // derivation (contact.email changed) and would 400 the create below
85
+ const stale = senders.find((s) => s.nickname === brandName && s.from_email !== fromEmail);
86
+
87
+ if (match && match.verified !== false) {
88
+ console.log(` ${chalk.green('✓')} Verified sender ${chalk.cyan(fromEmail)} ${chalk.dim(`(id: ${match.id})`)}`);
89
+ return { output: { senderIdentity: { id: match.id, fromEmail, verified: true } } };
90
+ }
91
+
92
+ // The physical address is required for creation — and it must be the
93
+ // brand's own (no company default to fall back on), so a run that can ask
94
+ // ASKS for it here rather than warning and stepping aside (#635).
95
+ const complete = (candidate) => Boolean(candidate?.line1 && candidate?.city && candidate?.country);
96
+
97
+ let address = brandConfig.brand?.address;
98
+ if (!complete(address) && canPrompt(options)) {
99
+ address = await askBrandAddress(context);
100
+ }
101
+
102
+ if (!complete(address)) {
103
+ console.log(` ${chalk.yellow('⚠')} No sender ${chalk.cyan(fromEmail)} yet, and CAN-SPAM requires a physical mailing address`);
104
+ console.log(` ${chalk.dim('→')} Set brand.address (line1, city, region, postalCode, country) in omega.json5, then rerun`);
105
+ return { status: 'warned', reason: 'no sender yet — brand.address is required by CAN-SPAM', output: { senderIdentity: { fromEmail, missingAddress: true } } };
106
+ }
107
+
108
+ if (options.dryRun) {
109
+ const planned = match ? 'recreate-unverified-sender' : stale ? 'replace-stale-nickname-sender' : 'create-sender';
110
+ return dryRunPlan(`${planned} (${fromEmail})`, { output: { senderIdentity: { planned, fromEmail } } });
111
+ }
112
+
113
+ // An unverified leftover can't be verified retroactively — recreate it
114
+ // cleanly now that domain authentication is in place
115
+ if (match) {
116
+ await api.deleteVerifiedSender(match.id);
117
+ console.log(` ${chalk.yellow('↻')} Deleted unverified sender ${chalk.cyan(fromEmail)} for recreation`);
118
+ }
119
+
120
+ if (stale) {
121
+ if (stale.verified === false) {
122
+ await api.deleteVerifiedSender(stale.id);
123
+ console.log(` ${chalk.yellow('↻')} Deleted stale unverified sender ${chalk.cyan(stale.from_email)} — the contact domain changed`);
124
+ } else {
125
+ console.log(` ${chalk.yellow('⚠')} Verified sender ${chalk.cyan(stale.from_email)} already uses the nickname ${chalk.cyan(brandName)} — delete it in SendGrid or align brand.contact.email, then rerun`);
126
+ return { status: 'warned', reason: 'a verified sender already uses the brand nickname', output: { senderIdentity: { fromEmail, staleNickname: stale.from_email } } };
127
+ }
128
+ }
129
+
130
+ const sender = await api.createVerifiedSender({
131
+ nickname: brandName,
132
+ fromEmail,
133
+ fromName: brandName,
134
+ replyToEmail: contactEmail,
135
+ replyToName: brandName,
136
+ address: {
137
+ street: address.line1,
138
+ street2: address.line2 || '',
139
+ city: address.city,
140
+ state: address.region,
141
+ zip: address.postalCode,
142
+ country: address.country,
143
+ },
144
+ });
145
+
146
+ if (sender.verified === false) {
147
+ console.log(` ${chalk.yellow('⚠')} Created sender ${chalk.cyan(fromEmail)} but it did not auto-verify — ensure domain authentication is valid, then rerun`);
148
+ return { status: 'warned', reason: 'the created sender did not auto-verify', output: { senderIdentity: { id: sender.id, fromEmail, verified: false } } };
149
+ }
150
+
151
+ console.log(` ${chalk.green('✓')} Created verified sender ${chalk.cyan(fromEmail)} ${chalk.dim(`(id: ${sender.id})`)}`);
152
+ return { output: { senderIdentity: { id: sender.id, fromEmail, verified: true } } };
153
+ };
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Ensure the SendGrid unsubscribe groups (ASM) @omega.js/backend sends through
3
+ * exist, and land their ids in config.
4
+ *
5
+ * The KEYS come from @omega.js/backend's email SSOT — its send path names a
6
+ * group by key — and the recipient-facing name + description of each live
7
+ * HERE, the provider side. Groups are matched by NAME, never by id: ASM ids
8
+ * are per SendGrid ACCOUNT, so sibling brands sharing one account converge on
9
+ * the same seven groups and land the same ids.
10
+ *
11
+ * Each resolved id is written into omega.json5 at
12
+ * marketing.campaigns.providers.sendgrid.groups.<key>, its ONE authoritative
13
+ * home (comment-preserving) — @omega.js/backend reads it there and fails loudly
14
+ * at send time when one is missing
15
+ * ([#649](https://github.com/Omega-JS-Stack/omega/issues/649)).
16
+ */
17
+ const chalk = require('chalk').default;
18
+ const { BEM_GROUP_KEYS } = require('../../../lib/backend-marketing.js');
19
+ const { writeBrandConfig } = require('../../../lib/config-write.js');
20
+ const { dryRunPlan } = require('../../../lib/run-gates.js');
21
+
22
+ // The recipient-facing group name + description, one row per @omega.js/backend
23
+ // group key. Brand-neutral on purpose: the NAME is the match key, so two
24
+ // brands on one SendGrid account resolve the same group instead of each
25
+ // creating their own.
26
+ const GROUP_DEFINITIONS = {
27
+ orders: {
28
+ name: 'OMEGA - Order Updates',
29
+ description: 'Receipts, renewals, refunds, and changes to your plan.',
30
+ },
31
+ hello: {
32
+ name: 'OMEGA - Onboarding',
33
+ description: 'Welcome emails and getting-started tips for your first weeks.',
34
+ },
35
+ account: {
36
+ name: 'OMEGA - Account',
37
+ description: 'Account actions you asked for: deletions, data requests, and confirmations.',
38
+ },
39
+ marketing: {
40
+ name: 'OMEGA - Marketing & Promotions',
41
+ description: 'Offers, discounts, product news, and reminders.',
42
+ },
43
+ security: {
44
+ name: 'OMEGA - Security',
45
+ description: 'Password resets, sign-in codes, and security alerts.',
46
+ },
47
+ newsletter: {
48
+ name: 'OMEGA - Newsletter',
49
+ description: 'The regular newsletter: feature announcements and industry news.',
50
+ },
51
+ internal: {
52
+ name: 'OMEGA - Internal Alerts',
53
+ description: 'Operational alerts sent to the team.',
54
+ },
55
+ };
56
+
57
+ module.exports = async function ensureUnsubscribeGroups(context) {
58
+ const { sendgridApi: api, options = {} } = context;
59
+
60
+ // A key @omega.js/backend added without a row here would send through a group
61
+ // this service never provisions — loud, at the top, not at send time.
62
+ const undefinedKeys = BEM_GROUP_KEYS.filter((key) => !GROUP_DEFINITIONS[key]);
63
+ if (undefinedKeys.length > 0) {
64
+ throw new Error(`@omega.js/backend group key(s) with no name+description row here: ${undefinedKeys.join(', ')}`);
65
+ }
66
+
67
+ const existing = await api.getUnsubscribeGroups();
68
+ const existingByName = Object.fromEntries(existing.map((group) => [group.name, group]));
69
+
70
+ const missing = BEM_GROUP_KEYS.filter((key) => !existingByName[GROUP_DEFINITIONS[key].name]);
71
+
72
+ if (missing.length > 0 && options.dryRun) {
73
+ return dryRunPlan(
74
+ `create ${missing.length} unsubscribe group(s): ${missing.map((key) => GROUP_DEFINITIONS[key].name).join(', ')}`,
75
+ { output: { unsubscribeGroups: { planned: { create: missing.map((key) => GROUP_DEFINITIONS[key].name) } } } },
76
+ );
77
+ }
78
+
79
+ const ids = {};
80
+
81
+ for (const key of BEM_GROUP_KEYS) {
82
+ const { name, description } = GROUP_DEFINITIONS[key];
83
+ const found = existingByName[name];
84
+
85
+ if (found) {
86
+ ids[key] = found.id;
87
+ continue;
88
+ }
89
+
90
+ const created = await api.createUnsubscribeGroup(name, description);
91
+ console.log(` ${chalk.green('✓')} Created group ${chalk.cyan(`"${name}"`)} ${chalk.dim(`(${created.id})`)}`);
92
+ ids[key] = created.id;
93
+ }
94
+
95
+ if (missing.length === 0) {
96
+ console.log(` ${chalk.green('✓')} All ${BEM_GROUP_KEYS.length} unsubscribe groups exist`);
97
+ }
98
+
99
+ // Already-equal ids are skipped by the editor, so a converged brand leaves
100
+ // omega.json5 byte-identical.
101
+ const written = writeBrandConfig(context, Object.fromEntries(
102
+ BEM_GROUP_KEYS.map((key) => [`marketing.campaigns.providers.sendgrid.groups.${key}`, ids[key]]),
103
+ ));
104
+
105
+ return {
106
+ output: {
107
+ unsubscribeGroups: {
108
+ total: BEM_GROUP_KEYS.length,
109
+ created: missing.length,
110
+ written: written.length,
111
+ },
112
+ },
113
+ };
114
+ };
115
+
116
+ module.exports.GROUP_DEFINITIONS = GROUP_DEFINITIONS;