@withstudiocms/auth-kit
Version:
Utilities for managing authentication
158 lines (157 loc) • 4.91 kB
JavaScript
import { createHash } from "node:crypto";
import { resolveSrv } from "node:dns/promises";
import { stringify } from "node:querystring";
const BASE_URL = "http://cdn.libravatar.org/avatar/";
const SECURE_BASE_URL = "https://seccdn.libravatar.org/avatar/";
const SERVICE_BASE = "_avatars._tcp";
const SECURE_SERVICE_BASE = "_avatars-sec._tcp";
const srvHostname = (records) => {
if (records.length < 1) {
return [null, null];
}
if (records.length === 1) {
return [records[0].name, records[0].port];
}
let priorityRecords = [];
let totalWeight = 0;
let topPriority = records[0].priority;
records.forEach((srvRecord) => {
if (srvRecord.priority <= topPriority) {
if (srvRecord.priority < topPriority) {
topPriority = srvRecord.priority;
totalWeight = 0;
priorityRecords = [];
}
totalWeight += srvRecord.weight;
if (srvRecord.weight > 0) {
priorityRecords.push([totalWeight, srvRecord]);
} else {
priorityRecords.unshift([0, srvRecord]);
}
}
});
if (priorityRecords.length === 1) {
const srvRecord = priorityRecords[0][1];
return [srvRecord.name, srvRecord.port];
}
const randomNumber = Math.floor(Math.random() * (totalWeight + 1));
for (let i = 0; i < priorityRecords.length; i++) {
const weightedIndex = priorityRecords[i][0];
const target = priorityRecords[i][1];
if (weightedIndex >= randomNumber) {
return [target.name, target.port];
}
}
console.log("There is something wrong with our SRV weight ordering algorithm");
return [null, null];
};
const sanitizedTarget = (targetComponents, https) => {
const target = targetComponents[0];
const port = targetComponents[1];
if (target === null || port === null || Number.isNaN(port)) {
return null;
}
if (port < 1 || port > 65535) {
return null;
}
if (target.search(/^[0-9a-zA-Z\-.]+$/) === -1) {
return null;
}
if (target && (https && port !== 443 || !https && port !== 80)) {
return `${target}:${port}`;
}
return target;
};
const parseUserIdentity = (email, openid) => {
let hash = null;
let domain = null;
if (email != null) {
const lowercaseValue = email.trim().toLowerCase();
const emailParts = lowercaseValue.split("@");
if (emailParts.length > 1) {
domain = emailParts[emailParts.length - 1];
hash = createHash("md5").update(lowercaseValue).digest("hex");
}
} else if (openid != null) {
try {
const parsedUrl = new URL(openid);
let normalizedUrl = parsedUrl.protocol.toLowerCase();
normalizedUrl += "//";
if (parsedUrl.username || parsedUrl.password) {
const auth = parsedUrl.username + (parsedUrl.password ? `:${parsedUrl.password}` : "");
normalizedUrl += `${auth}@`;
}
normalizedUrl += parsedUrl.hostname.toLowerCase() + parsedUrl.pathname;
domain = parsedUrl.hostname.toLowerCase();
hash = createHash("sha256").update(normalizedUrl).digest("hex");
} catch (_error) {
return { hash: null, domain: null };
}
}
return { hash, domain };
};
const serviceName = (domain, https) => {
if (domain) {
return `${https ? SECURE_SERVICE_BASE : SERVICE_BASE}.${domain}`;
}
return null;
};
const composeAvatarUrl = (delegationServer, avatarHash, queryString, https) => {
let baseUrl = https ? SECURE_BASE_URL : BASE_URL;
if (delegationServer) {
baseUrl = `http${https ? "s" : ""}://${delegationServer}/avatar/`;
}
return baseUrl + avatarHash + queryString;
};
const getDelegationServer = async (domain, https) => {
if (!domain) {
return null;
}
const service = serviceName(domain, https);
if (!service) {
return null;
}
try {
const addresses = await resolveSrv(service);
return sanitizedTarget(srvHostname(addresses), https);
} catch (_error) {
return null;
}
};
const getAvatarUrl = async (options) => {
const identity = parseUserIdentity(options.email, options.openid);
const hash = identity.hash;
const domain = identity.domain;
const https = options.https || false;
if (!hash) {
throw new Error("An email or an OpenID must be provided.");
}
const queryOptions = { ...options };
delete queryOptions.email;
delete queryOptions.openid;
delete queryOptions.https;
const queryData = stringify(queryOptions);
const query = queryData ? `?${queryData}` : "";
const delegationServer = await getDelegationServer(domain, https);
return composeAvatarUrl(delegationServer, hash, query, https);
};
var libravatar_default = {
getAvatarUrl,
// Export utility functions for testing
sanitizedTarget,
srvHostname,
parseUserIdentity,
serviceName,
composeAvatarUrl,
getDelegationServer
};
export {
composeAvatarUrl,
libravatar_default as default,
getAvatarUrl,
getDelegationServer,
parseUserIdentity,
sanitizedTarget,
serviceName,
srvHostname
};