@aglyn/aglyn 1.0.0-beta.218 → 1.0.0-beta.220
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +12 -12
- package/src/lib/app-utils/actions.d.ts +26 -433
- package/src/lib/app-utils/actions.js +108 -633
- 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/analytics-events.d.ts +1 -1
- 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/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/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/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/consent-groups.d.ts +1 -1
- package/src/lib/app-utils/consent-groups.js.map +1 -1
- package/src/lib/app-utils/console-routes.d.ts +15 -42
- package/src/lib/app-utils/console-routes.js +10 -39
- package/src/lib/app-utils/console-routes.js.map +1 -1
- package/src/lib/app-utils/contacts.d.ts +24 -2
- package/src/lib/app-utils/contacts.js +21 -5
- package/src/lib/app-utils/contacts.js.map +1 -1
- package/src/lib/app-utils/container-membership.d.ts +93 -0
- package/src/lib/app-utils/container-membership.js +165 -0
- package/src/lib/app-utils/container-membership.js.map +1 -0
- package/src/lib/app-utils/crm-task-reminders.d.ts +23 -6
- package/src/lib/app-utils/crm-task-reminders.js +32 -11
- package/src/lib/app-utils/crm-task-reminders.js.map +1 -1
- package/src/lib/app-utils/crm.d.ts +24 -4
- package/src/lib/app-utils/crm.js +23 -0
- package/src/lib/app-utils/crm.js.map +1 -1
- package/src/lib/app-utils/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-csv.d.ts → csv.d.ts} +10 -43
- package/src/lib/app-utils/{dataset-csv.js → csv.js} +10 -81
- package/src/lib/app-utils/csv.js.map +1 -0
- package/src/lib/app-utils/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 -30
- package/src/lib/app-utils/datasets.js +74 -24
- 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 +14 -8
- package/src/lib/app-utils/docs-help.generated.js +23 -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 +570 -346
- 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 +3 -3
- 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/email-text.d.ts +80 -0
- package/src/lib/app-utils/email-text.js +189 -0
- package/src/lib/app-utils/email-text.js.map +1 -0
- package/src/lib/app-utils/{list-assignment-policy.d.ts → enrollment-basis.d.ts} +4 -0
- package/src/lib/app-utils/{list-assignment-policy.js → enrollment-basis.js} +5 -1
- package/src/lib/app-utils/enrollment-basis.js.map +1 -0
- package/src/lib/app-utils/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 +14 -14
- package/src/lib/app-utils/expand-repeatables.js.map +1 -1
- package/src/lib/app-utils/forms.js +2 -2
- package/src/lib/app-utils/forms.js.map +1 -1
- package/src/lib/app-utils/health-report.d.ts +2 -3
- package/src/lib/app-utils/health-report.js +8 -6
- package/src/lib/app-utils/health-report.js.map +1 -1
- package/src/lib/app-utils/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/inbound-mail-domain.d.ts +27 -0
- package/src/lib/app-utils/inbound-mail-domain.js +31 -0
- package/src/lib/app-utils/inbound-mail-domain.js.map +1 -0
- package/src/lib/app-utils/lockdown.d.ts +11 -0
- package/src/lib/app-utils/lockdown.js +33 -3
- package/src/lib/app-utils/lockdown.js.map +1 -1
- package/src/lib/app-utils/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/member-email-aliases.d.ts +25 -0
- package/src/lib/app-utils/member-email-aliases.js +29 -0
- package/src/lib/app-utils/member-email-aliases.js.map +1 -1
- package/src/lib/app-utils/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 +147 -47
- package/src/lib/app-utils/notifications.js +182 -44
- package/src/lib/app-utils/notifications.js.map +1 -1
- package/src/lib/app-utils/operator-alerts.d.ts +15 -1
- package/src/lib/app-utils/operator-alerts.js +47 -10
- package/src/lib/app-utils/operator-alerts.js.map +1 -1
- package/src/lib/app-utils/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 +207 -306
- package/src/lib/app-utils/plan-entitlements.js +345 -528
- 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/platform-marketing-consent.d.ts +56 -2
- package/src/lib/app-utils/platform-marketing-consent.js +6 -5
- package/src/lib/app-utils/platform-marketing-consent.js.map +1 -1
- package/src/lib/app-utils/plugin-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/plugin-starter-templates.generated.d.ts +12 -0
- package/src/lib/app-utils/plugin-starter-templates.generated.js +711 -0
- package/src/lib/app-utils/plugin-starter-templates.generated.js.map +1 -0
- package/src/lib/app-utils/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-query.d.ts +56 -0
- package/src/lib/app-utils/{dataset-query.js → repeat-query.js} +16 -16
- package/src/lib/app-utils/repeat-query.js.map +1 -0
- package/src/lib/app-utils/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.js +1 -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 +13 -18
- package/src/lib/app-utils/server.js +33 -51
- 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} +2 -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-template-nodes.d.ts +137 -0
- package/src/lib/app-utils/starter-template-nodes.js +129 -0
- package/src/lib/app-utils/starter-template-nodes.js.map +1 -0
- package/src/lib/app-utils/starter-templates.d.ts +11 -34
- package/src/lib/app-utils/starter-templates.js +19 -230
- package/src/lib/app-utils/starter-templates.js.map +1 -1
- package/src/lib/app-utils/{email-topics.d.ts → subscription-topics.d.ts} +64 -48
- package/src/lib/app-utils/{email-topics.js → subscription-topics.js} +39 -109
- package/src/lib/app-utils/subscription-topics.js.map +1 -0
- package/src/lib/app-utils/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/usage-budget.d.ts +73 -188
- package/src/lib/app-utils/usage-budget.js +58 -194
- package/src/lib/app-utils/usage-budget.js.map +1 -1
- package/src/lib/app-utils/{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/{campaign-touch.d.ts → utm-touch.d.ts} +25 -17
- package/src/lib/app-utils/{campaign-touch.js → utm-touch.js} +38 -30
- package/src/lib/app-utils/utm-touch.js.map +1 -0
- package/src/lib/app-utils/variables.d.ts +9 -9
- package/src/lib/app-utils/variables.js.map +1 -1
- package/src/lib/app-utils/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/visitor-record-ceiling.d.ts +32 -0
- package/src/lib/app-utils/visitor-record-ceiling.js +39 -0
- package/src/lib/app-utils/visitor-record-ceiling.js.map +1 -1
- package/src/lib/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/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 +11 -137
- package/src/lib/foundation/definitions/org-billing.types.js.map +1 -1
- package/src/lib/foundation/definitions/organization.types.d.ts +1 -3
- package/src/lib/foundation/definitions/organization.types.js.map +1 -1
- package/src/lib/foundation/definitions/platform.types.d.ts +11 -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 +372 -99
- package/src/lib/plugin-manager/feature-plugins.js +126 -51
- package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
- package/src/lib/plugin-manager/first-party-plugins.generated.d.ts +134 -0
- package/src/lib/plugin-manager/first-party-plugins.generated.js +840 -10
- package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
- package/src/lib/plugin-manager/index.d.ts +0 -1
- package/src/lib/plugin-manager/index.js +0 -1
- package/src/lib/plugin-manager/index.js.map +1 -1
- package/src/lib/plugin-manager/plugin-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-contact-capture.d.ts +9 -4
- package/src/lib/plugin-manager/plugin-contact-capture.js.map +1 -1
- package/src/lib/plugin-manager/plugin-containers.d.ts +68 -0
- package/src/lib/plugin-manager/plugin-containers.generated.d.ts +10 -0
- package/src/lib/plugin-manager/plugin-containers.generated.js +20 -0
- package/src/lib/plugin-manager/plugin-containers.generated.js.map +1 -0
- package/src/lib/plugin-manager/plugin-containers.js +27 -0
- package/src/lib/plugin-manager/plugin-containers.js.map +1 -0
- package/src/lib/plugin-manager/plugin-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-entitlements.d.ts +8 -0
- package/src/lib/plugin-manager/plugin-entitlements.js.map +1 -1
- 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-membership-detach.js +1 -1
- package/src/lib/plugin-manager/plugin-membership-detach.js.map +1 -1
- 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-email-state.d.ts +69 -0
- package/src/lib/plugin-manager/plugin-record-email-state.js +32 -0
- package/src/lib/plugin-manager/plugin-record-email-state.js.map +1 -1
- package/src/lib/plugin-manager/plugin-record-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-lists.d.ts +115 -0
- package/src/lib/plugin-manager/plugin-record-lists.js +75 -0
- package/src/lib/plugin-manager/plugin-record-lists.js.map +1 -0
- package/src/lib/plugin-manager/plugin-record-timeline.d.ts +42 -8
- package/src/lib/plugin-manager/plugin-record-timeline.js.map +1 -1
- package/src/lib/plugin-manager/plugin-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-beacons.d.ts +71 -0
- package/src/lib/plugin-manager/plugin-site-beacons.js +54 -0
- package/src/lib/plugin-manager/plugin-site-beacons.js.map +1 -0
- package/src/lib/plugin-manager/plugin-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-subscription-topics.generated.d.ts +11 -0
- package/src/lib/plugin-manager/plugin-subscription-topics.generated.js +41 -0
- package/src/lib/plugin-manager/plugin-subscription-topics.generated.js.map +1 -0
- package/src/lib/plugin-manager/plugin-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 +319 -0
- package/src/lib/plugin-manager/plugin-usage-axes.js +186 -0
- package/src/lib/plugin-manager/plugin-usage-axes.js.map +1 -0
- package/src/lib/plugin-manager/plugin-usage-meters.d.ts +108 -0
- package/src/lib/plugin-manager/plugin-usage-meters.js +105 -0
- package/src/lib/plugin-manager/plugin-usage-meters.js.map +1 -0
- package/src/lib/plugin-manager/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/usage-alert-contributors.d.ts +76 -6
- package/src/lib/plugin-manager/usage-alert-contributors.js +23 -10
- package/src/lib/plugin-manager/usage-alert-contributors.js.map +1 -1
- package/src/lib/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/assist-credits.d.ts +0 -482
- package/src/lib/app-utils/assist-credits.js +0 -509
- package/src/lib/app-utils/assist-credits.js.map +0 -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/campaign-membership.d.ts +0 -121
- package/src/lib/app-utils/campaign-membership.js +0 -189
- package/src/lib/app-utils/campaign-membership.js.map +0 -1
- package/src/lib/app-utils/campaign-touch.js.map +0 -1
- package/src/lib/app-utils/console-record-links.d.ts +0 -163
- package/src/lib/app-utils/console-record-links.js +0 -180
- package/src/lib/app-utils/console-record-links.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-booking.d.ts +0 -75
- package/src/lib/app-utils/crm-booking.js +0 -133
- package/src/lib/app-utils/crm-booking.js.map +0 -1
- package/src/lib/app-utils/crm-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/crm-inbound.d.ts +0 -245
- package/src/lib/app-utils/crm-inbound.js +0 -407
- package/src/lib/app-utils/crm-inbound.js.map +0 -1
- package/src/lib/app-utils/crm-reports.d.ts +0 -479
- package/src/lib/app-utils/crm-reports.js +0 -680
- package/src/lib/app-utils/crm-reports.js.map +0 -1
- package/src/lib/app-utils/dataset-csv.js.map +0 -1
- package/src/lib/app-utils/dataset-query.d.ts +0 -57
- package/src/lib/app-utils/dataset-query.js.map +0 -1
- package/src/lib/app-utils/email-topics.js.map +0 -1
- package/src/lib/app-utils/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-assignment-policy.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
- package/src/lib/plugin-manager/plugin-metered-lines.d.ts +0 -97
- package/src/lib/plugin-manager/plugin-metered-lines.js +0 -123
- package/src/lib/plugin-manager/plugin-metered-lines.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/email-starter-policy.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 * What a marketplace email starter may contain, and whether one may still be\n * mailed.\n *\n * In core rather than beside the rest of the marketplace model for the reason\n * `marketplace-provenance` gives: three different projects have to ask these\n * questions and a `scope:app` project may not depend on an `aglyn:addons` lib.\n * `marketplace-verification` used to be cited here too and no longer can be —\n * its console reader became the plugin's own surface, so the policy went with\n * it (AGL-3080). A shared reason is only shared while both halves still hold. The marketplace plugin enforces the\n * policy when content is published and installed; the marketing plugin asks\n * the kill question on the send path; the console renders the assurance.\n * Three readings of \"is this template safe to mail\" is the failure mode this\n * module exists to prevent.\n *\n * ## Why an email design needs a policy the page allowlist does not supply\n *\n * A published page component is rendered by us, inside a document we serve,\n * behind our CSP. A published EMAIL design is rendered by the recipient's mail\n * client, out of our reach entirely, and it leaves on a sending domain whose\n * reputation every tenant shares. The two facts that follow are the whole\n * reason this file is stricter than `sanitizeMarketplaceDefinition`:\n *\n * - **A remote asset in an email is a read receipt.** The recipient's client\n * fetches every `src` on open, so whatever host the string names learns that\n * this person opened this message, at this time, from this IP. Naming a\n * host the PUBLISHER controls hands a third party a recipient-level\n * engagement feed on the tenant's audience — processing with no recorded\n * basis and no `assertedBy` provenance, which is exactly what the consent\n * architecture exists to make impossible. A 1×1 pixel is the deliberate form;\n * an ordinary hero image is the same disclosure with a picture attached.\n * - **A link is sent under the tenant's name.** A recipient reads the From\n * header, not the publisher's listing page.\n *\n * So `src` is held to first-party or inline, and `href` to a transport a\n * recipient's client will not silently downgrade. Violations are REFUSED\n * rather than stripped: `sanitizePublishedNodeProps` drops an unsafe URL and\n * publishes the rest, which is right for a page — a missing image is a\n * cosmetic defect — and wrong here, because the publisher whose tracker was\n * quietly removed learns nothing and tries again.\n */\n\nimport { isMediaCdnPath, isMediaRef, parseMediaRef } from './media-ref'\nimport { isListingWideRevocation, isPluginRevoked } from './plugin-manifest'\nimport type { PluginRevocation } from './plugin-manifest'\nimport { decodeStoredNodes } from './stored-nodes'\n\n/** Props that carry a URL into a recipient's mail client. */\nconst URL_PROPS = ['src', 'href'] as const\n\n/**\n * Props whose value is markup rather than text.\n *\n * `emailHtml` is already off `MARKETPLACE_EMAIL_COMPONENT_ID_ALLOWLIST`, so a\n * publish cannot mint one. This catches the same content arriving as a PROP on\n * a block that is on the list — a hand-written document, or a block that grows\n * an `html` prop later — which the component-id allowlist alone would pass.\n */\nconst MARKUP_PROPS = ['html', 'dangerouslySetInnerHTML'] as const\n\n/** Merge-tag opener. Recipient data must never reach a URL (see below). */\nconst MERGE_TAG_OPEN = '{{'\n\n/** Inline image data, which is inert and discloses nothing on open. */\nconst INLINE_IMAGE = /^data:image\\//i\n\n/** Link transports a recipient's client will not silently downgrade. */\nconst MAIL_SAFE_HREF = /^(https:\\/\\/|mailto:|\\/|#)/i\n\nexport type EmailStarterViolationCode =\n /** A `src` naming a host that is not ours — the read-receipt vector. */\n | 'remote-asset'\n /** An `href` on a transport we will not send under a tenant's name. */\n | 'unsafe-link'\n /** Recipient data heading for a URL. */\n | 'merge-tag-in-url'\n /** Markup we would have to trust rather than render. */\n | 'raw-markup'\n /** The node map could not be read, so nothing about it is known. */\n | 'unreadable'\n\nexport interface EmailStarterViolation {\n code: EmailStarterViolationCode\n /** The offending node's id, or `'nodes'` when the map itself is the fault. */\n nodeId: string\n componentId: string\n /** The prop that carried it, for a message that can be acted on. */\n prop: string\n /** Truncated: a violation is reported to a publisher, and often logged. */\n value: string\n}\n\nexport interface EmailStarterInspection {\n ok: boolean\n violations: EmailStarterViolation[]\n /**\n * Every host this design would send a recipient to, sorted and deduplicated.\n *\n * The inspectable half of the link policy. A tenant about to mail their\n * customers under their own From header is entitled to see the list of\n * domains a stranger's template points them at, and no allowlist can decide\n * that for them — a link to a legitimate retailer and a link to the\n * publisher's affiliate funnel are the same shape.\n */\n linkHosts: string[]\n /**\n * Media scopes belonging to somebody other than the installing site.\n *\n * Not a violation: the asset is served by our own CDN, so the publisher\n * learns nothing about the recipient, and refusing would make almost every\n * real template uninstallable. It is REPORTED because the reference stays\n * live after a copy-on-install — the publisher can still replace the bytes\n * behind it — which is the one way an installed design can change without\n * an update being taken.\n */\n foreignMediaScopes: string[]\n /** The decoded map, so a caller need not decode twice. Null when unreadable. */\n nodes: Record<string, unknown> | null\n}\n\nfunction truncate(value: unknown): string {\n const text = typeof value === 'string' ? value : String(value ?? '')\n return text.length > 120 ? `${text.slice(0, 117)}...` : text\n}\n\n/**\n * The host a link points at, or null for a same-origin or non-URL target.\n *\n * `URL` rather than a regex because the question is \"which host will the mail\n * client resolve this to\", and that is the parser's answer, not ours. A value\n * that will not parse is not reported as a host — it is already refused by the\n * transport check, and inventing a host for it would put a fiction on a screen\n * a tenant is using to make a decision.\n */\nfunction linkHost(value: string): string | null {\n if (!/^https?:\\/\\//i.test(value)) return null\n try {\n return new URL(value).host.toLowerCase() || null\n } catch {\n return null\n }\n}\n\n/**\n * Whether a decoded value is a node MAP rather than something that merely\n * survived decoding.\n *\n * The case this exists for is the AGL-1394 shape: `nodes` read raw off a\n * screen version is a msgpack `Buffer`, and `Object.entries` of a Buffer\n * yields byte INDICES paired with NUMBERS. Every check below asks a node for a\n * `props` object, finds none, and the walk completes reporting a clean design.\n * A scanner that cannot read its input must say so, not pass it.\n *\n * `decodeStoredNodes` handles the three storage forms, so anything still\n * failing here was never a node map to begin with.\n */\nfunction isNodeMap(value: unknown): value is Record<string, unknown> {\n if (!value || typeof value !== 'object' || Array.isArray(value)) return false\n const entries = Object.values(value as Record<string, unknown>)\n if (!entries.length) return false\n return entries.every(\n (entry) => Boolean(entry) && typeof entry === 'object' && !Array.isArray(entry),\n )\n}\n\n/**\n * Reads a marketplace email design against the mail-client policy above.\n *\n * Takes `nodes` in whatever form the document stored it — a plain Firestore\n * map, compressed msgpack bytes, or the `{type:'Buffer'}` envelope an export\n * bundle carries — and decodes before walking, because a naive walk over the\n * compressed form finds nothing and reports the design clean.\n *\n * @param raw - the version document's `nodes` field, undecoded.\n * @param options - carries `hostId`, the site the design is being installed\n * onto, used only to classify media scopes as local or foreign. Omitted at\n * publish time, where there is no installing site yet.\n */\nexport function inspectEmailStarter(\n raw: unknown,\n options?: { hostId?: string | null },\n): EmailStarterInspection {\n const decoded = decodeStoredNodes<Record<string, unknown>>(raw)\n if (!isNodeMap(decoded)) {\n return {\n ok: false,\n violations: [\n {\n code: 'unreadable',\n nodeId: 'nodes',\n componentId: '',\n prop: 'nodes',\n value: decoded === null ? 'undecodable' : 'not a node map',\n },\n ],\n linkHosts: [],\n foreignMediaScopes: [],\n nodes: null,\n }\n }\n\n const violations: EmailStarterViolation[] = []\n const hosts = new Set<string>()\n const foreignScopes = new Set<string>()\n\n for (const [nodeId, value] of Object.entries(decoded)) {\n const node = value as { componentId?: unknown; props?: unknown }\n const componentId = String(node.componentId ?? '')\n const props =\n node.props && typeof node.props === 'object' && !Array.isArray(node.props)\n ? (node.props as Record<string, unknown>)\n : {}\n\n for (const prop of MARKUP_PROPS) {\n if (props[prop] === undefined) continue\n violations.push({\n code: 'raw-markup',\n nodeId,\n componentId,\n prop,\n value: truncate(props[prop]),\n })\n }\n\n for (const prop of URL_PROPS) {\n const value = props[prop]\n if (typeof value !== 'string' || !value) continue\n\n // Checked before the transport rules, and reported as its own code,\n // because the two failures need different sentences: an `http:` link is\n // a mistake to correct, and a merge tag in a URL is recipient data on\n // its way into a query string — a standing prohibition, and the reason\n // it is prohibited is that a URL is logged by every hop that serves it.\n if (value.includes(MERGE_TAG_OPEN)) {\n violations.push({\n code: 'merge-tag-in-url',\n nodeId,\n componentId,\n prop,\n value: truncate(value),\n })\n continue\n }\n\n if (prop === 'src') {\n if (isMediaRef(value)) {\n const ref = parseMediaRef(value)\n if (!ref) {\n violations.push({\n code: 'remote-asset',\n nodeId,\n componentId,\n prop,\n value: truncate(value),\n })\n continue\n }\n if (options?.hostId && !ref.scope.split(':').includes(options.hostId)) {\n foreignScopes.add(ref.scope)\n }\n continue\n }\n if (isMediaCdnPath(value) || INLINE_IMAGE.test(value)) continue\n violations.push({\n code: 'remote-asset',\n nodeId,\n componentId,\n prop,\n value: truncate(value),\n })\n continue\n }\n\n const host = linkHost(value)\n if (host) hosts.add(host)\n if (!MAIL_SAFE_HREF.test(value)) {\n violations.push({\n code: 'unsafe-link',\n nodeId,\n componentId,\n prop,\n value: truncate(value),\n })\n }\n }\n }\n\n return {\n ok: violations.length === 0,\n violations,\n linkHosts: [...hosts].sort(),\n foreignMediaScopes: [...foreignScopes].sort(),\n nodes: decoded,\n }\n}\n\n/** One sentence per violation code, in the second person. */\nconst VIOLATION_COPY: Record<EmailStarterViolationCode, string> = {\n 'remote-asset':\n 'images must come from your own media library or be inline — an image ' +\n 'loaded from another site tells that site who opened the email',\n 'unsafe-link':\n 'links must be https, mailto, or a path on the sending site',\n 'merge-tag-in-url':\n 'a merge tag may not appear in a link or image address — that puts ' +\n 'recipient data into a URL',\n 'raw-markup':\n 'raw HTML cannot be published as an email design; build it from email ' +\n 'blocks',\n unreadable: 'the design could not be read',\n}\n\n/**\n * A refusal message for a failed inspection, or `undefined` when it passed.\n *\n * One message naming ONE violation and its count, rather than a list: the\n * caller is an API route whose body is rendered in a dialog, and a publisher\n * fixing eleven images fixes them the same way they fix the first.\n */\nexport function emailStarterRefusal(\n inspection: EmailStarterInspection,\n): string | undefined {\n const [first] = inspection.violations\n if (!first) return undefined\n const more = inspection.violations.length - 1\n const at = first.componentId ? ` (on a ${first.componentId} block)` : ''\n return (\n `This email design cannot be published or installed: ` +\n `${VIOLATION_COPY[first.code]}${at}. Found \"${first.value}\"` +\n (more > 0 ? ` and ${more} more like it.` : '.')\n )\n}\n\n/**\n * How much is actually known about the version of an email starter that was\n * installed.\n *\n * `unreviewed` is a real state, not a missing one, and it is the DEFAULT.\n */\nexport type EmailStarterAssurance = 'approved' | 'unreviewed' | 'rejected'\n\n/**\n * The assurance of ONE version, read from that version's own document.\n *\n * Deliberately takes the version and nothing else. A listing carries\n * `reviewStatus: 'verified'` and a `latestVersionReviewState` mirror, and both\n * describe the listing or its newest build — neither is a statement about the\n * version somebody is installing right now. Passing the listing in here would\n * let version 7 inherit version 3's approval, which is the precise failure the\n * per-version model exists to prevent, and it would do it silently.\n *\n * An absent `reviewState` is `unreviewed`, never `approved`: an email starter\n * is auto-listed like every other copied artifact, so most versions have no\n * verdict at all and the honest answer is that nobody has looked.\n */\nexport function resolveEmailStarterAssurance(\n version: { reviewState?: unknown } | null | undefined,\n): EmailStarterAssurance {\n const state = String(version?.reviewState ?? '')\n if (state === 'approved') return 'approved'\n if (state === 'rejected') return 'rejected'\n return 'unreviewed'\n}\n\n/** What the console says about an assurance, so no surface invents its own. */\nexport function emailStarterAssuranceLabel(\n assurance: EmailStarterAssurance,\n): string {\n switch (assurance) {\n case 'approved':\n return 'Reviewed by Aglyn'\n case 'rejected':\n return 'Turned down in review'\n default:\n return 'Not reviewed'\n }\n}\n\n/** Why an installed email design may not be mailed. */\nexport interface EmailStarterSendBlock {\n code: 'killed'\n /** Rendered to the tenant; staff's reason when they recorded one. */\n reason: string\n}\n\n/**\n * Whether the kill switch stops this installed design from being SENT.\n *\n * ## What a kill has to reach, and why the other levers do not reach it\n *\n * An email starter is copied on install: the tree becomes a screen document in\n * the tenant's own site and nothing reads the listing again. That is the right\n * versioning model — a publisher's push must not rewrite an email a tenant is\n * mailing from — but it means every lever the marketplace already had stops at\n * the storefront. `deletedAt` unpublishes, `hiddenAt` blocks browse and blocks\n * the next install, and a rejected version was never installable. None of them\n * is felt by a tenant who installed last week and is sending today.\n *\n * So the kill switch is the one that has to reach through, and it is checked\n * HERE — at the send — rather than by deleting or rewriting the tenant's\n * document. Refusing the send and leaving the content alone is the same rule\n * capacity enforcement follows: a control that reaches a customer's own data\n * to enforce a decision about somebody else's artifact takes the wrong thing\n * away. The tenant keeps the design, keeps editing it, keeps previewing it;\n * what they cannot do is put it on the shared sending domain.\n *\n * Rejection is deliberately NOT a kill and must not become one here: a version\n * turned down in review was never installable, so nothing is running it, and\n * conflating the two would let a review verdict silently stop live mail.\n *\n * A listing-wide kill catches an install whose provenance never recorded a\n * version (everything installed before the provenance stamp). A per-version\n * kill cannot, and does not pretend to — `isPluginRevoked` answers false for\n * an empty version against a version list, which is the honest reading.\n */\nexport function emailStarterSendBlock(input: {\n /** The install stamp on the document being sent. */\n installedFrom: { listingId?: string | null; version?: string | null } | null | undefined\n /** `revocations/{listingId}`, or null when there is none. */\n revocation: PluginRevocation | null | undefined\n}): EmailStarterSendBlock | null {\n const listingId = input.installedFrom?.listingId\n if (!listingId || !input.revocation) return null\n const version = String(input.installedFrom?.version ?? '')\n const killed = version\n ? isPluginRevoked(input.revocation, version)\n : isListingWideRevocation(input.revocation)\n if (!killed) return null\n const reason = String(input.revocation.reason ?? '').trim()\n return {\n code: 'killed',\n reason:\n 'This email was installed from a marketplace template that Aglyn has ' +\n 'since stopped' +\n (reason ? `: ${reason}. ` : '. ') +\n 'Sending from it is blocked. The design is still here and still ' +\n 'editable, and a campaign built from your own design sends normally.',\n }\n}\n"],"names":["isMediaCdnPath","isMediaRef","parseMediaRef","isListingWideRevocation","isPluginRevoked","decodeStoredNodes","URL_PROPS","MARKUP_PROPS","MERGE_TAG_OPEN","INLINE_IMAGE","MAIL_SAFE_HREF","truncate","value","text","String","length","slice","linkHost","test","URL","host","toLowerCase","isNodeMap","Array","isArray","entries","Object","values","every","entry","Boolean","inspectEmailStarter","raw","options","decoded","ok","violations","code","nodeId","componentId","prop","linkHosts","foreignMediaScopes","nodes","hosts","Set","foreignScopes","node","props","undefined","push","includes","ref","hostId","scope","split","add","sort","VIOLATION_COPY","unreadable","emailStarterRefusal","inspection","first","more","at","resolveEmailStarterAssurance","version","state","reviewState","emailStarterAssuranceLabel","assurance","emailStarterSendBlock","input","listingId","installedFrom","revocation","killed","reason","trim"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/email-starter-policy.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 * What a marketplace email starter may contain, and whether one may still be\n * mailed.\n *\n * In core rather than beside the rest of the marketplace model for the reason\n * `artifact-provenance` gives: three different projects have to ask these\n * questions and a `scope:app` project may not depend on an `aglyn:addons` lib.\n * `marketplace-verification` used to be cited here too and no longer can be —\n * its console reader became the plugin's own surface, so the policy went with\n * it (AGL-3080). A shared reason is only shared while both halves still hold. The marketplace plugin enforces the\n * policy when content is published and installed; the marketing plugin asks\n * the kill question on the send path; the console renders the assurance.\n * Three readings of \"is this template safe to mail\" is the failure mode this\n * module exists to prevent.\n *\n * ## Why an email design needs a policy the page allowlist does not supply\n *\n * A published page component is rendered by us, inside a document we serve,\n * behind our CSP. A published EMAIL design is rendered by the recipient's mail\n * client, out of our reach entirely, and it leaves on a sending domain whose\n * reputation every tenant shares. The two facts that follow are the whole\n * reason this file is stricter than `sanitizePortableDefinition`:\n *\n * - **A remote asset in an email is a read receipt.** The recipient's client\n * fetches every `src` on open, so whatever host the string names learns that\n * this person opened this message, at this time, from this IP. Naming a\n * host the PUBLISHER controls hands a third party a recipient-level\n * engagement feed on the tenant's audience — processing with no recorded\n * basis and no `assertedBy` provenance, which is exactly what the consent\n * architecture exists to make impossible. A 1×1 pixel is the deliberate form;\n * an ordinary hero image is the same disclosure with a picture attached.\n * - **A link is sent under the tenant's name.** A recipient reads the From\n * header, not the publisher's listing page.\n *\n * So `src` is held to first-party or inline, and `href` to a transport a\n * recipient's client will not silently downgrade. Violations are REFUSED\n * rather than stripped: `sanitizePublishedNodeProps` drops an unsafe URL and\n * publishes the rest, which is right for a page — a missing image is a\n * cosmetic defect — and wrong here, because the publisher whose tracker was\n * quietly removed learns nothing and tries again.\n */\n\nimport { isMediaCdnPath, isMediaRef, parseMediaRef } from './media-ref'\nimport { isListingWideRevocation, isPluginRevoked } from './plugin-manifest'\nimport type { PluginRevocation } from './plugin-manifest'\nimport { decodeStoredNodes } from './stored-nodes'\n\n/** Props that carry a URL into a recipient's mail client. */\nconst URL_PROPS = ['src', 'href'] as const\n\n/**\n * Props whose value is markup rather than text.\n *\n * `emailHtml` is already off `PORTABLE_EMAIL_COMPONENT_ID_ALLOWLIST`, so a\n * publish cannot mint one. This catches the same content arriving as a PROP on\n * a block that is on the list — a hand-written document, or a block that grows\n * an `html` prop later — which the component-id allowlist alone would pass.\n */\nconst MARKUP_PROPS = ['html', 'dangerouslySetInnerHTML'] as const\n\n/** Merge-tag opener. Recipient data must never reach a URL (see below). */\nconst MERGE_TAG_OPEN = '{{'\n\n/** Inline image data, which is inert and discloses nothing on open. */\nconst INLINE_IMAGE = /^data:image\\//i\n\n/** Link transports a recipient's client will not silently downgrade. */\nconst MAIL_SAFE_HREF = /^(https:\\/\\/|mailto:|\\/|#)/i\n\nexport type EmailStarterViolationCode =\n /** A `src` naming a host that is not ours — the read-receipt vector. */\n | 'remote-asset'\n /** An `href` on a transport we will not send under a tenant's name. */\n | 'unsafe-link'\n /** Recipient data heading for a URL. */\n | 'merge-tag-in-url'\n /** Markup we would have to trust rather than render. */\n | 'raw-markup'\n /** The node map could not be read, so nothing about it is known. */\n | 'unreadable'\n\nexport interface EmailStarterViolation {\n code: EmailStarterViolationCode\n /** The offending node's id, or `'nodes'` when the map itself is the fault. */\n nodeId: string\n componentId: string\n /** The prop that carried it, for a message that can be acted on. */\n prop: string\n /** Truncated: a violation is reported to a publisher, and often logged. */\n value: string\n}\n\nexport interface EmailStarterInspection {\n ok: boolean\n violations: EmailStarterViolation[]\n /**\n * Every host this design would send a recipient to, sorted and deduplicated.\n *\n * The inspectable half of the link policy. A tenant about to mail their\n * customers under their own From header is entitled to see the list of\n * domains a stranger's template points them at, and no allowlist can decide\n * that for them — a link to a legitimate retailer and a link to the\n * publisher's affiliate funnel are the same shape.\n */\n linkHosts: string[]\n /**\n * Media scopes belonging to somebody other than the installing site.\n *\n * Not a violation: the asset is served by our own CDN, so the publisher\n * learns nothing about the recipient, and refusing would make almost every\n * real template uninstallable. It is REPORTED because the reference stays\n * live after a copy-on-install — the publisher can still replace the bytes\n * behind it — which is the one way an installed design can change without\n * an update being taken.\n */\n foreignMediaScopes: string[]\n /** The decoded map, so a caller need not decode twice. Null when unreadable. */\n nodes: Record<string, unknown> | null\n}\n\nfunction truncate(value: unknown): string {\n const text = typeof value === 'string' ? value : String(value ?? '')\n return text.length > 120 ? `${text.slice(0, 117)}...` : text\n}\n\n/**\n * The host a link points at, or null for a same-origin or non-URL target.\n *\n * `URL` rather than a regex because the question is \"which host will the mail\n * client resolve this to\", and that is the parser's answer, not ours. A value\n * that will not parse is not reported as a host — it is already refused by the\n * transport check, and inventing a host for it would put a fiction on a screen\n * a tenant is using to make a decision.\n */\nfunction linkHost(value: string): string | null {\n if (!/^https?:\\/\\//i.test(value)) return null\n try {\n return new URL(value).host.toLowerCase() || null\n } catch {\n return null\n }\n}\n\n/**\n * Whether a decoded value is a node MAP rather than something that merely\n * survived decoding.\n *\n * The case this exists for is the AGL-1394 shape: `nodes` read raw off a\n * screen version is a msgpack `Buffer`, and `Object.entries` of a Buffer\n * yields byte INDICES paired with NUMBERS. Every check below asks a node for a\n * `props` object, finds none, and the walk completes reporting a clean design.\n * A scanner that cannot read its input must say so, not pass it.\n *\n * `decodeStoredNodes` handles the three storage forms, so anything still\n * failing here was never a node map to begin with.\n */\nfunction isNodeMap(value: unknown): value is Record<string, unknown> {\n if (!value || typeof value !== 'object' || Array.isArray(value)) return false\n const entries = Object.values(value as Record<string, unknown>)\n if (!entries.length) return false\n return entries.every(\n (entry) => Boolean(entry) && typeof entry === 'object' && !Array.isArray(entry),\n )\n}\n\n/**\n * Reads a marketplace email design against the mail-client policy above.\n *\n * Takes `nodes` in whatever form the document stored it — a plain Firestore\n * map, compressed msgpack bytes, or the `{type:'Buffer'}` envelope an export\n * bundle carries — and decodes before walking, because a naive walk over the\n * compressed form finds nothing and reports the design clean.\n *\n * @param raw - the version document's `nodes` field, undecoded.\n * @param options - carries `hostId`, the site the design is being installed\n * onto, used only to classify media scopes as local or foreign. Omitted at\n * publish time, where there is no installing site yet.\n */\nexport function inspectEmailStarter(\n raw: unknown,\n options?: { hostId?: string | null },\n): EmailStarterInspection {\n const decoded = decodeStoredNodes<Record<string, unknown>>(raw)\n if (!isNodeMap(decoded)) {\n return {\n ok: false,\n violations: [\n {\n code: 'unreadable',\n nodeId: 'nodes',\n componentId: '',\n prop: 'nodes',\n value: decoded === null ? 'undecodable' : 'not a node map',\n },\n ],\n linkHosts: [],\n foreignMediaScopes: [],\n nodes: null,\n }\n }\n\n const violations: EmailStarterViolation[] = []\n const hosts = new Set<string>()\n const foreignScopes = new Set<string>()\n\n for (const [nodeId, value] of Object.entries(decoded)) {\n const node = value as { componentId?: unknown; props?: unknown }\n const componentId = String(node.componentId ?? '')\n const props =\n node.props && typeof node.props === 'object' && !Array.isArray(node.props)\n ? (node.props as Record<string, unknown>)\n : {}\n\n for (const prop of MARKUP_PROPS) {\n if (props[prop] === undefined) continue\n violations.push({\n code: 'raw-markup',\n nodeId,\n componentId,\n prop,\n value: truncate(props[prop]),\n })\n }\n\n for (const prop of URL_PROPS) {\n const value = props[prop]\n if (typeof value !== 'string' || !value) continue\n\n // Checked before the transport rules, and reported as its own code,\n // because the two failures need different sentences: an `http:` link is\n // a mistake to correct, and a merge tag in a URL is recipient data on\n // its way into a query string — a standing prohibition, and the reason\n // it is prohibited is that a URL is logged by every hop that serves it.\n if (value.includes(MERGE_TAG_OPEN)) {\n violations.push({\n code: 'merge-tag-in-url',\n nodeId,\n componentId,\n prop,\n value: truncate(value),\n })\n continue\n }\n\n if (prop === 'src') {\n if (isMediaRef(value)) {\n const ref = parseMediaRef(value)\n if (!ref) {\n violations.push({\n code: 'remote-asset',\n nodeId,\n componentId,\n prop,\n value: truncate(value),\n })\n continue\n }\n if (options?.hostId && !ref.scope.split(':').includes(options.hostId)) {\n foreignScopes.add(ref.scope)\n }\n continue\n }\n if (isMediaCdnPath(value) || INLINE_IMAGE.test(value)) continue\n violations.push({\n code: 'remote-asset',\n nodeId,\n componentId,\n prop,\n value: truncate(value),\n })\n continue\n }\n\n const host = linkHost(value)\n if (host) hosts.add(host)\n if (!MAIL_SAFE_HREF.test(value)) {\n violations.push({\n code: 'unsafe-link',\n nodeId,\n componentId,\n prop,\n value: truncate(value),\n })\n }\n }\n }\n\n return {\n ok: violations.length === 0,\n violations,\n linkHosts: [...hosts].sort(),\n foreignMediaScopes: [...foreignScopes].sort(),\n nodes: decoded,\n }\n}\n\n/** One sentence per violation code, in the second person. */\nconst VIOLATION_COPY: Record<EmailStarterViolationCode, string> = {\n 'remote-asset':\n 'images must come from your own media library or be inline — an image ' +\n 'loaded from another site tells that site who opened the email',\n 'unsafe-link':\n 'links must be https, mailto, or a path on the sending site',\n 'merge-tag-in-url':\n 'a merge tag may not appear in a link or image address — that puts ' +\n 'recipient data into a URL',\n 'raw-markup':\n 'raw HTML cannot be published as an email design; build it from email ' +\n 'blocks',\n unreadable: 'the design could not be read',\n}\n\n/**\n * A refusal message for a failed inspection, or `undefined` when it passed.\n *\n * One message naming ONE violation and its count, rather than a list: the\n * caller is an API route whose body is rendered in a dialog, and a publisher\n * fixing eleven images fixes them the same way they fix the first.\n */\nexport function emailStarterRefusal(\n inspection: EmailStarterInspection,\n): string | undefined {\n const [first] = inspection.violations\n if (!first) return undefined\n const more = inspection.violations.length - 1\n const at = first.componentId ? ` (on a ${first.componentId} block)` : ''\n return (\n `This email design cannot be published or installed: ` +\n `${VIOLATION_COPY[first.code]}${at}. Found \"${first.value}\"` +\n (more > 0 ? ` and ${more} more like it.` : '.')\n )\n}\n\n/**\n * How much is actually known about the version of an email starter that was\n * installed.\n *\n * `unreviewed` is a real state, not a missing one, and it is the DEFAULT.\n */\nexport type EmailStarterAssurance = 'approved' | 'unreviewed' | 'rejected'\n\n/**\n * The assurance of ONE version, read from that version's own document.\n *\n * Deliberately takes the version and nothing else. A listing carries\n * `reviewStatus: 'verified'` and a `latestVersionReviewState` mirror, and both\n * describe the listing or its newest build — neither is a statement about the\n * version somebody is installing right now. Passing the listing in here would\n * let version 7 inherit version 3's approval, which is the precise failure the\n * per-version model exists to prevent, and it would do it silently.\n *\n * An absent `reviewState` is `unreviewed`, never `approved`: an email starter\n * is auto-listed like every other copied artifact, so most versions have no\n * verdict at all and the honest answer is that nobody has looked.\n */\nexport function resolveEmailStarterAssurance(\n version: { reviewState?: unknown } | null | undefined,\n): EmailStarterAssurance {\n const state = String(version?.reviewState ?? '')\n if (state === 'approved') return 'approved'\n if (state === 'rejected') return 'rejected'\n return 'unreviewed'\n}\n\n/** What the console says about an assurance, so no surface invents its own. */\nexport function emailStarterAssuranceLabel(\n assurance: EmailStarterAssurance,\n): string {\n switch (assurance) {\n case 'approved':\n return 'Reviewed by Aglyn'\n case 'rejected':\n return 'Turned down in review'\n default:\n return 'Not reviewed'\n }\n}\n\n/** Why an installed email design may not be mailed. */\nexport interface EmailStarterSendBlock {\n code: 'killed'\n /** Rendered to the tenant; staff's reason when they recorded one. */\n reason: string\n}\n\n/**\n * Whether the kill switch stops this installed design from being SENT.\n *\n * ## What a kill has to reach, and why the other levers do not reach it\n *\n * An email starter is copied on install: the tree becomes a screen document in\n * the tenant's own site and nothing reads the listing again. That is the right\n * versioning model — a publisher's push must not rewrite an email a tenant is\n * mailing from — but it means every lever the marketplace already had stops at\n * the storefront. `deletedAt` unpublishes, `hiddenAt` blocks browse and blocks\n * the next install, and a rejected version was never installable. None of them\n * is felt by a tenant who installed last week and is sending today.\n *\n * So the kill switch is the one that has to reach through, and it is checked\n * HERE — at the send — rather than by deleting or rewriting the tenant's\n * document. Refusing the send and leaving the content alone is the same rule\n * capacity enforcement follows: a control that reaches a customer's own data\n * to enforce a decision about somebody else's artifact takes the wrong thing\n * away. The tenant keeps the design, keeps editing it, keeps previewing it;\n * what they cannot do is put it on the shared sending domain.\n *\n * Rejection is deliberately NOT a kill and must not become one here: a version\n * turned down in review was never installable, so nothing is running it, and\n * conflating the two would let a review verdict silently stop live mail.\n *\n * A listing-wide kill catches an install whose provenance never recorded a\n * version (everything installed before the provenance stamp). A per-version\n * kill cannot, and does not pretend to — `isPluginRevoked` answers false for\n * an empty version against a version list, which is the honest reading.\n */\nexport function emailStarterSendBlock(input: {\n /** The install stamp on the document being sent. */\n installedFrom: { listingId?: string | null; version?: string | null } | null | undefined\n /** `revocations/{listingId}`, or null when there is none. */\n revocation: PluginRevocation | null | undefined\n}): EmailStarterSendBlock | null {\n const listingId = input.installedFrom?.listingId\n if (!listingId || !input.revocation) return null\n const version = String(input.installedFrom?.version ?? '')\n const killed = version\n ? isPluginRevoked(input.revocation, version)\n : isListingWideRevocation(input.revocation)\n if (!killed) return null\n const reason = String(input.revocation.reason ?? '').trim()\n return {\n code: 'killed',\n reason:\n 'This email was installed from a marketplace template that Aglyn has ' +\n 'since stopped' +\n (reason ? `: ${reason}. ` : '. ') +\n 'Sending from it is blocked. The design is still here and still ' +\n 'editable, and a campaign built from your own design sends normally.',\n }\n}\n"],"names":["isMediaCdnPath","isMediaRef","parseMediaRef","isListingWideRevocation","isPluginRevoked","decodeStoredNodes","URL_PROPS","MARKUP_PROPS","MERGE_TAG_OPEN","INLINE_IMAGE","MAIL_SAFE_HREF","truncate","value","text","String","length","slice","linkHost","test","URL","host","toLowerCase","isNodeMap","Array","isArray","entries","Object","values","every","entry","Boolean","inspectEmailStarter","raw","options","decoded","ok","violations","code","nodeId","componentId","prop","linkHosts","foreignMediaScopes","nodes","hosts","Set","foreignScopes","node","props","undefined","push","includes","ref","hostId","scope","split","add","sort","VIOLATION_COPY","unreadable","emailStarterRefusal","inspection","first","more","at","resolveEmailStarterAssurance","version","state","reviewState","emailStarterAssuranceLabel","assurance","emailStarterSendBlock","input","listingId","installedFrom","revocation","killed","reason","trim"],"mappings":"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"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* READING A RECEIVED EMAIL (AGL-2657): the address in a header value, the
|
|
19
|
+
* thread a subject belongs to, the words of an HTML part, the message a
|
|
20
|
+
* forward carries and the reply above the quoted history.
|
|
21
|
+
*
|
|
22
|
+
* Pure, and the platform's: every surface that receives mail — a capture
|
|
23
|
+
* address filing a message on a record, a sequence reading a prospect's
|
|
24
|
+
* reply — reads a message the same way, so two of them can never disagree
|
|
25
|
+
* about who wrote it or what they said.
|
|
26
|
+
*/
|
|
27
|
+
/** The most a subject keeps, which is also what a one-to-one email may send. */
|
|
28
|
+
export declare const EMAIL_SUBJECT_MAX = 200;
|
|
29
|
+
/** The most an excerpt keeps: a letter, not a thread. */
|
|
30
|
+
export declare const EMAIL_EXCERPT_MAX = 4000;
|
|
31
|
+
/**
|
|
32
|
+
* The bare address in a header value — `Ada <ada@example.com>` and
|
|
33
|
+
* `ada@example.com` both answer `ada@example.com` — normalized as every
|
|
34
|
+
* contact address is, or `null` for anything that is not one.
|
|
35
|
+
*/
|
|
36
|
+
export declare function emailAddressOf(value: unknown): string | null;
|
|
37
|
+
/** The part after the `@`, or `null` — what an unmatched message is logged by. */
|
|
38
|
+
export declare function emailDomainOf(value: unknown): string | null;
|
|
39
|
+
/**
|
|
40
|
+
* The subject with its reply and forward prefixes removed, however many
|
|
41
|
+
* were stacked — `Re: Fwd: RE: Renewal` is the `Renewal` thread — and
|
|
42
|
+
* whitespace collapsed, so two rows of one conversation carry one value.
|
|
43
|
+
*/
|
|
44
|
+
export declare function threadSubject(subject: unknown): string;
|
|
45
|
+
/**
|
|
46
|
+
* The words in an HTML part, for a message that carried no text part.
|
|
47
|
+
* Block boundaries become line breaks, tags go, the common entities are
|
|
48
|
+
* read back — a best effort over a bounded slice, never a renderer.
|
|
49
|
+
*/
|
|
50
|
+
export declare function htmlToPlainText(html: unknown): string;
|
|
51
|
+
/** What a forwarded message's header block says, and the words after it. */
|
|
52
|
+
export interface ForwardedSection {
|
|
53
|
+
/** The address on the block's `From:` line, or `null` when it carried none. */
|
|
54
|
+
from: string | null;
|
|
55
|
+
/** The forwarded message's own text, header block removed. */
|
|
56
|
+
body: string;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The forwarded message inside a forward — the block a mail client puts
|
|
60
|
+
* under "Forwarded message" or, with no marker, a run of `From:` … `Subject:`
|
|
61
|
+
* header lines — or `null` when the text carries none. The `From:` line is
|
|
62
|
+
* where the correspondent of a forwarded reply is named, since the forward's
|
|
63
|
+
* own headers name only the teammate who forwarded it.
|
|
64
|
+
*/
|
|
65
|
+
export declare function forwardedSection(text: string): ForwardedSection | null;
|
|
66
|
+
/**
|
|
67
|
+
* The words above the quoted history: everything before the first `On …
|
|
68
|
+
* wrote:` line, quote mark, "Original Message" rule or reply header block.
|
|
69
|
+
* The intro may wrap onto a second line, so a line that begins `On` is
|
|
70
|
+
* read together with the one after it.
|
|
71
|
+
*/
|
|
72
|
+
export declare function stripQuotedHistory(text: string): string;
|
|
73
|
+
/**
|
|
74
|
+
* What is worth keeping of a received message: the text part — or the
|
|
75
|
+
* words of the HTML part when there is none — with a forward's header
|
|
76
|
+
* block and the quoted history below the reply removed, whitespace
|
|
77
|
+
* settled, bounded. Never the whole thread: the thread is in the mailbox,
|
|
78
|
+
* and a record of the message is a note that this exchange happened.
|
|
79
|
+
*/
|
|
80
|
+
export declare function emailExcerpt(text: unknown, html?: unknown): string;
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ import { normalizeContactEmail } from "./contacts.js";
|
|
17
|
+
/**
|
|
18
|
+
* READING A RECEIVED EMAIL (AGL-2657): the address in a header value, the
|
|
19
|
+
* thread a subject belongs to, the words of an HTML part, the message a
|
|
20
|
+
* forward carries and the reply above the quoted history.
|
|
21
|
+
*
|
|
22
|
+
* Pure, and the platform's: every surface that receives mail — a capture
|
|
23
|
+
* address filing a message on a record, a sequence reading a prospect's
|
|
24
|
+
* reply — reads a message the same way, so two of them can never disagree
|
|
25
|
+
* about who wrote it or what they said.
|
|
26
|
+
*/ /** The most a subject keeps, which is also what a one-to-one email may send. */ export const EMAIL_SUBJECT_MAX = 200;
|
|
27
|
+
/** The most an excerpt keeps: a letter, not a thread. */ export const EMAIL_EXCERPT_MAX = 4000;
|
|
28
|
+
/**
|
|
29
|
+
* The bare address in a header value — `Ada <ada@example.com>` and
|
|
30
|
+
* `ada@example.com` both answer `ada@example.com` — normalized as every
|
|
31
|
+
* contact address is, or `null` for anything that is not one.
|
|
32
|
+
*/ export function emailAddressOf(value) {
|
|
33
|
+
const raw = String(value != null ? value : '').trim();
|
|
34
|
+
const angled = /<([^<>]+)>\s*$/.exec(raw);
|
|
35
|
+
return normalizeContactEmail(angled ? angled[1] : raw);
|
|
36
|
+
}
|
|
37
|
+
/** The part after the `@`, or `null` — what an unmatched message is logged by. */ export function emailDomainOf(value) {
|
|
38
|
+
const address = emailAddressOf(value);
|
|
39
|
+
if (!address) return null;
|
|
40
|
+
const at = address.lastIndexOf('@');
|
|
41
|
+
return at > 0 ? address.slice(at + 1) : null;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The subject with its reply and forward prefixes removed, however many
|
|
45
|
+
* were stacked — `Re: Fwd: RE: Renewal` is the `Renewal` thread — and
|
|
46
|
+
* whitespace collapsed, so two rows of one conversation carry one value.
|
|
47
|
+
*/ export function threadSubject(subject) {
|
|
48
|
+
let value = String(subject != null ? subject : '').replace(/\s+/g, ' ').trim();
|
|
49
|
+
const prefix = /^(re|fwd?|aw|wg|tr|sv|vs)\s*(\[\d+\])?\s*:\s*/i;
|
|
50
|
+
while(prefix.test(value))value = value.replace(prefix, '');
|
|
51
|
+
return value.slice(0, EMAIL_SUBJECT_MAX);
|
|
52
|
+
}
|
|
53
|
+
const ENTITIES = {
|
|
54
|
+
amp: '&',
|
|
55
|
+
lt: '<',
|
|
56
|
+
gt: '>',
|
|
57
|
+
quot: '"',
|
|
58
|
+
apos: "'",
|
|
59
|
+
nbsp: ' '
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* The words in an HTML part, for a message that carried no text part.
|
|
63
|
+
* Block boundaries become line breaks, tags go, the common entities are
|
|
64
|
+
* read back — a best effort over a bounded slice, never a renderer.
|
|
65
|
+
*/ export function htmlToPlainText(html) {
|
|
66
|
+
return String(html != null ? html : '').slice(0, 200000).replace(/<(script|style)[\s\S]*?<\/\1>/gi, '').replace(/<br\s*\/?>/gi, '\n').replace(/<\/(p|div|li|tr|h[1-6]|blockquote|pre)>/gi, '\n').replace(/<[^>]+>/g, '').replace(/&(#\d+|#x[0-9a-f]+|[a-z]+);/gi, (whole, code)=>{
|
|
67
|
+
var _ENTITIES_code_toLowerCase;
|
|
68
|
+
if (code.startsWith('#x') || code.startsWith('#X')) {
|
|
69
|
+
return String.fromCodePoint(parseInt(code.slice(2), 16));
|
|
70
|
+
}
|
|
71
|
+
if (code.startsWith('#')) return String.fromCodePoint(parseInt(code.slice(1), 10));
|
|
72
|
+
return (_ENTITIES_code_toLowerCase = ENTITIES[code.toLowerCase()]) != null ? _ENTITIES_code_toLowerCase : whole;
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
const FORWARD_MARKER = /^\s*(-{2,}\s*Forwarded message\s*-{2,}|Begin forwarded message:)\s*$/i;
|
|
76
|
+
const HEADER_LINE = /^\s*\*?(From|Sent|Date|To|Cc|Subject|Reply-To)\s*:\*?\s*(.*)$/i;
|
|
77
|
+
/**
|
|
78
|
+
* The forwarded message inside a forward — the block a mail client puts
|
|
79
|
+
* under "Forwarded message" or, with no marker, a run of `From:` … `Subject:`
|
|
80
|
+
* header lines — or `null` when the text carries none. The `From:` line is
|
|
81
|
+
* where the correspondent of a forwarded reply is named, since the forward's
|
|
82
|
+
* own headers name only the teammate who forwarded it.
|
|
83
|
+
*/ export function forwardedSection(text) {
|
|
84
|
+
const lines = text.split('\n');
|
|
85
|
+
let start = -1;
|
|
86
|
+
for(let index = 0; index < lines.length; index += 1){
|
|
87
|
+
if (FORWARD_MARKER.test(lines[index])) {
|
|
88
|
+
start = index + 1;
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
if (/^\s*\*?From\s*:/i.test(lines[index]) && !lines[index].trimStart().startsWith('>')) {
|
|
92
|
+
const window = lines.slice(index, index + 8);
|
|
93
|
+
if (window.some((line)=>/^\s*\*?Subject\s*:/i.test(line))) {
|
|
94
|
+
start = index;
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (start < 0) return null;
|
|
100
|
+
let from = null;
|
|
101
|
+
let cursor = start;
|
|
102
|
+
// The header block: header lines, blank lines between them allowed, up to
|
|
103
|
+
// the first line that is neither.
|
|
104
|
+
let sawHeader = false;
|
|
105
|
+
while(cursor < lines.length){
|
|
106
|
+
const line = lines[cursor];
|
|
107
|
+
const header = HEADER_LINE.exec(line);
|
|
108
|
+
if (header) {
|
|
109
|
+
sawHeader = true;
|
|
110
|
+
if (header[1].toLowerCase() === 'from') from = from != null ? from : emailAddressOf(header[2]);
|
|
111
|
+
cursor += 1;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (!line.trim() && !sawHeader) {
|
|
115
|
+
cursor += 1;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (!line.trim() && sawHeader) {
|
|
119
|
+
var _lines_;
|
|
120
|
+
// A blank line after a header run may precede more headers (Apple
|
|
121
|
+
// Mail) or the body; look past it.
|
|
122
|
+
const next = (_lines_ = lines[cursor + 1]) != null ? _lines_ : '';
|
|
123
|
+
if (HEADER_LINE.test(next)) {
|
|
124
|
+
cursor += 1;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
cursor += 1;
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
from,
|
|
134
|
+
body: lines.slice(cursor).join('\n')
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
const QUOTE_INTRO = /^\s*On\b.{0,300}$/;
|
|
138
|
+
const QUOTE_END = /wrote:\s*$/i;
|
|
139
|
+
const HISTORY_MARKERS = [
|
|
140
|
+
/^\s*>/,
|
|
141
|
+
/^\s*-{2,}\s*Original Message\s*-{2,}\s*$/i,
|
|
142
|
+
/^\s*_{5,}\s*$/,
|
|
143
|
+
/^\s*From:\s.+$/,
|
|
144
|
+
/^\s*Sent from my\b/i,
|
|
145
|
+
/^\s*Le .{0,200} a écrit\s*:\s*$/i,
|
|
146
|
+
/^\s*Am .{0,200} schrieb .{0,100}:\s*$/i
|
|
147
|
+
];
|
|
148
|
+
/**
|
|
149
|
+
* The words above the quoted history: everything before the first `On …
|
|
150
|
+
* wrote:` line, quote mark, "Original Message" rule or reply header block.
|
|
151
|
+
* The intro may wrap onto a second line, so a line that begins `On` is
|
|
152
|
+
* read together with the one after it.
|
|
153
|
+
*/ export function stripQuotedHistory(text) {
|
|
154
|
+
const lines = text.split('\n');
|
|
155
|
+
let end = lines.length;
|
|
156
|
+
for(let index = 0; index < lines.length; index += 1){
|
|
157
|
+
const line = lines[index];
|
|
158
|
+
if (HISTORY_MARKERS.some((marker)=>marker.test(line))) {
|
|
159
|
+
end = index;
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
if (QUOTE_INTRO.test(line)) {
|
|
163
|
+
var _lines_;
|
|
164
|
+
const joined = `${line} ${(_lines_ = lines[index + 1]) != null ? _lines_ : ''}`;
|
|
165
|
+
if (QUOTE_END.test(line) || QUOTE_END.test(joined)) {
|
|
166
|
+
end = index;
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return lines.slice(0, end).join('\n');
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* What is worth keeping of a received message: the text part — or the
|
|
175
|
+
* words of the HTML part when there is none — with a forward's header
|
|
176
|
+
* block and the quoted history below the reply removed, whitespace
|
|
177
|
+
* settled, bounded. Never the whole thread: the thread is in the mailbox,
|
|
178
|
+
* and a record of the message is a note that this exchange happened.
|
|
179
|
+
*/ export function emailExcerpt(text, html) {
|
|
180
|
+
let body = String(text != null ? text : '');
|
|
181
|
+
if (!body.trim() && html) body = htmlToPlainText(html);
|
|
182
|
+
body = body.replace(/\r\n?/g, '\n').replace(/\u00a0/g, ' ');
|
|
183
|
+
const forwarded = forwardedSection(body);
|
|
184
|
+
if (forwarded) body = forwarded.body;
|
|
185
|
+
body = stripQuotedHistory(body);
|
|
186
|
+
return body.split('\n').map((line)=>line.replace(/[ \t]+$/g, '')).join('\n').replace(/\n{3,}/g, '\n\n').trim().slice(0, EMAIL_EXCERPT_MAX);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
//# sourceMappingURL=email-text.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/email-text.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 { normalizeContactEmail } from './contacts'\n\n/**\n * READING A RECEIVED EMAIL (AGL-2657): the address in a header value, the\n * thread a subject belongs to, the words of an HTML part, the message a\n * forward carries and the reply above the quoted history.\n *\n * Pure, and the platform's: every surface that receives mail — a capture\n * address filing a message on a record, a sequence reading a prospect's\n * reply — reads a message the same way, so two of them can never disagree\n * about who wrote it or what they said.\n */\n\n/** The most a subject keeps, which is also what a one-to-one email may send. */\nexport const EMAIL_SUBJECT_MAX = 200\n\n/** The most an excerpt keeps: a letter, not a thread. */\nexport const EMAIL_EXCERPT_MAX = 4_000\n\n/**\n * The bare address in a header value — `Ada <ada@example.com>` and\n * `ada@example.com` both answer `ada@example.com` — normalized as every\n * contact address is, or `null` for anything that is not one.\n */\nexport function emailAddressOf(value: unknown): string | null {\n const raw = String(value ?? '').trim()\n const angled = /<([^<>]+)>\\s*$/.exec(raw)\n return normalizeContactEmail(angled ? angled[1] : raw)\n}\n\n/** The part after the `@`, or `null` — what an unmatched message is logged by. */\nexport function emailDomainOf(value: unknown): string | null {\n const address = emailAddressOf(value)\n if (!address) return null\n const at = address.lastIndexOf('@')\n return at > 0 ? address.slice(at + 1) : null\n}\n\n/**\n * The subject with its reply and forward prefixes removed, however many\n * were stacked — `Re: Fwd: RE: Renewal` is the `Renewal` thread — and\n * whitespace collapsed, so two rows of one conversation carry one value.\n */\nexport function threadSubject(subject: unknown): string {\n let value = String(subject ?? '')\n .replace(/\\s+/g, ' ')\n .trim()\n const prefix = /^(re|fwd?|aw|wg|tr|sv|vs)\\s*(\\[\\d+\\])?\\s*:\\s*/i\n while (prefix.test(value)) value = value.replace(prefix, '')\n return value.slice(0, EMAIL_SUBJECT_MAX)\n}\n\nconst ENTITIES: Record<string, string> = {\n amp: '&',\n lt: '<',\n gt: '>',\n quot: '\"',\n apos: \"'\",\n nbsp: ' ',\n}\n\n/**\n * The words in an HTML part, for a message that carried no text part.\n * Block boundaries become line breaks, tags go, the common entities are\n * read back — a best effort over a bounded slice, never a renderer.\n */\nexport function htmlToPlainText(html: unknown): string {\n return String(html ?? '')\n .slice(0, 200_000)\n .replace(/<(script|style)[\\s\\S]*?<\\/\\1>/gi, '')\n .replace(/<br\\s*\\/?>/gi, '\\n')\n .replace(/<\\/(p|div|li|tr|h[1-6]|blockquote|pre)>/gi, '\\n')\n .replace(/<[^>]+>/g, '')\n .replace(/&(#\\d+|#x[0-9a-f]+|[a-z]+);/gi, (whole, code: string) => {\n if (code.startsWith('#x') || code.startsWith('#X')) {\n return String.fromCodePoint(parseInt(code.slice(2), 16))\n }\n if (code.startsWith('#')) return String.fromCodePoint(parseInt(code.slice(1), 10))\n return ENTITIES[code.toLowerCase()] ?? whole\n })\n}\n\n/** What a forwarded message's header block says, and the words after it. */\nexport interface ForwardedSection {\n /** The address on the block's `From:` line, or `null` when it carried none. */\n from: string | null\n /** The forwarded message's own text, header block removed. */\n body: string\n}\n\nconst FORWARD_MARKER = /^\\s*(-{2,}\\s*Forwarded message\\s*-{2,}|Begin forwarded message:)\\s*$/i\nconst HEADER_LINE = /^\\s*\\*?(From|Sent|Date|To|Cc|Subject|Reply-To)\\s*:\\*?\\s*(.*)$/i\n\n/**\n * The forwarded message inside a forward — the block a mail client puts\n * under \"Forwarded message\" or, with no marker, a run of `From:` … `Subject:`\n * header lines — or `null` when the text carries none. The `From:` line is\n * where the correspondent of a forwarded reply is named, since the forward's\n * own headers name only the teammate who forwarded it.\n */\nexport function forwardedSection(text: string): ForwardedSection | null {\n const lines = text.split('\\n')\n let start = -1\n for (let index = 0; index < lines.length; index += 1) {\n if (FORWARD_MARKER.test(lines[index])) {\n start = index + 1\n break\n }\n if (/^\\s*\\*?From\\s*:/i.test(lines[index]) && !lines[index].trimStart().startsWith('>')) {\n const window = lines.slice(index, index + 8)\n if (window.some((line) => /^\\s*\\*?Subject\\s*:/i.test(line))) {\n start = index\n break\n }\n }\n }\n if (start < 0) return null\n let from: string | null = null\n let cursor = start\n // The header block: header lines, blank lines between them allowed, up to\n // the first line that is neither.\n let sawHeader = false\n while (cursor < lines.length) {\n const line = lines[cursor]\n const header = HEADER_LINE.exec(line)\n if (header) {\n sawHeader = true\n if (header[1].toLowerCase() === 'from') from = from ?? emailAddressOf(header[2])\n cursor += 1\n continue\n }\n if (!line.trim() && !sawHeader) {\n cursor += 1\n continue\n }\n if (!line.trim() && sawHeader) {\n // A blank line after a header run may precede more headers (Apple\n // Mail) or the body; look past it.\n const next = lines[cursor + 1] ?? ''\n if (HEADER_LINE.test(next)) {\n cursor += 1\n continue\n }\n cursor += 1\n break\n }\n break\n }\n return { from, body: lines.slice(cursor).join('\\n') }\n}\n\nconst QUOTE_INTRO = /^\\s*On\\b.{0,300}$/\nconst QUOTE_END = /wrote:\\s*$/i\nconst HISTORY_MARKERS = [\n /^\\s*>/,\n /^\\s*-{2,}\\s*Original Message\\s*-{2,}\\s*$/i,\n /^\\s*_{5,}\\s*$/,\n /^\\s*From:\\s.+$/,\n /^\\s*Sent from my\\b/i,\n /^\\s*Le .{0,200} a écrit\\s*:\\s*$/i,\n /^\\s*Am .{0,200} schrieb .{0,100}:\\s*$/i,\n]\n\n/**\n * The words above the quoted history: everything before the first `On …\n * wrote:` line, quote mark, \"Original Message\" rule or reply header block.\n * The intro may wrap onto a second line, so a line that begins `On` is\n * read together with the one after it.\n */\nexport function stripQuotedHistory(text: string): string {\n const lines = text.split('\\n')\n let end = lines.length\n for (let index = 0; index < lines.length; index += 1) {\n const line = lines[index]\n if (HISTORY_MARKERS.some((marker) => marker.test(line))) {\n end = index\n break\n }\n if (QUOTE_INTRO.test(line)) {\n const joined = `${line} ${lines[index + 1] ?? ''}`\n if (QUOTE_END.test(line) || QUOTE_END.test(joined)) {\n end = index\n break\n }\n }\n }\n return lines.slice(0, end).join('\\n')\n}\n\n/**\n * What is worth keeping of a received message: the text part — or the\n * words of the HTML part when there is none — with a forward's header\n * block and the quoted history below the reply removed, whitespace\n * settled, bounded. Never the whole thread: the thread is in the mailbox,\n * and a record of the message is a note that this exchange happened.\n */\nexport function emailExcerpt(text: unknown, html?: unknown): string {\n let body = String(text ?? '')\n if (!body.trim() && html) body = htmlToPlainText(html)\n body = body.replace(/\\r\\n?/g, '\\n').replace(/\\u00a0/g, ' ')\n const forwarded = forwardedSection(body)\n if (forwarded) body = forwarded.body\n body = stripQuotedHistory(body)\n return body\n .split('\\n')\n .map((line) => line.replace(/[ \\t]+$/g, ''))\n .join('\\n')\n .replace(/\\n{3,}/g, '\\n\\n')\n .trim()\n .slice(0, EMAIL_EXCERPT_MAX)\n}\n"],"names":["normalizeContactEmail","EMAIL_SUBJECT_MAX","EMAIL_EXCERPT_MAX","emailAddressOf","value","raw","String","trim","angled","exec","emailDomainOf","address","at","lastIndexOf","slice","threadSubject","subject","replace","prefix","test","ENTITIES","amp","lt","gt","quot","apos","nbsp","htmlToPlainText","html","whole","code","startsWith","fromCodePoint","parseInt","toLowerCase","FORWARD_MARKER","HEADER_LINE","forwardedSection","text","lines","split","start","index","length","trimStart","window","some","line","from","cursor","sawHeader","header","next","body","join","QUOTE_INTRO","QUOTE_END","HISTORY_MARKERS","stripQuotedHistory","end","marker","joined","emailExcerpt","forwarded","map"],"mappings":"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"}
|
|
@@ -17,6 +17,10 @@
|
|
|
17
17
|
/**
|
|
18
18
|
* What basis lets somebody be put on a marketing list.
|
|
19
19
|
*
|
|
20
|
+
* The enrollment-time half of `marketing-consent.ts`: consent to the MARKETING
|
|
21
|
+
* send purpose, which every surface that enrolls a person in an audience owes
|
|
22
|
+
* whichever plugin keeps the audience. It names no plugin's lists.
|
|
23
|
+
*
|
|
20
24
|
* Pure policy, no Firestore and no network, so the rule that decides whether
|
|
21
25
|
* somebody joins a marketing audience can be asserted without an enrollment
|
|
22
26
|
* path.
|
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
*/ /**
|
|
17
17
|
* What basis lets somebody be put on a marketing list.
|
|
18
18
|
*
|
|
19
|
+
* The enrollment-time half of `marketing-consent.ts`: consent to the MARKETING
|
|
20
|
+
* send purpose, which every surface that enrolls a person in an audience owes
|
|
21
|
+
* whichever plugin keeps the audience. It names no plugin's lists.
|
|
22
|
+
*
|
|
19
23
|
* Pure policy, no Firestore and no network, so the rule that decides whether
|
|
20
24
|
* somebody joins a marketing audience can be asserted without an enrollment
|
|
21
25
|
* path.
|
|
@@ -167,4 +171,4 @@
|
|
|
167
171
|
'no-basis': 'This person has no marketing opt-in on record, so they can only be added if you state that you have their permission.'
|
|
168
172
|
};
|
|
169
173
|
|
|
170
|
-
//# sourceMappingURL=
|
|
174
|
+
//# sourceMappingURL=enrollment-basis.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/enrollment-basis.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 * What basis lets somebody be put on a marketing list.\n *\n * The enrollment-time half of `marketing-consent.ts`: consent to the MARKETING\n * send purpose, which every surface that enrolls a person in an audience owes\n * whichever plugin keeps the audience. It names no plugin's lists.\n *\n * Pure policy, no Firestore and no network, so the rule that decides whether\n * somebody joins a marketing audience can be asserted without an enrollment\n * path.\n *\n * ## Why it is here and not beside one of its callers\n *\n * There are two enrollment surfaces and neither can own this: the Inbox\n * assignment route (`@aglyn/plugins-inbox`) enrolls the sender of a form\n * submission, and the Emails console's audience card enrolls an address a\n * merchant types or pastes. `plugins-inbox` already depends on\n * `plugins-email`, so the second could not have imported the first, and a\n * copy in each is the shape that ends with one surface honoring a stored\n * refusal and the other not. The basis a membership carries has to be one\n * rule with one set of words, so it lives in the framework both import.\n *\n * ## A reply is transactional; a list is marketing\n *\n * These are two acts and this module exists because they are constantly\n * mistaken for one. Answering a form submission is the transaction the person\n * started, so the Inbox's `reply-policy.ts` requires no consent record.\n * Putting them on a list is a standing invitation to mail them about things\n * they never asked about, and it requires a basis. Nothing in the reply path\n * reaches this module and nothing here sends anything.\n *\n * ## An enrollment is not a license to send\n *\n * Passing this rule writes a membership and nothing else. It meters nothing,\n * sends nothing, and is not a promise the person is still mailable when a\n * campaign eventually runs — suppression is consulted again at send time, in\n * `filterSendableForHost`, because an address can be suppressed the day after\n * it is enrolled. `apps/console/specs/an-enrollment-is-not-a-license-to-send.spec.ts`\n * holds the two halves together end to end.\n *\n * ## A basis is DECLARED, never inferred\n *\n * The act that produced the address is not a basis. Somebody who filled in a\n * contact form asked to be answered; that is the whole of what they asked for,\n * and an address a merchant has in a spreadsheet says less than that. Neither\n * is writing to the merchant, buying, booking or holding an account — the\n * shared `marketing-consent` module makes the same point for the send-time\n * join, and this is the enrollment-time half of it. So there are exactly two\n * ways in:\n *\n * 1. the person's own record already carries a stored opt-in, which is\n * carried across unchanged, timestamp and all; or\n * 2. the merchant states that they have this person's permission, and THAT\n * STATEMENT IS ITSELF THE RECORDED BASIS — stored with the account that\n * made it and the moment they did.\n *\n * There is no third way and, in particular, no unattributed one. The absence\n * of a record is reported to the merchant as an absence rather than resolved\n * quietly in either direction.\n *\n * ## A stored refusal ends it\n *\n * `declined` is checked first and answers before anything else is considered,\n * including an attestation. A merchant cannot attest their way past somebody\n * who said no; that is the one thing an attestation must not be able to buy,\n * because if it could there would be no difference between recording a\n * refusal and discarding it.\n */\n\nimport {\n OPERATOR_ATTESTED_CONSENT_BASIS,\n type MarketingConsentRecord,\n} from './marketing-consent'\n\n/**\n * What is wrong with the ADDRESS, before anybody asks what is wrong with the\n * consent.\n *\n * Four failures that are properties of the address rather than of the act, so\n * every surface that touches an address shares them: a form with no email\n * field has nobody to enroll for the same reason it has nobody to answer, and\n * a bounced address is unmailable whichever route reached it. The Inbox's\n * `ReplyRefusal` is this type — one union, aliased there — because a reply\n * and an enrollment refuse an address for the same four reasons and a second\n * copy would eventually list three.\n */\nexport type AddressRefusal =\n /** No field the resolver recognizes as an email — nothing to enroll. */\n | 'no-address'\n /** The value is not a routable address. */\n | 'unroutable-address'\n /** The address is on the platform-wide bounce/complaint list. */\n | 'suppressed-platform'\n /** The address unsubscribed from, or bounced on, this site. */\n | 'suppressed-host'\n\n/** Why an address cannot be put on a list. Each maps to one refusal message. */\nexport type AssignmentRefusal =\n | AddressRefusal\n /** A stored refusal. Never enrollable, and there is no override. */\n | 'declined'\n /** No record either way, and the merchant asserted nothing. */\n | 'no-basis'\n\n/** The basis an enrollment would carry, resolved server-side. */\nexport interface AssignmentBasisDecision {\n /*\n * The attested value is the EXPORTED CONSTANT and not a literal spelled\n * again here. `readMarketingBasis` recognizes an attestation by comparing\n * the stored field against that constant, so a second spelling on the\n * deciding side is a rename away from writing a basis the reader reports as\n * the person's own act — the one conflation this whole area exists to stop.\n */\n basis: 'contact-opt-in' | typeof OPERATOR_ATTESTED_CONSENT_BASIS\n /** When the basis was recorded — the person's own moment, or the merchant's. */\n atMs: number\n /** The account answerable for an attestation; `null` for a pass-through. */\n byUid: string | null\n}\n\n/**\n * Decides what basis, if any, an enrollment may carry.\n *\n * The client never names the basis. It says only whether the merchant is\n * asserting one, and the pass-through is derived here from the person's own\n * record — a caller that could claim `contact-opt-in` would be a caller that\n * can manufacture an opt-in for anybody by naming it.\n *\n * @param stored the person's consent facts, read off their own record.\n * @param attested whether the merchant asserted permission in this request.\n */\nexport function assignmentBasis(input: {\n stored: MarketingConsentRecord\n attested: boolean\n actingUid: string\n nowMs: number\n}): AssignmentBasisDecision | { refusal: AssignmentRefusal } {\n // First, and above the attestation on purpose. See the module note.\n if (input.stored.basis === 'declined') return { refusal: 'declined' }\n if (input.stored.basis === 'granted') {\n return {\n basis: 'contact-opt-in',\n /*\n * The person's own timestamp, and `nowMs` only when their record\n * carries none. A pass-through that restamped the moment would report\n * every historical opt-in as having happened when a merchant pressed a\n * button, which moves records across the enforcement cutoff the consent\n * policy grandfathers on and makes the audit answer the wrong question.\n */\n atMs: input.stored.basisAtMs ?? input.nowMs,\n byUid: null,\n }\n }\n if (!input.attested) return { refusal: 'no-basis' }\n return {\n basis: OPERATOR_ATTESTED_CONSENT_BASIS,\n atMs: input.nowMs,\n // Never optional for this basis: an attestation nobody is named for is\n // indistinguishable from an opt-in, which is the conflation the whole\n // module exists to prevent.\n byUid: input.actingUid,\n }\n}\n\n/** What the merchant is shown before they decide. */\nexport interface AssignmentReadout {\n /** Can this person be enrolled at all, by any answer the merchant gives? */\n enrollable: boolean\n /**\n * Must the merchant assert permission for the enrollment to proceed?\n *\n * False both when a stored opt-in already carries it and when nothing can\n * carry it, so the UI reads this alone to decide whether to show the\n * assertion control — a surface that showed it on a refusal would offer an\n * override that does not exist.\n */\n requiresAttestation: boolean\n /** One sentence of consent facts, in the merchant's terms. */\n summary: string\n}\n\n/**\n * The readout for one address, from the same inputs the decision uses.\n *\n * It lives here rather than in the component so the sentence on screen and\n * the rule that runs on the server cannot drift: a UI that computed its own\n * summary would eventually tell a merchant that somebody may be added and\n * then refuse them, or — far worse — say a check was performed that was not.\n *\n * @param suppression a suppression refusal for the address, when there is\n * one. It outranks consent in the readout because it is\n * the answer regardless of what any consent record says.\n */\nexport function assignmentReadout(input: {\n stored: MarketingConsentRecord\n suppression: AddressRefusal | null\n}): AssignmentReadout {\n if (input.suppression) {\n return {\n enrollable: false,\n requiresAttestation: false,\n summary: ASSIGNMENT_REFUSAL_MESSAGES[input.suppression],\n }\n }\n if (input.stored.basis === 'declined') {\n return {\n enrollable: false,\n requiresAttestation: false,\n summary: ASSIGNMENT_REFUSAL_MESSAGES['declined'],\n }\n }\n if (input.stored.basis === 'granted') {\n const when = input.stored.basisAtMs\n ? ` on ${new Date(input.stored.basisAtMs).toLocaleDateString()}`\n : ''\n return {\n enrollable: true,\n requiresAttestation: false,\n summary: `This person opted in to marketing email${when}. Adding them carries that opt-in across.`,\n }\n }\n return {\n enrollable: true,\n requiresAttestation: true,\n summary:\n 'There is no marketing opt-in on record for this person. Sending this ' +\n 'form does not create one. To add them you have to state that you have ' +\n 'their permission, and that statement is recorded against your account.',\n }\n}\n\n/** What a refusal says to the merchant. One line, naming the cause. */\nexport const ASSIGNMENT_REFUSAL_MESSAGES: Record<AssignmentRefusal, string> = {\n 'no-address':\n 'This submission has no email field, so there is nobody to add to a list.',\n 'unroutable-address':\n 'The email address on this submission is not a valid address.',\n 'suppressed-platform':\n 'This address bounced or reported a message as spam, so it cannot be mailed or added to a list.',\n 'suppressed-host':\n 'This address unsubscribed from this site, so it cannot be mailed or added to a list.',\n declined:\n 'This person declined marketing email. They cannot be added to a list, and there is no way to override that.',\n 'no-basis':\n 'This person has no marketing opt-in on record, so they can only be added if you state that you have their permission.',\n}\n"],"names":["OPERATOR_ATTESTED_CONSENT_BASIS","assignmentBasis","input","stored","basis","refusal","atMs","basisAtMs","nowMs","byUid","attested","actingUid","assignmentReadout","suppression","enrollable","requiresAttestation","summary","ASSIGNMENT_REFUSAL_MESSAGES","when","Date","toLocaleDateString","declined"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmEC,GAED,SACEA,+BAA+B,QAE1B,yBAAqB;AAgD5B;;;;;;;;;;CAUC,GACD,OAAO,SAASC,gBAAgBC,KAK/B;IACC,oEAAoE;IACpE,IAAIA,MAAMC,MAAM,CAACC,KAAK,KAAK,YAAY,OAAO;QAAEC,SAAS;IAAW;IACpE,IAAIH,MAAMC,MAAM,CAACC,KAAK,KAAK,WAAW;YAU5BF;QATR,OAAO;YACLE,OAAO;YACP;;;;;;OAMC,GACDE,IAAI,GAAEJ,0BAAAA,MAAMC,MAAM,CAACI,SAAS,YAAtBL,0BAA0BA,MAAMM,KAAK;YAC3CC,OAAO;QACT;IACF;IACA,IAAI,CAACP,MAAMQ,QAAQ,EAAE,OAAO;QAAEL,SAAS;IAAW;IAClD,OAAO;QACLD,OAAOJ;QACPM,MAAMJ,MAAMM,KAAK;QACjB,uEAAuE;QACvE,sEAAsE;QACtE,4BAA4B;QAC5BC,OAAOP,MAAMS,SAAS;IACxB;AACF;AAmBA;;;;;;;;;;;CAWC,GACD,OAAO,SAASC,kBAAkBV,KAGjC;IACC,IAAIA,MAAMW,WAAW,EAAE;QACrB,OAAO;YACLC,YAAY;YACZC,qBAAqB;YACrBC,SAASC,2BAA2B,CAACf,MAAMW,WAAW,CAAC;QACzD;IACF;IACA,IAAIX,MAAMC,MAAM,CAACC,KAAK,KAAK,YAAY;QACrC,OAAO;YACLU,YAAY;YACZC,qBAAqB;YACrBC,SAASC,2BAA2B,CAAC,WAAW;QAClD;IACF;IACA,IAAIf,MAAMC,MAAM,CAACC,KAAK,KAAK,WAAW;QACpC,MAAMc,OAAOhB,MAAMC,MAAM,CAACI,SAAS,GAC/B,CAAC,IAAI,EAAE,IAAIY,KAAKjB,MAAMC,MAAM,CAACI,SAAS,EAAEa,kBAAkB,IAAI,GAC9D;QACJ,OAAO;YACLN,YAAY;YACZC,qBAAqB;YACrBC,SAAS,CAAC,uCAAuC,EAAEE,KAAK,yCAAyC,CAAC;QACpG;IACF;IACA,OAAO;QACLJ,YAAY;QACZC,qBAAqB;QACrBC,SACE,0EACA,2EACA;IACJ;AACF;AAEA,qEAAqE,GACrE,OAAO,MAAMC,8BAAiE;IAC5E,cACE;IACF,sBACE;IACF,uBACE;IACF,mBACE;IACFI,UACE;IACF,YACE;AACJ,EAAC"}
|
|
@@ -72,12 +72,17 @@ export interface EntityPickerContextValue {
|
|
|
72
72
|
/** The host's form entities, id-first, feeding FORM_SELECT attributes. */
|
|
73
73
|
forms?: EntityOption[];
|
|
74
74
|
/**
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
75
|
+
* The fields an entity's documents declare, keyed by kind and then by
|
|
76
|
+
* entity id, in the order the entity declares them: id = the stable field
|
|
77
|
+
* id a node persists, label = its current display name (AGL-556).
|
|
78
|
+
*
|
|
79
|
+
* Only kinds whose documents carry a field model appear. Two surfaces read
|
|
80
|
+
* it: a field picker that maps a value onto one of an entity's fields (a
|
|
81
|
+
* form field's "Maps to schema field"), and the insert-token menu inside a
|
|
82
|
+
* repeat, which offers `{{item.<field>}}` for each field of the entity the
|
|
83
|
+
* repeat names (see `RepeatSource.entityKind`).
|
|
79
84
|
*/
|
|
80
|
-
|
|
85
|
+
entityFields?: Partial<Record<EntityPickerKind, Readonly<Record<string, EntityOption[]>>>>;
|
|
81
86
|
/**
|
|
82
87
|
* Declare that a picker of this kind is on screen (AGL-703).
|
|
83
88
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/entity-picker-context.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// Same placement rationale as screen-link-context.ts: lives in\n// @aglyn/aglyn without a 'use client' banner so canvas-rendering surfaces\n// share one module graph.\nimport { createContext } from 'react'\n\nexport interface EntityOption {\n /** Stable document id — the persisted reference (AGL-343/344). */\n id: string\n /** Current display name, resolved at edit time. */\n label: string\n}\n\n/**\n * Edit-time option lists for id-based entity pickers in component\n * attributes (products, collections, categories, datasets, forms). Names are\n * display-only; nodes persist the id, so renames never break references —\n * the same contract as {@link ScreenLinkContext}. Provided by the\n * console's besigner/preview surfaces; absent on the tenant (the tenant\n * only resolves ids, never lists them).\n */\n/** The lists a picker can ask for. */\nexport type EntityPickerKind =\n | 'products'\n | 'collections'\n | 'categories'\n | 'datasets'\n | 'forms'\n\n/**\n * How much an empty list is worth believing.\n *\n * Every one of these renders as a dropdown with no entries, and an author\n * looking at four identical empty dropdowns cannot tell \"this site has no\n * forms yet\" from \"the read failed\" from \"this editor never lists forms at\n * all\". Only the first of those is a fact about their site; the other two\n * are the picker being broken, and a picker that cannot say which it is\n * sends the author looking for a form they already made.\n *\n * - `unavailable` — no provider is mounted, so nothing on this surface can\n * list entities. The tenant, and any editor outside a host.\n * - `loading` — a read is open or about to be, and no answer has arrived.\n * Also the state of a list nothing has asked for yet: demand is declared\n * on the same commit that would display it, so \"not asked\" and \"asked,\n * unanswered\" are the same beat.\n * - `ready` — the read settled. An empty list here IS the site's answer.\n * - `error` — the read failed. Empty means nothing.\n */\nexport type EntityListState = 'unavailable' | 'loading' | 'ready' | 'error'\n\n/**\n * How many documents a picker BROWSES.\n *\n * A picker is not a catalog viewer. It is a dropdown on a besigner surface\n * where the author is placing one element, and no table in the console offers\n * a page past `TABLE_PAGE_SIZE_OPTIONS` — a dropdown is the smaller surface of\n * the two, not the larger.\n *\n * The middle of that range rather than its floor: a table has a pager under it\n * and a dropdown does not, so the page a reader cannot advance is worth more\n * rows than the one they can.\n *\n * What makes this size SAFE is that browsing is the only job it does. A stored\n * value is named by {@link EntityPickerContextValue.resolve} — one keyed read\n * for the one document, exact at any catalog size — so nothing here has to be\n * wide enough to contain whatever an author picked last month. A window that\n * carried both jobs would have to be, and would still render a bound element\n * as unbound past its own width; widening this cannot fix that, only make it\n * rarer and more expensive.\n */\nexport const ENTITY_PICKER_BROWSE_LIMIT = 25\n\n/**\n * How many documents a server-side name search may return.\n *\n * The same size as the browse window, for the same reason: a reader who has\n * typed a query and still faces 25 rows narrows it further rather than\n * scrolling. Only the kinds in {@link EntityPickerContextValue.searchable}\n * spend this at all.\n */\nexport const ENTITY_PICKER_SEARCH_LIMIT = 25\n\nexport interface EntityPickerContextValue {\n products?: EntityOption[]\n collections?: EntityOption[]\n categories?: EntityOption[]\n datasets?: EntityOption[]\n /** The host's form entities, id-first, feeding FORM_SELECT attributes. */\n forms?: EntityOption[]\n /**\n * Per-dataset model fields (AGL-556), keyed by dataset id, in model\n * order: id = stable model fieldId, label = current display name. Feeds\n * DATASET_FIELD_SELECT attributes (e.g. a form field's \"Maps to schema\n * field\" picker).\n */\n datasetFields?: Record<string, EntityOption[]>\n /**\n * Declare that a picker of this kind is on screen (AGL-703).\n *\n * The provider reads these lists from Firestore, and it used to read ALL of\n * them the moment the besigner opened — up to 300 products, 200 catalog\n * collections, 200 categories and 200 datasets, on a site with a real\n * catalog, for pickers most editing sessions never open. Nothing about\n * moving a heading needs the product list.\n *\n * So the lists are demand-driven now, and the demand signal is the thing\n * that would DISPLAY one: a selected node whose schema declares a\n * `PRODUCT_SELECT`, a repeat bound to a dataset. Call this when that is\n * true and read the list on the render after; until then the list is\n * absent, exactly as it is for the beat before any listener syncs.\n *\n * Optional, and absent on the tenant — which resolves ids and never lists\n * them, so nothing there has a picker to ask for.\n */\n request?: (kind: EntityPickerKind) => void\n /**\n * Per-kind read state, so a picker can say WHY it has nothing to offer.\n * A kind the provider omits reads as `loading`; see {@link entityListState}.\n */\n status?: Partial<Record<EntityPickerKind, EntityListState>>\n /**\n * The browse window came back FULL for this kind, so the list above is a\n * page of the site's catalog and not the whole of it.\n *\n * Measured with a probe — the read asks for one document past\n * {@link ENTITY_PICKER_BROWSE_LIMIT} and the extra one is never offered —\n * so this is a fact about the collection rather than a guess from the\n * window being exactly full.\n *\n * A picker owes the reader this sentence for the same reason the inbox's\n * form filter does: \"not in the picker\" and \"does not exist\" are the same\n * empty dropdown, and only one of them is a fact about their site.\n */\n truncated?: Partial<Record<EntityPickerKind, boolean>>\n /**\n * This kind's documents carry the name-search keys, so a typed query\n * reaches the WHOLE collection rather than only the browse window.\n *\n * True for products alone today: `nameTokens`/`nameLower` are stamped by\n * the catalog's own write path, and `hosts/{host}/resources` deliberately\n * does not stamp them on the other four kinds. A picker for a kind that is\n * absent here must say its search covers the window only — claiming\n * otherwise would let an unfound entity read as a nonexistent one.\n */\n searchable?: Partial<Record<EntityPickerKind, boolean>>\n /**\n * Narrow this kind's offered list to what somebody typed.\n *\n * Matching is the provider's, not the dropdown's: it is the only side that\n * can merge the window's own matches with the ones a server query found\n * past it, and it uses `scoreMatch` so a query matches a word ANYWHERE in a\n * name — the same rule the switchers and global search follow.\n *\n * Passing an empty string restores the browse window.\n */\n search?: (kind: EntityPickerKind, text: string) => void\n /**\n * Resolve ONE stored id to its current name, whatever the browse window\n * holds.\n *\n * Deliberately NOT a job for the browse list, though a bulk read could do\n * both. Sharing one read makes the list's width the limit on which stored\n * values can be named, and past that width a picker renders a bound element\n * as UNBOUND rather than as its label — an empty control on an element that\n * is set, which an author repairs by picking again. That is how a correct\n * reference gets replaced with a different one.\n *\n * One keyed `getDoc` for one document instead: exact at any catalog size,\n * and asked for only when a node actually carries a value. Results land in\n * {@link resolved} and are never re-read.\n */\n resolve?: (kind: EntityPickerKind, id: string) => void\n /**\n * What {@link resolve} found, keyed by kind and then by id.\n *\n * Three states, and the picker renders a different label for each: an\n * `EntityOption` is the resolved name, `null` is a settled read that found\n * NO such document, and an absent key is a resolution still in flight.\n * Absent must never render as missing — a picker that flashed \"unavailable\"\n * over every live reference for the beat before its read landed would teach\n * authors to ignore the one warning that matters.\n */\n resolved?: Partial<\n Record<EntityPickerKind, Readonly<Record<string, EntityOption | null>>>\n >\n}\n\n/**\n * What the picker for `kind` should believe about its own emptiness.\n *\n * `request` is the \"a provider is mounted\" signal — it is the one member the\n * contract already says is absent wherever nothing lists entities — so a\n * surface with no provider resolves to `unavailable` rather than passing an\n * empty list off as the site's answer.\n */\nexport function entityListState(\n context: EntityPickerContextValue | undefined,\n kind: EntityPickerKind,\n): EntityListState {\n if (!context?.request) return 'unavailable'\n return context.status?.[kind] ?? 'loading'\n}\n\n/**\n * The id whose label a picker still needs a keyed read to learn, if any.\n *\n * Part of the contract rather than of one screen, because two surfaces ask\n * it — the attributes panel about a node's picker value, and the insert-token\n * menu about the dataset a repeat is bound to — and an answer that differed\n * between them would be a second keyed read for a document one of them\n * already had.\n *\n * Four cases resolve to \"ask nobody\", and each is a read not spent: there is\n * no value; the browse window already carries it, which is the common case on\n * a small site; a resolution has already answered, including the answer that\n * the document is gone; or the browse read has not settled, so the window may\n * be about to supply it for free.\n *\n * The last is the one worth stating. Asking while the listener is still\n * loading would spend a keyed read on nearly every picker on every site,\n * which is the cost splitting these two jobs exists to remove rather than to\n * move.\n */\nexport function entityValueNeedsResolution(\n context: EntityPickerContextValue | undefined,\n kind: EntityPickerKind,\n value: unknown,\n): string | undefined {\n const id = typeof value === 'string' ? value.trim() : ''\n if (!id || !context?.resolve) return undefined\n if ((context[kind] ?? []).some((entity) => entity.id === id)) return undefined\n if (id in (context.resolved?.[kind] ?? {})) return undefined\n if (entityListState(context, kind) !== 'ready') return undefined\n return id\n}\n\nexport const EntityPickerContext = createContext<EntityPickerContextValue>({})\nEntityPickerContext.displayName = 'EntityPickerContext'\n"],"names":["createContext","ENTITY_PICKER_BROWSE_LIMIT","ENTITY_PICKER_SEARCH_LIMIT","entityListState","context","kind","request","status","entityValueNeedsResolution","value","id","trim","resolve","undefined","some","entity","resolved","EntityPickerContext","displayName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD,+DAA+D;AAC/D,0EAA0E;AAC1E,0BAA0B;AAC1B,SAASA,aAAa,QAAQ,QAAO;AA8CrC;;;;;;;;;;;;;;;;;;;CAmBC,GACD,OAAO,MAAMC,6BAA6B,GAAE;AAE5C;;;;;;;CAOC,GACD,OAAO,MAAMC,6BAA6B,GAAE;AA2G5C;;;;;;;CAOC,GACD,OAAO,SAASC,gBACdC,OAA6C,EAC7CC,IAAsB;;QAGfD;IADP,IAAI,EAACA,2BAAAA,QAASE,OAAO,GAAE,OAAO;IAC9B,gBAAOF,kBAAAA,QAAQG,MAAM,qBAAdH,eAAgB,CAACC,KAAK,mBAAI;AACnC;AAEA;;;;;;;;;;;;;;;;;;;CAmBC,GACD,OAAO,SAASG,2BACdJ,OAA6C,EAC7CC,IAAsB,EACtBI,KAAc;QAITL;QACMA;IAHX,MAAMM,KAAK,OAAOD,UAAU,WAAWA,MAAME,IAAI,KAAK;IACtD,IAAI,CAACD,MAAM,EAACN,2BAAAA,QAASQ,OAAO,GAAE,OAAOC;IACrC,IAAI,EAACT,gBAAAA,OAAO,CAACC,KAAK,YAAbD,gBAAiB,EAAE,EAAEU,IAAI,CAAC,CAACC,SAAWA,OAAOL,EAAE,KAAKA,KAAK,OAAOG;IACrE,IAAIH,gBAAON,oBAAAA,QAAQY,QAAQ,qBAAhBZ,iBAAkB,CAACC,KAAK,mBAAI,CAAC,IAAI,OAAOQ;IACnD,IAAIV,gBAAgBC,SAASC,UAAU,SAAS,OAAOQ;IACvD,OAAOH;AACT;AAEA,OAAO,MAAMO,sBAAsBjB,cAAwC,CAAC,GAAE;AAC9EiB,oBAAoBC,WAAW,GAAG"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/entity-picker-context.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// Same placement rationale as screen-link-context.ts: lives in\n// @aglyn/aglyn without a 'use client' banner so canvas-rendering surfaces\n// share one module graph.\nimport { createContext } from 'react'\n\nexport interface EntityOption {\n /** Stable document id — the persisted reference (AGL-343/344). */\n id: string\n /** Current display name, resolved at edit time. */\n label: string\n}\n\n/**\n * Edit-time option lists for id-based entity pickers in component\n * attributes (products, collections, categories, datasets, forms). Names are\n * display-only; nodes persist the id, so renames never break references —\n * the same contract as {@link ScreenLinkContext}. Provided by the\n * console's besigner/preview surfaces; absent on the tenant (the tenant\n * only resolves ids, never lists them).\n */\n/** The lists a picker can ask for. */\nexport type EntityPickerKind =\n | 'products'\n | 'collections'\n | 'categories'\n | 'datasets'\n | 'forms'\n\n/**\n * How much an empty list is worth believing.\n *\n * Every one of these renders as a dropdown with no entries, and an author\n * looking at four identical empty dropdowns cannot tell \"this site has no\n * forms yet\" from \"the read failed\" from \"this editor never lists forms at\n * all\". Only the first of those is a fact about their site; the other two\n * are the picker being broken, and a picker that cannot say which it is\n * sends the author looking for a form they already made.\n *\n * - `unavailable` — no provider is mounted, so nothing on this surface can\n * list entities. The tenant, and any editor outside a host.\n * - `loading` — a read is open or about to be, and no answer has arrived.\n * Also the state of a list nothing has asked for yet: demand is declared\n * on the same commit that would display it, so \"not asked\" and \"asked,\n * unanswered\" are the same beat.\n * - `ready` — the read settled. An empty list here IS the site's answer.\n * - `error` — the read failed. Empty means nothing.\n */\nexport type EntityListState = 'unavailable' | 'loading' | 'ready' | 'error'\n\n/**\n * How many documents a picker BROWSES.\n *\n * A picker is not a catalog viewer. It is a dropdown on a besigner surface\n * where the author is placing one element, and no table in the console offers\n * a page past `TABLE_PAGE_SIZE_OPTIONS` — a dropdown is the smaller surface of\n * the two, not the larger.\n *\n * The middle of that range rather than its floor: a table has a pager under it\n * and a dropdown does not, so the page a reader cannot advance is worth more\n * rows than the one they can.\n *\n * What makes this size SAFE is that browsing is the only job it does. A stored\n * value is named by {@link EntityPickerContextValue.resolve} — one keyed read\n * for the one document, exact at any catalog size — so nothing here has to be\n * wide enough to contain whatever an author picked last month. A window that\n * carried both jobs would have to be, and would still render a bound element\n * as unbound past its own width; widening this cannot fix that, only make it\n * rarer and more expensive.\n */\nexport const ENTITY_PICKER_BROWSE_LIMIT = 25\n\n/**\n * How many documents a server-side name search may return.\n *\n * The same size as the browse window, for the same reason: a reader who has\n * typed a query and still faces 25 rows narrows it further rather than\n * scrolling. Only the kinds in {@link EntityPickerContextValue.searchable}\n * spend this at all.\n */\nexport const ENTITY_PICKER_SEARCH_LIMIT = 25\n\nexport interface EntityPickerContextValue {\n products?: EntityOption[]\n collections?: EntityOption[]\n categories?: EntityOption[]\n datasets?: EntityOption[]\n /** The host's form entities, id-first, feeding FORM_SELECT attributes. */\n forms?: EntityOption[]\n /**\n * The fields an entity's documents declare, keyed by kind and then by\n * entity id, in the order the entity declares them: id = the stable field\n * id a node persists, label = its current display name (AGL-556).\n *\n * Only kinds whose documents carry a field model appear. Two surfaces read\n * it: a field picker that maps a value onto one of an entity's fields (a\n * form field's \"Maps to schema field\"), and the insert-token menu inside a\n * repeat, which offers `{{item.<field>}}` for each field of the entity the\n * repeat names (see `RepeatSource.entityKind`).\n */\n entityFields?: Partial<\n Record<EntityPickerKind, Readonly<Record<string, EntityOption[]>>>\n >\n /**\n * Declare that a picker of this kind is on screen (AGL-703).\n *\n * The provider reads these lists from Firestore, and it used to read ALL of\n * them the moment the besigner opened — up to 300 products, 200 catalog\n * collections, 200 categories and 200 datasets, on a site with a real\n * catalog, for pickers most editing sessions never open. Nothing about\n * moving a heading needs the product list.\n *\n * So the lists are demand-driven now, and the demand signal is the thing\n * that would DISPLAY one: a selected node whose schema declares a\n * `PRODUCT_SELECT`, a repeat bound to a dataset. Call this when that is\n * true and read the list on the render after; until then the list is\n * absent, exactly as it is for the beat before any listener syncs.\n *\n * Optional, and absent on the tenant — which resolves ids and never lists\n * them, so nothing there has a picker to ask for.\n */\n request?: (kind: EntityPickerKind) => void\n /**\n * Per-kind read state, so a picker can say WHY it has nothing to offer.\n * A kind the provider omits reads as `loading`; see {@link entityListState}.\n */\n status?: Partial<Record<EntityPickerKind, EntityListState>>\n /**\n * The browse window came back FULL for this kind, so the list above is a\n * page of the site's catalog and not the whole of it.\n *\n * Measured with a probe — the read asks for one document past\n * {@link ENTITY_PICKER_BROWSE_LIMIT} and the extra one is never offered —\n * so this is a fact about the collection rather than a guess from the\n * window being exactly full.\n *\n * A picker owes the reader this sentence for the same reason the inbox's\n * form filter does: \"not in the picker\" and \"does not exist\" are the same\n * empty dropdown, and only one of them is a fact about their site.\n */\n truncated?: Partial<Record<EntityPickerKind, boolean>>\n /**\n * This kind's documents carry the name-search keys, so a typed query\n * reaches the WHOLE collection rather than only the browse window.\n *\n * True for products alone today: `nameTokens`/`nameLower` are stamped by\n * the catalog's own write path, and `hosts/{host}/resources` deliberately\n * does not stamp them on the other four kinds. A picker for a kind that is\n * absent here must say its search covers the window only — claiming\n * otherwise would let an unfound entity read as a nonexistent one.\n */\n searchable?: Partial<Record<EntityPickerKind, boolean>>\n /**\n * Narrow this kind's offered list to what somebody typed.\n *\n * Matching is the provider's, not the dropdown's: it is the only side that\n * can merge the window's own matches with the ones a server query found\n * past it, and it uses `scoreMatch` so a query matches a word ANYWHERE in a\n * name — the same rule the switchers and global search follow.\n *\n * Passing an empty string restores the browse window.\n */\n search?: (kind: EntityPickerKind, text: string) => void\n /**\n * Resolve ONE stored id to its current name, whatever the browse window\n * holds.\n *\n * Deliberately NOT a job for the browse list, though a bulk read could do\n * both. Sharing one read makes the list's width the limit on which stored\n * values can be named, and past that width a picker renders a bound element\n * as UNBOUND rather than as its label — an empty control on an element that\n * is set, which an author repairs by picking again. That is how a correct\n * reference gets replaced with a different one.\n *\n * One keyed `getDoc` for one document instead: exact at any catalog size,\n * and asked for only when a node actually carries a value. Results land in\n * {@link resolved} and are never re-read.\n */\n resolve?: (kind: EntityPickerKind, id: string) => void\n /**\n * What {@link resolve} found, keyed by kind and then by id.\n *\n * Three states, and the picker renders a different label for each: an\n * `EntityOption` is the resolved name, `null` is a settled read that found\n * NO such document, and an absent key is a resolution still in flight.\n * Absent must never render as missing — a picker that flashed \"unavailable\"\n * over every live reference for the beat before its read landed would teach\n * authors to ignore the one warning that matters.\n */\n resolved?: Partial<\n Record<EntityPickerKind, Readonly<Record<string, EntityOption | null>>>\n >\n}\n\n/**\n * What the picker for `kind` should believe about its own emptiness.\n *\n * `request` is the \"a provider is mounted\" signal — it is the one member the\n * contract already says is absent wherever nothing lists entities — so a\n * surface with no provider resolves to `unavailable` rather than passing an\n * empty list off as the site's answer.\n */\nexport function entityListState(\n context: EntityPickerContextValue | undefined,\n kind: EntityPickerKind,\n): EntityListState {\n if (!context?.request) return 'unavailable'\n return context.status?.[kind] ?? 'loading'\n}\n\n/**\n * The id whose label a picker still needs a keyed read to learn, if any.\n *\n * Part of the contract rather than of one screen, because two surfaces ask\n * it — the attributes panel about a node's picker value, and the insert-token\n * menu about the dataset a repeat is bound to — and an answer that differed\n * between them would be a second keyed read for a document one of them\n * already had.\n *\n * Four cases resolve to \"ask nobody\", and each is a read not spent: there is\n * no value; the browse window already carries it, which is the common case on\n * a small site; a resolution has already answered, including the answer that\n * the document is gone; or the browse read has not settled, so the window may\n * be about to supply it for free.\n *\n * The last is the one worth stating. Asking while the listener is still\n * loading would spend a keyed read on nearly every picker on every site,\n * which is the cost splitting these two jobs exists to remove rather than to\n * move.\n */\nexport function entityValueNeedsResolution(\n context: EntityPickerContextValue | undefined,\n kind: EntityPickerKind,\n value: unknown,\n): string | undefined {\n const id = typeof value === 'string' ? value.trim() : ''\n if (!id || !context?.resolve) return undefined\n if ((context[kind] ?? []).some((entity) => entity.id === id)) return undefined\n if (id in (context.resolved?.[kind] ?? {})) return undefined\n if (entityListState(context, kind) !== 'ready') return undefined\n return id\n}\n\nexport const EntityPickerContext = createContext<EntityPickerContextValue>({})\nEntityPickerContext.displayName = 'EntityPickerContext'\n"],"names":["createContext","ENTITY_PICKER_BROWSE_LIMIT","ENTITY_PICKER_SEARCH_LIMIT","entityListState","context","kind","request","status","entityValueNeedsResolution","value","id","trim","resolve","undefined","some","entity","resolved","EntityPickerContext","displayName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD,+DAA+D;AAC/D,0EAA0E;AAC1E,0BAA0B;AAC1B,SAASA,aAAa,QAAQ,QAAO;AA8CrC;;;;;;;;;;;;;;;;;;;CAmBC,GACD,OAAO,MAAMC,6BAA6B,GAAE;AAE5C;;;;;;;CAOC,GACD,OAAO,MAAMC,6BAA6B,GAAE;AAkH5C;;;;;;;CAOC,GACD,OAAO,SAASC,gBACdC,OAA6C,EAC7CC,IAAsB;;QAGfD;IADP,IAAI,EAACA,2BAAAA,QAASE,OAAO,GAAE,OAAO;IAC9B,gBAAOF,kBAAAA,QAAQG,MAAM,qBAAdH,eAAgB,CAACC,KAAK,mBAAI;AACnC;AAEA;;;;;;;;;;;;;;;;;;;CAmBC,GACD,OAAO,SAASG,2BACdJ,OAA6C,EAC7CC,IAAsB,EACtBI,KAAc;QAITL;QACMA;IAHX,MAAMM,KAAK,OAAOD,UAAU,WAAWA,MAAME,IAAI,KAAK;IACtD,IAAI,CAACD,MAAM,EAACN,2BAAAA,QAASQ,OAAO,GAAE,OAAOC;IACrC,IAAI,EAACT,gBAAAA,OAAO,CAACC,KAAK,YAAbD,gBAAiB,EAAE,EAAEU,IAAI,CAAC,CAACC,SAAWA,OAAOL,EAAE,KAAKA,KAAK,OAAOG;IACrE,IAAIH,gBAAON,oBAAAA,QAAQY,QAAQ,qBAAhBZ,iBAAkB,CAACC,KAAK,mBAAI,CAAC,IAAI,OAAOQ;IACnD,IAAIV,gBAAgBC,SAASC,UAAU,SAAS,OAAOQ;IACvD,OAAOH;AACT;AAEA,OAAO,MAAMO,sBAAsBjB,cAAwC,CAAC,GAAE;AAC9EiB,oBAAoBC,WAAW,GAAG"}
|