@aglyn/plugins-email 1.0.0-beta.195 → 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 (50) hide show
  1. package/package.json +17 -17
  2. package/src/lib/components/consent-group-access.d.ts +33 -0
  3. package/src/lib/components/consent-group-access.js +73 -0
  4. package/src/lib/components/consent-group-access.js.map +1 -0
  5. package/src/lib/components/consent-group-change-progress.d.ts +55 -0
  6. package/src/lib/components/consent-group-change-progress.js +282 -0
  7. package/src/lib/components/consent-group-change-progress.js.map +1 -0
  8. package/src/lib/components/consent-group-confirmation-card.d.ts +6 -16
  9. package/src/lib/components/consent-group-confirmation-card.js +7 -50
  10. package/src/lib/components/consent-group-confirmation-card.js.map +1 -1
  11. package/src/lib/components/consent-group-dialog.d.ts +53 -0
  12. package/src/lib/components/consent-group-dialog.js +495 -0
  13. package/src/lib/components/consent-group-dialog.js.map +1 -0
  14. package/src/lib/components/consent-group-editing.d.ts +127 -0
  15. package/src/lib/components/consent-group-editing.js +225 -0
  16. package/src/lib/components/consent-group-editing.js.map +1 -0
  17. package/src/lib/components/consent-group-review.d.ts +62 -0
  18. package/src/lib/components/consent-group-review.js +353 -0
  19. package/src/lib/components/consent-group-review.js.map +1 -0
  20. package/src/lib/components/consent-group-site-picker.d.ts +35 -0
  21. package/src/lib/components/consent-group-site-picker.js +190 -0
  22. package/src/lib/components/consent-group-site-picker.js.map +1 -0
  23. package/src/lib/components/consent-groups-api.d.ts +158 -0
  24. package/src/lib/components/consent-groups-api.js +250 -0
  25. package/src/lib/components/consent-groups-api.js.map +1 -0
  26. package/src/lib/components/consent-groups-card.d.ts +47 -0
  27. package/src/lib/components/consent-groups-card.js +351 -0
  28. package/src/lib/components/consent-groups-card.js.map +1 -0
  29. package/src/lib/components/email-screens-card.js +1 -1
  30. package/src/lib/components/email-screens-card.js.map +1 -1
  31. package/src/lib/components/emails-console-page.d.ts +1 -1
  32. package/src/lib/components/emails-console-page.js +62 -27
  33. package/src/lib/components/emails-console-page.js.map +1 -1
  34. package/src/lib/components/emails-console-sections.d.ts +1 -1
  35. package/src/lib/components/emails-console-sections.js +14 -1
  36. package/src/lib/components/emails-console-sections.js.map +1 -1
  37. package/src/lib/components/org-email-templates-card.js +1 -1
  38. package/src/lib/components/org-email-templates-card.js.map +1 -1
  39. package/src/lib/components/site-consent-group-card.d.ts +23 -0
  40. package/src/lib/components/site-consent-group-card.js +80 -0
  41. package/src/lib/components/site-consent-group-card.js.map +1 -0
  42. package/src/lib/components/suppressions-card.js +28 -7
  43. package/src/lib/components/suppressions-card.js.map +1 -1
  44. package/src/lib/server-console.js +4 -2
  45. package/src/lib/server-console.js.map +1 -1
  46. package/src/lib/server-list-gate.d.ts +9 -0
  47. package/src/lib/server-list-gate.js +23 -9
  48. package/src/lib/server-list-gate.js.map +1 -1
  49. package/src/lib/server-list-import.js +5 -2
  50. package/src/lib/server-list-import.js.map +1 -1
