@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,72 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
3
|
+
import { tenantApiKeys } from "../../db/schema/tenant-api-keys.js";
|
|
4
|
+
import { createTestDb, truncateAllTables } from "../../test/db.js";
|
|
5
|
+
import { DrizzleKeyResolutionRepository } from "./key-resolution-repository.js";
|
|
6
|
+
let db;
|
|
7
|
+
let pool;
|
|
8
|
+
async function insertKey(tenantId, provider, encryptedKey) {
|
|
9
|
+
const now = Date.now();
|
|
10
|
+
await db
|
|
11
|
+
.insert(tenantApiKeys)
|
|
12
|
+
.values({ id: randomUUID(), tenantId, provider, label: "", encryptedKey, createdAt: now, updatedAt: now });
|
|
13
|
+
}
|
|
14
|
+
beforeAll(async () => {
|
|
15
|
+
({ db, pool } = await createTestDb());
|
|
16
|
+
});
|
|
17
|
+
afterAll(async () => {
|
|
18
|
+
await pool.close();
|
|
19
|
+
});
|
|
20
|
+
describe("DrizzleKeyResolutionRepository", () => {
|
|
21
|
+
let repo;
|
|
22
|
+
beforeEach(async () => {
|
|
23
|
+
await truncateAllTables(pool);
|
|
24
|
+
repo = new DrizzleKeyResolutionRepository(db);
|
|
25
|
+
});
|
|
26
|
+
it("returns the encrypted key for a matching tenant and provider", async () => {
|
|
27
|
+
const payload = JSON.stringify({ iv: "aaa", authTag: "bbb", ciphertext: "ccc" });
|
|
28
|
+
await insertKey("tenant-1", "anthropic", payload);
|
|
29
|
+
const result = await repo.findEncryptedKey("tenant-1", "anthropic");
|
|
30
|
+
expect(result).not.toBeNull();
|
|
31
|
+
expect(result?.encryptedKey).toBe(payload);
|
|
32
|
+
});
|
|
33
|
+
it("returns null when no key exists for the tenant", async () => {
|
|
34
|
+
const result = await repo.findEncryptedKey("nonexistent-tenant", "anthropic");
|
|
35
|
+
expect(result).toBeNull();
|
|
36
|
+
});
|
|
37
|
+
it("returns null when no key exists for the provider", async () => {
|
|
38
|
+
const payload = JSON.stringify({ iv: "aaa", authTag: "bbb", ciphertext: "ccc" });
|
|
39
|
+
await insertKey("tenant-1", "anthropic", payload);
|
|
40
|
+
const result = await repo.findEncryptedKey("tenant-1", "openai");
|
|
41
|
+
expect(result).toBeNull();
|
|
42
|
+
});
|
|
43
|
+
it("isolates keys between tenants (cross-tenant isolation)", async () => {
|
|
44
|
+
const payloadA = JSON.stringify({ iv: "aaa", authTag: "bbb", ciphertext: "tenant-a-secret" });
|
|
45
|
+
const payloadB = JSON.stringify({ iv: "xxx", authTag: "yyy", ciphertext: "tenant-b-secret" });
|
|
46
|
+
await insertKey("tenant-a", "anthropic", payloadA);
|
|
47
|
+
await insertKey("tenant-b", "anthropic", payloadB);
|
|
48
|
+
const resultA = await repo.findEncryptedKey("tenant-a", "anthropic");
|
|
49
|
+
const resultB = await repo.findEncryptedKey("tenant-b", "anthropic");
|
|
50
|
+
expect(resultA).not.toBeNull();
|
|
51
|
+
expect(resultB).not.toBeNull();
|
|
52
|
+
expect(resultA?.encryptedKey).toBe(payloadA);
|
|
53
|
+
expect(resultB?.encryptedKey).toBe(payloadB);
|
|
54
|
+
expect(resultA?.encryptedKey).not.toBe(resultB?.encryptedKey);
|
|
55
|
+
});
|
|
56
|
+
it("tenant A cannot see tenant B keys", async () => {
|
|
57
|
+
const payload = JSON.stringify({ iv: "aaa", authTag: "bbb", ciphertext: "only-for-b" });
|
|
58
|
+
await insertKey("tenant-b", "openai", payload);
|
|
59
|
+
const result = await repo.findEncryptedKey("tenant-a", "openai");
|
|
60
|
+
expect(result).toBeNull();
|
|
61
|
+
});
|
|
62
|
+
it("returns correct key when tenant has multiple providers", async () => {
|
|
63
|
+
const anthropicPayload = JSON.stringify({ iv: "a1", authTag: "a2", ciphertext: "anthropic-key" });
|
|
64
|
+
const openaiPayload = JSON.stringify({ iv: "o1", authTag: "o2", ciphertext: "openai-key" });
|
|
65
|
+
await insertKey("tenant-1", "anthropic", anthropicPayload);
|
|
66
|
+
await insertKey("tenant-1", "openai", openaiPayload);
|
|
67
|
+
const anthropicResult = await repo.findEncryptedKey("tenant-1", "anthropic");
|
|
68
|
+
const openaiResult = await repo.findEncryptedKey("tenant-1", "openai");
|
|
69
|
+
expect(anthropicResult?.encryptedKey).toBe(anthropicPayload);
|
|
70
|
+
expect(openaiResult?.encryptedKey).toBe(openaiPayload);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Provider } from "../types.js";
|
|
2
|
+
import type { IKeyResolutionRepository } from "./key-resolution-repository.js";
|
|
3
|
+
/** Result of resolving which API key to use. */
|
|
4
|
+
export interface ResolvedKey {
|
|
5
|
+
/** The plaintext API key. */
|
|
6
|
+
key: string;
|
|
7
|
+
/** Where the key came from. */
|
|
8
|
+
source: "tenant" | "pooled";
|
|
9
|
+
/** The provider the key is for. */
|
|
10
|
+
provider: Provider;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Resolve which API key to use for a given tenant and provider.
|
|
14
|
+
*
|
|
15
|
+
* Resolution order:
|
|
16
|
+
* 1. If the tenant has a BYOK key stored, decrypt and return it.
|
|
17
|
+
* 2. Otherwise, fall back to the pooled (platform-level) key from env vars.
|
|
18
|
+
* 3. If neither exists, return null.
|
|
19
|
+
*
|
|
20
|
+
* SECURITY: The decrypted key is returned to the caller and must be discarded
|
|
21
|
+
* after use. This function does not log, persist, or cache the plaintext key.
|
|
22
|
+
*
|
|
23
|
+
* @param repo - Repository for looking up tenant BYOK keys
|
|
24
|
+
* @param tenantId - The tenant requesting the key
|
|
25
|
+
* @param provider - The AI provider (anthropic, openai, google, discord)
|
|
26
|
+
* @param encryptionKey - The 32-byte key used to decrypt the stored BYOK key
|
|
27
|
+
* @param pooledKeys - Map of provider -> pooled API key (from env vars)
|
|
28
|
+
*/
|
|
29
|
+
export declare function resolveApiKey(repo: IKeyResolutionRepository, tenantId: string, provider: Provider, encryptionKey: Buffer, pooledKeys: Map<Provider, string>): Promise<ResolvedKey | null>;
|
|
30
|
+
/**
|
|
31
|
+
* Build a pooled keys map from environment variables.
|
|
32
|
+
*
|
|
33
|
+
* Reads:
|
|
34
|
+
* - ANTHROPIC_API_KEY
|
|
35
|
+
* - OPENAI_API_KEY
|
|
36
|
+
* - GOOGLE_API_KEY
|
|
37
|
+
* - DISCORD_BOT_TOKEN
|
|
38
|
+
*/
|
|
39
|
+
export declare function buildPooledKeysMap(env?: Record<string, string | undefined>): Map<Provider, string>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { decrypt } from "../encryption.js";
|
|
2
|
+
/**
|
|
3
|
+
* Resolve which API key to use for a given tenant and provider.
|
|
4
|
+
*
|
|
5
|
+
* Resolution order:
|
|
6
|
+
* 1. If the tenant has a BYOK key stored, decrypt and return it.
|
|
7
|
+
* 2. Otherwise, fall back to the pooled (platform-level) key from env vars.
|
|
8
|
+
* 3. If neither exists, return null.
|
|
9
|
+
*
|
|
10
|
+
* SECURITY: The decrypted key is returned to the caller and must be discarded
|
|
11
|
+
* after use. This function does not log, persist, or cache the plaintext key.
|
|
12
|
+
*
|
|
13
|
+
* @param repo - Repository for looking up tenant BYOK keys
|
|
14
|
+
* @param tenantId - The tenant requesting the key
|
|
15
|
+
* @param provider - The AI provider (anthropic, openai, google, discord)
|
|
16
|
+
* @param encryptionKey - The 32-byte key used to decrypt the stored BYOK key
|
|
17
|
+
* @param pooledKeys - Map of provider -> pooled API key (from env vars)
|
|
18
|
+
*/
|
|
19
|
+
export async function resolveApiKey(repo, tenantId, provider, encryptionKey, pooledKeys) {
|
|
20
|
+
// 1. Check for tenant BYOK key
|
|
21
|
+
const row = await repo.findEncryptedKey(tenantId, provider);
|
|
22
|
+
if (row) {
|
|
23
|
+
const payload = JSON.parse(row.encryptedKey);
|
|
24
|
+
const plaintext = decrypt(payload, encryptionKey);
|
|
25
|
+
return { key: plaintext, source: "tenant", provider };
|
|
26
|
+
}
|
|
27
|
+
// 2. Fall back to pooled key
|
|
28
|
+
const pooledKey = pooledKeys.get(provider);
|
|
29
|
+
if (pooledKey) {
|
|
30
|
+
return { key: pooledKey, source: "pooled", provider };
|
|
31
|
+
}
|
|
32
|
+
// 3. No key available
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Build a pooled keys map from environment variables.
|
|
37
|
+
*
|
|
38
|
+
* Reads:
|
|
39
|
+
* - ANTHROPIC_API_KEY
|
|
40
|
+
* - OPENAI_API_KEY
|
|
41
|
+
* - GOOGLE_API_KEY
|
|
42
|
+
* - DISCORD_BOT_TOKEN
|
|
43
|
+
*/
|
|
44
|
+
export function buildPooledKeysMap(env = process.env) {
|
|
45
|
+
const keys = new Map();
|
|
46
|
+
const mapping = [
|
|
47
|
+
["ANTHROPIC_API_KEY", "anthropic"],
|
|
48
|
+
["OPENAI_API_KEY", "openai"],
|
|
49
|
+
["GOOGLE_API_KEY", "google"],
|
|
50
|
+
["DISCORD_BOT_TOKEN", "discord"],
|
|
51
|
+
];
|
|
52
|
+
for (const [envVar, provider] of mapping) {
|
|
53
|
+
const val = env[envVar]?.trim();
|
|
54
|
+
if (val) {
|
|
55
|
+
keys.set(provider, val);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return keys;
|
|
59
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { createTestDb, truncateAllTables } from "../../test/db.js";
|
|
3
|
+
import { encrypt, generateInstanceKey } from "../encryption.js";
|
|
4
|
+
import { buildPooledKeysMap, resolveApiKey } from "./key-resolution.js";
|
|
5
|
+
import { DrizzleKeyResolutionRepository } from "./key-resolution-repository.js";
|
|
6
|
+
async function insertTenantKey(pool, tenantId, provider, encryptedKey) {
|
|
7
|
+
const { randomUUID } = await import("node:crypto");
|
|
8
|
+
const now = Date.now();
|
|
9
|
+
await pool.query("INSERT INTO tenant_api_keys (id, tenant_id, provider, label, encrypted_key, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7)", [randomUUID(), tenantId, provider, "", JSON.stringify(encryptedKey), now, now]);
|
|
10
|
+
}
|
|
11
|
+
let db;
|
|
12
|
+
let pool;
|
|
13
|
+
beforeAll(async () => {
|
|
14
|
+
({ db, pool } = await createTestDb());
|
|
15
|
+
});
|
|
16
|
+
afterAll(async () => {
|
|
17
|
+
await pool.close();
|
|
18
|
+
});
|
|
19
|
+
describe("resolveApiKey", () => {
|
|
20
|
+
let encryptionKey;
|
|
21
|
+
beforeEach(async () => {
|
|
22
|
+
await truncateAllTables(pool);
|
|
23
|
+
encryptionKey = generateInstanceKey();
|
|
24
|
+
});
|
|
25
|
+
it("returns tenant BYOK key when one is stored", async () => {
|
|
26
|
+
const encrypted = encrypt("sk-ant-tenant-key", encryptionKey);
|
|
27
|
+
await insertTenantKey(pool, "t1", "anthropic", encrypted);
|
|
28
|
+
const pooled = new Map([["anthropic", "sk-ant-pooled"]]);
|
|
29
|
+
const result = await resolveApiKey(new DrizzleKeyResolutionRepository(db), "t1", "anthropic", encryptionKey, pooled);
|
|
30
|
+
expect(result).not.toBeNull();
|
|
31
|
+
expect(result?.key).toBe("sk-ant-tenant-key");
|
|
32
|
+
expect(result?.source).toBe("tenant");
|
|
33
|
+
expect(result?.provider).toBe("anthropic");
|
|
34
|
+
});
|
|
35
|
+
it("falls back to pooled key when no tenant key stored", async () => {
|
|
36
|
+
const pooled = new Map([["openai", "sk-pooled-openai"]]);
|
|
37
|
+
const result = await resolveApiKey(new DrizzleKeyResolutionRepository(db), "t1", "openai", encryptionKey, pooled);
|
|
38
|
+
expect(result).not.toBeNull();
|
|
39
|
+
expect(result?.key).toBe("sk-pooled-openai");
|
|
40
|
+
expect(result?.source).toBe("pooled");
|
|
41
|
+
expect(result?.provider).toBe("openai");
|
|
42
|
+
});
|
|
43
|
+
it("returns null when neither tenant nor pooled key exists", async () => {
|
|
44
|
+
const pooled = new Map();
|
|
45
|
+
const result = await resolveApiKey(new DrizzleKeyResolutionRepository(db), "t1", "anthropic", encryptionKey, pooled);
|
|
46
|
+
expect(result).toBeNull();
|
|
47
|
+
});
|
|
48
|
+
it("resolves different providers independently", async () => {
|
|
49
|
+
const encrypted = encrypt("my-google-key", encryptionKey);
|
|
50
|
+
await insertTenantKey(pool, "t1", "google", encrypted);
|
|
51
|
+
const pooled = new Map([["anthropic", "sk-ant-pooled"]]);
|
|
52
|
+
const googleResult = await resolveApiKey(new DrizzleKeyResolutionRepository(db), "t1", "google", encryptionKey, pooled);
|
|
53
|
+
expect(googleResult?.source).toBe("tenant");
|
|
54
|
+
expect(googleResult?.key).toBe("my-google-key");
|
|
55
|
+
const anthropicResult = await resolveApiKey(new DrizzleKeyResolutionRepository(db), "t1", "anthropic", encryptionKey, pooled);
|
|
56
|
+
expect(anthropicResult?.source).toBe("pooled");
|
|
57
|
+
expect(anthropicResult?.key).toBe("sk-ant-pooled");
|
|
58
|
+
const discordResult = await resolveApiKey(new DrizzleKeyResolutionRepository(db), "t1", "discord", encryptionKey, pooled);
|
|
59
|
+
expect(discordResult).toBeNull();
|
|
60
|
+
});
|
|
61
|
+
it("isolates keys between tenants", async () => {
|
|
62
|
+
const encrypted = encrypt("t1-anthropic-key", encryptionKey);
|
|
63
|
+
await insertTenantKey(pool, "t1", "anthropic", encrypted);
|
|
64
|
+
const pooled = new Map([["anthropic", "sk-ant-pooled"]]);
|
|
65
|
+
const t1Result = await resolveApiKey(new DrizzleKeyResolutionRepository(db), "t1", "anthropic", encryptionKey, pooled);
|
|
66
|
+
expect(t1Result?.source).toBe("tenant");
|
|
67
|
+
const t2Result = await resolveApiKey(new DrizzleKeyResolutionRepository(db), "t2", "anthropic", encryptionKey, pooled);
|
|
68
|
+
expect(t2Result?.source).toBe("pooled");
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
describe("buildPooledKeysMap", () => {
|
|
72
|
+
it("reads keys from environment variables", () => {
|
|
73
|
+
const env = {
|
|
74
|
+
ANTHROPIC_API_KEY: "sk-ant-test",
|
|
75
|
+
OPENAI_API_KEY: "sk-openai-test",
|
|
76
|
+
GOOGLE_API_KEY: "google-test",
|
|
77
|
+
DISCORD_BOT_TOKEN: "discord-test",
|
|
78
|
+
};
|
|
79
|
+
const keys = buildPooledKeysMap(env);
|
|
80
|
+
expect(keys.get("anthropic")).toBe("sk-ant-test");
|
|
81
|
+
expect(keys.get("openai")).toBe("sk-openai-test");
|
|
82
|
+
expect(keys.get("google")).toBe("google-test");
|
|
83
|
+
expect(keys.get("discord")).toBe("discord-test");
|
|
84
|
+
});
|
|
85
|
+
it("ignores missing env vars", () => {
|
|
86
|
+
const keys = buildPooledKeysMap({});
|
|
87
|
+
expect(keys.size).toBe(0);
|
|
88
|
+
});
|
|
89
|
+
it("trims whitespace from values", () => {
|
|
90
|
+
const keys = buildPooledKeysMap({ ANTHROPIC_API_KEY: " sk-ant-test " });
|
|
91
|
+
expect(keys.get("anthropic")).toBe("sk-ant-test");
|
|
92
|
+
});
|
|
93
|
+
it("ignores empty values", () => {
|
|
94
|
+
const keys = buildPooledKeysMap({ ANTHROPIC_API_KEY: " " });
|
|
95
|
+
expect(keys.size).toBe(0);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { PlatformDb } from "../../db/index.js";
|
|
2
|
+
import type { Provider } from "../types.js";
|
|
3
|
+
/** Minimal interface for org membership lookup (provided by consumer). */
|
|
4
|
+
export interface IOrgMembershipRepository {
|
|
5
|
+
getOrgTenantIdForMember(memberTenantId: string): Promise<string | null>;
|
|
6
|
+
}
|
|
7
|
+
export declare class DrizzleOrgMembershipRepository implements IOrgMembershipRepository {
|
|
8
|
+
private readonly db;
|
|
9
|
+
constructor(db: PlatformDb);
|
|
10
|
+
getOrgTenantIdForMember(memberTenantId: string): Promise<string | null>;
|
|
11
|
+
}
|
|
12
|
+
/** Extended resolution result that includes "org" as a source. */
|
|
13
|
+
export interface OrgResolvedKey {
|
|
14
|
+
key: string;
|
|
15
|
+
source: "tenant" | "org" | "pooled";
|
|
16
|
+
provider: Provider;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Resolve API key with org fallback.
|
|
20
|
+
*
|
|
21
|
+
* Resolution order:
|
|
22
|
+
* 1. Personal tenant BYOK key
|
|
23
|
+
* 2. Org tenant BYOK key (if member belongs to an org)
|
|
24
|
+
* 3. Pooled platform key
|
|
25
|
+
* 4. null
|
|
26
|
+
*
|
|
27
|
+
* @param lookupKey - Callback to look up a decrypted key for a given tenantId, provider, and encryption key
|
|
28
|
+
* @param deriveKey - Function to derive encryption key for a given tenantId
|
|
29
|
+
*/
|
|
30
|
+
export declare function resolveApiKeyWithOrgFallback(lookupKey: (tenantId: string, provider: Provider, encKey: Buffer) => Promise<string | null>, tenantId: string, provider: Provider, encryptionKey: Buffer, pooledKeys: Map<Provider, string>, deriveKey: (tenantId: string) => Buffer, orgMembershipRepo: IOrgMembershipRepository): Promise<OrgResolvedKey | null>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { eq } from "drizzle-orm";
|
|
2
|
+
import { orgMemberships } from "../../db/schema/index.js";
|
|
3
|
+
export class DrizzleOrgMembershipRepository {
|
|
4
|
+
db;
|
|
5
|
+
constructor(db) {
|
|
6
|
+
this.db = db;
|
|
7
|
+
}
|
|
8
|
+
async getOrgTenantIdForMember(memberTenantId) {
|
|
9
|
+
const rows = await this.db
|
|
10
|
+
.select({ orgTenantId: orgMemberships.orgTenantId })
|
|
11
|
+
.from(orgMemberships)
|
|
12
|
+
.where(eq(orgMemberships.memberTenantId, memberTenantId));
|
|
13
|
+
return rows[0]?.orgTenantId ?? null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Resolve API key with org fallback.
|
|
18
|
+
*
|
|
19
|
+
* Resolution order:
|
|
20
|
+
* 1. Personal tenant BYOK key
|
|
21
|
+
* 2. Org tenant BYOK key (if member belongs to an org)
|
|
22
|
+
* 3. Pooled platform key
|
|
23
|
+
* 4. null
|
|
24
|
+
*
|
|
25
|
+
* @param lookupKey - Callback to look up a decrypted key for a given tenantId, provider, and encryption key
|
|
26
|
+
* @param deriveKey - Function to derive encryption key for a given tenantId
|
|
27
|
+
*/
|
|
28
|
+
export async function resolveApiKeyWithOrgFallback(lookupKey, tenantId, provider, encryptionKey, pooledKeys, deriveKey, orgMembershipRepo) {
|
|
29
|
+
// 1. Check personal tenant key
|
|
30
|
+
const personal = await lookupKey(tenantId, provider, encryptionKey);
|
|
31
|
+
if (personal) {
|
|
32
|
+
return { key: personal, source: "tenant", provider };
|
|
33
|
+
}
|
|
34
|
+
// 2. Check org membership and org key
|
|
35
|
+
const orgTenantId = await orgMembershipRepo.getOrgTenantIdForMember(tenantId);
|
|
36
|
+
if (orgTenantId) {
|
|
37
|
+
const orgEncKey = deriveKey(orgTenantId);
|
|
38
|
+
const orgResult = await lookupKey(orgTenantId, provider, orgEncKey);
|
|
39
|
+
if (orgResult) {
|
|
40
|
+
return { key: orgResult, source: "org", provider };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// 3. Pooled key
|
|
44
|
+
const pooledKey = pooledKeys.get(provider);
|
|
45
|
+
if (pooledKey) {
|
|
46
|
+
return { key: pooledKey, source: "pooled", provider };
|
|
47
|
+
}
|
|
48
|
+
// 4. None
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
3
|
+
import { createTestDb, truncateAllTables } from "../../test/db.js";
|
|
4
|
+
import { encrypt, generateInstanceKey } from "../encryption.js";
|
|
5
|
+
import { resolveApiKey } from "./key-resolution.js";
|
|
6
|
+
import { DrizzleKeyResolutionRepository } from "./key-resolution-repository.js";
|
|
7
|
+
import { DrizzleOrgMembershipRepository, resolveApiKeyWithOrgFallback } from "./org-key-resolution.js";
|
|
8
|
+
async function insertTenantKey(pool, tenantId, provider, encryptedKey) {
|
|
9
|
+
const now = Date.now();
|
|
10
|
+
await pool.query("INSERT INTO tenant_api_keys (id, tenant_id, provider, label, encrypted_key, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7)", [randomUUID(), tenantId, provider, "", JSON.stringify(encryptedKey), now, now]);
|
|
11
|
+
}
|
|
12
|
+
async function insertOrgMembership(pool, orgTenantId, memberTenantId) {
|
|
13
|
+
await pool.query("INSERT INTO org_memberships (org_tenant_id, member_tenant_id, created_at) VALUES ($1, $2, $3)", [
|
|
14
|
+
orgTenantId,
|
|
15
|
+
memberTenantId,
|
|
16
|
+
Date.now(),
|
|
17
|
+
]);
|
|
18
|
+
}
|
|
19
|
+
describe("resolveApiKeyWithOrgFallback", () => {
|
|
20
|
+
let db;
|
|
21
|
+
let pool;
|
|
22
|
+
let membershipRepo;
|
|
23
|
+
let encryptionKey;
|
|
24
|
+
let orgEncryptionKey;
|
|
25
|
+
const pooled = new Map();
|
|
26
|
+
beforeAll(async () => {
|
|
27
|
+
({ db, pool } = await createTestDb());
|
|
28
|
+
});
|
|
29
|
+
afterAll(async () => {
|
|
30
|
+
await pool.close();
|
|
31
|
+
});
|
|
32
|
+
beforeEach(async () => {
|
|
33
|
+
await truncateAllTables(pool);
|
|
34
|
+
membershipRepo = new DrizzleOrgMembershipRepository(db);
|
|
35
|
+
encryptionKey = generateInstanceKey();
|
|
36
|
+
orgEncryptionKey = generateInstanceKey();
|
|
37
|
+
});
|
|
38
|
+
it("returns personal key when member has one (no org fallback)", async () => {
|
|
39
|
+
const encrypted = encrypt("sk-personal", encryptionKey);
|
|
40
|
+
await insertTenantKey(pool, "member-1", "anthropic", encrypted);
|
|
41
|
+
await insertOrgMembership(pool, "org-1", "member-1");
|
|
42
|
+
const orgEncrypted = encrypt("sk-org", orgEncryptionKey);
|
|
43
|
+
await insertTenantKey(pool, "org-1", "anthropic", orgEncrypted);
|
|
44
|
+
const result = await resolveApiKeyWithOrgFallback((tid, prov, encKey) => resolveApiKey(new DrizzleKeyResolutionRepository(db), tid, prov, encKey, new Map()).then((r) => r?.key ?? null), "member-1", "anthropic", encryptionKey, pooled, (tenantId) => (tenantId === "org-1" ? orgEncryptionKey : encryptionKey), membershipRepo);
|
|
45
|
+
expect(result).not.toBeNull();
|
|
46
|
+
expect(result?.key).toBe("sk-personal");
|
|
47
|
+
expect(result?.source).toBe("tenant");
|
|
48
|
+
});
|
|
49
|
+
it("falls back to org key when member has no personal key", async () => {
|
|
50
|
+
await insertOrgMembership(pool, "org-1", "member-1");
|
|
51
|
+
const orgEncrypted = encrypt("sk-org-key", orgEncryptionKey);
|
|
52
|
+
await insertTenantKey(pool, "org-1", "anthropic", orgEncrypted);
|
|
53
|
+
const result = await resolveApiKeyWithOrgFallback((tid, prov, encKey) => resolveApiKey(new DrizzleKeyResolutionRepository(db), tid, prov, encKey, new Map()).then((r) => r?.key ?? null), "member-1", "anthropic", encryptionKey, pooled, (tenantId) => (tenantId === "org-1" ? orgEncryptionKey : encryptionKey), membershipRepo);
|
|
54
|
+
expect(result).not.toBeNull();
|
|
55
|
+
expect(result?.key).toBe("sk-org-key");
|
|
56
|
+
expect(result?.source).toBe("org");
|
|
57
|
+
});
|
|
58
|
+
it("falls back to pooled when neither member nor org has a key", async () => {
|
|
59
|
+
await insertOrgMembership(pool, "org-1", "member-1");
|
|
60
|
+
const pooledKeys = new Map([["anthropic", "sk-pooled"]]);
|
|
61
|
+
const result = await resolveApiKeyWithOrgFallback((tid, prov, encKey) => resolveApiKey(new DrizzleKeyResolutionRepository(db), tid, prov, encKey, new Map()).then((r) => r?.key ?? null), "member-1", "anthropic", encryptionKey, pooledKeys, () => encryptionKey, membershipRepo);
|
|
62
|
+
expect(result).not.toBeNull();
|
|
63
|
+
expect(result?.key).toBe("sk-pooled");
|
|
64
|
+
expect(result?.source).toBe("pooled");
|
|
65
|
+
});
|
|
66
|
+
it("returns null when no key exists anywhere", async () => {
|
|
67
|
+
const result = await resolveApiKeyWithOrgFallback((tid, prov, encKey) => resolveApiKey(new DrizzleKeyResolutionRepository(db), tid, prov, encKey, new Map()).then((r) => r?.key ?? null), "member-1", "anthropic", encryptionKey, pooled, () => encryptionKey, membershipRepo);
|
|
68
|
+
expect(result).toBeNull();
|
|
69
|
+
});
|
|
70
|
+
it("works for tenants not in any org (same as resolveApiKey)", async () => {
|
|
71
|
+
const encrypted = encrypt("sk-solo", encryptionKey);
|
|
72
|
+
await insertTenantKey(pool, "solo-tenant", "openai", encrypted);
|
|
73
|
+
const result = await resolveApiKeyWithOrgFallback((tid, prov, encKey) => resolveApiKey(new DrizzleKeyResolutionRepository(db), tid, prov, encKey, new Map()).then((r) => r?.key ?? null), "solo-tenant", "openai", encryptionKey, pooled, () => encryptionKey, membershipRepo);
|
|
74
|
+
expect(result).not.toBeNull();
|
|
75
|
+
expect(result?.key).toBe("sk-solo");
|
|
76
|
+
expect(result?.source).toBe("tenant");
|
|
77
|
+
});
|
|
78
|
+
it("full chain: personal > org > pooled > null", async () => {
|
|
79
|
+
const memberKey = generateInstanceKey();
|
|
80
|
+
const orgKey = generateInstanceKey();
|
|
81
|
+
const pooledKeys = new Map([["discord", "pooled-discord"]]);
|
|
82
|
+
const deriveKey = (tid) => (tid === "org-1" ? orgKey : memberKey);
|
|
83
|
+
await insertOrgMembership(pool, "org-1", "m1");
|
|
84
|
+
await insertTenantKey(pool, "m1", "anthropic", encrypt("personal-anthropic", memberKey));
|
|
85
|
+
await insertTenantKey(pool, "org-1", "openai", encrypt("org-openai", orgKey));
|
|
86
|
+
const lookupKey = (tid, prov, encKey) => resolveApiKey(new DrizzleKeyResolutionRepository(db), tid, prov, encKey, new Map()).then((r) => r?.key ?? null);
|
|
87
|
+
// anthropic: personal wins
|
|
88
|
+
const r1 = await resolveApiKeyWithOrgFallback(lookupKey, "m1", "anthropic", memberKey, pooledKeys, deriveKey, membershipRepo);
|
|
89
|
+
expect(r1?.source).toBe("tenant");
|
|
90
|
+
expect(r1?.key).toBe("personal-anthropic");
|
|
91
|
+
// openai: org fallback
|
|
92
|
+
const r2 = await resolveApiKeyWithOrgFallback(lookupKey, "m1", "openai", memberKey, pooledKeys, deriveKey, membershipRepo);
|
|
93
|
+
expect(r2?.source).toBe("org");
|
|
94
|
+
expect(r2?.key).toBe("org-openai");
|
|
95
|
+
// discord: pooled fallback
|
|
96
|
+
const r3 = await resolveApiKeyWithOrgFallback(lookupKey, "m1", "discord", memberKey, pooledKeys, deriveKey, membershipRepo);
|
|
97
|
+
expect(r3?.source).toBe("pooled");
|
|
98
|
+
expect(r3?.key).toBe("pooled-discord");
|
|
99
|
+
// google: null
|
|
100
|
+
const r4 = await resolveApiKeyWithOrgFallback(lookupKey, "m1", "google", memberKey, new Map(), deriveKey, membershipRepo);
|
|
101
|
+
expect(r4).toBeNull();
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { PlatformDb } from "../../db/index.js";
|
|
2
|
+
import type { EncryptedPayload } from "../types.js";
|
|
3
|
+
/** A stored tenant API key record. */
|
|
4
|
+
export interface TenantApiKey {
|
|
5
|
+
id: string;
|
|
6
|
+
tenant_id: string;
|
|
7
|
+
provider: string;
|
|
8
|
+
/** Label for display (e.g. "My Anthropic key"). Never contains the key itself. */
|
|
9
|
+
label: string;
|
|
10
|
+
/** AES-256-GCM encrypted key payload (JSON-serialized EncryptedPayload). */
|
|
11
|
+
encrypted_key: string;
|
|
12
|
+
created_at: number;
|
|
13
|
+
updated_at: number;
|
|
14
|
+
}
|
|
15
|
+
export interface ITenantKeyRepository {
|
|
16
|
+
upsert(tenantId: string, provider: string, encryptedKey: EncryptedPayload, label?: string): Promise<string>;
|
|
17
|
+
get(tenantId: string, provider: string): Promise<TenantApiKey | undefined>;
|
|
18
|
+
listForTenant(tenantId: string): Promise<Omit<TenantApiKey, "encrypted_key">[]>;
|
|
19
|
+
delete(tenantId: string, provider: string): Promise<boolean>;
|
|
20
|
+
deleteAllForTenant(tenantId: string): Promise<number>;
|
|
21
|
+
}
|
|
22
|
+
/** CRUD store for tenant API keys using Drizzle ORM. */
|
|
23
|
+
export declare class TenantKeyRepository implements ITenantKeyRepository {
|
|
24
|
+
private readonly db;
|
|
25
|
+
constructor(db: PlatformDb);
|
|
26
|
+
/** Store or replace a tenant's key for a provider. Returns the record ID. */
|
|
27
|
+
upsert(tenantId: string, provider: string, encryptedKey: EncryptedPayload, label?: string): Promise<string>;
|
|
28
|
+
/** Get a tenant's key record for a provider. Returns undefined if none stored. */
|
|
29
|
+
get(tenantId: string, provider: string): Promise<TenantApiKey | undefined>;
|
|
30
|
+
/** List all key records for a tenant. Never returns plaintext keys. */
|
|
31
|
+
listForTenant(tenantId: string): Promise<Omit<TenantApiKey, "encrypted_key">[]>;
|
|
32
|
+
/** Delete a tenant's key for a provider. Returns true if a row was deleted. */
|
|
33
|
+
delete(tenantId: string, provider: string): Promise<boolean>;
|
|
34
|
+
/** Delete all keys for a tenant. Returns the number of rows deleted. */
|
|
35
|
+
deleteAllForTenant(tenantId: string): Promise<number>;
|
|
36
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { and, eq } from "drizzle-orm";
|
|
3
|
+
import { tenantApiKeys } from "../../db/schema/index.js";
|
|
4
|
+
/** CRUD store for tenant API keys using Drizzle ORM. */
|
|
5
|
+
export class TenantKeyRepository {
|
|
6
|
+
db;
|
|
7
|
+
constructor(db) {
|
|
8
|
+
this.db = db;
|
|
9
|
+
}
|
|
10
|
+
/** Store or replace a tenant's key for a provider. Returns the record ID. */
|
|
11
|
+
async upsert(tenantId, provider, encryptedKey, label = "") {
|
|
12
|
+
const now = Date.now();
|
|
13
|
+
const serialized = JSON.stringify(encryptedKey);
|
|
14
|
+
const existing = await this.db
|
|
15
|
+
.select({ id: tenantApiKeys.id })
|
|
16
|
+
.from(tenantApiKeys)
|
|
17
|
+
.where(and(eq(tenantApiKeys.tenantId, tenantId), eq(tenantApiKeys.provider, provider)))
|
|
18
|
+
.limit(1);
|
|
19
|
+
if (existing.length > 0) {
|
|
20
|
+
await this.db
|
|
21
|
+
.update(tenantApiKeys)
|
|
22
|
+
.set({ encryptedKey: serialized, label, updatedAt: now })
|
|
23
|
+
.where(eq(tenantApiKeys.id, existing[0].id));
|
|
24
|
+
return existing[0].id;
|
|
25
|
+
}
|
|
26
|
+
const id = randomUUID();
|
|
27
|
+
await this.db.insert(tenantApiKeys).values({
|
|
28
|
+
id,
|
|
29
|
+
tenantId,
|
|
30
|
+
provider,
|
|
31
|
+
label,
|
|
32
|
+
encryptedKey: serialized,
|
|
33
|
+
createdAt: now,
|
|
34
|
+
updatedAt: now,
|
|
35
|
+
});
|
|
36
|
+
return id;
|
|
37
|
+
}
|
|
38
|
+
/** Get a tenant's key record for a provider. Returns undefined if none stored. */
|
|
39
|
+
async get(tenantId, provider) {
|
|
40
|
+
const rows = await this.db
|
|
41
|
+
.select()
|
|
42
|
+
.from(tenantApiKeys)
|
|
43
|
+
.where(and(eq(tenantApiKeys.tenantId, tenantId), eq(tenantApiKeys.provider, provider)))
|
|
44
|
+
.limit(1);
|
|
45
|
+
if (rows.length === 0)
|
|
46
|
+
return undefined;
|
|
47
|
+
const r = rows[0];
|
|
48
|
+
return {
|
|
49
|
+
id: r.id,
|
|
50
|
+
tenant_id: r.tenantId,
|
|
51
|
+
provider: r.provider,
|
|
52
|
+
label: r.label,
|
|
53
|
+
encrypted_key: r.encryptedKey,
|
|
54
|
+
created_at: r.createdAt,
|
|
55
|
+
updated_at: r.updatedAt,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/** List all key records for a tenant. Never returns plaintext keys. */
|
|
59
|
+
async listForTenant(tenantId) {
|
|
60
|
+
const rows = await this.db
|
|
61
|
+
.select({
|
|
62
|
+
id: tenantApiKeys.id,
|
|
63
|
+
tenantId: tenantApiKeys.tenantId,
|
|
64
|
+
provider: tenantApiKeys.provider,
|
|
65
|
+
label: tenantApiKeys.label,
|
|
66
|
+
createdAt: tenantApiKeys.createdAt,
|
|
67
|
+
updatedAt: tenantApiKeys.updatedAt,
|
|
68
|
+
})
|
|
69
|
+
.from(tenantApiKeys)
|
|
70
|
+
.where(eq(tenantApiKeys.tenantId, tenantId));
|
|
71
|
+
return rows.map((r) => ({
|
|
72
|
+
id: r.id,
|
|
73
|
+
tenant_id: r.tenantId,
|
|
74
|
+
provider: r.provider,
|
|
75
|
+
label: r.label,
|
|
76
|
+
created_at: r.createdAt,
|
|
77
|
+
updated_at: r.updatedAt,
|
|
78
|
+
}));
|
|
79
|
+
}
|
|
80
|
+
/** Delete a tenant's key for a provider. Returns true if a row was deleted. */
|
|
81
|
+
async delete(tenantId, provider) {
|
|
82
|
+
const result = await this.db
|
|
83
|
+
.delete(tenantApiKeys)
|
|
84
|
+
.where(and(eq(tenantApiKeys.tenantId, tenantId), eq(tenantApiKeys.provider, provider)))
|
|
85
|
+
.returning({ id: tenantApiKeys.id });
|
|
86
|
+
return result.length > 0;
|
|
87
|
+
}
|
|
88
|
+
/** Delete all keys for a tenant. Returns the number of rows deleted. */
|
|
89
|
+
async deleteAllForTenant(tenantId) {
|
|
90
|
+
const result = await this.db
|
|
91
|
+
.delete(tenantApiKeys)
|
|
92
|
+
.where(eq(tenantApiKeys.tenantId, tenantId))
|
|
93
|
+
.returning({ id: tenantApiKeys.id });
|
|
94
|
+
return result.length;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|