@aglyn/tenant-data-admin 1.0.0-beta.200 → 1.0.0-beta.201

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.
Files changed (45) hide show
  1. package/package.json +8 -7
  2. package/src/lib/server/dns-probe.d.ts +7 -0
  3. package/src/lib/server/dns-probe.js +52 -0
  4. package/src/lib/server/dns-probe.js.map +1 -1
  5. package/src/lib/server/email-deliverability.d.ts +150 -0
  6. package/src/lib/server/email-deliverability.js +538 -0
  7. package/src/lib/server/email-deliverability.js.map +1 -0
  8. package/src/lib/server/email-sender-reputation.d.ts +2 -11
  9. package/src/lib/server/email-sender-reputation.js +8 -25
  10. package/src/lib/server/email-sender-reputation.js.map +1 -1
  11. package/src/lib/server/email-suppression.d.ts +10 -1
  12. package/src/lib/server/email-suppression.js +4 -3
  13. package/src/lib/server/email-suppression.js.map +1 -1
  14. package/src/lib/server/erase-person.js +31 -3
  15. package/src/lib/server/erase-person.js.map +1 -1
  16. package/src/lib/server/host-visitor-records.d.ts +16 -2
  17. package/src/lib/server/host-visitor-records.js +31 -6
  18. package/src/lib/server/host-visitor-records.js.map +1 -1
  19. package/src/lib/server/hosted-page-review.d.ts +92 -0
  20. package/src/lib/server/hosted-page-review.js +342 -0
  21. package/src/lib/server/hosted-page-review.js.map +1 -0
  22. package/src/lib/server/org-age.d.ts +27 -0
  23. package/src/lib/server/org-age.js +44 -0
  24. package/src/lib/server/org-age.js.map +1 -0
  25. package/src/lib/server/outbound-send-review.d.ts +222 -0
  26. package/src/lib/server/outbound-send-review.js +479 -0
  27. package/src/lib/server/outbound-send-review.js.map +1 -0
  28. package/src/lib/server/payment-fraud-signal.d.ts +156 -0
  29. package/src/lib/server/payment-fraud-signal.js +368 -0
  30. package/src/lib/server/payment-fraud-signal.js.map +1 -0
  31. package/src/lib/server/payment-risk-record.d.ts +59 -0
  32. package/src/lib/server/payment-risk-record.js +76 -0
  33. package/src/lib/server/payment-risk-record.js.map +1 -0
  34. package/src/lib/server/sending-domains.d.ts +6 -0
  35. package/src/lib/server/sending-domains.js +98 -6
  36. package/src/lib/server/sending-domains.js.map +1 -1
  37. package/src/lib/server/stripe-card-authentication.d.ts +35 -0
  38. package/src/lib/server/stripe-card-authentication.js +63 -0
  39. package/src/lib/server/stripe-card-authentication.js.map +1 -0
  40. package/src/lib/server/tenant-write-lockdown.d.ts +32 -2
  41. package/src/lib/server/tenant-write-lockdown.js +33 -3
  42. package/src/lib/server/tenant-write-lockdown.js.map +1 -1
  43. package/src/tenancy.d.ts +3 -0
  44. package/src/tenancy.js +8 -0
  45. package/src/tenancy.js.map +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/tenant-data-admin",
3
- "version": "1.0.0-beta.200",
3
+ "version": "1.0.0-beta.201",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -25,12 +25,12 @@
25
25
  "./package.json": "./package.json"
26
26
  },
27
27
  "dependencies": {
28
- "@aglyn/aglyn": "1.0.0-beta.200",
29
- "@aglyn/shared-util-email": "1.0.0-beta.200",
30
- "@aglyn/shared-util-fbserver": "1.0.0-beta.200",
31
- "@aglyn/shared-util-first-touch": "1.0.0-beta.200",
32
- "@aglyn/shared-util-http": "1.0.0-beta.200",
33
- "@aglyn/shared-util-tools": "1.0.0-beta.200",
28
+ "@aglyn/aglyn": "1.0.0-beta.201",
29
+ "@aglyn/shared-util-email": "1.0.0-beta.201",
30
+ "@aglyn/shared-util-fbserver": "1.0.0-beta.201",
31
+ "@aglyn/shared-util-first-touch": "1.0.0-beta.201",
32
+ "@aglyn/shared-util-http": "1.0.0-beta.201",
33
+ "@aglyn/shared-util-tools": "1.0.0-beta.201",
34
34
  "@msgpack/msgpack": "^3.1.3",
35
35
  "@swc/helpers": "0.5.23",
36
36
  "sharp": "^0.35.3",
@@ -43,6 +43,7 @@
43
43
  },
