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

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