@aglyn/shared-util-email 1.0.0-beta.180 → 1.0.0-beta.182
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/README.md +29 -0
- package/package.json +3 -3
- package/src/index.d.ts +2 -0
- package/src/index.js +3 -0
- package/src/index.js.map +1 -1
- package/src/lib/list-unsubscribe.d.ts +193 -0
- package/src/lib/list-unsubscribe.js +221 -0
- package/src/lib/list-unsubscribe.js.map +1 -0
- package/src/lib/marketing-send.js +4 -4
- package/src/lib/marketing-send.js.map +1 -1
- package/src/lib/sending-domain.d.ts +57 -0
- package/src/lib/sending-domain.js +19 -3
- package/src/lib/sending-domain.js.map +1 -1
- package/src/lib/tracking-host.d.ts +197 -0
- package/src/lib/tracking-host.js +186 -0
- package/src/lib/tracking-host.js.map +1 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ import { normalizeSendingDomain, SENDING_TRACKING_SUBDOMAIN, sendingTrackingHost, trackingRecords, validateSendingDomain } from "./sending-domain.js";
|
|
17
|
+
/**
|
|
18
|
+
* A CLICK-TRACKING HOST THIS DEPLOYMENT SERVES ITSELF — policy half.
|
|
19
|
+
*
|
|
20
|
+
* ## The same process as a sending domain's, with our redirector
|
|
21
|
+
*
|
|
22
|
+
* Campaign mail leaves through the mail provider, which rewrites every link to
|
|
23
|
+
* `links.<sending domain>` and redirects from there: the customer publishes a
|
|
24
|
+
* CNAME to the provider's host and the provider holds the certificate
|
|
25
|
+
* (`trackingRecords` in `sending-domain.ts`).
|
|
26
|
+
*
|
|
27
|
+
* Mail that leaves any other way — a sequence, sent as plain text through the
|
|
28
|
+
* member's own Gmail — never passes the provider, so the provider cannot
|
|
29
|
+
* rewrite or redirect its links. Its redirector is ours. What stays the same is
|
|
30
|
+
* everything a customer sees: the label ({@link SENDING_TRACKING_SUBDOMAIN}),
|
|
31
|
+
* the host (`links.<the mailbox's domain>`), a CNAME to publish, and a check
|
|
32
|
+
* that says when it is live. Only the CNAME's target differs — this
|
|
33
|
+
* deployment instead of the provider.
|
|
34
|
+
*
|
|
35
|
+
* ## Serving the name is a driver's job
|
|
36
|
+
*
|
|
37
|
+
* Attaching `links.<domain>` to the deployment and getting it a certificate is
|
|
38
|
+
* the domain driver's (`AGLYN_DOMAIN_PROVIDER`, `domain-provider.ts`), the
|
|
39
|
+
* same seam a workspace subdomain goes through: Vercel attaches it to the
|
|
40
|
+
* console project; `none` leaves it to the operator's DNS and proxy. Either
|
|
41
|
+
* way the host is served by the console app itself — the middleware answers
|
|
42
|
+
* only this module's paths on it ({@link trackingHostPath}) — so it works on
|
|
43
|
+
* any deployment, not just one vendor's edge.
|
|
44
|
+
*
|
|
45
|
+
* ## Verified by asking the host, not by reading a zone
|
|
46
|
+
*
|
|
47
|
+
* A zone can serve the name without the literal CNAME we print — a wildcard,
|
|
48
|
+
* an ALIAS, a proxy that terminates TLS itself — and a zone can carry the
|
|
49
|
+
* CNAME while nothing answers on it yet. So the check is end to end: fetch
|
|
50
|
+
* {@link TRACKING_HOST_PROBE_PATH} over HTTPS on the host and require this
|
|
51
|
+
* app's answer ({@link readTrackingHostProbe}). A valid certificate, working
|
|
52
|
+
* DNS and the routing all have to be true for that to succeed, and nothing
|
|
53
|
+
* less than all three is a host a link should point at.
|
|
54
|
+
*
|
|
55
|
+
* ## Only a verified host is ever used
|
|
56
|
+
*
|
|
57
|
+
* {@link trackingHostLinkOrigin} answers `null` for anything short of
|
|
58
|
+
* `verified`, and the caller falls back to the app's own address — so an
|
|
59
|
+
* install with no host set up, or one half set up, sends exactly the links it
|
|
60
|
+
* always did.
|
|
61
|
+
*/ /**
|
|
62
|
+
* The path the host answers its verification probe on.
|
|
63
|
+
*
|
|
64
|
+
* Not under `/.well-known/`, which the console refuses wholesale (AGL-3016),
|
|
65
|
+
* and not shaped like a link id, so the two can never be confused.
|
|
66
|
+
*/ export const TRACKING_HOST_PROBE_PATH = '/_aglyn/link-host';
|
|
67
|
+
/** What the probe's JSON names itself, so a stray 200 is not mistaken for it. */ export const TRACKING_HOST_PROBE_SERVICE = 'aglyn-link-host';
|
|
68
|
+
/**
|
|
69
|
+
* The console route a tracking host's ids resolve on.
|
|
70
|
+
*
|
|
71
|
+
* Sequences' short-link redirector (AGL-3297) — the only mail this deployment
|
|
72
|
+
* rewrites links for itself. A link `https://links.<domain>/<id>` is answered
|
|
73
|
+
* exactly as `<console>/api/outreach/l/<id>` is, by the same handler.
|
|
74
|
+
*/ export const TRACKING_HOST_LINK_ROUTE = '/api/outreach/l';
|
|
75
|
+
/**
|
|
76
|
+
* A link id as the host accepts one in its path: a single segment of letters
|
|
77
|
+
* and digits. The redirector checks its own exact shape; this only keeps
|
|
78
|
+
* every other path off it.
|
|
79
|
+
*/ const LINK_ID_PATH = /^\/([A-Za-z0-9]{6,32})$/;
|
|
80
|
+
/**
|
|
81
|
+
* Whether a hostname is shaped like a tracking host: `links.` over a valid
|
|
82
|
+
* domain. Shape only — whether this deployment serves it is the driver's and
|
|
83
|
+
* the probe's to say.
|
|
84
|
+
*/ export function isTrackingHostName(hostname) {
|
|
85
|
+
const host = String(hostname != null ? hostname : '').trim().toLowerCase().replace(/\.$/, '');
|
|
86
|
+
const prefix = `${SENDING_TRACKING_SUBDOMAIN}.`;
|
|
87
|
+
if (!host.startsWith(prefix)) return false;
|
|
88
|
+
return sendingTrackingHost(host.slice(prefix.length)) === host;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* What a tracking host answers for a path: a link id is rewritten to the
|
|
92
|
+
* redirector, the probe is answered, and everything else is a 404 — no
|
|
93
|
+
* console page, no API route, nothing but links.
|
|
94
|
+
*/ export function trackingHostPath(pathname) {
|
|
95
|
+
var _LINK_ID_PATH_exec;
|
|
96
|
+
if (pathname === TRACKING_HOST_PROBE_PATH) return {
|
|
97
|
+
kind: 'probe'
|
|
98
|
+
};
|
|
99
|
+
const id = (_LINK_ID_PATH_exec = LINK_ID_PATH.exec(pathname)) == null ? void 0 : _LINK_ID_PATH_exec[1];
|
|
100
|
+
return id ? {
|
|
101
|
+
kind: 'link',
|
|
102
|
+
rewrite: `${TRACKING_HOST_LINK_ROUTE}/${id}`
|
|
103
|
+
} : {
|
|
104
|
+
kind: 'not-found'
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/** The probe's answer on `host`. */ export function trackingHostProbeBody(host) {
|
|
108
|
+
return {
|
|
109
|
+
service: TRACKING_HOST_PROBE_SERVICE,
|
|
110
|
+
host: String(host != null ? host : '').toLowerCase()
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/** Whether a probe response body is this app answering for `host`. */ export function readTrackingHostProbe(body, host) {
|
|
114
|
+
var _record_host;
|
|
115
|
+
if (!body || typeof body !== 'object') return false;
|
|
116
|
+
const record = body;
|
|
117
|
+
return record['service'] === TRACKING_HOST_PROBE_SERVICE && String((_record_host = record['host']) != null ? _record_host : '').toLowerCase() === String(host != null ? host : '').toLowerCase();
|
|
118
|
+
}
|
|
119
|
+
/** Why a host is not verified, as a person reads it. Keys are what is stored. */ export const TRACKING_HOST_DETAIL = {
|
|
120
|
+
'attach-failed': "This deployment could not add the link domain. Try again, or ask your administrator.",
|
|
121
|
+
'provider-tracked': 'This domain already sends campaign mail with click tracking, so its link domain belongs to the mail provider. Sequence links keep the app address.',
|
|
122
|
+
'ownership-pending': 'Add the verification record below, then check again.',
|
|
123
|
+
'dns-misconfigured': 'The DNS record is missing or points somewhere else.',
|
|
124
|
+
'certificate-pending': 'The DNS record is in place and the certificate is being issued. Check again in a few minutes.',
|
|
125
|
+
unreachable: 'The link domain did not answer over HTTPS yet. DNS can take a while to spread; check again later.',
|
|
126
|
+
'not-this-app': 'The link domain answered, but not from this app. Point it at this deployment.'
|
|
127
|
+
};
|
|
128
|
+
/**
|
|
129
|
+
* The CNAME target a customer is told to point `links.<domain>` at, or `null`
|
|
130
|
+
* when the deployment names none.
|
|
131
|
+
*
|
|
132
|
+
* `AGLYN_LINK_HOST_TARGET` wins — a self-hoster's proxy hostname. Without it,
|
|
133
|
+
* the Vercel driver's documented target; any other driver has no default to
|
|
134
|
+
* guess, and the card says to point the name at this app instead.
|
|
135
|
+
*/ export function trackingHostTarget(providerId) {
|
|
136
|
+
var _process_env_AGLYN_LINK_HOST_TARGET;
|
|
137
|
+
const configured = String((_process_env_AGLYN_LINK_HOST_TARGET = process.env['AGLYN_LINK_HOST_TARGET']) != null ? _process_env_AGLYN_LINK_HOST_TARGET : '').trim().replace(/\.$/, '');
|
|
138
|
+
if (configured) return configured;
|
|
139
|
+
return providerId === 'vercel' ? 'cname.vercel-dns.com' : null;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* The CA that issues a self-served tracking host's certificate — Let's
|
|
143
|
+
* Encrypt, which is what Vercel, Caddy and Traefik use by default. Only ever
|
|
144
|
+
* printed in the CAA row, which only matters to a zone that already
|
|
145
|
+
* publishes CAA.
|
|
146
|
+
*/ export function trackingHostCertAuthority() {
|
|
147
|
+
var _process_env_AGLYN_LINK_HOST_CA;
|
|
148
|
+
return String((_process_env_AGLYN_LINK_HOST_CA = process.env['AGLYN_LINK_HOST_CA']) != null ? _process_env_AGLYN_LINK_HOST_CA : '').trim() || 'letsencrypt.org';
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* The records a member publishes for a tracking host: the same tracking rows
|
|
152
|
+
* a sending domain prints, pointed at this deployment, then any the driver
|
|
153
|
+
* asked for.
|
|
154
|
+
*/ export function trackingHostDnsRecords(record) {
|
|
155
|
+
var _ref, _ref1;
|
|
156
|
+
const domain = normalizeSendingDomain((_ref = record == null ? void 0 : record.domain) != null ? _ref : '');
|
|
157
|
+
const rows = trackingRecords(domain, record == null ? void 0 : record.target, trackingHostCertAuthority());
|
|
158
|
+
for (const entry of (_ref1 = record == null ? void 0 : record.verification) != null ? _ref1 : []){
|
|
159
|
+
var _ref2, _entry_domain, _entry_value;
|
|
160
|
+
const type = String((_ref2 = entry == null ? void 0 : entry.type) != null ? _ref2 : '').toUpperCase();
|
|
161
|
+
if (type !== 'TXT' && type !== 'CNAME') continue;
|
|
162
|
+
rows.push({
|
|
163
|
+
type,
|
|
164
|
+
name: String((_entry_domain = entry.domain) != null ? _entry_domain : ''),
|
|
165
|
+
value: String((_entry_value = entry.value) != null ? _entry_value : ''),
|
|
166
|
+
purpose: 'tracking',
|
|
167
|
+
required: true,
|
|
168
|
+
note: 'Proves to the hosting provider that you own this name. It can be removed once the link domain is verified.'
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
return rows;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* The origin links should use for mail from `domain` — `https://links.<domain>`
|
|
175
|
+
* — or `null` for a record that is absent or not verified, which means "use
|
|
176
|
+
* the app's own address".
|
|
177
|
+
*/ export function trackingHostLinkOrigin(record) {
|
|
178
|
+
if (!record || record.status !== 'verified') return null;
|
|
179
|
+
const host = sendingTrackingHost(record.domain);
|
|
180
|
+
return host && host === record.host ? `https://${host}` : null;
|
|
181
|
+
}
|
|
182
|
+
/** A domain a tracking host may be set up for, or an error sentence. */ export function validateTrackingHostDomain(input) {
|
|
183
|
+
return validateSendingDomain(input);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
//# sourceMappingURL=tracking-host.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/shared/util/email/src/lib/tracking-host.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n normalizeSendingDomain,\n SENDING_TRACKING_SUBDOMAIN,\n sendingTrackingHost,\n trackingRecords,\n validateSendingDomain,\n type SendingDnsRecord,\n type SendingDomainStatus,\n} from './sending-domain'\n\n/**\n * A CLICK-TRACKING HOST THIS DEPLOYMENT SERVES ITSELF — policy half.\n *\n * ## The same process as a sending domain's, with our redirector\n *\n * Campaign mail leaves through the mail provider, which rewrites every link to\n * `links.<sending domain>` and redirects from there: the customer publishes a\n * CNAME to the provider's host and the provider holds the certificate\n * (`trackingRecords` in `sending-domain.ts`).\n *\n * Mail that leaves any other way — a sequence, sent as plain text through the\n * member's own Gmail — never passes the provider, so the provider cannot\n * rewrite or redirect its links. Its redirector is ours. What stays the same is\n * everything a customer sees: the label ({@link SENDING_TRACKING_SUBDOMAIN}),\n * the host (`links.<the mailbox's domain>`), a CNAME to publish, and a check\n * that says when it is live. Only the CNAME's target differs — this\n * deployment instead of the provider.\n *\n * ## Serving the name is a driver's job\n *\n * Attaching `links.<domain>` to the deployment and getting it a certificate is\n * the domain driver's (`AGLYN_DOMAIN_PROVIDER`, `domain-provider.ts`), the\n * same seam a workspace subdomain goes through: Vercel attaches it to the\n * console project; `none` leaves it to the operator's DNS and proxy. Either\n * way the host is served by the console app itself — the middleware answers\n * only this module's paths on it ({@link trackingHostPath}) — so it works on\n * any deployment, not just one vendor's edge.\n *\n * ## Verified by asking the host, not by reading a zone\n *\n * A zone can serve the name without the literal CNAME we print — a wildcard,\n * an ALIAS, a proxy that terminates TLS itself — and a zone can carry the\n * CNAME while nothing answers on it yet. So the check is end to end: fetch\n * {@link TRACKING_HOST_PROBE_PATH} over HTTPS on the host and require this\n * app's answer ({@link readTrackingHostProbe}). A valid certificate, working\n * DNS and the routing all have to be true for that to succeed, and nothing\n * less than all three is a host a link should point at.\n *\n * ## Only a verified host is ever used\n *\n * {@link trackingHostLinkOrigin} answers `null` for anything short of\n * `verified`, and the caller falls back to the app's own address — so an\n * install with no host set up, or one half set up, sends exactly the links it\n * always did.\n */\n\n/**\n * The path the host answers its verification probe on.\n *\n * Not under `/.well-known/`, which the console refuses wholesale (AGL-3016),\n * and not shaped like a link id, so the two can never be confused.\n */\nexport const TRACKING_HOST_PROBE_PATH = '/_aglyn/link-host'\n\n/** What the probe's JSON names itself, so a stray 200 is not mistaken for it. */\nexport const TRACKING_HOST_PROBE_SERVICE = 'aglyn-link-host'\n\n/**\n * The console route a tracking host's ids resolve on.\n *\n * Sequences' short-link redirector (AGL-3297) — the only mail this deployment\n * rewrites links for itself. A link `https://links.<domain>/<id>` is answered\n * exactly as `<console>/api/outreach/l/<id>` is, by the same handler.\n */\nexport const TRACKING_HOST_LINK_ROUTE = '/api/outreach/l'\n\n/**\n * A link id as the host accepts one in its path: a single segment of letters\n * and digits. The redirector checks its own exact shape; this only keeps\n * every other path off it.\n */\nconst LINK_ID_PATH = /^\\/([A-Za-z0-9]{6,32})$/\n\n/** What the host does with one request path. */\nexport type TrackingHostRoute =\n | { kind: 'link'; rewrite: string }\n | { kind: 'probe' }\n | { kind: 'not-found' }\n\n/**\n * Whether a hostname is shaped like a tracking host: `links.` over a valid\n * domain. Shape only — whether this deployment serves it is the driver's and\n * the probe's to say.\n */\nexport function isTrackingHostName(hostname: string | null | undefined): boolean {\n const host = String(hostname ?? '').trim().toLowerCase().replace(/\\.$/, '')\n const prefix = `${SENDING_TRACKING_SUBDOMAIN}.`\n if (!host.startsWith(prefix)) return false\n return sendingTrackingHost(host.slice(prefix.length)) === host\n}\n\n/**\n * What a tracking host answers for a path: a link id is rewritten to the\n * redirector, the probe is answered, and everything else is a 404 — no\n * console page, no API route, nothing but links.\n */\nexport function trackingHostPath(pathname: string): TrackingHostRoute {\n if (pathname === TRACKING_HOST_PROBE_PATH) return { kind: 'probe' }\n const id = LINK_ID_PATH.exec(pathname)?.[1]\n return id ? { kind: 'link', rewrite: `${TRACKING_HOST_LINK_ROUTE}/${id}` } : { kind: 'not-found' }\n}\n\n/** The probe's answer on `host`. */\nexport function trackingHostProbeBody(host: string): { service: string; host: string } {\n return { service: TRACKING_HOST_PROBE_SERVICE, host: String(host ?? '').toLowerCase() }\n}\n\n/** Whether a probe response body is this app answering for `host`. */\nexport function readTrackingHostProbe(body: unknown, host: string): boolean {\n if (!body || typeof body !== 'object') return false\n const record = body as Record<string, unknown>\n return (\n record['service'] === TRACKING_HOST_PROBE_SERVICE &&\n String(record['host'] ?? '').toLowerCase() === String(host ?? '').toLowerCase()\n )\n}\n\n/*==========================================\n The record\n==========================================*/\n\n/**\n * The stored record, one per sending domain per org, at\n * `orgs/{orgId}/trackingHosts/{domain}`.\n *\n * `status` is the sending domain's lifecycle, reused so a surface reads both\n * the same way: `requested` (asked, but the driver could not attach the\n * name), `records-issued` (attached; DNS or the certificate still to come),\n * `verified` (the probe answered over HTTPS), `failed` (we looked, and it did\n * not).\n */\nexport interface TrackingHostRecord {\n /** The sending domain, normalized. Also the document id. */\n domain: string\n /** `links.<domain>`. */\n host: string\n status: SendingDomainStatus\n /** The CNAME target the member is shown, or `null` when the deployment names none. */\n target?: string | null\n /** The domain driver that attached the host (`vercel`, `none`, …). */\n provider?: string | null\n /**\n * Records the driver needs before it will serve the name — Vercel's\n * ownership TXT for a domain another account holds, for instance.\n */\n verification?: Array<{ type: string; domain: string; value: string; reason?: string }> | null\n /** Why it is not verified, from {@link TRACKING_HOST_DETAIL}'s vocabulary. */\n lastDetail?: string | null\n createdAtMs?: number | null\n verifiedAtMs?: number | null\n lastCheckedAtMs?: number | null\n}\n\n/**\n * What setting up, checking or removing a host answers. Both keys always\n * present, one always null — `strictNullChecks` is off.\n */\nexport interface TrackingHostResult {\n record: TrackingHostRecord | null\n error: string | null\n /** The HTTP status a route should answer with. */\n status: number\n}\n\n/** Why a host is not verified, as a person reads it. Keys are what is stored. */\nexport const TRACKING_HOST_DETAIL = {\n 'attach-failed': \"This deployment could not add the link domain. Try again, or ask your administrator.\",\n 'provider-tracked':\n 'This domain already sends campaign mail with click tracking, so its link domain belongs to the mail provider. Sequence links keep the app address.',\n 'ownership-pending': 'Add the verification record below, then check again.',\n 'dns-misconfigured': 'The DNS record is missing or points somewhere else.',\n 'certificate-pending': 'The DNS record is in place and the certificate is being issued. Check again in a few minutes.',\n unreachable: 'The link domain did not answer over HTTPS yet. DNS can take a while to spread; check again later.',\n 'not-this-app': 'The link domain answered, but not from this app. Point it at this deployment.',\n} as const\n\nexport type TrackingHostDetail = keyof typeof TRACKING_HOST_DETAIL\n\n/**\n * The CNAME target a customer is told to point `links.<domain>` at, or `null`\n * when the deployment names none.\n *\n * `AGLYN_LINK_HOST_TARGET` wins — a self-hoster's proxy hostname. Without it,\n * the Vercel driver's documented target; any other driver has no default to\n * guess, and the card says to point the name at this app instead.\n */\nexport function trackingHostTarget(providerId: string | null | undefined): string | null {\n const configured = String(process.env['AGLYN_LINK_HOST_TARGET'] ?? '').trim().replace(/\\.$/, '')\n if (configured) return configured\n return providerId === 'vercel' ? 'cname.vercel-dns.com' : null\n}\n\n/**\n * The CA that issues a self-served tracking host's certificate — Let's\n * Encrypt, which is what Vercel, Caddy and Traefik use by default. Only ever\n * printed in the CAA row, which only matters to a zone that already\n * publishes CAA.\n */\nexport function trackingHostCertAuthority(): string {\n return String(process.env['AGLYN_LINK_HOST_CA'] ?? '').trim() || 'letsencrypt.org'\n}\n\n/**\n * The records a member publishes for a tracking host: the same tracking rows\n * a sending domain prints, pointed at this deployment, then any the driver\n * asked for.\n */\nexport function trackingHostDnsRecords(\n record: Pick<TrackingHostRecord, 'domain' | 'target' | 'verification'>,\n): SendingDnsRecord[] {\n const domain = normalizeSendingDomain(record?.domain ?? '')\n const rows = trackingRecords(domain, record?.target, trackingHostCertAuthority())\n for (const entry of record?.verification ?? []) {\n const type = String(entry?.type ?? '').toUpperCase()\n if (type !== 'TXT' && type !== 'CNAME') continue\n rows.push({\n type,\n name: String(entry.domain ?? ''),\n value: String(entry.value ?? ''),\n purpose: 'tracking',\n required: true,\n note: 'Proves to the hosting provider that you own this name. It can be removed once the link domain is verified.',\n })\n }\n return rows\n}\n\n/**\n * The origin links should use for mail from `domain` — `https://links.<domain>`\n * — or `null` for a record that is absent or not verified, which means \"use\n * the app's own address\".\n */\nexport function trackingHostLinkOrigin(record: TrackingHostRecord | null | undefined): string | null {\n if (!record || record.status !== 'verified') return null\n const host = sendingTrackingHost(record.domain)\n return host && host === record.host ? `https://${host}` : null\n}\n\n/** A domain a tracking host may be set up for, or an error sentence. */\nexport function validateTrackingHostDomain(input: string): { domain: string | null; error: string | null } {\n return validateSendingDomain(input)\n}\n"],"names":["normalizeSendingDomain","SENDING_TRACKING_SUBDOMAIN","sendingTrackingHost","trackingRecords","validateSendingDomain","TRACKING_HOST_PROBE_PATH","TRACKING_HOST_PROBE_SERVICE","TRACKING_HOST_LINK_ROUTE","LINK_ID_PATH","isTrackingHostName","hostname","host","String","trim","toLowerCase","replace","prefix","startsWith","slice","length","trackingHostPath","pathname","kind","id","exec","rewrite","trackingHostProbeBody","service","readTrackingHostProbe","body","record","TRACKING_HOST_DETAIL","unreachable","trackingHostTarget","providerId","process","configured","env","trackingHostCertAuthority","trackingHostDnsRecords","domain","rows","target","entry","verification","type","toUpperCase","push","name","value","purpose","required","note","trackingHostLinkOrigin","status","validateTrackingHostDomain","input"],"mappings":"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"}
|