@aglyn/aglyn 1.0.0-beta.220 → 1.0.0-beta.222
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 +15 -11
- package/src/lib/app-utils/agent-openapi.js +1 -1
- package/src/lib/app-utils/agent-openapi.js.map +1 -1
- package/src/lib/app-utils/analytics-events.d.ts +3 -2
- package/src/lib/app-utils/analytics-events.js +3 -2
- package/src/lib/app-utils/analytics-events.js.map +1 -1
- package/src/lib/app-utils/api-plugins.d.ts +30 -0
- package/src/lib/app-utils/api-plugins.js +24 -1
- package/src/lib/app-utils/api-plugins.js.map +1 -1
- package/src/lib/app-utils/author-html.d.ts +1 -1
- package/src/lib/app-utils/author-html.js.map +1 -1
- package/src/lib/app-utils/bandwidth-cap.d.ts +12 -1
- package/src/lib/app-utils/bandwidth-cap.js +17 -1
- package/src/lib/app-utils/bandwidth-cap.js.map +1 -1
- package/src/lib/app-utils/compose-reusable-components.d.ts +19 -0
- package/src/lib/app-utils/compose-reusable-components.js +116 -26
- package/src/lib/app-utils/compose-reusable-components.js.map +1 -1
- package/src/lib/app-utils/console-routes.d.ts +4 -0
- package/src/lib/app-utils/console-routes.js +3 -0
- package/src/lib/app-utils/console-routes.js.map +1 -1
- package/src/lib/app-utils/contacts.d.ts +71 -0
- package/src/lib/app-utils/contacts.js +55 -0
- package/src/lib/app-utils/contacts.js.map +1 -1
- package/src/lib/app-utils/contexts.d.ts +1 -0
- package/src/lib/app-utils/contexts.js +1 -0
- package/src/lib/app-utils/contexts.js.map +1 -1
- package/src/lib/app-utils/crm-email-templates.js +21 -4
- package/src/lib/app-utils/crm-email-templates.js.map +1 -1
- package/src/lib/app-utils/crm-kinds.d.ts +2 -0
- package/src/lib/app-utils/crm-kinds.js +1 -0
- package/src/lib/app-utils/crm-kinds.js.map +1 -1
- package/src/lib/app-utils/crm.d.ts +1199 -67
- package/src/lib/app-utils/crm.js +1951 -310
- package/src/lib/app-utils/crm.js.map +1 -1
- package/src/lib/app-utils/csv-import.d.ts +10 -38
- package/src/lib/app-utils/csv-import.js +17 -42
- package/src/lib/app-utils/csv-import.js.map +1 -1
- package/src/lib/app-utils/csv.d.ts +32 -11
- package/src/lib/app-utils/csv.js +43 -5
- package/src/lib/app-utils/csv.js.map +1 -1
- package/src/lib/app-utils/default-site.d.ts +116 -0
- package/src/lib/app-utils/default-site.js +1024 -0
- package/src/lib/app-utils/default-site.js.map +1 -0
- package/src/lib/app-utils/docs-help.generated.d.ts +23 -23
- package/src/lib/app-utils/docs-help.generated.js +47 -10
- package/src/lib/app-utils/docs-help.generated.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.js +746 -151
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/draft-placeholders.d.ts +76 -0
- package/src/lib/app-utils/draft-placeholders.js +115 -0
- package/src/lib/app-utils/draft-placeholders.js.map +1 -0
- package/src/lib/app-utils/entity-picker-context.d.ts +19 -15
- package/src/lib/app-utils/entity-picker-context.js +4 -4
- package/src/lib/app-utils/entity-picker-context.js.map +1 -1
- package/src/lib/app-utils/expand-repeatables.d.ts +66 -10
- package/src/lib/app-utils/expand-repeatables.js +88 -14
- package/src/lib/app-utils/expand-repeatables.js.map +1 -1
- package/src/lib/app-utils/forms.d.ts +33 -22
- package/src/lib/app-utils/forms.js +47 -28
- package/src/lib/app-utils/forms.js.map +1 -1
- package/src/lib/app-utils/full-use-cost.d.ts +277 -0
- package/src/lib/app-utils/full-use-cost.js +421 -0
- package/src/lib/app-utils/full-use-cost.js.map +1 -0
- package/src/lib/app-utils/health-report.js +33 -0
- package/src/lib/app-utils/health-report.js.map +1 -1
- package/src/lib/app-utils/leaving-notice.d.ts +96 -0
- package/src/lib/app-utils/leaving-notice.js +157 -0
- package/src/lib/app-utils/leaving-notice.js.map +1 -0
- package/src/lib/app-utils/llms-txt.d.ts +20 -0
- package/src/lib/app-utils/llms-txt.js +7 -1
- package/src/lib/app-utils/llms-txt.js.map +1 -1
- package/src/lib/app-utils/media-asset-facts.d.ts +16 -1
- package/src/lib/app-utils/media-asset-facts.js +40 -13
- package/src/lib/app-utils/media-asset-facts.js.map +1 -1
- package/src/lib/app-utils/media-bandwidth.d.ts +52 -0
- package/src/lib/app-utils/media-bandwidth.js +94 -0
- package/src/lib/app-utils/media-bandwidth.js.map +1 -0
- package/src/lib/app-utils/media-ref.d.ts +241 -33
- package/src/lib/app-utils/media-ref.js +324 -43
- package/src/lib/app-utils/media-ref.js.map +1 -1
- package/src/lib/app-utils/media-src.d.ts +8 -8
- package/src/lib/app-utils/media-src.js +27 -11
- package/src/lib/app-utils/media-src.js.map +1 -1
- package/src/lib/app-utils/media-takedown-reach.d.ts +4 -1
- package/src/lib/app-utils/media-takedown-reach.js +6 -3
- package/src/lib/app-utils/media-takedown-reach.js.map +1 -1
- package/src/lib/app-utils/notifications.d.ts +1 -1
- package/src/lib/app-utils/notifications.js +2 -0
- package/src/lib/app-utils/notifications.js.map +1 -1
- package/src/lib/app-utils/operator-alerts.js +17 -0
- package/src/lib/app-utils/operator-alerts.js.map +1 -1
- package/src/lib/app-utils/organizations.d.ts +51 -2
- package/src/lib/app-utils/organizations.js +56 -3
- package/src/lib/app-utils/organizations.js.map +1 -1
- package/src/lib/app-utils/page-record-sources.d.ts +87 -0
- package/src/lib/app-utils/page-record-sources.js +53 -0
- package/src/lib/app-utils/page-record-sources.js.map +1 -0
- package/src/lib/app-utils/paid-media-source.d.ts +3 -7
- package/src/lib/app-utils/paid-media-source.js +5 -19
- package/src/lib/app-utils/paid-media-source.js.map +1 -1
- package/src/lib/app-utils/person-erasure.d.ts +5 -2
- package/src/lib/app-utils/person-erasure.js.map +1 -1
- package/src/lib/app-utils/picklists.d.ts +187 -0
- package/src/lib/app-utils/picklists.js +320 -0
- package/src/lib/app-utils/picklists.js.map +1 -0
- package/src/lib/app-utils/plan-entitlements.d.ts +220 -50
- package/src/lib/app-utils/plan-entitlements.js +309 -115
- package/src/lib/app-utils/plan-entitlements.js.map +1 -1
- 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 +1 -1
- package/src/lib/app-utils/release-flags.js +11 -0
- package/src/lib/app-utils/release-flags.js.map +1 -1
- package/src/lib/app-utils/rendered-widths.d.ts +121 -0
- package/src/lib/app-utils/rendered-widths.js +211 -0
- package/src/lib/app-utils/rendered-widths.js.map +1 -0
- package/src/lib/app-utils/repeat-sources.d.ts +2 -1
- package/src/lib/app-utils/repeat-sources.js.map +1 -1
- package/src/lib/app-utils/screen-link-context-value.d.ts +7 -0
- package/src/lib/app-utils/screen-link-context-value.js.map +1 -1
- package/src/lib/app-utils/screen-link-context.js +14 -2
- package/src/lib/app-utils/screen-link-context.js.map +1 -1
- package/src/lib/app-utils/screen-route.d.ts +157 -36
- package/src/lib/app-utils/screen-route.js +191 -41
- package/src/lib/app-utils/screen-route.js.map +1 -1
- package/src/lib/app-utils/seo-audit.d.ts +11 -1
- package/src/lib/app-utils/seo-audit.js +33 -13
- package/src/lib/app-utils/seo-audit.js.map +1 -1
- package/src/lib/app-utils/seo-keywords.d.ts +11 -0
- package/src/lib/app-utils/seo-keywords.js +19 -5
- package/src/lib/app-utils/seo-keywords.js.map +1 -1
- package/src/lib/app-utils/seo-page-facts.d.ts +47 -4
- package/src/lib/app-utils/seo-page-facts.js +136 -31
- package/src/lib/app-utils/seo-page-facts.js.map +1 -1
- package/src/lib/app-utils/seo-site-scan.d.ts +42 -4
- package/src/lib/app-utils/seo-site-scan.js +113 -12
- package/src/lib/app-utils/seo-site-scan.js.map +1 -1
- package/src/lib/app-utils/server.d.ts +5 -4
- package/src/lib/app-utils/server.js +14 -5
- package/src/lib/app-utils/server.js.map +1 -1
- package/src/lib/app-utils/site-icon-set.d.ts +195 -0
- package/src/lib/app-utils/site-icon-set.js +351 -0
- package/src/lib/app-utils/site-icon-set.js.map +1 -0
- package/src/lib/app-utils/site-interactions.d.ts +101 -4
- package/src/lib/app-utils/site-interactions.js +120 -4
- package/src/lib/app-utils/site-interactions.js.map +1 -1
- package/src/lib/app-utils/starter-template-nodes.d.ts +7 -0
- package/src/lib/app-utils/starter-template-nodes.js +3 -0
- package/src/lib/app-utils/starter-template-nodes.js.map +1 -1
- package/src/lib/app-utils/starter-templates.d.ts +5 -19
- package/src/lib/app-utils/starter-templates.js +5 -49
- package/src/lib/app-utils/starter-templates.js.map +1 -1
- package/src/lib/app-utils/transfer-launcher-context.d.ts +136 -0
- package/src/lib/app-utils/transfer-launcher-context.js +23 -0
- package/src/lib/app-utils/transfer-launcher-context.js.map +1 -0
- package/src/lib/app-utils/transfer-resume-import.d.ts +39 -0
- package/src/lib/app-utils/transfer-resume-import.js +128 -0
- package/src/lib/app-utils/transfer-resume-import.js.map +1 -0
- package/src/lib/app-utils/upgrade-proposal.d.ts +66 -0
- package/src/lib/app-utils/upgrade-proposal.js +95 -0
- package/src/lib/app-utils/upgrade-proposal.js.map +1 -0
- package/src/lib/app-utils/utc-month.d.ts +33 -0
- package/src/lib/app-utils/utc-month.js +35 -0
- package/src/lib/app-utils/utc-month.js.map +1 -0
- package/src/lib/app-utils/visitor-record-ceiling.d.ts +4 -4
- package/src/lib/app-utils/visitor-record-ceiling.js +4 -4
- package/src/lib/app-utils/visitor-record-ceiling.js.map +1 -1
- package/src/lib/app-utils/wire-nodes.d.ts +39 -19
- package/src/lib/app-utils/wire-nodes.js +120 -34
- package/src/lib/app-utils/wire-nodes.js.map +1 -1
- package/src/lib/data-transfer/access.d.ts +74 -0
- package/src/lib/data-transfer/access.js +93 -0
- package/src/lib/data-transfer/access.js.map +1 -0
- package/src/lib/data-transfer/derive.d.ts +246 -0
- package/src/lib/data-transfer/derive.js +1007 -0
- package/src/lib/data-transfer/derive.js.map +1 -0
- package/src/lib/data-transfer/export-file.d.ts +46 -0
- package/src/lib/data-transfer/export-file.js +146 -0
- package/src/lib/data-transfer/export-file.js.map +1 -0
- package/src/lib/data-transfer/field-catalog.d.ts +134 -0
- package/src/lib/data-transfer/field-catalog.js +278 -0
- package/src/lib/data-transfer/field-catalog.js.map +1 -0
- package/src/lib/data-transfer/header-match.d.ts +129 -0
- package/src/lib/data-transfer/header-match.js +367 -0
- package/src/lib/data-transfer/header-match.js.map +1 -0
- package/src/lib/data-transfer/index.d.ts +34 -0
- package/src/lib/data-transfer/index.js +40 -0
- package/src/lib/data-transfer/index.js.map +1 -0
- package/src/lib/data-transfer/job.d.ts +190 -0
- package/src/lib/data-transfer/job.js +229 -0
- package/src/lib/data-transfer/job.js.map +1 -0
- package/src/lib/data-transfer/lookup.d.ts +45 -0
- package/src/lib/data-transfer/lookup.js +71 -0
- package/src/lib/data-transfer/lookup.js.map +1 -0
- package/src/lib/data-transfer/match.d.ts +99 -0
- package/src/lib/data-transfer/match.js +244 -0
- package/src/lib/data-transfer/match.js.map +1 -0
- package/src/lib/data-transfer/package-plan.d.ts +205 -0
- package/src/lib/data-transfer/package-plan.js +475 -0
- package/src/lib/data-transfer/package-plan.js.map +1 -0
- package/src/lib/data-transfer/package.d.ts +181 -0
- package/src/lib/data-transfer/package.js +426 -0
- package/src/lib/data-transfer/package.js.map +1 -0
- package/src/lib/data-transfer/picklist-map.d.ts +139 -0
- package/src/lib/data-transfer/picklist-map.js +271 -0
- package/src/lib/data-transfer/picklist-map.js.map +1 -0
- package/src/lib/data-transfer/plan.d.ts +176 -0
- package/src/lib/data-transfer/plan.js +451 -0
- package/src/lib/data-transfer/plan.js.map +1 -0
- package/src/lib/data-transfer/policy.d.ts +170 -0
- package/src/lib/data-transfer/policy.js +378 -0
- package/src/lib/data-transfer/policy.js.map +1 -0
- package/src/lib/data-transfer/resource.d.ts +211 -0
- package/src/lib/data-transfer/resource.js +241 -0
- package/src/lib/data-transfer/resource.js.map +1 -0
- package/src/lib/data-transfer/review.d.ts +70 -0
- package/src/lib/data-transfer/review.js +233 -0
- package/src/lib/data-transfer/review.js.map +1 -0
- package/src/lib/data-transfer/similarity.d.ts +40 -0
- package/src/lib/data-transfer/similarity.js +106 -0
- package/src/lib/data-transfer/similarity.js.map +1 -0
- package/src/lib/data-transfer/site-package.d.ts +365 -0
- package/src/lib/data-transfer/site-package.js +1077 -0
- package/src/lib/data-transfer/site-package.js.map +1 -0
- package/src/lib/data-transfer/source.d.ts +74 -0
- package/src/lib/data-transfer/source.js +293 -0
- package/src/lib/data-transfer/source.js.map +1 -0
- package/src/lib/data-transfer/transfer-api.d.ts +898 -0
- package/src/lib/data-transfer/transfer-api.js +107 -0
- package/src/lib/data-transfer/transfer-api.js.map +1 -0
- package/src/lib/foundation/definitions/org-billing.types.d.ts +31 -0
- package/src/lib/foundation/definitions/org-billing.types.js.map +1 -1
- package/src/lib/foundation/definitions/organization.types.d.ts +42 -1
- package/src/lib/foundation/definitions/organization.types.js.map +1 -1
- package/src/lib/foundation/definitions/platform.types.d.ts +6 -4
- package/src/lib/foundation/definitions/platform.types.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/feature-plugins.d.ts +91 -2
- package/src/lib/plugin-manager/feature-plugins.js +48 -3
- package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
- package/src/lib/plugin-manager/first-party-plugins.generated.d.ts +56 -0
- package/src/lib/plugin-manager/first-party-plugins.generated.js +1132 -27
- package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
- package/src/lib/plugin-manager/index.d.ts +0 -1
- package/src/lib/plugin-manager/index.js +0 -1
- package/src/lib/plugin-manager/index.js.map +1 -1
- package/src/lib/plugin-manager/interaction-recipes.d.ts +142 -0
- package/src/lib/plugin-manager/interaction-recipes.js +104 -0
- package/src/lib/plugin-manager/interaction-recipes.js.map +1 -0
- package/src/lib/plugin-manager/interaction-step-checks.d.ts +66 -0
- package/src/lib/plugin-manager/interaction-step-checks.js +86 -0
- package/src/lib/plugin-manager/interaction-step-checks.js.map +1 -0
- package/src/lib/plugin-manager/plugin-artifact-types.d.ts +235 -0
- package/src/lib/plugin-manager/plugin-artifact-types.js +99 -0
- package/src/lib/plugin-manager/plugin-artifact-types.js.map +1 -0
- package/src/lib/plugin-manager/plugin-contact-capture.d.ts +8 -0
- package/src/lib/plugin-manager/plugin-contact-capture.js.map +1 -1
- package/src/lib/plugin-manager/plugin-conversion-credit.d.ts +216 -0
- package/src/lib/plugin-manager/plugin-conversion-credit.js +91 -0
- package/src/lib/plugin-manager/plugin-conversion-credit.js.map +1 -0
- package/src/lib/plugin-manager/plugin-entity-pickers.d.ts +83 -0
- package/src/lib/plugin-manager/plugin-entity-pickers.js +87 -0
- package/src/lib/plugin-manager/plugin-entity-pickers.js.map +1 -0
- package/src/lib/plugin-manager/plugin-host-resources.d.ts +6 -0
- package/src/lib/plugin-manager/plugin-host-resources.js +6 -1
- package/src/lib/plugin-manager/plugin-host-resources.js.map +1 -1
- package/src/lib/plugin-manager/plugin-intake-gates.d.ts +75 -0
- package/src/lib/plugin-manager/plugin-intake-gates.js +49 -0
- package/src/lib/plugin-manager/plugin-intake-gates.js.map +1 -0
- package/src/lib/plugin-manager/plugin-jobs.d.ts +5 -5
- package/src/lib/plugin-manager/plugin-jobs.js +7 -7
- package/src/lib/plugin-manager/plugin-jobs.js.map +1 -1
- package/src/lib/plugin-manager/plugin-live-paths.d.ts +70 -0
- package/src/lib/plugin-manager/plugin-live-paths.js +66 -0
- package/src/lib/plugin-manager/plugin-live-paths.js.map +1 -0
- package/src/lib/plugin-manager/plugin-org-erasure.d.ts +3 -2
- package/src/lib/plugin-manager/plugin-org-erasure.js +30 -5
- package/src/lib/plugin-manager/plugin-org-erasure.js.map +1 -1
- package/src/lib/plugin-manager/plugin-person-erasure.d.ts +68 -9
- package/src/lib/plugin-manager/plugin-person-erasure.js +196 -51
- package/src/lib/plugin-manager/plugin-person-erasure.js.map +1 -1
- package/src/lib/plugin-manager/plugin-person-records.d.ts +275 -0
- package/src/lib/plugin-manager/plugin-person-records.js +107 -0
- package/src/lib/plugin-manager/plugin-person-records.js.map +1 -0
- package/src/lib/plugin-manager/plugin-record-counts.d.ts +76 -0
- package/src/lib/plugin-manager/plugin-record-counts.js +49 -0
- package/src/lib/plugin-manager/plugin-record-counts.js.map +1 -0
- package/src/lib/plugin-manager/plugin-record-email-state.d.ts +26 -0
- package/src/lib/plugin-manager/plugin-record-email-state.js +16 -0
- package/src/lib/plugin-manager/plugin-record-email-state.js.map +1 -1
- package/src/lib/plugin-manager/plugin-record-index.d.ts +10 -0
- package/src/lib/plugin-manager/plugin-record-index.js.map +1 -1
- package/src/lib/plugin-manager/plugin-record-lists.d.ts +86 -6
- package/src/lib/plugin-manager/plugin-record-lists.js +42 -5
- package/src/lib/plugin-manager/plugin-record-lists.js.map +1 -1
- package/src/lib/plugin-manager/plugin-record-origin.d.ts +79 -0
- package/src/lib/plugin-manager/plugin-record-origin.js +46 -0
- package/src/lib/plugin-manager/plugin-record-origin.js.map +1 -0
- package/src/lib/plugin-manager/plugin-record-pages.d.ts +37 -0
- package/src/lib/plugin-manager/plugin-record-pages.js +52 -0
- package/src/lib/plugin-manager/plugin-record-pages.js.map +1 -0
- package/src/lib/plugin-manager/plugin-record-timeline.d.ts +60 -0
- package/src/lib/plugin-manager/plugin-record-timeline.js +16 -0
- package/src/lib/plugin-manager/plugin-record-timeline.js.map +1 -1
- package/src/lib/plugin-manager/plugin-resource-drafts.d.ts +5 -5
- package/src/lib/plugin-manager/plugin-resource-drafts.js.map +1 -1
- package/src/lib/plugin-manager/plugin-revocations.d.ts +64 -0
- package/src/lib/plugin-manager/plugin-revocations.js +47 -0
- package/src/lib/plugin-manager/plugin-revocations.js.map +1 -0
- package/src/lib/plugin-manager/plugin-send-tallies.d.ts +69 -0
- package/src/lib/plugin-manager/plugin-send-tallies.js +45 -0
- package/src/lib/plugin-manager/plugin-send-tallies.js.map +1 -0
- package/src/lib/plugin-manager/plugin-server-steps.d.ts +170 -0
- package/src/lib/plugin-manager/plugin-server-steps.js +116 -0
- package/src/lib/plugin-manager/plugin-server-steps.js.map +1 -0
- package/src/lib/plugin-manager/plugin-site-bundle.d.ts +40 -0
- package/src/lib/plugin-manager/plugin-site-bundle.js +4 -0
- package/src/lib/plugin-manager/plugin-site-bundle.js.map +1 -1
- package/src/lib/plugin-manager/plugin-site-export.d.ts +57 -1
- package/src/lib/plugin-manager/plugin-site-export.js +23 -10
- package/src/lib/plugin-manager/plugin-site-export.js.map +1 -1
- package/src/lib/plugin-manager/plugin-sitemap-readers.d.ts +146 -0
- package/src/lib/plugin-manager/plugin-sitemap-readers.js +110 -0
- package/src/lib/plugin-manager/plugin-sitemap-readers.js.map +1 -0
- package/src/lib/plugin-manager/plugin-transfer-resources.d.ts +714 -0
- package/src/lib/plugin-manager/plugin-transfer-resources.js +443 -0
- package/src/lib/plugin-manager/plugin-transfer-resources.js.map +1 -0
- package/src/lib/plugin-manager/plugin-usage-axes.d.ts +54 -5
- package/src/lib/plugin-manager/plugin-usage-axes.js +11 -3
- package/src/lib/plugin-manager/plugin-usage-axes.js.map +1 -1
- package/src/lib/plugin-manager/plugin-user-erasure.js +9 -1
- package/src/lib/plugin-manager/plugin-user-erasure.js.map +1 -1
- package/src/lib/plugin-manager/plugin-visitor-doors.d.ts +104 -0
- package/src/lib/plugin-manager/plugin-visitor-doors.js +108 -0
- package/src/lib/plugin-manager/plugin-visitor-doors.js.map +1 -0
- 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/repeat-rows.d.ts +5 -6
- package/src/lib/plugin-manager/repeat-rows.js.map +1 -1
- package/src/lib/app-utils/actions.d.ts +0 -310
- package/src/lib/app-utils/actions.js +0 -506
- package/src/lib/app-utils/actions.js.map +0 -1
- package/src/lib/app-utils/automation-placeholders.d.ts +0 -72
- package/src/lib/app-utils/automation-placeholders.js +0 -207
- package/src/lib/app-utils/automation-placeholders.js.map +0 -1
- package/src/lib/app-utils/dataset-models.d.ts +0 -339
- package/src/lib/app-utils/dataset-models.js +0 -722
- package/src/lib/app-utils/dataset-models.js.map +0 -1
- package/src/lib/app-utils/datasets.d.ts +0 -171
- package/src/lib/app-utils/datasets.js +0 -227
- package/src/lib/app-utils/datasets.js.map +0 -1
- package/src/lib/app-utils/form-abuse-ceiling.d.ts +0 -146
- package/src/lib/app-utils/form-abuse-ceiling.js +0 -158
- package/src/lib/app-utils/form-abuse-ceiling.js.map +0 -1
- package/src/lib/plugin-manager/order-fulfilment.d.ts +0 -142
- package/src/lib/plugin-manager/order-fulfilment.js +0 -104
- package/src/lib/plugin-manager/order-fulfilment.js.map +0 -1
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import type { PageRecordScope } from './expand-repeatables';
|
|
18
|
+
/**
|
|
19
|
+
* WHICH RECORD A PAGE IS DRAWN FOR, ON THE BESIGNER'S CANVAS (AGL-3475).
|
|
20
|
+
*
|
|
21
|
+
* A record template renders once per record on the published site: the
|
|
22
|
+
* composition hands it the routed record (`PageRecordScope`) and its
|
|
23
|
+
* `{{item.*}}` tokens fill in. The canvas has no routed record — it edits the
|
|
24
|
+
* template — so without one it shows the raw tokens, and the author designs a
|
|
25
|
+
* page of `{{item.name}}` placeholders.
|
|
26
|
+
*
|
|
27
|
+
* So a plugin that serves pages per record answers here, for the page in the
|
|
28
|
+
* editor: whether it is a template of theirs, and if so a record to draw it
|
|
29
|
+
* for, with the others an author may switch to. The canvas lays that record
|
|
30
|
+
* over every element's render copy exactly as a repeat lays its first record
|
|
31
|
+
* over its template (AGL-3111) — the nodes keep their tokens, and a save
|
|
32
|
+
* writes the template.
|
|
33
|
+
*
|
|
34
|
+
* The editor's side only: a hook, mounted by the console beside the canvas,
|
|
35
|
+
* because the designer reads no database. `repeat-sources.ts` is the same
|
|
36
|
+
* shape for a repeat's rows.
|
|
37
|
+
*/
|
|
38
|
+
/** One record an author can preview the page with. */
|
|
39
|
+
export interface PageRecordChoice {
|
|
40
|
+
/** The record's id. */
|
|
41
|
+
id: string;
|
|
42
|
+
/** What the picker shows for it. */
|
|
43
|
+
label: string;
|
|
44
|
+
}
|
|
45
|
+
/** What a source answers for one page. */
|
|
46
|
+
export type PageRecordAnswer =
|
|
47
|
+
/** Not a page this source serves records on. */
|
|
48
|
+
{
|
|
49
|
+
status: 'none';
|
|
50
|
+
} | {
|
|
51
|
+
status: 'loading';
|
|
52
|
+
} | (PageRecordScope & {
|
|
53
|
+
status: 'ready';
|
|
54
|
+
/** What the page is a template of: "Services". */
|
|
55
|
+
label: string;
|
|
56
|
+
/** The record being drawn. */
|
|
57
|
+
selectedId: string;
|
|
58
|
+
/** Every record the picker offers, the drawn one included. */
|
|
59
|
+
choices: readonly PageRecordChoice[];
|
|
60
|
+
/** Draws the page for another of the choices. */
|
|
61
|
+
select: (id: string) => void;
|
|
62
|
+
});
|
|
63
|
+
/** What a source is asked. */
|
|
64
|
+
export interface PageRecordRequest {
|
|
65
|
+
hostId: string;
|
|
66
|
+
screenId: string;
|
|
67
|
+
}
|
|
68
|
+
export interface PageRecordSource {
|
|
69
|
+
/** Stable id; one source per id. */
|
|
70
|
+
id: string;
|
|
71
|
+
/**
|
|
72
|
+
* A React hook, called once per source for the page in the editor, every
|
|
73
|
+
* render. Answers `none` for a page it does not serve.
|
|
74
|
+
*/
|
|
75
|
+
usePageRecord: (request: PageRecordRequest) => PageRecordAnswer;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Registers a source, replacing any under its id. Returns the unregister,
|
|
79
|
+
* which removes it only while it is still the one registered. Call it from a
|
|
80
|
+
* registration function the plugin loader calls by name, never from a
|
|
81
|
+
* module's top level (AGL-3025).
|
|
82
|
+
*/
|
|
83
|
+
export declare function registerPageRecordSource(source: PageRecordSource): () => void;
|
|
84
|
+
/** Every registered source, the SAME array until the set changes. */
|
|
85
|
+
export declare function listPageRecordSources(): readonly PageRecordSource[];
|
|
86
|
+
/** Called after the registered set changes. Returns the unsubscribe. */
|
|
87
|
+
export declare function subscribePageRecordSources(listener: () => void): () => void;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ const sources = new Map();
|
|
17
|
+
const listeners = new Set();
|
|
18
|
+
/** Replaced only when the set changes: the snapshot a store compares. */ let snapshot = [];
|
|
19
|
+
const notify = ()=>{
|
|
20
|
+
snapshot = [
|
|
21
|
+
...sources.values()
|
|
22
|
+
];
|
|
23
|
+
for (const listener of [
|
|
24
|
+
...listeners
|
|
25
|
+
])listener();
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Registers a source, replacing any under its id. Returns the unregister,
|
|
29
|
+
* which removes it only while it is still the one registered. Call it from a
|
|
30
|
+
* registration function the plugin loader calls by name, never from a
|
|
31
|
+
* module's top level (AGL-3025).
|
|
32
|
+
*/ export function registerPageRecordSource(source) {
|
|
33
|
+
if (sources.get(source.id) !== source) {
|
|
34
|
+
sources.set(source.id, source);
|
|
35
|
+
notify();
|
|
36
|
+
}
|
|
37
|
+
return ()=>{
|
|
38
|
+
if (sources.get(source.id) !== source) return;
|
|
39
|
+
sources.delete(source.id);
|
|
40
|
+
notify();
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/** Every registered source, the SAME array until the set changes. */ export function listPageRecordSources() {
|
|
44
|
+
return snapshot;
|
|
45
|
+
}
|
|
46
|
+
/** Called after the registered set changes. Returns the unsubscribe. */ export function subscribePageRecordSources(listener) {
|
|
47
|
+
listeners.add(listener);
|
|
48
|
+
return ()=>{
|
|
49
|
+
listeners.delete(listener);
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
//# sourceMappingURL=page-record-sources.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/page-record-sources.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { PageRecordScope } from './expand-repeatables'\n\n/**\n * WHICH RECORD A PAGE IS DRAWN FOR, ON THE BESIGNER'S CANVAS (AGL-3475).\n *\n * A record template renders once per record on the published site: the\n * composition hands it the routed record (`PageRecordScope`) and its\n * `{{item.*}}` tokens fill in. The canvas has no routed record — it edits the\n * template — so without one it shows the raw tokens, and the author designs a\n * page of `{{item.name}}` placeholders.\n *\n * So a plugin that serves pages per record answers here, for the page in the\n * editor: whether it is a template of theirs, and if so a record to draw it\n * for, with the others an author may switch to. The canvas lays that record\n * over every element's render copy exactly as a repeat lays its first record\n * over its template (AGL-3111) — the nodes keep their tokens, and a save\n * writes the template.\n *\n * The editor's side only: a hook, mounted by the console beside the canvas,\n * because the designer reads no database. `repeat-sources.ts` is the same\n * shape for a repeat's rows.\n */\n\n/** One record an author can preview the page with. */\nexport interface PageRecordChoice {\n /** The record's id. */\n id: string\n /** What the picker shows for it. */\n label: string\n}\n\n/** What a source answers for one page. */\nexport type PageRecordAnswer =\n /** Not a page this source serves records on. */\n | { status: 'none' }\n | { status: 'loading' }\n | (PageRecordScope & {\n status: 'ready'\n /** What the page is a template of: \"Services\". */\n label: string\n /** The record being drawn. */\n selectedId: string\n /** Every record the picker offers, the drawn one included. */\n choices: readonly PageRecordChoice[]\n /** Draws the page for another of the choices. */\n select: (id: string) => void\n })\n\n/** What a source is asked. */\nexport interface PageRecordRequest {\n hostId: string\n screenId: string\n}\n\nexport interface PageRecordSource {\n /** Stable id; one source per id. */\n id: string\n /**\n * A React hook, called once per source for the page in the editor, every\n * render. Answers `none` for a page it does not serve.\n */\n usePageRecord: (request: PageRecordRequest) => PageRecordAnswer\n}\n\nconst sources = new Map<string, PageRecordSource>()\nconst listeners = new Set<() => void>()\n/** Replaced only when the set changes: the snapshot a store compares. */\nlet snapshot: readonly PageRecordSource[] = []\n\nconst notify = () => {\n snapshot = [...sources.values()]\n for (const listener of [...listeners]) listener()\n}\n\n/**\n * Registers a source, replacing any under its id. Returns the unregister,\n * which removes it only while it is still the one registered. Call it from a\n * registration function the plugin loader calls by name, never from a\n * module's top level (AGL-3025).\n */\nexport function registerPageRecordSource(source: PageRecordSource): () => void {\n if (sources.get(source.id) !== source) {\n sources.set(source.id, source)\n notify()\n }\n return () => {\n if (sources.get(source.id) !== source) return\n sources.delete(source.id)\n notify()\n }\n}\n\n/** Every registered source, the SAME array until the set changes. */\nexport function listPageRecordSources(): readonly PageRecordSource[] {\n return snapshot\n}\n\n/** Called after the registered set changes. Returns the unsubscribe. */\nexport function subscribePageRecordSources(listener: () => void): () => void {\n listeners.add(listener)\n return () => {\n listeners.delete(listener)\n }\n}\n"],"names":["sources","Map","listeners","Set","snapshot","notify","values","listener","registerPageRecordSource","source","get","id","set","delete","listPageRecordSources","subscribePageRecordSources","add"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAkED,MAAMA,UAAU,IAAIC;AACpB,MAAMC,YAAY,IAAIC;AACtB,uEAAuE,GACvE,IAAIC,WAAwC,EAAE;AAE9C,MAAMC,SAAS;IACbD,WAAW;WAAIJ,QAAQM,MAAM;KAAG;IAChC,KAAK,MAAMC,YAAY;WAAIL;KAAU,CAAEK;AACzC;AAEA;;;;;CAKC,GACD,OAAO,SAASC,yBAAyBC,MAAwB;IAC/D,IAAIT,QAAQU,GAAG,CAACD,OAAOE,EAAE,MAAMF,QAAQ;QACrCT,QAAQY,GAAG,CAACH,OAAOE,EAAE,EAAEF;QACvBJ;IACF;IACA,OAAO;QACL,IAAIL,QAAQU,GAAG,CAACD,OAAOE,EAAE,MAAMF,QAAQ;QACvCT,QAAQa,MAAM,CAACJ,OAAOE,EAAE;QACxBN;IACF;AACF;AAEA,mEAAmE,GACnE,OAAO,SAASS;IACd,OAAOV;AACT;AAEA,sEAAsE,GACtE,OAAO,SAASW,2BAA2BR,QAAoB;IAC7DL,UAAUc,GAAG,CAACT;IACd,OAAO;QACLL,UAAUW,MAAM,CAACN;IACnB;AACF"}
|
|
@@ -56,13 +56,9 @@ export type PaidMediaSource =
|
|
|
56
56
|
*/
|
|
57
57
|
export declare function parsePaidMediaSource(stored: unknown): PaidMediaSource;
|
|
58
58
|
/**
|
|
59
|
-
* The library asset a media object key belongs to, or null.
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
* with `base` either `hosts/{hostId}` or `orgs/{orgId}`. So a key in that
|
|
63
|
-
* shape whose last segment is id-shaped IS that asset, wherever a folder move
|
|
64
|
-
* has since put it. Derived objects (`…__r720p.mp4`, `…__poster.webp`) carry
|
|
65
|
-
* a dot and are no asset's key.
|
|
59
|
+
* The library asset a media object key belongs to, or null. The grammar is
|
|
60
|
+
* {@link mediaAssetOfObjectKey}'s, which the render-time resolver shares
|
|
61
|
+
* (AGL-3506); this name stays for the paid-media callers.
|
|
66
62
|
*
|
|
67
63
|
* This names the asset and decides nothing about access: a caller that signs
|
|
68
64
|
* still has to check the bucket and that the library is its own.
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
|
-
*/ import { isFirstPartyHost, MEDIA_CDN_ROUTE, MEDIA_REF_PREFIX, parseMediaRef } from "./media-ref.js";
|
|
16
|
+
*/ import { isFirstPartyHost, MEDIA_CDN_ROUTE, mediaAssetOfObjectKey, MEDIA_REF_PREFIX, parseMediaRef } from "./media-ref.js";
|
|
17
17
|
const STORAGE_DOWNLOAD_HOST = 'firebasestorage.googleapis.com';
|
|
18
18
|
const GCS_HOST = 'storage.googleapis.com';
|
|
19
19
|
/**
|
|
@@ -131,28 +131,14 @@ const GCS_HOST = 'storage.googleapis.com';
|
|
|
131
131
|
};
|
|
132
132
|
}
|
|
133
133
|
/**
|
|
134
|
-
* The library asset a media object key belongs to, or null.
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
* with `base` either `hosts/{hostId}` or `orgs/{orgId}`. So a key in that
|
|
138
|
-
* shape whose last segment is id-shaped IS that asset, wherever a folder move
|
|
139
|
-
* has since put it. Derived objects (`…__r720p.mp4`, `…__poster.webp`) carry
|
|
140
|
-
* a dot and are no asset's key.
|
|
134
|
+
* The library asset a media object key belongs to, or null. The grammar is
|
|
135
|
+
* {@link mediaAssetOfObjectKey}'s, which the render-time resolver shares
|
|
136
|
+
* (AGL-3506); this name stays for the paid-media callers.
|
|
141
137
|
*
|
|
142
138
|
* This names the asset and decides nothing about access: a caller that signs
|
|
143
139
|
* still has to check the bucket and that the library is its own.
|
|
144
140
|
*/ export function paidMediaAssetOfObject(objectPath) {
|
|
145
|
-
|
|
146
|
-
if (segments.length < 4 || segments[2] !== 'media') return null;
|
|
147
|
-
if (segments.some((segment)=>!segment || segment === '..')) return null;
|
|
148
|
-
const [root, scopeId] = segments;
|
|
149
|
-
const scope = root === 'hosts' ? scopeId : root === 'orgs' ? `org:${scopeId}` : null;
|
|
150
|
-
if (!scope) return null;
|
|
151
|
-
const ref = parseMediaRef(`${MEDIA_REF_PREFIX}${scope}/${segments[segments.length - 1]}`);
|
|
152
|
-
return ref ? {
|
|
153
|
-
scope: ref.scope,
|
|
154
|
-
mediaId: ref.mediaId
|
|
155
|
-
} : null;
|
|
141
|
+
return mediaAssetOfObjectKey(objectPath);
|
|
156
142
|
}
|
|
157
143
|
/**
|
|
158
144
|
* The library asset a stored paid-media URL names, in whichever form it was
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/paid-media-source.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n isFirstPartyHost,\n MEDIA_CDN_ROUTE,\n MEDIA_REF_PREFIX,\n parseMediaRef,\n} from './media-ref'\n\n/**\n * What a URL stored on a product's paid media names (AGL-2814).\n *\n * A members video is stored as whatever wrote it: the paid-media picker's\n * reference, an older picker's CDN path, the raw Storage download URL a\n * free-tier library hands out, or a hotlink typed into an import. Three\n * places have to agree on which ASSET that is: the stream route that signs\n * it, the console that shows the author whether it is protected, and the\n * media route that refuses to publish a file a product still sells. One\n * grammar, here, so they cannot disagree.\n *\n * Pure and environment-light: nothing here reads Firestore, Storage or a\n * secret, so the console bundle can carry it.\n */\nexport type PaidMediaSource =\n /** A media-library asset, named by scope segment and id. */\n | { kind: 'asset'; scope: string; mediaId: string }\n /** An object in a Storage bucket, from a download or GCS URL. */\n | { kind: 'storage-object'; bucket: string; objectPath: string }\n /** Somebody else's server. */\n | { kind: 'external'; url: string }\n /** Nothing that can be delivered, including a Storage URL we cannot read. */\n | { kind: 'malformed' }\n\nconst STORAGE_DOWNLOAD_HOST = 'firebasestorage.googleapis.com'\nconst GCS_HOST = 'storage.googleapis.com'\n\n/**\n * The Storage emulator's `host:port`, whose download URLs have the same shape\n * as production's. Read per call so a test can set it.\n */\nfunction storageEmulatorHost(): string {\n const env =\n typeof process === 'undefined' ? undefined : process.env?.['FIREBASE_STORAGE_EMULATOR_HOST']\n return String(env ?? '')\n .trim()\n .toLowerCase()\n}\n\n/** A library asset from `/api/media/cdn/{scope}/{mediaId}[/{hash}]`. */\nfunction assetFromCdnPath(path: string): PaidMediaSource {\n const rest = path.slice(`${MEDIA_CDN_ROUTE}/`.length).split(/[?#]/)[0]\n const segments = rest.split('/')\n if (segments.length < 2 || segments.length > 3) return { kind: 'malformed' }\n let scope: string\n try {\n scope = decodeURIComponent(segments[0])\n } catch {\n return { kind: 'malformed' }\n }\n // Parsed through the reference grammar rather than a second copy of it. A\n // content pin in the path is dropped: a paid link names the asset, and the\n // stable URL always serves its current bytes.\n const ref = parseMediaRef(`${MEDIA_REF_PREFIX}${scope}/${segments[1]}`)\n return ref\n ? { kind: 'asset', scope: ref.scope, mediaId: ref.mediaId }\n : { kind: 'malformed' }\n}\n\n/** Decodes each `/`-separated piece of a path; null when one is malformed. */\nfunction decodePath(pieces: string[]): string | null {\n try {\n return pieces.map((piece) => decodeURIComponent(piece)).join('/')\n } catch {\n return null\n }\n}\n\n/**\n * Classifies a stored value. The same input always gives the same answer, so\n * every branch is checkable without a project.\n */\nexport function parsePaidMediaSource(stored: unknown): PaidMediaSource {\n const value = typeof stored === 'string' ? stored.trim() : ''\n if (!value) return { kind: 'malformed' }\n if (value.startsWith(MEDIA_REF_PREFIX)) {\n const ref = parseMediaRef(value)\n return ref\n ? { kind: 'asset', scope: ref.scope, mediaId: ref.mediaId }\n : { kind: 'malformed' }\n }\n if (value.startsWith(`${MEDIA_CDN_ROUTE}/`)) return assetFromCdnPath(value)\n if (!/^https?:\\/\\//i.test(value)) return { kind: 'malformed' }\n let url: URL\n try {\n url = new URL(value)\n } catch {\n return { kind: 'malformed' }\n }\n const hostname = url.hostname.toLowerCase()\n const emulator = storageEmulatorHost()\n if (\n hostname === STORAGE_DOWNLOAD_HOST ||\n (emulator && url.host.toLowerCase() === emulator)\n ) {\n // `/v0/b/{bucket}/o/{object}`, with the object key percent-encoded as one\n // segment. A download URL in any other shape is still a token URL, so it\n // is refused rather than handed through as a hotlink.\n const match = /^\\/v0\\/b\\/([^/]+)\\/o\\/([^/]+)$/.exec(url.pathname)\n const bucket = match ? decodePath([match[1]]) : null\n const objectPath = match ? decodePath([match[2]]) : null\n return bucket && objectPath\n ? { kind: 'storage-object', bucket, objectPath }\n : { kind: 'malformed' }\n }\n if (hostname === GCS_HOST) {\n const [, bucketPiece, ...objectPieces] = url.pathname.split('/')\n const bucket = bucketPiece ? decodePath([bucketPiece]) : null\n const objectPath = objectPieces.length ? decodePath(objectPieces) : null\n return bucket && objectPath\n ? { kind: 'storage-object', bucket, objectPath }\n : { kind: 'malformed' }\n }\n if (\n isFirstPartyHost(hostname) &&\n url.pathname.startsWith(`${MEDIA_CDN_ROUTE}/`)\n ) {\n return assetFromCdnPath(url.pathname)\n }\n return { kind: 'external', url: value }\n}\n\n/**\n * The library asset a media object key belongs to, or null.\n *\n * Every writer keys an asset's object as `{base}/media/[folders/]{mediaId}`,\n * with `base` either `hosts/{hostId}` or `orgs/{orgId}`. So a key in that\n * shape whose last segment is id-shaped IS that asset, wherever a folder move\n * has since put it. Derived objects (`…__r720p.mp4`, `…__poster.webp`) carry\n * a dot and are no asset's key.\n *\n * This names the asset and decides nothing about access: a caller that signs\n * still has to check the bucket and that the library is its own.\n */\nexport function paidMediaAssetOfObject(\n objectPath: string,\n): { scope: string; mediaId: string } | null {\n const segments = String(objectPath ?? '').split('/')\n if (segments.length < 4 || segments[2] !== 'media') return null\n if (segments.some((segment) => !segment || segment === '..')) return null\n const [root, scopeId] = segments\n const scope =\n root === 'hosts' ? scopeId : root === 'orgs' ? `org:${scopeId}` : null\n if (!scope) return null\n const ref = parseMediaRef(\n `${MEDIA_REF_PREFIX}${scope}/${segments[segments.length - 1]}`,\n )\n return ref ? { scope: ref.scope, mediaId: ref.mediaId } : null\n}\n\n/**\n * The library asset a stored paid-media URL names, in whichever form it was\n * written, or null for a hotlink, a malformed value, or a Storage URL that\n * names no asset. Pass `bucket` to refuse a download URL from any other\n * bucket, which is what every server-side caller should do.\n */\nexport function paidMediaAssetOf(\n stored: unknown,\n options?: { bucket?: string },\n): { scope: string; mediaId: string } | null {\n const source = parsePaidMediaSource(stored)\n if (source.kind === 'asset') {\n return { scope: source.scope, mediaId: source.mediaId }\n }\n if (source.kind !== 'storage-object') return null\n if (options?.bucket !== undefined && source.bucket !== options.bucket) {\n return null\n }\n return paidMediaAssetOfObject(source.objectPath)\n}\n\n/**\n * Whether two scope segments name the same library: the same host, or the\n * same org whatever site either is qualified with.\n */\nexport function samePaidMediaLibrary(a: string, b: string): boolean {\n const library = (scope: string) =>\n scope.startsWith('org:') ? `org:${scope.slice(4).split(':')[0]}` : scope\n return library(a) === library(b)\n}\n"],"names":["isFirstPartyHost","MEDIA_CDN_ROUTE","MEDIA_REF_PREFIX","parseMediaRef","STORAGE_DOWNLOAD_HOST","GCS_HOST","storageEmulatorHost","process","env","undefined","String","trim","toLowerCase","assetFromCdnPath","path","rest","slice","length","split","segments","kind","scope","decodeURIComponent","ref","mediaId","decodePath","pieces","map","piece","join","parsePaidMediaSource","stored","value","startsWith","test","url","URL","hostname","emulator","host","match","exec","pathname","bucket","objectPath","bucketPiece","objectPieces","paidMediaAssetOfObject","some","segment","root","scopeId","paidMediaAssetOf","options","source","samePaidMediaLibrary","a","b","library"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/paid-media-source.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n isFirstPartyHost,\n MEDIA_CDN_ROUTE,\n mediaAssetOfObjectKey,\n MEDIA_REF_PREFIX,\n parseMediaRef,\n} from './media-ref'\n\n/**\n * What a URL stored on a product's paid media names (AGL-2814).\n *\n * A members video is stored as whatever wrote it: the paid-media picker's\n * reference, an older picker's CDN path, the raw Storage download URL a\n * free-tier library hands out, or a hotlink typed into an import. Three\n * places have to agree on which ASSET that is: the stream route that signs\n * it, the console that shows the author whether it is protected, and the\n * media route that refuses to publish a file a product still sells. One\n * grammar, here, so they cannot disagree.\n *\n * Pure and environment-light: nothing here reads Firestore, Storage or a\n * secret, so the console bundle can carry it.\n */\nexport type PaidMediaSource =\n /** A media-library asset, named by scope segment and id. */\n | { kind: 'asset'; scope: string; mediaId: string }\n /** An object in a Storage bucket, from a download or GCS URL. */\n | { kind: 'storage-object'; bucket: string; objectPath: string }\n /** Somebody else's server. */\n | { kind: 'external'; url: string }\n /** Nothing that can be delivered, including a Storage URL we cannot read. */\n | { kind: 'malformed' }\n\nconst STORAGE_DOWNLOAD_HOST = 'firebasestorage.googleapis.com'\nconst GCS_HOST = 'storage.googleapis.com'\n\n/**\n * The Storage emulator's `host:port`, whose download URLs have the same shape\n * as production's. Read per call so a test can set it.\n */\nfunction storageEmulatorHost(): string {\n const env =\n typeof process === 'undefined' ? undefined : process.env?.['FIREBASE_STORAGE_EMULATOR_HOST']\n return String(env ?? '')\n .trim()\n .toLowerCase()\n}\n\n/** A library asset from `/api/media/cdn/{scope}/{mediaId}[/{hash}]`. */\nfunction assetFromCdnPath(path: string): PaidMediaSource {\n const rest = path.slice(`${MEDIA_CDN_ROUTE}/`.length).split(/[?#]/)[0]\n const segments = rest.split('/')\n if (segments.length < 2 || segments.length > 3) return { kind: 'malformed' }\n let scope: string\n try {\n scope = decodeURIComponent(segments[0])\n } catch {\n return { kind: 'malformed' }\n }\n // Parsed through the reference grammar rather than a second copy of it. A\n // content pin in the path is dropped: a paid link names the asset, and the\n // stable URL always serves its current bytes.\n const ref = parseMediaRef(`${MEDIA_REF_PREFIX}${scope}/${segments[1]}`)\n return ref\n ? { kind: 'asset', scope: ref.scope, mediaId: ref.mediaId }\n : { kind: 'malformed' }\n}\n\n/** Decodes each `/`-separated piece of a path; null when one is malformed. */\nfunction decodePath(pieces: string[]): string | null {\n try {\n return pieces.map((piece) => decodeURIComponent(piece)).join('/')\n } catch {\n return null\n }\n}\n\n/**\n * Classifies a stored value. The same input always gives the same answer, so\n * every branch is checkable without a project.\n */\nexport function parsePaidMediaSource(stored: unknown): PaidMediaSource {\n const value = typeof stored === 'string' ? stored.trim() : ''\n if (!value) return { kind: 'malformed' }\n if (value.startsWith(MEDIA_REF_PREFIX)) {\n const ref = parseMediaRef(value)\n return ref\n ? { kind: 'asset', scope: ref.scope, mediaId: ref.mediaId }\n : { kind: 'malformed' }\n }\n if (value.startsWith(`${MEDIA_CDN_ROUTE}/`)) return assetFromCdnPath(value)\n if (!/^https?:\\/\\//i.test(value)) return { kind: 'malformed' }\n let url: URL\n try {\n url = new URL(value)\n } catch {\n return { kind: 'malformed' }\n }\n const hostname = url.hostname.toLowerCase()\n const emulator = storageEmulatorHost()\n if (\n hostname === STORAGE_DOWNLOAD_HOST ||\n (emulator && url.host.toLowerCase() === emulator)\n ) {\n // `/v0/b/{bucket}/o/{object}`, with the object key percent-encoded as one\n // segment. A download URL in any other shape is still a token URL, so it\n // is refused rather than handed through as a hotlink.\n const match = /^\\/v0\\/b\\/([^/]+)\\/o\\/([^/]+)$/.exec(url.pathname)\n const bucket = match ? decodePath([match[1]]) : null\n const objectPath = match ? decodePath([match[2]]) : null\n return bucket && objectPath\n ? { kind: 'storage-object', bucket, objectPath }\n : { kind: 'malformed' }\n }\n if (hostname === GCS_HOST) {\n const [, bucketPiece, ...objectPieces] = url.pathname.split('/')\n const bucket = bucketPiece ? decodePath([bucketPiece]) : null\n const objectPath = objectPieces.length ? decodePath(objectPieces) : null\n return bucket && objectPath\n ? { kind: 'storage-object', bucket, objectPath }\n : { kind: 'malformed' }\n }\n if (\n isFirstPartyHost(hostname) &&\n url.pathname.startsWith(`${MEDIA_CDN_ROUTE}/`)\n ) {\n return assetFromCdnPath(url.pathname)\n }\n return { kind: 'external', url: value }\n}\n\n/**\n * The library asset a media object key belongs to, or null. The grammar is\n * {@link mediaAssetOfObjectKey}'s, which the render-time resolver shares\n * (AGL-3506); this name stays for the paid-media callers.\n *\n * This names the asset and decides nothing about access: a caller that signs\n * still has to check the bucket and that the library is its own.\n */\nexport function paidMediaAssetOfObject(\n objectPath: string,\n): { scope: string; mediaId: string } | null {\n return mediaAssetOfObjectKey(objectPath)\n}\n\n/**\n * The library asset a stored paid-media URL names, in whichever form it was\n * written, or null for a hotlink, a malformed value, or a Storage URL that\n * names no asset. Pass `bucket` to refuse a download URL from any other\n * bucket, which is what every server-side caller should do.\n */\nexport function paidMediaAssetOf(\n stored: unknown,\n options?: { bucket?: string },\n): { scope: string; mediaId: string } | null {\n const source = parsePaidMediaSource(stored)\n if (source.kind === 'asset') {\n return { scope: source.scope, mediaId: source.mediaId }\n }\n if (source.kind !== 'storage-object') return null\n if (options?.bucket !== undefined && source.bucket !== options.bucket) {\n return null\n }\n return paidMediaAssetOfObject(source.objectPath)\n}\n\n/**\n * Whether two scope segments name the same library: the same host, or the\n * same org whatever site either is qualified with.\n */\nexport function samePaidMediaLibrary(a: string, b: string): boolean {\n const library = (scope: string) =>\n scope.startsWith('org:') ? `org:${scope.slice(4).split(':')[0]}` : scope\n return library(a) === library(b)\n}\n"],"names":["isFirstPartyHost","MEDIA_CDN_ROUTE","mediaAssetOfObjectKey","MEDIA_REF_PREFIX","parseMediaRef","STORAGE_DOWNLOAD_HOST","GCS_HOST","storageEmulatorHost","process","env","undefined","String","trim","toLowerCase","assetFromCdnPath","path","rest","slice","length","split","segments","kind","scope","decodeURIComponent","ref","mediaId","decodePath","pieces","map","piece","join","parsePaidMediaSource","stored","value","startsWith","test","url","URL","hostname","emulator","host","match","exec","pathname","bucket","objectPath","bucketPiece","objectPieces","paidMediaAssetOfObject","paidMediaAssetOf","options","source","samePaidMediaLibrary","a","b","library"],"mappings":"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"}
|
|
@@ -71,8 +71,11 @@ export interface PersonErasureRequest {
|
|
|
71
71
|
*/
|
|
72
72
|
pendingSinceMs?: number;
|
|
73
73
|
erasedAtMs?: number;
|
|
74
|
-
/**
|
|
75
|
-
|
|
74
|
+
/**
|
|
75
|
+
* Counts only — what the sweep removed or anonymized, for the audit: the
|
|
76
|
+
* platform's own figures, and each plugin's share under `plugins`.
|
|
77
|
+
*/
|
|
78
|
+
result?: Readonly<Record<string, unknown>>;
|
|
76
79
|
failedAtMs?: number;
|
|
77
80
|
failureCount?: number;
|
|
78
81
|
/** The last failure's message, for the staff reader. Never the data. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/person-erasure.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 * Privacy erasure of one PERSON from one workspace (AGL-2623).\n *\n * The CRM's **Delete contact** is a detach: one site lets go of a shared row\n * and the document dies only when the last holder does. A privacy erasure is\n * the opposite act — the person is removed from every site in the workspace\n * regardless of who holds them, together with everything the workspace keeps\n * beside them — and it is filed as a REQUEST rather than executed on the\n * click, so that the same daily job that executes workspace erasures\n * executes these, with one audit trail and one place to watch the queue.\n *\n * A request lives at `personErasures/{orgId}__{sha256(email)}`: keyed by\n * the workspace AND the person, because the workspace is the controller of\n * this data and a second workspace that also knows the person has a\n * relationship of its own that this request has no claim on. The id is\n * deterministic so a second request for the same person is the same\n * document rather than a second one, and the address itself is carried only\n * while the request is pending — the sweep needs it to find the records,\n * and the completed request keeps the hash alone.\n *\n * Pure data module: names, types and the small helpers the dialog, the\n * route and the job would otherwise each write for themselves. No\n * Firestore, no React, no `node:` import — the client barrel re-exports it.\n */\n\nimport { normalizeContactEmail } from './contacts'\n\n/** Top-level collection holding one document per (workspace, person). */\nexport const PERSON_ERASURES_COLLECTION = 'personErasures'\n\n/**\n * The document id: workspace first, then the person's key, joined by a\n * separator neither can contain. A Firestore id may not hold a `/`, and a\n * person key is hex, so a double underscore is unambiguous.\n */\nexport function personErasureId(orgId: string, personKey: string): string {\n if (!orgId || !personKey) {\n throw new Error('a person erasure names a workspace and a person')\n }\n return `${orgId}__${personKey}`\n}\n\n/**\n * The field a pending request stamps on the records it covers — the contact\n * document and each site's lead — so a record page can show the state off\n * the document it already listens to, with no second read. Epoch millis.\n *\n * A marker, not the request: the job reads the queue, never this field, so\n * a client write that strips it hides the banner and changes nothing else.\n */\nexport const CONTACT_ERASURE_REQUESTED_FIELD = 'erasureRequestedAtMs'\n\n/** When a record's erasure was requested, or null for a record with none. */\nexport function readErasureRequestedAtMs(\n row: Record<string, unknown> | null | undefined,\n): number | null {\n const value = (row ?? {})[CONTACT_ERASURE_REQUESTED_FIELD]\n return typeof value === 'number' && Number.isFinite(value) && value > 0\n ? value\n : null\n}\n\n/**\n * Where a request is in its life.\n *\n * - `pending` — filed, waiting for the next run. `pendingSinceMs` is set,\n * which is what the job orders on.\n * - `erased` — the sweep ran and reported its counts. The address is gone\n * from the document.\n * - `failed` — the sweep threw. The request stays pending (it keeps\n * `pendingSinceMs`, moved to the back of the queue) so the next run tries\n * again, and the failure is on the document for a staff member to read.\n */\nexport type PersonErasureStatus = 'pending' | 'erased' | 'failed'\n\n/** `personErasures/{orgId}__{personKey}` document shape. */\nexport interface PersonErasureRequest {\n orgId: string\n /** `sha256(normalizedEmail)` — the same key `emailDeliveries` files under. */\n personKey: string\n status: PersonErasureStatus\n /**\n * The address, present ONLY while the request is pending: the sweep\n * queries by it, and nothing else may. Deleted by the job on completion.\n */\n email?: string\n requestedAtMs: number\n /** The workspace admin who filed it. */\n requestedByUid: string\n /** The site the request was filed from — for the activity feed's link. */\n hostId?: string\n /** The record it was filed from, when it was a contact. */\n contactId?: string\n /** The record it was filed from, when it was a lead. */\n leadId?: string\n /**\n * Set while pending. Absent once erased, so `orderBy('pendingSinceMs')`\n * lists exactly the queue — a document without the field is not returned.\n */\n pendingSinceMs?: number\n erasedAtMs?: number\n /** Counts only — what the sweep removed or anonymized, for the audit. */\n result?: Record<string, number>\n failedAtMs?: number\n failureCount?: number\n /** The last failure's message, for the staff reader. Never the data. */\n lastError?: string\n}\n\n/**\n * The `reason` a per-site suppression row carries when it was written by\n * an erasure. Such a row keeps NO address — the id is the hash — and does\n * two jobs the ordinary suppression already does one of: every campaign\n * gate refuses the address, and the contact capture path refuses to create\n * a record for it, so a later form fill or order does not quietly rebuild\n * the person the workspace just erased.\n */\nexport const PERSON_ERASURE_SUPPRESSION_REASON = 'erasure'\n\n/**\n * What a door says when it refuses to create a record for an erased address\n * — the contact create route and both lead-convert doors alike. The person\n * asked to be removed and a workspace admin filed it; a record cannot be\n * re-created by hand any more than by a form, and the sentence says where\n * the decision lives rather than implying the address is malformed.\n */\nexport const CONTACT_ERASED_MESSAGE =\n 'This person was erased from your workspace at their request, so a ' +\n 'record cannot be created for this address.'\n\n/**\n * What one request removes across the workspace — the dialog's list and the\n * docs' list, from one source so the two cannot drift. Each line names a\n * thing a reader recognizes from the console, not a collection.\n */\nexport const PERSON_ERASURE_REMOVES: readonly string[] = [\n 'The contact record on every site in this workspace — profile, notes, tags, timeline and custom fields',\n 'Every lead for this address, on every site',\n 'The tasks and logged activities filed against the person',\n 'The person on any email audience list',\n 'The campaign delivery log — every message sent to the address, with its opens and clicks',\n]\n\n/**\n * What survives, and how. Orders and bookings are the workspace's financial\n * and appointment records, and tax law keeps them; the person is taken off\n * them rather than the record destroyed. A deal is the team's own pipeline\n * record and is unlinked, not deleted.\n */\nexport const PERSON_ERASURE_RETAINS: readonly string[] = [\n 'Orders and bookings stay as financial records with the name, email, phone and addresses removed',\n 'Deals stay on the pipeline, unlinked from the person',\n 'Companies stay, with their contact count reduced',\n]\n\n/**\n * What the request does not reach, so the admin can finish the job by hand\n * where the person asked for everything. A form submission keeps the\n * address inside the answers it recorded, under whatever the form called\n * the field, so there is no key to find them by; a site member holds a\n * login of their own.\n */\nexport const PERSON_ERASURE_NOT_REACHED: readonly string[] = [\n 'Form submissions — delete them from the Inbox',\n 'A site member account — close it from the site’s Members page',\n]\n\n/**\n * Whether the typed confirmation names the record's address.\n *\n * Normalized on both sides, so `Jane@Example.com` confirms `jane@example.com`\n * — the confirmation is there to make the admin read the address, not to\n * test their capitalization.\n */\nexport function personErasureConfirmationMatches(\n typed: unknown,\n email: unknown,\n): boolean {\n const wanted = normalizeContactEmail(email)\n if (!wanted) return false\n return normalizeContactEmail(typed) === wanted\n}\n"],"names":["normalizeContactEmail","PERSON_ERASURES_COLLECTION","personErasureId","orgId","personKey","Error","CONTACT_ERASURE_REQUESTED_FIELD","readErasureRequestedAtMs","row","value","Number","isFinite","PERSON_ERASURE_SUPPRESSION_REASON","CONTACT_ERASED_MESSAGE","PERSON_ERASURE_REMOVES","PERSON_ERASURE_RETAINS","PERSON_ERASURE_NOT_REACHED","personErasureConfirmationMatches","typed","email","wanted"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;CAuBC,GAED,SAASA,qBAAqB,QAAQ,gBAAY;AAElD,uEAAuE,GACvE,OAAO,MAAMC,6BAA6B,iBAAgB;AAE1D;;;;CAIC,GACD,OAAO,SAASC,gBAAgBC,KAAa,EAAEC,SAAiB;IAC9D,IAAI,CAACD,SAAS,CAACC,WAAW;QACxB,MAAM,IAAIC,MAAM;IAClB;IACA,OAAO,GAAGF,MAAM,EAAE,EAAEC,WAAW;AACjC;AAEA;;;;;;;CAOC,GACD,OAAO,MAAME,kCAAkC,uBAAsB;AAErE,2EAA2E,GAC3E,OAAO,SAASC,yBACdC,GAA+C;IAE/C,MAAMC,QAAQ,CAACD,cAAAA,MAAO,CAAC,EAAE,CAACF,gCAAgC;IAC1D,OAAO,OAAOG,UAAU,YAAYC,OAAOC,QAAQ,CAACF,UAAUA,QAAQ,IAClEA,QACA;AACN;AAiDA;;;;;;;CAOC,GACD,OAAO,MAAMG,oCAAoC,UAAS;AAE1D;;;;;;CAMC,GACD,OAAO,MAAMC,yBACX,uEACA,6CAA4C;AAE9C;;;;CAIC,GACD,OAAO,MAAMC,yBAA4C;IACvD;IACA;IACA;IACA;IACA;CACD,CAAA;AAED;;;;;CAKC,GACD,OAAO,MAAMC,yBAA4C;IACvD;IACA;IACA;CACD,CAAA;AAED;;;;;;CAMC,GACD,OAAO,MAAMC,6BAAgD;IAC3D;IACA;CACD,CAAA;AAED;;;;;;CAMC,GACD,OAAO,SAASC,iCACdC,KAAc,EACdC,KAAc;IAEd,MAAMC,SAASpB,sBAAsBmB;IACrC,IAAI,CAACC,QAAQ,OAAO;IACpB,OAAOpB,sBAAsBkB,WAAWE;AAC1C"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/person-erasure.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 * Privacy erasure of one PERSON from one workspace (AGL-2623).\n *\n * The CRM's **Delete contact** is a detach: one site lets go of a shared row\n * and the document dies only when the last holder does. A privacy erasure is\n * the opposite act — the person is removed from every site in the workspace\n * regardless of who holds them, together with everything the workspace keeps\n * beside them — and it is filed as a REQUEST rather than executed on the\n * click, so that the same daily job that executes workspace erasures\n * executes these, with one audit trail and one place to watch the queue.\n *\n * A request lives at `personErasures/{orgId}__{sha256(email)}`: keyed by\n * the workspace AND the person, because the workspace is the controller of\n * this data and a second workspace that also knows the person has a\n * relationship of its own that this request has no claim on. The id is\n * deterministic so a second request for the same person is the same\n * document rather than a second one, and the address itself is carried only\n * while the request is pending — the sweep needs it to find the records,\n * and the completed request keeps the hash alone.\n *\n * Pure data module: names, types and the small helpers the dialog, the\n * route and the job would otherwise each write for themselves. No\n * Firestore, no React, no `node:` import — the client barrel re-exports it.\n */\n\nimport { normalizeContactEmail } from './contacts'\n\n/** Top-level collection holding one document per (workspace, person). */\nexport const PERSON_ERASURES_COLLECTION = 'personErasures'\n\n/**\n * The document id: workspace first, then the person's key, joined by a\n * separator neither can contain. A Firestore id may not hold a `/`, and a\n * person key is hex, so a double underscore is unambiguous.\n */\nexport function personErasureId(orgId: string, personKey: string): string {\n if (!orgId || !personKey) {\n throw new Error('a person erasure names a workspace and a person')\n }\n return `${orgId}__${personKey}`\n}\n\n/**\n * The field a pending request stamps on the records it covers — the contact\n * document and each site's lead — so a record page can show the state off\n * the document it already listens to, with no second read. Epoch millis.\n *\n * A marker, not the request: the job reads the queue, never this field, so\n * a client write that strips it hides the banner and changes nothing else.\n */\nexport const CONTACT_ERASURE_REQUESTED_FIELD = 'erasureRequestedAtMs'\n\n/** When a record's erasure was requested, or null for a record with none. */\nexport function readErasureRequestedAtMs(\n row: Record<string, unknown> | null | undefined,\n): number | null {\n const value = (row ?? {})[CONTACT_ERASURE_REQUESTED_FIELD]\n return typeof value === 'number' && Number.isFinite(value) && value > 0\n ? value\n : null\n}\n\n/**\n * Where a request is in its life.\n *\n * - `pending` — filed, waiting for the next run. `pendingSinceMs` is set,\n * which is what the job orders on.\n * - `erased` — the sweep ran and reported its counts. The address is gone\n * from the document.\n * - `failed` — the sweep threw. The request stays pending (it keeps\n * `pendingSinceMs`, moved to the back of the queue) so the next run tries\n * again, and the failure is on the document for a staff member to read.\n */\nexport type PersonErasureStatus = 'pending' | 'erased' | 'failed'\n\n/** `personErasures/{orgId}__{personKey}` document shape. */\nexport interface PersonErasureRequest {\n orgId: string\n /** `sha256(normalizedEmail)` — the same key `emailDeliveries` files under. */\n personKey: string\n status: PersonErasureStatus\n /**\n * The address, present ONLY while the request is pending: the sweep\n * queries by it, and nothing else may. Deleted by the job on completion.\n */\n email?: string\n requestedAtMs: number\n /** The workspace admin who filed it. */\n requestedByUid: string\n /** The site the request was filed from — for the activity feed's link. */\n hostId?: string\n /** The record it was filed from, when it was a contact. */\n contactId?: string\n /** The record it was filed from, when it was a lead. */\n leadId?: string\n /**\n * Set while pending. Absent once erased, so `orderBy('pendingSinceMs')`\n * lists exactly the queue — a document without the field is not returned.\n */\n pendingSinceMs?: number\n erasedAtMs?: number\n /**\n * Counts only — what the sweep removed or anonymized, for the audit: the\n * platform's own figures, and each plugin's share under `plugins`.\n */\n result?: Readonly<Record<string, unknown>>\n failedAtMs?: number\n failureCount?: number\n /** The last failure's message, for the staff reader. Never the data. */\n lastError?: string\n}\n\n/**\n * The `reason` a per-site suppression row carries when it was written by\n * an erasure. Such a row keeps NO address — the id is the hash — and does\n * two jobs the ordinary suppression already does one of: every campaign\n * gate refuses the address, and the contact capture path refuses to create\n * a record for it, so a later form fill or order does not quietly rebuild\n * the person the workspace just erased.\n */\nexport const PERSON_ERASURE_SUPPRESSION_REASON = 'erasure'\n\n/**\n * What a door says when it refuses to create a record for an erased address\n * — the contact create route and both lead-convert doors alike. The person\n * asked to be removed and a workspace admin filed it; a record cannot be\n * re-created by hand any more than by a form, and the sentence says where\n * the decision lives rather than implying the address is malformed.\n */\nexport const CONTACT_ERASED_MESSAGE =\n 'This person was erased from your workspace at their request, so a ' +\n 'record cannot be created for this address.'\n\n/**\n * What one request removes across the workspace — the dialog's list and the\n * docs' list, from one source so the two cannot drift. Each line names a\n * thing a reader recognizes from the console, not a collection.\n */\nexport const PERSON_ERASURE_REMOVES: readonly string[] = [\n 'The contact record on every site in this workspace — profile, notes, tags, timeline and custom fields',\n 'Every lead for this address, on every site',\n 'The tasks and logged activities filed against the person',\n 'The person on any email audience list',\n 'The campaign delivery log — every message sent to the address, with its opens and clicks',\n]\n\n/**\n * What survives, and how. Orders and bookings are the workspace's financial\n * and appointment records, and tax law keeps them; the person is taken off\n * them rather than the record destroyed. A deal is the team's own pipeline\n * record and is unlinked, not deleted.\n */\nexport const PERSON_ERASURE_RETAINS: readonly string[] = [\n 'Orders and bookings stay as financial records with the name, email, phone and addresses removed',\n 'Deals stay on the pipeline, unlinked from the person',\n 'Companies stay, with their contact count reduced',\n]\n\n/**\n * What the request does not reach, so the admin can finish the job by hand\n * where the person asked for everything. A form submission keeps the\n * address inside the answers it recorded, under whatever the form called\n * the field, so there is no key to find them by; a site member holds a\n * login of their own.\n */\nexport const PERSON_ERASURE_NOT_REACHED: readonly string[] = [\n 'Form submissions — delete them from the Inbox',\n 'A site member account — close it from the site’s Members page',\n]\n\n/**\n * Whether the typed confirmation names the record's address.\n *\n * Normalized on both sides, so `Jane@Example.com` confirms `jane@example.com`\n * — the confirmation is there to make the admin read the address, not to\n * test their capitalization.\n */\nexport function personErasureConfirmationMatches(\n typed: unknown,\n email: unknown,\n): boolean {\n const wanted = normalizeContactEmail(email)\n if (!wanted) return false\n return normalizeContactEmail(typed) === wanted\n}\n"],"names":["normalizeContactEmail","PERSON_ERASURES_COLLECTION","personErasureId","orgId","personKey","Error","CONTACT_ERASURE_REQUESTED_FIELD","readErasureRequestedAtMs","row","value","Number","isFinite","PERSON_ERASURE_SUPPRESSION_REASON","CONTACT_ERASED_MESSAGE","PERSON_ERASURE_REMOVES","PERSON_ERASURE_RETAINS","PERSON_ERASURE_NOT_REACHED","personErasureConfirmationMatches","typed","email","wanted"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;CAuBC,GAED,SAASA,qBAAqB,QAAQ,gBAAY;AAElD,uEAAuE,GACvE,OAAO,MAAMC,6BAA6B,iBAAgB;AAE1D;;;;CAIC,GACD,OAAO,SAASC,gBAAgBC,KAAa,EAAEC,SAAiB;IAC9D,IAAI,CAACD,SAAS,CAACC,WAAW;QACxB,MAAM,IAAIC,MAAM;IAClB;IACA,OAAO,GAAGF,MAAM,EAAE,EAAEC,WAAW;AACjC;AAEA;;;;;;;CAOC,GACD,OAAO,MAAME,kCAAkC,uBAAsB;AAErE,2EAA2E,GAC3E,OAAO,SAASC,yBACdC,GAA+C;IAE/C,MAAMC,QAAQ,CAACD,cAAAA,MAAO,CAAC,EAAE,CAACF,gCAAgC;IAC1D,OAAO,OAAOG,UAAU,YAAYC,OAAOC,QAAQ,CAACF,UAAUA,QAAQ,IAClEA,QACA;AACN;AAoDA;;;;;;;CAOC,GACD,OAAO,MAAMG,oCAAoC,UAAS;AAE1D;;;;;;CAMC,GACD,OAAO,MAAMC,yBACX,uEACA,6CAA4C;AAE9C;;;;CAIC,GACD,OAAO,MAAMC,yBAA4C;IACvD;IACA;IACA;IACA;IACA;CACD,CAAA;AAED;;;;;CAKC,GACD,OAAO,MAAMC,yBAA4C;IACvD;IACA;IACA;CACD,CAAA;AAED;;;;;;CAMC,GACD,OAAO,MAAMC,6BAAgD;IAC3D;IACA;CACD,CAAA;AAED;;;;;;CAMC,GACD,OAAO,SAASC,iCACdC,KAAc,EACdC,KAAc;IAEd,MAAMC,SAASpB,sBAAsBmB;IACrC,IAAI,CAACC,QAAQ,OAAO;IACpB,OAAOpB,sBAAsBkB,WAAWE;AAC1C"}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/** The most values one list holds — a menu, not a table. */
|
|
18
|
+
export declare const PICKLIST_VALUES_MAX = 200;
|
|
19
|
+
/** The longest label a value carries. */
|
|
20
|
+
export declare const PICKLIST_LABEL_MAX = 120;
|
|
21
|
+
/** One value of a list. `id` never changes; `label` is what records store. */
|
|
22
|
+
export interface PicklistValue {
|
|
23
|
+
id: string;
|
|
24
|
+
label: string;
|
|
25
|
+
active: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* The group the value is listed under, by group id; `null` for none.
|
|
28
|
+
* Present only on a list whose spec has groups.
|
|
29
|
+
*/
|
|
30
|
+
group?: string | null;
|
|
31
|
+
/**
|
|
32
|
+
* What the value MEANS to the platform, from the spec's `meanings`;
|
|
33
|
+
* `null` for an added value that names none. Present only on a list whose
|
|
34
|
+
* spec has meanings, and fixed by the spec for a standard value.
|
|
35
|
+
*/
|
|
36
|
+
meaning?: string | null;
|
|
37
|
+
}
|
|
38
|
+
/** A list as stored and as every reader takes it. */
|
|
39
|
+
export interface PicklistValueSet {
|
|
40
|
+
/** In the order every picker lists them. */
|
|
41
|
+
values: PicklistValue[];
|
|
42
|
+
/** The value a new record starts with, by id — `null` for none. */
|
|
43
|
+
defaultValueId: string | null;
|
|
44
|
+
}
|
|
45
|
+
/** One value a spec ships to every organization. */
|
|
46
|
+
export interface PicklistStandardValue {
|
|
47
|
+
id: string;
|
|
48
|
+
label: string;
|
|
49
|
+
group?: string;
|
|
50
|
+
meaning?: string;
|
|
51
|
+
}
|
|
52
|
+
/** One heading values may be listed under. */
|
|
53
|
+
export interface PicklistGroup {
|
|
54
|
+
id: string;
|
|
55
|
+
label: string;
|
|
56
|
+
}
|
|
57
|
+
/** What the engine needs to know of a picklist. */
|
|
58
|
+
export interface PicklistSpec {
|
|
59
|
+
/**
|
|
60
|
+
* A restricted list refuses a write naming a value it does not hold; an
|
|
61
|
+
* unrestricted one stores it as typed.
|
|
62
|
+
*/
|
|
63
|
+
restricted: boolean;
|
|
64
|
+
/** The values every organization has, in their starting order. */
|
|
65
|
+
standardValues: readonly PicklistStandardValue[];
|
|
66
|
+
/** The value a new record starts with while the organization has stored no list. */
|
|
67
|
+
defaultValueId?: string;
|
|
68
|
+
/** The headings values are listed under, in order; ungrouped values come last. */
|
|
69
|
+
groups?: readonly PicklistGroup[];
|
|
70
|
+
/**
|
|
71
|
+
* The platform's own vocabulary for what a value means, when it has one.
|
|
72
|
+
* Every value carries one of these or `null`, and a value deleted with a
|
|
73
|
+
* replacement moves its records only to a value of the SAME meaning.
|
|
74
|
+
*/
|
|
75
|
+
meanings?: readonly string[];
|
|
76
|
+
}
|
|
77
|
+
/** A label as a list stores it: trimmed, single-spaced, capped. */
|
|
78
|
+
export declare function normalizePicklistLabel(value: unknown): string;
|
|
79
|
+
/** The key two labels are compared by: case and spacing do not make a new value. */
|
|
80
|
+
export declare function picklistLabelKey(label: unknown): string;
|
|
81
|
+
/**
|
|
82
|
+
* A new value's id: the label as a slug, suffixed until it is not one of
|
|
83
|
+
* `taken`. Derived rather than random so a list reads the same in every
|
|
84
|
+
* organization, and never reused, because a default names a value by it.
|
|
85
|
+
* A caller minting an ADDED value counts every standard id as taken, so an
|
|
86
|
+
* added value can never be mistaken for a standard one.
|
|
87
|
+
*/
|
|
88
|
+
export declare function mintPicklistValueId(label: string, taken: Iterable<string>): string;
|
|
89
|
+
/** A list of active values from labels, in order, with no default. */
|
|
90
|
+
export declare function picklistFromLabels(labels: readonly string[]): PicklistValueSet;
|
|
91
|
+
/** The standard value an id names under `spec`, or `null`. */
|
|
92
|
+
export declare function picklistStandardValue(spec: PicklistSpec, id: string | null | undefined): PicklistStandardValue | null;
|
|
93
|
+
/** Whether `id` is one of the spec's standard values — computed, never stored. */
|
|
94
|
+
export declare function isStandardPicklistValueId(spec: PicklistSpec, id: string | null | undefined): boolean;
|
|
95
|
+
/**
|
|
96
|
+
* A stored document as a list, or `null` for one that holds none.
|
|
97
|
+
*
|
|
98
|
+
* Tolerant, because the document is client-written: a value with no label
|
|
99
|
+
* is dropped, a second value with the same label (in any case) or the same
|
|
100
|
+
* id is dropped, `active` is true unless it is `false`, and a default that
|
|
101
|
+
* names no value is no default. With a `spec`, each value's group and
|
|
102
|
+
* meaning are read against it as well.
|
|
103
|
+
*/
|
|
104
|
+
export declare function normalizePicklistValueSet(raw: unknown, spec?: PicklistSpec): PicklistValueSet | null;
|
|
105
|
+
/**
|
|
106
|
+
* The effective list: `stored` (already normalized against `spec`) with
|
|
107
|
+
* every standard value it lacks — see the block header. `null` for an
|
|
108
|
+
* organization that stored nothing answers the standard values in order,
|
|
109
|
+
* with the spec's default.
|
|
110
|
+
*/
|
|
111
|
+
export declare function mergePicklistStandard(spec: PicklistSpec, stored: PicklistValueSet | null): PicklistValueSet;
|
|
112
|
+
/** A stored document as every reader should take it under `spec`. */
|
|
113
|
+
export declare function effectivePicklistValueSet(spec: PicklistSpec, raw: unknown): PicklistValueSet;
|
|
114
|
+
/** The value a label names, in any case or spacing — `null` for none. */
|
|
115
|
+
export declare function picklistValueByLabel(set: PicklistValueSet, label: unknown): PicklistValue | null;
|
|
116
|
+
/** The values a picker offers. */
|
|
117
|
+
export declare function picklistActiveValues(set: PicklistValueSet): PicklistValue[];
|
|
118
|
+
/** The label a new record starts with — only while the default value is active. */
|
|
119
|
+
export declare function picklistDefaultLabel(set: PicklistValueSet): string | null;
|
|
120
|
+
/**
|
|
121
|
+
* The sentence a value outside a restricted list is refused with, naming
|
|
122
|
+
* what the list allows: `{field} must be one of: …`. A long list is cut at
|
|
123
|
+
* twenty names so the sentence stays one a person can read; `empty` is the
|
|
124
|
+
* sentence for a list with no active value at all.
|
|
125
|
+
*/
|
|
126
|
+
export declare function picklistRefusalSentence(set: PicklistValueSet, words: {
|
|
127
|
+
field: string;
|
|
128
|
+
empty: string;
|
|
129
|
+
}): string;
|
|
130
|
+
/** What a judged write stores, or why it is refused. */
|
|
131
|
+
export type PicklistJudgement = {
|
|
132
|
+
ok: true;
|
|
133
|
+
value: string | null;
|
|
134
|
+
} | {
|
|
135
|
+
ok: false;
|
|
136
|
+
error: string;
|
|
137
|
+
};
|
|
138
|
+
/**
|
|
139
|
+
* Whether a write may store `value` on a record, and the label it stores.
|
|
140
|
+
*
|
|
141
|
+
* - Blank or `null` clears the field.
|
|
142
|
+
* - An ACTIVE value, in any case or spacing, stores that value's label.
|
|
143
|
+
* - The record's `current` value is kept even when it is inactive or no
|
|
144
|
+
* longer listed: re-saving a record must never be refused for a value
|
|
145
|
+
* somebody else deactivated.
|
|
146
|
+
* - On an unrestricted list anything else is stored — as the list spells
|
|
147
|
+
* it when the list holds it, else as typed.
|
|
148
|
+
* - On a restricted list anything else is refused with `refusal()`.
|
|
149
|
+
*/
|
|
150
|
+
export declare function judgePicklistValue(set: PicklistValueSet, value: unknown, options: {
|
|
151
|
+
restricted: boolean;
|
|
152
|
+
current?: unknown;
|
|
153
|
+
refusal: () => string;
|
|
154
|
+
}): PicklistJudgement;
|
|
155
|
+
/** One option of a select over a list. */
|
|
156
|
+
export interface PicklistOption {
|
|
157
|
+
label: string;
|
|
158
|
+
/** Listed but deactivated — shown with an "(inactive)" hint. */
|
|
159
|
+
inactive: boolean;
|
|
160
|
+
/** Not in the list at all — a value written before the list held it. */
|
|
161
|
+
unlisted: boolean;
|
|
162
|
+
/** The group it is listed under, when the list has groups; `null` for none. */
|
|
163
|
+
group?: string | null;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* What a select offers for a record holding `current`: every active value
|
|
167
|
+
* in order, then the record's own value when the list would not otherwise
|
|
168
|
+
* show it, so the select keeps what the record holds until it is changed.
|
|
169
|
+
*/
|
|
170
|
+
export declare function picklistOptions(set: PicklistValueSet, current?: unknown): PicklistOption[];
|
|
171
|
+
/** One heading of a grouped select and the options under it; `group` null is the ungrouped tail. */
|
|
172
|
+
export interface PicklistOptionGroup {
|
|
173
|
+
group: PicklistGroup | null;
|
|
174
|
+
options: PicklistOption[];
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Options under their headings: each group in the spec's order, then every
|
|
178
|
+
* option with no group (or an unknown one) last. A heading with no option
|
|
179
|
+
* is left out, and the list's own order holds within each.
|
|
180
|
+
*/
|
|
181
|
+
export declare function groupPicklistOptions(options: readonly PicklistOption[], groups: readonly PicklistGroup[]): PicklistOptionGroup[];
|
|
182
|
+
/**
|
|
183
|
+
* Where a label sorts: its value's position in the list, unlisted values
|
|
184
|
+
* after every listed one, and a record with none last. The order the admin
|
|
185
|
+
* chose is the order a sort by the field reads in.
|
|
186
|
+
*/
|
|
187
|
+
export declare function picklistRank(set: PicklistValueSet, label: unknown): number;
|