@aglyn/aglyn 1.0.0-beta.217 → 1.0.0-beta.219
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 +14 -14
- package/src/lib/app-utils/actions.d.ts +25 -374
- package/src/lib/app-utils/actions.js +101 -561
- package/src/lib/app-utils/actions.js.map +1 -1
- package/src/lib/app-utils/activity-presenter.d.ts +3 -3
- package/src/lib/app-utils/activity-presenter.js +36 -52
- package/src/lib/app-utils/activity-presenter.js.map +1 -1
- package/src/lib/app-utils/admin-audit-index.js +2 -2
- package/src/lib/app-utils/admin-audit-index.js.map +1 -1
- package/src/lib/app-utils/analytics-events.d.ts +11 -2
- package/src/lib/app-utils/analytics-events.js +11 -2
- package/src/lib/app-utils/analytics-events.js.map +1 -1
- package/src/lib/app-utils/analytics-provider.d.ts +182 -0
- package/src/lib/app-utils/analytics-provider.js +133 -0
- package/src/lib/app-utils/analytics-provider.js.map +1 -0
- package/src/lib/app-utils/api-plugins.d.ts +46 -0
- package/src/lib/app-utils/api-plugins.js +19 -0
- package/src/lib/app-utils/api-plugins.js.map +1 -1
- package/src/lib/app-utils/{marketplace-overrides.d.ts → artifact-overrides.d.ts} +9 -6
- package/src/lib/app-utils/{marketplace-overrides.js → artifact-overrides.js} +10 -7
- package/src/lib/app-utils/artifact-overrides.js.map +1 -0
- package/src/lib/app-utils/{marketplace-provenance.d.ts → artifact-provenance.d.ts} +12 -10
- package/src/lib/app-utils/{marketplace-provenance.js → artifact-provenance.js} +9 -7
- package/src/lib/app-utils/artifact-provenance.js.map +1 -0
- package/src/lib/app-utils/assist-credits.js +2 -1
- package/src/lib/app-utils/assist-credits.js.map +1 -1
- package/src/lib/app-utils/binding-token-catalog.d.ts +5 -13
- package/src/lib/app-utils/binding-token-catalog.js +17 -41
- package/src/lib/app-utils/binding-token-catalog.js.map +1 -1
- package/src/lib/app-utils/binding-tokens.js +1 -1
- package/src/lib/app-utils/binding-tokens.js.map +1 -1
- package/src/lib/app-utils/brand-support-line.d.ts +36 -0
- package/src/lib/app-utils/brand-support-line.js +38 -0
- package/src/lib/app-utils/brand-support-line.js.map +1 -0
- package/src/lib/app-utils/campaign-touch.d.ts +5 -5
- package/src/lib/app-utils/campaign-touch.js +8 -8
- package/src/lib/app-utils/campaign-touch.js.map +1 -1
- package/src/lib/app-utils/collection-entries.d.ts +2 -2
- package/src/lib/app-utils/collection-entries.js.map +1 -1
- package/src/lib/app-utils/consent-group-change.d.ts +4 -0
- package/src/lib/app-utils/consent-group-change.js.map +1 -1
- package/src/lib/app-utils/console-record-links.d.ts +10 -89
- package/src/lib/app-utils/console-record-links.js +3 -107
- package/src/lib/app-utils/console-record-links.js.map +1 -1
- package/src/lib/app-utils/console-routes.d.ts +16 -7
- package/src/lib/app-utils/console-routes.js +9 -3
- package/src/lib/app-utils/console-routes.js.map +1 -1
- package/src/lib/app-utils/contacts.d.ts +23 -1
- package/src/lib/app-utils/contacts.js +21 -5
- package/src/lib/app-utils/contacts.js.map +1 -1
- package/src/lib/app-utils/cross-pool-desync.d.ts +39 -0
- package/src/lib/app-utils/cross-pool-desync.js +47 -0
- package/src/lib/app-utils/cross-pool-desync.js.map +1 -0
- package/src/lib/app-utils/csv-upload.d.ts +29 -0
- package/src/lib/app-utils/csv-upload.js +29 -0
- package/src/lib/app-utils/csv-upload.js.map +1 -0
- package/src/lib/app-utils/dataset-models.d.ts +5 -5
- package/src/lib/app-utils/dataset-models.js +19 -2
- package/src/lib/app-utils/dataset-models.js.map +1 -1
- package/src/lib/app-utils/datasets.d.ts +65 -11
- package/src/lib/app-utils/datasets.js +74 -11
- package/src/lib/app-utils/datasets.js.map +1 -1
- package/src/lib/app-utils/docs-deflection.d.ts +6 -6
- package/src/lib/app-utils/docs-deflection.js.map +1 -1
- package/src/lib/app-utils/docs-help.generated.d.ts +12 -6
- package/src/lib/app-utils/docs-help.generated.js +21 -6
- package/src/lib/app-utils/docs-help.generated.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.d.ts +2 -2
- package/src/lib/app-utils/docs-index.generated.js +514 -318
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/docs-retrieval.d.ts +3 -3
- package/src/lib/app-utils/docs-retrieval.js +14 -10
- package/src/lib/app-utils/docs-retrieval.js.map +1 -1
- package/src/lib/app-utils/duplicate-resource.d.ts +4 -1
- package/src/lib/app-utils/duplicate-resource.js +7 -4
- package/src/lib/app-utils/duplicate-resource.js.map +1 -1
- package/src/lib/app-utils/dynamic-list-rule.d.ts +1 -1
- package/src/lib/app-utils/dynamic-list-rule.js.map +1 -1
- package/src/lib/app-utils/element-ui.d.ts +1 -1
- package/src/lib/app-utils/element-ui.js +1 -1
- package/src/lib/app-utils/element-ui.js.map +1 -1
- package/src/lib/app-utils/email-starter-policy.js +3 -3
- package/src/lib/app-utils/email-starter-policy.js.map +1 -1
- package/src/lib/app-utils/entity-picker-context.d.ts +10 -5
- package/src/lib/app-utils/entity-picker-context.js.map +1 -1
- package/src/lib/app-utils/error-beacon.d.ts +75 -6
- package/src/lib/app-utils/error-beacon.js +160 -21
- package/src/lib/app-utils/error-beacon.js.map +1 -1
- package/src/lib/app-utils/expand-repeatables.d.ts +10 -10
- package/src/lib/app-utils/expand-repeatables.js +10 -10
- package/src/lib/app-utils/expand-repeatables.js.map +1 -1
- package/src/lib/app-utils/host-events.d.ts +82 -0
- package/src/lib/app-utils/host-events.js +62 -0
- package/src/lib/app-utils/host-events.js.map +1 -0
- package/src/lib/app-utils/lockdown.d.ts +33 -1
- package/src/lib/app-utils/lockdown.js +45 -4
- package/src/lib/app-utils/lockdown.js.map +1 -1
- package/src/lib/app-utils/margin-utilization.d.ts +4 -69
- package/src/lib/app-utils/margin-utilization.js +82 -69
- package/src/lib/app-utils/margin-utilization.js.map +1 -1
- package/src/lib/app-utils/node-definition-sanitizer.d.ts +16 -16
- package/src/lib/app-utils/node-definition-sanitizer.js +24 -20
- package/src/lib/app-utils/node-definition-sanitizer.js.map +1 -1
- package/src/lib/app-utils/node-interactions.d.ts +7 -7
- package/src/lib/app-utils/node-interactions.js.map +1 -1
- package/src/lib/app-utils/notifications.d.ts +78 -47
- package/src/lib/app-utils/notifications.js +63 -44
- package/src/lib/app-utils/notifications.js.map +1 -1
- package/src/lib/app-utils/payments-configured.d.ts +8 -21
- package/src/lib/app-utils/payments-configured.js +8 -23
- package/src/lib/app-utils/payments-configured.js.map +1 -1
- package/src/lib/app-utils/plan-entitlements.d.ts +43 -128
- package/src/lib/app-utils/plan-entitlements.js +145 -304
- package/src/lib/app-utils/plan-entitlements.js.map +1 -1
- package/src/lib/app-utils/platform-brand.d.ts +10 -0
- package/src/lib/app-utils/platform-brand.js +14 -3
- package/src/lib/app-utils/platform-brand.js.map +1 -1
- package/src/lib/app-utils/plugin-api-rate-limit.d.ts +7 -1
- package/src/lib/app-utils/plugin-api-rate-limit.js +11 -3
- package/src/lib/app-utils/plugin-api-rate-limit.js.map +1 -1
- package/src/lib/app-utils/plugin-bundle-checks.js +2 -2
- package/src/lib/app-utils/plugin-bundle-checks.js.map +1 -1
- package/src/lib/app-utils/plugin-host-events.generated.d.ts +12 -0
- package/src/lib/app-utils/plugin-host-events.generated.js +169 -0
- package/src/lib/app-utils/plugin-host-events.generated.js.map +1 -0
- package/src/lib/app-utils/plugin-release-flags.generated.d.ts +12 -0
- package/src/lib/app-utils/plugin-release-flags.generated.js +106 -0
- package/src/lib/app-utils/plugin-release-flags.generated.js.map +1 -0
- package/src/lib/app-utils/realm-host-surface.generated.js +2 -0
- package/src/lib/app-utils/realm-host-surface.generated.js.map +1 -1
- package/src/lib/app-utils/release-flags.d.ts +14 -5
- package/src/lib/app-utils/release-flags.js +21 -125
- package/src/lib/app-utils/release-flags.js.map +1 -1
- package/src/lib/app-utils/repeat-sources.d.ts +9 -0
- package/src/lib/app-utils/repeat-sources.js.map +1 -1
- package/src/lib/app-utils/retained-refusals.d.ts +73 -0
- package/src/lib/app-utils/retained-refusals.js +186 -0
- package/src/lib/app-utils/retained-refusals.js.map +1 -0
- package/src/lib/app-utils/scope-tokens.d.ts +27 -0
- package/src/lib/app-utils/scope-tokens.js +61 -0
- package/src/lib/app-utils/scope-tokens.js.map +1 -1
- package/src/lib/app-utils/screen-link-context.js +3 -3
- package/src/lib/app-utils/screen-link-context.js.map +1 -1
- package/src/lib/app-utils/screen-route.d.ts +15 -0
- package/src/lib/app-utils/screen-route.js +21 -1
- package/src/lib/app-utils/screen-route.js.map +1 -1
- package/src/lib/app-utils/seo-audit.d.ts +132 -0
- package/src/lib/app-utils/seo-audit.js +246 -0
- package/src/lib/app-utils/seo-audit.js.map +1 -0
- package/src/lib/app-utils/seo-keywords.d.ts +48 -0
- package/src/lib/app-utils/seo-keywords.js +60 -0
- package/src/lib/app-utils/seo-keywords.js.map +1 -0
- package/src/lib/app-utils/seo-page-facts.d.ts +66 -0
- package/src/lib/app-utils/seo-page-facts.js +155 -0
- package/src/lib/app-utils/seo-page-facts.js.map +1 -0
- package/src/lib/app-utils/seo-site-scan.d.ts +126 -0
- package/src/lib/app-utils/seo-site-scan.js +162 -0
- package/src/lib/app-utils/seo-site-scan.js.map +1 -0
- package/src/lib/app-utils/seo-title-variables.js +1 -1
- package/src/lib/app-utils/seo-title-variables.js.map +1 -1
- package/src/lib/app-utils/server.d.ts +6 -9
- package/src/lib/app-utils/server.js +17 -27
- package/src/lib/app-utils/server.js.map +1 -1
- package/src/lib/app-utils/site-interactions.d.ts +501 -0
- package/src/lib/app-utils/site-interactions.js +554 -0
- package/src/lib/app-utils/site-interactions.js.map +1 -0
- package/src/lib/app-utils/{marketplace-theme.d.ts → site-theme.d.ts} +13 -2
- package/src/lib/app-utils/{marketplace-theme.js → site-theme.js} +5 -2
- package/src/lib/app-utils/site-theme.js.map +1 -0
- package/src/lib/app-utils/sitemap.d.ts +4 -3
- package/src/lib/app-utils/sitemap.js +9 -7
- package/src/lib/app-utils/sitemap.js.map +1 -1
- package/src/lib/app-utils/stale-build-error.d.ts +51 -0
- package/src/lib/app-utils/stale-build-error.js +100 -1
- package/src/lib/app-utils/stale-build-error.js.map +1 -1
- package/src/lib/app-utils/starter-templates.d.ts +20 -0
- package/src/lib/app-utils/starter-templates.js +49 -0
- package/src/lib/app-utils/starter-templates.js.map +1 -1
- package/src/lib/app-utils/tax-jurisdiction-figures.d.ts +118 -0
- package/src/lib/app-utils/tax-jurisdiction-figures.js +123 -0
- package/src/lib/app-utils/tax-jurisdiction-figures.js.map +1 -0
- package/src/lib/app-utils/theme-library.d.ts +10 -3
- package/src/lib/app-utils/theme-library.js +13 -4
- package/src/lib/app-utils/theme-library.js.map +1 -1
- package/src/lib/app-utils/{campaign-attribution.d.ts → utm-attribution.d.ts} +11 -6
- package/src/lib/app-utils/{campaign-attribution.js → utm-attribution.js} +13 -8
- package/src/lib/app-utils/utm-attribution.js.map +1 -0
- package/src/lib/app-utils/{campaign-forwarding.d.ts → utm-forwarding.d.ts} +14 -14
- package/src/lib/app-utils/{campaign-forwarding.js → utm-forwarding.js} +26 -26
- package/src/lib/app-utils/utm-forwarding.js.map +1 -0
- package/src/lib/app-utils/video-object.js +6 -5
- package/src/lib/app-utils/video-object.js.map +1 -1
- package/src/lib/app-utils/visitor-consent.d.ts +41 -164
- package/src/lib/app-utils/visitor-consent.js +66 -277
- package/src/lib/app-utils/visitor-consent.js.map +1 -1
- package/src/lib/app-utils/web-vitals-rum.d.ts +7 -7
- package/src/lib/app-utils/web-vitals-rum.js +31 -63
- package/src/lib/app-utils/web-vitals-rum.js.map +1 -1
- package/src/lib/app-utils/where-used.d.ts +11 -6
- package/src/lib/app-utils/where-used.js +3 -8
- package/src/lib/app-utils/where-used.js.map +1 -1
- package/src/lib/canvas-manager/canvas-manager.js +3 -3
- package/src/lib/canvas-manager/canvas-manager.js.map +1 -1
- package/src/lib/foundation/definitions/components.types.d.ts +1 -1
- package/src/lib/foundation/definitions/components.types.js +1 -1
- package/src/lib/foundation/definitions/components.types.js.map +1 -1
- package/src/lib/foundation/definitions/org-billing.types.d.ts +18 -135
- package/src/lib/foundation/definitions/org-billing.types.js.map +1 -1
- package/src/lib/foundation/definitions/organization.types.d.ts +6 -3
- package/src/lib/foundation/definitions/organization.types.js.map +1 -1
- package/src/lib/foundation/definitions/platform.types.d.ts +27 -5
- package/src/lib/foundation/definitions/platform.types.js.map +1 -1
- package/src/lib/foundation/definitions/write-deny-coverage.util.d.ts +16 -0
- package/src/lib/foundation/definitions/write-deny-coverage.util.js +20 -0
- package/src/lib/foundation/definitions/write-deny-coverage.util.js.map +1 -1
- package/src/lib/foundation/index.d.ts +1 -1
- package/src/lib/foundation/index.js.map +1 -1
- package/src/lib/plugin-manager/besigner-documents.d.ts +96 -0
- package/src/lib/plugin-manager/besigner-documents.js +61 -0
- package/src/lib/plugin-manager/besigner-documents.js.map +1 -0
- package/src/lib/plugin-manager/computed-variables.d.ts +71 -0
- package/src/lib/plugin-manager/computed-variables.js +74 -0
- package/src/lib/plugin-manager/computed-variables.js.map +1 -0
- package/src/lib/plugin-manager/custom-fields.d.ts +29 -2
- package/src/lib/plugin-manager/custom-fields.js +47 -1
- package/src/lib/plugin-manager/custom-fields.js.map +1 -1
- package/src/lib/plugin-manager/feature-plugins.d.ts +292 -20
- package/src/lib/plugin-manager/feature-plugins.js +105 -17
- package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
- package/src/lib/plugin-manager/first-party-plugins.generated.d.ts +123 -0
- package/src/lib/plugin-manager/first-party-plugins.generated.js +785 -14
- package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
- package/src/lib/plugin-manager/plugin-activity-actions.d.ts +12 -0
- package/src/lib/plugin-manager/plugin-activity-actions.js +25 -0
- package/src/lib/plugin-manager/plugin-activity-actions.js.map +1 -1
- package/src/lib/plugin-manager/plugin-contributions.d.ts +1 -1
- package/src/lib/plugin-manager/plugin-contributions.js.map +1 -1
- package/src/lib/plugin-manager/plugin-declarations-repair.d.ts +36 -0
- package/src/lib/plugin-manager/plugin-declarations-repair.js +64 -0
- package/src/lib/plugin-manager/plugin-declarations-repair.js.map +1 -0
- package/src/lib/plugin-manager/plugin-dependents.d.ts +101 -0
- package/src/lib/plugin-manager/plugin-dependents.js +75 -0
- package/src/lib/plugin-manager/plugin-dependents.js.map +1 -0
- package/src/lib/plugin-manager/plugin-distribution.d.ts +55 -0
- package/src/lib/plugin-manager/plugin-distribution.js +45 -0
- package/src/lib/plugin-manager/plugin-distribution.js.map +1 -0
- package/src/lib/plugin-manager/plugin-host-collections.d.ts +17 -0
- package/src/lib/plugin-manager/plugin-host-collections.js +6 -0
- package/src/lib/plugin-manager/plugin-host-collections.js.map +1 -1
- package/src/lib/plugin-manager/plugin-host-resources.d.ts +133 -0
- package/src/lib/plugin-manager/plugin-host-resources.js +52 -0
- package/src/lib/plugin-manager/plugin-host-resources.js.map +1 -0
- package/src/lib/plugin-manager/plugin-org-erasure.d.ts +28 -2
- package/src/lib/plugin-manager/plugin-org-erasure.js +61 -2
- package/src/lib/plugin-manager/plugin-org-erasure.js.map +1 -1
- package/src/lib/plugin-manager/plugin-plan-entitlements.d.ts +123 -0
- package/src/lib/plugin-manager/plugin-plan-entitlements.js +101 -0
- package/src/lib/plugin-manager/plugin-plan-entitlements.js.map +1 -0
- package/src/lib/plugin-manager/plugin-record-index.d.ts +95 -0
- package/src/lib/plugin-manager/plugin-record-index.js +51 -0
- package/src/lib/plugin-manager/plugin-record-index.js.map +1 -0
- package/src/lib/plugin-manager/plugin-record-written.d.ts +58 -0
- package/src/lib/plugin-manager/plugin-record-written.js +42 -0
- package/src/lib/plugin-manager/plugin-record-written.js.map +1 -0
- package/src/lib/plugin-manager/plugin-revenue-sources.d.ts +213 -0
- package/src/lib/plugin-manager/plugin-revenue-sources.js +194 -0
- package/src/lib/plugin-manager/plugin-revenue-sources.js.map +1 -0
- package/src/lib/plugin-manager/plugin-site-bundle.d.ts +145 -0
- package/src/lib/plugin-manager/plugin-site-bundle.js +78 -0
- package/src/lib/plugin-manager/plugin-site-bundle.js.map +1 -0
- package/src/lib/plugin-manager/plugin-site-cache.d.ts +12 -0
- package/src/lib/plugin-manager/plugin-site-cache.js.map +1 -1
- package/src/lib/plugin-manager/plugin-site-export.d.ts +81 -0
- package/src/lib/plugin-manager/plugin-site-export.js +40 -0
- package/src/lib/plugin-manager/plugin-site-export.js.map +1 -0
- package/src/lib/plugin-manager/plugin-sitemap-sections.d.ts +88 -0
- package/src/lib/plugin-manager/plugin-sitemap-sections.js +29 -0
- package/src/lib/plugin-manager/plugin-sitemap-sections.js.map +1 -0
- package/src/lib/plugin-manager/plugin-tax-profile.d.ts +20 -4
- package/src/lib/plugin-manager/plugin-tax-profile.js.map +1 -1
- package/src/lib/plugin-manager/plugin-tax-return-sources.d.ts +255 -0
- package/src/lib/plugin-manager/plugin-tax-return-sources.js +191 -0
- package/src/lib/plugin-manager/plugin-tax-return-sources.js.map +1 -0
- package/src/lib/plugin-manager/plugin-template-sources.d.ts +75 -0
- package/src/lib/plugin-manager/plugin-template-sources.js +37 -0
- package/src/lib/plugin-manager/plugin-template-sources.js.map +1 -0
- package/src/lib/plugin-manager/plugin-usage-axes.d.ts +107 -0
- package/src/lib/plugin-manager/plugin-usage-axes.js +107 -0
- package/src/lib/plugin-manager/plugin-usage-axes.js.map +1 -0
- package/src/lib/plugin-manager/plugin-zones.d.ts +4 -5
- package/src/lib/plugin-manager/plugin-zones.js.map +1 -1
- package/src/lib/plugin-manager/realm-host-aglyn.generated.js +2 -0
- package/src/lib/plugin-manager/realm-host-aglyn.generated.js.map +1 -1
- package/src/lib/plugin-manager/record-captured-contact.d.ts +1 -14
- package/src/lib/plugin-manager/record-captured-contact.js +10 -32
- package/src/lib/plugin-manager/record-captured-contact.js.map +1 -1
- package/src/lib/plugin-manager/repeat-rows.d.ts +124 -0
- package/src/lib/plugin-manager/repeat-rows.js +108 -0
- package/src/lib/plugin-manager/repeat-rows.js.map +1 -0
- package/src/lib/plugin-manager/submission-record-target.d.ts +144 -0
- package/src/lib/plugin-manager/submission-record-target.js +125 -0
- package/src/lib/plugin-manager/submission-record-target.js.map +1 -0
- package/src/lib/plugin-manager/video-embed-provider.d.ts +108 -0
- package/src/lib/plugin-manager/video-embed-provider.js +165 -0
- package/src/lib/plugin-manager/video-embed-provider.js.map +1 -0
- package/src/lib/types/nodes.d.ts +2 -2
- package/src/lib/types/nodes.js.map +1 -1
- package/src/lib/app-utils/campaign-attribution.js.map +0 -1
- package/src/lib/app-utils/campaign-forwarding.js.map +0 -1
- package/src/lib/app-utils/contact-holder.d.ts +0 -68
- package/src/lib/app-utils/contact-holder.js +0 -129
- package/src/lib/app-utils/contact-holder.js.map +0 -1
- package/src/lib/app-utils/contact-merge.d.ts +0 -136
- package/src/lib/app-utils/contact-merge.js +0 -412
- package/src/lib/app-utils/contact-merge.js.map +0 -1
- package/src/lib/app-utils/crm-csv.d.ts +0 -239
- package/src/lib/app-utils/crm-csv.js +0 -467
- package/src/lib/app-utils/crm-csv.js.map +0 -1
- package/src/lib/app-utils/crm-digest.d.ts +0 -185
- package/src/lib/app-utils/crm-digest.js +0 -272
- package/src/lib/app-utils/crm-digest.js.map +0 -1
- package/src/lib/app-utils/form-dataset-binding.d.ts +0 -50
- package/src/lib/app-utils/form-dataset-binding.js +0 -74
- package/src/lib/app-utils/form-dataset-binding.js.map +0 -1
- package/src/lib/app-utils/list-import.d.ts +0 -158
- package/src/lib/app-utils/list-import.js +0 -332
- package/src/lib/app-utils/list-import.js.map +0 -1
- package/src/lib/app-utils/marketplace-listing-visibility.d.ts +0 -122
- package/src/lib/app-utils/marketplace-listing-visibility.js +0 -147
- package/src/lib/app-utils/marketplace-listing-visibility.js.map +0 -1
- package/src/lib/app-utils/marketplace-overrides.js.map +0 -1
- package/src/lib/app-utils/marketplace-provenance.js.map +0 -1
- package/src/lib/app-utils/marketplace-theme.js.map +0 -1
- package/src/lib/app-utils/marketplace-update-state.d.ts +0 -119
- package/src/lib/app-utils/marketplace-update-state.js +0 -132
- package/src/lib/app-utils/marketplace-update-state.js.map +0 -1
- package/src/lib/app-utils/publisher-agreement.d.ts +0 -289
- package/src/lib/app-utils/publisher-agreement.js +0 -321
- package/src/lib/app-utils/publisher-agreement.js.map +0 -1
- package/src/lib/app-utils/wistia-embed.d.ts +0 -104
- package/src/lib/app-utils/wistia-embed.js +0 -138
- package/src/lib/app-utils/wistia-embed.js.map +0 -1
- package/src/lib/app-utils/workflows.d.ts +0 -136
- package/src/lib/app-utils/workflows.js +0 -346
- package/src/lib/app-utils/workflows.js.map +0 -1
- package/src/lib/plugin-manager/commerce-zone-props.d.ts +0 -172
- package/src/lib/plugin-manager/commerce-zone-props.js +0 -31
- package/src/lib/plugin-manager/commerce-zone-props.js.map +0 -1
|
@@ -370,6 +370,8 @@ export interface ConsentGroupChangeCounts {
|
|
|
370
370
|
siteSuppressions: number;
|
|
371
371
|
topicOptOuts: number;
|
|
372
372
|
paces: number;
|
|
373
|
+
/** Refusals kept after their record was deleted, in `retainedRefusals` (AGL-3338). */
|
|
374
|
+
retainedRefusals: number;
|
|
373
375
|
}
|
|
374
376
|
/** A job's state, as the route answers it and the progress panel renders it. */
|
|
375
377
|
export interface ConsentGroupChangeProgress {
|
|
@@ -479,6 +481,8 @@ export interface ConsentGroupChangePreview {
|
|
|
479
481
|
siteSuppressions: number;
|
|
480
482
|
topicOptOuts: number;
|
|
481
483
|
paces: number;
|
|
484
|
+
/** The organization's retained refusals for the source site (AGL-3338). */
|
|
485
|
+
retainedRefusals: number;
|
|
482
486
|
}>;
|
|
483
487
|
/** Per joining site, the refusals on its new siblings it starts honoring. */
|
|
484
488
|
inherited: Array<{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/consent-group-change.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 * CHANGING WHICH SITES ARE ONE SENDER (AGL-3320) — the pure half.\n *\n * `consent-groups.ts` reads a declaration; this is what a change to one\n * means. It validates the next declaration an admin submits, decides what has\n * to move for the sites to stop or start being one sender, and words the\n * result — and it does all of that without a read, so the console's review\n * step, the route's refusal and the executor's plan are one computation.\n *\n * ## Why a change is a plan and not a write\n *\n * A declaration is read at send time, so flipping it is instant — and that is\n * exactly the problem. Two facts are stored per SITE and read across the\n * CURRENT group: a refusal (a suppression row, a topic opt-out, a pace, a\n * declined consent) and a holder's business record (the CRM facet keyed by\n * the group). When sites stop being one sender, each stops reading the\n * other's refusals; when they become one, the records they kept apart must\n * become one. The flip itself moves neither, so every change is:\n *\n * 1. CARRY the refusals onto the sites that are about to stop reading them,\n * in BOTH directions — the person who left one site of a sender was told\n * the opt-out covers all of them, so the site leaving keeps what it\n * honored and the sites staying keep what the leaver honored;\n * 2. FLIP the declaration;\n * 3. RE-HOME the holder records keyed by the old group ids.\n *\n * {@link planConsentGroupChange} says which pairs carry and which holder keys\n * move or split; the executor in `libs/tenant/data/admin` performs it.\n *\n * ## What the planner never touches\n *\n * Grants. A grant is written forward, one entry per disclosed site, and is\n * evidence of what a person was told, so a change never copies, re-keys or\n * revokes one — see `consent-groups.ts`, \"Pooling applies FORWARD only\".\n */\n\nimport {\n consentGroupDisclosure,\n CONSENT_GROUPS_FIELD,\n MAX_CONSENT_GROUP_HOSTS,\n readConsentGroups,\n type StoredConsentGroup,\n} from './consent-groups'\n\n/**\n * The org field that marks a change in flight:\n * {@link ConsentGroupsChangeMarker}. Authoritative — the job document holds\n * the detail, and this says whether there is a job at all.\n *\n * On the org document, beside the declaration it guards, so the transaction\n * that starts a change and the one that flips the declaration read one\n * document. Server-owned: denied to every client by the rules.\n */\nexport const CONSENT_GROUPS_CHANGE_FIELD = 'consentGroupsChange'\n\n/** `orgs/{orgId}/consentGroupChanges/{changeId}` — one job per change. */\nexport const CONSENT_GROUP_CHANGES_COLLECTION = 'consentGroupChanges'\n\n/**\n * The longest group name a declaration may carry.\n *\n * The name is rendered on every signup form of the group, inside a sentence,\n * so it is a brand and not a description.\n */\nexport const CONSENT_GROUP_NAME_MAX = 80\n\n/**\n * How long after the flip the sweep waits before its final pass.\n *\n * A long job — a campaign batch, a dynamic list, an outreach tick — resolves\n * the group once per batch and runs for at most five minutes, so a write it\n * made against the OLD declaration can land after the flip. Six minutes is\n * past the longest of them.\n */\nexport const CONSENT_GROUP_SWEEP_DELAY_MS = 6 * 60_000\n\n/**\n * What every minted group id starts with.\n *\n * A group id is a holder key beside the site ids a solo site is keyed by, so\n * the two must never meet. Firestore's automatic ids — which every site id is\n * — are alphanumeric, and an underscore cannot occur in one.\n */\nexport const CONSENT_GROUP_ID_PREFIX = 'cg_'\n\n/** Every group of a declaration, by id — what `readConsentGroups` answers. */\nexport type ConsentGroupDeclaration = Record<string, StoredConsentGroup>\n\n/** One group of the next declaration as the editor submits it. */\nexport interface ConsentGroupDraft {\n /** The id of a group that exists; absent for a new group. */\n id?: string | null\n name: string\n hostIds: string[]\n}\n\n/**\n * Why a submitted declaration was refused. Every code names something the\n * admin can fix in the editor; `groupIndex` points at the draft it is about,\n * and `hostId` at the site.\n */\nexport type ConsentGroupValidationCode =\n /** Not a list of `{ name, hostIds }` objects. */\n | 'malformed'\n | 'name-empty'\n | 'name-too-long'\n /** Two groups with the same name, ignoring case. */\n | 'name-duplicate'\n | 'too-few-sites'\n | 'too-many-sites'\n /** A site this organization does not own. */\n | 'unknown-site'\n | 'site-in-two-groups'\n /** An id no group of the current declaration has. */\n | 'unknown-group'\n /** One id on two drafts. */\n | 'duplicate-group'\n /**\n * A kept id whose sites are ALL new. Keeping the id would carry the old\n * group's records onto a set of sites none of which kept them; a new group\n * is what that is.\n */\n | 'group-replaced'\n /** The declaration submitted is the one in force. */\n | 'no-change'\n\nexport interface ConsentGroupValidationError {\n code: ConsentGroupValidationCode\n groupIndex?: number\n hostId?: string\n}\n\nexport type ConsentGroupValidation =\n | { ok: true; after: ConsentGroupDeclaration }\n | { ok: false; errors: ConsentGroupValidationError[] }\n\n/**\n * The sites an organization owns, from its `hosts` map, sorted.\n *\n * The map is `{ [hostId]: true }`; an array is read too, so a hand-built\n * fixture and the stored shape agree.\n */\nexport function orgSiteIds(\n org: Record<string, unknown> | null | undefined,\n): string[] {\n const hosts = (org ?? {})['hosts']\n const ids = Array.isArray(hosts)\n ? hosts.map((id) => String(id ?? ''))\n : hosts && typeof hosts === 'object'\n ? Object.entries(hosts as Record<string, unknown>)\n .filter(([, value]) => value !== false && value != null)\n .map(([id]) => id)\n : []\n return [...new Set(ids.map((id) => id.trim()).filter(Boolean))].sort()\n}\n\n/** The raw declaration's ids that `readConsentGroups` drops, sorted. */\nexport function discardedConsentGroupIds(\n org: Record<string, unknown> | null | undefined,\n): string[] {\n const raw = (org ?? {})[CONSENT_GROUPS_FIELD]\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return []\n const usable = readConsentGroups(org)\n return Object.keys(raw as Record<string, unknown>)\n .filter((id) => !(id in usable))\n .sort()\n}\n\nconst BASE36 = '0123456789abcdefghijklmnopqrstuvwxyz'\n\n/** Uniform in [0, 1), from the platform's CSPRNG where there is one. */\nfunction defaultRandom(): number {\n const crypto = (globalThis as { crypto?: { getRandomValues?: <T>(array: T) => T } })\n .crypto\n if (crypto?.getRandomValues) {\n const word = crypto.getRandomValues(new Uint32Array(1))[0]\n return word / 2 ** 32\n }\n return Math.random()\n}\n\n/**\n * A fresh group id: {@link CONSENT_GROUP_ID_PREFIX} and sixteen base-36\n * characters, never one in `taken`.\n *\n * `taken` holds every id the id must not equal — the current groups', the\n * raw declaration's, every site's and every id already minted for the same\n * declaration. Eighty bits make a collision a formality, and the loop makes it\n * impossible.\n */\nexport function mintConsentGroupId(\n taken: ReadonlySet<string>,\n random: () => number = defaultRandom,\n): string {\n for (;;) {\n let id = CONSENT_GROUP_ID_PREFIX\n for (let index = 0; index < 16; index += 1) {\n id += BASE36[Math.min(35, Math.floor(random() * 36))]\n }\n if (!taken.has(id)) return id\n }\n}\n\n/** A group's canonical form: its name trimmed, its sites unique and sorted. */\nfunction canonicalGroup(group: unknown): { name: string; hostIds: string[] } {\n const value = (group && typeof group === 'object' ? group : {}) as Record<string, unknown>\n const name = typeof value['name'] === 'string' ? value['name'].trim() : ''\n const hostIds = Array.isArray(value['hostIds'])\n ? [\n ...new Set(\n (value['hostIds'] as unknown[])\n .map((id) => String(id ?? '').trim())\n .filter(Boolean),\n ),\n ].sort()\n : []\n return { name, hostIds }\n}\n\n/**\n * Whether two declarations say the same thing: the same ids, each with the\n * same trimmed name and the same set of sites. Order never matters.\n */\nexport function consentGroupsEqual(\n a: Record<string, unknown> | null | undefined,\n b: Record<string, unknown> | null | undefined,\n): boolean {\n return consentGroupsFingerprint(a) === consentGroupsFingerprint(b)\n}\n\n/**\n * A declaration as one canonical string: what \"the value the editor\n * rendered\" and \"the value stored now\" are compared by.\n *\n * Every entry is kept, usable or not, because the question is whether the\n * stored value CHANGED, and an edit to an entry `readConsentGroups` drops is\n * still somebody else's edit. Absent, `null` and `{}` are one value — an\n * organization with no groups.\n */\nexport function consentGroupsFingerprint(raw: unknown): string {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return 'null'\n const entries = Object.entries(raw as Record<string, unknown>)\n .map(([id, group]) => [id, canonicalGroup(group)] as const)\n .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))\n return entries.length ? JSON.stringify(entries) : 'null'\n}\n\n/**\n * The next declaration, checked against the organization's sites and the\n * declaration in force, with an id minted for every new group.\n *\n * Refuses everything in the list on {@link ConsentGroupValidationCode}, and\n * reports every problem it finds rather than the first, so the editor can\n * mark each draft at once.\n */\nexport function validateConsentGroupDeclaration(input: {\n groups: unknown\n /** The declaration in force: `readConsentGroups(org)`. */\n before: ConsentGroupDeclaration\n /** The organization's sites: {@link orgSiteIds}. */\n siteIds: readonly string[]\n /** Ids a minted id must also avoid — the raw declaration's, for one. */\n reserved?: Iterable<string>\n random?: () => number\n}): ConsentGroupValidation {\n const { before } = input\n if (!Array.isArray(input.groups)) {\n return { ok: false, errors: [{ code: 'malformed' }] }\n }\n const sites = new Set(input.siteIds)\n const errors: ConsentGroupValidationError[] = []\n const names = new Set<string>()\n const ids = new Set<string>()\n const claimed = new Set<string>()\n const drafts: Array<{ id: string | null; name: string; hostIds: string[] }> = []\n\n input.groups.forEach((value, groupIndex) => {\n if (!value || typeof value !== 'object' || Array.isArray(value)) {\n errors.push({ code: 'malformed', groupIndex })\n return\n }\n const entry = value as Record<string, unknown>\n if (typeof entry['name'] !== 'string' || !Array.isArray(entry['hostIds'])) {\n errors.push({ code: 'malformed', groupIndex })\n return\n }\n const { name, hostIds } = canonicalGroup(entry)\n if (!name) errors.push({ code: 'name-empty', groupIndex })\n else if (name.length > CONSENT_GROUP_NAME_MAX) {\n errors.push({ code: 'name-too-long', groupIndex })\n } else if (names.has(name.toLowerCase())) {\n errors.push({ code: 'name-duplicate', groupIndex })\n }\n if (name) names.add(name.toLowerCase())\n\n for (const hostId of hostIds) {\n if (!sites.has(hostId)) {\n errors.push({ code: 'unknown-site', groupIndex, hostId })\n } else if (claimed.has(hostId)) {\n errors.push({ code: 'site-in-two-groups', groupIndex, hostId })\n }\n claimed.add(hostId)\n }\n if (hostIds.length < 2) errors.push({ code: 'too-few-sites', groupIndex })\n else if (hostIds.length > MAX_CONSENT_GROUP_HOSTS) {\n errors.push({ code: 'too-many-sites', groupIndex })\n }\n\n const rawId = entry['id']\n const id = typeof rawId === 'string' && rawId.trim() ? rawId.trim() : null\n if (id !== null) {\n if (ids.has(id)) errors.push({ code: 'duplicate-group', groupIndex })\n else if (!(id in before)) errors.push({ code: 'unknown-group', groupIndex })\n else if (!before[id].hostIds.some((hostId) => hostIds.includes(hostId))) {\n errors.push({ code: 'group-replaced', groupIndex })\n }\n ids.add(id)\n }\n drafts.push({ id, name, hostIds })\n })\n if (errors.length) return { ok: false, errors }\n\n const taken = new Set<string>([\n ...Object.keys(before),\n ...input.siteIds,\n ...(input.reserved ?? []),\n ...ids,\n ])\n const after: ConsentGroupDeclaration = {}\n for (const draft of drafts) {\n const id = draft.id ?? mintConsentGroupId(taken, input.random)\n taken.add(id)\n after[id] = { name: draft.name, hostIds: draft.hostIds }\n }\n if (consentGroupsEqual(before, after)) {\n return { ok: false, errors: [{ code: 'no-change' }] }\n }\n return { ok: true, after }\n}\n\n/*==========================================\n * THE PLAN\n *=========================================*/\n\n/**\n * One direction of a carry: `toHostId` is about to stop reading\n * `fromHostId`'s refusals, so they are copied onto it first.\n */\nexport interface ConsentGroupCarry {\n toHostId: string\n fromHostId: string\n}\n\n/**\n * Every site a holder key covered moves under ONE new key: the key's records\n * are folded into the new key's, and the old key is deleted in the same\n * write.\n */\nexport interface ConsentGroupHolderMove {\n kind: 'move'\n /** The key the records are under now: a group id, or a solo site's id. */\n from: string\n /** The sites `from` covered. */\n fromHostIds: string[]\n /** The key they move under: always a declared group of the next declaration. */\n to: string\n /** The sites of `to` in the next declaration. */\n toHostIds: string[]\n}\n\n/**\n * The sites a holder key covered go separate ways. Each successor may take a\n * copy of the key's records; the key itself survives when some of its sites\n * stay under it, and is deleted when none do.\n */\nexport interface ConsentGroupHolderSplit {\n kind: 'split'\n from: string\n fromHostIds: string[]\n /** Whether `from` is still a key afterwards. */\n survives: boolean\n /** The sites of `from` that stay under it. Empty when it does not survive. */\n stayHostIds: string[]\n /**\n * Where the other sites go: each successor key with the sites of `from`\n * that map to it — never the successor's other sites, whose records were\n * never under `from`.\n */\n successors: Array<{ key: string; hostIds: string[] }>\n}\n\nexport type ConsentGroupHolderFlow = ConsentGroupHolderMove | ConsentGroupHolderSplit\n\n/** What a change does to the declaration, as the activity log words it. */\nexport type ConsentGroupChangeLine =\n | { kind: 'created'; groupId: string; name: string; hostIds: string[] }\n | { kind: 'renamed'; groupId: string; from: string; to: string }\n | { kind: 'added'; groupId: string; name: string; hostId: string }\n | { kind: 'removed'; groupId: string; name: string; hostId: string }\n | {\n kind: 'moved'\n hostId: string\n fromGroupId: string\n fromName: string\n toGroupId: string\n toName: string\n }\n /** Includes a group absorbed into another: its sites each get a `moved`. */\n | { kind: 'dissolved'; groupId: string; name: string }\n\nexport interface ConsentGroupChangePlan {\n before: ConsentGroupDeclaration\n after: ConsentGroupDeclaration\n carries: ConsentGroupCarry[]\n flows: ConsentGroupHolderFlow[]\n lines: ConsentGroupChangeLine[]\n /**\n * Every site the change touches — whose group, members or group name\n * changes. What the marker names while the change runs: the sites that\n * may not be deleted, and whose holder records may be in motion.\n */\n hostIds: string[]\n /** Only names change: nothing to carry and nothing to re-home. */\n renameOnly: boolean\n}\n\nfunction groupOf(\n declaration: ConsentGroupDeclaration,\n): Map<string, { id: string; hostIds: string[] }> {\n const map = new Map<string, { id: string; hostIds: string[] }>()\n for (const [id, group] of Object.entries(declaration)) {\n for (const hostId of group.hostIds) map.set(hostId, { id, hostIds: group.hostIds })\n }\n return map\n}\n\nconst byString = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0)\n\n/** `declaration` without the sites `keep` does not name. */\nfunction withSitesOnly(\n declaration: ConsentGroupDeclaration,\n keep: ReadonlySet<string>,\n): ConsentGroupDeclaration {\n const out: ConsentGroupDeclaration = {}\n for (const [id, group] of Object.entries(declaration)) {\n const hostIds = group.hostIds.filter((hostId) => keep.has(hostId))\n if (hostIds.length) out[id] = { name: group.name, hostIds }\n }\n return out\n}\n\n/**\n * What moving from `before` to `after` requires.\n *\n * `key(s)` is the holder key of site `s` — its group's id, or `s` itself when\n * it is alone — and `members(s)` the sites of that group, or `[s]`.\n *\n * - **Carries.** For every pair a ≠ b that are one sender now and will not\n * be, `b`'s refusals are copied onto `a`. Pairs that stay together, and\n * pairs that come together, need nothing: a joined site reads its new\n * siblings' refusals the moment the declaration names it.\n * - **Holder flows.** For each key `k` in force, the sites it covers map to\n * a set `T` of keys afterwards. `T = {k}` needs nothing; one other key is\n * a MOVE; anything else is a SPLIT, which `k` survives when it is in `T`.\n *\n * `siteIds`, when given, are the sites the organization still owns: a site\n * named by `before` that is gone neither receives a carry nor takes a copy of\n * a record, because there is nothing left to hold either.\n */\nexport function planConsentGroupChange(\n before: ConsentGroupDeclaration,\n after: ConsentGroupDeclaration,\n options: { siteIds?: readonly string[] } = {},\n): ConsentGroupChangePlan {\n const owned = options.siteIds ? new Set(options.siteIds) : null\n const moving = owned ? withSitesOnly(before, owned) : before\n const beforeOf = groupOf(moving)\n const afterOf = groupOf(after)\n const keyBefore = (hostId: string) => beforeOf.get(hostId)?.id ?? hostId\n const keyAfter = (hostId: string) => afterOf.get(hostId)?.id ?? hostId\n const membersBefore = (hostId: string) => beforeOf.get(hostId)?.hostIds ?? [hostId]\n const membersAfter = (hostId: string) => afterOf.get(hostId)?.hostIds ?? [hostId]\n\n const sites = [\n ...new Set([\n ...Object.values(moving).flatMap((group) => group.hostIds),\n ...Object.values(after).flatMap((group) => group.hostIds),\n ]),\n ].sort(byString)\n\n const carries: ConsentGroupCarry[] = []\n for (const a of sites) {\n const stays = new Set(membersAfter(a))\n for (const b of membersBefore(a)) {\n if (b !== a && !stays.has(b)) carries.push({ toHostId: a, fromHostId: b })\n }\n }\n\n const flows: ConsentGroupHolderFlow[] = []\n const keys = [...new Set(sites.map(keyBefore))].sort(byString)\n for (const key of keys) {\n const covered = sites.filter((hostId) => keyBefore(hostId) === key)\n const targets = [...new Set(covered.map(keyAfter))].sort(byString)\n if (targets.length === 1 && targets[0] === key) continue\n if (targets.length === 1) {\n flows.push({\n kind: 'move',\n from: key,\n fromHostIds: covered,\n to: targets[0],\n toHostIds: [...membersAfter(covered[0])],\n })\n continue\n }\n flows.push({\n kind: 'split',\n from: key,\n fromHostIds: covered,\n survives: targets.includes(key),\n stayHostIds: covered.filter((hostId) => keyAfter(hostId) === key),\n successors: targets\n .filter((target) => target !== key)\n .map((target) => ({\n key: target,\n hostIds: covered.filter((hostId) => keyAfter(hostId) === target),\n })),\n })\n }\n\n const lines = planLines(before, after)\n const touched = new Set<string>()\n const beforeAll = groupOf(before)\n for (const hostId of new Set([...beforeAll.keys(), ...afterOf.keys()])) {\n const was = beforeAll.get(hostId)\n const now = afterOf.get(hostId)\n const sameMembers =\n (was?.hostIds ?? [hostId]).join('\\n') === (now?.hostIds ?? [hostId]).join('\\n')\n const sameName =\n !was || !now || before[was.id]?.name === after[now.id]?.name\n if (was?.id !== now?.id || !sameMembers || !sameName) touched.add(hostId)\n }\n return {\n before,\n after,\n carries,\n flows,\n lines,\n hostIds: [...touched].sort(byString),\n renameOnly:\n !carries.length &&\n !flows.length &&\n lines.length > 0 &&\n lines.every((line) => line.kind === 'renamed'),\n }\n}\n\n/** The declaration-level changes, in the order the activity log lists them. */\nfunction planLines(\n before: ConsentGroupDeclaration,\n after: ConsentGroupDeclaration,\n): ConsentGroupChangeLine[] {\n const beforeOf = groupOf(before)\n const afterOf = groupOf(after)\n const created: ConsentGroupChangeLine[] = []\n const renamed: ConsentGroupChangeLine[] = []\n const added: ConsentGroupChangeLine[] = []\n const removed: ConsentGroupChangeLine[] = []\n const moved: ConsentGroupChangeLine[] = []\n const dissolved: ConsentGroupChangeLine[] = []\n const moveInto = (hostId: string, toGroupId: string) => {\n const from = beforeOf.get(hostId)\n if (!from || from.id === toGroupId) return false\n moved.push({\n kind: 'moved',\n hostId,\n fromGroupId: from.id,\n fromName: before[from.id].name,\n toGroupId,\n toName: after[toGroupId].name,\n })\n return true\n }\n\n for (const id of Object.keys(after).sort(byString)) {\n const next = after[id]\n const previous = before[id]\n if (!previous) {\n created.push({ kind: 'created', groupId: id, name: next.name, hostIds: [...next.hostIds] })\n for (const hostId of next.hostIds) moveInto(hostId, id)\n continue\n }\n if (previous.name !== next.name) {\n renamed.push({ kind: 'renamed', groupId: id, from: previous.name, to: next.name })\n }\n for (const hostId of next.hostIds) {\n if (previous.hostIds.includes(hostId)) continue\n if (!moveInto(hostId, id)) {\n added.push({ kind: 'added', groupId: id, name: next.name, hostId })\n }\n }\n for (const hostId of previous.hostIds) {\n if (next.hostIds.includes(hostId)) continue\n // A site that went to another group is that group's `moved` line.\n if (!afterOf.has(hostId)) {\n removed.push({ kind: 'removed', groupId: id, name: next.name, hostId })\n }\n }\n }\n for (const id of Object.keys(before).sort(byString)) {\n if (!(id in after)) dissolved.push({ kind: 'dissolved', groupId: id, name: before[id].name })\n }\n return [...created, ...renamed, ...added, ...removed, ...moved, ...dissolved]\n}\n\n/**\n * The sites in a sentence: \"A\", \"A and B\", \"A, B and C\".\n */\nexport function listSiteNames(names: readonly string[]): string {\n if (names.length <= 1) return names[0] ?? ''\n return `${names.slice(0, -1).join(', ')} and ${names[names.length - 1]}`\n}\n\n/**\n * One line of the plan as the organization's activity log says it.\n *\n * `siteName` turns a site id into what the console shows for it; the id\n * itself is the fallback, so a line is never blank.\n */\nexport function describeConsentGroupChangeLine(\n line: ConsentGroupChangeLine,\n siteName: (hostId: string) => string = (hostId) => hostId,\n): string {\n const site = (hostId: string) => siteName(hostId) || hostId\n switch (line.kind) {\n case 'created':\n return `Created consent group \"${line.name}\" with ${listSiteNames(line.hostIds.map(site))}`\n case 'renamed':\n return `Renamed consent group \"${line.from}\" to \"${line.to}\"`\n case 'added':\n return `Added ${site(line.hostId)} to consent group \"${line.name}\"`\n case 'removed':\n return `Removed ${site(line.hostId)} from consent group \"${line.name}\"`\n case 'moved':\n return `Moved ${site(line.hostId)} from consent group \"${line.fromName}\" to \"${line.toName}\"`\n case 'dissolved':\n return `Dissolved consent group \"${line.name}\"`\n }\n}\n\n/*==========================================\n * THE CHANGE IN FLIGHT\n *=========================================*/\n\n/**\n * Where a change stands, on the org marker.\n *\n * - `carry` — copying refusals; the declaration in force is the old one,\n * and the change can still be canceled.\n * - `rehome` — the new declaration is in force; holder records are moving.\n * - `sweep` — waiting out {@link CONSENT_GROUP_SWEEP_DELAY_MS}, then one\n * last idempotent pass.\n */\nexport type ConsentGroupsChangePhase = 'carry' | 'rehome' | 'sweep'\n\n/** `org.consentGroupsChange` — present exactly while a change runs. */\nexport interface ConsentGroupsChangeMarker {\n changeId: string\n phase: ConsentGroupsChangePhase\n /** {@link ConsentGroupChangePlan.hostIds}. */\n hostIds: string[]\n startedAtMs: number\n /** When the new declaration took effect; absent before the flip. */\n declaredAtMs?: number\n}\n\nconst PHASES: readonly ConsentGroupsChangePhase[] = ['carry', 'rehome', 'sweep']\n\n/** The marker off an org document, or `null` when no change is in flight. */\nexport function readConsentGroupsChange(\n org: Record<string, unknown> | null | undefined,\n): ConsentGroupsChangeMarker | null {\n const raw = (org ?? {})[CONSENT_GROUPS_CHANGE_FIELD]\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null\n const value = raw as Record<string, unknown>\n const changeId = typeof value['changeId'] === 'string' ? value['changeId'] : ''\n const phase = PHASES.find((candidate) => candidate === value['phase'])\n if (!changeId || !phase) return null\n const declaredAtMs = Number(value['declaredAtMs'])\n return {\n changeId,\n phase,\n hostIds: Array.isArray(value['hostIds'])\n ? (value['hostIds'] as unknown[]).filter(\n (id): id is string => typeof id === 'string' && id !== '',\n )\n : [],\n startedAtMs: Number(value['startedAtMs']) || 0,\n ...(Number.isFinite(declaredAtMs) && declaredAtMs > 0 ? { declaredAtMs } : {}),\n }\n}\n\n/**\n * Whether the holder records `hostId` reads may be mid-move: a change in\n * flight has flipped the declaration and names this site.\n *\n * A reader that decides something by a record's ABSENCE — a dynamic list\n * that removes whoever no longer matches — must not decide it while this\n * holds, because a record that has not moved yet reads as missing.\n */\nexport function consentGroupHoldersInMotion(\n org: Record<string, unknown> | null | undefined,\n hostId: string,\n): boolean {\n const marker = readConsentGroupsChange(org)\n return Boolean(\n marker && marker.phase !== 'carry' && marker.hostIds.includes(hostId),\n )\n}\n\n/**\n * The executor's steps, in order.\n *\n * - `carry` — every carry, then every participant's carry phase.\n * - `catch-up-pre` — the refusals filed since the carry began.\n * - `declare` — the flip, in one transaction.\n * - `catch-up-post` — the same catch-up, for what raced the flip.\n * - `rehome` — every participant's holder records.\n * - `sweep-wait` — until {@link CONSENT_GROUP_SWEEP_DELAY_MS} has passed.\n * - `sweep` — every carry again in full, and every participant's sweep.\n * - `finish` — the marker cleared.\n *\n * A rename-only change runs `declare` and `finish` alone.\n */\nexport const CONSENT_GROUP_CHANGE_STEPS = [\n 'carry',\n 'catch-up-pre',\n 'declare',\n 'catch-up-post',\n 'rehome',\n 'sweep-wait',\n 'sweep',\n 'finish',\n] as const\n\nexport type ConsentGroupChangeStep = (typeof CONSENT_GROUP_CHANGE_STEPS)[number]\n\n/** The steps a rename-only change runs. */\nexport const CONSENT_GROUP_RENAME_STEPS: readonly ConsentGroupChangeStep[] = [\n 'declare',\n 'finish',\n]\n\n/** The marker phase a step runs under. */\nexport function consentGroupChangePhaseOf(\n step: ConsentGroupChangeStep,\n): ConsentGroupsChangePhase {\n if (step === 'carry' || step === 'catch-up-pre' || step === 'declare') return 'carry'\n if (step === 'catch-up-post' || step === 'rehome') return 'rehome'\n return 'sweep'\n}\n\nexport type ConsentGroupChangeStatus = 'running' | 'done' | 'canceled'\n\n/** What the refusal carries copied so far, across every step. */\nexport interface ConsentGroupChangeCounts {\n siteSuppressions: number\n topicOptOuts: number\n paces: number\n}\n\n/** A job's state, as the route answers it and the progress panel renders it. */\nexport interface ConsentGroupChangeProgress {\n status: ConsentGroupChangeStatus\n step: ConsentGroupChangeStep\n /** 1-based position of `step` among the steps this change runs. */\n stepIndex: number\n stepCount: number\n /** When the new declaration took effect, or `null` before the flip. */\n declaredAtMs: number | null\n /** When the sweep may start, or `null` before the flip. */\n sweepAtMs: number | null\n /** Five units in a row failed. The change keeps retrying. */\n stalled: boolean\n /** Consecutive failed units. */\n failures: number\n lastError: string | null\n /** Until when another caller holds the job; `null` when it is free. */\n leaseUntilMs: number | null\n counts: ConsentGroupChangeCounts\n /** The sites that received at least one carried refusal. */\n sitesReceiving: number\n /** Each participant's counts, by plugin id. */\n plugins: Record<string, Record<string, number>>\n}\n\n/*==========================================\n * THE ROUTE'S CONTRACT (`POST /api/orgs/consent-groups`)\n *=========================================*/\n\nexport type ConsentGroupsApiAction = 'preview' | 'apply' | 'continue' | 'cancel' | 'status'\n\nexport interface ConsentGroupsApiDeclarationRequest {\n orgId: string\n action: 'preview' | 'apply'\n /**\n * `org.consentGroups` exactly as the editor rendered it, or `null` for an\n * organization with none. Compared by {@link consentGroupsFingerprint}.\n */\n expected: Record<string, unknown> | null\n /** The COMPLETE next declaration. A new group has no id. */\n groups: ConsentGroupDraft[]\n}\n\nexport interface ConsentGroupsApiChangeRequest {\n orgId: string\n action: 'continue' | 'cancel' | 'status'\n changeId: string\n}\n\nexport type ConsentGroupsApiRequest =\n | ConsentGroupsApiDeclarationRequest\n | ConsentGroupsApiChangeRequest\n\n/** 200 for `preview`. */\nexport interface ConsentGroupsApiPreviewResponse {\n ok: true\n preview: ConsentGroupChangePreview\n}\n\n/** 200 for `apply`, `continue`, `cancel` and `status`. */\nexport interface ConsentGroupsApiChangeResponse {\n ok: true\n changeId: string\n phase: ConsentGroupsChangePhase | 'done' | 'canceled'\n done: boolean\n progress: ConsentGroupChangeProgress\n}\n\n/** 400: the declaration was refused. */\nexport interface ConsentGroupsApiRefusal {\n error: string\n errors: ConsentGroupValidationError[]\n}\n\n/** 409: the declaration changed since the editor rendered it. */\nexport interface ConsentGroupsApiStale {\n error: string\n current: Record<string, unknown> | null\n}\n\n/** 409: another change is running, or a cancel came after the flip. */\nexport interface ConsentGroupsApiInFlight {\n error: string\n changeId: string\n phase: ConsentGroupsChangePhase | 'done' | 'canceled'\n}\n\n/*==========================================\n * THE PREVIEW (`action: 'preview'`)\n *=========================================*/\n\n/**\n * One sentence a participant adds to the review step, with the figure it is\n * about. `id` is namespaced by the participant (`crm.combine`).\n */\nexport interface ConsentGroupChangePreviewLine {\n id: string\n text: string\n count: number | null\n severity: 'info' | 'warning'\n}\n\n/** How long a change should take, from the work the preview counted. */\nexport type ConsentGroupChangeEstimate = 'instant' | 'under-a-minute' | 'minutes' | 'long'\n\n/** The work bands behind {@link ConsentGroupChangeEstimate}. */\nexport function estimateConsentGroupChange(\n plan: Pick<ConsentGroupChangePlan, 'renameOnly'>,\n documents: number,\n): ConsentGroupChangeEstimate {\n if (plan.renameOnly) return 'instant'\n if (documents < 5_000) return 'under-a-minute'\n if (documents < 100_000) return 'minutes'\n return 'long'\n}\n\nexport interface ConsentGroupChangePreview {\n before: ConsentGroupDeclaration\n after: ConsentGroupDeclaration\n /** Stored ids `readConsentGroups` drops; the write removes them. */\n discarded: string[]\n /** The plan's lines, each with the sentence the activity log will write. */\n lines: Array<ConsentGroupChangeLine & { text: string }>\n /** Every group whose signup-form sentence changes. */\n disclosures: Array<{\n groupId: string\n hostIds: string[]\n before: string | null\n after: string | null\n }>\n /** Per carry, what the source site holds now (count aggregations). */\n carries: Array<{\n toHostId: string\n fromHostId: string\n siteSuppressions: number\n topicOptOuts: number\n paces: number\n }>\n /** Per joining site, the refusals on its new siblings it starts honoring. */\n inherited: Array<{ hostId: string; refusals: number }>\n /**\n * Pending confirmations that stop holding a separated site's mail. Only\n * when the organization's confirmation switch is on.\n */\n pendingHolds: Array<{\n hostId: string\n topicId: string\n count: number\n releasedHostIds: string[]\n }>\n /** Team members who can open only some of a changed group's sites. */\n partialAccessMembers: number\n /**\n * Set when the organization's consent policy is `forward` and a site joins:\n * the joining sites may email people their new siblings captured before\n * `enforceFromMs` without a recorded opt-in.\n */\n forwardPolicyWarning: { hostIds: string[]; enforceFromMs: number } | null\n /** Each participant's lines; `null` for one whose preview failed. */\n participants: Array<{\n pluginId: string\n lines: ConsentGroupChangePreviewLine[] | null\n }>\n /** The capture surfaces that show a group's disclosure. */\n capturesDisclosing: string[]\n estimate: ConsentGroupChangeEstimate\n}\n\n/** The groups whose disclosure sentence differs between the declarations. */\nexport function consentGroupDisclosureChanges(\n before: ConsentGroupDeclaration,\n after: ConsentGroupDeclaration,\n): ConsentGroupChangePreview['disclosures'] {\n const sentence = (id: string, group: StoredConsentGroup | undefined) =>\n group\n ? consentGroupDisclosure({\n hostId: group.hostIds[0],\n groupId: id,\n name: group.name,\n hostIds: group.hostIds,\n declared: true,\n awaitsConfirmation: false,\n })\n : null\n const out: ConsentGroupChangePreview['disclosures'] = []\n for (const id of [...new Set([...Object.keys(before), ...Object.keys(after)])].sort(byString)) {\n const was = sentence(id, before[id])\n const now = sentence(id, after[id])\n if (was === now) continue\n out.push({\n groupId: id,\n hostIds: [...(after[id] ?? before[id]).hostIds],\n before: was,\n after: now,\n })\n }\n return out\n}\n"],"names":["consentGroupDisclosure","CONSENT_GROUPS_FIELD","MAX_CONSENT_GROUP_HOSTS","readConsentGroups","CONSENT_GROUPS_CHANGE_FIELD","CONSENT_GROUP_CHANGES_COLLECTION","CONSENT_GROUP_NAME_MAX","CONSENT_GROUP_SWEEP_DELAY_MS","CONSENT_GROUP_ID_PREFIX","orgSiteIds","org","hosts","ids","Array","isArray","map","id","String","Object","entries","filter","value","Set","trim","Boolean","sort","discardedConsentGroupIds","raw","usable","keys","BASE36","defaultRandom","crypto","globalThis","getRandomValues","word","Uint32Array","Math","random","mintConsentGroupId","taken","index","min","floor","has","canonicalGroup","group","name","hostIds","consentGroupsEqual","a","b","consentGroupsFingerprint","length","JSON","stringify","validateConsentGroupDeclaration","input","before","groups","ok","errors","code","sites","siteIds","names","claimed","drafts","forEach","groupIndex","push","entry","toLowerCase","add","hostId","rawId","some","includes","reserved","after","draft","groupOf","declaration","Map","set","byString","withSitesOnly","keep","out","planConsentGroupChange","options","owned","moving","beforeOf","afterOf","keyBefore","get","keyAfter","membersBefore","membersAfter","values","flatMap","carries","stays","toHostId","fromHostId","flows","key","covered","targets","kind","from","fromHostIds","to","toHostIds","survives","stayHostIds","successors","target","lines","planLines","touched","beforeAll","was","now","sameMembers","join","sameName","renameOnly","every","line","created","renamed","added","removed","moved","dissolved","moveInto","toGroupId","fromGroupId","fromName","toName","next","previous","groupId","listSiteNames","slice","describeConsentGroupChangeLine","siteName","site","PHASES","readConsentGroupsChange","changeId","phase","find","candidate","declaredAtMs","Number","startedAtMs","isFinite","consentGroupHoldersInMotion","marker","CONSENT_GROUP_CHANGE_STEPS","CONSENT_GROUP_RENAME_STEPS","consentGroupChangePhaseOf","step","estimateConsentGroupChange","plan","documents","consentGroupDisclosureChanges","sentence","declared","awaitsConfirmation"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/consent-group-change.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 * CHANGING WHICH SITES ARE ONE SENDER (AGL-3320) — the pure half.\n *\n * `consent-groups.ts` reads a declaration; this is what a change to one\n * means. It validates the next declaration an admin submits, decides what has\n * to move for the sites to stop or start being one sender, and words the\n * result — and it does all of that without a read, so the console's review\n * step, the route's refusal and the executor's plan are one computation.\n *\n * ## Why a change is a plan and not a write\n *\n * A declaration is read at send time, so flipping it is instant — and that is\n * exactly the problem. Two facts are stored per SITE and read across the\n * CURRENT group: a refusal (a suppression row, a topic opt-out, a pace, a\n * declined consent) and a holder's business record (the CRM facet keyed by\n * the group). When sites stop being one sender, each stops reading the\n * other's refusals; when they become one, the records they kept apart must\n * become one. The flip itself moves neither, so every change is:\n *\n * 1. CARRY the refusals onto the sites that are about to stop reading them,\n * in BOTH directions — the person who left one site of a sender was told\n * the opt-out covers all of them, so the site leaving keeps what it\n * honored and the sites staying keep what the leaver honored;\n * 2. FLIP the declaration;\n * 3. RE-HOME the holder records keyed by the old group ids.\n *\n * {@link planConsentGroupChange} says which pairs carry and which holder keys\n * move or split; the executor in `libs/tenant/data/admin` performs it.\n *\n * ## What the planner never touches\n *\n * Grants. A grant is written forward, one entry per disclosed site, and is\n * evidence of what a person was told, so a change never copies, re-keys or\n * revokes one — see `consent-groups.ts`, \"Pooling applies FORWARD only\".\n */\n\nimport {\n consentGroupDisclosure,\n CONSENT_GROUPS_FIELD,\n MAX_CONSENT_GROUP_HOSTS,\n readConsentGroups,\n type StoredConsentGroup,\n} from './consent-groups'\n\n/**\n * The org field that marks a change in flight:\n * {@link ConsentGroupsChangeMarker}. Authoritative — the job document holds\n * the detail, and this says whether there is a job at all.\n *\n * On the org document, beside the declaration it guards, so the transaction\n * that starts a change and the one that flips the declaration read one\n * document. Server-owned: denied to every client by the rules.\n */\nexport const CONSENT_GROUPS_CHANGE_FIELD = 'consentGroupsChange'\n\n/** `orgs/{orgId}/consentGroupChanges/{changeId}` — one job per change. */\nexport const CONSENT_GROUP_CHANGES_COLLECTION = 'consentGroupChanges'\n\n/**\n * The longest group name a declaration may carry.\n *\n * The name is rendered on every signup form of the group, inside a sentence,\n * so it is a brand and not a description.\n */\nexport const CONSENT_GROUP_NAME_MAX = 80\n\n/**\n * How long after the flip the sweep waits before its final pass.\n *\n * A long job — a campaign batch, a dynamic list, an outreach tick — resolves\n * the group once per batch and runs for at most five minutes, so a write it\n * made against the OLD declaration can land after the flip. Six minutes is\n * past the longest of them.\n */\nexport const CONSENT_GROUP_SWEEP_DELAY_MS = 6 * 60_000\n\n/**\n * What every minted group id starts with.\n *\n * A group id is a holder key beside the site ids a solo site is keyed by, so\n * the two must never meet. Firestore's automatic ids — which every site id is\n * — are alphanumeric, and an underscore cannot occur in one.\n */\nexport const CONSENT_GROUP_ID_PREFIX = 'cg_'\n\n/** Every group of a declaration, by id — what `readConsentGroups` answers. */\nexport type ConsentGroupDeclaration = Record<string, StoredConsentGroup>\n\n/** One group of the next declaration as the editor submits it. */\nexport interface ConsentGroupDraft {\n /** The id of a group that exists; absent for a new group. */\n id?: string | null\n name: string\n hostIds: string[]\n}\n\n/**\n * Why a submitted declaration was refused. Every code names something the\n * admin can fix in the editor; `groupIndex` points at the draft it is about,\n * and `hostId` at the site.\n */\nexport type ConsentGroupValidationCode =\n /** Not a list of `{ name, hostIds }` objects. */\n | 'malformed'\n | 'name-empty'\n | 'name-too-long'\n /** Two groups with the same name, ignoring case. */\n | 'name-duplicate'\n | 'too-few-sites'\n | 'too-many-sites'\n /** A site this organization does not own. */\n | 'unknown-site'\n | 'site-in-two-groups'\n /** An id no group of the current declaration has. */\n | 'unknown-group'\n /** One id on two drafts. */\n | 'duplicate-group'\n /**\n * A kept id whose sites are ALL new. Keeping the id would carry the old\n * group's records onto a set of sites none of which kept them; a new group\n * is what that is.\n */\n | 'group-replaced'\n /** The declaration submitted is the one in force. */\n | 'no-change'\n\nexport interface ConsentGroupValidationError {\n code: ConsentGroupValidationCode\n groupIndex?: number\n hostId?: string\n}\n\nexport type ConsentGroupValidation =\n | { ok: true; after: ConsentGroupDeclaration }\n | { ok: false; errors: ConsentGroupValidationError[] }\n\n/**\n * The sites an organization owns, from its `hosts` map, sorted.\n *\n * The map is `{ [hostId]: true }`; an array is read too, so a hand-built\n * fixture and the stored shape agree.\n */\nexport function orgSiteIds(\n org: Record<string, unknown> | null | undefined,\n): string[] {\n const hosts = (org ?? {})['hosts']\n const ids = Array.isArray(hosts)\n ? hosts.map((id) => String(id ?? ''))\n : hosts && typeof hosts === 'object'\n ? Object.entries(hosts as Record<string, unknown>)\n .filter(([, value]) => value !== false && value != null)\n .map(([id]) => id)\n : []\n return [...new Set(ids.map((id) => id.trim()).filter(Boolean))].sort()\n}\n\n/** The raw declaration's ids that `readConsentGroups` drops, sorted. */\nexport function discardedConsentGroupIds(\n org: Record<string, unknown> | null | undefined,\n): string[] {\n const raw = (org ?? {})[CONSENT_GROUPS_FIELD]\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return []\n const usable = readConsentGroups(org)\n return Object.keys(raw as Record<string, unknown>)\n .filter((id) => !(id in usable))\n .sort()\n}\n\nconst BASE36 = '0123456789abcdefghijklmnopqrstuvwxyz'\n\n/** Uniform in [0, 1), from the platform's CSPRNG where there is one. */\nfunction defaultRandom(): number {\n const crypto = (globalThis as { crypto?: { getRandomValues?: <T>(array: T) => T } })\n .crypto\n if (crypto?.getRandomValues) {\n const word = crypto.getRandomValues(new Uint32Array(1))[0]\n return word / 2 ** 32\n }\n return Math.random()\n}\n\n/**\n * A fresh group id: {@link CONSENT_GROUP_ID_PREFIX} and sixteen base-36\n * characters, never one in `taken`.\n *\n * `taken` holds every id the id must not equal — the current groups', the\n * raw declaration's, every site's and every id already minted for the same\n * declaration. Eighty bits make a collision a formality, and the loop makes it\n * impossible.\n */\nexport function mintConsentGroupId(\n taken: ReadonlySet<string>,\n random: () => number = defaultRandom,\n): string {\n for (;;) {\n let id = CONSENT_GROUP_ID_PREFIX\n for (let index = 0; index < 16; index += 1) {\n id += BASE36[Math.min(35, Math.floor(random() * 36))]\n }\n if (!taken.has(id)) return id\n }\n}\n\n/** A group's canonical form: its name trimmed, its sites unique and sorted. */\nfunction canonicalGroup(group: unknown): { name: string; hostIds: string[] } {\n const value = (group && typeof group === 'object' ? group : {}) as Record<string, unknown>\n const name = typeof value['name'] === 'string' ? value['name'].trim() : ''\n const hostIds = Array.isArray(value['hostIds'])\n ? [\n ...new Set(\n (value['hostIds'] as unknown[])\n .map((id) => String(id ?? '').trim())\n .filter(Boolean),\n ),\n ].sort()\n : []\n return { name, hostIds }\n}\n\n/**\n * Whether two declarations say the same thing: the same ids, each with the\n * same trimmed name and the same set of sites. Order never matters.\n */\nexport function consentGroupsEqual(\n a: Record<string, unknown> | null | undefined,\n b: Record<string, unknown> | null | undefined,\n): boolean {\n return consentGroupsFingerprint(a) === consentGroupsFingerprint(b)\n}\n\n/**\n * A declaration as one canonical string: what \"the value the editor\n * rendered\" and \"the value stored now\" are compared by.\n *\n * Every entry is kept, usable or not, because the question is whether the\n * stored value CHANGED, and an edit to an entry `readConsentGroups` drops is\n * still somebody else's edit. Absent, `null` and `{}` are one value — an\n * organization with no groups.\n */\nexport function consentGroupsFingerprint(raw: unknown): string {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return 'null'\n const entries = Object.entries(raw as Record<string, unknown>)\n .map(([id, group]) => [id, canonicalGroup(group)] as const)\n .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))\n return entries.length ? JSON.stringify(entries) : 'null'\n}\n\n/**\n * The next declaration, checked against the organization's sites and the\n * declaration in force, with an id minted for every new group.\n *\n * Refuses everything in the list on {@link ConsentGroupValidationCode}, and\n * reports every problem it finds rather than the first, so the editor can\n * mark each draft at once.\n */\nexport function validateConsentGroupDeclaration(input: {\n groups: unknown\n /** The declaration in force: `readConsentGroups(org)`. */\n before: ConsentGroupDeclaration\n /** The organization's sites: {@link orgSiteIds}. */\n siteIds: readonly string[]\n /** Ids a minted id must also avoid — the raw declaration's, for one. */\n reserved?: Iterable<string>\n random?: () => number\n}): ConsentGroupValidation {\n const { before } = input\n if (!Array.isArray(input.groups)) {\n return { ok: false, errors: [{ code: 'malformed' }] }\n }\n const sites = new Set(input.siteIds)\n const errors: ConsentGroupValidationError[] = []\n const names = new Set<string>()\n const ids = new Set<string>()\n const claimed = new Set<string>()\n const drafts: Array<{ id: string | null; name: string; hostIds: string[] }> = []\n\n input.groups.forEach((value, groupIndex) => {\n if (!value || typeof value !== 'object' || Array.isArray(value)) {\n errors.push({ code: 'malformed', groupIndex })\n return\n }\n const entry = value as Record<string, unknown>\n if (typeof entry['name'] !== 'string' || !Array.isArray(entry['hostIds'])) {\n errors.push({ code: 'malformed', groupIndex })\n return\n }\n const { name, hostIds } = canonicalGroup(entry)\n if (!name) errors.push({ code: 'name-empty', groupIndex })\n else if (name.length > CONSENT_GROUP_NAME_MAX) {\n errors.push({ code: 'name-too-long', groupIndex })\n } else if (names.has(name.toLowerCase())) {\n errors.push({ code: 'name-duplicate', groupIndex })\n }\n if (name) names.add(name.toLowerCase())\n\n for (const hostId of hostIds) {\n if (!sites.has(hostId)) {\n errors.push({ code: 'unknown-site', groupIndex, hostId })\n } else if (claimed.has(hostId)) {\n errors.push({ code: 'site-in-two-groups', groupIndex, hostId })\n }\n claimed.add(hostId)\n }\n if (hostIds.length < 2) errors.push({ code: 'too-few-sites', groupIndex })\n else if (hostIds.length > MAX_CONSENT_GROUP_HOSTS) {\n errors.push({ code: 'too-many-sites', groupIndex })\n }\n\n const rawId = entry['id']\n const id = typeof rawId === 'string' && rawId.trim() ? rawId.trim() : null\n if (id !== null) {\n if (ids.has(id)) errors.push({ code: 'duplicate-group', groupIndex })\n else if (!(id in before)) errors.push({ code: 'unknown-group', groupIndex })\n else if (!before[id].hostIds.some((hostId) => hostIds.includes(hostId))) {\n errors.push({ code: 'group-replaced', groupIndex })\n }\n ids.add(id)\n }\n drafts.push({ id, name, hostIds })\n })\n if (errors.length) return { ok: false, errors }\n\n const taken = new Set<string>([\n ...Object.keys(before),\n ...input.siteIds,\n ...(input.reserved ?? []),\n ...ids,\n ])\n const after: ConsentGroupDeclaration = {}\n for (const draft of drafts) {\n const id = draft.id ?? mintConsentGroupId(taken, input.random)\n taken.add(id)\n after[id] = { name: draft.name, hostIds: draft.hostIds }\n }\n if (consentGroupsEqual(before, after)) {\n return { ok: false, errors: [{ code: 'no-change' }] }\n }\n return { ok: true, after }\n}\n\n/*==========================================\n * THE PLAN\n *=========================================*/\n\n/**\n * One direction of a carry: `toHostId` is about to stop reading\n * `fromHostId`'s refusals, so they are copied onto it first.\n */\nexport interface ConsentGroupCarry {\n toHostId: string\n fromHostId: string\n}\n\n/**\n * Every site a holder key covered moves under ONE new key: the key's records\n * are folded into the new key's, and the old key is deleted in the same\n * write.\n */\nexport interface ConsentGroupHolderMove {\n kind: 'move'\n /** The key the records are under now: a group id, or a solo site's id. */\n from: string\n /** The sites `from` covered. */\n fromHostIds: string[]\n /** The key they move under: always a declared group of the next declaration. */\n to: string\n /** The sites of `to` in the next declaration. */\n toHostIds: string[]\n}\n\n/**\n * The sites a holder key covered go separate ways. Each successor may take a\n * copy of the key's records; the key itself survives when some of its sites\n * stay under it, and is deleted when none do.\n */\nexport interface ConsentGroupHolderSplit {\n kind: 'split'\n from: string\n fromHostIds: string[]\n /** Whether `from` is still a key afterwards. */\n survives: boolean\n /** The sites of `from` that stay under it. Empty when it does not survive. */\n stayHostIds: string[]\n /**\n * Where the other sites go: each successor key with the sites of `from`\n * that map to it — never the successor's other sites, whose records were\n * never under `from`.\n */\n successors: Array<{ key: string; hostIds: string[] }>\n}\n\nexport type ConsentGroupHolderFlow = ConsentGroupHolderMove | ConsentGroupHolderSplit\n\n/** What a change does to the declaration, as the activity log words it. */\nexport type ConsentGroupChangeLine =\n | { kind: 'created'; groupId: string; name: string; hostIds: string[] }\n | { kind: 'renamed'; groupId: string; from: string; to: string }\n | { kind: 'added'; groupId: string; name: string; hostId: string }\n | { kind: 'removed'; groupId: string; name: string; hostId: string }\n | {\n kind: 'moved'\n hostId: string\n fromGroupId: string\n fromName: string\n toGroupId: string\n toName: string\n }\n /** Includes a group absorbed into another: its sites each get a `moved`. */\n | { kind: 'dissolved'; groupId: string; name: string }\n\nexport interface ConsentGroupChangePlan {\n before: ConsentGroupDeclaration\n after: ConsentGroupDeclaration\n carries: ConsentGroupCarry[]\n flows: ConsentGroupHolderFlow[]\n lines: ConsentGroupChangeLine[]\n /**\n * Every site the change touches — whose group, members or group name\n * changes. What the marker names while the change runs: the sites that\n * may not be deleted, and whose holder records may be in motion.\n */\n hostIds: string[]\n /** Only names change: nothing to carry and nothing to re-home. */\n renameOnly: boolean\n}\n\nfunction groupOf(\n declaration: ConsentGroupDeclaration,\n): Map<string, { id: string; hostIds: string[] }> {\n const map = new Map<string, { id: string; hostIds: string[] }>()\n for (const [id, group] of Object.entries(declaration)) {\n for (const hostId of group.hostIds) map.set(hostId, { id, hostIds: group.hostIds })\n }\n return map\n}\n\nconst byString = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0)\n\n/** `declaration` without the sites `keep` does not name. */\nfunction withSitesOnly(\n declaration: ConsentGroupDeclaration,\n keep: ReadonlySet<string>,\n): ConsentGroupDeclaration {\n const out: ConsentGroupDeclaration = {}\n for (const [id, group] of Object.entries(declaration)) {\n const hostIds = group.hostIds.filter((hostId) => keep.has(hostId))\n if (hostIds.length) out[id] = { name: group.name, hostIds }\n }\n return out\n}\n\n/**\n * What moving from `before` to `after` requires.\n *\n * `key(s)` is the holder key of site `s` — its group's id, or `s` itself when\n * it is alone — and `members(s)` the sites of that group, or `[s]`.\n *\n * - **Carries.** For every pair a ≠ b that are one sender now and will not\n * be, `b`'s refusals are copied onto `a`. Pairs that stay together, and\n * pairs that come together, need nothing: a joined site reads its new\n * siblings' refusals the moment the declaration names it.\n * - **Holder flows.** For each key `k` in force, the sites it covers map to\n * a set `T` of keys afterwards. `T = {k}` needs nothing; one other key is\n * a MOVE; anything else is a SPLIT, which `k` survives when it is in `T`.\n *\n * `siteIds`, when given, are the sites the organization still owns: a site\n * named by `before` that is gone neither receives a carry nor takes a copy of\n * a record, because there is nothing left to hold either.\n */\nexport function planConsentGroupChange(\n before: ConsentGroupDeclaration,\n after: ConsentGroupDeclaration,\n options: { siteIds?: readonly string[] } = {},\n): ConsentGroupChangePlan {\n const owned = options.siteIds ? new Set(options.siteIds) : null\n const moving = owned ? withSitesOnly(before, owned) : before\n const beforeOf = groupOf(moving)\n const afterOf = groupOf(after)\n const keyBefore = (hostId: string) => beforeOf.get(hostId)?.id ?? hostId\n const keyAfter = (hostId: string) => afterOf.get(hostId)?.id ?? hostId\n const membersBefore = (hostId: string) => beforeOf.get(hostId)?.hostIds ?? [hostId]\n const membersAfter = (hostId: string) => afterOf.get(hostId)?.hostIds ?? [hostId]\n\n const sites = [\n ...new Set([\n ...Object.values(moving).flatMap((group) => group.hostIds),\n ...Object.values(after).flatMap((group) => group.hostIds),\n ]),\n ].sort(byString)\n\n const carries: ConsentGroupCarry[] = []\n for (const a of sites) {\n const stays = new Set(membersAfter(a))\n for (const b of membersBefore(a)) {\n if (b !== a && !stays.has(b)) carries.push({ toHostId: a, fromHostId: b })\n }\n }\n\n const flows: ConsentGroupHolderFlow[] = []\n const keys = [...new Set(sites.map(keyBefore))].sort(byString)\n for (const key of keys) {\n const covered = sites.filter((hostId) => keyBefore(hostId) === key)\n const targets = [...new Set(covered.map(keyAfter))].sort(byString)\n if (targets.length === 1 && targets[0] === key) continue\n if (targets.length === 1) {\n flows.push({\n kind: 'move',\n from: key,\n fromHostIds: covered,\n to: targets[0],\n toHostIds: [...membersAfter(covered[0])],\n })\n continue\n }\n flows.push({\n kind: 'split',\n from: key,\n fromHostIds: covered,\n survives: targets.includes(key),\n stayHostIds: covered.filter((hostId) => keyAfter(hostId) === key),\n successors: targets\n .filter((target) => target !== key)\n .map((target) => ({\n key: target,\n hostIds: covered.filter((hostId) => keyAfter(hostId) === target),\n })),\n })\n }\n\n const lines = planLines(before, after)\n const touched = new Set<string>()\n const beforeAll = groupOf(before)\n for (const hostId of new Set([...beforeAll.keys(), ...afterOf.keys()])) {\n const was = beforeAll.get(hostId)\n const now = afterOf.get(hostId)\n const sameMembers =\n (was?.hostIds ?? [hostId]).join('\\n') === (now?.hostIds ?? [hostId]).join('\\n')\n const sameName =\n !was || !now || before[was.id]?.name === after[now.id]?.name\n if (was?.id !== now?.id || !sameMembers || !sameName) touched.add(hostId)\n }\n return {\n before,\n after,\n carries,\n flows,\n lines,\n hostIds: [...touched].sort(byString),\n renameOnly:\n !carries.length &&\n !flows.length &&\n lines.length > 0 &&\n lines.every((line) => line.kind === 'renamed'),\n }\n}\n\n/** The declaration-level changes, in the order the activity log lists them. */\nfunction planLines(\n before: ConsentGroupDeclaration,\n after: ConsentGroupDeclaration,\n): ConsentGroupChangeLine[] {\n const beforeOf = groupOf(before)\n const afterOf = groupOf(after)\n const created: ConsentGroupChangeLine[] = []\n const renamed: ConsentGroupChangeLine[] = []\n const added: ConsentGroupChangeLine[] = []\n const removed: ConsentGroupChangeLine[] = []\n const moved: ConsentGroupChangeLine[] = []\n const dissolved: ConsentGroupChangeLine[] = []\n const moveInto = (hostId: string, toGroupId: string) => {\n const from = beforeOf.get(hostId)\n if (!from || from.id === toGroupId) return false\n moved.push({\n kind: 'moved',\n hostId,\n fromGroupId: from.id,\n fromName: before[from.id].name,\n toGroupId,\n toName: after[toGroupId].name,\n })\n return true\n }\n\n for (const id of Object.keys(after).sort(byString)) {\n const next = after[id]\n const previous = before[id]\n if (!previous) {\n created.push({ kind: 'created', groupId: id, name: next.name, hostIds: [...next.hostIds] })\n for (const hostId of next.hostIds) moveInto(hostId, id)\n continue\n }\n if (previous.name !== next.name) {\n renamed.push({ kind: 'renamed', groupId: id, from: previous.name, to: next.name })\n }\n for (const hostId of next.hostIds) {\n if (previous.hostIds.includes(hostId)) continue\n if (!moveInto(hostId, id)) {\n added.push({ kind: 'added', groupId: id, name: next.name, hostId })\n }\n }\n for (const hostId of previous.hostIds) {\n if (next.hostIds.includes(hostId)) continue\n // A site that went to another group is that group's `moved` line.\n if (!afterOf.has(hostId)) {\n removed.push({ kind: 'removed', groupId: id, name: next.name, hostId })\n }\n }\n }\n for (const id of Object.keys(before).sort(byString)) {\n if (!(id in after)) dissolved.push({ kind: 'dissolved', groupId: id, name: before[id].name })\n }\n return [...created, ...renamed, ...added, ...removed, ...moved, ...dissolved]\n}\n\n/**\n * The sites in a sentence: \"A\", \"A and B\", \"A, B and C\".\n */\nexport function listSiteNames(names: readonly string[]): string {\n if (names.length <= 1) return names[0] ?? ''\n return `${names.slice(0, -1).join(', ')} and ${names[names.length - 1]}`\n}\n\n/**\n * One line of the plan as the organization's activity log says it.\n *\n * `siteName` turns a site id into what the console shows for it; the id\n * itself is the fallback, so a line is never blank.\n */\nexport function describeConsentGroupChangeLine(\n line: ConsentGroupChangeLine,\n siteName: (hostId: string) => string = (hostId) => hostId,\n): string {\n const site = (hostId: string) => siteName(hostId) || hostId\n switch (line.kind) {\n case 'created':\n return `Created consent group \"${line.name}\" with ${listSiteNames(line.hostIds.map(site))}`\n case 'renamed':\n return `Renamed consent group \"${line.from}\" to \"${line.to}\"`\n case 'added':\n return `Added ${site(line.hostId)} to consent group \"${line.name}\"`\n case 'removed':\n return `Removed ${site(line.hostId)} from consent group \"${line.name}\"`\n case 'moved':\n return `Moved ${site(line.hostId)} from consent group \"${line.fromName}\" to \"${line.toName}\"`\n case 'dissolved':\n return `Dissolved consent group \"${line.name}\"`\n }\n}\n\n/*==========================================\n * THE CHANGE IN FLIGHT\n *=========================================*/\n\n/**\n * Where a change stands, on the org marker.\n *\n * - `carry` — copying refusals; the declaration in force is the old one,\n * and the change can still be canceled.\n * - `rehome` — the new declaration is in force; holder records are moving.\n * - `sweep` — waiting out {@link CONSENT_GROUP_SWEEP_DELAY_MS}, then one\n * last idempotent pass.\n */\nexport type ConsentGroupsChangePhase = 'carry' | 'rehome' | 'sweep'\n\n/** `org.consentGroupsChange` — present exactly while a change runs. */\nexport interface ConsentGroupsChangeMarker {\n changeId: string\n phase: ConsentGroupsChangePhase\n /** {@link ConsentGroupChangePlan.hostIds}. */\n hostIds: string[]\n startedAtMs: number\n /** When the new declaration took effect; absent before the flip. */\n declaredAtMs?: number\n}\n\nconst PHASES: readonly ConsentGroupsChangePhase[] = ['carry', 'rehome', 'sweep']\n\n/** The marker off an org document, or `null` when no change is in flight. */\nexport function readConsentGroupsChange(\n org: Record<string, unknown> | null | undefined,\n): ConsentGroupsChangeMarker | null {\n const raw = (org ?? {})[CONSENT_GROUPS_CHANGE_FIELD]\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null\n const value = raw as Record<string, unknown>\n const changeId = typeof value['changeId'] === 'string' ? value['changeId'] : ''\n const phase = PHASES.find((candidate) => candidate === value['phase'])\n if (!changeId || !phase) return null\n const declaredAtMs = Number(value['declaredAtMs'])\n return {\n changeId,\n phase,\n hostIds: Array.isArray(value['hostIds'])\n ? (value['hostIds'] as unknown[]).filter(\n (id): id is string => typeof id === 'string' && id !== '',\n )\n : [],\n startedAtMs: Number(value['startedAtMs']) || 0,\n ...(Number.isFinite(declaredAtMs) && declaredAtMs > 0 ? { declaredAtMs } : {}),\n }\n}\n\n/**\n * Whether the holder records `hostId` reads may be mid-move: a change in\n * flight has flipped the declaration and names this site.\n *\n * A reader that decides something by a record's ABSENCE — a dynamic list\n * that removes whoever no longer matches — must not decide it while this\n * holds, because a record that has not moved yet reads as missing.\n */\nexport function consentGroupHoldersInMotion(\n org: Record<string, unknown> | null | undefined,\n hostId: string,\n): boolean {\n const marker = readConsentGroupsChange(org)\n return Boolean(\n marker && marker.phase !== 'carry' && marker.hostIds.includes(hostId),\n )\n}\n\n/**\n * The executor's steps, in order.\n *\n * - `carry` — every carry, then every participant's carry phase.\n * - `catch-up-pre` — the refusals filed since the carry began.\n * - `declare` — the flip, in one transaction.\n * - `catch-up-post` — the same catch-up, for what raced the flip.\n * - `rehome` — every participant's holder records.\n * - `sweep-wait` — until {@link CONSENT_GROUP_SWEEP_DELAY_MS} has passed.\n * - `sweep` — every carry again in full, and every participant's sweep.\n * - `finish` — the marker cleared.\n *\n * A rename-only change runs `declare` and `finish` alone.\n */\nexport const CONSENT_GROUP_CHANGE_STEPS = [\n 'carry',\n 'catch-up-pre',\n 'declare',\n 'catch-up-post',\n 'rehome',\n 'sweep-wait',\n 'sweep',\n 'finish',\n] as const\n\nexport type ConsentGroupChangeStep = (typeof CONSENT_GROUP_CHANGE_STEPS)[number]\n\n/** The steps a rename-only change runs. */\nexport const CONSENT_GROUP_RENAME_STEPS: readonly ConsentGroupChangeStep[] = [\n 'declare',\n 'finish',\n]\n\n/** The marker phase a step runs under. */\nexport function consentGroupChangePhaseOf(\n step: ConsentGroupChangeStep,\n): ConsentGroupsChangePhase {\n if (step === 'carry' || step === 'catch-up-pre' || step === 'declare') return 'carry'\n if (step === 'catch-up-post' || step === 'rehome') return 'rehome'\n return 'sweep'\n}\n\nexport type ConsentGroupChangeStatus = 'running' | 'done' | 'canceled'\n\n/** What the refusal carries copied so far, across every step. */\nexport interface ConsentGroupChangeCounts {\n siteSuppressions: number\n topicOptOuts: number\n paces: number\n /** Refusals kept after their record was deleted, in `retainedRefusals` (AGL-3338). */\n retainedRefusals: number\n}\n\n/** A job's state, as the route answers it and the progress panel renders it. */\nexport interface ConsentGroupChangeProgress {\n status: ConsentGroupChangeStatus\n step: ConsentGroupChangeStep\n /** 1-based position of `step` among the steps this change runs. */\n stepIndex: number\n stepCount: number\n /** When the new declaration took effect, or `null` before the flip. */\n declaredAtMs: number | null\n /** When the sweep may start, or `null` before the flip. */\n sweepAtMs: number | null\n /** Five units in a row failed. The change keeps retrying. */\n stalled: boolean\n /** Consecutive failed units. */\n failures: number\n lastError: string | null\n /** Until when another caller holds the job; `null` when it is free. */\n leaseUntilMs: number | null\n counts: ConsentGroupChangeCounts\n /** The sites that received at least one carried refusal. */\n sitesReceiving: number\n /** Each participant's counts, by plugin id. */\n plugins: Record<string, Record<string, number>>\n}\n\n/*==========================================\n * THE ROUTE'S CONTRACT (`POST /api/orgs/consent-groups`)\n *=========================================*/\n\nexport type ConsentGroupsApiAction = 'preview' | 'apply' | 'continue' | 'cancel' | 'status'\n\nexport interface ConsentGroupsApiDeclarationRequest {\n orgId: string\n action: 'preview' | 'apply'\n /**\n * `org.consentGroups` exactly as the editor rendered it, or `null` for an\n * organization with none. Compared by {@link consentGroupsFingerprint}.\n */\n expected: Record<string, unknown> | null\n /** The COMPLETE next declaration. A new group has no id. */\n groups: ConsentGroupDraft[]\n}\n\nexport interface ConsentGroupsApiChangeRequest {\n orgId: string\n action: 'continue' | 'cancel' | 'status'\n changeId: string\n}\n\nexport type ConsentGroupsApiRequest =\n | ConsentGroupsApiDeclarationRequest\n | ConsentGroupsApiChangeRequest\n\n/** 200 for `preview`. */\nexport interface ConsentGroupsApiPreviewResponse {\n ok: true\n preview: ConsentGroupChangePreview\n}\n\n/** 200 for `apply`, `continue`, `cancel` and `status`. */\nexport interface ConsentGroupsApiChangeResponse {\n ok: true\n changeId: string\n phase: ConsentGroupsChangePhase | 'done' | 'canceled'\n done: boolean\n progress: ConsentGroupChangeProgress\n}\n\n/** 400: the declaration was refused. */\nexport interface ConsentGroupsApiRefusal {\n error: string\n errors: ConsentGroupValidationError[]\n}\n\n/** 409: the declaration changed since the editor rendered it. */\nexport interface ConsentGroupsApiStale {\n error: string\n current: Record<string, unknown> | null\n}\n\n/** 409: another change is running, or a cancel came after the flip. */\nexport interface ConsentGroupsApiInFlight {\n error: string\n changeId: string\n phase: ConsentGroupsChangePhase | 'done' | 'canceled'\n}\n\n/*==========================================\n * THE PREVIEW (`action: 'preview'`)\n *=========================================*/\n\n/**\n * One sentence a participant adds to the review step, with the figure it is\n * about. `id` is namespaced by the participant (`crm.combine`).\n */\nexport interface ConsentGroupChangePreviewLine {\n id: string\n text: string\n count: number | null\n severity: 'info' | 'warning'\n}\n\n/** How long a change should take, from the work the preview counted. */\nexport type ConsentGroupChangeEstimate = 'instant' | 'under-a-minute' | 'minutes' | 'long'\n\n/** The work bands behind {@link ConsentGroupChangeEstimate}. */\nexport function estimateConsentGroupChange(\n plan: Pick<ConsentGroupChangePlan, 'renameOnly'>,\n documents: number,\n): ConsentGroupChangeEstimate {\n if (plan.renameOnly) return 'instant'\n if (documents < 5_000) return 'under-a-minute'\n if (documents < 100_000) return 'minutes'\n return 'long'\n}\n\nexport interface ConsentGroupChangePreview {\n before: ConsentGroupDeclaration\n after: ConsentGroupDeclaration\n /** Stored ids `readConsentGroups` drops; the write removes them. */\n discarded: string[]\n /** The plan's lines, each with the sentence the activity log will write. */\n lines: Array<ConsentGroupChangeLine & { text: string }>\n /** Every group whose signup-form sentence changes. */\n disclosures: Array<{\n groupId: string\n hostIds: string[]\n before: string | null\n after: string | null\n }>\n /** Per carry, what the source site holds now (count aggregations). */\n carries: Array<{\n toHostId: string\n fromHostId: string\n siteSuppressions: number\n topicOptOuts: number\n paces: number\n /** The organization's retained refusals for the source site (AGL-3338). */\n retainedRefusals: number\n }>\n /** Per joining site, the refusals on its new siblings it starts honoring. */\n inherited: Array<{ hostId: string; refusals: number }>\n /**\n * Pending confirmations that stop holding a separated site's mail. Only\n * when the organization's confirmation switch is on.\n */\n pendingHolds: Array<{\n hostId: string\n topicId: string\n count: number\n releasedHostIds: string[]\n }>\n /** Team members who can open only some of a changed group's sites. */\n partialAccessMembers: number\n /**\n * Set when the organization's consent policy is `forward` and a site joins:\n * the joining sites may email people their new siblings captured before\n * `enforceFromMs` without a recorded opt-in.\n */\n forwardPolicyWarning: { hostIds: string[]; enforceFromMs: number } | null\n /** Each participant's lines; `null` for one whose preview failed. */\n participants: Array<{\n pluginId: string\n lines: ConsentGroupChangePreviewLine[] | null\n }>\n /** The capture surfaces that show a group's disclosure. */\n capturesDisclosing: string[]\n estimate: ConsentGroupChangeEstimate\n}\n\n/** The groups whose disclosure sentence differs between the declarations. */\nexport function consentGroupDisclosureChanges(\n before: ConsentGroupDeclaration,\n after: ConsentGroupDeclaration,\n): ConsentGroupChangePreview['disclosures'] {\n const sentence = (id: string, group: StoredConsentGroup | undefined) =>\n group\n ? consentGroupDisclosure({\n hostId: group.hostIds[0],\n groupId: id,\n name: group.name,\n hostIds: group.hostIds,\n declared: true,\n awaitsConfirmation: false,\n })\n : null\n const out: ConsentGroupChangePreview['disclosures'] = []\n for (const id of [...new Set([...Object.keys(before), ...Object.keys(after)])].sort(byString)) {\n const was = sentence(id, before[id])\n const now = sentence(id, after[id])\n if (was === now) continue\n out.push({\n groupId: id,\n hostIds: [...(after[id] ?? before[id]).hostIds],\n before: was,\n after: now,\n })\n }\n return out\n}\n"],"names":["consentGroupDisclosure","CONSENT_GROUPS_FIELD","MAX_CONSENT_GROUP_HOSTS","readConsentGroups","CONSENT_GROUPS_CHANGE_FIELD","CONSENT_GROUP_CHANGES_COLLECTION","CONSENT_GROUP_NAME_MAX","CONSENT_GROUP_SWEEP_DELAY_MS","CONSENT_GROUP_ID_PREFIX","orgSiteIds","org","hosts","ids","Array","isArray","map","id","String","Object","entries","filter","value","Set","trim","Boolean","sort","discardedConsentGroupIds","raw","usable","keys","BASE36","defaultRandom","crypto","globalThis","getRandomValues","word","Uint32Array","Math","random","mintConsentGroupId","taken","index","min","floor","has","canonicalGroup","group","name","hostIds","consentGroupsEqual","a","b","consentGroupsFingerprint","length","JSON","stringify","validateConsentGroupDeclaration","input","before","groups","ok","errors","code","sites","siteIds","names","claimed","drafts","forEach","groupIndex","push","entry","toLowerCase","add","hostId","rawId","some","includes","reserved","after","draft","groupOf","declaration","Map","set","byString","withSitesOnly","keep","out","planConsentGroupChange","options","owned","moving","beforeOf","afterOf","keyBefore","get","keyAfter","membersBefore","membersAfter","values","flatMap","carries","stays","toHostId","fromHostId","flows","key","covered","targets","kind","from","fromHostIds","to","toHostIds","survives","stayHostIds","successors","target","lines","planLines","touched","beforeAll","was","now","sameMembers","join","sameName","renameOnly","every","line","created","renamed","added","removed","moved","dissolved","moveInto","toGroupId","fromGroupId","fromName","toName","next","previous","groupId","listSiteNames","slice","describeConsentGroupChangeLine","siteName","site","PHASES","readConsentGroupsChange","changeId","phase","find","candidate","declaredAtMs","Number","startedAtMs","isFinite","consentGroupHoldersInMotion","marker","CONSENT_GROUP_CHANGE_STEPS","CONSENT_GROUP_RENAME_STEPS","consentGroupChangePhaseOf","step","estimateConsentGroupChange","plan","documents","consentGroupDisclosureChanges","sentence","declared","awaitsConfirmation"],"mappings":"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"}
|
|
@@ -14,23 +14,18 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
|
-
import type { ContactInteraction, ContactSource } from './contacts';
|
|
18
17
|
/**
|
|
19
|
-
* Where the
|
|
20
|
-
* record belongs to (AGL-2622).
|
|
18
|
+
* Where the CRM's records live, for the console's top-bar search (AGL-2622).
|
|
21
19
|
*
|
|
22
|
-
* ##
|
|
20
|
+
* ## What is left here, and why
|
|
23
21
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* inside the plugin family; this module is the same address spelled once
|
|
32
|
-
* for the console side, in the shared library both may import, and the
|
|
33
|
-
* plugin's builder is pinned against it by a spec so the two cannot drift.
|
|
22
|
+
* Every other surface asks the record-route registry
|
|
23
|
+
* (`plugin-manager/plugin-record-routes`) for a record's address, which the
|
|
24
|
+
* plugin that owns the kind publishes — a contact, an order, a submission, a
|
|
25
|
+
* booking. The top-bar search still builds the CRM's four record kinds itself
|
|
26
|
+
* until its rows come from the plugins that keep them; this is that copy,
|
|
27
|
+
* pinned against the CRM plugin's own `crmRoutes(basePath)` by its spec so
|
|
28
|
+
* the two cannot drift.
|
|
34
29
|
*
|
|
35
30
|
* ## Why the arguments are the two route params and not a host document
|
|
36
31
|
*
|
|
@@ -38,8 +33,7 @@ import type { ContactInteraction, ContactSource } from './contacts';
|
|
|
38
33
|
* both segments are already in the URL a surface is rendered on. Building
|
|
39
34
|
* from them costs nothing; resolving them from a host id costs two document
|
|
40
35
|
* reads (`hostIndex`, then the org) on every open of a card, to render a
|
|
41
|
-
* link.
|
|
42
|
-
* for the same reason.
|
|
36
|
+
* link.
|
|
43
37
|
*/
|
|
44
38
|
export interface SiteConsoleContext {
|
|
45
39
|
orgSlug: string;
|
|
@@ -65,17 +59,6 @@ export declare function crmSectionHref(context: SiteConsoleContext, section: str
|
|
|
65
59
|
* not, and a slash in an id would otherwise read as a further segment.
|
|
66
60
|
*/
|
|
67
61
|
export declare function crmRecordHref(context: SiteConsoleContext, kind: CrmRecordKind, id: string): string;
|
|
68
|
-
/**
|
|
69
|
-
* The query key the Contacts list reads to OPEN the one person with an
|
|
70
|
-
* address (AGL-2612). A surface that holds only an email — an order, a
|
|
71
|
-
* booking, a site user — links to the list with this key, the list filters
|
|
72
|
-
* on the address and moves on to the record when exactly one matches. The
|
|
73
|
-
* plugin re-exports this constant as `CONTACTS_LIST_EMAIL_PARAM` so its
|
|
74
|
-
* parser and the console's builders read one string.
|
|
75
|
-
*/
|
|
76
|
-
export declare const CRM_CONTACTS_EMAIL_PARAM = "email";
|
|
77
|
-
/** The Contacts list asked to open the person with this address. */
|
|
78
|
-
export declare function crmContactByEmailHref(context: SiteConsoleContext, email: string): string;
|
|
79
62
|
/**
|
|
80
63
|
* `/{orgSlug}/crm` — the ORGANIZATION-level CRM hub (AGL-2630), the same
|
|
81
64
|
* sections mounted with no site.
|
|
@@ -99,65 +82,3 @@ export declare function crmOrgSectionHref(orgSlug: string, section: string): str
|
|
|
99
82
|
* person key unambiguous, so a lead addresses like every other record.
|
|
100
83
|
*/
|
|
101
84
|
export declare function crmOrgRecordHref(orgSlug: string, kind: CrmRecordKind, id: string): string;
|
|
102
|
-
/**
|
|
103
|
-
* The query keys a site record page reads on arrival, so a CRM timeline can
|
|
104
|
-
* open the submission or the order it names rather than the list it sits
|
|
105
|
-
* in. Each page parses its own key from `useSearchParams`; the CRM writes
|
|
106
|
-
* them through `siteRecordLinks`, and the constant is shared so neither
|
|
107
|
-
* side can misspell the other.
|
|
108
|
-
*/
|
|
109
|
-
export declare const INBOX_SUBMISSION_PARAM = "submission";
|
|
110
|
-
export declare const ORDERS_ORDER_PARAM = "order";
|
|
111
|
-
/**
|
|
112
|
-
* The orders list narrowed to one buyer. The same word as the CRM's own
|
|
113
|
-
* email key, deliberately: a reader who edits one URL into the other is
|
|
114
|
-
* not surprised.
|
|
115
|
-
*/
|
|
116
|
-
export declare const ORDERS_CUSTOMER_PARAM = "email";
|
|
117
|
-
/**
|
|
118
|
-
* The Bookings page narrowed to one booker (AGL-2660) — the same key as the
|
|
119
|
-
* orders list and the CRM, for the same reason: one address, one word.
|
|
120
|
-
*/
|
|
121
|
-
export declare const BOOKINGS_BOOKER_PARAM = "email";
|
|
122
|
-
/**
|
|
123
|
-
* Where the site records a contact's captured history names are read
|
|
124
|
-
* (AGL-2622): the Inbox for a submission, the Orders section of the
|
|
125
|
-
* Products hub for an order, the Bookings page and the Users page for the
|
|
126
|
-
* rest. Built from the route table so a section that moves is found here
|
|
127
|
-
* by its route name rather than by searching for a string.
|
|
128
|
-
*/
|
|
129
|
-
export declare function siteRecordLinks(context: SiteConsoleContext): {
|
|
130
|
-
/** The Inbox's Submissions tab, with the reader open on one submission. */
|
|
131
|
-
submission: (submissionId: string) => string;
|
|
132
|
-
/** The Orders list. */
|
|
133
|
-
orders: () => string;
|
|
134
|
-
/** The Orders list with one order's dialog open. */
|
|
135
|
-
order: (orderId: string) => string;
|
|
136
|
-
/** The Orders list narrowed to one buyer's address. */
|
|
137
|
-
ordersByCustomer: (email: string) => string;
|
|
138
|
-
/** The Bookings page; a booking has no page of its own. */
|
|
139
|
-
bookings: () => string;
|
|
140
|
-
/** The Bookings page narrowed to one booker's address (AGL-2660). */
|
|
141
|
-
bookingsByBooker: (email: string) => string;
|
|
142
|
-
/** The Users page, where a site account's row and drawer are. */
|
|
143
|
-
members: () => string;
|
|
144
|
-
};
|
|
145
|
-
export type SiteRecordLinks = ReturnType<typeof siteRecordLinks>;
|
|
146
|
-
/**
|
|
147
|
-
* What the link on a captured timeline entry says. Only the doors that
|
|
148
|
-
* leave a record the console can open are named; a newsletter opt-in, an
|
|
149
|
-
* API create, an import or a by-hand add leave nothing to open, so they
|
|
150
|
-
* carry no link and no label.
|
|
151
|
-
*/
|
|
152
|
-
export declare const INTERACTION_LINK_LABELS: Partial<Record<ContactSource, string>>;
|
|
153
|
-
/**
|
|
154
|
-
* The console page a captured interaction points back at, or `null` when
|
|
155
|
-
* the door left nothing to open.
|
|
156
|
-
*
|
|
157
|
-
* A submission and an order are addressed by the `refId` the capture door
|
|
158
|
-
* stamped — `formSubmissions/{id}`, `orders/{id}` — and an interaction that
|
|
159
|
-
* predates the stamp lands on nothing rather than on a list that would read
|
|
160
|
-
* as "the record is gone". A booking and a member sign-up land on their
|
|
161
|
-
* pages, which list them; neither page addresses one row.
|
|
162
|
-
*/
|
|
163
|
-
export declare function contactInteractionHref(interaction: Pick<ContactInteraction, 'type' | 'refId'>, context: SiteConsoleContext): string | null;
|
|
@@ -42,19 +42,6 @@
|
|
|
42
42
|
*/ export function crmRecordHref(context, kind, id) {
|
|
43
43
|
return `${crmSectionHref(context, CRM_RECORD_SECTIONS[kind])}/${encodeURIComponent(id)}`;
|
|
44
44
|
}
|
|
45
|
-
/**
|
|
46
|
-
* The query key the Contacts list reads to OPEN the one person with an
|
|
47
|
-
* address (AGL-2612). A surface that holds only an email — an order, a
|
|
48
|
-
* booking, a site user — links to the list with this key, the list filters
|
|
49
|
-
* on the address and moves on to the record when exactly one matches. The
|
|
50
|
-
* plugin re-exports this constant as `CONTACTS_LIST_EMAIL_PARAM` so its
|
|
51
|
-
* parser and the console's builders read one string.
|
|
52
|
-
*/ export const CRM_CONTACTS_EMAIL_PARAM = 'email';
|
|
53
|
-
/** The Contacts list asked to open the person with this address. */ export function crmContactByEmailHref(context, email) {
|
|
54
|
-
return `${crmSectionHref(context, CRM_RECORD_SECTIONS.contact)}?${new URLSearchParams({
|
|
55
|
-
[CRM_CONTACTS_EMAIL_PARAM]: email
|
|
56
|
-
}).toString()}`;
|
|
57
|
-
}
|
|
58
45
|
/**
|
|
59
46
|
* `/{orgSlug}/crm` — the ORGANIZATION-level CRM hub (AGL-2630), the same
|
|
60
47
|
* sections mounted with no site.
|
|
@@ -82,99 +69,8 @@
|
|
|
82
69
|
* person key unambiguous, so a lead addresses like every other record.
|
|
83
70
|
*/ export function crmOrgRecordHref(orgSlug, kind, id) {
|
|
84
71
|
return `${crmOrgSectionHref(orgSlug, CRM_RECORD_SECTIONS[kind])}/${encodeURIComponent(id)}`;
|
|
85
|
-
}
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
// is the whole of what it did.
|
|
89
|
-
/**
|
|
90
|
-
* The query keys a site record page reads on arrival, so a CRM timeline can
|
|
91
|
-
* open the submission or the order it names rather than the list it sits
|
|
92
|
-
* in. Each page parses its own key from `useSearchParams`; the CRM writes
|
|
93
|
-
* them through `siteRecordLinks`, and the constant is shared so neither
|
|
94
|
-
* side can misspell the other.
|
|
95
|
-
*/ export const INBOX_SUBMISSION_PARAM = 'submission';
|
|
96
|
-
export const ORDERS_ORDER_PARAM = 'order';
|
|
97
|
-
/**
|
|
98
|
-
* The orders list narrowed to one buyer. The same word as the CRM's own
|
|
99
|
-
* email key, deliberately: a reader who edits one URL into the other is
|
|
100
|
-
* not surprised.
|
|
101
|
-
*/ export const ORDERS_CUSTOMER_PARAM = 'email';
|
|
102
|
-
/**
|
|
103
|
-
* The Bookings page narrowed to one booker (AGL-2660) — the same key as the
|
|
104
|
-
* orders list and the CRM, for the same reason: one address, one word.
|
|
105
|
-
*/ export const BOOKINGS_BOOKER_PARAM = 'email';
|
|
106
|
-
/**
|
|
107
|
-
* Where the site records a contact's captured history names are read
|
|
108
|
-
* (AGL-2622): the Inbox for a submission, the Orders section of the
|
|
109
|
-
* Products hub for an order, the Bookings page and the Users page for the
|
|
110
|
-
* rest. Built from the route table so a section that moves is found here
|
|
111
|
-
* by its route name rather than by searching for a string.
|
|
112
|
-
*/ export function siteRecordLinks(context) {
|
|
113
|
-
const { orgSlug, host } = context;
|
|
114
|
-
const inbox = `${buildRoute(Route.HOST_INBOX, {
|
|
115
|
-
orgSlug,
|
|
116
|
-
host
|
|
117
|
-
})}/submissions`;
|
|
118
|
-
const orders = `${buildRoute(Route.HOST_PRODUCTS, {
|
|
119
|
-
orgSlug,
|
|
120
|
-
host
|
|
121
|
-
})}/orders`;
|
|
122
|
-
const withQuery = (base, key, value)=>`${base}?${new URLSearchParams({
|
|
123
|
-
[key]: value
|
|
124
|
-
}).toString()}`;
|
|
125
|
-
return {
|
|
126
|
-
/** The Inbox's Submissions tab, with the reader open on one submission. */ submission: (submissionId)=>withQuery(inbox, INBOX_SUBMISSION_PARAM, submissionId),
|
|
127
|
-
/** The Orders list. */ orders: ()=>orders,
|
|
128
|
-
/** The Orders list with one order's dialog open. */ order: (orderId)=>withQuery(orders, ORDERS_ORDER_PARAM, orderId),
|
|
129
|
-
/** The Orders list narrowed to one buyer's address. */ ordersByCustomer: (email)=>withQuery(orders, ORDERS_CUSTOMER_PARAM, email),
|
|
130
|
-
/** The Bookings page; a booking has no page of its own. */ bookings: ()=>buildRoute(Route.HOST_BOOKINGS, {
|
|
131
|
-
orgSlug,
|
|
132
|
-
host
|
|
133
|
-
}),
|
|
134
|
-
/** The Bookings page narrowed to one booker's address (AGL-2660). */ bookingsByBooker: (email)=>withQuery(buildRoute(Route.HOST_BOOKINGS, {
|
|
135
|
-
orgSlug,
|
|
136
|
-
host
|
|
137
|
-
}), BOOKINGS_BOOKER_PARAM, email),
|
|
138
|
-
/** The Users page, where a site account's row and drawer are. */ members: ()=>buildRoute(Route.HOST_USERS, {
|
|
139
|
-
orgSlug,
|
|
140
|
-
host
|
|
141
|
-
})
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
/**
|
|
145
|
-
* What the link on a captured timeline entry says. Only the doors that
|
|
146
|
-
* leave a record the console can open are named; a newsletter opt-in, an
|
|
147
|
-
* API create, an import or a by-hand add leave nothing to open, so they
|
|
148
|
-
* carry no link and no label.
|
|
149
|
-
*/ export const INTERACTION_LINK_LABELS = {
|
|
150
|
-
form: 'Open submission',
|
|
151
|
-
order: 'Open order',
|
|
152
|
-
booking: 'Open bookings',
|
|
153
|
-
member: 'Open site users'
|
|
154
|
-
};
|
|
155
|
-
/**
|
|
156
|
-
* The console page a captured interaction points back at, or `null` when
|
|
157
|
-
* the door left nothing to open.
|
|
158
|
-
*
|
|
159
|
-
* A submission and an order are addressed by the `refId` the capture door
|
|
160
|
-
* stamped — `formSubmissions/{id}`, `orders/{id}` — and an interaction that
|
|
161
|
-
* predates the stamp lands on nothing rather than on a list that would read
|
|
162
|
-
* as "the record is gone". A booking and a member sign-up land on their
|
|
163
|
-
* pages, which list them; neither page addresses one row.
|
|
164
|
-
*/ export function contactInteractionHref(interaction, context) {
|
|
165
|
-
const links = siteRecordLinks(context);
|
|
166
|
-
switch(interaction.type){
|
|
167
|
-
case 'form':
|
|
168
|
-
return interaction.refId ? links.submission(interaction.refId) : null;
|
|
169
|
-
case 'order':
|
|
170
|
-
return interaction.refId ? links.order(interaction.refId) : null;
|
|
171
|
-
case 'booking':
|
|
172
|
-
return links.bookings();
|
|
173
|
-
case 'member':
|
|
174
|
-
return links.members();
|
|
175
|
-
default:
|
|
176
|
-
return null;
|
|
177
|
-
}
|
|
178
|
-
}
|
|
72
|
+
} // `crmOrgLeadHref` is gone with the host-path fallback (AGL-3277). It took a
|
|
73
|
+
// site it had already stopped using; `crmOrgRecordHref(orgSlug, 'lead', id)`
|
|
74
|
+
// is the whole of what it did.
|
|
179
75
|
|
|
180
76
|
//# sourceMappingURL=console-record-links.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/console-record-links.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\nimport { buildRoute, Route } from './console-routes'\nimport type { ContactInteraction, ContactSource } from './contacts'\n\n/**\n * Where the console's records live, for a surface that is not the one the\n * record belongs to (AGL-2622).\n *\n * ## Why these addresses are built here and not where they are used\n *\n * A CRM record is reached from six surfaces that are not the CRM — the\n * top-bar search, an Inbox row, a form's page, an order, a booking, a site\n * user — and a CRM timeline points back at four of them. Two of those\n * readers are the console APP, which the module boundaries forbid from\n * importing a feature plugin at all: a plugin reaches the app only through\n * the generated loader manifests, so the app cannot ask the CRM for its own\n * route builder. The CRM plugin's `crmRoutes(basePath)` stays the builder\n * inside the plugin family; this module is the same address spelled once\n * for the console side, in the shared library both may import, and the\n * plugin's builder is pinned against it by a spec so the two cannot drift.\n *\n * ## Why the arguments are the two route params and not a host document\n *\n * Every site console page is addressed by `/{orgSlug}/hosts/{host}/…`, and\n * both segments are already in the URL a surface is rendered on. Building\n * from them costs nothing; resolving them from a host id costs two document\n * reads (`hostIndex`, then the org) on every open of a card, to render a\n * link. `useConsoleHubPath` in the marketing plugin makes the same choice\n * for the same reason.\n */\nexport interface SiteConsoleContext {\n orgSlug: string\n host: string\n}\n\n/** The CRM's nav slug — the `[pluginSlug]` segment the shell resolves. */\nexport const CRM_CONSOLE_SLUG = 'crm'\n\n/** The four record kinds a surface outside the CRM may name. */\nexport type CrmRecordKind = 'contact' | 'lead' | 'company' | 'deal'\n\n/**\n * The hub section each kind's record lives under, which is also the list a\n * kind-only link lands on. Named here rather than in the plugin's section\n * registry so the console app can address a record without the plugin.\n */\nexport const CRM_RECORD_SECTIONS: Record<CrmRecordKind, string> = {\n contact: 'contacts',\n lead: 'leads',\n company: 'companies',\n deal: 'deals',\n}\n\n/** `/{orgSlug}/hosts/{host}/crm` — the CRM hub on the site being read. */\nexport function crmHubHref(context: SiteConsoleContext): string {\n return buildRoute(Route.HOST_PLUGIN, {\n orgSlug: context.orgSlug,\n host: context.host,\n pluginSlug: CRM_CONSOLE_SLUG,\n })\n}\n\n/** One section of the hub — the Leads list, the Deals board. */\nexport function crmSectionHref(\n context: SiteConsoleContext,\n section: string,\n): string {\n return `${crmHubHref(context)}/${section}`\n}\n\n/**\n * One record's own page. The id is URL-encoded because a Firestore id is\n * opaque — the console mints its own, but an import or an API caller may\n * not, and a slash in an id would otherwise read as a further segment.\n */\nexport function crmRecordHref(\n context: SiteConsoleContext,\n kind: CrmRecordKind,\n id: string,\n): string {\n return `${crmSectionHref(context, CRM_RECORD_SECTIONS[kind])}/${encodeURIComponent(id)}`\n}\n\n/**\n * The query key the Contacts list reads to OPEN the one person with an\n * address (AGL-2612). A surface that holds only an email — an order, a\n * booking, a site user — links to the list with this key, the list filters\n * on the address and moves on to the record when exactly one matches. The\n * plugin re-exports this constant as `CONTACTS_LIST_EMAIL_PARAM` so its\n * parser and the console's builders read one string.\n */\nexport const CRM_CONTACTS_EMAIL_PARAM = 'email'\n\n/** The Contacts list asked to open the person with this address. */\nexport function crmContactByEmailHref(\n context: SiteConsoleContext,\n email: string,\n): string {\n return `${crmSectionHref(context, CRM_RECORD_SECTIONS.contact)}?${new URLSearchParams(\n { [CRM_CONTACTS_EMAIL_PARAM]: email },\n ).toString()}`\n}\n\n/**\n * `/{orgSlug}/crm` — the ORGANIZATION-level CRM hub (AGL-2630), the same\n * sections mounted with no site.\n *\n * The org-level twin of {@link crmHubHref}, and separate from it because the\n * two take different arguments: a site address needs the subdomain, and a\n * surface standing at the org hub has none to give. A caller that faked one\n * would put a site's name on a page about all of them.\n */\nexport function crmOrgHubHref(orgSlug: string): string {\n return buildRoute(Route.ORG_CRM, { orgSlug })\n}\n\n/** One section of the org-level hub — the cross-site Deals board. */\nexport function crmOrgSectionHref(orgSlug: string, section: string): string {\n return `${crmOrgHubHref(orgSlug)}/${section}`\n}\n\n/**\n * One record's page at the org level.\n *\n * A LEAD IS NO LONGER EXCLUDED (AGL-3275). It was, by the type rather than a\n * runtime check, because a lead's id is a person key — the same on every site\n * that met the person — while `hosts/{hostId}/leads` was host-scoped by path,\n * so two sites held two documents carrying one id and an org-level address\n * had to name the site to tell them apart. One org collection makes the\n * person key unambiguous, so a lead addresses like every other record.\n */\nexport function crmOrgRecordHref(\n orgSlug: string,\n kind: CrmRecordKind,\n id: string,\n): string {\n return `${crmOrgSectionHref(orgSlug, CRM_RECORD_SECTIONS[kind])}/${encodeURIComponent(id)}`\n}\n\n// `crmOrgLeadHref` is gone with the host-path fallback (AGL-3277). It took a\n// site it had already stopped using; `crmOrgRecordHref(orgSlug, 'lead', id)`\n// is the whole of what it did.\n\n/**\n * The query keys a site record page reads on arrival, so a CRM timeline can\n * open the submission or the order it names rather than the list it sits\n * in. Each page parses its own key from `useSearchParams`; the CRM writes\n * them through `siteRecordLinks`, and the constant is shared so neither\n * side can misspell the other.\n */\nexport const INBOX_SUBMISSION_PARAM = 'submission'\nexport const ORDERS_ORDER_PARAM = 'order'\n/**\n * The orders list narrowed to one buyer. The same word as the CRM's own\n * email key, deliberately: a reader who edits one URL into the other is\n * not surprised.\n */\nexport const ORDERS_CUSTOMER_PARAM = 'email'\n/**\n * The Bookings page narrowed to one booker (AGL-2660) — the same key as the\n * orders list and the CRM, for the same reason: one address, one word.\n */\nexport const BOOKINGS_BOOKER_PARAM = 'email'\n\n/**\n * Where the site records a contact's captured history names are read\n * (AGL-2622): the Inbox for a submission, the Orders section of the\n * Products hub for an order, the Bookings page and the Users page for the\n * rest. Built from the route table so a section that moves is found here\n * by its route name rather than by searching for a string.\n */\nexport function siteRecordLinks(context: SiteConsoleContext) {\n const { orgSlug, host } = context\n const inbox = `${buildRoute(Route.HOST_INBOX, { orgSlug, host })}/submissions`\n const orders = `${buildRoute(Route.HOST_PRODUCTS, { orgSlug, host })}/orders`\n const withQuery = (base: string, key: string, value: string) =>\n `${base}?${new URLSearchParams({ [key]: value }).toString()}`\n return {\n /** The Inbox's Submissions tab, with the reader open on one submission. */\n submission: (submissionId: string) =>\n withQuery(inbox, INBOX_SUBMISSION_PARAM, submissionId),\n /** The Orders list. */\n orders: () => orders,\n /** The Orders list with one order's dialog open. */\n order: (orderId: string) => withQuery(orders, ORDERS_ORDER_PARAM, orderId),\n /** The Orders list narrowed to one buyer's address. */\n ordersByCustomer: (email: string) =>\n withQuery(orders, ORDERS_CUSTOMER_PARAM, email),\n /** The Bookings page; a booking has no page of its own. */\n bookings: () => buildRoute(Route.HOST_BOOKINGS, { orgSlug, host }),\n /** The Bookings page narrowed to one booker's address (AGL-2660). */\n bookingsByBooker: (email: string) =>\n withQuery(\n buildRoute(Route.HOST_BOOKINGS, { orgSlug, host }),\n BOOKINGS_BOOKER_PARAM,\n email,\n ),\n /** The Users page, where a site account's row and drawer are. */\n members: () => buildRoute(Route.HOST_USERS, { orgSlug, host }),\n }\n}\n\nexport type SiteRecordLinks = ReturnType<typeof siteRecordLinks>\n\n/**\n * What the link on a captured timeline entry says. Only the doors that\n * leave a record the console can open are named; a newsletter opt-in, an\n * API create, an import or a by-hand add leave nothing to open, so they\n * carry no link and no label.\n */\nexport const INTERACTION_LINK_LABELS: Partial<Record<ContactSource, string>> = {\n form: 'Open submission',\n order: 'Open order',\n booking: 'Open bookings',\n member: 'Open site users',\n}\n\n/**\n * The console page a captured interaction points back at, or `null` when\n * the door left nothing to open.\n *\n * A submission and an order are addressed by the `refId` the capture door\n * stamped — `formSubmissions/{id}`, `orders/{id}` — and an interaction that\n * predates the stamp lands on nothing rather than on a list that would read\n * as \"the record is gone\". A booking and a member sign-up land on their\n * pages, which list them; neither page addresses one row.\n */\nexport function contactInteractionHref(\n interaction: Pick<ContactInteraction, 'type' | 'refId'>,\n context: SiteConsoleContext,\n): string | null {\n const links = siteRecordLinks(context)\n switch (interaction.type) {\n case 'form':\n return interaction.refId ? links.submission(interaction.refId) : null\n case 'order':\n return interaction.refId ? links.order(interaction.refId) : null\n case 'booking':\n return links.bookings()\n case 'member':\n return links.members()\n default:\n return null\n }\n}\n"],"names":["buildRoute","Route","CRM_CONSOLE_SLUG","CRM_RECORD_SECTIONS","contact","lead","company","deal","crmHubHref","context","HOST_PLUGIN","orgSlug","host","pluginSlug","crmSectionHref","section","crmRecordHref","kind","id","encodeURIComponent","CRM_CONTACTS_EMAIL_PARAM","crmContactByEmailHref","email","URLSearchParams","toString","crmOrgHubHref","ORG_CRM","crmOrgSectionHref","crmOrgRecordHref","INBOX_SUBMISSION_PARAM","ORDERS_ORDER_PARAM","ORDERS_CUSTOMER_PARAM","BOOKINGS_BOOKER_PARAM","siteRecordLinks","inbox","HOST_INBOX","orders","HOST_PRODUCTS","withQuery","base","key","value","submission","submissionId","order","orderId","ordersByCustomer","bookings","HOST_BOOKINGS","bookingsByBooker","members","HOST_USERS","INTERACTION_LINK_LABELS","form","booking","member","contactInteractionHref","interaction","links","type","refId"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/console-record-links.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\nimport { buildRoute, Route } from './console-routes'\n\n/**\n * Where the CRM's records live, for the console's top-bar search (AGL-2622).\n *\n * ## What is left here, and why\n *\n * Every other surface asks the record-route registry\n * (`plugin-manager/plugin-record-routes`) for a record's address, which the\n * plugin that owns the kind publishes — a contact, an order, a submission, a\n * booking. The top-bar search still builds the CRM's four record kinds itself\n * until its rows come from the plugins that keep them; this is that copy,\n * pinned against the CRM plugin's own `crmRoutes(basePath)` by its spec so\n * the two cannot drift.\n *\n * ## Why the arguments are the two route params and not a host document\n *\n * Every site console page is addressed by `/{orgSlug}/hosts/{host}/…`, and\n * both segments are already in the URL a surface is rendered on. Building\n * from them costs nothing; resolving them from a host id costs two document\n * reads (`hostIndex`, then the org) on every open of a card, to render a\n * link.\n */\nexport interface SiteConsoleContext {\n orgSlug: string\n host: string\n}\n\n/** The CRM's nav slug — the `[pluginSlug]` segment the shell resolves. */\nexport const CRM_CONSOLE_SLUG = 'crm'\n\n/** The four record kinds a surface outside the CRM may name. */\nexport type CrmRecordKind = 'contact' | 'lead' | 'company' | 'deal'\n\n/**\n * The hub section each kind's record lives under, which is also the list a\n * kind-only link lands on. Named here rather than in the plugin's section\n * registry so the console app can address a record without the plugin.\n */\nexport const CRM_RECORD_SECTIONS: Record<CrmRecordKind, string> = {\n contact: 'contacts',\n lead: 'leads',\n company: 'companies',\n deal: 'deals',\n}\n\n/** `/{orgSlug}/hosts/{host}/crm` — the CRM hub on the site being read. */\nexport function crmHubHref(context: SiteConsoleContext): string {\n return buildRoute(Route.HOST_PLUGIN, {\n orgSlug: context.orgSlug,\n host: context.host,\n pluginSlug: CRM_CONSOLE_SLUG,\n })\n}\n\n/** One section of the hub — the Leads list, the Deals board. */\nexport function crmSectionHref(\n context: SiteConsoleContext,\n section: string,\n): string {\n return `${crmHubHref(context)}/${section}`\n}\n\n/**\n * One record's own page. The id is URL-encoded because a Firestore id is\n * opaque — the console mints its own, but an import or an API caller may\n * not, and a slash in an id would otherwise read as a further segment.\n */\nexport function crmRecordHref(\n context: SiteConsoleContext,\n kind: CrmRecordKind,\n id: string,\n): string {\n return `${crmSectionHref(context, CRM_RECORD_SECTIONS[kind])}/${encodeURIComponent(id)}`\n}\n\n/**\n * `/{orgSlug}/crm` — the ORGANIZATION-level CRM hub (AGL-2630), the same\n * sections mounted with no site.\n *\n * The org-level twin of {@link crmHubHref}, and separate from it because the\n * two take different arguments: a site address needs the subdomain, and a\n * surface standing at the org hub has none to give. A caller that faked one\n * would put a site's name on a page about all of them.\n */\nexport function crmOrgHubHref(orgSlug: string): string {\n return buildRoute(Route.ORG_CRM, { orgSlug })\n}\n\n/** One section of the org-level hub — the cross-site Deals board. */\nexport function crmOrgSectionHref(orgSlug: string, section: string): string {\n return `${crmOrgHubHref(orgSlug)}/${section}`\n}\n\n/**\n * One record's page at the org level.\n *\n * A LEAD IS NO LONGER EXCLUDED (AGL-3275). It was, by the type rather than a\n * runtime check, because a lead's id is a person key — the same on every site\n * that met the person — while `hosts/{hostId}/leads` was host-scoped by path,\n * so two sites held two documents carrying one id and an org-level address\n * had to name the site to tell them apart. One org collection makes the\n * person key unambiguous, so a lead addresses like every other record.\n */\nexport function crmOrgRecordHref(\n orgSlug: string,\n kind: CrmRecordKind,\n id: string,\n): string {\n return `${crmOrgSectionHref(orgSlug, CRM_RECORD_SECTIONS[kind])}/${encodeURIComponent(id)}`\n}\n\n// `crmOrgLeadHref` is gone with the host-path fallback (AGL-3277). It took a\n// site it had already stopped using; `crmOrgRecordHref(orgSlug, 'lead', id)`\n// is the whole of what it did.\n"],"names":["buildRoute","Route","CRM_CONSOLE_SLUG","CRM_RECORD_SECTIONS","contact","lead","company","deal","crmHubHref","context","HOST_PLUGIN","orgSlug","host","pluginSlug","crmSectionHref","section","crmRecordHref","kind","id","encodeURIComponent","crmOrgHubHref","ORG_CRM","crmOrgSectionHref","crmOrgRecordHref"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,UAAU,EAAEC,KAAK,QAAQ,sBAAkB;AA4BpD,wEAAwE,GACxE,OAAO,MAAMC,mBAAmB,MAAK;AAKrC;;;;CAIC,GACD,OAAO,MAAMC,sBAAqD;IAChEC,SAAS;IACTC,MAAM;IACNC,SAAS;IACTC,MAAM;AACR,EAAC;AAED,wEAAwE,GACxE,OAAO,SAASC,WAAWC,OAA2B;IACpD,OAAOT,WAAWC,MAAMS,WAAW,EAAE;QACnCC,SAASF,QAAQE,OAAO;QACxBC,MAAMH,QAAQG,IAAI;QAClBC,YAAYX;IACd;AACF;AAEA,8DAA8D,GAC9D,OAAO,SAASY,eACdL,OAA2B,EAC3BM,OAAe;IAEf,OAAO,GAAGP,WAAWC,SAAS,CAAC,EAAEM,SAAS;AAC5C;AAEA;;;;CAIC,GACD,OAAO,SAASC,cACdP,OAA2B,EAC3BQ,IAAmB,EACnBC,EAAU;IAEV,OAAO,GAAGJ,eAAeL,SAASN,mBAAmB,CAACc,KAAK,EAAE,CAAC,EAAEE,mBAAmBD,KAAK;AAC1F;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASE,cAAcT,OAAe;IAC3C,OAAOX,WAAWC,MAAMoB,OAAO,EAAE;QAAEV;IAAQ;AAC7C;AAEA,mEAAmE,GACnE,OAAO,SAASW,kBAAkBX,OAAe,EAAEI,OAAe;IAChE,OAAO,GAAGK,cAAcT,SAAS,CAAC,EAAEI,SAAS;AAC/C;AAEA;;;;;;;;;CASC,GACD,OAAO,SAASQ,iBACdZ,OAAe,EACfM,IAAmB,EACnBC,EAAU;IAEV,OAAO,GAAGI,kBAAkBX,SAASR,mBAAmB,CAACc,KAAK,EAAE,CAAC,EAAEE,mBAAmBD,KAAK;AAC7F,EAEA,6EAA6E;CAC7E,6EAA6E;CAC7E,+BAA+B"}
|