@aglyn/aglyn 1.0.0-beta.218 → 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 +11 -11
- 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/analytics-events.d.ts +1 -1
- package/src/lib/app-utils/analytics-events.js.map +1 -1
- package/src/lib/app-utils/analytics-provider.d.ts +182 -0
- package/src/lib/app-utils/analytics-provider.js +133 -0
- package/src/lib/app-utils/analytics-provider.js.map +1 -0
- package/src/lib/app-utils/api-plugins.d.ts +46 -0
- package/src/lib/app-utils/api-plugins.js +19 -0
- package/src/lib/app-utils/api-plugins.js.map +1 -1
- package/src/lib/app-utils/{marketplace-overrides.d.ts → artifact-overrides.d.ts} +9 -6
- package/src/lib/app-utils/{marketplace-overrides.js → artifact-overrides.js} +10 -7
- package/src/lib/app-utils/artifact-overrides.js.map +1 -0
- package/src/lib/app-utils/{marketplace-provenance.d.ts → artifact-provenance.d.ts} +12 -10
- package/src/lib/app-utils/{marketplace-provenance.js → artifact-provenance.js} +9 -7
- package/src/lib/app-utils/artifact-provenance.js.map +1 -0
- package/src/lib/app-utils/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/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/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.js +1 -1
- package/src/lib/app-utils/screen-route.js.map +1 -1
- package/src/lib/app-utils/seo-audit.d.ts +132 -0
- package/src/lib/app-utils/seo-audit.js +246 -0
- package/src/lib/app-utils/seo-audit.js.map +1 -0
- package/src/lib/app-utils/seo-keywords.d.ts +48 -0
- package/src/lib/app-utils/seo-keywords.js +60 -0
- package/src/lib/app-utils/seo-keywords.js.map +1 -0
- package/src/lib/app-utils/seo-page-facts.d.ts +66 -0
- package/src/lib/app-utils/seo-page-facts.js +155 -0
- package/src/lib/app-utils/seo-page-facts.js.map +1 -0
- package/src/lib/app-utils/seo-site-scan.d.ts +126 -0
- package/src/lib/app-utils/seo-site-scan.js +162 -0
- package/src/lib/app-utils/seo-site-scan.js.map +1 -0
- package/src/lib/app-utils/seo-title-variables.js +1 -1
- package/src/lib/app-utils/seo-title-variables.js.map +1 -1
- package/src/lib/app-utils/server.d.ts +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} +2 -2
- package/src/lib/app-utils/site-theme.js.map +1 -0
- package/src/lib/app-utils/sitemap.d.ts +4 -3
- package/src/lib/app-utils/sitemap.js +9 -7
- package/src/lib/app-utils/sitemap.js.map +1 -1
- package/src/lib/app-utils/stale-build-error.d.ts +51 -0
- package/src/lib/app-utils/stale-build-error.js +100 -1
- package/src/lib/app-utils/stale-build-error.js.map +1 -1
- package/src/lib/app-utils/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/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 +9 -135
- package/src/lib/foundation/definitions/org-billing.types.js.map +1 -1
- package/src/lib/foundation/definitions/organization.types.d.ts +1 -3
- package/src/lib/foundation/definitions/organization.types.js.map +1 -1
- package/src/lib/foundation/definitions/platform.types.d.ts +11 -5
- package/src/lib/foundation/definitions/platform.types.js.map +1 -1
- package/src/lib/foundation/definitions/write-deny-coverage.util.d.ts +16 -0
- package/src/lib/foundation/definitions/write-deny-coverage.util.js +20 -0
- package/src/lib/foundation/definitions/write-deny-coverage.util.js.map +1 -1
- package/src/lib/foundation/index.d.ts +1 -1
- package/src/lib/foundation/index.js.map +1 -1
- package/src/lib/plugin-manager/besigner-documents.d.ts +96 -0
- package/src/lib/plugin-manager/besigner-documents.js +61 -0
- package/src/lib/plugin-manager/besigner-documents.js.map +1 -0
- package/src/lib/plugin-manager/computed-variables.d.ts +71 -0
- package/src/lib/plugin-manager/computed-variables.js +74 -0
- package/src/lib/plugin-manager/computed-variables.js.map +1 -0
- package/src/lib/plugin-manager/custom-fields.d.ts +29 -2
- package/src/lib/plugin-manager/custom-fields.js +47 -1
- package/src/lib/plugin-manager/custom-fields.js.map +1 -1
- package/src/lib/plugin-manager/feature-plugins.d.ts +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 +784 -10
- 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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/artifact-provenance.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 * The kinds of content a workspace can INSTALL rather than author (AGL-654).\n *\n * Installed content is a platform concept with more readers than writers:\n * whatever installs a component, a theme or an email template stamps where it\n * came from (below), and the theme editor, a template's page, the site's theme\n * resolution and the AI plugin all read that stamp without knowing who wrote\n * it. So the contract is core's and names no installer (AGL-3080). The\n * marketplace, which is today's only installer, re-exports the union under the\n * name its publish routes use, `MarketplaceArtifactType`.\n */\nexport type InstallableArtifactType =\n | 'component'\n | 'template'\n | 'plugin'\n | 'layout'\n | 'datasetSchema'\n | 'emailTemplate'\n /**\n * A campaign email design, installed as a new `kind: 'email'` screen.\n *\n * Distinct from `emailTemplate`, which is a design FOR one entry in the\n * fixed transactional catalog and can only ever land on that same key. A\n * starter belongs to no key: it is a blank-page replacement for the composer,\n * so it installs as a new document and the tenant may install ten of them.\n * Folding the two together would give one install button two behaviors and\n * would make the catalog key an absent-field discriminator.\n */\n | 'emailStarter'\n | 'theme'\n\n/**\n * Install provenance (AGL-1015): where an installed artifact came from, at\n * what version, and what it looked like when it arrived.\n *\n * Every install route used to stamp its own shape — plugins wrote a pin,\n * dataset schemas a `source`, email templates an `installedFrom`, components a\n * `marketplace` — and none of them kept the content as installed. Without the\n * original there is no way to tell the publisher's change from the user's, so\n * an update can only overwrite. This module defines the one shape all install\n * routes write and the one reader everything else asks.\n */\n\n/**\n * Immutable, content-addressed base snapshots — the content exactly as\n * installed, keyed by its own hash and stored outside the user's edit path.\n *\n * Top-level rather than per-host on purpose: the same listing version installed\n * onto twenty sites is one document, and re-installing it rewrites nothing.\n */\nexport const ARTIFACT_BASE_COLLECTION = 'marketplaceArtifactBases'\n\n/**\n * Firestore's per-document ceiling is 1 MiB. Snapshots are stored whole, so\n * anything near it is refused rather than truncated — a partial base is worse\n * than no base, because a diff would report the missing half as user edits.\n */\nexport const ARTIFACT_BASE_MAX_BYTES = 800 * 1024\n\n/** The provenance stamp written on every installed artifact (AGL-1015). */\nexport interface InstalledFrom {\n listingId: string\n /** The listing version installed, as a string; null when unversioned. */\n version: string | null\n /** Content hash of the base snapshot — the key into {@link ARTIFACT_BASE_COLLECTION}. */\n sha256: string | null\n artifactType: InstallableArtifactType\n installedAt: unknown\n /** The publishing org (`listing.profileId`, org-owned since AGL-652). */\n publisherOrgId: string | null\n /**\n * What review said about THE VERSION INSTALLED, recorded at install time.\n *\n * Optional because six of the seven artifact types are auto-listed and have\n * no verdict to record. Where it is written it is a fact about one version\n * and is never recomputed from the listing afterwards: a listing that earns\n * `reviewStatus: 'verified'` next month did not retroactively review the\n * bytes installed today, and a surface that resolved assurance live would\n * say it did. Stamping it is what keeps \"unreviewed\" from decaying into a\n * silence a reader mistakes for approval.\n */\n assurance?: 'approved' | 'unreviewed' | 'rejected'\n}\n\n/**\n * How much is actually known about where an artifact came from.\n *\n * `inferred` exists because the marketplace shipped before this stamp did:\n * pre-AGL-1015 installs carry a listing id somewhere but no hash and no base.\n * Calling those `recorded` would let an update think it has an origin to merge\n * against; calling them `unknown` would throw away a listing id we really do\n * have. They are their own state, and the UI says \"installed before update\n * tracking\" rather than implying either.\n */\nexport type ProvenanceState = 'recorded' | 'inferred' | 'unknown'\n\nexport interface ResolvedProvenance {\n state: ProvenanceState\n listingId: string | null\n version: string | null\n sha256: string | null\n artifactType: InstallableArtifactType | null\n publisherOrgId: string | null\n /**\n * A base snapshot exists, so this artifact can be diffed and safely updated.\n * False for everything installed before the stamp — and saying so plainly is\n * the point: a merge built on a fabricated origin destroys work silently.\n */\n updatable: boolean\n}\n\nconst UNKNOWN_PROVENANCE: ResolvedProvenance = {\n state: 'unknown',\n listingId: null,\n version: null,\n sha256: null,\n artifactType: null,\n publisherOrgId: null,\n updatable: false,\n}\n\nfunction asVersion(value: unknown): string | null {\n return value == null || value === '' ? null : String(value)\n}\n\n/**\n * Reads provenance off an installed document, tolerating every shape the\n * install routes wrote before AGL-1015.\n *\n * The legacy fields are read rather than migrated: a batch backfill could only\n * ever recover the listing id and version, which is exactly what this recovers\n * lazily, and it cannot invent a base snapshot. Installs re-stamp themselves\n * properly the next time they are updated.\n */\nexport function resolveProvenance(\n doc:\n | {\n installedFrom?: Partial<InstalledFrom> | null\n /** Component installs (`hosts/{h}/components`). */\n marketplace?: { listingId?: string; profileId?: string; version?: unknown } | null\n /** Template, layout and dataset-schema installs. */\n source?: { type?: string; listingId?: string; version?: unknown } | null\n /** Plugin pins are their own provenance: the pin IS the version. */\n pluginId?: string\n listingId?: string\n profileId?: string\n version?: unknown\n sha256?: string\n }\n | null\n | undefined,\n fallbackArtifactType?: InstallableArtifactType,\n): ResolvedProvenance {\n if (!doc) return UNKNOWN_PROVENANCE\n\n const stamped = doc.installedFrom\n // A full stamp is the only thing that carries a base snapshot. Email\n // templates have written a partial `installedFrom` since AGL-789, so the\n // discriminator is the hash and the artifact type, not the field's presence.\n if (stamped?.listingId && stamped.sha256 && stamped.artifactType) {\n return {\n state: 'recorded',\n listingId: stamped.listingId,\n version: asVersion(stamped.version),\n sha256: stamped.sha256,\n artifactType: stamped.artifactType as InstallableArtifactType,\n publisherOrgId: stamped.publisherOrgId ?? null,\n updatable: true,\n }\n }\n\n const legacy = stamped?.listingId\n ? { listingId: stamped.listingId, version: stamped.version, profileId: null }\n : doc.marketplace?.listingId\n ? {\n listingId: doc.marketplace.listingId,\n version: doc.marketplace.version,\n profileId: doc.marketplace.profileId ?? null,\n }\n : doc.source?.listingId\n ? {\n listingId: doc.source.listingId,\n version: doc.source.version,\n profileId: null,\n }\n : // A plugin pin is flat: `{listingId, version, sha256}` on the install\n // doc itself. It is the one artifact that never needed a snapshot —\n // the bytes are immutable and content-addressed already — so a pin\n // resolves as fully recorded.\n doc.listingId\n ? {\n listingId: doc.listingId,\n version: doc.version,\n profileId: doc.profileId ?? null,\n }\n : null\n if (!legacy) return UNKNOWN_PROVENANCE\n\n const isPin = Boolean(doc.pluginId && doc.sha256)\n return {\n state: isPin ? 'recorded' : 'inferred',\n listingId: legacy.listingId,\n version: asVersion(legacy.version),\n sha256: doc.sha256 ?? null,\n artifactType: isPin ? 'plugin' : (fallbackArtifactType ?? null),\n publisherOrgId: legacy.profileId,\n updatable: isPin,\n }\n}\n\n/**\n * Deterministic JSON with object keys in sorted order, so the same content\n * hashes to the same id no matter what order Firestore handed the fields back.\n *\n * `undefined` members are dropped exactly as `JSON.stringify` drops them, which\n * keeps `{a: 1}` and `{a: 1, b: undefined}` — the same document after a\n * Firestore round-trip — hashing alike.\n */\nexport function stableStringify(value: unknown): string {\n if (value === null || typeof value !== 'object') {\n return JSON.stringify(value ?? null)\n }\n if (Array.isArray(value)) {\n return `[${value.map((entry) => stableStringify(entry)).join(',')}]`\n }\n const entries = Object.entries(value as Record<string, unknown>)\n .filter(([, entryValue]) => entryValue !== undefined)\n .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))\n .map(([key, entryValue]) => `${JSON.stringify(key)}:${stableStringify(entryValue)}`)\n return `{${entries.join(',')}}`\n}\n"],"names":["ARTIFACT_BASE_COLLECTION","ARTIFACT_BASE_MAX_BYTES","UNKNOWN_PROVENANCE","state","listingId","version","sha256","artifactType","publisherOrgId","updatable","asVersion","value","String","resolveProvenance","doc","fallbackArtifactType","stamped","installedFrom","legacy","profileId","marketplace","source","isPin","Boolean","pluginId","stableStringify","JSON","stringify","Array","isArray","map","entry","join","entries","Object","filter","entryValue","undefined","sort","a","b","key"],"mappings":"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"}
|
|
@@ -15,6 +15,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
15
15
|
* See the License for the specific language governing permissions and
|
|
16
16
|
* limitations under the License.
|
|
17
17
|
*/ import { isUncappedPlanComp, resolveOrgEntitlements, resolvePlanPricing } from "./plan-entitlements.js";
|
|
18
|
+
import { planQuotaOf } from "../plugin-manager/plugin-plan-entitlements.js";
|
|
18
19
|
/**
|
|
19
20
|
* Aglyn Assist credits: the unit assist is sold, metered and refused in.
|
|
20
21
|
*
|
|
@@ -193,7 +194,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
193
194
|
* reservation's ceiling (`assistMonthlyCeilingUsd` in the AI plugin) drops
|
|
194
195
|
* the default and keeps only an operator's explicit figure.
|
|
195
196
|
*/ export function resolveAssistCreditBudget(org) {
|
|
196
|
-
const credits =
|
|
197
|
+
const credits = planQuotaOf(resolveOrgEntitlements(org), 'assistCreditsPerMonth');
|
|
197
198
|
if (!Number.isFinite(credits) || credits <= 0) return null;
|
|
198
199
|
return Math.floor(credits);
|
|
199
200
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/assist-credits.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 {\n isUncappedPlanComp,\n resolveOrgEntitlements,\n resolvePlanPricing,\n} from './plan-entitlements'\nimport type { AglynOrgBilling } from '../foundation/definitions/org-billing.types'\n\n/**\n * Aglyn Assist credits: the unit assist is sold, metered and refused in.\n *\n * ## Why cost, and not messages\n *\n * Assist is measured per exchange as `estCostUsd` — the exchange priced at\n * the serving model's billed rates, computed where the tokens were counted. A\n * message allowance would throw that measurement away and price every action\n * the same. It cannot: a question is a few thousand tokens, while generating\n * a screen carries the node tree, the component catalog and the theme tokens\n * in, emits structured markup out, and iterates. The two differ by up to two\n * orders of magnitude, so one workspace's ten screen builds can outspend\n * another's thousand questions while both read as \"within allowance\".\n *\n * Metering in cost is the only unit under which those two workspaces draw\n * down their bands differently, which is the entire point.\n *\n * ## Why a credit rather than the dollar figure\n *\n * `estCostUsd` is a dollar figure off our own cost model, not a price.\n * Publishing it would put our model choice, our per-model rates and our\n * margin on a billing page, and it would move under customers every time a\n * model is swapped. A credit is a fixed quantity of that spend with a stable\n * public meaning, so the band on the plan card stays the same number when\n * the model behind it changes.\n *\n * Everything customer-facing counts credits. This module is the ONLY place\n * the two units meet.\n *\n * ## `estCostUsd` is the BILLED figure, not the bill we pay (AGL-3015)\n *\n * It is every exchange priced at the catalog's billed rates, which a model\n * may carry above what its provider charges. Credits are drawn from it, so\n * it is the right figure for a band, a wall, a cap and an invoice line. It\n * is the WRONG figure for a margin or a cost meter, which read the rollup's\n * provider figure through `assistProviderCostUsd` instead.\n */\n\n/**\n * What one credit is BILLED at, in USD.\n *\n * A tenth of a cent, chosen for two properties rather than for roundness:\n *\n * - **1,000 credits cost exactly $1.00**, so the per-1,000 retail rates on\n * `PLAN_PRICING.extraAssistCreditsUsdPer1k` read directly as multiples of\n * cost. A rate of $2.00 per 1,000 is cost x2 with no arithmetic in between,\n * which is what makes the 50% margin floor checkable by eye.\n * - **A single question is tens of credits, a generated screen is hundreds.**\n * At Sonnet list rates a short grounded answer runs around $0.013, so ~13\n * credits; a screen build runs into the hundreds. Both are whole numbers\n * with room to differ, which a cent-sized credit would have flattened to\n * \"1 vs 20\" and a dollar-sized one to \"0 vs 0\".\n *\n * ⚠️ This is a COST-MODEL constant, not a price and not a rate we publish.\n * It never appears on a customer surface; `assistCreditsFromUsd` is how a\n * measured dollar figure becomes something a customer may be shown.\n *\n * ⚠️ AND IT IS NOT WHAT A CREDIT COSTS US (AGL-3015). The dollar figure this\n * divides is priced at the model catalog's BILLED rates, and a model may be\n * billed above what the provider charges. A credit is therefore a tenth of a\n * cent of BILLED spend, and at most a tenth of a cent of provider spend. Read\n * against a retail rate it yields the floor of the line margin, never the\n * middle of it — which is the safe direction, and the direction every margin\n * assertion in this repo depends on. What a period actually cost is measured,\n * not derived: `assistProviderCostUsd` reads it off the rollup.\n */\nexport const ASSIST_CREDIT_COST_USD = 0.001\n\n/**\n * The field a monthly assist rollup, a signal row and a kind bucket keep\n * PROVIDER spend under, beside the `estCostUsd` a customer's credits are\n * drawn from (AGL-3015).\n *\n * Named here rather than in the meter that writes it because the readers are\n * spread across the plugin, the billing routes and the staff surfaces, and a\n * field name spelled out at six call sites is a field name that will be\n * spelled differently at the seventh.\n */\nexport const ASSIST_PROVIDER_COST_FIELD = 'providerCostUsd'\n\n/**\n * What a metered period ACTUALLY COST US, from the two dollar figures a\n * rollup carries.\n *\n * `billedUsd` is `estCostUsd`: every exchange priced at its model's billed\n * rates, and the figure credits are drawn from. `providerUsd` is\n * `ASSIST_PROVIDER_COST_FIELD`: the same exchanges priced at what the\n * provider charged. The second is the answer wherever it exists.\n *\n * ## Why a rollup may not carry one, and which way that errs\n *\n * The provider figure is written from the commit that split the two rates\n * onward. A period closed before it answers with the BILLED figure, which is\n * at or above what we paid — so a margin taken on history reads low and a\n * cost reads high, the direction a cost figure is allowed to be wrong in.\n * The one period that under-reads is the month in flight when the split\n * shipped, whose provider figure covers only the exchanges after it; that\n * month's error is bounded by the markup on a few days of a meter whose\n * largest measured organization is a fraction of a cent.\n */\nexport function assistProviderCostUsd(\n billedUsd: unknown,\n providerUsd: unknown,\n): number {\n const provider = Number(providerUsd)\n if (Number.isFinite(provider) && provider > 0) return provider\n const billed = Number(billedUsd)\n return Number.isFinite(billed) && billed > 0 ? billed : 0\n}\n\n/**\n * The minimum line margin every retail assist rate must clear.\n *\n * The same floor now applied to every retail rate in this codebase, and the\n * reason the assist ladder stops descending at cost x2 on Agency instead of\n * taking one more step the way the contacts ladder once did.\n */\nexport const ASSIST_CREDIT_MIN_MARGIN_PCT = 0.5\n\n/**\n * Credits -> our cost in USD. Non-finite or negative input is 0 credits, so a\n * corrupt band cannot become a budget of `NaN` that every comparison passes.\n */\nexport function assistUsdFromCredits(credits: number): number {\n const value = Number(credits)\n if (!Number.isFinite(value) || value <= 0) return 0\n return Math.round(value * ASSIST_CREDIT_COST_USD * 1_000_000) / 1_000_000\n}\n\n/**\n * Measured USD -> credits, rounded UP.\n *\n * Up, because this converts spend the platform has ALREADY made into the\n * figure a customer is shown against their band. Rounding down would let a\n * long tail of sub-credit exchanges cost real money and draw nothing, which\n * is the one direction a meter must not err in.\n */\nexport function assistCreditsFromUsd(usd: number): number {\n const value = Number(usd)\n if (!Number.isFinite(value) || value <= 0) return 0\n return Math.ceil(value / ASSIST_CREDIT_COST_USD)\n}\n\n/**\n * The org's monthly assist band in credits, or `null` when its plan sells no\n * assist band at all.\n *\n * ## `null` is not zero, and the difference is the whole safety property\n *\n * A band of 0 resolves to `null` — \"this org sells no band\" — and never to\n * a budget of `$0`. An org with no band still reaches the console\n * assistant's docs-grounded rung, which is bounded by the free daily message\n * cap and by the operator's spend backstop. Resolving zero as a budget would\n * refuse that rung outright: a whole assistant turned off by a pricing field\n * that was never about it.\n *\n * Since AGL-3203 no PLAN ROW bands at zero. Free carries the AGL-2925 taste\n * of `FREE_AI_TASTE_CREDITS_PER_MONTH` with no rate beside it, so it\n * resolves to a budget here and `assistBandRefuses` makes that budget a\n * wall; Starter carries 750 WITH a rate, so its budget is a line it meters\n * past rather than a wall. Both draw their docs-grounded chat on the same\n * band, which is fine — each is sized so a month of it costs well under a\n * dollar.\n *\n * So the zero case is now reached only by a per-org override that writes\n * one, and the reading above is what keeps that org's assistant running.\n *\n * The Aglyn AI add-on (AGL-2896) needs no knowledge here either way: the\n * resolver has already added `AI_ADDON_CREDITS_PER_MONTH[plan]` to the\n * band, because the add-on widens the one pool rather than opening a\n * second.\n *\n * The same reading is what makes the guard survive a stubbed entitlements\n * module. A test double that answers 0 for every quota produces `null` here,\n * which falls through to the operator backstop, rather than a budget of zero\n * that refuses everything while the assertions read green. A clamp that goes\n * green having refused every request has proved nothing, and 0 is exactly the\n * value a stub returns.\n *\n * ## Non-finite is also `null`, deliberately\n *\n * `Infinity` must never resolve to a budget. It is unrepresentable off-process\n * — `JSON.stringify(Infinity)` is `null` and reads back as 0 — so a band that\n * reached here as `Infinity` is a band that would read as zero somewhere\n * else. Falling through to the backstop is bounded and consistent; honouring\n * it would be an unbounded budget on one process and a refusal on the next.\n *\n * An UNCAPPED staff comp (AGL-3049) is where that `Infinity` comes from on\n * purpose, and `null` is right for it here too: there is no band to measure\n * against, be over, price or alert on. What `null` must NOT bring with it is\n * the backstop, whose repo default would cap the one workspace staff said\n * has no cap — so `assistBandRefuses` answers false for it, and the\n * reservation's ceiling (`assistMonthlyCeilingUsd` in the AI plugin) drops\n * the default and keeps only an operator's explicit figure.\n */\nexport function resolveAssistCreditBudget(\n org: Partial<AglynOrgBilling> | null | undefined,\n): number | null {\n const credits = Number(resolveOrgEntitlements(org).assistCreditsPerMonth)\n if (!Number.isFinite(credits) || credits <= 0) return null\n return Math.floor(credits)\n}\n\n/** The org's monthly assist band in USD of provider spend, or `null`. */\nexport function resolveAssistBudgetUsd(\n org: Partial<AglynOrgBilling> | null | undefined,\n): number | null {\n const credits = resolveAssistCreditBudget(org)\n return credits === null ? null : assistUsdFromCredits(credits)\n}\n\n/**\n * Credits spent past the band. A `null` band yields 0 — an org with no band\n * has nothing to be over, and reporting the whole month's spend as overage\n * there would invoice the free tier for its docs answers.\n */\nexport function assistCreditOverage(\n usedCredits: number,\n bandCredits: number | null,\n): number {\n const used = Number(usedCredits)\n if (!Number.isFinite(used) || used <= 0) return 0\n if (bandCredits === null) return 0\n const band = Number(bandCredits)\n if (!Number.isFinite(band) || band <= 0) return 0\n return Math.max(0, used - band)\n}\n\n/**\n * The per-1,000 rate the org's assist credits are sold at past its band, or\n * `null` when nothing is sold past it — the ONE reading of the rate\n * (AGL-2896). `priceAssistCreditOverage`, `assistBandRefuses`,\n * `assistRefusedByHardCap` and `assistHardCapRefusalText` all go through\n * it, so the gate, the invoice and the refusal sentence cannot quote three\n * different rates for one org.\n *\n * The plan's `extraAssistCreditsUsdPer1k` IS the answer — the whole answer,\n * since AGL-3203. Starter used to be the one exception: it banded at 0, so\n * the rate could not sit on its plan row without advertising a fee on a\n * quantity the plan never sold, and the add-on's band was priced from a\n * constant read here instead. Starter now includes 750 credits\n * unconditionally and carries $3.00 on its own row, so the exception and its\n * constant are gone and there is exactly one place the rate is written.\n *\n * Free and Enterprise are null with or without the add-on: Free sells no\n * add-on and its taste band is a wall by decision (AGL-2925), and\n * Enterprise's usage is in the contract.\n *\n * A STAFF COMP is null on every plan and with every add-on (AGL-3034),\n * because `resolvePlanPricing` — never `PLAN_PRICING` indexed directly —\n * sells nothing on a comp. That is what makes a comp's band a wall at the\n * gate and prices its overage to zero at the charge: the AI overage path\n * (AGL-3011) claims only what `assistMonthOverage` prices, so a comp never\n * becomes a Stripe invoice. The function is a single read for that reason;\n * a second source for the rate is how a comp gets quoted one.\n */\nexport function resolveAssistOverageRateUsdPer1k(\n org: Partial<AglynOrgBilling> | null | undefined,\n): number | null {\n return resolvePlanPricing(org).extraAssistCreditsUsdPer1k\n}\n\nexport interface AssistCreditOveragePrice {\n /** Credits past the plan's band, as handed in. */\n overageCredits: number\n /** Estimated charge at the plan's per-1,000 rate. */\n overageMonthlyUsd: number\n /** Per-1,000 rate; null when the plan prices no assist overage. */\n overageRateUsd: number | null\n}\n\n/**\n * Prices assist credits past a plan's band.\n *\n * ## No `allowed`, deliberately — and for the opposite reason to email\n *\n * `priceEmailSendOverage` omits an `allowed` field because email has two\n * gates and a transactional sender consulting the wrong one drops a password\n * reset. Assist omits it because it has exactly ONE gate and this is not it:\n * whether the band refuses is `assistBandRefuses`, applied inside\n * `reserveAssistMessage` before a token is spent. A second answer to \"may\n * this proceed\", computed after the fact from a rounded credit figure, would\n * be a second gate to drift from the first.\n *\n * ## Takes the overage, does not recompute it\n *\n * `assistCreditOverage` derives the excess once, from the measured meter and\n * the resolved band. Re-deriving it here would be a second overage model.\n *\n * A null rate yields zero structurally rather than by a check, which is what\n * keeps Free and Starter at zero on this axis.\n */\nexport function priceAssistCreditOverage(\n org: Partial<AglynOrgBilling> | null | undefined,\n overageCredits: number,\n): AssistCreditOveragePrice {\n const overageRateUsd = resolveAssistOverageRateUsdPer1k(org)\n const credits = Number(overageCredits)\n const over = Number.isFinite(credits) && credits > 0 ? credits : 0\n return {\n overageCredits: over,\n overageMonthlyUsd:\n overageRateUsd === null\n ? 0\n : Math.round((over / 1000) * overageRateUsd * 100) / 100,\n overageRateUsd,\n }\n}\n\n/**\n * The line margin a per-1,000-credit retail rate earns AT LEAST, as a\n * fraction.\n *\n * One expression, so the ladder and its floor cannot be checked against two\n * different definitions of margin. A null or non-positive rate has no margin\n * to report and answers `null` rather than 1 or 0 — \"this plan sells no\n * overage\" is not \"this plan sells overage at 100% margin\".\n *\n * A LOWER BOUND, not the realized figure (AGL-3015). The cost side is\n * `ASSIST_CREDIT_COST_USD`, which is what a credit BILLS at; a model billed\n * above its provider rate costs us less than that per credit, so the real\n * line margin is this or better and never worse. The floor stays checkable\n * by eye — a $2.00 rate is still cost x2 — and a retail rate that clears the\n * floor here clears it on every model mix. What a period realized is the\n * staff margin card's business, and it reads measured provider spend.\n */\nexport function assistCreditRateMarginPct(\n rateUsdPer1k: number | null,\n): number | null {\n if (rateUsdPer1k === null) return null\n const rate = Number(rateUsdPer1k)\n if (!Number.isFinite(rate) || rate <= 0) return null\n const costPer1k = ASSIST_CREDIT_COST_USD * 1000\n return (rate - costPer1k) / rate\n}\n\n/**\n * What a refused or in-progress workspace may be TOLD about its assist band.\n *\n * The reservation carries `costUsd` and `costLimitUsd` because that is what\n * it measured and what it compared against, and both are our provider bill.\n * Shipping the reservation itself to a browser publishes them. This is the\n * projection that crosses that boundary: credits only, no dollars, no model,\n * no rates.\n */\nexport interface PublicAssistCredits {\n /** Credits drawn this month, rounded up. */\n used: number\n /** The band, or `null` when no band applied to this request. */\n limit: number | null\n /** Credits left in the band, or `null` when there is no band. */\n remaining: number | null\n}\n\n/**\n * Projects a measured spend figure and a ceiling into the credit view.\n *\n * Both inputs are nullable because the reservation's are: `costUsd` is `null`\n * when nothing consulted the monthly document, and a ceiling is `null` when\n * none applied. Neither collapses to 0 here — \"nobody looked\" and \"spent\n * nothing\" have to stay distinguishable at the surface, exactly as they do on\n * the reservation.\n */\nexport function publicAssistCredits(\n costUsd: number | null,\n ceilingUsd: number | null,\n): PublicAssistCredits {\n const used = costUsd === null ? 0 : assistCreditsFromUsd(costUsd)\n if (ceilingUsd === null) return { used, limit: null, remaining: null }\n const limit = assistCreditsFromUsd(ceilingUsd)\n return { used, limit, remaining: Math.max(0, limit - used) }\n}\n\n/**\n * The console's name for the org's hard-cap switch (AGL-2653), quoted by the\n * refusal so the words on the 402 and the words beside the switch are one\n * string. Change it here and both move.\n */\nexport const ASSIST_HARD_CAP_CONTROL_LABEL = 'Stop AI assist at the included band'\n\n/** Where the switch lives, in the words the refusal uses. */\nexport const ASSIST_HARD_CAP_CONTROL_LOCATION = 'Billing → Usage'\n\n/**\n * Whether the org asked to be stopped at its assist band (AGL-2653).\n *\n * Strictly `=== true`: the map is absent on every org that has not touched\n * the switch, and absent is the selling default. A truthy string or a `1`\n * written by hand must not switch an assistant off at the band, so nothing\n * short of the boolean counts.\n */\nexport function resolveAssistHardCap(\n org: Partial<AglynOrgBilling> | null | undefined,\n): boolean {\n return org?.assistOverage?.hardCap === true\n}\n\n/**\n * Is the org's assist band a WALL, or a line past which credits are sold?\n *\n * Two things make it a wall, and only two:\n *\n * - **The org asked.** `assistOverage.hardCap` is the customer's own ceiling,\n * the `storageOverage.capUsd` of this meter.\n * - **There is nothing to sell past it at.** An org whose\n * `resolveAssistOverageRateUsdPer1k` is `null` has no rate to bill, so\n * credits past its band would be provider spend with no invoice line — the\n * silent free overage `plan-entitlements.spec.ts` forbids. Enterprise is\n * one case, its band being contractual; FREE is the other, its taste band\n * a wall by the AGL-2925 decision so that nothing about a Free workspace\n * can produce a charge.\n *\n * Everything else sells past the band by default, which is the 2026-09-07\n * decision this function encodes — and since AGL-3203 that includes Starter,\n * which carries 750 credits and a $3.00 rate like every paid tier above it.\n *\n * An org with NO band at all answers the same as Enterprise, harmlessly:\n * `resolveAssistBudgetUsd` is `null` for it and the reservation never\n * measures against a band. No plan ROW is in that state any more — Free and\n * Starter both were, at different times — so it is reached by a per-org\n * override that writes a zero.\n *\n * An UNCAPPED staff comp (AGL-3049) is neither: it has no band to be a wall,\n * and nothing is sold past a band it does not have. It answers false, ahead\n * of the switch — a workspace staff uncapped is not stopped by a band, and\n * the reservation reads that answer as \"no wall, no backstop default\".\n */\nexport function assistBandRefuses(\n org: Partial<AglynOrgBilling> | null | undefined,\n): boolean {\n if (isUncappedPlanComp(org)) return false\n if (resolveAssistHardCap(org)) return true\n return resolveAssistOverageRateUsdPer1k(org) === null\n}\n\n/**\n * Which ceiling refused a reservation, or `null` when it was admitted.\n *\n * - `messages`: the message cap (daily on the free rung, monthly entitled).\n * - `budget`: the operator's spend backstop.\n * - `band`: the plan's own band, as a wall — the switch, or a plan with\n * no rate to sell past it at.\n * - `cap`: the org's own dollar ceiling on overage (AGL-2898).\n *\n * The four that exist only on the Free taste (AGL-2925), each a precaution\n * against the one band that has no invoice behind it:\n *\n * - `account`: the ACCOUNT's 300 credits are spent across every free\n * workspace it owns — the org band alone would multiply by\n * the workspaces one person may hold.\n * - `requests`: the account's daily free request cap.\n * - `refusals`: the account's free generation is paused for the day after\n * too many `refusal` stops — a brief the model declined is a\n * brief that should not be retried thirty times.\n * - `platform`: the platform-wide daily ceiling on free spend; nobody's\n * fault, and \"try again tomorrow\" is the whole answer.\n *\n * Declared here, beside the helpers that read it, so the reservation, its\n * public projection and both doors name one union.\n */\nexport type AssistRefusedBy =\n | 'messages'\n | 'budget'\n | 'band'\n | 'cap'\n | 'account'\n | 'requests'\n | 'refusals'\n | 'platform'\n | null\n\n/**\n * The sentence a Free workspace is told when one of the taste's own\n * precautions refused it, or `null` when the refusal was something else.\n *\n * One string per rung, for both doors and the gate ladder, so a customer\n * reading the console panel and one reading the besigner get the same\n * words. Every sentence is customer-safe: it names what to do (upgrade,\n * wait) and never a figure from our cost model, a counter document, or\n * another workspace.\n */\nexport function assistFreeTasteRefusalText(\n refusedBy: AssistRefusedBy,\n): string | null {\n switch (refusedBy) {\n case 'account':\n return (\n 'Your free AI credits for this month are used across your ' +\n 'workspaces — upgrade any workspace to keep going.'\n )\n case 'requests':\n return (\n 'Free workspaces get a limited number of AI requests a day — ' +\n 'try again tomorrow, or upgrade to keep going.'\n )\n case 'refusals':\n return (\n 'The AI declined several requests today, so free AI generation is ' +\n 'paused until tomorrow. Try a different kind of brief then.'\n )\n case 'platform':\n return (\n 'Free AI generation is paused for the rest of today — try again ' +\n 'tomorrow. Paid workspaces are not affected.'\n )\n default:\n return null\n }\n}\n\n/**\n * Whether a refusal at the band was the org's OWN doing — the switch is on\n * AND turning it off would have let the exchange through. The refusal that\n * names the switch has to be the one the switch caused: on a plan with no\n * rate the band refuses whatever the switch says, and telling that customer\n * to turn it off would send them to a control that does nothing.\n */\nexport function assistRefusedByHardCap(\n org: Partial<AglynOrgBilling> | null | undefined,\n refusedBy: AssistRefusedBy,\n): boolean {\n return (\n refusedBy === 'band' &&\n resolveAssistHardCap(org) &&\n resolveAssistOverageRateUsdPer1k(org) !== null\n )\n}\n\n/**\n * The sentence a workspace is told when its own hard cap refused it. ONE\n * string for both assist entrypoints, so the console panel and the besigner\n * cannot describe the same control in two ways. It names the switch by its\n * label, says where it lives, and quotes the plan's rate so the reader knows\n * what turning it off costs.\n */\nexport function assistHardCapRefusalText(\n org: Partial<AglynOrgBilling> | null | undefined,\n): string {\n const rate = resolveAssistOverageRateUsdPer1k(org)\n const rateClause =\n rate === null ? '' : ` at $${rate.toFixed(2)} per 1,000 credits`\n return (\n `This workspace used its assistant credits for the month, and ` +\n `\"${ASSIST_HARD_CAP_CONTROL_LABEL}\" is on. Turn it off under ` +\n `${ASSIST_HARD_CAP_CONTROL_LOCATION} to keep going${rateClause}.`\n )\n}\n\nexport interface AssistMonthOverage extends AssistCreditOveragePrice {\n /** Credits drawn this month, rounded up from the measured spend. */\n usedCredits: number\n /** The band those credits were measured against, or `null` for no band. */\n bandCredits: number | null\n}\n\n/**\n * A month's assist overage from its measured spend — the ONE derivation the\n * invoice and every meter read (AGL-2653).\n *\n * Takes `estCostUsd` as the rollup reads it and composes the three steps\n * that already existed — credits from spend, overage past the band, price at\n * the plan's rate — so `report-usage` cannot assemble them in a different\n * order from a console readout and arrive at a different figure. The\n * rounding is the other overage terms' rounding: `priceAssistCreditOverage`\n * rounds to the cent, and the rollup turns dollars into cents with the same\n * `Math.round(usd * 100)` it applies to contacts, API and dataset storage.\n *\n * A non-finite or negative spend is zero credits, for the reason\n * `assistCreditsFromUsd` gives; a plan with no band or no rate prices zero\n * structurally, so Free, Starter and Enterprise bill nothing here without a\n * check that could be forgotten.\n */\nexport function assistMonthOverage(\n org: Partial<AglynOrgBilling> | null | undefined,\n estCostUsd: number,\n): AssistMonthOverage {\n const usedCredits = assistCreditsFromUsd(estCostUsd)\n const bandCredits = resolveAssistCreditBudget(org)\n return {\n usedCredits,\n bandCredits,\n ...priceAssistCreditOverage(\n org,\n assistCreditOverage(usedCredits, bandCredits),\n ),\n }\n}\n\n/** The console's name for the org's overage ceiling (AGL-2898). */\nexport const ASSIST_OVERAGE_CAP_CONTROL_LABEL =\n 'Stop AI when this month’s overage reaches'\n\n/**\n * Bounds on the ceiling a self-serve org may set. The floor is a dollar\n * because a ceiling of cents would refuse on the first exchange past the\n * band while reading as a limit; the roof is the storage cap's, so no\n * self-serve org writes itself an enterprise-sized commitment.\n */\nexport const ASSIST_OVERAGE_CAP_MIN_USD = 1\nexport const ASSIST_OVERAGE_CAP_MAX_USD = 100_000\n\n/**\n * The org's monthly ceiling on AI overage dollars, or `null` for none.\n *\n * Strictly a finite positive number. `null`, absent, a string, `NaN`,\n * `Infinity` and zero all read as no ceiling — the failure mode of the\n * alternative is a wall of `NaN` that every comparison passes, or a ceiling\n * of zero that refuses the first exchange past the band while the card\n * says nothing is set.\n */\nexport function resolveAssistOverageCapUsd(\n org: Partial<AglynOrgBilling> | null | undefined,\n): number | null {\n const raw = org?.assistOverage?.capUsd\n if (typeof raw !== 'number' || !Number.isFinite(raw) || raw <= 0) return null\n return raw\n}\n\n/**\n * Has this month's overage reached the org's own ceiling (AGL-2898)?\n *\n * Composed from `assistMonthOverage`, the ONE derivation the invoice reads,\n * so the figure the ceiling stops at is the figure the invoice would have\n * billed. That composition is also what confines the ceiling to plans that\n * sell past their band: a plan with no rate prices its overage to zero, and\n * zero never reaches a positive ceiling — Free and Enterprise cannot get here\n * without a check that could be forgotten. `>=` rather than `>`, because an\n * org that asked to stop AT a figure is stopped when the figure is met.\n */\nexport function assistOverageCapReached(\n org: Partial<AglynOrgBilling> | null | undefined,\n estCostUsd: number,\n): boolean {\n const cap = resolveAssistOverageCapUsd(org)\n if (cap === null) return false\n return assistMonthOverage(org, estCostUsd).overageMonthlyUsd >= cap\n}\n\n/**\n * Whether a refusal was the org's own ceiling. The reservation answers\n * `'cap'` only when `assistOverageCapReached` did, and that helper only\n * reaches a ceiling on a plan with a rate, so the two conditions restated\n * here are the same guarantee `assistRefusedByHardCap` gives the switch: the\n * control the sentence names is the control that refused.\n */\nexport function assistRefusedByOverageCap(\n org: Partial<AglynOrgBilling> | null | undefined,\n refusedBy: AssistRefusedBy,\n): boolean {\n return (\n refusedBy === 'cap' &&\n resolveAssistOverageCapUsd(org) !== null &&\n resolveAssistOverageRateUsdPer1k(org) !== null\n )\n}\n\n/**\n * The sentence a workspace is told when its own overage ceiling refused it.\n * ONE string for both assist entrypoints, as `assistHardCapRefusalText` is\n * for the switch. It names the control by its label, quotes the figure the\n * org chose, and says where to raise or clear it.\n */\nexport function assistOverageCapRefusalText(\n org: Partial<AglynOrgBilling> | null | undefined,\n): string {\n const cap = resolveAssistOverageCapUsd(org)\n const figure = cap === null ? '' : ` $${cap.toFixed(2)}`\n return (\n `This month’s AI assist overage reached${figure}, the figure you set as ` +\n `\"${ASSIST_OVERAGE_CAP_CONTROL_LABEL}\". Raise or remove it under ` +\n `${ASSIST_HARD_CAP_CONTROL_LOCATION} to keep going.`\n )\n}\n\n/**\n * The refusal sentence for whichever of the org's OWN controls refused, or\n * `null` when the refusal was nobody's control — the message cap, the\n * operator's backstop, or a plan that sells no overage. Both doors answer a\n * 402 with this text when it is a string and their usual 429 otherwise, so\n * the status and the sentence cannot disagree about whose decision it was.\n */\nexport function assistOwnControlRefusalText(\n org: Partial<AglynOrgBilling> | null | undefined,\n refusedBy: AssistRefusedBy,\n): string | null {\n if (assistRefusedByHardCap(org, refusedBy)) return assistHardCapRefusalText(org)\n if (assistRefusedByOverageCap(org, refusedBy)) {\n return assistOverageCapRefusalText(org)\n }\n return null\n}\n"],"names":["isUncappedPlanComp","resolveOrgEntitlements","resolvePlanPricing","ASSIST_CREDIT_COST_USD","ASSIST_PROVIDER_COST_FIELD","assistProviderCostUsd","billedUsd","providerUsd","provider","Number","isFinite","billed","ASSIST_CREDIT_MIN_MARGIN_PCT","assistUsdFromCredits","credits","value","Math","round","assistCreditsFromUsd","usd","ceil","resolveAssistCreditBudget","org","assistCreditsPerMonth","floor","resolveAssistBudgetUsd","assistCreditOverage","usedCredits","bandCredits","used","band","max","resolveAssistOverageRateUsdPer1k","extraAssistCreditsUsdPer1k","priceAssistCreditOverage","overageCredits","overageRateUsd","over","overageMonthlyUsd","assistCreditRateMarginPct","rateUsdPer1k","rate","costPer1k","publicAssistCredits","costUsd","ceilingUsd","limit","remaining","ASSIST_HARD_CAP_CONTROL_LABEL","ASSIST_HARD_CAP_CONTROL_LOCATION","resolveAssistHardCap","assistOverage","hardCap","assistBandRefuses","assistFreeTasteRefusalText","refusedBy","assistRefusedByHardCap","assistHardCapRefusalText","rateClause","toFixed","assistMonthOverage","estCostUsd","ASSIST_OVERAGE_CAP_CONTROL_LABEL","ASSIST_OVERAGE_CAP_MIN_USD","ASSIST_OVERAGE_CAP_MAX_USD","resolveAssistOverageCapUsd","raw","capUsd","assistOverageCapReached","cap","assistRefusedByOverageCap","assistOverageCapRefusalText","figure","assistOwnControlRefusalText"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,kBAAkB,EAClBC,sBAAsB,EACtBC,kBAAkB,QACb,yBAAqB;AAG5B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoCC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2BC,GACD,OAAO,MAAMC,yBAAyB,MAAK;AAE3C;;;;;;;;;CASC,GACD,OAAO,MAAMC,6BAA6B,kBAAiB;AAE3D;;;;;;;;;;;;;;;;;;;CAmBC,GACD,OAAO,SAASC,sBACdC,SAAkB,EAClBC,WAAoB;IAEpB,MAAMC,WAAWC,OAAOF;IACxB,IAAIE,OAAOC,QAAQ,CAACF,aAAaA,WAAW,GAAG,OAAOA;IACtD,MAAMG,SAASF,OAAOH;IACtB,OAAOG,OAAOC,QAAQ,CAACC,WAAWA,SAAS,IAAIA,SAAS;AAC1D;AAEA;;;;;;CAMC,GACD,OAAO,MAAMC,+BAA+B,IAAG;AAE/C;;;CAGC,GACD,OAAO,SAASC,qBAAqBC,OAAe;IAClD,MAAMC,QAAQN,OAAOK;IACrB,IAAI,CAACL,OAAOC,QAAQ,CAACK,UAAUA,SAAS,GAAG,OAAO;IAClD,OAAOC,KAAKC,KAAK,CAACF,QAAQZ,yBAAyB,WAAa;AAClE;AAEA;;;;;;;CAOC,GACD,OAAO,SAASe,qBAAqBC,GAAW;IAC9C,MAAMJ,QAAQN,OAAOU;IACrB,IAAI,CAACV,OAAOC,QAAQ,CAACK,UAAUA,SAAS,GAAG,OAAO;IAClD,OAAOC,KAAKI,IAAI,CAACL,QAAQZ;AAC3B;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmDC,GACD,OAAO,SAASkB,0BACdC,GAAgD;IAEhD,MAAMR,UAAUL,OAAOR,uBAAuBqB,KAAKC,qBAAqB;IACxE,IAAI,CAACd,OAAOC,QAAQ,CAACI,YAAYA,WAAW,GAAG,OAAO;IACtD,OAAOE,KAAKQ,KAAK,CAACV;AACpB;AAEA,uEAAuE,GACvE,OAAO,SAASW,uBACdH,GAAgD;IAEhD,MAAMR,UAAUO,0BAA0BC;IAC1C,OAAOR,YAAY,OAAO,OAAOD,qBAAqBC;AACxD;AAEA;;;;CAIC,GACD,OAAO,SAASY,oBACdC,WAAmB,EACnBC,WAA0B;IAE1B,MAAMC,OAAOpB,OAAOkB;IACpB,IAAI,CAAClB,OAAOC,QAAQ,CAACmB,SAASA,QAAQ,GAAG,OAAO;IAChD,IAAID,gBAAgB,MAAM,OAAO;IACjC,MAAME,OAAOrB,OAAOmB;IACpB,IAAI,CAACnB,OAAOC,QAAQ,CAACoB,SAASA,QAAQ,GAAG,OAAO;IAChD,OAAOd,KAAKe,GAAG,CAAC,GAAGF,OAAOC;AAC5B;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2BC,GACD,OAAO,SAASE,iCACdV,GAAgD;IAEhD,OAAOpB,mBAAmBoB,KAAKW,0BAA0B;AAC3D;AAWA;;;;;;;;;;;;;;;;;;;;CAoBC,GACD,OAAO,SAASC,yBACdZ,GAAgD,EAChDa,cAAsB;IAEtB,MAAMC,iBAAiBJ,iCAAiCV;IACxD,MAAMR,UAAUL,OAAO0B;IACvB,MAAME,OAAO5B,OAAOC,QAAQ,CAACI,YAAYA,UAAU,IAAIA,UAAU;IACjE,OAAO;QACLqB,gBAAgBE;QAChBC,mBACEF,mBAAmB,OACf,IACApB,KAAKC,KAAK,CAAC,AAACoB,OAAO,OAAQD,iBAAiB,OAAO;QACzDA;IACF;AACF;AAEA;;;;;;;;;;;;;;;;CAgBC,GACD,OAAO,SAASG,0BACdC,YAA2B;IAE3B,IAAIA,iBAAiB,MAAM,OAAO;IAClC,MAAMC,OAAOhC,OAAO+B;IACpB,IAAI,CAAC/B,OAAOC,QAAQ,CAAC+B,SAASA,QAAQ,GAAG,OAAO;IAChD,MAAMC,YAAYvC,yBAAyB;IAC3C,OAAO,AAACsC,CAAAA,OAAOC,SAAQ,IAAKD;AAC9B;AAoBA;;;;;;;;CAQC,GACD,OAAO,SAASE,oBACdC,OAAsB,EACtBC,UAAyB;IAEzB,MAAMhB,OAAOe,YAAY,OAAO,IAAI1B,qBAAqB0B;IACzD,IAAIC,eAAe,MAAM,OAAO;QAAEhB;QAAMiB,OAAO;QAAMC,WAAW;IAAK;IACrE,MAAMD,QAAQ5B,qBAAqB2B;IACnC,OAAO;QAAEhB;QAAMiB;QAAOC,WAAW/B,KAAKe,GAAG,CAAC,GAAGe,QAAQjB;IAAM;AAC7D;AAEA;;;;CAIC,GACD,OAAO,MAAMmB,gCAAgC,sCAAqC;AAElF,2DAA2D,GAC3D,OAAO,MAAMC,mCAAmC,kBAAiB;AAEjE;;;;;;;CAOC,GACD,OAAO,SAASC,qBACd5B,GAAgD;QAEzCA;IAAP,OAAOA,CAAAA,wBAAAA,qBAAAA,IAAK6B,aAAa,qBAAlB7B,mBAAoB8B,OAAO,MAAK;AACzC;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6BC,GACD,OAAO,SAASC,kBACd/B,GAAgD;IAEhD,IAAItB,mBAAmBsB,MAAM,OAAO;IACpC,IAAI4B,qBAAqB5B,MAAM,OAAO;IACtC,OAAOU,iCAAiCV,SAAS;AACnD;AAsCA;;;;;;;;;CASC,GACD,OAAO,SAASgC,2BACdC,SAA0B;IAE1B,OAAQA;QACN,KAAK;YACH,OACE,8DACA;QAEJ,KAAK;YACH,OACE,iEACA;QAEJ,KAAK;YACH,OACE,sEACA;QAEJ,KAAK;YACH,OACE,oEACA;QAEJ;YACE,OAAO;IACX;AACF;AAEA;;;;;;CAMC,GACD,OAAO,SAASC,uBACdlC,GAAgD,EAChDiC,SAA0B;IAE1B,OACEA,cAAc,UACdL,qBAAqB5B,QACrBU,iCAAiCV,SAAS;AAE9C;AAEA;;;;;;CAMC,GACD,OAAO,SAASmC,yBACdnC,GAAgD;IAEhD,MAAMmB,OAAOT,iCAAiCV;IAC9C,MAAMoC,aACJjB,SAAS,OAAO,KAAK,CAAC,KAAK,EAAEA,KAAKkB,OAAO,CAAC,GAAG,kBAAkB,CAAC;IAClE,OACE,CAAC,6DAA6D,CAAC,GAC/D,CAAC,CAAC,EAAEX,8BAA8B,2BAA2B,CAAC,GAC9D,GAAGC,iCAAiC,cAAc,EAAES,WAAW,CAAC,CAAC;AAErE;AASA;;;;;;;;;;;;;;;;CAgBC,GACD,OAAO,SAASE,mBACdtC,GAAgD,EAChDuC,UAAkB;IAElB,MAAMlC,cAAcT,qBAAqB2C;IACzC,MAAMjC,cAAcP,0BAA0BC;IAC9C,OAAO;QACLK;QACAC;OACGM,yBACDZ,KACAI,oBAAoBC,aAAaC;AAGvC;AAEA,iEAAiE,GACjE,OAAO,MAAMkC,mCACX,4CAA2C;AAE7C;;;;;CAKC,GACD,OAAO,MAAMC,6BAA6B,EAAC;AAC3C,OAAO,MAAMC,6BAA6B,OAAO;AAEjD;;;;;;;;CAQC,GACD,OAAO,SAASC,2BACd3C,GAAgD;QAEpCA;IAAZ,MAAM4C,MAAM5C,wBAAAA,qBAAAA,IAAK6B,aAAa,qBAAlB7B,mBAAoB6C,MAAM;IACtC,IAAI,OAAOD,QAAQ,YAAY,CAACzD,OAAOC,QAAQ,CAACwD,QAAQA,OAAO,GAAG,OAAO;IACzE,OAAOA;AACT;AAEA;;;;;;;;;;CAUC,GACD,OAAO,SAASE,wBACd9C,GAAgD,EAChDuC,UAAkB;IAElB,MAAMQ,MAAMJ,2BAA2B3C;IACvC,IAAI+C,QAAQ,MAAM,OAAO;IACzB,OAAOT,mBAAmBtC,KAAKuC,YAAYvB,iBAAiB,IAAI+B;AAClE;AAEA;;;;;;CAMC,GACD,OAAO,SAASC,0BACdhD,GAAgD,EAChDiC,SAA0B;IAE1B,OACEA,cAAc,SACdU,2BAA2B3C,SAAS,QACpCU,iCAAiCV,SAAS;AAE9C;AAEA;;;;;CAKC,GACD,OAAO,SAASiD,4BACdjD,GAAgD;IAEhD,MAAM+C,MAAMJ,2BAA2B3C;IACvC,MAAMkD,SAASH,QAAQ,OAAO,KAAK,CAAC,EAAE,EAAEA,IAAIV,OAAO,CAAC,IAAI;IACxD,OACE,CAAC,sCAAsC,EAAEa,OAAO,wBAAwB,CAAC,GACzE,CAAC,CAAC,EAAEV,iCAAiC,4BAA4B,CAAC,GAClE,GAAGb,iCAAiC,eAAe,CAAC;AAExD;AAEA;;;;;;CAMC,GACD,OAAO,SAASwB,4BACdnD,GAAgD,EAChDiC,SAA0B;IAE1B,IAAIC,uBAAuBlC,KAAKiC,YAAY,OAAOE,yBAAyBnC;IAC5E,IAAIgD,0BAA0BhD,KAAKiC,YAAY;QAC7C,OAAOgB,4BAA4BjD;IACrC;IACA,OAAO;AACT"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/assist-credits.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 {\n isUncappedPlanComp,\n resolveOrgEntitlements,\n resolvePlanPricing,\n} from './plan-entitlements'\nimport { planQuotaOf } from '../plugin-manager/plugin-plan-entitlements'\nimport type { AglynOrgBilling } from '../foundation/definitions/org-billing.types'\n\n/**\n * Aglyn Assist credits: the unit assist is sold, metered and refused in.\n *\n * ## Why cost, and not messages\n *\n * Assist is measured per exchange as `estCostUsd` — the exchange priced at\n * the serving model's billed rates, computed where the tokens were counted. A\n * message allowance would throw that measurement away and price every action\n * the same. It cannot: a question is a few thousand tokens, while generating\n * a screen carries the node tree, the component catalog and the theme tokens\n * in, emits structured markup out, and iterates. The two differ by up to two\n * orders of magnitude, so one workspace's ten screen builds can outspend\n * another's thousand questions while both read as \"within allowance\".\n *\n * Metering in cost is the only unit under which those two workspaces draw\n * down their bands differently, which is the entire point.\n *\n * ## Why a credit rather than the dollar figure\n *\n * `estCostUsd` is a dollar figure off our own cost model, not a price.\n * Publishing it would put our model choice, our per-model rates and our\n * margin on a billing page, and it would move under customers every time a\n * model is swapped. A credit is a fixed quantity of that spend with a stable\n * public meaning, so the band on the plan card stays the same number when\n * the model behind it changes.\n *\n * Everything customer-facing counts credits. This module is the ONLY place\n * the two units meet.\n *\n * ## `estCostUsd` is the BILLED figure, not the bill we pay (AGL-3015)\n *\n * It is every exchange priced at the catalog's billed rates, which a model\n * may carry above what its provider charges. Credits are drawn from it, so\n * it is the right figure for a band, a wall, a cap and an invoice line. It\n * is the WRONG figure for a margin or a cost meter, which read the rollup's\n * provider figure through `assistProviderCostUsd` instead.\n */\n\n/**\n * What one credit is BILLED at, in USD.\n *\n * A tenth of a cent, chosen for two properties rather than for roundness:\n *\n * - **1,000 credits cost exactly $1.00**, so the per-1,000 retail rates on\n * `PLAN_PRICING.extraAssistCreditsUsdPer1k` read directly as multiples of\n * cost. A rate of $2.00 per 1,000 is cost x2 with no arithmetic in between,\n * which is what makes the 50% margin floor checkable by eye.\n * - **A single question is tens of credits, a generated screen is hundreds.**\n * At Sonnet list rates a short grounded answer runs around $0.013, so ~13\n * credits; a screen build runs into the hundreds. Both are whole numbers\n * with room to differ, which a cent-sized credit would have flattened to\n * \"1 vs 20\" and a dollar-sized one to \"0 vs 0\".\n *\n * ⚠️ This is a COST-MODEL constant, not a price and not a rate we publish.\n * It never appears on a customer surface; `assistCreditsFromUsd` is how a\n * measured dollar figure becomes something a customer may be shown.\n *\n * ⚠️ AND IT IS NOT WHAT A CREDIT COSTS US (AGL-3015). The dollar figure this\n * divides is priced at the model catalog's BILLED rates, and a model may be\n * billed above what the provider charges. A credit is therefore a tenth of a\n * cent of BILLED spend, and at most a tenth of a cent of provider spend. Read\n * against a retail rate it yields the floor of the line margin, never the\n * middle of it — which is the safe direction, and the direction every margin\n * assertion in this repo depends on. What a period actually cost is measured,\n * not derived: `assistProviderCostUsd` reads it off the rollup.\n */\nexport const ASSIST_CREDIT_COST_USD = 0.001\n\n/**\n * The field a monthly assist rollup, a signal row and a kind bucket keep\n * PROVIDER spend under, beside the `estCostUsd` a customer's credits are\n * drawn from (AGL-3015).\n *\n * Named here rather than in the meter that writes it because the readers are\n * spread across the plugin, the billing routes and the staff surfaces, and a\n * field name spelled out at six call sites is a field name that will be\n * spelled differently at the seventh.\n */\nexport const ASSIST_PROVIDER_COST_FIELD = 'providerCostUsd'\n\n/**\n * What a metered period ACTUALLY COST US, from the two dollar figures a\n * rollup carries.\n *\n * `billedUsd` is `estCostUsd`: every exchange priced at its model's billed\n * rates, and the figure credits are drawn from. `providerUsd` is\n * `ASSIST_PROVIDER_COST_FIELD`: the same exchanges priced at what the\n * provider charged. The second is the answer wherever it exists.\n *\n * ## Why a rollup may not carry one, and which way that errs\n *\n * The provider figure is written from the commit that split the two rates\n * onward. A period closed before it answers with the BILLED figure, which is\n * at or above what we paid — so a margin taken on history reads low and a\n * cost reads high, the direction a cost figure is allowed to be wrong in.\n * The one period that under-reads is the month in flight when the split\n * shipped, whose provider figure covers only the exchanges after it; that\n * month's error is bounded by the markup on a few days of a meter whose\n * largest measured organization is a fraction of a cent.\n */\nexport function assistProviderCostUsd(\n billedUsd: unknown,\n providerUsd: unknown,\n): number {\n const provider = Number(providerUsd)\n if (Number.isFinite(provider) && provider > 0) return provider\n const billed = Number(billedUsd)\n return Number.isFinite(billed) && billed > 0 ? billed : 0\n}\n\n/**\n * The minimum line margin every retail assist rate must clear.\n *\n * The same floor now applied to every retail rate in this codebase, and the\n * reason the assist ladder stops descending at cost x2 on Agency instead of\n * taking one more step the way the contacts ladder once did.\n */\nexport const ASSIST_CREDIT_MIN_MARGIN_PCT = 0.5\n\n/**\n * Credits -> our cost in USD. Non-finite or negative input is 0 credits, so a\n * corrupt band cannot become a budget of `NaN` that every comparison passes.\n */\nexport function assistUsdFromCredits(credits: number): number {\n const value = Number(credits)\n if (!Number.isFinite(value) || value <= 0) return 0\n return Math.round(value * ASSIST_CREDIT_COST_USD * 1_000_000) / 1_000_000\n}\n\n/**\n * Measured USD -> credits, rounded UP.\n *\n * Up, because this converts spend the platform has ALREADY made into the\n * figure a customer is shown against their band. Rounding down would let a\n * long tail of sub-credit exchanges cost real money and draw nothing, which\n * is the one direction a meter must not err in.\n */\nexport function assistCreditsFromUsd(usd: number): number {\n const value = Number(usd)\n if (!Number.isFinite(value) || value <= 0) return 0\n return Math.ceil(value / ASSIST_CREDIT_COST_USD)\n}\n\n/**\n * The org's monthly assist band in credits, or `null` when its plan sells no\n * assist band at all.\n *\n * ## `null` is not zero, and the difference is the whole safety property\n *\n * A band of 0 resolves to `null` — \"this org sells no band\" — and never to\n * a budget of `$0`. An org with no band still reaches the console\n * assistant's docs-grounded rung, which is bounded by the free daily message\n * cap and by the operator's spend backstop. Resolving zero as a budget would\n * refuse that rung outright: a whole assistant turned off by a pricing field\n * that was never about it.\n *\n * Since AGL-3203 no PLAN ROW bands at zero. Free carries the AGL-2925 taste\n * of `FREE_AI_TASTE_CREDITS_PER_MONTH` with no rate beside it, so it\n * resolves to a budget here and `assistBandRefuses` makes that budget a\n * wall; Starter carries 750 WITH a rate, so its budget is a line it meters\n * past rather than a wall. Both draw their docs-grounded chat on the same\n * band, which is fine — each is sized so a month of it costs well under a\n * dollar.\n *\n * So the zero case is now reached only by a per-org override that writes\n * one, and the reading above is what keeps that org's assistant running.\n *\n * The Aglyn AI add-on (AGL-2896) needs no knowledge here either way: the\n * resolver has already added `AI_ADDON_CREDITS_PER_MONTH[plan]` to the\n * band, because the add-on widens the one pool rather than opening a\n * second.\n *\n * The same reading is what makes the guard survive a stubbed entitlements\n * module. A test double that answers 0 for every quota produces `null` here,\n * which falls through to the operator backstop, rather than a budget of zero\n * that refuses everything while the assertions read green. A clamp that goes\n * green having refused every request has proved nothing, and 0 is exactly the\n * value a stub returns.\n *\n * ## Non-finite is also `null`, deliberately\n *\n * `Infinity` must never resolve to a budget. It is unrepresentable off-process\n * — `JSON.stringify(Infinity)` is `null` and reads back as 0 — so a band that\n * reached here as `Infinity` is a band that would read as zero somewhere\n * else. Falling through to the backstop is bounded and consistent; honouring\n * it would be an unbounded budget on one process and a refusal on the next.\n *\n * An UNCAPPED staff comp (AGL-3049) is where that `Infinity` comes from on\n * purpose, and `null` is right for it here too: there is no band to measure\n * against, be over, price or alert on. What `null` must NOT bring with it is\n * the backstop, whose repo default would cap the one workspace staff said\n * has no cap — so `assistBandRefuses` answers false for it, and the\n * reservation's ceiling (`assistMonthlyCeilingUsd` in the AI plugin) drops\n * the default and keeps only an operator's explicit figure.\n */\nexport function resolveAssistCreditBudget(\n org: Partial<AglynOrgBilling> | null | undefined,\n): number | null {\n const credits = planQuotaOf(resolveOrgEntitlements(org), 'assistCreditsPerMonth')\n if (!Number.isFinite(credits) || credits <= 0) return null\n return Math.floor(credits)\n}\n\n/** The org's monthly assist band in USD of provider spend, or `null`. */\nexport function resolveAssistBudgetUsd(\n org: Partial<AglynOrgBilling> | null | undefined,\n): number | null {\n const credits = resolveAssistCreditBudget(org)\n return credits === null ? null : assistUsdFromCredits(credits)\n}\n\n/**\n * Credits spent past the band. A `null` band yields 0 — an org with no band\n * has nothing to be over, and reporting the whole month's spend as overage\n * there would invoice the free tier for its docs answers.\n */\nexport function assistCreditOverage(\n usedCredits: number,\n bandCredits: number | null,\n): number {\n const used = Number(usedCredits)\n if (!Number.isFinite(used) || used <= 0) return 0\n if (bandCredits === null) return 0\n const band = Number(bandCredits)\n if (!Number.isFinite(band) || band <= 0) return 0\n return Math.max(0, used - band)\n}\n\n/**\n * The per-1,000 rate the org's assist credits are sold at past its band, or\n * `null` when nothing is sold past it — the ONE reading of the rate\n * (AGL-2896). `priceAssistCreditOverage`, `assistBandRefuses`,\n * `assistRefusedByHardCap` and `assistHardCapRefusalText` all go through\n * it, so the gate, the invoice and the refusal sentence cannot quote three\n * different rates for one org.\n *\n * The plan's `extraAssistCreditsUsdPer1k` IS the answer — the whole answer,\n * since AGL-3203. Starter used to be the one exception: it banded at 0, so\n * the rate could not sit on its plan row without advertising a fee on a\n * quantity the plan never sold, and the add-on's band was priced from a\n * constant read here instead. Starter now includes 750 credits\n * unconditionally and carries $3.00 on its own row, so the exception and its\n * constant are gone and there is exactly one place the rate is written.\n *\n * Free and Enterprise are null with or without the add-on: Free sells no\n * add-on and its taste band is a wall by decision (AGL-2925), and\n * Enterprise's usage is in the contract.\n *\n * A STAFF COMP is null on every plan and with every add-on (AGL-3034),\n * because `resolvePlanPricing` — never `PLAN_PRICING` indexed directly —\n * sells nothing on a comp. That is what makes a comp's band a wall at the\n * gate and prices its overage to zero at the charge: the AI overage path\n * (AGL-3011) claims only what `assistMonthOverage` prices, so a comp never\n * becomes a Stripe invoice. The function is a single read for that reason;\n * a second source for the rate is how a comp gets quoted one.\n */\nexport function resolveAssistOverageRateUsdPer1k(\n org: Partial<AglynOrgBilling> | null | undefined,\n): number | null {\n return resolvePlanPricing(org).extraAssistCreditsUsdPer1k\n}\n\nexport interface AssistCreditOveragePrice {\n /** Credits past the plan's band, as handed in. */\n overageCredits: number\n /** Estimated charge at the plan's per-1,000 rate. */\n overageMonthlyUsd: number\n /** Per-1,000 rate; null when the plan prices no assist overage. */\n overageRateUsd: number | null\n}\n\n/**\n * Prices assist credits past a plan's band.\n *\n * ## No `allowed`, deliberately — and for the opposite reason to email\n *\n * `priceEmailSendOverage` omits an `allowed` field because email has two\n * gates and a transactional sender consulting the wrong one drops a password\n * reset. Assist omits it because it has exactly ONE gate and this is not it:\n * whether the band refuses is `assistBandRefuses`, applied inside\n * `reserveAssistMessage` before a token is spent. A second answer to \"may\n * this proceed\", computed after the fact from a rounded credit figure, would\n * be a second gate to drift from the first.\n *\n * ## Takes the overage, does not recompute it\n *\n * `assistCreditOverage` derives the excess once, from the measured meter and\n * the resolved band. Re-deriving it here would be a second overage model.\n *\n * A null rate yields zero structurally rather than by a check, which is what\n * keeps Free and Starter at zero on this axis.\n */\nexport function priceAssistCreditOverage(\n org: Partial<AglynOrgBilling> | null | undefined,\n overageCredits: number,\n): AssistCreditOveragePrice {\n const overageRateUsd = resolveAssistOverageRateUsdPer1k(org)\n const credits = Number(overageCredits)\n const over = Number.isFinite(credits) && credits > 0 ? credits : 0\n return {\n overageCredits: over,\n overageMonthlyUsd:\n overageRateUsd === null\n ? 0\n : Math.round((over / 1000) * overageRateUsd * 100) / 100,\n overageRateUsd,\n }\n}\n\n/**\n * The line margin a per-1,000-credit retail rate earns AT LEAST, as a\n * fraction.\n *\n * One expression, so the ladder and its floor cannot be checked against two\n * different definitions of margin. A null or non-positive rate has no margin\n * to report and answers `null` rather than 1 or 0 — \"this plan sells no\n * overage\" is not \"this plan sells overage at 100% margin\".\n *\n * A LOWER BOUND, not the realized figure (AGL-3015). The cost side is\n * `ASSIST_CREDIT_COST_USD`, which is what a credit BILLS at; a model billed\n * above its provider rate costs us less than that per credit, so the real\n * line margin is this or better and never worse. The floor stays checkable\n * by eye — a $2.00 rate is still cost x2 — and a retail rate that clears the\n * floor here clears it on every model mix. What a period realized is the\n * staff margin card's business, and it reads measured provider spend.\n */\nexport function assistCreditRateMarginPct(\n rateUsdPer1k: number | null,\n): number | null {\n if (rateUsdPer1k === null) return null\n const rate = Number(rateUsdPer1k)\n if (!Number.isFinite(rate) || rate <= 0) return null\n const costPer1k = ASSIST_CREDIT_COST_USD * 1000\n return (rate - costPer1k) / rate\n}\n\n/**\n * What a refused or in-progress workspace may be TOLD about its assist band.\n *\n * The reservation carries `costUsd` and `costLimitUsd` because that is what\n * it measured and what it compared against, and both are our provider bill.\n * Shipping the reservation itself to a browser publishes them. This is the\n * projection that crosses that boundary: credits only, no dollars, no model,\n * no rates.\n */\nexport interface PublicAssistCredits {\n /** Credits drawn this month, rounded up. */\n used: number\n /** The band, or `null` when no band applied to this request. */\n limit: number | null\n /** Credits left in the band, or `null` when there is no band. */\n remaining: number | null\n}\n\n/**\n * Projects a measured spend figure and a ceiling into the credit view.\n *\n * Both inputs are nullable because the reservation's are: `costUsd` is `null`\n * when nothing consulted the monthly document, and a ceiling is `null` when\n * none applied. Neither collapses to 0 here — \"nobody looked\" and \"spent\n * nothing\" have to stay distinguishable at the surface, exactly as they do on\n * the reservation.\n */\nexport function publicAssistCredits(\n costUsd: number | null,\n ceilingUsd: number | null,\n): PublicAssistCredits {\n const used = costUsd === null ? 0 : assistCreditsFromUsd(costUsd)\n if (ceilingUsd === null) return { used, limit: null, remaining: null }\n const limit = assistCreditsFromUsd(ceilingUsd)\n return { used, limit, remaining: Math.max(0, limit - used) }\n}\n\n/**\n * The console's name for the org's hard-cap switch (AGL-2653), quoted by the\n * refusal so the words on the 402 and the words beside the switch are one\n * string. Change it here and both move.\n */\nexport const ASSIST_HARD_CAP_CONTROL_LABEL = 'Stop AI assist at the included band'\n\n/** Where the switch lives, in the words the refusal uses. */\nexport const ASSIST_HARD_CAP_CONTROL_LOCATION = 'Billing → Usage'\n\n/**\n * Whether the org asked to be stopped at its assist band (AGL-2653).\n *\n * Strictly `=== true`: the map is absent on every org that has not touched\n * the switch, and absent is the selling default. A truthy string or a `1`\n * written by hand must not switch an assistant off at the band, so nothing\n * short of the boolean counts.\n */\nexport function resolveAssistHardCap(\n org: Partial<AglynOrgBilling> | null | undefined,\n): boolean {\n return org?.assistOverage?.hardCap === true\n}\n\n/**\n * Is the org's assist band a WALL, or a line past which credits are sold?\n *\n * Two things make it a wall, and only two:\n *\n * - **The org asked.** `assistOverage.hardCap` is the customer's own ceiling,\n * the `storageOverage.capUsd` of this meter.\n * - **There is nothing to sell past it at.** An org whose\n * `resolveAssistOverageRateUsdPer1k` is `null` has no rate to bill, so\n * credits past its band would be provider spend with no invoice line — the\n * silent free overage `plan-entitlements.spec.ts` forbids. Enterprise is\n * one case, its band being contractual; FREE is the other, its taste band\n * a wall by the AGL-2925 decision so that nothing about a Free workspace\n * can produce a charge.\n *\n * Everything else sells past the band by default, which is the 2026-09-07\n * decision this function encodes — and since AGL-3203 that includes Starter,\n * which carries 750 credits and a $3.00 rate like every paid tier above it.\n *\n * An org with NO band at all answers the same as Enterprise, harmlessly:\n * `resolveAssistBudgetUsd` is `null` for it and the reservation never\n * measures against a band. No plan ROW is in that state any more — Free and\n * Starter both were, at different times — so it is reached by a per-org\n * override that writes a zero.\n *\n * An UNCAPPED staff comp (AGL-3049) is neither: it has no band to be a wall,\n * and nothing is sold past a band it does not have. It answers false, ahead\n * of the switch — a workspace staff uncapped is not stopped by a band, and\n * the reservation reads that answer as \"no wall, no backstop default\".\n */\nexport function assistBandRefuses(\n org: Partial<AglynOrgBilling> | null | undefined,\n): boolean {\n if (isUncappedPlanComp(org)) return false\n if (resolveAssistHardCap(org)) return true\n return resolveAssistOverageRateUsdPer1k(org) === null\n}\n\n/**\n * Which ceiling refused a reservation, or `null` when it was admitted.\n *\n * - `messages`: the message cap (daily on the free rung, monthly entitled).\n * - `budget`: the operator's spend backstop.\n * - `band`: the plan's own band, as a wall — the switch, or a plan with\n * no rate to sell past it at.\n * - `cap`: the org's own dollar ceiling on overage (AGL-2898).\n *\n * The four that exist only on the Free taste (AGL-2925), each a precaution\n * against the one band that has no invoice behind it:\n *\n * - `account`: the ACCOUNT's 300 credits are spent across every free\n * workspace it owns — the org band alone would multiply by\n * the workspaces one person may hold.\n * - `requests`: the account's daily free request cap.\n * - `refusals`: the account's free generation is paused for the day after\n * too many `refusal` stops — a brief the model declined is a\n * brief that should not be retried thirty times.\n * - `platform`: the platform-wide daily ceiling on free spend; nobody's\n * fault, and \"try again tomorrow\" is the whole answer.\n *\n * Declared here, beside the helpers that read it, so the reservation, its\n * public projection and both doors name one union.\n */\nexport type AssistRefusedBy =\n | 'messages'\n | 'budget'\n | 'band'\n | 'cap'\n | 'account'\n | 'requests'\n | 'refusals'\n | 'platform'\n | null\n\n/**\n * The sentence a Free workspace is told when one of the taste's own\n * precautions refused it, or `null` when the refusal was something else.\n *\n * One string per rung, for both doors and the gate ladder, so a customer\n * reading the console panel and one reading the besigner get the same\n * words. Every sentence is customer-safe: it names what to do (upgrade,\n * wait) and never a figure from our cost model, a counter document, or\n * another workspace.\n */\nexport function assistFreeTasteRefusalText(\n refusedBy: AssistRefusedBy,\n): string | null {\n switch (refusedBy) {\n case 'account':\n return (\n 'Your free AI credits for this month are used across your ' +\n 'workspaces — upgrade any workspace to keep going.'\n )\n case 'requests':\n return (\n 'Free workspaces get a limited number of AI requests a day — ' +\n 'try again tomorrow, or upgrade to keep going.'\n )\n case 'refusals':\n return (\n 'The AI declined several requests today, so free AI generation is ' +\n 'paused until tomorrow. Try a different kind of brief then.'\n )\n case 'platform':\n return (\n 'Free AI generation is paused for the rest of today — try again ' +\n 'tomorrow. Paid workspaces are not affected.'\n )\n default:\n return null\n }\n}\n\n/**\n * Whether a refusal at the band was the org's OWN doing — the switch is on\n * AND turning it off would have let the exchange through. The refusal that\n * names the switch has to be the one the switch caused: on a plan with no\n * rate the band refuses whatever the switch says, and telling that customer\n * to turn it off would send them to a control that does nothing.\n */\nexport function assistRefusedByHardCap(\n org: Partial<AglynOrgBilling> | null | undefined,\n refusedBy: AssistRefusedBy,\n): boolean {\n return (\n refusedBy === 'band' &&\n resolveAssistHardCap(org) &&\n resolveAssistOverageRateUsdPer1k(org) !== null\n )\n}\n\n/**\n * The sentence a workspace is told when its own hard cap refused it. ONE\n * string for both assist entrypoints, so the console panel and the besigner\n * cannot describe the same control in two ways. It names the switch by its\n * label, says where it lives, and quotes the plan's rate so the reader knows\n * what turning it off costs.\n */\nexport function assistHardCapRefusalText(\n org: Partial<AglynOrgBilling> | null | undefined,\n): string {\n const rate = resolveAssistOverageRateUsdPer1k(org)\n const rateClause =\n rate === null ? '' : ` at $${rate.toFixed(2)} per 1,000 credits`\n return (\n `This workspace used its assistant credits for the month, and ` +\n `\"${ASSIST_HARD_CAP_CONTROL_LABEL}\" is on. Turn it off under ` +\n `${ASSIST_HARD_CAP_CONTROL_LOCATION} to keep going${rateClause}.`\n )\n}\n\nexport interface AssistMonthOverage extends AssistCreditOveragePrice {\n /** Credits drawn this month, rounded up from the measured spend. */\n usedCredits: number\n /** The band those credits were measured against, or `null` for no band. */\n bandCredits: number | null\n}\n\n/**\n * A month's assist overage from its measured spend — the ONE derivation the\n * invoice and every meter read (AGL-2653).\n *\n * Takes `estCostUsd` as the rollup reads it and composes the three steps\n * that already existed — credits from spend, overage past the band, price at\n * the plan's rate — so `report-usage` cannot assemble them in a different\n * order from a console readout and arrive at a different figure. The\n * rounding is the other overage terms' rounding: `priceAssistCreditOverage`\n * rounds to the cent, and the rollup turns dollars into cents with the same\n * `Math.round(usd * 100)` it applies to contacts, API and dataset storage.\n *\n * A non-finite or negative spend is zero credits, for the reason\n * `assistCreditsFromUsd` gives; a plan with no band or no rate prices zero\n * structurally, so Free, Starter and Enterprise bill nothing here without a\n * check that could be forgotten.\n */\nexport function assistMonthOverage(\n org: Partial<AglynOrgBilling> | null | undefined,\n estCostUsd: number,\n): AssistMonthOverage {\n const usedCredits = assistCreditsFromUsd(estCostUsd)\n const bandCredits = resolveAssistCreditBudget(org)\n return {\n usedCredits,\n bandCredits,\n ...priceAssistCreditOverage(\n org,\n assistCreditOverage(usedCredits, bandCredits),\n ),\n }\n}\n\n/** The console's name for the org's overage ceiling (AGL-2898). */\nexport const ASSIST_OVERAGE_CAP_CONTROL_LABEL =\n 'Stop AI when this month’s overage reaches'\n\n/**\n * Bounds on the ceiling a self-serve org may set. The floor is a dollar\n * because a ceiling of cents would refuse on the first exchange past the\n * band while reading as a limit; the roof is the storage cap's, so no\n * self-serve org writes itself an enterprise-sized commitment.\n */\nexport const ASSIST_OVERAGE_CAP_MIN_USD = 1\nexport const ASSIST_OVERAGE_CAP_MAX_USD = 100_000\n\n/**\n * The org's monthly ceiling on AI overage dollars, or `null` for none.\n *\n * Strictly a finite positive number. `null`, absent, a string, `NaN`,\n * `Infinity` and zero all read as no ceiling — the failure mode of the\n * alternative is a wall of `NaN` that every comparison passes, or a ceiling\n * of zero that refuses the first exchange past the band while the card\n * says nothing is set.\n */\nexport function resolveAssistOverageCapUsd(\n org: Partial<AglynOrgBilling> | null | undefined,\n): number | null {\n const raw = org?.assistOverage?.capUsd\n if (typeof raw !== 'number' || !Number.isFinite(raw) || raw <= 0) return null\n return raw\n}\n\n/**\n * Has this month's overage reached the org's own ceiling (AGL-2898)?\n *\n * Composed from `assistMonthOverage`, the ONE derivation the invoice reads,\n * so the figure the ceiling stops at is the figure the invoice would have\n * billed. That composition is also what confines the ceiling to plans that\n * sell past their band: a plan with no rate prices its overage to zero, and\n * zero never reaches a positive ceiling — Free and Enterprise cannot get here\n * without a check that could be forgotten. `>=` rather than `>`, because an\n * org that asked to stop AT a figure is stopped when the figure is met.\n */\nexport function assistOverageCapReached(\n org: Partial<AglynOrgBilling> | null | undefined,\n estCostUsd: number,\n): boolean {\n const cap = resolveAssistOverageCapUsd(org)\n if (cap === null) return false\n return assistMonthOverage(org, estCostUsd).overageMonthlyUsd >= cap\n}\n\n/**\n * Whether a refusal was the org's own ceiling. The reservation answers\n * `'cap'` only when `assistOverageCapReached` did, and that helper only\n * reaches a ceiling on a plan with a rate, so the two conditions restated\n * here are the same guarantee `assistRefusedByHardCap` gives the switch: the\n * control the sentence names is the control that refused.\n */\nexport function assistRefusedByOverageCap(\n org: Partial<AglynOrgBilling> | null | undefined,\n refusedBy: AssistRefusedBy,\n): boolean {\n return (\n refusedBy === 'cap' &&\n resolveAssistOverageCapUsd(org) !== null &&\n resolveAssistOverageRateUsdPer1k(org) !== null\n )\n}\n\n/**\n * The sentence a workspace is told when its own overage ceiling refused it.\n * ONE string for both assist entrypoints, as `assistHardCapRefusalText` is\n * for the switch. It names the control by its label, quotes the figure the\n * org chose, and says where to raise or clear it.\n */\nexport function assistOverageCapRefusalText(\n org: Partial<AglynOrgBilling> | null | undefined,\n): string {\n const cap = resolveAssistOverageCapUsd(org)\n const figure = cap === null ? '' : ` $${cap.toFixed(2)}`\n return (\n `This month’s AI assist overage reached${figure}, the figure you set as ` +\n `\"${ASSIST_OVERAGE_CAP_CONTROL_LABEL}\". Raise or remove it under ` +\n `${ASSIST_HARD_CAP_CONTROL_LOCATION} to keep going.`\n )\n}\n\n/**\n * The refusal sentence for whichever of the org's OWN controls refused, or\n * `null` when the refusal was nobody's control — the message cap, the\n * operator's backstop, or a plan that sells no overage. Both doors answer a\n * 402 with this text when it is a string and their usual 429 otherwise, so\n * the status and the sentence cannot disagree about whose decision it was.\n */\nexport function assistOwnControlRefusalText(\n org: Partial<AglynOrgBilling> | null | undefined,\n refusedBy: AssistRefusedBy,\n): string | null {\n if (assistRefusedByHardCap(org, refusedBy)) return assistHardCapRefusalText(org)\n if (assistRefusedByOverageCap(org, refusedBy)) {\n return assistOverageCapRefusalText(org)\n }\n return null\n}\n"],"names":["isUncappedPlanComp","resolveOrgEntitlements","resolvePlanPricing","planQuotaOf","ASSIST_CREDIT_COST_USD","ASSIST_PROVIDER_COST_FIELD","assistProviderCostUsd","billedUsd","providerUsd","provider","Number","isFinite","billed","ASSIST_CREDIT_MIN_MARGIN_PCT","assistUsdFromCredits","credits","value","Math","round","assistCreditsFromUsd","usd","ceil","resolveAssistCreditBudget","org","floor","resolveAssistBudgetUsd","assistCreditOverage","usedCredits","bandCredits","used","band","max","resolveAssistOverageRateUsdPer1k","extraAssistCreditsUsdPer1k","priceAssistCreditOverage","overageCredits","overageRateUsd","over","overageMonthlyUsd","assistCreditRateMarginPct","rateUsdPer1k","rate","costPer1k","publicAssistCredits","costUsd","ceilingUsd","limit","remaining","ASSIST_HARD_CAP_CONTROL_LABEL","ASSIST_HARD_CAP_CONTROL_LOCATION","resolveAssistHardCap","assistOverage","hardCap","assistBandRefuses","assistFreeTasteRefusalText","refusedBy","assistRefusedByHardCap","assistHardCapRefusalText","rateClause","toFixed","assistMonthOverage","estCostUsd","ASSIST_OVERAGE_CAP_CONTROL_LABEL","ASSIST_OVERAGE_CAP_MIN_USD","ASSIST_OVERAGE_CAP_MAX_USD","resolveAssistOverageCapUsd","raw","capUsd","assistOverageCapReached","cap","assistRefusedByOverageCap","assistOverageCapRefusalText","figure","assistOwnControlRefusalText"],"mappings":"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"}
|
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
|
-
import { type DatasetModel } from './dataset-models';
|
|
18
17
|
/**
|
|
19
18
|
* Browsable data-placeholder catalogs for the designer's insert picker
|
|
20
19
|
* (AGL-583). Hand-typing `{{entry.title}}` stays the advanced path; these
|
|
@@ -67,16 +66,9 @@ export declare const COLLECTION_TOKEN_CATALOG: readonly BindingTokenCatalogEntry
|
|
|
67
66
|
*/
|
|
68
67
|
export declare const AUTHOR_TOKEN_CATALOG: readonly BindingTokenCatalogEntry[];
|
|
69
68
|
/**
|
|
70
|
-
*
|
|
71
|
-
* one reference to a field of the referenced record
|
|
72
|
-
* takes the stable
|
|
69
|
+
* The `{{item.*}}` token for one field of the record a repeat is rendering,
|
|
70
|
+
* optionally hopping one reference to a field of the referenced record
|
|
71
|
+
* (AGL-180). Always takes the stable field id — display names are labels
|
|
72
|
+
* only, so renaming a field never breaks a binding (AGL-578).
|
|
73
73
|
*/
|
|
74
|
-
export declare function
|
|
75
|
-
/**
|
|
76
|
-
* Dataset-item token entries for a repeatable container's model: one per
|
|
77
|
-
* field in model order, labeled with the CURRENT display name while the
|
|
78
|
-
* token carries the stable reference id (AGL-578 — renaming a label never
|
|
79
|
-
* breaks bindings). Reference fields with a configured display field also
|
|
80
|
-
* get their one-hop token (`{{item.author.name}}`).
|
|
81
|
-
*/
|
|
82
|
-
export declare function datasetItemTokens(model: DatasetModel): BindingTokenCatalogEntry[];
|
|
74
|
+
export declare function repeatItemToken(fieldId: string, targetFieldId?: string): string;
|
|
@@ -13,9 +13,17 @@
|
|
|
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
|
-
*/
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
*/ /**
|
|
17
|
+
* Browsable data-placeholder catalogs for the designer's insert picker
|
|
18
|
+
* (AGL-583). Hand-typing `{{entry.title}}` stays the advanced path; these
|
|
19
|
+
* catalogs give every token a friendly label + description so editors can
|
|
20
|
+
* browse and insert instead of memorizing the grammar.
|
|
21
|
+
*
|
|
22
|
+
* The token STRINGS are owned elsewhere — `collectionEntryTokens`
|
|
23
|
+
* (collection-entries.ts) and the tenant compose pipeline resolve them —
|
|
24
|
+
* this module only names them for pickers. The spec cross-checks the entry
|
|
25
|
+
* catalog against the resolver so the two can never drift.
|
|
26
|
+
*/ /**
|
|
19
27
|
* `{{entry.*}}` tokens (AGL-105/551/582) — resolve per entry inside a
|
|
20
28
|
* Collection entries block and page-wide on entry-template screens.
|
|
21
29
|
* Mirrors {@link collectionEntryTokens}; the spec enforces the mirror.
|
|
@@ -115,7 +123,7 @@ import { humanizeDatasetFieldId } from "./datasets.js";
|
|
|
115
123
|
{
|
|
116
124
|
token: '{{entry.coverVideo}}',
|
|
117
125
|
label: 'Featured video',
|
|
118
|
-
description: 'The featured video’s source — a library film, a video link or
|
|
126
|
+
description: 'The featured video’s source — a library film, a video file link or ' + 'a video host’s link — for a Video element.'
|
|
119
127
|
},
|
|
120
128
|
{
|
|
121
129
|
token: '{{entry.category}}',
|
|
@@ -257,44 +265,12 @@ import { humanizeDatasetFieldId } from "./datasets.js";
|
|
|
257
265
|
}
|
|
258
266
|
];
|
|
259
267
|
/**
|
|
260
|
-
*
|
|
261
|
-
* one reference to a field of the referenced record
|
|
262
|
-
* takes the stable
|
|
263
|
-
|
|
268
|
+
* The `{{item.*}}` token for one field of the record a repeat is rendering,
|
|
269
|
+
* optionally hopping one reference to a field of the referenced record
|
|
270
|
+
* (AGL-180). Always takes the stable field id — display names are labels
|
|
271
|
+
* only, so renaming a field never breaks a binding (AGL-578).
|
|
272
|
+
*/ export function repeatItemToken(fieldId, targetFieldId) {
|
|
264
273
|
return targetFieldId ? `{{item.${fieldId}.${targetFieldId}}}` : `{{item.${fieldId}}}`;
|
|
265
274
|
}
|
|
266
|
-
/**
|
|
267
|
-
* Dataset-item token entries for a repeatable container's model: one per
|
|
268
|
-
* field in model order, labeled with the CURRENT display name while the
|
|
269
|
-
* token carries the stable reference id (AGL-578 — renaming a label never
|
|
270
|
-
* breaks bindings). Reference fields with a configured display field also
|
|
271
|
-
* get their one-hop token (`{{item.author.name}}`).
|
|
272
|
-
*/ export function datasetItemTokens(model) {
|
|
273
|
-
var _model_order;
|
|
274
|
-
const entries = [];
|
|
275
|
-
for (const fieldId of (_model_order = model.order) != null ? _model_order : []){
|
|
276
|
-
var _DATASET_FIELD_TYPE_LABELS_field_type;
|
|
277
|
-
var _model_fields, _field_name, _field_reference;
|
|
278
|
-
const field = (_model_fields = model.fields) == null ? void 0 : _model_fields[fieldId];
|
|
279
|
-
if (!field) continue;
|
|
280
|
-
const label = ((_field_name = field.name) == null ? void 0 : _field_name.trim()) || humanizeDatasetFieldId(fieldId);
|
|
281
|
-
entries.push({
|
|
282
|
-
token: datasetItemToken(fieldId),
|
|
283
|
-
label,
|
|
284
|
-
description: (_DATASET_FIELD_TYPE_LABELS_field_type = DATASET_FIELD_TYPE_LABELS[field.type]) != null ? _DATASET_FIELD_TYPE_LABELS_field_type : field.type
|
|
285
|
-
});
|
|
286
|
-
// One reference hop (AGL-180): surface the configured display field of
|
|
287
|
-
// the referenced record — the hop editors reach for most.
|
|
288
|
-
const hopId = field.type === 'reference' ? (_field_reference = field.reference) == null ? void 0 : _field_reference.displayFieldId : undefined;
|
|
289
|
-
if (hopId) {
|
|
290
|
-
entries.push({
|
|
291
|
-
token: datasetItemToken(fieldId, hopId),
|
|
292
|
-
label: `${label} → ${humanizeDatasetFieldId(hopId)}`,
|
|
293
|
-
description: 'Field from the referenced record.'
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
return entries;
|
|
298
|
-
}
|
|
299
275
|
|
|
300
276
|
//# sourceMappingURL=binding-token-catalog.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/binding-token-catalog.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 {\n DATASET_FIELD_TYPE_LABELS,\n type DatasetModel,\n} from './dataset-models'\nimport { humanizeDatasetFieldId } from './datasets'\n\n/**\n * Browsable data-placeholder catalogs for the designer's insert picker\n * (AGL-583). Hand-typing `{{entry.title}}` stays the advanced path; these\n * catalogs give every token a friendly label + description so editors can\n * browse and insert instead of memorizing the grammar.\n *\n * The token STRINGS are owned elsewhere — `collectionEntryTokens`\n * (collection-entries.ts) and the tenant compose pipeline resolve them —\n * this module only names them for pickers. The spec cross-checks the entry\n * catalog against the resolver so the two can never drift.\n */\nexport interface BindingTokenCatalogEntry {\n /** The literal token inserted into the prop, e.g. `{{entry.title}}`. */\n token: string\n /** Friendly picker label, e.g. `Title`. */\n label: string\n /** One-line description shown as the option's secondary text. */\n description?: string\n}\n\n/**\n * `{{entry.*}}` tokens (AGL-105/551/582) — resolve per entry inside a\n * Collection entries block and page-wide on entry-template screens.\n * Mirrors {@link collectionEntryTokens}; the spec enforces the mirror.\n */\nexport const ENTRY_TOKEN_CATALOG: readonly BindingTokenCatalogEntry[] = [\n { token: '{{entry.title}}', label: 'Title', description: 'The entry headline.' },\n {\n token: '{{entry.excerpt}}',\n label: 'Excerpt',\n description: 'Short summary text.',\n },\n {\n token: '{{entry.body}}',\n label: 'Body',\n description: 'The full entry body.',\n },\n {\n token: '{{entry.url}}',\n label: 'Link URL',\n description: 'Auto-route to the entry page.',\n },\n {\n token: '{{entry.date}}',\n label: 'Published date',\n description: 'Formatted publish date.',\n },\n /*\n Beside the readable date on purpose: a designer reaching for \"the date\" in\n a Video element's Publication date field has to see that the readable one\n is the wrong pick there, and why.\n */\n {\n token: '{{entry.publishedAt}}',\n label: 'Publish timestamp',\n description:\n 'The publish date and time in ISO 8601, for a field that needs a ' +\n 'machine-readable date, such as a Video element’s publication date.',\n },\n {\n token: '{{entry.author}}',\n label: 'Author',\n description: 'The byline set on the entry.',\n },\n {\n token: '{{entry.authorBio}}',\n label: 'Author bio',\n description: 'Blurb from the author’s record.',\n },\n {\n token: '{{entry.authorImage}}',\n label: 'Author portrait',\n description: 'Portrait or logo from the author’s record.',\n },\n {\n token: '{{entry.authorUrl}}',\n label: 'Author link',\n description: 'The author’s own page.',\n },\n {\n token: '{{entry.authorPageUrl}}',\n label: 'Author page',\n description:\n 'This author’s page on this site — everything they wrote, across every ' +\n 'collection. Separate from Author link, which is their own site.',\n },\n {\n token: '{{entry.slug}}',\n label: 'Slug',\n description: 'URL-safe entry identifier.',\n },\n /*\n Named \"Its collection…\" rather than \"Collection…\", which is what these\n describe and also exactly what the `{{collection.*}}` entries below are\n called. The picker groups options by heading, so the two sets sit apart —\n but a designer scanning it reads the LABEL, and two options reading\n \"Collection name\" three lines apart is a choice made by guessing. The\n pronoun is doing real work: on the one page where both resolve, they mean\n different collections.\n */\n {\n token: '{{entry.collection}}',\n label: 'Its collection',\n description:\n 'Which section this entry belongs to. Worth binding on a listing that ' +\n 'MIXES collections — an author page — where a card otherwise cannot ' +\n 'tell a release note from an essay.',\n },\n {\n token: '{{entry.collectionSlug}}',\n label: 'Its collection slug',\n description: 'That collection’s URL segment.',\n },\n {\n token: '{{entry.collectionUrl}}',\n label: 'Its collection link',\n description: 'The listing this entry belongs to.',\n },\n {\n token: '{{entry.coverImage}}',\n label: 'Cover image',\n description: 'Cover image URL.',\n },\n {\n token: '{{entry.coverVideo}}',\n label: 'Featured video',\n description:\n 'The featured video’s source — a library film, a video link or a ' +\n 'Wistia link — for a Video element.',\n },\n {\n token: '{{entry.category}}',\n label: 'Category',\n description: 'The entry category.',\n },\n {\n token: '{{entry.tags}}',\n label: 'Tags',\n description: 'Tags, comma separated.',\n },\n {\n token: '{{entry.seoTitle}}',\n label: 'SEO title',\n description: 'Search title; falls back to Title.',\n },\n {\n token: '{{entry.seoDescription}}',\n label: 'SEO description',\n description: 'Meta description; falls back to Excerpt.',\n },\n]\n\n/**\n * `{{collection.*}}` and `{{pagination.*}}` tokens (AGL-551/1321/1386) —\n * resolve on collection list/entry template screens (see the tenant compose\n * pipeline's collection tokens).\n *\n * The category and pagination tokens all resolve to the empty string where\n * they do not apply, so they are safe to bind unconditionally: ONE list\n * screen serves the bare listing, every `/page/{n}` and every\n * `/category/{slug}`, and a template has no runtime conditional to vary\n * itself with.\n */\nexport const COLLECTION_TOKEN_CATALOG: readonly BindingTokenCatalogEntry[] = [\n {\n token: '{{collection.name}}',\n label: 'Collection name',\n description: 'Display name of the routed collection.',\n },\n {\n token: '{{collection.slug}}',\n label: 'Collection slug',\n description: 'URL slug of the routed collection.',\n },\n {\n token: '{{collection.category}}',\n label: 'Filtered category',\n description: 'Category the URL filtered on; empty when unfiltered.',\n },\n {\n token: '{{collection.categorySlug}}',\n label: 'Filtered category slug',\n description: 'That category’s URL segment; empty when unfiltered.',\n },\n {\n token: '{{pagination.page}}',\n label: 'Current page',\n description: 'Page number this URL is showing.',\n },\n {\n token: '{{pagination.totalPages}}',\n label: 'Total pages',\n // A listing bigger than one read cannot be counted honestly (AGL-3219):\n // the count and the entries were read at two different moments, and the\n // pager built from them disagreed with itself at the page boundary. It\n // still resolves, so a template binding it keeps rendering.\n description:\n 'Pages in the listing, after any category filter. Empty on a listing too large to count — bind the next link instead.',\n },\n {\n token: '{{pagination.prevUrl}}',\n label: 'Previous page link',\n description: 'Keeps the category; empty on the first page.',\n },\n {\n token: '{{pagination.nextUrl}}',\n label: 'Next page link',\n // The reliable \"is there more\" (AGL-3219): it is set from a read that\n // asked for one entry more than the page needed, so empty means there was\n // no such entry rather than that a total said so.\n description:\n 'Keeps the category; empty when there is nothing older to show.',\n },\n]\n\n/**\n * `{{author.*}}` tokens (AGL-2518) — resolve on an author's own page,\n * `/author/{slug}`.\n *\n * Empty everywhere else, like the category tokens above and for the same\n * reason: a template has no runtime conditional, so the tokens have to be the\n * thing that varies. A heading bound to `{{author.name}}` prints a name on an\n * author page and nothing anywhere else.\n *\n * The `{{pagination.*}}` tokens resolve HERE TOO, over this author's own\n * archive — deliberately the same four names a collection listing uses, so a\n * pager built once works on both.\n */\nexport const AUTHOR_TOKEN_CATALOG: readonly BindingTokenCatalogEntry[] = [\n {\n token: '{{author.name}}',\n label: 'Name',\n description: 'The byline this page collects.',\n },\n {\n token: '{{author.bio}}',\n label: 'Bio',\n description: 'Their blurb, from the author record.',\n },\n {\n token: '{{author.image}}',\n label: 'Portrait',\n description: 'Their portrait or logo, from the author record.',\n },\n {\n token: '{{author.jobTitle}}',\n label: 'Role',\n description: 'Their job title; empty for an Organization author.',\n },\n {\n token: '{{author.worksFor}}',\n label: 'Organization',\n description: 'Who they write for; empty for an Organization author.',\n },\n {\n token: '{{author.url}}',\n label: 'Their own site',\n description:\n 'The url on their record — a personal site, not this page. Empty when ' +\n 'they have none.',\n },\n {\n token: '{{author.pageUrl}}',\n label: 'This page',\n description: 'The canonical address of the page you are designing.',\n },\n {\n token: '{{author.entryCount}}',\n label: 'Post count',\n description: 'How many entries they have published, across every collection.',\n },\n {\n token: '{{author.entryCountLabel}}',\n label: 'Post count, worded',\n description:\n '\"1 post\" or \"12 posts\" — pluralized for you, because a template has no ' +\n 'conditional to do it with.',\n },\n]\n\n/**\n * Renders an `{{item.*}}` token for a dataset field, optionally hopping\n * one reference to a field of the referenced record (AGL-180). Always\n * takes the stable model field ID — display names are labels only.\n */\nexport function datasetItemToken(\n fieldId: string,\n targetFieldId?: string,\n): string {\n return targetFieldId\n ? `{{item.${fieldId}.${targetFieldId}}}`\n : `{{item.${fieldId}}}`\n}\n\n/**\n * Dataset-item token entries for a repeatable container's model: one per\n * field in model order, labeled with the CURRENT display name while the\n * token carries the stable reference id (AGL-578 — renaming a label never\n * breaks bindings). Reference fields with a configured display field also\n * get their one-hop token (`{{item.author.name}}`).\n */\nexport function datasetItemTokens(\n model: DatasetModel,\n): BindingTokenCatalogEntry[] {\n const entries: BindingTokenCatalogEntry[] = []\n for (const fieldId of model.order ?? []) {\n const field = model.fields?.[fieldId]\n if (!field) continue\n const label = field.name?.trim() || humanizeDatasetFieldId(fieldId)\n entries.push({\n token: datasetItemToken(fieldId),\n label,\n description: DATASET_FIELD_TYPE_LABELS[field.type] ?? field.type,\n })\n // One reference hop (AGL-180): surface the configured display field of\n // the referenced record — the hop editors reach for most.\n const hopId =\n field.type === 'reference' ? field.reference?.displayFieldId : undefined\n if (hopId) {\n entries.push({\n token: datasetItemToken(fieldId, hopId),\n label: `${label} → ${humanizeDatasetFieldId(hopId)}`,\n description: 'Field from the referenced record.',\n })\n }\n }\n return entries\n}\n"],"names":["DATASET_FIELD_TYPE_LABELS","humanizeDatasetFieldId","ENTRY_TOKEN_CATALOG","token","label","description","COLLECTION_TOKEN_CATALOG","AUTHOR_TOKEN_CATALOG","datasetItemToken","fieldId","targetFieldId","datasetItemTokens","model","entries","order","field","fields","name","trim","push","type","hopId","reference","displayFieldId","undefined"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,yBAAyB,QAEpB,sBAAkB;AACzB,SAASC,sBAAsB,QAAQ,gBAAY;AAsBnD;;;;CAIC,GACD,OAAO,MAAMC,sBAA2D;IACtE;QAAEC,OAAO;QAAmBC,OAAO;QAASC,aAAa;IAAsB;IAC/E;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;;;;EAIA,GACA;QACEF,OAAO;QACPC,OAAO;QACPC,aACE,qEACA;IACJ;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aACE,2EACA;IACJ;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;;;;;;;;EAQA,GACA;QACEF,OAAO;QACPC,OAAO;QACPC,aACE,0EACA,wEACA;IACJ;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aACE,qEACA;IACJ;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;CACD,CAAA;AAED;;;;;;;;;;CAUC,GACD,OAAO,MAAMC,2BAAgE;IAC3E;QACEH,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACP,wEAAwE;QACxE,wEAAwE;QACxE,uEAAuE;QACvE,4DAA4D;QAC5DC,aACE;IACJ;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACP,sEAAsE;QACtE,0EAA0E;QAC1E,kDAAkD;QAClDC,aACE;IACJ;CACD,CAAA;AAED;;;;;;;;;;;;CAYC,GACD,OAAO,MAAME,uBAA4D;IACvE;QACEJ,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aACE,0EACA;IACJ;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aACE,4EACA;IACJ;CACD,CAAA;AAED;;;;CAIC,GACD,OAAO,SAASG,iBACdC,OAAe,EACfC,aAAsB;IAEtB,OAAOA,gBACH,CAAC,OAAO,EAAED,QAAQ,CAAC,EAAEC,cAAc,EAAE,CAAC,GACtC,CAAC,OAAO,EAAED,QAAQ,EAAE,CAAC;AAC3B;AAEA;;;;;;CAMC,GACD,OAAO,SAASE,kBACdC,KAAmB;QAGGA;IADtB,MAAMC,UAAsC,EAAE;IAC9C,KAAK,MAAMJ,YAAWG,eAAAA,MAAME,KAAK,YAAXF,eAAe,EAAE,CAAE;YAOxBZ;YANDY,eAEAG,aASiBA;QAX/B,MAAMA,SAAQH,gBAAAA,MAAMI,MAAM,qBAAZJ,aAAc,CAACH,QAAQ;QACrC,IAAI,CAACM,OAAO;QACZ,MAAMX,QAAQW,EAAAA,cAAAA,MAAME,IAAI,qBAAVF,YAAYG,IAAI,OAAMjB,uBAAuBQ;QAC3DI,QAAQM,IAAI,CAAC;YACXhB,OAAOK,iBAAiBC;YACxBL;YACAC,WAAW,GAAEL,wCAAAA,yBAAyB,CAACe,MAAMK,IAAI,CAAC,YAArCpB,wCAAyCe,MAAMK,IAAI;QAClE;QACA,uEAAuE;QACvE,0DAA0D;QAC1D,MAAMC,QACJN,MAAMK,IAAI,KAAK,eAAcL,mBAAAA,MAAMO,SAAS,qBAAfP,iBAAiBQ,cAAc,GAAGC;QACjE,IAAIH,OAAO;YACTR,QAAQM,IAAI,CAAC;gBACXhB,OAAOK,iBAAiBC,SAASY;gBACjCjB,OAAO,GAAGA,MAAM,GAAG,EAAEH,uBAAuBoB,QAAQ;gBACpDhB,aAAa;YACf;QACF;IACF;IACA,OAAOQ;AACT"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/binding-token-catalog.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 * Browsable data-placeholder catalogs for the designer's insert picker\n * (AGL-583). Hand-typing `{{entry.title}}` stays the advanced path; these\n * catalogs give every token a friendly label + description so editors can\n * browse and insert instead of memorizing the grammar.\n *\n * The token STRINGS are owned elsewhere — `collectionEntryTokens`\n * (collection-entries.ts) and the tenant compose pipeline resolve them —\n * this module only names them for pickers. The spec cross-checks the entry\n * catalog against the resolver so the two can never drift.\n */\nexport interface BindingTokenCatalogEntry {\n /** The literal token inserted into the prop, e.g. `{{entry.title}}`. */\n token: string\n /** Friendly picker label, e.g. `Title`. */\n label: string\n /** One-line description shown as the option's secondary text. */\n description?: string\n}\n\n/**\n * `{{entry.*}}` tokens (AGL-105/551/582) — resolve per entry inside a\n * Collection entries block and page-wide on entry-template screens.\n * Mirrors {@link collectionEntryTokens}; the spec enforces the mirror.\n */\nexport const ENTRY_TOKEN_CATALOG: readonly BindingTokenCatalogEntry[] = [\n { token: '{{entry.title}}', label: 'Title', description: 'The entry headline.' },\n {\n token: '{{entry.excerpt}}',\n label: 'Excerpt',\n description: 'Short summary text.',\n },\n {\n token: '{{entry.body}}',\n label: 'Body',\n description: 'The full entry body.',\n },\n {\n token: '{{entry.url}}',\n label: 'Link URL',\n description: 'Auto-route to the entry page.',\n },\n {\n token: '{{entry.date}}',\n label: 'Published date',\n description: 'Formatted publish date.',\n },\n /*\n Beside the readable date on purpose: a designer reaching for \"the date\" in\n a Video element's Publication date field has to see that the readable one\n is the wrong pick there, and why.\n */\n {\n token: '{{entry.publishedAt}}',\n label: 'Publish timestamp',\n description:\n 'The publish date and time in ISO 8601, for a field that needs a ' +\n 'machine-readable date, such as a Video element’s publication date.',\n },\n {\n token: '{{entry.author}}',\n label: 'Author',\n description: 'The byline set on the entry.',\n },\n {\n token: '{{entry.authorBio}}',\n label: 'Author bio',\n description: 'Blurb from the author’s record.',\n },\n {\n token: '{{entry.authorImage}}',\n label: 'Author portrait',\n description: 'Portrait or logo from the author’s record.',\n },\n {\n token: '{{entry.authorUrl}}',\n label: 'Author link',\n description: 'The author’s own page.',\n },\n {\n token: '{{entry.authorPageUrl}}',\n label: 'Author page',\n description:\n 'This author’s page on this site — everything they wrote, across every ' +\n 'collection. Separate from Author link, which is their own site.',\n },\n {\n token: '{{entry.slug}}',\n label: 'Slug',\n description: 'URL-safe entry identifier.',\n },\n /*\n Named \"Its collection…\" rather than \"Collection…\", which is what these\n describe and also exactly what the `{{collection.*}}` entries below are\n called. The picker groups options by heading, so the two sets sit apart —\n but a designer scanning it reads the LABEL, and two options reading\n \"Collection name\" three lines apart is a choice made by guessing. The\n pronoun is doing real work: on the one page where both resolve, they mean\n different collections.\n */\n {\n token: '{{entry.collection}}',\n label: 'Its collection',\n description:\n 'Which section this entry belongs to. Worth binding on a listing that ' +\n 'MIXES collections — an author page — where a card otherwise cannot ' +\n 'tell a release note from an essay.',\n },\n {\n token: '{{entry.collectionSlug}}',\n label: 'Its collection slug',\n description: 'That collection’s URL segment.',\n },\n {\n token: '{{entry.collectionUrl}}',\n label: 'Its collection link',\n description: 'The listing this entry belongs to.',\n },\n {\n token: '{{entry.coverImage}}',\n label: 'Cover image',\n description: 'Cover image URL.',\n },\n {\n token: '{{entry.coverVideo}}',\n label: 'Featured video',\n description:\n 'The featured video’s source — a library film, a video file link or ' +\n 'a video host’s link — for a Video element.',\n },\n {\n token: '{{entry.category}}',\n label: 'Category',\n description: 'The entry category.',\n },\n {\n token: '{{entry.tags}}',\n label: 'Tags',\n description: 'Tags, comma separated.',\n },\n {\n token: '{{entry.seoTitle}}',\n label: 'SEO title',\n description: 'Search title; falls back to Title.',\n },\n {\n token: '{{entry.seoDescription}}',\n label: 'SEO description',\n description: 'Meta description; falls back to Excerpt.',\n },\n]\n\n/**\n * `{{collection.*}}` and `{{pagination.*}}` tokens (AGL-551/1321/1386) —\n * resolve on collection list/entry template screens (see the tenant compose\n * pipeline's collection tokens).\n *\n * The category and pagination tokens all resolve to the empty string where\n * they do not apply, so they are safe to bind unconditionally: ONE list\n * screen serves the bare listing, every `/page/{n}` and every\n * `/category/{slug}`, and a template has no runtime conditional to vary\n * itself with.\n */\nexport const COLLECTION_TOKEN_CATALOG: readonly BindingTokenCatalogEntry[] = [\n {\n token: '{{collection.name}}',\n label: 'Collection name',\n description: 'Display name of the routed collection.',\n },\n {\n token: '{{collection.slug}}',\n label: 'Collection slug',\n description: 'URL slug of the routed collection.',\n },\n {\n token: '{{collection.category}}',\n label: 'Filtered category',\n description: 'Category the URL filtered on; empty when unfiltered.',\n },\n {\n token: '{{collection.categorySlug}}',\n label: 'Filtered category slug',\n description: 'That category’s URL segment; empty when unfiltered.',\n },\n {\n token: '{{pagination.page}}',\n label: 'Current page',\n description: 'Page number this URL is showing.',\n },\n {\n token: '{{pagination.totalPages}}',\n label: 'Total pages',\n // A listing bigger than one read cannot be counted honestly (AGL-3219):\n // the count and the entries were read at two different moments, and the\n // pager built from them disagreed with itself at the page boundary. It\n // still resolves, so a template binding it keeps rendering.\n description:\n 'Pages in the listing, after any category filter. Empty on a listing too large to count — bind the next link instead.',\n },\n {\n token: '{{pagination.prevUrl}}',\n label: 'Previous page link',\n description: 'Keeps the category; empty on the first page.',\n },\n {\n token: '{{pagination.nextUrl}}',\n label: 'Next page link',\n // The reliable \"is there more\" (AGL-3219): it is set from a read that\n // asked for one entry more than the page needed, so empty means there was\n // no such entry rather than that a total said so.\n description:\n 'Keeps the category; empty when there is nothing older to show.',\n },\n]\n\n/**\n * `{{author.*}}` tokens (AGL-2518) — resolve on an author's own page,\n * `/author/{slug}`.\n *\n * Empty everywhere else, like the category tokens above and for the same\n * reason: a template has no runtime conditional, so the tokens have to be the\n * thing that varies. A heading bound to `{{author.name}}` prints a name on an\n * author page and nothing anywhere else.\n *\n * The `{{pagination.*}}` tokens resolve HERE TOO, over this author's own\n * archive — deliberately the same four names a collection listing uses, so a\n * pager built once works on both.\n */\nexport const AUTHOR_TOKEN_CATALOG: readonly BindingTokenCatalogEntry[] = [\n {\n token: '{{author.name}}',\n label: 'Name',\n description: 'The byline this page collects.',\n },\n {\n token: '{{author.bio}}',\n label: 'Bio',\n description: 'Their blurb, from the author record.',\n },\n {\n token: '{{author.image}}',\n label: 'Portrait',\n description: 'Their portrait or logo, from the author record.',\n },\n {\n token: '{{author.jobTitle}}',\n label: 'Role',\n description: 'Their job title; empty for an Organization author.',\n },\n {\n token: '{{author.worksFor}}',\n label: 'Organization',\n description: 'Who they write for; empty for an Organization author.',\n },\n {\n token: '{{author.url}}',\n label: 'Their own site',\n description:\n 'The url on their record — a personal site, not this page. Empty when ' +\n 'they have none.',\n },\n {\n token: '{{author.pageUrl}}',\n label: 'This page',\n description: 'The canonical address of the page you are designing.',\n },\n {\n token: '{{author.entryCount}}',\n label: 'Post count',\n description: 'How many entries they have published, across every collection.',\n },\n {\n token: '{{author.entryCountLabel}}',\n label: 'Post count, worded',\n description:\n '\"1 post\" or \"12 posts\" — pluralized for you, because a template has no ' +\n 'conditional to do it with.',\n },\n]\n\n/**\n * The `{{item.*}}` token for one field of the record a repeat is rendering,\n * optionally hopping one reference to a field of the referenced record\n * (AGL-180). Always takes the stable field id — display names are labels\n * only, so renaming a field never breaks a binding (AGL-578).\n */\nexport function repeatItemToken(\n fieldId: string,\n targetFieldId?: string,\n): string {\n return targetFieldId\n ? `{{item.${fieldId}.${targetFieldId}}}`\n : `{{item.${fieldId}}}`\n}\n"],"names":["ENTRY_TOKEN_CATALOG","token","label","description","COLLECTION_TOKEN_CATALOG","AUTHOR_TOKEN_CATALOG","repeatItemToken","fieldId","targetFieldId"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;CAUC,GAUD;;;;CAIC,GACD,OAAO,MAAMA,sBAA2D;IACtE;QAAEC,OAAO;QAAmBC,OAAO;QAASC,aAAa;IAAsB;IAC/E;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;;;;EAIA,GACA;QACEF,OAAO;QACPC,OAAO;QACPC,aACE,qEACA;IACJ;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aACE,2EACA;IACJ;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;;;;;;;;EAQA,GACA;QACEF,OAAO;QACPC,OAAO;QACPC,aACE,0EACA,wEACA;IACJ;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aACE,wEACA;IACJ;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;CACD,CAAA;AAED;;;;;;;;;;CAUC,GACD,OAAO,MAAMC,2BAAgE;IAC3E;QACEH,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACP,wEAAwE;QACxE,wEAAwE;QACxE,uEAAuE;QACvE,4DAA4D;QAC5DC,aACE;IACJ;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACP,sEAAsE;QACtE,0EAA0E;QAC1E,kDAAkD;QAClDC,aACE;IACJ;CACD,CAAA;AAED;;;;;;;;;;;;CAYC,GACD,OAAO,MAAME,uBAA4D;IACvE;QACEJ,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aACE,0EACA;IACJ;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aACE,4EACA;IACJ;CACD,CAAA;AAED;;;;;CAKC,GACD,OAAO,SAASG,gBACdC,OAAe,EACfC,aAAsB;IAEtB,OAAOA,gBACH,CAAC,OAAO,EAAED,QAAQ,CAAC,EAAEC,cAAc,EAAE,CAAC,GACtC,CAAC,OAAO,EAAED,QAAQ,EAAE,CAAC;AAC3B"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import type { ResolvedBrandingProfile } from './platform-brand';
|
|
18
|
+
/**
|
|
19
|
+
* The "Need help?" footer of a built-in transactional email — **or nothing**
|
|
20
|
+
* (AGL-2428).
|
|
21
|
+
*
|
|
22
|
+
* A white-label organization that left its Support URL blank resolves
|
|
23
|
+
* `supportUrl` to null, and this returns the empty string rather than a line
|
|
24
|
+
* pointing at Aglyn. The recipient of that mail is the organization's own
|
|
25
|
+
* customer: our desk cannot help them, and the link would name their vendor
|
|
26
|
+
* to somebody who was never told one exists.
|
|
27
|
+
*
|
|
28
|
+
* It returns the SEPARATOR too, not just the sentence. A caller that
|
|
29
|
+
* concatenated `\n\n` itself would leave two blank lines at the end of the
|
|
30
|
+
* message for exactly the orgs this exists for — the "gap where something
|
|
31
|
+
* should be" that `emailLogoUrl` already avoids one field over.
|
|
32
|
+
*
|
|
33
|
+
* Every org WITHOUT the concealment entitlement resolves to the platform
|
|
34
|
+
* profile, whose support URL is real, so those emails are unchanged.
|
|
35
|
+
*/
|
|
36
|
+
export declare function brandSupportLine(branding: Pick<ResolvedBrandingProfile, 'supportUrl'>): string;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ /**
|
|
17
|
+
* The "Need help?" footer of a built-in transactional email — **or nothing**
|
|
18
|
+
* (AGL-2428).
|
|
19
|
+
*
|
|
20
|
+
* A white-label organization that left its Support URL blank resolves
|
|
21
|
+
* `supportUrl` to null, and this returns the empty string rather than a line
|
|
22
|
+
* pointing at Aglyn. The recipient of that mail is the organization's own
|
|
23
|
+
* customer: our desk cannot help them, and the link would name their vendor
|
|
24
|
+
* to somebody who was never told one exists.
|
|
25
|
+
*
|
|
26
|
+
* It returns the SEPARATOR too, not just the sentence. A caller that
|
|
27
|
+
* concatenated `\n\n` itself would leave two blank lines at the end of the
|
|
28
|
+
* message for exactly the orgs this exists for — the "gap where something
|
|
29
|
+
* should be" that `emailLogoUrl` already avoids one field over.
|
|
30
|
+
*
|
|
31
|
+
* Every org WITHOUT the concealment entitlement resolves to the platform
|
|
32
|
+
* profile, whose support URL is real, so those emails are unchanged.
|
|
33
|
+
*/ export function brandSupportLine(branding) {
|
|
34
|
+
const url = typeof branding.supportUrl === 'string' ? branding.supportUrl.trim() : '';
|
|
35
|
+
return url ? `\n\nNeed help? ${url}` : '';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
//# sourceMappingURL=brand-support-line.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/brand-support-line.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { ResolvedBrandingProfile } from './platform-brand'\n\n/**\n * The \"Need help?\" footer of a built-in transactional email — **or nothing**\n * (AGL-2428).\n *\n * A white-label organization that left its Support URL blank resolves\n * `supportUrl` to null, and this returns the empty string rather than a line\n * pointing at Aglyn. The recipient of that mail is the organization's own\n * customer: our desk cannot help them, and the link would name their vendor\n * to somebody who was never told one exists.\n *\n * It returns the SEPARATOR too, not just the sentence. A caller that\n * concatenated `\\n\\n` itself would leave two blank lines at the end of the\n * message for exactly the orgs this exists for — the \"gap where something\n * should be\" that `emailLogoUrl` already avoids one field over.\n *\n * Every org WITHOUT the concealment entitlement resolves to the platform\n * profile, whose support URL is real, so those emails are unchanged.\n */\nexport function brandSupportLine(\n branding: Pick<ResolvedBrandingProfile, 'supportUrl'>,\n): string {\n const url = typeof branding.supportUrl === 'string' ? branding.supportUrl.trim() : ''\n return url ? `\\n\\nNeed help? ${url}` : ''\n}\n"],"names":["brandSupportLine","branding","url","supportUrl","trim"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAID;;;;;;;;;;;;;;;;;CAiBC,GACD,OAAO,SAASA,iBACdC,QAAqD;IAErD,MAAMC,MAAM,OAAOD,SAASE,UAAU,KAAK,WAAWF,SAASE,UAAU,CAACC,IAAI,KAAK;IACnF,OAAOF,MAAM,CAAC,eAAe,EAAEA,KAAK,GAAG;AACzC"}
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
*
|
|
43
43
|
* `localStorage`, expiring at {@link ATTRIBUTION_WINDOW_DAYS}.
|
|
44
44
|
*
|
|
45
|
-
* `
|
|
45
|
+
* `utm-forwarding.ts` holds its own campaign in `sessionStorage` and
|
|
46
46
|
* argues for it: a first touch that outlived the visit would start
|
|
47
47
|
* attributing next week's organic return to this week's ad. That argument is
|
|
48
48
|
* correct for the question it answers — which campaign produced THIS SIGNUP,
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
* The revenue join credits the LAST click, and a product where a lead and an
|
|
65
65
|
* order attribute by different rules is worse than one that is uniformly
|
|
66
66
|
* approximate. So a new campaign arrival OVERWRITES the stored one. This is
|
|
67
|
-
* the deliberate opposite of {@link
|
|
67
|
+
* the deliberate opposite of {@link rememberVisitUtm}, which keeps the
|
|
68
68
|
* first — that one describes a single visit's origin and this one describes
|
|
69
69
|
* which campaign most recently brought a person back.
|
|
70
70
|
*
|
|
@@ -104,7 +104,7 @@
|
|
|
104
104
|
* ## What is never carried
|
|
105
105
|
*
|
|
106
106
|
* Only the three allowlisted `utm_` labels, scrubbed by
|
|
107
|
-
* {@link
|
|
107
|
+
* {@link parseUtmAttribution} — which refuses an email-shaped value
|
|
108
108
|
* outright. No address, no person's identifier, no click id, nothing that
|
|
109
109
|
* names the visitor. A campaign link is exactly where putting a recipient in
|
|
110
110
|
* a query string would be tempting, and the parser is what makes it
|
|
@@ -120,7 +120,7 @@
|
|
|
120
120
|
* are the WEB channel's touch, and a campaign that carries none is joined by
|
|
121
121
|
* address instead.
|
|
122
122
|
*/
|
|
123
|
-
import { type
|
|
123
|
+
import { type UtmAttribution } from './utm-attribution';
|
|
124
124
|
/** Days between a campaign touch and a conversion it may be credited with. */
|
|
125
125
|
export declare const ATTRIBUTION_WINDOW_DAYS = 7;
|
|
126
126
|
/** The same window in milliseconds. */
|
|
@@ -137,7 +137,7 @@ export declare const CAMPAIGN_TOUCH_STORAGE_KEY = "aglyn:campaign-touch";
|
|
|
137
137
|
*/
|
|
138
138
|
export declare const CAMPAIGN_TOUCH_TIME_KEY = "t";
|
|
139
139
|
/** A campaign the visitor arrived from, and when they arrived from it. */
|
|
140
|
-
export interface CampaignTouch extends
|
|
140
|
+
export interface CampaignTouch extends UtmAttribution {
|
|
141
141
|
/** When the visitor followed the campaign link, epoch ms. */
|
|
142
142
|
atMs: number;
|
|
143
143
|
}
|