@infracraft/pulumi 1.19.0 → 1.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/railway/domain.cjs +43 -5
- package/dist/railway/domain.cjs.map +1 -1
- package/dist/railway/domain.d.cts +22 -0
- package/dist/railway/domain.d.cts.map +1 -1
- package/dist/railway/domain.d.mts +22 -0
- package/dist/railway/domain.d.mts.map +1 -1
- package/dist/railway/domain.mjs +43 -5
- package/dist/railway/domain.mjs.map +1 -1
- package/dist/vercel/domain.cjs +39 -11
- package/dist/vercel/domain.cjs.map +1 -1
- package/dist/vercel/domain.d.cts +14 -8
- package/dist/vercel/domain.d.cts.map +1 -1
- package/dist/vercel/domain.d.mts +14 -8
- package/dist/vercel/domain.d.mts.map +1 -1
- package/dist/vercel/domain.mjs +40 -11
- package/dist/vercel/domain.mjs.map +1 -1
- package/dist/vercel/index.cjs +0 -1
- package/dist/vercel/index.d.cts +2 -2
- package/dist/vercel/index.d.mts +2 -2
- package/dist/vercel/index.mjs +2 -2
- package/package.json +1 -1
package/dist/railway/domain.cjs
CHANGED
|
@@ -12,9 +12,32 @@ _pulumi_pulumi = require_chunk.__toESM(_pulumi_pulumi, 1);
|
|
|
12
12
|
function extractCnameTarget(dnsRecords) {
|
|
13
13
|
return dnsRecords?.find((record) => record.recordType === "DNS_RECORD_TYPE_CNAME" && record.purpose === "DNS_RECORD_PURPOSE_TRAFFIC_ROUTE")?.requiredValue;
|
|
14
14
|
}
|
|
15
|
+
const RAILWAY_VERIFY_PREFIX = "railway-verify=";
|
|
16
|
+
/**
|
|
17
|
+
* Composes the ownership-verification TXT record value from Railway's token.
|
|
18
|
+
* Idempotent: Railway currently returns the token already prefixed, but this
|
|
19
|
+
* guards against a future API response returning the bare token instead.
|
|
20
|
+
*/
|
|
21
|
+
function composeVerificationTxtValue(token) {
|
|
22
|
+
return token.startsWith(RAILWAY_VERIFY_PREFIX) ? token : `${RAILWAY_VERIFY_PREFIX}${token}`;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Extracts the ready-to-use ownership-verification TXT record (name + composed
|
|
26
|
+
* value) from a custom domain's status, or `undefined` if Railway hasn't assigned
|
|
27
|
+
* one (service domains, or custom domains needing no verification).
|
|
28
|
+
*/
|
|
29
|
+
function extractVerificationTxt(status) {
|
|
30
|
+
if (!status?.verificationDnsHost || !status.verificationToken) return;
|
|
31
|
+
return {
|
|
32
|
+
name: status.verificationDnsHost,
|
|
33
|
+
value: composeVerificationTxtValue(status.verificationToken)
|
|
34
|
+
};
|
|
35
|
+
}
|
|
15
36
|
const DOMAIN_STATUS_FIELDS = `
|
|
16
37
|
status {
|
|
17
38
|
dnsRecords { recordType purpose requiredValue }
|
|
39
|
+
verificationDnsHost
|
|
40
|
+
verificationToken
|
|
18
41
|
}
|
|
19
42
|
`;
|
|
20
43
|
const SERVICE_DOMAINS_QUERY = `
|
|
@@ -71,13 +94,16 @@ var RailwayDomainResourceProvider = class {
|
|
|
71
94
|
const found = existing.customDomains.find((d) => d.domain === inputs.customDomain);
|
|
72
95
|
if (found) {
|
|
73
96
|
_pulumi_pulumi.log.info(`Adopting existing custom domain "${found.domain}"`);
|
|
97
|
+
const verificationTxt = extractVerificationTxt(found.status);
|
|
74
98
|
return {
|
|
75
99
|
id: found.domain,
|
|
76
100
|
outs: {
|
|
77
101
|
...inputs,
|
|
78
102
|
domainId: found.id,
|
|
79
103
|
fqdn: found.domain,
|
|
80
|
-
cnameTarget: extractCnameTarget(found.status?.dnsRecords)
|
|
104
|
+
cnameTarget: extractCnameTarget(found.status?.dnsRecords),
|
|
105
|
+
verificationTxtName: verificationTxt?.name,
|
|
106
|
+
verificationTxtValue: verificationTxt?.value
|
|
81
107
|
}
|
|
82
108
|
};
|
|
83
109
|
}
|
|
@@ -87,13 +113,16 @@ var RailwayDomainResourceProvider = class {
|
|
|
87
113
|
environmentId: inputs.environmentId,
|
|
88
114
|
domain: inputs.customDomain
|
|
89
115
|
} });
|
|
116
|
+
const createdVerificationTxt = extractVerificationTxt(result.customDomainCreate.status);
|
|
90
117
|
return {
|
|
91
118
|
id: result.customDomainCreate.domain,
|
|
92
119
|
outs: {
|
|
93
120
|
...inputs,
|
|
94
121
|
domainId: result.customDomainCreate.id,
|
|
95
122
|
fqdn: result.customDomainCreate.domain,
|
|
96
|
-
cnameTarget: extractCnameTarget(result.customDomainCreate.status?.dnsRecords)
|
|
123
|
+
cnameTarget: extractCnameTarget(result.customDomainCreate.status?.dnsRecords),
|
|
124
|
+
verificationTxtName: createdVerificationTxt?.name,
|
|
125
|
+
verificationTxtValue: createdVerificationTxt?.value
|
|
97
126
|
}
|
|
98
127
|
};
|
|
99
128
|
}
|
|
@@ -127,13 +156,16 @@ var RailwayDomainResourceProvider = class {
|
|
|
127
156
|
if (props.customDomain) {
|
|
128
157
|
const found = existing.customDomains.find((d) => d.domain === props.customDomain);
|
|
129
158
|
if (!found) throw new Error(`Custom domain "${props.customDomain}" not found during refresh`);
|
|
159
|
+
const refreshedVerificationTxt = extractVerificationTxt(found.status);
|
|
130
160
|
return {
|
|
131
161
|
id: found.domain,
|
|
132
162
|
props: {
|
|
133
163
|
...props,
|
|
134
164
|
domainId: found.id,
|
|
135
165
|
fqdn: found.domain,
|
|
136
|
-
cnameTarget: extractCnameTarget(found.status?.dnsRecords)
|
|
166
|
+
cnameTarget: extractCnameTarget(found.status?.dnsRecords),
|
|
167
|
+
verificationTxtName: refreshedVerificationTxt?.name,
|
|
168
|
+
verificationTxtValue: refreshedVerificationTxt?.value
|
|
137
169
|
}
|
|
138
170
|
};
|
|
139
171
|
}
|
|
@@ -178,7 +210,9 @@ var RailwayDomainResource = class extends _pulumi_pulumi.dynamic.Resource {
|
|
|
178
210
|
...args,
|
|
179
211
|
domainId: void 0,
|
|
180
212
|
fqdn: void 0,
|
|
181
|
-
cnameTarget: void 0
|
|
213
|
+
cnameTarget: void 0,
|
|
214
|
+
verificationTxtName: void 0,
|
|
215
|
+
verificationTxtValue: void 0
|
|
182
216
|
}, opts);
|
|
183
217
|
}
|
|
184
218
|
};
|
|
@@ -215,9 +249,13 @@ var RailwayDomain = class extends _pulumi_pulumi.ComponentResource {
|
|
|
215
249
|
}, { parent: this });
|
|
216
250
|
this.fqdn = resource.fqdn;
|
|
217
251
|
this.cnameTarget = resource.cnameTarget;
|
|
252
|
+
this.verificationTxtName = resource.verificationTxtName;
|
|
253
|
+
this.verificationTxtValue = resource.verificationTxtValue;
|
|
218
254
|
this.registerOutputs({
|
|
219
255
|
fqdn: this.fqdn,
|
|
220
|
-
cnameTarget: this.cnameTarget
|
|
256
|
+
cnameTarget: this.cnameTarget,
|
|
257
|
+
verificationTxtName: this.verificationTxtName,
|
|
258
|
+
verificationTxtValue: this.verificationTxtValue
|
|
221
259
|
});
|
|
222
260
|
}
|
|
223
261
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"domain.cjs","names":["RailwayClient","pulumi"],"sources":["../../src/railway/domain.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\";\nimport type { RailwayService } from \"./service\";\n\n/** Resolved inputs for the Railway domain dynamic provider. */\ninterface RailwayDomainInputs {\n\t/** Railway API bearer token. */\n\ttoken: string;\n\n\t/** Railway project UUID. */\n\tprojectId: string;\n\n\t/** Railway service UUID to attach the domain to. */\n\tserviceId: string;\n\n\t/** Railway environment UUID (e.g. production). */\n\tenvironmentId: string;\n\n\t/** Custom domain FQDN (e.g. `\"api.example.com\"`). Omit for auto-generated Railway domain. */\n\tcustomDomain?: string;\n}\n\n/** Persisted state for the Railway domain, extending inputs with Railway-assigned identifiers. */\ninterface RailwayDomainOutputs extends RailwayDomainInputs {\n\t/** Railway-assigned domain UUID (used for deletion API calls). */\n\tdomainId: string;\n\n\t/** Fully qualified domain name (e.g. `\"api-production-abc.up.railway.app\"`). */\n\tfqdn: string;\n\n\t/**\n\t * The value to CNAME `customDomain` to. Only present for custom domains — a\n\t * service's auto-generated `*.up.railway.app` domain needs no DNS record of its\n\t * own. `undefined` if Railway hasn't returned a traffic-routing CNAME record yet.\n\t */\n\tcnameTarget?: string;\n}\n\n/** A single DNS record Railway expects for a custom domain (CNAME target, ACME challenge, ...). */\ninterface DomainDnsRecord {\n\trecordType: string;\n\tpurpose: string;\n\trequiredValue: string;\n}\n\n/** Shape returned for a custom domain, including its required DNS records. */\ninterface CustomDomainEntry {\n\tid: string;\n\tdomain: string;\n\tstatus?: { dnsRecords: DomainDnsRecord[] };\n}\n\n/**\n * Picks the CNAME record Railway expects for routing traffic to a custom domain out\n * of its full DNS record list (which also includes e.g. an ACME challenge TXT record).\n */\nfunction extractCnameTarget(\n\tdnsRecords: DomainDnsRecord[] | undefined,\n): string | undefined {\n\treturn dnsRecords?.find(\n\t\t(record) =>\n\t\t\trecord.recordType === \"DNS_RECORD_TYPE_CNAME\" &&\n\t\t\trecord.purpose === \"DNS_RECORD_PURPOSE_TRAFFIC_ROUTE\",\n\t)?.requiredValue;\n}\n\nconst DOMAIN_STATUS_FIELDS = `\n status {\n dnsRecords { recordType purpose requiredValue }\n }\n`;\n\nconst SERVICE_DOMAINS_QUERY = `\n query($projectId: String!, $serviceId: String!, $environmentId: String!) {\n domains(\n projectId: $projectId\n serviceId: $serviceId\n environmentId: $environmentId\n ) {\n serviceDomains { id domain }\n customDomains { id domain ${DOMAIN_STATUS_FIELDS} }\n }\n }\n`;\n\nconst SERVICE_DOMAIN_CREATE = `\n mutation($input: ServiceDomainCreateInput!) {\n serviceDomainCreate(input: $input) { id domain }\n }\n`;\n\nconst CUSTOM_DOMAIN_CREATE = `\n mutation($input: CustomDomainCreateInput!) {\n customDomainCreate(input: $input) { id domain ${DOMAIN_STATUS_FIELDS} }\n }\n`;\n\nconst CUSTOM_DOMAIN_DELETE = `\n mutation($id: String!) { customDomainDelete(id: $id) }\n`;\n\nconst SERVICE_DOMAIN_DELETE = `\n mutation($id: String!) { serviceDomainDelete(id: $id) }\n`;\n\n/**\n * Queries all existing domains (service and custom) for a Railway service.\n */\nasync function findExistingDomains(\n\tclient: RailwayClient,\n\tprojectId: string,\n\tserviceId: string,\n\tenvironmentId: string,\n): Promise<{\n\tserviceDomains: Array<{ id: string; domain: string }>;\n\tcustomDomains: CustomDomainEntry[];\n}> {\n\tconst result = await client.query<{\n\t\tdomains: {\n\t\t\tserviceDomains: Array<{ id: string; domain: string }>;\n\t\t\tcustomDomains: CustomDomainEntry[];\n\t\t};\n\t}>(SERVICE_DOMAINS_QUERY, { projectId, serviceId, environmentId });\n\n\treturn result.domains;\n}\n\n/**\n * Dynamic provider implementing CRUD for Railway domains.\n *\n * Uses adopt-or-create: queries existing domains before creating new ones.\n * Uses the FQDN as the Pulumi resource ID.\n *\n * @internal Exported only for unit testing; not part of the public API surface.\n */\nexport class RailwayDomainResourceProvider\n\timplements pulumi.dynamic.ResourceProvider\n{\n\tasync create(\n\t\tinputs: RailwayDomainInputs,\n\t): Promise<pulumi.dynamic.CreateResult> {\n\t\tconst client = new RailwayClient(inputs.token);\n\n\t\tconst existing = await findExistingDomains(\n\t\t\tclient,\n\t\t\tinputs.projectId,\n\t\t\tinputs.serviceId,\n\t\t\tinputs.environmentId,\n\t\t);\n\n\t\tif (inputs.customDomain) {\n\t\t\tconst found = existing.customDomains.find(\n\t\t\t\t(d) => d.domain === inputs.customDomain,\n\t\t\t);\n\n\t\t\tif (found) {\n\t\t\t\tpulumi.log.info(`Adopting existing custom domain \"${found.domain}\"`);\n\n\t\t\t\treturn {\n\t\t\t\t\tid: found.domain,\n\t\t\t\t\touts: {\n\t\t\t\t\t\t...inputs,\n\t\t\t\t\t\tdomainId: found.id,\n\t\t\t\t\t\tfqdn: found.domain,\n\t\t\t\t\t\tcnameTarget: extractCnameTarget(found.status?.dnsRecords),\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tconst result = await client.query<{\n\t\t\t\tcustomDomainCreate: CustomDomainEntry;\n\t\t\t}>(CUSTOM_DOMAIN_CREATE, {\n\t\t\t\tinput: {\n\t\t\t\t\tprojectId: inputs.projectId,\n\t\t\t\t\tserviceId: inputs.serviceId,\n\t\t\t\t\tenvironmentId: inputs.environmentId,\n\t\t\t\t\tdomain: inputs.customDomain,\n\t\t\t\t},\n\t\t\t});\n\n\t\t\treturn {\n\t\t\t\tid: result.customDomainCreate.domain,\n\t\t\t\touts: {\n\t\t\t\t\t...inputs,\n\t\t\t\t\tdomainId: result.customDomainCreate.id,\n\t\t\t\t\tfqdn: result.customDomainCreate.domain,\n\t\t\t\t\tcnameTarget: extractCnameTarget(\n\t\t\t\t\t\tresult.customDomainCreate.status?.dnsRecords,\n\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tif (existing.serviceDomains.length > 0) {\n\t\t\tconst found = existing.serviceDomains[0];\n\n\t\t\tpulumi.log.info(`Adopting existing service domain \"${found.domain}\"`);\n\n\t\t\treturn {\n\t\t\t\tid: found.domain,\n\t\t\t\touts: { ...inputs, domainId: found.id, fqdn: found.domain },\n\t\t\t};\n\t\t}\n\n\t\tconst result = await client.query<{\n\t\t\tserviceDomainCreate: { id: string; domain: string };\n\t\t}>(SERVICE_DOMAIN_CREATE, {\n\t\t\tinput: {\n\t\t\t\tserviceId: inputs.serviceId,\n\t\t\t\tenvironmentId: inputs.environmentId,\n\t\t\t},\n\t\t});\n\n\t\treturn {\n\t\t\tid: result.serviceDomainCreate.domain,\n\t\t\touts: {\n\t\t\t\t...inputs,\n\t\t\t\tdomainId: result.serviceDomainCreate.id,\n\t\t\t\tfqdn: result.serviceDomainCreate.domain,\n\t\t\t},\n\t\t};\n\t}\n\n\tasync read(\n\t\t_id: string,\n\t\tprops: RailwayDomainOutputs,\n\t): Promise<pulumi.dynamic.ReadResult> {\n\t\tconst client = new RailwayClient(props.token);\n\n\t\tconst existing = await findExistingDomains(\n\t\t\tclient,\n\t\t\tprops.projectId,\n\t\t\tprops.serviceId,\n\t\t\tprops.environmentId,\n\t\t);\n\n\t\tif (props.customDomain) {\n\t\t\tconst found = existing.customDomains.find(\n\t\t\t\t(d) => d.domain === props.customDomain,\n\t\t\t);\n\n\t\t\tif (!found) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Custom domain \"${props.customDomain}\" not found during refresh`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tid: found.domain,\n\t\t\t\tprops: {\n\t\t\t\t\t...props,\n\t\t\t\t\tdomainId: found.id,\n\t\t\t\t\tfqdn: found.domain,\n\t\t\t\t\tcnameTarget: extractCnameTarget(found.status?.dnsRecords),\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tif (existing.serviceDomains.length > 0) {\n\t\t\tconst found = existing.serviceDomains[0];\n\n\t\t\treturn {\n\t\t\t\tid: found.domain,\n\t\t\t\tprops: { ...props, domainId: found.id, fqdn: found.domain },\n\t\t\t};\n\t\t}\n\n\t\tthrow new Error(\"Railway domain not found during refresh\");\n\t}\n\n\tasync delete(_id: string, props: RailwayDomainOutputs): Promise<void> {\n\t\tconst client = new RailwayClient(props.token);\n\n\t\tconst mutation = props.customDomain\n\t\t\t? CUSTOM_DOMAIN_DELETE\n\t\t\t: SERVICE_DOMAIN_DELETE;\n\n\t\ttry {\n\t\t\tawait client.query(mutation, { id: props.domainId });\n\t\t} catch {\n\t\t\tpulumi.log.warn(\n\t\t\t\t\"Failed to delete Railway domain (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: RailwayDomainOutputs,\n\t\tnews: RailwayDomainInputs,\n\t): Promise<pulumi.dynamic.DiffResult> {\n\t\tconst replaces: string[] = [];\n\n\t\tif (olds.serviceId !== news.serviceId) {\n\t\t\treplaces.push(\"serviceId\");\n\t\t}\n\n\t\tif (olds.customDomain !== news.customDomain) {\n\t\t\treplaces.push(\"customDomain\");\n\t\t}\n\n\t\tif (olds.environmentId !== news.environmentId) {\n\t\t\treplaces.push(\"environmentId\");\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 RailwayDomainResource extends pulumi.dynamic.Resource {\n\tpublic declare readonly fqdn: pulumi.Output<string>;\n\tpublic declare readonly cnameTarget: pulumi.Output<string | undefined>;\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\tserviceId: pulumi.Input<string>;\n\t\t\tenvironmentId: pulumi.Input<string>;\n\t\t\tcustomDomain?: pulumi.Input<string>;\n\t\t},\n\t\topts?: pulumi.CustomResourceOptions,\n\t) {\n\t\tsuper(\n\t\t\tnew RailwayDomainResourceProvider(),\n\t\t\tname,\n\t\t\t{ ...args, domainId: undefined, fqdn: undefined, cnameTarget: undefined },\n\t\t\topts,\n\t\t);\n\t}\n}\n\n/** Options type for RailwayDomain — replaces Pulumi's native `provider` field. */\ntype RailwayDomainOptions = Omit<\n\tpulumi.ComponentResourceOptions,\n\t\"provider\"\n> & {\n\t/** Railway authentication context. */\n\tprovider: RailwayProvider;\n\n\t/** Railway project context. */\n\tproject: RailwayProject;\n\n\t/** Railway environment context. */\n\tenvironment: RailwayEnvironment;\n\n\t/** Railway service context. */\n\tservice: RailwayService;\n};\n\n/** Args for RailwayDomain. */\nexport interface RailwayDomainArgs {\n\t/** Custom domain FQDN. Omit to create an auto-generated Railway service domain. */\n\tcustomDomain?: pulumi.Input<string>;\n}\n\n/**\n * Manages a Railway domain (service or custom) with adopt-or-create semantics.\n *\n * A service can carry more than one custom domain — declare one `RailwayDomain` per\n * domain; each instance adopts, creates, and deletes only its own domain.\n *\n * @example\n * ```typescript\n * const apiDomain = new RailwayDomain(\"api-domain\", { customDomain: \"api.example.com\" }, {\n * provider, project, environment, service,\n * });\n * const wwwDomain = new RailwayDomain(\"www-domain\", { customDomain: \"www.example.com\" }, {\n * provider, project, environment, service,\n * });\n *\n * // Point each domain's DNS CNAME at its own target.\n * const apiCnameTarget = apiDomain.cnameTarget;\n * const wwwCnameTarget = wwwDomain.cnameTarget;\n * ```\n */\nexport class RailwayDomain extends pulumi.ComponentResource {\n\t/** Fully qualified domain name. */\n\tpublic readonly fqdn: pulumi.Output<string>;\n\n\t/**\n\t * The value to CNAME `customDomain` to. Only set for custom domains — `undefined`\n\t * for a service's auto-generated `*.up.railway.app` domain, or if Railway hasn't\n\t * returned a traffic-routing CNAME record for it yet.\n\t */\n\tpublic readonly cnameTarget: pulumi.Output<string | undefined>;\n\n\tconstructor(\n\t\tname: string,\n\t\targs: RailwayDomainArgs,\n\t\topts: RailwayDomainOptions,\n\t) {\n\t\tconst { provider, project, environment, service, ...pulumiOpts } = opts;\n\n\t\tsuper(\"infracraft:railway:Domain\", name, {}, pulumiOpts);\n\n\t\tconst resource = new RailwayDomainResource(\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\tserviceId: service.id,\n\t\t\t\tenvironmentId: environment.id,\n\t\t\t\tcustomDomain: args.customDomain,\n\t\t\t},\n\t\t\t{ parent: this },\n\t\t);\n\n\t\tthis.fqdn = resource.fqdn;\n\t\tthis.cnameTarget = resource.cnameTarget;\n\n\t\tthis.registerOutputs({ fqdn: this.fqdn, cnameTarget: this.cnameTarget });\n\t}\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"domain.cjs","names":["RailwayClient","pulumi"],"sources":["../../src/railway/domain.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\";\nimport type { RailwayService } from \"./service\";\n\n/** Resolved inputs for the Railway domain dynamic provider. */\ninterface RailwayDomainInputs {\n\t/** Railway API bearer token. */\n\ttoken: string;\n\n\t/** Railway project UUID. */\n\tprojectId: string;\n\n\t/** Railway service UUID to attach the domain to. */\n\tserviceId: string;\n\n\t/** Railway environment UUID (e.g. production). */\n\tenvironmentId: string;\n\n\t/** Custom domain FQDN (e.g. `\"api.example.com\"`). Omit for auto-generated Railway domain. */\n\tcustomDomain?: string;\n}\n\n/** Persisted state for the Railway domain, extending inputs with Railway-assigned identifiers. */\ninterface RailwayDomainOutputs extends RailwayDomainInputs {\n\t/** Railway-assigned domain UUID (used for deletion API calls). */\n\tdomainId: string;\n\n\t/** Fully qualified domain name (e.g. `\"api-production-abc.up.railway.app\"`). */\n\tfqdn: string;\n\n\t/**\n\t * The value to CNAME `customDomain` to. Only present for custom domains — a\n\t * service's auto-generated `*.up.railway.app` domain needs no DNS record of its\n\t * own. `undefined` if Railway hasn't returned a traffic-routing CNAME record yet.\n\t */\n\tcnameTarget?: string;\n\n\t/**\n\t * DNS record name for the ownership-verification TXT record (e.g.\n\t * `\"_railway-verify.staging.api\"`). `undefined` for service domains, and for\n\t * custom domains Railway doesn't require ownership verification for.\n\t */\n\tverificationTxtName?: string;\n\n\t/**\n\t * DNS record value for the ownership-verification TXT record, ready to write\n\t * as-is (the `railway-verify=` prefix is already composed — see\n\t * {@link composeVerificationTxtValue}). `undefined` under the same conditions as\n\t * {@link verificationTxtName}.\n\t */\n\tverificationTxtValue?: string;\n}\n\n/** A single DNS record Railway expects for a custom domain (CNAME target, ACME challenge, ...). */\ninterface DomainDnsRecord {\n\trecordType: string;\n\tpurpose: string;\n\trequiredValue: string;\n}\n\n/** Domain ownership-verification status, distinct from `dnsRecords` (see live API notes below). */\ninterface CustomDomainStatus {\n\tdnsRecords: DomainDnsRecord[];\n\t/**\n\t * DNS host for the ownership-verification TXT record. Verified live against\n\t * Railway's current API: this sits on `CustomDomain.status`, as a sibling of\n\t * `dnsRecords` rather than inside it — the two are populated independently\n\t * (`dnsRecords` can be non-empty with `verificationDnsHost` still null, and\n\t * vice versa), so both must be queried explicitly.\n\t */\n\tverificationDnsHost?: string | null;\n\t/**\n\t * Raw ownership-verification token. Verified live: Railway already returns this\n\t * pre-composed with the `railway-verify=` prefix (not a bare token) — see\n\t * {@link composeVerificationTxtValue}, which stays idempotent in case that ever\n\t * changes.\n\t */\n\tverificationToken?: string | null;\n}\n\n/** Shape returned for a custom domain, including its required DNS records. */\ninterface CustomDomainEntry {\n\tid: string;\n\tdomain: string;\n\tstatus?: CustomDomainStatus;\n}\n\n/**\n * Picks the CNAME record Railway expects for routing traffic to a custom domain out\n * of its full DNS record list (which also includes e.g. an ACME challenge TXT record).\n */\nfunction extractCnameTarget(\n\tdnsRecords: DomainDnsRecord[] | undefined,\n): string | undefined {\n\treturn dnsRecords?.find(\n\t\t(record) =>\n\t\t\trecord.recordType === \"DNS_RECORD_TYPE_CNAME\" &&\n\t\t\trecord.purpose === \"DNS_RECORD_PURPOSE_TRAFFIC_ROUTE\",\n\t)?.requiredValue;\n}\n\nconst RAILWAY_VERIFY_PREFIX = \"railway-verify=\";\n\n/**\n * Composes the ownership-verification TXT record value from Railway's token.\n * Idempotent: Railway currently returns the token already prefixed, but this\n * guards against a future API response returning the bare token instead.\n */\nfunction composeVerificationTxtValue(token: string): string {\n\treturn token.startsWith(RAILWAY_VERIFY_PREFIX)\n\t\t? token\n\t\t: `${RAILWAY_VERIFY_PREFIX}${token}`;\n}\n\n/**\n * Extracts the ready-to-use ownership-verification TXT record (name + composed\n * value) from a custom domain's status, or `undefined` if Railway hasn't assigned\n * one (service domains, or custom domains needing no verification).\n */\nfunction extractVerificationTxt(\n\tstatus: CustomDomainStatus | undefined,\n): { name: string; value: string } | undefined {\n\tif (!status?.verificationDnsHost || !status.verificationToken) {\n\t\treturn undefined;\n\t}\n\n\treturn {\n\t\tname: status.verificationDnsHost,\n\t\tvalue: composeVerificationTxtValue(status.verificationToken),\n\t};\n}\n\nconst DOMAIN_STATUS_FIELDS = `\n status {\n dnsRecords { recordType purpose requiredValue }\n verificationDnsHost\n verificationToken\n }\n`;\n\nconst SERVICE_DOMAINS_QUERY = `\n query($projectId: String!, $serviceId: String!, $environmentId: String!) {\n domains(\n projectId: $projectId\n serviceId: $serviceId\n environmentId: $environmentId\n ) {\n serviceDomains { id domain }\n customDomains { id domain ${DOMAIN_STATUS_FIELDS} }\n }\n }\n`;\n\nconst SERVICE_DOMAIN_CREATE = `\n mutation($input: ServiceDomainCreateInput!) {\n serviceDomainCreate(input: $input) { id domain }\n }\n`;\n\nconst CUSTOM_DOMAIN_CREATE = `\n mutation($input: CustomDomainCreateInput!) {\n customDomainCreate(input: $input) { id domain ${DOMAIN_STATUS_FIELDS} }\n }\n`;\n\nconst CUSTOM_DOMAIN_DELETE = `\n mutation($id: String!) { customDomainDelete(id: $id) }\n`;\n\nconst SERVICE_DOMAIN_DELETE = `\n mutation($id: String!) { serviceDomainDelete(id: $id) }\n`;\n\n/**\n * Queries all existing domains (service and custom) for a Railway service.\n */\nasync function findExistingDomains(\n\tclient: RailwayClient,\n\tprojectId: string,\n\tserviceId: string,\n\tenvironmentId: string,\n): Promise<{\n\tserviceDomains: Array<{ id: string; domain: string }>;\n\tcustomDomains: CustomDomainEntry[];\n}> {\n\tconst result = await client.query<{\n\t\tdomains: {\n\t\t\tserviceDomains: Array<{ id: string; domain: string }>;\n\t\t\tcustomDomains: CustomDomainEntry[];\n\t\t};\n\t}>(SERVICE_DOMAINS_QUERY, { projectId, serviceId, environmentId });\n\n\treturn result.domains;\n}\n\n/**\n * Dynamic provider implementing CRUD for Railway domains.\n *\n * Uses adopt-or-create: queries existing domains before creating new ones.\n * Uses the FQDN as the Pulumi resource ID.\n *\n * @internal Exported only for unit testing; not part of the public API surface.\n */\nexport class RailwayDomainResourceProvider\n\timplements pulumi.dynamic.ResourceProvider\n{\n\tasync create(\n\t\tinputs: RailwayDomainInputs,\n\t): Promise<pulumi.dynamic.CreateResult> {\n\t\tconst client = new RailwayClient(inputs.token);\n\n\t\tconst existing = await findExistingDomains(\n\t\t\tclient,\n\t\t\tinputs.projectId,\n\t\t\tinputs.serviceId,\n\t\t\tinputs.environmentId,\n\t\t);\n\n\t\tif (inputs.customDomain) {\n\t\t\tconst found = existing.customDomains.find(\n\t\t\t\t(d) => d.domain === inputs.customDomain,\n\t\t\t);\n\n\t\t\tif (found) {\n\t\t\t\tpulumi.log.info(`Adopting existing custom domain \"${found.domain}\"`);\n\n\t\t\t\tconst verificationTxt = extractVerificationTxt(found.status);\n\n\t\t\t\treturn {\n\t\t\t\t\tid: found.domain,\n\t\t\t\t\touts: {\n\t\t\t\t\t\t...inputs,\n\t\t\t\t\t\tdomainId: found.id,\n\t\t\t\t\t\tfqdn: found.domain,\n\t\t\t\t\t\tcnameTarget: extractCnameTarget(found.status?.dnsRecords),\n\t\t\t\t\t\tverificationTxtName: verificationTxt?.name,\n\t\t\t\t\t\tverificationTxtValue: verificationTxt?.value,\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tconst result = await client.query<{\n\t\t\t\tcustomDomainCreate: CustomDomainEntry;\n\t\t\t}>(CUSTOM_DOMAIN_CREATE, {\n\t\t\t\tinput: {\n\t\t\t\t\tprojectId: inputs.projectId,\n\t\t\t\t\tserviceId: inputs.serviceId,\n\t\t\t\t\tenvironmentId: inputs.environmentId,\n\t\t\t\t\tdomain: inputs.customDomain,\n\t\t\t\t},\n\t\t\t});\n\n\t\t\tconst createdVerificationTxt = extractVerificationTxt(\n\t\t\t\tresult.customDomainCreate.status,\n\t\t\t);\n\n\t\t\treturn {\n\t\t\t\tid: result.customDomainCreate.domain,\n\t\t\t\touts: {\n\t\t\t\t\t...inputs,\n\t\t\t\t\tdomainId: result.customDomainCreate.id,\n\t\t\t\t\tfqdn: result.customDomainCreate.domain,\n\t\t\t\t\tcnameTarget: extractCnameTarget(\n\t\t\t\t\t\tresult.customDomainCreate.status?.dnsRecords,\n\t\t\t\t\t),\n\t\t\t\t\tverificationTxtName: createdVerificationTxt?.name,\n\t\t\t\t\tverificationTxtValue: createdVerificationTxt?.value,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tif (existing.serviceDomains.length > 0) {\n\t\t\tconst found = existing.serviceDomains[0];\n\n\t\t\tpulumi.log.info(`Adopting existing service domain \"${found.domain}\"`);\n\n\t\t\treturn {\n\t\t\t\tid: found.domain,\n\t\t\t\touts: { ...inputs, domainId: found.id, fqdn: found.domain },\n\t\t\t};\n\t\t}\n\n\t\tconst result = await client.query<{\n\t\t\tserviceDomainCreate: { id: string; domain: string };\n\t\t}>(SERVICE_DOMAIN_CREATE, {\n\t\t\tinput: {\n\t\t\t\tserviceId: inputs.serviceId,\n\t\t\t\tenvironmentId: inputs.environmentId,\n\t\t\t},\n\t\t});\n\n\t\treturn {\n\t\t\tid: result.serviceDomainCreate.domain,\n\t\t\touts: {\n\t\t\t\t...inputs,\n\t\t\t\tdomainId: result.serviceDomainCreate.id,\n\t\t\t\tfqdn: result.serviceDomainCreate.domain,\n\t\t\t},\n\t\t};\n\t}\n\n\tasync read(\n\t\t_id: string,\n\t\tprops: RailwayDomainOutputs,\n\t): Promise<pulumi.dynamic.ReadResult> {\n\t\tconst client = new RailwayClient(props.token);\n\n\t\tconst existing = await findExistingDomains(\n\t\t\tclient,\n\t\t\tprops.projectId,\n\t\t\tprops.serviceId,\n\t\t\tprops.environmentId,\n\t\t);\n\n\t\tif (props.customDomain) {\n\t\t\tconst found = existing.customDomains.find(\n\t\t\t\t(d) => d.domain === props.customDomain,\n\t\t\t);\n\n\t\t\tif (!found) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Custom domain \"${props.customDomain}\" not found during refresh`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst refreshedVerificationTxt = extractVerificationTxt(found.status);\n\n\t\t\treturn {\n\t\t\t\tid: found.domain,\n\t\t\t\tprops: {\n\t\t\t\t\t...props,\n\t\t\t\t\tdomainId: found.id,\n\t\t\t\t\tfqdn: found.domain,\n\t\t\t\t\tcnameTarget: extractCnameTarget(found.status?.dnsRecords),\n\t\t\t\t\tverificationTxtName: refreshedVerificationTxt?.name,\n\t\t\t\t\tverificationTxtValue: refreshedVerificationTxt?.value,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tif (existing.serviceDomains.length > 0) {\n\t\t\tconst found = existing.serviceDomains[0];\n\n\t\t\treturn {\n\t\t\t\tid: found.domain,\n\t\t\t\tprops: { ...props, domainId: found.id, fqdn: found.domain },\n\t\t\t};\n\t\t}\n\n\t\tthrow new Error(\"Railway domain not found during refresh\");\n\t}\n\n\tasync delete(_id: string, props: RailwayDomainOutputs): Promise<void> {\n\t\tconst client = new RailwayClient(props.token);\n\n\t\tconst mutation = props.customDomain\n\t\t\t? CUSTOM_DOMAIN_DELETE\n\t\t\t: SERVICE_DOMAIN_DELETE;\n\n\t\ttry {\n\t\t\tawait client.query(mutation, { id: props.domainId });\n\t\t} catch {\n\t\t\tpulumi.log.warn(\n\t\t\t\t\"Failed to delete Railway domain (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: RailwayDomainOutputs,\n\t\tnews: RailwayDomainInputs,\n\t): Promise<pulumi.dynamic.DiffResult> {\n\t\tconst replaces: string[] = [];\n\n\t\tif (olds.serviceId !== news.serviceId) {\n\t\t\treplaces.push(\"serviceId\");\n\t\t}\n\n\t\tif (olds.customDomain !== news.customDomain) {\n\t\t\treplaces.push(\"customDomain\");\n\t\t}\n\n\t\tif (olds.environmentId !== news.environmentId) {\n\t\t\treplaces.push(\"environmentId\");\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 RailwayDomainResource extends pulumi.dynamic.Resource {\n\tpublic declare readonly fqdn: pulumi.Output<string>;\n\tpublic declare readonly cnameTarget: pulumi.Output<string | undefined>;\n\tpublic declare readonly verificationTxtName: pulumi.Output<\n\t\tstring | undefined\n\t>;\n\tpublic declare readonly verificationTxtValue: pulumi.Output<\n\t\tstring | undefined\n\t>;\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\tserviceId: pulumi.Input<string>;\n\t\t\tenvironmentId: pulumi.Input<string>;\n\t\t\tcustomDomain?: pulumi.Input<string>;\n\t\t},\n\t\topts?: pulumi.CustomResourceOptions,\n\t) {\n\t\tsuper(\n\t\t\tnew RailwayDomainResourceProvider(),\n\t\t\tname,\n\t\t\t{\n\t\t\t\t...args,\n\t\t\t\tdomainId: undefined,\n\t\t\t\tfqdn: undefined,\n\t\t\t\tcnameTarget: undefined,\n\t\t\t\tverificationTxtName: undefined,\n\t\t\t\tverificationTxtValue: undefined,\n\t\t\t},\n\t\t\topts,\n\t\t);\n\t}\n}\n\n/** Options type for RailwayDomain — replaces Pulumi's native `provider` field. */\ntype RailwayDomainOptions = Omit<\n\tpulumi.ComponentResourceOptions,\n\t\"provider\"\n> & {\n\t/** Railway authentication context. */\n\tprovider: RailwayProvider;\n\n\t/** Railway project context. */\n\tproject: RailwayProject;\n\n\t/** Railway environment context. */\n\tenvironment: RailwayEnvironment;\n\n\t/** Railway service context. */\n\tservice: RailwayService;\n};\n\n/** Args for RailwayDomain. */\nexport interface RailwayDomainArgs {\n\t/** Custom domain FQDN. Omit to create an auto-generated Railway service domain. */\n\tcustomDomain?: pulumi.Input<string>;\n}\n\n/**\n * Manages a Railway domain (service or custom) with adopt-or-create semantics.\n *\n * A service can carry more than one custom domain — declare one `RailwayDomain` per\n * domain; each instance adopts, creates, and deletes only its own domain.\n *\n * @example\n * ```typescript\n * const apiDomain = new RailwayDomain(\"api-domain\", { customDomain: \"api.example.com\" }, {\n * provider, project, environment, service,\n * });\n * const wwwDomain = new RailwayDomain(\"www-domain\", { customDomain: \"www.example.com\" }, {\n * provider, project, environment, service,\n * });\n *\n * // Point each domain's DNS CNAME at its own target.\n * const apiCnameTarget = apiDomain.cnameTarget;\n * const wwwCnameTarget = wwwDomain.cnameTarget;\n * ```\n */\nexport class RailwayDomain extends pulumi.ComponentResource {\n\t/** Fully qualified domain name. */\n\tpublic readonly fqdn: pulumi.Output<string>;\n\n\t/**\n\t * The value to CNAME `customDomain` to. Only set for custom domains — `undefined`\n\t * for a service's auto-generated `*.up.railway.app` domain, or if Railway hasn't\n\t * returned a traffic-routing CNAME record for it yet.\n\t */\n\tpublic readonly cnameTarget: pulumi.Output<string | undefined>;\n\n\t/**\n\t * DNS record name for the ownership-verification TXT record. `undefined` for\n\t * service domains, and for custom domains Railway doesn't require verification\n\t * for — write this record (alongside `cnameTarget`'s CNAME) to flip Railway's\n\t * domain status to verified and let its TLS certificate issue.\n\t */\n\tpublic readonly verificationTxtName: pulumi.Output<string | undefined>;\n\n\t/** DNS record value for the ownership-verification TXT record, ready to write as-is. */\n\tpublic readonly verificationTxtValue: pulumi.Output<string | undefined>;\n\n\tconstructor(\n\t\tname: string,\n\t\targs: RailwayDomainArgs,\n\t\topts: RailwayDomainOptions,\n\t) {\n\t\tconst { provider, project, environment, service, ...pulumiOpts } = opts;\n\n\t\tsuper(\"infracraft:railway:Domain\", name, {}, pulumiOpts);\n\n\t\tconst resource = new RailwayDomainResource(\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\tserviceId: service.id,\n\t\t\t\tenvironmentId: environment.id,\n\t\t\t\tcustomDomain: args.customDomain,\n\t\t\t},\n\t\t\t{ parent: this },\n\t\t);\n\n\t\tthis.fqdn = resource.fqdn;\n\t\tthis.cnameTarget = resource.cnameTarget;\n\t\tthis.verificationTxtName = resource.verificationTxtName;\n\t\tthis.verificationTxtValue = resource.verificationTxtValue;\n\n\t\tthis.registerOutputs({\n\t\t\tfqdn: this.fqdn,\n\t\t\tcnameTarget: this.cnameTarget,\n\t\t\tverificationTxtName: this.verificationTxtName,\n\t\t\tverificationTxtValue: this.verificationTxtValue,\n\t\t});\n\t}\n}\n"],"mappings":"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"}
|
|
@@ -31,6 +31,19 @@ interface RailwayDomainOutputs extends RailwayDomainInputs {
|
|
|
31
31
|
* own. `undefined` if Railway hasn't returned a traffic-routing CNAME record yet.
|
|
32
32
|
*/
|
|
33
33
|
cnameTarget?: string;
|
|
34
|
+
/**
|
|
35
|
+
* DNS record name for the ownership-verification TXT record (e.g.
|
|
36
|
+
* `"_railway-verify.staging.api"`). `undefined` for service domains, and for
|
|
37
|
+
* custom domains Railway doesn't require ownership verification for.
|
|
38
|
+
*/
|
|
39
|
+
verificationTxtName?: string;
|
|
40
|
+
/**
|
|
41
|
+
* DNS record value for the ownership-verification TXT record, ready to write
|
|
42
|
+
* as-is (the `railway-verify=` prefix is already composed — see
|
|
43
|
+
* {@link composeVerificationTxtValue}). `undefined` under the same conditions as
|
|
44
|
+
* {@link verificationTxtName}.
|
|
45
|
+
*/
|
|
46
|
+
verificationTxtValue?: string;
|
|
34
47
|
}
|
|
35
48
|
/**
|
|
36
49
|
* Dynamic provider implementing CRUD for Railway domains.
|
|
@@ -87,6 +100,15 @@ declare class RailwayDomain extends pulumi.ComponentResource {
|
|
|
87
100
|
* returned a traffic-routing CNAME record for it yet.
|
|
88
101
|
*/
|
|
89
102
|
readonly cnameTarget: pulumi.Output<string | undefined>;
|
|
103
|
+
/**
|
|
104
|
+
* DNS record name for the ownership-verification TXT record. `undefined` for
|
|
105
|
+
* service domains, and for custom domains Railway doesn't require verification
|
|
106
|
+
* for — write this record (alongside `cnameTarget`'s CNAME) to flip Railway's
|
|
107
|
+
* domain status to verified and let its TLS certificate issue.
|
|
108
|
+
*/
|
|
109
|
+
readonly verificationTxtName: pulumi.Output<string | undefined>;
|
|
110
|
+
/** DNS record value for the ownership-verification TXT record, ready to write as-is. */
|
|
111
|
+
readonly verificationTxtValue: pulumi.Output<string | undefined>;
|
|
90
112
|
constructor(name: string, args: RailwayDomainArgs, opts: RailwayDomainOptions);
|
|
91
113
|
}
|
|
92
114
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"domain.d.cts","names":[],"sources":["../../src/railway/domain.ts"],"mappings":";;;;;;;;;UAQU,mBAAA;;EAET,KAAA;EAF4B;EAK5B,SAAA;EAL4B;EAQ5B,SAAA;EAHA;EAMA,aAAA;EAAA;EAGA,YAAA;AAAA;AAAY;AAAA,UAIH,oBAAA,SAA6B,mBAAmB;EAA3B;EAE9B,QAAA;EAFyD;EAKzD,IAAA;EAHA;;;;
|
|
1
|
+
{"version":3,"file":"domain.d.cts","names":[],"sources":["../../src/railway/domain.ts"],"mappings":";;;;;;;;;UAQU,mBAAA;;EAET,KAAA;EAF4B;EAK5B,SAAA;EAL4B;EAQ5B,SAAA;EAHA;EAMA,aAAA;EAAA;EAGA,YAAA;AAAA;AAAY;AAAA,UAIH,oBAAA,SAA6B,mBAAmB;EAA3B;EAE9B,QAAA;EAFyD;EAKzD,IAAA;EAHA;;;;;EAUA,WAAA;EAeoB;AAyJrB;;;;EAjKC,mBAAA;EAsKG;;;;;;EA9JH,oBAAA;AAAA;;;;;;;;;cAyJY,6BAAA,YACD,MAAA,CAAO,OAAA,CAAQ,gBAAA;EAEpB,MAAA,CACL,MAAA,EAAQ,mBAAA,GACN,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,YAAA;EA6FpB,IAAA,CACL,GAAA,UACA,KAAA,EAAO,oBAAA,GACL,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,UAAA;EAgDpB,MAAA,CAAO,GAAA,UAAa,KAAA,EAAO,oBAAA,GAAuB,OAAA;EAgBlD,IAAA,CACL,GAAA,UACA,IAAA,EAAM,oBAAA,EACN,IAAA,EAAM,mBAAA,GACJ,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,UAAA;AAAA;;KA8DtB,oBAAA,GAAuB,IAAA,CAC3B,MAAA,CAAO,wBAAA;EArIN,sCAyID,QAAA,EAAU,eAAA,EAxIT;EA2ID,OAAA,EAAS,cAAA,EA1IE;EA6IX,WAAA,EAAa,kBAAA,EA7Ia;EAgJ1B,OAAA,EAAS,cAAA;AAAA;;UAIO,iBAAA;EApGwC;EAsGxD,YAAA,GAAe,MAAA,CAAO,KAAK;AAAA;;;;;;;;;;AAlFS;AAqBpC;;;;;;;;;;cAoFY,aAAA,SAAsB,MAAA,CAAO,iBAAA;EA3Cd;EAAA,SA6CX,IAAA,EAAM,MAAA,CAAO,MAAA;EA5CtB;;;;;EAAA,SAmDS,WAAA,EAAa,MAAA,CAAO,MAAA;EAzCvB;;;;AAGU;AAIxB;EAPc,SAiDG,mBAAA,EAAqB,MAAA,CAAO,MAAA;;WAG5B,oBAAA,EAAsB,MAAA,CAAO,MAAA;cAG5C,IAAA,UACA,IAAA,EAAM,iBAAA,EACN,IAAA,EAAM,oBAAA;AAAA"}
|
|
@@ -31,6 +31,19 @@ interface RailwayDomainOutputs extends RailwayDomainInputs {
|
|
|
31
31
|
* own. `undefined` if Railway hasn't returned a traffic-routing CNAME record yet.
|
|
32
32
|
*/
|
|
33
33
|
cnameTarget?: string;
|
|
34
|
+
/**
|
|
35
|
+
* DNS record name for the ownership-verification TXT record (e.g.
|
|
36
|
+
* `"_railway-verify.staging.api"`). `undefined` for service domains, and for
|
|
37
|
+
* custom domains Railway doesn't require ownership verification for.
|
|
38
|
+
*/
|
|
39
|
+
verificationTxtName?: string;
|
|
40
|
+
/**
|
|
41
|
+
* DNS record value for the ownership-verification TXT record, ready to write
|
|
42
|
+
* as-is (the `railway-verify=` prefix is already composed — see
|
|
43
|
+
* {@link composeVerificationTxtValue}). `undefined` under the same conditions as
|
|
44
|
+
* {@link verificationTxtName}.
|
|
45
|
+
*/
|
|
46
|
+
verificationTxtValue?: string;
|
|
34
47
|
}
|
|
35
48
|
/**
|
|
36
49
|
* Dynamic provider implementing CRUD for Railway domains.
|
|
@@ -87,6 +100,15 @@ declare class RailwayDomain extends pulumi.ComponentResource {
|
|
|
87
100
|
* returned a traffic-routing CNAME record for it yet.
|
|
88
101
|
*/
|
|
89
102
|
readonly cnameTarget: pulumi.Output<string | undefined>;
|
|
103
|
+
/**
|
|
104
|
+
* DNS record name for the ownership-verification TXT record. `undefined` for
|
|
105
|
+
* service domains, and for custom domains Railway doesn't require verification
|
|
106
|
+
* for — write this record (alongside `cnameTarget`'s CNAME) to flip Railway's
|
|
107
|
+
* domain status to verified and let its TLS certificate issue.
|
|
108
|
+
*/
|
|
109
|
+
readonly verificationTxtName: pulumi.Output<string | undefined>;
|
|
110
|
+
/** DNS record value for the ownership-verification TXT record, ready to write as-is. */
|
|
111
|
+
readonly verificationTxtValue: pulumi.Output<string | undefined>;
|
|
90
112
|
constructor(name: string, args: RailwayDomainArgs, opts: RailwayDomainOptions);
|
|
91
113
|
}
|
|
92
114
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"domain.d.mts","names":[],"sources":["../../src/railway/domain.ts"],"mappings":";;;;;;;;;UAQU,mBAAA;;EAET,KAAA;EAF4B;EAK5B,SAAA;EAL4B;EAQ5B,SAAA;EAHA;EAMA,aAAA;EAAA;EAGA,YAAA;AAAA;AAAY;AAAA,UAIH,oBAAA,SAA6B,mBAAmB;EAA3B;EAE9B,QAAA;EAFyD;EAKzD,IAAA;EAHA;;;;
|
|
1
|
+
{"version":3,"file":"domain.d.mts","names":[],"sources":["../../src/railway/domain.ts"],"mappings":";;;;;;;;;UAQU,mBAAA;;EAET,KAAA;EAF4B;EAK5B,SAAA;EAL4B;EAQ5B,SAAA;EAHA;EAMA,aAAA;EAAA;EAGA,YAAA;AAAA;AAAY;AAAA,UAIH,oBAAA,SAA6B,mBAAmB;EAA3B;EAE9B,QAAA;EAFyD;EAKzD,IAAA;EAHA;;;;;EAUA,WAAA;EAeoB;AAyJrB;;;;EAjKC,mBAAA;EAsKG;;;;;;EA9JH,oBAAA;AAAA;;;;;;;;;cAyJY,6BAAA,YACD,MAAA,CAAO,OAAA,CAAQ,gBAAA;EAEpB,MAAA,CACL,MAAA,EAAQ,mBAAA,GACN,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,YAAA;EA6FpB,IAAA,CACL,GAAA,UACA,KAAA,EAAO,oBAAA,GACL,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,UAAA;EAgDpB,MAAA,CAAO,GAAA,UAAa,KAAA,EAAO,oBAAA,GAAuB,OAAA;EAgBlD,IAAA,CACL,GAAA,UACA,IAAA,EAAM,oBAAA,EACN,IAAA,EAAM,mBAAA,GACJ,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,UAAA;AAAA;;KA8DtB,oBAAA,GAAuB,IAAA,CAC3B,MAAA,CAAO,wBAAA;EArIN,sCAyID,QAAA,EAAU,eAAA,EAxIT;EA2ID,OAAA,EAAS,cAAA,EA1IE;EA6IX,WAAA,EAAa,kBAAA,EA7Ia;EAgJ1B,OAAA,EAAS,cAAA;AAAA;;UAIO,iBAAA;EApGwC;EAsGxD,YAAA,GAAe,MAAA,CAAO,KAAK;AAAA;;;;;;;;;;AAlFS;AAqBpC;;;;;;;;;;cAoFY,aAAA,SAAsB,MAAA,CAAO,iBAAA;EA3Cd;EAAA,SA6CX,IAAA,EAAM,MAAA,CAAO,MAAA;EA5CtB;;;;;EAAA,SAmDS,WAAA,EAAa,MAAA,CAAO,MAAA;EAzCvB;;;;AAGU;AAIxB;EAPc,SAiDG,mBAAA,EAAqB,MAAA,CAAO,MAAA;;WAG5B,oBAAA,EAAsB,MAAA,CAAO,MAAA;cAG5C,IAAA,UACA,IAAA,EAAM,iBAAA,EACN,IAAA,EAAM,oBAAA;AAAA"}
|
package/dist/railway/domain.mjs
CHANGED
|
@@ -10,9 +10,32 @@ import * as pulumi from "@pulumi/pulumi";
|
|
|
10
10
|
function extractCnameTarget(dnsRecords) {
|
|
11
11
|
return dnsRecords?.find((record) => record.recordType === "DNS_RECORD_TYPE_CNAME" && record.purpose === "DNS_RECORD_PURPOSE_TRAFFIC_ROUTE")?.requiredValue;
|
|
12
12
|
}
|
|
13
|
+
const RAILWAY_VERIFY_PREFIX = "railway-verify=";
|
|
14
|
+
/**
|
|
15
|
+
* Composes the ownership-verification TXT record value from Railway's token.
|
|
16
|
+
* Idempotent: Railway currently returns the token already prefixed, but this
|
|
17
|
+
* guards against a future API response returning the bare token instead.
|
|
18
|
+
*/
|
|
19
|
+
function composeVerificationTxtValue(token) {
|
|
20
|
+
return token.startsWith(RAILWAY_VERIFY_PREFIX) ? token : `${RAILWAY_VERIFY_PREFIX}${token}`;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Extracts the ready-to-use ownership-verification TXT record (name + composed
|
|
24
|
+
* value) from a custom domain's status, or `undefined` if Railway hasn't assigned
|
|
25
|
+
* one (service domains, or custom domains needing no verification).
|
|
26
|
+
*/
|
|
27
|
+
function extractVerificationTxt(status) {
|
|
28
|
+
if (!status?.verificationDnsHost || !status.verificationToken) return;
|
|
29
|
+
return {
|
|
30
|
+
name: status.verificationDnsHost,
|
|
31
|
+
value: composeVerificationTxtValue(status.verificationToken)
|
|
32
|
+
};
|
|
33
|
+
}
|
|
13
34
|
const DOMAIN_STATUS_FIELDS = `
|
|
14
35
|
status {
|
|
15
36
|
dnsRecords { recordType purpose requiredValue }
|
|
37
|
+
verificationDnsHost
|
|
38
|
+
verificationToken
|
|
16
39
|
}
|
|
17
40
|
`;
|
|
18
41
|
const SERVICE_DOMAINS_QUERY = `
|
|
@@ -69,13 +92,16 @@ var RailwayDomainResourceProvider = class {
|
|
|
69
92
|
const found = existing.customDomains.find((d) => d.domain === inputs.customDomain);
|
|
70
93
|
if (found) {
|
|
71
94
|
pulumi.log.info(`Adopting existing custom domain "${found.domain}"`);
|
|
95
|
+
const verificationTxt = extractVerificationTxt(found.status);
|
|
72
96
|
return {
|
|
73
97
|
id: found.domain,
|
|
74
98
|
outs: {
|
|
75
99
|
...inputs,
|
|
76
100
|
domainId: found.id,
|
|
77
101
|
fqdn: found.domain,
|
|
78
|
-
cnameTarget: extractCnameTarget(found.status?.dnsRecords)
|
|
102
|
+
cnameTarget: extractCnameTarget(found.status?.dnsRecords),
|
|
103
|
+
verificationTxtName: verificationTxt?.name,
|
|
104
|
+
verificationTxtValue: verificationTxt?.value
|
|
79
105
|
}
|
|
80
106
|
};
|
|
81
107
|
}
|
|
@@ -85,13 +111,16 @@ var RailwayDomainResourceProvider = class {
|
|
|
85
111
|
environmentId: inputs.environmentId,
|
|
86
112
|
domain: inputs.customDomain
|
|
87
113
|
} });
|
|
114
|
+
const createdVerificationTxt = extractVerificationTxt(result.customDomainCreate.status);
|
|
88
115
|
return {
|
|
89
116
|
id: result.customDomainCreate.domain,
|
|
90
117
|
outs: {
|
|
91
118
|
...inputs,
|
|
92
119
|
domainId: result.customDomainCreate.id,
|
|
93
120
|
fqdn: result.customDomainCreate.domain,
|
|
94
|
-
cnameTarget: extractCnameTarget(result.customDomainCreate.status?.dnsRecords)
|
|
121
|
+
cnameTarget: extractCnameTarget(result.customDomainCreate.status?.dnsRecords),
|
|
122
|
+
verificationTxtName: createdVerificationTxt?.name,
|
|
123
|
+
verificationTxtValue: createdVerificationTxt?.value
|
|
95
124
|
}
|
|
96
125
|
};
|
|
97
126
|
}
|
|
@@ -125,13 +154,16 @@ var RailwayDomainResourceProvider = class {
|
|
|
125
154
|
if (props.customDomain) {
|
|
126
155
|
const found = existing.customDomains.find((d) => d.domain === props.customDomain);
|
|
127
156
|
if (!found) throw new Error(`Custom domain "${props.customDomain}" not found during refresh`);
|
|
157
|
+
const refreshedVerificationTxt = extractVerificationTxt(found.status);
|
|
128
158
|
return {
|
|
129
159
|
id: found.domain,
|
|
130
160
|
props: {
|
|
131
161
|
...props,
|
|
132
162
|
domainId: found.id,
|
|
133
163
|
fqdn: found.domain,
|
|
134
|
-
cnameTarget: extractCnameTarget(found.status?.dnsRecords)
|
|
164
|
+
cnameTarget: extractCnameTarget(found.status?.dnsRecords),
|
|
165
|
+
verificationTxtName: refreshedVerificationTxt?.name,
|
|
166
|
+
verificationTxtValue: refreshedVerificationTxt?.value
|
|
135
167
|
}
|
|
136
168
|
};
|
|
137
169
|
}
|
|
@@ -176,7 +208,9 @@ var RailwayDomainResource = class extends pulumi.dynamic.Resource {
|
|
|
176
208
|
...args,
|
|
177
209
|
domainId: void 0,
|
|
178
210
|
fqdn: void 0,
|
|
179
|
-
cnameTarget: void 0
|
|
211
|
+
cnameTarget: void 0,
|
|
212
|
+
verificationTxtName: void 0,
|
|
213
|
+
verificationTxtValue: void 0
|
|
180
214
|
}, opts);
|
|
181
215
|
}
|
|
182
216
|
};
|
|
@@ -213,9 +247,13 @@ var RailwayDomain = class extends pulumi.ComponentResource {
|
|
|
213
247
|
}, { parent: this });
|
|
214
248
|
this.fqdn = resource.fqdn;
|
|
215
249
|
this.cnameTarget = resource.cnameTarget;
|
|
250
|
+
this.verificationTxtName = resource.verificationTxtName;
|
|
251
|
+
this.verificationTxtValue = resource.verificationTxtValue;
|
|
216
252
|
this.registerOutputs({
|
|
217
253
|
fqdn: this.fqdn,
|
|
218
|
-
cnameTarget: this.cnameTarget
|
|
254
|
+
cnameTarget: this.cnameTarget,
|
|
255
|
+
verificationTxtName: this.verificationTxtName,
|
|
256
|
+
verificationTxtValue: this.verificationTxtValue
|
|
219
257
|
});
|
|
220
258
|
}
|
|
221
259
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"domain.mjs","names":[],"sources":["../../src/railway/domain.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\";\nimport type { RailwayService } from \"./service\";\n\n/** Resolved inputs for the Railway domain dynamic provider. */\ninterface RailwayDomainInputs {\n\t/** Railway API bearer token. */\n\ttoken: string;\n\n\t/** Railway project UUID. */\n\tprojectId: string;\n\n\t/** Railway service UUID to attach the domain to. */\n\tserviceId: string;\n\n\t/** Railway environment UUID (e.g. production). */\n\tenvironmentId: string;\n\n\t/** Custom domain FQDN (e.g. `\"api.example.com\"`). Omit for auto-generated Railway domain. */\n\tcustomDomain?: string;\n}\n\n/** Persisted state for the Railway domain, extending inputs with Railway-assigned identifiers. */\ninterface RailwayDomainOutputs extends RailwayDomainInputs {\n\t/** Railway-assigned domain UUID (used for deletion API calls). */\n\tdomainId: string;\n\n\t/** Fully qualified domain name (e.g. `\"api-production-abc.up.railway.app\"`). */\n\tfqdn: string;\n\n\t/**\n\t * The value to CNAME `customDomain` to. Only present for custom domains — a\n\t * service's auto-generated `*.up.railway.app` domain needs no DNS record of its\n\t * own. `undefined` if Railway hasn't returned a traffic-routing CNAME record yet.\n\t */\n\tcnameTarget?: string;\n}\n\n/** A single DNS record Railway expects for a custom domain (CNAME target, ACME challenge, ...). */\ninterface DomainDnsRecord {\n\trecordType: string;\n\tpurpose: string;\n\trequiredValue: string;\n}\n\n/** Shape returned for a custom domain, including its required DNS records. */\ninterface CustomDomainEntry {\n\tid: string;\n\tdomain: string;\n\tstatus?: { dnsRecords: DomainDnsRecord[] };\n}\n\n/**\n * Picks the CNAME record Railway expects for routing traffic to a custom domain out\n * of its full DNS record list (which also includes e.g. an ACME challenge TXT record).\n */\nfunction extractCnameTarget(\n\tdnsRecords: DomainDnsRecord[] | undefined,\n): string | undefined {\n\treturn dnsRecords?.find(\n\t\t(record) =>\n\t\t\trecord.recordType === \"DNS_RECORD_TYPE_CNAME\" &&\n\t\t\trecord.purpose === \"DNS_RECORD_PURPOSE_TRAFFIC_ROUTE\",\n\t)?.requiredValue;\n}\n\nconst DOMAIN_STATUS_FIELDS = `\n status {\n dnsRecords { recordType purpose requiredValue }\n }\n`;\n\nconst SERVICE_DOMAINS_QUERY = `\n query($projectId: String!, $serviceId: String!, $environmentId: String!) {\n domains(\n projectId: $projectId\n serviceId: $serviceId\n environmentId: $environmentId\n ) {\n serviceDomains { id domain }\n customDomains { id domain ${DOMAIN_STATUS_FIELDS} }\n }\n }\n`;\n\nconst SERVICE_DOMAIN_CREATE = `\n mutation($input: ServiceDomainCreateInput!) {\n serviceDomainCreate(input: $input) { id domain }\n }\n`;\n\nconst CUSTOM_DOMAIN_CREATE = `\n mutation($input: CustomDomainCreateInput!) {\n customDomainCreate(input: $input) { id domain ${DOMAIN_STATUS_FIELDS} }\n }\n`;\n\nconst CUSTOM_DOMAIN_DELETE = `\n mutation($id: String!) { customDomainDelete(id: $id) }\n`;\n\nconst SERVICE_DOMAIN_DELETE = `\n mutation($id: String!) { serviceDomainDelete(id: $id) }\n`;\n\n/**\n * Queries all existing domains (service and custom) for a Railway service.\n */\nasync function findExistingDomains(\n\tclient: RailwayClient,\n\tprojectId: string,\n\tserviceId: string,\n\tenvironmentId: string,\n): Promise<{\n\tserviceDomains: Array<{ id: string; domain: string }>;\n\tcustomDomains: CustomDomainEntry[];\n}> {\n\tconst result = await client.query<{\n\t\tdomains: {\n\t\t\tserviceDomains: Array<{ id: string; domain: string }>;\n\t\t\tcustomDomains: CustomDomainEntry[];\n\t\t};\n\t}>(SERVICE_DOMAINS_QUERY, { projectId, serviceId, environmentId });\n\n\treturn result.domains;\n}\n\n/**\n * Dynamic provider implementing CRUD for Railway domains.\n *\n * Uses adopt-or-create: queries existing domains before creating new ones.\n * Uses the FQDN as the Pulumi resource ID.\n *\n * @internal Exported only for unit testing; not part of the public API surface.\n */\nexport class RailwayDomainResourceProvider\n\timplements pulumi.dynamic.ResourceProvider\n{\n\tasync create(\n\t\tinputs: RailwayDomainInputs,\n\t): Promise<pulumi.dynamic.CreateResult> {\n\t\tconst client = new RailwayClient(inputs.token);\n\n\t\tconst existing = await findExistingDomains(\n\t\t\tclient,\n\t\t\tinputs.projectId,\n\t\t\tinputs.serviceId,\n\t\t\tinputs.environmentId,\n\t\t);\n\n\t\tif (inputs.customDomain) {\n\t\t\tconst found = existing.customDomains.find(\n\t\t\t\t(d) => d.domain === inputs.customDomain,\n\t\t\t);\n\n\t\t\tif (found) {\n\t\t\t\tpulumi.log.info(`Adopting existing custom domain \"${found.domain}\"`);\n\n\t\t\t\treturn {\n\t\t\t\t\tid: found.domain,\n\t\t\t\t\touts: {\n\t\t\t\t\t\t...inputs,\n\t\t\t\t\t\tdomainId: found.id,\n\t\t\t\t\t\tfqdn: found.domain,\n\t\t\t\t\t\tcnameTarget: extractCnameTarget(found.status?.dnsRecords),\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tconst result = await client.query<{\n\t\t\t\tcustomDomainCreate: CustomDomainEntry;\n\t\t\t}>(CUSTOM_DOMAIN_CREATE, {\n\t\t\t\tinput: {\n\t\t\t\t\tprojectId: inputs.projectId,\n\t\t\t\t\tserviceId: inputs.serviceId,\n\t\t\t\t\tenvironmentId: inputs.environmentId,\n\t\t\t\t\tdomain: inputs.customDomain,\n\t\t\t\t},\n\t\t\t});\n\n\t\t\treturn {\n\t\t\t\tid: result.customDomainCreate.domain,\n\t\t\t\touts: {\n\t\t\t\t\t...inputs,\n\t\t\t\t\tdomainId: result.customDomainCreate.id,\n\t\t\t\t\tfqdn: result.customDomainCreate.domain,\n\t\t\t\t\tcnameTarget: extractCnameTarget(\n\t\t\t\t\t\tresult.customDomainCreate.status?.dnsRecords,\n\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tif (existing.serviceDomains.length > 0) {\n\t\t\tconst found = existing.serviceDomains[0];\n\n\t\t\tpulumi.log.info(`Adopting existing service domain \"${found.domain}\"`);\n\n\t\t\treturn {\n\t\t\t\tid: found.domain,\n\t\t\t\touts: { ...inputs, domainId: found.id, fqdn: found.domain },\n\t\t\t};\n\t\t}\n\n\t\tconst result = await client.query<{\n\t\t\tserviceDomainCreate: { id: string; domain: string };\n\t\t}>(SERVICE_DOMAIN_CREATE, {\n\t\t\tinput: {\n\t\t\t\tserviceId: inputs.serviceId,\n\t\t\t\tenvironmentId: inputs.environmentId,\n\t\t\t},\n\t\t});\n\n\t\treturn {\n\t\t\tid: result.serviceDomainCreate.domain,\n\t\t\touts: {\n\t\t\t\t...inputs,\n\t\t\t\tdomainId: result.serviceDomainCreate.id,\n\t\t\t\tfqdn: result.serviceDomainCreate.domain,\n\t\t\t},\n\t\t};\n\t}\n\n\tasync read(\n\t\t_id: string,\n\t\tprops: RailwayDomainOutputs,\n\t): Promise<pulumi.dynamic.ReadResult> {\n\t\tconst client = new RailwayClient(props.token);\n\n\t\tconst existing = await findExistingDomains(\n\t\t\tclient,\n\t\t\tprops.projectId,\n\t\t\tprops.serviceId,\n\t\t\tprops.environmentId,\n\t\t);\n\n\t\tif (props.customDomain) {\n\t\t\tconst found = existing.customDomains.find(\n\t\t\t\t(d) => d.domain === props.customDomain,\n\t\t\t);\n\n\t\t\tif (!found) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Custom domain \"${props.customDomain}\" not found during refresh`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tid: found.domain,\n\t\t\t\tprops: {\n\t\t\t\t\t...props,\n\t\t\t\t\tdomainId: found.id,\n\t\t\t\t\tfqdn: found.domain,\n\t\t\t\t\tcnameTarget: extractCnameTarget(found.status?.dnsRecords),\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tif (existing.serviceDomains.length > 0) {\n\t\t\tconst found = existing.serviceDomains[0];\n\n\t\t\treturn {\n\t\t\t\tid: found.domain,\n\t\t\t\tprops: { ...props, domainId: found.id, fqdn: found.domain },\n\t\t\t};\n\t\t}\n\n\t\tthrow new Error(\"Railway domain not found during refresh\");\n\t}\n\n\tasync delete(_id: string, props: RailwayDomainOutputs): Promise<void> {\n\t\tconst client = new RailwayClient(props.token);\n\n\t\tconst mutation = props.customDomain\n\t\t\t? CUSTOM_DOMAIN_DELETE\n\t\t\t: SERVICE_DOMAIN_DELETE;\n\n\t\ttry {\n\t\t\tawait client.query(mutation, { id: props.domainId });\n\t\t} catch {\n\t\t\tpulumi.log.warn(\n\t\t\t\t\"Failed to delete Railway domain (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: RailwayDomainOutputs,\n\t\tnews: RailwayDomainInputs,\n\t): Promise<pulumi.dynamic.DiffResult> {\n\t\tconst replaces: string[] = [];\n\n\t\tif (olds.serviceId !== news.serviceId) {\n\t\t\treplaces.push(\"serviceId\");\n\t\t}\n\n\t\tif (olds.customDomain !== news.customDomain) {\n\t\t\treplaces.push(\"customDomain\");\n\t\t}\n\n\t\tif (olds.environmentId !== news.environmentId) {\n\t\t\treplaces.push(\"environmentId\");\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 RailwayDomainResource extends pulumi.dynamic.Resource {\n\tpublic declare readonly fqdn: pulumi.Output<string>;\n\tpublic declare readonly cnameTarget: pulumi.Output<string | undefined>;\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\tserviceId: pulumi.Input<string>;\n\t\t\tenvironmentId: pulumi.Input<string>;\n\t\t\tcustomDomain?: pulumi.Input<string>;\n\t\t},\n\t\topts?: pulumi.CustomResourceOptions,\n\t) {\n\t\tsuper(\n\t\t\tnew RailwayDomainResourceProvider(),\n\t\t\tname,\n\t\t\t{ ...args, domainId: undefined, fqdn: undefined, cnameTarget: undefined },\n\t\t\topts,\n\t\t);\n\t}\n}\n\n/** Options type for RailwayDomain — replaces Pulumi's native `provider` field. */\ntype RailwayDomainOptions = Omit<\n\tpulumi.ComponentResourceOptions,\n\t\"provider\"\n> & {\n\t/** Railway authentication context. */\n\tprovider: RailwayProvider;\n\n\t/** Railway project context. */\n\tproject: RailwayProject;\n\n\t/** Railway environment context. */\n\tenvironment: RailwayEnvironment;\n\n\t/** Railway service context. */\n\tservice: RailwayService;\n};\n\n/** Args for RailwayDomain. */\nexport interface RailwayDomainArgs {\n\t/** Custom domain FQDN. Omit to create an auto-generated Railway service domain. */\n\tcustomDomain?: pulumi.Input<string>;\n}\n\n/**\n * Manages a Railway domain (service or custom) with adopt-or-create semantics.\n *\n * A service can carry more than one custom domain — declare one `RailwayDomain` per\n * domain; each instance adopts, creates, and deletes only its own domain.\n *\n * @example\n * ```typescript\n * const apiDomain = new RailwayDomain(\"api-domain\", { customDomain: \"api.example.com\" }, {\n * provider, project, environment, service,\n * });\n * const wwwDomain = new RailwayDomain(\"www-domain\", { customDomain: \"www.example.com\" }, {\n * provider, project, environment, service,\n * });\n *\n * // Point each domain's DNS CNAME at its own target.\n * const apiCnameTarget = apiDomain.cnameTarget;\n * const wwwCnameTarget = wwwDomain.cnameTarget;\n * ```\n */\nexport class RailwayDomain extends pulumi.ComponentResource {\n\t/** Fully qualified domain name. */\n\tpublic readonly fqdn: pulumi.Output<string>;\n\n\t/**\n\t * The value to CNAME `customDomain` to. Only set for custom domains — `undefined`\n\t * for a service's auto-generated `*.up.railway.app` domain, or if Railway hasn't\n\t * returned a traffic-routing CNAME record for it yet.\n\t */\n\tpublic readonly cnameTarget: pulumi.Output<string | undefined>;\n\n\tconstructor(\n\t\tname: string,\n\t\targs: RailwayDomainArgs,\n\t\topts: RailwayDomainOptions,\n\t) {\n\t\tconst { provider, project, environment, service, ...pulumiOpts } = opts;\n\n\t\tsuper(\"infracraft:railway:Domain\", name, {}, pulumiOpts);\n\n\t\tconst resource = new RailwayDomainResource(\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\tserviceId: service.id,\n\t\t\t\tenvironmentId: environment.id,\n\t\t\t\tcustomDomain: args.customDomain,\n\t\t\t},\n\t\t\t{ parent: this },\n\t\t);\n\n\t\tthis.fqdn = resource.fqdn;\n\t\tthis.cnameTarget = resource.cnameTarget;\n\n\t\tthis.registerOutputs({ fqdn: this.fqdn, cnameTarget: this.cnameTarget });\n\t}\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"domain.mjs","names":[],"sources":["../../src/railway/domain.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\";\nimport type { RailwayService } from \"./service\";\n\n/** Resolved inputs for the Railway domain dynamic provider. */\ninterface RailwayDomainInputs {\n\t/** Railway API bearer token. */\n\ttoken: string;\n\n\t/** Railway project UUID. */\n\tprojectId: string;\n\n\t/** Railway service UUID to attach the domain to. */\n\tserviceId: string;\n\n\t/** Railway environment UUID (e.g. production). */\n\tenvironmentId: string;\n\n\t/** Custom domain FQDN (e.g. `\"api.example.com\"`). Omit for auto-generated Railway domain. */\n\tcustomDomain?: string;\n}\n\n/** Persisted state for the Railway domain, extending inputs with Railway-assigned identifiers. */\ninterface RailwayDomainOutputs extends RailwayDomainInputs {\n\t/** Railway-assigned domain UUID (used for deletion API calls). */\n\tdomainId: string;\n\n\t/** Fully qualified domain name (e.g. `\"api-production-abc.up.railway.app\"`). */\n\tfqdn: string;\n\n\t/**\n\t * The value to CNAME `customDomain` to. Only present for custom domains — a\n\t * service's auto-generated `*.up.railway.app` domain needs no DNS record of its\n\t * own. `undefined` if Railway hasn't returned a traffic-routing CNAME record yet.\n\t */\n\tcnameTarget?: string;\n\n\t/**\n\t * DNS record name for the ownership-verification TXT record (e.g.\n\t * `\"_railway-verify.staging.api\"`). `undefined` for service domains, and for\n\t * custom domains Railway doesn't require ownership verification for.\n\t */\n\tverificationTxtName?: string;\n\n\t/**\n\t * DNS record value for the ownership-verification TXT record, ready to write\n\t * as-is (the `railway-verify=` prefix is already composed — see\n\t * {@link composeVerificationTxtValue}). `undefined` under the same conditions as\n\t * {@link verificationTxtName}.\n\t */\n\tverificationTxtValue?: string;\n}\n\n/** A single DNS record Railway expects for a custom domain (CNAME target, ACME challenge, ...). */\ninterface DomainDnsRecord {\n\trecordType: string;\n\tpurpose: string;\n\trequiredValue: string;\n}\n\n/** Domain ownership-verification status, distinct from `dnsRecords` (see live API notes below). */\ninterface CustomDomainStatus {\n\tdnsRecords: DomainDnsRecord[];\n\t/**\n\t * DNS host for the ownership-verification TXT record. Verified live against\n\t * Railway's current API: this sits on `CustomDomain.status`, as a sibling of\n\t * `dnsRecords` rather than inside it — the two are populated independently\n\t * (`dnsRecords` can be non-empty with `verificationDnsHost` still null, and\n\t * vice versa), so both must be queried explicitly.\n\t */\n\tverificationDnsHost?: string | null;\n\t/**\n\t * Raw ownership-verification token. Verified live: Railway already returns this\n\t * pre-composed with the `railway-verify=` prefix (not a bare token) — see\n\t * {@link composeVerificationTxtValue}, which stays idempotent in case that ever\n\t * changes.\n\t */\n\tverificationToken?: string | null;\n}\n\n/** Shape returned for a custom domain, including its required DNS records. */\ninterface CustomDomainEntry {\n\tid: string;\n\tdomain: string;\n\tstatus?: CustomDomainStatus;\n}\n\n/**\n * Picks the CNAME record Railway expects for routing traffic to a custom domain out\n * of its full DNS record list (which also includes e.g. an ACME challenge TXT record).\n */\nfunction extractCnameTarget(\n\tdnsRecords: DomainDnsRecord[] | undefined,\n): string | undefined {\n\treturn dnsRecords?.find(\n\t\t(record) =>\n\t\t\trecord.recordType === \"DNS_RECORD_TYPE_CNAME\" &&\n\t\t\trecord.purpose === \"DNS_RECORD_PURPOSE_TRAFFIC_ROUTE\",\n\t)?.requiredValue;\n}\n\nconst RAILWAY_VERIFY_PREFIX = \"railway-verify=\";\n\n/**\n * Composes the ownership-verification TXT record value from Railway's token.\n * Idempotent: Railway currently returns the token already prefixed, but this\n * guards against a future API response returning the bare token instead.\n */\nfunction composeVerificationTxtValue(token: string): string {\n\treturn token.startsWith(RAILWAY_VERIFY_PREFIX)\n\t\t? token\n\t\t: `${RAILWAY_VERIFY_PREFIX}${token}`;\n}\n\n/**\n * Extracts the ready-to-use ownership-verification TXT record (name + composed\n * value) from a custom domain's status, or `undefined` if Railway hasn't assigned\n * one (service domains, or custom domains needing no verification).\n */\nfunction extractVerificationTxt(\n\tstatus: CustomDomainStatus | undefined,\n): { name: string; value: string } | undefined {\n\tif (!status?.verificationDnsHost || !status.verificationToken) {\n\t\treturn undefined;\n\t}\n\n\treturn {\n\t\tname: status.verificationDnsHost,\n\t\tvalue: composeVerificationTxtValue(status.verificationToken),\n\t};\n}\n\nconst DOMAIN_STATUS_FIELDS = `\n status {\n dnsRecords { recordType purpose requiredValue }\n verificationDnsHost\n verificationToken\n }\n`;\n\nconst SERVICE_DOMAINS_QUERY = `\n query($projectId: String!, $serviceId: String!, $environmentId: String!) {\n domains(\n projectId: $projectId\n serviceId: $serviceId\n environmentId: $environmentId\n ) {\n serviceDomains { id domain }\n customDomains { id domain ${DOMAIN_STATUS_FIELDS} }\n }\n }\n`;\n\nconst SERVICE_DOMAIN_CREATE = `\n mutation($input: ServiceDomainCreateInput!) {\n serviceDomainCreate(input: $input) { id domain }\n }\n`;\n\nconst CUSTOM_DOMAIN_CREATE = `\n mutation($input: CustomDomainCreateInput!) {\n customDomainCreate(input: $input) { id domain ${DOMAIN_STATUS_FIELDS} }\n }\n`;\n\nconst CUSTOM_DOMAIN_DELETE = `\n mutation($id: String!) { customDomainDelete(id: $id) }\n`;\n\nconst SERVICE_DOMAIN_DELETE = `\n mutation($id: String!) { serviceDomainDelete(id: $id) }\n`;\n\n/**\n * Queries all existing domains (service and custom) for a Railway service.\n */\nasync function findExistingDomains(\n\tclient: RailwayClient,\n\tprojectId: string,\n\tserviceId: string,\n\tenvironmentId: string,\n): Promise<{\n\tserviceDomains: Array<{ id: string; domain: string }>;\n\tcustomDomains: CustomDomainEntry[];\n}> {\n\tconst result = await client.query<{\n\t\tdomains: {\n\t\t\tserviceDomains: Array<{ id: string; domain: string }>;\n\t\t\tcustomDomains: CustomDomainEntry[];\n\t\t};\n\t}>(SERVICE_DOMAINS_QUERY, { projectId, serviceId, environmentId });\n\n\treturn result.domains;\n}\n\n/**\n * Dynamic provider implementing CRUD for Railway domains.\n *\n * Uses adopt-or-create: queries existing domains before creating new ones.\n * Uses the FQDN as the Pulumi resource ID.\n *\n * @internal Exported only for unit testing; not part of the public API surface.\n */\nexport class RailwayDomainResourceProvider\n\timplements pulumi.dynamic.ResourceProvider\n{\n\tasync create(\n\t\tinputs: RailwayDomainInputs,\n\t): Promise<pulumi.dynamic.CreateResult> {\n\t\tconst client = new RailwayClient(inputs.token);\n\n\t\tconst existing = await findExistingDomains(\n\t\t\tclient,\n\t\t\tinputs.projectId,\n\t\t\tinputs.serviceId,\n\t\t\tinputs.environmentId,\n\t\t);\n\n\t\tif (inputs.customDomain) {\n\t\t\tconst found = existing.customDomains.find(\n\t\t\t\t(d) => d.domain === inputs.customDomain,\n\t\t\t);\n\n\t\t\tif (found) {\n\t\t\t\tpulumi.log.info(`Adopting existing custom domain \"${found.domain}\"`);\n\n\t\t\t\tconst verificationTxt = extractVerificationTxt(found.status);\n\n\t\t\t\treturn {\n\t\t\t\t\tid: found.domain,\n\t\t\t\t\touts: {\n\t\t\t\t\t\t...inputs,\n\t\t\t\t\t\tdomainId: found.id,\n\t\t\t\t\t\tfqdn: found.domain,\n\t\t\t\t\t\tcnameTarget: extractCnameTarget(found.status?.dnsRecords),\n\t\t\t\t\t\tverificationTxtName: verificationTxt?.name,\n\t\t\t\t\t\tverificationTxtValue: verificationTxt?.value,\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tconst result = await client.query<{\n\t\t\t\tcustomDomainCreate: CustomDomainEntry;\n\t\t\t}>(CUSTOM_DOMAIN_CREATE, {\n\t\t\t\tinput: {\n\t\t\t\t\tprojectId: inputs.projectId,\n\t\t\t\t\tserviceId: inputs.serviceId,\n\t\t\t\t\tenvironmentId: inputs.environmentId,\n\t\t\t\t\tdomain: inputs.customDomain,\n\t\t\t\t},\n\t\t\t});\n\n\t\t\tconst createdVerificationTxt = extractVerificationTxt(\n\t\t\t\tresult.customDomainCreate.status,\n\t\t\t);\n\n\t\t\treturn {\n\t\t\t\tid: result.customDomainCreate.domain,\n\t\t\t\touts: {\n\t\t\t\t\t...inputs,\n\t\t\t\t\tdomainId: result.customDomainCreate.id,\n\t\t\t\t\tfqdn: result.customDomainCreate.domain,\n\t\t\t\t\tcnameTarget: extractCnameTarget(\n\t\t\t\t\t\tresult.customDomainCreate.status?.dnsRecords,\n\t\t\t\t\t),\n\t\t\t\t\tverificationTxtName: createdVerificationTxt?.name,\n\t\t\t\t\tverificationTxtValue: createdVerificationTxt?.value,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tif (existing.serviceDomains.length > 0) {\n\t\t\tconst found = existing.serviceDomains[0];\n\n\t\t\tpulumi.log.info(`Adopting existing service domain \"${found.domain}\"`);\n\n\t\t\treturn {\n\t\t\t\tid: found.domain,\n\t\t\t\touts: { ...inputs, domainId: found.id, fqdn: found.domain },\n\t\t\t};\n\t\t}\n\n\t\tconst result = await client.query<{\n\t\t\tserviceDomainCreate: { id: string; domain: string };\n\t\t}>(SERVICE_DOMAIN_CREATE, {\n\t\t\tinput: {\n\t\t\t\tserviceId: inputs.serviceId,\n\t\t\t\tenvironmentId: inputs.environmentId,\n\t\t\t},\n\t\t});\n\n\t\treturn {\n\t\t\tid: result.serviceDomainCreate.domain,\n\t\t\touts: {\n\t\t\t\t...inputs,\n\t\t\t\tdomainId: result.serviceDomainCreate.id,\n\t\t\t\tfqdn: result.serviceDomainCreate.domain,\n\t\t\t},\n\t\t};\n\t}\n\n\tasync read(\n\t\t_id: string,\n\t\tprops: RailwayDomainOutputs,\n\t): Promise<pulumi.dynamic.ReadResult> {\n\t\tconst client = new RailwayClient(props.token);\n\n\t\tconst existing = await findExistingDomains(\n\t\t\tclient,\n\t\t\tprops.projectId,\n\t\t\tprops.serviceId,\n\t\t\tprops.environmentId,\n\t\t);\n\n\t\tif (props.customDomain) {\n\t\t\tconst found = existing.customDomains.find(\n\t\t\t\t(d) => d.domain === props.customDomain,\n\t\t\t);\n\n\t\t\tif (!found) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Custom domain \"${props.customDomain}\" not found during refresh`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst refreshedVerificationTxt = extractVerificationTxt(found.status);\n\n\t\t\treturn {\n\t\t\t\tid: found.domain,\n\t\t\t\tprops: {\n\t\t\t\t\t...props,\n\t\t\t\t\tdomainId: found.id,\n\t\t\t\t\tfqdn: found.domain,\n\t\t\t\t\tcnameTarget: extractCnameTarget(found.status?.dnsRecords),\n\t\t\t\t\tverificationTxtName: refreshedVerificationTxt?.name,\n\t\t\t\t\tverificationTxtValue: refreshedVerificationTxt?.value,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tif (existing.serviceDomains.length > 0) {\n\t\t\tconst found = existing.serviceDomains[0];\n\n\t\t\treturn {\n\t\t\t\tid: found.domain,\n\t\t\t\tprops: { ...props, domainId: found.id, fqdn: found.domain },\n\t\t\t};\n\t\t}\n\n\t\tthrow new Error(\"Railway domain not found during refresh\");\n\t}\n\n\tasync delete(_id: string, props: RailwayDomainOutputs): Promise<void> {\n\t\tconst client = new RailwayClient(props.token);\n\n\t\tconst mutation = props.customDomain\n\t\t\t? CUSTOM_DOMAIN_DELETE\n\t\t\t: SERVICE_DOMAIN_DELETE;\n\n\t\ttry {\n\t\t\tawait client.query(mutation, { id: props.domainId });\n\t\t} catch {\n\t\t\tpulumi.log.warn(\n\t\t\t\t\"Failed to delete Railway domain (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: RailwayDomainOutputs,\n\t\tnews: RailwayDomainInputs,\n\t): Promise<pulumi.dynamic.DiffResult> {\n\t\tconst replaces: string[] = [];\n\n\t\tif (olds.serviceId !== news.serviceId) {\n\t\t\treplaces.push(\"serviceId\");\n\t\t}\n\n\t\tif (olds.customDomain !== news.customDomain) {\n\t\t\treplaces.push(\"customDomain\");\n\t\t}\n\n\t\tif (olds.environmentId !== news.environmentId) {\n\t\t\treplaces.push(\"environmentId\");\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 RailwayDomainResource extends pulumi.dynamic.Resource {\n\tpublic declare readonly fqdn: pulumi.Output<string>;\n\tpublic declare readonly cnameTarget: pulumi.Output<string | undefined>;\n\tpublic declare readonly verificationTxtName: pulumi.Output<\n\t\tstring | undefined\n\t>;\n\tpublic declare readonly verificationTxtValue: pulumi.Output<\n\t\tstring | undefined\n\t>;\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\tserviceId: pulumi.Input<string>;\n\t\t\tenvironmentId: pulumi.Input<string>;\n\t\t\tcustomDomain?: pulumi.Input<string>;\n\t\t},\n\t\topts?: pulumi.CustomResourceOptions,\n\t) {\n\t\tsuper(\n\t\t\tnew RailwayDomainResourceProvider(),\n\t\t\tname,\n\t\t\t{\n\t\t\t\t...args,\n\t\t\t\tdomainId: undefined,\n\t\t\t\tfqdn: undefined,\n\t\t\t\tcnameTarget: undefined,\n\t\t\t\tverificationTxtName: undefined,\n\t\t\t\tverificationTxtValue: undefined,\n\t\t\t},\n\t\t\topts,\n\t\t);\n\t}\n}\n\n/** Options type for RailwayDomain — replaces Pulumi's native `provider` field. */\ntype RailwayDomainOptions = Omit<\n\tpulumi.ComponentResourceOptions,\n\t\"provider\"\n> & {\n\t/** Railway authentication context. */\n\tprovider: RailwayProvider;\n\n\t/** Railway project context. */\n\tproject: RailwayProject;\n\n\t/** Railway environment context. */\n\tenvironment: RailwayEnvironment;\n\n\t/** Railway service context. */\n\tservice: RailwayService;\n};\n\n/** Args for RailwayDomain. */\nexport interface RailwayDomainArgs {\n\t/** Custom domain FQDN. Omit to create an auto-generated Railway service domain. */\n\tcustomDomain?: pulumi.Input<string>;\n}\n\n/**\n * Manages a Railway domain (service or custom) with adopt-or-create semantics.\n *\n * A service can carry more than one custom domain — declare one `RailwayDomain` per\n * domain; each instance adopts, creates, and deletes only its own domain.\n *\n * @example\n * ```typescript\n * const apiDomain = new RailwayDomain(\"api-domain\", { customDomain: \"api.example.com\" }, {\n * provider, project, environment, service,\n * });\n * const wwwDomain = new RailwayDomain(\"www-domain\", { customDomain: \"www.example.com\" }, {\n * provider, project, environment, service,\n * });\n *\n * // Point each domain's DNS CNAME at its own target.\n * const apiCnameTarget = apiDomain.cnameTarget;\n * const wwwCnameTarget = wwwDomain.cnameTarget;\n * ```\n */\nexport class RailwayDomain extends pulumi.ComponentResource {\n\t/** Fully qualified domain name. */\n\tpublic readonly fqdn: pulumi.Output<string>;\n\n\t/**\n\t * The value to CNAME `customDomain` to. Only set for custom domains — `undefined`\n\t * for a service's auto-generated `*.up.railway.app` domain, or if Railway hasn't\n\t * returned a traffic-routing CNAME record for it yet.\n\t */\n\tpublic readonly cnameTarget: pulumi.Output<string | undefined>;\n\n\t/**\n\t * DNS record name for the ownership-verification TXT record. `undefined` for\n\t * service domains, and for custom domains Railway doesn't require verification\n\t * for — write this record (alongside `cnameTarget`'s CNAME) to flip Railway's\n\t * domain status to verified and let its TLS certificate issue.\n\t */\n\tpublic readonly verificationTxtName: pulumi.Output<string | undefined>;\n\n\t/** DNS record value for the ownership-verification TXT record, ready to write as-is. */\n\tpublic readonly verificationTxtValue: pulumi.Output<string | undefined>;\n\n\tconstructor(\n\t\tname: string,\n\t\targs: RailwayDomainArgs,\n\t\topts: RailwayDomainOptions,\n\t) {\n\t\tconst { provider, project, environment, service, ...pulumiOpts } = opts;\n\n\t\tsuper(\"infracraft:railway:Domain\", name, {}, pulumiOpts);\n\n\t\tconst resource = new RailwayDomainResource(\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\tserviceId: service.id,\n\t\t\t\tenvironmentId: environment.id,\n\t\t\t\tcustomDomain: args.customDomain,\n\t\t\t},\n\t\t\t{ parent: this },\n\t\t);\n\n\t\tthis.fqdn = resource.fqdn;\n\t\tthis.cnameTarget = resource.cnameTarget;\n\t\tthis.verificationTxtName = resource.verificationTxtName;\n\t\tthis.verificationTxtValue = resource.verificationTxtValue;\n\n\t\tthis.registerOutputs({\n\t\t\tfqdn: this.fqdn,\n\t\t\tcnameTarget: this.cnameTarget,\n\t\t\tverificationTxtName: this.verificationTxtName,\n\t\t\tverificationTxtValue: this.verificationTxtValue,\n\t\t});\n\t}\n}\n"],"mappings":"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"}
|
package/dist/vercel/domain.cjs
CHANGED
|
@@ -6,10 +6,29 @@ _pulumi_pulumi = require_chunk.__toESM(_pulumi_pulumi, 1);
|
|
|
6
6
|
//#region src/vercel/domain.ts
|
|
7
7
|
const VERCEL_API_URL = "https://api.vercel.com";
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
9
|
+
* Fallback CNAME target, used only when Vercel's domain-config endpoint returns no
|
|
10
|
+
* recommendation (see {@link fetchCnameTarget}). Not the primary source of truth —
|
|
11
|
+
* `VercelDomain.cnameTarget` queries Vercel's per-domain recommendation dynamically,
|
|
12
|
+
* since the actual target can vary by account/domain.
|
|
10
13
|
* https://vercel.com/docs/domains/working-with-domains/add-a-domain
|
|
11
14
|
*/
|
|
12
|
-
const
|
|
15
|
+
const VERCEL_CNAME_TARGET_FALLBACK = "cname.vercel-dns.com";
|
|
16
|
+
/**
|
|
17
|
+
* Fetches Vercel's recommended CNAME target for a domain from its DNS config endpoint.
|
|
18
|
+
* Falls back to {@link VERCEL_CNAME_TARGET_FALLBACK} if Vercel returns no recommendation
|
|
19
|
+
* (logged, since that's an unusual state worth noticing rather than silently accepting).
|
|
20
|
+
*/
|
|
21
|
+
async function fetchCnameTarget(token, teamId, name) {
|
|
22
|
+
const response = await fetch(`${VERCEL_API_URL}/v6/domains/${encodeURIComponent(name)}/config?teamId=${teamId}`, { headers: { Authorization: `Bearer ${token}` } });
|
|
23
|
+
if (!response.ok) throw new Error(`Vercel API error fetching domain config for "${name}" (${response.status}): ${await response.text()}`);
|
|
24
|
+
const { recommendedCNAME = [] } = await response.json();
|
|
25
|
+
const preferred = [...recommendedCNAME].sort((a, b) => a.rank - b.rank)[0];
|
|
26
|
+
if (!preferred) {
|
|
27
|
+
_pulumi_pulumi.log.info(`Vercel returned no recommended CNAME for "${name}" — falling back to "${VERCEL_CNAME_TARGET_FALLBACK}"`);
|
|
28
|
+
return VERCEL_CNAME_TARGET_FALLBACK;
|
|
29
|
+
}
|
|
30
|
+
return preferred.value;
|
|
31
|
+
}
|
|
13
32
|
/**
|
|
14
33
|
* Fetches a project domain by name. Returns `null` if not attached (404).
|
|
15
34
|
*/
|
|
@@ -34,7 +53,8 @@ var VercelDomainResourceProvider = class {
|
|
|
34
53
|
_pulumi_pulumi.log.info(`Adopted existing Vercel domain "${inputs.name}" on project ${inputs.projectId}`);
|
|
35
54
|
const outs = {
|
|
36
55
|
...inputs,
|
|
37
|
-
verified: existing.verified
|
|
56
|
+
verified: existing.verified,
|
|
57
|
+
cnameTarget: await fetchCnameTarget(inputs.token, inputs.teamId, inputs.name)
|
|
38
58
|
};
|
|
39
59
|
return {
|
|
40
60
|
id: `${inputs.projectId}/${inputs.name}`,
|
|
@@ -53,7 +73,8 @@ var VercelDomainResourceProvider = class {
|
|
|
53
73
|
const created = await response.json();
|
|
54
74
|
const outs = {
|
|
55
75
|
...inputs,
|
|
56
|
-
verified: created.verified
|
|
76
|
+
verified: created.verified,
|
|
77
|
+
cnameTarget: await fetchCnameTarget(inputs.token, inputs.teamId, inputs.name)
|
|
57
78
|
};
|
|
58
79
|
return {
|
|
59
80
|
id: `${inputs.projectId}/${inputs.name}`,
|
|
@@ -67,15 +88,17 @@ var VercelDomainResourceProvider = class {
|
|
|
67
88
|
id: `${props.projectId}/${props.name}`,
|
|
68
89
|
props: {
|
|
69
90
|
...props,
|
|
70
|
-
verified: domain.verified
|
|
91
|
+
verified: domain.verified,
|
|
92
|
+
cnameTarget: await fetchCnameTarget(props.token, props.teamId, props.name)
|
|
71
93
|
}
|
|
72
94
|
};
|
|
73
95
|
}
|
|
74
96
|
/** All fields replace (see `diff`) — this is never actually invoked. */
|
|
75
|
-
async update(_id,
|
|
97
|
+
async update(_id, olds, news) {
|
|
76
98
|
return { outs: {
|
|
77
99
|
...news,
|
|
78
|
-
verified: false
|
|
100
|
+
verified: false,
|
|
101
|
+
cnameTarget: olds.cnameTarget
|
|
79
102
|
} };
|
|
80
103
|
}
|
|
81
104
|
async delete(_id, props) {
|
|
@@ -111,7 +134,8 @@ var VercelDomainResource = class extends _pulumi_pulumi.dynamic.Resource {
|
|
|
111
134
|
constructor(name, args, opts) {
|
|
112
135
|
super(new VercelDomainResourceProvider(), name, {
|
|
113
136
|
...args,
|
|
114
|
-
verified: void 0
|
|
137
|
+
verified: void 0,
|
|
138
|
+
cnameTarget: void 0
|
|
115
139
|
}, opts);
|
|
116
140
|
}
|
|
117
141
|
};
|
|
@@ -120,7 +144,10 @@ var VercelDomainResource = class extends _pulumi_pulumi.dynamic.Resource {
|
|
|
120
144
|
*
|
|
121
145
|
* @example
|
|
122
146
|
* ```typescript
|
|
123
|
-
* new VercelDomain("aura-domain", { name: "app.example.com" }, { provider, project });
|
|
147
|
+
* const domain = new VercelDomain("aura-domain", { name: "app.example.com" }, { provider, project });
|
|
148
|
+
*
|
|
149
|
+
* // Point app.example.com's DNS CNAME at this value.
|
|
150
|
+
* const cnameTarget = domain.cnameTarget;
|
|
124
151
|
* ```
|
|
125
152
|
*/
|
|
126
153
|
var VercelDomain = class extends _pulumi_pulumi.ComponentResource {
|
|
@@ -135,15 +162,16 @@ var VercelDomain = class extends _pulumi_pulumi.ComponentResource {
|
|
|
135
162
|
}, { parent: this });
|
|
136
163
|
this.verified = resource.verified;
|
|
137
164
|
this.name = resource.name;
|
|
165
|
+
this.cnameTarget = resource.cnameTarget;
|
|
138
166
|
this.registerOutputs({
|
|
139
167
|
verified: this.verified,
|
|
140
|
-
name: this.name
|
|
168
|
+
name: this.name,
|
|
169
|
+
cnameTarget: this.cnameTarget
|
|
141
170
|
});
|
|
142
171
|
}
|
|
143
172
|
};
|
|
144
173
|
|
|
145
174
|
//#endregion
|
|
146
|
-
exports.VERCEL_CNAME_TARGET = VERCEL_CNAME_TARGET;
|
|
147
175
|
exports.VercelDomain = VercelDomain;
|
|
148
176
|
exports.VercelDomainResourceProvider = VercelDomainResourceProvider;
|
|
149
177
|
//# sourceMappingURL=domain.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"domain.cjs","names":["pulumi"],"sources":["../../src/vercel/domain.ts"],"sourcesContent":["import * as pulumi from \"@pulumi/pulumi\";\nimport type { VercelProject } from \"./project\";\nimport type { VercelProvider } from \"./provider\";\n\nconst VERCEL_API_URL = \"https://api.vercel.com\";\n\n/**\n * Vercel's documented CNAME target for attaching a subdomain to a project.\n * https://vercel.com/docs/domains/working-with-domains/add-a-domain\n */\nexport const VERCEL_CNAME_TARGET = \"cname.vercel-dns.com\";\n\n/** Resolved inputs for the Vercel domain dynamic provider. */\nexport interface VercelDomainInputs {\n\t/** Vercel API bearer token. */\n\ttoken: string;\n\n\t/** Vercel team/org ID. */\n\tteamId: string;\n\n\t/** Vercel project ID to attach the domain to. */\n\tprojectId: string;\n\n\t/** Domain name (e.g. `\"app.example.com\"`). */\n\tname: string;\n}\n\n/** Persisted state for the Vercel domain, extending inputs with verification status. */\ninterface VercelDomainOutputs extends VercelDomainInputs {\n\t/** `true` once the domain's DNS has been verified by Vercel. */\n\tverified: boolean;\n}\n\n/** Vercel API response shape for a project domain. */\ninterface VercelDomainResponse {\n\tname: string;\n\tprojectId: string;\n\tverified: boolean;\n}\n\n/**\n * Fetches a project domain by name. Returns `null` if not attached (404).\n */\nasync function fetchDomain(\n\ttoken: string,\n\tteamId: string,\n\tprojectId: string,\n\tname: string,\n): Promise<VercelDomainResponse | null> {\n\tconst response = await fetch(\n\t\t`${VERCEL_API_URL}/v9/projects/${encodeURIComponent(projectId)}/domains/${encodeURIComponent(name)}?teamId=${teamId}`,\n\t\t{ headers: { Authorization: `Bearer ${token}` } },\n\t);\n\n\tif (response.status === 404) {\n\t\treturn null;\n\t}\n\n\tif (!response.ok) {\n\t\tthrow new Error(\n\t\t\t`Vercel API error fetching domain \"${name}\" (${response.status}): ${await response.text()}`,\n\t\t);\n\t}\n\n\treturn (await response.json()) as VercelDomainResponse;\n}\n\n/**\n * Dynamic provider implementing adopt-or-create for a Vercel project domain.\n *\n * On `create()`, calls `GET /v9/projects/{projectId}/domains/{name}`. If found, adopts\n * the existing attachment. If 404, attaches it via `POST /v10/projects/{projectId}/domains`.\n *\n * @internal Exported only for unit testing; not part of the public API surface.\n */\nexport class VercelDomainResourceProvider\n\timplements pulumi.dynamic.ResourceProvider\n{\n\tasync create(\n\t\tinputs: VercelDomainInputs,\n\t): Promise<pulumi.dynamic.CreateResult> {\n\t\tconst existing = await fetchDomain(\n\t\t\tinputs.token,\n\t\t\tinputs.teamId,\n\t\t\tinputs.projectId,\n\t\t\tinputs.name,\n\t\t);\n\n\t\tif (existing) {\n\t\t\tpulumi.log.info(\n\t\t\t\t`Adopted existing Vercel domain \"${inputs.name}\" on project ${inputs.projectId}`,\n\t\t\t);\n\n\t\t\tconst outs: VercelDomainOutputs = {\n\t\t\t\t...inputs,\n\t\t\t\tverified: existing.verified,\n\t\t\t};\n\n\t\t\treturn { id: `${inputs.projectId}/${inputs.name}`, outs };\n\t\t}\n\n\t\tconst response = await fetch(\n\t\t\t`${VERCEL_API_URL}/v10/projects/${encodeURIComponent(inputs.projectId)}/domains?teamId=${inputs.teamId}`,\n\t\t\t{\n\t\t\t\tmethod: \"POST\",\n\t\t\t\theaders: {\n\t\t\t\t\tAuthorization: `Bearer ${inputs.token}`,\n\t\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\t},\n\t\t\t\tbody: JSON.stringify({ name: inputs.name }),\n\t\t\t},\n\t\t);\n\n\t\tif (!response.ok) {\n\t\t\tthrow new Error(\n\t\t\t\t`Vercel API error creating domain \"${inputs.name}\" (${response.status}): ${await response.text()}`,\n\t\t\t);\n\t\t}\n\n\t\tconst created = (await response.json()) as VercelDomainResponse;\n\n\t\tconst outs: VercelDomainOutputs = { ...inputs, verified: created.verified };\n\n\t\treturn { id: `${inputs.projectId}/${inputs.name}`, outs };\n\t}\n\n\tasync read(\n\t\t_id: string,\n\t\tprops: VercelDomainOutputs,\n\t): Promise<pulumi.dynamic.ReadResult> {\n\t\tconst domain = await fetchDomain(\n\t\t\tprops.token,\n\t\t\tprops.teamId,\n\t\t\tprops.projectId,\n\t\t\tprops.name,\n\t\t);\n\n\t\tif (!domain) {\n\t\t\tthrow new Error(\n\t\t\t\t`Vercel domain \"${props.name}\" not found on project ${props.projectId}`,\n\t\t\t);\n\t\t}\n\n\t\treturn {\n\t\t\tid: `${props.projectId}/${props.name}`,\n\t\t\tprops: { ...props, verified: domain.verified },\n\t\t};\n\t}\n\n\t/** All fields replace (see `diff`) — this is never actually invoked. */\n\tasync update(\n\t\t_id: string,\n\t\t_olds: VercelDomainOutputs,\n\t\tnews: VercelDomainInputs,\n\t): Promise<pulumi.dynamic.UpdateResult> {\n\t\treturn { outs: { ...news, verified: false } };\n\t}\n\n\tasync delete(_id: string, props: VercelDomainOutputs): Promise<void> {\n\t\tconst response = await fetch(\n\t\t\t`${VERCEL_API_URL}/v9/projects/${encodeURIComponent(props.projectId)}/domains/${encodeURIComponent(props.name)}?teamId=${props.teamId}`,\n\t\t\t{\n\t\t\t\tmethod: \"DELETE\",\n\t\t\t\theaders: { Authorization: `Bearer ${props.token}` },\n\t\t\t},\n\t\t);\n\n\t\tif (response.status === 404) {\n\t\t\tpulumi.log.warn(\n\t\t\t\t`Vercel domain \"${props.name}\" already gone from project ${props.projectId}`,\n\t\t\t);\n\n\t\t\treturn;\n\t\t}\n\n\t\tif (!response.ok) {\n\t\t\tthrow new Error(\n\t\t\t\t`Vercel API error deleting domain \"${props.name}\" (${response.status}): ${await response.text()}`,\n\t\t\t);\n\t\t}\n\n\t\tpulumi.log.info(\n\t\t\t`Deleted Vercel domain \"${props.name}\" from project ${props.projectId}`,\n\t\t);\n\t}\n\n\t/**\n\t * A domain can only be attached to one project, so every field change replaces\n\t * rather than updates.\n\t */\n\tasync diff(\n\t\t_id: string,\n\t\tolds: VercelDomainOutputs,\n\t\tnews: VercelDomainInputs,\n\t): Promise<pulumi.dynamic.DiffResult> {\n\t\tconst replaces: string[] = [];\n\n\t\tif (olds.name !== news.name) {\n\t\t\treplaces.push(\"name\");\n\t\t}\n\n\t\tif (olds.projectId !== news.projectId) {\n\t\t\treplaces.push(\"projectId\");\n\t\t}\n\n\t\tif (olds.teamId !== news.teamId) {\n\t\t\treplaces.push(\"teamId\");\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 VercelDomainResource extends pulumi.dynamic.Resource {\n\tpublic declare readonly verified: pulumi.Output<boolean>;\n\tpublic declare readonly name: pulumi.Output<string>;\n\n\tconstructor(\n\t\tname: string,\n\t\targs: {\n\t\t\ttoken: pulumi.Input<string>;\n\t\t\tteamId: pulumi.Input<string>;\n\t\t\tprojectId: pulumi.Input<string>;\n\t\t\tname: pulumi.Input<string>;\n\t\t},\n\t\topts?: pulumi.CustomResourceOptions,\n\t) {\n\t\tsuper(\n\t\t\tnew VercelDomainResourceProvider(),\n\t\t\tname,\n\t\t\t{ ...args, verified: undefined },\n\t\t\topts,\n\t\t);\n\t}\n}\n\n/** Options type for VercelDomain — replaces Pulumi's native `provider` field. */\ntype VercelDomainOptions = Omit<pulumi.ComponentResourceOptions, \"provider\"> & {\n\t/** Vercel authentication context. */\n\tprovider: VercelProvider;\n\n\t/** Vercel project to attach the domain to. */\n\tproject: VercelProject;\n};\n\n/** Args for VercelDomain. */\nexport interface VercelDomainArgs {\n\t/** Domain name (e.g. `\"app.example.com\"`). */\n\tname: pulumi.Input<string>;\n}\n\n/**\n * Attaches a custom domain to a Vercel project, with adopt-or-create semantics.\n *\n * @example\n * ```typescript\n * new VercelDomain(\"aura-domain\", { name: \"app.example.com\" }, { provider, project });\n * ```\n */\nexport class VercelDomain extends pulumi.ComponentResource {\n\t/** `true` once the domain's DNS has been verified by Vercel. */\n\tpublic readonly verified: pulumi.Output<boolean>;\n\n\t/** The attached domain name. */\n\tpublic readonly name: pulumi.Output<string>;\n\n\tconstructor(name: string, args: VercelDomainArgs, opts: VercelDomainOptions) {\n\t\tconst { provider, project, ...pulumiOpts } = opts;\n\n\t\tsuper(\"infracraft:vercel:Domain\", name, {}, pulumiOpts);\n\n\t\tconst resource = new VercelDomainResource(\n\t\t\t`${name}-resource`,\n\t\t\t{\n\t\t\t\ttoken: provider.token,\n\t\t\t\tteamId: provider.teamId,\n\t\t\t\tprojectId: project.id,\n\t\t\t\t...args,\n\t\t\t},\n\t\t\t{ parent: this },\n\t\t);\n\n\t\tthis.verified = resource.verified;\n\t\tthis.name = resource.name;\n\n\t\tthis.registerOutputs({ verified: this.verified, name: this.name });\n\t}\n}\n"],"mappings":";;;;;;AAIA,MAAM,iBAAiB;;;;;AAMvB,MAAa,sBAAsB;;;;AAiCnC,eAAe,YACd,OACA,QACA,WACA,MACuC;CACvC,MAAM,WAAW,MAAM,MACtB,GAAG,eAAe,eAAe,mBAAmB,SAAS,EAAE,WAAW,mBAAmB,IAAI,EAAE,UAAU,UAC7G,EAAE,SAAS,EAAE,eAAe,UAAU,QAAQ,EAAE,CACjD;CAEA,IAAI,SAAS,WAAW,KACvB,OAAO;CAGR,IAAI,CAAC,SAAS,IACb,MAAM,IAAI,MACT,qCAAqC,KAAK,KAAK,SAAS,OAAO,KAAK,MAAM,SAAS,KAAK,GACzF;CAGD,OAAQ,MAAM,SAAS,KAAK;AAC7B;;;;;;;;;AAUA,IAAa,+BAAb,MAEA;CACC,MAAM,OACL,QACuC;EACvC,MAAM,WAAW,MAAM,YACtB,OAAO,OACP,OAAO,QACP,OAAO,WACP,OAAO,IACR;EAEA,IAAI,UAAU;GACb,eAAO,IAAI,KACV,mCAAmC,OAAO,KAAK,eAAe,OAAO,WACtE;GAEA,MAAM,OAA4B;IACjC,GAAG;IACH,UAAU,SAAS;GACpB;GAEA,OAAO;IAAE,IAAI,GAAG,OAAO,UAAU,GAAG,OAAO;IAAQ;GAAK;EACzD;EAEA,MAAM,WAAW,MAAM,MACtB,GAAG,eAAe,gBAAgB,mBAAmB,OAAO,SAAS,EAAE,kBAAkB,OAAO,UAChG;GACC,QAAQ;GACR,SAAS;IACR,eAAe,UAAU,OAAO;IAChC,gBAAgB;GACjB;GACA,MAAM,KAAK,UAAU,EAAE,MAAM,OAAO,KAAK,CAAC;EAC3C,CACD;EAEA,IAAI,CAAC,SAAS,IACb,MAAM,IAAI,MACT,qCAAqC,OAAO,KAAK,KAAK,SAAS,OAAO,KAAK,MAAM,SAAS,KAAK,GAChG;EAGD,MAAM,UAAW,MAAM,SAAS,KAAK;EAErC,MAAM,OAA4B;GAAE,GAAG;GAAQ,UAAU,QAAQ;EAAS;EAE1E,OAAO;GAAE,IAAI,GAAG,OAAO,UAAU,GAAG,OAAO;GAAQ;EAAK;CACzD;CAEA,MAAM,KACL,KACA,OACqC;EACrC,MAAM,SAAS,MAAM,YACpB,MAAM,OACN,MAAM,QACN,MAAM,WACN,MAAM,IACP;EAEA,IAAI,CAAC,QACJ,MAAM,IAAI,MACT,kBAAkB,MAAM,KAAK,yBAAyB,MAAM,WAC7D;EAGD,OAAO;GACN,IAAI,GAAG,MAAM,UAAU,GAAG,MAAM;GAChC,OAAO;IAAE,GAAG;IAAO,UAAU,OAAO;GAAS;EAC9C;CACD;;CAGA,MAAM,OACL,KACA,OACA,MACuC;EACvC,OAAO,EAAE,MAAM;GAAE,GAAG;GAAM,UAAU;EAAM,EAAE;CAC7C;CAEA,MAAM,OAAO,KAAa,OAA2C;EACpE,MAAM,WAAW,MAAM,MACtB,GAAG,eAAe,eAAe,mBAAmB,MAAM,SAAS,EAAE,WAAW,mBAAmB,MAAM,IAAI,EAAE,UAAU,MAAM,UAC/H;GACC,QAAQ;GACR,SAAS,EAAE,eAAe,UAAU,MAAM,QAAQ;EACnD,CACD;EAEA,IAAI,SAAS,WAAW,KAAK;GAC5B,eAAO,IAAI,KACV,kBAAkB,MAAM,KAAK,8BAA8B,MAAM,WAClE;GAEA;EACD;EAEA,IAAI,CAAC,SAAS,IACb,MAAM,IAAI,MACT,qCAAqC,MAAM,KAAK,KAAK,SAAS,OAAO,KAAK,MAAM,SAAS,KAAK,GAC/F;EAGD,eAAO,IAAI,KACV,0BAA0B,MAAM,KAAK,iBAAiB,MAAM,WAC7D;CACD;;;;;CAMA,MAAM,KACL,KACA,MACA,MACqC;EACrC,MAAM,WAAqB,CAAC;EAE5B,IAAI,KAAK,SAAS,KAAK,MACtB,SAAS,KAAK,MAAM;EAGrB,IAAI,KAAK,cAAc,KAAK,WAC3B,SAAS,KAAK,WAAW;EAG1B,IAAI,KAAK,WAAW,KAAK,QACxB,SAAS,KAAK,QAAQ;EAGvB,OAAO;GACN,SAAS,SAAS,SAAS;GAC3B;GACA,qBAAqB;EACtB;CACD;AACD;;AAGA,IAAM,uBAAN,cAAmCA,eAAO,QAAQ,SAAS;CAI1D,YACC,MACA,MAMA,MACC;EACD,MACC,IAAI,6BAA6B,GACjC,MACA;GAAE,GAAG;GAAM,UAAU;EAAU,GAC/B,IACD;CACD;AACD;;;;;;;;;AAyBA,IAAa,eAAb,cAAkCA,eAAO,kBAAkB;CAO1D,YAAY,MAAc,MAAwB,MAA2B;EAC5E,MAAM,EAAE,UAAU,SAAS,GAAG,eAAe;EAE7C,MAAM,4BAA4B,MAAM,CAAC,GAAG,UAAU;EAEtD,MAAM,WAAW,IAAI,qBACpB,GAAG,KAAK,YACR;GACC,OAAO,SAAS;GAChB,QAAQ,SAAS;GACjB,WAAW,QAAQ;GACnB,GAAG;EACJ,GACA,EAAE,QAAQ,KAAK,CAChB;EAEA,KAAK,WAAW,SAAS;EACzB,KAAK,OAAO,SAAS;EAErB,KAAK,gBAAgB;GAAE,UAAU,KAAK;GAAU,MAAM,KAAK;EAAK,CAAC;CAClE;AACD"}
|
|
1
|
+
{"version":3,"file":"domain.cjs","names":["pulumi"],"sources":["../../src/vercel/domain.ts"],"sourcesContent":["import * as pulumi from \"@pulumi/pulumi\";\nimport type { VercelProject } from \"./project\";\nimport type { VercelProvider } from \"./provider\";\n\nconst VERCEL_API_URL = \"https://api.vercel.com\";\n\n/**\n * Fallback CNAME target, used only when Vercel's domain-config endpoint returns no\n * recommendation (see {@link fetchCnameTarget}). Not the primary source of truth —\n * `VercelDomain.cnameTarget` queries Vercel's per-domain recommendation dynamically,\n * since the actual target can vary by account/domain.\n * https://vercel.com/docs/domains/working-with-domains/add-a-domain\n */\nconst VERCEL_CNAME_TARGET_FALLBACK = \"cname.vercel-dns.com\";\n\n/** Resolved inputs for the Vercel domain dynamic provider. */\nexport interface VercelDomainInputs {\n\t/** Vercel API bearer token. */\n\ttoken: string;\n\n\t/** Vercel team/org ID. */\n\tteamId: string;\n\n\t/** Vercel project ID to attach the domain to. */\n\tprojectId: string;\n\n\t/** Domain name (e.g. `\"app.example.com\"`). */\n\tname: string;\n}\n\n/** Persisted state for the Vercel domain, extending inputs with verification status. */\ninterface VercelDomainOutputs extends VercelDomainInputs {\n\t/** `true` once the domain's DNS has been verified by Vercel. */\n\tverified: boolean;\n\n\t/** The value to CNAME `name` to, per Vercel's own per-domain recommendation. */\n\tcnameTarget: string;\n}\n\n/** Vercel API response shape for a project domain. */\ninterface VercelDomainResponse {\n\tname: string;\n\tprojectId: string;\n\tverified: boolean;\n}\n\n/** A single ranked CNAME recommendation from `GET /v6/domains/{domain}/config`. */\ninterface VercelRecommendedCname {\n\trank: number;\n\tvalue: string;\n}\n\n/**\n * Fetches Vercel's recommended CNAME target for a domain from its DNS config endpoint.\n * Falls back to {@link VERCEL_CNAME_TARGET_FALLBACK} if Vercel returns no recommendation\n * (logged, since that's an unusual state worth noticing rather than silently accepting).\n */\nasync function fetchCnameTarget(\n\ttoken: string,\n\tteamId: string,\n\tname: string,\n): Promise<string> {\n\tconst response = await fetch(\n\t\t`${VERCEL_API_URL}/v6/domains/${encodeURIComponent(name)}/config?teamId=${teamId}`,\n\t\t{ headers: { Authorization: `Bearer ${token}` } },\n\t);\n\n\tif (!response.ok) {\n\t\tthrow new Error(\n\t\t\t`Vercel API error fetching domain config for \"${name}\" (${response.status}): ${await response.text()}`,\n\t\t);\n\t}\n\n\tconst { recommendedCNAME = [] } = (await response.json()) as {\n\t\trecommendedCNAME?: VercelRecommendedCname[];\n\t};\n\n\tconst preferred = [...recommendedCNAME].sort((a, b) => a.rank - b.rank)[0];\n\n\tif (!preferred) {\n\t\tpulumi.log.info(\n\t\t\t`Vercel returned no recommended CNAME for \"${name}\" — falling back to \"${VERCEL_CNAME_TARGET_FALLBACK}\"`,\n\t\t);\n\n\t\treturn VERCEL_CNAME_TARGET_FALLBACK;\n\t}\n\n\treturn preferred.value;\n}\n\n/**\n * Fetches a project domain by name. Returns `null` if not attached (404).\n */\nasync function fetchDomain(\n\ttoken: string,\n\tteamId: string,\n\tprojectId: string,\n\tname: string,\n): Promise<VercelDomainResponse | null> {\n\tconst response = await fetch(\n\t\t`${VERCEL_API_URL}/v9/projects/${encodeURIComponent(projectId)}/domains/${encodeURIComponent(name)}?teamId=${teamId}`,\n\t\t{ headers: { Authorization: `Bearer ${token}` } },\n\t);\n\n\tif (response.status === 404) {\n\t\treturn null;\n\t}\n\n\tif (!response.ok) {\n\t\tthrow new Error(\n\t\t\t`Vercel API error fetching domain \"${name}\" (${response.status}): ${await response.text()}`,\n\t\t);\n\t}\n\n\treturn (await response.json()) as VercelDomainResponse;\n}\n\n/**\n * Dynamic provider implementing adopt-or-create for a Vercel project domain.\n *\n * On `create()`, calls `GET /v9/projects/{projectId}/domains/{name}`. If found, adopts\n * the existing attachment. If 404, attaches it via `POST /v10/projects/{projectId}/domains`.\n *\n * @internal Exported only for unit testing; not part of the public API surface.\n */\nexport class VercelDomainResourceProvider\n\timplements pulumi.dynamic.ResourceProvider\n{\n\tasync create(\n\t\tinputs: VercelDomainInputs,\n\t): Promise<pulumi.dynamic.CreateResult> {\n\t\tconst existing = await fetchDomain(\n\t\t\tinputs.token,\n\t\t\tinputs.teamId,\n\t\t\tinputs.projectId,\n\t\t\tinputs.name,\n\t\t);\n\n\t\tif (existing) {\n\t\t\tpulumi.log.info(\n\t\t\t\t`Adopted existing Vercel domain \"${inputs.name}\" on project ${inputs.projectId}`,\n\t\t\t);\n\n\t\t\tconst outs: VercelDomainOutputs = {\n\t\t\t\t...inputs,\n\t\t\t\tverified: existing.verified,\n\t\t\t\tcnameTarget: await fetchCnameTarget(\n\t\t\t\t\tinputs.token,\n\t\t\t\t\tinputs.teamId,\n\t\t\t\t\tinputs.name,\n\t\t\t\t),\n\t\t\t};\n\n\t\t\treturn { id: `${inputs.projectId}/${inputs.name}`, outs };\n\t\t}\n\n\t\tconst response = await fetch(\n\t\t\t`${VERCEL_API_URL}/v10/projects/${encodeURIComponent(inputs.projectId)}/domains?teamId=${inputs.teamId}`,\n\t\t\t{\n\t\t\t\tmethod: \"POST\",\n\t\t\t\theaders: {\n\t\t\t\t\tAuthorization: `Bearer ${inputs.token}`,\n\t\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\t},\n\t\t\t\tbody: JSON.stringify({ name: inputs.name }),\n\t\t\t},\n\t\t);\n\n\t\tif (!response.ok) {\n\t\t\tthrow new Error(\n\t\t\t\t`Vercel API error creating domain \"${inputs.name}\" (${response.status}): ${await response.text()}`,\n\t\t\t);\n\t\t}\n\n\t\tconst created = (await response.json()) as VercelDomainResponse;\n\n\t\tconst outs: VercelDomainOutputs = {\n\t\t\t...inputs,\n\t\t\tverified: created.verified,\n\t\t\tcnameTarget: await fetchCnameTarget(\n\t\t\t\tinputs.token,\n\t\t\t\tinputs.teamId,\n\t\t\t\tinputs.name,\n\t\t\t),\n\t\t};\n\n\t\treturn { id: `${inputs.projectId}/${inputs.name}`, outs };\n\t}\n\n\tasync read(\n\t\t_id: string,\n\t\tprops: VercelDomainOutputs,\n\t): Promise<pulumi.dynamic.ReadResult> {\n\t\tconst domain = await fetchDomain(\n\t\t\tprops.token,\n\t\t\tprops.teamId,\n\t\t\tprops.projectId,\n\t\t\tprops.name,\n\t\t);\n\n\t\tif (!domain) {\n\t\t\tthrow new Error(\n\t\t\t\t`Vercel domain \"${props.name}\" not found on project ${props.projectId}`,\n\t\t\t);\n\t\t}\n\n\t\treturn {\n\t\t\tid: `${props.projectId}/${props.name}`,\n\t\t\tprops: {\n\t\t\t\t...props,\n\t\t\t\tverified: domain.verified,\n\t\t\t\tcnameTarget: await fetchCnameTarget(\n\t\t\t\t\tprops.token,\n\t\t\t\t\tprops.teamId,\n\t\t\t\t\tprops.name,\n\t\t\t\t),\n\t\t\t},\n\t\t};\n\t}\n\n\t/** All fields replace (see `diff`) — this is never actually invoked. */\n\tasync update(\n\t\t_id: string,\n\t\tolds: VercelDomainOutputs,\n\t\tnews: VercelDomainInputs,\n\t): Promise<pulumi.dynamic.UpdateResult> {\n\t\treturn {\n\t\t\touts: { ...news, verified: false, cnameTarget: olds.cnameTarget },\n\t\t};\n\t}\n\n\tasync delete(_id: string, props: VercelDomainOutputs): Promise<void> {\n\t\tconst response = await fetch(\n\t\t\t`${VERCEL_API_URL}/v9/projects/${encodeURIComponent(props.projectId)}/domains/${encodeURIComponent(props.name)}?teamId=${props.teamId}`,\n\t\t\t{\n\t\t\t\tmethod: \"DELETE\",\n\t\t\t\theaders: { Authorization: `Bearer ${props.token}` },\n\t\t\t},\n\t\t);\n\n\t\tif (response.status === 404) {\n\t\t\tpulumi.log.warn(\n\t\t\t\t`Vercel domain \"${props.name}\" already gone from project ${props.projectId}`,\n\t\t\t);\n\n\t\t\treturn;\n\t\t}\n\n\t\tif (!response.ok) {\n\t\t\tthrow new Error(\n\t\t\t\t`Vercel API error deleting domain \"${props.name}\" (${response.status}): ${await response.text()}`,\n\t\t\t);\n\t\t}\n\n\t\tpulumi.log.info(\n\t\t\t`Deleted Vercel domain \"${props.name}\" from project ${props.projectId}`,\n\t\t);\n\t}\n\n\t/**\n\t * A domain can only be attached to one project, so every field change replaces\n\t * rather than updates.\n\t */\n\tasync diff(\n\t\t_id: string,\n\t\tolds: VercelDomainOutputs,\n\t\tnews: VercelDomainInputs,\n\t): Promise<pulumi.dynamic.DiffResult> {\n\t\tconst replaces: string[] = [];\n\n\t\tif (olds.name !== news.name) {\n\t\t\treplaces.push(\"name\");\n\t\t}\n\n\t\tif (olds.projectId !== news.projectId) {\n\t\t\treplaces.push(\"projectId\");\n\t\t}\n\n\t\tif (olds.teamId !== news.teamId) {\n\t\t\treplaces.push(\"teamId\");\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 VercelDomainResource extends pulumi.dynamic.Resource {\n\tpublic declare readonly verified: pulumi.Output<boolean>;\n\tpublic declare readonly name: pulumi.Output<string>;\n\tpublic declare readonly cnameTarget: pulumi.Output<string>;\n\n\tconstructor(\n\t\tname: string,\n\t\targs: {\n\t\t\ttoken: pulumi.Input<string>;\n\t\t\tteamId: pulumi.Input<string>;\n\t\t\tprojectId: pulumi.Input<string>;\n\t\t\tname: pulumi.Input<string>;\n\t\t},\n\t\topts?: pulumi.CustomResourceOptions,\n\t) {\n\t\tsuper(\n\t\t\tnew VercelDomainResourceProvider(),\n\t\t\tname,\n\t\t\t{ ...args, verified: undefined, cnameTarget: undefined },\n\t\t\topts,\n\t\t);\n\t}\n}\n\n/** Options type for VercelDomain — replaces Pulumi's native `provider` field. */\ntype VercelDomainOptions = Omit<pulumi.ComponentResourceOptions, \"provider\"> & {\n\t/** Vercel authentication context. */\n\tprovider: VercelProvider;\n\n\t/** Vercel project to attach the domain to. */\n\tproject: VercelProject;\n};\n\n/** Args for VercelDomain. */\nexport interface VercelDomainArgs {\n\t/** Domain name (e.g. `\"app.example.com\"`). */\n\tname: pulumi.Input<string>;\n}\n\n/**\n * Attaches a custom domain to a Vercel project, with adopt-or-create semantics.\n *\n * @example\n * ```typescript\n * const domain = new VercelDomain(\"aura-domain\", { name: \"app.example.com\" }, { provider, project });\n *\n * // Point app.example.com's DNS CNAME at this value.\n * const cnameTarget = domain.cnameTarget;\n * ```\n */\nexport class VercelDomain extends pulumi.ComponentResource {\n\t/** `true` once the domain's DNS has been verified by Vercel. */\n\tpublic readonly verified: pulumi.Output<boolean>;\n\n\t/** The attached domain name. */\n\tpublic readonly name: pulumi.Output<string>;\n\n\t/**\n\t * The value to CNAME `name` to — Vercel's own recommendation for this specific\n\t * domain, fetched fresh from its DNS config endpoint (falls back to a static default\n\t * only if Vercel returns no recommendation).\n\t */\n\tpublic readonly cnameTarget: pulumi.Output<string>;\n\n\tconstructor(name: string, args: VercelDomainArgs, opts: VercelDomainOptions) {\n\t\tconst { provider, project, ...pulumiOpts } = opts;\n\n\t\tsuper(\"infracraft:vercel:Domain\", name, {}, pulumiOpts);\n\n\t\tconst resource = new VercelDomainResource(\n\t\t\t`${name}-resource`,\n\t\t\t{\n\t\t\t\ttoken: provider.token,\n\t\t\t\tteamId: provider.teamId,\n\t\t\t\tprojectId: project.id,\n\t\t\t\t...args,\n\t\t\t},\n\t\t\t{ parent: this },\n\t\t);\n\n\t\tthis.verified = resource.verified;\n\t\tthis.name = resource.name;\n\t\tthis.cnameTarget = resource.cnameTarget;\n\n\t\tthis.registerOutputs({\n\t\t\tverified: this.verified,\n\t\t\tname: this.name,\n\t\t\tcnameTarget: this.cnameTarget,\n\t\t});\n\t}\n}\n"],"mappings":"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"}
|
package/dist/vercel/domain.d.cts
CHANGED
|
@@ -4,11 +4,6 @@ import { VercelProject } from "./project.cjs";
|
|
|
4
4
|
import * as pulumi from "@pulumi/pulumi";
|
|
5
5
|
|
|
6
6
|
//#region src/vercel/domain.d.ts
|
|
7
|
-
/**
|
|
8
|
-
* Vercel's documented CNAME target for attaching a subdomain to a project.
|
|
9
|
-
* https://vercel.com/docs/domains/working-with-domains/add-a-domain
|
|
10
|
-
*/
|
|
11
|
-
declare const VERCEL_CNAME_TARGET = "cname.vercel-dns.com";
|
|
12
7
|
/** Resolved inputs for the Vercel domain dynamic provider. */
|
|
13
8
|
interface VercelDomainInputs {
|
|
14
9
|
/** Vercel API bearer token. */
|
|
@@ -24,6 +19,8 @@ interface VercelDomainInputs {
|
|
|
24
19
|
interface VercelDomainOutputs extends VercelDomainInputs {
|
|
25
20
|
/** `true` once the domain's DNS has been verified by Vercel. */
|
|
26
21
|
verified: boolean;
|
|
22
|
+
/** The value to CNAME `name` to, per Vercel's own per-domain recommendation. */
|
|
23
|
+
cnameTarget: string;
|
|
27
24
|
}
|
|
28
25
|
/**
|
|
29
26
|
* Dynamic provider implementing adopt-or-create for a Vercel project domain.
|
|
@@ -37,7 +34,7 @@ declare class VercelDomainResourceProvider implements pulumi.dynamic.ResourcePro
|
|
|
37
34
|
create(inputs: VercelDomainInputs): Promise<pulumi.dynamic.CreateResult>;
|
|
38
35
|
read(_id: string, props: VercelDomainOutputs): Promise<pulumi.dynamic.ReadResult>;
|
|
39
36
|
/** All fields replace (see `diff`) — this is never actually invoked. */
|
|
40
|
-
update(_id: string,
|
|
37
|
+
update(_id: string, olds: VercelDomainOutputs, news: VercelDomainInputs): Promise<pulumi.dynamic.UpdateResult>;
|
|
41
38
|
delete(_id: string, props: VercelDomainOutputs): Promise<void>;
|
|
42
39
|
/**
|
|
43
40
|
* A domain can only be attached to one project, so every field change replaces
|
|
@@ -60,7 +57,10 @@ interface VercelDomainArgs {
|
|
|
60
57
|
*
|
|
61
58
|
* @example
|
|
62
59
|
* ```typescript
|
|
63
|
-
* new VercelDomain("aura-domain", { name: "app.example.com" }, { provider, project });
|
|
60
|
+
* const domain = new VercelDomain("aura-domain", { name: "app.example.com" }, { provider, project });
|
|
61
|
+
*
|
|
62
|
+
* // Point app.example.com's DNS CNAME at this value.
|
|
63
|
+
* const cnameTarget = domain.cnameTarget;
|
|
64
64
|
* ```
|
|
65
65
|
*/
|
|
66
66
|
declare class VercelDomain extends pulumi.ComponentResource {
|
|
@@ -68,8 +68,14 @@ declare class VercelDomain extends pulumi.ComponentResource {
|
|
|
68
68
|
readonly verified: pulumi.Output<boolean>;
|
|
69
69
|
/** The attached domain name. */
|
|
70
70
|
readonly name: pulumi.Output<string>;
|
|
71
|
+
/**
|
|
72
|
+
* The value to CNAME `name` to — Vercel's own recommendation for this specific
|
|
73
|
+
* domain, fetched fresh from its DNS config endpoint (falls back to a static default
|
|
74
|
+
* only if Vercel returns no recommendation).
|
|
75
|
+
*/
|
|
76
|
+
readonly cnameTarget: pulumi.Output<string>;
|
|
71
77
|
constructor(name: string, args: VercelDomainArgs, opts: VercelDomainOptions);
|
|
72
78
|
}
|
|
73
79
|
//#endregion
|
|
74
|
-
export {
|
|
80
|
+
export { VercelDomain, VercelDomainArgs, VercelDomainInputs, VercelDomainResourceProvider };
|
|
75
81
|
//# sourceMappingURL=domain.d.cts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"domain.d.cts","names":[],"sources":["../../src/vercel/domain.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"domain.d.cts","names":[],"sources":["../../src/vercel/domain.ts"],"mappings":";;;;;;;UAgBiB,kBAAA;;EAEhB,KAAA;EAFkC;EAKlC,MAAA;EALkC;EAQlC,SAAA;EAHA;EAMA,IAAA;AAAA;;UAIS,mBAAA,SAA4B,kBAAkB;EAA9C;EAET,QAAA;;EAGA,WAAA;AAAA;;;;AAAW;AAyFZ;;;;cAAa,4BAAA,YACD,MAAA,CAAO,OAAA,CAAQ,gBAAA;EAEpB,MAAA,CACL,MAAA,EAAQ,kBAAA,GACN,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,YAAA;EA2DpB,IAAA,CACL,GAAA,UACA,KAAA,EAAO,mBAAA,GACL,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,UAAA;EAAf;EA6BL,MAAA,CACL,GAAA,UACA,IAAA,EAAM,mBAAA,EACN,IAAA,EAAM,kBAAA,GACJ,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,YAAA;EAMpB,MAAA,CAAO,GAAA,UAAa,KAAA,EAAO,mBAAA,GAAsB,OAAA;EAPhD;;;;EAuCD,IAAA,CACL,GAAA,UACA,IAAA,EAAM,mBAAA,EACN,IAAA,EAAM,kBAAA,GACJ,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,UAAA;AAAA;;KAiDtB,mBAAA,GAAsB,IAAA,CAAK,MAAA,CAAO,wBAAA;EAjDnC,qCAmDH,QAAA,EAAU,cAAA,EAhMgC;EAmM1C,OAAA,EAAS,aAAA;AAAA;;UAIO,gBAAA;EArMV;EAuMN,IAAA,EAAM,MAAA,CAAO,KAAK;AAAA;;;;;;;;;;;;cAcN,YAAA,SAAqB,MAAA,CAAO,iBAAA;EAxHlC;EAAA,SA0HU,QAAA,EAAU,MAAA,CAAO,MAAA;EAxH1B;EAAA,SA2HS,IAAA,EAAM,MAAA,CAAO,MAAA;EA1HtB;;;;;EAAA,SAiIS,WAAA,EAAa,MAAA,CAAO,MAAA;cAExB,IAAA,UAAc,IAAA,EAAM,gBAAA,EAAkB,IAAA,EAAM,mBAAA;AAAA"}
|
package/dist/vercel/domain.d.mts
CHANGED
|
@@ -4,11 +4,6 @@ import { VercelProject } from "./project.mjs";
|
|
|
4
4
|
import * as pulumi from "@pulumi/pulumi";
|
|
5
5
|
|
|
6
6
|
//#region src/vercel/domain.d.ts
|
|
7
|
-
/**
|
|
8
|
-
* Vercel's documented CNAME target for attaching a subdomain to a project.
|
|
9
|
-
* https://vercel.com/docs/domains/working-with-domains/add-a-domain
|
|
10
|
-
*/
|
|
11
|
-
declare const VERCEL_CNAME_TARGET = "cname.vercel-dns.com";
|
|
12
7
|
/** Resolved inputs for the Vercel domain dynamic provider. */
|
|
13
8
|
interface VercelDomainInputs {
|
|
14
9
|
/** Vercel API bearer token. */
|
|
@@ -24,6 +19,8 @@ interface VercelDomainInputs {
|
|
|
24
19
|
interface VercelDomainOutputs extends VercelDomainInputs {
|
|
25
20
|
/** `true` once the domain's DNS has been verified by Vercel. */
|
|
26
21
|
verified: boolean;
|
|
22
|
+
/** The value to CNAME `name` to, per Vercel's own per-domain recommendation. */
|
|
23
|
+
cnameTarget: string;
|
|
27
24
|
}
|
|
28
25
|
/**
|
|
29
26
|
* Dynamic provider implementing adopt-or-create for a Vercel project domain.
|
|
@@ -37,7 +34,7 @@ declare class VercelDomainResourceProvider implements pulumi.dynamic.ResourcePro
|
|
|
37
34
|
create(inputs: VercelDomainInputs): Promise<pulumi.dynamic.CreateResult>;
|
|
38
35
|
read(_id: string, props: VercelDomainOutputs): Promise<pulumi.dynamic.ReadResult>;
|
|
39
36
|
/** All fields replace (see `diff`) — this is never actually invoked. */
|
|
40
|
-
update(_id: string,
|
|
37
|
+
update(_id: string, olds: VercelDomainOutputs, news: VercelDomainInputs): Promise<pulumi.dynamic.UpdateResult>;
|
|
41
38
|
delete(_id: string, props: VercelDomainOutputs): Promise<void>;
|
|
42
39
|
/**
|
|
43
40
|
* A domain can only be attached to one project, so every field change replaces
|
|
@@ -60,7 +57,10 @@ interface VercelDomainArgs {
|
|
|
60
57
|
*
|
|
61
58
|
* @example
|
|
62
59
|
* ```typescript
|
|
63
|
-
* new VercelDomain("aura-domain", { name: "app.example.com" }, { provider, project });
|
|
60
|
+
* const domain = new VercelDomain("aura-domain", { name: "app.example.com" }, { provider, project });
|
|
61
|
+
*
|
|
62
|
+
* // Point app.example.com's DNS CNAME at this value.
|
|
63
|
+
* const cnameTarget = domain.cnameTarget;
|
|
64
64
|
* ```
|
|
65
65
|
*/
|
|
66
66
|
declare class VercelDomain extends pulumi.ComponentResource {
|
|
@@ -68,8 +68,14 @@ declare class VercelDomain extends pulumi.ComponentResource {
|
|
|
68
68
|
readonly verified: pulumi.Output<boolean>;
|
|
69
69
|
/** The attached domain name. */
|
|
70
70
|
readonly name: pulumi.Output<string>;
|
|
71
|
+
/**
|
|
72
|
+
* The value to CNAME `name` to — Vercel's own recommendation for this specific
|
|
73
|
+
* domain, fetched fresh from its DNS config endpoint (falls back to a static default
|
|
74
|
+
* only if Vercel returns no recommendation).
|
|
75
|
+
*/
|
|
76
|
+
readonly cnameTarget: pulumi.Output<string>;
|
|
71
77
|
constructor(name: string, args: VercelDomainArgs, opts: VercelDomainOptions);
|
|
72
78
|
}
|
|
73
79
|
//#endregion
|
|
74
|
-
export {
|
|
80
|
+
export { VercelDomain, VercelDomainArgs, VercelDomainInputs, VercelDomainResourceProvider };
|
|
75
81
|
//# sourceMappingURL=domain.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"domain.d.mts","names":[],"sources":["../../src/vercel/domain.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"domain.d.mts","names":[],"sources":["../../src/vercel/domain.ts"],"mappings":";;;;;;;UAgBiB,kBAAA;;EAEhB,KAAA;EAFkC;EAKlC,MAAA;EALkC;EAQlC,SAAA;EAHA;EAMA,IAAA;AAAA;;UAIS,mBAAA,SAA4B,kBAAkB;EAA9C;EAET,QAAA;;EAGA,WAAA;AAAA;;;;AAAW;AAyFZ;;;;cAAa,4BAAA,YACD,MAAA,CAAO,OAAA,CAAQ,gBAAA;EAEpB,MAAA,CACL,MAAA,EAAQ,kBAAA,GACN,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,YAAA;EA2DpB,IAAA,CACL,GAAA,UACA,KAAA,EAAO,mBAAA,GACL,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,UAAA;EAAf;EA6BL,MAAA,CACL,GAAA,UACA,IAAA,EAAM,mBAAA,EACN,IAAA,EAAM,kBAAA,GACJ,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,YAAA;EAMpB,MAAA,CAAO,GAAA,UAAa,KAAA,EAAO,mBAAA,GAAsB,OAAA;EAPhD;;;;EAuCD,IAAA,CACL,GAAA,UACA,IAAA,EAAM,mBAAA,EACN,IAAA,EAAM,kBAAA,GACJ,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,UAAA;AAAA;;KAiDtB,mBAAA,GAAsB,IAAA,CAAK,MAAA,CAAO,wBAAA;EAjDnC,qCAmDH,QAAA,EAAU,cAAA,EAhMgC;EAmM1C,OAAA,EAAS,aAAA;AAAA;;UAIO,gBAAA;EArMV;EAuMN,IAAA,EAAM,MAAA,CAAO,KAAK;AAAA;;;;;;;;;;;;cAcN,YAAA,SAAqB,MAAA,CAAO,iBAAA;EAxHlC;EAAA,SA0HU,QAAA,EAAU,MAAA,CAAO,MAAA;EAxH1B;EAAA,SA2HS,IAAA,EAAM,MAAA,CAAO,MAAA;EA1HtB;;;;;EAAA,SAiIS,WAAA,EAAa,MAAA,CAAO,MAAA;cAExB,IAAA,UAAc,IAAA,EAAM,gBAAA,EAAkB,IAAA,EAAM,mBAAA;AAAA"}
|
package/dist/vercel/domain.mjs
CHANGED
|
@@ -4,10 +4,29 @@ import * as pulumi from "@pulumi/pulumi";
|
|
|
4
4
|
//#region src/vercel/domain.ts
|
|
5
5
|
const VERCEL_API_URL = "https://api.vercel.com";
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
7
|
+
* Fallback CNAME target, used only when Vercel's domain-config endpoint returns no
|
|
8
|
+
* recommendation (see {@link fetchCnameTarget}). Not the primary source of truth —
|
|
9
|
+
* `VercelDomain.cnameTarget` queries Vercel's per-domain recommendation dynamically,
|
|
10
|
+
* since the actual target can vary by account/domain.
|
|
8
11
|
* https://vercel.com/docs/domains/working-with-domains/add-a-domain
|
|
9
12
|
*/
|
|
10
|
-
const
|
|
13
|
+
const VERCEL_CNAME_TARGET_FALLBACK = "cname.vercel-dns.com";
|
|
14
|
+
/**
|
|
15
|
+
* Fetches Vercel's recommended CNAME target for a domain from its DNS config endpoint.
|
|
16
|
+
* Falls back to {@link VERCEL_CNAME_TARGET_FALLBACK} if Vercel returns no recommendation
|
|
17
|
+
* (logged, since that's an unusual state worth noticing rather than silently accepting).
|
|
18
|
+
*/
|
|
19
|
+
async function fetchCnameTarget(token, teamId, name) {
|
|
20
|
+
const response = await fetch(`${VERCEL_API_URL}/v6/domains/${encodeURIComponent(name)}/config?teamId=${teamId}`, { headers: { Authorization: `Bearer ${token}` } });
|
|
21
|
+
if (!response.ok) throw new Error(`Vercel API error fetching domain config for "${name}" (${response.status}): ${await response.text()}`);
|
|
22
|
+
const { recommendedCNAME = [] } = await response.json();
|
|
23
|
+
const preferred = [...recommendedCNAME].sort((a, b) => a.rank - b.rank)[0];
|
|
24
|
+
if (!preferred) {
|
|
25
|
+
pulumi.log.info(`Vercel returned no recommended CNAME for "${name}" — falling back to "${VERCEL_CNAME_TARGET_FALLBACK}"`);
|
|
26
|
+
return VERCEL_CNAME_TARGET_FALLBACK;
|
|
27
|
+
}
|
|
28
|
+
return preferred.value;
|
|
29
|
+
}
|
|
11
30
|
/**
|
|
12
31
|
* Fetches a project domain by name. Returns `null` if not attached (404).
|
|
13
32
|
*/
|
|
@@ -32,7 +51,8 @@ var VercelDomainResourceProvider = class {
|
|
|
32
51
|
pulumi.log.info(`Adopted existing Vercel domain "${inputs.name}" on project ${inputs.projectId}`);
|
|
33
52
|
const outs = {
|
|
34
53
|
...inputs,
|
|
35
|
-
verified: existing.verified
|
|
54
|
+
verified: existing.verified,
|
|
55
|
+
cnameTarget: await fetchCnameTarget(inputs.token, inputs.teamId, inputs.name)
|
|
36
56
|
};
|
|
37
57
|
return {
|
|
38
58
|
id: `${inputs.projectId}/${inputs.name}`,
|
|
@@ -51,7 +71,8 @@ var VercelDomainResourceProvider = class {
|
|
|
51
71
|
const created = await response.json();
|
|
52
72
|
const outs = {
|
|
53
73
|
...inputs,
|
|
54
|
-
verified: created.verified
|
|
74
|
+
verified: created.verified,
|
|
75
|
+
cnameTarget: await fetchCnameTarget(inputs.token, inputs.teamId, inputs.name)
|
|
55
76
|
};
|
|
56
77
|
return {
|
|
57
78
|
id: `${inputs.projectId}/${inputs.name}`,
|
|
@@ -65,15 +86,17 @@ var VercelDomainResourceProvider = class {
|
|
|
65
86
|
id: `${props.projectId}/${props.name}`,
|
|
66
87
|
props: {
|
|
67
88
|
...props,
|
|
68
|
-
verified: domain.verified
|
|
89
|
+
verified: domain.verified,
|
|
90
|
+
cnameTarget: await fetchCnameTarget(props.token, props.teamId, props.name)
|
|
69
91
|
}
|
|
70
92
|
};
|
|
71
93
|
}
|
|
72
94
|
/** All fields replace (see `diff`) — this is never actually invoked. */
|
|
73
|
-
async update(_id,
|
|
95
|
+
async update(_id, olds, news) {
|
|
74
96
|
return { outs: {
|
|
75
97
|
...news,
|
|
76
|
-
verified: false
|
|
98
|
+
verified: false,
|
|
99
|
+
cnameTarget: olds.cnameTarget
|
|
77
100
|
} };
|
|
78
101
|
}
|
|
79
102
|
async delete(_id, props) {
|
|
@@ -109,7 +132,8 @@ var VercelDomainResource = class extends pulumi.dynamic.Resource {
|
|
|
109
132
|
constructor(name, args, opts) {
|
|
110
133
|
super(new VercelDomainResourceProvider(), name, {
|
|
111
134
|
...args,
|
|
112
|
-
verified: void 0
|
|
135
|
+
verified: void 0,
|
|
136
|
+
cnameTarget: void 0
|
|
113
137
|
}, opts);
|
|
114
138
|
}
|
|
115
139
|
};
|
|
@@ -118,7 +142,10 @@ var VercelDomainResource = class extends pulumi.dynamic.Resource {
|
|
|
118
142
|
*
|
|
119
143
|
* @example
|
|
120
144
|
* ```typescript
|
|
121
|
-
* new VercelDomain("aura-domain", { name: "app.example.com" }, { provider, project });
|
|
145
|
+
* const domain = new VercelDomain("aura-domain", { name: "app.example.com" }, { provider, project });
|
|
146
|
+
*
|
|
147
|
+
* // Point app.example.com's DNS CNAME at this value.
|
|
148
|
+
* const cnameTarget = domain.cnameTarget;
|
|
122
149
|
* ```
|
|
123
150
|
*/
|
|
124
151
|
var VercelDomain = class extends pulumi.ComponentResource {
|
|
@@ -133,13 +160,15 @@ var VercelDomain = class extends pulumi.ComponentResource {
|
|
|
133
160
|
}, { parent: this });
|
|
134
161
|
this.verified = resource.verified;
|
|
135
162
|
this.name = resource.name;
|
|
163
|
+
this.cnameTarget = resource.cnameTarget;
|
|
136
164
|
this.registerOutputs({
|
|
137
165
|
verified: this.verified,
|
|
138
|
-
name: this.name
|
|
166
|
+
name: this.name,
|
|
167
|
+
cnameTarget: this.cnameTarget
|
|
139
168
|
});
|
|
140
169
|
}
|
|
141
170
|
};
|
|
142
171
|
|
|
143
172
|
//#endregion
|
|
144
|
-
export {
|
|
173
|
+
export { VercelDomain, VercelDomainResourceProvider };
|
|
145
174
|
//# sourceMappingURL=domain.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"domain.mjs","names":[],"sources":["../../src/vercel/domain.ts"],"sourcesContent":["import * as pulumi from \"@pulumi/pulumi\";\nimport type { VercelProject } from \"./project\";\nimport type { VercelProvider } from \"./provider\";\n\nconst VERCEL_API_URL = \"https://api.vercel.com\";\n\n/**\n * Vercel's documented CNAME target for attaching a subdomain to a project.\n * https://vercel.com/docs/domains/working-with-domains/add-a-domain\n */\nexport const VERCEL_CNAME_TARGET = \"cname.vercel-dns.com\";\n\n/** Resolved inputs for the Vercel domain dynamic provider. */\nexport interface VercelDomainInputs {\n\t/** Vercel API bearer token. */\n\ttoken: string;\n\n\t/** Vercel team/org ID. */\n\tteamId: string;\n\n\t/** Vercel project ID to attach the domain to. */\n\tprojectId: string;\n\n\t/** Domain name (e.g. `\"app.example.com\"`). */\n\tname: string;\n}\n\n/** Persisted state for the Vercel domain, extending inputs with verification status. */\ninterface VercelDomainOutputs extends VercelDomainInputs {\n\t/** `true` once the domain's DNS has been verified by Vercel. */\n\tverified: boolean;\n}\n\n/** Vercel API response shape for a project domain. */\ninterface VercelDomainResponse {\n\tname: string;\n\tprojectId: string;\n\tverified: boolean;\n}\n\n/**\n * Fetches a project domain by name. Returns `null` if not attached (404).\n */\nasync function fetchDomain(\n\ttoken: string,\n\tteamId: string,\n\tprojectId: string,\n\tname: string,\n): Promise<VercelDomainResponse | null> {\n\tconst response = await fetch(\n\t\t`${VERCEL_API_URL}/v9/projects/${encodeURIComponent(projectId)}/domains/${encodeURIComponent(name)}?teamId=${teamId}`,\n\t\t{ headers: { Authorization: `Bearer ${token}` } },\n\t);\n\n\tif (response.status === 404) {\n\t\treturn null;\n\t}\n\n\tif (!response.ok) {\n\t\tthrow new Error(\n\t\t\t`Vercel API error fetching domain \"${name}\" (${response.status}): ${await response.text()}`,\n\t\t);\n\t}\n\n\treturn (await response.json()) as VercelDomainResponse;\n}\n\n/**\n * Dynamic provider implementing adopt-or-create for a Vercel project domain.\n *\n * On `create()`, calls `GET /v9/projects/{projectId}/domains/{name}`. If found, adopts\n * the existing attachment. If 404, attaches it via `POST /v10/projects/{projectId}/domains`.\n *\n * @internal Exported only for unit testing; not part of the public API surface.\n */\nexport class VercelDomainResourceProvider\n\timplements pulumi.dynamic.ResourceProvider\n{\n\tasync create(\n\t\tinputs: VercelDomainInputs,\n\t): Promise<pulumi.dynamic.CreateResult> {\n\t\tconst existing = await fetchDomain(\n\t\t\tinputs.token,\n\t\t\tinputs.teamId,\n\t\t\tinputs.projectId,\n\t\t\tinputs.name,\n\t\t);\n\n\t\tif (existing) {\n\t\t\tpulumi.log.info(\n\t\t\t\t`Adopted existing Vercel domain \"${inputs.name}\" on project ${inputs.projectId}`,\n\t\t\t);\n\n\t\t\tconst outs: VercelDomainOutputs = {\n\t\t\t\t...inputs,\n\t\t\t\tverified: existing.verified,\n\t\t\t};\n\n\t\t\treturn { id: `${inputs.projectId}/${inputs.name}`, outs };\n\t\t}\n\n\t\tconst response = await fetch(\n\t\t\t`${VERCEL_API_URL}/v10/projects/${encodeURIComponent(inputs.projectId)}/domains?teamId=${inputs.teamId}`,\n\t\t\t{\n\t\t\t\tmethod: \"POST\",\n\t\t\t\theaders: {\n\t\t\t\t\tAuthorization: `Bearer ${inputs.token}`,\n\t\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\t},\n\t\t\t\tbody: JSON.stringify({ name: inputs.name }),\n\t\t\t},\n\t\t);\n\n\t\tif (!response.ok) {\n\t\t\tthrow new Error(\n\t\t\t\t`Vercel API error creating domain \"${inputs.name}\" (${response.status}): ${await response.text()}`,\n\t\t\t);\n\t\t}\n\n\t\tconst created = (await response.json()) as VercelDomainResponse;\n\n\t\tconst outs: VercelDomainOutputs = { ...inputs, verified: created.verified };\n\n\t\treturn { id: `${inputs.projectId}/${inputs.name}`, outs };\n\t}\n\n\tasync read(\n\t\t_id: string,\n\t\tprops: VercelDomainOutputs,\n\t): Promise<pulumi.dynamic.ReadResult> {\n\t\tconst domain = await fetchDomain(\n\t\t\tprops.token,\n\t\t\tprops.teamId,\n\t\t\tprops.projectId,\n\t\t\tprops.name,\n\t\t);\n\n\t\tif (!domain) {\n\t\t\tthrow new Error(\n\t\t\t\t`Vercel domain \"${props.name}\" not found on project ${props.projectId}`,\n\t\t\t);\n\t\t}\n\n\t\treturn {\n\t\t\tid: `${props.projectId}/${props.name}`,\n\t\t\tprops: { ...props, verified: domain.verified },\n\t\t};\n\t}\n\n\t/** All fields replace (see `diff`) — this is never actually invoked. */\n\tasync update(\n\t\t_id: string,\n\t\t_olds: VercelDomainOutputs,\n\t\tnews: VercelDomainInputs,\n\t): Promise<pulumi.dynamic.UpdateResult> {\n\t\treturn { outs: { ...news, verified: false } };\n\t}\n\n\tasync delete(_id: string, props: VercelDomainOutputs): Promise<void> {\n\t\tconst response = await fetch(\n\t\t\t`${VERCEL_API_URL}/v9/projects/${encodeURIComponent(props.projectId)}/domains/${encodeURIComponent(props.name)}?teamId=${props.teamId}`,\n\t\t\t{\n\t\t\t\tmethod: \"DELETE\",\n\t\t\t\theaders: { Authorization: `Bearer ${props.token}` },\n\t\t\t},\n\t\t);\n\n\t\tif (response.status === 404) {\n\t\t\tpulumi.log.warn(\n\t\t\t\t`Vercel domain \"${props.name}\" already gone from project ${props.projectId}`,\n\t\t\t);\n\n\t\t\treturn;\n\t\t}\n\n\t\tif (!response.ok) {\n\t\t\tthrow new Error(\n\t\t\t\t`Vercel API error deleting domain \"${props.name}\" (${response.status}): ${await response.text()}`,\n\t\t\t);\n\t\t}\n\n\t\tpulumi.log.info(\n\t\t\t`Deleted Vercel domain \"${props.name}\" from project ${props.projectId}`,\n\t\t);\n\t}\n\n\t/**\n\t * A domain can only be attached to one project, so every field change replaces\n\t * rather than updates.\n\t */\n\tasync diff(\n\t\t_id: string,\n\t\tolds: VercelDomainOutputs,\n\t\tnews: VercelDomainInputs,\n\t): Promise<pulumi.dynamic.DiffResult> {\n\t\tconst replaces: string[] = [];\n\n\t\tif (olds.name !== news.name) {\n\t\t\treplaces.push(\"name\");\n\t\t}\n\n\t\tif (olds.projectId !== news.projectId) {\n\t\t\treplaces.push(\"projectId\");\n\t\t}\n\n\t\tif (olds.teamId !== news.teamId) {\n\t\t\treplaces.push(\"teamId\");\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 VercelDomainResource extends pulumi.dynamic.Resource {\n\tpublic declare readonly verified: pulumi.Output<boolean>;\n\tpublic declare readonly name: pulumi.Output<string>;\n\n\tconstructor(\n\t\tname: string,\n\t\targs: {\n\t\t\ttoken: pulumi.Input<string>;\n\t\t\tteamId: pulumi.Input<string>;\n\t\t\tprojectId: pulumi.Input<string>;\n\t\t\tname: pulumi.Input<string>;\n\t\t},\n\t\topts?: pulumi.CustomResourceOptions,\n\t) {\n\t\tsuper(\n\t\t\tnew VercelDomainResourceProvider(),\n\t\t\tname,\n\t\t\t{ ...args, verified: undefined },\n\t\t\topts,\n\t\t);\n\t}\n}\n\n/** Options type for VercelDomain — replaces Pulumi's native `provider` field. */\ntype VercelDomainOptions = Omit<pulumi.ComponentResourceOptions, \"provider\"> & {\n\t/** Vercel authentication context. */\n\tprovider: VercelProvider;\n\n\t/** Vercel project to attach the domain to. */\n\tproject: VercelProject;\n};\n\n/** Args for VercelDomain. */\nexport interface VercelDomainArgs {\n\t/** Domain name (e.g. `\"app.example.com\"`). */\n\tname: pulumi.Input<string>;\n}\n\n/**\n * Attaches a custom domain to a Vercel project, with adopt-or-create semantics.\n *\n * @example\n * ```typescript\n * new VercelDomain(\"aura-domain\", { name: \"app.example.com\" }, { provider, project });\n * ```\n */\nexport class VercelDomain extends pulumi.ComponentResource {\n\t/** `true` once the domain's DNS has been verified by Vercel. */\n\tpublic readonly verified: pulumi.Output<boolean>;\n\n\t/** The attached domain name. */\n\tpublic readonly name: pulumi.Output<string>;\n\n\tconstructor(name: string, args: VercelDomainArgs, opts: VercelDomainOptions) {\n\t\tconst { provider, project, ...pulumiOpts } = opts;\n\n\t\tsuper(\"infracraft:vercel:Domain\", name, {}, pulumiOpts);\n\n\t\tconst resource = new VercelDomainResource(\n\t\t\t`${name}-resource`,\n\t\t\t{\n\t\t\t\ttoken: provider.token,\n\t\t\t\tteamId: provider.teamId,\n\t\t\t\tprojectId: project.id,\n\t\t\t\t...args,\n\t\t\t},\n\t\t\t{ parent: this },\n\t\t);\n\n\t\tthis.verified = resource.verified;\n\t\tthis.name = resource.name;\n\n\t\tthis.registerOutputs({ verified: this.verified, name: this.name });\n\t}\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"domain.mjs","names":[],"sources":["../../src/vercel/domain.ts"],"sourcesContent":["import * as pulumi from \"@pulumi/pulumi\";\nimport type { VercelProject } from \"./project\";\nimport type { VercelProvider } from \"./provider\";\n\nconst VERCEL_API_URL = \"https://api.vercel.com\";\n\n/**\n * Fallback CNAME target, used only when Vercel's domain-config endpoint returns no\n * recommendation (see {@link fetchCnameTarget}). Not the primary source of truth —\n * `VercelDomain.cnameTarget` queries Vercel's per-domain recommendation dynamically,\n * since the actual target can vary by account/domain.\n * https://vercel.com/docs/domains/working-with-domains/add-a-domain\n */\nconst VERCEL_CNAME_TARGET_FALLBACK = \"cname.vercel-dns.com\";\n\n/** Resolved inputs for the Vercel domain dynamic provider. */\nexport interface VercelDomainInputs {\n\t/** Vercel API bearer token. */\n\ttoken: string;\n\n\t/** Vercel team/org ID. */\n\tteamId: string;\n\n\t/** Vercel project ID to attach the domain to. */\n\tprojectId: string;\n\n\t/** Domain name (e.g. `\"app.example.com\"`). */\n\tname: string;\n}\n\n/** Persisted state for the Vercel domain, extending inputs with verification status. */\ninterface VercelDomainOutputs extends VercelDomainInputs {\n\t/** `true` once the domain's DNS has been verified by Vercel. */\n\tverified: boolean;\n\n\t/** The value to CNAME `name` to, per Vercel's own per-domain recommendation. */\n\tcnameTarget: string;\n}\n\n/** Vercel API response shape for a project domain. */\ninterface VercelDomainResponse {\n\tname: string;\n\tprojectId: string;\n\tverified: boolean;\n}\n\n/** A single ranked CNAME recommendation from `GET /v6/domains/{domain}/config`. */\ninterface VercelRecommendedCname {\n\trank: number;\n\tvalue: string;\n}\n\n/**\n * Fetches Vercel's recommended CNAME target for a domain from its DNS config endpoint.\n * Falls back to {@link VERCEL_CNAME_TARGET_FALLBACK} if Vercel returns no recommendation\n * (logged, since that's an unusual state worth noticing rather than silently accepting).\n */\nasync function fetchCnameTarget(\n\ttoken: string,\n\tteamId: string,\n\tname: string,\n): Promise<string> {\n\tconst response = await fetch(\n\t\t`${VERCEL_API_URL}/v6/domains/${encodeURIComponent(name)}/config?teamId=${teamId}`,\n\t\t{ headers: { Authorization: `Bearer ${token}` } },\n\t);\n\n\tif (!response.ok) {\n\t\tthrow new Error(\n\t\t\t`Vercel API error fetching domain config for \"${name}\" (${response.status}): ${await response.text()}`,\n\t\t);\n\t}\n\n\tconst { recommendedCNAME = [] } = (await response.json()) as {\n\t\trecommendedCNAME?: VercelRecommendedCname[];\n\t};\n\n\tconst preferred = [...recommendedCNAME].sort((a, b) => a.rank - b.rank)[0];\n\n\tif (!preferred) {\n\t\tpulumi.log.info(\n\t\t\t`Vercel returned no recommended CNAME for \"${name}\" — falling back to \"${VERCEL_CNAME_TARGET_FALLBACK}\"`,\n\t\t);\n\n\t\treturn VERCEL_CNAME_TARGET_FALLBACK;\n\t}\n\n\treturn preferred.value;\n}\n\n/**\n * Fetches a project domain by name. Returns `null` if not attached (404).\n */\nasync function fetchDomain(\n\ttoken: string,\n\tteamId: string,\n\tprojectId: string,\n\tname: string,\n): Promise<VercelDomainResponse | null> {\n\tconst response = await fetch(\n\t\t`${VERCEL_API_URL}/v9/projects/${encodeURIComponent(projectId)}/domains/${encodeURIComponent(name)}?teamId=${teamId}`,\n\t\t{ headers: { Authorization: `Bearer ${token}` } },\n\t);\n\n\tif (response.status === 404) {\n\t\treturn null;\n\t}\n\n\tif (!response.ok) {\n\t\tthrow new Error(\n\t\t\t`Vercel API error fetching domain \"${name}\" (${response.status}): ${await response.text()}`,\n\t\t);\n\t}\n\n\treturn (await response.json()) as VercelDomainResponse;\n}\n\n/**\n * Dynamic provider implementing adopt-or-create for a Vercel project domain.\n *\n * On `create()`, calls `GET /v9/projects/{projectId}/domains/{name}`. If found, adopts\n * the existing attachment. If 404, attaches it via `POST /v10/projects/{projectId}/domains`.\n *\n * @internal Exported only for unit testing; not part of the public API surface.\n */\nexport class VercelDomainResourceProvider\n\timplements pulumi.dynamic.ResourceProvider\n{\n\tasync create(\n\t\tinputs: VercelDomainInputs,\n\t): Promise<pulumi.dynamic.CreateResult> {\n\t\tconst existing = await fetchDomain(\n\t\t\tinputs.token,\n\t\t\tinputs.teamId,\n\t\t\tinputs.projectId,\n\t\t\tinputs.name,\n\t\t);\n\n\t\tif (existing) {\n\t\t\tpulumi.log.info(\n\t\t\t\t`Adopted existing Vercel domain \"${inputs.name}\" on project ${inputs.projectId}`,\n\t\t\t);\n\n\t\t\tconst outs: VercelDomainOutputs = {\n\t\t\t\t...inputs,\n\t\t\t\tverified: existing.verified,\n\t\t\t\tcnameTarget: await fetchCnameTarget(\n\t\t\t\t\tinputs.token,\n\t\t\t\t\tinputs.teamId,\n\t\t\t\t\tinputs.name,\n\t\t\t\t),\n\t\t\t};\n\n\t\t\treturn { id: `${inputs.projectId}/${inputs.name}`, outs };\n\t\t}\n\n\t\tconst response = await fetch(\n\t\t\t`${VERCEL_API_URL}/v10/projects/${encodeURIComponent(inputs.projectId)}/domains?teamId=${inputs.teamId}`,\n\t\t\t{\n\t\t\t\tmethod: \"POST\",\n\t\t\t\theaders: {\n\t\t\t\t\tAuthorization: `Bearer ${inputs.token}`,\n\t\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\t},\n\t\t\t\tbody: JSON.stringify({ name: inputs.name }),\n\t\t\t},\n\t\t);\n\n\t\tif (!response.ok) {\n\t\t\tthrow new Error(\n\t\t\t\t`Vercel API error creating domain \"${inputs.name}\" (${response.status}): ${await response.text()}`,\n\t\t\t);\n\t\t}\n\n\t\tconst created = (await response.json()) as VercelDomainResponse;\n\n\t\tconst outs: VercelDomainOutputs = {\n\t\t\t...inputs,\n\t\t\tverified: created.verified,\n\t\t\tcnameTarget: await fetchCnameTarget(\n\t\t\t\tinputs.token,\n\t\t\t\tinputs.teamId,\n\t\t\t\tinputs.name,\n\t\t\t),\n\t\t};\n\n\t\treturn { id: `${inputs.projectId}/${inputs.name}`, outs };\n\t}\n\n\tasync read(\n\t\t_id: string,\n\t\tprops: VercelDomainOutputs,\n\t): Promise<pulumi.dynamic.ReadResult> {\n\t\tconst domain = await fetchDomain(\n\t\t\tprops.token,\n\t\t\tprops.teamId,\n\t\t\tprops.projectId,\n\t\t\tprops.name,\n\t\t);\n\n\t\tif (!domain) {\n\t\t\tthrow new Error(\n\t\t\t\t`Vercel domain \"${props.name}\" not found on project ${props.projectId}`,\n\t\t\t);\n\t\t}\n\n\t\treturn {\n\t\t\tid: `${props.projectId}/${props.name}`,\n\t\t\tprops: {\n\t\t\t\t...props,\n\t\t\t\tverified: domain.verified,\n\t\t\t\tcnameTarget: await fetchCnameTarget(\n\t\t\t\t\tprops.token,\n\t\t\t\t\tprops.teamId,\n\t\t\t\t\tprops.name,\n\t\t\t\t),\n\t\t\t},\n\t\t};\n\t}\n\n\t/** All fields replace (see `diff`) — this is never actually invoked. */\n\tasync update(\n\t\t_id: string,\n\t\tolds: VercelDomainOutputs,\n\t\tnews: VercelDomainInputs,\n\t): Promise<pulumi.dynamic.UpdateResult> {\n\t\treturn {\n\t\t\touts: { ...news, verified: false, cnameTarget: olds.cnameTarget },\n\t\t};\n\t}\n\n\tasync delete(_id: string, props: VercelDomainOutputs): Promise<void> {\n\t\tconst response = await fetch(\n\t\t\t`${VERCEL_API_URL}/v9/projects/${encodeURIComponent(props.projectId)}/domains/${encodeURIComponent(props.name)}?teamId=${props.teamId}`,\n\t\t\t{\n\t\t\t\tmethod: \"DELETE\",\n\t\t\t\theaders: { Authorization: `Bearer ${props.token}` },\n\t\t\t},\n\t\t);\n\n\t\tif (response.status === 404) {\n\t\t\tpulumi.log.warn(\n\t\t\t\t`Vercel domain \"${props.name}\" already gone from project ${props.projectId}`,\n\t\t\t);\n\n\t\t\treturn;\n\t\t}\n\n\t\tif (!response.ok) {\n\t\t\tthrow new Error(\n\t\t\t\t`Vercel API error deleting domain \"${props.name}\" (${response.status}): ${await response.text()}`,\n\t\t\t);\n\t\t}\n\n\t\tpulumi.log.info(\n\t\t\t`Deleted Vercel domain \"${props.name}\" from project ${props.projectId}`,\n\t\t);\n\t}\n\n\t/**\n\t * A domain can only be attached to one project, so every field change replaces\n\t * rather than updates.\n\t */\n\tasync diff(\n\t\t_id: string,\n\t\tolds: VercelDomainOutputs,\n\t\tnews: VercelDomainInputs,\n\t): Promise<pulumi.dynamic.DiffResult> {\n\t\tconst replaces: string[] = [];\n\n\t\tif (olds.name !== news.name) {\n\t\t\treplaces.push(\"name\");\n\t\t}\n\n\t\tif (olds.projectId !== news.projectId) {\n\t\t\treplaces.push(\"projectId\");\n\t\t}\n\n\t\tif (olds.teamId !== news.teamId) {\n\t\t\treplaces.push(\"teamId\");\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 VercelDomainResource extends pulumi.dynamic.Resource {\n\tpublic declare readonly verified: pulumi.Output<boolean>;\n\tpublic declare readonly name: pulumi.Output<string>;\n\tpublic declare readonly cnameTarget: pulumi.Output<string>;\n\n\tconstructor(\n\t\tname: string,\n\t\targs: {\n\t\t\ttoken: pulumi.Input<string>;\n\t\t\tteamId: pulumi.Input<string>;\n\t\t\tprojectId: pulumi.Input<string>;\n\t\t\tname: pulumi.Input<string>;\n\t\t},\n\t\topts?: pulumi.CustomResourceOptions,\n\t) {\n\t\tsuper(\n\t\t\tnew VercelDomainResourceProvider(),\n\t\t\tname,\n\t\t\t{ ...args, verified: undefined, cnameTarget: undefined },\n\t\t\topts,\n\t\t);\n\t}\n}\n\n/** Options type for VercelDomain — replaces Pulumi's native `provider` field. */\ntype VercelDomainOptions = Omit<pulumi.ComponentResourceOptions, \"provider\"> & {\n\t/** Vercel authentication context. */\n\tprovider: VercelProvider;\n\n\t/** Vercel project to attach the domain to. */\n\tproject: VercelProject;\n};\n\n/** Args for VercelDomain. */\nexport interface VercelDomainArgs {\n\t/** Domain name (e.g. `\"app.example.com\"`). */\n\tname: pulumi.Input<string>;\n}\n\n/**\n * Attaches a custom domain to a Vercel project, with adopt-or-create semantics.\n *\n * @example\n * ```typescript\n * const domain = new VercelDomain(\"aura-domain\", { name: \"app.example.com\" }, { provider, project });\n *\n * // Point app.example.com's DNS CNAME at this value.\n * const cnameTarget = domain.cnameTarget;\n * ```\n */\nexport class VercelDomain extends pulumi.ComponentResource {\n\t/** `true` once the domain's DNS has been verified by Vercel. */\n\tpublic readonly verified: pulumi.Output<boolean>;\n\n\t/** The attached domain name. */\n\tpublic readonly name: pulumi.Output<string>;\n\n\t/**\n\t * The value to CNAME `name` to — Vercel's own recommendation for this specific\n\t * domain, fetched fresh from its DNS config endpoint (falls back to a static default\n\t * only if Vercel returns no recommendation).\n\t */\n\tpublic readonly cnameTarget: pulumi.Output<string>;\n\n\tconstructor(name: string, args: VercelDomainArgs, opts: VercelDomainOptions) {\n\t\tconst { provider, project, ...pulumiOpts } = opts;\n\n\t\tsuper(\"infracraft:vercel:Domain\", name, {}, pulumiOpts);\n\n\t\tconst resource = new VercelDomainResource(\n\t\t\t`${name}-resource`,\n\t\t\t{\n\t\t\t\ttoken: provider.token,\n\t\t\t\tteamId: provider.teamId,\n\t\t\t\tprojectId: project.id,\n\t\t\t\t...args,\n\t\t\t},\n\t\t\t{ parent: this },\n\t\t);\n\n\t\tthis.verified = resource.verified;\n\t\tthis.name = resource.name;\n\t\tthis.cnameTarget = resource.cnameTarget;\n\n\t\tthis.registerOutputs({\n\t\t\tverified: this.verified,\n\t\t\tname: this.name,\n\t\t\tcnameTarget: this.cnameTarget,\n\t\t});\n\t}\n}\n"],"mappings":"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"}
|
package/dist/vercel/index.cjs
CHANGED
|
@@ -8,7 +8,6 @@ const require_vercel_provider = require('./provider.cjs');
|
|
|
8
8
|
const require_vercel_resource_connection = require('./resource-connection.cjs');
|
|
9
9
|
const require_vercel_variable = require('./variable.cjs');
|
|
10
10
|
|
|
11
|
-
exports.VERCEL_CNAME_TARGET = require_vercel_domain.VERCEL_CNAME_TARGET;
|
|
12
11
|
exports.VERCEL_FRAMEWORKS = require_vercel_project.VERCEL_FRAMEWORKS;
|
|
13
12
|
exports.VercelDeploy = require_vercel_deploy.VercelDeploy;
|
|
14
13
|
exports.VercelDomain = require_vercel_domain.VercelDomain;
|
package/dist/vercel/index.d.cts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { VercelProvider, VercelProviderArgs } from "./provider.cjs";
|
|
2
2
|
import { VERCEL_FRAMEWORKS, VercelFramework, VercelProject, VercelProjectArgs } from "./project.cjs";
|
|
3
3
|
import { VercelDeploy, VercelDeployArgs } from "./deploy.cjs";
|
|
4
|
-
import {
|
|
4
|
+
import { VercelDomain, VercelDomainArgs } from "./domain.cjs";
|
|
5
5
|
import { VercelIntegration, VercelIntegrationArgs } from "./integration.cjs";
|
|
6
6
|
import { VercelMarketplaceResource, VercelMarketplaceResourceArgs } from "./marketplace-resource.cjs";
|
|
7
7
|
import { VercelResourceConnection, VercelResourceConnectionArgs } from "./resource-connection.cjs";
|
|
8
8
|
import { VercelVariable, VercelVariableArgs } from "./variable.cjs";
|
|
9
|
-
export {
|
|
9
|
+
export { VERCEL_FRAMEWORKS, VercelDeploy, type VercelDeployArgs, VercelDomain, type VercelDomainArgs, type VercelFramework, VercelIntegration, type VercelIntegrationArgs, VercelMarketplaceResource, type VercelMarketplaceResourceArgs, VercelProject, type VercelProjectArgs, VercelProvider, type VercelProviderArgs, VercelResourceConnection, type VercelResourceConnectionArgs, VercelVariable, type VercelVariableArgs };
|
package/dist/vercel/index.d.mts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { VercelProvider, VercelProviderArgs } from "./provider.mjs";
|
|
2
2
|
import { VERCEL_FRAMEWORKS, VercelFramework, VercelProject, VercelProjectArgs } from "./project.mjs";
|
|
3
3
|
import { VercelDeploy, VercelDeployArgs } from "./deploy.mjs";
|
|
4
|
-
import {
|
|
4
|
+
import { VercelDomain, VercelDomainArgs } from "./domain.mjs";
|
|
5
5
|
import { VercelIntegration, VercelIntegrationArgs } from "./integration.mjs";
|
|
6
6
|
import { VercelMarketplaceResource, VercelMarketplaceResourceArgs } from "./marketplace-resource.mjs";
|
|
7
7
|
import { VercelResourceConnection, VercelResourceConnectionArgs } from "./resource-connection.mjs";
|
|
8
8
|
import { VercelVariable, VercelVariableArgs } from "./variable.mjs";
|
|
9
|
-
export {
|
|
9
|
+
export { VERCEL_FRAMEWORKS, VercelDeploy, type VercelDeployArgs, VercelDomain, type VercelDomainArgs, type VercelFramework, VercelIntegration, type VercelIntegrationArgs, VercelMarketplaceResource, type VercelMarketplaceResourceArgs, VercelProject, type VercelProjectArgs, VercelProvider, type VercelProviderArgs, VercelResourceConnection, type VercelResourceConnectionArgs, VercelVariable, type VercelVariableArgs };
|
package/dist/vercel/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { VercelDeploy } from "./deploy.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { VercelDomain } from "./domain.mjs";
|
|
3
3
|
import { VercelIntegration } from "./integration.mjs";
|
|
4
4
|
import { VercelMarketplaceResource } from "./marketplace-resource.mjs";
|
|
5
5
|
import { VERCEL_FRAMEWORKS, VercelProject } from "./project.mjs";
|
|
@@ -7,4 +7,4 @@ import { VercelProvider } from "./provider.mjs";
|
|
|
7
7
|
import { VercelResourceConnection } from "./resource-connection.mjs";
|
|
8
8
|
import { VercelVariable } from "./variable.mjs";
|
|
9
9
|
|
|
10
|
-
export {
|
|
10
|
+
export { VERCEL_FRAMEWORKS, VercelDeploy, VercelDomain, VercelIntegration, VercelMarketplaceResource, VercelProject, VercelProvider, VercelResourceConnection, VercelVariable };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@infracraft/pulumi",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.21.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.",
|