| 1 | import * as siteVerification from "@distilled.cloud/gcp/siteVerification_v1"; |
| 2 | import * as Effect from "effect/Effect"; |
| 3 | import { createPhysicalName } from "../../PhysicalName.ts"; |
| 4 | |
| 5 | export const DEFAULT_SITE_TYPE = "SITE"; |
| 6 | export const OWNERSHIP_HOST = "alchemy-site-verification.test"; |
| 7 | export const MAX_LABEL_LENGTH = 63; |
| 8 | |
| 9 | export type SiteType = "SITE" | "INET_DOMAIN" | (string & {}); |
| 10 | |
| 11 | export type VerificationMethod = |
| 12 | | "FILE" |
| 13 | | "META" |
| 14 | | "ANALYTICS" |
| 15 | | "TAG_MANAGER" |
| 16 | | "DNS_TXT" |
| 17 | | "DNS_CNAME" |
| 18 | | "DNS" |
| 19 | | (string & {}); |
| 20 | |
| 21 | export const defaultVerificationMethod = ( |
| 22 | siteType: string | undefined, |
| 23 | ): VerificationMethod => |
| 24 | (siteType ?? DEFAULT_SITE_TYPE) === "INET_DOMAIN" ? "DNS_TXT" : "FILE"; |
| 25 | |
| 26 | export const sameText = (left: string | undefined, right: string | undefined) => |
| 27 | (left ?? "") === (right ?? ""); |
| 28 | |
| 29 | export 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 | |
| 35 | export 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 | */ |
| 45 | export const toPathId = (id: string | undefined) => { |
| 46 | if (!id) return ""; |
| 47 | try { |
| 48 | return decodeURIComponent(id); |
| 49 | } catch { |
| 50 | return id; |
| 51 | } |
| 52 | }; |
| 53 | |
| 54 | export 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 | |
| 63 | export const hasOwnershipMarker = (identifier: string | undefined) => |
| 64 | (identifier ?? "").toLowerCase().includes(OWNERSHIP_HOST); |
| 65 | |
| 66 | export 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 | |
| 90 | const emptyWebResources = () => |
| 91 | Effect.succeed([] as siteVerification.SiteVerificationWebResourceResource[]); |
| 92 | |
| 93 | export 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 | |
| 102 | export const listWebResources = () => |
| 103 | siteVerification.listWebResource({}).pipe( |
| 104 | Effect.map((page) => page.items ?? []), |
| 105 | Effect.catchTag("NotFound", () => emptyWebResources()), |
| 106 | ); |
| 107 | |
| 108 | export 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 | |
| 130 | export 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 | |
| 139 | export 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 | }; |