@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,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Certificates service — Apple signing certificates, bundle IDs, and
|
|
3
|
+
* provisioning profiles for brands with desktop/mobile targets, reconciled
|
|
4
|
+
* via the App Store Connect API.
|
|
5
|
+
*
|
|
6
|
+
* Signing home: ONE Apple account signs everything a company ships, so a
|
|
7
|
+
* company-managed brand (`.omega/company.json` marker → context.companyRoot)
|
|
8
|
+
* shares the COMPANY workspace's signing tree; a standalone brand (the
|
|
9
|
+
* playground today — no parent yet) keeps it brand-local, and the material
|
|
10
|
+
* simply moves to the company root when one is born. Layout under the
|
|
11
|
+
* gitignored {companyRoot||brandRoot}/.omega/certificates/apple/:
|
|
12
|
+
* AuthKey_*.p8 (App Store Connect API key — the interactive rescue files it from Downloads)
|
|
13
|
+
* certificates/{TYPE}.cer + .p12 (downloaded/created + exported)
|
|
14
|
+
* csr/{TYPE}/{private.key,request.csr} (PRESERVED — the issued cert is paired with this key)
|
|
15
|
+
* profiles/{BRAND_ID}/{TYPE}/{PLATFORM}.mobileprovision (per-brand — profiles bind one bundle ID)
|
|
16
|
+
*
|
|
17
|
+
* De-ITW'd from omega-manager: certs lived in the company instance's shared
|
|
18
|
+
* .output/_shared/ tree (one set for all brands) and the bundle-ID prefix
|
|
19
|
+
* was hardcoded to the company — the port keeps everything brand-local and
|
|
20
|
+
* reads certificates.providers.apple.bundleIdPrefix from config. The interactive
|
|
21
|
+
* rescue for portal-only certs is restored and upgraded (Ian 2026-07-14,
|
|
22
|
+
* lib/manual-walkthrough.js): the pipeline stages the CSR itself — legacy
|
|
23
|
+
* sent users to Keychain Access — opens the portal create page Enter-gated,
|
|
24
|
+
* and watches Downloads for the issued .cer; non-interactive runs keep
|
|
25
|
+
* printed guidance + converge-on-rerun. The --force-recreate CLI flag was
|
|
26
|
+
* not ported (delete the local .cer to force a re-download; expired certs
|
|
27
|
+
* recreate automatically).
|
|
28
|
+
*
|
|
29
|
+
* Env (brand .env): APPLE_API_ISSUER, APPLE_API_KEY_ID, APPLE_TEAM_ID
|
|
30
|
+
* required; CSC_KEY_PASSWORD auto-generated + persisted to the brand .env
|
|
31
|
+
* on first real run (never rotate it — that orphans existing .p12 files);
|
|
32
|
+
* APPLE_KEYCHAIN_PASSWORD optional (suppresses macOS keychain prompts).
|
|
33
|
+
*/
|
|
34
|
+
const { join } = require('node:path');
|
|
35
|
+
const { homedir } = require('node:os');
|
|
36
|
+
const { randomBytes } = require('node:crypto');
|
|
37
|
+
const jetpack = require('fs-jetpack');
|
|
38
|
+
const chalk = require('chalk').default;
|
|
39
|
+
const { openBrowserAndPoll } = require('../../vendor/devkit/flows.js');
|
|
40
|
+
|
|
41
|
+
const { serviceInputSpec } = require('../../config.js');
|
|
42
|
+
const { createServiceRunner } = require('../../lib/service-runner.js');
|
|
43
|
+
const { requestServiceInput } = require('../../lib/service-input.js');
|
|
44
|
+
const { canPrompt } = require('../../lib/run-gates.js');
|
|
45
|
+
const { writeEnvValue } = require('../../lib/env-secret.js');
|
|
46
|
+
const { createAppleClient } = require('./lib/apple-api.js');
|
|
47
|
+
const { importP12Files } = require('./lib/keychain.js');
|
|
48
|
+
|
|
49
|
+
const APPSTORE_KEYS_URL = 'https://appstoreconnect.apple.com/access/api';
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Find the user-placed App Store Connect API key (AuthKey_*.p8) in the
|
|
53
|
+
* brand's Apple dir. Auto-detected by prefix so no path config is needed.
|
|
54
|
+
*
|
|
55
|
+
* @param {string} appleDir - {brandRoot}/.omega/certificates/apple
|
|
56
|
+
* @returns {string|null} - Absolute path to the .p8, or null
|
|
57
|
+
*/
|
|
58
|
+
function findAuthKey(appleDir) {
|
|
59
|
+
if (!jetpack.exists(appleDir)) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
const p8Files = jetpack.find(appleDir, { matching: 'AuthKey_*.p8', recursive: false });
|
|
63
|
+
return p8Files[0] || null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Generate a random CSC_KEY_PASSWORD and persist it to the signing root's
|
|
68
|
+
* .env (the company workspace when company-managed — shared .p12s need the
|
|
69
|
+
* ONE shared password, and the env chain loads the company .env under every
|
|
70
|
+
* sibling brand's — else the brand root).
|
|
71
|
+
*
|
|
72
|
+
* .p12 files MUST carry a real password — modern macOS rejects
|
|
73
|
+
* empty-password .p12 in `security import` ("MAC verification failed").
|
|
74
|
+
* The value is meaningless to the user; it just has to stay stable, so it
|
|
75
|
+
* is written back to .env for every future run (and for CI signing).
|
|
76
|
+
*
|
|
77
|
+
* @param {string} signingRoot - The signing tree's owning root (its .env is the target)
|
|
78
|
+
* @returns {string} - The generated password
|
|
79
|
+
*/
|
|
80
|
+
function ensureCertPassword(signingRoot) {
|
|
81
|
+
const password = randomBytes(24).toString('base64url');
|
|
82
|
+
writeEnvValue(signingRoot, 'CSC_KEY_PASSWORD', password);
|
|
83
|
+
console.log(` ${chalk.green('✓')} Generated CSC_KEY_PASSWORD and saved to the signing root's .env`);
|
|
84
|
+
|
|
85
|
+
return password;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Interactive rescue for a missing App Store Connect API key. The .p8
|
|
90
|
+
* downloads ONCE (Apple never re-serves it), so the walkthrough opens the
|
|
91
|
+
* keys page Enter-gated and watches Downloads for a fresh AuthKey_*.p8,
|
|
92
|
+
* filing it into the signing tree. Non-interactive runs keep the skip +
|
|
93
|
+
* guidance contract.
|
|
94
|
+
*/
|
|
95
|
+
async function rescueAuthKey(appleDir, downloadsDir = join(homedir(), 'Downloads')) {
|
|
96
|
+
const startedAt = Date.now();
|
|
97
|
+
|
|
98
|
+
const result = await openBrowserAndPoll({
|
|
99
|
+
url: APPSTORE_KEYS_URL,
|
|
100
|
+
promptMessage: [
|
|
101
|
+
'An App Store Connect API key is needed (Users and Access → Integrations):',
|
|
102
|
+
` 1. Generate an API key with the ${chalk.bold('Admin')} role — or download an existing one (each .p8 downloads ONCE)`,
|
|
103
|
+
' 2. I\'ll detect the AuthKey_*.p8 in Downloads and file it into the signing tree',
|
|
104
|
+
].join('\n'),
|
|
105
|
+
label: 'the App Store Connect API keys page',
|
|
106
|
+
waitMessage: 'Waiting for the AuthKey_*.p8 download',
|
|
107
|
+
intervalMs: 3000,
|
|
108
|
+
check: async () => {
|
|
109
|
+
for (const name of jetpack.list(downloadsDir) || []) {
|
|
110
|
+
if (!/^AuthKey_[A-Z0-9]+\.p8$/.test(name)) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const modified = jetpack.inspect(join(downloadsDir, name), { times: true })?.modifyTime;
|
|
114
|
+
if (!modified || modified.getTime() < startedAt) {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
jetpack.move(join(downloadsDir, name), join(appleDir, name));
|
|
118
|
+
return { done: true, result: name };
|
|
119
|
+
}
|
|
120
|
+
return { done: false };
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
if (result.success) {
|
|
125
|
+
console.log(` ${chalk.green('✓')} Filed ${chalk.cyan(result.result)} into ${chalk.gray(appleDir)}`);
|
|
126
|
+
return findAuthKey(appleDir);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Resolve the App Store Connect credentials from the environment + the
|
|
134
|
+
* signing tree. Returns { skip, reason } when anything is missing — except
|
|
135
|
+
* a missing .p8 on an interactive run, which gets the download rescue.
|
|
136
|
+
*/
|
|
137
|
+
async function resolveAppleSecrets(context, appleDir, signingRoot) {
|
|
138
|
+
const dryRun = context.options?.dryRun || false;
|
|
139
|
+
|
|
140
|
+
// The shared setup contract (#608): an interactive run walks the App Store
|
|
141
|
+
// Connect keys page and takes the three ids right here; a headless one steps
|
|
142
|
+
// aside loudly, and "disable" retires the service for this brand.
|
|
143
|
+
const gate = await requestServiceInput(context, serviceInputSpec('certificates'));
|
|
144
|
+
if (gate) {
|
|
145
|
+
return gate;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const issuerId = process.env.APPLE_API_ISSUER;
|
|
149
|
+
const keyId = process.env.APPLE_API_KEY_ID;
|
|
150
|
+
const teamId = process.env.APPLE_TEAM_ID;
|
|
151
|
+
|
|
152
|
+
let privateKeyPath = findAuthKey(appleDir);
|
|
153
|
+
if (!privateKeyPath && canPrompt(context.options)) {
|
|
154
|
+
privateKeyPath = await rescueAuthKey(appleDir, context.downloadsDir);
|
|
155
|
+
}
|
|
156
|
+
if (!privateKeyPath) {
|
|
157
|
+
return {
|
|
158
|
+
skip: true,
|
|
159
|
+
reason: `no App Store Connect API key found — download the .p8 from ${APPSTORE_KEYS_URL} and save it as AuthKey_<KEYID>.p8 in ${appleDir}`,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let certificatePassword = process.env.CSC_KEY_PASSWORD || '';
|
|
164
|
+
if (!certificatePassword) {
|
|
165
|
+
if (dryRun) {
|
|
166
|
+
console.log(` ${chalk.cyan('[DRY RUN]')} Would generate CSC_KEY_PASSWORD and save it to the signing root's .env`);
|
|
167
|
+
} else {
|
|
168
|
+
certificatePassword = ensureCertPassword(signingRoot);
|
|
169
|
+
process.env.CSC_KEY_PASSWORD = certificatePassword;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
secrets: {
|
|
175
|
+
issuerId,
|
|
176
|
+
keyId,
|
|
177
|
+
teamId,
|
|
178
|
+
privateKeyPath,
|
|
179
|
+
certificatePassword,
|
|
180
|
+
keychainPassword: process.env.APPLE_KEYCHAIN_PASSWORD || null,
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
module.exports.run = createServiceRunner({
|
|
186
|
+
serviceDir: __dirname,
|
|
187
|
+
setup: async (context) => {
|
|
188
|
+
const config = context.brandConfig.certificates;
|
|
189
|
+
|
|
190
|
+
if (config === false || config?.enabled === false) {
|
|
191
|
+
return { skip: true, reason: 'certificates.enabled = false' };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const targets = context.brandConfig.targets || {};
|
|
195
|
+
if (!targets.desktop && !targets.mobile) {
|
|
196
|
+
return { skip: true, reason: 'no desktop or mobile target' };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// One Apple account signs everything a company ships — company-managed
|
|
200
|
+
// brands share the company workspace's signing tree; standalone brands
|
|
201
|
+
// keep it brand-local (and the material moves when a company is born)
|
|
202
|
+
const signingRoot = context.companyRoot || context.brandRoot;
|
|
203
|
+
const appleDir = join(signingRoot, '.omega', 'certificates', 'apple');
|
|
204
|
+
|
|
205
|
+
// Tests inject a fake client + secrets via context
|
|
206
|
+
let appleClient = context.appleClient;
|
|
207
|
+
let appleSecrets = context.appleSecrets;
|
|
208
|
+
if (!appleClient) {
|
|
209
|
+
const resolved = await resolveAppleSecrets(context, appleDir, signingRoot);
|
|
210
|
+
if (resolved.skip) {
|
|
211
|
+
return resolved;
|
|
212
|
+
}
|
|
213
|
+
appleSecrets = resolved.secrets;
|
|
214
|
+
appleClient = createAppleClient(appleSecrets);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
appleClient,
|
|
219
|
+
appleSecrets,
|
|
220
|
+
appleDir,
|
|
221
|
+
// Tests inject a recorder so the real macOS keychain is never touched
|
|
222
|
+
keychainImport: context.keychainImport || importP12Files,
|
|
223
|
+
};
|
|
224
|
+
},
|
|
225
|
+
});
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App Store Connect API client — ES256 JWT auth over node:crypto + global
|
|
3
|
+
* fetch (omega-manager used jsonwebtoken + wonderful-fetch; the port needs
|
|
4
|
+
* neither). Exposes request() (JSON, throws on non-2xx with Apple's error
|
|
5
|
+
* text) and paginate() (follows links.next).
|
|
6
|
+
*
|
|
7
|
+
* Apple's "agreements expired" 403 gets a printed fix-it (accept the
|
|
8
|
+
* pending agreements at the developer site) — omega-manager auto-opened
|
|
9
|
+
* the browser; the port prints the URL and stays non-interactive.
|
|
10
|
+
*/
|
|
11
|
+
const jetpack = require('fs-jetpack');
|
|
12
|
+
const chalk = require('chalk').default;
|
|
13
|
+
|
|
14
|
+
const { signJwt } = require('../../../lib/jwt.js');
|
|
15
|
+
|
|
16
|
+
const API_BASE = 'https://api.appstoreconnect.apple.com/v1';
|
|
17
|
+
|
|
18
|
+
// Universal Apple Developer landing — pending agreements surface here as a
|
|
19
|
+
// banner (the per-team agreement URL is unique per developer account).
|
|
20
|
+
const APPLE_AGREEMENTS_URL = 'https://developer.apple.com/account/';
|
|
21
|
+
|
|
22
|
+
const TOKEN_TTL_SECONDS = 20 * 60; // Apple max — 20 minutes
|
|
23
|
+
const TOKEN_REFRESH_BUFFER_SECONDS = 5 * 60; // Refresh 5 min before expiry
|
|
24
|
+
|
|
25
|
+
// Print the agreements guidance once per process — multiple API calls
|
|
26
|
+
// hitting the same 403 shouldn't repeat it.
|
|
27
|
+
let agreementsNoticePrinted = false;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Detect Apple's "agreements expired" 403 from an error message. Matches
|
|
31
|
+
* BOTH the error code and the human title — live 2026-07-14: the message
|
|
32
|
+
* carried only title+detail (no code) and the code-only detector missed it,
|
|
33
|
+
* so the first certificates exercise errored raw instead of warning.
|
|
34
|
+
*/
|
|
35
|
+
function isAgreementsError(errorMessage) {
|
|
36
|
+
return /FORBIDDEN\.REQUIRED_AGREEMENTS_MISSING_OR_EXPIRED|required agreement is missing or has expired/i.test(errorMessage || '');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Wrap an ensure operation so Apple's agreements 403 downgrades to a WARNED
|
|
41
|
+
* result with the fix-it guidance — a pending agreement is a human-gated
|
|
42
|
+
* prerequisite (the AdSense-approval pattern), not a service failure.
|
|
43
|
+
* Anything else rethrows untouched.
|
|
44
|
+
*/
|
|
45
|
+
function catchAgreements(operation) {
|
|
46
|
+
const warnedResult = () => {
|
|
47
|
+
printAgreementsNotice();
|
|
48
|
+
return {
|
|
49
|
+
status: 'warned',
|
|
50
|
+
reason: 'Apple Developer agreements need acceptance',
|
|
51
|
+
output: {
|
|
52
|
+
agreements: 'pending',
|
|
53
|
+
warning: `Apple Developer agreements need acceptance — visit ${APPLE_AGREEMENTS_URL} (Paid Apps / Free Apps / Program License), then re-run`,
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
return async function agreementsGuarded(context) {
|
|
59
|
+
let result;
|
|
60
|
+
try {
|
|
61
|
+
result = await operation(context);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if (!isAgreementsError(error?.message)) {
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
return warnedResult();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// The ensures also catch internally and RETURN error statuses — an
|
|
70
|
+
// agreements 403 must downgrade on that path too
|
|
71
|
+
if (result?.status === 'error' && isAgreementsError(result.error)) {
|
|
72
|
+
return warnedResult();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return result;
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function printAgreementsNotice() {
|
|
80
|
+
if (agreementsNoticePrinted) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
agreementsNoticePrinted = true;
|
|
84
|
+
console.log('');
|
|
85
|
+
console.log(` ${chalk.yellow('⚠')} ${chalk.bold('Apple Developer Program agreements need to be accepted')}`);
|
|
86
|
+
console.log(` ${chalk.gray('→')} Visit ${chalk.cyan(APPLE_AGREEMENTS_URL)}, accept all pending agreements (Paid Apps, Free Apps, Developer Program License), then re-run`);
|
|
87
|
+
console.log('');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Build an authenticated App Store Connect client.
|
|
92
|
+
*
|
|
93
|
+
* @param {Object} secrets - { issuerId, keyId, privateKeyPath }
|
|
94
|
+
* @returns {{ request: Function, paginate: Function }}
|
|
95
|
+
*/
|
|
96
|
+
function createAppleClient(secrets) {
|
|
97
|
+
const privateKey = jetpack.read(secrets.privateKeyPath);
|
|
98
|
+
if (!privateKey) {
|
|
99
|
+
throw new Error(`Failed to read App Store Connect private key from ${secrets.privateKeyPath}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let cachedToken = null;
|
|
103
|
+
let tokenExpiresAt = 0;
|
|
104
|
+
|
|
105
|
+
function getToken() {
|
|
106
|
+
const now = Math.floor(Date.now() / 1000);
|
|
107
|
+
if (!cachedToken || now >= tokenExpiresAt - TOKEN_REFRESH_BUFFER_SECONDS) {
|
|
108
|
+
cachedToken = signJwt(
|
|
109
|
+
{ alg: 'ES256', kid: secrets.keyId, typ: 'JWT' },
|
|
110
|
+
{ iss: secrets.issuerId, iat: now, exp: now + TOKEN_TTL_SECONDS, aud: 'appstoreconnect-v1' },
|
|
111
|
+
privateKey,
|
|
112
|
+
);
|
|
113
|
+
tokenExpiresAt = now + TOKEN_TTL_SECONDS;
|
|
114
|
+
}
|
|
115
|
+
return cachedToken;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* JSON request to the App Store Connect API. Returns the parsed body
|
|
120
|
+
* (null for 204/allowed-404). Throws with Apple's error text otherwise.
|
|
121
|
+
*
|
|
122
|
+
* @param {string} url - Full URL
|
|
123
|
+
* @param {Object} [options] - { method, body, allow404 }
|
|
124
|
+
*/
|
|
125
|
+
async function request(url, options = {}) {
|
|
126
|
+
const { method = 'GET', body, allow404 = false } = options;
|
|
127
|
+
|
|
128
|
+
const response = await fetch(url, {
|
|
129
|
+
method,
|
|
130
|
+
headers: {
|
|
131
|
+
Authorization: `Bearer ${getToken()}`,
|
|
132
|
+
'Content-Type': 'application/json',
|
|
133
|
+
},
|
|
134
|
+
body: body || undefined,
|
|
135
|
+
signal: AbortSignal.timeout(60000),
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
if (response.status === 404 && allow404) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
if (response.status === 204) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const data = await response.json().catch(() => null);
|
|
146
|
+
|
|
147
|
+
if (!response.ok) {
|
|
148
|
+
// Keep Apple's error CODE in the message — detectors and humans both
|
|
149
|
+
// need it (title+detail alone hid REQUIRED_AGREEMENTS_MISSING_OR_EXPIRED)
|
|
150
|
+
const message = (data?.errors || [])
|
|
151
|
+
.map((e) => `${e.code ? `[${e.code}] ` : ''}${e.title}: ${e.detail || ''}`.trim())
|
|
152
|
+
.join('; ')
|
|
153
|
+
|| `HTTP ${response.status}`;
|
|
154
|
+
|
|
155
|
+
if (isAgreementsError(message)) {
|
|
156
|
+
printAgreementsNotice();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
throw new Error(`Apple API request failed (${method} ${url}): ${message}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return data;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Paginated GET — follows `links.next` until exhausted and returns the
|
|
167
|
+
* concatenated `data` array.
|
|
168
|
+
*/
|
|
169
|
+
async function paginate(url) {
|
|
170
|
+
let all = [];
|
|
171
|
+
let nextUrl = url;
|
|
172
|
+
while (nextUrl) {
|
|
173
|
+
const data = await request(nextUrl);
|
|
174
|
+
all = all.concat(data?.data || []);
|
|
175
|
+
nextUrl = data?.links?.next || null;
|
|
176
|
+
}
|
|
177
|
+
return all;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return { request, paginate };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
module.exports = { API_BASE, createAppleClient, isAgreementsError, catchAgreements };
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Certificate CRUD + local key material: list/create/download signing
|
|
3
|
+
* certificates via the App Store Connect API, generate CSRs with openssl,
|
|
4
|
+
* export .cer + private key pairs to Keychain-importable .p12 files, and
|
|
5
|
+
* validate manually-downloaded .cer files.
|
|
6
|
+
*
|
|
7
|
+
* openssl notes (both verified against OpenSSL 3.x, which auto-detects
|
|
8
|
+
* DER/PEM input): Apple serves .cer files as DER; `pkcs12 -export -legacy`
|
|
9
|
+
* forces the older PBE-SHA1-3DES format because macOS `security import`
|
|
10
|
+
* rejects OpenSSL 3's default PBES2/AES-256-CBC ("MAC verification
|
|
11
|
+
* failed").
|
|
12
|
+
*/
|
|
13
|
+
const { execFileSync } = require('node:child_process');
|
|
14
|
+
const jetpack = require('fs-jetpack');
|
|
15
|
+
const chalk = require('chalk').default;
|
|
16
|
+
|
|
17
|
+
const { isStale } = require('../../../lib/stale.js');
|
|
18
|
+
const { API_BASE } = require('./apple-api.js');
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* List all certificates (paginated).
|
|
22
|
+
*/
|
|
23
|
+
async function listCertificates(client) {
|
|
24
|
+
return client.paginate(`${API_BASE}/certificates?limit=200`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Download a certificate's .cer content (base64-decoded DER) to disk.
|
|
29
|
+
*/
|
|
30
|
+
async function downloadCertificate(client, certificateId, outputPath) {
|
|
31
|
+
const data = await client.request(`${API_BASE}/certificates/${certificateId}`);
|
|
32
|
+
const certificateContent = data?.data?.attributes?.certificateContent;
|
|
33
|
+
if (!certificateContent) {
|
|
34
|
+
throw new Error(`Certificate ${certificateId} has no certificateContent`);
|
|
35
|
+
}
|
|
36
|
+
jetpack.write(outputPath, Buffer.from(certificateContent, 'base64'));
|
|
37
|
+
console.log(` ${chalk.green('✓')} Downloaded ${chalk.gray(outputPath)}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Create a new certificate by submitting a CSR.
|
|
42
|
+
*/
|
|
43
|
+
async function createCertificate(client, type, csrContent) {
|
|
44
|
+
const data = await client.request(`${API_BASE}/certificates`, {
|
|
45
|
+
method: 'POST',
|
|
46
|
+
body: JSON.stringify({
|
|
47
|
+
data: {
|
|
48
|
+
type: 'certificates',
|
|
49
|
+
attributes: { certificateType: type, csrContent },
|
|
50
|
+
},
|
|
51
|
+
}),
|
|
52
|
+
});
|
|
53
|
+
return data.data;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Generate a new RSA-2048 private key + CSR via openssl. Writes
|
|
58
|
+
* `private.key` and `request.csr` to outputDir; returns the cleaned CSR
|
|
59
|
+
* body (no PEM headers/newlines) ready to POST to Apple.
|
|
60
|
+
*/
|
|
61
|
+
function generateCSR(emailAddress, commonName, outputDir) {
|
|
62
|
+
jetpack.dir(outputDir);
|
|
63
|
+
const keyPath = `${outputDir}/private.key`;
|
|
64
|
+
const csrPath = `${outputDir}/request.csr`;
|
|
65
|
+
const subject = `/emailAddress=${emailAddress}/CN=${commonName}/C=US`;
|
|
66
|
+
|
|
67
|
+
execFileSync(
|
|
68
|
+
'openssl',
|
|
69
|
+
['req', '-nodes', '-newkey', 'rsa:2048', '-keyout', keyPath, '-out', csrPath, '-subj', subject],
|
|
70
|
+
{ stdio: 'pipe' },
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
return cleanCSR(jetpack.read(csrPath));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get CSR content for a cert type, reusing an existing CSR when found.
|
|
78
|
+
*
|
|
79
|
+
* IMPORTANT: CSR private keys must be preserved across runs — they're
|
|
80
|
+
* paired with the issued certificate. Deleting a CSR private key while a
|
|
81
|
+
* matching cert is active makes the cert unusable for signing.
|
|
82
|
+
*/
|
|
83
|
+
function getCSRContent(type, appleDir, teamId) {
|
|
84
|
+
const csrDir = `${appleDir}/csr/${type}`;
|
|
85
|
+
const csrPath = `${csrDir}/request.csr`;
|
|
86
|
+
|
|
87
|
+
if (jetpack.exists(csrPath)) {
|
|
88
|
+
console.log(` ${chalk.gray('ℹ')} Reusing existing CSR from ${csrPath}`);
|
|
89
|
+
return cleanCSR(jetpack.read(csrPath));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
console.log(` Generating new CSR...`);
|
|
93
|
+
return generateCSR(`${teamId}@apple.com`, `${type} Certificate`, csrDir);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function cleanCSR(csrContent) {
|
|
97
|
+
return csrContent
|
|
98
|
+
.replace('-----BEGIN CERTIFICATE REQUEST-----', '')
|
|
99
|
+
.replace('-----END CERTIFICATE REQUEST-----', '')
|
|
100
|
+
.replace(/\n/g, '');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Export the .cer + private.key pair to a Keychain-importable .p12.
|
|
105
|
+
*
|
|
106
|
+
* No-ops when the .cer is missing or when the .p12 is already fresher than
|
|
107
|
+
* the .cer (mtime diff — a converged brand runs zero openssl execs). A cert
|
|
108
|
+
* WITHOUT its paired key warns instead of silently skipping: that cert
|
|
109
|
+
* can't sign anything until csr/{TYPE}/private.key arrives from the machine
|
|
110
|
+
* whose CSR created it.
|
|
111
|
+
*/
|
|
112
|
+
function exportToP12(type, appleDir, certificatePassword = '') {
|
|
113
|
+
const certPath = `${appleDir}/certificates/${type}.cer`;
|
|
114
|
+
const keyPath = `${appleDir}/csr/${type}/private.key`;
|
|
115
|
+
const p12Path = `${appleDir}/certificates/${type}.p12`;
|
|
116
|
+
|
|
117
|
+
if (!jetpack.exists(certPath)) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (!jetpack.exists(keyPath)) {
|
|
121
|
+
console.log(` ${chalk.yellow('⚠')} No local private key for ${type} — .p12 not exported (copy csr/${type}/private.key from the machine that created the CSR, then re-run)`);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (!isStale(certPath, p12Path)) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
// -legacy: macOS `security import` requires the old PKCS12 format.
|
|
130
|
+
// Password rides an env var (-passout env:) — never argv, never a shell string.
|
|
131
|
+
execFileSync(
|
|
132
|
+
'openssl',
|
|
133
|
+
['pkcs12', '-export', '-legacy', '-inkey', keyPath, '-in', certPath, '-out', p12Path, '-passout', 'env:OMEGA_P12_PASSWORD'],
|
|
134
|
+
{ stdio: 'pipe', env: { ...process.env, OMEGA_P12_PASSWORD: certificatePassword } },
|
|
135
|
+
);
|
|
136
|
+
console.log(` ${chalk.green('✓')} Exported .p12 ${chalk.gray(p12Path)}`);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
console.log(` ${chalk.yellow('⚠')} Failed to export .p12: ${chalk.gray(error.message)}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Find a non-expired certificate of the given type, preferring the one
|
|
144
|
+
* that expires furthest in the future.
|
|
145
|
+
*/
|
|
146
|
+
function findValidCertificate(existingCerts, type) {
|
|
147
|
+
const now = new Date();
|
|
148
|
+
const validCerts = existingCerts.filter((cert) => {
|
|
149
|
+
return cert.attributes.certificateType === type
|
|
150
|
+
&& new Date(cert.attributes.expirationDate) > now;
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
if (validCerts.length === 0) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return validCerts.sort((a, b) => {
|
|
158
|
+
return new Date(b.attributes.expirationDate) - new Date(a.attributes.expirationDate);
|
|
159
|
+
})[0];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Validate a manually-downloaded .cer via openssl (exists + not expired).
|
|
164
|
+
* Manual types (DEVELOPER_ID_*) can only be downloaded from the developer
|
|
165
|
+
* portal — the API doesn't allow creating them.
|
|
166
|
+
*
|
|
167
|
+
* @returns {{ valid: boolean, expirationDate: Date|null, reason: string|null }}
|
|
168
|
+
*/
|
|
169
|
+
function validateManualCertificate(certPath) {
|
|
170
|
+
if (!jetpack.exists(certPath)) {
|
|
171
|
+
return { valid: false, expirationDate: null, reason: 'file not found' };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
const endDateOutput = execFileSync(
|
|
176
|
+
'openssl',
|
|
177
|
+
['x509', '-enddate', '-noout', '-in', certPath],
|
|
178
|
+
{ encoding: 'utf8', stdio: 'pipe' },
|
|
179
|
+
).trim();
|
|
180
|
+
|
|
181
|
+
const dateMatch = endDateOutput.match(/notAfter=(.+)/);
|
|
182
|
+
if (!dateMatch) {
|
|
183
|
+
return { valid: false, expirationDate: null, reason: 'could not parse expiration date' };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const expirationDate = new Date(dateMatch[1]);
|
|
187
|
+
if (expirationDate <= new Date()) {
|
|
188
|
+
return { valid: false, expirationDate, reason: 'expired' };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return { valid: true, expirationDate, reason: null };
|
|
192
|
+
} catch (error) {
|
|
193
|
+
return { valid: false, expirationDate: null, reason: error.message };
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Common Name of a certificate's subject (openssl auto-detects DER/PEM).
|
|
199
|
+
* Apple stamps the portal type into the CN ("Developer ID Installer: Team
|
|
200
|
+
* Name (TEAMID)") — how a downloaded .cer is matched to the expected type.
|
|
201
|
+
*
|
|
202
|
+
* @returns {string|null}
|
|
203
|
+
*/
|
|
204
|
+
function getCertificateCommonName(certPath) {
|
|
205
|
+
try {
|
|
206
|
+
const subject = execFileSync(
|
|
207
|
+
'openssl',
|
|
208
|
+
['x509', '-subject', '-noout', '-in', certPath],
|
|
209
|
+
{ encoding: 'utf8', stdio: 'pipe' },
|
|
210
|
+
);
|
|
211
|
+
const match = subject.match(/CN\s*=\s*([^,]+)/);
|
|
212
|
+
return match ? match[1].trim() : null;
|
|
213
|
+
} catch (error) {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Whether the certificate's public key pairs with the private key (modulus
|
|
220
|
+
* comparison) — proof a portal-downloaded .cer was issued from the
|
|
221
|
+
* pipeline's own CSR, so the .p12 export has its pair.
|
|
222
|
+
*/
|
|
223
|
+
function certificateMatchesKey(certPath, keyPath) {
|
|
224
|
+
try {
|
|
225
|
+
const certModulus = execFileSync(
|
|
226
|
+
'openssl',
|
|
227
|
+
['x509', '-modulus', '-noout', '-in', certPath],
|
|
228
|
+
{ encoding: 'utf8', stdio: 'pipe' },
|
|
229
|
+
).trim();
|
|
230
|
+
const keyModulus = execFileSync(
|
|
231
|
+
'openssl',
|
|
232
|
+
['rsa', '-modulus', '-noout', '-in', keyPath],
|
|
233
|
+
{ encoding: 'utf8', stdio: 'pipe' },
|
|
234
|
+
).trim();
|
|
235
|
+
return certModulus.length > 0 && certModulus === keyModulus;
|
|
236
|
+
} catch (error) {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
module.exports = {
|
|
242
|
+
listCertificates,
|
|
243
|
+
downloadCertificate,
|
|
244
|
+
createCertificate,
|
|
245
|
+
generateCSR,
|
|
246
|
+
getCSRContent,
|
|
247
|
+
exportToP12,
|
|
248
|
+
findValidCertificate,
|
|
249
|
+
validateManualCertificate,
|
|
250
|
+
getCertificateCommonName,
|
|
251
|
+
certificateMatchesKey,
|
|
252
|
+
};
|