@aglyn/aglyn 1.0.0-beta.217 → 1.0.0-beta.219
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +14 -14
- package/src/lib/app-utils/actions.d.ts +25 -374
- package/src/lib/app-utils/actions.js +101 -561
- package/src/lib/app-utils/actions.js.map +1 -1
- package/src/lib/app-utils/activity-presenter.d.ts +3 -3
- package/src/lib/app-utils/activity-presenter.js +36 -52
- package/src/lib/app-utils/activity-presenter.js.map +1 -1
- package/src/lib/app-utils/admin-audit-index.js +2 -2
- package/src/lib/app-utils/admin-audit-index.js.map +1 -1
- package/src/lib/app-utils/analytics-events.d.ts +11 -2
- package/src/lib/app-utils/analytics-events.js +11 -2
- package/src/lib/app-utils/analytics-events.js.map +1 -1
- package/src/lib/app-utils/analytics-provider.d.ts +182 -0
- package/src/lib/app-utils/analytics-provider.js +133 -0
- package/src/lib/app-utils/analytics-provider.js.map +1 -0
- package/src/lib/app-utils/api-plugins.d.ts +46 -0
- package/src/lib/app-utils/api-plugins.js +19 -0
- package/src/lib/app-utils/api-plugins.js.map +1 -1
- package/src/lib/app-utils/{marketplace-overrides.d.ts → artifact-overrides.d.ts} +9 -6
- package/src/lib/app-utils/{marketplace-overrides.js → artifact-overrides.js} +10 -7
- package/src/lib/app-utils/artifact-overrides.js.map +1 -0
- package/src/lib/app-utils/{marketplace-provenance.d.ts → artifact-provenance.d.ts} +12 -10
- package/src/lib/app-utils/{marketplace-provenance.js → artifact-provenance.js} +9 -7
- package/src/lib/app-utils/artifact-provenance.js.map +1 -0
- package/src/lib/app-utils/assist-credits.js +2 -1
- package/src/lib/app-utils/assist-credits.js.map +1 -1
- package/src/lib/app-utils/binding-token-catalog.d.ts +5 -13
- package/src/lib/app-utils/binding-token-catalog.js +17 -41
- package/src/lib/app-utils/binding-token-catalog.js.map +1 -1
- package/src/lib/app-utils/binding-tokens.js +1 -1
- package/src/lib/app-utils/binding-tokens.js.map +1 -1
- package/src/lib/app-utils/brand-support-line.d.ts +36 -0
- package/src/lib/app-utils/brand-support-line.js +38 -0
- package/src/lib/app-utils/brand-support-line.js.map +1 -0
- package/src/lib/app-utils/campaign-touch.d.ts +5 -5
- package/src/lib/app-utils/campaign-touch.js +8 -8
- package/src/lib/app-utils/campaign-touch.js.map +1 -1
- package/src/lib/app-utils/collection-entries.d.ts +2 -2
- package/src/lib/app-utils/collection-entries.js.map +1 -1
- package/src/lib/app-utils/consent-group-change.d.ts +4 -0
- package/src/lib/app-utils/consent-group-change.js.map +1 -1
- package/src/lib/app-utils/console-record-links.d.ts +10 -89
- package/src/lib/app-utils/console-record-links.js +3 -107
- package/src/lib/app-utils/console-record-links.js.map +1 -1
- package/src/lib/app-utils/console-routes.d.ts +16 -7
- package/src/lib/app-utils/console-routes.js +9 -3
- package/src/lib/app-utils/console-routes.js.map +1 -1
- package/src/lib/app-utils/contacts.d.ts +23 -1
- package/src/lib/app-utils/contacts.js +21 -5
- package/src/lib/app-utils/contacts.js.map +1 -1
- package/src/lib/app-utils/cross-pool-desync.d.ts +39 -0
- package/src/lib/app-utils/cross-pool-desync.js +47 -0
- package/src/lib/app-utils/cross-pool-desync.js.map +1 -0
- package/src/lib/app-utils/csv-upload.d.ts +29 -0
- package/src/lib/app-utils/csv-upload.js +29 -0
- package/src/lib/app-utils/csv-upload.js.map +1 -0
- package/src/lib/app-utils/dataset-models.d.ts +5 -5
- package/src/lib/app-utils/dataset-models.js +19 -2
- package/src/lib/app-utils/dataset-models.js.map +1 -1
- package/src/lib/app-utils/datasets.d.ts +65 -11
- package/src/lib/app-utils/datasets.js +74 -11
- package/src/lib/app-utils/datasets.js.map +1 -1
- package/src/lib/app-utils/docs-deflection.d.ts +6 -6
- package/src/lib/app-utils/docs-deflection.js.map +1 -1
- package/src/lib/app-utils/docs-help.generated.d.ts +12 -6
- package/src/lib/app-utils/docs-help.generated.js +21 -6
- package/src/lib/app-utils/docs-help.generated.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.d.ts +2 -2
- package/src/lib/app-utils/docs-index.generated.js +514 -318
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/docs-retrieval.d.ts +3 -3
- package/src/lib/app-utils/docs-retrieval.js +14 -10
- package/src/lib/app-utils/docs-retrieval.js.map +1 -1
- package/src/lib/app-utils/duplicate-resource.d.ts +4 -1
- package/src/lib/app-utils/duplicate-resource.js +7 -4
- package/src/lib/app-utils/duplicate-resource.js.map +1 -1
- package/src/lib/app-utils/dynamic-list-rule.d.ts +1 -1
- package/src/lib/app-utils/dynamic-list-rule.js.map +1 -1
- package/src/lib/app-utils/element-ui.d.ts +1 -1
- package/src/lib/app-utils/element-ui.js +1 -1
- package/src/lib/app-utils/element-ui.js.map +1 -1
- package/src/lib/app-utils/email-starter-policy.js +3 -3
- package/src/lib/app-utils/email-starter-policy.js.map +1 -1
- package/src/lib/app-utils/entity-picker-context.d.ts +10 -5
- package/src/lib/app-utils/entity-picker-context.js.map +1 -1
- package/src/lib/app-utils/error-beacon.d.ts +75 -6
- package/src/lib/app-utils/error-beacon.js +160 -21
- package/src/lib/app-utils/error-beacon.js.map +1 -1
- package/src/lib/app-utils/expand-repeatables.d.ts +10 -10
- package/src/lib/app-utils/expand-repeatables.js +10 -10
- package/src/lib/app-utils/expand-repeatables.js.map +1 -1
- package/src/lib/app-utils/host-events.d.ts +82 -0
- package/src/lib/app-utils/host-events.js +62 -0
- package/src/lib/app-utils/host-events.js.map +1 -0
- package/src/lib/app-utils/lockdown.d.ts +33 -1
- package/src/lib/app-utils/lockdown.js +45 -4
- package/src/lib/app-utils/lockdown.js.map +1 -1
- package/src/lib/app-utils/margin-utilization.d.ts +4 -69
- package/src/lib/app-utils/margin-utilization.js +82 -69
- package/src/lib/app-utils/margin-utilization.js.map +1 -1
- package/src/lib/app-utils/node-definition-sanitizer.d.ts +16 -16
- package/src/lib/app-utils/node-definition-sanitizer.js +24 -20
- package/src/lib/app-utils/node-definition-sanitizer.js.map +1 -1
- package/src/lib/app-utils/node-interactions.d.ts +7 -7
- package/src/lib/app-utils/node-interactions.js.map +1 -1
- package/src/lib/app-utils/notifications.d.ts +78 -47
- package/src/lib/app-utils/notifications.js +63 -44
- package/src/lib/app-utils/notifications.js.map +1 -1
- package/src/lib/app-utils/payments-configured.d.ts +8 -21
- package/src/lib/app-utils/payments-configured.js +8 -23
- package/src/lib/app-utils/payments-configured.js.map +1 -1
- package/src/lib/app-utils/plan-entitlements.d.ts +43 -128
- package/src/lib/app-utils/plan-entitlements.js +145 -304
- package/src/lib/app-utils/plan-entitlements.js.map +1 -1
- package/src/lib/app-utils/platform-brand.d.ts +10 -0
- package/src/lib/app-utils/platform-brand.js +14 -3
- package/src/lib/app-utils/platform-brand.js.map +1 -1
- package/src/lib/app-utils/plugin-api-rate-limit.d.ts +7 -1
- package/src/lib/app-utils/plugin-api-rate-limit.js +11 -3
- package/src/lib/app-utils/plugin-api-rate-limit.js.map +1 -1
- package/src/lib/app-utils/plugin-bundle-checks.js +2 -2
- package/src/lib/app-utils/plugin-bundle-checks.js.map +1 -1
- package/src/lib/app-utils/plugin-host-events.generated.d.ts +12 -0
- package/src/lib/app-utils/plugin-host-events.generated.js +169 -0
- package/src/lib/app-utils/plugin-host-events.generated.js.map +1 -0
- package/src/lib/app-utils/plugin-release-flags.generated.d.ts +12 -0
- package/src/lib/app-utils/plugin-release-flags.generated.js +106 -0
- package/src/lib/app-utils/plugin-release-flags.generated.js.map +1 -0
- package/src/lib/app-utils/realm-host-surface.generated.js +2 -0
- package/src/lib/app-utils/realm-host-surface.generated.js.map +1 -1
- package/src/lib/app-utils/release-flags.d.ts +14 -5
- package/src/lib/app-utils/release-flags.js +21 -125
- package/src/lib/app-utils/release-flags.js.map +1 -1
- package/src/lib/app-utils/repeat-sources.d.ts +9 -0
- package/src/lib/app-utils/repeat-sources.js.map +1 -1
- package/src/lib/app-utils/retained-refusals.d.ts +73 -0
- package/src/lib/app-utils/retained-refusals.js +186 -0
- package/src/lib/app-utils/retained-refusals.js.map +1 -0
- package/src/lib/app-utils/scope-tokens.d.ts +27 -0
- package/src/lib/app-utils/scope-tokens.js +61 -0
- package/src/lib/app-utils/scope-tokens.js.map +1 -1
- package/src/lib/app-utils/screen-link-context.js +3 -3
- package/src/lib/app-utils/screen-link-context.js.map +1 -1
- package/src/lib/app-utils/screen-route.d.ts +15 -0
- package/src/lib/app-utils/screen-route.js +21 -1
- package/src/lib/app-utils/screen-route.js.map +1 -1
- package/src/lib/app-utils/seo-audit.d.ts +132 -0
- package/src/lib/app-utils/seo-audit.js +246 -0
- package/src/lib/app-utils/seo-audit.js.map +1 -0
- package/src/lib/app-utils/seo-keywords.d.ts +48 -0
- package/src/lib/app-utils/seo-keywords.js +60 -0
- package/src/lib/app-utils/seo-keywords.js.map +1 -0
- package/src/lib/app-utils/seo-page-facts.d.ts +66 -0
- package/src/lib/app-utils/seo-page-facts.js +155 -0
- package/src/lib/app-utils/seo-page-facts.js.map +1 -0
- package/src/lib/app-utils/seo-site-scan.d.ts +126 -0
- package/src/lib/app-utils/seo-site-scan.js +162 -0
- package/src/lib/app-utils/seo-site-scan.js.map +1 -0
- package/src/lib/app-utils/seo-title-variables.js +1 -1
- package/src/lib/app-utils/seo-title-variables.js.map +1 -1
- package/src/lib/app-utils/server.d.ts +6 -9
- package/src/lib/app-utils/server.js +17 -27
- package/src/lib/app-utils/server.js.map +1 -1
- package/src/lib/app-utils/site-interactions.d.ts +501 -0
- package/src/lib/app-utils/site-interactions.js +554 -0
- package/src/lib/app-utils/site-interactions.js.map +1 -0
- package/src/lib/app-utils/{marketplace-theme.d.ts → site-theme.d.ts} +13 -2
- package/src/lib/app-utils/{marketplace-theme.js → site-theme.js} +5 -2
- package/src/lib/app-utils/site-theme.js.map +1 -0
- package/src/lib/app-utils/sitemap.d.ts +4 -3
- package/src/lib/app-utils/sitemap.js +9 -7
- package/src/lib/app-utils/sitemap.js.map +1 -1
- package/src/lib/app-utils/stale-build-error.d.ts +51 -0
- package/src/lib/app-utils/stale-build-error.js +100 -1
- package/src/lib/app-utils/stale-build-error.js.map +1 -1
- package/src/lib/app-utils/starter-templates.d.ts +20 -0
- package/src/lib/app-utils/starter-templates.js +49 -0
- package/src/lib/app-utils/starter-templates.js.map +1 -1
- package/src/lib/app-utils/tax-jurisdiction-figures.d.ts +118 -0
- package/src/lib/app-utils/tax-jurisdiction-figures.js +123 -0
- package/src/lib/app-utils/tax-jurisdiction-figures.js.map +1 -0
- package/src/lib/app-utils/theme-library.d.ts +10 -3
- package/src/lib/app-utils/theme-library.js +13 -4
- package/src/lib/app-utils/theme-library.js.map +1 -1
- package/src/lib/app-utils/{campaign-attribution.d.ts → utm-attribution.d.ts} +11 -6
- package/src/lib/app-utils/{campaign-attribution.js → utm-attribution.js} +13 -8
- package/src/lib/app-utils/utm-attribution.js.map +1 -0
- package/src/lib/app-utils/{campaign-forwarding.d.ts → utm-forwarding.d.ts} +14 -14
- package/src/lib/app-utils/{campaign-forwarding.js → utm-forwarding.js} +26 -26
- package/src/lib/app-utils/utm-forwarding.js.map +1 -0
- package/src/lib/app-utils/video-object.js +6 -5
- package/src/lib/app-utils/video-object.js.map +1 -1
- package/src/lib/app-utils/visitor-consent.d.ts +41 -164
- package/src/lib/app-utils/visitor-consent.js +66 -277
- package/src/lib/app-utils/visitor-consent.js.map +1 -1
- package/src/lib/app-utils/web-vitals-rum.d.ts +7 -7
- package/src/lib/app-utils/web-vitals-rum.js +31 -63
- package/src/lib/app-utils/web-vitals-rum.js.map +1 -1
- package/src/lib/app-utils/where-used.d.ts +11 -6
- package/src/lib/app-utils/where-used.js +3 -8
- package/src/lib/app-utils/where-used.js.map +1 -1
- package/src/lib/canvas-manager/canvas-manager.js +3 -3
- package/src/lib/canvas-manager/canvas-manager.js.map +1 -1
- package/src/lib/foundation/definitions/components.types.d.ts +1 -1
- package/src/lib/foundation/definitions/components.types.js +1 -1
- package/src/lib/foundation/definitions/components.types.js.map +1 -1
- package/src/lib/foundation/definitions/org-billing.types.d.ts +18 -135
- package/src/lib/foundation/definitions/org-billing.types.js.map +1 -1
- package/src/lib/foundation/definitions/organization.types.d.ts +6 -3
- package/src/lib/foundation/definitions/organization.types.js.map +1 -1
- package/src/lib/foundation/definitions/platform.types.d.ts +27 -5
- package/src/lib/foundation/definitions/platform.types.js.map +1 -1
- package/src/lib/foundation/definitions/write-deny-coverage.util.d.ts +16 -0
- package/src/lib/foundation/definitions/write-deny-coverage.util.js +20 -0
- package/src/lib/foundation/definitions/write-deny-coverage.util.js.map +1 -1
- package/src/lib/foundation/index.d.ts +1 -1
- package/src/lib/foundation/index.js.map +1 -1
- package/src/lib/plugin-manager/besigner-documents.d.ts +96 -0
- package/src/lib/plugin-manager/besigner-documents.js +61 -0
- package/src/lib/plugin-manager/besigner-documents.js.map +1 -0
- package/src/lib/plugin-manager/computed-variables.d.ts +71 -0
- package/src/lib/plugin-manager/computed-variables.js +74 -0
- package/src/lib/plugin-manager/computed-variables.js.map +1 -0
- package/src/lib/plugin-manager/custom-fields.d.ts +29 -2
- package/src/lib/plugin-manager/custom-fields.js +47 -1
- package/src/lib/plugin-manager/custom-fields.js.map +1 -1
- package/src/lib/plugin-manager/feature-plugins.d.ts +292 -20
- package/src/lib/plugin-manager/feature-plugins.js +105 -17
- package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
- package/src/lib/plugin-manager/first-party-plugins.generated.d.ts +123 -0
- package/src/lib/plugin-manager/first-party-plugins.generated.js +785 -14
- package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
- package/src/lib/plugin-manager/plugin-activity-actions.d.ts +12 -0
- package/src/lib/plugin-manager/plugin-activity-actions.js +25 -0
- package/src/lib/plugin-manager/plugin-activity-actions.js.map +1 -1
- package/src/lib/plugin-manager/plugin-contributions.d.ts +1 -1
- package/src/lib/plugin-manager/plugin-contributions.js.map +1 -1
- package/src/lib/plugin-manager/plugin-declarations-repair.d.ts +36 -0
- package/src/lib/plugin-manager/plugin-declarations-repair.js +64 -0
- package/src/lib/plugin-manager/plugin-declarations-repair.js.map +1 -0
- package/src/lib/plugin-manager/plugin-dependents.d.ts +101 -0
- package/src/lib/plugin-manager/plugin-dependents.js +75 -0
- package/src/lib/plugin-manager/plugin-dependents.js.map +1 -0
- package/src/lib/plugin-manager/plugin-distribution.d.ts +55 -0
- package/src/lib/plugin-manager/plugin-distribution.js +45 -0
- package/src/lib/plugin-manager/plugin-distribution.js.map +1 -0
- package/src/lib/plugin-manager/plugin-host-collections.d.ts +17 -0
- package/src/lib/plugin-manager/plugin-host-collections.js +6 -0
- package/src/lib/plugin-manager/plugin-host-collections.js.map +1 -1
- package/src/lib/plugin-manager/plugin-host-resources.d.ts +133 -0
- package/src/lib/plugin-manager/plugin-host-resources.js +52 -0
- package/src/lib/plugin-manager/plugin-host-resources.js.map +1 -0
- package/src/lib/plugin-manager/plugin-org-erasure.d.ts +28 -2
- package/src/lib/plugin-manager/plugin-org-erasure.js +61 -2
- package/src/lib/plugin-manager/plugin-org-erasure.js.map +1 -1
- package/src/lib/plugin-manager/plugin-plan-entitlements.d.ts +123 -0
- package/src/lib/plugin-manager/plugin-plan-entitlements.js +101 -0
- package/src/lib/plugin-manager/plugin-plan-entitlements.js.map +1 -0
- package/src/lib/plugin-manager/plugin-record-index.d.ts +95 -0
- package/src/lib/plugin-manager/plugin-record-index.js +51 -0
- package/src/lib/plugin-manager/plugin-record-index.js.map +1 -0
- package/src/lib/plugin-manager/plugin-record-written.d.ts +58 -0
- package/src/lib/plugin-manager/plugin-record-written.js +42 -0
- package/src/lib/plugin-manager/plugin-record-written.js.map +1 -0
- package/src/lib/plugin-manager/plugin-revenue-sources.d.ts +213 -0
- package/src/lib/plugin-manager/plugin-revenue-sources.js +194 -0
- package/src/lib/plugin-manager/plugin-revenue-sources.js.map +1 -0
- package/src/lib/plugin-manager/plugin-site-bundle.d.ts +145 -0
- package/src/lib/plugin-manager/plugin-site-bundle.js +78 -0
- package/src/lib/plugin-manager/plugin-site-bundle.js.map +1 -0
- package/src/lib/plugin-manager/plugin-site-cache.d.ts +12 -0
- package/src/lib/plugin-manager/plugin-site-cache.js.map +1 -1
- package/src/lib/plugin-manager/plugin-site-export.d.ts +81 -0
- package/src/lib/plugin-manager/plugin-site-export.js +40 -0
- package/src/lib/plugin-manager/plugin-site-export.js.map +1 -0
- package/src/lib/plugin-manager/plugin-sitemap-sections.d.ts +88 -0
- package/src/lib/plugin-manager/plugin-sitemap-sections.js +29 -0
- package/src/lib/plugin-manager/plugin-sitemap-sections.js.map +1 -0
- package/src/lib/plugin-manager/plugin-tax-profile.d.ts +20 -4
- package/src/lib/plugin-manager/plugin-tax-profile.js.map +1 -1
- package/src/lib/plugin-manager/plugin-tax-return-sources.d.ts +255 -0
- package/src/lib/plugin-manager/plugin-tax-return-sources.js +191 -0
- package/src/lib/plugin-manager/plugin-tax-return-sources.js.map +1 -0
- package/src/lib/plugin-manager/plugin-template-sources.d.ts +75 -0
- package/src/lib/plugin-manager/plugin-template-sources.js +37 -0
- package/src/lib/plugin-manager/plugin-template-sources.js.map +1 -0
- package/src/lib/plugin-manager/plugin-usage-axes.d.ts +107 -0
- package/src/lib/plugin-manager/plugin-usage-axes.js +107 -0
- package/src/lib/plugin-manager/plugin-usage-axes.js.map +1 -0
- package/src/lib/plugin-manager/plugin-zones.d.ts +4 -5
- package/src/lib/plugin-manager/plugin-zones.js.map +1 -1
- package/src/lib/plugin-manager/realm-host-aglyn.generated.js +2 -0
- package/src/lib/plugin-manager/realm-host-aglyn.generated.js.map +1 -1
- package/src/lib/plugin-manager/record-captured-contact.d.ts +1 -14
- package/src/lib/plugin-manager/record-captured-contact.js +10 -32
- package/src/lib/plugin-manager/record-captured-contact.js.map +1 -1
- package/src/lib/plugin-manager/repeat-rows.d.ts +124 -0
- package/src/lib/plugin-manager/repeat-rows.js +108 -0
- package/src/lib/plugin-manager/repeat-rows.js.map +1 -0
- package/src/lib/plugin-manager/submission-record-target.d.ts +144 -0
- package/src/lib/plugin-manager/submission-record-target.js +125 -0
- package/src/lib/plugin-manager/submission-record-target.js.map +1 -0
- package/src/lib/plugin-manager/video-embed-provider.d.ts +108 -0
- package/src/lib/plugin-manager/video-embed-provider.js +165 -0
- package/src/lib/plugin-manager/video-embed-provider.js.map +1 -0
- package/src/lib/types/nodes.d.ts +2 -2
- package/src/lib/types/nodes.js.map +1 -1
- package/src/lib/app-utils/campaign-attribution.js.map +0 -1
- package/src/lib/app-utils/campaign-forwarding.js.map +0 -1
- package/src/lib/app-utils/contact-holder.d.ts +0 -68
- package/src/lib/app-utils/contact-holder.js +0 -129
- package/src/lib/app-utils/contact-holder.js.map +0 -1
- package/src/lib/app-utils/contact-merge.d.ts +0 -136
- package/src/lib/app-utils/contact-merge.js +0 -412
- package/src/lib/app-utils/contact-merge.js.map +0 -1
- package/src/lib/app-utils/crm-csv.d.ts +0 -239
- package/src/lib/app-utils/crm-csv.js +0 -467
- package/src/lib/app-utils/crm-csv.js.map +0 -1
- package/src/lib/app-utils/crm-digest.d.ts +0 -185
- package/src/lib/app-utils/crm-digest.js +0 -272
- package/src/lib/app-utils/crm-digest.js.map +0 -1
- package/src/lib/app-utils/form-dataset-binding.d.ts +0 -50
- package/src/lib/app-utils/form-dataset-binding.js +0 -74
- package/src/lib/app-utils/form-dataset-binding.js.map +0 -1
- package/src/lib/app-utils/list-import.d.ts +0 -158
- package/src/lib/app-utils/list-import.js +0 -332
- package/src/lib/app-utils/list-import.js.map +0 -1
- package/src/lib/app-utils/marketplace-listing-visibility.d.ts +0 -122
- package/src/lib/app-utils/marketplace-listing-visibility.js +0 -147
- package/src/lib/app-utils/marketplace-listing-visibility.js.map +0 -1
- package/src/lib/app-utils/marketplace-overrides.js.map +0 -1
- package/src/lib/app-utils/marketplace-provenance.js.map +0 -1
- package/src/lib/app-utils/marketplace-theme.js.map +0 -1
- package/src/lib/app-utils/marketplace-update-state.d.ts +0 -119
- package/src/lib/app-utils/marketplace-update-state.js +0 -132
- package/src/lib/app-utils/marketplace-update-state.js.map +0 -1
- package/src/lib/app-utils/publisher-agreement.d.ts +0 -289
- package/src/lib/app-utils/publisher-agreement.js +0 -321
- package/src/lib/app-utils/publisher-agreement.js.map +0 -1
- package/src/lib/app-utils/wistia-embed.d.ts +0 -104
- package/src/lib/app-utils/wistia-embed.js +0 -138
- package/src/lib/app-utils/wistia-embed.js.map +0 -1
- package/src/lib/app-utils/workflows.d.ts +0 -136
- package/src/lib/app-utils/workflows.js +0 -346
- package/src/lib/app-utils/workflows.js.map +0 -1
- package/src/lib/plugin-manager/commerce-zone-props.d.ts +0 -172
- package/src/lib/plugin-manager/commerce-zone-props.js +0 -31
- package/src/lib/plugin-manager/commerce-zone-props.js.map +0 -1
|
@@ -327,5 +327,66 @@ export function isScopeToken(value) {
|
|
|
327
327
|
...tail
|
|
328
328
|
].join('/');
|
|
329
329
|
}
|
|
330
|
+
/*==========================================
|
|
331
|
+
* SEEN, AND HELD (AGL-3336).
|
|
332
|
+
*
|
|
333
|
+
* `visibleTo` answers one question, "who may see this", because it is the
|
|
334
|
+
* one field both enforcement layers can evaluate. A visibility GRANT — an
|
|
335
|
+
* org manager sharing a record with another site, by hand or by a rule —
|
|
336
|
+
* has to widen exactly that field, or the other site's lists, which are
|
|
337
|
+
* real `array-contains-any` queries, could never return the record.
|
|
338
|
+
*
|
|
339
|
+
* But a site a record was shared with does not HOLD it. It did not capture
|
|
340
|
+
* the person, it holds no consent from them, and letting go of the record
|
|
341
|
+
* is not its decision. So a document that carries a grant records which of
|
|
342
|
+
* its tokens the grant added, under `sharing.added`, and every question
|
|
343
|
+
* about holding — who may delete it, whose audience it joins, which site's
|
|
344
|
+
* list it enters — reads {@link heldScopeTokens} instead of `visibleTo`.
|
|
345
|
+
*
|
|
346
|
+
* A document without the field is held by everything that sees it, which
|
|
347
|
+
* is every document written before grants existed.
|
|
348
|
+
*=========================================*/ /** The map a scoped document records its visibility grants under. */ export const SCOPE_GRANTS_FIELD = 'sharing';
|
|
349
|
+
/** The tokens a grant ADDED to `visibleTo`: seen through, never held. */ export function grantedScopeTokens(doc) {
|
|
350
|
+
const grants = (doc != null ? doc : {})[SCOPE_GRANTS_FIELD];
|
|
351
|
+
if (!grants || typeof grants !== 'object' || Array.isArray(grants)) return [];
|
|
352
|
+
const added = grants['added'];
|
|
353
|
+
return Array.isArray(added) ? added.filter((token)=>typeof token === 'string') : [];
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* The tokens that HOLD a document: `visibleTo` without the ones a grant
|
|
357
|
+
* added. Missing `visibleTo` is held by nobody, as it is seen by nobody.
|
|
358
|
+
*
|
|
359
|
+
* A site that created the document (`hostId`) or captured the person on it
|
|
360
|
+
* (`capturedByHostIds`) holds its token whatever the grants say: a record
|
|
361
|
+
* shared with a site that then captures the person itself is that site's
|
|
362
|
+
* own from the capture on, before anything re-evaluates the grants.
|
|
363
|
+
*/ export function heldScopeTokens(doc) {
|
|
364
|
+
const visibleTo = (doc != null ? doc : {})['visibleTo'];
|
|
365
|
+
if (!Array.isArray(visibleTo)) return [];
|
|
366
|
+
const granted = new Set(grantedScopeTokens(doc));
|
|
367
|
+
const capturing = new Set();
|
|
368
|
+
const hostId = (doc != null ? doc : {})['hostId'];
|
|
369
|
+
if (typeof hostId === 'string' && hostId) capturing.add(hostScopeToken(hostId));
|
|
370
|
+
const captured = (doc != null ? doc : {})['capturedByHostIds'];
|
|
371
|
+
if (Array.isArray(captured)) {
|
|
372
|
+
for (const id of captured)if (typeof id === 'string' && id) capturing.add(hostScopeToken(id));
|
|
373
|
+
}
|
|
374
|
+
return visibleTo.filter((token)=>typeof token === 'string' && (!granted.has(token) || capturing.has(token)));
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Whether a site HOLDS a document rather than only seeing it through a
|
|
378
|
+
* grant. The question a marketing audience, a list rule, a sequence and a
|
|
379
|
+
* holder count ask; {@link visibleToHost} is the one a reader asks.
|
|
380
|
+
*/ export function heldByHost(doc, hostId) {
|
|
381
|
+
return visibleToHost(heldScopeTokens(doc), hostId);
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Whether a site sees a document ONLY through a grant: it carries grants and
|
|
385
|
+
* the site does not hold it. The filter an audience sweep applies to what
|
|
386
|
+
* its scoped query returned — a document with no grants answers `false`, so
|
|
387
|
+
* every read that predates sharing keeps exactly the rows it had.
|
|
388
|
+
*/ export function seenOnlyThroughGrant(doc, hostId) {
|
|
389
|
+
return grantedScopeTokens(doc).length > 0 && !heldByHost(doc, hostId);
|
|
390
|
+
}
|
|
330
391
|
|
|
331
392
|
//# sourceMappingURL=scope-tokens.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/scope-tokens.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 * Scoped sharing (AGL-1037): org-owned resources — datasets, media assets,\n * media folders — carry a `visibleTo` array naming who may see them, so an\n * agency can keep an internal dataset off its client sites without forking\n * storage into a second host-scoped collection (see AGL-237, which moved\n * these the other way).\n *\n * The shape is dictated by what both enforcement layers can evaluate:\n * Firestore rules have no `.map()`, so the caller's read set is denormalized\n * onto their member doc (AGL-1038) and matched with `list.hasAny(list)`;\n * clients match the same field with `array-contains-any`. A flat array of\n * strings is the only shape both primitives accept — the friendlier\n * `{ kind, hostIds }` form is a presentation concern and lives in\n * `describeScope` below, never on the doc.\n */\n\nimport type { ScopeToken } from '../foundation'\n\n// The type itself lives in foundation (app-utils imports foundation, never\n// the reverse) so the media doc interfaces can reference it; re-exported\n// here so callers get the type and its helpers from one place.\nexport type { ScopeToken }\n\n/** Everyone in the owning org, and every host it owns. */\nexport const ORG_SCOPE_TOKEN = 'org' as const satisfies ScopeToken\n\nconst HOST_TOKEN_PREFIX = 'host:'\n\n/**\n * `array-contains-any` and `hasAny` both cap at 30 values. That caps a\n * selected-sites scope at 30 hosts, and a collaborator's read query at 29\n * hosts plus `'org'`. Wider than that is the `['org']` case.\n */\nexport const MAX_SCOPE_HOSTS = 30\n\n/** `host:{hostId}` — the token naming a single site. */\nexport function hostScopeToken(hostId: string): ScopeToken {\n return `${HOST_TOKEN_PREFIX}${hostId}`\n}\n\nexport function isScopeToken(value: unknown): value is ScopeToken {\n if (typeof value !== 'string') return false\n if (value === ORG_SCOPE_TOKEN) return true\n return (\n value.startsWith(HOST_TOKEN_PREFIX) &&\n value.length > HOST_TOKEN_PREFIX.length\n )\n}\n\n/** Reads a token back apart; null when it isn't one. */\nexport function parseScopeToken(\n token: string\n): { kind: 'org' } | { kind: 'host'; hostId: string } | null {\n if (token === ORG_SCOPE_TOKEN) return { kind: 'org' }\n if (!isScopeToken(token)) return null\n return { kind: 'host', hostId: token.slice(HOST_TOKEN_PREFIX.length) }\n}\n\n/** Every host id named by a scope, in order; `['org']` names none. */\nexport function hostIdsFromScope(\n visibleTo: readonly string[] | undefined\n): string[] {\n const ids: string[] = []\n for (const token of visibleTo ?? []) {\n const parsed = parseScopeToken(token)\n if (parsed?.kind === 'host') ids.push(parsed.hostId)\n }\n return ids\n}\n\n/**\n * The read set for one site: what a server read path or a scoped client\n * query passes to `array-contains-any`. `'org'` is included because an\n * org-wide resource is visible to every host in the org.\n */\nexport function scopeTokensForHost(hostId: string): ScopeToken[] {\n return [ORG_SCOPE_TOKEN, hostScopeToken(hostId)]\n}\n\n/**\n * Whether a host may see a resource.\n *\n * ## A missing `visibleTo` is visible to NOBODY\n *\n * It once read as org-wide, so that documents predating the scope backfill\n * stayed visible until it reached them. That default is the shape of failure\n * this codebase keeps paying for: **a read that cannot see a value answered\n * the same as a value that is absent.** \"This document was never scoped\" and\n * \"this document is shared with every site\" are different facts, and one\n * unscoped document under an agency account was one client's data on every\n * other client's site — silently, because the permissive answer is the one\n * nothing complains about.\n *\n * Both enforcement layers below this function already fail CLOSED on the\n * missing field: `array-contains-any` matches nothing on a document that\n * lacks the array, and the rules' `hasAny` is false for the same reason. So\n * the permissive default was never a policy either of them agreed with — it\n * was one TypeScript helper disagreeing with the database, which is what made\n * AGL-1466 invisible for three weeks.\n *\n * The migration is `tools/scripts/backfill-consent-host.mjs`, which stamps\n * `['org']` on the documents that should carry it. Until a document is\n * stamped it is hidden, and hiding is the recoverable direction.\n *\n * An **empty array** answers the same and always did.\n */\nexport function visibleToHost(\n visibleTo: readonly string[] | undefined | null,\n hostId: string\n): boolean {\n if (visibleTo == null) return false\n if (visibleTo.includes(ORG_SCOPE_TOKEN)) return true\n return visibleTo.includes(hostScopeToken(hostId))\n}\n\n/**\n * Whether a caller holding `scopeTokens` (AGL-1038) may see a resource.\n * The TypeScript twin of the rules' `visibleTo.hasAny(scopeTokens)`, for\n * the Admin-SDK paths that never evaluate rules at all.\n *\n * A missing `visibleTo` is visible to nobody, for the reason\n * {@link visibleToHost} gives — and here the twinning is the argument on its\n * own: `hasAny` over a missing field is false in the rules, so a helper that\n * answered `true` was not a twin of anything.\n */\nexport function visibleToTokens(\n visibleTo: readonly string[] | undefined | null,\n scopeTokens: readonly string[] | undefined | null\n): boolean {\n if (visibleTo == null) return false\n if (!scopeTokens?.length) return false\n return visibleTo.some((token) => scopeTokens.includes(token))\n}\n\n/**\n * The scope a document ACTUALLY carries — `null` when it carries none\n * (AGL-1466/AGL-1480).\n *\n * Every helper above this line answers \"may this be seen\", and answers it\n * the way both enforcement layers do: a missing `visibleTo` is seen by\n * nobody.\n *\n * An **editor** asks a different question — \"what did somebody choose\" —\n * and it still needs its own answer, because \"nobody chose\" and \"somebody\n * chose nobody\" are the same to an editor and different on the document.\n * Four call sites across two files each wrote their own\n *\n * ```ts\n * Array.isArray(doc.visibleTo) ? doc.visibleTo : [ORG_SCOPE_TOKEN]\n * ```\n *\n * and every one of them rendered \"All sites\" over a document no site could\n * see. That inversion is why AGL-1466 went three weeks unnoticed with the\n * folder tree missing from every host, and AGL-1480 is the third and fourth\n * copies of it turning up at the surface people use most.\n *\n * So: one function, the same lesson `newMediaFolderDoc` drew on the write\n * side. A caller that wants a display default may still `?? []` or\n * `?? [ORG_SCOPE_TOKEN]` — but it has to say so, in one visible place,\n * instead of a ternary that reads like a null check.\n *\n * An empty array answers `null` too. On the document the two differ, and\n * `newResourceScopeFields` refuses to WRITE one; to an editor they are the\n * same fact — nobody chose this — and both must offer the choice rather\n * than pre-fill an answer.\n *\n * Not normalized, deliberately. `normalizeVisibleTo` collapses for STORAGE;\n * doing it here would make an untouched editor differ from the document it\n * was seeded from, and the save gates compare exactly those two to decide\n * whether to write at all.\n */\nexport function storedScope(\n visibleTo: readonly string[] | undefined | null\n): string[] | null {\n return Array.isArray(visibleTo) && visibleTo.length ? [...visibleTo] : null\n}\n\n/**\n * Cleans a scope for storage: drops junk, dedupes, and collapses to\n * `['org']` when org-wide is present, since that already implies every\n * host. Returns null when the result is unusable — empty, or over\n * `MAX_SCOPE_HOSTS` — so the caller decides rather than this silently\n * dropping hosts (losing access) or widening to org (leaking).\n */\nexport function normalizeVisibleTo(\n input: readonly string[] | undefined | null\n): ScopeToken[] | null {\n const tokens = (input ?? []).filter(isScopeToken)\n if (!tokens.length) return null\n if (tokens.includes(ORG_SCOPE_TOKEN)) return [ORG_SCOPE_TOKEN]\n const unique = [...new Set(tokens)]\n if (unique.length > MAX_SCOPE_HOSTS) return null\n return unique\n}\n\n/**\n * The scope a save writes, or why the selection cannot be written.\n *\n * {@link normalizeVisibleTo} answers null for a selection it cannot store, and\n * a save has no safe substitute for that null: the org token shares the\n * resource with every site, and dropping tokens takes access from sites\n * somebody picked. So a caller refuses the save and shows `problem`.\n */\nexport function scopeToStore(\n input: readonly string[] | undefined | null\n): { scope: ScopeToken[]; problem: null } | { scope: null; problem: string } {\n const scope = normalizeVisibleTo(input)\n if (scope) return { scope, problem: null }\n return {\n scope: null,\n problem: (input ?? []).some(isScopeToken)\n ? `Choose ${MAX_SCOPE_HOSTS} sites or fewer, or share with All sites.`\n : 'Choose at least one site, or share with All sites.',\n }\n}\n\n/** A scope covering exactly the given hosts; null when unusable. */\nexport function scopeForHosts(\n hostIds: readonly string[]\n): ScopeToken[] | null {\n return normalizeVisibleTo(hostIds.map(hostScopeToken))\n}\n\n/**\n * Whether a scope is the org-wide one.\n *\n * An absent scope is NOT org-wide — see {@link visibleToHost}. That is the\n * whole correction: this predicate is what `describeScope` renders and what\n * `scopeCovers` and `narrowsScope` reason from, and while it answered `true`\n * for a document nothing could see, the console said \"All sites\" over it.\n */\nexport function isOrgWideScope(\n visibleTo: readonly string[] | undefined | null\n): boolean {\n return visibleTo != null && visibleTo.includes(ORG_SCOPE_TOKEN)\n}\n\n/**\n * Whether moving from `before` to `after` takes visibility away from any\n * host — the direction that can break a published page, and the one the\n * console confirms before saving (AGL-1044/AGL-1045).\n */\nexport function narrowsScope(\n before: readonly string[] | undefined | null,\n after: readonly string[] | undefined | null\n): boolean {\n if (isOrgWideScope(before)) return !isOrgWideScope(after)\n if (isOrgWideScope(after)) return false\n const kept = new Set(after ?? [])\n return (before ?? []).some((token) => !kept.has(token))\n}\n\n/**\n * The sentence the console shows. `hostNames` maps host id → display name;\n * ids missing from it fall back to the count wording, since a name we\n * can't resolve is worse than no name.\n */\nexport function describeScope(\n visibleTo: readonly string[] | undefined | null,\n hostNames: Readonly<Record<string, string>> = {}\n): string {\n if (isOrgWideScope(visibleTo)) return 'All sites'\n const hostIds = hostIdsFromScope(visibleTo ?? [])\n if (!hostIds.length) return 'No sites'\n if (hostIds.length === 1) {\n const name = hostNames[hostIds[0]]\n return name ? `${name} only` : '1 site'\n }\n return `${hostIds.length} sites`\n}\n\n/**\n * The scope a newly created resource starts with (AGL-1048).\n *\n * Honours the org's `defaultResourceScope`, but only when a site is\n * actually in context: created from the org Media or Data page there is no\n * host to scope to, and inventing one would hide the resource from the\n * page that just created it.\n */\nexport function defaultScopeForNewResource(options: {\n defaultResourceScope?: 'org' | 'host'\n hostId?: string | null\n}): ScopeToken[] {\n return options.defaultResourceScope === 'host' && options.hostId\n ? [hostScopeToken(options.hostId)]\n : [ORG_SCOPE_TOKEN]\n}\n\n/**\n * The scope fields a document being CREATED in a scoped collection carries\n * (AGL-1478), as a spreadable object.\n *\n * The argument is REQUIRED and has no default, which is the whole point.\n * AGL-1466 was two call sites that each built a `mediaFolders` document\n * inline and each forgot `visibleTo`; `newMediaFolderDoc` fixed that one\n * collection by making the input mandatory, and this is the same guarantee\n * for the collections that have no document constructor of their own —\n * `datasets`, `media`, `contacts`, `contactSegments`. A creator that has\n * not decided cannot compile.\n *\n * Three inputs, three meanings:\n *\n * - **tokens** — stamp them.\n * - **`null`** — this collection is NOT scoped (a `hosts/{hostId}`\n * subcollection is private by construction), so store nothing. Saying it\n * out loud is what distinguishes \"not applicable\" from \"forgotten\".\n * - **`[]`** — throws. Unlike a missing field, a stored empty array is a\n * written \"visible to nobody\": the backfill leaves it alone by design and\n * no read path treats it as legacy, so it is permanent where absent is\n * repairable. Getting here means a caller computed a scope and came back\n * with nothing, which is worth a stack trace.\n */\nexport function newResourceScopeFields(\n visibleTo: readonly ScopeToken[] | null\n): { visibleTo?: ScopeToken[] } {\n if (visibleTo && !visibleTo.length) {\n throw new Error('A scoped resource cannot be created with an empty scope')\n }\n return visibleTo ? { visibleTo: [...visibleTo] } : {}\n}\n\n/**\n * Whether `target` is visible everywhere `source` is — the condition a\n * cross-dataset REFERENCE has to satisfy (AGL-1044).\n *\n * A reference field on dataset A pointing at dataset B is only resolvable\n * on the sites that can see BOTH. If A is shared with a site that cannot\n * see B, a page there resolves the reference to nothing: the row renders\n * with a blank where the linked value should be, and nothing says why. So\n * B's scope must COVER A's.\n *\n * Org-wide covers everything. A missing scope is seen by nobody and\n * therefore covers nothing, matching the rest of this module.\n */\nexport function scopeCovers(\n target: readonly string[] | undefined | null,\n source: readonly string[] | undefined | null,\n): boolean {\n if (isOrgWideScope(target)) return true\n // The target is restricted; an org-wide source reaches sites it cannot.\n if (isOrgWideScope(source)) return false\n const reach = new Set(target ?? [])\n return (source ?? []).every((token) => reach.has(token))\n}\n\n/**\n * Rewrites an org media CDN path so it names the site using the asset\n * (AGL-1043/1045).\n *\n * `/api/media/cdn/org:{orgId}/{mediaId}` serves ORG-WIDE assets only. A\n * restricted asset has to be requested through the form that names the\n * site — `org:{orgId}:{hostId}` — because the CDN is unauthenticated and\n * the URL is the only thing it can decide from.\n *\n * Returns the path untouched when there is nothing to do: an org-wide\n * asset, a host-library asset, no host context, or a path that is not an\n * org CDN URL. That keeps it safe to apply blindly at every pick site.\n */\nexport function hostQualifiedCdnPath(\n cdnPath: string | undefined | null,\n visibleTo: readonly string[] | undefined | null,\n hostId: string | undefined | null,\n): string | undefined {\n if (!cdnPath) return cdnPath ?? undefined\n if (!hostId || isOrgWideScope(visibleTo)) return cdnPath\n // `/api/media/cdn/{scope}/{mediaId}[/{hash}]`\n const marker = '/api/media/cdn/'\n const at = cdnPath.indexOf(marker)\n if (at === -1) return cdnPath\n const head = cdnPath.slice(0, at + marker.length)\n const rest = cdnPath.slice(at + marker.length)\n const [scopeSegment, ...tail] = rest.split('/')\n // Only the bare `org:{orgId}` form is rewritten; an already-qualified\n // scope is left alone rather than gaining a second host.\n if (!scopeSegment.startsWith('org:') || scopeSegment.includes(':', 4)) {\n return cdnPath\n }\n return [`${head}${scopeSegment}:${hostId}`, ...tail].join('/')\n}\n"],"names":["ORG_SCOPE_TOKEN","HOST_TOKEN_PREFIX","MAX_SCOPE_HOSTS","hostScopeToken","hostId","isScopeToken","value","startsWith","length","parseScopeToken","token","kind","slice","hostIdsFromScope","visibleTo","ids","parsed","push","scopeTokensForHost","visibleToHost","includes","visibleToTokens","scopeTokens","some","storedScope","Array","isArray","normalizeVisibleTo","input","tokens","filter","unique","Set","scopeToStore","scope","problem","scopeForHosts","hostIds","map","isOrgWideScope","narrowsScope","before","after","kept","has","describeScope","hostNames","name","defaultScopeForNewResource","options","defaultResourceScope","newResourceScopeFields","Error","scopeCovers","target","source","reach","every","hostQualifiedCdnPath","cdnPath","undefined","marker","at","indexOf","head","rest","scopeSegment","tail","split","join"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/scope-tokens.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 * Scoped sharing (AGL-1037): org-owned resources — datasets, media assets,\n * media folders — carry a `visibleTo` array naming who may see them, so an\n * agency can keep an internal dataset off its client sites without forking\n * storage into a second host-scoped collection (see AGL-237, which moved\n * these the other way).\n *\n * The shape is dictated by what both enforcement layers can evaluate:\n * Firestore rules have no `.map()`, so the caller's read set is denormalized\n * onto their member doc (AGL-1038) and matched with `list.hasAny(list)`;\n * clients match the same field with `array-contains-any`. A flat array of\n * strings is the only shape both primitives accept — the friendlier\n * `{ kind, hostIds }` form is a presentation concern and lives in\n * `describeScope` below, never on the doc.\n */\n\nimport type { ScopeToken } from '../foundation'\n\n// The type itself lives in foundation (app-utils imports foundation, never\n// the reverse) so the media doc interfaces can reference it; re-exported\n// here so callers get the type and its helpers from one place.\nexport type { ScopeToken }\n\n/** Everyone in the owning org, and every host it owns. */\nexport const ORG_SCOPE_TOKEN = 'org' as const satisfies ScopeToken\n\nconst HOST_TOKEN_PREFIX = 'host:'\n\n/**\n * `array-contains-any` and `hasAny` both cap at 30 values. That caps a\n * selected-sites scope at 30 hosts, and a collaborator's read query at 29\n * hosts plus `'org'`. Wider than that is the `['org']` case.\n */\nexport const MAX_SCOPE_HOSTS = 30\n\n/** `host:{hostId}` — the token naming a single site. */\nexport function hostScopeToken(hostId: string): ScopeToken {\n return `${HOST_TOKEN_PREFIX}${hostId}`\n}\n\nexport function isScopeToken(value: unknown): value is ScopeToken {\n if (typeof value !== 'string') return false\n if (value === ORG_SCOPE_TOKEN) return true\n return (\n value.startsWith(HOST_TOKEN_PREFIX) &&\n value.length > HOST_TOKEN_PREFIX.length\n )\n}\n\n/** Reads a token back apart; null when it isn't one. */\nexport function parseScopeToken(\n token: string\n): { kind: 'org' } | { kind: 'host'; hostId: string } | null {\n if (token === ORG_SCOPE_TOKEN) return { kind: 'org' }\n if (!isScopeToken(token)) return null\n return { kind: 'host', hostId: token.slice(HOST_TOKEN_PREFIX.length) }\n}\n\n/** Every host id named by a scope, in order; `['org']` names none. */\nexport function hostIdsFromScope(\n visibleTo: readonly string[] | undefined\n): string[] {\n const ids: string[] = []\n for (const token of visibleTo ?? []) {\n const parsed = parseScopeToken(token)\n if (parsed?.kind === 'host') ids.push(parsed.hostId)\n }\n return ids\n}\n\n/**\n * The read set for one site: what a server read path or a scoped client\n * query passes to `array-contains-any`. `'org'` is included because an\n * org-wide resource is visible to every host in the org.\n */\nexport function scopeTokensForHost(hostId: string): ScopeToken[] {\n return [ORG_SCOPE_TOKEN, hostScopeToken(hostId)]\n}\n\n/**\n * Whether a host may see a resource.\n *\n * ## A missing `visibleTo` is visible to NOBODY\n *\n * It once read as org-wide, so that documents predating the scope backfill\n * stayed visible until it reached them. That default is the shape of failure\n * this codebase keeps paying for: **a read that cannot see a value answered\n * the same as a value that is absent.** \"This document was never scoped\" and\n * \"this document is shared with every site\" are different facts, and one\n * unscoped document under an agency account was one client's data on every\n * other client's site — silently, because the permissive answer is the one\n * nothing complains about.\n *\n * Both enforcement layers below this function already fail CLOSED on the\n * missing field: `array-contains-any` matches nothing on a document that\n * lacks the array, and the rules' `hasAny` is false for the same reason. So\n * the permissive default was never a policy either of them agreed with — it\n * was one TypeScript helper disagreeing with the database, which is what made\n * AGL-1466 invisible for three weeks.\n *\n * The migration is `tools/scripts/backfill-consent-host.mjs`, which stamps\n * `['org']` on the documents that should carry it. Until a document is\n * stamped it is hidden, and hiding is the recoverable direction.\n *\n * An **empty array** answers the same and always did.\n */\nexport function visibleToHost(\n visibleTo: readonly string[] | undefined | null,\n hostId: string\n): boolean {\n if (visibleTo == null) return false\n if (visibleTo.includes(ORG_SCOPE_TOKEN)) return true\n return visibleTo.includes(hostScopeToken(hostId))\n}\n\n/**\n * Whether a caller holding `scopeTokens` (AGL-1038) may see a resource.\n * The TypeScript twin of the rules' `visibleTo.hasAny(scopeTokens)`, for\n * the Admin-SDK paths that never evaluate rules at all.\n *\n * A missing `visibleTo` is visible to nobody, for the reason\n * {@link visibleToHost} gives — and here the twinning is the argument on its\n * own: `hasAny` over a missing field is false in the rules, so a helper that\n * answered `true` was not a twin of anything.\n */\nexport function visibleToTokens(\n visibleTo: readonly string[] | undefined | null,\n scopeTokens: readonly string[] | undefined | null\n): boolean {\n if (visibleTo == null) return false\n if (!scopeTokens?.length) return false\n return visibleTo.some((token) => scopeTokens.includes(token))\n}\n\n/**\n * The scope a document ACTUALLY carries — `null` when it carries none\n * (AGL-1466/AGL-1480).\n *\n * Every helper above this line answers \"may this be seen\", and answers it\n * the way both enforcement layers do: a missing `visibleTo` is seen by\n * nobody.\n *\n * An **editor** asks a different question — \"what did somebody choose\" —\n * and it still needs its own answer, because \"nobody chose\" and \"somebody\n * chose nobody\" are the same to an editor and different on the document.\n * Four call sites across two files each wrote their own\n *\n * ```ts\n * Array.isArray(doc.visibleTo) ? doc.visibleTo : [ORG_SCOPE_TOKEN]\n * ```\n *\n * and every one of them rendered \"All sites\" over a document no site could\n * see. That inversion is why AGL-1466 went three weeks unnoticed with the\n * folder tree missing from every host, and AGL-1480 is the third and fourth\n * copies of it turning up at the surface people use most.\n *\n * So: one function, the same lesson `newMediaFolderDoc` drew on the write\n * side. A caller that wants a display default may still `?? []` or\n * `?? [ORG_SCOPE_TOKEN]` — but it has to say so, in one visible place,\n * instead of a ternary that reads like a null check.\n *\n * An empty array answers `null` too. On the document the two differ, and\n * `newResourceScopeFields` refuses to WRITE one; to an editor they are the\n * same fact — nobody chose this — and both must offer the choice rather\n * than pre-fill an answer.\n *\n * Not normalized, deliberately. `normalizeVisibleTo` collapses for STORAGE;\n * doing it here would make an untouched editor differ from the document it\n * was seeded from, and the save gates compare exactly those two to decide\n * whether to write at all.\n */\nexport function storedScope(\n visibleTo: readonly string[] | undefined | null\n): string[] | null {\n return Array.isArray(visibleTo) && visibleTo.length ? [...visibleTo] : null\n}\n\n/**\n * Cleans a scope for storage: drops junk, dedupes, and collapses to\n * `['org']` when org-wide is present, since that already implies every\n * host. Returns null when the result is unusable — empty, or over\n * `MAX_SCOPE_HOSTS` — so the caller decides rather than this silently\n * dropping hosts (losing access) or widening to org (leaking).\n */\nexport function normalizeVisibleTo(\n input: readonly string[] | undefined | null\n): ScopeToken[] | null {\n const tokens = (input ?? []).filter(isScopeToken)\n if (!tokens.length) return null\n if (tokens.includes(ORG_SCOPE_TOKEN)) return [ORG_SCOPE_TOKEN]\n const unique = [...new Set(tokens)]\n if (unique.length > MAX_SCOPE_HOSTS) return null\n return unique\n}\n\n/**\n * The scope a save writes, or why the selection cannot be written.\n *\n * {@link normalizeVisibleTo} answers null for a selection it cannot store, and\n * a save has no safe substitute for that null: the org token shares the\n * resource with every site, and dropping tokens takes access from sites\n * somebody picked. So a caller refuses the save and shows `problem`.\n */\nexport function scopeToStore(\n input: readonly string[] | undefined | null\n): { scope: ScopeToken[]; problem: null } | { scope: null; problem: string } {\n const scope = normalizeVisibleTo(input)\n if (scope) return { scope, problem: null }\n return {\n scope: null,\n problem: (input ?? []).some(isScopeToken)\n ? `Choose ${MAX_SCOPE_HOSTS} sites or fewer, or share with All sites.`\n : 'Choose at least one site, or share with All sites.',\n }\n}\n\n/** A scope covering exactly the given hosts; null when unusable. */\nexport function scopeForHosts(\n hostIds: readonly string[]\n): ScopeToken[] | null {\n return normalizeVisibleTo(hostIds.map(hostScopeToken))\n}\n\n/**\n * Whether a scope is the org-wide one.\n *\n * An absent scope is NOT org-wide — see {@link visibleToHost}. That is the\n * whole correction: this predicate is what `describeScope` renders and what\n * `scopeCovers` and `narrowsScope` reason from, and while it answered `true`\n * for a document nothing could see, the console said \"All sites\" over it.\n */\nexport function isOrgWideScope(\n visibleTo: readonly string[] | undefined | null\n): boolean {\n return visibleTo != null && visibleTo.includes(ORG_SCOPE_TOKEN)\n}\n\n/**\n * Whether moving from `before` to `after` takes visibility away from any\n * host — the direction that can break a published page, and the one the\n * console confirms before saving (AGL-1044/AGL-1045).\n */\nexport function narrowsScope(\n before: readonly string[] | undefined | null,\n after: readonly string[] | undefined | null\n): boolean {\n if (isOrgWideScope(before)) return !isOrgWideScope(after)\n if (isOrgWideScope(after)) return false\n const kept = new Set(after ?? [])\n return (before ?? []).some((token) => !kept.has(token))\n}\n\n/**\n * The sentence the console shows. `hostNames` maps host id → display name;\n * ids missing from it fall back to the count wording, since a name we\n * can't resolve is worse than no name.\n */\nexport function describeScope(\n visibleTo: readonly string[] | undefined | null,\n hostNames: Readonly<Record<string, string>> = {}\n): string {\n if (isOrgWideScope(visibleTo)) return 'All sites'\n const hostIds = hostIdsFromScope(visibleTo ?? [])\n if (!hostIds.length) return 'No sites'\n if (hostIds.length === 1) {\n const name = hostNames[hostIds[0]]\n return name ? `${name} only` : '1 site'\n }\n return `${hostIds.length} sites`\n}\n\n/**\n * The scope a newly created resource starts with (AGL-1048).\n *\n * Honours the org's `defaultResourceScope`, but only when a site is\n * actually in context: created from the org Media or Data page there is no\n * host to scope to, and inventing one would hide the resource from the\n * page that just created it.\n */\nexport function defaultScopeForNewResource(options: {\n defaultResourceScope?: 'org' | 'host'\n hostId?: string | null\n}): ScopeToken[] {\n return options.defaultResourceScope === 'host' && options.hostId\n ? [hostScopeToken(options.hostId)]\n : [ORG_SCOPE_TOKEN]\n}\n\n/**\n * The scope fields a document being CREATED in a scoped collection carries\n * (AGL-1478), as a spreadable object.\n *\n * The argument is REQUIRED and has no default, which is the whole point.\n * AGL-1466 was two call sites that each built a `mediaFolders` document\n * inline and each forgot `visibleTo`; `newMediaFolderDoc` fixed that one\n * collection by making the input mandatory, and this is the same guarantee\n * for the collections that have no document constructor of their own —\n * `datasets`, `media`, `contacts`, `contactSegments`. A creator that has\n * not decided cannot compile.\n *\n * Three inputs, three meanings:\n *\n * - **tokens** — stamp them.\n * - **`null`** — this collection is NOT scoped (a `hosts/{hostId}`\n * subcollection is private by construction), so store nothing. Saying it\n * out loud is what distinguishes \"not applicable\" from \"forgotten\".\n * - **`[]`** — throws. Unlike a missing field, a stored empty array is a\n * written \"visible to nobody\": the backfill leaves it alone by design and\n * no read path treats it as legacy, so it is permanent where absent is\n * repairable. Getting here means a caller computed a scope and came back\n * with nothing, which is worth a stack trace.\n */\nexport function newResourceScopeFields(\n visibleTo: readonly ScopeToken[] | null\n): { visibleTo?: ScopeToken[] } {\n if (visibleTo && !visibleTo.length) {\n throw new Error('A scoped resource cannot be created with an empty scope')\n }\n return visibleTo ? { visibleTo: [...visibleTo] } : {}\n}\n\n/**\n * Whether `target` is visible everywhere `source` is — the condition a\n * cross-dataset REFERENCE has to satisfy (AGL-1044).\n *\n * A reference field on dataset A pointing at dataset B is only resolvable\n * on the sites that can see BOTH. If A is shared with a site that cannot\n * see B, a page there resolves the reference to nothing: the row renders\n * with a blank where the linked value should be, and nothing says why. So\n * B's scope must COVER A's.\n *\n * Org-wide covers everything. A missing scope is seen by nobody and\n * therefore covers nothing, matching the rest of this module.\n */\nexport function scopeCovers(\n target: readonly string[] | undefined | null,\n source: readonly string[] | undefined | null,\n): boolean {\n if (isOrgWideScope(target)) return true\n // The target is restricted; an org-wide source reaches sites it cannot.\n if (isOrgWideScope(source)) return false\n const reach = new Set(target ?? [])\n return (source ?? []).every((token) => reach.has(token))\n}\n\n/**\n * Rewrites an org media CDN path so it names the site using the asset\n * (AGL-1043/1045).\n *\n * `/api/media/cdn/org:{orgId}/{mediaId}` serves ORG-WIDE assets only. A\n * restricted asset has to be requested through the form that names the\n * site — `org:{orgId}:{hostId}` — because the CDN is unauthenticated and\n * the URL is the only thing it can decide from.\n *\n * Returns the path untouched when there is nothing to do: an org-wide\n * asset, a host-library asset, no host context, or a path that is not an\n * org CDN URL. That keeps it safe to apply blindly at every pick site.\n */\nexport function hostQualifiedCdnPath(\n cdnPath: string | undefined | null,\n visibleTo: readonly string[] | undefined | null,\n hostId: string | undefined | null,\n): string | undefined {\n if (!cdnPath) return cdnPath ?? undefined\n if (!hostId || isOrgWideScope(visibleTo)) return cdnPath\n // `/api/media/cdn/{scope}/{mediaId}[/{hash}]`\n const marker = '/api/media/cdn/'\n const at = cdnPath.indexOf(marker)\n if (at === -1) return cdnPath\n const head = cdnPath.slice(0, at + marker.length)\n const rest = cdnPath.slice(at + marker.length)\n const [scopeSegment, ...tail] = rest.split('/')\n // Only the bare `org:{orgId}` form is rewritten; an already-qualified\n // scope is left alone rather than gaining a second host.\n if (!scopeSegment.startsWith('org:') || scopeSegment.includes(':', 4)) {\n return cdnPath\n }\n return [`${head}${scopeSegment}:${hostId}`, ...tail].join('/')\n}\n\n/*==========================================\n * SEEN, AND HELD (AGL-3336).\n *\n * `visibleTo` answers one question, \"who may see this\", because it is the\n * one field both enforcement layers can evaluate. A visibility GRANT — an\n * org manager sharing a record with another site, by hand or by a rule —\n * has to widen exactly that field, or the other site's lists, which are\n * real `array-contains-any` queries, could never return the record.\n *\n * But a site a record was shared with does not HOLD it. It did not capture\n * the person, it holds no consent from them, and letting go of the record\n * is not its decision. So a document that carries a grant records which of\n * its tokens the grant added, under `sharing.added`, and every question\n * about holding — who may delete it, whose audience it joins, which site's\n * list it enters — reads {@link heldScopeTokens} instead of `visibleTo`.\n *\n * A document without the field is held by everything that sees it, which\n * is every document written before grants existed.\n *=========================================*/\n\n/** The map a scoped document records its visibility grants under. */\nexport const SCOPE_GRANTS_FIELD = 'sharing'\n\n/** The tokens a grant ADDED to `visibleTo`: seen through, never held. */\nexport function grantedScopeTokens(\n doc: Readonly<Record<string, unknown>> | null | undefined,\n): string[] {\n const grants = (doc ?? {})[SCOPE_GRANTS_FIELD]\n if (!grants || typeof grants !== 'object' || Array.isArray(grants)) return []\n const added = (grants as Record<string, unknown>)['added']\n return Array.isArray(added)\n ? added.filter((token): token is string => typeof token === 'string')\n : []\n}\n\n/**\n * The tokens that HOLD a document: `visibleTo` without the ones a grant\n * added. Missing `visibleTo` is held by nobody, as it is seen by nobody.\n *\n * A site that created the document (`hostId`) or captured the person on it\n * (`capturedByHostIds`) holds its token whatever the grants say: a record\n * shared with a site that then captures the person itself is that site's\n * own from the capture on, before anything re-evaluates the grants.\n */\nexport function heldScopeTokens(\n doc: Readonly<Record<string, unknown>> | null | undefined,\n): string[] {\n const visibleTo = (doc ?? {})['visibleTo']\n if (!Array.isArray(visibleTo)) return []\n const granted = new Set(grantedScopeTokens(doc))\n const capturing = new Set<string>()\n const hostId = (doc ?? {})['hostId']\n if (typeof hostId === 'string' && hostId) capturing.add(hostScopeToken(hostId))\n const captured = (doc ?? {})['capturedByHostIds']\n if (Array.isArray(captured)) {\n for (const id of captured) if (typeof id === 'string' && id) capturing.add(hostScopeToken(id))\n }\n return visibleTo.filter(\n (token): token is string =>\n typeof token === 'string' && (!granted.has(token) || capturing.has(token)),\n )\n}\n\n/**\n * Whether a site HOLDS a document rather than only seeing it through a\n * grant. The question a marketing audience, a list rule, a sequence and a\n * holder count ask; {@link visibleToHost} is the one a reader asks.\n */\nexport function heldByHost(\n doc: Readonly<Record<string, unknown>> | null | undefined,\n hostId: string,\n): boolean {\n return visibleToHost(heldScopeTokens(doc), hostId)\n}\n\n/**\n * Whether a site sees a document ONLY through a grant: it carries grants and\n * the site does not hold it. The filter an audience sweep applies to what\n * its scoped query returned — a document with no grants answers `false`, so\n * every read that predates sharing keeps exactly the rows it had.\n */\nexport function seenOnlyThroughGrant(\n doc: Readonly<Record<string, unknown>> | null | undefined,\n hostId: string,\n): boolean {\n return grantedScopeTokens(doc).length > 0 && !heldByHost(doc, hostId)\n}\n"],"names":["ORG_SCOPE_TOKEN","HOST_TOKEN_PREFIX","MAX_SCOPE_HOSTS","hostScopeToken","hostId","isScopeToken","value","startsWith","length","parseScopeToken","token","kind","slice","hostIdsFromScope","visibleTo","ids","parsed","push","scopeTokensForHost","visibleToHost","includes","visibleToTokens","scopeTokens","some","storedScope","Array","isArray","normalizeVisibleTo","input","tokens","filter","unique","Set","scopeToStore","scope","problem","scopeForHosts","hostIds","map","isOrgWideScope","narrowsScope","before","after","kept","has","describeScope","hostNames","name","defaultScopeForNewResource","options","defaultResourceScope","newResourceScopeFields","Error","scopeCovers","target","source","reach","every","hostQualifiedCdnPath","cdnPath","undefined","marker","at","indexOf","head","rest","scopeSegment","tail","split","join","SCOPE_GRANTS_FIELD","grantedScopeTokens","doc","grants","added","heldScopeTokens","granted","capturing","add","captured","id","heldByHost","seenOnlyThroughGrant"],"mappings":"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"}
|
|
@@ -94,7 +94,7 @@ export * from "./screen-link-context-value.js";
|
|
|
94
94
|
return `⚠ Unavailable RSS feed (${feedCollectionId}) — collection deleted or has no slug`;
|
|
95
95
|
}
|
|
96
96
|
const entry = parseEntryLinkValue(screenId);
|
|
97
|
-
return entry ? `⚠ Unavailable entry (${entry.entryId}) — unpublished or deleted` : `⚠ Unavailable
|
|
97
|
+
return entry ? `⚠ Unavailable entry (${entry.entryId}) — unpublished or deleted` : `⚠ Unavailable page (${screenId}) — unpublished or deleted`;
|
|
98
98
|
}
|
|
99
99
|
export function unresolvedScreenOption(value, screens) {
|
|
100
100
|
const raw = typeof value === 'string' ? value.trim() : '';
|
|
@@ -111,7 +111,7 @@ export function unresolvedScreenOption(value, screens) {
|
|
|
111
111
|
}
|
|
112
112
|
return target.href ? {
|
|
113
113
|
value: raw,
|
|
114
|
-
label: `⚠ Plain address (${target.href}) — not a
|
|
114
|
+
label: `⚠ Plain address (${target.href}) — not a page`
|
|
115
115
|
} : undefined;
|
|
116
116
|
}
|
|
117
117
|
/**
|
|
@@ -211,7 +211,7 @@ export function unresolvedScreenOption(value, screens) {
|
|
|
211
211
|
* a smoke pass can then find every dead control on a page with one selector
|
|
212
212
|
* instead of reading five different components' conditions.
|
|
213
213
|
*/ export const BROKEN_SCREEN_LINK_ATTR = 'data-aglyn-broken-link';
|
|
214
|
-
/** What the AUTHOR is told, on the one surface that can fix it. */ export const BROKEN_SCREEN_LINK_MESSAGE = 'Broken link: this points at a
|
|
214
|
+
/** What the AUTHOR is told, on the one surface that can fix it. */ export const BROKEN_SCREEN_LINK_MESSAGE = 'Broken link: this points at a page or collection listing that is ' + 'unpublished or deleted, so it will not work on the published site. Pick ' + 'a target again in the attributes panel, or clear the link.';
|
|
215
215
|
/**
|
|
216
216
|
* The editor-only outline. A warning ring rather than an error one: the
|
|
217
217
|
* screen may be deliberately unpublished and about to come back, and the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/screen-link-context.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n// Lives in @aglyn/aglyn (not a UI lib) deliberately, WITHOUT a 'use client'\n// banner: every surface that renders canvas nodes already imports this\n// package, and a client boundary here (or importing the shared-ui-jsx\n// barrel from the tenant page) makes the bundler duplicate parts of the\n// module graph — a second canvas/emitter instance renders the site blank.\nimport { useContext, useMemo } from 'react'\nimport {\n ScreenLinkContext,\n type ScreenRouteMap,\n} from './screen-link-context-value'\nimport {\n EXTERNAL_HREF_PATTERN,\n SAFE_HREF_PATTERN,\n formatCollectionLinkValue,\n linkTargetKind,\n parseCollectionLinkValue,\n parseEntryLinkValue,\n parseFeedLinkValue,\n resolveScreenHref,\n screenRoutesAnswerFor,\n splitLinkValue,\n} from './screen-link-value'\n\nexport * from './screen-link-context-value'\n\nexport interface ResolvedScreenLink {\n /** Site-relative href (`/`, `/company/about`), undefined when unresolvable. */\n href?: string\n suppressNavigation: boolean\n /** True only on the static besigner canvas — interactions are inert. */\n editorInert: boolean\n /**\n * A screen id WAS authored and the routing map does not have it — the\n * target is unpublished or deleted (AGL-1893).\n *\n * Distinct from \"no href\": the canvas and the preview withhold hrefs on\n * purpose, and an element with no link at all never had one to lose.\n * Only this flag means the author asked for a link that cannot exist, and\n * only elements reading THIS may say so — see {@link isScreenLinkBroken}\n * for why an absent or empty map is deliberately not \"broken\".\n */\n broken: boolean\n}\n\n/**\n * Whether an authored screen id points at nothing (AGL-1893).\n *\n * Unpublishing or deleting a screen is a normal authoring action, and until\n * this existed nothing anywhere reported what it had just broken: a Tabs\n * link to a retired screen shipped as a live-looking control that silently\n * did nothing on `aglyn.com/changelog` and `/newsroom` for two days.\n *\n * The two \"we cannot tell\" cases are NOT broken, and the distinction is the\n * whole safety of this predicate:\n *\n * - **no map** — a surface that renders canvas nodes without providing\n * `ScreenLinkContext` (an isolated spec, an embed) knows nothing about\n * the host's screens, and must not conclude that every link is dead;\n * - **an empty map** — the same thing one beat earlier: the console's live\n * host subscription starts empty, and a moment of `{}` must not repaint a\n * whole navigation row as broken and then repaint it back.\n *\n * Both failure directions were weighed. Calling a live link broken hides or\n * disables working navigation on a customer's site; calling a dead link fine\n * leaves exactly the defect this issue is about. Only the first is\n * unrecoverable from the visitor's side, so the doubt resolves that way.\n */\nexport function isScreenLinkBroken(\n screens: ScreenRouteMap | undefined,\n screenId: string | null | undefined,\n): boolean {\n if (!screenId) return false\n // Both \"we cannot tell\" cases, asked of the half of the map that holds this\n // target's kind: a collection listing (AGL-2799) is judged against the\n // listings, a screen against the screens — see `screenRoutesAnswerFor`.\n if (!screenRoutesAnswerFor(screens, screenId)) return false\n return resolveScreenHref(screens, screenId) === undefined\n}\n\n/**\n * The extra option a Screen picker needs when its stored value matches none\n * of the host's screens (AGL-1893).\n *\n * The console builds `SCREEN_SELECT` options from the routing map, so a\n * value the map has lost — the retired `/blog` screen, say — matches\n * nothing and the field renders EMPTY. Which reads, to the person looking\n * at it, as \"no link set\". Meanwhile the element still behaves as linked:\n * the strip is still a nav landmark, that tab is still skipped as\n * \"navigates\", and the panel it would have revealed is still suppressed. So\n * the one surface where this is repairable was also the one actively\n * denying there was anything to repair.\n *\n * Returns the stored value UNCHANGED as the option's value: naming a dead\n * target must not rewrite it, or opening the panel and pressing Save would\n * quietly convert a recoverable reference into something else.\n *\n * Three shapes get named, because a Screen picker can legally hold any of\n * them (AGL-1335 / AGL-1894 / AGL-2799): a screen reference whose screen is\n * gone, a collection listing whose collection is gone, and a plain address\n * typed in before the picker existed. The last is not broken — but it is\n * just as invisible, and an author who cannot see it is the reason those\n * links are not rename-safe yet.\n */\nexport function unavailableScreenLabel(\n screenId: string,\n screensKnown: boolean,\n): string {\n // With no map loaded yet nothing is known to be missing, so the id is\n // shown plainly. Flashing \"unavailable\" over every link for the beat\n // before the console's host subscription lands would teach authors to\n // ignore the warning that matters.\n if (!screensKnown) return screenId\n // A listing has no publish state of its own: its collection is either gone\n // or no longer has the slug its address is built from. A feed hangs off the\n // same listing, so it is lost the same two ways (AGL-3118).\n const collectionId = parseCollectionLinkValue(screenId)\n if (collectionId) {\n return `⚠ Unavailable collection listing (${collectionId}) — deleted or has no slug`\n }\n const feedCollectionId = parseFeedLinkValue(screenId)\n if (feedCollectionId) {\n return `⚠ Unavailable RSS feed (${feedCollectionId}) — collection deleted or has no slug`\n }\n const entry = parseEntryLinkValue(screenId)\n return entry\n ? `⚠ Unavailable entry (${entry.entryId}) — unpublished or deleted`\n : `⚠ Unavailable screen (${screenId}) — unpublished or deleted`\n}\n\nexport function unresolvedScreenOption(\n value: unknown,\n screens: ScreenRouteMap | undefined,\n): { value: string; label: string } | undefined {\n const raw = typeof value === 'string' ? value.trim() : ''\n if (!raw) return undefined\n const target = splitLinkValue(raw, undefined)\n if (target.screenId) {\n if (resolveScreenHref(screens, target.screenId) !== undefined) {\n return undefined\n }\n return {\n value: raw,\n label: unavailableScreenLabel(\n target.screenId,\n screenRoutesAnswerFor(screens, target.screenId),\n ),\n }\n }\n return target.href\n ? { value: raw, label: `⚠ Plain address (${target.href}) — not a screen` }\n : undefined\n}\n\n/** One target a link picker offers — see {@link screenLinkTargetOptions}. */\nexport interface ScreenLinkTargetOption {\n /**\n * The routing-map key: a bare screen id, a collection listing's\n * `collection:<id>`, or its feed's `feed:<id>`. Stored as it is in a screen\n * slot, and through `formatScreenLinkValue` in a `Link`-typed prop.\n */\n value: string\n label: string\n kind: 'screen' | 'collection-listing' | 'feed'\n}\n\n/**\n * What a picker calls one routing-map target, or `undefined` for a key the\n * map does not hold — the label {@link screenLinkTargetOptions} gives it.\n *\n * A listing and a feed are labeled with their collection's name and address,\n * like a screen, and marked as what they are IN the label rather than only by\n * position: a closed picker shows the chosen label and nothing else, and\n * \"Blog (/blog)\" there reads exactly like a screen named Blog. A feed borrows\n * its collection's name, which is the only name it has (AGL-3119).\n *\n * An entry is never named here. A map holds only the entries a page\n * references, under no label, so an entry is named by the search seam\n * (`LinkTargetSearchContext`), which reads the entry itself.\n */\nexport function screenLinkTargetLabel(\n key: string,\n screens: ScreenRouteMap | undefined,\n labels: Record<string, string> | undefined,\n): string | undefined {\n const href = resolveScreenHref(screens, key)\n if (href === undefined) return undefined\n const collectionId = parseCollectionLinkValue(key)\n if (collectionId) {\n return `${labels?.[key] ?? collectionId} (${href}) — collection listing`\n }\n const feedCollectionId = parseFeedLinkValue(key)\n if (feedCollectionId) {\n const name =\n labels?.[key] ??\n labels?.[formatCollectionLinkValue(feedCollectionId)] ??\n feedCollectionId\n return `${name} (${href}) — RSS feed`\n }\n if (parseEntryLinkValue(key)) return undefined\n return `${labels?.[key] ?? key} (${href})`\n}\n\n/**\n * Everything a Screen picker offers, in the order it offers it (AGL-2799):\n * the host's screens, then its content collections' listing pages, then\n * their RSS feeds (AGL-3119).\n *\n * ONE builder for both pickers — the attributes panel's `SCREEN_SELECT` and\n * the `Link`-typed prop picker. Each used to map the routing map into options\n * by itself, so a target added to one list was missing from the other; and\n * neither offered a collection's listing at all, which is how the drawer on\n * aglyn.com came to link its blog as a typed `/blog`.\n *\n * Entries are left out even when the map holds some: they are searched, not\n * listed, because a site's entries are far too many to read into a dropdown\n * — see `LinkTargetSearchContext`.\n *\n * `order` keeps each picker's established screen order — by path in the\n * attributes panel, by name in the prop picker — and the listings and feeds\n * follow the screens in the same order.\n */\nexport function screenLinkTargetOptions(\n screens: ScreenRouteMap | undefined,\n labels: Record<string, string> | undefined,\n order: 'path' | 'label' = 'path',\n): ScreenLinkTargetOption[] {\n type Ranked = ScreenLinkTargetOption & { path: string }\n const pages: Ranked[] = []\n const listings: Ranked[] = []\n const feeds: Ranked[] = []\n for (const [key, path] of Object.entries(screens ?? {})) {\n const kind = linkTargetKind(key)\n if (kind === 'entry') continue\n const label = screenLinkTargetLabel(key, screens, labels) ?? key\n if (kind === 'collection') {\n listings.push({ value: key, label, kind: 'collection-listing', path })\n } else if (kind === 'feed') {\n feeds.push({ value: key, label, kind: 'feed', path })\n } else {\n pages.push({ value: key, label, kind: 'screen', path })\n }\n }\n const compare = (a: Ranked, b: Ranked) =>\n order === 'path'\n ? a.path.localeCompare(b.path)\n : a.label.localeCompare(b.label)\n return [\n ...pages.sort(compare),\n ...listings.sort(compare),\n ...feeds.sort(compare),\n ].map(({ value, label, kind }) => ({ value, label, kind }))\n}\n\n/**\n * Marks the element in the DOM. Present on the live site too, deliberately:\n * a smoke pass can then find every dead control on a page with one selector\n * instead of reading five different components' conditions.\n */\nexport const BROKEN_SCREEN_LINK_ATTR = 'data-aglyn-broken-link'\n\n/** What the AUTHOR is told, on the one surface that can fix it. */\nexport const BROKEN_SCREEN_LINK_MESSAGE =\n 'Broken link: this points at a screen or collection listing that is ' +\n 'unpublished or deleted, so it will not work on the published site. Pick ' +\n 'a target again in the attributes panel, or clear the link.'\n\n/**\n * The editor-only outline. A warning ring rather than an error one: the\n * screen may be deliberately unpublished and about to come back, and the\n * page is not broken, one control on it is.\n */\nexport const BROKEN_SCREEN_LINK_SX = {\n outline: '2px dashed',\n outlineColor: 'warning.main',\n outlineOffset: '2px',\n borderRadius: 1,\n}\n\n/**\n * Props that make a dead screen link visible to its author (AGL-1893).\n *\n * The tooltip and the ring are `editorInert` only — that is the static\n * besigner canvas, the one surface where the person who can fix this is\n * looking at it. Preview deliberately does NOT get them: preview exists to\n * show what visitors will see, and an authoring annotation painted into it\n * is a lie of a different kind.\n *\n * The data attribute ships everywhere, canvas and live site alike.\n */\nexport function brokenScreenLinkProps(\n broken: boolean,\n editorInert: boolean,\n): Record<string, unknown> {\n if (!broken) return {}\n return {\n [BROKEN_SCREEN_LINK_ATTR]: '',\n ...(editorInert ? { title: BROKEN_SCREEN_LINK_MESSAGE } : null),\n }\n}\n\n/**\n * Link VALUE parsing AND resolution live in `./screen-link-value` —\n * server-safe, because the where-used scan and the Markdown representation of\n * a page both run on the server, and the `createContext` this module reaches\n * keeps it out of the `@aglyn/aglyn/server` barrel (AGL-703, AGL-2740).\n * Re-exported here so every existing importer, and the spec beside this file,\n * keeps reaching them at the address they have always used.\n */\nexport {\n COLLECTION_LINK_VALUE_PREFIX,\n ENTRY_LINK_VALUE_PREFIX,\n EXTERNAL_HREF_PATTERN,\n FEED_LINK_VALUE_PREFIX,\n SAFE_HREF_PATTERN,\n SCREEN_LINK_VALUE_PREFIX,\n formatCollectionLinkValue,\n formatEntryLinkValue,\n formatFeedLinkValue,\n formatScreenLinkValue,\n linkTargetKind,\n nodesReferenceScreen,\n parseCollectionLinkValue,\n parseEntryLinkValue,\n parseFeedLinkValue,\n parseScreenLinkValue,\n resolveScreenHref,\n screenRoutesAnswerFor,\n splitLinkValue,\n type LinkTargetKind,\n} from './screen-link-value'\n\n/** What a linking element renders — see {@link useLinkTarget}. */\nexport interface ResolvedLinkTarget extends ResolvedScreenLink {\n /** The literal href in play once safety-checked, when no screen resolved. */\n externalHref?: string\n /** True when `href` leaves the site — what \"open in a new tab\" may act on. */\n leavesSite: boolean\n}\n\n/**\n * The single href a linking element should render, from the `screenId` +\n * `href` pair every one of them declares (AGL-1335).\n *\n * Button, Screen Link, Link Box and Image each had the same four lines —\n * resolve the id, safety-check the URL, prefer the id — which is three\n * copies too many now that a `Link`-typed component prop can feed either\n * slot with either shape. Hook-shaped because {@link useScreenLink} is;\n * it is always called, so the hook order never depends on the values.\n */\nexport function useLinkTarget(\n screenId: string | null | undefined,\n href: string | null | undefined,\n): ResolvedLinkTarget {\n const target = splitLinkValue(screenId, href)\n const resolved = useScreenLink(target.screenId)\n const externalHref =\n target.href && SAFE_HREF_PATTERN.test(target.href) ? target.href : undefined\n const finalHref = target.screenId ? resolved.href : externalHref\n return {\n ...resolved,\n href: finalHref,\n externalHref,\n leavesSite: Boolean(\n !target.screenId && externalHref && EXTERNAL_HREF_PATTERN.test(externalHref),\n ),\n }\n}\n\n/**\n * Resolves a screen id to its current href. Memoized on the routing-map\n * identity: a slug rename, parent-slug change, or re-parent produces a new\n * map value, so cached hrefs reset exactly when the map changes. Returns no\n * href for unknown/unpublished ids — callers degrade to plain content\n * instead of rendering a dead link.\n */\nexport function useScreenLink(\n screenId: string | null | undefined,\n): ResolvedScreenLink {\n const { screens, suppressNavigation, editorInert } = useContext(ScreenLinkContext)\n const href = useMemo(() => {\n if (!screenId) return undefined\n const resolved = resolveScreenHref(screens, screenId)\n if (resolved === undefined) {\n if (process.env.NODE_ENV !== 'production') {\n console.warn(\n `[ScreenLink] screen \"${screenId}\" has no routing-map entry — ` +\n 'it may be unpublished or deleted; rendering without an href.',\n )\n }\n return undefined\n }\n return resolved\n }, [screenId, screens])\n return {\n href,\n suppressNavigation: Boolean(suppressNavigation),\n editorInert: Boolean(editorInert),\n // NOT `!href`: a suppressed surface has no href for links that resolve\n // perfectly well, and the whole point of this flag is to separate the\n // two (AGL-1893).\n broken: isScreenLinkBroken(screens, screenId),\n }\n}\n"],"names":["useContext","useMemo","ScreenLinkContext","EXTERNAL_HREF_PATTERN","SAFE_HREF_PATTERN","formatCollectionLinkValue","linkTargetKind","parseCollectionLinkValue","parseEntryLinkValue","parseFeedLinkValue","resolveScreenHref","screenRoutesAnswerFor","splitLinkValue","isScreenLinkBroken","screens","screenId","undefined","unavailableScreenLabel","screensKnown","collectionId","feedCollectionId","entry","entryId","unresolvedScreenOption","value","raw","trim","target","label","href","screenLinkTargetLabel","key","labels","name","screenLinkTargetOptions","order","pages","listings","feeds","path","Object","entries","kind","push","compare","a","b","localeCompare","sort","map","BROKEN_SCREEN_LINK_ATTR","BROKEN_SCREEN_LINK_MESSAGE","BROKEN_SCREEN_LINK_SX","outline","outlineColor","outlineOffset","borderRadius","brokenScreenLinkProps","broken","editorInert","title","COLLECTION_LINK_VALUE_PREFIX","ENTRY_LINK_VALUE_PREFIX","FEED_LINK_VALUE_PREFIX","SCREEN_LINK_VALUE_PREFIX","formatEntryLinkValue","formatFeedLinkValue","formatScreenLinkValue","nodesReferenceScreen","parseScreenLinkValue","useLinkTarget","resolved","useScreenLink","externalHref","test","finalHref","leavesSite","Boolean","suppressNavigation","process","env","NODE_ENV","console","warn"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/screen-link-context.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n// Lives in @aglyn/aglyn (not a UI lib) deliberately, WITHOUT a 'use client'\n// banner: every surface that renders canvas nodes already imports this\n// package, and a client boundary here (or importing the shared-ui-jsx\n// barrel from the tenant page) makes the bundler duplicate parts of the\n// module graph — a second canvas/emitter instance renders the site blank.\nimport { useContext, useMemo } from 'react'\nimport {\n ScreenLinkContext,\n type ScreenRouteMap,\n} from './screen-link-context-value'\nimport {\n EXTERNAL_HREF_PATTERN,\n SAFE_HREF_PATTERN,\n formatCollectionLinkValue,\n linkTargetKind,\n parseCollectionLinkValue,\n parseEntryLinkValue,\n parseFeedLinkValue,\n resolveScreenHref,\n screenRoutesAnswerFor,\n splitLinkValue,\n} from './screen-link-value'\n\nexport * from './screen-link-context-value'\n\nexport interface ResolvedScreenLink {\n /** Site-relative href (`/`, `/company/about`), undefined when unresolvable. */\n href?: string\n suppressNavigation: boolean\n /** True only on the static besigner canvas — interactions are inert. */\n editorInert: boolean\n /**\n * A screen id WAS authored and the routing map does not have it — the\n * target is unpublished or deleted (AGL-1893).\n *\n * Distinct from \"no href\": the canvas and the preview withhold hrefs on\n * purpose, and an element with no link at all never had one to lose.\n * Only this flag means the author asked for a link that cannot exist, and\n * only elements reading THIS may say so — see {@link isScreenLinkBroken}\n * for why an absent or empty map is deliberately not \"broken\".\n */\n broken: boolean\n}\n\n/**\n * Whether an authored screen id points at nothing (AGL-1893).\n *\n * Unpublishing or deleting a screen is a normal authoring action, and until\n * this existed nothing anywhere reported what it had just broken: a Tabs\n * link to a retired screen shipped as a live-looking control that silently\n * did nothing on `aglyn.com/changelog` and `/newsroom` for two days.\n *\n * The two \"we cannot tell\" cases are NOT broken, and the distinction is the\n * whole safety of this predicate:\n *\n * - **no map** — a surface that renders canvas nodes without providing\n * `ScreenLinkContext` (an isolated spec, an embed) knows nothing about\n * the host's screens, and must not conclude that every link is dead;\n * - **an empty map** — the same thing one beat earlier: the console's live\n * host subscription starts empty, and a moment of `{}` must not repaint a\n * whole navigation row as broken and then repaint it back.\n *\n * Both failure directions were weighed. Calling a live link broken hides or\n * disables working navigation on a customer's site; calling a dead link fine\n * leaves exactly the defect this issue is about. Only the first is\n * unrecoverable from the visitor's side, so the doubt resolves that way.\n */\nexport function isScreenLinkBroken(\n screens: ScreenRouteMap | undefined,\n screenId: string | null | undefined,\n): boolean {\n if (!screenId) return false\n // Both \"we cannot tell\" cases, asked of the half of the map that holds this\n // target's kind: a collection listing (AGL-2799) is judged against the\n // listings, a screen against the screens — see `screenRoutesAnswerFor`.\n if (!screenRoutesAnswerFor(screens, screenId)) return false\n return resolveScreenHref(screens, screenId) === undefined\n}\n\n/**\n * The extra option a Screen picker needs when its stored value matches none\n * of the host's screens (AGL-1893).\n *\n * The console builds `SCREEN_SELECT` options from the routing map, so a\n * value the map has lost — the retired `/blog` screen, say — matches\n * nothing and the field renders EMPTY. Which reads, to the person looking\n * at it, as \"no link set\". Meanwhile the element still behaves as linked:\n * the strip is still a nav landmark, that tab is still skipped as\n * \"navigates\", and the panel it would have revealed is still suppressed. So\n * the one surface where this is repairable was also the one actively\n * denying there was anything to repair.\n *\n * Returns the stored value UNCHANGED as the option's value: naming a dead\n * target must not rewrite it, or opening the panel and pressing Save would\n * quietly convert a recoverable reference into something else.\n *\n * Three shapes get named, because a Screen picker can legally hold any of\n * them (AGL-1335 / AGL-1894 / AGL-2799): a screen reference whose screen is\n * gone, a collection listing whose collection is gone, and a plain address\n * typed in before the picker existed. The last is not broken — but it is\n * just as invisible, and an author who cannot see it is the reason those\n * links are not rename-safe yet.\n */\nexport function unavailableScreenLabel(\n screenId: string,\n screensKnown: boolean,\n): string {\n // With no map loaded yet nothing is known to be missing, so the id is\n // shown plainly. Flashing \"unavailable\" over every link for the beat\n // before the console's host subscription lands would teach authors to\n // ignore the warning that matters.\n if (!screensKnown) return screenId\n // A listing has no publish state of its own: its collection is either gone\n // or no longer has the slug its address is built from. A feed hangs off the\n // same listing, so it is lost the same two ways (AGL-3118).\n const collectionId = parseCollectionLinkValue(screenId)\n if (collectionId) {\n return `⚠ Unavailable collection listing (${collectionId}) — deleted or has no slug`\n }\n const feedCollectionId = parseFeedLinkValue(screenId)\n if (feedCollectionId) {\n return `⚠ Unavailable RSS feed (${feedCollectionId}) — collection deleted or has no slug`\n }\n const entry = parseEntryLinkValue(screenId)\n return entry\n ? `⚠ Unavailable entry (${entry.entryId}) — unpublished or deleted`\n : `⚠ Unavailable page (${screenId}) — unpublished or deleted`\n}\n\nexport function unresolvedScreenOption(\n value: unknown,\n screens: ScreenRouteMap | undefined,\n): { value: string; label: string } | undefined {\n const raw = typeof value === 'string' ? value.trim() : ''\n if (!raw) return undefined\n const target = splitLinkValue(raw, undefined)\n if (target.screenId) {\n if (resolveScreenHref(screens, target.screenId) !== undefined) {\n return undefined\n }\n return {\n value: raw,\n label: unavailableScreenLabel(\n target.screenId,\n screenRoutesAnswerFor(screens, target.screenId),\n ),\n }\n }\n return target.href\n ? { value: raw, label: `⚠ Plain address (${target.href}) — not a page` }\n : undefined\n}\n\n/** One target a link picker offers — see {@link screenLinkTargetOptions}. */\nexport interface ScreenLinkTargetOption {\n /**\n * The routing-map key: a bare screen id, a collection listing's\n * `collection:<id>`, or its feed's `feed:<id>`. Stored as it is in a screen\n * slot, and through `formatScreenLinkValue` in a `Link`-typed prop.\n */\n value: string\n label: string\n kind: 'screen' | 'collection-listing' | 'feed'\n}\n\n/**\n * What a picker calls one routing-map target, or `undefined` for a key the\n * map does not hold — the label {@link screenLinkTargetOptions} gives it.\n *\n * A listing and a feed are labeled with their collection's name and address,\n * like a screen, and marked as what they are IN the label rather than only by\n * position: a closed picker shows the chosen label and nothing else, and\n * \"Blog (/blog)\" there reads exactly like a screen named Blog. A feed borrows\n * its collection's name, which is the only name it has (AGL-3119).\n *\n * An entry is never named here. A map holds only the entries a page\n * references, under no label, so an entry is named by the search seam\n * (`LinkTargetSearchContext`), which reads the entry itself.\n */\nexport function screenLinkTargetLabel(\n key: string,\n screens: ScreenRouteMap | undefined,\n labels: Record<string, string> | undefined,\n): string | undefined {\n const href = resolveScreenHref(screens, key)\n if (href === undefined) return undefined\n const collectionId = parseCollectionLinkValue(key)\n if (collectionId) {\n return `${labels?.[key] ?? collectionId} (${href}) — collection listing`\n }\n const feedCollectionId = parseFeedLinkValue(key)\n if (feedCollectionId) {\n const name =\n labels?.[key] ??\n labels?.[formatCollectionLinkValue(feedCollectionId)] ??\n feedCollectionId\n return `${name} (${href}) — RSS feed`\n }\n if (parseEntryLinkValue(key)) return undefined\n return `${labels?.[key] ?? key} (${href})`\n}\n\n/**\n * Everything a Screen picker offers, in the order it offers it (AGL-2799):\n * the host's screens, then its content collections' listing pages, then\n * their RSS feeds (AGL-3119).\n *\n * ONE builder for both pickers — the attributes panel's `SCREEN_SELECT` and\n * the `Link`-typed prop picker. Each used to map the routing map into options\n * by itself, so a target added to one list was missing from the other; and\n * neither offered a collection's listing at all, which is how the drawer on\n * aglyn.com came to link its blog as a typed `/blog`.\n *\n * Entries are left out even when the map holds some: they are searched, not\n * listed, because a site's entries are far too many to read into a dropdown\n * — see `LinkTargetSearchContext`.\n *\n * `order` keeps each picker's established screen order — by path in the\n * attributes panel, by name in the prop picker — and the listings and feeds\n * follow the screens in the same order.\n */\nexport function screenLinkTargetOptions(\n screens: ScreenRouteMap | undefined,\n labels: Record<string, string> | undefined,\n order: 'path' | 'label' = 'path',\n): ScreenLinkTargetOption[] {\n type Ranked = ScreenLinkTargetOption & { path: string }\n const pages: Ranked[] = []\n const listings: Ranked[] = []\n const feeds: Ranked[] = []\n for (const [key, path] of Object.entries(screens ?? {})) {\n const kind = linkTargetKind(key)\n if (kind === 'entry') continue\n const label = screenLinkTargetLabel(key, screens, labels) ?? key\n if (kind === 'collection') {\n listings.push({ value: key, label, kind: 'collection-listing', path })\n } else if (kind === 'feed') {\n feeds.push({ value: key, label, kind: 'feed', path })\n } else {\n pages.push({ value: key, label, kind: 'screen', path })\n }\n }\n const compare = (a: Ranked, b: Ranked) =>\n order === 'path'\n ? a.path.localeCompare(b.path)\n : a.label.localeCompare(b.label)\n return [\n ...pages.sort(compare),\n ...listings.sort(compare),\n ...feeds.sort(compare),\n ].map(({ value, label, kind }) => ({ value, label, kind }))\n}\n\n/**\n * Marks the element in the DOM. Present on the live site too, deliberately:\n * a smoke pass can then find every dead control on a page with one selector\n * instead of reading five different components' conditions.\n */\nexport const BROKEN_SCREEN_LINK_ATTR = 'data-aglyn-broken-link'\n\n/** What the AUTHOR is told, on the one surface that can fix it. */\nexport const BROKEN_SCREEN_LINK_MESSAGE =\n 'Broken link: this points at a page or collection listing that is ' +\n 'unpublished or deleted, so it will not work on the published site. Pick ' +\n 'a target again in the attributes panel, or clear the link.'\n\n/**\n * The editor-only outline. A warning ring rather than an error one: the\n * screen may be deliberately unpublished and about to come back, and the\n * page is not broken, one control on it is.\n */\nexport const BROKEN_SCREEN_LINK_SX = {\n outline: '2px dashed',\n outlineColor: 'warning.main',\n outlineOffset: '2px',\n borderRadius: 1,\n}\n\n/**\n * Props that make a dead screen link visible to its author (AGL-1893).\n *\n * The tooltip and the ring are `editorInert` only — that is the static\n * besigner canvas, the one surface where the person who can fix this is\n * looking at it. Preview deliberately does NOT get them: preview exists to\n * show what visitors will see, and an authoring annotation painted into it\n * is a lie of a different kind.\n *\n * The data attribute ships everywhere, canvas and live site alike.\n */\nexport function brokenScreenLinkProps(\n broken: boolean,\n editorInert: boolean,\n): Record<string, unknown> {\n if (!broken) return {}\n return {\n [BROKEN_SCREEN_LINK_ATTR]: '',\n ...(editorInert ? { title: BROKEN_SCREEN_LINK_MESSAGE } : null),\n }\n}\n\n/**\n * Link VALUE parsing AND resolution live in `./screen-link-value` —\n * server-safe, because the where-used scan and the Markdown representation of\n * a page both run on the server, and the `createContext` this module reaches\n * keeps it out of the `@aglyn/aglyn/server` barrel (AGL-703, AGL-2740).\n * Re-exported here so every existing importer, and the spec beside this file,\n * keeps reaching them at the address they have always used.\n */\nexport {\n COLLECTION_LINK_VALUE_PREFIX,\n ENTRY_LINK_VALUE_PREFIX,\n EXTERNAL_HREF_PATTERN,\n FEED_LINK_VALUE_PREFIX,\n SAFE_HREF_PATTERN,\n SCREEN_LINK_VALUE_PREFIX,\n formatCollectionLinkValue,\n formatEntryLinkValue,\n formatFeedLinkValue,\n formatScreenLinkValue,\n linkTargetKind,\n nodesReferenceScreen,\n parseCollectionLinkValue,\n parseEntryLinkValue,\n parseFeedLinkValue,\n parseScreenLinkValue,\n resolveScreenHref,\n screenRoutesAnswerFor,\n splitLinkValue,\n type LinkTargetKind,\n} from './screen-link-value'\n\n/** What a linking element renders — see {@link useLinkTarget}. */\nexport interface ResolvedLinkTarget extends ResolvedScreenLink {\n /** The literal href in play once safety-checked, when no screen resolved. */\n externalHref?: string\n /** True when `href` leaves the site — what \"open in a new tab\" may act on. */\n leavesSite: boolean\n}\n\n/**\n * The single href a linking element should render, from the `screenId` +\n * `href` pair every one of them declares (AGL-1335).\n *\n * Button, Screen Link, Link Box and Image each had the same four lines —\n * resolve the id, safety-check the URL, prefer the id — which is three\n * copies too many now that a `Link`-typed component prop can feed either\n * slot with either shape. Hook-shaped because {@link useScreenLink} is;\n * it is always called, so the hook order never depends on the values.\n */\nexport function useLinkTarget(\n screenId: string | null | undefined,\n href: string | null | undefined,\n): ResolvedLinkTarget {\n const target = splitLinkValue(screenId, href)\n const resolved = useScreenLink(target.screenId)\n const externalHref =\n target.href && SAFE_HREF_PATTERN.test(target.href) ? target.href : undefined\n const finalHref = target.screenId ? resolved.href : externalHref\n return {\n ...resolved,\n href: finalHref,\n externalHref,\n leavesSite: Boolean(\n !target.screenId && externalHref && EXTERNAL_HREF_PATTERN.test(externalHref),\n ),\n }\n}\n\n/**\n * Resolves a screen id to its current href. Memoized on the routing-map\n * identity: a slug rename, parent-slug change, or re-parent produces a new\n * map value, so cached hrefs reset exactly when the map changes. Returns no\n * href for unknown/unpublished ids — callers degrade to plain content\n * instead of rendering a dead link.\n */\nexport function useScreenLink(\n screenId: string | null | undefined,\n): ResolvedScreenLink {\n const { screens, suppressNavigation, editorInert } = useContext(ScreenLinkContext)\n const href = useMemo(() => {\n if (!screenId) return undefined\n const resolved = resolveScreenHref(screens, screenId)\n if (resolved === undefined) {\n if (process.env.NODE_ENV !== 'production') {\n console.warn(\n `[ScreenLink] screen \"${screenId}\" has no routing-map entry — ` +\n 'it may be unpublished or deleted; rendering without an href.',\n )\n }\n return undefined\n }\n return resolved\n }, [screenId, screens])\n return {\n href,\n suppressNavigation: Boolean(suppressNavigation),\n editorInert: Boolean(editorInert),\n // NOT `!href`: a suppressed surface has no href for links that resolve\n // perfectly well, and the whole point of this flag is to separate the\n // two (AGL-1893).\n broken: isScreenLinkBroken(screens, screenId),\n }\n}\n"],"names":["useContext","useMemo","ScreenLinkContext","EXTERNAL_HREF_PATTERN","SAFE_HREF_PATTERN","formatCollectionLinkValue","linkTargetKind","parseCollectionLinkValue","parseEntryLinkValue","parseFeedLinkValue","resolveScreenHref","screenRoutesAnswerFor","splitLinkValue","isScreenLinkBroken","screens","screenId","undefined","unavailableScreenLabel","screensKnown","collectionId","feedCollectionId","entry","entryId","unresolvedScreenOption","value","raw","trim","target","label","href","screenLinkTargetLabel","key","labels","name","screenLinkTargetOptions","order","pages","listings","feeds","path","Object","entries","kind","push","compare","a","b","localeCompare","sort","map","BROKEN_SCREEN_LINK_ATTR","BROKEN_SCREEN_LINK_MESSAGE","BROKEN_SCREEN_LINK_SX","outline","outlineColor","outlineOffset","borderRadius","brokenScreenLinkProps","broken","editorInert","title","COLLECTION_LINK_VALUE_PREFIX","ENTRY_LINK_VALUE_PREFIX","FEED_LINK_VALUE_PREFIX","SCREEN_LINK_VALUE_PREFIX","formatEntryLinkValue","formatFeedLinkValue","formatScreenLinkValue","nodesReferenceScreen","parseScreenLinkValue","useLinkTarget","resolved","useScreenLink","externalHref","test","finalHref","leavesSite","Boolean","suppressNavigation","process","env","NODE_ENV","console","warn"],"mappings":"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"}
|
|
@@ -208,6 +208,21 @@ export declare function buildScreenRouteEntries(screenId: ScreenUid, screensById
|
|
|
208
208
|
* uniqueness checks before publishing.
|
|
209
209
|
*/
|
|
210
210
|
export declare function findScreenIdByRoutePath(screens: Record<ScreenUid, string> | null | undefined, path: string): ScreenUid | undefined;
|
|
211
|
+
/**
|
|
212
|
+
* The screen a publish at `path` would collide with, or `undefined` when the
|
|
213
|
+
* address is free to take (AGL-3408).
|
|
214
|
+
*
|
|
215
|
+
* {@link findScreenIdByRoutePath} with one exception: the placeholder home
|
|
216
|
+
* page a site is created with (`host.defaultHomeScreenId`) does not block the
|
|
217
|
+
* site root. It holds `/` only so a new site never answers its own address
|
|
218
|
+
* with a 404, and the first real page published there replaces it — the
|
|
219
|
+
* publish seam removes its entry in the same write. It still blocks any other
|
|
220
|
+
* path it has been moved to, and no other screen is ever passed over.
|
|
221
|
+
*
|
|
222
|
+
* Every publish surface's conflict check reads this rather than the raw owner,
|
|
223
|
+
* so "is this address taken?" has one answer across them.
|
|
224
|
+
*/
|
|
225
|
+
export declare function blockingRouteOwner(screens: Record<ScreenUid, string> | null | undefined, path: string, defaultHomeScreenId?: string | null): ScreenUid | undefined;
|
|
211
226
|
/** Human-facing URL for a routing-map path (`'/'` stays `/`, `about` → `/about`). */
|
|
212
227
|
export declare function screenRoutePathToUrl(path: string): string;
|
|
213
228
|
/**
|
|
@@ -162,7 +162,7 @@ import { formatCollectionLinkValue, formatEntryLinkValue, formatFeedLinkValue, p
|
|
|
162
162
|
*
|
|
163
163
|
* It names the alternative, because a `/` in this field almost always means
|
|
164
164
|
* "nest me" and the hierarchy is what actually composes the longer path.
|
|
165
|
-
*/ export const SCREEN_SLUG_PATH_SEPARATOR_MESSAGE = 'A slug is one path segment — remove the "/" and nest the
|
|
165
|
+
*/ export const SCREEN_SLUG_PATH_SEPARATOR_MESSAGE = 'A slug is one path segment — remove the "/" and nest the page under a ' + 'parent page instead';
|
|
166
166
|
/** Defensive cap: parent chains deeper than this are treated as invalid. */ const MAX_SCREEN_DEPTH = 32;
|
|
167
167
|
/**
|
|
168
168
|
* Composes a screen's routing-map path from its own slug plus its ancestor
|
|
@@ -266,6 +266,26 @@ import { formatCollectionLinkValue, formatEntryLinkValue, formatFeedLinkValue, p
|
|
|
266
266
|
const entry = Object.entries(screens).find(([, value])=>value === path);
|
|
267
267
|
return entry == null ? void 0 : entry[0];
|
|
268
268
|
}
|
|
269
|
+
/**
|
|
270
|
+
* The screen a publish at `path` would collide with, or `undefined` when the
|
|
271
|
+
* address is free to take (AGL-3408).
|
|
272
|
+
*
|
|
273
|
+
* {@link findScreenIdByRoutePath} with one exception: the placeholder home
|
|
274
|
+
* page a site is created with (`host.defaultHomeScreenId`) does not block the
|
|
275
|
+
* site root. It holds `/` only so a new site never answers its own address
|
|
276
|
+
* with a 404, and the first real page published there replaces it — the
|
|
277
|
+
* publish seam removes its entry in the same write. It still blocks any other
|
|
278
|
+
* path it has been moved to, and no other screen is ever passed over.
|
|
279
|
+
*
|
|
280
|
+
* Every publish surface's conflict check reads this rather than the raw owner,
|
|
281
|
+
* so "is this address taken?" has one answer across them.
|
|
282
|
+
*/ export function blockingRouteOwner(screens, path, defaultHomeScreenId) {
|
|
283
|
+
const owner = findScreenIdByRoutePath(screens, path);
|
|
284
|
+
if (owner && path === SCREEN_ROOT_PATH && owner === defaultHomeScreenId) {
|
|
285
|
+
return undefined;
|
|
286
|
+
}
|
|
287
|
+
return owner;
|
|
288
|
+
}
|
|
269
289
|
/** Human-facing URL for a routing-map path (`'/'` stays `/`, `about` → `/about`). */ export function screenRoutePathToUrl(path) {
|
|
270
290
|
return path === SCREEN_ROOT_PATH ? SCREEN_ROOT_PATH : `/${path}`;
|
|
271
291
|
}
|