@aglyn/tenant-data-admin 1.0.0-beta.203 → 1.0.0-beta.205

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 (54) hide show
  1. package/package.json +7 -7
  2. package/src/lib/server/capture-email-check.d.ts +109 -0
  3. package/src/lib/server/capture-email-check.js +362 -0
  4. package/src/lib/server/capture-email-check.js.map +1 -0
  5. package/src/lib/server/card-payment-velocity.d.ts +11 -16
  6. package/src/lib/server/card-payment-velocity.js +24 -28
  7. package/src/lib/server/card-payment-velocity.js.map +1 -1
  8. package/src/lib/server/email-deliverability.d.ts +19 -3
  9. package/src/lib/server/email-deliverability.js +34 -16
  10. package/src/lib/server/email-deliverability.js.map +1 -1
  11. package/src/lib/server/held-page-subject.d.ts +93 -0
  12. package/src/lib/server/held-page-subject.js +206 -0
  13. package/src/lib/server/held-page-subject.js.map +1 -0
  14. package/src/lib/server/host-email-tokens.d.ts +42 -1
  15. package/src/lib/server/host-email-tokens.js +100 -12
  16. package/src/lib/server/host-email-tokens.js.map +1 -1
  17. package/src/lib/server/host-visitor-records.js +21 -5
  18. package/src/lib/server/host-visitor-records.js.map +1 -1
  19. package/src/lib/server/hosted-page-review.d.ts +16 -5
  20. package/src/lib/server/hosted-page-review.js +242 -61
  21. package/src/lib/server/hosted-page-review.js.map +1 -1
  22. package/src/lib/server/list-members.js +16 -0
  23. package/src/lib/server/list-members.js.map +1 -1
  24. package/src/lib/server/notifications.d.ts +7 -0
  25. package/src/lib/server/notifications.js +27 -4
  26. package/src/lib/server/notifications.js.map +1 -1
  27. package/src/lib/server/outbound-send-review.d.ts +22 -0
  28. package/src/lib/server/outbound-send-review.js +74 -6
  29. package/src/lib/server/outbound-send-review.js.map +1 -1
  30. package/src/lib/server/payment-fraud-signal.d.ts +20 -12
  31. package/src/lib/server/payment-fraud-signal.js +41 -11
  32. package/src/lib/server/payment-fraud-signal.js.map +1 -1
  33. package/src/lib/server/payment-risk-record.d.ts +37 -13
  34. package/src/lib/server/payment-risk-record.js +23 -8
  35. package/src/lib/server/payment-risk-record.js.map +1 -1
  36. package/src/lib/server/render-system-email.d.ts +3 -0
  37. package/src/lib/server/render-system-email.js +39 -11
  38. package/src/lib/server/render-system-email.js.map +1 -1
  39. package/src/lib/server/risk-notice.d.ts +327 -0
  40. package/src/lib/server/risk-notice.js +1240 -0
  41. package/src/lib/server/risk-notice.js.map +1 -0
  42. package/src/lib/server/sender-readiness.d.ts +36 -0
  43. package/src/lib/server/sender-readiness.js +99 -0
  44. package/src/lib/server/sender-readiness.js.map +1 -0
  45. package/src/lib/server/staff-alert-email.d.ts +29 -15
  46. package/src/lib/server/staff-alert-email.js +131 -28
  47. package/src/lib/server/staff-alert-email.js.map +1 -1
  48. package/src/lib/server/test-firestore.js +1 -0
  49. package/src/lib/server/test-firestore.js.map +1 -1
  50. package/src/lib/server/upsert-contact.js +19 -3
  51. package/src/lib/server/upsert-contact.js.map +1 -1
  52. package/src/tenancy.d.ts +2 -0
  53. package/src/tenancy.js +3 -0
  54. package/src/tenancy.js.map +1 -1
