@aglyn/shared-util-email 1.0.0-beta.218 → 1.0.0-beta.220
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 +58 -19
- package/package.json +3 -3
- package/src/index.d.ts +2 -0
- package/src/index.js +9 -1
- package/src/index.js.map +1 -1
- package/src/lib/email-delivery-events.d.ts +41 -48
- package/src/lib/email-delivery-events.js +18 -247
- package/src/lib/email-delivery-events.js.map +1 -1
- package/src/lib/email-health.d.ts +30 -43
- package/src/lib/email-health.js +45 -141
- package/src/lib/email-health.js.map +1 -1
- package/src/lib/hosted-page-screen.d.ts +27 -0
- package/src/lib/hosted-page-screen.js +228 -23
- package/src/lib/hosted-page-screen.js.map +1 -1
- package/src/lib/mail-provider-resend.d.ts +194 -0
- package/src/lib/mail-provider-resend.js +649 -0
- package/src/lib/mail-provider-resend.js.map +1 -0
- package/src/lib/mail-provider-webhook.d.ts +80 -0
- package/src/lib/mail-provider-webhook.js +156 -0
- package/src/lib/mail-provider-webhook.js.map +1 -0
- package/src/lib/mail-provider.d.ts +148 -0
- package/src/lib/mail-provider.js +84 -0
- package/src/lib/mail-provider.js.map +1 -0
- package/src/lib/mail-providers.d.ts +99 -0
- package/src/lib/mail-providers.js +182 -0
- package/src/lib/mail-providers.js.map +1 -0
- package/src/lib/outbound-phishing-screen.d.ts +80 -3
- package/src/lib/outbound-phishing-screen.js +457 -29
- package/src/lib/outbound-phishing-screen.js.map +1 -1
- package/src/lib/received-email.d.ts +7 -44
- package/src/lib/received-email.js +8 -104
- package/src/lib/received-email.js.map +1 -1
- package/src/lib/risk-notice-catalog.d.ts +32 -2
- package/src/lib/risk-notice-catalog.js +86 -55
- package/src/lib/risk-notice-catalog.js.map +1 -1
- package/src/lib/send-ceilings.d.ts +0 -57
- package/src/lib/send-ceilings.js +0 -47
- package/src/lib/send-ceilings.js.map +1 -1
- package/src/lib/send-email.d.ts +52 -58
- package/src/lib/send-email.js +128 -134
- package/src/lib/send-email.js.map +1 -1
- package/src/lib/send-rate.js +4 -3
- package/src/lib/send-rate.js.map +1 -1
- package/src/lib/sending-domain.d.ts +15 -0
- package/src/lib/sending-domain.js.map +1 -1
- package/src/lib/system-email-catalog.js +37 -11
- package/src/lib/system-email-catalog.js.map +1 -1
- package/src/lib/tenant-email-catalog.d.ts +7 -0
- package/src/lib/tenant-email-catalog.js +10 -591
- package/src/lib/tenant-email-catalog.js.map +1 -1
- package/src/lib/tenant-emails.generated.d.ts +11 -0
- package/src/lib/tenant-emails.generated.js +649 -0
- package/src/lib/tenant-emails.generated.js.map +1 -0
|
@@ -0,0 +1,99 @@
|
|
|
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
|
+
/**
|
|
18
|
+
* WHICH PROVIDER CARRIES THIS DEPLOYMENT'S MAIL.
|
|
19
|
+
*
|
|
20
|
+
* | provider | for |
|
|
21
|
+
* |-----------|--------------------------------------------------------------|
|
|
22
|
+
* | `resend` | Aglyn's own deployment, and anyone with a Resend account |
|
|
23
|
+
* | `webhook` | anything else — the operator's own relay, SMTP, SES, Postmark |
|
|
24
|
+
* | any other | a provider a plugin or a fork registered under that id |
|
|
25
|
+
*
|
|
26
|
+
* `AGLYN_MAIL_PROVIDER` picks one. Unset, the first built-in that has its
|
|
27
|
+
* settings wins, in the order above — so a deployment that already had
|
|
28
|
+
* `RESEND_API_KEY` keeps Resend however else it is configured, and a
|
|
29
|
+
* self-hosted one that set only `AGLYN_MAIL_WEBHOOK_URL` gets its own relay
|
|
30
|
+
* with no other setting. With neither, it is Resend, unconfigured, and every
|
|
31
|
+
* send is skipped with a log line naming the setting — the behavior an
|
|
32
|
+
* install without mail has always had.
|
|
33
|
+
*
|
|
34
|
+
* ## Why the built-ins are compiled in, and not registered by a plugin
|
|
35
|
+
*
|
|
36
|
+
* This is the transactional rail: password resets, invitations, receipts,
|
|
37
|
+
* billing notices and operator alerts, sent from every server process with
|
|
38
|
+
* or without any plugin loaded. A provider a plugin registered at boot is a
|
|
39
|
+
* provider that can be absent — a boot whose declarations threw part-way, a
|
|
40
|
+
* route bundle that evaluates the registry apart from boot's — and an absent
|
|
41
|
+
* provider is indistinguishable from an unconfigured one: every send skipped
|
|
42
|
+
* as if mail were simply off, nothing red. Selecting among providers that
|
|
43
|
+
* are part of this library has no such state. Registration is kept for what
|
|
44
|
+
* cannot be compiled in, and is only ever consulted when the operator named
|
|
45
|
+
* the provider.
|
|
46
|
+
*
|
|
47
|
+
* ## An explicit choice never becomes another vendor
|
|
48
|
+
*
|
|
49
|
+
* A value naming no built-in and no registered provider is REFUSED — every
|
|
50
|
+
* send skipped with an error naming the value — rather than falling back to
|
|
51
|
+
* detection. Falling back would hand an operator's mail, recipients and
|
|
52
|
+
* bodies included, to a vendor they did not choose, because a plugin failed
|
|
53
|
+
* to load or a name was misspelled.
|
|
54
|
+
*/
|
|
55
|
+
import { type MailInboundEvent, type MailProvider, type MailProviderReads } from './mail-provider';
|
|
56
|
+
import type { EmailDeliveryEvent } from './email-delivery-events';
|
|
57
|
+
/** The setting that names the provider. */
|
|
58
|
+
export declare const MAIL_PROVIDER_SETTING = "AGLYN_MAIL_PROVIDER";
|
|
59
|
+
type Env = Record<string, string | undefined>;
|
|
60
|
+
/** The ids no registration may take. */
|
|
61
|
+
export declare function builtInMailProviderIds(): string[];
|
|
62
|
+
/**
|
|
63
|
+
* Registers a provider under its `id`, for a deployment that names it in
|
|
64
|
+
* `AGLYN_MAIL_PROVIDER`. The same provider object again is a no-op; a
|
|
65
|
+
* different one under a taken id throws naming it, because two providers
|
|
66
|
+
* answering to one name would split an account's mail between them. A
|
|
67
|
+
* built-in id throws too: the platform's default carrier is not something a
|
|
68
|
+
* plugin can replace by registering.
|
|
69
|
+
*/
|
|
70
|
+
export declare function registerMailProvider(provider: MailProvider): void;
|
|
71
|
+
/** Forgets every registration — for specs. */
|
|
72
|
+
export declare function resetMailProvidersForTests(): void;
|
|
73
|
+
/**
|
|
74
|
+
* The provider this deployment's mail is handed to. Never `null`: an
|
|
75
|
+
* unconfigured provider reports what it is missing, and a provider the
|
|
76
|
+
* operator named that this process lacks is a stand-in that refuses (see
|
|
77
|
+
* {@link mailProviderProblem}). Read per call, so a spec — or a runtime
|
|
78
|
+
* whose environment arrives after module load — sees its current settings.
|
|
79
|
+
*/
|
|
80
|
+
export declare function mailProvider(env?: Env): MailProvider;
|
|
81
|
+
/**
|
|
82
|
+
* A sentence saying the operator's chosen provider is not available in this
|
|
83
|
+
* process, or `null` when it is (or none was chosen). Printed as an ERROR
|
|
84
|
+
* wherever a skipped send is reported, because unlike an absent key it is
|
|
85
|
+
* not a deployment that simply has no mail: somebody chose a provider and
|
|
86
|
+
* this process cannot find it.
|
|
87
|
+
*/
|
|
88
|
+
export declare function mailProviderProblem(provider?: MailProvider): string | null;
|
|
89
|
+
/** One delivery-webhook payload as events, one per recipient; `[]` for anything else. */
|
|
90
|
+
export declare function normalizeDeliveryEvents(payload: unknown, receivedAtMs: number, provider?: MailProvider): EmailDeliveryEvent[];
|
|
91
|
+
/** A received-mail notification, or `null` for any other payload. */
|
|
92
|
+
export declare function readInboundMailEvent(payload: unknown, provider?: MailProvider): MailInboundEvent | null;
|
|
93
|
+
/**
|
|
94
|
+
* The provider's reads of its own account. A provider without any answers
|
|
95
|
+
* with reads whose `unmet` says so, so a route handles "cannot read" the
|
|
96
|
+
* same way whichever the reason.
|
|
97
|
+
*/
|
|
98
|
+
export declare function mailProviderReads(provider?: MailProvider): MailProviderReads;
|
|
99
|
+
export {};
|
|
@@ -0,0 +1,182 @@
|
|
|
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
|
+
* WHICH PROVIDER CARRIES THIS DEPLOYMENT'S MAIL.
|
|
18
|
+
*
|
|
19
|
+
* | provider | for |
|
|
20
|
+
* |-----------|--------------------------------------------------------------|
|
|
21
|
+
* | `resend` | Aglyn's own deployment, and anyone with a Resend account |
|
|
22
|
+
* | `webhook` | anything else — the operator's own relay, SMTP, SES, Postmark |
|
|
23
|
+
* | any other | a provider a plugin or a fork registered under that id |
|
|
24
|
+
*
|
|
25
|
+
* `AGLYN_MAIL_PROVIDER` picks one. Unset, the first built-in that has its
|
|
26
|
+
* settings wins, in the order above — so a deployment that already had
|
|
27
|
+
* `RESEND_API_KEY` keeps Resend however else it is configured, and a
|
|
28
|
+
* self-hosted one that set only `AGLYN_MAIL_WEBHOOK_URL` gets its own relay
|
|
29
|
+
* with no other setting. With neither, it is Resend, unconfigured, and every
|
|
30
|
+
* send is skipped with a log line naming the setting — the behavior an
|
|
31
|
+
* install without mail has always had.
|
|
32
|
+
*
|
|
33
|
+
* ## Why the built-ins are compiled in, and not registered by a plugin
|
|
34
|
+
*
|
|
35
|
+
* This is the transactional rail: password resets, invitations, receipts,
|
|
36
|
+
* billing notices and operator alerts, sent from every server process with
|
|
37
|
+
* or without any plugin loaded. A provider a plugin registered at boot is a
|
|
38
|
+
* provider that can be absent — a boot whose declarations threw part-way, a
|
|
39
|
+
* route bundle that evaluates the registry apart from boot's — and an absent
|
|
40
|
+
* provider is indistinguishable from an unconfigured one: every send skipped
|
|
41
|
+
* as if mail were simply off, nothing red. Selecting among providers that
|
|
42
|
+
* are part of this library has no such state. Registration is kept for what
|
|
43
|
+
* cannot be compiled in, and is only ever consulted when the operator named
|
|
44
|
+
* the provider.
|
|
45
|
+
*
|
|
46
|
+
* ## An explicit choice never becomes another vendor
|
|
47
|
+
*
|
|
48
|
+
* A value naming no built-in and no registered provider is REFUSED — every
|
|
49
|
+
* send skipped with an error naming the value — rather than falling back to
|
|
50
|
+
* detection. Falling back would hand an operator's mail, recipients and
|
|
51
|
+
* bodies included, to a vendor they did not choose, because a plugin failed
|
|
52
|
+
* to load or a name was misspelled.
|
|
53
|
+
*/ import { resendMailProvider } from "./mail-provider-resend.js";
|
|
54
|
+
import { webhookMailProvider } from "./mail-provider-webhook.js";
|
|
55
|
+
/** The setting that names the provider. */ export const MAIL_PROVIDER_SETTING = 'AGLYN_MAIL_PROVIDER';
|
|
56
|
+
/** The built-in providers, in the order detection tries them. */ function builtInMailProviders(env) {
|
|
57
|
+
return [
|
|
58
|
+
resendMailProvider(env),
|
|
59
|
+
webhookMailProvider(env)
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
/** The ids no registration may take. */ export function builtInMailProviderIds() {
|
|
63
|
+
return builtInMailProviders({}).map((provider)=>provider.id);
|
|
64
|
+
}
|
|
65
|
+
/*==========================================
|
|
66
|
+
* PROVIDERS A PLUGIN OR A FORK SUPPLIES.
|
|
67
|
+
*
|
|
68
|
+
* The slots live on `globalThis` under a registered symbol: a Next server
|
|
69
|
+
* evaluates a module once for boot and again inside a route's bundle, and a
|
|
70
|
+
* module-level map would give the route an empty copy of what boot filled.
|
|
71
|
+
*=========================================*/ const REGISTRY_KEY = Symbol.for('aglyn.mail-providers');
|
|
72
|
+
function registered() {
|
|
73
|
+
const slots = globalThis;
|
|
74
|
+
if (!slots[REGISTRY_KEY]) slots[REGISTRY_KEY] = new Map();
|
|
75
|
+
return slots[REGISTRY_KEY];
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Registers a provider under its `id`, for a deployment that names it in
|
|
79
|
+
* `AGLYN_MAIL_PROVIDER`. The same provider object again is a no-op; a
|
|
80
|
+
* different one under a taken id throws naming it, because two providers
|
|
81
|
+
* answering to one name would split an account's mail between them. A
|
|
82
|
+
* built-in id throws too: the platform's default carrier is not something a
|
|
83
|
+
* plugin can replace by registering.
|
|
84
|
+
*/ export function registerMailProvider(provider) {
|
|
85
|
+
var _ref;
|
|
86
|
+
const id = String((_ref = provider == null ? void 0 : provider.id) != null ? _ref : '').trim().toLowerCase();
|
|
87
|
+
if (!id) throw new Error('a mail provider needs an id');
|
|
88
|
+
if (builtInMailProviderIds().includes(id)) {
|
|
89
|
+
throw new Error(`"${id}" is a built-in mail provider and cannot be registered`);
|
|
90
|
+
}
|
|
91
|
+
const slots = registered();
|
|
92
|
+
const existing = slots.get(id);
|
|
93
|
+
if (existing && existing !== provider) {
|
|
94
|
+
throw new Error(`a different mail provider is already registered as "${id}"`);
|
|
95
|
+
}
|
|
96
|
+
slots.set(id, provider);
|
|
97
|
+
}
|
|
98
|
+
/** Forgets every registration — for specs. */ export function resetMailProvidersForTests() {
|
|
99
|
+
registered().clear();
|
|
100
|
+
}
|
|
101
|
+
/*==========================================
|
|
102
|
+
* SELECTION.
|
|
103
|
+
*=========================================*/ /** Why each provider that cannot be used cannot, keyed by the stand-in {@link mailProvider} returned. */ const UNAVAILABLE = new WeakMap();
|
|
104
|
+
/**
|
|
105
|
+
* The stand-in for a provider the operator named and this process does not
|
|
106
|
+
* have. It sends nothing and says why in everything that reads it.
|
|
107
|
+
*/ function unavailableMailProvider(id) {
|
|
108
|
+
const problem = `${MAIL_PROVIDER_SETTING} is "${id}", which is not a built-in mail ` + `provider (${builtInMailProviderIds().join(', ')}) and was not registered ` + 'in this process, so no mail is sent.';
|
|
109
|
+
const provider = {
|
|
110
|
+
id,
|
|
111
|
+
missingSettings: ()=>[
|
|
112
|
+
MAIL_PROVIDER_SETTING
|
|
113
|
+
],
|
|
114
|
+
send: async ()=>{
|
|
115
|
+
throw new Error(problem);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
UNAVAILABLE.set(provider, problem);
|
|
119
|
+
return provider;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* The provider this deployment's mail is handed to. Never `null`: an
|
|
123
|
+
* unconfigured provider reports what it is missing, and a provider the
|
|
124
|
+
* operator named that this process lacks is a stand-in that refuses (see
|
|
125
|
+
* {@link mailProviderProblem}). Read per call, so a spec — or a runtime
|
|
126
|
+
* whose environment arrives after module load — sees its current settings.
|
|
127
|
+
*/ export function mailProvider(env = process.env) {
|
|
128
|
+
var _env_MAIL_PROVIDER_SETTING, _ref, _builtIns_find;
|
|
129
|
+
const requested = String((_env_MAIL_PROVIDER_SETTING = env[MAIL_PROVIDER_SETTING]) != null ? _env_MAIL_PROVIDER_SETTING : '').trim().toLowerCase();
|
|
130
|
+
const builtIns = builtInMailProviders(env);
|
|
131
|
+
if (!requested) {
|
|
132
|
+
var _builtIns_find1;
|
|
133
|
+
return (_builtIns_find1 = builtIns.find((provider)=>!provider.missingSettings().length)) != null ? _builtIns_find1 : builtIns[0];
|
|
134
|
+
}
|
|
135
|
+
return (_ref = (_builtIns_find = builtIns.find((provider)=>provider.id === requested)) != null ? _builtIns_find : registered().get(requested)) != null ? _ref : unavailableMailProvider(requested);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* A sentence saying the operator's chosen provider is not available in this
|
|
139
|
+
* process, or `null` when it is (or none was chosen). Printed as an ERROR
|
|
140
|
+
* wherever a skipped send is reported, because unlike an absent key it is
|
|
141
|
+
* not a deployment that simply has no mail: somebody chose a provider and
|
|
142
|
+
* this process cannot find it.
|
|
143
|
+
*/ export function mailProviderProblem(provider = mailProvider()) {
|
|
144
|
+
var _UNAVAILABLE_get;
|
|
145
|
+
return (_UNAVAILABLE_get = UNAVAILABLE.get(provider)) != null ? _UNAVAILABLE_get : null;
|
|
146
|
+
}
|
|
147
|
+
/*==========================================
|
|
148
|
+
* WHAT THE REST OF THE PLATFORM ASKS.
|
|
149
|
+
*
|
|
150
|
+
* Each question in the platform's own vocabulary, answered by whichever
|
|
151
|
+
* provider this deployment uses. A provider that cannot answer one gives the
|
|
152
|
+
* empty answer — no events, no inbound message, reads that say they cannot
|
|
153
|
+
* be made — never a guess.
|
|
154
|
+
*=========================================*/ /** One delivery-webhook payload as events, one per recipient; `[]` for anything else. */ export function normalizeDeliveryEvents(payload, receivedAtMs, provider = mailProvider()) {
|
|
155
|
+
var _ref;
|
|
156
|
+
return (_ref = provider.deliveryEvents == null ? void 0 : provider.deliveryEvents.call(provider, payload, receivedAtMs)) != null ? _ref : [];
|
|
157
|
+
}
|
|
158
|
+
/** A received-mail notification, or `null` for any other payload. */ export function readInboundMailEvent(payload, provider = mailProvider()) {
|
|
159
|
+
var _ref;
|
|
160
|
+
return (_ref = provider.inboundEvent == null ? void 0 : provider.inboundEvent.call(provider, payload)) != null ? _ref : null;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* The provider's reads of its own account. A provider without any answers
|
|
164
|
+
* with reads whose `unmet` says so, so a route handles "cannot read" the
|
|
165
|
+
* same way whichever the reason.
|
|
166
|
+
*/ export function mailProviderReads(provider = mailProvider()) {
|
|
167
|
+
var _mailProviderProblem;
|
|
168
|
+
if (provider.reads) return provider.reads;
|
|
169
|
+
const reason = (_mailProviderProblem = mailProviderProblem(provider)) != null ? _mailProviderProblem : `The "${provider.id}" mail provider cannot read mail it sent or received.`;
|
|
170
|
+
const refuse = async ()=>{
|
|
171
|
+
throw new Error(reason);
|
|
172
|
+
};
|
|
173
|
+
return {
|
|
174
|
+
unmet: ()=>reason,
|
|
175
|
+
history: refuse,
|
|
176
|
+
message: refuse,
|
|
177
|
+
received: refuse,
|
|
178
|
+
sendingDomains: refuse
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
//# sourceMappingURL=mail-providers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/shared/util/email/src/lib/mail-providers.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 * WHICH PROVIDER CARRIES THIS DEPLOYMENT'S MAIL.\n *\n * | provider | for |\n * |-----------|--------------------------------------------------------------|\n * | `resend` | Aglyn's own deployment, and anyone with a Resend account |\n * | `webhook` | anything else — the operator's own relay, SMTP, SES, Postmark |\n * | any other | a provider a plugin or a fork registered under that id |\n *\n * `AGLYN_MAIL_PROVIDER` picks one. Unset, the first built-in that has its\n * settings wins, in the order above — so a deployment that already had\n * `RESEND_API_KEY` keeps Resend however else it is configured, and a\n * self-hosted one that set only `AGLYN_MAIL_WEBHOOK_URL` gets its own relay\n * with no other setting. With neither, it is Resend, unconfigured, and every\n * send is skipped with a log line naming the setting — the behavior an\n * install without mail has always had.\n *\n * ## Why the built-ins are compiled in, and not registered by a plugin\n *\n * This is the transactional rail: password resets, invitations, receipts,\n * billing notices and operator alerts, sent from every server process with\n * or without any plugin loaded. A provider a plugin registered at boot is a\n * provider that can be absent — a boot whose declarations threw part-way, a\n * route bundle that evaluates the registry apart from boot's — and an absent\n * provider is indistinguishable from an unconfigured one: every send skipped\n * as if mail were simply off, nothing red. Selecting among providers that\n * are part of this library has no such state. Registration is kept for what\n * cannot be compiled in, and is only ever consulted when the operator named\n * the provider.\n *\n * ## An explicit choice never becomes another vendor\n *\n * A value naming no built-in and no registered provider is REFUSED — every\n * send skipped with an error naming the value — rather than falling back to\n * detection. Falling back would hand an operator's mail, recipients and\n * bodies included, to a vendor they did not choose, because a plugin failed\n * to load or a name was misspelled.\n */\n\nimport {\n type MailInboundEvent,\n type MailProvider,\n type MailProviderReads,\n} from './mail-provider'\nimport { resendMailProvider } from './mail-provider-resend'\nimport { webhookMailProvider } from './mail-provider-webhook'\nimport type { EmailDeliveryEvent } from './email-delivery-events'\n\n/** The setting that names the provider. */\nexport const MAIL_PROVIDER_SETTING = 'AGLYN_MAIL_PROVIDER'\n\ntype Env = Record<string, string | undefined>\n\n/** The built-in providers, in the order detection tries them. */\nfunction builtInMailProviders(env: Env): MailProvider[] {\n return [resendMailProvider(env), webhookMailProvider(env)]\n}\n\n/** The ids no registration may take. */\nexport function builtInMailProviderIds(): string[] {\n return builtInMailProviders({}).map((provider) => provider.id)\n}\n\n/*==========================================\n * PROVIDERS A PLUGIN OR A FORK SUPPLIES.\n *\n * The slots live on `globalThis` under a registered symbol: a Next server\n * evaluates a module once for boot and again inside a route's bundle, and a\n * module-level map would give the route an empty copy of what boot filled.\n *=========================================*/\n\nconst REGISTRY_KEY = Symbol.for('aglyn.mail-providers')\n\nfunction registered(): Map<string, MailProvider> {\n const slots = globalThis as { [REGISTRY_KEY]?: Map<string, MailProvider> }\n if (!slots[REGISTRY_KEY]) slots[REGISTRY_KEY] = new Map()\n return slots[REGISTRY_KEY] as Map<string, MailProvider>\n}\n\n/**\n * Registers a provider under its `id`, for a deployment that names it in\n * `AGLYN_MAIL_PROVIDER`. The same provider object again is a no-op; a\n * different one under a taken id throws naming it, because two providers\n * answering to one name would split an account's mail between them. A\n * built-in id throws too: the platform's default carrier is not something a\n * plugin can replace by registering.\n */\nexport function registerMailProvider(provider: MailProvider): void {\n const id = String(provider?.id ?? '').trim().toLowerCase()\n if (!id) throw new Error('a mail provider needs an id')\n if (builtInMailProviderIds().includes(id)) {\n throw new Error(`\"${id}\" is a built-in mail provider and cannot be registered`)\n }\n const slots = registered()\n const existing = slots.get(id)\n if (existing && existing !== provider) {\n throw new Error(`a different mail provider is already registered as \"${id}\"`)\n }\n slots.set(id, provider)\n}\n\n/** Forgets every registration — for specs. */\nexport function resetMailProvidersForTests(): void {\n registered().clear()\n}\n\n/*==========================================\n * SELECTION.\n *=========================================*/\n\n/** Why each provider that cannot be used cannot, keyed by the stand-in {@link mailProvider} returned. */\nconst UNAVAILABLE = new WeakMap<MailProvider, string>()\n\n/**\n * The stand-in for a provider the operator named and this process does not\n * have. It sends nothing and says why in everything that reads it.\n */\nfunction unavailableMailProvider(id: string): MailProvider {\n const problem =\n `${MAIL_PROVIDER_SETTING} is \"${id}\", which is not a built-in mail ` +\n `provider (${builtInMailProviderIds().join(', ')}) and was not registered ` +\n 'in this process, so no mail is sent.'\n const provider: MailProvider = {\n id,\n missingSettings: () => [MAIL_PROVIDER_SETTING],\n send: async () => {\n throw new Error(problem)\n },\n }\n UNAVAILABLE.set(provider, problem)\n return provider\n}\n\n/**\n * The provider this deployment's mail is handed to. Never `null`: an\n * unconfigured provider reports what it is missing, and a provider the\n * operator named that this process lacks is a stand-in that refuses (see\n * {@link mailProviderProblem}). Read per call, so a spec — or a runtime\n * whose environment arrives after module load — sees its current settings.\n */\nexport function mailProvider(env: Env = process.env as Env): MailProvider {\n const requested = String(env[MAIL_PROVIDER_SETTING] ?? '').trim().toLowerCase()\n const builtIns = builtInMailProviders(env)\n if (!requested) {\n return builtIns.find((provider) => !provider.missingSettings().length) ?? builtIns[0]\n }\n return (\n builtIns.find((provider) => provider.id === requested) ??\n registered().get(requested) ??\n unavailableMailProvider(requested)\n )\n}\n\n/**\n * A sentence saying the operator's chosen provider is not available in this\n * process, or `null` when it is (or none was chosen). Printed as an ERROR\n * wherever a skipped send is reported, because unlike an absent key it is\n * not a deployment that simply has no mail: somebody chose a provider and\n * this process cannot find it.\n */\nexport function mailProviderProblem(provider: MailProvider = mailProvider()): string | null {\n return UNAVAILABLE.get(provider) ?? null\n}\n\n/*==========================================\n * WHAT THE REST OF THE PLATFORM ASKS.\n *\n * Each question in the platform's own vocabulary, answered by whichever\n * provider this deployment uses. A provider that cannot answer one gives the\n * empty answer — no events, no inbound message, reads that say they cannot\n * be made — never a guess.\n *=========================================*/\n\n/** One delivery-webhook payload as events, one per recipient; `[]` for anything else. */\nexport function normalizeDeliveryEvents(\n payload: unknown,\n receivedAtMs: number,\n provider: MailProvider = mailProvider(),\n): EmailDeliveryEvent[] {\n return provider.deliveryEvents?.(payload, receivedAtMs) ?? []\n}\n\n/** A received-mail notification, or `null` for any other payload. */\nexport function readInboundMailEvent(\n payload: unknown,\n provider: MailProvider = mailProvider(),\n): MailInboundEvent | null {\n return provider.inboundEvent?.(payload) ?? null\n}\n\n/**\n * The provider's reads of its own account. A provider without any answers\n * with reads whose `unmet` says so, so a route handles \"cannot read\" the\n * same way whichever the reason.\n */\nexport function mailProviderReads(\n provider: MailProvider = mailProvider(),\n): MailProviderReads {\n if (provider.reads) return provider.reads\n const reason =\n mailProviderProblem(provider) ??\n `The \"${provider.id}\" mail provider cannot read mail it sent or received.`\n const refuse = async (): Promise<never> => {\n throw new Error(reason)\n }\n return {\n unmet: () => reason,\n history: refuse,\n message: refuse,\n received: refuse,\n sendingDomains: refuse,\n }\n}\n"],"names":["resendMailProvider","webhookMailProvider","MAIL_PROVIDER_SETTING","builtInMailProviders","env","builtInMailProviderIds","map","provider","id","REGISTRY_KEY","Symbol","for","registered","slots","globalThis","Map","registerMailProvider","String","trim","toLowerCase","Error","includes","existing","get","set","resetMailProvidersForTests","clear","UNAVAILABLE","WeakMap","unavailableMailProvider","problem","join","missingSettings","send","mailProvider","process","builtIns","requested","find","length","mailProviderProblem","normalizeDeliveryEvents","payload","receivedAtMs","deliveryEvents","readInboundMailEvent","inboundEvent","mailProviderReads","reads","reason","refuse","unmet","history","message","received","sendingDomains"],"mappings":"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"}
|
|
@@ -58,9 +58,22 @@ export interface PhishingScreenBrand {
|
|
|
58
58
|
}
|
|
59
59
|
export declare const PHISHING_SCREEN_BRANDS: readonly PhishingScreenBrand[];
|
|
60
60
|
/**
|
|
61
|
-
* Account
|
|
62
|
-
*
|
|
63
|
-
|
|
61
|
+
* Account and credential phrasing: the lure that asks the reader to DO
|
|
62
|
+
* something with an account — verify it, confirm it, sign in to it.
|
|
63
|
+
*/
|
|
64
|
+
export declare const ACCOUNT_LURE_PATTERNS: readonly RegExp[];
|
|
65
|
+
/**
|
|
66
|
+
* Document-share phrasing (AGL-3447): the lure that says a file is waiting
|
|
67
|
+
* behind a secure door — "Secure Document Access Portal", "Continue to
|
|
68
|
+
* Document", "shared a file with you", "encrypted message". The credential
|
|
69
|
+
* harvest is on the far side of the button, so these are the words the page
|
|
70
|
+
* or the mail itself carries.
|
|
71
|
+
*/
|
|
72
|
+
export declare const DOCUMENT_SHARE_LURE_PATTERNS: readonly RegExp[];
|
|
73
|
+
/**
|
|
74
|
+
* Every lure — the half of the three-part lure that asks the reader to act.
|
|
75
|
+
* Only ever counted beside a brand AND a foreign link; alone, several of
|
|
76
|
+
* these are ordinary transactional copy.
|
|
64
77
|
*/
|
|
65
78
|
export declare const PHISHING_LURE_PATTERNS: readonly RegExp[];
|
|
66
79
|
/** A signal the screen found. */
|
|
@@ -97,6 +110,54 @@ export type PhishingScreenSignal = {
|
|
|
97
110
|
code: 'brand-action-page';
|
|
98
111
|
brand: string;
|
|
99
112
|
action: string;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* The same, read over the page as a WHOLE (AGL-3447): a brand that is not
|
|
116
|
+
* the workspace's anywhere on a page that carries an account or
|
|
117
|
+
* document-share lure anywhere, or whose call to action leaves the site
|
|
118
|
+
* worded as one, and that links away (`hosted-page-screen.ts`). Soft.
|
|
119
|
+
*/
|
|
120
|
+
| {
|
|
121
|
+
code: 'brand-lure-page';
|
|
122
|
+
brand: string;
|
|
123
|
+
lure: string;
|
|
124
|
+
host: string;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* A published page's call to action — its only one, or one worded as an
|
|
128
|
+
* account or document action — sends visitors to a host that is not the
|
|
129
|
+
* site's, a listed brand's or a common link, on a page that carries an
|
|
130
|
+
* account or document-share lure (`hosted-page-screen.ts`, AGL-3447). No
|
|
131
|
+
* brand needed. Soft.
|
|
132
|
+
*/
|
|
133
|
+
| {
|
|
134
|
+
code: 'offsite-action-page';
|
|
135
|
+
action: string;
|
|
136
|
+
lure: string;
|
|
137
|
+
host: string;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* A site redirect whose path reads as an account or document-share lure
|
|
141
|
+
* sends visitors to such a host (`screenSiteRedirect`, AGL-3447). Soft.
|
|
142
|
+
*/
|
|
143
|
+
| {
|
|
144
|
+
code: 'offsite-redirect';
|
|
145
|
+
source: string;
|
|
146
|
+
lure: string;
|
|
147
|
+
host: string;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Account or password phrasing beside a link elsewhere that carries the
|
|
151
|
+
* reader's own address (AGL-3453). STRONG when the address is the whole
|
|
152
|
+
* fragment (`#victim@example.com`), which only a landing page's script
|
|
153
|
+
* reads; soft when it is a query parameter or part of a longer fragment.
|
|
154
|
+
* The address itself is never stored.
|
|
155
|
+
*/
|
|
156
|
+
| {
|
|
157
|
+
code: 'recipient-prefill-link';
|
|
158
|
+
host: string;
|
|
159
|
+
lure: string;
|
|
160
|
+
place: 'fragment' | 'parameter';
|
|
100
161
|
};
|
|
101
162
|
export interface PhishingScreenInput {
|
|
102
163
|
subject?: string | null;
|
|
@@ -163,6 +224,21 @@ declare function isAnyOfficialDomain(registrable: string): boolean;
|
|
|
163
224
|
export declare const MEDIA_EMBED_HOSTS: ReadonlySet<string>;
|
|
164
225
|
/** The brand a host impersonates, or null. */
|
|
165
226
|
export declare function lookalikeBrandForHost(host: string): PhishingScreenBrand | null;
|
|
227
|
+
/**
|
|
228
|
+
* What a reader SEES in a body (AGL-3453): its text with the markup taken
|
|
229
|
+
* out, the way a mail client or a browser draws it.
|
|
230
|
+
*
|
|
231
|
+
* A kit splits every letter of a brand and a lure with a tag —
|
|
232
|
+
* `G<SPAN class=…>o<SPAN …>o…gle`, `P<SPAN …>a<SPAN …>s…word E<SPAN …>x…pired`
|
|
233
|
+
* — so the raw HTML never contains the word a reader reads. Tags are
|
|
234
|
+
* therefore removed with NO separator, which rejoins the letters; `<style>`,
|
|
235
|
+
* `<script>` and `<head>` are dropped with their contents; comments and
|
|
236
|
+
* zero-width characters are dropped; entities are decoded; whitespace is
|
|
237
|
+
* collapsed. A second reading that puts a space where a line or block
|
|
238
|
+
* breaks is added when it differs, so two words in two paragraphs are still
|
|
239
|
+
* two words. Links are NOT read from this: an href lives inside a tag.
|
|
240
|
+
*/
|
|
241
|
+
export declare function visibleTextOf(html: string | null | undefined): string;
|
|
166
242
|
/** The hosts every link in the text points at, lowercased. */
|
|
167
243
|
export declare function linkHostsIn(text: string): string[];
|
|
168
244
|
/** Lowercased alphanumerics only, so "Wells-Fargo Advisors" contains `wellsfargo`. */
|
|
@@ -202,6 +278,7 @@ export declare const ACCOUNT_SUBDOMAIN_HOSTS: ReadonlySet<string>;
|
|
|
202
278
|
export declare function phishingSignalTier(signal: {
|
|
203
279
|
code: string;
|
|
204
280
|
host?: string;
|
|
281
|
+
place?: string;
|
|
205
282
|
}): PhishingSignalTier;
|
|
206
283
|
/** Is a workspace of this age (days, `null` = unreadable) screened by the soft rules? */
|
|
207
284
|
export declare function isYoungWorkspaceAge(ageDays: number | null | undefined): boolean;
|