@infracraft/pulumi 1.23.0 → 1.25.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.
@@ -69,13 +69,30 @@ var NeonRoleResourceProvider = class {
69
69
  _pulumi_pulumi.log.warn(`Failed to delete Neon role "${props.name}" (may already be deleted)`);
70
70
  }
71
71
  }
72
+ /**
73
+ * Rotates the password in place when `passwordVersion` changes. This is the
74
+ * only updatable input (everything else replaces), so an update firing means
75
+ * a rotation was requested.
76
+ */
77
+ async update(_id, olds, news) {
78
+ let password = olds.password;
79
+ if (olds.passwordVersion !== news.passwordVersion) {
80
+ _pulumi_pulumi.log.info(`Rotating password for Neon role "${news.name}" (passwordVersion ${olds.passwordVersion ?? "unset"} → ${news.passwordVersion ?? "unset"})`);
81
+ password = await resetRolePassword(new require_neon_client.NeonClient(news.apiKey), news.projectId, news.branchId, news.name);
82
+ }
83
+ return { outs: {
84
+ ...news,
85
+ password
86
+ } };
87
+ }
72
88
  async diff(_id, olds, news) {
73
89
  const replaces = [];
74
90
  if (olds.projectId !== news.projectId) replaces.push("projectId");
75
91
  if (olds.branchId !== news.branchId) replaces.push("branchId");
76
92
  if (olds.name !== news.name) replaces.push("name");
93
+ const rotates = olds.passwordVersion !== news.passwordVersion;
77
94
  return {
78
- changes: replaces.length > 0,
95
+ changes: replaces.length > 0 || rotates,
79
96
  replaces,
80
97
  deleteBeforeReplace: true
81
98
  };
@@ -113,7 +130,8 @@ var NeonRole = class extends _pulumi_pulumi.ComponentResource {
113
130
  projectId: project.id,
114
131
  branchId: branch.id,
115
132
  name: args.name,
116
- resetPassword: args.resetPassword ?? false
133
+ resetPassword: args.resetPassword ?? false,
134
+ passwordVersion: args.passwordVersion
117
135
  }, { parent: this });
118
136
  this.password = resource.password;
119
137
  this.registerOutputs({ password: this.password });