44
44
  "sideEffects": [
45
45
  "./src/lib/server/client-error-report.*",
46
+ "./src/lib/server/email-deliverability.*",
46
47
  "./src/lib/server/email-marketing-gate.*",
47
48
  "./src/lib/server/email-send-rate.*"
48
49
  ],
@@ -51,3 +51,10 @@ export interface MxRecord {
51
51
  }
52
52
  /** MX records at `host`, exchanges lowercased and stripped of a trailing dot. */
53
53
  export declare function lookupMx(host: string): Promise<DnsLookupResult<MxRecord>>;
54
+ /**
55
+ * Whether `host` has an address record, A or AAAA — the implicit MX a domain
56
+ * with no MX is still delivered to (RFC 5321 §5.1). `records` holds the
57
+ * addresses found; empty on a conclusive "none". Unanswered only when
58
+ * neither family could be asked.
59
+ */
60
+ export declare function lookupAddress(host: string): Promise<DnsLookupResult<string>>;
@@ -180,5 +180,57 @@
180
180
  }
181
181
  }
182
182
  }
183
+ /**
184
+ * Whether `host` has an address record, A or AAAA — the implicit MX a domain
185
+ * with no MX is still delivered to (RFC 5321 §5.1). `records` holds the
186
+ * addresses found; empty on a conclusive "none". Unanswered only when
187
+ * neither family could be asked.
188
+ */ export async function lookupAddress(host) {
189
+ const ask = async (family)=>{
190
+ try {
191
+ const resolver = new CallbackResolver();
192
+ resolver.setServers(PUBLIC_DNS_RESOLVERS);
193
+ const records = await new Promise((resolve, reject)=>{
194
+ const done = (error, addresses)=>error ? reject(error) : resolve(addresses);
195
+ if (family === 4) resolver.resolve4(host, done);
196
+ else resolver.resolve6(host, done);
197
+ });
198
+ return {
199
+ answered: true,
200
+ records
201
+ };
202
+ } catch (error) {
203
+ const code = error == null ? void 0 : error.code;
204
+ if (isConclusiveDnsCode(code)) return {
205
+ answered: true,
206
+ records: []
207
+ };
208
+ try {
209
+ return {
210
+ answered: true,
211
+ records: family === 4 ? await dns.resolve4(host) : await dns.resolve6(host)
212
+ };
213
+ } catch (fallbackError) {
214
+ const fallbackCode = fallbackError == null ? void 0 : fallbackError.code;
215
+ if (isConclusiveDnsCode(fallbackCode)) return {
216
+ answered: true,
217
+ records: []
218
+ };
219
+ return {
220
+ answered: false,
221
+ records: []
222
+ };
223
+ }
224
+ }
225
+ };
226
+ const v4 = await ask(4);
227
+ if (v4.records.length) return v4;
228
+ const v6 = await ask(6);
229
+ if (v6.records.length) return v6;
230
+ return {
231
+ answered: v4.answered && v6.answered,
232
+ records: []
233
+ };
234
+ }
183
235
 
