feat: Update website, API, and infra (#164)

>Adds `maitred` in charge of handling automated game installs, updates,
and even execution.

>Not only that, we have the hosted stuff here
>- [x] AWS Task on ECS GPUs
>- [ ] Add a service to listen for game starts and stops
(docker-compose.yml)
>- [x] Add a queue for requesting a game to start
>- [x] Fix up the play/watch UI 

>TODO:
>- Add a README
>- Add an SST docs

Edit:

- This adds a new landing page, updates the homepage etc etc
>I forgot what the rest of the updated stuff are 😅
This commit is contained in:
Wanjohi
2025-02-11 12:26:35 +03:00
committed by GitHub
parent 93327bdf1a
commit 060718d8b0
139 changed files with 5814 additions and 5049 deletions

View File

@@ -0,0 +1,38 @@
import { Resource } from "sst";
import { subjects } from "../subjects";
import { realtime } from "sst/aws/realtime";
import { createClient } from "@openauthjs/openauth/client";
export const handler = realtime.authorizer(async (token) => {
//TODO: Use the following criteria for a topic - team-slug/container-id (container ids are not unique globally)
//TODO: Allow the authorizer to subscriber/publisher to listen on - team-slug topics only (as the container will listen on the team-slug/container-id topic to be specific)
// Return the topics to subscribe and publish
const client = createClient({
clientID: "api",
issuer: Resource.Urls.auth
});
const result = await client.verify(subjects, token);
if (result.err) {
console.log("error", result.err)
return {
subscribe: [],
publish: [],
};
}
if (result.subject.type != "device") {
return {
subscribe: [],
publish: [],
};
}
return {
//It can publish and listen to other instances under this team
subscribe: [`${Resource.App.name}/${Resource.App.stage}/${result.subject.properties.teamSlug}/*`],
publish: [`${Resource.App.name}/${Resource.App.stage}/${result.subject.properties.teamSlug}/*`],
};
});

View File

@@ -0,0 +1,64 @@
import { ECSClient, RunTaskCommand } from "@aws-sdk/client-ecs";
const client = new ECSClient()
export const handler = async (event: any) => {
console.log("event", event)
const clusterArn = process.env.ECS_CLUSTER
const taskDefinitionArn = process.env.TASK_DEFINITION
const authFingerprintKey = process.env.AUTH_FINGERPRINT
try {
const runResponse = await client.send(new RunTaskCommand({
taskDefinition: taskDefinitionArn,
cluster: clusterArn,
count: 1,
launchType: "EC2",
overrides: {
containerOverrides: [
{
name: "nestri",
environment: [
{
name: "AUTH_FINGERPRINT_KEY",
value: authFingerprintKey
},
{
name: "NESTRI_ROOM",
value: "testing-right-now"
}
]
}
]
}
}))
// Check if tasks were started
if (!runResponse.tasks || runResponse.tasks.length === 0) {
throw new Error("No tasks were started");
}
// Extract task details
const task = runResponse.tasks[0];
const taskArn = task.taskArn!;
const taskId = taskArn.split('/').pop()!; // Extract task ID from ARN
const taskStatus = task.lastStatus!;
return {
statusCode: 200,
body: JSON.stringify({
status: "sent",
taskId: taskId,
taskStatus: taskStatus,
taskArn: taskArn
}, null, 2),
};
} catch (err) {
console.error("Error starting task:", err);
return {
statusCode: 500,
body: JSON.stringify({ error: "Failed to start task" }, null, 2),
};
}
};

View File

@@ -1,65 +0,0 @@
import "zod-openapi/extend";
import { Hono } from "hono";
import { logger } from "hono/logger";
import type { HonoBindings } from "./types";
import { ApiSession } from "./session";
import { openAPISpecs } from "hono-openapi";
const app = new Hono<{ Bindings: HonoBindings }>().basePath('/parties/main/:room');
app
.use(logger(), async (c, next) => {
c.header("Cache-Control", "no-store");
try {
await next();
} catch (e: any) {
return c.json(
{
error: {
message: e.message || "Internal Server Error",
status: e.status || 500,
},
},
e.status || 500
);
}
})
const routes = app
.get("/health", (c) => {
return c.json({
status: "healthy",
timestamp: new Date().toISOString(),
});
})
.route("/session", ApiSession.route)
app.get(
"/doc",
openAPISpecs(routes, {
documentation: {
info: {
title: "Nestri Realtime API",
description:
"The Nestri realtime API gives you the power to connect to your remote machine and relays from a single station",
version: "0.3.0",
},
components: {
securitySchemes: {
Bearer: {
type: "http",
scheme: "bearer",
bearerFormat: "JWT",
},
},
},
security: [{ Bearer: [] }],
servers: [
{ description: "Production", url: "https://api.nestri.io" },
],
},
}),
);
export type Routes = typeof routes;
export default app

View File

@@ -1,63 +0,0 @@
import app from "./hono"
import type * as Party from "partykit/server";
import { tryAuthentication } from "./utils";
export default class Server implements Party.Server {
constructor(readonly room: Party.Room) { }
static async onBeforeRequest(req: Party.Request, lobby: Party.Lobby) {
const docs = new URL(req.url).toString().endsWith("/doc")
if (docs) {
return req
}
try {
return await tryAuthentication(req, lobby)
} catch (e: any) {
// authentication failed!
return new Response(e, { status: 401 });
}
}
static async onBeforeConnect(request: Party.Request, lobby: Party.Lobby) {
try {
return await tryAuthentication(request, lobby)
} catch (e: any) {
// authentication failed!
return new Response(e, { status: 401 });
}
}
onRequest(req: Party.Request): Response | Promise<Response> {
return app.fetch(req as any, { room: this.room })
}
getConnectionTags(conn: Party.Connection, ctx: Party.ConnectionContext) {
return [conn.id, ctx.request.cf?.country as any]
}
onConnect(conn: Party.Connection, ctx: Party.ConnectionContext): void | Promise<void> {
console.log(`Connected:, id:${conn.id}, room: ${this.room.id}, url: ${new URL(ctx.request.url).pathname}`);
this.getConnectionTags(conn, ctx)
}
onMessage(message: string, sender: Party.Connection) {
// let's log the message
console.log(`connection ${sender.id} sent message: ${message}`);
// console.log("tags", this.room.getConnections())
// for (const british of this.room.getConnections(sender.id)) {
// british.send(`Pip-pip!`);
// }
// // as well as broadcast it to all the other connections in the room...
// this.room.broadcast(
// `${sender.id}: ${message}`,
// // ...except for the connection it came from
// [sender.id]
// );
}
}
Server satisfies Party.Worker;

View File

@@ -1,217 +0,0 @@
import { z } from "zod";
import { Hono } from "hono";
import { Result } from "../common"
import { describeRoute } from "hono-openapi";
import type { HonoBindings, WSMessage } from "./types";
import { validator, resolver } from "hono-openapi/zod";
export module ApiSession {
export const route = new Hono<{ Bindings: HonoBindings }>()
.post("/:sessionID/start",
describeRoute({
tags: ["Session"],
summary: "Start a session",
description: "Start a session on this machine",
responses: {
200: {
content: {
"application/json": {
schema: Result(z.object({
success: z.boolean(),
message: z.string(),
sessionID: z.string()
}))
},
},
description: "Session started successfully",
},
500: {
content: {
"application/json": {
schema: resolver(z.object({ error: z.string(), details: z.string() })),
},
},
description: "There was a problem trying to start your session",
},
},
}),
validator(
"param",
z.object({
sessionID: z.string().openapi({
description: "The session ID to start",
example: "18d8b4b5-29ba-4a62-8cf9-7059449907a7",
}),
}),
),
async (c) => {
const param = c.req.valid("param");
const room = c.env.room
const message: WSMessage = {
type: "START_GAME",
sessionID: param.sessionID,
};
try {
room.broadcast(JSON.stringify(message));
return c.json({
success: true,
message: "Game start signal sent",
"sessionID": param.sessionID,
});
} catch (error: any) {
return c.json(
{
error: {
message: "Failed to start game session",
details: error.message,
},
},
500
);
}
}
)
.post("/:sessionID/end",
describeRoute({
tags: ["Session"],
summary: "End a session",
description: "End a session on this machine",
responses: {
200: {
content: {
"application/json": {
schema: Result(z.object({
success: z.boolean(),
message: z.string(),
sessionID: z.string()
}))
},
},
description: "Session successfully ended",
},
500: {
content: {
"application/json": {
schema: resolver(z.object({ error: z.string(), details: z.string() })),
},
},
description: "There was a problem trying to end your session",
},
},
}),
validator(
"param",
z.object({
sessionID: z.string().openapi({
description: "The session ID to end",
example: "18d8b4b5-29ba-4a62-8cf9-7059449907a7",
}),
}),
),
async (c) => {
const param = c.req.valid("param");
const room = c.env.room
const message: WSMessage = {
type: "END_GAME",
sessionID: param.sessionID,
};
try {
room.broadcast(JSON.stringify(message));
return c.json({
success: true,
message: "Game end signal sent",
"sessionID": param.sessionID,
});
} catch (error: any) {
return c.json(
{
error: {
message: "Failed to end game session",
details: error.message,
},
},
500
);
}
}
)
.post("/:sessionID/status",
describeRoute({
tags: ["Session"],
summary: "Get the status of a session",
description: "Get the status of a session on this machine",
responses: {
200: {
content: {
"application/json": {
schema: Result(z.object({
success: z.boolean(),
message: z.string(),
sessionID: z.string()
}))
},
},
description: "Session status query was successful"
},
500: {
content: {
"application/json": {
schema: resolver(z.object({ error: z.string(), details: z.string() })),
},
},
description: "There was a problem trying to querying the status of your session",
},
},
}),
validator(
"param",
z.object({
sessionID: z.string().openapi({
description: "The session ID to query",
example: "18d8b4b5-29ba-4a62-8cf9-7059449907a7",
}),
}),
),
async (c) => {
const param = c.req.valid("param");
const room = c.env.room
const message: WSMessage = {
type: "END_GAME",
sessionID: param.sessionID,
};
try {
room.broadcast(JSON.stringify(message));
return c.json({
success: true,
message: "Game end signal sent",
"sessionID": param.sessionID,
});
} catch (error: any) {
return c.json(
{
error: {
message: "Failed to end game session",
details: error.message,
},
},
500
);
}
}
)
}

View File

@@ -0,0 +1,4 @@
export const handler = async (event: any) => {
console.log(event);
return "ok";
};

View File

@@ -1,11 +0,0 @@
import type * as Party from "partykit/server";
export interface HonoBindings {
room: Party.Room;
}
export type WSMessage = {
type: "START_GAME" | "END_GAME" | "GAME_STATUS";
sessionID: string;
payload?: any;
};

View File

@@ -1,21 +0,0 @@
import type * as Party from "partykit/server";
export async function tryAuthentication(req: Party.Request, lobby: Party.Lobby) {
const authHeader = req.headers.get("authorization") ?? new URL(req.url).searchParams.get("authorization")
if (authHeader) {
const match = authHeader.match(/^Bearer (.+)$/);
if (!match || !match[1]) {
throw new Error("Bearer token not found or improperly formatted");
}
const bearerToken = match[1];
if (bearerToken !== lobby.env.AUTH_FINGERPRINT) {
throw new Error("Invalid authorization token");
}
return req// app.fetch(req as any, { room: this.room })
}
throw new Error("You are not authorized to be here")
}