@@ -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\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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"}
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\t/**\n\t * Rotation handle: bumping this number resets the role's password IN PLACE on\n\t * the next `up` (an update, never a replace — replacing would try to delete\n\t * the role, which Neon refuses for default roles and which would drop real\n\t * grants for others). Leave unset until the first rotation is needed.\n\t */\n\tpasswordVersion?: number;\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\t/**\n\t * Rotates the password in place when `passwordVersion` changes. This is the\n\t * only updatable input (everything else replaces), so an update firing means\n\t * a rotation was requested.\n\t */\n\tasync update(\n\t\t_id: string,\n\t\tolds: NeonRoleOutputs,\n\t\tnews: NeonRoleInputs,\n\t): Promise<pulumi.dynamic.UpdateResult> {\n\t\tlet password = olds.password;\n\n\t\tif (olds.passwordVersion !== news.passwordVersion) {\n\t\t\tpulumi.log.info(\n\t\t\t\t`Rotating password for Neon role \"${news.name}\" (passwordVersion ${olds.passwordVersion ?? \"unset\"} → ${news.passwordVersion ?? \"unset\"})`,\n\t\t\t);\n\n\t\t\tconst client = new NeonClient(news.apiKey);\n\n\t\t\tpassword = await resetRolePassword(\n\t\t\t\tclient,\n\t\t\t\tnews.projectId,\n\t\t\t\tnews.branchId,\n\t\t\t\tnews.name,\n\t\t\t);\n\t\t}\n\n\t\treturn { outs: { ...news, password } };\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\tconst rotates = olds.passwordVersion !== news.passwordVersion;\n\n\t\treturn {\n\t\t\tchanges: replaces.length > 0 || rotates,\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\tpasswordVersion?: pulumi.Input<number>;\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\t/**\n\t * Rotation handle: bump to rotate the role's password in place on the next\n\t * `up`. Everything that consumes `password` (connection strings, service env\n\t * vars, dependent redeploys) cascades automatically.\n\t */\n\tpasswordVersion?: pulumi.Input<number>;\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\tpasswordVersion: args.passwordVersion,\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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"}
@@ -22,6 +22,13 @@ interface NeonRoleInputs {
22
22
  * freshly created (a new role already gets its own password).
23
23
  */
24
24
  resetPassword: boolean;
25
+ /**
26
+ * Rotation handle: bumping this number resets the role's password IN PLACE on
27
+ * the next `up` (an update, never a replace — replacing would try to delete
28
+ * the role, which Neon refuses for default roles and which would drop real
29
+ * grants for others). Leave unset until the first rotation is needed.
30
+ */
31
+ passwordVersion?: number;
25
32
  }
26
33
  /** Persisted state for the Neon role. */
27
34
  interface NeonRoleOutputs extends NeonRoleInputs {
@@ -40,6 +47,12 @@ declare class NeonRoleResourceProvider implements pulumi.dynamic.ResourceProvide
40
47
  create(inputs: NeonRoleInputs): Promise<pulumi.dynamic.CreateResult>;
41
48
  read(id: string, props: NeonRoleOutputs): Promise<pulumi.dynamic.ReadResult>;
42
49
  delete(_id: string, props: NeonRoleOutputs): Promise<void>;
50
+ /**
51
+ * Rotates the password in place when `passwordVersion` changes. This is the
52
+ * only updatable input (everything else replaces), so an update firing means
53
+ * a rotation was requested.
54
+ */
55
+ update(_id: string, olds: NeonRoleOutputs, news: NeonRoleInputs): Promise<pulumi.dynamic.UpdateResult>;
43
56
  diff(_id: string, olds: NeonRoleOutputs, news: NeonRoleInputs): Promise<pulumi.dynamic.DiffResult>;
44
57
  }
45
58
  /** Options type for NeonRole — replaces Pulumi's native `provider` field. */
@@ -59,6 +72,12 @@ interface NeonRoleArgs {
59
72
  * `false` (adopt and reveal the existing password). The reset runs once, at creation.
60
73
  */
61
74
  resetPassword?: pulumi.Input<boolean>;
75
+ /**
76
+ * Rotation handle: bump to rotate the role's password in place on the next
77
+ * `up`. Everything that consumes `password` (connection strings, service env
78
+ * vars, dependent redeploys) cascades automatically.
79
+ */
80
+ passwordVersion?: pulumi.Input<number>;
62
81
  }
63
82
  /**
64
83
  * Manages a Neon database role with adopt-or-create semantics.
@@ -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;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"}
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;;;;AAgBe;AACf;EATA,aAAA;;;AAcQ;AAuFT;;;EA7FC,eAAA;AAAA;;UAIS,eAAA,SAAwB,cAAc;EA2IpC;EAzIX,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;EAjExC;;;;;EAoFL,MAAA,CACL,GAAA,UACA,IAAA,EAAM,eAAA,EACN,IAAA,EAAM,cAAA,GACJ,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,YAAA;EAqBpB,IAAA,CACL,GAAA,UACA,IAAA,EAAM,eAAA,EACN,IAAA,EAAM,cAAA,GACJ,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,UAAA;AAAA;;KAyDtB,eAAA,GAAkB,IAAA,CAAK,MAAA,CAAO,wBAAA;EA5H5B,mCA8HN,QAAA,EAAU,YAAA,EA5HF;EA+HR,OAAA,EAAS,WAAA,EA9HN;EAiIH,MAAA,EAAQ,UAAA;AAAA;;UAIQ,YAAA;EArHH;EAuHb,IAAA,EAAM,MAAA,CAAO,KAAA;EAvHa;;;;;;EA+H1B,aAAA,GAAgB,MAAA,CAAO,KAAA;EAzGtB;;;;;EAgHD,eAAA,GAAkB,MAAA,CAAO,KAAA;AAAA;;;;;;;;;;AAtFW;AAuBpC;cA6EY,QAAA,SAAiB,MAAA,CAAO,iBAAA;;WAEpB,QAAA,EAAU,MAAA,CAAO,MAAA;cAErB,IAAA,UAAc,IAAA,EAAM,YAAA,EAAc,IAAA,EAAM,eAAA;AAAA"}
@@ -22,6 +22,13 @@ interface NeonRoleInputs {
22
22
  * freshly created (a new role already gets its own password).
23
23
  */
24
24
  resetPassword: boolean;
25
+ /**
26
+ * Rotation handle: bumping this number resets the role's password IN PLACE on
27
+ * the next `up` (an update, never a replace — replacing would try to delete
28
+ * the role, which Neon refuses for default roles and which would drop real
29
+ * grants for others). Leave unset until the first rotation is needed.
30
+ */
31
+ passwordVersion?: number;
25
32
  }
26
33
  /** Persisted state for the Neon role. */
27
34
  interface NeonRoleOutputs extends NeonRoleInputs {
@@ -40,6 +47,12 @@ declare class NeonRoleResourceProvider implements pulumi.dynamic.ResourceProvide
40
47
  create(inputs: NeonRoleInputs): Promise<pulumi.dynamic.CreateResult>;
41
48
  read(id: string, props: NeonRoleOutputs): Promise<pulumi.dynamic.ReadResult>;
42
49
  delete(_id: string, props: NeonRoleOutputs): Promise<void>;
50
+ /**
51
+ * Rotates the password in place when `passwordVersion` changes. This is the
52
+ * only updatable input (everything else replaces), so an update firing means
53
+ * a rotation was requested.
54
+ */
55
+ update(_id: string, olds: NeonRoleOutputs, news: NeonRoleInputs): Promise<pulumi.dynamic.UpdateResult>;
43
56
  diff(_id: string, olds: NeonRoleOutputs, news: NeonRoleInputs): Promise<pulumi.dynamic.DiffResult>;
44
57
  }
45
58
  /** Options type for NeonRole — replaces Pulumi's native `provider` field. */
@@ -59,6 +72,12 @@ interface NeonRoleArgs {
59
72
  * `false` (adopt and reveal the existing password). The reset runs once, at creation.
60
73
  */
61
74
  resetPassword?: pulumi.Input<boolean>;
75
+ /**
76
+ * Rotation handle: bump to rotate the role's password in place on the next
77
+ * `up`. Everything that consumes `password` (connection strings, service env
78
+ * vars, dependent redeploys) cascades automatically.
79
+ */
80
+ passwordVersion?: pulumi.Input<number>;
62
81
  }
63
82
  /**
64
83
  * Manages a Neon database role with adopt-or-create semantics.
@@ -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;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"}
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;;;;AAgBe;AACf;EATA,aAAA;;;AAcQ;AAuFT;;;EA7FC,eAAA;AAAA;;UAIS,eAAA,SAAwB,cAAc;EA2IpC;EAzIX,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;EAjExC;;;;;EAoFL,MAAA,CACL,GAAA,UACA,IAAA,EAAM,eAAA,EACN,IAAA,EAAM,cAAA,GACJ,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,YAAA;EAqBpB,IAAA,CACL,GAAA,UACA,IAAA,EAAM,eAAA,EACN,IAAA,EAAM,cAAA,GACJ,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,UAAA;AAAA;;KAyDtB,eAAA,GAAkB,IAAA,CAAK,MAAA,CAAO,wBAAA;EA5H5B,mCA8HN,QAAA,EAAU,YAAA,EA5HF;EA+HR,OAAA,EAAS,WAAA,EA9HN;EAiIH,MAAA,EAAQ,UAAA;AAAA;;UAIQ,YAAA;EArHH;EAuHb,IAAA,EAAM,MAAA,CAAO,KAAA;EAvHa;;;;;;EA+H1B,aAAA,GAAgB,MAAA,CAAO,KAAA;EAzGtB;;;;;EAgHD,eAAA,GAAkB,MAAA,CAAO,KAAA;AAAA;;;;;;;;;;AAtFW;AAuBpC;cA6EY,QAAA,SAAiB,MAAA,CAAO,iBAAA;;WAEpB,QAAA,EAAU,MAAA,CAAO,MAAA;cAErB,IAAA,UAAc,IAAA,EAAM,YAAA,EAAc,IAAA,EAAM,eAAA;AAAA"}
@@ -67,13 +67,30 @@ var NeonRoleResourceProvider = class {
67
67
  pulumi.log.warn(`Failed to delete Neon role "${props.name}" (may already be deleted)`);
68
68
  }
69
69
  }
70
+ /**
71
+ * Rotates the password in place when `passwordVersion` changes. This is the
72
+ * only updatable input (everything else replaces), so an update firing means
73
+ * a rotation was requested.
74
+ */
75
+ async update(_id, olds, news) {
76
+ let password = olds.password;
77
+ if (olds.passwordVersion !== news.passwordVersion) {
78
+ pulumi.log.info(`Rotating password for Neon role "${news.name}" (passwordVersion ${olds.passwordVersion ?? "unset"} → ${news.passwordVersion ?? "unset"})`);
79
+ password = await resetRolePassword(new NeonClient(news.apiKey), news.projectId, news.branchId, news.name);
80
+ }
81
+ return { outs: {
82
+ ...news,
83
+ password
84
+ } };
85
+ }
70
86
  async diff(_id, olds, news) {
71
87
  const replaces = [];
72
88
  if (olds.projectId !== news.projectId) replaces.push("projectId");
73
89
  if (olds.branchId !== news.branchId) replaces.push("branchId");
74
90
  if (olds.name !== news.name) replaces.push("name");
91
+ const rotates = olds.passwordVersion !== news.passwordVersion;
75
92
  return {
76
- changes: replaces.length > 0,
93
+ changes: replaces.length > 0 || rotates,
77
94
  replaces,
78
95
  deleteBeforeReplace: true
79
96
  };
@@ -111,7 +128,8 @@ var NeonRole = class extends pulumi.ComponentResource {
111
128
  projectId: project.id,
112
129
  branchId: branch.id,
113
130
  name: args.name,
114
- resetPassword: args.resetPassword ?? false
131
+ resetPassword: args.resetPassword ?? false,
132
+ passwordVersion: args.passwordVersion
115
133
  }, { parent: this });
116
134
  this.password = resource.password;
117
135
  this.registerOutputs({ password: this.password });
@@ -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\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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"}
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\t/**\n\t * Rotation handle: bumping this number resets the role's password IN PLACE on\n\t * the next `up` (an update, never a replace — replacing would try to delete\n\t * the role, which Neon refuses for default roles and which would drop real\n\t * grants for others). Leave unset until the first rotation is needed.\n\t */\n\tpasswordVersion?: number;\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\t/**\n\t * Rotates the password in place when `passwordVersion` changes. This is the\n\t * only updatable input (everything else replaces), so an update firing means\n\t * a rotation was requested.\n\t */\n\tasync update(\n\t\t_id: string,\n\t\tolds: NeonRoleOutputs,\n\t\tnews: NeonRoleInputs,\n\t): Promise<pulumi.dynamic.UpdateResult> {\n\t\tlet password = olds.password;\n\n\t\tif (olds.passwordVersion !== news.passwordVersion) {\n\t\t\tpulumi.log.info(\n\t\t\t\t`Rotating password for Neon role \"${news.name}\" (passwordVersion ${olds.passwordVersion ?? \"unset\"} → ${news.passwordVersion ?? \"unset\"})`,\n\t\t\t);\n\n\t\t\tconst client = new NeonClient(news.apiKey);\n\n\t\t\tpassword = await resetRolePassword(\n\t\t\t\tclient,\n\t\t\t\tnews.projectId,\n\t\t\t\tnews.branchId,\n\t\t\t\tnews.name,\n\t\t\t);\n\t\t}\n\n\t\treturn { outs: { ...news, password } };\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\tconst rotates = olds.passwordVersion !== news.passwordVersion;\n\n\t\treturn {\n\t\t\tchanges: replaces.length > 0 || rotates,\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\tpasswordVersion?: pulumi.Input<number>;\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\t/**\n\t * Rotation handle: bump to rotate the role's password in place on the next\n\t * `up`. Everything that consumes `password` (connection strings, service env\n\t * vars, dependent redeploys) cascades automatically.\n\t */\n\tpasswordVersion?: pulumi.Input<number>;\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\tpasswordVersion: args.passwordVersion,\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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"}
@@ -89,14 +89,15 @@ var RailwayProjectTokenResourceProvider = class {
89
89
  * @param news Newly resolved inputs.
90
90
  */
91
91
  async diff(_id, olds, news) {
92
- const replaces = [];
93
- if (olds.projectId !== news.projectId) replaces.push("projectId");
94
- if (olds.environmentId !== news.environmentId) replaces.push("environmentId");
95
- if (olds.name !== news.name) replaces.push("name");
92
+ const identityReplaces = [];
93
+ if (olds.projectId !== news.projectId) identityReplaces.push("projectId");
94
+ if (olds.environmentId !== news.environmentId) identityReplaces.push("environmentId");
95
+ if (olds.name !== news.name) identityReplaces.push("name");
96
+ const rotates = olds.tokenVersion !== news.tokenVersion;
96
97
  return {
97
- changes: replaces.length > 0,
98
- replaces,
99
- deleteBeforeReplace: true
98
+ changes: identityReplaces.length > 0 || rotates,
99
+ replaces: rotates ? [...identityReplaces, "tokenVersion"] : identityReplaces,
100
+ deleteBeforeReplace: identityReplaces.length > 0
100
101
  };
101
102
  }
102
103
  };
@@ -143,7 +144,8 @@ var RailwayProjectToken = class extends _pulumi_pulumi.ComponentResource {
143
144
  token: provider.token,
144
145
  projectId: project.id,
145
146
  environmentId: environment.id,
146
- name: args.name
147
+ name: args.name,
148
+ tokenVersion: args.tokenVersion
147
149
  }, { parent: this });
148
150
  this.token = resource.value;
149
151
  this.registerOutputs({ token: this.token });
@@ -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\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
+ {"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\t/**\n\t * Rotation handle: bumping this number replaces the token on the next `up` —\n\t * the new token is minted BEFORE the old one is revoked (create-before-delete,\n\t * unlike identity changes), so there is never a tokenless window. Leave unset\n\t * until the first rotation is needed.\n\t */\n\ttokenVersion?: number;\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 identityReplaces: string[] = [];\n\n\t\tif (olds.projectId !== news.projectId) {\n\t\t\tidentityReplaces.push(\"projectId\");\n\t\t}\n\n\t\tif (olds.environmentId !== news.environmentId) {\n\t\t\tidentityReplaces.push(\"environmentId\");\n\t\t}\n\n\t\tif (olds.name !== news.name) {\n\t\t\tidentityReplaces.push(\"name\");\n\t\t}\n\n\t\tconst rotates = olds.tokenVersion !== news.tokenVersion;\n\n\t\treturn {\n\t\t\tchanges: identityReplaces.length > 0 || rotates,\n\t\t\treplaces: rotates\n\t\t\t\t? [...identityReplaces, \"tokenVersion\"]\n\t\t\t\t: identityReplaces,\n\t\t\t// Identity changes revoke the old token first (names must stay unique);\n\t\t\t// a pure rotation mints the new token BEFORE revoking the old so there\n\t\t\t// is never a tokenless window — `create()` already cleans up stale\n\t\t\t// same-name tokens, so the transient name collision is handled.\n\t\t\tdeleteBeforeReplace: identityReplaces.length > 0,\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\ttokenVersion?: pulumi.Input<number>;\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\t/**\n\t * Rotation handle: bump to mint a fresh token on the next `up` (the old one\n\t * is revoked only after the new one exists). Consumers of `token` cascade\n\t * automatically.\n\t */\n\ttokenVersion?: pulumi.Input<number>;\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\ttokenVersion: args.tokenVersion,\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":";;;;;;;AAsCA,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,mBAA6B,CAAC;EAEpC,IAAI,KAAK,cAAc,KAAK,WAC3B,iBAAiB,KAAK,WAAW;EAGlC,IAAI,KAAK,kBAAkB,KAAK,eAC/B,iBAAiB,KAAK,eAAe;EAGtC,IAAI,KAAK,SAAS,KAAK,MACtB,iBAAiB,KAAK,MAAM;EAG7B,MAAM,UAAU,KAAK,iBAAiB,KAAK;EAE3C,OAAO;GACN,SAAS,iBAAiB,SAAS,KAAK;GACxC,UAAU,UACP,CAAC,GAAG,kBAAkB,cAAc,IACpC;GAKH,qBAAqB,iBAAiB,SAAS;EAChD;CACD;AACD;;AAGA,IAAM,8BAAN,cAA0CC,eAAO,QAAQ,SAAS;CAIjE,YACC,MACA,MAOA,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;;;;;;;;;;;;;;;;;;;;;;;AAwDA,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;GACX,cAAc,KAAK;EACpB,GACA,EAAE,QAAQ,KAAK,CAChB;EAEA,KAAK,QAAQ,SAAS;EAEtB,KAAK,gBAAgB,EAAE,OAAO,KAAK,MAAM,CAAC;CAC3C;AACD"}
@@ -15,6 +15,13 @@ interface RailwayProjectTokenInputs {
15
15
  environmentId: string;
16
16
  /** Distinct token name, e.g. `"pulumi-staging"`. Must be unique per environment. */
17
17
  name: string;
18
+ /**
19
+ * Rotation handle: bumping this number replaces the token on the next `up` —
20
+ * the new token is minted BEFORE the old one is revoked (create-before-delete,
21
+ * unlike identity changes), so there is never a tokenless window. Leave unset
22
+ * until the first rotation is needed.
23
+ */
24
+ tokenVersion?: number;
18
25
  }
19
26
  /** Persisted state for the Railway project token. */
20
27
  interface RailwayProjectTokenOutputs extends RailwayProjectTokenInputs {
@@ -79,6 +86,12 @@ interface RailwayProjectTokenArgs {
79
86
  * a project never collide on token ownership.
80
87
  */
81
88
  name: pulumi.Input<string>;
89
+ /**
90
+ * Rotation handle: bump to mint a fresh token on the next `up` (the old one
91
+ * is revoked only after the new one exists). Consumers of `token` cascade
92
+ * automatically.
93
+ */
94
+ tokenVersion?: pulumi.Input<number>;
82
95
  }
83
96
  /**
84
97
  * Provisions an environment-scoped Railway deploy token.
@@ -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;;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
+ {"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;EAAA;;;AAQY;AAAA;;EAAZ,YAAA;AAAA;;UAIS,0BAAA,SAAmC,yBAAyB;EAKrE;EAHA,KAAA;EAGO;EAAP,OAAA;AAAA;;;;;;;;;;cA8BY,mCAAA,YACD,MAAA,CAAO,OAAA,CAAQ,gBAAA;EA8GnB;;;;;;;EArGD,MAAA,CACL,MAAA,EAAQ,yBAAA,GACN,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,YAAA;EAXA;;;;;;;EA+EpB,IAAA,CACL,EAAA,UACA,KAAA,EAAO,0BAAA,GACL,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,UAAA;EAHpB;;;;;;EAaA,MAAA,CAAO,GAAA,UAAa,KAAA,EAAO,0BAAA,GAA6B,OAAA;EAVpC;;;;;;;EAyBpB,IAAA,CACL,GAAA,UACA,IAAA,EAAM,0BAAA,EACN,IAAA,EAAM,yBAAA,GACJ,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,UAAA;AAAA;;KAoEtB,0BAAA,GAA6B,IAAA,CACjC,MAAA,CAAO,wBAAA;EAtEN,sCA0ED,QAAA,EAAU,eAAA,EAzEC;EA4EX,OAAA,EAAS,cAAA,EA5EiB;EA+E1B,WAAA,EAAa,kBAAA;AAAA;AAlDb;AAAA,UAsDgB,uBAAA;;;;;;EAMhB,IAAA,EAAM,MAAA,CAAO,KAAA;EAVkB;;;;;EAiB/B,YAAA,GAAe,MAAA,CAAO,KAAK;AAAA;;;;;;AAjBI;AAIhC;;;;;;;;;;;AAa4B;AAyB5B;;;;cAAa,mBAAA,SAA4B,MAAA,CAAO,iBAAA;EAUxC;;;;EAAA,SALS,KAAA,EAAO,MAAA,CAAO,MAAA;cAG7B,IAAA,UACA,IAAA,EAAM,uBAAA,EACN,IAAA,EAAM,0BAAA;AAAA"}
@@ -15,6 +15,13 @@ interface RailwayProjectTokenInputs {
15
15
  environmentId: string;
16
16
  /** Distinct token name, e.g. `"pulumi-staging"`. Must be unique per environment. */
17
17
  name: string;
18
+ /**
19
+ * Rotation handle: bumping this number replaces the token on the next `up` —
20
+ * the new token is minted BEFORE the old one is revoked (create-before-delete,
21
+ * unlike identity changes), so there is never a tokenless window. Leave unset
22
+ * until the first rotation is needed.
23
+ */
24
+ tokenVersion?: number;
18
25
  }
19
26
  /** Persisted state for the Railway project token. */
20
27
  interface RailwayProjectTokenOutputs extends RailwayProjectTokenInputs {
@@ -79,6 +86,12 @@ interface RailwayProjectTokenArgs {
79
86
  * a project never collide on token ownership.
80
87
  */
81
88
  name: pulumi.Input<string>;
89
+ /**
90
+ * Rotation handle: bump to mint a fresh token on the next `up` (the old one
91
+ * is revoked only after the new one exists). Consumers of `token` cascade
92
+ * automatically.
93
+ */
94
+ tokenVersion?: pulumi.Input<number>;
82
95
  }
83
96
  /**
84
97
  * Provisions an environment-scoped Railway deploy token.
@@ -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;;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
+ {"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;EAAA;;;AAQY;AAAA;;EAAZ,YAAA;AAAA;;UAIS,0BAAA,SAAmC,yBAAyB;EAKrE;EAHA,KAAA;EAGO;EAAP,OAAA;AAAA;;;;;;;;;;cA8BY,mCAAA,YACD,MAAA,CAAO,OAAA,CAAQ,gBAAA;EA8GnB;;;;;;;EArGD,MAAA,CACL,MAAA,EAAQ,yBAAA,GACN,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,YAAA;EAXA;;;;;;;EA+EpB,IAAA,CACL,EAAA,UACA,KAAA,EAAO,0BAAA,GACL,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,UAAA;EAHpB;;;;;;EAaA,MAAA,CAAO,GAAA,UAAa,KAAA,EAAO,0BAAA,GAA6B,OAAA;EAVpC;;;;;;;EAyBpB,IAAA,CACL,GAAA,UACA,IAAA,EAAM,0BAAA,EACN,IAAA,EAAM,yBAAA,GACJ,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,UAAA;AAAA;;KAoEtB,0BAAA,GAA6B,IAAA,CACjC,MAAA,CAAO,wBAAA;EAtEN,sCA0ED,QAAA,EAAU,eAAA,EAzEC;EA4EX,OAAA,EAAS,cAAA,EA5EiB;EA+E1B,WAAA,EAAa,kBAAA;AAAA;AAlDb;AAAA,UAsDgB,uBAAA;;;;;;EAMhB,IAAA,EAAM,MAAA,CAAO,KAAA;EAVkB;;;;;EAiB/B,YAAA,GAAe,MAAA,CAAO,KAAK;AAAA;;;;;;AAjBI;AAIhC;;;;;;;;;;;AAa4B;AAyB5B;;;;cAAa,mBAAA,SAA4B,MAAA,CAAO,iBAAA;EAUxC;;;;EAAA,SALS,KAAA,EAAO,MAAA,CAAO,MAAA;cAG7B,IAAA,UACA,IAAA,EAAM,uBAAA,EACN,IAAA,EAAM,0BAAA;AAAA"}
@@ -87,14 +87,15 @@ var RailwayProjectTokenResourceProvider = class {
87
87
  * @param news Newly resolved inputs.
88
88
  */
89
89
  async diff(_id, olds, news) {
90
- const replaces = [];
91
- if (olds.projectId !== news.projectId) replaces.push("projectId");
92
- if (olds.environmentId !== news.environmentId) replaces.push("environmentId");
93
- if (olds.name !== news.name) replaces.push("name");
90
+ const identityReplaces = [];
91
+ if (olds.projectId !== news.projectId) identityReplaces.push("projectId");
92
+ if (olds.environmentId !== news.environmentId) identityReplaces.push("environmentId");
93
+ if (olds.name !== news.name) identityReplaces.push("name");
94
+ const rotates = olds.tokenVersion !== news.tokenVersion;
94
95
  return {
95
- changes: replaces.length > 0,
96
- replaces,
97
- deleteBeforeReplace: true
96
+ changes: identityReplaces.length > 0 || rotates,
97
+ replaces: rotates ? [...identityReplaces, "tokenVersion"] : identityReplaces,
98
+ deleteBeforeReplace: identityReplaces.length > 0
98
99
  };
99
100
  }
100
101
  };
@@ -141,7 +142,8 @@ var RailwayProjectToken = class extends pulumi.ComponentResource {
141
142
  token: provider.token,
142
143
  projectId: project.id,
143
144
  environmentId: environment.id,
144
- name: args.name
145
+ name: args.name,
146
+ tokenVersion: args.tokenVersion
145
147
  }, { parent: this });
146
148
  this.token = resource.value;
147
149
  this.registerOutputs({ token: this.token });
@@ -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\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"}
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\t/**\n\t * Rotation handle: bumping this number replaces the token on the next `up` —\n\t * the new token is minted BEFORE the old one is revoked (create-before-delete,\n\t * unlike identity changes), so there is never a tokenless window. Leave unset\n\t * until the first rotation is needed.\n\t */\n\ttokenVersion?: number;\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 identityReplaces: string[] = [];\n\n\t\tif (olds.projectId !== news.projectId) {\n\t\t\tidentityReplaces.push(\"projectId\");\n\t\t}\n\n\t\tif (olds.environmentId !== news.environmentId) {\n\t\t\tidentityReplaces.push(\"environmentId\");\n\t\t}\n\n\t\tif (olds.name !== news.name) {\n\t\t\tidentityReplaces.push(\"name\");\n\t\t}\n\n\t\tconst rotates = olds.tokenVersion !== news.tokenVersion;\n\n\t\treturn {\n\t\t\tchanges: identityReplaces.length > 0 || rotates,\n\t\t\treplaces: rotates\n\t\t\t\t? [...identityReplaces, \"tokenVersion\"]\n\t\t\t\t: identityReplaces,\n\t\t\t// Identity changes revoke the old token first (names must stay unique);\n\t\t\t// a pure rotation mints the new token BEFORE revoking the old so there\n\t\t\t// is never a tokenless window — `create()` already cleans up stale\n\t\t\t// same-name tokens, so the transient name collision is handled.\n\t\t\tdeleteBeforeReplace: identityReplaces.length > 0,\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\ttokenVersion?: pulumi.Input<number>;\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\t/**\n\t * Rotation handle: bump to mint a fresh token on the next `up` (the old one\n\t * is revoked only after the new one exists). Consumers of `token` cascade\n\t * automatically.\n\t */\n\ttokenVersion?: pulumi.Input<number>;\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\ttokenVersion: args.tokenVersion,\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":";;;;;AAsCA,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,mBAA6B,CAAC;EAEpC,IAAI,KAAK,cAAc,KAAK,WAC3B,iBAAiB,KAAK,WAAW;EAGlC,IAAI,KAAK,kBAAkB,KAAK,eAC/B,iBAAiB,KAAK,eAAe;EAGtC,IAAI,KAAK,SAAS,KAAK,MACtB,iBAAiB,KAAK,MAAM;EAG7B,MAAM,UAAU,KAAK,iBAAiB,KAAK;EAE3C,OAAO;GACN,SAAS,iBAAiB,SAAS,KAAK;GACxC,UAAU,UACP,CAAC,GAAG,kBAAkB,cAAc,IACpC;GAKH,qBAAqB,iBAAiB,SAAS;EAChD;CACD;AACD;;AAGA,IAAM,8BAAN,cAA0C,OAAO,QAAQ,SAAS;CAIjE,YACC,MACA,MAOA,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;;;;;;;;;;;;;;;;;;;;;;;AAwDA,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;GACX,cAAc,KAAK;EACpB,GACA,EAAE,QAAQ,KAAK,CAChB;EAEA,KAAK,QAAQ,SAAS;EAEtB,KAAK,gBAAgB,EAAE,OAAO,KAAK,MAAM,CAAC;CAC3C;AACD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@infracraft/pulumi",
3
- "version": "1.23.0",
3
+ "version": "1.25.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Native Pulumi providers for Railway, Neon, Vercel, and Fly.io with adopt-or-create semantics and deploy orchestration. No Terraform bridge.",