@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,74 @@
|
|
|
1
|
+
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { createTestDb, truncateAllTables } from "../test/db.js";
|
|
3
|
+
import { DrizzleRateLimitRepository } from "./drizzle-rate-limit-repository.js";
|
|
4
|
+
describe("DrizzleRateLimitRepository", () => {
|
|
5
|
+
let repo;
|
|
6
|
+
let db;
|
|
7
|
+
let pool;
|
|
8
|
+
beforeAll(async () => {
|
|
9
|
+
({ db, pool } = await createTestDb());
|
|
10
|
+
});
|
|
11
|
+
afterAll(async () => {
|
|
12
|
+
await pool.close();
|
|
13
|
+
});
|
|
14
|
+
beforeEach(async () => {
|
|
15
|
+
vi.useFakeTimers();
|
|
16
|
+
vi.setSystemTime(new Date("2026-02-21T12:00:00Z"));
|
|
17
|
+
await truncateAllTables(pool);
|
|
18
|
+
repo = new DrizzleRateLimitRepository(db);
|
|
19
|
+
});
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
vi.useRealTimers();
|
|
22
|
+
});
|
|
23
|
+
it("starts count at 1 for the first request", async () => {
|
|
24
|
+
const entry = await repo.increment("1.2.3.4", "api:default", 60_000);
|
|
25
|
+
expect(entry.count).toBe(1);
|
|
26
|
+
expect(entry.key).toBe("1.2.3.4");
|
|
27
|
+
expect(entry.scope).toBe("api:default");
|
|
28
|
+
});
|
|
29
|
+
it("increments count on subsequent requests within the window", async () => {
|
|
30
|
+
await repo.increment("1.2.3.4", "api:default", 60_000);
|
|
31
|
+
await repo.increment("1.2.3.4", "api:default", 60_000);
|
|
32
|
+
const entry = await repo.increment("1.2.3.4", "api:default", 60_000);
|
|
33
|
+
expect(entry.count).toBe(3);
|
|
34
|
+
});
|
|
35
|
+
it("resets count when window expires", async () => {
|
|
36
|
+
await repo.increment("1.2.3.4", "api:default", 60_000);
|
|
37
|
+
await repo.increment("1.2.3.4", "api:default", 60_000);
|
|
38
|
+
// Advance past the window
|
|
39
|
+
vi.advanceTimersByTime(61_000);
|
|
40
|
+
const entry = await repo.increment("1.2.3.4", "api:default", 60_000);
|
|
41
|
+
expect(entry.count).toBe(1);
|
|
42
|
+
});
|
|
43
|
+
it("tracks different keys independently", async () => {
|
|
44
|
+
await repo.increment("10.0.0.1", "api:default", 60_000);
|
|
45
|
+
await repo.increment("10.0.0.1", "api:default", 60_000);
|
|
46
|
+
const entry = await repo.increment("10.0.0.2", "api:default", 60_000);
|
|
47
|
+
expect(entry.count).toBe(1);
|
|
48
|
+
});
|
|
49
|
+
it("tracks different scopes independently", async () => {
|
|
50
|
+
await repo.increment("1.2.3.4", "api:default", 60_000);
|
|
51
|
+
await repo.increment("1.2.3.4", "api:default", 60_000);
|
|
52
|
+
const entry = await repo.increment("1.2.3.4", "api:webhook", 60_000);
|
|
53
|
+
expect(entry.count).toBe(1);
|
|
54
|
+
});
|
|
55
|
+
it("get returns null for unknown entry", async () => {
|
|
56
|
+
expect(await repo.get("1.2.3.4", "api:default")).toBeNull();
|
|
57
|
+
});
|
|
58
|
+
it("get returns current entry", async () => {
|
|
59
|
+
await repo.increment("1.2.3.4", "api:default", 60_000);
|
|
60
|
+
const entry = await repo.get("1.2.3.4", "api:default");
|
|
61
|
+
expect(entry).not.toBeNull();
|
|
62
|
+
expect(entry?.count).toBe(1);
|
|
63
|
+
});
|
|
64
|
+
it("purgeStale removes entries older than windowMs", async () => {
|
|
65
|
+
await repo.increment("1.2.3.4", "api:default", 60_000);
|
|
66
|
+
// Advance 2 minutes
|
|
67
|
+
vi.advanceTimersByTime(2 * 60_000);
|
|
68
|
+
await repo.increment("10.0.0.2", "api:default", 60_000);
|
|
69
|
+
const removed = await repo.purgeStale(60_000);
|
|
70
|
+
expect(removed).toBe(1);
|
|
71
|
+
expect(await repo.get("1.2.3.4", "api:default")).toBeNull();
|
|
72
|
+
expect(await repo.get("10.0.0.2", "api:default")).not.toBeNull();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Context } from "hono";
|
|
2
|
+
/**
|
|
3
|
+
* Parse the TRUSTED_PROXY_IPS env var into a Set of IP addresses.
|
|
4
|
+
* Returns an empty set if the value is undefined or empty.
|
|
5
|
+
*/
|
|
6
|
+
export declare function parseTrustedProxies(envValue: string | undefined): Set<string>;
|
|
7
|
+
/**
|
|
8
|
+
* Determine the real client IP.
|
|
9
|
+
*
|
|
10
|
+
* - If `socketAddr` matches a trusted proxy, use the **last** (rightmost)
|
|
11
|
+
* value from `X-Forwarded-For` (closest hop to the trusted proxy).
|
|
12
|
+
* - Otherwise, use `socketAddr` directly (XFF is untrusted).
|
|
13
|
+
* - Falls back to `"unknown"` if neither is available.
|
|
14
|
+
*/
|
|
15
|
+
export declare function getClientIp(xffHeader: string | undefined, socketAddr: string | undefined, trusted?: Set<string>): string;
|
|
16
|
+
/**
|
|
17
|
+
* Convenience wrapper: extract client IP from a Hono Context.
|
|
18
|
+
* Reads XFF header and socket address from the request.
|
|
19
|
+
* Optionally accepts a trusted proxy set (defaults to the module-level set
|
|
20
|
+
* parsed from TRUSTED_PROXY_IPS — useful for testing).
|
|
21
|
+
*/
|
|
22
|
+
export declare function getClientIpFromContext(c: Context, trusted?: Set<string>): string;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse the TRUSTED_PROXY_IPS env var into a Set of IP addresses.
|
|
3
|
+
* Returns an empty set if the value is undefined or empty.
|
|
4
|
+
*/
|
|
5
|
+
export function parseTrustedProxies(envValue) {
|
|
6
|
+
if (!envValue)
|
|
7
|
+
return new Set();
|
|
8
|
+
return new Set(envValue
|
|
9
|
+
.split(",")
|
|
10
|
+
.map((ip) => ip.trim())
|
|
11
|
+
.filter(Boolean));
|
|
12
|
+
}
|
|
13
|
+
/** Strip IPv6-mapped-IPv4 prefix (::ffff:) for comparison. */
|
|
14
|
+
function normalizeIp(ip) {
|
|
15
|
+
return ip.startsWith("::ffff:") ? ip.slice(7) : ip;
|
|
16
|
+
}
|
|
17
|
+
// Parsed once at module load — no per-request overhead.
|
|
18
|
+
const trustedProxies = parseTrustedProxies(process.env.TRUSTED_PROXY_IPS);
|
|
19
|
+
/**
|
|
20
|
+
* Determine the real client IP.
|
|
21
|
+
*
|
|
22
|
+
* - If `socketAddr` matches a trusted proxy, use the **last** (rightmost)
|
|
23
|
+
* value from `X-Forwarded-For` (closest hop to the trusted proxy).
|
|
24
|
+
* - Otherwise, use `socketAddr` directly (XFF is untrusted).
|
|
25
|
+
* - Falls back to `"unknown"` if neither is available.
|
|
26
|
+
*/
|
|
27
|
+
export function getClientIp(xffHeader, socketAddr, trusted = trustedProxies) {
|
|
28
|
+
const normalizedSocket = socketAddr ? normalizeIp(socketAddr) : undefined;
|
|
29
|
+
if (xffHeader && normalizedSocket && trusted.has(normalizedSocket)) {
|
|
30
|
+
// Trust XFF — take the rightmost (last) value
|
|
31
|
+
const parts = xffHeader.split(",");
|
|
32
|
+
const last = parts[parts.length - 1]?.trim();
|
|
33
|
+
if (last)
|
|
34
|
+
return last;
|
|
35
|
+
}
|
|
36
|
+
if (socketAddr)
|
|
37
|
+
return socketAddr;
|
|
38
|
+
return "unknown";
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Convenience wrapper: extract client IP from a Hono Context.
|
|
42
|
+
* Reads XFF header and socket address from the request.
|
|
43
|
+
* Optionally accepts a trusted proxy set (defaults to the module-level set
|
|
44
|
+
* parsed from TRUSTED_PROXY_IPS — useful for testing).
|
|
45
|
+
*/
|
|
46
|
+
export function getClientIpFromContext(c, trusted) {
|
|
47
|
+
const xff = c.req.header("x-forwarded-for");
|
|
48
|
+
const incoming = c.env?.incoming;
|
|
49
|
+
const socketAddr = incoming?.socket?.remoteAddress;
|
|
50
|
+
return trusted !== undefined ? getClientIp(xff, socketAddr, trusted) : getClientIp(xff, socketAddr);
|
|
51
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { getClientIp, parseTrustedProxies } from "./get-client-ip.js";
|
|
3
|
+
describe("parseTrustedProxies", () => {
|
|
4
|
+
it("returns empty set for undefined", () => {
|
|
5
|
+
expect(parseTrustedProxies(undefined).size).toBe(0);
|
|
6
|
+
});
|
|
7
|
+
it("returns empty set for empty string", () => {
|
|
8
|
+
expect(parseTrustedProxies("").size).toBe(0);
|
|
9
|
+
});
|
|
10
|
+
it("parses comma-separated IPs", () => {
|
|
11
|
+
const result = parseTrustedProxies("10.0.0.1, 10.0.0.2");
|
|
12
|
+
expect(result.has("10.0.0.1")).toBe(true);
|
|
13
|
+
expect(result.has("10.0.0.2")).toBe(true);
|
|
14
|
+
expect(result.size).toBe(2);
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
describe("getClientIp", () => {
|
|
18
|
+
it("returns socket address when no trusted proxies configured", () => {
|
|
19
|
+
expect(getClientIp("attacker-spoofed", "1.2.3.4", new Set())).toBe("1.2.3.4");
|
|
20
|
+
});
|
|
21
|
+
it("ignores XFF when socket is not in trusted proxy set", () => {
|
|
22
|
+
const trusted = new Set(["10.0.0.1"]);
|
|
23
|
+
expect(getClientIp("spoofed-ip", "9.9.9.9", trusted)).toBe("9.9.9.9");
|
|
24
|
+
});
|
|
25
|
+
it("trusts XFF rightmost value when socket is a trusted proxy", () => {
|
|
26
|
+
const trusted = new Set(["10.0.0.1"]);
|
|
27
|
+
expect(getClientIp("client-ip, 10.0.0.1", "10.0.0.1", trusted)).toBe("10.0.0.1");
|
|
28
|
+
});
|
|
29
|
+
it("uses rightmost XFF entry (closest to trusted proxy)", () => {
|
|
30
|
+
const trusted = new Set(["10.0.0.1"]);
|
|
31
|
+
expect(getClientIp("spoofed, real-client", "10.0.0.1", trusted)).toBe("real-client");
|
|
32
|
+
});
|
|
33
|
+
it("handles IPv6-mapped IPv4 socket addresses", () => {
|
|
34
|
+
const trusted = new Set(["10.0.0.1"]);
|
|
35
|
+
expect(getClientIp("client-ip", "::ffff:10.0.0.1", trusted)).toBe("client-ip");
|
|
36
|
+
});
|
|
37
|
+
it("returns 'unknown' when no socket and no XFF", () => {
|
|
38
|
+
expect(getClientIp(undefined, undefined, new Set())).toBe("unknown");
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type { IRateLimitRepository, RateLimitEntry } from "./rate-limit-repository.js";
|
|
2
|
+
export { DrizzleRateLimitRepository } from "./drizzle-rate-limit-repository.js";
|
|
3
|
+
export { rateLimit, rateLimitByRoute, getClientIp, parseTrustedProxies, type RateLimitConfig, type RateLimitRule, } from "./rate-limit.js";
|
|
4
|
+
export { getClientIpFromContext } from "./get-client-ip.js";
|
|
5
|
+
export { csrfProtection, validateCsrfOrigin, type CsrfOptions } from "./csrf.js";
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { DrizzleRateLimitRepository } from "./drizzle-rate-limit-repository.js";
|
|
2
|
+
export { rateLimit, rateLimitByRoute, getClientIp, parseTrustedProxies, } from "./rate-limit.js";
|
|
3
|
+
export { getClientIpFromContext } from "./get-client-ip.js";
|
|
4
|
+
export { csrfProtection, validateCsrfOrigin } from "./csrf.js";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface RateLimitEntry {
|
|
2
|
+
key: string;
|
|
3
|
+
scope: string;
|
|
4
|
+
count: number;
|
|
5
|
+
windowStart: number;
|
|
6
|
+
}
|
|
7
|
+
export interface IRateLimitRepository {
|
|
8
|
+
/**
|
|
9
|
+
* Increment the counter for the given key + scope.
|
|
10
|
+
* If the current window has expired (now - windowStart >= windowMs), resets
|
|
11
|
+
* the counter to 1 and starts a new window.
|
|
12
|
+
* Returns the updated entry.
|
|
13
|
+
*/
|
|
14
|
+
increment(key: string, scope: string, windowMs: number): Promise<RateLimitEntry>;
|
|
15
|
+
/** Read the current entry without modifying it. Returns null if absent. */
|
|
16
|
+
get(key: string, scope: string): Promise<RateLimitEntry | null>;
|
|
17
|
+
/** Delete entries whose window started more than windowMs ago. */
|
|
18
|
+
purgeStale(windowMs: number): Promise<number>;
|
|
19
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rate-limiting middleware for Hono.
|
|
3
|
+
*
|
|
4
|
+
* Uses a fixed-window counter keyed by client IP. Each window is `windowMs`
|
|
5
|
+
* milliseconds wide. When a client exceeds `max` requests in a window the
|
|
6
|
+
* middleware responds with 429 Too Many Requests and a `Retry-After` header
|
|
7
|
+
* indicating how many seconds remain in the current window.
|
|
8
|
+
*
|
|
9
|
+
* State is persisted via IRateLimitRepository (DB-backed in production).
|
|
10
|
+
*/
|
|
11
|
+
import type { Context, MiddlewareHandler } from "hono";
|
|
12
|
+
import type { IRateLimitRepository } from "./rate-limit-repository.js";
|
|
13
|
+
export { getClientIp, parseTrustedProxies } from "./get-client-ip.js";
|
|
14
|
+
export interface RateLimitConfig {
|
|
15
|
+
/** Maximum number of requests per window. */
|
|
16
|
+
max: number;
|
|
17
|
+
/** Window size in milliseconds (default: 60 000 = 1 minute). */
|
|
18
|
+
windowMs?: number;
|
|
19
|
+
/** Extract the rate-limit key from a request (default: client IP). */
|
|
20
|
+
keyGenerator?: (c: Context) => string;
|
|
21
|
+
/** Custom message returned in the 429 body (default provided). */
|
|
22
|
+
message?: string;
|
|
23
|
+
/** Repository for persisting rate-limit state. Required when rate limiting is active. */
|
|
24
|
+
repo?: IRateLimitRepository;
|
|
25
|
+
/** Scope identifier for this limiter (used as the DB scope key). */
|
|
26
|
+
scope?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface RateLimitRule {
|
|
29
|
+
/** HTTP method to match, or "*" for any. */
|
|
30
|
+
method: string;
|
|
31
|
+
/** Path prefix to match (matched with `startsWith`). */
|
|
32
|
+
pathPrefix: string;
|
|
33
|
+
/** Rate-limit configuration for matching requests. */
|
|
34
|
+
config: Omit<RateLimitConfig, "repo" | "scope">;
|
|
35
|
+
/** Scope override (defaults to pathPrefix). */
|
|
36
|
+
scope?: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Create a rate-limiting middleware for a single configuration.
|
|
40
|
+
*
|
|
41
|
+
* ```ts
|
|
42
|
+
* app.use("/api/billing/*", rateLimit({ max: 10, repo, scope: "billing" }));
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export declare function rateLimit(cfg: RateLimitConfig): MiddlewareHandler;
|
|
46
|
+
/**
|
|
47
|
+
* Create a global rate-limiting middleware that applies different limits based
|
|
48
|
+
* on the request path and method.
|
|
49
|
+
*
|
|
50
|
+
* Rules are evaluated top-to-bottom; the **first** matching rule wins. If no
|
|
51
|
+
* rule matches, the `defaultConfig` is used.
|
|
52
|
+
*
|
|
53
|
+
* ```ts
|
|
54
|
+
* app.use("*", rateLimitByRoute(rules, { max: 60 }, repo));
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export declare function rateLimitByRoute(rules: RateLimitRule[], defaultConfig: Omit<RateLimitConfig, "repo" | "scope">, repo: IRateLimitRepository): MiddlewareHandler;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rate-limiting middleware for Hono.
|
|
3
|
+
*
|
|
4
|
+
* Uses a fixed-window counter keyed by client IP. Each window is `windowMs`
|
|
5
|
+
* milliseconds wide. When a client exceeds `max` requests in a window the
|
|
6
|
+
* middleware responds with 429 Too Many Requests and a `Retry-After` header
|
|
7
|
+
* indicating how many seconds remain in the current window.
|
|
8
|
+
*
|
|
9
|
+
* State is persisted via IRateLimitRepository (DB-backed in production).
|
|
10
|
+
*/
|
|
11
|
+
import { getClientIpFromContext } from "./get-client-ip.js";
|
|
12
|
+
export { getClientIp, parseTrustedProxies } from "./get-client-ip.js";
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Helpers
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
function defaultKeyGenerator(c) {
|
|
17
|
+
return getClientIpFromContext(c);
|
|
18
|
+
}
|
|
19
|
+
const DEFAULT_WINDOW_MS = 60_000; // 1 minute
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Single-route rate limiter
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
/**
|
|
24
|
+
* Create a rate-limiting middleware for a single configuration.
|
|
25
|
+
*
|
|
26
|
+
* ```ts
|
|
27
|
+
* app.use("/api/billing/*", rateLimit({ max: 10, repo, scope: "billing" }));
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export function rateLimit(cfg) {
|
|
31
|
+
const windowMs = cfg.windowMs ?? DEFAULT_WINDOW_MS;
|
|
32
|
+
const keyGen = cfg.keyGenerator ?? defaultKeyGenerator;
|
|
33
|
+
const scope = cfg.scope ?? "default";
|
|
34
|
+
return async (c, next) => {
|
|
35
|
+
// No repo — rate limiting disabled (e.g., test environments)
|
|
36
|
+
if (!cfg.repo)
|
|
37
|
+
return next();
|
|
38
|
+
const now = Date.now();
|
|
39
|
+
const key = keyGen(c);
|
|
40
|
+
const entry = await cfg.repo.increment(key, scope, windowMs);
|
|
41
|
+
const windowStart = entry.windowStart;
|
|
42
|
+
const count = entry.count;
|
|
43
|
+
// Check limit BEFORE this request counted — increment already happened
|
|
44
|
+
const retryAfterSec = Math.ceil((windowStart + windowMs - now) / 1000);
|
|
45
|
+
if (count > cfg.max) {
|
|
46
|
+
c.header("X-RateLimit-Limit", String(cfg.max));
|
|
47
|
+
c.header("X-RateLimit-Remaining", "0");
|
|
48
|
+
c.header("X-RateLimit-Reset", String(Math.ceil((windowStart + windowMs) / 1000)));
|
|
49
|
+
c.header("Retry-After", String(retryAfterSec));
|
|
50
|
+
return c.json({ error: cfg.message ?? "Too many requests, please try again later" }, 429);
|
|
51
|
+
}
|
|
52
|
+
const remaining = Math.max(0, cfg.max - count);
|
|
53
|
+
c.header("X-RateLimit-Limit", String(cfg.max));
|
|
54
|
+
c.header("X-RateLimit-Remaining", String(remaining));
|
|
55
|
+
c.header("X-RateLimit-Reset", String(Math.ceil((windowStart + windowMs) / 1000)));
|
|
56
|
+
return next();
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// Multi-route rate limiter (global middleware with per-route overrides)
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
/**
|
|
63
|
+
* Create a global rate-limiting middleware that applies different limits based
|
|
64
|
+
* on the request path and method.
|
|
65
|
+
*
|
|
66
|
+
* Rules are evaluated top-to-bottom; the **first** matching rule wins. If no
|
|
67
|
+
* rule matches, the `defaultConfig` is used.
|
|
68
|
+
*
|
|
69
|
+
* ```ts
|
|
70
|
+
* app.use("*", rateLimitByRoute(rules, { max: 60 }, repo));
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
export function rateLimitByRoute(rules, defaultConfig, repo) {
|
|
74
|
+
return async (c, next) => {
|
|
75
|
+
const method = c.req.method.toUpperCase();
|
|
76
|
+
const path = c.req.path;
|
|
77
|
+
// Find matching rule
|
|
78
|
+
let cfg = defaultConfig;
|
|
79
|
+
let scope = "default";
|
|
80
|
+
for (const rule of rules) {
|
|
81
|
+
const methodMatch = rule.method === "*" || rule.method.toUpperCase() === method;
|
|
82
|
+
if (methodMatch && path.startsWith(rule.pathPrefix)) {
|
|
83
|
+
cfg = rule.config;
|
|
84
|
+
scope = rule.scope ?? rule.pathPrefix;
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const windowMs = cfg.windowMs ?? DEFAULT_WINDOW_MS;
|
|
89
|
+
const keyGen = cfg.keyGenerator ?? defaultKeyGenerator;
|
|
90
|
+
const now = Date.now();
|
|
91
|
+
const key = keyGen(c);
|
|
92
|
+
const entry = await repo.increment(key, scope, windowMs);
|
|
93
|
+
const windowStart = entry.windowStart;
|
|
94
|
+
const count = entry.count;
|
|
95
|
+
const retryAfterSec = Math.ceil((windowStart + windowMs - now) / 1000);
|
|
96
|
+
if (count > cfg.max) {
|
|
97
|
+
c.header("X-RateLimit-Limit", String(cfg.max));
|
|
98
|
+
c.header("X-RateLimit-Remaining", "0");
|
|
99
|
+
c.header("X-RateLimit-Reset", String(Math.ceil((windowStart + windowMs) / 1000)));
|
|
100
|
+
c.header("Retry-After", String(retryAfterSec));
|
|
101
|
+
return c.json({ error: cfg.message ?? "Too many requests, please try again later" }, 429);
|
|
102
|
+
}
|
|
103
|
+
const remaining = Math.max(0, cfg.max - count);
|
|
104
|
+
c.header("X-RateLimit-Limit", String(cfg.max));
|
|
105
|
+
c.header("X-RateLimit-Remaining", String(remaining));
|
|
106
|
+
c.header("X-RateLimit-Reset", String(Math.ceil((windowStart + windowMs) / 1000)));
|
|
107
|
+
return next();
|
|
108
|
+
};
|
|
109
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { createTestDb, truncateAllTables } from "../test/db.js";
|
|
4
|
+
import { DrizzleRateLimitRepository } from "./drizzle-rate-limit-repository.js";
|
|
5
|
+
import { getClientIp, parseTrustedProxies, rateLimit, rateLimitByRoute, } from "./rate-limit.js";
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Helpers
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
/** Build a Hono app with a single rate-limited GET /test route. */
|
|
10
|
+
function buildApp(cfg, repo) {
|
|
11
|
+
const app = new Hono();
|
|
12
|
+
app.use("/test", rateLimit({ ...cfg, repo, scope: "test" }));
|
|
13
|
+
app.get("/test", (c) => c.json({ ok: true }));
|
|
14
|
+
return app;
|
|
15
|
+
}
|
|
16
|
+
function req(path = "/test", ip = "127.0.0.1") {
|
|
17
|
+
return new Request(`http://localhost${path}`, {
|
|
18
|
+
headers: { "x-forwarded-for": ip },
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
function postReq(path, ip = "127.0.0.1") {
|
|
22
|
+
return new Request(`http://localhost${path}`, {
|
|
23
|
+
method: "POST",
|
|
24
|
+
headers: { "x-forwarded-for": ip },
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Shared PGlite instance (one pool for the entire file)
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
let pool;
|
|
31
|
+
let db;
|
|
32
|
+
beforeAll(async () => {
|
|
33
|
+
({ db, pool } = await createTestDb());
|
|
34
|
+
});
|
|
35
|
+
afterAll(async () => {
|
|
36
|
+
await pool.close();
|
|
37
|
+
});
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// rateLimit (single-route)
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
describe("rateLimit", () => {
|
|
42
|
+
let repo;
|
|
43
|
+
beforeEach(async () => {
|
|
44
|
+
vi.useFakeTimers();
|
|
45
|
+
await truncateAllTables(pool);
|
|
46
|
+
repo = new DrizzleRateLimitRepository(db);
|
|
47
|
+
});
|
|
48
|
+
afterEach(() => {
|
|
49
|
+
vi.useRealTimers();
|
|
50
|
+
});
|
|
51
|
+
it("allows requests within the limit", async () => {
|
|
52
|
+
const app = buildApp({ max: 3 }, repo);
|
|
53
|
+
for (let i = 0; i < 3; i++) {
|
|
54
|
+
const res = await app.request(req());
|
|
55
|
+
expect(res.status).toBe(200);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
it("returns 429 when limit is exceeded", async () => {
|
|
59
|
+
const app = buildApp({ max: 2 }, repo);
|
|
60
|
+
await app.request(req());
|
|
61
|
+
await app.request(req());
|
|
62
|
+
const res = await app.request(req());
|
|
63
|
+
expect(res.status).toBe(429);
|
|
64
|
+
const body = await res.json();
|
|
65
|
+
expect(body.error).toContain("Too many requests");
|
|
66
|
+
});
|
|
67
|
+
it("sets X-RateLimit-* headers on every response", async () => {
|
|
68
|
+
const app = buildApp({ max: 5 }, repo);
|
|
69
|
+
const res = await app.request(req());
|
|
70
|
+
expect(res.headers.get("X-RateLimit-Limit")).toBe("5");
|
|
71
|
+
expect(res.headers.get("X-RateLimit-Remaining")).toBe("4");
|
|
72
|
+
expect(res.headers.get("X-RateLimit-Reset")).toBeTruthy();
|
|
73
|
+
});
|
|
74
|
+
it("sets Retry-After header on 429", async () => {
|
|
75
|
+
const app = buildApp({ max: 1 }, repo);
|
|
76
|
+
await app.request(req());
|
|
77
|
+
const res = await app.request(req());
|
|
78
|
+
expect(res.status).toBe(429);
|
|
79
|
+
const retryAfter = res.headers.get("Retry-After");
|
|
80
|
+
expect(retryAfter).toBeTruthy();
|
|
81
|
+
expect(Number(retryAfter)).toBeGreaterThan(0);
|
|
82
|
+
});
|
|
83
|
+
it("resets the window after windowMs elapses", async () => {
|
|
84
|
+
const app = buildApp({ max: 1, windowMs: 10_000 }, repo);
|
|
85
|
+
const res1 = await app.request(req());
|
|
86
|
+
expect(res1.status).toBe(200);
|
|
87
|
+
const res2 = await app.request(req());
|
|
88
|
+
expect(res2.status).toBe(429);
|
|
89
|
+
vi.advanceTimersByTime(10_001);
|
|
90
|
+
const res3 = await app.request(req());
|
|
91
|
+
expect(res3.status).toBe(200);
|
|
92
|
+
});
|
|
93
|
+
it("tracks different IPs independently", async () => {
|
|
94
|
+
const app = new Hono();
|
|
95
|
+
app.use("/test", rateLimit({ max: 1, repo, scope: "test", keyGenerator: (c) => c.req.header("x-forwarded-for") ?? "unknown" }));
|
|
96
|
+
app.get("/test", (c) => c.json({ ok: true }));
|
|
97
|
+
const res1 = await app.request(req("/test", "10.0.0.1"));
|
|
98
|
+
expect(res1.status).toBe(200);
|
|
99
|
+
const res2 = await app.request(req("/test", "10.0.0.2"));
|
|
100
|
+
expect(res2.status).toBe(200);
|
|
101
|
+
const res3 = await app.request(req("/test", "10.0.0.1"));
|
|
102
|
+
expect(res3.status).toBe(429);
|
|
103
|
+
});
|
|
104
|
+
it("uses a custom message when provided", async () => {
|
|
105
|
+
const app = buildApp({ max: 1, message: "Slow down" }, repo);
|
|
106
|
+
await app.request(req());
|
|
107
|
+
const res = await app.request(req());
|
|
108
|
+
expect(res.status).toBe(429);
|
|
109
|
+
const body = await res.json();
|
|
110
|
+
expect(body.error).toBe("Slow down");
|
|
111
|
+
});
|
|
112
|
+
it("supports custom key generator", async () => {
|
|
113
|
+
const app = new Hono();
|
|
114
|
+
app.use("/test", rateLimit({ max: 1, repo, scope: "api-key", keyGenerator: (c) => c.req.header("x-api-key") ?? "anon" }));
|
|
115
|
+
app.get("/test", (c) => c.json({ ok: true }));
|
|
116
|
+
const r1 = new Request("http://localhost/test", { headers: { "x-api-key": "key-a" } });
|
|
117
|
+
const r2 = new Request("http://localhost/test", { headers: { "x-api-key": "key-b" } });
|
|
118
|
+
const r3 = new Request("http://localhost/test", { headers: { "x-api-key": "key-a" } });
|
|
119
|
+
expect((await app.request(r1)).status).toBe(200);
|
|
120
|
+
expect((await app.request(r2)).status).toBe(200);
|
|
121
|
+
expect((await app.request(r3)).status).toBe(429);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
// rateLimitByRoute (multi-route)
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
describe("rateLimitByRoute", () => {
|
|
128
|
+
let repo;
|
|
129
|
+
beforeEach(async () => {
|
|
130
|
+
vi.useFakeTimers();
|
|
131
|
+
await truncateAllTables(pool);
|
|
132
|
+
repo = new DrizzleRateLimitRepository(db);
|
|
133
|
+
});
|
|
134
|
+
afterEach(() => {
|
|
135
|
+
vi.useRealTimers();
|
|
136
|
+
});
|
|
137
|
+
it("applies rule-specific limits based on path prefix", async () => {
|
|
138
|
+
const rules = [{ method: "POST", pathPrefix: "/strict", config: { max: 1 }, scope: "strict" }];
|
|
139
|
+
const app = new Hono();
|
|
140
|
+
app.use("*", rateLimitByRoute(rules, { max: 100 }, repo));
|
|
141
|
+
app.post("/strict", (c) => c.json({ ok: true }));
|
|
142
|
+
app.get("/lenient", (c) => c.json({ ok: true }));
|
|
143
|
+
expect((await app.request(postReq("/strict"))).status).toBe(200);
|
|
144
|
+
expect((await app.request(postReq("/strict"))).status).toBe(429);
|
|
145
|
+
expect((await app.request(req("/lenient"))).status).toBe(200);
|
|
146
|
+
});
|
|
147
|
+
it("falls back to default config when no rule matches", async () => {
|
|
148
|
+
const rules = [{ method: "POST", pathPrefix: "/special", config: { max: 1 }, scope: "special" }];
|
|
149
|
+
const app = new Hono();
|
|
150
|
+
app.use("*", rateLimitByRoute(rules, { max: 2 }, repo));
|
|
151
|
+
app.get("/other", (c) => c.json({ ok: true }));
|
|
152
|
+
expect((await app.request(req("/other"))).status).toBe(200);
|
|
153
|
+
expect((await app.request(req("/other"))).status).toBe(200);
|
|
154
|
+
expect((await app.request(req("/other"))).status).toBe(429);
|
|
155
|
+
});
|
|
156
|
+
it("matches method correctly (wildcard vs specific)", async () => {
|
|
157
|
+
const rules = [
|
|
158
|
+
{ method: "*", pathPrefix: "/any-method", config: { max: 1 }, scope: "any-method" },
|
|
159
|
+
{ method: "GET", pathPrefix: "/get-only", config: { max: 1 }, scope: "get-only" },
|
|
160
|
+
];
|
|
161
|
+
const app = new Hono();
|
|
162
|
+
app.use("*", rateLimitByRoute(rules, { max: 100 }, repo));
|
|
163
|
+
app.get("/any-method", (c) => c.json({ ok: true }));
|
|
164
|
+
app.post("/any-method", (c) => c.json({ ok: true }));
|
|
165
|
+
app.get("/get-only", (c) => c.json({ ok: true }));
|
|
166
|
+
app.post("/get-only", (c) => c.json({ ok: true }));
|
|
167
|
+
expect((await app.request(req("/any-method"))).status).toBe(200);
|
|
168
|
+
expect((await app.request(postReq("/any-method"))).status).toBe(429);
|
|
169
|
+
expect((await app.request(req("/get-only"))).status).toBe(200);
|
|
170
|
+
expect((await app.request(req("/get-only"))).status).toBe(429);
|
|
171
|
+
expect((await app.request(postReq("/get-only"))).status).toBe(200);
|
|
172
|
+
});
|
|
173
|
+
it("first matching rule wins", async () => {
|
|
174
|
+
const rules = [
|
|
175
|
+
{ method: "POST", pathPrefix: "/api/billing/checkout", config: { max: 2 }, scope: "billing:checkout" },
|
|
176
|
+
{ method: "POST", pathPrefix: "/api/billing", config: { max: 100 }, scope: "billing" },
|
|
177
|
+
];
|
|
178
|
+
const app = new Hono();
|
|
179
|
+
app.use("*", rateLimitByRoute(rules, { max: 100 }, repo));
|
|
180
|
+
app.post("/api/billing/checkout", (c) => c.json({ ok: true }));
|
|
181
|
+
expect((await app.request(postReq("/api/billing/checkout"))).status).toBe(200);
|
|
182
|
+
expect((await app.request(postReq("/api/billing/checkout"))).status).toBe(200);
|
|
183
|
+
expect((await app.request(postReq("/api/billing/checkout"))).status).toBe(429);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
// Trusted proxy validation
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
describe("trusted proxy validation", () => {
|
|
190
|
+
let repo;
|
|
191
|
+
beforeEach(async () => {
|
|
192
|
+
vi.useFakeTimers();
|
|
193
|
+
await truncateAllTables(pool);
|
|
194
|
+
repo = new DrizzleRateLimitRepository(db);
|
|
195
|
+
});
|
|
196
|
+
afterEach(() => {
|
|
197
|
+
vi.useRealTimers();
|
|
198
|
+
});
|
|
199
|
+
it("ignores X-Forwarded-For when TRUSTED_PROXY_IPS is not set", () => {
|
|
200
|
+
const trusted = parseTrustedProxies(undefined);
|
|
201
|
+
expect(trusted.size).toBe(0);
|
|
202
|
+
const ip = getClientIp("spoofed-ip", "192.168.1.100", trusted);
|
|
203
|
+
expect(ip).toBe("192.168.1.100");
|
|
204
|
+
});
|
|
205
|
+
it("trusts X-Forwarded-For when socket address is in TRUSTED_PROXY_IPS", () => {
|
|
206
|
+
const trusted = parseTrustedProxies("172.18.0.5,10.0.0.1");
|
|
207
|
+
expect(trusted.size).toBe(2);
|
|
208
|
+
const ip = getClientIp("real-client-ip", "172.18.0.5", trusted);
|
|
209
|
+
expect(ip).toBe("real-client-ip");
|
|
210
|
+
});
|
|
211
|
+
it("strips ::ffff: prefix when matching trusted proxies", () => {
|
|
212
|
+
const trusted = parseTrustedProxies("172.18.0.5");
|
|
213
|
+
const ip = getClientIp("real-client-ip", "::ffff:172.18.0.5", trusted);
|
|
214
|
+
expect(ip).toBe("real-client-ip");
|
|
215
|
+
});
|
|
216
|
+
it("falls back to socket address when socket is not a trusted proxy", () => {
|
|
217
|
+
const trusted = parseTrustedProxies("172.18.0.5");
|
|
218
|
+
const ip = getClientIp("spoofed-ip", "evil-direct-client", trusted);
|
|
219
|
+
expect(ip).toBe("evil-direct-client");
|
|
220
|
+
});
|
|
221
|
+
it("uses last XFF value (rightmost) when proxy is trusted", () => {
|
|
222
|
+
const trusted = parseTrustedProxies("172.18.0.5");
|
|
223
|
+
const ip = getClientIp("fake, real-client", "172.18.0.5", trusted);
|
|
224
|
+
expect(ip).toBe("real-client");
|
|
225
|
+
});
|
|
226
|
+
it("returns 'unknown' when no socket address and no trusted proxy", () => {
|
|
227
|
+
const trusted = parseTrustedProxies(undefined);
|
|
228
|
+
const ip = getClientIp(undefined, undefined, trusted);
|
|
229
|
+
expect(ip).toBe("unknown");
|
|
230
|
+
});
|
|
231
|
+
it("rate limits by socket IP when XFF is spoofed without trusted proxy", async () => {
|
|
232
|
+
delete process.env.TRUSTED_PROXY_IPS;
|
|
233
|
+
const app = new Hono();
|
|
234
|
+
app.use("/test", rateLimit({ max: 1, repo, scope: "test" }));
|
|
235
|
+
app.get("/test", (c) => c.json({ ok: true }));
|
|
236
|
+
const r1 = new Request("http://localhost/test", {
|
|
237
|
+
headers: { "x-forwarded-for": "attacker-ip-1" },
|
|
238
|
+
});
|
|
239
|
+
const r2 = new Request("http://localhost/test", {
|
|
240
|
+
headers: { "x-forwarded-for": "attacker-ip-2" },
|
|
241
|
+
});
|
|
242
|
+
const res1 = await app.request(r1);
|
|
243
|
+
expect(res1.status).toBe(200);
|
|
244
|
+
const res2 = await app.request(r2);
|
|
245
|
+
expect(res2.status).toBe(429);
|
|
246
|
+
});
|
|
247
|
+
});
|