@aglyn/tenant-data-admin 1.0.0-beta.200 → 1.0.0-beta.201
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +8 -7
- package/src/lib/server/dns-probe.d.ts +7 -0
- package/src/lib/server/dns-probe.js +52 -0
- package/src/lib/server/dns-probe.js.map +1 -1
- package/src/lib/server/email-deliverability.d.ts +150 -0
- package/src/lib/server/email-deliverability.js +538 -0
- package/src/lib/server/email-deliverability.js.map +1 -0
- package/src/lib/server/email-sender-reputation.d.ts +2 -11
- package/src/lib/server/email-sender-reputation.js +8 -25
- package/src/lib/server/email-sender-reputation.js.map +1 -1
- package/src/lib/server/email-suppression.d.ts +10 -1
- package/src/lib/server/email-suppression.js +4 -3
- package/src/lib/server/email-suppression.js.map +1 -1
- package/src/lib/server/erase-person.js +31 -3
- package/src/lib/server/erase-person.js.map +1 -1
- package/src/lib/server/host-visitor-records.d.ts +16 -2
- package/src/lib/server/host-visitor-records.js +31 -6
- package/src/lib/server/host-visitor-records.js.map +1 -1
- package/src/lib/server/hosted-page-review.d.ts +92 -0
- package/src/lib/server/hosted-page-review.js +342 -0
- package/src/lib/server/hosted-page-review.js.map +1 -0
- package/src/lib/server/org-age.d.ts +27 -0
- package/src/lib/server/org-age.js +44 -0
- package/src/lib/server/org-age.js.map +1 -0
- package/src/lib/server/outbound-send-review.d.ts +222 -0
- package/src/lib/server/outbound-send-review.js +479 -0
- package/src/lib/server/outbound-send-review.js.map +1 -0
- package/src/lib/server/payment-fraud-signal.d.ts +156 -0
- package/src/lib/server/payment-fraud-signal.js +368 -0
- package/src/lib/server/payment-fraud-signal.js.map +1 -0
- package/src/lib/server/payment-risk-record.d.ts +59 -0
- package/src/lib/server/payment-risk-record.js +76 -0
- package/src/lib/server/payment-risk-record.js.map +1 -0
- package/src/lib/server/sending-domains.d.ts +6 -0
- package/src/lib/server/sending-domains.js +98 -6
- package/src/lib/server/sending-domains.js.map +1 -1
- package/src/lib/server/stripe-card-authentication.d.ts +35 -0
- package/src/lib/server/stripe-card-authentication.js +63 -0
- package/src/lib/server/stripe-card-authentication.js.map +1 -0
- package/src/lib/server/tenant-write-lockdown.d.ts +32 -2
- package/src/lib/server/tenant-write-lockdown.js +33 -3
- package/src/lib/server/tenant-write-lockdown.js.map +1 -1
- package/src/tenancy.d.ts +3 -0
- package/src/tenancy.js +8 -0
- package/src/tenancy.js.map +1 -1
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import { OUTBOUND_REVIEW_YOUNG_DAYS, type PhishingScreenInput, type PhishingScreenSignal } from '@aglyn/shared-util-email/outbound-phishing-screen';
|
|
18
|
+
import { type OutboundScreenGateRequest, type OutboundScreenGateVerdict, type SendingWorkspace } from '@aglyn/shared-util-email/outbound-screen-gate';
|
|
19
|
+
/** A workspace younger than this many days has the soft rules applied. */
|
|
20
|
+
export { OUTBOUND_REVIEW_YOUNG_DAYS };
|
|
21
|
+
/**
|
|
22
|
+
* Where a held campaign is parked: a send time no processor run reaches.
|
|
23
|
+
* A release replaces it with "now"; nothing else ever does, so a hold that
|
|
24
|
+
* nobody decides stays a hold rather than lapsing into a send.
|
|
25
|
+
*/
|
|
26
|
+
export declare const HELD_SEND_AT_MS = 253402300799000;
|
|
27
|
+
/**
|
|
28
|
+
* Which surface held it: a campaign, an automation step, any other tenant
|
|
29
|
+
* message at the send seam (`message`), or a site publish (`page`).
|
|
30
|
+
*/
|
|
31
|
+
export type HeldOutboundSendKind = 'campaign' | 'action' | 'workflow' | 'orgAutomation' | 'message' | 'page';
|
|
32
|
+
/** The decision state a held send carries on its review row. */
|
|
33
|
+
export type HeldOutboundSendState = 'held' | 'released' | 'rejected';
|
|
34
|
+
/** What the review row records about the message it holds. */
|
|
35
|
+
export interface HeldOutboundSend {
|
|
36
|
+
kind: HeldOutboundSendKind;
|
|
37
|
+
/** The document the message came from — a send, an action, a workflow. */
|
|
38
|
+
path: string;
|
|
39
|
+
hostId: string;
|
|
40
|
+
orgId: string | null;
|
|
41
|
+
contentHash: string;
|
|
42
|
+
subject: string;
|
|
43
|
+
fromName: string | null;
|
|
44
|
+
signals: PhishingScreenSignal[];
|
|
45
|
+
state: HeldOutboundSendState;
|
|
46
|
+
heldAtMs: number;
|
|
47
|
+
/** Workspace age in days when it was held, for the reviewer. */
|
|
48
|
+
ageDays: number | null;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Is this workspace young enough to screen?
|
|
52
|
+
*
|
|
53
|
+
* Age alone, deliberately. Payment history was the other candidate and the
|
|
54
|
+
* incident is the argument against it: the actor PAID, on a card that never
|
|
55
|
+
* met 3-D Secure, within days of signing up. A paid invoice proves a card
|
|
56
|
+
* worked, not that its holder is who they say.
|
|
57
|
+
*/
|
|
58
|
+
export declare function isYoungWorkspace(org: {
|
|
59
|
+
createdAt?: unknown;
|
|
60
|
+
} | null | undefined, nowMs?: number): boolean;
|
|
61
|
+
/**
|
|
62
|
+
* The workspace a host's mail belongs to, as the send seam's screen reads it
|
|
63
|
+
* — stamped on the sending identity by `hostSendingIdentity` from the host
|
|
64
|
+
* and org documents it has already read.
|
|
65
|
+
*/
|
|
66
|
+
export declare function sendingWorkspaceFor(input: {
|
|
67
|
+
hostId: string;
|
|
68
|
+
orgId: string | null;
|
|
69
|
+
org?: Record<string, unknown> | null;
|
|
70
|
+
host?: Record<string, unknown> | null;
|
|
71
|
+
nowMs?: number;
|
|
72
|
+
}): SendingWorkspace;
|
|
73
|
+
/**
|
|
74
|
+
* The workspace's own names and hosts, as the screen reads them: a brand in
|
|
75
|
+
* its own name is its own, and a link to its own site is not "elsewhere".
|
|
76
|
+
*/
|
|
77
|
+
export declare function workspaceScreenIdentity(input: {
|
|
78
|
+
org?: Record<string, unknown> | null;
|
|
79
|
+
host?: Record<string, unknown> | null;
|
|
80
|
+
}): Pick<PhishingScreenInput, 'ownNames' | 'ownDomains'>;
|
|
81
|
+
/**
|
|
82
|
+
* JSON with every object's keys sorted, so the same design serializes to the
|
|
83
|
+
* same string however it was read. Firestore hands back a map's keys in an
|
|
84
|
+
* order that differs by reader, and a release keyed on a hash of an unsorted
|
|
85
|
+
* serialization would not match the very content it released.
|
|
86
|
+
*/
|
|
87
|
+
export declare function canonicalJson(value: unknown): string;
|
|
88
|
+
/** A stable hash of what a message says, so a release covers these words only. */
|
|
89
|
+
export declare function outboundContentHash(parts: readonly unknown[]): string;
|
|
90
|
+
/**
|
|
91
|
+
* The review row's id: one row per (source, content). Hex, so the admin
|
|
92
|
+
* route's report-id pattern addresses it like any intake row.
|
|
93
|
+
*/
|
|
94
|
+
export declare function heldOutboundReviewId(path: string, contentHash: string): string;
|
|
95
|
+
/** The staff-facing reference, in the intake's `AR-` style. */
|
|
96
|
+
export declare function heldOutboundReference(reviewId: string): string;
|
|
97
|
+
export interface OutboundScreenRequest {
|
|
98
|
+
kind: HeldOutboundSendKind;
|
|
99
|
+
path: string;
|
|
100
|
+
hostId: string;
|
|
101
|
+
orgId: string | null;
|
|
102
|
+
org: Record<string, unknown> | null;
|
|
103
|
+
host: Record<string, unknown> | null;
|
|
104
|
+
subject: string;
|
|
105
|
+
fromName?: string | null;
|
|
106
|
+
/**
|
|
107
|
+
* The address it leaves from (AGL-3362). Read by the lookalike rule only,
|
|
108
|
+
* and kept out of the content hash so a release granted before this was
|
|
109
|
+
* read still names the same row.
|
|
110
|
+
*/
|
|
111
|
+
fromAddress?: string | null;
|
|
112
|
+
replyTo?: string | readonly string[] | null;
|
|
113
|
+
preheader?: string | null;
|
|
114
|
+
bodies: readonly (string | null | undefined)[];
|
|
115
|
+
nowMs?: number;
|
|
116
|
+
}
|
|
117
|
+
/** What a screen answers for one piece of content. */
|
|
118
|
+
export type OutboundScreenOutcome = {
|
|
119
|
+
outcome: 'send';
|
|
120
|
+
/** The review row staff released this content under, when there is one. */
|
|
121
|
+
releasedReviewId?: string;
|
|
122
|
+
} | {
|
|
123
|
+
outcome: 'held' | 'rejected';
|
|
124
|
+
reviewId: string;
|
|
125
|
+
reference: string;
|
|
126
|
+
contentHash: string;
|
|
127
|
+
};
|
|
128
|
+
/** What a surface files when its screen holds, for {@link fileOutboundHold}. */
|
|
129
|
+
export interface OutboundHoldFiling {
|
|
130
|
+
reviewId: string;
|
|
131
|
+
heldSend: HeldOutboundSend;
|
|
132
|
+
/** Extra fields a surface records beside `heldSend` (a page's version). */
|
|
133
|
+
extra?: Record<string, unknown>;
|
|
134
|
+
/** The first line of the row's details, naming what was held. */
|
|
135
|
+
headline: string;
|
|
136
|
+
/** The staff alert's title and body, the first time. */
|
|
137
|
+
alertTitle: string;
|
|
138
|
+
alertBody: string;
|
|
139
|
+
/** The URL the row reports: the flagged host, or the held page. */
|
|
140
|
+
url: string | null;
|
|
141
|
+
reportedHostname: string | null;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Write (or count) the review row for a hold, and tell staff the first time.
|
|
145
|
+
* Returns the row's decision state — a row staff already decided answers
|
|
146
|
+
* with that decision and is not written.
|
|
147
|
+
*
|
|
148
|
+
* One writer for every surface, so a campaign, a seam message and a page
|
|
149
|
+
* land in the queue in the one shape the admin route and page already read.
|
|
150
|
+
*/
|
|
151
|
+
export declare function fileOutboundHold(filing: OutboundHoldFiling): Promise<HeldOutboundSendState>;
|
|
152
|
+
/** The first signal that names a host, for the row's reported URL. */
|
|
153
|
+
export declare function flaggedHostOf(signals: readonly PhishingScreenSignal[]): string | null;
|
|
154
|
+
/**
|
|
155
|
+
* The screen's answer for one message, and what to do with it.
|
|
156
|
+
*
|
|
157
|
+
* - `send` — clean under the tiers, or held earlier and RELEASED by staff
|
|
158
|
+
* for exactly this content (then `releasedReviewId` names the row, and the
|
|
159
|
+
* caller hands it to `sendEmail` so the seam's screen honors it too).
|
|
160
|
+
* - `held` — hold it. The review row is written (or its count bumped) and
|
|
161
|
+
* staff are told the first time.
|
|
162
|
+
* - `rejected` — staff rejected exactly this content. Never sent.
|
|
163
|
+
*
|
|
164
|
+
* Every workspace is screened; `signalsThatHold` applies the tiers, so an
|
|
165
|
+
* established workspace is held only by a lookalike link.
|
|
166
|
+
*
|
|
167
|
+
* Fails OPEN on a store error, the posture of every other control on the
|
|
168
|
+
* send path: an outage on the review queue must not become an outage on
|
|
169
|
+
* every workspace's mail. The screen itself cannot fail.
|
|
170
|
+
*/
|
|
171
|
+
export declare function screenOutboundSend(request: OutboundScreenRequest): Promise<OutboundScreenOutcome>;
|
|
172
|
+
/** Test seam: forget what this process learned about seam rows. */
|
|
173
|
+
export declare function resetOutboundSeamMemoForTests(): void;
|
|
174
|
+
/**
|
|
175
|
+
* The seam row's id for a site and the signals that held. Keyed on the
|
|
176
|
+
* SIGNALS rather than the words: a message at the seam is already
|
|
177
|
+
* personalized per recipient, so its words differ for every person while
|
|
178
|
+
* what makes it phishing — the lookalike host, the brand and the lure — does
|
|
179
|
+
* not. A release therefore covers this site's use of exactly these signals,
|
|
180
|
+
* and a message carrying any other signal is screened afresh.
|
|
181
|
+
*/
|
|
182
|
+
export declare function seamReviewId(hostId: string, signals: readonly PhishingScreenSignal[]): string;
|
|
183
|
+
/**
|
|
184
|
+
* The send seam's gate: what `sendEmail` asks when a tenant message carries
|
|
185
|
+
* a signal that holds (see `outbound-screen-gate.ts`). Installed at module
|
|
186
|
+
* load by {@link installOutboundScreenGate}.
|
|
187
|
+
*
|
|
188
|
+
* A seam hold REFUSES that one message (`held-for-review`) — the sender's own
|
|
189
|
+
* failure handling reports it, the way it reports a suppression — and files
|
|
190
|
+
* one row per site and signal set. Once staff release it, the site's next
|
|
191
|
+
* message carrying those signals sends; reject it, and none does.
|
|
192
|
+
*/
|
|
193
|
+
export declare function screenSeamMessage(request: OutboundScreenGateRequest): Promise<OutboundScreenGateVerdict>;
|
|
194
|
+
/**
|
|
195
|
+
* Puts the review store on `sendEmail`'s phishing screen.
|
|
196
|
+
*
|
|
197
|
+
* **Called at module load**, from the bottom of this file, for the reason
|
|
198
|
+
* `installEmailSendGovernor` is — and this module is also imported by
|
|
199
|
+
* `sending-domains.ts`, so any sender that resolved its identity through
|
|
200
|
+
* `hostSendingIdentity` (the only thing that stamps a workspace on it, and
|
|
201
|
+
* so the only thing the seam screens) has installed it.
|
|
202
|
+
*/
|
|
203
|
+
export declare function installOutboundScreenGate(): void;
|
|
204
|
+
/**
|
|
205
|
+
* Apply a staff decision to a held send (the admin abuse route, in the same
|
|
206
|
+
* act as the row's status change).
|
|
207
|
+
*
|
|
208
|
+
* The row's `heldSend.state` is written first: it is what the screen reads,
|
|
209
|
+
* so an automation step released here sends on its next run whatever
|
|
210
|
+
* happens to the rest. A CAMPAIGN is also put back on the clock (released)
|
|
211
|
+
* or canceled (rejected) — but only while it is still the parked send this
|
|
212
|
+
* row held: a send that has since been edited, sent or canceled by the
|
|
213
|
+
* merchant is left alone, and the answer says so.
|
|
214
|
+
*/
|
|
215
|
+
export declare function decideHeldOutboundSend(input: {
|
|
216
|
+
reviewId: string;
|
|
217
|
+
decision: 'release' | 'reject';
|
|
218
|
+
actorUid: string;
|
|
219
|
+
actorEmail: string | null;
|
|
220
|
+
nowMs?: number;
|
|
221
|
+
firestore?: FirebaseFirestore.Firestore;
|
|
222
|
+
}): Promise<'released' | 'rejected' | 'not-held' | 'campaign-moved'>;
|