@omega.js/backend 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 (993) hide show
  1. package/LICENSE +98 -0
  2. package/README.md +1053 -0
  3. package/bin/omega +2 -0
  4. package/bin/omega-backend +2 -0
  5. package/bin/omg +2 -0
  6. package/cli.js +3 -0
  7. package/dist/cli/command-table.js +268 -0
  8. package/dist/cli/commands/auth.js +259 -0
  9. package/dist/cli/commands/base-command.js +451 -0
  10. package/dist/cli/commands/build.js +27 -0
  11. package/dist/cli/commands/clean.js +13 -0
  12. package/dist/cli/commands/clear.js +11 -0
  13. package/dist/cli/commands/cwd.js +9 -0
  14. package/dist/cli/commands/deploy.js +205 -0
  15. package/dist/cli/commands/emulator-orphans.js +440 -0
  16. package/dist/cli/commands/emulator.js +1829 -0
  17. package/dist/cli/commands/firebase-init.js +101 -0
  18. package/dist/cli/commands/firestore.js +261 -0
  19. package/dist/cli/commands/indexes.js +51 -0
  20. package/dist/cli/commands/install.js +151 -0
  21. package/dist/cli/commands/logs.js +358 -0
  22. package/dist/cli/commands/mcp.js +41 -0
  23. package/dist/cli/commands/migrate-markers.js +176 -0
  24. package/dist/cli/commands/migrate-rules.js +90 -0
  25. package/dist/cli/commands/migrate.js +59 -0
  26. package/dist/cli/commands/serve.js +208 -0
  27. package/dist/cli/commands/setup-tests/base-test.js +174 -0
  28. package/dist/cli/commands/setup-tests/emulator-config.js +109 -0
  29. package/dist/cli/commands/setup-tests/env-runtime-config-deprecated.js +47 -0
  30. package/dist/cli/commands/setup-tests/firebase-admin.js +49 -0
  31. package/dist/cli/commands/setup-tests/firebase-auth.js +26 -0
  32. package/dist/cli/commands/setup-tests/firebase-cli.js +26 -0
  33. package/dist/cli/commands/setup-tests/firebase-functions.js +49 -0
  34. package/dist/cli/commands/setup-tests/firestore-indexes-file.js +72 -0
  35. package/dist/cli/commands/setup-tests/firestore-indexes-in-json.js +20 -0
  36. package/dist/cli/commands/setup-tests/firestore-indexes-required.js +108 -0
  37. package/dist/cli/commands/setup-tests/firestore-indexes-synced.js +236 -0
  38. package/dist/cli/commands/setup-tests/firestore-rules-file.js +115 -0
  39. package/dist/cli/commands/setup-tests/firestore-rules-in-json.js +49 -0
  40. package/dist/cli/commands/setup-tests/functions-package.js +44 -0
  41. package/dist/cli/commands/setup-tests/gcloud-cli.js +26 -0
  42. package/dist/cli/commands/setup-tests/gitignore.js +122 -0
  43. package/dist/cli/commands/setup-tests/helpers/merge-line-files.js +28 -0
  44. package/dist/cli/commands/setup-tests/helpers/required-indexes.js +109 -0
  45. package/dist/cli/commands/setup-tests/helpers/seed-campaigns.js +246 -0
  46. package/dist/cli/commands/setup-tests/helpers.js +72 -0
  47. package/dist/cli/commands/setup-tests/hosting-folder.js +23 -0
  48. package/dist/cli/commands/setup-tests/hosting-rewrites.js +52 -0
  49. package/dist/cli/commands/setup-tests/index.js +111 -0
  50. package/dist/cli/commands/setup-tests/is-firebase-project.js +21 -0
  51. package/dist/cli/commands/setup-tests/java-installed.js +27 -0
  52. package/dist/cli/commands/setup-tests/legacy-tests-cleanup.js +43 -0
  53. package/dist/cli/commands/setup-tests/marketing-campaigns-seeded.js +230 -0
  54. package/dist/cli/commands/setup-tests/node-version.js +60 -0
  55. package/dist/cli/commands/setup-tests/npm-project-scripts.js +42 -0
  56. package/dist/cli/commands/setup-tests/nvmrc-version.js +36 -0
  57. package/dist/cli/commands/setup-tests/omega-backend.js +42 -0
  58. package/dist/cli/commands/setup-tests/omega-config.js +84 -0
  59. package/dist/cli/commands/setup-tests/project-directories.js +34 -0
  60. package/dist/cli/commands/setup-tests/project-id-consistency.js +201 -0
  61. package/dist/cli/commands/setup-tests/public-html-files.js +22 -0
  62. package/dist/cli/commands/setup-tests/realtime-rules-file.js +69 -0
  63. package/dist/cli/commands/setup-tests/realtime-rules-in-json.js +20 -0
  64. package/dist/cli/commands/setup-tests/remoteconfig-template-file.js +32 -0
  65. package/dist/cli/commands/setup-tests/remoteconfig-template-in-json.js +31 -0
  66. package/dist/cli/commands/setup-tests/service-account.js +86 -0
  67. package/dist/cli/commands/setup-tests/storage-lifecycle-policy.js +81 -0
  68. package/dist/cli/commands/setup-tests/storage-rules-file.js +32 -0
  69. package/dist/cli/commands/setup-tests/storage-rules-in-json.js +20 -0
  70. package/dist/cli/commands/stripe.js +14 -0
  71. package/dist/cli/commands/test-lanes/stripe-live.js +440 -0
  72. package/dist/cli/commands/test.js +976 -0
  73. package/dist/cli/commands/update.js +27 -0
  74. package/dist/cli/commands/version.js +10 -0
  75. package/dist/cli/commands/watch.js +196 -0
  76. package/dist/cli/flags.js +13 -0
  77. package/dist/cli/index.js +173 -0
  78. package/dist/cli/run.js +33 -0
  79. package/dist/cli/utils/attach-log-file.js +4 -0
  80. package/dist/cli/utils/compile-rules.js +1155 -0
  81. package/dist/cli/utils/ensure-target.js +314 -0
  82. package/dist/cli/utils/project-type.js +140 -0
  83. package/dist/cli/utils/public-files.js +49 -0
  84. package/dist/cli/utils/safe-install.js +4 -0
  85. package/dist/cli/utils/spawn-shell.js +94 -0
  86. package/dist/cli/utils/stage-functions.js +338 -0
  87. package/dist/cli/utils/stage-local-packages.js +210 -0
  88. package/dist/cli/utils/target-checks.js +161 -0
  89. package/dist/cli/utils/target.js +24 -0
  90. package/dist/cli/utils/ui.js +292 -0
  91. package/dist/defaults/AGENTS.md +125 -0
  92. package/dist/defaults/CHANGELOG.md +15 -0
  93. package/dist/defaults/CLAUDE.md +1 -0
  94. package/dist/defaults/_.gitignore +65 -0
  95. package/dist/defaults/docs/README.md +17 -0
  96. package/dist/defaults/test/README.md +65 -0
  97. package/dist/defaults/test/_init.js +14 -0
  98. package/dist/defaults/test/helpers/connect-trap.js +72 -0
  99. package/dist/defaults/test/unit/registration.test.js +143 -0
  100. package/dist/defaults/test/unit/rules-posture.test.js +101 -0
  101. package/dist/defaults/test/unit/socket-free.test.js +43 -0
  102. package/dist/manager/events/auth/before-create.js +89 -0
  103. package/dist/manager/events/auth/before-signin.js +76 -0
  104. package/dist/manager/events/auth/on-create.js +112 -0
  105. package/dist/manager/events/auth/on-delete.js +141 -0
  106. package/dist/manager/events/auth/utils.js +93 -0
  107. package/dist/manager/events/cron/daily/blog-auto-publisher.js +287 -0
  108. package/dist/manager/events/cron/daily/data-requests.js +57 -0
  109. package/dist/manager/events/cron/daily/expire-paypal-cancellations.js +191 -0
  110. package/dist/manager/events/cron/daily/marketing-prune.js +486 -0
  111. package/dist/manager/events/cron/daily/reset-usage.js +188 -0
  112. package/dist/manager/events/cron/daily/trial-lapse-sweep.js +355 -0
  113. package/dist/manager/events/cron/daily.js +3 -0
  114. package/dist/manager/events/cron/frequent/abandoned-carts.js +204 -0
  115. package/dist/manager/events/cron/frequent/email-queue.js +71 -0
  116. package/dist/manager/events/cron/frequent/marketing-campaigns.js +463 -0
  117. package/dist/manager/events/cron/frequent/retry-failed-webhooks.js +70 -0
  118. package/dist/manager/events/cron/frequent.js +3 -0
  119. package/dist/manager/events/cron/runner.js +64 -0
  120. package/dist/manager/events/firestore/notifications/on-write.js +123 -0
  121. package/dist/manager/events/firestore/payments-disputes/on-write.js +256 -0
  122. package/dist/manager/events/firestore/payments-disputes/providers/stripe.js +218 -0
  123. package/dist/manager/events/firestore/payments-disputes/providers/test.js +231 -0
  124. package/dist/manager/events/firestore/payments-webhooks/analytics.js +687 -0
  125. package/dist/manager/events/firestore/payments-webhooks/on-write.js +1224 -0
  126. package/dist/manager/events/firestore/payments-webhooks/transitions/index.js +395 -0
  127. package/dist/manager/events/firestore/payments-webhooks/transitions/one-time/purchase-completed.js +58 -0
  128. package/dist/manager/events/firestore/payments-webhooks/transitions/one-time/purchase-failed.js +28 -0
  129. package/dist/manager/events/firestore/payments-webhooks/transitions/one-time/purchase-refunded.js +30 -0
  130. package/dist/manager/events/firestore/payments-webhooks/transitions/send-email.js +74 -0
  131. package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/cancellation-removed.js +14 -0
  132. package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/cancellation-requested.js +28 -0
  133. package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/checkout-declined.js +18 -0
  134. package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/new-subscription.js +67 -0
  135. package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/payment-failed.js +25 -0
  136. package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/payment-recovered.js +26 -0
  137. package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/payment-refunded.js +34 -0
  138. package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/plan-changed.js +42 -0
  139. package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/subscription-cancelled.js +33 -0
  140. package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/subscription-winback.js +17 -0
  141. package/dist/manager/functions/_legacy/actions/create-post-handler.js +188 -0
  142. package/dist/manager/functions/_legacy/actions/generate-uuid.js +63 -0
  143. package/dist/manager/functions/_legacy/actions/sign-up-handler.js +205 -0
  144. package/dist/manager/functions/_legacy/admin/create-post.js +207 -0
  145. package/dist/manager/functions/_legacy/admin/firestore-write.js +72 -0
  146. package/dist/manager/functions/_legacy/admin/get-stats.js +218 -0
  147. package/dist/manager/functions/_legacy/admin/query.js +198 -0
  148. package/dist/manager/functions/_legacy/admin/send-notification.js +206 -0
  149. package/dist/manager/functions/_legacy/template.js +33 -0
  150. package/dist/manager/functions/_legacy/test/authenticate.js +40 -0
  151. package/dist/manager/functions/_legacy/test/webhook.js +37 -0
  152. package/dist/manager/functions/wrappers/mailchimp/addToList.js +25 -0
  153. package/dist/manager/helpers/analytics.js +498 -0
  154. package/dist/manager/helpers/api-manager.js +313 -0
  155. package/dist/manager/helpers/backend-router.js +37 -0
  156. package/dist/manager/helpers/context/authenticate.js +215 -0
  157. package/dist/manager/helpers/context/client-info.js +91 -0
  158. package/dist/manager/helpers/context/index.js +228 -0
  159. package/dist/manager/helpers/context/logging.js +150 -0
  160. package/dist/manager/helpers/context/parse.js +160 -0
  161. package/dist/manager/helpers/context/respond.js +247 -0
  162. package/dist/manager/helpers/event-middleware.js +109 -0
  163. package/dist/manager/helpers/metadata.js +32 -0
  164. package/dist/manager/helpers/middleware.js +541 -0
  165. package/dist/manager/helpers/redact-secret.js +25 -0
  166. package/dist/manager/helpers/resolved-config.js +34 -0
  167. package/dist/manager/helpers/roles.js +69 -0
  168. package/dist/manager/helpers/safe-compare.js +31 -0
  169. package/dist/manager/helpers/schema-engine.js +314 -0
  170. package/dist/manager/helpers/schema-zod.js +203 -0
  171. package/dist/manager/helpers/settings.js +215 -0
  172. package/dist/manager/helpers/usage.js +541 -0
  173. package/dist/manager/helpers/user.js +57 -0
  174. package/dist/manager/helpers/utilities.js +565 -0
  175. package/dist/manager/index.js +1404 -0
  176. package/dist/manager/libraries/abandoned-cart-config.js +12 -0
  177. package/dist/manager/libraries/ai/index.js +321 -0
  178. package/dist/manager/libraries/ai/prompt.js +85 -0
  179. package/dist/manager/libraries/ai/providers/anthropic-format.js +297 -0
  180. package/dist/manager/libraries/ai/providers/anthropic.js +169 -0
  181. package/dist/manager/libraries/ai/providers/claude-code.js +183 -0
  182. package/dist/manager/libraries/ai/providers/openai.js +1272 -0
  183. package/dist/manager/libraries/ai/providers/test.js +257 -0
  184. package/dist/manager/libraries/ai/tokens.js +54 -0
  185. package/dist/manager/libraries/analytics/conversions.js +478 -0
  186. package/dist/manager/libraries/analytics/match-data.js +573 -0
  187. package/dist/manager/libraries/analytics/signup.js +120 -0
  188. package/dist/manager/libraries/auth-user.js +46 -0
  189. package/dist/manager/libraries/content/feed-parser.js +198 -0
  190. package/dist/manager/libraries/content/ghostii.js +191 -0
  191. package/dist/manager/libraries/content/source-resolver.js +629 -0
  192. package/dist/manager/libraries/email/constants.js +532 -0
  193. package/dist/manager/libraries/email/data/blocked-local-parts.json +55 -0
  194. package/dist/manager/libraries/email/data/blocked-local-patterns.js +14 -0
  195. package/dist/manager/libraries/email/data/corporate-domains.json +23 -0
  196. package/dist/manager/libraries/email/data/custom-disposable-domains.json +58 -0
  197. package/dist/manager/libraries/email/data/disposable-domains.json +8184 -0
  198. package/dist/manager/libraries/email/data/typo-domains.js +83 -0
  199. package/dist/manager/libraries/email/disposable-domains.js +92 -0
  200. package/dist/manager/libraries/email/generators/lib/filter.js +179 -0
  201. package/dist/manager/libraries/email/generators/lib/image-host.js +344 -0
  202. package/dist/manager/libraries/email/generators/lib/image-illustrator.js +154 -0
  203. package/dist/manager/libraries/email/generators/lib/markdown-renderer.js +294 -0
  204. package/dist/manager/libraries/email/generators/lib/mjml-template.js +120 -0
  205. package/dist/manager/libraries/email/generators/lib/structure.js +299 -0
  206. package/dist/manager/libraries/email/generators/lib/svg-illustrator.js +197 -0
  207. package/dist/manager/libraries/email/generators/lib/templates/base.js +229 -0
  208. package/dist/manager/libraries/email/generators/lib/templates/card.js +33 -0
  209. package/dist/manager/libraries/email/generators/lib/templates/classic-schema.js +59 -0
  210. package/dist/manager/libraries/email/generators/lib/templates/clean.js +82 -0
  211. package/dist/manager/libraries/email/generators/lib/templates/editorial/helpers.js +100 -0
  212. package/dist/manager/libraries/email/generators/lib/templates/editorial/index.js +318 -0
  213. package/dist/manager/libraries/email/generators/lib/templates/feedback.js +101 -0
  214. package/dist/manager/libraries/email/generators/lib/templates/field-report/helpers.js +138 -0
  215. package/dist/manager/libraries/email/generators/lib/templates/field-report/index.js +494 -0
  216. package/dist/manager/libraries/email/generators/lib/templates/index.js +47 -0
  217. package/dist/manager/libraries/email/generators/lib/templates/newsletter-shared.js +610 -0
  218. package/dist/manager/libraries/email/generators/lib/templates/order.js +461 -0
  219. package/dist/manager/libraries/email/generators/lib/templates/plain.js +83 -0
  220. package/dist/manager/libraries/email/generators/lib/templates/shared-campaign.js +64 -0
  221. package/dist/manager/libraries/email/generators/newsletter.js +899 -0
  222. package/dist/manager/libraries/email/index.js +146 -0
  223. package/dist/manager/libraries/email/marketing/index.js +754 -0
  224. package/dist/manager/libraries/email/prepare.js +365 -0
  225. package/dist/manager/libraries/email/providers/beehiiv.js +589 -0
  226. package/dist/manager/libraries/email/providers/sendgrid.js +845 -0
  227. package/dist/manager/libraries/email/transactional/index.js +510 -0
  228. package/dist/manager/libraries/email/utm.js +86 -0
  229. package/dist/manager/libraries/email/validation-provider-neverbounce.js +70 -0
  230. package/dist/manager/libraries/email/validation-provider-zerobounce.js +42 -0
  231. package/dist/manager/libraries/email/validation.js +255 -0
  232. package/dist/manager/libraries/env.js +198 -0
  233. package/dist/manager/libraries/infer-contact.js +101 -0
  234. package/dist/manager/libraries/load-provider.js +65 -0
  235. package/dist/manager/libraries/notification.js +256 -0
  236. package/dist/manager/libraries/openai.js +13 -0
  237. package/dist/manager/libraries/payment/discount-codes.js +204 -0
  238. package/dist/manager/libraries/payment/fetch-failure.js +56 -0
  239. package/dist/manager/libraries/payment/license.js +44 -0
  240. package/dist/manager/libraries/payment/order-id.js +57 -0
  241. package/dist/manager/libraries/payment/provider-errors.js +71 -0
  242. package/dist/manager/libraries/payment/providers/chargebee.js +817 -0
  243. package/dist/manager/libraries/payment/providers/coinbase.js +330 -0
  244. package/dist/manager/libraries/payment/providers/paypal.js +987 -0
  245. package/dist/manager/libraries/payment/providers/stripe.js +755 -0
  246. package/dist/manager/libraries/payment/providers/test.js +250 -0
  247. package/dist/manager/libraries/payment/refund-linkage.js +78 -0
  248. package/dist/manager/libraries/payment/refund-policy.js +113 -0
  249. package/dist/manager/libraries/payment/winback.js +55 -0
  250. package/dist/manager/libraries/prompts/infer-contact.md +78 -0
  251. package/dist/manager/libraries/rate-limits.js +17 -0
  252. package/dist/manager/libraries/recaptcha.js +58 -0
  253. package/dist/manager/libraries/user-doc.js +234 -0
  254. package/dist/manager/routes/admin/backup/post.js +106 -0
  255. package/dist/manager/routes/admin/cron/post.js +37 -0
  256. package/dist/manager/routes/admin/database/get.js +35 -0
  257. package/dist/manager/routes/admin/database/post.js +34 -0
  258. package/dist/manager/routes/admin/email/post.js +74 -0
  259. package/dist/manager/routes/admin/firestore/get.js +35 -0
  260. package/dist/manager/routes/admin/firestore/post.js +56 -0
  261. package/dist/manager/routes/admin/firestore/query/post.js +128 -0
  262. package/dist/manager/routes/admin/hook/post.js +104 -0
  263. package/dist/manager/routes/admin/infer-contact/post.js +35 -0
  264. package/dist/manager/routes/admin/notification/post.js +37 -0
  265. package/dist/manager/routes/admin/payment/post.js +56 -0
  266. package/dist/manager/routes/admin/post/deduplicate-image-alts.js +52 -0
  267. package/dist/manager/routes/admin/post/dispatch-deploy.js +36 -0
  268. package/dist/manager/routes/admin/post/post.js +487 -0
  269. package/dist/manager/routes/admin/post/put.js +146 -0
  270. package/dist/manager/routes/admin/post/templates/post.html +14 -0
  271. package/dist/manager/routes/admin/repo/content/post.js +106 -0
  272. package/dist/manager/routes/admin/stats/get.js +222 -0
  273. package/dist/manager/routes/admin/users/disable/post.js +50 -0
  274. package/dist/manager/routes/admin/users/list/get.js +110 -0
  275. package/dist/manager/routes/admin/users/sync/post.js +119 -0
  276. package/dist/manager/routes/brand/get.js +26 -0
  277. package/dist/manager/routes/content/post/get.js +110 -0
  278. package/dist/manager/routes/general/email/post.js +89 -0
  279. package/dist/manager/routes/general/email/templates/general/download-app-link.js +42 -0
  280. package/dist/manager/routes/general/uuid/post.js +31 -0
  281. package/dist/manager/routes/handler/post/post.js +142 -0
  282. package/dist/manager/routes/health/get.js +43 -0
  283. package/dist/manager/routes/index.js +11 -0
  284. package/dist/manager/routes/marketing/campaign/delete.js +45 -0
  285. package/dist/manager/routes/marketing/campaign/get.js +69 -0
  286. package/dist/manager/routes/marketing/campaign/post.js +124 -0
  287. package/dist/manager/routes/marketing/campaign/put.js +86 -0
  288. package/dist/manager/routes/marketing/campaign/utils.js +59 -0
  289. package/dist/manager/routes/marketing/contact/delete.js +100 -0
  290. package/dist/manager/routes/marketing/contact/post.js +149 -0
  291. package/dist/manager/routes/marketing/contact/put.js +37 -0
  292. package/dist/manager/routes/marketing/email-preferences/post.js +275 -0
  293. package/dist/manager/routes/marketing/webhook/forward/post.js +170 -0
  294. package/dist/manager/routes/marketing/webhook/post.js +123 -0
  295. package/dist/manager/routes/marketing/webhook/providers/beehiiv.js +197 -0
  296. package/dist/manager/routes/marketing/webhook/providers/sendgrid.js +194 -0
  297. package/dist/manager/routes/payments/cancel/_is-trialing.js +40 -0
  298. package/dist/manager/routes/payments/cancel/post.js +149 -0
  299. package/dist/manager/routes/payments/cancel/providers/chargebee.js +41 -0
  300. package/dist/manager/routes/payments/cancel/providers/paypal.js +56 -0
  301. package/dist/manager/routes/payments/cancel/providers/stripe.js +33 -0
  302. package/dist/manager/routes/payments/cancel/providers/test.js +118 -0
  303. package/dist/manager/routes/payments/discount/get.js +25 -0
  304. package/dist/manager/routes/payments/dispute-alert/post.js +117 -0
  305. package/dist/manager/routes/payments/dispute-alert/providers/chargeblast.js +55 -0
  306. package/dist/manager/routes/payments/intent/post.js +341 -0
  307. package/dist/manager/routes/payments/intent/providers/chargebee.js +197 -0
  308. package/dist/manager/routes/payments/intent/providers/coinbase.js +94 -0
  309. package/dist/manager/routes/payments/intent/providers/paypal.js +244 -0
  310. package/dist/manager/routes/payments/intent/providers/stripe.js +141 -0
  311. package/dist/manager/routes/payments/intent/providers/test.js +341 -0
  312. package/dist/manager/routes/payments/plan/post.js +147 -0
  313. package/dist/manager/routes/payments/plan/providers/chargebee.js +62 -0
  314. package/dist/manager/routes/payments/plan/providers/paypal.js +62 -0
  315. package/dist/manager/routes/payments/plan/providers/stripe.js +59 -0
  316. package/dist/manager/routes/payments/plan/providers/test.js +113 -0
  317. package/dist/manager/routes/payments/portal/post.js +113 -0
  318. package/dist/manager/routes/payments/portal/providers/chargebee.js +62 -0
  319. package/dist/manager/routes/payments/portal/providers/paypal.js +25 -0
  320. package/dist/manager/routes/payments/portal/providers/stripe.js +62 -0
  321. package/dist/manager/routes/payments/portal/providers/test.js +18 -0
  322. package/dist/manager/routes/payments/refund/post.js +207 -0
  323. package/dist/manager/routes/payments/refund/providers/chargebee.js +147 -0
  324. package/dist/manager/routes/payments/refund/providers/coinbase.js +40 -0
  325. package/dist/manager/routes/payments/refund/providers/paypal.js +239 -0
  326. package/dist/manager/routes/payments/refund/providers/stripe.js +160 -0
  327. package/dist/manager/routes/payments/refund/providers/test.js +204 -0
  328. package/dist/manager/routes/payments/trial-eligibility/get.js +29 -0
  329. package/dist/manager/routes/payments/uncancel/post.js +106 -0
  330. package/dist/manager/routes/payments/uncancel/providers/chargebee.js +30 -0
  331. package/dist/manager/routes/payments/uncancel/providers/paypal.js +25 -0
  332. package/dist/manager/routes/payments/uncancel/providers/stripe.js +27 -0
  333. package/dist/manager/routes/payments/uncancel/providers/test.js +114 -0
  334. package/dist/manager/routes/payments/webhook/post.js +157 -0
  335. package/dist/manager/routes/payments/webhook/providers/chargebee.js +218 -0
  336. package/dist/manager/routes/payments/webhook/providers/coinbase.js +91 -0
  337. package/dist/manager/routes/payments/webhook/providers/paypal.js +214 -0
  338. package/dist/manager/routes/payments/webhook/providers/stripe.js +182 -0
  339. package/dist/manager/routes/payments/webhook/providers/test.js +18 -0
  340. package/dist/manager/routes/payments/winback/post.js +276 -0
  341. package/dist/manager/routes/payments/winback/providers/chargebee.js +24 -0
  342. package/dist/manager/routes/payments/winback/providers/paypal.js +22 -0
  343. package/dist/manager/routes/payments/winback/providers/stripe.js +41 -0
  344. package/dist/manager/routes/payments/winback/providers/test.js +42 -0
  345. package/dist/manager/routes/restart/index.js +37 -0
  346. package/dist/manager/routes/special/electron-client/post.js +59 -0
  347. package/dist/manager/routes/test/authenticate/get.js +11 -0
  348. package/dist/manager/routes/test/health/get.js +8 -0
  349. package/dist/manager/routes/test/index.js +16 -0
  350. package/dist/manager/routes/test/lab/post.js +17 -0
  351. package/dist/manager/routes/test/redirect/get.js +27 -0
  352. package/dist/manager/routes/test/reset-account/post.js +79 -0
  353. package/dist/manager/routes/test/roster/get.js +40 -0
  354. package/dist/manager/routes/test/schema/post.js +20 -0
  355. package/dist/manager/routes/test/usage/post.js +44 -0
  356. package/dist/manager/routes/test/webhook/post.js +21 -0
  357. package/dist/manager/routes/user/api-keys/post.js +51 -0
  358. package/dist/manager/routes/user/connections/_context.js +194 -0
  359. package/dist/manager/routes/user/connections/_grant.js +247 -0
  360. package/dist/manager/routes/user/connections/_lease.js +184 -0
  361. package/dist/manager/routes/user/connections/_providers.js +191 -0
  362. package/dist/manager/routes/user/connections/_state.js +117 -0
  363. package/dist/manager/routes/user/connections/delete.js +55 -0
  364. package/dist/manager/routes/user/connections/get.js +167 -0
  365. package/dist/manager/routes/user/connections/post.js +370 -0
  366. package/dist/manager/routes/user/connections/providers/discord.js +41 -0
  367. package/dist/manager/routes/user/connections/providers/google.js +55 -0
  368. package/dist/manager/routes/user/connections/providers/kick.js +68 -0
  369. package/dist/manager/routes/user/connections/providers/spotify.js +40 -0
  370. package/dist/manager/routes/user/connections/providers/twitch.js +52 -0
  371. package/dist/manager/routes/user/data-request/delete.js +84 -0
  372. package/dist/manager/routes/user/data-request/get.js +229 -0
  373. package/dist/manager/routes/user/data-request/post.js +136 -0
  374. package/dist/manager/routes/user/delete.js +149 -0
  375. package/dist/manager/routes/user/feedback/post.js +81 -0
  376. package/dist/manager/routes/user/get.js +14 -0
  377. package/dist/manager/routes/user/orders/get.js +95 -0
  378. package/dist/manager/routes/user/sessions/delete.js +95 -0
  379. package/dist/manager/routes/user/sessions/get.js +40 -0
  380. package/dist/manager/routes/user/settings/validate/post.js +86 -0
  381. package/dist/manager/routes/user/signup/post.js +662 -0
  382. package/dist/manager/routes/user/subscription/get.js +77 -0
  383. package/dist/manager/routes/user/token/post.js +30 -0
  384. package/dist/manager/routes/verts/delete.js +43 -0
  385. package/dist/manager/routes/verts/get.js +49 -0
  386. package/dist/manager/routes/verts/post.js +61 -0
  387. package/dist/manager/routes/verts/put.js +75 -0
  388. package/dist/manager/routes/verts/redirect/get.js +36 -0
  389. package/dist/manager/routes/verts/serve/get.js +67 -0
  390. package/dist/manager/routes/verts/utils.js +436 -0
  391. package/dist/manager/schemas/admin/backup/post.js +8 -0
  392. package/dist/manager/schemas/admin/cron/post.js +8 -0
  393. package/dist/manager/schemas/admin/database/get.js +5 -0
  394. package/dist/manager/schemas/admin/database/post.js +6 -0
  395. package/dist/manager/schemas/admin/email/post.js +31 -0
  396. package/dist/manager/schemas/admin/firestore/get.js +5 -0
  397. package/dist/manager/schemas/admin/firestore/post.js +8 -0
  398. package/dist/manager/schemas/admin/firestore/query/post.js +5 -0
  399. package/dist/manager/schemas/admin/hook/post.js +8 -0
  400. package/dist/manager/schemas/admin/infer-contact/post.js +6 -0
  401. package/dist/manager/schemas/admin/notification/post.js +23 -0
  402. package/dist/manager/schemas/admin/payment/post.js +8 -0
  403. package/dist/manager/schemas/admin/post/post.js +23 -0
  404. package/dist/manager/schemas/admin/post/put.js +13 -0
  405. package/dist/manager/schemas/admin/repo/content/post.js +10 -0
  406. package/dist/manager/schemas/admin/stats/get.js +5 -0
  407. package/dist/manager/schemas/admin/users/disable/post.js +9 -0
  408. package/dist/manager/schemas/admin/users/list/get.js +10 -0
  409. package/dist/manager/schemas/admin/users/sync/post.js +7 -0
  410. package/dist/manager/schemas/brand/get.js +3 -0
  411. package/dist/manager/schemas/content/post/get.js +8 -0
  412. package/dist/manager/schemas/general/email/post.js +10 -0
  413. package/dist/manager/schemas/general/uuid/post.js +9 -0
  414. package/dist/manager/schemas/handler/post/post.js +13 -0
  415. package/dist/manager/schemas/health/get.js +3 -0
  416. package/dist/manager/schemas/marketing/campaign/delete.js +8 -0
  417. package/dist/manager/schemas/marketing/campaign/get.js +13 -0
  418. package/dist/manager/schemas/marketing/campaign/post.js +43 -0
  419. package/dist/manager/schemas/marketing/campaign/put.js +37 -0
  420. package/dist/manager/schemas/marketing/contact/delete.js +8 -0
  421. package/dist/manager/schemas/marketing/contact/post.js +14 -0
  422. package/dist/manager/schemas/marketing/contact/put.js +8 -0
  423. package/dist/manager/schemas/marketing/email-preferences/post.js +15 -0
  424. package/dist/manager/schemas/marketing/webhook/forward/post.js +8 -0
  425. package/dist/manager/schemas/marketing/webhook/post.js +7 -0
  426. package/dist/manager/schemas/payments/cancel/post.js +15 -0
  427. package/dist/manager/schemas/payments/discount/get.js +8 -0
  428. package/dist/manager/schemas/payments/dispute-alert/post.js +6 -0
  429. package/dist/manager/schemas/payments/intent/post.js +28 -0
  430. package/dist/manager/schemas/payments/plan/post.js +11 -0
  431. package/dist/manager/schemas/payments/portal/post.js +12 -0
  432. package/dist/manager/schemas/payments/refund/post.js +14 -0
  433. package/dist/manager/schemas/payments/trial-eligibility/get.js +7 -0
  434. package/dist/manager/schemas/payments/uncancel/post.js +9 -0
  435. package/dist/manager/schemas/payments/webhook/post.js +6 -0
  436. package/dist/manager/schemas/payments/winback/post.js +12 -0
  437. package/dist/manager/schemas/restart/index.js +5 -0
  438. package/dist/manager/schemas/special/electron-client/post.js +11 -0
  439. package/dist/manager/schemas/test/authenticate/get.js +3 -0
  440. package/dist/manager/schemas/test/health/get.js +3 -0
  441. package/dist/manager/schemas/test/index.js +5 -0
  442. package/dist/manager/schemas/test/lab/post.js +3 -0
  443. package/dist/manager/schemas/test/redirect/get.js +7 -0
  444. package/dist/manager/schemas/test/reset-account/post.js +3 -0
  445. package/dist/manager/schemas/test/roster/get.js +3 -0
  446. package/dist/manager/schemas/test/schema/post.js +70 -0
  447. package/dist/manager/schemas/test/usage/post.js +8 -0
  448. package/dist/manager/schemas/test/webhook/post.js +7 -0
  449. package/dist/manager/schemas/user/api-keys/post.js +6 -0
  450. package/dist/manager/schemas/user/connections/delete.js +8 -0
  451. package/dist/manager/schemas/user/connections/get.js +21 -0
  452. package/dist/manager/schemas/user/connections/post.js +12 -0
  453. package/dist/manager/schemas/user/data-request/delete.js +3 -0
  454. package/dist/manager/schemas/user/data-request/get.js +5 -0
  455. package/dist/manager/schemas/user/data-request/post.js +6 -0
  456. package/dist/manager/schemas/user/delete.js +6 -0
  457. package/dist/manager/schemas/user/feedback/post.js +8 -0
  458. package/dist/manager/schemas/user/get.js +3 -0
  459. package/dist/manager/schemas/user/orders/get.js +6 -0
  460. package/dist/manager/schemas/user/sessions/delete.js +6 -0
  461. package/dist/manager/schemas/user/sessions/get.js +6 -0
  462. package/dist/manager/schemas/user/settings/validate/post.js +8 -0
  463. package/dist/manager/schemas/user/signup/post.js +27 -0
  464. package/dist/manager/schemas/user/subscription/get.js +5 -0
  465. package/dist/manager/schemas/user/token/post.js +5 -0
  466. package/dist/manager/schemas/verts/delete.js +8 -0
  467. package/dist/manager/schemas/verts/get.js +9 -0
  468. package/dist/manager/schemas/verts/post.js +23 -0
  469. package/dist/manager/schemas/verts/put.js +20 -0
  470. package/dist/manager/schemas/verts/redirect/get.js +10 -0
  471. package/dist/manager/schemas/verts/serve/get.js +14 -0
  472. package/dist/manager/server-manager.js +69 -0
  473. package/dist/mcp/client.js +98 -0
  474. package/dist/mcp/handler.js +521 -0
  475. package/dist/mcp/index.js +157 -0
  476. package/dist/mcp/tools.js +555 -0
  477. package/dist/mcp/utils.js +108 -0
  478. package/dist/omega-bin.js +6 -0
  479. package/dist/require.js +3 -0
  480. package/dist/test/fixtures/firebase-project/.firebaserc +5 -0
  481. package/dist/test/fixtures/firebase-project/config/omega.json5 +131 -0
  482. package/dist/test/fixtures/firebase-project/database.rules.json +6 -0
  483. package/dist/test/fixtures/firebase-project/firebase.json +49 -0
  484. package/dist/test/fixtures/firebase-project/firestore.indexes.json +4 -0
  485. package/dist/test/fixtures/firebase-project/package.json +14 -0
  486. package/dist/test/fixtures/firebase-project/src/index.js +11 -0
  487. package/dist/test/fixtures/firebase-project/storage.rules +8 -0
  488. package/dist/test/parse-audit.js +25 -0
  489. package/dist/test/run-tests.js +90 -0
  490. package/dist/test/runner.js +982 -0
  491. package/dist/test/seed.js +242 -0
  492. package/dist/test/test-accounts.js +2660 -0
  493. package/dist/test/utils/assertions.js +189 -0
  494. package/dist/test/utils/email-capture.js +252 -0
  495. package/dist/test/utils/extended-mode-warning.js +11 -0
  496. package/dist/test/utils/firestore-rules-client.js +277 -0
  497. package/dist/test/utils/http-client.js +228 -0
  498. package/dist/test/utils/test-mode-file.js +192 -0
  499. package/dist/utils/merge-line-files.js +6 -0
  500. package/dist/utils/scaffold-defaults.js +71 -0
  501. package/dist/utils/test-lanes.js +62 -0
  502. package/dist/vendor/account/engine.js +182 -0
  503. package/dist/vendor/account/features.js +220 -0
  504. package/dist/vendor/account/index.js +53 -0
  505. package/dist/vendor/account/schema.js +272 -0
  506. package/dist/vendor/account/subscription.js +38 -0
  507. package/dist/vendor/analytics/adapters/ga4.js +26 -0
  508. package/dist/vendor/analytics/adapters/meta.js +26 -0
  509. package/dist/vendor/analytics/adapters/resolve.js +130 -0
  510. package/dist/vendor/analytics/adapters/tiktok.js +27 -0
  511. package/dist/vendor/analytics/catalog.js +908 -0
  512. package/dist/vendor/analytics/identity.js +136 -0
  513. package/dist/vendor/config/company.js +31 -0
  514. package/dist/vendor/config/defaults.js +173 -0
  515. package/dist/vendor/config/demo.js +18 -0
  516. package/dist/vendor/config/desktop-artifacts.js +110 -0
  517. package/dist/vendor/config/edit.js +769 -0
  518. package/dist/vendor/config/env-delivery.js +145 -0
  519. package/dist/vendor/config/env-rules.js +93 -0
  520. package/dist/vendor/config/env-schema.js +1078 -0
  521. package/dist/vendor/config/env.js +445 -0
  522. package/dist/vendor/config/hooks.js +97 -0
  523. package/dist/vendor/config/index.js +237 -0
  524. package/dist/vendor/config/instances.js +208 -0
  525. package/dist/vendor/config/load.js +490 -0
  526. package/dist/vendor/config/merge.js +68 -0
  527. package/dist/vendor/config/order.js +139 -0
  528. package/dist/vendor/config/ports.js +374 -0
  529. package/dist/vendor/config/providers.js +32 -0
  530. package/dist/vendor/config/repo.js +142 -0
  531. package/dist/vendor/config/retired-keys.js +430 -0
  532. package/dist/vendor/config/schema.js +1610 -0
  533. package/dist/vendor/config/secrets.js +50 -0
  534. package/dist/vendor/config/seed.js +34 -0
  535. package/dist/vendor/config/site-global.js +205 -0
  536. package/dist/vendor/config/validate.js +554 -0
  537. package/dist/vendor/config/winback.js +61 -0
  538. package/dist/vendor/devkit/attach-log-file.js +262 -0
  539. package/dist/vendor/devkit/bare-requires.js +153 -0
  540. package/dist/vendor/devkit/command-path.js +46 -0
  541. package/dist/vendor/devkit/defaults-engine.js +372 -0
  542. package/dist/vendor/devkit/deploy-record.js +180 -0
  543. package/dist/vendor/devkit/deploy.js +286 -0
  544. package/dist/vendor/devkit/license.js +155 -0
  545. package/dist/vendor/devkit/local-https.js +360 -0
  546. package/dist/vendor/devkit/local.js +1905 -0
  547. package/dist/vendor/devkit/logger.js +128 -0
  548. package/dist/vendor/devkit/merge-line-files.js +296 -0
  549. package/dist/vendor/devkit/npm-registry.js +52 -0
  550. package/dist/vendor/devkit/omega-bin.js +345 -0
  551. package/dist/vendor/devkit/parse-audit.js +74 -0
  552. package/dist/vendor/devkit/safe-install.js +18 -0
  553. package/dist/vendor/devkit/scaffold-guard.js +96 -0
  554. package/dist/vendor/devkit/stop-signals.js +28 -0
  555. package/dist/vendor/devkit/test/assert.js +120 -0
  556. package/dist/vendor/devkit/test/define-cases.js +104 -0
  557. package/dist/vendor/devkit/test/runner-core.js +554 -0
  558. package/dist/vendor/devkit/test/scope.js +162 -0
  559. package/dist/vendor/devkit/update.js +569 -0
  560. package/dist/vendor/monitoring/core.js +180 -0
  561. package/dist/vendor/monitoring/env.js +49 -0
  562. package/dist/vendor/monitoring/logger.js +39 -0
  563. package/dist/vendor/monitoring/node.js +72 -0
  564. package/docs/admin-post-route.md +57 -0
  565. package/docs/ai-library.md +171 -0
  566. package/docs/architecture.md +62 -0
  567. package/docs/audit.md +70 -0
  568. package/docs/auth-hooks.md +74 -0
  569. package/docs/build-system.md +65 -0
  570. package/docs/cdp-debugging.md +29 -0
  571. package/docs/cli-firestore-auth.md +85 -0
  572. package/docs/cli-logs.md +67 -0
  573. package/docs/cli-output.md +146 -0
  574. package/docs/code-patterns.md +77 -0
  575. package/docs/common-mistakes.md +12 -0
  576. package/docs/common-operations.md +60 -0
  577. package/docs/connections.md +212 -0
  578. package/docs/consent.md +362 -0
  579. package/docs/directory-structure.md +150 -0
  580. package/docs/email-system.md +459 -0
  581. package/docs/environment-detection.md +93 -0
  582. package/docs/file-naming.md +10 -0
  583. package/docs/firestore.md +134 -0
  584. package/docs/ghostii.md +240 -0
  585. package/docs/index.md +299 -0
  586. package/docs/key-files.md +37 -0
  587. package/docs/logging.md +60 -0
  588. package/docs/marketing-campaigns.md +407 -0
  589. package/docs/marketing-fields.md +25 -0
  590. package/docs/mcp.md +222 -0
  591. package/docs/migration.md +129 -0
  592. package/docs/payment-system.md +856 -0
  593. package/docs/paypal-sandbox-qa.md +105 -0
  594. package/docs/response-headers.md +9 -0
  595. package/docs/routes.md +211 -0
  596. package/docs/sanitization.md +71 -0
  597. package/docs/schemas.md +163 -0
  598. package/docs/shared/agent-docs.md +89 -0
  599. package/docs/shared/analytics.md +612 -0
  600. package/docs/shared/brands.md +51 -0
  601. package/docs/shared/breaking-changes.md +497 -0
  602. package/docs/shared/config.md +1387 -0
  603. package/docs/shared/deploys.md +215 -0
  604. package/docs/shared/icons.md +201 -0
  605. package/docs/shared/local-dev.md +147 -0
  606. package/docs/shared/logging.md +202 -0
  607. package/docs/shared/monitoring.md +153 -0
  608. package/docs/shared/publishing.md +183 -0
  609. package/docs/shared/rulings.md +34 -0
  610. package/docs/shared/testing.md +147 -0
  611. package/docs/shared/theming.md +604 -0
  612. package/docs/shared/translation.md +291 -0
  613. package/docs/shared/updates.md +61 -0
  614. package/docs/stripe-webhook-forwarding.md +20 -0
  615. package/docs/test-boot-layer.md +67 -0
  616. package/docs/test-framework.md +583 -0
  617. package/docs/usage-rate-limiting.md +121 -0
  618. package/docs/verts.md +29 -0
  619. package/package.json +143 -0
  620. package/templates/config/omega.json5 +322 -0
  621. package/templates/database.rules.json +82 -0
  622. package/templates/firebase.json +67 -0
  623. package/templates/firestore.framework.rules +191 -0
  624. package/templates/firestore.indexes.json +4 -0
  625. package/templates/firestore.rules +59 -0
  626. package/templates/index.js +12 -0
  627. package/templates/public/404.html +26 -0
  628. package/templates/public/index.html +24 -0
  629. package/templates/remoteconfig.template.json +1 -0
  630. package/templates/storage-lifecycle-config-1-day.json +9 -0
  631. package/templates/storage-lifecycle-config-30-days.json +9 -0
  632. package/templates/storage.rules +11 -0
  633. package/test/_init/accounts-validation.js +58 -0
  634. package/test/ai/tools-live.test.js +171 -0
  635. package/test/analytics/conversion-delivery.test.js +1013 -0
  636. package/test/analytics/match-normalization.test.js +238 -0
  637. package/test/analytics/signup-conversion.test.js +135 -0
  638. package/test/boot/cli-dispatch.test.js +142 -0
  639. package/test/boot/defaults-scaffold.test.js +248 -0
  640. package/test/boot/deploy-staging.test.js +446 -0
  641. package/test/boot/disposable-domains.test.js +154 -0
  642. package/test/boot/emulator-boots.test.js +39 -0
  643. package/test/boot/emulator-port-preflight.test.js +126 -0
  644. package/test/boot/emulator-ready-timeout.test.js +46 -0
  645. package/test/boot/project-id-refresh.test.js +73 -0
  646. package/test/boot/suite-portability.test.js +278 -0
  647. package/test/boot/update-command.test.js +63 -0
  648. package/test/cli/cross-platform.test.js +168 -0
  649. package/test/cli/custom-project-type.test.js +426 -0
  650. package/test/cli/deploy-license-env.test.js +100 -0
  651. package/test/cli/emulator-adoption.test.js +254 -0
  652. package/test/cli/emulator-orphans.test.js +658 -0
  653. package/test/cli/emulator-ownership.test.js +177 -0
  654. package/test/cli/emulator-port-retry.test.js +395 -0
  655. package/test/cli/emulator-shutdown.test.js +788 -0
  656. package/test/cli/emulator-stale-reap.test.js +574 -0
  657. package/test/cli/ensure-target.test.js +325 -0
  658. package/test/cli/flags.test.js +64 -0
  659. package/test/cli/https-trust.test.js +38 -0
  660. package/test/cli/install-update-dispatch.test.js +47 -0
  661. package/test/cli/lane-environments.test.js +159 -0
  662. package/test/cli/marketing-campaigns-seeded.test.js +194 -0
  663. package/test/cli/migrate-bare-requires.test.js +128 -0
  664. package/test/cli/migrate-markers.test.js +530 -0
  665. package/test/cli/required-indexes.test.js +64 -0
  666. package/test/cli/rules-compile.test.js +622 -0
  667. package/test/cli/rules-migration-deferral.test.js +259 -0
  668. package/test/cli/rules-version.test.js +104 -0
  669. package/test/cli/setup-load-files.test.js +66 -0
  670. package/test/cli/setup-offline-mode.test.js +276 -0
  671. package/test/cli/setup-retired.test.js +64 -0
  672. package/test/cli/setup-shared-project-indexes.test.js +262 -0
  673. package/test/cli/setup-tests-requires.test.js +45 -0
  674. package/test/cli/stage-env-compose.test.js +316 -0
  675. package/test/cli/stripe-live-lane.test.js +303 -0
  676. package/test/cli/target.test.js +182 -0
  677. package/test/cli/templates.test.js +66 -0
  678. package/test/cli/test-runner-env.test.js +115 -0
  679. package/test/cli/test-stack-shutdown.test.js +275 -0
  680. package/test/cli/test-target-no-match.test.js +128 -0
  681. package/test/cli/verb-logs.test.js +118 -0
  682. package/test/cli/version-dispatch.test.js +39 -0
  683. package/test/content/blog-generate.test.js +164 -0
  684. package/test/email/campaign-config-fault.test.js +376 -0
  685. package/test/email/campaign-cron-pipeline.test.js +536 -0
  686. package/test/email/campaign-send.test.js +47 -0
  687. package/test/email/consent-lifecycle.test.js +258 -0
  688. package/test/email/content-html-policy.test.js +270 -0
  689. package/test/email/feedback-and-plain-send.test.js +54 -0
  690. package/test/email/fixtures/clean.json +30 -0
  691. package/test/email/fixtures/editorial.json +30 -0
  692. package/test/email/fixtures/field-report.json +53 -0
  693. package/test/email/identity.test.js +321 -0
  694. package/test/email/marketing/consent-gate.test.js +265 -0
  695. package/test/email/marketing/custom-fields-catalog.test.js +259 -0
  696. package/test/email/marketing/prune-per-provider.test.js +762 -0
  697. package/test/email/marketing/remove-log-privacy.test.js +225 -0
  698. package/test/email/marketing-lifecycle.test.js +139 -0
  699. package/test/email/newsletter-generate.test.js +853 -0
  700. package/test/email/newsletter-svg-tokens.test.js +51 -0
  701. package/test/email/newsletter-templates.test.js +492 -0
  702. package/test/email/order-one-time-cta.test.js +97 -0
  703. package/test/email/render-content.test.js +248 -0
  704. package/test/email/safe-url.test.js +0 -0
  705. package/test/email/sanitize-images.test.js +66 -0
  706. package/test/email/send-log-privacy.test.js +107 -0
  707. package/test/email/templates.test.js +288 -0
  708. package/test/email/testing-capture.test.js +260 -0
  709. package/test/email/transactional-send.test.js +36 -0
  710. package/test/email/transactional.test.js +562 -0
  711. package/test/email/unsubscribe-groups.test.js +155 -0
  712. package/test/email/unsubscribe-key.test.js +91 -0
  713. package/test/email/validation-cases.test.js +152 -0
  714. package/test/email/validation.test.js +746 -0
  715. package/test/events/auth-delete-conversion.test.js +138 -0
  716. package/test/events/auth-delete-race.test.js +211 -0
  717. package/test/events/auth-on-create-log.test.js +164 -0
  718. package/test/events/auth-signup-conversion.test.js +141 -0
  719. package/test/events/auth-signup-limit.test.js +142 -0
  720. package/test/events/auth-trigger-log-privacy.test.js +256 -0
  721. package/test/events/cron-job-doc-shape.test.js +101 -0
  722. package/test/events/cron-reset-usage.test.js +116 -0
  723. package/test/events/notification-conversion.test.js +182 -0
  724. package/test/events/payments/_webhook-harness.js +307 -0
  725. package/test/events/payments/abandoned-cart-activity.test.js +135 -0
  726. package/test/events/payments/analytics-payment-events.test.js +945 -0
  727. package/test/events/payments/dispute-email-status.test.js +55 -0
  728. package/test/events/payments/journey-payments-abandoned.test.js +126 -0
  729. package/test/events/payments/journey-payments-cancel-endpoint.test.js +102 -0
  730. package/test/events/payments/journey-payments-cancel-no-order.test.js +76 -0
  731. package/test/events/payments/journey-payments-cancel.test.js +184 -0
  732. package/test/events/payments/journey-payments-decline.test.js +225 -0
  733. package/test/events/payments/journey-payments-discount.test.js +91 -0
  734. package/test/events/payments/journey-payments-dispute.test.js +190 -0
  735. package/test/events/payments/journey-payments-failure.test.js +148 -0
  736. package/test/events/payments/journey-payments-legacy-product.test.js +151 -0
  737. package/test/events/payments/journey-payments-one-time-decline.test.js +127 -0
  738. package/test/events/payments/journey-payments-one-time-failure.test.js +113 -0
  739. package/test/events/payments/journey-payments-one-time-refund.test.js +186 -0
  740. package/test/events/payments/journey-payments-one-time.test.js +175 -0
  741. package/test/events/payments/journey-payments-plan-change.test.js +146 -0
  742. package/test/events/payments/journey-payments-plan-switch-trial.test.js +161 -0
  743. package/test/events/payments/journey-payments-plan-switch.test.js +182 -0
  744. package/test/events/payments/journey-payments-refund-no-order.test.js +76 -0
  745. package/test/events/payments/journey-payments-refund-webhook.test.js +225 -0
  746. package/test/events/payments/journey-payments-suspend.test.js +183 -0
  747. package/test/events/payments/journey-payments-trial-cancel.test.js +132 -0
  748. package/test/events/payments/journey-payments-trial.test.js +173 -0
  749. package/test/events/payments/journey-payments-uid-resolution.test.js +134 -0
  750. package/test/events/payments/journey-payments-uncancel.test.js +157 -0
  751. package/test/events/payments/journey-payments-upgrade.test.js +137 -0
  752. package/test/events/payments/journey-payments-winback-decline.test.js +171 -0
  753. package/test/events/payments/journey-payments-winback.test.js +195 -0
  754. package/test/events/payments/paypal-expiry-cron.test.js +166 -0
  755. package/test/events/payments/purchase-failed-handler.test.js +80 -0
  756. package/test/events/payments/purchase-refunded-handler.test.js +108 -0
  757. package/test/events/payments/test-processor-doc-shape.test.js +162 -0
  758. package/test/events/payments/transition-order-emails.test.js +185 -0
  759. package/test/events/payments/transition-promo-lines.test.js +192 -0
  760. package/test/events/payments/transitions-detect.test.js +767 -0
  761. package/test/events/payments/trial-lapse-sweep-staleness.test.js +173 -0
  762. package/test/events/payments/trial-lapse-sweep.test.js +359 -0
  763. package/test/events/payments/webhook-atomic-writes.test.js +69 -0
  764. package/test/events/payments/webhook-chargebee-unreachable.test.js +106 -0
  765. package/test/events/payments/webhook-discount-clear.test.js +189 -0
  766. package/test/events/payments/webhook-failure-intent.test.js +93 -0
  767. package/test/events/payments/webhook-hosted-page-uid-trust.test.js +212 -0
  768. package/test/events/payments/webhook-ordering.test.js +326 -0
  769. package/test/events/payments/webhook-provider-lookup-trust.test.js +206 -0
  770. package/test/events/payments/webhook-refund-amount-trust.test.js +223 -0
  771. package/test/events/payments/webhook-refund-envelope-shape.test.js +148 -0
  772. package/test/events/payments/webhook-refund-linkage-trust.test.js +404 -0
  773. package/test/events/payments/webhook-refund-transaction-fallback.test.js +184 -0
  774. package/test/events/payments/webhook-refund-without-order.test.js +170 -0
  775. package/test/events/payments/webhook-refusal-reporting.test.js +194 -0
  776. package/test/events/payments/webhook-refused-intent.test.js +142 -0
  777. package/test/events/payments/webhook-retry-sweep.test.js +252 -0
  778. package/test/events/payments/webhook-transition-claim.test.js +389 -0
  779. package/test/events/payments/webhook-uid-trust.test.js +203 -0
  780. package/test/events/payments/webhook-user-without-auth.test.js +161 -0
  781. package/test/fixtures/chargebee/invoice-one-time.json +27 -0
  782. package/test/fixtures/chargebee/subscription-active.json +44 -0
  783. package/test/fixtures/chargebee/subscription-cancelled.json +42 -0
  784. package/test/fixtures/chargebee/subscription-in-trial.json +41 -0
  785. package/test/fixtures/chargebee/subscription-legacy-plan.json +41 -0
  786. package/test/fixtures/chargebee/subscription-non-renewing.json +41 -0
  787. package/test/fixtures/chargebee/subscription-paused.json +42 -0
  788. package/test/fixtures/chargebee/webhook-payment-failed.json +51 -0
  789. package/test/fixtures/chargebee/webhook-subscription-created.json +47 -0
  790. package/test/fixtures/coinbase/charge-confirmed.json +50 -0
  791. package/test/fixtures/coinbase/charge-failed.json +34 -0
  792. package/test/fixtures/coinbase/charge-pending.json +34 -0
  793. package/test/fixtures/migrate/ported-route.js.txt +11 -0
  794. package/test/fixtures/paypal/capture-completed.json +51 -0
  795. package/test/fixtures/paypal/capture-refunded.json +43 -0
  796. package/test/fixtures/paypal/order-approved.json +62 -0
  797. package/test/fixtures/paypal/order-completed.json +110 -0
  798. package/test/fixtures/paypal/sale-refunded.json +38 -0
  799. package/test/fixtures/paypal/subscription-active.json +76 -0
  800. package/test/fixtures/paypal/subscription-cancelled.json +50 -0
  801. package/test/fixtures/paypal/subscription-suspended.json +65 -0
  802. package/test/fixtures/stripe/checkout-session-completed.json +130 -0
  803. package/test/fixtures/stripe/invoice-payment-failed.json +148 -0
  804. package/test/fixtures/stripe/invoice-subscription-payment-failed.json +28 -0
  805. package/test/fixtures/stripe/invoice-subscription-payment-succeeded.json +28 -0
  806. package/test/fixtures/stripe/subscription-active.json +161 -0
  807. package/test/fixtures/stripe/subscription-canceled.json +161 -0
  808. package/test/fixtures/stripe/subscription-trialing.json +161 -0
  809. package/test/helpers/_shared-config.js +15 -0
  810. package/test/helpers/ai-request-payload.test.js +619 -0
  811. package/test/helpers/ai-schema-resolve.test.js +125 -0
  812. package/test/helpers/ai-test-provider.test.js +227 -0
  813. package/test/helpers/ai-token-accounting.test.js +150 -0
  814. package/test/helpers/ai-tools-format.test.js +384 -0
  815. package/test/helpers/analytics-no-id-notice.test.js +146 -0
  816. package/test/helpers/analytics-user-data.test.js +194 -0
  817. package/test/helpers/api-manager.test.js +311 -0
  818. package/test/helpers/backend-router.test.js +136 -0
  819. package/test/helpers/content/blog-auto-publisher.test.js +482 -0
  820. package/test/helpers/content/feed-parser.test.js +529 -0
  821. package/test/helpers/content/ghostii-blocks.test.js +135 -0
  822. package/test/helpers/content/ghostii-feed-integration.test.js +405 -0
  823. package/test/helpers/content/ghostii-write-article.test.js +244 -0
  824. package/test/helpers/dev-only-routes.test.js +225 -0
  825. package/test/helpers/env-reader.test.js +362 -0
  826. package/test/helpers/environment.test.js +263 -0
  827. package/test/helpers/event-middleware.test.js +407 -0
  828. package/test/helpers/infer-contact.test.js +157 -0
  829. package/test/helpers/lane-url.test.js +90 -0
  830. package/test/helpers/merge-line-files.test.js +272 -0
  831. package/test/helpers/metadata.test.js +122 -0
  832. package/test/helpers/middleware-request-log.test.js +134 -0
  833. package/test/helpers/middleware-user-log.test.js +124 -0
  834. package/test/helpers/payment/chargebee/parse-webhook.test.js +490 -0
  835. package/test/helpers/payment/chargebee/refund-details.test.js +198 -0
  836. package/test/helpers/payment/chargebee/to-unified-one-time.test.js +148 -0
  837. package/test/helpers/payment/chargebee/to-unified-subscription.test.js +649 -0
  838. package/test/helpers/payment/coinbase/create-intent.test.js +247 -0
  839. package/test/helpers/payment/coinbase/parse-webhook.test.js +195 -0
  840. package/test/helpers/payment/coinbase/refund-unsupported.test.js +141 -0
  841. package/test/helpers/payment/coinbase/to-unified-one-time.test.js +201 -0
  842. package/test/helpers/payment/discount-codes.test.js +141 -0
  843. package/test/helpers/payment/extract-resource.test.js +113 -0
  844. package/test/helpers/payment/fetch-failure.test.js +215 -0
  845. package/test/helpers/payment/license-gate.test.js +108 -0
  846. package/test/helpers/payment/order-id.test.js +99 -0
  847. package/test/helpers/payment/paypal/create-intent.test.js +382 -0
  848. package/test/helpers/payment/paypal/fetch-capture.test.js +123 -0
  849. package/test/helpers/payment/paypal/fetch-sale.test.js +183 -0
  850. package/test/helpers/payment/paypal/parse-webhook.test.js +678 -0
  851. package/test/helpers/payment/paypal/refund-details.test.js +202 -0
  852. package/test/helpers/payment/paypal/resolve-plan-id.test.js +108 -0
  853. package/test/helpers/payment/paypal/switch-plan.test.js +92 -0
  854. package/test/helpers/payment/paypal/to-unified-one-time.test.js +383 -0
  855. package/test/helpers/payment/paypal/to-unified-subscription.test.js +884 -0
  856. package/test/helpers/payment/stripe/fetch-charge.test.js +147 -0
  857. package/test/helpers/payment/stripe/parse-webhook.test.js +448 -0
  858. package/test/helpers/payment/stripe/refund-details.test.js +147 -0
  859. package/test/helpers/payment/stripe/to-unified-one-time.test.js +307 -0
  860. package/test/helpers/payment/stripe/to-unified-subscription.test.js +709 -0
  861. package/test/helpers/persona-domain.test.js +87 -0
  862. package/test/helpers/recaptcha.test.js +166 -0
  863. package/test/helpers/resolved-config.test.js +106 -0
  864. package/test/helpers/response-log-redaction.test.js +262 -0
  865. package/test/helpers/roles.test.js +155 -0
  866. package/test/helpers/route-context-debug-gate.test.js +118 -0
  867. package/test/helpers/route-context-logging.test.js +214 -0
  868. package/test/helpers/route-context.test.js +129 -0
  869. package/test/helpers/safe-compare.test.js +114 -0
  870. package/test/helpers/sanitize.test.js +223 -0
  871. package/test/helpers/schema-engine.test.js +537 -0
  872. package/test/helpers/schema-zod.test.js +554 -0
  873. package/test/helpers/seed-accounts-load.test.js +163 -0
  874. package/test/helpers/seed-google-personas.test.js +114 -0
  875. package/test/helpers/seeded-personas.test.js +823 -0
  876. package/test/helpers/settings.test.js +292 -0
  877. package/test/helpers/setup-engines-pin.test.js +68 -0
  878. package/test/helpers/setup-manifest-sync.test.js +116 -0
  879. package/test/helpers/slugify.test.js +395 -0
  880. package/test/helpers/storage.test.js +195 -0
  881. package/test/helpers/test-banner-latch.test.js +139 -0
  882. package/test/helpers/usage-consume.test.js +460 -0
  883. package/test/helpers/usage-log-privacy.test.js +121 -0
  884. package/test/helpers/user-doc-heal.test.js +540 -0
  885. package/test/helpers/user.test.js +771 -0
  886. package/test/helpers/webhook-forward.test.js +419 -0
  887. package/test/helpers/wipe-auth-project.test.js +143 -0
  888. package/test/mcp/discovery.test.js +52 -0
  889. package/test/mcp/oauth.test.js +160 -0
  890. package/test/mcp/protocol.test.js +278 -0
  891. package/test/mcp/roles.test.js +203 -0
  892. package/test/mcp/utils.test.js +245 -0
  893. package/test/notification/identity.test.js +155 -0
  894. package/test/routes/admin/create-post.test.js +362 -0
  895. package/test/routes/admin/database.test.js +133 -0
  896. package/test/routes/admin/deduplicate-image-alts.test.js +191 -0
  897. package/test/routes/admin/email-content-html.test.js +58 -0
  898. package/test/routes/admin/email-request-log.test.js +86 -0
  899. package/test/routes/admin/email.test.js +116 -0
  900. package/test/routes/admin/firestore-query.test.js +206 -0
  901. package/test/routes/admin/firestore.test.js +129 -0
  902. package/test/routes/admin/infer-contact.test.js +220 -0
  903. package/test/routes/admin/notification.test.js +199 -0
  904. package/test/routes/admin/post-convert-image.test.js +159 -0
  905. package/test/routes/admin/post-deploy-flag.test.js +81 -0
  906. package/test/routes/admin/post-download-error.test.js +90 -0
  907. package/test/routes/admin/post-resize-image.test.js +185 -0
  908. package/test/routes/admin/post.test.js +369 -0
  909. package/test/routes/admin/repo-content.test.js +223 -0
  910. package/test/routes/admin/stats.test.js +114 -0
  911. package/test/routes/admin/users-disable.test.js +63 -0
  912. package/test/routes/admin/users-list.test.js +71 -0
  913. package/test/routes/content/post.test.js +60 -0
  914. package/test/routes/general/uuid.test.js +133 -0
  915. package/test/routes/health.test.js +110 -0
  916. package/test/routes/marketing/campaign.test.js +184 -0
  917. package/test/routes/marketing/contact.test.js +416 -0
  918. package/test/routes/marketing/email-preferences.test.js +293 -0
  919. package/test/routes/marketing/push-send.test.js +34 -0
  920. package/test/routes/marketing/webhook-forward.test.js +63 -0
  921. package/test/routes/marketing/webhook.test.js +641 -0
  922. package/test/routes/payments/_route-harness.js +145 -0
  923. package/test/routes/payments/cancel-provider-errors.test.js +227 -0
  924. package/test/routes/payments/cancel-skip-guards.test.js +126 -0
  925. package/test/routes/payments/cancel-trialing.test.js +428 -0
  926. package/test/routes/payments/cancel.test.js +165 -0
  927. package/test/routes/payments/dedup-race.test.js +186 -0
  928. package/test/routes/payments/discount.test.js +82 -0
  929. package/test/routes/payments/dispute-alert.test.js +324 -0
  930. package/test/routes/payments/intent-discount-amounts.test.js +550 -0
  931. package/test/routes/payments/intent-discount-coupons.test.js +293 -0
  932. package/test/routes/payments/intent-one-time-metadata.test.js +122 -0
  933. package/test/routes/payments/intent-purchaser-guard.test.js +139 -0
  934. package/test/routes/payments/intent-zero-total.test.js +260 -0
  935. package/test/routes/payments/intent.test.js +413 -0
  936. package/test/routes/payments/plan.test.js +367 -0
  937. package/test/routes/payments/portal-return-url.test.js +107 -0
  938. package/test/routes/payments/portal.test.js +94 -0
  939. package/test/routes/payments/refund-one-time.test.js +254 -0
  940. package/test/routes/payments/refund-paypal-proration.test.js +190 -0
  941. package/test/routes/payments/refund.test.js +181 -0
  942. package/test/routes/payments/trial-eligibility.test.js +73 -0
  943. package/test/routes/payments/uncancel.test.js +270 -0
  944. package/test/routes/payments/webhook-stripe-invoice.test.js +129 -0
  945. package/test/routes/payments/webhook-stripe-refund-one-time.test.js +130 -0
  946. package/test/routes/payments/webhook-test-provider.test.js +156 -0
  947. package/test/routes/payments/webhook.test.js +115 -0
  948. package/test/routes/payments/winback-stripe-coupon.test.js +126 -0
  949. package/test/routes/payments/winback.test.js +597 -0
  950. package/test/routes/test/authenticate.test.js +79 -0
  951. package/test/routes/test/redirect.test.js +66 -0
  952. package/test/routes/test/reset-account.test.js +126 -0
  953. package/test/routes/test/roster.test.js +101 -0
  954. package/test/routes/test/schema.test.js +556 -0
  955. package/test/routes/test/usage.test.js +374 -0
  956. package/test/routes/user/api-keys.test.js +158 -0
  957. package/test/routes/user/connections-grant.test.js +759 -0
  958. package/test/routes/user/connections-identity.test.js +260 -0
  959. package/test/routes/user/connections-log-privacy.test.js +206 -0
  960. package/test/routes/user/connections-refresh-lease.test.js +696 -0
  961. package/test/routes/user/connections-return.test.js +261 -0
  962. package/test/routes/user/connections-uid.test.js +516 -0
  963. package/test/routes/user/delete.test.js +138 -0
  964. package/test/routes/user/feedback.test.js +106 -0
  965. package/test/routes/user/orders.test.js +233 -0
  966. package/test/routes/user/sessions.test.js +231 -0
  967. package/test/routes/user/settings-validate.test.js +84 -0
  968. package/test/routes/user/signup-emails.test.js +113 -0
  969. package/test/routes/user/signup-location.test.js +84 -0
  970. package/test/routes/user/signup-log-privacy.test.js +112 -0
  971. package/test/routes/user/signup.test.js +579 -0
  972. package/test/routes/user/subscription.test.js +101 -0
  973. package/test/routes/user/token.test.js +111 -0
  974. package/test/routes/user/user.test.js +158 -0
  975. package/test/routes/verts/cache.test.js +78 -0
  976. package/test/routes/verts/click-destination.test.js +76 -0
  977. package/test/routes/verts/crud.test.js +194 -0
  978. package/test/routes/verts/redirect.test.js +147 -0
  979. package/test/routes/verts/selection.test.js +237 -0
  980. package/test/routes/verts/serve.test.js +185 -0
  981. package/test/routes/verts/unit-document.test.js +117 -0
  982. package/test/rules/_environment.js +66 -0
  983. package/test/rules/brand-merge.test.js +195 -0
  984. package/test/rules/field-helpers.test.js +309 -0
  985. package/test/rules/notifications.test.js +577 -0
  986. package/test/rules/payments-carts.test.js +406 -0
  987. package/test/rules/sessions.test.js +170 -0
  988. package/test/rules/user.test.js +517 -0
  989. package/test/rules/verts.test.js +101 -0
  990. package/test/security/fetch-log-secrets.test.js +124 -0
  991. package/test/security/primitives.test.js +105 -0
  992. package/test/security/repo-pinning.test.js +77 -0
  993. package/test/stripe-live/subscription-lifecycle.test.js +141 -0