184
236
  //# sourceMappingURL=dns-probe.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/dns-probe.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\n/**\n * One DNS lookup, pinned to public resolvers, that can say \"I do not know\".\n *\n * The same shape appeared independently in `/api/domains/verify` (AGL-734) and\n * in `sso-provisioning.ts` (AGL-1210), and a third copy was about to be\n * written for sending domains. It is here once instead.\n *\n * ## Why the resolvers are pinned\n *\n * The runtime's default resolver is not a neutral observer. A stale zone once\n * made the hosting platform's own resolver return NXDOMAIN for records every\n * public resolver could see, which reads as \"the customer deleted their\n * record\" and is indistinguishable from the real thing. Asking a public\n * resolver first, and falling back to the runtime's only when the pinned ones\n * are unreachable, keeps a genuine \"no such record\" answer conclusive.\n *\n * ## Why there are three outcomes and not two\n *\n * A boolean cannot tell \"the record is gone\" from \"nobody answered\", and the\n * difference decides whether a customer's working configuration gets marked\n * broken during someone else's outage. `NXDOMAIN`/`ENOTFOUND`/`ENODATA` are\n * ANSWERS — that name has no such record — and stay conclusive. Anything else,\n * from both the pinned resolvers and the fallback, is `unreachable`, and a\n * caller must treat it as evidence of nothing in either direction.\n */\n\nimport { promises as dns, Resolver as CallbackResolver } from 'dns'\n\n/** Resolvers asked before the runtime's own. */\nexport const PUBLIC_DNS_RESOLVERS = ['1.1.1.1', '8.8.8.8']\n\n/** Whether a resolver error code is an answer rather than a failure to get one. */\nexport function isConclusiveDnsCode(code: string | undefined): boolean {\n return code === 'ENOTFOUND' || code === 'ENODATA' || code === 'NXDOMAIN'\n}\n\nexport interface DnsLookupResult<T> {\n /**\n * True when the lookup got an answer, including the answer \"there is\n * nothing here\". False only when nobody answered.\n */\n answered: boolean\n /** Empty on a conclusive miss and on an unreachable lookup alike. */\n records: T[]\n}\n\n/**\n * TXT records at `host`, each joined from its chunks and trimmed.\n *\n * A TXT answer arrives as an array of string CHUNKS per record, because DNS\n * splits strings longer than 255 bytes. Comparing without joining means a long\n * value — every DKIM public key, for instance — can never match.\n */\nexport async function lookupTxt(host: string): Promise<DnsLookupResult<string>> {\n const flatten = (records: string[][]) =>\n records.map((chunks) => chunks.join('').trim())\n\n try {\n const resolver = new CallbackResolver()\n resolver.setServers(PUBLIC_DNS_RESOLVERS)\n const records = await new Promise<string[][]>((resolve, reject) => {\n resolver.resolveTxt(host, (error, addresses) =>\n error ? reject(error) : resolve(addresses),\n )\n })\n return { answered: true, records: flatten(records) }\n } catch (error) {\n const code = (error as NodeJS.ErrnoException)?.code\n if (isConclusiveDnsCode(code)) return { answered: true, records: [] }\n try {\n return { answered: true, records: flatten(await dns.resolveTxt(host)) }\n } catch (fallbackError) {\n const fallbackCode = (fallbackError as NodeJS.ErrnoException)?.code\n if (isConclusiveDnsCode(fallbackCode)) return { answered: true, records: [] }\n return { answered: false, records: [] }\n }\n }\n}\n\n/**\n * CAA records at `host`, as `<flags> <tag> \"<value>\"` strings.\n *\n * NOT walked up the tree. The caller asks about one exact name, because the\n * question this exists for is \"does THIS name publish a policy of its own\",\n * and a walk would answer a different one — a parent's policy is inherited by\n * resolution, not by publication, and reporting the parent's records as the\n * child's would say a name restricts issuance when it publishes nothing.\n */\nexport async function lookupCaa(host: string): Promise<DnsLookupResult<string>> {\n const normalize = (records: unknown[]) =>\n records\n .map((entry) => {\n const row = entry as { critical?: number; issue?: string; issuewild?: string; iodef?: string }\n const tag = row?.issue !== undefined\n ? 'issue'\n : row?.issuewild !== undefined\n ? 'issuewild'\n : row?.iodef !== undefined\n ? 'iodef'\n : ''\n if (!tag) return ''\n const value = String(\n row.issue ?? row.issuewild ?? row.iodef ?? '',\n ).trim()\n return `${Number(row?.critical) || 0} ${tag} \"${value}\"`\n })\n .filter(Boolean)\n\n try {\n const resolver = new CallbackResolver()\n resolver.setServers(PUBLIC_DNS_RESOLVERS)\n const records = await new Promise<unknown[]>((resolve, reject) => {\n resolver.resolveCaa(host, (error, addresses) =>\n error ? reject(error) : resolve(addresses as unknown[]),\n )\n })\n return { answered: true, records: normalize(records) }\n } catch (error) {\n const code = (error as NodeJS.ErrnoException)?.code\n if (isConclusiveDnsCode(code)) return { answered: true, records: [] }\n try {\n return {\n answered: true,\n records: normalize((await dns.resolveCaa(host)) as unknown[]),\n }\n } catch (fallbackError) {\n const fallbackCode = (fallbackError as NodeJS.ErrnoException)?.code\n if (isConclusiveDnsCode(fallbackCode)) return { answered: true, records: [] }\n return { answered: false, records: [] }\n }\n }\n}\n\nexport interface MxRecord {\n exchange: string\n priority: number\n}\n\n/** MX records at `host`, exchanges lowercased and stripped of a trailing dot. */\nexport async function lookupMx(host: string): Promise<DnsLookupResult<MxRecord>> {\n const normalize = (records: { exchange: string; priority: number }[]) =>\n records.map((entry) => ({\n exchange: String(entry?.exchange ?? '')\n .trim()\n .toLowerCase()\n .replace(/\\.$/, ''),\n priority: Number(entry?.priority) || 0,\n }))\n\n try {\n const resolver = new CallbackResolver()\n resolver.setServers(PUBLIC_DNS_RESOLVERS)\n const records = await new Promise<{ exchange: string; priority: number }[]>(\n (resolve, reject) => {\n resolver.resolveMx(host, (error, addresses) =>\n error ? reject(error) : resolve(addresses),\n )\n },\n )\n return { answered: true, records: normalize(records) }\n } catch (error) {\n const code = (error as NodeJS.ErrnoException)?.code\n if (isConclusiveDnsCode(code)) return { answered: true, records: [] }\n try {\n return { answered: true, records: normalize(await dns.resolveMx(host)) }\n } catch (fallbackError) {\n const fallbackCode = (fallbackError as NodeJS.ErrnoException)?.code\n if (isConclusiveDnsCode(fallbackCode)) return { answered: true, records: [] }\n return { answered: false, records: [] }\n }\n }\n}\n"],"names":["promises","dns","Resolver","CallbackResolver","PUBLIC_DNS_RESOLVERS","isConclusiveDnsCode","code","lookupTxt","host","flatten","records","map","chunks","join","trim","resolver","setServers","Promise","resolve","reject","resolveTxt","error","addresses","answered","fallbackError","fallbackCode","lookupCaa","normalize","entry","row","tag","issue","undefined","issuewild","iodef","value","String","Number","critical","filter","Boolean","resolveCaa","lookupMx","exchange","toLowerCase","replace","priority","resolveMx"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/dns-probe.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\n/**\n * One DNS lookup, pinned to public resolvers, that can say \"I do not know\".\n *\n * The same shape appeared independently in `/api/domains/verify` (AGL-734) and\n * in `sso-provisioning.ts` (AGL-1210), and a third copy was about to be\n * written for sending domains. It is here once instead.\n *\n * ## Why the resolvers are pinned\n *\n * The runtime's default resolver is not a neutral observer. A stale zone once\n * made the hosting platform's own resolver return NXDOMAIN for records every\n * public resolver could see, which reads as \"the customer deleted their\n * record\" and is indistinguishable from the real thing. Asking a public\n * resolver first, and falling back to the runtime's only when the pinned ones\n * are unreachable, keeps a genuine \"no such record\" answer conclusive.\n *\n * ## Why there are three outcomes and not two\n *\n * A boolean cannot tell \"the record is gone\" from \"nobody answered\", and the\n * difference decides whether a customer's working configuration gets marked\n * broken during someone else's outage. `NXDOMAIN`/`ENOTFOUND`/`ENODATA` are\n * ANSWERS — that name has no such record — and stay conclusive. Anything else,\n * from both the pinned resolvers and the fallback, is `unreachable`, and a\n * caller must treat it as evidence of nothing in either direction.\n */\n\nimport { promises as dns, Resolver as CallbackResolver } from 'dns'\n\n/** Resolvers asked before the runtime's own. */\nexport const PUBLIC_DNS_RESOLVERS = ['1.1.1.1', '8.8.8.8']\n\n/** Whether a resolver error code is an answer rather than a failure to get one. */\nexport function isConclusiveDnsCode(code: string | undefined): boolean {\n return code === 'ENOTFOUND' || code === 'ENODATA' || code === 'NXDOMAIN'\n}\n\nexport interface DnsLookupResult<T> {\n /**\n * True when the lookup got an answer, including the answer \"there is\n * nothing here\". False only when nobody answered.\n */\n answered: boolean\n /** Empty on a conclusive miss and on an unreachable lookup alike. */\n records: T[]\n}\n\n/**\n * TXT records at `host`, each joined from its chunks and trimmed.\n *\n * A TXT answer arrives as an array of string CHUNKS per record, because DNS\n * splits strings longer than 255 bytes. Comparing without joining means a long\n * value — every DKIM public key, for instance — can never match.\n */\nexport async function lookupTxt(host: string): Promise<DnsLookupResult<string>> {\n const flatten = (records: string[][]) =>\n records.map((chunks) => chunks.join('').trim())\n\n try {\n const resolver = new CallbackResolver()\n resolver.setServers(PUBLIC_DNS_RESOLVERS)\n const records = await new Promise<string[][]>((resolve, reject) => {\n resolver.resolveTxt(host, (error, addresses) =>\n error ? reject(error) : resolve(addresses),\n )\n })\n return { answered: true, records: flatten(records) }\n } catch (error) {\n const code = (error as NodeJS.ErrnoException)?.code\n if (isConclusiveDnsCode(code)) return { answered: true, records: [] }\n try {\n return { answered: true, records: flatten(await dns.resolveTxt(host)) }\n } catch (fallbackError) {\n const fallbackCode = (fallbackError as NodeJS.ErrnoException)?.code\n if (isConclusiveDnsCode(fallbackCode)) return { answered: true, records: [] }\n return { answered: false, records: [] }\n }\n }\n}\n\n/**\n * CAA records at `host`, as `<flags> <tag> \"<value>\"` strings.\n *\n * NOT walked up the tree. The caller asks about one exact name, because the\n * question this exists for is \"does THIS name publish a policy of its own\",\n * and a walk would answer a different one — a parent's policy is inherited by\n * resolution, not by publication, and reporting the parent's records as the\n * child's would say a name restricts issuance when it publishes nothing.\n */\nexport async function lookupCaa(host: string): Promise<DnsLookupResult<string>> {\n const normalize = (records: unknown[]) =>\n records\n .map((entry) => {\n const row = entry as { critical?: number; issue?: string; issuewild?: string; iodef?: string }\n const tag = row?.issue !== undefined\n ? 'issue'\n : row?.issuewild !== undefined\n ? 'issuewild'\n : row?.iodef !== undefined\n ? 'iodef'\n : ''\n if (!tag) return ''\n const value = String(\n row.issue ?? row.issuewild ?? row.iodef ?? '',\n ).trim()\n return `${Number(row?.critical) || 0} ${tag} \"${value}\"`\n })\n .filter(Boolean)\n\n try {\n const resolver = new CallbackResolver()\n resolver.setServers(PUBLIC_DNS_RESOLVERS)\n const records = await new Promise<unknown[]>((resolve, reject) => {\n resolver.resolveCaa(host, (error, addresses) =>\n error ? reject(error) : resolve(addresses as unknown[]),\n )\n })\n return { answered: true, records: normalize(records) }\n } catch (error) {\n const code = (error as NodeJS.ErrnoException)?.code\n if (isConclusiveDnsCode(code)) return { answered: true, records: [] }\n try {\n return {\n answered: true,\n records: normalize((await dns.resolveCaa(host)) as unknown[]),\n }\n } catch (fallbackError) {\n const fallbackCode = (fallbackError as NodeJS.ErrnoException)?.code\n if (isConclusiveDnsCode(fallbackCode)) return { answered: true, records: [] }\n return { answered: false, records: [] }\n }\n }\n}\n\nexport interface MxRecord {\n exchange: string\n priority: number\n}\n\n/** MX records at `host`, exchanges lowercased and stripped of a trailing dot. */\nexport async function lookupMx(host: string): Promise<DnsLookupResult<MxRecord>> {\n const normalize = (records: { exchange: string; priority: number }[]) =>\n records.map((entry) => ({\n exchange: String(entry?.exchange ?? '')\n .trim()\n .toLowerCase()\n .replace(/\\.$/, ''),\n priority: Number(entry?.priority) || 0,\n }))\n\n try {\n const resolver = new CallbackResolver()\n resolver.setServers(PUBLIC_DNS_RESOLVERS)\n const records = await new Promise<{ exchange: string; priority: number }[]>(\n (resolve, reject) => {\n resolver.resolveMx(host, (error, addresses) =>\n error ? reject(error) : resolve(addresses),\n )\n },\n )\n return { answered: true, records: normalize(records) }\n } catch (error) {\n const code = (error as NodeJS.ErrnoException)?.code\n if (isConclusiveDnsCode(code)) return { answered: true, records: [] }\n try {\n return { answered: true, records: normalize(await dns.resolveMx(host)) }\n } catch (fallbackError) {\n const fallbackCode = (fallbackError as NodeJS.ErrnoException)?.code\n if (isConclusiveDnsCode(fallbackCode)) return { answered: true, records: [] }\n return { answered: false, records: [] }\n }\n }\n}\n\n/**\n * Whether `host` has an address record, A or AAAA — the implicit MX a domain\n * with no MX is still delivered to (RFC 5321 §5.1). `records` holds the\n * addresses found; empty on a conclusive \"none\". Unanswered only when\n * neither family could be asked.\n */\nexport async function lookupAddress(host: string): Promise<DnsLookupResult<string>> {\n const ask = async (family: 4 | 6): Promise<DnsLookupResult<string>> => {\n try {\n const resolver = new CallbackResolver()\n resolver.setServers(PUBLIC_DNS_RESOLVERS)\n const records = await new Promise<string[]>((resolve, reject) => {\n const done = (error: NodeJS.ErrnoException | null, addresses: string[]) =>\n error ? reject(error) : resolve(addresses)\n if (family === 4) resolver.resolve4(host, done)\n else resolver.resolve6(host, done)\n })\n return { answered: true, records }\n } catch (error) {\n const code = (error as NodeJS.ErrnoException)?.code\n if (isConclusiveDnsCode(code)) return { answered: true, records: [] }\n try {\n return {\n answered: true,\n records: family === 4 ? await dns.resolve4(host) : await dns.resolve6(host),\n }\n } catch (fallbackError) {\n const fallbackCode = (fallbackError as NodeJS.ErrnoException)?.code\n if (isConclusiveDnsCode(fallbackCode)) return { answered: true, records: [] }\n return { answered: false, records: [] }\n }\n }\n }\n const v4 = await ask(4)\n if (v4.records.length) return v4\n const v6 = await ask(6)\n if (v6.records.length) return v6\n return { answered: v4.answered && v6.answered, records: [] }\n}\n"],"names":["promises","dns","Resolver","CallbackResolver","PUBLIC_DNS_RESOLVERS","isConclusiveDnsCode","code","lookupTxt","host","flatten","records","map","chunks","join","trim","resolver","setServers","Promise","resolve","reject","resolveTxt","error","addresses","answered","fallbackError","fallbackCode","lookupCaa","normalize","entry","row","tag","issue","undefined","issuewild","iodef","value","String","Number","critical","filter","Boolean","resolveCaa","lookupMx","exchange","toLowerCase","replace","priority","resolveMx","lookupAddress","ask","family","done","resolve4","resolve6","v4","length","v6"],"mappings":"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"}
