@aglyn/plugins-outreach 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 (90) hide show
  1. package/package.json +14 -14
  2. package/src/lib/components/enrollment-actions.d.ts +33 -0
  3. package/src/lib/components/enrollment-actions.js +174 -0
  4. package/src/lib/components/enrollment-actions.js.map +1 -0
  5. package/src/lib/components/enrollment-detail.d.ts +21 -0
  6. package/src/lib/components/enrollment-detail.js +870 -0
  7. package/src/lib/components/enrollment-detail.js.map +1 -0
  8. package/src/lib/components/enrollment-filter-params.d.ts +58 -0
  9. package/src/lib/components/enrollment-filter-params.js +98 -0
  10. package/src/lib/components/enrollment-filter-params.js.map +1 -0
  11. package/src/lib/components/enrollments-table.d.ts +21 -0
  12. package/src/lib/components/enrollments-table.js +249 -210
  13. package/src/lib/components/enrollments-table.js.map +1 -1
  14. package/src/lib/components/outreach-console-page.js +10 -0
  15. package/src/lib/components/outreach-console-page.js.map +1 -1
  16. package/src/lib/components/sequence-detail.d.ts +2 -0
  17. package/src/lib/components/sequence-detail.js +70 -8
  18. package/src/lib/components/sequence-detail.js.map +1 -1
  19. package/src/lib/components/sequence-report-card.d.ts +17 -0
  20. package/src/lib/components/sequence-report-card.js +63 -15
  21. package/src/lib/components/sequence-report-card.js.map +1 -1
  22. package/src/lib/components/sequences-section.d.ts +4 -1
  23. package/src/lib/components/sequences-section.js +12 -1
  24. package/src/lib/components/sequences-section.js.map +1 -1
  25. package/src/lib/components/use-outreach-api.d.ts +3 -1
  26. package/src/lib/components/use-outreach-api.js +6 -0
  27. package/src/lib/components/use-outreach-api.js.map +1 -1
  28. package/src/lib/components/use-outreach-enrollment.d.ts +18 -0
  29. package/src/lib/components/use-outreach-enrollment.js +133 -0
  30. package/src/lib/components/use-outreach-enrollment.js.map +1 -0
  31. package/src/lib/constants/api-routes.d.ts +2 -0
  32. package/src/lib/constants/api-routes.js +1 -0
  33. package/src/lib/constants/api-routes.js.map +1 -1
  34. package/src/lib/engine/gates.d.ts +4 -3
  35. package/src/lib/engine/gates.js.map +1 -1
  36. package/src/lib/engine/gateway-block.d.ts +4 -8
  37. package/src/lib/engine/gateway-block.js +6 -26
  38. package/src/lib/engine/gateway-block.js.map +1 -1
  39. package/src/lib/engine/mail-gateway.d.ts +18 -73
  40. package/src/lib/engine/mail-gateway.js +32 -206
  41. package/src/lib/engine/mail-gateway.js.map +1 -1
  42. package/src/lib/model/enrollment-engagement.d.ts +26 -2
  43. package/src/lib/model/enrollment-engagement.js +40 -3
  44. package/src/lib/model/enrollment-engagement.js.map +1 -1
  45. package/src/lib/model/enrollment-history.d.ts +40 -0
  46. package/src/lib/model/enrollment-history.js +95 -0
  47. package/src/lib/model/enrollment-history.js.map +1 -0
  48. package/src/lib/model/enrollment-list-query.d.ts +6 -0
  49. package/src/lib/model/enrollment-list-query.js +35 -1
  50. package/src/lib/model/enrollment-list-query.js.map +1 -1
  51. package/src/lib/model/enrollment-timeline.d.ts +78 -0
  52. package/src/lib/model/enrollment-timeline.js +430 -0
  53. package/src/lib/model/enrollment-timeline.js.map +1 -0
  54. package/src/lib/model/outreach-api.d.ts +24 -0
  55. package/src/lib/model/outreach-api.js.map +1 -1
  56. package/src/lib/model/outreach.types.d.ts +78 -29
  57. package/src/lib/model/outreach.types.js +33 -8
  58. package/src/lib/model/outreach.types.js.map +1 -1
  59. package/src/lib/routes/enroll-routes.js +26 -3
  60. package/src/lib/routes/enroll-routes.js.map +1 -1
  61. package/src/lib/routes/enrollment-routes.d.ts +10 -0
  62. package/src/lib/routes/enrollment-routes.js +82 -1
  63. package/src/lib/routes/enrollment-routes.js.map +1 -1
  64. package/src/lib/routes/register-routes.js +14 -4
  65. package/src/lib/routes/register-routes.js.map +1 -1
  66. package/src/lib/routes/route-deps.d.ts +3 -1
  67. package/src/lib/routes/route-deps.js.map +1 -1
  68. package/src/lib/runtime/click-events.d.ts +10 -5
  69. package/src/lib/runtime/click-events.js +45 -6
  70. package/src/lib/runtime/click-events.js.map +1 -1
  71. package/src/lib/runtime/gateway-ledger.d.ts +6 -0
  72. package/src/lib/runtime/gateway-ledger.js +9 -3
  73. package/src/lib/runtime/gateway-ledger.js.map +1 -1
  74. package/src/lib/runtime/host-suspension.d.ts +30 -0
  75. package/src/lib/runtime/host-suspension.js +35 -0
  76. package/src/lib/runtime/host-suspension.js.map +1 -0
  77. package/src/lib/runtime/person-erasure.d.ts +4 -1
  78. package/src/lib/runtime/person-erasure.js +14 -3
  79. package/src/lib/runtime/person-erasure.js.map +1 -1
  80. package/src/lib/runtime/platform-runtime-deps.js +34 -3
  81. package/src/lib/runtime/platform-runtime-deps.js.map +1 -1
  82. package/src/lib/runtime/runtime-deps.d.ts +27 -1
  83. package/src/lib/runtime/runtime-deps.js.map +1 -1
  84. package/src/lib/runtime/send-job.js +46 -6
  85. package/src/lib/runtime/send-job.js.map +1 -1
  86. package/src/lib/runtime/sync-job.js +4 -0
  87. package/src/lib/runtime/sync-job.js.map +1 -1
  88. package/src/lib/storage/domain-intel-store.d.ts +61 -43
  89. package/src/lib/storage/domain-intel-store.js +138 -226
  90. package/src/lib/storage/domain-intel-store.js.map +1 -1