@@ -0,0 +1,190 @@
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
+ */ 'use client';
17
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
18
+ import { Box, Checkbox, Chip, FormControl, FormControlLabel, FormHelperText, FormLabel, Stack, TextField, Typography } from "@mui/material";
19
+ import { useMemo, useState } from "react";
20
+ import { MAX_CONSENT_GROUP_HOSTS } from "./consent-groups-api.js";
21
+ /** Past this many sites the list gets a filter, so a long one stays usable. */ const FILTER_FROM = 10;
22
+ /**
23
+ * The sites a consent group names.
24
+ *
25
+ * A site can be one sender with only one set of siblings, so a site already
26
+ * in another group says which — "In Acme" — and, once ticked, what ticking it
27
+ * does: "Moves from Acme". The move is legitimate, and the review spells out
28
+ * what it costs the group it leaves; the badge is there so nobody makes it
29
+ * without seeing it.
30
+ *
31
+ * A site the group names that the org no longer lists is still shown, ticked
32
+ * and marked, so it can be unticked rather than silently carried into the
33
+ * next declaration.
34
+ */ export function ConsentGroupSitePicker(props) {
35
+ const { sites, groups, groupId, selected, onChange, error, refusedHostIds, disabled } = props;
36
+ const [filter, setFilter] = useState('');
37
+ /** The group each site belongs to now, other than the one being edited. */ const memberOf = useMemo(()=>{
38
+ const found = new Map();
39
+ for (const group of groups){
40
+ if (group.id === groupId) continue;
41
+ for (const hostId of group.hostIds)found.set(hostId, group);
42
+ }
43
+ return found;
44
+ }, [
45
+ groups,
46
+ groupId
47
+ ]);
48
+ const rows = useMemo(()=>{
49
+ var _ref;
50
+ var _groups_find;
51
+ const listed = sites.map((site)=>({
52
+ id: site.id,
53
+ name: site.name || site.subdomain || site.id,
54
+ missing: false
55
+ }));
56
+ const known = new Set(listed.map((site)=>site.id));
57
+ const own = (_ref = (_groups_find = groups.find((group)=>group.id === groupId)) == null ? void 0 : _groups_find.hostIds) != null ? _ref : [];
58
+ const orphans = own.filter((hostId)=>!known.has(hostId)).map((hostId)=>({
59
+ id: hostId,
60
+ name: hostId,
61
+ missing: true
62
+ }));
63
+ return [
64
+ ...listed,
65
+ ...orphans
66
+ ].sort((a, b)=>a.name.localeCompare(b.name));
67
+ }, [
68
+ sites,
69
+ groups,
70
+ groupId
71
+ ]);
72
+ const chosen = useMemo(()=>new Set(selected), [
73
+ selected
74
+ ]);
75
+ const full = chosen.size >= MAX_CONSENT_GROUP_HOSTS;
76
+ const needle = filter.trim().toLocaleLowerCase();
77
+ const visible = needle ? rows.filter((row)=>chosen.has(row.id) || row.name.toLocaleLowerCase().includes(needle)) : rows;
78
+ const toggle = (hostId, on)=>{
79
+ const next = new Set(chosen);
80
+ if (on) next.add(hostId);
81
+ else next.delete(hostId);
82
+ onChange([
83
+ ...next
84
+ ].sort());
85
+ };
86
+ return /*#__PURE__*/ _jsxs(FormControl, {
87
+ component: "fieldset",
88
+ error: Boolean(error),
89
+ disabled: disabled,
90
+ fullWidth: true,
91
+ children: [
92
+ /*#__PURE__*/ _jsx(FormLabel, {
93
+ component: "legend",
94
+ children: 'Sites'
95
+ }),
96
+ /*#__PURE__*/ _jsx(FormHelperText, {
97
+ sx: {
98
+ mx: 0
99
+ },
100
+ children: `Choose at least two sites and at most ${MAX_CONSENT_GROUP_HOSTS}. ` + 'A site can be in only one consent group.'
101
+ }),
102
+ rows.length > FILTER_FROM ? /*#__PURE__*/ _jsx(TextField, {
103
+ size: "small",
104
+ label: "Find a site",
105
+ value: filter,
106
+ onChange: (event)=>setFilter(event.target.value),
107
+ sx: {
108
+ mt: 1,
109
+ maxWidth: 320
110
+ }
111
+ }) : null,
112
+ /*#__PURE__*/ _jsxs(Box, {
113
+ sx: {
114
+ mt: 1,
115
+ maxHeight: 320,
116
+ overflowY: 'auto',
117
+ border: 1,
118
+ borderColor: 'divider',
119
+ borderRadius: 1,
120
+ px: 1.5,
121
+ py: 0.5
122
+ },
123
+ children: [
124
+ visible.map((row)=>{
125
+ const checked = chosen.has(row.id);
126
+ const other = memberOf.get(row.id);
127
+ return /*#__PURE__*/ _jsxs(Stack, {
128
+ direction: "row",
129
+ spacing: 1,
130
+ sx: {
131
+ alignItems: 'center',
132
+ flexWrap: 'wrap'
133
+ },
134
+ children: [
135
+ /*#__PURE__*/ _jsx(FormControlLabel, {
136
+ control: /*#__PURE__*/ _jsx(Checkbox, {
137
+ size: "small",
138
+ checked: checked,
139
+ disabled: disabled || !checked && full,
140
+ onChange: (event)=>toggle(row.id, event.target.checked)
141
+ }),
142
+ label: row.name,
143
+ sx: {
144
+ mr: 0
145
+ }
146
+ }),
147
+ other ? /*#__PURE__*/ _jsx(Chip, {
148
+ size: "small",
149
+ variant: "outlined",
150
+ color: checked ? 'warning' : 'default',
151
+ label: checked ? `Moves from ${other.name}` : `In ${other.name}`
152
+ }) : null,
153
+ row.missing ? /*#__PURE__*/ _jsx(Chip, {
154
+ size: "small",
155
+ variant: "outlined",
156
+ color: "warning",
157
+ label: "Not in your organization"
158
+ }) : null,
159
+ (refusedHostIds == null ? void 0 : refusedHostIds.has(row.id)) ? /*#__PURE__*/ _jsx(Chip, {
160
+ size: "small",
161
+ color: "error",
162
+ variant: "outlined",
163
+ label: "Can’t be added"
164
+ }) : null
165
+ ]
166
+ }, row.id);
167
+ }),
168
+ !visible.length ? /*#__PURE__*/ _jsx(Typography, {
169
+ variant: "body2",
170
+ color: "text.secondary",
171
+ sx: {
172
+ py: 1
173
+ },
174
+ children: rows.length ? 'No site matches.' : 'Your organization has no sites yet.'
175
+ }) : null
176
+ ]
177
+ }),
178
+ /*#__PURE__*/ _jsx(FormHelperText, {
179
+ sx: {
180
+ mx: 0
181
+ },
182
+ children: error != null ? error : full ? `That’s the most one group can include (${MAX_CONSENT_GROUP_HOSTS}).` : `${chosen.size} selected`
183
+ })
184
+ ]
185
+ });
186
+ }
187
+ ConsentGroupSitePicker.displayName = 'ConsentGroupSitePicker';
188
+ export default ConsentGroupSitePicker;
189
+
190
+ //# sourceMappingURL=consent-group-site-picker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/consent-group-site-picker.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport type { ConsolePluginOrgHost } from '@aglyn/aglyn'\nimport {\n Box,\n Checkbox,\n Chip,\n FormControl,\n FormControlLabel,\n FormHelperText,\n FormLabel,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { useMemo, useState } from 'react'\nimport type { ListedConsentGroup } from './consent-group-editing'\nimport { MAX_CONSENT_GROUP_HOSTS } from './consent-groups-api'\n\n/** Past this many sites the list gets a filter, so a long one stays usable. */\nconst FILTER_FROM = 10\n\nexport interface ConsentGroupSitePickerProps {\n /** Every site the organization has. */\n sites: readonly ConsolePluginOrgHost[]\n /** The groups the org declares now. */\n groups: readonly ListedConsentGroup[]\n /** The group being edited, whose own sites carry no badge; `null` for a new one. */\n groupId: string | null\n selected: readonly string[]\n onChange: (hostIds: string[]) => void\n /** What is wrong with the selection, said under the list. */\n error?: string\n /** Sites the route refused by id, marked where they are listed. */\n refusedHostIds?: ReadonlySet<string>\n disabled?: boolean\n}\n\n/**\n * The sites a consent group names.\n *\n * A site can be one sender with only one set of siblings, so a site already\n * in another group says which — \"In Acme\" — and, once ticked, what ticking it\n * does: \"Moves from Acme\". The move is legitimate, and the review spells out\n * what it costs the group it leaves; the badge is there so nobody makes it\n * without seeing it.\n *\n * A site the group names that the org no longer lists is still shown, ticked\n * and marked, so it can be unticked rather than silently carried into the\n * next declaration.\n */\nexport function ConsentGroupSitePicker(props: ConsentGroupSitePickerProps) {\n const {\n sites,\n groups,\n groupId,\n selected,\n onChange,\n error,\n refusedHostIds,\n disabled,\n } = props\n const [filter, setFilter] = useState('')\n\n /** The group each site belongs to now, other than the one being edited. */\n const memberOf = useMemo(() => {\n const found = new Map<string, ListedConsentGroup>()\n for (const group of groups) {\n if (group.id === groupId) continue\n for (const hostId of group.hostIds) found.set(hostId, group)\n }\n return found\n }, [groups, groupId])\n\n const rows = useMemo(() => {\n const listed = sites.map((site) => ({\n id: site.id,\n name: site.name || site.subdomain || site.id,\n missing: false,\n }))\n const known = new Set(listed.map((site) => site.id))\n const own = groups.find((group) => group.id === groupId)?.hostIds ?? []\n const orphans = own\n .filter((hostId) => !known.has(hostId))\n .map((hostId) => ({ id: hostId, name: hostId, missing: true }))\n return [...listed, ...orphans].sort((a, b) => a.name.localeCompare(b.name))\n }, [sites, groups, groupId])\n\n const chosen = useMemo(() => new Set(selected), [selected])\n const full = chosen.size >= MAX_CONSENT_GROUP_HOSTS\n const needle = filter.trim().toLocaleLowerCase()\n const visible = needle\n ? rows.filter(\n (row) => chosen.has(row.id) || row.name.toLocaleLowerCase().includes(needle),\n )\n : rows\n\n const toggle = (hostId: string, on: boolean) => {\n const next = new Set(chosen)\n if (on) next.add(hostId)\n else next.delete(hostId)\n onChange([...next].sort())\n }\n\n return (\n <FormControl component=\"fieldset\" error={Boolean(error)} disabled={disabled} fullWidth>\n <FormLabel component=\"legend\">{'Sites'}</FormLabel>\n <FormHelperText sx={{ mx: 0 }}>\n {`Choose at least two sites and at most ${MAX_CONSENT_GROUP_HOSTS}. ` +\n 'A site can be in only one consent group.'}\n </FormHelperText>\n {rows.length > FILTER_FROM ? (\n <TextField\n size=\"small\"\n label=\"Find a site\"\n value={filter}\n onChange={(event) => setFilter(event.target.value)}\n sx={{ mt: 1, maxWidth: 320 }}\n />\n ) : null}\n <Box\n sx={{\n mt: 1,\n maxHeight: 320,\n overflowY: 'auto',\n border: 1,\n borderColor: 'divider',\n borderRadius: 1,\n px: 1.5,\n py: 0.5,\n }}\n >\n {visible.map((row) => {\n const checked = chosen.has(row.id)\n const other = memberOf.get(row.id)\n return (\n <Stack\n key={row.id}\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center', flexWrap: 'wrap' }}\n >\n <FormControlLabel\n control={\n <Checkbox\n size=\"small\"\n checked={checked}\n disabled={disabled || (!checked && full)}\n onChange={(event) => toggle(row.id, event.target.checked)}\n />\n }\n label={row.name}\n sx={{ mr: 0 }}\n />\n {other ? (\n <Chip\n size=\"small\"\n variant=\"outlined\"\n color={checked ? 'warning' : 'default'}\n label={checked ? `Moves from ${other.name}` : `In ${other.name}`}\n />\n ) : null}\n {row.missing ? (\n <Chip\n size=\"small\"\n variant=\"outlined\"\n color=\"warning\"\n label=\"Not in your organization\"\n />\n ) : null}\n {refusedHostIds?.has(row.id) ? (\n <Chip size=\"small\" color=\"error\" variant=\"outlined\" label=\"Can’t be added\" />\n ) : null}\n </Stack>\n )\n })}\n {!visible.length ? (\n <Typography variant=\"body2\" color=\"text.secondary\" sx={{ py: 1 }}>\n {rows.length ? 'No site matches.' : 'Your organization has no sites yet.'}\n </Typography>\n ) : null}\n </Box>\n <FormHelperText sx={{ mx: 0 }}>\n {error ??\n (full\n ? `That’s the most one group can include (${MAX_CONSENT_GROUP_HOSTS}).`\n : `${chosen.size} selected`)}\n </FormHelperText>\n </FormControl>\n )\n}\nConsentGroupSitePicker.displayName = 'ConsentGroupSitePicker'\n\nexport default ConsentGroupSitePicker\n"],"names":["Box","Checkbox","Chip","FormControl","FormControlLabel","FormHelperText","FormLabel","Stack","TextField","Typography","useMemo","useState","MAX_CONSENT_GROUP_HOSTS","FILTER_FROM","ConsentGroupSitePicker","props","sites","groups","groupId","selected","onChange","error","refusedHostIds","disabled","filter","setFilter","memberOf","found","Map","group","id","hostId","hostIds","set","rows","listed","map","site","name","subdomain","missing","known","Set","own","find","orphans","has","sort","a","b","localeCompare","chosen","full","size","needle","trim","toLocaleLowerCase","visible","row","includes","toggle","on","next","add","delete","component","Boolean","fullWidth","sx","mx","length","label","value","event","target","mt","maxWidth","maxHeight","overflowY","border","borderColor","borderRadius","px","py","checked","other","get","direction","spacing","alignItems","flexWrap","control","mr","variant","color","displayName"],"mappings":"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"}
@@ -0,0 +1,158 @@
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 { CONSENT_GROUP_CHANGES_COLLECTION, CONSENT_GROUP_NAME_MAX, CONSENT_GROUP_SWEEP_DELAY_MS, CONSENT_GROUPS_CHANGE_FIELD, type ConsentGroupChangeEstimate, type ConsentGroupChangePreview, type ConsentGroupChangePreviewLine, type ConsentGroupDraft, type ConsentGroupsApiChangeRequest, type ConsentGroupsApiChangeResponse, type ConsentGroupsApiDeclarationRequest, type ConsentGroupsApiPreviewResponse, type ConsentGroupsApiRequest, type ConsentGroupsChangeMarker, type ConsentGroupsChangePhase, type ConsentGroupValidationCode } from '@aglyn/aglyn/app-utils/consent-group-change';
18
+ import { MAX_CONSENT_GROUP_HOSTS } from '@aglyn/aglyn/app-utils/consent-groups';
19
+ import { authorizedFetch } from '@aglyn/shared-util-http/authorized-token';
20
+ /** The route every call below posts to. */
21
+ export declare const CONSENT_GROUPS_ROUTE = "/api/orgs/consent-groups";
22
+ export { CONSENT_GROUP_CHANGES_COLLECTION, CONSENT_GROUP_NAME_MAX, CONSENT_GROUP_SWEEP_DELAY_MS, CONSENT_GROUPS_CHANGE_FIELD, MAX_CONSENT_GROUP_HOSTS, };
23
+ /** The fewest sites a group may name; one site is already its own sender. */
24
+ export declare const CONSENT_GROUP_MIN_HOSTS = 2;
25
+ export type { ConsentGroupChangeEstimate, ConsentGroupChangePreview };
26
+ /** One group of the complete next declaration; a new group has no id. */
27
+ export type ConsentGroupDeclarationEntry = ConsentGroupDraft;
28
+ /**
29
+ * `preview` or `apply` a declaration.
30
+ *
31
+ * `groups` is the WHOLE next declaration, not a patch: a group left out is a
32
+ * group dissolved. `expected` is the raw `org.consentGroups` the editor was
33
+ * opened against, or `null` when the org had none, so a change made by
34
+ * somebody else in the meantime is refused rather than overwritten.
35
+ */
36
+ export type ConsentGroupsDeclareRequest = ConsentGroupsApiDeclarationRequest;
37
+ /** Drive, stop or read one change that is already running. */
38
+ export type ConsentGroupsChangeRequest = ConsentGroupsApiChangeRequest;
39
+ export type ConsentGroupsRequest = ConsentGroupsApiRequest;
40
+ /**
41
+ * Where a running change is.
42
+ *
43
+ * - `carry` — refusals are being copied; the old declaration still holds.
44
+ * - `rehome` — the new declaration is in force; CRM records are moving.
45
+ * - `sweep` — a last pass for anything written while it changed.
46
+ */
47
+ export type ConsentGroupChangePhase = ConsentGroupsChangePhase;
48
+ /** `org.consentGroupsChange`, present exactly while a change runs. */
49
+ export type ConsentGroupChangeMarker = ConsentGroupsChangeMarker;
50
+ /**
51
+ * A plugin's own account of what the change does to its data, in its words.
52
+ * The editor prints these as given; `count` is `null` when it was not
53
+ * counted.
54
+ */
55
+ export type ConsentGroupParticipantLine = ConsentGroupChangePreviewLine;
56
+ export type ConsentGroupsPreviewResponse = ConsentGroupsApiPreviewResponse;
57
+ /** `apply`, `continue`, `cancel` and `status` all answer the change's state. */
58
+ export type ConsentGroupsProgressResponse = ConsentGroupsApiChangeResponse;
59
+ /** One refusal from the route's validation, pointing at what it refused. */
60
+ export interface ConsentGroupValidationIssue {
61
+ code: string;
62
+ /** Index into the `groups` that were posted. */
63
+ groupIndex?: number;
64
+ hostId?: string;
65
+ }
66
+ /** Why a call did not succeed, sorted into what the editor does about it. */
67
+ export type ConsentGroupsFailure = {
68
+ /** 400: the declaration itself; `errors` point at what to fix. */
69
+ kind: 'invalid';
70
+ message: string;
71
+ errors: ConsentGroupValidationIssue[];
72
+ } | {
73
+ /** 409: somebody else changed the declaration first. */
74
+ kind: 'stale';
75
+ message: string;
76
+ /** The raw declaration as it now stands. */
77
+ current: Record<string, unknown> | null;
78
+ } | {
79
+ /** 409: another change is still running, sweep included. */
80
+ kind: 'in-flight';
81
+ message: string;
82
+ changeId: string | null;
83
+ } | {
84
+ /** 409 on `cancel`: the declaration already flipped. */
85
+ kind: 'took-effect';
86
+ message: string;
87
+ } | {
88
+ kind: 'locked';
89
+ message: string;
90
+ } | {
91
+ kind: 'refused';
92
+ status: number;
93
+ message: string;
94
+ };
95
+ /**
96
+ * A call's outcome. Each branch names the other's field as absent, so a
97
+ * caller can read `failure` after testing `ok` — this workspace compiles
98
+ * without `strictNullChecks`, under which a boolean discriminant alone does
99
+ * not narrow.
100
+ */
101
+ export type ConsentGroupsCallResult<T> = {
102
+ ok: true;
103
+ body: T;
104
+ failure?: undefined;
105
+ } | {
106
+ ok: false;
107
+ body?: undefined;
108
+ failure: ConsentGroupsFailure;
109
+ };
110
+ /**
111
+ * Every refusal the route's validation can name, and what to tell the admin.
112
+ *
113
+ * The route is authoritative — the editor checks only what it can check
114
+ * without asking (a name, a site count) and shows the rest when the route
115
+ * answers. The table is keyed by the route's own codes, so a code added there
116
+ * without a sentence here fails the type check; one it still does not know —
117
+ * a server newer than this bundle — gets a general sentence rather than none.
118
+ */
119
+ export declare const CONSENT_GROUP_ISSUE_MESSAGES: Readonly<Record<ConsentGroupValidationCode, string>>;
120
+ /** The sentence for one validation refusal, naming the site when it can. */
121
+ export declare function consentGroupIssueMessage(issue: ConsentGroupValidationIssue, siteName?: (hostId: string) => string): string;
122
+ /** A token source `authorizedFetch` accepts: the signed-in user. */
123
+ type ConsentGroupsCaller = Parameters<typeof authorizedFetch>[0];
124
+ /**
125
+ * Sorts a refused answer into what the editor does next.
126
+ *
127
+ * Exported for the specs, which assert that each status the route documents
128
+ * lands in the branch the dialog handles.
129
+ */
130
+ export declare function classifyConsentGroupsFailure(status: number, payload: unknown, action: ConsentGroupsRequest['action']): ConsentGroupsFailure;
131
+ /**
132
+ * Posts one action and sorts the answer. Never throws: a network failure is
133
+ * a refusal with a sentence, so every caller has exactly two branches.
134
+ */
135
+ export declare function postConsentGroups<T>(user: ConsentGroupsCaller, request: ConsentGroupsRequest): Promise<ConsentGroupsCallResult<T>>;
136
+ /**
137
+ * The org's in-flight change, or `null` when none runs.
138
+ *
139
+ * A marker without a change id is not a change anybody can drive or stop,
140
+ * so it reads as absent; an unknown phase reads as `carry`, the phase in
141
+ * which nothing has taken effect, because that is the claim a banner can
142
+ * make without being wrong.
143
+ */
144
+ export declare function readConsentGroupChangeMarker(org: Record<string, unknown> | null | undefined): ConsentGroupChangeMarker | null;
145
+ /** What the progress banner needs from a change's job document. */
146
+ export interface ConsentGroupChangeJobView {
147
+ /** The job's own phase, when it records one. */
148
+ phase: string | null;
149
+ /** When another runner's claim on the job lapses; `null` when unclaimed. */
150
+ leaseUntilMs: number | null;
151
+ stalled: boolean;
152
+ lastError: string | null;
153
+ failures: number;
154
+ /** The change finished, or was stopped before it took effect. */
155
+ finished: boolean;
156
+ }
157
+ /** Reads a job document defensively. `undefined` reads as a fresh job. */
158
+ export declare function readConsentGroupChangeJob(data: Record<string, unknown> | null | undefined): ConsentGroupChangeJobView;
@@ -0,0 +1,250 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ /**
18
+ * The console's client of `POST /api/orgs/consent-groups` — the one door
19
+ * through which a consent group is declared, edited or dissolved (AGL-3320).
20
+ *
21
+ * ## Why the editor never writes Firestore
22
+ *
23
+ * `consentGroups` decides whose opt-outs hold a send and who a grant covers,
24
+ * so the rules deny it to every client. A change is also not one write: the
25
+ * route carries every refusal the change would otherwise lose onto the sites
26
+ * that stop reading each other, flips the declaration, and then re-homes the
27
+ * CRM records keyed by the old group — a job of several invocations with its
28
+ * own lock. The console only asks: `preview` what a change would do, `apply`
29
+ * it, and drive it with `continue` while somebody is watching. Everything
30
+ * else is the server's.
31
+ *
32
+ * ## The wire shapes are the route's own
33
+ *
34
+ * Every request and response type below is the planner module's
35
+ * (`@aglyn/aglyn/app-utils/consent-group-change`), imported as a type and
36
+ * erased from the bundle, so a field the route renames fails this plugin's
37
+ * type check instead of rendering a sentence with a hole in it. The constants
38
+ * come from the same module; it is pure, with no read and no server import.
39
+ *
40
+ * Nothing the server says is trusted to be well formed all the same: every
41
+ * reader below accepts a missing or mistyped field and answers the narrow
42
+ * value, because a progress banner that throws on a field it did not expect
43
+ * would hide the one change an org cannot afford to lose track of.
44
+ */ import { lockdownRefusalText, parseLockdownRefusal } from "@aglyn/aglyn";
45
+ import { CONSENT_GROUP_CHANGES_COLLECTION, CONSENT_GROUP_NAME_MAX, CONSENT_GROUP_SWEEP_DELAY_MS, CONSENT_GROUPS_CHANGE_FIELD } from "@aglyn/aglyn/app-utils/consent-group-change";
46
+ import { MAX_CONSENT_GROUP_HOSTS } from "@aglyn/aglyn/app-utils/consent-groups";
47
+ import { authorizedFetch } from "@aglyn/shared-util-http/authorized-token";
48
+ /** The route every call below posts to. */ export const CONSENT_GROUPS_ROUTE = '/api/orgs/consent-groups';
49
+ export { CONSENT_GROUP_CHANGES_COLLECTION, CONSENT_GROUP_NAME_MAX, CONSENT_GROUP_SWEEP_DELAY_MS, CONSENT_GROUPS_CHANGE_FIELD, MAX_CONSENT_GROUP_HOSTS, };
50
+ /** The fewest sites a group may name; one site is already its own sender. */ export const CONSENT_GROUP_MIN_HOSTS = 2;
51
+ /*==========================================
52
+ * VALIDATION CODES
53
+ *=========================================*/ /**
54
+ * Every refusal the route's validation can name, and what to tell the admin.
55
+ *
56
+ * The route is authoritative — the editor checks only what it can check
57
+ * without asking (a name, a site count) and shows the rest when the route
58
+ * answers. The table is keyed by the route's own codes, so a code added there
59
+ * without a sentence here fails the type check; one it still does not know —
60
+ * a server newer than this bundle — gets a general sentence rather than none.
61
+ */ export const CONSENT_GROUP_ISSUE_MESSAGES = {
62
+ malformed: 'This change couldn’t be read. Reload the page and try again.',
63
+ 'name-empty': 'Give the group a name. Signup forms show it, so it can’t be blank.',
64
+ 'name-too-long': `Keep the name to ${CONSENT_GROUP_NAME_MAX} characters or fewer.`,
65
+ 'name-duplicate': 'Another consent group already has this name.',
66
+ 'too-few-sites': 'A consent group needs at least two sites.',
67
+ 'too-many-sites': `A consent group can include at most ${MAX_CONSENT_GROUP_HOSTS} sites.`,
68
+ 'unknown-site': 'One of these sites is no longer part of your organization.',
69
+ 'site-in-two-groups': 'A site can be in only one consent group.',
70
+ 'unknown-group': 'This consent group no longer exists. Close this and start again.',
71
+ 'group-replaced': 'Every site in this group would be replaced. Create a new group for these sites instead.',
72
+ 'duplicate-group': 'The same consent group appears twice. Reload the page and try again.',
73
+ 'no-change': 'Nothing has changed yet.'
74
+ };
75
+ /** The sentence for one validation refusal, naming the site when it can. */ export function consentGroupIssueMessage(issue, siteName) {
76
+ var _CONSENT_GROUP_ISSUE_MESSAGES_code;
77
+ const code = issue.code;
78
+ const site = issue.hostId && siteName ? siteName(issue.hostId) : '';
79
+ if (site && code === 'unknown-site') {
80
+ return `${site} is no longer part of your organization.`;
81
+ }
82
+ if (site && code === 'site-in-two-groups') {
83
+ return `${site} can be in only one consent group.`;
84
+ }
85
+ return (_CONSENT_GROUP_ISSUE_MESSAGES_code = CONSENT_GROUP_ISSUE_MESSAGES[code]) != null ? _CONSENT_GROUP_ISSUE_MESSAGES_code : 'This change can’t be made as it stands. Check the name and sites and try again.';
86
+ }
87
+ const STALE_MESSAGE = 'Someone else changed consent groups while you were editing.';
88
+ const IN_FLIGHT_MESSAGE = 'Finishing the last change. You can make another once it’s done.';
89
+ const TOOK_EFFECT_MESSAGE = 'This change already took effect, so it can’t be stopped now. It will finish on its own.';
90
+ const asRecord = (value)=>value && typeof value === 'object' && !Array.isArray(value) ? value : {};
91
+ /**
92
+ * Sorts a refused answer into what the editor does next.
93
+ *
94
+ * Exported for the specs, which assert that each status the route documents
95
+ * lands in the branch the dialog handles.
96
+ */ export function classifyConsentGroupsFailure(status, payload, action) {
97
+ const body = asRecord(payload);
98
+ const said = typeof body['error'] === 'string' ? body['error'] : '';
99
+ if (status === 400) {
100
+ const errors = Array.isArray(body['errors']) ? body['errors'].map(asRecord).filter((issue)=>typeof issue['code'] === 'string').map((issue)=>_extends({
101
+ code: String(issue['code'])
102
+ }, typeof issue['groupIndex'] === 'number' ? {
103
+ groupIndex: issue['groupIndex']
104
+ } : {}, typeof issue['hostId'] === 'string' ? {
105
+ hostId: issue['hostId']
106
+ } : {})) : [];
107
+ return {
108
+ kind: 'invalid',
109
+ message: errors.length ? consentGroupIssueMessage(errors[0]) : said || 'This change can’t be made as it stands.',
110
+ errors
111
+ };
112
+ }
113
+ if (status === 409) {
114
+ if (action === 'cancel') {
115
+ return {
116
+ kind: 'took-effect',
117
+ message: TOOK_EFFECT_MESSAGE
118
+ };
119
+ }
120
+ if ('current' in body) {
121
+ const current = body['current'];
122
+ return {
123
+ kind: 'stale',
124
+ message: STALE_MESSAGE,
125
+ current: current && typeof current === 'object' && !Array.isArray(current) ? current : null
126
+ };
127
+ }
128
+ return {
129
+ kind: 'in-flight',
130
+ message: IN_FLIGHT_MESSAGE,
131
+ changeId: typeof body['changeId'] === 'string' ? body['changeId'] : null
132
+ };
133
+ }
134
+ if (status === 423) {
135
+ const locked = parseLockdownRefusal(status, payload);
136
+ return {
137
+ kind: 'locked',
138
+ message: locked ? lockdownRefusalText(locked) : 'Changes are paused right now. Try again later.'
139
+ };
140
+ }
141
+ if (status === 401) {
142
+ return {
143
+ kind: 'refused',
144
+ status,
145
+ message: 'Your session has ended. Sign in again to change consent groups.'
146
+ };
147
+ }
148
+ if (status === 403) {
149
+ return {
150
+ kind: 'refused',
151
+ status,
152
+ message: body['reason'] === 'email-unverified' ? 'Verify your email address to change consent groups.' : said || 'You don’t have permission to change consent groups.'
153
+ };
154
+ }
155
+ if (status === 404) {
156
+ return {
157
+ kind: 'refused',
158
+ status,
159
+ message: said || 'This organization could not be found.'
160
+ };
161
+ }
162
+ return {
163
+ kind: 'refused',
164
+ status,
165
+ message: said || 'Something went wrong. Try again in a moment.'
166
+ };
167
+ }
168
+ /**
169
+ * Posts one action and sorts the answer. Never throws: a network failure is
170
+ * a refusal with a sentence, so every caller has exactly two branches.
171
+ */ export async function postConsentGroups(user, request) {
172
+ let response;
173
+ try {
174
+ response = await authorizedFetch(user, CONSENT_GROUPS_ROUTE, {
175
+ method: 'POST',
176
+ headers: {
177
+ 'Content-Type': 'application/json'
178
+ },
179
+ body: JSON.stringify(request)
180
+ });
181
+ } catch (unused) {
182
+ return {
183
+ ok: false,
184
+ failure: {
185
+ kind: 'refused',
186
+ status: 0,
187
+ message: 'The server couldn’t be reached. Check your connection and try again.'
188
+ }
189
+ };
190
+ }
191
+ const payload = await response.json().catch(()=>null);
192
+ if (response.ok && asRecord(payload)['ok'] === true) {
193
+ return {
194
+ ok: true,
195
+ body: payload
196
+ };
197
+ }
198
+ return {
199
+ ok: false,
200
+ failure: classifyConsentGroupsFailure(response.ok ? 500 : response.status, payload, request.action)
201
+ };
202
+ }
203
+ /*==========================================
204
+ * READING WHAT THE ORG DOCUMENT AND THE JOB SAY
205
+ *=========================================*/ const PHASES = [
206
+ 'carry',
207
+ 'rehome',
208
+ 'sweep'
209
+ ];
210
+ const finiteNumber = (value)=>typeof value === 'number' && Number.isFinite(value) ? value : null;
211
+ /**
212
+ * The org's in-flight change, or `null` when none runs.
213
+ *
214
+ * A marker without a change id is not a change anybody can drive or stop,
215
+ * so it reads as absent; an unknown phase reads as `carry`, the phase in
216
+ * which nothing has taken effect, because that is the claim a banner can
217
+ * make without being wrong.
218
+ */ export function readConsentGroupChangeMarker(org) {
219
+ var _finiteNumber;
220
+ const raw = asRecord(asRecord(org)[CONSENT_GROUPS_CHANGE_FIELD]);
221
+ const changeId = typeof raw['changeId'] === 'string' ? raw['changeId'] : '';
222
+ if (!changeId) return null;
223
+ const phase = PHASES.includes(raw['phase']) ? raw['phase'] : 'carry';
224
+ const declaredAtMs = finiteNumber(raw['declaredAtMs']);
225
+ return _extends({
226
+ changeId,
227
+ phase,
228
+ hostIds: Array.isArray(raw['hostIds']) ? raw['hostIds'].filter((id)=>typeof id === 'string' && !!id) : [],
229
+ startedAtMs: (_finiteNumber = finiteNumber(raw['startedAtMs'])) != null ? _finiteNumber : 0
230
+ }, declaredAtMs != null ? {
231
+ declaredAtMs
232
+ } : {});
233
+ }
234
+ /** Reads a job document defensively. `undefined` reads as a fresh job. */ export function readConsentGroupChangeJob(data) {
235
+ var _finiteNumber;
236
+ const job = asRecord(data);
237
+ const lease = asRecord(job['lease']);
238
+ const lastError = job['lastError'];
239
+ const phase = typeof job['phase'] === 'string' ? job['phase'] : null;
240
+ return {
241
+ phase,
242
+ leaseUntilMs: finiteNumber(lease['untilMs']),
243
+ stalled: job['stalled'] === true,
244
+ lastError: typeof lastError === 'string' ? lastError : typeof asRecord(lastError)['message'] === 'string' ? String(asRecord(lastError)['message']) : null,
245
+ failures: (_finiteNumber = finiteNumber(job['failures'])) != null ? _finiteNumber : 0,
246
+ finished: job['done'] === true || finiteNumber(job['finishedAtMs']) != null || finiteNumber(job['canceledAtMs']) != null || phase === 'done' || phase === 'canceled'
247
+ };
248
+ }
249
+
250
+ //# sourceMappingURL=consent-groups-api.js.map