@aglyn/aglyn 1.0.0-beta.196 → 1.0.0-beta.197

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 (26) hide show
  1. package/package.json +11 -11
  2. package/src/lib/app-utils/consent-group-change.d.ts +519 -0
  3. package/src/lib/app-utils/consent-group-change.js +671 -0
  4. package/src/lib/app-utils/consent-group-change.js.map +1 -0
  5. package/src/lib/app-utils/consent-groups.d.ts +106 -8
  6. package/src/lib/app-utils/consent-groups.js +163 -8
  7. package/src/lib/app-utils/consent-groups.js.map +1 -1
  8. package/src/lib/app-utils/contact-merge.js +37 -4
  9. package/src/lib/app-utils/contact-merge.js.map +1 -1
  10. package/src/lib/app-utils/docs-help.generated.d.ts +7 -7
  11. package/src/lib/app-utils/docs-help.generated.js +13 -0
  12. package/src/lib/app-utils/docs-help.generated.js.map +1 -1
  13. package/src/lib/app-utils/docs-index.generated.js +48 -34
  14. package/src/lib/app-utils/docs-index.generated.js.map +1 -1
  15. package/src/lib/app-utils/health-report.js +13 -0
  16. package/src/lib/app-utils/health-report.js.map +1 -1
  17. package/src/lib/app-utils/marketing-consent.d.ts +59 -0
  18. package/src/lib/app-utils/marketing-consent.js +86 -0
  19. package/src/lib/app-utils/marketing-consent.js.map +1 -1
  20. package/src/lib/foundation/definitions/org-billing.types.d.ts +22 -3
  21. package/src/lib/foundation/definitions/org-billing.types.js.map +1 -1
  22. package/src/lib/plugin-manager/plugin-consent-group-change.d.ts +116 -0
  23. package/src/lib/plugin-manager/plugin-consent-group-change.js +113 -0
  24. package/src/lib/plugin-manager/plugin-consent-group-change.js.map +1 -0
  25. package/src/lib/plugin-manager/plugin-contact-capture.d.ts +8 -0
  26. 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.196",
3
+ "version": "1.0.0-beta.197",
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.196",
37
- "@aglyn/shared-data-mdi": "1.0.0-beta.196",
38
- "@aglyn/shared-data-types": "1.0.0-beta.196",
39
- "@aglyn/shared-util-email": "1.0.0-beta.196",
40
- "@aglyn/shared-util-first-touch": "1.0.0-beta.196",
41
- "@aglyn/shared-util-http": "1.0.0-beta.196",
42
- "@aglyn/shared-util-logger": "1.0.0-beta.196",
43
- "@aglyn/shared-util-timestamp": "1.0.0-beta.196",
44
- "@aglyn/shared-util-tools": "1.0.0-beta.196",
45
- "@aglyn/shared-util-vendor": "1.0.0-beta.196",
36
+ "@aglyn/shared-data-enums": "1.0.0-beta.197",
37
+ "@aglyn/shared-data-mdi": "1.0.0-beta.197",
38
+ "@aglyn/shared-data-types": "1.0.0-beta.197",
39
+ "@aglyn/shared-util-email": "1.0.0-beta.197",
40
+ "@aglyn/shared-util-first-touch": "1.0.0-beta.197",
41
+ "@aglyn/shared-util-http": "1.0.0-beta.197",
42
+ "@aglyn/shared-util-logger": "1.0.0-beta.197",
43
+ "@aglyn/shared-util-timestamp": "1.0.0-beta.197",
44
+ "@aglyn/shared-util-tools": "1.0.0-beta.197",
45
+ "@aglyn/shared-util-vendor": "1.0.0-beta.197",
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'];