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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/package.json +8 -7
  2. package/src/lib/server/dns-probe.d.ts +7 -0
  3. package/src/lib/server/dns-probe.js +52 -0
  4. package/src/lib/server/dns-probe.js.map +1 -1
  5. package/src/lib/server/email-deliverability.d.ts +150 -0
  6. package/src/lib/server/email-deliverability.js +538 -0
  7. package/src/lib/server/email-deliverability.js.map +1 -0
  8. package/src/lib/server/email-sender-reputation.d.ts +2 -11
  9. package/src/lib/server/email-sender-reputation.js +8 -25
  10. package/src/lib/server/email-sender-reputation.js.map +1 -1
  11. package/src/lib/server/email-suppression.d.ts +10 -1
  12. package/src/lib/server/email-suppression.js +4 -3
  13. package/src/lib/server/email-suppression.js.map +1 -1
  14. package/src/lib/server/erase-person.js +31 -3
  15. package/src/lib/server/erase-person.js.map +1 -1
  16. package/src/lib/server/host-visitor-records.d.ts +16 -2
  17. package/src/lib/server/host-visitor-records.js +31 -6
  18. package/src/lib/server/host-visitor-records.js.map +1 -1
  19. package/src/lib/server/hosted-page-review.d.ts +92 -0
  20. package/src/lib/server/hosted-page-review.js +342 -0
  21. package/src/lib/server/hosted-page-review.js.map +1 -0
  22. package/src/lib/server/org-age.d.ts +27 -0
  23. package/src/lib/server/org-age.js +44 -0
  24. package/src/lib/server/org-age.js.map +1 -0
  25. package/src/lib/server/outbound-send-review.d.ts +222 -0
  26. package/src/lib/server/outbound-send-review.js +479 -0
  27. package/src/lib/server/outbound-send-review.js.map +1 -0
  28. package/src/lib/server/payment-fraud-signal.d.ts +156 -0
  29. package/src/lib/server/payment-fraud-signal.js +368 -0
  30. package/src/lib/server/payment-fraud-signal.js.map +1 -0
  31. package/src/lib/server/payment-risk-record.d.ts +59 -0
  32. package/src/lib/server/payment-risk-record.js +76 -0
  33. package/src/lib/server/payment-risk-record.js.map +1 -0
  34. package/src/lib/server/sending-domains.d.ts +6 -0
  35. package/src/lib/server/sending-domains.js +98 -6
  36. package/src/lib/server/sending-domains.js.map +1 -1
  37. package/src/lib/server/stripe-card-authentication.d.ts +35 -0
  38. package/src/lib/server/stripe-card-authentication.js +63 -0
  39. package/src/lib/server/stripe-card-authentication.js.map +1 -0
  40. package/src/lib/server/tenant-write-lockdown.d.ts +32 -2
  41. package/src/lib/server/tenant-write-lockdown.js +33 -3
  42. package/src/lib/server/tenant-write-lockdown.js.map +1 -1
  43. package/src/tenancy.d.ts +3 -0
  44. package/src/tenancy.js +8 -0
  45. package/src/tenancy.js.map +1 -1
@@ -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'>;