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
|
|
@ -66,6 +66,8 @@ vi.mock("../config.js", () => ({
|
|||
loadConfig: vi.fn(() => ({
|
||||
deploymentMode: "authenticated",
|
||||
deploymentExposure: "private",
|
||||
bind: "loopback",
|
||||
customBindHost: undefined,
|
||||
host: "127.0.0.1",
|
||||
port: 3210,
|
||||
allowedHostnames: [],
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { readConfigFile } from "./config-file.js";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { existsSync, realpathSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { config as loadDotenv } from "dotenv";
|
||||
|
|
@ -6,15 +7,20 @@ import { resolvePaperclipEnvPath } from "./paths.js";
|
|||
import { maybeRepairLegacyWorktreeConfigAndEnvFiles } from "./worktree-config.js";
|
||||
import {
|
||||
AUTH_BASE_URL_MODES,
|
||||
BIND_MODES,
|
||||
DEPLOYMENT_EXPOSURES,
|
||||
DEPLOYMENT_MODES,
|
||||
SECRET_PROVIDERS,
|
||||
STORAGE_PROVIDERS,
|
||||
type BindMode,
|
||||
type AuthBaseUrlMode,
|
||||
type DeploymentExposure,
|
||||
type DeploymentMode,
|
||||
type SecretProvider,
|
||||
type StorageProvider,
|
||||
inferBindModeFromHost,
|
||||
resolveRuntimeBind,
|
||||
validateConfiguredBindMode,
|
||||
} from "@paperclipai/shared";
|
||||
import {
|
||||
resolveDefaultBackupDir,
|
||||
|
|
@ -44,6 +50,8 @@ type DatabaseMode = "embedded-postgres" | "postgres";
|
|||
export interface Config {
|
||||
deploymentMode: DeploymentMode;
|
||||
deploymentExposure: DeploymentExposure;
|
||||
bind: BindMode;
|
||||
customBindHost: string | undefined;
|
||||
host: string;
|
||||
port: number;
|
||||
allowedHostnames: string[];
|
||||
|
|
@ -78,6 +86,24 @@ export interface Config {
|
|||
telemetryEnabled: boolean;
|
||||
}
|
||||
|
||||
function detectTailnetBindHost(): string | undefined {
|
||||
const explicit = process.env.PAPERCLIP_TAILNET_BIND_HOST?.trim();
|
||||
if (explicit) return explicit;
|
||||
|
||||
try {
|
||||
const stdout = execFileSync("tailscale", ["ip", "-4"], {
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "ignore"],
|
||||
});
|
||||
return stdout
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.find(Boolean);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function loadConfig(): Config {
|
||||
const fileConfig = readConfigFile();
|
||||
const fileDatabaseMode =
|
||||
|
|
@ -148,6 +174,18 @@ export function loadConfig(): Config {
|
|||
deploymentMode === "local_trusted"
|
||||
? "private"
|
||||
: (deploymentExposureFromEnv ?? fileConfig?.server.exposure ?? "private");
|
||||
const bindFromEnvRaw = process.env.PAPERCLIP_BIND;
|
||||
const bindFromEnv =
|
||||
bindFromEnvRaw && BIND_MODES.includes(bindFromEnvRaw as BindMode)
|
||||
? (bindFromEnvRaw as BindMode)
|
||||
: null;
|
||||
const configuredHost = process.env.HOST ?? fileConfig?.server.host ?? "127.0.0.1";
|
||||
const tailnetBindHost = detectTailnetBindHost();
|
||||
const bind =
|
||||
bindFromEnv ??
|
||||
fileConfig?.server.bind ??
|
||||
inferBindModeFromHost(configuredHost, { tailnetBindHost });
|
||||
const customBindHost = process.env.PAPERCLIP_BIND_HOST ?? fileConfig?.server.customBindHost;
|
||||
const authBaseUrlModeFromEnvRaw = process.env.PAPERCLIP_AUTH_BASE_URL_MODE;
|
||||
const authBaseUrlModeFromEnv =
|
||||
authBaseUrlModeFromEnvRaw &&
|
||||
|
|
@ -223,11 +261,32 @@ export function loadConfig(): Config {
|
|||
fileDatabaseBackup?.dir ??
|
||||
resolveDefaultBackupDir(),
|
||||
);
|
||||
const bindValidationErrors = validateConfiguredBindMode({
|
||||
deploymentMode,
|
||||
deploymentExposure,
|
||||
bind,
|
||||
host: configuredHost,
|
||||
customBindHost,
|
||||
});
|
||||
if (bindValidationErrors.length > 0) {
|
||||
throw new Error(bindValidationErrors[0]);
|
||||
}
|
||||
const resolvedBind = resolveRuntimeBind({
|
||||
bind,
|
||||
host: configuredHost,
|
||||
customBindHost,
|
||||
tailnetBindHost,
|
||||
});
|
||||
if (resolvedBind.errors.length > 0) {
|
||||
throw new Error(resolvedBind.errors[0]);
|
||||
}
|
||||
|
||||
return {
|
||||
deploymentMode,
|
||||
deploymentExposure,
|
||||
host: process.env.HOST ?? fileConfig?.server.host ?? "127.0.0.1",
|
||||
bind: resolvedBind.bind,
|
||||
customBindHost: resolvedBind.customBindHost,
|
||||
host: resolvedBind.host,
|
||||
port: Number(process.env.PORT) || fileConfig?.server.port || 3100,
|
||||
allowedHostnames,
|
||||
authBaseUrlMode,
|
||||
|
|
|
|||
|
|
@ -701,9 +701,10 @@ export async function startServer(): Promise<StartedServer> {
|
|||
logger.warn({ err, url }, "Failed to open browser on startup");
|
||||
});
|
||||
}
|
||||
printStartupBanner({
|
||||
host: config.host,
|
||||
deploymentMode: config.deploymentMode,
|
||||
printStartupBanner({
|
||||
bind: config.bind,
|
||||
host: config.host,
|
||||
deploymentMode: config.deploymentMode,
|
||||
deploymentExposure: config.deploymentExposure,
|
||||
authReady,
|
||||
requestedPort: config.port,
|
||||
|
|
|
|||
|
|
@ -928,7 +928,7 @@ function buildOnboardingDiscoveryDiagnostics(input: {
|
|||
code: "openclaw_onboarding_private_loopback_bind",
|
||||
level: "warn",
|
||||
message: "Paperclip is bound to loopback in authenticated/private mode.",
|
||||
hint: "Run with a reachable bind host or use pnpm dev --tailscale-auth for private-network onboarding."
|
||||
hint: "Use a reachable private bind mode such as `pnpm dev --bind lan` or `pnpm dev --bind tailnet` for private-network onboarding."
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { resolvePaperclipConfigPath, resolvePaperclipEnvPath } from "./paths.js";
|
||||
import type { DeploymentExposure, DeploymentMode } from "@paperclipai/shared";
|
||||
import type { BindMode, DeploymentExposure, DeploymentMode } from "@paperclipai/shared";
|
||||
|
||||
import { parse as parseEnvFileContents } from "dotenv";
|
||||
|
||||
|
|
@ -18,6 +18,7 @@ type EmbeddedPostgresInfo = {
|
|||
};
|
||||
|
||||
type StartupBannerOptions = {
|
||||
bind: BindMode;
|
||||
host: string;
|
||||
deploymentMode: DeploymentMode;
|
||||
deploymentExposure: DeploymentExposure;
|
||||
|
|
@ -148,6 +149,7 @@ export function printStartupBanner(opts: StartupBannerOptions): void {
|
|||
color(" ───────────────────────────────────────────────────────", "blue"),
|
||||
row("Mode", `${dbMode} | ${uiMode}`),
|
||||
row("Deploy", `${opts.deploymentMode} (${opts.deploymentExposure})`),
|
||||
row("Bind", `${opts.bind} ${color(`(${opts.host})`, "dim")}`),
|
||||
row("Auth", opts.authReady ? color("ready", "green") : color("not-ready", "yellow")),
|
||||
row("Server", portValue),
|
||||
row("API", `${apiUrl} ${color(`(health: ${apiUrl}/health)`, "dim")}`),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue