@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,66 @@
|
|
|
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
|
+
* Retention for the AGL-82 analytics day docs (AGL-1844).
|
|
18
|
+
*
|
|
19
|
+
* Day docs accumulated forever: every pageview, media serve and redirect hit
|
|
20
|
+
* left a `{hosts|orgs}/{id}/analytics/{YYYY-MM-DD}` (or
|
|
21
|
+
* `hosts/{id}/screenAnalytics/{screenId}:{day}`) document behind, and
|
|
22
|
+
* nothing ever deleted one. Every WRITER now stamps `expiresAt`, and a
|
|
23
|
+
* Firestore TTL policy on that field (declared as `fieldOverrides` in
|
|
24
|
+
* `cloud/firebase-firestore.indexes.json`, on the `analytics` and
|
|
25
|
+
* `screenAnalytics` collection groups) sweeps the old ones.
|
|
26
|
+
*
|
|
27
|
+
* ## Why 400 days
|
|
28
|
+
*
|
|
29
|
+
* Long enough that every reader's window closes with room to spare:
|
|
30
|
+
*
|
|
31
|
+
* - the console's widest range is 90 days;
|
|
32
|
+
* - /api/billing/report-usage meters `analytics/{day}.total` for the CURRENT
|
|
33
|
+
* period — an invoice dispute a year later still finds its days;
|
|
34
|
+
* - a year-over-year comparison (no surface does one yet) would still have
|
|
35
|
+
* both years.
|
|
36
|
+
*
|
|
37
|
+
* Short enough that storage is bounded: ~400 docs per host (plus per-screen
|
|
38
|
+
* and per-org docs) at steady state, instead of unbounded growth.
|
|
39
|
+
*
|
|
40
|
+
* ## Why TTL and NOT monthly rollups (the AGL-1844 decision)
|
|
41
|
+
*
|
|
42
|
+
* Rollups were considered and deliberately not built: no console surface
|
|
43
|
+
* reads more than 90 dailies (bounded, and blunted by the AGL-1440 cache),
|
|
44
|
+
* so a monthly doc would be written by a scheduled job — new moving part,
|
|
45
|
+
* new failure mode — to serve a view nothing renders. If a >90-day console
|
|
46
|
+
* range ever ships, rollups become that feature's first commit.
|
|
47
|
+
*
|
|
48
|
+
* TTL deletion is Firestore's own sweeper: typically within 24h of expiry,
|
|
49
|
+
* no code of ours runs. Docs written BEFORE this shipped carry no
|
|
50
|
+
* `expiresAt` and are never swept — the trickle of writes to a live site
|
|
51
|
+
* stamps its recent days naturally, and old unstamped days are exactly the
|
|
52
|
+
* ones nothing reads.
|
|
53
|
+
*/ /** See the module doc for why 400. */ export const ANALYTICS_DAY_RETENTION_DAYS = 400;
|
|
54
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
55
|
+
/**
|
|
56
|
+
* When the day doc named `day` (YYYY-MM-DD, UTC) may be swept. Anchored on
|
|
57
|
+
* the day itself, not the write clock, so every write to one doc agrees on
|
|
58
|
+
* one expiry. An unparsable day anchors on "now", which only ever shortens
|
|
59
|
+
* retention.
|
|
60
|
+
*/ export function analyticsDayExpiresAt(day) {
|
|
61
|
+
const dayStart = Date.parse(`${day}T00:00:00Z`);
|
|
62
|
+
const base = Number.isFinite(dayStart) ? dayStart : Date.now();
|
|
63
|
+
return new Date(base + ANALYTICS_DAY_RETENTION_DAYS * DAY_MS);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
//# sourceMappingURL=analytics-retention.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/analytics-retention.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 * Retention for the AGL-82 analytics day docs (AGL-1844).\n *\n * Day docs accumulated forever: every pageview, media serve and redirect hit\n * left a `{hosts|orgs}/{id}/analytics/{YYYY-MM-DD}` (or\n * `hosts/{id}/screenAnalytics/{screenId}:{day}`) document behind, and\n * nothing ever deleted one. Every WRITER now stamps `expiresAt`, and a\n * Firestore TTL policy on that field (declared as `fieldOverrides` in\n * `cloud/firebase-firestore.indexes.json`, on the `analytics` and\n * `screenAnalytics` collection groups) sweeps the old ones.\n *\n * ## Why 400 days\n *\n * Long enough that every reader's window closes with room to spare:\n *\n * - the console's widest range is 90 days;\n * - /api/billing/report-usage meters `analytics/{day}.total` for the CURRENT\n * period — an invoice dispute a year later still finds its days;\n * - a year-over-year comparison (no surface does one yet) would still have\n * both years.\n *\n * Short enough that storage is bounded: ~400 docs per host (plus per-screen\n * and per-org docs) at steady state, instead of unbounded growth.\n *\n * ## Why TTL and NOT monthly rollups (the AGL-1844 decision)\n *\n * Rollups were considered and deliberately not built: no console surface\n * reads more than 90 dailies (bounded, and blunted by the AGL-1440 cache),\n * so a monthly doc would be written by a scheduled job — new moving part,\n * new failure mode — to serve a view nothing renders. If a >90-day console\n * range ever ships, rollups become that feature's first commit.\n *\n * TTL deletion is Firestore's own sweeper: typically within 24h of expiry,\n * no code of ours runs. Docs written BEFORE this shipped carry no\n * `expiresAt` and are never swept — the trickle of writes to a live site\n * stamps its recent days naturally, and old unstamped days are exactly the\n * ones nothing reads.\n */\n\n/** See the module doc for why 400. */\nexport const ANALYTICS_DAY_RETENTION_DAYS = 400\n\nconst DAY_MS = 24 * 60 * 60 * 1000\n\n/**\n * When the day doc named `day` (YYYY-MM-DD, UTC) may be swept. Anchored on\n * the day itself, not the write clock, so every write to one doc agrees on\n * one expiry. An unparsable day anchors on \"now\", which only ever shortens\n * retention.\n */\nexport function analyticsDayExpiresAt(day: string): Date {\n const dayStart = Date.parse(`${day}T00:00:00Z`)\n const base = Number.isFinite(dayStart) ? dayStart : Date.now()\n return new Date(base + ANALYTICS_DAY_RETENTION_DAYS * DAY_MS)\n}\n"],"names":["ANALYTICS_DAY_RETENTION_DAYS","DAY_MS","analyticsDayExpiresAt","day","dayStart","Date","parse","base","Number","isFinite","now"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqCC,GAED,oCAAoC,GACpC,OAAO,MAAMA,+BAA+B,IAAG;AAE/C,MAAMC,SAAS,KAAK,KAAK,KAAK;AAE9B;;;;;CAKC,GACD,OAAO,SAASC,sBAAsBC,GAAW;IAC/C,MAAMC,WAAWC,KAAKC,KAAK,CAAC,GAAGH,IAAI,UAAU,CAAC;IAC9C,MAAMI,OAAOC,OAAOC,QAAQ,CAACL,YAAYA,WAAWC,KAAKK,GAAG;IAC5D,OAAO,IAAIL,KAAKE,OAAOP,+BAA+BC;AACxD"}
|
|
@@ -0,0 +1,159 @@
|
|
|
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
|
+
* HTTP building blocks for the customer REST API (`/api/v1`, AGL-617): a
|
|
19
|
+
* consistent JSON error envelope, list/pagination helpers, and a per-key
|
|
20
|
+
* rate limiter. Everything here is pure (Web `Response` + plain data), so it
|
|
21
|
+
* is unit-tested directly.
|
|
22
|
+
*/
|
|
23
|
+
/** Every error response is `{ error: { type, message, code?, fields? } }`. */
|
|
24
|
+
export interface ApiErrorBody {
|
|
25
|
+
error: {
|
|
26
|
+
type: string;
|
|
27
|
+
message: string;
|
|
28
|
+
code?: string;
|
|
29
|
+
/** Per-field reasons on a validation failure (AGL-901). */
|
|
30
|
+
fields?: Record<string, string>;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export interface ApiResponseInit {
|
|
34
|
+
message?: string;
|
|
35
|
+
code?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Field id → reason, surfaced on `validation_failed`. Additive: clients
|
|
38
|
+
* that only read `type`/`message` are unaffected.
|
|
39
|
+
*/
|
|
40
|
+
fields?: Record<string, string>;
|
|
41
|
+
headers?: Record<string, string>;
|
|
42
|
+
}
|
|
43
|
+
/** Canonical error responses. Pass `{ headers }` to carry rate-limit headers. */
|
|
44
|
+
export declare const ApiErrors: {
|
|
45
|
+
unauthorized: (init?: ApiResponseInit) => Response;
|
|
46
|
+
planRequired: (init?: ApiResponseInit) => Response;
|
|
47
|
+
insufficientScope: (scope: string, headers?: Record<string, string>) => Response;
|
|
48
|
+
forbidden: (init?: ApiResponseInit) => Response;
|
|
49
|
+
notFound: (init?: ApiResponseInit) => Response;
|
|
50
|
+
badRequest: (init?: ApiResponseInit) => Response;
|
|
51
|
+
/**
|
|
52
|
+
* The request conflicts with work already in flight (AGL-1709). Used by the
|
|
53
|
+
* idempotency claim for the one case it must fail CLOSED: an identical
|
|
54
|
+
* attempt that has taken the claim but not yet settled. Letting the second
|
|
55
|
+
* caller through is the duplicate the key exists to prevent, so it is
|
|
56
|
+
* refused rather than served — and a client that reads `type` gets a
|
|
57
|
+
* retriable answer rather than a mystery.
|
|
58
|
+
*/
|
|
59
|
+
conflict: (init?: ApiResponseInit) => Response;
|
|
60
|
+
methodNotAllowed: (init?: ApiResponseInit) => Response;
|
|
61
|
+
/**
|
|
62
|
+
* The three refusals an upload can produce that a JSON-only API never could
|
|
63
|
+
* (AGL-2463). Built here rather than at the call site so `/v1` keeps ONE
|
|
64
|
+
* error envelope — a client branching on `error.type` must not meet a bare
|
|
65
|
+
* `{ error: '<sentence>' }` on the one surface that handles bytes.
|
|
66
|
+
*/
|
|
67
|
+
unsupportedMediaType: (init?: ApiResponseInit) => Response;
|
|
68
|
+
payloadTooLarge: (init?: ApiResponseInit) => Response;
|
|
69
|
+
/**
|
|
70
|
+
* 451, matching the console's own media chokepoints: the asset is refused
|
|
71
|
+
* because its content hash is on the takedown ledger, which is a legal
|
|
72
|
+
* verdict rather than a permission or a quota.
|
|
73
|
+
*/
|
|
74
|
+
unavailableForLegalReasons: (init?: ApiResponseInit) => Response;
|
|
75
|
+
rateLimited: (retryAfterSec: number, headers?: Record<string, string>) => Response;
|
|
76
|
+
internal: (init?: ApiResponseInit) => Response;
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Re-emit `response` with `headers` merged in — the escape hatch for a
|
|
80
|
+
* refusal built by a shared helper that knows nothing about this request's
|
|
81
|
+
* rate-limit budget (the lockdown 423, AGL-1596). Status, body and the
|
|
82
|
+
* response's own headers are preserved; `set` overwrites rather than
|
|
83
|
+
* appends, so a header named on both sides appears exactly once (the passed
|
|
84
|
+
* value wins) and no client ever reads a doubled `X-RateLimit-Remaining`.
|
|
85
|
+
*/
|
|
86
|
+
export declare function withResponseHeaders(response: Response, headers: Record<string, string>): Response;
|
|
87
|
+
/** Success JSON with an optional status + headers (e.g. rate-limit headers). */
|
|
88
|
+
export declare function apiJson(data: unknown, init?: {
|
|
89
|
+
status?: number;
|
|
90
|
+
headers?: Record<string, string>;
|
|
91
|
+
}): Response;
|
|
92
|
+
export declare const DEFAULT_PAGE_LIMIT = 25;
|
|
93
|
+
export declare const MAX_PAGE_LIMIT = 100;
|
|
94
|
+
/** Clamp a `?limit=` value to [1, MAX], defaulting when absent/invalid. */
|
|
95
|
+
export declare function parseLimit(raw: string | null | undefined): number;
|
|
96
|
+
/** Opaque cursor = base64url of an underlying key (e.g. a Firestore doc id). */
|
|
97
|
+
export declare function encodeCursor(value: string): string;
|
|
98
|
+
export declare function decodeCursor(cursor: string | null | undefined): string | undefined;
|
|
99
|
+
/** List envelope: `{ data, next_cursor, has_more }`. */
|
|
100
|
+
export declare function listResponse(data: unknown[], nextCursor: string | null, headers?: Record<string, string>): Response;
|
|
101
|
+
export interface RateLimitState {
|
|
102
|
+
count: number;
|
|
103
|
+
windowStartMs: number;
|
|
104
|
+
}
|
|
105
|
+
export interface RateLimitResult {
|
|
106
|
+
allowed: boolean;
|
|
107
|
+
limit: number;
|
|
108
|
+
remaining: number;
|
|
109
|
+
resetMs: number;
|
|
110
|
+
}
|
|
111
|
+
export interface RateLimitOptions {
|
|
112
|
+
limit?: number;
|
|
113
|
+
windowMs?: number;
|
|
114
|
+
now?: number;
|
|
115
|
+
store?: Map<string, RateLimitState>;
|
|
116
|
+
}
|
|
117
|
+
export declare const DEFAULT_RATE_LIMIT = 120;
|
|
118
|
+
export declare const DEFAULT_RATE_WINDOW_MS = 60000;
|
|
119
|
+
/** Count one request against `key`'s fixed window. `now`/`store` injectable. */
|
|
120
|
+
export declare function checkRateLimit(key: string, options?: RateLimitOptions): RateLimitResult;
|
|
121
|
+
/**
|
|
122
|
+
* Report `key`'s window WITHOUT counting a request against it (AGL-2414).
|
|
123
|
+
*
|
|
124
|
+
* `checkRateLimit` fuses "is there budget" with "spend some", which is the
|
|
125
|
+
* right shape when every call is a request to be counted. It is the wrong
|
|
126
|
+
* shape when the budget meters something the caller has not done *yet* and
|
|
127
|
+
* may never do — `/api/v1` gates the Firestore key lookup on an IP's budget
|
|
128
|
+
* of *unproductive* lookups, so it has to ask before the lookup and only
|
|
129
|
+
* charge if the lookup came back empty. Asking with `checkRateLimit` would
|
|
130
|
+
* charge every request, including the successful ones, which is precisely the
|
|
131
|
+
* customer-visible cap that budget must not become.
|
|
132
|
+
*
|
|
133
|
+
* `allowed` answers "would the NEXT call be admitted", i.e. `count < limit` —
|
|
134
|
+
* one off from `checkRateLimit`'s post-increment `count <= limit`, and the
|
|
135
|
+
* same verdict for the same request. `remaining` is what is left now, so an
|
|
136
|
+
* untouched or expired window reports the full `limit` rather than
|
|
137
|
+
* `limit - 1`.
|
|
138
|
+
*/
|
|
139
|
+
export declare function peekRateLimit(key: string, options?: RateLimitOptions): RateLimitResult;
|
|
140
|
+
/**
|
|
141
|
+
* Rate-limit headers, in BOTH spellings (AGL-2727).
|
|
142
|
+
*
|
|
143
|
+
* `X-RateLimit-*` is the de-facto set this API has always sent, and it is kept
|
|
144
|
+
* verbatim — including `Reset` as an epoch second — because integrations are
|
|
145
|
+
* already reading it and a header that changes meaning under a client is worse
|
|
146
|
+
* than one it has to learn.
|
|
147
|
+
*
|
|
148
|
+
* `RateLimit-*` is RFC 9331, which is what a caller that was not written
|
|
149
|
+
* against this specific API looks for, and increasingly what an agent looks
|
|
150
|
+
* for. Same numbers, standard spelling. Its `Reset` is SECONDS REMAINING, not
|
|
151
|
+
* an epoch — that is the RFC's own choice, and the better one: a duration is
|
|
152
|
+
* read against the caller's clock rather than ours, so a skewed clock cannot
|
|
153
|
+
* turn "wait 12 seconds" into "wait until 1970".
|
|
154
|
+
*
|
|
155
|
+
* ⚠️ Both are computed from the SAME `result`, so they can never disagree.
|
|
156
|
+
* Two independently-derived budgets on one response is a bug that reads as a
|
|
157
|
+
* rounding error.
|
|
158
|
+
*/
|
|
159
|
+
export declare function rateLimitHeaders(result: RateLimitResult, now?: number): Record<string, string>;
|
|
@@ -0,0 +1,230 @@
|
|
|
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
|
+
* HTTP building blocks for the customer REST API (`/api/v1`, AGL-617): a
|
|
18
|
+
* consistent JSON error envelope, list/pagination helpers, and a per-key
|
|
19
|
+
* rate limiter. Everything here is pure (Web `Response` + plain data), so it
|
|
20
|
+
* is unit-tested directly.
|
|
21
|
+
*/ /** Every error response is `{ error: { type, message, code?, fields? } }`. */ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
22
|
+
function errorResponse(status, type, fallbackMessage, init) {
|
|
23
|
+
var _ref;
|
|
24
|
+
const error = {
|
|
25
|
+
type,
|
|
26
|
+
message: (_ref = init == null ? void 0 : init.message) != null ? _ref : fallbackMessage
|
|
27
|
+
};
|
|
28
|
+
if (init == null ? void 0 : init.code) error.code = init.code;
|
|
29
|
+
if ((init == null ? void 0 : init.fields) && Object.keys(init.fields).length) error.fields = init.fields;
|
|
30
|
+
return Response.json({
|
|
31
|
+
error
|
|
32
|
+
}, {
|
|
33
|
+
status,
|
|
34
|
+
headers: init == null ? void 0 : init.headers
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
/** Canonical error responses. Pass `{ headers }` to carry rate-limit headers. */ export const ApiErrors = {
|
|
38
|
+
unauthorized: (init)=>errorResponse(401, 'unauthorized', 'Invalid or missing API key', init),
|
|
39
|
+
planRequired: (init)=>errorResponse(403, 'plan_required', 'API access requires the Business or Advanced plan', init),
|
|
40
|
+
insufficientScope: (scope, headers)=>errorResponse(403, 'insufficient_scope', `Missing the "${scope}" scope`, {
|
|
41
|
+
code: scope,
|
|
42
|
+
headers
|
|
43
|
+
}),
|
|
44
|
+
forbidden: (init)=>errorResponse(403, 'forbidden', 'Not permitted', init),
|
|
45
|
+
notFound: (init)=>errorResponse(404, 'not_found', 'Not found', init),
|
|
46
|
+
badRequest: (init)=>errorResponse(400, 'bad_request', 'Bad request', init),
|
|
47
|
+
/**
|
|
48
|
+
* The request conflicts with work already in flight (AGL-1709). Used by the
|
|
49
|
+
* idempotency claim for the one case it must fail CLOSED: an identical
|
|
50
|
+
* attempt that has taken the claim but not yet settled. Letting the second
|
|
51
|
+
* caller through is the duplicate the key exists to prevent, so it is
|
|
52
|
+
* refused rather than served — and a client that reads `type` gets a
|
|
53
|
+
* retriable answer rather than a mystery.
|
|
54
|
+
*/ conflict: (init)=>errorResponse(409, 'conflict', 'Conflicts with the current state', init),
|
|
55
|
+
methodNotAllowed: (init)=>errorResponse(405, 'method_not_allowed', 'Method not allowed', init),
|
|
56
|
+
/**
|
|
57
|
+
* The three refusals an upload can produce that a JSON-only API never could
|
|
58
|
+
* (AGL-2463). Built here rather than at the call site so `/v1` keeps ONE
|
|
59
|
+
* error envelope — a client branching on `error.type` must not meet a bare
|
|
60
|
+
* `{ error: '<sentence>' }` on the one surface that handles bytes.
|
|
61
|
+
*/ unsupportedMediaType: (init)=>errorResponse(415, 'unsupported_media_type', 'Unsupported file type', init),
|
|
62
|
+
payloadTooLarge: (init)=>errorResponse(413, 'payload_too_large', 'File too large', init),
|
|
63
|
+
/**
|
|
64
|
+
* 451, matching the console's own media chokepoints: the asset is refused
|
|
65
|
+
* because its content hash is on the takedown ledger, which is a legal
|
|
66
|
+
* verdict rather than a permission or a quota.
|
|
67
|
+
*/ unavailableForLegalReasons: (init)=>errorResponse(451, 'unavailable_for_legal_reasons', 'This file has been removed and cannot be uploaded', init),
|
|
68
|
+
rateLimited: (retryAfterSec, headers)=>errorResponse(429, 'rate_limited', 'Too many requests', {
|
|
69
|
+
headers: _extends({
|
|
70
|
+
'Retry-After': String(Math.max(1, retryAfterSec))
|
|
71
|
+
}, headers)
|
|
72
|
+
}),
|
|
73
|
+
internal: (init)=>errorResponse(500, 'internal_error', 'Something went wrong', init)
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Re-emit `response` with `headers` merged in — the escape hatch for a
|
|
77
|
+
* refusal built by a shared helper that knows nothing about this request's
|
|
78
|
+
* rate-limit budget (the lockdown 423, AGL-1596). Status, body and the
|
|
79
|
+
* response's own headers are preserved; `set` overwrites rather than
|
|
80
|
+
* appends, so a header named on both sides appears exactly once (the passed
|
|
81
|
+
* value wins) and no client ever reads a doubled `X-RateLimit-Remaining`.
|
|
82
|
+
*/ export function withResponseHeaders(response, headers) {
|
|
83
|
+
const merged = new Headers(response.headers);
|
|
84
|
+
for (const [name, value] of Object.entries(headers))merged.set(name, value);
|
|
85
|
+
return new Response(response.body, {
|
|
86
|
+
status: response.status,
|
|
87
|
+
statusText: response.statusText,
|
|
88
|
+
headers: merged
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
/** Success JSON with an optional status + headers (e.g. rate-limit headers). */ export function apiJson(data, init) {
|
|
92
|
+
var _ref;
|
|
93
|
+
return Response.json(data, {
|
|
94
|
+
status: (_ref = init == null ? void 0 : init.status) != null ? _ref : 200,
|
|
95
|
+
headers: init == null ? void 0 : init.headers
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
// ── Pagination ──────────────────────────────────────────────────────────────
|
|
99
|
+
export const DEFAULT_PAGE_LIMIT = 25;
|
|
100
|
+
export const MAX_PAGE_LIMIT = 100;
|
|
101
|
+
/** Clamp a `?limit=` value to [1, MAX], defaulting when absent/invalid. */ export function parseLimit(raw) {
|
|
102
|
+
const n = Number(raw);
|
|
103
|
+
if (!Number.isFinite(n) || n < 1) return DEFAULT_PAGE_LIMIT;
|
|
104
|
+
return Math.min(Math.floor(n), MAX_PAGE_LIMIT);
|
|
105
|
+
}
|
|
106
|
+
/** Opaque cursor = base64url of an underlying key (e.g. a Firestore doc id). */ export function encodeCursor(value) {
|
|
107
|
+
return Buffer.from(value, 'utf8').toString('base64url');
|
|
108
|
+
}
|
|
109
|
+
export function decodeCursor(cursor) {
|
|
110
|
+
if (!cursor) return undefined;
|
|
111
|
+
try {
|
|
112
|
+
const decoded = Buffer.from(cursor, 'base64url').toString('utf8');
|
|
113
|
+
return decoded.length > 0 ? decoded : undefined;
|
|
114
|
+
} catch (unused) {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/** List envelope: `{ data, next_cursor, has_more }`. */ export function listResponse(data, nextCursor, headers) {
|
|
119
|
+
return apiJson({
|
|
120
|
+
object: 'list',
|
|
121
|
+
data,
|
|
122
|
+
next_cursor: nextCursor,
|
|
123
|
+
has_more: nextCursor != null
|
|
124
|
+
}, {
|
|
125
|
+
headers
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
export const DEFAULT_RATE_LIMIT = 120;
|
|
129
|
+
export const DEFAULT_RATE_WINDOW_MS = 60000;
|
|
130
|
+
// Per-instance store. Serverless instances are ephemeral, so this blunts
|
|
131
|
+
// bursts rather than enforcing a global cap (matches the app's other
|
|
132
|
+
// limiters); a durable per-key counter can back it later.
|
|
133
|
+
const defaultStore = new Map();
|
|
134
|
+
/** Count one request against `key`'s fixed window. `now`/`store` injectable. */ export function checkRateLimit(key, options) {
|
|
135
|
+
var _ref, _ref1, _ref2, _ref3;
|
|
136
|
+
const limit = (_ref = options == null ? void 0 : options.limit) != null ? _ref : DEFAULT_RATE_LIMIT;
|
|
137
|
+
const windowMs = (_ref1 = options == null ? void 0 : options.windowMs) != null ? _ref1 : DEFAULT_RATE_WINDOW_MS;
|
|
138
|
+
const now = (_ref2 = options == null ? void 0 : options.now) != null ? _ref2 : Date.now();
|
|
139
|
+
const store = (_ref3 = options == null ? void 0 : options.store) != null ? _ref3 : defaultStore;
|
|
140
|
+
const state = store.get(key);
|
|
141
|
+
if (!state || now - state.windowStartMs >= windowMs) {
|
|
142
|
+
store.set(key, {
|
|
143
|
+
count: 1,
|
|
144
|
+
windowStartMs: now
|
|
145
|
+
});
|
|
146
|
+
return {
|
|
147
|
+
allowed: true,
|
|
148
|
+
limit,
|
|
149
|
+
remaining: limit - 1,
|
|
150
|
+
resetMs: now + windowMs
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
state.count += 1;
|
|
154
|
+
return {
|
|
155
|
+
allowed: state.count <= limit,
|
|
156
|
+
limit,
|
|
157
|
+
remaining: Math.max(0, limit - state.count),
|
|
158
|
+
resetMs: state.windowStartMs + windowMs
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Report `key`'s window WITHOUT counting a request against it (AGL-2414).
|
|
163
|
+
*
|
|
164
|
+
* `checkRateLimit` fuses "is there budget" with "spend some", which is the
|
|
165
|
+
* right shape when every call is a request to be counted. It is the wrong
|
|
166
|
+
* shape when the budget meters something the caller has not done *yet* and
|
|
167
|
+
* may never do — `/api/v1` gates the Firestore key lookup on an IP's budget
|
|
168
|
+
* of *unproductive* lookups, so it has to ask before the lookup and only
|
|
169
|
+
* charge if the lookup came back empty. Asking with `checkRateLimit` would
|
|
170
|
+
* charge every request, including the successful ones, which is precisely the
|
|
171
|
+
* customer-visible cap that budget must not become.
|
|
172
|
+
*
|
|
173
|
+
* `allowed` answers "would the NEXT call be admitted", i.e. `count < limit` —
|
|
174
|
+
* one off from `checkRateLimit`'s post-increment `count <= limit`, and the
|
|
175
|
+
* same verdict for the same request. `remaining` is what is left now, so an
|
|
176
|
+
* untouched or expired window reports the full `limit` rather than
|
|
177
|
+
* `limit - 1`.
|
|
178
|
+
*/ export function peekRateLimit(key, options) {
|
|
179
|
+
var _ref, _ref1, _ref2, _ref3;
|
|
180
|
+
const limit = (_ref = options == null ? void 0 : options.limit) != null ? _ref : DEFAULT_RATE_LIMIT;
|
|
181
|
+
const windowMs = (_ref1 = options == null ? void 0 : options.windowMs) != null ? _ref1 : DEFAULT_RATE_WINDOW_MS;
|
|
182
|
+
const now = (_ref2 = options == null ? void 0 : options.now) != null ? _ref2 : Date.now();
|
|
183
|
+
const store = (_ref3 = options == null ? void 0 : options.store) != null ? _ref3 : defaultStore;
|
|
184
|
+
const state = store.get(key);
|
|
185
|
+
if (!state || now - state.windowStartMs >= windowMs) {
|
|
186
|
+
return {
|
|
187
|
+
allowed: true,
|
|
188
|
+
limit,
|
|
189
|
+
remaining: limit,
|
|
190
|
+
resetMs: now + windowMs
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
allowed: state.count < limit,
|
|
195
|
+
limit,
|
|
196
|
+
remaining: Math.max(0, limit - state.count),
|
|
197
|
+
resetMs: state.windowStartMs + windowMs
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Rate-limit headers, in BOTH spellings (AGL-2727).
|
|
202
|
+
*
|
|
203
|
+
* `X-RateLimit-*` is the de-facto set this API has always sent, and it is kept
|
|
204
|
+
* verbatim — including `Reset` as an epoch second — because integrations are
|
|
205
|
+
* already reading it and a header that changes meaning under a client is worse
|
|
206
|
+
* than one it has to learn.
|
|
207
|
+
*
|
|
208
|
+
* `RateLimit-*` is RFC 9331, which is what a caller that was not written
|
|
209
|
+
* against this specific API looks for, and increasingly what an agent looks
|
|
210
|
+
* for. Same numbers, standard spelling. Its `Reset` is SECONDS REMAINING, not
|
|
211
|
+
* an epoch — that is the RFC's own choice, and the better one: a duration is
|
|
212
|
+
* read against the caller's clock rather than ours, so a skewed clock cannot
|
|
213
|
+
* turn "wait 12 seconds" into "wait until 1970".
|
|
214
|
+
*
|
|
215
|
+
* ⚠️ Both are computed from the SAME `result`, so they can never disagree.
|
|
216
|
+
* Two independently-derived budgets on one response is a bug that reads as a
|
|
217
|
+
* rounding error.
|
|
218
|
+
*/ export function rateLimitHeaders(result, now = Date.now()) {
|
|
219
|
+
const remainingSeconds = Math.max(0, Math.ceil((result.resetMs - now) / 1000));
|
|
220
|
+
return {
|
|
221
|
+
'X-RateLimit-Limit': String(result.limit),
|
|
222
|
+
'X-RateLimit-Remaining': String(result.remaining),
|
|
223
|
+
'X-RateLimit-Reset': String(Math.ceil(result.resetMs / 1000)),
|
|
224
|
+
'RateLimit-Limit': String(result.limit),
|
|
225
|
+
'RateLimit-Remaining': String(result.remaining),
|
|
226
|
+
'RateLimit-Reset': String(remainingSeconds)
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
//# sourceMappingURL=api-http.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/api-http.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 * HTTP building blocks for the customer REST API (`/api/v1`, AGL-617): a\n * consistent JSON error envelope, list/pagination helpers, and a per-key\n * rate limiter. Everything here is pure (Web `Response` + plain data), so it\n * is unit-tested directly.\n */\n\n/** Every error response is `{ error: { type, message, code?, fields? } }`. */\nexport interface ApiErrorBody {\n error: {\n type: string\n message: string\n code?: string\n /** Per-field reasons on a validation failure (AGL-901). */\n fields?: Record<string, string>\n }\n}\n\nexport interface ApiResponseInit {\n message?: string\n code?: string\n /**\n * Field id → reason, surfaced on `validation_failed`. Additive: clients\n * that only read `type`/`message` are unaffected.\n */\n fields?: Record<string, string>\n headers?: Record<string, string>\n}\n\nfunction errorResponse(\n status: number,\n type: string,\n fallbackMessage: string,\n init?: ApiResponseInit,\n): Response {\n const error: ApiErrorBody['error'] = {\n type,\n message: init?.message ?? fallbackMessage,\n }\n if (init?.code) error.code = init.code\n if (init?.fields && Object.keys(init.fields).length) error.fields = init.fields\n return Response.json({ error }, { status, headers: init?.headers })\n}\n\n/** Canonical error responses. Pass `{ headers }` to carry rate-limit headers. */\nexport const ApiErrors = {\n unauthorized: (init?: ApiResponseInit) =>\n errorResponse(401, 'unauthorized', 'Invalid or missing API key', init),\n planRequired: (init?: ApiResponseInit) =>\n errorResponse(\n 403,\n 'plan_required',\n 'API access requires the Business or Advanced plan',\n init,\n ),\n insufficientScope: (scope: string, headers?: Record<string, string>) =>\n errorResponse(403, 'insufficient_scope', `Missing the \"${scope}\" scope`, {\n code: scope,\n headers,\n }),\n forbidden: (init?: ApiResponseInit) =>\n errorResponse(403, 'forbidden', 'Not permitted', init),\n notFound: (init?: ApiResponseInit) =>\n errorResponse(404, 'not_found', 'Not found', init),\n badRequest: (init?: ApiResponseInit) =>\n errorResponse(400, 'bad_request', 'Bad request', init),\n /**\n * The request conflicts with work already in flight (AGL-1709). Used by the\n * idempotency claim for the one case it must fail CLOSED: an identical\n * attempt that has taken the claim but not yet settled. Letting the second\n * caller through is the duplicate the key exists to prevent, so it is\n * refused rather than served — and a client that reads `type` gets a\n * retriable answer rather than a mystery.\n */\n conflict: (init?: ApiResponseInit) =>\n errorResponse(409, 'conflict', 'Conflicts with the current state', init),\n methodNotAllowed: (init?: ApiResponseInit) =>\n errorResponse(405, 'method_not_allowed', 'Method not allowed', init),\n /**\n * The three refusals an upload can produce that a JSON-only API never could\n * (AGL-2463). Built here rather than at the call site so `/v1` keeps ONE\n * error envelope — a client branching on `error.type` must not meet a bare\n * `{ error: '<sentence>' }` on the one surface that handles bytes.\n */\n unsupportedMediaType: (init?: ApiResponseInit) =>\n errorResponse(415, 'unsupported_media_type', 'Unsupported file type', init),\n payloadTooLarge: (init?: ApiResponseInit) =>\n errorResponse(413, 'payload_too_large', 'File too large', init),\n /**\n * 451, matching the console's own media chokepoints: the asset is refused\n * because its content hash is on the takedown ledger, which is a legal\n * verdict rather than a permission or a quota.\n */\n unavailableForLegalReasons: (init?: ApiResponseInit) =>\n errorResponse(\n 451,\n 'unavailable_for_legal_reasons',\n 'This file has been removed and cannot be uploaded',\n init,\n ),\n rateLimited: (retryAfterSec: number, headers?: Record<string, string>) =>\n errorResponse(429, 'rate_limited', 'Too many requests', {\n headers: { 'Retry-After': String(Math.max(1, retryAfterSec)), ...headers },\n }),\n internal: (init?: ApiResponseInit) =>\n errorResponse(500, 'internal_error', 'Something went wrong', init),\n}\n\n/**\n * Re-emit `response` with `headers` merged in — the escape hatch for a\n * refusal built by a shared helper that knows nothing about this request's\n * rate-limit budget (the lockdown 423, AGL-1596). Status, body and the\n * response's own headers are preserved; `set` overwrites rather than\n * appends, so a header named on both sides appears exactly once (the passed\n * value wins) and no client ever reads a doubled `X-RateLimit-Remaining`.\n */\nexport function withResponseHeaders(\n response: Response,\n headers: Record<string, string>,\n): Response {\n const merged = new Headers(response.headers)\n for (const [name, value] of Object.entries(headers)) merged.set(name, value)\n return new Response(response.body, {\n status: response.status,\n statusText: response.statusText,\n headers: merged,\n })\n}\n\n/** Success JSON with an optional status + headers (e.g. rate-limit headers). */\nexport function apiJson(\n data: unknown,\n init?: { status?: number; headers?: Record<string, string> },\n): Response {\n return Response.json(data, {\n status: init?.status ?? 200,\n headers: init?.headers,\n })\n}\n\n// ── Pagination ──────────────────────────────────────────────────────────────\n\nexport const DEFAULT_PAGE_LIMIT = 25\nexport const MAX_PAGE_LIMIT = 100\n\n/** Clamp a `?limit=` value to [1, MAX], defaulting when absent/invalid. */\nexport function parseLimit(raw: string | null | undefined): number {\n const n = Number(raw)\n if (!Number.isFinite(n) || n < 1) return DEFAULT_PAGE_LIMIT\n return Math.min(Math.floor(n), MAX_PAGE_LIMIT)\n}\n\n/** Opaque cursor = base64url of an underlying key (e.g. a Firestore doc id). */\nexport function encodeCursor(value: string): string {\n return Buffer.from(value, 'utf8').toString('base64url')\n}\n\nexport function decodeCursor(\n cursor: string | null | undefined,\n): string | undefined {\n if (!cursor) return undefined\n try {\n const decoded = Buffer.from(cursor, 'base64url').toString('utf8')\n return decoded.length > 0 ? decoded : undefined\n } catch {\n return undefined\n }\n}\n\n/** List envelope: `{ data, next_cursor, has_more }`. */\nexport function listResponse(\n data: unknown[],\n nextCursor: string | null,\n headers?: Record<string, string>,\n): Response {\n return apiJson(\n { object: 'list', data, next_cursor: nextCursor, has_more: nextCursor != null },\n { headers },\n )\n}\n\n// ── Rate limiting (fixed window, per key) ───────────────────────────────────\n\nexport interface RateLimitState {\n count: number\n windowStartMs: number\n}\n\nexport interface RateLimitResult {\n allowed: boolean\n limit: number\n remaining: number\n resetMs: number\n}\n\nexport interface RateLimitOptions {\n limit?: number\n windowMs?: number\n now?: number\n store?: Map<string, RateLimitState>\n}\n\nexport const DEFAULT_RATE_LIMIT = 120\nexport const DEFAULT_RATE_WINDOW_MS = 60_000\n\n// Per-instance store. Serverless instances are ephemeral, so this blunts\n// bursts rather than enforcing a global cap (matches the app's other\n// limiters); a durable per-key counter can back it later.\nconst defaultStore = new Map<string, RateLimitState>()\n\n/** Count one request against `key`'s fixed window. `now`/`store` injectable. */\nexport function checkRateLimit(\n key: string,\n options?: RateLimitOptions,\n): RateLimitResult {\n const limit = options?.limit ?? DEFAULT_RATE_LIMIT\n const windowMs = options?.windowMs ?? DEFAULT_RATE_WINDOW_MS\n const now = options?.now ?? Date.now()\n const store = options?.store ?? defaultStore\n\n const state = store.get(key)\n if (!state || now - state.windowStartMs >= windowMs) {\n store.set(key, { count: 1, windowStartMs: now })\n return { allowed: true, limit, remaining: limit - 1, resetMs: now + windowMs }\n }\n state.count += 1\n return {\n allowed: state.count <= limit,\n limit,\n remaining: Math.max(0, limit - state.count),\n resetMs: state.windowStartMs + windowMs,\n }\n}\n\n/**\n * Report `key`'s window WITHOUT counting a request against it (AGL-2414).\n *\n * `checkRateLimit` fuses \"is there budget\" with \"spend some\", which is the\n * right shape when every call is a request to be counted. It is the wrong\n * shape when the budget meters something the caller has not done *yet* and\n * may never do — `/api/v1` gates the Firestore key lookup on an IP's budget\n * of *unproductive* lookups, so it has to ask before the lookup and only\n * charge if the lookup came back empty. Asking with `checkRateLimit` would\n * charge every request, including the successful ones, which is precisely the\n * customer-visible cap that budget must not become.\n *\n * `allowed` answers \"would the NEXT call be admitted\", i.e. `count < limit` —\n * one off from `checkRateLimit`'s post-increment `count <= limit`, and the\n * same verdict for the same request. `remaining` is what is left now, so an\n * untouched or expired window reports the full `limit` rather than\n * `limit - 1`.\n */\nexport function peekRateLimit(\n key: string,\n options?: RateLimitOptions,\n): RateLimitResult {\n const limit = options?.limit ?? DEFAULT_RATE_LIMIT\n const windowMs = options?.windowMs ?? DEFAULT_RATE_WINDOW_MS\n const now = options?.now ?? Date.now()\n const store = options?.store ?? defaultStore\n\n const state = store.get(key)\n if (!state || now - state.windowStartMs >= windowMs) {\n return { allowed: true, limit, remaining: limit, resetMs: now + windowMs }\n }\n return {\n allowed: state.count < limit,\n limit,\n remaining: Math.max(0, limit - state.count),\n resetMs: state.windowStartMs + windowMs,\n }\n}\n\n/**\n * Rate-limit headers, in BOTH spellings (AGL-2727).\n *\n * `X-RateLimit-*` is the de-facto set this API has always sent, and it is kept\n * verbatim — including `Reset` as an epoch second — because integrations are\n * already reading it and a header that changes meaning under a client is worse\n * than one it has to learn.\n *\n * `RateLimit-*` is RFC 9331, which is what a caller that was not written\n * against this specific API looks for, and increasingly what an agent looks\n * for. Same numbers, standard spelling. Its `Reset` is SECONDS REMAINING, not\n * an epoch — that is the RFC's own choice, and the better one: a duration is\n * read against the caller's clock rather than ours, so a skewed clock cannot\n * turn \"wait 12 seconds\" into \"wait until 1970\".\n *\n * ⚠️ Both are computed from the SAME `result`, so they can never disagree.\n * Two independently-derived budgets on one response is a bug that reads as a\n * rounding error.\n */\nexport function rateLimitHeaders(\n result: RateLimitResult,\n now: number = Date.now(),\n): Record<string, string> {\n const remainingSeconds = Math.max(0, Math.ceil((result.resetMs - now) / 1000))\n return {\n 'X-RateLimit-Limit': String(result.limit),\n 'X-RateLimit-Remaining': String(result.remaining),\n 'X-RateLimit-Reset': String(Math.ceil(result.resetMs / 1000)),\n 'RateLimit-Limit': String(result.limit),\n 'RateLimit-Remaining': String(result.remaining),\n 'RateLimit-Reset': String(remainingSeconds),\n }\n}\n"],"names":["errorResponse","status","type","fallbackMessage","init","error","message","code","fields","Object","keys","length","Response","json","headers","ApiErrors","unauthorized","planRequired","insufficientScope","scope","forbidden","notFound","badRequest","conflict","methodNotAllowed","unsupportedMediaType","payloadTooLarge","unavailableForLegalReasons","rateLimited","retryAfterSec","String","Math","max","internal","withResponseHeaders","response","merged","Headers","name","value","entries","set","body","statusText","apiJson","data","DEFAULT_PAGE_LIMIT","MAX_PAGE_LIMIT","parseLimit","raw","n","Number","isFinite","min","floor","encodeCursor","Buffer","from","toString","decodeCursor","cursor","undefined","decoded","listResponse","nextCursor","object","next_cursor","has_more","DEFAULT_RATE_LIMIT","DEFAULT_RATE_WINDOW_MS","defaultStore","Map","checkRateLimit","key","options","limit","windowMs","now","Date","store","state","get","windowStartMs","count","allowed","remaining","resetMs","peekRateLimit","rateLimitHeaders","result","remainingSeconds","ceil"],"mappings":"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"}
|
|
@@ -0,0 +1,117 @@
|
|
|
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
|
+
* Scopes a key can be granted, as `resource:action`. Every entry must be
|
|
19
|
+
* enforced by a real endpoint — a mintable scope that grants nothing reads
|
|
20
|
+
* as a broken permission (AGL-899 removed `contacts:write` on those grounds,
|
|
21
|
+
* and AGL-2276 re-added it in the same change that shipped the writes, which
|
|
22
|
+
* is the condition AGL-899 set).
|
|
23
|
+
*/
|
|
24
|
+
export declare const API_SCOPES: readonly ["datasets:read", "datasets:write", "contacts:read", "contacts:write", "sites:read", "sites:publish", "sites:write", "forms:read", "forms:write", "orders:read", "orders:write", "products:read", "media:read", "media:write", "crm:read", "crm:write"];
|
|
25
|
+
export type ApiScope = (typeof API_SCOPES)[number];
|
|
26
|
+
/** Live-key token prefix. A `_test_` variant can follow the same shape later. */
|
|
27
|
+
export declare const API_KEY_TOKEN_PREFIX = "aglyn_sk_";
|
|
28
|
+
/** Top-level collection; doc id = the token hash. */
|
|
29
|
+
export declare const API_KEYS_COLLECTION = "apiKeys";
|
|
30
|
+
/** Stored document. `createdAt`/`lastUsedAt`/etc. are Firestore Timestamps. */
|
|
31
|
+
export interface ApiKeyDocument {
|
|
32
|
+
/** Public, stable id shown in the UI and used for revoke (never the hash). */
|
|
33
|
+
keyId: string;
|
|
34
|
+
orgId: string;
|
|
35
|
+
name: string;
|
|
36
|
+
/** e.g. `aglyn_sk_ab12cd…` — enough to recognize a key, not to use it. */
|
|
37
|
+
keyPrefix: string;
|
|
38
|
+
scopes: ApiScope[];
|
|
39
|
+
createdBy: string;
|
|
40
|
+
createdAt: FirebaseFirestore.Timestamp;
|
|
41
|
+
lastUsedAt: FirebaseFirestore.Timestamp | null;
|
|
42
|
+
revokedAt: FirebaseFirestore.Timestamp | null;
|
|
43
|
+
expiresAt: FirebaseFirestore.Timestamp | null;
|
|
44
|
+
}
|
|
45
|
+
/** Metadata safe to return to a client (no hash, no raw token). */
|
|
46
|
+
export interface PublicApiKey {
|
|
47
|
+
keyId: string;
|
|
48
|
+
name: string;
|
|
49
|
+
keyPrefix: string;
|
|
50
|
+
scopes: ApiScope[];
|
|
51
|
+
createdAt: string | null;
|
|
52
|
+
lastUsedAt: string | null;
|
|
53
|
+
revokedAt: string | null;
|
|
54
|
+
expiresAt: string | null;
|
|
55
|
+
}
|
|
56
|
+
/** The resolved principal after a successful key verification. */
|
|
57
|
+
export interface VerifiedApiKey {
|
|
58
|
+
orgId: string;
|
|
59
|
+
keyId: string;
|
|
60
|
+
scopes: ApiScope[];
|
|
61
|
+
/**
|
|
62
|
+
* The name the organization gave the key — what a feed attributes the
|
|
63
|
+
* key's writes to (AGL-2632). Every minted key carries one; empty only
|
|
64
|
+
* for a document written before the field was required.
|
|
65
|
+
*/
|
|
66
|
+
name: string;
|
|
67
|
+
}
|
|
68
|
+
/** True if `value` is one of the known scopes. */
|
|
69
|
+
export declare function isApiScope(value: string): value is ApiScope;
|
|
70
|
+
/** Keep only valid, de-duplicated scopes, preserving `API_SCOPES` order. */
|
|
71
|
+
export declare function normalizeScopes(scopes: readonly string[]): ApiScope[];
|
|
72
|
+
/** SHA-256 hex of a raw token — the Firestore doc id and lookup key. */
|
|
73
|
+
export declare function hashApiKey(token: string): string;
|
|
74
|
+
/** Structural check before hashing/looking up — cheap reject of junk input. */
|
|
75
|
+
export declare function isValidApiKeyFormat(token: string): boolean;
|
|
76
|
+
/**
|
|
77
|
+
* Generate a fresh token plus the derived hash and display prefix. The raw
|
|
78
|
+
* `token` is the only time the secret exists in plaintext — persist the hash,
|
|
79
|
+
* hand the token to the user once.
|
|
80
|
+
*/
|
|
81
|
+
export declare function generateApiKeyToken(): {
|
|
82
|
+
token: string;
|
|
83
|
+
hash: string;
|
|
84
|
+
keyPrefix: string;
|
|
85
|
+
};
|
|
86
|
+
/** True when the granted scopes include the one an operation requires. */
|
|
87
|
+
export declare function hasScope(granted: readonly ApiScope[], required: ApiScope): boolean;
|
|
88
|
+
/** A random public key id (`key_…`), distinct from the secret and its hash. */
|
|
89
|
+
export declare function generateKeyId(): string;
|
|
90
|
+
/** Public projection of a stored key — never includes the hash/doc id. */
|
|
91
|
+
export declare function toPublicApiKey(doc: ApiKeyDocument): PublicApiKey;
|
|
92
|
+
export interface MintApiKeyInput {
|
|
93
|
+
orgId: string;
|
|
94
|
+
name: string;
|
|
95
|
+
scopes: readonly string[];
|
|
96
|
+
createdBy: string;
|
|
97
|
+
/** Optional absolute expiry, epoch ms. */
|
|
98
|
+
expiresAtMs?: number | null;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Create a key for an org. Returns the raw `token` (show once) and the public
|
|
102
|
+
* metadata. Invalid scopes are dropped; at least one valid scope is required.
|
|
103
|
+
*/
|
|
104
|
+
export declare function mintApiKey(input: MintApiKeyInput): Promise<{
|
|
105
|
+
token: string;
|
|
106
|
+
key: PublicApiKey;
|
|
107
|
+
}>;
|
|
108
|
+
/**
|
|
109
|
+
* Resolve a raw token to its org/scopes, or `null` if it is malformed,
|
|
110
|
+
* unknown, revoked, or expired. Refreshes `lastUsedAt` at most once a minute
|
|
111
|
+
* (fire-and-forget) to avoid a write on every request.
|
|
112
|
+
*/
|
|
113
|
+
export declare function verifyApiKey(token: string): Promise<VerifiedApiKey | null>;
|
|
114
|
+
/** All keys for an org, newest first (metadata only). */
|
|
115
|
+
export declare function listApiKeys(orgId: string): Promise<PublicApiKey[]>;
|
|
116
|
+
/** Revoke a key by its public id. Returns false if not found/already revoked. */
|
|
117
|
+
export declare function revokeApiKey(orgId: string, keyId: string): Promise<boolean>;
|