(feat): add first release
This commit is contained in:
parent
fc0a1a982d
commit
2ba049f576
19 changed files with 1819 additions and 0 deletions
161
dist/index.js
vendored
Normal file
161
dist/index.js
vendored
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
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),
|
||||
};
|
||||
}
|
||||
Loading…
Reference in a new issue