mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-14 01:50:39 +09:00
Introduce bind presets for deployment setup
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
e1bf9d66a7
commit
2a84e53c1b
35 changed files with 915 additions and 176 deletions
|
|
@ -1,11 +1,13 @@
|
|||
import { z } from "zod";
|
||||
import {
|
||||
AUTH_BASE_URL_MODES,
|
||||
BIND_MODES,
|
||||
DEPLOYMENT_EXPOSURES,
|
||||
DEPLOYMENT_MODES,
|
||||
SECRET_PROVIDERS,
|
||||
STORAGE_PROVIDERS,
|
||||
} from "./constants.js";
|
||||
import { validateConfiguredBindMode } from "./network-bind.js";
|
||||
|
||||
export const configMetaSchema = z.object({
|
||||
version: z.literal(1),
|
||||
|
|
@ -46,6 +48,8 @@ export const loggingConfigSchema = z.object({
|
|||
export const serverConfigSchema = z.object({
|
||||
deploymentMode: z.enum(DEPLOYMENT_MODES).default("local_trusted"),
|
||||
exposure: z.enum(DEPLOYMENT_EXPOSURES).default("private"),
|
||||
bind: z.enum(BIND_MODES).optional(),
|
||||
customBindHost: z.string().optional(),
|
||||
host: z.string().default("127.0.0.1"),
|
||||
port: z.number().int().min(1).max(65535).default(3100),
|
||||
allowedHostnames: z.array(z.string().min(1)).default([]),
|
||||
|
|
@ -132,15 +136,26 @@ export const paperclipConfigSchema = z
|
|||
}),
|
||||
})
|
||||
.superRefine((value, ctx) => {
|
||||
if (value.server.deploymentMode === "local_trusted") {
|
||||
if (value.server.exposure !== "private") {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "server.exposure must be private when deploymentMode is local_trusted",
|
||||
path: ["server", "exposure"],
|
||||
});
|
||||
}
|
||||
return;
|
||||
if (value.server.deploymentMode === "local_trusted" && value.server.exposure !== "private") {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "server.exposure must be private when deploymentMode is local_trusted",
|
||||
path: ["server", "exposure"],
|
||||
});
|
||||
}
|
||||
|
||||
for (const message of validateConfiguredBindMode({
|
||||
deploymentMode: value.server.deploymentMode,
|
||||
deploymentExposure: value.server.exposure,
|
||||
bind: value.server.bind,
|
||||
host: value.server.host,
|
||||
customBindHost: value.server.customBindHost,
|
||||
})) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message,
|
||||
path: message.includes("customBindHost") ? ["server", "customBindHost"] : ["server", "bind"],
|
||||
});
|
||||
}
|
||||
|
||||
if (value.auth.baseUrlMode === "explicit" && !value.auth.publicBaseUrl) {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@ export type DeploymentMode = (typeof DEPLOYMENT_MODES)[number];
|
|||
export const DEPLOYMENT_EXPOSURES = ["private", "public"] as const;
|
||||
export type DeploymentExposure = (typeof DEPLOYMENT_EXPOSURES)[number];
|
||||
|
||||
export const BIND_MODES = ["loopback", "lan", "tailnet", "custom"] as const;
|
||||
export type BindMode = (typeof BIND_MODES)[number];
|
||||
|
||||
export const AUTH_BASE_URL_MODES = ["auto", "explicit"] as const;
|
||||
export type AuthBaseUrlMode = (typeof AUTH_BASE_URL_MODES)[number];
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ export {
|
|||
COMPANY_STATUSES,
|
||||
DEPLOYMENT_MODES,
|
||||
DEPLOYMENT_EXPOSURES,
|
||||
BIND_MODES,
|
||||
AUTH_BASE_URL_MODES,
|
||||
AGENT_STATUSES,
|
||||
AGENT_ADAPTER_TYPES,
|
||||
|
|
@ -79,6 +80,7 @@ export {
|
|||
type CompanyStatus,
|
||||
type DeploymentMode,
|
||||
type DeploymentExposure,
|
||||
type BindMode,
|
||||
type AuthBaseUrlMode,
|
||||
type AgentStatus,
|
||||
type AgentAdapterType,
|
||||
|
|
@ -149,6 +151,16 @@ export {
|
|||
type PluginBridgeErrorCode,
|
||||
} from "./constants.js";
|
||||
|
||||
export {
|
||||
ALL_INTERFACES_BIND_HOST,
|
||||
LOOPBACK_BIND_HOST,
|
||||
inferBindModeFromHost,
|
||||
isAllInterfacesHost,
|
||||
isLoopbackHost,
|
||||
resolveRuntimeBind,
|
||||
validateConfiguredBindMode,
|
||||
} from "./network-bind.js";
|
||||
|
||||
export type {
|
||||
Company,
|
||||
FeedbackVote,
|
||||
|
|
|
|||
105
packages/shared/src/network-bind.ts
Normal file
105
packages/shared/src/network-bind.ts
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
import type { BindMode, DeploymentExposure, DeploymentMode } from "./constants.js";
|
||||
|
||||
export const LOOPBACK_BIND_HOST = "127.0.0.1";
|
||||
export const ALL_INTERFACES_BIND_HOST = "0.0.0.0";
|
||||
|
||||
function normalizeHost(host: string | null | undefined): string | undefined {
|
||||
const trimmed = host?.trim();
|
||||
return trimmed ? trimmed : undefined;
|
||||
}
|
||||
|
||||
export function isLoopbackHost(host: string | null | undefined): boolean {
|
||||
const normalized = normalizeHost(host)?.toLowerCase();
|
||||
return normalized === "127.0.0.1" || normalized === "localhost" || normalized === "::1";
|
||||
}
|
||||
|
||||
export function isAllInterfacesHost(host: string | null | undefined): boolean {
|
||||
const normalized = normalizeHost(host)?.toLowerCase();
|
||||
return normalized === "0.0.0.0" || normalized === "::";
|
||||
}
|
||||
|
||||
export function inferBindModeFromHost(
|
||||
host: string | null | undefined,
|
||||
opts?: { tailnetBindHost?: string | null | undefined },
|
||||
): BindMode {
|
||||
const normalized = normalizeHost(host);
|
||||
const tailnetBindHost = normalizeHost(opts?.tailnetBindHost);
|
||||
|
||||
if (!normalized || isLoopbackHost(normalized)) return "loopback";
|
||||
if (isAllInterfacesHost(normalized)) return "lan";
|
||||
if (tailnetBindHost && normalized === tailnetBindHost) return "tailnet";
|
||||
return "custom";
|
||||
}
|
||||
|
||||
export function validateConfiguredBindMode(input: {
|
||||
deploymentMode: DeploymentMode;
|
||||
deploymentExposure: DeploymentExposure;
|
||||
bind?: BindMode | null | undefined;
|
||||
host?: string | null | undefined;
|
||||
customBindHost?: string | null | undefined;
|
||||
}): string[] {
|
||||
const bind = input.bind ?? inferBindModeFromHost(input.host);
|
||||
const customBindHost = normalizeHost(input.customBindHost);
|
||||
const errors: string[] = [];
|
||||
|
||||
if (input.deploymentMode === "local_trusted" && bind !== "loopback") {
|
||||
errors.push("local_trusted requires server.bind=loopback");
|
||||
}
|
||||
|
||||
if (bind === "custom" && !customBindHost) {
|
||||
const legacyHost = normalizeHost(input.host);
|
||||
if (!legacyHost || isLoopbackHost(legacyHost) || isAllInterfacesHost(legacyHost)) {
|
||||
errors.push("server.customBindHost is required when server.bind=custom");
|
||||
}
|
||||
}
|
||||
|
||||
if (input.deploymentMode === "authenticated" && input.deploymentExposure === "public" && bind === "tailnet") {
|
||||
errors.push("server.bind=tailnet is only supported for authenticated/private deployments");
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
export function resolveRuntimeBind(input: {
|
||||
bind?: BindMode | null | undefined;
|
||||
host?: string | null | undefined;
|
||||
customBindHost?: string | null | undefined;
|
||||
tailnetBindHost?: string | null | undefined;
|
||||
}): {
|
||||
bind: BindMode;
|
||||
host: string;
|
||||
customBindHost?: string;
|
||||
errors: string[];
|
||||
} {
|
||||
const bind = input.bind ?? inferBindModeFromHost(input.host, { tailnetBindHost: input.tailnetBindHost });
|
||||
const legacyHost = normalizeHost(input.host);
|
||||
const customBindHost =
|
||||
normalizeHost(input.customBindHost) ??
|
||||
(bind === "custom" && legacyHost && !isLoopbackHost(legacyHost) && !isAllInterfacesHost(legacyHost)
|
||||
? legacyHost
|
||||
: undefined);
|
||||
|
||||
switch (bind) {
|
||||
case "loopback":
|
||||
return { bind, host: LOOPBACK_BIND_HOST, customBindHost, errors: [] };
|
||||
case "lan":
|
||||
return { bind, host: ALL_INTERFACES_BIND_HOST, customBindHost, errors: [] };
|
||||
case "custom":
|
||||
return customBindHost
|
||||
? { bind, host: customBindHost, customBindHost, errors: [] }
|
||||
: { bind, host: legacyHost ?? LOOPBACK_BIND_HOST, errors: ["server.customBindHost is required when server.bind=custom"] };
|
||||
case "tailnet": {
|
||||
const tailnetBindHost = normalizeHost(input.tailnetBindHost);
|
||||
return tailnetBindHost
|
||||
? { bind, host: tailnetBindHost, customBindHost, errors: [] }
|
||||
: {
|
||||
bind,
|
||||
host: legacyHost ?? LOOPBACK_BIND_HOST,
|
||||
customBindHost,
|
||||
errors: [
|
||||
"server.bind=tailnet requires a detected Tailscale address or PAPERCLIP_TAILNET_BIND_HOST",
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue