mirror of
https://github.com/AeThex-Corporation/AeThex-OS.git
synced 2026-04-17 22:27:19 +00:00
Major Features: - Custom .aethex programming language with cross-platform compilation - Compiles to JavaScript, Lua (Roblox), Verse (UEFN), and C# (Unity) - Built-in COPPA compliance and PII detection for safe metaverse development Integration Points: 1. Terminal Integration - Added 'aethex' command for in-terminal compilation - Support for all compilation targets with --target flag - Real-time error reporting and syntax highlighting 2. IDE Integration - Native .aethex file support in Monaco editor - One-click compilation with target selector - Download compiled code functionality - Two example files: hello.aethex and auth.aethex 3. Curriculum Integration - New "AeThex Language" section in Foundry tech tree - Three modules: Realities & Journeys, Cross-Platform Sync, COPPA Compliance - Certification path for students 4. Documentation Site - Complete docs at /docs route (client/src/pages/aethex-docs.tsx) - Searchable documentation with sidebar navigation - Language guide, standard library reference, and examples - Ready for deployment to aethex.dev 5. npm Package Publishing - @aethex.os/core@1.0.0 - Standard library (published) - @aethex.os/cli@1.0.1 - Command line compiler (published) - Both packages live on npm and globally installable Domain Configuration: - DNS setup for 29+ domains (aethex.app, aethex.co, etc.) - nginx reverse proxy configuration - CORS configuration for cross-domain requests - OAuth redirect fixes for hash-based routing Standard Library Features: - Passport: Universal identity across platforms - DataSync: Cross-platform data synchronization - SafeInput: PII detection (phone, email, SSN, credit cards) - Compliance: COPPA/FERPA age gates and audit logging Documentation Package: - Created aethex-dev-docs.zip with complete documentation - Ready for static site deployment - Includes examples, API reference, and quickstart guide Technical Improvements: - Fixed OAuth blank page issue (hash routing) - Added .gitignore rules for temp files - Cleaned up build artifacts and temporary files - Updated all package references to @aethex.os namespace Co-Authored-By: Claude <noreply@anthropic.com>
409 lines
11 KiB
TypeScript
409 lines
11 KiB
TypeScript
import type { Request, Response } from "express";
|
|
import { supabase } from "./supabase";
|
|
|
|
// Extend Express Request type to include user
|
|
declare global {
|
|
namespace Express {
|
|
interface Request {
|
|
user?: {
|
|
id: string;
|
|
email?: string;
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
// OAuth State Management (in-memory for now, use Redis in production)
|
|
const oauthStates = new Map<string, {
|
|
userId: string;
|
|
provider: string;
|
|
codeVerifier?: string;
|
|
createdAt: number;
|
|
}>();
|
|
|
|
// Clean up expired states (5 min TTL)
|
|
setInterval(() => {
|
|
const now = Date.now();
|
|
const keysToDelete: string[] = [];
|
|
|
|
oauthStates.forEach((data, state) => {
|
|
if (now - data.createdAt > 300000) {
|
|
keysToDelete.push(state);
|
|
}
|
|
});
|
|
|
|
keysToDelete.forEach(key => oauthStates.delete(key));
|
|
}, 60000);
|
|
|
|
/**
|
|
* Start OAuth linking flow
|
|
* Client calls this to get authorization URL
|
|
*/
|
|
export async function startOAuthLinking(req: Request, res: Response) {
|
|
const { provider } = req.params;
|
|
const userId = req.session?.userId;
|
|
|
|
if (!userId) {
|
|
return res.status(401).json({ error: "Unauthorized" });
|
|
}
|
|
|
|
const validProviders = ["discord", "roblox", "github", "minecraft", "steam", "meta", "twitch", "youtube"];
|
|
if (!validProviders.includes(provider)) {
|
|
return res.status(400).json({ error: "Invalid provider" });
|
|
}
|
|
|
|
// Generate state token
|
|
const state = crypto.randomUUID();
|
|
const codeVerifier = generateCodeVerifier();
|
|
const codeChallenge = await generateCodeChallenge(codeVerifier);
|
|
|
|
// Store state
|
|
oauthStates.set(state, {
|
|
userId,
|
|
provider,
|
|
codeVerifier,
|
|
createdAt: Date.now()
|
|
});
|
|
|
|
// Build authorization URL
|
|
const redirectUri = getRedirectUri(provider);
|
|
const authUrl = buildAuthorizationUrl(provider, state, codeChallenge, redirectUri);
|
|
|
|
res.json({ authUrl, state });
|
|
}
|
|
|
|
/**
|
|
* OAuth callback handler
|
|
* Provider redirects here with authorization code
|
|
*/
|
|
export async function handleOAuthCallback(req: Request, res: Response) {
|
|
const { provider } = req.params;
|
|
const { code, state } = req.query;
|
|
|
|
if (!code || !state || typeof state !== "string") {
|
|
return res.status(400).send("Invalid callback parameters");
|
|
}
|
|
|
|
// Validate state
|
|
const stateData = oauthStates.get(state);
|
|
if (!stateData || stateData.provider !== provider) {
|
|
return res.status(400).send("Invalid or expired state");
|
|
}
|
|
|
|
oauthStates.delete(state);
|
|
|
|
try {
|
|
// Exchange code for token
|
|
const tokenData = await exchangeCodeForToken(
|
|
provider,
|
|
code as string,
|
|
getRedirectUri(provider),
|
|
stateData.codeVerifier
|
|
);
|
|
|
|
// Fetch user identity from provider
|
|
const identity = await fetchProviderIdentity(provider, tokenData.access_token);
|
|
|
|
// Find or create subject
|
|
const { data: existingSubject } = await supabase
|
|
.from("aethex_subjects")
|
|
.select("*")
|
|
.eq("supabase_user_id", stateData.userId)
|
|
.single();
|
|
|
|
let subjectId: string;
|
|
|
|
if (!existingSubject) {
|
|
const { data: newSubject, error: createError } = await supabase
|
|
.from("aethex_subjects")
|
|
.insert({ supabase_user_id: stateData.userId })
|
|
.select()
|
|
.single();
|
|
|
|
if (createError) throw createError;
|
|
subjectId = newSubject.id;
|
|
} else {
|
|
subjectId = existingSubject.id;
|
|
}
|
|
|
|
// Check if identity already exists
|
|
const { data: existingIdentity } = await supabase
|
|
.from("aethex_subject_identities")
|
|
.select("*")
|
|
.eq("issuer", provider)
|
|
.eq("external_id", identity.id)
|
|
.single();
|
|
|
|
if (existingIdentity && existingIdentity.subject_id !== subjectId) {
|
|
return res.status(409).send(
|
|
`This ${provider} account is already linked to another AeThex account.`
|
|
);
|
|
}
|
|
|
|
if (!existingIdentity) {
|
|
// Create new identity link
|
|
await supabase
|
|
.from("aethex_subject_identities")
|
|
.insert({
|
|
subject_id: subjectId,
|
|
issuer: provider,
|
|
external_id: identity.id,
|
|
external_username: identity.username,
|
|
verified: true,
|
|
metadata: identity.metadata
|
|
});
|
|
}
|
|
|
|
// Redirect to success page
|
|
res.redirect(`${getAppBaseUrl()}/#/hub/settings?oauth=success&provider=${provider}`);
|
|
} catch (error) {
|
|
console.error("OAuth callback error:", error);
|
|
res.redirect(`${getAppBaseUrl()}/#/hub/settings?oauth=error&provider=${provider}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Exchange authorization code for access token
|
|
*/
|
|
async function exchangeCodeForToken(
|
|
provider: string,
|
|
code: string,
|
|
redirectUri: string,
|
|
codeVerifier?: string
|
|
): Promise<{ access_token: string; token_type: string }> {
|
|
const config = getProviderConfig(provider);
|
|
|
|
const params = new URLSearchParams({
|
|
grant_type: "authorization_code",
|
|
code,
|
|
redirect_uri: redirectUri,
|
|
client_id: config.clientId,
|
|
client_secret: config.clientSecret,
|
|
});
|
|
|
|
if (codeVerifier && provider === "roblox") {
|
|
params.append("code_verifier", codeVerifier);
|
|
}
|
|
|
|
const response = await fetch(config.tokenUrl, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
"Accept": "application/json"
|
|
},
|
|
body: params
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.text();
|
|
throw new Error(`Token exchange failed: ${error}`);
|
|
}
|
|
|
|
return response.json();
|
|
}
|
|
|
|
/**
|
|
* Fetch user identity from provider
|
|
*/
|
|
async function fetchProviderIdentity(
|
|
provider: string,
|
|
accessToken: string
|
|
): Promise<{ id: string; username: string; metadata: any }> {
|
|
const config = getProviderConfig(provider);
|
|
|
|
const response = await fetch(config.userInfoUrl, {
|
|
headers: {
|
|
"Authorization": `Bearer ${accessToken}`,
|
|
"Accept": "application/json"
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to fetch ${provider} user info`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
// Map provider-specific response to our format
|
|
switch (provider) {
|
|
case "discord":
|
|
return {
|
|
id: data.id,
|
|
username: `${data.username}#${data.discriminator}`,
|
|
metadata: {
|
|
avatar: data.avatar,
|
|
email: data.email,
|
|
verified: data.verified
|
|
}
|
|
};
|
|
|
|
case "roblox":
|
|
return {
|
|
id: data.sub,
|
|
username: data.preferred_username || data.name,
|
|
metadata: {
|
|
profile: data.profile,
|
|
picture: data.picture
|
|
}
|
|
};
|
|
|
|
case "github":
|
|
return {
|
|
id: String(data.id),
|
|
username: data.login,
|
|
metadata: {
|
|
name: data.name,
|
|
email: data.email,
|
|
avatar_url: data.avatar_url,
|
|
html_url: data.html_url
|
|
}
|
|
};
|
|
|
|
default:
|
|
throw new Error(`Unknown provider: ${provider}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Build OAuth authorization URL
|
|
*/
|
|
function buildAuthorizationUrl(
|
|
provider: string,
|
|
state: string,
|
|
codeChallenge: string,
|
|
redirectUri: string
|
|
): string {
|
|
const config = getProviderConfig(provider);
|
|
const params = new URLSearchParams({
|
|
client_id: config.clientId,
|
|
redirect_uri: redirectUri,
|
|
response_type: "code",
|
|
state,
|
|
scope: config.scope
|
|
});
|
|
|
|
if (provider === "roblox") {
|
|
params.append("code_challenge", codeChallenge);
|
|
params.append("code_challenge_method", "S256");
|
|
}
|
|
|
|
return `${config.authUrl}?${params}`;
|
|
}
|
|
|
|
/**
|
|
* Get provider configuration
|
|
*/
|
|
function getProviderConfig(provider: string) {
|
|
const configs = {
|
|
discord: {
|
|
clientId: process.env.DISCORD_CLIENT_ID!,
|
|
clientSecret: process.env.DISCORD_CLIENT_SECRET!,
|
|
authUrl: "https://discord.com/api/oauth2/authorize",
|
|
tokenUrl: "https://discord.com/api/oauth2/token",
|
|
userInfoUrl: "https://discord.com/api/users/@me",
|
|
scope: "identify email"
|
|
},
|
|
roblox: {
|
|
clientId: process.env.ROBLOX_CLIENT_ID!,
|
|
clientSecret: process.env.ROBLOX_CLIENT_SECRET!,
|
|
authUrl: "https://apis.roblox.com/oauth/v1/authorize",
|
|
tokenUrl: "https://apis.roblox.com/oauth/v1/token",
|
|
userInfoUrl: "https://apis.roblox.com/oauth/v1/userinfo",
|
|
scope: "openid profile"
|
|
},
|
|
github: {
|
|
clientId: process.env.GITHUB_CLIENT_ID!,
|
|
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
|
|
authUrl: "https://github.com/login/oauth/authorize",
|
|
tokenUrl: "https://github.com/login/oauth/access_token",
|
|
userInfoUrl: "https://api.github.com/user",
|
|
scope: "read:user user:email"
|
|
},
|
|
minecraft: {
|
|
clientId: process.env.MINECRAFT_CLIENT_ID!,
|
|
clientSecret: process.env.MINECRAFT_CLIENT_SECRET!,
|
|
authUrl: "https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize",
|
|
tokenUrl: "https://login.microsoftonline.com/consumers/oauth2/v2.0/token",
|
|
userInfoUrl: "https://api.minecraftservices.com/minecraft/profile",
|
|
scope: "XboxLive.signin offline_access"
|
|
},
|
|
steam: {
|
|
clientId: process.env.STEAM_API_KEY!,
|
|
clientSecret: process.env.STEAM_API_KEY!,
|
|
authUrl: "https://steamcommunity.com/openid/login",
|
|
tokenUrl: "https://steamcommunity.com/openid/login",
|
|
userInfoUrl: "https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v2",
|
|
scope: ""
|
|
},
|
|
meta: {
|
|
clientId: process.env.META_APP_ID!,
|
|
clientSecret: process.env.META_APP_SECRET!,
|
|
authUrl: "https://www.facebook.com/v18.0/dialog/oauth",
|
|
tokenUrl: "https://graph.instagram.com/v18.0/oauth/access_token",
|
|
userInfoUrl: "https://graph.instagram.com/me?fields=id,name,picture,username",
|
|
scope: "user_profile,user_friends"
|
|
},
|
|
twitch: {
|
|
clientId: process.env.TWITCH_CLIENT_ID!,
|
|
clientSecret: process.env.TWITCH_CLIENT_SECRET!,
|
|
authUrl: "https://id.twitch.tv/oauth2/authorize",
|
|
tokenUrl: "https://id.twitch.tv/oauth2/token",
|
|
userInfoUrl: "https://api.twitch.tv/helix/users",
|
|
scope: "user:read:email channel:read:stream_key"
|
|
},
|
|
youtube: {
|
|
clientId: process.env.YOUTUBE_CLIENT_ID!,
|
|
clientSecret: process.env.YOUTUBE_CLIENT_SECRET!,
|
|
authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
tokenUrl: "https://oauth2.googleapis.com/token",
|
|
userInfoUrl: "https://www.googleapis.com/oauth2/v2/userinfo",
|
|
scope: "https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/youtube"
|
|
}
|
|
};
|
|
|
|
return configs[provider as keyof typeof configs];
|
|
}
|
|
|
|
/**
|
|
* Get OAuth redirect URI
|
|
*/
|
|
function getRedirectUri(provider: string): string {
|
|
const baseUrl = process.env.NODE_ENV === "production"
|
|
? "https://aethex.app"
|
|
: `http://localhost:${process.env.PORT || 5000}`;
|
|
|
|
return `${baseUrl}/api/oauth/callback/${provider}`;
|
|
}
|
|
|
|
/**
|
|
* Get app base URL
|
|
*/
|
|
function getAppBaseUrl(): string {
|
|
return process.env.NODE_ENV === "production"
|
|
? "https://aethex.app"
|
|
: `http://localhost:${process.env.PORT || 5000}`;
|
|
}
|
|
|
|
/**
|
|
* PKCE helpers
|
|
*/
|
|
function generateCodeVerifier(): string {
|
|
const array = new Uint8Array(32);
|
|
crypto.getRandomValues(array);
|
|
return base64UrlEncode(array);
|
|
}
|
|
|
|
async function generateCodeChallenge(verifier: string): Promise<string> {
|
|
const encoder = new TextEncoder();
|
|
const data = encoder.encode(verifier);
|
|
const hash = await crypto.subtle.digest("SHA-256", data);
|
|
return base64UrlEncode(new Uint8Array(hash));
|
|
}
|
|
|
|
function base64UrlEncode(array: Uint8Array): string {
|
|
return Buffer.from(array)
|
|
.toString("base64")
|
|
.replace(/\+/g, "-")
|
|
.replace(/\//g, "_")
|
|
.replace(/=/g, "");
|
|
}
|