@@ -0,0 +1,327 @@
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 { type ResolvedRiskAction, type RiskEventKind } from '@aglyn/shared-util-email/risk-notice-catalog';
18
+ import { type SendEmailOptions, type SendEmailResult } from '@aglyn/shared-util-email';
19
+ import { type HeldPageSubject, type HeldPageTarget, type PageHoldNoticeKind } from '@aglyn/shared-util-email/held-page';
20
+ import { type NotificationPayload, type NotifyUsersOptions } from './notifications';
21
+ import { type SystemEmailBrand } from './render-system-email';
22
+ /** Admin-SDK only; no client rule opens it. One document per notice. */
23
+ export declare const RISK_NOTICE_COLLECTION = "riskNotices";
24
+ /** Admin-SDK only: each workspace's burst window and folded notices. */
25
+ export declare const RISK_NOTICE_LEDGER_COLLECTION = "riskNoticeLedger";
26
+ /** The in-app notification type every owner notice is written as. */
27
+ export declare const RISK_NOTICE_NOTIFICATION_TYPE: "system.riskNotice";
28
+ /**
29
+ * The burst allowance: how many notices one workspace's owners, and staff
30
+ * about one workspace, receive one by one in {@link RISK_NOTICE_BURST.windowMs}
31
+ * before the rest are folded into a digest.
32
+ */
33
+ export declare const RISK_NOTICE_BURST: {
34
+ readonly owners: 5;
35
+ readonly staff: 10;
36
+ readonly windowMs: number;
37
+ /** How many folded notices a digest lists by name. */
38
+ readonly digestListMax: 25;
39
+ };
40
+ /** Longest review-request note stored. */
41
+ export declare const RISK_REVIEW_NOTE_MAX = 2000;
42
+ /** How many review requests one row keeps. */
43
+ export declare const RISK_REVIEW_REQUESTS_MAX = 20;
44
+ /** How soon the same workspace may ask again about the same row. */
45
+ export declare const RISK_REVIEW_REQUEST_COOLDOWN_MS: number;
46
+ /** What a source knows about the thing the notice is about. */
47
+ export interface RiskEventItem {
48
+ /** In the owner's words: `the campaign "Spring sale"`, `order 1042`. */
49
+ label: string;
50
+ /**
51
+ * Its console page, in the stored-notification shape (`/org/…` or
52
+ * `/{hostId}/…`) the console rewrites when the link is followed.
53
+ */
54
+ path?: string | null;
55
+ }
56
+ export interface RiskEventInput {
57
+ kind: RiskEventKind;
58
+ orgId: string | null;
59
+ hostId?: string | null;
60
+ /** An account lock's person. */
61
+ userUid?: string | null;
62
+ /** The abuse-queue row, when the event has one. */
63
+ reviewId?: string | null;
64
+ reference?: string | null;
65
+ item?: RiskEventItem | null;
66
+ occurredAtMs?: number;
67
+ /** `$56.00 USD`. */
68
+ amount?: string | null;
69
+ evidenceDueByMs?: number | null;
70
+ /** Staff only: the Stripe Dashboard page for the charge, review or dispute. */
71
+ stripeUrl?: string | null;
72
+ lock?: {
73
+ /** The customer-facing message the lock stores, verbatim. */
74
+ message?: string | null;
75
+ /** What the lock or pause covers, in plain words. */
76
+ affected?: string | null;
77
+ scope?: string | null;
78
+ targetId?: string | null;
79
+ } | null;
80
+ /**
81
+ * A held or flagged page, described (AGL-3374). Stored on the notice so
82
+ * the console can put the hold on the page, template, layout or component
83
+ * it is about, and name it there. Owner-safe: no signal, no evidence.
84
+ */
85
+ page?: HeldPageSubject | null;
86
+ /** Staff only: what the screen or Stripe reported. Never shown to owners. */
87
+ staffEvidence?: string | null;
88
+ /** Overrides the default dedupe key. */
89
+ dedupeKey?: string | null;
90
+ /**
91
+ * `false` to send no owner EMAIL for this event (a lock placed under a
92
+ * legal hold). The in-app notice and the record are still written.
93
+ */
94
+ emailOwners?: boolean;
95
+ /**
96
+ * Exactly who the owner half goes to, in place of the workspace's owners
97
+ * and admins: a batch that already resolved its people and groups several
98
+ * events into one notice per person (the lockdown's "Resend owner
99
+ * notice").
100
+ */
101
+ recipients?: ReadonlyArray<{
102
+ uid: string;
103
+ email: string | null;
104
+ }>;
105
+ }
106
+ export interface RiskNoticeDelivery {
107
+ /** How many people the owner half was for. */
108
+ recipients: number;
109
+ /** In-app notifications written (before per-person mutes). */
110
+ inApp: number;
111
+ emailed: number;
112
+ emailFailed: number;
113
+ /** Why no email went, when none did. */
114
+ emailSkipped: string | null;
115
+ /** Folded into the next digest instead of delivered now. */
116
+ digested: boolean;
117
+ }
118
+ export interface RiskNoticeResult {
119
+ noticeId: string | null;
120
+ /** The same notice was already delivered: nothing was sent now. */
121
+ duplicate: boolean;
122
+ owners: RiskNoticeDelivery;
123
+ staff: {
124
+ alerted: boolean;
125
+ digested: boolean;
126
+ };
127
+ /** Set when the notice could not be processed at all. */
128
+ error: string | null;
129
+ }
130
+ /** Everything the seam touches, injectable for specs. */
131
+ export interface RiskNoticeDeps {
132
+ firestore: FirebaseFirestore.Firestore;
133
+ sendEmail: (options: SendEmailOptions) => Promise<SendEmailResult>;
134
+ notifyUsers: (uids: Iterable<string>, payload: NotificationPayload, options?: NotifyUsersOptions) => Promise<void>;
135
+ notifyStaff: (payload: NotificationPayload) => Promise<void>;
136
+ listOwners: (orgId: string) => Promise<Array<{
137
+ uid: string;
138
+ email: string | null;
139
+ }>>;
140
+ lookupEmail: (uid: string) => Promise<string | null>;
141
+ nowMs: () => number;
142
+ }
143
+ /** The help page every notice links, on the docs site. */
144
+ export declare function riskNoticeHelpUrl(anchor?: string): string;
145
+ /** `Sep 28, 2026, 16:31 UTC` — one format for every notice. */
146
+ export declare function formatRiskNoticeTime(ms: number): string;
147
+ /** The id of the notice a dedupe key names: hex, like the abuse queue's. */
148
+ export declare function riskNoticeId(dedupeKey: string): string;
149
+ /** The default dedupe key: one notice per kind per row, else per event. */
150
+ export declare function riskNoticeDedupeKey(input: RiskEventInput, occurredAtMs: number): string;
151
+ /** The console path an owner's Holds & reviews page is at, in stored shape. */
152
+ export declare function riskHoldsPath(noticeId?: string | null): string;
153
+ /** A staff deep link to one abuse-queue row. */
154
+ export declare function staffAbuseRowPath(reviewId: string): string;
155
+ /**
156
+ * Stored-shape site paths (`/{hostId}/…`) as console routes, for a reader
157
+ * with no org context of its own — staff reading the abuse queue
158
+ * (AGL-3374). One read of the sites and one of their workspaces for the
159
+ * whole batch; a path that cannot be resolved is left out.
160
+ */
161
+ export declare function resolveHostConsolePaths(firestore: FirebaseFirestore.Firestore, entries: ReadonlyArray<{
162
+ hostId: string;
163
+ orgId?: string | null;
164
+ path: string;
165
+ }>): Promise<Map<string, string>>;
166
+ /** What one notice email sends. */
167
+ export interface RenderedRiskNoticeEmail {
168
+ key: string;
169
+ subject: string;
170
+ text: string;
171
+ html?: string;
172
+ /** The display name to send under; absent sends under the platform's. */
173
+ fromName?: string;
174
+ }
175
+ /**
176
+ * The brand a kind's email is sent in: the workspace's own for a routine
177
+ * notice about its customers' payments, the platform's for everything the
178
+ * platform does (holds, locks, lifts, cancellations, reviews).
179
+ */
180
+ export declare function riskNoticeEmailBrand(kind: RiskEventKind | null, orgId: string | null): Promise<SystemEmailBrand>;
181
+ /**
182
+ * Renders one notice email through the System emails seam (AGL-3367): the
183
+ * published design for the kind's own `risk-…` key, or its built-in copy,
184
+ * inside the header and footer every platform email wears. The plain-text
185
+ * composition below is the last resort for a render that produced nothing.
186
+ * ONE function, so every notice renders the same way.
187
+ */
188
+ export declare function renderRiskNoticeEmail(key: string, merge: Record<string, string>, brand?: SystemEmailBrand): Promise<RenderedRiskNoticeEmail>;
189
+ /**
190
+ * Tell the right people about one risk event. See the module header.
191
+ * Never throws.
192
+ */
193
+ export declare function notifyRiskEvent(input: RiskEventInput, overrides?: Partial<RiskNoticeDeps>): Promise<RiskNoticeResult>;
194
+ /**
195
+ * Send every digest whose hour has closed (the hourly cron). Returns how
196
+ * many went out. Never throws.
197
+ */
198
+ export declare function flushRiskNoticeDigests(overrides?: Partial<RiskNoticeDeps>): Promise<{
199
+ flushed: number;
200
+ failed: number;
201
+ }>;
202
+ /**
203
+ * The closing notice for a row staff decided: the kind the row's opening
204
+ * notice names in `closesWith`, sent to the same people about the same item.
205
+ * Silent (returns null) for a row with no risk notice or a kind that does
206
+ * not close. Never throws.
207
+ */
208
+ export declare function closeRiskNotice(input: {
209
+ reviewId: string;
210
+ decision: 'released' | 'rejected';
211
+ }, overrides?: Partial<RiskNoticeDeps>): Promise<RiskNoticeResult | null>;
212
+ /** A review request as it is stored on the abuse-queue row. */
213
+ export interface RiskReviewRequest {
214
+ atMs: number;
215
+ uid: string;
216
+ email: string | null;
217
+ note: string;
218
+ noticeId: string;
219
+ }
220
+ export type RiskReviewRequestOutcome = {
221
+ ok: true;
222
+ reference: string | null;
223
+ requestedAtMs: number;
224
+ } | {
225
+ ok: false;
226
+ status: 400 | 404 | 409 | 429;
227
+ error: string;
228
+ };
229
+ /**
230
+ * An owner or admin asks for a review of a held or flagged item.
231
+ *
232
+ * APPENDS to the item's existing abuse-queue row — never a new row, and
233
+ * never a decision: the row's `status` and its `heldSend` are not written
234
+ * here, so no request can release a hold or lift a lock. Staff see the
235
+ * request as a note on the row, and are notified with a link to it.
236
+ */
237
+ export declare function requestRiskReview(input: {
238
+ orgId: string;
239
+ noticeId: string;
240
+ uid: string;
241
+ email: string | null;
242
+ note: string;
243
+ }, overrides?: Partial<RiskNoticeDeps>): Promise<RiskReviewRequestOutcome>;
244
+ /** One owner notice, as the Holds & reviews page renders it. */
245
+ export interface OwnerRiskNoticeView {
246
+ noticeId: string;
247
+ kind: RiskEventKind;
248
+ severity: string;
249
+ title: string;
250
+ summary: string;
251
+ meaning: string;
252
+ steps: string[];
253
+ actions: ResolvedRiskAction[];
254
+ reference: string | null;
255
+ occurredAtMs: number;
256
+ helpUrl: string;
257
+ /** The review state, for a notice with a row. */
258
+ status: 'held' | 'in-review' | 'released' | 'rejected' | 'closed' | null;
259
+ reviewable: boolean;
260
+ reviewRequests: Array<{
261
+ atMs: number;
262
+ note: string;
263
+ mine: boolean;
264
+ }>;
265
+ /** A page notice: the page by name, where it lives, and what visitors see (AGL-3374). */
266
+ page: OwnerPageHoldFacts | null;
267
+ }
268
+ /** A page notice's facts, as every owner surface renders them (AGL-3374). */
269
+ export interface OwnerPageHoldFacts {
270
+ /** `the "Video detail" template (/videos/:slug)`. */
271
+ label: string;
272
+ /** Where it was seen, and the layout or component the content lives in. */
273
+ details: string[];
274
+ /** What visitors receive meanwhile. */
275
+ visitorSentence: string;
276
+ /** The page, and the layout or component, the hold is shown on. */
277
+ targets: HeldPageTarget[];
278
+ }
279
+ /**
280
+ * A page notice's facts from its stored subject, or — for a notice written
281
+ * before notices carried one — from its label and item path. Pure.
282
+ */
283
+ export declare function ownerPageHoldFacts(kind: string, notice: {
284
+ page?: unknown;
285
+ ownerValues?: unknown;
286
+ actionParams?: unknown;
287
+ }): OwnerPageHoldFacts | null;
288
+ /** One open page hold on a site, as the site's console surfaces show it (AGL-3374). */
289
+ export interface PageHoldView extends OwnerPageHoldFacts {
290
+ noticeId: string;
291
+ kind: PageHoldNoticeKind;
292
+ status: OwnerRiskNoticeView['status'];
293
+ /** The status chip. Never paired with a release control. */
294
+ chip: {
295
+ label: string;
296
+ color: 'warning' | 'error' | 'info';
297
+ };
298
+ reference: string | null;
299
+ occurredAtMs: number;
300
+ /** Whether a review can be requested on it (the viewer's role decides the rest). */
301
+ reviewable: boolean;
302
+ /** When anyone on the workspace last asked for a review, if they did. */
303
+ reviewRequestedAtMs: number | null;
304
+ }
305
+ /**
306
+ * Every page on a site that is held, flagged or not approved (AGL-3374): the
307
+ * `riskNotices` of the two page kinds for this host, newest first — one
308
+ * query on (`hostId`, `kind`, `createdAtMs`), which
309
+ * `cloud/firebase-firestore.indexes.json` holds — and each notice's row for
310
+ * its state. A released or cleared page is not listed.
311
+ */
312
+ export declare function listHostPageHolds(input: {
313
+ hostId: string;
314
+ limit?: number;
315
+ }, overrides?: Partial<RiskNoticeDeps>): Promise<PageHoldView[]>;
316
+ /**
317
+ * The workspace's notices, newest first, rendered for its owners: the
318
+ * catalog's owner half only, and the row's decision state — never its
319
+ * evidence, never its staff notes.
320
+ */
321
+ export declare function listOwnerRiskNotices(input: {
322
+ orgId: string;
323
+ viewerUid: string;
324
+ limit?: number;
325
+ }, overrides?: Partial<RiskNoticeDeps>): Promise<OwnerRiskNoticeView[]>;
326
+ /** A row's state in the owner's words. Pure. */
327
+ export declare function ownerRowStatus(row: Pick<FirebaseFirestore.DocumentSnapshot, 'get'>): OwnerRiskNoticeView['status'];