UNPKG

@tribute-tg/better-auth

Version:

Tribute integration for better-auth

293 lines (286 loc) 9.21 kB
import { mergeSchema } from "better-auth/db"; import { z } from "zod"; import { APIError, createAuthEndpoint, sessionMiddleware } from "better-auth/api"; import { createAuthEndpoint as createAuthEndpoint$1 } from "better-auth/plugins"; //#region src/schema.ts const subscriptions = { subscription: { fields: { tributeSubscriptionId: { type: "number", required: true }, tributeSubscriptionName: { type: "string", required: true }, tributeUserId: { type: "number", required: true }, telegramUserId: { type: "number", required: true }, channelId: { type: "number", required: true }, period: { type: "string", required: true }, price: { type: "number", required: true }, amount: { type: "number", required: true }, currency: { type: "string", required: true }, expiresAt: { type: "string", required: true }, status: { type: "string", defaultValue: "active" } } } }; const user = { user: { fields: { tributeUserId: { type: "number", required: false } } } }; const getSchema = (options) => { return mergeSchema({ ...subscriptions, ...user }, options.schema); }; //#endregion //#region src/util.ts const verifyHmac = async (key, data, expectedHmac) => { const encoder = new TextEncoder(); const keyData = encoder.encode(key); const dataToVerify = encoder.encode(data); const cryptoKey = await crypto.subtle.importKey("raw", keyData, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]); const signature = await crypto.subtle.sign("HMAC", cryptoKey, dataToVerify); return Array.from(new Uint8Array(signature)).map((b) => b.toString(16).padStart(2, "0")).join("") === expectedHmac; }; //#endregion //#region src/webhooks.ts const webhooks = (webhooksOptions) => (tribute$1) => { return { tributeWebhooks: createAuthEndpoint("/tribute/webhooks", { method: "POST", body: z.object({ name: z.string(), created_at: z.string(), sent_at: z.string(), payload: z.any() }) }, async (ctx) => { const signature = ctx.headers?.get("Trbt-Signature"); if (!signature) return ctx.error(401); if (!await verifyHmac(tribute$1.apiKey, JSON.stringify(ctx.body), signature)) return ctx.error(401); const { onNewSubscription, onCancelledSubscription, onEvent } = webhooksOptions; const event = ctx.body; const eventName = event.name; const payload = event.payload; if (eventName === "new_subscription") { await onNewSubscription?.(event.payload); const telegramUserId = payload.telegram_user_id; const account = await ctx.context.adapter.findOne({ model: "account", where: [{ field: "accountId", value: String(telegramUserId) }, { field: "providerId", value: "telegram" }] }); if (account) { const userId = account.userId; const tributeUserId = payload.user_id; await ctx.context.adapter.update({ model: "user", update: { tributeUserId }, where: [{ field: "id", value: userId }] }); await ctx.context.adapter.create({ model: "subscription", data: { userId, telegramUserId, tributeUserId: payload.user_id, tributeSubscriptionId: payload.subscription_id, tributeSubscriptionName: payload.subscription_name, channelId: payload.channel_id, period: payload.period, price: payload.price / 100, amount: payload.amount / 100, currency: payload.currency, expiresAt: payload.expires_at, status: "active" } }); } else console.log("Account not found"); } else if (eventName === "cancelled_subscription") { await onCancelledSubscription?.(payload); await ctx.context.adapter.update({ model: "subscription", update: { status: "cancelled" }, where: [{ field: "tributeSubscriptionId", value: payload.subscription_id }, { field: "tributeUserId", value: payload.user_id }] }); } await onEvent?.(event); }) }; }; //#endregion //#region src/subscription.ts const cachedSubscriptions = []; const subscription = (options) => (tribute$1) => { return { upgradeSubscription: createAuthEndpoint$1("/subscription/upgrade", { method: "POST", body: z.object({ subscriptionId: z.number().optional(), period: z.string().optional(), currency: z.string().optional(), slug: z.string().optional() }) }, async (ctx) => { if (!ctx.body.slug && !ctx.body.subscriptionId) throw new APIError("BAD_REQUEST", { message: "Either slug or subscriptionId is required" }); const findFrom = (subscriptions$1) => subscriptions$1.find((subscription$2) => (!ctx.body.subscriptionId || subscription$2.subscriptionId === ctx.body.subscriptionId) && (!ctx.body.slug || subscription$2.slug === ctx.body.slug) && (!ctx.body.period || subscription$2.period === ctx.body.period) && (!ctx.body.currency || subscription$2.currency === ctx.body.currency)); let subscription$1 = findFrom(cachedSubscriptions); if (!subscription$1) { const resolvedSubscription = findFrom(await (typeof options.subscriptions === "function" ? options.subscriptions() : options.subscriptions) ?? []); if (resolvedSubscription) subscription$1 = resolvedSubscription; } if (!subscription$1) throw new APIError("BAD_REQUEST", { message: "Subscription not found" }); try { const cachedLink = subscription$1.redirectUrl?.trim(); if (cachedLink) return ctx.json({ url: cachedLink, redirect: true }); ctx.context.logger.info(`Fetching subscription link for ${subscription$1.subscriptionId}`); const url = (await tribute$1.getSubscription(subscription$1.subscriptionId)).subscription.webLink; const index = cachedSubscriptions.findIndex((s) => s.subscriptionId === subscription$1.subscriptionId); if (index === -1) cachedSubscriptions.push({ ...subscription$1, redirectUrl: url }); else cachedSubscriptions[index] = { ...subscription$1, redirectUrl: url }; return ctx.json({ url, redirect: true }); } catch (e) { if (e instanceof Error) ctx.context.logger.error(`Tribute checkout creation failed. Error: ${e.message}`); throw new APIError("INTERNAL_SERVER_ERROR", { message: "Checkout creation failed" }); } }), listActiveSubscriptions: createAuthEndpoint$1("/subscription/list", { method: "GET", use: [sessionMiddleware] }, async (ctx) => { if (!ctx.context.session.user.id || !ctx.context.session.user["tributeUserId"]) throw new APIError("BAD_REQUEST", { message: "User not found" }); try { const activeSubscriptions = (await ctx.context.adapter.findMany({ model: "subscription", where: [{ field: "tributeUserId", value: ctx.context.session.user["tributeUserId"] }] })).filter((s) => { return new Date(s.expiresAt) > /* @__PURE__ */ new Date() || s.status === "active"; }); return ctx.json(activeSubscriptions); } catch (e) { if (e instanceof Error) ctx.context.logger.error(`Tribute subscriptions list failed. Error: ${e.message}`); throw new APIError("INTERNAL_SERVER_ERROR", { message: "Subscriptions list failed" }); } }), portal: createAuthEndpoint$1("/customer/portal", { method: "GET", use: [sessionMiddleware] }, async (ctx) => { if (!ctx.context.session?.user.id) throw new APIError("BAD_REQUEST", { message: "User not found" }); try { return ctx.json({ url: "https://t.me/tribute/app?startapp=", redirect: true }); } catch (e) { if (e instanceof Error) ctx.context.logger.error(`Tribute customer portal creation failed. Error: ${e.message}`); throw new APIError("INTERNAL_SERVER_ERROR", { message: "Customer portal creation failed" }); } }), state: createAuthEndpoint$1("/customer/state", { method: "GET", use: [sessionMiddleware] }, async (ctx) => { if (!ctx.context.session.user.id || !ctx.context.session.user["tributeUserId"]) throw new APIError("BAD_REQUEST", { message: "User not found" }); try { const activeSubscriptions = (await ctx.context.adapter.findMany({ model: "subscription", where: [{ field: "tributeUserId", value: ctx.context.session.user["tributeUserId"] }] })).filter((s) => { return new Date(s.expiresAt) > /* @__PURE__ */ new Date() || s.status === "active"; }); const state = { ...ctx.context.session.user, activeSubscriptions }; return ctx.json(state); } catch (e) { if (e instanceof Error) ctx.context.logger.error(`Tribute customer state failed. Error: ${e.message}`); throw new APIError("INTERNAL_SERVER_ERROR", { message: "Customer state failed" }); } }) }; }; //#endregion //#region src/client.ts const tributeClient = () => { return { id: "tribute-client", $InferServerPlugin: {} }; }; //#endregion //#region src/index.ts const tribute = (options) => { return { id: "tribute", endpoints: { ...[webhooks(options), subscription(options)].map((use) => use(options.tributeClient)).reduce((acc, plugin) => { Object.assign(acc, plugin); return acc; }, {}) }, schema: getSchema(options) }; }; //#endregion export { subscription, tribute, tributeClient, webhooks }; //# sourceMappingURL=index.mjs.map