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 { freshHandler } from "@infinity-beyond/modules/networking/server.ts";
import type { ResolvedFreshConfig } from "$fresh/server.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";
import type { ServeHandlerInfo } from "$fresh/src/server/types.ts";
import { prepareApplication } from "@infinity-beyond/modules/distribution/prepare_application.ts";
import { existsSync } from "jsr:@std/fs";
import { join } from "jsr:@std/path";
import { plural } from "https://deno.land/x/deno_plural@2.0.0/mod.ts";
import { create_fresh_context } from "@infinity-beyond/modules/networking/context.ts";
const COOKIE_NAME = `${(Deno.env.get('INFINITY_APP')?.toLowerCase().replace(/\s+/g, '-').replace(/_/g, '-') || 'Infinity')}-session`;
export class Infinity extends CustomEventEmitter<Infinity.Events> {
readonly app_name: string
constructor(app_name: string) {
super();
this.app_name = app_name;
State.Application = this;
}
private fresh_handler!: ((req: Request, connInfo?: ServeHandlerInfo) => Promise<Response>);
private ctx_config!: ResolvedFreshConfig;
async listen({ port: PORT }: Infinity.ListenOptions = {}) {
const port = Number(Deno.env.get('PORT')) || PORT || 9090;
State.Entities = await FetchInfinityClasses();
Infinity.announce();
if(!existsSync(join(Deno.cwd(), 'app/islands/utils'))) await prepareApplication();
const [fresh_handler, ctx_config] = await freshHandler({
dev: Deno.args.includes('dev')
});
this.ctx_config = ctx_config;
this.fresh_handler = fresh_handler;
Deno.serve({
port,
onListen: () => {
console.log(`♾️ Infinity Beyond listening at http://localhost:${port}/`);
}
}, this.handler.bind(this));
}
private async handler(request: Request, info: Deno.ServeHandlerInfo<Deno.NetAddr>) {
request.state = {} as Infinity.Context;
const { pathname } = new URL(request.url);
switch(pathname) {
case '/ping': return new Response('pong');
case '/__worker__/handshake': return Infinity.handshake(request);
}
request.state.uuid = randomUUID();
request.state.start = new Date();
if(pathname.startsWith('/_frsh')) return this.fresh_handler(request, info);
if(pathname.match(/\.(?:css|svg|png|jpg)$/)) return this.fresh_handler(request, info);
const IS_EXTERNAL_API = /\/api\/v\d+\//.test(pathname);
if(IS_EXTERNAL_API) {
const slug = pathname.replace(/^.*\/api\/v\d+\/(\w+)\/?.*$/, '$1');
const entity = State.Entities.values().find(e => plural(e.slug) == slug);
const ctx = create_fresh_context(this.ctx_config, request, info);
const handler = entity?.REST?.handler;
if(handler) return handler(request, ctx as any) || Response.json({}, { status: 404 });
};
return this.fresh_handler(request, info);
}
static async Cookie(request: Request, ctx: InfinityContext) {
ctx.state ||= request.state;
const cookie_uuid = cookie.getCookies(request.headers)[COOKIE_NAME] as string | undefined;
if(cookie_uuid) {
request.state.cookie_uuid = ctx.state.cookie_uuid = cookie_uuid;
request.state.cookie_existed = ctx.state.cookie_existed = true;
} else {
request.state.cookie_uuid = ctx.state.cookie_uuid = randomUUID();
request.state.cookie_existed = ctx.state.cookie_existed = false;
}
}
static async Middleware(request: Request, ctx: InfinityContext) {
const { pathname } = new URL(request.url);
ctx.state = request.state;
this.Cookie(request, ctx);
await Infinity.draco_setup(ctx);
await Infinity.notification_setup(ctx.state);
// Special requests
switch(pathname) {
case '/logout':
return Infinity.logout();
case '/login': {
if(!request.state.user) return Infinity.login(request);
return new Response(undefined, {
status: 307,
headers: {
Location: '/'
}
})
}
}
await RequestAnalytics.Process(request);
const response = await ctx.next();
await ResponseAnalytics.Process(request, response);
InfinityHeaders.apply(response.headers, {
'x-request-id': request.state.uuid,
'x-time-taken': `${Date.now() - request.state.start.getTime()}`,
}, InfinityHeaders.Default);
if(!request.state.cookie_existed) {
cookie.setCookie(response.headers, {
name: COOKIE_NAME,
value: request.state.cookie_uuid,
maxAge: 60 * 60 * 24 * 7, // 7 days
secure: new URL(request.url).protocol == 'https:',
});
}
return response;
}
private static handshake(request: Request) {
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;
}
private static async draco_setup({ state }: { state: Infinity.Context }) {
state.user = await GetDracoUser(state.cookie_uuid);
}
private static async notification_setup(state: Infinity.Context) {
state.notifications = [{
id: 1,
title: "Testing",
body: "This is a test notification",
read: false,
}];
}
private static async login(request: Request) {
return await LoginRequest(request.state.cookie_uuid);
}
private static 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() {
if(this.HAS_ANNOUNCED) return;
this.HAS_ANNOUNCED = true;
if(State.IS_BUILDING) return;
const EntityPermissions: string[] = [];
Object.values(State.Entities).forEach(entity => {
if(entity instanceof Entity) {
EntityPermissions.push(...Object.keys((entity as any).__permissions));
}
});
AnnouncePermissions(EntityPermissions);
}
}