@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
|
@@ -69,6 +69,16 @@ export declare const PLATFORM_BRAND_LEGAL_NAME: string;
|
|
|
69
69
|
* console at all.
|
|
70
70
|
*/
|
|
71
71
|
export declare const PLATFORM_SUPPORT_URL: string;
|
|
72
|
+
/**
|
|
73
|
+
* This install's console origin, with no trailing slash — what a link in a
|
|
74
|
+
* notification or an email the platform sends is built on (AGL-3080).
|
|
75
|
+
*
|
|
76
|
+
* `NEXT_PUBLIC_CONSOLE_URL`, which every self-hosted install sets; Aglyn's
|
|
77
|
+
* own console only for a build that names none. One reading for the support
|
|
78
|
+
* page above and for every scheduled sender, a plugin's included, so none of
|
|
79
|
+
* them spells the fallback again.
|
|
80
|
+
*/
|
|
81
|
+
export declare function platformConsoleOrigin(): string;
|
|
72
82
|
/** True when this deployment still answers to Aglyn's own brand. */
|
|
73
83
|
export declare const isAglynOperatedBrand: () => boolean;
|
|
74
84
|
/**
|
|
@@ -110,14 +110,25 @@ var _clean4;
|
|
|
110
110
|
* platform sends. `app.aglyn.com` is left only for a build that names no
|
|
111
111
|
* console at all.
|
|
112
112
|
*/ export const PLATFORM_SUPPORT_URL = (()=>{
|
|
113
|
-
var _clean;
|
|
114
113
|
const configured = clean(process.env.NEXT_PUBLIC_PLATFORM_SUPPORT_URL);
|
|
115
114
|
if (configured) return configured;
|
|
116
115
|
const operatorEmail = clean(process.env.NEXT_PUBLIC_OPERATOR_SUPPORT_EMAIL);
|
|
117
116
|
if (operatorEmail) return `mailto:${operatorEmail}`;
|
|
118
|
-
|
|
119
|
-
return `${consoleOrigin || 'https://app.aglyn.com'}/support`;
|
|
117
|
+
return `${platformConsoleOrigin()}/support`;
|
|
120
118
|
})();
|
|
119
|
+
/**
|
|
120
|
+
* This install's console origin, with no trailing slash — what a link in a
|
|
121
|
+
* notification or an email the platform sends is built on (AGL-3080).
|
|
122
|
+
*
|
|
123
|
+
* `NEXT_PUBLIC_CONSOLE_URL`, which every self-hosted install sets; Aglyn's
|
|
124
|
+
* own console only for a build that names none. One reading for the support
|
|
125
|
+
* page above and for every scheduled sender, a plugin's included, so none of
|
|
126
|
+
* them spells the fallback again.
|
|
127
|
+
*/ export function platformConsoleOrigin() {
|
|
128
|
+
var _clean;
|
|
129
|
+
const configured = (_clean = clean(process.env.NEXT_PUBLIC_CONSOLE_URL)) == null ? void 0 : _clean.replace(/\/+$/, '');
|
|
130
|
+
return configured || 'https://app.aglyn.com';
|
|
131
|
+
}
|
|
121
132
|
/** True when this deployment still answers to Aglyn's own brand. */ export const isAglynOperatedBrand = ()=>PLATFORM_BRAND_NAME === 'Aglyn';
|
|
122
133
|
/**
|
|
123
134
|
* Where the platform's own front door is — the marketing home, not the help
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/platform-brand.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * What this deployment calls itself (AGL-2153).\n *\n * the rule for the self-host path: *\"the self hosted owner should not have\n * to edit the source to update aglyn branded items urls or personal\n * identification items, should move these to env vars\"*. AGL-2016 did that for\n * the values where getting it wrong misroutes a legal notice. This is the\n * other half — the product name itself, which was a bare `'Aglyn'` literal in\n * nine places.\n *\n * ## Why one module rather than nine reads\n *\n * The literals were not merely duplicated, they were duplicated across a\n * boundary: `PLATFORM_GENERATOR_NAME` exists here AND as a hand-copied\n * constant in `apps/tenant/middleware.ts`, because the edge bundle cannot\n * import this lib. A single source they both read is what stops the copy from\n * drifting into a deployment that half-renames itself.\n *\n * ## Why a default at all, when operator-identity.ts refuses to have one\n *\n * `operator-identity.ts` sets out the rule and it is the right one: a value\n * whose wrongness is invisible to the operator and visible only off-site must\n * not be guessed. The brand is the opposite case. An operator who does not set\n * it sees \"Aglyn\" in their own browser tab on their own first page load, in\n * their own console. That is a default they can see and correct, which is a\n * default rather than a trap — the same test AGL-2022 applied to the tenant\n * apex.\n *\n * ## Why `NEXT_PUBLIC_`, and why dot notation is load-bearing\n *\n * The brand is printed to the public — that is what it is for — so\n * `NEXT_PUBLIC_` carries no disclosure risk. Next inlines `NEXT_PUBLIC_*` into\n * the browser bundle by textually substituting `process.env.NAME`; the\n * **bracket** form is never substituted and reads `undefined` in the browser\n * (AGL-2037). Every read below is dot notation on purpose.\n *\n * Build-time consequence for self-hosters: these are baked in when the image\n * is built, so they must be set *before* `docker compose build`, not merely\n * before `up`. `docs/SELF_HOSTING.md` says so.\n */\n\n/** A non-empty trimmed string, else undefined — `' '` is a half-finished `.env`. */\nconst clean = (value: unknown): string | undefined => {\n const text = typeof value === 'string' ? value.trim() : ''\n return text.length ? text : undefined\n}\n\n/**\n * The product name, as shown to end users.\n *\n * This is the browser-tab affix, the PWA name, the WebAuthn relying-party name\n * the OS credential prompt displays, the `<meta name=\"generator\">` and\n * `x-powered-by` on published sites, and the platform default `productName` /\n * `fromName` every non-white-label org's surfaces resolve to.\n */\nexport const PLATFORM_BRAND_NAME =\n clean(process.env.NEXT_PUBLIC_PLATFORM_BRAND_NAME) ?? 'Aglyn'\n\n/**\n * The legal entity operating the software's own cloud, for copy that needs a\n * company rather than a product (`BRAND.ORG_NAME_LEGAL`).\n *\n * NOT the same thing as `operatorIdentity().name`, and the distinction matters.\n * This is a *brand* string with a visible default; that one is the identity a\n * statutory notice is addressed to and refuses to default. An operator who\n * sets only this has renamed the product, not published who they are.\n */\nexport const PLATFORM_BRAND_LEGAL_NAME =\n clean(process.env.NEXT_PUBLIC_PLATFORM_BRAND_LEGAL_NAME) ??\n `${PLATFORM_BRAND_NAME} LLC`\n\n/**\n * Where a \"need help?\" line points.\n *\n * Falls through the operator's OWN support address before it falls back to\n * ours, deliberately: an operator who followed the runbook has already set\n * `NEXT_PUBLIC_OPERATOR_SUPPORT_EMAIL`, and making them discover a second\n * variable to stop pointing their customers at Aglyn's support desk — which\n * cannot help those customers with anything — is a trap dressed as a default.\n *\n * Read here rather than imported from `operator-identity.ts` so this module\n * stays dependency-free and safe to import from anywhere, including the places\n * that cannot take the operator module's weight.\n *\n * ## Why the last fallback is the CONSOLE and not the marketing site\n *\n * It was `aglyn.com/support`, which answers 404 and has never existed\n * (AGL-3262) — MEASURED against the live site, not inferred: `/support`,\n * `/help` and `/docs` all render the not-found page and none is in the\n * sitemap. A support URL that 404s is worse than none, because it is printed\n * on a receipt beside a charge the reader is already unsure about.\n *\n * `app.aglyn.com/support` is a real page (AGL-3265): it resolves the\n * workspace from the session, so a customer with one lands on its support\n * umbrella and a customer with several picks. That is the whole reason it\n * exists — it is the one support address that needs nothing interpolated\n * into it, which is exactly what this constant is.\n *\n * The console is THIS install's (AGL-3322). `/support` is a console route, so\n * every console serves its own, and `NEXT_PUBLIC_CONSOLE_URL` is required of\n * every self-hosted install — so one that configured neither variable above\n * sends its customers to its own support page rather than to ours, where they\n * have no account. The line is in the footer of every system email the\n * platform sends. `app.aglyn.com` is left only for a build that names no\n * console at all.\n */\nexport const PLATFORM_SUPPORT_URL: string = (() => {\n const configured = clean(process.env.NEXT_PUBLIC_PLATFORM_SUPPORT_URL)\n if (configured) return configured\n const operatorEmail = clean(process.env.NEXT_PUBLIC_OPERATOR_SUPPORT_EMAIL)\n if (operatorEmail) return `mailto:${operatorEmail}`\n const consoleOrigin = clean(process.env.NEXT_PUBLIC_CONSOLE_URL)?.replace(\n /\\/+$/,\n '',\n )\n return `${consoleOrigin || 'https://app.aglyn.com'}/support`\n})()\n\n/** True when this deployment still answers to Aglyn's own brand. */\nexport const isAglynOperatedBrand = (): boolean =>\n PLATFORM_BRAND_NAME === 'Aglyn'\n\n/**\n * Where the platform's own front door is — the marketing home, not the help\n * desk.\n *\n * ## Why this is not `PLATFORM_SUPPORT_URL`\n *\n * The \"Made with …\" badge on a published free-tier site used to link to\n * `supportUrl`, and on this deployment `supportUrl` falls through to\n * `mailto:` + the operator support address. So the one link the platform gets\n * on every free customer's site — its only organic acquisition surface —\n * opened the visitor's mail client addressed to our support desk. A visitor\n * who liked the site and clicked to find out what built it got a blank email,\n * and support got the misdirected replies.\n *\n * They are genuinely different destinations. `supportUrl` answers \"I am a\n * customer and something is wrong\"; this answers \"what is this, and can I have\n * one\". Keeping one value for both guarantees one of them is wrong.\n *\n * ## Why it is null rather than aglyn.com off-brand\n *\n * A self-hosted deployment that renamed the product but set no home URL has\n * nowhere honest to send that visitor: \"Made with Foo\" pointing at aglyn.com\n * advertises the wrong company on Foo's customers' sites. The badge already\n * renders as a plain label when it has no destination (AGL-2428), which is the\n * correct outcome — so the Aglyn default applies only while this really is the\n * Aglyn-operated brand.\n */\nexport const PLATFORM_HOME_URL: string | null =\n clean(process.env.NEXT_PUBLIC_PLATFORM_HOME_URL) ??\n (isAglynOperatedBrand() ? 'https://aglyn.com' : null)\n\n/**\n * The platform's own logo MARK — the compact, square, no-wordmark form — as a\n * URL an `<img>` on a PUBLISHED SITE can load.\n *\n * Site-relative on purpose: every app that renders it serves\n * `public/_static/…` from its own origin, so one path works on\n * `{sub}.aglyn.app` and on a customer's custom domain alike without knowing\n * either. The white variant is the one that survives the badge's dark pill and\n * an org's `primaryColor` behind it.\n *\n * Null unless this is the Aglyn-operated brand, for the same reason as\n * {@link PLATFORM_HOME_URL}: a renamed deployment must not stamp our mark on\n * its customers' sites. Such an operator sets `NEXT_PUBLIC_PLATFORM_MARK_URL`\n * to their own, or gets a text-only badge.\n */\nexport const PLATFORM_MARK_URL: string | null =\n clean(process.env.NEXT_PUBLIC_PLATFORM_MARK_URL) ??\n (isAglynOperatedBrand()\n ? '/_static/images/brand/aglyn-logo-mark-white.svg'\n : null)\n\n/** An absolute `https://` URL, else undefined — anything else has no origin in an inbox. */\nconst httpsUrl = (value: string | undefined): string | undefined => {\n if (!value) return undefined\n try {\n return new URL(value).protocol === 'https:' ? value : undefined\n } catch {\n return undefined\n }\n}\n\n/**\n * The platform's logo as its own system email draws it: the header of mail\n * sent under the platform's brand (AGL-3322).\n *\n * ABSOLUTE and https, unlike {@link PLATFORM_MARK_URL}. That mark is a\n * site-relative path because every app that draws it serves the file from its\n * own origin; an inbox has no origin, so a relative path there is a\n * broken-image box. A configured value that is not an absolute `https://` URL\n * is ignored rather than handed on to fail in somebody's inbox.\n *\n * The WORDMARK rather than the square mark: a header is read at a glance, and\n * the wordmark names the sender where the mark would need the reader to\n * already know it.\n *\n * A PNG sized for the slot (AGL-3367): 396×120, three times the 132×40 the\n * header draws it at, about 15 KB, served as a static file off aglyn.com's\n * edge. The master-resolution file behind the media CDN's 60-second cache is\n * four times the bytes and makes the header the last thing in the email to\n * appear. Not an SVG: Gmail and Outlook do not draw SVG in mail, so every\n * recipient on either would get a broken-image box where the logo belongs.\n *\n * Null unless this is the Aglyn-operated brand, for the reason\n * {@link PLATFORM_HOME_URL} is: a renamed deployment must not put our logo at\n * the top of its customers' mail. Such an operator sets\n * `NEXT_PUBLIC_PLATFORM_EMAIL_LOGO_URL`, or gets its product name as the\n * header, in bold text.\n *\n * Deliberately NOT `PLATFORM_BRANDING_PROFILE.emailLogoUrl`, which stays null.\n * That field is an ORG's email logo: a white-label profile inherits it when\n * the org set none, and a send that carries one is read as an org's own mail.\n * Putting the platform's logo there would stamp it on every white-label org\n * that left the field blank.\n */\nexport const PLATFORM_EMAIL_LOGO_URL: string | null =\n httpsUrl(clean(process.env.NEXT_PUBLIC_PLATFORM_EMAIL_LOGO_URL)) ??\n (isAglynOperatedBrand()\n ? 'https://aglyn.com/_static/images/brand/aglyn-email-wordmark.png'\n : null)\n\n/**\n * The postal address printed beside {@link PLATFORM_BRAND_LEGAL_NAME} at the\n * foot of the platform's own system email (AGL-3322).\n *\n * A BRAND string, beside the legal name whose line it completes, rather than\n * a field of `operatorIdentity()`, and the difference is the default. The\n * operator identity is what a statutory notice is addressed to and refuses to\n * guess; this is a line in a footer the operator sees in their own mail and\n * can correct, and for the Aglyn-operated brand the right value is known: the\n * registered agent's address the Aglyn marketing site's email footer prints.\n *\n * Null unless this is the Aglyn-operated brand: a renamed deployment must\n * never print our address as its own. Such an operator sets\n * `NEXT_PUBLIC_PLATFORM_POSTAL_ADDRESS`, or their footer carries the legal\n * name alone. Line breaks in a configured value are folded into commas,\n * because the footer prints the address on one line.\n */\nexport const PLATFORM_POSTAL_ADDRESS: string | null =\n clean(process.env.NEXT_PUBLIC_PLATFORM_POSTAL_ADDRESS)\n ?.split(/\\s*\\n\\s*/)\n .filter(Boolean)\n .join(', ') ??\n (isAglynOperatedBrand()\n ? 'c/o Northwest Registered Agent, LLC, 5900 Balcones Drive STE 100, Austin, TX 78731'\n : null)\n\n/**\n * A branding profile with every field present — what a branded surface\n * renders. Image/color/domain fields are nullable (Aglyn's own surfaces\n * bake those in rather than carry a URL); the remaining text fields always\n * have a value so callers never string-concatenate `undefined`.\n *\n * `supportUrl` is nullable too, and it is the one field where null carries a\n * decision rather than an absence — see {@link resolveBrandingProfile}. A\n * caller must render NOTHING for it, never a placeholder and never a\n * substitute.\n */\nexport interface ResolvedBrandingProfile {\n productName: string\n logoUrl: string | null\n /** The logo for dark grounds; null means {@link logoUrl} serves both. */\n logoDarkUrl: string | null\n faviconUrl: string | null\n /** The favicon for a dark tab strip; null means {@link faviconUrl} serves both. */\n faviconDarkUrl: string | null\n primaryColor: string | null\n supportUrl: string | null\n /**\n * The brand's own FRONT DOOR — where \"Made with <product>\" sends a visitor\n * who wants to know what built the site they are looking at.\n *\n * Deliberately separate from {@link ResolvedBrandingProfile.supportUrl},\n * which the badge used to borrow: a help-desk address is the wrong answer to\n * \"what is this\", and on this deployment it resolves to a `mailto:`, so the\n * badge opened a blank email instead of a web page.\n *\n * Nullable for the same reason `supportUrl` is — a brand with nowhere to\n * send that visitor gets a plain label, never a substitute destination.\n */\n homeUrl: string | null\n fromName: string\n emailLogoUrl: string | null\n customConsoleDomain: string | null\n}\n\n/**\n * The PLATFORM (non-white-label) brand — the fallback every surface gets when\n * an org lacks the `whiteLabel` entitlement, and the gap-filler for a partial\n * agency profile.\n *\n * It lives beside the literals it is assembled from rather than beside the\n * entitlement gate that hands it out, and that placement is load-bearing\n * rather than tidying. A published tenant page renders the attribution badge\n * from this constant and reads nothing else out of `plan-entitlements.ts`;\n * defining it there put that module's whole plan, quota and entitlement table\n * — the single largest first-party module a visitor could reach — into the\n * first load of every anonymous visitor to every customer site.\n * `plan-entitlements.ts` re-exports it, so every existing caller is unchanged.\n *\n * The name says the PLATFORM's brand rather than Aglyn's because on a\n * self-host install that assertion stops being true: which platform this is,\n * is configuration. `resolveBrandingProfile` is the single resolver EVERY\n * branded surface routes through — console chrome, published-site badge and\n * title, transactional email — and this is its fallback, so all of them get a\n * self-host brand without extending the white-label machinery at all.\n */\nexport const PLATFORM_BRANDING_PROFILE: ResolvedBrandingProfile = {\n productName: PLATFORM_BRAND_NAME,\n // The platform's own surfaces still bake their logo in — this is here for\n // the surfaces that CANNOT, which is every surface rendered on somebody\n // else's site. The free-tier attribution badge is the one that exists\n // today, and it carried no mark at all while this was null.\n logoUrl: PLATFORM_MARK_URL,\n logoDarkUrl: null,\n faviconUrl: null,\n faviconDarkUrl: null,\n primaryColor: null,\n supportUrl: PLATFORM_SUPPORT_URL,\n homeUrl: PLATFORM_HOME_URL,\n fromName: PLATFORM_BRAND_NAME,\n emailLogoUrl: null,\n customConsoleDomain: null,\n}\n"],"names":["clean","httpsUrl","value","text","trim","length","undefined","PLATFORM_BRAND_NAME","process","env","NEXT_PUBLIC_PLATFORM_BRAND_NAME","PLATFORM_BRAND_LEGAL_NAME","NEXT_PUBLIC_PLATFORM_BRAND_LEGAL_NAME","PLATFORM_SUPPORT_URL","configured","NEXT_PUBLIC_PLATFORM_SUPPORT_URL","operatorEmail","NEXT_PUBLIC_OPERATOR_SUPPORT_EMAIL","consoleOrigin","NEXT_PUBLIC_CONSOLE_URL","replace","isAglynOperatedBrand","PLATFORM_HOME_URL","NEXT_PUBLIC_PLATFORM_HOME_URL","PLATFORM_MARK_URL","NEXT_PUBLIC_PLATFORM_MARK_URL","URL","protocol","PLATFORM_EMAIL_LOGO_URL","NEXT_PUBLIC_PLATFORM_EMAIL_LOGO_URL","PLATFORM_POSTAL_ADDRESS","NEXT_PUBLIC_PLATFORM_POSTAL_ADDRESS","split","filter","Boolean","join","PLATFORM_BRANDING_PROFILE","productName","logoUrl","logoDarkUrl","faviconUrl","faviconDarkUrl","primaryColor","supportUrl","homeUrl","fromName","emailLogoUrl","customConsoleDomain"],"mappings":"IAyEEA,QAYAA,SAkFAA,SAmBAA,SAiDAC;IAuBAD;AAlQF;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuCC,GAED,oFAAoF,GACpF,MAAMA,QAAQ,CAACE;IACb,MAAMC,OAAO,OAAOD,UAAU,WAAWA,MAAME,IAAI,KAAK;IACxD,OAAOD,KAAKE,MAAM,GAAGF,OAAOG;AAC9B;AAEA;;;;;;;CAOC,GACD,OAAO,MAAMC,uBACXP,SAAAA,MAAMQ,QAAQC,GAAG,CAACC,+BAA+B,aAAjDV,SAAsD,QAAO;AAE/D;;;;;;;;CAQC,GACD,OAAO,MAAMW,6BACXX,UAAAA,MAAMQ,QAAQC,GAAG,CAACG,qCAAqC,aAAvDZ,UACA,GAAGO,oBAAoB,IAAI,CAAC,CAAA;AAE9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkCC,GACD,OAAO,MAAMM,uBAA+B,AAAC,CAAA;QAKrBb;IAJtB,MAAMc,aAAad,MAAMQ,QAAQC,GAAG,CAACM,gCAAgC;IACrE,IAAID,YAAY,OAAOA;IACvB,MAAME,gBAAgBhB,MAAMQ,QAAQC,GAAG,CAACQ,kCAAkC;IAC1E,IAAID,eAAe,OAAO,CAAC,OAAO,EAAEA,eAAe;IACnD,MAAME,iBAAgBlB,SAAAA,MAAMQ,QAAQC,GAAG,CAACU,uBAAuB,sBAAzCnB,OAA4CoB,OAAO,CACvE,QACA;IAEF,OAAO,GAAGF,iBAAiB,wBAAwB,QAAQ,CAAC;AAC9D,CAAA,IAAI;AAEJ,kEAAkE,GAClE,OAAO,MAAMG,uBAAuB,IAClCd,wBAAwB,QAAO;AAEjC;;;;;;;;;;;;;;;;;;;;;;;;;;CA0BC,GACD,OAAO,MAAMe,qBACXtB,UAAAA,MAAMQ,QAAQC,GAAG,CAACc,6BAA6B,aAA/CvB,UACCqB,yBAAyB,sBAAsB,KAAK;AAEvD;;;;;;;;;;;;;;CAcC,GACD,OAAO,MAAMG,qBACXxB,UAAAA,MAAMQ,QAAQC,GAAG,CAACgB,6BAA6B,aAA/CzB,UACCqB,yBACG,oDACA,KAAK;AAEX,0FAA0F,GAC1F,MAAMpB,WAAW,CAACC;IAChB,IAAI,CAACA,OAAO,OAAOI;IACnB,IAAI;QACF,OAAO,IAAIoB,IAAIxB,OAAOyB,QAAQ,KAAK,WAAWzB,QAAQI;IACxD,EAAE,eAAM;QACN,OAAOA;IACT;AACF;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgCC,GACD,OAAO,MAAMsB,2BACX3B,YAAAA,SAASD,MAAMQ,QAAQC,GAAG,CAACoB,mCAAmC,cAA9D5B,YACCoB,yBACG,oEACA,KAAK;AAEX;;;;;;;;;;;;;;;;CAgBC,GACD,OAAO,MAAMS,mCACX9B,UAAAA,MAAMQ,QAAQC,GAAG,CAACsB,mCAAmC,sBAArD/B,QACIgC,KAAK,CAAC,YACPC,MAAM,CAACC,SACPC,IAAI,CAAC,wBACPd,yBACG,uFACA,KAAK;AAyCX;;;;;;;;;;;;;;;;;;;;CAoBC,GACD,OAAO,MAAMe,4BAAqD;IAChEC,aAAa9B;IACb,0EAA0E;IAC1E,wEAAwE;IACxE,sEAAsE;IACtE,4DAA4D;IAC5D+B,SAASd;IACTe,aAAa;IACbC,YAAY;IACZC,gBAAgB;IAChBC,cAAc;IACdC,YAAY9B;IACZ+B,SAAStB;IACTuB,UAAUtC;IACVuC,cAAc;IACdC,qBAAqB;AACvB,EAAC"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/platform-brand.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * What this deployment calls itself (AGL-2153).\n *\n * the rule for the self-host path: *\"the self hosted owner should not have\n * to edit the source to update aglyn branded items urls or personal\n * identification items, should move these to env vars\"*. AGL-2016 did that for\n * the values where getting it wrong misroutes a legal notice. This is the\n * other half — the product name itself, which was a bare `'Aglyn'` literal in\n * nine places.\n *\n * ## Why one module rather than nine reads\n *\n * The literals were not merely duplicated, they were duplicated across a\n * boundary: `PLATFORM_GENERATOR_NAME` exists here AND as a hand-copied\n * constant in `apps/tenant/middleware.ts`, because the edge bundle cannot\n * import this lib. A single source they both read is what stops the copy from\n * drifting into a deployment that half-renames itself.\n *\n * ## Why a default at all, when operator-identity.ts refuses to have one\n *\n * `operator-identity.ts` sets out the rule and it is the right one: a value\n * whose wrongness is invisible to the operator and visible only off-site must\n * not be guessed. The brand is the opposite case. An operator who does not set\n * it sees \"Aglyn\" in their own browser tab on their own first page load, in\n * their own console. That is a default they can see and correct, which is a\n * default rather than a trap — the same test AGL-2022 applied to the tenant\n * apex.\n *\n * ## Why `NEXT_PUBLIC_`, and why dot notation is load-bearing\n *\n * The brand is printed to the public — that is what it is for — so\n * `NEXT_PUBLIC_` carries no disclosure risk. Next inlines `NEXT_PUBLIC_*` into\n * the browser bundle by textually substituting `process.env.NAME`; the\n * **bracket** form is never substituted and reads `undefined` in the browser\n * (AGL-2037). Every read below is dot notation on purpose.\n *\n * Build-time consequence for self-hosters: these are baked in when the image\n * is built, so they must be set *before* `docker compose build`, not merely\n * before `up`. `docs/SELF_HOSTING.md` says so.\n */\n\n/** A non-empty trimmed string, else undefined — `' '` is a half-finished `.env`. */\nconst clean = (value: unknown): string | undefined => {\n const text = typeof value === 'string' ? value.trim() : ''\n return text.length ? text : undefined\n}\n\n/**\n * The product name, as shown to end users.\n *\n * This is the browser-tab affix, the PWA name, the WebAuthn relying-party name\n * the OS credential prompt displays, the `<meta name=\"generator\">` and\n * `x-powered-by` on published sites, and the platform default `productName` /\n * `fromName` every non-white-label org's surfaces resolve to.\n */\nexport const PLATFORM_BRAND_NAME =\n clean(process.env.NEXT_PUBLIC_PLATFORM_BRAND_NAME) ?? 'Aglyn'\n\n/**\n * The legal entity operating the software's own cloud, for copy that needs a\n * company rather than a product (`BRAND.ORG_NAME_LEGAL`).\n *\n * NOT the same thing as `operatorIdentity().name`, and the distinction matters.\n * This is a *brand* string with a visible default; that one is the identity a\n * statutory notice is addressed to and refuses to default. An operator who\n * sets only this has renamed the product, not published who they are.\n */\nexport const PLATFORM_BRAND_LEGAL_NAME =\n clean(process.env.NEXT_PUBLIC_PLATFORM_BRAND_LEGAL_NAME) ??\n `${PLATFORM_BRAND_NAME} LLC`\n\n/**\n * Where a \"need help?\" line points.\n *\n * Falls through the operator's OWN support address before it falls back to\n * ours, deliberately: an operator who followed the runbook has already set\n * `NEXT_PUBLIC_OPERATOR_SUPPORT_EMAIL`, and making them discover a second\n * variable to stop pointing their customers at Aglyn's support desk — which\n * cannot help those customers with anything — is a trap dressed as a default.\n *\n * Read here rather than imported from `operator-identity.ts` so this module\n * stays dependency-free and safe to import from anywhere, including the places\n * that cannot take the operator module's weight.\n *\n * ## Why the last fallback is the CONSOLE and not the marketing site\n *\n * It was `aglyn.com/support`, which answers 404 and has never existed\n * (AGL-3262) — MEASURED against the live site, not inferred: `/support`,\n * `/help` and `/docs` all render the not-found page and none is in the\n * sitemap. A support URL that 404s is worse than none, because it is printed\n * on a receipt beside a charge the reader is already unsure about.\n *\n * `app.aglyn.com/support` is a real page (AGL-3265): it resolves the\n * workspace from the session, so a customer with one lands on its support\n * umbrella and a customer with several picks. That is the whole reason it\n * exists — it is the one support address that needs nothing interpolated\n * into it, which is exactly what this constant is.\n *\n * The console is THIS install's (AGL-3322). `/support` is a console route, so\n * every console serves its own, and `NEXT_PUBLIC_CONSOLE_URL` is required of\n * every self-hosted install — so one that configured neither variable above\n * sends its customers to its own support page rather than to ours, where they\n * have no account. The line is in the footer of every system email the\n * platform sends. `app.aglyn.com` is left only for a build that names no\n * console at all.\n */\nexport const PLATFORM_SUPPORT_URL: string = (() => {\n const configured = clean(process.env.NEXT_PUBLIC_PLATFORM_SUPPORT_URL)\n if (configured) return configured\n const operatorEmail = clean(process.env.NEXT_PUBLIC_OPERATOR_SUPPORT_EMAIL)\n if (operatorEmail) return `mailto:${operatorEmail}`\n return `${platformConsoleOrigin()}/support`\n})()\n\n/**\n * This install's console origin, with no trailing slash — what a link in a\n * notification or an email the platform sends is built on (AGL-3080).\n *\n * `NEXT_PUBLIC_CONSOLE_URL`, which every self-hosted install sets; Aglyn's\n * own console only for a build that names none. One reading for the support\n * page above and for every scheduled sender, a plugin's included, so none of\n * them spells the fallback again.\n */\nexport function platformConsoleOrigin(): string {\n const configured = clean(process.env.NEXT_PUBLIC_CONSOLE_URL)?.replace(/\\/+$/, '')\n return configured || 'https://app.aglyn.com'\n}\n\n/** True when this deployment still answers to Aglyn's own brand. */\nexport const isAglynOperatedBrand = (): boolean =>\n PLATFORM_BRAND_NAME === 'Aglyn'\n\n/**\n * Where the platform's own front door is — the marketing home, not the help\n * desk.\n *\n * ## Why this is not `PLATFORM_SUPPORT_URL`\n *\n * The \"Made with …\" badge on a published free-tier site used to link to\n * `supportUrl`, and on this deployment `supportUrl` falls through to\n * `mailto:` + the operator support address. So the one link the platform gets\n * on every free customer's site — its only organic acquisition surface —\n * opened the visitor's mail client addressed to our support desk. A visitor\n * who liked the site and clicked to find out what built it got a blank email,\n * and support got the misdirected replies.\n *\n * They are genuinely different destinations. `supportUrl` answers \"I am a\n * customer and something is wrong\"; this answers \"what is this, and can I have\n * one\". Keeping one value for both guarantees one of them is wrong.\n *\n * ## Why it is null rather than aglyn.com off-brand\n *\n * A self-hosted deployment that renamed the product but set no home URL has\n * nowhere honest to send that visitor: \"Made with Foo\" pointing at aglyn.com\n * advertises the wrong company on Foo's customers' sites. The badge already\n * renders as a plain label when it has no destination (AGL-2428), which is the\n * correct outcome — so the Aglyn default applies only while this really is the\n * Aglyn-operated brand.\n */\nexport const PLATFORM_HOME_URL: string | null =\n clean(process.env.NEXT_PUBLIC_PLATFORM_HOME_URL) ??\n (isAglynOperatedBrand() ? 'https://aglyn.com' : null)\n\n/**\n * The platform's own logo MARK — the compact, square, no-wordmark form — as a\n * URL an `<img>` on a PUBLISHED SITE can load.\n *\n * Site-relative on purpose: every app that renders it serves\n * `public/_static/…` from its own origin, so one path works on\n * `{sub}.aglyn.app` and on a customer's custom domain alike without knowing\n * either. The white variant is the one that survives the badge's dark pill and\n * an org's `primaryColor` behind it.\n *\n * Null unless this is the Aglyn-operated brand, for the same reason as\n * {@link PLATFORM_HOME_URL}: a renamed deployment must not stamp our mark on\n * its customers' sites. Such an operator sets `NEXT_PUBLIC_PLATFORM_MARK_URL`\n * to their own, or gets a text-only badge.\n */\nexport const PLATFORM_MARK_URL: string | null =\n clean(process.env.NEXT_PUBLIC_PLATFORM_MARK_URL) ??\n (isAglynOperatedBrand()\n ? '/_static/images/brand/aglyn-logo-mark-white.svg'\n : null)\n\n/** An absolute `https://` URL, else undefined — anything else has no origin in an inbox. */\nconst httpsUrl = (value: string | undefined): string | undefined => {\n if (!value) return undefined\n try {\n return new URL(value).protocol === 'https:' ? value : undefined\n } catch {\n return undefined\n }\n}\n\n/**\n * The platform's logo as its own system email draws it: the header of mail\n * sent under the platform's brand (AGL-3322).\n *\n * ABSOLUTE and https, unlike {@link PLATFORM_MARK_URL}. That mark is a\n * site-relative path because every app that draws it serves the file from its\n * own origin; an inbox has no origin, so a relative path there is a\n * broken-image box. A configured value that is not an absolute `https://` URL\n * is ignored rather than handed on to fail in somebody's inbox.\n *\n * The WORDMARK rather than the square mark: a header is read at a glance, and\n * the wordmark names the sender where the mark would need the reader to\n * already know it.\n *\n * A PNG sized for the slot (AGL-3367): 396×120, three times the 132×40 the\n * header draws it at, about 15 KB, served as a static file off aglyn.com's\n * edge. The master-resolution file behind the media CDN's 60-second cache is\n * four times the bytes and makes the header the last thing in the email to\n * appear. Not an SVG: Gmail and Outlook do not draw SVG in mail, so every\n * recipient on either would get a broken-image box where the logo belongs.\n *\n * Null unless this is the Aglyn-operated brand, for the reason\n * {@link PLATFORM_HOME_URL} is: a renamed deployment must not put our logo at\n * the top of its customers' mail. Such an operator sets\n * `NEXT_PUBLIC_PLATFORM_EMAIL_LOGO_URL`, or gets its product name as the\n * header, in bold text.\n *\n * Deliberately NOT `PLATFORM_BRANDING_PROFILE.emailLogoUrl`, which stays null.\n * That field is an ORG's email logo: a white-label profile inherits it when\n * the org set none, and a send that carries one is read as an org's own mail.\n * Putting the platform's logo there would stamp it on every white-label org\n * that left the field blank.\n */\nexport const PLATFORM_EMAIL_LOGO_URL: string | null =\n httpsUrl(clean(process.env.NEXT_PUBLIC_PLATFORM_EMAIL_LOGO_URL)) ??\n (isAglynOperatedBrand()\n ? 'https://aglyn.com/_static/images/brand/aglyn-email-wordmark.png'\n : null)\n\n/**\n * The postal address printed beside {@link PLATFORM_BRAND_LEGAL_NAME} at the\n * foot of the platform's own system email (AGL-3322).\n *\n * A BRAND string, beside the legal name whose line it completes, rather than\n * a field of `operatorIdentity()`, and the difference is the default. The\n * operator identity is what a statutory notice is addressed to and refuses to\n * guess; this is a line in a footer the operator sees in their own mail and\n * can correct, and for the Aglyn-operated brand the right value is known: the\n * registered agent's address the Aglyn marketing site's email footer prints.\n *\n * Null unless this is the Aglyn-operated brand: a renamed deployment must\n * never print our address as its own. Such an operator sets\n * `NEXT_PUBLIC_PLATFORM_POSTAL_ADDRESS`, or their footer carries the legal\n * name alone. Line breaks in a configured value are folded into commas,\n * because the footer prints the address on one line.\n */\nexport const PLATFORM_POSTAL_ADDRESS: string | null =\n clean(process.env.NEXT_PUBLIC_PLATFORM_POSTAL_ADDRESS)\n ?.split(/\\s*\\n\\s*/)\n .filter(Boolean)\n .join(', ') ??\n (isAglynOperatedBrand()\n ? 'c/o Northwest Registered Agent, LLC, 5900 Balcones Drive STE 100, Austin, TX 78731'\n : null)\n\n/**\n * A branding profile with every field present — what a branded surface\n * renders. Image/color/domain fields are nullable (Aglyn's own surfaces\n * bake those in rather than carry a URL); the remaining text fields always\n * have a value so callers never string-concatenate `undefined`.\n *\n * `supportUrl` is nullable too, and it is the one field where null carries a\n * decision rather than an absence — see {@link resolveBrandingProfile}. A\n * caller must render NOTHING for it, never a placeholder and never a\n * substitute.\n */\nexport interface ResolvedBrandingProfile {\n productName: string\n logoUrl: string | null\n /** The logo for dark grounds; null means {@link logoUrl} serves both. */\n logoDarkUrl: string | null\n faviconUrl: string | null\n /** The favicon for a dark tab strip; null means {@link faviconUrl} serves both. */\n faviconDarkUrl: string | null\n primaryColor: string | null\n supportUrl: string | null\n /**\n * The brand's own FRONT DOOR — where \"Made with <product>\" sends a visitor\n * who wants to know what built the site they are looking at.\n *\n * Deliberately separate from {@link ResolvedBrandingProfile.supportUrl},\n * which the badge used to borrow: a help-desk address is the wrong answer to\n * \"what is this\", and on this deployment it resolves to a `mailto:`, so the\n * badge opened a blank email instead of a web page.\n *\n * Nullable for the same reason `supportUrl` is — a brand with nowhere to\n * send that visitor gets a plain label, never a substitute destination.\n */\n homeUrl: string | null\n fromName: string\n emailLogoUrl: string | null\n customConsoleDomain: string | null\n}\n\n/**\n * The PLATFORM (non-white-label) brand — the fallback every surface gets when\n * an org lacks the `whiteLabel` entitlement, and the gap-filler for a partial\n * agency profile.\n *\n * It lives beside the literals it is assembled from rather than beside the\n * entitlement gate that hands it out, and that placement is load-bearing\n * rather than tidying. A published tenant page renders the attribution badge\n * from this constant and reads nothing else out of `plan-entitlements.ts`;\n * defining it there put that module's whole plan, quota and entitlement table\n * — the single largest first-party module a visitor could reach — into the\n * first load of every anonymous visitor to every customer site.\n * `plan-entitlements.ts` re-exports it, so every existing caller is unchanged.\n *\n * The name says the PLATFORM's brand rather than Aglyn's because on a\n * self-host install that assertion stops being true: which platform this is,\n * is configuration. `resolveBrandingProfile` is the single resolver EVERY\n * branded surface routes through — console chrome, published-site badge and\n * title, transactional email — and this is its fallback, so all of them get a\n * self-host brand without extending the white-label machinery at all.\n */\nexport const PLATFORM_BRANDING_PROFILE: ResolvedBrandingProfile = {\n productName: PLATFORM_BRAND_NAME,\n // The platform's own surfaces still bake their logo in — this is here for\n // the surfaces that CANNOT, which is every surface rendered on somebody\n // else's site. The free-tier attribution badge is the one that exists\n // today, and it carried no mark at all while this was null.\n logoUrl: PLATFORM_MARK_URL,\n logoDarkUrl: null,\n faviconUrl: null,\n faviconDarkUrl: null,\n primaryColor: null,\n supportUrl: PLATFORM_SUPPORT_URL,\n homeUrl: PLATFORM_HOME_URL,\n fromName: PLATFORM_BRAND_NAME,\n emailLogoUrl: null,\n customConsoleDomain: null,\n}\n"],"names":["clean","httpsUrl","value","text","trim","length","undefined","PLATFORM_BRAND_NAME","process","env","NEXT_PUBLIC_PLATFORM_BRAND_NAME","PLATFORM_BRAND_LEGAL_NAME","NEXT_PUBLIC_PLATFORM_BRAND_LEGAL_NAME","PLATFORM_SUPPORT_URL","configured","NEXT_PUBLIC_PLATFORM_SUPPORT_URL","operatorEmail","NEXT_PUBLIC_OPERATOR_SUPPORT_EMAIL","platformConsoleOrigin","NEXT_PUBLIC_CONSOLE_URL","replace","isAglynOperatedBrand","PLATFORM_HOME_URL","NEXT_PUBLIC_PLATFORM_HOME_URL","PLATFORM_MARK_URL","NEXT_PUBLIC_PLATFORM_MARK_URL","URL","protocol","PLATFORM_EMAIL_LOGO_URL","NEXT_PUBLIC_PLATFORM_EMAIL_LOGO_URL","PLATFORM_POSTAL_ADDRESS","NEXT_PUBLIC_PLATFORM_POSTAL_ADDRESS","split","filter","Boolean","join","PLATFORM_BRANDING_PROFILE","productName","logoUrl","logoDarkUrl","faviconUrl","faviconDarkUrl","primaryColor","supportUrl","homeUrl","fromName","emailLogoUrl","customConsoleDomain"],"mappings":"IAyEEA,QAYAA,SA4FAA,SAmBAA,SAiDAC;IAuBAD;AA5QF;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuCC,GAED,oFAAoF,GACpF,MAAMA,QAAQ,CAACE;IACb,MAAMC,OAAO,OAAOD,UAAU,WAAWA,MAAME,IAAI,KAAK;IACxD,OAAOD,KAAKE,MAAM,GAAGF,OAAOG;AAC9B;AAEA;;;;;;;CAOC,GACD,OAAO,MAAMC,uBACXP,SAAAA,MAAMQ,QAAQC,GAAG,CAACC,+BAA+B,aAAjDV,SAAsD,QAAO;AAE/D;;;;;;;;CAQC,GACD,OAAO,MAAMW,6BACXX,UAAAA,MAAMQ,QAAQC,GAAG,CAACG,qCAAqC,aAAvDZ,UACA,GAAGO,oBAAoB,IAAI,CAAC,CAAA;AAE9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkCC,GACD,OAAO,MAAMM,uBAA+B,AAAC,CAAA;IAC3C,MAAMC,aAAad,MAAMQ,QAAQC,GAAG,CAACM,gCAAgC;IACrE,IAAID,YAAY,OAAOA;IACvB,MAAME,gBAAgBhB,MAAMQ,QAAQC,GAAG,CAACQ,kCAAkC;IAC1E,IAAID,eAAe,OAAO,CAAC,OAAO,EAAEA,eAAe;IACnD,OAAO,GAAGE,wBAAwB,QAAQ,CAAC;AAC7C,CAAA,IAAI;AAEJ;;;;;;;;CAQC,GACD,OAAO,SAASA;QACKlB;IAAnB,MAAMc,cAAad,SAAAA,MAAMQ,QAAQC,GAAG,CAACU,uBAAuB,sBAAzCnB,OAA4CoB,OAAO,CAAC,QAAQ;IAC/E,OAAON,cAAc;AACvB;AAEA,kEAAkE,GAClE,OAAO,MAAMO,uBAAuB,IAClCd,wBAAwB,QAAO;AAEjC;;;;;;;;;;;;;;;;;;;;;;;;;;CA0BC,GACD,OAAO,MAAMe,qBACXtB,UAAAA,MAAMQ,QAAQC,GAAG,CAACc,6BAA6B,aAA/CvB,UACCqB,yBAAyB,sBAAsB,KAAK;AAEvD;;;;;;;;;;;;;;CAcC,GACD,OAAO,MAAMG,qBACXxB,UAAAA,MAAMQ,QAAQC,GAAG,CAACgB,6BAA6B,aAA/CzB,UACCqB,yBACG,oDACA,KAAK;AAEX,0FAA0F,GAC1F,MAAMpB,WAAW,CAACC;IAChB,IAAI,CAACA,OAAO,OAAOI;IACnB,IAAI;QACF,OAAO,IAAIoB,IAAIxB,OAAOyB,QAAQ,KAAK,WAAWzB,QAAQI;IACxD,EAAE,eAAM;QACN,OAAOA;IACT;AACF;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgCC,GACD,OAAO,MAAMsB,2BACX3B,YAAAA,SAASD,MAAMQ,QAAQC,GAAG,CAACoB,mCAAmC,cAA9D5B,YACCoB,yBACG,oEACA,KAAK;AAEX;;;;;;;;;;;;;;;;CAgBC,GACD,OAAO,MAAMS,mCACX9B,UAAAA,MAAMQ,QAAQC,GAAG,CAACsB,mCAAmC,sBAArD/B,QACIgC,KAAK,CAAC,YACPC,MAAM,CAACC,SACPC,IAAI,CAAC,wBACPd,yBACG,uFACA,KAAK;AAyCX;;;;;;;;;;;;;;;;;;;;CAoBC,GACD,OAAO,MAAMe,4BAAqD;IAChEC,aAAa9B;IACb,0EAA0E;IAC1E,wEAAwE;IACxE,sEAAsE;IACtE,4DAA4D;IAC5D+B,SAASd;IACTe,aAAa;IACbC,YAAY;IACZC,gBAAgB;IAChBC,cAAc;IACdC,YAAY9B;IACZ+B,SAAStB;IACTuB,UAAUtC;IACVuC,cAAc;IACdC,qBAAqB;AACvB,EAAC"}
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
*/
|
|
17
17
|
/**
|
|
18
18
|
* Policy for the tenant plugin-API dispatcher's visitor-write rate limit
|
|
19
|
-
* (AGL-1770). Pure
|
|
19
|
+
* (AGL-1770). Pure but for one read of the route registry, for a route its
|
|
20
|
+
* plugin registered as a machine's, so it is unit-testable without a route
|
|
20
21
|
* harness — the durable half lives in `@aglyn/tenant-data-admin` beside
|
|
21
22
|
* `visitorWriteRefusal`, exactly like the lockdown split.
|
|
22
23
|
*
|
|
@@ -76,6 +77,11 @@ export declare const VISITOR_WRITE_RATE_WINDOW_MS = 60000;
|
|
|
76
77
|
/**
|
|
77
78
|
* Is this dispatcher path a credentialed machine surface (exempt), rather
|
|
78
79
|
* than a visitor one (limited)? Unknown paths are visitor by default.
|
|
80
|
+
*
|
|
81
|
+
* A path is a machine's when it is on the list above, or when the plugin
|
|
82
|
+
* that registered it declared it one (`machine: true`, AGL-3080) — the same
|
|
83
|
+
* polarity as the list: a route is exempt only by saying so, and one that
|
|
84
|
+
* forgot is limited, which fails loudly as a 429 its scheduler retries.
|
|
79
85
|
*/
|
|
80
86
|
export declare function isMachinePluginApiPath(path: string): boolean;
|
|
81
87
|
/**
|
|
@@ -13,9 +13,11 @@
|
|
|
13
13
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
|
-
*/
|
|
16
|
+
*/ import { isPluginMachineRoute } from "./api-plugins.js";
|
|
17
|
+
/**
|
|
17
18
|
* Policy for the tenant plugin-API dispatcher's visitor-write rate limit
|
|
18
|
-
* (AGL-1770). Pure
|
|
19
|
+
* (AGL-1770). Pure but for one read of the route registry, for a route its
|
|
20
|
+
* plugin registered as a machine's, so it is unit-testable without a route
|
|
19
21
|
* harness — the durable half lives in `@aglyn/tenant-data-admin` beside
|
|
20
22
|
* `visitorWriteRefusal`, exactly like the lockdown split.
|
|
21
23
|
*
|
|
@@ -150,10 +152,16 @@
|
|
|
150
152
|
/**
|
|
151
153
|
* Is this dispatcher path a credentialed machine surface (exempt), rather
|
|
152
154
|
* than a visitor one (limited)? Unknown paths are visitor by default.
|
|
155
|
+
*
|
|
156
|
+
* A path is a machine's when it is on the list above, or when the plugin
|
|
157
|
+
* that registered it declared it one (`machine: true`, AGL-3080) — the same
|
|
158
|
+
* polarity as the list: a route is exempt only by saying so, and one that
|
|
159
|
+
* forgot is limited, which fails loudly as a 429 its scheduler retries.
|
|
153
160
|
*/ export function isMachinePluginApiPath(path) {
|
|
154
161
|
const normalized = normalize(path);
|
|
155
162
|
if (MACHINE_API_PATHS.has(normalized)) return true;
|
|
156
|
-
|
|
163
|
+
if (MACHINE_API_PREFIXES.some((prefix)=>normalized.startsWith(prefix))) return true;
|
|
164
|
+
return isPluginMachineRoute(normalized);
|
|
157
165
|
}
|
|
158
166
|
/**
|
|
159
167
|
* The limiter key: one bucket per (site, client IP).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/plugin-api-rate-limit.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 * Policy for the tenant plugin-API dispatcher's visitor-write rate limit\n * (AGL-1770). Pure and dependency-free so it is unit-testable without a route\n * harness — the durable half lives in `@aglyn/tenant-data-admin` beside\n * `visitorWriteRefusal`, exactly like the lockdown split.\n *\n * ## What this bounds\n *\n * `apps/tenant/app/api/[...pluginApi]/route.ts` dispatches every plugin's\n * visitor-facing handler and, before this, applied no rate limit of any kind.\n * The gates it did have each refuse something narrower: per-site enablement is\n * skipped by an unresolvable `hostId`, the release gate only checks that the\n * plugin is on, and `visitorWriteRefusal` refuses only a paused or suspended\n * site. None of them bounds volume from a working, enabled site — which is\n * every live merchant.\n *\n * AGL-1769 closed the *shape* of the unauthenticated cart write (the caller\n * chose the document path) and was explicit that it did not bound the\n * *quantity*: \"id control never multiplied volume; it bought shape\". This is\n * the other half. The cost of the unbounded half lands in the merchant's own\n * Firestore — document count and storage on a tenant who did nothing wrong.\n */\n\n/**\n * 120 writes per minute per (site, client IP).\n *\n * Deliberately generous, and the reasoning matters more than the number: a\n * limit that trips on genuine use is worse than no limit, because it gets\n * raised until it means nothing. Measured against what the storefront actually\n * emits:\n *\n * - `cart.tsx:214` puts the line-quantity `TextField`'s `onChange` straight\n * onto a POST with no debounce, so *typing* `100` into a quantity box is\n * three cart writes. Editing a few lines is a genuine ten-to-twenty-write\n * burst in a couple of seconds.\n * - `experiments/track` is an A/B beacon: one exposure write per experiment\n * per page view, so a visitor clicking through pages at ~1/s with two live\n * experiments sustains ~2 writes/s legitimately.\n * - Every visitor write on a site shares ONE budget (see the key below), so\n * these add rather than each getting their own headroom.\n *\n * What a tighter number would actually buy is small. At 120/min a single\n * source can still mint ~172k documents a day against one site; at 30/min it\n * is ~43k. Neither is \"safe\" in absolute terms — what the cap buys is turning\n * a one-line flood script from *millions* per day into something that needs a\n * distributed botnet to stay interesting, and turning an incident from\n * instantaneous into detectable. That property is bought at 120 as well as at\n * 30, and 30 would refuse real shoppers.\n *\n * 120/60s is also `DEFAULT_RATE_LIMIT` / `DEFAULT_RATE_WINDOW_MS` — the same\n * pair the public REST API enforces — so this adds no bespoke number.\n */\nexport const VISITOR_WRITE_RATE_LIMIT = 120\n\n/**\n * Fixed 60s window, matching every other limiter in the codebase rather than\n * improving on it. A fixed window admits up to 2× the cap across a boundary;\n * consistency with the shared bucket ids, the degradation markers and\n * `/api/health/rate-limits` is worth more here than closing that.\n */\nexport const VISITOR_WRITE_RATE_WINDOW_MS = 60_000\n\n/**\n * Machine-caller paths on EITHER plugin-API surface: neither dispatcher rate\n * limits these.\n *\n * The membership rule, so this list is decidable rather than a taste call: the\n * caller proves a shared secret, a webhook signature or a console session that\n * a visitor has no way to obtain, and its request volume is set by campaign\n * size or cron schedule rather than by human behaviour. A 120/min cap does not\n * merely inconvenience these — Resend delivering open/click webhooks for one\n * 50k-recipient campaign arrives from a handful of IPs for a single host and\n * would be shredded by any shopper-sized ceiling.\n *\n * ### Why a path list is right here when the dispatcher argues against them\n *\n * The lockdown gate two lines below is keyed by METHOD precisely to avoid a\n * path list, \"for the same reason `useSiteFetch` draws the Preview boundary\n * that way: a per-path list means thirteen handlers each remembering and the\n * fourteenth silently not.\"\n *\n * That argument turns on POLARITY, and this list has the opposite one. The\n * lockdown list would have enumerated what to *protect*, so a forgotten entry\n * is silently unprotected — the failure you never find. This enumerates what\n * to *exempt*, so the default is limited: a new visitor endpoint added\n * tomorrow is covered by doing nothing, and a forgotten machine endpoint\n * fails loudly and recoverably as 429s with `Retry-After` on a caller that\n * retries. Safe default, noisy mistake.\n *\n * Each entry, with the credential that makes it a machine caller:\n *\n * - `email/events` — Resend's webhook; `verifySvix` over `svix-id`/\n * `svix-timestamp`/`svix-signature` against `RESEND_WEBHOOK_SECRET`.\n * - `campaigns/send` — a Firebase ID token with a host member role, or\n * `CRON_SECRET` for the scheduled-send path.\n * - `bookings/reminders` — `x-cron-secret` against `CRON_SECRET`; refuses\n * outright when it is unset.\n * - `hooks/…` — the merchant's per-hook secret in `x-aglyn-secret`, compared\n * with `timingSafeEqual`. It already carries its own 30/min per-hook\n * limiter, so exempting it here removes nothing.\n *\n * The four below are registered on the CONSOLE surface only, and each is a\n * chunked cron sweep rather than a single beat — `sweepConsoleCron` follows\n * `nextCursor` up to `CRON_SWEEP_MAX_CHUNKS` (50) POSTs per route per run,\n * from one Cloud Functions address, and the key below carries no path. Three\n * such routes on one beat is 150 requests in a minute against a single\n * bucket, so this exemption is load-bearing and not decorative:\n *\n * - `campaigns/process-scheduled` — `x-cron-secret` against `CRON_SECRET`;\n * refuses outright when it is unset.\n * - `lists/materialize` — the same header, the same refusal.\n * - `commerce/process-abandoned` — the same header, the same refusal.\n * - `commerce/process-restock` — the same header, the same refusal.\n *\n * Note what is deliberately NOT exempt: presence of an `authorization` or\n * `svix-signature` HEADER. Header presence is not a credential — the\n * dispatcher cannot verify signatures it does not own, so exempting on the\n * header would let any caller skip the limiter by attaching a garbage one to\n * a cart POST.\n *\n * Nor is `commerce/supplier-update`, whose HMAC token makes it a machine\n * caller but whose volume is set by a supplier's own system rather than by a\n * schedule. It fails the second half of the membership rule, and the polarity\n * above says which way to resolve a borderline case: leave it limited, where\n * a mistake is a recoverable 429.\n */\nconst MACHINE_API_PATHS: ReadonlySet<string> = new Set([\n 'email/events',\n 'campaigns/send',\n 'bookings/reminders',\n 'campaigns/process-scheduled',\n 'lists/materialize',\n 'commerce/process-abandoned',\n 'commerce/process-restock',\n])\n\n/** Prefixes whose whole subtree is a machine surface (`hooks/{host}/{hook}`). */\nconst MACHINE_API_PREFIXES: readonly string[] = ['hooks/']\n\n/** Leading/trailing slashes stripped, matching `normalizeApiPath`. */\nfunction normalize(path: string): string {\n return String(path ?? '').replace(/^\\/+|\\/+$/g, '')\n}\n\n/**\n * Is this dispatcher path a credentialed machine surface (exempt), rather\n * than a visitor one (limited)? Unknown paths are visitor by default.\n */\nexport function isMachinePluginApiPath(path: string): boolean {\n const normalized = normalize(path)\n if (MACHINE_API_PATHS.has(normalized)) return true\n return MACHINE_API_PREFIXES.some((prefix) => normalized.startsWith(prefix))\n}\n\n/**\n * The limiter key: one bucket per (site, client IP).\n *\n * ## Why compound, and why NOT a second limiter in either direction\n *\n * **Per-site alone is rejected.** A single attacker would consume the whole\n * merchant's allowance and every real shopper would then be refused — an\n * attacker-triggered denial of service against the victim, cheaper to mount\n * than the flood it replaces and costing the merchant *sales* rather than\n * storage. That is strictly worse than the growth it guards.\n *\n * **Per-IP alone is rejected.** Trivially distributed, and it would let one\n * merchant's traffic spend another's budget.\n *\n * **Compound is what \"per-host and per-IP together\" should mean here.** Each\n * source is bounded on each site, and no source can affect another's budget,\n * so the ceiling a site is exposed to is (distinct source IPs) × 120/min —\n * the same shape legitimate traffic has. What it does not stop is a genuine\n * botnet, and that is stated rather than papered over: bounding *that* needs\n * an edge WAF, not an application limiter, and every application-level scheme\n * that would catch it (a site-wide ceiling) reintroduces the self-DoS above.\n *\n * **A second, platform-wide per-IP limiter was considered and dropped.** It\n * would bound one source sweeping many merchants, but costs a second\n * Firestore transaction on every storefront write — tripling the ops on a\n * legitimate add-to-cart — to bound an attack that the compound key already\n * prices at one full budget per merchant. One transaction per visitor write\n * is the established cost here (`forms/submit`, `protection/unlock`); two\n * would be novel and is not paid for by what it buys.\n *\n * The path is **not** in the key, on purpose: one shared budget per (site,\n * IP) across cart, reviews, newsletter, bookings and beacons. Keying per path\n * would let a caller multiply its allowance by cycling endpoints, which is\n * free to do and defeats the point.\n *\n * `hostId` is frequently `''` — the dispatcher only resolves one from the\n * query or a JSON body, and a handler that self-gates may supply none. It is\n * kept in the key rather than skipped: AGL-1769 named \"an unresolvable\n * `hostId` skips the gate\" as an existing hole, and a limiter that a caller\n * turns off by declining to name a site is not a limiter. Host-less writes\n * simply share one bucket per IP.\n */\nexport function visitorWriteRateLimitKey(hostId: string, ip: string): string {\n return `pluginwrite:${hostId || '-'}:${ip || 'unknown'}`\n}\n\n/**\n * Policy for the CONSOLE plugin-API dispatcher's write rate limit.\n *\n * `apps/console/app/api/[...pluginApi]/route.ts` dispatches every plugin's\n * console-facing handler — the marketplace installs and publishes, `ai/assist`,\n * gift cards, POS orders, refunds, the email list previews and the suppression\n * writes — and it carries no limiter of any kind. Its gates each refuse\n * something narrower: per-site enablement needs a resolvable `hostId`, the\n * release flag only asks whether the plugin is on, and lockdown only refuses\n * during an incident. None of them bounds volume from an ordinary signed-in\n * operator, which is every request the surface serves.\n *\n * The exposure is not the same one the visitor limiter closes. These handlers\n * are authenticated, so nobody is minting documents anonymously; what they do\n * instead is spend REAL money per call — a contact scan with a read budget, a\n * bundle download and verify, a model call to a paid vendor — on a surface\n * where a stuck retry loop in one console tab is indistinguishable from abuse.\n */\n\n/**\n * 120 requests per minute per console subject.\n *\n * `DEFAULT_RATE_LIMIT` / `DEFAULT_RATE_WINDOW_MS` again, and the same pair the\n * visitor limiter and the public REST API use, so this introduces no bespoke\n * number to reason about. It is far above a person pressing buttons: the\n * costly routes on this surface are one-per-click, and the console's own\n * page-load traffic reaches Firestore through the client SDK rather than\n * through this dispatcher.\n *\n * Deliberately not tighter even though each call here costs more than a cart\n * write. A limit that trips on genuine use gets raised until it means nothing,\n * and the per-call cost is the wrong lever for that anyway — an entitlement or\n * a quota bounds spend per org over a month, where this bounds a runaway loop\n * over a minute. Two different jobs; this one only has to make the loop stop.\n */\nexport const CONSOLE_API_RATE_LIMIT = 120\n\n/** Fixed 60s window, matching every other limiter in the codebase. */\nexport const CONSOLE_API_RATE_WINDOW_MS = 60_000\n\n/**\n * The limiter key: one bucket per authenticated subject.\n *\n * ## Why this is NOT the visitor key\n *\n * `visitorWriteRateLimitKey` is compound because its surface is\n * unauthenticated, and both halves of that argument are answers to anonymity.\n * Per-site alone was rejected there because one attacker would spend a whole\n * merchant's allowance and refuse real shoppers — an attacker-triggered denial\n * of service against the victim. On the console the subject is a verified\n * `uid`, so a caller can only ever refuse ITSELF: the self-DoS the compound key\n * exists to prevent cannot be aimed at anyone else, and the isolation it buys\n * is already bought by the identity.\n *\n * `hostId` is therefore left OUT rather than added, which is the opposite of\n * the visitor key and for the reason the visitor key leaves the PATH out: an\n * operator with fifty sites would otherwise hold fifty budgets and could\n * multiply its allowance by cycling a field it controls. One person is one\n * budget across every site they can reach.\n *\n * ## The unauthenticated fallback\n *\n * A caller whose bearer token did not decode — a plugin key, a POS device, an\n * anonymous probe — has no `uid`, and its client address stands in. That\n * bucket is shared by every such caller behind one address, which is the\n * conservative direction: an unidentified caller on an authenticated surface\n * should not get a private allowance for declining to identify itself. Keeping\n * it counted at all is the point. A limiter a caller switches off by sending\n * no credential is not a limiter, and this surface's whole exposure is what\n * runs BEFORE a handler decides who is asking.\n */\nexport function consoleApiRateLimitKey(subject: string): string {\n return `consoleapi:${subject || 'unknown'}`\n}\n"],"names":["VISITOR_WRITE_RATE_LIMIT","VISITOR_WRITE_RATE_WINDOW_MS","MACHINE_API_PATHS","Set","MACHINE_API_PREFIXES","normalize","path","String","replace","isMachinePluginApiPath","normalized","has","some","prefix","startsWith","visitorWriteRateLimitKey","hostId","ip","CONSOLE_API_RATE_LIMIT","CONSOLE_API_RATE_WINDOW_MS","consoleApiRateLimitKey","subject"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;CAqBC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4BC,GACD,OAAO,MAAMA,2BAA2B,IAAG;AAE3C;;;;;CAKC,GACD,OAAO,MAAMC,+BAA+B,MAAM;AAElD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+DC,GACD,MAAMC,oBAAyC,IAAIC,IAAI;IACrD;IACA;IACA;IACA;IACA;IACA;IACA;CACD;AAED,+EAA+E,GAC/E,MAAMC,uBAA0C;IAAC;CAAS;AAE1D,oEAAoE,GACpE,SAASC,UAAUC,IAAY;IAC7B,OAAOC,OAAOD,eAAAA,OAAQ,IAAIE,OAAO,CAAC,cAAc;AAClD;AAEA;;;CAGC,GACD,OAAO,SAASC,uBAAuBH,IAAY;IACjD,MAAMI,aAAaL,UAAUC;IAC7B,IAAIJ,kBAAkBS,GAAG,CAACD,aAAa,OAAO;IAC9C,OAAON,qBAAqBQ,IAAI,CAAC,CAACC,SAAWH,WAAWI,UAAU,CAACD;AACrE;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyCC,GACD,OAAO,SAASE,yBAAyBC,MAAc,EAAEC,EAAU;IACjE,OAAO,CAAC,YAAY,EAAED,UAAU,IAAI,CAAC,EAAEC,MAAM,WAAW;AAC1D;AAEA;;;;;;;;;;;;;;;;;CAiBC,GAED;;;;;;;;;;;;;;;CAeC,GACD,OAAO,MAAMC,yBAAyB,IAAG;AAEzC,oEAAoE,GACpE,OAAO,MAAMC,6BAA6B,MAAM;AAEhD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8BC,GACD,OAAO,SAASC,uBAAuBC,OAAe;IACpD,OAAO,CAAC,WAAW,EAAEA,WAAW,WAAW;AAC7C"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/plugin-api-rate-limit.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 { isPluginMachineRoute } from './api-plugins'\n\n/**\n * Policy for the tenant plugin-API dispatcher's visitor-write rate limit\n * (AGL-1770). Pure but for one read of the route registry, for a route its\n * plugin registered as a machine's, so it is unit-testable without a route\n * harness — the durable half lives in `@aglyn/tenant-data-admin` beside\n * `visitorWriteRefusal`, exactly like the lockdown split.\n *\n * ## What this bounds\n *\n * `apps/tenant/app/api/[...pluginApi]/route.ts` dispatches every plugin's\n * visitor-facing handler and, before this, applied no rate limit of any kind.\n * The gates it did have each refuse something narrower: per-site enablement is\n * skipped by an unresolvable `hostId`, the release gate only checks that the\n * plugin is on, and `visitorWriteRefusal` refuses only a paused or suspended\n * site. None of them bounds volume from a working, enabled site — which is\n * every live merchant.\n *\n * AGL-1769 closed the *shape* of the unauthenticated cart write (the caller\n * chose the document path) and was explicit that it did not bound the\n * *quantity*: \"id control never multiplied volume; it bought shape\". This is\n * the other half. The cost of the unbounded half lands in the merchant's own\n * Firestore — document count and storage on a tenant who did nothing wrong.\n */\n\n/**\n * 120 writes per minute per (site, client IP).\n *\n * Deliberately generous, and the reasoning matters more than the number: a\n * limit that trips on genuine use is worse than no limit, because it gets\n * raised until it means nothing. Measured against what the storefront actually\n * emits:\n *\n * - `cart.tsx:214` puts the line-quantity `TextField`'s `onChange` straight\n * onto a POST with no debounce, so *typing* `100` into a quantity box is\n * three cart writes. Editing a few lines is a genuine ten-to-twenty-write\n * burst in a couple of seconds.\n * - `experiments/track` is an A/B beacon: one exposure write per experiment\n * per page view, so a visitor clicking through pages at ~1/s with two live\n * experiments sustains ~2 writes/s legitimately.\n * - Every visitor write on a site shares ONE budget (see the key below), so\n * these add rather than each getting their own headroom.\n *\n * What a tighter number would actually buy is small. At 120/min a single\n * source can still mint ~172k documents a day against one site; at 30/min it\n * is ~43k. Neither is \"safe\" in absolute terms — what the cap buys is turning\n * a one-line flood script from *millions* per day into something that needs a\n * distributed botnet to stay interesting, and turning an incident from\n * instantaneous into detectable. That property is bought at 120 as well as at\n * 30, and 30 would refuse real shoppers.\n *\n * 120/60s is also `DEFAULT_RATE_LIMIT` / `DEFAULT_RATE_WINDOW_MS` — the same\n * pair the public REST API enforces — so this adds no bespoke number.\n */\nexport const VISITOR_WRITE_RATE_LIMIT = 120\n\n/**\n * Fixed 60s window, matching every other limiter in the codebase rather than\n * improving on it. A fixed window admits up to 2× the cap across a boundary;\n * consistency with the shared bucket ids, the degradation markers and\n * `/api/health/rate-limits` is worth more here than closing that.\n */\nexport const VISITOR_WRITE_RATE_WINDOW_MS = 60_000\n\n/**\n * Machine-caller paths on EITHER plugin-API surface: neither dispatcher rate\n * limits these.\n *\n * The membership rule, so this list is decidable rather than a taste call: the\n * caller proves a shared secret, a webhook signature or a console session that\n * a visitor has no way to obtain, and its request volume is set by campaign\n * size or cron schedule rather than by human behaviour. A 120/min cap does not\n * merely inconvenience these — Resend delivering open/click webhooks for one\n * 50k-recipient campaign arrives from a handful of IPs for a single host and\n * would be shredded by any shopper-sized ceiling.\n *\n * ### Why a path list is right here when the dispatcher argues against them\n *\n * The lockdown gate two lines below is keyed by METHOD precisely to avoid a\n * path list, \"for the same reason `useSiteFetch` draws the Preview boundary\n * that way: a per-path list means thirteen handlers each remembering and the\n * fourteenth silently not.\"\n *\n * That argument turns on POLARITY, and this list has the opposite one. The\n * lockdown list would have enumerated what to *protect*, so a forgotten entry\n * is silently unprotected — the failure you never find. This enumerates what\n * to *exempt*, so the default is limited: a new visitor endpoint added\n * tomorrow is covered by doing nothing, and a forgotten machine endpoint\n * fails loudly and recoverably as 429s with `Retry-After` on a caller that\n * retries. Safe default, noisy mistake.\n *\n * Each entry, with the credential that makes it a machine caller:\n *\n * - `email/events` — Resend's webhook; `verifySvix` over `svix-id`/\n * `svix-timestamp`/`svix-signature` against `RESEND_WEBHOOK_SECRET`.\n * - `campaigns/send` — a Firebase ID token with a host member role, or\n * `CRON_SECRET` for the scheduled-send path.\n * - `bookings/reminders` — `x-cron-secret` against `CRON_SECRET`; refuses\n * outright when it is unset.\n * - `hooks/…` — the merchant's per-hook secret in `x-aglyn-secret`, compared\n * with `timingSafeEqual`. It already carries its own 30/min per-hook\n * limiter, so exempting it here removes nothing.\n *\n * The four below are registered on the CONSOLE surface only, and each is a\n * chunked cron sweep rather than a single beat — `sweepConsoleCron` follows\n * `nextCursor` up to `CRON_SWEEP_MAX_CHUNKS` (50) POSTs per route per run,\n * from one Cloud Functions address, and the key below carries no path. Three\n * such routes on one beat is 150 requests in a minute against a single\n * bucket, so this exemption is load-bearing and not decorative:\n *\n * - `campaigns/process-scheduled` — `x-cron-secret` against `CRON_SECRET`;\n * refuses outright when it is unset.\n * - `lists/materialize` — the same header, the same refusal.\n * - `commerce/process-abandoned` — the same header, the same refusal.\n * - `commerce/process-restock` — the same header, the same refusal.\n *\n * Note what is deliberately NOT exempt: presence of an `authorization` or\n * `svix-signature` HEADER. Header presence is not a credential — the\n * dispatcher cannot verify signatures it does not own, so exempting on the\n * header would let any caller skip the limiter by attaching a garbage one to\n * a cart POST.\n *\n * Nor is `commerce/supplier-update`, whose HMAC token makes it a machine\n * caller but whose volume is set by a supplier's own system rather than by a\n * schedule. It fails the second half of the membership rule, and the polarity\n * above says which way to resolve a borderline case: leave it limited, where\n * a mistake is a recoverable 429.\n */\nconst MACHINE_API_PATHS: ReadonlySet<string> = new Set([\n 'email/events',\n 'campaigns/send',\n 'bookings/reminders',\n 'campaigns/process-scheduled',\n 'lists/materialize',\n 'commerce/process-abandoned',\n 'commerce/process-restock',\n])\n\n/** Prefixes whose whole subtree is a machine surface (`hooks/{host}/{hook}`). */\nconst MACHINE_API_PREFIXES: readonly string[] = ['hooks/']\n\n/** Leading/trailing slashes stripped, matching `normalizeApiPath`. */\nfunction normalize(path: string): string {\n return String(path ?? '').replace(/^\\/+|\\/+$/g, '')\n}\n\n/**\n * Is this dispatcher path a credentialed machine surface (exempt), rather\n * than a visitor one (limited)? Unknown paths are visitor by default.\n *\n * A path is a machine's when it is on the list above, or when the plugin\n * that registered it declared it one (`machine: true`, AGL-3080) — the same\n * polarity as the list: a route is exempt only by saying so, and one that\n * forgot is limited, which fails loudly as a 429 its scheduler retries.\n */\nexport function isMachinePluginApiPath(path: string): boolean {\n const normalized = normalize(path)\n if (MACHINE_API_PATHS.has(normalized)) return true\n if (MACHINE_API_PREFIXES.some((prefix) => normalized.startsWith(prefix))) return true\n return isPluginMachineRoute(normalized)\n}\n\n/**\n * The limiter key: one bucket per (site, client IP).\n *\n * ## Why compound, and why NOT a second limiter in either direction\n *\n * **Per-site alone is rejected.** A single attacker would consume the whole\n * merchant's allowance and every real shopper would then be refused — an\n * attacker-triggered denial of service against the victim, cheaper to mount\n * than the flood it replaces and costing the merchant *sales* rather than\n * storage. That is strictly worse than the growth it guards.\n *\n * **Per-IP alone is rejected.** Trivially distributed, and it would let one\n * merchant's traffic spend another's budget.\n *\n * **Compound is what \"per-host and per-IP together\" should mean here.** Each\n * source is bounded on each site, and no source can affect another's budget,\n * so the ceiling a site is exposed to is (distinct source IPs) × 120/min —\n * the same shape legitimate traffic has. What it does not stop is a genuine\n * botnet, and that is stated rather than papered over: bounding *that* needs\n * an edge WAF, not an application limiter, and every application-level scheme\n * that would catch it (a site-wide ceiling) reintroduces the self-DoS above.\n *\n * **A second, platform-wide per-IP limiter was considered and dropped.** It\n * would bound one source sweeping many merchants, but costs a second\n * Firestore transaction on every storefront write — tripling the ops on a\n * legitimate add-to-cart — to bound an attack that the compound key already\n * prices at one full budget per merchant. One transaction per visitor write\n * is the established cost here (`forms/submit`, `protection/unlock`); two\n * would be novel and is not paid for by what it buys.\n *\n * The path is **not** in the key, on purpose: one shared budget per (site,\n * IP) across cart, reviews, newsletter, bookings and beacons. Keying per path\n * would let a caller multiply its allowance by cycling endpoints, which is\n * free to do and defeats the point.\n *\n * `hostId` is frequently `''` — the dispatcher only resolves one from the\n * query or a JSON body, and a handler that self-gates may supply none. It is\n * kept in the key rather than skipped: AGL-1769 named \"an unresolvable\n * `hostId` skips the gate\" as an existing hole, and a limiter that a caller\n * turns off by declining to name a site is not a limiter. Host-less writes\n * simply share one bucket per IP.\n */\nexport function visitorWriteRateLimitKey(hostId: string, ip: string): string {\n return `pluginwrite:${hostId || '-'}:${ip || 'unknown'}`\n}\n\n/**\n * Policy for the CONSOLE plugin-API dispatcher's write rate limit.\n *\n * `apps/console/app/api/[...pluginApi]/route.ts` dispatches every plugin's\n * console-facing handler — the marketplace installs and publishes, `ai/assist`,\n * gift cards, POS orders, refunds, the email list previews and the suppression\n * writes — and it carries no limiter of any kind. Its gates each refuse\n * something narrower: per-site enablement needs a resolvable `hostId`, the\n * release flag only asks whether the plugin is on, and lockdown only refuses\n * during an incident. None of them bounds volume from an ordinary signed-in\n * operator, which is every request the surface serves.\n *\n * The exposure is not the same one the visitor limiter closes. These handlers\n * are authenticated, so nobody is minting documents anonymously; what they do\n * instead is spend REAL money per call — a contact scan with a read budget, a\n * bundle download and verify, a model call to a paid vendor — on a surface\n * where a stuck retry loop in one console tab is indistinguishable from abuse.\n */\n\n/**\n * 120 requests per minute per console subject.\n *\n * `DEFAULT_RATE_LIMIT` / `DEFAULT_RATE_WINDOW_MS` again, and the same pair the\n * visitor limiter and the public REST API use, so this introduces no bespoke\n * number to reason about. It is far above a person pressing buttons: the\n * costly routes on this surface are one-per-click, and the console's own\n * page-load traffic reaches Firestore through the client SDK rather than\n * through this dispatcher.\n *\n * Deliberately not tighter even though each call here costs more than a cart\n * write. A limit that trips on genuine use gets raised until it means nothing,\n * and the per-call cost is the wrong lever for that anyway — an entitlement or\n * a quota bounds spend per org over a month, where this bounds a runaway loop\n * over a minute. Two different jobs; this one only has to make the loop stop.\n */\nexport const CONSOLE_API_RATE_LIMIT = 120\n\n/** Fixed 60s window, matching every other limiter in the codebase. */\nexport const CONSOLE_API_RATE_WINDOW_MS = 60_000\n\n/**\n * The limiter key: one bucket per authenticated subject.\n *\n * ## Why this is NOT the visitor key\n *\n * `visitorWriteRateLimitKey` is compound because its surface is\n * unauthenticated, and both halves of that argument are answers to anonymity.\n * Per-site alone was rejected there because one attacker would spend a whole\n * merchant's allowance and refuse real shoppers — an attacker-triggered denial\n * of service against the victim. On the console the subject is a verified\n * `uid`, so a caller can only ever refuse ITSELF: the self-DoS the compound key\n * exists to prevent cannot be aimed at anyone else, and the isolation it buys\n * is already bought by the identity.\n *\n * `hostId` is therefore left OUT rather than added, which is the opposite of\n * the visitor key and for the reason the visitor key leaves the PATH out: an\n * operator with fifty sites would otherwise hold fifty budgets and could\n * multiply its allowance by cycling a field it controls. One person is one\n * budget across every site they can reach.\n *\n * ## The unauthenticated fallback\n *\n * A caller whose bearer token did not decode — a plugin key, a POS device, an\n * anonymous probe — has no `uid`, and its client address stands in. That\n * bucket is shared by every such caller behind one address, which is the\n * conservative direction: an unidentified caller on an authenticated surface\n * should not get a private allowance for declining to identify itself. Keeping\n * it counted at all is the point. A limiter a caller switches off by sending\n * no credential is not a limiter, and this surface's whole exposure is what\n * runs BEFORE a handler decides who is asking.\n */\nexport function consoleApiRateLimitKey(subject: string): string {\n return `consoleapi:${subject || 'unknown'}`\n}\n"],"names":["isPluginMachineRoute","VISITOR_WRITE_RATE_LIMIT","VISITOR_WRITE_RATE_WINDOW_MS","MACHINE_API_PATHS","Set","MACHINE_API_PREFIXES","normalize","path","String","replace","isMachinePluginApiPath","normalized","has","some","prefix","startsWith","visitorWriteRateLimitKey","hostId","ip","CONSOLE_API_RATE_LIMIT","CONSOLE_API_RATE_WINDOW_MS","consoleApiRateLimitKey","subject"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,oBAAoB,QAAQ,mBAAe;AAEpD;;;;;;;;;;;;;;;;;;;;;;CAsBC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4BC,GACD,OAAO,MAAMC,2BAA2B,IAAG;AAE3C;;;;;CAKC,GACD,OAAO,MAAMC,+BAA+B,MAAM;AAElD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+DC,GACD,MAAMC,oBAAyC,IAAIC,IAAI;IACrD;IACA;IACA;IACA;IACA;IACA;IACA;CACD;AAED,+EAA+E,GAC/E,MAAMC,uBAA0C;IAAC;CAAS;AAE1D,oEAAoE,GACpE,SAASC,UAAUC,IAAY;IAC7B,OAAOC,OAAOD,eAAAA,OAAQ,IAAIE,OAAO,CAAC,cAAc;AAClD;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASC,uBAAuBH,IAAY;IACjD,MAAMI,aAAaL,UAAUC;IAC7B,IAAIJ,kBAAkBS,GAAG,CAACD,aAAa,OAAO;IAC9C,IAAIN,qBAAqBQ,IAAI,CAAC,CAACC,SAAWH,WAAWI,UAAU,CAACD,UAAU,OAAO;IACjF,OAAOd,qBAAqBW;AAC9B;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyCC,GACD,OAAO,SAASK,yBAAyBC,MAAc,EAAEC,EAAU;IACjE,OAAO,CAAC,YAAY,EAAED,UAAU,IAAI,CAAC,EAAEC,MAAM,WAAW;AAC1D;AAEA;;;;;;;;;;;;;;;;;CAiBC,GAED;;;;;;;;;;;;;;;CAeC,GACD,OAAO,MAAMC,yBAAyB,IAAG;AAEzC,oEAAoE,GACpE,OAAO,MAAMC,6BAA6B,MAAM;AAEhD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8BC,GACD,OAAO,SAASC,uBAAuBC,OAAe;IACpD,OAAO,CAAC,WAAW,EAAEA,WAAW,WAAW;AAC7C"}
|
|
@@ -496,11 +496,11 @@ const BASE64_BLOB = /^[A-Za-z0-9+/\r\n]+={0,2}$/;
|
|
|
496
496
|
};
|
|
497
497
|
missing('the site component', (_found_site = found.site) == null ? void 0 : _found_site.components, (_declared_site = declared.site) == null ? void 0 : _declared_site.components, 'a published page that places it would never load the plugin');
|
|
498
498
|
missing('the site feature', (_found_site1 = found.site) == null ? void 0 : _found_site1.features, (_declared_site1 = declared.site) == null ? void 0 : _declared_site1.features, 'no published page would ever load the plugin to run it');
|
|
499
|
-
missing('a widget in the console slot', (_found_console = found.console) == null ? void 0 : _found_console.slots, (_declared_console = declared.console) == null ? void 0 : _declared_console.slots, 'a
|
|
499
|
+
missing('a widget in the console slot', (_found_console = found.console) == null ? void 0 : _found_console.slots, (_declared_console = declared.console) == null ? void 0 : _declared_console.slots, 'a console page that renders the slot would never load the plugin');
|
|
500
500
|
missing('the console route', (_found_console1 = found.console) == null ? void 0 : _found_console1.routes, (_declared_console1 = declared.console) == null ? void 0 : _declared_console1.routes, 'the route would not find the plugin that serves it');
|
|
501
501
|
missing('the console organization route', (_found_console2 = found.console) == null ? void 0 : _found_console2.orgRoutes, (_declared_console2 = declared.console) == null ? void 0 : _declared_console2.orgRoutes, 'the route would not find the plugin that serves it');
|
|
502
502
|
if (((_found_console3 = found.console) == null ? void 0 : _found_console3.shell) && !((_declared_console3 = declared.console) == null ? void 0 : _declared_console3.shell)) {
|
|
503
|
-
add('error', 'contributions', 'register() adds a nav tab, a provider or another entry the console ' + 'shell draws on every
|
|
503
|
+
add('error', 'contributions', 'register() adds a nav tab, a provider or another entry the console ' + 'shell draws on every page, and the manifest does not declare ' + '"console": { "shell": true } — the shell would draw it only on ' + 'pages that happened to load the plugin for something else');
|
|
504
504
|
}
|
|
505
505
|
if (detected.unresolved.length) {
|
|
506
506
|
add('warning', 'contributions', `the checker could not read ${detected.unresolved.join('; ')} — ` + 'confirm the declaration covers what it registers');
|