diff --git a/apps/www/package.json b/apps/www/package.json index 0e4a221f..3ec9d758 100644 --- a/apps/www/package.json +++ b/apps/www/package.json @@ -35,8 +35,10 @@ "@builder.io/qwik": "^1.8.0", "@builder.io/qwik-city": "^1.8.0", "@builder.io/qwik-react": "0.5.0", - "@fontsource-variable/bricolage-grotesque": "^5.1.1", - "@fontsource-variable/mona-sans": "^5.0.1", + "@fontsource/bricolage-grotesque": "^5.0.7", + "@fontsource/geist-mono": "^5.1.0", + "@fontsource/geist-sans": "^5.1.0", + "@fontsource/mona-sans": "^5.0.1", "@modular-forms/qwik": "^0.29.0", "@nestri/input": "*", "@nestri/libmoq": "*", diff --git a/infra/database.ts b/infra/database.ts index 44f0e0f6..83e99870 100644 --- a/infra/database.ts +++ b/infra/database.ts @@ -1,7 +1,7 @@ const dbProject = new neon.Project("Nestri", { historyRetentionSeconds: 86400, - // name:"Nestri" + name:"Nestri" }) const dbBranchId = $app.stage !== "production" ? diff --git a/infra:old/api.ts b/infra:old/api.ts deleted file mode 100644 index 2877b0c4..00000000 --- a/infra:old/api.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { authFingerprintKey } from "./auth"; -import { domain } from "./dns"; -import { secret } from "./secrets" -// import { party } from "./party" -import { gpuTaskDefinition, ecsCluster } from "./cluster"; - -export const urls = new sst.Linkable("Urls", { - properties: { - api: "https://api." + domain, - auth: "https://auth." + domain, - }, -}); - -export const kv = new sst.cloudflare.Kv("CloudflareAuthKV") - -export const auth = new sst.cloudflare.Worker("Auth", { - link: [ - kv, - urls, - authFingerprintKey, - secret.InstantAdminToken, - secret.InstantAppId, - secret.LoopsApiKey, - secret.GithubClientID, - secret.GithubClientSecret, - secret.DiscordClientID, - secret.DiscordClientSecret, - ], - handler: "./packages/functions/src/auth.ts", - url: true, - domain: "auth." + domain -}); - -export const api = new sst.cloudflare.Worker("Api", { - link: [ - urls, - ecsCluster, - gpuTaskDefinition, - authFingerprintKey, - secret.LoopsApiKey, - secret.InstantAppId, - secret.AwsAccessKey, - secret.AwsSecretKey, - secret.InstantAdminToken, - ], - url: true, - handler: "./packages/functions/src/api/index.ts", - domain: "api." + domain -}) - -export const outputs = { - auth: auth.url, - api: api.url -} \ No newline at end of file diff --git a/infra:old/auth.ts b/infra:old/auth.ts deleted file mode 100644 index b9c9fad5..00000000 --- a/infra:old/auth.ts +++ /dev/null @@ -1,12 +0,0 @@ -export const authFingerprintKey = new random.RandomString( - "AuthFingerprintKey", - { - length: 32, - }, -); - -sst.Linkable.wrap(random.RandomString, (resource) => ({ - properties: { - value: resource.result, - }, -})); \ No newline at end of file diff --git a/infra:old/cluster.ts b/infra:old/cluster.ts deleted file mode 100644 index 8916b7f6..00000000 --- a/infra:old/cluster.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { sshKey } from "./ssh"; -import { authFingerprintKey } from "./auth"; - -export const ecsCluster = new aws.ecs.Cluster("NestriGPUCluster", { - name: "NestriGPUCluster", -}); - -const ecsInstanceRole = new aws.iam.Role("NestriGPUInstanceRole", { - name: "GPUAssumeRoleProd", - assumeRolePolicy: JSON.stringify({ - Version: "2012-10-17", - Statement: [{ - Action: "sts:AssumeRole", - Principal: { - Service: "ec2.amazonaws.com", - }, - Effect: "Allow", - Sid: "", - }], - }), -}); - -new aws.iam.RolePolicyAttachment("NestriGPUInstancePolicyAttachment", { - role: ecsInstanceRole.name, - policyArn: "arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role", -}); - -const ecsInstanceProfile = new aws.iam.InstanceProfile("NestriGPUInstanceProfile", { - role: ecsInstanceRole.name, -}); - -// const server = new aws.ec2.Instance("NestriGPU", { -// instanceType: aws.ec2.InstanceType.G4dn_XLarge, -// ami: "ami-046a6af96ef510bb6",//Fedora cloud -// keyName: sshKey.keyName, -// instanceMarketOptions: { -// marketType: "spot", -// spotOptions: { -// maxPrice: "0.2", -// spotInstanceType: "persistent", -// instanceInterruptionBehavior: "stop" -// }, -// }, -// iamInstanceProfile: ecsInstanceProfile, -// }); - -const logGroup = new aws.cloudwatch.LogGroup("NestriGPULogGroup", { - name: "/ecs/nestri-gpu-prod", - retentionInDays: 7, -}); - -// Create a Task Definition for the ECS service to test it -export const gpuTaskDefinition = new aws.ecs.TaskDefinition("NestriGPUTask", { - family: "NestriGPUTaskProd", - requiresCompatibilities: ["EC2"], - volumes: [ - { - name: "host", - hostPath: "/mnt/" - // efsVolumeConfiguration: { - // fileSystemId: storage.id, - // authorizationConfig: { accessPointId: storage.accessPoint }, - // transitEncryption: "ENABLED", - // } - } - ], - containerDefinitions: authFingerprintKey.result.apply(v => JSON.stringify([{ - "essential": true, - "name": "nestri", - "memory": 1024, - "cpu": 200, - "gpu": 1, - "image": "ghcr.io/nestrilabs/nestri/runner:nightly", - "environment": [ - { - "name": "RESOLUTION", - "value": "1920x1080" - }, - { - "name": "AUTH_FINGERPRINT", - "value": v - }, - { - "name": "FRAMERATE", - "value": "60" - }, - { - "name": "NESTRI_ROOM", - "value": "aws-testing" - }, - { - "name": "RELAY_URL", - "value": "https://relay.dathorse.com" - }, - { - "name": "NESTRI_PARAMS", - "value": "--verbose=true --video-codec=h264 --video-bitrate=4000 --video-bitrate-max=6000 --gpu-card-path=/dev/dri/card0" - }, - ], - "mountPoints": [{ "containerPath": "/home/nestri", "sourceVolume": "host" }], - "disableNetworking": false, - "linuxParameter": { - "sharedMemorySize": 5120 - }, - "logConfiguration": { - "logDriver": "awslogs", - "options": { - "awslogs-group": "/ecs/nestri-gpu-prod", - "awslogs-region": "us-east-1", - "awslogs-stream-prefix": "nestri-gpu-task" - } - } - }])) -}); - -sst.Linkable.wrap(aws.ecs.TaskDefinition, (resource) => ({ - properties: { - value: resource.arn, - }, -})); - -sst.Linkable.wrap(aws.ecs.Cluster, (resource) => ({ - properties: { - value: resource.arn, - }, -})); - -// userData: $interpolate`#!/bin/bash -// sudo rm /etc/sysconfig/docker -// echo DAEMON_MAXFILES=1048576 | sudo tee -a /etc/sysconfig/docker -// echo DAEMON_PIDFILE_TIMEOUT=10 | sud o tee -a /etc/sysconfig/docker -// echo OPTIONS="--default-ulimit nofile=32768:65536" | sudo tee -a /etc/sysconfig/docker -// sudo tee "/etc/docker/daemon.json" > /dev/null < party <-websocket-> container -// The container is it's own this, and can listen to Websocket connections to start or stop a Steam Game - -// import { authFingerprintKey } from "./auth"; -// import { ecsCluster, gpuTaskDefinition } from "./cluster"; - -// export const party = new sst.aws.Realtime("Party", { -// authorizer: "packages/functions/src/party/authorizer.handler" -// }); - -// export const partyFn = new sst.aws.Function("NestriPartyFn", { -// handler: "packages/functions/src/party/create.handler", -// // link: [queue], -// link: [authFingerprintKey], -// environment: { -// TASK_DEFINITION: gpuTaskDefinition.arn, -// // AUTH_FINGERPRINT: authFingerprintKey.result, -// ECS_CLUSTER: ecsCluster.arn, -// }, -// permissions: [ -// { -// effect: "allow", -// actions: ["ecs:RunTask"], -// resources: [gpuTaskDefinition.arn] -// } -// ], -// url: true, -// }); - -// export const outputs = { -// partyFunction: partyFn.url -// } \ No newline at end of file diff --git a/infra:old/relay.ts b/infra:old/relay.ts deleted file mode 100644 index e6f6e435..00000000 --- a/infra:old/relay.ts +++ /dev/null @@ -1,179 +0,0 @@ -// const vpc = new sst.aws.Vpc("NestriRelayVpc", { az: 2 }) -// import { subnet1, subnet2, securityGroup } from "./vpc" - -// const taskExecutionRole = new aws.iam.Role('NestriRelayExecutionRole', { -// assumeRolePolicy: JSON.stringify({ -// Version: '2012-10-17', -// Statement: [ -// { -// Effect: 'Allow', -// Principal: { -// Service: 'ecs-tasks.amazonaws.com', -// }, -// Action: 'sts:AssumeRole', -// }, -// ], -// }), -// }); - -// const taskRole = new aws.iam.Role('NestriRelayTaskRole', { -// assumeRolePolicy: JSON.stringify({ -// Version: '2012-10-17', -// Statement: [ -// { -// Effect: 'Allow', -// Principal: { -// Service: 'ecs-tasks.amazonaws.com', -// }, -// Action: 'sts:AssumeRole', -// }, -// ], -// }), -// }); - -// new aws.cloudwatch.LogGroup('NestriRelayLogGroup', { -// name: '/ecs/nestri-relay', -// retentionInDays: 7, -// }); - -// new aws.iam.RolePolicyAttachment('NestriRelayExecutionRoleAttachment', { -// policyArn: 'arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy', -// role: taskRole, -// }); - -// const logPolicy = new aws.iam.Policy('NestriRelayLogPolicy', { -// policy: JSON.stringify({ -// Version: '2012-10-17', -// Statement: [ -// { -// Effect: 'Allow', -// Action: ['logs:CreateLogStream', 'logs:PutLogEvents'], -// Resource: 'arn:aws:logs:*:*:*', -// }, -// ], -// }), -// }); - -// new aws.iam.RolePolicyAttachment('NestriRelayTaskRoleAttachment', { -// policyArn: logPolicy.arn, -// role: taskExecutionRole, -// }); - -// const taskDefinition = new aws.ecs.TaskDefinition("NestriRelayTask", { -// family: "NestriRelay", -// cpu: "1024", -// memory: "2048", -// networkMode: "awsvpc", -// taskRoleArn: taskRole.arn, -// requiresCompatibilities: ["FARGATE"], -// executionRoleArn: taskExecutionRole.arn, -// containerDefinitions: JSON.stringify([{ -// name: "nestri-relay", -// essential: true, -// memory: 2048, -// image: "ghcr.io/nestrilabs/nestri/relay:nightly", -// portMappings: [ -// // HTTP port -// { -// protocol: "tcp", -// hostPort: 80, -// containerPort: 80, -// }, -// // UDP port range (1,000 ports) -// { -// containerPortRange: "10000-11000", -// protocol: "udp", -// }, -// ], -// "environment": [ -// { -// name: "ENDPOINT_PORT", -// value: "80" -// }, -// ], -// logConfiguration: { -// logDriver: 'awslogs', -// options: { -// 'awslogs-group': '/ecs/nestri-relay', -// 'awslogs-region': 'us-east-1', -// 'awslogs-stream-prefix': 'ecs', -// }, -// }, -// }]), -// }); - -// const relayCluster = new aws.ecs.Cluster('NestriRelay'); - -// new aws.ecs.Service('NestriRelayService', { -// name: 'NestriRelayService', -// cluster: relayCluster.arn, -// desiredCount: 1, -// launchType: 'FARGATE', -// taskDefinition: taskDefinition.arn, -// deploymentCircuitBreaker: { -// enable: true, -// rollback: true, -// }, -// enableExecuteCommand: true, -// networkConfiguration: { -// assignPublicIp: true, -// subnets: [subnet1.id, subnet2.id], -// securityGroups: [securityGroup.id], -// }, -// }); - -//FIXME: I cannot create Global Accelerators (Something to do with Quotas - Yet my account is fine) -// const usWest2 = new aws.Provider("GlobalAccelerator", { region: aws.Region.USWest2 }) - -// const accelerator = new aws.globalaccelerator.Accelerator('Accelerator', { -// name: 'NestriRelayAccelerator', -// enabled: true, -// ipAddressType: 'IPV4', -// }, { provider: usWest2 }); - -// const httpListener = new aws.globalaccelerator.Listener('TcpListener', { -// acceleratorArn: accelerator.id, -// clientAffinity: 'SOURCE_IP', -// protocol: 'TCP', -// portRanges: [{ -// fromPort: 80, -// toPort: 80, -// }], -// }, { provider: usWest2 }); - -// const udpListener = new aws.globalaccelerator.Listener('UdpListener', { -// acceleratorArn: accelerator.id, -// clientAffinity: 'SOURCE_IP', -// protocol: 'UDP', -// portRanges: [{ -// fromPort: 10000, -// toPort: 11000, -// }], -// }, { provider: usWest2 }); - -// new aws.globalaccelerator.EndpointGroup('TcpRelay', { -// listenerArn: httpListener.id, -// // healthCheckPath: '/', -// endpointGroupRegion: aws.Region.USEast1, -// endpointConfigurations: [{ -// clientIpPreservationEnabled: true, -// endpointId: subnet1.id, //vpc.publicSubnets[0].apply(i => i), -// weight: 100, -// }], -// }, { provider: usWest2 }); - -// new aws.globalaccelerator.EndpointGroup('UdpRelay', { -// listenerArn: udpListener.id, -// // healthCheckPort: 80, -// // healthCheckPath: "/", -// endpointGroupRegion: aws.Region.USEast1, -// endpointConfigurations: [{ -// clientIpPreservationEnabled: true, -// endpointId: subnet1.id,//vpc.publicSubnets[0].apply(i => i), -// weight: 100, -// }], -// }, { provider: usWest2 }); - -// export const outputs = { -// relay: accelerator.dnsName -// } \ No newline at end of file diff --git a/infra:old/secrets.ts b/infra:old/secrets.ts deleted file mode 100644 index 5b564072..00000000 --- a/infra:old/secrets.ts +++ /dev/null @@ -1,13 +0,0 @@ -export const secret = { - LoopsApiKey: new sst.Secret("LoopsApiKey"), - InstantAppId: new sst.Secret("InstantAppId"), - AwsSecretKey: new sst.Secret("AwsSecretKey"), - AwsAccessKey: new sst.Secret("AwsAccessKey"), - GithubClientID: new sst.Secret("GithubClientID"), - DiscordClientID: new sst.Secret("DiscordClientID"), - GithubClientSecret: new sst.Secret("GithubClientSecret"), - InstantAdminToken: new sst.Secret("InstantAdminToken"), - DiscordClientSecret: new sst.Secret("DiscordClientSecret"), - }; - - export const allSecrets = Object.values(secret); \ No newline at end of file diff --git a/infra:old/ssh.ts b/infra:old/ssh.ts deleted file mode 100644 index 9a809291..00000000 --- a/infra:old/ssh.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { resolve } from "path"; -import { writeFileSync } from "fs"; - -export const privateKey = new tls.PrivateKey("NestriGPUPrivateKey", { - algorithm: "RSA", - rsaBits: 4096, -}); - -// Just in case you want to SSH -export const sshKey = new aws.ec2.KeyPair("NestriGPUKey", { - keyName: "NestriGPUKeyProd", - publicKey: privateKey.publicKeyOpenssh -}) - -export const keyPath = privateKey.privateKeyOpenssh.apply((key) => { - const path = "key_ssh"; - writeFileSync(path, key, { mode: 0o600 }); - return resolve(path); -}); diff --git a/infra:old/stage.ts b/infra:old/stage.ts deleted file mode 100644 index 3f4f5335..00000000 --- a/infra:old/stage.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const isPermanentStage = - $app.stage === "production" || $app.stage === "dev"; \ No newline at end of file diff --git a/infra:old/storage.ts b/infra:old/storage.ts deleted file mode 100644 index 0b5e92f6..00000000 --- a/infra:old/storage.ts +++ /dev/null @@ -1,4 +0,0 @@ -// export const vpc = new sst.aws.Vpc("Vpc") - -// export const storage = new sst.aws.Efs("GameStorage",{ vpc }) -// // \ No newline at end of file diff --git a/infra:old/vpc.ts b/infra:old/vpc.ts deleted file mode 100644 index bff06b74..00000000 --- a/infra:old/vpc.ts +++ /dev/null @@ -1,103 +0,0 @@ -// export const vpc = new aws.ec2.Vpc('NestriVpc', { -// cidrBlock: '172.16.0.0/16', -// }); - -// export const subnet1 = new aws.ec2.Subnet('NestriSubnet1', { -// vpcId: vpc.id, -// cidrBlock: '172.16.1.0/24', -// // cidrBlock: '110.0.12.0/22', -// availabilityZone: 'us-east-1a', -// }); - -// export const subnet2 = new aws.ec2.Subnet('NestriSubnet2', { -// vpcId: vpc.id, -// cidrBlock: '172.16.2.0/24', -// // cidrBlock: '10.0.20.0/22', -// availabilityZone: 'us-east-1b', -// }); - -// const internetGateway = new aws.ec2.InternetGateway('NestriInternetGateway', { -// vpcId: vpc.id, -// }); - -// const routeTable = new aws.ec2.RouteTable('NestriRouteTable', { -// vpcId: vpc.id, -// routes: [ -// { -// cidrBlock: '0.0.0.0/0', -// gatewayId: internetGateway.id, -// }, -// ], -// }); - -// new aws.ec2.RouteTableAssociation('NestriSubnet1RouteTable', { -// subnetId: subnet1.id, -// routeTableId: routeTable.id, -// }); - -// new aws.ec2.RouteTableAssociation('NestriSubnet2RouteTable', { -// subnetId: subnet2.id, -// routeTableId: routeTable.id, -// }); - -// // const vpc = new sst.aws.Vpc("NestriRelayVpc") - -// export const securityGroup = new aws.ec2.SecurityGroup("NestriSecurityGroup", { -// vpcId: vpc.id, -// description: "Managed thru SST", -// ingress: [ -// { -// protocol: "tcp", -// fromPort: 80, -// toPort: 80, -// cidrBlocks: ["0.0.0.0/0"], -// }, -// { -// protocol: "udp", -// fromPort: 10000, -// toPort: 20000, -// cidrBlocks: ["0.0.0.0/0"], -// }, -// ], -// egress: [ -// { -// protocol: "-1", -// cidrBlocks: ["0.0.0.0/0"], -// fromPort: 0, -// toPort: 0 -// } -// ] -// }); - -// const loadBalancer = new aws.lb.LoadBalancer('NestriVpcLoadBalancer', { -// name: 'NestriVpcLoadBalancer', -// internal: false, -// securityGroups: [securityGroup.id], -// subnets: vpc.publicSubnets -// }); - -// const targetGroup = new aws.lb.TargetGroup('NestriVpcTargetGroup', { -// name: 'NestriVpcTargetGroup', -// port: 80, -// protocol: 'HTTP', -// targetType: 'ip', -// vpcId: vpc.id, -// healthCheck: { -// path: '/', -// protocol: 'HTTP', -// }, -// }); - -// new aws.lb.Listener('NestriVpcLoadBalancerListener', { -// loadBalancerArn: loadBalancer.arn, -// port: 80, -// protocol: 'HTTP', -// defaultActions: [ -// { -// type: 'forward', -// targetGroupArn: targetGroup.arn, -// }, -// ], -// }); - -// // export const subnets = [subnet1, subnet2] diff --git a/packages/core/src:old/actor.ts b/packages/core/src:old/actor.ts deleted file mode 100644 index 6fd4296a..00000000 --- a/packages/core/src:old/actor.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { createContext } from "../src/context"; -import { VisibleError } from "./error"; - -export interface UserActor { - type: "user"; - properties: { - accessToken: string; - userID: string; - auth?: - | { - type: "personal"; - token: string; - } - | { - type: "oauth"; - clientID: string; - }; - }; -} - -export interface DeviceActor { - type: "device"; - properties: { - teamSlug: string; - hostname: string; - auth?: - | { - type: "personal"; - token: string; - } - | { - type: "oauth"; - clientID: string; - }; - }; -} - -export interface PublicActor { - type: "public"; - properties: {}; -} - -type Actor = UserActor | PublicActor | DeviceActor; -export const ActorContext = createContext(); - -export function useCurrentUser() { - const actor = ActorContext.use(); - if (actor.type === "user") return { - id:actor.properties.userID, - token: actor.properties.accessToken, - }; - - throw new VisibleError( - "auth", - "unauthorized", - `You don't have permission to access this resource`, - ); -} - -export function useCurrentDevice() { - const actor = ActorContext.use(); - if (actor.type === "device") return { - hostname:actor.properties.hostname, - teamSlug: actor.properties.teamSlug - }; - throw new VisibleError( - "auth", - "unauthorized", - `You don't have permission to access this resource`, - ); -} - -export function useActor() { - try { - return ActorContext.use(); - } catch { - return { type: "public", properties: {} } as PublicActor; - } - } - - export function assertActor(type: T) { - const actor = useActor(); - if (actor.type !== type) - throw new VisibleError("auth", "actor.invalid", `Actor is not "${type}"`); - return actor as Extract; - } \ No newline at end of file diff --git a/packages/core/src:old/aws/client.ts b/packages/core/src:old/aws/client.ts deleted file mode 100644 index 21e528ec..00000000 --- a/packages/core/src:old/aws/client.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { z } from "zod" -import { Resource } from "sst"; -import { doubleFn, fn } from "../utils"; -import { AwsClient } from "aws4fetch"; -import { DescribeTasksCommandOutput, StopTaskCommandOutput, type RunTaskCommandOutput } from "@aws-sdk/client-ecs"; - - -export module Aws { - export const client = async () => { - return new AwsClient({ - accessKeyId: Resource.AwsAccessKey.value, - secretAccessKey: Resource.AwsSecretKey.value, - region: "us-east-1", - }); - } - - export const EcsRunTask = fn(z.object({ - cluster: z.string(), - count: z.number(), - taskDefinition: z.string(), - launchType: z.enum(["EC2", "FARGATE"]), - overrides: z.object({ - containerOverrides: z.object({ - name: z.string(), - environment: z.object({ - name: z.string(), - value: z.string().or(z.number()) - }).array() - }).array() - }) - }), async (body) => { - - const c = await client(); - - const url = new URL(`https://ecs.${c.region}.amazonaws.com/`) - - const res = await c.fetch(url, { - method: "POST", - headers: { - "X-Amz-Target": "AmazonEC2ContainerServiceV20141113.RunTask", - "Content-Type": "application/x-amz-json-1.1", - }, - body: JSON.stringify(body) - }) - - return await res.json() as RunTaskCommandOutput - }) - - export const EcsDescribeTasks = fn(z.object({ tasks: z.string().array(), cluster: z.string() }), async (body) => { - const c = await client(); - - const url = new URL(`https://ecs.${c.region}.amazonaws.com/`) - - const res = await c.fetch(url, { - method: "POST", - headers: { - "X-Amz-Target": "AmazonEC2ContainerServiceV20141113.DescribeTasks", - "Content-Type": "application/x-amz-json-1.1", - }, - body: JSON.stringify(body) - }) - - return await res.json() as DescribeTasksCommandOutput - }) - - - export const EcsStopTask = fn(z.object({ - cluster: z.string().optional(), - reason: z.string().optional(), - task: z.string() - }), async (body) => { - const c = await client(); - - const url = new URL(`https://ecs.${c.region}.amazonaws.com/`) - - const res = await c.fetch(url, { - method: "POST", - headers: { - "X-Amz-Target": "AmazonEC2ContainerServiceV20141113.StopTask", - "Content-Type": "application/x-amz-json-1.1", - }, - body: JSON.stringify(body) - }) - - return await res.json() as StopTaskCommandOutput - }) - -} - - diff --git a/packages/core/src:old/common.ts b/packages/core/src:old/common.ts deleted file mode 100644 index a7ec75d8..00000000 --- a/packages/core/src:old/common.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { z } from "zod"; -import "zod-openapi/extend"; - -export module Common { - export const IdDescription = `Unique object identifier. -The format and length of IDs may change over time.`; -} \ No newline at end of file diff --git a/packages/core/src:old/database.ts b/packages/core/src:old/database.ts deleted file mode 100644 index c782219c..00000000 --- a/packages/core/src:old/database.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Resource } from "sst"; -import { init } from "@instantdb/admin"; -import schema from "../instant.schema"; - -const databaseClient = () => init({ - appId: Resource.InstantAppId.value, - adminToken: Resource.InstantAdminToken.value, - schema - }) - - -export default databaseClient \ No newline at end of file diff --git a/packages/core/src:old/email/index.ts b/packages/core/src:old/email/index.ts deleted file mode 100644 index 769f1b31..00000000 --- a/packages/core/src:old/email/index.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { LoopsClient } from "loops"; -import { Resource } from "sst/resource" -export namespace Email { - export const Client = () => new LoopsClient(Resource.LoopsApiKey.value); - - export async function send( - to: string, - body: string, - ) { - - try { - await Client().sendTransactionalEmail( - { - transactionalId: "cm58pdf8d03upb5ecirnmvrfb", - email: to, - dataVariables: { - logincode: body - } - } - ); - } catch (error) { - console.log("error sending email", error) - } - } - - export async function sendWelcome( - to: string, - name: string, - ) { - - try { - await Client().sendTransactionalEmail( - { - transactionalId: "cm61jrbbx02twlstfwfcywt5u", - email: to, - dataVariables: { - name - } - } - ); - } catch (error) { - console.log("error sending email", error) - } - } -} \ No newline at end of file diff --git a/packages/core/src:old/error.ts b/packages/core/src:old/error.ts deleted file mode 100644 index 9aab0425..00000000 --- a/packages/core/src:old/error.ts +++ /dev/null @@ -1,9 +0,0 @@ -export class VisibleError extends Error { - constructor( - public kind: "input" | "auth", - public code: string, - public message: string, - ) { - super(message); - } - } \ No newline at end of file diff --git a/packages/core/src:old/examples.ts b/packages/core/src:old/examples.ts deleted file mode 100644 index 6792355b..00000000 --- a/packages/core/src:old/examples.ts +++ /dev/null @@ -1,75 +0,0 @@ -export module Examples { - - export const User = { - id: "0bfcc712-df13-4454-81a8-fbee66eddca4", - email: "john@example.com", - }; - - export const Task = { - id: "0bfcc712-df13-4454-81a8-fbee66eddca4", - taskID: "b8302fca2d224d91ab342a2e4ab926d3", - type: "AWS" as const, //or "on-premises", - lastStatus: "RUNNING" as const, - healthStatus: "UNKNOWN" as const, - startedAt: '2025-01-09T01:56:23.902Z', - lastUpdated: '2025-01-09T01:56:23.902Z', - stoppedAt: '2025-01-09T04:46:23.902Z' - } - - export const Profile = { - id: "0bfcb712-df13-4454-81a8-fbee66eddca4", - username: "janedoe47", - status: "active" as const, - avatarUrl: "https://cdn.discordapp.com/avatars/xxxxxxx/xxxxxxx.png", - discriminator: 12, //it needs to be two digits - createdAt: '2025-01-04T11:56:23.902Z', - updatedAt: '2025-01-09T01:56:23.902Z' - } - - export const Subscription = { - id: "0bfcb712-df13-4454-81a8-fbee66eddca4", - checkoutID: "0bfcb712-df43-4454-81a8-fbee66eddca4", - // productID: "0bfcb712-df43-4454-81a8-fbee66eddca4", - // quantity: 1, - // frequency: "monthly" as const, - // next: '2025-01-09T01:56:23.902Z', - canceledAt: '2025-02-09T01:56:23.902Z' - } - - export const Team = { - id: "0bfcb712-df13-4454-81a8-fbee66eddca4", - // owner: true, - name: "Jane Doe's Games", - slug: "jane-does-games", - createdAt: '2025-01-04T11:56:23.902Z', - updatedAt: '2025-01-09T01:56:23.902Z' - } - - export const Machine = { - id: "0bfcb712-df13-4454-81a8-fbee66eddca4", - hostname: "DESKTOP-EUO8VSF", - fingerprint: "fc27f428f9ca47d4b41b70889ae0c62090", - createdAt: '2025-01-04T11:56:23.902Z', - deletedAt: '2025-01-09T01:56:23.902Z' - } - - export const Instance = { - id: "0bfcb712-df13-4454-81a8-fbee66eddca4", - hostname: "a955e059f05d", - createdAt: '2025-01-04T11:56:23.902Z', - lastActive: '2025-01-09T01:56:23.902Z' - } - - export const Game = { - id: '0bfcb712-df13-4454-81a8-fbee66eddca4', - name: "Control Ultimate Edition", - steamID: 870780, - } - - export const Session = { - id: "0bfcb712-df13-4454-81a8-fbee66eddca4", - public: true, - startedAt: '2025-01-04T11:56:23.902Z', - endedAt: '2025-01-04T12:36:23.902Z' - } -} \ No newline at end of file diff --git a/packages/core/src:old/game/index.ts b/packages/core/src:old/game/index.ts deleted file mode 100644 index 93e11282..00000000 --- a/packages/core/src:old/game/index.ts +++ /dev/null @@ -1,151 +0,0 @@ -// import { z } from "zod" -// import { fn } from "../utils"; -// import { Common } from "../common"; -// import { Examples } from "../examples"; -// import databaseClient from "../database" -// import { id as createID } from "@instantdb/admin"; -// import { groupBy, map, pipe, values } from "remeda" -// import { useCurrentDevice, useCurrentUser } from "../actor"; - -// export module Games { -// export const Info = z -// .object({ -// id: z.string().openapi({ -// description: Common.IdDescription, -// example: Examples.Game.id, -// }), -// name: z.string().openapi({ -// description: "A human-readable name for the game, used for easy identification.", -// example: Examples.Game.name, -// }), -// steamID: z.number().openapi({ -// description: "The Steam ID of the game, used to identify it during installation and runtime.", -// example: Examples.Game.steamID, -// }) -// }) -// .openapi({ -// ref: "Game", -// description: "Represents a Steam game that can be installed and played on a machine.", -// example: Examples.Game, -// }); - -// export type Info = z.infer; - -// export const create = fn(Info.pick({ name: true, steamID: true }), async (input) => { -// const id = createID() -// const db = databaseClient() -// const device = useCurrentDevice() - -// await db.transact( -// db.tx.games[id]!.update({ -// name: input.name, -// steamID: input.steamID, -// }).link({ machines: device.id }) -// ) -// // -// return id -// }) - -// export const list = async () => { -// const db = databaseClient() -// const user = useCurrentUser() - -// const query = { -// $users: { -// $: { where: { id: user.id } }, -// games: {} -// }, -// } - -// const res = await db.query(query) - -// const games = res.$users[0]?.games -// if (games && games.length > 0) { -// const result = pipe( -// games, -// groupBy(x => x.id), -// values(), -// map((group): Info => ({ -// id: group[0].id, -// name: group[0].name, -// steamID: group[0].steamID, -// })) -// ) -// return result -// } -// return null -// } - -// export const fromSteamID = fn(z.number(), async (steamID) => { -// const db = databaseClient() - -// const query = { -// games: { -// $: { -// where: { -// steamID, -// } -// } -// } -// } - -// const res = await db.query(query) - -// const games = res.games - -// if (games.length > 0) { -// const result = pipe( -// games, -// groupBy(x => x.id), -// values(), -// map((group): Info => ({ -// id: group[0].id, -// name: group[0].name, -// steamID: group[0].steamID, -// })) -// ) -// return result[0] -// } - -// return null -// }) - -// export const linkToCurrentUser = fn(z.string(), async (steamID) => { -// const user = useCurrentUser() -// const db = databaseClient() - -// await db.transact(db.tx.games[steamID]!.link({ owners: user.id })) - -// return "ok" -// }) - -// export const unLinkFromCurrentUser = fn(z.number(), async (steamID) => { -// const user = useCurrentUser() -// const db = databaseClient() - -// const query = { -// $users: { -// $: { where: { id: user.id } }, -// games: { -// $: { -// where: { -// steamID, -// } -// } -// } -// }, -// } - -// const res = await db.query(query) -// const games = res.$users[0]?.games -// if (games && games.length > 0) { -// const game = games[0] as Info -// await db.transact(db.tx.games[game.id]!.unlink({ owners: user.id })) - -// return "ok" -// } - -// return null -// }) - -// } \ No newline at end of file diff --git a/packages/core/src:old/instance/index.ts b/packages/core/src:old/instance/index.ts deleted file mode 100644 index 04a419f4..00000000 --- a/packages/core/src:old/instance/index.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { z } from "zod" -import { fn } from "../utils"; -import { Common } from "../common"; -import { Examples } from "../examples"; -import databaseClient from "../database" -import { id as createID } from "@instantdb/admin"; -import { groupBy, map, pipe, values } from "remeda" - -export module Instances { - export const Info = z - .object({ - id: z.string().openapi({ - description: Common.IdDescription, - example: Examples.Instance.id, - }), - hostname: z.string().openapi({ - description: "The container's hostname", - example: Examples.Instance.hostname, - }), - createdAt: z.string().or(z.number()).openapi({ - description: "The time this instances was registered on the network", - example: Examples.Instance.createdAt, - }), - lastActive: z.string().or(z.number()).optional().openapi({ - description: "The time this instance was last seen on the network", - example: Examples.Instance.lastActive, - }) - }) - .openapi({ - ref: "Instance", - description: "Represents a running container that is connected to the Nestri network..", - example: Examples.Instance, - }); - - export type Info = z.infer; - export const create = fn(z.object({ hostname: z.string(), teamID: z.string() }), async (input) => { - const id = createID() - const now = new Date().toISOString() - const db = databaseClient() - await db.transact( - db.tx.instances[id]!.update({ - hostname: input.hostname, - createdAt: now, - }).link({ owners: input.teamID }) - ) - - return "ok" - }) - - export const fromTeamID = fn(z.string(), async (teamID) => { - const db = databaseClient() - - const query = { - instances: { - $: { - where: { - owners: teamID - } - } - } - } - - const res = await db.query(query) - const data = res.instances - - if (data && data.length > 0) { - const result = pipe( - data, - groupBy(x => x.id), - values(), - map((group): Info => ({ - id: group[0].id, - lastActive: group[0].lastActive, - hostname: group[0].hostname, - createdAt: group[0].createdAt - })) - ) - return result - } - - return null - }) -} \ No newline at end of file diff --git a/packages/core/src:old/machine/index.ts b/packages/core/src:old/machine/index.ts deleted file mode 100644 index 06403421..00000000 --- a/packages/core/src:old/machine/index.ts +++ /dev/null @@ -1,232 +0,0 @@ -// import { z } from "zod" -// import { fn } from "../utils"; -// import { Games } from "../game" -// import { Common } from "../common"; -// import { Examples } from "../examples"; -// import { useCurrentUser } from "../actor"; -// import databaseClient from "../database" -// import { id as createID } from "@instantdb/admin"; -// import { groupBy, map, pipe, values } from "remeda" -// export module Machines { -// export const Info = z -// .object({ -// id: z.string().openapi({ -// description: Common.IdDescription, -// example: Examples.Machine.id, -// }), -// hostname: z.string().openapi({ -// description: "The Linux hostname that identifies this machine", -// example: Examples.Machine.hostname, -// }), -// fingerprint: z.string().openapi({ -// description: "A unique identifier derived from the machine's Linux machine ID.", -// example: Examples.Machine.fingerprint, -// }), -// createdAt: z.string().or(z.number()).openapi({ -// description: "Represents a machine running on the Nestri network, containing its identifying information and metadata.", -// example: Examples.Machine.createdAt, -// }) -// }) -// .openapi({ -// ref: "Machine", -// description: "Represents a physical or virtual machine connected to the Nestri network..", -// example: Examples.Machine, -// }); - -// export type Info = z.infer; - -// export const create = fn(Info.pick({ fingerprint: true, hostname: true }), async (input) => { -// const id = createID() -// const now = new Date().toISOString() -// const db = databaseClient() -// await db.transact( -// db.tx.machines[id]!.update({ -// fingerprint: input.fingerprint, -// hostname: input.hostname, -// createdAt: now, -// //Just in case it had been previously deleted -// deletedAt: undefined -// }) -// ) - -// return id -// }) - -// // export const fromID = fn(z.string(), async (id) => { -// const db = databaseClient() - -// const query = { -// machines: { -// $: { -// where: { -// id: id, -// deletedAt: { $isNull: true } -// } -// } -// } -// } - -// const res = await db.query(query) -// const machines = res.machines - -// if (machines && machines.length > 0) { -// const result = pipe( -// machines, -// groupBy(x => x.id), -// values(), -// map((group): Info => ({ -// id: group[0].id, -// fingerprint: group[0].fingerprint, -// hostname: group[0].hostname, -// createdAt: group[0].createdAt -// })) -// ) -// return result -// } - -// return null -// }) - -// export const installedGames = fn(z.string(), async (id) => { -// const db = databaseClient() - -// const query = { -// machines: { -// $: { -// where: { -// id: id, -// deletedAt: { $isNull: true } -// } -// }, -// games: {} -// } -// } - -// const res = await db.query(query) -// const machines = res.machines - -// if (machines && machines.length > 0) { -// const games = machines[0]?.games as any -// if (games.length > 0) { -// return games as Games.Info[] -// } -// return null -// } - -// return null -// }) - -// export const fromFingerprint = fn(z.string(), async (input) => { -// const db = databaseClient() - -// const query = { -// machines: { -// $: { -// where: { -// fingerprint: input, -// deletedAt: { $isNull: true } -// } -// } -// } -// } - -// const res = await db.query(query) - -// const machines = res.machines - -// if (machines.length > 0) { -// const result = pipe( -// machines, -// groupBy(x => x.id), -// values(), -// map((group): Info => ({ -// id: group[0].id, -// fingerprint: group[0].fingerprint, -// hostname: group[0].hostname, -// createdAt: group[0].createdAt -// })) -// ) -// return result[0] -// } - -// return null -// }) - -// export const list = async () => { -// const user = useCurrentUser() -// const db = databaseClient() - -// const query = { -// $users: { -// $: { where: { id: user.id } }, -// machines: { -// $: { -// where: { -// deletedAt: { $isNull: true } -// } -// } -// } -// }, -// } - -// const res = await db.query(query) - -// const machines = res.$users[0]?.machines -// if (machines && machines.length > 0) { -// const result = pipe( -// machines, -// groupBy(x => x.id), -// values(), -// map((group): Info => ({ -// id: group[0].id, -// fingerprint: group[0].fingerprint, -// hostname: group[0].hostname, -// createdAt: group[0].createdAt -// })) -// ) -// return result -// } -// return null -// } - -// export const linkToCurrentUser = fn(z.string(), async (id) => { -// const user = useCurrentUser() -// const db = databaseClient() - -// await db.transact(db.tx.machines[id]!.link({ owner: user.id })) - -// return "ok" -// }) - -// export const unLinkFromCurrentUser = fn(z.string(), async (id) => { -// const user = useCurrentUser() -// const db = databaseClient() -// const now = new Date().toISOString() - -// const query = { -// $users: { -// $: { where: { id: user.id } }, -// machines: { -// $: { -// where: { -// id, -// deletedAt: { $isNull: true } -// } -// } -// } -// }, -// } - -// const res = await db.query(query) -// const machines = res.$users[0]?.machines -// if (machines && machines.length > 0) { -// const machine = machines[0] as Info -// await db.transact(db.tx.machines[machine.id]!.update({ deletedAt: now })) - -// return "ok" -// } - -// return null -// }) - -// } \ No newline at end of file diff --git a/packages/core/src:old/profile/index.ts b/packages/core/src:old/profile/index.ts deleted file mode 100644 index b17a5c0c..00000000 --- a/packages/core/src:old/profile/index.ts +++ /dev/null @@ -1,412 +0,0 @@ -import { z } from "zod" -import { fn } from "../utils"; -import { Common } from "../common"; -import { Examples } from "../examples"; -import databaseClient from "../database"; -import { groupBy, map, pipe, values } from "remeda" -import { id as createID, } from "@instantdb/admin"; -import { useCurrentUser } from "../actor"; - -export const userStatus = z.enum([ - "active", //online and playing a game - "idle", //online and not playing - "offline", -]); - -export module Profiles { - const MAX_ATTEMPTS = 50; - - export const Info = z - .object({ - id: z.string().openapi({ - description: Common.IdDescription, - example: Examples.Machine.id, - }), - username: z.string().openapi({ - description: "The user's unique username", - example: Examples.Profile.username, - }), - avatarUrl: z.string().or(z.undefined()).openapi({ - description: "The url to the profile picture.", - example: Examples.Profile.username, - }), - status: userStatus.openapi({ - description: "Whether the user is active, idle or offline", - example: Examples.Profile.status - }), - discriminator: z.string().or(z.number()).openapi({ - description: "The number discriminator for each username", - example: Examples.Profile.discriminator, - }), - createdAt: z.string().or(z.number()).openapi({ - description: "The time when this profile was first created", - example: Examples.Profile.createdAt, - }), - updatedAt: z.string().or(z.number()).openapi({ - description: "The time when this profile was last edited", - example: Examples.Profile.updatedAt, - }) - }) - .openapi({ - ref: "Profile", - description: "Represents a profile of a user on Nestri", - example: Examples.Profile, - }); - - export type Info = z.infer; - export type userStatus = z.infer; - - export const sanitizeUsername = (username: string): string => { - // Remove spaces and numbers - return username.replace(/[\s0-9]/g, ''); - }; - - export const generateDiscriminator = (): string => { - return Math.floor(Math.random() * 100).toString().padStart(2, '0'); - }; - - export const isValidDiscriminator = (discriminator: string): boolean => { - return /^\d{2}$/.test(discriminator); - }; - - export const fromUsername = fn(z.string(), async (input) => { - const sanitizedUsername = sanitizeUsername(input); - - const db = databaseClient() - - const query = { - profiles: { - $: { - where: { - username: sanitizedUsername, - } - } - } - } - - const res = await db.query(query) - - const profiles = res.profiles - - if (!profiles || profiles.length == 0) { - - return null - } - - return pipe( - profiles, - groupBy(x => x.id), - values(), - map((group): Info => ({ - id: group[0].id, - username: group[0].username, - createdAt: group[0].createdAt, - discriminator: group[0].discriminator, - updatedAt: group[0].updatedAt, - status: group[0].status as userStatus - })) - ) - }) - - export const findAvailableDiscriminator = fn(z.string(), async (input) => { - const db = databaseClient() - const username = sanitizeUsername(input); - - for (let i = 0; i < MAX_ATTEMPTS; i++) { - const discriminator = generateDiscriminator(); - const query = { - profiles: { - $: { - where: { - username, - discriminator - } - } - } - } - const res = await db.query(query) - const profiles = res.profiles - if (profiles.length === 0) { - return discriminator; - } - } - return null; // No available discriminators - - }) - - export const create = fn(z.object({ username: z.string(), customDiscriminator: z.string().optional(), avatarUrl: z.string().optional(), owner: z.string() }), async (input) => { - const username = sanitizeUsername(input.username); - - const db = databaseClient() - const id = createID() - const now = new Date().toISOString() - - let discriminator: string | null; - if (input.customDiscriminator) { - if (!isValidDiscriminator(input.customDiscriminator)) { - console.error('Invalid discriminator format') - return null - // throw new Error('Invalid discriminator format'); - } - - const query = { - profiles: { - $: { - where: { - username, - discriminator: input.customDiscriminator - } - } - } - } - - const res = await db.query(query) - const profiles = res.profiles - if (profiles.length != 0) { - console.error("Username and discriminator combination already taken ") - return null - // throw new Error('Username and discriminator combination already taken'); - } - - discriminator = input.customDiscriminator - } else { - // Generate a random available discriminator - discriminator = await findAvailableDiscriminator(username); - - if (!discriminator) { - console.error("No available discriminators for this username ") - return null - // throw new Error('No available discriminators for this username'); - } - } - - return await db.transact( - db.tx.profiles[id]!.update({ - username, - avatarUrl: input.avatarUrl, - createdAt: now, - updatedAt: now, - discriminator, - status: "idle" - }).link({ owner: input.owner }) - ) - }) - - export const getFullUsername = async (username: string) => { - const db = databaseClient() - - const query = { - profiles: { - $: { - where: { - username, - } - } - } - } - const res = await db.query(query) - const profiles = res.profiles - - if (!profiles || profiles.length === 0) { - console.error('User not found') - return null - // throw new Error('User not found'); - } - - return `${profiles[0]?.username}#${profiles[0]?.discriminator}`; - } - - export const fromOwnerID = async (ownerID: string) => { - try { - - const db = databaseClient() - - const query = { - profiles: { - $: { - where: { - owner: ownerID - } - }, - } - } - const res = await db.query(query) - - const profiles = res.profiles - - if (!profiles || profiles.length === 0) { - throw new Error("No profiles were found"); - } - - const profile = pipe( - profiles, - groupBy(x => x.id), - values(), - map((group): Info => ({ - id: group[0].id, - username: group[0].username, - createdAt: group[0].createdAt, - updatedAt: group[0].updatedAt, - avatarUrl: group[0].avatarUrl, - discriminator: group[0].discriminator, - status: group[0].status as userStatus - })) - ) - - return profile[0] - } catch (error) { - console.log("user fromOwnerID", error) - return null - } - } - - export const fromID = async (id: string) => { - try { - - const db = databaseClient() - - const query = { - profiles: { - $: { - where: { - id - } - }, - } - } - const res = await db.query(query) - - const profiles = res.profiles - - if (!profiles || profiles.length === 0) { - throw new Error("No profiles were found"); - } - - const profile = pipe( - profiles, - groupBy(x => x.id), - values(), - map((group): Info => ({ - id: group[0].id, - username: group[0].username, - createdAt: group[0].createdAt, - updatedAt: group[0].updatedAt, - avatarUrl: group[0].avatarUrl, - discriminator: group[0].discriminator, - status: group[0].status as userStatus - })) - ) - - return profile[0] - } catch (error) { - console.log("user fromID", error) - return null - } - } - - export const fromIDToOwner = async (id: string) => { - try { - - const db = databaseClient() - - const query = { - profiles: { - $: { - where: { - id - } - }, - } - } - const res = await db.query(query) - - const profiles = res.profiles as any - - if (!profiles || profiles.length === 0) { - throw new Error("No profiles were found"); - } - - return profiles[0]!.owner as string - } catch (error) { - console.log("user fromID", error) - return null - } - } - export const getCurrentProfile = async () => { - const user = useCurrentUser() - const currentProfile = await fromOwnerID(user.id); - - return currentProfile - } - - export const setStatus = fn(userStatus, async (status) => { - try { - const user = useCurrentUser() - const db = databaseClient() - - const now = new Date().toISOString() - - await db.transact( - db.tx.profiles[user.id]!.update({ - status, - updatedAt: now - }) - ) - } catch (error) { - console.log("user setStatus error", error) - return null - } - }) - - export const list = async () => { - try { - const db = databaseClient() - // const ago = new Date(Date.now() - (60 * 1000 * 30)).toISOString() - const ago = new Date(Date.now() - (24 * 60 * 60 * 1000)).toISOString() - - const query = { - profiles: { - $: { - limit: 10, - where: { - updatedAt: { $gt: ago }, - }, - order: { - updatedAt: "desc" as const, - }, - } - } - } - - const res = await db.query(query) - - const profiles = res.profiles - - if (!profiles || profiles.length === 0) { - throw new Error("No profiles were found"); - - } - - const result = pipe( - profiles, - groupBy(x => x.id), - values(), - map((group): Info => ({ - id: group[0].id, - username: group[0].username, - createdAt: group[0].createdAt, - updatedAt: group[0].updatedAt, - avatarUrl: group[0].avatarUrl, - discriminator: group[0].discriminator, - status: group[0].status as userStatus - })) - ) - - return result - - } catch (error) { - console.log("user list error", error) - return null - } - } -} \ No newline at end of file diff --git a/packages/core/src:old/session/index.ts b/packages/core/src:old/session/index.ts deleted file mode 100644 index d15d0528..00000000 --- a/packages/core/src:old/session/index.ts +++ /dev/null @@ -1,251 +0,0 @@ -import { z } from "zod" -import { fn } from "../utils"; -import { Common } from "../common"; -import { Examples } from "../examples"; -import databaseClient from "../database" -import { useCurrentUser } from "../actor"; -import { groupBy, map, pipe, values } from "remeda" -import { id as createID } from "@instantdb/admin"; - -export module Sessions { - export const Info = z - .object({ - id: z.string().openapi({ - description: Common.IdDescription, - example: Examples.Session.id, - }), - public: z.boolean().openapi({ - description: "If true, the session is publicly viewable by all users. If false, only authorized users can access it", - example: Examples.Session.public, - }), - endedAt: z.string().or(z.number()).or(z.undefined()).openapi({ - description: "The timestamp indicating when this session was completed or terminated. Null if session is still active.", - example: Examples.Session.endedAt, - }), - startedAt: z.string().or(z.number()).openapi({ - description: "The timestamp indicating when this session started.", - example: Examples.Session.startedAt, - }) - }) - .openapi({ - ref: "Session", - description: "Represents a single game play session, tracking its lifetime and accessibility settings.", - example: Examples.Session, - }); - - export type Info = z.infer; - - export const create = fn(z.object({ public: z.boolean() }), async (input) => { - try { - const id = createID() - const db = databaseClient() - const user = useCurrentUser() - const now = new Date().toISOString() - - await db.transact( - db.tx.sessions[id]!.update({ - public: input.public, - startedAt: now, - }).link({ owner: user.id }) - ) - - return id - } catch (err) { - return null - } - }) - - export const getActive = async () => { - try { - const db = databaseClient() - - const query = { - sessions: { - $: { - where: { - endedAt: { $isNull: true } - } - } - } - } - - const res = await db.query(query) - - const sessions = res.sessions - if (!sessions || sessions.length === 0) { - throw new Error("No active sessions found") - } - - const result = pipe( - sessions, - groupBy(x => x.id), - values(), - map((group): Info => ({ - id: group[0].id, - endedAt: group[0].endedAt, - startedAt: group[0].startedAt, - public: group[0].public, - })) - ) - - return result - - } catch (error) { - return null - } - } - - export const fromID = fn(z.string(), async (id) => { - try { - const db = databaseClient() - - const query = { - sessions: { - $: { - where: { - id: id, - } - } - } - } - - const res = await db.query(query) - const sessions = res.sessions - - if (!sessions || sessions.length === 0) { - throw new Error("No sessions were found"); - } - - const result = pipe( - sessions, - groupBy(x => x.id), - values(), - map((group): Info => ({ - id: group[0].id, - endedAt: group[0].endedAt, - startedAt: group[0].startedAt, - public: group[0].public, - })) - ) - return result - } catch (err) { - console.log("sessions error", err) - return null - } - }) - - export const fromTaskID = fn(z.string(), async (taskID) => { - try { - const db = databaseClient() - - const query = { - sessions: { - $: { - where: { - task: taskID, - endedAt: { $isNull: true } - } - } - } - } - - const res = await db.query(query) - const sessions = res.sessions - - if (!sessions || sessions.length === 0) { - throw new Error("No sessions were found"); - } - console.log("sessions", sessions) - - const result = pipe( - sessions, - groupBy(x => x.id), - values(), - map((group): Info => ({ - id: group[0].id, - endedAt: group[0].endedAt, - startedAt: group[0].startedAt, - public: group[0].public, - })) - ) - return result[0] - } catch (err) { - console.log("sessions error", err) - return null - } - }) - - export const end = fn(z.string(), async (id) => { - const user = useCurrentUser() - try { - const db = databaseClient() - const now = new Date().toISOString() - - const query = { - sessions: { - $: { - where: { - owner: user.id, - id, - } - } - }, - } - - const res = await db.query(query) - const sessions = res.sessions - if (!sessions || sessions.length === 0) { - throw new Error("No sessions were found"); - } - - await db.transact(db.tx.sessions[sessions[0]!.id]!.update({ endedAt: now })) - - return "ok" - - } catch (error) { - - return null - } - - }) - - export const fromOwnerID = fn(z.string(), async (id) => { - try { - const db = databaseClient() - - const query = { - sessions: { - $: { - where: { - owner: id, - endedAt: { $isNull: true } - } - } - } - } - - const res = await db.query(query) - const sessions = res.sessions - - if (!sessions || sessions.length === 0) { - throw new Error("No sessions were found"); - } - - const result = pipe( - sessions, - groupBy(x => x.id), - values(), - map((group): Info => ({ - id: group[0].id, - endedAt: group[0].endedAt, - startedAt: group[0].startedAt, - public: group[0].public, - })) - ) - return result[0] - } catch (err) { - console.log("session owner error", err) - return null - } - }) -} \ No newline at end of file diff --git a/packages/core/src:old/subscription/index.ts b/packages/core/src:old/subscription/index.ts deleted file mode 100644 index 75ef5acd..00000000 --- a/packages/core/src:old/subscription/index.ts +++ /dev/null @@ -1,205 +0,0 @@ -import { z } from "zod"; -import databaseClient from "../database" -import { fn } from "../utils"; -import { groupBy, map, pipe, values } from "remeda" -import { Common } from "../common"; -import { Examples } from "../examples"; -import { useCurrentUser } from "../actor"; -import { id as createID } from "@instantdb/admin"; -import { Email } from "../email"; -import { Profiles } from "../profile"; - -export const SubscriptionFrequency = z.enum([ - "fixed", - "daily", - "weekly", - "monthly", - "yearly", -]); - -export type SubscriptionFrequency = z.infer; - -export namespace Subscriptions { - export const Info = z - .object({ - id: z.string().openapi({ - description: Common.IdDescription, - example: Examples.Subscription.id, - }), - checkoutID: z.string().openapi({ - description: "The polar.sh checkout id", - example: Examples.Subscription.checkoutID, - }), - // productID: z.string().openapi({ - // description: "ID of the product being subscribed to.", - // example: Examples.Subscription.productID, - // }), - // quantity: z.number().int().openapi({ - // description: "Quantity of the subscription.", - // example: Examples.Subscription.quantity, - // }), - // frequency: SubscriptionFrequency.openapi({ - // description: "Frequency of the subscription.", - // example: Examples.Subscription.frequency, - // }), - // next: z.string().or(z.number()).openapi({ - // description: "Next billing date for the subscription.", - // example: Examples.Subscription.next, - // }), - canceledAt: z.string().or(z.number()).optional().openapi({ - description: "Cancelled date for the subscription.", - example: Examples.Subscription.canceledAt, - }), - }) - .openapi({ - ref: "Subscription", - description: "Subscription to a Nestri product.", - example: Examples.Subscription, - }); - - export type Info = z.infer; - - export const list = fn(z.string().optional(), async (userID) => { - const db = databaseClient() - const user = userID ? userID : useCurrentUser().id - - const query = { - subscriptions: { - $: { - where: { - owner: user, - canceledAt: { $isNull: true } - } - }, - } - } - - const res = await db.query(query) - - const response = res.subscriptions - if (!response || response.length === 0) { - return null - } - - const result = pipe( - response, - groupBy(x => x.id), - values(), - map((group): Info => ({ - id: group[0].id, - // next: group[0].next, - // frequency: group[0].frequency as any, - // quantity: group[0].quantity, - // productID: group[0].productID, - checkoutID: group[0].checkoutID, - })) - ) - - return result - }) - - export const create = fn(Info.omit({ id: true, canceledAt: true }), async (input) => { - // const id = createID() - const id = createID() - const db = databaseClient() - const user = useCurrentUser() - - //Use the polar.sh ID - await db.transact(db.tx.subscriptions[id]!.update({ - // next: input.next, - // frequency: input.frequency, - // quantity: input.quantity, - checkoutID: input.checkoutID, - }).link({ owner: user.id })) - const res = await db.auth.getUser({ id: user.id }) - const profile = await Profiles.fromOwnerID(user.id) - if (profile) { - await Email.sendWelcome(res.email, profile.username) - } - - }) - - export const remove = fn(z.string(), async (id) => { - const db = databaseClient() - - await db.transact(db.tx.subscriptions[id]!.update({ - canceledAt: new Date().toISOString() - })) - }) - - export const fromID = fn(z.string(), async (id) => { - const db = databaseClient() - const user = useCurrentUser() - const query = { - subscriptions: { - $: { - where: { - id, - //Make sure they can only get subscriptions they own - owner: user.id, - canceledAt: { $isNull: true } - } - }, - } - } - - const res = await db.query(query) - - const response = res.subscriptions - if (!response || response.length === 0) { - return null - } - - const result = pipe( - response, - groupBy(x => x.id), - values(), - map((group): Info => ({ - id: group[0].id, - checkoutID: group[0].checkoutID, - // next: group[0].next, - // frequency: group[0].frequency as any, - // quantity: group[0].quantity, - // productID: group[0].productID, - })) - ) - - return result[0] - }) - - export const fromCheckoutID = fn(z.string(), async (id) => { - const db = databaseClient() - const user = useCurrentUser() - const query = { - subscriptions: { - $: { - where: { - id, - //Make sure they can only get subscriptions they own - checkoutID: id, - canceledAt: { $isNull: true } - } - }, - } - } - - const res = await db.query(query) - - const response = res.subscriptions - if (!response || response.length === 0) { - return null - } - - const result = pipe( - response, - groupBy(x => x.id), - values(), - map((group): Info => ({ - id: group[0].id, - checkoutID: group[0].checkoutID, - })) - ) - - return result[0] - }) -} \ No newline at end of file diff --git a/packages/core/src:old/task/index.ts b/packages/core/src:old/task/index.ts deleted file mode 100644 index c15b0372..00000000 --- a/packages/core/src:old/task/index.ts +++ /dev/null @@ -1,331 +0,0 @@ -import { z } from "zod"; -import { fn } from "../utils"; -import { Resource } from "sst"; -import { Aws } from "../aws/client"; -import { Common } from "../common"; -import { Examples } from "../examples"; -import databaseClient from "../database" -import { useCurrentUser } from "../actor"; -import { id as createID } from "@instantdb/admin"; -import { groupBy, map, pipe, values } from "remeda" -import { Sessions } from "../session"; - -export const lastStatus = z.enum([ - "RUNNING", - "PENDING", - "UNKNOWN", - "STOPPED", -]); - -export const taskType = z.enum([ - "AWS", - "ON_PREMISES", - "UNKNOWN" -]); - -export const healthStatus = z.enum([ - "HEALTHY", - "UNHEALTHY", - "UNKNOWN", -]); - -export type taskType = z.infer; -export type lastStatus = z.infer; -export type healthStatus = z.infer; - -export module Tasks { - export const Info = z - .object({ - id: z.string().openapi({ - description: Common.IdDescription, - example: Examples.Task.id, - }), - type: taskType.openapi({ - description: "Where this task is hosted on", - example: Examples.Task.type, - }), - taskID: z.string().openapi({ - description: "The id of this task as seen on AWS", - example: Examples.Task.taskID, - }), - startedAt: z.string().or(z.number()).openapi({ - description: "The time this task was started", - example: Examples.Task.startedAt, - }), - lastUpdated: z.string().or(z.number()).openapi({ - description: "The time the information about this task was last updated", - example: Examples.Task.lastUpdated, - }), - stoppedAt: z.string().or(z.number()).optional().openapi({ - description: "The time this task was stopped or quit", - example: Examples.Task.lastUpdated, - }), - lastStatus: lastStatus.openapi({ - description: "The last registered status of this task", - example: Examples.Task.lastStatus, - }), - healthStatus: healthStatus.openapi({ - description: "The health status of this task", - example: Examples.Task.healthStatus, - }) - }) - .openapi({ - ref: "Subscription", - description: "Subscription to a Nestri product.", - example: Examples.Task, - }); - - export type Info = z.infer; - - export const list = async () => { - const db = databaseClient() - const user = useCurrentUser() - - try { - const query = { - tasks: { - $: { - where: { - stoppedAt: { $isNull: true }, - owner: user.id - } - }, - } - } - - const data = await db.query(query) - - const response = data.tasks - if (!response || response.length === 0) { - throw new Error("No task for this user were found"); - } - - const result = pipe( - response, - groupBy(x => x.id), - values(), - map((group): Info => ({ - id: group[0].id, - taskID: group[0].taskID, - type: group[0].type as taskType, - lastStatus: group[0].lastStatus as lastStatus, - healthStatus: group[0].healthStatus as healthStatus, - startedAt: group[0].startedAt, - stoppedAt: group[0].stoppedAt, - lastUpdated: group[0].lastUpdated, - })) - ) - - return result - } catch (e) { - return null - } - } - - export const create = async () => { - const user = useCurrentUser() - - try { - - //TODO: Use a simpler way to set the session ID - // const sessionID = createID() - - const sessionID = await Sessions.create({ public: true }) - if (!sessionID) throw new Error("No session id was given"); - - const run = await Aws.EcsRunTask({ - count: 1, - cluster: Resource.NestriGPUCluster.value, - taskDefinition: Resource.NestriGPUTask.value, - launchType: "EC2", - overrides: { - containerOverrides: [ - { - name: "nestri", - environment: [ - { - name: "NESTRI_ROOM", - value: sessionID - } - ] - } - ] - } - }) - - if (!run.tasks || run.tasks.length === 0) { - throw new Error(`No tasks were started`); - } - - // Extract task details - const task = run.tasks[0]; - const taskArn = task?.taskArn!; - const taskId = taskArn.split('/').pop()!; // Extract task ID from ARN - const taskStatus = task?.lastStatus; - const taskHealthStatus = task?.healthStatus; - const startedAt = task?.startedAt!; - - const id = createID() - const db = databaseClient() - const now = new Date().toISOString() - await db.transact(db.tx.tasks[id]!.update({ - taskID: taskId, - type: "AWS", - healthStatus: taskHealthStatus ? taskHealthStatus.toString() : "UNKNOWN", - startedAt: startedAt ? startedAt.toISOString() : now, - lastStatus: taskStatus, - lastUpdated: now, - }).link({ owner: user.id, sessions: sessionID })) - - return id - } catch (e) { - console.error("error", e) - return null - } - } - - export const fromID = fn(z.string(), async (taskID) => { - const db = databaseClient() - try { - const query = { - tasks: { - $: { - where: { - id: taskID, - stoppedAt: { $isNull: true } - } - }, - } - } - - const data = await db.query(query) - - const response = data.tasks - if (!response || response.length === 0) { - throw new Error("No task with the given id was found"); - } - - const result = pipe( - response, - groupBy(x => x.id), - values(), - map((group): Info => ({ - id: group[0].id, - taskID: group[0].taskID, - type: group[0].type as taskType, - lastStatus: group[0].lastStatus as lastStatus, - healthStatus: group[0].healthStatus as healthStatus, - startedAt: group[0].startedAt, - stoppedAt: group[0].stoppedAt, - lastUpdated: group[0].lastUpdated, - })) - ) - - return result[0] - - } catch (error) { - return null - } - }) - - export const update = fn(z.string(), async (taskID) => { - try { - const db = databaseClient() - - const query = { - tasks: { - $: { - where: { - id: taskID, - stoppedAt: { $isNull: true } - } - }, - } - } - - const data = await db.query(query) - - const response = data.tasks - if (!response || response.length === 0) { - throw new Error("No task with the given taskID was found"); - } - - const now = new Date().toISOString() - const describeResponse = await Aws.EcsDescribeTasks({ - tasks: [response[0]!.taskID], - cluster: Resource.NestriGPUCluster.value - }) - - if (!describeResponse.tasks || describeResponse.tasks.length === 0) { - throw new Error("No tasks were found"); - } - - const task = describeResponse.tasks[0]! - - const updatedDb = { - healthStatus: task.healthStatus ? task.healthStatus : "UNKNOWN", - lastStatus: task.lastStatus ? task.lastStatus : "UNKNOWN", - lastUpdated: now, - } - - await db.transact(db.tx.tasks[response[0]!.id]!.update({ - ...updatedDb - })) - - const updatedRes = [{ ...response[0]!, ...updatedDb }] - - const result = pipe( - updatedRes, - groupBy(x => x.id), - values(), - map((group): Info => ({ - id: group[0].id, - taskID: group[0].taskID, - type: group[0].type as taskType, - lastStatus: group[0].lastStatus as lastStatus, - healthStatus: group[0].healthStatus as healthStatus, - startedAt: group[0].startedAt, - stoppedAt: group[0].stoppedAt, - lastUpdated: group[0].lastUpdated, - })) - ) - - return result - - } catch (error) { - console.error("update error", error) - return null - } - }) - - export const stop = fn(z.object({ taskID: z.string(), id: z.string() }), async (input) => { - const db = databaseClient() - const now = new Date().toISOString() - try { - //TODO:Check whether they own this task first - - const stopResponse = await Aws.EcsStopTask({ - task: input.taskID, - cluster: Resource.NestriGPUCluster.value, - reason: "Client requested a shutdown" - }) - - if (!stopResponse.task) { - throw new Error(`No task was stopped`); - } - - await db.transact(db.tx.tasks[input.id]!.update({ - stoppedAt: now, - lastUpdated: now, - lastStatus: "STOPPED", - healthStatus: "UNKNOWN" - })) - - return "ok" - - } catch (error) { - console.error("stop error", error) - return null - } - }) -} \ No newline at end of file diff --git a/packages/core/src:old/team/index.ts b/packages/core/src:old/team/index.ts deleted file mode 100644 index f472be54..00000000 --- a/packages/core/src:old/team/index.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { z } from "zod"; -import databaseClient from "../database" -import { fn } from "../utils"; -import { groupBy, map, pipe, values } from "remeda" -import { Common } from "../common"; -import { Examples } from "../examples"; -import { useCurrentUser } from "../actor"; -import { id as createID } from "@instantdb/admin"; - -export namespace Teams { - export const Info = z - .object({ - id: z.string().openapi({ - description: Common.IdDescription, - example: Examples.Team.id, - }), - name: z.string().openapi({ - description: "Name of the team", - example: Examples.Team.name, - }), - createdAt: z.string().or(z.number()).openapi({ - description: "The time when this team was first created", - example: Examples.Team.createdAt, - }), - updatedAt: z.string().or(z.number()).openapi({ - description: "The time when this team was last edited", - example: Examples.Team.updatedAt, - }), - // owner: z.boolean().openapi({ - // description: "Whether this team is owned by this user", - // example: Examples.Team.owner, - // }), - slug: z.string().openapi({ - description: "This is the unique name identifier for the team", - example: Examples.Team.slug - }) - }) - .openapi({ - ref: "Team", - description: "A group of users sharing the same machines for gaming.", - example: Examples.Team, - }); - - export type Info = z.infer; - - export const list = async () => { - const db = databaseClient() - const user = useCurrentUser() - - const query = { - teams: { - $: { - where: { - members: user.id, - deletedAt: { $isNull: true } - } - }, - } - } - - const res = await db.query(query) - - const teams = res.teams - if (!teams || teams.length === 0) { - return null - } - - const result = pipe( - teams, - groupBy(x => x.id), - values(), - map((group): Info => ({ - id: group[0].id, - name: group[0].name, - createdAt: group[0].createdAt, - updatedAt: group[0].updatedAt, - slug: group[0].slug, - //@ts-expect-error - owner: group[0].owner === user.id - })) - ) - - return result - } - - - export const fromSlug = fn(z.string(), async (slug) => { - const db = databaseClient() - - const query = { - teams: { - $: { - where: { - slug, - deletedAt: { $isNull: true } - } - }, - } - } - - const res = await db.query(query) - - const teams = res.teams - if (!teams || teams.length === 0) { - return null - } - - const result = pipe( - teams, - groupBy(x => x.id), - values(), - map((group): Info => ({ - id: group[0].id, - name: group[0].name, - slug: group[0].slug, - createdAt: group[0].createdAt, - updatedAt: group[0].updatedAt, - // owner: group[0].owner === user.id - })) - ) - - return result[0] - }) - - export const create = fn(Info.pick({ name: true, slug: true }), async (input) => { - const id = createID() - const db = databaseClient() - const user = useCurrentUser() - const now = new Date().toISOString() - - await db.transact(db.tx.teams[id]!.update({ - name: input.name, - slug: input.slug, - createdAt: now, - updatedAt: now, - }).link({ owner: user.id, members: user.id })) - - return id - }) - - export const remove = fn(z.string(), async (id) => { - const db = databaseClient() - const now = new Date().toISOString() - - await db.transact(db.tx.teams[id]!.update({ - deletedAt: now - })) - - return "ok" - }) - - export const invite = fn(z.object({email:z.string(), id: z.string()}), async (input) => { - //TODO: - // const db = databaseClient() - // const now = new Date().toISOString() - - // await db.transact(db.tx.teams[id]!.update({ - // deletedAt: now - // })) - - return "ok" - }) - -} \ No newline at end of file diff --git a/packages/core/src:old/types.ts b/packages/core/src:old/types.ts deleted file mode 100644 index 2e8fc014..00000000 --- a/packages/core/src:old/types.ts +++ /dev/null @@ -1,17 +0,0 @@ -export interface CloudflareCF { - colo: string; - continent: string; - country: string, - city: string; - region: string; - longitude: number; - latitude: number; - metroCode: string; - postalCode: string; - timezone: string; - regionCode: number; -} - -export interface CFRequest extends Request { - cf: CloudflareCF -} \ No newline at end of file diff --git a/packages/core/src:old/user/index.ts b/packages/core/src:old/user/index.ts deleted file mode 100644 index 278fbbcc..00000000 --- a/packages/core/src:old/user/index.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { z } from "zod"; -import databaseClient from "../database" -import { fn } from "../utils"; -import { Common } from "../common"; -import { Examples } from "../examples"; - -export module Users { - export const Info = z - .object({ - id: z.string().openapi({ - description: Common.IdDescription, - example: Examples.User.id, - }), - email: z.string().nullable().openapi({ - description: "Email address of the user.", - example: Examples.User.email, - }), - }) - .openapi({ - ref: "User", - description: "A Nestri console user.", - example: Examples.User, - }); - - export const fromEmail = fn(z.string(), async (email) => { - const db = databaseClient() - const res = await db.auth.getUser({ email }) - return res - }) - - export const create = fn(z.string(), async (email) => { - const db = databaseClient() - const token = await db.auth.createToken(email) - - return token - }) -} \ No newline at end of file diff --git a/packages/core/src:old/utils/fn.ts b/packages/core/src:old/utils/fn.ts deleted file mode 100644 index 105656b3..00000000 --- a/packages/core/src:old/utils/fn.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { ZodSchema, z } from "zod"; - -export function fn< - Arg1 extends ZodSchema, - Callback extends (arg1: z.output) => any, ->(arg1: Arg1, cb: Callback) { - const result = function (input: z.input): ReturnType { - const parsed = arg1.parse(input); - return cb.apply(cb, [parsed as any]); - }; - result.schema = arg1; - return result; -} - -export function doubleFn< - Arg1 extends ZodSchema, - Arg2 extends ZodSchema, - Callback extends (arg1: z.output, arg2: z.output) => any, ->(arg1: Arg1, arg2: Arg2, cb: Callback) { - const result = function (input: z.input, input2: z.input): ReturnType { - const parsed = arg1.parse(input); - const parsed2 = arg2.parse(input2); - return cb.apply(cb, [parsed as any, parsed2 as any]); - }; - result.schema = arg1; - return result; -} \ No newline at end of file diff --git a/packages/core/src:old/utils/id.ts b/packages/core/src:old/utils/id.ts deleted file mode 100644 index badcc0d9..00000000 --- a/packages/core/src:old/utils/id.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { ulid } from "ulid"; - -export const prefixes = { - user: "usr", -} as const; - -export function createID(prefix: keyof typeof prefixes): string { - return [prefixes[prefix], ulid()].join("_"); -} \ No newline at end of file diff --git a/packages/core/src:old/utils/index.ts b/packages/core/src:old/utils/index.ts deleted file mode 100644 index 16ec78f3..00000000 --- a/packages/core/src:old/utils/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./fn" -export * from "./id" \ No newline at end of file diff --git a/packages/ui/src/fonts.tsx b/packages/ui/src/fonts.tsx index 46aaea3d..3ed240ca 100644 --- a/packages/ui/src/fonts.tsx +++ b/packages/ui/src/fonts.tsx @@ -10,10 +10,6 @@ import "@fontsource/geist-mono/400.css" import "@fontsource/geist-mono/700.css" //font-mona import "@fontsource-variable/mona-sans" -// import "@fontsource/mona-sans/500.css" -// import "@fontsource/mona-sans/600.css" -// import "@fontsource/mona-sans/700.css" -// import "@fontsource/mona-sans/800.css" //font-bricolage import '@fontsource-variable/bricolage-grotesque';