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 { createReadableStreamFromReadable } from '@react-router/node';
import { isbot } from 'isbot';
import type { RenderToPipeableStreamOptions } from 'react-dom/server';
import { renderToPipeableStream } from 'react-dom/server';
import type { AppLoadContext, EntryContext } from 'react-router';
import { ServerRouter } from 'react-router';
import type { RenderToPipeableStreamOptions } from "react-dom/server";
import type { AppLoadContext, EntryContext } from "react-router";
import { createReadableStreamFromReadable } from "@react-router/node";
import { isbot } from "isbot";
import { PassThrough } from "node:stream";
import { renderToPipeableStream } from "react-dom/server";
import { ServerRouter } from "react-router";
export const streamTimeout = 5_000;
export default function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
routerContext: EntryContext,
loadContext: AppLoadContext,
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
routerContext: EntryContext,
_loadContext: AppLoadContext,
) {
return new Promise((resolve, reject) => {
let shellRendered = false;
const userAgent = request.headers.get('user-agent');
return new Promise((resolve, reject) => {
let shellRendered = false;
const userAgent = request.headers.get("user-agent");
// 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
const readyOption: keyof RenderToPipeableStreamOptions =
(userAgent && isbot(userAgent)) || routerContext.isSpaMode
? 'onAllReady'
: 'onShellReady';
// 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
const readyOption: keyof RenderToPipeableStreamOptions =
(userAgent && isbot(userAgent)) || routerContext.isSpaMode ? "onAllReady" : "onShellReady";
const { pipe, abort } = renderToPipeableStream(
<ServerRouter context={routerContext} url={request.url} />,
{
[readyOption]() {
shellRendered = true;
const body = new PassThrough();
const stream = createReadableStreamFromReadable(body);
const { pipe, abort } = renderToPipeableStream(
<ServerRouter context={routerContext} url={request.url} />,
{
[readyOption]() {
shellRendered = true;
const body = new PassThrough();
const stream = createReadableStreamFromReadable(body);
responseHeaders.set('Content-Type', 'text/html');
responseHeaders.set("Content-Type", "text/html");
resolve(
new Response(stream, {
headers: responseHeaders,
status: responseStatusCode,
}),
);
resolve(
new Response(stream, {
headers: responseHeaders,
status: responseStatusCode,
}),
);
pipe(body);
},
onShellError(error: unknown) {
reject(error);
},
onError(error: unknown) {
// biome-ignore lint/style/noParameterAssign: Lazy
responseStatusCode = 500;
// Log streaming rendering errors from inside the shell. Don't log
// errors encountered during initial shell rendering since they'll
// reject and get logged in handleDocumentRequest.
if (shellRendered) {
console.error(error);
}
},
},
);
pipe(body);
},
onShellError(error: unknown) {
reject(error);
},
onError(error: unknown) {
responseStatusCode = 500;
// Log streaming rendering errors from inside the shell. Don't log
// errors encountered during initial shell rendering since they'll
// reject and get logged in handleDocumentRequest.
if (shellRendered) {
console.error(error);
}
},
},
);
// Abort the rendering stream after the `streamTimeout` so it has tine to
// flush down the rejected boundaries
setTimeout(abort, streamTimeout + 1000);
});
// Abort the rendering stream after the `streamTimeout` so it has tine to
// flush down the rejected boundaries
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 { 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 { EphemeralNodeInsert, ephemeralNodes } from '~/server/db/schema';
import { useLiveData } from '~/utils/live-data';
import type { Route } from './+types/console';
import UserPrompt from './user-prompt';
import XTerm from './xterm.client';
import { EphemeralNodeInsert, ephemeralNodes } from "~/server/db/schema";
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 = () => {
return false;
return false;
};
export async function loader({ request, context }: Route.LoaderArgs) {
const origin = new URL(request.url).origin;
const assets = ['/wasm_exec.js', '/hp_ssh.wasm'];
const missing: string[] = [];
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);
}
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 (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',
400,
);
}
if (!context.agents?.agentID()) {
throw data("WebSSH is only available with the Headplane agent integration", 400);
}
const session = await context.sessions.auth(request);
if (session.user.subject === 'unknown-non-oauth') {
throw data('Only OAuth users are allowed to use WebSSH', 403);
}
const session = await context.sessions.auth(request);
if (session.user.subject === "unknown-non-oauth") {
throw data("Only OAuth users are allowed to use WebSSH", 403);
}
const api = context.hsApi.getRuntimeClient(session.api_key);
const users = await api.getUsers();
const api = context.hsApi.getRuntimeClient(session.api_key);
const users = await api.getUsers();
// MARK: This assumes that a user has authenticated with Headscale first
// 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
// authenticated with Headscale first.
const lookup = users.find((u) => {
const subject = u.providerId?.split('/').pop();
if (!subject) {
return false;
}
return subject === session.user.subject;
});
// MARK: This assumes that a user has authenticated with Headscale first
// 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
// authenticated with Headscale first.
const lookup = users.find((u) => {
const subject = u.providerId?.split("/").pop();
if (!subject) {
return false;
}
return subject === session.user.subject;
});
if (!lookup) {
throw data(
`User with subject ${session.user.subject} not found within Headscale`,
404,
);
}
if (!lookup) {
throw data(`User with subject ${session.user.subject} not found within Headscale`, 404);
}
const preAuthKey = await api.createPreAuthKey(
lookup.id,
true, // ephemeral
false, // reusable
new Date(Date.now() + 60 * 1000), // expiration: 1 minute
null, // aclTags
);
const preAuthKey = await api.createPreAuthKey(
lookup.id,
true, // ephemeral
false, // reusable
new Date(Date.now() + 60 * 1000), // expiration: 1 minute
null, // aclTags
);
// TODO: Enable config to enforce generate_authkeys capability
// For now, any user is capable of WebSSH connections
// const check = await context.sessions.check(
// request,
// Capabilities.generate_authkeys,
// );
// TODO: Enable config to enforce generate_authkeys capability
// For now, any user is capable of WebSSH connections
// const check = await context.sessions.check(
// request,
// Capabilities.generate_authkeys,
// );
const qp = new URL(request.url).searchParams;
const username = qp.get('username') || undefined;
const hostname = qp.get('hostname') || undefined;
if (!hostname) {
throw data('Missing required parameter: hostname', 400);
}
const qp = new URL(request.url).searchParams;
const username = qp.get("username") || undefined;
const hostname = qp.get("hostname") || undefined;
if (!hostname) {
throw data("Missing required parameter: hostname", 400);
}
if (!username) {
return {
ipnDetails: undefined,
sshDetails: {
username,
hostname,
},
};
}
if (!username) {
return {
ipnDetails: undefined,
sshDetails: {
username,
hostname,
},
};
}
// 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 res = await fetch(`${u}/key?v=116`, {
// method: 'GET',
// });
// 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 res = await fetch(`${u}/key?v=116`, {
// method: 'GET',
// });
// const corsOrigin = res.headers.get('Access-Control-Allow-Origin');
// const corsMethods = res.headers.get('Access-Control-Allow-Methods');
// const corsHeaders = res.headers.get('Access-Control-Allow-Headers');
// console.log(corsOrigin, corsMethods, corsHeaders);
// const corsOrigin = res.headers.get('Access-Control-Allow-Origin');
// const corsMethods = res.headers.get('Access-Control-Allow-Methods');
// const corsHeaders = res.headers.get('Access-Control-Allow-Headers');
// console.log(corsOrigin, corsMethods, corsHeaders);
// if (!corsOrigin || !corsMethods || !corsHeaders) {
// throw data(
// 'Headscale server does not have the required CORS headers for WebSSH',
// 500,
// );
// }
// if (!corsOrigin || !corsMethods || !corsHeaders) {
// throw data(
// 'Headscale server does not have the required CORS headers for WebSSH',
// 500,
// );
// }
const nodes = await api.getNodes();
const lookupNode = nodes.find((n) => n.givenName === hostname);
if (!lookupNode) {
throw data(`Node with hostname ${hostname} not found`, 404);
}
const nodes = await api.getNodes();
const lookupNode = nodes.find((n) => n.givenName === hostname);
if (!lookupNode) {
throw data(`Node with hostname ${hostname} not found`, 404);
}
// Last thing is keeping track of the ephemeral node in the database
// because Headscale doesn't automatically delete ephemeral nodes???
const [_ephemeralNode] = await context.db
.insert(ephemeralNodes)
.values({
auth_key: preAuthKey.key,
} satisfies EphemeralNodeInsert)
.returning();
// Last thing is keeping track of the ephemeral node in the database
// because Headscale doesn't automatically delete ephemeral nodes???
const [_ephemeralNode] = await context.db
.insert(ephemeralNodes)
.values({
auth_key: preAuthKey.key,
} satisfies EphemeralNodeInsert)
.returning();
return {
ipnDetails: {
PreAuthKey: preAuthKey.key,
Hostname: generateHostname(username),
ControlURL: u,
},
return {
ipnDetails: {
PreAuthKey: preAuthKey.key,
Hostname: generateHostname(username),
ControlURL: u,
},
sshDetails: {
username,
hostname,
},
};
sshDetails: {
username,
hostname,
},
};
}
function generateHostname(username: string) {
const adjective = faker.word.adjective({
length: {
min: 3,
max: 6,
},
});
const adjective = faker.word.adjective({
length: {
min: 3,
max: 6,
},
});
const noun = faker.word.noun({
length: {
min: 3,
max: 6,
},
});
const noun = faker.word.noun({
length: {
min: 3,
max: 6,
},
});
return `ssh-${adjective}-${noun}-${username}`;
return `ssh-${adjective}-${noun}-${username}`;
}
export async function action({ request, context }: Route.ActionArgs) {
await context.sessions.auth(request);
if (!context.agents?.agentID()) {
throw data(
'WebSSH is only available with the Headplane agent integration',
400,
);
}
await context.sessions.auth(request);
if (!context.agents?.agentID()) {
throw data("WebSSH is only available with the Headplane agent integration", 400);
}
const form = await request.formData();
const nodeKey = form.get('node_key');
const authKey = form.get('auth_key');
const form = await request.formData();
const nodeKey = form.get("node_key");
const authKey = form.get("auth_key");
if (nodeKey === null || typeof nodeKey !== 'string') {
throw data('Missing node_key', 400);
}
if (nodeKey === null || typeof nodeKey !== "string") {
throw data("Missing node_key", 400);
}
if (authKey === null || typeof authKey !== 'string') {
throw data('Missing auth_key', 400);
}
if (authKey === null || typeof authKey !== "string") {
throw data("Missing auth_key", 400);
}
await context.db
.update(ephemeralNodes)
.set({
node_key: nodeKey,
})
.where(eq(ephemeralNodes.auth_key, authKey));
await context.db
.update(ephemeralNodes)
.set({
node_key: nodeKey,
})
.where(eq(ephemeralNodes.auth_key, authKey));
}
export const links: Route.LinksFunction = () => [
{
rel: 'preload',
href: '/hp_ssh.wasm',
as: 'fetch',
type: 'application/wasm',
crossOrigin: 'anonymous',
},
{
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,
},
],
scripts: [
{
src: "/wasm_exec.js",
crossOrigin: "anonymous",
preload: true,
},
],
};
export default function Page({
loaderData: { ipnDetails, sshDetails },
}: Route.ComponentProps) {
const submit = useSubmit();
const { pause } = useLiveData();
export default function Page({ loaderData: { ipnDetails, sshDetails } }: Route.ComponentProps) {
const submit = useSubmit();
const { pause } = useLiveData();
const [ipn, setIpn] = useState<TsWasmNet | null>(null);
const [nodeKey, setNodeKey] = useState<string | null>(null);
const [ipn, setIpn] = useState<TsWasmNet | null>(null);
const [nodeKey, setNodeKey] = useState<string | null>(null);
useEffect(() => {
if (!ipnDetails) {
return;
}
useEffect(() => {
if (!ipnDetails) {
return;
}
pause();
const go = new Go(); // Go is defined by wasm_exec.js
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);
},
});
pause();
const go = new Go(); // Go is defined by wasm_exec.js
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();
});
}, []);
handle.Start();
});
}, []);
if (!sshDetails.username) {
return <UserPrompt hostname={sshDetails.hostname} />;
}
if (!sshDetails.username) {
return <UserPrompt hostname={sshDetails.hostname} />;
}
return (
<div className="w-screen h-screen bg-headplane-900">
{ipn === null ? (
<div className="mx-auto h-screen flex items-center justify-center">
<Loader2 className="animate-spin size-10 text-headplane-50" />
</div>
) : (
<div className="flex flex-col h-screen">
<XTerm
hostname={sshDetails.hostname}
ipn={ipn}
username={sshDetails.username}
/>
</div>
)}
</div>
);
return (
<div className="bg-headplane-900 h-screen w-screen">
{ipn === null ? (
<div className="mx-auto flex h-screen items-center justify-center">
<Loader2 className="text-headplane-50 size-10 animate-spin" />
</div>
) : (
<div className="flex h-screen flex-col">
<XTerm hostname={sshDetails.hostname} ipn={ipn} username={sshDetails.username} />
</div>
)}
</div>
);
}
+71 -77
View File
@@ -4,115 +4,109 @@
@plugin "tailwindcss-react-aria-components";
@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,
sans-serif;
--font-sans: Inter, -apple-system, BlinkMacSystemFont, Helvetica, Arial, sans-serif;
--transition-duration-25: 25ms;
--transition-duration-50: 50ms;
--transition-duration-25: 25ms;
--transition-duration-50: 50ms;
--color-main-50: #f8fafc;
--color-main-100: #f1f5f9;
--color-main-200: #e2e8f0;
--color-main-300: #cbd5e1;
--color-main-400: #94a3b8;
--color-main-500: #64748b;
--color-main-600: #475569;
--color-main-700: #334155;
--color-main-800: #1e293b;
--color-main-900: #0f172a;
--color-main-950: #020617;
--color-main-50: #f8fafc;
--color-main-100: #f1f5f9;
--color-main-200: #e2e8f0;
--color-main-300: #cbd5e1;
--color-main-400: #94a3b8;
--color-main-500: #64748b;
--color-main-600: #475569;
--color-main-700: #334155;
--color-main-800: #1e293b;
--color-main-900: #0f172a;
--color-main-950: #020617;
--color-ui-50: #fafafa;
--color-ui-100: #f5f5f5;
--color-ui-200: #e5e5e5;
--color-ui-300: #d4d4d4;
--color-ui-400: #a3a3a3;
--color-ui-500: #737373;
--color-ui-600: #525252;
--color-ui-700: #404040;
--color-ui-800: #262626;
--color-ui-900: #171717;
--color-ui-950: #0a0a0a;
--color-ui-50: #fafafa;
--color-ui-100: #f5f5f5;
--color-ui-200: #e5e5e5;
--color-ui-300: #d4d4d4;
--color-ui-400: #a3a3a3;
--color-ui-500: #737373;
--color-ui-600: #525252;
--color-ui-700: #404040;
--color-ui-800: #262626;
--color-ui-900: #171717;
--color-ui-950: #0a0a0a;
--color-headplane-50: #f2f2f2;
--color-headplane-100: #e6e6e6;
--color-headplane-200: #cccccc;
--color-headplane-300: #b3b3b3;
--color-headplane-400: #999999;
--color-headplane-500: #808080;
--color-headplane-600: #666666;
--color-headplane-700: #4d4d4d;
--color-headplane-800: #343434;
--color-headplane-900: #1a1a1a;
--color-headplane-950: #0d0d0d;
--color-headplane-50: #f2f2f2;
--color-headplane-100: #e6e6e6;
--color-headplane-200: #cccccc;
--color-headplane-300: #b3b3b3;
--color-headplane-400: #999999;
--color-headplane-500: #808080;
--color-headplane-600: #666666;
--color-headplane-700: #4d4d4d;
--color-headplane-800: #343434;
--color-headplane-900: #1a1a1a;
--color-headplane-950: #0d0d0d;
--animate-loading: loader 0.8s infinite ease-in-out;
--animate-loading: loader 0.8s infinite ease-in-out;
@keyframes loader {
from {
transform: translateX(-100%);
}
to {
transform: translateX(100%);
}
}
@keyframes loader {
from {
transform: translateX(-100%);
}
to {
transform: translateX(100%);
}
}
}
@utility container {
margin-inline: auto;
padding-inline: 1rem;
width: 100%;
max-width: 96rem; /* 1536px - constrain on large displays */
margin-inline: auto;
padding-inline: 1rem;
width: 100%;
max-width: 96rem; /* 1536px - constrain on large displays */
/* biome-ignore lint/correctness/noUnknownFunction: Tailwind CSS */
@media (width >= theme(--breakpoint-sm)) {
padding-inline: 2rem;
}
@media (width >= theme(--breakpoint-sm)) {
padding-inline: 2rem;
}
@media (width >= theme(--breakpoint-lg)) {
padding-inline: 4rem;
}
/* biome-ignore lint/correctness/noUnknownFunction: Tailwind CSS */
@media (width >= theme(--breakpoint-lg)) {
padding-inline: 4rem;
}
@media (width >= theme(--breakpoint-xl)) {
padding-inline: 5rem;
}
/* biome-ignore lint/correctness/noUnknownFunction: Tailwind CSS */
@media (width >= theme(--breakpoint-xl)) {
padding-inline: 5rem;
}
/* biome-ignore lint/correctness/noUnknownFunction: Tailwind CSS */
@media (width >= theme(--breakpoint-2xl)) {
padding-inline: 6rem;
}
@media (width >= theme(--breakpoint-2xl)) {
padding-inline: 6rem;
}
}
@supports (scrollbar-gutter: stable) {
html {
scrollbar-gutter: stable;
}
html {
scrollbar-gutter: stable;
}
}
.cm-merge-theme {
height: 100% !important;
height: 100% !important;
}
.cm-mergeView {
height: 100% !important;
height: 100% !important;
}
.cm-mergeViewEditors {
height: 100%;
height: 100%;
}
.cm-mergeViewEditor {
height: 100% !important;
height: 100% !important;
}
/* Weirdest class name characters but ok */
.cm-mergeView .ͼ1 .cm-scroller,
.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.
## Contribution Types
- **Bug Reports/Feature Requests**: If you find a bug, please open an
[issue](https://github.com/tale/headplane/issues) using one of the predefined
templates. Issues are only for bug reports and feature requests, not problems
that you encounter from misconfiguration or usage. Those belong in the
[discussions](https://github.com/tale/headplane/discussions) section.
[issue](https://github.com/tale/headplane/issues) using one of the predefined
templates. Issues are only for bug reports and feature requests, not problems
that you encounter from misconfiguration or usage. Those belong in the
[discussions](https://github.com/tale/headplane/discussions) section.
- **Documentation/Examples**: If you find any issues in the documentation or
would like to contribute examples for setting up Headplane, please open a PR
and I will review it and possibly make changes.
would like to contribute examples for setting up Headplane, please open a PR
and I will review it and possibly make changes.
- **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).
Please tag them with the appropriate labels when opening an issue or PR.
- **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).
Please tag them with the appropriate labels when opening an issue or PR.
### Code Contribution Restrictions
- **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,
maintain, and merge. This also means you should split up multiple contributions
into multiple PRs if they are unrelated to each other.
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
into multiple PRs if they are unrelated to each other.
- **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
used to develop Headplane. This includes things like changing the package
manager, docker environment, or CI/CD.
I will not accept changes to the project structure, build system, or tooling
used to develop Headplane. This includes things like changing the package
manager, docker environment, or CI/CD.
- **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
to do so. If you want to make a breaking change, please open an issue first
and discuss it with me.
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
and discuss it with me.
### 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
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.
### Contribution Tooling
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`.
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:
commands:
check:
lint:
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
check_go:
glob: "*.go"
+120 -117
View File
@@ -1,119 +1,122 @@
{
"name": "headplane",
"private": true,
"sideEffects": false,
"version": "0.6.1",
"type": "module",
"scripts": {
"preinstall": "npx only-allow pnpm",
"build": "react-router build",
"dev": "HEADPLANE_CONFIG_PATH=./config.example.yaml react-router dev",
"start": "node build/server/index.js",
"typecheck": "react-router typegen && tsgo",
"test:unit": "vitest run --project unit",
"test:integration": "vitest run --project integration",
"docs:dev": "vitepress dev docs",
"docs:build": "vitepress build docs",
"docs:preview": "vitepress preview docs",
"gen:hashes": "tsx tests/generate-openapi-hashes.ts"
},
"dependencies": {
"@libsql/client": "0.15.15"
},
"devDependencies": {
"@biomejs/biome": "^2.3.5",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^7.0.0",
"@dnd-kit/sortable": "^8.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@faker-js/faker": "9.9.0",
"@fontsource-variable/inter": "^5.2.8",
"@iconify/react": "^6.0.2",
"@kubernetes/client-node": "^1.4.0",
"@react-aria/toast": "3.0.8",
"@react-router/dev": "^7.9.6",
"@react-router/node": "^7.9.6",
"@react-stately/toast": "3.1.2",
"@readme/openapi-parser": "^5.2.1",
"@shopify/lang-jsonc": "^1.0.1",
"@tailwindcss/vite": "^4.1.17",
"@types/js-yaml": "^4.0.9",
"@types/node": "^24.10.1",
"@types/react": "^19.2.5",
"@types/react-dom": "^19.2.3",
"@typescript/native-preview": "7.0.0-dev.20251203.1",
"@uiw/codemirror-theme-github": "4.25.1",
"@uiw/codemirror-theme-xcode": "4.25.3",
"@uiw/react-codemirror": "4.25.3",
"@xterm/addon-clipboard": "^0.1.0",
"@xterm/addon-fit": "^0.10.0",
"@xterm/addon-unicode11": "^0.8.0",
"@xterm/addon-web-links": "^0.11.0",
"@xterm/xterm": "^5.5.0",
"arktype": "^2.1.26",
"clsx": "^2.1.1",
"drizzle-kit": "^0.31.7",
"drizzle-orm": "0.44.7",
"isbot": "5.1.32",
"jose": "6.1.2",
"js-yaml": "^4.1.1",
"lefthook": "^2.0.4",
"lucide-react": "^0.540.0",
"mime": "^4.1.0",
"openapi-types": "^12.1.3",
"openid-client": "6.8.1",
"postcss": "^8.5.6",
"react": "19.2.0",
"react-aria": "3.44.0",
"react-codemirror-merge": "4.25.3",
"react-dom": "19.2.0",
"react-error-boundary": "^6.0.0",
"react-router": "^7.9.6",
"react-router-dom": "^7.9.6",
"react-router-hono-server": "2.22.0",
"react-stately": "3.42.0",
"remix-utils": "^9.0.0",
"tailwind-merge": "3.4.0",
"tailwindcss": "^4.1.17",
"tailwindcss-animate": "^1.0.7",
"tailwindcss-react-aria-components": "^2.0.1",
"testcontainers": "^11.8.1",
"tsx": "^4.20.6",
"typescript": "^5.9.3",
"ulidx": "2.4.1",
"undici": "7.16.0",
"usehooks-ts": "^3.1.1",
"vite": "8.0.0-beta.0",
"vite-tsconfig-paths": "^5.1.4",
"vitepress": "next",
"vitest": "^4.0.9",
"yaml": "2.8.1"
},
"packageManager": "pnpm@10.4.0",
"engines": {
"node": ">=22.18 <23",
"pnpm": ">=10.4 <11"
},
"pnpm": {
"supportedArchitectures": {
"os": [
"current",
"linux"
],
"cpu": [
"x64",
"arm64"
]
},
"onlyBuiltDependencies": [
"@biomejs/biome",
"@tailwindcss/oxide",
"better-sqlite3",
"esbuild",
"lefthook"
],
"patchedDependencies": {
"react-router-hono-server": "patches/react-router-hono-server.patch"
}
}
"name": "headplane",
"version": "0.6.1",
"private": true,
"type": "module",
"sideEffects": false,
"scripts": {
"preinstall": "npx only-allow pnpm",
"build": "react-router build",
"dev": "HEADPLANE_CONFIG_PATH=./config.example.yaml react-router dev",
"start": "node build/server/index.js",
"typecheck": "react-router typegen && tsgo",
"test:unit": "vitest run --project unit",
"test:integration": "vitest run --project integration",
"docs:dev": "vitepress dev docs",
"docs:build": "vitepress build docs",
"docs:preview": "vitepress preview docs",
"gen:hashes": "tsx tests/generate-openapi-hashes.ts",
"lint": "oxlint",
"format": "oxfmt"
},
"dependencies": {
"@libsql/client": "0.15.15"
},
"devDependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^7.0.0",
"@dnd-kit/sortable": "^8.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@faker-js/faker": "9.9.0",
"@fontsource-variable/inter": "^5.2.8",
"@iconify/react": "^6.0.2",
"@kubernetes/client-node": "^1.4.0",
"@react-aria/toast": "3.0.8",
"@react-router/dev": "^7.9.6",
"@react-router/node": "^7.9.6",
"@react-stately/toast": "3.1.2",
"@readme/openapi-parser": "^5.2.1",
"@shopify/lang-jsonc": "^1.0.1",
"@tailwindcss/vite": "^4.1.17",
"@types/js-yaml": "^4.0.9",
"@types/node": "^24.10.1",
"@types/react": "^19.2.5",
"@types/react-dom": "^19.2.3",
"@typescript/native-preview": "7.0.0-dev.20251203.1",
"@uiw/codemirror-theme-github": "4.25.1",
"@uiw/codemirror-theme-xcode": "4.25.3",
"@uiw/react-codemirror": "4.25.3",
"@xterm/addon-clipboard": "^0.1.0",
"@xterm/addon-fit": "^0.10.0",
"@xterm/addon-unicode11": "^0.8.0",
"@xterm/addon-web-links": "^0.11.0",
"@xterm/xterm": "^5.5.0",
"arktype": "^2.1.26",
"clsx": "^2.1.1",
"drizzle-kit": "^0.31.7",
"drizzle-orm": "0.44.7",
"isbot": "5.1.32",
"jose": "6.1.2",
"js-yaml": "^4.1.1",
"lefthook": "^2.0.4",
"lucide-react": "^0.540.0",
"mime": "^4.1.0",
"openapi-types": "^12.1.3",
"openid-client": "6.8.1",
"oxfmt": "^0.24.0",
"oxlint": "^1.39.0",
"oxlint-tsgolint": "^0.11.1",
"postcss": "^8.5.6",
"react": "19.2.0",
"react-aria": "3.44.0",
"react-codemirror-merge": "4.25.3",
"react-dom": "19.2.0",
"react-error-boundary": "^6.0.0",
"react-router": "^7.9.6",
"react-router-dom": "^7.9.6",
"react-router-hono-server": "2.22.0",
"react-stately": "3.42.0",
"remix-utils": "^9.0.0",
"tailwind-merge": "3.4.0",
"tailwindcss": "^4.1.17",
"tailwindcss-animate": "^1.0.7",
"tailwindcss-react-aria-components": "^2.0.1",
"testcontainers": "^11.8.1",
"tsx": "^4.20.6",
"typescript": "^5.9.3",
"ulidx": "2.4.1",
"undici": "7.16.0",
"usehooks-ts": "^3.1.1",
"vite": "8.0.0-beta.0",
"vite-tsconfig-paths": "^5.1.4",
"vitepress": "next",
"vitest": "^4.0.9",
"yaml": "2.8.1"
},
"engines": {
"node": ">=22.18 <23",
"pnpm": ">=10.4 <11"
},
"packageManager": "pnpm@10.4.0",
"pnpm": {
"supportedArchitectures": {
"os": [
"current",
"linux"
],
"cpu": [
"x64",
"arm64"
]
},
"onlyBuiltDependencies": [
"@tailwindcss/oxide",
"better-sqlite3",
"esbuild",
"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
version: 0.15.15(bufferutil@4.0.9)(utf-8-validate@5.0.10)
devDependencies:
'@biomejs/biome':
specifier: ^2.3.5
version: 2.3.5
'@dnd-kit/core':
specifier: ^6.3.1
version: 6.3.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
@@ -140,6 +137,15 @@ importers:
openid-client:
specifier: 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:
specifier: ^8.5.6
version: 8.5.6
@@ -389,59 +395,6 @@ packages:
'@balena/dockerignore@1.0.2':
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':
resolution: {integrity: sha512-wJGylKtMFR/Ds6Gh01+OovXE/pncPiKZNNBKuC39pKnH+XK5d9+WsNqcrdxPjFPFTigRBqse0rfxw9UxrfyhPg==}
peerDependencies:
@@ -1230,6 +1183,116 @@ packages:
'@oxc-project/types@0.101.0':
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':
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'}
@@ -3601,6 +3664,25 @@ packages:
openid-client@6.8.1:
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:
resolution: {integrity: sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==}
engines: {node: '>=18'}
@@ -4077,6 +4159,10 @@ packages:
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
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:
resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==}
engines: {node: '>=14.0.0'}
@@ -4662,41 +4748,6 @@ snapshots:
'@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)':
dependencies:
'@codemirror/language': 6.11.3
@@ -5363,6 +5414,72 @@ snapshots:
'@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':
optional: true
@@ -8043,6 +8160,40 @@ snapshots:
jose: 6.1.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: {}
package-json-from-dist@1.0.1: {}
@@ -8653,6 +8804,8 @@ snapshots:
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
tinypool@2.0.0: {}
tinyrainbow@3.0.3: {}
tmp@0.2.5: {}
+69 -71
View File
@@ -1,83 +1,81 @@
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';
import { describe, expect, test } from "vitest";
describe('Configuration secret path handling', () => {
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;
import type { PartialHeadplaneConfigWithPaths } from "~/server/config/config-schema";
await loadConfigKeyPaths(config);
expect(config.server?.cookie_secret).toBe('supersecretcookievalue');
});
import { ConfigError } from "~/server/config/error";
import { loadConfigKeyPaths } from "~/server/config/load";
test('should throw error for missing secret file', async () => {
const config = {
server: {
cookie_secret_path: '/secrets/missing_cookie_secret.txt',
},
} as PartialHeadplaneConfigWithPaths;
import { createFakeFile } from "../setup/overlay-fs";
await expect(loadConfigKeyPaths(config)).rejects.toMatchObject(
ConfigError.from('MISSING_SECRET_FILE', {
pathKey: 'server.cookie_secret_path',
filePath: '/secrets/missing_cookie_secret.txt',
}),
);
});
describe("Configuration secret path handling", () => {
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;
test('should throw error for conflicting secret path and field', async () => {
const config = {
server: {
cookie_secret: 'explicitsecretvalue',
cookie_secret_path: '/secrets/cookie_secret.txt',
},
} as PartialHeadplaneConfigWithPaths;
await loadConfigKeyPaths(config);
expect(config.server?.cookie_secret).toBe("supersecretcookievalue");
});
await expect(loadConfigKeyPaths(config)).rejects.toMatchObject(
ConfigError.from('CONFLICTING_SECRET_PATH_FIELD', {
fieldName: 'server.cookie_secret',
}),
);
});
test("should throw error for missing secret file", async () => {
const config = {
server: {
cookie_secret_path: "/secrets/missing_cookie_secret.txt",
},
} as PartialHeadplaneConfigWithPaths;
test('should correctly interpolate env vars in secret paths', async () => {
process.env.HP_TEST_COOKIE_SECRET_FILE = 'cookie_secret.txt';
createFakeFile(
`/secrets/${process.env.HP_TEST_COOKIE_SECRET_FILE}`,
'envvarsecretvalue',
);
await expect(loadConfigKeyPaths(config)).rejects.toMatchObject(
ConfigError.from("MISSING_SECRET_FILE", {
pathKey: "server.cookie_secret_path",
filePath: "/secrets/missing_cookie_secret.txt",
}),
);
});
const config = {
server: {
// biome-ignore lint/suspicious/noTemplateCurlyInString: Test supports interpolation
cookie_secret_path: '/secrets/${HP_TEST_COOKIE_SECRET_FILE}',
},
} as PartialHeadplaneConfigWithPaths;
test("should throw error for conflicting secret path and field", async () => {
const config = {
server: {
cookie_secret: "explicitsecretvalue",
cookie_secret_path: "/secrets/cookie_secret.txt",
},
} as PartialHeadplaneConfigWithPaths;
await loadConfigKeyPaths(config);
expect(config.server?.cookie_secret).toBe('envvarsecretvalue');
});
await expect(loadConfigKeyPaths(config)).rejects.toMatchObject(
ConfigError.from("CONFLICTING_SECRET_PATH_FIELD", {
fieldName: "server.cookie_secret",
}),
);
});
test('should throw error for missing interpolated env var in secret path', async () => {
const config = {
server: {
// biome-ignore lint/suspicious/noTemplateCurlyInString: Test supports interpolation
cookie_secret_path: '/secrets/${MISSING_ENV_VAR}',
},
} as PartialHeadplaneConfigWithPaths;
test("should correctly interpolate env vars in secret paths", async () => {
process.env.HP_TEST_COOKIE_SECRET_FILE = "cookie_secret.txt";
createFakeFile(`/secrets/${process.env.HP_TEST_COOKIE_SECRET_FILE}`, "envvarsecretvalue");
await expect(loadConfigKeyPaths(config)).rejects.toMatchObject(
ConfigError.from('MISSING_INTERPOLATION_VARIABLE', {
pathKey: 'server.cookie_secret_path',
variableName: 'MISSING_ENV_VAR',
}),
);
});
const config = {
server: {
cookie_secret_path: "/secrets/${HP_TEST_COOKIE_SECRET_FILE}",
},
} 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",
}),
);
});
});