@ley0x/better-auth-lastfm
Version:
Last.fm authentication plugin for BetterAuth
192 lines (187 loc) • 7.2 kB
JavaScript
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
createLastfmApiSignature: () => createLastfmApiSignature,
lastfmPlugin: () => lastfmPlugin
});
module.exports = __toCommonJS(index_exports);
// src/server.ts
var import_api = require("better-auth/api");
var import_zod = require("zod");
// src/utils/api-signature.ts
var import_crypto = require("crypto");
function createLastfmApiSignature(params, secret) {
const sortedParams = Object.keys(params).sort().map((key) => `${key}${params[key]}`).join("");
return (0, import_crypto.createHash)("md5").update(sortedParams + secret).digest("hex");
}
// src/server.ts
var lastfmResponseSchema = import_zod.z.object({
session: import_zod.z.object({
key: import_zod.z.string(),
name: import_zod.z.string(),
subscriber: import_zod.z.number()
})
});
var userSchema = import_zod.z.object({
id: import_zod.z.string(),
name: import_zod.z.string(),
email: import_zod.z.string(),
emailVerified: import_zod.z.boolean(),
image: import_zod.z.string().nullish(),
createdAt: import_zod.z.date(),
updatedAt: import_zod.z.date()
});
var accountSchema = import_zod.z.object({
id: import_zod.z.string(),
accountId: import_zod.z.string(),
providerId: import_zod.z.string(),
userId: import_zod.z.string(),
accessToken: import_zod.z.string().nullish(),
refreshToken: import_zod.z.string().nullish(),
idToken: import_zod.z.string().nullish(),
accessTokenExpiresAt: import_zod.z.date().nullish(),
refreshTokenExpiresAt: import_zod.z.date().nullish(),
scope: import_zod.z.string().nullish(),
password: import_zod.z.string().nullish(),
createdAt: import_zod.z.date(),
updatedAt: import_zod.z.date()
});
function lastfmPlugin(options) {
const {
apiKey,
sharedSecret,
baseUrl = process.env.BETTER_AUTH_URL || "http://localhost:3000",
redirectTo = "/dashboard"
} = options;
if (!apiKey || !sharedSecret) {
throw new Error("Last.fm plugin requires both apiKey and sharedSecret");
}
return {
id: "lastfm",
endpoints: {
"/lastfm/signin": (0, import_api.createAuthEndpoint)("/lastfm/signin", { method: "GET" }, async (ctx) => {
const callbackUrl = `${baseUrl}/api/auth/lastfm/callback`;
const authUrl = `https://www.last.fm/api/auth/?api_key=${apiKey}&cb=${encodeURIComponent(callbackUrl)}`;
return ctx.redirect(authUrl);
}),
"/lastfm/callback": (0, import_api.createAuthEndpoint)("/lastfm/callback", { method: "GET" }, async (ctx) => {
const { token } = ctx.query;
if (!token) {
ctx.context.logger?.error("Last.fm callback: Missing token parameter");
return ctx.json({ error: "Authentication failed: Missing token" }, { status: 400 });
}
try {
const sessionData = await exchangeTokenForSession(token, apiKey, sharedSecret);
const { username, sessionKey } = sessionData;
const existingAccount = await ctx.context.adapter.findOne({
model: "account",
where: [
{ field: "providerId", value: "lastfm" },
{ field: "accountId", value: username }
]
});
let user;
if (existingAccount) {
const validatedAccount = accountSchema.parse(existingAccount);
await ctx.context.adapter.update({
model: "account",
where: [{ field: "id", value: validatedAccount.id }],
update: {
accessToken: sessionKey,
updatedAt: /* @__PURE__ */ new Date()
}
});
const existingUser = await ctx.context.adapter.findOne({
model: "user",
where: [{ field: "id", value: validatedAccount.userId }]
});
if (!existingUser) {
return ctx.json({ error: "User not found" }, { status: 404 });
}
user = userSchema.parse(existingUser);
} else {
const newUser = await ctx.context.adapter.create({
model: "user",
data: {
name: username,
email: `${username}@lastfm.local`,
emailVerified: true,
image: null
}
});
user = userSchema.parse(newUser);
await ctx.context.adapter.create({
model: "account",
data: {
accountId: username,
providerId: "lastfm",
userId: user.id,
accessToken: sessionKey,
createdAt: /* @__PURE__ */ new Date(),
updatedAt: /* @__PURE__ */ new Date()
}
});
}
const session = await ctx.context.internalAdapter.createSession(user.id);
const cookieName = ctx.context.authCookies.sessionToken.name;
const cookieOptions = ctx.context.authCookies.sessionToken.attributes;
await ctx.setSignedCookie(cookieName, session.token, ctx.context.secret, {
...cookieOptions,
maxAge: cookieOptions.maxAge || void 0
});
return ctx.redirect(redirectTo);
} catch (error) {
ctx.context.logger?.error("Last.fm authentication error:", error);
return ctx.json({ error: "Authentication failed" }, { status: 500 });
}
})
}
};
}
async function exchangeTokenForSession(token, apiKey, sharedSecret) {
const params = {
api_key: apiKey,
method: "auth.getSession",
token
};
const apiSignature = createLastfmApiSignature(params, sharedSecret);
const url = new URL("https://ws.audioscrobbler.com/2.0/");
url.searchParams.set("method", "auth.getSession");
url.searchParams.set("api_key", apiKey);
url.searchParams.set("token", token);
url.searchParams.set("api_sig", apiSignature);
url.searchParams.set("format", "json");
const response = await fetch(url.toString());
if (!response.ok) {
throw new Error(`Last.fm API error: ${response.status} ${response.statusText}`);
}
const data = await response.json();
const validatedData = lastfmResponseSchema.parse(data);
return {
username: validatedData.session.name,
sessionKey: validatedData.session.key
};
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
createLastfmApiSignature,
lastfmPlugin
});