@@ -1,3 +1,4 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
1
2
  /**
2
3
  * @license
3
4
  * Copyright 2026 Aglyn LLC
@@ -14,64 +15,103 @@
14
15
  * See the License for the specific language governing permissions and
15
16
  * limitations under the License.
16
17
  */ /**
17
- * WHAT THE ORGANIZATION KNOWS ABOUT THE DOMAINS IT WRITES TO (AGL-3326).
18
+ * WHAT SEQUENCES KNOWS ABOUT THE DOMAINS IT WRITES TO (AGL-3326).
18
19
  *
19
- * Two documents, both server-written:
20
+ * Both halves live on the platform (AGL-3328), in `@aglyn/tenant-data-admin`'s
21
+ * `email-deliverability`, which the Resend path reads and writes too:
20
22
  *
21
- * - `outreachDomainIntel/{domain}` — the domain's MX records and the mail
22
- * gateway they name, looked up with the resolver the caller hands in and
23
- * trusted for {@link OUTREACH_DOMAIN_INTEL_TTL_MS}; beside them, what this
24
- * organization's mail met at the domain;
25
- * - `outreachGatewayStats/{gateway}` — the same three counts per gateway,
26
- * in total and by UTC day, which is the ledger the hold and the
27
- * Check-people chip read.
23
+ * - a recipient domain's MX — the platform-wide `mailDomains/{domain}`
24
+ * cache, looked up with the resolver the caller hands in and trusted for
25
+ * a week (a day for a domain that takes no mail). The first workspace to
26
+ * write to a domain warms it for every other;
27
+ * - the ledger — `orgs/{orgId}/mailGatewayLedger/{sendingDomain}~{gateway}`,
28
+ * keyed by the MAILBOX's sending domain, because a gateway refuses a
29
+ * sender: the Barracuda that refused `aglyn.io` refuses `aglyn.io`, not
30
+ * the organization's other mailbox on another domain. It stays under the
31
+ * organization because a mailbox's domain is not one organization's — two
32
+ * workspaces can each connect a `gmail.com` mailbox.
28
33
  *
29
34
  * A read that could not be made answers `null` for the addresses it
30
35
  * covered, which the gates refuse as "we couldn't check" — except that a
31
- * stale answer outranks no answer: a domain whose MX was known last week
32
- * and cannot be looked up today is read as it was, because a resolver
33
- * timing out says nothing about the domain. A domain that resolves to no
34
- * MX at all is `none`, and the gates block it.
36
+ * stale answer that the domain takes mail outranks no answer. A domain that
37
+ * resolves to no MX is `none`, and the gates block it.
35
38
  *
36
- * The ledger is written in a transaction over both documents so a run that
37
- * records a block and a run that credits a delivery cannot lose each
38
- * other's count, and the days map is pruned on every write so it never
39
- * grows past the window it serves.
40
- */ import { _ as _extends } from "@swc/helpers/_/_extends";
41
- import { classifyOutreachMailGateway, OUTREACH_DOMAIN_INTEL_TTL_MS, OUTREACH_GATEWAY_CHIP_DAYS, OUTREACH_GATEWAY_WINDOW_DAYS, OUTREACH_MAIL_GATEWAYS, outreachGatewayDay, outreachGatewayWindow, pruneOutreachGatewayDays } from "../engine/mail-gateway.js";
42
- import { normalizeOutreachDomain, outreachEmailDomain } from "../engine/do-not-contact-domain.js";
39
+ * ## The two collections this module wrote first
40
+ *
41
+ * `outreachDomainIntel` and `outreachGatewayStats` are READ THROUGH, not
42
+ * migrated. The first held MX answers, which a lookup re-derives, so it is
43
+ * no longer read. The second held a per-gateway ledger with no sending
44
+ * domain; its days are summed into every sending domain's standing for the
45
+ * thirty days they can still hold a send — the conservative reading, since
46
+ * an organization with one mailbox domain loses nothing and one with two
47
+ * holds a little more. Nothing writes either any more; both are retired,
48
+ * with their rules, once the window has passed.
49
+ */ import { mailGatewayStanding, normalizeLedgerSendingDomain, resolveMailDomain } from "@aglyn/shared-util-email";
50
+ import { readMailDomains, readMailGatewayLedgers, recordMailGatewayOutcome } from "@aglyn/tenant-data-admin/server/email-deliverability";
51
+ import { OUTREACH_MAIL_GATEWAYS } from "../engine/mail-gateway.js";
52
+ import { outreachEmailDomain } from "../engine/do-not-contact-domain.js";
43
53
  import { outreachOrgCollection } from "./outreach-records.js";
44
- /** `getAll` takes this many references at most per call. */ const GET_ALL_CHUNK = 300;
45
- /** The resolver's codes that mean "this domain has no MX", as opposed to "I could not ask". */ const NO_MX_CODES = new Set([
46
- 'ENOTFOUND',
47
- 'ENODATA',
48
- 'NXDOMAIN',
49
- 'NOTFOUND'
50
- ]);
54
+ /** The resolver the platform store reads with, from what a caller hands in. */ function resolverOf(input) {
55
+ return _extends({
56
+ resolveMx: input.resolveMx
57
+ }, input.resolveAddress ? {
58
+ resolveAddress: input.resolveAddress
59
+ } : {});
60
+ }
61
+ /** The domain a mailbox's mail leaves from: its send-as address's, else the account's. */ export function outreachMailboxSendingDomain(mailbox) {
62
+ return normalizeLedgerSendingDomain((mailbox == null ? void 0 : mailbox.sendAs) || (mailbox == null ? void 0 : mailbox.email));
63
+ }
64
+ /**
65
+ * A domain's MX, resolved and classified. A domain the resolver says has no
66
+ * MX answers `none`; a resolver that could not answer rejects, and the
67
+ * caller decides what a missing answer means.
68
+ */ export async function resolveOutreachDomainMx(resolveMx, domain) {
69
+ const intel = await resolveMailDomain({
70
+ resolveMx
71
+ }, domain, 0);
72
+ return {
73
+ mx: intel.mx,
74
+ gateway: intel.gateway
75
+ };
76
+ }
77
+ /**
78
+ * The MX answer for every address, keyed by address — read through the
79
+ * platform cache, and looked up and stored when missing or stale. An
80
+ * address with no domain, and one whose domain could not be looked up and
81
+ * was never known, answers `null`.
82
+ */ export async function readOutreachDomainIntel(firestore, _orgId, emails, input) {
83
+ var _intel_get;
84
+ const answers = new Map();
85
+ const domainOf = new Map();
86
+ for (const email of emails){
87
+ const domain = outreachEmailDomain(email);
88
+ if (domain) domainOf.set(email, domain);
89
+ else answers.set(email, null);
90
+ }
91
+ if (!domainOf.size) return answers;
92
+ let intel;
93
+ try {
94
+ intel = await readMailDomains([
95
+ ...new Set(domainOf.values())
96
+ ], {
97
+ firestore,
98
+ resolver: resolverOf(input),
99
+ nowMs: input.nowMs
100
+ });
101
+ } catch (error) {
102
+ console.error('[outreach] domain intel lookup failed; reading as unchecked', error);
103
+ intel = new Map();
104
+ }
105
+ for (const [email, domain] of domainOf)answers.set(email, (_intel_get = intel.get(domain)) != null ? _intel_get : null);
106
+ return answers;
107
+ }
51
108
  const count = (value)=>{
52
109
  const number = Number(value);
53
110
  return Number.isFinite(number) ? Math.max(0, Math.floor(number)) : 0;
54
111
  };
55
112
  const ms = (value)=>typeof value === 'number' && Number.isFinite(value) ? value : null;
56
113
  const gatewayOf = (value)=>OUTREACH_MAIL_GATEWAYS.includes(value) ? value : null;
57
- /** A stored domain-intel document in its model shape, or `null` for no document. */ export function readStoredOutreachDomainIntel(id, data) {
58
- var _ms, _ms1;
59
- if (!data) return null;
60
- const gateway = gatewayOf(data['gateway']);
61
- if (!gateway) return null;
62
- return {
63
- domain: typeof data['domain'] === 'string' && data['domain'] ? data['domain'] : id,
64
- mx: Array.isArray(data['mx']) ? data['mx'].filter((entry)=>typeof entry === 'string') : [],
65
- gateway,
66
- resolvedAtMs: (_ms = ms(data['resolvedAtMs'])) != null ? _ms : 0,
67
- sent: count(data['sent']),
68
- delivered: count(data['delivered']),
69
- blocked: count(data['blocked']),
70
- lastBlockedAtMs: ms(data['lastBlockedAtMs']),
71
- updatedAtMs: (_ms1 = ms(data['updatedAtMs'])) != null ? _ms1 : 0
72
- };
73
- }
74
- /** A stored gateway ledger in its model shape, or `null` for no document. */ export function readStoredOutreachGatewayStats(id, data) {
114
+ /** A stored per-gateway ledger from before the sending domain keyed it, or `null` for no document. */ export function readStoredOutreachGatewayStats(id, data) {
75
115
  var _gatewayOf, _ms;
76
116
  if (!data) return null;
77
117
  const gateway = (_gatewayOf = gatewayOf(data['gateway'])) != null ? _gatewayOf : gatewayOf(id);
@@ -98,118 +138,13 @@ const gatewayOf = (value)=>OUTREACH_MAIL_GATEWAYS.includes(value) ? value : null
98
138
  updatedAtMs: (_ms = ms(data['updatedAtMs'])) != null ? _ms : 0
99
139
  };
100
140
  }
