@aglyn/aglyn 1.0.0-beta.217 → 1.0.0-beta.219
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +14 -14
- package/src/lib/app-utils/actions.d.ts +25 -374
- package/src/lib/app-utils/actions.js +101 -561
- package/src/lib/app-utils/actions.js.map +1 -1
- package/src/lib/app-utils/activity-presenter.d.ts +3 -3
- package/src/lib/app-utils/activity-presenter.js +36 -52
- package/src/lib/app-utils/activity-presenter.js.map +1 -1
- package/src/lib/app-utils/admin-audit-index.js +2 -2
- package/src/lib/app-utils/admin-audit-index.js.map +1 -1
- package/src/lib/app-utils/analytics-events.d.ts +11 -2
- package/src/lib/app-utils/analytics-events.js +11 -2
- package/src/lib/app-utils/analytics-events.js.map +1 -1
- package/src/lib/app-utils/analytics-provider.d.ts +182 -0
- package/src/lib/app-utils/analytics-provider.js +133 -0
- package/src/lib/app-utils/analytics-provider.js.map +1 -0
- package/src/lib/app-utils/api-plugins.d.ts +46 -0
- package/src/lib/app-utils/api-plugins.js +19 -0
- package/src/lib/app-utils/api-plugins.js.map +1 -1
- package/src/lib/app-utils/{marketplace-overrides.d.ts → artifact-overrides.d.ts} +9 -6
- package/src/lib/app-utils/{marketplace-overrides.js → artifact-overrides.js} +10 -7
- package/src/lib/app-utils/artifact-overrides.js.map +1 -0
- package/src/lib/app-utils/{marketplace-provenance.d.ts → artifact-provenance.d.ts} +12 -10
- package/src/lib/app-utils/{marketplace-provenance.js → artifact-provenance.js} +9 -7
- package/src/lib/app-utils/artifact-provenance.js.map +1 -0
- package/src/lib/app-utils/assist-credits.js +2 -1
- package/src/lib/app-utils/assist-credits.js.map +1 -1
- package/src/lib/app-utils/binding-token-catalog.d.ts +5 -13
- package/src/lib/app-utils/binding-token-catalog.js +17 -41
- package/src/lib/app-utils/binding-token-catalog.js.map +1 -1
- package/src/lib/app-utils/binding-tokens.js +1 -1
- package/src/lib/app-utils/binding-tokens.js.map +1 -1
- package/src/lib/app-utils/brand-support-line.d.ts +36 -0
- package/src/lib/app-utils/brand-support-line.js +38 -0
- package/src/lib/app-utils/brand-support-line.js.map +1 -0
- package/src/lib/app-utils/campaign-touch.d.ts +5 -5
- package/src/lib/app-utils/campaign-touch.js +8 -8
- package/src/lib/app-utils/campaign-touch.js.map +1 -1
- package/src/lib/app-utils/collection-entries.d.ts +2 -2
- package/src/lib/app-utils/collection-entries.js.map +1 -1
- package/src/lib/app-utils/consent-group-change.d.ts +4 -0
- package/src/lib/app-utils/consent-group-change.js.map +1 -1
- package/src/lib/app-utils/console-record-links.d.ts +10 -89
- package/src/lib/app-utils/console-record-links.js +3 -107
- package/src/lib/app-utils/console-record-links.js.map +1 -1
- package/src/lib/app-utils/console-routes.d.ts +16 -7
- package/src/lib/app-utils/console-routes.js +9 -3
- package/src/lib/app-utils/console-routes.js.map +1 -1
- package/src/lib/app-utils/contacts.d.ts +23 -1
- package/src/lib/app-utils/contacts.js +21 -5
- package/src/lib/app-utils/contacts.js.map +1 -1
- package/src/lib/app-utils/cross-pool-desync.d.ts +39 -0
- package/src/lib/app-utils/cross-pool-desync.js +47 -0
- package/src/lib/app-utils/cross-pool-desync.js.map +1 -0
- package/src/lib/app-utils/csv-upload.d.ts +29 -0
- package/src/lib/app-utils/csv-upload.js +29 -0
- package/src/lib/app-utils/csv-upload.js.map +1 -0
- package/src/lib/app-utils/dataset-models.d.ts +5 -5
- package/src/lib/app-utils/dataset-models.js +19 -2
- package/src/lib/app-utils/dataset-models.js.map +1 -1
- package/src/lib/app-utils/datasets.d.ts +65 -11
- package/src/lib/app-utils/datasets.js +74 -11
- package/src/lib/app-utils/datasets.js.map +1 -1
- package/src/lib/app-utils/docs-deflection.d.ts +6 -6
- package/src/lib/app-utils/docs-deflection.js.map +1 -1
- package/src/lib/app-utils/docs-help.generated.d.ts +12 -6
- package/src/lib/app-utils/docs-help.generated.js +21 -6
- package/src/lib/app-utils/docs-help.generated.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.d.ts +2 -2
- package/src/lib/app-utils/docs-index.generated.js +514 -318
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/docs-retrieval.d.ts +3 -3
- package/src/lib/app-utils/docs-retrieval.js +14 -10
- package/src/lib/app-utils/docs-retrieval.js.map +1 -1
- package/src/lib/app-utils/duplicate-resource.d.ts +4 -1
- package/src/lib/app-utils/duplicate-resource.js +7 -4
- package/src/lib/app-utils/duplicate-resource.js.map +1 -1
- package/src/lib/app-utils/dynamic-list-rule.d.ts +1 -1
- package/src/lib/app-utils/dynamic-list-rule.js.map +1 -1
- package/src/lib/app-utils/element-ui.d.ts +1 -1
- package/src/lib/app-utils/element-ui.js +1 -1
- package/src/lib/app-utils/element-ui.js.map +1 -1
- package/src/lib/app-utils/email-starter-policy.js +3 -3
- package/src/lib/app-utils/email-starter-policy.js.map +1 -1
- package/src/lib/app-utils/entity-picker-context.d.ts +10 -5
- package/src/lib/app-utils/entity-picker-context.js.map +1 -1
- package/src/lib/app-utils/error-beacon.d.ts +75 -6
- package/src/lib/app-utils/error-beacon.js +160 -21
- package/src/lib/app-utils/error-beacon.js.map +1 -1
- package/src/lib/app-utils/expand-repeatables.d.ts +10 -10
- package/src/lib/app-utils/expand-repeatables.js +10 -10
- package/src/lib/app-utils/expand-repeatables.js.map +1 -1
- package/src/lib/app-utils/host-events.d.ts +82 -0
- package/src/lib/app-utils/host-events.js +62 -0
- package/src/lib/app-utils/host-events.js.map +1 -0
- package/src/lib/app-utils/lockdown.d.ts +33 -1
- package/src/lib/app-utils/lockdown.js +45 -4
- package/src/lib/app-utils/lockdown.js.map +1 -1
- package/src/lib/app-utils/margin-utilization.d.ts +4 -69
- package/src/lib/app-utils/margin-utilization.js +82 -69
- package/src/lib/app-utils/margin-utilization.js.map +1 -1
- package/src/lib/app-utils/node-definition-sanitizer.d.ts +16 -16
- package/src/lib/app-utils/node-definition-sanitizer.js +24 -20
- package/src/lib/app-utils/node-definition-sanitizer.js.map +1 -1
- package/src/lib/app-utils/node-interactions.d.ts +7 -7
- package/src/lib/app-utils/node-interactions.js.map +1 -1
- package/src/lib/app-utils/notifications.d.ts +78 -47
- package/src/lib/app-utils/notifications.js +63 -44
- package/src/lib/app-utils/notifications.js.map +1 -1
- package/src/lib/app-utils/payments-configured.d.ts +8 -21
- package/src/lib/app-utils/payments-configured.js +8 -23
- package/src/lib/app-utils/payments-configured.js.map +1 -1
- package/src/lib/app-utils/plan-entitlements.d.ts +43 -128
- package/src/lib/app-utils/plan-entitlements.js +145 -304
- package/src/lib/app-utils/plan-entitlements.js.map +1 -1
- package/src/lib/app-utils/platform-brand.d.ts +10 -0
- package/src/lib/app-utils/platform-brand.js +14 -3
- package/src/lib/app-utils/platform-brand.js.map +1 -1
- package/src/lib/app-utils/plugin-api-rate-limit.d.ts +7 -1
- package/src/lib/app-utils/plugin-api-rate-limit.js +11 -3
- package/src/lib/app-utils/plugin-api-rate-limit.js.map +1 -1
- package/src/lib/app-utils/plugin-bundle-checks.js +2 -2
- package/src/lib/app-utils/plugin-bundle-checks.js.map +1 -1
- package/src/lib/app-utils/plugin-host-events.generated.d.ts +12 -0
- package/src/lib/app-utils/plugin-host-events.generated.js +169 -0
- package/src/lib/app-utils/plugin-host-events.generated.js.map +1 -0
- package/src/lib/app-utils/plugin-release-flags.generated.d.ts +12 -0
- package/src/lib/app-utils/plugin-release-flags.generated.js +106 -0
- package/src/lib/app-utils/plugin-release-flags.generated.js.map +1 -0
- package/src/lib/app-utils/realm-host-surface.generated.js +2 -0
- package/src/lib/app-utils/realm-host-surface.generated.js.map +1 -1
- package/src/lib/app-utils/release-flags.d.ts +14 -5
- package/src/lib/app-utils/release-flags.js +21 -125
- package/src/lib/app-utils/release-flags.js.map +1 -1
- package/src/lib/app-utils/repeat-sources.d.ts +9 -0
- package/src/lib/app-utils/repeat-sources.js.map +1 -1
- package/src/lib/app-utils/retained-refusals.d.ts +73 -0
- package/src/lib/app-utils/retained-refusals.js +186 -0
- package/src/lib/app-utils/retained-refusals.js.map +1 -0
- package/src/lib/app-utils/scope-tokens.d.ts +27 -0
- package/src/lib/app-utils/scope-tokens.js +61 -0
- package/src/lib/app-utils/scope-tokens.js.map +1 -1
- package/src/lib/app-utils/screen-link-context.js +3 -3
- package/src/lib/app-utils/screen-link-context.js.map +1 -1
- package/src/lib/app-utils/screen-route.d.ts +15 -0
- package/src/lib/app-utils/screen-route.js +21 -1
- package/src/lib/app-utils/screen-route.js.map +1 -1
- package/src/lib/app-utils/seo-audit.d.ts +132 -0
- package/src/lib/app-utils/seo-audit.js +246 -0
- package/src/lib/app-utils/seo-audit.js.map +1 -0
- package/src/lib/app-utils/seo-keywords.d.ts +48 -0
- package/src/lib/app-utils/seo-keywords.js +60 -0
- package/src/lib/app-utils/seo-keywords.js.map +1 -0
- package/src/lib/app-utils/seo-page-facts.d.ts +66 -0
- package/src/lib/app-utils/seo-page-facts.js +155 -0
- package/src/lib/app-utils/seo-page-facts.js.map +1 -0
- package/src/lib/app-utils/seo-site-scan.d.ts +126 -0
- package/src/lib/app-utils/seo-site-scan.js +162 -0
- package/src/lib/app-utils/seo-site-scan.js.map +1 -0
- package/src/lib/app-utils/seo-title-variables.js +1 -1
- package/src/lib/app-utils/seo-title-variables.js.map +1 -1
- package/src/lib/app-utils/server.d.ts +6 -9
- package/src/lib/app-utils/server.js +17 -27
- package/src/lib/app-utils/server.js.map +1 -1
- package/src/lib/app-utils/site-interactions.d.ts +501 -0
- package/src/lib/app-utils/site-interactions.js +554 -0
- package/src/lib/app-utils/site-interactions.js.map +1 -0
- package/src/lib/app-utils/{marketplace-theme.d.ts → site-theme.d.ts} +13 -2
- package/src/lib/app-utils/{marketplace-theme.js → site-theme.js} +5 -2
- package/src/lib/app-utils/site-theme.js.map +1 -0
- package/src/lib/app-utils/sitemap.d.ts +4 -3
- package/src/lib/app-utils/sitemap.js +9 -7
- package/src/lib/app-utils/sitemap.js.map +1 -1
- package/src/lib/app-utils/stale-build-error.d.ts +51 -0
- package/src/lib/app-utils/stale-build-error.js +100 -1
- package/src/lib/app-utils/stale-build-error.js.map +1 -1
- package/src/lib/app-utils/starter-templates.d.ts +20 -0
- package/src/lib/app-utils/starter-templates.js +49 -0
- package/src/lib/app-utils/starter-templates.js.map +1 -1
- package/src/lib/app-utils/tax-jurisdiction-figures.d.ts +118 -0
- package/src/lib/app-utils/tax-jurisdiction-figures.js +123 -0
- package/src/lib/app-utils/tax-jurisdiction-figures.js.map +1 -0
- package/src/lib/app-utils/theme-library.d.ts +10 -3
- package/src/lib/app-utils/theme-library.js +13 -4
- package/src/lib/app-utils/theme-library.js.map +1 -1
- package/src/lib/app-utils/{campaign-attribution.d.ts → utm-attribution.d.ts} +11 -6
- package/src/lib/app-utils/{campaign-attribution.js → utm-attribution.js} +13 -8
- package/src/lib/app-utils/utm-attribution.js.map +1 -0
- package/src/lib/app-utils/{campaign-forwarding.d.ts → utm-forwarding.d.ts} +14 -14
- package/src/lib/app-utils/{campaign-forwarding.js → utm-forwarding.js} +26 -26
- package/src/lib/app-utils/utm-forwarding.js.map +1 -0
- package/src/lib/app-utils/video-object.js +6 -5
- package/src/lib/app-utils/video-object.js.map +1 -1
- package/src/lib/app-utils/visitor-consent.d.ts +41 -164
- package/src/lib/app-utils/visitor-consent.js +66 -277
- package/src/lib/app-utils/visitor-consent.js.map +1 -1
- package/src/lib/app-utils/web-vitals-rum.d.ts +7 -7
- package/src/lib/app-utils/web-vitals-rum.js +31 -63
- package/src/lib/app-utils/web-vitals-rum.js.map +1 -1
- package/src/lib/app-utils/where-used.d.ts +11 -6
- package/src/lib/app-utils/where-used.js +3 -8
- package/src/lib/app-utils/where-used.js.map +1 -1
- package/src/lib/canvas-manager/canvas-manager.js +3 -3
- package/src/lib/canvas-manager/canvas-manager.js.map +1 -1
- package/src/lib/foundation/definitions/components.types.d.ts +1 -1
- package/src/lib/foundation/definitions/components.types.js +1 -1
- package/src/lib/foundation/definitions/components.types.js.map +1 -1
- package/src/lib/foundation/definitions/org-billing.types.d.ts +18 -135
- package/src/lib/foundation/definitions/org-billing.types.js.map +1 -1
- package/src/lib/foundation/definitions/organization.types.d.ts +6 -3
- package/src/lib/foundation/definitions/organization.types.js.map +1 -1
- package/src/lib/foundation/definitions/platform.types.d.ts +27 -5
- package/src/lib/foundation/definitions/platform.types.js.map +1 -1
- package/src/lib/foundation/definitions/write-deny-coverage.util.d.ts +16 -0
- package/src/lib/foundation/definitions/write-deny-coverage.util.js +20 -0
- package/src/lib/foundation/definitions/write-deny-coverage.util.js.map +1 -1
- package/src/lib/foundation/index.d.ts +1 -1
- package/src/lib/foundation/index.js.map +1 -1
- package/src/lib/plugin-manager/besigner-documents.d.ts +96 -0
- package/src/lib/plugin-manager/besigner-documents.js +61 -0
- package/src/lib/plugin-manager/besigner-documents.js.map +1 -0
- package/src/lib/plugin-manager/computed-variables.d.ts +71 -0
- package/src/lib/plugin-manager/computed-variables.js +74 -0
- package/src/lib/plugin-manager/computed-variables.js.map +1 -0
- package/src/lib/plugin-manager/custom-fields.d.ts +29 -2
- package/src/lib/plugin-manager/custom-fields.js +47 -1
- package/src/lib/plugin-manager/custom-fields.js.map +1 -1
- package/src/lib/plugin-manager/feature-plugins.d.ts +292 -20
- package/src/lib/plugin-manager/feature-plugins.js +105 -17
- package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
- package/src/lib/plugin-manager/first-party-plugins.generated.d.ts +123 -0
- package/src/lib/plugin-manager/first-party-plugins.generated.js +785 -14
- package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
- package/src/lib/plugin-manager/plugin-activity-actions.d.ts +12 -0
- package/src/lib/plugin-manager/plugin-activity-actions.js +25 -0
- package/src/lib/plugin-manager/plugin-activity-actions.js.map +1 -1
- package/src/lib/plugin-manager/plugin-contributions.d.ts +1 -1
- package/src/lib/plugin-manager/plugin-contributions.js.map +1 -1
- package/src/lib/plugin-manager/plugin-declarations-repair.d.ts +36 -0
- package/src/lib/plugin-manager/plugin-declarations-repair.js +64 -0
- package/src/lib/plugin-manager/plugin-declarations-repair.js.map +1 -0
- package/src/lib/plugin-manager/plugin-dependents.d.ts +101 -0
- package/src/lib/plugin-manager/plugin-dependents.js +75 -0
- package/src/lib/plugin-manager/plugin-dependents.js.map +1 -0
- package/src/lib/plugin-manager/plugin-distribution.d.ts +55 -0
- package/src/lib/plugin-manager/plugin-distribution.js +45 -0
- package/src/lib/plugin-manager/plugin-distribution.js.map +1 -0
- package/src/lib/plugin-manager/plugin-host-collections.d.ts +17 -0
- package/src/lib/plugin-manager/plugin-host-collections.js +6 -0
- package/src/lib/plugin-manager/plugin-host-collections.js.map +1 -1
- package/src/lib/plugin-manager/plugin-host-resources.d.ts +133 -0
- package/src/lib/plugin-manager/plugin-host-resources.js +52 -0
- package/src/lib/plugin-manager/plugin-host-resources.js.map +1 -0
- package/src/lib/plugin-manager/plugin-org-erasure.d.ts +28 -2
- package/src/lib/plugin-manager/plugin-org-erasure.js +61 -2
- package/src/lib/plugin-manager/plugin-org-erasure.js.map +1 -1
- package/src/lib/plugin-manager/plugin-plan-entitlements.d.ts +123 -0
- package/src/lib/plugin-manager/plugin-plan-entitlements.js +101 -0
- package/src/lib/plugin-manager/plugin-plan-entitlements.js.map +1 -0
- package/src/lib/plugin-manager/plugin-record-index.d.ts +95 -0
- package/src/lib/plugin-manager/plugin-record-index.js +51 -0
- package/src/lib/plugin-manager/plugin-record-index.js.map +1 -0
- package/src/lib/plugin-manager/plugin-record-written.d.ts +58 -0
- package/src/lib/plugin-manager/plugin-record-written.js +42 -0
- package/src/lib/plugin-manager/plugin-record-written.js.map +1 -0
- package/src/lib/plugin-manager/plugin-revenue-sources.d.ts +213 -0
- package/src/lib/plugin-manager/plugin-revenue-sources.js +194 -0
- package/src/lib/plugin-manager/plugin-revenue-sources.js.map +1 -0
- package/src/lib/plugin-manager/plugin-site-bundle.d.ts +145 -0
- package/src/lib/plugin-manager/plugin-site-bundle.js +78 -0
- package/src/lib/plugin-manager/plugin-site-bundle.js.map +1 -0
- package/src/lib/plugin-manager/plugin-site-cache.d.ts +12 -0
- package/src/lib/plugin-manager/plugin-site-cache.js.map +1 -1
- package/src/lib/plugin-manager/plugin-site-export.d.ts +81 -0
- package/src/lib/plugin-manager/plugin-site-export.js +40 -0
- package/src/lib/plugin-manager/plugin-site-export.js.map +1 -0
- package/src/lib/plugin-manager/plugin-sitemap-sections.d.ts +88 -0
- package/src/lib/plugin-manager/plugin-sitemap-sections.js +29 -0
- package/src/lib/plugin-manager/plugin-sitemap-sections.js.map +1 -0
- package/src/lib/plugin-manager/plugin-tax-profile.d.ts +20 -4
- package/src/lib/plugin-manager/plugin-tax-profile.js.map +1 -1
- package/src/lib/plugin-manager/plugin-tax-return-sources.d.ts +255 -0
- package/src/lib/plugin-manager/plugin-tax-return-sources.js +191 -0
- package/src/lib/plugin-manager/plugin-tax-return-sources.js.map +1 -0
- package/src/lib/plugin-manager/plugin-template-sources.d.ts +75 -0
- package/src/lib/plugin-manager/plugin-template-sources.js +37 -0
- package/src/lib/plugin-manager/plugin-template-sources.js.map +1 -0
- package/src/lib/plugin-manager/plugin-usage-axes.d.ts +107 -0
- package/src/lib/plugin-manager/plugin-usage-axes.js +107 -0
- package/src/lib/plugin-manager/plugin-usage-axes.js.map +1 -0
- package/src/lib/plugin-manager/plugin-zones.d.ts +4 -5
- package/src/lib/plugin-manager/plugin-zones.js.map +1 -1
- package/src/lib/plugin-manager/realm-host-aglyn.generated.js +2 -0
- package/src/lib/plugin-manager/realm-host-aglyn.generated.js.map +1 -1
- package/src/lib/plugin-manager/record-captured-contact.d.ts +1 -14
- package/src/lib/plugin-manager/record-captured-contact.js +10 -32
- package/src/lib/plugin-manager/record-captured-contact.js.map +1 -1
- package/src/lib/plugin-manager/repeat-rows.d.ts +124 -0
- package/src/lib/plugin-manager/repeat-rows.js +108 -0
- package/src/lib/plugin-manager/repeat-rows.js.map +1 -0
- package/src/lib/plugin-manager/submission-record-target.d.ts +144 -0
- package/src/lib/plugin-manager/submission-record-target.js +125 -0
- package/src/lib/plugin-manager/submission-record-target.js.map +1 -0
- package/src/lib/plugin-manager/video-embed-provider.d.ts +108 -0
- package/src/lib/plugin-manager/video-embed-provider.js +165 -0
- package/src/lib/plugin-manager/video-embed-provider.js.map +1 -0
- package/src/lib/types/nodes.d.ts +2 -2
- package/src/lib/types/nodes.js.map +1 -1
- package/src/lib/app-utils/campaign-attribution.js.map +0 -1
- package/src/lib/app-utils/campaign-forwarding.js.map +0 -1
- package/src/lib/app-utils/contact-holder.d.ts +0 -68
- package/src/lib/app-utils/contact-holder.js +0 -129
- package/src/lib/app-utils/contact-holder.js.map +0 -1
- package/src/lib/app-utils/contact-merge.d.ts +0 -136
- package/src/lib/app-utils/contact-merge.js +0 -412
- package/src/lib/app-utils/contact-merge.js.map +0 -1
- package/src/lib/app-utils/crm-csv.d.ts +0 -239
- package/src/lib/app-utils/crm-csv.js +0 -467
- package/src/lib/app-utils/crm-csv.js.map +0 -1
- package/src/lib/app-utils/crm-digest.d.ts +0 -185
- package/src/lib/app-utils/crm-digest.js +0 -272
- package/src/lib/app-utils/crm-digest.js.map +0 -1
- package/src/lib/app-utils/form-dataset-binding.d.ts +0 -50
- package/src/lib/app-utils/form-dataset-binding.js +0 -74
- package/src/lib/app-utils/form-dataset-binding.js.map +0 -1
- package/src/lib/app-utils/list-import.d.ts +0 -158
- package/src/lib/app-utils/list-import.js +0 -332
- package/src/lib/app-utils/list-import.js.map +0 -1
- package/src/lib/app-utils/marketplace-listing-visibility.d.ts +0 -122
- package/src/lib/app-utils/marketplace-listing-visibility.js +0 -147
- package/src/lib/app-utils/marketplace-listing-visibility.js.map +0 -1
- package/src/lib/app-utils/marketplace-overrides.js.map +0 -1
- package/src/lib/app-utils/marketplace-provenance.js.map +0 -1
- package/src/lib/app-utils/marketplace-theme.js.map +0 -1
- package/src/lib/app-utils/marketplace-update-state.d.ts +0 -119
- package/src/lib/app-utils/marketplace-update-state.js +0 -132
- package/src/lib/app-utils/marketplace-update-state.js.map +0 -1
- package/src/lib/app-utils/publisher-agreement.d.ts +0 -289
- package/src/lib/app-utils/publisher-agreement.js +0 -321
- package/src/lib/app-utils/publisher-agreement.js.map +0 -1
- package/src/lib/app-utils/wistia-embed.d.ts +0 -104
- package/src/lib/app-utils/wistia-embed.js +0 -138
- package/src/lib/app-utils/wistia-embed.js.map +0 -1
- package/src/lib/app-utils/workflows.d.ts +0 -136
- package/src/lib/app-utils/workflows.js +0 -346
- package/src/lib/app-utils/workflows.js.map +0 -1
- package/src/lib/plugin-manager/commerce-zone-props.d.ts +0 -172
- package/src/lib/plugin-manager/commerce-zone-props.js +0 -31
- package/src/lib/plugin-manager/commerce-zone-props.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/release-flags.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 * Release flags (AGL-227): platform-level \"is this feature launched\"\n * gating backed by Firebase Remote Config. A separate axis from plan\n * entitlements (`plan-entitlements.ts`) — entitlements ask whether the\n * tenant's PLAN includes a feature, release flags ask whether the feature\n * is released to the public at all (or partially, via percentage rollout).\n * Staff bypass release flags everywhere but see a flagged warning.\n */\n\n/** Remote Config parameter keys for release-gated features. */\nimport type { OrgPlan } from '../foundation'\nimport { PLAN_LABELS, SELF_SERVE_PLANS } from './plan-entitlements'\nimport { PLATFORM_BRAND_NAME } from './platform-brand'\n\nexport type ReleaseFlagKey =\n | 'release_crm'\n | 'release_outreach'\n | 'release_bookings'\n | 'release_events'\n | 'release_data_store'\n | 'release_workflows'\n | 'release_redirects'\n | 'release_commerce_v2'\n | 'release_member_accounts'\n | 'release_marketplace'\n | 'release_marketing'\n | 'release_email'\n | 'release_inbox'\n | 'release_logic'\n | 'release_addon_store'\n | 'release_native_checkout'\n | 'release_edit_bar'\n | 'release_assist'\n | 'release_video_uploads'\n | 'release_video_delivery'\n | 'release_ai_generative'\n\nexport interface ReleaseFlagDefinition {\n key: ReleaseFlagKey\n label: string\n description: string\n /**\n * Fallback verdict when Remote Config is unreachable (offline, blocked,\n * first paint before activate). MUST match the seeded value in\n * cloud/firebase-remoteconfig.template.json so environments without a\n * published template behave like the template intends.\n */\n defaultEnabled: boolean\n /** Host dashboard tab id (host-nav-tabs.ts) this flag hides, if any. */\n navTabId?: string\n}\n\n/**\n * The registry: one entry per gated feature. Adding a flag = add it here,\n * seed it in the Remote Config template, and (optionally) wrap the page in\n * `<FeatureGate>` — the staff admin editor and nav filtering pick it up\n * from this list.\n */\nexport const RELEASE_FLAGS: readonly ReleaseFlagDefinition[] = [\n {\n key: 'release_crm',\n label: 'CRM',\n description:\n 'The CRM: contacts, leads, companies, deals, tasks, reports and fields.',\n defaultEnabled: true,\n navTabId: 'nav-tab-contacts',\n },\n // Sequences (AGL-2974): the plugin's kill switch as well as its launch\n // gate, like every first-party plugin's flag since AGL-422 — off, it is\n // subtracted from the console loader and the API dispatcher for everyone\n // but staff. OFF by default, because nothing behind it sends mail yet and\n // no plan carries the `outreach` entitlement it also needs. The KEY stays\n // `release_outreach`: it is persisted in Remote Config (AGL-3199).\n {\n key: 'release_outreach',\n label: 'Sequences',\n description:\n 'One-to-one, multi-step email sequences sent from a rep’s own ' +\n 'connected mailbox and logged in the CRM (AGL-2974). OFF by default ' +\n 'and staff preview only. An organization also needs the `outreach` ' +\n 'entitlement, which no plan carries, so a customer reaches it only ' +\n 'through a per-org entitlement override.',\n defaultEnabled: false,\n navTabId: 'nav-tab-org-outreach',\n },\n {\n key: 'release_bookings',\n label: 'Bookings',\n description: 'Bookings & scheduling for host sites.',\n defaultEnabled: true,\n navTabId: 'nav-tab-bookings',\n },\n {\n key: 'release_events',\n label: 'Events',\n description: 'Event calendar management (AGL-145 add-on surface).',\n defaultEnabled: true,\n navTabId: 'nav-tab-events',\n },\n {\n key: 'release_data_store',\n label: 'Data store',\n description: 'Datasets, models and dynamic data bindings.',\n defaultEnabled: true,\n navTabId: 'nav-tab-data',\n },\n {\n key: 'release_workflows',\n label: 'Automation',\n description: 'Workflows, actions, webhooks and their run history.',\n defaultEnabled: true,\n navTabId: 'nav-tab-workflows',\n },\n {\n key: 'release_redirects',\n label: 'Redirects',\n description: 'Redirect manager with usage analytics.',\n defaultEnabled: true,\n navTabId: 'nav-tab-redirects',\n },\n {\n key: 'release_commerce_v2',\n label: 'Commerce v2',\n description:\n 'Full ecommerce wave: catalog/variants, cart + checkout, digital ' +\n 'goods, reservations, POS, and the repriced commerce tiers ' +\n '(AGL-276..331).',\n defaultEnabled: true,\n },\n {\n key: 'release_member_accounts',\n label: 'User Accounts',\n description:\n 'Visitor accounts on published sites: the /signin, /signup and ' +\n '/recover pages and the Members blocks (AGL-2486). Platform-wide ' +\n 'kill switch only — whether a given SITE serves those pages is the ' +\n 'per-site User Accounts toggle, which is off until a site opts in.',\n defaultEnabled: true,\n },\n {\n key: 'release_marketplace',\n label: 'Marketplace',\n description: 'Marketplace browsing, publishing and plugin installs.',\n defaultEnabled: true,\n // AGL-1654: was 'nav-tab-marketplace', which matched no nav item, so\n // `gateNavTabItems` never hid the tab. The id is still held to a real\n // declaration by `release-flag-nav-tab-ids.spec.ts` — but it is no longer\n // held there BY HAND: AGL-3080 made the org Marketplace tab the\n // marketplace plugin's own `orgNavItems` entry, so the tab and the\n // surface behind it are one declaration that names this id once.\n //\n // The second half of that comment — \"the page behind it carries no\n // `<FeatureGate>`\" — was still true until AGL-2019 and is no longer.\n // Flipping this flag off used to subtract the marketplace plugin from\n // the loader and both API dispatchers while the page went on rendering\n // in full, so the OFF state was itself broken and \"just turn the flag\n // off\" was not an available answer for an operator who does not want a\n // marketplace. The page is gated now, so off means off.\n navTabId: 'nav-tab-org-marketplace',\n },\n // AGL-422: every first-party plugin is release-flagged — the flag now\n // feeds the plugin LOADER (console, published sites, API dispatch), not\n // just nav visibility, so staff can kill-switch a whole plugin platform-\n // wide from the Feature Flags page.\n {\n key: 'release_marketing',\n label: 'Marketing',\n description: 'Overlays, campaigns at-a-glance, and A/B experiments.',\n defaultEnabled: true,\n navTabId: 'nav-tab-marketing',\n },\n {\n key: 'release_email',\n label: 'Email',\n description: 'Designed emails, campaigns, and audience sending.',\n defaultEnabled: true,\n },\n {\n key: 'release_inbox',\n label: 'Inbox',\n description: 'Form submissions, site members, and the lead inbox.',\n defaultEnabled: true,\n navTabId: 'nav-tab-inbox',\n },\n {\n key: 'release_logic',\n label: 'Logic',\n description: 'Variables, no-code functions, and reference health.',\n defaultEnabled: true,\n navTabId: 'nav-tab-logic',\n },\n {\n key: 'release_addon_store',\n label: 'Add-on store',\n description:\n 'Self-serve add-on purchases on the Billing page: seats, datasets, ' +\n 'extra sites, POS registers, Event Calendar (AGL-524..531).',\n defaultEnabled: true,\n },\n {\n key: 'release_native_checkout',\n label: 'In-page checkout',\n description:\n 'Pay without leaving the page, instead of a redirect to ' +\n 'checkout.stripe.com. TWO surfaces: the console plan checkout uses ' +\n 'embedded Checkout (AGL-1132), and a merchant storefront uses the ' +\n 'Payment Element on the merchant\\u2019s own domain (AGL-1944) \\u2014 ' +\n 'which is the half that costs conversions, since leaving the store ' +\n 'mid-purchase is where carts get abandoned. Both keep the webhook as ' +\n 'the only thing that fulfils. OFF by default: the redirect is the ' +\n 'proven path, and neither surface can be verified without putting a ' +\n 'real card through it. Also gated on a publishable key being set, ' +\n 'per surface \\u2014 so flipping this alone degrades to the redirect ' +\n 'rather than to a dead button.',\n defaultEnabled: false,\n },\n // Released to every site 2026-09-16 (AGL-3041). Still the whole surface's\n // kill switch, globally or for one org by override: off, the tenant slot\n // renders nothing, both token mints refuse, and `/api/edit-context` turns\n // away every token already issued.\n {\n key: 'release_edit_bar',\n label: 'Site admin bar',\n description:\n 'Admin bar on published sites: a signed-in editor jumps from a live ' +\n 'page into the besigner for the screen serving it. Released ' +\n '2026-09-16 (AGL-3041) for every site; turning it off hides the bar ' +\n 'and revokes every outstanding edit token at the verify site.',\n defaultEnabled: true,\n },\n {\n key: 'release_assist',\n label: `${PLATFORM_BRAND_NAME} Assist`,\n description:\n 'The in-console AI chat helper on every page (AGL-1860): docs-' +\n 'grounded answers with deep links, page-context guidance on Pro+. ' +\n 'OFF by default, and blocked on TWO published legal artifacts ' +\n '(AGL-1909), neither of which is a repo file — both are live besigner ' +\n 'pages, so publication is what satisfies them: (1) the privacy-policy ' +\n 'disclosure for stored Q&A, because the data loop records every ' +\n 'exchange org-scoped under orgs/{orgId}/assistExchanges; and (2) the ' +\n 'Anthropic row on /legal/subprocessors, which was deliberately ' +\n 'REMOVED on 2026-08-13 (subprocessorsV2a-20260813) on the premise ' +\n 'that no production key existed — so the page is not merely ' +\n 'incomplete without it, it is affirmatively wrong. NOTE the real ' +\n 'trigger is ANTHROPIC_API_KEY, not this flag: /api/ai/assist (the ' +\n 'besigner copy assistant, AGL-89/130/169) carries no release flag at ' +\n 'all and sends customer site content to Anthropic on the key plus a ' +\n 'Pro entitlement alone. Setting the key in production therefore makes ' +\n 'Anthropic a subprocessor whether or not this flag is ever flipped.',\n defaultEnabled: false,\n },\n // Ingress only, and with no staff preview on the server: a video a staff\n // session uploads into a customer's library serves on that customer's pages\n // like any other, so a grant goes to the org through its override.\n // `apps/console/utils/server/video-uploads.ts` holds the gate.\n {\n key: 'release_video_uploads',\n label: 'Video uploads',\n description:\n 'New video in the media library, on every upload path (AGL-2830). ' +\n 'Off: refused with 403 video_uploads_paused, as DAM video delivery is ' +\n 'not yet metered or bounded per org (AGL-2810, AGL-2812). Images, ' +\n 'documents and stored videos are unaffected.',\n defaultEnabled: false,\n },\n // Delivery, not ingress: this flag decides whether a video already in the\n // library is COPIED to the configured delivery provider and served from it\n // through a short-lived signed redirect (AGL-2824). Per org, with no staff\n // preview on the server — a copy is customer data at the provider, so a\n // grant goes to the org through its override. Off, or with no provider\n // configured, every video serves from this platform exactly as before.\n // `media-delivery.ts` in `tenant-data-admin` holds the gate.\n {\n key: 'release_video_delivery',\n label: 'Video delivery',\n description:\n 'Serves library video from a configured delivery provider through ' +\n 'short-lived signed redirects, and copies each video to it (AGL-2824). ' +\n 'OFF by default: video serves from this platform as before. Also ' +\n \"needs the provider's settings.\",\n defaultEnabled: false,\n },\n // The generative doors share one runtime with Assist (AGL-2903) but not\n // its flag: `release_assist` answers questions, this builds things, and\n // the two are sold, metered and disclosed separately. The server-side\n // gate lives in `aiGateLadder` (404 when off, staff preview).\n // Labeled for what it switches — the generative doors — and not with the\n // add-on's own name, which is sold and live whether or not the doors are\n // released: the docs name the add-on, and a flag wearing the same words\n // reads as documented when it is not.\n {\n key: 'release_ai_generative',\n label: `${PLATFORM_BRAND_NAME} AI generation`,\n description:\n 'Generative building and automation behind the AI add-on ' +\n '(AGL-2903): sections, pages and workflows written by a model from ' +\n 'a brief, on the shared Anthropic runtime. OFF by default and staff ' +\n 'preview only; every generative route answers 404 while it is off. ' +\n 'Turning it on sends customer briefs and site content to Anthropic ' +\n 'on the same ANTHROPIC_API_KEY and under the same subprocessor ' +\n 'disclosure as Assist (AGL-1909).',\n defaultEnabled: false,\n },\n]\n\nexport const RELEASE_FLAG_KEYS = RELEASE_FLAGS.map(\n (definition) => definition.key,\n) as readonly ReleaseFlagKey[]\n\nexport function isReleaseFlagKey(value: string): value is ReleaseFlagKey {\n return (RELEASE_FLAG_KEYS as readonly string[]).includes(value)\n}\n\nexport function getReleaseFlagDefinition(\n key: ReleaseFlagKey,\n): ReleaseFlagDefinition {\n const definition = RELEASE_FLAGS.find((entry) => entry.key === key)\n if (!definition) throw new Error(`Unknown release flag: ${key}`)\n return definition\n}\n\n/**\n * The JSON payload stored in each Remote Config parameter. `enabled: true`\n * turns the feature on for everyone; `enabled: false` with a positive\n * `rolloutPercent` enables it for that percentage of subjects (stable\n * per-tenant bucketing, GrowthBook-style); `plans` narrows either of those\n * to a set of tiers (AGL-2486).\n */\nexport interface ReleaseFlagValue {\n enabled: boolean\n /** 0–100; only consulted while `enabled` is false. */\n rolloutPercent?: number\n /**\n * Plan/tier targeting (AGL-2486). The tiers this flag is being rolled out\n * to, or ABSENT for \"every tier\".\n *\n * ABSENT AND EMPTY BOTH MEAN EVERY TIER, and that is load-bearing: every\n * flag stored before this field existed parses to `undefined` here, and an\n * operator who unticks every box has plainly not asked for a flag that\n * reaches nobody. Reading an empty list as \"no tiers\" would dark-launch\n * nothing while the console still said the flag was on — the inverted\n * reading of this field is the whole hazard, so it is closed in the parser\n * rather than left to each call site.\n */\n plans?: OrgPlan[]\n /** Free-form staff note (\"waiting on AGL-199\", owner, etc.). */\n note?: string\n}\n\nconst clampPercent = (value: unknown): number => {\n const percent = typeof value === 'number' && Number.isFinite(value) ? value : 0\n return Math.min(100, Math.max(0, Math.round(percent)))\n}\n\n/**\n * Every plan a flag may target, cheapest first — `SELF_SERVE_PLANS` then\n * `enterprise`, the same ladder `planGrantingFeature` walks and the same\n * order the plan grid renders. Derived, never re-typed: pricing v3 inserted\n * `scale` mid-ladder and added `agency`, and a second hand-written tier list\n * here would have missed both.\n */\nexport const RELEASE_FLAG_PLAN_LADDER: readonly OrgPlan[] = [\n ...SELF_SERVE_PLANS,\n 'enterprise' as OrgPlan,\n]\n\nconst isOrgPlan = (value: unknown): value is OrgPlan =>\n typeof value === 'string' &&\n Object.prototype.hasOwnProperty.call(PLAN_LABELS, value)\n\n/**\n * The tiers on the ladder at or above `plan` — what the console's \"Pro and\n * above\" shortcut expands to before it is STORED as an explicit list.\n *\n * Deliberately a UI convenience and not a stored \"minimum plan\" mode: a\n * stored threshold would silently re-aim every live flag the day a tier is\n * inserted into the middle of the ladder (pricing v3 did exactly that with\n * `scale`). An explicit list means the audience of a published flag only\n * ever changes because a human changed it.\n */\nexport function releaseFlagPlansAtOrAbove(plan: OrgPlan): OrgPlan[] {\n const index = RELEASE_FLAG_PLAN_LADDER.indexOf(plan)\n return index < 0 ? [] : RELEASE_FLAG_PLAN_LADDER.slice(index)\n}\n\n/**\n * Sanitises whatever is stored at `plans`, in ladder order.\n *\n * Returns `undefined` — not `[]` — for \"no targeting declared\", so the\n * absence survives a round trip through the staff editor and cannot be\n * written back as a list that means something else. Unknown tier names are\n * dropped for the same reason `parseOrgReleaseFlagOverrides` drops unknown\n * keys: a renamed or retired tier must never gate anything. A list that\n * names ONLY unknown tiers collapses to `undefined` (every tier) rather than\n * to an empty list, because a typo must inherit, never silently target.\n */\nexport function parseReleaseFlagPlans(raw: unknown): OrgPlan[] | undefined {\n if (!Array.isArray(raw)) return undefined\n const named = new Set(raw.filter(isOrgPlan))\n if (named.size === 0) return undefined\n return RELEASE_FLAG_PLAN_LADDER.filter((plan) => named.has(plan))\n}\n\n/**\n * Parses a Remote Config parameter string into a `ReleaseFlagValue`.\n * Tolerant by design — the template is hand-editable in the Firebase\n * console, so plain \"true\"/\"false\" strings and malformed JSON must not\n * crash gating: anything unreadable falls back to the registry default.\n */\nexport function parseReleaseFlagValue(\n raw: string | null | undefined,\n fallbackEnabled: boolean,\n): ReleaseFlagValue {\n const text = raw?.trim()\n if (!text) return { enabled: fallbackEnabled }\n if (text === 'true') return { enabled: true }\n if (text === 'false') return { enabled: false }\n try {\n const parsed = JSON.parse(text)\n if (typeof parsed === 'boolean') return { enabled: parsed }\n if (parsed && typeof parsed === 'object') {\n const plans = parseReleaseFlagPlans(parsed.plans)\n return {\n enabled: parsed.enabled === true,\n rolloutPercent: clampPercent(parsed.rolloutPercent),\n // Spread, so a flag stored before AGL-2486 has NO `plans` key at all\n // rather than an explicit `undefined`. `JSON.stringify` drops both,\n // but the two are not the same to `'plans' in value`, which is what\n // the staff PUT uses to tell \"the operator cleared the targeting\"\n // apart from \"this client never sent the field\".\n ...(plans ? { plans } : {}),\n note: typeof parsed.note === 'string' ? parsed.note : undefined,\n }\n }\n } catch {\n // fall through to the registry default\n }\n return { enabled: fallbackEnabled }\n}\n\n/**\n * The longest parameter description Remote Config will publish (AGL-3048).\n *\n * One description past it refuses the WHOLE publish with\n * `DESCRIPTION_EXCEEDS_MAXIMUM_SIZE`, which the staff flags route answered as\n * a 500. Registry descriptions are written for the staff flags page, and\n * several run far past the limit, so what the page shows and what a\n * parameter carries cannot always be the same text.\n *\n * Counted in UTF-8 BYTES. Remote Config states the limit in characters\n * without saying how it counts them, and a UTF-8 byte count is never smaller\n * than the code-point or UTF-16 count, so text within 256 bytes is within the\n * limit on every reading. The cost is a few characters of headroom on text\n * with typographic punctuation.\n */\nexport const REMOTE_CONFIG_DESCRIPTION_MAX_BYTES = 256\n\nfunction utf8ByteLength(text: string): number {\n let bytes = 0\n for (const character of text) {\n const codePoint = character.codePointAt(0) ?? 0\n bytes +=\n codePoint < 0x80 ? 1 : codePoint < 0x800 ? 2 : codePoint < 0x10000 ? 3 : 4\n }\n return bytes\n}\n\n/** Whether Remote Config accepts `text` as a parameter description. */\nexport function fitsRemoteConfigDescription(text: string): boolean {\n return utf8ByteLength(text) <= REMOTE_CONFIG_DESCRIPTION_MAX_BYTES\n}\n\nconst DESCRIPTION_ELLIPSIS = '…'\n\n/**\n * `text` cut to a length Remote Config accepts, at a word boundary, with an\n * ellipsis marking the cut. Text that already fits comes back unchanged.\n *\n * The cut takes whole code points, so a multi-byte character is never split.\n * It backs up to the last space when it lands inside a word (only a single\n * word longer than the whole budget is cut hard), and punctuation left\n * dangling before the ellipsis is dropped.\n */\nexport function clampRemoteConfigDescription(text: string): string {\n if (fitsRemoteConfigDescription(text)) return text\n const budget =\n REMOTE_CONFIG_DESCRIPTION_MAX_BYTES - utf8ByteLength(DESCRIPTION_ELLIPSIS)\n let kept = ''\n let bytes = 0\n for (const character of text) {\n const size = utf8ByteLength(character)\n if (bytes + size > budget) break\n kept += character\n bytes += size\n }\n const next = text.charAt(kept.length)\n if (next && !/\\s/.test(next)) {\n const lastSpace = kept.search(/\\s\\S*$/)\n if (lastSpace > 0) kept = kept.slice(0, lastSpace)\n }\n return `${kept.replace(/[\\s.,;:(–—-]+$/, '')}${DESCRIPTION_ELLIPSIS}`\n}\n\n/**\n * The description to publish with a release flag's Remote Config parameter\n * (AGL-3048). Always one Remote Config accepts:\n *\n * 1. The registry description, when it fits, so the flags page and the\n * parameter say the same thing.\n * 2. Otherwise the LIVE parameter's description, when it has one that fits.\n * Remote Config has already accepted exactly that text, and someone chose\n * it for the Firebase console. The first 250 characters of a longer\n * paragraph would drop whatever the paragraph says last: for\n * `release_assist`, the `/legal/subprocessors` precondition that its\n * AGL-1909 suite wants in front of whoever reads the flag there. The full\n * registry text stays on the staff flags page either way.\n * 3. Otherwise, as on a first publish with nothing live to keep, the\n * registry description clamped at a word boundary.\n */\nexport function releaseFlagParameterDescription(\n registryDescription: string,\n liveDescription: string | null | undefined,\n): string {\n if (fitsRemoteConfigDescription(registryDescription)) {\n return registryDescription\n }\n if (\n typeof liveDescription === 'string' &&\n liveDescription.trim().length > 0 &&\n fitsRemoteConfigDescription(liveDescription)\n ) {\n return liveDescription\n }\n return clampRemoteConfigDescription(registryDescription)\n}\n\n/**\n * FNV-1a 32-bit hash → 0–99 bucket. Deterministic so a subject keeps the\n * same rollout verdict across sessions and surfaces, and seeded with the\n * flag key so a subject doesn't land in the same bucket for every flag.\n */\nexport function releaseFlagBucket(flagKey: string, subjectId: string): number {\n let hash = 0x811c9dc5\n const seed = `${flagKey}:${subjectId}`\n for (let index = 0; index < seed.length; index += 1) {\n hash ^= seed.charCodeAt(index)\n hash = Math.imul(hash, 0x01000193)\n }\n return (hash >>> 0) % 100\n}\n\n/**\n * The gating verdict for one subject.\n *\n * `subjectId` is the ORG ID, or nothing. A rollout is a cohort of\n * workspaces, so a whole workspace has to land on one side of the\n * percentage: every surface that asks about an org must ask with the same\n * string, or they answer differently about the same customer.\n *\n * This docstring used to sanction falling back to the uid, and the server\n * gates fell back to a hostId. Since a hostId, a uid and an orgId hash to\n * three different buckets, a mid-rollout flag could be on in the console\n * and off on the published site for one workspace — stable per subject, so\n * it never flickered, it just stayed wrong (AGL-1656).\n *\n * An empty subject only passes fully-enabled flags. That is the deliberate\n * cost: a request with no resolvable org never joins a partial rollout,\n * which is the conservative answer rather than a confidently wrong one.\n */\nexport function isReleaseFlagOn(\n flagKey: ReleaseFlagKey,\n value: ReleaseFlagValue,\n subjectId: string | null | undefined,\n /**\n * The subject org's plan, when the caller knows it (AGL-2486). Optional so\n * every pre-existing call site compiles and answers unchanged: a flag that\n * declares no tier targeting never reads this.\n */\n plan?: OrgPlan | null,\n): boolean {\n if (!releaseFlagTargetsPlan(value, plan)) return false\n if (value.enabled) return true\n const percent = clampPercent(value.rolloutPercent)\n if (percent <= 0 || !subjectId) return false\n if (percent >= 100) return true\n return releaseFlagBucket(flagKey, subjectId) < percent\n}\n\n/**\n * Does this flag's tier targeting admit `plan`? (AGL-2486)\n *\n * THE SEMANTICS, because an operator who cannot predict the audience will\n * not stage a rollout at all:\n *\n * - The tier list is a FILTER, applied before and independently of the\n * percentage. The percentage then picks a cohort WITHIN the admitted\n * tiers. \"Pro and above at 50%\" is therefore both \"half of the Pro+\n * workspaces\" and \"the global 50% cohort, restricted to Pro+\" — the two\n * readings name the same set, and they do so because\n * {@link releaseFlagBucket} hashes `flagKey:orgId` and NOTHING ELSE.\n * - Which is also why the bucket is stable. Adding, removing or reordering\n * tiers cannot reshuffle the bucket, so an org already inside a 50%\n * rollout keeps the feature when an unrelated tier joins the list. Had\n * the plan been mixed into the hash — the obvious way to write this — a\n * tier edit would have re-drawn the cohort under every customer already\n * in it, and a plan change would have re-rolled the dice for one.\n * - The filter binds the FULLY-ENABLED path too, not just the rollout.\n * \"On, Enterprise + Agency\" is how a launch to the top of the ladder is\n * expressed, and it is what was actually asked for. Untargeted flags are\n * unaffected: no list means every tier, so `enabled` still means everyone.\n * - An UNKNOWN plan fails a declared list. Same conservatism as a missing\n * subject on a percentage rollout: a caller that cannot say which\n * workspace it is asking about gets the safe answer rather than a\n * confidently wrong one. A per-org staff override still wins over all of\n * it — see {@link isReleaseFlagOnForOrg} — so a targeted flag can still be\n * handed to one org off-ladder.\n */\nexport function releaseFlagTargetsPlan(\n value: ReleaseFlagValue,\n plan: OrgPlan | null | undefined,\n): boolean {\n const plans = value.plans\n // `!plans` would be the idiom here, but `strictNullChecks` is off repo-wide\n // and an empty array is truthy — both \"absent\" and \"empty\" have to be named\n // for the every-tier reading to actually hold.\n if (plans == null || plans.length === 0) return true\n if (!plan) return false\n return plans.includes(plan)\n}\n\n/**\n * Per-org release-flag overrides (AGL-1635), stored on the org doc at\n * `releaseFlags`. A present key is a staff DECISION about one organization\n * and wins over both the Remote Config value and the rollout bucket; an\n * absent key inherits.\n *\n * A separate field from `entitlements.features` on purpose: those ask\n * whether the org's PLAN includes a feature, these ask whether a\n * not-yet-released feature is switched on for this one customer. Folding\n * them together would make \"granted by the deal\" and \"previewing an\n * unreleased build\" indistinguishable at the point a support question is\n * asked.\n *\n * Why the org doc and not a Remote Config condition: RC conditions are\n * template-global and would need one published condition per org, with a\n * template publish (a manual, separate deploy) for every grant.\n */\nexport type OrgReleaseFlagOverrides = Partial<Record<ReleaseFlagKey, boolean>>\n\n/**\n * Sanitises whatever is actually stored at `org.releaseFlags`.\n *\n * Tolerant for the same reason `parseReleaseFlagValue` is: this map is\n * hand-editable in the Firebase console and survives registry renames, so a\n * retired flag key or a non-boolean must be dropped rather than allowed to\n * gate anything. Unknown keys are discarded — a stale key can never grant a\n * flag that no longer exists, and a typo silently inherits instead of\n * silently forcing.\n */\nexport function parseOrgReleaseFlagOverrides(\n raw: unknown,\n): OrgReleaseFlagOverrides {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return {}\n const overrides: OrgReleaseFlagOverrides = {}\n for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {\n if (typeof value === 'boolean' && isReleaseFlagKey(key)) {\n overrides[key] = value\n }\n }\n return overrides\n}\n\n/**\n * The gating verdict with a per-org override applied on top of\n * `isReleaseFlagOn`.\n *\n * The override is checked FIRST and short-circuits, so a forced-off flag\n * stays off for an org even while the flag is globally enabled — the\n * per-org kill switch is half the point, not just the per-org grant. Every\n * release-flag gate resolves through here so the console, the tenant\n * runtime and the API dispatchers cannot disagree about one org.\n */\nexport function isReleaseFlagOnForOrg(\n flagKey: ReleaseFlagKey,\n value: ReleaseFlagValue,\n subjectId: string | null | undefined,\n overrides: OrgReleaseFlagOverrides | null | undefined,\n /**\n * The org's plan, for tier targeting (AGL-2486). Checked only AFTER the\n * override: a staff grant is a decision about one named customer and\n * outranks the tier filter exactly as it already outranks the rollout\n * bucket. That ordering is what lets a Free org preview a\n * Business-targeted flag without widening the flag for every Free org.\n *\n * REQUIRED, and `null` has to be typed out. It was optional for one\n * release and that cost a silent revenue bug: `report-usage` held the org\n * document, dropped the argument, and every `plans`-declaring flag read\n * OFF — so the contacts overage went uninvoiced for exactly the orgs\n * entitled to reach the feature, with nothing thrown and nothing logged.\n * An unknown tier still refuses, which is the correct conservatism; what\n * cannot be allowed is a caller MANUFACTURING an unknown out of a tier it\n * already has in hand. Spelling `null` makes that a statement rather than\n * an omission, and makes the next call site that forgets fail to compile\n * instead of failing to bill. `getOrgReleaseFlagTargeting` returns the\n * plan beside the overrides from ONE document read precisely so that\n * passing it costs nothing.\n */\n plan: OrgPlan | null,\n): boolean {\n const override = overrides?.[flagKey]\n if (typeof override === 'boolean') return override\n return isReleaseFlagOn(flagKey, value, subjectId, plan)\n}\n"],"names":["PLAN_LABELS","SELF_SERVE_PLANS","PLATFORM_BRAND_NAME","RELEASE_FLAGS","key","label","description","defaultEnabled","navTabId","RELEASE_FLAG_KEYS","map","definition","isReleaseFlagKey","value","includes","getReleaseFlagDefinition","find","entry","Error","clampPercent","percent","Number","isFinite","Math","min","max","round","RELEASE_FLAG_PLAN_LADDER","isOrgPlan","Object","prototype","hasOwnProperty","call","releaseFlagPlansAtOrAbove","plan","index","indexOf","slice","parseReleaseFlagPlans","raw","Array","isArray","undefined","named","Set","filter","size","has","parseReleaseFlagValue","fallbackEnabled","text","trim","enabled","parsed","JSON","parse","plans","rolloutPercent","note","REMOTE_CONFIG_DESCRIPTION_MAX_BYTES","utf8ByteLength","bytes","character","codePoint","codePointAt","fitsRemoteConfigDescription","DESCRIPTION_ELLIPSIS","clampRemoteConfigDescription","budget","kept","next","charAt","length","test","lastSpace","search","replace","releaseFlagParameterDescription","registryDescription","liveDescription","releaseFlagBucket","flagKey","subjectId","hash","seed","charCodeAt","imul","isReleaseFlagOn","releaseFlagTargetsPlan","parseOrgReleaseFlagOverrides","overrides","entries","isReleaseFlagOnForOrg","override"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;CAOC,GAED,6DAA6D;AAE7D,SAASA,WAAW,EAAEC,gBAAgB,QAAQ,yBAAqB;AACnE,SAASC,mBAAmB,QAAQ,sBAAkB;AAwCtD;;;;;CAKC,GACD,OAAO,MAAMC,gBAAkD;IAC7D;QACEC,KAAK;QACLC,OAAO;QACPC,aACE;QACFC,gBAAgB;QAChBC,UAAU;IACZ;IACA,uEAAuE;IACvE,wEAAwE;IACxE,yEAAyE;IACzE,0EAA0E;IAC1E,0EAA0E;IAC1E,mEAAmE;IACnE;QACEJ,KAAK;QACLC,OAAO;QACPC,aACE,kEACA,wEACA,uEACA,uEACA;QACFC,gBAAgB;QAChBC,UAAU;IACZ;IACA;QACEJ,KAAK;QACLC,OAAO;QACPC,aAAa;QACbC,gBAAgB;QAChBC,UAAU;IACZ;IACA;QACEJ,KAAK;QACLC,OAAO;QACPC,aAAa;QACbC,gBAAgB;QAChBC,UAAU;IACZ;IACA;QACEJ,KAAK;QACLC,OAAO;QACPC,aAAa;QACbC,gBAAgB;QAChBC,UAAU;IACZ;IACA;QACEJ,KAAK;QACLC,OAAO;QACPC,aAAa;QACbC,gBAAgB;QAChBC,UAAU;IACZ;IACA;QACEJ,KAAK;QACLC,OAAO;QACPC,aAAa;QACbC,gBAAgB;QAChBC,UAAU;IACZ;IACA;QACEJ,KAAK;QACLC,OAAO;QACPC,aACE,qEACA,+DACA;QACFC,gBAAgB;IAClB;IACA;QACEH,KAAK;QACLC,OAAO;QACPC,aACE,mEACA,qEACA,uEACA;QACFC,gBAAgB;IAClB;IACA;QACEH,KAAK;QACLC,OAAO;QACPC,aAAa;QACbC,gBAAgB;QAChB,qEAAqE;QACrE,sEAAsE;QACtE,0EAA0E;QAC1E,gEAAgE;QAChE,mEAAmE;QACnE,iEAAiE;QACjE,EAAE;QACF,mEAAmE;QACnE,qEAAqE;QACrE,sEAAsE;QACtE,uEAAuE;QACvE,sEAAsE;QACtE,uEAAuE;QACvE,wDAAwD;QACxDC,UAAU;IACZ;IACA,sEAAsE;IACtE,wEAAwE;IACxE,yEAAyE;IACzE,oCAAoC;IACpC;QACEJ,KAAK;QACLC,OAAO;QACPC,aAAa;QACbC,gBAAgB;QAChBC,UAAU;IACZ;IACA;QACEJ,KAAK;QACLC,OAAO;QACPC,aAAa;QACbC,gBAAgB;IAClB;IACA;QACEH,KAAK;QACLC,OAAO;QACPC,aAAa;QACbC,gBAAgB;QAChBC,UAAU;IACZ;IACA;QACEJ,KAAK;QACLC,OAAO;QACPC,aAAa;QACbC,gBAAgB;QAChBC,UAAU;IACZ;IACA;QACEJ,KAAK;QACLC,OAAO;QACPC,aACE,uEACA;QACFC,gBAAgB;IAClB;IACA;QACEH,KAAK;QACLC,OAAO;QACPC,aACE,4DACA,uEACA,sEACA,yEACA,uEACA,yEACA,sEACA,wEACA,sEACA,wEACA;QACFC,gBAAgB;IAClB;IACA,0EAA0E;IAC1E,yEAAyE;IACzE,0EAA0E;IAC1E,mCAAmC;IACnC;QACEH,KAAK;QACLC,OAAO;QACPC,aACE,wEACA,gEACA,wEACA;QACFC,gBAAgB;IAClB;IACA;QACEH,KAAK;QACLC,OAAO,GAAGH,oBAAoB,OAAO,CAAC;QACtCI,aACE,kEACA,sEACA,kEACA,0EACA,0EACA,oEACA,yEACA,mEACA,sEACA,gEACA,qEACA,sEACA,yEACA,wEACA,0EACA;QACFC,gBAAgB;IAClB;IACA,yEAAyE;IACzE,4EAA4E;IAC5E,mEAAmE;IACnE,+DAA+D;IAC/D;QACEH,KAAK;QACLC,OAAO;QACPC,aACE,sEACA,0EACA,sEACA;QACFC,gBAAgB;IAClB;IACA,0EAA0E;IAC1E,2EAA2E;IAC3E,2EAA2E;IAC3E,wEAAwE;IACxE,uEAAuE;IACvE,uEAAuE;IACvE,6DAA6D;IAC7D;QACEH,KAAK;QACLC,OAAO;QACPC,aACE,sEACA,2EACA,qEACA;QACFC,gBAAgB;IAClB;IACA,wEAAwE;IACxE,wEAAwE;IACxE,sEAAsE;IACtE,8DAA8D;IAC9D,yEAAyE;IACzE,yEAAyE;IACzE,wEAAwE;IACxE,sCAAsC;IACtC;QACEH,KAAK;QACLC,OAAO,GAAGH,oBAAoB,cAAc,CAAC;QAC7CI,aACE,6DACA,uEACA,wEAC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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/release-flags.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 * Release flags (AGL-227): platform-level \"is this feature launched\"\n * gating backed by Firebase Remote Config. A separate axis from plan\n * entitlements (`plan-entitlements.ts`) — entitlements ask whether the\n * tenant's PLAN includes a feature, release flags ask whether the feature\n * is released to the public at all (or partially, via percentage rollout).\n * Staff bypass release flags everywhere but see a flagged warning.\n */\n\n/** Remote Config parameter keys for release-gated features. */\nimport type { OrgPlan } from '../foundation'\nimport { PLAN_LABELS, SELF_SERVE_PLANS } from './plan-entitlements'\nimport { PLATFORM_BRAND_NAME } from './platform-brand'\nimport {\n PLUGIN_RELEASE_FLAGS,\n type PluginReleaseFlagKey,\n} from './plugin-release-flags.generated'\n\n/**\n * Every release flag: the ones plugins declare (compiled from\n * plugins.config.json, AGL-3080) and the platform's own below.\n */\nexport type ReleaseFlagKey =\n | PluginReleaseFlagKey\n | 'release_addon_store'\n | 'release_native_checkout'\n | 'release_edit_bar'\n | 'release_assist'\n | 'release_video_uploads'\n | 'release_video_delivery'\n | 'release_ai_generative'\n\nexport interface ReleaseFlagDefinition {\n key: ReleaseFlagKey\n label: string\n description: string\n /**\n * Fallback verdict when Remote Config is unreachable (offline, blocked,\n * first paint before activate). MUST match the seeded value in\n * cloud/firebase-remoteconfig.template.json so environments without a\n * published template behave like the template intends.\n */\n defaultEnabled: boolean\n /** Host dashboard tab id (host-nav-tabs.ts) this flag hides, if any. */\n navTabId?: string\n}\n\n/** A release flag a plugin declares, as the generator compiles it. */\nexport type PluginReleaseFlagDefinition = ReleaseFlagDefinition & {\n key: PluginReleaseFlagKey\n}\n\n/**\n * The platform's own release flags: features no plugin owns.\n *\n * Every first-party plugin is release-flagged too (AGL-422) — the flag feeds\n * the plugin LOADER (console, published sites, API dispatch), not just nav\n * visibility, so staff can kill-switch a whole plugin platform-wide from the\n * Feature Flags page. Those flags are the plugins' own: each is defined by\n * its catalog row's `releaseFlagDefinition` in plugins.config.json and\n * compiled into `PLUGIN_RELEASE_FLAGS`, so adding a plugin edits nothing\n * here.\n */\nconst PLATFORM_RELEASE_FLAGS: readonly ReleaseFlagDefinition[] = [\n {\n key: 'release_addon_store',\n label: 'Add-on store',\n description:\n 'Self-serve add-on purchases on the Billing page: seats, datasets, ' +\n 'extra sites, POS registers, Event Calendar (AGL-524..531).',\n defaultEnabled: true,\n },\n {\n key: 'release_native_checkout',\n label: 'In-page checkout',\n description:\n 'Pay without leaving the page, instead of a redirect to ' +\n 'checkout.stripe.com. TWO surfaces: the console plan checkout uses ' +\n 'embedded Checkout (AGL-1132), and a merchant storefront uses the ' +\n 'Payment Element on the merchant\\u2019s own domain (AGL-1944) \\u2014 ' +\n 'which is the half that costs conversions, since leaving the store ' +\n 'mid-purchase is where carts get abandoned. Both keep the webhook as ' +\n 'the only thing that fulfils. OFF by default: the redirect is the ' +\n 'proven path, and neither surface can be verified without putting a ' +\n 'real card through it. Also gated on a publishable key being set, ' +\n 'per surface \\u2014 so flipping this alone degrades to the redirect ' +\n 'rather than to a dead button.',\n defaultEnabled: false,\n },\n // Released to every site 2026-09-16 (AGL-3041). Still the whole surface's\n // kill switch, globally or for one org by override: off, the tenant slot\n // renders nothing, both token mints refuse, and `/api/edit-context` turns\n // away every token already issued.\n {\n key: 'release_edit_bar',\n label: 'Site admin bar',\n description:\n 'Admin bar on published sites: a signed-in editor jumps from a live ' +\n 'page straight into the Besigner to edit it. Released ' +\n '2026-09-16 (AGL-3041) for every site; turning it off hides the bar ' +\n 'and revokes every outstanding edit token at the verify site.',\n defaultEnabled: true,\n },\n {\n key: 'release_assist',\n label: `${PLATFORM_BRAND_NAME} Assist`,\n description:\n 'The in-console AI chat helper on every page (AGL-1860): docs-' +\n 'grounded answers with deep links, page-context guidance on Pro+. ' +\n 'OFF by default, and blocked on TWO published legal artifacts ' +\n '(AGL-1909), neither of which is a repo file — both are live besigner ' +\n 'pages, so publication is what satisfies them: (1) the privacy-policy ' +\n 'disclosure for stored Q&A, because the data loop records every ' +\n 'exchange org-scoped under orgs/{orgId}/assistExchanges; and (2) the ' +\n 'Anthropic row on /legal/subprocessors, which was deliberately ' +\n 'REMOVED on 2026-08-13 (subprocessorsV2a-20260813) on the premise ' +\n 'that no production key existed — so the page is not merely ' +\n 'incomplete without it, it is affirmatively wrong. NOTE the real ' +\n 'trigger is ANTHROPIC_API_KEY, not this flag: /api/ai/assist (the ' +\n 'besigner copy assistant, AGL-89/130/169) carries no release flag at ' +\n 'all and sends customer site content to Anthropic on the key plus a ' +\n 'Pro entitlement alone. Setting the key in production therefore makes ' +\n 'Anthropic a subprocessor whether or not this flag is ever flipped.',\n defaultEnabled: false,\n },\n // Ingress only, and with no staff preview on the server: a video a staff\n // session uploads into a customer's library serves on that customer's pages\n // like any other, so a grant goes to the org through its override.\n // `apps/console/utils/server/video-uploads.ts` holds the gate.\n {\n key: 'release_video_uploads',\n label: 'Video uploads',\n description:\n 'New video in the media library, on every upload path (AGL-2830). ' +\n 'Off: refused with 403 video_uploads_paused, as DAM video delivery is ' +\n 'not yet metered or bounded per org (AGL-2810, AGL-2812). Images, ' +\n 'documents and stored videos are unaffected.',\n defaultEnabled: false,\n },\n // Delivery, not ingress: this flag decides whether a video already in the\n // library is COPIED to the configured delivery provider and served from it\n // through a short-lived signed redirect (AGL-2824). Per org, with no staff\n // preview on the server — a copy is customer data at the provider, so a\n // grant goes to the org through its override. Off, or with no provider\n // configured, every video serves from this platform exactly as before.\n // `media-delivery.ts` in `tenant-data-admin` holds the gate.\n {\n key: 'release_video_delivery',\n label: 'Video delivery',\n description:\n 'Serves library video from a configured delivery provider through ' +\n 'short-lived signed redirects, and copies each video to it (AGL-2824). ' +\n 'OFF by default: video serves from this platform as before. Also ' +\n \"needs the provider's settings.\",\n defaultEnabled: false,\n },\n // The generative doors share one runtime with Assist (AGL-2903) but not\n // its flag: `release_assist` answers questions, this builds things, and\n // the two are sold, metered and disclosed separately. The server-side\n // gate lives in `aiGateLadder` (404 when off, staff preview).\n // Labeled for what it switches — the generative doors — and not with the\n // add-on's own name, which is sold and live whether or not the doors are\n // released: the docs name the add-on, and a flag wearing the same words\n // reads as documented when it is not.\n {\n key: 'release_ai_generative',\n label: `${PLATFORM_BRAND_NAME} AI generation`,\n description:\n 'Generative building and automation behind the AI add-on ' +\n '(AGL-2903): sections, pages and workflows written by a model from ' +\n 'a brief, on the shared Anthropic runtime. OFF by default and staff ' +\n 'preview only; every generative route answers 404 while it is off. ' +\n 'Turning it on sends customer briefs and site content to Anthropic ' +\n 'on the same ANTHROPIC_API_KEY and under the same subprocessor ' +\n 'disclosure as Assist (AGL-1909).',\n defaultEnabled: false,\n },\n]\n\n/**\n * The registry: one entry per gated feature, the plugins' first. Adding a\n * platform flag = add it above, seed it in the Remote Config template, and\n * (optionally) wrap the page in `<FeatureGate>` — the staff admin editor and\n * nav filtering pick it up from this list.\n */\nexport const RELEASE_FLAGS: readonly ReleaseFlagDefinition[] = [\n ...PLUGIN_RELEASE_FLAGS,\n ...PLATFORM_RELEASE_FLAGS,\n]\n\nexport const RELEASE_FLAG_KEYS = RELEASE_FLAGS.map(\n (definition) => definition.key,\n) as readonly ReleaseFlagKey[]\n\nexport function isReleaseFlagKey(value: string): value is ReleaseFlagKey {\n return (RELEASE_FLAG_KEYS as readonly string[]).includes(value)\n}\n\nexport function getReleaseFlagDefinition(\n key: ReleaseFlagKey,\n): ReleaseFlagDefinition {\n const definition = RELEASE_FLAGS.find((entry) => entry.key === key)\n if (!definition) throw new Error(`Unknown release flag: ${key}`)\n return definition\n}\n\n/**\n * The JSON payload stored in each Remote Config parameter. `enabled: true`\n * turns the feature on for everyone; `enabled: false` with a positive\n * `rolloutPercent` enables it for that percentage of subjects (stable\n * per-tenant bucketing, GrowthBook-style); `plans` narrows either of those\n * to a set of tiers (AGL-2486).\n */\nexport interface ReleaseFlagValue {\n enabled: boolean\n /** 0–100; only consulted while `enabled` is false. */\n rolloutPercent?: number\n /**\n * Plan/tier targeting (AGL-2486). The tiers this flag is being rolled out\n * to, or ABSENT for \"every tier\".\n *\n * ABSENT AND EMPTY BOTH MEAN EVERY TIER, and that is load-bearing: every\n * flag stored before this field existed parses to `undefined` here, and an\n * operator who unticks every box has plainly not asked for a flag that\n * reaches nobody. Reading an empty list as \"no tiers\" would dark-launch\n * nothing while the console still said the flag was on — the inverted\n * reading of this field is the whole hazard, so it is closed in the parser\n * rather than left to each call site.\n */\n plans?: OrgPlan[]\n /** Free-form staff note (\"waiting on AGL-199\", owner, etc.). */\n note?: string\n}\n\nconst clampPercent = (value: unknown): number => {\n const percent = typeof value === 'number' && Number.isFinite(value) ? value : 0\n return Math.min(100, Math.max(0, Math.round(percent)))\n}\n\n/**\n * Every plan a flag may target, cheapest first — `SELF_SERVE_PLANS` then\n * `enterprise`, the same ladder `planGrantingFeature` walks and the same\n * order the plan grid renders. Derived, never re-typed: pricing v3 inserted\n * `scale` mid-ladder and added `agency`, and a second hand-written tier list\n * here would have missed both.\n */\nexport const RELEASE_FLAG_PLAN_LADDER: readonly OrgPlan[] = [\n ...SELF_SERVE_PLANS,\n 'enterprise' as OrgPlan,\n]\n\nconst isOrgPlan = (value: unknown): value is OrgPlan =>\n typeof value === 'string' &&\n Object.prototype.hasOwnProperty.call(PLAN_LABELS, value)\n\n/**\n * The tiers on the ladder at or above `plan` — what the console's \"Pro and\n * above\" shortcut expands to before it is STORED as an explicit list.\n *\n * Deliberately a UI convenience and not a stored \"minimum plan\" mode: a\n * stored threshold would silently re-aim every live flag the day a tier is\n * inserted into the middle of the ladder (pricing v3 did exactly that with\n * `scale`). An explicit list means the audience of a published flag only\n * ever changes because a human changed it.\n */\nexport function releaseFlagPlansAtOrAbove(plan: OrgPlan): OrgPlan[] {\n const index = RELEASE_FLAG_PLAN_LADDER.indexOf(plan)\n return index < 0 ? [] : RELEASE_FLAG_PLAN_LADDER.slice(index)\n}\n\n/**\n * Sanitises whatever is stored at `plans`, in ladder order.\n *\n * Returns `undefined` — not `[]` — for \"no targeting declared\", so the\n * absence survives a round trip through the staff editor and cannot be\n * written back as a list that means something else. Unknown tier names are\n * dropped for the same reason `parseOrgReleaseFlagOverrides` drops unknown\n * keys: a renamed or retired tier must never gate anything. A list that\n * names ONLY unknown tiers collapses to `undefined` (every tier) rather than\n * to an empty list, because a typo must inherit, never silently target.\n */\nexport function parseReleaseFlagPlans(raw: unknown): OrgPlan[] | undefined {\n if (!Array.isArray(raw)) return undefined\n const named = new Set(raw.filter(isOrgPlan))\n if (named.size === 0) return undefined\n return RELEASE_FLAG_PLAN_LADDER.filter((plan) => named.has(plan))\n}\n\n/**\n * Parses a Remote Config parameter string into a `ReleaseFlagValue`.\n * Tolerant by design — the template is hand-editable in the Firebase\n * console, so plain \"true\"/\"false\" strings and malformed JSON must not\n * crash gating: anything unreadable falls back to the registry default.\n */\nexport function parseReleaseFlagValue(\n raw: string | null | undefined,\n fallbackEnabled: boolean,\n): ReleaseFlagValue {\n const text = raw?.trim()\n if (!text) return { enabled: fallbackEnabled }\n if (text === 'true') return { enabled: true }\n if (text === 'false') return { enabled: false }\n try {\n const parsed = JSON.parse(text)\n if (typeof parsed === 'boolean') return { enabled: parsed }\n if (parsed && typeof parsed === 'object') {\n const plans = parseReleaseFlagPlans(parsed.plans)\n return {\n enabled: parsed.enabled === true,\n rolloutPercent: clampPercent(parsed.rolloutPercent),\n // Spread, so a flag stored before AGL-2486 has NO `plans` key at all\n // rather than an explicit `undefined`. `JSON.stringify` drops both,\n // but the two are not the same to `'plans' in value`, which is what\n // the staff PUT uses to tell \"the operator cleared the targeting\"\n // apart from \"this client never sent the field\".\n ...(plans ? { plans } : {}),\n note: typeof parsed.note === 'string' ? parsed.note : undefined,\n }\n }\n } catch {\n // fall through to the registry default\n }\n return { enabled: fallbackEnabled }\n}\n\n/**\n * The longest parameter description Remote Config will publish (AGL-3048).\n *\n * One description past it refuses the WHOLE publish with\n * `DESCRIPTION_EXCEEDS_MAXIMUM_SIZE`, which the staff flags route answered as\n * a 500. Registry descriptions are written for the staff flags page, and\n * several run far past the limit, so what the page shows and what a\n * parameter carries cannot always be the same text.\n *\n * Counted in UTF-8 BYTES. Remote Config states the limit in characters\n * without saying how it counts them, and a UTF-8 byte count is never smaller\n * than the code-point or UTF-16 count, so text within 256 bytes is within the\n * limit on every reading. The cost is a few characters of headroom on text\n * with typographic punctuation.\n */\nexport const REMOTE_CONFIG_DESCRIPTION_MAX_BYTES = 256\n\nfunction utf8ByteLength(text: string): number {\n let bytes = 0\n for (const character of text) {\n const codePoint = character.codePointAt(0) ?? 0\n bytes +=\n codePoint < 0x80 ? 1 : codePoint < 0x800 ? 2 : codePoint < 0x10000 ? 3 : 4\n }\n return bytes\n}\n\n/** Whether Remote Config accepts `text` as a parameter description. */\nexport function fitsRemoteConfigDescription(text: string): boolean {\n return utf8ByteLength(text) <= REMOTE_CONFIG_DESCRIPTION_MAX_BYTES\n}\n\nconst DESCRIPTION_ELLIPSIS = '…'\n\n/**\n * `text` cut to a length Remote Config accepts, at a word boundary, with an\n * ellipsis marking the cut. Text that already fits comes back unchanged.\n *\n * The cut takes whole code points, so a multi-byte character is never split.\n * It backs up to the last space when it lands inside a word (only a single\n * word longer than the whole budget is cut hard), and punctuation left\n * dangling before the ellipsis is dropped.\n */\nexport function clampRemoteConfigDescription(text: string): string {\n if (fitsRemoteConfigDescription(text)) return text\n const budget =\n REMOTE_CONFIG_DESCRIPTION_MAX_BYTES - utf8ByteLength(DESCRIPTION_ELLIPSIS)\n let kept = ''\n let bytes = 0\n for (const character of text) {\n const size = utf8ByteLength(character)\n if (bytes + size > budget) break\n kept += character\n bytes += size\n }\n const next = text.charAt(kept.length)\n if (next && !/\\s/.test(next)) {\n const lastSpace = kept.search(/\\s\\S*$/)\n if (lastSpace > 0) kept = kept.slice(0, lastSpace)\n }\n return `${kept.replace(/[\\s.,;:(–—-]+$/, '')}${DESCRIPTION_ELLIPSIS}`\n}\n\n/**\n * The description to publish with a release flag's Remote Config parameter\n * (AGL-3048). Always one Remote Config accepts:\n *\n * 1. The registry description, when it fits, so the flags page and the\n * parameter say the same thing.\n * 2. Otherwise the LIVE parameter's description, when it has one that fits.\n * Remote Config has already accepted exactly that text, and someone chose\n * it for the Firebase console. The first 250 characters of a longer\n * paragraph would drop whatever the paragraph says last: for\n * `release_assist`, the `/legal/subprocessors` precondition that its\n * AGL-1909 suite wants in front of whoever reads the flag there. The full\n * registry text stays on the staff flags page either way.\n * 3. Otherwise, as on a first publish with nothing live to keep, the\n * registry description clamped at a word boundary.\n */\nexport function releaseFlagParameterDescription(\n registryDescription: string,\n liveDescription: string | null | undefined,\n): string {\n if (fitsRemoteConfigDescription(registryDescription)) {\n return registryDescription\n }\n if (\n typeof liveDescription === 'string' &&\n liveDescription.trim().length > 0 &&\n fitsRemoteConfigDescription(liveDescription)\n ) {\n return liveDescription\n }\n return clampRemoteConfigDescription(registryDescription)\n}\n\n/**\n * FNV-1a 32-bit hash → 0–99 bucket. Deterministic so a subject keeps the\n * same rollout verdict across sessions and surfaces, and seeded with the\n * flag key so a subject doesn't land in the same bucket for every flag.\n */\nexport function releaseFlagBucket(flagKey: string, subjectId: string): number {\n let hash = 0x811c9dc5\n const seed = `${flagKey}:${subjectId}`\n for (let index = 0; index < seed.length; index += 1) {\n hash ^= seed.charCodeAt(index)\n hash = Math.imul(hash, 0x01000193)\n }\n return (hash >>> 0) % 100\n}\n\n/**\n * The gating verdict for one subject.\n *\n * `subjectId` is the ORG ID, or nothing. A rollout is a cohort of\n * workspaces, so a whole workspace has to land on one side of the\n * percentage: every surface that asks about an org must ask with the same\n * string, or they answer differently about the same customer.\n *\n * This docstring used to sanction falling back to the uid, and the server\n * gates fell back to a hostId. Since a hostId, a uid and an orgId hash to\n * three different buckets, a mid-rollout flag could be on in the console\n * and off on the published site for one workspace — stable per subject, so\n * it never flickered, it just stayed wrong (AGL-1656).\n *\n * An empty subject only passes fully-enabled flags. That is the deliberate\n * cost: a request with no resolvable org never joins a partial rollout,\n * which is the conservative answer rather than a confidently wrong one.\n */\nexport function isReleaseFlagOn(\n flagKey: ReleaseFlagKey,\n value: ReleaseFlagValue,\n subjectId: string | null | undefined,\n /**\n * The subject org's plan, when the caller knows it (AGL-2486). Optional so\n * every pre-existing call site compiles and answers unchanged: a flag that\n * declares no tier targeting never reads this.\n */\n plan?: OrgPlan | null,\n): boolean {\n if (!releaseFlagTargetsPlan(value, plan)) return false\n if (value.enabled) return true\n const percent = clampPercent(value.rolloutPercent)\n if (percent <= 0 || !subjectId) return false\n if (percent >= 100) return true\n return releaseFlagBucket(flagKey, subjectId) < percent\n}\n\n/**\n * Does this flag's tier targeting admit `plan`? (AGL-2486)\n *\n * THE SEMANTICS, because an operator who cannot predict the audience will\n * not stage a rollout at all:\n *\n * - The tier list is a FILTER, applied before and independently of the\n * percentage. The percentage then picks a cohort WITHIN the admitted\n * tiers. \"Pro and above at 50%\" is therefore both \"half of the Pro+\n * workspaces\" and \"the global 50% cohort, restricted to Pro+\" — the two\n * readings name the same set, and they do so because\n * {@link releaseFlagBucket} hashes `flagKey:orgId` and NOTHING ELSE.\n * - Which is also why the bucket is stable. Adding, removing or reordering\n * tiers cannot reshuffle the bucket, so an org already inside a 50%\n * rollout keeps the feature when an unrelated tier joins the list. Had\n * the plan been mixed into the hash — the obvious way to write this — a\n * tier edit would have re-drawn the cohort under every customer already\n * in it, and a plan change would have re-rolled the dice for one.\n * - The filter binds the FULLY-ENABLED path too, not just the rollout.\n * \"On, Enterprise + Agency\" is how a launch to the top of the ladder is\n * expressed, and it is what was actually asked for. Untargeted flags are\n * unaffected: no list means every tier, so `enabled` still means everyone.\n * - An UNKNOWN plan fails a declared list. Same conservatism as a missing\n * subject on a percentage rollout: a caller that cannot say which\n * workspace it is asking about gets the safe answer rather than a\n * confidently wrong one. A per-org staff override still wins over all of\n * it — see {@link isReleaseFlagOnForOrg} — so a targeted flag can still be\n * handed to one org off-ladder.\n */\nexport function releaseFlagTargetsPlan(\n value: ReleaseFlagValue,\n plan: OrgPlan | null | undefined,\n): boolean {\n const plans = value.plans\n // `!plans` would be the idiom here, but `strictNullChecks` is off repo-wide\n // and an empty array is truthy — both \"absent\" and \"empty\" have to be named\n // for the every-tier reading to actually hold.\n if (plans == null || plans.length === 0) return true\n if (!plan) return false\n return plans.includes(plan)\n}\n\n/**\n * Per-org release-flag overrides (AGL-1635), stored on the org doc at\n * `releaseFlags`. A present key is a staff DECISION about one organization\n * and wins over both the Remote Config value and the rollout bucket; an\n * absent key inherits.\n *\n * A separate field from `entitlements.features` on purpose: those ask\n * whether the org's PLAN includes a feature, these ask whether a\n * not-yet-released feature is switched on for this one customer. Folding\n * them together would make \"granted by the deal\" and \"previewing an\n * unreleased build\" indistinguishable at the point a support question is\n * asked.\n *\n * Why the org doc and not a Remote Config condition: RC conditions are\n * template-global and would need one published condition per org, with a\n * template publish (a manual, separate deploy) for every grant.\n */\nexport type OrgReleaseFlagOverrides = Partial<Record<ReleaseFlagKey, boolean>>\n\n/**\n * Sanitises whatever is actually stored at `org.releaseFlags`.\n *\n * Tolerant for the same reason `parseReleaseFlagValue` is: this map is\n * hand-editable in the Firebase console and survives registry renames, so a\n * retired flag key or a non-boolean must be dropped rather than allowed to\n * gate anything. Unknown keys are discarded — a stale key can never grant a\n * flag that no longer exists, and a typo silently inherits instead of\n * silently forcing.\n */\nexport function parseOrgReleaseFlagOverrides(\n raw: unknown,\n): OrgReleaseFlagOverrides {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return {}\n const overrides: OrgReleaseFlagOverrides = {}\n for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {\n if (typeof value === 'boolean' && isReleaseFlagKey(key)) {\n overrides[key] = value\n }\n }\n return overrides\n}\n\n/**\n * The gating verdict with a per-org override applied on top of\n * `isReleaseFlagOn`.\n *\n * The override is checked FIRST and short-circuits, so a forced-off flag\n * stays off for an org even while the flag is globally enabled — the\n * per-org kill switch is half the point, not just the per-org grant. Every\n * release-flag gate resolves through here so the console, the tenant\n * runtime and the API dispatchers cannot disagree about one org.\n */\nexport function isReleaseFlagOnForOrg(\n flagKey: ReleaseFlagKey,\n value: ReleaseFlagValue,\n subjectId: string | null | undefined,\n overrides: OrgReleaseFlagOverrides | null | undefined,\n /**\n * The org's plan, for tier targeting (AGL-2486). Checked only AFTER the\n * override: a staff grant is a decision about one named customer and\n * outranks the tier filter exactly as it already outranks the rollout\n * bucket. That ordering is what lets a Free org preview a\n * Business-targeted flag without widening the flag for every Free org.\n *\n * REQUIRED, and `null` has to be typed out. It was optional for one\n * release and that cost a silent revenue bug: `report-usage` held the org\n * document, dropped the argument, and every `plans`-declaring flag read\n * OFF — so the contacts overage went uninvoiced for exactly the orgs\n * entitled to reach the feature, with nothing thrown and nothing logged.\n * An unknown tier still refuses, which is the correct conservatism; what\n * cannot be allowed is a caller MANUFACTURING an unknown out of a tier it\n * already has in hand. Spelling `null` makes that a statement rather than\n * an omission, and makes the next call site that forgets fail to compile\n * instead of failing to bill. `getOrgReleaseFlagTargeting` returns the\n * plan beside the overrides from ONE document read precisely so that\n * passing it costs nothing.\n */\n plan: OrgPlan | null,\n): boolean {\n const override = overrides?.[flagKey]\n if (typeof override === 'boolean') return override\n return isReleaseFlagOn(flagKey, value, subjectId, plan)\n}\n"],"names":["PLAN_LABELS","SELF_SERVE_PLANS","PLATFORM_BRAND_NAME","PLUGIN_RELEASE_FLAGS","PLATFORM_RELEASE_FLAGS","key","label","description","defaultEnabled","RELEASE_FLAGS","RELEASE_FLAG_KEYS","map","definition","isReleaseFlagKey","value","includes","getReleaseFlagDefinition","find","entry","Error","clampPercent","percent","Number","isFinite","Math","min","max","round","RELEASE_FLAG_PLAN_LADDER","isOrgPlan","Object","prototype","hasOwnProperty","call","releaseFlagPlansAtOrAbove","plan","index","indexOf","slice","parseReleaseFlagPlans","raw","Array","isArray","undefined","named","Set","filter","size","has","parseReleaseFlagValue","fallbackEnabled","text","trim","enabled","parsed","JSON","parse","plans","rolloutPercent","note","REMOTE_CONFIG_DESCRIPTION_MAX_BYTES","utf8ByteLength","bytes","character","codePoint","codePointAt","fitsRemoteConfigDescription","DESCRIPTION_ELLIPSIS","clampRemoteConfigDescription","budget","kept","next","charAt","length","test","lastSpace","search","replace","releaseFlagParameterDescription","registryDescription","liveDescription","releaseFlagBucket","flagKey","subjectId","hash","seed","charCodeAt","imul","isReleaseFlagOn","releaseFlagTargetsPlan","parseOrgReleaseFlagOverrides","overrides","entries","isReleaseFlagOnForOrg","override"],"mappings":"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"}
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
17
|
import type { AglynAttributeSchema } from '../foundation/definitions/components.types';
|
|
18
|
+
import type { EntityPickerKind } from './entity-picker-context';
|
|
18
19
|
import type { RepeatableDataset } from './expand-repeatables';
|
|
19
20
|
/**
|
|
20
21
|
* Where a repeat's rows come from, for the editor (AGL-3111).
|
|
@@ -78,6 +79,14 @@ export interface RepeatSource {
|
|
|
78
79
|
* component attribute is. The panel names the field after `keyProp`.
|
|
79
80
|
*/
|
|
80
81
|
keyAttribute: Omit<AglynAttributeSchema, 'name'>;
|
|
82
|
+
/**
|
|
83
|
+
* The entity picker kind a key names, when a key is an entity the picker
|
|
84
|
+
* lists. With it, the insert-token menu inside the repeat names the entity
|
|
85
|
+
* through the picker and offers `{{item.<field>}}` for each field the
|
|
86
|
+
* entity declares (`EntityPickerContextValue.entityFields`). Without it the
|
|
87
|
+
* menu offers no item tokens and authors type them.
|
|
88
|
+
*/
|
|
89
|
+
entityKind?: EntityPickerKind;
|
|
81
90
|
/**
|
|
82
91
|
* Reads one key's rows for the besigner's canvas preview and badge. A React
|
|
83
92
|
* hook: the editor mounts one reader per key and calls it unconditionally,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/repeat-sources.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 type { AglynAttributeSchema } from '../foundation/definitions/components.types'\nimport type { RepeatableDataset } from './expand-repeatables'\n\n/**\n * Where a repeat's rows come from, for the editor (AGL-3111).\n *\n * Any element can repeat, and the platform decides what a repeat MEANS — the\n * scope, the copies, the bounds (`expand-repeatables.ts`). What it does not\n * decide is what an element repeats OVER. That belongs to the plugin that owns\n * the rows, which registers a source here from the registration function its\n * loader calls by name. Core names no source.\n *\n * Reached by path, never through a barrel: nothing on a published page asks\n * where rows come from, so nothing here is in a published page's download.\n */\n\n/**\n * What a source answers for one key.\n *\n * - `loading` — no answer yet.\n * - `missing` — the key names nothing this site may use. The published page\n * renders the element once, as written.\n * - `error` — the read failed. Nothing is known about the rows.\n * - `ready` — the rows, read and bounded exactly as the published page's\n * composition reads them.\n */\nexport type RepeatRowsAnswer =\n | { status: 'loading' }\n | { status: 'missing' }\n | { status: 'error' }\n | {\n status: 'ready'\n /** The name authors know the entry by, for the canvas badge. */\n label: string\n /**\n * The rows under every key the expansion looks them up by: the key as\n * the node stores it, the entry's id, and each entry a one-hop reference\n * in the rows reads (`{{item.ref.field}}`).\n */\n rowsByKey: Readonly<Record<string, RepeatableDataset>>\n }\n\n/** One read a source is asked for. */\nexport interface RepeatRowsRequest {\n /** The site the canvas edits. A source answers only what it may show. */\n hostId: string\n /** The key exactly as the node stores it. */\n key: string\n}\n\n/** One kind of thing an element can repeat over. */\nexport interface RepeatSource {\n /** Stable id, e.g. the plugin's name for the kind. */\n id: string\n /** What the Attributes panel calls the kind. */\n label: string\n /**\n * The node prop the chosen key persists under. Persisted in documents, so a\n * source never renames it.\n */\n keyProp: string\n /**\n * The attribute that picks a key, drawn by the Attributes panel as any\n * component attribute is. The panel names the field after `keyProp`.\n */\n keyAttribute: Omit<AglynAttributeSchema, 'name'>\n /**\n * Reads one key's rows for the besigner's canvas preview and badge. A React\n * hook: the editor mounts one reader per key and calls it unconditionally,\n * so its identity must not change after registration.\n */\n useRows(request: RepeatRowsRequest): RepeatRowsAnswer\n}\n\nconst sources = new Map<string, RepeatSource>()\nconst listeners = new Set<() => void>()\nlet version = 0\n/**\n * Replaced only when the registered set changes, never rebuilt per call: it is\n * the snapshot `useSyncExternalStore` compares, and a fresh array every time\n * is a render loop rather than a wasted allocation.\n */\nlet snapshot: readonly RepeatSource[] = []\n\nconst notify = () => {\n version += 1\n snapshot = [...sources.values()]\n for (const listener of [...listeners]) listener()\n}\n\n/**\n * Registers a source, replacing any registered under its id. Returns the\n * unregister, which removes the source only if it is still the one registered.\n *\n * Call it from a registration function the plugin loader calls BY NAME, never\n * from a module's top level: a bundler deletes a module imported only for its\n * side effect when its package says it has none (AGL-3025).\n */\nexport function registerRepeatSource(source: RepeatSource): () => void {\n if (sources.get(source.id) !== source) {\n sources.set(source.id, source)\n notify()\n }\n return () => {\n if (sources.get(source.id) !== source) return\n sources.delete(source.id)\n notify()\n }\n}\n\n/**\n * Every registered source, in registration order. The SAME array until the\n * registered set changes, so it can be read as a store snapshot.\n */\nexport function listRepeatSources(): readonly RepeatSource[] {\n return snapshot\n}\n\n/** The source registered under `id`. */\nexport function getRepeatSource(id: string): RepeatSource | undefined {\n return sources.get(id)\n}\n\n/** Called after the registered set changes. Returns the unsubscribe. */\nexport function subscribeRepeatSources(listener: () => void): () => void {\n listeners.add(listener)\n return () => {\n listeners.delete(listener)\n }\n}\n\n/** Moves on every change — the snapshot `useSyncExternalStore` reads. */\nexport function getRepeatSourcesVersion(): number {\n return version\n}\n\n/**\n * The registered source a node repeats over, and the key it names, trimmed.\n * `undefined` for a node that repeats over nothing a registered source owns.\n */\nexport function repeatSourceOf(\n node: unknown,\n): { source: RepeatSource; key: string } | undefined {\n const props = (node as { props?: Record<string, unknown> } | undefined)?.props\n if (!props) return undefined\n for (const source of sources.values()) {\n const value = props[source.keyProp]\n const key = typeof value === 'string' ? value.trim() : ''\n if (key) return { source, key }\n }\n return undefined\n}\n"],"names":["sources","Map","listeners","Set","version","snapshot","notify","values","listener","registerRepeatSource","source","get","id","set","delete","listRepeatSources","getRepeatSource","subscribeRepeatSources","add","getRepeatSourcesVersion","repeatSourceOf","node","props","undefined","value","keyProp","key","trim"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/repeat-sources.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 type { AglynAttributeSchema } from '../foundation/definitions/components.types'\nimport type { EntityPickerKind } from './entity-picker-context'\nimport type { RepeatableDataset } from './expand-repeatables'\n\n/**\n * Where a repeat's rows come from, for the editor (AGL-3111).\n *\n * Any element can repeat, and the platform decides what a repeat MEANS — the\n * scope, the copies, the bounds (`expand-repeatables.ts`). What it does not\n * decide is what an element repeats OVER. That belongs to the plugin that owns\n * the rows, which registers a source here from the registration function its\n * loader calls by name. Core names no source.\n *\n * Reached by path, never through a barrel: nothing on a published page asks\n * where rows come from, so nothing here is in a published page's download.\n */\n\n/**\n * What a source answers for one key.\n *\n * - `loading` — no answer yet.\n * - `missing` — the key names nothing this site may use. The published page\n * renders the element once, as written.\n * - `error` — the read failed. Nothing is known about the rows.\n * - `ready` — the rows, read and bounded exactly as the published page's\n * composition reads them.\n */\nexport type RepeatRowsAnswer =\n | { status: 'loading' }\n | { status: 'missing' }\n | { status: 'error' }\n | {\n status: 'ready'\n /** The name authors know the entry by, for the canvas badge. */\n label: string\n /**\n * The rows under every key the expansion looks them up by: the key as\n * the node stores it, the entry's id, and each entry a one-hop reference\n * in the rows reads (`{{item.ref.field}}`).\n */\n rowsByKey: Readonly<Record<string, RepeatableDataset>>\n }\n\n/** One read a source is asked for. */\nexport interface RepeatRowsRequest {\n /** The site the canvas edits. A source answers only what it may show. */\n hostId: string\n /** The key exactly as the node stores it. */\n key: string\n}\n\n/** One kind of thing an element can repeat over. */\nexport interface RepeatSource {\n /** Stable id, e.g. the plugin's name for the kind. */\n id: string\n /** What the Attributes panel calls the kind. */\n label: string\n /**\n * The node prop the chosen key persists under. Persisted in documents, so a\n * source never renames it.\n */\n keyProp: string\n /**\n * The attribute that picks a key, drawn by the Attributes panel as any\n * component attribute is. The panel names the field after `keyProp`.\n */\n keyAttribute: Omit<AglynAttributeSchema, 'name'>\n /**\n * The entity picker kind a key names, when a key is an entity the picker\n * lists. With it, the insert-token menu inside the repeat names the entity\n * through the picker and offers `{{item.<field>}}` for each field the\n * entity declares (`EntityPickerContextValue.entityFields`). Without it the\n * menu offers no item tokens and authors type them.\n */\n entityKind?: EntityPickerKind\n /**\n * Reads one key's rows for the besigner's canvas preview and badge. A React\n * hook: the editor mounts one reader per key and calls it unconditionally,\n * so its identity must not change after registration.\n */\n useRows(request: RepeatRowsRequest): RepeatRowsAnswer\n}\n\nconst sources = new Map<string, RepeatSource>()\nconst listeners = new Set<() => void>()\nlet version = 0\n/**\n * Replaced only when the registered set changes, never rebuilt per call: it is\n * the snapshot `useSyncExternalStore` compares, and a fresh array every time\n * is a render loop rather than a wasted allocation.\n */\nlet snapshot: readonly RepeatSource[] = []\n\nconst notify = () => {\n version += 1\n snapshot = [...sources.values()]\n for (const listener of [...listeners]) listener()\n}\n\n/**\n * Registers a source, replacing any registered under its id. Returns the\n * unregister, which removes the source only if it is still the one registered.\n *\n * Call it from a registration function the plugin loader calls BY NAME, never\n * from a module's top level: a bundler deletes a module imported only for its\n * side effect when its package says it has none (AGL-3025).\n */\nexport function registerRepeatSource(source: RepeatSource): () => void {\n if (sources.get(source.id) !== source) {\n sources.set(source.id, source)\n notify()\n }\n return () => {\n if (sources.get(source.id) !== source) return\n sources.delete(source.id)\n notify()\n }\n}\n\n/**\n * Every registered source, in registration order. The SAME array until the\n * registered set changes, so it can be read as a store snapshot.\n */\nexport function listRepeatSources(): readonly RepeatSource[] {\n return snapshot\n}\n\n/** The source registered under `id`. */\nexport function getRepeatSource(id: string): RepeatSource | undefined {\n return sources.get(id)\n}\n\n/** Called after the registered set changes. Returns the unsubscribe. */\nexport function subscribeRepeatSources(listener: () => void): () => void {\n listeners.add(listener)\n return () => {\n listeners.delete(listener)\n }\n}\n\n/** Moves on every change — the snapshot `useSyncExternalStore` reads. */\nexport function getRepeatSourcesVersion(): number {\n return version\n}\n\n/**\n * The registered source a node repeats over, and the key it names, trimmed.\n * `undefined` for a node that repeats over nothing a registered source owns.\n */\nexport function repeatSourceOf(\n node: unknown,\n): { source: RepeatSource; key: string } | undefined {\n const props = (node as { props?: Record<string, unknown> } | undefined)?.props\n if (!props) return undefined\n for (const source of sources.values()) {\n const value = props[source.keyProp]\n const key = typeof value === 'string' ? value.trim() : ''\n if (key) return { source, key }\n }\n return undefined\n}\n"],"names":["sources","Map","listeners","Set","version","snapshot","notify","values","listener","registerRepeatSource","source","get","id","set","delete","listRepeatSources","getRepeatSource","subscribeRepeatSources","add","getRepeatSourcesVersion","repeatSourceOf","node","props","undefined","value","keyProp","key","trim"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAqFD,MAAMA,UAAU,IAAIC;AACpB,MAAMC,YAAY,IAAIC;AACtB,IAAIC,UAAU;AACd;;;;CAIC,GACD,IAAIC,WAAoC,EAAE;AAE1C,MAAMC,SAAS;IACbF,WAAW;IACXC,WAAW;WAAIL,QAAQO,MAAM;KAAG;IAChC,KAAK,MAAMC,YAAY;WAAIN;KAAU,CAAEM;AACzC;AAEA;;;;;;;CAOC,GACD,OAAO,SAASC,qBAAqBC,MAAoB;IACvD,IAAIV,QAAQW,GAAG,CAACD,OAAOE,EAAE,MAAMF,QAAQ;QACrCV,QAAQa,GAAG,CAACH,OAAOE,EAAE,EAAEF;QACvBJ;IACF;IACA,OAAO;QACL,IAAIN,QAAQW,GAAG,CAACD,OAAOE,EAAE,MAAMF,QAAQ;QACvCV,QAAQc,MAAM,CAACJ,OAAOE,EAAE;QACxBN;IACF;AACF;AAEA;;;CAGC,GACD,OAAO,SAASS;IACd,OAAOV;AACT;AAEA,sCAAsC,GACtC,OAAO,SAASW,gBAAgBJ,EAAU;IACxC,OAAOZ,QAAQW,GAAG,CAACC;AACrB;AAEA,sEAAsE,GACtE,OAAO,SAASK,uBAAuBT,QAAoB;IACzDN,UAAUgB,GAAG,CAACV;IACd,OAAO;QACLN,UAAUY,MAAM,CAACN;IACnB;AACF;AAEA,uEAAuE,GACvE,OAAO,SAASW;IACd,OAAOf;AACT;AAEA;;;CAGC,GACD,OAAO,SAASgB,eACdC,IAAa;IAEb,MAAMC,QAASD,wBAAD,AAACA,KAA0DC,KAAK;IAC9E,IAAI,CAACA,OAAO,OAAOC;IACnB,KAAK,MAAMb,UAAUV,QAAQO,MAAM,GAAI;QACrC,MAAMiB,QAAQF,KAAK,CAACZ,OAAOe,OAAO,CAAC;QACnC,MAAMC,MAAM,OAAOF,UAAU,WAAWA,MAAMG,IAAI,KAAK;QACvD,IAAID,KAAK,OAAO;YAAEhB;YAAQgB;QAAI;IAChC;IACA,OAAOH;AACT"}
|
|
@@ -0,0 +1,73 @@
|
|
|
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
|
+
/** `orgs/{orgId}/retainedRefusals/{personKey}` — server-only, closed to every client. */
|
|
18
|
+
export declare const RETAINED_REFUSALS_COLLECTION = "retainedRefusals";
|
|
19
|
+
/** When a refusal was retained, epoch ms: on each entry and on the document. */
|
|
20
|
+
export declare const RETAINED_AT_FIELD = "retainedAtMs";
|
|
21
|
+
/** The record an entry was retained from, on each entry. */
|
|
22
|
+
export declare const RETAINED_FROM_RECORD_FIELD = "retainedFromContactId";
|
|
23
|
+
/** Every refusal one person's record holds. */
|
|
24
|
+
export interface RetainedRefusals {
|
|
25
|
+
/** Each site's refusal entry, keyed by the site, as the record stored it. */
|
|
26
|
+
byHost: Record<string, Record<string, unknown>>;
|
|
27
|
+
/** The top-level refusal, which names no site and is honored against every one. */
|
|
28
|
+
unscoped: boolean;
|
|
29
|
+
}
|
|
30
|
+
type Doc = Record<string, unknown>;
|
|
31
|
+
/**
|
|
32
|
+
* Every refusal `record` holds, or `null` when it holds none.
|
|
33
|
+
*
|
|
34
|
+
* Every SITE, not the sites of one group: a record can carry a refusal for a
|
|
35
|
+
* site that let it go earlier, or one a consent group change carried onto
|
|
36
|
+
* it, and each is still somebody's "no". Reads a retained document as well
|
|
37
|
+
* as a person record, since the two share their field names.
|
|
38
|
+
*/
|
|
39
|
+
export declare function refusalsOf(record: Doc | null | undefined): RetainedRefusals | null;
|
|
40
|
+
/**
|
|
41
|
+
* `record` with what was retained for the same person laid over it — the
|
|
42
|
+
* record a reader of the stored basis should read.
|
|
43
|
+
*
|
|
44
|
+
* Per retained site, the record's entry stands only when it is a GRANT whose
|
|
45
|
+
* date is strictly after the refusal's (the person's own date, or when it was
|
|
46
|
+
* retained where they gave none). Anything else — no entry, a refusal of its
|
|
47
|
+
* own, an older or undated grant — reads as the retained refusal. The
|
|
48
|
+
* unscoped refusal is laid on only where the record gives no top-level
|
|
49
|
+
* answer of its own.
|
|
50
|
+
*
|
|
51
|
+
* With nothing retained the record comes back as it was, `null` included, so
|
|
52
|
+
* a person with no retained refusal reads exactly as they did before this
|
|
53
|
+
* store existed.
|
|
54
|
+
*/
|
|
55
|
+
export declare function withRetainedRefusals(record: Doc | null | undefined, retained: RetainedRefusals | null | undefined): Doc | null;
|
|
56
|
+
/**
|
|
57
|
+
* The entries one retention writes: each refusal stamped with when it was
|
|
58
|
+
* retained and from which record, keyed by site.
|
|
59
|
+
*/
|
|
60
|
+
export declare function retainedEntries(retained: RetainedRefusals, recordId: string, nowMs: number): Record<string, Doc>;
|
|
61
|
+
/**
|
|
62
|
+
* One retained document's share of a consent group change's carry `to ← from`
|
|
63
|
+
* — the dotted-path patch that gives `to` the refusal `from` holds, or `null`
|
|
64
|
+
* when `from` holds none or `to` already has an entry.
|
|
65
|
+
*
|
|
66
|
+
* Never overwrites: every entry in this store is a refusal, so an entry on
|
|
67
|
+
* `to` is already the answer, and a re-run after a crash writes nothing.
|
|
68
|
+
*/
|
|
69
|
+
export declare function retainedRefusalCarry(retained: Doc | null | undefined, carry: {
|
|
70
|
+
toHostId: string;
|
|
71
|
+
fromHostId: string;
|
|
72
|
+
}, changeId: string): Doc | null;
|
|
73
|
+
export {};
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ /**
|
|
18
|
+
* RETAINED REFUSALS — a person's "no" outlives the record it was written on
|
|
19
|
+
* (AGL-3338).
|
|
20
|
+
*
|
|
21
|
+
* ## Where a refusal lives, and why that is not enough
|
|
22
|
+
*
|
|
23
|
+
* A person's refusal of marketing email is stored on the record an operator
|
|
24
|
+
* keeps about them: an entry under {@link MARKETING_CONSENT_BY_HOST_FIELD}
|
|
25
|
+
* for the site they refused, or the top-level field for a refusal that names
|
|
26
|
+
* no site. The list gate reads it there. An operator adding people to a list
|
|
27
|
+
* by hand or by import states that they have permission to email them, and a
|
|
28
|
+
* stored refusal is the one fact that attestation cannot overrule: the
|
|
29
|
+
* person said no, and no operator's word turns that into a yes.
|
|
30
|
+
*
|
|
31
|
+
* The record, though, is the operator's, and the operator may delete it. A
|
|
32
|
+
* deleted record is an address with no record at all, which reads as
|
|
33
|
+
* `unrecorded` — nothing known either way — and the attestation is accepted.
|
|
34
|
+
* Deleting somebody from a CRM would then be the way to mail a person who
|
|
35
|
+
* refused, which is the one outcome the refusal exists to prevent.
|
|
36
|
+
*
|
|
37
|
+
* So a refusal is not the record's to lose. When a holder lets a shared
|
|
38
|
+
* record go, its refusal entries stay on the document for the holders that
|
|
39
|
+
* remain. When the last holder lets go, every refusal the document holds —
|
|
40
|
+
* every site's, not only the deleting holder's, and the unscoped one — is
|
|
41
|
+
* copied into {@link RETAINED_REFUSALS_COLLECTION} in the same transaction
|
|
42
|
+
* that deletes it, and the list gate reads that store beside the records.
|
|
43
|
+
*
|
|
44
|
+
* ## No address
|
|
45
|
+
*
|
|
46
|
+
* One document per person, keyed by `personKey` — the sha256 of the
|
|
47
|
+
* normalized address — and holding no address, as a suppression row keyed
|
|
48
|
+
* the same way holds none. The store answers one question about an address
|
|
49
|
+
* somebody already has in hand, "did this person refuse", and it has no
|
|
50
|
+
* business being a list of the people who did.
|
|
51
|
+
*
|
|
52
|
+
* ## The same field names as the record
|
|
53
|
+
*
|
|
54
|
+
* An entry keeps the record's own names, so a record merged with what was
|
|
55
|
+
* retained ({@link withRetainedRefusals}) is read by `readMarketingBasis`
|
|
56
|
+
* like any other — a second set of names would be a second parser for the
|
|
57
|
+
* two to disagree through. Each entry adds when it was retained and from
|
|
58
|
+
* which record.
|
|
59
|
+
*
|
|
60
|
+
* ## A newer grant wins, and nothing else does
|
|
61
|
+
*
|
|
62
|
+
* The person may come back. Signing up again on a form that asks writes a
|
|
63
|
+
* grant, and a grant given after the refusal is the person changing their
|
|
64
|
+
* mind — the merge lets it stand. A grant that is not strictly newer, a grant
|
|
65
|
+
* with no date, and every entry that is not a grant read as the refusal
|
|
66
|
+
* still: withholding mail is recoverable, and sending it is not.
|
|
67
|
+
*
|
|
68
|
+
* ## Carried like the refusals it came from
|
|
69
|
+
*
|
|
70
|
+
* The list gate reads a refusal across the CURRENT consent group, so two
|
|
71
|
+
* sites separating would stop one seeing the other's retained refusals. A
|
|
72
|
+
* consent group change therefore carries this store as it carries the site
|
|
73
|
+
* suppression lists and a record's own refusals: {@link retainedRefusalCarry}
|
|
74
|
+
* decides one document, and the executor pages the store.
|
|
75
|
+
*/ import { MARKETING_CONSENT_BY_HOST_FIELD, MARKETING_CONSENT_FIELD } from "./marketing-consent.js";
|
|
76
|
+
/** `orgs/{orgId}/retainedRefusals/{personKey}` — server-only, closed to every client. */ export const RETAINED_REFUSALS_COLLECTION = 'retainedRefusals';
|
|
77
|
+
/** When a refusal was retained, epoch ms: on each entry and on the document. */ export const RETAINED_AT_FIELD = 'retainedAtMs';
|
|
78
|
+
/** The record an entry was retained from, on each entry. */ export const RETAINED_FROM_RECORD_FIELD = 'retainedFromContactId';
|
|
79
|
+
/** When the person gave the answer an entry records, as every writer stamps it. */ const GIVEN_AT_FIELD = 'marketingConsentAtMs';
|
|
80
|
+
function mapOf(value) {
|
|
81
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
82
|
+
}
|
|
83
|
+
/** Epoch millis off a number, a `Date` or either SDK's `Timestamp`, else `null`. */ function millisOf(value) {
|
|
84
|
+
if (typeof value === 'number') return Number.isFinite(value) ? value : null;
|
|
85
|
+
if (value instanceof Date) return value.getTime();
|
|
86
|
+
const toMillis = value == null ? void 0 : value.toMillis;
|
|
87
|
+
if (typeof toMillis === 'function') {
|
|
88
|
+
const millis = Number(toMillis.call(value));
|
|
89
|
+
return Number.isFinite(millis) ? millis : null;
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Every refusal `record` holds, or `null` when it holds none.
|
|
95
|
+
*
|
|
96
|
+
* Every SITE, not the sites of one group: a record can carry a refusal for a
|
|
97
|
+
* site that let it go earlier, or one a consent group change carried onto
|
|
98
|
+
* it, and each is still somebody's "no". Reads a retained document as well
|
|
99
|
+
* as a person record, since the two share their field names.
|
|
100
|
+
*/ export function refusalsOf(record) {
|
|
101
|
+
var _mapOf;
|
|
102
|
+
if (!record) return null;
|
|
103
|
+
const byHost = {};
|
|
104
|
+
for (const [hostId, value] of Object.entries((_mapOf = mapOf(record[MARKETING_CONSENT_BY_HOST_FIELD])) != null ? _mapOf : {})){
|
|
105
|
+
const entry = mapOf(value);
|
|
106
|
+
if (hostId && (entry == null ? void 0 : entry[MARKETING_CONSENT_FIELD]) === false) byHost[hostId] = _extends({}, entry);
|
|
107
|
+
}
|
|
108
|
+
const unscoped = record[MARKETING_CONSENT_FIELD] === false;
|
|
109
|
+
return unscoped || Object.keys(byHost).length ? {
|
|
110
|
+
byHost,
|
|
111
|
+
unscoped
|
|
112
|
+
} : null;
|
|
113
|
+
}
|
|
114
|
+
/** When a retained refusal counts as given: the person's own date, else the retention's. */ function refusedAtMs(refusal) {
|
|
115
|
+
var _millisOf;
|
|
116
|
+
return (_millisOf = millisOf(refusal[GIVEN_AT_FIELD])) != null ? _millisOf : millisOf(refusal[RETAINED_AT_FIELD]);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* `record` with what was retained for the same person laid over it — the
|
|
120
|
+
* record a reader of the stored basis should read.
|
|
121
|
+
*
|
|
122
|
+
* Per retained site, the record's entry stands only when it is a GRANT whose
|
|
123
|
+
* date is strictly after the refusal's (the person's own date, or when it was
|
|
124
|
+
* retained where they gave none). Anything else — no entry, a refusal of its
|
|
125
|
+
* own, an older or undated grant — reads as the retained refusal. The
|
|
126
|
+
* unscoped refusal is laid on only where the record gives no top-level
|
|
127
|
+
* answer of its own.
|
|
128
|
+
*
|
|
129
|
+
* With nothing retained the record comes back as it was, `null` included, so
|
|
130
|
+
* a person with no retained refusal reads exactly as they did before this
|
|
131
|
+
* store existed.
|
|
132
|
+
*/ export function withRetainedRefusals(record, retained) {
|
|
133
|
+
var _ref, _mapOf;
|
|
134
|
+
const hosts = Object.entries((_ref = retained == null ? void 0 : retained.byHost) != null ? _ref : {});
|
|
135
|
+
if (!retained || !hosts.length && !retained.unscoped) return record != null ? record : null;
|
|
136
|
+
const base = record != null ? record : {};
|
|
137
|
+
const byHost = _extends({}, (_mapOf = mapOf(base[MARKETING_CONSENT_BY_HOST_FIELD])) != null ? _mapOf : {});
|
|
138
|
+
for (const [hostId, refusal] of hosts){
|
|
139
|
+
const entry = mapOf(byHost[hostId]);
|
|
140
|
+
const grantedAt = (entry == null ? void 0 : entry[MARKETING_CONSENT_FIELD]) === true ? millisOf(entry[GIVEN_AT_FIELD]) : null;
|
|
141
|
+
const refusedAt = refusedAtMs(refusal);
|
|
142
|
+
const newerGrant = grantedAt !== null && refusedAt !== null && grantedAt > refusedAt;
|
|
143
|
+
if (!newerGrant) byHost[hostId] = refusal;
|
|
144
|
+
}
|
|
145
|
+
const merged = _extends({}, base, {
|
|
146
|
+
[MARKETING_CONSENT_BY_HOST_FIELD]: byHost
|
|
147
|
+
});
|
|
148
|
+
if (retained.unscoped && typeof base[MARKETING_CONSENT_FIELD] !== 'boolean') {
|
|
149
|
+
merged[MARKETING_CONSENT_FIELD] = false;
|
|
150
|
+
}
|
|
151
|
+
return merged;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* The entries one retention writes: each refusal stamped with when it was
|
|
155
|
+
* retained and from which record, keyed by site.
|
|
156
|
+
*/ export function retainedEntries(retained, recordId, nowMs) {
|
|
157
|
+
return Object.fromEntries(Object.entries(retained.byHost).map(([hostId, refusal])=>[
|
|
158
|
+
hostId,
|
|
159
|
+
_extends({}, refusal, {
|
|
160
|
+
[RETAINED_AT_FIELD]: nowMs,
|
|
161
|
+
[RETAINED_FROM_RECORD_FIELD]: recordId
|
|
162
|
+
})
|
|
163
|
+
]));
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* One retained document's share of a consent group change's carry `to ← from`
|
|
167
|
+
* — the dotted-path patch that gives `to` the refusal `from` holds, or `null`
|
|
168
|
+
* when `from` holds none or `to` already has an entry.
|
|
169
|
+
*
|
|
170
|
+
* Never overwrites: every entry in this store is a refusal, so an entry on
|
|
171
|
+
* `to` is already the answer, and a re-run after a crash writes nothing.
|
|
172
|
+
*/ export function retainedRefusalCarry(retained, carry, changeId) {
|
|
173
|
+
var _mapOf;
|
|
174
|
+
const byHost = (_mapOf = mapOf(retained == null ? void 0 : retained[MARKETING_CONSENT_BY_HOST_FIELD])) != null ? _mapOf : {};
|
|
175
|
+
const source = mapOf(byHost[carry.fromHostId]);
|
|
176
|
+
if ((source == null ? void 0 : source[MARKETING_CONSENT_FIELD]) !== false) return null;
|
|
177
|
+
if (byHost[carry.toHostId] !== undefined) return null;
|
|
178
|
+
return {
|
|
179
|
+
[`${MARKETING_CONSENT_BY_HOST_FIELD}.${carry.toHostId}`]: _extends({}, source, {
|
|
180
|
+
carriedFromHostId: carry.fromHostId,
|
|
181
|
+
carriedByChangeId: changeId
|
|
182
|
+
})
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
//# sourceMappingURL=retained-refusals.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/retained-refusals.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 * RETAINED REFUSALS — a person's \"no\" outlives the record it was written on\n * (AGL-3338).\n *\n * ## Where a refusal lives, and why that is not enough\n *\n * A person's refusal of marketing email is stored on the record an operator\n * keeps about them: an entry under {@link MARKETING_CONSENT_BY_HOST_FIELD}\n * for the site they refused, or the top-level field for a refusal that names\n * no site. The list gate reads it there. An operator adding people to a list\n * by hand or by import states that they have permission to email them, and a\n * stored refusal is the one fact that attestation cannot overrule: the\n * person said no, and no operator's word turns that into a yes.\n *\n * The record, though, is the operator's, and the operator may delete it. A\n * deleted record is an address with no record at all, which reads as\n * `unrecorded` — nothing known either way — and the attestation is accepted.\n * Deleting somebody from a CRM would then be the way to mail a person who\n * refused, which is the one outcome the refusal exists to prevent.\n *\n * So a refusal is not the record's to lose. When a holder lets a shared\n * record go, its refusal entries stay on the document for the holders that\n * remain. When the last holder lets go, every refusal the document holds —\n * every site's, not only the deleting holder's, and the unscoped one — is\n * copied into {@link RETAINED_REFUSALS_COLLECTION} in the same transaction\n * that deletes it, and the list gate reads that store beside the records.\n *\n * ## No address\n *\n * One document per person, keyed by `personKey` — the sha256 of the\n * normalized address — and holding no address, as a suppression row keyed\n * the same way holds none. The store answers one question about an address\n * somebody already has in hand, \"did this person refuse\", and it has no\n * business being a list of the people who did.\n *\n * ## The same field names as the record\n *\n * An entry keeps the record's own names, so a record merged with what was\n * retained ({@link withRetainedRefusals}) is read by `readMarketingBasis`\n * like any other — a second set of names would be a second parser for the\n * two to disagree through. Each entry adds when it was retained and from\n * which record.\n *\n * ## A newer grant wins, and nothing else does\n *\n * The person may come back. Signing up again on a form that asks writes a\n * grant, and a grant given after the refusal is the person changing their\n * mind — the merge lets it stand. A grant that is not strictly newer, a grant\n * with no date, and every entry that is not a grant read as the refusal\n * still: withholding mail is recoverable, and sending it is not.\n *\n * ## Carried like the refusals it came from\n *\n * The list gate reads a refusal across the CURRENT consent group, so two\n * sites separating would stop one seeing the other's retained refusals. A\n * consent group change therefore carries this store as it carries the site\n * suppression lists and a record's own refusals: {@link retainedRefusalCarry}\n * decides one document, and the executor pages the store.\n */\n\nimport {\n MARKETING_CONSENT_BY_HOST_FIELD,\n MARKETING_CONSENT_FIELD,\n} from './marketing-consent'\n\n/** `orgs/{orgId}/retainedRefusals/{personKey}` — server-only, closed to every client. */\nexport const RETAINED_REFUSALS_COLLECTION = 'retainedRefusals'\n\n/** When a refusal was retained, epoch ms: on each entry and on the document. */\nexport const RETAINED_AT_FIELD = 'retainedAtMs'\n\n/** The record an entry was retained from, on each entry. */\nexport const RETAINED_FROM_RECORD_FIELD = 'retainedFromContactId'\n\n/** When the person gave the answer an entry records, as every writer stamps it. */\nconst GIVEN_AT_FIELD = 'marketingConsentAtMs'\n\n/** Every refusal one person's record holds. */\nexport interface RetainedRefusals {\n /** Each site's refusal entry, keyed by the site, as the record stored it. */\n byHost: Record<string, Record<string, unknown>>\n /** The top-level refusal, which names no site and is honored against every one. */\n unscoped: boolean\n}\n\ntype Doc = Record<string, unknown>\n\nfunction mapOf(value: unknown): Doc | null {\n return value && typeof value === 'object' && !Array.isArray(value) ? (value as Doc) : null\n}\n\n/** Epoch millis off a number, a `Date` or either SDK's `Timestamp`, else `null`. */\nfunction millisOf(value: unknown): number | null {\n if (typeof value === 'number') return Number.isFinite(value) ? value : null\n if (value instanceof Date) return value.getTime()\n const toMillis = (value as { toMillis?: unknown } | null)?.toMillis\n if (typeof toMillis === 'function') {\n const millis = Number(toMillis.call(value))\n return Number.isFinite(millis) ? millis : null\n }\n return null\n}\n\n/**\n * Every refusal `record` holds, or `null` when it holds none.\n *\n * Every SITE, not the sites of one group: a record can carry a refusal for a\n * site that let it go earlier, or one a consent group change carried onto\n * it, and each is still somebody's \"no\". Reads a retained document as well\n * as a person record, since the two share their field names.\n */\nexport function refusalsOf(record: Doc | null | undefined): RetainedRefusals | null {\n if (!record) return null\n const byHost: Record<string, Doc> = {}\n for (const [hostId, value] of Object.entries(mapOf(record[MARKETING_CONSENT_BY_HOST_FIELD]) ?? {})) {\n const entry = mapOf(value)\n if (hostId && entry?.[MARKETING_CONSENT_FIELD] === false) byHost[hostId] = { ...entry }\n }\n const unscoped = record[MARKETING_CONSENT_FIELD] === false\n return unscoped || Object.keys(byHost).length ? { byHost, unscoped } : null\n}\n\n/** When a retained refusal counts as given: the person's own date, else the retention's. */\nfunction refusedAtMs(refusal: Doc): number | null {\n return millisOf(refusal[GIVEN_AT_FIELD]) ?? millisOf(refusal[RETAINED_AT_FIELD])\n}\n\n/**\n * `record` with what was retained for the same person laid over it — the\n * record a reader of the stored basis should read.\n *\n * Per retained site, the record's entry stands only when it is a GRANT whose\n * date is strictly after the refusal's (the person's own date, or when it was\n * retained where they gave none). Anything else — no entry, a refusal of its\n * own, an older or undated grant — reads as the retained refusal. The\n * unscoped refusal is laid on only where the record gives no top-level\n * answer of its own.\n *\n * With nothing retained the record comes back as it was, `null` included, so\n * a person with no retained refusal reads exactly as they did before this\n * store existed.\n */\nexport function withRetainedRefusals(\n record: Doc | null | undefined,\n retained: RetainedRefusals | null | undefined,\n): Doc | null {\n const hosts = Object.entries(retained?.byHost ?? {})\n if (!retained || (!hosts.length && !retained.unscoped)) return record ?? null\n const base = record ?? {}\n const byHost: Doc = { ...(mapOf(base[MARKETING_CONSENT_BY_HOST_FIELD]) ?? {}) }\n for (const [hostId, refusal] of hosts) {\n const entry = mapOf(byHost[hostId])\n const grantedAt = entry?.[MARKETING_CONSENT_FIELD] === true ? millisOf(entry[GIVEN_AT_FIELD]) : null\n const refusedAt = refusedAtMs(refusal)\n const newerGrant = grantedAt !== null && refusedAt !== null && grantedAt > refusedAt\n if (!newerGrant) byHost[hostId] = refusal\n }\n const merged: Doc = { ...base, [MARKETING_CONSENT_BY_HOST_FIELD]: byHost }\n if (retained.unscoped && typeof base[MARKETING_CONSENT_FIELD] !== 'boolean') {\n merged[MARKETING_CONSENT_FIELD] = false\n }\n return merged\n}\n\n/**\n * The entries one retention writes: each refusal stamped with when it was\n * retained and from which record, keyed by site.\n */\nexport function retainedEntries(\n retained: RetainedRefusals,\n recordId: string,\n nowMs: number,\n): Record<string, Doc> {\n return Object.fromEntries(\n Object.entries(retained.byHost).map(([hostId, refusal]) => [\n hostId,\n { ...refusal, [RETAINED_AT_FIELD]: nowMs, [RETAINED_FROM_RECORD_FIELD]: recordId },\n ]),\n )\n}\n\n/**\n * One retained document's share of a consent group change's carry `to ← from`\n * — the dotted-path patch that gives `to` the refusal `from` holds, or `null`\n * when `from` holds none or `to` already has an entry.\n *\n * Never overwrites: every entry in this store is a refusal, so an entry on\n * `to` is already the answer, and a re-run after a crash writes nothing.\n */\nexport function retainedRefusalCarry(\n retained: Doc | null | undefined,\n carry: { toHostId: string; fromHostId: string },\n changeId: string,\n): Doc | null {\n const byHost = mapOf(retained?.[MARKETING_CONSENT_BY_HOST_FIELD]) ?? {}\n const source = mapOf(byHost[carry.fromHostId])\n if (source?.[MARKETING_CONSENT_FIELD] !== false) return null\n if (byHost[carry.toHostId] !== undefined) return null\n return {\n [`${MARKETING_CONSENT_BY_HOST_FIELD}.${carry.toHostId}`]: {\n ...source,\n carriedFromHostId: carry.fromHostId,\n carriedByChangeId: changeId,\n },\n }\n}\n"],"names":["MARKETING_CONSENT_BY_HOST_FIELD","MARKETING_CONSENT_FIELD","RETAINED_REFUSALS_COLLECTION","RETAINED_AT_FIELD","RETAINED_FROM_RECORD_FIELD","GIVEN_AT_FIELD","mapOf","value","Array","isArray","millisOf","Number","isFinite","Date","getTime","toMillis","millis","call","refusalsOf","record","byHost","hostId","Object","entries","entry","unscoped","keys","length","refusedAtMs","refusal","withRetainedRefusals","retained","hosts","base","grantedAt","refusedAt","newerGrant","merged","retainedEntries","recordId","nowMs","fromEntries","map","retainedRefusalCarry","carry","changeId","source","fromHostId","toHostId","undefined","carriedFromHostId","carriedByChangeId"],"mappings":"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"}
|
|
@@ -247,3 +247,30 @@ export declare function scopeCovers(target: readonly string[] | undefined | null
|
|
|
247
247
|
* org CDN URL. That keeps it safe to apply blindly at every pick site.
|
|
248
248
|
*/
|
|
249
249
|
export declare function hostQualifiedCdnPath(cdnPath: string | undefined | null, visibleTo: readonly string[] | undefined | null, hostId: string | undefined | null): string | undefined;
|
|
250
|
+
/** The map a scoped document records its visibility grants under. */
|
|
251
|
+
export declare const SCOPE_GRANTS_FIELD = "sharing";
|
|
252
|
+
/** The tokens a grant ADDED to `visibleTo`: seen through, never held. */
|
|
253
|
+
export declare function grantedScopeTokens(doc: Readonly<Record<string, unknown>> | null | undefined): string[];
|
|
254
|
+
/**
|
|
255
|
+
* The tokens that HOLD a document: `visibleTo` without the ones a grant
|
|
256
|
+
* added. Missing `visibleTo` is held by nobody, as it is seen by nobody.
|
|
257
|
+
*
|
|
258
|
+
* A site that created the document (`hostId`) or captured the person on it
|
|
259
|
+
* (`capturedByHostIds`) holds its token whatever the grants say: a record
|
|
260
|
+
* shared with a site that then captures the person itself is that site's
|
|
261
|
+
* own from the capture on, before anything re-evaluates the grants.
|
|
262
|
+
*/
|
|
263
|
+
export declare function heldScopeTokens(doc: Readonly<Record<string, unknown>> | null | undefined): string[];
|
|
264
|
+
/**
|
|
265
|
+
* Whether a site HOLDS a document rather than only seeing it through a
|
|
266
|
+
* grant. The question a marketing audience, a list rule, a sequence and a
|
|
267
|
+
* holder count ask; {@link visibleToHost} is the one a reader asks.
|
|
268
|
+
*/
|
|
269
|
+
export declare function heldByHost(doc: Readonly<Record<string, unknown>> | null | undefined, hostId: string): boolean;
|
|
270
|
+
/**
|
|
271
|
+
* Whether a site sees a document ONLY through a grant: it carries grants and
|
|
272
|
+
* the site does not hold it. The filter an audience sweep applies to what
|
|
273
|
+
* its scoped query returned — a document with no grants answers `false`, so
|
|
274
|
+
* every read that predates sharing keeps exactly the rows it had.
|
|
275
|
+
*/
|
|
276
|
+
export declare function seenOnlyThroughGrant(doc: Readonly<Record<string, unknown>> | null | undefined, hostId: string): boolean;
|