🐜 fix: Make sure the db uses the same name (#199)

## Description
This fixes the issue where Cloudflare fails

## Related Issues
<!-- List any related issues (e.g., "Closes #123", "Fixes #456") -->

## Type of Change

- [x] Bug fix (non-breaking change)
- [ ] New feature (non-breaking change)
- [ ] Breaking change (fix or feature that changes existing
functionality)
- [ ] Documentation update
- [ ] Other (please describe):

## Checklist

- [x] I have updated relevant documentation
- [x] My code follows the project's coding style
- [x] My changes generate no new warnings/errors
This commit is contained in:
Wanjohi
2025-03-02 00:01:25 +03:00
committed by GitHub
parent b18b08b822
commit 178c612f0f
34 changed files with 5 additions and 2835 deletions

View File

@@ -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": "*",

View File

@@ -1,7 +1,7 @@
const dbProject = new neon.Project("Nestri", {
historyRetentionSeconds: 86400,
// name:"Nestri"
name:"Nestri"
})
const dbBranchId = $app.stage !== "production" ?

View File

@@ -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
}

View File

@@ -1,12 +0,0 @@
export const authFingerprintKey = new random.RandomString(
"AuthFingerprintKey",
{
length: 32,
},
);
sst.Linkable.wrap(random.RandomString, (resource) => ({
properties: {
value: resource.result,
},
}));

View File

@@ -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 <<EOF
// {
// "default-runtime": "nvidia",
// "runtimes": {
// "nvidia": {
// "path": "/usr/bin/nvidia-container-runtime",
// "runtimeArgs": []
// }
// }
// }
// EOF
// sudo systemctl restart docker
// echo ECS_CLUSTER='${ecsCluster.name}' | sudo tee -a /etc/ecs/ecs.config
// echo ECS_ENABLE_GPU_SUPPORT=true | sudo tee -a /etc/ecs/ecs.config
// echo ECS_CONTAINER_STOP_TIMEOUT=3h | sudo tee -a /etc/ecs/ecs.config
// echo ECS_ENABLE_SPOT_INSTANCE_DRAINING=true | sudo tee -a /etc/ecs/ecs.config
// `,
// This is used for requesting a container to be deployed on AWS
// const queue = new sst.aws.Queue("PartyQueue", { fifo: true });
// queue.subscribe({ handler: "packages/functions/src/party/subscriber.handler", permissions:{}, link:[taskF]})
// const authRes = $interpolate`${authFingerprintKey.result}`

View File

@@ -1,9 +0,0 @@
export const domain =
{
production: "nestri.io",
dev: "dev.nestri.io",
}[$app.stage] || $app.stage + ".dev.nestri.io";
export const zone = cloudflare.getZoneOutput({
name: "nestri.io",
});

View File

@@ -1,33 +0,0 @@
// This is for the websocket/MQTT endpoint that helps the API communicate with the container
// [API] <-> 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
// }

View File

@@ -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
// }

View File

@@ -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);

View File

@@ -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);
});

View File

@@ -1,2 +0,0 @@
export const isPermanentStage =
$app.stage === "production" || $app.stage === "dev";

View File

@@ -1,4 +0,0 @@
// export const vpc = new sst.aws.Vpc("Vpc")
// export const storage = new sst.aws.Efs("GameStorage",{ vpc })
// //

View File

@@ -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]

View File

@@ -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<Actor>();
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<T extends Actor["type"]>(type: T) {
const actor = useActor();
if (actor.type !== type)
throw new VisibleError("auth", "actor.invalid", `Actor is not "${type}"`);
return actor as Extract<Actor, { type: T }>;
}

View File

@@ -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
})
}

View File

@@ -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.`;
}

View File

@@ -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

View File

@@ -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)
}
}
}

View File

@@ -1,9 +0,0 @@
export class VisibleError extends Error {
constructor(
public kind: "input" | "auth",
public code: string,
public message: string,
) {
super(message);
}
}

View File

@@ -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'
}
}

View File

@@ -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<typeof Info>;
// 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
// })
// }

View File

@@ -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<typeof Info>;
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
})
}

View File

@@ -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<typeof Info>;
// 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
// })
// }

View File

@@ -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<typeof Info>;
export type userStatus = z.infer<typeof userStatus>;
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
}
}
}

View File

@@ -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<typeof Info>;
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
}
})
}

View File

@@ -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<typeof SubscriptionFrequency>;
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<typeof Info>;
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]
})
}

View File

@@ -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<typeof taskType>;
export type lastStatus = z.infer<typeof lastStatus>;
export type healthStatus = z.infer<typeof healthStatus>;
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<typeof Info>;
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
}
})
}

View File

@@ -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<typeof Info>;
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"
})
}

View File

@@ -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
}

View File

@@ -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
})
}

View File

@@ -1,27 +0,0 @@
import { ZodSchema, z } from "zod";
export function fn<
Arg1 extends ZodSchema,
Callback extends (arg1: z.output<Arg1>) => any,
>(arg1: Arg1, cb: Callback) {
const result = function (input: z.input<typeof arg1>): ReturnType<Callback> {
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<Arg1>, arg2: z.output<Arg2>) => any,
>(arg1: Arg1, arg2: Arg2, cb: Callback) {
const result = function (input: z.input<typeof arg1>, input2: z.input<typeof arg2>): ReturnType<Callback> {
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;
}

View File

@@ -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("_");
}

View File

@@ -1,2 +0,0 @@
export * from "./fn"
export * from "./id"

View File

@@ -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';