mirror of
https://github.com/tale/headplane.git
synced 2026-07-26 07:48:14 +00:00
feat: switch to oxc linting/formatting
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxfmt/configuration_schema.json",
|
||||
"endOfLine": "lf",
|
||||
"experimentalSortImports": {},
|
||||
"experimentalTailwindcss": {},
|
||||
"ignorePatterns": [],
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false
|
||||
}
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"plugins": ["unicorn", "typescript", "oxc"],
|
||||
"categories": {},
|
||||
"rules": {
|
||||
"constructor-super": "warn",
|
||||
"for-direction": "warn",
|
||||
"no-async-promise-executor": "warn",
|
||||
"no-caller": "warn",
|
||||
"no-class-assign": "warn",
|
||||
"no-compare-neg-zero": "warn",
|
||||
"no-cond-assign": "warn",
|
||||
"no-const-assign": "warn",
|
||||
"no-constant-binary-expression": "warn",
|
||||
"no-constant-condition": "warn",
|
||||
"no-control-regex": "warn",
|
||||
"no-debugger": "warn",
|
||||
"no-delete-var": "warn",
|
||||
"no-dupe-class-members": "warn",
|
||||
"no-dupe-else-if": "warn",
|
||||
"no-dupe-keys": "warn",
|
||||
"no-duplicate-case": "warn",
|
||||
"no-empty-character-class": "warn",
|
||||
"no-empty-pattern": "warn",
|
||||
"no-empty-static-block": "warn",
|
||||
"no-eval": "warn",
|
||||
"no-ex-assign": "warn",
|
||||
"no-extra-boolean-cast": "warn",
|
||||
"no-func-assign": "warn",
|
||||
"no-global-assign": "warn",
|
||||
"no-import-assign": "warn",
|
||||
"no-invalid-regexp": "warn",
|
||||
"no-irregular-whitespace": "warn",
|
||||
"no-loss-of-precision": "warn",
|
||||
"no-new-native-nonconstructor": "warn",
|
||||
"no-nonoctal-decimal-escape": "warn",
|
||||
"no-obj-calls": "warn",
|
||||
"no-self-assign": "warn",
|
||||
"no-setter-return": "warn",
|
||||
"no-shadow-restricted-names": "warn",
|
||||
"no-sparse-arrays": "warn",
|
||||
"no-this-before-super": "warn",
|
||||
"no-unassigned-vars": "warn",
|
||||
"no-unsafe-finally": "warn",
|
||||
"no-unsafe-negation": "warn",
|
||||
"no-unsafe-optional-chaining": "warn",
|
||||
"no-unused-expressions": "warn",
|
||||
"no-unused-labels": "warn",
|
||||
"no-unused-private-class-members": "warn",
|
||||
"no-unused-vars": "warn",
|
||||
"no-useless-backreference": "warn",
|
||||
"no-useless-catch": "warn",
|
||||
"no-useless-escape": "warn",
|
||||
"no-useless-rename": "warn",
|
||||
"no-with": "warn",
|
||||
"require-yield": "warn",
|
||||
"use-isnan": "warn",
|
||||
"valid-typeof": "warn",
|
||||
"oxc/bad-array-method-on-arguments": "warn",
|
||||
"oxc/bad-char-at-comparison": "warn",
|
||||
"oxc/bad-comparison-sequence": "warn",
|
||||
"oxc/bad-min-max-func": "warn",
|
||||
"oxc/bad-object-literal-comparison": "warn",
|
||||
"oxc/bad-replace-all-arg": "warn",
|
||||
"oxc/const-comparisons": "warn",
|
||||
"oxc/double-comparisons": "warn",
|
||||
"oxc/erasing-op": "warn",
|
||||
"oxc/missing-throw": "warn",
|
||||
"oxc/number-arg-out-of-range": "warn",
|
||||
"oxc/only-used-in-recursion": "warn",
|
||||
"oxc/uninvoked-array-callback": "warn",
|
||||
"typescript/await-thenable": "warn",
|
||||
"typescript/no-array-delete": "warn",
|
||||
"typescript/no-base-to-string": "warn",
|
||||
"typescript/no-duplicate-enum-values": "warn",
|
||||
"typescript/no-duplicate-type-constituents": "warn",
|
||||
"typescript/no-extra-non-null-assertion": "warn",
|
||||
"typescript/no-floating-promises": "warn",
|
||||
"typescript/no-for-in-array": "warn",
|
||||
"typescript/no-implied-eval": "warn",
|
||||
"typescript/no-meaningless-void-operator": "warn",
|
||||
"typescript/no-misused-new": "warn",
|
||||
"typescript/no-misused-spread": "warn",
|
||||
"typescript/no-non-null-asserted-optional-chain": "warn",
|
||||
"typescript/no-redundant-type-constituents": "warn",
|
||||
"typescript/no-this-alias": "warn",
|
||||
"typescript/no-unnecessary-parameter-property-assignment": "warn",
|
||||
"typescript/no-unsafe-declaration-merging": "warn",
|
||||
"typescript/no-unsafe-unary-minus": "warn",
|
||||
"typescript/no-useless-empty-export": "warn",
|
||||
"typescript/no-wrapper-object-types": "warn",
|
||||
"typescript/prefer-as-const": "warn",
|
||||
"typescript/require-array-sort-compare": "warn",
|
||||
"typescript/restrict-template-expressions": "warn",
|
||||
"typescript/triple-slash-reference": "warn",
|
||||
"typescript/unbound-method": "warn",
|
||||
"unicorn/no-await-in-promise-methods": "warn",
|
||||
"unicorn/no-empty-file": "warn",
|
||||
"unicorn/no-invalid-fetch-options": "warn",
|
||||
"unicorn/no-invalid-remove-event-listener": "warn",
|
||||
"unicorn/no-new-array": "warn",
|
||||
"unicorn/no-single-promise-in-promise-methods": "warn",
|
||||
"unicorn/no-thenable": "warn",
|
||||
"unicorn/no-unnecessary-await": "warn",
|
||||
"unicorn/no-useless-fallback-in-spread": "warn",
|
||||
"unicorn/no-useless-length-check": "warn",
|
||||
"unicorn/no-useless-spread": "warn",
|
||||
"unicorn/prefer-set-size": "warn",
|
||||
"unicorn/prefer-string-starts-ends-with": "warn"
|
||||
},
|
||||
"settings": {
|
||||
"jsx-a11y": {
|
||||
"polymorphicPropName": null,
|
||||
"components": {},
|
||||
"attributes": {}
|
||||
},
|
||||
"next": {
|
||||
"rootDir": []
|
||||
},
|
||||
"react": {
|
||||
"formComponents": [],
|
||||
"linkComponents": [],
|
||||
"version": null
|
||||
},
|
||||
"jsdoc": {
|
||||
"ignorePrivate": false,
|
||||
"ignoreInternal": false,
|
||||
"ignoreReplacesDocs": true,
|
||||
"overrideReplacesDocs": true,
|
||||
"augmentsExtendsReplacesDocs": false,
|
||||
"implementsReplacesDocs": false,
|
||||
"exemptDestructuredRootsFromChecks": false,
|
||||
"tagNamePreference": {}
|
||||
},
|
||||
"vitest": {
|
||||
"typecheck": false
|
||||
}
|
||||
},
|
||||
"env": {
|
||||
"builtin": true
|
||||
},
|
||||
"globals": {},
|
||||
"ignorePatterns": []
|
||||
}
|
||||
+54
-56
@@ -1,67 +1,65 @@
|
||||
import { PassThrough } from 'node:stream';
|
||||
import { createReadableStreamFromReadable } from '@react-router/node';
|
||||
import { isbot } from 'isbot';
|
||||
import type { RenderToPipeableStreamOptions } from 'react-dom/server';
|
||||
import { renderToPipeableStream } from 'react-dom/server';
|
||||
import type { AppLoadContext, EntryContext } from 'react-router';
|
||||
import { ServerRouter } from 'react-router';
|
||||
import type { RenderToPipeableStreamOptions } from "react-dom/server";
|
||||
import type { AppLoadContext, EntryContext } from "react-router";
|
||||
|
||||
import { createReadableStreamFromReadable } from "@react-router/node";
|
||||
import { isbot } from "isbot";
|
||||
import { PassThrough } from "node:stream";
|
||||
import { renderToPipeableStream } from "react-dom/server";
|
||||
import { ServerRouter } from "react-router";
|
||||
|
||||
export const streamTimeout = 5_000;
|
||||
export default function handleRequest(
|
||||
request: Request,
|
||||
responseStatusCode: number,
|
||||
responseHeaders: Headers,
|
||||
routerContext: EntryContext,
|
||||
loadContext: AppLoadContext,
|
||||
request: Request,
|
||||
responseStatusCode: number,
|
||||
responseHeaders: Headers,
|
||||
routerContext: EntryContext,
|
||||
_loadContext: AppLoadContext,
|
||||
) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let shellRendered = false;
|
||||
const userAgent = request.headers.get('user-agent');
|
||||
return new Promise((resolve, reject) => {
|
||||
let shellRendered = false;
|
||||
const userAgent = request.headers.get("user-agent");
|
||||
|
||||
// Ensure requests from bots and SPA Mode renders wait for all content to load before responding
|
||||
// https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation
|
||||
const readyOption: keyof RenderToPipeableStreamOptions =
|
||||
(userAgent && isbot(userAgent)) || routerContext.isSpaMode
|
||||
? 'onAllReady'
|
||||
: 'onShellReady';
|
||||
// Ensure requests from bots and SPA Mode renders wait for all content to load before responding
|
||||
// https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation
|
||||
const readyOption: keyof RenderToPipeableStreamOptions =
|
||||
(userAgent && isbot(userAgent)) || routerContext.isSpaMode ? "onAllReady" : "onShellReady";
|
||||
|
||||
const { pipe, abort } = renderToPipeableStream(
|
||||
<ServerRouter context={routerContext} url={request.url} />,
|
||||
{
|
||||
[readyOption]() {
|
||||
shellRendered = true;
|
||||
const body = new PassThrough();
|
||||
const stream = createReadableStreamFromReadable(body);
|
||||
const { pipe, abort } = renderToPipeableStream(
|
||||
<ServerRouter context={routerContext} url={request.url} />,
|
||||
{
|
||||
[readyOption]() {
|
||||
shellRendered = true;
|
||||
const body = new PassThrough();
|
||||
const stream = createReadableStreamFromReadable(body);
|
||||
|
||||
responseHeaders.set('Content-Type', 'text/html');
|
||||
responseHeaders.set("Content-Type", "text/html");
|
||||
|
||||
resolve(
|
||||
new Response(stream, {
|
||||
headers: responseHeaders,
|
||||
status: responseStatusCode,
|
||||
}),
|
||||
);
|
||||
resolve(
|
||||
new Response(stream, {
|
||||
headers: responseHeaders,
|
||||
status: responseStatusCode,
|
||||
}),
|
||||
);
|
||||
|
||||
pipe(body);
|
||||
},
|
||||
onShellError(error: unknown) {
|
||||
reject(error);
|
||||
},
|
||||
onError(error: unknown) {
|
||||
// biome-ignore lint/style/noParameterAssign: Lazy
|
||||
responseStatusCode = 500;
|
||||
// Log streaming rendering errors from inside the shell. Don't log
|
||||
// errors encountered during initial shell rendering since they'll
|
||||
// reject and get logged in handleDocumentRequest.
|
||||
if (shellRendered) {
|
||||
console.error(error);
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
pipe(body);
|
||||
},
|
||||
onShellError(error: unknown) {
|
||||
reject(error);
|
||||
},
|
||||
onError(error: unknown) {
|
||||
responseStatusCode = 500;
|
||||
// Log streaming rendering errors from inside the shell. Don't log
|
||||
// errors encountered during initial shell rendering since they'll
|
||||
// reject and get logged in handleDocumentRequest.
|
||||
if (shellRendered) {
|
||||
console.error(error);
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// Abort the rendering stream after the `streamTimeout` so it has tine to
|
||||
// flush down the rejected boundaries
|
||||
setTimeout(abort, streamTimeout + 1000);
|
||||
});
|
||||
// Abort the rendering stream after the `streamTimeout` so it has tine to
|
||||
// flush down the rejected boundaries
|
||||
setTimeout(abort, streamTimeout + 1000);
|
||||
});
|
||||
}
|
||||
|
||||
+225
-242
@@ -1,293 +1,276 @@
|
||||
/** biome-ignore-all lint/correctness/noNestedComponentDefinitions: Wtf? */
|
||||
import { faker } from "@faker-js/faker";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { data, type ShouldRevalidateFunction, useSubmit } from "react-router";
|
||||
import { ExternalScriptsHandle } from "remix-utils/external-scripts";
|
||||
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { data, type ShouldRevalidateFunction, useSubmit } from 'react-router';
|
||||
import { ExternalScriptsHandle } from 'remix-utils/external-scripts';
|
||||
import { EphemeralNodeInsert, ephemeralNodes } from '~/server/db/schema';
|
||||
import { useLiveData } from '~/utils/live-data';
|
||||
import type { Route } from './+types/console';
|
||||
import UserPrompt from './user-prompt';
|
||||
import XTerm from './xterm.client';
|
||||
import { EphemeralNodeInsert, ephemeralNodes } from "~/server/db/schema";
|
||||
import { useLiveData } from "~/utils/live-data";
|
||||
|
||||
import type { Route } from "./+types/console";
|
||||
|
||||
import UserPrompt from "./user-prompt";
|
||||
import XTerm from "./xterm.client";
|
||||
|
||||
export const shouldRevalidate: ShouldRevalidateFunction = () => {
|
||||
return false;
|
||||
return false;
|
||||
};
|
||||
|
||||
export async function loader({ request, context }: Route.LoaderArgs) {
|
||||
const origin = new URL(request.url).origin;
|
||||
const assets = ['/wasm_exec.js', '/hp_ssh.wasm'];
|
||||
const missing: string[] = [];
|
||||
const origin = new URL(request.url).origin;
|
||||
const assets = ["/wasm_exec.js", "/hp_ssh.wasm"];
|
||||
const missing: string[] = [];
|
||||
|
||||
for (const file of assets) {
|
||||
const res = await fetch(`${origin}${file}`, { method: 'HEAD' });
|
||||
if (!res.ok) missing.push(file);
|
||||
}
|
||||
for (const file of assets) {
|
||||
const res = await fetch(`${origin}${file}`, { method: "HEAD" });
|
||||
if (!res.ok) missing.push(file);
|
||||
}
|
||||
|
||||
if (missing.length > 0) {
|
||||
throw data('WebSSH is not configured in this build.', 405);
|
||||
}
|
||||
if (missing.length > 0) {
|
||||
throw data("WebSSH is not configured in this build.", 405);
|
||||
}
|
||||
|
||||
if (!context.agents?.agentID()) {
|
||||
throw data(
|
||||
'WebSSH is only available with the Headplane agent integration',
|
||||
400,
|
||||
);
|
||||
}
|
||||
if (!context.agents?.agentID()) {
|
||||
throw data("WebSSH is only available with the Headplane agent integration", 400);
|
||||
}
|
||||
|
||||
const session = await context.sessions.auth(request);
|
||||
if (session.user.subject === 'unknown-non-oauth') {
|
||||
throw data('Only OAuth users are allowed to use WebSSH', 403);
|
||||
}
|
||||
const session = await context.sessions.auth(request);
|
||||
if (session.user.subject === "unknown-non-oauth") {
|
||||
throw data("Only OAuth users are allowed to use WebSSH", 403);
|
||||
}
|
||||
|
||||
const api = context.hsApi.getRuntimeClient(session.api_key);
|
||||
const users = await api.getUsers();
|
||||
const api = context.hsApi.getRuntimeClient(session.api_key);
|
||||
const users = await api.getUsers();
|
||||
|
||||
// MARK: This assumes that a user has authenticated with Headscale first
|
||||
// Since the only way to enforce permissions via ACLs is to generate a
|
||||
// pre-authkey which REQUIRES a user ID, meaning the user has to have
|
||||
// authenticated with Headscale first.
|
||||
const lookup = users.find((u) => {
|
||||
const subject = u.providerId?.split('/').pop();
|
||||
if (!subject) {
|
||||
return false;
|
||||
}
|
||||
return subject === session.user.subject;
|
||||
});
|
||||
// MARK: This assumes that a user has authenticated with Headscale first
|
||||
// Since the only way to enforce permissions via ACLs is to generate a
|
||||
// pre-authkey which REQUIRES a user ID, meaning the user has to have
|
||||
// authenticated with Headscale first.
|
||||
const lookup = users.find((u) => {
|
||||
const subject = u.providerId?.split("/").pop();
|
||||
if (!subject) {
|
||||
return false;
|
||||
}
|
||||
return subject === session.user.subject;
|
||||
});
|
||||
|
||||
if (!lookup) {
|
||||
throw data(
|
||||
`User with subject ${session.user.subject} not found within Headscale`,
|
||||
404,
|
||||
);
|
||||
}
|
||||
if (!lookup) {
|
||||
throw data(`User with subject ${session.user.subject} not found within Headscale`, 404);
|
||||
}
|
||||
|
||||
const preAuthKey = await api.createPreAuthKey(
|
||||
lookup.id,
|
||||
true, // ephemeral
|
||||
false, // reusable
|
||||
new Date(Date.now() + 60 * 1000), // expiration: 1 minute
|
||||
null, // aclTags
|
||||
);
|
||||
const preAuthKey = await api.createPreAuthKey(
|
||||
lookup.id,
|
||||
true, // ephemeral
|
||||
false, // reusable
|
||||
new Date(Date.now() + 60 * 1000), // expiration: 1 minute
|
||||
null, // aclTags
|
||||
);
|
||||
|
||||
// TODO: Enable config to enforce generate_authkeys capability
|
||||
// For now, any user is capable of WebSSH connections
|
||||
// const check = await context.sessions.check(
|
||||
// request,
|
||||
// Capabilities.generate_authkeys,
|
||||
// );
|
||||
// TODO: Enable config to enforce generate_authkeys capability
|
||||
// For now, any user is capable of WebSSH connections
|
||||
// const check = await context.sessions.check(
|
||||
// request,
|
||||
// Capabilities.generate_authkeys,
|
||||
// );
|
||||
|
||||
const qp = new URL(request.url).searchParams;
|
||||
const username = qp.get('username') || undefined;
|
||||
const hostname = qp.get('hostname') || undefined;
|
||||
if (!hostname) {
|
||||
throw data('Missing required parameter: hostname', 400);
|
||||
}
|
||||
const qp = new URL(request.url).searchParams;
|
||||
const username = qp.get("username") || undefined;
|
||||
const hostname = qp.get("hostname") || undefined;
|
||||
if (!hostname) {
|
||||
throw data("Missing required parameter: hostname", 400);
|
||||
}
|
||||
|
||||
if (!username) {
|
||||
return {
|
||||
ipnDetails: undefined,
|
||||
sshDetails: {
|
||||
username,
|
||||
hostname,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (!username) {
|
||||
return {
|
||||
ipnDetails: undefined,
|
||||
sshDetails: {
|
||||
username,
|
||||
hostname,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// We're making a request to <url>/key?v=116 to check the CORS headers
|
||||
const u = context.config.headscale.public_url ?? context.config.headscale.url;
|
||||
// const res = await fetch(`${u}/key?v=116`, {
|
||||
// method: 'GET',
|
||||
// });
|
||||
// We're making a request to <url>/key?v=116 to check the CORS headers
|
||||
const u = context.config.headscale.public_url ?? context.config.headscale.url;
|
||||
// const res = await fetch(`${u}/key?v=116`, {
|
||||
// method: 'GET',
|
||||
// });
|
||||
|
||||
// const corsOrigin = res.headers.get('Access-Control-Allow-Origin');
|
||||
// const corsMethods = res.headers.get('Access-Control-Allow-Methods');
|
||||
// const corsHeaders = res.headers.get('Access-Control-Allow-Headers');
|
||||
// console.log(corsOrigin, corsMethods, corsHeaders);
|
||||
// const corsOrigin = res.headers.get('Access-Control-Allow-Origin');
|
||||
// const corsMethods = res.headers.get('Access-Control-Allow-Methods');
|
||||
// const corsHeaders = res.headers.get('Access-Control-Allow-Headers');
|
||||
// console.log(corsOrigin, corsMethods, corsHeaders);
|
||||
|
||||
// if (!corsOrigin || !corsMethods || !corsHeaders) {
|
||||
// throw data(
|
||||
// 'Headscale server does not have the required CORS headers for WebSSH',
|
||||
// 500,
|
||||
// );
|
||||
// }
|
||||
// if (!corsOrigin || !corsMethods || !corsHeaders) {
|
||||
// throw data(
|
||||
// 'Headscale server does not have the required CORS headers for WebSSH',
|
||||
// 500,
|
||||
// );
|
||||
// }
|
||||
|
||||
const nodes = await api.getNodes();
|
||||
const lookupNode = nodes.find((n) => n.givenName === hostname);
|
||||
if (!lookupNode) {
|
||||
throw data(`Node with hostname ${hostname} not found`, 404);
|
||||
}
|
||||
const nodes = await api.getNodes();
|
||||
const lookupNode = nodes.find((n) => n.givenName === hostname);
|
||||
if (!lookupNode) {
|
||||
throw data(`Node with hostname ${hostname} not found`, 404);
|
||||
}
|
||||
|
||||
// Last thing is keeping track of the ephemeral node in the database
|
||||
// because Headscale doesn't automatically delete ephemeral nodes???
|
||||
const [_ephemeralNode] = await context.db
|
||||
.insert(ephemeralNodes)
|
||||
.values({
|
||||
auth_key: preAuthKey.key,
|
||||
} satisfies EphemeralNodeInsert)
|
||||
.returning();
|
||||
// Last thing is keeping track of the ephemeral node in the database
|
||||
// because Headscale doesn't automatically delete ephemeral nodes???
|
||||
const [_ephemeralNode] = await context.db
|
||||
.insert(ephemeralNodes)
|
||||
.values({
|
||||
auth_key: preAuthKey.key,
|
||||
} satisfies EphemeralNodeInsert)
|
||||
.returning();
|
||||
|
||||
return {
|
||||
ipnDetails: {
|
||||
PreAuthKey: preAuthKey.key,
|
||||
Hostname: generateHostname(username),
|
||||
ControlURL: u,
|
||||
},
|
||||
return {
|
||||
ipnDetails: {
|
||||
PreAuthKey: preAuthKey.key,
|
||||
Hostname: generateHostname(username),
|
||||
ControlURL: u,
|
||||
},
|
||||
|
||||
sshDetails: {
|
||||
username,
|
||||
hostname,
|
||||
},
|
||||
};
|
||||
sshDetails: {
|
||||
username,
|
||||
hostname,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function generateHostname(username: string) {
|
||||
const adjective = faker.word.adjective({
|
||||
length: {
|
||||
min: 3,
|
||||
max: 6,
|
||||
},
|
||||
});
|
||||
const adjective = faker.word.adjective({
|
||||
length: {
|
||||
min: 3,
|
||||
max: 6,
|
||||
},
|
||||
});
|
||||
|
||||
const noun = faker.word.noun({
|
||||
length: {
|
||||
min: 3,
|
||||
max: 6,
|
||||
},
|
||||
});
|
||||
const noun = faker.word.noun({
|
||||
length: {
|
||||
min: 3,
|
||||
max: 6,
|
||||
},
|
||||
});
|
||||
|
||||
return `ssh-${adjective}-${noun}-${username}`;
|
||||
return `ssh-${adjective}-${noun}-${username}`;
|
||||
}
|
||||
|
||||
export async function action({ request, context }: Route.ActionArgs) {
|
||||
await context.sessions.auth(request);
|
||||
if (!context.agents?.agentID()) {
|
||||
throw data(
|
||||
'WebSSH is only available with the Headplane agent integration',
|
||||
400,
|
||||
);
|
||||
}
|
||||
await context.sessions.auth(request);
|
||||
if (!context.agents?.agentID()) {
|
||||
throw data("WebSSH is only available with the Headplane agent integration", 400);
|
||||
}
|
||||
|
||||
const form = await request.formData();
|
||||
const nodeKey = form.get('node_key');
|
||||
const authKey = form.get('auth_key');
|
||||
const form = await request.formData();
|
||||
const nodeKey = form.get("node_key");
|
||||
const authKey = form.get("auth_key");
|
||||
|
||||
if (nodeKey === null || typeof nodeKey !== 'string') {
|
||||
throw data('Missing node_key', 400);
|
||||
}
|
||||
if (nodeKey === null || typeof nodeKey !== "string") {
|
||||
throw data("Missing node_key", 400);
|
||||
}
|
||||
|
||||
if (authKey === null || typeof authKey !== 'string') {
|
||||
throw data('Missing auth_key', 400);
|
||||
}
|
||||
if (authKey === null || typeof authKey !== "string") {
|
||||
throw data("Missing auth_key", 400);
|
||||
}
|
||||
|
||||
await context.db
|
||||
.update(ephemeralNodes)
|
||||
.set({
|
||||
node_key: nodeKey,
|
||||
})
|
||||
.where(eq(ephemeralNodes.auth_key, authKey));
|
||||
await context.db
|
||||
.update(ephemeralNodes)
|
||||
.set({
|
||||
node_key: nodeKey,
|
||||
})
|
||||
.where(eq(ephemeralNodes.auth_key, authKey));
|
||||
}
|
||||
|
||||
export const links: Route.LinksFunction = () => [
|
||||
{
|
||||
rel: 'preload',
|
||||
href: '/hp_ssh.wasm',
|
||||
as: 'fetch',
|
||||
type: 'application/wasm',
|
||||
crossOrigin: 'anonymous',
|
||||
},
|
||||
{
|
||||
rel: "preload",
|
||||
href: "/hp_ssh.wasm",
|
||||
as: "fetch",
|
||||
type: "application/wasm",
|
||||
crossOrigin: "anonymous",
|
||||
},
|
||||
];
|
||||
|
||||
export const handle: ExternalScriptsHandle = {
|
||||
scripts: [
|
||||
{
|
||||
src: '/wasm_exec.js',
|
||||
crossOrigin: 'anonymous',
|
||||
preload: true,
|
||||
},
|
||||
],
|
||||
scripts: [
|
||||
{
|
||||
src: "/wasm_exec.js",
|
||||
crossOrigin: "anonymous",
|
||||
preload: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default function Page({
|
||||
loaderData: { ipnDetails, sshDetails },
|
||||
}: Route.ComponentProps) {
|
||||
const submit = useSubmit();
|
||||
const { pause } = useLiveData();
|
||||
export default function Page({ loaderData: { ipnDetails, sshDetails } }: Route.ComponentProps) {
|
||||
const submit = useSubmit();
|
||||
const { pause } = useLiveData();
|
||||
|
||||
const [ipn, setIpn] = useState<TsWasmNet | null>(null);
|
||||
const [nodeKey, setNodeKey] = useState<string | null>(null);
|
||||
const [ipn, setIpn] = useState<TsWasmNet | null>(null);
|
||||
const [nodeKey, setNodeKey] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ipnDetails) {
|
||||
return;
|
||||
}
|
||||
useEffect(() => {
|
||||
if (!ipnDetails) {
|
||||
return;
|
||||
}
|
||||
|
||||
pause();
|
||||
const go = new Go(); // Go is defined by wasm_exec.js
|
||||
WebAssembly.instantiateStreaming(
|
||||
fetch('/hp_ssh.wasm'),
|
||||
go.importObject,
|
||||
).then((value) => {
|
||||
go.run(value.instance);
|
||||
const handle = TsWasmNet(ipnDetails, {
|
||||
NotifyState: (state) => {
|
||||
console.log('State changed:', state);
|
||||
if (state === 'Running') {
|
||||
setIpn(handle);
|
||||
}
|
||||
},
|
||||
NotifyNetMap: (netmap) => {
|
||||
// Only set NodeKey if it is not already set and then
|
||||
// also dispatch that to the backend to track the
|
||||
// ephemeral node.
|
||||
//
|
||||
// We open an SSE connection to the backend
|
||||
// so that when the connection is closed,
|
||||
// the backend can delete the ephemeral node.
|
||||
if (nodeKey === null) {
|
||||
setNodeKey(netmap.NodeKey);
|
||||
submit(
|
||||
{
|
||||
node_key: netmap.NodeKey,
|
||||
auth_key: ipnDetails.PreAuthKey,
|
||||
},
|
||||
{ method: 'POST' },
|
||||
);
|
||||
}
|
||||
},
|
||||
NotifyBrowseToURL: (url) => {
|
||||
console.log('Browse to URL:', url);
|
||||
},
|
||||
NotifyPanicRecover: (message) => {
|
||||
console.error('Panic recover:', message);
|
||||
},
|
||||
});
|
||||
pause();
|
||||
const go = new Go(); // Go is defined by wasm_exec.js
|
||||
WebAssembly.instantiateStreaming(fetch("/hp_ssh.wasm"), go.importObject).then((value) => {
|
||||
go.run(value.instance);
|
||||
const handle = TsWasmNet(ipnDetails, {
|
||||
NotifyState: (state) => {
|
||||
console.log("State changed:", state);
|
||||
if (state === "Running") {
|
||||
setIpn(handle);
|
||||
}
|
||||
},
|
||||
NotifyNetMap: (netmap) => {
|
||||
// Only set NodeKey if it is not already set and then
|
||||
// also dispatch that to the backend to track the
|
||||
// ephemeral node.
|
||||
//
|
||||
// We open an SSE connection to the backend
|
||||
// so that when the connection is closed,
|
||||
// the backend can delete the ephemeral node.
|
||||
if (nodeKey === null) {
|
||||
setNodeKey(netmap.NodeKey);
|
||||
submit(
|
||||
{
|
||||
node_key: netmap.NodeKey,
|
||||
auth_key: ipnDetails.PreAuthKey,
|
||||
},
|
||||
{ method: "POST" },
|
||||
);
|
||||
}
|
||||
},
|
||||
NotifyBrowseToURL: (url) => {
|
||||
console.log("Browse to URL:", url);
|
||||
},
|
||||
NotifyPanicRecover: (message) => {
|
||||
console.error("Panic recover:", message);
|
||||
},
|
||||
});
|
||||
|
||||
handle.Start();
|
||||
});
|
||||
}, []);
|
||||
handle.Start();
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (!sshDetails.username) {
|
||||
return <UserPrompt hostname={sshDetails.hostname} />;
|
||||
}
|
||||
if (!sshDetails.username) {
|
||||
return <UserPrompt hostname={sshDetails.hostname} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-screen h-screen bg-headplane-900">
|
||||
{ipn === null ? (
|
||||
<div className="mx-auto h-screen flex items-center justify-center">
|
||||
<Loader2 className="animate-spin size-10 text-headplane-50" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col h-screen">
|
||||
<XTerm
|
||||
hostname={sshDetails.hostname}
|
||||
ipn={ipn}
|
||||
username={sshDetails.username}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="bg-headplane-900 h-screen w-screen">
|
||||
{ipn === null ? (
|
||||
<div className="mx-auto flex h-screen items-center justify-center">
|
||||
<Loader2 className="text-headplane-50 size-10 animate-spin" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-screen flex-col">
|
||||
<XTerm hostname={sshDetails.hostname} ipn={ipn} username={sshDetails.username} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+71
-77
@@ -4,115 +4,109 @@
|
||||
@plugin "tailwindcss-react-aria-components";
|
||||
|
||||
@theme {
|
||||
--blur-xs: 2px;
|
||||
--blur-xs: 2px;
|
||||
|
||||
--height-editor: calc(100vh - 20rem);
|
||||
--height-editor: calc(100vh - 20rem);
|
||||
|
||||
--font-sans: Inter, -apple-system, BlinkMacSystemFont, Helvetica, Arial,
|
||||
sans-serif;
|
||||
--font-sans: Inter, -apple-system, BlinkMacSystemFont, Helvetica, Arial, sans-serif;
|
||||
|
||||
--transition-duration-25: 25ms;
|
||||
--transition-duration-50: 50ms;
|
||||
--transition-duration-25: 25ms;
|
||||
--transition-duration-50: 50ms;
|
||||
|
||||
--color-main-50: #f8fafc;
|
||||
--color-main-100: #f1f5f9;
|
||||
--color-main-200: #e2e8f0;
|
||||
--color-main-300: #cbd5e1;
|
||||
--color-main-400: #94a3b8;
|
||||
--color-main-500: #64748b;
|
||||
--color-main-600: #475569;
|
||||
--color-main-700: #334155;
|
||||
--color-main-800: #1e293b;
|
||||
--color-main-900: #0f172a;
|
||||
--color-main-950: #020617;
|
||||
--color-main-50: #f8fafc;
|
||||
--color-main-100: #f1f5f9;
|
||||
--color-main-200: #e2e8f0;
|
||||
--color-main-300: #cbd5e1;
|
||||
--color-main-400: #94a3b8;
|
||||
--color-main-500: #64748b;
|
||||
--color-main-600: #475569;
|
||||
--color-main-700: #334155;
|
||||
--color-main-800: #1e293b;
|
||||
--color-main-900: #0f172a;
|
||||
--color-main-950: #020617;
|
||||
|
||||
--color-ui-50: #fafafa;
|
||||
--color-ui-100: #f5f5f5;
|
||||
--color-ui-200: #e5e5e5;
|
||||
--color-ui-300: #d4d4d4;
|
||||
--color-ui-400: #a3a3a3;
|
||||
--color-ui-500: #737373;
|
||||
--color-ui-600: #525252;
|
||||
--color-ui-700: #404040;
|
||||
--color-ui-800: #262626;
|
||||
--color-ui-900: #171717;
|
||||
--color-ui-950: #0a0a0a;
|
||||
--color-ui-50: #fafafa;
|
||||
--color-ui-100: #f5f5f5;
|
||||
--color-ui-200: #e5e5e5;
|
||||
--color-ui-300: #d4d4d4;
|
||||
--color-ui-400: #a3a3a3;
|
||||
--color-ui-500: #737373;
|
||||
--color-ui-600: #525252;
|
||||
--color-ui-700: #404040;
|
||||
--color-ui-800: #262626;
|
||||
--color-ui-900: #171717;
|
||||
--color-ui-950: #0a0a0a;
|
||||
|
||||
--color-headplane-50: #f2f2f2;
|
||||
--color-headplane-100: #e6e6e6;
|
||||
--color-headplane-200: #cccccc;
|
||||
--color-headplane-300: #b3b3b3;
|
||||
--color-headplane-400: #999999;
|
||||
--color-headplane-500: #808080;
|
||||
--color-headplane-600: #666666;
|
||||
--color-headplane-700: #4d4d4d;
|
||||
--color-headplane-800: #343434;
|
||||
--color-headplane-900: #1a1a1a;
|
||||
--color-headplane-950: #0d0d0d;
|
||||
--color-headplane-50: #f2f2f2;
|
||||
--color-headplane-100: #e6e6e6;
|
||||
--color-headplane-200: #cccccc;
|
||||
--color-headplane-300: #b3b3b3;
|
||||
--color-headplane-400: #999999;
|
||||
--color-headplane-500: #808080;
|
||||
--color-headplane-600: #666666;
|
||||
--color-headplane-700: #4d4d4d;
|
||||
--color-headplane-800: #343434;
|
||||
--color-headplane-900: #1a1a1a;
|
||||
--color-headplane-950: #0d0d0d;
|
||||
|
||||
--animate-loading: loader 0.8s infinite ease-in-out;
|
||||
--animate-loading: loader 0.8s infinite ease-in-out;
|
||||
|
||||
@keyframes loader {
|
||||
from {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
to {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
@keyframes loader {
|
||||
from {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
to {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@utility container {
|
||||
margin-inline: auto;
|
||||
padding-inline: 1rem;
|
||||
width: 100%;
|
||||
max-width: 96rem; /* 1536px - constrain on large displays */
|
||||
margin-inline: auto;
|
||||
padding-inline: 1rem;
|
||||
width: 100%;
|
||||
max-width: 96rem; /* 1536px - constrain on large displays */
|
||||
|
||||
/* biome-ignore lint/correctness/noUnknownFunction: Tailwind CSS */
|
||||
@media (width >= theme(--breakpoint-sm)) {
|
||||
padding-inline: 2rem;
|
||||
}
|
||||
@media (width >= theme(--breakpoint-sm)) {
|
||||
padding-inline: 2rem;
|
||||
}
|
||||
@media (width >= theme(--breakpoint-lg)) {
|
||||
padding-inline: 4rem;
|
||||
}
|
||||
|
||||
/* biome-ignore lint/correctness/noUnknownFunction: Tailwind CSS */
|
||||
@media (width >= theme(--breakpoint-lg)) {
|
||||
padding-inline: 4rem;
|
||||
}
|
||||
@media (width >= theme(--breakpoint-xl)) {
|
||||
padding-inline: 5rem;
|
||||
}
|
||||
|
||||
/* biome-ignore lint/correctness/noUnknownFunction: Tailwind CSS */
|
||||
@media (width >= theme(--breakpoint-xl)) {
|
||||
padding-inline: 5rem;
|
||||
}
|
||||
|
||||
/* biome-ignore lint/correctness/noUnknownFunction: Tailwind CSS */
|
||||
@media (width >= theme(--breakpoint-2xl)) {
|
||||
padding-inline: 6rem;
|
||||
}
|
||||
@media (width >= theme(--breakpoint-2xl)) {
|
||||
padding-inline: 6rem;
|
||||
}
|
||||
}
|
||||
|
||||
@supports (scrollbar-gutter: stable) {
|
||||
html {
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
html {
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
}
|
||||
|
||||
.cm-merge-theme {
|
||||
height: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
.cm-mergeView {
|
||||
height: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
.cm-mergeViewEditors {
|
||||
height: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.cm-mergeViewEditor {
|
||||
height: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
/* Weirdest class name characters but ok */
|
||||
.cm-mergeView .ͼ1 .cm-scroller,
|
||||
.cm-mergeView .ͼ1 {
|
||||
height: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
-48
@@ -1,48 +0,0 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.2.0/schema.json",
|
||||
"vcs": {
|
||||
"enabled": false,
|
||||
"clientKind": "git",
|
||||
"useIgnoreFile": true
|
||||
},
|
||||
"files": {
|
||||
"ignoreUnknown": false,
|
||||
"includes": ["**", "!app/wasm_exec.js"]
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"indentStyle": "tab",
|
||||
"lineWidth": 80,
|
||||
"lineEnding": "lf"
|
||||
},
|
||||
"assist": {
|
||||
"actions": {
|
||||
"source": {
|
||||
"organizeImports": "on",
|
||||
"useSortedAttributes": "on"
|
||||
}
|
||||
}
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true,
|
||||
"style": {
|
||||
"useImportType": "off",
|
||||
"noNonNullAssertion": "off"
|
||||
},
|
||||
"correctness": {
|
||||
"useExhaustiveDependencies": "off"
|
||||
},
|
||||
"suspicious": {
|
||||
"noExplicitAny": "warn"
|
||||
}
|
||||
}
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"quoteStyle": "single",
|
||||
"jsxQuoteStyle": "double"
|
||||
}
|
||||
}
|
||||
}
|
||||
+24
-20
@@ -6,39 +6,42 @@ sole maintainer, I have a few guidelines which help make it significantly easier
|
||||
for me to review and merge your contributions.
|
||||
|
||||
## Contribution Types
|
||||
|
||||
- **Bug Reports/Feature Requests**: If you find a bug, please open an
|
||||
[issue](https://github.com/tale/headplane/issues) using one of the predefined
|
||||
templates. Issues are only for bug reports and feature requests, not problems
|
||||
that you encounter from misconfiguration or usage. Those belong in the
|
||||
[discussions](https://github.com/tale/headplane/discussions) section.
|
||||
[issue](https://github.com/tale/headplane/issues) using one of the predefined
|
||||
templates. Issues are only for bug reports and feature requests, not problems
|
||||
that you encounter from misconfiguration or usage. Those belong in the
|
||||
[discussions](https://github.com/tale/headplane/discussions) section.
|
||||
|
||||
- **Documentation/Examples**: If you find any issues in the documentation or
|
||||
would like to contribute examples for setting up Headplane, please open a PR
|
||||
and I will review it and possibly make changes.
|
||||
would like to contribute examples for setting up Headplane, please open a PR
|
||||
and I will review it and possibly make changes.
|
||||
|
||||
- **Code Contributions**: Code contributions are done via PRs but *must* be
|
||||
linked to an issue or a feature request (you can make one if it doesn't exist).
|
||||
Please tag them with the appropriate labels when opening an issue or PR.
|
||||
- **Code Contributions**: Code contributions are done via PRs but _must_ be
|
||||
linked to an issue or a feature request (you can make one if it doesn't exist).
|
||||
Please tag them with the appropriate labels when opening an issue or PR.
|
||||
|
||||
### Code Contribution Restrictions
|
||||
|
||||
- **No Large Refactors**: I am not interested in large refactors of the codebase
|
||||
because I've already had do to this a lot and it becomes a headache to review,
|
||||
maintain, and merge. This also means you should split up multiple contributions
|
||||
into multiple PRs if they are unrelated to each other.
|
||||
because I've already had do to this a lot and it becomes a headache to review,
|
||||
maintain, and merge. This also means you should split up multiple contributions
|
||||
into multiple PRs if they are unrelated to each other.
|
||||
|
||||
- **No Project/Tooling Changes**: Unless there is a very good reason to do so,
|
||||
I will not accept changes to the project structure, build system, or tooling
|
||||
used to develop Headplane. This includes things like changing the package
|
||||
manager, docker environment, or CI/CD.
|
||||
I will not accept changes to the project structure, build system, or tooling
|
||||
used to develop Headplane. This includes things like changing the package
|
||||
manager, docker environment, or CI/CD.
|
||||
|
||||
- **Minimal Breaking Changes**: I will not accept any changes that break any
|
||||
existing functionality or change the API unless there is a very good reason
|
||||
to do so. If you want to make a breaking change, please open an issue first
|
||||
and discuss it with me.
|
||||
existing functionality or change the API unless there is a very good reason
|
||||
to do so. If you want to make a breaking change, please open an issue first
|
||||
and discuss it with me.
|
||||
|
||||
### Code Style
|
||||
This is very easy and self-explanatory. [Biome](https://biomejs.dev) is used as
|
||||
a linter and formatter for the TypeScript, while Go's default formatting and
|
||||
|
||||
This is very easy and self-explanatory. [oxlint/oxfmt](https://oxc.rs) is used
|
||||
as a linter and formatter for the TypeScript, while Go's default formatting and
|
||||
lint tooling are used within `agent/` code. I've setup a git hook to run
|
||||
before commit to make these changes automatically.
|
||||
|
||||
@@ -47,6 +50,7 @@ before commit to make these changes automatically.
|
||||
> but instead we can discuss them and see if we can come to a compromise.
|
||||
|
||||
### Contribution Tooling
|
||||
|
||||
If you plan to work with the WASM SSH agent, you will need to install
|
||||
`mkcert` or an equivalent to create certificates in `./test/caddy/certs`.
|
||||
It expects a `localhost.pem` and `localhost-key.pem` file to be present in that
|
||||
|
||||
+6
-2
@@ -1,8 +1,12 @@
|
||||
pre-commit:
|
||||
commands:
|
||||
check:
|
||||
lint:
|
||||
glob: "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}"
|
||||
run: pnpm biome check --write --no-errors-on-unmatched --files-ignore-unknown=true --colors=off {staged_files}
|
||||
run: pnpm lint --fix {staged_files}
|
||||
stage_fixed: true
|
||||
format:
|
||||
glob: "*"
|
||||
run: pnpm format {staged_files}
|
||||
stage_fixed: true
|
||||
check_go:
|
||||
glob: "*.go"
|
||||
|
||||
+120
-117
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+244
-91
@@ -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: {}
|
||||
|
||||
@@ -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",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user