UNPKG

@polar-sh/better-auth

Version:

Polar integration for better-auth

726 lines (720 loc) 22 kB
// src/hooks/customer.ts import { APIError } from "better-auth/api"; var onUserCreate = (options) => async (user, ctx) => { if (ctx && options.createCustomerOnSignUp) { try { const params = options.getCustomerCreateParams ? await options.getCustomerCreateParams({ user }) : {}; const { result: existingCustomers } = await options.client.customers.list({ email: user.email }); const existingCustomer = existingCustomers.items[0]; if (existingCustomer) { if (existingCustomer.externalId !== user.id) { await options.client.customers.update({ id: existingCustomer.id, customerUpdate: { externalId: user.id, ...params } }); } } else { const customer = await options.client.customers.create({ ...params, email: user.email, name: user.name, externalId: user.id }); console.log(customer); } } catch (e) { if (e instanceof Error) { throw new APIError("INTERNAL_SERVER_ERROR", { message: `Polar customer creation failed. Error: ${e.message}` }); } throw new APIError("INTERNAL_SERVER_ERROR", { message: `Polar customer creation failed. Error: ${e}` }); } } }; var onUserUpdate = (options) => async (user, ctx) => { if (ctx && options.createCustomerOnSignUp) { try { await options.client.customers.updateExternal({ externalId: user.id, customerUpdateExternalID: { email: user.email, name: user.name } }); } catch (e) { if (e instanceof Error) { ctx.context.logger.error( `Polar customer update failed. Error: ${e.message}` ); } else { ctx.context.logger.error(`Polar customer update failed. Error: ${e}`); } } } }; // src/client.ts var polarClient = () => { return { id: "polar-client", $InferServerPlugin: {} }; }; // src/plugins/portal.ts import { APIError as APIError2 } from "better-auth/api"; import { sessionMiddleware } from "better-auth/api"; import { createAuthEndpoint } from "better-auth/plugins"; import { z } from "zod"; var portal = () => (polar2) => { return { portal: createAuthEndpoint( "/customer/portal", { method: "GET", use: [sessionMiddleware] }, async (ctx) => { if (!ctx.context.session?.user.id) { throw new APIError2("BAD_REQUEST", { message: "User not found" }); } try { const customerSession = await polar2.customerSessions.create({ customerExternalId: ctx.context.session?.user.id }); return ctx.json({ url: customerSession.customerPortalUrl, redirect: true }); } catch (e) { if (e instanceof Error) { ctx.context.logger.error( `Polar customer portal creation failed. Error: ${e.message}` ); } throw new APIError2("INTERNAL_SERVER_ERROR", { message: "Customer portal creation failed" }); } } ), state: createAuthEndpoint( "/customer/state", { method: "GET", use: [sessionMiddleware] }, async (ctx) => { if (!ctx.context.session.user.id) { throw new APIError2("BAD_REQUEST", { message: "User not found" }); } try { const state = await polar2.customers.getStateExternal({ externalId: ctx.context.session?.user.id }); return ctx.json(state); } catch (e) { if (e instanceof Error) { ctx.context.logger.error( `Polar subscriptions list failed. Error: ${e.message}` ); } throw new APIError2("INTERNAL_SERVER_ERROR", { message: "Subscriptions list failed" }); } } ), benefits: createAuthEndpoint( "/customer/benefits/list", { method: "GET", query: z.object({ page: z.coerce.number().optional(), limit: z.coerce.number().optional() }).optional(), use: [sessionMiddleware] }, async (ctx) => { if (!ctx.context.session.user.id) { throw new APIError2("BAD_REQUEST", { message: "User not found" }); } try { const customerSession = await polar2.customerSessions.create({ customerExternalId: ctx.context.session?.user.id }); const benefits = await polar2.customerPortal.benefitGrants.list( { customerSession: customerSession.token }, { page: ctx.query?.page, limit: ctx.query?.limit } ); return ctx.json(benefits); } catch (e) { if (e instanceof Error) { ctx.context.logger.error( `Polar benefits list failed. Error: ${e.message}` ); } throw new APIError2("INTERNAL_SERVER_ERROR", { message: "Benefits list failed" }); } } ), subscriptions: createAuthEndpoint( "/customer/subscriptions/list", { method: "GET", query: z.object({ referenceId: z.string().optional(), page: z.coerce.number().optional(), limit: z.coerce.number().optional(), active: z.coerce.boolean().optional() }).optional(), use: [sessionMiddleware] }, async (ctx) => { if (!ctx.context.session.user.id) { throw new APIError2("BAD_REQUEST", { message: "User not found" }); } if (ctx.query?.referenceId) { try { const subscriptions = await polar2.subscriptions.list({ page: ctx.query?.page, limit: ctx.query?.limit, active: ctx.query?.active, metadata: { referenceId: ctx.query?.referenceId } }); return ctx.json(subscriptions); } catch (e) { console.log(e); if (e instanceof Error) { ctx.context.logger.error( `Polar subscriptions list with referenceId failed. Error: ${e.message}` ); } throw new APIError2("INTERNAL_SERVER_ERROR", { message: "Subscriptions list with referenceId failed" }); } } try { const customerSession = await polar2.customerSessions.create({ customerExternalId: ctx.context.session?.user.id }); const subscriptions = await polar2.customerPortal.subscriptions.list( { customerSession: customerSession.token }, { page: ctx.query?.page, limit: ctx.query?.limit, active: ctx.query?.active } ); return ctx.json(subscriptions); } catch (e) { if (e instanceof Error) { ctx.context.logger.error( `Polar subscriptions list failed. Error: ${e.message}` ); } throw new APIError2("INTERNAL_SERVER_ERROR", { message: "Polar subscriptions list failed" }); } } ), orders: createAuthEndpoint( "/customer/orders/list", { method: "GET", query: z.object({ page: z.coerce.number().optional(), limit: z.coerce.number().optional(), productBillingType: z.enum(["recurring", "one_time"]).optional() }).optional(), use: [sessionMiddleware] }, async (ctx) => { if (!ctx.context.session.user.id) { throw new APIError2("BAD_REQUEST", { message: "User not found" }); } try { const customerSession = await polar2.customerSessions.create({ customerExternalId: ctx.context.session?.user.id }); const orders = await polar2.customerPortal.orders.list( { customerSession: customerSession.token }, { page: ctx.query?.page, limit: ctx.query?.limit, productBillingType: ctx.query?.productBillingType } ); return ctx.json(orders); } catch (e) { if (e instanceof Error) { ctx.context.logger.error( `Polar orders list failed. Error: ${e.message}` ); } throw new APIError2("INTERNAL_SERVER_ERROR", { message: "Orders list failed" }); } } ) }; }; // src/plugins/checkout.ts import { APIError as APIError3, getSessionFromCtx } from "better-auth/api"; import { createAuthEndpoint as createAuthEndpoint2 } from "better-auth/plugins"; import { z as z2 } from "zod"; var checkout = (checkoutOptions = {}) => (polar2) => { return { checkout: createAuthEndpoint2( "/checkout", { method: "POST", body: z2.object({ products: z2.union([z2.array(z2.string()), z2.string()]).optional(), slug: z2.string().optional(), referenceId: z2.string().optional(), customFieldData: z2.record( z2.string(), z2.union([z2.string(), z2.number(), z2.boolean()]) ).optional(), metadata: z2.record( z2.string(), z2.union([z2.string(), z2.number(), z2.boolean()]) ).optional() }) }, async (ctx) => { const session = await getSessionFromCtx(ctx); let productIds = []; if (ctx.body.slug) { const resolvedProducts = await (typeof checkoutOptions.products === "function" ? checkoutOptions.products() : checkoutOptions.products); const productId = resolvedProducts?.find( (product) => product.slug === ctx.body.slug )?.productId; if (!productId) { throw new APIError3("BAD_REQUEST", { message: "Product not found" }); } productIds = [productId]; } else { productIds = Array.isArray(ctx.body.products) ? ctx.body.products.filter((id) => id !== void 0) : [ctx.body.products].filter((id) => id !== void 0); } if (checkoutOptions.authenticatedUsersOnly && !session?.user.id) { throw new APIError3("UNAUTHORIZED", { message: "You must be logged in to checkout" }); } try { const checkout2 = await polar2.checkouts.create({ customerExternalId: session?.user.id, products: productIds, successUrl: checkoutOptions.successUrl ? new URL( checkoutOptions.successUrl, ctx.request?.url ).toString() : void 0, metadata: ctx.body.referenceId ? { referenceId: ctx.body.referenceId, ...ctx.body.metadata } : ctx.body.metadata, customFieldData: ctx.body.customFieldData }); return ctx.json({ url: checkout2.url, redirect: true }); } catch (e) { if (e instanceof Error) { ctx.context.logger.error( `Polar checkout creation failed. Error: ${e.message}` ); } throw new APIError3("INTERNAL_SERVER_ERROR", { message: "Checkout creation failed" }); } } ) }; }; // src/plugins/usage.ts import { APIError as APIError4, createAuthEndpoint as createAuthEndpoint3, sessionMiddleware as sessionMiddleware2 } from "better-auth/api"; import { z as z3 } from "zod"; var usage = (_usageOptions) => (polar2) => { return { meters: createAuthEndpoint3( "/usage/meters/list", { method: "GET", use: [sessionMiddleware2], query: z3.object({ page: z3.coerce.number().optional(), limit: z3.coerce.number().optional() }) }, async (ctx) => { if (!ctx.context.session.user.id) { throw new APIError4("BAD_REQUEST", { message: "User not found" }); } try { const customerSession = await polar2.customerSessions.create({ customerExternalId: ctx.context.session.user.id }); const customerMeters = await polar2.customerPortal.customerMeters.list( { customerSession: customerSession.token }, { page: ctx.query?.page, limit: ctx.query?.limit } ); return ctx.json(customerMeters); } catch (e) { if (e instanceof Error) { ctx.context.logger.error( `Polar meters list failed. Error: ${e.message}` ); } throw new APIError4("INTERNAL_SERVER_ERROR", { message: "Meters list failed" }); } } ), ingestion: createAuthEndpoint3( "/usage/ingest", { method: "POST", body: z3.object({ event: z3.string(), metadata: z3.record( z3.string(), z3.union([z3.string(), z3.number(), z3.boolean()]) ) }), use: [sessionMiddleware2] }, async (ctx) => { if (!ctx.context.session.user.id) { throw new APIError4("BAD_REQUEST", { message: "User not found" }); } try { const ingestion = await polar2.events.ingest({ events: [ { name: ctx.body.event, metadata: ctx.body.metadata, externalCustomerId: ctx.context.session.user.id } ] }); return ctx.json(ingestion); } catch (e) { if (e instanceof Error) { ctx.context.logger.error( `Polar ingestion failed. Error: ${e.message}` ); } throw new APIError4("INTERNAL_SERVER_ERROR", { message: "Ingestion failed" }); } } ) }; }; // src/plugins/webhooks.ts import { validateEvent } from "@polar-sh/sdk/webhooks.js"; import { APIError as APIError5, createAuthEndpoint as createAuthEndpoint4 } from "better-auth/api"; var webhooks = (options) => (_polar) => { return { polarWebhooks: createAuthEndpoint4( "/polar/webhooks", { method: "POST", metadata: { isAction: false }, cloneRequest: true }, async (ctx) => { const { secret, onPayload, onCheckoutCreated, onCheckoutUpdated, onOrderCreated, onOrderPaid, onOrderRefunded, onRefundCreated, onRefundUpdated, onSubscriptionCreated, onSubscriptionUpdated, onSubscriptionActive, onSubscriptionCanceled, onSubscriptionRevoked, onSubscriptionUncanceled, onProductCreated, onProductUpdated, onOrganizationUpdated, onBenefitCreated, onBenefitUpdated, onBenefitGrantCreated, onBenefitGrantUpdated, onBenefitGrantRevoked, onCustomerCreated, onCustomerUpdated, onCustomerDeleted, onCustomerStateChanged } = options; if (!ctx.request?.body) { throw new APIError5("INTERNAL_SERVER_ERROR"); } const buf = await ctx.request.text(); let event; try { if (!secret) { throw new APIError5("INTERNAL_SERVER_ERROR", { message: "Polar webhook secret not found" }); } const headers = { "webhook-id": ctx.request.headers.get("webhook-id"), "webhook-timestamp": ctx.request.headers.get( "webhook-timestamp" ), "webhook-signature": ctx.request.headers.get( "webhook-signature" ) }; event = validateEvent(buf, headers, secret); } catch (err) { if (err instanceof Error) { ctx.context.logger.error(`${err.message}`); throw new APIError5("BAD_REQUEST", { message: `Webhook Error: ${err.message}` }); } throw new APIError5("BAD_REQUEST", { message: `Webhook Error: ${err}` }); } try { if (onPayload) { onPayload(event); } switch (event.type) { case "checkout.created": if (onCheckoutCreated) { onCheckoutCreated(event); } break; case "checkout.updated": if (onCheckoutUpdated) { onCheckoutUpdated(event); } break; case "order.created": if (onOrderCreated) { onOrderCreated(event); } break; case "order.paid": if (onOrderPaid) { onOrderPaid(event); } break; case "subscription.created": if (onSubscriptionCreated) { onSubscriptionCreated(event); } break; case "subscription.updated": if (onSubscriptionUpdated) { onSubscriptionUpdated(event); } break; case "subscription.active": if (onSubscriptionActive) { onSubscriptionActive(event); } break; case "subscription.canceled": if (onSubscriptionCanceled) { onSubscriptionCanceled(event); } break; case "subscription.uncanceled": if (onSubscriptionUncanceled) { onSubscriptionUncanceled(event); } break; case "subscription.revoked": if (onSubscriptionRevoked) { onSubscriptionRevoked(event); } break; case "product.created": if (onProductCreated) { onProductCreated(event); } break; case "product.updated": if (onProductUpdated) { onProductUpdated(event); } break; case "organization.updated": if (onOrganizationUpdated) { onOrganizationUpdated(event); } break; case "benefit.created": if (onBenefitCreated) { onBenefitCreated(event); } break; case "benefit.updated": if (onBenefitUpdated) { onBenefitUpdated(event); } break; case "benefit_grant.created": if (onBenefitGrantCreated) { onBenefitGrantCreated(event); } break; case "benefit_grant.updated": if (onBenefitGrantUpdated) { onBenefitGrantUpdated(event); } break; case "benefit_grant.revoked": if (onBenefitGrantRevoked) { onBenefitGrantRevoked(event); } break; case "order.refunded": if (onOrderRefunded) { onOrderRefunded(event); } break; case "refund.created": if (onRefundCreated) { onRefundCreated(event); } break; case "refund.updated": if (onRefundUpdated) { onRefundUpdated(event); } break; case "customer.created": if (onCustomerCreated) { onCustomerCreated(event); } break; case "customer.updated": if (onCustomerUpdated) { onCustomerUpdated(event); } break; case "customer.deleted": if (onCustomerDeleted) { onCustomerDeleted(event); } break; case "customer.state_changed": if (onCustomerStateChanged) { onCustomerStateChanged(event); } break; } } catch (e) { if (e instanceof Error) { ctx.context.logger.error( `Polar webhook failed. Error: ${e.message}` ); } else { ctx.context.logger.error(`Polar webhook failed. Error: ${e}`); } throw new APIError5("BAD_REQUEST", { message: "Webhook error: See server logs for more information." }); } return ctx.json({ received: true }); } ) }; }; // src/index.ts var polar = (options) => { const plugins = options.use.map((use) => use(options.client)).reduce((acc, plugin) => { Object.assign(acc, plugin); return acc; }, {}); return { id: "polar", endpoints: { ...plugins }, init() { return { options: { databaseHooks: { user: { create: { after: onUserCreate(options) }, update: { after: onUserUpdate(options) } } } } }; } }; }; export { checkout, polar, polarClient, portal, usage, webhooks }; //# sourceMappingURL=index.js.map