diff --git a/.gitignore b/.gitignore index 48df90c..78303e3 100644 --- a/.gitignore +++ b/.gitignore @@ -6,8 +6,10 @@ node_modules .env /app/hp_ssh.wasm /app/wasm_exec.js + +/public/hp_ssh.wasm +/public/wasm_exec.js + /docs/.vitepress/dist/ /docs/.vitepress/cache/ - ->>>>>>> origin/main /.direnv diff --git a/app/root.tsx b/app/root.tsx index 6cccf27..25e3130 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -8,6 +8,7 @@ import { useNavigation, } from 'react-router'; import '@fontsource-variable/inter'; +import { ExternalScripts } from 'remix-utils/external-scripts'; import { ErrorPopup } from '~/components/Error'; import ProgressBar from '~/components/ProgressBar'; import ToastProvider from '~/components/ToastProvider'; @@ -38,16 +39,17 @@ export function Layout({ children }: { readonly children: React.ReactNode }) { - + - + {children} + diff --git a/app/routes/ssh/console.tsx b/app/routes/ssh/console.tsx index 27ec0ce..8dd6fba 100644 --- a/app/routes/ssh/console.tsx +++ b/app/routes/ssh/console.tsx @@ -1,4 +1,5 @@ /** biome-ignore-all lint/correctness/noNestedComponentDefinitions: Wtf? */ + import { faker } from '@faker-js/faker'; import { eq } from 'drizzle-orm'; import { Loader2 } from 'lucide-react'; @@ -6,16 +7,17 @@ import { useEffect, useState } from 'react'; import { ActionFunctionArgs, data, + LinksFunction, LoaderFunctionArgs, ShouldRevalidateFunction, useLoaderData, useSubmit, } from 'react-router'; +import { ExternalScriptsHandle } from 'remix-utils/external-scripts'; import { LoadContext } from '~/server'; import { EphemeralNodeInsert, ephemeralNodes } from '~/server/db/schema'; import { Machine, PreAuthKey, User } from '~/types'; import { useLiveData } from '~/utils/live-data'; -import '~/wasm_exec'; import UserPrompt from './user-prompt'; import XTerm from './xterm.client'; @@ -27,6 +29,19 @@ export async function loader({ request, context, }: LoaderFunctionArgs) { + const origin = new URL(request.url).origin; + const assets = ['/wasm_exec.js', '/hp_ssh.wasm']; + const missing: string[] = []; + + for (const file of assets) { + const res = await fetch(`${origin}${file}`, { method: 'HEAD' }); + if (!res.ok) missing.push(file); + } + + if (missing.length > 0) { + throw data('WebSSH is not configured in this build.', 405); + } + if (!context.agents?.agentID()) { throw data( 'WebSSH is only available with the Headplane agent integration', @@ -199,6 +214,26 @@ export async function action({ .where(eq(ephemeralNodes.auth_key, authKey)); } +export const links: LinksFunction = () => [ + { + rel: 'preload', + href: '/hp_ssh.wasm', + as: 'fetch', + type: 'application/wasm', + crossOrigin: 'anonymous', + }, +]; + +export const handle: ExternalScriptsHandle = { + scripts: [ + { + src: '/wasm_exec.js', + crossOrigin: 'anonymous', + preload: true, + }, + ], +}; + export default function Page() { const submit = useSubmit(); const { pause } = useLiveData(); @@ -214,48 +249,46 @@ export default function Page() { pause(); const go = new Go(); // Go is defined by wasm_exec.js - - import('~/hp_ssh.wasm?url').then(({ default: url }) => { - WebAssembly.instantiateStreaming(fetch(url), go.importObject).then( - (value) => { - go.run(value.instance); - const handle = TsWasmNet(ipnDetails, { - NotifyState: (state) => { - console.log('State changed:', state); - if (state === 'Running') { - setIpn(handle); - } - }, - NotifyNetMap: (netmap) => { - // Only set NodeKey if it is not already set and then - // also dispatch that to the backend to track the - // ephemeral node. - // - // We open an SSE connection to the backend - // so that when the connection is closed, - // the backend can delete the ephemeral node. - if (nodeKey === null) { - setNodeKey(netmap.NodeKey); - submit( - { - node_key: netmap.NodeKey, - auth_key: ipnDetails.PreAuthKey, - }, - { method: 'POST' }, - ); - } - }, - NotifyBrowseToURL: (url) => { - console.log('Browse to URL:', url); - }, - NotifyPanicRecover: (message) => { - console.error('Panic recover:', message); - }, - }); - - handle.Start(); + WebAssembly.instantiateStreaming( + fetch('/hp_ssh.wasm'), + go.importObject, + ).then((value) => { + go.run(value.instance); + const handle = TsWasmNet(ipnDetails, { + NotifyState: (state) => { + console.log('State changed:', state); + if (state === 'Running') { + setIpn(handle); + } }, - ); + NotifyNetMap: (netmap) => { + // Only set NodeKey if it is not already set and then + // also dispatch that to the backend to track the + // ephemeral node. + // + // We open an SSE connection to the backend + // so that when the connection is closed, + // the backend can delete the ephemeral node. + if (nodeKey === null) { + setNodeKey(netmap.NodeKey); + submit( + { + node_key: netmap.NodeKey, + auth_key: ipnDetails.PreAuthKey, + }, + { method: 'POST' }, + ); + } + }, + NotifyBrowseToURL: (url) => { + console.log('Browse to URL:', url); + }, + NotifyPanicRecover: (message) => { + console.error('Panic recover:', message); + }, + }); + + handle.Start(); }); }, []); diff --git a/build.sh b/build.sh index 38fcb05..9e0a189 100755 --- a/build.sh +++ b/build.sh @@ -10,6 +10,7 @@ cd "$ROOT_DIR" || exit 1 APP_DIR="$ROOT_DIR/app" BUILD_DIR="$ROOT_DIR/build" +PUBLIC_DIR="$ROOT_DIR/public" BUILD_WASM=0 BUILD_APP=0 @@ -19,7 +20,7 @@ SKIP_PATH_CHECKS=0 SKIP_PNPM_PRUNE=0 APP_INSTALL_ONLY=0 -WASM_OUTPUT="$APP_DIR/hp_ssh.wasm" +WASM_OUTPUT="$PUBLIC_DIR/hp_ssh.wasm" AGENT_OUTPUT="$BUILD_DIR/hp_agent" FAKE_SHELL_OUTPUT="$BUILD_DIR/hp_fake_sh"