From fc5a755408a839fb59973b26116d0295ebaf9d55 Mon Sep 17 00:00:00 2001 From: Wanjohi <71614375+wanjohiryan@users.noreply.github.com> Date: Sat, 4 Jan 2025 00:02:28 +0300 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20Add=20auth=20flow=20(#146)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This adds a simple way to incorporate a centralized authentication flow. The idea is to have the user, API and SSH (for machine authentication) all in one place using `openauthjs` + `SST` We also have a database now :) > We are using InstantDB as it allows us to authenticate a use with just the email. Plus it is super simple simple to use _of course after the initial fumbles trying to design the db and relationships_ --- .gitignore | 5 +- {infra => apps/docs}/RELAY.md | 0 apps/docs/sst-env.d.ts | 42 +++ apps/www/.eslintrc.cjs | 30 +- apps/www/package.json | 3 +- apps/www/postcss.config.cjs | 1 - apps/www/src/routes/(auth)/device/index.tsx | 9 + apps/www/src/routes/(auth)/login/index.tsx | 54 ++++ .../src/routes/(moq)/moq/checker/index.tsx | 119 -------- .../src/routes/(moq)/moq/checker/tester.ts | 208 ------------- apps/www/src/routes/home/index.tsx | 9 +- apps/www/src/routes/play/[id]/index.tsx | 15 +- apps/www/src/routes/pricing/index.tsx | 5 +- apps/www/sst-env.d.ts | 40 ++- apps/www/tsconfig.json | 30 +- bun.lockb | Bin 803824 -> 740728 bytes infra/api.ts | 68 +++-- infra/cli.ts | 10 + infra/dns.ts | 9 + infra/domain.ts | 9 - infra/relay.ts | 22 -- infra/secrets.ts | 10 +- infra/www.ts | 79 ----- package.json | 3 +- packages/api/.gitignore | 33 -- packages/api/README.md | 12 - packages/api/assets/favicon.ico | Bin 3230 -> 0 bytes packages/api/assets/favicon.svg | 14 - packages/api/package.json | 23 -- packages/api/src/image/avatar.ts | 38 --- packages/api/src/image/banner.ts | 72 ----- packages/api/src/image/cover.ts | 72 ----- packages/api/src/image/index.ts | 39 --- packages/api/src/index.ts | 18 -- packages/api/src/types/api.d.ts | 4 - packages/api/src/utils/create-avatar.tsx | 33 -- packages/api/src/utils/gradient.ts | 24 -- packages/api/src/utils/index.ts | 2 - packages/api/tsconfig.json | 17 -- packages/api/wrangler.toml | 26 -- packages/cache/caches.ts | 47 --- packages/cache/index.ts | 8 - packages/cache/middleware.ts | 47 --- packages/cache/package.json | 13 - packages/cache/tsconfig.json | 15 - packages/cache/types.ts | 3 - packages/certs/.gitignore | 3 - packages/certs/.terraform.lock.hcl | 61 ---- packages/certs/README.md | 24 -- packages/certs/input.tf | 7 - packages/certs/main.tf | 65 ---- packages/certs/terraform.tfvars | 2 - packages/cli/cmd/root.go | 50 +++ packages/cli/cmd/run.go | 26 ++ packages/cli/go.mod | 54 ++++ packages/{master => cli}/go.sum | 118 +++++--- packages/cli/internal/api/api.go | 26 ++ packages/cli/internal/auth/auth.go | 44 +++ packages/cli/internal/machine/machine.go | 202 +++++++++++++ packages/cli/internal/party/client.go | 112 +++++++ packages/cli/internal/party/retry.go | 125 ++++++++ packages/cli/internal/resource/resource.go | 38 +++ packages/cli/internal/session/start.go | 286 ++++++++++++++++++ packages/cli/internal/session/steam.go | 76 +++++ packages/cli/internal/session/utils.go | 72 +++++ packages/cli/main.go | 58 ++++ packages/core/image-brightness-analyzer.ts | 73 ----- packages/core/index.ts | 1 - packages/core/instant.perms.ts | 35 +++ packages/core/instant.schema.ts | 80 +++++ packages/core/package.json | 19 +- packages/core/src/actor.ts | 85 ++++++ packages/core/src/common.ts | 7 + packages/core/src/context.ts | 17 ++ packages/core/src/database.ts | 12 + packages/core/src/email/index.ts | 25 ++ packages/core/src/error.ts | 9 + packages/core/src/examples.ts | 45 +++ packages/core/src/machine/index.ts | 140 +++++++++ packages/core/src/team/index.ts | 48 +++ packages/core/src/types.ts | 17 ++ packages/core/src/user/index.ts | 37 +++ packages/core/src/utils/fn.ts | 13 + packages/core/src/utils/index.ts | 1 + packages/core/sst-env.d.ts | 42 +++ packages/core/tsconfig.json | 9 + packages/docker-to-rootfs/Dockerfile | 6 - packages/docker-to-rootfs/README.md | 15 - packages/docker-to-rootfs/create_image.sh | 54 ---- packages/docker-to-rootfs/make.sh | 112 ------- packages/docker-to-rootfs/syslinux.cfg | 5 - packages/eslint-config/README.md | 3 - packages/eslint-config/library.js | 34 --- packages/eslint-config/next.js | 35 --- packages/eslint-config/package.json | 25 -- packages/eslint-config/qwik.js | 50 --- packages/eslint-config/react-internal.js | 39 --- packages/functions/.gitignore | 175 +++++++++++ ...5b8f1d619378926c2dc36df98eae727ff69.sqlite | Bin 0 -> 8192 bytes ...1d619378926c2dc36df98eae727ff69.sqlite-shm | Bin 0 -> 32768 bytes ...1d619378926c2dc36df98eae727ff69.sqlite-wal | 0 ...e132553b5beef99527702def1f3679cb4de.sqlite | Bin 0 -> 8192 bytes ...736c54a14b079b0bae3773d08ab17264bae.sqlite | Bin 0 -> 4096 bytes ...54a14b079b0bae3773d08ab17264bae.sqlite-shm | Bin 0 -> 32768 bytes ...54a14b079b0bae3773d08ab17264bae.sqlite-wal | Bin 0 -> 8272 bytes packages/functions/README.md | 15 + packages/functions/package.json | 20 ++ packages/functions/partykit.json | 6 + packages/functions/src/adapter.ts | 121 ++++++++ packages/functions/src/api/index.ts | 153 ++++++++++ packages/functions/src/api/machine.ts | 160 ++++++++++ packages/functions/src/auth.ts | 140 +++++++++ packages/functions/src/common.ts | 10 + packages/functions/src/error.ts | 9 + packages/functions/src/party/auth.ts | 81 +++++ packages/functions/src/party/hono.ts | 116 +++++++ packages/functions/src/party/index.ts | 53 ++++ packages/functions/src/subjects.ts | 13 + packages/functions/sst-env.d.ts | 41 +++ packages/functions/tsconfig.json | 27 ++ packages/input/sst-env.d.ts | 42 +++ packages/master/go.mod | 32 -- packages/master/main.go | 80 ----- packages/moq/sst-env.d.ts | 42 +++ packages/typescript-config/base.json | 21 -- packages/typescript-config/nextjs.json | 13 - packages/typescript-config/package.json | 9 - packages/typescript-config/react-library.json | 8 - packages/ui/.eslintrc.cjs | 32 +- packages/ui/package.json | 6 +- packages/ui/src/image/index.ts | 4 +- packages/ui/sst-env.d.ts | 42 +++ packages/ui/tsconfig.json | 28 +- packages/ui/tsconfig.lint.json | 9 - sst-env.d.ts | 33 +- sst.config.ts | 3 +- 136 files changed, 3512 insertions(+), 1914 deletions(-) rename {infra => apps/docs}/RELAY.md (100%) create mode 100644 apps/docs/sst-env.d.ts create mode 100644 apps/www/src/routes/(auth)/device/index.tsx create mode 100644 apps/www/src/routes/(auth)/login/index.tsx delete mode 100644 apps/www/src/routes/(moq)/moq/checker/index.tsx delete mode 100644 apps/www/src/routes/(moq)/moq/checker/tester.ts create mode 100644 infra/cli.ts create mode 100644 infra/dns.ts delete mode 100644 infra/domain.ts delete mode 100644 infra/relay.ts delete mode 100644 infra/www.ts delete mode 100644 packages/api/.gitignore delete mode 100644 packages/api/README.md delete mode 100644 packages/api/assets/favicon.ico delete mode 100644 packages/api/assets/favicon.svg delete mode 100644 packages/api/package.json delete mode 100644 packages/api/src/image/avatar.ts delete mode 100644 packages/api/src/image/banner.ts delete mode 100644 packages/api/src/image/cover.ts delete mode 100644 packages/api/src/image/index.ts delete mode 100644 packages/api/src/index.ts delete mode 100644 packages/api/src/types/api.d.ts delete mode 100644 packages/api/src/utils/create-avatar.tsx delete mode 100644 packages/api/src/utils/gradient.ts delete mode 100644 packages/api/src/utils/index.ts delete mode 100644 packages/api/tsconfig.json delete mode 100644 packages/api/wrangler.toml delete mode 100644 packages/cache/caches.ts delete mode 100644 packages/cache/index.ts delete mode 100644 packages/cache/middleware.ts delete mode 100644 packages/cache/package.json delete mode 100644 packages/cache/tsconfig.json delete mode 100644 packages/cache/types.ts delete mode 100644 packages/certs/.gitignore delete mode 100644 packages/certs/.terraform.lock.hcl delete mode 100644 packages/certs/README.md delete mode 100644 packages/certs/input.tf delete mode 100644 packages/certs/main.tf delete mode 100644 packages/certs/terraform.tfvars create mode 100644 packages/cli/cmd/root.go create mode 100644 packages/cli/cmd/run.go create mode 100644 packages/cli/go.mod rename packages/{master => cli}/go.sum (53%) create mode 100644 packages/cli/internal/api/api.go create mode 100644 packages/cli/internal/auth/auth.go create mode 100644 packages/cli/internal/machine/machine.go create mode 100644 packages/cli/internal/party/client.go create mode 100644 packages/cli/internal/party/retry.go create mode 100644 packages/cli/internal/resource/resource.go create mode 100644 packages/cli/internal/session/start.go create mode 100644 packages/cli/internal/session/steam.go create mode 100644 packages/cli/internal/session/utils.go create mode 100644 packages/cli/main.go delete mode 100644 packages/core/image-brightness-analyzer.ts delete mode 100644 packages/core/index.ts create mode 100644 packages/core/instant.perms.ts create mode 100644 packages/core/instant.schema.ts create mode 100644 packages/core/src/actor.ts create mode 100644 packages/core/src/common.ts create mode 100644 packages/core/src/context.ts create mode 100644 packages/core/src/database.ts create mode 100644 packages/core/src/email/index.ts create mode 100644 packages/core/src/error.ts create mode 100644 packages/core/src/examples.ts create mode 100644 packages/core/src/machine/index.ts create mode 100644 packages/core/src/team/index.ts create mode 100644 packages/core/src/types.ts create mode 100644 packages/core/src/user/index.ts create mode 100644 packages/core/src/utils/fn.ts create mode 100644 packages/core/src/utils/index.ts create mode 100644 packages/core/sst-env.d.ts create mode 100644 packages/core/tsconfig.json delete mode 100644 packages/docker-to-rootfs/Dockerfile delete mode 100644 packages/docker-to-rootfs/README.md delete mode 100644 packages/docker-to-rootfs/create_image.sh delete mode 100644 packages/docker-to-rootfs/make.sh delete mode 100644 packages/docker-to-rootfs/syslinux.cfg delete mode 100644 packages/eslint-config/README.md delete mode 100644 packages/eslint-config/library.js delete mode 100644 packages/eslint-config/next.js delete mode 100644 packages/eslint-config/package.json delete mode 100644 packages/eslint-config/qwik.js delete mode 100644 packages/eslint-config/react-internal.js create mode 100644 packages/functions/.gitignore create mode 100644 packages/functions/.partykit/state/party/-PartyKitDurable/68b01ab8eae54eaeb4dddaeba12765b8f1d619378926c2dc36df98eae727ff69.sqlite create mode 100644 packages/functions/.partykit/state/party/-PartyKitDurable/68b01ab8eae54eaeb4dddaeba12765b8f1d619378926c2dc36df98eae727ff69.sqlite-shm create mode 100644 packages/functions/.partykit/state/party/-PartyKitDurable/68b01ab8eae54eaeb4dddaeba12765b8f1d619378926c2dc36df98eae727ff69.sqlite-wal create mode 100644 packages/functions/.partykit/state/party/-PartyKitDurable/73ab631fd54eedb7cc3c85a33a542e132553b5beef99527702def1f3679cb4de.sqlite create mode 100644 packages/functions/.partykit/state/party/-PartyKitDurable/edf5bdf97b1b08ea4cbf3f72c6dac736c54a14b079b0bae3773d08ab17264bae.sqlite create mode 100644 packages/functions/.partykit/state/party/-PartyKitDurable/edf5bdf97b1b08ea4cbf3f72c6dac736c54a14b079b0bae3773d08ab17264bae.sqlite-shm create mode 100644 packages/functions/.partykit/state/party/-PartyKitDurable/edf5bdf97b1b08ea4cbf3f72c6dac736c54a14b079b0bae3773d08ab17264bae.sqlite-wal create mode 100644 packages/functions/README.md create mode 100644 packages/functions/package.json create mode 100644 packages/functions/partykit.json create mode 100644 packages/functions/src/adapter.ts create mode 100644 packages/functions/src/api/index.ts create mode 100644 packages/functions/src/api/machine.ts create mode 100644 packages/functions/src/auth.ts create mode 100644 packages/functions/src/common.ts create mode 100644 packages/functions/src/error.ts create mode 100644 packages/functions/src/party/auth.ts create mode 100644 packages/functions/src/party/hono.ts create mode 100644 packages/functions/src/party/index.ts create mode 100644 packages/functions/src/subjects.ts create mode 100644 packages/functions/sst-env.d.ts create mode 100644 packages/functions/tsconfig.json create mode 100644 packages/input/sst-env.d.ts delete mode 100644 packages/master/go.mod delete mode 100644 packages/master/main.go create mode 100644 packages/moq/sst-env.d.ts delete mode 100644 packages/typescript-config/base.json delete mode 100644 packages/typescript-config/nextjs.json delete mode 100644 packages/typescript-config/package.json delete mode 100644 packages/typescript-config/react-library.json create mode 100644 packages/ui/sst-env.d.ts delete mode 100644 packages/ui/tsconfig.lint.json diff --git a/.gitignore b/.gitignore index 0bb76dbf..1c0dba4f 100644 --- a/.gitignore +++ b/.gitignore @@ -49,4 +49,7 @@ bun.lockb id_* #Rust -target \ No newline at end of file +target + +tmp +.partykit \ No newline at end of file diff --git a/infra/RELAY.md b/apps/docs/RELAY.md similarity index 100% rename from infra/RELAY.md rename to apps/docs/RELAY.md diff --git a/apps/docs/sst-env.d.ts b/apps/docs/sst-env.d.ts new file mode 100644 index 00000000..f90ea1f4 --- /dev/null +++ b/apps/docs/sst-env.d.ts @@ -0,0 +1,42 @@ +/* This file is auto-generated by SST. Do not edit. */ +/* tslint:disable */ +/* eslint-disable */ +/* deno-fmt-ignore-file */ +import "sst" +export {} +declare module "sst" { + export interface Resource { + "Api": { + "type": "sst.cloudflare.Worker" + "url": string + } + "Auth": { + "type": "sst.cloudflare.Worker" + "url": string + } + "AuthFingerprintKey": { + "type": "random.index/randomString.RandomString" + "value": string + } + "CloudflareAuthKV": { + "type": "sst.cloudflare.Kv" + } + "InstantAdminToken": { + "type": "sst.sst.Secret" + "value": string + } + "InstantAppId": { + "type": "sst.sst.Secret" + "value": string + } + "LoopsApiKey": { + "type": "sst.sst.Secret" + "value": string + } + "Urls": { + "api": string + "auth": string + "type": "sst.sst.Linkable" + } + } +} diff --git a/apps/www/.eslintrc.cjs b/apps/www/.eslintrc.cjs index 878c3386..92f00457 100644 --- a/apps/www/.eslintrc.cjs +++ b/apps/www/.eslintrc.cjs @@ -1,7 +1,14 @@ module.exports = { root: true, + env: { + browser: true, + es2021: true, + node: true, + }, extends: [ - "@nestri/eslint-config/qwik.js", + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:qwik/recommended", ], parser: "@typescript-eslint/parser", parserOptions: { @@ -12,5 +19,24 @@ module.exports = { ecmaFeatures: { jsx: true, }, - } + }, + plugins: ["@typescript-eslint"], + rules: { + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/explicit-module-boundary-types": "off", + "@typescript-eslint/no-inferrable-types": "off", + "@typescript-eslint/no-non-null-assertion": "off", + "@typescript-eslint/no-empty-interface": "off", + "@typescript-eslint/no-namespace": "off", + "@typescript-eslint/no-empty-function": "off", + "@typescript-eslint/no-this-alias": "off", + "@typescript-eslint/ban-types": "off", + "@typescript-eslint/ban-ts-comment": "off", + "prefer-spread": "off", + "no-case-declarations": "off", + "no-console": "off", + "@typescript-eslint/no-unused-vars": ["error"], + "@typescript-eslint/consistent-type-imports": "warn", + "@typescript-eslint/no-unnecessary-condition": "warn", + }, }; \ No newline at end of file diff --git a/apps/www/package.json b/apps/www/package.json index 5ffec96c..b34c47b1 100644 --- a/apps/www/package.json +++ b/apps/www/package.json @@ -35,11 +35,10 @@ "@builder.io/qwik-city": "^1.8.0", "@builder.io/qwik-react": "0.5.0", "@modular-forms/qwik": "^0.27.0", - "@nestri/eslint-config": "*", "@nestri/input": "*", "@nestri/libmoq": "*", - "@nestri/typescript-config": "*", "@nestri/ui": "*", + "@openauthjs/openauth": "^0.2.6", "@types/eslint": "8.56.10", "@types/howler": "^2.2.12", "@types/node": "^22.5.1", diff --git a/apps/www/postcss.config.cjs b/apps/www/postcss.config.cjs index c1464c07..63889e76 100644 --- a/apps/www/postcss.config.cjs +++ b/apps/www/postcss.config.cjs @@ -1,4 +1,3 @@ -// module.exports = require("@nestri/ui/postcss.config"); module.exports = { plugins: { tailwindcss: {}, diff --git a/apps/www/src/routes/(auth)/device/index.tsx b/apps/www/src/routes/(auth)/device/index.tsx new file mode 100644 index 00000000..88c75a05 --- /dev/null +++ b/apps/www/src/routes/(auth)/device/index.tsx @@ -0,0 +1,9 @@ +import { component$ } from "@builder.io/qwik" + +export default component$(() => { + return ( +
+ Device +
+ ) +}) \ No newline at end of file diff --git a/apps/www/src/routes/(auth)/login/index.tsx b/apps/www/src/routes/(auth)/login/index.tsx new file mode 100644 index 00000000..cb615a80 --- /dev/null +++ b/apps/www/src/routes/(auth)/login/index.tsx @@ -0,0 +1,54 @@ +import { $, component$, useVisibleTask$ } from "@builder.io/qwik"; +import { createClient } from "@openauthjs/openauth/client"; + +function getHashParams(url: URL) { + const urlString = url.toString() + const hash = urlString.substring(urlString.indexOf('#') + 1); // Extract the part after the # + console.log("url", hash) + const params = new URLSearchParams(hash); + const paramsObj = {} as any; + for (const [key, value] of params.entries()) { + paramsObj[key] = decodeURIComponent(value); + } + console.log(paramsObj) + return paramsObj; +} + +function removeURLParams() { + const newURL = window.location.origin + window.location.pathname; // Just origin and path + window.location.replace(newURL); +} + +export default component$(() => { + + const login = $(async () => { + const client = createClient({ + clientID: "www", + issuer: "https://auth.lauryn.dev.nestri.io" + }) + + const { url } = await client.authorize("http://localhost:5173/login", "token", { pkce: true }) + window.location.href = url + }) + + // eslint-disable-next-line qwik/no-use-visible-task + useVisibleTask$(async () => { + const urlObj = new URL(window.location.href); + const params = getHashParams(urlObj) + if (params.access_token && params.refresh_token) { + + localStorage.setItem("access_token", params.access_token) + localStorage.setItem("refresh_token", params.refresh_token) + removeURLParams() + } + + + }) + return ( +
+ +
+ ) +}) \ No newline at end of file diff --git a/apps/www/src/routes/(moq)/moq/checker/index.tsx b/apps/www/src/routes/(moq)/moq/checker/index.tsx deleted file mode 100644 index 612f2666..00000000 --- a/apps/www/src/routes/(moq)/moq/checker/index.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import * as v from "valibot" -//FIXME: Make sure this works -// import { Broadcast } from "./tester"; -import { cn } from "@nestri/ui/design"; -import { routeLoader$ } from "@builder.io/qwik-city"; -import { component$, $, useSignal } from "@builder.io/qwik"; -import { MotionComponent, transition, TitleSection, Button } from "@nestri/ui/react"; -import { type InitialValues, type SubmitHandler, useForm, valiForm$ } from "@modular-forms/qwik" - -const Schema = v.object({ - url: v.pipe( - v.string(), - v.minLength(10, "Please input a valid url"), - v.url("Please input a valid url"), - ) -}, "Please fill in all the fields correctly.") - -type Form = v.InferInput; - -export const useFormLoader = routeLoader$>(async () => { - return { - url: "" - } -}) - -const generateRandomWord = (length: number) => { - const characters = 'abcdefghijklmnopqrstuvwxyz'; - return Array.from({ length }, () => characters[Math.floor(Math.random() * characters.length)]).join(''); -}; - -export default component$(() => { - const broadcasterOk = useSignal(); - const [state, { Form, Field }] = useForm
({ - loader: useFormLoader(), - validate: valiForm$(Schema) - }); - - const handleSubmit = $>(async (values) => { - const randomNamespace = generateRandomWord(6); - // const sub = await Broadcast.init({ url: values.url, fingerprint: undefined, namespace: randomNamespace }) - - // setTimeout(() => { - // broadcasterOk.value = sub.isSubscribed() - // }, 1000); - }); - - return ( - <> - - -
- - - {(field, props) => { - return ( -
-
- -
- {field.error && (

{field.error}

)} -
- ) - }} -
- - {/* */} - - {/* - - - - - - */} - - Check - -
- - - {typeof broadcasterOk.value !== "undefined" && broadcasterOk.value == true ? ( - - Your relay is doing okay - - ) : typeof broadcasterOk.value !== "undefined" && ( - - Your relay has an issue - - )} -
-
- - ) -}) \ No newline at end of file diff --git a/apps/www/src/routes/(moq)/moq/checker/tester.ts b/apps/www/src/routes/(moq)/moq/checker/tester.ts deleted file mode 100644 index 09cca337..00000000 --- a/apps/www/src/routes/(moq)/moq/checker/tester.ts +++ /dev/null @@ -1,208 +0,0 @@ -// import type { Connection, SubscribeRecv } from "@nestri/libmoq/transport" -// import { asError } from "@nestri/moq/common/error" -// import { Client } from "@nestri/moq/transport/client" -// import * as Catalog from "@nestri/moq/media/catalog" -// import { type GroupWriter } from "@nestri/moq/transport/objects" - -// export interface BroadcastConfig { -// namespace: string -// connection: Connection -// } -// export interface BroadcasterConfig { -// url: string -// namespace: string -// fingerprint?: string // URL to fetch TLS certificate fingerprint -// } - -// export interface BroadcastConfigTrack { -// input: string -// bitrate: number -// } - -// export class Broadcast { -// stream: GroupWriter | null -// subscriber: SubscribeRecv | null -// subscribed: boolean; - - -// readonly config: BroadcastConfig -// readonly catalog: Catalog.Root -// readonly connection: Connection -// readonly namespace: string - -// #running: Promise - -// constructor(config: BroadcastConfig) { -// this.subscribed = false -// this.namespace = config.namespace -// this.connection = config.connection -// this.config = config -// //Arbitrary values, just to keep TypeScript happy :) -// this.catalog = { -// version: 1, -// streamingFormat: 1, -// streamingFormatVersion: "0.2", -// supportsDeltaUpdates: false, -// commonTrackFields: { -// packaging: "loc", -// renderGroup: 1, -// }, -// tracks: [{ -// name: "tester", -// namespace: "tester", -// selectionParams: {} -// }], -// } -// this.stream = null -// this.subscriber = null - -// this.#running = this.#run() -// } - -// static async init(config: BroadcasterConfig): Promise { -// const client = new Client({ url: config.url, fingerprint: config.fingerprint, role: "publisher" }) -// const connection = await client.connect(); - -// return new Broadcast({ connection, namespace: config.namespace }) -// } - -// async #run() { -// try { -// await this.connection.announce(this.namespace) -// this.subscribed = true -// } catch (error) { - -// this.subscribed = false -// } - -// for (; ;) { -// const subscriber = await this.connection.subscribed() - -// if (!subscriber) { -// this.subscribed = false - -// break -// } - -// await subscriber.ack() - -// this.subscriber = subscriber - -// this.subscribed = true - -// const bytes = Catalog.encode(this.catalog); - -// const stream = await subscriber.group({ group: 0 }); - -// await stream.write({ object: 0, payload: bytes }) - -// this.stream = stream -// } -// } - -// isSubscribed(): boolean { -// return this.subscribed; -// } - -// // async #serveSubscribe(subscriber: SubscribeRecv) { -// // try { - -// // // Send a SUBSCRIBE_OK -// // await subscriber.ack() - -// // console.log("catalog track name:", subscriber.track) - -// // const stream = await subscriber.group({ group: 0 }); - -// // // const bytes = this.catalog.encode("Hello World") - -// // await stream.write({ object: 0, payload: bytes }) - - - -// // } catch (e) { -// // const err = asError(e) -// // await subscriber.close(1n, `failed to process publish: ${err.message}`) -// // } finally { -// // // TODO we can't close subscribers because there's no support for clean termination -// // // await subscriber.close() -// // } -// // } - -// // async mouseUpdatePosition({ x, y }: { x: number, y: number }, stream: GroupWriter) { - -// // const mouse_move = { -// // input_type: "mouse_move", -// // delta_y: y, -// // delta_x: x, -// // } - -// // const bytes = Catalog.encode(this.catalog) - -// // await stream.write({ object: 0, payload: bytes }); -// // } - -// // async mouseUpdateButtons(e: MouseEvent, stream: GroupWriter) { -// // const data: { input_type?: "mouse_key_down" | "mouse_key_up"; button: number; } = { button: e.button }; - -// // if (e.type === "mousedown") { -// // data["input_type"] = "mouse_key_down" -// // } else if (e.type === "mouseup") { -// // data["input_type"] = "mouse_key_up" -// // } - -// // const bytes = Catalog.encode(this.catalog) - -// // await stream.write({ object: 0, payload: bytes }); -// // } - -// // async mouseUpdateWheel(e: WheelEvent, stream: GroupWriter) { -// // const data: { input_type?: "mouse_wheel_up" | "mouse_wheel_down" } = {} - -// // if (e.deltaY < 0.0) { -// // data["input_type"] = "mouse_wheel_up" -// // } else { -// // data["input_type"] = "mouse_wheel_down" -// // } - -// // const bytes = Catalog.encode(this.catalog) - -// // await stream.write({ object: 0, payload: bytes }); -// // } - -// // async updateKeyUp(e: KeyboardEvent, stream: GroupWriter) { -// // const data = { -// // input_type: "key_up", -// // key_code: e.keyCode -// // } - -// // const bytes = Catalog.encode(this.catalog) - -// // await stream.write({ object: 0, payload: bytes }); -// // } - -// // async updateKeyDown(e: KeyboardEvent, stream: GroupWriter) { -// // const data = { -// // input_type: "key_down", -// // key_code: e.keyCode -// // } - -// // const bytes = Catalog.encode(this.catalog) - -// // await stream.write({ object: 0, payload: bytes }); -// // } - -// close() { -// // TODO implement publish close -// } - -// // Returns the error message when the connection is closed -// async closed(): Promise { -// try { -// await this.#running -// return new Error("closed") // clean termination -// } catch (e) { -// return asError(e) -// } -// } -// } \ No newline at end of file diff --git a/apps/www/src/routes/home/index.tsx b/apps/www/src/routes/home/index.tsx index b1e2ba9e..f9f7b8c1 100644 --- a/apps/www/src/routes/home/index.tsx +++ b/apps/www/src/routes/home/index.tsx @@ -1,12 +1,5 @@ import { component$ } from "@builder.io/qwik"; -import { HomeNavBar, Card } from "@nestri/ui"; - -function getGreeting(): string { - const hour = new Date().getHours(); - if (hour >= 5 && hour < 12) return "Good Morning"; - if (hour >= 12 && hour < 18) return "Good Afternoon"; - return "Good Evening"; -} +import { HomeNavBar } from "@nestri/ui"; export default component$(() => { return ( diff --git a/apps/www/src/routes/play/[id]/index.tsx b/apps/www/src/routes/play/[id]/index.tsx index f94dc4ec..7720c94f 100644 --- a/apps/www/src/routes/play/[id]/index.tsx +++ b/apps/www/src/routes/play/[id]/index.tsx @@ -1,12 +1,13 @@ -import {useLocation} from "@builder.io/qwik-city"; -import {Keyboard, Mouse, WebRTCStream} from "@nestri/input" -import {component$, useSignal, useVisibleTask$} from "@builder.io/qwik"; +import { useLocation } from "@builder.io/qwik-city"; +import { Keyboard, Mouse, WebRTCStream } from "@nestri/input" +import { component$, useSignal, useVisibleTask$ } from "@builder.io/qwik"; export default component$(() => { const id = useLocation().params.id; const canvas = useSignal(); - useVisibleTask$(({track}) => { + // eslint-disable-next-line qwik/no-use-visible-task + useVisibleTask$(({ track }) => { track(() => canvas.value); if (!canvas.value) return; // Ensure canvas is available @@ -66,9 +67,9 @@ export default component$(() => { // @ts-ignore if (document.pointerLockElement && !window.nestrimouse && !window.nestrikeyboard) { // @ts-ignore - window.nestrimouse = new Mouse({canvas: canvas.value, webrtc}); + window.nestrimouse = new Mouse({ canvas: canvas.value, webrtc }); // @ts-ignore - window.nestrikeyboard = new Keyboard({canvas: canvas.value, webrtc}); + window.nestrikeyboard = new Keyboard({ canvas: canvas.value, webrtc }); // @ts-ignore } else if (!document.pointerLockElement && window.nestrimouse && window.nestrikeyboard) { // @ts-ignore @@ -180,7 +181,7 @@ export default component$(() => { } }} //TODO: go full screen, then lock on "landscape" screen-orientation on mobile - class="aspect-video h-full w-full object-contain max-h-screen"/> + class="aspect-video h-full w-full object-contain max-h-screen" /> ) }) diff --git a/apps/www/src/routes/pricing/index.tsx b/apps/www/src/routes/pricing/index.tsx index 7a1f88a1..b4532958 100644 --- a/apps/www/src/routes/pricing/index.tsx +++ b/apps/www/src/routes/pricing/index.tsx @@ -1,8 +1,8 @@ -import { $, component$, noSerialize, NoSerialize, useSignal, useVisibleTask$ } from "@builder.io/qwik"; +import { $, component$, noSerialize, type NoSerialize, useSignal, useVisibleTask$ } from "@builder.io/qwik"; import { TitleSection, MotionComponent, transition } from "@nestri/ui/react"; import { NavBar, Footer, Book } from "@nestri/ui" import { cn } from "@nestri/ui/design"; -import { Howl, Howler } from 'howler'; +import { Howl } from 'howler'; //FIXME: Add a FAQ section // FIXME: Takes too long for the price input radio input to become responsive @@ -68,6 +68,7 @@ export default component$(() => { const bookRef = useSignal() const audio = useSignal | undefined>() + // eslint-disable-next-line qwik/no-use-visible-task useVisibleTask$(() => { audio.value = noSerialize(new Howl({ src: ["/audio/cash.mp3"], volume: 0.5 })) diff --git a/apps/www/sst-env.d.ts b/apps/www/sst-env.d.ts index af3a3c21..f90ea1f4 100644 --- a/apps/www/sst-env.d.ts +++ b/apps/www/sst-env.d.ts @@ -1,4 +1,42 @@ +/* This file is auto-generated by SST. Do not edit. */ /* tslint:disable */ /* eslint-disable */ -/// +/* deno-fmt-ignore-file */ +import "sst" export {} +declare module "sst" { + export interface Resource { + "Api": { + "type": "sst.cloudflare.Worker" + "url": string + } + "Auth": { + "type": "sst.cloudflare.Worker" + "url": string + } + "AuthFingerprintKey": { + "type": "random.index/randomString.RandomString" + "value": string + } + "CloudflareAuthKV": { + "type": "sst.cloudflare.Kv" + } + "InstantAdminToken": { + "type": "sst.sst.Secret" + "value": string + } + "InstantAppId": { + "type": "sst.sst.Secret" + "value": string + } + "LoopsApiKey": { + "type": "sst.sst.Secret" + "value": string + } + "Urls": { + "api": string + "auth": string + "type": "sst.sst.Linkable" + } + } +} diff --git a/apps/www/tsconfig.json b/apps/www/tsconfig.json index 6a9efe3a..80fd788f 100644 --- a/apps/www/tsconfig.json +++ b/apps/www/tsconfig.json @@ -1,22 +1,15 @@ { - "extends": "@nestri/typescript-config/base.json", "compilerOptions": { "allowJs": true, - "target": "ES2022", + "target": "ES2017", "module": "ES2022", - "lib": [ - "es2022", - "DOM", - "WebWorker", - "DOM.Iterable" - ], + "lib": ["es2022", "DOM", "WebWorker", "DOM.Iterable"], "jsx": "react-jsx", "jsxImportSource": "@builder.io/qwik", "strict": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "moduleResolution": "Bundler", - "allowImportingTsExtensions": true, "esModuleInterop": true, "skipLibCheck": true, "incremental": true, @@ -24,22 +17,9 @@ "outDir": "tmp", "noEmit": true, "paths": { - "@/*": [ - "./src/*" - ], - "content-collections": [ - "./.content-collections/generated" - ] + "@/*": ["./src/*"] } }, - "files": [ - ".eslintrc.cjs" - ], - "include": [ - "src", - "./*.d.ts", - "./*.config.ts", - "./*.config.js", - "content-collections.ts" -, "../../packages/input/src/webrtc-stream.ts" ] + "files": ["./.eslintrc.cjs"], + "include": ["src", "./*.d.ts", "./*.config.ts","./*.config.cjs"] } \ No newline at end of file diff --git a/bun.lockb b/bun.lockb index c6d64e31a94f04820c6f1bf9de19b7747a58e2da..97a9fac091cfc016a5157dd36bf7363159be75c3 100755 GIT binary patch delta 168523 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 z%0D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 delta 208758 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�P}A{!??8{+W;ReisR`z* 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 d5 ({ + properties: { + value: resource.result, + }, +})); -if (!isPermanentStage) { - new sst.x.DevCommand("apiDev", { - dev: { - command: "bun run dev", - directory: "packages/api", - autostart: true, - }, - }) -} +export const authFingerprintKey = new random.RandomString( + "AuthFingerprintKey", + { + length: 32, + }, +); -// export const outputs = { -// api: api.url -// } \ No newline at end of file +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 + ], + handler: "./packages/functions/src/auth.ts", + url: true, + domain: "auth." + domain +}); + +export const api = new sst.cloudflare.Worker("Api", { + link: [ + urls, + ], + 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/cli.ts b/infra/cli.ts new file mode 100644 index 00000000..0b5f2728 --- /dev/null +++ b/infra/cli.ts @@ -0,0 +1,10 @@ +import { auth, urls } from "./api" + + +// export const cmd = new sst.x.DevCommand("Cmd", { +// link: [urls, auth], +// dev: { +// autostart: true, +// command: "cd packages/cmd && go run main.go" +// } +// }) \ No newline at end of file diff --git a/infra/dns.ts b/infra/dns.ts new file mode 100644 index 00000000..2dd0569e --- /dev/null +++ b/infra/dns.ts @@ -0,0 +1,9 @@ +export const domain = + { + production: "prod.nestri.io", //temporary use until we go into the real production + dev: "dev.nestri.io", + }[$app.stage] || $app.stage + ".dev.nestri.io"; + + export const zone = cloudflare.getZoneOutput({ + name: "nestri.io", + }); \ No newline at end of file diff --git a/infra/domain.ts b/infra/domain.ts deleted file mode 100644 index c11f87bc..00000000 --- a/infra/domain.ts +++ /dev/null @@ -1,9 +0,0 @@ -// export const domain = -// { -// production: "fst.so", -// dev: "dev.fst.so", -// }[$app.stage] || $app.stage + ".dev.fst.so"; - -// export const zone = cloudflare.getZoneOutput({ -// name: "fst.so", -// }); \ No newline at end of file diff --git a/infra/relay.ts b/infra/relay.ts deleted file mode 100644 index 5da8dc5b..00000000 --- a/infra/relay.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { resolve as pathResolve } from "node:path"; -import { readFileSync as readFile } from "node:fs"; -//Copy your (known) ssh public key to the remote machine -//ssh-copy-id "-p $port" user@host - -const domain = "fst.so" -const ips = ["95.216.29.238"] - -// Get the hosted zone -const zone = aws.route53.getZone({ name: domain }); - -// Create an A record -const record = new aws.route53.Record("Relay DNS Records", { - zoneId: zone.then(zone => zone.zoneId), - type: "A", - name: `relay.${domain}`, - ttl: 300, - records: ips, -}); - -// Export the URL -export const url = $interpolate`https://${record.name}`; \ No newline at end of file diff --git a/infra/secrets.ts b/infra/secrets.ts index 28377c8d..5e642c9c 100644 --- a/infra/secrets.ts +++ b/infra/secrets.ts @@ -1,5 +1,7 @@ export const secret = { - CloudflareAccountIdSecret: new sst.Secret("CloudflareAccountId"), -}; - -export const allSecrets = Object.values(secret); \ No newline at end of file + InstantAdminToken: new sst.Secret("InstantAdminToken"), + InstantAppId: new sst.Secret("InstantAppId"), + LoopsApiKey: new sst.Secret("LoopsApiKey") + }; + + export const allSecrets = Object.values(secret); \ No newline at end of file diff --git a/infra/www.ts b/infra/www.ts deleted file mode 100644 index 4a2f9cc7..00000000 --- a/infra/www.ts +++ /dev/null @@ -1,79 +0,0 @@ -//Deploys the website to cloudflare pages under the domain nestri.io (redirects all requests to www.nestri.io to avoid duplicate content) - -import { isPermanentStage } from "./stage"; - -export const www = new cloudflare.PagesProject("www", { - name: "nestri", - accountId: "8405b2acb6746935b975bc2cfcb5c288", - productionBranch: "main", - buildConfig: { - rootDir: "apps/www", - buildCommand: "bun run build", - destinationDir: "dist" - }, - deploymentConfigs: { - production: { - compatibilityFlags: ["nodejs_compat"] - }, - preview: { - compatibilityFlags: ["nodejs_compat"] - } - }, - source: { - type: "github", - config: { - owner: "nestriness", - deploymentsEnabled: true, - productionBranch: "main", - repoName: "nestri", - productionDeploymentEnabled: true, - prCommentsEnabled: true, - } - } -}); - -//TODO: Maybe handle building Qwik ourselves? This prevents us from relying on CF too much, we are open-source anyway 🤷🏾‍♂️ -//TODO: Add a local dev server for Qwik that can be linked with whatever we want -//TODO: Link the www PageRule with whatever we give to the local dev server -if (!isPermanentStage) { - new sst.x.DevCommand("www", { - dev: { - command: "bun run dev", - directory: "apps/www", - autostart: true, - }, - }) -} - -// //This creates a resource that can be accessed by itself -// new sst.Linkable.wrap(cloudflare.PageRule, (resource) => ({ -// // these properties will be available when linked -// properties: { -// arn: resource.urn -// } -// })) -// //And then you call your linkable resource like this: -// // const www = cloudflare.PageRule("www", {}) - -// //this creates a linkable resource that can be linked to other resources -// export const linkable2 = new sst.Linkable("ExistingResource", { -// properties: { -// arn: "arn:aws:s3:::nestri-website-artifacts-prod-nestri-io-01h70zg50qz5z" -// }, -// include: [ -// sst.aws.permission({ -// actions: ["s3:*"], -// resources: ["arn:aws:s3:::nestri-website-artifacts-prod-nestri-io-01h70zg50qz5z"] -// }), -// sst.cloudflare.binding({ -// type: "r2BucketBindings", -// properties: { -// bucketName: "nestri-website-artifacts-prod-nestri-io-01h70zg50qz5z", -// } -// }) -// ] -// }) - -export const outputs = { - www: www.subdomain, -}; \ No newline at end of file diff --git a/package.json b/package.json index 2581272a..5ca668e7 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "scripts": { "build": "turbo build", "dev": "turbo dev", + "sst": "sst dev", "format": "prettier --write \"**/*.{ts,tsx,md}\"", "lint": "turbo lint" }, @@ -29,6 +30,6 @@ "workerd" ], "dependencies": { - "sst": "3.0.94" + "sst": "^3.4.32" } } \ No newline at end of file diff --git a/packages/api/.gitignore b/packages/api/.gitignore deleted file mode 100644 index e319e063..00000000 --- a/packages/api/.gitignore +++ /dev/null @@ -1,33 +0,0 @@ -# prod -dist/ - -# dev -.yarn/ -!.yarn/releases -.vscode/* -!.vscode/launch.json -!.vscode/*.code-snippets -.idea/workspace.xml -.idea/usage.statistics.xml -.idea/shelf - -# deps -node_modules/ -.wrangler - -# env -.env -.env.production -.dev.vars - -# logs -logs/ -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -# misc -.DS_Store diff --git a/packages/api/README.md b/packages/api/README.md deleted file mode 100644 index 4620f9bc..00000000 --- a/packages/api/README.md +++ /dev/null @@ -1,12 +0,0 @@ -# Nexus - -## Development - -``` -npm install -npm run dev -``` - -``` -npm run deploy -``` diff --git a/packages/api/assets/favicon.ico b/packages/api/assets/favicon.ico deleted file mode 100644 index 69cc4180965c59edc88bef29af15d3c2c6539ade..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3230 zcmeH~Jxc>Y5QZmx>R=g2lPW|I!G9p4MC;gSuV_?kgs2b^41p*Xiim=UScrxGFA?)8 z`Z$wdm1`%vMdoV-Gl8Ln6D~FPN??AHGFkPXYkpE*&UDtWzh-Mp5B6wRBK}Wrk>gN~6L6W!S&z^m`1}i5%nD*{1v?lo*`2^N~ zLgznp{u-~^pRZt3d$b3pef9#j)?=%V>+G%5_+6_+Z)x50m`Gy8njEpRK>zGpsdX V{oDuBK7NWbf1&!p|9^nj^$$WzN$LOq diff --git a/packages/api/assets/favicon.svg b/packages/api/assets/favicon.svg deleted file mode 100644 index 736b34cb..00000000 --- a/packages/api/assets/favicon.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - diff --git a/packages/api/package.json b/packages/api/package.json deleted file mode 100644 index 1bd375eb..00000000 --- a/packages/api/package.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "@nestri/nexus", - "version": "0.0.1", - "private": true, - "description": "Nestri's core", - "scripts": { - "dev": "wrangler dev src/index.ts", - "deploy": "wrangler deploy --minify src/index.ts" - }, - "dependencies": { - "hono": "^4.5.5" - }, - "devDependencies": { - "@cf-wasm/resvg": "^0.1.21", - "@cloudflare/workers-types": "^4.20240529.0", - "@jsquash/avif": "^1.3.0", - "@jsquash/jpeg": "^1.4.0", - "@jsquash/resize": "^2.0.0", - "@nestri/cache": "*", - "tinycolor2": "^1.6.0", - "wrangler": "^3.72.2" - } -} diff --git a/packages/api/src/image/avatar.ts b/packages/api/src/image/avatar.ts deleted file mode 100644 index 5d95c350..00000000 --- a/packages/api/src/image/avatar.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Hono } from 'hono' -import { Resvg } from "@cf-wasm/resvg"; -import { generateGradient, createAvatarSvg } from '../utils'; -import { kvCaches } from "@nestri/cache" - -const cacheOptions = { - key: "nexus", - namespace: "avatar-cache" -}; - -const app = new Hono() - -const middleware = kvCaches(cacheOptions); - -app.get('/:id', middleware, async (c) => { - const id = c.req.param('id'); - const [name, fileType] = id.split('.'); - const size = parseInt(c.req.query("size") || "200"); - - const validImageTypes = ["png"] //['jpg', 'png', 'webp', 'avif']; - if (!validImageTypes.includes(fileType)) { - return c.text('Invalid image type', 400); - } - - const gradient = generateGradient(name || Math.random() + ""); - // console.log(`gradient: ${JSON.stringify(gradient)}`) - const svg = createAvatarSvg(size, gradient, fileType); - const resvg = await Resvg.create(svg.toString()); - const pngData = resvg.render() - const pngBuffer = pngData.asPng() - - return c.newResponse(pngBuffer, 200, { - "Content-Type": `image/${fileType}`, - 'Cache-Control': 'public, max-age=31536000, immutable' - }) -}) - -export default app \ No newline at end of file diff --git a/packages/api/src/image/banner.ts b/packages/api/src/image/banner.ts deleted file mode 100644 index 45391ca0..00000000 --- a/packages/api/src/image/banner.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { Hono } from 'hono' -import { kvCaches } from "@nestri/cache" -import resize, { initResize } from '@jsquash/resize'; -import decodeJpeg, { init as initDecodeJpeg } from '@jsquash/jpeg/decode'; -import encodeAvif, { init as initEncodeAvif } from '@jsquash/avif/encode.js'; -import JPEG_DEC_WASM from "@jsquash/jpeg/codec/dec/mozjpeg_dec.wasm"; -import RESIZE_WASM from "@jsquash/resize/lib/resize/pkg/squoosh_resize_bg.wasm"; -import AVIF_ENC_WASM from "@jsquash/avif/codec/enc/avif_enc.wasm"; - -const cacheOptions = { - key: "nexus", - namespace: "cover-cache" -}; - -const middleware = kvCaches(cacheOptions); - -const decodeImage = async (buffer: ArrayBuffer) => { - await initDecodeJpeg(JPEG_DEC_WASM); - return decodeJpeg(buffer); -} - -const resizeImage = async (image: { width: number; height: number }, width: number, height: number) => { - await initResize(RESIZE_WASM); - // Resize image with respect to aspect ratio - const aspectRatio = image.width / image.height; - const newWidth = width; - const newHeight = width / aspectRatio; - return resize(image, { width: newWidth, height: newHeight, fitMethod: "stretch" }); -} - -const encodeImage = async (image: { width: number; height: number }, format: string) => { - if (format === 'avif') { - await initEncodeAvif(AVIF_ENC_WASM); - return encodeAvif(image); - } - throw new Error(`Unsupported image format: ${format}`); -} - -const app = new Hono() - -app.notFound((c) => c.json({ message: 'Not Found', ok: false }, 404)) - -app.get('/:id', middleware, async (c) => { - const [gameId, imageType] = c.req.param("id").split('.'); - const width = parseInt(c.req.query("width") || "460"); - //We don't even use this, but let us keep it for future use - const height = parseInt(c.req.query("height") || "215"); - if (!gameId || !imageType) { - return c.text("Invalid image parameters", 400) - } - //Support Avif only because of it's small size - const validImageTypes = ["avif"] //['jpg', 'png', 'webp', 'avif']; - if (!validImageTypes.includes(imageType)) { - return c.text('Invalid image type', 400); - } - - const imageUrl = `https://shared.cloudflare.steamstatic.com/store_item_assets/steam/apps/${gameId}/header.jpg`; - const image = await fetch(imageUrl); - if (!image.ok) { - return c.text('Image not found', 404); - } - const imageBuffer = await image.arrayBuffer(); - const imageData = await decodeImage(imageBuffer); - const resizedImage = await resizeImage(imageData, width, height); - const resizedImageBuffer = await encodeImage(resizedImage, imageType); - return c.newResponse(resizedImageBuffer, 200, { - "Content-Type": `image/${imageType}`, - 'Cache-Control': 'public, max-age=31536000, immutable' - }) -}) - -export default app diff --git a/packages/api/src/image/cover.ts b/packages/api/src/image/cover.ts deleted file mode 100644 index ef372bce..00000000 --- a/packages/api/src/image/cover.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { Hono } from 'hono' -import { kvCaches } from "@nestri/cache" -import resize, { initResize } from '@jsquash/resize'; -import decodeJpeg, { init as initDecodeJpeg } from '@jsquash/jpeg/decode'; -import encodeAvif, { init as initEncodeAvif } from '@jsquash/avif/encode.js'; -import JPEG_DEC_WASM from "@jsquash/jpeg/codec/dec/mozjpeg_dec.wasm"; -import RESIZE_WASM from "@jsquash/resize/lib/resize/pkg/squoosh_resize_bg.wasm"; -import AVIF_ENC_WASM from "@jsquash/avif/codec/enc/avif_enc.wasm"; - -const cacheOptions = { - key: "nexus", - namespace: "cover-cache" -}; - -const middleware = kvCaches(cacheOptions); - -const decodeImage = async (buffer: ArrayBuffer) => { - await initDecodeJpeg(JPEG_DEC_WASM); - return decodeJpeg(buffer); -} - -const resizeImage = async (image: { width: number; height: number }, width: number, height: number) => { - await initResize(RESIZE_WASM); - // Resize image with respect to aspect ratio - const aspectRatio = image.width / image.height; - const newWidth = width; - const newHeight = width / aspectRatio; - return resize(image, { width: newWidth, height: newHeight, fitMethod: "stretch" }); -} - -const encodeImage = async (image: { width: number; height: number }, format: string) => { - if (format === 'avif') { - await initEncodeAvif(AVIF_ENC_WASM); - return encodeAvif(image); - } - throw new Error(`Unsupported image format: ${format}`); -} - -const app = new Hono() - -app.notFound((c) => c.json({ message: 'Not Found', ok: false }, 404)) - -app.get('/:id', middleware, async (c) => { - const [gameId, imageType] = c.req.param("id").split('.'); - const width = parseInt(c.req.query("width") || "600"); - //We don't even use this, but let us keep it for future use - const height = parseInt(c.req.query("height") || "900"); - if (!gameId || !imageType) { - return c.text("Invalid image parameters", 400) - } - //Support Avif only because of it's small size - const validImageTypes = ["avif"] //['jpg', 'png', 'webp', 'avif']; - if (!validImageTypes.includes(imageType)) { - return c.text('Invalid image type', 400); - } - - const imageUrl = `https://shared.cloudflare.steamstatic.com/store_item_assets/steam/apps/${gameId}/library_600x900_2x.jpg`; - const image = await fetch(imageUrl); - if (!image.ok) { - return c.text('Image not found', 404); - } - const imageBuffer = await image.arrayBuffer(); - const imageData = await decodeImage(imageBuffer); - const resizedImage = await resizeImage(imageData, width, height); - const resizedImageBuffer = await encodeImage(resizedImage, imageType); - return c.newResponse(resizedImageBuffer, 200, { - "Content-Type": `image/${imageType}`, - 'Cache-Control': 'public, max-age=31536000, immutable' - }) -}) - -export default app diff --git a/packages/api/src/image/index.ts b/packages/api/src/image/index.ts deleted file mode 100644 index 93b2908d..00000000 --- a/packages/api/src/image/index.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Hono } from 'hono' -import { cors } from 'hono/cors'; -import Cover from './cover' -import Avatar from './avatar' -import Banner from './banner' - -const app = new Hono() - -app.get('/', (c) => { - return c.text('Hello There! 👋🏾') -}) - -app.notFound((c) => c.json({ message: 'Not Found', ok: false }, 404)) - -app.use( - '/*', - cors({ - origin: (origin, c) => { - const allowedOriginPatterns = [ - /^https:\/\/.*\.nestri\.pages\.dev$/, - /^https:\/\/.*\.nestri\.io$/, - /^http:\/\/localhost:\d+$/ // For local development - ]; - - return allowedOriginPatterns.some(pattern => pattern.test(origin)) - ? origin - : 'https://nexus.nestri.io' - }, - }) -) - - -app.route('/cover', Cover) - -app.route('/avatar', Avatar) - -app.route("/banner", Banner) - -export default app diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts deleted file mode 100644 index b54acb2c..00000000 --- a/packages/api/src/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Hono } from 'hono' -import Image from "./image" - -const app = new Hono() - -app.get('/', (c) => { - return c.text('Hello There! 👋🏾') -}) - -app.get('/favicon.ico', (c) => { - return c.newResponse(null, 302, { - Location: 'https://nestri.pages.dev/favicon.svg' - }) -}) - -app.route("/image", Image) - -export default app \ No newline at end of file diff --git a/packages/api/src/types/api.d.ts b/packages/api/src/types/api.d.ts deleted file mode 100644 index b85329a1..00000000 --- a/packages/api/src/types/api.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module '*wasm' { - const content: any; - export default content; -} \ No newline at end of file diff --git a/packages/api/src/utils/create-avatar.tsx b/packages/api/src/utils/create-avatar.tsx deleted file mode 100644 index a33255d3..00000000 --- a/packages/api/src/utils/create-avatar.tsx +++ /dev/null @@ -1,33 +0,0 @@ -export const createAvatarSvg = (size: number, gradient: { fromColor: string, toColor: string }, fileType?: string, text?: string) => ( - - - - - - - - - - {fileType === "svg" && text && ( - - {text} - - )} - - -); \ No newline at end of file diff --git a/packages/api/src/utils/gradient.ts b/packages/api/src/utils/gradient.ts deleted file mode 100644 index 5a2b8fa0..00000000 --- a/packages/api/src/utils/gradient.ts +++ /dev/null @@ -1,24 +0,0 @@ -import color from "tinycolor2"; - -function fnv1a(str: string) { - let hash = 0x811c9dc5; - for (let i = 0; i < str.length; i++) { - hash ^= str.charCodeAt(i); - hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24); - } - return hash >>> 0; -} - -export function generateGradient(username: string) { - const hash = fnv1a(username); - const hue1 = hash % 360; - const hue2 = (hash >> 16) % 360; - - const c1 = color({ h: hue1, s: 0.8, l: 0.6 }); - const c2 = color({ h: hue2, s: 0.8, l: 0.5 }); - - return { - fromColor: c1.toHexString(), - toColor: c2.toHexString(), - }; -} \ No newline at end of file diff --git a/packages/api/src/utils/index.ts b/packages/api/src/utils/index.ts deleted file mode 100644 index bfe71ecc..00000000 --- a/packages/api/src/utils/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './gradient' -export * from './create-avatar' \ No newline at end of file diff --git a/packages/api/tsconfig.json b/packages/api/tsconfig.json deleted file mode 100644 index 934f03cc..00000000 --- a/packages/api/tsconfig.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "compilerOptions": { - "target": "ESNext", - "module": "ESNext", - "moduleResolution": "Bundler", - "strict": true, - "skipLibCheck": true, - "lib": [ - "ESNext" - ], - "types": [ - "@cloudflare/workers-types/2023-07-01" - ], - "jsx": "react-jsx", - "jsxImportSource": "hono/jsx" - }, -} \ No newline at end of file diff --git a/packages/api/wrangler.toml b/packages/api/wrangler.toml deleted file mode 100644 index 405ebb38..00000000 --- a/packages/api/wrangler.toml +++ /dev/null @@ -1,26 +0,0 @@ -name = "nexus" -compatibility_date = "2024-08-14" -send_metrics = false - -[[kv_namespaces]] -binding = "nexus" -id = "e21527f9f1ed4adbaca1fa23d7f147c9" - -# [vars] -# MY_VAR = "my-variable" - -# [[kv_namespaces]] -# binding = "MY_KV_NAMESPACE" -# id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" - -# [[r2_buckets]] -# binding = "MY_BUCKET" -# bucket_name = "my-bucket" - -# [[d1_databases]] -# binding = "DB" -# database_name = "my-database" -# database_id = "" - -# [ai] -# binding = "AI" \ No newline at end of file diff --git a/packages/cache/caches.ts b/packages/cache/caches.ts deleted file mode 100644 index 327e530a..00000000 --- a/packages/cache/caches.ts +++ /dev/null @@ -1,47 +0,0 @@ -const buildCacheKey = (namespace: string) => (key: string) => { - return `${namespace}:${key}`; - }; - - export interface KVResponseCache { - match(key: string): Promise; - put(key: string, res: Response, options?: KVNamespacePutOptions): Promise; - delete(key: string): Promise; - } - - export const kvResponseCache = - (kv: KVNamespace) => - (cacheName: string): KVResponseCache => { - const cacheKey = buildCacheKey(cacheName); - - return { - async match(key: string) { - const [headers, status, body] = await Promise.all([ - kv.get(cacheKey(`${key}:headers`)), - kv.get(cacheKey(`${key}:status`)), - kv.get(cacheKey(`${key}:body`), "stream"), - ]); - - if (headers === null || body === null || status === null) return null; - - return new Response(body, { headers: JSON.parse(headers), status: parseInt(status, 10) }); - }, - async put(key: string, res: Response, options?: KVNamespacePutOptions) { - const headers = Array.from(res.headers.entries()).reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}); - const body = res.body; - if (body === null) return; - - await Promise.all([ - kv.put(cacheKey(`${key}:headers`), JSON.stringify(headers), options), - kv.put(cacheKey(`${key}:status`), `${res.status}`, options), - kv.put(cacheKey(`${key}:body`), body, options), - ]); - }, - async delete(key: string) { - await Promise.all([ - kv.delete(cacheKey(`${key}:headers`)), - kv.delete(cacheKey(`${key}:status`)), - kv.delete(cacheKey(`${key}:body`)), - ]); - }, - }; - }; \ No newline at end of file diff --git a/packages/cache/index.ts b/packages/cache/index.ts deleted file mode 100644 index a0518f32..00000000 --- a/packages/cache/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -//copied from https://github.com/napolab/kv-response-cache with some minor changes -import { kvResponseCache } from "./caches"; -import { kvCaches, defaultGetCacheKey } from "./middleware"; - -import type { KVResponseCache } from "./caches"; - -export type { KVResponseCache }; -export { kvResponseCache, kvCaches, defaultGetCacheKey as getCacheKey }; \ No newline at end of file diff --git a/packages/cache/middleware.ts b/packages/cache/middleware.ts deleted file mode 100644 index e001e1e0..00000000 --- a/packages/cache/middleware.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { kvResponseCache } from "./caches"; - -import type { Filter } from "./types"; -import type { Context, Env, MiddlewareHandler } from "hono"; - -type Namespace = string | ((c: Context) => string); -interface GetCacheKey { - (c: Context): string; -} - -type KVCacheOption }> = { - key: keyof E["Bindings"]; - namespace: Namespace; - getCacheKey?: GetCacheKey; - options?: KVNamespacePutOptions; -}; - -export const defaultGetCacheKey = (c: Context) => c.req.url; - -export const kvCaches = - }>({ - key: bindingKey, - namespace, - options, - getCacheKey = defaultGetCacheKey, - }: KVCacheOption): MiddlewareHandler => - async (c, next) => { - const kv: KVNamespace = c.env?.[bindingKey] as KVNamespace; - const kvNamespace = typeof namespace === "function" ? namespace(c) : namespace; - - const kvCaches = kvResponseCache(kv); - const cache = kvCaches(kvNamespace); - - const key = getCacheKey(c); - const response = await cache.match(key); - if (response) { - response.headers.set("X-KV-CACHE", "hit"); - - return response; - } - - await next(); - - if (c.res.status >= 200 && c.res.status < 300) { - c.executionCtx.waitUntil(cache.put(key, c.res.clone(), options)); - } - }; \ No newline at end of file diff --git a/packages/cache/package.json b/packages/cache/package.json deleted file mode 100644 index 1e0fc5a1..00000000 --- a/packages/cache/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "@nestri/cache", - "version": "0.0.0", - "private": true, - "sideEffects": false, - "exports":{ - ".":"./index.ts" - }, - "devDependencies": { - "@cloudflare/workers-types": "^4.20240529.0", - "wrangler": "^3.57.2" - } - } \ No newline at end of file diff --git a/packages/cache/tsconfig.json b/packages/cache/tsconfig.json deleted file mode 100644 index 3fc747d7..00000000 --- a/packages/cache/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "compilerOptions": { - "target": "ESNext", - "module": "ESNext", - "moduleResolution": "Bundler", - "strict": true, - "skipLibCheck": true, - "lib": [ - "ESNext" - ], - "types": [ - "@cloudflare/workers-types/2023-07-01" - ] - }, - } \ No newline at end of file diff --git a/packages/cache/types.ts b/packages/cache/types.ts deleted file mode 100644 index d0aa4616..00000000 --- a/packages/cache/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type Filter, V> = { - [K in keyof T as T[K] extends V ? K : never]: T[K]; -}; \ No newline at end of file diff --git a/packages/certs/.gitignore b/packages/certs/.gitignore deleted file mode 100644 index 21924bc5..00000000 --- a/packages/certs/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -.terraform -relay_* -terraform.tfstate \ No newline at end of file diff --git a/packages/certs/.terraform.lock.hcl b/packages/certs/.terraform.lock.hcl deleted file mode 100644 index 65fa884f..00000000 --- a/packages/certs/.terraform.lock.hcl +++ /dev/null @@ -1,61 +0,0 @@ -# This file is maintained automatically by "terraform init". -# Manual edits may be lost in future updates. - -provider "registry.terraform.io/hashicorp/local" { - version = "2.5.2" - hashes = [ - "h1:JlMZD6nYqJ8sSrFfEAH0Vk/SL8WLZRmFaMUF9PJK5wM=", - "zh:136299545178ce281c56f36965bf91c35407c11897f7082b3b983d86cb79b511", - "zh:3b4486858aa9cb8163378722b642c57c529b6c64bfbfc9461d940a84cd66ebea", - "zh:4855ee628ead847741aa4f4fc9bed50cfdbf197f2912775dd9fe7bc43fa077c0", - "zh:4b8cd2583d1edcac4011caafe8afb7a95e8110a607a1d5fb87d921178074a69b", - "zh:52084ddaff8c8cd3f9e7bcb7ce4dc1eab00602912c96da43c29b4762dc376038", - "zh:71562d330d3f92d79b2952ffdda0dad167e952e46200c767dd30c6af8d7c0ed3", - "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", - "zh:805f81ade06ff68fa8b908d31892eaed5c180ae031c77ad35f82cb7a74b97cf4", - "zh:8b6b3ebeaaa8e38dd04e56996abe80db9be6f4c1df75ac3cccc77642899bd464", - "zh:ad07750576b99248037b897de71113cc19b1a8d0bc235eb99173cc83d0de3b1b", - "zh:b9f1c3bfadb74068f5c205292badb0661e17ac05eb23bfe8bd809691e4583d0e", - "zh:cc4cbcd67414fefb111c1bf7ab0bc4beb8c0b553d01719ad17de9a047adff4d1", - ] -} - -provider "registry.terraform.io/hashicorp/tls" { - version = "4.0.6" - hashes = [ - "h1:dYSb3V94K5dDMtrBRLPzBpkMTPn+3cXZ/kIJdtFL+2M=", - "zh:10de0d8af02f2e578101688fd334da3849f56ea91b0d9bd5b1f7a243417fdda8", - "zh:37fc01f8b2bc9d5b055dc3e78bfd1beb7c42cfb776a4c81106e19c8911366297", - "zh:4578ca03d1dd0b7f572d96bd03f744be24c726bfd282173d54b100fd221608bb", - "zh:6c475491d1250050765a91a493ef330adc24689e8837a0f07da5a0e1269e11c1", - "zh:81bde94d53cdababa5b376bbc6947668be4c45ab655de7aa2e8e4736dfd52509", - "zh:abdce260840b7b050c4e401d4f75c7a199fafe58a8b213947a258f75ac18b3e8", - "zh:b754cebfc5184873840f16a642a7c9ef78c34dc246a8ae29e056c79939963c7a", - "zh:c928b66086078f9917aef0eec15982f2e337914c5c4dbc31dd4741403db7eb18", - "zh:cded27bee5f24de6f2ee0cfd1df46a7f88e84aaffc2ecbf3ff7094160f193d50", - "zh:d65eb3867e8f69aaf1b8bb53bd637c99c6b649ba3db16ded50fa9a01076d1a27", - "zh:ecb0c8b528c7a619fa71852bb3fb5c151d47576c5aab2bf3af4db52588722eeb", - "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", - ] -} - -provider "registry.terraform.io/vancluever/acme" { - version = "2.26.0" - constraints = "~> 2.0" - hashes = [ - "h1:4Lk5cb2Fg1q1JEQf1jkrShjPC3ayukp4eFcdL4e+y0w=", - "zh:11f554916ee99d8930de6d7bb5a014ec636b53ef9ba35eea84b0d2522c78230f", - "zh:231c31271c25477c95e0a4972857b6d5e9d7c3a300cbc4b0948566d87bc46e04", - "zh:2ae165ca7a994a4c77801a82ebd9f2f6de33a4c8882381bea575b3385cc251d8", - "zh:2cf01e4694d81b24972f5dab8e5f374aa59100082ff6e2435615d9c0f24cc00e", - "zh:3de6f6f9d052dfaa5d5f366d7ca26bdebb42fc74b6e19325e67420c37ff630d3", - "zh:3fd2b4b680b970394e4d0d49c2a8e5365297e79cea418ce87197cc8bb456d8c7", - "zh:46ea249cc01dce23ff6c8f02106e693be3b046059834b60b670c45a8f4093980", - "zh:57cb181c73b6e7397744d885c788d8815ad6a43f07769e98c6327bbc37272896", - "zh:761f2adf3e63559bd279763eb91247cdebf31401d79853755453274f143cbb36", - "zh:c4a9905bf81d38201c080cb91ea85002194c47ca26619644628184a56c394b7d", - "zh:d6e3a757c357239edefb640807778fb69805b9ae5df84a811a2d505c51089367", - "zh:d713856e4a459e1091cbb19ffb830d25cd88953d3e54acd46db0729c77a531d8", - "zh:f7cb8dec263d0ee223737dad3b6fa8071258f41cfa9e0b8cf7f337f9f501fc3b", - ] -} diff --git a/packages/certs/README.md b/packages/certs/README.md deleted file mode 100644 index 071f07dd..00000000 --- a/packages/certs/README.md +++ /dev/null @@ -1,24 +0,0 @@ -## Usage -1. Update the terraform.tfvars file with your domain and email. - -2. Run `terraform init` to initialize the Terraform working directory. - -3. Run `terraform plan` to see the planned changes. - -4. Run `terraform apply` to create the resources and obtain the certificate. -Outputs - -The configuration provides two sensitive outputs: -```bash -certificate_pem: The full certificate chain -private_key_pem: The private key for the certificate -``` - -These can be then be used in your `moq-relay` as it requires SSL/TLS certificates. - -## Note -The generated certificate and key files are saved locally and ignored by git: -```git -.terraform -relay_* -``` \ No newline at end of file diff --git a/packages/certs/input.tf b/packages/certs/input.tf deleted file mode 100644 index 4d543861..00000000 --- a/packages/certs/input.tf +++ /dev/null @@ -1,7 +0,0 @@ -variable "email" { - description = "Your email address, used for LetsEncrypt" -} - -variable "domain" { - description = "domain name" -} \ No newline at end of file diff --git a/packages/certs/main.tf b/packages/certs/main.tf deleted file mode 100644 index a4915613..00000000 --- a/packages/certs/main.tf +++ /dev/null @@ -1,65 +0,0 @@ -terraform { - required_providers { - acme = { - source = "vancluever/acme" - version = "~> 2.0" - } - } -} - -provider "acme" { - server_url = "https://acme-v02.api.letsencrypt.org/directory" -} - -resource "acme_registration" "reg" { - email_address = "wanjohiryan33@gmail.com" -} - -resource "tls_private_key" "relay" { - algorithm = "ECDSA" - ecdsa_curve = "P256" -} - -resource "acme_registration" "relay" { - account_key_pem = tls_private_key.relay.private_key_pem - email_address = var.email -} - -resource "acme_certificate" "relay" { - account_key_pem = acme_registration.relay.account_key_pem - common_name = "relay.${var.domain}" - subject_alternative_names = ["*.relay.${var.domain}"] - key_type = tls_private_key.relay.ecdsa_curve - - recursive_nameservers = ["8.8.8.8:53"] - - dns_challenge { - provider = "route53" - } -} - -# New resources to save certificate and private key -resource "local_file" "cert_file" { - content = "${acme_certificate.relay.certificate_pem}${acme_certificate.relay.issuer_pem}" - filename = "${path.module}/relay_cert.crt" - file_permission = "0644" - directory_permission = "0755" -} - -resource "local_file" "key_file" { - content = acme_certificate.relay.private_key_pem - filename = "${path.module}/relay_key.key" - file_permission = "0600" - directory_permission = "0755" -} - -# Outputs for certificate and private key -output "certificate_pem" { - value = "${acme_certificate.relay.certificate_pem}${acme_certificate.relay.issuer_pem}" - sensitive = true -} - -output "private_key_pem" { - value = acme_certificate.relay.private_key_pem - sensitive = true -} diff --git a/packages/certs/terraform.tfvars b/packages/certs/terraform.tfvars deleted file mode 100644 index c63348f1..00000000 --- a/packages/certs/terraform.tfvars +++ /dev/null @@ -1,2 +0,0 @@ -domain = "fst.so" -email = "wanjohiryan33@gmail.com" \ No newline at end of file diff --git a/packages/cli/cmd/root.go b/packages/cli/cmd/root.go new file mode 100644 index 00000000..b008e3e0 --- /dev/null +++ b/packages/cli/cmd/root.go @@ -0,0 +1,50 @@ +package cmd + +import ( + "runtime/debug" + + "github.com/spf13/cobra" +) + +var rootCmd = &cobra.Command{ + Use: "nestri", + Short: "A CLI tool to run and manage your self-hosted cloud gaming service", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return cmd.Help() + }, +} + +// Execute adds all child commands to the root command and sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func Execute() error { + err := rootCmd.Execute() + return err +} + +var ( + // Version stores the build version of VHS at the time of package through + // -ldflags. + // + // go build -ldflags "-s -w -X=main.Version=$(VERSION)" + Version string + + // CommitSHA stores the git commit SHA at the time of package through -ldflags. + CommitSHA string +) + +func init() { + rootCmd.AddCommand(runCmd) + if len(CommitSHA) >= 7 { //nolint:gomnd + vt := rootCmd.VersionTemplate() + rootCmd.SetVersionTemplate(vt[:len(vt)-1] + " (" + CommitSHA[0:7] + ")\n") + } + if Version == "" { + if info, ok := debug.ReadBuildInfo(); ok && info.Main.Sum != "" { + Version = info.Main.Version + } else { + Version = "unknown (built from source)" + } + } + rootCmd.Version = Version +} diff --git a/packages/cli/cmd/run.go b/packages/cli/cmd/run.go new file mode 100644 index 00000000..84fe45a7 --- /dev/null +++ b/packages/cli/cmd/run.go @@ -0,0 +1,26 @@ +package cmd + +import ( + "nestrilabs/cli/internal/auth" + + "github.com/charmbracelet/log" + "github.com/spf13/cobra" +) + +var runCmd = &cobra.Command{ + Use: "run", + Short: "Run a new Nestri node", + Long: "Create and run a new Nestri node from this machine", + Args: cobra.NoArgs, + RunE: func(_ *cobra.Command, _ []string) error { + credentials, err := auth.FetchUserCredentials() + if err != nil { + return err + } + + log.Info("Credentials", "access_token", credentials.AccessToken) + log.Info("Credentials", "refresh_token", credentials.RefreshToken) + + return nil + }, +} diff --git a/packages/cli/go.mod b/packages/cli/go.mod new file mode 100644 index 00000000..9598c240 --- /dev/null +++ b/packages/cli/go.mod @@ -0,0 +1,54 @@ +module nestrilabs/cli + +go 1.23.3 + +require ( + github.com/charmbracelet/log v0.4.0 + github.com/docker/docker v27.4.1+incompatible + github.com/gorilla/websocket v1.5.3 + github.com/nestrilabs/nestri-go-sdk v0.1.0-alpha.3 + github.com/spf13/cobra v1.8.1 +) + +require ( + github.com/Microsoft/go-winio v0.4.14 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/charmbracelet/lipgloss v0.13.0 // indirect + github.com/charmbracelet/x/ansi v0.2.3 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/go-connections v0.5.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logfmt/logfmt v0.6.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/term v0.5.0 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/tidwall/gjson v1.18.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.1 // indirect + github.com/tidwall/sjson v1.2.5 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect + go.opentelemetry.io/otel v1.33.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 // indirect + go.opentelemetry.io/otel/metric v1.33.0 // indirect + go.opentelemetry.io/otel/sdk v1.33.0 // indirect + go.opentelemetry.io/otel/trace v1.33.0 // indirect + golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/time v0.8.0 // indirect + gotest.tools/v3 v3.5.1 // indirect +) diff --git a/packages/master/go.sum b/packages/cli/go.sum similarity index 53% rename from packages/master/go.sum rename to packages/cli/go.sum index 4acbdbf3..66f1c359 100644 --- a/packages/master/go.sum +++ b/packages/cli/go.sum @@ -2,22 +2,33 @@ github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOEl github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU= github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw= +github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY= +github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM= +github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM= +github.com/charmbracelet/x/ansi v0.2.3 h1:VfFN0NUpcjBRd4DnKfRaIRo53KRgey/nhOoEqosGDEY= +github.com/charmbracelet/x/ansi v0.2.3/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI= -github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v27.4.1+incompatible h1:ZJvcY7gfwHn1JF48PfbyXg7Jyt9ZCWDW+GGXOIxEwp4= +github.com/docker/docker v27.4.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= +github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -29,17 +40,29 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0 h1:ad0vkEBuk23VJzZR9nkLVG0YAoN9coASF1GusYX6AlU= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0/go.mod h1:igFoXX2ELCW06bol23DWPB5BEWfZISOzSP5K2sbLea0= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 h1:TmHmbvxPmaegwhDubVz0lICL0J5Ka2vwTzhoePEXsGE= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a h1:2MaM6YC3mGu54x+RKAA6JiFFHlHDY1UbkxqppT7wYOg= +github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a/go.mod h1:hxSnBBYLK21Vtq/PHd0S2FYCxBXzBua8ov5s1RobyRQ= +github.com/nestrilabs/nestri-go-sdk v0.1.0-alpha.3 h1:IqtLHbOF3y/SD3riYYKauQKj9dpqU7uuEExqL5zQ390= +github.com/nestrilabs/nestri-go-sdk v0.1.0-alpha.3/go.mod h1:b4AuAQSxfqtAzu4ie0Q+NOVNF9YUZTyP4XnxK0ZN05U= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= @@ -49,42 +72,63 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 h1:DheMAlT6POBP+gh8RUH19EOTnQIor5QE0uSRPtzCpSw= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0/go.mod h1:wZcGmeVO9nzP67aYSLDqXNWK87EZWhi7JWj1v7ZXf94= -go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U= -go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0 h1:IJFEoHiytixx8cMiVAO+GmHR6Frwu+u5Ur8njpFO6Ac= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0/go.mod h1:3rHrKNtLIoS0oZwkY2vxi+oJcwFRWdtUyRII+so45p8= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0 h1:cMyu9O88joYEaI47CnQkxO1XZdpoTF9fEnW2duIddhw= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0/go.mod h1:6Am3rn7P9TVVeXYG+wtcGE7IE1tsQ+bP3AuWcKt/gOI= -go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M= -go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8= -go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4= -go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU= -go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM= -go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8= -go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= -go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= +go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw= +go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 h1:wpMfgF8E1rkrT1Z6meFh1NDtownE9Ii3n3X2GJYjsaU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0/go.mod h1:wAy0T/dUbs468uOlkT31xjvqQgEVXv58BRFWEgn5v/0= +go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ= +go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M= +go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM= +go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM= +go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s= +go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck= +go.opentelemetry.io/proto/otlp v1.4.0 h1:TA9WRvW6zMwP+Ssb6fLoUIuirti1gGbP28GcKG1jgeg= +go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= -golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI= +golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -93,12 +137,13 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= -golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= -golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -109,14 +154,15 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28 h1:M0KvPgPmDZHPlbRbaNU1APr28TvwvvdUPlSv7PUvy8g= -google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28/go.mod h1:dguCy7UOdZhTvLzDyt15+rOrawrpM4q7DD9dQ1P11P4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28 h1:XVhgTWWV3kGQlwJHR3upFWZeTsei6Oks1apkZSeonIE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= -google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= -google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= -google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= -google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q= +google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 h1:8ZmaLZE4XWrtU3MyClkYqqtl6Oegr3235h7jxsDyqCY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= +google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0= +google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw= +google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= +google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= diff --git a/packages/cli/internal/api/api.go b/packages/cli/internal/api/api.go new file mode 100644 index 00000000..7c32cf87 --- /dev/null +++ b/packages/cli/internal/api/api.go @@ -0,0 +1,26 @@ +package api + +import ( + "context" + "fmt" + "nestrilabs/cli/internal/resource" + + "github.com/nestrilabs/nestri-go-sdk" + "github.com/nestrilabs/nestri-go-sdk/option" +) + +func RegisterMachine(token string) { + client := nestri.NewClient( + option.WithBearerToken(token), + option.WithBaseURL(resource.Resource.Api.Url), + ) + + machine, err := client.Machines.New( + context.TODO(), + nestri.MachineNewParams{}) + + if err != nil { + panic(err.Error()) + } + fmt.Printf("%+v\n", machine.Data) +} diff --git a/packages/cli/internal/auth/auth.go b/packages/cli/internal/auth/auth.go new file mode 100644 index 00000000..3f82461f --- /dev/null +++ b/packages/cli/internal/auth/auth.go @@ -0,0 +1,44 @@ +package auth + +import ( + "encoding/json" + "fmt" + "io" + "nestrilabs/cli/internal/machine" + "nestrilabs/cli/internal/resource" + "net/http" + "net/url" +) + +type UserCredentials struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` +} + +func FetchUserCredentials() (*UserCredentials, error) { + m := machine.NewMachine() + fingerprint := m.GetMachineID() + data := url.Values{} + data.Set("grant_type", "client_credentials") + data.Set("client_id", "device") + data.Set("client_secret", resource.Resource.AuthFingerprintKey.Value) + data.Set("hostname", m.Hostname) + data.Set("fingerprint", fingerprint) + data.Set("provider", "device") + resp, err := http.PostForm(resource.Resource.Auth.Url+"/token", data) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != 200 { + body, _ := io.ReadAll(resp.Body) + fmt.Println(string(body)) + return nil, fmt.Errorf("failed to auth: " + string(body)) + } + credentials := UserCredentials{} + err = json.NewDecoder(resp.Body).Decode(&credentials) + if err != nil { + return nil, err + } + return &credentials, nil +} diff --git a/packages/cli/internal/machine/machine.go b/packages/cli/internal/machine/machine.go new file mode 100644 index 00000000..5c598f9b --- /dev/null +++ b/packages/cli/internal/machine/machine.go @@ -0,0 +1,202 @@ +package machine + +import ( + "fmt" + "os" + "os/exec" + "regexp" + "strings" + + "github.com/charmbracelet/log" +) + +type Machine struct { + OperatingSystem string + Arch string + Kernel string + Virtualization string + Hostname string +} + +func NewMachine() *Machine { + var OS string + var architecture string + var kernel string + var virtualisation string + var hostname string + + output, _ := exec.Command("hostnamectl", "status").Output() + os := regexp.MustCompile(`Operating System:\s+(.*)`) + matchingOS := os.FindStringSubmatch(string(output)) + if len(matchingOS) > 1 { + OS = matchingOS[1] + } + + arch := regexp.MustCompile(`Architecture:\s+(\w+)`) + matchingArch := arch.FindStringSubmatch(string(output)) + if len(matchingArch) > 1 { + architecture = matchingArch[1] + } + + kern := regexp.MustCompile(`Kernel:\s+(.*)`) + matchingKernel := kern.FindStringSubmatch(string(output)) + if len(matchingKernel) > 1 { + kernel = matchingKernel[1] + } + + virt := regexp.MustCompile(`Virtualization:\s+(\w+)`) + matchingVirt := virt.FindStringSubmatch(string(output)) + if len(matchingVirt) > 1 { + virtualisation = matchingVirt[1] + } + + host := regexp.MustCompile(`Static hostname:\s+(.*)`) + matchingHost := host.FindStringSubmatch(string(output)) + if len(matchingHost) > 1 { + hostname = matchingHost[1] + } + + return &Machine{ + OperatingSystem: OS, + Arch: architecture, + Kernel: kernel, + Virtualization: virtualisation, + Hostname: hostname, + } +} + +func (m *Machine) GetOS() string { + if m.OperatingSystem != "" { + return m.OperatingSystem + } + return "unknown" +} + +func (m *Machine) GetArchitecture() string { + + if m.Arch != "" { + return m.Arch + } + return "unknown" + +} + +func (m *Machine) GetKernel() string { + if m.Kernel != "" { + return m.Kernel + } + return "unknown" +} + +func (m *Machine) GetVirtualization() string { + if m.Virtualization != "" { + return m.Virtualization + } + return "none" +} + +func (m *Machine) GetHostname() string { + if m.Hostname != "" { + return m.Hostname + } + return "unknown" +} + +func (m *Machine) GetMachineID() string { + id, err := os.ReadFile("/etc/machine-id") + if err != nil { + log.Error("Error getting your machine's ID", "err", err) + os.Exit(1) + } + return strings.TrimSpace(string(id)) +} + +func (m *Machine) GPUInfo() (string, string, error) { + // The command for GPU information varies depending on the system and drivers. + // lshw is a good general-purpose tool, but might need adjustments for specific hardware. + output, err := exec.Command("lshw", "-C", "display").Output() + if err != nil { + return "", "", fmt.Errorf("failed to get GPU information: %w", err) + } + + gpuType := "" + gpuSize := "" + + // Regular expressions for extracting product and size information. These might need to be + // adapted based on the output of lshw on your specific system. + typeRegex := regexp.MustCompile(`product:\s+(.*)`) + sizeRegex := regexp.MustCompile(`size:\s+(\d+MiB)`) // Example: extracts size in MiB + + typeMatch := typeRegex.FindStringSubmatch(string(output)) + if len(typeMatch) > 1 { + gpuType = typeMatch[1] + } + + sizeMatch := sizeRegex.FindStringSubmatch(string(output)) + if len(sizeMatch) > 1 { + gpuSize = sizeMatch[1] + } + + if gpuType == "" && gpuSize == "" { + return "", "", fmt.Errorf("could not parse GPU information using lshw") + } + + return gpuType, gpuSize, nil +} + +func (m *Machine) GetCPUInfo() (string, string, error) { + output, err := exec.Command("lscpu").Output() + if err != nil { + return "", "", fmt.Errorf("failed to get CPU information: %w", err) + } + + cpuType := "" + cpuSize := "" // This will store the number of cores + + typeRegex := regexp.MustCompile(`Model name:\s+(.*)`) + coresRegex := regexp.MustCompile(`CPU\(s\):\s+(\d+)`) + + typeMatch := typeRegex.FindStringSubmatch(string(output)) + if len(typeMatch) > 1 { + cpuType = typeMatch[1] + } + + coresMatch := coresRegex.FindStringSubmatch(string(output)) + if len(coresMatch) > 1 { + cpuSize = coresMatch[1] + } + + if cpuType == "" && cpuSize == "" { + return "", "", fmt.Errorf("could not parse CPU information using lscpu") + } + + return cpuType, cpuSize, nil + +} + +func (m *Machine) GetRAMSize() (string, error) { + output, err := exec.Command("free", "-h", "--si").Output() // Using -h for human-readable and --si for base-10 units + if err != nil { + return "", fmt.Errorf("failed to get RAM information: %w", err) + } + + ramSize := "" + + ramRegex := regexp.MustCompile(`Mem:\s+(\S+)`) // Matches the total memory size + + ramMatch := ramRegex.FindStringSubmatch(string(output)) + if len(ramMatch) > 1 { + ramSize = ramMatch[1] + } else { + return "", fmt.Errorf("could not parse RAM information from free command") + } + + return ramSize, nil +} + +// func cleanString(s string) string { +// s = strings.ToLower(s) + +// reg := regexp.MustCompile("[^a-z0-9]+") // Matches one or more non-alphanumeric characters +// return reg.ReplaceAllString(s, "") +// } diff --git a/packages/cli/internal/party/client.go b/packages/cli/internal/party/client.go new file mode 100644 index 00000000..24e6fefa --- /dev/null +++ b/packages/cli/internal/party/client.go @@ -0,0 +1,112 @@ +package party + +import ( + "fmt" + "nestrilabs/cli/internal/machine" + "net/url" + "time" + + "github.com/charmbracelet/log" + "github.com/gorilla/websocket" +) + +const ( + // Initial retry delay + initialRetryDelay = 1 * time.Second + // Maximum retry delay + maxRetryDelay = 30 * time.Second + // Factor to increase delay by after each attempt + backoffFactor = 2 +) + +type Party struct { + // Channel to signal shutdown + done chan struct{} + fingerprint string + hostname string +} + +func NewParty() *Party { + m := machine.NewMachine() + fingerpint := m.GetMachineID() + return &Party{ + done: make(chan struct{}), + fingerprint: fingerpint, + hostname: m.Hostname, + } +} + +// Shutdown gracefully closes the connection +func (p *Party) Shutdown() { + close(p.done) +} + +func (p *Party) Connect() { + baseURL := fmt.Sprintf("ws://localhost:1999/parties/main/%s", p.fingerprint) + params := url.Values{} + params.Add("_pk", p.hostname) + wsURL := baseURL + "?" + params.Encode() + + retryDelay := initialRetryDelay + + for { + select { + case <-p.done: + log.Info("Shutting down connection") + return + default: + conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil) + if err != nil { + log.Error("Failed to connect to party server", "err", err) + time.Sleep(retryDelay) + // Increase retry delay exponentially, but cap it + retryDelay = time.Duration(float64(retryDelay) * backoffFactor) + if retryDelay > maxRetryDelay { + retryDelay = maxRetryDelay + } + continue + } + + // Reset retry delay on successful connection + retryDelay = initialRetryDelay + + // Handle connection in a separate goroutine + connectionClosed := make(chan struct{}) + go func() { + defer close(connectionClosed) + defer conn.Close() + + // Send initial message + if err := conn.WriteMessage(websocket.TextMessage, []byte("hello there")); err != nil { + log.Error("Failed to send initial message", "err", err) + return + } + + // Read messages loop + for { + select { + case <-p.done: + return + default: + _, message, err := conn.ReadMessage() + if err != nil { + log.Error("Error reading message", "err", err) + return + } + log.Info("Received message from party server", "message", string(message)) + } + } + }() + + // Wait for either connection to close or shutdown signal + select { + case <-connectionClosed: + log.Warn("Connection closed, attempting to reconnect...") + time.Sleep(retryDelay) + case <-p.done: + log.Info("Shutting down connection") + return + } + } + } +} diff --git a/packages/cli/internal/party/retry.go b/packages/cli/internal/party/retry.go new file mode 100644 index 00000000..7b635800 --- /dev/null +++ b/packages/cli/internal/party/retry.go @@ -0,0 +1,125 @@ +package party + +import ( + "encoding/json" + "fmt" + "nestrilabs/cli/internal/machine" + "net/url" + "time" + + "github.com/charmbracelet/log" + "github.com/gorilla/websocket" +) + +// RetryConfig holds configuration for retry behavior +type RetryConfig struct { + InitialDelay time.Duration + MaxDelay time.Duration + BackoffFactor float64 + MaxAttempts int // use 0 for infinite retries +} + +// DefaultRetryConfig provides sensible default values +var DefaultRetryConfig = RetryConfig{ + InitialDelay: time.Second, + MaxDelay: 30 * time.Second, + BackoffFactor: 2.0, + MaxAttempts: 0, // infinite retries +} + +// RetryFunc is a function that will be retried +type RetryFunc[T any] func() (T, error) + +// Retry executes the given function with retries based on the config +func Retry[T any](config RetryConfig, operation RetryFunc[T]) (T, error) { + var result T + currentDelay := config.InitialDelay + attempts := 0 + + for { + if config.MaxAttempts > 0 && attempts >= config.MaxAttempts { + return result, fmt.Errorf("max retry attempts (%d) exceeded", config.MaxAttempts) + } + + result, err := operation() + if err == nil { + return result, nil + } + + log.Warn("Operation failed, retrying...", + "attempt", attempts+1, + "delay", currentDelay, + "error", err) + + time.Sleep(currentDelay) + + // Increase delay for next attempt + currentDelay = time.Duration(float64(currentDelay) * config.BackoffFactor) + if currentDelay > config.MaxDelay { + currentDelay = config.MaxDelay + } + + attempts++ + } +} + +// MessageHandler processes a message and returns true if it's the expected type +type MessageHandler[T any] func(msg T) bool + +type TypeListener[T any] struct { + retryConfig RetryConfig + handler MessageHandler[T] + fingerprint string + hostname string +} + +func NewTypeListener[T any](handler MessageHandler[T]) *TypeListener[T] { + m := machine.NewMachine() + fingerprint := m.GetMachineID() + + return &TypeListener[T]{ + retryConfig: DefaultRetryConfig, + handler: handler, + fingerprint: fingerprint, + hostname: m.Hostname, + } +} + +// SetRetryConfig allows customizing the retry behavior +func (t *TypeListener[T]) SetRetryConfig(config RetryConfig) { + t.retryConfig = config +} + +func (t *TypeListener[T]) ConnectUntilMessage() (T, error) { + baseURL := fmt.Sprintf("ws://localhost:1999/parties/main/%s", t.fingerprint) + params := url.Values{} + params.Add("_pk", t.hostname) + wsURL := baseURL + "?" + params.Encode() + + return Retry(t.retryConfig, func() (T, error) { + var result T + + conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil) + if err != nil { + return result, fmt.Errorf("connection failed: %w", err) + } + defer conn.Close() + + // Read messages until we get the one we want + for { + _, message, err := conn.ReadMessage() + if err != nil { + return result, fmt.Errorf("read error: %w", err) + } + + if err := json.Unmarshal(message, &result); err != nil { + // log.Error("Failed to unmarshal message", "err", err) + continue + } + + if t.handler(result) { + return result, nil + } + } + }) +} diff --git a/packages/cli/internal/resource/resource.go b/packages/cli/internal/resource/resource.go new file mode 100644 index 00000000..cd8044bc --- /dev/null +++ b/packages/cli/internal/resource/resource.go @@ -0,0 +1,38 @@ +package resource + +import ( + "encoding/json" + "fmt" + "os" + "reflect" +) + +type resource struct { + Api struct { + Url string `json:"url"` + } + Auth struct { + Url string `json:"url"` + } + AuthFingerprintKey struct { + Value string `json:"value"` + } +} + +var Resource resource + +func init() { + val := reflect.ValueOf(&Resource).Elem() + for i := 0; i < val.NumField(); i++ { + field := val.Field(i) + typeField := val.Type().Field(i) + envVarName := fmt.Sprintf("SST_RESOURCE_%s", typeField.Name) + envValue, exists := os.LookupEnv(envVarName) + if !exists { + panic(fmt.Sprintf("Environment variable %s is required", envVarName)) + } + if err := json.Unmarshal([]byte(envValue), field.Addr().Interface()); err != nil { + panic(err) + } + } +} diff --git a/packages/cli/internal/session/start.go b/packages/cli/internal/session/start.go new file mode 100644 index 00000000..25118ba8 --- /dev/null +++ b/packages/cli/internal/session/start.go @@ -0,0 +1,286 @@ +package session + +import ( + "context" + "fmt" + "io" + "os" + "strings" + "sync" + + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/image" + "github.com/docker/docker/client" +) + +// GPUType represents the type of GPU available +type GPUType int + +const ( + GPUNone GPUType = iota + GPUNvidia + GPUIntelAMD +) + +// Session represents a Docker container session +type Session struct { + client *client.Client + containerID string + imageName string + config *SessionConfig + mu sync.RWMutex + isRunning bool +} + +// SessionConfig holds the configuration for the session +type SessionConfig struct { + Room string + Resolution string + Framerate string + RelayURL string + Params string + GamePath string +} + +// NewSession creates a new Docker session +func NewSession(config *SessionConfig) (*Session, error) { + cli, err := client.NewClientWithOpts(client.FromEnv) + if err != nil { + return nil, fmt.Errorf("failed to create Docker client: %v", err) + } + + return &Session{ + client: cli, + imageName: "archlinux", //"ghcr.io/datcaptainhorse/nestri-cachyos:latest-noavx2", + config: config, + }, nil +} + +// Start initiates the Docker container session +func (s *Session) Start(ctx context.Context) error { + s.mu.Lock() + defer s.mu.Unlock() + + if s.isRunning { + return fmt.Errorf("session is already running") + } + + // Detect GPU type + gpuType := detectGPU() + if gpuType == GPUNone { + return fmt.Errorf("no supported GPU detected") + } + + // Get GPU-specific configurations + deviceRequests, err := getGPUDeviceRequests(gpuType) + if err != nil { + return err + } + + devices := getGPUDevices(gpuType) + + // Check if image exists locally + _, _, err = s.client.ImageInspectWithRaw(ctx, s.imageName) + if err != nil { + // Pull the image if it doesn't exist + reader, err := s.client.ImagePull(ctx, s.imageName, image.PullOptions{}) + if err != nil { + return fmt.Errorf("failed to pull image: %v", err) + } + defer reader.Close() + + // Copy pull output to stdout + io.Copy(os.Stdout, reader) + } + + // Create container + resp, err := s.client.ContainerCreate(ctx, &container.Config{ + Image: s.imageName, + Env: []string{ + fmt.Sprintf("NESTRI_ROOM=%s", s.config.Room), + fmt.Sprintf("RESOLUTION=%s", s.config.Resolution), + fmt.Sprintf("NESTRI_PARAMS=%s", s.config.Params), + fmt.Sprintf("FRAMERATE=%s", s.config.Framerate), + fmt.Sprintf("RELAY_URL=%s", s.config.RelayURL), + }, + }, &container.HostConfig{ + Binds: []string{ + fmt.Sprintf("%s:/home/nestri/.steam/", s.config.GamePath), + }, + Resources: container.Resources{ + DeviceRequests: deviceRequests, + Devices: devices, + }, + SecurityOpt: []string{"label=disable"}, + ShmSize: 5368709120, // 5GB + // ShmSize: 1073741824, // 1GB + }, nil, nil, "") + if err != nil { + return fmt.Errorf("failed to create container: %v", err) + } + + // Start container + if err := s.client.ContainerStart(ctx, resp.ID, container.StartOptions{}); err != nil { + return fmt.Errorf("failed to start container: %v", err) + } + + // Store container ID and update state + s.containerID = resp.ID + s.isRunning = true + + // Start logging in a goroutine + go s.streamLogs(ctx) + + return nil +} + +// Stop stops the Docker container session +func (s *Session) Stop(ctx context.Context) error { + s.mu.Lock() + defer s.mu.Unlock() + + if !s.isRunning { + return fmt.Errorf("session is not running") + } + + timeout := 30 // seconds + if err := s.client.ContainerStop(ctx, s.containerID, container.StopOptions{Timeout: &timeout}); err != nil { + return fmt.Errorf("failed to stop container: %v", err) + } + + if err := s.client.ContainerRemove(ctx, s.containerID, container.RemoveOptions{}); err != nil { + return fmt.Errorf("failed to remove container: %v", err) + } + + s.isRunning = false + s.containerID = "" + return nil +} + +// IsRunning returns the current state of the session +func (s *Session) IsRunning() bool { + s.mu.RLock() + defer s.mu.RUnlock() + return s.isRunning +} + +// GetContainerID returns the current container ID +func (s *Session) GetContainerID() string { + s.mu.RLock() + defer s.mu.RUnlock() + return s.containerID +} + +// streamLogs streams container logs to stdout +func (s *Session) streamLogs(ctx context.Context) { + opts := container.LogsOptions{ + ShowStdout: true, + ShowStderr: true, + Follow: true, + } + + logs, err := s.client.ContainerLogs(ctx, s.containerID, opts) + if err != nil { + fmt.Fprintf(os.Stderr, "Error getting container logs: %v\n", err) + return + } + defer logs.Close() + + _, err = io.Copy(os.Stdout, logs) + if err != nil { + fmt.Fprintf(os.Stderr, "Error streaming logs: %v\n", err) + } +} + +// VerifyEnvironment checks if all expected environment variables are set correctly in the container +func (s *Session) VerifyEnvironment(ctx context.Context) error { + s.mu.RLock() + defer s.mu.RUnlock() + + if !s.isRunning { + return fmt.Errorf("session is not running") + } + + // Get container info to verify it's actually running + inspect, err := s.client.ContainerInspect(ctx, s.containerID) + if err != nil { + return fmt.Errorf("failed to inspect container: %v", err) + } + + if !inspect.State.Running { + return fmt.Errorf("container is not in running state") + } + + // Expected environment variables + expectedEnv := map[string]string{ + "NESTRI_ROOM": s.config.Room, + "RESOLUTION": s.config.Resolution, + "FRAMERATE": s.config.Framerate, + "RELAY_URL": s.config.RelayURL, + "NESTRI_PARAMS": s.config.Params, + } + + // Get actual environment variables from container + containerEnv := make(map[string]string) + for _, env := range inspect.Config.Env { + parts := strings.SplitN(env, "=", 2) + if len(parts) == 2 { + containerEnv[parts[0]] = parts[1] + } + } + + // Check each expected variable + var missingVars []string + var mismatchedVars []string + + for key, expectedValue := range expectedEnv { + actualValue, exists := containerEnv[key] + if !exists { + missingVars = append(missingVars, key) + } else if actualValue != expectedValue { + mismatchedVars = append(mismatchedVars, fmt.Sprintf("%s (expected: %s, got: %s)", + key, expectedValue, actualValue)) + } + } + + // Build error message if there are any issues + if len(missingVars) > 0 || len(mismatchedVars) > 0 { + var errorMsg strings.Builder + if len(missingVars) > 0 { + errorMsg.WriteString(fmt.Sprintf("Missing environment variables: %s\n", + strings.Join(missingVars, ", "))) + } + if len(mismatchedVars) > 0 { + errorMsg.WriteString(fmt.Sprintf("Mismatched environment variables: %s", + strings.Join(mismatchedVars, ", "))) + } + return fmt.Errorf(errorMsg.String()) + } + + return nil +} + +// GetEnvironment returns all environment variables in the container +func (s *Session) GetEnvironment(ctx context.Context) (map[string]string, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + if !s.isRunning { + return nil, fmt.Errorf("session is not running") + } + + inspect, err := s.client.ContainerInspect(ctx, s.containerID) + if err != nil { + return nil, fmt.Errorf("failed to inspect container: %v", err) + } + + env := make(map[string]string) + for _, e := range inspect.Config.Env { + parts := strings.SplitN(e, "=", 2) + if len(parts) == 2 { + env[parts[0]] = parts[1] + } + } + + return env, nil +} diff --git a/packages/cli/internal/session/steam.go b/packages/cli/internal/session/steam.go new file mode 100644 index 00000000..3498b992 --- /dev/null +++ b/packages/cli/internal/session/steam.go @@ -0,0 +1,76 @@ +package session + +import ( + "bytes" + "context" + "fmt" + "io" + "strings" + + "github.com/docker/docker/api/types/container" +) + +// ExecResult holds the output from a container command +type ExecResult struct { + ExitCode int + Stdout string + Stderr string +} + +func (s *Session) execInContainer(ctx context.Context, cmd []string) (*ExecResult, error) { + execConfig := container.ExecOptions{ + Cmd: cmd, + AttachStdout: true, + AttachStderr: true, + } + + execID, err := s.client.ContainerExecCreate(ctx, s.containerID, execConfig) + if err != nil { + return nil, err + } + + resp, err := s.client.ContainerExecAttach(ctx, execID.ID, container.ExecAttachOptions{}) + if err != nil { + return nil, err + } + defer resp.Close() + + var outBuf bytes.Buffer + _, err = io.Copy(&outBuf, resp.Reader) + if err != nil { + return nil, err + } + + inspect, err := s.client.ContainerExecInspect(ctx, execID.ID) + if err != nil { + return nil, err + } + + return &ExecResult{ + ExitCode: inspect.ExitCode, + Stdout: outBuf.String(), + }, nil +} + +// CheckSteamGames returns the list of installed games in the container +func (s *Session) CheckInstalledSteamGames(ctx context.Context) ([]uint64, error) { + result, err := s.execInContainer(ctx, []string{ + "sh", "-c", + "find /home/nestri/.steam/steam/steamapps -name '*.acf' -exec grep -H '\"appid\"' {} \\;", + }) + if err != nil { + return nil, fmt.Errorf("failed to check steam games: %v", err) + } + + var gameIDs []uint64 + for _, line := range strings.Split(result.Stdout, "\n") { + if strings.Contains(line, "appid") { + var id uint64 + if _, err := fmt.Sscanf(line, `"appid" "%d"`, &id); err == nil { + gameIDs = append(gameIDs, id) + } + } + } + + return gameIDs, nil +} diff --git a/packages/cli/internal/session/utils.go b/packages/cli/internal/session/utils.go new file mode 100644 index 00000000..86aa31b3 --- /dev/null +++ b/packages/cli/internal/session/utils.go @@ -0,0 +1,72 @@ +package session + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + + "github.com/docker/docker/api/types/container" +) + +// detectGPU checks for available GPU type +func detectGPU() GPUType { + // First check for NVIDIA + cmd := exec.Command("nvidia-smi") + if err := cmd.Run(); err == nil { + return GPUNvidia + } + + // Check for Intel/AMD GPU by looking for DRI devices + if _, err := os.Stat("/dev/dri"); err == nil { + return GPUIntelAMD + } + + return GPUNone +} + +// getGPUDeviceRequests returns appropriate device configuration based on GPU type +func getGPUDeviceRequests(gpuType GPUType) ([]container.DeviceRequest, error) { + switch gpuType { + case GPUNvidia: + return []container.DeviceRequest{ + { + Driver: "nvidia", + Count: 1, + DeviceIDs: []string{"0"}, + Capabilities: [][]string{{"gpu"}}, + }, + }, nil + case GPUIntelAMD: + return []container.DeviceRequest{}, nil // Empty as we'll handle this in Devices + default: + return nil, fmt.Errorf("no supported GPU detected") + } +} + +// getGPUDevices returns appropriate device mappings based on GPU type +func getGPUDevices(gpuType GPUType) []container.DeviceMapping { + if gpuType == GPUIntelAMD { + devices := []container.DeviceMapping{} + // Only look for card and renderD nodes + for _, pattern := range []string{"card[0-9]*", "renderD[0-9]*"} { + matches, err := filepath.Glob(fmt.Sprintf("/dev/dri/%s", pattern)) + if err != nil { + continue + } + + for _, match := range matches { + // Verify it's a device file + if info, err := os.Stat(match); err == nil && (info.Mode()&os.ModeDevice) != 0 { + devices = append(devices, container.DeviceMapping{ + PathOnHost: match, + PathInContainer: match, + CgroupPermissions: "rwm", + }) + } + } + } + return devices + } + return nil +} diff --git a/packages/cli/main.go b/packages/cli/main.go new file mode 100644 index 00000000..98534501 --- /dev/null +++ b/packages/cli/main.go @@ -0,0 +1,58 @@ +package main + +import ( + "context" + "nestrilabs/cli/internal/session" + + "github.com/charmbracelet/log" +) + +func main() { + // err := cmd.Execute() + // if err != nil { + // log.Error("Error running the cmd command", "err", err) + // } + + ctx := context.Background() + + config := &session.SessionConfig{ + Room: "victortest", + Resolution: "1920x1080", + Framerate: "60", + RelayURL: "https://relay.dathorse.com", + Params: "--verbose=true --video-codec=h264 --video-bitrate=4000 --video-bitrate-max=6000 --gpu-card-path=/dev/dri/card1", + GamePath: "/path/to/your/game", + } + + sess, err := session.NewSession(config) + if err != nil { + log.Error("Failed to create session", "err", err) + } + + // Start the session + if err := sess.Start(ctx); err != nil { + log.Error("Failed to start session", "err", err) + } + + // Check if it's running + if sess.IsRunning() { + log.Info("Session is running with container ID", "containerId", sess.GetContainerID()) + } + + env, err := sess.GetEnvironment(ctx) + if err != nil { + log.Printf("Failed to get environment: %v", err) + } else { + for key, value := range env { + log.Info("Found this environment variables", key, value) + } + } + + // Let it run for a while + // time.Sleep(time.Second * 50) + + // Stop the session + if err := sess.Stop(ctx); err != nil { + log.Error("Failed to stop session", "err", err) + } +} diff --git a/packages/core/image-brightness-analyzer.ts b/packages/core/image-brightness-analyzer.ts deleted file mode 100644 index a20b688d..00000000 --- a/packages/core/image-brightness-analyzer.ts +++ /dev/null @@ -1,73 +0,0 @@ -// image-brightness-analyzer.js - -export class ImageBrightnessAnalyzer { - canvas: HTMLCanvasElement; - ctx: CanvasRenderingContext2D ; - - constructor() { - this.canvas = document.createElement('canvas'); - this.ctx = this.canvas.getContext('2d')!; - } - - analyze(imgElement: HTMLImageElement) { - if (!(imgElement instanceof HTMLImageElement)) { - throw new Error('Input must be an HTMLImageElement'); - } - - this.canvas.width = imgElement.width; - this.canvas.height = imgElement.height; - this.ctx.drawImage(imgElement, 0, 0); - - const imageData = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height); - const data = imageData.data; - - let brightestPixel = { value: 0, x: 0, y: 0 }; - let dullestPixel = { value: 765, x: 0, y: 0 }; // 765 is the max value (255 * 3) - - for (let y = 0; y < this.canvas.height; y++) { - for (let x = 0; x < this.canvas.width; x++) { - const index = (y * this.canvas.width + x) * 4; - const brightness = data[index] + data[index + 1] + data[index + 2]; - - if (brightness > brightestPixel.value) { - brightestPixel = { value: brightness, x, y }; - } - if (brightness < dullestPixel.value) { - dullestPixel = { value: brightness, x, y }; - } - } - } - - return { - brightest: { - x: brightestPixel.x, - y: brightestPixel.y, - color: this.getPixelColor(data, brightestPixel.x, brightestPixel.y) - }, - dullest: { - x: dullestPixel.x, - y: dullestPixel.y, - color: this.getPixelColor(data, dullestPixel.x, dullestPixel.y) - } - }; - } - - getPixelColor(data: any[] | Uint8ClampedArray, x: number, y: number) { - const index = (y * this.canvas.width + x) * 4; - return { - r: data[index], - g: data[index + 1], - b: data[index + 2] - }; - } - } - -// // Export the class for use in browser environments -// if (typeof window !== 'undefined') { -// window.ImageBrightnessAnalyzer = ImageBrightnessAnalyzer; -// } - -// // Export for module environments (if using a bundler) -// if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { -// module.exports = ImageBrightnessAnalyzer; -// } \ No newline at end of file diff --git a/packages/core/index.ts b/packages/core/index.ts deleted file mode 100644 index 5193379f..00000000 --- a/packages/core/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./image-brightness-analyzer.ts" \ No newline at end of file diff --git a/packages/core/instant.perms.ts b/packages/core/instant.perms.ts new file mode 100644 index 00000000..c43f0fa7 --- /dev/null +++ b/packages/core/instant.perms.ts @@ -0,0 +1,35 @@ +// Docs: https://www.instantdb.com/docs/permissions + +import type { InstantRules } from "@instantdb/core"; + +const rules = { + /** + * Welcome to Instant's permission system! + * Right now your rules are empty. To start filling them in, check out the docs: + * https://www.instantdb.com/docs/permissions + * + * Here's an example to give you a feel: + * posts: { + * allow: { + * view: "true", + * create: "isOwner", + * update: "isOwner", + * delete: "isOwner", + * }, + * bind: ["isOwner", "auth.id != null && auth.id == data.ownerId"], + * }, + */ + "$default": { + "allow": { + "$default": "false" + } + }, + machines: { + allow: { + "$default": "isOwner", + }, + bind: ["isOwner", "auth.id != null && auth.id == data.ownerID"], + } +} satisfies InstantRules; + +export default rules; diff --git a/packages/core/instant.schema.ts b/packages/core/instant.schema.ts new file mode 100644 index 00000000..87c6ad76 --- /dev/null +++ b/packages/core/instant.schema.ts @@ -0,0 +1,80 @@ +import { i } from "@instantdb/core"; + +const _schema = i.schema({ + // This section lets you define entities: think `posts`, `comments`, etc + // Take a look at the docs to learn more: + // https://www.instantdb.com/docs/modeling-data#2-attributes + entities: { + $users: i.entity({ + email: i.string().unique().indexed(), + }), + // This is here because the $users entity has no more than 1 property; email + // profiles: i.entity({ + // name: i.string(), + // location: i.string(), + // createdAt: i.date(), + // deletedAt: i.date().optional() + // }), + machines: i.entity({ + hostname: i.string(), + location: i.string(), + fingerprint: i.string().indexed(), + createdAt: i.date(), + deletedAt: i.date().optional().indexed() + }), + // teams: i.entity({ + // name: i.string(), + // type: i.string(), // "Personal" or "Family" + // createdAt: i.date(), + // deletedAt: i.date().optional() + // }), + // subscriptions: i.entity({ + // quantity: i.number(), + // polarOrderID: i.string(), + // frequency: i.string(), + // next: i.date().optional(), + // }), + // productVariants: i.entity({ + // name: i.string(), + // price: i.number() + // }) + }, + // links: { + // userProfiles: { + // forward: { on: 'profiles', has: 'one', label: 'owner' }, + // reverse: { on: '$users', has: 'one', label: 'profile' }, + // }, + // machineOwners: { + // forward: { on: 'machines', has: 'one', label: 'owner' }, + // reverse: { on: '$users', has: 'many', label: 'machinesOwned' }, + // }, + // machineTeams: { + // forward: { on: 'machines', has: 'one', label: 'team' }, + // reverse: { on: 'teams', has: 'many', label: 'machines' }, + // }, + // userTeams: { + // forward: { on: 'teams', has: 'one', label: 'owner' }, + // reverse: { on: '$users', has: 'many', label: 'teamsOwned' }, + // }, + // teamMembers: { + // forward: { on: 'teams', has: 'many', label: 'members' }, + // reverse: { on: '$users', has: 'many', label: 'teams' }, + // }, + // subscribedProduct: { + // forward: { on: "subscriptions", has: "one", label: "productVariant" }, + // reverse: { on: "productVariants", has: "many", label: "subscriptions" } + // }, + // subscribedUser: { + // forward: { on: "subscriptions", has: "one", label: "owner" }, + // reverse: { on: "$users", has: "many", label: "subscriptions" } + // } + // } +}); + +// This helps Typescript display nicer intellisense +type _AppSchema = typeof _schema; +interface AppSchema extends _AppSchema { } +const schema: AppSchema = _schema; + +export type { AppSchema }; +export default schema; diff --git a/packages/core/package.json b/packages/core/package.json index 20d2d8c2..c58fe305 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,13 +1,20 @@ { "name": "@nestri/core", "version": "0.0.0", - "private": true, "sideEffects": false, - "exports":{ - ".":"./index.ts" + "type": "module", + "exports": { + "./*": "./src/*.ts" }, "devDependencies": { - "@cloudflare/workers-types": "^4.20240529.0", - "wrangler": "^3.57.2" + "@tsconfig/node20": "^20.1.4", + "loops": "^3.4.1", + "ulid": "^2.3.0", + "uuid": "^11.0.3", + "zod": "^3.24.1", + "zod-openapi": "^4.2.2" + }, + "dependencies": { + "@instantdb/admin": "^0.17.3" } - } \ No newline at end of file +} \ No newline at end of file diff --git a/packages/core/src/actor.ts b/packages/core/src/actor.ts new file mode 100644 index 00000000..ef98e3d5 --- /dev/null +++ b/packages/core/src/actor.ts @@ -0,0 +1,85 @@ +import { createContext } from "./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: { + fingerprint: string; + id: 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 { + fingerprint:actor.properties.fingerprint, + id: actor.properties.id + }; + 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/common.ts b/packages/core/src/common.ts new file mode 100644 index 00000000..a7ec75d8 --- /dev/null +++ b/packages/core/src/common.ts @@ -0,0 +1,7 @@ +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/context.ts b/packages/core/src/context.ts new file mode 100644 index 00000000..f160bab3 --- /dev/null +++ b/packages/core/src/context.ts @@ -0,0 +1,17 @@ +import { AsyncLocalStorage } from "node:async_hooks"; + +export function createContext() { + const storage = new AsyncLocalStorage(); + return { + use() { + const result = storage.getStore(); + if (!result) { + throw new Error("No context available"); + } + return result; + }, + with(value: T, fn: () => R) { + return storage.run(value, fn); + }, + }; +} \ No newline at end of file diff --git a/packages/core/src/database.ts b/packages/core/src/database.ts new file mode 100644 index 00000000..a1674a7d --- /dev/null +++ b/packages/core/src/database.ts @@ -0,0 +1,12 @@ +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/email/index.ts b/packages/core/src/email/index.ts new file mode 100644 index 00000000..b552aa33 --- /dev/null +++ b/packages/core/src/email/index.ts @@ -0,0 +1,25 @@ +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) + } + } +} \ No newline at end of file diff --git a/packages/core/src/error.ts b/packages/core/src/error.ts new file mode 100644 index 00000000..9aab0425 --- /dev/null +++ b/packages/core/src/error.ts @@ -0,0 +1,9 @@ +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/examples.ts b/packages/core/src/examples.ts new file mode 100644 index 00000000..5a12a5aa --- /dev/null +++ b/packages/core/src/examples.ts @@ -0,0 +1,45 @@ +export module Examples { + + export const User = { + id: "0bfcc712-df13-4454-81a8-fbee66eddca4", + email: "john@example.com", + }; + + export const Machine = { + id: "0bfcb712-df13-4454-81a8-fbee66eddca4", + hostname: "desktopeuo8vsf", + fingerprint: "fc27f428f9ca47d4b41b70889ae0c62090", + location: "KE, AF" + } + + // export const Team = { + // id: createID(), + // name: "Jane's Family", + // type: "Family" + // } + + // export const ProductVariant = { + // id: createID(), + // name: "FamilySM", + // price: 10, + // }; + + // export const Product = { + // id: createID(), + // name: "Family", + // description: "The ideal subscription tier for dedicated gamers who crave more flexibility and social gaming experiences.", + // variants: [ProductVariant], + // subscription: "allowed" as const, + // }; + + // export const Subscription = { + // id: createID(), + // productVariant: ProductVariant, + // quantity: 1, + // polarOrderID: createID(), + // frequency: "monthly" as const, + // next: new Date("2024-02-01 19:36:19.000").getTime(), + // owner: User + // }; + +} \ No newline at end of file diff --git a/packages/core/src/machine/index.ts b/packages/core/src/machine/index.ts new file mode 100644 index 00000000..9fe4a8ca --- /dev/null +++ b/packages/core/src/machine/index.ts @@ -0,0 +1,140 @@ +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 { id as createID } from "@instantdb/admin"; + +export module Machine { + export const Info = z + .object({ + id: z.string().openapi({ + description: Common.IdDescription, + example: Examples.Machine.id, + }), + hostname: z.string().openapi({ + description: "Hostname of the machine", + example: Examples.Machine.hostname, + }), + fingerprint: z.string().openapi({ + description: "The machine's fingerprint, derived from the machine's Linux machine ID.", + example: Examples.Machine.fingerprint, + }), + location: z.string().openapi({ + description: "The machine's approximate location; country and continent.", + example: Examples.Machine.location, + }) + }) + .openapi({ + ref: "Machine", + description: "A machine running on the Nestri network.", + example: Examples.Machine, + }); + + export const create = fn(z.object({ + fingerprint: z.string(), + hostname: z.string(), + location: z.string() + }), async (input) => { + const id = createID() + const now = new Date().getTime() + const db = databaseClient() + await db.transact( + db.tx.machines[id]!.update({ + fingerprint: input.fingerprint, + hostname: input.hostname, + location: input.location, + createdAt: now, + }) + ) + + return id + }) + + export const remove = fn(z.string(), async (id) => { + const now = new Date().getTime() + // const device = useCurrentDevice() + // const db = databaseClient() + + // if (device.id) { // the machine can delete itself + // await db.transact(db.tx.machines[device.id]!.update({ deletedAt: now })) + // } else {// the user can delete it manually + const user = useCurrentUser() + const db = databaseClient().asUser({ token: user.token }) + await db.transact(db.tx.machines[id]!.update({ deletedAt: now })) + // } + + return "ok" + }) + + export const fromID = fn(z.string(), async (id) => { + const user = useCurrentUser() + const db = databaseClient().asUser({ token: user.token }) + + const query = { + machines: { + $: { + where: { + id: id, + deletedAt: { $isNull: true } + } + } + } + } + + const res = await db.query(query) + + return res.machines[0] + }) + + 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) + + return res.machines[0] + }) + + export const list = async () => { + const user = useCurrentUser() + const db = databaseClient().asUser({ token: user.token }) + + const query = { + $users: { + $: { where: { id: user.id } }, + machines: { + $: { + deletedAt: { $isNull: true } + } + } + }, + } + + const res = await db.query(query) + + return res.$users[0]?.machines + } + + export const link = fn(z.object({ + machineId: z.string() + }), async (input) => { + const user = useCurrentUser() + const db = databaseClient() + + await db.transact(db.tx.machines[input.machineId]!.link({ owner: user.id })) + + return "ok" + }) +} \ No newline at end of file diff --git a/packages/core/src/team/index.ts b/packages/core/src/team/index.ts new file mode 100644 index 00000000..c2f56603 --- /dev/null +++ b/packages/core/src/team/index.ts @@ -0,0 +1,48 @@ +import databaseClient from "../database" +import { z } from "zod" +import { Common } from "../common"; +import { createID, fn } from "../utils"; +import { Examples } from "../examples"; + +export module Team { + export const Info = z + .object({ + id: z.string().openapi({ + description: Common.IdDescription, + example: Examples.Team.id, + }), + name: z.string().openapi({ + description: "Name of the machine", + example: Examples.Team.name, + }), + type: z.string().nullable().openapi({ + description: "Whether this is a personal or family type of team", + example: Examples.Team.type, + }) + }) + .openapi({ + ref: "Team", + description: "A group of Nestri user's who share the same machine", + example: Examples.Team, + }); + + export const create = fn(z.object({ + name: z.string(), + type: z.enum(["personal", "family"]), + owner: z.string(), + }), async (input) => { + const id = createID("machine") + const now = new Date().getTime() + const db = databaseClient() + + await db.transact(db.tx.teams[id]!.update({ + name: input.name, + type: input.type, + createdAt: now + }).link({ + owner: input.owner, + })) + + return id + }) +} \ No newline at end of file diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts new file mode 100644 index 00000000..2e8fc014 --- /dev/null +++ b/packages/core/src/types.ts @@ -0,0 +1,17 @@ +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/user/index.ts b/packages/core/src/user/index.ts new file mode 100644 index 00000000..12c4babd --- /dev/null +++ b/packages/core/src/user/index.ts @@ -0,0 +1,37 @@ +import { z } from "zod"; +import databaseClient from "../database" +import { fn } from "../utils"; +import { Common } from "../common"; +import { Examples } from "../examples"; + +export module User { + 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/utils/fn.ts b/packages/core/src/utils/fn.ts new file mode 100644 index 00000000..b2c64bd0 --- /dev/null +++ b/packages/core/src/utils/fn.ts @@ -0,0 +1,13 @@ +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; +} \ No newline at end of file diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts new file mode 100644 index 00000000..e9dcbbde --- /dev/null +++ b/packages/core/src/utils/index.ts @@ -0,0 +1 @@ +export * from "./fn" \ No newline at end of file diff --git a/packages/core/sst-env.d.ts b/packages/core/sst-env.d.ts new file mode 100644 index 00000000..f90ea1f4 --- /dev/null +++ b/packages/core/sst-env.d.ts @@ -0,0 +1,42 @@ +/* This file is auto-generated by SST. Do not edit. */ +/* tslint:disable */ +/* eslint-disable */ +/* deno-fmt-ignore-file */ +import "sst" +export {} +declare module "sst" { + export interface Resource { + "Api": { + "type": "sst.cloudflare.Worker" + "url": string + } + "Auth": { + "type": "sst.cloudflare.Worker" + "url": string + } + "AuthFingerprintKey": { + "type": "random.index/randomString.RandomString" + "value": string + } + "CloudflareAuthKV": { + "type": "sst.cloudflare.Kv" + } + "InstantAdminToken": { + "type": "sst.sst.Secret" + "value": string + } + "InstantAppId": { + "type": "sst.sst.Secret" + "value": string + } + "LoopsApiKey": { + "type": "sst.sst.Secret" + "value": string + } + "Urls": { + "api": string + "auth": string + "type": "sst.sst.Linkable" + } + } +} diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json new file mode 100644 index 00000000..bca727cd --- /dev/null +++ b/packages/core/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@tsconfig/node20/tsconfig.json", + "compilerOptions": { + "module": "esnext", + "jsx": "react-jsx", + "moduleResolution": "bundler", + "noUncheckedIndexedAccess": true, + } + } \ No newline at end of file diff --git a/packages/docker-to-rootfs/Dockerfile b/packages/docker-to-rootfs/Dockerfile deleted file mode 100644 index 75f8e3e7..00000000 --- a/packages/docker-to-rootfs/Dockerfile +++ /dev/null @@ -1,6 +0,0 @@ -# https://github.com/iximiuz/docker-to-linux/blob/master/Dockerfile - -FROM amd64/debian:bullseye -LABEL com.iximiuz-project="docker-to-linux" -RUN apt-get -y update -RUN apt-get -y install extlinux fdisk \ No newline at end of file diff --git a/packages/docker-to-rootfs/README.md b/packages/docker-to-rootfs/README.md deleted file mode 100644 index 6e79237f..00000000 --- a/packages/docker-to-rootfs/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# Docker to RootFS - -We are building the rootfs as a docker image, mainly for consistency and ease of use. -So, to convert the Docker image to a rootfs good enough to run inside CrosVM we use this script - -Run it like so: -```bash -./make your-docker-image -``` - -TODO: -1. Make sure the docker image name passed in exists -2. Reduce the dependencies of this script to 1 (If possible) -3. Extract not only the rootfs, but also kernel and initrd -4. Add a way to pass in the size of the rootfs the user wants diff --git a/packages/docker-to-rootfs/create_image.sh b/packages/docker-to-rootfs/create_image.sh deleted file mode 100644 index ee8e5100..00000000 --- a/packages/docker-to-rootfs/create_image.sh +++ /dev/null @@ -1,54 +0,0 @@ -#!/bin/bash - -set -e - -UID_HOST=$1 -GID_HOST=$2 -VM_DISK_SIZE_MB=$3 - -echo_blue() { - local font_blue="\033[94m" - local font_bold="\033[1m" - local font_end="\033[0m" - - echo -e "\n${font_blue}${font_bold}${1}${font_end}" -} - -echo_blue "[Create disk image]" -[ -z "${VM_DISK_SIZE_MB}" ] && VM_DISK_SIZE_MB=1024 -VM_DISK_SIZE_SECTOR=$(expr $VM_DISK_SIZE_MB \* 1024 \* 1024 / 512) -dd if=/dev/zero of=/os/${DISTR}.img bs=${VM_DISK_SIZE_SECTOR} count=512 - -echo_blue "[Make partition]" -echo "type=83,bootable" | sfdisk /os/${DISTR}.img - -echo_blue "\n[Format partition with ext4]" -losetup -D -LOOPDEVICE=$(losetup -f) -echo -e "\n[Using ${LOOPDEVICE} loop device]" -losetup -o $(expr 512 \* 2048) ${LOOPDEVICE} /os/${DISTR}.img -mkfs.ext4 ${LOOPDEVICE} - -echo_blue "[Copy ${DISTR} directory structure to partition]" -mkdir -p /os/mnt -mount -t auto ${LOOPDEVICE} /os/mnt/ -cp -a /os/${DISTR}.dir/. /os/mnt/ - -echo_blue "[Setup extlinux]" -extlinux --install /os/mnt/boot/ -cp /os/syslinux.cfg /os/mnt/boot/syslinux.cfg -rm /os/mnt/.dockerenv - -echo_blue "[Unmount]" -umount /os/mnt -losetup -D - -echo_blue "[Write syslinux MBR]" -dd if=/usr/lib/syslinux/mbr/mbr.bin of=/os/${DISTR}.img bs=440 count=1 conv=notrunc - -#echo_blue "[Convert to qcow2]" -#qemu-img convert -c /os/${DISTR}.img -O qcow2 /os/${DISTR}.qcow2 - -[ "${UID_HOST}" -a "${GID_HOST}" ] && chown ${UID_HOST}:${GID_HOST} /os/${DISTR}.img - -rm -r /os/${DISTR}.dir /os/${DISTR}.tar \ No newline at end of file diff --git a/packages/docker-to-rootfs/make.sh b/packages/docker-to-rootfs/make.sh deleted file mode 100644 index d9c09fa2..00000000 --- a/packages/docker-to-rootfs/make.sh +++ /dev/null @@ -1,112 +0,0 @@ -#!/bin/bash - -# Define colors -COL_RED="\033[0;31m" -COL_GRN="\033[0;32m" -COL_END="\033[0m" - -# Get current user and group IDs -USER_ID=$(id -u) -GROUP_ID=$(id -g) - -# Set default disk size -VM_DISK_SIZE_MB=${VM_DISK_SIZE_MB:-4096} - -# Set repository name -REPO="nestri" - -# Function to create a tar archive from a Docker image -create_tar() { - local name=$1 - echo -e "\n${COL_GRN}[Dump $name directory structure to tar archive]${COL_END}" - docker export -o $name.tar $(docker run -d $name /bin/true) -} - -# Function to extract a tar archive -extract_tar() { - local name=$1 - echo -e "\n${COL_GRN}[Extract $name tar archive]${COL_END}" - docker run -it \ - -v $(pwd):/os:rw \ - $REPO/builder bash -c "mkdir -p /os/$name.dir && tar -C /os/$name.dir --numeric-owner -xf /os/$name.tar" -} - -# Function to create a disk image -create_image() { - local name=$1 - echo -e "\n${COL_GRN}[Create $name disk image]${COL_END}" - docker run -it \ - -v $(pwd):/os:rw \ - -e DISTR=$name \ - --privileged \ - --cap-add SYS_ADMIN \ - $REPO/builder bash /os/create_image.sh $USER_ID $GROUP_ID $VM_DISK_SIZE_MB -} - -# Function to ensure builder is ready -ensure_builder() { - echo -e "\n${COL_GRN}[Ensure builder is ready]${COL_END}" - if [ "$(docker images -q $REPO/builder)" = '' ]; then - docker build -f Dockerfile -t $REPO/builder . - fi -} - -# Function to run builder interactively -run_builder_interactive() { - docker run -it \ - -v $(pwd):/os:rw \ - --cap-add SYS_ADMIN \ - $REPO/builder bash -} - -# Function to clean up -clean_up() { - echo -e "\n${COL_GRN}[Remove leftovers]${COL_END}" - rm -rf mnt debian.* alpine.* ubuntu.* - clean_docker_procs - clean_docker_images -} - -# Function to clean Docker processes -clean_docker_procs() { - echo -e "\n${COL_GRN}[Remove Docker Processes]${COL_END}" - if [ "$(docker ps -qa -f=label=com.iximiuz-project=$REPO)" != '' ]; then - docker rm $(docker ps -qa -f=label=com.iximiuz-project=$REPO) - else - echo "" - fi -} - -# Function to clean Docker images -clean_docker_images() { - echo -e "\n${COL_GRN}[Remove Docker Images]${COL_END}" - if [ "$(docker images -q $REPO/*)" != '' ]; then - docker rmi $(docker images -q $REPO/*) - else - echo "" - fi -} - -# Main script -if [ $# -lt 1 ]; then - echo "Usage: $0 [clean|builder-interactive]" - exit 1 -fi - -IMAGE_NAME=$1 - -case $2 in - builder-interactive) - run_builder_interactive - ;; - clean) - clean_up - ;; -esac - -ensure_builder -create_tar $IMAGE_NAME -extract_tar $IMAGE_NAME -create_image $IMAGE_NAME - -# Extract kernel `` virt-builder --get-kernel "${IMAGE_NAME}.img" -o . `` \ No newline at end of file diff --git a/packages/docker-to-rootfs/syslinux.cfg b/packages/docker-to-rootfs/syslinux.cfg deleted file mode 100644 index 0e7d6fcd..00000000 --- a/packages/docker-to-rootfs/syslinux.cfg +++ /dev/null @@ -1,5 +0,0 @@ -DEFAULT linux - SAY Now booting the kernel from SYSLINUX... - LABEL linux - KERNEL /vmlinuz - APPEND rw root=/dev/sda1 initrd=/initrd.img \ No newline at end of file diff --git a/packages/eslint-config/README.md b/packages/eslint-config/README.md deleted file mode 100644 index 8b42d901..00000000 --- a/packages/eslint-config/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# `@turbo/eslint-config` - -Collection of internal eslint configurations. diff --git a/packages/eslint-config/library.js b/packages/eslint-config/library.js deleted file mode 100644 index 9b59cc0f..00000000 --- a/packages/eslint-config/library.js +++ /dev/null @@ -1,34 +0,0 @@ -const { resolve } = require("node:path"); - -const project = resolve(process.cwd(), "tsconfig.json"); - -/** @type {import("eslint").Linter.Config} */ -module.exports = { - extends: ["eslint:recommended", "prettier", "turbo"], - plugins: ["only-warn"], - globals: { - React: true, - JSX: true, - }, - env: { - node: true, - }, - settings: { - "import/resolver": { - typescript: { - project, - }, - }, - }, - ignorePatterns: [ - // Ignore dotfiles - ".*.js", - "node_modules/", - "dist/", - ], - overrides: [ - { - files: ["*.js?(x)", "*.ts?(x)"], - }, - ], -}; diff --git a/packages/eslint-config/next.js b/packages/eslint-config/next.js deleted file mode 100644 index 88445be0..00000000 --- a/packages/eslint-config/next.js +++ /dev/null @@ -1,35 +0,0 @@ -const { resolve } = require("node:path"); - -const project = resolve(process.cwd(), "tsconfig.json"); - -/** @type {import("eslint").Linter.Config} */ -module.exports = { - extends: [ - "eslint:recommended", - "prettier", - require.resolve("@vercel/style-guide/eslint/next"), - "turbo", - ], - globals: { - React: true, - JSX: true, - }, - env: { - node: true, - browser: true, - }, - plugins: ["only-warn"], - settings: { - "import/resolver": { - typescript: { - project, - }, - }, - }, - ignorePatterns: [ - // Ignore dotfiles - ".*.js", - "node_modules/", - ], - overrides: [{ files: ["*.js?(x)", "*.ts?(x)"] }], -}; diff --git a/packages/eslint-config/package.json b/packages/eslint-config/package.json deleted file mode 100644 index 6de9f8a1..00000000 --- a/packages/eslint-config/package.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "@nestri/eslint-config", - "version": "0.0.0", - "private": true, - "files": [ - "library.js", - "next.js", - "qwik.js", - "react-internal.js" - ], - "devDependencies": { - "@vercel/style-guide": "^5.2.0", - "eslint-config-turbo": "^2.0.0", - "eslint-config-prettier": "^9.1.0", - "eslint-plugin-only-warn": "^1.1.0", - "@types/eslint": "8.56.10", - "@types/node": "20.14.11", - "@typescript-eslint/eslint-plugin": "7.16.1", - "@typescript-eslint/parser": "7.16.1", - "eslint": "8.57.0", - "eslint-plugin-qwik": "^1.8.0", - "prettier": "3.3.3", - "typescript": "5.4.5" - } -} \ No newline at end of file diff --git a/packages/eslint-config/qwik.js b/packages/eslint-config/qwik.js deleted file mode 100644 index 374e759a..00000000 --- a/packages/eslint-config/qwik.js +++ /dev/null @@ -1,50 +0,0 @@ -module.exports = { - env: { - browser: true, - es2021: true, - node: true, - }, - extends: [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended", - "plugin:qwik/recommended", - ], - parser: "@typescript-eslint/parser", - parserOptions: { - tsconfigRootDir: __dirname, - project: ["./tsconfig.json"], - ecmaVersion: 2021, - sourceType: "module", - ecmaFeatures: { - jsx: true, - }, - }, - plugins: ["@typescript-eslint"], - rules: { - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/explicit-module-boundary-types": "off", - "@typescript-eslint/no-inferrable-types": "off", - "@typescript-eslint/no-non-null-assertion": "off", - "@typescript-eslint/no-empty-interface": "off", - "@typescript-eslint/no-namespace": "off", - "@typescript-eslint/no-empty-function": "off", - "@typescript-eslint/no-this-alias": "off", - "@typescript-eslint/ban-types": "off", - "@typescript-eslint/ban-ts-comment": "off", - // Warn when an unused variable doesn't start with an underscore - "@typescript-eslint/no-unused-vars": [ - "warn", - { - argsIgnorePattern: "^_", - varsIgnorePattern: "^_", - caughtErrorsIgnorePattern: "^_", - }, - ], - "prefer-spread": "off", - "no-case-declarations": "off", - "no-console": "off", - "qwik/no-use-visible-task": "off", - "@typescript-eslint/consistent-type-imports": "warn", - "@typescript-eslint/no-unnecessary-condition": "warn", - }, -}; diff --git a/packages/eslint-config/react-internal.js b/packages/eslint-config/react-internal.js deleted file mode 100644 index bf0a2083..00000000 --- a/packages/eslint-config/react-internal.js +++ /dev/null @@ -1,39 +0,0 @@ -const { resolve } = require("node:path"); - -const project = resolve(process.cwd(), "tsconfig.json"); - -/* - * This is a custom ESLint configuration for use with - * internal (bundled by their consumer) libraries - * that utilize React. - */ - -/** @type {import("eslint").Linter.Config} */ -module.exports = { - extends: ["eslint:recommended", "prettier", "turbo"], - plugins: ["only-warn"], - globals: { - React: true, - JSX: true, - }, - env: { - browser: true, - }, - settings: { - "import/resolver": { - typescript: { - project, - }, - }, - }, - ignorePatterns: [ - // Ignore dotfiles - ".*.js", - "node_modules/", - "dist/", - ], - overrides: [ - // Force ESLint to detect .tsx files - { files: ["*.js?(x)", "*.ts?(x)"] }, - ], -}; diff --git a/packages/functions/.gitignore b/packages/functions/.gitignore new file mode 100644 index 00000000..ce007acb --- /dev/null +++ b/packages/functions/.gitignore @@ -0,0 +1,175 @@ +# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore + +# Logs + +logs +_.log +npm-debug.log_ +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Caches + +.cache + +# Diagnostic reports (https://nodejs.org/api/report.html) + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# Runtime data + +pids +_.pid +_.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover + +lib-cov + +# Coverage directory used by tools like istanbul + +coverage +*.lcov + +# nyc test coverage + +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +.grunt + +# Bower dependency directory (https://bower.io/) + +bower_components + +# node-waf configuration + +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +build/Release + +# Dependency directories + +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) + +web_modules/ + +# TypeScript cache + +*.tsbuildinfo + +# Optional npm cache directory + +.npm + +# Optional eslint cache + +.eslintcache + +# Optional stylelint cache + +.stylelintcache + +# Microbundle cache + +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history + +.node_repl_history + +# Output of 'npm pack' + +*.tgz + +# Yarn Integrity file + +.yarn-integrity + +# dotenv environment variable files + +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) + +.parcel-cache + +# Next.js build output + +.next +out + +# Nuxt.js build / generate output + +.nuxt +dist + +# Gatsby files + +# Comment in the public line in if your project uses Gatsby and not Next.js + +# https://nextjs.org/blog/next-9-1#public-directory-support + +# public + +# vuepress build output + +.vuepress/dist + +# vuepress v2.x temp and cache directory + +.temp + +# Docusaurus cache and generated files + +.docusaurus + +# Serverless directories + +.serverless/ + +# FuseBox cache + +.fusebox/ + +# DynamoDB Local files + +.dynamodb/ + +# TernJS port file + +.tern-port + +# Stores VSCode versions used for testing VSCode extensions + +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store \ No newline at end of file diff --git a/packages/functions/.partykit/state/party/-PartyKitDurable/68b01ab8eae54eaeb4dddaeba12765b8f1d619378926c2dc36df98eae727ff69.sqlite b/packages/functions/.partykit/state/party/-PartyKitDurable/68b01ab8eae54eaeb4dddaeba12765b8f1d619378926c2dc36df98eae727ff69.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..68b6a7db221de8a327fd8c545318b84b413fff55 GIT binary patch literal 8192 zcmeIuy9&ZE6b9haAP9o!Cf&}cATCaBwi>XsQmygQsZ?n}tQ4dQjy{2};G^2x?k@jN za!zuR>D#QGa~5%&-mYQBW9pI+G-W0tQSZE!(em1;&qXWlzanUK`?T|4TIWsqLLdME z2tWV=5P$##AOHafKmY;|AWF4nPuI2ABnyv86rAF~-c?o1JZUJ&$}kV ztmHxQ%CnY+=d&&SVg2?9lY7Lb&Ebs-X7A4mJ literal 0 HcmV?d00001 diff --git a/packages/functions/.partykit/state/party/-PartyKitDurable/68b01ab8eae54eaeb4dddaeba12765b8f1d619378926c2dc36df98eae727ff69.sqlite-shm b/packages/functions/.partykit/state/party/-PartyKitDurable/68b01ab8eae54eaeb4dddaeba12765b8f1d619378926c2dc36df98eae727ff69.sqlite-shm new file mode 100644 index 0000000000000000000000000000000000000000..fe9ac2845eca6fe6da8a63cd096d9cf9e24ece10 GIT binary patch literal 32768 zcmeIuAr62r3XsQmygQsZ?n}tQ4dQjy{2};G^2x?k@jN za!zuR>D#QGa~5%&-mYQBW9pI+G-W0tQSZE!(em1;&qXWlzanUK`?T|4TIWsqLLdME z2tWV=5P$##AOHafKmY;|AWF4nPuI2ABnyv86rAF~-c?o1JZUJ&$}kV ztmHxQ%CnY+=d&&SVg2?9lY7Lb&Ebs-X7A4mJ literal 0 HcmV?d00001 diff --git a/packages/functions/.partykit/state/party/-PartyKitDurable/edf5bdf97b1b08ea4cbf3f72c6dac736c54a14b079b0bae3773d08ab17264bae.sqlite b/packages/functions/.partykit/state/party/-PartyKitDurable/edf5bdf97b1b08ea4cbf3f72c6dac736c54a14b079b0bae3773d08ab17264bae.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..4ebf78cf96088c7148cb47caf804cc036de75a34 GIT binary patch literal 4096 zcmWFz^vNtqRY=P(%1ta$FlG>7U}9o$P*7lCU|@t|AVoG{WY8;GzzfnYK(-m98b?E5 nGz3ONU^E0qLtr!nMnhmU1V%$(Gz3ONU^E0qLtr!nC=3ArfDQ*E literal 0 HcmV?d00001 diff --git a/packages/functions/.partykit/state/party/-PartyKitDurable/edf5bdf97b1b08ea4cbf3f72c6dac736c54a14b079b0bae3773d08ab17264bae.sqlite-shm b/packages/functions/.partykit/state/party/-PartyKitDurable/edf5bdf97b1b08ea4cbf3f72c6dac736c54a14b079b0bae3773d08ab17264bae.sqlite-shm new file mode 100644 index 0000000000000000000000000000000000000000..e65f8bb756a0c540d7f30e22d3929d47441caa70 GIT binary patch literal 32768 zcmeI)u?fOJ6b4{*0=r}h_posTHwZXI2zDtgoWa5^#3r2sxPn;3Cdoqyw&?=*{qVTs zIPP)Z^_M?4i^%d@>(wwuy&uQoRGqKw_P)DxuTN9f`|MEM@`?M0qsDZ985e((=pTKB z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkJxC4u;LLI|WO;J=kL-`%YV5FkK+009C72oNAZfB*pk y1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5;&c??_i35r45Kmeql1 + } + +export type ApiAdapterError = + | { + type: "invalid_code" + } + | { + type: "invalid_claim" + key: string + value: string + } + +export function ApiAdapter< + Claims extends Record = Record, +>(config: { + length?: number + request: ( + req: Request, + state: ApiAdapterState, + body?: Claims, + error?: ApiAdapterError, + ) => Promise + sendCode: (claims: Claims, code: string) => Promise +}) { + const length = config.length || 6 + function generate() { + return generateUnbiasedDigits(length) + } + + return { + type: "api", // this is a miscellaneous name, for lack of a better one + init(routes, ctx) { + async function transition( + c: Context, + next: ApiAdapterState, + claims?: Claims, + err?: ApiAdapterError, + ) { + await ctx.set(c, "adapter", 60 * 60 * 24, next) + const resp = ctx.forward( + c, + await config.request(c.req.raw, next, claims, err), + ) + return resp + } + routes.get("/authorize", async (c) => { + const resp = await transition(c, { + type: "start", + }) + return resp + }) + + routes.post("/authorize", async (c) => { + const code = generate() + const body = await c.req.json() + const state = await ctx.get(c, "adapter") + const action = body.action + + if (action === "request" || action === "resend") { + const claims = body.claims as Claims + delete body.action + const err = await config.sendCode(claims, code) + if (err) return transition(c, { type: "start" }, claims, err) + return transition( + c, + { + type: "code", + resend: action === "resend", + claims, + code, + }, + claims, + ) + } + + if ( + body.action === "verify" && + state.type === "code" + ) { + const body = await c.req.json() + const compare = body.code + if ( + !state.code || + !compare || + !timingSafeCompare(state.code, compare) + ) { + return transition( + c, + { + ...state, + resend: false, + }, + body.claims, + { type: "invalid_code" }, + ) + } + await ctx.unset(c, "adapter") + return ctx.forward( + c, + await ctx.success(c, { claims: state.claims as Claims }), + ) + } + }) + }, + } satisfies Adapter<{ claims: Claims }> +} + +export type ApiAdapterOptions = Parameters[0] \ No newline at end of file diff --git a/packages/functions/src/api/index.ts b/packages/functions/src/api/index.ts new file mode 100644 index 00000000..9a49ed6e --- /dev/null +++ b/packages/functions/src/api/index.ts @@ -0,0 +1,153 @@ +import "zod-openapi/extend"; +import { Resource } from "sst"; +import { ZodError } from "zod"; +import { logger } from "hono/logger"; +import { subjects } from "../subjects"; +import { VisibleError } from "../error"; +import { MachineApi } from "./machine"; +import { openAPISpecs } from "hono-openapi"; +import { ActorContext } from '@nestri/core/actor'; +import { Hono, type MiddlewareHandler } from "hono"; +import { HTTPException } from "hono/http-exception"; +import { createClient } from "@openauthjs/openauth/client"; + +const auth: MiddlewareHandler = async (c, next) => { + const client = createClient({ + clientID: "api", + issuer: Resource.Urls.auth + }); + + const authHeader = + c.req.query("authorization") ?? c.req.header("authorization"); + if (authHeader) { + const match = authHeader.match(/^Bearer (.+)$/); + if (!match || !match[1]) { + throw new VisibleError( + "input", + "auth.token", + "Bearer token not found or improperly formatted", + ); + } + const bearerToken = match[1]; + + const result = await client.verify(subjects, bearerToken!); + if (result.err) + throw new VisibleError("input", "auth.invalid", "Invalid bearer token"); + if (result.subject.type === "user") { + return ActorContext.with( + { + type: "user", + properties: { + userID: result.subject.properties.userID, + accessToken: result.subject.properties.accessToken, + auth: { + type: "oauth", + clientID: result.aud, + }, + }, + }, + next, + ); + } else if (result.subject.type === "device") { + return ActorContext.with( + { + type: "device", + properties: { + fingerprint: result.subject.properties.fingerprint, + id: result.subject.properties.id, + auth: { + type: "oauth", + clientID: result.aud, + }, + }, + }, + next, + ); + } + } + + return ActorContext.with({ type: "public", properties: {} }, next); +}; + + +const app = new Hono(); +app + .use(logger(), async (c, next) => { + c.header("Cache-Control", "no-store"); + return next(); + }) + .use(auth); + +const routes = app + .get("/", (c) => c.text("Hello there 👋🏾")) + .route("/machine", MachineApi.route) + .onError((error, c) => { + console.error(error); + if (error instanceof VisibleError) { + return c.json( + { + code: error.code, + message: error.message, + }, + error.kind === "auth" ? 401 : 400, + ); + } + if (error instanceof ZodError) { + const e = error.errors[0]; + if (e) { + return c.json( + { + code: e?.code, + message: e?.message, + }, + 400, + ); + } + } + if (error instanceof HTTPException) { + return c.json( + { + code: "request", + message: "Invalid request", + }, + 400, + ); + } + return c.json( + { + code: "internal", + message: "Internal server error", + }, + 500, + ); + }); + +app.get( + "/doc", + openAPISpecs(routes, { + documentation: { + info: { + title: "Nestri API", + description: + "The Nestri API gives you the power to run your own customized cloud gaming platform.", + version: "0.0.3", + }, + components: { + securitySchemes: { + Bearer: { + type: "http", + scheme: "bearer", + bearerFormat: "JWT", + }, + }, + }, + security: [{ Bearer: [] }], + servers: [ + { description: "Production", url: "https://api.nestri.io" }, + ], + }, + }), +); + +export type Routes = typeof routes; +export default app \ No newline at end of file diff --git a/packages/functions/src/api/machine.ts b/packages/functions/src/api/machine.ts new file mode 100644 index 00000000..05e45711 --- /dev/null +++ b/packages/functions/src/api/machine.ts @@ -0,0 +1,160 @@ +import { z } from "zod"; +import { Result } from "../common"; +import { Hono } from "hono"; +import { describeRoute } from "hono-openapi"; +import { validator, resolver } from "hono-openapi/zod"; +import { Examples } from "@nestri/core/examples"; +import { Machine } from "@nestri/core/machine/index"; +import { useCurrentUser } from "@nestri/core/actor"; + +export module MachineApi { + export const route = new Hono() + .get( + "/", + describeRoute({ + tags: ["Machine"], + summary: "List machines", + description: "List the current user's machines.", + responses: { + 200: { + content: { + "application/json": { + schema: Result( + Machine.Info.array().openapi({ + description: "List of machines.", + example: [Examples.Machine], + }), + ), + }, + }, + description: "List of machines.", + }, + 404: { + content: { + "application/json": { + schema: resolver(z.object({ error: z.string() })), + }, + }, + description: "This user has no machines.", + }, + }, + }), + async (c) => { + const machines = await Machine.list(); + if (!machines) return c.json({ error: "This user has no machines." }, 404); + return c.json({ data: machines }, 200); + }, + ) + .get( + "/:id", + describeRoute({ + tags: ["Machine"], + summary: "Get machine", + description: "Get the machine with the given ID.", + responses: { + 404: { + content: { + "application/json": { + schema: resolver(z.object({ error: z.string() })), + }, + }, + description: "Machine not found.", + }, + 200: { + content: { + "application/json": { + schema: Result( + Machine.Info.openapi({ + description: "Machine.", + example: Examples.Machine, + }), + ), + }, + }, + description: "Machine.", + }, + }, + }), + validator( + "param", + z.object({ + id: z.string().openapi({ + description: "ID of the machine to get.", + example: Examples.Machine.id, + }), + }), + ), + async (c) => { + const param = c.req.valid("param"); + const machine = await Machine.fromID(param.id); + if (!machine) return c.json({ error: "Machine not found." }, 404); + return c.json({ data: machine }, 200); + }, + ) + .post( + "/:id", + describeRoute({ + tags: ["Machine"], + summary: "Link a machine to a user", + description: "Link a machine to the owner.", + responses: { + 200: { + content: { + "application/json": { + schema: Result(z.literal("ok")) + }, + }, + description: "Machine was linked successfully.", + }, + }, + }), + validator( + "param", + z.object({ + id: Machine.Info.shape.fingerprint.openapi({ + description: "Fingerprint of the machine to link to.", + example: Examples.Machine.id, + }), + }), + ), + async (c) => { + const request = c.req.valid("param") + const machine = await Machine.fromFingerprint(request.id) + if (!machine) return c.json({ error: "Machine not found." }, 404); + await Machine.link({machineId:machine.id }) + return c.json({ data: "ok" as const }, 200); + }, + ) + .delete( + "/:id", + describeRoute({ + tags: ["Machine"], + summary: "Delete machine", + description: "Delete the machine with the given ID.", + responses: { + 200: { + content: { + "application/json": { + schema: Result(z.literal("ok")), + }, + }, + description: "Machine was deleted successfully.", + }, + }, + }), + validator( + "param", + z.object({ + id: Machine.Info.shape.id.openapi({ + description: "ID of the machine to delete.", + example: Examples.Machine.id, + }), + }), + ), + async (c) => { + const param = c.req.valid("param"); + await Machine.remove(param.id); + return c.json({ data: "ok" as const }, 200); + }, + ); +} \ No newline at end of file diff --git a/packages/functions/src/auth.ts b/packages/functions/src/auth.ts new file mode 100644 index 00000000..48e2d018 --- /dev/null +++ b/packages/functions/src/auth.ts @@ -0,0 +1,140 @@ +import { Resource } from "sst" +import { + type ExecutionContext, + type KVNamespace, +} from "@cloudflare/workers-types" +import { subjects } from "./subjects" +import { User } from "@nestri/core/user/index" +import { Email } from "@nestri/core/email/index" +import { authorizer } from "@openauthjs/openauth" +import { type CFRequest } from "@nestri/core/types" +import { Select } from "@openauthjs/openauth/ui/select"; +import { PasswordUI } from "@openauthjs/openauth/ui/password" +import type { Adapter } from "@openauthjs/openauth/adapter/adapter" +import { PasswordAdapter } from "@openauthjs/openauth/adapter/password" +import { CloudflareStorage } from "@openauthjs/openauth/storage/cloudflare" +import { Machine } from "@nestri/core/machine/index" + +interface Env { + CloudflareAuthKV: KVNamespace +} + +export type CodeAdapterState = + | { + type: "start" + } + | { + type: "code" + resend?: boolean + code: string + claims: Record + } + +export default { + async fetch(request: CFRequest, env: Env, ctx: ExecutionContext) { + const location = `${request.cf.country},${request.cf.continent}` + return authorizer({ + select: Select({ + providers: { + device: { + hide: true, + }, + }, + }), + theme: { + title: "Nestri | Auth", + primary: "#FF4F01", + //TODO: Change this in prod + logo: "https://nestri.pages.dev/logo.webp", + favicon: "https://nestri.pages.dev/seo/favicon.ico", + background: { + light: "#f5f5f5 ", + dark: "#171717" + }, + radius: "lg", + font: { + family: "Geist, sans-serif", + }, + css: ` + @import url('https://fonts.googleapis.com/css2?family=Geist:wght@100;200;300;400;500;600;700;800;900&display=swap'); + `, + }, + storage: CloudflareStorage({ + namespace: env.CloudflareAuthKV, + }), + subjects, + providers: { + password: PasswordAdapter( + PasswordUI({ + sendCode: async (email, code) => { + console.log("email & code:", email, code) + await Email.send(email, code) + }, + }), + ), + device: { + type: "device", + async client(input) { + if (input.clientSecret !== Resource.AuthFingerprintKey.value) { + throw new Error("Invalid authorization token"); + } + + const fingerprint = input.params.fingerprint; + if (!fingerprint) { + throw new Error("Fingerprint is required"); + } + + const hostname = input.params.hostname; + if (!hostname) { + throw new Error("Hostname is required"); + } + return { + fingerprint, + hostname + }; + }, + init() { } + } as Adapter<{ fingerprint: string; hostname: string }>, + }, + allow: async (input) => { + const url = new URL(input.redirectURI); + const hostname = url.hostname; + if (hostname.endsWith("nestri.io")) return true; + if (hostname === "localhost") return true; + return true; + }, + success: async (ctx, value) => { + if (value.provider === "device") { + let machineID = await Machine.fromFingerprint(value.fingerprint).then((x) => x?.id); + + if (!machineID) { + machineID = await Machine.create({ + fingerprint: value.fingerprint, + hostname: value.hostname, + location, + }); + } + + return await ctx.subject("device", { + id: machineID, + fingerprint: value.fingerprint + }) + } + + const email = value.email; + + if (email) { + const token = await User.create(email); + const user = await User.fromEmail(email); + + return await ctx.subject("user", { + accessToken: token, + userID: user.id + }); + } + + throw new Error("This is not implemented yet"); + }, + }).fetch(request, env, ctx) + } +} \ No newline at end of file diff --git a/packages/functions/src/common.ts b/packages/functions/src/common.ts new file mode 100644 index 00000000..bed7e1c6 --- /dev/null +++ b/packages/functions/src/common.ts @@ -0,0 +1,10 @@ +import { z } from "zod"; +import { resolver } from "hono-openapi/zod"; + +export function Result(schema: T) { + return resolver( + z.object({ + data: schema, + }), + ); +} \ No newline at end of file diff --git a/packages/functions/src/error.ts b/packages/functions/src/error.ts new file mode 100644 index 00000000..9aab0425 --- /dev/null +++ b/packages/functions/src/error.ts @@ -0,0 +1,9 @@ +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/functions/src/party/auth.ts b/packages/functions/src/party/auth.ts new file mode 100644 index 00000000..c8ca3633 --- /dev/null +++ b/packages/functions/src/party/auth.ts @@ -0,0 +1,81 @@ +import { z } from "zod"; +import { Hono } from "hono"; +import { Result } from "../common" +import { describeRoute } from "hono-openapi"; +import type * as Party from "partykit/server"; +import { validator, resolver } from "hono-openapi/zod"; + +const paramsObj = z.object({ + code: z.string(), + state: z.string() +}) + +export module AuthApi { + export const route = new Hono() + .get("/:connection", + describeRoute({ + tags: ["Auth"], + summary: "Authenticate the remote device", + description: "This is a callback function to authenticate the remote device.", + responses: { + 200: { + content: { + "application/json": { + schema: Result(z.literal("Device authenticated successfully")) + }, + }, + description: "Authentication successful.", + }, + 404: { + content: { + "application/json": { + schema: resolver(z.object({ error: z.string() })), + }, + }, + description: "This device does not exist.", + }, + }, + }), + validator( + "param", + z.object({ + connection: z.string().openapi({ + description: "The hostname of the device to login to.", + example: "desktopeuo8vsf", + }), + }), + ), + async (c) => { + const param = c.req.valid("param"); + const env = c.env as any + const room = env.room as Party.Room + + const connection = room.getConnection(param.connection) + if (!connection) { + return c.json({ error: "This device does not exist." }, 404); + } + + const authParams = getUrlParams(new URL(c.req.url)) + const res = paramsObj.safeParse(authParams) + if (res.error) { + return c.json({ error: "Expected url params are missing" }) + } + + connection.send(JSON.stringify({ ...authParams, type: "auth" })) + + // FIXME:We just assume the authentication was successful, might wanna do some questioning in the future + return c.text("Device authenticated successfully") + } + ) +} + +function getUrlParams(url: URL) { + const urlString = url.toString() + const hash = urlString.substring(urlString.indexOf('?') + 1); // Extract the part after the # + const params = new URLSearchParams(hash); + const paramsObj = {} as any; + for (const [key, value] of params.entries()) { + paramsObj[key] = decodeURIComponent(value); + } + return paramsObj; +} \ No newline at end of file diff --git a/packages/functions/src/party/hono.ts b/packages/functions/src/party/hono.ts new file mode 100644 index 00000000..da31f286 --- /dev/null +++ b/packages/functions/src/party/hono.ts @@ -0,0 +1,116 @@ +import "zod-openapi/extend"; +import type * as Party from "partykit/server"; +// import { Resource } from "sst"; +import { ZodError } from "zod"; +import { logger } from "hono/logger"; +// import { subjects } from "../subjects"; +import { VisibleError } from "../error"; +// import { ActorContext } from '@nestri/core/actor'; +import { Hono, type MiddlewareHandler } from "hono"; +import { HTTPException } from "hono/http-exception"; +import { AuthApi } from "./auth"; + + +const app = new Hono().basePath('/parties/main/:id'); +// const auth: MiddlewareHandler = async (c, next) => { +// const client = createClient({ +// clientID: "api", +// issuer: "http://auth.nestri.io" //Resource.Urls.auth +// }); + +// const authHeader = +// c.req.query("authorization") ?? c.req.header("authorization"); +// if (authHeader) { +// const match = authHeader.match(/^Bearer (.+)$/); +// if (!match || !match[1]) { +// throw new VisibleError( +// "input", +// "auth.token", +// "Bearer token not found or improperly formatted", +// ); +// } +// const bearerToken = match[1]; + +// const result = await client.verify(subjects, bearerToken!); +// if (result.err) +// throw new VisibleError("input", "auth.invalid", "Invalid bearer token"); +// if (result.subject.type === "user") { +// // return ActorContext.with( +// // { +// // type: "user", +// // properties: { +// // accessToken: result.subject.properties.accessToken, +// // userID: result.subject.properties.userID, +// // auth: { +// // type: "oauth", +// // clientID: result.aud, +// // }, +// // }, +// // }, +// // next, +// // ); +// } +// } +// } + +app + .use(logger(), async (c, next) => { + c.header("Cache-Control", "no-store"); + return next(); + }) +// .use(auth) + + +app + .route("/auth", AuthApi.route) + // .get("/parties/main/:id", (c) => { + // const id = c.req.param(); + // const env = c.env as any + // const party = env.room as Party.Room + // party.broadcast("hello from hono") + + // return c.text(`Hello there, ${id.id} 👋🏾`) + // }) + .onError((error, c) => { + console.error(error); + if (error instanceof VisibleError) { + return c.json( + { + code: error.code, + message: error.message, + }, + error.kind === "auth" ? 401 : 400, + ); + } + if (error instanceof ZodError) { + const e = error.errors[0]; + if (e) { + return c.json( + { + code: e?.code, + message: e?.message, + }, + 400, + ); + } + } + if (error instanceof HTTPException) { + return c.json( + { + code: "request", + message: "Invalid request", + }, + 400, + ); + } + return c.json( + { + code: "internal", + message: "Internal server error", + }, + 500, + ); + }); + + +export default app \ No newline at end of file diff --git a/packages/functions/src/party/index.ts b/packages/functions/src/party/index.ts new file mode 100644 index 00000000..16392c4c --- /dev/null +++ b/packages/functions/src/party/index.ts @@ -0,0 +1,53 @@ +import type * as Party from "partykit/server"; +import app from "./hono" +export default class Server implements Party.Server { + constructor(readonly room: Party.Room) { } + + onRequest(request: Party.Request): Response | Promise { + + return app.fetch(request as any, { room: this.room }) + } + + getConnectionTags( + conn: Party.Connection, + ctx: Party.ConnectionContext + ) { + console.log("Tagging", conn.id) + // const country = (ctx.request.cf?.country as string) ?? "unknown"; + // return [country]; + return [conn.id] + // return ["AF"] + } + + onConnect(conn: Party.Connection, ctx: Party.ConnectionContext) { + // A websocket just connected! + this.getConnectionTags(conn, ctx) + + console.log( + `Connected: + id: ${conn.id} + room: ${this.room.id} + url: ${new URL(ctx.request.url).pathname}` + ); + + // let's send a message to the connection + // conn.send("hello from server"); + } + + onMessage(message: string, sender: Party.Connection) { + // let's log the message + console.log(`connection ${sender.id} sent message: ${message}`); + // console.log("tags", this.room.getConnections()) + // for (const british of this.room.getConnections(sender.id)) { + // british.send(`Pip-pip!`); + // } + // // as well as broadcast it to all the other connections in the room... + // this.room.broadcast( + // `${sender.id}: ${message}`, + // // ...except for the connection it came from + // [sender.id] + // ); + } +} + +Server satisfies Party.Worker; diff --git a/packages/functions/src/subjects.ts b/packages/functions/src/subjects.ts new file mode 100644 index 00000000..2d8f13c7 --- /dev/null +++ b/packages/functions/src/subjects.ts @@ -0,0 +1,13 @@ +import * as v from "valibot" +import { createSubjects } from "@openauthjs/openauth" + +export const subjects = createSubjects({ + user: v.object({ + accessToken: v.string(), + userID: v.string(), + }), + device: v.object({ + fingerprint: v.string(), + id: v.string() + }) +}) \ No newline at end of file diff --git a/packages/functions/sst-env.d.ts b/packages/functions/sst-env.d.ts new file mode 100644 index 00000000..e31f9fc8 --- /dev/null +++ b/packages/functions/sst-env.d.ts @@ -0,0 +1,41 @@ +/* This file is auto-generated by SST. Do not edit. */ +/* tslint:disable */ +/* eslint-disable */ +/* deno-fmt-ignore-file */ +import "sst" +export {} +import "sst" +declare module "sst" { + export interface Resource { + "AuthFingerprintKey": { + "type": "random.index/randomString.RandomString" + "value": string + } + "InstantAdminToken": { + "type": "sst.sst.Secret" + "value": string + } + "InstantAppId": { + "type": "sst.sst.Secret" + "value": string + } + "LoopsApiKey": { + "type": "sst.sst.Secret" + "value": string + } + "Urls": { + "api": string + "auth": string + "type": "sst.sst.Linkable" + } + } +} +// cloudflare +import * as cloudflare from "@cloudflare/workers-types"; +declare module "sst" { + export interface Resource { + "Api": cloudflare.Service + "Auth": cloudflare.Service + "CloudflareAuthKV": cloudflare.KVNamespace + } +} diff --git a/packages/functions/tsconfig.json b/packages/functions/tsconfig.json new file mode 100644 index 00000000..238655f2 --- /dev/null +++ b/packages/functions/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + // Enable latest features + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +} diff --git a/packages/input/sst-env.d.ts b/packages/input/sst-env.d.ts new file mode 100644 index 00000000..f90ea1f4 --- /dev/null +++ b/packages/input/sst-env.d.ts @@ -0,0 +1,42 @@ +/* This file is auto-generated by SST. Do not edit. */ +/* tslint:disable */ +/* eslint-disable */ +/* deno-fmt-ignore-file */ +import "sst" +export {} +declare module "sst" { + export interface Resource { + "Api": { + "type": "sst.cloudflare.Worker" + "url": string + } + "Auth": { + "type": "sst.cloudflare.Worker" + "url": string + } + "AuthFingerprintKey": { + "type": "random.index/randomString.RandomString" + "value": string + } + "CloudflareAuthKV": { + "type": "sst.cloudflare.Kv" + } + "InstantAdminToken": { + "type": "sst.sst.Secret" + "value": string + } + "InstantAppId": { + "type": "sst.sst.Secret" + "value": string + } + "LoopsApiKey": { + "type": "sst.sst.Secret" + "value": string + } + "Urls": { + "api": string + "auth": string + "type": "sst.sst.Linkable" + } + } +} diff --git a/packages/master/go.mod b/packages/master/go.mod deleted file mode 100644 index 7565b333..00000000 --- a/packages/master/go.mod +++ /dev/null @@ -1,32 +0,0 @@ -module master - -go 1.23.3 - -require github.com/docker/docker v27.3.1+incompatible - -require ( - github.com/Microsoft/go-winio v0.4.14 // indirect - github.com/containerd/log v0.1.0 // indirect - github.com/distribution/reference v0.6.0 // indirect - github.com/docker/go-connections v0.5.0 // indirect - github.com/docker/go-units v0.5.0 // indirect - github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/go-logr/logr v1.4.2 // indirect - github.com/go-logr/stdr v1.2.2 // indirect - github.com/gogo/protobuf v1.3.2 // indirect - github.com/moby/docker-image-spec v1.3.1 // indirect - github.com/moby/term v0.5.0 // indirect - github.com/morikuni/aec v1.0.0 // indirect - github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.1.0 // indirect - github.com/pkg/errors v0.9.1 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 // indirect - go.opentelemetry.io/otel v1.32.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0 // indirect - go.opentelemetry.io/otel/metric v1.32.0 // indirect - go.opentelemetry.io/otel/sdk v1.32.0 // indirect - go.opentelemetry.io/otel/trace v1.32.0 // indirect - golang.org/x/sys v0.27.0 // indirect - golang.org/x/time v0.8.0 // indirect - gotest.tools/v3 v3.5.1 // indirect -) diff --git a/packages/master/main.go b/packages/master/main.go deleted file mode 100644 index 13164f8f..00000000 --- a/packages/master/main.go +++ /dev/null @@ -1,80 +0,0 @@ -package main - -import ( - "context" - "io" - "os" - - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/image" - "github.com/docker/docker/client" - "github.com/docker/docker/pkg/stdcopy" -) - -func main() { - ctx := context.Background() - - cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) - if err != nil { - panic(err) - } - defer cli.Close() - - // Try to get the Docker version - _, err = cli.ServerVersion(ctx) - if err != nil { - // If an error occurs (e.g., Docker is not running), return false - panic(err) - } - - // Download the image - containerName := "hello-world" - - reader, err := cli.ImagePull(ctx, containerName, image.PullOptions{}) - if err != nil { - panic(err) - } - - defer reader.Close() - - // cli.ImagePull is asynchronous. - // The reader needs to be read completely for the pull operation to complete. - // If stdout is not required, consider using io.Discard instead of os.Stdout. - io.Copy(os.Stdout, reader) - - resp, err := cli.ContainerCreate(ctx, &container.Config{ - Image: "hello-world", - }, - nil, nil, nil, containerName) - if err != nil { - panic(err) - } - - // Start the container - if err := cli.ContainerStart(ctx, resp.ID, container.StartOptions{}); err != nil { - panic(err) - } - - // Wait for the container to finish and get its logs - statusCh, errCh := cli.ContainerWait(ctx, resp.ID, container.WaitConditionNotRunning) - select { - case err := <-errCh: - if err != nil { - panic(err) - } - case <-statusCh: - } - - out, err := cli.ContainerLogs(ctx, resp.ID, container.LogsOptions{ShowStdout: true}) - if err != nil { - panic(err) - } - - stdcopy.StdCopy(os.Stdout, os.Stderr, out) - - // Remove the container - if err := cli.ContainerRemove(ctx, resp.ID, container.RemoveOptions{}); err != nil { - panic(err) - } - -} diff --git a/packages/moq/sst-env.d.ts b/packages/moq/sst-env.d.ts new file mode 100644 index 00000000..f90ea1f4 --- /dev/null +++ b/packages/moq/sst-env.d.ts @@ -0,0 +1,42 @@ +/* This file is auto-generated by SST. Do not edit. */ +/* tslint:disable */ +/* eslint-disable */ +/* deno-fmt-ignore-file */ +import "sst" +export {} +declare module "sst" { + export interface Resource { + "Api": { + "type": "sst.cloudflare.Worker" + "url": string + } + "Auth": { + "type": "sst.cloudflare.Worker" + "url": string + } + "AuthFingerprintKey": { + "type": "random.index/randomString.RandomString" + "value": string + } + "CloudflareAuthKV": { + "type": "sst.cloudflare.Kv" + } + "InstantAdminToken": { + "type": "sst.sst.Secret" + "value": string + } + "InstantAppId": { + "type": "sst.sst.Secret" + "value": string + } + "LoopsApiKey": { + "type": "sst.sst.Secret" + "value": string + } + "Urls": { + "api": string + "auth": string + "type": "sst.sst.Linkable" + } + } +} diff --git a/packages/typescript-config/base.json b/packages/typescript-config/base.json deleted file mode 100644 index 5064c1e8..00000000 --- a/packages/typescript-config/base.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/tsconfig", - "compilerOptions": { - "allowJs": true, - "target": "ES2022", - "module": "ES2022", - "lib": ["es2022", "DOM", "WebWorker", "DOM.Iterable"], - "jsx": "react-jsx", - "jsxImportSource": "@builder.io/qwik", - "strict": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "moduleResolution": "Bundler", - "esModuleInterop": true, - "skipLibCheck": true, - "incremental": true, - "isolatedModules": true, - "outDir": "tmp", - "noEmit": true, - } -} diff --git a/packages/typescript-config/nextjs.json b/packages/typescript-config/nextjs.json deleted file mode 100644 index 44f42899..00000000 --- a/packages/typescript-config/nextjs.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/tsconfig", - "display": "Next.js", - "extends": "./base.json", - "compilerOptions": { - "plugins": [{ "name": "next" }], - "module": "ESNext", - "moduleResolution": "Bundler", - "allowJs": true, - "jsx": "preserve", - "noEmit": true - } -} diff --git a/packages/typescript-config/package.json b/packages/typescript-config/package.json deleted file mode 100644 index 41f60c53..00000000 --- a/packages/typescript-config/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "@nestri/typescript-config", - "version": "0.0.0", - "private": true, - "license": "MIT", - "publishConfig": { - "access": "public" - } -} diff --git a/packages/typescript-config/react-library.json b/packages/typescript-config/react-library.json deleted file mode 100644 index 44924d9e..00000000 --- a/packages/typescript-config/react-library.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/tsconfig", - "display": "React Library", - "extends": "./base.json", - "compilerOptions": { - "jsx": "react-jsx" - } -} diff --git a/packages/ui/.eslintrc.cjs b/packages/ui/.eslintrc.cjs index bbb2665d..92f00457 100644 --- a/packages/ui/.eslintrc.cjs +++ b/packages/ui/.eslintrc.cjs @@ -1,7 +1,14 @@ module.exports = { root: true, + env: { + browser: true, + es2021: true, + node: true, + }, extends: [ - "@nestri/eslint-config/qwik.js", + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:qwik/recommended", ], parser: "@typescript-eslint/parser", parserOptions: { @@ -12,5 +19,24 @@ module.exports = { ecmaFeatures: { jsx: true, }, - } -}; + }, + plugins: ["@typescript-eslint"], + rules: { + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/explicit-module-boundary-types": "off", + "@typescript-eslint/no-inferrable-types": "off", + "@typescript-eslint/no-non-null-assertion": "off", + "@typescript-eslint/no-empty-interface": "off", + "@typescript-eslint/no-namespace": "off", + "@typescript-eslint/no-empty-function": "off", + "@typescript-eslint/no-this-alias": "off", + "@typescript-eslint/ban-types": "off", + "@typescript-eslint/ban-ts-comment": "off", + "prefer-spread": "off", + "no-case-declarations": "off", + "no-console": "off", + "@typescript-eslint/no-unused-vars": ["error"], + "@typescript-eslint/consistent-type-imports": "warn", + "@typescript-eslint/no-unnecessary-condition": "warn", + }, +}; \ No newline at end of file diff --git a/packages/ui/package.json b/packages/ui/package.json index bee583b5..c8550fb4 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -29,9 +29,9 @@ "@fontsource/geist-sans": "^5.1.0", "@modular-forms/qwik": "0.26.1", "@nestri/core": "*", - "@nestri/eslint-config": "*", - "@nestri/typescript-config": "*", "@types/eslint": "^8.56.5", + "@typescript-eslint/eslint-plugin": "latest", + "@typescript-eslint/parser": "latest", "@types/node": "^20.11.24", "@types/nprogress": "^0.2.3", "@types/react": "^18.2.28", @@ -40,10 +40,12 @@ "body-scroll-lock-upgrade": "^1.1.0", "clsx": "^2.1.1", "eslint": "^8.57.0", + "eslint-plugin-qwik": "latest", "focus-trap": "^7.5.4", "framer-motion": "^11.3.24", "nprogress": "^0.2.0", "postcss": "^8.4.41", + "prettier": "latest", "react": "18.2.0", "react-dom": "18.2.0", "react-wrap-balancer": "^1.1.1", diff --git a/packages/ui/src/image/index.ts b/packages/ui/src/image/index.ts index 6ce4c71f..794ef13f 100644 --- a/packages/ui/src/image/index.ts +++ b/packages/ui/src/image/index.ts @@ -1,2 +1,2 @@ -export * from "./image-loader.tsx" -export * from "./basic-image-loader.tsx" \ No newline at end of file +export * from "./image-loader" +export * from "./basic-image-loader" \ No newline at end of file diff --git a/packages/ui/sst-env.d.ts b/packages/ui/sst-env.d.ts new file mode 100644 index 00000000..f90ea1f4 --- /dev/null +++ b/packages/ui/sst-env.d.ts @@ -0,0 +1,42 @@ +/* This file is auto-generated by SST. Do not edit. */ +/* tslint:disable */ +/* eslint-disable */ +/* deno-fmt-ignore-file */ +import "sst" +export {} +declare module "sst" { + export interface Resource { + "Api": { + "type": "sst.cloudflare.Worker" + "url": string + } + "Auth": { + "type": "sst.cloudflare.Worker" + "url": string + } + "AuthFingerprintKey": { + "type": "random.index/randomString.RandomString" + "value": string + } + "CloudflareAuthKV": { + "type": "sst.cloudflare.Kv" + } + "InstantAdminToken": { + "type": "sst.sst.Secret" + "value": string + } + "InstantAppId": { + "type": "sst.sst.Secret" + "value": string + } + "LoopsApiKey": { + "type": "sst.sst.Secret" + "value": string + } + "Urls": { + "api": string + "auth": string + "type": "sst.sst.Linkable" + } + } +} diff --git a/packages/ui/tsconfig.json b/packages/ui/tsconfig.json index 673ef4b7..bdd4600f 100644 --- a/packages/ui/tsconfig.json +++ b/packages/ui/tsconfig.json @@ -1,10 +1,26 @@ { - "extends": "@nestri/typescript-config/base.json", "compilerOptions": { + "allowJs": true, + "target": "ES2017", + "rootDir": "./", + "module": "ES2022", + "lib": ["es2022", "DOM", "WebWorker", "DOM.Iterable"], + "jsx": "react-jsx", + "jsxImportSource": "@builder.io/qwik", + "strict": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "moduleResolution": "Bundler", + "esModuleInterop": true, + "skipLibCheck": true, + "incremental": true, + "isolatedModules": true, "outDir": "tmp", - "rootDir": ".", - "allowImportingTsExtensions": true + "noEmit": true, + "paths": { + "@/*": ["./src/*"] + } }, - "files": [".eslintrc.cjs"], - "include": ["src", "./*.d.ts"] -} + "files": ["./.eslintrc.cjs"], + "include": ["src", "./*.d.ts", "./*.config.ts","./*.config.js"] +} \ No newline at end of file diff --git a/packages/ui/tsconfig.lint.json b/packages/ui/tsconfig.lint.json deleted file mode 100644 index f150d092..00000000 --- a/packages/ui/tsconfig.lint.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "@nestri/typescript-config/base.json", - "compilerOptions": { - "outDir": "dist", - "allowImportingTsExtensions": true - }, - "include": ["src", "./*.config.js","./.eslintrc.js"], - "exclude": ["node_modules", "dist"] -} diff --git a/sst-env.d.ts b/sst-env.d.ts index 9ee5cc1b..e31f9fc8 100644 --- a/sst-env.d.ts +++ b/sst-env.d.ts @@ -1,12 +1,41 @@ +/* This file is auto-generated by SST. Do not edit. */ /* tslint:disable */ /* eslint-disable */ +/* deno-fmt-ignore-file */ +import "sst" +export {} import "sst" declare module "sst" { export interface Resource { - "CloudflareAccountId": { + "AuthFingerprintKey": { + "type": "random.index/randomString.RandomString" + "value": string + } + "InstantAdminToken": { "type": "sst.sst.Secret" "value": string } + "InstantAppId": { + "type": "sst.sst.Secret" + "value": string + } + "LoopsApiKey": { + "type": "sst.sst.Secret" + "value": string + } + "Urls": { + "api": string + "auth": string + "type": "sst.sst.Linkable" + } + } +} +// cloudflare +import * as cloudflare from "@cloudflare/workers-types"; +declare module "sst" { + export interface Resource { + "Api": cloudflare.Service + "Auth": cloudflare.Service + "CloudflareAuthKV": cloudflare.KVNamespace } } -export {} diff --git a/sst.config.ts b/sst.config.ts index ba0495de..423e9da0 100644 --- a/sst.config.ts +++ b/sst.config.ts @@ -5,11 +5,12 @@ export default $config({ return { name: "nestri", removal: input?.stage === "production" ? "retain" : "remove", - home: "aws", + home: "cloudflare", providers: { cloudflare: "5.37.1", docker: "4.5.5", "@pulumi/command": "1.0.1", + random: "4.16.8", }, }; },