@infracraft/pulumi 1.6.6 → 1.7.1

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.
@@ -93,6 +93,41 @@ async function fetchProject(token, teamId, idOrName) {
93
93
  return await response.json();
94
94
  }
95
95
  /**
96
+ * Picks a project's production domain from its domain list, mirroring how Vercel
97
+ * derives `VERCEL_PROJECT_PRODUCTION_URL`: a verified, non-redirect, non-branch
98
+ * domain, preferring a custom domain over the `*.vercel.app` default. Returns a
99
+ * full `https://` URL. Falls back to `<name>.vercel.app` when the list is empty
100
+ * (e.g. a freshly created project whose domain has not yet propagated).
101
+ *
102
+ * @param domains Domain entries from `GET /v9/projects/{id}/domains`
103
+ * @param name Project name, used for the `<name>.vercel.app` fallback
104
+ * @returns The production URL, e.g. `https://app.example.com`
105
+ * @example
106
+ * ```typescript
107
+ * pickProductionDomain(
108
+ * [{ name: "x.vercel.app", verified: true, redirect: null, gitBranch: null },
109
+ * { name: "app.example.com", verified: true, redirect: null, gitBranch: null }],
110
+ * "x",
111
+ * ); // => "https://app.example.com"
112
+ * ```
113
+ */
114
+ function pickProductionDomain(domains, name) {
115
+ const production = domains.filter((domain) => domain.verified && domain.redirect === null && domain.gitBranch === null);
116
+ const custom = production.find((domain) => !domain.name.endsWith(".vercel.app"));
117
+ const fallback = production.find((domain) => domain.name.endsWith(".vercel.app"));
118
+ return `https://${custom?.name ?? fallback?.name ?? `${name}.vercel.app`}`;
119
+ }
120
+ /**
121
+ * Fetches a project's production URL from the Vercel domains API.
122
+ * Throws on API failure — a wrong URL would silently misconfigure the app.
123
+ */
124
+ async function fetchProductionUrl(token, teamId, idOrName, name) {
125
+ const response = await fetch(`${VERCEL_API_URL}/v9/projects/${encodeURIComponent(idOrName)}/domains?teamId=${teamId}`, { headers: { Authorization: `Bearer ${token}` } });
126
+ if (!response.ok) throw new Error(`Vercel API error fetching domains for "${idOrName}" (${response.status}): ${await response.text()}`);
127
+ const { domains = [] } = await response.json();
128
+ return pickProductionDomain(domains, name);
129
+ }
130
+ /**
96
131
  * Builds the project body for create / update calls.
97
132
  * Only includes defined optional fields.
98
133
  */