101
- /**
102
- * A domain's MX, resolved and classified — see the module note. A domain
103
- * the resolver says has no MX answers `none`; a resolver that could not
104
- * answer rejects, and the caller decides what a missing answer means.
105
- */ export async function resolveOutreachDomainMx(resolveMx, domain) {
106
- let records;
107
- try {
108
- records = await resolveMx(domain);
109
- } catch (error) {
110
- var _ref;
111
- const code = String((_ref = error == null ? void 0 : error.code) != null ? _ref : '').toUpperCase();
112
- if (NO_MX_CODES.has(code)) return {
113
- mx: [],
114
- gateway: 'none'
115
- };
116
- throw error;
117
- }
118
- const mx = [
119
- ...records
120
- ].filter((record)=>record && typeof record.exchange === 'string').sort((a, b)=>(Number(a.priority) || 0) - (Number(b.priority) || 0)).map((record)=>record.exchange.trim().toLowerCase().replace(/\.$/, ''))// RFC 7505's null MX — a lone "." — says the domain takes no mail.
121
- .filter((exchange)=>exchange && exchange !== '.');
122
- return {
123
- mx,
124
- gateway: classifyOutreachMailGateway(mx)
125
- };
126
- }
127
- const intelCollection = (firestore, orgId)=>outreachOrgCollection(firestore, orgId, 'domainIntel');
128
- const statsCollection = (firestore, orgId)=>outreachOrgCollection(firestore, orgId, 'gatewayStats');
129
- /**
130
- * The domain intel for every address, keyed by address — fresh from the
131
- * store, or looked up and stored when missing or older than the TTL. An
132
- * address with no domain, and one whose domain could not be looked up and
133
- * was never known, answers `null`.
134
- */ export async function readOutreachDomainIntel(firestore, orgId, emails, input) {
135
- var _known_get;
136
- const answers = new Map();
137
- const domainOf = new Map();
138
- for (const email of emails){
139
- const domain = outreachEmailDomain(email);
140
- if (domain) domainOf.set(email, domain);
141
- else answers.set(email, null);
142
- }
143
- const domains = [
144
- ...new Set(domainOf.values())
145
- ];
146
- if (!domains.length) return answers;
147
- const collection = intelCollection(firestore, orgId);
148
- const known = new Map();
149
- try {
150
- for(let start = 0; start < domains.length; start += GET_ALL_CHUNK){
151
- const chunk = domains.slice(start, start + GET_ALL_CHUNK);
152
- const snapshots = await firestore.getAll(...chunk.map((domain)=>collection.doc(domain)));
153
- chunk.forEach((domain, index)=>{
154
- const snapshot = snapshots[index];
155
- known.set(domain, readStoredOutreachDomainIntel(domain, (snapshot == null ? void 0 : snapshot.exists) ? snapshot.data() : undefined));
156
- });
157
- }
158
- } catch (error) {
159
- console.error('[outreach] domain intel lookup failed; reading as unchecked', error);
160
- for (const email of domainOf.keys())answers.set(email, null);
161
- return answers;
162
- }
163
- await Promise.all(domains.map(async (domain)=>{
164
- var _known_get, _ref, _ref1, _ref2, _ref3;
165
- const stored = (_known_get = known.get(domain)) != null ? _known_get : null;
166
- if (stored && stored.resolvedAtMs + OUTREACH_DOMAIN_INTEL_TTL_MS > input.nowMs) return;
167
- let resolved;
168
- try {
169
- resolved = await resolveOutreachDomainMx(input.resolveMx, domain);
170
- } catch (error) {
171
- // A resolver that could not answer: the stale answer stands, and a
172
- // domain never looked up is unchecked.
173
- console.error(`[outreach] the MX of ${domain} could not be looked up`, error);
174
- return;
175
- }
176
- const fresh = {
177
- domain,
178
- mx: resolved.mx,
179
- gateway: resolved.gateway,
180
- resolvedAtMs: input.nowMs,
181
- sent: (_ref = stored == null ? void 0 : stored.sent) != null ? _ref : 0,
182
- delivered: (_ref1 = stored == null ? void 0 : stored.delivered) != null ? _ref1 : 0,
183
- blocked: (_ref2 = stored == null ? void 0 : stored.blocked) != null ? _ref2 : 0,
184
- lastBlockedAtMs: (_ref3 = stored == null ? void 0 : stored.lastBlockedAtMs) != null ? _ref3 : null,
185
- updatedAtMs: input.nowMs
186
- };
187
- known.set(domain, fresh);
188
- try {
189
- await collection.doc(domain).set({
190
- domain,
191
- mx: fresh.mx,
192
- gateway: fresh.gateway,
193
- resolvedAtMs: fresh.resolvedAtMs,
194
- updatedAtMs: fresh.updatedAtMs
195
- }, {
196
- merge: true
197
- });
198
- } catch (error) {
199
- // The cache is a convenience; the answer is already in hand.
200
- console.error(`[outreach] the MX of ${domain} could not be cached`, error);
201
- }
202
- }));
203
- for (const [email, domain] of domainOf)answers.set(email, (_known_get = known.get(domain)) != null ? _known_get : null);
204
- return answers;
205
- }
206
- /** The ledger of each gateway named, keyed by gateway; a gateway with no ledger yet answers `null`. */ export async function readOutreachGatewayStats(firestore, orgId, gateways) {
141
+ /** The per-gateway ledgers written before the sending domain keyed them — see the module note. */ async function readLegacyGatewayStats(firestore, orgId, gateways) {
207
142
  const answers = new Map();
208
143
  const wanted = [
209
144
  ...new Set(gateways)
210
- ];
145
+ ].filter((gateway)=>gateway !== 'none');
211
146
  if (!wanted.length) return answers;
212
- const collection = statsCollection(firestore, orgId);
147
+ const collection = outreachOrgCollection(firestore, orgId, 'gatewayStats');
213
148
  const snapshots = await firestore.getAll(...wanted.map((gateway)=>collection.doc(gateway)));
214
149
  wanted.forEach((gateway, index)=>{
215
150
  const snapshot = snapshots[index];
@@ -217,29 +152,39 @@ const statsCollection = (firestore, orgId)=>outreachOrgCollection(firestore, org
217
152
  });
218
153
  return answers;
219
154
  }
220
- /** What the gates and the chip read for one domain: its gateway and that gateway's recent record here. */ export function outreachGatewayStandingOf(intel, stats, nowMs) {
221
- const week = outreachGatewayWindow(stats == null ? void 0 : stats.days, nowMs, OUTREACH_GATEWAY_CHIP_DAYS);
222
- const month = outreachGatewayWindow(stats == null ? void 0 : stats.days, nowMs, OUTREACH_GATEWAY_WINDOW_DAYS);
155
+ /** Two standings for one gateway, summed. */ function sumStandings(a, b) {
223
156
  return {
224
- gateway: intel.gateway,
225
- blocked7: week.blocked,
226
- delivered7: week.delivered,
227
- blocked30: month.blocked,
228
- delivered30: month.delivered
157
+ gateway: a.gateway,
158
+ blocked7: a.blocked7 + b.blocked7,
159
+ delivered7: a.delivered7 + b.delivered7,
160
+ blocked30: a.blocked30 + b.blocked30,
161
+ delivered30: a.delivered30 + b.delivered30
229
162
  };
230
163
  }
231
164
  /**
232
- * The gateway standing of every address, keyed by address: the domain
233
- * intel read (and refreshed) above, joined to the ledger of each gateway it
234
- * names. `null` for an address whose domain could not be checked.
165
+ * The gateway standing of every address, keyed by address: the MX answer
166
+ * read (and refreshed) above, joined to the mailbox's sending-domain ledger
167
+ * with each gateway it names, plus what the legacy per-gateway ledger still
168
+ * holds. `null` for an address whose domain could not be checked.
235
169
  */ export async function readOutreachGatewayStandings(firestore, orgId, emails, input) {
236
170
  const intel = await readOutreachDomainIntel(firestore, orgId, emails, input);
237
171
  const gateways = [
238
172
  ...intel.values()
239
173
  ].filter((entry)=>entry !== null).map((entry)=>entry.gateway);
240
- let stats;
174
+ const sendingDomain = normalizeLedgerSendingDomain(input.sendingDomain);
175
+ let ledgers;
176
+ let legacy;
241
177
  try {
242
- stats = await readOutreachGatewayStats(firestore, orgId, gateways);
178
+ ;
179
+ [ledgers, legacy] = await Promise.all([
180
+ sendingDomain ? readMailGatewayLedgers({
181
+ orgId
182
+ }, sendingDomain, gateways, {
183
+ firestore,
184
+ nowMs: input.nowMs
185
+ }) : Promise.resolve(new Map()),
186
+ readLegacyGatewayStats(firestore, orgId, gateways)
187
+ ]);
243
188
  } catch (error) {
244
189
  console.error('[outreach] gateway ledger lookup failed; reading as unchecked', error);
245
190
  return new Map(emails.map((email)=>[
@@ -249,72 +194,39 @@ const statsCollection = (firestore, orgId)=>outreachOrgCollection(firestore, org
249
194
  }
250
195
  const answers = new Map();
251
196
  for (const email of emails){
252
- var _intel_get, _stats_get;
197
+ var _intel_get, _ledgers_get, _legacy_get;
253
198
  const entry = (_intel_get = intel.get(email)) != null ? _intel_get : null;
254
- answers.set(email, entry ? outreachGatewayStandingOf(entry, (_stats_get = stats.get(entry.gateway)) != null ? _stats_get : null, input.nowMs) : null);
199
+ if (!entry) {
200
+ answers.set(email, null);
201
+ continue;
202
+ }
203
+ const current = mailGatewayStanding(entry.gateway, (_ledgers_get = ledgers.get(entry.gateway)) != null ? _ledgers_get : null, input.nowMs);
204
+ const previous = mailGatewayStanding(entry.gateway, (_legacy_get = legacy.get(entry.gateway)) != null ? _legacy_get : null, input.nowMs);
205
+ answers.set(email, sumStandings(current, previous));
255
206
  }
256
207
  return answers;
257
208
  }
258
209
  /**
259
- * Counts an outcome on the domain's intel and on the gateway's ledger — see
260
- * the module note. Never throws: the send or the bounce it records has
261
- * already happened, and a lost count understates a gateway while a thrown
262
- * one loses the step that called.
210
+ * Counts an outcome on the mailbox's sending-domain ledger with a gateway.
211
+ * Never throws: the send or the bounce it records has already happened,
212
+ * and a lost count understates a gateway while a thrown one loses the step
213
+ * that called. A mailbox whose sending domain cannot be read counts nothing.
263
214
  */ export async function recordOutreachGatewayOutcome(firestore, orgId, input) {
264
- var _input_count;
265
- const domain = normalizeOutreachDomain(input.domain);
266
- const by = count((_input_count = input.count) != null ? _input_count : 1);
267
- if (!domain || !by) return;
268
- const intelRef = intelCollection(firestore, orgId).doc(domain);
269
- const statsRef = statsCollection(firestore, orgId).doc(input.gateway);
270
- const day = outreachGatewayDay(input.atMs);
271
- try {
272
- await firestore.runTransaction(async (transaction)=>{
273
- var _ref, _ref1, _ref2, _ref3, _ref4, _ref5, _ref6, _days_day, _ref7, _ref8, _ref9, _ref10;
274
- const [intelSnapshot, statsSnapshot] = await Promise.all([
275
- transaction.get(intelRef),
276
- transaction.get(statsRef)
277
- ]);
278
- const intel = readStoredOutreachDomainIntel(domain, intelSnapshot.exists ? intelSnapshot.data() : undefined);
279
- const stats = readStoredOutreachGatewayStats(input.gateway, statsSnapshot.exists ? statsSnapshot.data() : undefined);
280
- const blockedAt = input.outcome === 'blocked' ? input.atMs : null;
281
- const nextIntel = {
282
- domain,
283
- // A domain never looked up — enrolled before the intel existed —
284
- // is written with what the bounce said, and looked up on its next read.
285
- mx: (_ref = intel == null ? void 0 : intel.mx) != null ? _ref : [],
286
- gateway: (_ref1 = intel == null ? void 0 : intel.gateway) != null ? _ref1 : input.gateway,
287
- resolvedAtMs: (_ref2 = intel == null ? void 0 : intel.resolvedAtMs) != null ? _ref2 : 0,
288
- sent: ((_ref3 = intel == null ? void 0 : intel.sent) != null ? _ref3 : 0) + (input.outcome === 'sent' ? by : 0),
289
- delivered: ((_ref4 = intel == null ? void 0 : intel.delivered) != null ? _ref4 : 0) + (input.outcome === 'delivered' ? by : 0),
290
- blocked: ((_ref5 = intel == null ? void 0 : intel.blocked) != null ? _ref5 : 0) + (input.outcome === 'blocked' ? by : 0),
291
- lastBlockedAtMs: Math.max((_ref6 = intel == null ? void 0 : intel.lastBlockedAtMs) != null ? _ref6 : 0, blockedAt != null ? blockedAt : 0) || null,
292
- updatedAtMs: input.atMs
293
- };
294
- const days = pruneOutreachGatewayDays(stats == null ? void 0 : stats.days, input.atMs, OUTREACH_GATEWAY_WINDOW_DAYS);
295
- const today = (_days_day = days[day]) != null ? _days_day : {
296
- sent: 0,
297
- delivered: 0,
298
- blocked: 0
299
- };
300
- days[day] = _extends({}, today, {
301
- [input.outcome]: today[input.outcome] + by
302
- });
303
- const nextStats = {
304
- gateway: input.gateway,
305
- sent: ((_ref7 = stats == null ? void 0 : stats.sent) != null ? _ref7 : 0) + (input.outcome === 'sent' ? by : 0),
306
- delivered: ((_ref8 = stats == null ? void 0 : stats.delivered) != null ? _ref8 : 0) + (input.outcome === 'delivered' ? by : 0),
307
- blocked: ((_ref9 = stats == null ? void 0 : stats.blocked) != null ? _ref9 : 0) + (input.outcome === 'blocked' ? by : 0),
308
- lastBlockedAtMs: Math.max((_ref10 = stats == null ? void 0 : stats.lastBlockedAtMs) != null ? _ref10 : 0, blockedAt != null ? blockedAt : 0) || null,
309
- days,
310
- updatedAtMs: input.atMs
311
- };
312
- transaction.set(intelRef, nextIntel);
313
- transaction.set(statsRef, nextStats);
314
- });
315
- } catch (error) {
316
- console.error(`[outreach] the gateway ledger could not count a ${input.outcome} at ${domain}`, error);
317
- }
215
+ var _input_detail;
216
+ const sendingDomain = normalizeLedgerSendingDomain(input.sendingDomain);
217
+ if (!sendingDomain) return;
218
+ await recordMailGatewayOutcome({
219
+ orgId
220
+ }, {
221
+ sendingDomain,
222
+ gateway: input.gateway,
223
+ outcome: input.outcome,
224
+ count: input.count,
225
+ atMs: input.atMs,
226
+ detail: (_input_detail = input.detail) != null ? _input_detail : null
227
+ }, {
228
+ firestore
229
+ });
318
230
  }
319
231
 
320
232
  //# sourceMappingURL=domain-intel-store.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/storage/domain-intel-store.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 * WHAT THE ORGANIZATION KNOWS ABOUT THE DOMAINS IT WRITES TO (AGL-3326).\n *\n * Two documents, both server-written:\n *\n * - `outreachDomainIntel/{domain}` — the domain's MX records and the mail\n * gateway they name, looked up with the resolver the caller hands in and\n * trusted for {@link OUTREACH_DOMAIN_INTEL_TTL_MS}; beside them, what this\n * organization's mail met at the domain;\n * - `outreachGatewayStats/{gateway}` — the same three counts per gateway,\n * in total and by UTC day, which is the ledger the hold and the\n * Check-people chip read.\n *\n * A read that could not be made answers `null` for the addresses it\n * covered, which the gates refuse as \"we couldn't check\" — except that a\n * stale answer outranks no answer: a domain whose MX was known last week\n * and cannot be looked up today is read as it was, because a resolver\n * timing out says nothing about the domain. A domain that resolves to no\n * MX at all is `none`, and the gates block it.\n *\n * The ledger is written in a transaction over both documents so a run that\n * records a block and a run that credits a delivery cannot lose each\n * other's count, and the days map is pruned on every write so it never\n * grows past the window it serves.\n */\n\nimport type { OutreachGatewayStanding, OutreachMailGateway } from '../engine/mail-gateway'\nimport {\n classifyOutreachMailGateway,\n OUTREACH_DOMAIN_INTEL_TTL_MS,\n OUTREACH_GATEWAY_CHIP_DAYS,\n OUTREACH_GATEWAY_WINDOW_DAYS,\n OUTREACH_MAIL_GATEWAYS,\n outreachGatewayDay,\n outreachGatewayWindow,\n pruneOutreachGatewayDays,\n} from '../engine/mail-gateway'\nimport { normalizeOutreachDomain, outreachEmailDomain } from '../engine/do-not-contact-domain'\nimport type { OutreachDomainIntel, OutreachGatewayStats } from '../model/outreach.types'\nimport { outreachOrgCollection } from './outreach-records'\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** One MX record as Node's resolver answers it. */\nexport interface OutreachMxRecord {\n exchange: string\n priority: number\n}\n\n/**\n * `dns.promises.resolveMx`, or a stand-in: the platform hands in Node's,\n * the specs hand in a table. Rejects the way Node does — `code`\n * `ENOTFOUND` or `ENODATA` for a domain with no MX — and any other\n * rejection is a resolver that could not answer.\n */\nexport type OutreachResolveMx = (domain: string) => Promise<ReadonlyArray<OutreachMxRecord>>\n\n/** What a domain-intel read needs beside the store. */\nexport interface OutreachDomainIntelReadInput {\n resolveMx: OutreachResolveMx\n nowMs: number\n}\n\n/** `getAll` takes this many references at most per call. */\nconst GET_ALL_CHUNK = 300\n\n/** The resolver's codes that mean \"this domain has no MX\", as opposed to \"I could not ask\". */\nconst NO_MX_CODES = new Set(['ENOTFOUND', 'ENODATA', 'NXDOMAIN', 'NOTFOUND'])\n\nconst count = (value: unknown): number => {\n const number = Number(value)\n return Number.isFinite(number) ? Math.max(0, Math.floor(number)) : 0\n}\nconst ms = (value: unknown): number | null =>\n typeof value === 'number' && Number.isFinite(value) ? value : null\nconst gatewayOf = (value: unknown): OutreachMailGateway | null =>\n (OUTREACH_MAIL_GATEWAYS as readonly unknown[]).includes(value) ? (value as OutreachMailGateway) : null\n\n/** A stored domain-intel document in its model shape, or `null` for no document. */\nexport function readStoredOutreachDomainIntel(\n id: string,\n data: Record<string, unknown> | undefined,\n): OutreachDomainIntel | null {\n if (!data) return null\n const gateway = gatewayOf(data['gateway'])\n if (!gateway) return null\n return {\n domain: typeof data['domain'] === 'string' && data['domain'] ? data['domain'] : id,\n mx: Array.isArray(data['mx']) ? data['mx'].filter((entry): entry is string => typeof entry === 'string') : [],\n gateway,\n resolvedAtMs: ms(data['resolvedAtMs']) ?? 0,\n sent: count(data['sent']),\n delivered: count(data['delivered']),\n blocked: count(data['blocked']),\n lastBlockedAtMs: ms(data['lastBlockedAtMs']),\n updatedAtMs: ms(data['updatedAtMs']) ?? 0,\n }\n}\n\n/** A stored gateway ledger in its model shape, or `null` for no document. */\nexport function readStoredOutreachGatewayStats(\n id: string,\n data: Record<string, unknown> | undefined,\n): OutreachGatewayStats | null {\n if (!data) return null\n const gateway = gatewayOf(data['gateway']) ?? gatewayOf(id)\n if (!gateway) return null\n const days: OutreachGatewayStats['days'] = {}\n const raw = data['days']\n if (raw && typeof raw === 'object') {\n for (const [day, counts] of Object.entries(raw as Record<string, unknown>)) {\n const entry = (counts && typeof counts === 'object' ? counts : {}) as Record<string, unknown>\n days[day] = { sent: count(entry['sent']), delivered: count(entry['delivered']), blocked: count(entry['blocked']) }\n }\n }\n return {\n gateway,\n sent: count(data['sent']),\n delivered: count(data['delivered']),\n blocked: count(data['blocked']),\n lastBlockedAtMs: ms(data['lastBlockedAtMs']),\n days,\n updatedAtMs: ms(data['updatedAtMs']) ?? 0,\n }\n}\n\n/**\n * A domain's MX, resolved and classified — see the module note. A domain\n * the resolver says has no MX answers `none`; a resolver that could not\n * answer rejects, and the caller decides what a missing answer means.\n */\nexport async function resolveOutreachDomainMx(\n resolveMx: OutreachResolveMx,\n domain: string,\n): Promise<{ mx: string[]; gateway: OutreachMailGateway }> {\n let records: ReadonlyArray<OutreachMxRecord>\n try {\n records = await resolveMx(domain)\n } catch (error) {\n const code = String((error as { code?: unknown })?.code ?? '').toUpperCase()\n if (NO_MX_CODES.has(code)) return { mx: [], gateway: 'none' }\n throw error\n }\n const mx = [...records]\n .filter((record) => record && typeof record.exchange === 'string')\n .sort((a, b) => (Number(a.priority) || 0) - (Number(b.priority) || 0))\n .map((record) => record.exchange.trim().toLowerCase().replace(/\\.$/, ''))\n // RFC 7505's null MX — a lone \".\" — says the domain takes no mail.\n .filter((exchange) => exchange && exchange !== '.')\n return { mx, gateway: classifyOutreachMailGateway(mx) }\n}\n\nconst intelCollection = (firestore: Firestore, orgId: string) =>\n outreachOrgCollection(firestore, orgId, 'domainIntel')\nconst statsCollection = (firestore: Firestore, orgId: string) =>\n outreachOrgCollection(firestore, orgId, 'gatewayStats')\n\n/**\n * The domain intel for every address, keyed by address — fresh from the\n * store, or looked up and stored when missing or older than the TTL. An\n * address with no domain, and one whose domain could not be looked up and\n * was never known, answers `null`.\n */\nexport async function readOutreachDomainIntel(\n firestore: Firestore,\n orgId: string,\n emails: readonly string[],\n input: OutreachDomainIntelReadInput,\n): Promise<Map<string, OutreachDomainIntel | null>> {\n const answers = new Map<string, OutreachDomainIntel | null>()\n const domainOf = new Map<string, string>()\n for (const email of emails) {\n const domain = outreachEmailDomain(email)\n if (domain) domainOf.set(email, domain)\n else answers.set(email, null)\n }\n const domains = [...new Set(domainOf.values())]\n if (!domains.length) return answers\n const collection = intelCollection(firestore, orgId)\n const known = new Map<string, OutreachDomainIntel | null>()\n try {\n for (let start = 0; start < domains.length; start += GET_ALL_CHUNK) {\n const chunk = domains.slice(start, start + GET_ALL_CHUNK)\n const snapshots = await firestore.getAll(...chunk.map((domain) => collection.doc(domain)))\n chunk.forEach((domain, index) => {\n const snapshot = snapshots[index]\n known.set(domain, readStoredOutreachDomainIntel(domain, snapshot?.exists ? snapshot.data() : undefined))\n })\n }\n } catch (error) {\n console.error('[outreach] domain intel lookup failed; reading as unchecked', error)\n for (const email of domainOf.keys()) answers.set(email, null)\n return answers\n }\n await Promise.all(\n domains.map(async (domain) => {\n const stored = known.get(domain) ?? null\n if (stored && stored.resolvedAtMs + OUTREACH_DOMAIN_INTEL_TTL_MS > input.nowMs) return\n let resolved: { mx: string[]; gateway: OutreachMailGateway }\n try {\n resolved = await resolveOutreachDomainMx(input.resolveMx, domain)\n } catch (error) {\n // A resolver that could not answer: the stale answer stands, and a\n // domain never looked up is unchecked.\n console.error(`[outreach] the MX of ${domain} could not be looked up`, error)\n return\n }\n const fresh: OutreachDomainIntel = {\n domain,\n mx: resolved.mx,\n gateway: resolved.gateway,\n resolvedAtMs: input.nowMs,\n sent: stored?.sent ?? 0,\n delivered: stored?.delivered ?? 0,\n blocked: stored?.blocked ?? 0,\n lastBlockedAtMs: stored?.lastBlockedAtMs ?? null,\n updatedAtMs: input.nowMs,\n }\n known.set(domain, fresh)\n try {\n await collection.doc(domain).set(\n { domain, mx: fresh.mx, gateway: fresh.gateway, resolvedAtMs: fresh.resolvedAtMs, updatedAtMs: fresh.updatedAtMs },\n { merge: true },\n )\n } catch (error) {\n // The cache is a convenience; the answer is already in hand.\n console.error(`[outreach] the MX of ${domain} could not be cached`, error)\n }\n }),\n )\n for (const [email, domain] of domainOf) answers.set(email, known.get(domain) ?? null)\n return answers\n}\n\n/** The ledger of each gateway named, keyed by gateway; a gateway with no ledger yet answers `null`. */\nexport async function readOutreachGatewayStats(\n firestore: Firestore,\n orgId: string,\n gateways: readonly OutreachMailGateway[],\n): Promise<Map<OutreachMailGateway, OutreachGatewayStats | null>> {\n const answers = new Map<OutreachMailGateway, OutreachGatewayStats | null>()\n const wanted = [...new Set(gateways)]\n if (!wanted.length) return answers\n const collection = statsCollection(firestore, orgId)\n const snapshots = await firestore.getAll(...wanted.map((gateway) => collection.doc(gateway)))\n wanted.forEach((gateway, index) => {\n const snapshot = snapshots[index]\n answers.set(gateway, readStoredOutreachGatewayStats(gateway, snapshot?.exists ? snapshot.data() : undefined))\n })\n return answers\n}\n\n/** What the gates and the chip read for one domain: its gateway and that gateway's recent record here. */\nexport function outreachGatewayStandingOf(\n intel: OutreachDomainIntel,\n stats: OutreachGatewayStats | null,\n nowMs: number,\n): OutreachGatewayStanding {\n const week = outreachGatewayWindow(stats?.days, nowMs, OUTREACH_GATEWAY_CHIP_DAYS)\n const month = outreachGatewayWindow(stats?.days, nowMs, OUTREACH_GATEWAY_WINDOW_DAYS)\n return {\n gateway: intel.gateway,\n blocked7: week.blocked,\n delivered7: week.delivered,\n blocked30: month.blocked,\n delivered30: month.delivered,\n }\n}\n\n/**\n * The gateway standing of every address, keyed by address: the domain\n * intel read (and refreshed) above, joined to the ledger of each gateway it\n * names. `null` for an address whose domain could not be checked.\n */\nexport async function readOutreachGatewayStandings(\n firestore: Firestore,\n orgId: string,\n emails: readonly string[],\n input: OutreachDomainIntelReadInput,\n): Promise<Map<string, OutreachGatewayStanding | null>> {\n const intel = await readOutreachDomainIntel(firestore, orgId, emails, input)\n const gateways = [...intel.values()]\n .filter((entry): entry is OutreachDomainIntel => entry !== null)\n .map((entry) => entry.gateway)\n let stats: Map<OutreachMailGateway, OutreachGatewayStats | null>\n try {\n stats = await readOutreachGatewayStats(firestore, orgId, gateways)\n } catch (error) {\n console.error('[outreach] gateway ledger lookup failed; reading as unchecked', error)\n return new Map(emails.map((email) => [email, null]))\n }\n const answers = new Map<string, OutreachGatewayStanding | null>()\n for (const email of emails) {\n const entry = intel.get(email) ?? null\n answers.set(email, entry ? outreachGatewayStandingOf(entry, stats.get(entry.gateway) ?? null, input.nowMs) : null)\n }\n return answers\n}\n\n/** What a send met at a gateway, as the ledger counts it. */\nexport type OutreachGatewayOutcome = 'sent' | 'delivered' | 'blocked'\n\n/**\n * Counts an outcome on the domain's intel and on the gateway's ledger — see\n * the module note. Never throws: the send or the bounce it records has\n * already happened, and a lost count understates a gateway while a thrown\n * one loses the step that called.\n */\nexport async function recordOutreachGatewayOutcome(\n firestore: Firestore,\n orgId: string,\n input: { domain: string; gateway: OutreachMailGateway; outcome: OutreachGatewayOutcome; count?: number; atMs: number },\n): Promise<void> {\n const domain = normalizeOutreachDomain(input.domain)\n const by = count(input.count ?? 1)\n if (!domain || !by) return\n const intelRef = intelCollection(firestore, orgId).doc(domain)\n const statsRef = statsCollection(firestore, orgId).doc(input.gateway)\n const day = outreachGatewayDay(input.atMs)\n try {\n await firestore.runTransaction(async (transaction) => {\n const [intelSnapshot, statsSnapshot] = await Promise.all([transaction.get(intelRef), transaction.get(statsRef)])\n const intel = readStoredOutreachDomainIntel(domain, intelSnapshot.exists ? intelSnapshot.data() : undefined)\n const stats = readStoredOutreachGatewayStats(input.gateway, statsSnapshot.exists ? statsSnapshot.data() : undefined)\n const blockedAt = input.outcome === 'blocked' ? input.atMs : null\n const nextIntel: OutreachDomainIntel = {\n domain,\n // A domain never looked up — enrolled before the intel existed —\n // is written with what the bounce said, and looked up on its next read.\n mx: intel?.mx ?? [],\n gateway: intel?.gateway ?? input.gateway,\n resolvedAtMs: intel?.resolvedAtMs ?? 0,\n sent: (intel?.sent ?? 0) + (input.outcome === 'sent' ? by : 0),\n delivered: (intel?.delivered ?? 0) + (input.outcome === 'delivered' ? by : 0),\n blocked: (intel?.blocked ?? 0) + (input.outcome === 'blocked' ? by : 0),\n lastBlockedAtMs: Math.max(intel?.lastBlockedAtMs ?? 0, blockedAt ?? 0) || null,\n updatedAtMs: input.atMs,\n }\n const days = pruneOutreachGatewayDays(stats?.days, input.atMs, OUTREACH_GATEWAY_WINDOW_DAYS)\n const today = days[day] ?? { sent: 0, delivered: 0, blocked: 0 }\n days[day] = { ...today, [input.outcome]: today[input.outcome] + by }\n const nextStats: OutreachGatewayStats = {\n gateway: input.gateway,\n sent: (stats?.sent ?? 0) + (input.outcome === 'sent' ? by : 0),\n delivered: (stats?.delivered ?? 0) + (input.outcome === 'delivered' ? by : 0),\n blocked: (stats?.blocked ?? 0) + (input.outcome === 'blocked' ? by : 0),\n lastBlockedAtMs: Math.max(stats?.lastBlockedAtMs ?? 0, blockedAt ?? 0) || null,\n days,\n updatedAtMs: input.atMs,\n }\n transaction.set(intelRef, nextIntel)\n transaction.set(statsRef, nextStats)\n })\n } catch (error) {\n console.error(`[outreach] the gateway ledger could not count a ${input.outcome} at ${domain}`, error)\n }\n}\n"],"names":["classifyOutreachMailGateway","OUTREACH_DOMAIN_INTEL_TTL_MS","OUTREACH_GATEWAY_CHIP_DAYS","OUTREACH_GATEWAY_WINDOW_DAYS","OUTREACH_MAIL_GATEWAYS","outreachGatewayDay","outreachGatewayWindow","pruneOutreachGatewayDays","normalizeOutreachDomain","outreachEmailDomain","outreachOrgCollection","GET_ALL_CHUNK","NO_MX_CODES","Set","count","value","number","Number","isFinite","Math","max","floor","ms","gatewayOf","includes","readStoredOutreachDomainIntel","id","data","gateway","domain","mx","Array","isArray","filter","entry","resolvedAtMs","sent","delivered","blocked","lastBlockedAtMs","updatedAtMs","readStoredOutreachGatewayStats","days","raw","day","counts","Object","entries","resolveOutreachDomainMx","resolveMx","records","error","code","String","toUpperCase","has","record","exchange","sort","a","b","priority","map","trim","toLowerCase","replace","intelCollection","firestore","orgId","statsCollection","readOutreachDomainIntel","emails","input","known","answers","Map","domainOf","email","set","domains","values","length","collection","start","chunk","slice","snapshots","getAll","doc","forEach","index","snapshot","exists","undefined","console","keys","Promise","all","stored","get","nowMs","resolved","fresh","merge","readOutreachGatewayStats","gateways","wanted","outreachGatewayStandingOf","intel","stats","week","month","blocked7","delivered7","blocked30","delivered30","readOutreachGatewayStandings","recordOutreachGatewayOutcome","by","intelRef","statsRef","atMs","runTransaction","transaction","intelSnapshot","statsSnapshot","blockedAt","outcome","nextIntel","today","nextStats"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/storage/domain-intel-store.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 * WHAT SEQUENCES KNOWS ABOUT THE DOMAINS IT WRITES TO (AGL-3326).\n *\n * Both halves live on the platform (AGL-3328), in `@aglyn/tenant-data-admin`'s\n * `email-deliverability`, which the Resend path reads and writes too:\n *\n * - a recipient domain's MX — the platform-wide `mailDomains/{domain}`\n * cache, looked up with the resolver the caller hands in and trusted for\n * a week (a day for a domain that takes no mail). The first workspace to\n * write to a domain warms it for every other;\n * - the ledger — `orgs/{orgId}/mailGatewayLedger/{sendingDomain}~{gateway}`,\n * keyed by the MAILBOX's sending domain, because a gateway refuses a\n * sender: the Barracuda that refused `aglyn.io` refuses `aglyn.io`, not\n * the organization's other mailbox on another domain. It stays under the\n * organization because a mailbox's domain is not one organization's — two\n * workspaces can each connect a `gmail.com` mailbox.\n *\n * A read that could not be made answers `null` for the addresses it\n * covered, which the gates refuse as \"we couldn't check\" — except that a\n * stale answer that the domain takes mail outranks no answer. A domain that\n * resolves to no MX is `none`, and the gates block it.\n *\n * ## The two collections this module wrote first\n *\n * `outreachDomainIntel` and `outreachGatewayStats` are READ THROUGH, not\n * migrated. The first held MX answers, which a lookup re-derives, so it is\n * no longer read. The second held a per-gateway ledger with no sending\n * domain; its days are summed into every sending domain's standing for the\n * thirty days they can still hold a send — the conservative reading, since\n * an organization with one mailbox domain loses nothing and one with two\n * holds a little more. Nothing writes either any more; both are retired,\n * with their rules, once the window has passed.\n */\n\nimport {\n type MailDnsResolver,\n type MailDomainIntel,\n type MailGatewayOutcome,\n mailGatewayStanding,\n normalizeLedgerSendingDomain,\n resolveMailDomain,\n} from '@aglyn/shared-util-email'\nimport {\n readMailDomains,\n readMailGatewayLedgers,\n recordMailGatewayOutcome,\n} from '@aglyn/tenant-data-admin/server/email-deliverability'\nimport type { OutreachGatewayStanding, OutreachMailGateway } from '../engine/mail-gateway'\nimport { OUTREACH_MAIL_GATEWAYS } from '../engine/mail-gateway'\nimport { outreachEmailDomain } from '../engine/do-not-contact-domain'\nimport type { OutreachGatewayStats, OutreachMailbox } from '../model/outreach.types'\nimport { outreachOrgCollection } from './outreach-records'\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** One MX record as Node's resolver answers it. */\nexport interface OutreachMxRecord {\n exchange: string\n priority: number\n}\n\n/**\n * `dns.promises.resolveMx`, or a stand-in: the platform hands in its own\n * resolver, the specs hand in a table. Rejects the way Node does — `code`\n * `ENOTFOUND` or `ENODATA` for a domain with no MX — and any other\n * rejection is a resolver that could not answer.\n */\nexport type OutreachResolveMx = (domain: string) => Promise<ReadonlyArray<OutreachMxRecord>>\n\n/** What a domain-intel read needs beside the store. */\nexport interface OutreachDomainIntelReadInput {\n resolveMx: OutreachResolveMx\n /**\n * Whether a domain with no MX has an address record — the implicit MX\n * RFC 5321 still delivers to. Omitted, a domain with no MX reads as none.\n */\n resolveAddress?: (domain: string) => Promise<boolean>\n nowMs: number\n /**\n * The domain the mailbox sends from (AGL-3328): the ledger is keyed by\n * it. Omitted or `null`, no ledger is read and nothing is held.\n */\n sendingDomain?: string | null\n}\n\n/** The resolver the platform store reads with, from what a caller hands in. */\nfunction resolverOf(input: Pick<OutreachDomainIntelReadInput, 'resolveMx' | 'resolveAddress'>): MailDnsResolver {\n return {\n resolveMx: input.resolveMx,\n ...(input.resolveAddress ? { resolveAddress: input.resolveAddress } : {}),\n }\n}\n\n/** The domain a mailbox's mail leaves from: its send-as address's, else the account's. */\nexport function outreachMailboxSendingDomain(\n mailbox: Pick<OutreachMailbox, 'sendAs' | 'email'> | null | undefined,\n): string | null {\n return normalizeLedgerSendingDomain(mailbox?.sendAs || mailbox?.email)\n}\n\n/**\n * A domain's MX, resolved and classified. A domain the resolver says has no\n * MX answers `none`; a resolver that could not answer rejects, and the\n * caller decides what a missing answer means.\n */\nexport async function resolveOutreachDomainMx(\n resolveMx: OutreachResolveMx,\n domain: string,\n): Promise<{ mx: string[]; gateway: OutreachMailGateway }> {\n const intel = await resolveMailDomain({ resolveMx }, domain, 0)\n return { mx: intel.mx, gateway: intel.gateway }\n}\n\n/**\n * The MX answer for every address, keyed by address — read through the\n * platform cache, and looked up and stored when missing or stale. An\n * address with no domain, and one whose domain could not be looked up and\n * was never known, answers `null`.\n */\nexport async function readOutreachDomainIntel(\n firestore: Firestore,\n _orgId: string,\n emails: readonly string[],\n input: OutreachDomainIntelReadInput,\n): Promise<Map<string, MailDomainIntel | null>> {\n const answers = new Map<string, MailDomainIntel | null>()\n const domainOf = new Map<string, string>()\n for (const email of emails) {\n const domain = outreachEmailDomain(email)\n if (domain) domainOf.set(email, domain)\n else answers.set(email, null)\n }\n if (!domainOf.size) return answers\n let intel: Map<string, MailDomainIntel | null>\n try {\n intel = await readMailDomains([...new Set(domainOf.values())], {\n firestore,\n resolver: resolverOf(input),\n nowMs: input.nowMs,\n })\n } catch (error) {\n console.error('[outreach] domain intel lookup failed; reading as unchecked', error)\n intel = new Map()\n }\n for (const [email, domain] of domainOf) answers.set(email, intel.get(domain) ?? null)\n return answers\n}\n\nconst count = (value: unknown): number => {\n const number = Number(value)\n return Number.isFinite(number) ? Math.max(0, Math.floor(number)) : 0\n}\nconst ms = (value: unknown): number | null =>\n typeof value === 'number' && Number.isFinite(value) ? value : null\nconst gatewayOf = (value: unknown): OutreachMailGateway | null =>\n (OUTREACH_MAIL_GATEWAYS as readonly unknown[]).includes(value) ? (value as OutreachMailGateway) : null\n\n/** A stored per-gateway ledger from before the sending domain keyed it, or `null` for no document. */\nexport function readStoredOutreachGatewayStats(\n id: string,\n data: Record<string, unknown> | undefined,\n): OutreachGatewayStats | null {\n if (!data) return null\n const gateway = gatewayOf(data['gateway']) ?? gatewayOf(id)\n if (!gateway) return null\n const days: OutreachGatewayStats['days'] = {}\n const raw = data['days']\n if (raw && typeof raw === 'object') {\n for (const [day, counts] of Object.entries(raw as Record<string, unknown>)) {\n const entry = (counts && typeof counts === 'object' ? counts : {}) as Record<string, unknown>\n days[day] = { sent: count(entry['sent']), delivered: count(entry['delivered']), blocked: count(entry['blocked']) }\n }\n }\n return {\n gateway,\n sent: count(data['sent']),\n delivered: count(data['delivered']),\n blocked: count(data['blocked']),\n lastBlockedAtMs: ms(data['lastBlockedAtMs']),\n days,\n updatedAtMs: ms(data['updatedAtMs']) ?? 0,\n }\n}\n\n/** The per-gateway ledgers written before the sending domain keyed them — see the module note. */\nasync function readLegacyGatewayStats(\n firestore: Firestore,\n orgId: string,\n gateways: readonly OutreachMailGateway[],\n): Promise<Map<OutreachMailGateway, OutreachGatewayStats | null>> {\n const answers = new Map<OutreachMailGateway, OutreachGatewayStats | null>()\n const wanted = [...new Set(gateways)].filter((gateway) => gateway !== 'none')\n if (!wanted.length) return answers\n const collection = outreachOrgCollection(firestore, orgId, 'gatewayStats')\n const snapshots = await firestore.getAll(...wanted.map((gateway) => collection.doc(gateway)))\n wanted.forEach((gateway, index) => {\n const snapshot = snapshots[index]\n answers.set(gateway, readStoredOutreachGatewayStats(gateway, snapshot?.exists ? snapshot.data() : undefined))\n })\n return answers\n}\n\n/** Two standings for one gateway, summed. */\nfunction sumStandings(a: OutreachGatewayStanding, b: OutreachGatewayStanding): OutreachGatewayStanding {\n return {\n gateway: a.gateway,\n blocked7: a.blocked7 + b.blocked7,\n delivered7: a.delivered7 + b.delivered7,\n blocked30: a.blocked30 + b.blocked30,\n delivered30: a.delivered30 + b.delivered30,\n }\n}\n\n/**\n * The gateway standing of every address, keyed by address: the MX answer\n * read (and refreshed) above, joined to the mailbox's sending-domain ledger\n * with each gateway it names, plus what the legacy per-gateway ledger still\n * holds. `null` for an address whose domain could not be checked.\n */\nexport async function readOutreachGatewayStandings(\n firestore: Firestore,\n orgId: string,\n emails: readonly string[],\n input: OutreachDomainIntelReadInput,\n): Promise<Map<string, OutreachGatewayStanding | null>> {\n const intel = await readOutreachDomainIntel(firestore, orgId, emails, input)\n const gateways = [...intel.values()]\n .filter((entry): entry is MailDomainIntel => entry !== null)\n .map((entry) => entry.gateway)\n const sendingDomain = normalizeLedgerSendingDomain(input.sendingDomain)\n let ledgers: Awaited<ReturnType<typeof readMailGatewayLedgers>>\n let legacy: Map<OutreachMailGateway, OutreachGatewayStats | null>\n try {\n ;[ledgers, legacy] = await Promise.all([\n sendingDomain\n ? readMailGatewayLedgers({ orgId }, sendingDomain, gateways, { firestore, nowMs: input.nowMs })\n : Promise.resolve(new Map()),\n readLegacyGatewayStats(firestore, orgId, gateways),\n ])\n } catch (error) {\n console.error('[outreach] gateway ledger lookup failed; reading as unchecked', error)\n return new Map(emails.map((email) => [email, null]))\n }\n const answers = new Map<string, OutreachGatewayStanding | null>()\n for (const email of emails) {\n const entry = intel.get(email) ?? null\n if (!entry) {\n answers.set(email, null)\n continue\n }\n const current = mailGatewayStanding(entry.gateway, ledgers.get(entry.gateway) ?? null, input.nowMs)\n const previous = mailGatewayStanding(entry.gateway, legacy.get(entry.gateway) ?? null, input.nowMs)\n answers.set(email, sumStandings(current, previous))\n }\n return answers\n}\n\n/** What a send met at a gateway, as the ledger counts it. */\nexport type OutreachGatewayOutcome = MailGatewayOutcome\n\n/**\n * Counts an outcome on the mailbox's sending-domain ledger with a gateway.\n * Never throws: the send or the bounce it records has already happened,\n * and a lost count understates a gateway while a thrown one loses the step\n * that called. A mailbox whose sending domain cannot be read counts nothing.\n */\nexport async function recordOutreachGatewayOutcome(\n firestore: Firestore,\n orgId: string,\n input: {\n sendingDomain: string | null\n gateway: OutreachMailGateway\n outcome: OutreachGatewayOutcome\n count?: number\n atMs: number\n /** A refusal's diagnostic, kept scrubbed of addresses. */\n detail?: string | null\n },\n): Promise<void> {\n const sendingDomain = normalizeLedgerSendingDomain(input.sendingDomain)\n if (!sendingDomain) return\n await recordMailGatewayOutcome(\n { orgId },\n {\n sendingDomain,\n gateway: input.gateway,\n outcome: input.outcome,\n count: input.count,\n atMs: input.atMs,\n detail: input.detail ?? null,\n },\n { firestore },\n )\n}\n"],"names":["mailGatewayStanding","normalizeLedgerSendingDomain","resolveMailDomain","readMailDomains","readMailGatewayLedgers","recordMailGatewayOutcome","OUTREACH_MAIL_GATEWAYS","outreachEmailDomain","outreachOrgCollection","resolverOf","input","resolveMx","resolveAddress","outreachMailboxSendingDomain","mailbox","sendAs","email","resolveOutreachDomainMx","domain","intel","mx","gateway","readOutreachDomainIntel","firestore","_orgId","emails","answers","Map","domainOf","set","size","Set","values","resolver","nowMs","error","console","get","count","value","number","Number","isFinite","Math","max","floor","ms","gatewayOf","includes","readStoredOutreachGatewayStats","id","data","days","raw","day","counts","Object","entries","entry","sent","delivered","blocked","lastBlockedAtMs","updatedAtMs","readLegacyGatewayStats","orgId","gateways","wanted","filter","length","collection","snapshots","getAll","map","doc","forEach","index","snapshot","exists","undefined","sumStandings","a","b","blocked7","delivered7","blocked30","delivered30","readOutreachGatewayStandings","sendingDomain","ledgers","legacy","Promise","all","resolve","current","previous","recordOutreachGatewayOutcome","outcome","atMs","detail"],"mappings":"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"}