UNPKG

stripe-replit-sync

Version:

Stripe Sync Engine to sync Stripe data to Postgres

386 lines (379 loc) 19.7 kB
import Stripe from 'stripe'; import pg, { PoolConfig, QueryResult } from 'pg'; import { ConnectionOptions } from 'node:tls'; type PostgresConfig = { schema: string; poolConfig: PoolConfig; }; declare class PostgresClient { private config; pool: pg.Pool; constructor(config: PostgresConfig); delete(table: string, id: string): Promise<boolean>; query(text: string, params?: any[]): Promise<QueryResult>; upsertMany<T extends { [Key: string]: any; }>(entries: T[], table: string): Promise<T[]>; upsertManyWithTimestampProtection<T extends { [Key: string]: any; }>(entries: T[], table: string, accountId: string, syncTimestamp?: string): Promise<T[]>; private cleanseArrayField; findMissingEntries(table: string, ids: string[]): Promise<string[]>; getSyncCursor(resource: string, accountId: string): Promise<number | null>; updateSyncCursor(resource: string, accountId: string, cursor: number): Promise<void>; markSyncRunning(resource: string, accountId: string): Promise<void>; markSyncComplete(resource: string, accountId: string): Promise<void>; markSyncError(resource: string, accountId: string, errorMessage: string): Promise<void>; upsertAccount(accountData: { id: string; raw_data: any; }, apiKeyHash: string): Promise<void>; getAllAccounts(): Promise<any[]>; /** * Looks up an account ID by API key hash * Uses the GIN index on api_key_hashes for fast lookups * @param apiKeyHash - SHA-256 hash of the Stripe API key * @returns Account ID if found, null otherwise */ getAccountIdByApiKeyHash(apiKeyHash: string): Promise<string | null>; getAccountRecordCounts(accountId: string): Promise<{ [tableName: string]: number; }>; deleteAccountWithCascade(accountId: string, useTransaction: boolean): Promise<{ [tableName: string]: number; }>; /** * Hash a string to a 32-bit integer for use with PostgreSQL advisory locks. * Uses a simple hash algorithm that produces consistent results. */ private hashToInt32; /** * Acquire a PostgreSQL advisory lock for the given key. * This lock is automatically released when the connection is closed or explicitly released. * Advisory locks are session-level and will block until the lock is available. * * @param key - A string key to lock on (will be hashed to an integer) */ acquireAdvisoryLock(key: string): Promise<void>; /** * Release a PostgreSQL advisory lock for the given key. * * @param key - The same string key used to acquire the lock */ releaseAdvisoryLock(key: string): Promise<void>; /** * Execute a function while holding an advisory lock. * The lock is automatically released after the function completes (success or error). * * IMPORTANT: This acquires a dedicated connection from the pool and holds it for the * duration of the function execution. PostgreSQL advisory locks are session-level, * so we must use the same connection for lock acquisition, operations, and release. * * @param key - A string key to lock on (will be hashed to an integer) * @param fn - The function to execute while holding the lock * @returns The result of the function */ withAdvisoryLock<T>(key: string, fn: () => Promise<T>): Promise<T>; } /** * Simple logger interface compatible with both pino and console */ interface Logger { info(message?: unknown, ...optionalParams: unknown[]): void; warn(message?: unknown, ...optionalParams: unknown[]): void; error(message?: unknown, ...optionalParams: unknown[]): void; } type RevalidateEntity = 'charge' | 'credit_note' | 'customer' | 'dispute' | 'invoice' | 'payment_intent' | 'payment_method' | 'plan' | 'price' | 'product' | 'refund' | 'review' | 'radar.early_fraud_warning' | 'setup_intent' | 'subscription' | 'subscription_schedule' | 'tax_id' | 'entitlements'; type StripeSyncConfig = { /** @deprecated Use `poolConfig` with a connection string instead. */ databaseUrl?: string; /** Stripe secret key used to authenticate requests to the Stripe API. Defaults to empty string */ stripeSecretKey: string; /** Stripe account ID. If not provided, will be retrieved from Stripe API. Used as fallback option. */ stripeAccountId?: string; /** Stripe webhook signing secret for validating webhook signatures. Required if not using managed webhooks. */ stripeWebhookSecret?: string; /** Stripe API version for the webhooks, defaults to 2020-08-27 */ stripeApiVersion?: string; /** * Stripe limits related lists like invoice items in an invoice to 10 by default. * By enabling this, sync-engine automatically fetches the remaining elements before saving * */ autoExpandLists?: boolean; /** * If true, the sync engine will backfill related entities, i.e. when a invoice webhook comes in, it ensures that the customer is present and synced. * This ensures foreign key integrity, but comes at the cost of additional queries to the database (and added latency for Stripe calls if the entity is actually missing). */ backfillRelatedEntities?: boolean; /** * If true, the webhook data is not used and instead the webhook is just a trigger to fetch the entity from Stripe again. This ensures that a race condition with failed webhooks can never accidentally overwrite the data with an older state. * * Default: false */ revalidateObjectsViaStripeApi?: Array<RevalidateEntity>; /** @deprecated Use `poolConfig` instead. */ maxPostgresConnections?: number; poolConfig: PoolConfig; logger?: Logger; /** * Maximum number of retry attempts for 429 rate limit errors. * Default: 5 */ maxRetries?: number; /** * Initial delay in milliseconds before first retry attempt. * Delay increases exponentially: 1s, 2s, 4s, 8s, 16s, etc. * Default: 1000 (1 second) */ initialRetryDelayMs?: number; /** * Maximum delay in milliseconds between retry attempts. * Default: 60000 (60 seconds) */ maxRetryDelayMs?: number; /** * Random jitter in milliseconds added to retry delays to prevent thundering herd. * Default: 500 */ retryJitterMs?: number; }; type SyncObject = 'all' | 'customer' | 'customer_with_entitlements' | 'invoice' | 'price' | 'product' | 'subscription' | 'subscription_schedules' | 'setup_intent' | 'payment_method' | 'dispute' | 'charge' | 'payment_intent' | 'plan' | 'tax_id' | 'credit_note' | 'early_fraud_warning' | 'refund' | 'checkout_sessions'; interface Sync { synced: number; } interface SyncBackfill { products?: Sync; prices?: Sync; plans?: Sync; customers?: Sync; subscriptions?: Sync; subscriptionSchedules?: Sync; invoices?: Sync; setupIntents?: Sync; paymentIntents?: Sync; paymentMethods?: Sync; disputes?: Sync; charges?: Sync; taxIds?: Sync; creditNotes?: Sync; earlyFraudWarnings?: Sync; refunds?: Sync; checkoutSessions?: Sync; } interface SyncBackfillParams { created?: { /** * Minimum value to filter by (exclusive) */ gt?: number; /** * Minimum value to filter by (inclusive) */ gte?: number; /** * Maximum value to filter by (exclusive) */ lt?: number; /** * Maximum value to filter by (inclusive) */ lte?: number; }; object?: SyncObject; backfillRelatedEntities?: boolean; } interface SyncEntitlementsParams { object: 'entitlements'; customerId: string; pagination?: Pick<Stripe.PaginationParams, 'starting_after' | 'ending_before'>; } interface SyncFeaturesParams { object: 'features'; pagination?: Pick<Stripe.PaginationParams, 'starting_after' | 'ending_before'>; } declare class StripeSync { private config; stripe: Stripe; postgresClient: PostgresClient; private cachedAccount; constructor(config: StripeSyncConfig); /** * Get the Stripe account ID. Uses database lookup by API key hash for fast lookups, * with fallback to Stripe API if not found (first-time setup or new API key). */ getAccountId(objectAccountId?: string): Promise<string>; /** * Upsert Stripe account information to the database * @param account - Stripe account object * @param apiKeyHash - SHA-256 hash of API key to store for fast lookups */ private upsertAccount; /** * Get the current account being synced */ getCurrentAccount(): Promise<Stripe.Account | null>; /** * Get all accounts that have been synced to the database */ getAllSyncedAccounts(): Promise<Stripe.Account[]>; /** * DANGEROUS: Delete an account and all associated data from the database * This operation cannot be undone! * * @param accountId - The Stripe account ID to delete * @param options - Options for deletion behavior * @param options.dryRun - If true, only count records without deleting (default: false) * @param options.useTransaction - If true, use transaction for atomic deletion (default: true) * @returns Deletion summary with counts and warnings */ dangerouslyDeleteSyncedAccountData(accountId: string, options?: { dryRun?: boolean; useTransaction?: boolean; }): Promise<{ deletedAccountId: string; deletedRecordCounts: { [tableName: string]: number; }; warnings: string[]; }>; processWebhook(payload: Buffer | string, signature: string | undefined): Promise<void>; private readonly eventHandlers; processEvent(event: Stripe.Event): Promise<void>; /** * Returns an array of all webhook event types that this sync engine can handle. * Useful for configuring webhook endpoints with specific event subscriptions. */ getSupportedEventTypes(): Stripe.WebhookEndpointCreateParams.EnabledEvent[]; private handleChargeEvent; private handleCustomerDeletedEvent; private handleCustomerEvent; private handleCheckoutSessionEvent; private handleSubscriptionEvent; private handleTaxIdEvent; private handleTaxIdDeletedEvent; private handleInvoiceEvent; private handleProductEvent; private handleProductDeletedEvent; private handlePriceEvent; private handlePriceDeletedEvent; private handlePlanEvent; private handlePlanDeletedEvent; private handleSetupIntentEvent; private handleSubscriptionScheduleEvent; private handlePaymentMethodEvent; private handleDisputeEvent; private handlePaymentIntentEvent; private handleCreditNoteEvent; private handleEarlyFraudWarningEvent; private handleRefundEvent; private handleReviewEvent; private handleEntitlementSummaryEvent; private getSyncTimestamp; private shouldRefetchEntity; private fetchOrUseWebhookData; syncSingleEntity(stripeId: string): Promise<Stripe.Charge[] | (Stripe.Customer | Stripe.DeletedCustomer)[] | Stripe.Checkout.Session[] | Stripe.Subscription[] | Stripe.TaxId[] | Stripe.Invoice[] | Stripe.Product[] | Stripe.Price[] | Stripe.SetupIntent[] | Stripe.PaymentMethod[] | Stripe.Dispute[] | Stripe.PaymentIntent[] | Stripe.CreditNote[] | Stripe.Radar.EarlyFraudWarning[] | Stripe.Refund[] | Stripe.Review[] | Stripe.Entitlements.Feature[] | undefined>; syncBackfill(params?: SyncBackfillParams): Promise<SyncBackfill>; syncProducts(syncParams?: SyncBackfillParams): Promise<Sync>; syncPrices(syncParams?: SyncBackfillParams): Promise<Sync>; syncPlans(syncParams?: SyncBackfillParams): Promise<Sync>; syncCustomers(syncParams?: SyncBackfillParams): Promise<Sync>; syncSubscriptions(syncParams?: SyncBackfillParams): Promise<Sync>; syncSubscriptionSchedules(syncParams?: SyncBackfillParams): Promise<Sync>; syncInvoices(syncParams?: SyncBackfillParams): Promise<Sync>; syncCharges(syncParams?: SyncBackfillParams): Promise<Sync>; syncSetupIntents(syncParams?: SyncBackfillParams): Promise<Sync>; syncPaymentIntents(syncParams?: SyncBackfillParams): Promise<Sync>; syncTaxIds(syncParams?: SyncBackfillParams): Promise<Sync>; syncPaymentMethods(syncParams?: SyncBackfillParams): Promise<Sync>; syncDisputes(syncParams?: SyncBackfillParams): Promise<Sync>; syncEarlyFraudWarnings(syncParams?: SyncBackfillParams): Promise<Sync>; syncRefunds(syncParams?: SyncBackfillParams): Promise<Sync>; syncCreditNotes(syncParams?: SyncBackfillParams): Promise<Sync>; syncFeatures(syncParams?: SyncFeaturesParams): Promise<Sync>; syncEntitlements(customerId: string, syncParams?: SyncEntitlementsParams): Promise<Sync>; syncCheckoutSessions(syncParams?: SyncBackfillParams): Promise<Sync>; private fetchAndUpsert; private upsertCharges; private backfillCharges; private backfillPaymentIntents; private upsertCreditNotes; upsertCheckoutSessions(checkoutSessions: Stripe.Checkout.Session[], accountId: string, backfillRelatedEntities?: boolean, syncTimestamp?: string): Promise<Stripe.Checkout.Session[]>; upsertEarlyFraudWarning(earlyFraudWarnings: Stripe.Radar.EarlyFraudWarning[], accountId: string, backfillRelatedEntities?: boolean, syncTimestamp?: string): Promise<Stripe.Radar.EarlyFraudWarning[]>; upsertRefunds(refunds: Stripe.Refund[], accountId: string, backfillRelatedEntities?: boolean, syncTimestamp?: string): Promise<Stripe.Refund[]>; upsertReviews(reviews: Stripe.Review[], accountId: string, backfillRelatedEntities?: boolean, syncTimestamp?: string): Promise<Stripe.Review[]>; upsertCustomers(customers: (Stripe.Customer | Stripe.DeletedCustomer)[], accountId: string, syncTimestamp?: string): Promise<(Stripe.Customer | Stripe.DeletedCustomer)[]>; backfillCustomers(customerIds: string[], accountId: string): Promise<void>; upsertDisputes(disputes: Stripe.Dispute[], accountId: string, backfillRelatedEntities?: boolean, syncTimestamp?: string): Promise<Stripe.Dispute[]>; upsertInvoices(invoices: Stripe.Invoice[], accountId: string, backfillRelatedEntities?: boolean, syncTimestamp?: string): Promise<Stripe.Invoice[]>; backfillInvoices: (invoiceIds: string[], accountId: string) => Promise<void>; backfillPrices: (priceIds: string[], accountId: string) => Promise<void>; upsertPlans(plans: Stripe.Plan[], accountId: string, backfillRelatedEntities?: boolean, syncTimestamp?: string): Promise<Stripe.Plan[]>; deletePlan(id: string): Promise<boolean>; upsertPrices(prices: Stripe.Price[], accountId: string, backfillRelatedEntities?: boolean, syncTimestamp?: string): Promise<Stripe.Price[]>; deletePrice(id: string): Promise<boolean>; upsertProducts(products: Stripe.Product[], accountId: string, syncTimestamp?: string): Promise<Stripe.Product[]>; deleteProduct(id: string): Promise<boolean>; backfillProducts(productIds: string[], accountId: string): Promise<void>; upsertPaymentIntents(paymentIntents: Stripe.PaymentIntent[], accountId: string, backfillRelatedEntities?: boolean, syncTimestamp?: string): Promise<Stripe.PaymentIntent[]>; upsertPaymentMethods(paymentMethods: Stripe.PaymentMethod[], accountId: string, backfillRelatedEntities?: boolean, syncTimestamp?: string): Promise<Stripe.PaymentMethod[]>; upsertSetupIntents(setupIntents: Stripe.SetupIntent[], accountId: string, backfillRelatedEntities?: boolean, syncTimestamp?: string): Promise<Stripe.SetupIntent[]>; upsertTaxIds(taxIds: Stripe.TaxId[], accountId: string, backfillRelatedEntities?: boolean, syncTimestamp?: string): Promise<Stripe.TaxId[]>; deleteTaxId(id: string): Promise<boolean>; upsertSubscriptionItems(subscriptionItems: Stripe.SubscriptionItem[], accountId: string, syncTimestamp?: string): Promise<void>; fillCheckoutSessionsLineItems(checkoutSessionIds: string[], accountId: string, syncTimestamp?: string): Promise<void>; upsertCheckoutSessionLineItems(lineItems: Stripe.LineItem[], checkoutSessionId: string, accountId: string, syncTimestamp?: string): Promise<void>; markDeletedSubscriptionItems(subscriptionId: string, currentSubItemIds: string[]): Promise<{ rowCount: number; }>; upsertSubscriptionSchedules(subscriptionSchedules: Stripe.SubscriptionSchedule[], accountId: string, backfillRelatedEntities?: boolean, syncTimestamp?: string): Promise<Stripe.SubscriptionSchedule[]>; upsertSubscriptions(subscriptions: Stripe.Subscription[], accountId: string, backfillRelatedEntities?: boolean, syncTimestamp?: string): Promise<Stripe.Subscription[]>; deleteRemovedActiveEntitlements(customerId: string, currentActiveEntitlementIds: string[]): Promise<{ rowCount: number; }>; upsertFeatures(features: Stripe.Entitlements.Feature[], accountId: string, syncTimestamp?: string): Promise<Stripe.Entitlements.Feature[]>; backfillFeatures(featureIds: string[], accountId: string): Promise<void>; upsertActiveEntitlements(customerId: string, activeEntitlements: Stripe.Entitlements.ActiveEntitlement[], accountId: string, backfillRelatedEntities?: boolean, syncTimestamp?: string): Promise<{ id: string; object: "entitlements.active_entitlement"; feature: string; customer: string; livemode: boolean; lookup_key: string; }[]>; findOrCreateManagedWebhook(url: string, params?: Omit<Stripe.WebhookEndpointCreateParams, 'url'>): Promise<Stripe.WebhookEndpoint>; getManagedWebhook(id: string): Promise<Stripe.WebhookEndpoint | null>; /** * Get a managed webhook by URL and account ID. * Used for race condition recovery: when createManagedWebhook hits a unique constraint * violation (another instance created the webhook), we need to fetch the existing webhook * by URL since we only know the URL, not the ID of the webhook that won the race. */ getManagedWebhookByUrl(url: string): Promise<Stripe.WebhookEndpoint | null>; listManagedWebhooks(): Promise<Array<Stripe.WebhookEndpoint>>; updateManagedWebhook(id: string, params: Stripe.WebhookEndpointUpdateParams): Promise<Stripe.WebhookEndpoint>; deleteManagedWebhook(id: string): Promise<boolean>; upsertManagedWebhooks(webhooks: Array<Stripe.WebhookEndpoint>, accountId: string, syncTimestamp?: string): Promise<Array<Stripe.WebhookEndpoint>>; backfillSubscriptions(subscriptionIds: string[], accountId: string): Promise<void>; backfillSubscriptionSchedules: (subscriptionIds: string[], accountId: string) => Promise<void>; /** * Stripe only sends the first 10 entries by default, the option will actively fetch all entries. */ private expandEntity; private fetchMissingEntities; } type MigrationConfig = { databaseUrl: string; ssl?: ConnectionOptions; logger?: Logger; }; declare function runMigrations(config: MigrationConfig): Promise<void>; /** * Hashes a Stripe API key using SHA-256 * Used to store API key hashes in the database for fast account lookups * without storing the actual API key or making Stripe API calls * * @param apiKey - The Stripe API key (e.g., sk_test_... or sk_live_...) * @returns SHA-256 hash of the API key as a hex string */ declare function hashApiKey(apiKey: string): string; export { type Logger, PostgresClient, type RevalidateEntity, StripeSync, type StripeSyncConfig, type Sync, type SyncBackfill, type SyncBackfillParams, type SyncEntitlementsParams, type SyncFeaturesParams, type SyncObject, hashApiKey, runMigrations };