@@ -216,7 +251,8 @@ var VercelProjectResource = class extends _pulumi_pulumi.dynamic.Resource {
216
251
  *
217
252
  * new VercelVariable("nexus-vars", {
218
253
  * projectId: project.id,
219
- * variables: { NEXT_PUBLIC_API_URL: meshUrl },
254
+ * // The app's own URL comes from the project, not from config or a derived name.
255
+ * variables: { NEXTAUTH_URL: project.url },
220
256
  * }, { provider });
221
257
  * ```
222
258
  */
@@ -230,11 +266,21 @@ var VercelProject = class extends _pulumi_pulumi.ComponentResource {
230
266
  ...args
231
267
  }, { parent: this });
232
268
  this.id = resource.projectId;
233
- this.registerOutputs({ id: this.id });
269
+ this.url = _pulumi_pulumi.unsecret(_pulumi_pulumi.all([
270
+ this.id,
271
+ provider.token,
272
+ provider.teamId,
273
+ _pulumi_pulumi.output(args.name)
274
+ ]).apply(([id, token, teamId, projectName]) => fetchProductionUrl(token, teamId, id, projectName)));
275
+ this.registerOutputs({
276
+ id: this.id,
277
+ url: this.url
278
+ });
234
279
  }
235
280
  };
236
281
 
237
282
  //#endregion
238
283
  exports.VERCEL_FRAMEWORKS = VERCEL_FRAMEWORKS;
239
284
  exports.VercelProject = VercelProject;
285
+ exports.pickProductionDomain = pickProductionDomain;
240
286
  //# sourceMappingURL=project.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"project.cjs","names":["pulumi"],"sources":["../../src/vercel/project.ts"],"sourcesContent":["import * as pulumi from \"@pulumi/pulumi\";\nimport type { VercelProvider } from \"./provider\";\n\nconst VERCEL_API_URL = \"https://api.vercel.com\";\n\n/**\n * Authoritative list of Vercel framework preset slugs.\n * Consumers may use this array for validation or UI rendering.\n * Source of truth: @vercel/frameworks (run `bun run script` to regenerate).\n * When Vercel adds a new framework, update this array and release a new version.\n */\nexport const VERCEL_FRAMEWORKS = [\n\t// Full-stack & React\n\t\"blitzjs\",\n\t\"nextjs\",\n\t\"gatsby\",\n\t\"remix\",\n\t\"react-router\",\n\t\"astro\",\n\t\"preact\",\n\t\"solidstart-1\",\n\t\"solidstart\",\n\t\"create-react-app\",\n\t\"ionic-react\",\n\t\"tanstack-start\",\n\t\"redwoodjs\",\n\t\"hydrogen\",\n\t// Vue ecosystem\n\t\"vue\",\n\t\"nuxtjs\",\n\t\"vitepress\",\n\t\"vuepress\",\n\t\"gridsome\",\n\t\"saber\",\n\t// Svelte ecosystem\n\t\"svelte\",\n\t\"sveltekit\",\n\t\"sveltekit-1\",\n\t\"sapper\",\n\t// Angular ecosystem\n\t\"angular\",\n\t\"ionic-angular\",\n\t\"scully\",\n\t// Static site generators\n\t\"hexo\",\n\t\"eleventy\",\n\t\"docusaurus-2\",\n\t\"docusaurus\",\n\t\"hugo\",\n\t\"jekyll\",\n\t\"brunch\",\n\t\"middleman\",\n\t\"zola\",\n\t// UI / component tools\n\t\"storybook\",\n\t\"stencil\",\n\t\"dojo\",\n\t\"ember\",\n\t\"polymer\",\n\t// Build tools\n\t\"vite\",\n\t\"parcel\",\n\t// CMS\n\t\"sanity-v3\",\n\t\"sanity\",\n\t// Node.js back-ends\n\t\"nitro\",\n\t\"hono\",\n\t\"express\",\n\t\"h3\",\n\t\"koa\",\n\t\"nestjs\",\n\t\"elysia\",\n\t\"fastify\",\n\t// Python\n\t\"fastapi\",\n\t\"flask\",\n\t\"fasthtml\",\n\t\"django\",\n\t// Other languages\n\t\"ash\",\n\t\"axum\",\n\t\"actix-web\",\n\t\"ruby\",\n\t\"rust\",\n\t\"go\",\n\t\"python\",\n\t\"node\",\n\t// Misc\n\t\"xmcp\",\n\t\"umijs\",\n\t\"mastra\",\n\t\"services\",\n] as const;\n\n/**\n * Vercel framework preset slug. Derived from {@link VERCEL_FRAMEWORKS} — single source of truth.\n * When Vercel adds a new framework, update {@link VERCEL_FRAMEWORKS} and release a new version.\n */\nexport type VercelFramework = (typeof VERCEL_FRAMEWORKS)[number];\n\n/** Resolved inputs for the Vercel project dynamic provider. */\nexport interface VercelProjectInputs {\n\t/** Vercel API bearer token. */\n\ttoken: string;\n\n\t/** Vercel team/org ID. */\n\tteamId: string;\n\n\t/** Project name. */\n\tname: string;\n\n\t/** Framework preset. */\n\tframework?: VercelFramework;\n\n\t/** Relative path to the project root within a monorepo (e.g. `\"apps/nexus\"`). */\n\trootDirectory?: string;\n\n\t/** Custom build command. */\n\tbuildCommand?: string;\n\n\t/** Custom install command. */\n\tinstallCommand?: string;\n\n\t/** Custom output directory. */\n\toutputDirectory?: string;\n}\n\n/** Persisted state for the Vercel project. */\ninterface VercelProjectOutputs extends VercelProjectInputs {\n\t/** Vercel-assigned project ID. */\n\tprojectId: string;\n}\n\n/** Vercel API response shape for a project. */\ninterface VercelProjectResponse {\n\tid: string;\n\tname: string;\n}\n\n/**\n * Fetches a Vercel project by name or ID.\n * Returns `null` if the project is not found (404).\n */\nasync function fetchProject(\n\ttoken: string,\n\tteamId: string,\n\tidOrName: string,\n): Promise<VercelProjectResponse | null> {\n\tconst response = await fetch(\n\t\t`${VERCEL_API_URL}/v9/projects/${encodeURIComponent(idOrName)}?teamId=${teamId}`,\n\t\t{ headers: { Authorization: `Bearer ${token}` } },\n\t);\n\n\tif (response.status === 404) {\n\t\treturn null;\n\t}\n\n\tif (!response.ok) {\n\t\tthrow new Error(\n\t\t\t`Vercel API error fetching project \"${idOrName}\" (${response.status}): ${await response.text()}`,\n\t\t);\n\t}\n\n\treturn (await response.json()) as VercelProjectResponse;\n}\n\n/**\n * Builds the project body for create / update calls.\n * Only includes defined optional fields.\n */\nfunction buildProjectBody(\n\tinputs: Omit<VercelProjectInputs, \"token\" | \"teamId\">,\n): Record<string, string> {\n\tconst body: Record<string, string> = { name: inputs.name };\n\n\tif (inputs.framework !== undefined) {\n\t\tbody.framework = inputs.framework;\n\t}\n\n\tif (inputs.rootDirectory !== undefined) {\n\t\tbody.rootDirectory = inputs.rootDirectory;\n\t}\n\n\tif (inputs.buildCommand !== undefined) {\n\t\tbody.buildCommand = inputs.buildCommand;\n\t}\n\n\tif (inputs.installCommand !== undefined) {\n\t\tbody.installCommand = inputs.installCommand;\n\t}\n\n\tif (inputs.outputDirectory !== undefined) {\n\t\tbody.outputDirectory = inputs.outputDirectory;\n\t}\n\n\treturn body;\n}\n\n/**\n * Dynamic provider implementing adopt-or-create for Vercel projects.\n *\n * On `create()`, calls `GET /v9/projects/{name}?teamId=…`. If found, adopts\n * the existing project. If 404, creates a new one via `POST /v9/projects`.\n * Deletion is a no-op to protect production projects.\n */\nclass VercelProjectResourceProvider implements pulumi.dynamic.ResourceProvider {\n\tasync create(\n\t\tinputs: VercelProjectInputs,\n\t): Promise<pulumi.dynamic.CreateResult> {\n\t\tconst existing = await fetchProject(\n\t\t\tinputs.token,\n\t\t\tinputs.teamId,\n\t\t\tinputs.name,\n\t\t);\n\n\t\tlet projectId: string;\n\n\t\tif (existing) {\n\t\t\tpulumi.log.info(\n\t\t\t\t`Adopting existing Vercel project \"${inputs.name}\" (${existing.id})`,\n\t\t\t);\n\n\t\t\tprojectId = existing.id;\n\t\t} else {\n\t\t\tpulumi.log.info(\n\t\t\t\t`Vercel project \"${inputs.name}\" not found — creating...`,\n\t\t\t);\n\n\t\t\tconst response = await fetch(\n\t\t\t\t`${VERCEL_API_URL}/v9/projects?teamId=${inputs.teamId}`,\n\t\t\t\t{\n\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\theaders: {\n\t\t\t\t\t\tAuthorization: `Bearer ${inputs.token}`,\n\t\t\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\t\t},\n\t\t\t\t\tbody: JSON.stringify(buildProjectBody(inputs)),\n\t\t\t\t},\n\t\t\t);\n\n\t\t\tif (!response.ok) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Vercel API error creating project \"${inputs.name}\" (${response.status}): ${await response.text()}`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst created = (await response.json()) as VercelProjectResponse;\n\n\t\t\tprojectId = created.id;\n\t\t}\n\n\t\tconst outs: VercelProjectOutputs = { ...inputs, projectId };\n\n\t\treturn { id: projectId, outs };\n\t}\n\n\tasync read(\n\t\tid: string,\n\t\tprops: VercelProjectOutputs,\n\t): Promise<pulumi.dynamic.ReadResult> {\n\t\tconst project = await fetchProject(props.token, props.teamId, id);\n\n\t\tif (!project) {\n\t\t\tthrow new Error(`Vercel project \"${id}\" not found during refresh`);\n\t\t}\n\n\t\treturn {\n\t\t\tid: project.id,\n\t\t\tprops: { ...props, name: project.name, projectId: project.id },\n\t\t};\n\t}\n\n\tasync update(\n\t\tid: string,\n\t\t_olds: VercelProjectOutputs,\n\t\tnews: VercelProjectInputs,\n\t): Promise<pulumi.dynamic.UpdateResult> {\n\t\tconst response = await fetch(\n\t\t\t`${VERCEL_API_URL}/v9/projects/${id}?teamId=${news.teamId}`,\n\t\t\t{\n\t\t\t\tmethod: \"PATCH\",\n\t\t\t\theaders: {\n\t\t\t\t\tAuthorization: `Bearer ${news.token}`,\n\t\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\t},\n\t\t\t\tbody: JSON.stringify(buildProjectBody(news)),\n\t\t\t},\n\t\t);\n\n\t\tif (!response.ok) {\n\t\t\tthrow new Error(\n\t\t\t\t`Vercel API error updating project \"${id}\" (${response.status}): ${await response.text()}`,\n\t\t\t);\n\t\t}\n\n\t\treturn { outs: { ...news, projectId: id } };\n\t}\n\n\tasync delete(): Promise<void> {\n\t\tpulumi.log.warn(\n\t\t\t\"Vercel project deletion skipped — projects are not deleted by Pulumi\",\n\t\t);\n\t}\n\n\tasync diff(\n\t\t_id: string,\n\t\tolds: VercelProjectOutputs,\n\t\tnews: VercelProjectInputs,\n\t): Promise<pulumi.dynamic.DiffResult> {\n\t\tconst replaces: string[] = [];\n\t\tconst changes: string[] = [];\n\n\t\tif (olds.teamId !== news.teamId) {\n\t\t\treplaces.push(\"teamId\");\n\t\t}\n\n\t\tconst updatableFields = [\n\t\t\t\"name\",\n\t\t\t\"framework\",\n\t\t\t\"rootDirectory\",\n\t\t\t\"buildCommand\",\n\t\t\t\"installCommand\",\n\t\t\t\"outputDirectory\",\n\t\t] as const;\n\n\t\tfor (const field of updatableFields) {\n\t\t\tif (olds[field] !== news[field]) {\n\t\t\t\tchanges.push(field);\n\t\t\t}\n\t\t}\n\n\t\treturn {\n\t\t\tchanges: replaces.length > 0 || changes.length > 0,\n\t\t\treplaces,\n\t\t\tdeleteBeforeReplace: true,\n\t\t};\n\t}\n}\n\n/** Internal dynamic resource — not part of the public API. */\nclass VercelProjectResource extends pulumi.dynamic.Resource {\n\tpublic declare readonly projectId: pulumi.Output<string>;\n\n\tconstructor(\n\t\tname: string,\n\t\targs: {\n\t\t\ttoken: pulumi.Input<string>;\n\t\t\tteamId: pulumi.Input<string>;\n\t\t\tname: pulumi.Input<string>;\n\t\t\tframework?: pulumi.Input<VercelFramework>;\n\t\t\trootDirectory?: pulumi.Input<string>;\n\t\t\tbuildCommand?: pulumi.Input<string>;\n\t\t\tinstallCommand?: pulumi.Input<string>;\n\t\t\toutputDirectory?: pulumi.Input<string>;\n\t\t},\n\t\topts?: pulumi.CustomResourceOptions,\n\t) {\n\t\tsuper(\n\t\t\tnew VercelProjectResourceProvider(),\n\t\t\tname,\n\t\t\t{ ...args, projectId: undefined },\n\t\t\topts,\n\t\t);\n\t}\n}\n\n/** Options type for VercelProject — replaces Pulumi's native `provider` field. */\ntype VercelProjectOptions = Omit<\n\tpulumi.ComponentResourceOptions,\n\t\"provider\"\n> & {\n\t/** Vercel authentication context. */\n\tprovider: VercelProvider;\n};\n\n/** Args for VercelProject. */\nexport interface VercelProjectArgs {\n\t/** Project name. Used for both adoption lookup and display name. */\n\tname: pulumi.Input<string>;\n\n\t/** Framework preset. */\n\tframework?: pulumi.Input<VercelFramework>;\n\n\t/** Relative path to the project root within a monorepo (e.g. `\"apps/nexus\"`). */\n\trootDirectory?: pulumi.Input<string>;\n\n\t/** Custom build command. */\n\tbuildCommand?: pulumi.Input<string>;\n\n\t/** Custom install command. */\n\tinstallCommand?: pulumi.Input<string>;\n\n\t/** Custom output directory. */\n\toutputDirectory?: pulumi.Input<string>;\n}\n\n/**\n * Manages a Vercel project with adopt-or-create semantics.\n *\n * On first `pulumi up`, looks up the project by name. If it already exists,\n * the resource adopts it. If not, a new project is created. Deletion is a\n * no-op to protect production projects.\n *\n * @example\n * ```typescript\n * const project = new VercelProject(\"nexus\", {\n * name: \"nexus\",\n * framework: \"nextjs\",\n * rootDirectory: \"apps/nexus\",\n * }, { provider });\n *\n * new VercelVariable(\"nexus-vars\", {\n * projectId: project.id,\n * variables: { NEXT_PUBLIC_API_URL: meshUrl },\n * }, { provider });\n * ```\n */\nexport class VercelProject extends pulumi.ComponentResource {\n\t/** Vercel-assigned project ID. */\n\tpublic readonly id: pulumi.Output<string>;\n\n\tconstructor(\n\t\tname: string,\n\t\targs: VercelProjectArgs,\n\t\topts: VercelProjectOptions,\n\t) {\n\t\tconst { provider, ...pulumiOpts } = opts;\n\n\t\tsuper(\"infracraft:vercel:Project\", name, {}, pulumiOpts);\n\n\t\tconst resource = new VercelProjectResource(\n\t\t\t`${name}-resource`,\n\t\t\t{\n\t\t\t\ttoken: provider.token,\n\t\t\t\tteamId: provider.teamId,\n\t\t\t\t...args,\n\t\t\t},\n\t\t\t{ parent: this },\n\t\t);\n\n\t\tthis.id = resource.projectId;\n\n\t\tthis.registerOutputs({ id: this.id });\n\t}\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"project.cjs","names":["pulumi"],"sources":["../../src/vercel/project.ts"],"sourcesContent":["import * as pulumi from \"@pulumi/pulumi\";\nimport type { VercelProvider } from \"./provider\";\n\nconst VERCEL_API_URL = \"https://api.vercel.com\";\n\n/**\n * Authoritative list of Vercel framework preset slugs.\n * Consumers may use this array for validation or UI rendering.\n * Source of truth: @vercel/frameworks (run `bun run script` to regenerate).\n * When Vercel adds a new framework, update this array and release a new version.\n */\nexport const VERCEL_FRAMEWORKS = [\n\t// Full-stack & React\n\t\"blitzjs\",\n\t\"nextjs\",\n\t\"gatsby\",\n\t\"remix\",\n\t\"react-router\",\n\t\"astro\",\n\t\"preact\",\n\t\"solidstart-1\",\n\t\"solidstart\",\n\t\"create-react-app\",\n\t\"ionic-react\",\n\t\"tanstack-start\",\n\t\"redwoodjs\",\n\t\"hydrogen\",\n\t// Vue ecosystem\n\t\"vue\",\n\t\"nuxtjs\",\n\t\"vitepress\",\n\t\"vuepress\",\n\t\"gridsome\",\n\t\"saber\",\n\t// Svelte ecosystem\n\t\"svelte\",\n\t\"sveltekit\",\n\t\"sveltekit-1\",\n\t\"sapper\",\n\t// Angular ecosystem\n\t\"angular\",\n\t\"ionic-angular\",\n\t\"scully\",\n\t// Static site generators\n\t\"hexo\",\n\t\"eleventy\",\n\t\"docusaurus-2\",\n\t\"docusaurus\",\n\t\"hugo\",\n\t\"jekyll\",\n\t\"brunch\",\n\t\"middleman\",\n\t\"zola\",\n\t// UI / component tools\n\t\"storybook\",\n\t\"stencil\",\n\t\"dojo\",\n\t\"ember\",\n\t\"polymer\",\n\t// Build tools\n\t\"vite\",\n\t\"parcel\",\n\t// CMS\n\t\"sanity-v3\",\n\t\"sanity\",\n\t// Node.js back-ends\n\t\"nitro\",\n\t\"hono\",\n\t\"express\",\n\t\"h3\",\n\t\"koa\",\n\t\"nestjs\",\n\t\"elysia\",\n\t\"fastify\",\n\t// Python\n\t\"fastapi\",\n\t\"flask\",\n\t\"fasthtml\",\n\t\"django\",\n\t// Other languages\n\t\"ash\",\n\t\"axum\",\n\t\"actix-web\",\n\t\"ruby\",\n\t\"rust\",\n\t\"go\",\n\t\"python\",\n\t\"node\",\n\t// Misc\n\t\"xmcp\",\n\t\"umijs\",\n\t\"mastra\",\n\t\"services\",\n] as const;\n\n/**\n * Vercel framework preset slug. Derived from {@link VERCEL_FRAMEWORKS} — single source of truth.\n * When Vercel adds a new framework, update {@link VERCEL_FRAMEWORKS} and release a new version.\n */\nexport type VercelFramework = (typeof VERCEL_FRAMEWORKS)[number];\n\n/** Resolved inputs for the Vercel project dynamic provider. */\nexport interface VercelProjectInputs {\n\t/** Vercel API bearer token. */\n\ttoken: string;\n\n\t/** Vercel team/org ID. */\n\tteamId: string;\n\n\t/** Project name. */\n\tname: string;\n\n\t/** Framework preset. */\n\tframework?: VercelFramework;\n\n\t/** Relative path to the project root within a monorepo (e.g. `\"apps/nexus\"`). */\n\trootDirectory?: string;\n\n\t/** Custom build command. */\n\tbuildCommand?: string;\n\n\t/** Custom install command. */\n\tinstallCommand?: string;\n\n\t/** Custom output directory. */\n\toutputDirectory?: string;\n}\n\n/** Persisted state for the Vercel project. */\ninterface VercelProjectOutputs extends VercelProjectInputs {\n\t/** Vercel-assigned project ID. */\n\tprojectId: string;\n}\n\n/** Vercel API response shape for a project. */\ninterface VercelProjectResponse {\n\tid: string;\n\tname: string;\n}\n\n/** A single entry from `GET /v9/projects/{id}/domains`. */\ninterface VercelDomainEntry {\n\tname: string;\n\tverified: boolean;\n\tredirect: string | null;\n\tgitBranch: string | null;\n}\n\n/**\n * Fetches a Vercel project by name or ID.\n * Returns `null` if the project is not found (404).\n */\nasync function fetchProject(\n\ttoken: string,\n\tteamId: string,\n\tidOrName: string,\n): Promise<VercelProjectResponse | null> {\n\tconst response = await fetch(\n\t\t`${VERCEL_API_URL}/v9/projects/${encodeURIComponent(idOrName)}?teamId=${teamId}`,\n\t\t{ headers: { Authorization: `Bearer ${token}` } },\n\t);\n\n\tif (response.status === 404) {\n\t\treturn null;\n\t}\n\n\tif (!response.ok) {\n\t\tthrow new Error(\n\t\t\t`Vercel API error fetching project \"${idOrName}\" (${response.status}): ${await response.text()}`,\n\t\t);\n\t}\n\n\treturn (await response.json()) as VercelProjectResponse;\n}\n\n/**\n * Picks a project's production domain from its domain list, mirroring how Vercel\n * derives `VERCEL_PROJECT_PRODUCTION_URL`: a verified, non-redirect, non-branch\n * domain, preferring a custom domain over the `*.vercel.app` default. Returns a\n * full `https://` URL. Falls back to `<name>.vercel.app` when the list is empty\n * (e.g. a freshly created project whose domain has not yet propagated).\n *\n * @param domains Domain entries from `GET /v9/projects/{id}/domains`\n * @param name Project name, used for the `<name>.vercel.app` fallback\n * @returns The production URL, e.g. `https://app.example.com`\n * @example\n * ```typescript\n * pickProductionDomain(\n * [{ name: \"x.vercel.app\", verified: true, redirect: null, gitBranch: null },\n * { name: \"app.example.com\", verified: true, redirect: null, gitBranch: null }],\n * \"x\",\n * ); // => \"https://app.example.com\"\n * ```\n */\nexport function pickProductionDomain(\n\tdomains: VercelDomainEntry[],\n\tname: string,\n): string {\n\tconst production = domains.filter(\n\t\t(domain) =>\n\t\t\tdomain.verified && domain.redirect === null && domain.gitBranch === null,\n\t);\n\n\tconst custom = production.find(\n\t\t(domain) => !domain.name.endsWith(\".vercel.app\"),\n\t);\n\tconst fallback = production.find((domain) =>\n\t\tdomain.name.endsWith(\".vercel.app\"),\n\t);\n\n\treturn `https://${custom?.name ?? fallback?.name ?? `${name}.vercel.app`}`;\n}\n\n/**\n * Fetches a project's production URL from the Vercel domains API.\n * Throws on API failure — a wrong URL would silently misconfigure the app.\n */\nasync function fetchProductionUrl(\n\ttoken: string,\n\tteamId: string,\n\tidOrName: string,\n\tname: string,\n): Promise<string> {\n\tconst response = await fetch(\n\t\t`${VERCEL_API_URL}/v9/projects/${encodeURIComponent(idOrName)}/domains?teamId=${teamId}`,\n\t\t{ headers: { Authorization: `Bearer ${token}` } },\n\t);\n\n\tif (!response.ok) {\n\t\tthrow new Error(\n\t\t\t`Vercel API error fetching domains for \"${idOrName}\" (${response.status}): ${await response.text()}`,\n\t\t);\n\t}\n\n\tconst { domains = [] } = (await response.json()) as {\n\t\tdomains?: VercelDomainEntry[];\n\t};\n\n\treturn pickProductionDomain(domains, name);\n}\n\n/**\n * Builds the project body for create / update calls.\n * Only includes defined optional fields.\n */\nfunction buildProjectBody(\n\tinputs: Omit<VercelProjectInputs, \"token\" | \"teamId\">,\n): Record<string, string> {\n\tconst body: Record<string, string> = { name: inputs.name };\n\n\tif (inputs.framework !== undefined) {\n\t\tbody.framework = inputs.framework;\n\t}\n\n\tif (inputs.rootDirectory !== undefined) {\n\t\tbody.rootDirectory = inputs.rootDirectory;\n\t}\n\n\tif (inputs.buildCommand !== undefined) {\n\t\tbody.buildCommand = inputs.buildCommand;\n\t}\n\n\tif (inputs.installCommand !== undefined) {\n\t\tbody.installCommand = inputs.installCommand;\n\t}\n\n\tif (inputs.outputDirectory !== undefined) {\n\t\tbody.outputDirectory = inputs.outputDirectory;\n\t}\n\n\treturn body;\n}\n\n/**\n * Dynamic provider implementing adopt-or-create for Vercel projects.\n *\n * On `create()`, calls `GET /v9/projects/{name}?teamId=…`. If found, adopts\n * the existing project. If 404, creates a new one via `POST /v9/projects`.\n * Deletion is a no-op to protect production projects.\n */\nclass VercelProjectResourceProvider implements pulumi.dynamic.ResourceProvider {\n\tasync create(\n\t\tinputs: VercelProjectInputs,\n\t): Promise<pulumi.dynamic.CreateResult> {\n\t\tconst existing = await fetchProject(\n\t\t\tinputs.token,\n\t\t\tinputs.teamId,\n\t\t\tinputs.name,\n\t\t);\n\n\t\tlet projectId: string;\n\n\t\tif (existing) {\n\t\t\tpulumi.log.info(\n\t\t\t\t`Adopting existing Vercel project \"${inputs.name}\" (${existing.id})`,\n\t\t\t);\n\n\t\t\tprojectId = existing.id;\n\t\t} else {\n\t\t\tpulumi.log.info(\n\t\t\t\t`Vercel project \"${inputs.name}\" not found — creating...`,\n\t\t\t);\n\n\t\t\tconst response = await fetch(\n\t\t\t\t`${VERCEL_API_URL}/v9/projects?teamId=${inputs.teamId}`,\n\t\t\t\t{\n\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\theaders: {\n\t\t\t\t\t\tAuthorization: `Bearer ${inputs.token}`,\n\t\t\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\t\t},\n\t\t\t\t\tbody: JSON.stringify(buildProjectBody(inputs)),\n\t\t\t\t},\n\t\t\t);\n\n\t\t\tif (!response.ok) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Vercel API error creating project \"${inputs.name}\" (${response.status}): ${await response.text()}`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst created = (await response.json()) as VercelProjectResponse;\n\n\t\t\tprojectId = created.id;\n\t\t}\n\n\t\tconst outs: VercelProjectOutputs = { ...inputs, projectId };\n\n\t\treturn { id: projectId, outs };\n\t}\n\n\tasync read(\n\t\tid: string,\n\t\tprops: VercelProjectOutputs,\n\t): Promise<pulumi.dynamic.ReadResult> {\n\t\tconst project = await fetchProject(props.token, props.teamId, id);\n\n\t\tif (!project) {\n\t\t\tthrow new Error(`Vercel project \"${id}\" not found during refresh`);\n\t\t}\n\n\t\treturn {\n\t\t\tid: project.id,\n\t\t\tprops: { ...props, name: project.name, projectId: project.id },\n\t\t};\n\t}\n\n\tasync update(\n\t\tid: string,\n\t\t_olds: VercelProjectOutputs,\n\t\tnews: VercelProjectInputs,\n\t): Promise<pulumi.dynamic.UpdateResult> {\n\t\tconst response = await fetch(\n\t\t\t`${VERCEL_API_URL}/v9/projects/${id}?teamId=${news.teamId}`,\n\t\t\t{\n\t\t\t\tmethod: \"PATCH\",\n\t\t\t\theaders: {\n\t\t\t\t\tAuthorization: `Bearer ${news.token}`,\n\t\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\t},\n\t\t\t\tbody: JSON.stringify(buildProjectBody(news)),\n\t\t\t},\n\t\t);\n\n\t\tif (!response.ok) {\n\t\t\tthrow new Error(\n\t\t\t\t`Vercel API error updating project \"${id}\" (${response.status}): ${await response.text()}`,\n\t\t\t);\n\t\t}\n\n\t\treturn { outs: { ...news, projectId: id } };\n\t}\n\n\tasync delete(): Promise<void> {\n\t\tpulumi.log.warn(\n\t\t\t\"Vercel project deletion skipped — projects are not deleted by Pulumi\",\n\t\t);\n\t}\n\n\tasync diff(\n\t\t_id: string,\n\t\tolds: VercelProjectOutputs,\n\t\tnews: VercelProjectInputs,\n\t): Promise<pulumi.dynamic.DiffResult> {\n\t\tconst replaces: string[] = [];\n\t\tconst changes: string[] = [];\n\n\t\tif (olds.teamId !== news.teamId) {\n\t\t\treplaces.push(\"teamId\");\n\t\t}\n\n\t\tconst updatableFields = [\n\t\t\t\"name\",\n\t\t\t\"framework\",\n\t\t\t\"rootDirectory\",\n\t\t\t\"buildCommand\",\n\t\t\t\"installCommand\",\n\t\t\t\"outputDirectory\",\n\t\t] as const;\n\n\t\tfor (const field of updatableFields) {\n\t\t\tif (olds[field] !== news[field]) {\n\t\t\t\tchanges.push(field);\n\t\t\t}\n\t\t}\n\n\t\treturn {\n\t\t\tchanges: replaces.length > 0 || changes.length > 0,\n\t\t\treplaces,\n\t\t\tdeleteBeforeReplace: true,\n\t\t};\n\t}\n}\n\n/** Internal dynamic resource — not part of the public API. */\nclass VercelProjectResource extends pulumi.dynamic.Resource {\n\tpublic declare readonly projectId: pulumi.Output<string>;\n\n\tconstructor(\n\t\tname: string,\n\t\targs: {\n\t\t\ttoken: pulumi.Input<string>;\n\t\t\tteamId: pulumi.Input<string>;\n\t\t\tname: pulumi.Input<string>;\n\t\t\tframework?: pulumi.Input<VercelFramework>;\n\t\t\trootDirectory?: pulumi.Input<string>;\n\t\t\tbuildCommand?: pulumi.Input<string>;\n\t\t\tinstallCommand?: pulumi.Input<string>;\n\t\t\toutputDirectory?: pulumi.Input<string>;\n\t\t},\n\t\topts?: pulumi.CustomResourceOptions,\n\t) {\n\t\tsuper(\n\t\t\tnew VercelProjectResourceProvider(),\n\t\t\tname,\n\t\t\t{ ...args, projectId: undefined },\n\t\t\topts,\n\t\t);\n\t}\n}\n\n/** Options type for VercelProject — replaces Pulumi's native `provider` field. */\ntype VercelProjectOptions = Omit<\n\tpulumi.ComponentResourceOptions,\n\t\"provider\"\n> & {\n\t/** Vercel authentication context. */\n\tprovider: VercelProvider;\n};\n\n/** Args for VercelProject. */\nexport interface VercelProjectArgs {\n\t/** Project name. Used for both adoption lookup and display name. */\n\tname: pulumi.Input<string>;\n\n\t/** Framework preset. */\n\tframework?: pulumi.Input<VercelFramework>;\n\n\t/** Relative path to the project root within a monorepo (e.g. `\"apps/nexus\"`). */\n\trootDirectory?: pulumi.Input<string>;\n\n\t/** Custom build command. */\n\tbuildCommand?: pulumi.Input<string>;\n\n\t/** Custom install command. */\n\tinstallCommand?: pulumi.Input<string>;\n\n\t/** Custom output directory. */\n\toutputDirectory?: pulumi.Input<string>;\n}\n\n/**\n * Manages a Vercel project with adopt-or-create semantics.\n *\n * On first `pulumi up`, looks up the project by name. If it already exists,\n * the resource adopts it. If not, a new project is created. Deletion is a\n * no-op to protect production projects.\n *\n * @example\n * ```typescript\n * const project = new VercelProject(\"nexus\", {\n * name: \"nexus\",\n * framework: \"nextjs\",\n * rootDirectory: \"apps/nexus\",\n * }, { provider });\n *\n * new VercelVariable(\"nexus-vars\", {\n * projectId: project.id,\n * // The app's own URL comes from the project, not from config or a derived name.\n * variables: { NEXTAUTH_URL: project.url },\n * }, { provider });\n * ```\n */\nexport class VercelProject extends pulumi.ComponentResource {\n\t/** Vercel-assigned project ID. */\n\tpublic readonly id: pulumi.Output<string>;\n\n\t/**\n\t * The project's production URL (with `https://`), e.g. `https://app.example.com`.\n\t * Resolves to the custom production domain when one is attached, otherwise the\n\t * `<name>.vercel.app` default — the source of truth for the app's own URL.\n\t */\n\tpublic readonly url: pulumi.Output<string>;\n\n\tconstructor(\n\t\tname: string,\n\t\targs: VercelProjectArgs,\n\t\topts: VercelProjectOptions,\n\t) {\n\t\tconst { provider, ...pulumiOpts } = opts;\n\n\t\tsuper(\"infracraft:vercel:Project\", name, {}, pulumiOpts);\n\n\t\tconst resource = new VercelProjectResource(\n\t\t\t`${name}-resource`,\n\t\t\t{\n\t\t\t\ttoken: provider.token,\n\t\t\t\tteamId: provider.teamId,\n\t\t\t\t...args,\n\t\t\t},\n\t\t\t{ parent: this },\n\t\t);\n\n\t\tthis.id = resource.projectId;\n\n\t\t// The production URL is fetched fresh from Vercel on every run (not persisted as\n\t\t// dynamic-resource state), so it is always current and resolves correctly on any\n\t\t// `up` without recreating the project. `unsecret` because a public domain is not\n\t\t// sensitive (only the token used to fetch it is) — keeping it out of the secret\n\t\t// serialization that feeds downstream Variable resources.\n\t\tthis.url = pulumi.unsecret(\n\t\t\tpulumi\n\t\t\t\t.all([this.id, provider.token, provider.teamId, pulumi.output(args.name)])\n\t\t\t\t.apply(([id, token, teamId, projectName]) =>\n\t\t\t\t\tfetchProductionUrl(token, teamId, id, projectName),\n\t\t\t\t),\n\t\t);\n\n\t\tthis.registerOutputs({ id: this.id, url: this.url });\n\t}\n}\n"],"mappings":"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"}
