@omega.js/manager 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +14 -0
- package/LICENSE +98 -0
- package/README.md +165 -0
- package/bin/omega +2 -0
- package/bin/omg +2 -0
- package/claude-plugin/.claude-plugin/plugin.json +11 -0
- package/claude-plugin/.mcp.json +9 -0
- package/claude-plugin/README.md +139 -0
- package/claude-plugin/hooks/gate/mark.sh +58 -0
- package/claude-plugin/hooks/gate/run.sh +97 -0
- package/claude-plugin/hooks/guard/run.sh +249 -0
- package/claude-plugin/hooks/hooks.json +63 -0
- package/claude-plugin/hooks/inject/run.sh +127 -0
- package/claude-plugin/hooks/lib/omega-gate.sh +20 -0
- package/claude-plugin/hooks/lib/omega-scope.sh +41 -0
- package/claude-plugin/hooks/lib/omega-skills.sh +209 -0
- package/claude-plugin/hooks/quality/run.sh +132 -0
- package/claude-plugin/hooks/shape/run.sh +45 -0
- package/claude-plugin/mcp-router-launch.js +29 -0
- package/claude-plugin/skills/README.md +48 -0
- package/claude-plugin/skills/accessibility/SKILL.md +31 -0
- package/claude-plugin/skills/analytics/SKILL.md +30 -0
- package/claude-plugin/skills/backend/SKILL.md +25 -0
- package/claude-plugin/skills/brandcheck/SKILL.md +29 -0
- package/claude-plugin/skills/browser/SKILL.md +64 -0
- package/claude-plugin/skills/client/SKILL.md +24 -0
- package/claude-plugin/skills/desktop/SKILL.md +25 -0
- package/claude-plugin/skills/extension/SKILL.md +25 -0
- package/claude-plugin/skills/main/SKILL.md +54 -0
- package/claude-plugin/skills/manager/SKILL.md +25 -0
- package/claude-plugin/skills/seo/SKILL.md +36 -0
- package/claude-plugin/skills/theme/SKILL.md +33 -0
- package/claude-plugin/skills/web/SKILL.md +29 -0
- package/dist/cli-run.js +34 -0
- package/dist/cli.js +29 -0
- package/dist/commands/build.js +17 -0
- package/dist/commands/clean.js +16 -0
- package/dist/commands/company.js +46 -0
- package/dist/commands/deploy.js +143 -0
- package/dist/commands/dev.js +437 -0
- package/dist/commands/devlog.js +25 -0
- package/dist/commands/help.js +48 -0
- package/dist/commands/manage.js +53 -0
- package/dist/commands/migrate.js +76 -0
- package/dist/commands/onboard.js +31 -0
- package/dist/commands/pipeline.js +319 -0
- package/dist/commands/test.js +380 -0
- package/dist/commands/update.js +128 -0
- package/dist/commands/version.js +8 -0
- package/dist/company-init.js +167 -0
- package/dist/company.js +283 -0
- package/dist/config.js +1359 -0
- package/dist/devlog/index.js +226 -0
- package/dist/devlog/lib/collect-commits.js +136 -0
- package/dist/devlog/lib/generate-post.js +201 -0
- package/dist/devlog/lib/ghostii.js +122 -0
- package/dist/devlog/lib/project-map.js +98 -0
- package/dist/devlog/lib/publish-website.js +88 -0
- package/dist/index.js +45 -0
- package/dist/lib/agents-md.js +201 -0
- package/dist/lib/analytics-secret.js +22 -0
- package/dist/lib/argv.js +21 -0
- package/dist/lib/auth-admin.js +120 -0
- package/dist/lib/automation-client.js +210 -0
- package/dist/lib/backend-marketing.js +49 -0
- package/dist/lib/brand.js +240 -0
- package/dist/lib/bundle-id.js +53 -0
- package/dist/lib/claude-settings.js +125 -0
- package/dist/lib/company-scaffold.js +213 -0
- package/dist/lib/company.js +227 -0
- package/dist/lib/config-flow.js +350 -0
- package/dist/lib/config-write.js +41 -0
- package/dist/lib/custom-target.js +75 -0
- package/dist/lib/domain-utils.js +19 -0
- package/dist/lib/duration.js +39 -0
- package/dist/lib/env-order.js +236 -0
- package/dist/lib/env-secret.js +78 -0
- package/dist/lib/firestore-rest.js +326 -0
- package/dist/lib/framework-bin.js +156 -0
- package/dist/lib/gitignore.js +63 -0
- package/dist/lib/google-auth.js +495 -0
- package/dist/lib/google-token.js +66 -0
- package/dist/lib/jwt.js +31 -0
- package/dist/lib/legacy-oauth.js +59 -0
- package/dist/lib/node-version.js +127 -0
- package/dist/lib/owner-plan.js +78 -0
- package/dist/lib/package-scripts.js +152 -0
- package/dist/lib/preflight.js +470 -0
- package/dist/lib/product-create.js +144 -0
- package/dist/lib/run-command.js +74 -0
- package/dist/lib/run-gates.js +70 -0
- package/dist/lib/run-output.js +42 -0
- package/dist/lib/run-summary.js +384 -0
- package/dist/lib/scaffold.js +427 -0
- package/dist/lib/service-input.js +207 -0
- package/dist/lib/service-runner.js +466 -0
- package/dist/lib/stale.js +24 -0
- package/dist/lib/target-selection.js +155 -0
- package/dist/lib/verb-fanout.js +128 -0
- package/dist/lib/verify-live.js +188 -0
- package/dist/manage.js +311 -0
- package/dist/omega-bin.js +7 -0
- package/dist/onboard.js +532 -0
- package/dist/services/account/ensure/users.js +259 -0
- package/dist/services/account/index.js +97 -0
- package/dist/services/account/lib/backend-client.js +107 -0
- package/dist/services/account/lib/password.js +36 -0
- package/dist/services/account/lib/resolve-password.js +100 -0
- package/dist/services/advertising/ensure/sites.js +101 -0
- package/dist/services/advertising/index.js +101 -0
- package/dist/services/advertising/lib/adsense-api.js +41 -0
- package/dist/services/ai/ensure/keys.js +21 -0
- package/dist/services/ai/index.js +36 -0
- package/dist/services/analytics/ensure/google-firebase-link.js +230 -0
- package/dist/services/analytics/ensure/google-streams.js +256 -0
- package/dist/services/analytics/ensure/meta-pixel.js +22 -0
- package/dist/services/analytics/ensure/tiktok-pixel.js +22 -0
- package/dist/services/analytics/index.js +134 -0
- package/dist/services/analytics/lib/analytics-api.js +156 -0
- package/dist/services/analytics/lib/meta-api.js +83 -0
- package/dist/services/analytics/lib/pixel-account.js +78 -0
- package/dist/services/analytics/lib/pixel-provision.js +152 -0
- package/dist/services/analytics/lib/pixel-specs.js +57 -0
- package/dist/services/analytics/lib/pixel-token.js +88 -0
- package/dist/services/analytics/lib/property-flow.js +77 -0
- package/dist/services/analytics/lib/tiktok-api.js +157 -0
- package/dist/services/analytics/lib/tiktok-auth.js +153 -0
- package/dist/services/assets/ensure/logo-gen.js +68 -0
- package/dist/services/assets/index.js +109 -0
- package/dist/services/assets/lib/assets-config.js +168 -0
- package/dist/services/assets/lib/brandmark-api.js +160 -0
- package/dist/services/assets/lib/derived.js +113 -0
- package/dist/services/assets/lib/font-loader.js +67 -0
- package/dist/services/assets/lib/reconcile.js +122 -0
- package/dist/services/assets/lib/reset.js +118 -0
- package/dist/services/assets/lib/svg-logo-generator.js +154 -0
- package/dist/services/assets/lib/svg-to-black.js +32 -0
- package/dist/services/assets/write/favicons.js +82 -0
- package/dist/services/assets/write/icons.js +64 -0
- package/dist/services/assets/write/process.js +96 -0
- package/dist/services/assets/write/reconcile.js +30 -0
- package/dist/services/assets/write/social-icons.js +108 -0
- package/dist/services/assets/write/templates.js +345 -0
- package/dist/services/bookmark/ensure/sync.js +255 -0
- package/dist/services/bookmark/index.js +17 -0
- package/dist/services/campaigns/ensure/contact-person.js +40 -0
- package/dist/services/campaigns/ensure/custom-fields.js +84 -0
- package/dist/services/campaigns/ensure/domain-auth.js +92 -0
- package/dist/services/campaigns/ensure/event-webhook.js +89 -0
- package/dist/services/campaigns/ensure/link-branding.js +187 -0
- package/dist/services/campaigns/ensure/list.js +50 -0
- package/dist/services/campaigns/ensure/segments.js +109 -0
- package/dist/services/campaigns/ensure/sender-identity.js +153 -0
- package/dist/services/campaigns/ensure/unsubscribe-groups.js +116 -0
- package/dist/services/campaigns/index.js +79 -0
- package/dist/services/campaigns/lib/dns-sync.js +81 -0
- package/dist/services/campaigns/lib/segment-query.js +138 -0
- package/dist/services/campaigns/lib/sendgrid-api.js +287 -0
- package/dist/services/captcha/ensure/site-key.js +83 -0
- package/dist/services/captcha/index.js +78 -0
- package/dist/services/captcha/lib/console-url.js +45 -0
- package/dist/services/captcha/lib/recaptcha-api.js +36 -0
- package/dist/services/certificates/ensure/api-key.js +22 -0
- package/dist/services/certificates/ensure/bundle-ids.js +115 -0
- package/dist/services/certificates/ensure/certificates.js +187 -0
- package/dist/services/certificates/ensure/profiles.js +143 -0
- package/dist/services/certificates/index.js +225 -0
- package/dist/services/certificates/lib/apple-api.js +183 -0
- package/dist/services/certificates/lib/certificate-manager.js +252 -0
- package/dist/services/certificates/lib/identifier-manager.js +107 -0
- package/dist/services/certificates/lib/keychain.js +135 -0
- package/dist/services/certificates/lib/manual-walkthrough.js +165 -0
- package/dist/services/certificates/lib/profile-manager.js +115 -0
- package/dist/services/chat/data/baseline-knowledge.md +96 -0
- package/dist/services/chat/ensure/chat.js +106 -0
- package/dist/services/chat/ensure/user.js +45 -0
- package/dist/services/chat/index.js +139 -0
- package/dist/services/chat/lib/baseline-knowledge.js +137 -0
- package/dist/services/cloud/ensure/authentication.js +267 -0
- package/dist/services/cloud/ensure/billing.js +120 -0
- package/dist/services/cloud/ensure/cloud-messaging.js +105 -0
- package/dist/services/cloud/ensure/database.js +60 -0
- package/dist/services/cloud/ensure/firestore.js +79 -0
- package/dist/services/cloud/ensure/functions.js +49 -0
- package/dist/services/cloud/ensure/hosting.js +398 -0
- package/dist/services/cloud/ensure/oauth-consent.js +248 -0
- package/dist/services/cloud/ensure/project-settings.js +72 -0
- package/dist/services/cloud/ensure/sdk-config.js +95 -0
- package/dist/services/cloud/ensure/service-account.js +152 -0
- package/dist/services/cloud/ensure/services.js +103 -0
- package/dist/services/cloud/ensure/storage.js +47 -0
- package/dist/services/cloud/index.js +130 -0
- package/dist/services/cloud/lib/access-heal.js +130 -0
- package/dist/services/cloud/lib/firebase-api.js +662 -0
- package/dist/services/cloud/lib/project-flow.js +102 -0
- package/dist/services/directory/ensure/entry.js +48 -0
- package/dist/services/directory/index.js +64 -0
- package/dist/services/directory/lib/blocks.js +104 -0
- package/dist/services/disperse/index.js +45 -0
- package/dist/services/disperse/write/certs.js +117 -0
- package/dist/services/domain/ensure/nameservers.js +179 -0
- package/dist/services/domain/index.js +69 -0
- package/dist/services/domain/lib/namecheap-api.js +167 -0
- package/dist/services/domain/lib/registrars.js +46 -0
- package/dist/services/domain/lib/whitelist-walkthrough.js +76 -0
- package/dist/services/edge/ensure/cache-rules.js +105 -0
- package/dist/services/edge/ensure/dns-records.js +304 -0
- package/dist/services/edge/ensure/email-routing.js +292 -0
- package/dist/services/edge/ensure/rules-configuration.js +98 -0
- package/dist/services/edge/ensure/rules-managed-transforms.js +101 -0
- package/dist/services/edge/ensure/rules-redirect.js +107 -0
- package/dist/services/edge/ensure/rules-response-headers.js +90 -0
- package/dist/services/edge/ensure/rules-security.js +142 -0
- package/dist/services/edge/ensure/speed-scheduled-tests.js +98 -0
- package/dist/services/edge/ensure/workers.js +219 -0
- package/dist/services/edge/ensure/zone-settings.js +108 -0
- package/dist/services/edge/ensure/zone.js +183 -0
- package/dist/services/edge/index.js +73 -0
- package/dist/services/edge/lib/cloudflare-api.js +84 -0
- package/dist/services/edge/lib/dns-records-helpers.js +518 -0
- package/dist/services/edge/lib/read-cache.js +16 -0
- package/dist/services/edge/lib/ruleset-helper.js +108 -0
- package/dist/services/edge/workers/omega-api-proxy.js +43 -0
- package/dist/services/email/data/baseline-filter.md +4 -0
- package/dist/services/email/data/baseline-knowledge.md +86 -0
- package/dist/services/email/ensure/agent.js +107 -0
- package/dist/services/email/ensure/user.js +45 -0
- package/dist/services/email/index.js +138 -0
- package/dist/services/email/lib/baseline.js +82 -0
- package/dist/services/email/lib/knowledge-file.js +53 -0
- package/dist/services/forms/ensure/form.js +46 -0
- package/dist/services/forms/ensure/user.js +45 -0
- package/dist/services/forms/index.js +138 -0
- package/dist/services/migrations/ensure/notifications.js +189 -0
- package/dist/services/migrations/ensure/orders.js +34 -0
- package/dist/services/migrations/ensure/payment-provider.js +132 -0
- package/dist/services/migrations/ensure/payments-intents.js +34 -0
- package/dist/services/migrations/ensure/state-retirement.js +266 -0
- package/dist/services/migrations/ensure/targets-rename.js +124 -0
- package/dist/services/migrations/ensure/users.js +1176 -0
- package/dist/services/migrations/index.js +101 -0
- package/dist/services/migrations/lib/attribution-touch.js +64 -0
- package/dist/services/migrations/lib/ensure-metadata.js +144 -0
- package/dist/services/migrations/lib/migration-runner.js +473 -0
- package/dist/services/migrations/lib/sanitize-strings.js +112 -0
- package/dist/services/migrations/lib/schema-validator.js +154 -0
- package/dist/services/monitoring/ensure/dsn.js +54 -0
- package/dist/services/monitoring/ensure/projects.js +135 -0
- package/dist/services/monitoring/index.js +54 -0
- package/dist/services/monitoring/lib/sentry-api.js +94 -0
- package/dist/services/newsletter/ensure/custom-fields.js +90 -0
- package/dist/services/newsletter/ensure/publication.js +94 -0
- package/dist/services/newsletter/ensure/segments.js +140 -0
- package/dist/services/newsletter/ensure/webhook.js +108 -0
- package/dist/services/newsletter/index.js +64 -0
- package/dist/services/newsletter/lib/beehiiv-api.js +124 -0
- package/dist/services/newsletter/lib/segment-automation.js +577 -0
- package/dist/services/payment/ensure/chargebee-account.js +30 -0
- package/dist/services/payment/ensure/chargebee-products.js +359 -0
- package/dist/services/payment/ensure/chargebee-webhook.js +113 -0
- package/dist/services/payment/ensure/paypal-account.js +36 -0
- package/dist/services/payment/ensure/paypal-products.js +374 -0
- package/dist/services/payment/ensure/paypal-webhook.js +128 -0
- package/dist/services/payment/ensure/stripe-account.js +93 -0
- package/dist/services/payment/ensure/stripe-disputes.js +56 -0
- package/dist/services/payment/ensure/stripe-products.js +245 -0
- package/dist/services/payment/ensure/stripe-radar.js +74 -0
- package/dist/services/payment/ensure/stripe-webhook.js +130 -0
- package/dist/services/payment/index.js +163 -0
- package/dist/services/payment/lib/chargebee-api.js +338 -0
- package/dist/services/payment/lib/payment-utils.js +116 -0
- package/dist/services/payment/lib/paypal-api.js +354 -0
- package/dist/services/payment/lib/provider-setup.js +134 -0
- package/dist/services/payment/lib/stripe-api.js +152 -0
- package/dist/services/repo/ensure/org.js +70 -0
- package/dist/services/repo/ensure/pages.js +72 -0
- package/dist/services/repo/ensure/repo.js +90 -0
- package/dist/services/repo/index.js +52 -0
- package/dist/services/repo/lib/github-api.js +196 -0
- package/dist/services/search/ensure/ga-link.js +57 -0
- package/dist/services/search/ensure/property.js +137 -0
- package/dist/services/search/ensure/sitemaps.js +70 -0
- package/dist/services/search/index.js +61 -0
- package/dist/services/search/lib/search-console-api.js +92 -0
- package/dist/services/seo/ensure/github-repos.js +271 -0
- package/dist/services/seo/index.js +55 -0
- package/dist/services/seo/lib/gh-api.js +161 -0
- package/dist/services/seo/templates/developer-tool/.github/workflows/maintenance.yml +25 -0
- package/dist/services/seo/templates/developer-tool/.nvmrc +1 -0
- package/dist/services/seo/templates/developer-tool/_README.md.js +121 -0
- package/dist/services/seo/templates/developer-tool/_package.json.js +33 -0
- package/dist/services/seo/templates/developer-tool/src/index.js +120 -0
- package/dist/services/seo/templates/index.js +57 -0
- package/dist/services/server/ensure/brands.js +63 -0
- package/dist/services/server/index.js +42 -0
- package/dist/services/testing/ensure/target-checks.js +97 -0
- package/dist/services/testing/index.js +11 -0
- package/dist/services/testing/lib/checks.js +467 -0
- package/dist/services/update/index.js +20 -0
- package/dist/services/update/lib/cache.js +59 -0
- package/dist/services/update/lib/fingerprint.js +157 -0
- package/dist/services/update/write/targets.js +206 -0
- package/dist/services/workspace/ensure/agents.js +42 -0
- package/dist/services/workspace/ensure/claude-settings.js +34 -0
- package/dist/services/workspace/ensure/config.js +54 -0
- package/dist/services/workspace/ensure/defaults.js +60 -0
- package/dist/services/workspace/ensure/env-keys.js +55 -0
- package/dist/services/workspace/ensure/env-order.js +66 -0
- package/dist/services/workspace/ensure/env-rules.js +97 -0
- package/dist/services/workspace/ensure/gitignore.js +18 -0
- package/dist/services/workspace/ensure/scripts.js +126 -0
- package/dist/services/workspace/ensure/structure.js +97 -0
- package/dist/services/workspace/ensure/translation-sdk.js +148 -0
- package/dist/services/workspace/ensure/workflows.js +54 -0
- package/dist/services/workspace/index.js +8 -0
- package/dist/vendor/account/engine.js +182 -0
- package/dist/vendor/account/features.js +220 -0
- package/dist/vendor/account/index.js +53 -0
- package/dist/vendor/account/schema.js +272 -0
- package/dist/vendor/account/subscription.js +38 -0
- package/dist/vendor/config/company.js +31 -0
- package/dist/vendor/config/defaults.js +173 -0
- package/dist/vendor/config/demo.js +18 -0
- package/dist/vendor/config/desktop-artifacts.js +110 -0
- package/dist/vendor/config/edit.js +769 -0
- package/dist/vendor/config/env-delivery.js +145 -0
- package/dist/vendor/config/env-rules.js +93 -0
- package/dist/vendor/config/env-schema.js +1078 -0
- package/dist/vendor/config/env.js +445 -0
- package/dist/vendor/config/hooks.js +97 -0
- package/dist/vendor/config/index.js +237 -0
- package/dist/vendor/config/instances.js +208 -0
- package/dist/vendor/config/load.js +490 -0
- package/dist/vendor/config/merge.js +68 -0
- package/dist/vendor/config/order.js +139 -0
- package/dist/vendor/config/ports.js +374 -0
- package/dist/vendor/config/providers.js +32 -0
- package/dist/vendor/config/repo.js +142 -0
- package/dist/vendor/config/retired-keys.js +430 -0
- package/dist/vendor/config/schema.js +1610 -0
- package/dist/vendor/config/secrets.js +50 -0
- package/dist/vendor/config/seed.js +34 -0
- package/dist/vendor/config/site-global.js +205 -0
- package/dist/vendor/config/validate.js +554 -0
- package/dist/vendor/config/winback.js +61 -0
- package/dist/vendor/devkit/attach-log-file.js +262 -0
- package/dist/vendor/devkit/certs.js +199 -0
- package/dist/vendor/devkit/ci-workflows.js +520 -0
- package/dist/vendor/devkit/cli-router.js +156 -0
- package/dist/vendor/devkit/command-path.js +46 -0
- package/dist/vendor/devkit/deploy-record.js +180 -0
- package/dist/vendor/devkit/flows.js +317 -0
- package/dist/vendor/devkit/local-https.js +360 -0
- package/dist/vendor/devkit/local.js +1905 -0
- package/dist/vendor/devkit/logger.js +128 -0
- package/dist/vendor/devkit/omega-bin.js +345 -0
- package/dist/vendor/devkit/prompt.js +187 -0
- package/dist/vendor/devkit/safe-install.js +18 -0
- package/dist/vendor/devkit/stop-signals.js +28 -0
- package/dist/vendor/devkit/test/scope.js +162 -0
- package/dist/vendor/devkit/translate/cache.js +84 -0
- package/dist/vendor/devkit/translate/engine.js +243 -0
- package/dist/vendor/devkit/translate/index.js +49 -0
- package/dist/vendor/devkit/translate/languages.js +134 -0
- package/dist/vendor/devkit/translate/providers.js +188 -0
- package/dist/vendor/devkit/update.js +569 -0
- package/docs/AGENTS.md +200 -0
- package/docs/account.md +45 -0
- package/docs/advertising.md +46 -0
- package/docs/ai.md +34 -0
- package/docs/analytics.md +69 -0
- package/docs/assets.md +57 -0
- package/docs/bookmark.md +26 -0
- package/docs/brand.md +151 -0
- package/docs/campaigns.md +96 -0
- package/docs/captcha.md +46 -0
- package/docs/certificates.md +68 -0
- package/docs/chat.md +47 -0
- package/docs/cloud.md +110 -0
- package/docs/company.md +64 -0
- package/docs/directory.md +140 -0
- package/docs/disperse.md +46 -0
- package/docs/domain.md +56 -0
- package/docs/edge.md +234 -0
- package/docs/email.md +42 -0
- package/docs/forms.md +48 -0
- package/docs/index.md +109 -0
- package/docs/migration.md +203 -0
- package/docs/migrations.md +56 -0
- package/docs/monitoring.md +41 -0
- package/docs/newsletter.md +48 -0
- package/docs/payment.md +83 -0
- package/docs/repo.md +48 -0
- package/docs/search.md +44 -0
- package/docs/seo.md +34 -0
- package/docs/server.md +37 -0
- package/docs/shared/agent-docs.md +89 -0
- package/docs/shared/analytics.md +612 -0
- package/docs/shared/brands.md +51 -0
- package/docs/shared/breaking-changes.md +497 -0
- package/docs/shared/config.md +1387 -0
- package/docs/shared/deploys.md +215 -0
- package/docs/shared/icons.md +201 -0
- package/docs/shared/local-dev.md +147 -0
- package/docs/shared/logging.md +202 -0
- package/docs/shared/monitoring.md +153 -0
- package/docs/shared/publishing.md +183 -0
- package/docs/shared/rulings.md +34 -0
- package/docs/shared/testing.md +147 -0
- package/docs/shared/theming.md +604 -0
- package/docs/shared/translation.md +291 -0
- package/docs/shared/updates.md +61 -0
- package/docs/testing.md +34 -0
- package/docs/update.md +36 -0
- package/docs/workspace.md +64 -0
- package/package.json +88 -0
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ensure PayPal catalog products and billing plans exist for all paid
|
|
3
|
+
* products.
|
|
4
|
+
*
|
|
5
|
+
* Product resolution: config productId → state (paypalProducts map) →
|
|
6
|
+
* exact-name match against the catalog (self-heal when state is lost) →
|
|
7
|
+
* create. Created/matched IDs are written back into omega.json5
|
|
8
|
+
* (payment.products[id=…].paypal.productId — comment-preserving) and
|
|
9
|
+
* mirrored in state. Plans are managed by interval + amount + trial-cycle
|
|
10
|
+
* presence — matching active plans are kept, duplicates and stale plans
|
|
11
|
+
* deactivated, missing ones created; a product with trial days carries a TWIN
|
|
12
|
+
* pair per interval (with the trial cycle, and without it) so a buyer who is
|
|
13
|
+
* not owed a trial has a plan to land on. Legacy products (product.paypal.legacyProductIds)
|
|
14
|
+
* get their active plans deactivated so no new subscriptions land on them.
|
|
15
|
+
*
|
|
16
|
+
* In sandbox mode the ensured products are the payments-QA fixtures, so the
|
|
17
|
+
* step closes with the sandbox-buyer reminder (see below).
|
|
18
|
+
*/
|
|
19
|
+
const chalk = require('chalk').default;
|
|
20
|
+
const { paidProducts, productDisplayName, productImage } = require('../lib/payment-utils.js');
|
|
21
|
+
const { writeBrandConfig } = require('../../../lib/config-write.js');
|
|
22
|
+
|
|
23
|
+
// Map config interval names to PayPal interval units
|
|
24
|
+
const FREQUENCY_TO_INTERVAL = {
|
|
25
|
+
annually: 'YEAR',
|
|
26
|
+
monthly: 'MONTH',
|
|
27
|
+
weekly: 'WEEK',
|
|
28
|
+
daily: 'DAY',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// PayPal Developer Dashboard → Sandbox → Accounts (the only surface that mints
|
|
32
|
+
// a sandbox buyer) and the drive runbook that documents the hoops
|
|
33
|
+
const SANDBOX_ACCOUNTS_URL = 'https://developer.paypal.com/dashboard/accounts';
|
|
34
|
+
const SANDBOX_QA_RUNBOOK = '@omega.js/backend/docs/paypal-sandbox-qa.md';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Build desired catalog-product details from brand/product config
|
|
38
|
+
*/
|
|
39
|
+
function buildProductDetails(brandConfig, product) {
|
|
40
|
+
return {
|
|
41
|
+
name: productDisplayName(brandConfig, product),
|
|
42
|
+
description: (product.description || brandConfig.brand.description || '').slice(0, 256),
|
|
43
|
+
imageUrl: productImage(brandConfig),
|
|
44
|
+
homeUrl: brandConfig.brand.url,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Diff a PayPal catalog product against desired and return JSON Patch ops.
|
|
50
|
+
* Images are only managed when brand.images.brandmark is configured.
|
|
51
|
+
*/
|
|
52
|
+
function getProductUpdates(paypalProduct, desired) {
|
|
53
|
+
const patches = [];
|
|
54
|
+
|
|
55
|
+
if (paypalProduct.name !== desired.name) {
|
|
56
|
+
patches.push({ op: 'replace', path: '/name', value: desired.name });
|
|
57
|
+
}
|
|
58
|
+
if ((paypalProduct.description || '') !== desired.description) {
|
|
59
|
+
patches.push({ op: 'replace', path: '/description', value: desired.description });
|
|
60
|
+
}
|
|
61
|
+
if (desired.imageUrl && paypalProduct.image_url !== desired.imageUrl) {
|
|
62
|
+
patches.push({ op: 'replace', path: '/image_url', value: desired.imageUrl });
|
|
63
|
+
}
|
|
64
|
+
if (paypalProduct.home_url !== desired.homeUrl) {
|
|
65
|
+
patches.push({ op: 'replace', path: '/home_url', value: desired.homeUrl });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return patches.length > 0 ? patches : null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Check if a plan's billing cycles match the expected config.
|
|
73
|
+
* Matches on: interval unit, amount, and trial days.
|
|
74
|
+
*
|
|
75
|
+
* `expectedTrialDays` carries the twin dimension: a positive value matches the
|
|
76
|
+
* trial twin (a TRIAL cycle of exactly that many days ahead of the REGULAR
|
|
77
|
+
* one), 0 matches the skip-trial twin (a REGULAR cycle and nothing else), so
|
|
78
|
+
* neither twin ever matches the other's row ([#761]).
|
|
79
|
+
*/
|
|
80
|
+
function isPlanMatch(plan, intervalUnit, expectedAmount, expectedTrialDays) {
|
|
81
|
+
if (plan.status !== 'ACTIVE') {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const regularCycle = plan.billing_cycles?.find((c) => c.tenure_type === 'REGULAR');
|
|
86
|
+
if (!regularCycle) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (regularCycle.frequency?.interval_unit !== intervalUnit) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const planAmount = parseFloat(regularCycle.pricing_scheme?.fixed_price?.value || '0');
|
|
95
|
+
if (planAmount !== expectedAmount) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const trialCycle = plan.billing_cycles?.find((c) => c.tenure_type === 'TRIAL');
|
|
100
|
+
if (expectedTrialDays > 0) {
|
|
101
|
+
if (!trialCycle || trialCycle.total_cycles !== expectedTrialDays) {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
} else if (trialCycle) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Ensure billing plans on a PayPal product match config.
|
|
113
|
+
* Creates new plans if missing, deactivates duplicates and stale plans.
|
|
114
|
+
*
|
|
115
|
+
* A product that configures trial days carries TWIN plans per paid interval:
|
|
116
|
+
* the trial twin (TRIAL cycle + REGULAR) and the skip-trial twin (REGULAR
|
|
117
|
+
* only). PayPal puts trials on the PLAN, never on the subscribe call, so a
|
|
118
|
+
* returning buyer who is not owed a trial has to subscribe to a plan that has
|
|
119
|
+
* no free cycle on it at all — the backend picks the twin by the same
|
|
120
|
+
* interval + amount + trial-presence rule this walk matches on
|
|
121
|
+
* ([#761](https://github.com/Omega-JS-Stack/omega/issues/761)). A product
|
|
122
|
+
* without trial days carries one plan, as before. Neither twin is a duplicate
|
|
123
|
+
* or a stale copy of the other, so a rerun creates and deactivates nothing.
|
|
124
|
+
*/
|
|
125
|
+
async function ensurePlans(api, paypalProductId, product, brandConfig, dryRun) {
|
|
126
|
+
const plans = await api.listPlansForProduct(paypalProductId);
|
|
127
|
+
const trialDays = product.trial?.days || 0;
|
|
128
|
+
|
|
129
|
+
// The twins to ensure, in dashboard order: with a trial cycle, then without
|
|
130
|
+
const twins = trialDays > 0 ? [trialDays, 0] : [0];
|
|
131
|
+
|
|
132
|
+
// Only a twinned product has plans to tell apart — a single-plan product's
|
|
133
|
+
// names and log lines stay exactly as they were
|
|
134
|
+
const isSkipTrialTwin = (twinTrialDays) => trialDays > 0 && twinTrialDays === 0;
|
|
135
|
+
const twinLabel = (twinTrialDays) => {
|
|
136
|
+
if (trialDays === 0) {
|
|
137
|
+
return '';
|
|
138
|
+
}
|
|
139
|
+
return isSkipTrialTwin(twinTrialDays) ? ' (no trial)' : ' (trial)';
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
for (const interval of Object.keys(product.prices)) {
|
|
143
|
+
const expectedAmount = product.prices[interval];
|
|
144
|
+
|
|
145
|
+
if (!expectedAmount) {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const intervalUnit = FREQUENCY_TO_INTERVAL[interval];
|
|
150
|
+
|
|
151
|
+
// Skip 'once' — one-time products don't have plans
|
|
152
|
+
if (!intervalUnit) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Every active plan on this interval, split ONCE across the twins: a keeper
|
|
157
|
+
// per twin, its extra matches are duplicates, and what no twin claims is
|
|
158
|
+
// stale (splitting per twin instead would read one twin as the other's
|
|
159
|
+
// stale plan and deactivate it every run)
|
|
160
|
+
const intervalPlans = plans.filter((plan) => {
|
|
161
|
+
if (plan.status !== 'ACTIVE') {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
const regularCycle = plan.billing_cycles?.find((c) => c.tenure_type === 'REGULAR');
|
|
165
|
+
return regularCycle?.frequency?.interval_unit === intervalUnit;
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const claimed = new Set();
|
|
169
|
+
const duplicates = [];
|
|
170
|
+
const missing = [];
|
|
171
|
+
|
|
172
|
+
for (const twinTrialDays of twins) {
|
|
173
|
+
const matches = intervalPlans.filter((plan) => isPlanMatch(plan, intervalUnit, expectedAmount, twinTrialDays));
|
|
174
|
+
|
|
175
|
+
for (const plan of matches) {
|
|
176
|
+
claimed.add(plan);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (matches.length === 0) {
|
|
180
|
+
missing.push(twinTrialDays);
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
console.log(` ${chalk.green('✓')} ${interval}${twinLabel(twinTrialDays)}: $${expectedAmount} ${chalk.dim(matches[0].id)}`);
|
|
185
|
+
duplicates.push(...matches.slice(1));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const stale = intervalPlans.filter((plan) => !claimed.has(plan));
|
|
189
|
+
|
|
190
|
+
for (const dup of duplicates) {
|
|
191
|
+
if (dryRun) {
|
|
192
|
+
console.log(` ${chalk.yellow('↓')} ${interval}: would deactivate duplicate ${chalk.dim(dup.id)} ${chalk.yellow('[DRY RUN]')}`);
|
|
193
|
+
} else {
|
|
194
|
+
await api.deactivatePlan(dup.id);
|
|
195
|
+
console.log(` ${chalk.yellow('↓')} ${interval}: deactivated duplicate ${chalk.dim(dup.id)}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
for (const old of stale) {
|
|
200
|
+
if (dryRun) {
|
|
201
|
+
console.log(` ${chalk.yellow('↓')} ${interval}: would deactivate ${chalk.dim(old.id)} ${chalk.yellow('[DRY RUN]')}`);
|
|
202
|
+
} else {
|
|
203
|
+
await api.deactivatePlan(old.id);
|
|
204
|
+
console.log(` ${chalk.yellow('↓')} ${interval}: deactivated ${chalk.dim(old.id)}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Create the twins nothing matched
|
|
209
|
+
for (const twinTrialDays of missing) {
|
|
210
|
+
const intervalName = interval.charAt(0).toUpperCase() + interval.slice(1);
|
|
211
|
+
const planName = `${productDisplayName(brandConfig, product)} (${intervalName}${isSkipTrialTwin(twinTrialDays) ? ', no trial' : ''})`;
|
|
212
|
+
|
|
213
|
+
if (dryRun) {
|
|
214
|
+
console.log(` ${chalk.cyan('+')} ${interval}: would create $${expectedAmount} plan "${planName}" ${chalk.yellow('[DRY RUN]')}`);
|
|
215
|
+
} else {
|
|
216
|
+
const newPlan = await api.createPlan({
|
|
217
|
+
productId: paypalProductId,
|
|
218
|
+
name: planName,
|
|
219
|
+
interval,
|
|
220
|
+
amount: expectedAmount,
|
|
221
|
+
trialDays: twinTrialDays,
|
|
222
|
+
});
|
|
223
|
+
console.log(` ${chalk.green('✓')} ${interval}${twinLabel(twinTrialDays)}: created $${expectedAmount} ${chalk.cyan(newPlan.id)}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Deactivate active plans on legacy PayPal products
|
|
231
|
+
* (product.paypal.legacyProductIds). Existing subscribers keep their plans;
|
|
232
|
+
* no new subscriptions can be created on them.
|
|
233
|
+
*/
|
|
234
|
+
async function deactivateLegacyPlans(api, products, dryRun) {
|
|
235
|
+
const legacyIds = new Set();
|
|
236
|
+
for (const product of products) {
|
|
237
|
+
for (const id of product.paypal?.legacyProductIds || []) {
|
|
238
|
+
legacyIds.add(id);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (legacyIds.size === 0) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
console.log(` ${chalk.dim('Legacy PayPal products:')}`);
|
|
247
|
+
|
|
248
|
+
for (const legacyProductId of legacyIds) {
|
|
249
|
+
const plans = await api.listPlansForProduct(legacyProductId);
|
|
250
|
+
const activePlans = plans.filter((p) => p.status === 'ACTIVE');
|
|
251
|
+
|
|
252
|
+
if (activePlans.length === 0) {
|
|
253
|
+
console.log(` ${chalk.green('✓')} ${chalk.dim(legacyProductId)}: no active plans`);
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
for (const plan of activePlans) {
|
|
258
|
+
if (dryRun) {
|
|
259
|
+
console.log(` ${chalk.yellow('↓')} ${chalk.dim(legacyProductId)}: would deactivate ${plan.name} ${chalk.dim(plan.id)} ${chalk.yellow('[DRY RUN]')}`);
|
|
260
|
+
} else {
|
|
261
|
+
await api.deactivatePlan(plan.id);
|
|
262
|
+
console.log(` ${chalk.yellow('↓')} ${chalk.dim(legacyProductId)}: deactivated ${plan.name} ${chalk.dim(plan.id)}`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Sandbox only: the fixtures just ensured can be bought only by a sandbox
|
|
270
|
+
* BUYER account, and PayPal exposes no API to mint one (probed 2026-08-18,
|
|
271
|
+
* #348 — the app-credential token carries no sandbox-account scope), so the
|
|
272
|
+
* walk reminds the operator instead of precreating it. Live runs say nothing.
|
|
273
|
+
*/
|
|
274
|
+
function remindSandboxBuyer(api) {
|
|
275
|
+
if (api.getAccountInfo().environment !== 'sandbox') {
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
console.log(` ${chalk.yellow('⚠')} Sandbox — buying the QA fixture (proof-press) through guest checkout needs a sandbox BUYER account (no API mints one)`);
|
|
280
|
+
console.log(` ${chalk.dim('→')} Sandbox → Accounts: ${chalk.cyan(SANDBOX_ACCOUNTS_URL)} ${chalk.dim(`· runbook: ${SANDBOX_QA_RUNBOOK}`)}`);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
module.exports = async function ensurePayPalProducts(context) {
|
|
284
|
+
const { brandConfig, paypalApi: api, options } = context;
|
|
285
|
+
const dryRun = options?.dryRun || false;
|
|
286
|
+
|
|
287
|
+
if (!api) {
|
|
288
|
+
console.log(` ${chalk.dim('⊘ PayPal not configured')}`);
|
|
289
|
+
return {};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (dryRun) {
|
|
293
|
+
console.log(` ${chalk.yellow('[DRY RUN]')} No changes will be made`);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const products = paidProducts(brandConfig);
|
|
297
|
+
const configEdits = {};
|
|
298
|
+
let catalog = null; // Lazy-listed only when a product has no configured ID
|
|
299
|
+
|
|
300
|
+
for (const product of products) {
|
|
301
|
+
console.log(` ${chalk.bold(product.name)} (${product.id}):`);
|
|
302
|
+
|
|
303
|
+
const desired = buildProductDetails(brandConfig, product);
|
|
304
|
+
|
|
305
|
+
// --- Resolve the PayPal catalog product: config → name match → create ---
|
|
306
|
+
let paypalId = product.paypal?.productId || null;
|
|
307
|
+
let paypalProduct = null;
|
|
308
|
+
|
|
309
|
+
if (!paypalId) {
|
|
310
|
+
// Self-heal: the catalog list is summarized, so a name match still
|
|
311
|
+
// needs a full fetch before diffing
|
|
312
|
+
catalog = catalog || await api.listProducts();
|
|
313
|
+
const match = catalog.find((p) => p.name === desired.name);
|
|
314
|
+
if (match) {
|
|
315
|
+
paypalId = match.id;
|
|
316
|
+
console.log(` ${chalk.green('✓')} Matched existing product by name: ${chalk.dim(match.id)}`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (paypalId) {
|
|
321
|
+
paypalProduct = await api.getProduct(paypalId);
|
|
322
|
+
console.log(` ${chalk.green('✓')} Product exists: ${chalk.dim(paypalProduct.id)}`);
|
|
323
|
+
|
|
324
|
+
const patches = getProductUpdates(paypalProduct, desired);
|
|
325
|
+
if (patches) {
|
|
326
|
+
const fields = patches.map((p) => p.path.slice(1)).join(', ');
|
|
327
|
+
if (dryRun) {
|
|
328
|
+
console.log(` ${chalk.cyan('~')} Would update: ${chalk.dim(fields)} ${chalk.yellow('[DRY RUN]')}`);
|
|
329
|
+
} else {
|
|
330
|
+
await api.updateProduct(paypalProduct.id, patches);
|
|
331
|
+
console.log(` ${chalk.green('✓')} Updated: ${chalk.dim(fields)}`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
} else {
|
|
335
|
+
if (dryRun) {
|
|
336
|
+
console.log(` ${chalk.cyan('+')} Would create product: ${chalk.dim(desired.name)} ${chalk.yellow('[DRY RUN]')}`);
|
|
337
|
+
// Can't manage plans without a real product ID
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
paypalProduct = await api.createProduct({
|
|
342
|
+
name: desired.name,
|
|
343
|
+
description: desired.description,
|
|
344
|
+
type: product.type === 'subscription' ? 'SERVICE' : 'DIGITAL',
|
|
345
|
+
imageUrl: desired.imageUrl,
|
|
346
|
+
homeUrl: desired.homeUrl,
|
|
347
|
+
});
|
|
348
|
+
console.log(` ${chalk.green('✓')} Created product: ${chalk.cyan(paypalProduct.id)}`);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (paypalProduct.id !== product.paypal?.productId) {
|
|
352
|
+
configEdits[`payment.products[id=${product.id}].paypal.productId`] = paypalProduct.id;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// --- Ensure billing plans match config (subscriptions only) ---
|
|
356
|
+
if (product.type === 'subscription') {
|
|
357
|
+
await ensurePlans(api, paypalProduct.id, product, brandConfig, dryRun);
|
|
358
|
+
} else {
|
|
359
|
+
console.log(` ${chalk.dim('⊘ One-time — no plans needed (Orders API)')}`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (Object.keys(configEdits).length > 0) {
|
|
364
|
+
writeBrandConfig(context, configEdits);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// --- Deactivate plans on legacy products ---
|
|
368
|
+
await deactivateLegacyPlans(api, products, dryRun);
|
|
369
|
+
|
|
370
|
+
// --- The fixtures exist; a sandbox drive still needs a buyer ---
|
|
371
|
+
remindSandboxBuyer(api);
|
|
372
|
+
|
|
373
|
+
return { output: { paypalSync: { productsProcessed: products.length } } };
|
|
374
|
+
};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ensure a PayPal webhook endpoint exists for this brand's backend.
|
|
3
|
+
*
|
|
4
|
+
* Matches by exact URL, deletes every OTHER endpoint on the brand's API host
|
|
5
|
+
* (stale twins — one endpoint per host, and endpoints on other hosts are never
|
|
6
|
+
* touched), diffs event_types, PATCHes when drifted, creates when missing.
|
|
7
|
+
* Skipped for shared Firebase projects (no api.{domain} backend to receive
|
|
8
|
+
* events). Requires OMEGA_WEBHOOK_KEY.
|
|
9
|
+
*/
|
|
10
|
+
const chalk = require('chalk').default;
|
|
11
|
+
const { buildWebhookUrl, diffEventSets, redactWebhookUrl, staleWebhookEndpoints } = require('../lib/payment-utils.js');
|
|
12
|
+
|
|
13
|
+
// Events the backend handles for PayPal subscription/payment processing
|
|
14
|
+
const ENABLED_EVENTS = [
|
|
15
|
+
// Subscriptions
|
|
16
|
+
'BILLING.SUBSCRIPTION.CREATED',
|
|
17
|
+
'BILLING.SUBSCRIPTION.ACTIVATED',
|
|
18
|
+
'BILLING.SUBSCRIPTION.UPDATED',
|
|
19
|
+
'BILLING.SUBSCRIPTION.CANCELLED',
|
|
20
|
+
'BILLING.SUBSCRIPTION.SUSPENDED',
|
|
21
|
+
'BILLING.SUBSCRIPTION.EXPIRED',
|
|
22
|
+
'BILLING.SUBSCRIPTION.PAYMENT.FAILED',
|
|
23
|
+
'BILLING.SUBSCRIPTION.RE-ACTIVATED',
|
|
24
|
+
|
|
25
|
+
// One-time purchases (Orders/Checkout)
|
|
26
|
+
'CHECKOUT.ORDER.COMPLETED',
|
|
27
|
+
'CHECKOUT.ORDER.APPROVED',
|
|
28
|
+
|
|
29
|
+
// Payment captures and sales
|
|
30
|
+
'PAYMENT.SALE.COMPLETED',
|
|
31
|
+
'PAYMENT.SALE.DENIED',
|
|
32
|
+
'PAYMENT.SALE.REFUNDED',
|
|
33
|
+
'PAYMENT.CAPTURE.COMPLETED',
|
|
34
|
+
'PAYMENT.CAPTURE.DENIED',
|
|
35
|
+
'PAYMENT.CAPTURE.REFUNDED',
|
|
36
|
+
|
|
37
|
+
// Invoices
|
|
38
|
+
'INVOICING.INVOICE.CREATED',
|
|
39
|
+
'INVOICING.INVOICE.PAID',
|
|
40
|
+
'INVOICING.INVOICE.CANCELLED',
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
module.exports = async function ensurePayPalWebhook(context) {
|
|
44
|
+
const { brandConfig, paypalApi: api, options } = context;
|
|
45
|
+
const dryRun = options?.dryRun || false;
|
|
46
|
+
|
|
47
|
+
if (!api) {
|
|
48
|
+
console.log(` ${chalk.dim('⊘ PayPal not configured')}`);
|
|
49
|
+
return {};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Shared Firebase project — no backend deploys from this brand, so api.{domain} doesn't exist
|
|
53
|
+
if (brandConfig.cloud?.shared === true) {
|
|
54
|
+
console.log(` ${chalk.dim('⊘ Shared Firebase project — no brand API for webhooks')}`);
|
|
55
|
+
return {};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!process.env.OMEGA_WEBHOOK_KEY) {
|
|
59
|
+
// Unreachable by design (#635): the key is OMEGA's own (`generated:` in
|
|
60
|
+
// the env schema) and the payment service's setup mints it through the
|
|
61
|
+
// shared contract before any operation runs. Absent here = broken wiring.
|
|
62
|
+
throw new Error('OMEGA_WEBHOOK_KEY absent after the payment setup ran — its REQUIRES entry or its requestServiceInput call is missing');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const desiredUrl = buildWebhookUrl(brandConfig, 'paypal');
|
|
66
|
+
|
|
67
|
+
const { webhooks = [] } = await api.listWebhooks();
|
|
68
|
+
|
|
69
|
+
// Exactly ONE endpoint on the brand's API host: the desired URL. Legacy twins
|
|
70
|
+
// there receive every event and answer 400 forever (#570).
|
|
71
|
+
for (const stale of staleWebhookEndpoints(webhooks, desiredUrl)) {
|
|
72
|
+
if (dryRun) {
|
|
73
|
+
console.log(` ${chalk.cyan('-')} Would delete stale webhook ${chalk.dim(redactWebhookUrl(stale.url))} ${chalk.yellow('[DRY RUN]')}`);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
await api.deleteWebhook(stale.id);
|
|
78
|
+
console.log(` ${chalk.yellow('⚠')} Deleted stale webhook ${chalk.dim(`(${stale.id})`)} ${redactWebhookUrl(stale.url)}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const existing = webhooks.find((wh) => wh.url === desiredUrl);
|
|
82
|
+
|
|
83
|
+
if (existing) {
|
|
84
|
+
const currentEvents = (existing.event_types || []).map((e) => e.name || e);
|
|
85
|
+
const diff = diffEventSets(currentEvents, ENABLED_EVENTS);
|
|
86
|
+
|
|
87
|
+
if (!diff) {
|
|
88
|
+
console.log(` ${chalk.green('✓')} Webhook up to date ${chalk.dim(`(${existing.id})`)}`);
|
|
89
|
+
return { output: { paypalWebhook: { id: existing.id, updated: false } } };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (diff.missing.length > 0) {
|
|
93
|
+
console.log(` Adding events: ${chalk.cyan(diff.missing.join(', '))}`);
|
|
94
|
+
}
|
|
95
|
+
if (diff.extra.length > 0) {
|
|
96
|
+
console.log(` Removing events: ${chalk.dim(diff.extra.join(', '))}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (dryRun) {
|
|
100
|
+
console.log(` ${chalk.cyan('~')} Would update webhook events ${chalk.yellow('[DRY RUN]')}`);
|
|
101
|
+
return { output: { paypalWebhook: { id: existing.id, planned: 'update' } } };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// PayPal webhook update uses JSON Patch to replace event_types
|
|
105
|
+
await api.updateWebhook(existing.id, [
|
|
106
|
+
{
|
|
107
|
+
op: 'replace',
|
|
108
|
+
path: '/event_types',
|
|
109
|
+
value: ENABLED_EVENTS.map((name) => ({ name })),
|
|
110
|
+
},
|
|
111
|
+
]);
|
|
112
|
+
console.log(` ${chalk.green('✓')} Webhook events updated ${chalk.dim(`(${existing.id})`)}`);
|
|
113
|
+
|
|
114
|
+
return { output: { paypalWebhook: { id: existing.id, updated: true } } };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (dryRun) {
|
|
118
|
+
console.log(` ${chalk.cyan('+')} Would create webhook ${chalk.yellow('[DRY RUN]')}`);
|
|
119
|
+
console.log(` ${chalk.dim(redactWebhookUrl(desiredUrl))}`);
|
|
120
|
+
return { output: { paypalWebhook: { planned: 'create' } } };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const webhook = await api.createWebhook(desiredUrl, ENABLED_EVENTS);
|
|
124
|
+
console.log(` ${chalk.green('✓')} Webhook created: ${chalk.dim(webhook.id)}`);
|
|
125
|
+
console.log(` ${chalk.dim(redactWebhookUrl(desiredUrl))}`);
|
|
126
|
+
|
|
127
|
+
return { output: { paypalWebhook: { id: webhook.id, created: true } } };
|
|
128
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ensure the Stripe account's business profile matches brand config
|
|
3
|
+
* (name, url, support email, support url). Diff-then-update; account
|
|
4
|
+
* updates aren't supported on every account type (org sub-accounts), so an
|
|
5
|
+
* update failure warns instead of erroring. Disable entirely with
|
|
6
|
+
* payment.providers.stripe.updateAccountInfo = false.
|
|
7
|
+
*/
|
|
8
|
+
const chalk = require('chalk').default;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Build desired account settings from brand config
|
|
12
|
+
*/
|
|
13
|
+
function buildDesiredAccount(brandConfig) {
|
|
14
|
+
const domain = brandConfig.brand.url.replace(/^https?:\/\//, '');
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
business_profile: {
|
|
18
|
+
name: brandConfig.brand.name,
|
|
19
|
+
url: brandConfig.brand.url,
|
|
20
|
+
support_email: brandConfig.brand.contact?.email || `support@${domain}`,
|
|
21
|
+
support_url: brandConfig.brand.url,
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Diff current account against desired and return updates
|
|
28
|
+
*/
|
|
29
|
+
function getAccountUpdates(account, desired) {
|
|
30
|
+
const bp = account.business_profile || {};
|
|
31
|
+
const desiredBp = desired.business_profile;
|
|
32
|
+
|
|
33
|
+
const bpUpdates = {};
|
|
34
|
+
for (const key of ['name', 'url', 'support_email', 'support_url']) {
|
|
35
|
+
if (bp[key] !== desiredBp[key]) {
|
|
36
|
+
bpUpdates[key] = desiredBp[key];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return Object.keys(bpUpdates).length > 0 ? { business_profile: bpUpdates } : null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = async function ensureStripeAccount(context) {
|
|
44
|
+
const { brandConfig, stripeApi: api, options } = context;
|
|
45
|
+
const dryRun = options?.dryRun || false;
|
|
46
|
+
|
|
47
|
+
if (!api) {
|
|
48
|
+
console.log(` ${chalk.dim('⊘ Stripe not configured')}`);
|
|
49
|
+
return {};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (brandConfig.payment?.providers?.stripe?.updateAccountInfo === false) {
|
|
53
|
+
console.log(` ${chalk.dim('⊘ Account updates disabled (updateAccountInfo = false)')}`);
|
|
54
|
+
return {};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let account;
|
|
58
|
+
try {
|
|
59
|
+
account = await api.getAccount();
|
|
60
|
+
} catch (error) {
|
|
61
|
+
console.log(` ${chalk.yellow('⚠')} Could not read account${chalk.dim(`: ${error.message}`)}`);
|
|
62
|
+
return { status: 'warned', reason: 'could not read the Stripe account', output: { stripeAccount: { error: error.message } } };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const desired = buildDesiredAccount(brandConfig);
|
|
66
|
+
const updates = getAccountUpdates(account, desired);
|
|
67
|
+
|
|
68
|
+
if (!updates) {
|
|
69
|
+
console.log(` ${chalk.green('✓')} Account settings up to date`);
|
|
70
|
+
return { output: { stripeAccount: { updated: false } } };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Log what's changing
|
|
74
|
+
for (const [key, value] of Object.entries(updates.business_profile)) {
|
|
75
|
+
const current = account.business_profile?.[key] || '(none)';
|
|
76
|
+
console.log(` ${key}: "${current}" ${chalk.dim('→')} "${chalk.cyan(value)}"`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (dryRun) {
|
|
80
|
+
console.log(` ${chalk.cyan('~')} Would update account settings ${chalk.yellow('[DRY RUN]')}`);
|
|
81
|
+
return { output: { stripeAccount: { planned: 'update' } } };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
await api.updateAccount(account.id, updates);
|
|
86
|
+
console.log(` ${chalk.green('✓')} Account settings updated`);
|
|
87
|
+
return { output: { stripeAccount: { updated: true } } };
|
|
88
|
+
} catch (error) {
|
|
89
|
+
// Account updates may not be supported for this account type
|
|
90
|
+
console.log(` ${chalk.yellow('⚠')} Could not update account${chalk.dim(`: ${error.message}`)}`);
|
|
91
|
+
return { status: 'warned', reason: 'could not update the Stripe account settings', output: { stripeAccount: { error: error.message } } };
|
|
92
|
+
}
|
|
93
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Report Stripe Enhanced Dispute Protection status.
|
|
3
|
+
*
|
|
4
|
+
* The Stripe API has no dispute-protection management (Dashboard-only), so
|
|
5
|
+
* this follows the no-API manual-action pattern: print the settings
|
|
6
|
+
* deep-link, confirm interactively, and stay warned until activation is
|
|
7
|
+
* confirmed. A confirmation nothing can re-check is the one kind of
|
|
8
|
+
* reconcile flag config keeps (#434): it lands at
|
|
9
|
+
* `payment.providers.stripe.disputesConfirmed`. Cannot mutate by
|
|
10
|
+
* construction.
|
|
11
|
+
*/
|
|
12
|
+
const chalk = require('chalk').default;
|
|
13
|
+
const { confirm, pressEnterToOpen } = require('../../../vendor/devkit/prompt.js');
|
|
14
|
+
const { writeBrandConfig } = require('../../../lib/config-write.js');
|
|
15
|
+
const { canPrompt } = require('../../../lib/run-gates.js');
|
|
16
|
+
|
|
17
|
+
const CONFIG_PATH = 'payment.providers.stripe.disputesConfirmed';
|
|
18
|
+
|
|
19
|
+
module.exports = async function ensureStripeDisputes(context) {
|
|
20
|
+
const { brandConfig, stripeApi: api, serviceData, options = {} } = context;
|
|
21
|
+
|
|
22
|
+
if (!api) {
|
|
23
|
+
console.log(` ${chalk.dim('⊘ Stripe not configured')}`);
|
|
24
|
+
return {};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (brandConfig.payment?.providers?.stripe?.disputesConfirmed === true) {
|
|
28
|
+
console.log(` ${chalk.green('✓')} Enhanced Dispute Protection confirmed`);
|
|
29
|
+
return {};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const disputesUrl = serviceData.stripeAccountId
|
|
33
|
+
? `https://dashboard.stripe.com/${serviceData.stripeAccountId}/settings/disputes`
|
|
34
|
+
: 'https://dashboard.stripe.com/settings/disputes';
|
|
35
|
+
|
|
36
|
+
console.log(` ${chalk.yellow('⚠')} No API for dispute protection — activate ${chalk.bold('Enhanced Dispute Protection')} in the Dashboard`);
|
|
37
|
+
|
|
38
|
+
if (canPrompt(options)) {
|
|
39
|
+
await pressEnterToOpen(disputesUrl, 'the Stripe dispute settings');
|
|
40
|
+
const done = await confirm({ message: 'Enhanced Dispute Protection activated in the Dashboard?', default: false });
|
|
41
|
+
if (done) {
|
|
42
|
+
writeBrandConfig(context, { [CONFIG_PATH]: true });
|
|
43
|
+
console.log(` ${chalk.green('✓')} Enhanced Dispute Protection confirmed`);
|
|
44
|
+
return {};
|
|
45
|
+
}
|
|
46
|
+
} else {
|
|
47
|
+
console.log(` ${chalk.dim('→')} Dispute settings: ${chalk.cyan(disputesUrl)}`);
|
|
48
|
+
console.log(` ${chalk.dim('→')} (rerun in an interactive terminal to confirm)`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
status: 'warned',
|
|
53
|
+
reason: 'Enhanced Dispute Protection not confirmed — activate it in the Stripe Dashboard',
|
|
54
|
+
output: { stripeDisputes: { disputesUrl } },
|
|
55
|
+
};
|
|
56
|
+
};
|