mirror of
https://github.com/nestriness/nestri.git
synced 2025-12-12 08:45:38 +02:00
✨ feat: Add auth flow (#146)
This adds a simple way to incorporate a centralized authentication flow. The idea is to have the user, API and SSH (for machine authentication) all in one place using `openauthjs` + `SST` We also have a database now :) > We are using InstantDB as it allows us to authenticate a use with just the email. Plus it is super simple simple to use _of course after the initial fumbles trying to design the db and relationships_
This commit is contained in:
33
packages/api/.gitignore
vendored
33
packages/api/.gitignore
vendored
@@ -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
|
||||
@@ -1,12 +0,0 @@
|
||||
# Nexus
|
||||
|
||||
## Development
|
||||
|
||||
```
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
```
|
||||
npm run deploy
|
||||
```
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.2 KiB |
@@ -1,14 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="48.672001"
|
||||
height="36.804001"
|
||||
viewBox="0 0 12.8778 9.7377253"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="layer1">
|
||||
<path
|
||||
d="m 2.093439,1.7855532 h 8.690922 V 2.2639978 H 2.093439 Z m 0,2.8440874 h 8.690922 V 5.1080848 H 2.093439 Z m 0,2.8440866 h 8.690922 V 7.952172 H 2.093439 Z"
|
||||
style="font-size:12px;fill:#ff4f01;fill-opacity:1;fill-rule:evenodd;stroke:#ff4f01;stroke-width:1.66201;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 590 B |
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
4
packages/api/src/types/api.d.ts
vendored
4
packages/api/src/types/api.d.ts
vendored
@@ -1,4 +0,0 @@
|
||||
declare module '*wasm' {
|
||||
const content: any;
|
||||
export default content;
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
export const createAvatarSvg = (size: number, gradient: { fromColor: string, toColor: string }, fileType?: string, text?: string) => (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g>
|
||||
<defs>
|
||||
<linearGradient id="gradient" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stopColor={gradient.fromColor} />
|
||||
<stop offset="100%" stopColor={gradient.toColor} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect fill="url(#gradient)" x="0" y="0" width={size} height={size} />
|
||||
{fileType === "svg" && text && (
|
||||
<text
|
||||
x="50%"
|
||||
y="50%"
|
||||
alignmentBaseline="central"
|
||||
dominantBaseline="central"
|
||||
textAnchor="middle"
|
||||
fill="#fff"
|
||||
fontFamily="sans-serif"
|
||||
fontSize={(size * 0.9) / text.length}
|
||||
>
|
||||
{text}
|
||||
</text>
|
||||
)}
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
@@ -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(),
|
||||
};
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './gradient'
|
||||
export * from './create-avatar'
|
||||
@@ -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"
|
||||
},
|
||||
}
|
||||
@@ -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"
|
||||
47
packages/cache/caches.ts
vendored
47
packages/cache/caches.ts
vendored
@@ -1,47 +0,0 @@
|
||||
const buildCacheKey = (namespace: string) => (key: string) => {
|
||||
return `${namespace}:${key}`;
|
||||
};
|
||||
|
||||
export interface KVResponseCache {
|
||||
match(key: string): Promise<Response | null>;
|
||||
put(key: string, res: Response, options?: KVNamespacePutOptions): Promise<void>;
|
||||
delete(key: string): Promise<void>;
|
||||
}
|
||||
|
||||
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`)),
|
||||
]);
|
||||
},
|
||||
};
|
||||
};
|
||||
8
packages/cache/index.ts
vendored
8
packages/cache/index.ts
vendored
@@ -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 };
|
||||
47
packages/cache/middleware.ts
vendored
47
packages/cache/middleware.ts
vendored
@@ -1,47 +0,0 @@
|
||||
import { kvResponseCache } from "./caches";
|
||||
|
||||
import type { Filter } from "./types";
|
||||
import type { Context, Env, MiddlewareHandler } from "hono";
|
||||
|
||||
type Namespace<E extends Env> = string | ((c: Context<E>) => string);
|
||||
interface GetCacheKey<E extends Env> {
|
||||
(c: Context<E>): string;
|
||||
}
|
||||
|
||||
type KVCacheOption<E extends Env & { Bindings: Record<string, unknown> }> = {
|
||||
key: keyof E["Bindings"];
|
||||
namespace: Namespace<E>;
|
||||
getCacheKey?: GetCacheKey<E>;
|
||||
options?: KVNamespacePutOptions;
|
||||
};
|
||||
|
||||
export const defaultGetCacheKey = <E extends Env>(c: Context<E>) => c.req.url;
|
||||
|
||||
export const kvCaches =
|
||||
<E extends Env & { Bindings: Record<string, unknown> }>({
|
||||
key: bindingKey,
|
||||
namespace,
|
||||
options,
|
||||
getCacheKey = defaultGetCacheKey,
|
||||
}: KVCacheOption<E>): MiddlewareHandler<E> =>
|
||||
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));
|
||||
}
|
||||
};
|
||||
13
packages/cache/package.json
vendored
13
packages/cache/package.json
vendored
@@ -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"
|
||||
}
|
||||
}
|
||||
15
packages/cache/tsconfig.json
vendored
15
packages/cache/tsconfig.json
vendored
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"lib": [
|
||||
"ESNext"
|
||||
],
|
||||
"types": [
|
||||
"@cloudflare/workers-types/2023-07-01"
|
||||
]
|
||||
},
|
||||
}
|
||||
3
packages/cache/types.ts
vendored
3
packages/cache/types.ts
vendored
@@ -1,3 +0,0 @@
|
||||
export type Filter<T extends Record<string, unknown>, V> = {
|
||||
[K in keyof T as T[K] extends V ? K : never]: T[K];
|
||||
};
|
||||
3
packages/certs/.gitignore
vendored
3
packages/certs/.gitignore
vendored
@@ -1,3 +0,0 @@
|
||||
.terraform
|
||||
relay_*
|
||||
terraform.tfstate
|
||||
61
packages/certs/.terraform.lock.hcl
generated
61
packages/certs/.terraform.lock.hcl
generated
@@ -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",
|
||||
]
|
||||
}
|
||||
@@ -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_*
|
||||
```
|
||||
@@ -1,7 +0,0 @@
|
||||
variable "email" {
|
||||
description = "Your email address, used for LetsEncrypt"
|
||||
}
|
||||
|
||||
variable "domain" {
|
||||
description = "domain name"
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
domain = "fst.so"
|
||||
email = "wanjohiryan33@gmail.com"
|
||||
50
packages/cli/cmd/root.go
Normal file
50
packages/cli/cmd/root.go
Normal file
@@ -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
|
||||
}
|
||||
26
packages/cli/cmd/run.go
Normal file
26
packages/cli/cmd/run.go
Normal file
@@ -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
|
||||
},
|
||||
}
|
||||
54
packages/cli/go.mod
Normal file
54
packages/cli/go.mod
Normal file
@@ -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
|
||||
)
|
||||
@@ -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=
|
||||
26
packages/cli/internal/api/api.go
Normal file
26
packages/cli/internal/api/api.go
Normal file
@@ -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)
|
||||
}
|
||||
44
packages/cli/internal/auth/auth.go
Normal file
44
packages/cli/internal/auth/auth.go
Normal file
@@ -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
|
||||
}
|
||||
202
packages/cli/internal/machine/machine.go
Normal file
202
packages/cli/internal/machine/machine.go
Normal file
@@ -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, "")
|
||||
// }
|
||||
112
packages/cli/internal/party/client.go
Normal file
112
packages/cli/internal/party/client.go
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
125
packages/cli/internal/party/retry.go
Normal file
125
packages/cli/internal/party/retry.go
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
38
packages/cli/internal/resource/resource.go
Normal file
38
packages/cli/internal/resource/resource.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
286
packages/cli/internal/session/start.go
Normal file
286
packages/cli/internal/session/start.go
Normal file
@@ -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
|
||||
}
|
||||
76
packages/cli/internal/session/steam.go
Normal file
76
packages/cli/internal/session/steam.go
Normal file
@@ -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
|
||||
}
|
||||
72
packages/cli/internal/session/utils.go
Normal file
72
packages/cli/internal/session/utils.go
Normal file
@@ -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
|
||||
}
|
||||
58
packages/cli/main.go
Normal file
58
packages/cli/main.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
// }
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./image-brightness-analyzer.ts"
|
||||
35
packages/core/instant.perms.ts
Normal file
35
packages/core/instant.perms.ts
Normal file
@@ -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;
|
||||
80
packages/core/instant.schema.ts
Normal file
80
packages/core/instant.schema.ts
Normal file
@@ -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;
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
85
packages/core/src/actor.ts
Normal file
85
packages/core/src/actor.ts
Normal file
@@ -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<Actor>();
|
||||
|
||||
export function useCurrentUser() {
|
||||
const actor = ActorContext.use();
|
||||
if (actor.type === "user") return {
|
||||
id:actor.properties.userID,
|
||||
token: actor.properties.accessToken
|
||||
};
|
||||
throw new VisibleError(
|
||||
"auth",
|
||||
"unauthorized",
|
||||
`You don't have permission to access this resource`,
|
||||
);
|
||||
}
|
||||
|
||||
export function useCurrentDevice() {
|
||||
const actor = ActorContext.use();
|
||||
if (actor.type === "device") return {
|
||||
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<T extends Actor["type"]>(type: T) {
|
||||
const actor = useActor();
|
||||
if (actor.type !== type)
|
||||
throw new VisibleError("auth", "actor.invalid", `Actor is not "${type}"`);
|
||||
return actor as Extract<Actor, { type: T }>;
|
||||
}
|
||||
7
packages/core/src/common.ts
Normal file
7
packages/core/src/common.ts
Normal file
@@ -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.`;
|
||||
}
|
||||
17
packages/core/src/context.ts
Normal file
17
packages/core/src/context.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { AsyncLocalStorage } from "node:async_hooks";
|
||||
|
||||
export function createContext<T>() {
|
||||
const storage = new AsyncLocalStorage<T>();
|
||||
return {
|
||||
use() {
|
||||
const result = storage.getStore();
|
||||
if (!result) {
|
||||
throw new Error("No context available");
|
||||
}
|
||||
return result;
|
||||
},
|
||||
with<R>(value: T, fn: () => R) {
|
||||
return storage.run<R>(value, fn);
|
||||
},
|
||||
};
|
||||
}
|
||||
12
packages/core/src/database.ts
Normal file
12
packages/core/src/database.ts
Normal file
@@ -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
|
||||
25
packages/core/src/email/index.ts
Normal file
25
packages/core/src/email/index.ts
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
9
packages/core/src/error.ts
Normal file
9
packages/core/src/error.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export class VisibleError extends Error {
|
||||
constructor(
|
||||
public kind: "input" | "auth",
|
||||
public code: string,
|
||||
public message: string,
|
||||
) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
45
packages/core/src/examples.ts
Normal file
45
packages/core/src/examples.ts
Normal file
@@ -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
|
||||
// };
|
||||
|
||||
}
|
||||
140
packages/core/src/machine/index.ts
Normal file
140
packages/core/src/machine/index.ts
Normal file
@@ -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"
|
||||
})
|
||||
}
|
||||
48
packages/core/src/team/index.ts
Normal file
48
packages/core/src/team/index.ts
Normal file
@@ -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
|
||||
})
|
||||
}
|
||||
17
packages/core/src/types.ts
Normal file
17
packages/core/src/types.ts
Normal file
@@ -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
|
||||
}
|
||||
37
packages/core/src/user/index.ts
Normal file
37
packages/core/src/user/index.ts
Normal file
@@ -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
|
||||
})
|
||||
}
|
||||
13
packages/core/src/utils/fn.ts
Normal file
13
packages/core/src/utils/fn.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { ZodSchema, z } from "zod";
|
||||
|
||||
export function fn<
|
||||
Arg1 extends ZodSchema,
|
||||
Callback extends (arg1: z.output<Arg1>) => any,
|
||||
>(arg1: Arg1, cb: Callback) {
|
||||
const result = function (input: z.input<typeof arg1>): ReturnType<Callback> {
|
||||
const parsed = arg1.parse(input);
|
||||
return cb.apply(cb, [parsed as any]);
|
||||
};
|
||||
result.schema = arg1;
|
||||
return result;
|
||||
}
|
||||
1
packages/core/src/utils/index.ts
Normal file
1
packages/core/src/utils/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./fn"
|
||||
42
packages/core/sst-env.d.ts
vendored
Normal file
42
packages/core/sst-env.d.ts
vendored
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
9
packages/core/tsconfig.json
Normal file
9
packages/core/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "@tsconfig/node20/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "esnext",
|
||||
"jsx": "react-jsx",
|
||||
"moduleResolution": "bundler",
|
||||
"noUncheckedIndexedAccess": true,
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 "<noop>"
|
||||
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 "<noop>"
|
||||
fi
|
||||
}
|
||||
|
||||
# Main script
|
||||
if [ $# -lt 1 ]; then
|
||||
echo "Usage: $0 <image_name> [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 . ``
|
||||
@@ -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
|
||||
@@ -1,3 +0,0 @@
|
||||
# `@turbo/eslint-config`
|
||||
|
||||
Collection of internal eslint configurations.
|
||||
@@ -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)"],
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -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)"] }],
|
||||
};
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
},
|
||||
};
|
||||
39
packages/eslint-config/react-internal.js
vendored
39
packages/eslint-config/react-internal.js
vendored
@@ -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)"] },
|
||||
],
|
||||
};
|
||||
175
packages/functions/.gitignore
vendored
Normal file
175
packages/functions/.gitignore
vendored
Normal file
@@ -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
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
15
packages/functions/README.md
Normal file
15
packages/functions/README.md
Normal file
@@ -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.
|
||||
20
packages/functions/package.json
Normal file
20
packages/functions/package.json
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
6
packages/functions/partykit.json
Normal file
6
packages/functions/partykit.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"$schema": "https://www.partykit.io/schema.json",
|
||||
"name": "nestri-party",
|
||||
"main": "src/party/index.ts",
|
||||
"compatibilityDate": "2024-12-31"
|
||||
}
|
||||
121
packages/functions/src/adapter.ts
Normal file
121
packages/functions/src/adapter.ts
Normal file
@@ -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<string, string>
|
||||
}
|
||||
|
||||
export type ApiAdapterError =
|
||||
| {
|
||||
type: "invalid_code"
|
||||
}
|
||||
| {
|
||||
type: "invalid_claim"
|
||||
key: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export function ApiAdapter<
|
||||
Claims extends Record<string, string> = Record<string, string>,
|
||||
>(config: {
|
||||
length?: number
|
||||
request: (
|
||||
req: Request,
|
||||
state: ApiAdapterState,
|
||||
body?: Claims,
|
||||
error?: ApiAdapterError,
|
||||
) => Promise<Response>
|
||||
sendCode: (claims: Claims, code: string) => Promise<void | ApiAdapterError>
|
||||
}) {
|
||||
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<ApiAdapterState>(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<ApiAdapterState>(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<typeof ApiAdapter>[0]
|
||||
153
packages/functions/src/api/index.ts
Normal file
153
packages/functions/src/api/index.ts
Normal file
@@ -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
|
||||
160
packages/functions/src/api/machine.ts
Normal file
160
packages/functions/src/api/machine.ts
Normal file
@@ -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);
|
||||
},
|
||||
);
|
||||
}
|
||||
140
packages/functions/src/auth.ts
Normal file
140
packages/functions/src/auth.ts
Normal file
@@ -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<string, string>
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
10
packages/functions/src/common.ts
Normal file
10
packages/functions/src/common.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { z } from "zod";
|
||||
import { resolver } from "hono-openapi/zod";
|
||||
|
||||
export function Result<T extends z.ZodTypeAny>(schema: T) {
|
||||
return resolver(
|
||||
z.object({
|
||||
data: schema,
|
||||
}),
|
||||
);
|
||||
}
|
||||
9
packages/functions/src/error.ts
Normal file
9
packages/functions/src/error.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export class VisibleError extends Error {
|
||||
constructor(
|
||||
public kind: "input" | "auth",
|
||||
public code: string,
|
||||
public message: string,
|
||||
) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
81
packages/functions/src/party/auth.ts
Normal file
81
packages/functions/src/party/auth.ts
Normal file
@@ -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;
|
||||
}
|
||||
116
packages/functions/src/party/hono.ts
Normal file
116
packages/functions/src/party/hono.ts
Normal file
@@ -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
|
||||
53
packages/functions/src/party/index.ts
Normal file
53
packages/functions/src/party/index.ts
Normal file
@@ -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<Response> {
|
||||
|
||||
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;
|
||||
13
packages/functions/src/subjects.ts
Normal file
13
packages/functions/src/subjects.ts
Normal file
@@ -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()
|
||||
})
|
||||
})
|
||||
41
packages/functions/sst-env.d.ts
vendored
Normal file
41
packages/functions/sst-env.d.ts
vendored
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
27
packages/functions/tsconfig.json
Normal file
27
packages/functions/tsconfig.json
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
42
packages/input/sst-env.d.ts
vendored
Normal file
42
packages/input/sst-env.d.ts
vendored
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
42
packages/moq/sst-env.d.ts
vendored
Normal file
42
packages/moq/sst-env.d.ts
vendored
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user