@@ -34,6 +34,33 @@ interface VercelProjectInputs {
34
34
  /** Custom output directory. */
35
35
  outputDirectory?: string;
36
36
  }
37
+ /** A single entry from `GET /v9/projects/{id}/domains`. */
38
+ interface VercelDomainEntry {
39
+ name: string;
40
+ verified: boolean;
41
+ redirect: string | null;
42
+ gitBranch: string | null;
43
+ }
44
+ /**
45
+ * Picks a project's production domain from its domain list, mirroring how Vercel
46
+ * derives `VERCEL_PROJECT_PRODUCTION_URL`: a verified, non-redirect, non-branch
47
+ * domain, preferring a custom domain over the `*.vercel.app` default. Returns a
48
+ * full `https://` URL. Falls back to `<name>.vercel.app` when the list is empty
49
+ * (e.g. a freshly created project whose domain has not yet propagated).
50
+ *
51
+ * @param domains Domain entries from `GET /v9/projects/{id}/domains`
52
+ * @param name Project name, used for the `<name>.vercel.app` fallback
53
+ * @returns The production URL, e.g. `https://app.example.com`
54
+ * @example
55
+ * ```typescript
56
+ * pickProductionDomain(
57
+ * [{ name: "x.vercel.app", verified: true, redirect: null, gitBranch: null },
58
+ * { name: "app.example.com", verified: true, redirect: null, gitBranch: null }],
59
+ * "x",
60
+ * ); // => "https://app.example.com"
61
+ * ```
62
+ */
63
+ declare function pickProductionDomain(domains: VercelDomainEntry[], name: string): string;
37
64
  /** Options type for VercelProject — replaces Pulumi's native `provider` field. */
