feat: switch to oxc linting/formatting

This commit is contained in:
Aarnav Tale
2026-01-15 22:05:29 -05:00
parent 0df4130217
commit 034ba25ce3
11 changed files with 967 additions and 724 deletions
+10
View File
@@ -0,0 +1,10 @@
{
"$schema": "./node_modules/oxfmt/configuration_schema.json",
"endOfLine": "lf",
"experimentalSortImports": {},
"experimentalTailwindcss": {},
"ignorePatterns": [],
"printWidth": 100,
"tabWidth": 2,
"useTabs": false
}
+144
View File
@@ -0,0 +1,144 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["unicorn", "typescript", "oxc"],
"categories": {},
"rules": {
"constructor-super": "warn",
"for-direction": "warn",
"no-async-promise-executor": "warn",
"no-caller": "warn",
"no-class-assign": "warn",
"no-compare-neg-zero": "warn",
"no-cond-assign": "warn",
"no-const-assign": "warn",
"no-constant-binary-expression": "warn",
"no-constant-condition": "warn",
"no-control-regex": "warn",
"no-debugger": "warn",
"no-delete-var": "warn",
"no-dupe-class-members": "warn",
"no-dupe-else-if": "warn",
"no-dupe-keys": "warn",
"no-duplicate-case": "warn",
"no-empty-character-class": "warn",
"no-empty-pattern": "warn",
"no-empty-static-block": "warn",
"no-eval": "warn",
"no-ex-assign": "warn",
"no-extra-boolean-cast": "warn",
"no-func-assign": "warn",
"no-global-assign": "warn",
"no-import-assign": "warn",
"no-invalid-regexp": "warn",
"no-irregular-whitespace": "warn",
"no-loss-of-precision": "warn",
"no-new-native-nonconstructor": "warn",
"no-nonoctal-decimal-escape": "warn",
"no-obj-calls": "warn",
"no-self-assign": "warn",
"no-setter-return": "warn",
"no-shadow-restricted-names": "warn",
"no-sparse-arrays": "warn",
"no-this-before-super": "warn",
"no-unassigned-vars": "warn",
"no-unsafe-finally": "warn",
"no-unsafe-negation": "warn",
"no-unsafe-optional-chaining": "warn",
"no-unused-expressions": "warn",
"no-unused-labels": "warn",
"no-unused-private-class-members": "warn",
"no-unused-vars": "warn",
"no-useless-backreference": "warn",
"no-useless-catch": "warn",
"no-useless-escape": "warn",
"no-useless-rename": "warn",
"no-with": "warn",
"require-yield": "warn",
"use-isnan": "warn",
"valid-typeof": "warn",
"oxc/bad-array-method-on-arguments": "warn",
"oxc/bad-char-at-comparison": "warn",
"oxc/bad-comparison-sequence": "warn",
"oxc/bad-min-max-func": "warn",
"oxc/bad-object-literal-comparison": "warn",
"oxc/bad-replace-all-arg": "warn",
"oxc/const-comparisons": "warn",
"oxc/double-comparisons": "warn",
"oxc/erasing-op": "warn",
"oxc/missing-throw": "warn",
"oxc/number-arg-out-of-range": "warn",
"oxc/only-used-in-recursion": "warn",
"oxc/uninvoked-array-callback": "warn",
"typescript/await-thenable": "warn",
"typescript/no-array-delete": "warn",
"typescript/no-base-to-string": "warn",
"typescript/no-duplicate-enum-values": "warn",
"typescript/no-duplicate-type-constituents": "warn",
"typescript/no-extra-non-null-assertion": "warn",
"typescript/no-floating-promises": "warn",
"typescript/no-for-in-array": "warn",
"typescript/no-implied-eval": "warn",
"typescript/no-meaningless-void-operator": "warn",
"typescript/no-misused-new": "warn",
"typescript/no-misused-spread": "warn",
"typescript/no-non-null-asserted-optional-chain": "warn",
"typescript/no-redundant-type-constituents": "warn",
"typescript/no-this-alias": "warn",
"typescript/no-unnecessary-parameter-property-assignment": "warn",
"typescript/no-unsafe-declaration-merging": "warn",
"typescript/no-unsafe-unary-minus": "warn",
"typescript/no-useless-empty-export": "warn",
"typescript/no-wrapper-object-types": "warn",
"typescript/prefer-as-const": "warn",
"typescript/require-array-sort-compare": "warn",
"typescript/restrict-template-expressions": "warn",
"typescript/triple-slash-reference": "warn",
"typescript/unbound-method": "warn",
"unicorn/no-await-in-promise-methods": "warn",
"unicorn/no-empty-file": "warn",
"unicorn/no-invalid-fetch-options": "warn",
"unicorn/no-invalid-remove-event-listener": "warn",
"unicorn/no-new-array": "warn",
"unicorn/no-single-promise-in-promise-methods": "warn",
"unicorn/no-thenable": "warn",
"unicorn/no-unnecessary-await": "warn",
"unicorn/no-useless-fallback-in-spread": "warn",
"unicorn/no-useless-length-check": "warn",
"unicorn/no-useless-spread": "warn",
"unicorn/prefer-set-size": "warn",
"unicorn/prefer-string-starts-ends-with": "warn"
},
"settings": {
"jsx-a11y": {
"polymorphicPropName": null,
"components": {},
"attributes": {}
},
"next": {
"rootDir": []
},
"react": {
"formComponents": [],
"linkComponents": [],
"version": null
},
"jsdoc": {
"ignorePrivate": false,
"ignoreInternal": false,
"ignoreReplacesDocs": true,
"overrideReplacesDocs": true,
"augmentsExtendsReplacesDocs": false,
"implementsReplacesDocs": false,
"exemptDestructuredRootsFromChecks": false,
"tagNamePreference": {}
},
"vitest": {
"typecheck": false
}
},
"env": {
"builtin": true
},
"globals": {},
"ignorePatterns": []
}
+54 -56
View File
@@ -1,67 +1,65 @@
import { PassThrough } from 'node:stream'; import type { RenderToPipeableStreamOptions } from "react-dom/server";
import { createReadableStreamFromReadable } from '@react-router/node'; import type { AppLoadContext, EntryContext } from "react-router";
import { isbot } from 'isbot';
import type { RenderToPipeableStreamOptions } from 'react-dom/server'; import { createReadableStreamFromReadable } from "@react-router/node";
import { renderToPipeableStream } from 'react-dom/server'; import { isbot } from "isbot";
import type { AppLoadContext, EntryContext } from 'react-router'; import { PassThrough } from "node:stream";
import { ServerRouter } from 'react-router'; import { renderToPipeableStream } from "react-dom/server";
import { ServerRouter } from "react-router";
export const streamTimeout = 5_000; export const streamTimeout = 5_000;
export default function handleRequest( export default function handleRequest(
request: Request, request: Request,
responseStatusCode: number, responseStatusCode: number,
responseHeaders: Headers, responseHeaders: Headers,
routerContext: EntryContext, routerContext: EntryContext,
loadContext: AppLoadContext, _loadContext: AppLoadContext,
) { ) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let shellRendered = false; let shellRendered = false;
const userAgent = request.headers.get('user-agent'); const userAgent = request.headers.get("user-agent");
// Ensure requests from bots and SPA Mode renders wait for all content to load before responding // Ensure requests from bots and SPA Mode renders wait for all content to load before responding
// https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation // https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation
const readyOption: keyof RenderToPipeableStreamOptions = const readyOption: keyof RenderToPipeableStreamOptions =
(userAgent && isbot(userAgent)) || routerContext.isSpaMode (userAgent && isbot(userAgent)) || routerContext.isSpaMode ? "onAllReady" : "onShellReady";
? 'onAllReady'
: 'onShellReady';
const { pipe, abort } = renderToPipeableStream( const { pipe, abort } = renderToPipeableStream(
<ServerRouter context={routerContext} url={request.url} />, <ServerRouter context={routerContext} url={request.url} />,
{ {
[readyOption]() { [readyOption]() {
shellRendered = true; shellRendered = true;
const body = new PassThrough(); const body = new PassThrough();
const stream = createReadableStreamFromReadable(body); const stream = createReadableStreamFromReadable(body);
responseHeaders.set('Content-Type', 'text/html'); responseHeaders.set("Content-Type", "text/html");
resolve( resolve(
new Response(stream, { new Response(stream, {
headers: responseHeaders, headers: responseHeaders,
status: responseStatusCode, status: responseStatusCode,
}), }),
); );
pipe(body); pipe(body);
}, },
onShellError(error: unknown) { onShellError(error: unknown) {
reject(error); reject(error);
}, },
onError(error: unknown) { onError(error: unknown) {
// biome-ignore lint/style/noParameterAssign: Lazy responseStatusCode = 500;
responseStatusCode = 500; // Log streaming rendering errors from inside the shell. Don't log
// Log streaming rendering errors from inside the shell. Don't log // errors encountered during initial shell rendering since they'll
// errors encountered during initial shell rendering since they'll // reject and get logged in handleDocumentRequest.
// reject and get logged in handleDocumentRequest. if (shellRendered) {
if (shellRendered) { console.error(error);
console.error(error); }
} },
}, },
}, );
);
// Abort the rendering stream after the `streamTimeout` so it has tine to // Abort the rendering stream after the `streamTimeout` so it has tine to
// flush down the rejected boundaries // flush down the rejected boundaries
setTimeout(abort, streamTimeout + 1000); setTimeout(abort, streamTimeout + 1000);
}); });
} }
+225 -242
View File
@@ -1,293 +1,276 @@
/** biome-ignore-all lint/correctness/noNestedComponentDefinitions: Wtf? */ import { faker } from "@faker-js/faker";
import { eq } from "drizzle-orm";
import { Loader2 } from "lucide-react";
import { useEffect, useState } from "react";
import { data, type ShouldRevalidateFunction, useSubmit } from "react-router";
import { ExternalScriptsHandle } from "remix-utils/external-scripts";
import { faker } from '@faker-js/faker'; import { EphemeralNodeInsert, ephemeralNodes } from "~/server/db/schema";
import { eq } from 'drizzle-orm'; import { useLiveData } from "~/utils/live-data";
import { Loader2 } from 'lucide-react';
import { useEffect, useState } from 'react'; import type { Route } from "./+types/console";
import { data, type ShouldRevalidateFunction, useSubmit } from 'react-router';
import { ExternalScriptsHandle } from 'remix-utils/external-scripts'; import UserPrompt from "./user-prompt";
import { EphemeralNodeInsert, ephemeralNodes } from '~/server/db/schema'; import XTerm from "./xterm.client";
import { useLiveData } from '~/utils/live-data';
import type { Route } from './+types/console';
import UserPrompt from './user-prompt';
import XTerm from './xterm.client';
export const shouldRevalidate: ShouldRevalidateFunction = () => { export const shouldRevalidate: ShouldRevalidateFunction = () => {
return false; return false;
}; };
export async function loader({ request, context }: Route.LoaderArgs) { export async function loader({ request, context }: Route.LoaderArgs) {
const origin = new URL(request.url).origin; const origin = new URL(request.url).origin;
const assets = ['/wasm_exec.js', '/hp_ssh.wasm']; const assets = ["/wasm_exec.js", "/hp_ssh.wasm"];
const missing: string[] = []; const missing: string[] = [];
for (const file of assets) { for (const file of assets) {
const res = await fetch(`${origin}${file}`, { method: 'HEAD' }); const res = await fetch(`${origin}${file}`, { method: "HEAD" });
if (!res.ok) missing.push(file); if (!res.ok) missing.push(file);
} }
if (missing.length > 0) { if (missing.length > 0) {
throw data('WebSSH is not configured in this build.', 405); throw data("WebSSH is not configured in this build.", 405);
} }
if (!context.agents?.agentID()) { if (!context.agents?.agentID()) {
throw data( throw data("WebSSH is only available with the Headplane agent integration", 400);
'WebSSH is only available with the Headplane agent integration', }
400,
);
}
const session = await context.sessions.auth(request); const session = await context.sessions.auth(request);
if (session.user.subject === 'unknown-non-oauth') { if (session.user.subject === "unknown-non-oauth") {
throw data('Only OAuth users are allowed to use WebSSH', 403); throw data("Only OAuth users are allowed to use WebSSH", 403);
} }
const api = context.hsApi.getRuntimeClient(session.api_key); const api = context.hsApi.getRuntimeClient(session.api_key);
const users = await api.getUsers(); const users = await api.getUsers();
// MARK: This assumes that a user has authenticated with Headscale first // MARK: This assumes that a user has authenticated with Headscale first
// Since the only way to enforce permissions via ACLs is to generate a // Since the only way to enforce permissions via ACLs is to generate a
// pre-authkey which REQUIRES a user ID, meaning the user has to have // pre-authkey which REQUIRES a user ID, meaning the user has to have
// authenticated with Headscale first. // authenticated with Headscale first.
const lookup = users.find((u) => { const lookup = users.find((u) => {
const subject = u.providerId?.split('/').pop(); const subject = u.providerId?.split("/").pop();
if (!subject) { if (!subject) {
return false; return false;
} }
return subject === session.user.subject; return subject === session.user.subject;
}); });
if (!lookup) { if (!lookup) {
throw data( throw data(`User with subject ${session.user.subject} not found within Headscale`, 404);
`User with subject ${session.user.subject} not found within Headscale`, }
404,
);
}
const preAuthKey = await api.createPreAuthKey( const preAuthKey = await api.createPreAuthKey(
lookup.id, lookup.id,
true, // ephemeral true, // ephemeral
false, // reusable false, // reusable
new Date(Date.now() + 60 * 1000), // expiration: 1 minute new Date(Date.now() + 60 * 1000), // expiration: 1 minute
null, // aclTags null, // aclTags
); );
// TODO: Enable config to enforce generate_authkeys capability // TODO: Enable config to enforce generate_authkeys capability
// For now, any user is capable of WebSSH connections // For now, any user is capable of WebSSH connections
// const check = await context.sessions.check( // const check = await context.sessions.check(
// request, // request,
// Capabilities.generate_authkeys, // Capabilities.generate_authkeys,
// ); // );
const qp = new URL(request.url).searchParams; const qp = new URL(request.url).searchParams;
const username = qp.get('username') || undefined; const username = qp.get("username") || undefined;
const hostname = qp.get('hostname') || undefined; const hostname = qp.get("hostname") || undefined;
if (!hostname) { if (!hostname) {
throw data('Missing required parameter: hostname', 400); throw data("Missing required parameter: hostname", 400);
} }
if (!username) { if (!username) {
return { return {
ipnDetails: undefined, ipnDetails: undefined,
sshDetails: { sshDetails: {
username, username,
hostname, hostname,
}, },
}; };
} }
// We're making a request to <url>/key?v=116 to check the CORS headers // We're making a request to <url>/key?v=116 to check the CORS headers
const u = context.config.headscale.public_url ?? context.config.headscale.url; const u = context.config.headscale.public_url ?? context.config.headscale.url;
// const res = await fetch(`${u}/key?v=116`, { // const res = await fetch(`${u}/key?v=116`, {
// method: 'GET', // method: 'GET',
// }); // });
// const corsOrigin = res.headers.get('Access-Control-Allow-Origin'); // const corsOrigin = res.headers.get('Access-Control-Allow-Origin');
// const corsMethods = res.headers.get('Access-Control-Allow-Methods'); // const corsMethods = res.headers.get('Access-Control-Allow-Methods');
// const corsHeaders = res.headers.get('Access-Control-Allow-Headers'); // const corsHeaders = res.headers.get('Access-Control-Allow-Headers');
// console.log(corsOrigin, corsMethods, corsHeaders); // console.log(corsOrigin, corsMethods, corsHeaders);
// if (!corsOrigin || !corsMethods || !corsHeaders) { // if (!corsOrigin || !corsMethods || !corsHeaders) {
// throw data( // throw data(
// 'Headscale server does not have the required CORS headers for WebSSH', // 'Headscale server does not have the required CORS headers for WebSSH',
// 500, // 500,
// ); // );
// } // }
const nodes = await api.getNodes(); const nodes = await api.getNodes();
const lookupNode = nodes.find((n) => n.givenName === hostname); const lookupNode = nodes.find((n) => n.givenName === hostname);
if (!lookupNode) { if (!lookupNode) {
throw data(`Node with hostname ${hostname} not found`, 404); throw data(`Node with hostname ${hostname} not found`, 404);
} }
// Last thing is keeping track of the ephemeral node in the database // Last thing is keeping track of the ephemeral node in the database
// because Headscale doesn't automatically delete ephemeral nodes??? // because Headscale doesn't automatically delete ephemeral nodes???
const [_ephemeralNode] = await context.db const [_ephemeralNode] = await context.db
.insert(ephemeralNodes) .insert(ephemeralNodes)
.values({ .values({
auth_key: preAuthKey.key, auth_key: preAuthKey.key,
} satisfies EphemeralNodeInsert) } satisfies EphemeralNodeInsert)
.returning(); .returning();
return { return {
ipnDetails: { ipnDetails: {
PreAuthKey: preAuthKey.key, PreAuthKey: preAuthKey.key,
Hostname: generateHostname(username), Hostname: generateHostname(username),
ControlURL: u, ControlURL: u,
}, },
sshDetails: { sshDetails: {
username, username,
hostname, hostname,
}, },
}; };
} }
function generateHostname(username: string) { function generateHostname(username: string) {
const adjective = faker.word.adjective({ const adjective = faker.word.adjective({
length: { length: {
min: 3, min: 3,
max: 6, max: 6,
}, },
}); });
const noun = faker.word.noun({ const noun = faker.word.noun({
length: { length: {
min: 3, min: 3,
max: 6, max: 6,
}, },
}); });
return `ssh-${adjective}-${noun}-${username}`; return `ssh-${adjective}-${noun}-${username}`;
} }
export async function action({ request, context }: Route.ActionArgs) { export async function action({ request, context }: Route.ActionArgs) {
await context.sessions.auth(request); await context.sessions.auth(request);
if (!context.agents?.agentID()) { if (!context.agents?.agentID()) {
throw data( throw data("WebSSH is only available with the Headplane agent integration", 400);
'WebSSH is only available with the Headplane agent integration', }
400,
);
}
const form = await request.formData(); const form = await request.formData();
const nodeKey = form.get('node_key'); const nodeKey = form.get("node_key");
const authKey = form.get('auth_key'); const authKey = form.get("auth_key");
if (nodeKey === null || typeof nodeKey !== 'string') { if (nodeKey === null || typeof nodeKey !== "string") {
throw data('Missing node_key', 400); throw data("Missing node_key", 400);
} }
if (authKey === null || typeof authKey !== 'string') { if (authKey === null || typeof authKey !== "string") {
throw data('Missing auth_key', 400); throw data("Missing auth_key", 400);
} }
await context.db await context.db
.update(ephemeralNodes) .update(ephemeralNodes)
.set({ .set({
node_key: nodeKey, node_key: nodeKey,
}) })
.where(eq(ephemeralNodes.auth_key, authKey)); .where(eq(ephemeralNodes.auth_key, authKey));
} }
export const links: Route.LinksFunction = () => [ export const links: Route.LinksFunction = () => [
{ {
rel: 'preload', rel: "preload",
href: '/hp_ssh.wasm', href: "/hp_ssh.wasm",
as: 'fetch', as: "fetch",
type: 'application/wasm', type: "application/wasm",
crossOrigin: 'anonymous', crossOrigin: "anonymous",
}, },
]; ];
export const handle: ExternalScriptsHandle = { export const handle: ExternalScriptsHandle = {
scripts: [ scripts: [
{ {
src: '/wasm_exec.js', src: "/wasm_exec.js",
crossOrigin: 'anonymous', crossOrigin: "anonymous",
preload: true, preload: true,
}, },
], ],
}; };
export default function Page({ export default function Page({ loaderData: { ipnDetails, sshDetails } }: Route.ComponentProps) {
loaderData: { ipnDetails, sshDetails }, const submit = useSubmit();
}: Route.ComponentProps) { const { pause } = useLiveData();
const submit = useSubmit();
const { pause } = useLiveData();
const [ipn, setIpn] = useState<TsWasmNet | null>(null); const [ipn, setIpn] = useState<TsWasmNet | null>(null);
const [nodeKey, setNodeKey] = useState<string | null>(null); const [nodeKey, setNodeKey] = useState<string | null>(null);
useEffect(() => { useEffect(() => {
if (!ipnDetails) { if (!ipnDetails) {
return; return;
} }
pause(); pause();
const go = new Go(); // Go is defined by wasm_exec.js const go = new Go(); // Go is defined by wasm_exec.js
WebAssembly.instantiateStreaming( WebAssembly.instantiateStreaming(fetch("/hp_ssh.wasm"), go.importObject).then((value) => {
fetch('/hp_ssh.wasm'), go.run(value.instance);
go.importObject, const handle = TsWasmNet(ipnDetails, {
).then((value) => { NotifyState: (state) => {
go.run(value.instance); console.log("State changed:", state);
const handle = TsWasmNet(ipnDetails, { if (state === "Running") {
NotifyState: (state) => { setIpn(handle);
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
NotifyNetMap: (netmap) => { // ephemeral node.
// Only set NodeKey if it is not already set and then //
// also dispatch that to the backend to track the // We open an SSE connection to the backend
// ephemeral node. // so that when the connection is closed,
// // the backend can delete the ephemeral node.
// We open an SSE connection to the backend if (nodeKey === null) {
// so that when the connection is closed, setNodeKey(netmap.NodeKey);
// the backend can delete the ephemeral node. submit(
if (nodeKey === null) { {
setNodeKey(netmap.NodeKey); node_key: netmap.NodeKey,
submit( auth_key: ipnDetails.PreAuthKey,
{ },
node_key: netmap.NodeKey, { method: "POST" },
auth_key: ipnDetails.PreAuthKey, );
}, }
{ method: 'POST' }, },
); NotifyBrowseToURL: (url) => {
} console.log("Browse to URL:", url);
}, },
NotifyBrowseToURL: (url) => { NotifyPanicRecover: (message) => {
console.log('Browse to URL:', url); console.error("Panic recover:", message);
}, },
NotifyPanicRecover: (message) => { });
console.error('Panic recover:', message);
},
});
handle.Start(); handle.Start();
}); });
}, []); }, []);
if (!sshDetails.username) { if (!sshDetails.username) {
return <UserPrompt hostname={sshDetails.hostname} />; return <UserPrompt hostname={sshDetails.hostname} />;
} }
return ( return (
<div className="w-screen h-screen bg-headplane-900"> <div className="bg-headplane-900 h-screen w-screen">
{ipn === null ? ( {ipn === null ? (
<div className="mx-auto h-screen flex items-center justify-center"> <div className="mx-auto flex h-screen items-center justify-center">
<Loader2 className="animate-spin size-10 text-headplane-50" /> <Loader2 className="text-headplane-50 size-10 animate-spin" />
</div> </div>
) : ( ) : (
<div className="flex flex-col h-screen"> <div className="flex h-screen flex-col">
<XTerm <XTerm hostname={sshDetails.hostname} ipn={ipn} username={sshDetails.username} />
hostname={sshDetails.hostname} </div>
ipn={ipn} )}
username={sshDetails.username} </div>
/> );
</div>
)}
</div>
);
} }
+71 -77
View File
@@ -4,115 +4,109 @@
@plugin "tailwindcss-react-aria-components"; @plugin "tailwindcss-react-aria-components";
@theme { @theme {
--blur-xs: 2px; --blur-xs: 2px;
--height-editor: calc(100vh - 20rem); --height-editor: calc(100vh - 20rem);
--font-sans: Inter, -apple-system, BlinkMacSystemFont, Helvetica, Arial, --font-sans: Inter, -apple-system, BlinkMacSystemFont, Helvetica, Arial, sans-serif;
sans-serif;
--transition-duration-25: 25ms; --transition-duration-25: 25ms;
--transition-duration-50: 50ms; --transition-duration-50: 50ms;
--color-main-50: #f8fafc; --color-main-50: #f8fafc;
--color-main-100: #f1f5f9; --color-main-100: #f1f5f9;
--color-main-200: #e2e8f0; --color-main-200: #e2e8f0;
--color-main-300: #cbd5e1; --color-main-300: #cbd5e1;
--color-main-400: #94a3b8; --color-main-400: #94a3b8;
--color-main-500: #64748b; --color-main-500: #64748b;
--color-main-600: #475569; --color-main-600: #475569;
--color-main-700: #334155; --color-main-700: #334155;
--color-main-800: #1e293b; --color-main-800: #1e293b;
--color-main-900: #0f172a; --color-main-900: #0f172a;
--color-main-950: #020617; --color-main-950: #020617;
--color-ui-50: #fafafa; --color-ui-50: #fafafa;
--color-ui-100: #f5f5f5; --color-ui-100: #f5f5f5;
--color-ui-200: #e5e5e5; --color-ui-200: #e5e5e5;
--color-ui-300: #d4d4d4; --color-ui-300: #d4d4d4;
--color-ui-400: #a3a3a3; --color-ui-400: #a3a3a3;
--color-ui-500: #737373; --color-ui-500: #737373;
--color-ui-600: #525252; --color-ui-600: #525252;
--color-ui-700: #404040; --color-ui-700: #404040;
--color-ui-800: #262626; --color-ui-800: #262626;
--color-ui-900: #171717; --color-ui-900: #171717;
--color-ui-950: #0a0a0a; --color-ui-950: #0a0a0a;
--color-headplane-50: #f2f2f2; --color-headplane-50: #f2f2f2;
--color-headplane-100: #e6e6e6; --color-headplane-100: #e6e6e6;
--color-headplane-200: #cccccc; --color-headplane-200: #cccccc;
--color-headplane-300: #b3b3b3; --color-headplane-300: #b3b3b3;
--color-headplane-400: #999999; --color-headplane-400: #999999;
--color-headplane-500: #808080; --color-headplane-500: #808080;
--color-headplane-600: #666666; --color-headplane-600: #666666;
--color-headplane-700: #4d4d4d; --color-headplane-700: #4d4d4d;
--color-headplane-800: #343434; --color-headplane-800: #343434;
--color-headplane-900: #1a1a1a; --color-headplane-900: #1a1a1a;
--color-headplane-950: #0d0d0d; --color-headplane-950: #0d0d0d;
--animate-loading: loader 0.8s infinite ease-in-out; --animate-loading: loader 0.8s infinite ease-in-out;
@keyframes loader { @keyframes loader {
from { from {
transform: translateX(-100%); transform: translateX(-100%);
} }
to { to {
transform: translateX(100%); transform: translateX(100%);
} }
} }
} }
@utility container { @utility container {
margin-inline: auto; margin-inline: auto;
padding-inline: 1rem; padding-inline: 1rem;
width: 100%; width: 100%;
max-width: 96rem; /* 1536px - constrain on large displays */ max-width: 96rem; /* 1536px - constrain on large displays */
/* biome-ignore lint/correctness/noUnknownFunction: Tailwind CSS */ @media (width >= theme(--breakpoint-sm)) {
@media (width >= theme(--breakpoint-sm)) { padding-inline: 2rem;
padding-inline: 2rem; }
} @media (width >= theme(--breakpoint-lg)) {
padding-inline: 4rem;
}
/* biome-ignore lint/correctness/noUnknownFunction: Tailwind CSS */ @media (width >= theme(--breakpoint-xl)) {
@media (width >= theme(--breakpoint-lg)) { padding-inline: 5rem;
padding-inline: 4rem; }
}
/* biome-ignore lint/correctness/noUnknownFunction: Tailwind CSS */ @media (width >= theme(--breakpoint-2xl)) {
@media (width >= theme(--breakpoint-xl)) { padding-inline: 6rem;
padding-inline: 5rem; }
}
/* biome-ignore lint/correctness/noUnknownFunction: Tailwind CSS */
@media (width >= theme(--breakpoint-2xl)) {
padding-inline: 6rem;
}
} }
@supports (scrollbar-gutter: stable) { @supports (scrollbar-gutter: stable) {
html { html {
scrollbar-gutter: stable; scrollbar-gutter: stable;
} }
} }
.cm-merge-theme { .cm-merge-theme {
height: 100% !important; height: 100% !important;
} }
.cm-mergeView { .cm-mergeView {
height: 100% !important; height: 100% !important;
} }
.cm-mergeViewEditors { .cm-mergeViewEditors {
height: 100%; height: 100%;
} }
.cm-mergeViewEditor { .cm-mergeViewEditor {
height: 100% !important; height: 100% !important;
} }
/* Weirdest class name characters but ok */ /* Weirdest class name characters but ok */
.cm-mergeView .ͼ1 .cm-scroller, .cm-mergeView .ͼ1 .cm-scroller,
.cm-mergeView .ͼ1 { .cm-mergeView .ͼ1 {
height: 100% !important; height: 100% !important;
} }
-48
View File
@@ -1,48 +0,0 @@
{
"$schema": "https://biomejs.dev/schemas/2.2.0/schema.json",
"vcs": {
"enabled": false,
"clientKind": "git",
"useIgnoreFile": true
},
"files": {
"ignoreUnknown": false,
"includes": ["**", "!app/wasm_exec.js"]
},
"formatter": {
"enabled": true,
"indentStyle": "tab",
"lineWidth": 80,
"lineEnding": "lf"
},
"assist": {
"actions": {
"source": {
"organizeImports": "on",
"useSortedAttributes": "on"
}
}
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"style": {
"useImportType": "off",
"noNonNullAssertion": "off"
},
"correctness": {
"useExhaustiveDependencies": "off"
},
"suspicious": {
"noExplicitAny": "warn"
}
}
},
"javascript": {
"formatter": {
"quoteStyle": "single",
"jsxQuoteStyle": "double"
}
}
}
+24 -20
View File
@@ -6,39 +6,42 @@ sole maintainer, I have a few guidelines which help make it significantly easier
for me to review and merge your contributions. for me to review and merge your contributions.
## Contribution Types ## Contribution Types
- **Bug Reports/Feature Requests**: If you find a bug, please open an - **Bug Reports/Feature Requests**: If you find a bug, please open an
[issue](https://github.com/tale/headplane/issues) using one of the predefined [issue](https://github.com/tale/headplane/issues) using one of the predefined
templates. Issues are only for bug reports and feature requests, not problems templates. Issues are only for bug reports and feature requests, not problems
that you encounter from misconfiguration or usage. Those belong in the that you encounter from misconfiguration or usage. Those belong in the
[discussions](https://github.com/tale/headplane/discussions) section. [discussions](https://github.com/tale/headplane/discussions) section.
- **Documentation/Examples**: If you find any issues in the documentation or - **Documentation/Examples**: If you find any issues in the documentation or
would like to contribute examples for setting up Headplane, please open a PR would like to contribute examples for setting up Headplane, please open a PR
and I will review it and possibly make changes. and I will review it and possibly make changes.
- **Code Contributions**: Code contributions are done via PRs but *must* be - **Code Contributions**: Code contributions are done via PRs but _must_ be
linked to an issue or a feature request (you can make one if it doesn't exist). linked to an issue or a feature request (you can make one if it doesn't exist).
Please tag them with the appropriate labels when opening an issue or PR. Please tag them with the appropriate labels when opening an issue or PR.
### Code Contribution Restrictions ### Code Contribution Restrictions
- **No Large Refactors**: I am not interested in large refactors of the codebase - **No Large Refactors**: I am not interested in large refactors of the codebase
because I've already had do to this a lot and it becomes a headache to review, because I've already had do to this a lot and it becomes a headache to review,
maintain, and merge. This also means you should split up multiple contributions maintain, and merge. This also means you should split up multiple contributions
into multiple PRs if they are unrelated to each other. into multiple PRs if they are unrelated to each other.
- **No Project/Tooling Changes**: Unless there is a very good reason to do so, - **No Project/Tooling Changes**: Unless there is a very good reason to do so,
I will not accept changes to the project structure, build system, or tooling I will not accept changes to the project structure, build system, or tooling
used to develop Headplane. This includes things like changing the package used to develop Headplane. This includes things like changing the package
manager, docker environment, or CI/CD. manager, docker environment, or CI/CD.
- **Minimal Breaking Changes**: I will not accept any changes that break any - **Minimal Breaking Changes**: I will not accept any changes that break any
existing functionality or change the API unless there is a very good reason existing functionality or change the API unless there is a very good reason
to do so. If you want to make a breaking change, please open an issue first to do so. If you want to make a breaking change, please open an issue first
and discuss it with me. and discuss it with me.
### Code Style ### Code Style
This is very easy and self-explanatory. [Biome](https://biomejs.dev) is used as
a linter and formatter for the TypeScript, while Go's default formatting and This is very easy and self-explanatory. [oxlint/oxfmt](https://oxc.rs) is used
as a linter and formatter for the TypeScript, while Go's default formatting and
lint tooling are used within `agent/` code. I've setup a git hook to run lint tooling are used within `agent/` code. I've setup a git hook to run
before commit to make these changes automatically. before commit to make these changes automatically.
@@ -47,6 +50,7 @@ before commit to make these changes automatically.
> but instead we can discuss them and see if we can come to a compromise. > but instead we can discuss them and see if we can come to a compromise.
### Contribution Tooling ### Contribution Tooling
If you plan to work with the WASM SSH agent, you will need to install If you plan to work with the WASM SSH agent, you will need to install
`mkcert` or an equivalent to create certificates in `./test/caddy/certs`. `mkcert` or an equivalent to create certificates in `./test/caddy/certs`.
It expects a `localhost.pem` and `localhost-key.pem` file to be present in that It expects a `localhost.pem` and `localhost-key.pem` file to be present in that
+6 -2
View File
@@ -1,8 +1,12 @@
pre-commit: pre-commit:
commands: commands:
check: lint:
glob: "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}" glob: "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}"
run: pnpm biome check --write --no-errors-on-unmatched --files-ignore-unknown=true --colors=off {staged_files} run: pnpm lint --fix {staged_files}
stage_fixed: true
format:
glob: "*"
run: pnpm format {staged_files}
stage_fixed: true stage_fixed: true
check_go: check_go:
glob: "*.go" glob: "*.go"
+120 -117
View File
@@ -1,119 +1,122 @@
{ {
"name": "headplane", "name": "headplane",
"private": true, "version": "0.6.1",
"sideEffects": false, "private": true,
"version": "0.6.1", "type": "module",
"type": "module", "sideEffects": false,
"scripts": { "scripts": {
"preinstall": "npx only-allow pnpm", "preinstall": "npx only-allow pnpm",
"build": "react-router build", "build": "react-router build",
"dev": "HEADPLANE_CONFIG_PATH=./config.example.yaml react-router dev", "dev": "HEADPLANE_CONFIG_PATH=./config.example.yaml react-router dev",
"start": "node build/server/index.js", "start": "node build/server/index.js",
"typecheck": "react-router typegen && tsgo", "typecheck": "react-router typegen && tsgo",
"test:unit": "vitest run --project unit", "test:unit": "vitest run --project unit",
"test:integration": "vitest run --project integration", "test:integration": "vitest run --project integration",
"docs:dev": "vitepress dev docs", "docs:dev": "vitepress dev docs",
"docs:build": "vitepress build docs", "docs:build": "vitepress build docs",
"docs:preview": "vitepress preview docs", "docs:preview": "vitepress preview docs",
"gen:hashes": "tsx tests/generate-openapi-hashes.ts" "gen:hashes": "tsx tests/generate-openapi-hashes.ts",
}, "lint": "oxlint",
"dependencies": { "format": "oxfmt"
"@libsql/client": "0.15.15" },
}, "dependencies": {
"devDependencies": { "@libsql/client": "0.15.15"
"@biomejs/biome": "^2.3.5", },
"@dnd-kit/core": "^6.3.1", "devDependencies": {
"@dnd-kit/modifiers": "^7.0.0", "@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^8.0.0", "@dnd-kit/modifiers": "^7.0.0",
"@dnd-kit/utilities": "^3.2.2", "@dnd-kit/sortable": "^8.0.0",
"@faker-js/faker": "9.9.0", "@dnd-kit/utilities": "^3.2.2",
"@fontsource-variable/inter": "^5.2.8", "@faker-js/faker": "9.9.0",
"@iconify/react": "^6.0.2", "@fontsource-variable/inter": "^5.2.8",
"@kubernetes/client-node": "^1.4.0", "@iconify/react": "^6.0.2",
"@react-aria/toast": "3.0.8", "@kubernetes/client-node": "^1.4.0",
"@react-router/dev": "^7.9.6", "@react-aria/toast": "3.0.8",
"@react-router/node": "^7.9.6", "@react-router/dev": "^7.9.6",
"@react-stately/toast": "3.1.2", "@react-router/node": "^7.9.6",
"@readme/openapi-parser": "^5.2.1", "@react-stately/toast": "3.1.2",
"@shopify/lang-jsonc": "^1.0.1", "@readme/openapi-parser": "^5.2.1",
"@tailwindcss/vite": "^4.1.17", "@shopify/lang-jsonc": "^1.0.1",
"@types/js-yaml": "^4.0.9", "@tailwindcss/vite": "^4.1.17",
"@types/node": "^24.10.1", "@types/js-yaml": "^4.0.9",
"@types/react": "^19.2.5", "@types/node": "^24.10.1",
"@types/react-dom": "^19.2.3", "@types/react": "^19.2.5",
"@typescript/native-preview": "7.0.0-dev.20251203.1", "@types/react-dom": "^19.2.3",
"@uiw/codemirror-theme-github": "4.25.1", "@typescript/native-preview": "7.0.0-dev.20251203.1",
"@uiw/codemirror-theme-xcode": "4.25.3", "@uiw/codemirror-theme-github": "4.25.1",
"@uiw/react-codemirror": "4.25.3", "@uiw/codemirror-theme-xcode": "4.25.3",
"@xterm/addon-clipboard": "^0.1.0", "@uiw/react-codemirror": "4.25.3",
"@xterm/addon-fit": "^0.10.0", "@xterm/addon-clipboard": "^0.1.0",
"@xterm/addon-unicode11": "^0.8.0", "@xterm/addon-fit": "^0.10.0",
"@xterm/addon-web-links": "^0.11.0", "@xterm/addon-unicode11": "^0.8.0",
"@xterm/xterm": "^5.5.0", "@xterm/addon-web-links": "^0.11.0",
"arktype": "^2.1.26", "@xterm/xterm": "^5.5.0",
"clsx": "^2.1.1", "arktype": "^2.1.26",
"drizzle-kit": "^0.31.7", "clsx": "^2.1.1",
"drizzle-orm": "0.44.7", "drizzle-kit": "^0.31.7",
"isbot": "5.1.32", "drizzle-orm": "0.44.7",
"jose": "6.1.2", "isbot": "5.1.32",
"js-yaml": "^4.1.1", "jose": "6.1.2",
"lefthook": "^2.0.4", "js-yaml": "^4.1.1",
"lucide-react": "^0.540.0", "lefthook": "^2.0.4",
"mime": "^4.1.0", "lucide-react": "^0.540.0",
"openapi-types": "^12.1.3", "mime": "^4.1.0",
"openid-client": "6.8.1", "openapi-types": "^12.1.3",
"postcss": "^8.5.6", "openid-client": "6.8.1",
"react": "19.2.0", "oxfmt": "^0.24.0",
"react-aria": "3.44.0", "oxlint": "^1.39.0",
"react-codemirror-merge": "4.25.3", "oxlint-tsgolint": "^0.11.1",
"react-dom": "19.2.0", "postcss": "^8.5.6",
"react-error-boundary": "^6.0.0", "react": "19.2.0",
"react-router": "^7.9.6", "react-aria": "3.44.0",
"react-router-dom": "^7.9.6", "react-codemirror-merge": "4.25.3",
"react-router-hono-server": "2.22.0", "react-dom": "19.2.0",
"react-stately": "3.42.0", "react-error-boundary": "^6.0.0",
"remix-utils": "^9.0.0", "react-router": "^7.9.6",
"tailwind-merge": "3.4.0", "react-router-dom": "^7.9.6",
"tailwindcss": "^4.1.17", "react-router-hono-server": "2.22.0",
"tailwindcss-animate": "^1.0.7", "react-stately": "3.42.0",
"tailwindcss-react-aria-components": "^2.0.1", "remix-utils": "^9.0.0",
"testcontainers": "^11.8.1", "tailwind-merge": "3.4.0",
"tsx": "^4.20.6", "tailwindcss": "^4.1.17",
"typescript": "^5.9.3", "tailwindcss-animate": "^1.0.7",
"ulidx": "2.4.1", "tailwindcss-react-aria-components": "^2.0.1",
"undici": "7.16.0", "testcontainers": "^11.8.1",
"usehooks-ts": "^3.1.1", "tsx": "^4.20.6",
"vite": "8.0.0-beta.0", "typescript": "^5.9.3",
"vite-tsconfig-paths": "^5.1.4", "ulidx": "2.4.1",
"vitepress": "next", "undici": "7.16.0",
"vitest": "^4.0.9", "usehooks-ts": "^3.1.1",
"yaml": "2.8.1" "vite": "8.0.0-beta.0",
}, "vite-tsconfig-paths": "^5.1.4",
"packageManager": "pnpm@10.4.0", "vitepress": "next",
"engines": { "vitest": "^4.0.9",
"node": ">=22.18 <23", "yaml": "2.8.1"
"pnpm": ">=10.4 <11" },
}, "engines": {
"pnpm": { "node": ">=22.18 <23",
"supportedArchitectures": { "pnpm": ">=10.4 <11"
"os": [ },
"current", "packageManager": "pnpm@10.4.0",
"linux" "pnpm": {
], "supportedArchitectures": {
"cpu": [ "os": [
"x64", "current",
"arm64" "linux"
] ],
}, "cpu": [
"onlyBuiltDependencies": [ "x64",
"@biomejs/biome", "arm64"
"@tailwindcss/oxide", ]
"better-sqlite3", },
"esbuild", "onlyBuiltDependencies": [
"lefthook" "@tailwindcss/oxide",
], "better-sqlite3",
"patchedDependencies": { "esbuild",
"react-router-hono-server": "patches/react-router-hono-server.patch" "lefthook"
} ],
} "patchedDependencies": {
"react-router-hono-server": "patches/react-router-hono-server.patch"
}
}
} }
+244 -91
View File
@@ -17,9 +17,6 @@ importers:
specifier: 0.15.15 specifier: 0.15.15
version: 0.15.15(bufferutil@4.0.9)(utf-8-validate@5.0.10) version: 0.15.15(bufferutil@4.0.9)(utf-8-validate@5.0.10)
devDependencies: devDependencies:
'@biomejs/biome':
specifier: ^2.3.5
version: 2.3.5
'@dnd-kit/core': '@dnd-kit/core':
specifier: ^6.3.1 specifier: ^6.3.1
version: 6.3.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) version: 6.3.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
@@ -140,6 +137,15 @@ importers:
openid-client: openid-client:
specifier: 6.8.1 specifier: 6.8.1
version: 6.8.1 version: 6.8.1
oxfmt:
specifier: ^0.24.0
version: 0.24.0
oxlint:
specifier: ^1.39.0
version: 1.39.0(oxlint-tsgolint@0.11.1)
oxlint-tsgolint:
specifier: ^0.11.1
version: 0.11.1
postcss: postcss:
specifier: ^8.5.6 specifier: ^8.5.6
version: 8.5.6 version: 8.5.6
@@ -389,59 +395,6 @@ packages:
'@balena/dockerignore@1.0.2': '@balena/dockerignore@1.0.2':
resolution: {integrity: sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==} resolution: {integrity: sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==}
'@biomejs/biome@2.3.5':
resolution: {integrity: sha512-HvLhNlIlBIbAV77VysRIBEwp55oM/QAjQEin74QQX9Xb259/XP/D5AGGnZMOyF1el4zcvlNYYR3AyTMUV3ILhg==}
engines: {node: '>=14.21.3'}
hasBin: true
'@biomejs/cli-darwin-arm64@2.3.5':
resolution: {integrity: sha512-fLdTur8cJU33HxHUUsii3GLx/TR0BsfQx8FkeqIiW33cGMtUD56fAtrh+2Fx1uhiCsVZlFh6iLKUU3pniZREQw==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [darwin]
'@biomejs/cli-darwin-x64@2.3.5':
resolution: {integrity: sha512-qpT8XDqeUlzrOW8zb4k3tjhT7rmvVRumhi2657I2aGcY4B+Ft5fNwDdZGACzn8zj7/K1fdWjgwYE3i2mSZ+vOA==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [darwin]
'@biomejs/cli-linux-arm64-musl@2.3.5':
resolution: {integrity: sha512-eGUG7+hcLgGnMNl1KHVZUYxahYAhC462jF/wQolqu4qso2MSk32Q+QrpN7eN4jAHAg7FUMIo897muIhK4hXhqg==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [linux]
'@biomejs/cli-linux-arm64@2.3.5':
resolution: {integrity: sha512-u/pybjTBPGBHB66ku4pK1gj+Dxgx7/+Z0jAriZISPX1ocTO8aHh8x8e7Kb1rB4Ms0nA/SzjtNOVJ4exVavQBCw==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [linux]
'@biomejs/cli-linux-x64-musl@2.3.5':
resolution: {integrity: sha512-awVuycTPpVTH/+WDVnEEYSf6nbCBHf/4wB3lquwT7puhNg8R4XvonWNZzUsfHZrCkjkLhFH/vCZK5jHatD9FEg==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [linux]
'@biomejs/cli-linux-x64@2.3.5':
resolution: {integrity: sha512-XrIVi9YAW6ye0CGQ+yax0gLfx+BFOtKaNX74n+xHWla6Cl6huUmcKNO7HPx7BiKnJUzrxXY1qYlm7xMvi08X4g==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [linux]
'@biomejs/cli-win32-arm64@2.3.5':
resolution: {integrity: sha512-DlBiMlBZZ9eIq4H7RimDSGsYcOtfOIfZOaI5CqsWiSlbTfqbPVfWtCf92wNzx8GNMbu1s7/g3ZZESr6+GwM/SA==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [win32]
'@biomejs/cli-win32-x64@2.3.5':
resolution: {integrity: sha512-nUmR8gb6yvrKhtRgzwo/gDimPwnO5a4sCydf8ZS2kHIJhEmSmk+STsusr1LHTuM//wXppBawvSQi2xFXJCdgKQ==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [win32]
'@codemirror/autocomplete@6.18.2': '@codemirror/autocomplete@6.18.2':
resolution: {integrity: sha512-wJGylKtMFR/Ds6Gh01+OovXE/pncPiKZNNBKuC39pKnH+XK5d9+WsNqcrdxPjFPFTigRBqse0rfxw9UxrfyhPg==} resolution: {integrity: sha512-wJGylKtMFR/Ds6Gh01+OovXE/pncPiKZNNBKuC39pKnH+XK5d9+WsNqcrdxPjFPFTigRBqse0rfxw9UxrfyhPg==}
peerDependencies: peerDependencies:
@@ -1230,6 +1183,116 @@ packages:
'@oxc-project/types@0.101.0': '@oxc-project/types@0.101.0':
resolution: {integrity: sha512-nuFhqlUzJX+gVIPPfuE6xurd4lST3mdcWOhyK/rZO0B9XWMKm79SuszIQEnSMmmDhq1DC8WWVYGVd+6F93o1gQ==} resolution: {integrity: sha512-nuFhqlUzJX+gVIPPfuE6xurd4lST3mdcWOhyK/rZO0B9XWMKm79SuszIQEnSMmmDhq1DC8WWVYGVd+6F93o1gQ==}
'@oxfmt/darwin-arm64@0.24.0':
resolution: {integrity: sha512-aYXuGf/yq8nsyEcHindGhiz9I+GEqLkVq8CfPbd+6VE259CpPEH+CaGHEO1j6vIOmNr8KHRq+IAjeRO2uJpb8A==}
cpu: [arm64]
os: [darwin]
'@oxfmt/darwin-x64@0.24.0':
resolution: {integrity: sha512-vs3b8Bs53hbiNvcNeBilzE/+IhDTWKjSBB3v/ztr664nZk65j0xr+5IHMBNz3CFppmX7o/aBta2PxY+t+4KoPg==}
cpu: [x64]
os: [darwin]
'@oxfmt/linux-arm64-gnu@0.24.0':
resolution: {integrity: sha512-ItPDOPoQ0wLj/s8osc5ch57uUcA1Wk8r0YdO8vLRpXA3UNg7KPOm1vdbkIZRRiSUphZcuX5ioOEetEK8H7RlTw==}
cpu: [arm64]
os: [linux]
'@oxfmt/linux-arm64-musl@0.24.0':
resolution: {integrity: sha512-JkQO3WnQjQTJONx8nxdgVBfl6BBFfpp9bKhChYhWeakwJdr7QPOAWJ/v3FGZfr0TbqINwnNR74aVZayDDRyXEA==}
cpu: [arm64]
os: [linux]
'@oxfmt/linux-x64-gnu@0.24.0':
resolution: {integrity: sha512-N/SXlFO+2kak5gMt0oxApi0WXQDhwA0PShR0UbkY0PwtHjfSiDqJSOumyNqgQVoroKr1GNnoRmUqjZIz6DKIcw==}
cpu: [x64]
os: [linux]
'@oxfmt/linux-x64-musl@0.24.0':
resolution: {integrity: sha512-WM0pek5YDCQf50XQ7GLCE9sMBCMPW/NPAEPH/Hx6Qyir37lEsP4rUmSECo/QFNTU6KBc9NnsviAyJruWPpCMXw==}
cpu: [x64]
os: [linux]
'@oxfmt/win32-arm64@0.24.0':
resolution: {integrity: sha512-vFCseli1KWtwdHrVlT/jWfZ8jP8oYpnPPEjI23mPLW8K/6GEJmmvy0PZP5NpWUFNTzX0lqie58XnrATJYAe9Xw==}
cpu: [arm64]
os: [win32]
'@oxfmt/win32-x64@0.24.0':
resolution: {integrity: sha512-0tmlNzcyewAnauNeBCq0xmAkmiKzl+H09p0IdHy+QKrTQdtixtf+AOjDAADbRfihkS+heF15Pjc4IyJMdAAJjw==}
cpu: [x64]
os: [win32]
'@oxlint-tsgolint/darwin-arm64@0.11.1':
resolution: {integrity: sha512-UJIOFeJZpFTJIGS+bMdFXcvjslvnXBEouMvzynfQD7RTazcFIRLbokYgEbhrN2P6B352Ut1TUtvR0CLAp/9QfA==}
cpu: [arm64]
os: [darwin]
'@oxlint-tsgolint/darwin-x64@0.11.1':
resolution: {integrity: sha512-68O8YvexIm+ISZKl2vBFII1dMfLrteDyPcuCIecDuiBIj2tV0KYq13zpSCMz4dvJUWJW6RmOOGZKrkkvOAy6uQ==}
cpu: [x64]
os: [darwin]
'@oxlint-tsgolint/linux-arm64@0.11.1':
resolution: {integrity: sha512-hXBInrFxPNbPPbPQYozo8YpSsFFYdtHBWRUiLMxul71vTy1CdSA7H5Qq2KbrKomr/ASmhvIDVAQZxh9hIJNHMA==}
cpu: [arm64]
os: [linux]
'@oxlint-tsgolint/linux-x64@0.11.1':
resolution: {integrity: sha512-aMaGctlwrJhaIQPOdVJR+AGHZGPm4D1pJ457l0SqZt4dLXAhuUt2ene6cUUGF+864R7bDyFVGZqbZHODYpENyA==}
cpu: [x64]
os: [linux]
'@oxlint-tsgolint/win32-arm64@0.11.1':
resolution: {integrity: sha512-ipOs6kKo8fz5n5LSHvcbyZFmEpEIsh2m7+B03RW3jGjBEPMiXb4PfKNuxnusFYTtJM9WaR3bCVm5UxeJTA8r3w==}
cpu: [arm64]
os: [win32]
'@oxlint-tsgolint/win32-x64@0.11.1':
resolution: {integrity: sha512-m2apsAXg6qU3ulQG45W/qshyEpOjoL+uaQyXJG5dBoDoa66XPtCaSkBlKltD0EwGu0aoB8lM4I5I3OzQ6raNhw==}
cpu: [x64]
os: [win32]
'@oxlint/darwin-arm64@1.39.0':
resolution: {integrity: sha512-lT3hNhIa02xCujI6YGgjmYGg3Ht/X9ag5ipUVETaMpx5Rd4BbTNWUPif1WN1YZHxt3KLCIqaAe7zVhatv83HOQ==}
cpu: [arm64]
os: [darwin]
'@oxlint/darwin-x64@1.39.0':
resolution: {integrity: sha512-UT+rfTWd+Yr7iJeSLd/7nF8X4gTYssKh+n77hxl6Oilp3NnG1CKRHxZDy3o3lIBnwgzJkdyUAiYWO1bTMXQ1lA==}
cpu: [x64]
os: [darwin]
'@oxlint/linux-arm64-gnu@1.39.0':
resolution: {integrity: sha512-qocBkvS2V6rH0t9AT3DfQunMnj3xkM7srs5/Ycj2j5ZqMoaWd/FxHNVJDFP++35roKSvsRJoS0mtA8/77jqm6Q==}
cpu: [arm64]
os: [linux]
'@oxlint/linux-arm64-musl@1.39.0':
resolution: {integrity: sha512-arZzAc1PPcz9epvGBBCMHICeyQloKtHX3eoOe62B3Dskn7gf6Q14wnDHr1r9Vp4vtcBATNq6HlKV14smdlC/qA==}
cpu: [arm64]
os: [linux]
'@oxlint/linux-x64-gnu@1.39.0':
resolution: {integrity: sha512-ZVt5qsECpuNprdWxAPpDBwoixr1VTcZ4qAEQA2l/wmFyVPDYFD3oBY/SWACNnWBddMrswjTg9O8ALxYWoEpmXw==}
cpu: [x64]
os: [linux]
'@oxlint/linux-x64-musl@1.39.0':
resolution: {integrity: sha512-pB0hlGyKPbxr9NMIV783lD6cWL3MpaqnZRM9MWni4yBdHPTKyFNYdg5hGD0Bwg+UP4S2rOevq/+OO9x9Bi7E6g==}
cpu: [x64]
os: [linux]
'@oxlint/win32-arm64@1.39.0':
resolution: {integrity: sha512-Gg2SFaJohI9+tIQVKXlPw3FsPQFi/eCSWiCgwPtPn5uzQxHRTeQEZKuluz1fuzR5U70TXubb2liZi4Dgl8LJQA==}
cpu: [arm64]
os: [win32]
'@oxlint/win32-x64@1.39.0':
resolution: {integrity: sha512-sbi25lfj74hH+6qQtb7s1wEvd1j8OQbTaH8v3xTcDjrwm579Cyh0HBv1YSZ2+gsnVwfVDiCTL1D0JsNqYXszVA==}
cpu: [x64]
os: [win32]
'@pkgjs/parseargs@0.11.0': '@pkgjs/parseargs@0.11.0':
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'} engines: {node: '>=14'}
@@ -3601,6 +3664,25 @@ packages:
openid-client@6.8.1: openid-client@6.8.1:
resolution: {integrity: sha512-VoYT6enBo6Vj2j3Q5Ec0AezS+9YGzQo1f5Xc42lreMGlfP4ljiXPKVDvCADh+XHCV/bqPu/wWSiCVXbJKvrODw==} resolution: {integrity: sha512-VoYT6enBo6Vj2j3Q5Ec0AezS+9YGzQo1f5Xc42lreMGlfP4ljiXPKVDvCADh+XHCV/bqPu/wWSiCVXbJKvrODw==}
oxfmt@0.24.0:
resolution: {integrity: sha512-UjeM3Peez8Tl7IJ9s5UwAoZSiDRMww7BEc21gDYxLq3S3/KqJnM3mjNxsoSHgmBvSeX6RBhoVc2MfC/+96RdSw==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
oxlint-tsgolint@0.11.1:
resolution: {integrity: sha512-WulCp+0/6RvpM4zPv+dAXybf03QvRA8ATxaBlmj4XMIQqTs5jeq3cUTk48WCt4CpLwKhyyGZPHmjLl1KHQ/cvA==}
hasBin: true
oxlint@1.39.0:
resolution: {integrity: sha512-wSiLr0wjG+KTU6c1LpVoQk7JZ7l8HCKlAkVDVTJKWmCGazsNxexxnOXl7dsar92mQcRnzko5g077ggP3RINSjA==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
peerDependencies:
oxlint-tsgolint: '>=0.10.0'
peerDependenciesMeta:
oxlint-tsgolint:
optional: true
p-map@7.0.4: p-map@7.0.4:
resolution: {integrity: sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==} resolution: {integrity: sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -4077,6 +4159,10 @@ packages:
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
engines: {node: '>=12.0.0'} engines: {node: '>=12.0.0'}
tinypool@2.0.0:
resolution: {integrity: sha512-/RX9RzeH2xU5ADE7n2Ykvmi9ED3FBGPAjw9u3zucrNNaEBIO0HPSYgL0NT7+3p147ojeSdaVu08F6hjpv31HJg==}
engines: {node: ^20.0.0 || >=22.0.0}
tinyrainbow@3.0.3: tinyrainbow@3.0.3:
resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
@@ -4662,41 +4748,6 @@ snapshots:
'@balena/dockerignore@1.0.2': {} '@balena/dockerignore@1.0.2': {}
'@biomejs/biome@2.3.5':
optionalDependencies:
'@biomejs/cli-darwin-arm64': 2.3.5
'@biomejs/cli-darwin-x64': 2.3.5
'@biomejs/cli-linux-arm64': 2.3.5
'@biomejs/cli-linux-arm64-musl': 2.3.5
'@biomejs/cli-linux-x64': 2.3.5
'@biomejs/cli-linux-x64-musl': 2.3.5
'@biomejs/cli-win32-arm64': 2.3.5
'@biomejs/cli-win32-x64': 2.3.5
'@biomejs/cli-darwin-arm64@2.3.5':
optional: true
'@biomejs/cli-darwin-x64@2.3.5':
optional: true
'@biomejs/cli-linux-arm64-musl@2.3.5':
optional: true
'@biomejs/cli-linux-arm64@2.3.5':
optional: true
'@biomejs/cli-linux-x64-musl@2.3.5':
optional: true
'@biomejs/cli-linux-x64@2.3.5':
optional: true
'@biomejs/cli-win32-arm64@2.3.5':
optional: true
'@biomejs/cli-win32-x64@2.3.5':
optional: true
'@codemirror/autocomplete@6.18.2(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.38.7)(@lezer/common@1.3.0)': '@codemirror/autocomplete@6.18.2(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.38.7)(@lezer/common@1.3.0)':
dependencies: dependencies:
'@codemirror/language': 6.11.3 '@codemirror/language': 6.11.3
@@ -5363,6 +5414,72 @@ snapshots:
'@oxc-project/types@0.101.0': {} '@oxc-project/types@0.101.0': {}
'@oxfmt/darwin-arm64@0.24.0':
optional: true
'@oxfmt/darwin-x64@0.24.0':
optional: true
'@oxfmt/linux-arm64-gnu@0.24.0':
optional: true
'@oxfmt/linux-arm64-musl@0.24.0':
optional: true
'@oxfmt/linux-x64-gnu@0.24.0':
optional: true
'@oxfmt/linux-x64-musl@0.24.0':
optional: true
'@oxfmt/win32-arm64@0.24.0':
optional: true
'@oxfmt/win32-x64@0.24.0':
optional: true
'@oxlint-tsgolint/darwin-arm64@0.11.1':
optional: true
'@oxlint-tsgolint/darwin-x64@0.11.1':
optional: true
'@oxlint-tsgolint/linux-arm64@0.11.1':
optional: true
'@oxlint-tsgolint/linux-x64@0.11.1':
optional: true
'@oxlint-tsgolint/win32-arm64@0.11.1':
optional: true
'@oxlint-tsgolint/win32-x64@0.11.1':
optional: true
'@oxlint/darwin-arm64@1.39.0':
optional: true
'@oxlint/darwin-x64@1.39.0':
optional: true
'@oxlint/linux-arm64-gnu@1.39.0':
optional: true
'@oxlint/linux-arm64-musl@1.39.0':
optional: true
'@oxlint/linux-x64-gnu@1.39.0':
optional: true
'@oxlint/linux-x64-musl@1.39.0':
optional: true
'@oxlint/win32-arm64@1.39.0':
optional: true
'@oxlint/win32-x64@1.39.0':
optional: true
'@pkgjs/parseargs@0.11.0': '@pkgjs/parseargs@0.11.0':
optional: true optional: true
@@ -8043,6 +8160,40 @@ snapshots:
jose: 6.1.2 jose: 6.1.2
oauth4webapi: 3.8.2 oauth4webapi: 3.8.2
oxfmt@0.24.0:
dependencies:
tinypool: 2.0.0
optionalDependencies:
'@oxfmt/darwin-arm64': 0.24.0
'@oxfmt/darwin-x64': 0.24.0
'@oxfmt/linux-arm64-gnu': 0.24.0
'@oxfmt/linux-arm64-musl': 0.24.0
'@oxfmt/linux-x64-gnu': 0.24.0
'@oxfmt/linux-x64-musl': 0.24.0
'@oxfmt/win32-arm64': 0.24.0
'@oxfmt/win32-x64': 0.24.0
oxlint-tsgolint@0.11.1:
optionalDependencies:
'@oxlint-tsgolint/darwin-arm64': 0.11.1
'@oxlint-tsgolint/darwin-x64': 0.11.1
'@oxlint-tsgolint/linux-arm64': 0.11.1
'@oxlint-tsgolint/linux-x64': 0.11.1
'@oxlint-tsgolint/win32-arm64': 0.11.1
'@oxlint-tsgolint/win32-x64': 0.11.1
oxlint@1.39.0(oxlint-tsgolint@0.11.1):
optionalDependencies:
'@oxlint/darwin-arm64': 1.39.0
'@oxlint/darwin-x64': 1.39.0
'@oxlint/linux-arm64-gnu': 1.39.0
'@oxlint/linux-arm64-musl': 1.39.0
'@oxlint/linux-x64-gnu': 1.39.0
'@oxlint/linux-x64-musl': 1.39.0
'@oxlint/win32-arm64': 1.39.0
'@oxlint/win32-x64': 1.39.0
oxlint-tsgolint: 0.11.1
p-map@7.0.4: {} p-map@7.0.4: {}
package-json-from-dist@1.0.1: {} package-json-from-dist@1.0.1: {}
@@ -8653,6 +8804,8 @@ snapshots:
fdir: 6.5.0(picomatch@4.0.3) fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3 picomatch: 4.0.3
tinypool@2.0.0: {}
tinyrainbow@3.0.3: {} tinyrainbow@3.0.3: {}
tmp@0.2.5: {} tmp@0.2.5: {}
+69 -71
View File
@@ -1,83 +1,81 @@
import { describe, expect, test } from 'vitest'; import { describe, expect, test } from "vitest";
import type { PartialHeadplaneConfigWithPaths } from '~/server/config/config-schema';
import { ConfigError } from '~/server/config/error';
import { loadConfigKeyPaths } from '~/server/config/load';
import { createFakeFile } from '../setup/overlay-fs';
describe('Configuration secret path handling', () => { import type { PartialHeadplaneConfigWithPaths } from "~/server/config/config-schema";
test('should correctly substitute server.cookie_secret', async () => {
createFakeFile('/secrets/cookie_secret.txt', 'supersecretcookievalue');
const config = {
server: {
cookie_secret_path: '/secrets/cookie_secret.txt',
},
} as PartialHeadplaneConfigWithPaths;
await loadConfigKeyPaths(config); import { ConfigError } from "~/server/config/error";
expect(config.server?.cookie_secret).toBe('supersecretcookievalue'); import { loadConfigKeyPaths } from "~/server/config/load";
});
test('should throw error for missing secret file', async () => { import { createFakeFile } from "../setup/overlay-fs";
const config = {
server: {
cookie_secret_path: '/secrets/missing_cookie_secret.txt',
},
} as PartialHeadplaneConfigWithPaths;
await expect(loadConfigKeyPaths(config)).rejects.toMatchObject( describe("Configuration secret path handling", () => {
ConfigError.from('MISSING_SECRET_FILE', { test("should correctly substitute server.cookie_secret", async () => {
pathKey: 'server.cookie_secret_path', createFakeFile("/secrets/cookie_secret.txt", "supersecretcookievalue");
filePath: '/secrets/missing_cookie_secret.txt', const config = {
}), server: {
); cookie_secret_path: "/secrets/cookie_secret.txt",
}); },
} as PartialHeadplaneConfigWithPaths;
test('should throw error for conflicting secret path and field', async () => { await loadConfigKeyPaths(config);
const config = { expect(config.server?.cookie_secret).toBe("supersecretcookievalue");
server: { });
cookie_secret: 'explicitsecretvalue',
cookie_secret_path: '/secrets/cookie_secret.txt',
},
} as PartialHeadplaneConfigWithPaths;
await expect(loadConfigKeyPaths(config)).rejects.toMatchObject( test("should throw error for missing secret file", async () => {
ConfigError.from('CONFLICTING_SECRET_PATH_FIELD', { const config = {
fieldName: 'server.cookie_secret', server: {
}), cookie_secret_path: "/secrets/missing_cookie_secret.txt",
); },
}); } as PartialHeadplaneConfigWithPaths;
test('should correctly interpolate env vars in secret paths', async () => { await expect(loadConfigKeyPaths(config)).rejects.toMatchObject(
process.env.HP_TEST_COOKIE_SECRET_FILE = 'cookie_secret.txt'; ConfigError.from("MISSING_SECRET_FILE", {
createFakeFile( pathKey: "server.cookie_secret_path",
`/secrets/${process.env.HP_TEST_COOKIE_SECRET_FILE}`, filePath: "/secrets/missing_cookie_secret.txt",
'envvarsecretvalue', }),
); );
});
const config = { test("should throw error for conflicting secret path and field", async () => {
server: { const config = {
// biome-ignore lint/suspicious/noTemplateCurlyInString: Test supports interpolation server: {
cookie_secret_path: '/secrets/${HP_TEST_COOKIE_SECRET_FILE}', cookie_secret: "explicitsecretvalue",
}, cookie_secret_path: "/secrets/cookie_secret.txt",
} as PartialHeadplaneConfigWithPaths; },
} as PartialHeadplaneConfigWithPaths;
await loadConfigKeyPaths(config); await expect(loadConfigKeyPaths(config)).rejects.toMatchObject(
expect(config.server?.cookie_secret).toBe('envvarsecretvalue'); ConfigError.from("CONFLICTING_SECRET_PATH_FIELD", {
}); fieldName: "server.cookie_secret",
}),
);
});
test('should throw error for missing interpolated env var in secret path', async () => { test("should correctly interpolate env vars in secret paths", async () => {
const config = { process.env.HP_TEST_COOKIE_SECRET_FILE = "cookie_secret.txt";
server: { createFakeFile(`/secrets/${process.env.HP_TEST_COOKIE_SECRET_FILE}`, "envvarsecretvalue");
// biome-ignore lint/suspicious/noTemplateCurlyInString: Test supports interpolation
cookie_secret_path: '/secrets/${MISSING_ENV_VAR}',
},
} as PartialHeadplaneConfigWithPaths;
await expect(loadConfigKeyPaths(config)).rejects.toMatchObject( const config = {
ConfigError.from('MISSING_INTERPOLATION_VARIABLE', { server: {
pathKey: 'server.cookie_secret_path', cookie_secret_path: "/secrets/${HP_TEST_COOKIE_SECRET_FILE}",
variableName: 'MISSING_ENV_VAR', },
}), } as PartialHeadplaneConfigWithPaths;
);
}); await loadConfigKeyPaths(config);
expect(config.server?.cookie_secret).toBe("envvarsecretvalue");
});
test("should throw error for missing interpolated env var in secret path", async () => {
const config = {
server: {
cookie_secret_path: "/secrets/${MISSING_ENV_VAR}",
},
} as PartialHeadplaneConfigWithPaths;
await expect(loadConfigKeyPaths(config)).rejects.toMatchObject(
ConfigError.from("MISSING_INTERPOLATION_VARIABLE", {
pathKey: "server.cookie_secret_path",
variableName: "MISSING_ENV_VAR",
}),
);
});
}); });