@@ -0,0 +1,1829 @@
1
+ const BaseCommand = require('./base-command');
2
+ const path = require('path');
3
+ const fs = require('fs');
4
+ const { spawnShell } = require('../utils/spawn-shell');
5
+ const chalk = require('chalk').default;
6
+ const jetpack = require('fs-jetpack');
7
+ const JSON5 = require('json5');
8
+ const powertools = require('node-powertools');
9
+ const WatchCommand = require('./watch');
10
+ const { loadEmulatorPorts } = require('./setup-tests/emulator-config');
11
+ const { resolvePorts, writePortsFile, clearPortsFile, portsToEnv, isPortFree } = require('../../vendor/config/index.js');
12
+ const { EXTENDED_MODE_WARNING } = require('../../test/utils/extended-mode-warning');
13
+ const { writeTestMode, captureSyncedEnv } = require('../../test/utils/test-mode-file');
14
+ const { seed } = require('../../test/seed.js');
15
+ const { createChildLog } = require('../utils/attach-log-file');
16
+ const { refuseWhenCustom } = require('../utils/project-type');
17
+ const emulatorOrphans = require('./emulator-orphans');
18
+ const { STOP_SIGNALS } = require('../../vendor/devkit/stop-signals.js');
19
+
20
+ // Used by both `npx omega emulator` and `npx omega test` auto-start path.
21
+ // Note: `emulators:start` enables the UI by default (controlled by firebase.json's
22
+ // `emulators.ui.enabled`), so no `--ui` flag here — that flag only exists on `:exec`.
23
+ const EMULATOR_FLAGS = '--only functions,firestore,auth,database,hosting,pubsub';
24
+
25
+ // The pid record this run writes when its stack is up — the sweep's primary
26
+ // ownership proof ([#274](https://github.com/Omega-JS-Stack/omega/issues/274)).
27
+ const PID_RECORD_FILE = 'emulator-pids.json';
28
+
29
+ // How long a recorded pid stays evidence. The record is rewritten at boot and
30
+ // again at shutdown, so a live run's is always seconds old; anything older
31
+ // belongs to a run that died without teardown. Pids get RECYCLED, and a
32
+ // recorded pid is a kill order the sweep acts on without further proof — past
33
+ // this bound the number is no longer known to name the process it named.
34
+ const PID_RECORD_MAX_AGE_MS = 12 * 60 * 60 * 1000;
35
+
36
+ // A command line that belongs to emulator machinery. Deliberately broad: it
37
+ // only ever narrows a candidate that ALREADY named this project id.
38
+ const EMULATOR_COMMAND = /emulator|firebase/i;
39
+
40
+ // How long a recorded emulator process gets to leave on SIGTERM before the
41
+ // stop path escalates, and how long its ports get to come back free after the
42
+ // last one is gone (a java emulator releases its listener as it unwinds).
43
+ const STOP_GRACE_MS = 2000;
44
+ const PORT_RELEASE_TIMEOUT_MS = 5000;
45
+ const POLL_INTERVAL_MS = 100;
46
+
47
+ // How long the port→pid lookup gets to answer before a sweep gives up on it.
48
+ // lsof stats every mounted filesystem first, so a machine with a network mount
49
+ // pays for the walk on every call — measured at ~90ms a call here, and a stall
50
+ // on the smbfs volume runs to MINUTES
51
+ // ([#332](https://github.com/Omega-JS-Stack/omega/issues/332)). The call is
52
+ // synchronous, so an unbounded one freezes the whole run: the boot path
53
+ // absorbed that in its ready window, and the stop path simply sat there until
54
+ // a ship gate's lane budget was gone
55
+ // ([#459](https://github.com/Omega-JS-Stack/omega/issues/459)). This is ~50x a
56
+ // healthy answer, and the same window the port verdict already waits.
57
+ const PORT_LOOKUP_TIMEOUT_MS = 5000;
58
+
59
+ /**
60
+ * Does this command line name the given project as an ARGUMENT?
61
+ *
62
+ * Substring matching would be wrong twice over: `demo-x` appears inside
63
+ * `demo-x-staging` (a different project), and a repo path containing the brand
64
+ * name is not a project id at all. Only `--project <id>` / `--project_id <id>`
65
+ * (space or `=`) counts — the form the firestore emulator actually publishes.
66
+ * @param {string} command - Full command line from ps.
67
+ * @param {string} projectId - The project id to look for.
68
+ * @returns {boolean}
69
+ */
70
+ function commandNamesProject(command, projectId) {
71
+ const escaped = projectId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
72
+
73
+ return new RegExp(`--project(?:_id)?[= ]${escaped}(?:\\s|$)`).test(command);
74
+ }
75
+
76
+ /**
77
+ * The project a command line names as an argument, or null when it names none.
78
+ *
79
+ * Same argument form as commandNamesProject(), read the other way round: not
80
+ * "is it this project" but "which project does it say it is". A jar that names
81
+ * one is self-identifying evidence that outranks a recorded pid number.
82
+ * @param {string} command - Full command line from ps.
83
+ * @returns {string|null}
84
+ */
85
+ function commandProjectId(command) {
86
+ const match = /--project(?:_id)?[= ](\S+)/.exec(String(command || ''));
87
+
88
+ return match ? match[1] : null;
89
+ }
90
+
91
+ // How long a boot may take before the run declares it dead and shuts the
92
+ // partial stack down. The default absorbs a slow port sweep — lsof stalling on
93
+ // a network mount takes a real boot past the old 60s cap, which failed every
94
+ // self-booting lane on such a machine
95
+ // ([#332](https://github.com/Omega-JS-Stack/omega/issues/332)).
96
+ const DEFAULT_READY_TIMEOUT_MS = 180000;
97
+
98
+ /**
99
+ * Resolve the emulator ready deadline from the environment.
100
+ * @param {string} [raw] - The OMEGA_EMULATOR_READY_TIMEOUT value, in ms.
101
+ * @returns {number} The deadline in milliseconds.
102
+ */
103
+ function resolveReadyTimeout(raw) {
104
+ if (raw === undefined || raw === '') {
105
+ return DEFAULT_READY_TIMEOUT_MS;
106
+ }
107
+
108
+ const parsed = Number(raw);
109
+
110
+ // A junk override fails loudly instead of silently racing an unknown
111
+ // deadline — the same rule the #211 lane multiplier follows.
112
+ if (!Number.isFinite(parsed) || parsed <= 0) {
113
+ throw new Error(`OMEGA_EMULATOR_READY_TIMEOUT must be a positive number of milliseconds, got "${raw}"`);
114
+ }
115
+
116
+ return parsed;
117
+ }
118
+
119
+ /**
120
+ * The subset of `ports` something is actually listening on.
121
+ *
122
+ * The sweeps below need a port to name a pid, and `lsof` is the only tool that
123
+ * maps one. It stats every mounted filesystem before it answers, so a machine
124
+ * with a network mount (an smbfs Time Machine volume) pays seconds per call,
125
+ * and a boot ran one call per port whether or not anything was there: ~96s of
126
+ * pure waste on a run with every port free, which pushed boot past the ready
127
+ * deadline ([#332](https://github.com/Omega-JS-Stack/omega/issues/332)).
128
+ *
129
+ * A port nothing holds cannot have a holder to name, so its lookup could only
130
+ * ever come back empty. Probe first with the same bind primitive the allocator
131
+ * uses (in-process, no shell, no filesystem walk) and look up only the ports
132
+ * that answer. Same ports considered, same decisions, minus the empty calls.
133
+ * @param {number[]} ports - The ports a sweep is about to consider.
134
+ * @param {(port: number) => Promise<boolean>} [isFree] - The probe (injectable).
135
+ * @returns {Promise<number[]>} The held subset, deduped.
136
+ */
137
+ async function heldPorts(ports, isFree = isPortFree) {
138
+ const unique = [...new Set(ports || [])].filter((port) => Number.isInteger(port));
139
+ const free = await Promise.all(unique.map((port) => isFree(port)));
140
+
141
+ return unique.filter((port, index) => !free[index]);
142
+ }
143
+
144
+ /**
145
+ * The pids LISTENING on a port.
146
+ *
147
+ * The one step with no Node primitive: only lsof maps a port to a process. It
148
+ * is reached solely for a port heldPorts() already proved is held, so a normal
149
+ * boot never shells here at all.
150
+ *
151
+ * And it is BOUNDED, because the tool can stall indefinitely on a network
152
+ * mount (PORT_LOOKUP_TIMEOUT_MS). A lookup that overruns names nobody, which
153
+ * is what every caller already does with a port whose holder it cannot
154
+ * identify: report it, leave it running. The bound can only ever spare a
155
+ * process, never take one.
156
+ * @param {number} port - A port something is known to hold.
157
+ * @returns {string[]} The listening pids, or [] when the read fails or overruns.
158
+ */
159
+ function listListeningPids(port) {
160
+ const { execFileSync } = require('child_process');
161
+
162
+ try {
163
+ // No shell: the bound has to land on lsof ITSELF, and an `sh -c` wrapper
164
+ // is one more process for the kill to hit instead. stderr is dropped by
165
+ // the stdio map, which is all the old `2>/dev/null` was doing. SIGKILL
166
+ // because a process wedged in a filesystem call is exactly the one that
167
+ // would ignore a polite signal and hold the run past its own timeout.
168
+ return execFileSync('lsof', ['-ti', `TCP:${port}`, '-sTCP:LISTEN'], {
169
+ encoding: 'utf8',
170
+ stdio: ['ignore', 'pipe', 'ignore'],
171
+ timeout: PORT_LOOKUP_TIMEOUT_MS,
172
+ killSignal: 'SIGKILL',
173
+ }).trim().split('\n').filter(Boolean);
174
+ } catch (error) {
175
+ return [];
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Every port the firebase config tells the emulator child to bind.
181
+ *
182
+ * Read from the config, never hard-coded: a brand may declare emulators this
183
+ * CLI's allocator knows nothing about (eventarc, tasks), and those are exactly
184
+ * the ones a boot cannot relocate.
185
+ * @param {object} firebaseConfig - The parsed firebase config the child reads.
186
+ * @returns {object} name to port, for every emulator that names a port.
187
+ */
188
+ function declaredEmulatorPorts(firebaseConfig) {
189
+ const declared = {};
190
+
191
+ for (const [name, entry] of Object.entries(firebaseConfig?.emulators || {})) {
192
+ if (Number.isInteger(entry?.port)) {
193
+ declared[name] = entry.port;
194
+ }
195
+ }
196
+
197
+ return declared;
198
+ }
199
+
200
+ /**
201
+ * The boot's port plan: what the emulator child is about to bind.
202
+ *
203
+ * The resolved map wins over the declared value, because a bumped port is what
204
+ * the child actually receives (via firebase.resolved.json). `https` is left
205
+ * out: that is THIS process's TLS proxy, not the child's listener.
206
+ * @param {object} declared - name to port, from the firebase config.
207
+ * @param {object} resolved - This run's allocated map (already bumped).
208
+ * @returns {Array<{name: string, port: number}>}
209
+ */
210
+ function plannedEmulatorPorts(declared, resolved) {
211
+ const plan = [];
212
+
213
+ for (const [name, port] of Object.entries({ ...declared, ...resolved })) {
214
+ if (name === 'https') {
215
+ continue;
216
+ }
217
+
218
+ plan.push({ name: name, port: port });
219
+ }
220
+
221
+ return plan;
222
+ }
223
+
224
+ /**
225
+ * The fail-fast report for a boot whose plan cannot work.
226
+ * @param {Array<{name: string, port: number}>} blocked - The held planned ports.
227
+ * @returns {string}
228
+ */
229
+ function formatPortPreflightFailure(blocked) {
230
+ const named = blocked.map(({ name, port }) => `port ${port} (${name})`).join(', ');
231
+ const numbers = blocked.map(({ port }) => port).join(', ');
232
+
233
+ return [
234
+ `Port preflight failed: this emulator must bind ${named}, and something already holds ${blocked.length > 1 ? 'them' : 'it'}.`,
235
+ 'Another dev stack is in the way: a tower app on that port, another brand\'s `omega dev`, or an emulator a crashed run left behind.',
236
+ `Stop whatever holds ${numbers} and run this again. Nothing was spawned, so there is no partial stack to clean up.`,
237
+ ].join('\n ');
238
+ }
239
+
240
+ /**
241
+ * Stop the boot BEFORE firebase is spawned when the plan cannot work.
242
+ *
243
+ * The allocator already relocates around a busy port (N7 bump-if-taken), so a
244
+ * plan that still names a held port names one this run cannot move off. Left
245
+ * to firebase, that boot never prints its ready marker and the run burns the
246
+ * whole ready deadline before anything says why
247
+ * ([#332](https://github.com/Omega-JS-Stack/omega/issues/332)).
248
+ * @param {Array<{name: string, port: number}>} planned - What the child will bind.
249
+ * @param {(port: number) => Promise<boolean>} [isFree] - The probe (injectable).
250
+ * @returns {Promise<void>} Rejects with the report when a planned port is held.
251
+ */
252
+ async function assertPlannedPortsFree(planned, isFree = isPortFree) {
253
+ const held = await heldPorts(planned.map(({ port }) => port), isFree);
254
+
255
+ if (held.length === 0) {
256
+ return;
257
+ }
258
+
259
+ throw new Error(formatPortPreflightFailure(planned.filter(({ port }) => held.includes(port))));
260
+ }
261
+
262
+ /**
263
+ * The allocated port firebase-tools just failed to bind, or null.
264
+ *
265
+ * The probe and the bind are two moments, and on a machine running a second
266
+ * brand's stack a foreign listener can take a port between them, and the boot
267
+ * then dies on a port the allocator read as free seconds earlier
268
+ * ([#778](https://github.com/Omega-JS-Stack/omega/issues/778)). The child says
269
+ * so in one line, and the port is read out of it by matching against THIS
270
+ * run's allocation rather than by parsing an address shape: node writes the
271
+ * same failure as `127.0.0.1:9099`, `:::9099` and `port 9099` depending on
272
+ * which layer reports it, and only a number this run actually asked for can be
273
+ * the one it could not bind.
274
+ * @param {string} text - A chunk of the child's output.
275
+ * @param {number[]} ports - This attempt's allocated ports.
276
+ * @returns {number|null}
277
+ */
278
+ function addressInUsePort(text, ports) {
279
+ const allocated = new Set(ports);
280
+
281
+ for (const line of String(text || '').split('\n')) {
282
+ if (!/EADDRINUSE/i.test(line)) {
283
+ continue;
284
+ }
285
+
286
+ for (const [number] of line.matchAll(/\d+/g)) {
287
+ if (allocated.has(Number(number))) {
288
+ return Number(number);
289
+ }
290
+ }
291
+ }
292
+
293
+ return null;
294
+ }
295
+
296
+ /**
297
+ * The report for a boot whose retry hit the same wall (#778).
298
+ *
299
+ * The holder is NAMED, never signalled: it is another session's live stack,
300
+ * and this run has no proof of ownership over it, which is the same bar every
301
+ * sweep in this file clears before it sends a signal.
302
+ * @param {{port: number, name: string|null}} failed - The port the retry could not bind.
303
+ * @param {Array<{pid: number, command: string}>} holders - Who holds it now.
304
+ * @returns {string}
305
+ */
306
+ function formatAddressInUseFailure(failed, holders) {
307
+ const named = `port ${failed.port}${failed.name ? ` (${failed.name})` : ''}`;
308
+ const report = [`Emulator boot failed: ${named} was taken while the stack was coming up, and the bumped retry hit the same wall.`];
309
+
310
+ for (const { pid, command } of holders) {
311
+ report.push(`Port ${failed.port} is held by pid ${pid}${command ? `: ${command}` : ''}.`);
312
+ }
313
+
314
+ if (holders.length === 0) {
315
+ report.push(`Nothing answers for port ${failed.port} now: whatever took it came and went inside this boot.`);
316
+ }
317
+
318
+ report.push(`Stop whatever keeps taking ${failed.port} and run this again. This run's stack was shut down; nothing of it is left running.`);
319
+
320
+ return report.join('\n ');
321
+ }
322
+
323
+ /**
324
+ * The allocation's name for a port ('auth' for 9099), or null.
325
+ * @param {object} ports - The resolved name to port map.
326
+ * @param {number} port - The port to name.
327
+ * @returns {string|null}
328
+ */
329
+ function portName(ports, port) {
330
+ const found = Object.entries(ports || {}).find(([, value]) => value === port);
331
+
332
+ return found ? found[0] : null;
333
+ }
334
+
335
+ /**
336
+ * Can this process be PROVEN to be an emulator process of THIS project?
337
+ *
338
+ * The sweep used to signal whatever was listening on its ports, which
339
+ * terminated another project's live emulator sharing the hub/storage ports
340
+ * ([#274](https://github.com/Omega-JS-Stack/omega/issues/274)). Occupying a
341
+ * port is not ownership; only these two proofs are:
342
+ *
343
+ * 1. The pid record this run wrote when it spawned the stack. Primary,
344
+ * because the java emulators name no project on their own — and they
345
+ * reparent to PID 1 when orphaned, so the record made while they were
346
+ * still attached is the only surviving link.
347
+ * 2. A command line that is emulator machinery AND names this project id
348
+ * (the firestore emulator's `--project_id`), which covers a leftover from
349
+ * an earlier run of the same project that no live record mentions.
350
+ *
351
+ * Anything else is somebody else's process and is left running.
352
+ * @param {object} candidate - { pid, command } as read from ps.
353
+ * @param {object} [ownership] - { pids: number[], projectId: string|null }.
354
+ * @returns {boolean}
355
+ */
356
+ function isOwnedEmulatorProcess(candidate, ownership) {
357
+ const pid = Number(candidate?.pid);
358
+ const command = String(candidate?.command || '');
359
+
360
+ // PID 1 is init, and a non-numeric row is a parse failure — never candidates.
361
+ if (!Number.isInteger(pid) || pid <= 1) {
362
+ return false;
363
+ }
364
+
365
+ if ((ownership?.pids || []).some((recorded) => Number(recorded) === pid)) {
366
+ return true;
367
+ }
368
+
369
+ const projectId = ownership?.projectId;
370
+
371
+ if (!projectId) {
372
+ return false;
373
+ }
374
+
375
+ return EMULATOR_COMMAND.test(command) && commandNamesProject(command, projectId);
376
+ }
377
+
378
+ /**
379
+ * Should the STOP path signal this recorded process?
380
+ *
381
+ * The stop path has no port to narrow by — it walks the record itself — so the
382
+ * recorded pid alone would be the whole proof, and pids get RECYCLED. The live
383
+ * command line is the second half: a recorded number that now names something
384
+ * that is not emulator machinery is a stranger the OS handed our pid to, and
385
+ * it is left running ([#274](https://github.com/Omega-JS-Stack/omega/issues/274)).
386
+ *
387
+ * Recycling inside the SAME machinery is the harder half: a recorded number
388
+ * that now runs the neighbouring brand's firestore jar reads as ours on the pid
389
+ * alone. A jar that names a project id names it truthfully, so when this run
390
+ * knows its own project id and the live command line disagrees, the number is
391
+ * recycled and the process is left running. A jar that names none (the pubsub
392
+ * emulator) is decided by the record, as before.
393
+ * @param {{pid: number|string, command: string}} candidate - One ps row.
394
+ * @param {{pids: number[], projectId: string|null}} ownership - This run's record.
395
+ * @returns {boolean}
396
+ */
397
+ function isStoppableEmulatorProcess(candidate, ownership) {
398
+ const command = String(candidate?.command || '');
399
+
400
+ if (!EMULATOR_COMMAND.test(command)) {
401
+ return false;
402
+ }
403
+
404
+ const named = commandProjectId(command);
405
+
406
+ if (named && ownership?.projectId && named !== ownership.projectId) {
407
+ return false;
408
+ }
409
+
410
+ return isOwnedEmulatorProcess(candidate, ownership);
411
+ }
412
+
413
+ /**
414
+ * Should the STALE-RECORD reap signal this recorded process?
415
+ *
416
+ * The stop path's bar first (isStoppableEmulatorProcess): emulator machinery
417
+ * that does not name a different project. That bar decides a jar naming NO
418
+ * project — the pubsub emulator — on record membership alone, and a record is a
419
+ * list of NUMBERS: a recycled number now running the NEIGHBOUR brand's pubsub
420
+ * jar reads exactly like our own leftover
421
+ * ([#730](https://github.com/Omega-JS-Stack/omega/issues/730)).
422
+ *
423
+ * So the live PARENT is the second proof, and it is only ever asked of a record
424
+ * whose root is already proven dead. A dead run's survivors are reparented (PID
425
+ * 1, where every orphan lands) or still hang off another member of that same
426
+ * dead run — the firebase parent that outlived the `sh` root, say. A number
427
+ * handed to somebody else's LIVE stack has a live parent that this record has
428
+ * never heard of, and it is left running.
429
+ * @param {{pid: number|string, ppid: number|string, command: string}} candidate - One ps row.
430
+ * @param {{pids: number[], projectId: string|null, rootPid: number|null}} ownership - The PREVIOUS record.
431
+ * @returns {boolean}
432
+ */
433
+ function isReapableRecordedProcess(candidate, ownership) {
434
+ if (!isStoppableEmulatorProcess(candidate, ownership)) {
435
+ return false;
436
+ }
437
+
438
+ const parent = Number(candidate?.ppid);
439
+
440
+ if (parent === 1 || parent === Number(ownership?.rootPid)) {
441
+ return true;
442
+ }
443
+
444
+ return (ownership?.pids || []).some((recorded) => Number(recorded) === parent);
445
+ }
446
+
447
+ /**
448
+ * The ownership a pid record still supports, given its age.
449
+ *
450
+ * The pids are only evidence while they are known to name the processes they
451
+ * named at spawn: the record is never deleted, so a run days later reads the
452
+ * last one, and by then the OS may have handed those numbers to anything. The
453
+ * project id is not pid-based — a command line naming this project proves
454
+ * itself at any age — so it survives an expiry.
455
+ *
456
+ * The ports ride with the pids: they are the map the recorded run bound, and
457
+ * they are only ever read to settle ports those very pids just released.
458
+ * @param {object|null} record - The parsed pid-record file.
459
+ * @param {number} [now] - Epoch ms to age against (defaults to Date.now()).
460
+ * @returns {{pids: number[], projectId: string|null, rootPid: number|null, ports: object}}
461
+ */
462
+ function ownershipFromRecord(record, now) {
463
+ const startedAt = Date.parse(record?.startedAt);
464
+ const fresh = Number.isFinite(startedAt) && (now || Date.now()) - startedAt < PID_RECORD_MAX_AGE_MS;
465
+
466
+ return {
467
+ pids: fresh && Array.isArray(record?.pids) ? record.pids : [],
468
+ projectId: record?.projectId || null,
469
+ rootPid: record?.rootPid || null,
470
+ ports: fresh && record?.ports ? record.ports : {},
471
+ };
472
+ }
473
+
474
+ /**
475
+ * The command line a pid is running right now, or '' when it is gone.
476
+ * @param {number|string} pid - The pid to read.
477
+ * @returns {string}
478
+ */
479
+ function readProcessCommand(pid) {
480
+ const { execSync } = require('child_process');
481
+
482
+ try {
483
+ return execSync(`ps -o command= -p ${Number(pid)} 2>/dev/null`, { encoding: 'utf8' }).trim();
484
+ } catch (error) {
485
+ return '';
486
+ }
487
+ }
488
+
489
+ /**
490
+ * The command line a pid is running right now AND who forked it, or null when
491
+ * it is gone.
492
+ *
493
+ * The parent is half of the stale reap's identity proof, and it is only
494
+ * readable in the same breath as the command line: two `ps` calls describe two
495
+ * moments, and a number that changed hands between them reads as one process
496
+ * that never existed ([#730](https://github.com/Omega-JS-Stack/omega/issues/730)).
497
+ * @param {number|string} pid - The pid to read.
498
+ * @returns {{ppid: number, command: string}|null}
499
+ */
500
+ function readProcessInfo(pid) {
501
+ const { execSync } = require('child_process');
502
+
503
+ try {
504
+ const row = execSync(`ps -o ppid=,command= -p ${Number(pid)} 2>/dev/null`, { encoding: 'utf8' }).trim();
505
+ const match = row.match(/^\s*(\d+)\s+(.*)$/s);
506
+
507
+ return match ? { ppid: Number(match[1]), command: match[2] } : null;
508
+ } catch (error) {
509
+ // `ps` exiting 1 is the one provable answer: the pid does not exist. Any
510
+ // other failure (fork pressure, a spawn error) proves NOTHING — and it
511
+ // must read as alive-but-unprovable, never as gone, because the reap's
512
+ // root gate turns "gone" into a kill order. The sentinel is truthy (the
513
+ // root gate leaves the record alone) and matches no identity proof (a
514
+ // member read spares the pid) ([#730](https://github.com/Omega-JS-Stack/omega/issues/730)).
515
+ if (error.status === 1) {
516
+ return null;
517
+ }
518
+
519
+ return { ppid: -1, command: '' };
520
+ }
521
+ }
522
+
523
+ /**
524
+ * Every descendant pid of `rootPid`, from one ps snapshot.
525
+ *
526
+ * Taken while the stack is UP: firebase-tools puts each java emulator in its
527
+ * own process group and they reparent to PID 1 once orphaned, so neither the
528
+ * group nor the parent link survives the moment the sweep needs it.
529
+ * @param {number} rootPid - The spawned child's pid.
530
+ * @returns {number[]} rootPid plus every descendant, deduped.
531
+ */
532
+ function collectDescendantPids(rootPid) {
533
+ const { execSync } = require('child_process');
534
+ const children = new Map();
535
+
536
+ try {
537
+ const rows = execSync('ps -A -o pid=,ppid=', { encoding: 'utf8' }).trim().split('\n');
538
+
539
+ for (const row of rows) {
540
+ const [pid, ppid] = row.trim().split(/\s+/).map(Number);
541
+
542
+ if (!Number.isInteger(pid) || !Number.isInteger(ppid)) continue;
543
+
544
+ if (!children.has(ppid)) children.set(ppid, []);
545
+ children.get(ppid).push(pid);
546
+ }
547
+ } catch (error) {
548
+ return [rootPid];
549
+ }
550
+
551
+ const collected = new Set([rootPid]);
552
+ const queue = [rootPid];
553
+
554
+ while (queue.length > 0) {
555
+ for (const child of children.get(queue.shift()) || []) {
556
+ if (collected.has(child)) continue;
557
+
558
+ collected.add(child);
559
+ queue.push(child);
560
+ }
561
+ }
562
+
563
+ return [...collected];
564
+ }
565
+
566
+ class EmulatorCommand extends BaseCommand {
567
+ async execute() {
568
+ // Custom-server mode exports no Cloud Functions to emulate (#584)
569
+ if (refuseWhenCustom(this.main.firebaseProjectPath, 'emulator')) return;
570
+
571
+ // The emulator IS the backend's dev leg under brand-root `omega dev`, so it
572
+ // shares the dev-log lane with `omega serve` (they never run together — same
573
+ // ports). The firebase CHILD keeps its own dist/emulator.log (#197).
574
+ this.attachVerbLog('dev');
575
+
576
+ this.log(chalk.cyan('\n Starting Firebase emulator (keep-alive mode)...\n'));
577
+ this.log(chalk.gray(' Emulator will stay running until you press Ctrl+C\n'));
578
+
579
+ // Boot-time: seed the shared state file with whatever this emulator was
580
+ // started with. Two flows are supported:
581
+ // - Recommended: start emulator without the flag, set TEST_EXTENDED_MODE
582
+ // on `npx omega test` instead. The test command writes the file; the
583
+ // emulator's function workers watch it and flip live.
584
+ // - Also supported: start emulator with TEST_EXTENDED_MODE=true. We
585
+ // write the file here as a boot default. Useful for inspecting the
586
+ // emulator before any tests fire. Note: the next `npx omega test`
587
+ // overwrites the file regardless of how the emulator booted.
588
+ {
589
+ const projectDir = this.main.firebaseProjectPath;
590
+ const envSubset = captureSyncedEnv(process.env);
591
+ writeTestMode(projectDir, envSubset);
592
+ }
593
+
594
+ // Show the standard warning if the emulator boots in extended mode.
595
+ if (process.env.TEST_EXTENDED_MODE) {
596
+ this.log(chalk.yellow.bold(`\n ${EXTENDED_MODE_WARNING[0]}`));
597
+ EXTENDED_MODE_WARNING.slice(1).forEach((line) => this.log(chalk.yellow(` ${line}`)));
598
+ this.log(chalk.gray(` (Tip: you can also flip mode per-run by setting TEST_EXTENDED_MODE on \`npx omega test\`.)`));
599
+ this.log('');
600
+ }
601
+
602
+ // Start @omega.js/backend watcher in background. Keep the child handle —
603
+ // interactive Ctrl+C kills it via the terminal process group, but a
604
+ // PROGRAMMATIC signal to this process alone (e2e harness, kill -INT)
605
+ // doesn't, leaving an immortal nodemon re-creating the reload trigger
606
+ // (found live by the cp88 two-emulator proof).
607
+ const watcher = new WatchCommand(this.main);
608
+ const watcherChild = watcher.startBackground();
609
+
610
+ // The watcher hangs off THIS process, not off the emulator child, so the pid
611
+ // record's descendant walk from the emulator's root pid cannot see it — name
612
+ // it for the record explicitly, or a run that dies without teardown leaves a
613
+ // nodemon no later stop path can even name
614
+ // ([#629](https://github.com/Omega-JS-Stack/omega/issues/629)).
615
+ this.backgroundWatcherPid = watcherChild ? watcherChild.pid : null;
616
+
617
+ // Keep-alive: boot emulators and wait for Ctrl+C. No "command" subprocess —
618
+ // the emulator child IS the foreground process from the user's perspective.
619
+ // HTTPS default-on for the interactive command (--no-https disables); the
620
+ // `omega test` auto-start path never passes it — the harness talks plain
621
+ // http to hosting directly.
622
+ // Kept outside the try so the failure path below can stop whatever came up.
623
+ let started = null;
624
+
625
+ try {
626
+ started = await this.startEmulators({
627
+ https: this.argv.https !== false,
628
+ });
629
+ const { shutdown, emulatorPorts, exitPromise } = started;
630
+
631
+ // Start Stripe webhook forwarding in background — AFTER boot so it
632
+ // targets the RESOLVED hosting port, not a classic that may have bumped
633
+ this.startStripeWebhookForwarding(emulatorPorts.hosting);
634
+
635
+ // Seed personas unless --no-seed was passed (yargs boolean negation:
636
+ // `--no-seed` parses as argv.seed === false). seedPersonas is fully
637
+ // non-fatal — any failure logs a warning and the emulator keeps running.
638
+ if (this.argv.seed !== false) {
639
+ await this.seedPersonas(emulatorPorts);
640
+ }
641
+
642
+ // NOTE: this line is a readiness MARKER for external drivers (the devkit
643
+ // e2e harness waits for it) — it must print AFTER seeding so drivers
644
+ // don't race the seed wipe. Change the text in lockstep with
645
+ // @omega.js/devkit/test/e2e-harness.js.
646
+ this.log(chalk.gray('\n Emulator ready. Press Ctrl+C to shut down...\n'));
647
+
648
+ // Synchronous stop handler — must NOT be async. In Node, registering a
649
+ // listener for one of these signals suppresses the default "exit on signal"
650
+ // behavior, but only if the listener is present when the signal fires. An
651
+ // async handler loses the race: Node starts it, doesn't await it, and the
652
+ // process dies mid-shutdown. So: set a flag synchronously, kick off shutdown
653
+ // (no await), and let the main `await exitPromise` below resolve naturally
654
+ // once shutdown kills the child.
655
+ let stopCount = 0;
656
+ // The handler cannot await, but the run must not exit ahead of the
657
+ // shutdown it started: the jar reap runs AFTER the child is gone, and
658
+ // process.exit() below would cut it off mid-signal
659
+ // ([#304](https://github.com/Omega-JS-Stack/omega/issues/304)). Keep the
660
+ // promise and join it once the child has exited.
661
+ let shutdownRun = null;
662
+ const onStopSignal = () => {
663
+ stopCount++;
664
+ if (stopCount === 1) {
665
+ this.log(chalk.gray('\n Shutting down emulator... (Ctrl+C again to force kill)'));
666
+ shutdownRun = shutdown();
667
+ } else {
668
+ this.log(chalk.gray(' Force killing emulator...'));
669
+ shutdownRun = shutdown();
670
+ }
671
+ };
672
+ // ONE handler, three signals: the stop path is the same whether the ask is
673
+ // Ctrl+C, `omega dev`'s SIGTERM, or the SIGHUP of a closed terminal
674
+ // ([#629](https://github.com/Omega-JS-Stack/omega/issues/629)).
675
+ STOP_SIGNALS.forEach((signal) => process.on(signal, onStopSignal));
676
+
677
+ // Resolve when the emulator exits (via shutdown or crash)
678
+ await exitPromise;
679
+ // A child that exited on its own (a crash, firebase-tools stopping
680
+ // itself) started no shutdown — run one anyway, so the same reap covers
681
+ // both ways this line is reached. shutdown() owns the whole teardown now:
682
+ // the recorded jars, the orphan sweep, and the port verdict. The stop
683
+ // listeners stay active across it so Ctrl+C spam can't cut it short.
684
+ await (shutdownRun || shutdown());
685
+ // Reap the background watcher — it is NOT in the firebase child's
686
+ // process group, so nothing else kills it on a programmatic shutdown.
687
+ if (watcherChild) {
688
+ try { watcherChild.kill('SIGTERM'); } catch (e) { /* already gone */ }
689
+ }
690
+ STOP_SIGNALS.forEach((signal) => process.removeListener(signal, onStopSignal));
691
+ this.log(chalk.gray(' Emulator stopped.\n'));
692
+ if (stopCount > 0) {
693
+ process.exit(0);
694
+ }
695
+ } catch (error) {
696
+ this.logError(`Emulator error: ${error.message || error}`);
697
+ // A failed boot spawned things too: the stack (startEmulators stops its
698
+ // own before it throws) and this frame's background watcher, which
699
+ // nothing else signals — exiting here left an immortal nodemon behind
700
+ // ([#304](https://github.com/Omega-JS-Stack/omega/issues/304)). Anything
701
+ // that got as far as a handle gets the same stop path.
702
+ if (started) {
703
+ try { await started.shutdown(); } catch (e) { /* the stop path already reported */ }
704
+ }
705
+ if (watcherChild) {
706
+ try { watcherChild.kill('SIGTERM'); } catch (e) { /* already gone */ }
707
+ }
708
+ process.exit(1);
709
+ }
710
+ }
711
+
712
+ /**
713
+ * Seed the running emulator with test personas so a developer can sign in
714
+ * manually on any emulator-connected dev site (email + TEST_ACCOUNT_PASSWORD).
715
+ * Fully non-fatal — the ENTIRE body is guarded; any failure (config load,
716
+ * firebase-admin init, account creation) logs a warning and the emulator
717
+ * keeps running. execute()'s catch would otherwise process.exit(1).
718
+ */
719
+ async seedPersonas(emulatorPorts) {
720
+ try {
721
+ this.log(chalk.cyan('\n Seeding test personas...\n'));
722
+
723
+ const projectDir = this.main.firebaseProjectPath;
724
+ const functionsDir = path.join(projectDir, 'dist');
725
+
726
+ // Load project config (same pattern as test.js loadProjectConfig)
727
+ const { hasOmegaConfig, loadConfig, loadEnv, resolvedBrandHost } = require('../../vendor/config/index.js');
728
+ loadEnv(functionsDir);
729
+
730
+ let config = {};
731
+ let domain = '';
732
+ if (hasOmegaConfig(functionsDir)) {
733
+ config = loadConfig(functionsDir, 'backend').config;
734
+ // A persona lives on the brand's HOST — the one derivation, shared with
735
+ // the dev palette that signs in as one
736
+ // ([#708](https://github.com/Omega-JS-Stack/omega/issues/708)). The
737
+ // brand's contact address is a support inbox — often on the apex while
738
+ // the site is a subdomain of it — so it shapes no persona identity.
739
+ domain = resolvedBrandHost(config);
740
+ }
741
+
742
+ // Persona emails are `_test.<id>@{domain}` — without a domain every
743
+ // createUser call fails with an invalid email. Skip cleanly instead.
744
+ if (!domain) {
745
+ this.logWarning('Skipping persona seeding: no brand.url in config/omega.json5 (personas are seeded on the brand host)');
746
+ return;
747
+ }
748
+
749
+ // Point firebase-admin at the emulators. Set AFTER the firebase child was
750
+ // spawned, so only THIS process (and the seed module's emulator-only
751
+ // guards) see them — the emulator child's env is unaffected.
752
+ process.env.FIRESTORE_EMULATOR_HOST = `127.0.0.1:${emulatorPorts.firestore}`;
753
+ process.env.FIREBASE_AUTH_EMULATOR_HOST = `127.0.0.1:${emulatorPorts.auth}`;
754
+ process.env.GCLOUD_PROJECT = config.cloud?.config?.projectId || 'demo-test';
755
+
756
+ const firebaseAdmin = require('firebase-admin');
757
+ if (firebaseAdmin.apps.length === 0) {
758
+ firebaseAdmin.initializeApp({
759
+ projectId: process.env.GCLOUD_PROJECT,
760
+ });
761
+ }
762
+
763
+ const { TEST_ACCOUNT_PASSWORD } = require('../../test/test-accounts.js');
764
+ const result = await seed({
765
+ admin: firebaseAdmin,
766
+ domain,
767
+ config,
768
+ projectDir,
769
+ });
770
+
771
+ if (result.accounts) {
772
+ this.log(chalk.green(`\n ✓ Personas seeded (${result.created} accounts)`));
773
+ this.log(chalk.gray(` Sign in on any emulator-connected dev site with email + password: ${TEST_ACCOUNT_PASSWORD}\n`));
774
+ } else {
775
+ this.logWarning('Persona seeding completed with errors (see above)');
776
+ }
777
+ } catch (e) {
778
+ this.logWarning(`Persona seeding failed: ${e.message}`);
779
+ this.log(chalk.gray(' The emulator is still running — seeding is non-fatal. Re-run with --no-seed to skip.\n'));
780
+ }
781
+ }
782
+
783
+ /**
784
+ * Boot Firebase emulators as a long-running child process.
785
+ * Stdout/stderr are teed to console + emulator.log.
786
+ * Resolves once the emulator hub is listening (i.e., emulators are ready).
787
+ * Caller is responsible for calling shutdown() to send SIGTERM and wait for exit.
788
+ *
789
+ * @param {object} [options]
790
+ * @param {boolean} [options.https] - Front the public hosting port with the
791
+ * shared mkcert TLS proxy (interactive `omega emulator` default)
792
+ * @param {string} [options.environment] - The environment the stage composes
793
+ * dist/.env for (#586). Omitted = `development`, the local lane's pin —
794
+ * never the shell's answer, so an exported ENVIRONMENT can't stage
795
+ * production credentials into a local boot. `omega test`'s auto-start
796
+ * names `testing`.
797
+ *
798
+ * The STAGED overlay and the emulator RUNTIME's own environment answer
799
+ * differ on purpose: functions running under the emulator report `testing`
800
+ * (OMEGA_TEST_MODE), while the artifact they read was composed from
801
+ * `.env.development` — the file lane is the developer's local credentials,
802
+ * the runtime lane is "don't do real side effects".
803
+ * @returns {Promise<{ child: ChildProcess, shutdown: () => Promise<void>, emulatorPorts: object }>}
804
+ */
805
+ async startEmulators(options) {
806
+ const projectDir = this.main.firebaseProjectPath;
807
+
808
+ // dist/ is staged output (src/dist pillar): stage fresh (including
809
+ // dist/public/ for hosting), then keep it fresh — the emulator watches
810
+ // dist/ natively, so a re-stage IS the hot reload. The watcher belongs to
811
+ // the boot ATTEMPT below, which dies with the emulator child it feeds.
812
+ // The local lane's pin (#586): a boot nobody named an environment for is
813
+ // `development`, so the composed dist/.env can never follow the shell.
814
+ const environment = options?.environment || 'development';
815
+ this.ensureStaged({ environment });
816
+
817
+ // N7 port allocation: firebase.json values (classic defaults) when free,
818
+ // bump-if-taken — a second brand's stack relocates instead of the old
819
+ // behavior of KILLING the incumbent. Explicit config `ports` pins never
820
+ // bump (busy pin = hard error).
821
+ const wanted = loadEmulatorPorts(projectDir);
822
+
823
+ // HTTPS (the classic https://localhost:5002 contract, shared with `omega
824
+ // serve` + web's `omega dev` via @omega.js/devkit/local-https): the PUBLIC
825
+ // hosting port speaks TLS through the mkcert proxy; hosting itself moves
826
+ // to an internal plain-http port. No mkcert → plain http on the classic
827
+ // port, same fallback as serve.
828
+ let httpsCerts = null;
829
+ if (options?.https) {
830
+ const { ensureLocalHttpsCerts, mkcertInstallHint } = require('../../vendor/devkit/local-https.js');
831
+ httpsCerts = await ensureLocalHttpsCerts({
832
+ certsDir: path.join(this.getTempPath(), 'certs'),
833
+ log: (line) => this.log(chalk.gray(` ${line}`)),
834
+ });
835
+
836
+ if (httpsCerts) {
837
+ wanted.https = wanted.hosting;
838
+ wanted.hosting = 5443;
839
+ } else {
840
+ this.log(chalk.yellow(' HTTPS disabled — could not obtain certificates.'));
841
+ this.log(chalk.yellow(` Install mkcert for trusted local HTTPS: ${mkcertInstallHint()}\n`));
842
+ }
843
+ }
844
+
845
+ // Crash leftovers first, MACHINE-WIDE: a run that died without teardown
846
+ // leaves java emulator grandchildren and functions workers squatting
847
+ // FOREVER, and the port-driven reaper that used to stand here could only
848
+ // ever see the ports THIS run wants and only what it could prove was this
849
+ // project's. A hub on a bumped port, a legacy install's workers, another
850
+ // brand's leftovers: all invisible, all still holding memory and ports. An
851
+ // ORPHAN of the emulator family is nobody's, so it goes whatever project it
852
+ // names ([#781](https://github.com/Omega-JS-Stack/omega/issues/781)). The
853
+ // strict command-shape match is what survives of
854
+ // [#293](https://github.com/Omega-JS-Stack/omega/issues/293): live stacks,
855
+ // reload watchers and other users' processes are never candidates.
856
+ await this.reapMachineOrphans();
857
+
858
+ // Then the leftovers no ORPHAN test can reach: a dead run's members that
859
+ // still hang off another member of that same dead run. The record naming
860
+ // them is about to be overwritten by this boot — read it while it still
861
+ // exists ([#721](https://github.com/Omega-JS-Stack/omega/issues/721)).
862
+ const recorded = this.readEmulatorOwnership();
863
+ const ownership = {
864
+ pids: recorded.pids,
865
+ projectId: this.loadProjectId(projectDir) || recorded.projectId,
866
+ rootPid: recorded.rootPid,
867
+ ports: recorded.ports,
868
+ };
869
+ await this.reapStaleRecordedProcesses(ownership);
870
+
871
+ // ONE bump-and-retry (#778): the allocator probed these ports seconds ago,
872
+ // and on a machine that shares them with another brand's session a foreign
873
+ // listener can take one between that probe and firebase-tools' bind. The
874
+ // port the child could not bind is marked claimed, so the next allocation
875
+ // moves that name off it, republishes the map, and boots again. A second
876
+ // failure is no longer a race: something is sitting on that port, and the
877
+ // report names it (and never signals it).
878
+ const taken = new Set();
879
+
880
+ try {
881
+ return await this.bootEmulatorStack({ projectDir, environment, wanted, httpsCerts, taken });
882
+ } catch (error) {
883
+ if (!error?.addressInUsePort) {
884
+ throw error;
885
+ }
886
+
887
+ taken.add(error.addressInUsePort);
888
+ this.log(chalk.yellow(` Port ${error.addressInUsePort}${error.addressInUseName ? ` (${error.addressInUseName})` : ''} was taken while the stack came up; reallocating around it and booting once more.`));
889
+
890
+ try {
891
+ return await this.bootEmulatorStack({ projectDir, environment, wanted, httpsCerts, taken });
892
+ } catch (retryError) {
893
+ if (!retryError?.addressInUsePort) {
894
+ throw retryError;
895
+ }
896
+
897
+ throw new Error(formatAddressInUseFailure(
898
+ { port: retryError.addressInUsePort, name: retryError.addressInUseName },
899
+ this.describePortHolders(retryError.addressInUsePort),
900
+ ));
901
+ }
902
+ }
903
+ }
904
+
905
+ /**
906
+ * ONE boot attempt: allocate the ports, publish them, spawn firebase-tools,
907
+ * and wait for the ready marker.
908
+ *
909
+ * Each attempt owns its stage watcher, because the watcher is torn down with
910
+ * the emulator child (exitPromise below), so a failed attempt takes its
911
+ * watcher with it and the retry starts a fresh one, instead of hot-reloading
912
+ * into a stack that no longer exists (#778).
913
+ * @param {object} attempt
914
+ * @param {string} attempt.projectDir - The firebase project directory.
915
+ * @param {string} attempt.environment - The environment each re-stage composes for.
916
+ * @param {object} attempt.wanted - The wanted name to port map (firebase.json + https).
917
+ * @param {object|null} attempt.httpsCerts - The mkcert pair, when HTTPS is on.
918
+ * @param {Set<number>} attempt.taken - Ports this attempt may not allocate:
919
+ * the ones a previous attempt proved something else is holding.
920
+ * @returns {Promise<{ child: ChildProcess, shutdown: () => Promise<void>, emulatorPorts: object, bumped: string[], exitPromise: Promise<object> }>}
921
+ */
922
+ async bootEmulatorStack({ projectDir, environment, wanted, httpsCerts, taken }) {
923
+ // The stamp for THIS attempt, and the newest one owns the shared artifacts.
924
+ // A first attempt's child can close LATE: after shutdown's SIGTERM/SIGKILL
925
+ // races gave up on it, and after the retry published its own map. Both
926
+ // attempts stamp the same process.pid, so the pid cannot tell them apart
927
+ // ([#778](https://github.com/Omega-JS-Stack/omega/issues/778)).
928
+ const attempt = this.activeAttempt = (this.activeAttempt || 0) + 1;
929
+ const stageWatch = this.startStageWatch({ environment });
930
+
931
+ const { ports: emulatorPorts, bumped } = await resolvePorts({
932
+ wanted,
933
+ pins: this.loadPortPins(projectDir),
934
+ // A FRESH claimed set per attempt: the allocator claims every port it
935
+ // hands out into it, so carrying one over would bump the whole map. The
936
+ // only thing an attempt inherits is what the last one proved is taken.
937
+ claimed: new Set(taken),
938
+ });
939
+
940
+ // Preflight: the allocator has relocated around every busy port it owns,
941
+ // so anything still held is a port this run cannot move off. Report it now,
942
+ // by name, instead of spawning a stack that never comes up and burning the
943
+ // whole ready deadline first
944
+ // ([#332](https://github.com/Omega-JS-Stack/omega/issues/332)).
945
+ try {
946
+ await this.preflightEmulatorPorts(projectDir, emulatorPorts);
947
+ } catch (error) {
948
+ // Nothing is spawned yet, but the stage watcher is: it is normally torn
949
+ // down with the emulator child, and this boot will never have one.
950
+ stageWatch.close();
951
+ throw error;
952
+ }
953
+
954
+ // Bumped ports can't ride the committed firebase.json — materialize a
955
+ // patched copy NEXT TO it (same dir, so relative paths keep resolving)
956
+ // and boot with --config. The https flip ALWAYS materializes (hosting
957
+ // moved to the internal port). Defaults-free runs spawn exactly as always.
958
+ let configFlag = '';
959
+ if (bumped.length > 0 || httpsCerts) {
960
+ const resolvedName = 'firebase.resolved.json';
961
+ const firebaseConfig = JSON5.parse(jetpack.read(path.join(projectDir, 'firebase.json')));
962
+ for (const [name, port] of Object.entries(emulatorPorts)) {
963
+ if (firebaseConfig.emulators?.[name]) {
964
+ firebaseConfig.emulators[name].port = port;
965
+ }
966
+ }
967
+ jetpack.write(path.join(projectDir, resolvedName), JSON.stringify(firebaseConfig, null, 2));
968
+ configFlag = ` --config ${resolvedName}`;
969
+ if (bumped.length > 0) {
970
+ this.log(chalk.yellow(` Ports in use — bumped: ${bumped.map((name) => `${name}→${emulatorPorts[name]}`).join(', ')} (booting via ${resolvedName})`));
971
+ }
972
+ if (httpsCerts) {
973
+ this.log(chalk.gray(` Hosting moves to internal :${emulatorPorts.hosting} under the HTTPS proxy on :${emulatorPorts.https} (booting via ${resolvedName})`));
974
+ }
975
+ }
976
+
977
+ // Publish the resolved map: env for our own children (functions workers
978
+ // inherit through the firebase spawn; URL getters read OMEGA_*_PORT) and
979
+ // the ports file for sibling processes of this brand (`omega test`
980
+ // against a running emulator, `omega dev`, the e2e harness).
981
+ Object.assign(process.env, portsToEnv(emulatorPorts));
982
+ writePortsFile(projectDir, emulatorPorts);
983
+
984
+ // Start the TLS terminator now — requests 502 until hosting is up, then
985
+ // https://localhost:<https> serves the whole surface. Torn down with the
986
+ // emulator child (close handler below).
987
+ let httpsProxy = null;
988
+ if (httpsCerts) {
989
+ const { startLocalHttpsProxy } = require('../../vendor/devkit/local-https.js');
990
+ httpsProxy = startLocalHttpsProxy({
991
+ port: emulatorPorts.https,
992
+ targetPort: emulatorPorts.hosting,
993
+ certs: httpsCerts,
994
+ log: (line) => this.log(chalk.green(` ${line}`)),
995
+ });
996
+ }
997
+
998
+ // Wipe stale firebase-tools debug logs + any leftover @omega.js/backend logs from older versions.
999
+ this.sweepStaleLogs();
1000
+
1001
+ // The emulator child's own log, beside firebase-tools' *-debug.log files.
1002
+ // The reset sentinel lets `omega test` ask this long-lived log for a fresh
1003
+ // slate mid-run; the poll + roll live in the shared child-log sink.
1004
+ const logPath = this.getLogsPath('emulator.log');
1005
+ const childLog = createChildLog({
1006
+ logPath,
1007
+ resetPath: this.getTempPath('emulator.log.reset'),
1008
+ });
1009
+
1010
+ // Write pre-emulator info to log file
1011
+ if (process.env.TEST_EXTENDED_MODE) {
1012
+ EXTENDED_MODE_WARNING.forEach((line) => childLog.write(`${line}\n`));
1013
+ childLog.write('\n');
1014
+ }
1015
+
1016
+ this.log(chalk.gray(` Logs saving to: ${logPath}`));
1017
+
1018
+ // OMEGA_TEST_MODE=true is passed so Functions skip external API calls (emails, SendGrid)
1019
+ // hosting is included so localhost:5002 rewrites work (e.g., /omega -> omega_api)
1020
+ // pubsub is included so scheduled functions (omega_cronDaily) can be triggered in tests
1021
+ const env = {
1022
+ ...process.env,
1023
+ FORCE_COLOR: '1',
1024
+ OMEGA_TEST_MODE: 'true',
1025
+ };
1026
+
1027
+ // Internal calls (Manager.getApiUrl) loop through the HTTPS proxy under the
1028
+ // local mkcert certificate. The child TRUSTS that root (#795) instead of
1029
+ // switching verification off wholesale — same handoff as `omega serve` and
1030
+ // `omega dev`'s legs, and Node prints no warning for it. A shell-set value
1031
+ // wins verbatim; only a host whose mkcert root vanished under existing
1032
+ // certs still takes the old bypass.
1033
+ if (httpsCerts) {
1034
+ const { mkcertCaRootPem } = require('../../vendor/devkit/local-https.js');
1035
+ const caPem = process.env.NODE_EXTRA_CA_CERTS || mkcertCaRootPem();
1036
+
1037
+ if (caPem) {
1038
+ env.NODE_EXTRA_CA_CERTS = caPem;
1039
+ } else {
1040
+ env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
1041
+ this.log(chalk.yellow(' mkcert root CA not found — internal calls fall back to NODE_TLS_REJECT_UNAUTHORIZED=0 (Node will warn)'));
1042
+ }
1043
+ }
1044
+
1045
+ // Spawn `firebase emulators:start` as a background child through the host's
1046
+ // own shell (spawnShell owns which shell that is), so its PATH resolves
1047
+ // `firebase` the way the interactive shell does.
1048
+ //
1049
+ // `detached: true` puts the child into its own process group. We need this so that
1050
+ // shutdown() can kill the entire group (sh → firebase → java emulators) by
1051
+ // signalling the negative pgid. Without it, SIGTERM to the shell doesn't propagate
1052
+ // to firebase or its java grandchildren, leaving orphan firestore/pubsub processes.
1053
+ const child = spawnShell(`firebase emulators:start ${EMULATOR_FLAGS}${configFlag}`, {
1054
+ cwd: projectDir,
1055
+ env,
1056
+ stdio: ['ignore', 'pipe', 'pipe'],
1057
+ detached: true,
1058
+ });
1059
+
1060
+ // Wire readiness detection into the stdout/stderr handlers.
1061
+ //
1062
+ // We watch for firebase-tools' explicit "All emulators ready!" line — that's the
1063
+ // signal that function discovery + load is complete and the runtime can serve HTTP.
1064
+ // Port-listening alone isn't enough: firebase-tools binds the functions socket
1065
+ // ~5-10s before user functions are actually loadable, so HTTP requests fail with
1066
+ // ECONNREFUSED / "fetch failed" if we proceed when only the port is open.
1067
+ let readyResolve;
1068
+ let readyReject;
1069
+ const readyPromise = new Promise((resolve, reject) => {
1070
+ readyResolve = resolve;
1071
+ readyReject = reject;
1072
+ });
1073
+ let ready = false;
1074
+ const READY_MARKER = /All emulators ready/i;
1075
+
1076
+ // The other end a boot can come to: firebase-tools failing to bind a port
1077
+ // the allocator handed it, because something took it in between (#778).
1078
+ // Read here rather than left to the ready deadline, so the retry starts in
1079
+ // the second the child gave up instead of three minutes later, and TAGGED,
1080
+ // so the caller retries this failure and no other.
1081
+ let inUsePort = null;
1082
+ const noteAddressInUse = (text) => {
1083
+ if (ready || inUsePort) {
1084
+ return;
1085
+ }
1086
+
1087
+ const port = addressInUsePort(text, Object.values(emulatorPorts));
1088
+
1089
+ if (!port) {
1090
+ return;
1091
+ }
1092
+
1093
+ inUsePort = port;
1094
+ const name = portName(emulatorPorts, port);
1095
+
1096
+ readyReject(Object.assign(
1097
+ new Error(`Emulator could not bind port ${port}${name ? ` (${name})` : ''}: something took it between the port probe and the bind`),
1098
+ { addressInUsePort: port, addressInUseName: name },
1099
+ ));
1100
+ };
1101
+
1102
+ child.stdout.on('data', (data) => {
1103
+ const text = data.toString();
1104
+ process.stdout.write(data);
1105
+ childLog.write(data);
1106
+ if (!ready && READY_MARKER.test(text)) {
1107
+ ready = true;
1108
+ readyResolve();
1109
+ }
1110
+ noteAddressInUse(text);
1111
+ });
1112
+
1113
+ child.stderr.on('data', (data) => {
1114
+ const text = data.toString();
1115
+ process.stderr.write(data);
1116
+ childLog.write(data);
1117
+ // firebase-tools prints the ready line to stderr sometimes — watch both.
1118
+ if (!ready && READY_MARKER.test(text)) {
1119
+ ready = true;
1120
+ readyResolve();
1121
+ }
1122
+ noteAddressInUse(text);
1123
+ });
1124
+
1125
+ // Track exit state so shutdown() can resolve when the process is gone
1126
+ let exitPromiseResolve;
1127
+ const exitPromise = new Promise((resolve) => {
1128
+ exitPromiseResolve = resolve;
1129
+ });
1130
+
1131
+ child.on('close', (code, signal) => {
1132
+ childLog.close();
1133
+ // The TLS proxy lives in THIS process — release the public port with
1134
+ // the stack (and drop any keep-alive sockets holding it open). Never
1135
+ // gated on the attempt: this handle is the attempt's OWN server, it can
1136
+ // reach no other attempt's proxy, and a retry that found this port still
1137
+ // held simply allocated around it. Skipping the close would strand the
1138
+ // port for the life of the CLI process (#778).
1139
+ if (httpsProxy) {
1140
+ httpsProxy.close();
1141
+ httpsProxy.closeAllConnections?.();
1142
+ }
1143
+ // Retract the published port map (clean shutdown). The resolved
1144
+ // firebase config is per-run scratch — remove it too.
1145
+ //
1146
+ // Both are shared BY NAME (one of each per project), and a retried boot
1147
+ // has already replaced both by the time a late first attempt closes:
1148
+ // retracting them here would unpublish the stack that is actually up and
1149
+ // delete the config it is running on. Only the attempt still in charge
1150
+ // cleans up (#778).
1151
+ if (this.activeAttempt === attempt) {
1152
+ clearPortsFile(projectDir);
1153
+ try { fs.unlinkSync(path.join(projectDir, 'firebase.resolved.json')); } catch (e) { /* not a bumped run */ }
1154
+ }
1155
+ exitPromiseResolve({ code, signal });
1156
+ // If we exited before becoming ready, fail the readiness wait too
1157
+ if (!ready) {
1158
+ readyReject(new Error(`Emulator child exited before ready (code=${code}, signal=${signal})`));
1159
+ }
1160
+ });
1161
+
1162
+ // The stage watcher lives exactly as long as the emulator child — bound
1163
+ // here, before the readiness wait, so a boot that never comes up takes it
1164
+ // down too ([#304](https://github.com/Omega-JS-Stack/omega/issues/304)).
1165
+ exitPromise.then(() => stageWatch.close());
1166
+
1167
+ // shutdown() signals the entire emulator process group (sh + firebase + java
1168
+ // grandchildren), waits up to 10s for clean exit, then escalates to SIGKILL.
1169
+ //
1170
+ // We use `process.kill(-pgid, ...)` instead of `child.kill(...)` because firebase
1171
+ // tools spawns several Java subprocesses (firestore + pubsub) that survive if
1172
+ // only the sh wrapper is killed. The negative PID targets the whole process group
1173
+ // (made possible by `detached: true` above).
1174
+ const killGroup = (signal) => {
1175
+ try {
1176
+ process.kill(-child.pid, signal);
1177
+ } catch (e) {
1178
+ // ESRCH = group already dead. EPERM = macOS kill(2) quirk when the
1179
+ // group's remaining members are zombies awaiting reap. Either way the
1180
+ // group is unreachable — and killGroup must NEVER throw out of
1181
+ // shutdown(): the un-awaited shutdown() in the SIGINT handlers turns
1182
+ // a throw into an unhandled rejection that crashes the CLI BEFORE
1183
+ // the orphan sweep + ports-file cleanup run (this exact crash leaked
1184
+ // 3 java emulators per e2e run). The port-based sweep after shutdown
1185
+ // is the safety net for anything a failed signal left behind.
1186
+ if (e.code !== 'ESRCH' && e.code !== 'EPERM') {
1187
+ this.log(chalk.yellow(` killGroup(${signal}) failed: ${e.message} — relying on the orphan sweep`));
1188
+ }
1189
+ }
1190
+ };
1191
+
1192
+ let shutdownDone = false;
1193
+ const shutdown = async () => {
1194
+ if (shutdownDone) {
1195
+ return;
1196
+ }
1197
+
1198
+ // 0. Re-record the pid set while the tree is still attached. The boot
1199
+ // snapshot misses anything spawned since (function runtime workers come
1200
+ // and go), and once the parent is gone the descendants reparent to PID 1
1201
+ // — this is the last moment ownership is readable
1202
+ // ([#274](https://github.com/Omega-JS-Stack/omega/issues/274)).
1203
+ try {
1204
+ this.writeEmulatorPidRecord(child.pid, this.loadProjectId(projectDir), emulatorPorts);
1205
+ } catch (error) { /* the boot record still stands */ }
1206
+
1207
+ // 1. Signal the process group (sh + firebase + direct children)
1208
+ if (child.exitCode === null && child.signalCode === null) {
1209
+ killGroup('SIGTERM');
1210
+
1211
+ // Wait up to 5s for clean exit, then SIGKILL the group
1212
+ const exited = await Promise.race([
1213
+ exitPromise.then(() => true),
1214
+ new Promise((r) => setTimeout(() => r(false), 5000)),
1215
+ ]);
1216
+
1217
+ if (!exited) {
1218
+ killGroup('SIGKILL');
1219
+ await Promise.race([
1220
+ exitPromise.then(() => true),
1221
+ new Promise((r) => setTimeout(() => r(false), 3000)),
1222
+ ]);
1223
+ }
1224
+ }
1225
+
1226
+ // 2. Take the jars the group signal cannot reach, by pid, sweep whatever
1227
+ // orphaned outside the record, and prove the ports came back
1228
+ // ([#304](https://github.com/Omega-JS-Stack/omega/issues/304)). The
1229
+ // shared hub/storage ports belong to this run only when nothing bumped.
1230
+ await this.terminateRecordedEmulatorProcesses(emulatorPorts, { sweepShared: bumped.length === 0 });
1231
+
1232
+ shutdownDone = true;
1233
+ };
1234
+
1235
+ // Race the readiness marker against a deadline. A boot that never comes
1236
+ // up has still spawned: the shell, firebase, and any jar it got as far as.
1237
+ // Throwing straight out of here left them running and handed the caller no
1238
+ // handle to stop them, so a failed boot takes the same stop path a normal
1239
+ // one does before it propagates
1240
+ // ([#304](https://github.com/Omega-JS-Stack/omega/issues/304)). The
1241
+ // deadline is env-tunable and defaults high enough to absorb a slow port
1242
+ // sweep ([#332](https://github.com/Omega-JS-Stack/omega/issues/332)).
1243
+ const readyTimeoutMs = resolveReadyTimeout(process.env.OMEGA_EMULATOR_READY_TIMEOUT);
1244
+ try {
1245
+ await Promise.race([
1246
+ readyPromise,
1247
+ new Promise((_, reject) => setTimeout(
1248
+ () => reject(new Error(`Emulator did not print "All emulators ready" within ${readyTimeoutMs}ms`)),
1249
+ readyTimeoutMs,
1250
+ )),
1251
+ ]);
1252
+ } catch (error) {
1253
+ await shutdown();
1254
+ throw error;
1255
+ }
1256
+
1257
+ // The stack is UP and every java emulator exists — record the pids now, so
1258
+ // the post-shutdown sweep can prove which orphans are its own instead of
1259
+ // signaling whatever holds a port
1260
+ // ([#274](https://github.com/Omega-JS-Stack/omega/issues/274)). Non-fatal:
1261
+ // without a record the sweep falls back to command-line proof and spares
1262
+ // anything it cannot place.
1263
+ try {
1264
+ this.writeEmulatorPidRecord(child.pid, this.loadProjectId(projectDir), emulatorPorts);
1265
+ } catch (error) {
1266
+ this.logWarning(`Could not record emulator pids (${error.message}) — the orphan sweep will only spare, never over-reach`);
1267
+ }
1268
+
1269
+ return { child, shutdown, emulatorPorts, bumped, exitPromise };
1270
+ }
1271
+
1272
+ /**
1273
+ * Boot emulators and run a single command against them. Sends SIGTERM to the emulator
1274
+ * when the command exits (or this process is interrupted) and waits for clean shutdown.
1275
+ *
1276
+ * No current caller — `npx omega emulator`'s keep-alive flow boots through
1277
+ * execute() now; kept for programmatic embedding.
1278
+ * `npx omega test`'s auto-start path uses startEmulators() directly so it can tee the
1279
+ * test command's output to its own log (test.log) separate from emulator.log.
1280
+ *
1281
+ * @param {string} command - shell command to run while emulators are up
1282
+ */
1283
+ async runWithEmulator(command) {
1284
+ // shutdown() carries the whole teardown (recorded jars, orphan sweep, port
1285
+ // verdict), so both paths below just join it.
1286
+ const { shutdown, exitPromise } = await this.startEmulators();
1287
+
1288
+ // Same synchronous handler and same three signals as execute() — see the
1289
+ // comment there.
1290
+ let stopCount = 0;
1291
+ const onStopSignal = () => {
1292
+ stopCount++;
1293
+ shutdown();
1294
+ };
1295
+ STOP_SIGNALS.forEach((signal) => process.on(signal, onStopSignal));
1296
+
1297
+ try {
1298
+ // Run the user command; when it exits we tear down the emulator.
1299
+ const cmdChild = spawnShell(command, {
1300
+ cwd: this.main.firebaseProjectPath,
1301
+ env: { ...process.env, FORCE_COLOR: '1' },
1302
+ stdio: 'inherit',
1303
+ });
1304
+
1305
+ const cmdExit = await new Promise((resolve) => {
1306
+ cmdChild.on('close', (code, signal) => resolve({ code, signal }));
1307
+ });
1308
+
1309
+ STOP_SIGNALS.forEach((signal) => process.removeListener(signal, onStopSignal));
1310
+ await shutdown();
1311
+ await exitPromise;
1312
+
1313
+ if (cmdExit.code !== 0) {
1314
+ throw Object.assign(new Error(`Command exited with code ${cmdExit.code}`), { code: cmdExit.code });
1315
+ }
1316
+ } catch (e) {
1317
+ STOP_SIGNALS.forEach((signal) => process.removeListener(signal, onStopSignal));
1318
+ await shutdown();
1319
+ throw e;
1320
+ }
1321
+ }
1322
+
1323
+ /**
1324
+ * Sweep the ports this boot is about to bind and fail fast when one is held.
1325
+ *
1326
+ * The set is read from the firebase config, never hard-coded: the resolved
1327
+ * map covers everything the allocator owns, and the config's own `emulators`
1328
+ * block covers anything it does not (an eventarc or tasks port a brand
1329
+ * declared), which is the half that cannot bump. A lenient read matches
1330
+ * loadPortPins(): an unreadable config means no extra ports, never a boot
1331
+ * failure on its own.
1332
+ * @param {string} projectDir - The firebase project directory.
1333
+ * @param {object} emulatorPorts - This run's resolved port map.
1334
+ * @param {Function} [isFree] - Port probe (injectable).
1335
+ * @returns {Promise<Array<{name: string, port: number}>>} The plan that cleared.
1336
+ */
1337
+ async preflightEmulatorPorts(projectDir, emulatorPorts, isFree = isPortFree) {
1338
+ let declared = {};
1339
+
1340
+ try {
1341
+ declared = declaredEmulatorPorts(JSON5.parse(jetpack.read(path.join(projectDir, 'firebase.json'))));
1342
+ } catch (error) { /* no readable config, so the resolved map is the whole plan */ }
1343
+
1344
+ const planned = plannedEmulatorPorts(declared, emulatorPorts);
1345
+ await assertPlannedPortsFree(planned, isFree);
1346
+
1347
+ return planned;
1348
+ }
1349
+
1350
+ /**
1351
+ * Who is listening on a port right now: pid and command line, for a report.
1352
+ *
1353
+ * READ ONLY, deliberately: a port this run could not bind is held by someone
1354
+ * else's live process, and nothing in this file signals a process it cannot
1355
+ * prove is its own ([#274](https://github.com/Omega-JS-Stack/omega/issues/274),
1356
+ * [#778](https://github.com/Omega-JS-Stack/omega/issues/778)).
1357
+ * @param {number} port - The port to name a holder for.
1358
+ * @param {object} [seams]
1359
+ * @param {Function} [seams.listPids] - Port to pid lookup (injectable).
1360
+ * @param {Function} [seams.readCommand] - The `ps` read (injectable).
1361
+ * @returns {Array<{pid: number, command: string}>}
1362
+ */
1363
+ describePortHolders(port, { listPids = listListeningPids, readCommand = readProcessCommand } = {}) {
1364
+ return listPids(port).map((pid) => ({ pid: Number(pid), command: readCommand(pid) }));
1365
+ }
1366
+
1367
+ /**
1368
+ * Read explicit port pins from the brand config's `ports` section (N7).
1369
+ * Lenient — a missing/broken config means no pins, never a boot failure.
1370
+ */
1371
+ loadPortPins(projectDir) {
1372
+ try {
1373
+ const { hasOmegaConfig, loadConfig } = require('../../vendor/config/index.js');
1374
+ const functionsDir = path.join(projectDir, 'dist');
1375
+ if (!hasOmegaConfig(functionsDir)) {
1376
+ return {};
1377
+ }
1378
+ const config = loadConfig(functionsDir, 'backend').config;
1379
+ return config.ports && typeof config.ports === 'object' ? config.ports : {};
1380
+ } catch (error) {
1381
+ return {};
1382
+ }
1383
+ }
1384
+
1385
+ /**
1386
+ * The project this stack serves, read from the brand config. Lenient like
1387
+ * loadPortPins() — a missing/broken config means no project id, which the
1388
+ * ownership matcher treats as "no evidence", never a boot failure.
1389
+ */
1390
+ loadProjectId(projectDir) {
1391
+ try {
1392
+ const { hasOmegaConfig, loadConfig } = require('../../vendor/config/index.js');
1393
+ const functionsDir = path.join(projectDir, 'dist');
1394
+ if (!hasOmegaConfig(functionsDir)) {
1395
+ return null;
1396
+ }
1397
+ return loadConfig(functionsDir, 'backend').config.cloud?.config?.projectId || null;
1398
+ } catch (error) {
1399
+ return null;
1400
+ }
1401
+ }
1402
+
1403
+ /**
1404
+ * The ownership evidence this run can offer the sweep: the pids it recorded
1405
+ * when its stack came up, and the project it belongs to. Recorded pids age
1406
+ * out — see ownershipFromRecord().
1407
+ * @returns {{pids: number[], projectId: string|null, rootPid: number|null}}
1408
+ */
1409
+ readEmulatorOwnership() {
1410
+ return ownershipFromRecord(jetpack.read(this.getTempPath(PID_RECORD_FILE), 'json'));
1411
+ }
1412
+
1413
+ /**
1414
+ * Record the stack's pids while it is UP, so the post-shutdown sweep can
1415
+ * prove which orphans are its own
1416
+ * ([#274](https://github.com/Omega-JS-Stack/omega/issues/274)).
1417
+ *
1418
+ * The emulator child's descendants cover firebase-tools' own tree — the java
1419
+ * emulators and every functions runtime worker it forks. The background
1420
+ * watcher is the one member that hangs off the CLI process instead, so it is
1421
+ * named separately ([#629](https://github.com/Omega-JS-Stack/omega/issues/629)).
1422
+ * The resolved port map rides along: it is what the NEXT boot's stale reap
1423
+ * waits on after it takes these pids down, since a pid it killed names no
1424
+ * port of its own ([#730](https://github.com/Omega-JS-Stack/omega/issues/730)).
1425
+ * @param {number} rootPid - The spawned child's pid.
1426
+ * @param {string|null} projectId - The project this stack serves.
1427
+ * @param {object} [ports] - This run's resolved port map.
1428
+ */
1429
+ writeEmulatorPidRecord(rootPid, projectId, ports) {
1430
+ const existing = this.readEmulatorOwnership();
1431
+
1432
+ // UNION with what this run already recorded: a later snapshot can only see
1433
+ // what is still attached, and a child that orphaned in between is exactly
1434
+ // the one the sweep exists for. Pids of processes that have since exited
1435
+ // cost nothing — signaling one is a caught ESRCH.
1436
+ const previous = existing.pids.length > 0 && existing.rootPid === rootPid ? existing.pids : [];
1437
+ // NAME DEPENDENCY: if this record misses the watcher, the command-line fallback
1438
+ // (EMULATOR_COMMAND, /emulator|firebase/i) only catches it because watch.js bakes
1439
+ // the `emulator.log.reset` sentinel PATH into nodemon's --exec argv. Rename that
1440
+ // sentinel and the watcher stops matching — spared by every stop path, immortal.
1441
+ const watcher = this.backgroundWatcherPid ? collectDescendantPids(this.backgroundWatcherPid) : [];
1442
+ const pids = [...new Set([...previous, ...collectDescendantPids(rootPid), ...watcher])];
1443
+
1444
+ jetpack.write(this.getTempPath(PID_RECORD_FILE), {
1445
+ pids: pids,
1446
+ projectId: projectId || null,
1447
+ rootPid: rootPid,
1448
+ ports: ports || {},
1449
+ startedAt: new Date().toISOString(),
1450
+ });
1451
+
1452
+ return pids;
1453
+ }
1454
+
1455
+ /**
1456
+ * Terminate the stack this run RECORDED, then prove its ports came back.
1457
+ *
1458
+ * The group signal in shutdown() reaches `sh`/firebase and its direct
1459
+ * children only: firebase-tools starts each java emulator in its OWN process
1460
+ * group, so the jars never see it, and they reparent to PID 1 the moment
1461
+ * their parent goes. A stop that escalates to SIGKILL (a firestore emulator
1462
+ * that does not finish its graceful stop inside the grace window) therefore
1463
+ * leaves the jars running, and the next brand's boot bumps around a squatter
1464
+ * that answers on the classic ports forever
1465
+ * ([#304](https://github.com/Omega-JS-Stack/omega/issues/304)). The pid
1466
+ * record written while the tree was still attached is the only surviving
1467
+ * link to them, so the stop path signals what it names, BY PID.
1468
+ *
1469
+ * Ownership stays proven per process — a recorded number whose live command
1470
+ * line is not emulator machinery is a recycled pid, never our jar — and the
1471
+ * port check only REPORTS: nothing is signaled for occupying a port
1472
+ * ([#274](https://github.com/Omega-JS-Stack/omega/issues/274)).
1473
+ *
1474
+ * The owned-orphan sweep runs BEFORE that check, because it can still free a
1475
+ * port: a jar the record never named (spawned after the last snapshot, then
1476
+ * reparented to PID 1) is the sweep's case, not the record's. Verifying
1477
+ * first spent the whole release window on a port the next step was about to
1478
+ * clear, then reported it as somebody else's.
1479
+ *
1480
+ * @param {object} emulatorPorts - This run's resolved port map.
1481
+ * @param {object} [options]
1482
+ * @param {boolean} [options.sweepShared] - Look at the shared hub/storage
1483
+ * ports too (a defaults run owns them; a bumped run does not).
1484
+ */
1485
+ async terminateRecordedEmulatorProcesses(emulatorPorts, { sweepShared = true } = {}) {
1486
+ const ownership = this.readEmulatorOwnership();
1487
+ const targets = [];
1488
+
1489
+ for (const recorded of ownership.pids) {
1490
+ const pid = Number(recorded);
1491
+ const command = readProcessCommand(pid);
1492
+
1493
+ // Gone already (the group signal took it) or not ours anymore.
1494
+ if (!command || !isStoppableEmulatorProcess({ pid: pid, command: command }, ownership)) {
1495
+ continue;
1496
+ }
1497
+
1498
+ targets.push(pid);
1499
+ }
1500
+
1501
+ for (const pid of targets) {
1502
+ try { process.kill(pid, 'SIGTERM'); } catch (e) { /* exited between the read and the signal */ }
1503
+ }
1504
+
1505
+ // Give them the grace window, then take whatever is still standing.
1506
+ const survivors = await this.waitForProcessesToExit(targets, STOP_GRACE_MS);
1507
+
1508
+ for (const pid of survivors) {
1509
+ try { process.kill(pid, 'SIGKILL'); } catch (e) { /* gone in the meantime */ }
1510
+ }
1511
+
1512
+ if (targets.length > 0) {
1513
+ this.log(chalk.gray(` Stopped ${targets.length} recorded emulator process${targets.length > 1 ? 'es' : ''}${survivors.length > 0 ? ` (${survivors.length} needed SIGKILL)` : ''}.`));
1514
+ }
1515
+
1516
+ // Anything of ours the record could not name is the sweep's to take, and
1517
+ // it runs while the ports still matter — before the verdict below.
1518
+ await this.terminateOrphanedEmulatorProcesses(emulatorPorts, { sweepShared: sweepShared });
1519
+
1520
+ // The ports are the only proof that matters to the NEXT boot.
1521
+ const { https: _httpsPort, ...checkable } = emulatorPorts || {};
1522
+ const held = await this.waitForPortsReleased(Object.values(checkable));
1523
+
1524
+ if (held.length > 0) {
1525
+ this.logWarning(`Emulator ports still in use after shutdown: ${held.join(', ')} (everything this run could prove it owns is stopped; whatever holds them did not prove to be ours)`);
1526
+ }
1527
+ }
1528
+
1529
+ /**
1530
+ * Poll a pid set until every one is gone or the window closes.
1531
+ * @param {number[]} pids - The pids to watch.
1532
+ * @param {number} timeoutMs - How long to wait in total.
1533
+ * @param {object} [seams]
1534
+ * @param {Function} [seams.readCommand] - The `ps` read (injectable).
1535
+ * @returns {Promise<number[]>} The pids still alive when the window closed.
1536
+ */
1537
+ async waitForProcessesToExit(pids, timeoutMs, { readCommand = readProcessCommand } = {}) {
1538
+ const deadline = Date.now() + timeoutMs;
1539
+ let alive = pids;
1540
+
1541
+ while (alive.length > 0 && Date.now() < deadline) {
1542
+ await powertools.wait(POLL_INTERVAL_MS);
1543
+ alive = alive.filter((pid) => !!readCommand(pid));
1544
+ }
1545
+
1546
+ return alive;
1547
+ }
1548
+
1549
+ /**
1550
+ * Poll a port set until nothing is listening or the window closes.
1551
+ * @param {number[]} ports - The ports this run resolved.
1552
+ * @returns {Promise<number[]>} The ports still held when the window closed.
1553
+ */
1554
+ async waitForPortsReleased(ports, { isFree = isPortFree } = {}) {
1555
+ const deadline = Date.now() + PORT_RELEASE_TIMEOUT_MS;
1556
+ let held = [...new Set(ports)];
1557
+
1558
+ while (held.length > 0 && Date.now() < deadline) {
1559
+ const free = await Promise.all(held.map((port) => isFree(port)));
1560
+ held = held.filter((port, index) => !free[index]);
1561
+
1562
+ if (held.length > 0) {
1563
+ await powertools.wait(POLL_INTERVAL_MS);
1564
+ }
1565
+ }
1566
+
1567
+ return held;
1568
+ }
1569
+
1570
+ /**
1571
+ * Terminate processes still listening on THIS RUN's emulator ports after
1572
+ * shutdown. Firebase-tools spawns Java emulators (Firestore, Database,
1573
+ * PubSub) that often survive SIGTERM/SIGKILL of the firebase node process.
1574
+ * This sweep runs AFTER the main child exits, so anything still on these
1575
+ * ports is orphaned.
1576
+ *
1577
+ * Ownership is PROVEN per process, never inferred from the port: the sweep
1578
+ * used to signal whatever was listening and took down another project's live
1579
+ * emulator on the shared hub/storage ports
1580
+ * ([#274](https://github.com/Omega-JS-Stack/omega/issues/274)). A port only
1581
+ * decides where to LOOK; isOwnedEmulatorProcess() decides what to signal.
1582
+ *
1583
+ * N7: the sweep takes the RESOLVED port map — no map → no sweep. The shared
1584
+ * hub (4400) + storage (9199) ports are looked at only on a defaults run
1585
+ * (`sweepShared`); on a bumped run they belong to the incumbent.
1586
+ * @param {object} emulatorPorts - This run's resolved port map.
1587
+ * @param {object} [options]
1588
+ * @param {boolean} [options.sweepShared] - Include the shared hub/storage ports.
1589
+ * @param {Function} [options.isFree] - Port probe (injectable).
1590
+ * @param {Function} [options.listPids] - Port to pid lookup (injectable).
1591
+ */
1592
+ async terminateOrphanedEmulatorProcesses(emulatorPorts, { sweepShared = true, isFree = isPortFree, listPids = listListeningPids } = {}) {
1593
+ if (!emulatorPorts) {
1594
+ return;
1595
+ }
1596
+
1597
+ // `https` is OUR in-process TLS proxy (closed with the child), never an
1598
+ // orphaned java emulator — sweeping it would signal this very process
1599
+ // on a close-timing race.
1600
+ const { https: _httpsPort, ...sweepable } = emulatorPorts;
1601
+ const ports = Object.values(sweepable);
1602
+ if (sweepShared) {
1603
+ ports.push(4400, 9199);
1604
+ }
1605
+
1606
+ const ownership = this.readEmulatorOwnership();
1607
+
1608
+ // The probe is the only await, and it runs before a single signal is sent.
1609
+ // The kill loop itself stays synchronous, so Ctrl+C spam cannot land
1610
+ // between two signals of the same sweep.
1611
+ const { execSync } = require('child_process');
1612
+ let terminated = 0;
1613
+ let spared = 0;
1614
+
1615
+ for (const port of await heldPorts(ports, isFree)) {
1616
+ for (const pid of listPids(port)) {
1617
+ try {
1618
+ const command = execSync(`ps -o command= -p ${pid} 2>/dev/null`, { encoding: 'utf8' }).trim();
1619
+
1620
+ if (!isOwnedEmulatorProcess({ pid: pid, command: command }, ownership)) {
1621
+ spared++;
1622
+ continue;
1623
+ }
1624
+
1625
+ process.kill(Number(pid), 'SIGKILL');
1626
+ terminated++;
1627
+ } catch (e) { /* vanished mid-check, or already gone */ }
1628
+ }
1629
+ }
1630
+
1631
+ if (terminated > 0) {
1632
+ this.log(chalk.gray(` Cleaned up ${terminated} orphaned emulator process${terminated > 1 ? 'es' : ''}.`));
1633
+ }
1634
+
1635
+ if (spared > 0) {
1636
+ this.log(chalk.gray(` Left ${spared} process${spared > 1 ? 'es' : ''} on these ports alone — not this project's emulator.`));
1637
+ }
1638
+ }
1639
+
1640
+ /**
1641
+ * Pre-boot reap of every ORPHANED emulator-family process on the MACHINE
1642
+ * ([#781](https://github.com/Omega-JS-Stack/omega/issues/781)).
1643
+ *
1644
+ * The verdict, the escalation and the settle wait all live in
1645
+ * cli/commands/emulator-orphans.js, which reads the world through seams; this
1646
+ * is the wiring: the boot's own settle windows and the boot's voice.
1647
+ * @param {object} [seams] - The sweep's seams (tests state a process table here).
1648
+ */
1649
+ async reapMachineOrphans(seams) {
1650
+ await emulatorOrphans.reapMachineOrphans({
1651
+ graceMs: STOP_GRACE_MS,
1652
+ portReleaseTimeoutMs: PORT_RELEASE_TIMEOUT_MS,
1653
+ pollIntervalMs: POLL_INTERVAL_MS,
1654
+ log: (message) => this.log(chalk.gray(` ${message}`)),
1655
+ ...seams,
1656
+ });
1657
+ }
1658
+
1659
+ /**
1660
+ * Pre-boot reap of a STALE pid record: the leftovers of a previous run that
1661
+ * died without teardown, taken BY PID, before this boot overwrites the file
1662
+ * that names them ([#721](https://github.com/Omega-JS-Stack/omega/issues/721)).
1663
+ *
1664
+ * The sibling sweep above only ever considers an ORPHAN, so it cannot reach a
1665
+ * dead run's member that still hangs off another member of that same dead run
1666
+ * — the reload watcher's usual shape while its root is unwinding — and
1667
+ * writeEmulatorPidRecord drops the previous record the moment a new root pid
1668
+ * appears. Between the two, a dead boot's nodemon survived every stop path
1669
+ * this framework has, forever.
1670
+ *
1671
+ * STALE is the first proof: the record's ROOT pid is gone, so the run that
1672
+ * wrote it is not around to own its own processes. A live root means a LIVE
1673
+ * record — this boot is a second stack beside a running one — and nothing in
1674
+ * it is anyone's to reap. A record that names no usable root at all proves
1675
+ * neither, and an unprovable root is not a kill order.
1676
+ *
1677
+ * IDENTITY is the second, and it is the whole safety story. A record is a
1678
+ * list of NUMBERS, the OS recycles numbers, and the run that could vouch for
1679
+ * them is by definition gone. So nothing is signalled on the number: the LIVE
1680
+ * row must still read as emulator machinery of this project AND hang off the
1681
+ * dead run itself (isReapableRecordedProcess). A pid whose row cannot be read
1682
+ * has no identity to prove and is never signalled. The escalation re-proves
1683
+ * it, because the grace window is long enough for a number to be freed and
1684
+ * handed out again — and there is no apologising to a SIGKILL.
1685
+ *
1686
+ * @param {{pids: number[], projectId: string|null, rootPid: number|null, ports: object}} ownership - The PREVIOUS record.
1687
+ * @param {object} [seams]
1688
+ * @param {Function} [seams.readProcess] - The `ps -o ppid=,command=` read (injectable).
1689
+ * @param {Function} [seams.kill] - The signal (injectable).
1690
+ * @param {Function} [seams.isFree] - Port probe for the settle wait (injectable).
1691
+ */
1692
+ async reapStaleRecordedProcesses(ownership, { readProcess = readProcessInfo, kill = process.kill, isFree = isPortFree } = {}) {
1693
+ const rootPid = Number(ownership?.rootPid);
1694
+
1695
+ // Staleness is proved BY the root, so a record that names no usable one
1696
+ // proves nothing — and an unprovable root is not a kill order. Read the
1697
+ // other way ("no root answering"), a record written without a rootPid over
1698
+ // a LIVE stack of this same project reads as pure leftovers and the reap
1699
+ // takes the running firebase parent with it.
1700
+ if (!Number.isInteger(rootPid) || rootPid <= 1) {
1701
+ return;
1702
+ }
1703
+
1704
+ // A root still answering is a run still up. A root whose number was
1705
+ // recycled onto a stranger reads the same and is also left alone: erring
1706
+ // toward "somebody is using this" is the only safe direction here.
1707
+ if (readProcess(rootPid)) {
1708
+ return;
1709
+ }
1710
+
1711
+ const targets = [];
1712
+ let spared = 0;
1713
+
1714
+ for (const recorded of ownership?.pids || []) {
1715
+ const pid = Number(recorded);
1716
+
1717
+ // Never this run or its parent. `omega emulator` matches the machinery
1718
+ // signature by NAME, so a recorded number recycled onto this very process
1719
+ // would otherwise make the boot signal itself.
1720
+ if (pid === process.pid || pid === process.ppid) {
1721
+ continue;
1722
+ }
1723
+
1724
+ const live = readProcess(pid);
1725
+
1726
+ // Gone (nothing to do) or unreadable (no proof, so no kill order).
1727
+ if (!live) {
1728
+ continue;
1729
+ }
1730
+
1731
+ if (!isReapableRecordedProcess({ pid: pid, ppid: live.ppid, command: live.command }, ownership)) {
1732
+ spared++;
1733
+ continue;
1734
+ }
1735
+
1736
+ targets.push(pid);
1737
+ }
1738
+
1739
+ for (const pid of targets) {
1740
+ try { kill(pid, 'SIGTERM'); } catch (e) { /* exited between the read and the signal */ }
1741
+ }
1742
+
1743
+ // The exit watch reads through the same `ps` seam the identity proof does.
1744
+ const readCommand = (pid) => {
1745
+ const live = readProcess(pid);
1746
+ return live ? live.command : '';
1747
+ };
1748
+
1749
+ const survivors = await this.waitForProcessesToExit(targets, STOP_GRACE_MS, { readCommand: readCommand });
1750
+ const killed = [];
1751
+
1752
+ for (const pid of survivors) {
1753
+ const live = readProcess(pid);
1754
+
1755
+ if (!live || !isReapableRecordedProcess({ pid: pid, ppid: live.ppid, command: live.command }, ownership)) {
1756
+ spared++;
1757
+ continue;
1758
+ }
1759
+
1760
+ try {
1761
+ kill(pid, 'SIGKILL');
1762
+ killed.push(pid);
1763
+ } catch (e) { /* gone in the meantime */ }
1764
+ }
1765
+
1766
+ // Only what actually went down: a target that outlived the grace window and
1767
+ // came back as somebody else's number is spared, so counting the kill
1768
+ // ORDERS would report it reaped and left alone in the same breath.
1769
+ const downed = targets.length - survivors.length + killed.length;
1770
+
1771
+ if (downed > 0) {
1772
+ this.log(chalk.gray(` Reaped ${downed} recorded process${downed > 1 ? 'es' : ''} left by a previous run${killed.length > 0 ? ` (${killed.length} needed SIGKILL)` : ''}.`));
1773
+
1774
+ // A signal is not an exit: kill() returns while the kernel is still
1775
+ // unwinding the process, so a probe in the same tick asks about a
1776
+ // shutdown that has not happened yet, and a jar mid-unwind can answer
1777
+ // FREE on a port it is about to keep holding, which is exactly the
1778
+ // "probed free, then failed to bind" the retry above exists to catch.
1779
+ // The numbers go first, THEN the ports are asked
1780
+ // ([#778](https://github.com/Omega-JS-Stack/omega/issues/778)).
1781
+ const lingering = await this.waitForProcessesToExit(killed, STOP_GRACE_MS, { readCommand: readCommand });
1782
+
1783
+ if (lingering.length > 0) {
1784
+ this.logWarning(`Pid${lingering.length > 1 ? 's' : ''} ${lingering.join(', ')} outlived SIGKILL; the port check below is the last word on ${lingering.length > 1 ? 'them' : 'it'}`);
1785
+ }
1786
+
1787
+ // Same race the port-driven reaper answers above: a SIGKILLed JVM holds
1788
+ // its listener for a moment after the process is gone, and the allocator
1789
+ // probes these very ports a few lines later. A reap here is BY PID, so
1790
+ // nothing says which port each one held — the record's own map is the
1791
+ // set, and the worst case is the release window spent on a port a live
1792
+ // neighbour took over, which the log names before the allocator bumps
1793
+ // around it. `https` is not in it: that port belonged to the dead run's
1794
+ // in-process TLS proxy, never to a jar this reap could take.
1795
+ const { https: _httpsPort, ...settleable } = ownership?.ports || {};
1796
+
1797
+ if (Object.keys(settleable).length === 0) {
1798
+ this.log(chalk.gray(' The record named no ports, so nothing was waited on; the allocation probe is the only check on what this reap just freed.'));
1799
+ }
1800
+
1801
+ const held = await this.waitForPortsReleased(Object.values(settleable), { isFree });
1802
+
1803
+ if (held.length > 0) {
1804
+ this.log(chalk.gray(` Port${held.length > 1 ? 's' : ''} ${held.join(', ')} still closing after the reap; the allocator will bump around ${held.length > 1 ? 'them' : 'it'}.`));
1805
+ }
1806
+ }
1807
+
1808
+ if (spared > 0) {
1809
+ this.log(chalk.gray(` Left ${spared} recorded pid${spared > 1 ? 's' : ''} alone — the number is no longer this project's emulator.`));
1810
+ }
1811
+ }
1812
+ }
1813
+
1814
+ // Static, alongside Middleware's precedent — the ownership decision is pure,
1815
+ // so tests exercise it directly with real `ps` rows instead of live processes.
1816
+ EmulatorCommand.resolveReadyTimeout = resolveReadyTimeout;
1817
+ EmulatorCommand.heldPorts = heldPorts;
1818
+ EmulatorCommand.listListeningPids = listListeningPids;
1819
+ EmulatorCommand.PORT_LOOKUP_TIMEOUT_MS = PORT_LOOKUP_TIMEOUT_MS;
1820
+ EmulatorCommand.plannedEmulatorPorts = plannedEmulatorPorts;
1821
+ EmulatorCommand.assertPlannedPortsFree = assertPlannedPortsFree;
1822
+ EmulatorCommand.isOwnedEmulatorProcess = isOwnedEmulatorProcess;
1823
+ EmulatorCommand.isStoppableEmulatorProcess = isStoppableEmulatorProcess;
1824
+ EmulatorCommand.isReapableRecordedProcess = isReapableRecordedProcess;
1825
+ EmulatorCommand.readProcessInfo = readProcessInfo;
1826
+ EmulatorCommand.ownershipFromRecord = ownershipFromRecord;
1827
+ EmulatorCommand.collectDescendantPids = collectDescendantPids;
1828
+
1829
+ module.exports = EmulatorCommand;