tauth-sdk/dist/index.js
2026-04-14 00:58:38 +02:00

161 lines
7 KiB
JavaScript

import { createTransportClient, READY_STATE, } from "@tensamin/ttp-core";
import { createSchema } from "./schema";
import { x448 } from "@noble/curves/ed448.js";
import Bun from "bun";
import { get } from "./user";
import { decrypt, encrypt, getSharedSecret } from "./crypto";
import { readFile } from "node:fs/promises";
/**
* @param redirectUrl The URL should point to the TAuth HTTP Server at http://hostname:port/callback
*/
export class TAuthClient {
frontendUrl;
identifier;
privateKey;
publicKey;
redirectUrl;
schema;
httpServer;
clientMap = new Map();
constructor({ frontendUrl, identifier, privateKey, publicKey, redirectUrl, appData, httpServer, htmlSuccessPage, }) {
this.frontendUrl = frontendUrl;
this.identifier = identifier;
this.privateKey = privateKey;
this.publicKey = publicKey;
this.redirectUrl = new URL(redirectUrl);
this.schema = createSchema(appData);
this.httpServer = httpServer ?? {
port: 7878,
hostname: "localhost",
};
Bun.serve({
hostname: this.httpServer.hostname,
port: this.httpServer.port,
fetch: async (request) => {
const url = new URL(request.url);
switch (url.pathname) {
case "/callback": {
const userId = Number(url.searchParams.get("userId"));
if (!userId) {
return new Response("Missing userId", { status: 400 });
}
const potentialChallenge = url.searchParams.get("challenge");
if (!potentialChallenge) {
const challenge = await this.generateChallenge(userId);
const newLink = this.generateLink(challenge);
return new Response(null, {
status: 302,
headers: {
Location: newLink,
},
});
}
const originalChallenge = url.searchParams.get("originalChallenge");
if (!originalChallenge) {
return new Response("Missing originalChallenge", { status: 400 });
}
const potentialSessionId = Number(url.searchParams.get("sessionId"));
if (!potentialSessionId || isNaN(potentialSessionId)) {
return new Response("Invalid or missing sessionId", {
status: 400,
});
}
if ((await this.solveChallenge(userId, originalChallenge)) ===
potentialChallenge) {
const { ip_address } = await fetch("https://omega.tensamin.net/api/get/omikron/" + userId).then((res) => res.json());
this.createTTP(userId, potentialSessionId, ip_address);
return new Response(htmlSuccessPage || (await readFile("./index.html")), {
headers: {
"Content-Type": "text/html",
},
status: 200,
});
}
else {
return new Response("Failed to solve challenge", { status: 400 });
}
}
case "/auth": {
return new Response(null, {
status: 302,
headers: {
Location: this.generateLink(),
},
});
}
default: {
return new Response("Not Found", { status: 404 });
}
}
},
});
console.log(`[tauth] HTTP Server running at http://${this.httpServer.hostname}:${this.httpServer.port}`);
}
async generateChallenge(userId) {
const userData = await get(userId);
if (!userData) {
throw new Error("User not found");
}
const sharedSecret = await getSharedSecret(this.privateKey, this.publicKey, userData.public_key);
const encrypted = await encrypt(sharedSecret, crypto.randomUUID());
const decrypted = await decrypt(sharedSecret, encrypted);
console.log(`[tauth] Generated challenge for user ${userId}: ${decrypted} (encrypted: ${encrypted})`);
return Array.from(atob(encrypted))
.map((chat) => chat.charCodeAt(0).toString(16).padStart(2, "0"))
.join("");
}
async solveChallenge(userId, challenge) {
const userData = await get(userId);
if (!userData) {
throw new Error("User not found");
}
const sharedSecret = await getSharedSecret(this.privateKey, this.publicKey, userData.public_key);
return await decrypt(sharedSecret, challenge);
}
createTTP(userId, sessionId, omikronUrl) {
console.log(`[tauth] Creating TTP client for user ${userId} with session ${sessionId} at ${omikronUrl}`);
this.clientMap.set(`${userId}:${sessionId}`, createTransportClient(this.schema, {
url: omikronUrl,
onReadyStateChange: (stateIndex) => {
const state = getFriendlyReadyState(stateIndex);
console.log(state);
if (state === "OPEN") {
this.clientMap
.get(`${userId}:${sessionId}`)
?.send("identification", {
app_identifier: this.identifier,
app_session_id: sessionId,
app_public_key: this.publicKey,
user_id: userId,
});
}
},
}));
this.clientMap.get(`${userId}:${sessionId}`)?.connect();
}
generateLink(challenge) {
return new URL(`?identifier=${this.identifier}&redirect=${this.redirectUrl}${challenge ? `&challenge=${challenge}` : ""}`, this.frontendUrl).toString();
}
}
export function getFriendlyReadyState(stateIndex) {
return Object.keys(READY_STATE).find((key) => READY_STATE[key] === stateIndex);
}
export function generateKeyPair() {
function toBase64(bytes) {
if (typeof Buffer !== "undefined") {
return Buffer.from(bytes).toString("base64");
}
let binary = "";
for (const byte of bytes) {
binary += String.fromCharCode(byte);
}
return btoa(binary);
}
const priv = new Uint8Array(56);
crypto.getRandomValues(priv);
const pub = x448.getPublicKey(priv);
return {
private: toBase64(priv),
public: toBase64(pub),
};
}