@aglyn/aglyn 1.0.0-beta.219 → 1.0.0-beta.220
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 +12 -12
- package/src/lib/app-utils/actions.d.ts +1 -59
- package/src/lib/app-utils/actions.js +7 -72
- package/src/lib/app-utils/actions.js.map +1 -1
- package/src/lib/app-utils/consent-groups.d.ts +1 -1
- package/src/lib/app-utils/consent-groups.js.map +1 -1
- package/src/lib/app-utils/console-routes.d.ts +0 -36
- package/src/lib/app-utils/console-routes.js +2 -37
- package/src/lib/app-utils/console-routes.js.map +1 -1
- package/src/lib/app-utils/contacts.d.ts +1 -1
- package/src/lib/app-utils/contacts.js.map +1 -1
- package/src/lib/app-utils/container-membership.d.ts +93 -0
- package/src/lib/app-utils/container-membership.js +165 -0
- package/src/lib/app-utils/container-membership.js.map +1 -0
- package/src/lib/app-utils/crm-task-reminders.d.ts +23 -6
- package/src/lib/app-utils/crm-task-reminders.js +32 -11
- package/src/lib/app-utils/crm-task-reminders.js.map +1 -1
- package/src/lib/app-utils/crm.d.ts +24 -4
- package/src/lib/app-utils/crm.js +23 -0
- package/src/lib/app-utils/crm.js.map +1 -1
- package/src/lib/app-utils/csv-upload.d.ts +1 -1
- package/src/lib/app-utils/csv-upload.js +1 -1
- package/src/lib/app-utils/csv-upload.js.map +1 -1
- package/src/lib/app-utils/{dataset-csv.d.ts → csv.d.ts} +10 -43
- package/src/lib/app-utils/{dataset-csv.js → csv.js} +10 -81
- package/src/lib/app-utils/csv.js.map +1 -0
- package/src/lib/app-utils/datasets.d.ts +0 -19
- package/src/lib/app-utils/datasets.js +0 -13
- package/src/lib/app-utils/datasets.js.map +1 -1
- package/src/lib/app-utils/docs-help.generated.d.ts +2 -2
- package/src/lib/app-utils/docs-help.generated.js +2 -0
- package/src/lib/app-utils/docs-help.generated.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.js +63 -35
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/dynamic-list-rule.js +3 -3
- package/src/lib/app-utils/dynamic-list-rule.js.map +1 -1
- package/src/lib/app-utils/email-text.d.ts +80 -0
- package/src/lib/app-utils/email-text.js +189 -0
- package/src/lib/app-utils/email-text.js.map +1 -0
- package/src/lib/app-utils/{list-assignment-policy.d.ts → enrollment-basis.d.ts} +4 -0
- package/src/lib/app-utils/{list-assignment-policy.js → enrollment-basis.js} +5 -1
- package/src/lib/app-utils/enrollment-basis.js.map +1 -0
- package/src/lib/app-utils/expand-repeatables.js +4 -4
- package/src/lib/app-utils/expand-repeatables.js.map +1 -1
- package/src/lib/app-utils/forms.js +2 -2
- package/src/lib/app-utils/forms.js.map +1 -1
- package/src/lib/app-utils/health-report.d.ts +2 -3
- package/src/lib/app-utils/health-report.js +8 -6
- package/src/lib/app-utils/health-report.js.map +1 -1
- package/src/lib/app-utils/inbound-mail-domain.d.ts +27 -0
- package/src/lib/app-utils/inbound-mail-domain.js +31 -0
- package/src/lib/app-utils/inbound-mail-domain.js.map +1 -0
- package/src/lib/app-utils/lockdown.d.ts +11 -0
- package/src/lib/app-utils/lockdown.js +33 -3
- package/src/lib/app-utils/lockdown.js.map +1 -1
- package/src/lib/app-utils/member-email-aliases.d.ts +25 -0
- package/src/lib/app-utils/member-email-aliases.js +29 -0
- package/src/lib/app-utils/member-email-aliases.js.map +1 -1
- package/src/lib/app-utils/notifications.d.ts +69 -0
- package/src/lib/app-utils/notifications.js +119 -0
- package/src/lib/app-utils/notifications.js.map +1 -1
- package/src/lib/app-utils/operator-alerts.d.ts +15 -1
- package/src/lib/app-utils/operator-alerts.js +47 -10
- package/src/lib/app-utils/operator-alerts.js.map +1 -1
- package/src/lib/app-utils/plan-entitlements.d.ts +164 -178
- package/src/lib/app-utils/plan-entitlements.js +200 -224
- package/src/lib/app-utils/plan-entitlements.js.map +1 -1
- package/src/lib/app-utils/platform-marketing-consent.d.ts +56 -2
- package/src/lib/app-utils/platform-marketing-consent.js +6 -5
- package/src/lib/app-utils/platform-marketing-consent.js.map +1 -1
- package/src/lib/app-utils/plugin-starter-templates.generated.d.ts +12 -0
- package/src/lib/app-utils/plugin-starter-templates.generated.js +711 -0
- package/src/lib/app-utils/plugin-starter-templates.generated.js.map +1 -0
- package/src/lib/app-utils/repeat-query.d.ts +56 -0
- package/src/lib/app-utils/{dataset-query.js → repeat-query.js} +16 -16
- package/src/lib/app-utils/repeat-query.js.map +1 -0
- package/src/lib/app-utils/server.d.ts +7 -9
- package/src/lib/app-utils/server.js +18 -26
- package/src/lib/app-utils/server.js.map +1 -1
- package/src/lib/app-utils/starter-template-nodes.d.ts +137 -0
- package/src/lib/app-utils/starter-template-nodes.js +129 -0
- package/src/lib/app-utils/starter-template-nodes.js.map +1 -0
- package/src/lib/app-utils/starter-templates.d.ts +11 -34
- package/src/lib/app-utils/starter-templates.js +19 -230
- package/src/lib/app-utils/starter-templates.js.map +1 -1
- package/src/lib/app-utils/{email-topics.d.ts → subscription-topics.d.ts} +64 -48
- package/src/lib/app-utils/{email-topics.js → subscription-topics.js} +39 -109
- package/src/lib/app-utils/subscription-topics.js.map +1 -0
- package/src/lib/app-utils/usage-budget.d.ts +73 -188
- package/src/lib/app-utils/usage-budget.js +58 -194
- package/src/lib/app-utils/usage-budget.js.map +1 -1
- package/src/lib/app-utils/utm-attribution.d.ts +1 -1
- package/src/lib/app-utils/utm-attribution.js +1 -1
- package/src/lib/app-utils/utm-attribution.js.map +1 -1
- package/src/lib/app-utils/{campaign-touch.d.ts → utm-touch.d.ts} +21 -13
- package/src/lib/app-utils/{campaign-touch.js → utm-touch.js} +30 -22
- package/src/lib/app-utils/utm-touch.js.map +1 -0
- package/src/lib/app-utils/variables.d.ts +9 -9
- package/src/lib/app-utils/variables.js.map +1 -1
- package/src/lib/app-utils/visitor-record-ceiling.d.ts +32 -0
- package/src/lib/app-utils/visitor-record-ceiling.js +39 -0
- package/src/lib/app-utils/visitor-record-ceiling.js.map +1 -1
- package/src/lib/foundation/definitions/org-billing.types.d.ts +2 -2
- package/src/lib/foundation/definitions/org-billing.types.js.map +1 -1
- package/src/lib/plugin-manager/feature-plugins.d.ts +80 -79
- package/src/lib/plugin-manager/feature-plugins.js +22 -35
- package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
- package/src/lib/plugin-manager/first-party-plugins.generated.d.ts +12 -1
- package/src/lib/plugin-manager/first-party-plugins.generated.js +58 -2
- package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
- package/src/lib/plugin-manager/index.d.ts +0 -1
- package/src/lib/plugin-manager/index.js +0 -1
- package/src/lib/plugin-manager/index.js.map +1 -1
- package/src/lib/plugin-manager/plugin-contact-capture.d.ts +9 -4
- package/src/lib/plugin-manager/plugin-contact-capture.js.map +1 -1
- package/src/lib/plugin-manager/plugin-containers.d.ts +68 -0
- package/src/lib/plugin-manager/plugin-containers.generated.d.ts +10 -0
- package/src/lib/plugin-manager/plugin-containers.generated.js +20 -0
- package/src/lib/plugin-manager/plugin-containers.generated.js.map +1 -0
- package/src/lib/plugin-manager/plugin-containers.js +27 -0
- package/src/lib/plugin-manager/plugin-containers.js.map +1 -0
- package/src/lib/plugin-manager/plugin-entitlements.d.ts +8 -0
- package/src/lib/plugin-manager/plugin-entitlements.js.map +1 -1
- package/src/lib/plugin-manager/plugin-membership-detach.js +1 -1
- package/src/lib/plugin-manager/plugin-membership-detach.js.map +1 -1
- package/src/lib/plugin-manager/plugin-record-email-state.d.ts +69 -0
- package/src/lib/plugin-manager/plugin-record-email-state.js +32 -0
- package/src/lib/plugin-manager/plugin-record-email-state.js.map +1 -1
- package/src/lib/plugin-manager/plugin-record-lists.d.ts +115 -0
- package/src/lib/plugin-manager/plugin-record-lists.js +75 -0
- package/src/lib/plugin-manager/plugin-record-lists.js.map +1 -0
- package/src/lib/plugin-manager/plugin-record-timeline.d.ts +42 -8
- package/src/lib/plugin-manager/plugin-record-timeline.js.map +1 -1
- package/src/lib/plugin-manager/plugin-site-beacons.d.ts +71 -0
- package/src/lib/plugin-manager/plugin-site-beacons.js +54 -0
- package/src/lib/plugin-manager/plugin-site-beacons.js.map +1 -0
- package/src/lib/plugin-manager/plugin-subscription-topics.generated.d.ts +11 -0
- package/src/lib/plugin-manager/plugin-subscription-topics.generated.js +41 -0
- package/src/lib/plugin-manager/plugin-subscription-topics.generated.js.map +1 -0
- package/src/lib/plugin-manager/plugin-usage-axes.d.ts +212 -0
- package/src/lib/plugin-manager/plugin-usage-axes.js +82 -3
- package/src/lib/plugin-manager/plugin-usage-axes.js.map +1 -1
- package/src/lib/plugin-manager/plugin-usage-meters.d.ts +108 -0
- package/src/lib/plugin-manager/plugin-usage-meters.js +105 -0
- package/src/lib/plugin-manager/plugin-usage-meters.js.map +1 -0
- package/src/lib/plugin-manager/usage-alert-contributors.d.ts +76 -6
- package/src/lib/plugin-manager/usage-alert-contributors.js +23 -10
- package/src/lib/plugin-manager/usage-alert-contributors.js.map +1 -1
- package/src/lib/app-utils/assist-credits.d.ts +0 -482
- package/src/lib/app-utils/assist-credits.js +0 -510
- package/src/lib/app-utils/assist-credits.js.map +0 -1
- package/src/lib/app-utils/campaign-membership.d.ts +0 -121
- package/src/lib/app-utils/campaign-membership.js +0 -189
- package/src/lib/app-utils/campaign-membership.js.map +0 -1
- package/src/lib/app-utils/campaign-touch.js.map +0 -1
- package/src/lib/app-utils/console-record-links.d.ts +0 -84
- package/src/lib/app-utils/console-record-links.js +0 -76
- package/src/lib/app-utils/console-record-links.js.map +0 -1
- package/src/lib/app-utils/crm-booking.d.ts +0 -75
- package/src/lib/app-utils/crm-booking.js +0 -133
- package/src/lib/app-utils/crm-booking.js.map +0 -1
- package/src/lib/app-utils/crm-inbound.d.ts +0 -245
- package/src/lib/app-utils/crm-inbound.js +0 -407
- package/src/lib/app-utils/crm-inbound.js.map +0 -1
- package/src/lib/app-utils/crm-reports.d.ts +0 -479
- package/src/lib/app-utils/crm-reports.js +0 -680
- package/src/lib/app-utils/crm-reports.js.map +0 -1
- package/src/lib/app-utils/dataset-csv.js.map +0 -1
- package/src/lib/app-utils/dataset-query.d.ts +0 -57
- package/src/lib/app-utils/dataset-query.js.map +0 -1
- package/src/lib/app-utils/email-topics.js.map +0 -1
- package/src/lib/app-utils/list-assignment-policy.js.map +0 -1
- package/src/lib/plugin-manager/plugin-metered-lines.d.ts +0 -97
- package/src/lib/plugin-manager/plugin-metered-lines.js +0 -123
- package/src/lib/plugin-manager/plugin-metered-lines.js.map +0 -1
|
@@ -14,11 +14,65 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
|
+
/**
|
|
18
|
+
* Marketing consent for the PLATFORM's own product email (AGL-3185).
|
|
19
|
+
*
|
|
20
|
+
* The strict policy (`marketing-consent.ts`) means a person with no declared
|
|
21
|
+
* basis is unsendable, and until this module the only doors that declared
|
|
22
|
+
* one were the marketing site's forms. Somebody who created a console account
|
|
23
|
+
* — the warmest audience the platform has — was never asked. Three console
|
|
24
|
+
* doors ask now: a checkbox on the sign-up form, a switch in the account
|
|
25
|
+
* preferences, and a one-time prompt for accounts that predate the checkbox.
|
|
26
|
+
*
|
|
27
|
+
* ## One wording, versioned
|
|
28
|
+
*
|
|
29
|
+
* Every door shows {@link PLATFORM_MARKETING_CONSENT_TEXT} and every record
|
|
30
|
+
* stores {@link PLATFORM_MARKETING_CONSENT_TEXT_VERSION} beside the exact
|
|
31
|
+
* timestamp, so "what did this person agree to" is answered by a version and
|
|
32
|
+
* not by whatever the checkbox says today. Changing the sentence is a bump of
|
|
33
|
+
* the version in the same edit.
|
|
34
|
+
*
|
|
35
|
+
* ## The record lives in two places, and the two do not disagree
|
|
36
|
+
*
|
|
37
|
+
* The decision is written by the server onto the person's own `users/{uid}`
|
|
38
|
+
* document, so the console can show a preference without reading the
|
|
39
|
+
* operator's CRM, and onto the operator's marketing contact for the same
|
|
40
|
+
* person, so the send path sees a declared basis with provenance. The
|
|
41
|
+
* contact half is written only when the deployment names a marketing host
|
|
42
|
+
* (`PLATFORM_MARKETING_HOST_ID`, read in `@aglyn/tenant-data-admin`); a
|
|
43
|
+
* self-hosted install with no audience of its own records the preference
|
|
44
|
+
* and skips the contact — and keeps asking, so the answers are already on
|
|
45
|
+
* file the day the operator names a marketing host.
|
|
46
|
+
*
|
|
47
|
+
* ## An unsubscribe reaches the account, for its own list only (AGL-3305)
|
|
48
|
+
*
|
|
49
|
+
* The marketing site's emails carry their own way out, and a person who uses
|
|
50
|
+
* it has answered the console's question too — but only if what they left
|
|
51
|
+
* is the list the question was about. Leaving
|
|
52
|
+
* {@link PLATFORM_MARKETING_TOPIC_ID}, or everything the site sends, turns
|
|
53
|
+
* the account's answer to No through an email door; leaving the newsletter
|
|
54
|
+
* leaves it alone. The email doors write the account and nothing else: the
|
|
55
|
+
* site's lists already say what was left, and declining the contact's basis
|
|
56
|
+
* would stop every list, not the one the person chose.
|
|
57
|
+
*
|
|
58
|
+
* ## Absence is not refusal
|
|
59
|
+
*
|
|
60
|
+
* An unticked sign-up checkbox records nothing. A switch turned off and a
|
|
61
|
+
* prompt answered "no" record a refusal, which the strict policy already
|
|
62
|
+
* honors. A prompt dismissed without an answer records only the dismissal,
|
|
63
|
+
* and the prompt stays away for {@link PLATFORM_MARKETING_PROMPT_SNOOZE_MS}.
|
|
64
|
+
*
|
|
65
|
+
* Pure and dependency-light on purpose: the sign-up page renders the text,
|
|
66
|
+
* the console route validates the body, and the server library writes the
|
|
67
|
+
* record, all from this one module.
|
|
68
|
+
*/
|
|
17
69
|
import { type MarketingConsentSource } from './marketing-consent';
|
|
18
70
|
/**
|
|
19
71
|
* The list the account's answer stands for on the marketing site (AGL-3305):
|
|
20
|
-
* the built-in "Product updates"
|
|
21
|
-
* announcements are sent on.
|
|
72
|
+
* the built-in "Product updates" stream, which is what the platform's own
|
|
73
|
+
* announcements are sent on. A wire value like every topic id — it keys the
|
|
74
|
+
* opt-outs already recorded on the marketing host — and the same stream the
|
|
75
|
+
* built-in catalog offers every site as "Product updates".
|
|
22
76
|
*
|
|
23
77
|
* Named so that "which unsubscribe is about this answer" has one spelling.
|
|
24
78
|
* Leaving this list, or leaving everything the site sends, is a No to the
|
|
@@ -64,20 +64,21 @@
|
|
|
64
64
|
* Pure and dependency-light on purpose: the sign-up page renders the text,
|
|
65
65
|
* the console route validates the body, and the server library writes the
|
|
66
66
|
* record, all from this one module.
|
|
67
|
-
*/ import {
|
|
68
|
-
import { MARKETING_CONSENT_FIELD, MARKETING_CONSENT_SOURCE_FIELD } from "./marketing-consent.js";
|
|
67
|
+
*/ import { MARKETING_CONSENT_FIELD, MARKETING_CONSENT_SOURCE_FIELD } from "./marketing-consent.js";
|
|
69
68
|
import { PLATFORM_BRAND_NAME } from "./platform-brand.js";
|
|
70
69
|
/**
|
|
71
70
|
* The list the account's answer stands for on the marketing site (AGL-3305):
|
|
72
|
-
* the built-in "Product updates"
|
|
73
|
-
* announcements are sent on.
|
|
71
|
+
* the built-in "Product updates" stream, which is what the platform's own
|
|
72
|
+
* announcements are sent on. A wire value like every topic id — it keys the
|
|
73
|
+
* opt-outs already recorded on the marketing host — and the same stream the
|
|
74
|
+
* built-in catalog offers every site as "Product updates".
|
|
74
75
|
*
|
|
75
76
|
* Named so that "which unsubscribe is about this answer" has one spelling.
|
|
76
77
|
* Leaving this list, or leaving everything the site sends, is a No to the
|
|
77
78
|
* question the console asked. Leaving any other list is not, and must not
|
|
78
79
|
* touch the answer: someone who stops the newsletter has said nothing about
|
|
79
80
|
* product updates.
|
|
80
|
-
*/ export const PLATFORM_MARKETING_TOPIC_ID =
|
|
81
|
+
*/ export const PLATFORM_MARKETING_TOPIC_ID = 'product-updates';
|
|
81
82
|
/** The console doors a decision can come through, as the record names them. */ export const PLATFORM_MARKETING_CONSOLE_SOURCE_KINDS = [
|
|
82
83
|
'console-signup',
|
|
83
84
|
'console-preferences',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/platform-marketing-consent.ts"],"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\n/**\n * Marketing consent for the PLATFORM's own product email (AGL-3185).\n *\n * The strict policy (`marketing-consent.ts`) means a person with no declared\n * basis is unsendable, and until this module the only doors that declared\n * one were the marketing site's forms. Somebody who created a console account\n * — the warmest audience the platform has — was never asked. Three console\n * doors ask now: a checkbox on the sign-up form, a switch in the account\n * preferences, and a one-time prompt for accounts that predate the checkbox.\n *\n * ## One wording, versioned\n *\n * Every door shows {@link PLATFORM_MARKETING_CONSENT_TEXT} and every record\n * stores {@link PLATFORM_MARKETING_CONSENT_TEXT_VERSION} beside the exact\n * timestamp, so \"what did this person agree to\" is answered by a version and\n * not by whatever the checkbox says today. Changing the sentence is a bump of\n * the version in the same edit.\n *\n * ## The record lives in two places, and the two do not disagree\n *\n * The decision is written by the server onto the person's own `users/{uid}`\n * document, so the console can show a preference without reading the\n * operator's CRM, and onto the operator's marketing contact for the same\n * person, so the send path sees a declared basis with provenance. The\n * contact half is written only when the deployment names a marketing host\n * (`PLATFORM_MARKETING_HOST_ID`, read in `@aglyn/tenant-data-admin`); a\n * self-hosted install with no audience of its own records the preference\n * and skips the contact — and keeps asking, so the answers are already on\n * file the day the operator names a marketing host.\n *\n * ## An unsubscribe reaches the account, for its own list only (AGL-3305)\n *\n * The marketing site's emails carry their own way out, and a person who uses\n * it has answered the console's question too — but only if what they left\n * is the list the question was about. Leaving\n * {@link PLATFORM_MARKETING_TOPIC_ID}, or everything the site sends, turns\n * the account's answer to No through an email door; leaving the newsletter\n * leaves it alone. The email doors write the account and nothing else: the\n * site's lists already say what was left, and declining the contact's basis\n * would stop every list, not the one the person chose.\n *\n * ## Absence is not refusal\n *\n * An unticked sign-up checkbox records nothing. A switch turned off and a\n * prompt answered \"no\" record a refusal, which the strict policy already\n * honors. A prompt dismissed without an answer records only the dismissal,\n * and the prompt stays away for {@link PLATFORM_MARKETING_PROMPT_SNOOZE_MS}.\n *\n * Pure and dependency-light on purpose: the sign-up page renders the text,\n * the console route validates the body, and the server library writes the\n * record, all from this one module.\n */\n\nimport { EMAIL_TOPIC_PRODUCT_UPDATES } from './email-topics'\nimport {\n MARKETING_CONSENT_FIELD,\n MARKETING_CONSENT_SOURCE_FIELD,\n type MarketingConsentSource,\n} from './marketing-consent'\nimport { PLATFORM_BRAND_NAME } from './platform-brand'\n\n/**\n * The list the account's answer stands for on the marketing site (AGL-3305):\n * the built-in \"Product updates\" topic, which is what the platform's own\n * announcements are sent on.\n *\n * Named so that \"which unsubscribe is about this answer\" has one spelling.\n * Leaving this list, or leaving everything the site sends, is a No to the\n * question the console asked. Leaving any other list is not, and must not\n * touch the answer: someone who stops the newsletter has said nothing about\n * product updates.\n */\nexport const PLATFORM_MARKETING_TOPIC_ID = EMAIL_TOPIC_PRODUCT_UPDATES\n\n/** The console doors a decision can come through, as the record names them. */\nexport const PLATFORM_MARKETING_CONSOLE_SOURCE_KINDS = [\n 'console-signup',\n 'console-preferences',\n 'console-prompt',\n] as const\n\nexport type PlatformMarketingConsoleSourceKind =\n (typeof PLATFORM_MARKETING_CONSOLE_SOURCE_KINDS)[number]\n\n/**\n * The email doors (AGL-3305): the marketing site's own unsubscribe,\n * preference and resubscribe pages, when what the person changed there was\n * {@link PLATFORM_MARKETING_TOPIC_ID} or everything.\n *\n * Kept apart from the console doors because nothing the console sends may\n * claim one. The server records these from the signed link a recipient\n * followed; a console request naming one is refused, because a click in the\n * console is not a click in a mailbox.\n */\nexport const PLATFORM_MARKETING_EMAIL_SOURCE_KINDS = [\n 'email-unsubscribe',\n 'email-preferences',\n 'email-resubscribe',\n] as const\n\nexport type PlatformMarketingEmailSourceKind =\n (typeof PLATFORM_MARKETING_EMAIL_SOURCE_KINDS)[number]\n\n/** Every door a stored decision can name. */\nexport const PLATFORM_MARKETING_CONSENT_SOURCE_KINDS = [\n ...PLATFORM_MARKETING_CONSOLE_SOURCE_KINDS,\n ...PLATFORM_MARKETING_EMAIL_SOURCE_KINDS,\n] as const\n\nexport type PlatformMarketingConsentSourceKind =\n (typeof PLATFORM_MARKETING_CONSENT_SOURCE_KINDS)[number]\n\n/** Any door, for READING a stored decision. */\nexport function isPlatformMarketingConsentSourceKind(\n value: unknown,\n): value is PlatformMarketingConsentSourceKind {\n return (PLATFORM_MARKETING_CONSENT_SOURCE_KINDS as readonly unknown[]).includes(\n value,\n )\n}\n\n/** A console door: the only kind a console request may record. */\nexport function isPlatformMarketingConsoleSourceKind(\n value: unknown,\n): value is PlatformMarketingConsoleSourceKind {\n return (PLATFORM_MARKETING_CONSOLE_SOURCE_KINDS as readonly unknown[]).includes(\n value,\n )\n}\n\n/** An email door: a decision made from a link in one of the site's emails. */\nexport function isPlatformMarketingEmailSourceKind(\n value: unknown,\n): value is PlatformMarketingEmailSourceKind {\n return (PLATFORM_MARKETING_EMAIL_SOURCE_KINDS as readonly unknown[]).includes(\n value,\n )\n}\n\n/** What a door records: a grant or a refusal. Absence is never recorded. */\nexport const PLATFORM_MARKETING_CONSENT_DECISIONS = ['granted', 'declined'] as const\n\nexport type PlatformMarketingConsentDecision =\n (typeof PLATFORM_MARKETING_CONSENT_DECISIONS)[number]\n\nexport function isPlatformMarketingConsentDecision(\n value: unknown,\n): value is PlatformMarketingConsentDecision {\n return (PLATFORM_MARKETING_CONSENT_DECISIONS as readonly unknown[]).includes(\n value,\n )\n}\n\n/**\n * The version of the consent wording every door shows. Bumped in the same\n * edit as {@link platformMarketingConsentText}; a record stores the version\n * it was shown under, so the two must move together.\n */\nexport const PLATFORM_MARKETING_CONSENT_TEXT_VERSION = '2026-09-20'\n\n/**\n * The sentence the person agrees to, with the deployment's brand in it.\n *\n * Affirmative and standalone: it names what is sent, who sends it, and that\n * it can be stopped — never bundled with the terms acceptance, and never\n * pre-ticked anywhere it is rendered.\n */\nexport function platformMarketingConsentText(brandName: string): string {\n return `Send me product updates from ${brandName}. You can opt out any time.`\n}\n\n/** {@link platformMarketingConsentText} for the brand this deployment wears. */\nexport const PLATFORM_MARKETING_CONSENT_TEXT = platformMarketingConsentText(\n PLATFORM_BRAND_NAME,\n)\n\n/**\n * The `users/{uid}` fields the decision is mirrored onto.\n *\n * The same names the contact record uses for the same facts, so one reader\n * can parse either document. The prompt-dismissal stamp has no contact\n * counterpart: it is about what the console asked, not what the person\n * agreed to.\n */\nexport const USER_MARKETING_CONSENT_FIELD = MARKETING_CONSENT_FIELD\nexport const USER_MARKETING_CONSENT_AT_FIELD = 'marketingConsentAtMs'\nexport const USER_MARKETING_CONSENT_SOURCE_FIELD = MARKETING_CONSENT_SOURCE_FIELD\nexport const USER_MARKETING_PROMPT_DISMISSED_AT_FIELD =\n 'marketingConsentPromptDismissedAtMs'\n\n/** How long a dismissed prompt stays away: ninety days. */\nexport const PLATFORM_MARKETING_PROMPT_SNOOZE_MS = 90 * 24 * 60 * 60 * 1000\n\n/** What the person's own document says about product email. */\nexport interface PlatformMarketingConsentState {\n /** `null` when nothing has been recorded — asked, but not answered. */\n decision: PlatformMarketingConsentDecision | null\n /** When the decision was recorded. */\n atMs: number | null\n /** The wording version the decision was made under. */\n textVersion: string | null\n /**\n * Which door recorded it — a console door, or an email door for a decision\n * mirrored from the marketing site's own pages. `null` for a record another\n * writer left.\n */\n sourceKind: PlatformMarketingConsentSourceKind | null\n /** When the prompt was last dismissed without an answer. */\n promptDismissedAtMs: number | null\n}\n\nfunction finiteOrNull(value: unknown): number | null {\n return typeof value === 'number' && Number.isFinite(value) ? value : null\n}\n\n/**\n * Reads the decision off a `users/{uid}` document, or off no document.\n *\n * Every unusable value reads as its absent case rather than throwing: a\n * malformed field must degrade to \"nothing recorded\", which asks again,\n * never to a grant.\n */\nexport function readPlatformMarketingConsent(\n document: Record<string, unknown> | null | undefined,\n): PlatformMarketingConsentState {\n const consent = document?.[USER_MARKETING_CONSENT_FIELD]\n const rawSource = document?.[USER_MARKETING_CONSENT_SOURCE_FIELD]\n const source =\n rawSource && typeof rawSource === 'object' && !Array.isArray(rawSource)\n ? (rawSource as Record<string, unknown>)\n : null\n const textVersion = source?.['textVersion']\n return {\n decision: consent === true ? 'granted' : consent === false ? 'declined' : null,\n atMs: finiteOrNull(document?.[USER_MARKETING_CONSENT_AT_FIELD]),\n textVersion:\n typeof textVersion === 'string' && textVersion ? textVersion : null,\n sourceKind: isPlatformMarketingConsentSourceKind(source?.['kind'])\n ? source['kind']\n : null,\n promptDismissedAtMs: finiteOrNull(\n document?.[USER_MARKETING_PROMPT_DISMISSED_AT_FIELD],\n ),\n }\n}\n\n/**\n * Whether the console should ask this person now.\n *\n * Never after a decision, in either direction: a \"no\" is an answer and a\n * \"yes\" needs no repeating. A dismissal is not an answer, so the prompt\n * returns once the snooze has run out.\n */\nexport function platformMarketingPromptDue(\n state: Pick<PlatformMarketingConsentState, 'decision' | 'promptDismissedAtMs'>,\n nowMs: number,\n): boolean {\n if (state.decision !== null) return false\n if (state.promptDismissedAtMs === null) return true\n return nowMs - state.promptDismissedAtMs >= PLATFORM_MARKETING_PROMPT_SNOOZE_MS\n}\n\n/**\n * What each door did, in prose, for whoever audits the record later.\n *\n * Keyed by door and decision so the sentence is decided once, here, and the\n * three doors cannot describe the same act three different ways.\n */\nconst PROVENANCE_REASONS: Record<\n PlatformMarketingConsoleSourceKind,\n Record<PlatformMarketingConsentDecision, string>\n> = {\n 'console-signup': {\n granted: 'Ticked the product-updates checkbox on the console sign-up form.',\n declined: 'Declined product updates on the console sign-up form.',\n },\n 'console-preferences': {\n granted: 'Turned product updates on in the console account preferences.',\n declined: 'Turned product updates off in the console account preferences.',\n },\n 'console-prompt': {\n granted: 'Answered yes to the product-updates prompt in the console.',\n declined: 'Answered no to the product-updates prompt in the console.',\n },\n}\n\n/**\n * The provenance stored with a console decision, on both documents.\n *\n * `actor: 'person'` is what keeps {@link MarketingConsentSource} honest here:\n * the reader treats provenance as an operator's assertion unless it says\n * otherwise, and this is the person's own click, carrying which door and\n * which wording. `by` is the account that clicked — the subject and the\n * actor are the same human, which is the fact the field records.\n */\nexport function platformMarketingConsentSource(input: {\n kind: PlatformMarketingConsoleSourceKind\n decision: PlatformMarketingConsentDecision\n uid: string\n atMs: number\n textVersion: string\n}): MarketingConsentSource {\n return {\n kind: input.kind,\n by: input.uid,\n atMs: input.atMs,\n reason: PROVENANCE_REASONS[input.kind][input.decision],\n textVersion: input.textVersion,\n actor: 'person',\n }\n}\n\n/**\n * What an email door did, and which decisions each door can make.\n *\n * The pairs are the whole vocabulary: an unsubscribe only ever refuses, a\n * resubscribe only ever restores, and only the preference page does both.\n * Typed as the union rather than a free kind and decision, so an\n * \"unsubscribe that granted\" cannot be written down at all.\n */\nexport type PlatformMarketingEmailDecision =\n | { kind: 'email-unsubscribe'; decision: 'declined' }\n | { kind: 'email-preferences'; decision: PlatformMarketingConsentDecision }\n | { kind: 'email-resubscribe'; decision: 'granted' }\n\nconst EMAIL_PROVENANCE_REASONS: {\n [K in PlatformMarketingEmailDecision as `${K['kind']}:${K['decision']}`]: string\n} = {\n 'email-unsubscribe:declined':\n 'Unsubscribed from every email the marketing site sends, from a link in one of its emails.',\n 'email-preferences:declined':\n 'Left product updates on the marketing site’s email preference page.',\n 'email-preferences:granted':\n 'Chose product updates again on the marketing site’s email preference page.',\n 'email-resubscribe:granted':\n 'Resubscribed from the link in one of the marketing site’s emails.',\n}\n\n/**\n * The provenance stored with a decision mirrored from the marketing site's\n * own pages (AGL-3305), on the account only.\n *\n * Still `actor: 'person'`: the click was the recipient's, on a link signed\n * for their address. No `textVersion`, because those pages do not version\n * their wording; the field is absent rather than borrowing the console's\n * version for a sentence the person never saw.\n */\nexport function platformMarketingEmailSource(\n input: PlatformMarketingEmailDecision & { uid: string; atMs: number },\n): MarketingConsentSource {\n return {\n kind: input.kind,\n by: input.uid,\n atMs: input.atMs,\n // The union narrows the pair; the template type cannot see that, so the\n // key is asserted to the table's own keys rather than widened to string.\n reason: EMAIL_PROVENANCE_REASONS[\n `${input.kind}:${input.decision}` as keyof typeof EMAIL_PROVENANCE_REASONS\n ],\n actor: 'person',\n }\n}\n\n/**\n * The `users/{uid}` fields a decision writes, as one merge-able object.\n *\n * `false` for a refusal, not a deleted field: absence is the third state and\n * means \"never asked\", which a person who said no must never read as again.\n */\nexport function platformMarketingUserFields(input: {\n decision: PlatformMarketingConsentDecision\n atMs: number\n source: MarketingConsentSource\n}): Record<string, unknown> {\n return {\n [USER_MARKETING_CONSENT_FIELD]: input.decision === 'granted',\n [USER_MARKETING_CONSENT_AT_FIELD]: input.atMs,\n [USER_MARKETING_CONSENT_SOURCE_FIELD]: input.source,\n }\n}\n"],"names":["EMAIL_TOPIC_PRODUCT_UPDATES","MARKETING_CONSENT_FIELD","MARKETING_CONSENT_SOURCE_FIELD","PLATFORM_BRAND_NAME","PLATFORM_MARKETING_TOPIC_ID","PLATFORM_MARKETING_CONSOLE_SOURCE_KINDS","PLATFORM_MARKETING_EMAIL_SOURCE_KINDS","PLATFORM_MARKETING_CONSENT_SOURCE_KINDS","isPlatformMarketingConsentSourceKind","value","includes","isPlatformMarketingConsoleSourceKind","isPlatformMarketingEmailSourceKind","PLATFORM_MARKETING_CONSENT_DECISIONS","isPlatformMarketingConsentDecision","PLATFORM_MARKETING_CONSENT_TEXT_VERSION","platformMarketingConsentText","brandName","PLATFORM_MARKETING_CONSENT_TEXT","USER_MARKETING_CONSENT_FIELD","USER_MARKETING_CONSENT_AT_FIELD","USER_MARKETING_CONSENT_SOURCE_FIELD","USER_MARKETING_PROMPT_DISMISSED_AT_FIELD","PLATFORM_MARKETING_PROMPT_SNOOZE_MS","finiteOrNull","Number","isFinite","readPlatformMarketingConsent","document","consent","rawSource","source","Array","isArray","textVersion","decision","atMs","sourceKind","promptDismissedAtMs","platformMarketingPromptDue","state","nowMs","PROVENANCE_REASONS","granted","declined","platformMarketingConsentSource","input","kind","by","uid","reason","actor","EMAIL_PROVENANCE_REASONS","platformMarketingEmailSource","platformMarketingUserFields"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmDC,GAED,SAASA,2BAA2B,QAAQ,oBAAgB;AAC5D,SACEC,uBAAuB,EACvBC,8BAA8B,QAEzB,yBAAqB;AAC5B,SAASC,mBAAmB,QAAQ,sBAAkB;AAEtD;;;;;;;;;;CAUC,GACD,OAAO,MAAMC,8BAA8BJ,4BAA2B;AAEtE,6EAA6E,GAC7E,OAAO,MAAMK,0CAA0C;IACrD;IACA;IACA;CACD,CAAS;AAKV;;;;;;;;;CASC,GACD,OAAO,MAAMC,wCAAwC;IACnD;IACA;IACA;CACD,CAAS;AAKV,2CAA2C,GAC3C,OAAO,MAAMC,0CAA0C;OAClDF;OACAC;CACJ,CAAS;AAKV,6CAA6C,GAC7C,OAAO,SAASE,qCACdC,KAAc;IAEd,OAAO,AAACF,wCAA+DG,QAAQ,CAC7ED;AAEJ;AAEA,gEAAgE,GAChE,OAAO,SAASE,qCACdF,KAAc;IAEd,OAAO,AAACJ,wCAA+DK,QAAQ,CAC7ED;AAEJ;AAEA,4EAA4E,GAC5E,OAAO,SAASG,mCACdH,KAAc;IAEd,OAAO,AAACH,sCAA6DI,QAAQ,CAC3ED;AAEJ;AAEA,0EAA0E,GAC1E,OAAO,MAAMI,uCAAuC;IAAC;IAAW;CAAW,CAAS;AAKpF,OAAO,SAASC,mCACdL,KAAc;IAEd,OAAO,AAACI,qCAA4DH,QAAQ,CAC1ED;AAEJ;AAEA;;;;CAIC,GACD,OAAO,MAAMM,0CAA0C,aAAY;AAEnE;;;;;;CAMC,GACD,OAAO,SAASC,6BAA6BC,SAAiB;IAC5D,OAAO,CAAC,6BAA6B,EAAEA,UAAU,2BAA2B,CAAC;AAC/E;AAEA,8EAA8E,GAC9E,OAAO,MAAMC,kCAAkCF,6BAC7Cb,qBACD;AAED;;;;;;;CAOC,GACD,OAAO,MAAMgB,+BAA+BlB,wBAAuB;AACnE,OAAO,MAAMmB,kCAAkC,uBAAsB;AACrE,OAAO,MAAMC,sCAAsCnB,+BAA8B;AACjF,OAAO,MAAMoB,2CACX,sCAAqC;AAEvC,yDAAyD,GACzD,OAAO,MAAMC,sCAAsC,KAAK,KAAK,KAAK,KAAK,KAAI;AAoB3E,SAASC,aAAaf,KAAc;IAClC,OAAO,OAAOA,UAAU,YAAYgB,OAAOC,QAAQ,CAACjB,SAASA,QAAQ;AACvE;AAEA;;;;;;CAMC,GACD,OAAO,SAASkB,6BACdC,QAAoD;IAEpD,MAAMC,UAAUD,4BAAAA,QAAU,CAACT,6BAA6B;IACxD,MAAMW,YAAYF,4BAAAA,QAAU,CAACP,oCAAoC;IACjE,MAAMU,SACJD,aAAa,OAAOA,cAAc,YAAY,CAACE,MAAMC,OAAO,CAACH,aACxDA,YACD;IACN,MAAMI,cAAcH,0BAAAA,MAAQ,CAAC,cAAc;IAC3C,OAAO;QACLI,UAAUN,YAAY,OAAO,YAAYA,YAAY,QAAQ,aAAa;QAC1EO,MAAMZ,aAAaI,4BAAAA,QAAU,CAACR,gCAAgC;QAC9Dc,aACE,OAAOA,gBAAgB,YAAYA,cAAcA,cAAc;QACjEG,YAAY7B,qCAAqCuB,0BAAAA,MAAQ,CAAC,OAAO,IAC7DA,MAAM,CAAC,OAAO,GACd;QACJO,qBAAqBd,aACnBI,4BAAAA,QAAU,CAACN,yCAAyC;IAExD;AACF;AAEA;;;;;;CAMC,GACD,OAAO,SAASiB,2BACdC,KAA8E,EAC9EC,KAAa;IAEb,IAAID,MAAML,QAAQ,KAAK,MAAM,OAAO;IACpC,IAAIK,MAAMF,mBAAmB,KAAK,MAAM,OAAO;IAC/C,OAAOG,QAAQD,MAAMF,mBAAmB,IAAIf;AAC9C;AAEA;;;;;CAKC,GACD,MAAMmB,qBAGF;IACF,kBAAkB;QAChBC,SAAS;QACTC,UAAU;IACZ;IACA,uBAAuB;QACrBD,SAAS;QACTC,UAAU;IACZ;IACA,kBAAkB;QAChBD,SAAS;QACTC,UAAU;IACZ;AACF;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASC,+BAA+BC,KAM9C;IACC,OAAO;QACLC,MAAMD,MAAMC,IAAI;QAChBC,IAAIF,MAAMG,GAAG;QACbb,MAAMU,MAAMV,IAAI;QAChBc,QAAQR,kBAAkB,CAACI,MAAMC,IAAI,CAAC,CAACD,MAAMX,QAAQ,CAAC;QACtDD,aAAaY,MAAMZ,WAAW;QAC9BiB,OAAO;IACT;AACF;AAeA,MAAMC,2BAEF;IACF,8BACE;IACF,8BACE;IACF,6BACE;IACF,6BACE;AACJ;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASC,6BACdP,KAAqE;IAErE,OAAO;QACLC,MAAMD,MAAMC,IAAI;QAChBC,IAAIF,MAAMG,GAAG;QACbb,MAAMU,MAAMV,IAAI;QAChB,wEAAwE;QACxE,yEAAyE;QACzEc,QAAQE,wBAAwB,CAC9B,GAAGN,MAAMC,IAAI,CAAC,CAAC,EAAED,MAAMX,QAAQ,EAAE,CAClC;QACDgB,OAAO;IACT;AACF;AAEA;;;;;CAKC,GACD,OAAO,SAASG,4BAA4BR,KAI3C;IACC,OAAO;QACL,CAAC3B,6BAA6B,EAAE2B,MAAMX,QAAQ,KAAK;QACnD,CAACf,gCAAgC,EAAE0B,MAAMV,IAAI;QAC7C,CAACf,oCAAoC,EAAEyB,MAAMf,MAAM;IACrD;AACF"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/platform-marketing-consent.ts"],"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\n/**\n * Marketing consent for the PLATFORM's own product email (AGL-3185).\n *\n * The strict policy (`marketing-consent.ts`) means a person with no declared\n * basis is unsendable, and until this module the only doors that declared\n * one were the marketing site's forms. Somebody who created a console account\n * — the warmest audience the platform has — was never asked. Three console\n * doors ask now: a checkbox on the sign-up form, a switch in the account\n * preferences, and a one-time prompt for accounts that predate the checkbox.\n *\n * ## One wording, versioned\n *\n * Every door shows {@link PLATFORM_MARKETING_CONSENT_TEXT} and every record\n * stores {@link PLATFORM_MARKETING_CONSENT_TEXT_VERSION} beside the exact\n * timestamp, so \"what did this person agree to\" is answered by a version and\n * not by whatever the checkbox says today. Changing the sentence is a bump of\n * the version in the same edit.\n *\n * ## The record lives in two places, and the two do not disagree\n *\n * The decision is written by the server onto the person's own `users/{uid}`\n * document, so the console can show a preference without reading the\n * operator's CRM, and onto the operator's marketing contact for the same\n * person, so the send path sees a declared basis with provenance. The\n * contact half is written only when the deployment names a marketing host\n * (`PLATFORM_MARKETING_HOST_ID`, read in `@aglyn/tenant-data-admin`); a\n * self-hosted install with no audience of its own records the preference\n * and skips the contact — and keeps asking, so the answers are already on\n * file the day the operator names a marketing host.\n *\n * ## An unsubscribe reaches the account, for its own list only (AGL-3305)\n *\n * The marketing site's emails carry their own way out, and a person who uses\n * it has answered the console's question too — but only if what they left\n * is the list the question was about. Leaving\n * {@link PLATFORM_MARKETING_TOPIC_ID}, or everything the site sends, turns\n * the account's answer to No through an email door; leaving the newsletter\n * leaves it alone. The email doors write the account and nothing else: the\n * site's lists already say what was left, and declining the contact's basis\n * would stop every list, not the one the person chose.\n *\n * ## Absence is not refusal\n *\n * An unticked sign-up checkbox records nothing. A switch turned off and a\n * prompt answered \"no\" record a refusal, which the strict policy already\n * honors. A prompt dismissed without an answer records only the dismissal,\n * and the prompt stays away for {@link PLATFORM_MARKETING_PROMPT_SNOOZE_MS}.\n *\n * Pure and dependency-light on purpose: the sign-up page renders the text,\n * the console route validates the body, and the server library writes the\n * record, all from this one module.\n */\n\nimport {\n MARKETING_CONSENT_FIELD,\n MARKETING_CONSENT_SOURCE_FIELD,\n type MarketingConsentSource,\n} from './marketing-consent'\nimport { PLATFORM_BRAND_NAME } from './platform-brand'\n\n/**\n * The list the account's answer stands for on the marketing site (AGL-3305):\n * the built-in \"Product updates\" stream, which is what the platform's own\n * announcements are sent on. A wire value like every topic id — it keys the\n * opt-outs already recorded on the marketing host — and the same stream the\n * built-in catalog offers every site as \"Product updates\".\n *\n * Named so that \"which unsubscribe is about this answer\" has one spelling.\n * Leaving this list, or leaving everything the site sends, is a No to the\n * question the console asked. Leaving any other list is not, and must not\n * touch the answer: someone who stops the newsletter has said nothing about\n * product updates.\n */\nexport const PLATFORM_MARKETING_TOPIC_ID = 'product-updates'\n\n/** The console doors a decision can come through, as the record names them. */\nexport const PLATFORM_MARKETING_CONSOLE_SOURCE_KINDS = [\n 'console-signup',\n 'console-preferences',\n 'console-prompt',\n] as const\n\nexport type PlatformMarketingConsoleSourceKind =\n (typeof PLATFORM_MARKETING_CONSOLE_SOURCE_KINDS)[number]\n\n/**\n * The email doors (AGL-3305): the marketing site's own unsubscribe,\n * preference and resubscribe pages, when what the person changed there was\n * {@link PLATFORM_MARKETING_TOPIC_ID} or everything.\n *\n * Kept apart from the console doors because nothing the console sends may\n * claim one. The server records these from the signed link a recipient\n * followed; a console request naming one is refused, because a click in the\n * console is not a click in a mailbox.\n */\nexport const PLATFORM_MARKETING_EMAIL_SOURCE_KINDS = [\n 'email-unsubscribe',\n 'email-preferences',\n 'email-resubscribe',\n] as const\n\nexport type PlatformMarketingEmailSourceKind =\n (typeof PLATFORM_MARKETING_EMAIL_SOURCE_KINDS)[number]\n\n/** Every door a stored decision can name. */\nexport const PLATFORM_MARKETING_CONSENT_SOURCE_KINDS = [\n ...PLATFORM_MARKETING_CONSOLE_SOURCE_KINDS,\n ...PLATFORM_MARKETING_EMAIL_SOURCE_KINDS,\n] as const\n\nexport type PlatformMarketingConsentSourceKind =\n (typeof PLATFORM_MARKETING_CONSENT_SOURCE_KINDS)[number]\n\n/** Any door, for READING a stored decision. */\nexport function isPlatformMarketingConsentSourceKind(\n value: unknown,\n): value is PlatformMarketingConsentSourceKind {\n return (PLATFORM_MARKETING_CONSENT_SOURCE_KINDS as readonly unknown[]).includes(\n value,\n )\n}\n\n/** A console door: the only kind a console request may record. */\nexport function isPlatformMarketingConsoleSourceKind(\n value: unknown,\n): value is PlatformMarketingConsoleSourceKind {\n return (PLATFORM_MARKETING_CONSOLE_SOURCE_KINDS as readonly unknown[]).includes(\n value,\n )\n}\n\n/** An email door: a decision made from a link in one of the site's emails. */\nexport function isPlatformMarketingEmailSourceKind(\n value: unknown,\n): value is PlatformMarketingEmailSourceKind {\n return (PLATFORM_MARKETING_EMAIL_SOURCE_KINDS as readonly unknown[]).includes(\n value,\n )\n}\n\n/** What a door records: a grant or a refusal. Absence is never recorded. */\nexport const PLATFORM_MARKETING_CONSENT_DECISIONS = ['granted', 'declined'] as const\n\nexport type PlatformMarketingConsentDecision =\n (typeof PLATFORM_MARKETING_CONSENT_DECISIONS)[number]\n\nexport function isPlatformMarketingConsentDecision(\n value: unknown,\n): value is PlatformMarketingConsentDecision {\n return (PLATFORM_MARKETING_CONSENT_DECISIONS as readonly unknown[]).includes(\n value,\n )\n}\n\n/**\n * The version of the consent wording every door shows. Bumped in the same\n * edit as {@link platformMarketingConsentText}; a record stores the version\n * it was shown under, so the two must move together.\n */\nexport const PLATFORM_MARKETING_CONSENT_TEXT_VERSION = '2026-09-20'\n\n/**\n * The sentence the person agrees to, with the deployment's brand in it.\n *\n * Affirmative and standalone: it names what is sent, who sends it, and that\n * it can be stopped — never bundled with the terms acceptance, and never\n * pre-ticked anywhere it is rendered.\n */\nexport function platformMarketingConsentText(brandName: string): string {\n return `Send me product updates from ${brandName}. You can opt out any time.`\n}\n\n/** {@link platformMarketingConsentText} for the brand this deployment wears. */\nexport const PLATFORM_MARKETING_CONSENT_TEXT = platformMarketingConsentText(\n PLATFORM_BRAND_NAME,\n)\n\n/**\n * The `users/{uid}` fields the decision is mirrored onto.\n *\n * The same names the contact record uses for the same facts, so one reader\n * can parse either document. The prompt-dismissal stamp has no contact\n * counterpart: it is about what the console asked, not what the person\n * agreed to.\n */\nexport const USER_MARKETING_CONSENT_FIELD = MARKETING_CONSENT_FIELD\nexport const USER_MARKETING_CONSENT_AT_FIELD = 'marketingConsentAtMs'\nexport const USER_MARKETING_CONSENT_SOURCE_FIELD = MARKETING_CONSENT_SOURCE_FIELD\nexport const USER_MARKETING_PROMPT_DISMISSED_AT_FIELD =\n 'marketingConsentPromptDismissedAtMs'\n\n/** How long a dismissed prompt stays away: ninety days. */\nexport const PLATFORM_MARKETING_PROMPT_SNOOZE_MS = 90 * 24 * 60 * 60 * 1000\n\n/** What the person's own document says about product email. */\nexport interface PlatformMarketingConsentState {\n /** `null` when nothing has been recorded — asked, but not answered. */\n decision: PlatformMarketingConsentDecision | null\n /** When the decision was recorded. */\n atMs: number | null\n /** The wording version the decision was made under. */\n textVersion: string | null\n /**\n * Which door recorded it — a console door, or an email door for a decision\n * mirrored from the marketing site's own pages. `null` for a record another\n * writer left.\n */\n sourceKind: PlatformMarketingConsentSourceKind | null\n /** When the prompt was last dismissed without an answer. */\n promptDismissedAtMs: number | null\n}\n\nfunction finiteOrNull(value: unknown): number | null {\n return typeof value === 'number' && Number.isFinite(value) ? value : null\n}\n\n/**\n * Reads the decision off a `users/{uid}` document, or off no document.\n *\n * Every unusable value reads as its absent case rather than throwing: a\n * malformed field must degrade to \"nothing recorded\", which asks again,\n * never to a grant.\n */\nexport function readPlatformMarketingConsent(\n document: Record<string, unknown> | null | undefined,\n): PlatformMarketingConsentState {\n const consent = document?.[USER_MARKETING_CONSENT_FIELD]\n const rawSource = document?.[USER_MARKETING_CONSENT_SOURCE_FIELD]\n const source =\n rawSource && typeof rawSource === 'object' && !Array.isArray(rawSource)\n ? (rawSource as Record<string, unknown>)\n : null\n const textVersion = source?.['textVersion']\n return {\n decision: consent === true ? 'granted' : consent === false ? 'declined' : null,\n atMs: finiteOrNull(document?.[USER_MARKETING_CONSENT_AT_FIELD]),\n textVersion:\n typeof textVersion === 'string' && textVersion ? textVersion : null,\n sourceKind: isPlatformMarketingConsentSourceKind(source?.['kind'])\n ? source['kind']\n : null,\n promptDismissedAtMs: finiteOrNull(\n document?.[USER_MARKETING_PROMPT_DISMISSED_AT_FIELD],\n ),\n }\n}\n\n/**\n * Whether the console should ask this person now.\n *\n * Never after a decision, in either direction: a \"no\" is an answer and a\n * \"yes\" needs no repeating. A dismissal is not an answer, so the prompt\n * returns once the snooze has run out.\n */\nexport function platformMarketingPromptDue(\n state: Pick<PlatformMarketingConsentState, 'decision' | 'promptDismissedAtMs'>,\n nowMs: number,\n): boolean {\n if (state.decision !== null) return false\n if (state.promptDismissedAtMs === null) return true\n return nowMs - state.promptDismissedAtMs >= PLATFORM_MARKETING_PROMPT_SNOOZE_MS\n}\n\n/**\n * What each door did, in prose, for whoever audits the record later.\n *\n * Keyed by door and decision so the sentence is decided once, here, and the\n * three doors cannot describe the same act three different ways.\n */\nconst PROVENANCE_REASONS: Record<\n PlatformMarketingConsoleSourceKind,\n Record<PlatformMarketingConsentDecision, string>\n> = {\n 'console-signup': {\n granted: 'Ticked the product-updates checkbox on the console sign-up form.',\n declined: 'Declined product updates on the console sign-up form.',\n },\n 'console-preferences': {\n granted: 'Turned product updates on in the console account preferences.',\n declined: 'Turned product updates off in the console account preferences.',\n },\n 'console-prompt': {\n granted: 'Answered yes to the product-updates prompt in the console.',\n declined: 'Answered no to the product-updates prompt in the console.',\n },\n}\n\n/**\n * The provenance stored with a console decision, on both documents.\n *\n * `actor: 'person'` is what keeps {@link MarketingConsentSource} honest here:\n * the reader treats provenance as an operator's assertion unless it says\n * otherwise, and this is the person's own click, carrying which door and\n * which wording. `by` is the account that clicked — the subject and the\n * actor are the same human, which is the fact the field records.\n */\nexport function platformMarketingConsentSource(input: {\n kind: PlatformMarketingConsoleSourceKind\n decision: PlatformMarketingConsentDecision\n uid: string\n atMs: number\n textVersion: string\n}): MarketingConsentSource {\n return {\n kind: input.kind,\n by: input.uid,\n atMs: input.atMs,\n reason: PROVENANCE_REASONS[input.kind][input.decision],\n textVersion: input.textVersion,\n actor: 'person',\n }\n}\n\n/**\n * What an email door did, and which decisions each door can make.\n *\n * The pairs are the whole vocabulary: an unsubscribe only ever refuses, a\n * resubscribe only ever restores, and only the preference page does both.\n * Typed as the union rather than a free kind and decision, so an\n * \"unsubscribe that granted\" cannot be written down at all.\n */\nexport type PlatformMarketingEmailDecision =\n | { kind: 'email-unsubscribe'; decision: 'declined' }\n | { kind: 'email-preferences'; decision: PlatformMarketingConsentDecision }\n | { kind: 'email-resubscribe'; decision: 'granted' }\n\nconst EMAIL_PROVENANCE_REASONS: {\n [K in PlatformMarketingEmailDecision as `${K['kind']}:${K['decision']}`]: string\n} = {\n 'email-unsubscribe:declined':\n 'Unsubscribed from every email the marketing site sends, from a link in one of its emails.',\n 'email-preferences:declined':\n 'Left product updates on the marketing site’s email preference page.',\n 'email-preferences:granted':\n 'Chose product updates again on the marketing site’s email preference page.',\n 'email-resubscribe:granted':\n 'Resubscribed from the link in one of the marketing site’s emails.',\n}\n\n/**\n * The provenance stored with a decision mirrored from the marketing site's\n * own pages (AGL-3305), on the account only.\n *\n * Still `actor: 'person'`: the click was the recipient's, on a link signed\n * for their address. No `textVersion`, because those pages do not version\n * their wording; the field is absent rather than borrowing the console's\n * version for a sentence the person never saw.\n */\nexport function platformMarketingEmailSource(\n input: PlatformMarketingEmailDecision & { uid: string; atMs: number },\n): MarketingConsentSource {\n return {\n kind: input.kind,\n by: input.uid,\n atMs: input.atMs,\n // The union narrows the pair; the template type cannot see that, so the\n // key is asserted to the table's own keys rather than widened to string.\n reason: EMAIL_PROVENANCE_REASONS[\n `${input.kind}:${input.decision}` as keyof typeof EMAIL_PROVENANCE_REASONS\n ],\n actor: 'person',\n }\n}\n\n/**\n * The `users/{uid}` fields a decision writes, as one merge-able object.\n *\n * `false` for a refusal, not a deleted field: absence is the third state and\n * means \"never asked\", which a person who said no must never read as again.\n */\nexport function platformMarketingUserFields(input: {\n decision: PlatformMarketingConsentDecision\n atMs: number\n source: MarketingConsentSource\n}): Record<string, unknown> {\n return {\n [USER_MARKETING_CONSENT_FIELD]: input.decision === 'granted',\n [USER_MARKETING_CONSENT_AT_FIELD]: input.atMs,\n [USER_MARKETING_CONSENT_SOURCE_FIELD]: input.source,\n }\n}\n"],"names":["MARKETING_CONSENT_FIELD","MARKETING_CONSENT_SOURCE_FIELD","PLATFORM_BRAND_NAME","PLATFORM_MARKETING_TOPIC_ID","PLATFORM_MARKETING_CONSOLE_SOURCE_KINDS","PLATFORM_MARKETING_EMAIL_SOURCE_KINDS","PLATFORM_MARKETING_CONSENT_SOURCE_KINDS","isPlatformMarketingConsentSourceKind","value","includes","isPlatformMarketingConsoleSourceKind","isPlatformMarketingEmailSourceKind","PLATFORM_MARKETING_CONSENT_DECISIONS","isPlatformMarketingConsentDecision","PLATFORM_MARKETING_CONSENT_TEXT_VERSION","platformMarketingConsentText","brandName","PLATFORM_MARKETING_CONSENT_TEXT","USER_MARKETING_CONSENT_FIELD","USER_MARKETING_CONSENT_AT_FIELD","USER_MARKETING_CONSENT_SOURCE_FIELD","USER_MARKETING_PROMPT_DISMISSED_AT_FIELD","PLATFORM_MARKETING_PROMPT_SNOOZE_MS","finiteOrNull","Number","isFinite","readPlatformMarketingConsent","document","consent","rawSource","source","Array","isArray","textVersion","decision","atMs","sourceKind","promptDismissedAtMs","platformMarketingPromptDue","state","nowMs","PROVENANCE_REASONS","granted","declined","platformMarketingConsentSource","input","kind","by","uid","reason","actor","EMAIL_PROVENANCE_REASONS","platformMarketingEmailSource","platformMarketingUserFields"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmDC,GAED,SACEA,uBAAuB,EACvBC,8BAA8B,QAEzB,yBAAqB;AAC5B,SAASC,mBAAmB,QAAQ,sBAAkB;AAEtD;;;;;;;;;;;;CAYC,GACD,OAAO,MAAMC,8BAA8B,kBAAiB;AAE5D,6EAA6E,GAC7E,OAAO,MAAMC,0CAA0C;IACrD;IACA;IACA;CACD,CAAS;AAKV;;;;;;;;;CASC,GACD,OAAO,MAAMC,wCAAwC;IACnD;IACA;IACA;CACD,CAAS;AAKV,2CAA2C,GAC3C,OAAO,MAAMC,0CAA0C;OAClDF;OACAC;CACJ,CAAS;AAKV,6CAA6C,GAC7C,OAAO,SAASE,qCACdC,KAAc;IAEd,OAAO,AAACF,wCAA+DG,QAAQ,CAC7ED;AAEJ;AAEA,gEAAgE,GAChE,OAAO,SAASE,qCACdF,KAAc;IAEd,OAAO,AAACJ,wCAA+DK,QAAQ,CAC7ED;AAEJ;AAEA,4EAA4E,GAC5E,OAAO,SAASG,mCACdH,KAAc;IAEd,OAAO,AAACH,sCAA6DI,QAAQ,CAC3ED;AAEJ;AAEA,0EAA0E,GAC1E,OAAO,MAAMI,uCAAuC;IAAC;IAAW;CAAW,CAAS;AAKpF,OAAO,SAASC,mCACdL,KAAc;IAEd,OAAO,AAACI,qCAA4DH,QAAQ,CAC1ED;AAEJ;AAEA;;;;CAIC,GACD,OAAO,MAAMM,0CAA0C,aAAY;AAEnE;;;;;;CAMC,GACD,OAAO,SAASC,6BAA6BC,SAAiB;IAC5D,OAAO,CAAC,6BAA6B,EAAEA,UAAU,2BAA2B,CAAC;AAC/E;AAEA,8EAA8E,GAC9E,OAAO,MAAMC,kCAAkCF,6BAC7Cb,qBACD;AAED;;;;;;;CAOC,GACD,OAAO,MAAMgB,+BAA+BlB,wBAAuB;AACnE,OAAO,MAAMmB,kCAAkC,uBAAsB;AACrE,OAAO,MAAMC,sCAAsCnB,+BAA8B;AACjF,OAAO,MAAMoB,2CACX,sCAAqC;AAEvC,yDAAyD,GACzD,OAAO,MAAMC,sCAAsC,KAAK,KAAK,KAAK,KAAK,KAAI;AAoB3E,SAASC,aAAaf,KAAc;IAClC,OAAO,OAAOA,UAAU,YAAYgB,OAAOC,QAAQ,CAACjB,SAASA,QAAQ;AACvE;AAEA;;;;;;CAMC,GACD,OAAO,SAASkB,6BACdC,QAAoD;IAEpD,MAAMC,UAAUD,4BAAAA,QAAU,CAACT,6BAA6B;IACxD,MAAMW,YAAYF,4BAAAA,QAAU,CAACP,oCAAoC;IACjE,MAAMU,SACJD,aAAa,OAAOA,cAAc,YAAY,CAACE,MAAMC,OAAO,CAACH,aACxDA,YACD;IACN,MAAMI,cAAcH,0BAAAA,MAAQ,CAAC,cAAc;IAC3C,OAAO;QACLI,UAAUN,YAAY,OAAO,YAAYA,YAAY,QAAQ,aAAa;QAC1EO,MAAMZ,aAAaI,4BAAAA,QAAU,CAACR,gCAAgC;QAC9Dc,aACE,OAAOA,gBAAgB,YAAYA,cAAcA,cAAc;QACjEG,YAAY7B,qCAAqCuB,0BAAAA,MAAQ,CAAC,OAAO,IAC7DA,MAAM,CAAC,OAAO,GACd;QACJO,qBAAqBd,aACnBI,4BAAAA,QAAU,CAACN,yCAAyC;IAExD;AACF;AAEA;;;;;;CAMC,GACD,OAAO,SAASiB,2BACdC,KAA8E,EAC9EC,KAAa;IAEb,IAAID,MAAML,QAAQ,KAAK,MAAM,OAAO;IACpC,IAAIK,MAAMF,mBAAmB,KAAK,MAAM,OAAO;IAC/C,OAAOG,QAAQD,MAAMF,mBAAmB,IAAIf;AAC9C;AAEA;;;;;CAKC,GACD,MAAMmB,qBAGF;IACF,kBAAkB;QAChBC,SAAS;QACTC,UAAU;IACZ;IACA,uBAAuB;QACrBD,SAAS;QACTC,UAAU;IACZ;IACA,kBAAkB;QAChBD,SAAS;QACTC,UAAU;IACZ;AACF;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASC,+BAA+BC,KAM9C;IACC,OAAO;QACLC,MAAMD,MAAMC,IAAI;QAChBC,IAAIF,MAAMG,GAAG;QACbb,MAAMU,MAAMV,IAAI;QAChBc,QAAQR,kBAAkB,CAACI,MAAMC,IAAI,CAAC,CAACD,MAAMX,QAAQ,CAAC;QACtDD,aAAaY,MAAMZ,WAAW;QAC9BiB,OAAO;IACT;AACF;AAeA,MAAMC,2BAEF;IACF,8BACE;IACF,8BACE;IACF,6BACE;IACF,6BACE;AACJ;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASC,6BACdP,KAAqE;IAErE,OAAO;QACLC,MAAMD,MAAMC,IAAI;QAChBC,IAAIF,MAAMG,GAAG;QACbb,MAAMU,MAAMV,IAAI;QAChB,wEAAwE;QACxE,yEAAyE;QACzEc,QAAQE,wBAAwB,CAC9B,GAAGN,MAAMC,IAAI,CAAC,CAAC,EAAED,MAAMX,QAAQ,EAAE,CAClC;QACDgB,OAAO;IACT;AACF;AAEA;;;;;CAKC,GACD,OAAO,SAASG,4BAA4BR,KAI3C;IACC,OAAO;QACL,CAAC3B,6BAA6B,EAAE2B,MAAMX,QAAQ,KAAK;QACnD,CAACf,gCAAgC,EAAE0B,MAAMV,IAAI;QAC7C,CAACf,oCAAoC,EAAEyB,MAAMf,MAAM;IACrD;AACF"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GENERATED FILE — do not edit. Regenerate with:
|
|
3
|
+
* node tools/scripts/generate-plugin-manifests.mjs
|
|
4
|
+
*
|
|
5
|
+
* The starter sites plugins offer (AGL-3080): what each plugin's
|
|
6
|
+
* `starterTemplates` entry returned when this file was generated, in
|
|
7
|
+
* config order. `STARTER_TEMPLATES` in `starter-templates.ts` is the
|
|
8
|
+
* platform's own starters followed by these.
|
|
9
|
+
* Source of truth: plugins.config.json and the entries it names.
|
|
10
|
+
*/
|
|
11
|
+
import type { StarterTemplate } from './starter-template-nodes';
|
|
12
|
+
export declare const PLUGIN_STARTER_TEMPLATES: readonly StarterTemplate[];
|