UNPKG

@polar-sh/better-auth

Version:

Polar integration for better-auth

833 lines (826 loc) 26.3 kB
import { polarClient } from "./chunk-XH6OI6P2.js"; // src/hooks/customer.ts import { APIError } from "better-auth/api"; var onBeforeUserCreate = (options) => async (user, context) => { if (context && options.createCustomerOnSignUp) { try { if (user.isAnonymous) { return; } const params = options.getCustomerCreateParams ? await options.getCustomerCreateParams({ user }) : {}; if (!user.email) { throw new APIError("BAD_REQUEST", { message: "An associated email is required" }); } const { result: existingCustomers } = await options.client.customers.list({ email: user.email }); const existingCustomer = existingCustomers.items[0]; if (!existingCustomer) { await options.client.customers.create({ ...params, email: user.email, name: user.name }); } } 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 onAfterUserCreate = (options) => async (user, context) => { if (context && options.createCustomerOnSignUp) { if (user.isAnonymous) { return; } try { 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 } }); } } } 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, context) => { if (context && options.createCustomerOnSignUp) { try { if (user.isAnonymous) { return; } await options.client.customers.updateExternal({ externalId: user.id, customerUpdateExternalID: { email: user.email, name: user.name } }); } catch (e) { if (e instanceof Error) { context.context.logger.error( `Polar customer update failed. Error: ${e.message}` ); } else { context.context.logger.error( `Polar customer update failed. Error: ${e}` ); } } } }; var onUserDelete = (options) => async (user, context) => { if (context && options.createCustomerOnSignUp) { try { if (user.isAnonymous) { return; } if (user.email) { const { result: existingCustomers } = await options.client.customers.list({ email: user.email }); const existingCustomer = existingCustomers.items[0]; if (existingCustomer) { await options.client.customers.delete({ id: existingCustomer.id }); } } } catch (e) { if (e instanceof Error) { context?.context.logger.error( `Polar customer delete failed. Error: ${e.message}` ); return; } context?.context.logger.error( `Polar customer delete failed. Error: ${e}` ); } } }; // src/server.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: { before: onBeforeUserCreate(options), after: onAfterUserCreate(options) }, update: { after: onUserUpdate(options) }, delete: { after: onUserDelete(options) } } } } }; } }; }; // src/plugins/portal.ts import { APIError as APIError2 } from "better-auth/api"; import { createAuthEndpoint, sessionMiddleware } from "better-auth/api"; import * as z from "zod/v4"; var portal = ({ returnUrl, theme } = {}) => (polar2) => { const retUrl = returnUrl ? new URL(returnUrl) : void 0; return { portal: createAuthEndpoint( "/customer/portal", { method: ["GET", "POST"], body: z.object({ redirect: z.boolean().optional() }).optional(), use: [sessionMiddleware] }, async (ctx) => { if (!ctx.context.session?.user.id) { throw new APIError2("BAD_REQUEST", { message: "User not found" }); } if (ctx.context.session?.user["isAnonymous"]) { throw new APIError2("UNAUTHORIZED", { message: "Anonymous users cannot access the portal" }); } try { const customerSession = await polar2.customerSessions.create({ externalCustomerId: ctx.context.session?.user.id, returnUrl: retUrl ? decodeURI(retUrl.toString()) : void 0 }); const portalUrl = new URL(customerSession.customerPortalUrl); if (theme) { portalUrl.searchParams.set("theme", theme); } return ctx.json({ url: portalUrl.toString(), redirect: ctx.body?.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({ externalCustomerId: 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({ externalCustomerId: 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({ externalCustomerId: 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, createAuthEndpoint as createAuthEndpoint2, getSessionFromCtx } from "better-auth/api"; import * as z2 from "zod/v4"; var CheckoutParams = 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().max(500), z2.number(), z2.boolean()])).refine((obj) => Object.keys(obj).length <= 50, { message: "Metadata can have at most 50 key-value pairs" }).refine((obj) => Object.keys(obj).every((key) => key.length <= 40), { message: "Metadata keys must be at most 40 characters" }).optional(), allowDiscountCodes: z2.coerce.boolean().optional(), discountId: z2.string().optional(), redirect: z2.coerce.boolean().optional(), embedOrigin: z2.url().optional(), successUrl: z2.string().refine((val) => val.startsWith("/") || URL.canParse(val), { message: "Must be a valid URL or a relative path starting with /" }).optional(), returnUrl: z2.string().refine((val) => val.startsWith("/") || URL.canParse(val), { message: "Must be a valid URL or a relative path starting with /" }).optional(), allowTrial: z2.boolean().optional(), trialInterval: z2.enum(["day", "week", "month", "year"]).optional(), trialIntervalCount: z2.number().int().min(1).max(1e3).optional() }); var checkout = (checkoutOptions = {}) => (polar2) => { return { checkout: createAuthEndpoint2( "/checkout", { method: "POST", body: CheckoutParams }, 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) { if (!session?.user.id) { throw new APIError3("UNAUTHORIZED", { message: "You must be logged in to checkout" }); } if (session.user["isAnonymous"]) { throw new APIError3("UNAUTHORIZED", { message: "Anonymous users are not allowed to checkout" }); } } const successUrl = ctx.body.successUrl ?? checkoutOptions.successUrl; const returnUrl = ctx.body.returnUrl ?? checkoutOptions.returnUrl; try { const checkout2 = await polar2.checkouts.create({ externalCustomerId: session?.user.id, products: productIds, successUrl: successUrl ? new URL( successUrl, ctx.request?.url ?? ctx.context.baseURL ).toString() : void 0, metadata: ctx.body.referenceId ? { referenceId: ctx.body.referenceId, ...ctx.body.metadata } : ctx.body.metadata, customFieldData: ctx.body.customFieldData, allowDiscountCodes: ctx.body.allowDiscountCodes ?? true, discountId: ctx.body.discountId, embedOrigin: ctx.body.embedOrigin, allowTrial: ctx.body.allowTrial, trialInterval: ctx.body.trialInterval, trialIntervalCount: ctx.body.trialIntervalCount, returnUrl: returnUrl ? new URL( returnUrl, ctx.request?.url ?? ctx.context.baseURL ).toString() : void 0 }); const redirectUrl = new URL(checkout2.url); if (checkoutOptions.theme) { redirectUrl.searchParams.set("theme", checkoutOptions.theme); } return ctx.json({ url: redirectUrl.toString(), redirect: ctx.body.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 * as z3 from "zod/v4"; 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({ externalCustomerId: 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" }); } } ) }; }; // ../adapter-utils/dist/index.js var handleWebhookPayload = async (payload, { webhookSecret, entitlements, onPayload, ...eventHandlers }) => { const promises = []; if (onPayload) { promises.push(onPayload(payload)); } switch (payload.type) { case "checkout.created": if (eventHandlers.onCheckoutCreated) { promises.push(eventHandlers.onCheckoutCreated(payload)); } break; case "checkout.updated": if (eventHandlers.onCheckoutUpdated) { promises.push(eventHandlers.onCheckoutUpdated(payload)); } break; case "order.created": if (eventHandlers.onOrderCreated) { promises.push(eventHandlers.onOrderCreated(payload)); } break; case "order.updated": if (eventHandlers.onOrderUpdated) { promises.push(eventHandlers.onOrderUpdated(payload)); } break; case "order.paid": if (eventHandlers.onOrderPaid) { promises.push(eventHandlers.onOrderPaid(payload)); } break; case "subscription.created": if (eventHandlers.onSubscriptionCreated) { promises.push(eventHandlers.onSubscriptionCreated(payload)); } break; case "subscription.updated": if (eventHandlers.onSubscriptionUpdated) { promises.push(eventHandlers.onSubscriptionUpdated(payload)); } break; case "subscription.active": if (eventHandlers.onSubscriptionActive) { promises.push(eventHandlers.onSubscriptionActive(payload)); } break; case "subscription.canceled": if (eventHandlers.onSubscriptionCanceled) { promises.push(eventHandlers.onSubscriptionCanceled(payload)); } break; case "subscription.uncanceled": if (eventHandlers.onSubscriptionUncanceled) { promises.push(eventHandlers.onSubscriptionUncanceled(payload)); } break; case "subscription.revoked": if (eventHandlers.onSubscriptionRevoked) { promises.push(eventHandlers.onSubscriptionRevoked(payload)); } break; case "product.created": if (eventHandlers.onProductCreated) { promises.push(eventHandlers.onProductCreated(payload)); } break; case "product.updated": if (eventHandlers.onProductUpdated) { promises.push(eventHandlers.onProductUpdated(payload)); } break; case "organization.updated": if (eventHandlers.onOrganizationUpdated) { promises.push(eventHandlers.onOrganizationUpdated(payload)); } break; case "benefit.created": if (eventHandlers.onBenefitCreated) { promises.push(eventHandlers.onBenefitCreated(payload)); } break; case "benefit.updated": if (eventHandlers.onBenefitUpdated) { promises.push(eventHandlers.onBenefitUpdated(payload)); } break; case "benefit_grant.created": if (eventHandlers.onBenefitGrantCreated) { promises.push(eventHandlers.onBenefitGrantCreated(payload)); } break; case "benefit_grant.updated": if (eventHandlers.onBenefitGrantUpdated) { promises.push(eventHandlers.onBenefitGrantUpdated(payload)); } break; case "benefit_grant.revoked": if (eventHandlers.onBenefitGrantRevoked) { promises.push(eventHandlers.onBenefitGrantRevoked(payload)); } break; case "customer.created": if (eventHandlers.onCustomerCreated) { promises.push(eventHandlers.onCustomerCreated(payload)); } break; case "customer.updated": if (eventHandlers.onCustomerUpdated) { promises.push(eventHandlers.onCustomerUpdated(payload)); } break; case "customer.deleted": if (eventHandlers.onCustomerDeleted) { promises.push(eventHandlers.onCustomerDeleted(payload)); } break; case "customer.state_changed": if (eventHandlers.onCustomerStateChanged) { promises.push(eventHandlers.onCustomerStateChanged(payload)); } break; case "order.refunded": if (eventHandlers.onOrderRefunded) { promises.push(eventHandlers.onOrderRefunded(payload)); } break; case "refund.created": if (eventHandlers.onRefundCreated) { promises.push(eventHandlers.onRefundCreated(payload)); } break; case "refund.updated": if (eventHandlers.onRefundUpdated) { promises.push(eventHandlers.onRefundUpdated(payload)); } break; } switch (payload.type) { case "benefit_grant.created": case "benefit_grant.revoked": if (entitlements) { for (const handler of entitlements.handlers) { promises.push(handler(payload)); } } } return Promise.all(promises); }; // src/plugins/webhooks.ts import { validateEvent } from "@polar-sh/sdk/webhooks"; 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, ...eventHandlers } = 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 { await handleWebhookPayload(event, { webhookSecret: secret, ...eventHandlers }); } 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 }); } ) }; }; export { CheckoutParams, checkout, polar, polarClient, portal, usage, webhooks }; //# sourceMappingURL=index.js.map