import { randomUUID } from "node:crypto";
import * as cookie from "https://deno.land/std@0.224.0/http/cookie.ts";
import { RequestAnalytics } from "@infinity-beyond/modules/analytics/request_analytics.ts";
import { ResponseAnalytics } from "@infinity-beyond/modules/analytics/response_analytics.ts";
import { Entity } from "@infinity-beyond/classes/data_types/entity/mod.ts";
import State from "@infinity-beyond/modules/state.ts";
import { CustomEventEmitter } from "@infinity-beyond/classes/custom_event_emitter.ts";
import InfinityHeaders from "@infinity-beyond/modules/networking/infinity_headers.ts";
import { LoginRequest } from "@infinity-beyond/modules/security/draco/login_request.ts";
import { GetDracoUser } from "@infinity-beyond/modules/security/draco/get_draco_user.ts";
import { DefaultPermissions } from "@infinity-beyond/modules/security/default_permissions.ts";
import { InfinityRequest } from "@infinity-beyond/modules/analytics/infinity_request.ts";
import { AnnouncePermissions } from "@infinity-beyond/modules/security/draco/announce_permissions.ts";
import { FetchInfinityClasses } from "@infinity-beyond/modules/delegation/entity_ref.ts";
import { CircleAlert } from "lucide-preact";
const COOKIE_NAME = `${(Deno.env.get('INFINITY_APP')?.toLowerCase().replace(/\s+/g, '-').replace(/_/g, '-') || '∞')}-session`;
export class Infinity extends CustomEventEmitter<InfinityEvents> {
readonly app_name: string
constructor(app_name: string, import_meta_dirname: string) {
super();
this.app_name = app_name;
Infinity.announce(import_meta_dirname);
}
async Middleware(request: Request, ctx: InfinityContext) {
const { pathname } = new URL(request.url);
switch(pathname) {
case '/ping': return new Response('pong');
case '/__worker__/handshake': return this.handshake(request, ctx);
}
ctx.state.request = new InfinityRequest();
const IS_EXTERNAL_API = /\/api\/:v\d+\//.test(pathname);
if(IS_EXTERNAL_API) return Response.json({});
if(!IS_EXTERNAL_API) {
const cookie_uuid = cookie.getCookies(request.headers)[COOKIE_NAME] as string | undefined;
if(cookie_uuid) {
ctx.state.cookie_uuid = cookie_uuid;
ctx.state.cookie_existed = true;
} else {
ctx.state.cookie_uuid = randomUUID();
ctx.state.cookie_existed = false;
}
await this.draco_setup(ctx);
await this.notification_setup(ctx);
// Special requests
switch(pathname) {
case '/__worker__/handshake':
return this.handshake(request, ctx);
case '/logout':
return this.logout();
case '/login': {
if(!ctx.state.user) return this.login(ctx);
return new Response(undefined, {
status: 307,
headers: {
Location: '/'
}
})
}
}
}
// Handle standard request
this.emit('request', request, ctx);
await RequestAnalytics.Process(request, ctx);
const response = await ctx.next();
const extension = request.url.replace(/^.+(\.\w+)$/, '$1');
switch(extension) {
case '.png':
case '.js':
case '.css':
case '.svg':
ctx.state.request.finish_without_saving();
break;
default: {
ctx.state.request.finish();
}
}
await ResponseAnalytics.Process(response, ctx)
this.emit('response', response, ctx);
InfinityHeaders.apply(response.headers, {
'x-request-id': ctx.state.request.request_id,
'x-time-taken': ctx.state.request.duration?.toString(),
}, InfinityHeaders.Default);
if(!IS_EXTERNAL_API) {
if(!ctx.state.cookie_existed) {
cookie.setCookie(response.headers, {
name: COOKIE_NAME,
value: ctx.state.cookie_uuid,
maxAge: 60 * 60 * 24 * 7, // 7 days
secure: new URL(request.url).protocol == 'https:',
});
}
}
return response;
}
handshake(request: Request, _ctx: InfinityContext) {
const signature = request.headers.get('sec-websocket-protocol')?.replace(/^SIGNATURE, (.+$)$/, '$1');
if(!signature || signature !== State.SIGNATURE) {
return new Response('Invalid signature', { status: 403 });
}
const { socket, response } = Deno.upgradeWebSocket(request);
const query: Record<string, string> = new URL(request.url).search
?.replace(/^\?/, '')
.split('&')
.reduce((p, query) => {
const [key, value] = query.split('=');
p[key] = value;
return p;
}, {} as Record<string, string>) ?? {};
const { uuid } = query;
if(!uuid) {
socket.close(1, 'No provided uuid');
return new Response('', { status: 403 });
}
socket.onopen = () => {
Entity.__AddWorker(uuid, socket);
}
return response;
}
async draco_setup(ctx: InfinityContext) {
ctx.state.user = await GetDracoUser(ctx.state.cookie_uuid);
}
async notification_setup(ctx: InfinityContext) {
ctx.state.notifications = [{
id: 1,
title: "Testing",
body: "This is a test notification",
read: false,
}, {
id: 2,
title: "Hello Willie",
body: ":)",
read: false,
}, {
id: 3,
title: "Roof shingles in your area!",
body: "",
read: false,
icon: CircleAlert,
action: {
link: '/',
text: 'Wow!'
}
}];
}
async login(ctx: InfinityContext) {
return await LoginRequest(ctx.state.cookie_uuid);
}
async logout() {
const response = new Response(undefined, {
status: 307,
headers: {
Location: '/',
}
});
cookie.deleteCookie(response.headers, COOKIE_NAME);
return response;
}
static get Permissions() {
return DefaultPermissions;
}
private static HAS_ANNOUNCED = false;
private static announce(import_meta_dirname: string) {
if(this.HAS_ANNOUNCED) return;
this.HAS_ANNOUNCED = true;
if(State.IS_BUILDING) return;
setTimeout(async () => {
const Entities = await FetchInfinityClasses(import_meta_dirname);
const EntityPermissions: string[] = [];
Object.values(Entities).forEach(entity => {
if(entity instanceof Entity) {
EntityPermissions.push(...Object.keys((entity as any).__permissions));
}
});
AnnouncePermissions(EntityPermissions);
}, 100);
}
}
type InfinityEvents = {
request: [request: Request, ctx: InfinityContext],
response: [response: Response, ctx: InfinityContext],
}