@wopr-network/platform-core 0.1.0
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/biome.json +61 -0
- package/dist/admin/admin-audit-log-repository.d.ts +33 -0
- package/dist/admin/admin-audit-log-repository.js +102 -0
- package/dist/admin/audit-log.d.ts +49 -0
- package/dist/admin/audit-log.js +63 -0
- package/dist/admin/index.d.ts +6 -0
- package/dist/admin/index.js +3 -0
- package/dist/admin/role-store.d.ts +37 -0
- package/dist/admin/role-store.js +106 -0
- package/dist/auth/api-key-repository.d.ts +11 -0
- package/dist/auth/api-key-repository.js +33 -0
- package/dist/auth/api-key-repository.test.d.ts +1 -0
- package/dist/auth/api-key-repository.test.js +46 -0
- package/dist/auth/auth.test.d.ts +1 -0
- package/dist/auth/auth.test.js +140 -0
- package/dist/auth/better-auth.d.ts +42 -0
- package/dist/auth/better-auth.js +196 -0
- package/dist/auth/index.d.ts +186 -0
- package/dist/auth/index.js +422 -0
- package/dist/auth/login-history-repository.d.ts +14 -0
- package/dist/auth/login-history-repository.js +15 -0
- package/dist/auth/login-history-repository.test.d.ts +1 -0
- package/dist/auth/login-history-repository.test.js +47 -0
- package/dist/auth/middleware.d.ts +55 -0
- package/dist/auth/middleware.js +101 -0
- package/dist/auth/middleware.test.d.ts +1 -0
- package/dist/auth/middleware.test.js +213 -0
- package/dist/auth/scoped-tokens.test.d.ts +1 -0
- package/dist/auth/scoped-tokens.test.js +306 -0
- package/dist/auth/tenant-access.test.d.ts +1 -0
- package/dist/auth/tenant-access.test.js +62 -0
- package/dist/auth/user-creator.d.ts +9 -0
- package/dist/auth/user-creator.js +47 -0
- package/dist/auth/user-creator.test.d.ts +1 -0
- package/dist/auth/user-creator.test.js +78 -0
- package/dist/auth/user-role-repository.d.ts +31 -0
- package/dist/auth/user-role-repository.js +53 -0
- package/dist/auth/user-role-repository.test.d.ts +1 -0
- package/dist/auth/user-role-repository.test.js +122 -0
- package/dist/billing/drizzle-webhook-seen-repository.d.ts +10 -0
- package/dist/billing/drizzle-webhook-seen-repository.js +28 -0
- package/dist/billing/index.d.ts +7 -0
- package/dist/billing/index.js +7 -0
- package/dist/billing/payment-processor.d.ts +127 -0
- package/dist/billing/payment-processor.js +8 -0
- package/dist/billing/payment-processor.test.d.ts +1 -0
- package/dist/billing/payment-processor.test.js +71 -0
- package/dist/billing/payram/cents-credits-boundary.test.d.ts +1 -0
- package/dist/billing/payram/cents-credits-boundary.test.js +75 -0
- package/dist/billing/payram/charge-store.d.ts +41 -0
- package/dist/billing/payram/charge-store.js +72 -0
- package/dist/billing/payram/charge-store.test.d.ts +1 -0
- package/dist/billing/payram/charge-store.test.js +64 -0
- package/dist/billing/payram/checkout.d.ts +15 -0
- package/dist/billing/payram/checkout.js +24 -0
- package/dist/billing/payram/checkout.test.d.ts +1 -0
- package/dist/billing/payram/checkout.test.js +74 -0
- package/dist/billing/payram/client.d.ts +7 -0
- package/dist/billing/payram/client.js +15 -0
- package/dist/billing/payram/client.test.d.ts +1 -0
- package/dist/billing/payram/client.test.js +52 -0
- package/dist/billing/payram/index.d.ts +8 -0
- package/dist/billing/payram/index.js +4 -0
- package/dist/billing/payram/types.d.ts +40 -0
- package/dist/billing/payram/types.js +1 -0
- package/dist/billing/payram/webhook.d.ts +19 -0
- package/dist/billing/payram/webhook.js +67 -0
- package/dist/billing/payram/webhook.test.d.ts +7 -0
- package/dist/billing/payram/webhook.test.js +248 -0
- package/dist/billing/stripe/cents-credits-boundary.test.d.ts +1 -0
- package/dist/billing/stripe/cents-credits-boundary.test.js +62 -0
- package/dist/billing/stripe/checkout.d.ts +20 -0
- package/dist/billing/stripe/checkout.js +63 -0
- package/dist/billing/stripe/checkout.test.d.ts +1 -0
- package/dist/billing/stripe/checkout.test.js +148 -0
- package/dist/billing/stripe/client.d.ts +14 -0
- package/dist/billing/stripe/client.js +33 -0
- package/dist/billing/stripe/client.test.d.ts +1 -0
- package/dist/billing/stripe/client.test.js +58 -0
- package/dist/billing/stripe/credit-prices.d.ts +63 -0
- package/dist/billing/stripe/credit-prices.js +81 -0
- package/dist/billing/stripe/credit-prices.test.d.ts +1 -0
- package/dist/billing/stripe/credit-prices.test.js +87 -0
- package/dist/billing/stripe/index.d.ts +14 -0
- package/dist/billing/stripe/index.js +8 -0
- package/dist/billing/stripe/payment-methods-detach-all.test.d.ts +1 -0
- package/dist/billing/stripe/payment-methods-detach-all.test.js +40 -0
- package/dist/billing/stripe/payment-methods.d.ts +25 -0
- package/dist/billing/stripe/payment-methods.js +53 -0
- package/dist/billing/stripe/payment-methods.test.d.ts +1 -0
- package/dist/billing/stripe/payment-methods.test.js +122 -0
- package/dist/billing/stripe/portal.d.ts +10 -0
- package/dist/billing/stripe/portal.js +16 -0
- package/dist/billing/stripe/portal.test.d.ts +1 -0
- package/dist/billing/stripe/portal.test.js +48 -0
- package/dist/billing/stripe/setup-intent.d.ts +16 -0
- package/dist/billing/stripe/setup-intent.js +22 -0
- package/dist/billing/stripe/setup-intent.test.d.ts +1 -0
- package/dist/billing/stripe/setup-intent.test.js +58 -0
- package/dist/billing/stripe/stripe-payment-processor.d.ts +49 -0
- package/dist/billing/stripe/stripe-payment-processor.js +166 -0
- package/dist/billing/stripe/stripe-payment-processor.test.d.ts +1 -0
- package/dist/billing/stripe/stripe-payment-processor.test.js +413 -0
- package/dist/billing/stripe/tenant-store.d.ts +56 -0
- package/dist/billing/stripe/tenant-store.js +119 -0
- package/dist/billing/stripe/tenant-store.test.d.ts +1 -0
- package/dist/billing/stripe/tenant-store.test.js +97 -0
- package/dist/billing/stripe/types.d.ts +49 -0
- package/dist/billing/stripe/types.js +1 -0
- package/dist/billing/webhook-seen-repository.d.ts +14 -0
- package/dist/billing/webhook-seen-repository.js +13 -0
- package/dist/config/billing-env.test.d.ts +1 -0
- package/dist/config/billing-env.test.js +48 -0
- package/dist/config/index.d.ts +46 -0
- package/dist/config/index.js +38 -0
- package/dist/config/logger.d.ts +2 -0
- package/dist/config/logger.js +11 -0
- package/dist/config/provider-endpoints.d.ts +6 -0
- package/dist/config/provider-endpoints.js +12 -0
- package/dist/credits/auto-topup-charge.d.ts +27 -0
- package/dist/credits/auto-topup-charge.js +139 -0
- package/dist/credits/auto-topup-charge.test.d.ts +1 -0
- package/dist/credits/auto-topup-charge.test.js +242 -0
- package/dist/credits/auto-topup-event-log-repository.d.ts +16 -0
- package/dist/credits/auto-topup-event-log-repository.js +18 -0
- package/dist/credits/auto-topup-event-log-repository.test.d.ts +1 -0
- package/dist/credits/auto-topup-event-log-repository.test.js +83 -0
- package/dist/credits/auto-topup-schedule.d.ts +27 -0
- package/dist/credits/auto-topup-schedule.js +66 -0
- package/dist/credits/auto-topup-schedule.test.d.ts +1 -0
- package/dist/credits/auto-topup-schedule.test.js +145 -0
- package/dist/credits/auto-topup-settings-repository.d.ts +54 -0
- package/dist/credits/auto-topup-settings-repository.js +184 -0
- package/dist/credits/auto-topup-settings-repository.test.d.ts +1 -0
- package/dist/credits/auto-topup-settings-repository.test.js +104 -0
- package/dist/credits/auto-topup-usage.d.ts +22 -0
- package/dist/credits/auto-topup-usage.js +56 -0
- package/dist/credits/auto-topup-usage.test.d.ts +1 -0
- package/dist/credits/auto-topup-usage.test.js +181 -0
- package/dist/credits/credit-expiry-cron.d.ts +19 -0
- package/dist/credits/credit-expiry-cron.js +50 -0
- package/dist/credits/credit-expiry-cron.test.d.ts +1 -0
- package/dist/credits/credit-expiry-cron.test.js +67 -0
- package/dist/credits/credit-ledger-extra.test.d.ts +1 -0
- package/dist/credits/credit-ledger-extra.test.js +40 -0
- package/dist/credits/credit-ledger.bench.d.ts +1 -0
- package/dist/credits/credit-ledger.bench.js +33 -0
- package/dist/credits/credit-ledger.d.ts +130 -0
- package/dist/credits/credit-ledger.js +293 -0
- package/dist/credits/credit-ledger.test.d.ts +4 -0
- package/dist/credits/credit-ledger.test.js +203 -0
- package/dist/credits/credit-transaction-repository.d.ts +17 -0
- package/dist/credits/credit-transaction-repository.js +35 -0
- package/dist/credits/credit-transaction-repository.test.d.ts +1 -0
- package/dist/credits/credit-transaction-repository.test.js +232 -0
- package/dist/credits/credit.d.ts +75 -0
- package/dist/credits/credit.js +139 -0
- package/dist/credits/credit.test.d.ts +1 -0
- package/dist/credits/credit.test.js +196 -0
- package/dist/credits/dividend-cron.d.ts +29 -0
- package/dist/credits/dividend-cron.js +88 -0
- package/dist/credits/dividend-cron.test.d.ts +1 -0
- package/dist/credits/dividend-cron.test.js +128 -0
- package/dist/credits/dividend-repository.d.ts +29 -0
- package/dist/credits/dividend-repository.js +126 -0
- package/dist/credits/dividend-repository.test.d.ts +1 -0
- package/dist/credits/dividend-repository.test.js +176 -0
- package/dist/credits/index.d.ts +9 -0
- package/dist/credits/index.js +5 -0
- package/dist/credits/repository-types.d.ts +29 -0
- package/dist/credits/repository-types.js +1 -0
- package/dist/credits/signup-grant.d.ts +12 -0
- package/dist/credits/signup-grant.js +35 -0
- package/dist/credits/signup-grant.test.d.ts +1 -0
- package/dist/credits/signup-grant.test.js +51 -0
- package/dist/credits/tenant-customer-repository.d.ts +30 -0
- package/dist/credits/tenant-customer-repository.js +5 -0
- package/dist/db/auth-user-repository.d.ts +46 -0
- package/dist/db/auth-user-repository.js +90 -0
- package/dist/db/credit-column.d.ts +27 -0
- package/dist/db/credit-column.js +13 -0
- package/dist/db/index.d.ts +14 -0
- package/dist/db/index.js +8 -0
- package/dist/db/schema/account-deletion-requests.d.ts +203 -0
- package/dist/db/schema/account-deletion-requests.js +36 -0
- package/dist/db/schema/account-export-requests.d.ts +148 -0
- package/dist/db/schema/account-export-requests.js +19 -0
- package/dist/db/schema/admin-audit.d.ts +194 -0
- package/dist/db/schema/admin-audit.js +21 -0
- package/dist/db/schema/admin-users.d.ts +177 -0
- package/dist/db/schema/admin-users.js +23 -0
- package/dist/db/schema/affiliate-fraud.d.ts +160 -0
- package/dist/db/schema/affiliate-fraud.js +18 -0
- package/dist/db/schema/affiliate.d.ts +277 -0
- package/dist/db/schema/affiliate.js +32 -0
- package/dist/db/schema/coupon-codes.d.ts +143 -0
- package/dist/db/schema/coupon-codes.js +17 -0
- package/dist/db/schema/credit-auto-topup-settings.d.ts +232 -0
- package/dist/db/schema/credit-auto-topup-settings.js +27 -0
- package/dist/db/schema/credit-auto-topup.d.ts +130 -0
- package/dist/db/schema/credit-auto-topup.js +21 -0
- package/dist/db/schema/credits.d.ts +283 -0
- package/dist/db/schema/credits.js +38 -0
- package/dist/db/schema/dividend-distributions.d.ts +130 -0
- package/dist/db/schema/dividend-distributions.js +19 -0
- package/dist/db/schema/email-notifications.d.ts +99 -0
- package/dist/db/schema/email-notifications.js +21 -0
- package/dist/db/schema/index.d.ts +33 -0
- package/dist/db/schema/index.js +33 -0
- package/dist/db/schema/meter-events.d.ts +599 -0
- package/dist/db/schema/meter-events.js +55 -0
- package/dist/db/schema/notification-preferences.d.ts +165 -0
- package/dist/db/schema/notification-preferences.js +18 -0
- package/dist/db/schema/notification-queue.d.ts +236 -0
- package/dist/db/schema/notification-queue.js +40 -0
- package/dist/db/schema/org-memberships.d.ts +63 -0
- package/dist/db/schema/org-memberships.js +15 -0
- package/dist/db/schema/organization-members.d.ts +235 -0
- package/dist/db/schema/organization-members.js +27 -0
- package/dist/db/schema/payram.d.ts +164 -0
- package/dist/db/schema/payram.js +21 -0
- package/dist/db/schema/platform-api-keys.d.ts +143 -0
- package/dist/db/schema/platform-api-keys.js +20 -0
- package/dist/db/schema/promotion-redemptions.d.ts +143 -0
- package/dist/db/schema/promotion-redemptions.js +18 -0
- package/dist/db/schema/promotions.d.ts +445 -0
- package/dist/db/schema/promotions.js +48 -0
- package/dist/db/schema/provider-credentials.d.ts +201 -0
- package/dist/db/schema/provider-credentials.js +36 -0
- package/dist/db/schema/rate-limit-entries.d.ts +75 -0
- package/dist/db/schema/rate-limit-entries.js +7 -0
- package/dist/db/schema/secret-audit-log.d.ts +109 -0
- package/dist/db/schema/secret-audit-log.js +15 -0
- package/dist/db/schema/session-usage.d.ts +194 -0
- package/dist/db/schema/session-usage.js +19 -0
- package/dist/db/schema/spending-limits.d.ts +92 -0
- package/dist/db/schema/spending-limits.js +8 -0
- package/dist/db/schema/tenant-addons.d.ts +58 -0
- package/dist/db/schema/tenant-addons.js +9 -0
- package/dist/db/schema/tenant-api-keys.d.ts +131 -0
- package/dist/db/schema/tenant-api-keys.js +21 -0
- package/dist/db/schema/tenant-capability-settings.d.ts +79 -0
- package/dist/db/schema/tenant-capability-settings.js +12 -0
- package/dist/db/schema/tenant-customers.d.ts +303 -0
- package/dist/db/schema/tenant-customers.js +25 -0
- package/dist/db/schema/tenants.d.ts +126 -0
- package/dist/db/schema/tenants.js +18 -0
- package/dist/db/schema/user-roles.d.ts +98 -0
- package/dist/db/schema/user-roles.js +18 -0
- package/dist/db/schema/webhook-seen-events.d.ts +58 -0
- package/dist/db/schema/webhook-seen-events.js +9 -0
- package/dist/email/billing-emails.d.ts +51 -0
- package/dist/email/billing-emails.js +163 -0
- package/dist/email/billing-emails.test.d.ts +1 -0
- package/dist/email/billing-emails.test.js +162 -0
- package/dist/email/client.d.ts +51 -0
- package/dist/email/client.js +102 -0
- package/dist/email/client.test.d.ts +1 -0
- package/dist/email/client.test.js +120 -0
- package/dist/email/drizzle-billing-email-repository.d.ts +21 -0
- package/dist/email/drizzle-billing-email-repository.js +36 -0
- package/dist/email/drizzle-billing-email-repository.test.d.ts +1 -0
- package/dist/email/drizzle-billing-email-repository.test.js +42 -0
- package/dist/email/index.d.ts +33 -0
- package/dist/email/index.js +22 -0
- package/dist/email/notification-preferences-store.d.ts +12 -0
- package/dist/email/notification-preferences-store.js +82 -0
- package/dist/email/notification-preferences-store.test.d.ts +1 -0
- package/dist/email/notification-preferences-store.test.js +86 -0
- package/dist/email/notification-queue-store.d.ts +25 -0
- package/dist/email/notification-queue-store.js +97 -0
- package/dist/email/notification-queue-store.test.d.ts +1 -0
- package/dist/email/notification-queue-store.test.js +177 -0
- package/dist/email/notification-repository-types.d.ts +70 -0
- package/dist/email/notification-repository-types.js +6 -0
- package/dist/email/notification-service.d.ts +41 -0
- package/dist/email/notification-service.js +196 -0
- package/dist/email/notification-service.test.d.ts +1 -0
- package/dist/email/notification-service.test.js +160 -0
- package/dist/email/notification-templates.d.ts +18 -0
- package/dist/email/notification-templates.js +574 -0
- package/dist/email/notification-templates.test.d.ts +1 -0
- package/dist/email/notification-templates.test.js +238 -0
- package/dist/email/notification-worker.d.ts +24 -0
- package/dist/email/notification-worker.js +109 -0
- package/dist/email/notification-worker.test.d.ts +1 -0
- package/dist/email/notification-worker.test.js +153 -0
- package/dist/email/require-verified.d.ts +25 -0
- package/dist/email/require-verified.js +52 -0
- package/dist/email/require-verified.test.d.ts +1 -0
- package/dist/email/require-verified.test.js +62 -0
- package/dist/email/resend-adapter.d.ts +47 -0
- package/dist/email/resend-adapter.js +137 -0
- package/dist/email/resend-adapter.test.d.ts +1 -0
- package/dist/email/resend-adapter.test.js +190 -0
- package/dist/email/templates.d.ts +22 -0
- package/dist/email/templates.js +359 -0
- package/dist/email/templates.test.d.ts +1 -0
- package/dist/email/templates.test.js +170 -0
- package/dist/email/verification.d.ts +42 -0
- package/dist/email/verification.js +83 -0
- package/dist/email/verification.test.d.ts +1 -0
- package/dist/email/verification.test.js +141 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +23 -0
- package/dist/metering/aggregator.d.ts +54 -0
- package/dist/metering/aggregator.js +123 -0
- package/dist/metering/aggregator.test.d.ts +1 -0
- package/dist/metering/aggregator.test.js +179 -0
- package/dist/metering/dlq.d.ts +31 -0
- package/dist/metering/dlq.js +82 -0
- package/dist/metering/dlq.test.d.ts +1 -0
- package/dist/metering/dlq.test.js +117 -0
- package/dist/metering/drizzle-usage-summary-repository.d.ts +67 -0
- package/dist/metering/drizzle-usage-summary-repository.js +98 -0
- package/dist/metering/emitter.d.ts +66 -0
- package/dist/metering/emitter.js +185 -0
- package/dist/metering/emitter.test.d.ts +1 -0
- package/dist/metering/emitter.test.js +171 -0
- package/dist/metering/index.d.ts +11 -0
- package/dist/metering/index.js +5 -0
- package/dist/metering/load-test.bench.d.ts +1 -0
- package/dist/metering/load-test.bench.js +103 -0
- package/dist/metering/meter-event-repository.d.ts +33 -0
- package/dist/metering/meter-event-repository.js +58 -0
- package/dist/metering/meter-repositories.test.d.ts +1 -0
- package/dist/metering/meter-repositories.test.js +419 -0
- package/dist/metering/metering.test.d.ts +1 -0
- package/dist/metering/metering.test.js +1046 -0
- package/dist/metering/reconciliation-cron.d.ts +37 -0
- package/dist/metering/reconciliation-cron.js +85 -0
- package/dist/metering/reconciliation-cron.test.d.ts +1 -0
- package/dist/metering/reconciliation-cron.test.js +162 -0
- package/dist/metering/reconciliation-repository.d.ts +27 -0
- package/dist/metering/reconciliation-repository.js +43 -0
- package/dist/metering/reconciliation-repository.test.d.ts +1 -0
- package/dist/metering/reconciliation-repository.test.js +160 -0
- package/dist/metering/types.d.ts +88 -0
- package/dist/metering/types.js +1 -0
- package/dist/metering/wal.d.ts +49 -0
- package/dist/metering/wal.js +124 -0
- package/dist/metering/wal.test.d.ts +1 -0
- package/dist/metering/wal.test.js +175 -0
- package/dist/middleware/csrf.d.ts +24 -0
- package/dist/middleware/csrf.js +80 -0
- package/dist/middleware/csrf.test.d.ts +1 -0
- package/dist/middleware/csrf.test.js +152 -0
- package/dist/middleware/drizzle-rate-limit-repository.d.ts +9 -0
- package/dist/middleware/drizzle-rate-limit-repository.js +52 -0
- package/dist/middleware/drizzle-rate-limit-repository.test.d.ts +1 -0
- package/dist/middleware/drizzle-rate-limit-repository.test.js +74 -0
- package/dist/middleware/get-client-ip.d.ts +22 -0
- package/dist/middleware/get-client-ip.js +51 -0
- package/dist/middleware/get-client-ip.test.d.ts +1 -0
- package/dist/middleware/get-client-ip.test.js +40 -0
- package/dist/middleware/index.d.ts +5 -0
- package/dist/middleware/index.js +4 -0
- package/dist/middleware/rate-limit-repository.d.ts +19 -0
- package/dist/middleware/rate-limit-repository.js +1 -0
- package/dist/middleware/rate-limit.d.ts +57 -0
- package/dist/middleware/rate-limit.js +109 -0
- package/dist/middleware/rate-limit.test.d.ts +1 -0
- package/dist/middleware/rate-limit.test.js +247 -0
- package/dist/security/credential-vault/audit-repository.d.ts +27 -0
- package/dist/security/credential-vault/audit-repository.js +42 -0
- package/dist/security/credential-vault/audit-repository.test.d.ts +1 -0
- package/dist/security/credential-vault/audit-repository.test.js +78 -0
- package/dist/security/credential-vault/credential-repository.d.ts +94 -0
- package/dist/security/credential-vault/credential-repository.js +145 -0
- package/dist/security/credential-vault/credential-repository.test.d.ts +1 -0
- package/dist/security/credential-vault/credential-repository.test.js +206 -0
- package/dist/security/credential-vault/index.d.ts +12 -0
- package/dist/security/credential-vault/index.js +6 -0
- package/dist/security/credential-vault/key-rotation.d.ts +18 -0
- package/dist/security/credential-vault/key-rotation.js +52 -0
- package/dist/security/credential-vault/key-rotation.test.d.ts +1 -0
- package/dist/security/credential-vault/key-rotation.test.js +95 -0
- package/dist/security/credential-vault/migrate-plaintext.d.ts +15 -0
- package/dist/security/credential-vault/migrate-plaintext.js +80 -0
- package/dist/security/credential-vault/migrate-plaintext.test.d.ts +1 -0
- package/dist/security/credential-vault/migrate-plaintext.test.js +111 -0
- package/dist/security/credential-vault/migration-check.d.ts +15 -0
- package/dist/security/credential-vault/migration-check.js +71 -0
- package/dist/security/credential-vault/migration-check.test.d.ts +1 -0
- package/dist/security/credential-vault/migration-check.test.js +457 -0
- package/dist/security/credential-vault/store.d.ts +106 -0
- package/dist/security/credential-vault/store.js +181 -0
- package/dist/security/credential-vault/store.test.d.ts +1 -0
- package/dist/security/credential-vault/store.test.js +482 -0
- package/dist/security/encryption.d.ts +22 -0
- package/dist/security/encryption.js +53 -0
- package/dist/security/encryption.test.d.ts +1 -0
- package/dist/security/encryption.test.js +95 -0
- package/dist/security/host-validation.d.ts +11 -0
- package/dist/security/host-validation.js +108 -0
- package/dist/security/host-validation.test.d.ts +1 -0
- package/dist/security/host-validation.test.js +106 -0
- package/dist/security/index.d.ts +11 -0
- package/dist/security/index.js +11 -0
- package/dist/security/key-audit.d.ts +16 -0
- package/dist/security/key-audit.js +35 -0
- package/dist/security/key-audit.test.d.ts +1 -0
- package/dist/security/key-audit.test.js +50 -0
- package/dist/security/key-injection.d.ts +28 -0
- package/dist/security/key-injection.js +57 -0
- package/dist/security/key-injection.test.d.ts +1 -0
- package/dist/security/key-injection.test.js +97 -0
- package/dist/security/key-validation.d.ts +16 -0
- package/dist/security/key-validation.js +78 -0
- package/dist/security/key-validation.test.d.ts +1 -0
- package/dist/security/key-validation.test.js +87 -0
- package/dist/security/redirect-allowlist.d.ts +6 -0
- package/dist/security/redirect-allowlist.js +36 -0
- package/dist/security/redirect-allowlist.test.d.ts +1 -0
- package/dist/security/redirect-allowlist.test.js +55 -0
- package/dist/security/tenant-keys/capability-settings-store.d.ts +22 -0
- package/dist/security/tenant-keys/capability-settings-store.js +33 -0
- package/dist/security/tenant-keys/capability-settings-store.test.d.ts +1 -0
- package/dist/security/tenant-keys/capability-settings-store.test.js +77 -0
- package/dist/security/tenant-keys/index.d.ts +10 -0
- package/dist/security/tenant-keys/index.js +5 -0
- package/dist/security/tenant-keys/key-resolution-repository.d.ts +15 -0
- package/dist/security/tenant-keys/key-resolution-repository.js +18 -0
- package/dist/security/tenant-keys/key-resolution-repository.test.d.ts +1 -0
- package/dist/security/tenant-keys/key-resolution-repository.test.js +72 -0
- package/dist/security/tenant-keys/key-resolution.d.ts +39 -0
- package/dist/security/tenant-keys/key-resolution.js +59 -0
- package/dist/security/tenant-keys/key-resolution.test.d.ts +1 -0
- package/dist/security/tenant-keys/key-resolution.test.js +97 -0
- package/dist/security/tenant-keys/org-key-resolution.d.ts +30 -0
- package/dist/security/tenant-keys/org-key-resolution.js +50 -0
- package/dist/security/tenant-keys/org-key-resolution.test.d.ts +1 -0
- package/dist/security/tenant-keys/org-key-resolution.test.js +103 -0
- package/dist/security/tenant-keys/tenant-key-repository.d.ts +36 -0
- package/dist/security/tenant-keys/tenant-key-repository.js +96 -0
- package/dist/security/tenant-keys/tenant-key-repository.test.d.ts +1 -0
- package/dist/security/tenant-keys/tenant-key-repository.test.js +114 -0
- package/dist/security/types.d.ts +35 -0
- package/dist/security/types.js +15 -0
- package/dist/tenancy/drizzle-org-repository.d.ts +40 -0
- package/dist/tenancy/drizzle-org-repository.js +126 -0
- package/dist/tenancy/index.d.ts +6 -0
- package/dist/tenancy/index.js +3 -0
- package/dist/tenancy/org-member-repository.d.ts +57 -0
- package/dist/tenancy/org-member-repository.js +99 -0
- package/dist/tenancy/org-repository.test.d.ts +1 -0
- package/dist/tenancy/org-repository.test.js +143 -0
- package/dist/tenancy/org-service.d.ts +70 -0
- package/dist/tenancy/org-service.js +223 -0
- package/dist/tenancy/org-service.test.d.ts +1 -0
- package/dist/tenancy/org-service.test.js +550 -0
- package/dist/test/db.d.ts +33 -0
- package/dist/test/db.js +65 -0
- package/dist/trpc/index.d.ts +1 -0
- package/dist/trpc/index.js +1 -0
- package/dist/trpc/init.d.ts +49 -0
- package/dist/trpc/init.js +108 -0
- package/dist/trpc/init.test.d.ts +1 -0
- package/dist/trpc/init.test.js +154 -0
- package/drizzle/migrations/0000_slippery_mandrill.sql +559 -0
- package/drizzle/migrations/meta/0000_snapshot.json +4374 -0
- package/drizzle/migrations/meta/_journal.json +13 -0
- package/drizzle.config.ts +41 -0
- package/package.json +64 -0
- package/src/admin/admin-audit-log-repository.ts +135 -0
- package/src/admin/audit-log.ts +111 -0
- package/src/admin/index.ts +6 -0
- package/src/admin/role-store.ts +134 -0
- package/src/auth/api-key-repository.test.ts +63 -0
- package/src/auth/api-key-repository.ts +46 -0
- package/src/auth/auth.test.ts +166 -0
- package/src/auth/better-auth.ts +216 -0
- package/src/auth/index.ts +520 -0
- package/src/auth/login-history-repository.test.ts +54 -0
- package/src/auth/login-history-repository.ts +28 -0
- package/src/auth/middleware.test.ts +264 -0
- package/src/auth/middleware.ts +117 -0
- package/src/auth/scoped-tokens.test.ts +362 -0
- package/src/auth/tenant-access.test.ts +69 -0
- package/src/auth/user-creator.test.ts +98 -0
- package/src/auth/user-creator.ts +54 -0
- package/src/auth/user-role-repository.test.ts +149 -0
- package/src/auth/user-role-repository.ts +67 -0
- package/src/billing/drizzle-webhook-seen-repository.ts +34 -0
- package/src/billing/index.ts +22 -0
- package/src/billing/payment-processor.test.ts +93 -0
- package/src/billing/payment-processor.ts +150 -0
- package/src/billing/payram/cents-credits-boundary.test.ts +84 -0
- package/src/billing/payram/charge-store.test.ts +84 -0
- package/src/billing/payram/charge-store.ts +109 -0
- package/src/billing/payram/checkout.test.ts +99 -0
- package/src/billing/payram/checkout.ts +40 -0
- package/src/billing/payram/client.test.ts +62 -0
- package/src/billing/payram/client.ts +21 -0
- package/src/billing/payram/index.ts +14 -0
- package/src/billing/payram/types.ts +44 -0
- package/src/billing/payram/webhook.test.ts +318 -0
- package/src/billing/payram/webhook.ts +97 -0
- package/src/billing/stripe/cents-credits-boundary.test.ts +70 -0
- package/src/billing/stripe/checkout.test.ts +186 -0
- package/src/billing/stripe/checkout.ts +82 -0
- package/src/billing/stripe/client.test.ts +64 -0
- package/src/billing/stripe/client.ts +39 -0
- package/src/billing/stripe/credit-prices.test.ts +114 -0
- package/src/billing/stripe/credit-prices.ts +113 -0
- package/src/billing/stripe/index.ts +14 -0
- package/src/billing/stripe/payment-methods-detach-all.test.ts +53 -0
- package/src/billing/stripe/payment-methods.test.ts +157 -0
- package/src/billing/stripe/payment-methods.ts +76 -0
- package/src/billing/stripe/portal.test.ts +63 -0
- package/src/billing/stripe/portal.ts +25 -0
- package/src/billing/stripe/setup-intent.test.ts +78 -0
- package/src/billing/stripe/setup-intent.ts +34 -0
- package/src/billing/stripe/stripe-payment-processor.test.ts +517 -0
- package/src/billing/stripe/stripe-payment-processor.ts +255 -0
- package/src/billing/stripe/tenant-store.test.ts +124 -0
- package/src/billing/stripe/tenant-store.ts +151 -0
- package/src/billing/stripe/types.ts +53 -0
- package/src/billing/webhook-seen-repository.ts +24 -0
- package/src/config/billing-env.test.ts +54 -0
- package/src/config/index.ts +44 -0
- package/src/config/logger.ts +12 -0
- package/src/config/provider-endpoints.ts +14 -0
- package/src/credits/auto-topup-charge.test.ts +292 -0
- package/src/credits/auto-topup-charge.ts +171 -0
- package/src/credits/auto-topup-event-log-repository.test.ts +99 -0
- package/src/credits/auto-topup-event-log-repository.ts +30 -0
- package/src/credits/auto-topup-schedule.test.ts +179 -0
- package/src/credits/auto-topup-schedule.ts +93 -0
- package/src/credits/auto-topup-settings-repository.test.ts +123 -0
- package/src/credits/auto-topup-settings-repository.ts +245 -0
- package/src/credits/auto-topup-usage.test.ts +220 -0
- package/src/credits/auto-topup-usage.ts +68 -0
- package/src/credits/credit-expiry-cron.test.ts +125 -0
- package/src/credits/credit-expiry-cron.ts +76 -0
- package/src/credits/credit-ledger-extra.test.ts +57 -0
- package/src/credits/credit-ledger.bench.ts +56 -0
- package/src/credits/credit-ledger.test.ts +276 -0
- package/src/credits/credit-ledger.ts +450 -0
- package/src/credits/credit-transaction-repository.test.ts +274 -0
- package/src/credits/credit-transaction-repository.ts +62 -0
- package/src/credits/credit.test.ts +234 -0
- package/src/credits/credit.ts +160 -0
- package/src/credits/dividend-cron.test.ts +158 -0
- package/src/credits/dividend-cron.ts +127 -0
- package/src/credits/dividend-repository.test.ts +223 -0
- package/src/credits/dividend-repository.ts +182 -0
- package/src/credits/index.ts +25 -0
- package/src/credits/repository-types.ts +33 -0
- package/src/credits/signup-grant.test.ts +63 -0
- package/src/credits/signup-grant.ts +44 -0
- package/src/credits/tenant-customer-repository.ts +28 -0
- package/src/db/auth-user-repository.ts +124 -0
- package/src/db/credit-column.ts +17 -0
- package/src/db/index.ts +21 -0
- package/src/db/schema/account-deletion-requests.ts +41 -0
- package/src/db/schema/account-export-requests.ts +24 -0
- package/src/db/schema/admin-audit.ts +26 -0
- package/src/db/schema/admin-users.ts +31 -0
- package/src/db/schema/affiliate-fraud.ts +23 -0
- package/src/db/schema/affiliate.ts +38 -0
- package/src/db/schema/coupon-codes.ts +22 -0
- package/src/db/schema/credit-auto-topup-settings.ts +32 -0
- package/src/db/schema/credit-auto-topup.ts +26 -0
- package/src/db/schema/credits.ts +44 -0
- package/src/db/schema/dividend-distributions.ts +24 -0
- package/src/db/schema/email-notifications.ts +26 -0
- package/src/db/schema/index.ts +33 -0
- package/src/db/schema/meter-events.ts +70 -0
- package/src/db/schema/notification-preferences.ts +19 -0
- package/src/db/schema/notification-queue.ts +45 -0
- package/src/db/schema/org-memberships.ts +20 -0
- package/src/db/schema/organization-members.ts +37 -0
- package/src/db/schema/payram.ts +26 -0
- package/src/db/schema/platform-api-keys.ts +25 -0
- package/src/db/schema/promotion-redemptions.ts +23 -0
- package/src/db/schema/promotions.ts +57 -0
- package/src/db/schema/provider-credentials.ts +41 -0
- package/src/db/schema/rate-limit-entries.ts +12 -0
- package/src/db/schema/secret-audit-log.ts +20 -0
- package/src/db/schema/session-usage.ts +24 -0
- package/src/db/schema/spending-limits.ts +9 -0
- package/src/db/schema/tenant-addons.ts +14 -0
- package/src/db/schema/tenant-api-keys.ts +26 -0
- package/src/db/schema/tenant-capability-settings.ts +17 -0
- package/src/db/schema/tenant-customers.ts +35 -0
- package/src/db/schema/tenants.ts +23 -0
- package/src/db/schema/user-roles.ts +23 -0
- package/src/db/schema/webhook-seen-events.ts +14 -0
- package/src/email/billing-emails.test.ts +198 -0
- package/src/email/billing-emails.ts +211 -0
- package/src/email/client.test.ts +149 -0
- package/src/email/client.ts +137 -0
- package/src/email/drizzle-billing-email-repository.test.ts +52 -0
- package/src/email/drizzle-billing-email-repository.ts +59 -0
- package/src/email/index.ts +57 -0
- package/src/email/notification-preferences-store.test.ts +102 -0
- package/src/email/notification-preferences-store.ts +90 -0
- package/src/email/notification-queue-store.test.ts +215 -0
- package/src/email/notification-queue-store.ts +127 -0
- package/src/email/notification-repository-types.ts +101 -0
- package/src/email/notification-service.test.ts +178 -0
- package/src/email/notification-service.ts +265 -0
- package/src/email/notification-templates.test.ts +261 -0
- package/src/email/notification-templates.ts +727 -0
- package/src/email/notification-worker.test.ts +189 -0
- package/src/email/notification-worker.ts +133 -0
- package/src/email/require-verified.ts +65 -0
- package/src/email/resend-adapter.test.ts +253 -0
- package/src/email/resend-adapter.ts +157 -0
- package/src/email/templates.test.ts +217 -0
- package/src/email/templates.ts +469 -0
- package/src/email/verification.test.ts +185 -0
- package/src/email/verification.ts +110 -0
- package/src/index.ts +51 -0
- package/src/metering/aggregator.test.ts +239 -0
- package/src/metering/aggregator.ts +160 -0
- package/src/metering/dlq.test.ts +134 -0
- package/src/metering/dlq.ts +102 -0
- package/src/metering/drizzle-usage-summary-repository.ts +167 -0
- package/src/metering/emitter.test.ts +202 -0
- package/src/metering/emitter.ts +227 -0
- package/src/metering/index.ts +21 -0
- package/src/metering/load-test.bench.ts +130 -0
- package/src/metering/meter-event-repository.ts +87 -0
- package/src/metering/meter-repositories.test.ts +491 -0
- package/src/metering/metering.test.ts +1317 -0
- package/src/metering/reconciliation-cron.test.ts +202 -0
- package/src/metering/reconciliation-cron.ts +134 -0
- package/src/metering/reconciliation-repository.test.ts +196 -0
- package/src/metering/reconciliation-repository.ts +83 -0
- package/src/metering/types.ts +93 -0
- package/src/metering/wal.test.ts +222 -0
- package/src/metering/wal.ts +139 -0
- package/src/middleware/csrf.test.ts +178 -0
- package/src/middleware/csrf.ts +101 -0
- package/src/middleware/drizzle-rate-limit-repository.test.ts +97 -0
- package/src/middleware/drizzle-rate-limit-repository.ts +57 -0
- package/src/middleware/get-client-ip.test.ts +49 -0
- package/src/middleware/get-client-ip.ts +62 -0
- package/src/middleware/index.ts +12 -0
- package/src/middleware/rate-limit-repository.ts +22 -0
- package/src/middleware/rate-limit.test.ts +338 -0
- package/src/middleware/rate-limit.ts +169 -0
- package/src/security/credential-vault/audit-repository.test.ts +91 -0
- package/src/security/credential-vault/audit-repository.ts +64 -0
- package/src/security/credential-vault/credential-repository.test.ts +264 -0
- package/src/security/credential-vault/credential-repository.ts +233 -0
- package/src/security/credential-vault/index.ts +26 -0
- package/src/security/credential-vault/key-rotation.test.ts +139 -0
- package/src/security/credential-vault/key-rotation.ts +70 -0
- package/src/security/credential-vault/migrate-plaintext.test.ts +138 -0
- package/src/security/credential-vault/migrate-plaintext.ts +101 -0
- package/src/security/credential-vault/migration-check.test.ts +533 -0
- package/src/security/credential-vault/migration-check.ts +88 -0
- package/src/security/credential-vault/store.test.ts +569 -0
- package/src/security/credential-vault/store.ts +284 -0
- package/src/security/encryption.test.ts +114 -0
- package/src/security/encryption.ts +65 -0
- package/src/security/host-validation.test.ts +136 -0
- package/src/security/host-validation.ts +116 -0
- package/src/security/index.ts +59 -0
- package/src/security/key-audit.test.ts +57 -0
- package/src/security/key-audit.ts +45 -0
- package/src/security/key-injection.test.ts +131 -0
- package/src/security/key-injection.ts +71 -0
- package/src/security/key-validation.test.ts +111 -0
- package/src/security/key-validation.ts +84 -0
- package/src/security/redirect-allowlist.test.ts +70 -0
- package/src/security/redirect-allowlist.ts +35 -0
- package/src/security/tenant-keys/capability-settings-store.test.ts +98 -0
- package/src/security/tenant-keys/capability-settings-store.ts +53 -0
- package/src/security/tenant-keys/index.ts +10 -0
- package/src/security/tenant-keys/key-resolution-repository.test.ts +95 -0
- package/src/security/tenant-keys/key-resolution-repository.ts +31 -0
- package/src/security/tenant-keys/key-resolution.test.ts +173 -0
- package/src/security/tenant-keys/key-resolution.ts +87 -0
- package/src/security/tenant-keys/org-key-resolution.test.ts +217 -0
- package/src/security/tenant-keys/org-key-resolution.ts +76 -0
- package/src/security/tenant-keys/tenant-key-repository.test.ts +143 -0
- package/src/security/tenant-keys/tenant-key-repository.ts +130 -0
- package/src/security/types.ts +43 -0
- package/src/tenancy/drizzle-org-repository.ts +169 -0
- package/src/tenancy/index.ts +6 -0
- package/src/tenancy/org-member-repository.ts +159 -0
- package/src/tenancy/org-repository.test.ts +172 -0
- package/src/tenancy/org-service.test.ts +634 -0
- package/src/tenancy/org-service.ts +290 -0
- package/src/test/db.ts +97 -0
- package/src/trpc/index.ts +11 -0
- package/src/trpc/init.test.ts +196 -0
- package/src/trpc/init.ts +138 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { PGlite } from "@electric-sql/pglite";
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
3
|
+
import { generateVerificationToken, getUserEmail, initVerificationSchema, isEmailVerified, verifyToken, } from "./verification.js";
|
|
4
|
+
/** Minimal Pool-like wrapper around PGlite for testing. */
|
|
5
|
+
// biome-ignore lint/suspicious/noExplicitAny: test helper wrapping PGlite as Pool
|
|
6
|
+
function pgliteAsPool(pg) {
|
|
7
|
+
return { query: (text, params) => pg.query(text, params) };
|
|
8
|
+
}
|
|
9
|
+
describe("email verification", () => {
|
|
10
|
+
let pg;
|
|
11
|
+
// biome-ignore lint/suspicious/noExplicitAny: test pool wrapper
|
|
12
|
+
let pool;
|
|
13
|
+
beforeEach(async () => {
|
|
14
|
+
pg = new PGlite();
|
|
15
|
+
pool = pgliteAsPool(pg);
|
|
16
|
+
// Create a minimal user table mimicking better-auth's schema
|
|
17
|
+
await pg.query(`
|
|
18
|
+
CREATE TABLE "user" (
|
|
19
|
+
id TEXT PRIMARY KEY,
|
|
20
|
+
email TEXT NOT NULL,
|
|
21
|
+
name TEXT,
|
|
22
|
+
"createdAt" TEXT,
|
|
23
|
+
"emailVerified" boolean DEFAULT false
|
|
24
|
+
)
|
|
25
|
+
`);
|
|
26
|
+
await initVerificationSchema(pool);
|
|
27
|
+
// Insert test users
|
|
28
|
+
await pg.query(`INSERT INTO "user" (id, email, name) VALUES ($1, $2, $3)`, ["user-1", "alice@test.com", "Alice"]);
|
|
29
|
+
await pg.query(`INSERT INTO "user" (id, email, name) VALUES ($1, $2, $3)`, ["user-2", "bob@test.com", "Bob"]);
|
|
30
|
+
});
|
|
31
|
+
afterEach(async () => {
|
|
32
|
+
await pg.close();
|
|
33
|
+
});
|
|
34
|
+
describe("initVerificationSchema", () => {
|
|
35
|
+
it("should add verification columns idempotently", async () => {
|
|
36
|
+
// Call again — should not throw
|
|
37
|
+
await initVerificationSchema(pool);
|
|
38
|
+
const { rows } = await pg.query(`SELECT column_name FROM information_schema.columns WHERE table_name = 'user' ORDER BY ordinal_position`);
|
|
39
|
+
const names = rows.map((r) => r.column_name);
|
|
40
|
+
expect(names).toContain("email_verified");
|
|
41
|
+
expect(names).toContain("verification_token");
|
|
42
|
+
expect(names).toContain("verification_expires");
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
describe("generateVerificationToken", () => {
|
|
46
|
+
it("should generate a 64-char hex token", async () => {
|
|
47
|
+
const result = await generateVerificationToken(pool, "user-1");
|
|
48
|
+
expect(result.token).toHaveLength(64);
|
|
49
|
+
expect(result.token).toMatch(/^[a-f0-9]+$/);
|
|
50
|
+
});
|
|
51
|
+
it("should store token and expiry in the database", async () => {
|
|
52
|
+
const result = await generateVerificationToken(pool, "user-1");
|
|
53
|
+
const { rows } = await pg.query(`SELECT verification_token, verification_expires FROM "user" WHERE id = $1`, [
|
|
54
|
+
"user-1",
|
|
55
|
+
]);
|
|
56
|
+
const row = rows[0];
|
|
57
|
+
expect(row.verification_token).toBe(result.token);
|
|
58
|
+
expect(row.verification_expires).toBe(result.expiresAt);
|
|
59
|
+
});
|
|
60
|
+
it("should set expiry 24 hours in the future", async () => {
|
|
61
|
+
const before = Date.now();
|
|
62
|
+
const result = await generateVerificationToken(pool, "user-1");
|
|
63
|
+
const after = Date.now();
|
|
64
|
+
const expiresMs = new Date(result.expiresAt).getTime();
|
|
65
|
+
const twentyFourHours = 24 * 60 * 60 * 1000;
|
|
66
|
+
expect(expiresMs).toBeGreaterThanOrEqual(before + twentyFourHours);
|
|
67
|
+
expect(expiresMs).toBeLessThanOrEqual(after + twentyFourHours);
|
|
68
|
+
});
|
|
69
|
+
it("should overwrite previous token on re-generation", async () => {
|
|
70
|
+
const first = await generateVerificationToken(pool, "user-1");
|
|
71
|
+
const second = await generateVerificationToken(pool, "user-1");
|
|
72
|
+
expect(first.token).not.toBe(second.token);
|
|
73
|
+
const { rows } = await pg.query(`SELECT verification_token FROM "user" WHERE id = $1`, ["user-1"]);
|
|
74
|
+
const row = rows[0];
|
|
75
|
+
expect(row.verification_token).toBe(second.token);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
describe("verifyToken", () => {
|
|
79
|
+
it("should verify a valid token and mark user as verified", async () => {
|
|
80
|
+
const { token } = await generateVerificationToken(pool, "user-1");
|
|
81
|
+
const result = await verifyToken(pool, token);
|
|
82
|
+
expect(result).toEqual({ userId: "user-1", email: "alice@test.com" });
|
|
83
|
+
const { rows } = await pg.query(`SELECT email_verified, verification_token FROM "user" WHERE id = $1`, [
|
|
84
|
+
"user-1",
|
|
85
|
+
]);
|
|
86
|
+
const row = rows[0];
|
|
87
|
+
expect(row.email_verified).toBe(true);
|
|
88
|
+
expect(row.verification_token).toBeNull();
|
|
89
|
+
});
|
|
90
|
+
it("should return null for non-existent token", async () => {
|
|
91
|
+
expect(await verifyToken(pool, "a".repeat(64))).toBeNull();
|
|
92
|
+
});
|
|
93
|
+
it("should return null for empty token", async () => {
|
|
94
|
+
expect(await verifyToken(pool, "")).toBeNull();
|
|
95
|
+
});
|
|
96
|
+
it("should return null for wrong-length token", async () => {
|
|
97
|
+
expect(await verifyToken(pool, "abc")).toBeNull();
|
|
98
|
+
});
|
|
99
|
+
it("should return null for expired token", async () => {
|
|
100
|
+
const { token } = await generateVerificationToken(pool, "user-1");
|
|
101
|
+
await pg.query(`UPDATE "user" SET verification_expires = $1 WHERE id = $2`, [
|
|
102
|
+
new Date(Date.now() - 1000).toISOString(),
|
|
103
|
+
"user-1",
|
|
104
|
+
]);
|
|
105
|
+
expect(await verifyToken(pool, token)).toBeNull();
|
|
106
|
+
});
|
|
107
|
+
it("should return null for already-verified user", async () => {
|
|
108
|
+
const { token } = await generateVerificationToken(pool, "user-1");
|
|
109
|
+
await pg.query(`UPDATE "user" SET email_verified = true WHERE id = $1`, ["user-1"]);
|
|
110
|
+
expect(await verifyToken(pool, token)).toBeNull();
|
|
111
|
+
});
|
|
112
|
+
it("should only allow single verification per token", async () => {
|
|
113
|
+
const { token } = await generateVerificationToken(pool, "user-1");
|
|
114
|
+
const first = await verifyToken(pool, token);
|
|
115
|
+
const second = await verifyToken(pool, token);
|
|
116
|
+
expect(first).toEqual({ userId: "user-1", email: "alice@test.com" });
|
|
117
|
+
expect(second).toBeNull();
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
describe("isEmailVerified", () => {
|
|
121
|
+
it("should return false for unverified user", async () => {
|
|
122
|
+
expect(await isEmailVerified(pool, "user-1")).toBe(false);
|
|
123
|
+
});
|
|
124
|
+
it("should return true after verification", async () => {
|
|
125
|
+
const { token } = await generateVerificationToken(pool, "user-1");
|
|
126
|
+
await verifyToken(pool, token);
|
|
127
|
+
expect(await isEmailVerified(pool, "user-1")).toBe(true);
|
|
128
|
+
});
|
|
129
|
+
it("should return false for non-existent user", async () => {
|
|
130
|
+
expect(await isEmailVerified(pool, "no-such-user")).toBe(false);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
describe("getUserEmail", () => {
|
|
134
|
+
it("should return email for existing user", async () => {
|
|
135
|
+
expect(await getUserEmail(pool, "user-1")).toBe("alice@test.com");
|
|
136
|
+
});
|
|
137
|
+
it("should return null for non-existent user", async () => {
|
|
138
|
+
expect(await getUserEmail(pool, "no-such-user")).toBeNull();
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
});
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type { PlatformDb, PlatformSchema } from "./db/index.js";
|
|
2
|
+
export { createDb, schema } from "./db/index.js";
|
|
3
|
+
export * from "./admin/index.js";
|
|
4
|
+
export * from "./auth/index.js";
|
|
5
|
+
export { PaymentMethodOwnershipError, noOpReplayGuard, DrizzleWebhookSeenRepository, type SavedPaymentMethod, type CheckoutOpts, type CheckoutSession, type ChargeOpts, type ChargeResult, type SetupResult, type PortalOpts, type WebhookResult, type IPaymentProcessor, type Invoice, type IWebhookSeenRepository, } from "./billing/index.js";
|
|
6
|
+
export { config, billingConfigSchema, type PlatformConfig } from "./config/index.js";
|
|
7
|
+
export * from "./credits/index.js";
|
|
8
|
+
export * from "./email/index.js";
|
|
9
|
+
export * from "./metering/index.js";
|
|
10
|
+
export * from "./middleware/index.js";
|
|
11
|
+
export * from "./security/index.js";
|
|
12
|
+
export * from "./tenancy/index.js";
|
|
13
|
+
export * from "./trpc/index.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export { createDb, schema } from "./db/index.js";
|
|
2
|
+
// Admin
|
|
3
|
+
export * from "./admin/index.js";
|
|
4
|
+
// Auth
|
|
5
|
+
export * from "./auth/index.js";
|
|
6
|
+
// Billing (selective — ITenantCustomerRepository/TenantCustomerRow also in credits)
|
|
7
|
+
export { PaymentMethodOwnershipError, noOpReplayGuard, DrizzleWebhookSeenRepository, } from "./billing/index.js";
|
|
8
|
+
// Config
|
|
9
|
+
export { config, billingConfigSchema } from "./config/index.js";
|
|
10
|
+
// Credits
|
|
11
|
+
export * from "./credits/index.js";
|
|
12
|
+
// Email
|
|
13
|
+
export * from "./email/index.js";
|
|
14
|
+
// Metering
|
|
15
|
+
export * from "./metering/index.js";
|
|
16
|
+
// Middleware
|
|
17
|
+
export * from "./middleware/index.js";
|
|
18
|
+
// Security
|
|
19
|
+
export * from "./security/index.js";
|
|
20
|
+
// Tenancy
|
|
21
|
+
export * from "./tenancy/index.js";
|
|
22
|
+
// tRPC
|
|
23
|
+
export * from "./trpc/index.js";
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { IUsageSummaryRepository } from "./drizzle-usage-summary-repository.js";
|
|
2
|
+
import type { UsageSummary } from "./types.js";
|
|
3
|
+
export interface IMeterAggregator {
|
|
4
|
+
aggregate(now?: number): Promise<number>;
|
|
5
|
+
start(intervalMs?: number): void;
|
|
6
|
+
stop(): void;
|
|
7
|
+
querySummaries(tenant: string, opts?: {
|
|
8
|
+
since?: number;
|
|
9
|
+
until?: number;
|
|
10
|
+
limit?: number;
|
|
11
|
+
}): Promise<UsageSummary[]>;
|
|
12
|
+
getTenantTotal(tenant: string, since: number): Promise<{
|
|
13
|
+
totalCost: number;
|
|
14
|
+
totalCharge: number;
|
|
15
|
+
eventCount: number;
|
|
16
|
+
}>;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Background aggregator that rolls up raw meter events into per-tenant
|
|
20
|
+
* usage summaries over fixed time windows.
|
|
21
|
+
*
|
|
22
|
+
* Designed to run periodically (e.g., every minute). Each run aggregates
|
|
23
|
+
* events from a completed window that haven't been summarized yet.
|
|
24
|
+
*/
|
|
25
|
+
export declare class DrizzleMeterAggregator implements IMeterAggregator {
|
|
26
|
+
private readonly repo;
|
|
27
|
+
private readonly windowMs;
|
|
28
|
+
private timer;
|
|
29
|
+
constructor(repo: IUsageSummaryRepository, opts?: {
|
|
30
|
+
windowMs?: number;
|
|
31
|
+
});
|
|
32
|
+
/**
|
|
33
|
+
* Aggregate all events in completed windows up to `now`.
|
|
34
|
+
* Returns the number of summary rows inserted.
|
|
35
|
+
*/
|
|
36
|
+
aggregate(now?: number): Promise<number>;
|
|
37
|
+
/** Start periodic aggregation. */
|
|
38
|
+
start(intervalMs?: number): void;
|
|
39
|
+
/** Stop periodic aggregation. */
|
|
40
|
+
stop(): void;
|
|
41
|
+
/** Query usage summaries for a tenant within a time range. */
|
|
42
|
+
querySummaries(tenant: string, opts?: {
|
|
43
|
+
since?: number;
|
|
44
|
+
until?: number;
|
|
45
|
+
limit?: number;
|
|
46
|
+
}): Promise<UsageSummary[]>;
|
|
47
|
+
/** Get a tenant's total usage across all capabilities since a given time. */
|
|
48
|
+
getTenantTotal(tenant: string, since: number): Promise<{
|
|
49
|
+
totalCost: number;
|
|
50
|
+
totalCharge: number;
|
|
51
|
+
eventCount: number;
|
|
52
|
+
}>;
|
|
53
|
+
}
|
|
54
|
+
export { DrizzleMeterAggregator as MeterAggregator };
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
/**
|
|
3
|
+
* Background aggregator that rolls up raw meter events into per-tenant
|
|
4
|
+
* usage summaries over fixed time windows.
|
|
5
|
+
*
|
|
6
|
+
* Designed to run periodically (e.g., every minute). Each run aggregates
|
|
7
|
+
* events from a completed window that haven't been summarized yet.
|
|
8
|
+
*/
|
|
9
|
+
export class DrizzleMeterAggregator {
|
|
10
|
+
repo;
|
|
11
|
+
windowMs;
|
|
12
|
+
timer = null;
|
|
13
|
+
constructor(repo, opts = {}) {
|
|
14
|
+
this.repo = repo;
|
|
15
|
+
this.windowMs = opts.windowMs ?? 60_000; // 1 minute default
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Aggregate all events in completed windows up to `now`.
|
|
19
|
+
* Returns the number of summary rows inserted.
|
|
20
|
+
*/
|
|
21
|
+
async aggregate(now = Date.now()) {
|
|
22
|
+
let lastEnd = await this.repo.getLastWindowEnd();
|
|
23
|
+
// Calculate the current window boundary. We only aggregate *completed* windows.
|
|
24
|
+
const currentWindowStart = Math.floor(now / this.windowMs) * this.windowMs;
|
|
25
|
+
if (lastEnd >= currentWindowStart) {
|
|
26
|
+
// Nothing new to aggregate.
|
|
27
|
+
return 0;
|
|
28
|
+
}
|
|
29
|
+
let totalInserted = 0;
|
|
30
|
+
// Process one window at a time to respect fixed time window boundaries.
|
|
31
|
+
// On first run (lastEnd === 0), align to the first window boundary at or
|
|
32
|
+
// before currentWindowStart so we don't create thousands of empty sentinels.
|
|
33
|
+
if (lastEnd === 0) {
|
|
34
|
+
// Find earliest event to anchor the first window.
|
|
35
|
+
const earliest = await this.repo.getEarliestEventTimestamp(currentWindowStart);
|
|
36
|
+
if (earliest != null) {
|
|
37
|
+
lastEnd = Math.floor(earliest / this.windowMs) * this.windowMs;
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
// No events at all -- insert a single sentinel to mark we've processed up to now.
|
|
41
|
+
await this.repo.insertSummary({
|
|
42
|
+
id: crypto.randomUUID(),
|
|
43
|
+
tenant: "__sentinel__",
|
|
44
|
+
capability: "__none__",
|
|
45
|
+
provider: "__none__",
|
|
46
|
+
eventCount: 0,
|
|
47
|
+
totalCost: 0,
|
|
48
|
+
totalCharge: 0,
|
|
49
|
+
totalDuration: 0,
|
|
50
|
+
windowStart: 0,
|
|
51
|
+
windowEnd: currentWindowStart,
|
|
52
|
+
});
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
while (lastEnd < currentWindowStart) {
|
|
57
|
+
const windowStart = lastEnd;
|
|
58
|
+
const windowEnd = Math.min(lastEnd + this.windowMs, currentWindowStart);
|
|
59
|
+
// Group events by tenant + capability + provider within this single window.
|
|
60
|
+
const rows = await this.repo.getAggregatedEvents(windowStart, windowEnd);
|
|
61
|
+
if (rows.length === 0) {
|
|
62
|
+
// Advance past this empty window by inserting a sentinel with zero counts.
|
|
63
|
+
await this.repo.insertSummary({
|
|
64
|
+
id: crypto.randomUUID(),
|
|
65
|
+
tenant: "__sentinel__",
|
|
66
|
+
capability: "__none__",
|
|
67
|
+
provider: "__none__",
|
|
68
|
+
eventCount: 0,
|
|
69
|
+
totalCost: 0,
|
|
70
|
+
totalCharge: 0,
|
|
71
|
+
totalDuration: 0,
|
|
72
|
+
windowStart,
|
|
73
|
+
windowEnd,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
await this.repo.insertSummariesBatch(rows.map((s) => ({
|
|
78
|
+
id: crypto.randomUUID(),
|
|
79
|
+
tenant: s.tenant,
|
|
80
|
+
capability: s.capability,
|
|
81
|
+
provider: s.provider,
|
|
82
|
+
eventCount: s.eventCount,
|
|
83
|
+
totalCost: s.totalCost,
|
|
84
|
+
totalCharge: s.totalCharge,
|
|
85
|
+
totalDuration: s.totalDuration,
|
|
86
|
+
windowStart,
|
|
87
|
+
windowEnd,
|
|
88
|
+
})));
|
|
89
|
+
totalInserted += rows.length;
|
|
90
|
+
}
|
|
91
|
+
lastEnd = windowEnd;
|
|
92
|
+
}
|
|
93
|
+
return totalInserted;
|
|
94
|
+
}
|
|
95
|
+
/** Start periodic aggregation. */
|
|
96
|
+
start(intervalMs) {
|
|
97
|
+
if (this.timer) {
|
|
98
|
+
return; // Already running -- avoid leaking a second interval timer.
|
|
99
|
+
}
|
|
100
|
+
const interval = intervalMs ?? this.windowMs;
|
|
101
|
+
this.timer = setInterval(() => this.aggregate(), interval);
|
|
102
|
+
if (this.timer.unref) {
|
|
103
|
+
this.timer.unref();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/** Stop periodic aggregation. */
|
|
107
|
+
stop() {
|
|
108
|
+
if (this.timer) {
|
|
109
|
+
clearInterval(this.timer);
|
|
110
|
+
this.timer = null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/** Query usage summaries for a tenant within a time range. */
|
|
114
|
+
async querySummaries(tenant, opts = {}) {
|
|
115
|
+
return this.repo.querySummaries(tenant, opts);
|
|
116
|
+
}
|
|
117
|
+
/** Get a tenant's total usage across all capabilities since a given time. */
|
|
118
|
+
async getTenantTotal(tenant, since) {
|
|
119
|
+
return this.repo.getTenantTotal(tenant, since);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// Backward-compat alias.
|
|
123
|
+
export { DrizzleMeterAggregator as MeterAggregator };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
3
|
+
import { meterEvents, usageSummaries } from "../db/schema/meter-events.js";
|
|
4
|
+
import { createTestDb } from "../test/db.js";
|
|
5
|
+
import { Credit } from "../credits/credit.js";
|
|
6
|
+
import { DrizzleMeterAggregator } from "./aggregator.js";
|
|
7
|
+
import { DrizzleUsageSummaryRepository } from "./drizzle-usage-summary-repository.js";
|
|
8
|
+
const WINDOW_MS = 60_000; // 1-minute windows
|
|
9
|
+
describe("DrizzleMeterAggregator edge cases", () => {
|
|
10
|
+
let db;
|
|
11
|
+
let pool;
|
|
12
|
+
let aggregator;
|
|
13
|
+
beforeEach(async () => {
|
|
14
|
+
const testDb = await createTestDb();
|
|
15
|
+
db = testDb.db;
|
|
16
|
+
pool = testDb.pool;
|
|
17
|
+
aggregator = new DrizzleMeterAggregator(new DrizzleUsageSummaryRepository(db), { windowMs: WINDOW_MS });
|
|
18
|
+
});
|
|
19
|
+
afterEach(async () => {
|
|
20
|
+
aggregator.stop();
|
|
21
|
+
await pool.close();
|
|
22
|
+
});
|
|
23
|
+
async function insertEvent(overrides) {
|
|
24
|
+
await db.insert(meterEvents).values({
|
|
25
|
+
id: overrides.id ?? crypto.randomUUID(),
|
|
26
|
+
tenant: overrides.tenant ?? "tenant-1",
|
|
27
|
+
cost: overrides.cost ?? Credit.fromDollars(0.001).toRaw(),
|
|
28
|
+
charge: overrides.charge ?? Credit.fromDollars(0.002).toRaw(),
|
|
29
|
+
capability: overrides.capability ?? "embeddings",
|
|
30
|
+
provider: overrides.provider ?? "openai",
|
|
31
|
+
timestamp: overrides.timestamp,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
it("sums events correctly within a single completed window", async () => {
|
|
35
|
+
const baseTime = 0;
|
|
36
|
+
await insertEvent({ timestamp: baseTime + 10_000, cost: 1_000_000, charge: 2_000_000 });
|
|
37
|
+
await insertEvent({ timestamp: baseTime + 20_000, cost: 3_000_000, charge: 4_000_000 });
|
|
38
|
+
await insertEvent({ timestamp: baseTime + 30_000, cost: 5_000_000, charge: 6_000_000 });
|
|
39
|
+
const inserted = await aggregator.aggregate(WINDOW_MS + 1);
|
|
40
|
+
expect(inserted).toBe(1);
|
|
41
|
+
const summaries = await db.select().from(usageSummaries);
|
|
42
|
+
const real = summaries.filter((s) => s.tenant !== "__sentinel__");
|
|
43
|
+
expect(real).toHaveLength(1);
|
|
44
|
+
expect(real[0].tenant).toBe("tenant-1");
|
|
45
|
+
expect(real[0].eventCount).toBe(3);
|
|
46
|
+
expect(real[0].totalCost).toBe(9_000_000);
|
|
47
|
+
expect(real[0].totalCharge).toBe(12_000_000);
|
|
48
|
+
expect(real[0].windowStart).toBe(0);
|
|
49
|
+
expect(real[0].windowEnd).toBe(WINDOW_MS);
|
|
50
|
+
});
|
|
51
|
+
it("returns zero and inserts sentinel when no events exist", async () => {
|
|
52
|
+
const inserted = await aggregator.aggregate(WINDOW_MS + 1);
|
|
53
|
+
expect(inserted).toBe(0);
|
|
54
|
+
const summaries = await db.select().from(usageSummaries);
|
|
55
|
+
expect(summaries).toHaveLength(1);
|
|
56
|
+
expect(summaries[0].tenant).toBe("__sentinel__");
|
|
57
|
+
expect(summaries[0].eventCount).toBe(0);
|
|
58
|
+
});
|
|
59
|
+
it("rejects duplicate event IDs via primary key constraint", async () => {
|
|
60
|
+
const eventId = crypto.randomUUID();
|
|
61
|
+
await insertEvent({ timestamp: 10_000, id: eventId });
|
|
62
|
+
await expect(insertEvent({ timestamp: 10_000, id: eventId })).rejects.toThrow();
|
|
63
|
+
const inserted = await aggregator.aggregate(WINDOW_MS + 1);
|
|
64
|
+
expect(inserted).toBe(1);
|
|
65
|
+
const summaries = await db.select().from(usageSummaries);
|
|
66
|
+
const real = summaries.filter((s) => s.tenant !== "__sentinel__");
|
|
67
|
+
expect(real).toHaveLength(1);
|
|
68
|
+
expect(real[0].eventCount).toBe(1);
|
|
69
|
+
});
|
|
70
|
+
it("handles very large cost totals without overflow", async () => {
|
|
71
|
+
const largeCost = 1_000_000_000_000;
|
|
72
|
+
const largeCharge = 2_000_000_000_000;
|
|
73
|
+
for (let i = 0; i < 5; i++) {
|
|
74
|
+
await insertEvent({ timestamp: 10_000 + i * 1000, cost: largeCost, charge: largeCharge });
|
|
75
|
+
}
|
|
76
|
+
const inserted = await aggregator.aggregate(WINDOW_MS + 1);
|
|
77
|
+
expect(inserted).toBe(1);
|
|
78
|
+
const summaries = await db.select().from(usageSummaries);
|
|
79
|
+
const real = summaries.filter((s) => s.tenant !== "__sentinel__");
|
|
80
|
+
expect(real).toHaveLength(1);
|
|
81
|
+
expect(real[0].totalCost).toBe(5_000_000_000_000);
|
|
82
|
+
expect(real[0].totalCharge).toBe(10_000_000_000_000);
|
|
83
|
+
expect(real[0].eventCount).toBe(5);
|
|
84
|
+
});
|
|
85
|
+
it("isolates aggregation per tenant", async () => {
|
|
86
|
+
await insertEvent({ tenant: "tenant-A", timestamp: 10_000, cost: 1_000_000, charge: 2_000_000 });
|
|
87
|
+
await insertEvent({ tenant: "tenant-A", timestamp: 20_000, cost: 3_000_000, charge: 4_000_000 });
|
|
88
|
+
await insertEvent({ tenant: "tenant-B", timestamp: 15_000, cost: 5_000_000, charge: 6_000_000 });
|
|
89
|
+
const inserted = await aggregator.aggregate(WINDOW_MS + 1);
|
|
90
|
+
expect(inserted).toBe(2);
|
|
91
|
+
const summaries = await db.select().from(usageSummaries);
|
|
92
|
+
const real = summaries.filter((s) => s.tenant !== "__sentinel__");
|
|
93
|
+
expect(real).toHaveLength(2);
|
|
94
|
+
const tenantA = real.find((s) => s.tenant === "tenant-A");
|
|
95
|
+
expect(tenantA).toEqual(expect.objectContaining({
|
|
96
|
+
tenant: "tenant-A",
|
|
97
|
+
eventCount: 2,
|
|
98
|
+
totalCost: 4_000_000,
|
|
99
|
+
totalCharge: 6_000_000,
|
|
100
|
+
}));
|
|
101
|
+
const tenantB = real.find((s) => s.tenant === "tenant-B");
|
|
102
|
+
expect(tenantB).toEqual(expect.objectContaining({
|
|
103
|
+
tenant: "tenant-B",
|
|
104
|
+
eventCount: 1,
|
|
105
|
+
totalCost: 5_000_000,
|
|
106
|
+
totalCharge: 6_000_000,
|
|
107
|
+
}));
|
|
108
|
+
});
|
|
109
|
+
it("includes events at window start, excludes events at window end", async () => {
|
|
110
|
+
await insertEvent({ timestamp: 0, cost: 1_000_000, charge: 1_000_000 });
|
|
111
|
+
await insertEvent({ timestamp: 59_999, cost: 2_000_000, charge: 2_000_000 });
|
|
112
|
+
await insertEvent({ timestamp: 60_000, cost: 100_000_000, charge: 100_000_000 });
|
|
113
|
+
const inserted = await aggregator.aggregate(2 * WINDOW_MS + 1);
|
|
114
|
+
const summaries = await db.select().from(usageSummaries);
|
|
115
|
+
const real = summaries.filter((s) => s.tenant !== "__sentinel__");
|
|
116
|
+
const window0 = real.find((s) => s.windowStart === 0);
|
|
117
|
+
expect(window0).toEqual(expect.objectContaining({
|
|
118
|
+
windowStart: 0,
|
|
119
|
+
eventCount: 2,
|
|
120
|
+
totalCost: 3_000_000,
|
|
121
|
+
}));
|
|
122
|
+
const window1 = real.find((s) => s.windowStart === 60_000);
|
|
123
|
+
expect(window1).toEqual(expect.objectContaining({
|
|
124
|
+
windowStart: 60_000,
|
|
125
|
+
eventCount: 1,
|
|
126
|
+
totalCost: 100_000_000,
|
|
127
|
+
}));
|
|
128
|
+
// window0: 1 tenant/capability/provider group = 1 row
|
|
129
|
+
// window1: 1 tenant/capability/provider group = 1 row
|
|
130
|
+
expect(inserted).toBe(2);
|
|
131
|
+
});
|
|
132
|
+
it("aggregates zero-cost events correctly", async () => {
|
|
133
|
+
await insertEvent({ timestamp: 10_000, cost: 0, charge: 0 });
|
|
134
|
+
await insertEvent({ timestamp: 20_000, cost: 0, charge: 0 });
|
|
135
|
+
const inserted = await aggregator.aggregate(WINDOW_MS + 1);
|
|
136
|
+
expect(inserted).toBe(1);
|
|
137
|
+
const summaries = await db.select().from(usageSummaries);
|
|
138
|
+
const real = summaries.filter((s) => s.tenant !== "__sentinel__");
|
|
139
|
+
expect(real).toHaveLength(1);
|
|
140
|
+
expect(real[0].totalCost).toBe(0);
|
|
141
|
+
expect(real[0].totalCharge).toBe(0);
|
|
142
|
+
expect(real[0].eventCount).toBe(2);
|
|
143
|
+
});
|
|
144
|
+
it("handles a single event exactly at window boundary", async () => {
|
|
145
|
+
// Event at timestamp 0 (window start) — should be in window [0, WINDOW_MS)
|
|
146
|
+
await insertEvent({ timestamp: 0, cost: 1_000_000, charge: 2_000_000 });
|
|
147
|
+
const inserted = await aggregator.aggregate(WINDOW_MS + 1);
|
|
148
|
+
expect(inserted).toBe(1);
|
|
149
|
+
const summaries = await db.select().from(usageSummaries);
|
|
150
|
+
const real = summaries.filter((s) => s.tenant !== "__sentinel__");
|
|
151
|
+
expect(real).toHaveLength(1);
|
|
152
|
+
expect(real[0].eventCount).toBe(1);
|
|
153
|
+
expect(real[0].totalCost).toBe(1_000_000);
|
|
154
|
+
});
|
|
155
|
+
it("handles event one unit before window end", async () => {
|
|
156
|
+
// Event at WINDOW_MS - 1 should be included in first window
|
|
157
|
+
await insertEvent({ timestamp: WINDOW_MS - 1, cost: 500_000, charge: 600_000 });
|
|
158
|
+
const inserted = await aggregator.aggregate(WINDOW_MS + 1);
|
|
159
|
+
expect(inserted).toBe(1);
|
|
160
|
+
const summaries = await db.select().from(usageSummaries);
|
|
161
|
+
const real = summaries.filter((s) => s.tenant !== "__sentinel__");
|
|
162
|
+
expect(real).toHaveLength(1);
|
|
163
|
+
expect(real[0].windowStart).toBe(0);
|
|
164
|
+
expect(real[0].eventCount).toBe(1);
|
|
165
|
+
expect(real[0].totalCost).toBe(500_000);
|
|
166
|
+
});
|
|
167
|
+
it("handles event exactly at window end (goes to next window)", async () => {
|
|
168
|
+
// Event at exactly WINDOW_MS should be in window [WINDOW_MS, 2*WINDOW_MS)
|
|
169
|
+
await insertEvent({ timestamp: WINDOW_MS, cost: 700_000, charge: 800_000 });
|
|
170
|
+
await aggregator.aggregate(2 * WINDOW_MS + 1);
|
|
171
|
+
const summaries = await db.select().from(usageSummaries);
|
|
172
|
+
const real = summaries.filter((s) => s.tenant !== "__sentinel__");
|
|
173
|
+
// Should be in second window, not first
|
|
174
|
+
expect(real).toHaveLength(1);
|
|
175
|
+
expect(real[0].windowStart).toBe(WINDOW_MS);
|
|
176
|
+
expect(real[0].eventCount).toBe(1);
|
|
177
|
+
expect(real[0].totalCost).toBe(700_000);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { MeterEvent } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Dead-Letter Queue for meter events that failed to flush after max retries.
|
|
4
|
+
*
|
|
5
|
+
* Events written here are permanently failed and require manual intervention
|
|
6
|
+
* to recover. This ensures we never silently drop billing events.
|
|
7
|
+
*/
|
|
8
|
+
export declare class MeterDLQ {
|
|
9
|
+
private readonly dlqPath;
|
|
10
|
+
constructor(dlqPath: string);
|
|
11
|
+
private ensureDir;
|
|
12
|
+
/**
|
|
13
|
+
* Append a failed event to the DLQ with failure metadata.
|
|
14
|
+
*/
|
|
15
|
+
append(event: MeterEvent & {
|
|
16
|
+
id: string;
|
|
17
|
+
}, error: string, retries: number): void;
|
|
18
|
+
/**
|
|
19
|
+
* Read all events from the DLQ for manual recovery.
|
|
20
|
+
*/
|
|
21
|
+
readAll(): Array<MeterEvent & {
|
|
22
|
+
id: string;
|
|
23
|
+
dlq_timestamp: number;
|
|
24
|
+
dlq_error: string;
|
|
25
|
+
dlq_retries: number;
|
|
26
|
+
}>;
|
|
27
|
+
/**
|
|
28
|
+
* Get the number of events in the DLQ.
|
|
29
|
+
*/
|
|
30
|
+
count(): number;
|
|
31
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* Dead-Letter Queue for meter events that failed to flush after max retries.
|
|
5
|
+
*
|
|
6
|
+
* Events written here are permanently failed and require manual intervention
|
|
7
|
+
* to recover. This ensures we never silently drop billing events.
|
|
8
|
+
*/
|
|
9
|
+
export class MeterDLQ {
|
|
10
|
+
dlqPath;
|
|
11
|
+
constructor(dlqPath) {
|
|
12
|
+
this.dlqPath = dlqPath;
|
|
13
|
+
this.ensureDir();
|
|
14
|
+
}
|
|
15
|
+
ensureDir() {
|
|
16
|
+
const dir = dirname(this.dlqPath);
|
|
17
|
+
if (!existsSync(dir)) {
|
|
18
|
+
mkdirSync(dir, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Append a failed event to the DLQ with failure metadata.
|
|
23
|
+
*/
|
|
24
|
+
append(event, error, retries) {
|
|
25
|
+
const dlqEntry = {
|
|
26
|
+
...event,
|
|
27
|
+
dlq_timestamp: Date.now(),
|
|
28
|
+
dlq_error: error,
|
|
29
|
+
dlq_retries: retries,
|
|
30
|
+
};
|
|
31
|
+
const line = `${JSON.stringify(dlqEntry)}\n`;
|
|
32
|
+
appendFileSync(this.dlqPath, line, { encoding: "utf8", flag: "a" });
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Read all events from the DLQ for manual recovery.
|
|
36
|
+
*/
|
|
37
|
+
readAll() {
|
|
38
|
+
if (!existsSync(this.dlqPath)) {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
const content = readFileSync(this.dlqPath, "utf8");
|
|
42
|
+
if (!content.trim()) {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
const entries = [];
|
|
46
|
+
for (const line of content.trim().split("\n")) {
|
|
47
|
+
try {
|
|
48
|
+
entries.push(JSON.parse(line));
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// Skip malformed lines (e.g., from incomplete writes during error conditions).
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return entries;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Get the number of events in the DLQ.
|
|
58
|
+
*/
|
|
59
|
+
count() {
|
|
60
|
+
let content;
|
|
61
|
+
try {
|
|
62
|
+
content = readFileSync(this.dlqPath, "utf8");
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
if (err.code === "ENOENT") {
|
|
66
|
+
return 0;
|
|
67
|
+
}
|
|
68
|
+
throw err;
|
|
69
|
+
}
|
|
70
|
+
if (!content) {
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
73
|
+
let n = 0;
|
|
74
|
+
for (let i = 0; i < content.length; i++) {
|
|
75
|
+
if (content.charCodeAt(i) === 10)
|
|
76
|
+
n++;
|
|
77
|
+
}
|
|
78
|
+
if (content.length > 0 && content.charCodeAt(content.length - 1) !== 10)
|
|
79
|
+
n++;
|
|
80
|
+
return n;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|