@jini-ai/agent-plugins 0.6.16 → 0.6.19
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/CHANGELOG.md +35 -1
- package/NOTICE +14 -0
- package/README.md +35 -14
- package/charts-echarts/plugin.json +39 -0
- package/charts-echarts/skills/charts-echarts/SKILL.md +110 -0
- package/charts-recharts/plugin.json +32 -0
- package/charts-recharts/skills/charts-recharts/SKILL.md +108 -0
- package/cloudflare/NOTES.md +16 -0
- package/cloudflare/form-destinations/cloudflare-d1.mjs +9 -0
- package/cloudflare/lib/api.mjs +96 -0
- package/cloudflare/lib/assets.mjs +65 -0
- package/cloudflare/lib/dns.mjs +32 -0
- package/cloudflare/lib/domain-inventory.mjs +61 -0
- package/cloudflare/lib/domain-status.mjs +48 -0
- package/cloudflare/lib/domains.mjs +95 -0
- package/cloudflare/lib/forms.mjs +254 -0
- package/cloudflare/lib/pages-upload.mjs +51 -0
- package/cloudflare/lib/target.mjs +165 -0
- package/cloudflare/lib/workers-upload.mjs +48 -0
- package/cloudflare/lib/zones.mjs +25 -0
- package/cloudflare/mcp.json +4 -0
- package/cloudflare/plugin.json +96 -0
- package/cloudflare/skills/cloudflare/SKILL.md +57 -0
- package/cloudflare/skills/cloudflare/references/domains.md +31 -0
- package/cloudflare/skills/cloudflare/references/pages.md +18 -0
- package/cloudflare/skills/cloudflare/references/permissions.md +29 -0
- package/cloudflare/skills/cloudflare/references/troubleshooting.md +20 -0
- package/cloudflare/skills/cloudflare/references/workers.md +42 -0
- package/cloudflare/targets/cloudflare-pages.mjs +12 -0
- package/cloudflare/targets/cloudflare-workers.mjs +17 -0
- package/cloudflare/tovu-consent.json +6 -0
- package/cloudflare/tovu-deploy-targets.json +630 -0
- package/cloudflare/tovu-form-destinations.json +12 -0
- package/cloudflare/worker/forms-receiver.mjs +249 -0
- package/composio/mcp.json +10 -0
- package/composio/plugin.json +68 -0
- package/composio/skills/composio/SKILL.md +85 -0
- package/dist/core/identity-namespace.d.ts +12 -0
- package/dist/core/identity-namespace.js +20 -0
- package/dist/folder-zip-node.js +29 -7
- package/dist/lifecycle/bundled-digests.js +4 -3
- package/dist/lifecycle/install.d.ts +7 -0
- package/dist/lifecycle/install.js +45 -3
- package/dist/lifecycle/manifest.d.ts +2 -2
- package/dist/lifecycle/manifest.js +5 -3
- package/dist/lifecycle/node.js +2 -0
- package/dist/lifecycle/persistent-state.js +1 -1
- package/dist/lifecycle/ports.d.ts +8 -0
- package/dist/lifecycle/seed-bundled.js +4 -1
- package/dist/manifest.d.ts +1 -1
- package/dist/manifest.js +3 -2
- package/fly/deploy-configs/fly.mjs +115 -0
- package/fly/deploy-ops/fly.mjs +102 -0
- package/fly/mcp.json +4 -0
- package/fly/plugin.json +81 -0
- package/fly/scripts/render-deploy-workflow.mjs +150 -0
- package/fly/skills/fly/SKILL.md +43 -0
- package/fly/skills/fly/references/fly-deploy.template.yml +117 -0
- package/fly/skills/fly/references/fly.template.toml +81 -0
- package/fly/tovu-credential-schemes.json +11 -0
- package/fly/tovu-deploy-configs.json +21 -0
- package/fly/tovu-deploy-ops.json +14 -0
- package/github/deploy-ops/github-actions.mjs +164 -0
- package/github/deployments/github.mjs +415 -0
- package/github/mcp.json +4 -0
- package/github/plugin.json +105 -0
- package/github/skills/github/SKILL.md +249 -0
- package/github/skills/github/references/actions.md +224 -0
- package/github/skills/github/references/auth-and-tokens.md +140 -0
- package/github/skills/github/references/repo-files.md +135 -0
- package/github/skills/github/references/site-backup.md +159 -0
- package/github/skills/github/references/source-control.md +123 -0
- package/github/source-control/backup-push.mjs +242 -0
- package/github/source-control/commit-site.mjs +459 -0
- package/github/source-control/github.mjs +93 -0
- package/github/source-control/write-files.mjs +468 -0
- package/github/tovu-deploy-ops.json +13 -0
- package/github/tovu-source-control.json +117 -0
- package/github-pages/NOTES.md +19 -0
- package/github-pages/mcp.json +4 -0
- package/github-pages/plugin.json +48 -0
- package/github-pages/skills/github-pages/SKILL.md +44 -0
- package/github-pages/targets/github-pages.mjs +543 -0
- package/github-pages/tovu-deploy-targets.json +327 -0
- package/google-login/exports/google-g.svg +1 -0
- package/google-login/exports/sign-in-button.css +68 -0
- package/google-login/exports/sign-in-button.html +4 -0
- package/google-login/mcp.json +4 -0
- package/google-login/plugin.json +59 -0
- package/google-login/skills/google-login/SKILL.md +96 -0
- package/google-login/tovu-login-providers.json +21 -0
- package/google-workspace/README.md +132 -0
- package/google-workspace/form-destinations/README.md +87 -0
- package/google-workspace/form-destinations/dedupe.mjs +43 -0
- package/google-workspace/form-destinations/delivery-result.mjs +69 -0
- package/google-workspace/form-destinations/mapping.mjs +152 -0
- package/google-workspace/form-destinations/sheets.mjs +202 -0
- package/google-workspace/lib/auth.mjs +81 -0
- package/google-workspace/lib/drive-files.mjs +54 -0
- package/google-workspace/lib/form-destination-settings.mjs +72 -0
- package/google-workspace/lib/http.mjs +168 -0
- package/google-workspace/lib/picker-flow.mjs +171 -0
- package/google-workspace/lib/picker-selection.mjs +107 -0
- package/google-workspace/lib/product-state.mjs +93 -0
- package/google-workspace/lib/request-policy.mjs +163 -0
- package/google-workspace/lib/sheets-a1.mjs +79 -0
- package/google-workspace/lib/sheets-admin-csv.mjs +82 -0
- package/google-workspace/lib/sheets-admin-request-kinds.mjs +183 -0
- package/google-workspace/lib/sheets-client.mjs +286 -0
- package/google-workspace/lib/surface-actions.mjs +107 -0
- package/google-workspace/mcp.json +4 -0
- package/google-workspace/plugin.json +77 -0
- package/google-workspace/products.json +12 -0
- package/google-workspace/skills/google-workspace/SKILL.md +82 -0
- package/google-workspace/skills/google-workspace/references/connection.md +102 -0
- package/google-workspace/skills/google-workspace/references/form-delivery.md +50 -0
- package/google-workspace/skills/google-workspace/references/sheets.md +149 -0
- package/google-workspace/skills/google-workspace/references/troubleshooting.md +31 -0
- package/google-workspace/tools/account-info.mjs +18 -0
- package/google-workspace/tools/sheets/README.md +54 -0
- package/google-workspace/tools/sheets/copy-tab.mjs +23 -0
- package/google-workspace/tools/sheets/copy.mjs +23 -0
- package/google-workspace/tools/sheets/create.mjs +35 -0
- package/google-workspace/tools/sheets/find-replace.mjs +38 -0
- package/google-workspace/tools/sheets/find.mjs +52 -0
- package/google-workspace/tools/sheets/get-by-data-filter.mjs +23 -0
- package/google-workspace/tools/sheets/get.mjs +22 -0
- package/google-workspace/tools/sheets/values-append.mjs +27 -0
- package/google-workspace/tools/sheets/values-batch-clear.mjs +20 -0
- package/google-workspace/tools/sheets/values-batch-get.mjs +20 -0
- package/google-workspace/tools/sheets/values-batch-update.mjs +29 -0
- package/google-workspace/tools/sheets/values-by-data-filter.mjs +62 -0
- package/google-workspace/tools/sheets/values-clear.mjs +20 -0
- package/google-workspace/tools/sheets/values-get.mjs +21 -0
- package/google-workspace/tools/sheets/values-update.mjs +26 -0
- package/google-workspace/tools/sheets-admin/README.md +45 -0
- package/google-workspace/tools/sheets-admin/basic-filter.mjs +32 -0
- package/google-workspace/tools/sheets-admin/batch-update.mjs +25 -0
- package/google-workspace/tools/sheets-admin/developer-metadata.mjs +28 -0
- package/google-workspace/tools/sheets-admin/export-csv.mjs +67 -0
- package/google-workspace/tools/sheets-admin/filter-views.mjs +38 -0
- package/google-workspace/tools/sheets-admin/format.mjs +70 -0
- package/google-workspace/tools/sheets-admin/named-ranges.mjs +42 -0
- package/google-workspace/tools/sheets-admin/open-existing.mjs +34 -0
- package/google-workspace/tools/sheets-admin/protected-ranges-get.mjs +18 -0
- package/google-workspace/tools/sheets-admin/sort.mjs +18 -0
- package/google-workspace/tools/sheets-admin/tab-modify.mjs +36 -0
- package/google-workspace/tovu-admin-surfaces.json +63 -0
- package/google-workspace/tovu-agent-tools.json +1178 -0
- package/google-workspace/tovu-connections.json +35 -0
- package/google-workspace/tovu-form-destinations.json +10 -0
- package/google-workspace/ui/README.md +73 -0
- package/google-workspace/ui/connection-panel.handler.mjs +167 -0
- package/google-workspace/ui/connection-panel.html +17 -0
- package/google-workspace/ui/connection-panel.mjs +189 -0
- package/google-workspace/ui/connection-view.mjs +120 -0
- package/google-workspace/ui/destination-panel.handler.mjs +106 -0
- package/google-workspace/ui/destination-panel.html +17 -0
- package/google-workspace/ui/destination-panel.mjs +150 -0
- package/google-workspace/ui/dom.mjs +140 -0
- package/google-workspace/ui/panel.css +170 -0
- package/google-workspace/ui/picker-flow.mjs +87 -0
- package/google-workspace/ui/picker-view.mjs +44 -0
- package/google-workspace/ui/picker.handler.mjs +56 -0
- package/google-workspace/ui/picker.html +17 -0
- package/google-workspace/ui/picker.mjs +88 -0
- package/google-workspace/ui/surface-client.mjs +227 -0
- package/higgsfield-media/mcp.json +9 -0
- package/higgsfield-media/plugin.json +109 -0
- package/higgsfield-media/skills/higgsfield-media/SKILL.md +325 -0
- package/higgsfield-media/skills/higgsfield-media/references/failure-modes.md +116 -0
- package/higgsfield-media/skills/higgsfield-media/references/models-and-plan-gates.md +142 -0
- package/jev/mcp.json +4 -0
- package/jev/plugin.json +44 -0
- package/jev/skills/jev/SKILL.md +308 -0
- package/jev/skills/jev/references/cookbooks.md +197 -0
- package/jev/skills/jev/references/jaggedness.md +135 -0
- package/jev/skills/jev/references/javascript-sdk.md +146 -0
- package/jev/skills/jev/references/patterns.md +123 -0
- package/jev/skills/jev/references/primitives-reference.md +187 -0
- package/langfuse/plugin.json +22 -0
- package/meilisearch/mcp.json +4 -0
- package/meilisearch/plugin.json +54 -0
- package/meilisearch/providers/meilisearch.mjs +651 -0
- package/meilisearch/skills/meilisearch/SKILL.md +43 -0
- package/meilisearch/tovu-search-providers.json +10 -0
- package/netlify/NOTES.md +13 -0
- package/netlify/mcp.json +4 -0
- package/netlify/plugin.json +47 -0
- package/netlify/skills/netlify/SKILL.md +39 -0
- package/netlify/targets/netlify.mjs +384 -0
- package/netlify/tovu-consent.json +6 -0
- package/netlify/tovu-deploy-targets.json +163 -0
- package/opentelemetry/mcp.json +4 -0
- package/opentelemetry/plugin.json +52 -0
- package/opentelemetry/skills/opentelemetry/SKILL.md +39 -0
- package/package.json +161 -6
- package/posthog/mcp.json +4 -0
- package/posthog/plugin.json +46 -0
- package/posthog/skills/posthog/SKILL.md +36 -0
- package/railway/deploy-configs/railway.mjs +135 -0
- package/railway/deploy-ops/railway.mjs +167 -0
- package/railway/mcp.json +4 -0
- package/railway/plugin.json +45 -0
- package/railway/skills/railway/SKILL.md +66 -0
- package/railway/tovu-deploy-configs.json +16 -0
- package/railway/tovu-deploy-ops.json +11 -0
- package/render/deploy-configs/render.mjs +141 -0
- package/render/deploy-ops/render.mjs +152 -0
- package/render/mcp.json +4 -0
- package/render/plugin.json +47 -0
- package/render/skills/render/SKILL.md +63 -0
- package/render/tovu-deploy-configs.json +16 -0
- package/render/tovu-deploy-ops.json +11 -0
- package/resend/NOTES.md +52 -0
- package/resend/adapters/resend.mjs +161 -0
- package/resend/lib/domains.mjs +137 -0
- package/resend/lib/errors.mjs +25 -0
- package/resend/lib/feedback.mjs +118 -0
- package/resend/mcp.json +13 -0
- package/resend/plugin.json +192 -0
- package/resend/skills/resend/SKILL.md +107 -0
- package/resend/tovu-mail-adapters.json +22 -0
- package/s3-compatible/NOTES.md +11 -0
- package/s3-compatible/mcp.json +4 -0
- package/s3-compatible/object-stores/s3-compatible.mjs +287 -0
- package/s3-compatible/plugin.json +56 -0
- package/s3-compatible/skills/s3-compatible/SKILL.md +39 -0
- package/s3-compatible/targets/s3-compatible.mjs +1054 -0
- package/s3-compatible/tovu-deploy-targets.json +432 -0
- package/s3-compatible/tovu-object-stores.json +29 -0
- package/sentry/mcp.json +4 -0
- package/sentry/plugin.json +49 -0
- package/sentry/skills/sentry/SKILL.md +34 -0
- package/shopify/mcp.json +4 -0
- package/shopify/plugin.json +22 -0
- package/shopify/skills/shopify/SKILL.md +103 -0
- package/shopify/skills/shopify/references/catalog-sync.md +74 -0
- package/shopify/skills/shopify/references/domains.md +26 -0
- package/shopify/skills/shopify/references/live-storefront.md +34 -0
- package/shopify/skills/shopify/references/storefront-snippets.md +21 -0
- package/site-compliance/mcp.json +4 -0
- package/site-compliance/plugin.json +34 -0
- package/site-compliance/skills/site-compliance/SKILL.md +220 -0
- package/site-compliance/skills/site-compliance/references/ccpa-cpra.md +115 -0
- package/site-compliance/skills/site-compliance/references/gdpr-eu-uk.md +143 -0
- package/site-compliance/skills/site-compliance/references/wcag-2-2.md +129 -0
- package/site-import/mcp.json +4 -0
- package/site-import/plugin.json +41 -0
- package/site-import/skills/site-import/SKILL.md +232 -0
- package/site-import/skills/site-import/references/content-mapping.md +166 -0
- package/site-import/skills/site-import/references/discovery-and-classification.md +117 -0
- package/site-import/skills/site-import/references/theme-extraction.md +34 -0
- package/slide-design/com.anthropic.claude-code/hooks/hooks.json +4 -0
- package/slide-design/mcp.json +4 -0
- package/slide-design/plugin.json +26 -0
- package/slide-design/skills/slide-design/SKILL.md +103 -0
- package/slide-design/skills/slide-design/references/charts-and-exhibits.md +100 -0
- package/slide-design/skills/slide-design/references/html-deck.md +175 -0
- package/slide-design/skills/slide-design/references/layout-and-style.md +131 -0
- package/slide-design/skills/slide-design/references/review.md +93 -0
- package/slide-design/skills/slide-design/references/storyline.md +118 -0
- package/stripe/plugin.json +22 -0
- package/supabase/mcp.json +26 -0
- package/supabase/plugin.json +78 -0
- package/supabase/skills/supabase/SKILL.md +169 -0
- package/supabase/skills/supabase/references/failure-modes.md +101 -0
- package/ui-ux-design/CHANGELOG.md +28 -0
- package/ui-ux-design/LICENSE +201 -0
- package/ui-ux-design/NOTICE +14 -0
- package/ui-ux-design/README.md +62 -0
- package/ui-ux-design/com.anthropic.claude-code/hooks/hooks.json +4 -0
- package/ui-ux-design/mcp.json +4 -0
- package/ui-ux-design/plugin.json +26 -0
- package/ui-ux-design/skills/frontend-accessibility/SKILL.md +60 -0
- package/ui-ux-design/skills/gstack-design/SKILL.md +53 -0
- package/ui-ux-design/skills/gstack-design/references/consultation.md +30 -0
- package/ui-ux-design/skills/gstack-design/references/html.md +29 -0
- package/ui-ux-design/skills/gstack-design/references/review.md +28 -0
- package/ui-ux-design/skills/gstack-design/references/shotgun.md +33 -0
- package/ui-ux-design/skills/gstack-design/references/upstream-notes.md +23 -0
- package/ui-ux-design/skills/interface-design/ORIGINAL.md +16 -0
- package/ui-ux-design/skills/interface-design/SKILL.md +41 -0
- package/ui-ux-design/skills/interface-design/references/commands/audit.md +40 -0
- package/ui-ux-design/skills/interface-design/references/commands/critique.md +21 -0
- package/ui-ux-design/skills/interface-design/references/commands/extract.md +34 -0
- package/ui-ux-design/skills/interface-design/references/commands/init.md +31 -0
- package/ui-ux-design/skills/interface-design/references/commands/status.md +22 -0
- package/ui-ux-design/skills/interface-design/references/examples/system-precision.md +29 -0
- package/ui-ux-design/skills/interface-design/references/examples/system-warmth.md +29 -0
- package/ui-ux-design/skills/interface-design/references/system-template.md +57 -0
- package/ui-ux-design/skills/shadcn-ui/README.md +244 -0
- package/ui-ux-design/skills/shadcn-ui/SKILL.md +333 -0
- package/ui-ux-design/skills/shadcn-ui/examples/auth-layout.tsx +177 -0
- package/ui-ux-design/skills/shadcn-ui/examples/data-table.tsx +313 -0
- package/ui-ux-design/skills/shadcn-ui/examples/form-pattern.tsx +177 -0
- package/ui-ux-design/skills/shadcn-ui/resources/component-catalog.md +481 -0
- package/ui-ux-design/skills/shadcn-ui/resources/customization-guide.md +516 -0
- package/ui-ux-design/skills/shadcn-ui/resources/migration-guide.md +463 -0
- package/ui-ux-design/skills/shadcn-ui/resources/setup-guide.md +412 -0
- package/ui-ux-design/skills/shadcn-ui/scripts/verify-setup.sh +134 -0
- package/ui-ux-design/skills/ui-ux-design/SKILL.md +171 -0
- package/ui-ux-design/skills/ui-ux-design/references/brand-and-voice.md +78 -0
- package/ui-ux-design/skills/ui-ux-design/references/chrome-mobile-test-matrix.md +137 -0
- package/ui-ux-design/skills/ui-ux-design/references/components-and-states.md +112 -0
- package/ui-ux-design/skills/ui-ux-design/references/delight-and-motion.md +54 -0
- package/ui-ux-design/skills/ui-ux-design/references/foundations.md +145 -0
- package/ui-ux-design/skills/ui-ux-design/references/inclusive-ai-imagery.md +57 -0
- package/ui-ux-design/skills/ui-ux-design/references/kole-jain-uiux-concepts.md +104 -0
- package/ui-ux-design/skills/ui-ux-design/references/kuinetic-reference.md +184 -0
- package/ui-ux-design/skills/ui-ux-design/references/openai-frontend-skill.md +139 -0
- package/ui-ux-design/skills/ui-ux-design/references/premium-ui.md +55 -0
- package/ui-ux-design/skills/ui-ux-design/references/research-and-validation.md +88 -0
- package/ui-ux-design/skills/ui-ux-design/references/sam-crawford-premium-websites.md +103 -0
- package/ui-ux-design/skills/ui-ux-design/references/self-made-web-designer-core-skills.md +119 -0
- package/ui-ux-design/skills/ui-ux-design/references/ui-animation.md +173 -0
- package/ui-ux-design/skills/ui-ux-design/references/ui-loop.md +234 -0
- package/ui-ux-design/skills/ui-ux-design/references/visual-storytelling.md +54 -0
- package/ui-ux-design/skills/vercel-web-design-guidelines/SKILL.md +39 -0
- package/ui-ux-design/skills/web-compliance/SKILL.md +134 -0
- package/ui-ux-design/skills/web-compliance/references/audit-evidence.md +59 -0
- package/vercel/NOTES.md +23 -0
- package/vercel/mcp.json +4 -0
- package/vercel/object-stores/vercel-blob.mjs +147 -0
- package/vercel/plugin.json +47 -0
- package/vercel/skills/vercel/SKILL.md +48 -0
- package/vercel/targets/vercel.mjs +586 -0
- package/vercel/tovu-consent.json +6 -0
- package/vercel/tovu-deploy-targets.json +219 -0
- package/vercel/tovu-object-stores.json +10 -0
- package/wordpress/NOTES.md +33 -0
- package/wordpress/plugin.json +30 -0
- package/wordpress/skills/wordpress/SKILL.md +43 -0
- package/wordpress/skills/wordpress/references/free-site-redirect.md +46 -0
- package/wordpress/skills/wordpress/references/subdomain.md +66 -0
|
@@ -0,0 +1,586 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Vercel deploy target of the `vercel` Agent Plugin.
|
|
3
|
+
*
|
|
4
|
+
* Ported from `@jini-ai/devops` `src/deploy/vercel.ts` at Jini commit 28f67f9a: the body of
|
|
5
|
+
* {@link bindVercel} is that source with its TypeScript types stripped, same Vercel calls, error texts,
|
|
6
|
+
* redirect guard and Deployment Protection detector (`isVercelProtectedResponse`). Changes:
|
|
7
|
+
*
|
|
8
|
+
* - Network, timeouts, poll delay, reachability, naming, the redirect guard and `DeployError` come
|
|
9
|
+
* from the host kit the host injects (see {@link bindVercel}),
|
|
10
|
+
* bound once per target by {@link bindVercel}: an installed plugin has no npm dependencies.
|
|
11
|
+
* - `responseHeaders` (the host's security header set) is rendered into a root `vercel.json` here,
|
|
12
|
+
* which the first host (Tovu) used to add in its core.
|
|
13
|
+
* - Files are uploaded one per request to `POST /v2/files` (by SHA-1) and the deployment references
|
|
14
|
+
* them as `{ file, sha, size }`, the way Vercel's CLI does (2026-10-11). The devops source sent every
|
|
15
|
+
* file inline, base64, in the one `/v13/deployments` body, which Vercel refuses past 10 MB
|
|
16
|
+
* ("Request body too large"): any real site with photos.
|
|
17
|
+
*
|
|
18
|
+
* Plain JS by owner decision (2026-09-29).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { createHash, randomInt } from "node:crypto";
|
|
22
|
+
|
|
23
|
+
export const VERCEL_TARGET_ID = "vercel";
|
|
24
|
+
|
|
25
|
+
/** `/v2/files` uploads in flight at once per publish: a few hundred files finish in reasonable time
|
|
26
|
+
* without tripping Vercel's rate limit. */
|
|
27
|
+
const VERCEL_UPLOAD_CONCURRENCY = 5;
|
|
28
|
+
/** Tries per file upload; a 429, a 5xx or a dropped connection is retried, anything else fails the
|
|
29
|
+
* publish at once. */
|
|
30
|
+
const VERCEL_UPLOAD_ATTEMPTS = 3;
|
|
31
|
+
|
|
32
|
+
/** The root file Vercel reads response-header rules from. */
|
|
33
|
+
const VERCEL_CONFIG_FILE = "vercel.json";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Renders a header set as a `vercel.json` with one catch-all header rule. Byte-identical to what
|
|
37
|
+
* the first host (Tovu) rendered in its core before this moved here.
|
|
38
|
+
*
|
|
39
|
+
* @param {Readonly<Record<string, string>>} headers
|
|
40
|
+
* @returns {string}
|
|
41
|
+
*/
|
|
42
|
+
function renderVercelConfig(headers) {
|
|
43
|
+
const rule = { source: "/(.*)", headers: Object.entries(headers).map(([key, value]) => ({ key, value })) };
|
|
44
|
+
return `${JSON.stringify({ headers: [rule] }, null, 2)}\n`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The file set actually deployed: `files`, plus a root `vercel.json` rendered from
|
|
49
|
+
* `responseHeaders`, which REPLACES a site-supplied one (the header set is the host's policy).
|
|
50
|
+
*
|
|
51
|
+
* @param {Array<{ file: string, data: string | Uint8Array, contentType?: string }>} files
|
|
52
|
+
* @param {Readonly<Record<string, string>> | undefined} responseHeaders
|
|
53
|
+
*/
|
|
54
|
+
function withVercelConfigFile(files, responseHeaders) {
|
|
55
|
+
if (!responseHeaders || Object.keys(responseHeaders).length === 0) return files;
|
|
56
|
+
return [...files.filter((file) => file.file !== VERCEL_CONFIG_FILE), { file: VERCEL_CONFIG_FILE, data: renderVercelConfig(responseHeaders) }];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Content-addresses a file set for Vercel: the deployment's `files` entries (`{ file, sha, size }`,
|
|
61
|
+
* no data) and one upload body per distinct SHA-1, so identical files upload once.
|
|
62
|
+
*
|
|
63
|
+
* @param {Array<{ file: string, data: string | Uint8Array }>} files
|
|
64
|
+
* @returns {{ refs: Array<{ file: string, sha: string, size: number }>, blobs: Map<string, Buffer> }}
|
|
65
|
+
*/
|
|
66
|
+
function vercelFileManifest(files) {
|
|
67
|
+
const blobs = new Map();
|
|
68
|
+
const refs = files.map((f) => {
|
|
69
|
+
const data = Buffer.from(f.data);
|
|
70
|
+
// Vercel requires SHA-1 in file references and x-vercel-digest upload headers.
|
|
71
|
+
// This is a wire-format requirement; substituting SHA-256 would break uploads.
|
|
72
|
+
const sha = createHash("sha1").update(data).digest("hex");
|
|
73
|
+
blobs.set(sha, data);
|
|
74
|
+
return { file: f.file, sha, size: data.length };
|
|
75
|
+
});
|
|
76
|
+
return { refs, blobs };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Binds the ported Vercel code to one host kit.
|
|
81
|
+
*
|
|
82
|
+
* @param {{ fetch(url: string, init: RequestInit, options: { timeoutMs: number }): Promise<Response>,
|
|
83
|
+
* timeouts: { QUICK: number, DEPLOY: number, UPLOAD: number }, sleep(ms: number): Promise<void> } & Record<string, any>} kit
|
|
84
|
+
* The host's deploy kit in the positional ABI installed modules are written against (devops'
|
|
85
|
+
* `DeployHostKit` helpers, called positionally; `DeployError` is `new (message, status?, details?)`).
|
|
86
|
+
* @returns The target class (constructed with `{ token, teamId?, teamSlug? }`, as in devops) and the
|
|
87
|
+
* protected-response detector.
|
|
88
|
+
*/
|
|
89
|
+
export function bindVercel(kit) {
|
|
90
|
+
const { fetch: fetchWithTimeout, timeouts: FETCH_TIMEOUT_MS, sleep, DeployError, checkDeploymentUrl, normalizeDeploymentUrl, waitForReachableDeploymentUrl, safeProjectLabel, assertNotRedirected, redirectGuardInit } = kit;
|
|
91
|
+
const VERCEL_TARGET_ID = 'vercel';
|
|
92
|
+
const VERCEL_API = 'https://api.vercel.com';
|
|
93
|
+
const VERCEL_PROTECTED_MESSAGE = 'Deployment is protected by Vercel. Disable Deployment Protection or use a custom domain to make this link public.';
|
|
94
|
+
/**
|
|
95
|
+
* Heuristically detects Vercel's own Deployment Protection auth wall from a
|
|
96
|
+
* 401 response, so `checkReachability` can report `status: 'protected'`
|
|
97
|
+
* (an actionable, non-error outcome) instead of `reachable: false` with a
|
|
98
|
+
* generic message.
|
|
99
|
+
*
|
|
100
|
+
* @param resp - The HTTP response received from probing a deployment URL.
|
|
101
|
+
* @param body - The response body (only read for 401s by the caller).
|
|
102
|
+
* @returns `true` if the response looks like Vercel's SSO/auth gate.
|
|
103
|
+
* @complexity O(1) — a handful of regex tests over already-fetched strings.
|
|
104
|
+
* @overallScore 100/100
|
|
105
|
+
*/
|
|
106
|
+
function isVercelProtectedResponse(resp, body = '') {
|
|
107
|
+
const { server, setCookie } = protectionHeaders({ resp });
|
|
108
|
+
const text = String(body || '');
|
|
109
|
+
if (/vercel/i.test(server) || /_vercel_sso_nonce/i.test(setCookie)) return true;
|
|
110
|
+
return /Authentication Required|Vercel Authentication|vercel\.com\/sso-api/i.test(text);
|
|
111
|
+
}
|
|
112
|
+
function protectionHeaders({ resp }, _options = {}) {
|
|
113
|
+
return { server: resp.headers?.get?.('server') || '', setCookie: resp.headers?.get?.('set-cookie') || '' };
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* `DeployTarget` adapter for Vercel's v13 deployments API. Publishes a
|
|
117
|
+
* caller-supplied file set as a single deployment, polls until Vercel
|
|
118
|
+
* reports a terminal `readyState`, then waits for the resulting URL to
|
|
119
|
+
* become publicly reachable.
|
|
120
|
+
*
|
|
121
|
+
* Genericized from the origin product's daemon deploy module's
|
|
122
|
+
* `deployToVercel`: the origin's product-specific deployment-name
|
|
123
|
+
* convention is replaced by `input.projectName` (sanitized the same way);
|
|
124
|
+
* token/team config is supplied by the caller instead of being read from a
|
|
125
|
+
* product-owned local config file — persistence of that config is now the
|
|
126
|
+
* caller's concern, not this package's.
|
|
127
|
+
*/
|
|
128
|
+
class VercelDeployTarget {
|
|
129
|
+
config;
|
|
130
|
+
id = VERCEL_TARGET_ID;
|
|
131
|
+
resolvedProjectName;
|
|
132
|
+
resolvedPublicUrl;
|
|
133
|
+
constructor(config) {
|
|
134
|
+
this.config = config;
|
|
135
|
+
}
|
|
136
|
+
/** Resolve the host-assigned production domain before rendering. A first publish creates
|
|
137
|
+
* the same project publish() would create, then reads its actual alias (names may collide).
|
|
138
|
+
* @complexity At most three bounded API calls, at most 100 domain records; no file uploads.
|
|
139
|
+
* @throws DeployError for refused/ambiguous inventories; never fabricates a public URL.
|
|
140
|
+
*/
|
|
141
|
+
async resolvePublicUrl({ projectName }, _options = {}) {
|
|
142
|
+
this.resolvedProjectName = safeVercelProjectName(projectName);
|
|
143
|
+
this.resolvedPublicUrl = await resolveVercelPublicUrl({ config: this.config, name: this.resolvedProjectName });
|
|
144
|
+
return this.resolvedPublicUrl;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Publishes `input.files` as a new Vercel deployment and waits for the
|
|
148
|
+
* resulting URL to be reachable.
|
|
149
|
+
*
|
|
150
|
+
* @param input - File set plus a caller-chosen project name label.
|
|
151
|
+
* @returns The published deployment's URL/status once Vercel has a
|
|
152
|
+
* terminal `readyState` (or the poll budget is exhausted).
|
|
153
|
+
* @throws {DeployError} If `config.token` is missing, or Vercel's API
|
|
154
|
+
* rejects the deployment request or reports `readyState: 'ERROR'`.
|
|
155
|
+
* @complexity O(files) to hash and upload them, plus a bounded poll loop
|
|
156
|
+
* (`pollVercelDeployment`) and the reachability wait's own bound.
|
|
157
|
+
* @overallScore 100/100
|
|
158
|
+
*/
|
|
159
|
+
async publish(input) {
|
|
160
|
+
if (!this.config.token) {
|
|
161
|
+
throw new DeployError('Vercel token is required.', 400);
|
|
162
|
+
}
|
|
163
|
+
const { refs, blobs } = vercelFileManifest(withVercelConfigFile(input.files, input.responseHeaders));
|
|
164
|
+
await uploadVercelFiles(this.config, blobs, [...blobs.keys()]);
|
|
165
|
+
const name = this.resolvedProjectName ?? safeVercelProjectName(input.projectName);
|
|
166
|
+
const created = await createUploadedVercelDeployment({ config: this.config, name, refs, blobs });
|
|
167
|
+
const fallbackId = typeof created.uid === 'string' ? created.uid : undefined;
|
|
168
|
+
const deploymentId = typeof created.id === 'string' ? created.id : fallbackId;
|
|
169
|
+
const initialUrl = deploymentUrl(created);
|
|
170
|
+
const ready = await readyVercelDeployment({ config: this.config, deploymentId, created });
|
|
171
|
+
const link = await publicVercelLink({ name, ready, created, initialUrl, publicUrl: this.resolvedPublicUrl });
|
|
172
|
+
return {
|
|
173
|
+
targetId: this.id,
|
|
174
|
+
url: link.url || deploymentUrl(ready) || initialUrl,
|
|
175
|
+
...(deploymentId ? { deploymentId } : {}),
|
|
176
|
+
status: link.status,
|
|
177
|
+
statusMessage: link.statusMessage,
|
|
178
|
+
...(link.reachableAt !== undefined ? { reachableAt: link.reachableAt } : {}),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
/** Probes `url`, recognizing Vercel's own Deployment Protection auth wall. */
|
|
182
|
+
async checkReachability(url) {
|
|
183
|
+
return checkDeploymentUrl(url, {
|
|
184
|
+
detectProtected: isVercelProtectedResponse,
|
|
185
|
+
protectedMessage: VERCEL_PROTECTED_MESSAGE,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/** Read the provider's production-only, verified, nonredirecting domains. Refuse a partial
|
|
190
|
+
* inventory rather than select a different canonical URL on different pages of the result.
|
|
191
|
+
* @complexity One bounded request; response limited to 100 records by the provider.
|
|
192
|
+
*/
|
|
193
|
+
async function readPublicDomains({ config, name }, _options = {}) {
|
|
194
|
+
const url = new URL(`${VERCEL_API}/v9/projects/${encodeURIComponent(name)}/domains${vercelTeamQuery(config)}`);
|
|
195
|
+
for (const [key, value] of Object.entries({ production: 'true', redirects: 'false', verified: 'true', limit: '100' })) url.searchParams.set(key, value);
|
|
196
|
+
return fetchWithTimeout(url.href, redirectGuardInit({ method: 'GET', headers: { Authorization: `Bearer ${config.token}` } }), { timeoutMs: FETCH_TIMEOUT_MS.QUICK });
|
|
197
|
+
}
|
|
198
|
+
/** Provision only after an authoritative not-found response; permission/network failures never create.
|
|
199
|
+
* @complexity One POST with the same project name and framework as the existing publish request.
|
|
200
|
+
*/
|
|
201
|
+
async function createPublicProject({ config, name }, _options = {}) {
|
|
202
|
+
const response = await fetchWithTimeout(`${VERCEL_API}/v11/projects${vercelTeamQuery(config)}`, redirectGuardInit({
|
|
203
|
+
method: 'POST', headers: { Authorization: `Bearer ${config.token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ name, framework: null }),
|
|
204
|
+
}), { timeoutMs: FETCH_TIMEOUT_MS.QUICK });
|
|
205
|
+
assertNotRedirected(response, 'Vercel project creation');
|
|
206
|
+
const body = await readVercelJson(response);
|
|
207
|
+
if (!response.ok) throw vercelError(body, response.status);
|
|
208
|
+
}
|
|
209
|
+
/** Actual domain evidence, never a name-derived hostname; preparation stays inside this provider.
|
|
210
|
+
* @complexity At most three API requests and O(domains), capped at 100; errors propagate.
|
|
211
|
+
*/
|
|
212
|
+
async function resolveVercelPublicUrl({ config, name }, _options = {}) {
|
|
213
|
+
if (!config.token) throw new DeployError('Vercel token is required.', 400);
|
|
214
|
+
let response = await readPublicDomains({ config, name });
|
|
215
|
+
if (response.status === 404) {
|
|
216
|
+
await response.body?.cancel();
|
|
217
|
+
await createPublicProject({ config, name });
|
|
218
|
+
response = await readPublicDomains({ config, name });
|
|
219
|
+
}
|
|
220
|
+
assertNotRedirected(response, 'Vercel public domain lookup');
|
|
221
|
+
const body = await readVercelJson(response);
|
|
222
|
+
if (!response.ok) throw vercelError(body, response.status);
|
|
223
|
+
return publicDomainUrl({ body, name });
|
|
224
|
+
}
|
|
225
|
+
/** Select a complete, bounded inventory. Missing evidence is a refusal, never a guessed alias.
|
|
226
|
+
* @complexity O(domains), capped at 100 records; pure, throws DeployError for missing evidence.
|
|
227
|
+
*/
|
|
228
|
+
function publicDomainUrl({ body, name }, _options = {}) {
|
|
229
|
+
const entries = Array.isArray(body.domains) ? body.domains : [];
|
|
230
|
+
if (body.pagination?.next != null || entries.length > 100) throw new DeployError('Public domain inventory exceeds 100 domains; choose a smaller destination.', 400);
|
|
231
|
+
const domains = entries.filter(domain => domain.verified === true && !domain.redirect && !domain.gitBranch && typeof domain.name === 'string');
|
|
232
|
+
const domain = domains.find(domain => domain.name === `${name}.vercel.app`) ?? domains[0];
|
|
233
|
+
if (!domain) throw new DeployError('Vercel did not report a verified public domain for this project.', 400);
|
|
234
|
+
return `https://${domain.name}`;
|
|
235
|
+
}
|
|
236
|
+
async function createUploadedVercelDeployment({ config, name, refs, blobs }, _options = {}) {
|
|
237
|
+
// `target: "production"`: without it Vercel makes a preview deployment, and only a project's
|
|
238
|
+
// FIRST deployment takes the production alias, so every later publish never reached the
|
|
239
|
+
// public address (2026-10-11). A publish is meant to update the live site.
|
|
240
|
+
const body = JSON.stringify({ name, files: refs, target: 'production', projectSettings: { framework: null } });
|
|
241
|
+
let createResp = await createVercelDeployment(config, body);
|
|
242
|
+
let created = await readVercelJson(createResp);
|
|
243
|
+
const missing = created?.error?.code === 'missing_files' ? created.error.missing : undefined;
|
|
244
|
+
if (!createResp.ok && Array.isArray(missing)) {
|
|
245
|
+
// Vercel did not keep (or never got) some uploads: send exactly what it names, then create
|
|
246
|
+
// once more. A second `missing_files` is reported like any other create failure.
|
|
247
|
+
await uploadVercelFiles(config, blobs, missing.filter((sha) => blobs.has(sha)));
|
|
248
|
+
createResp = await createVercelDeployment(config, body);
|
|
249
|
+
created = await readVercelJson(createResp);
|
|
250
|
+
}
|
|
251
|
+
if (!createResp.ok)
|
|
252
|
+
throw vercelError(created, createResp.status);
|
|
253
|
+
return created;
|
|
254
|
+
}
|
|
255
|
+
async function readyVercelDeployment({ config, deploymentId, created }, _options = {}) {
|
|
256
|
+
const ready = deploymentId ? await pollVercelDeployment(config, deploymentId) : created;
|
|
257
|
+
if (ready?.readyState === 'ERROR') {
|
|
258
|
+
const readyError = ready?.error;
|
|
259
|
+
// `ready ?? undefined` — the `undefined` side is unreachable: this line
|
|
260
|
+
// only runs when `ready?.readyState === 'ERROR'` just matched, which
|
|
261
|
+
// requires `ready` itself to already be a real object (optional
|
|
262
|
+
// chaining on a null/undefined `ready` would make the whole comparison
|
|
263
|
+
// `undefined === 'ERROR'`, i.e. false, skipping this block entirely).
|
|
264
|
+
// So by the time `ready ?? undefined` evaluates, `ready` is always
|
|
265
|
+
// truthy and the `??` is a no-op at runtime. Kept (not simplified to
|
|
266
|
+
// just `ready`) purely for type-level reasons: `ready`'s static type is
|
|
267
|
+
// `JsonObject | null` (from `pollVercelDeployment`'s return type /
|
|
268
|
+
// `created`'s), and `DeployError`'s `details` parameter accepts
|
|
269
|
+
// `JsonObject | string | undefined` — not `null` — so this coalesce is
|
|
270
|
+
// a compile-time-required null-to-undefined conversion that TypeScript's
|
|
271
|
+
// control-flow analysis cannot narrow away just from the optional-chained
|
|
272
|
+
// comparison above. See packages/devops/source-map.md's 2026-07-22
|
|
273
|
+
// addition for the full re-derivation.
|
|
274
|
+
throw new DeployError(readyError?.message || 'Vercel deployment failed.', 502, ready ?? undefined);
|
|
275
|
+
}
|
|
276
|
+
return ready;
|
|
277
|
+
}
|
|
278
|
+
async function publicVercelLink({ name, ready, created, initialUrl, publicUrl }, _options = {}) {
|
|
279
|
+
const aliases = deploymentUrlCandidates(`${name}.vercel.app`, ready, created);
|
|
280
|
+
const candidates = publicUrl ? [publicUrl, ...aliases.filter(alias => alias !== publicUrl)] : aliases;
|
|
281
|
+
let link = await waitForReachableDeploymentUrl(candidates.length ? candidates : [initialUrl], {
|
|
282
|
+
providerLabel: 'Vercel',
|
|
283
|
+
detectProtected: isVercelProtectedResponse,
|
|
284
|
+
protectedMessage: VERCEL_PROTECTED_MESSAGE,
|
|
285
|
+
});
|
|
286
|
+
// The kit counts any 3xx as reachable, but Deployment Protection answers a per-deployment
|
|
287
|
+
// URL with a redirect to Vercel's login, which is not a public link.
|
|
288
|
+
if (link.status === 'ready' && await redirectsToVercelLogin(link.url)) {
|
|
289
|
+
link = { status: 'protected', url: link.url, statusMessage: VERCEL_PROTECTED_MESSAGE };
|
|
290
|
+
}
|
|
291
|
+
return link;
|
|
292
|
+
}
|
|
293
|
+
/** One HEAD, redirects not followed: does `url` send visitors to Vercel's SSO/login wall? Any
|
|
294
|
+
* failure answers `false`, leaving the reachability result as it was. */
|
|
295
|
+
async function redirectsToVercelLogin(url) {
|
|
296
|
+
try {
|
|
297
|
+
const resp = await fetchWithTimeout(url, { method: 'HEAD', redirect: 'manual' }, { timeoutMs: FETCH_TIMEOUT_MS.QUICK });
|
|
298
|
+
await resp.arrayBuffer().catch(() => undefined);
|
|
299
|
+
return resp.status >= 300 && resp.status < 400 && /vercel\.com\/(sso-api|login)/i.test(resp.headers.get('location') || '');
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
async function createVercelDeployment(config, body) {
|
|
306
|
+
const resp = await fetchWithTimeout(`${VERCEL_API}/v13/deployments${vercelTeamQuery(config)}`, redirectGuardInit({
|
|
307
|
+
method: 'POST',
|
|
308
|
+
headers: {
|
|
309
|
+
Authorization: `Bearer ${config.token}`,
|
|
310
|
+
'Content-Type': 'application/json',
|
|
311
|
+
},
|
|
312
|
+
body,
|
|
313
|
+
}), { timeoutMs: FETCH_TIMEOUT_MS.UPLOAD });
|
|
314
|
+
assertNotRedirected(resp, 'Vercel deployment creation');
|
|
315
|
+
return resp;
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Uploads `shas` (keys of `blobs`) to `POST /v2/files`, {@link VERCEL_UPLOAD_CONCURRENCY} at a
|
|
319
|
+
* time. The first failure stops the remaining queue and rejects.
|
|
320
|
+
*
|
|
321
|
+
* @complexity O(n) requests, plus at most `VERCEL_UPLOAD_ATTEMPTS - 1` retries each.
|
|
322
|
+
*/
|
|
323
|
+
async function uploadVercelFiles(config, blobs, shas) {
|
|
324
|
+
const queue = [...shas];
|
|
325
|
+
const worker = async () => {
|
|
326
|
+
while (queue.length) {
|
|
327
|
+
const sha = queue.shift();
|
|
328
|
+
try {
|
|
329
|
+
await uploadVercelFile(config, sha, blobs.get(sha));
|
|
330
|
+
}
|
|
331
|
+
catch (err) {
|
|
332
|
+
queue.length = 0;
|
|
333
|
+
throw err;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
await Promise.all(Array.from({ length: Math.min(VERCEL_UPLOAD_CONCURRENCY, queue.length) }, worker));
|
|
338
|
+
}
|
|
339
|
+
function vercelUploadTransportError({ err }, _options = {}) {
|
|
340
|
+
// Node's fetch reports every transport failure as a bare "fetch failed"; the cause (a
|
|
341
|
+
// reset socket, a TLS or DNS error) is the only part that says what happened.
|
|
342
|
+
const cause = err?.cause?.message || err?.cause?.code;
|
|
343
|
+
const detail = cause ? ` (${cause})` : '';
|
|
344
|
+
return new DeployError(`Vercel file upload failed: ${err?.message ?? err}${detail}`, 502);
|
|
345
|
+
}
|
|
346
|
+
async function tryVercelFileUpload({ config, sha, data, attempt }, _options = {}) {
|
|
347
|
+
try {
|
|
348
|
+
return await fetchWithTimeout(`${VERCEL_API}/v2/files${vercelTeamQuery(config)}`, redirectGuardInit({
|
|
349
|
+
method: 'POST',
|
|
350
|
+
headers: {
|
|
351
|
+
Authorization: `Bearer ${config.token}`,
|
|
352
|
+
'Content-Type': 'application/octet-stream',
|
|
353
|
+
'Content-Length': String(data.length),
|
|
354
|
+
'x-vercel-digest': sha,
|
|
355
|
+
},
|
|
356
|
+
body: data,
|
|
357
|
+
}), { timeoutMs: FETCH_TIMEOUT_MS.UPLOAD });
|
|
358
|
+
}
|
|
359
|
+
catch (err) {
|
|
360
|
+
// A dropped connection is retried like a 5xx (the first real plannie publish lost one of
|
|
361
|
+
// ~200 uploads that way); a timeout is not, since each try already waited the full bound.
|
|
362
|
+
if (attempt < VERCEL_UPLOAD_ATTEMPTS && !/timed out/i.test(String(err?.message))) {
|
|
363
|
+
await sleep(1000 * attempt);
|
|
364
|
+
return undefined;
|
|
365
|
+
}
|
|
366
|
+
throw vercelUploadTransportError({ err });
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
async function uploadVercelFile(config, sha, data) {
|
|
370
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
371
|
+
const resp = await tryVercelFileUpload({ config, sha, data, attempt });
|
|
372
|
+
if (resp === undefined) continue;
|
|
373
|
+
assertNotRedirected(resp, 'Vercel file upload');
|
|
374
|
+
if (resp.ok) {
|
|
375
|
+
// Drain the small JSON ack so the connection goes back to the pool.
|
|
376
|
+
await resp.arrayBuffer().catch(() => undefined);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
if (attempt < VERCEL_UPLOAD_ATTEMPTS && (resp.status === 429 || resp.status >= 500)) {
|
|
380
|
+
await resp.arrayBuffer().catch(() => undefined);
|
|
381
|
+
await sleep(1000 * attempt);
|
|
382
|
+
} else {
|
|
383
|
+
throw vercelError(await readVercelJson(resp), resp.status);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Polls a just-created Vercel deployment until it reaches a terminal
|
|
389
|
+
* `readyState` (`READY`/`ERROR`) or the fixed 30-attempt budget is spent
|
|
390
|
+
* (~1s for the first 5 attempts, 2s thereafter — roughly a minute).
|
|
391
|
+
*
|
|
392
|
+
* A response this file cannot parse as JSON is treated as "keep polling,"
|
|
393
|
+
* not an error — same reasoning as `github-pages.ts`'s `pollGitHubPagesBuild`
|
|
394
|
+
* (fixed 2026-08-16 for the identical bug class): the deployment already
|
|
395
|
+
* exists server-side by the time this loop runs (the create call above
|
|
396
|
+
* already succeeded), so a transient/edge/empty poll response is exactly as
|
|
397
|
+
* uninformative as a stuck in-progress state, never a reason to report an
|
|
398
|
+
* already-created deployment as failed. Any other non-2xx response whose
|
|
399
|
+
* body DID parse still fails fast, unchanged.
|
|
400
|
+
*
|
|
401
|
+
* @complexity O(30) network round-trips, bounded and fixed regardless of input size.
|
|
402
|
+
* @overallScore 90/100
|
|
403
|
+
* @tradeoffs Fixed attempt/backoff constants (lifted verbatim from the OD
|
|
404
|
+
* origin) rather than caller-configurable ones — acceptable for now since
|
|
405
|
+
* `publish` has no caller-facing timeout knob yet either; flagged as a
|
|
406
|
+
* minor extensibility gap rather than a correctness issue.
|
|
407
|
+
*/
|
|
408
|
+
async function pollVercelDeployment(config, id) {
|
|
409
|
+
let last = null;
|
|
410
|
+
for (let i = 0; i < 30; i += 1) {
|
|
411
|
+
await sleep(i < 5 ? 1000 : 2000);
|
|
412
|
+
const resp = await fetchWithTimeout(`${VERCEL_API}/v13/deployments/${encodeURIComponent(id)}${vercelTeamQuery(config)}`, redirectGuardInit({ headers: { Authorization: `Bearer ${config.token}` } }), { timeoutMs: FETCH_TIMEOUT_MS.DEPLOY });
|
|
413
|
+
assertNotRedirected(resp, 'Vercel deployment status poll');
|
|
414
|
+
let json;
|
|
415
|
+
try {
|
|
416
|
+
json = await readVercelJson(resp);
|
|
417
|
+
}
|
|
418
|
+
catch {
|
|
419
|
+
// Nothing usable to report yet — keep polling within the same fixed budget
|
|
420
|
+
// rather than turning an ambiguous mid-poll hiccup into a reported failure.
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
if (!resp.ok)
|
|
424
|
+
throw vercelError(json, resp.status);
|
|
425
|
+
last = json;
|
|
426
|
+
if (json.readyState === 'READY' || json.readyState === 'ERROR')
|
|
427
|
+
return json;
|
|
428
|
+
}
|
|
429
|
+
return last;
|
|
430
|
+
}
|
|
431
|
+
function vercelTeamQuery(config) {
|
|
432
|
+
const params = new URLSearchParams();
|
|
433
|
+
if (config.teamId)
|
|
434
|
+
params.set('teamId', config.teamId);
|
|
435
|
+
else if (config.teamSlug)
|
|
436
|
+
params.set('slug', config.teamSlug);
|
|
437
|
+
else {
|
|
438
|
+
// Personal deployments need no team query parameter.
|
|
439
|
+
}
|
|
440
|
+
const s = params.toString();
|
|
441
|
+
return s ? `?${s}` : '';
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Derives a Vercel-safe project name from a caller-supplied label, falling
|
|
445
|
+
* back to a random id if the label sanitizes to nothing. Replaces the OD
|
|
446
|
+
* origin's hardcoded `od-${projectId}` convention.
|
|
447
|
+
*/
|
|
448
|
+
function safeVercelProjectName(projectName) {
|
|
449
|
+
return safeProjectLabel(projectName, 80) || `deploy-${randomInt(36 ** 8).toString(36).padStart(8, '0')}`;
|
|
450
|
+
}
|
|
451
|
+
async function readVercelJson(resp) {
|
|
452
|
+
try {
|
|
453
|
+
return (await resp.json());
|
|
454
|
+
}
|
|
455
|
+
catch {
|
|
456
|
+
throw new DeployError('Vercel returned a non-JSON response.', resp.status || 502);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
function vercelErrorField({ json, field }, _options = {}) {
|
|
460
|
+
return json?.error && typeof json.error === 'object' ? json.error[field] : undefined;
|
|
461
|
+
}
|
|
462
|
+
function vercelError(json, status) {
|
|
463
|
+
const code = vercelErrorField({ json, field: 'code' });
|
|
464
|
+
const message = vercelErrorField({ json, field: 'message' }) ||
|
|
465
|
+
json?.message ||
|
|
466
|
+
`Vercel request failed (${status}).`;
|
|
467
|
+
if (code === 'forbidden' || /permission/i.test(String(message))) {
|
|
468
|
+
return new DeployError("You don't have permission to create a project.", status, json);
|
|
469
|
+
}
|
|
470
|
+
return new DeployError(String(message), status, json);
|
|
471
|
+
}
|
|
472
|
+
function deploymentUrl(json) {
|
|
473
|
+
const url = json?.url || json?.alias?.[0] || '';
|
|
474
|
+
if (!url)
|
|
475
|
+
return '';
|
|
476
|
+
return /^https?:\/\//i.test(url) ? url : `https://${url}`;
|
|
477
|
+
}
|
|
478
|
+
function appendDeploymentAlias({ alias, urls }, _options = {}) {
|
|
479
|
+
if (typeof alias === 'string') {
|
|
480
|
+
urls.push(alias);
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
if (alias && typeof alias === 'object') {
|
|
484
|
+
const a = alias;
|
|
485
|
+
if (typeof a.domain === 'string')
|
|
486
|
+
urls.push(a.domain);
|
|
487
|
+
else if (typeof a.url === 'string')
|
|
488
|
+
urls.push(a.url);
|
|
489
|
+
else {
|
|
490
|
+
// Objects without a domain or URL are not deployment aliases.
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
function appendDeploymentAliases({ json, urls }, _options = {}) {
|
|
495
|
+
for (const alias of json.alias ?? []) {
|
|
496
|
+
if (typeof alias === 'string')
|
|
497
|
+
urls.push(alias);
|
|
498
|
+
}
|
|
499
|
+
for (const alias of json.aliases ?? []) {
|
|
500
|
+
appendDeploymentAlias({ alias, urls });
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* `if (!json) continue` skips a response that is not there. Of this file's one call site,
|
|
505
|
+
* `deploymentUrlCandidates(publicHost, ready, created)` in `publish()`, `created` is always a parsed
|
|
506
|
+
* `JsonObject` (`readVercelJson` returns one or throws), but `ready` is `null` when every one of
|
|
507
|
+
* `pollVercelDeployment`'s 30 status reads was unparseable (each such read is skipped with
|
|
508
|
+
* `continue`, so `last` is never assigned) — then only `created` supplies candidates. Same guard
|
|
509
|
+
* `netlify.ts`'s own `netlifyUrlCandidates` keeps for the same reason.
|
|
510
|
+
*/
|
|
511
|
+
function deploymentUrlCandidates(publicHost, ...responses) {
|
|
512
|
+
// Aliases (the production `<project>.vercel.app` among them) are tried before `url`: `url` is the
|
|
513
|
+
// per-deployment address, which Deployment Protection puts behind Vercel's login by default.
|
|
514
|
+
const urls = [];
|
|
515
|
+
const deploymentUrls = [];
|
|
516
|
+
for (const json of responses) {
|
|
517
|
+
if (!json)
|
|
518
|
+
continue;
|
|
519
|
+
if (typeof json.url === 'string')
|
|
520
|
+
deploymentUrls.push(json.url);
|
|
521
|
+
appendDeploymentAliases({ json, urls });
|
|
522
|
+
}
|
|
523
|
+
const candidates = [...new Set([...urls, ...deploymentUrls].map(normalizeDeploymentUrl).filter(Boolean))];
|
|
524
|
+
// The public `<project>.vercel.app` alias goes first when Vercel lists it. Vercel orders aliases
|
|
525
|
+
// its own way, so the team-scoped `<project>-<team>.vercel.app` (behind Deployment Protection)
|
|
526
|
+
// can come first, and the reachability wait stops at the first `protected` answer: a publish
|
|
527
|
+
// whose public alias served 200 was reported `protected` (2026-10-11).
|
|
528
|
+
const publicUrl = normalizeDeploymentUrl(publicHost);
|
|
529
|
+
return candidates.includes(publicUrl) ? [publicUrl, ...candidates.filter((url) => url !== publicUrl)] : candidates;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
return { VercelDeployTarget, isVercelProtectedResponse };
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Reads one credential-check response WITHOUT touching its body: 401/403 means the host refused the
|
|
537
|
+
* credential, any other failure says nothing about it (a 5xx, a rate limit).
|
|
538
|
+
*
|
|
539
|
+
* @param {Response} response
|
|
540
|
+
* @returns {{ ok: true } | { ok: false, reason: "rejected" | "unreachable", statusCode: number }}
|
|
541
|
+
*/
|
|
542
|
+
function classifyCredentialResponse(response) {
|
|
543
|
+
if (response.ok) return { ok: true };
|
|
544
|
+
return { ok: false, reason: response.status === 401 || response.status === 403 ? "rejected" : "unreachable", statusCode: response.status };
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* The token's account `user.username` from a `GET /v2/user` body, or `undefined`. Reads no other field.
|
|
549
|
+
* @param {unknown} body
|
|
550
|
+
* @returns {string | undefined}
|
|
551
|
+
*/
|
|
552
|
+
function readVercelUsername(body) {
|
|
553
|
+
const user = typeof body === "object" && body !== null ? /** @type {Record<string, unknown>} */ (body).user : undefined;
|
|
554
|
+
const username = typeof user === "object" && user !== null ? /** @type {Record<string, unknown>} */ (user).username : undefined;
|
|
555
|
+
return typeof username === "string" && username !== "" ? username : undefined;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/** The module contract a host's deploy-target registry loads (`DeployTargetModule`). The one config
|
|
559
|
+
* field is the optional Vercel `teamId`. */
|
|
560
|
+
export default {
|
|
561
|
+
/** @param {{ credential: { token: string }, config: { teamId?: string }, kit: any }} context */
|
|
562
|
+
create({ credential, config, kit }) {
|
|
563
|
+
const { VercelDeployTarget } = bindVercel(kit);
|
|
564
|
+
return new VercelDeployTarget({ token: credential.token, ...(typeof config.teamId === "string" ? { teamId: config.teamId } : {}) });
|
|
565
|
+
},
|
|
566
|
+
/**
|
|
567
|
+
* `GET /v2/user`: 401/403 for a bad token. Reads back only `user.username`, required on both body
|
|
568
|
+
* shapes Vercel's OpenAPI schema declares and public (`vercel.com/<username>`), never email,
|
|
569
|
+
* billing or `defaultTeamId`.
|
|
570
|
+
* @param {{ credential: { token: string }, kit: any }} context
|
|
571
|
+
*/
|
|
572
|
+
async verifyCredential({ credential, kit }) {
|
|
573
|
+
const response = await kit.fetch("https://api.vercel.com/v2/user", { headers: { Authorization: `Bearer ${credential.token}` } }, { timeoutMs: kit.timeouts.QUICK });
|
|
574
|
+
const check = classifyCredentialResponse(response);
|
|
575
|
+
if (!check.ok) return check;
|
|
576
|
+
return { ok: true, accountLabel: readVercelUsername(await response.json().catch(() => undefined)) };
|
|
577
|
+
},
|
|
578
|
+
/** @param {{ teamId?: unknown }} config */
|
|
579
|
+
validateConfig(config) {
|
|
580
|
+
if (typeof config.teamId === "string" && config.teamId.trim() === "") return "teamId must not be blank when provided";
|
|
581
|
+
return null;
|
|
582
|
+
},
|
|
583
|
+
basePath() {
|
|
584
|
+
return undefined;
|
|
585
|
+
},
|
|
586
|
+
};
|