UNPKG

@tribute-tg/better-auth

Version:

Tribute integration for better-auth

296 lines (289 loc) 9.59 kB
let better_auth_db = require("better-auth/db"); let zod = require("zod"); let better_auth_api = require("better-auth/api"); let better_auth_plugins = require("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 (0, better_auth_db.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: (0, better_auth_api.createAuthEndpoint)("/tribute/webhooks", { method: "POST", body: zod.z.object({ name: zod.z.string(), created_at: zod.z.string(), sent_at: zod.z.string(), payload: zod.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: (0, better_auth_plugins.createAuthEndpoint)("/subscription/upgrade", { method: "POST", body: zod.z.object({ subscriptionId: zod.z.number().optional(), period: zod.z.string().optional(), currency: zod.z.string().optional(), slug: zod.z.string().optional() }) }, async (ctx) => { if (!ctx.body.slug && !ctx.body.subscriptionId) throw new better_auth_api.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 better_auth_api.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 better_auth_api.APIError("INTERNAL_SERVER_ERROR", { message: "Checkout creation failed" }); } }), listActiveSubscriptions: (0, better_auth_plugins.createAuthEndpoint)("/subscription/list", { method: "GET", use: [better_auth_api.sessionMiddleware] }, async (ctx) => { if (!ctx.context.session.user.id || !ctx.context.session.user["tributeUserId"]) throw new better_auth_api.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 better_auth_api.APIError("INTERNAL_SERVER_ERROR", { message: "Subscriptions list failed" }); } }), portal: (0, better_auth_plugins.createAuthEndpoint)("/customer/portal", { method: "GET", use: [better_auth_api.sessionMiddleware] }, async (ctx) => { if (!ctx.context.session?.user.id) throw new better_auth_api.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 better_auth_api.APIError("INTERNAL_SERVER_ERROR", { message: "Customer portal creation failed" }); } }), state: (0, better_auth_plugins.createAuthEndpoint)("/customer/state", { method: "GET", use: [better_auth_api.sessionMiddleware] }, async (ctx) => { if (!ctx.context.session.user.id || !ctx.context.session.user["tributeUserId"]) throw new better_auth_api.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 better_auth_api.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 exports.subscription = subscription; exports.tribute = tribute; exports.tributeClient = tributeClient; exports.webhooks = webhooks; //# sourceMappingURL=index.cjs.map