@aglyn/tenant-data-admin 1.0.0-beta.143
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/LICENSE +201 -0
- package/README.md +7 -0
- package/jest.integration.config.d.ts +39 -0
- package/jest.integration.setup.d.ts +17 -0
- package/package.json +51 -0
- package/src/index.d.ts +17 -0
- package/src/index.js +18 -0
- package/src/index.js.map +1 -0
- package/src/lib/render-cache.d.ts +184 -0
- package/src/lib/render-cache.js +198 -0
- package/src/lib/render-cache.js.map +1 -0
- package/src/lib/server/account-addresses.d.ts +160 -0
- package/src/lib/server/account-addresses.js +311 -0
- package/src/lib/server/account-addresses.js.map +1 -0
- package/src/lib/server/account-emails.d.ts +219 -0
- package/src/lib/server/account-emails.js +640 -0
- package/src/lib/server/account-emails.js.map +1 -0
- package/src/lib/server/admin-audit.d.ts +180 -0
- package/src/lib/server/admin-audit.js +233 -0
- package/src/lib/server/admin-audit.js.map +1 -0
- package/src/lib/server/analytics-retention.d.ts +63 -0
- package/src/lib/server/analytics-retention.js +66 -0
- package/src/lib/server/analytics-retention.js.map +1 -0
- package/src/lib/server/api-http.d.ts +159 -0
- package/src/lib/server/api-http.js +230 -0
- package/src/lib/server/api-http.js.map +1 -0
- package/src/lib/server/api-keys.d.ts +117 -0
- package/src/lib/server/api-keys.js +298 -0
- package/src/lib/server/api-keys.js.map +1 -0
- package/src/lib/server/auth-handoff.d.ts +165 -0
- package/src/lib/server/auth-handoff.js +482 -0
- package/src/lib/server/auth-handoff.js.map +1 -0
- package/src/lib/server/auth-pools.d.ts +246 -0
- package/src/lib/server/auth-pools.js +551 -0
- package/src/lib/server/auth-pools.js.map +1 -0
- package/src/lib/server/billing-addons.d.ts +171 -0
- package/src/lib/server/billing-addons.js +277 -0
- package/src/lib/server/billing-addons.js.map +1 -0
- package/src/lib/server/campaign-attribution-store.d.ts +55 -0
- package/src/lib/server/campaign-attribution-store.js +95 -0
- package/src/lib/server/campaign-attribution-store.js.map +1 -0
- package/src/lib/server/campaign-conversion-attribution.d.ts +205 -0
- package/src/lib/server/campaign-conversion-attribution.js +279 -0
- package/src/lib/server/campaign-conversion-attribution.js.map +1 -0
- package/src/lib/server/client-error-report.d.ts +301 -0
- package/src/lib/server/client-error-report.js +755 -0
- package/src/lib/server/client-error-report.js.map +1 -0
- package/src/lib/server/connect-account-status.d.ts +74 -0
- package/src/lib/server/connect-account-status.js +94 -0
- package/src/lib/server/connect-account-status.js.map +1 -0
- package/src/lib/server/connect-payout-failure.d.ts +103 -0
- package/src/lib/server/connect-payout-failure.js +127 -0
- package/src/lib/server/connect-payout-failure.js.map +1 -0
- package/src/lib/server/console-api-rate-limit.d.ts +81 -0
- package/src/lib/server/console-api-rate-limit.js +111 -0
- package/src/lib/server/console-api-rate-limit.js.map +1 -0
- package/src/lib/server/console-domains.d.ts +326 -0
- package/src/lib/server/console-domains.js +779 -0
- package/src/lib/server/console-domains.js.map +1 -0
- package/src/lib/server/consume-once.d.ts +91 -0
- package/src/lib/server/consume-once.js +96 -0
- package/src/lib/server/consume-once.js.map +1 -0
- package/src/lib/server/contact-company-link.d.ts +85 -0
- package/src/lib/server/contact-company-link.js +105 -0
- package/src/lib/server/contact-company-link.js.map +1 -0
- package/src/lib/server/contact-email-engagement.d.ts +32 -0
- package/src/lib/server/contact-email-engagement.js +145 -0
- package/src/lib/server/contact-email-engagement.js.map +1 -0
- package/src/lib/server/contact-email-index.d.ts +56 -0
- package/src/lib/server/contact-email-index.js +158 -0
- package/src/lib/server/contact-email-index.js.map +1 -0
- package/src/lib/server/contact-lifecycle-floor.d.ts +89 -0
- package/src/lib/server/contact-lifecycle-floor.js +88 -0
- package/src/lib/server/contact-lifecycle-floor.js.map +1 -0
- package/src/lib/server/contact-merge.d.ts +55 -0
- package/src/lib/server/contact-merge.js +279 -0
- package/src/lib/server/contact-merge.js.map +1 -0
- package/src/lib/server/contact-suppression.d.ts +153 -0
- package/src/lib/server/contact-suppression.js +259 -0
- package/src/lib/server/contact-suppression.js.map +1 -0
- package/src/lib/server/crm-booking-activity.d.ts +86 -0
- package/src/lib/server/crm-booking-activity.js +251 -0
- package/src/lib/server/crm-booking-activity.js.map +1 -0
- package/src/lib/server/crm-email-activity.d.ts +112 -0
- package/src/lib/server/crm-email-activity.js +143 -0
- package/src/lib/server/crm-email-activity.js.map +1 -0
- package/src/lib/server/crm-inbound-email.d.ts +145 -0
- package/src/lib/server/crm-inbound-email.js +287 -0
- package/src/lib/server/crm-inbound-email.js.map +1 -0
- package/src/lib/server/crm-next-activity.d.ts +74 -0
- package/src/lib/server/crm-next-activity.js +194 -0
- package/src/lib/server/crm-next-activity.js.map +1 -0
- package/src/lib/server/crm-records.d.ts +144 -0
- package/src/lib/server/crm-records.js +218 -0
- package/src/lib/server/crm-records.js.map +1 -0
- package/src/lib/server/csp-aggregate.d.ts +62 -0
- package/src/lib/server/csp-aggregate.js +276 -0
- package/src/lib/server/csp-aggregate.js.map +1 -0
- package/src/lib/server/data-storage-gate.d.ts +66 -0
- package/src/lib/server/data-storage-gate.js +48 -0
- package/src/lib/server/data-storage-gate.js.map +1 -0
- package/src/lib/server/dataset-live-pages.d.ts +179 -0
- package/src/lib/server/dataset-live-pages.js +421 -0
- package/src/lib/server/dataset-live-pages.js.map +1 -0
- package/src/lib/server/dns-probe.d.ts +53 -0
- package/src/lib/server/dns-probe.js +184 -0
- package/src/lib/server/dns-probe.js.map +1 -0
- package/src/lib/server/document-id.d.ts +29 -0
- package/src/lib/server/document-id.js +100 -0
- package/src/lib/server/document-id.js.map +1 -0
- package/src/lib/server/domain-provider-vercel.d.ts +33 -0
- package/src/lib/server/domain-provider-vercel.js +298 -0
- package/src/lib/server/domain-provider-vercel.js.map +1 -0
- package/src/lib/server/domain-provider-webhook.d.ts +73 -0
- package/src/lib/server/domain-provider-webhook.js +223 -0
- package/src/lib/server/domain-provider-webhook.js.map +1 -0
- package/src/lib/server/domain-provider-wildcard.d.ts +54 -0
- package/src/lib/server/domain-provider-wildcard.js +163 -0
- package/src/lib/server/domain-provider-wildcard.js.map +1 -0
- package/src/lib/server/domain-provider.d.ts +197 -0
- package/src/lib/server/domain-provider.js +221 -0
- package/src/lib/server/domain-provider.js.map +1 -0
- package/src/lib/server/duplicate-activity.d.ts +54 -0
- package/src/lib/server/duplicate-activity.js +67 -0
- package/src/lib/server/duplicate-activity.js.map +1 -0
- package/src/lib/server/duplicate-resource.d.ts +55 -0
- package/src/lib/server/duplicate-resource.js +474 -0
- package/src/lib/server/duplicate-resource.js.map +1 -0
- package/src/lib/server/dynamic-list-materialize.d.ts +153 -0
- package/src/lib/server/dynamic-list-materialize.js +581 -0
- package/src/lib/server/dynamic-list-materialize.js.map +1 -0
- package/src/lib/server/edit-access-authz.d.ts +56 -0
- package/src/lib/server/edit-access-authz.js +107 -0
- package/src/lib/server/edit-access-authz.js.map +1 -0
- package/src/lib/server/edit-access-token.d.ts +72 -0
- package/src/lib/server/edit-access-token.js +116 -0
- package/src/lib/server/edit-access-token.js.map +1 -0
- package/src/lib/server/edit-hint-token.d.ts +83 -0
- package/src/lib/server/edit-hint-token.js +119 -0
- package/src/lib/server/edit-hint-token.js.map +1 -0
- package/src/lib/server/email-delivery-log.d.ts +509 -0
- package/src/lib/server/email-delivery-log.js +1043 -0
- package/src/lib/server/email-delivery-log.js.map +1 -0
- package/src/lib/server/email-flow-gate.d.ts +80 -0
- package/src/lib/server/email-flow-gate.js +153 -0
- package/src/lib/server/email-flow-gate.js.map +1 -0
- package/src/lib/server/email-marketing-gate.d.ts +333 -0
- package/src/lib/server/email-marketing-gate.js +639 -0
- package/src/lib/server/email-marketing-gate.js.map +1 -0
- package/src/lib/server/email-metering.d.ts +293 -0
- package/src/lib/server/email-metering.js +359 -0
- package/src/lib/server/email-metering.js.map +1 -0
- package/src/lib/server/email-revenue-attribution.d.ts +185 -0
- package/src/lib/server/email-revenue-attribution.js +300 -0
- package/src/lib/server/email-revenue-attribution.js.map +1 -0
- package/src/lib/server/email-send-rate.d.ts +232 -0
- package/src/lib/server/email-send-rate.js +417 -0
- package/src/lib/server/email-send-rate.js.map +1 -0
- package/src/lib/server/email-sender-reputation.d.ts +317 -0
- package/src/lib/server/email-sender-reputation.js +464 -0
- package/src/lib/server/email-sender-reputation.js.map +1 -0
- package/src/lib/server/email-suppression.d.ts +490 -0
- package/src/lib/server/email-suppression.js +648 -0
- package/src/lib/server/email-suppression.js.map +1 -0
- package/src/lib/server/email-topic-confirmation.d.ts +150 -0
- package/src/lib/server/email-topic-confirmation.js +272 -0
- package/src/lib/server/email-topic-confirmation.js.map +1 -0
- package/src/lib/server/email-unsubscribe-link.d.ts +174 -0
- package/src/lib/server/email-unsubscribe-link.js +236 -0
- package/src/lib/server/email-unsubscribe-link.js.map +1 -0
- package/src/lib/server/erase-person.d.ts +116 -0
- package/src/lib/server/erase-person.js +272 -0
- package/src/lib/server/erase-person.js.map +1 -0
- package/src/lib/server/erase.d.ts +458 -0
- package/src/lib/server/erase.js +1506 -0
- package/src/lib/server/erase.js.map +1 -0
- package/src/lib/server/firebase-admin.d.ts +71 -0
- package/src/lib/server/firebase-admin.js +343 -0
- package/src/lib/server/firebase-admin.js.map +1 -0
- package/src/lib/server/form-dataset-binding-token.d.ts +35 -0
- package/src/lib/server/form-dataset-binding-token.js +107 -0
- package/src/lib/server/form-dataset-binding-token.js.map +1 -0
- package/src/lib/server/free-workspace-cap.d.ts +201 -0
- package/src/lib/server/free-workspace-cap.js +404 -0
- package/src/lib/server/free-workspace-cap.js.map +1 -0
- package/src/lib/server/ga4-measurement-protocol.d.ts +256 -0
- package/src/lib/server/ga4-measurement-protocol.js +558 -0
- package/src/lib/server/ga4-measurement-protocol.js.map +1 -0
- package/src/lib/server/host-email-tokens.d.ts +47 -0
- package/src/lib/server/host-email-tokens.js +72 -0
- package/src/lib/server/host-email-tokens.js.map +1 -0
- package/src/lib/server/host-memberships.d.ts +54 -0
- package/src/lib/server/host-memberships.js +194 -0
- package/src/lib/server/host-memberships.js.map +1 -0
- package/src/lib/server/host-sending-domain.d.ts +292 -0
- package/src/lib/server/host-sending-domain.js +451 -0
- package/src/lib/server/host-sending-domain.js.map +1 -0
- package/src/lib/server/host-visitor-records.d.ts +150 -0
- package/src/lib/server/host-visitor-records.js +262 -0
- package/src/lib/server/host-visitor-records.js.map +1 -0
- package/src/lib/server/id-token-refusal.d.ts +28 -0
- package/src/lib/server/id-token-refusal.js +111 -0
- package/src/lib/server/id-token-refusal.js.map +1 -0
- package/src/lib/server/legal-acceptance.d.ts +214 -0
- package/src/lib/server/legal-acceptance.js +305 -0
- package/src/lib/server/legal-acceptance.js.map +1 -0
- package/src/lib/server/list-members.d.ts +211 -0
- package/src/lib/server/list-members.js +176 -0
- package/src/lib/server/list-members.js.map +1 -0
- package/src/lib/server/live-page-usage.d.ts +202 -0
- package/src/lib/server/live-page-usage.js +300 -0
- package/src/lib/server/live-page-usage.js.map +1 -0
- package/src/lib/server/lockdown.d.ts +313 -0
- package/src/lib/server/lockdown.js +643 -0
- package/src/lib/server/lockdown.js.map +1 -0
- package/src/lib/server/media-cdn-rate-limit.d.ts +188 -0
- package/src/lib/server/media-cdn-rate-limit.js +204 -0
- package/src/lib/server/media-cdn-rate-limit.js.map +1 -0
- package/src/lib/server/media-delivery.d.ts +267 -0
- package/src/lib/server/media-delivery.js +485 -0
- package/src/lib/server/media-delivery.js.map +1 -0
- package/src/lib/server/media-download-tokens.d.ts +161 -0
- package/src/lib/server/media-download-tokens.js +386 -0
- package/src/lib/server/media-download-tokens.js.map +1 -0
- package/src/lib/server/media-quarantine.d.ts +127 -0
- package/src/lib/server/media-quarantine.js +170 -0
- package/src/lib/server/media-quarantine.js.map +1 -0
- package/src/lib/server/media-signing.d.ts +126 -0
- package/src/lib/server/media-signing.js +159 -0
- package/src/lib/server/media-signing.js.map +1 -0
- package/src/lib/server/media-storage-path.d.ts +96 -0
- package/src/lib/server/media-storage-path.js +111 -0
- package/src/lib/server/media-storage-path.js.map +1 -0
- package/src/lib/server/media-strong-digest.d.ts +85 -0
- package/src/lib/server/media-strong-digest.js +190 -0
- package/src/lib/server/media-strong-digest.js.map +1 -0
- package/src/lib/server/media-tombstone.d.ts +276 -0
- package/src/lib/server/media-tombstone.js +421 -0
- package/src/lib/server/media-tombstone.js.map +1 -0
- package/src/lib/server/media-variants.d.ts +309 -0
- package/src/lib/server/media-variants.js +428 -0
- package/src/lib/server/media-variants.js.map +1 -0
- package/src/lib/server/member-email-aliases.d.ts +180 -0
- package/src/lib/server/member-email-aliases.js +420 -0
- package/src/lib/server/member-email-aliases.js.map +1 -0
- package/src/lib/server/member-photo.d.ts +88 -0
- package/src/lib/server/member-photo.js +185 -0
- package/src/lib/server/member-photo.js.map +1 -0
- package/src/lib/server/membership-recover-throttle.d.ts +211 -0
- package/src/lib/server/membership-recover-throttle.js +231 -0
- package/src/lib/server/membership-recover-throttle.js.map +1 -0
- package/src/lib/server/notifications.d.ts +55 -0
- package/src/lib/server/notifications.js +134 -0
- package/src/lib/server/notifications.js.map +1 -0
- package/src/lib/server/org-billing.d.ts +97 -0
- package/src/lib/server/org-billing.js +236 -0
- package/src/lib/server/org-billing.js.map +1 -0
- package/src/lib/server/organizations.d.ts +837 -0
- package/src/lib/server/organizations.js +1814 -0
- package/src/lib/server/organizations.js.map +1 -0
- package/src/lib/server/password-reset-throttle.d.ts +84 -0
- package/src/lib/server/password-reset-throttle.js +102 -0
- package/src/lib/server/password-reset-throttle.js.map +1 -0
- package/src/lib/server/personal-data-export.d.ts +241 -0
- package/src/lib/server/personal-data-export.js +858 -0
- package/src/lib/server/personal-data-export.js.map +1 -0
- package/src/lib/server/platform-domain-names.d.ts +62 -0
- package/src/lib/server/platform-domain-names.js +197 -0
- package/src/lib/server/platform-domain-names.js.map +1 -0
- package/src/lib/server/platform-marketing-consent.d.ts +133 -0
- package/src/lib/server/platform-marketing-consent.js +209 -0
- package/src/lib/server/platform-marketing-consent.js.map +1 -0
- package/src/lib/server/rate-limit-store.d.ts +702 -0
- package/src/lib/server/rate-limit-store.js +1220 -0
- package/src/lib/server/rate-limit-store.js.map +1 -0
- package/src/lib/server/realm-plugins.d.ts +67 -0
- package/src/lib/server/realm-plugins.js +250 -0
- package/src/lib/server/realm-plugins.js.map +1 -0
- package/src/lib/server/recaptcha-allowlist.d.ts +90 -0
- package/src/lib/server/recaptcha-allowlist.js +409 -0
- package/src/lib/server/recaptcha-allowlist.js.map +1 -0
- package/src/lib/server/release-flags.d.ts +105 -0
- package/src/lib/server/release-flags.js +183 -0
- package/src/lib/server/release-flags.js.map +1 -0
- package/src/lib/server/replay-erasures.d.ts +126 -0
- package/src/lib/server/replay-erasures.js +217 -0
- package/src/lib/server/replay-erasures.js.map +1 -0
- package/src/lib/server/resolve-people.d.ts +66 -0
- package/src/lib/server/resolve-people.js +112 -0
- package/src/lib/server/resolve-people.js.map +1 -0
- package/src/lib/server/safe-equal.d.ts +45 -0
- package/src/lib/server/safe-equal.js +58 -0
- package/src/lib/server/safe-equal.js.map +1 -0
- package/src/lib/server/sending-domain-debt.d.ts +127 -0
- package/src/lib/server/sending-domain-debt.js +268 -0
- package/src/lib/server/sending-domain-debt.js.map +1 -0
- package/src/lib/server/sending-domain-recheck.d.ts +151 -0
- package/src/lib/server/sending-domain-recheck.js +253 -0
- package/src/lib/server/sending-domain-recheck.js.map +1 -0
- package/src/lib/server/sending-domains.d.ts +364 -0
- package/src/lib/server/sending-domains.js +683 -0
- package/src/lib/server/sending-domains.js.map +1 -0
- package/src/lib/server/serve-media-cdn.d.ts +433 -0
- package/src/lib/server/serve-media-cdn.js +1370 -0
- package/src/lib/server/serve-media-cdn.js.map +1 -0
- package/src/lib/server/serve-plugin-fetch.d.ts +61 -0
- package/src/lib/server/serve-plugin-fetch.js +294 -0
- package/src/lib/server/serve-plugin-fetch.js.map +1 -0
- package/src/lib/server/sms-keywords.d.ts +63 -0
- package/src/lib/server/sms-keywords.js +146 -0
- package/src/lib/server/sms-keywords.js.map +1 -0
- package/src/lib/server/sso-break-glass-owners.d.ts +91 -0
- package/src/lib/server/sso-break-glass-owners.js +227 -0
- package/src/lib/server/sso-break-glass-owners.js.map +1 -0
- package/src/lib/server/sso-domain-policy.d.ts +140 -0
- package/src/lib/server/sso-domain-policy.js +239 -0
- package/src/lib/server/sso-domain-policy.js.map +1 -0
- package/src/lib/server/sso-drift-logic.d.ts +106 -0
- package/src/lib/server/sso-drift-logic.js +95 -0
- package/src/lib/server/sso-drift-logic.js.map +1 -0
- package/src/lib/server/sso-enforcement.d.ts +204 -0
- package/src/lib/server/sso-enforcement.js +309 -0
- package/src/lib/server/sso-enforcement.js.map +1 -0
- package/src/lib/server/sso-provisioning.d.ts +320 -0
- package/src/lib/server/sso-provisioning.js +715 -0
- package/src/lib/server/sso-provisioning.js.map +1 -0
- package/src/lib/server/staff-alert-email.d.ts +41 -0
- package/src/lib/server/staff-alert-email.js +63 -0
- package/src/lib/server/staff-alert-email.js.map +1 -0
- package/src/lib/server/stripe-account-mode.d.ts +156 -0
- package/src/lib/server/stripe-account-mode.js +162 -0
- package/src/lib/server/stripe-account-mode.js.map +1 -0
- package/src/lib/server/tenant-write-lockdown.d.ts +131 -0
- package/src/lib/server/tenant-write-lockdown.js +176 -0
- package/src/lib/server/tenant-write-lockdown.js.map +1 -0
- package/src/lib/server/test-firestore.d.ts +45 -0
- package/src/lib/server/test-firestore.js +124 -0
- package/src/lib/server/test-firestore.js.map +1 -0
- package/src/lib/server/token-revocation.d.ts +140 -0
- package/src/lib/server/token-revocation.js +255 -0
- package/src/lib/server/token-revocation.js.map +1 -0
- package/src/lib/server/update-existing.d.ts +32 -0
- package/src/lib/server/update-existing.js +73 -0
- package/src/lib/server/update-existing.js.map +1 -0
- package/src/lib/server/upload-cors-reconcile.d.ts +118 -0
- package/src/lib/server/upload-cors-reconcile.js +234 -0
- package/src/lib/server/upload-cors-reconcile.js.map +1 -0
- package/src/lib/server/upsert-contact.d.ts +262 -0
- package/src/lib/server/upsert-contact.js +546 -0
- package/src/lib/server/upsert-contact.js.map +1 -0
- package/src/lib/server/usage-invoice.d.ts +203 -0
- package/src/lib/server/usage-invoice.js +397 -0
- package/src/lib/server/usage-invoice.js.map +1 -0
- package/src/lib/server/user-profiles.d.ts +151 -0
- package/src/lib/server/user-profiles.js +364 -0
- package/src/lib/server/user-profiles.js.map +1 -0
- package/src/lib/server/vercel-drain-signature.d.ts +30 -0
- package/src/lib/server/vercel-drain-signature.js +61 -0
- package/src/lib/server/vercel-drain-signature.js.map +1 -0
- package/src/lib/server/vercel-log-drain.d.ts +259 -0
- package/src/lib/server/vercel-log-drain.js +479 -0
- package/src/lib/server/vercel-log-drain.js.map +1 -0
- package/src/lib/server/verify-email-cooldown.d.ts +102 -0
- package/src/lib/server/verify-email-cooldown.js +114 -0
- package/src/lib/server/verify-email-cooldown.js.map +1 -0
- package/src/lib/server/visitor-write-rate-limit.d.ts +87 -0
- package/src/lib/server/visitor-write-rate-limit.js +117 -0
- package/src/lib/server/visitor-write-rate-limit.js.map +1 -0
- package/src/lib/server/workspace-domains.d.ts +156 -0
- package/src/lib/server/workspace-domains.js +271 -0
- package/src/lib/server/workspace-domains.js.map +1 -0
- package/src/tenancy.d.ts +115 -0
- package/src/tenancy.js +142 -0
- package/src/tenancy.js.map +1 -0
|
@@ -0,0 +1,214 @@
|
|
|
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
|
+
/**
|
|
18
|
+
* A document as it was PRESENTED.
|
|
19
|
+
*
|
|
20
|
+
* The URL alone is worthless as evidence: it is mutable, and answers "what
|
|
21
|
+
* does that page say today?" rather than "what did this person see?". The
|
|
22
|
+
* `sha256` is what makes the record self-contained — it pins the exact text,
|
|
23
|
+
* so the archived snapshot can be proved to be the one that was accepted and
|
|
24
|
+
* any later edit to it is detectable instead of silent.
|
|
25
|
+
*/
|
|
26
|
+
export interface LegalDocumentReference {
|
|
27
|
+
/** Stable key, e.g. `terms`, `privacy`. */
|
|
28
|
+
key: string;
|
|
29
|
+
/** The absolute URL rendered in the consent control. */
|
|
30
|
+
url: string;
|
|
31
|
+
/** SHA-256 of the document text as published at this version. */
|
|
32
|
+
sha256?: string;
|
|
33
|
+
/** Byte length of that same text. */
|
|
34
|
+
bytes?: number;
|
|
35
|
+
}
|
|
36
|
+
export interface RecordLegalAcceptanceInput {
|
|
37
|
+
/** The document-set version presented. Becomes the document id. */
|
|
38
|
+
version: string;
|
|
39
|
+
/** Exactly what was linked, so the record pins content and not just a date. */
|
|
40
|
+
documents: LegalDocumentReference[];
|
|
41
|
+
/** Which door the acceptance came through, e.g. `signup-password`. */
|
|
42
|
+
context: string;
|
|
43
|
+
/** Standard clickwrap evidence; both are best-effort. */
|
|
44
|
+
ipAddress?: string | null;
|
|
45
|
+
userAgent?: string | null;
|
|
46
|
+
/** Injectable for tests; defaults to the admin app's Firestore. */
|
|
47
|
+
firestore?: any;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Write the acceptance record, once, for `version`.
|
|
51
|
+
*
|
|
52
|
+
* @returns `recorded: false` when a record for this version already existed —
|
|
53
|
+
* a normal outcome (a retry, or a second sign-up attempt on an account that
|
|
54
|
+
* already agreed), not an error.
|
|
55
|
+
*/
|
|
56
|
+
export declare function recordLegalAcceptance(uid: string, input: RecordLegalAcceptanceInput): Promise<{
|
|
57
|
+
recorded: boolean;
|
|
58
|
+
version: string;
|
|
59
|
+
}>;
|
|
60
|
+
export default recordLegalAcceptance;
|
|
61
|
+
/**
|
|
62
|
+
* ToS §18.5: written notice to legal@aglyn.com "within 30 days of first
|
|
63
|
+
* accepting these Terms".
|
|
64
|
+
*
|
|
65
|
+
* Pinned here, and pinned AGAINST THE PUBLISHED TEXT by
|
|
66
|
+
* `apps/console/specs/legal-acceptance-optout-window.spec.ts`, which re-reads
|
|
67
|
+
* the number out of the snapshot for the current version. A window that
|
|
68
|
+
* silently disagreed with the document it implements would compute a
|
|
69
|
+
* confident wrong answer, which is worse than computing none.
|
|
70
|
+
*/
|
|
71
|
+
export declare const ARBITRATION_OPT_OUT_DAYS = 30;
|
|
72
|
+
/**
|
|
73
|
+
* The numeric rank of a `v<N>` version, or `null` when the id is not of that
|
|
74
|
+
* shape. `apps/console/specs/legal-document-version.spec.ts` already requires
|
|
75
|
+
* `/^v\d+$/` of the live constant, so `null` here means a stored record from
|
|
76
|
+
* some other scheme — not a normal case, and deliberately not guessed at.
|
|
77
|
+
*/
|
|
78
|
+
export declare function parseLegalDocumentVersion(version: string): number | null;
|
|
79
|
+
/**
|
|
80
|
+
* Order two version ids: negative when `a` is older, positive when newer, 0
|
|
81
|
+
* when equal.
|
|
82
|
+
*
|
|
83
|
+
* An unparseable id sorts OLDER than any `v<N>`, so an unrecognised stored
|
|
84
|
+
* version can never suppress a re-acceptance prompt by looking newest. Two
|
|
85
|
+
* unparseable ids fall back to a string compare, which is arbitrary but
|
|
86
|
+
* stable — the alternative is a comparator that is not a total order, and
|
|
87
|
+
* `Array#sort` with one of those produces a different answer per engine.
|
|
88
|
+
*/
|
|
89
|
+
export declare function compareLegalDocumentVersions(a: string, b: string): number;
|
|
90
|
+
/** One stored acceptance, flattened for transport to a surface. */
|
|
91
|
+
export interface StoredLegalAcceptance {
|
|
92
|
+
version: string;
|
|
93
|
+
/** ISO-8601, from the SERVER timestamp. Null only if the write was partial. */
|
|
94
|
+
acceptedAt: string | null;
|
|
95
|
+
/** The door it came through, e.g. `signup-password`, `reaccept-console`. */
|
|
96
|
+
context: string | null;
|
|
97
|
+
method: string | null;
|
|
98
|
+
ipAddress: string | null;
|
|
99
|
+
userAgent: string | null;
|
|
100
|
+
/** What was presented, with the content hashes that pin it. */
|
|
101
|
+
documents: LegalDocumentReference[];
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* The §18.5 clock. Measured from the FIRST acceptance of ANY version, not
|
|
105
|
+
* from the current one: the clause says "first accepting these Terms", and a
|
|
106
|
+
* window that restarted on every re-acceptance would hand a returning
|
|
107
|
+
* customer a right the document does not give them — and would silently take
|
|
108
|
+
* one away in the other direction if it were measured from the newest.
|
|
109
|
+
*/
|
|
110
|
+
export interface ArbitrationOptOutWindow {
|
|
111
|
+
/** ISO of the earliest acceptance on file; null when there is none. */
|
|
112
|
+
firstAcceptedAt: string | null;
|
|
113
|
+
/** ISO of the last instant notice may be sent; null when unmeasurable. */
|
|
114
|
+
deadline: string | null;
|
|
115
|
+
/**
|
|
116
|
+
* Null — NOT false — when there is nothing to measure from. "We hold no
|
|
117
|
+
* acceptance" and "the window has closed" are different answers to a
|
|
118
|
+
* dispute, and collapsing them is how a staff surface asserts something
|
|
119
|
+
* nobody verified.
|
|
120
|
+
*/
|
|
121
|
+
open: boolean | null;
|
|
122
|
+
/** Whole days left, 0 once closed; null when unmeasurable. */
|
|
123
|
+
daysRemaining: number | null;
|
|
124
|
+
}
|
|
125
|
+
export type LegalReacceptanceReason =
|
|
126
|
+
/** Current version is on file — nothing to ask for. */
|
|
127
|
+
'none'
|
|
128
|
+
/** No acceptance of any version. Pre-AGL-1497 accounts, and SSO/invite doors. */
|
|
129
|
+
| 'never-accepted'
|
|
130
|
+
/** They accepted, but an older version than the one published now. */
|
|
131
|
+
| 'version-superseded';
|
|
132
|
+
export interface LegalAcceptanceStatus {
|
|
133
|
+
/** The version the caller says is current — echoed so a surface can show it. */
|
|
134
|
+
currentVersion: string;
|
|
135
|
+
/** True when THIS version is on file. */
|
|
136
|
+
accepted: boolean;
|
|
137
|
+
/** Every accepted version, oldest first. */
|
|
138
|
+
acceptedVersions: string[];
|
|
139
|
+
latestAcceptedVersion: string | null;
|
|
140
|
+
/** ISO of the acceptance of `currentVersion`, when there is one. */
|
|
141
|
+
currentVersionAcceptedAt: string | null;
|
|
142
|
+
/**
|
|
143
|
+
* ISO of the acceptance of `latestAcceptedVersion` — WHEN this person last
|
|
144
|
+
* agreed to anything (2026-08-24).
|
|
145
|
+
*
|
|
146
|
+
* `currentVersionAcceptedAt` beside it is null in exactly the case a
|
|
147
|
+
* re-acceptance banner renders, so it cannot answer "when did you last
|
|
148
|
+
* agree?" for the person being asked. Null when there is no acceptance, and
|
|
149
|
+
* ALSO null when the record's own `acceptedAt` never landed — a surface
|
|
150
|
+
* MUST branch on it rather than formatting it, or it prints a date that
|
|
151
|
+
* does not exist.
|
|
152
|
+
*/
|
|
153
|
+
latestAcceptedAt: string | null;
|
|
154
|
+
/**
|
|
155
|
+
* Which pinned documents actually MOVED between `latestAcceptedVersion` and
|
|
156
|
+
* the version published now — derived by comparing the `sha256` on the
|
|
157
|
+
* stored record against the current manifest, which is the only "what
|
|
158
|
+
* changed" signal that exists. There is no changelog mechanism, and none is
|
|
159
|
+
* invented here.
|
|
160
|
+
*
|
|
161
|
+
* NULL means UNKNOWN — no current manifest was supplied, there is nothing
|
|
162
|
+
* accepted to compare against, or a hash is missing on one of the two
|
|
163
|
+
* sides. That third state is deliberate: an empty array asserts "nothing
|
|
164
|
+
* changed", which is a claim, and a comparison that could not run must not
|
|
165
|
+
* make it.
|
|
166
|
+
*/
|
|
167
|
+
changedDocumentKeys: string[] | null;
|
|
168
|
+
reacceptanceRequired: boolean;
|
|
169
|
+
reacceptanceReason: LegalReacceptanceReason;
|
|
170
|
+
arbitration: ArbitrationOptOutWindow;
|
|
171
|
+
/** The full history, oldest first — the evidence a dispute is answered from. */
|
|
172
|
+
acceptances: StoredLegalAcceptance[];
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Read the whole (tiny) acceptance history for one user.
|
|
176
|
+
*
|
|
177
|
+
* A plain collection `get()` with NO `orderBy` and NO `where`, sorted in
|
|
178
|
+
* memory. That is not laziness: a query with an order or a filter is the kind
|
|
179
|
+
* that needs an index, indexes here deploy by hand, and a read that throws
|
|
180
|
+
* `FAILED_PRECONDITION` on the one page a dispute is answered from is the
|
|
181
|
+
* worst possible failure for this feature. One document per version means the
|
|
182
|
+
* collection is single digits, so the sort is free.
|
|
183
|
+
*/
|
|
184
|
+
export declare function readLegalAcceptances(uid: string, options?: {
|
|
185
|
+
firestore?: any;
|
|
186
|
+
}): Promise<StoredLegalAcceptance[]>;
|
|
187
|
+
/**
|
|
188
|
+
* Turn a history into the two answers AGL-2316 asks for: did this person
|
|
189
|
+
* accept and which version, and is the §18.5 window still open.
|
|
190
|
+
*
|
|
191
|
+
* Pure, and separated from the read so both boundaries are testable without a
|
|
192
|
+
* Firestore double.
|
|
193
|
+
*/
|
|
194
|
+
export declare function evaluateLegalAcceptance(input: {
|
|
195
|
+
acceptances: StoredLegalAcceptance[];
|
|
196
|
+
currentVersion: string;
|
|
197
|
+
/** Injectable for tests; defaults to now. */
|
|
198
|
+
now?: Date;
|
|
199
|
+
optOutDays?: number;
|
|
200
|
+
/**
|
|
201
|
+
* The manifest published NOW. Optional, and its absence is why
|
|
202
|
+
* `changedDocumentKeys` has an UNKNOWN state rather than defaulting to
|
|
203
|
+
* "nothing changed".
|
|
204
|
+
*/
|
|
205
|
+
currentDocuments?: LegalDocumentReference[];
|
|
206
|
+
}): LegalAcceptanceStatus;
|
|
207
|
+
/** Read + evaluate in one call — what every surface actually wants. */
|
|
208
|
+
export declare function getLegalAcceptanceStatus(uid: string, options: {
|
|
209
|
+
currentVersion: string;
|
|
210
|
+
firestore?: any;
|
|
211
|
+
now?: Date;
|
|
212
|
+
/** See `evaluateLegalAcceptance`. Needed for `changedDocumentKeys`. */
|
|
213
|
+
currentDocuments?: LegalDocumentReference[];
|
|
214
|
+
}): Promise<LegalAcceptanceStatus>;
|
|
@@ -0,0 +1,305 @@
|
|
|
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 clickwrap acceptance record, `users/{uid}/legalAcceptances/{version}`
|
|
18
|
+
* (AGL-1497).
|
|
19
|
+
*
|
|
20
|
+
* WHY A COLLECTION, AND WHY UNDER THE USER
|
|
21
|
+
*
|
|
22
|
+
* Acceptance is a fact about a HUMAN, not about an org. An org has many
|
|
23
|
+
* members and they arrive by different doors — the founder who created it, a
|
|
24
|
+
* teammate who accepted an invite, and a site collaborator who is created with
|
|
25
|
+
* org `role: 'viewer'` and may never touch the org's billing at all. "The org
|
|
26
|
+
* accepted" and "this person accepted" are different statements, and only the
|
|
27
|
+
* second one is a contract with the person in front of you. Storing it on
|
|
28
|
+
* `orgs/{id}` would make the first stand in for the second, and storing it as
|
|
29
|
+
* a field on `users/{uid}` would keep only the latest answer.
|
|
30
|
+
*
|
|
31
|
+
* One document PER VERSION, so the history is additive: when the Terms change
|
|
32
|
+
* — and ToS §5.3 expressly reserves that right — a re-acceptance ADDS a
|
|
33
|
+
* document instead of overwriting the evidence of what was agreed before it.
|
|
34
|
+
* The version is the document id, which makes the write idempotent for free:
|
|
35
|
+
* a retried request cannot produce a second record of the same acceptance.
|
|
36
|
+
*
|
|
37
|
+
* IMMUTABILITY. The first acceptance of a version wins and is never rewritten.
|
|
38
|
+
* ToS §18.5 gives a 30-day window to opt out of arbitration measured from
|
|
39
|
+
* FIRST accepting, so that timestamp decides a real right — a later write that
|
|
40
|
+
* moved it would silently restart somebody's clock.
|
|
41
|
+
*
|
|
42
|
+
* The timestamp is the SERVER's. A record whose time the client supplied is
|
|
43
|
+
* worth very little in the dispute it exists for.
|
|
44
|
+
*
|
|
45
|
+
* Firestore rules make this collection owner-readable and `write: if false`,
|
|
46
|
+
* so it is Admin-SDK-only: the person the record is about cannot forge, amend
|
|
47
|
+
* or delete their own acceptance.
|
|
48
|
+
*/ import { FieldValue } from "firebase-admin/firestore";
|
|
49
|
+
import firebaseAdmin from "./firebase-admin.js";
|
|
50
|
+
const firestore = ()=>firebaseAdmin.app().firestore();
|
|
51
|
+
/**
|
|
52
|
+
* Write the acceptance record, once, for `version`.
|
|
53
|
+
*
|
|
54
|
+
* @returns `recorded: false` when a record for this version already existed —
|
|
55
|
+
* a normal outcome (a retry, or a second sign-up attempt on an account that
|
|
56
|
+
* already agreed), not an error.
|
|
57
|
+
*/ export async function recordLegalAcceptance(uid, input) {
|
|
58
|
+
var _input_version, _input_firestore, _input_documents, _input_context, _input_ipAddress, _input_userAgent;
|
|
59
|
+
const version = String((_input_version = input.version) != null ? _input_version : '').trim();
|
|
60
|
+
// Loud, not best-effort. Everything else on the sign-up path is allowed to
|
|
61
|
+
// fail quietly because the account still exists and the user is signed in —
|
|
62
|
+
// but an acceptance we cannot attribute to a person and a version is not
|
|
63
|
+
// evidence of anything, and silently keeping half of one is worse than
|
|
64
|
+
// failing where somebody can see it.
|
|
65
|
+
if (!uid) throw new Error('recordLegalAcceptance: uid is required');
|
|
66
|
+
if (!version) throw new Error('recordLegalAcceptance: version is required');
|
|
67
|
+
const ref = ((_input_firestore = input.firestore) != null ? _input_firestore : firestore()).collection('users').doc(uid).collection('legalAcceptances').doc(version);
|
|
68
|
+
const snapshot = await ref.get();
|
|
69
|
+
if (snapshot.exists) return {
|
|
70
|
+
recorded: false,
|
|
71
|
+
version
|
|
72
|
+
};
|
|
73
|
+
await ref.set({
|
|
74
|
+
version,
|
|
75
|
+
documents: (_input_documents = input.documents) != null ? _input_documents : [],
|
|
76
|
+
method: 'clickwrap',
|
|
77
|
+
context: (_input_context = input.context) != null ? _input_context : null,
|
|
78
|
+
acceptedAt: FieldValue.serverTimestamp(),
|
|
79
|
+
ipAddress: (_input_ipAddress = input.ipAddress) != null ? _input_ipAddress : null,
|
|
80
|
+
userAgent: (_input_userAgent = input.userAgent) != null ? _input_userAgent : null
|
|
81
|
+
});
|
|
82
|
+
return {
|
|
83
|
+
recorded: true,
|
|
84
|
+
version
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
export default recordLegalAcceptance;
|
|
88
|
+
/* -------------------------------------------------------------------------
|
|
89
|
+
* READING the record (AGL-2316)
|
|
90
|
+
*
|
|
91
|
+
* Everything above writes. Until AGL-2316 nothing read, which made both
|
|
92
|
+
* promises in this module's header undeliverable: the §18.5 opt-out window
|
|
93
|
+
* was unprovable, and "re-acceptance is additive" described a mechanism with
|
|
94
|
+
* no trigger, because nobody ever compared an accepted version against a
|
|
95
|
+
* current one.
|
|
96
|
+
*
|
|
97
|
+
* WHICH STRING IS "THE CURRENT VERSION" — and why nothing new is invented
|
|
98
|
+
* here. `apps/console/constants/legal-documents.ts` has carried
|
|
99
|
+
* `LEGAL_DOCUMENT_VERSION` since AGL-1497; it is `v6` today, it is what the
|
|
100
|
+
* writer route already stamps on every record, and `v1`..`v6` all exist as
|
|
101
|
+
* immutable snapshots under `apps/console/constants/legal/`. AGL-2316 was
|
|
102
|
+
* filed believing no such constant existed — it greps for `LEGAL_VERSION` and
|
|
103
|
+
* `legalVersion`, which is not its name. So the scheme is not a decision; it
|
|
104
|
+
* is an existing, published fact, and re-basing it (on an effective date, say)
|
|
105
|
+
* would orphan every acceptance already written. The constants file argues the
|
|
106
|
+
* date case and rejects it: the masters carry an unfilled `[EFFECTIVE DATE]`,
|
|
107
|
+
* and ToS §5.3 changes the Terms by MOVING the "Last updated" date, so the
|
|
108
|
+
* date is the field that shifts under the identifier rather than being one.
|
|
109
|
+
*
|
|
110
|
+
* The current version is a PARAMETER here rather than an import. This library
|
|
111
|
+
* is consumed by the console and by the tenant app, and the constant plus its
|
|
112
|
+
* snapshots live in the console; passing it in keeps the comparison logic on
|
|
113
|
+
* the server library and the published fact in the one place a publish edits.
|
|
114
|
+
* ------------------------------------------------------------------------- */ /**
|
|
115
|
+
* ToS §18.5: written notice to legal@aglyn.com "within 30 days of first
|
|
116
|
+
* accepting these Terms".
|
|
117
|
+
*
|
|
118
|
+
* Pinned here, and pinned AGAINST THE PUBLISHED TEXT by
|
|
119
|
+
* `apps/console/specs/legal-acceptance-optout-window.spec.ts`, which re-reads
|
|
120
|
+
* the number out of the snapshot for the current version. A window that
|
|
121
|
+
* silently disagreed with the document it implements would compute a
|
|
122
|
+
* confident wrong answer, which is worse than computing none.
|
|
123
|
+
*/ export const ARBITRATION_OPT_OUT_DAYS = 30;
|
|
124
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
125
|
+
/**
|
|
126
|
+
* The numeric rank of a `v<N>` version, or `null` when the id is not of that
|
|
127
|
+
* shape. `apps/console/specs/legal-document-version.spec.ts` already requires
|
|
128
|
+
* `/^v\d+$/` of the live constant, so `null` here means a stored record from
|
|
129
|
+
* some other scheme — not a normal case, and deliberately not guessed at.
|
|
130
|
+
*/ export function parseLegalDocumentVersion(version) {
|
|
131
|
+
const match = /^v(\d+)$/.exec(String(version != null ? version : '').trim());
|
|
132
|
+
if (!match) return null;
|
|
133
|
+
const rank = Number(match[1]);
|
|
134
|
+
return Number.isFinite(rank) ? rank : null;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Order two version ids: negative when `a` is older, positive when newer, 0
|
|
138
|
+
* when equal.
|
|
139
|
+
*
|
|
140
|
+
* An unparseable id sorts OLDER than any `v<N>`, so an unrecognised stored
|
|
141
|
+
* version can never suppress a re-acceptance prompt by looking newest. Two
|
|
142
|
+
* unparseable ids fall back to a string compare, which is arbitrary but
|
|
143
|
+
* stable — the alternative is a comparator that is not a total order, and
|
|
144
|
+
* `Array#sort` with one of those produces a different answer per engine.
|
|
145
|
+
*/ export function compareLegalDocumentVersions(a, b) {
|
|
146
|
+
const left = parseLegalDocumentVersion(a);
|
|
147
|
+
const right = parseLegalDocumentVersion(b);
|
|
148
|
+
if (left !== null && right !== null) return left - right;
|
|
149
|
+
if (left !== null) return 1;
|
|
150
|
+
if (right !== null) return -1;
|
|
151
|
+
return String(a != null ? a : '').localeCompare(String(b != null ? b : ''));
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Compare the documents pinned on a stored acceptance against the manifest
|
|
155
|
+
* published now, and return the keys whose text moved.
|
|
156
|
+
*
|
|
157
|
+
* Returns null — NOT `[]` — whenever the comparison cannot be made in full.
|
|
158
|
+
*/ function diffPinnedDocuments(accepted, current) {
|
|
159
|
+
var _accepted_documents;
|
|
160
|
+
if (!accepted || !current || current.length === 0) return null;
|
|
161
|
+
const acceptedByKey = new Map();
|
|
162
|
+
for (const doc of (_accepted_documents = accepted.documents) != null ? _accepted_documents : []){
|
|
163
|
+
if (doc == null ? void 0 : doc.key) acceptedByKey.set(doc.key, doc);
|
|
164
|
+
}
|
|
165
|
+
if (acceptedByKey.size === 0) return null;
|
|
166
|
+
const changed = [];
|
|
167
|
+
for (const doc of current){
|
|
168
|
+
if (!(doc == null ? void 0 : doc.key)) continue;
|
|
169
|
+
const was = acceptedByKey.get(doc.key);
|
|
170
|
+
// A document the old record never carried is new to the set, which IS a
|
|
171
|
+
// change to what is being agreed to.
|
|
172
|
+
if (!was) {
|
|
173
|
+
changed.push(doc.key);
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
// Either side missing a hash makes THIS key unknowable, and one unknown
|
|
177
|
+
// key makes the whole answer unknown — a partial list would read as a
|
|
178
|
+
// complete one.
|
|
179
|
+
if (!was.sha256 || !doc.sha256) return null;
|
|
180
|
+
if (was.sha256 !== doc.sha256) changed.push(doc.key);
|
|
181
|
+
}
|
|
182
|
+
return changed;
|
|
183
|
+
}
|
|
184
|
+
function toIso(value) {
|
|
185
|
+
var _value_toDate;
|
|
186
|
+
if (!value) return null;
|
|
187
|
+
const asDate = value == null ? void 0 : (_value_toDate = value.toDate) == null ? void 0 : _value_toDate.call(value);
|
|
188
|
+
if (asDate instanceof Date && !Number.isNaN(asDate.getTime())) {
|
|
189
|
+
return asDate.toISOString();
|
|
190
|
+
}
|
|
191
|
+
if (value instanceof Date) {
|
|
192
|
+
return Number.isNaN(value.getTime()) ? null : value.toISOString();
|
|
193
|
+
}
|
|
194
|
+
if (typeof value === 'string') {
|
|
195
|
+
const parsed = new Date(value);
|
|
196
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString();
|
|
197
|
+
}
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Read the whole (tiny) acceptance history for one user.
|
|
202
|
+
*
|
|
203
|
+
* A plain collection `get()` with NO `orderBy` and NO `where`, sorted in
|
|
204
|
+
* memory. That is not laziness: a query with an order or a filter is the kind
|
|
205
|
+
* that needs an index, indexes here deploy by hand, and a read that throws
|
|
206
|
+
* `FAILED_PRECONDITION` on the one page a dispute is answered from is the
|
|
207
|
+
* worst possible failure for this feature. One document per version means the
|
|
208
|
+
* collection is single digits, so the sort is free.
|
|
209
|
+
*/ export async function readLegalAcceptances(uid, options = {}) {
|
|
210
|
+
var _options_firestore, _ref;
|
|
211
|
+
if (!uid) throw new Error('readLegalAcceptances: uid is required');
|
|
212
|
+
const snapshot = await ((_options_firestore = options.firestore) != null ? _options_firestore : firestore()).collection('users').doc(uid).collection('legalAcceptances').get();
|
|
213
|
+
const records = ((_ref = snapshot == null ? void 0 : snapshot.docs) != null ? _ref : []).map((doc)=>{
|
|
214
|
+
var _ref, _doc_get, _doc_get1, _doc_get2, _doc_get3, _doc_get4, _doc_get5;
|
|
215
|
+
return {
|
|
216
|
+
// The document id IS the version (see the header). Falling back to it
|
|
217
|
+
// means a record whose `version` field never landed is still usable.
|
|
218
|
+
version: String((_ref = (_doc_get = doc.get('version')) != null ? _doc_get : doc.id) != null ? _ref : ''),
|
|
219
|
+
acceptedAt: toIso(doc.get('acceptedAt')),
|
|
220
|
+
context: (_doc_get1 = doc.get('context')) != null ? _doc_get1 : null,
|
|
221
|
+
method: (_doc_get2 = doc.get('method')) != null ? _doc_get2 : null,
|
|
222
|
+
ipAddress: (_doc_get3 = doc.get('ipAddress')) != null ? _doc_get3 : null,
|
|
223
|
+
userAgent: (_doc_get4 = doc.get('userAgent')) != null ? _doc_get4 : null,
|
|
224
|
+
documents: (_doc_get5 = doc.get('documents')) != null ? _doc_get5 : []
|
|
225
|
+
};
|
|
226
|
+
});
|
|
227
|
+
return records.sort((a, b)=>compareLegalDocumentVersions(a.version, b.version));
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Turn a history into the two answers AGL-2316 asks for: did this person
|
|
231
|
+
* accept and which version, and is the §18.5 window still open.
|
|
232
|
+
*
|
|
233
|
+
* Pure, and separated from the read so both boundaries are testable without a
|
|
234
|
+
* Firestore double.
|
|
235
|
+
*/ export function evaluateLegalAcceptance(input) {
|
|
236
|
+
var _input_currentVersion, _input_acceptances, _input_now, _input_optOutDays, _ref, _ref1, _input_currentDocuments;
|
|
237
|
+
const currentVersion = String((_input_currentVersion = input.currentVersion) != null ? _input_currentVersion : '').trim();
|
|
238
|
+
const acceptances = [
|
|
239
|
+
...(_input_acceptances = input.acceptances) != null ? _input_acceptances : []
|
|
240
|
+
].sort((a, b)=>compareLegalDocumentVersions(a.version, b.version));
|
|
241
|
+
const now = (_input_now = input.now) != null ? _input_now : new Date();
|
|
242
|
+
const optOutDays = (_input_optOutDays = input.optOutDays) != null ? _input_optOutDays : ARBITRATION_OPT_OUT_DAYS;
|
|
243
|
+
const acceptedVersions = acceptances.map((record)=>record.version);
|
|
244
|
+
const latestAcceptedVersion = acceptedVersions.length > 0 ? acceptedVersions[acceptedVersions.length - 1] : null;
|
|
245
|
+
const currentRecord = acceptances.find((record)=>record.version === currentVersion);
|
|
246
|
+
// The record for `latestAcceptedVersion` — the LAST thing this person
|
|
247
|
+
// agreed to, which is what a banner has to acknowledge. `acceptances` is
|
|
248
|
+
// sorted by version rank above, so this is its tail.
|
|
249
|
+
const latestRecord = acceptances.length > 0 ? acceptances[acceptances.length - 1] : null;
|
|
250
|
+
// FIRST acceptance by TIME, not by version rank. They usually agree, but a
|
|
251
|
+
// backfill or an out-of-order repair would make the ranks lie, and §18.5's
|
|
252
|
+
// clock is a clock.
|
|
253
|
+
const timestamps = acceptances.map((record)=>record.acceptedAt).filter((iso)=>Boolean(iso)).sort();
|
|
254
|
+
const firstAcceptedAt = timestamps.length > 0 ? timestamps[0] : null;
|
|
255
|
+
let arbitration = {
|
|
256
|
+
firstAcceptedAt: null,
|
|
257
|
+
deadline: null,
|
|
258
|
+
open: null,
|
|
259
|
+
daysRemaining: null
|
|
260
|
+
};
|
|
261
|
+
if (firstAcceptedAt) {
|
|
262
|
+
const deadlineMs = new Date(firstAcceptedAt).getTime() + optOutDays * DAY_MS;
|
|
263
|
+
const remainingMs = deadlineMs - now.getTime();
|
|
264
|
+
arbitration = {
|
|
265
|
+
firstAcceptedAt,
|
|
266
|
+
deadline: new Date(deadlineMs).toISOString(),
|
|
267
|
+
// INCLUSIVE of the deadline instant. "within 30 days of first
|
|
268
|
+
// accepting" includes the thirtieth day, and the cheap `<` would close
|
|
269
|
+
// the window a full day early for anyone who read the clause literally.
|
|
270
|
+
open: remainingMs >= 0,
|
|
271
|
+
daysRemaining: remainingMs > 0 ? Math.ceil(remainingMs / DAY_MS) : 0
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
let reacceptanceReason = 'none';
|
|
275
|
+
if (acceptances.length === 0) reacceptanceReason = 'never-accepted';
|
|
276
|
+
else if (currentVersion && compareLegalDocumentVersions(currentVersion, latestAcceptedVersion) > 0) {
|
|
277
|
+
reacceptanceReason = 'version-superseded';
|
|
278
|
+
}
|
|
279
|
+
return {
|
|
280
|
+
currentVersion,
|
|
281
|
+
accepted: Boolean(currentRecord),
|
|
282
|
+
acceptedVersions,
|
|
283
|
+
latestAcceptedVersion,
|
|
284
|
+
currentVersionAcceptedAt: (_ref = currentRecord == null ? void 0 : currentRecord.acceptedAt) != null ? _ref : null,
|
|
285
|
+
latestAcceptedAt: (_ref1 = latestRecord == null ? void 0 : latestRecord.acceptedAt) != null ? _ref1 : null,
|
|
286
|
+
changedDocumentKeys: diffPinnedDocuments(latestRecord, (_input_currentDocuments = input.currentDocuments) != null ? _input_currentDocuments : null),
|
|
287
|
+
reacceptanceRequired: reacceptanceReason !== 'none',
|
|
288
|
+
reacceptanceReason,
|
|
289
|
+
arbitration,
|
|
290
|
+
acceptances
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
/** Read + evaluate in one call — what every surface actually wants. */ export async function getLegalAcceptanceStatus(uid, options) {
|
|
294
|
+
const acceptances = await readLegalAcceptances(uid, {
|
|
295
|
+
firestore: options.firestore
|
|
296
|
+
});
|
|
297
|
+
return evaluateLegalAcceptance({
|
|
298
|
+
acceptances,
|
|
299
|
+
currentVersion: options.currentVersion,
|
|
300
|
+
now: options.now,
|
|
301
|
+
currentDocuments: options.currentDocuments
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
//# sourceMappingURL=legal-acceptance.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/legal-acceptance.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * The clickwrap acceptance record, `users/{uid}/legalAcceptances/{version}`\n * (AGL-1497).\n *\n * WHY A COLLECTION, AND WHY UNDER THE USER\n *\n * Acceptance is a fact about a HUMAN, not about an org. An org has many\n * members and they arrive by different doors — the founder who created it, a\n * teammate who accepted an invite, and a site collaborator who is created with\n * org `role: 'viewer'` and may never touch the org's billing at all. \"The org\n * accepted\" and \"this person accepted\" are different statements, and only the\n * second one is a contract with the person in front of you. Storing it on\n * `orgs/{id}` would make the first stand in for the second, and storing it as\n * a field on `users/{uid}` would keep only the latest answer.\n *\n * One document PER VERSION, so the history is additive: when the Terms change\n * — and ToS §5.3 expressly reserves that right — a re-acceptance ADDS a\n * document instead of overwriting the evidence of what was agreed before it.\n * The version is the document id, which makes the write idempotent for free:\n * a retried request cannot produce a second record of the same acceptance.\n *\n * IMMUTABILITY. The first acceptance of a version wins and is never rewritten.\n * ToS §18.5 gives a 30-day window to opt out of arbitration measured from\n * FIRST accepting, so that timestamp decides a real right — a later write that\n * moved it would silently restart somebody's clock.\n *\n * The timestamp is the SERVER's. A record whose time the client supplied is\n * worth very little in the dispute it exists for.\n *\n * Firestore rules make this collection owner-readable and `write: if false`,\n * so it is Admin-SDK-only: the person the record is about cannot forge, amend\n * or delete their own acceptance.\n */\n\nimport { FieldValue } from 'firebase-admin/firestore'\nimport firebaseAdmin from './firebase-admin'\n\nconst firestore = () => firebaseAdmin.app().firestore()\n\n/**\n * A document as it was PRESENTED.\n *\n * The URL alone is worthless as evidence: it is mutable, and answers \"what\n * does that page say today?\" rather than \"what did this person see?\". The\n * `sha256` is what makes the record self-contained — it pins the exact text,\n * so the archived snapshot can be proved to be the one that was accepted and\n * any later edit to it is detectable instead of silent.\n */\nexport interface LegalDocumentReference {\n /** Stable key, e.g. `terms`, `privacy`. */\n key: string\n /** The absolute URL rendered in the consent control. */\n url: string\n /** SHA-256 of the document text as published at this version. */\n sha256?: string\n /** Byte length of that same text. */\n bytes?: number\n}\n\nexport interface RecordLegalAcceptanceInput {\n /** The document-set version presented. Becomes the document id. */\n version: string\n /** Exactly what was linked, so the record pins content and not just a date. */\n documents: LegalDocumentReference[]\n /** Which door the acceptance came through, e.g. `signup-password`. */\n context: string\n /** Standard clickwrap evidence; both are best-effort. */\n ipAddress?: string | null\n userAgent?: string | null\n /** Injectable for tests; defaults to the admin app's Firestore. */\n firestore?: any\n}\n\n/**\n * Write the acceptance record, once, for `version`.\n *\n * @returns `recorded: false` when a record for this version already existed —\n * a normal outcome (a retry, or a second sign-up attempt on an account that\n * already agreed), not an error.\n */\nexport async function recordLegalAcceptance(\n uid: string,\n input: RecordLegalAcceptanceInput,\n): Promise<{ recorded: boolean; version: string }> {\n const version = String(input.version ?? '').trim()\n // Loud, not best-effort. Everything else on the sign-up path is allowed to\n // fail quietly because the account still exists and the user is signed in —\n // but an acceptance we cannot attribute to a person and a version is not\n // evidence of anything, and silently keeping half of one is worse than\n // failing where somebody can see it.\n if (!uid) throw new Error('recordLegalAcceptance: uid is required')\n if (!version) throw new Error('recordLegalAcceptance: version is required')\n\n const ref = (input.firestore ?? firestore())\n .collection('users')\n .doc(uid)\n .collection('legalAcceptances')\n .doc(version)\n\n const snapshot = await ref.get()\n if (snapshot.exists) return { recorded: false, version }\n\n await ref.set({\n version,\n documents: input.documents ?? [],\n method: 'clickwrap',\n context: input.context ?? null,\n acceptedAt: FieldValue.serverTimestamp(),\n ipAddress: input.ipAddress ?? null,\n userAgent: input.userAgent ?? null,\n })\n return { recorded: true, version }\n}\n\nexport default recordLegalAcceptance\n\n/* -------------------------------------------------------------------------\n * READING the record (AGL-2316)\n *\n * Everything above writes. Until AGL-2316 nothing read, which made both\n * promises in this module's header undeliverable: the §18.5 opt-out window\n * was unprovable, and \"re-acceptance is additive\" described a mechanism with\n * no trigger, because nobody ever compared an accepted version against a\n * current one.\n *\n * WHICH STRING IS \"THE CURRENT VERSION\" — and why nothing new is invented\n * here. `apps/console/constants/legal-documents.ts` has carried\n * `LEGAL_DOCUMENT_VERSION` since AGL-1497; it is `v6` today, it is what the\n * writer route already stamps on every record, and `v1`..`v6` all exist as\n * immutable snapshots under `apps/console/constants/legal/`. AGL-2316 was\n * filed believing no such constant existed — it greps for `LEGAL_VERSION` and\n * `legalVersion`, which is not its name. So the scheme is not a decision; it\n * is an existing, published fact, and re-basing it (on an effective date, say)\n * would orphan every acceptance already written. The constants file argues the\n * date case and rejects it: the masters carry an unfilled `[EFFECTIVE DATE]`,\n * and ToS §5.3 changes the Terms by MOVING the \"Last updated\" date, so the\n * date is the field that shifts under the identifier rather than being one.\n *\n * The current version is a PARAMETER here rather than an import. This library\n * is consumed by the console and by the tenant app, and the constant plus its\n * snapshots live in the console; passing it in keeps the comparison logic on\n * the server library and the published fact in the one place a publish edits.\n * ------------------------------------------------------------------------- */\n\n/**\n * ToS §18.5: written notice to legal@aglyn.com \"within 30 days of first\n * accepting these Terms\".\n *\n * Pinned here, and pinned AGAINST THE PUBLISHED TEXT by\n * `apps/console/specs/legal-acceptance-optout-window.spec.ts`, which re-reads\n * the number out of the snapshot for the current version. A window that\n * silently disagreed with the document it implements would compute a\n * confident wrong answer, which is worse than computing none.\n */\nexport const ARBITRATION_OPT_OUT_DAYS = 30\n\nconst DAY_MS = 24 * 60 * 60 * 1000\n\n/**\n * The numeric rank of a `v<N>` version, or `null` when the id is not of that\n * shape. `apps/console/specs/legal-document-version.spec.ts` already requires\n * `/^v\\d+$/` of the live constant, so `null` here means a stored record from\n * some other scheme — not a normal case, and deliberately not guessed at.\n */\nexport function parseLegalDocumentVersion(version: string): number | null {\n const match = /^v(\\d+)$/.exec(String(version ?? '').trim())\n if (!match) return null\n const rank = Number(match[1])\n return Number.isFinite(rank) ? rank : null\n}\n\n/**\n * Order two version ids: negative when `a` is older, positive when newer, 0\n * when equal.\n *\n * An unparseable id sorts OLDER than any `v<N>`, so an unrecognised stored\n * version can never suppress a re-acceptance prompt by looking newest. Two\n * unparseable ids fall back to a string compare, which is arbitrary but\n * stable — the alternative is a comparator that is not a total order, and\n * `Array#sort` with one of those produces a different answer per engine.\n */\nexport function compareLegalDocumentVersions(a: string, b: string): number {\n const left = parseLegalDocumentVersion(a)\n const right = parseLegalDocumentVersion(b)\n if (left !== null && right !== null) return left - right\n if (left !== null) return 1\n if (right !== null) return -1\n return String(a ?? '').localeCompare(String(b ?? ''))\n}\n\n/** One stored acceptance, flattened for transport to a surface. */\nexport interface StoredLegalAcceptance {\n version: string\n /** ISO-8601, from the SERVER timestamp. Null only if the write was partial. */\n acceptedAt: string | null\n /** The door it came through, e.g. `signup-password`, `reaccept-console`. */\n context: string | null\n method: string | null\n ipAddress: string | null\n userAgent: string | null\n /** What was presented, with the content hashes that pin it. */\n documents: LegalDocumentReference[]\n}\n\n/**\n * The §18.5 clock. Measured from the FIRST acceptance of ANY version, not\n * from the current one: the clause says \"first accepting these Terms\", and a\n * window that restarted on every re-acceptance would hand a returning\n * customer a right the document does not give them — and would silently take\n * one away in the other direction if it were measured from the newest.\n */\nexport interface ArbitrationOptOutWindow {\n /** ISO of the earliest acceptance on file; null when there is none. */\n firstAcceptedAt: string | null\n /** ISO of the last instant notice may be sent; null when unmeasurable. */\n deadline: string | null\n /**\n * Null — NOT false — when there is nothing to measure from. \"We hold no\n * acceptance\" and \"the window has closed\" are different answers to a\n * dispute, and collapsing them is how a staff surface asserts something\n * nobody verified.\n */\n open: boolean | null\n /** Whole days left, 0 once closed; null when unmeasurable. */\n daysRemaining: number | null\n}\n\nexport type LegalReacceptanceReason =\n /** Current version is on file — nothing to ask for. */\n | 'none'\n /** No acceptance of any version. Pre-AGL-1497 accounts, and SSO/invite doors. */\n | 'never-accepted'\n /** They accepted, but an older version than the one published now. */\n | 'version-superseded'\n\nexport interface LegalAcceptanceStatus {\n /** The version the caller says is current — echoed so a surface can show it. */\n currentVersion: string\n /** True when THIS version is on file. */\n accepted: boolean\n /** Every accepted version, oldest first. */\n acceptedVersions: string[]\n latestAcceptedVersion: string | null\n /** ISO of the acceptance of `currentVersion`, when there is one. */\n currentVersionAcceptedAt: string | null\n /**\n * ISO of the acceptance of `latestAcceptedVersion` — WHEN this person last\n * agreed to anything (2026-08-24).\n *\n * `currentVersionAcceptedAt` beside it is null in exactly the case a\n * re-acceptance banner renders, so it cannot answer \"when did you last\n * agree?\" for the person being asked. Null when there is no acceptance, and\n * ALSO null when the record's own `acceptedAt` never landed — a surface\n * MUST branch on it rather than formatting it, or it prints a date that\n * does not exist.\n */\n latestAcceptedAt: string | null\n /**\n * Which pinned documents actually MOVED between `latestAcceptedVersion` and\n * the version published now — derived by comparing the `sha256` on the\n * stored record against the current manifest, which is the only \"what\n * changed\" signal that exists. There is no changelog mechanism, and none is\n * invented here.\n *\n * NULL means UNKNOWN — no current manifest was supplied, there is nothing\n * accepted to compare against, or a hash is missing on one of the two\n * sides. That third state is deliberate: an empty array asserts \"nothing\n * changed\", which is a claim, and a comparison that could not run must not\n * make it.\n */\n changedDocumentKeys: string[] | null\n reacceptanceRequired: boolean\n reacceptanceReason: LegalReacceptanceReason\n arbitration: ArbitrationOptOutWindow\n /** The full history, oldest first — the evidence a dispute is answered from. */\n acceptances: StoredLegalAcceptance[]\n}\n\n/**\n * Compare the documents pinned on a stored acceptance against the manifest\n * published now, and return the keys whose text moved.\n *\n * Returns null — NOT `[]` — whenever the comparison cannot be made in full.\n */\nfunction diffPinnedDocuments(\n accepted: StoredLegalAcceptance | null,\n current: LegalDocumentReference[] | null,\n): string[] | null {\n if (!accepted || !current || current.length === 0) return null\n const acceptedByKey = new Map<string, LegalDocumentReference>()\n for (const doc of accepted.documents ?? []) {\n if (doc?.key) acceptedByKey.set(doc.key, doc)\n }\n if (acceptedByKey.size === 0) return null\n\n const changed: string[] = []\n for (const doc of current) {\n if (!doc?.key) continue\n const was = acceptedByKey.get(doc.key)\n // A document the old record never carried is new to the set, which IS a\n // change to what is being agreed to.\n if (!was) {\n changed.push(doc.key)\n continue\n }\n // Either side missing a hash makes THIS key unknowable, and one unknown\n // key makes the whole answer unknown — a partial list would read as a\n // complete one.\n if (!was.sha256 || !doc.sha256) return null\n if (was.sha256 !== doc.sha256) changed.push(doc.key)\n }\n return changed\n}\n\nfunction toIso(value: unknown): string | null {\n if (!value) return null\n const asDate = (value as { toDate?: () => Date })?.toDate?.()\n if (asDate instanceof Date && !Number.isNaN(asDate.getTime())) {\n return asDate.toISOString()\n }\n if (value instanceof Date) {\n return Number.isNaN(value.getTime()) ? null : value.toISOString()\n }\n if (typeof value === 'string') {\n const parsed = new Date(value)\n return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString()\n }\n return null\n}\n\n/**\n * Read the whole (tiny) acceptance history for one user.\n *\n * A plain collection `get()` with NO `orderBy` and NO `where`, sorted in\n * memory. That is not laziness: a query with an order or a filter is the kind\n * that needs an index, indexes here deploy by hand, and a read that throws\n * `FAILED_PRECONDITION` on the one page a dispute is answered from is the\n * worst possible failure for this feature. One document per version means the\n * collection is single digits, so the sort is free.\n */\nexport async function readLegalAcceptances(\n uid: string,\n options: { firestore?: any } = {},\n): Promise<StoredLegalAcceptance[]> {\n if (!uid) throw new Error('readLegalAcceptances: uid is required')\n const snapshot = await (options.firestore ?? firestore())\n .collection('users')\n .doc(uid)\n .collection('legalAcceptances')\n .get()\n\n const records: StoredLegalAcceptance[] = (snapshot?.docs ?? []).map(\n (doc: any) => ({\n // The document id IS the version (see the header). Falling back to it\n // means a record whose `version` field never landed is still usable.\n version: String(doc.get('version') ?? doc.id ?? ''),\n acceptedAt: toIso(doc.get('acceptedAt')),\n context: (doc.get('context') as string) ?? null,\n method: (doc.get('method') as string) ?? null,\n ipAddress: (doc.get('ipAddress') as string) ?? null,\n userAgent: (doc.get('userAgent') as string) ?? null,\n documents: (doc.get('documents') as LegalDocumentReference[]) ?? [],\n }),\n )\n return records.sort((a, b) =>\n compareLegalDocumentVersions(a.version, b.version),\n )\n}\n\n/**\n * Turn a history into the two answers AGL-2316 asks for: did this person\n * accept and which version, and is the §18.5 window still open.\n *\n * Pure, and separated from the read so both boundaries are testable without a\n * Firestore double.\n */\nexport function evaluateLegalAcceptance(input: {\n acceptances: StoredLegalAcceptance[]\n currentVersion: string\n /** Injectable for tests; defaults to now. */\n now?: Date\n optOutDays?: number\n /**\n * The manifest published NOW. Optional, and its absence is why\n * `changedDocumentKeys` has an UNKNOWN state rather than defaulting to\n * \"nothing changed\".\n */\n currentDocuments?: LegalDocumentReference[]\n}): LegalAcceptanceStatus {\n const currentVersion = String(input.currentVersion ?? '').trim()\n const acceptances = [...(input.acceptances ?? [])].sort((a, b) =>\n compareLegalDocumentVersions(a.version, b.version),\n )\n const now = input.now ?? new Date()\n const optOutDays = input.optOutDays ?? ARBITRATION_OPT_OUT_DAYS\n\n const acceptedVersions = acceptances.map((record) => record.version)\n const latestAcceptedVersion =\n acceptedVersions.length > 0\n ? acceptedVersions[acceptedVersions.length - 1]\n : null\n const currentRecord = acceptances.find(\n (record) => record.version === currentVersion,\n )\n // The record for `latestAcceptedVersion` — the LAST thing this person\n // agreed to, which is what a banner has to acknowledge. `acceptances` is\n // sorted by version rank above, so this is its tail.\n const latestRecord =\n acceptances.length > 0 ? acceptances[acceptances.length - 1] : null\n\n // FIRST acceptance by TIME, not by version rank. They usually agree, but a\n // backfill or an out-of-order repair would make the ranks lie, and §18.5's\n // clock is a clock.\n const timestamps = acceptances\n .map((record) => record.acceptedAt)\n .filter((iso): iso is string => Boolean(iso))\n .sort()\n const firstAcceptedAt = timestamps.length > 0 ? timestamps[0] : null\n\n let arbitration: ArbitrationOptOutWindow = {\n firstAcceptedAt: null,\n deadline: null,\n open: null,\n daysRemaining: null,\n }\n if (firstAcceptedAt) {\n const deadlineMs = new Date(firstAcceptedAt).getTime() + optOutDays * DAY_MS\n const remainingMs = deadlineMs - now.getTime()\n arbitration = {\n firstAcceptedAt,\n deadline: new Date(deadlineMs).toISOString(),\n // INCLUSIVE of the deadline instant. \"within 30 days of first\n // accepting\" includes the thirtieth day, and the cheap `<` would close\n // the window a full day early for anyone who read the clause literally.\n open: remainingMs >= 0,\n daysRemaining: remainingMs > 0 ? Math.ceil(remainingMs / DAY_MS) : 0,\n }\n }\n\n let reacceptanceReason: LegalReacceptanceReason = 'none'\n if (acceptances.length === 0) reacceptanceReason = 'never-accepted'\n else if (\n currentVersion &&\n compareLegalDocumentVersions(currentVersion, latestAcceptedVersion) > 0\n ) {\n reacceptanceReason = 'version-superseded'\n }\n\n return {\n currentVersion,\n accepted: Boolean(currentRecord),\n acceptedVersions,\n latestAcceptedVersion,\n currentVersionAcceptedAt: currentRecord?.acceptedAt ?? null,\n latestAcceptedAt: latestRecord?.acceptedAt ?? null,\n changedDocumentKeys: diffPinnedDocuments(\n latestRecord,\n input.currentDocuments ?? null,\n ),\n reacceptanceRequired: reacceptanceReason !== 'none',\n reacceptanceReason,\n arbitration,\n acceptances,\n }\n}\n\n/** Read + evaluate in one call — what every surface actually wants. */\nexport async function getLegalAcceptanceStatus(\n uid: string,\n options: {\n currentVersion: string\n firestore?: any\n now?: Date\n /** See `evaluateLegalAcceptance`. Needed for `changedDocumentKeys`. */\n currentDocuments?: LegalDocumentReference[]\n },\n): Promise<LegalAcceptanceStatus> {\n const acceptances = await readLegalAcceptances(uid, {\n firestore: options.firestore,\n })\n return evaluateLegalAcceptance({\n acceptances,\n currentVersion: options.currentVersion,\n now: options.now,\n currentDocuments: options.currentDocuments,\n })\n}\n"],"names":["FieldValue","firebaseAdmin","firestore","app","recordLegalAcceptance","uid","input","version","String","trim","Error","ref","collection","doc","snapshot","get","exists","recorded","set","documents","method","context","acceptedAt","serverTimestamp","ipAddress","userAgent","ARBITRATION_OPT_OUT_DAYS","DAY_MS","parseLegalDocumentVersion","match","exec","rank","Number","isFinite","compareLegalDocumentVersions","a","b","left","right","localeCompare","diffPinnedDocuments","accepted","current","length","acceptedByKey","Map","key","size","changed","was","push","sha256","toIso","value","asDate","toDate","Date","isNaN","getTime","toISOString","parsed","readLegalAcceptances","options","records","docs","map","id","sort","evaluateLegalAcceptance","currentVersion","acceptances","now","optOutDays","acceptedVersions","record","latestAcceptedVersion","currentRecord","find","latestRecord","timestamps","filter","iso","Boolean","firstAcceptedAt","arbitration","deadline","open","daysRemaining","deadlineMs","remainingMs","Math","ceil","reacceptanceReason","currentVersionAcceptedAt","latestAcceptedAt","changedDocumentKeys","currentDocuments","reacceptanceRequired","getLegalAcceptanceStatus"],"mappings":"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"}
|