@@ -0,0 +1,150 @@
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
+ */
17
+ import { type EmailDeliverabilityPreflightRequest, type EmailDeliverabilityPreflightVerdict, type EmailDeliverabilityVerdict, type EmailDeliveryEvent, type EmailSendPurpose, type MailDnsResolver, type MailDomainIntel, type MailGateway, type MailGatewayLedger, type MailGatewayOutcome, type MailGatewayStanding } from '@aglyn/shared-util-email';
18
+ import type { EmailDeliveryEventOutcome } from './email-delivery-log';
19
+ type Firestore = FirebaseFirestore.Firestore;
20
+ /** The platform MX cache, keyed by recipient domain. */
21
+ export declare const MAIL_DOMAINS_COLLECTION = "mailDomains";
22
+ /** The gateway ledger, at the top level and under an organization — see the module note. */
23
+ export declare const MAIL_GATEWAY_LEDGER_COLLECTION = "mailGatewayLedger";
24
+ /**
25
+ * Where a ledger lives: `null` for the top level (a Resend sending domain),
26
+ * or the organization a connected mailbox belongs to.
27
+ */
28
+ export type MailGatewayLedgerScope = {
29
+ orgId: string;
30
+ } | null;
31
+ /** What the reads and writes here need beside their input; every field defaults to the platform's. */
32
+ export interface MailDeliverabilityDeps {
33
+ firestore?: Firestore;
34
+ resolver?: MailDnsResolver;
35
+ nowMs?: number;
36
+ }
37
+ /**
38
+ * The platform's resolver: the pinned public resolvers first, and — for the
39
+ * one answer that stops mail, "no MX" — the runtime's own as a second
40
+ * opinion that must agree. See the module note.
41
+ */
42
+ export declare function platformMailDnsResolver(): MailDnsResolver;
43
+ /** Test seam: forget everything remembered in this process. */
44
+ export declare function resetMailDeliverabilityMemoryForTests(): void;
45
+ /**
46
+ * Each domain's MX answer, keyed by domain: remembered, stored and fresh, or
47
+ * looked up and stored. `null` for a domain that could not be looked up and
48
+ * has no answer that may stand — see the module note.
49
+ */
50
+ export declare function readMailDomains(domains: readonly string[], deps?: MailDeliverabilityDeps): Promise<Map<string, MailDomainIntel | null>>;
51
+ /**
52
+ * Whether a block on this sending domain hurts every tenant — Aglyn's own
53
+ * domain or a shared pool member — which the staff console surfaces first.
54
+ */
55
+ export declare function isSharedMailSendingDomain(domain: string | null | undefined): boolean;
56
+ /** The ledger of each gateway named for one sending domain, keyed by gateway; `null` where none exists yet. */
57
+ export declare function readMailGatewayLedgers(scope: MailGatewayLedgerScope, sendingDomain: string, gateways: readonly MailGateway[], deps?: MailDeliverabilityDeps): Promise<Map<MailGateway, MailGatewayLedger | null>>;
58
+ /**
59
+ * Counts one outcome on a sending domain's ledger with a gateway. Never
60
+ * throws: the send or the bounce it records has already happened, and a
61
+ * lost count understates a gateway while a thrown one loses the step that
62
+ * called. A delivery past the day's cap costs no write at all.
63
+ */
64
+ export declare function recordMailGatewayOutcome(scope: MailGatewayLedgerScope, input: {
65
+ sendingDomain: string;
66
+ gateway: MailGateway;
67
+ outcome: MailGatewayOutcome;
68
+ count?: number;
69
+ atMs: number;
70
+ /** A refusal's diagnostic, scrubbed here of addresses before it is kept. */
71
+ detail?: string | null;
72
+ }, deps?: MailDeliverabilityDeps): Promise<void>;
73
+ /** What a batch read needs to know about the mail being sent. */
74
+ export interface MailDeliverabilityQuestion {
75
+ emails: readonly string[];
76
+ purpose: EmailSendPurpose;
77
+ /** The domain the mail leaves from; without one, no ledger is read and nothing is held. */
78
+ sendingDomain: string | null;
79
+ scope: MailGatewayLedgerScope;
80
+ /** Addresses a person already released the gateway hold for. */
81
+ holdReleased?: (email: string) => boolean;
82
+ }
83
+ /**
84
+ * The deliverability verdict for every address, keyed by the address as
85
+ * given: the MX answers read (and refreshed) above, joined to the sending
86
+ * domain's ledger with each gateway they name. A ledger that cannot be read
87
+ * reads as no history, which holds nothing.
88
+ */
89
+ export declare function readMailDeliverability(question: MailDeliverabilityQuestion, deps?: MailDeliverabilityDeps): Promise<Map<string, EmailDeliverabilityVerdict>>;
90
+ /** Whether a verdict refuses because the domain takes no mail — the refusal that suppresses. */
91
+ export declare function isNoMailServerVerdict(verdict: EmailDeliverabilityVerdict | null | undefined): boolean;
92
+ /** Whether a verdict holds bulk mail for its gateway. */
93
+ export declare function isGatewayHeldVerdict(verdict: EmailDeliverabilityVerdict | null | undefined): boolean;
94
+ /** A campaign's audience after the checks, with who was excluded and why. */
95
+ export interface DeliverableRecipients {
96
+ deliverable: string[];
97
+ /** Addresses whose domain takes no mail. */
98
+ noMailServer: string[];
99
+ /** Addresses behind a gateway that refused this sending domain twice lately. */
100
+ gatewayHeld: string[];
101
+ }
102
+ /**
103
+ * A bulk audience with the addresses that would bounce, or that sit behind
104
+ * a gateway holding this sending domain, taken out — the count a campaign's
105
+ * review shows before Send, and the filter its batches run. Fails open: an
106
+ * error keeps everyone.
107
+ */
108
+ export declare function filterDeliverableRecipients(input: {
109
+ emails: readonly string[];
110
+ sendingDomain: string | null;
111
+ purpose?: EmailSendPurpose;
112
+ }, deps?: MailDeliverabilityDeps): Promise<DeliverableRecipients>;
113
+ /**
114
+ * What `sendEmail` asks before a message leaves — see the seam in
115
+ * `@aglyn/shared-util-email`. Refuses every recipient whose domain takes no
116
+ * mail, and files the address on the platform suppression list with its own
117
+ * reason; holds bulk mail behind a gateway that refused this sending domain
118
+ * twice. The Resend path's ledger is keyed by the sending domain alone.
119
+ */
120
+ export declare function runEmailDeliverabilityPreflight(request: EmailDeliverabilityPreflightRequest, deps?: MailDeliverabilityDeps): Promise<EmailDeliverabilityPreflightVerdict>;
121
+ /**
122
+ * Puts the preflight on `sendEmail`'s path. **Called at module load**, from
123
+ * the bottom of this file, for the reason `installEmailSendGovernor` is.
124
+ * Idempotent; the closure holds no state of its own.
125
+ */
126
+ export declare function installEmailDeliverabilityPreflight(): void;
127
+ /**
128
+ * Counts what a Resend webhook reported on the sending domain's ledger: a
129
+ * delivery credits the gateway in front of the recipient, and a permanent
130
+ * bounce that reads as the gateway refusing the SENDER — not an unknown
131
+ * address — counts as a block, filed under the gateway the bounce names
132
+ * (its remote server, or its own words) or else the domain's MX. Only an
133
+ * event the delivery log saw for the first time counts, so a redelivered
134
+ * webhook counts nothing twice. Answers how many outcomes it recorded.
135
+ */
136
+ export declare function recordDeliverabilityFromDeliveryEvents(events: readonly EmailDeliveryEvent[], outcomes: readonly EmailDeliveryEventOutcome[] | null, deps?: MailDeliverabilityDeps): Promise<number>;
137
+ /** One row of the platform ledger as the staff console shows it. */
138
+ export interface MailGatewayLedgerRow extends MailGatewayLedger {
139
+ id: string;
140
+ /** A pooled or platform sending domain: a block there hurts every tenant. */
141
+ shared: boolean;
142
+ standing: MailGatewayStanding;
143
+ /** Whether bulk mail from this domain into this gateway is held today. */
144
+ holds: boolean;
145
+ }
146
+ /** The platform ledger, most recently moved first. */
147
+ export declare function listMailGatewayLedgers(options?: {
148
+ limit?: number;
149
+ }, deps?: MailDeliverabilityDeps): Promise<MailGatewayLedgerRow[]>;
150
+ export {};