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",
+ }),
+ );
+ });
});