38
65
  type VercelProjectOptions = Omit<pulumi.ComponentResourceOptions, "provider"> & {
39
66
  /** Vercel authentication context. */provider: VercelProvider;
@@ -70,15 +97,22 @@ interface VercelProjectArgs {
70
97
  *
71
98
  * new VercelVariable("nexus-vars", {
72
99
  * projectId: project.id,
73
- * variables: { NEXT_PUBLIC_API_URL: meshUrl },
100
+ * // The app's own URL comes from the project, not from config or a derived name.
101
+ * variables: { NEXTAUTH_URL: project.url },
74
102
  * }, { provider });
75
103
  * ```
76
104
  */
77
105
  declare class VercelProject extends pulumi.ComponentResource {
78
106
  /** Vercel-assigned project ID. */
79
107
  readonly id: pulumi.Output<string>;
108
+ /**
109
+ * The project's production URL (with `https://`), e.g. `https://app.example.com`.
110
+ * Resolves to the custom production domain when one is attached, otherwise the
111
+ * `<name>.vercel.app` default — the source of truth for the app's own URL.
112
+ */
113
+ readonly url: pulumi.Output<string>;
80
114
  constructor(name: string, args: VercelProjectArgs, opts: VercelProjectOptions);
81
115
  }
82
116
  //#endregion
83
- export { VERCEL_FRAMEWORKS, VercelFramework, VercelProject, VercelProjectArgs, VercelProjectInputs };
117
+ export { VERCEL_FRAMEWORKS, VercelFramework, VercelProject, VercelProjectArgs, VercelProjectInputs, pickProductionDomain };
84
118
  //# sourceMappingURL=project.d.cts.map
@@ -1 +1 @@
1
- {"version":3,"file":"project.d.cts","names":[],"sources":["../../src/vercel/project.ts"],"mappings":";;;;;;;;AAWA;;;cAAa,iBAAA;AAkFH;AAMV;;;AANU,KAME,eAAA,WAA0B,iBAAiB;AAAA;AAAA,UAGtC,mBAAA;EAAmB;EAEnC,KAAA;EAS2B;EAN3B,MAAA;EAAA;EAGA,IAAA;EAGA;EAAA,SAAA,GAAY,eAAe;EAG3B;EAAA,aAAA;EAMA;EAHA,YAAA;EAMe;EAHf,cAAA;EAsPI;EAnPJ,eAAA;AAAA;;KAmPI,oBAAA,GAAuB,IAAA,CAC3B,MAAA,CAAO,wBAAA;EAIG,qCAAV,QAAA,EAAU,cAAA;AAAA;;UAIM,iBAAA;EART;EAUP,IAAA,EAAM,MAAA,CAAO,KAAA;EANH;EASV,SAAA,GAAY,MAAA,CAAO,KAAA,CAAM,eAAA;EATD;EAYxB,aAAA,GAAgB,MAAA,CAAO,KAAA;EARU;EAWjC,YAAA,GAAe,MAAA,CAAO,KAAA;EAThB;EAYN,cAAA,GAAiB,MAAA,CAAO,KAAA;EATZ;EAYZ,eAAA,GAAkB,MAAA,CAAO,KAAA;AAAA;;;;;;;;;;;;;;;;;;;;;;cAwBb,aAAA,SAAsB,MAAA,CAAO,iBAAA;EAxBhB;EAAA,SA0BT,EAAA,EAAI,MAAA,CAAO,MAAA;cAG1B,IAAA,UACA,IAAA,EAAM,iBAAA,EACN,IAAA,EAAM,oBAAA;AAAA"}
1
+ {"version":3,"file":"project.d.cts","names":[],"sources":["../../src/vercel/project.ts"],"mappings":";;;;;;;;AAWA;;;cAAa,iBAAA;AAkFH;AAMV;;;AANU,KAME,eAAA,WAA0B,iBAAiB;AAAA;AAAA,UAGtC,mBAAA;EAAmB;EAEnC,KAAA;EAS2B;EAN3B,MAAA;EAAA;EAGA,IAAA;EAGA;EAAA,SAAA,GAAY,eAAe;EAG3B;EAAA,aAAA;EAMA;EAHA,YAAA;EAMe;EAHf,cAAA;EAmBS;EAhBT,eAAA;AAAA;;UAgBS,iBAAA;EACT,IAAA;EACA,QAAA;EACA,QAAA;EACA,SAAA;AAAA;AAiDD;;;;;;;;AAEa;AAeZ;;;;;;;;;;AAjBD,iBAAgB,oBAAA,CACf,OAAA,EAAS,iBAAiB,IAC1B,IAAA;;KAsPI,oBAAA,GAAuB,IAAA,CAC3B,MAAA,CAAO,wBAAA;EAIiB,qCAAxB,QAAA,EAAU,cAAA;AAAA;;UAIM,iBAAA;EAEV;EAAN,IAAA,EAAM,MAAA,CAAO,KAAA;EAGD;EAAZ,SAAA,GAAY,MAAA,CAAO,KAAA,CAAM,eAAA;EAMV;EAHf,aAAA,GAAgB,MAAA,CAAO,KAAA;EASL;EANlB,YAAA,GAAe,MAAA,CAAO,KAAA;EAMQ;EAH9B,cAAA,GAAiB,MAAA,CAAO,KAAA;EAZlB;EAeN,eAAA,GAAkB,MAAA,CAAO,KAAA;AAAA;;;;;;;;;;;;;;;;;AAAK;AAyB/B;;;;;cAAa,aAAA,SAAsB,MAAA,CAAO,iBAAA;EAclC;EAAA,SAZS,EAAA,EAAI,MAAA,CAAO,MAAA;EAF+B;;;;;EAAA,SAS1C,GAAA,EAAK,MAAA,CAAO,MAAA;cAG3B,IAAA,UACA,IAAA,EAAM,iBAAA,EACN,IAAA,EAAM,oBAAA;AAAA"}
@@ -34,6 +34,33 @@ interface VercelProjectInputs {
34
34
  /** Custom output directory. */
35
35
  outputDirectory?: string;
36
36
  }
37
+ /** A single entry from `GET /v9/projects/{id}/domains`. */
38
+ interface VercelDomainEntry {
39
+ name: string;
40
+ verified: boolean;
41
+ redirect: string | null;
42
+ gitBranch: string | null;
43
+ }
44
+ /**
45
+ * Picks a project's production domain from its domain list, mirroring how Vercel
46
+ * derives `VERCEL_PROJECT_PRODUCTION_URL`: a verified, non-redirect, non-branch
47
+ * domain, preferring a custom domain over the `*.vercel.app` default. Returns a
48
+ * full `https://` URL. Falls back to `<name>.vercel.app` when the list is empty
49
+ * (e.g. a freshly created project whose domain has not yet propagated).
50
+ *
51
+ * @param domains Domain entries from `GET /v9/projects/{id}/domains`
52
+ * @param name Project name, used for the `<name>.vercel.app` fallback
53
+ * @returns The production URL, e.g. `https://app.example.com`
54
+ * @example
55
+ * ```typescript
56
+ * pickProductionDomain(
57
+ * [{ name: "x.vercel.app", verified: true, redirect: null, gitBranch: null },
58
+ * { name: "app.example.com", verified: true, redirect: null, gitBranch: null }],
59
+ * "x",
60
+ * ); // => "https://app.example.com"
61
+ * ```
62
+ */
63
+ declare function pickProductionDomain(domains: VercelDomainEntry[], name: string): string;
37
64
  /** Options type for VercelProject — replaces Pulumi's native `provider` field. */
38
65
  type VercelProjectOptions = Omit<pulumi.ComponentResourceOptions, "provider"> & {
39
66
  /** Vercel authentication context. */provider: VercelProvider;
@@ -70,15 +97,22 @@ interface VercelProjectArgs {
70
97
  *
71
98
  * new VercelVariable("nexus-vars", {
72
99
  * projectId: project.id,
73
- * variables: { NEXT_PUBLIC_API_URL: meshUrl },
100
+ * // The app's own URL comes from the project, not from config or a derived name.
101
+ * variables: { NEXTAUTH_URL: project.url },
74
102
  * }, { provider });
75
103
  * ```
76
104
  */
77
105
  declare class VercelProject extends pulumi.ComponentResource {
78
106
  /** Vercel-assigned project ID. */
79
107
  readonly id: pulumi.Output<string>;
108
+ /**
109
+ * The project's production URL (with `https://`), e.g. `https://app.example.com`.
110
+ * Resolves to the custom production domain when one is attached, otherwise the
111
+ * `<name>.vercel.app` default — the source of truth for the app's own URL.
112
+ */
113
+ readonly url: pulumi.Output<string>;
80
114
  constructor(name: string, args: VercelProjectArgs, opts: VercelProjectOptions);
81
115
  }
82
116
  //#endregion
83
- export { VERCEL_FRAMEWORKS, VercelFramework, VercelProject, VercelProjectArgs, VercelProjectInputs };
117
+ export { VERCEL_FRAMEWORKS, VercelFramework, VercelProject, VercelProjectArgs, VercelProjectInputs, pickProductionDomain };
84
118
  //# sourceMappingURL=project.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"project.d.mts","names":[],"sources":["../../src/vercel/project.ts"],"mappings":";;;;;;;;AAWA;;;cAAa,iBAAA;AAkFH;AAMV;;;AANU,KAME,eAAA,WAA0B,iBAAiB;AAAA;AAAA,UAGtC,mBAAA;EAAmB;EAEnC,KAAA;EAS2B;EAN3B,MAAA;EAAA;EAGA,IAAA;EAGA;EAAA,SAAA,GAAY,eAAe;EAG3B;EAAA,aAAA;EAMA;EAHA,YAAA;EAMe;EAHf,cAAA;EAsPI;EAnPJ,eAAA;AAAA;;KAmPI,oBAAA,GAAuB,IAAA,CAC3B,MAAA,CAAO,wBAAA;EAIG,qCAAV,QAAA,EAAU,cAAA;AAAA;;UAIM,iBAAA;EART;EAUP,IAAA,EAAM,MAAA,CAAO,KAAA;EANH;EASV,SAAA,GAAY,MAAA,CAAO,KAAA,CAAM,eAAA;EATD;EAYxB,aAAA,GAAgB,MAAA,CAAO,KAAA;EARU;EAWjC,YAAA,GAAe,MAAA,CAAO,KAAA;EAThB;EAYN,cAAA,GAAiB,MAAA,CAAO,KAAA;EATZ;EAYZ,eAAA,GAAkB,MAAA,CAAO,KAAA;AAAA;;;;;;;;;;;;;;;;;;;;;;cAwBb,aAAA,SAAsB,MAAA,CAAO,iBAAA;EAxBhB;EAAA,SA0BT,EAAA,EAAI,MAAA,CAAO,MAAA;cAG1B,IAAA,UACA,IAAA,EAAM,iBAAA,EACN,IAAA,EAAM,oBAAA;AAAA"}
1
+ {"version":3,"file":"project.d.mts","names":[],"sources":["../../src/vercel/project.ts"],"mappings":";;;;;;;;AAWA;;;cAAa,iBAAA;AAkFH;AAMV;;;AANU,KAME,eAAA,WAA0B,iBAAiB;AAAA;AAAA,UAGtC,mBAAA;EAAmB;EAEnC,KAAA;EAS2B;EAN3B,MAAA;EAAA;EAGA,IAAA;EAGA;EAAA,SAAA,GAAY,eAAe;EAG3B;EAAA,aAAA;EAMA;EAHA,YAAA;EAMe;EAHf,cAAA;EAmBS;EAhBT,eAAA;AAAA;;UAgBS,iBAAA;EACT,IAAA;EACA,QAAA;EACA,QAAA;EACA,SAAA;AAAA;AAiDD;;;;;;;;AAEa;AAeZ;;;;;;;;;;AAjBD,iBAAgB,oBAAA,CACf,OAAA,EAAS,iBAAiB,IAC1B,IAAA;;KAsPI,oBAAA,GAAuB,IAAA,CAC3B,MAAA,CAAO,wBAAA;EAIiB,qCAAxB,QAAA,EAAU,cAAA;AAAA;;UAIM,iBAAA;EAEV;EAAN,IAAA,EAAM,MAAA,CAAO,KAAA;EAGD;EAAZ,SAAA,GAAY,MAAA,CAAO,KAAA,CAAM,eAAA;EAMV;EAHf,aAAA,GAAgB,MAAA,CAAO,KAAA;EASL;EANlB,YAAA,GAAe,MAAA,CAAO,KAAA;EAMQ;EAH9B,cAAA,GAAiB,MAAA,CAAO,KAAA;EAZlB;EAeN,eAAA,GAAkB,MAAA,CAAO,KAAA;AAAA;;;;;;;;;;;;;;;;;AAAK;AAyB/B;;;;;cAAa,aAAA,SAAsB,MAAA,CAAO,iBAAA;EAclC;EAAA,SAZS,EAAA,EAAI,MAAA,CAAO,MAAA;EAF+B;;;;;EAAA,SAS1C,GAAA,EAAK,MAAA,CAAO,MAAA;cAG3B,IAAA,UACA,IAAA,EAAM,iBAAA,EACN,IAAA,EAAM,oBAAA;AAAA"}
@@ -91,6 +91,41 @@ async function fetchProject(token, teamId, idOrName) {
91
91
  return await response.json();
92
92
  }
93
93
  /**
94
+ * Picks a project's production domain from its domain list, mirroring how Vercel
95
+ * derives `VERCEL_PROJECT_PRODUCTION_URL`: a verified, non-redirect, non-branch
96
+ * domain, preferring a custom domain over the `*.vercel.app` default. Returns a
97
+ * full `https://` URL. Falls back to `<name>.vercel.app` when the list is empty
98
+ * (e.g. a freshly created project whose domain has not yet propagated).
99
+ *
100
+ * @param domains Domain entries from `GET /v9/projects/{id}/domains`
101
+ * @param name Project name, used for the `<name>.vercel.app` fallback
102
+ * @returns The production URL, e.g. `https://app.example.com`
103
+ * @example
104
+ * ```typescript
105
+ * pickProductionDomain(
106
+ * [{ name: "x.vercel.app", verified: true, redirect: null, gitBranch: null },
107
+ * { name: "app.example.com", verified: true, redirect: null, gitBranch: null }],
108
+ * "x",
109
+ * ); // => "https://app.example.com"
110
+ * ```
111
+ */
112
+ function pickProductionDomain(domains, name) {
113
+ const production = domains.filter((domain) => domain.verified && domain.redirect === null && domain.gitBranch === null);
114
+ const custom = production.find((domain) => !domain.name.endsWith(".vercel.app"));
115
+ const fallback = production.find((domain) => domain.name.endsWith(".vercel.app"));
116
+ return `https://${custom?.name ?? fallback?.name ?? `${name}.vercel.app`}`;
117
+ }
118
+ /**
119
+ * Fetches a project's production URL from the Vercel domains API.
120
+ * Throws on API failure — a wrong URL would silently misconfigure the app.
121
+ */
122
+ async function fetchProductionUrl(token, teamId, idOrName, name) {
123
+ const response = await fetch(`${VERCEL_API_URL}/v9/projects/${encodeURIComponent(idOrName)}/domains?teamId=${teamId}`, { headers: { Authorization: `Bearer ${token}` } });
124
+ if (!response.ok) throw new Error(`Vercel API error fetching domains for "${idOrName}" (${response.status}): ${await response.text()}`);
125
+ const { domains = [] } = await response.json();
126
+ return pickProductionDomain(domains, name);
127
+ }
128
+ /**
94
129
  * Builds the project body for create / update calls.
95
130
  * Only includes defined optional fields.
96
131
  */
@@ -214,7 +249,8 @@ var VercelProjectResource = class extends pulumi.dynamic.Resource {
214
249
  *
215
250
  * new VercelVariable("nexus-vars", {
216
251
  * projectId: project.id,
217
- * variables: { NEXT_PUBLIC_API_URL: meshUrl },
252
+ * // The app's own URL comes from the project, not from config or a derived name.
253
+ * variables: { NEXTAUTH_URL: project.url },
218
254
  * }, { provider });
219
255
  * ```
220
256
  */
@@ -228,10 +264,19 @@ var VercelProject = class extends pulumi.ComponentResource {
228
264
  ...args
229
265
  }, { parent: this });
230
266
  this.id = resource.projectId;
231
- this.registerOutputs({ id: this.id });
267
+ this.url = pulumi.unsecret(pulumi.all([
268
+ this.id,
269
+ provider.token,
270
+ provider.teamId,
271
+ pulumi.output(args.name)
272
+ ]).apply(([id, token, teamId, projectName]) => fetchProductionUrl(token, teamId, id, projectName)));
273
+ this.registerOutputs({
274
+ id: this.id,
275
+ url: this.url
276
+ });
232
277
  }
233
278
  };
234
279
 
235
280
  //#endregion
236
- export { VERCEL_FRAMEWORKS, VercelProject };
281
+ export { VERCEL_FRAMEWORKS, VercelProject, pickProductionDomain };
237
282
  //# sourceMappingURL=project.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"project.mjs","names":[],"sources":["../../src/vercel/project.ts"],"sourcesContent":["import * as pulumi from \"@pulumi/pulumi\";\nimport type { VercelProvider } from \"./provider\";\n\nconst VERCEL_API_URL = \"https://api.vercel.com\";\n\n/**\n * Authoritative list of Vercel framework preset slugs.\n * Consumers may use this array for validation or UI rendering.\n * Source of truth: @vercel/frameworks (run `bun run script` to regenerate).\n * When Vercel adds a new framework, update this array and release a new version.\n */\nexport const VERCEL_FRAMEWORKS = [\n\t// Full-stack & React\n\t\"blitzjs\",\n\t\"nextjs\",\n\t\"gatsby\",\n\t\"remix\",\n\t\"react-router\",\n\t\"astro\",\n\t\"preact\",\n\t\"solidstart-1\",\n\t\"solidstart\",\n\t\"create-react-app\",\n\t\"ionic-react\",\n\t\"tanstack-start\",\n\t\"redwoodjs\",\n\t\"hydrogen\",\n\t// Vue ecosystem\n\t\"vue\",\n\t\"nuxtjs\",\n\t\"vitepress\",\n\t\"vuepress\",\n\t\"gridsome\",\n\t\"saber\",\n\t// Svelte ecosystem\n\t\"svelte\",\n\t\"sveltekit\",\n\t\"sveltekit-1\",\n\t\"sapper\",\n\t// Angular ecosystem\n\t\"angular\",\n\t\"ionic-angular\",\n\t\"scully\",\n\t// Static site generators\n\t\"hexo\",\n\t\"eleventy\",\n\t\"docusaurus-2\",\n\t\"docusaurus\",\n\t\"hugo\",\n\t\"jekyll\",\n\t\"brunch\",\n\t\"middleman\",\n\t\"zola\",\n\t// UI / component tools\n\t\"storybook\",\n\t\"stencil\",\n\t\"dojo\",\n\t\"ember\",\n\t\"polymer\",\n\t// Build tools\n\t\"vite\",\n\t\"parcel\",\n\t// CMS\n\t\"sanity-v3\",\n\t\"sanity\",\n\t// Node.js back-ends\n\t\"nitro\",\n\t\"hono\",\n\t\"express\",\n\t\"h3\",\n\t\"koa\",\n\t\"nestjs\",\n\t\"elysia\",\n\t\"fastify\",\n\t// Python\n\t\"fastapi\",\n\t\"flask\",\n\t\"fasthtml\",\n\t\"django\",\n\t// Other languages\n\t\"ash\",\n\t\"axum\",\n\t\"actix-web\",\n\t\"ruby\",\n\t\"rust\",\n\t\"go\",\n\t\"python\",\n\t\"node\",\n\t// Misc\n\t\"xmcp\",\n\t\"umijs\",\n\t\"mastra\",\n\t\"services\",\n] as const;\n\n/**\n * Vercel framework preset slug. Derived from {@link VERCEL_FRAMEWORKS} — single source of truth.\n * When Vercel adds a new framework, update {@link VERCEL_FRAMEWORKS} and release a new version.\n */\nexport type VercelFramework = (typeof VERCEL_FRAMEWORKS)[number];\n\n/** Resolved inputs for the Vercel project dynamic provider. */\nexport interface VercelProjectInputs {\n\t/** Vercel API bearer token. */\n\ttoken: string;\n\n\t/** Vercel team/org ID. */\n\tteamId: string;\n\n\t/** Project name. */\n\tname: string;\n\n\t/** Framework preset. */\n\tframework?: VercelFramework;\n\n\t/** Relative path to the project root within a monorepo (e.g. `\"apps/nexus\"`). */\n\trootDirectory?: string;\n\n\t/** Custom build command. */\n\tbuildCommand?: string;\n\n\t/** Custom install command. */\n\tinstallCommand?: string;\n\n\t/** Custom output directory. */\n\toutputDirectory?: string;\n}\n\n/** Persisted state for the Vercel project. */\ninterface VercelProjectOutputs extends VercelProjectInputs {\n\t/** Vercel-assigned project ID. */\n\tprojectId: string;\n}\n\n/** Vercel API response shape for a project. */\ninterface VercelProjectResponse {\n\tid: string;\n\tname: string;\n}\n\n/**\n * Fetches a Vercel project by name or ID.\n * Returns `null` if the project is not found (404).\n */\nasync function fetchProject(\n\ttoken: string,\n\tteamId: string,\n\tidOrName: string,\n): Promise<VercelProjectResponse | null> {\n\tconst response = await fetch(\n\t\t`${VERCEL_API_URL}/v9/projects/${encodeURIComponent(idOrName)}?teamId=${teamId}`,\n\t\t{ headers: { Authorization: `Bearer ${token}` } },\n\t);\n\n\tif (response.status === 404) {\n\t\treturn null;\n\t}\n\n\tif (!response.ok) {\n\t\tthrow new Error(\n\t\t\t`Vercel API error fetching project \"${idOrName}\" (${response.status}): ${await response.text()}`,\n\t\t);\n\t}\n\n\treturn (await response.json()) as VercelProjectResponse;\n}\n\n/**\n * Builds the project body for create / update calls.\n * Only includes defined optional fields.\n */\nfunction buildProjectBody(\n\tinputs: Omit<VercelProjectInputs, \"token\" | \"teamId\">,\n): Record<string, string> {\n\tconst body: Record<string, string> = { name: inputs.name };\n\n\tif (inputs.framework !== undefined) {\n\t\tbody.framework = inputs.framework;\n\t}\n\n\tif (inputs.rootDirectory !== undefined) {\n\t\tbody.rootDirectory = inputs.rootDirectory;\n\t}\n\n\tif (inputs.buildCommand !== undefined) {\n\t\tbody.buildCommand = inputs.buildCommand;\n\t}\n\n\tif (inputs.installCommand !== undefined) {\n\t\tbody.installCommand = inputs.installCommand;\n\t}\n\n\tif (inputs.outputDirectory !== undefined) {\n\t\tbody.outputDirectory = inputs.outputDirectory;\n\t}\n\n\treturn body;\n}\n\n/**\n * Dynamic provider implementing adopt-or-create for Vercel projects.\n *\n * On `create()`, calls `GET /v9/projects/{name}?teamId=…`. If found, adopts\n * the existing project. If 404, creates a new one via `POST /v9/projects`.\n * Deletion is a no-op to protect production projects.\n */\nclass VercelProjectResourceProvider implements pulumi.dynamic.ResourceProvider {\n\tasync create(\n\t\tinputs: VercelProjectInputs,\n\t): Promise<pulumi.dynamic.CreateResult> {\n\t\tconst existing = await fetchProject(\n\t\t\tinputs.token,\n\t\t\tinputs.teamId,\n\t\t\tinputs.name,\n\t\t);\n\n\t\tlet projectId: string;\n\n\t\tif (existing) {\n\t\t\tpulumi.log.info(\n\t\t\t\t`Adopting existing Vercel project \"${inputs.name}\" (${existing.id})`,\n\t\t\t);\n\n\t\t\tprojectId = existing.id;\n\t\t} else {\n\t\t\tpulumi.log.info(\n\t\t\t\t`Vercel project \"${inputs.name}\" not found — creating...`,\n\t\t\t);\n\n\t\t\tconst response = await fetch(\n\t\t\t\t`${VERCEL_API_URL}/v9/projects?teamId=${inputs.teamId}`,\n\t\t\t\t{\n\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\theaders: {\n\t\t\t\t\t\tAuthorization: `Bearer ${inputs.token}`,\n\t\t\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\t\t},\n\t\t\t\t\tbody: JSON.stringify(buildProjectBody(inputs)),\n\t\t\t\t},\n\t\t\t);\n\n\t\t\tif (!response.ok) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Vercel API error creating project \"${inputs.name}\" (${response.status}): ${await response.text()}`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst created = (await response.json()) as VercelProjectResponse;\n\n\t\t\tprojectId = created.id;\n\t\t}\n\n\t\tconst outs: VercelProjectOutputs = { ...inputs, projectId };\n\n\t\treturn { id: projectId, outs };\n\t}\n\n\tasync read(\n\t\tid: string,\n\t\tprops: VercelProjectOutputs,\n\t): Promise<pulumi.dynamic.ReadResult> {\n\t\tconst project = await fetchProject(props.token, props.teamId, id);\n\n\t\tif (!project) {\n\t\t\tthrow new Error(`Vercel project \"${id}\" not found during refresh`);\n\t\t}\n\n\t\treturn {\n\t\t\tid: project.id,\n\t\t\tprops: { ...props, name: project.name, projectId: project.id },\n\t\t};\n\t}\n\n\tasync update(\n\t\tid: string,\n\t\t_olds: VercelProjectOutputs,\n\t\tnews: VercelProjectInputs,\n\t): Promise<pulumi.dynamic.UpdateResult> {\n\t\tconst response = await fetch(\n\t\t\t`${VERCEL_API_URL}/v9/projects/${id}?teamId=${news.teamId}`,\n\t\t\t{\n\t\t\t\tmethod: \"PATCH\",\n\t\t\t\theaders: {\n\t\t\t\t\tAuthorization: `Bearer ${news.token}`,\n\t\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\t},\n\t\t\t\tbody: JSON.stringify(buildProjectBody(news)),\n\t\t\t},\n\t\t);\n\n\t\tif (!response.ok) {\n\t\t\tthrow new Error(\n\t\t\t\t`Vercel API error updating project \"${id}\" (${response.status}): ${await response.text()}`,\n\t\t\t);\n\t\t}\n\n\t\treturn { outs: { ...news, projectId: id } };\n\t}\n\n\tasync delete(): Promise<void> {\n\t\tpulumi.log.warn(\n\t\t\t\"Vercel project deletion skipped — projects are not deleted by Pulumi\",\n\t\t);\n\t}\n\n\tasync diff(\n\t\t_id: string,\n\t\tolds: VercelProjectOutputs,\n\t\tnews: VercelProjectInputs,\n\t): Promise<pulumi.dynamic.DiffResult> {\n\t\tconst replaces: string[] = [];\n\t\tconst changes: string[] = [];\n\n\t\tif (olds.teamId !== news.teamId) {\n\t\t\treplaces.push(\"teamId\");\n\t\t}\n\n\t\tconst updatableFields = [\n\t\t\t\"name\",\n\t\t\t\"framework\",\n\t\t\t\"rootDirectory\",\n\t\t\t\"buildCommand\",\n\t\t\t\"installCommand\",\n\t\t\t\"outputDirectory\",\n\t\t] as const;\n\n\t\tfor (const field of updatableFields) {\n\t\t\tif (olds[field] !== news[field]) {\n\t\t\t\tchanges.push(field);\n\t\t\t}\n\t\t}\n\n\t\treturn {\n\t\t\tchanges: replaces.length > 0 || changes.length > 0,\n\t\t\treplaces,\n\t\t\tdeleteBeforeReplace: true,\n\t\t};\n\t}\n}\n\n/** Internal dynamic resource — not part of the public API. */\nclass VercelProjectResource extends pulumi.dynamic.Resource {\n\tpublic declare readonly projectId: pulumi.Output<string>;\n\n\tconstructor(\n\t\tname: string,\n\t\targs: {\n\t\t\ttoken: pulumi.Input<string>;\n\t\t\tteamId: pulumi.Input<string>;\n\t\t\tname: pulumi.Input<string>;\n\t\t\tframework?: pulumi.Input<VercelFramework>;\n\t\t\trootDirectory?: pulumi.Input<string>;\n\t\t\tbuildCommand?: pulumi.Input<string>;\n\t\t\tinstallCommand?: pulumi.Input<string>;\n\t\t\toutputDirectory?: pulumi.Input<string>;\n\t\t},\n\t\topts?: pulumi.CustomResourceOptions,\n\t) {\n\t\tsuper(\n\t\t\tnew VercelProjectResourceProvider(),\n\t\t\tname,\n\t\t\t{ ...args, projectId: undefined },\n\t\t\topts,\n\t\t);\n\t}\n}\n\n/** Options type for VercelProject — replaces Pulumi's native `provider` field. */\ntype VercelProjectOptions = Omit<\n\tpulumi.ComponentResourceOptions,\n\t\"provider\"\n> & {\n\t/** Vercel authentication context. */\n\tprovider: VercelProvider;\n};\n\n/** Args for VercelProject. */\nexport interface VercelProjectArgs {\n\t/** Project name. Used for both adoption lookup and display name. */\n\tname: pulumi.Input<string>;\n\n\t/** Framework preset. */\n\tframework?: pulumi.Input<VercelFramework>;\n\n\t/** Relative path to the project root within a monorepo (e.g. `\"apps/nexus\"`). */\n\trootDirectory?: pulumi.Input<string>;\n\n\t/** Custom build command. */\n\tbuildCommand?: pulumi.Input<string>;\n\n\t/** Custom install command. */\n\tinstallCommand?: pulumi.Input<string>;\n\n\t/** Custom output directory. */\n\toutputDirectory?: pulumi.Input<string>;\n}\n\n/**\n * Manages a Vercel project with adopt-or-create semantics.\n *\n * On first `pulumi up`, looks up the project by name. If it already exists,\n * the resource adopts it. If not, a new project is created. Deletion is a\n * no-op to protect production projects.\n *\n * @example\n * ```typescript\n * const project = new VercelProject(\"nexus\", {\n * name: \"nexus\",\n * framework: \"nextjs\",\n * rootDirectory: \"apps/nexus\",\n * }, { provider });\n *\n * new VercelVariable(\"nexus-vars\", {\n * projectId: project.id,\n * variables: { NEXT_PUBLIC_API_URL: meshUrl },\n * }, { provider });\n * ```\n */\nexport class VercelProject extends pulumi.ComponentResource {\n\t/** Vercel-assigned project ID. */\n\tpublic readonly id: pulumi.Output<string>;\n\n\tconstructor(\n\t\tname: string,\n\t\targs: VercelProjectArgs,\n\t\topts: VercelProjectOptions,\n\t) {\n\t\tconst { provider, ...pulumiOpts } = opts;\n\n\t\tsuper(\"infracraft:vercel:Project\", name, {}, pulumiOpts);\n\n\t\tconst resource = new VercelProjectResource(\n\t\t\t`${name}-resource`,\n\t\t\t{\n\t\t\t\ttoken: provider.token,\n\t\t\t\tteamId: provider.teamId,\n\t\t\t\t...args,\n\t\t\t},\n\t\t\t{ parent: this },\n\t\t);\n\n\t\tthis.id = resource.projectId;\n\n\t\tthis.registerOutputs({ id: this.id });\n\t}\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"project.mjs","names":[],"sources":["../../src/vercel/project.ts"],"sourcesContent":["import * as pulumi from \"@pulumi/pulumi\";\nimport type { VercelProvider } from \"./provider\";\n\nconst VERCEL_API_URL = \"https://api.vercel.com\";\n\n/**\n * Authoritative list of Vercel framework preset slugs.\n * Consumers may use this array for validation or UI rendering.\n * Source of truth: @vercel/frameworks (run `bun run script` to regenerate).\n * When Vercel adds a new framework, update this array and release a new version.\n */\nexport const VERCEL_FRAMEWORKS = [\n\t// Full-stack & React\n\t\"blitzjs\",\n\t\"nextjs\",\n\t\"gatsby\",\n\t\"remix\",\n\t\"react-router\",\n\t\"astro\",\n\t\"preact\",\n\t\"solidstart-1\",\n\t\"solidstart\",\n\t\"create-react-app\",\n\t\"ionic-react\",\n\t\"tanstack-start\",\n\t\"redwoodjs\",\n\t\"hydrogen\",\n\t// Vue ecosystem\n\t\"vue\",\n\t\"nuxtjs\",\n\t\"vitepress\",\n\t\"vuepress\",\n\t\"gridsome\",\n\t\"saber\",\n\t// Svelte ecosystem\n\t\"svelte\",\n\t\"sveltekit\",\n\t\"sveltekit-1\",\n\t\"sapper\",\n\t// Angular ecosystem\n\t\"angular\",\n\t\"ionic-angular\",\n\t\"scully\",\n\t// Static site generators\n\t\"hexo\",\n\t\"eleventy\",\n\t\"docusaurus-2\",\n\t\"docusaurus\",\n\t\"hugo\",\n\t\"jekyll\",\n\t\"brunch\",\n\t\"middleman\",\n\t\"zola\",\n\t// UI / component tools\n\t\"storybook\",\n\t\"stencil\",\n\t\"dojo\",\n\t\"ember\",\n\t\"polymer\",\n\t// Build tools\n\t\"vite\",\n\t\"parcel\",\n\t// CMS\n\t\"sanity-v3\",\n\t\"sanity\",\n\t// Node.js back-ends\n\t\"nitro\",\n\t\"hono\",\n\t\"express\",\n\t\"h3\",\n\t\"koa\",\n\t\"nestjs\",\n\t\"elysia\",\n\t\"fastify\",\n\t// Python\n\t\"fastapi\",\n\t\"flask\",\n\t\"fasthtml\",\n\t\"django\",\n\t// Other languages\n\t\"ash\",\n\t\"axum\",\n\t\"actix-web\",\n\t\"ruby\",\n\t\"rust\",\n\t\"go\",\n\t\"python\",\n\t\"node\",\n\t// Misc\n\t\"xmcp\",\n\t\"umijs\",\n\t\"mastra\",\n\t\"services\",\n] as const;\n\n/**\n * Vercel framework preset slug. Derived from {@link VERCEL_FRAMEWORKS} — single source of truth.\n * When Vercel adds a new framework, update {@link VERCEL_FRAMEWORKS} and release a new version.\n */\nexport type VercelFramework = (typeof VERCEL_FRAMEWORKS)[number];\n\n/** Resolved inputs for the Vercel project dynamic provider. */\nexport interface VercelProjectInputs {\n\t/** Vercel API bearer token. */\n\ttoken: string;\n\n\t/** Vercel team/org ID. */\n\tteamId: string;\n\n\t/** Project name. */\n\tname: string;\n\n\t/** Framework preset. */\n\tframework?: VercelFramework;\n\n\t/** Relative path to the project root within a monorepo (e.g. `\"apps/nexus\"`). */\n\trootDirectory?: string;\n\n\t/** Custom build command. */\n\tbuildCommand?: string;\n\n\t/** Custom install command. */\n\tinstallCommand?: string;\n\n\t/** Custom output directory. */\n\toutputDirectory?: string;\n}\n\n/** Persisted state for the Vercel project. */\ninterface VercelProjectOutputs extends VercelProjectInputs {\n\t/** Vercel-assigned project ID. */\n\tprojectId: string;\n}\n\n/** Vercel API response shape for a project. */\ninterface VercelProjectResponse {\n\tid: string;\n\tname: string;\n}\n\n/** A single entry from `GET /v9/projects/{id}/domains`. */\ninterface VercelDomainEntry {\n\tname: string;\n\tverified: boolean;\n\tredirect: string | null;\n\tgitBranch: string | null;\n}\n\n/**\n * Fetches a Vercel project by name or ID.\n * Returns `null` if the project is not found (404).\n */\nasync function fetchProject(\n\ttoken: string,\n\tteamId: string,\n\tidOrName: string,\n): Promise<VercelProjectResponse | null> {\n\tconst response = await fetch(\n\t\t`${VERCEL_API_URL}/v9/projects/${encodeURIComponent(idOrName)}?teamId=${teamId}`,\n\t\t{ headers: { Authorization: `Bearer ${token}` } },\n\t);\n\n\tif (response.status === 404) {\n\t\treturn null;\n\t}\n\n\tif (!response.ok) {\n\t\tthrow new Error(\n\t\t\t`Vercel API error fetching project \"${idOrName}\" (${response.status}): ${await response.text()}`,\n\t\t);\n\t}\n\n\treturn (await response.json()) as VercelProjectResponse;\n}\n\n/**\n * Picks a project's production domain from its domain list, mirroring how Vercel\n * derives `VERCEL_PROJECT_PRODUCTION_URL`: a verified, non-redirect, non-branch\n * domain, preferring a custom domain over the `*.vercel.app` default. Returns a\n * full `https://` URL. Falls back to `<name>.vercel.app` when the list is empty\n * (e.g. a freshly created project whose domain has not yet propagated).\n *\n * @param domains Domain entries from `GET /v9/projects/{id}/domains`\n * @param name Project name, used for the `<name>.vercel.app` fallback\n * @returns The production URL, e.g. `https://app.example.com`\n * @example\n * ```typescript\n * pickProductionDomain(\n * [{ name: \"x.vercel.app\", verified: true, redirect: null, gitBranch: null },\n * { name: \"app.example.com\", verified: true, redirect: null, gitBranch: null }],\n * \"x\",\n * ); // => \"https://app.example.com\"\n * ```\n */\nexport function pickProductionDomain(\n\tdomains: VercelDomainEntry[],\n\tname: string,\n): string {\n\tconst production = domains.filter(\n\t\t(domain) =>\n\t\t\tdomain.verified && domain.redirect === null && domain.gitBranch === null,\n\t);\n\n\tconst custom = production.find(\n\t\t(domain) => !domain.name.endsWith(\".vercel.app\"),\n\t);\n\tconst fallback = production.find((domain) =>\n\t\tdomain.name.endsWith(\".vercel.app\"),\n\t);\n\n\treturn `https://${custom?.name ?? fallback?.name ?? `${name}.vercel.app`}`;\n}\n\n/**\n * Fetches a project's production URL from the Vercel domains API.\n * Throws on API failure — a wrong URL would silently misconfigure the app.\n */\nasync function fetchProductionUrl(\n\ttoken: string,\n\tteamId: string,\n\tidOrName: string,\n\tname: string,\n): Promise<string> {\n\tconst response = await fetch(\n\t\t`${VERCEL_API_URL}/v9/projects/${encodeURIComponent(idOrName)}/domains?teamId=${teamId}`,\n\t\t{ headers: { Authorization: `Bearer ${token}` } },\n\t);\n\n\tif (!response.ok) {\n\t\tthrow new Error(\n\t\t\t`Vercel API error fetching domains for \"${idOrName}\" (${response.status}): ${await response.text()}`,\n\t\t);\n\t}\n\n\tconst { domains = [] } = (await response.json()) as {\n\t\tdomains?: VercelDomainEntry[];\n\t};\n\n\treturn pickProductionDomain(domains, name);\n}\n\n/**\n * Builds the project body for create / update calls.\n * Only includes defined optional fields.\n */\nfunction buildProjectBody(\n\tinputs: Omit<VercelProjectInputs, \"token\" | \"teamId\">,\n): Record<string, string> {\n\tconst body: Record<string, string> = { name: inputs.name };\n\n\tif (inputs.framework !== undefined) {\n\t\tbody.framework = inputs.framework;\n\t}\n\n\tif (inputs.rootDirectory !== undefined) {\n\t\tbody.rootDirectory = inputs.rootDirectory;\n\t}\n\n\tif (inputs.buildCommand !== undefined) {\n\t\tbody.buildCommand = inputs.buildCommand;\n\t}\n\n\tif (inputs.installCommand !== undefined) {\n\t\tbody.installCommand = inputs.installCommand;\n\t}\n\n\tif (inputs.outputDirectory !== undefined) {\n\t\tbody.outputDirectory = inputs.outputDirectory;\n\t}\n\n\treturn body;\n}\n\n/**\n * Dynamic provider implementing adopt-or-create for Vercel projects.\n *\n * On `create()`, calls `GET /v9/projects/{name}?teamId=…`. If found, adopts\n * the existing project. If 404, creates a new one via `POST /v9/projects`.\n * Deletion is a no-op to protect production projects.\n */\nclass VercelProjectResourceProvider implements pulumi.dynamic.ResourceProvider {\n\tasync create(\n\t\tinputs: VercelProjectInputs,\n\t): Promise<pulumi.dynamic.CreateResult> {\n\t\tconst existing = await fetchProject(\n\t\t\tinputs.token,\n\t\t\tinputs.teamId,\n\t\t\tinputs.name,\n\t\t);\n\n\t\tlet projectId: string;\n\n\t\tif (existing) {\n\t\t\tpulumi.log.info(\n\t\t\t\t`Adopting existing Vercel project \"${inputs.name}\" (${existing.id})`,\n\t\t\t);\n\n\t\t\tprojectId = existing.id;\n\t\t} else {\n\t\t\tpulumi.log.info(\n\t\t\t\t`Vercel project \"${inputs.name}\" not found — creating...`,\n\t\t\t);\n\n\t\t\tconst response = await fetch(\n\t\t\t\t`${VERCEL_API_URL}/v9/projects?teamId=${inputs.teamId}`,\n\t\t\t\t{\n\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\theaders: {\n\t\t\t\t\t\tAuthorization: `Bearer ${inputs.token}`,\n\t\t\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\t\t},\n\t\t\t\t\tbody: JSON.stringify(buildProjectBody(inputs)),\n\t\t\t\t},\n\t\t\t);\n\n\t\t\tif (!response.ok) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Vercel API error creating project \"${inputs.name}\" (${response.status}): ${await response.text()}`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst created = (await response.json()) as VercelProjectResponse;\n\n\t\t\tprojectId = created.id;\n\t\t}\n\n\t\tconst outs: VercelProjectOutputs = { ...inputs, projectId };\n\n\t\treturn { id: projectId, outs };\n\t}\n\n\tasync read(\n\t\tid: string,\n\t\tprops: VercelProjectOutputs,\n\t): Promise<pulumi.dynamic.ReadResult> {\n\t\tconst project = await fetchProject(props.token, props.teamId, id);\n\n\t\tif (!project) {\n\t\t\tthrow new Error(`Vercel project \"${id}\" not found during refresh`);\n\t\t}\n\n\t\treturn {\n\t\t\tid: project.id,\n\t\t\tprops: { ...props, name: project.name, projectId: project.id },\n\t\t};\n\t}\n\n\tasync update(\n\t\tid: string,\n\t\t_olds: VercelProjectOutputs,\n\t\tnews: VercelProjectInputs,\n\t): Promise<pulumi.dynamic.UpdateResult> {\n\t\tconst response = await fetch(\n\t\t\t`${VERCEL_API_URL}/v9/projects/${id}?teamId=${news.teamId}`,\n\t\t\t{\n\t\t\t\tmethod: \"PATCH\",\n\t\t\t\theaders: {\n\t\t\t\t\tAuthorization: `Bearer ${news.token}`,\n\t\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\t},\n\t\t\t\tbody: JSON.stringify(buildProjectBody(news)),\n\t\t\t},\n\t\t);\n\n\t\tif (!response.ok) {\n\t\t\tthrow new Error(\n\t\t\t\t`Vercel API error updating project \"${id}\" (${response.status}): ${await response.text()}`,\n\t\t\t);\n\t\t}\n\n\t\treturn { outs: { ...news, projectId: id } };\n\t}\n\n\tasync delete(): Promise<void> {\n\t\tpulumi.log.warn(\n\t\t\t\"Vercel project deletion skipped — projects are not deleted by Pulumi\",\n\t\t);\n\t}\n\n\tasync diff(\n\t\t_id: string,\n\t\tolds: VercelProjectOutputs,\n\t\tnews: VercelProjectInputs,\n\t): Promise<pulumi.dynamic.DiffResult> {\n\t\tconst replaces: string[] = [];\n\t\tconst changes: string[] = [];\n\n\t\tif (olds.teamId !== news.teamId) {\n\t\t\treplaces.push(\"teamId\");\n\t\t}\n\n\t\tconst updatableFields = [\n\t\t\t\"name\",\n\t\t\t\"framework\",\n\t\t\t\"rootDirectory\",\n\t\t\t\"buildCommand\",\n\t\t\t\"installCommand\",\n\t\t\t\"outputDirectory\",\n\t\t] as const;\n\n\t\tfor (const field of updatableFields) {\n\t\t\tif (olds[field] !== news[field]) {\n\t\t\t\tchanges.push(field);\n\t\t\t}\n\t\t}\n\n\t\treturn {\n\t\t\tchanges: replaces.length > 0 || changes.length > 0,\n\t\t\treplaces,\n\t\t\tdeleteBeforeReplace: true,\n\t\t};\n\t}\n}\n\n/** Internal dynamic resource — not part of the public API. */\nclass VercelProjectResource extends pulumi.dynamic.Resource {\n\tpublic declare readonly projectId: pulumi.Output<string>;\n\n\tconstructor(\n\t\tname: string,\n\t\targs: {\n\t\t\ttoken: pulumi.Input<string>;\n\t\t\tteamId: pulumi.Input<string>;\n\t\t\tname: pulumi.Input<string>;\n\t\t\tframework?: pulumi.Input<VercelFramework>;\n\t\t\trootDirectory?: pulumi.Input<string>;\n\t\t\tbuildCommand?: pulumi.Input<string>;\n\t\t\tinstallCommand?: pulumi.Input<string>;\n\t\t\toutputDirectory?: pulumi.Input<string>;\n\t\t},\n\t\topts?: pulumi.CustomResourceOptions,\n\t) {\n\t\tsuper(\n\t\t\tnew VercelProjectResourceProvider(),\n\t\t\tname,\n\t\t\t{ ...args, projectId: undefined },\n\t\t\topts,\n\t\t);\n\t}\n}\n\n/** Options type for VercelProject — replaces Pulumi's native `provider` field. */\ntype VercelProjectOptions = Omit<\n\tpulumi.ComponentResourceOptions,\n\t\"provider\"\n> & {\n\t/** Vercel authentication context. */\n\tprovider: VercelProvider;\n};\n\n/** Args for VercelProject. */\nexport interface VercelProjectArgs {\n\t/** Project name. Used for both adoption lookup and display name. */\n\tname: pulumi.Input<string>;\n\n\t/** Framework preset. */\n\tframework?: pulumi.Input<VercelFramework>;\n\n\t/** Relative path to the project root within a monorepo (e.g. `\"apps/nexus\"`). */\n\trootDirectory?: pulumi.Input<string>;\n\n\t/** Custom build command. */\n\tbuildCommand?: pulumi.Input<string>;\n\n\t/** Custom install command. */\n\tinstallCommand?: pulumi.Input<string>;\n\n\t/** Custom output directory. */\n\toutputDirectory?: pulumi.Input<string>;\n}\n\n/**\n * Manages a Vercel project with adopt-or-create semantics.\n *\n * On first `pulumi up`, looks up the project by name. If it already exists,\n * the resource adopts it. If not, a new project is created. Deletion is a\n * no-op to protect production projects.\n *\n * @example\n * ```typescript\n * const project = new VercelProject(\"nexus\", {\n * name: \"nexus\",\n * framework: \"nextjs\",\n * rootDirectory: \"apps/nexus\",\n * }, { provider });\n *\n * new VercelVariable(\"nexus-vars\", {\n * projectId: project.id,\n * // The app's own URL comes from the project, not from config or a derived name.\n * variables: { NEXTAUTH_URL: project.url },\n * }, { provider });\n * ```\n */\nexport class VercelProject extends pulumi.ComponentResource {\n\t/** Vercel-assigned project ID. */\n\tpublic readonly id: pulumi.Output<string>;\n\n\t/**\n\t * The project's production URL (with `https://`), e.g. `https://app.example.com`.\n\t * Resolves to the custom production domain when one is attached, otherwise the\n\t * `<name>.vercel.app` default — the source of truth for the app's own URL.\n\t */\n\tpublic readonly url: pulumi.Output<string>;\n\n\tconstructor(\n\t\tname: string,\n\t\targs: VercelProjectArgs,\n\t\topts: VercelProjectOptions,\n\t) {\n\t\tconst { provider, ...pulumiOpts } = opts;\n\n\t\tsuper(\"infracraft:vercel:Project\", name, {}, pulumiOpts);\n\n\t\tconst resource = new VercelProjectResource(\n\t\t\t`${name}-resource`,\n\t\t\t{\n\t\t\t\ttoken: provider.token,\n\t\t\t\tteamId: provider.teamId,\n\t\t\t\t...args,\n\t\t\t},\n\t\t\t{ parent: this },\n\t\t);\n\n\t\tthis.id = resource.projectId;\n\n\t\t// The production URL is fetched fresh from Vercel on every run (not persisted as\n\t\t// dynamic-resource state), so it is always current and resolves correctly on any\n\t\t// `up` without recreating the project. `unsecret` because a public domain is not\n\t\t// sensitive (only the token used to fetch it is) — keeping it out of the secret\n\t\t// serialization that feeds downstream Variable resources.\n\t\tthis.url = pulumi.unsecret(\n\t\t\tpulumi\n\t\t\t\t.all([this.id, provider.token, provider.teamId, pulumi.output(args.name)])\n\t\t\t\t.apply(([id, token, teamId, projectName]) =>\n\t\t\t\t\tfetchProductionUrl(token, teamId, id, projectName),\n\t\t\t\t),\n\t\t);\n\n\t\tthis.registerOutputs({ id: this.id, url: this.url });\n\t}\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@infracraft/pulumi",
3
- "version": "1.6.6",
3
+ "version": "1.7.1",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -46,8 +46,8 @@
46
46
  "lint": "biome check --write"
47
47
  },
48
48
  "peerDependencies": {
49
- "@pulumi/pulumi": "^3",
50
- "@pulumi/command": "^1"
49
+ "@pulumi/command": "^1",
50
+ "@pulumi/pulumi": "^3"
51
51
  },
52
52
  "peerDependenciesMeta": {
53
53
  "@pulumi/command": {