@infracraft/pulumi 1.6.5 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -18,17 +18,31 @@ async function revealPassword(client, projectId, branchId, name) {
18
18
  return (await client.get(`/projects/${projectId}/branches/${branchId}/roles/${name}/reveal_password`)).password;
19
19
  }
20
20
  /**
21
+ * Resets an existing role's password to a fresh value and returns it.
22
+ * Used to isolate an adopted (copy-on-write) role from the parent branch's credential.
23
+ */
24
+ async function resetRolePassword(client, projectId, branchId, name) {
25
+ const result = await client.post(`/projects/${projectId}/branches/${branchId}/roles/${name}/reset_password`, {});
26
+ const password = result.role?.password ?? result.password;
27
+ if (!password) throw new Error(`Neon reset_password returned no password for role "${name}"`);
28
+ return password;
29
+ }
30
+ /**
21
31
  * Dynamic provider implementing CRUD for Neon database roles.
22
32
  *
23
33
  * Uses adopt-or-create on `create()`: finds an existing role by name
24
34
  * before creating a new one.
35
+ *
36
+ * @internal Exported only for unit testing; not part of the public API surface.
25
37
  */
26
38
  var NeonRoleResourceProvider = class {
27
39
  async create(inputs) {
28
40
  const client = new require_neon_client.NeonClient(inputs.apiKey);
29
- if (!await roleExists(client, inputs.projectId, inputs.branchId, inputs.name)) await client.post(`/projects/${inputs.projectId}/branches/${inputs.branchId}/roles`, { role: { name: inputs.name } });
41
+ const exists = await roleExists(client, inputs.projectId, inputs.branchId, inputs.name);
42
+ if (!exists) await client.post(`/projects/${inputs.projectId}/branches/${inputs.branchId}/roles`, { role: { name: inputs.name } });
43
+ else if (inputs.resetPassword) _pulumi_pulumi.log.info(`Resetting password for adopted Neon role "${inputs.name}" to isolate it from the parent branch`);
30
44
  else _pulumi_pulumi.log.info(`Adopting existing Neon role "${inputs.name}"`);
31
- const password = await revealPassword(client, inputs.projectId, inputs.branchId, inputs.name);
45
+ const password = exists && inputs.resetPassword ? await resetRolePassword(client, inputs.projectId, inputs.branchId, inputs.name) : await revealPassword(client, inputs.projectId, inputs.branchId, inputs.name);
32
46
  return {
33
47
  id: `${inputs.branchId}/${inputs.name}`,
34
48
  outs: {
@@ -72,8 +86,11 @@ var NeonRoleResource = class extends _pulumi_pulumi.dynamic.Resource {
72
86
  constructor(name, args, opts) {
73
87
  super(new NeonRoleResourceProvider(), name, {
74
88
  ...args,
75
- password: _pulumi_pulumi.secret(void 0)
76
- }, opts);
89
+ password: void 0
90
+ }, {
91
+ ...opts,
92
+ additionalSecretOutputs: ["password"]
93
+ });
77
94
  }
78
95
  };
79
96
  /**
@@ -95,7 +112,8 @@ var NeonRole = class extends _pulumi_pulumi.ComponentResource {
95
112
  apiKey: provider.apiKey,
96
113
  projectId: project.id,
97
114
  branchId: branch.id,
98
- name: args.name
115
+ name: args.name,
116
+ resetPassword: args.resetPassword ?? false
99
117
  }, { parent: this });
100
118
  this.password = resource.password;
101
119
  this.registerOutputs({ password: this.password });
@@ -104,4 +122,5 @@ var NeonRole = class extends _pulumi_pulumi.ComponentResource {
104
122
 
105
123
  //#endregion
106
124
  exports.NeonRole = NeonRole;
125
+ exports.NeonRoleResourceProvider = NeonRoleResourceProvider;
107
126
  //# sourceMappingURL=role.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"role.cjs","names":["NeonClient","pulumi"],"sources":["../../src/neon/role.ts"],"sourcesContent":["import * as pulumi from \"@pulumi/pulumi\";\nimport type { NeonBranch } from \"./branch\";\nimport { NeonClient } from \"./client\";\nimport type { NeonProject } from \"./project\";\nimport type { NeonProvider } from \"./provider\";\n\n/** Resolved inputs for the Neon role dynamic provider. */\nexport interface NeonRoleInputs {\n\t/** Neon API key. */\n\tapiKey: string;\n\n\t/** Neon project ID. */\n\tprojectId: string;\n\n\t/** Branch ID the role belongs to. */\n\tbranchId: string;\n\n\t/** Role name (e.g. `\"neondb_owner\"`). */\n\tname: string;\n}\n\n/** Persisted state for the Neon role. */\ninterface NeonRoleOutputs extends NeonRoleInputs {\n\t/** Role password (auto-generated by Neon). */\n\tpassword: string;\n}\n\n/** Neon API response for the reveal_password endpoint. */\ninterface PasswordResponse {\n\tpassword: string;\n}\n\n/** Neon API response for listing roles. */\ninterface RoleListResponse {\n\troles: Array<{\n\t\tname: string;\n\t}>;\n}\n\n/**\n * Checks if a role exists by name on a branch.\n */\nasync function roleExists(\n\tclient: NeonClient,\n\tprojectId: string,\n\tbranchId: string,\n\tname: string,\n): Promise<boolean> {\n\tconst result = await client.get<RoleListResponse>(\n\t\t`/projects/${projectId}/branches/${branchId}/roles`,\n\t);\n\n\treturn result.roles.some((r) => r.name === name);\n}\n\n/**\n * Reveals the password for an existing role via the dedicated API endpoint.\n */\nasync function revealPassword(\n\tclient: NeonClient,\n\tprojectId: string,\n\tbranchId: string,\n\tname: string,\n): Promise<string> {\n\tconst result = await client.get<PasswordResponse>(\n\t\t`/projects/${projectId}/branches/${branchId}/roles/${name}/reveal_password`,\n\t);\n\n\treturn result.password;\n}\n\n/**\n * Dynamic provider implementing CRUD for Neon database roles.\n *\n * Uses adopt-or-create on `create()`: finds an existing role by name\n * before creating a new one.\n */\nclass NeonRoleResourceProvider implements pulumi.dynamic.ResourceProvider {\n\tasync create(inputs: NeonRoleInputs): Promise<pulumi.dynamic.CreateResult> {\n\t\tconst client = new NeonClient(inputs.apiKey);\n\n\t\tconst exists = await roleExists(\n\t\t\tclient,\n\t\t\tinputs.projectId,\n\t\t\tinputs.branchId,\n\t\t\tinputs.name,\n\t\t);\n\n\t\tif (!exists) {\n\t\t\tawait client.post(\n\t\t\t\t`/projects/${inputs.projectId}/branches/${inputs.branchId}/roles`,\n\t\t\t\t{ role: { name: inputs.name } },\n\t\t\t);\n\t\t} else {\n\t\t\tpulumi.log.info(`Adopting existing Neon role \"${inputs.name}\"`);\n\t\t}\n\n\t\tconst password = await revealPassword(\n\t\t\tclient,\n\t\t\tinputs.projectId,\n\t\t\tinputs.branchId,\n\t\t\tinputs.name,\n\t\t);\n\n\t\treturn {\n\t\t\tid: `${inputs.branchId}/${inputs.name}`,\n\t\t\touts: { ...inputs, password },\n\t\t};\n\t}\n\n\tasync read(\n\t\tid: string,\n\t\tprops: NeonRoleOutputs,\n\t): Promise<pulumi.dynamic.ReadResult> {\n\t\tconst client = new NeonClient(props.apiKey);\n\n\t\tconst password = await revealPassword(\n\t\t\tclient,\n\t\t\tprops.projectId,\n\t\t\tprops.branchId,\n\t\t\tprops.name,\n\t\t);\n\n\t\treturn {\n\t\t\tid,\n\t\t\tprops: { ...props, password },\n\t\t};\n\t}\n\n\tasync delete(_id: string, props: NeonRoleOutputs): Promise<void> {\n\t\tconst client = new NeonClient(props.apiKey);\n\n\t\ttry {\n\t\t\tawait client.delete(\n\t\t\t\t`/projects/${props.projectId}/branches/${props.branchId}/roles/${props.name}`,\n\t\t\t);\n\t\t} catch {\n\t\t\tpulumi.log.warn(\n\t\t\t\t`Failed to delete Neon role \"${props.name}\" (may already be deleted)`,\n\t\t\t);\n\t\t}\n\t}\n\n\tasync diff(\n\t\t_id: string,\n\t\tolds: NeonRoleOutputs,\n\t\tnews: NeonRoleInputs,\n\t): Promise<pulumi.dynamic.DiffResult> {\n\t\tconst replaces: string[] = [];\n\n\t\tif (olds.projectId !== news.projectId) {\n\t\t\treplaces.push(\"projectId\");\n\t\t}\n\n\t\tif (olds.branchId !== news.branchId) {\n\t\t\treplaces.push(\"branchId\");\n\t\t}\n\n\t\tif (olds.name !== news.name) {\n\t\t\treplaces.push(\"name\");\n\t\t}\n\n\t\treturn {\n\t\t\tchanges: replaces.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 NeonRoleResource extends pulumi.dynamic.Resource {\n\tpublic declare readonly password: pulumi.Output<string>;\n\n\tconstructor(\n\t\tname: string,\n\t\targs: {\n\t\t\tapiKey: pulumi.Input<string>;\n\t\t\tprojectId: pulumi.Input<string>;\n\t\t\tbranchId: pulumi.Input<string>;\n\t\t\tname: pulumi.Input<string>;\n\t\t},\n\t\topts?: pulumi.CustomResourceOptions,\n\t) {\n\t\tsuper(\n\t\t\tnew NeonRoleResourceProvider(),\n\t\t\tname,\n\t\t\t{ ...args, password: pulumi.secret(undefined as unknown as string) },\n\t\t\topts,\n\t\t);\n\t}\n}\n\n/** Options type for NeonRole — replaces Pulumi's native `provider` field. */\ntype NeonRoleOptions = Omit<pulumi.ComponentResourceOptions, \"provider\"> & {\n\t/** Neon authentication context. */\n\tprovider: NeonProvider;\n\n\t/** Neon project context. */\n\tproject: NeonProject;\n\n\t/** Neon branch context. */\n\tbranch: NeonBranch;\n};\n\n/** Args for NeonRole. */\nexport interface NeonRoleArgs {\n\t/** Role name (e.g. `\"neondb_owner\"`). */\n\tname: pulumi.Input<string>;\n}\n\n/**\n * Manages a Neon database role with adopt-or-create semantics.\n * Exposes `password` as a secret output for connection string composition.\n *\n * @example\n * ```typescript\n * const role = new NeonRole(\"owner\", {\n * name: \"neondb_owner\",\n * }, { provider, project, branch });\n * ```\n */\nexport class NeonRole extends pulumi.ComponentResource {\n\t/** Role password (secret). */\n\tpublic readonly password: pulumi.Output<string>;\n\n\tconstructor(name: string, args: NeonRoleArgs, opts: NeonRoleOptions) {\n\t\tconst { provider, project, branch, ...pulumiOpts } = opts;\n\n\t\tsuper(\"infracraft:neon:Role\", name, {}, pulumiOpts);\n\n\t\tconst resource = new NeonRoleResource(\n\t\t\t`${name}-resource`,\n\t\t\t{\n\t\t\t\tapiKey: provider.apiKey,\n\t\t\t\tprojectId: project.id,\n\t\t\t\tbranchId: branch.id,\n\t\t\t\tname: args.name,\n\t\t\t},\n\t\t\t{ parent: this },\n\t\t);\n\n\t\tthis.password = resource.password;\n\n\t\tthis.registerOutputs({ password: this.password });\n\t}\n}\n"],"mappings":";;;;;;;;;;AA0CA,eAAe,WACd,QACA,WACA,UACA,MACmB;CAKnB,QAAO,MAJc,OAAO,IAC3B,aAAa,UAAU,YAAY,SAAS,OAC7C,GAEc,MAAM,MAAM,MAAM,EAAE,SAAS,IAAI;AAChD;;;;AAKA,eAAe,eACd,QACA,WACA,UACA,MACkB;CAKlB,QAAO,MAJc,OAAO,IAC3B,aAAa,UAAU,YAAY,SAAS,SAAS,KAAK,iBAC3D,GAEc;AACf;;;;;;;AAQA,IAAM,2BAAN,MAA0E;CACzE,MAAM,OAAO,QAA8D;EAC1E,MAAM,SAAS,IAAIA,+BAAW,OAAO,MAAM;EAS3C,IAAI,CAAC,MAPgB,WACpB,QACA,OAAO,WACP,OAAO,UACP,OAAO,IACR,GAGC,MAAM,OAAO,KACZ,aAAa,OAAO,UAAU,YAAY,OAAO,SAAS,SAC1D,EAAE,MAAM,EAAE,MAAM,OAAO,KAAK,EAAE,CAC/B;OAEA,eAAO,IAAI,KAAK,gCAAgC,OAAO,KAAK,EAAE;EAG/D,MAAM,WAAW,MAAM,eACtB,QACA,OAAO,WACP,OAAO,UACP,OAAO,IACR;EAEA,OAAO;GACN,IAAI,GAAG,OAAO,SAAS,GAAG,OAAO;GACjC,MAAM;IAAE,GAAG;IAAQ;GAAS;EAC7B;CACD;CAEA,MAAM,KACL,IACA,OACqC;EAGrC,MAAM,WAAW,MAAM,eACtB,IAHkBA,+BAAW,MAAM,MAG9B,GACL,MAAM,WACN,MAAM,UACN,MAAM,IACP;EAEA,OAAO;GACN;GACA,OAAO;IAAE,GAAG;IAAO;GAAS;EAC7B;CACD;CAEA,MAAM,OAAO,KAAa,OAAuC;EAChE,MAAM,SAAS,IAAIA,+BAAW,MAAM,MAAM;EAE1C,IAAI;GACH,MAAM,OAAO,OACZ,aAAa,MAAM,UAAU,YAAY,MAAM,SAAS,SAAS,MAAM,MACxE;EACD,QAAQ;GACP,eAAO,IAAI,KACV,+BAA+B,MAAM,KAAK,2BAC3C;EACD;CACD;CAEA,MAAM,KACL,KACA,MACA,MACqC;EACrC,MAAM,WAAqB,CAAC;EAE5B,IAAI,KAAK,cAAc,KAAK,WAC3B,SAAS,KAAK,WAAW;EAG1B,IAAI,KAAK,aAAa,KAAK,UAC1B,SAAS,KAAK,UAAU;EAGzB,IAAI,KAAK,SAAS,KAAK,MACtB,SAAS,KAAK,MAAM;EAGrB,OAAO;GACN,SAAS,SAAS,SAAS;GAC3B;GACA,qBAAqB;EACtB;CACD;AACD;;AAGA,IAAM,mBAAN,cAA+BC,eAAO,QAAQ,SAAS;CAGtD,YACC,MACA,MAMA,MACC;EACD,MACC,IAAI,yBAAyB,GAC7B,MACA;GAAE,GAAG;GAAM,UAAUA,eAAO,OAAO,MAA8B;EAAE,GACnE,IACD;CACD;AACD;;;;;;;;;;;;AA+BA,IAAa,WAAb,cAA8BA,eAAO,kBAAkB;CAItD,YAAY,MAAc,MAAoB,MAAuB;EACpE,MAAM,EAAE,UAAU,SAAS,QAAQ,GAAG,eAAe;EAErD,MAAM,wBAAwB,MAAM,CAAC,GAAG,UAAU;EAElD,MAAM,WAAW,IAAI,iBACpB,GAAG,KAAK,YACR;GACC,QAAQ,SAAS;GACjB,WAAW,QAAQ;GACnB,UAAU,OAAO;GACjB,MAAM,KAAK;EACZ,GACA,EAAE,QAAQ,KAAK,CAChB;EAEA,KAAK,WAAW,SAAS;EAEzB,KAAK,gBAAgB,EAAE,UAAU,KAAK,SAAS,CAAC;CACjD;AACD"}
1
+ {"version":3,"file":"role.cjs","names":["NeonClient","pulumi"],"sources":["../../src/neon/role.ts"],"sourcesContent":["import * as pulumi from \"@pulumi/pulumi\";\nimport type { NeonBranch } from \"./branch\";\nimport { NeonClient } from \"./client\";\nimport type { NeonProject } from \"./project\";\nimport type { NeonProvider } from \"./provider\";\n\n/** Resolved inputs for the Neon role dynamic provider. */\nexport interface NeonRoleInputs {\n\t/** Neon API key. */\n\tapiKey: string;\n\n\t/** Neon project ID. */\n\tprojectId: string;\n\n\t/** Branch ID the role belongs to. */\n\tbranchId: string;\n\n\t/** Role name (e.g. `\"neondb_owner\"`). */\n\tname: string;\n\n\t/**\n\t * When the role already exists (adopted), reset its password to a fresh value\n\t * instead of revealing the inherited one. Use on copy-on-write branches to\n\t * isolate the credential from the parent branch. Ignored when the role is\n\t * freshly created (a new role already gets its own password).\n\t */\n\tresetPassword: boolean;\n}\n\n/** Persisted state for the Neon role. */\ninterface NeonRoleOutputs extends NeonRoleInputs {\n\t/** Role password (auto-generated by Neon). */\n\tpassword: string;\n}\n\n/** Neon API response for the reveal_password endpoint. */\ninterface PasswordResponse {\n\tpassword: string;\n}\n\n/** Neon API response for the reset_password endpoint (nested under `role`). */\ninterface ResetPasswordResponse {\n\trole?: { password?: string };\n\tpassword?: string;\n}\n\n/** Neon API response for listing roles. */\ninterface RoleListResponse {\n\troles: Array<{\n\t\tname: string;\n\t}>;\n}\n\n/**\n * Checks if a role exists by name on a branch.\n */\nasync function roleExists(\n\tclient: NeonClient,\n\tprojectId: string,\n\tbranchId: string,\n\tname: string,\n): Promise<boolean> {\n\tconst result = await client.get<RoleListResponse>(\n\t\t`/projects/${projectId}/branches/${branchId}/roles`,\n\t);\n\n\treturn result.roles.some((r) => r.name === name);\n}\n\n/**\n * Reveals the password for an existing role via the dedicated API endpoint.\n */\nasync function revealPassword(\n\tclient: NeonClient,\n\tprojectId: string,\n\tbranchId: string,\n\tname: string,\n): Promise<string> {\n\tconst result = await client.get<PasswordResponse>(\n\t\t`/projects/${projectId}/branches/${branchId}/roles/${name}/reveal_password`,\n\t);\n\n\treturn result.password;\n}\n\n/**\n * Resets an existing role's password to a fresh value and returns it.\n * Used to isolate an adopted (copy-on-write) role from the parent branch's credential.\n */\nasync function resetRolePassword(\n\tclient: NeonClient,\n\tprojectId: string,\n\tbranchId: string,\n\tname: string,\n): Promise<string> {\n\tconst result = await client.post<ResetPasswordResponse>(\n\t\t`/projects/${projectId}/branches/${branchId}/roles/${name}/reset_password`,\n\t\t{},\n\t);\n\n\tconst password = result.role?.password ?? result.password;\n\n\tif (!password) {\n\t\tthrow new Error(\n\t\t\t`Neon reset_password returned no password for role \"${name}\"`,\n\t\t);\n\t}\n\n\treturn password;\n}\n\n/**\n * Dynamic provider implementing CRUD for Neon database roles.\n *\n * Uses adopt-or-create on `create()`: finds an existing role by name\n * before creating a new one.\n *\n * @internal Exported only for unit testing; not part of the public API surface.\n */\nexport class NeonRoleResourceProvider\n\timplements pulumi.dynamic.ResourceProvider\n{\n\tasync create(inputs: NeonRoleInputs): Promise<pulumi.dynamic.CreateResult> {\n\t\tconst client = new NeonClient(inputs.apiKey);\n\n\t\tconst exists = await roleExists(\n\t\t\tclient,\n\t\t\tinputs.projectId,\n\t\t\tinputs.branchId,\n\t\t\tinputs.name,\n\t\t);\n\n\t\tif (!exists) {\n\t\t\tawait client.post(\n\t\t\t\t`/projects/${inputs.projectId}/branches/${inputs.branchId}/roles`,\n\t\t\t\t{ role: { name: inputs.name } },\n\t\t\t);\n\t\t} else if (inputs.resetPassword) {\n\t\t\tpulumi.log.info(\n\t\t\t\t`Resetting password for adopted Neon role \"${inputs.name}\" to isolate it from the parent branch`,\n\t\t\t);\n\t\t} else {\n\t\t\tpulumi.log.info(`Adopting existing Neon role \"${inputs.name}\"`);\n\t\t}\n\n\t\tconst password =\n\t\t\texists && inputs.resetPassword\n\t\t\t\t? await resetRolePassword(\n\t\t\t\t\t\tclient,\n\t\t\t\t\t\tinputs.projectId,\n\t\t\t\t\t\tinputs.branchId,\n\t\t\t\t\t\tinputs.name,\n\t\t\t\t\t)\n\t\t\t\t: await revealPassword(\n\t\t\t\t\t\tclient,\n\t\t\t\t\t\tinputs.projectId,\n\t\t\t\t\t\tinputs.branchId,\n\t\t\t\t\t\tinputs.name,\n\t\t\t\t\t);\n\n\t\treturn {\n\t\t\tid: `${inputs.branchId}/${inputs.name}`,\n\t\t\touts: { ...inputs, password },\n\t\t};\n\t}\n\n\tasync read(\n\t\tid: string,\n\t\tprops: NeonRoleOutputs,\n\t): Promise<pulumi.dynamic.ReadResult> {\n\t\tconst client = new NeonClient(props.apiKey);\n\n\t\tconst password = await revealPassword(\n\t\t\tclient,\n\t\t\tprops.projectId,\n\t\t\tprops.branchId,\n\t\t\tprops.name,\n\t\t);\n\n\t\treturn {\n\t\t\tid,\n\t\t\tprops: { ...props, password },\n\t\t};\n\t}\n\n\tasync delete(_id: string, props: NeonRoleOutputs): Promise<void> {\n\t\tconst client = new NeonClient(props.apiKey);\n\n\t\ttry {\n\t\t\tawait client.delete(\n\t\t\t\t`/projects/${props.projectId}/branches/${props.branchId}/roles/${props.name}`,\n\t\t\t);\n\t\t} catch {\n\t\t\tpulumi.log.warn(\n\t\t\t\t`Failed to delete Neon role \"${props.name}\" (may already be deleted)`,\n\t\t\t);\n\t\t}\n\t}\n\n\tasync diff(\n\t\t_id: string,\n\t\tolds: NeonRoleOutputs,\n\t\tnews: NeonRoleInputs,\n\t): Promise<pulumi.dynamic.DiffResult> {\n\t\tconst replaces: string[] = [];\n\n\t\tif (olds.projectId !== news.projectId) {\n\t\t\treplaces.push(\"projectId\");\n\t\t}\n\n\t\tif (olds.branchId !== news.branchId) {\n\t\t\treplaces.push(\"branchId\");\n\t\t}\n\n\t\tif (olds.name !== news.name) {\n\t\t\treplaces.push(\"name\");\n\t\t}\n\n\t\treturn {\n\t\t\tchanges: replaces.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 NeonRoleResource extends pulumi.dynamic.Resource {\n\tpublic declare readonly password: pulumi.Output<string>;\n\n\tconstructor(\n\t\tname: string,\n\t\targs: {\n\t\t\tapiKey: pulumi.Input<string>;\n\t\t\tprojectId: pulumi.Input<string>;\n\t\t\tbranchId: pulumi.Input<string>;\n\t\t\tname: pulumi.Input<string>;\n\t\t\tresetPassword: pulumi.Input<boolean>;\n\t\t},\n\t\topts?: pulumi.CustomResourceOptions,\n\t) {\n\t\t// `password` is an output-only secret. Declaring it via additionalSecretOutputs\n\t\t// (rather than a pulumi.secret(undefined) input placeholder) is the only reliable\n\t\t// way to mark a dynamic-provider output secret: the placeholder gives the property\n\t\t// both a secret-undefined input and a real output, and a downstream dynamic resource\n\t\t// consuming it can race onto the undefined, producing \"Unexpected struct type\"\n\t\t// during protobuf serialization (Pulumi #16041, #3012).\n\t\tsuper(\n\t\t\tnew NeonRoleResourceProvider(),\n\t\t\tname,\n\t\t\t{ ...args, password: undefined },\n\t\t\t{ ...opts, additionalSecretOutputs: [\"password\"] },\n\t\t);\n\t}\n}\n\n/** Options type for NeonRole — replaces Pulumi's native `provider` field. */\ntype NeonRoleOptions = Omit<pulumi.ComponentResourceOptions, \"provider\"> & {\n\t/** Neon authentication context. */\n\tprovider: NeonProvider;\n\n\t/** Neon project context. */\n\tproject: NeonProject;\n\n\t/** Neon branch context. */\n\tbranch: NeonBranch;\n};\n\n/** Args for NeonRole. */\nexport interface NeonRoleArgs {\n\t/** Role name (e.g. `\"neondb_owner\"`). */\n\tname: pulumi.Input<string>;\n\n\t/**\n\t * When the role is adopted (already exists on the branch), reset its password to a\n\t * fresh value instead of inheriting the parent's. Set this on copy-on-write\n\t * (non-production) branches to isolate the credential from production. Defaults to\n\t * `false` (adopt and reveal the existing password). The reset runs once, at creation.\n\t */\n\tresetPassword?: pulumi.Input<boolean>;\n}\n\n/**\n * Manages a Neon database role with adopt-or-create semantics.\n * Exposes `password` as a secret output for connection string composition.\n *\n * @example\n * ```typescript\n * const role = new NeonRole(\"owner\", {\n * name: \"neondb_owner\",\n * }, { provider, project, branch });\n * ```\n */\nexport class NeonRole extends pulumi.ComponentResource {\n\t/** Role password (secret). */\n\tpublic readonly password: pulumi.Output<string>;\n\n\tconstructor(name: string, args: NeonRoleArgs, opts: NeonRoleOptions) {\n\t\tconst { provider, project, branch, ...pulumiOpts } = opts;\n\n\t\tsuper(\"infracraft:neon:Role\", name, {}, pulumiOpts);\n\n\t\tconst resource = new NeonRoleResource(\n\t\t\t`${name}-resource`,\n\t\t\t{\n\t\t\t\tapiKey: provider.apiKey,\n\t\t\t\tprojectId: project.id,\n\t\t\t\tbranchId: branch.id,\n\t\t\t\tname: args.name,\n\t\t\t\tresetPassword: args.resetPassword ?? false,\n\t\t\t},\n\t\t\t{ parent: this },\n\t\t);\n\n\t\tthis.password = resource.password;\n\n\t\tthis.registerOutputs({ password: this.password });\n\t}\n}\n"],"mappings":"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"}
@@ -15,6 +15,32 @@ interface NeonRoleInputs {
15
15
  branchId: string;
16
16
  /** Role name (e.g. `"neondb_owner"`). */
17
17
  name: string;
18
+ /**
19
+ * When the role already exists (adopted), reset its password to a fresh value
20
+ * instead of revealing the inherited one. Use on copy-on-write branches to
21
+ * isolate the credential from the parent branch. Ignored when the role is
22
+ * freshly created (a new role already gets its own password).
23
+ */
24
+ resetPassword: boolean;
25
+ }
26
+ /** Persisted state for the Neon role. */
27
+ interface NeonRoleOutputs extends NeonRoleInputs {
28
+ /** Role password (auto-generated by Neon). */
29
+ password: string;
30
+ }
31
+ /**
32
+ * Dynamic provider implementing CRUD for Neon database roles.
33
+ *
34
+ * Uses adopt-or-create on `create()`: finds an existing role by name
35
+ * before creating a new one.
36
+ *
37
+ * @internal Exported only for unit testing; not part of the public API surface.
38
+ */
39
+ declare class NeonRoleResourceProvider implements pulumi.dynamic.ResourceProvider {
40
+ create(inputs: NeonRoleInputs): Promise<pulumi.dynamic.CreateResult>;
41
+ read(id: string, props: NeonRoleOutputs): Promise<pulumi.dynamic.ReadResult>;
42
+ delete(_id: string, props: NeonRoleOutputs): Promise<void>;
43
+ diff(_id: string, olds: NeonRoleOutputs, news: NeonRoleInputs): Promise<pulumi.dynamic.DiffResult>;
18
44
  }
19
45
  /** Options type for NeonRole — replaces Pulumi's native `provider` field. */
20
46
  type NeonRoleOptions = Omit<pulumi.ComponentResourceOptions, "provider"> & {
@@ -26,6 +52,13 @@ type NeonRoleOptions = Omit<pulumi.ComponentResourceOptions, "provider"> & {
26
52
  interface NeonRoleArgs {
27
53
  /** Role name (e.g. `"neondb_owner"`). */
28
54
  name: pulumi.Input<string>;
55
+ /**
56
+ * When the role is adopted (already exists on the branch), reset its password to a
57
+ * fresh value instead of inheriting the parent's. Set this on copy-on-write
58
+ * (non-production) branches to isolate the credential from production. Defaults to
59
+ * `false` (adopt and reveal the existing password). The reset runs once, at creation.
60
+ */
61
+ resetPassword?: pulumi.Input<boolean>;
29
62
  }
30
63
  /**
31
64
  * Manages a Neon database role with adopt-or-create semantics.
@@ -44,5 +77,5 @@ declare class NeonRole extends pulumi.ComponentResource {
44
77
  constructor(name: string, args: NeonRoleArgs, opts: NeonRoleOptions);
45
78
  }
46
79
  //#endregion
47
- export { NeonRole, NeonRoleArgs, NeonRoleInputs };
80
+ export { NeonRole, NeonRoleArgs, NeonRoleInputs, NeonRoleResourceProvider };
48
81
  //# sourceMappingURL=role.d.cts.map
@@ -1 +1 @@
1
- {"version":3,"file":"role.d.cts","names":[],"sources":["../../src/neon/role.ts"],"mappings":";;;;;;;;UAOiB,cAAA;;EAEhB,MAAA;EAF8B;EAK9B,SAAA;EAL8B;EAQ9B,QAAA;EAHA;EAMA,IAAA;AAAA;;KAgLI,eAAA,GAAkB,IAAA,CAAK,MAAA,CAAO,wBAAA;EAA9B,mCAEJ,QAAA,EAAU,YAAA;EAGV,OAAA,EAAS,WAAA,EALa;EAQtB,MAAA,EAAQ,UAAA;AAAA;;UAIQ,YAAA;EAJE;EAMlB,IAAA,EAAM,MAAA,CAAO,KAAK;AAAA;;;;;;;;;AANA;AAInB;;cAgBa,QAAA,SAAiB,MAAA,CAAO,iBAAA;EAdlB;EAAA,SAgBF,QAAA,EAAU,MAAA,CAAO,MAAA;cAErB,IAAA,UAAc,IAAA,EAAM,YAAA,EAAc,IAAA,EAAM,eAAA;AAAA"}
1
+ {"version":3,"file":"role.d.cts","names":[],"sources":["../../src/neon/role.ts"],"mappings":";;;;;;;;UAOiB,cAAA;;EAEhB,MAAA;EAF8B;EAK9B,SAAA;EAL8B;EAQ9B,QAAA;EAHA;EAMA,IAAA;EAAA;;;AAQa;AACb;;EADA,aAAA;AAAA;AAMQ;AAAA,UAFC,eAAA,SAAwB,cAAc;EA0F/C;EAxFA,QAAQ;AAAA;;;;;;;;;cAuFI,wBAAA,YACD,MAAA,CAAO,OAAA,CAAQ,gBAAA;EAEpB,MAAA,CAAO,MAAA,EAAQ,cAAA,GAAiB,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,YAAA;EA4CvD,IAAA,CACL,EAAA,UACA,KAAA,EAAO,eAAA,GACL,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,UAAA;EAgBpB,MAAA,CAAO,GAAA,UAAa,KAAA,EAAO,eAAA,GAAkB,OAAA;EAc7C,IAAA,CACL,GAAA,UACA,IAAA,EAAM,eAAA,EACN,IAAA,EAAM,cAAA,GACJ,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,UAAA;AAAA;;KAsDtB,eAAA,GAAkB,IAAA,CAAK,MAAA,CAAO,wBAAA;EAzIR,mCA2I1B,QAAA,EAAU,YAAA,EAzIW;EA4IrB,OAAA,EAAS,WAAA,EA5I6B;EA+ItC,MAAA,EAAQ,UAAA;AAAA;;UAIQ,YAAA;EAtGf;EAwGD,IAAA,EAAM,MAAA,CAAO,KAAA;EAvGZ;;;;;;EA+GD,aAAA,GAAgB,MAAA,CAAO,KAAK;AAAA;;;;;;;;;;;;cAchB,QAAA,SAAiB,MAAA,CAAO,iBAAA;EA1FA;EAAA,SA4FpB,QAAA,EAAU,MAAA,CAAO,MAAA;cAErB,IAAA,UAAc,IAAA,EAAM,YAAA,EAAc,IAAA,EAAM,eAAA;AAAA"}
@@ -15,6 +15,32 @@ interface NeonRoleInputs {
15
15
  branchId: string;
16
16
  /** Role name (e.g. `"neondb_owner"`). */
17
17
  name: string;
18
+ /**
19
+ * When the role already exists (adopted), reset its password to a fresh value
20
+ * instead of revealing the inherited one. Use on copy-on-write branches to
21
+ * isolate the credential from the parent branch. Ignored when the role is
22
+ * freshly created (a new role already gets its own password).
23
+ */
24
+ resetPassword: boolean;
25
+ }
26
+ /** Persisted state for the Neon role. */
27
+ interface NeonRoleOutputs extends NeonRoleInputs {
28
+ /** Role password (auto-generated by Neon). */
29
+ password: string;
30
+ }
31
+ /**
32
+ * Dynamic provider implementing CRUD for Neon database roles.
33
+ *
34
+ * Uses adopt-or-create on `create()`: finds an existing role by name
35
+ * before creating a new one.
36
+ *
37
+ * @internal Exported only for unit testing; not part of the public API surface.
38
+ */
39
+ declare class NeonRoleResourceProvider implements pulumi.dynamic.ResourceProvider {
40
+ create(inputs: NeonRoleInputs): Promise<pulumi.dynamic.CreateResult>;
41
+ read(id: string, props: NeonRoleOutputs): Promise<pulumi.dynamic.ReadResult>;
42
+ delete(_id: string, props: NeonRoleOutputs): Promise<void>;
43
+ diff(_id: string, olds: NeonRoleOutputs, news: NeonRoleInputs): Promise<pulumi.dynamic.DiffResult>;
18
44
  }
19
45
  /** Options type for NeonRole — replaces Pulumi's native `provider` field. */
20
46
  type NeonRoleOptions = Omit<pulumi.ComponentResourceOptions, "provider"> & {
@@ -26,6 +52,13 @@ type NeonRoleOptions = Omit<pulumi.ComponentResourceOptions, "provider"> & {
26
52
  interface NeonRoleArgs {
27
53
  /** Role name (e.g. `"neondb_owner"`). */
28
54
  name: pulumi.Input<string>;
55
+ /**
56
+ * When the role is adopted (already exists on the branch), reset its password to a
57
+ * fresh value instead of inheriting the parent's. Set this on copy-on-write
58
+ * (non-production) branches to isolate the credential from production. Defaults to
59
+ * `false` (adopt and reveal the existing password). The reset runs once, at creation.
60
+ */
61
+ resetPassword?: pulumi.Input<boolean>;
29
62
  }
30
63
  /**
31
64
  * Manages a Neon database role with adopt-or-create semantics.
@@ -44,5 +77,5 @@ declare class NeonRole extends pulumi.ComponentResource {
44
77
  constructor(name: string, args: NeonRoleArgs, opts: NeonRoleOptions);
45
78
  }
46
79
  //#endregion
47
- export { NeonRole, NeonRoleArgs, NeonRoleInputs };
80
+ export { NeonRole, NeonRoleArgs, NeonRoleInputs, NeonRoleResourceProvider };
48
81
  //# sourceMappingURL=role.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"role.d.mts","names":[],"sources":["../../src/neon/role.ts"],"mappings":";;;;;;;;UAOiB,cAAA;;EAEhB,MAAA;EAF8B;EAK9B,SAAA;EAL8B;EAQ9B,QAAA;EAHA;EAMA,IAAA;AAAA;;KAgLI,eAAA,GAAkB,IAAA,CAAK,MAAA,CAAO,wBAAA;EAA9B,mCAEJ,QAAA,EAAU,YAAA;EAGV,OAAA,EAAS,WAAA,EALa;EAQtB,MAAA,EAAQ,UAAA;AAAA;;UAIQ,YAAA;EAJE;EAMlB,IAAA,EAAM,MAAA,CAAO,KAAK;AAAA;;;;;;;;;AANA;AAInB;;cAgBa,QAAA,SAAiB,MAAA,CAAO,iBAAA;EAdlB;EAAA,SAgBF,QAAA,EAAU,MAAA,CAAO,MAAA;cAErB,IAAA,UAAc,IAAA,EAAM,YAAA,EAAc,IAAA,EAAM,eAAA;AAAA"}
1
+ {"version":3,"file":"role.d.mts","names":[],"sources":["../../src/neon/role.ts"],"mappings":";;;;;;;;UAOiB,cAAA;;EAEhB,MAAA;EAF8B;EAK9B,SAAA;EAL8B;EAQ9B,QAAA;EAHA;EAMA,IAAA;EAAA;;;AAQa;AACb;;EADA,aAAA;AAAA;AAMQ;AAAA,UAFC,eAAA,SAAwB,cAAc;EA0F/C;EAxFA,QAAQ;AAAA;;;;;;;;;cAuFI,wBAAA,YACD,MAAA,CAAO,OAAA,CAAQ,gBAAA;EAEpB,MAAA,CAAO,MAAA,EAAQ,cAAA,GAAiB,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,YAAA;EA4CvD,IAAA,CACL,EAAA,UACA,KAAA,EAAO,eAAA,GACL,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,UAAA;EAgBpB,MAAA,CAAO,GAAA,UAAa,KAAA,EAAO,eAAA,GAAkB,OAAA;EAc7C,IAAA,CACL,GAAA,UACA,IAAA,EAAM,eAAA,EACN,IAAA,EAAM,cAAA,GACJ,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,UAAA;AAAA;;KAsDtB,eAAA,GAAkB,IAAA,CAAK,MAAA,CAAO,wBAAA;EAzIR,mCA2I1B,QAAA,EAAU,YAAA,EAzIW;EA4IrB,OAAA,EAAS,WAAA,EA5I6B;EA+ItC,MAAA,EAAQ,UAAA;AAAA;;UAIQ,YAAA;EAtGf;EAwGD,IAAA,EAAM,MAAA,CAAO,KAAA;EAvGZ;;;;;;EA+GD,aAAA,GAAgB,MAAA,CAAO,KAAK;AAAA;;;;;;;;;;;;cAchB,QAAA,SAAiB,MAAA,CAAO,iBAAA;EA1FA;EAAA,SA4FpB,QAAA,EAAU,MAAA,CAAO,MAAA;cAErB,IAAA,UAAc,IAAA,EAAM,YAAA,EAAc,IAAA,EAAM,eAAA;AAAA"}
@@ -16,17 +16,31 @@ async function revealPassword(client, projectId, branchId, name) {
16
16
  return (await client.get(`/projects/${projectId}/branches/${branchId}/roles/${name}/reveal_password`)).password;
17
17
  }
18
18
  /**
19
+ * Resets an existing role's password to a fresh value and returns it.
20
+ * Used to isolate an adopted (copy-on-write) role from the parent branch's credential.
21
+ */
22
+ async function resetRolePassword(client, projectId, branchId, name) {
23
+ const result = await client.post(`/projects/${projectId}/branches/${branchId}/roles/${name}/reset_password`, {});
24
+ const password = result.role?.password ?? result.password;
25
+ if (!password) throw new Error(`Neon reset_password returned no password for role "${name}"`);
26
+ return password;
27
+ }
28
+ /**
19
29
  * Dynamic provider implementing CRUD for Neon database roles.
20
30
  *
21
31
  * Uses adopt-or-create on `create()`: finds an existing role by name
22
32
  * before creating a new one.
33
+ *
34
+ * @internal Exported only for unit testing; not part of the public API surface.
23
35
  */
24
36
  var NeonRoleResourceProvider = class {
25
37
  async create(inputs) {
26
38
  const client = new NeonClient(inputs.apiKey);
27
- if (!await roleExists(client, inputs.projectId, inputs.branchId, inputs.name)) await client.post(`/projects/${inputs.projectId}/branches/${inputs.branchId}/roles`, { role: { name: inputs.name } });
39
+ const exists = await roleExists(client, inputs.projectId, inputs.branchId, inputs.name);
40
+ if (!exists) await client.post(`/projects/${inputs.projectId}/branches/${inputs.branchId}/roles`, { role: { name: inputs.name } });
41
+ else if (inputs.resetPassword) pulumi.log.info(`Resetting password for adopted Neon role "${inputs.name}" to isolate it from the parent branch`);
28
42
  else pulumi.log.info(`Adopting existing Neon role "${inputs.name}"`);
29
- const password = await revealPassword(client, inputs.projectId, inputs.branchId, inputs.name);
43
+ const password = exists && inputs.resetPassword ? await resetRolePassword(client, inputs.projectId, inputs.branchId, inputs.name) : await revealPassword(client, inputs.projectId, inputs.branchId, inputs.name);
30
44
  return {
31
45
  id: `${inputs.branchId}/${inputs.name}`,
32
46
  outs: {
@@ -70,8 +84,11 @@ var NeonRoleResource = class extends pulumi.dynamic.Resource {
70
84
  constructor(name, args, opts) {
71
85
  super(new NeonRoleResourceProvider(), name, {
72
86
  ...args,
73
- password: pulumi.secret(void 0)
74
- }, opts);
87
+ password: void 0
88
+ }, {
89
+ ...opts,
90
+ additionalSecretOutputs: ["password"]
91
+ });
75
92
  }
76
93
  };
77
94
  /**
@@ -93,7 +110,8 @@ var NeonRole = class extends pulumi.ComponentResource {
93
110
  apiKey: provider.apiKey,
94
111
  projectId: project.id,
95
112
  branchId: branch.id,
96
- name: args.name
113
+ name: args.name,
114
+ resetPassword: args.resetPassword ?? false
97
115
  }, { parent: this });
98
116
  this.password = resource.password;
99
117
  this.registerOutputs({ password: this.password });
@@ -101,5 +119,5 @@ var NeonRole = class extends pulumi.ComponentResource {
101
119
  };
102
120
 
103
121
  //#endregion
104
- export { NeonRole };
122
+ export { NeonRole, NeonRoleResourceProvider };
105
123
  //# sourceMappingURL=role.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"role.mjs","names":[],"sources":["../../src/neon/role.ts"],"sourcesContent":["import * as pulumi from \"@pulumi/pulumi\";\nimport type { NeonBranch } from \"./branch\";\nimport { NeonClient } from \"./client\";\nimport type { NeonProject } from \"./project\";\nimport type { NeonProvider } from \"./provider\";\n\n/** Resolved inputs for the Neon role dynamic provider. */\nexport interface NeonRoleInputs {\n\t/** Neon API key. */\n\tapiKey: string;\n\n\t/** Neon project ID. */\n\tprojectId: string;\n\n\t/** Branch ID the role belongs to. */\n\tbranchId: string;\n\n\t/** Role name (e.g. `\"neondb_owner\"`). */\n\tname: string;\n}\n\n/** Persisted state for the Neon role. */\ninterface NeonRoleOutputs extends NeonRoleInputs {\n\t/** Role password (auto-generated by Neon). */\n\tpassword: string;\n}\n\n/** Neon API response for the reveal_password endpoint. */\ninterface PasswordResponse {\n\tpassword: string;\n}\n\n/** Neon API response for listing roles. */\ninterface RoleListResponse {\n\troles: Array<{\n\t\tname: string;\n\t}>;\n}\n\n/**\n * Checks if a role exists by name on a branch.\n */\nasync function roleExists(\n\tclient: NeonClient,\n\tprojectId: string,\n\tbranchId: string,\n\tname: string,\n): Promise<boolean> {\n\tconst result = await client.get<RoleListResponse>(\n\t\t`/projects/${projectId}/branches/${branchId}/roles`,\n\t);\n\n\treturn result.roles.some((r) => r.name === name);\n}\n\n/**\n * Reveals the password for an existing role via the dedicated API endpoint.\n */\nasync function revealPassword(\n\tclient: NeonClient,\n\tprojectId: string,\n\tbranchId: string,\n\tname: string,\n): Promise<string> {\n\tconst result = await client.get<PasswordResponse>(\n\t\t`/projects/${projectId}/branches/${branchId}/roles/${name}/reveal_password`,\n\t);\n\n\treturn result.password;\n}\n\n/**\n * Dynamic provider implementing CRUD for Neon database roles.\n *\n * Uses adopt-or-create on `create()`: finds an existing role by name\n * before creating a new one.\n */\nclass NeonRoleResourceProvider implements pulumi.dynamic.ResourceProvider {\n\tasync create(inputs: NeonRoleInputs): Promise<pulumi.dynamic.CreateResult> {\n\t\tconst client = new NeonClient(inputs.apiKey);\n\n\t\tconst exists = await roleExists(\n\t\t\tclient,\n\t\t\tinputs.projectId,\n\t\t\tinputs.branchId,\n\t\t\tinputs.name,\n\t\t);\n\n\t\tif (!exists) {\n\t\t\tawait client.post(\n\t\t\t\t`/projects/${inputs.projectId}/branches/${inputs.branchId}/roles`,\n\t\t\t\t{ role: { name: inputs.name } },\n\t\t\t);\n\t\t} else {\n\t\t\tpulumi.log.info(`Adopting existing Neon role \"${inputs.name}\"`);\n\t\t}\n\n\t\tconst password = await revealPassword(\n\t\t\tclient,\n\t\t\tinputs.projectId,\n\t\t\tinputs.branchId,\n\t\t\tinputs.name,\n\t\t);\n\n\t\treturn {\n\t\t\tid: `${inputs.branchId}/${inputs.name}`,\n\t\t\touts: { ...inputs, password },\n\t\t};\n\t}\n\n\tasync read(\n\t\tid: string,\n\t\tprops: NeonRoleOutputs,\n\t): Promise<pulumi.dynamic.ReadResult> {\n\t\tconst client = new NeonClient(props.apiKey);\n\n\t\tconst password = await revealPassword(\n\t\t\tclient,\n\t\t\tprops.projectId,\n\t\t\tprops.branchId,\n\t\t\tprops.name,\n\t\t);\n\n\t\treturn {\n\t\t\tid,\n\t\t\tprops: { ...props, password },\n\t\t};\n\t}\n\n\tasync delete(_id: string, props: NeonRoleOutputs): Promise<void> {\n\t\tconst client = new NeonClient(props.apiKey);\n\n\t\ttry {\n\t\t\tawait client.delete(\n\t\t\t\t`/projects/${props.projectId}/branches/${props.branchId}/roles/${props.name}`,\n\t\t\t);\n\t\t} catch {\n\t\t\tpulumi.log.warn(\n\t\t\t\t`Failed to delete Neon role \"${props.name}\" (may already be deleted)`,\n\t\t\t);\n\t\t}\n\t}\n\n\tasync diff(\n\t\t_id: string,\n\t\tolds: NeonRoleOutputs,\n\t\tnews: NeonRoleInputs,\n\t): Promise<pulumi.dynamic.DiffResult> {\n\t\tconst replaces: string[] = [];\n\n\t\tif (olds.projectId !== news.projectId) {\n\t\t\treplaces.push(\"projectId\");\n\t\t}\n\n\t\tif (olds.branchId !== news.branchId) {\n\t\t\treplaces.push(\"branchId\");\n\t\t}\n\n\t\tif (olds.name !== news.name) {\n\t\t\treplaces.push(\"name\");\n\t\t}\n\n\t\treturn {\n\t\t\tchanges: replaces.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 NeonRoleResource extends pulumi.dynamic.Resource {\n\tpublic declare readonly password: pulumi.Output<string>;\n\n\tconstructor(\n\t\tname: string,\n\t\targs: {\n\t\t\tapiKey: pulumi.Input<string>;\n\t\t\tprojectId: pulumi.Input<string>;\n\t\t\tbranchId: pulumi.Input<string>;\n\t\t\tname: pulumi.Input<string>;\n\t\t},\n\t\topts?: pulumi.CustomResourceOptions,\n\t) {\n\t\tsuper(\n\t\t\tnew NeonRoleResourceProvider(),\n\t\t\tname,\n\t\t\t{ ...args, password: pulumi.secret(undefined as unknown as string) },\n\t\t\topts,\n\t\t);\n\t}\n}\n\n/** Options type for NeonRole — replaces Pulumi's native `provider` field. */\ntype NeonRoleOptions = Omit<pulumi.ComponentResourceOptions, \"provider\"> & {\n\t/** Neon authentication context. */\n\tprovider: NeonProvider;\n\n\t/** Neon project context. */\n\tproject: NeonProject;\n\n\t/** Neon branch context. */\n\tbranch: NeonBranch;\n};\n\n/** Args for NeonRole. */\nexport interface NeonRoleArgs {\n\t/** Role name (e.g. `\"neondb_owner\"`). */\n\tname: pulumi.Input<string>;\n}\n\n/**\n * Manages a Neon database role with adopt-or-create semantics.\n * Exposes `password` as a secret output for connection string composition.\n *\n * @example\n * ```typescript\n * const role = new NeonRole(\"owner\", {\n * name: \"neondb_owner\",\n * }, { provider, project, branch });\n * ```\n */\nexport class NeonRole extends pulumi.ComponentResource {\n\t/** Role password (secret). */\n\tpublic readonly password: pulumi.Output<string>;\n\n\tconstructor(name: string, args: NeonRoleArgs, opts: NeonRoleOptions) {\n\t\tconst { provider, project, branch, ...pulumiOpts } = opts;\n\n\t\tsuper(\"infracraft:neon:Role\", name, {}, pulumiOpts);\n\n\t\tconst resource = new NeonRoleResource(\n\t\t\t`${name}-resource`,\n\t\t\t{\n\t\t\t\tapiKey: provider.apiKey,\n\t\t\t\tprojectId: project.id,\n\t\t\t\tbranchId: branch.id,\n\t\t\t\tname: args.name,\n\t\t\t},\n\t\t\t{ parent: this },\n\t\t);\n\n\t\tthis.password = resource.password;\n\n\t\tthis.registerOutputs({ password: this.password });\n\t}\n}\n"],"mappings":";;;;;;;;AA0CA,eAAe,WACd,QACA,WACA,UACA,MACmB;CAKnB,QAAO,MAJc,OAAO,IAC3B,aAAa,UAAU,YAAY,SAAS,OAC7C,GAEc,MAAM,MAAM,MAAM,EAAE,SAAS,IAAI;AAChD;;;;AAKA,eAAe,eACd,QACA,WACA,UACA,MACkB;CAKlB,QAAO,MAJc,OAAO,IAC3B,aAAa,UAAU,YAAY,SAAS,SAAS,KAAK,iBAC3D,GAEc;AACf;;;;;;;AAQA,IAAM,2BAAN,MAA0E;CACzE,MAAM,OAAO,QAA8D;EAC1E,MAAM,SAAS,IAAI,WAAW,OAAO,MAAM;EAS3C,IAAI,CAAC,MAPgB,WACpB,QACA,OAAO,WACP,OAAO,UACP,OAAO,IACR,GAGC,MAAM,OAAO,KACZ,aAAa,OAAO,UAAU,YAAY,OAAO,SAAS,SAC1D,EAAE,MAAM,EAAE,MAAM,OAAO,KAAK,EAAE,CAC/B;OAEA,OAAO,IAAI,KAAK,gCAAgC,OAAO,KAAK,EAAE;EAG/D,MAAM,WAAW,MAAM,eACtB,QACA,OAAO,WACP,OAAO,UACP,OAAO,IACR;EAEA,OAAO;GACN,IAAI,GAAG,OAAO,SAAS,GAAG,OAAO;GACjC,MAAM;IAAE,GAAG;IAAQ;GAAS;EAC7B;CACD;CAEA,MAAM,KACL,IACA,OACqC;EAGrC,MAAM,WAAW,MAAM,eACtB,IAHkB,WAAW,MAAM,MAG9B,GACL,MAAM,WACN,MAAM,UACN,MAAM,IACP;EAEA,OAAO;GACN;GACA,OAAO;IAAE,GAAG;IAAO;GAAS;EAC7B;CACD;CAEA,MAAM,OAAO,KAAa,OAAuC;EAChE,MAAM,SAAS,IAAI,WAAW,MAAM,MAAM;EAE1C,IAAI;GACH,MAAM,OAAO,OACZ,aAAa,MAAM,UAAU,YAAY,MAAM,SAAS,SAAS,MAAM,MACxE;EACD,QAAQ;GACP,OAAO,IAAI,KACV,+BAA+B,MAAM,KAAK,2BAC3C;EACD;CACD;CAEA,MAAM,KACL,KACA,MACA,MACqC;EACrC,MAAM,WAAqB,CAAC;EAE5B,IAAI,KAAK,cAAc,KAAK,WAC3B,SAAS,KAAK,WAAW;EAG1B,IAAI,KAAK,aAAa,KAAK,UAC1B,SAAS,KAAK,UAAU;EAGzB,IAAI,KAAK,SAAS,KAAK,MACtB,SAAS,KAAK,MAAM;EAGrB,OAAO;GACN,SAAS,SAAS,SAAS;GAC3B;GACA,qBAAqB;EACtB;CACD;AACD;;AAGA,IAAM,mBAAN,cAA+B,OAAO,QAAQ,SAAS;CAGtD,YACC,MACA,MAMA,MACC;EACD,MACC,IAAI,yBAAyB,GAC7B,MACA;GAAE,GAAG;GAAM,UAAU,OAAO,OAAO,MAA8B;EAAE,GACnE,IACD;CACD;AACD;;;;;;;;;;;;AA+BA,IAAa,WAAb,cAA8B,OAAO,kBAAkB;CAItD,YAAY,MAAc,MAAoB,MAAuB;EACpE,MAAM,EAAE,UAAU,SAAS,QAAQ,GAAG,eAAe;EAErD,MAAM,wBAAwB,MAAM,CAAC,GAAG,UAAU;EAElD,MAAM,WAAW,IAAI,iBACpB,GAAG,KAAK,YACR;GACC,QAAQ,SAAS;GACjB,WAAW,QAAQ;GACnB,UAAU,OAAO;GACjB,MAAM,KAAK;EACZ,GACA,EAAE,QAAQ,KAAK,CAChB;EAEA,KAAK,WAAW,SAAS;EAEzB,KAAK,gBAAgB,EAAE,UAAU,KAAK,SAAS,CAAC;CACjD;AACD"}
1
+ {"version":3,"file":"role.mjs","names":[],"sources":["../../src/neon/role.ts"],"sourcesContent":["import * as pulumi from \"@pulumi/pulumi\";\nimport type { NeonBranch } from \"./branch\";\nimport { NeonClient } from \"./client\";\nimport type { NeonProject } from \"./project\";\nimport type { NeonProvider } from \"./provider\";\n\n/** Resolved inputs for the Neon role dynamic provider. */\nexport interface NeonRoleInputs {\n\t/** Neon API key. */\n\tapiKey: string;\n\n\t/** Neon project ID. */\n\tprojectId: string;\n\n\t/** Branch ID the role belongs to. */\n\tbranchId: string;\n\n\t/** Role name (e.g. `\"neondb_owner\"`). */\n\tname: string;\n\n\t/**\n\t * When the role already exists (adopted), reset its password to a fresh value\n\t * instead of revealing the inherited one. Use on copy-on-write branches to\n\t * isolate the credential from the parent branch. Ignored when the role is\n\t * freshly created (a new role already gets its own password).\n\t */\n\tresetPassword: boolean;\n}\n\n/** Persisted state for the Neon role. */\ninterface NeonRoleOutputs extends NeonRoleInputs {\n\t/** Role password (auto-generated by Neon). */\n\tpassword: string;\n}\n\n/** Neon API response for the reveal_password endpoint. */\ninterface PasswordResponse {\n\tpassword: string;\n}\n\n/** Neon API response for the reset_password endpoint (nested under `role`). */\ninterface ResetPasswordResponse {\n\trole?: { password?: string };\n\tpassword?: string;\n}\n\n/** Neon API response for listing roles. */\ninterface RoleListResponse {\n\troles: Array<{\n\t\tname: string;\n\t}>;\n}\n\n/**\n * Checks if a role exists by name on a branch.\n */\nasync function roleExists(\n\tclient: NeonClient,\n\tprojectId: string,\n\tbranchId: string,\n\tname: string,\n): Promise<boolean> {\n\tconst result = await client.get<RoleListResponse>(\n\t\t`/projects/${projectId}/branches/${branchId}/roles`,\n\t);\n\n\treturn result.roles.some((r) => r.name === name);\n}\n\n/**\n * Reveals the password for an existing role via the dedicated API endpoint.\n */\nasync function revealPassword(\n\tclient: NeonClient,\n\tprojectId: string,\n\tbranchId: string,\n\tname: string,\n): Promise<string> {\n\tconst result = await client.get<PasswordResponse>(\n\t\t`/projects/${projectId}/branches/${branchId}/roles/${name}/reveal_password`,\n\t);\n\n\treturn result.password;\n}\n\n/**\n * Resets an existing role's password to a fresh value and returns it.\n * Used to isolate an adopted (copy-on-write) role from the parent branch's credential.\n */\nasync function resetRolePassword(\n\tclient: NeonClient,\n\tprojectId: string,\n\tbranchId: string,\n\tname: string,\n): Promise<string> {\n\tconst result = await client.post<ResetPasswordResponse>(\n\t\t`/projects/${projectId}/branches/${branchId}/roles/${name}/reset_password`,\n\t\t{},\n\t);\n\n\tconst password = result.role?.password ?? result.password;\n\n\tif (!password) {\n\t\tthrow new Error(\n\t\t\t`Neon reset_password returned no password for role \"${name}\"`,\n\t\t);\n\t}\n\n\treturn password;\n}\n\n/**\n * Dynamic provider implementing CRUD for Neon database roles.\n *\n * Uses adopt-or-create on `create()`: finds an existing role by name\n * before creating a new one.\n *\n * @internal Exported only for unit testing; not part of the public API surface.\n */\nexport class NeonRoleResourceProvider\n\timplements pulumi.dynamic.ResourceProvider\n{\n\tasync create(inputs: NeonRoleInputs): Promise<pulumi.dynamic.CreateResult> {\n\t\tconst client = new NeonClient(inputs.apiKey);\n\n\t\tconst exists = await roleExists(\n\t\t\tclient,\n\t\t\tinputs.projectId,\n\t\t\tinputs.branchId,\n\t\t\tinputs.name,\n\t\t);\n\n\t\tif (!exists) {\n\t\t\tawait client.post(\n\t\t\t\t`/projects/${inputs.projectId}/branches/${inputs.branchId}/roles`,\n\t\t\t\t{ role: { name: inputs.name } },\n\t\t\t);\n\t\t} else if (inputs.resetPassword) {\n\t\t\tpulumi.log.info(\n\t\t\t\t`Resetting password for adopted Neon role \"${inputs.name}\" to isolate it from the parent branch`,\n\t\t\t);\n\t\t} else {\n\t\t\tpulumi.log.info(`Adopting existing Neon role \"${inputs.name}\"`);\n\t\t}\n\n\t\tconst password =\n\t\t\texists && inputs.resetPassword\n\t\t\t\t? await resetRolePassword(\n\t\t\t\t\t\tclient,\n\t\t\t\t\t\tinputs.projectId,\n\t\t\t\t\t\tinputs.branchId,\n\t\t\t\t\t\tinputs.name,\n\t\t\t\t\t)\n\t\t\t\t: await revealPassword(\n\t\t\t\t\t\tclient,\n\t\t\t\t\t\tinputs.projectId,\n\t\t\t\t\t\tinputs.branchId,\n\t\t\t\t\t\tinputs.name,\n\t\t\t\t\t);\n\n\t\treturn {\n\t\t\tid: `${inputs.branchId}/${inputs.name}`,\n\t\t\touts: { ...inputs, password },\n\t\t};\n\t}\n\n\tasync read(\n\t\tid: string,\n\t\tprops: NeonRoleOutputs,\n\t): Promise<pulumi.dynamic.ReadResult> {\n\t\tconst client = new NeonClient(props.apiKey);\n\n\t\tconst password = await revealPassword(\n\t\t\tclient,\n\t\t\tprops.projectId,\n\t\t\tprops.branchId,\n\t\t\tprops.name,\n\t\t);\n\n\t\treturn {\n\t\t\tid,\n\t\t\tprops: { ...props, password },\n\t\t};\n\t}\n\n\tasync delete(_id: string, props: NeonRoleOutputs): Promise<void> {\n\t\tconst client = new NeonClient(props.apiKey);\n\n\t\ttry {\n\t\t\tawait client.delete(\n\t\t\t\t`/projects/${props.projectId}/branches/${props.branchId}/roles/${props.name}`,\n\t\t\t);\n\t\t} catch {\n\t\t\tpulumi.log.warn(\n\t\t\t\t`Failed to delete Neon role \"${props.name}\" (may already be deleted)`,\n\t\t\t);\n\t\t}\n\t}\n\n\tasync diff(\n\t\t_id: string,\n\t\tolds: NeonRoleOutputs,\n\t\tnews: NeonRoleInputs,\n\t): Promise<pulumi.dynamic.DiffResult> {\n\t\tconst replaces: string[] = [];\n\n\t\tif (olds.projectId !== news.projectId) {\n\t\t\treplaces.push(\"projectId\");\n\t\t}\n\n\t\tif (olds.branchId !== news.branchId) {\n\t\t\treplaces.push(\"branchId\");\n\t\t}\n\n\t\tif (olds.name !== news.name) {\n\t\t\treplaces.push(\"name\");\n\t\t}\n\n\t\treturn {\n\t\t\tchanges: replaces.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 NeonRoleResource extends pulumi.dynamic.Resource {\n\tpublic declare readonly password: pulumi.Output<string>;\n\n\tconstructor(\n\t\tname: string,\n\t\targs: {\n\t\t\tapiKey: pulumi.Input<string>;\n\t\t\tprojectId: pulumi.Input<string>;\n\t\t\tbranchId: pulumi.Input<string>;\n\t\t\tname: pulumi.Input<string>;\n\t\t\tresetPassword: pulumi.Input<boolean>;\n\t\t},\n\t\topts?: pulumi.CustomResourceOptions,\n\t) {\n\t\t// `password` is an output-only secret. Declaring it via additionalSecretOutputs\n\t\t// (rather than a pulumi.secret(undefined) input placeholder) is the only reliable\n\t\t// way to mark a dynamic-provider output secret: the placeholder gives the property\n\t\t// both a secret-undefined input and a real output, and a downstream dynamic resource\n\t\t// consuming it can race onto the undefined, producing \"Unexpected struct type\"\n\t\t// during protobuf serialization (Pulumi #16041, #3012).\n\t\tsuper(\n\t\t\tnew NeonRoleResourceProvider(),\n\t\t\tname,\n\t\t\t{ ...args, password: undefined },\n\t\t\t{ ...opts, additionalSecretOutputs: [\"password\"] },\n\t\t);\n\t}\n}\n\n/** Options type for NeonRole — replaces Pulumi's native `provider` field. */\ntype NeonRoleOptions = Omit<pulumi.ComponentResourceOptions, \"provider\"> & {\n\t/** Neon authentication context. */\n\tprovider: NeonProvider;\n\n\t/** Neon project context. */\n\tproject: NeonProject;\n\n\t/** Neon branch context. */\n\tbranch: NeonBranch;\n};\n\n/** Args for NeonRole. */\nexport interface NeonRoleArgs {\n\t/** Role name (e.g. `\"neondb_owner\"`). */\n\tname: pulumi.Input<string>;\n\n\t/**\n\t * When the role is adopted (already exists on the branch), reset its password to a\n\t * fresh value instead of inheriting the parent's. Set this on copy-on-write\n\t * (non-production) branches to isolate the credential from production. Defaults to\n\t * `false` (adopt and reveal the existing password). The reset runs once, at creation.\n\t */\n\tresetPassword?: pulumi.Input<boolean>;\n}\n\n/**\n * Manages a Neon database role with adopt-or-create semantics.\n * Exposes `password` as a secret output for connection string composition.\n *\n * @example\n * ```typescript\n * const role = new NeonRole(\"owner\", {\n * name: \"neondb_owner\",\n * }, { provider, project, branch });\n * ```\n */\nexport class NeonRole extends pulumi.ComponentResource {\n\t/** Role password (secret). */\n\tpublic readonly password: pulumi.Output<string>;\n\n\tconstructor(name: string, args: NeonRoleArgs, opts: NeonRoleOptions) {\n\t\tconst { provider, project, branch, ...pulumiOpts } = opts;\n\n\t\tsuper(\"infracraft:neon:Role\", name, {}, pulumiOpts);\n\n\t\tconst resource = new NeonRoleResource(\n\t\t\t`${name}-resource`,\n\t\t\t{\n\t\t\t\tapiKey: provider.apiKey,\n\t\t\t\tprojectId: project.id,\n\t\t\t\tbranchId: branch.id,\n\t\t\t\tname: args.name,\n\t\t\t\tresetPassword: args.resetPassword ?? false,\n\t\t\t},\n\t\t\t{ parent: this },\n\t\t);\n\n\t\tthis.password = resource.password;\n\n\t\tthis.registerOutputs({ password: this.password });\n\t}\n}\n"],"mappings":"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"}
@@ -105,9 +105,12 @@ var RailwayProjectTokenResource = class extends _pulumi_pulumi.dynamic.Resource
105
105
  constructor(name, args, opts) {
106
106
  super(new RailwayProjectTokenResourceProvider(), name, {
107
107
  ...args,
108
- value: _pulumi_pulumi.secret(void 0),
108
+ value: void 0,
109
109
  tokenId: void 0
110
- }, opts);
110
+ }, {
111
+ ...opts,
112
+ additionalSecretOutputs: ["value"]
113
+ });
111
114
  }
112
115
  };
113
116
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"project-token.cjs","names":["RailwayClient","pulumi"],"sources":["../../src/railway/project-token.ts"],"sourcesContent":["import * as pulumi from \"@pulumi/pulumi\";\nimport { RailwayClient } from \"./client\";\nimport type { RailwayEnvironment } from \"./environment\";\nimport type { RailwayProject } from \"./project\";\nimport type { RailwayProvider } from \"./provider\";\n\n/** Resolved inputs for the Railway project token dynamic provider. */\ninterface RailwayProjectTokenInputs {\n\t/** Railway API bearer token (account-scoped, used for API calls). */\n\ttoken: string;\n\n\t/** Railway project UUID. */\n\tprojectId: string;\n\n\t/** Railway environment UUID this deploy token is scoped to. */\n\tenvironmentId: string;\n\n\t/** Distinct token name, e.g. `\"pulumi-staging\"`. Must be unique per environment. */\n\tname: string;\n}\n\n/** Persisted state for the Railway project token. */\ninterface RailwayProjectTokenOutputs extends RailwayProjectTokenInputs {\n\t/** Minted deploy token secret value. */\n\tvalue: string;\n\n\t/** Railway-assigned token UUID (used for clean teardown on delete). */\n\ttokenId: string;\n}\n\nconst PROJECT_TOKENS_QUERY = `\n query($projectId: String!) {\n projectTokens(projectId: $projectId) {\n edges { node { id name } }\n }\n }\n`;\n\nconst PROJECT_TOKEN_CREATE = `\n mutation($input: ProjectTokenCreateInput!) {\n projectTokenCreate(input: $input)\n }\n`;\n\nconst PROJECT_TOKEN_DELETE = `\n mutation($id: String!) { projectTokenDelete(id: $id) }\n`;\n\n/**\n * Dynamic provider that mints a Railway environment-scoped deploy token.\n *\n * On create, it deletes any existing tokens with the same name (stale tokens\n * from previous runs), mints a fresh one scoped to the target environment, then\n * re-lists tokens to capture the Railway-assigned ID for later teardown.\n *\n * @internal Exported only for unit testing; not part of the public API surface.\n */\nexport class RailwayProjectTokenResourceProvider\n\timplements pulumi.dynamic.ResourceProvider\n{\n\t/**\n\t * Deletes stale same-named tokens, mints a new environment-scoped token,\n\t * and captures its ID via a follow-up list query.\n\t *\n\t * @param inputs Resolved provider inputs.\n\t * @returns Pulumi dynamic create result with `value` (secret) and `tokenId`.\n\t */\n\tasync create(\n\t\tinputs: RailwayProjectTokenInputs,\n\t): Promise<pulumi.dynamic.CreateResult> {\n\t\tconst client = new RailwayClient(inputs.token);\n\n\t\tconst tokensResult = await client.query<{\n\t\t\tprojectTokens: {\n\t\t\t\tedges: Array<{ node: { id: string; name: string } }>;\n\t\t\t};\n\t\t}>(PROJECT_TOKENS_QUERY, { projectId: inputs.projectId });\n\n\t\tconst stale = tokensResult.projectTokens.edges.filter(\n\t\t\t(edge) => edge.node.name === inputs.name,\n\t\t);\n\n\t\tfor (const entry of stale) {\n\t\t\tawait client.query(PROJECT_TOKEN_DELETE, { id: entry.node.id });\n\t\t}\n\n\t\tconst createResult = await client.query<{ projectTokenCreate: string }>(\n\t\t\tPROJECT_TOKEN_CREATE,\n\t\t\t{\n\t\t\t\tinput: {\n\t\t\t\t\tprojectId: inputs.projectId,\n\t\t\t\t\tenvironmentId: inputs.environmentId,\n\t\t\t\t\tname: inputs.name,\n\t\t\t\t},\n\t\t\t},\n\t\t);\n\n\t\tconst value = createResult.projectTokenCreate;\n\n\t\t// Re-list tokens to capture the Railway-assigned ID: the create mutation\n\t\t// returns only the token value, not its UUID.\n\t\tconst refreshResult = await client.query<{\n\t\t\tprojectTokens: {\n\t\t\t\tedges: Array<{ node: { id: string; name: string } }>;\n\t\t\t};\n\t\t}>(PROJECT_TOKENS_QUERY, { projectId: inputs.projectId });\n\n\t\t// After the delete sweep above, exactly one token with this name exists (the one we just\n\t\t// created). Resolve its id from the re-list so delete() can revoke it later.\n\t\tconst found = refreshResult.projectTokens.edges.find(\n\t\t\t(edge) => edge.node.name === inputs.name,\n\t\t);\n\n\t\tif (!found) {\n\t\t\tthrow new Error(\n\t\t\t\t`Could not resolve token id for newly created token \"${inputs.name}\" in project ${inputs.projectId}`,\n\t\t\t);\n\t\t}\n\n\t\tconst tokenId = found.node.id;\n\n\t\tconst outs: RailwayProjectTokenOutputs = {\n\t\t\t...inputs,\n\t\t\tvalue,\n\t\t\ttokenId,\n\t\t};\n\n\t\treturn { id: `${inputs.projectId}:${inputs.name}`, outs };\n\t}\n\n\t/**\n\t * Pass-through read — the token value is a write-once secret that Railway\n\t * never re-exposes via API, so the stored state is the only source of truth.\n\t *\n\t * @param id Resource ID.\n\t * @param props Currently stored outputs.\n\t */\n\tasync read(\n\t\tid: string,\n\t\tprops: RailwayProjectTokenOutputs,\n\t): Promise<pulumi.dynamic.ReadResult> {\n\t\treturn { id, props };\n\t}\n\n\t/**\n\t * Deletes the Railway token by its stored UUID for clean teardown.\n\t *\n\t * @param _id Resource ID (unused).\n\t * @param props Currently stored outputs containing the tokenId.\n\t */\n\tasync delete(_id: string, props: RailwayProjectTokenOutputs): Promise<void> {\n\t\tif (props.tokenId) {\n\t\t\tconst client = new RailwayClient(props.token);\n\n\t\t\tawait client.query(PROJECT_TOKEN_DELETE, { id: props.tokenId });\n\t\t}\n\t}\n\n\t/**\n\t * Triggers replacement when the project, environment, or token name changes.\n\t *\n\t * @param _id Resource ID (unused).\n\t * @param olds Previously stored outputs.\n\t * @param news Newly resolved inputs.\n\t */\n\tasync diff(\n\t\t_id: string,\n\t\tolds: RailwayProjectTokenOutputs,\n\t\tnews: RailwayProjectTokenInputs,\n\t): Promise<pulumi.dynamic.DiffResult> {\n\t\tconst replaces: string[] = [];\n\n\t\tif (olds.projectId !== news.projectId) {\n\t\t\treplaces.push(\"projectId\");\n\t\t}\n\n\t\tif (olds.environmentId !== news.environmentId) {\n\t\t\treplaces.push(\"environmentId\");\n\t\t}\n\n\t\tif (olds.name !== news.name) {\n\t\t\treplaces.push(\"name\");\n\t\t}\n\n\t\treturn {\n\t\t\tchanges: replaces.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 RailwayProjectTokenResource extends pulumi.dynamic.Resource {\n\tpublic declare readonly value: pulumi.Output<string>;\n\tpublic declare readonly tokenId: pulumi.Output<string>;\n\n\tconstructor(\n\t\tname: string,\n\t\targs: {\n\t\t\ttoken: pulumi.Input<string>;\n\t\t\tprojectId: pulumi.Input<string>;\n\t\t\tenvironmentId: pulumi.Input<string>;\n\t\t\tname: pulumi.Input<string>;\n\t\t},\n\t\topts?: pulumi.CustomResourceOptions,\n\t) {\n\t\tsuper(\n\t\t\tnew RailwayProjectTokenResourceProvider(),\n\t\t\tname,\n\t\t\t{\n\t\t\t\t...args,\n\t\t\t\tvalue: pulumi.secret(undefined as unknown as string),\n\t\t\t\ttokenId: undefined,\n\t\t\t},\n\t\t\topts,\n\t\t);\n\t}\n}\n\n/** Options type for RailwayProjectToken — replaces Pulumi's native `provider` field. */\ntype RailwayProjectTokenOptions = Omit<\n\tpulumi.ComponentResourceOptions,\n\t\"provider\"\n> & {\n\t/** Railway authentication context. */\n\tprovider: RailwayProvider;\n\n\t/** Railway project this token belongs to. */\n\tproject: RailwayProject;\n\n\t/** Railway environment this deploy token is scoped to. */\n\tenvironment: RailwayEnvironment;\n};\n\n/** Args for RailwayProjectToken. */\nexport interface RailwayProjectTokenArgs {\n\t/**\n\t * Distinct token name, e.g. `\"pulumi-staging\"`.\n\t * Each environment should use a unique name so multiple stacks sharing\n\t * a project never collide on token ownership.\n\t */\n\tname: pulumi.Input<string>;\n}\n\n/**\n * Provisions an environment-scoped Railway deploy token.\n *\n * Each environment gets its own correctly-scoped token with a distinct name,\n * so multiple stacks sharing the same Railway project never collide.\n * The token value is exposed as a secret output for use in {@link RailwayDeploy}.\n *\n * @example\n * ```typescript\n * const project = new RailwayProject(\"my-project\", { name: \"my-app\" }, { provider });\n * const staging = new RailwayEnvironment(\"staging\", { name: \"staging\" }, { provider, project });\n *\n * const stagingToken = new RailwayProjectToken(\"staging-token\", {\n * name: \"pulumi-staging\",\n * }, { provider, project, environment: staging });\n *\n * new RailwayDeploy(\"api-deploy\", {\n * directory: monorepoRoot,\n * triggers: [sourceHash],\n * }, { provider, project, environment: staging, service, projectToken: stagingToken.token });\n * ```\n */\nexport class RailwayProjectToken extends pulumi.ComponentResource {\n\t/**\n\t * Environment-scoped Railway deploy token value (secret).\n\t * Pass this to `RailwayDeployOptions.projectToken`.\n\t */\n\tpublic readonly token: pulumi.Output<string>;\n\n\tconstructor(\n\t\tname: string,\n\t\targs: RailwayProjectTokenArgs,\n\t\topts: RailwayProjectTokenOptions,\n\t) {\n\t\tconst { provider, project, environment, ...pulumiOpts } = opts;\n\n\t\tsuper(\"infracraft:railway:ProjectToken\", name, {}, pulumiOpts);\n\n\t\tconst resource = new RailwayProjectTokenResource(\n\t\t\t`${name}-resource`,\n\t\t\t{\n\t\t\t\ttoken: provider.token,\n\t\t\t\tprojectId: project.id,\n\t\t\t\tenvironmentId: environment.id,\n\t\t\t\tname: args.name,\n\t\t\t},\n\t\t\t{ parent: this },\n\t\t);\n\n\t\tthis.token = resource.value;\n\n\t\tthis.registerOutputs({ token: this.token });\n\t}\n}\n"],"mappings":";;;;;;;AA8BA,MAAM,uBAAuB;;;;;;;AAQ7B,MAAM,uBAAuB;;;;;AAM7B,MAAM,uBAAuB;;;;;;;;;;;;AAa7B,IAAa,sCAAb,MAEA;;;;;;;;CAQC,MAAM,OACL,QACuC;EACvC,MAAM,SAAS,IAAIA,qCAAc,OAAO,KAAK;EAQ7C,MAAM,SAAQ,MANa,OAAO,MAI/B,sBAAsB,EAAE,WAAW,OAAO,UAAU,CAAC,GAE7B,cAAc,MAAM,QAC7C,SAAS,KAAK,KAAK,SAAS,OAAO,IACrC;EAEA,KAAK,MAAM,SAAS,OACnB,MAAM,OAAO,MAAM,sBAAsB,EAAE,IAAI,MAAM,KAAK,GAAG,CAAC;EAc/D,MAAM,SAAQ,MAXa,OAAO,MACjC,sBACA,EACC,OAAO;GACN,WAAW,OAAO;GAClB,eAAe,OAAO;GACtB,MAAM,OAAO;EACd,EACD,CACD,GAE2B;EAY3B,MAAM,SAAQ,MARc,OAAO,MAIhC,sBAAsB,EAAE,WAAW,OAAO,UAAU,CAAC,GAI5B,cAAc,MAAM,MAC9C,SAAS,KAAK,KAAK,SAAS,OAAO,IACrC;EAEA,IAAI,CAAC,OACJ,MAAM,IAAI,MACT,uDAAuD,OAAO,KAAK,eAAe,OAAO,WAC1F;EAGD,MAAM,UAAU,MAAM,KAAK;EAE3B,MAAM,OAAmC;GACxC,GAAG;GACH;GACA;EACD;EAEA,OAAO;GAAE,IAAI,GAAG,OAAO,UAAU,GAAG,OAAO;GAAQ;EAAK;CACzD;;;;;;;;CASA,MAAM,KACL,IACA,OACqC;EACrC,OAAO;GAAE;GAAI;EAAM;CACpB;;;;;;;CAQA,MAAM,OAAO,KAAa,OAAkD;EAC3E,IAAI,MAAM,SAGT,MAAM,IAFaA,qCAAc,MAAM,KAE5B,EAAE,MAAM,sBAAsB,EAAE,IAAI,MAAM,QAAQ,CAAC;CAEhE;;;;;;;;CASA,MAAM,KACL,KACA,MACA,MACqC;EACrC,MAAM,WAAqB,CAAC;EAE5B,IAAI,KAAK,cAAc,KAAK,WAC3B,SAAS,KAAK,WAAW;EAG1B,IAAI,KAAK,kBAAkB,KAAK,eAC/B,SAAS,KAAK,eAAe;EAG9B,IAAI,KAAK,SAAS,KAAK,MACtB,SAAS,KAAK,MAAM;EAGrB,OAAO;GACN,SAAS,SAAS,SAAS;GAC3B;GACA,qBAAqB;EACtB;CACD;AACD;;AAGA,IAAM,8BAAN,cAA0CC,eAAO,QAAQ,SAAS;CAIjE,YACC,MACA,MAMA,MACC;EACD,MACC,IAAI,oCAAoC,GACxC,MACA;GACC,GAAG;GACH,OAAOA,eAAO,OAAO,MAA8B;GACnD,SAAS;EACV,GACA,IACD;CACD;AACD;;;;;;;;;;;;;;;;;;;;;;;AAiDA,IAAa,sBAAb,cAAyCA,eAAO,kBAAkB;CAOjE,YACC,MACA,MACA,MACC;EACD,MAAM,EAAE,UAAU,SAAS,aAAa,GAAG,eAAe;EAE1D,MAAM,mCAAmC,MAAM,CAAC,GAAG,UAAU;EAE7D,MAAM,WAAW,IAAI,4BACpB,GAAG,KAAK,YACR;GACC,OAAO,SAAS;GAChB,WAAW,QAAQ;GACnB,eAAe,YAAY;GAC3B,MAAM,KAAK;EACZ,GACA,EAAE,QAAQ,KAAK,CAChB;EAEA,KAAK,QAAQ,SAAS;EAEtB,KAAK,gBAAgB,EAAE,OAAO,KAAK,MAAM,CAAC;CAC3C;AACD"}
1
+ {"version":3,"file":"project-token.cjs","names":["RailwayClient","pulumi"],"sources":["../../src/railway/project-token.ts"],"sourcesContent":["import * as pulumi from \"@pulumi/pulumi\";\nimport { RailwayClient } from \"./client\";\nimport type { RailwayEnvironment } from \"./environment\";\nimport type { RailwayProject } from \"./project\";\nimport type { RailwayProvider } from \"./provider\";\n\n/** Resolved inputs for the Railway project token dynamic provider. */\ninterface RailwayProjectTokenInputs {\n\t/** Railway API bearer token (account-scoped, used for API calls). */\n\ttoken: string;\n\n\t/** Railway project UUID. */\n\tprojectId: string;\n\n\t/** Railway environment UUID this deploy token is scoped to. */\n\tenvironmentId: string;\n\n\t/** Distinct token name, e.g. `\"pulumi-staging\"`. Must be unique per environment. */\n\tname: string;\n}\n\n/** Persisted state for the Railway project token. */\ninterface RailwayProjectTokenOutputs extends RailwayProjectTokenInputs {\n\t/** Minted deploy token secret value. */\n\tvalue: string;\n\n\t/** Railway-assigned token UUID (used for clean teardown on delete). */\n\ttokenId: string;\n}\n\nconst PROJECT_TOKENS_QUERY = `\n query($projectId: String!) {\n projectTokens(projectId: $projectId) {\n edges { node { id name } }\n }\n }\n`;\n\nconst PROJECT_TOKEN_CREATE = `\n mutation($input: ProjectTokenCreateInput!) {\n projectTokenCreate(input: $input)\n }\n`;\n\nconst PROJECT_TOKEN_DELETE = `\n mutation($id: String!) { projectTokenDelete(id: $id) }\n`;\n\n/**\n * Dynamic provider that mints a Railway environment-scoped deploy token.\n *\n * On create, it deletes any existing tokens with the same name (stale tokens\n * from previous runs), mints a fresh one scoped to the target environment, then\n * re-lists tokens to capture the Railway-assigned ID for later teardown.\n *\n * @internal Exported only for unit testing; not part of the public API surface.\n */\nexport class RailwayProjectTokenResourceProvider\n\timplements pulumi.dynamic.ResourceProvider\n{\n\t/**\n\t * Deletes stale same-named tokens, mints a new environment-scoped token,\n\t * and captures its ID via a follow-up list query.\n\t *\n\t * @param inputs Resolved provider inputs.\n\t * @returns Pulumi dynamic create result with `value` (secret) and `tokenId`.\n\t */\n\tasync create(\n\t\tinputs: RailwayProjectTokenInputs,\n\t): Promise<pulumi.dynamic.CreateResult> {\n\t\tconst client = new RailwayClient(inputs.token);\n\n\t\tconst tokensResult = await client.query<{\n\t\t\tprojectTokens: {\n\t\t\t\tedges: Array<{ node: { id: string; name: string } }>;\n\t\t\t};\n\t\t}>(PROJECT_TOKENS_QUERY, { projectId: inputs.projectId });\n\n\t\tconst stale = tokensResult.projectTokens.edges.filter(\n\t\t\t(edge) => edge.node.name === inputs.name,\n\t\t);\n\n\t\tfor (const entry of stale) {\n\t\t\tawait client.query(PROJECT_TOKEN_DELETE, { id: entry.node.id });\n\t\t}\n\n\t\tconst createResult = await client.query<{ projectTokenCreate: string }>(\n\t\t\tPROJECT_TOKEN_CREATE,\n\t\t\t{\n\t\t\t\tinput: {\n\t\t\t\t\tprojectId: inputs.projectId,\n\t\t\t\t\tenvironmentId: inputs.environmentId,\n\t\t\t\t\tname: inputs.name,\n\t\t\t\t},\n\t\t\t},\n\t\t);\n\n\t\tconst value = createResult.projectTokenCreate;\n\n\t\t// Re-list tokens to capture the Railway-assigned ID: the create mutation\n\t\t// returns only the token value, not its UUID.\n\t\tconst refreshResult = await client.query<{\n\t\t\tprojectTokens: {\n\t\t\t\tedges: Array<{ node: { id: string; name: string } }>;\n\t\t\t};\n\t\t}>(PROJECT_TOKENS_QUERY, { projectId: inputs.projectId });\n\n\t\t// After the delete sweep above, exactly one token with this name exists (the one we just\n\t\t// created). Resolve its id from the re-list so delete() can revoke it later.\n\t\tconst found = refreshResult.projectTokens.edges.find(\n\t\t\t(edge) => edge.node.name === inputs.name,\n\t\t);\n\n\t\tif (!found) {\n\t\t\tthrow new Error(\n\t\t\t\t`Could not resolve token id for newly created token \"${inputs.name}\" in project ${inputs.projectId}`,\n\t\t\t);\n\t\t}\n\n\t\tconst tokenId = found.node.id;\n\n\t\tconst outs: RailwayProjectTokenOutputs = {\n\t\t\t...inputs,\n\t\t\tvalue,\n\t\t\ttokenId,\n\t\t};\n\n\t\treturn { id: `${inputs.projectId}:${inputs.name}`, outs };\n\t}\n\n\t/**\n\t * Pass-through read — the token value is a write-once secret that Railway\n\t * never re-exposes via API, so the stored state is the only source of truth.\n\t *\n\t * @param id Resource ID.\n\t * @param props Currently stored outputs.\n\t */\n\tasync read(\n\t\tid: string,\n\t\tprops: RailwayProjectTokenOutputs,\n\t): Promise<pulumi.dynamic.ReadResult> {\n\t\treturn { id, props };\n\t}\n\n\t/**\n\t * Deletes the Railway token by its stored UUID for clean teardown.\n\t *\n\t * @param _id Resource ID (unused).\n\t * @param props Currently stored outputs containing the tokenId.\n\t */\n\tasync delete(_id: string, props: RailwayProjectTokenOutputs): Promise<void> {\n\t\tif (props.tokenId) {\n\t\t\tconst client = new RailwayClient(props.token);\n\n\t\t\tawait client.query(PROJECT_TOKEN_DELETE, { id: props.tokenId });\n\t\t}\n\t}\n\n\t/**\n\t * Triggers replacement when the project, environment, or token name changes.\n\t *\n\t * @param _id Resource ID (unused).\n\t * @param olds Previously stored outputs.\n\t * @param news Newly resolved inputs.\n\t */\n\tasync diff(\n\t\t_id: string,\n\t\tolds: RailwayProjectTokenOutputs,\n\t\tnews: RailwayProjectTokenInputs,\n\t): Promise<pulumi.dynamic.DiffResult> {\n\t\tconst replaces: string[] = [];\n\n\t\tif (olds.projectId !== news.projectId) {\n\t\t\treplaces.push(\"projectId\");\n\t\t}\n\n\t\tif (olds.environmentId !== news.environmentId) {\n\t\t\treplaces.push(\"environmentId\");\n\t\t}\n\n\t\tif (olds.name !== news.name) {\n\t\t\treplaces.push(\"name\");\n\t\t}\n\n\t\treturn {\n\t\t\tchanges: replaces.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 RailwayProjectTokenResource extends pulumi.dynamic.Resource {\n\tpublic declare readonly value: pulumi.Output<string>;\n\tpublic declare readonly tokenId: pulumi.Output<string>;\n\n\tconstructor(\n\t\tname: string,\n\t\targs: {\n\t\t\ttoken: pulumi.Input<string>;\n\t\t\tprojectId: pulumi.Input<string>;\n\t\t\tenvironmentId: pulumi.Input<string>;\n\t\t\tname: pulumi.Input<string>;\n\t\t},\n\t\topts?: pulumi.CustomResourceOptions,\n\t) {\n\t\t// `value` is an output-only secret. Declaring it via additionalSecretOutputs\n\t\t// (rather than a pulumi.secret(undefined) input placeholder) is the only reliable\n\t\t// way to mark a dynamic-provider output secret: the placeholder gives the property\n\t\t// both a secret-undefined input and a real output, so a downstream dynamic resource\n\t\t// consuming the token can race onto the undefined (\"malformed RPC secret: missing\n\t\t// value\" / \"Unexpected struct type\"). `tokenId` is a plain (non-secret) output. See\n\t\t// Pulumi #16041, #3012.\n\t\tsuper(\n\t\t\tnew RailwayProjectTokenResourceProvider(),\n\t\t\tname,\n\t\t\t{\n\t\t\t\t...args,\n\t\t\t\tvalue: undefined,\n\t\t\t\ttokenId: undefined,\n\t\t\t},\n\t\t\t{ ...opts, additionalSecretOutputs: [\"value\"] },\n\t\t);\n\t}\n}\n\n/** Options type for RailwayProjectToken — replaces Pulumi's native `provider` field. */\ntype RailwayProjectTokenOptions = Omit<\n\tpulumi.ComponentResourceOptions,\n\t\"provider\"\n> & {\n\t/** Railway authentication context. */\n\tprovider: RailwayProvider;\n\n\t/** Railway project this token belongs to. */\n\tproject: RailwayProject;\n\n\t/** Railway environment this deploy token is scoped to. */\n\tenvironment: RailwayEnvironment;\n};\n\n/** Args for RailwayProjectToken. */\nexport interface RailwayProjectTokenArgs {\n\t/**\n\t * Distinct token name, e.g. `\"pulumi-staging\"`.\n\t * Each environment should use a unique name so multiple stacks sharing\n\t * a project never collide on token ownership.\n\t */\n\tname: pulumi.Input<string>;\n}\n\n/**\n * Provisions an environment-scoped Railway deploy token.\n *\n * Each environment gets its own correctly-scoped token with a distinct name,\n * so multiple stacks sharing the same Railway project never collide.\n * The token value is exposed as a secret output for use in {@link RailwayDeploy}.\n *\n * @example\n * ```typescript\n * const project = new RailwayProject(\"my-project\", { name: \"my-app\" }, { provider });\n * const staging = new RailwayEnvironment(\"staging\", { name: \"staging\" }, { provider, project });\n *\n * const stagingToken = new RailwayProjectToken(\"staging-token\", {\n * name: \"pulumi-staging\",\n * }, { provider, project, environment: staging });\n *\n * new RailwayDeploy(\"api-deploy\", {\n * directory: monorepoRoot,\n * triggers: [sourceHash],\n * }, { provider, project, environment: staging, service, projectToken: stagingToken.token });\n * ```\n */\nexport class RailwayProjectToken extends pulumi.ComponentResource {\n\t/**\n\t * Environment-scoped Railway deploy token value (secret).\n\t * Pass this to `RailwayDeployOptions.projectToken`.\n\t */\n\tpublic readonly token: pulumi.Output<string>;\n\n\tconstructor(\n\t\tname: string,\n\t\targs: RailwayProjectTokenArgs,\n\t\topts: RailwayProjectTokenOptions,\n\t) {\n\t\tconst { provider, project, environment, ...pulumiOpts } = opts;\n\n\t\tsuper(\"infracraft:railway:ProjectToken\", name, {}, pulumiOpts);\n\n\t\tconst resource = new RailwayProjectTokenResource(\n\t\t\t`${name}-resource`,\n\t\t\t{\n\t\t\t\ttoken: provider.token,\n\t\t\t\tprojectId: project.id,\n\t\t\t\tenvironmentId: environment.id,\n\t\t\t\tname: args.name,\n\t\t\t},\n\t\t\t{ parent: this },\n\t\t);\n\n\t\tthis.token = resource.value;\n\n\t\tthis.registerOutputs({ token: this.token });\n\t}\n}\n"],"mappings":";;;;;;;AA8BA,MAAM,uBAAuB;;;;;;;AAQ7B,MAAM,uBAAuB;;;;;AAM7B,MAAM,uBAAuB;;;;;;;;;;;;AAa7B,IAAa,sCAAb,MAEA;;;;;;;;CAQC,MAAM,OACL,QACuC;EACvC,MAAM,SAAS,IAAIA,qCAAc,OAAO,KAAK;EAQ7C,MAAM,SAAQ,MANa,OAAO,MAI/B,sBAAsB,EAAE,WAAW,OAAO,UAAU,CAAC,GAE7B,cAAc,MAAM,QAC7C,SAAS,KAAK,KAAK,SAAS,OAAO,IACrC;EAEA,KAAK,MAAM,SAAS,OACnB,MAAM,OAAO,MAAM,sBAAsB,EAAE,IAAI,MAAM,KAAK,GAAG,CAAC;EAc/D,MAAM,SAAQ,MAXa,OAAO,MACjC,sBACA,EACC,OAAO;GACN,WAAW,OAAO;GAClB,eAAe,OAAO;GACtB,MAAM,OAAO;EACd,EACD,CACD,GAE2B;EAY3B,MAAM,SAAQ,MARc,OAAO,MAIhC,sBAAsB,EAAE,WAAW,OAAO,UAAU,CAAC,GAI5B,cAAc,MAAM,MAC9C,SAAS,KAAK,KAAK,SAAS,OAAO,IACrC;EAEA,IAAI,CAAC,OACJ,MAAM,IAAI,MACT,uDAAuD,OAAO,KAAK,eAAe,OAAO,WAC1F;EAGD,MAAM,UAAU,MAAM,KAAK;EAE3B,MAAM,OAAmC;GACxC,GAAG;GACH;GACA;EACD;EAEA,OAAO;GAAE,IAAI,GAAG,OAAO,UAAU,GAAG,OAAO;GAAQ;EAAK;CACzD;;;;;;;;CASA,MAAM,KACL,IACA,OACqC;EACrC,OAAO;GAAE;GAAI;EAAM;CACpB;;;;;;;CAQA,MAAM,OAAO,KAAa,OAAkD;EAC3E,IAAI,MAAM,SAGT,MAAM,IAFaA,qCAAc,MAAM,KAE5B,EAAE,MAAM,sBAAsB,EAAE,IAAI,MAAM,QAAQ,CAAC;CAEhE;;;;;;;;CASA,MAAM,KACL,KACA,MACA,MACqC;EACrC,MAAM,WAAqB,CAAC;EAE5B,IAAI,KAAK,cAAc,KAAK,WAC3B,SAAS,KAAK,WAAW;EAG1B,IAAI,KAAK,kBAAkB,KAAK,eAC/B,SAAS,KAAK,eAAe;EAG9B,IAAI,KAAK,SAAS,KAAK,MACtB,SAAS,KAAK,MAAM;EAGrB,OAAO;GACN,SAAS,SAAS,SAAS;GAC3B;GACA,qBAAqB;EACtB;CACD;AACD;;AAGA,IAAM,8BAAN,cAA0CC,eAAO,QAAQ,SAAS;CAIjE,YACC,MACA,MAMA,MACC;EAQD,MACC,IAAI,oCAAoC,GACxC,MACA;GACC,GAAG;GACH,OAAO;GACP,SAAS;EACV,GACA;GAAE,GAAG;GAAM,yBAAyB,CAAC,OAAO;EAAE,CAC/C;CACD;AACD;;;;;;;;;;;;;;;;;;;;;;;AAiDA,IAAa,sBAAb,cAAyCA,eAAO,kBAAkB;CAOjE,YACC,MACA,MACA,MACC;EACD,MAAM,EAAE,UAAU,SAAS,aAAa,GAAG,eAAe;EAE1D,MAAM,mCAAmC,MAAM,CAAC,GAAG,UAAU;EAE7D,MAAM,WAAW,IAAI,4BACpB,GAAG,KAAK,YACR;GACC,OAAO,SAAS;GAChB,WAAW,QAAQ;GACnB,eAAe,YAAY;GAC3B,MAAM,KAAK;EACZ,GACA,EAAE,QAAQ,KAAK,CAChB;EAEA,KAAK,QAAQ,SAAS;EAEtB,KAAK,gBAAgB,EAAE,OAAO,KAAK,MAAM,CAAC;CAC3C;AACD"}
@@ -1 +1 @@
1
- {"version":3,"file":"project-token.d.cts","names":[],"sources":["../../src/railway/project-token.ts"],"mappings":";;;;;;;;UAOU,yBAAA;;EAET,KAAA;EAFkC;EAKlC,SAAA;EALkC;EAQlC,aAAA;EAHA;EAMA,IAAA;AAAA;;UAIS,0BAAA,SAAmC,yBAAyB;EAA5D;EAET,KAAA;;EAGA,OAAA;AAAA;;;;AAAO;AA8BR;;;;;cAAa,mCAAA,YACD,MAAA,CAAO,OAAA,CAAQ,gBAAA;EAiFlB;;;;;;;EAxEF,MAAA,CACL,MAAA,EAAQ,yBAAA,GACN,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,YAAA;EAoGvB;;;;;;;EAhCG,IAAA,CACL,EAAA,UACA,KAAA,EAAO,0BAAA,GACL,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,UAAA;EAxEjB;;;;;;EAkFH,MAAA,CAAO,GAAA,UAAa,KAAA,EAAO,0BAAA,GAA6B,OAAA;EAZ7D;;;;;;;EA2BK,IAAA,CACL,GAAA,UACA,IAAA,EAAM,0BAAA,EACN,IAAA,EAAM,yBAAA,GACJ,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,UAAA;AAAA;;KAoDtB,0BAAA,GAA6B,IAAA,CACjC,MAAA,CAAO,wBAAA;EAxEuD,sCA4E9D,QAAA,EAAU,eAAA,EA5DT;EA+DD,OAAA,EAAS,cAAA,EA9DR;EAiED,WAAA,EAAa,kBAAA;AAAA;;UAIG,uBAAA;EAnEE;;;AAAkB;AAqBpC;EAoDA,IAAA,EAAM,MAAA,CAAO,KAAK;AAAA;;;;;;;;;;;;;;;;;;AAVa;AAIhC;;;;cA+Ba,mBAAA,SAA4B,MAAA,CAAO,iBAAA;EAzBzC;;;AAAY;EAAZ,SA8BU,KAAA,EAAO,MAAA,CAAO,MAAA;cAG7B,IAAA,UACA,IAAA,EAAM,uBAAA,EACN,IAAA,EAAM,0BAAA;AAAA"}
1
+ {"version":3,"file":"project-token.d.cts","names":[],"sources":["../../src/railway/project-token.ts"],"mappings":";;;;;;;;UAOU,yBAAA;;EAET,KAAA;EAFkC;EAKlC,SAAA;EALkC;EAQlC,aAAA;EAHA;EAMA,IAAA;AAAA;;UAIS,0BAAA,SAAmC,yBAAyB;EAA5D;EAET,KAAA;;EAGA,OAAA;AAAA;;;;AAAO;AA8BR;;;;;cAAa,mCAAA,YACD,MAAA,CAAO,OAAA,CAAQ,gBAAA;EAiFlB;;;;;;;EAxEF,MAAA,CACL,MAAA,EAAQ,yBAAA,GACN,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,YAAA;EAoGvB;;;;;;;EAhCG,IAAA,CACL,EAAA,UACA,KAAA,EAAO,0BAAA,GACL,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,UAAA;EAxEjB;;;;;;EAkFH,MAAA,CAAO,GAAA,UAAa,KAAA,EAAO,0BAAA,GAA6B,OAAA;EAZ7D;;;;;;;EA2BK,IAAA,CACL,GAAA,UACA,IAAA,EAAM,0BAAA,EACN,IAAA,EAAM,yBAAA,GACJ,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,UAAA;AAAA;;KA2DtB,0BAAA,GAA6B,IAAA,CACjC,MAAA,CAAO,wBAAA;EA/EuD,sCAmF9D,QAAA,EAAU,eAAA,EAnET;EAsED,OAAA,EAAS,cAAA,EArER;EAwED,WAAA,EAAa,kBAAA;AAAA;;UAIG,uBAAA;EA1EE;;;AAAkB;AAqBpC;EA2DA,IAAA,EAAM,MAAA,CAAO,KAAK;AAAA;;;;;;;;;;;;;;;;;;AAVa;AAIhC;;;;cA+Ba,mBAAA,SAA4B,MAAA,CAAO,iBAAA;EAzBzC;;;AAAY;EAAZ,SA8BU,KAAA,EAAO,MAAA,CAAO,MAAA;cAG7B,IAAA,UACA,IAAA,EAAM,uBAAA,EACN,IAAA,EAAM,0BAAA;AAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"project-token.d.mts","names":[],"sources":["../../src/railway/project-token.ts"],"mappings":";;;;;;;;UAOU,yBAAA;;EAET,KAAA;EAFkC;EAKlC,SAAA;EALkC;EAQlC,aAAA;EAHA;EAMA,IAAA;AAAA;;UAIS,0BAAA,SAAmC,yBAAyB;EAA5D;EAET,KAAA;;EAGA,OAAA;AAAA;;;;AAAO;AA8BR;;;;;cAAa,mCAAA,YACD,MAAA,CAAO,OAAA,CAAQ,gBAAA;EAiFlB;;;;;;;EAxEF,MAAA,CACL,MAAA,EAAQ,yBAAA,GACN,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,YAAA;EAoGvB;;;;;;;EAhCG,IAAA,CACL,EAAA,UACA,KAAA,EAAO,0BAAA,GACL,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,UAAA;EAxEjB;;;;;;EAkFH,MAAA,CAAO,GAAA,UAAa,KAAA,EAAO,0BAAA,GAA6B,OAAA;EAZ7D;;;;;;;EA2BK,IAAA,CACL,GAAA,UACA,IAAA,EAAM,0BAAA,EACN,IAAA,EAAM,yBAAA,GACJ,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,UAAA;AAAA;;KAoDtB,0BAAA,GAA6B,IAAA,CACjC,MAAA,CAAO,wBAAA;EAxEuD,sCA4E9D,QAAA,EAAU,eAAA,EA5DT;EA+DD,OAAA,EAAS,cAAA,EA9DR;EAiED,WAAA,EAAa,kBAAA;AAAA;;UAIG,uBAAA;EAnEE;;;AAAkB;AAqBpC;EAoDA,IAAA,EAAM,MAAA,CAAO,KAAK;AAAA;;;;;;;;;;;;;;;;;;AAVa;AAIhC;;;;cA+Ba,mBAAA,SAA4B,MAAA,CAAO,iBAAA;EAzBzC;;;AAAY;EAAZ,SA8BU,KAAA,EAAO,MAAA,CAAO,MAAA;cAG7B,IAAA,UACA,IAAA,EAAM,uBAAA,EACN,IAAA,EAAM,0BAAA;AAAA"}
1
+ {"version":3,"file":"project-token.d.mts","names":[],"sources":["../../src/railway/project-token.ts"],"mappings":";;;;;;;;UAOU,yBAAA;;EAET,KAAA;EAFkC;EAKlC,SAAA;EALkC;EAQlC,aAAA;EAHA;EAMA,IAAA;AAAA;;UAIS,0BAAA,SAAmC,yBAAyB;EAA5D;EAET,KAAA;;EAGA,OAAA;AAAA;;;;AAAO;AA8BR;;;;;cAAa,mCAAA,YACD,MAAA,CAAO,OAAA,CAAQ,gBAAA;EAiFlB;;;;;;;EAxEF,MAAA,CACL,MAAA,EAAQ,yBAAA,GACN,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,YAAA;EAoGvB;;;;;;;EAhCG,IAAA,CACL,EAAA,UACA,KAAA,EAAO,0BAAA,GACL,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,UAAA;EAxEjB;;;;;;EAkFH,MAAA,CAAO,GAAA,UAAa,KAAA,EAAO,0BAAA,GAA6B,OAAA;EAZ7D;;;;;;;EA2BK,IAAA,CACL,GAAA,UACA,IAAA,EAAM,0BAAA,EACN,IAAA,EAAM,yBAAA,GACJ,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,UAAA;AAAA;;KA2DtB,0BAAA,GAA6B,IAAA,CACjC,MAAA,CAAO,wBAAA;EA/EuD,sCAmF9D,QAAA,EAAU,eAAA,EAnET;EAsED,OAAA,EAAS,cAAA,EArER;EAwED,WAAA,EAAa,kBAAA;AAAA;;UAIG,uBAAA;EA1EE;;;AAAkB;AAqBpC;EA2DA,IAAA,EAAM,MAAA,CAAO,KAAK;AAAA;;;;;;;;;;;;;;;;;;AAVa;AAIhC;;;;cA+Ba,mBAAA,SAA4B,MAAA,CAAO,iBAAA;EAzBzC;;;AAAY;EAAZ,SA8BU,KAAA,EAAO,MAAA,CAAO,MAAA;cAG7B,IAAA,UACA,IAAA,EAAM,uBAAA,EACN,IAAA,EAAM,0BAAA;AAAA"}
@@ -103,9 +103,12 @@ var RailwayProjectTokenResource = class extends pulumi.dynamic.Resource {
103
103
  constructor(name, args, opts) {
104
104
  super(new RailwayProjectTokenResourceProvider(), name, {
105
105
  ...args,
106
- value: pulumi.secret(void 0),
106
+ value: void 0,
107
107
  tokenId: void 0
108
- }, opts);
108
+ }, {
109
+ ...opts,
110
+ additionalSecretOutputs: ["value"]
111
+ });
109
112
  }
110
113
  };
111
114
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"project-token.mjs","names":[],"sources":["../../src/railway/project-token.ts"],"sourcesContent":["import * as pulumi from \"@pulumi/pulumi\";\nimport { RailwayClient } from \"./client\";\nimport type { RailwayEnvironment } from \"./environment\";\nimport type { RailwayProject } from \"./project\";\nimport type { RailwayProvider } from \"./provider\";\n\n/** Resolved inputs for the Railway project token dynamic provider. */\ninterface RailwayProjectTokenInputs {\n\t/** Railway API bearer token (account-scoped, used for API calls). */\n\ttoken: string;\n\n\t/** Railway project UUID. */\n\tprojectId: string;\n\n\t/** Railway environment UUID this deploy token is scoped to. */\n\tenvironmentId: string;\n\n\t/** Distinct token name, e.g. `\"pulumi-staging\"`. Must be unique per environment. */\n\tname: string;\n}\n\n/** Persisted state for the Railway project token. */\ninterface RailwayProjectTokenOutputs extends RailwayProjectTokenInputs {\n\t/** Minted deploy token secret value. */\n\tvalue: string;\n\n\t/** Railway-assigned token UUID (used for clean teardown on delete). */\n\ttokenId: string;\n}\n\nconst PROJECT_TOKENS_QUERY = `\n query($projectId: String!) {\n projectTokens(projectId: $projectId) {\n edges { node { id name } }\n }\n }\n`;\n\nconst PROJECT_TOKEN_CREATE = `\n mutation($input: ProjectTokenCreateInput!) {\n projectTokenCreate(input: $input)\n }\n`;\n\nconst PROJECT_TOKEN_DELETE = `\n mutation($id: String!) { projectTokenDelete(id: $id) }\n`;\n\n/**\n * Dynamic provider that mints a Railway environment-scoped deploy token.\n *\n * On create, it deletes any existing tokens with the same name (stale tokens\n * from previous runs), mints a fresh one scoped to the target environment, then\n * re-lists tokens to capture the Railway-assigned ID for later teardown.\n *\n * @internal Exported only for unit testing; not part of the public API surface.\n */\nexport class RailwayProjectTokenResourceProvider\n\timplements pulumi.dynamic.ResourceProvider\n{\n\t/**\n\t * Deletes stale same-named tokens, mints a new environment-scoped token,\n\t * and captures its ID via a follow-up list query.\n\t *\n\t * @param inputs Resolved provider inputs.\n\t * @returns Pulumi dynamic create result with `value` (secret) and `tokenId`.\n\t */\n\tasync create(\n\t\tinputs: RailwayProjectTokenInputs,\n\t): Promise<pulumi.dynamic.CreateResult> {\n\t\tconst client = new RailwayClient(inputs.token);\n\n\t\tconst tokensResult = await client.query<{\n\t\t\tprojectTokens: {\n\t\t\t\tedges: Array<{ node: { id: string; name: string } }>;\n\t\t\t};\n\t\t}>(PROJECT_TOKENS_QUERY, { projectId: inputs.projectId });\n\n\t\tconst stale = tokensResult.projectTokens.edges.filter(\n\t\t\t(edge) => edge.node.name === inputs.name,\n\t\t);\n\n\t\tfor (const entry of stale) {\n\t\t\tawait client.query(PROJECT_TOKEN_DELETE, { id: entry.node.id });\n\t\t}\n\n\t\tconst createResult = await client.query<{ projectTokenCreate: string }>(\n\t\t\tPROJECT_TOKEN_CREATE,\n\t\t\t{\n\t\t\t\tinput: {\n\t\t\t\t\tprojectId: inputs.projectId,\n\t\t\t\t\tenvironmentId: inputs.environmentId,\n\t\t\t\t\tname: inputs.name,\n\t\t\t\t},\n\t\t\t},\n\t\t);\n\n\t\tconst value = createResult.projectTokenCreate;\n\n\t\t// Re-list tokens to capture the Railway-assigned ID: the create mutation\n\t\t// returns only the token value, not its UUID.\n\t\tconst refreshResult = await client.query<{\n\t\t\tprojectTokens: {\n\t\t\t\tedges: Array<{ node: { id: string; name: string } }>;\n\t\t\t};\n\t\t}>(PROJECT_TOKENS_QUERY, { projectId: inputs.projectId });\n\n\t\t// After the delete sweep above, exactly one token with this name exists (the one we just\n\t\t// created). Resolve its id from the re-list so delete() can revoke it later.\n\t\tconst found = refreshResult.projectTokens.edges.find(\n\t\t\t(edge) => edge.node.name === inputs.name,\n\t\t);\n\n\t\tif (!found) {\n\t\t\tthrow new Error(\n\t\t\t\t`Could not resolve token id for newly created token \"${inputs.name}\" in project ${inputs.projectId}`,\n\t\t\t);\n\t\t}\n\n\t\tconst tokenId = found.node.id;\n\n\t\tconst outs: RailwayProjectTokenOutputs = {\n\t\t\t...inputs,\n\t\t\tvalue,\n\t\t\ttokenId,\n\t\t};\n\n\t\treturn { id: `${inputs.projectId}:${inputs.name}`, outs };\n\t}\n\n\t/**\n\t * Pass-through read — the token value is a write-once secret that Railway\n\t * never re-exposes via API, so the stored state is the only source of truth.\n\t *\n\t * @param id Resource ID.\n\t * @param props Currently stored outputs.\n\t */\n\tasync read(\n\t\tid: string,\n\t\tprops: RailwayProjectTokenOutputs,\n\t): Promise<pulumi.dynamic.ReadResult> {\n\t\treturn { id, props };\n\t}\n\n\t/**\n\t * Deletes the Railway token by its stored UUID for clean teardown.\n\t *\n\t * @param _id Resource ID (unused).\n\t * @param props Currently stored outputs containing the tokenId.\n\t */\n\tasync delete(_id: string, props: RailwayProjectTokenOutputs): Promise<void> {\n\t\tif (props.tokenId) {\n\t\t\tconst client = new RailwayClient(props.token);\n\n\t\t\tawait client.query(PROJECT_TOKEN_DELETE, { id: props.tokenId });\n\t\t}\n\t}\n\n\t/**\n\t * Triggers replacement when the project, environment, or token name changes.\n\t *\n\t * @param _id Resource ID (unused).\n\t * @param olds Previously stored outputs.\n\t * @param news Newly resolved inputs.\n\t */\n\tasync diff(\n\t\t_id: string,\n\t\tolds: RailwayProjectTokenOutputs,\n\t\tnews: RailwayProjectTokenInputs,\n\t): Promise<pulumi.dynamic.DiffResult> {\n\t\tconst replaces: string[] = [];\n\n\t\tif (olds.projectId !== news.projectId) {\n\t\t\treplaces.push(\"projectId\");\n\t\t}\n\n\t\tif (olds.environmentId !== news.environmentId) {\n\t\t\treplaces.push(\"environmentId\");\n\t\t}\n\n\t\tif (olds.name !== news.name) {\n\t\t\treplaces.push(\"name\");\n\t\t}\n\n\t\treturn {\n\t\t\tchanges: replaces.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 RailwayProjectTokenResource extends pulumi.dynamic.Resource {\n\tpublic declare readonly value: pulumi.Output<string>;\n\tpublic declare readonly tokenId: pulumi.Output<string>;\n\n\tconstructor(\n\t\tname: string,\n\t\targs: {\n\t\t\ttoken: pulumi.Input<string>;\n\t\t\tprojectId: pulumi.Input<string>;\n\t\t\tenvironmentId: pulumi.Input<string>;\n\t\t\tname: pulumi.Input<string>;\n\t\t},\n\t\topts?: pulumi.CustomResourceOptions,\n\t) {\n\t\tsuper(\n\t\t\tnew RailwayProjectTokenResourceProvider(),\n\t\t\tname,\n\t\t\t{\n\t\t\t\t...args,\n\t\t\t\tvalue: pulumi.secret(undefined as unknown as string),\n\t\t\t\ttokenId: undefined,\n\t\t\t},\n\t\t\topts,\n\t\t);\n\t}\n}\n\n/** Options type for RailwayProjectToken — replaces Pulumi's native `provider` field. */\ntype RailwayProjectTokenOptions = Omit<\n\tpulumi.ComponentResourceOptions,\n\t\"provider\"\n> & {\n\t/** Railway authentication context. */\n\tprovider: RailwayProvider;\n\n\t/** Railway project this token belongs to. */\n\tproject: RailwayProject;\n\n\t/** Railway environment this deploy token is scoped to. */\n\tenvironment: RailwayEnvironment;\n};\n\n/** Args for RailwayProjectToken. */\nexport interface RailwayProjectTokenArgs {\n\t/**\n\t * Distinct token name, e.g. `\"pulumi-staging\"`.\n\t * Each environment should use a unique name so multiple stacks sharing\n\t * a project never collide on token ownership.\n\t */\n\tname: pulumi.Input<string>;\n}\n\n/**\n * Provisions an environment-scoped Railway deploy token.\n *\n * Each environment gets its own correctly-scoped token with a distinct name,\n * so multiple stacks sharing the same Railway project never collide.\n * The token value is exposed as a secret output for use in {@link RailwayDeploy}.\n *\n * @example\n * ```typescript\n * const project = new RailwayProject(\"my-project\", { name: \"my-app\" }, { provider });\n * const staging = new RailwayEnvironment(\"staging\", { name: \"staging\" }, { provider, project });\n *\n * const stagingToken = new RailwayProjectToken(\"staging-token\", {\n * name: \"pulumi-staging\",\n * }, { provider, project, environment: staging });\n *\n * new RailwayDeploy(\"api-deploy\", {\n * directory: monorepoRoot,\n * triggers: [sourceHash],\n * }, { provider, project, environment: staging, service, projectToken: stagingToken.token });\n * ```\n */\nexport class RailwayProjectToken extends pulumi.ComponentResource {\n\t/**\n\t * Environment-scoped Railway deploy token value (secret).\n\t * Pass this to `RailwayDeployOptions.projectToken`.\n\t */\n\tpublic readonly token: pulumi.Output<string>;\n\n\tconstructor(\n\t\tname: string,\n\t\targs: RailwayProjectTokenArgs,\n\t\topts: RailwayProjectTokenOptions,\n\t) {\n\t\tconst { provider, project, environment, ...pulumiOpts } = opts;\n\n\t\tsuper(\"infracraft:railway:ProjectToken\", name, {}, pulumiOpts);\n\n\t\tconst resource = new RailwayProjectTokenResource(\n\t\t\t`${name}-resource`,\n\t\t\t{\n\t\t\t\ttoken: provider.token,\n\t\t\t\tprojectId: project.id,\n\t\t\t\tenvironmentId: environment.id,\n\t\t\t\tname: args.name,\n\t\t\t},\n\t\t\t{ parent: this },\n\t\t);\n\n\t\tthis.token = resource.value;\n\n\t\tthis.registerOutputs({ token: this.token });\n\t}\n}\n"],"mappings":";;;;;AA8BA,MAAM,uBAAuB;;;;;;;AAQ7B,MAAM,uBAAuB;;;;;AAM7B,MAAM,uBAAuB;;;;;;;;;;;;AAa7B,IAAa,sCAAb,MAEA;;;;;;;;CAQC,MAAM,OACL,QACuC;EACvC,MAAM,SAAS,IAAI,cAAc,OAAO,KAAK;EAQ7C,MAAM,SAAQ,MANa,OAAO,MAI/B,sBAAsB,EAAE,WAAW,OAAO,UAAU,CAAC,GAE7B,cAAc,MAAM,QAC7C,SAAS,KAAK,KAAK,SAAS,OAAO,IACrC;EAEA,KAAK,MAAM,SAAS,OACnB,MAAM,OAAO,MAAM,sBAAsB,EAAE,IAAI,MAAM,KAAK,GAAG,CAAC;EAc/D,MAAM,SAAQ,MAXa,OAAO,MACjC,sBACA,EACC,OAAO;GACN,WAAW,OAAO;GAClB,eAAe,OAAO;GACtB,MAAM,OAAO;EACd,EACD,CACD,GAE2B;EAY3B,MAAM,SAAQ,MARc,OAAO,MAIhC,sBAAsB,EAAE,WAAW,OAAO,UAAU,CAAC,GAI5B,cAAc,MAAM,MAC9C,SAAS,KAAK,KAAK,SAAS,OAAO,IACrC;EAEA,IAAI,CAAC,OACJ,MAAM,IAAI,MACT,uDAAuD,OAAO,KAAK,eAAe,OAAO,WAC1F;EAGD,MAAM,UAAU,MAAM,KAAK;EAE3B,MAAM,OAAmC;GACxC,GAAG;GACH;GACA;EACD;EAEA,OAAO;GAAE,IAAI,GAAG,OAAO,UAAU,GAAG,OAAO;GAAQ;EAAK;CACzD;;;;;;;;CASA,MAAM,KACL,IACA,OACqC;EACrC,OAAO;GAAE;GAAI;EAAM;CACpB;;;;;;;CAQA,MAAM,OAAO,KAAa,OAAkD;EAC3E,IAAI,MAAM,SAGT,MAAM,IAFa,cAAc,MAAM,KAE5B,EAAE,MAAM,sBAAsB,EAAE,IAAI,MAAM,QAAQ,CAAC;CAEhE;;;;;;;;CASA,MAAM,KACL,KACA,MACA,MACqC;EACrC,MAAM,WAAqB,CAAC;EAE5B,IAAI,KAAK,cAAc,KAAK,WAC3B,SAAS,KAAK,WAAW;EAG1B,IAAI,KAAK,kBAAkB,KAAK,eAC/B,SAAS,KAAK,eAAe;EAG9B,IAAI,KAAK,SAAS,KAAK,MACtB,SAAS,KAAK,MAAM;EAGrB,OAAO;GACN,SAAS,SAAS,SAAS;GAC3B;GACA,qBAAqB;EACtB;CACD;AACD;;AAGA,IAAM,8BAAN,cAA0C,OAAO,QAAQ,SAAS;CAIjE,YACC,MACA,MAMA,MACC;EACD,MACC,IAAI,oCAAoC,GACxC,MACA;GACC,GAAG;GACH,OAAO,OAAO,OAAO,MAA8B;GACnD,SAAS;EACV,GACA,IACD;CACD;AACD;;;;;;;;;;;;;;;;;;;;;;;AAiDA,IAAa,sBAAb,cAAyC,OAAO,kBAAkB;CAOjE,YACC,MACA,MACA,MACC;EACD,MAAM,EAAE,UAAU,SAAS,aAAa,GAAG,eAAe;EAE1D,MAAM,mCAAmC,MAAM,CAAC,GAAG,UAAU;EAE7D,MAAM,WAAW,IAAI,4BACpB,GAAG,KAAK,YACR;GACC,OAAO,SAAS;GAChB,WAAW,QAAQ;GACnB,eAAe,YAAY;GAC3B,MAAM,KAAK;EACZ,GACA,EAAE,QAAQ,KAAK,CAChB;EAEA,KAAK,QAAQ,SAAS;EAEtB,KAAK,gBAAgB,EAAE,OAAO,KAAK,MAAM,CAAC;CAC3C;AACD"}
1
+ {"version":3,"file":"project-token.mjs","names":[],"sources":["../../src/railway/project-token.ts"],"sourcesContent":["import * as pulumi from \"@pulumi/pulumi\";\nimport { RailwayClient } from \"./client\";\nimport type { RailwayEnvironment } from \"./environment\";\nimport type { RailwayProject } from \"./project\";\nimport type { RailwayProvider } from \"./provider\";\n\n/** Resolved inputs for the Railway project token dynamic provider. */\ninterface RailwayProjectTokenInputs {\n\t/** Railway API bearer token (account-scoped, used for API calls). */\n\ttoken: string;\n\n\t/** Railway project UUID. */\n\tprojectId: string;\n\n\t/** Railway environment UUID this deploy token is scoped to. */\n\tenvironmentId: string;\n\n\t/** Distinct token name, e.g. `\"pulumi-staging\"`. Must be unique per environment. */\n\tname: string;\n}\n\n/** Persisted state for the Railway project token. */\ninterface RailwayProjectTokenOutputs extends RailwayProjectTokenInputs {\n\t/** Minted deploy token secret value. */\n\tvalue: string;\n\n\t/** Railway-assigned token UUID (used for clean teardown on delete). */\n\ttokenId: string;\n}\n\nconst PROJECT_TOKENS_QUERY = `\n query($projectId: String!) {\n projectTokens(projectId: $projectId) {\n edges { node { id name } }\n }\n }\n`;\n\nconst PROJECT_TOKEN_CREATE = `\n mutation($input: ProjectTokenCreateInput!) {\n projectTokenCreate(input: $input)\n }\n`;\n\nconst PROJECT_TOKEN_DELETE = `\n mutation($id: String!) { projectTokenDelete(id: $id) }\n`;\n\n/**\n * Dynamic provider that mints a Railway environment-scoped deploy token.\n *\n * On create, it deletes any existing tokens with the same name (stale tokens\n * from previous runs), mints a fresh one scoped to the target environment, then\n * re-lists tokens to capture the Railway-assigned ID for later teardown.\n *\n * @internal Exported only for unit testing; not part of the public API surface.\n */\nexport class RailwayProjectTokenResourceProvider\n\timplements pulumi.dynamic.ResourceProvider\n{\n\t/**\n\t * Deletes stale same-named tokens, mints a new environment-scoped token,\n\t * and captures its ID via a follow-up list query.\n\t *\n\t * @param inputs Resolved provider inputs.\n\t * @returns Pulumi dynamic create result with `value` (secret) and `tokenId`.\n\t */\n\tasync create(\n\t\tinputs: RailwayProjectTokenInputs,\n\t): Promise<pulumi.dynamic.CreateResult> {\n\t\tconst client = new RailwayClient(inputs.token);\n\n\t\tconst tokensResult = await client.query<{\n\t\t\tprojectTokens: {\n\t\t\t\tedges: Array<{ node: { id: string; name: string } }>;\n\t\t\t};\n\t\t}>(PROJECT_TOKENS_QUERY, { projectId: inputs.projectId });\n\n\t\tconst stale = tokensResult.projectTokens.edges.filter(\n\t\t\t(edge) => edge.node.name === inputs.name,\n\t\t);\n\n\t\tfor (const entry of stale) {\n\t\t\tawait client.query(PROJECT_TOKEN_DELETE, { id: entry.node.id });\n\t\t}\n\n\t\tconst createResult = await client.query<{ projectTokenCreate: string }>(\n\t\t\tPROJECT_TOKEN_CREATE,\n\t\t\t{\n\t\t\t\tinput: {\n\t\t\t\t\tprojectId: inputs.projectId,\n\t\t\t\t\tenvironmentId: inputs.environmentId,\n\t\t\t\t\tname: inputs.name,\n\t\t\t\t},\n\t\t\t},\n\t\t);\n\n\t\tconst value = createResult.projectTokenCreate;\n\n\t\t// Re-list tokens to capture the Railway-assigned ID: the create mutation\n\t\t// returns only the token value, not its UUID.\n\t\tconst refreshResult = await client.query<{\n\t\t\tprojectTokens: {\n\t\t\t\tedges: Array<{ node: { id: string; name: string } }>;\n\t\t\t};\n\t\t}>(PROJECT_TOKENS_QUERY, { projectId: inputs.projectId });\n\n\t\t// After the delete sweep above, exactly one token with this name exists (the one we just\n\t\t// created). Resolve its id from the re-list so delete() can revoke it later.\n\t\tconst found = refreshResult.projectTokens.edges.find(\n\t\t\t(edge) => edge.node.name === inputs.name,\n\t\t);\n\n\t\tif (!found) {\n\t\t\tthrow new Error(\n\t\t\t\t`Could not resolve token id for newly created token \"${inputs.name}\" in project ${inputs.projectId}`,\n\t\t\t);\n\t\t}\n\n\t\tconst tokenId = found.node.id;\n\n\t\tconst outs: RailwayProjectTokenOutputs = {\n\t\t\t...inputs,\n\t\t\tvalue,\n\t\t\ttokenId,\n\t\t};\n\n\t\treturn { id: `${inputs.projectId}:${inputs.name}`, outs };\n\t}\n\n\t/**\n\t * Pass-through read — the token value is a write-once secret that Railway\n\t * never re-exposes via API, so the stored state is the only source of truth.\n\t *\n\t * @param id Resource ID.\n\t * @param props Currently stored outputs.\n\t */\n\tasync read(\n\t\tid: string,\n\t\tprops: RailwayProjectTokenOutputs,\n\t): Promise<pulumi.dynamic.ReadResult> {\n\t\treturn { id, props };\n\t}\n\n\t/**\n\t * Deletes the Railway token by its stored UUID for clean teardown.\n\t *\n\t * @param _id Resource ID (unused).\n\t * @param props Currently stored outputs containing the tokenId.\n\t */\n\tasync delete(_id: string, props: RailwayProjectTokenOutputs): Promise<void> {\n\t\tif (props.tokenId) {\n\t\t\tconst client = new RailwayClient(props.token);\n\n\t\t\tawait client.query(PROJECT_TOKEN_DELETE, { id: props.tokenId });\n\t\t}\n\t}\n\n\t/**\n\t * Triggers replacement when the project, environment, or token name changes.\n\t *\n\t * @param _id Resource ID (unused).\n\t * @param olds Previously stored outputs.\n\t * @param news Newly resolved inputs.\n\t */\n\tasync diff(\n\t\t_id: string,\n\t\tolds: RailwayProjectTokenOutputs,\n\t\tnews: RailwayProjectTokenInputs,\n\t): Promise<pulumi.dynamic.DiffResult> {\n\t\tconst replaces: string[] = [];\n\n\t\tif (olds.projectId !== news.projectId) {\n\t\t\treplaces.push(\"projectId\");\n\t\t}\n\n\t\tif (olds.environmentId !== news.environmentId) {\n\t\t\treplaces.push(\"environmentId\");\n\t\t}\n\n\t\tif (olds.name !== news.name) {\n\t\t\treplaces.push(\"name\");\n\t\t}\n\n\t\treturn {\n\t\t\tchanges: replaces.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 RailwayProjectTokenResource extends pulumi.dynamic.Resource {\n\tpublic declare readonly value: pulumi.Output<string>;\n\tpublic declare readonly tokenId: pulumi.Output<string>;\n\n\tconstructor(\n\t\tname: string,\n\t\targs: {\n\t\t\ttoken: pulumi.Input<string>;\n\t\t\tprojectId: pulumi.Input<string>;\n\t\t\tenvironmentId: pulumi.Input<string>;\n\t\t\tname: pulumi.Input<string>;\n\t\t},\n\t\topts?: pulumi.CustomResourceOptions,\n\t) {\n\t\t// `value` is an output-only secret. Declaring it via additionalSecretOutputs\n\t\t// (rather than a pulumi.secret(undefined) input placeholder) is the only reliable\n\t\t// way to mark a dynamic-provider output secret: the placeholder gives the property\n\t\t// both a secret-undefined input and a real output, so a downstream dynamic resource\n\t\t// consuming the token can race onto the undefined (\"malformed RPC secret: missing\n\t\t// value\" / \"Unexpected struct type\"). `tokenId` is a plain (non-secret) output. See\n\t\t// Pulumi #16041, #3012.\n\t\tsuper(\n\t\t\tnew RailwayProjectTokenResourceProvider(),\n\t\t\tname,\n\t\t\t{\n\t\t\t\t...args,\n\t\t\t\tvalue: undefined,\n\t\t\t\ttokenId: undefined,\n\t\t\t},\n\t\t\t{ ...opts, additionalSecretOutputs: [\"value\"] },\n\t\t);\n\t}\n}\n\n/** Options type for RailwayProjectToken — replaces Pulumi's native `provider` field. */\ntype RailwayProjectTokenOptions = Omit<\n\tpulumi.ComponentResourceOptions,\n\t\"provider\"\n> & {\n\t/** Railway authentication context. */\n\tprovider: RailwayProvider;\n\n\t/** Railway project this token belongs to. */\n\tproject: RailwayProject;\n\n\t/** Railway environment this deploy token is scoped to. */\n\tenvironment: RailwayEnvironment;\n};\n\n/** Args for RailwayProjectToken. */\nexport interface RailwayProjectTokenArgs {\n\t/**\n\t * Distinct token name, e.g. `\"pulumi-staging\"`.\n\t * Each environment should use a unique name so multiple stacks sharing\n\t * a project never collide on token ownership.\n\t */\n\tname: pulumi.Input<string>;\n}\n\n/**\n * Provisions an environment-scoped Railway deploy token.\n *\n * Each environment gets its own correctly-scoped token with a distinct name,\n * so multiple stacks sharing the same Railway project never collide.\n * The token value is exposed as a secret output for use in {@link RailwayDeploy}.\n *\n * @example\n * ```typescript\n * const project = new RailwayProject(\"my-project\", { name: \"my-app\" }, { provider });\n * const staging = new RailwayEnvironment(\"staging\", { name: \"staging\" }, { provider, project });\n *\n * const stagingToken = new RailwayProjectToken(\"staging-token\", {\n * name: \"pulumi-staging\",\n * }, { provider, project, environment: staging });\n *\n * new RailwayDeploy(\"api-deploy\", {\n * directory: monorepoRoot,\n * triggers: [sourceHash],\n * }, { provider, project, environment: staging, service, projectToken: stagingToken.token });\n * ```\n */\nexport class RailwayProjectToken extends pulumi.ComponentResource {\n\t/**\n\t * Environment-scoped Railway deploy token value (secret).\n\t * Pass this to `RailwayDeployOptions.projectToken`.\n\t */\n\tpublic readonly token: pulumi.Output<string>;\n\n\tconstructor(\n\t\tname: string,\n\t\targs: RailwayProjectTokenArgs,\n\t\topts: RailwayProjectTokenOptions,\n\t) {\n\t\tconst { provider, project, environment, ...pulumiOpts } = opts;\n\n\t\tsuper(\"infracraft:railway:ProjectToken\", name, {}, pulumiOpts);\n\n\t\tconst resource = new RailwayProjectTokenResource(\n\t\t\t`${name}-resource`,\n\t\t\t{\n\t\t\t\ttoken: provider.token,\n\t\t\t\tprojectId: project.id,\n\t\t\t\tenvironmentId: environment.id,\n\t\t\t\tname: args.name,\n\t\t\t},\n\t\t\t{ parent: this },\n\t\t);\n\n\t\tthis.token = resource.value;\n\n\t\tthis.registerOutputs({ token: this.token });\n\t}\n}\n"],"mappings":";;;;;AA8BA,MAAM,uBAAuB;;;;;;;AAQ7B,MAAM,uBAAuB;;;;;AAM7B,MAAM,uBAAuB;;;;;;;;;;;;AAa7B,IAAa,sCAAb,MAEA;;;;;;;;CAQC,MAAM,OACL,QACuC;EACvC,MAAM,SAAS,IAAI,cAAc,OAAO,KAAK;EAQ7C,MAAM,SAAQ,MANa,OAAO,MAI/B,sBAAsB,EAAE,WAAW,OAAO,UAAU,CAAC,GAE7B,cAAc,MAAM,QAC7C,SAAS,KAAK,KAAK,SAAS,OAAO,IACrC;EAEA,KAAK,MAAM,SAAS,OACnB,MAAM,OAAO,MAAM,sBAAsB,EAAE,IAAI,MAAM,KAAK,GAAG,CAAC;EAc/D,MAAM,SAAQ,MAXa,OAAO,MACjC,sBACA,EACC,OAAO;GACN,WAAW,OAAO;GAClB,eAAe,OAAO;GACtB,MAAM,OAAO;EACd,EACD,CACD,GAE2B;EAY3B,MAAM,SAAQ,MARc,OAAO,MAIhC,sBAAsB,EAAE,WAAW,OAAO,UAAU,CAAC,GAI5B,cAAc,MAAM,MAC9C,SAAS,KAAK,KAAK,SAAS,OAAO,IACrC;EAEA,IAAI,CAAC,OACJ,MAAM,IAAI,MACT,uDAAuD,OAAO,KAAK,eAAe,OAAO,WAC1F;EAGD,MAAM,UAAU,MAAM,KAAK;EAE3B,MAAM,OAAmC;GACxC,GAAG;GACH;GACA;EACD;EAEA,OAAO;GAAE,IAAI,GAAG,OAAO,UAAU,GAAG,OAAO;GAAQ;EAAK;CACzD;;;;;;;;CASA,MAAM,KACL,IACA,OACqC;EACrC,OAAO;GAAE;GAAI;EAAM;CACpB;;;;;;;CAQA,MAAM,OAAO,KAAa,OAAkD;EAC3E,IAAI,MAAM,SAGT,MAAM,IAFa,cAAc,MAAM,KAE5B,EAAE,MAAM,sBAAsB,EAAE,IAAI,MAAM,QAAQ,CAAC;CAEhE;;;;;;;;CASA,MAAM,KACL,KACA,MACA,MACqC;EACrC,MAAM,WAAqB,CAAC;EAE5B,IAAI,KAAK,cAAc,KAAK,WAC3B,SAAS,KAAK,WAAW;EAG1B,IAAI,KAAK,kBAAkB,KAAK,eAC/B,SAAS,KAAK,eAAe;EAG9B,IAAI,KAAK,SAAS,KAAK,MACtB,SAAS,KAAK,MAAM;EAGrB,OAAO;GACN,SAAS,SAAS,SAAS;GAC3B;GACA,qBAAqB;EACtB;CACD;AACD;;AAGA,IAAM,8BAAN,cAA0C,OAAO,QAAQ,SAAS;CAIjE,YACC,MACA,MAMA,MACC;EAQD,MACC,IAAI,oCAAoC,GACxC,MACA;GACC,GAAG;GACH,OAAO;GACP,SAAS;EACV,GACA;GAAE,GAAG;GAAM,yBAAyB,CAAC,OAAO;EAAE,CAC/C;CACD;AACD;;;;;;;;;;;;;;;;;;;;;;;AAiDA,IAAa,sBAAb,cAAyC,OAAO,kBAAkB;CAOjE,YACC,MACA,MACA,MACC;EACD,MAAM,EAAE,UAAU,SAAS,aAAa,GAAG,eAAe;EAE1D,MAAM,mCAAmC,MAAM,CAAC,GAAG,UAAU;EAE7D,MAAM,WAAW,IAAI,4BACpB,GAAG,KAAK,YACR;GACC,OAAO,SAAS;GAChB,WAAW,QAAQ;GACnB,eAAe,YAAY;GAC3B,MAAM,KAAK;EACZ,GACA,EAAE,QAAQ,KAAK,CAChB;EAEA,KAAK,QAAQ,SAAS;EAEtB,KAAK,gBAAgB,EAAE,OAAO,KAAK,MAAM,CAAC;CAC3C;AACD"}
@@ -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
  */
@@ -132,9 +167,11 @@ var VercelProjectResourceProvider = class {
132
167
  if (!response.ok) throw new Error(`Vercel API error creating project "${inputs.name}" (${response.status}): ${await response.text()}`);
133
168
  projectId = (await response.json()).id;
134
169
  }
170
+ const productionUrl = await fetchProductionUrl(inputs.token, inputs.teamId, projectId, inputs.name);
135
171
  const outs = {
136
172
  ...inputs,
137
- projectId
173
+ projectId,
174
+ productionUrl
138
175
  };
139
176
  return {
140
177
  id: projectId,
@@ -144,12 +181,14 @@ var VercelProjectResourceProvider = class {
144
181
  async read(id, props) {
145
182
  const project = await fetchProject(props.token, props.teamId, id);
146
183
  if (!project) throw new Error(`Vercel project "${id}" not found during refresh`);
184
+ const productionUrl = await fetchProductionUrl(props.token, props.teamId, id, project.name);
147
185
  return {
148
186
  id: project.id,
149
187
  props: {
150
188
  ...props,
151
189
  name: project.name,
152
- projectId: project.id
190
+ projectId: project.id,
191
+ productionUrl
153
192
  }
154
193
  };
155
194
  }
@@ -163,9 +202,11 @@ var VercelProjectResourceProvider = class {
163
202
  body: JSON.stringify(buildProjectBody(news))
164
203
  });
165
204
  if (!response.ok) throw new Error(`Vercel API error updating project "${id}" (${response.status}): ${await response.text()}`);
205
+ const productionUrl = await fetchProductionUrl(news.token, news.teamId, id, news.name);
166
206
  return { outs: {
167
207
  ...news,
168
- projectId: id
208
+ projectId: id,
209
+ productionUrl
169
210
  } };
170
211
  }
171
212
  async delete() {
@@ -195,7 +236,8 @@ var VercelProjectResource = class extends _pulumi_pulumi.dynamic.Resource {
195
236
  constructor(name, args, opts) {
196
237
  super(new VercelProjectResourceProvider(), name, {
197
238
  ...args,
198
- projectId: void 0
239
+ projectId: void 0,
240
+ productionUrl: void 0
199
241
  }, opts);
200
242
  }
201
243
  };
@@ -216,7 +258,8 @@ var VercelProjectResource = class extends _pulumi_pulumi.dynamic.Resource {
216
258
  *
217
259
  * new VercelVariable("nexus-vars", {
218
260
  * projectId: project.id,
219
- * variables: { NEXT_PUBLIC_API_URL: meshUrl },
261
+ * // The app's own URL comes from the project, not from config or a derived name.
262
+ * variables: { NEXTAUTH_URL: project.url },
220
263
  * }, { provider });
221
264
  * ```
222
265
  */
@@ -230,11 +273,16 @@ var VercelProject = class extends _pulumi_pulumi.ComponentResource {
230
273
  ...args
231
274
  }, { parent: this });
232
275
  this.id = resource.projectId;
233
- this.registerOutputs({ id: this.id });
276
+ this.url = resource.productionUrl;
277
+ this.registerOutputs({
278
+ id: this.id,
279
+ url: this.url
280
+ });
234
281
  }
235
282
  };
236
283
 
237
284
  //#endregion
238
285
  exports.VERCEL_FRAMEWORKS = VERCEL_FRAMEWORKS;
239
286
  exports.VercelProject = VercelProject;
287
+ exports.pickProductionDomain = pickProductionDomain;
240
288
  //# 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\t/**\n\t * The project's production URL (with `https://`), mirroring Vercel's\n\t * `VERCEL_PROJECT_PRODUCTION_URL`: the custom production domain if one is\n\t * attached, otherwise the `<name>.vercel.app` default.\n\t */\n\tproductionUrl: 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 productionUrl = await fetchProductionUrl(\n\t\t\tinputs.token,\n\t\t\tinputs.teamId,\n\t\t\tprojectId,\n\t\t\tinputs.name,\n\t\t);\n\n\t\tconst outs: VercelProjectOutputs = { ...inputs, projectId, productionUrl };\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\tconst productionUrl = await fetchProductionUrl(\n\t\t\tprops.token,\n\t\t\tprops.teamId,\n\t\t\tid,\n\t\t\tproject.name,\n\t\t);\n\n\t\treturn {\n\t\t\tid: project.id,\n\t\t\tprops: {\n\t\t\t\t...props,\n\t\t\t\tname: project.name,\n\t\t\t\tprojectId: project.id,\n\t\t\t\tproductionUrl,\n\t\t\t},\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\tconst productionUrl = await fetchProductionUrl(\n\t\t\tnews.token,\n\t\t\tnews.teamId,\n\t\t\tid,\n\t\t\tnews.name,\n\t\t);\n\n\t\treturn { outs: { ...news, projectId: id, productionUrl } };\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\tpublic declare readonly productionUrl: 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, productionUrl: 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\t\tthis.url = resource.productionUrl;\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;EA0BS;EAvBT,eAAA;AAAA;;UAuBS,iBAAA;EACT,IAAA;EACA,QAAA;EACA,QAAA;EACA,SAAA;AAAA;AAiDD;;;;;;;;AAEa;AAeZ;;;;;;;;;;AAjBD,iBAAgB,oBAAA,CACf,OAAA,EAAS,iBAAiB,IAC1B,IAAA;;KAiRI,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;EA0BS;EAvBT,eAAA;AAAA;;UAuBS,iBAAA;EACT,IAAA;EACA,QAAA;EACA,QAAA;EACA,SAAA;AAAA;AAiDD;;;;;;;;AAEa;AAeZ;;;;;;;;;;AAjBD,iBAAgB,oBAAA,CACf,OAAA,EAAS,iBAAiB,IAC1B,IAAA;;KAiRI,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
  */
@@ -130,9 +165,11 @@ var VercelProjectResourceProvider = class {
130
165
  if (!response.ok) throw new Error(`Vercel API error creating project "${inputs.name}" (${response.status}): ${await response.text()}`);
131
166
  projectId = (await response.json()).id;
132
167
  }
168
+ const productionUrl = await fetchProductionUrl(inputs.token, inputs.teamId, projectId, inputs.name);
133
169
  const outs = {
134
170
  ...inputs,
135
- projectId
171
+ projectId,
172
+ productionUrl
136
173
  };
137
174
  return {
138
175
  id: projectId,
@@ -142,12 +179,14 @@ var VercelProjectResourceProvider = class {
142
179
  async read(id, props) {
143
180
  const project = await fetchProject(props.token, props.teamId, id);
144
181
  if (!project) throw new Error(`Vercel project "${id}" not found during refresh`);
182
+ const productionUrl = await fetchProductionUrl(props.token, props.teamId, id, project.name);
145
183
  return {
146
184
  id: project.id,
147
185
  props: {
148
186
  ...props,
149
187
  name: project.name,
150
- projectId: project.id
188
+ projectId: project.id,
189
+ productionUrl
151
190
  }
152
191
  };
153
192
  }
@@ -161,9 +200,11 @@ var VercelProjectResourceProvider = class {
161
200
  body: JSON.stringify(buildProjectBody(news))
162
201
  });
163
202
  if (!response.ok) throw new Error(`Vercel API error updating project "${id}" (${response.status}): ${await response.text()}`);
203
+ const productionUrl = await fetchProductionUrl(news.token, news.teamId, id, news.name);
164
204
  return { outs: {
165
205
  ...news,
166
- projectId: id
206
+ projectId: id,
207
+ productionUrl
167
208
  } };
168
209
  }
169
210
  async delete() {
@@ -193,7 +234,8 @@ var VercelProjectResource = class extends pulumi.dynamic.Resource {
193
234
  constructor(name, args, opts) {
194
235
  super(new VercelProjectResourceProvider(), name, {
195
236
  ...args,
196
- projectId: void 0
237
+ projectId: void 0,
238
+ productionUrl: void 0
197
239
  }, opts);
198
240
  }
199
241
  };
@@ -214,7 +256,8 @@ var VercelProjectResource = class extends pulumi.dynamic.Resource {
214
256
  *
215
257
  * new VercelVariable("nexus-vars", {
216
258
  * projectId: project.id,
217
- * variables: { NEXT_PUBLIC_API_URL: meshUrl },
259
+ * // The app's own URL comes from the project, not from config or a derived name.
260
+ * variables: { NEXTAUTH_URL: project.url },
218
261
  * }, { provider });
219
262
  * ```
220
263
  */
@@ -228,10 +271,14 @@ var VercelProject = class extends pulumi.ComponentResource {
228
271
  ...args
229
272
  }, { parent: this });
230
273
  this.id = resource.projectId;
231
- this.registerOutputs({ id: this.id });
274
+ this.url = resource.productionUrl;
275
+ this.registerOutputs({
276
+ id: this.id,
277
+ url: this.url
278
+ });
232
279
  }
233
280
  };
234
281
 
235
282
  //#endregion
236
- export { VERCEL_FRAMEWORKS, VercelProject };
283
+ export { VERCEL_FRAMEWORKS, VercelProject, pickProductionDomain };
237
284
  //# 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":";;;;AAGA,MAAM,iBAAiB;;;;;;;AAQvB,MAAa,oBAAoB;CAEhC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA;CACA;CACA;CACA;CACA;CACA;CAEA;CACA;CACA;CACA;CAEA;CACA;CACA;CAEA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA;CACA;CACA;CACA;CACA;CAEA;CACA;CAEA;CACA;CAEA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA;CACA;CACA;CACA;CAEA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA;CACA;CACA;CACA;AACD;;;;;AAmDA,eAAe,aACd,OACA,QACA,UACwC;CACxC,MAAM,WAAW,MAAM,MACtB,GAAG,eAAe,eAAe,mBAAmB,QAAQ,EAAE,UAAU,UACxE,EAAE,SAAS,EAAE,eAAe,UAAU,QAAQ,EAAE,CACjD;CAEA,IAAI,SAAS,WAAW,KACvB,OAAO;CAGR,IAAI,CAAC,SAAS,IACb,MAAM,IAAI,MACT,sCAAsC,SAAS,KAAK,SAAS,OAAO,KAAK,MAAM,SAAS,KAAK,GAC9F;CAGD,OAAQ,MAAM,SAAS,KAAK;AAC7B;;;;;AAMA,SAAS,iBACR,QACyB;CACzB,MAAM,OAA+B,EAAE,MAAM,OAAO,KAAK;CAEzD,IAAI,OAAO,cAAc,QACxB,KAAK,YAAY,OAAO;CAGzB,IAAI,OAAO,kBAAkB,QAC5B,KAAK,gBAAgB,OAAO;CAG7B,IAAI,OAAO,iBAAiB,QAC3B,KAAK,eAAe,OAAO;CAG5B,IAAI,OAAO,mBAAmB,QAC7B,KAAK,iBAAiB,OAAO;CAG9B,IAAI,OAAO,oBAAoB,QAC9B,KAAK,kBAAkB,OAAO;CAG/B,OAAO;AACR;;;;;;;;AASA,IAAM,gCAAN,MAA+E;CAC9E,MAAM,OACL,QACuC;EACvC,MAAM,WAAW,MAAM,aACtB,OAAO,OACP,OAAO,QACP,OAAO,IACR;EAEA,IAAI;EAEJ,IAAI,UAAU;GACb,OAAO,IAAI,KACV,qCAAqC,OAAO,KAAK,KAAK,SAAS,GAAG,EACnE;GAEA,YAAY,SAAS;EACtB,OAAO;GACN,OAAO,IAAI,KACV,mBAAmB,OAAO,KAAK,0BAChC;GAEA,MAAM,WAAW,MAAM,MACtB,GAAG,eAAe,sBAAsB,OAAO,UAC/C;IACC,QAAQ;IACR,SAAS;KACR,eAAe,UAAU,OAAO;KAChC,gBAAgB;IACjB;IACA,MAAM,KAAK,UAAU,iBAAiB,MAAM,CAAC;GAC9C,CACD;GAEA,IAAI,CAAC,SAAS,IACb,MAAM,IAAI,MACT,sCAAsC,OAAO,KAAK,KAAK,SAAS,OAAO,KAAK,MAAM,SAAS,KAAK,GACjG;GAKD,aAAY,MAFW,SAAS,KAAK,GAEjB;EACrB;EAEA,MAAM,OAA6B;GAAE,GAAG;GAAQ;EAAU;EAE1D,OAAO;GAAE,IAAI;GAAW;EAAK;CAC9B;CAEA,MAAM,KACL,IACA,OACqC;EACrC,MAAM,UAAU,MAAM,aAAa,MAAM,OAAO,MAAM,QAAQ,EAAE;EAEhE,IAAI,CAAC,SACJ,MAAM,IAAI,MAAM,mBAAmB,GAAG,2BAA2B;EAGlE,OAAO;GACN,IAAI,QAAQ;GACZ,OAAO;IAAE,GAAG;IAAO,MAAM,QAAQ;IAAM,WAAW,QAAQ;GAAG;EAC9D;CACD;CAEA,MAAM,OACL,IACA,OACA,MACuC;EACvC,MAAM,WAAW,MAAM,MACtB,GAAG,eAAe,eAAe,GAAG,UAAU,KAAK,UACnD;GACC,QAAQ;GACR,SAAS;IACR,eAAe,UAAU,KAAK;IAC9B,gBAAgB;GACjB;GACA,MAAM,KAAK,UAAU,iBAAiB,IAAI,CAAC;EAC5C,CACD;EAEA,IAAI,CAAC,SAAS,IACb,MAAM,IAAI,MACT,sCAAsC,GAAG,KAAK,SAAS,OAAO,KAAK,MAAM,SAAS,KAAK,GACxF;EAGD,OAAO,EAAE,MAAM;GAAE,GAAG;GAAM,WAAW;EAAG,EAAE;CAC3C;CAEA,MAAM,SAAwB;EAC7B,OAAO,IAAI,KACV,sEACD;CACD;CAEA,MAAM,KACL,KACA,MACA,MACqC;EACrC,MAAM,WAAqB,CAAC;EAC5B,MAAM,UAAoB,CAAC;EAE3B,IAAI,KAAK,WAAW,KAAK,QACxB,SAAS,KAAK,QAAQ;EAYvB,KAAK,MAAM,SAAS;GARnB;GACA;GACA;GACA;GACA;GACA;EAGiC,GACjC,IAAI,KAAK,WAAW,KAAK,QACxB,QAAQ,KAAK,KAAK;EAIpB,OAAO;GACN,SAAS,SAAS,SAAS,KAAK,QAAQ,SAAS;GACjD;GACA,qBAAqB;EACtB;CACD;AACD;;AAGA,IAAM,wBAAN,cAAoC,OAAO,QAAQ,SAAS;CAG3D,YACC,MACA,MAUA,MACC;EACD,MACC,IAAI,8BAA8B,GAClC,MACA;GAAE,GAAG;GAAM,WAAW;EAAU,GAChC,IACD;CACD;AACD;;;;;;;;;;;;;;;;;;;;;;AAqDA,IAAa,gBAAb,cAAmC,OAAO,kBAAkB;CAI3D,YACC,MACA,MACA,MACC;EACD,MAAM,EAAE,UAAU,GAAG,eAAe;EAEpC,MAAM,6BAA6B,MAAM,CAAC,GAAG,UAAU;EAEvD,MAAM,WAAW,IAAI,sBACpB,GAAG,KAAK,YACR;GACC,OAAO,SAAS;GAChB,QAAQ,SAAS;GACjB,GAAG;EACJ,GACA,EAAE,QAAQ,KAAK,CAChB;EAEA,KAAK,KAAK,SAAS;EAEnB,KAAK,gBAAgB,EAAE,IAAI,KAAK,GAAG,CAAC;CACrC;AACD"}
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\t/**\n\t * The project's production URL (with `https://`), mirroring Vercel's\n\t * `VERCEL_PROJECT_PRODUCTION_URL`: the custom production domain if one is\n\t * attached, otherwise the `<name>.vercel.app` default.\n\t */\n\tproductionUrl: 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 productionUrl = await fetchProductionUrl(\n\t\t\tinputs.token,\n\t\t\tinputs.teamId,\n\t\t\tprojectId,\n\t\t\tinputs.name,\n\t\t);\n\n\t\tconst outs: VercelProjectOutputs = { ...inputs, projectId, productionUrl };\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\tconst productionUrl = await fetchProductionUrl(\n\t\t\tprops.token,\n\t\t\tprops.teamId,\n\t\t\tid,\n\t\t\tproject.name,\n\t\t);\n\n\t\treturn {\n\t\t\tid: project.id,\n\t\t\tprops: {\n\t\t\t\t...props,\n\t\t\t\tname: project.name,\n\t\t\t\tprojectId: project.id,\n\t\t\t\tproductionUrl,\n\t\t\t},\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\tconst productionUrl = await fetchProductionUrl(\n\t\t\tnews.token,\n\t\t\tnews.teamId,\n\t\t\tid,\n\t\t\tnews.name,\n\t\t);\n\n\t\treturn { outs: { ...news, projectId: id, productionUrl } };\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\tpublic declare readonly productionUrl: 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, productionUrl: 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\t\tthis.url = resource.productionUrl;\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.5",
3
+ "version": "1.7.0",
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": {