1import * as gmailpostmastertools from "@distilled.cloud/gcp/gmailpostmastertools_v2";
2import * as Data from "effect/Data";
3import * as Effect from "effect/Effect";
4import * as Schedule from "effect/Schedule";
5import { Unowned } from "../../AdoptPolicy.ts";
6import { isResolved } from "../../Diff.ts";
7import * as Provider from "../../Provider.ts";
8import { Resource } from "../../Resource.ts";
9import { GcpEnvironment } from "../Environment.ts";
10import type { Providers } from "../Providers.ts";
11import {
12 domainIdOf,
13 getDomain,
14 ignoreMissing,
15 isAlchemyDomain,
16 listOwnedDomains,
17 sameText,
18 toDomainName,
19 toGeneratedDomainId,
20} from "./internal.ts";
21
22export type DomainProps = {
23 /**
24 * Fully qualified domain name registered with Postmaster Tools
25 * (`mail.example.com`). If omitted, a unique `alchemy-*.example.com`
26 * name is generated from the stack, stage, and logical id. Immutable —
27 * changing it replaces the domain. Domains have no labels field, so
28 * Alchemy stamps ownership into the generated name prefix for
29 * `list` / nuke.
30 */
31 domainId?: string;
32 /**
33 * DNS verification method. When set, reconcile calls `domains.verify`
34 * if the domain is not already `VERIFIED`. Requires the matching TXT
35 * or CNAME record.
36 */
37 verificationMethod?:
38 | gmailpostmastertools.VerifyDomainRequestVerificationMethodEnum
39 | (string & {});
40};
41
42export type Domain = Resource<
43 "GCP.GmailPostmasterTools.Domain",
44 DomainProps,
45 {
46 /** Full resource name `domains/{domain}`. */
47 name: string;
48 /** Fully qualified domain name. */
49 domainId: string;
50 /** Project id used when the domain was reconciled. */
51 project: string;
52 /** Caller's permission on the domain. */
53 permission: string | undefined;
54 /** Domain ownership verification state. */
55 verificationState: string | undefined;
56 /** RFC3339 time the domain was added. */
57 createTime: string | undefined;
58 /** RFC3339 time the domain was last verified. */
59 lastVerifyTime: string | undefined;
60 },
61 never,
62 Providers
63>;
64
65/**
66 * A Gmail Postmaster Tools domain.
67 *
68 * Domains have no labels or description, so identity is the FQDN and
69 * `list` / nuke returns names whose first label starts with `alchemy-`.
70 * There is nothing mutable beyond identity except optional DNS
71 * verification. Changing `domainId` replaces the domain.
72 *
73 * ### Creating a Domain
74 * **Example:** Generated domain
75 * ```typescript
76 * const domain = yield* GCP.GmailPostmasterTools.Domain("Mail", {});
77 * ```
78 *
79 * **Example:** Explicit domain
80 * ```typescript
81 * const domain = yield* GCP.GmailPostmasterTools.Domain("Mail", {
82 * domainId: "mail.example.com",
83 * });
84 * ```
85 *
86 * ### Verifying a Domain
87 * **Example:** Verify with a TXT record
88 * ```typescript
89 * const domain = yield* GCP.GmailPostmasterTools.Domain("Mail", {
90 * domainId: "mail.example.com",
91 * verificationMethod: "TXT",
92 * });
93 * ```
94 *
95 * @resource
96 * @category Gmail Postmaster Tools
97 */
98export const Domain = Resource<Domain>("GCP.GmailPostmasterTools.Domain");
99
100export class DomainNotResolved extends Data.TaggedError(
101 "GCP.GmailPostmasterTools.DomainNotResolved",
102)<{
103 name: string;
104}> {}
105
106const toAttrs = (
107 domain: gmailpostmastertools.Domain,
108 project: string,
109 fallbackId?: string,
110) => {
111 const name = domain.name ?? (fallbackId ? toDomainName(fallbackId) : "");
112 return {
113 name,
114 domainId: domainIdOf(name) || fallbackId || "",
115 project,
116 permission: domain.permission,
117 verificationState: domain.verificationState,
118 createTime: domain.createTime,
119 lastVerifyTime: domain.lastVerifyTime,
120 };
121};
122
123export const DomainProvider = () =>
124 Provider.succeed(Domain, {
125 stables: ["name", "domainId", "project", "createTime"],
126
127 diff: Effect.fn(function* ({ news, olds, output }) {
128 if (!isResolved(news)) return undefined;
129 const previous = olds?.domainId ?? output?.domainId;
130 if (
131 previous !== undefined &&
132 news.domainId !== undefined &&
133 domainIdOf(news.domainId) !== domainIdOf(previous)
134 ) {
135 return { action: "replace" as const, deleteFirst: true };
136 }
137 return undefined;
138 }),
139
140 read: Effect.fn(function* ({ olds, output }) {
141 const env = yield* GcpEnvironment.current;
142 const domainId = olds?.domainId ?? output?.domainId ?? "";
143 const name = toDomainName(output?.name || domainId);
144 const existing = yield* getDomain(name);
145 if (existing === undefined) return undefined;
146 const attrs = toAttrs(existing, env.project, domainId);
147 return output !== undefined || isAlchemyDomain(existing.name)
148 ? attrs
149 : Unowned(attrs);
150 }),
151
152 list: () =>
153 Effect.gen(function* () {
154 const env = yield* GcpEnvironment.current;
155 const domains = yield* listOwnedDomains();
156 return domains.map((domain) => toAttrs(domain, env.project));
157 }),
158
159 reconcile: Effect.fn(function* ({ id, news, output }) {
160 const env = yield* GcpEnvironment.current;
161 const domainId = yield* toGeneratedDomainId(
162 id,
163 news.domainId,
164 output?.domainId,
165 );
166 const name = toDomainName(domainId);
167
168 let current = yield* getDomain(output?.name || name);
169
170 if (current === undefined) {
171 const created = yield* gmailpostmastertools
172 .createDomains({ body: { domainId } })
173 .pipe(Effect.catchTag("Conflict", () => getDomain(name)));
174 current = created ?? undefined;
175 }
176
177 if (current === undefined) {
178 return yield* new DomainNotResolved({ name });
179 }
180
181 const resourceName = current.name ?? name;
182 if (
183 news.verificationMethod !== undefined &&
184 !sameText(current.verificationState, "VERIFIED")
185 ) {
186 yield* gmailpostmastertools.verifyDomains({
187 name: resourceName,
188 body: { verificationMethod: news.verificationMethod },
189 });
190 current = (yield* getDomain(resourceName)) ?? current;
191 }
192
193 return toAttrs(current, env.project, domainId);
194 }),
195
196 delete: Effect.fn(function* ({ output }) {
197 const name = output.name || toDomainName(output.domainId);
198 if (name.length === 0) return;
199 yield* ignoreMissing(
200 gmailpostmastertools.deleteDomains({ name }).pipe(
201 Effect.retry({
202 while: (error) => error._tag === "Conflict",
203 schedule: Schedule.spaced("1 second"),
204 times: 8,
205 }),
206 ),
207 );
208 }),
209 });