@aglyn/aglyn 1.0.0-beta.196 → 1.0.0-beta.198
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 +11 -11
- package/src/lib/app-utils/consent-group-change.d.ts +519 -0
- package/src/lib/app-utils/consent-group-change.js +671 -0
- package/src/lib/app-utils/consent-group-change.js.map +1 -0
- package/src/lib/app-utils/consent-groups.d.ts +106 -8
- package/src/lib/app-utils/consent-groups.js +163 -8
- package/src/lib/app-utils/consent-groups.js.map +1 -1
- package/src/lib/app-utils/contact-merge.js +37 -4
- package/src/lib/app-utils/contact-merge.js.map +1 -1
- package/src/lib/app-utils/docs-help.generated.d.ts +7 -7
- package/src/lib/app-utils/docs-help.generated.js +14 -0
- package/src/lib/app-utils/docs-help.generated.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.js +49 -35
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/health-report.js +13 -0
- package/src/lib/app-utils/health-report.js.map +1 -1
- package/src/lib/app-utils/marketing-consent.d.ts +59 -0
- package/src/lib/app-utils/marketing-consent.js +86 -0
- package/src/lib/app-utils/marketing-consent.js.map +1 -1
- package/src/lib/foundation/definitions/org-billing.types.d.ts +22 -3
- package/src/lib/foundation/definitions/org-billing.types.js.map +1 -1
- package/src/lib/plugin-manager/plugin-consent-group-change.d.ts +116 -0
- package/src/lib/plugin-manager/plugin-consent-group-change.js +113 -0
- package/src/lib/plugin-manager/plugin-consent-group-change.js.map +1 -0
- package/src/lib/plugin-manager/plugin-contact-capture.d.ts +8 -0
- package/src/lib/plugin-manager/plugin-contact-capture.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/aglyn",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.198",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -33,16 +33,16 @@
|
|
|
33
33
|
"./package.json": "./package.json"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@aglyn/shared-data-enums": "1.0.0-beta.
|
|
37
|
-
"@aglyn/shared-data-mdi": "1.0.0-beta.
|
|
38
|
-
"@aglyn/shared-data-types": "1.0.0-beta.
|
|
39
|
-
"@aglyn/shared-util-email": "1.0.0-beta.
|
|
40
|
-
"@aglyn/shared-util-first-touch": "1.0.0-beta.
|
|
41
|
-
"@aglyn/shared-util-http": "1.0.0-beta.
|
|
42
|
-
"@aglyn/shared-util-logger": "1.0.0-beta.
|
|
43
|
-
"@aglyn/shared-util-timestamp": "1.0.0-beta.
|
|
44
|
-
"@aglyn/shared-util-tools": "1.0.0-beta.
|
|
45
|
-
"@aglyn/shared-util-vendor": "1.0.0-beta.
|
|
36
|
+
"@aglyn/shared-data-enums": "1.0.0-beta.198",
|
|
37
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.198",
|
|
38
|
+
"@aglyn/shared-data-types": "1.0.0-beta.198",
|
|
39
|
+
"@aglyn/shared-util-email": "1.0.0-beta.198",
|
|
40
|
+
"@aglyn/shared-util-first-touch": "1.0.0-beta.198",
|
|
41
|
+
"@aglyn/shared-util-http": "1.0.0-beta.198",
|
|
42
|
+
"@aglyn/shared-util-logger": "1.0.0-beta.198",
|
|
43
|
+
"@aglyn/shared-util-timestamp": "1.0.0-beta.198",
|
|
44
|
+
"@aglyn/shared-util-tools": "1.0.0-beta.198",
|
|
45
|
+
"@aglyn/shared-util-vendor": "1.0.0-beta.198",
|
|
46
46
|
"@data-driven-forms/react-form-renderer": "^4.2.0",
|
|
47
47
|
"@msgpack/msgpack": "^3.1.3",
|
|
48
48
|
"@types/unist": "^3.0.3",
|
|
@@ -0,0 +1,519 @@
|
|
|
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
|
+
* CHANGING WHICH SITES ARE ONE SENDER (AGL-3320) — the pure half.
|
|
19
|
+
*
|
|
20
|
+
* `consent-groups.ts` reads a declaration; this is what a change to one
|
|
21
|
+
* means. It validates the next declaration an admin submits, decides what has
|
|
22
|
+
* to move for the sites to stop or start being one sender, and words the
|
|
23
|
+
* result — and it does all of that without a read, so the console's review
|
|
24
|
+
* step, the route's refusal and the executor's plan are one computation.
|
|
25
|
+
*
|
|
26
|
+
* ## Why a change is a plan and not a write
|
|
27
|
+
*
|
|
28
|
+
* A declaration is read at send time, so flipping it is instant — and that is
|
|
29
|
+
* exactly the problem. Two facts are stored per SITE and read across the
|
|
30
|
+
* CURRENT group: a refusal (a suppression row, a topic opt-out, a pace, a
|
|
31
|
+
* declined consent) and a holder's business record (the CRM facet keyed by
|
|
32
|
+
* the group). When sites stop being one sender, each stops reading the
|
|
33
|
+
* other's refusals; when they become one, the records they kept apart must
|
|
34
|
+
* become one. The flip itself moves neither, so every change is:
|
|
35
|
+
*
|
|
36
|
+
* 1. CARRY the refusals onto the sites that are about to stop reading them,
|
|
37
|
+
* in BOTH directions — the person who left one site of a sender was told
|
|
38
|
+
* the opt-out covers all of them, so the site leaving keeps what it
|
|
39
|
+
* honored and the sites staying keep what the leaver honored;
|
|
40
|
+
* 2. FLIP the declaration;
|
|
41
|
+
* 3. RE-HOME the holder records keyed by the old group ids.
|
|
42
|
+
*
|
|
43
|
+
* {@link planConsentGroupChange} says which pairs carry and which holder keys
|
|
44
|
+
* move or split; the executor in `libs/tenant/data/admin` performs it.
|
|
45
|
+
*
|
|
46
|
+
* ## What the planner never touches
|
|
47
|
+
*
|
|
48
|
+
* Grants. A grant is written forward, one entry per disclosed site, and is
|
|
49
|
+
* evidence of what a person was told, so a change never copies, re-keys or
|
|
50
|
+
* revokes one — see `consent-groups.ts`, "Pooling applies FORWARD only".
|
|
51
|
+
*/
|
|
52
|
+
import { type StoredConsentGroup } from './consent-groups';
|
|
53
|
+
/**
|
|
54
|
+
* The org field that marks a change in flight:
|
|
55
|
+
* {@link ConsentGroupsChangeMarker}. Authoritative — the job document holds
|
|
56
|
+
* the detail, and this says whether there is a job at all.
|
|
57
|
+
*
|
|
58
|
+
* On the org document, beside the declaration it guards, so the transaction
|
|
59
|
+
* that starts a change and the one that flips the declaration read one
|
|
60
|
+
* document. Server-owned: denied to every client by the rules.
|
|
61
|
+
*/
|
|
62
|
+
export declare const CONSENT_GROUPS_CHANGE_FIELD = "consentGroupsChange";
|
|
63
|
+
/** `orgs/{orgId}/consentGroupChanges/{changeId}` — one job per change. */
|
|
64
|
+
export declare const CONSENT_GROUP_CHANGES_COLLECTION = "consentGroupChanges";
|
|
65
|
+
/**
|
|
66
|
+
* The longest group name a declaration may carry.
|
|
67
|
+
*
|
|
68
|
+
* The name is rendered on every signup form of the group, inside a sentence,
|
|
69
|
+
* so it is a brand and not a description.
|
|
70
|
+
*/
|
|
71
|
+
export declare const CONSENT_GROUP_NAME_MAX = 80;
|
|
72
|
+
/**
|
|
73
|
+
* How long after the flip the sweep waits before its final pass.
|
|
74
|
+
*
|
|
75
|
+
* A long job — a campaign batch, a dynamic list, an outreach tick — resolves
|
|
76
|
+
* the group once per batch and runs for at most five minutes, so a write it
|
|
77
|
+
* made against the OLD declaration can land after the flip. Six minutes is
|
|
78
|
+
* past the longest of them.
|
|
79
|
+
*/
|
|
80
|
+
export declare const CONSENT_GROUP_SWEEP_DELAY_MS: number;
|
|
81
|
+
/**
|
|
82
|
+
* What every minted group id starts with.
|
|
83
|
+
*
|
|
84
|
+
* A group id is a holder key beside the site ids a solo site is keyed by, so
|
|
85
|
+
* the two must never meet. Firestore's automatic ids — which every site id is
|
|
86
|
+
* — are alphanumeric, and an underscore cannot occur in one.
|
|
87
|
+
*/
|
|
88
|
+
export declare const CONSENT_GROUP_ID_PREFIX = "cg_";
|
|
89
|
+
/** Every group of a declaration, by id — what `readConsentGroups` answers. */
|
|
90
|
+
export type ConsentGroupDeclaration = Record<string, StoredConsentGroup>;
|
|
91
|
+
/** One group of the next declaration as the editor submits it. */
|
|
92
|
+
export interface ConsentGroupDraft {
|
|
93
|
+
/** The id of a group that exists; absent for a new group. */
|
|
94
|
+
id?: string | null;
|
|
95
|
+
name: string;
|
|
96
|
+
hostIds: string[];
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Why a submitted declaration was refused. Every code names something the
|
|
100
|
+
* admin can fix in the editor; `groupIndex` points at the draft it is about,
|
|
101
|
+
* and `hostId` at the site.
|
|
102
|
+
*/
|
|
103
|
+
export type ConsentGroupValidationCode =
|
|
104
|
+
/** Not a list of `{ name, hostIds }` objects. */
|
|
105
|
+
'malformed' | 'name-empty' | 'name-too-long'
|
|
106
|
+
/** Two groups with the same name, ignoring case. */
|
|
107
|
+
| 'name-duplicate' | 'too-few-sites' | 'too-many-sites'
|
|
108
|
+
/** A site this organization does not own. */
|
|
109
|
+
| 'unknown-site' | 'site-in-two-groups'
|
|
110
|
+
/** An id no group of the current declaration has. */
|
|
111
|
+
| 'unknown-group'
|
|
112
|
+
/** One id on two drafts. */
|
|
113
|
+
| 'duplicate-group'
|
|
114
|
+
/**
|
|
115
|
+
* A kept id whose sites are ALL new. Keeping the id would carry the old
|
|
116
|
+
* group's records onto a set of sites none of which kept them; a new group
|
|
117
|
+
* is what that is.
|
|
118
|
+
*/
|
|
119
|
+
| 'group-replaced'
|
|
120
|
+
/** The declaration submitted is the one in force. */
|
|
121
|
+
| 'no-change';
|
|
122
|
+
export interface ConsentGroupValidationError {
|
|
123
|
+
code: ConsentGroupValidationCode;
|
|
124
|
+
groupIndex?: number;
|
|
125
|
+
hostId?: string;
|
|
126
|
+
}
|
|
127
|
+
export type ConsentGroupValidation = {
|
|
128
|
+
ok: true;
|
|
129
|
+
after: ConsentGroupDeclaration;
|
|
130
|
+
} | {
|
|
131
|
+
ok: false;
|
|
132
|
+
errors: ConsentGroupValidationError[];
|
|
133
|
+
};
|
|
134
|
+
/**
|
|
135
|
+
* The sites an organization owns, from its `hosts` map, sorted.
|
|
136
|
+
*
|
|
137
|
+
* The map is `{ [hostId]: true }`; an array is read too, so a hand-built
|
|
138
|
+
* fixture and the stored shape agree.
|
|
139
|
+
*/
|
|
140
|
+
export declare function orgSiteIds(org: Record<string, unknown> | null | undefined): string[];
|
|
141
|
+
/** The raw declaration's ids that `readConsentGroups` drops, sorted. */
|
|
142
|
+
export declare function discardedConsentGroupIds(org: Record<string, unknown> | null | undefined): string[];
|
|
143
|
+
/**
|
|
144
|
+
* A fresh group id: {@link CONSENT_GROUP_ID_PREFIX} and sixteen base-36
|
|
145
|
+
* characters, never one in `taken`.
|
|
146
|
+
*
|
|
147
|
+
* `taken` holds every id the id must not equal — the current groups', the
|
|
148
|
+
* raw declaration's, every site's and every id already minted for the same
|
|
149
|
+
* declaration. Eighty bits make a collision a formality, and the loop makes it
|
|
150
|
+
* impossible.
|
|
151
|
+
*/
|
|
152
|
+
export declare function mintConsentGroupId(taken: ReadonlySet<string>, random?: () => number): string;
|
|
153
|
+
/**
|
|
154
|
+
* Whether two declarations say the same thing: the same ids, each with the
|
|
155
|
+
* same trimmed name and the same set of sites. Order never matters.
|
|
156
|
+
*/
|
|
157
|
+
export declare function consentGroupsEqual(a: Record<string, unknown> | null | undefined, b: Record<string, unknown> | null | undefined): boolean;
|
|
158
|
+
/**
|
|
159
|
+
* A declaration as one canonical string: what "the value the editor
|
|
160
|
+
* rendered" and "the value stored now" are compared by.
|
|
161
|
+
*
|
|
162
|
+
* Every entry is kept, usable or not, because the question is whether the
|
|
163
|
+
* stored value CHANGED, and an edit to an entry `readConsentGroups` drops is
|
|
164
|
+
* still somebody else's edit. Absent, `null` and `{}` are one value — an
|
|
165
|
+
* organization with no groups.
|
|
166
|
+
*/
|
|
167
|
+
export declare function consentGroupsFingerprint(raw: unknown): string;
|
|
168
|
+
/**
|
|
169
|
+
* The next declaration, checked against the organization's sites and the
|
|
170
|
+
* declaration in force, with an id minted for every new group.
|
|
171
|
+
*
|
|
172
|
+
* Refuses everything in the list on {@link ConsentGroupValidationCode}, and
|
|
173
|
+
* reports every problem it finds rather than the first, so the editor can
|
|
174
|
+
* mark each draft at once.
|
|
175
|
+
*/
|
|
176
|
+
export declare function validateConsentGroupDeclaration(input: {
|
|
177
|
+
groups: unknown;
|
|
178
|
+
/** The declaration in force: `readConsentGroups(org)`. */
|
|
179
|
+
before: ConsentGroupDeclaration;
|
|
180
|
+
/** The organization's sites: {@link orgSiteIds}. */
|
|
181
|
+
siteIds: readonly string[];
|
|
182
|
+
/** Ids a minted id must also avoid — the raw declaration's, for one. */
|
|
183
|
+
reserved?: Iterable<string>;
|
|
184
|
+
random?: () => number;
|
|
185
|
+
}): ConsentGroupValidation;
|
|
186
|
+
/**
|
|
187
|
+
* One direction of a carry: `toHostId` is about to stop reading
|
|
188
|
+
* `fromHostId`'s refusals, so they are copied onto it first.
|
|
189
|
+
*/
|
|
190
|
+
export interface ConsentGroupCarry {
|
|
191
|
+
toHostId: string;
|
|
192
|
+
fromHostId: string;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Every site a holder key covered moves under ONE new key: the key's records
|
|
196
|
+
* are folded into the new key's, and the old key is deleted in the same
|
|
197
|
+
* write.
|
|
198
|
+
*/
|
|
199
|
+
export interface ConsentGroupHolderMove {
|
|
200
|
+
kind: 'move';
|
|
201
|
+
/** The key the records are under now: a group id, or a solo site's id. */
|
|
202
|
+
from: string;
|
|
203
|
+
/** The sites `from` covered. */
|
|
204
|
+
fromHostIds: string[];
|
|
205
|
+
/** The key they move under: always a declared group of the next declaration. */
|
|
206
|
+
to: string;
|
|
207
|
+
/** The sites of `to` in the next declaration. */
|
|
208
|
+
toHostIds: string[];
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* The sites a holder key covered go separate ways. Each successor may take a
|
|
212
|
+
* copy of the key's records; the key itself survives when some of its sites
|
|
213
|
+
* stay under it, and is deleted when none do.
|
|
214
|
+
*/
|
|
215
|
+
export interface ConsentGroupHolderSplit {
|
|
216
|
+
kind: 'split';
|
|
217
|
+
from: string;
|
|
218
|
+
fromHostIds: string[];
|
|
219
|
+
/** Whether `from` is still a key afterwards. */
|
|
220
|
+
survives: boolean;
|
|
221
|
+
/** The sites of `from` that stay under it. Empty when it does not survive. */
|
|
222
|
+
stayHostIds: string[];
|
|
223
|
+
/**
|
|
224
|
+
* Where the other sites go: each successor key with the sites of `from`
|
|
225
|
+
* that map to it — never the successor's other sites, whose records were
|
|
226
|
+
* never under `from`.
|
|
227
|
+
*/
|
|
228
|
+
successors: Array<{
|
|
229
|
+
key: string;
|
|
230
|
+
hostIds: string[];
|
|
231
|
+
}>;
|
|
232
|
+
}
|
|
233
|
+
export type ConsentGroupHolderFlow = ConsentGroupHolderMove | ConsentGroupHolderSplit;
|
|
234
|
+
/** What a change does to the declaration, as the activity log words it. */
|
|
235
|
+
export type ConsentGroupChangeLine = {
|
|
236
|
+
kind: 'created';
|
|
237
|
+
groupId: string;
|
|
238
|
+
name: string;
|
|
239
|
+
hostIds: string[];
|
|
240
|
+
} | {
|
|
241
|
+
kind: 'renamed';
|
|
242
|
+
groupId: string;
|
|
243
|
+
from: string;
|
|
244
|
+
to: string;
|
|
245
|
+
} | {
|
|
246
|
+
kind: 'added';
|
|
247
|
+
groupId: string;
|
|
248
|
+
name: string;
|
|
249
|
+
hostId: string;
|
|
250
|
+
} | {
|
|
251
|
+
kind: 'removed';
|
|
252
|
+
groupId: string;
|
|
253
|
+
name: string;
|
|
254
|
+
hostId: string;
|
|
255
|
+
} | {
|
|
256
|
+
kind: 'moved';
|
|
257
|
+
hostId: string;
|
|
258
|
+
fromGroupId: string;
|
|
259
|
+
fromName: string;
|
|
260
|
+
toGroupId: string;
|
|
261
|
+
toName: string;
|
|
262
|
+
}
|
|
263
|
+
/** Includes a group absorbed into another: its sites each get a `moved`. */
|
|
264
|
+
| {
|
|
265
|
+
kind: 'dissolved';
|
|
266
|
+
groupId: string;
|
|
267
|
+
name: string;
|
|
268
|
+
};
|
|
269
|
+
export interface ConsentGroupChangePlan {
|
|
270
|
+
before: ConsentGroupDeclaration;
|
|
271
|
+
after: ConsentGroupDeclaration;
|
|
272
|
+
carries: ConsentGroupCarry[];
|
|
273
|
+
flows: ConsentGroupHolderFlow[];
|
|
274
|
+
lines: ConsentGroupChangeLine[];
|
|
275
|
+
/**
|
|
276
|
+
* Every site the change touches — whose group, members or group name
|
|
277
|
+
* changes. What the marker names while the change runs: the sites that
|
|
278
|
+
* may not be deleted, and whose holder records may be in motion.
|
|
279
|
+
*/
|
|
280
|
+
hostIds: string[];
|
|
281
|
+
/** Only names change: nothing to carry and nothing to re-home. */
|
|
282
|
+
renameOnly: boolean;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* What moving from `before` to `after` requires.
|
|
286
|
+
*
|
|
287
|
+
* `key(s)` is the holder key of site `s` — its group's id, or `s` itself when
|
|
288
|
+
* it is alone — and `members(s)` the sites of that group, or `[s]`.
|
|
289
|
+
*
|
|
290
|
+
* - **Carries.** For every pair a ≠ b that are one sender now and will not
|
|
291
|
+
* be, `b`'s refusals are copied onto `a`. Pairs that stay together, and
|
|
292
|
+
* pairs that come together, need nothing: a joined site reads its new
|
|
293
|
+
* siblings' refusals the moment the declaration names it.
|
|
294
|
+
* - **Holder flows.** For each key `k` in force, the sites it covers map to
|
|
295
|
+
* a set `T` of keys afterwards. `T = {k}` needs nothing; one other key is
|
|
296
|
+
* a MOVE; anything else is a SPLIT, which `k` survives when it is in `T`.
|
|
297
|
+
*
|
|
298
|
+
* `siteIds`, when given, are the sites the organization still owns: a site
|
|
299
|
+
* named by `before` that is gone neither receives a carry nor takes a copy of
|
|
300
|
+
* a record, because there is nothing left to hold either.
|
|
301
|
+
*/
|
|
302
|
+
export declare function planConsentGroupChange(before: ConsentGroupDeclaration, after: ConsentGroupDeclaration, options?: {
|
|
303
|
+
siteIds?: readonly string[];
|
|
304
|
+
}): ConsentGroupChangePlan;
|
|
305
|
+
/**
|
|
306
|
+
* The sites in a sentence: "A", "A and B", "A, B and C".
|
|
307
|
+
*/
|
|
308
|
+
export declare function listSiteNames(names: readonly string[]): string;
|
|
309
|
+
/**
|
|
310
|
+
* One line of the plan as the organization's activity log says it.
|
|
311
|
+
*
|
|
312
|
+
* `siteName` turns a site id into what the console shows for it; the id
|
|
313
|
+
* itself is the fallback, so a line is never blank.
|
|
314
|
+
*/
|
|
315
|
+
export declare function describeConsentGroupChangeLine(line: ConsentGroupChangeLine, siteName?: (hostId: string) => string): string;
|
|
316
|
+
/**
|
|
317
|
+
* Where a change stands, on the org marker.
|
|
318
|
+
*
|
|
319
|
+
* - `carry` — copying refusals; the declaration in force is the old one,
|
|
320
|
+
* and the change can still be canceled.
|
|
321
|
+
* - `rehome` — the new declaration is in force; holder records are moving.
|
|
322
|
+
* - `sweep` — waiting out {@link CONSENT_GROUP_SWEEP_DELAY_MS}, then one
|
|
323
|
+
* last idempotent pass.
|
|
324
|
+
*/
|
|
325
|
+
export type ConsentGroupsChangePhase = 'carry' | 'rehome' | 'sweep';
|
|
326
|
+
/** `org.consentGroupsChange` — present exactly while a change runs. */
|
|
327
|
+
export interface ConsentGroupsChangeMarker {
|
|
328
|
+
changeId: string;
|
|
329
|
+
phase: ConsentGroupsChangePhase;
|
|
330
|
+
/** {@link ConsentGroupChangePlan.hostIds}. */
|
|
331
|
+
hostIds: string[];
|
|
332
|
+
startedAtMs: number;
|
|
333
|
+
/** When the new declaration took effect; absent before the flip. */
|
|
334
|
+
declaredAtMs?: number;
|
|
335
|
+
}
|
|
336
|
+
/** The marker off an org document, or `null` when no change is in flight. */
|
|
337
|
+
export declare function readConsentGroupsChange(org: Record<string, unknown> | null | undefined): ConsentGroupsChangeMarker | null;
|
|
338
|
+
/**
|
|
339
|
+
* Whether the holder records `hostId` reads may be mid-move: a change in
|
|
340
|
+
* flight has flipped the declaration and names this site.
|
|
341
|
+
*
|
|
342
|
+
* A reader that decides something by a record's ABSENCE — a dynamic list
|
|
343
|
+
* that removes whoever no longer matches — must not decide it while this
|
|
344
|
+
* holds, because a record that has not moved yet reads as missing.
|
|
345
|
+
*/
|
|
346
|
+
export declare function consentGroupHoldersInMotion(org: Record<string, unknown> | null | undefined, hostId: string): boolean;
|
|
347
|
+
/**
|
|
348
|
+
* The executor's steps, in order.
|
|
349
|
+
*
|
|
350
|
+
* - `carry` — every carry, then every participant's carry phase.
|
|
351
|
+
* - `catch-up-pre` — the refusals filed since the carry began.
|
|
352
|
+
* - `declare` — the flip, in one transaction.
|
|
353
|
+
* - `catch-up-post` — the same catch-up, for what raced the flip.
|
|
354
|
+
* - `rehome` — every participant's holder records.
|
|
355
|
+
* - `sweep-wait` — until {@link CONSENT_GROUP_SWEEP_DELAY_MS} has passed.
|
|
356
|
+
* - `sweep` — every carry again in full, and every participant's sweep.
|
|
357
|
+
* - `finish` — the marker cleared.
|
|
358
|
+
*
|
|
359
|
+
* A rename-only change runs `declare` and `finish` alone.
|
|
360
|
+
*/
|
|
361
|
+
export declare const CONSENT_GROUP_CHANGE_STEPS: readonly ["carry", "catch-up-pre", "declare", "catch-up-post", "rehome", "sweep-wait", "sweep", "finish"];
|
|
362
|
+
export type ConsentGroupChangeStep = (typeof CONSENT_GROUP_CHANGE_STEPS)[number];
|
|
363
|
+
/** The steps a rename-only change runs. */
|
|
364
|
+
export declare const CONSENT_GROUP_RENAME_STEPS: readonly ConsentGroupChangeStep[];
|
|
365
|
+
/** The marker phase a step runs under. */
|
|
366
|
+
export declare function consentGroupChangePhaseOf(step: ConsentGroupChangeStep): ConsentGroupsChangePhase;
|
|
367
|
+
export type ConsentGroupChangeStatus = 'running' | 'done' | 'canceled';
|
|
368
|
+
/** What the refusal carries copied so far, across every step. */
|
|
369
|
+
export interface ConsentGroupChangeCounts {
|
|
370
|
+
siteSuppressions: number;
|
|
371
|
+
topicOptOuts: number;
|
|
372
|
+
paces: number;
|
|
373
|
+
}
|
|
374
|
+
/** A job's state, as the route answers it and the progress panel renders it. */
|
|
375
|
+
export interface ConsentGroupChangeProgress {
|
|
376
|
+
status: ConsentGroupChangeStatus;
|
|
377
|
+
step: ConsentGroupChangeStep;
|
|
378
|
+
/** 1-based position of `step` among the steps this change runs. */
|
|
379
|
+
stepIndex: number;
|
|
380
|
+
stepCount: number;
|
|
381
|
+
/** When the new declaration took effect, or `null` before the flip. */
|
|
382
|
+
declaredAtMs: number | null;
|
|
383
|
+
/** When the sweep may start, or `null` before the flip. */
|
|
384
|
+
sweepAtMs: number | null;
|
|
385
|
+
/** Five units in a row failed. The change keeps retrying. */
|
|
386
|
+
stalled: boolean;
|
|
387
|
+
/** Consecutive failed units. */
|
|
388
|
+
failures: number;
|
|
389
|
+
lastError: string | null;
|
|
390
|
+
/** Until when another caller holds the job; `null` when it is free. */
|
|
391
|
+
leaseUntilMs: number | null;
|
|
392
|
+
counts: ConsentGroupChangeCounts;
|
|
393
|
+
/** The sites that received at least one carried refusal. */
|
|
394
|
+
sitesReceiving: number;
|
|
395
|
+
/** Each participant's counts, by plugin id. */
|
|
396
|
+
plugins: Record<string, Record<string, number>>;
|
|
397
|
+
}
|
|
398
|
+
export type ConsentGroupsApiAction = 'preview' | 'apply' | 'continue' | 'cancel' | 'status';
|
|
399
|
+
export interface ConsentGroupsApiDeclarationRequest {
|
|
400
|
+
orgId: string;
|
|
401
|
+
action: 'preview' | 'apply';
|
|
402
|
+
/**
|
|
403
|
+
* `org.consentGroups` exactly as the editor rendered it, or `null` for an
|
|
404
|
+
* organization with none. Compared by {@link consentGroupsFingerprint}.
|
|
405
|
+
*/
|
|
406
|
+
expected: Record<string, unknown> | null;
|
|
407
|
+
/** The COMPLETE next declaration. A new group has no id. */
|
|
408
|
+
groups: ConsentGroupDraft[];
|
|
409
|
+
}
|
|
410
|
+
export interface ConsentGroupsApiChangeRequest {
|
|
411
|
+
orgId: string;
|
|
412
|
+
action: 'continue' | 'cancel' | 'status';
|
|
413
|
+
changeId: string;
|
|
414
|
+
}
|
|
415
|
+
export type ConsentGroupsApiRequest = ConsentGroupsApiDeclarationRequest | ConsentGroupsApiChangeRequest;
|
|
416
|
+
/** 200 for `preview`. */
|
|
417
|
+
export interface ConsentGroupsApiPreviewResponse {
|
|
418
|
+
ok: true;
|
|
419
|
+
preview: ConsentGroupChangePreview;
|
|
420
|
+
}
|
|
421
|
+
/** 200 for `apply`, `continue`, `cancel` and `status`. */
|
|
422
|
+
export interface ConsentGroupsApiChangeResponse {
|
|
423
|
+
ok: true;
|
|
424
|
+
changeId: string;
|
|
425
|
+
phase: ConsentGroupsChangePhase | 'done' | 'canceled';
|
|
426
|
+
done: boolean;
|
|
427
|
+
progress: ConsentGroupChangeProgress;
|
|
428
|
+
}
|
|
429
|
+
/** 400: the declaration was refused. */
|
|
430
|
+
export interface ConsentGroupsApiRefusal {
|
|
431
|
+
error: string;
|
|
432
|
+
errors: ConsentGroupValidationError[];
|
|
433
|
+
}
|
|
434
|
+
/** 409: the declaration changed since the editor rendered it. */
|
|
435
|
+
export interface ConsentGroupsApiStale {
|
|
436
|
+
error: string;
|
|
437
|
+
current: Record<string, unknown> | null;
|
|
438
|
+
}
|
|
439
|
+
/** 409: another change is running, or a cancel came after the flip. */
|
|
440
|
+
export interface ConsentGroupsApiInFlight {
|
|
441
|
+
error: string;
|
|
442
|
+
changeId: string;
|
|
443
|
+
phase: ConsentGroupsChangePhase | 'done' | 'canceled';
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* One sentence a participant adds to the review step, with the figure it is
|
|
447
|
+
* about. `id` is namespaced by the participant (`crm.combine`).
|
|
448
|
+
*/
|
|
449
|
+
export interface ConsentGroupChangePreviewLine {
|
|
450
|
+
id: string;
|
|
451
|
+
text: string;
|
|
452
|
+
count: number | null;
|
|
453
|
+
severity: 'info' | 'warning';
|
|
454
|
+
}
|
|
455
|
+
/** How long a change should take, from the work the preview counted. */
|
|
456
|
+
export type ConsentGroupChangeEstimate = 'instant' | 'under-a-minute' | 'minutes' | 'long';
|
|
457
|
+
/** The work bands behind {@link ConsentGroupChangeEstimate}. */
|
|
458
|
+
export declare function estimateConsentGroupChange(plan: Pick<ConsentGroupChangePlan, 'renameOnly'>, documents: number): ConsentGroupChangeEstimate;
|
|
459
|
+
export interface ConsentGroupChangePreview {
|
|
460
|
+
before: ConsentGroupDeclaration;
|
|
461
|
+
after: ConsentGroupDeclaration;
|
|
462
|
+
/** Stored ids `readConsentGroups` drops; the write removes them. */
|
|
463
|
+
discarded: string[];
|
|
464
|
+
/** The plan's lines, each with the sentence the activity log will write. */
|
|
465
|
+
lines: Array<ConsentGroupChangeLine & {
|
|
466
|
+
text: string;
|
|
467
|
+
}>;
|
|
468
|
+
/** Every group whose signup-form sentence changes. */
|
|
469
|
+
disclosures: Array<{
|
|
470
|
+
groupId: string;
|
|
471
|
+
hostIds: string[];
|
|
472
|
+
before: string | null;
|
|
473
|
+
after: string | null;
|
|
474
|
+
}>;
|
|
475
|
+
/** Per carry, what the source site holds now (count aggregations). */
|
|
476
|
+
carries: Array<{
|
|
477
|
+
toHostId: string;
|
|
478
|
+
fromHostId: string;
|
|
479
|
+
siteSuppressions: number;
|
|
480
|
+
topicOptOuts: number;
|
|
481
|
+
paces: number;
|
|
482
|
+
}>;
|
|
483
|
+
/** Per joining site, the refusals on its new siblings it starts honoring. */
|
|
484
|
+
inherited: Array<{
|
|
485
|
+
hostId: string;
|
|
486
|
+
refusals: number;
|
|
487
|
+
}>;
|
|
488
|
+
/**
|
|
489
|
+
* Pending confirmations that stop holding a separated site's mail. Only
|
|
490
|
+
* when the organization's confirmation switch is on.
|
|
491
|
+
*/
|
|
492
|
+
pendingHolds: Array<{
|
|
493
|
+
hostId: string;
|
|
494
|
+
topicId: string;
|
|
495
|
+
count: number;
|
|
496
|
+
releasedHostIds: string[];
|
|
497
|
+
}>;
|
|
498
|
+
/** Team members who can open only some of a changed group's sites. */
|
|
499
|
+
partialAccessMembers: number;
|
|
500
|
+
/**
|
|
501
|
+
* Set when the organization's consent policy is `forward` and a site joins:
|
|
502
|
+
* the joining sites may email people their new siblings captured before
|
|
503
|
+
* `enforceFromMs` without a recorded opt-in.
|
|
504
|
+
*/
|
|
505
|
+
forwardPolicyWarning: {
|
|
506
|
+
hostIds: string[];
|
|
507
|
+
enforceFromMs: number;
|
|
508
|
+
} | null;
|
|
509
|
+
/** Each participant's lines; `null` for one whose preview failed. */
|
|
510
|
+
participants: Array<{
|
|
511
|
+
pluginId: string;
|
|
512
|
+
lines: ConsentGroupChangePreviewLine[] | null;
|
|
513
|
+
}>;
|
|
514
|
+
/** The capture surfaces that show a group's disclosure. */
|
|
515
|
+
capturesDisclosing: string[];
|
|
516
|
+
estimate: ConsentGroupChangeEstimate;
|
|
517
|
+
}
|
|
518
|
+
/** The groups whose disclosure sentence differs between the declarations. */
|
|
519
|
+
export declare function consentGroupDisclosureChanges(before: ConsentGroupDeclaration, after: ConsentGroupDeclaration): ConsentGroupChangePreview['disclosures'];
|