@aglyn/tenant-data-admin 1.0.0-beta.250 → 1.0.0-beta.253
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 +7 -7
- package/src/lib/server/ad-audience-sync-run.d.ts +134 -0
- package/src/lib/server/ad-audience-sync-run.js +364 -0
- package/src/lib/server/ad-audience-sync-run.js.map +1 -0
- package/src/lib/server/ad-audience-sync.d.ts +250 -0
- package/src/lib/server/ad-audience-sync.js +446 -0
- package/src/lib/server/ad-audience-sync.js.map +1 -0
- package/src/lib/server/consent-by-address.d.ts +101 -0
- package/src/lib/server/consent-by-address.js +178 -0
- package/src/lib/server/consent-by-address.js.map +1 -0
- package/src/lib/server/notifications.d.ts +23 -0
- package/src/lib/server/notifications.js +35 -16
- package/src/lib/server/notifications.js.map +1 -1
- package/src/lib/server/serve-media-cdn.js +4 -1
- package/src/lib/server/serve-media-cdn.js.map +1 -1
- package/src/lib/server/storage-read-stream.d.ts +43 -0
- package/src/lib/server/storage-read-stream.js +77 -0
- package/src/lib/server/storage-read-stream.js.map +1 -0
|
@@ -0,0 +1,250 @@
|
|
|
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
|
+
* THE ACCOUNT-HOLDER AD AUDIENCE (AGL-3780) — the pure half.
|
|
19
|
+
*
|
|
20
|
+
* Once a night the operator's own account holders are kept in step with a
|
|
21
|
+
* customer-list audience on each of the operator's OWN ad accounts — Meta
|
|
22
|
+
* Custom Audiences, Google Customer Match and LinkedIn DMP segments — so
|
|
23
|
+
* signed-up people can be retargeted and left out of prospecting. This is
|
|
24
|
+
* the platform's marketing, not a merchant feature: a merchant's own pixels
|
|
25
|
+
* and conversion APIs are the ad-conversions plugin, which this shares
|
|
26
|
+
* nothing with.
|
|
27
|
+
*
|
|
28
|
+
* Everything here is pure — normalization, hashing, who is eligible, the
|
|
29
|
+
* add/remove diff and each provider's request — so the spec walks every rule
|
|
30
|
+
* without a network. `ad-audience-sync-run.ts` reads the accounts, sends and
|
|
31
|
+
* records.
|
|
32
|
+
*
|
|
33
|
+
* No address ever leaves in the clear: each provider receives the SHA-256 hex
|
|
34
|
+
* digest of the address normalized the way that provider documents, and
|
|
35
|
+
* nothing here logs either one.
|
|
36
|
+
*/
|
|
37
|
+
export declare const AD_AUDIENCE_PROVIDERS: readonly ["meta", "google", "linkedin"];
|
|
38
|
+
export type AdAudienceProvider = (typeof AD_AUDIENCE_PROVIDERS)[number];
|
|
39
|
+
export declare const AD_AUDIENCE_PROVIDER_LABELS: Record<AdAudienceProvider, string>;
|
|
40
|
+
/**
|
|
41
|
+
* The address the way one provider says to normalize it before hashing, or
|
|
42
|
+
* null when it is not an address at all.
|
|
43
|
+
*
|
|
44
|
+
* - **Meta** (Custom Audiences, `EMAIL`): trim leading and trailing white
|
|
45
|
+
* space, lowercase.
|
|
46
|
+
* - **LinkedIn** (DMP segment users, `SHA256_EMAIL`): lowercase, remove any
|
|
47
|
+
* white space.
|
|
48
|
+
* - **Google** (Data Manager API, Customer Match): lowercase, remove leading,
|
|
49
|
+
* trailing and inner white space, and for `gmail.com` and `googlemail.com`
|
|
50
|
+
* only, remove the dots from the local part and drop a `+` and everything
|
|
51
|
+
* after it. Other domains keep both.
|
|
52
|
+
*
|
|
53
|
+
* Inner white space is never part of a deliverable address, so removing it
|
|
54
|
+
* for Meta as well would change nothing real; it is left exactly as Meta
|
|
55
|
+
* documents it.
|
|
56
|
+
*/
|
|
57
|
+
export declare function normalizeAudienceEmail(provider: AdAudienceProvider, raw: unknown): string | null;
|
|
58
|
+
/** Lowercase SHA-256 hex of a UTF-8 string: what all three providers take. */
|
|
59
|
+
export declare function sha256Hex(value: string): string;
|
|
60
|
+
/** The hash one provider receives for an address, or null for no address. */
|
|
61
|
+
export declare function hashAudienceEmail(provider: AdAudienceProvider, raw: unknown): string | null;
|
|
62
|
+
/** What the sync knows about one account, read from Auth and Firestore. */
|
|
63
|
+
export interface AdAudienceAccount {
|
|
64
|
+
uid: string;
|
|
65
|
+
email: string | null;
|
|
66
|
+
emailVerified: boolean;
|
|
67
|
+
disabled: boolean;
|
|
68
|
+
/** The `staff` custom claim. */
|
|
69
|
+
staff: boolean;
|
|
70
|
+
/** `users/{uid}.adAudienceOptOut` is set. */
|
|
71
|
+
optedOut: boolean;
|
|
72
|
+
/** An account lockdown is in force (`lockdowns/user--{uid}`). */
|
|
73
|
+
locked: boolean;
|
|
74
|
+
/** The address is on the account-ban suppression list (an `abuse` ban). */
|
|
75
|
+
banned: boolean;
|
|
76
|
+
/**
|
|
77
|
+
* The billing-address countries (`orgs/{id}.contact.address.country`) of
|
|
78
|
+
* the workspaces the account OWNS.
|
|
79
|
+
*/
|
|
80
|
+
billingCountries: readonly string[];
|
|
81
|
+
/** `users/{uid}.acquisition.geo.country`: where the sign-up request came from. */
|
|
82
|
+
signupCountry: string | null;
|
|
83
|
+
}
|
|
84
|
+
/** Why an account is left out. Counted in every report; never per person. */
|
|
85
|
+
export type AdAudienceExclusion = 'no-email' | 'disabled' | 'staff' | 'internal' | 'locked' | 'banned' | 'opted-out' | 'unverified' | 'no-country' | 'not-us';
|
|
86
|
+
export declare const AD_AUDIENCE_EXCLUSIONS: readonly AdAudienceExclusion[];
|
|
87
|
+
/** The one country the audience is built for. */
|
|
88
|
+
export declare const AD_AUDIENCE_COUNTRY = "US";
|
|
89
|
+
export type AdAudienceCountrySource = 'billing' | 'signup';
|
|
90
|
+
/**
|
|
91
|
+
* The account's country, and which signal said so.
|
|
92
|
+
*
|
|
93
|
+
* 1. **The billing address** of a workspace the account owns. It is the
|
|
94
|
+
* address the account holder typed or gave Stripe, it is the address tax
|
|
95
|
+
* is computed on, and it is the strongest country fact the platform holds.
|
|
96
|
+
* When the account owns workspaces billed in different countries, the
|
|
97
|
+
* answer is US only when EVERY one of them is US: an account that bills
|
|
98
|
+
* from abroad is not counted as a US account holder on the strength of
|
|
99
|
+
* one US workspace.
|
|
100
|
+
* 2. Otherwise **the sign-up geography**: the country the edge reported for
|
|
101
|
+
* the request that created the account (AGL-3289). It says where the
|
|
102
|
+
* person was when they signed up, which is a weaker fact, and it is the
|
|
103
|
+
* one most accounts have.
|
|
104
|
+
* 3. Otherwise nothing, and an account with no country is left out. A phone
|
|
105
|
+
* number's `+1` is deliberately not a signal: it is shared with Canada and
|
|
106
|
+
* the Caribbean.
|
|
107
|
+
*/
|
|
108
|
+
export declare function resolveAccountCountry(account: {
|
|
109
|
+
billingCountries: readonly string[];
|
|
110
|
+
signupCountry: string | null;
|
|
111
|
+
}): {
|
|
112
|
+
country: string | null;
|
|
113
|
+
source: AdAudienceCountrySource | null;
|
|
114
|
+
};
|
|
115
|
+
/** The domain part of an operator address, lowercased, or null. */
|
|
116
|
+
export declare function emailDomain(address: string | null | undefined): string | null;
|
|
117
|
+
/**
|
|
118
|
+
* Why this account is left out, or null when it belongs in the audience.
|
|
119
|
+
*
|
|
120
|
+
* Every condition must hold: an address, not disabled, not staff, not on the
|
|
121
|
+
* operator's own domain, no account lock (an `abuse` lock is a permanent ban),
|
|
122
|
+
* not on the ban list, not opted out, a verified address, and a United
|
|
123
|
+
* States country. Account deletion needs no rule: an erased account has no
|
|
124
|
+
* Auth record, so it is never read at all, and the diff removes it.
|
|
125
|
+
*
|
|
126
|
+
* The address must be VERIFIED because anyone can sign up with an address
|
|
127
|
+
* that is not theirs, and uploading it would put a person who never signed up
|
|
128
|
+
* into the audience. A Google or SSO sign-in arrives verified.
|
|
129
|
+
*/
|
|
130
|
+
export declare function adAudienceExclusion(account: AdAudienceAccount, options?: {
|
|
131
|
+
internalDomains?: readonly string[];
|
|
132
|
+
}): AdAudienceExclusion | null;
|
|
133
|
+
/** What has to change for a provider to hold `next` when it holds `previous`. */
|
|
134
|
+
export declare function diffAudience(previous: Iterable<string>, next: Iterable<string>): {
|
|
135
|
+
toAdd: string[];
|
|
136
|
+
toRemove: string[];
|
|
137
|
+
};
|
|
138
|
+
/**
|
|
139
|
+
* Whether a night's set shrank so far it is more likely a failed read than
|
|
140
|
+
* real churn. Below twenty there is no telling the two apart, so it is never
|
|
141
|
+
* refused; above it, losing more than half the audience in one night is held
|
|
142
|
+
* until a person passes `force`.
|
|
143
|
+
*/
|
|
144
|
+
export declare function isSuspiciousShrink(previousSize: number, nextSize: number): boolean;
|
|
145
|
+
/** `items` in runs of at most `size`. */
|
|
146
|
+
export declare function chunk<T>(items: readonly T[], size: number): T[][];
|
|
147
|
+
export interface MetaAudienceConfig {
|
|
148
|
+
accessToken: string;
|
|
149
|
+
audienceId: string;
|
|
150
|
+
}
|
|
151
|
+
export interface GoogleAudienceConfig {
|
|
152
|
+
clientId: string;
|
|
153
|
+
clientSecret: string;
|
|
154
|
+
refreshToken: string;
|
|
155
|
+
/** The Google Ads customer that owns the list, digits only. */
|
|
156
|
+
customerId: string;
|
|
157
|
+
/** A manager account the credentials reach the customer through, digits only. */
|
|
158
|
+
loginCustomerId: string | null;
|
|
159
|
+
/** The Customer Match user list id. */
|
|
160
|
+
userListId: string;
|
|
161
|
+
}
|
|
162
|
+
export interface LinkedInAudienceConfig {
|
|
163
|
+
accessToken: string;
|
|
164
|
+
segmentId: string;
|
|
165
|
+
}
|
|
166
|
+
export interface AdAudienceConfig {
|
|
167
|
+
meta: MetaAudienceConfig | null;
|
|
168
|
+
google: GoogleAudienceConfig | null;
|
|
169
|
+
linkedin: LinkedInAudienceConfig | null;
|
|
170
|
+
/** For each provider left unconfigured, the variables it is missing. */
|
|
171
|
+
missing: Record<AdAudienceProvider, string[]>;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* The variables, read here by their literal names so the self-host
|
|
175
|
+
* environment reference can see every one of them.
|
|
176
|
+
*/
|
|
177
|
+
export declare function adAudienceEnv(): Record<string, string | undefined>;
|
|
178
|
+
/**
|
|
179
|
+
* Each provider's settings, or null with the names it lacks. A provider is
|
|
180
|
+
* configured only when every required variable is set; a half-configured one
|
|
181
|
+
* is skipped, never half-sent.
|
|
182
|
+
*
|
|
183
|
+
* The Meta audience is never created here. Creating one when the id is unset
|
|
184
|
+
* would mint a fresh, empty audience on every run whose variable was lost, and
|
|
185
|
+
* the ads targeting the real one would go on pointing at it, so the id is
|
|
186
|
+
* required and the audience is made once, by hand.
|
|
187
|
+
*/
|
|
188
|
+
export declare function readAdAudienceConfig(env?: Record<string, string | undefined>): AdAudienceConfig;
|
|
189
|
+
/** One HTTP request, built without sending it. */
|
|
190
|
+
export interface AudienceRequest {
|
|
191
|
+
method: 'POST';
|
|
192
|
+
url: string;
|
|
193
|
+
headers: Record<string, string>;
|
|
194
|
+
/** A JSON body, or a form body when `headers` says so. */
|
|
195
|
+
body: Record<string, unknown> | string;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* META — the replace session.
|
|
199
|
+
*
|
|
200
|
+
* `POST /{audience}/usersreplace` swaps the audience's whole membership for
|
|
201
|
+
* the batches of one session: `batch_seq` 1 opens it and `last_batch_flag`
|
|
202
|
+
* closes it, so a removal needs no separate call and nothing has to be
|
|
203
|
+
* remembered about who was there before. Each batch carries at most 10,000
|
|
204
|
+
* hashes, and the session must finish inside 90 minutes. A replace does not
|
|
205
|
+
* reset an ad set's learning phase, which an add and a delete would.
|
|
206
|
+
*/
|
|
207
|
+
export declare const META_GRAPH_VERSION = "v25.0";
|
|
208
|
+
export declare const META_REPLACE_BATCH = 10000;
|
|
209
|
+
export declare function buildMetaReplaceRequests(config: MetaAudienceConfig, hashes: readonly string[], sessionId: number): AudienceRequest[];
|
|
210
|
+
/**
|
|
211
|
+
* GOOGLE — the Data Manager API, not the Google Ads API.
|
|
212
|
+
*
|
|
213
|
+
* Since 2026-04-01 the Google Ads API's `OfflineUserDataJobService` refuses
|
|
214
|
+
* Customer Match uploads for a developer token with no Customer Match
|
|
215
|
+
* activity in the six months before, and Google points every such project at
|
|
216
|
+
* the Data Manager API instead. It needs an OAuth token with the
|
|
217
|
+
* `datamanager` scope and no developer token.
|
|
218
|
+
*
|
|
219
|
+
* Adds go to `audienceMembers:ingest` and removals to `audienceMembers:remove`,
|
|
220
|
+
* 10,000 members a request. The Data Manager API has no replace, so the
|
|
221
|
+
* removals come from the set this sync last sent.
|
|
222
|
+
*
|
|
223
|
+
* No `consent` is sent. The audience is United States account holders only,
|
|
224
|
+
* for whom Google requires no consent signal, and stating `CONSENT_GRANTED`
|
|
225
|
+
* would assert a consent this platform does not collect.
|
|
226
|
+
* `customerMatchTermsOfServiceStatus: ACCEPTED` is the operator's own
|
|
227
|
+
* acceptance of the Customer Match terms, which Google requires on an
|
|
228
|
+
* ingest; they are accepted in the Google Ads account before this is
|
|
229
|
+
* configured.
|
|
230
|
+
*/
|
|
231
|
+
export declare const GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
232
|
+
export declare const GOOGLE_DATA_MANAGER_BASE = "https://datamanager.googleapis.com/v1";
|
|
233
|
+
export declare const GOOGLE_MEMBERS_BATCH = 10000;
|
|
234
|
+
export declare function buildGoogleTokenRequest(config: GoogleAudienceConfig): AudienceRequest;
|
|
235
|
+
export declare function buildGoogleMemberRequests(config: GoogleAudienceConfig, accessToken: string, action: 'ingest' | 'remove', hashes: readonly string[]): AudienceRequest[];
|
|
236
|
+
/**
|
|
237
|
+
* LINKEDIN — DMP segment users, streamed.
|
|
238
|
+
*
|
|
239
|
+
* `POST /rest/dmpSegments/{id}/users` with `X-RestLi-Method: BATCH_CREATE`
|
|
240
|
+
* takes up to 5,000 `ADD` or `REMOVE` elements a request. There is no
|
|
241
|
+
* replace, so the removals come from the set this sync last sent. Adding an
|
|
242
|
+
* id twice or removing one already gone both answer 201, so a retry is safe.
|
|
243
|
+
* The API is open only to developers LinkedIn has approved for Matched
|
|
244
|
+
* Audiences, with the `rw_dmp_segments` permission.
|
|
245
|
+
*/
|
|
246
|
+
export declare const LINKEDIN_VERSION = "202609";
|
|
247
|
+
export declare const LINKEDIN_USERS_BATCH = 5000;
|
|
248
|
+
export declare function buildLinkedInUserRequests(config: LinkedInAudienceConfig, action: 'ADD' | 'REMOVE', hashes: readonly string[]): AudienceRequest[];
|
|
249
|
+
/** How many elements of a LinkedIn batch response were refused. */
|
|
250
|
+
export declare function linkedInRefusedCount(answer: unknown): number;
|