From 034ba25ce3bfcd86cb84e25e9839fbdaa85d5c4a Mon Sep 17 00:00:00 2001 From: Aarnav Tale Date: Thu, 15 Jan 2026 22:05:29 -0500 Subject: [PATCH] feat: switch to oxc linting/formatting --- .oxfmtrc.json | 10 + .oxlintrc.json | 144 ++++++++ app/entry.server.tsx | 110 +++--- app/routes/ssh/console.tsx | 467 +++++++++++++------------- app/tailwind.css | 148 ++++---- biome.json | 48 --- docs/CONTRIBUTING.md | 44 +-- lefthook.yml | 8 +- package.json | 237 ++++++------- pnpm-lock.yaml | 335 +++++++++++++----- tests/unit/config/secret-path.test.ts | 140 ++++---- 11 files changed, 967 insertions(+), 724 deletions(-) create mode 100644 .oxfmtrc.json create mode 100644 .oxlintrc.json delete mode 100644 biome.json diff --git a/.oxfmtrc.json b/.oxfmtrc.json new file mode 100644 index 0000000..26e8662 --- /dev/null +++ b/.oxfmtrc.json @@ -0,0 +1,10 @@ +{ + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "endOfLine": "lf", + "experimentalSortImports": {}, + "experimentalTailwindcss": {}, + "ignorePatterns": [], + "printWidth": 100, + "tabWidth": 2, + "useTabs": false +} diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 0000000..f8d1670 --- /dev/null +++ b/.oxlintrc.json @@ -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": [] +} diff --git a/app/entry.server.tsx b/app/entry.server.tsx index be7066c..89102f6 100644 --- a/app/entry.server.tsx +++ b/app/entry.server.tsx @@ -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( - , - { - [readyOption]() { - shellRendered = true; - const body = new PassThrough(); - const stream = createReadableStreamFromReadable(body); + const { pipe, abort } = renderToPipeableStream( + , + { + [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); + }); } diff --git a/app/routes/ssh/console.tsx b/app/routes/ssh/console.tsx index f5f030a..b68c223 100644 --- a/app/routes/ssh/console.tsx +++ b/app/routes/ssh/console.tsx @@ -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 /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 /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(null); - const [nodeKey, setNodeKey] = useState(null); + const [ipn, setIpn] = useState(null); + const [nodeKey, setNodeKey] = useState(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 ; - } + if (!sshDetails.username) { + return ; + } - return ( -
- {ipn === null ? ( -
- -
- ) : ( -
- -
- )} -
- ); + return ( +
+ {ipn === null ? ( +
+ +
+ ) : ( +
+ +
+ )} +
+ ); } diff --git a/app/tailwind.css b/app/tailwind.css index 8f590c4..070cf2d 100644 --- a/app/tailwind.css +++ b/app/tailwind.css @@ -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; } diff --git a/biome.json b/biome.json deleted file mode 100644 index eb8e0d2..0000000 --- a/biome.json +++ /dev/null @@ -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" - } - } -} diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 947a1a5..d264d88 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -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 diff --git a/lefthook.yml b/lefthook.yml index 6f1be6e..e46098b 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -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" diff --git a/package.json b/package.json index e4a997c..b2c1945 100644 --- a/package.json +++ b/package.json @@ -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" + } + } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 066ecf9..f0d6f3d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: {} diff --git a/tests/unit/config/secret-path.test.ts b/tests/unit/config/secret-path.test.ts index 0e5cbe3..4c3782f 100644 --- a/tests/unit/config/secret-path.test.ts +++ b/tests/unit/config/secret-path.test.ts @@ -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", + }), + ); + }); });