1import * as siteVerification from "@distilled.cloud/gcp/siteVerification_v1";
2import * as Effect from "effect/Effect";
3import { createPhysicalName } from "../../PhysicalName.ts";
4
5export const DEFAULT_SITE_TYPE = "SITE";
6export const OWNERSHIP_HOST = "alchemy-site-verification.test";
7export const MAX_LABEL_LENGTH = 63;
8
9export type SiteType = "SITE" | "INET_DOMAIN" | (string & {});
10
11export type VerificationMethod =
12 | "FILE"
13 | "META"
14 | "ANALYTICS"
15 | "TAG_MANAGER"
16 | "DNS_TXT"
17 | "DNS_CNAME"
18 | "DNS"
19 | (string & {});
20
21export const defaultVerificationMethod = (
22 siteType: string | undefined,
23): VerificationMethod =>
24 (siteType ?? DEFAULT_SITE_TYPE) === "INET_DOMAIN" ? "DNS_TXT" : "FILE";
25
26export const sameText = (left: string | undefined, right: string | undefined) =>
27 (left ?? "") === (right ?? "");
28
29export const sortedOwners = (owners: readonly string[] | undefined) =>
30 [...(owners ?? [])]
31 .map((owner) => owner.trim())
32 .filter((owner) => owner.length > 0)
33 .sort((left, right) => left.localeCompare(right));
34
35export const sameOwners = (
36 left: readonly string[] | undefined,
37 right: readonly string[] | undefined,
38) => JSON.stringify(sortedOwners(left)) === JSON.stringify(sortedOwners(right));
39
40/**
41 * Distilled fully-encodes `{id}` path labels. Google returns the web
42 * resource id already percent-encoded (`http%3A%2F%2F…`), so decode
43 * first and let the protocol encode once.
44 */
45export const toPathId = (id: string | undefined) => {
46 if (!id) return "";
47 try {
48 return decodeURIComponent(id);
49 } catch {
50 return id;
51 }
52};
53
54export const normalizeIdentifier = (
55 identifier: string,
56 siteType: string | undefined,
57) => {
58 const trimmed = identifier.trim();
59 if ((siteType ?? DEFAULT_SITE_TYPE) !== "SITE") return trimmed;
60 return trimmed.endsWith("/") ? trimmed : `${trimmed}/`;
61};
62
63export const hasOwnershipMarker = (identifier: string | undefined) =>
64 (identifier ?? "").toLowerCase().includes(OWNERSHIP_HOST);
65
66export const toGeneratedIdentifier = (
67 id: string,
68 siteType: string | undefined,
69 requested: string | undefined,
70 existing: string | undefined,
71) =>
72 Effect.gen(function* () {
73 if (requested !== undefined && requested.length > 0) {
74 return normalizeIdentifier(requested, siteType);
75 }
76 if (existing !== undefined && existing.length > 0) {
77 return existing;
78 }
79 const label = yield* createPhysicalName({
80 id,
81 maxLength: MAX_LABEL_LENGTH,
82 lowercase: true,
83 });
84 const host = `${label}.${OWNERSHIP_HOST}`;
85 return (siteType ?? DEFAULT_SITE_TYPE) === "INET_DOMAIN"
86 ? host
87 : `https://${host}/`;
88 });
89
90const emptyWebResources = () =>
91 Effect.succeed([] as siteVerification.SiteVerificationWebResourceResource[]);
92
93export const getWebResource = (id: string | undefined) => {
94 const pathId = toPathId(id);
95 return pathId.length === 0
96 ? Effect.succeed(undefined)
97 : siteVerification
98 .getWebResource({ id: pathId })
99 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
100};
101
102export const listWebResources = () =>
103 siteVerification.listWebResource({}).pipe(
104 Effect.map((page) => page.items ?? []),
105 Effect.catchTag("NotFound", () => emptyWebResources()),
106 );
107
108export const findWebResource = (
109 identifier: string | undefined,
110 siteType: string | undefined,
111) =>
112 Effect.gen(function* () {
113 if (!identifier) return undefined;
114 const desiredType = siteType ?? DEFAULT_SITE_TYPE;
115 const desiredId = normalizeIdentifier(identifier, desiredType);
116 const items = yield* listWebResources();
117 return items.find((item) => {
118 const type = item.site?.type ?? DEFAULT_SITE_TYPE;
119 const observed = item.site?.identifier ?? "";
120 return (
121 type === desiredType &&
122 (sameText(observed, desiredId) ||
123 sameText(normalizeIdentifier(observed, type), desiredId) ||
124 sameText(toPathId(item.id), desiredId) ||
125 sameText(item.id, identifier))
126 );
127 });
128 });
129
130export const deleteWebResource = (id: string | undefined) => {
131 const pathId = toPathId(id);
132 return pathId.length === 0
133 ? Effect.void
134 : siteVerification
135 .deleteWebResource({ id: pathId })
136 .pipe(Effect.catchTag("NotFound", () => Effect.void));
137};
138
139export const getWebResourceToken = (request: {
140 identifier: string;
141 siteType?: SiteType;
142 verificationMethod?: VerificationMethod;
143}) => {
144 const siteType = request.siteType ?? DEFAULT_SITE_TYPE;
145 return siteVerification.getTokenWebResource({
146 body: {
147 site: {
148 identifier: request.identifier,
149 type: siteType,
150 },
151 verificationMethod:
152 request.verificationMethod ?? defaultVerificationMethod(siteType),
153 },
154 });
155};