@polar-sh/better-auth
Version:
Polar integration for better-auth
833 lines (826 loc) • 26.3 kB
JavaScript
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