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"