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