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 c6d64e31..97a9fac0 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/infra/api.ts b/infra/api.ts index 8b431c48..bd2eb3b4 100644 --- a/infra/api.ts +++ b/infra/api.ts @@ -1,22 +1,52 @@ -import { isPermanentStage } from "./stage"; +import { domain } from "./dns"; +import { secret } from "./secrets" -//TODO: Use this instead of wrangler -// export const api = new sst.cloudflare.Worker("apiApi", { -// url: true, -// handler: "packages/api/src/index.ts", -// // live: true, -// }); +sst.Linkable.wrap(random.RandomString, (resource) => ({ + 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 69cc4180..00000000 Binary files a/packages/api/assets/favicon.ico and /dev/null differ 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 00000000..68b6a7db Binary files /dev/null and b/packages/functions/.partykit/state/party/-PartyKitDurable/68b01ab8eae54eaeb4dddaeba12765b8f1d619378926c2dc36df98eae727ff69.sqlite differ 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 00000000..fe9ac284 Binary files /dev/null and b/packages/functions/.partykit/state/party/-PartyKitDurable/68b01ab8eae54eaeb4dddaeba12765b8f1d619378926c2dc36df98eae727ff69.sqlite-shm differ diff --git a/packages/functions/.partykit/state/party/-PartyKitDurable/68b01ab8eae54eaeb4dddaeba12765b8f1d619378926c2dc36df98eae727ff69.sqlite-wal b/packages/functions/.partykit/state/party/-PartyKitDurable/68b01ab8eae54eaeb4dddaeba12765b8f1d619378926c2dc36df98eae727ff69.sqlite-wal new file mode 100644 index 00000000..e69de29b diff --git a/packages/functions/.partykit/state/party/-PartyKitDurable/73ab631fd54eedb7cc3c85a33a542e132553b5beef99527702def1f3679cb4de.sqlite b/packages/functions/.partykit/state/party/-PartyKitDurable/73ab631fd54eedb7cc3c85a33a542e132553b5beef99527702def1f3679cb4de.sqlite new file mode 100644 index 00000000..68b6a7db Binary files /dev/null and b/packages/functions/.partykit/state/party/-PartyKitDurable/73ab631fd54eedb7cc3c85a33a542e132553b5beef99527702def1f3679cb4de.sqlite differ 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 00000000..4ebf78cf Binary files /dev/null and b/packages/functions/.partykit/state/party/-PartyKitDurable/edf5bdf97b1b08ea4cbf3f72c6dac736c54a14b079b0bae3773d08ab17264bae.sqlite differ 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 00000000..e65f8bb7 Binary files /dev/null and b/packages/functions/.partykit/state/party/-PartyKitDurable/edf5bdf97b1b08ea4cbf3f72c6dac736c54a14b079b0bae3773d08ab17264bae.sqlite-shm differ diff --git a/packages/functions/.partykit/state/party/-PartyKitDurable/edf5bdf97b1b08ea4cbf3f72c6dac736c54a14b079b0bae3773d08ab17264bae.sqlite-wal b/packages/functions/.partykit/state/party/-PartyKitDurable/edf5bdf97b1b08ea4cbf3f72c6dac736c54a14b079b0bae3773d08ab17264bae.sqlite-wal new file mode 100644 index 00000000..2e704a8f Binary files /dev/null and b/packages/functions/.partykit/state/party/-PartyKitDurable/edf5bdf97b1b08ea4cbf3f72c6dac736c54a14b079b0bae3773d08ab17264bae.sqlite-wal differ diff --git a/packages/functions/README.md b/packages/functions/README.md new file mode 100644 index 00000000..ed5db4f0 --- /dev/null +++ b/packages/functions/README.md @@ -0,0 +1,15 @@ +# auth + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run index.ts +``` + +This project was created using `bun init` in bun v1.1.34. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. diff --git a/packages/functions/package.json b/packages/functions/package.json new file mode 100644 index 00000000..84dc3afd --- /dev/null +++ b/packages/functions/package.json @@ -0,0 +1,20 @@ +{ + "name": "@nestri/functions", + "module": "index.ts", + "type": "module", + "devDependencies": { + "@cloudflare/workers-types": "^4.20241224.0", + "@nestri/core": "*", + "@types/bun": "latest", + "partykit": "^0.0.111", + "valibot": "^1.0.0-beta.9" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "dependencies": { + "hono": "^4.6.15", + "hono-openapi": "^0.3.1", + "partysocket": "1.0.3" + } +} diff --git a/packages/functions/partykit.json b/packages/functions/partykit.json new file mode 100644 index 00000000..5bb41d86 --- /dev/null +++ b/packages/functions/partykit.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://www.partykit.io/schema.json", + "name": "nestri-party", + "main": "src/party/index.ts", + "compatibilityDate": "2024-12-31" +} \ No newline at end of file diff --git a/packages/functions/src/adapter.ts b/packages/functions/src/adapter.ts new file mode 100644 index 00000000..14be1907 --- /dev/null +++ b/packages/functions/src/adapter.ts @@ -0,0 +1,121 @@ +import type { Context } from "hono" +import type { Adapter } from "@openauthjs/openauth/adapter/adapter" +import { generateUnbiasedDigits, timingSafeCompare } from "@openauthjs/openauth/random" + +export type ApiAdapterState = + | { + type: "start" + } + | { + type: "code" + resend?: boolean + code: string + claims: Record + } + +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", }, }; },